Skip to content

Commit dbfaf6a

Browse files
committed
fix&feature:
(1) feature: Add option boxplot.clip (2) feature: Enable boxplot and candlestick containShape for "value"/"time"/"log" axis, enable proper shadow axisPointer. (candlestick-case.html, boxplot-multi.html) (3) fix: Fix candlestick ends shapes can not be displayed on "value"/"time" axis. (candlestick-case.html) (4) fix: candlestick shape width is inappropriate when datazoom filterMode is 'none'/'empty'. (candlestick-case.html) (5) fix: In polar coordinate system, support "value"/"time" axis as angle/radius axis, enable propere bandWidth for bar series, enable proper shadow axisPointer. (bar-polar-multi-series-radial.html, bar-polar-multi-series.html)
1 parent 7a8d38b commit dbfaf6a

64 files changed

Lines changed: 2522 additions & 1051 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/chart/bar/BarView.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ function getClipArea(coord: CoordSysOfBar, data: SeriesData) {
9999
// When boundaryGap is false in category axis, bar may exceed the grid.
100100
// We should not clip this part.
101101
// See test/bar2.html
102+
// PENDING: The effect is not preferable, but we preserve it for backward compatibility.
102103
if (baseAxis.type === 'category' && !baseAxis.onBand) {
103104
const expandWidth = data.getLayout('bandWidth');
104105
if (baseAxis.isHorizontal()) {

src/chart/boxplot/BoxplotSeries.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export interface BoxplotSeriesOption
6565
coordinateSystem?: 'cartesian2d'
6666

6767
layout?: LayoutOrient
68+
clip?: boolean;
6869
/**
6970
* [min, max] can be percent of band width.
7071
*/
@@ -73,9 +74,11 @@ export interface BoxplotSeriesOption
7374
data?: (BoxplotDataValue | BoxplotDataItemOption)[]
7475
}
7576

77+
export const SERIES_TYPE_BOXPLOT = 'boxplot';
78+
7679
class BoxplotSeriesModel extends SeriesModel<BoxplotSeriesOption> {
7780

78-
static readonly type = 'series.boxplot';
81+
static readonly type = 'series.' + SERIES_TYPE_BOXPLOT;
7982
readonly type = BoxplotSeriesModel.type;
8083

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

111114
layout: null,
115+
clip: true,
112116
boxWidth: [7, 50],
113117

114118
itemStyle: {

src/chart/boxplot/BoxplotView.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,27 @@
1717
* under the License.
1818
*/
1919

20-
import * as zrUtil from 'zrender/src/core/util';
2120
import ChartView from '../../view/Chart';
2221
import * as graphic from '../../util/graphic';
2322
import { setStatesStylesFromModel, toggleHoverEmphasis } from '../../util/states';
2423
import Path, { PathProps } from 'zrender/src/graphic/Path';
25-
import BoxplotSeriesModel, { BoxplotDataItemOption } from './BoxplotSeries';
24+
import BoxplotSeriesModel, { SERIES_TYPE_BOXPLOT, BoxplotDataItemOption } from './BoxplotSeries';
2625
import GlobalModel from '../../model/Global';
2726
import ExtensionAPI from '../../core/ExtensionAPI';
2827
import SeriesData from '../../data/SeriesData';
2928
import { BoxplotItemLayout } from './boxplotLayout';
3029
import { saveOldStyle } from '../../animation/basicTransition';
30+
import { resolveNormalBoxClipping } from '../helper/whiskerBoxCommon';
31+
import {
32+
createClipPath, SHAPE_CLIP_KIND_FULLY_CLIPPED, SHAPE_CLIP_KIND_NOT_CLIPPED,
33+
SHAPE_CLIP_KIND_PARTIALLY_CLIPPED,
34+
updateClipPath
35+
} from '../helper/createClipPathFromCoordSys';
36+
import { map } from 'zrender/src/core/util';
37+
3138

3239
class BoxplotView extends ChartView {
33-
static type = 'boxplot';
40+
static type = SERIES_TYPE_BOXPLOT;
3441
type = BoxplotView.type;
3542

3643
private _data: SeriesData;
@@ -47,12 +54,29 @@ class BoxplotView extends ChartView {
4754
}
4855

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

5162
data.diff(oldData)
5263
.add(function (newIdx) {
5364
if (data.hasValue(newIdx)) {
5465
const itemLayout = data.getItemLayout(newIdx) as BoxplotItemLayout;
66+
67+
const clipKind = needClip
68+
? resolveNormalBoxClipping(clipArea, itemLayout) : SHAPE_CLIP_KIND_NOT_CLIPPED;
69+
if (clipKind === SHAPE_CLIP_KIND_FULLY_CLIPPED) {
70+
return;
71+
}
72+
5573
const symbolEl = createNormalBox(itemLayout, data, newIdx, constDim, true);
74+
// One axis tick can corresponds to a group of box items (from different series),
75+
// so it may be visually misleading when a group of items are partially outside
76+
// but no clipping is applied.
77+
// Consider performance of zr Element['clipPath'], only set to partially clipped elements.
78+
updateClipPath(clipKind === SHAPE_CLIP_KIND_PARTIALLY_CLIPPED, symbolEl, clipPath);
79+
5680
data.setItemGraphicEl(newIdx, symbolEl);
5781
group.add(symbolEl);
5882
}
@@ -67,6 +91,14 @@ class BoxplotView extends ChartView {
6791
}
6892

6993
const itemLayout = data.getItemLayout(newIdx) as BoxplotItemLayout;
94+
95+
const clipKind = needClip
96+
? resolveNormalBoxClipping(clipArea, itemLayout) : SHAPE_CLIP_KIND_NOT_CLIPPED;
97+
if (clipKind === SHAPE_CLIP_KIND_FULLY_CLIPPED) {
98+
group.remove(symbolEl);
99+
return;
100+
}
101+
70102
if (!symbolEl) {
71103
symbolEl = createNormalBox(itemLayout, data, newIdx, constDim);
72104
}
@@ -75,6 +107,9 @@ class BoxplotView extends ChartView {
75107
updateNormalBoxData(itemLayout, symbolEl, data, newIdx);
76108
}
77109

110+
// See `updateClipPath` in `add`.
111+
updateClipPath(clipKind === SHAPE_CLIP_KIND_PARTIALLY_CLIPPED, symbolEl, clipPath);
112+
78113
group.add(symbolEl);
79114

80115
data.setItemGraphicEl(newIdx, symbolEl);
@@ -192,7 +227,7 @@ function updateNormalBoxData(
192227
}
193228

194229
function transInit(points: number[][], dim: number, itemLayout: BoxplotItemLayout) {
195-
return zrUtil.map(points, function (point) {
230+
return map(points, function (point) {
196231
point = point.slice();
197232
point[dim] = itemLayout.initBaseline;
198233
return point;

src/chart/boxplot/boxplotLayout.ts

Lines changed: 60 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -17,103 +17,69 @@
1717
* under the License.
1818
*/
1919

20-
import * as zrUtil from 'zrender/src/core/util';
20+
import { isArray } from 'zrender/src/core/util';
2121
import {parsePercent} from '../../util/number';
2222
import type GlobalModel from '../../model/Global';
23-
import BoxplotSeriesModel from './BoxplotSeries';
24-
import Axis2D from '../../coord/cartesian/Axis2D';
25-
26-
const each = zrUtil.each;
27-
28-
interface GroupItem {
29-
seriesModels: BoxplotSeriesModel[]
30-
axis: Axis2D
31-
boxOffsetList: number[]
32-
boxWidthList: number[]
33-
}
23+
import BoxplotSeriesModel, { SERIES_TYPE_BOXPLOT } from './BoxplotSeries';
24+
import {
25+
eachCollectedAxis, eachCollectedSeries, getCollectedSeriesLength,
26+
requireAxisStatistics
27+
} from '../../coord/axisStatistics';
28+
import { makeCallOnlyOnce } from '../../util/model';
29+
import { EChartsExtensionInstallRegisters } from '../../extension';
30+
import Axis from '../../coord/Axis';
31+
import { registerAxisContainShapeHandler } from '../../coord/scaleRawExtentInfo';
32+
import { calcBandWidth } from '../../coord/axisBand';
33+
import {
34+
makeAxisStatKey,
35+
createSimpleAxisStatClient, createBandWidthBasedAxisContainShapeHandler
36+
} from '../helper/axisSnippets';
37+
38+
39+
const callOnlyOnce = makeCallOnlyOnce();
3440

3541
export interface BoxplotItemLayout {
3642
ends: number[][]
3743
initBaseline: number
3844
}
3945

40-
export default function boxplotLayout(ecModel: GlobalModel) {
41-
42-
const groupResult = groupSeriesByAxis(ecModel);
43-
44-
each(groupResult, function (groupItem) {
45-
const seriesModels = groupItem.seriesModels;
46-
47-
if (!seriesModels.length) {
46+
export function boxplotLayout(ecModel: GlobalModel) {
47+
const axisStatKey = makeAxisStatKey(SERIES_TYPE_BOXPLOT);
48+
eachCollectedAxis(ecModel, axisStatKey, function (axis) {
49+
const seriesCount = getCollectedSeriesLength(axis, axisStatKey);
50+
if (!seriesCount) {
4851
return;
4952
}
50-
51-
calculateBase(groupItem);
52-
53-
each(seriesModels, function (seriesModel, idx) {
53+
const baseResult = calculateBase(axis, seriesCount);
54+
eachCollectedSeries(axis, axisStatKey, function (seriesModel: BoxplotSeriesModel, idx) {
5455
layoutSingleSeries(
5556
seriesModel,
56-
groupItem.boxOffsetList[idx],
57-
groupItem.boxWidthList[idx]
57+
baseResult.boxOffsetList[idx],
58+
baseResult.boxWidthList[idx]
5859
);
5960
});
6061
});
6162
}
6263

63-
/**
64-
* Group series by axis.
65-
*/
66-
function groupSeriesByAxis(ecModel: GlobalModel) {
67-
const result: GroupItem[] = [];
68-
const axisList: Axis2D[] = [];
69-
70-
ecModel.eachSeriesByType('boxplot', function (seriesModel: BoxplotSeriesModel) {
71-
const baseAxis = seriesModel.getBaseAxis();
72-
let idx = zrUtil.indexOf(axisList, baseAxis);
73-
74-
if (idx < 0) {
75-
idx = axisList.length;
76-
axisList[idx] = baseAxis;
77-
result[idx] = {
78-
axis: baseAxis,
79-
seriesModels: []
80-
} as GroupItem;
81-
}
82-
83-
result[idx].seriesModels.push(seriesModel);
84-
});
85-
86-
return result;
87-
}
88-
8964
/**
9065
* Calculate offset and box width for each series.
9166
*/
92-
function calculateBase(groupItem: GroupItem) {
93-
const baseAxis = groupItem.axis;
94-
const seriesModels = groupItem.seriesModels;
95-
const seriesCount = seriesModels.length;
96-
97-
const boxWidthList: number[] = groupItem.boxWidthList = [];
98-
const boxOffsetList: number[] = groupItem.boxOffsetList = [];
67+
function calculateBase(baseAxis: Axis, seriesCount: number): {
68+
boxOffsetList: number[];
69+
boxWidthList: number[];
70+
} {
71+
const boxWidthList: number[] = [];
72+
const boxOffsetList: number[] = [];
9973
const boundList: number[][] = [];
10074

101-
let bandWidth: number;
102-
if (baseAxis.type === 'category') {
103-
bandWidth = baseAxis.getBandWidth();
104-
}
105-
else {
106-
let maxDataCount = 0;
107-
each(seriesModels, function (seriesModel) {
108-
maxDataCount = Math.max(maxDataCount, seriesModel.getData().count());
109-
});
110-
const extent = baseAxis.getExtent();
111-
bandWidth = Math.abs(extent[1] - extent[0]) / maxDataCount;
112-
}
75+
const bandWidth = calcBandWidth(
76+
baseAxis,
77+
{fromStat: {key: makeAxisStatKey(SERIES_TYPE_BOXPLOT)}, min: 1},
78+
).w;
11379

114-
each(seriesModels, function (seriesModel) {
80+
eachCollectedSeries(baseAxis, makeAxisStatKey(SERIES_TYPE_BOXPLOT), function (seriesModel: BoxplotSeriesModel) {
11581
let boxWidthBound = seriesModel.get('boxWidth');
116-
if (!zrUtil.isArray(boxWidthBound)) {
82+
if (!isArray(boxWidthBound)) {
11783
boxWidthBound = [boxWidthBound, boxWidthBound];
11884
}
11985
boundList.push([
@@ -127,14 +93,19 @@ function calculateBase(groupItem: GroupItem) {
12793
const boxWidth = (availableWidth - boxGap * (seriesCount - 1)) / seriesCount;
12894
let base = boxWidth / 2 - availableWidth / 2;
12995

130-
each(seriesModels, function (seriesModel, idx) {
96+
eachCollectedSeries(baseAxis, makeAxisStatKey(SERIES_TYPE_BOXPLOT), function (seriesModel, idx) {
13197
boxOffsetList.push(base);
13298
base += boxGap + boxWidth;
13399

134100
boxWidthList.push(
135101
Math.min(Math.max(boxWidth, boundList[idx][0]), boundList[idx][1])
136102
);
137103
});
104+
105+
return {
106+
boxOffsetList,
107+
boxWidthList,
108+
};
138109
}
139110

140111
/**
@@ -212,3 +183,18 @@ function layoutSingleSeries(seriesModel: BoxplotSeriesModel, offset: number, box
212183
ends.push(from, to);
213184
}
214185
}
186+
187+
export function registerBoxplotAxisHandlers(registers: EChartsExtensionInstallRegisters) {
188+
callOnlyOnce(registers, function () {
189+
const axisStatKey = makeAxisStatKey(SERIES_TYPE_BOXPLOT);
190+
requireAxisStatistics(
191+
registers,
192+
axisStatKey,
193+
createSimpleAxisStatClient(SERIES_TYPE_BOXPLOT)
194+
);
195+
registerAxisContainShapeHandler(
196+
axisStatKey,
197+
createBandWidthBasedAxisContainShapeHandler(axisStatKey)
198+
);
199+
});
200+
}

src/chart/boxplot/install.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
import { EChartsExtensionInstallRegisters } from '../../extension';
2121
import BoxplotSeriesModel from './BoxplotSeries';
2222
import BoxplotView from './BoxplotView';
23-
import boxplotLayout from './boxplotLayout';
23+
import {boxplotLayout, registerBoxplotAxisHandlers} from './boxplotLayout';
2424
import { boxplotTransform } from './boxplotTransform';
2525

2626
export function install(registers: EChartsExtensionInstallRegisters) {
2727
registers.registerSeriesModel(BoxplotSeriesModel);
2828
registers.registerChartView(BoxplotView);
2929
registers.registerLayout(boxplotLayout);
3030
registers.registerTransform(boxplotTransform);
31+
32+
registerBoxplotAxisHandlers(registers);
3133
}

src/chart/candlestick/CandlestickSeries.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,11 @@ export interface CandlestickSeriesOption
8282
data?: (CandlestickDataValue | CandlestickDataItemOption)[]
8383
}
8484

85+
export const SERIES_TYPE_CANDLESTICK = 'candlestick';
86+
8587
class CandlestickSeriesModel extends SeriesModel<CandlestickSeriesOption> {
8688

87-
static readonly type = 'series.candlestick';
89+
static readonly type = 'series.' + SERIES_TYPE_CANDLESTICK;
8890
readonly type = CandlestickSeriesModel.type;
8991

9092
static readonly dependencies = ['xAxis', 'yAxis', 'grid'];

0 commit comments

Comments
 (0)