Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
eb45d17
fix(axis): fix axis label may have inappropriate precision or take to…
plainheart Dec 17, 2025
4cf7cb4
test(dataZoom): fix test case -- accidentally broken in v6. And add m…
100pah Jan 8, 2026
d013d5e
timeScale: Add the missing rounding (broken from a previous version) …
100pah Jan 8, 2026
479dcd4
fix&feat: Change and clarify the rounding error and auto-precision ut…
100pah Jan 9, 2026
d168bf2
fix(axisTick&dataZoom): (1) Apply a better auto-precision method. (2)…
100pah Jan 9, 2026
ffcc636
fix(alignTicks): Change alignTick strategy: (1) Previously some serie…
100pah Jan 9, 2026
a6ab245
feat(alignTicks): (1) Fix LogScale precision. (2) Tweak align ticks l…
100pah Jan 14, 2026
18a23a8
refactor(scale): For readability and maintainability (1) Migrate `cal…
100pah Jan 18, 2026
0f4561d
refactor&fix: (1) Unify series data union code - remove union code fr…
100pah Jan 24, 2026
dedc5dc
fix(logScale): (1) Thoroughly resolve a long-standing issue of non-po…
100pah Jan 25, 2026
28e74ef
fix(pie-on-cartesian): Previously when pie is located on Cartesian, a…
100pah Jan 25, 2026
64305a4
fix(dataZoom): (1) Fix dataZoom dragging cursor style (use 'drag' and…
100pah Jan 25, 2026
fe932a2
refactor&feature:
100pah Feb 17, 2026
1f74fd7
test(visual): Update markers.
100pah Feb 17, 2026
c4a3823
Merge branch 'release' into pr/plainheart_fix/alignTicks-precision
100pah Feb 17, 2026
bdec91e
refactor: Remove the default value of number round due to its unreaso…
100pah Feb 17, 2026
52ceb92
fix: (1) Fix dataZoom AxisProxy can not be cleared when dataZoom opti…
100pah Feb 18, 2026
d47ea4a
fix(dataZoom): Do not display values outside of effective extent.
100pah Feb 19, 2026
b16d96c
chore: Tweak the usage of isFinite.
100pah Feb 20, 2026
8de2b64
feature&fix(axis):
100pah Feb 27, 2026
7a8d38b
fix: Fix inappropriate impl introduced by the previous commits.
100pah Feb 27, 2026
dbfaf6a
fix&feature:
100pah Mar 6, 2026
18bedbb
fix: Temporarily fix incorrect stack base dimension selection when bo…
100pah Mar 6, 2026
bb56e9a
chore: Sync the modification of #21448 to release.
100pah Mar 6, 2026
bd19110
fix: (1) Clarify and uniform terminology and add comments to explain …
100pah Mar 7, 2026
eb7530b
fix(appendData): Fix that the dataZoom inside is disabled when append…
100pah Mar 8, 2026
d2cc085
tweak: Clarity the previous implements of axis statistics.
100pah Mar 9, 2026
12b6d9f
Merge branch 'release' into PR/plainheart_fix/alignTicks-precision
100pah Mar 9, 2026
b094f98
fix(toolbox): Fix that toolbox theme cause corresponding icons are al…
100pah Mar 10, 2026
6de824d
fix(toolbox): Simplify toolbox and fix that toolbox throw error when …
100pah Mar 10, 2026
abb3ad4
fix: fix previous commit.
100pah Mar 12, 2026
9335851
fix: (1) Previously hoverLayerThreshold modification does not work. (…
100pah Mar 15, 2026
56a32c0
fix(axisPointer&tooltip): (1) axisPointer and tooltip should be able …
100pah Mar 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/chart/bar/BarView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,15 @@ type RealtimeSortConfig = {
// Return a number, based on which the ordinal sorted.
type OrderMapping = (dataIndex: number) => number;

function getClipArea(coord: CoordSysOfBar, data: SeriesData, strictClip?: boolean) {
function getClipArea(coord: CoordSysOfBar, data: SeriesData) {
const coordSysClipArea = coord.getArea && coord.getArea();
if (isCoordinateSystemType<Cartesian2D>(coord, 'cartesian2d')) {
const baseAxis = coord.getBaseAxis();
// When boundaryGap is false or using time axis. bar may exceed the grid.
// When boundaryGap is false in category axis, bar may exceed the grid.
// We should not clip this part.
// See test/bar2.html
if (!strictClip && (baseAxis.type !== 'category' || !baseAxis.onBand)) {
// PENDING: The effect is not preferable, but we preserve it for backward compatibility.
if (baseAxis.type === 'category' && !baseAxis.onBand) {
const expandWidth = data.getLayout('bandWidth');
if (baseAxis.isHorizontal()) {
(coordSysClipArea as CartesianCoordArea).x -= expandWidth;
Expand Down Expand Up @@ -220,12 +221,12 @@ class BarView extends ChartView {
}

const needsClip = seriesModel.get('clip', true) || realtimeSortCfg;
const strictClip = seriesModel.get('clip', true);
const coordSysClipArea = getClipArea(coord, data, strictClip);
const coordSysClipArea = getClipArea(coord, data);
// If there is clipPath created in large mode. Remove it.
group.removeClipPath();
// We don't use clipPath in normal mode because we needs a perfect animation
// And don't want the label are clipped.
// Instead, `Clipper` is used in normal mode.

const roundCap = seriesModel.get('roundCap', true);

Expand Down
3 changes: 2 additions & 1 deletion src/chart/bar/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import { EChartsExtensionInstallRegisters } from '../../extension';
import * as zrUtil from 'zrender/src/core/util';
import {layout, createProgressiveLayout} from '../../layout/barGrid';
import {layout, createProgressiveLayout, registerBarGridAxisHandlers} from '../../layout/barGrid';
import dataSample from '../../processor/dataSample';

import BarSeries from './BarSeries';
Expand Down Expand Up @@ -67,4 +67,5 @@ export function install(registers: EChartsExtensionInstallRegisters) {
);
});

registerBarGridAxisHandlers(registers);
}
4 changes: 3 additions & 1 deletion src/chart/bar/installPictorialBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import { EChartsExtensionInstallRegisters } from '../../extension';
import PictorialBarView from './PictorialBarView';
import PictorialBarSeriesModel from './PictorialBarSeries';
import { createProgressiveLayout, layout } from '../../layout/barGrid';
import { createProgressiveLayout, layout, registerBarGridAxisHandlers } from '../../layout/barGrid';
import { curry } from 'zrender/src/core/util';

export function install(registers: EChartsExtensionInstallRegisters) {
Expand All @@ -30,4 +30,6 @@ export function install(registers: EChartsExtensionInstallRegisters) {
registers.registerLayout(registers.PRIORITY.VISUAL.LAYOUT, curry(layout, 'pictorialBar'));
// Do layout after other overall layout, which can prepare some information.
registers.registerLayout(registers.PRIORITY.VISUAL.PROGRESSIVE_LAYOUT, createProgressiveLayout('pictorialBar'));

registerBarGridAxisHandlers(registers);
}
6 changes: 5 additions & 1 deletion src/chart/boxplot/BoxplotSeries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export interface BoxplotSeriesOption
coordinateSystem?: 'cartesian2d'

layout?: LayoutOrient
clip?: boolean;
/**
* [min, max] can be percent of band width.
*/
Expand All @@ -73,9 +74,11 @@ export interface BoxplotSeriesOption
data?: (BoxplotDataValue | BoxplotDataItemOption)[]
}

export const SERIES_TYPE_BOXPLOT = 'boxplot';

class BoxplotSeriesModel extends SeriesModel<BoxplotSeriesOption> {

static readonly type = 'series.boxplot';
static readonly type = 'series.' + SERIES_TYPE_BOXPLOT;
readonly type = BoxplotSeriesModel.type;

static readonly dependencies = ['xAxis', 'yAxis', 'grid'];
Expand Down Expand Up @@ -109,6 +112,7 @@ class BoxplotSeriesModel extends SeriesModel<BoxplotSeriesOption> {
legendHoverLink: true,

layout: null,
clip: true,
boxWidth: [7, 50],

itemStyle: {
Expand Down
43 changes: 39 additions & 4 deletions src/chart/boxplot/BoxplotView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,27 @@
* under the License.
*/

import * as zrUtil from 'zrender/src/core/util';
import ChartView from '../../view/Chart';
import * as graphic from '../../util/graphic';
import { setStatesStylesFromModel, toggleHoverEmphasis } from '../../util/states';
import Path, { PathProps } from 'zrender/src/graphic/Path';
import BoxplotSeriesModel, { BoxplotDataItemOption } from './BoxplotSeries';
import BoxplotSeriesModel, { SERIES_TYPE_BOXPLOT, BoxplotDataItemOption } from './BoxplotSeries';
import GlobalModel from '../../model/Global';
import ExtensionAPI from '../../core/ExtensionAPI';
import SeriesData from '../../data/SeriesData';
import { BoxplotItemLayout } from './boxplotLayout';
import { saveOldStyle } from '../../animation/basicTransition';
import { resolveNormalBoxClipping } from '../helper/whiskerBoxCommon';
import {
createClipPath, SHAPE_CLIP_KIND_FULLY_CLIPPED, SHAPE_CLIP_KIND_NOT_CLIPPED,
SHAPE_CLIP_KIND_PARTIALLY_CLIPPED,
updateClipPath
} from '../helper/createClipPathFromCoordSys';
import { map } from 'zrender/src/core/util';


class BoxplotView extends ChartView {
static type = 'boxplot';
static type = SERIES_TYPE_BOXPLOT;
type = BoxplotView.type;

private _data: SeriesData;
Expand All @@ -47,12 +54,29 @@ class BoxplotView extends ChartView {
}

const constDim = seriesModel.getWhiskerBoxesLayout() === 'horizontal' ? 1 : 0;
const needClip = seriesModel.get('clip', true);
const coordSys = seriesModel.coordinateSystem;
const clipArea = coordSys.getArea && coordSys.getArea();
const clipPath = needClip && createClipPath(coordSys, false, seriesModel);

data.diff(oldData)
.add(function (newIdx) {
if (data.hasValue(newIdx)) {
const itemLayout = data.getItemLayout(newIdx) as BoxplotItemLayout;

const clipKind = needClip
? resolveNormalBoxClipping(clipArea, itemLayout) : SHAPE_CLIP_KIND_NOT_CLIPPED;
if (clipKind === SHAPE_CLIP_KIND_FULLY_CLIPPED) {
return;
}

const symbolEl = createNormalBox(itemLayout, data, newIdx, constDim, true);
// One axis tick can corresponds to a group of box items (from different series),
// so it may be visually misleading when a group of items are partially outside
// but no clipping is applied.
// Consider performance of zr Element['clipPath'], only set to partially clipped elements.
updateClipPath(clipKind === SHAPE_CLIP_KIND_PARTIALLY_CLIPPED, symbolEl, clipPath);

data.setItemGraphicEl(newIdx, symbolEl);
group.add(symbolEl);
}
Expand All @@ -67,6 +91,14 @@ class BoxplotView extends ChartView {
}

const itemLayout = data.getItemLayout(newIdx) as BoxplotItemLayout;

const clipKind = needClip
? resolveNormalBoxClipping(clipArea, itemLayout) : SHAPE_CLIP_KIND_NOT_CLIPPED;
if (clipKind === SHAPE_CLIP_KIND_FULLY_CLIPPED) {
group.remove(symbolEl);
return;
}

if (!symbolEl) {
symbolEl = createNormalBox(itemLayout, data, newIdx, constDim);
}
Expand All @@ -75,6 +107,9 @@ class BoxplotView extends ChartView {
updateNormalBoxData(itemLayout, symbolEl, data, newIdx);
}

// See `updateClipPath` in `add`.
updateClipPath(clipKind === SHAPE_CLIP_KIND_PARTIALLY_CLIPPED, symbolEl, clipPath);

group.add(symbolEl);

data.setItemGraphicEl(newIdx, symbolEl);
Expand Down Expand Up @@ -192,7 +227,7 @@ function updateNormalBoxData(
}

function transInit(points: number[][], dim: number, itemLayout: BoxplotItemLayout) {
return zrUtil.map(points, function (point) {
return map(points, function (point) {
point = point.slice();
point[dim] = itemLayout.initBaseline;
return point;
Expand Down
138 changes: 64 additions & 74 deletions src/chart/boxplot/boxplotLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,103 +17,70 @@
* under the License.
*/

import * as zrUtil from 'zrender/src/core/util';
import { isArray } from 'zrender/src/core/util';
import {parsePercent} from '../../util/number';
import type GlobalModel from '../../model/Global';
import BoxplotSeriesModel from './BoxplotSeries';
import Axis2D from '../../coord/cartesian/Axis2D';

const each = zrUtil.each;

interface GroupItem {
seriesModels: BoxplotSeriesModel[]
axis: Axis2D
boxOffsetList: number[]
boxWidthList: number[]
}
import BoxplotSeriesModel, { SERIES_TYPE_BOXPLOT } from './BoxplotSeries';
import {
countSeriesOnAxisOnKey, eachAxisOnKey, eachSeriesOnAxisOnKey,
requireAxisStatistics
} from '../../coord/axisStatistics';
import { makeCallOnlyOnce } from '../../util/model';
import { EChartsExtensionInstallRegisters } from '../../extension';
import Axis from '../../coord/Axis';
import { registerAxisContainShapeHandler } from '../../coord/scaleRawExtentInfo';
import { calcBandWidth } from '../../coord/axisBand';
import {
createBandWidthBasedAxisContainShapeHandler,
getMetricsNonOrdinalLinearPositiveMinGap,
makeAxisStatKey
} from '../helper/axisSnippets';


const callOnlyOnce = makeCallOnlyOnce();

export interface BoxplotItemLayout {
ends: number[][]
initBaseline: number
}

export default function boxplotLayout(ecModel: GlobalModel) {

const groupResult = groupSeriesByAxis(ecModel);

each(groupResult, function (groupItem) {
const seriesModels = groupItem.seriesModels;

if (!seriesModels.length) {
export function boxplotLayout(ecModel: GlobalModel) {
const axisStatKey = makeAxisStatKey(SERIES_TYPE_BOXPLOT);
eachAxisOnKey(ecModel, axisStatKey, function (axis) {
const seriesCount = countSeriesOnAxisOnKey(axis, axisStatKey);
if (!seriesCount) {
return;
}

calculateBase(groupItem);

each(seriesModels, function (seriesModel, idx) {
const baseResult = calculateBase(axis, seriesCount);
eachSeriesOnAxisOnKey(axis, axisStatKey, function (seriesModel: BoxplotSeriesModel, idx) {
layoutSingleSeries(
seriesModel,
groupItem.boxOffsetList[idx],
groupItem.boxWidthList[idx]
baseResult.boxOffsetList[idx],
baseResult.boxWidthList[idx]
);
});
});
}

/**
* Group series by axis.
*/
function groupSeriesByAxis(ecModel: GlobalModel) {
const result: GroupItem[] = [];
const axisList: Axis2D[] = [];

ecModel.eachSeriesByType('boxplot', function (seriesModel: BoxplotSeriesModel) {
const baseAxis = seriesModel.getBaseAxis();
let idx = zrUtil.indexOf(axisList, baseAxis);

if (idx < 0) {
idx = axisList.length;
axisList[idx] = baseAxis;
result[idx] = {
axis: baseAxis,
seriesModels: []
} as GroupItem;
}

result[idx].seriesModels.push(seriesModel);
});

return result;
}

/**
* Calculate offset and box width for each series.
*/
function calculateBase(groupItem: GroupItem) {
const baseAxis = groupItem.axis;
const seriesModels = groupItem.seriesModels;
const seriesCount = seriesModels.length;

const boxWidthList: number[] = groupItem.boxWidthList = [];
const boxOffsetList: number[] = groupItem.boxOffsetList = [];
function calculateBase(baseAxis: Axis, seriesCount: number): {
boxOffsetList: number[];
boxWidthList: number[];
} {
const boxWidthList: number[] = [];
const boxOffsetList: number[] = [];
const boundList: number[][] = [];

let bandWidth: number;
if (baseAxis.type === 'category') {
bandWidth = baseAxis.getBandWidth();
}
else {
let maxDataCount = 0;
each(seriesModels, function (seriesModel) {
maxDataCount = Math.max(maxDataCount, seriesModel.getData().count());
});
const extent = baseAxis.getExtent();
bandWidth = Math.abs(extent[1] - extent[0]) / maxDataCount;
}
const bandWidth = calcBandWidth(
baseAxis,
{fromStat: {key: makeAxisStatKey(SERIES_TYPE_BOXPLOT)}, min: 1},
).w;

each(seriesModels, function (seriesModel) {
eachSeriesOnAxisOnKey(baseAxis, makeAxisStatKey(SERIES_TYPE_BOXPLOT), function (seriesModel: BoxplotSeriesModel) {
let boxWidthBound = seriesModel.get('boxWidth');
if (!zrUtil.isArray(boxWidthBound)) {
if (!isArray(boxWidthBound)) {
boxWidthBound = [boxWidthBound, boxWidthBound];
}
boundList.push([
Expand All @@ -127,14 +94,19 @@ function calculateBase(groupItem: GroupItem) {
const boxWidth = (availableWidth - boxGap * (seriesCount - 1)) / seriesCount;
let base = boxWidth / 2 - availableWidth / 2;

each(seriesModels, function (seriesModel, idx) {
eachSeriesOnAxisOnKey(baseAxis, makeAxisStatKey(SERIES_TYPE_BOXPLOT), function (seriesModel, idx) {
boxOffsetList.push(base);
base += boxGap + boxWidth;

boxWidthList.push(
Math.min(Math.max(boxWidth, boundList[idx][0]), boundList[idx][1])
);
});

return {
boxOffsetList,
boxWidthList,
};
}

/**
Expand Down Expand Up @@ -212,3 +184,21 @@ function layoutSingleSeries(seriesModel: BoxplotSeriesModel, offset: number, box
ends.push(from, to);
}
}

export function registerBoxplotAxisHandlers(registers: EChartsExtensionInstallRegisters) {
callOnlyOnce(registers, function () {
const axisStatKey = makeAxisStatKey(SERIES_TYPE_BOXPLOT);
requireAxisStatistics(
registers,
{
key: axisStatKey,
seriesType: SERIES_TYPE_BOXPLOT,
getMetrics: getMetricsNonOrdinalLinearPositiveMinGap,
}
);
registerAxisContainShapeHandler(
axisStatKey,
createBandWidthBasedAxisContainShapeHandler(axisStatKey)
);
});
}
4 changes: 3 additions & 1 deletion src/chart/boxplot/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
import { EChartsExtensionInstallRegisters } from '../../extension';
import BoxplotSeriesModel from './BoxplotSeries';
import BoxplotView from './BoxplotView';
import boxplotLayout from './boxplotLayout';
import {boxplotLayout, registerBoxplotAxisHandlers} from './boxplotLayout';
import { boxplotTransform } from './boxplotTransform';

export function install(registers: EChartsExtensionInstallRegisters) {
registers.registerSeriesModel(BoxplotSeriesModel);
registers.registerChartView(BoxplotView);
registers.registerLayout(boxplotLayout);
registers.registerTransform(boxplotTransform);

registerBoxplotAxisHandlers(registers);
}
Loading
Loading