Skip to content

Commit 6afc12c

Browse files
authored
feat(box): 重构和增强箱线图 (#2583)
* docs(legend): 图例文档 * refactor(box): 使用 geometry adaptor 处理 box,优化 tooltip & 增加单测 * docs(box): 箱线图增加设置字段别名的 demo
1 parent 34e3e83 commit 6afc12c

File tree

7 files changed

+166
-103
lines changed

7 files changed

+166
-103
lines changed
+63-26
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,88 @@
11
import { Box } from '../../../../src';
22
import { boxData, groupBoxData } from '../../../data/box';
3-
import { createDiv } from '../../../utils/dom';
3+
import { createDiv, removeDom } from '../../../utils/dom';
44

55
describe('box tooltip', () => {
6-
it('x*y and tooltip', () => {
7-
const box = new Box(createDiv('box tooltip'), {
8-
width: 400,
9-
height: 500,
10-
data: boxData,
11-
xField: 'x',
12-
yField: ['low', 'q1', 'median', 'q3', 'high'],
13-
tooltip: {
14-
title: 'hello world',
15-
},
16-
});
6+
const div = createDiv('');
7+
const box = new Box(div, {
8+
width: 400,
9+
height: 500,
10+
data: boxData,
11+
xField: 'x',
12+
yField: ['low', 'q1', 'median', 'q3', 'high'],
13+
tooltip: {
14+
title: 'hello world',
15+
},
16+
});
1717

18-
box.render();
18+
box.render();
19+
20+
it('tooltip: custom title', () => {
1921
// @ts-ignore
2022
expect(box.chart.options.tooltip.title).toBe('hello world');
23+
const bbox = box.chart.geometries[0].elements[0].getBBox();
24+
25+
box.chart.showTooltip({ x: bbox.width / 2 + bbox.x, y: bbox.height / 2 + bbox.y });
26+
expect(div.querySelectorAll('.g2-tooltip-list-item').length).toBe(5);
27+
box.chart.hideTooltip();
28+
});
2129

30+
it('tooltip: fields', () => {
31+
box.update({ tooltip: { fields: ['low', 'q1', 'median'] } });
32+
const bbox = box.chart.geometries[0].elements[0].getBBox();
33+
34+
box.chart.showTooltip({ x: bbox.width / 2 + bbox.x, y: bbox.height / 2 + bbox.y });
35+
expect(div.querySelectorAll('.g2-tooltip-list-item').length).toBe(3);
36+
box.chart.hideTooltip();
37+
});
38+
39+
it('tooltip: fields & customContent', () => {
2240
box.update({
23-
...box.options,
24-
tooltip: false,
41+
tooltip: {
42+
fields: ['low', 'q1', 'median'],
43+
customContent: (text, items) =>
44+
`<div>${items.map((item, idx) => `<div class="custom-tooltip-item-content">${idx}</div>`)}<div>`,
45+
},
2546
});
26-
// @ts-ignore
27-
expect(box.chart.options.tooltip).toBe(false);
28-
expect(box.chart.getComponents().find((co) => co.type === 'tooltip')).toBe(undefined);
2947

30-
box.destroy();
48+
const bbox = box.chart.geometries[0].elements[0].getBBox();
49+
box.chart.showTooltip({ x: bbox.width / 2 + bbox.x, y: bbox.height / 2 + bbox.y });
50+
expect(div.getElementsByClassName('custom-tooltip-item-content').length).toBe(3);
51+
52+
// 设置hide
53+
box.chart.hideTooltip();
3154
});
3255

33-
it('default toolip', () => {
34-
const box = new Box(createDiv('default tooltip'), {
35-
width: 400,
36-
height: 500,
56+
it('tooltip: groupField', () => {
57+
box.update({
3758
data: groupBoxData,
3859
xField: 'type',
3960
yField: '_bin',
4061
groupField: 'Species',
62+
tooltip: { fields: undefined, customContent: undefined },
4163
});
42-
43-
box.render();
4464
// @ts-ignore
4565
expect(box.chart.options.tooltip.shared).toBe(true);
66+
// @ts-ignore 箱线图 默认不展示 crosshairs(ugly)
67+
expect(box.chart.options.tooltip.showCrosshairs).toBeUndefined();
68+
69+
const bbox = box.chart.geometries[0].elements[0].getBBox();
70+
box.chart.showTooltip({ x: bbox.width / 2 + bbox.x, y: bbox.height / 2 + bbox.y });
71+
72+
expect(div.querySelectorAll('.g2-tooltip-list-item').length).toBe(3);
73+
expect((div.querySelector('.g2-tooltip-name') as HTMLElement).innerText).toBe('I. setosa');
74+
box.chart.hideTooltip();
75+
});
76+
77+
it('tooltip: false', () => {
78+
box.update({ tooltip: false });
4679
// @ts-ignore
47-
expect(box.chart.options.tooltip.showCrosshairs).toBe(true);
80+
expect(box.chart.options.tooltip).toBe(false);
81+
expect(box.chart.getComponents().find((co) => co.type === 'tooltip')).toBe(undefined);
82+
});
4883

84+
afterAll(() => {
4985
box.destroy();
86+
removeDom(div);
5087
});
5188
});

docs/common/legend-cfg.en.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ Apply to <tag color="cyan" text="Continuous legend">Continuous legend</tag>, sel
212212

213213
Apply to <tag color="cyan" text="Continuous legend">Continuous legend</tag>, 当前选中的范围.
214214

215-
##### legendOption.selected ✨ 🆕
215+
##### selected ✨ 🆕
216216

217217
<description> _object_ **optional** </description>
218218

docs/common/legend-cfg.zh.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ pageNavigator: {
231231

232232
适用于 <tag color="cyan" text="连续图例">连续图例</tag>,当前选中的范围。
233233

234-
##### legendOption.selected ✨ 🆕
234+
##### selected ✨ 🆕
235235

236236
<description> _object_ **optional** </description>
237237

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Box } from '@antv/g2plot';
2+
3+
const data = [
4+
{ x: 'Oceania', low: 1, q1: 9, median: 16, q3: 22, high: 24 },
5+
{ x: 'East Europe', low: 1, q1: 5, median: 8, q3: 12, high: 16 },
6+
{ x: 'Australia', low: 1, q1: 8, median: 12, q3: 19, high: 26 },
7+
{ x: 'South America', low: 2, q1: 8, median: 12, q3: 21, high: 28 },
8+
{ x: 'North Africa', low: 1, q1: 8, median: 14, q3: 18, high: 24 },
9+
{ x: 'North America', low: 3, q1: 10, median: 17, q3: 28, high: 30 },
10+
{ x: 'West Europe', low: 1, q1: 7, median: 10, q3: 17, high: 22 },
11+
{ x: 'West Africa', low: 1, q1: 6, median: 8, q3: 13, high: 16 },
12+
];
13+
14+
const boxPlot = new Box('container', {
15+
width: 400,
16+
height: 500,
17+
data: data,
18+
xField: 'x',
19+
yField: ['low', 'q1', 'median', 'q3', 'high'],
20+
meta: {
21+
low: {
22+
alias: '最低值',
23+
},
24+
q1: {
25+
alias: '下四分位数',
26+
},
27+
median: {
28+
alias: '最低值',
29+
},
30+
q3: {
31+
alias: '上四分位数',
32+
},
33+
high: {
34+
alias: '最高值',
35+
},
36+
},
37+
tooltip: {
38+
fields: ['high', 'q3', 'median', 'q1', 'low'],
39+
},
40+
boxStyle: {
41+
stroke: '#545454',
42+
fill: '#1890FF',
43+
fillOpacity: 0.3,
44+
},
45+
animation: false,
46+
});
47+
48+
boxPlot.render();

examples/more-plots/box/demo/meta.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@
2727
"en": "Box plot with error"
2828
},
2929
"screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*cE6vR461omUAAAAAAAAAAAAAARQnAQ"
30+
},
31+
{
32+
"filename": "meta-alias.ts",
33+
"title": {
34+
"zh": "设置字段别名",
35+
"en": "Set alias of field"
36+
},
37+
"screenshot": "https://gw.alipayobjects.com/zos/antfincdn/BUWqiUYOhY/box-alias.png"
3038
}
3139
]
32-
}
40+
}

src/plots/box/adaptor.ts

+41-73
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { isFunction, isObject } from '@antv/util';
1+
import { isArray } from '@antv/util';
22
import { Params } from '../../core/adaptor';
3-
import { interaction, animation, theme } from '../../adaptor/common';
4-
import { findGeometry } from '../../utils';
3+
import { interaction, animation, theme, tooltip } from '../../adaptor/common';
4+
import { point, schema } from '../../adaptor/geometries';
55
import { flow, pick, deepAssign } from '../../utils';
66
import { AXIS_META_CONFIG_KEYS } from '../../constant';
77
import { BoxOptions } from './types';
@@ -14,18 +14,38 @@ import { transformData } from './utils';
1414
*/
1515
function field(params: Params<BoxOptions>): Params<BoxOptions> {
1616
const { chart, options } = params;
17-
const { xField, yField, groupField, color } = options;
17+
const { xField, yField, groupField, color, tooltip, boxStyle } = options;
1818

19-
const yFieldName = Array.isArray(yField) ? BOX_RANGE : yField;
19+
chart.data(transformData(options.data, yField));
2020

21-
const geometry = chart.schema().position(`${xField}*${yFieldName}`).shape('box');
21+
const yFieldName = isArray(yField) ? BOX_RANGE : yField;
22+
const rawFields = yField ? (isArray(yField) ? yField : [yField]) : [];
2223

23-
// set group field as color channel
24-
if (groupField) {
25-
geometry.color(groupField, color).adjust('dodge');
24+
let tooltipOptions = tooltip;
25+
if (tooltipOptions !== false) {
26+
tooltipOptions = deepAssign({}, { fields: isArray(yField) ? yField : [] }, tooltipOptions);
2627
}
2728

28-
chart.data(transformData(options.data, yField));
29+
const { ext } = schema(
30+
deepAssign({}, params, {
31+
options: {
32+
xField,
33+
yField: yFieldName,
34+
seriesField: groupField,
35+
tooltip: tooltipOptions,
36+
rawFields,
37+
schema: {
38+
shape: 'box',
39+
color,
40+
style: boxStyle,
41+
},
42+
},
43+
})
44+
);
45+
46+
if (groupField) {
47+
ext.geometry.adjust('dodge');
48+
}
2949

3050
return params;
3151
}
@@ -38,24 +58,17 @@ function outliersPoint(params: Params<BoxOptions>): Params<BoxOptions> {
3858

3959
const outliersView = chart.createView({ padding, id: OUTLIERS_VIEW_ID });
4060
outliersView.data(data);
61+
62+
point({
63+
chart: outliersView,
64+
options: {
65+
xField,
66+
yField: outliersField,
67+
point: { shape: 'circle', style: outliersStyle },
68+
},
69+
});
70+
4171
outliersView.axis(false);
42-
const geometry = outliersView.point().position(`${xField}*${outliersField}`).shape('circle');
43-
44-
/**
45-
* style 的几种情况
46-
* g.style({ fill: 'red' });
47-
* g.style('x*y*color', (x, y, color) => ({ fill: 'red' }));
48-
*/
49-
if (isFunction(outliersStyle)) {
50-
geometry.style(`${xField}*${outliersField}`, (_x: string, _outliers: number) => {
51-
return outliersStyle({
52-
[xField]: _x,
53-
[outliersField]: _outliers,
54-
});
55-
});
56-
} else if (isObject(outliersStyle)) {
57-
geometry.style(outliersStyle);
58-
}
5972

6073
return params;
6174
}
@@ -137,55 +150,10 @@ export function legend(params: Params<BoxOptions>): Params<BoxOptions> {
137150
return params;
138151
}
139152

140-
/**
141-
* 样式
142-
* @param params
143-
*/
144-
function style(params: Params<BoxOptions>): Params<BoxOptions> {
145-
const { chart, options } = params;
146-
const { xField, yField, boxStyle } = options;
147-
148-
const geometry = findGeometry(chart, 'schema');
149-
const yFieldName = Array.isArray(yField) ? BOX_RANGE : yField;
150-
151-
/**
152-
* style 的几种情况
153-
* g.style({ fill: 'red' });
154-
* g.style('x*y*color', (x, y, color) => ({ fill: 'red' }));
155-
*/
156-
if (isFunction(boxStyle)) {
157-
geometry.style(`${xField}*${yFieldName}`, (_x: string, _y: number) => {
158-
return boxStyle({
159-
[xField]: _x,
160-
[yFieldName]: _y,
161-
});
162-
});
163-
} else if (isObject(boxStyle)) {
164-
geometry.style(boxStyle);
165-
}
166-
167-
return params;
168-
}
169-
170-
/**
171-
* tooltip 配置
172-
* @param params
173-
*/
174-
export function tooltip(params: Params<BoxOptions>): Params<BoxOptions> {
175-
const { chart, options } = params;
176-
const { tooltip } = options;
177-
178-
if (tooltip !== undefined) {
179-
chart.tooltip(tooltip);
180-
}
181-
182-
return params;
183-
}
184-
185153
/**
186154
* 箱型图适配器
187155
* @param params
188156
*/
189157
export function adaptor(params: Params<BoxOptions>) {
190-
return flow(field, outliersPoint, meta, axis, style, legend, tooltip, interaction, animation, theme)(params);
158+
return flow(field, outliersPoint, meta, axis, legend, tooltip, interaction, animation, theme)(params);
191159
}

src/plots/box/constant.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ export const DEFAULT_OPTIONS = deepAssign({}, Plot.getDefaultOptions(), {
2222
// 默认 tooltips 共享,不显示 markers
2323
tooltip: {
2424
showMarkers: false,
25-
showCrosshairs: true,
2625
shared: true,
2726
},
27+
boxStyle: {
28+
lineWidth: 1,
29+
},
2830
});

0 commit comments

Comments
 (0)