Skip to content

Commit c660d8f

Browse files
authored
feat: a new chart type - Box Chart (#1382)
1 parent eacf58f commit c660d8f

29 files changed

+757
-55
lines changed

__tests__/data/box.ts

+26
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,29 @@ export const boxData = [
88
{ x: 'West Europe', low: 1, q1: 7, median: 10, q3: 17, high: 22 },
99
{ x: 'West Africa', low: 1, q1: 6, median: 8, q3: 13, high: 16 },
1010
];
11+
12+
export const groupBoxData = [
13+
{ Species: 'I. setosa', type: 'SepalLength', value: 5.1, _bin: [4.3, 4.8, 5, 5.2, 5.8] },
14+
{ Species: 'I. setosa', type: 'SepalWidth', value: 3.5, _bin: [2.3, 3.2, 3.4, 3.7, 4.4] },
15+
{ Species: 'I. setosa', type: 'PetalLength', value: 1.4, _bin: [1, 1.4, 1.5, 1.6, 1.9] },
16+
{ Species: 'I. setosa', type: 'PetalWidth', value: 0.2, _bin: [0.1, 0.2, 0.2, 0.3, 0.6] },
17+
{ Species: 'I. versicolor', type: 'SepalLength', value: 7, _bin: [4.9, 5.6, 5.9, 6.3, 7] },
18+
{ Species: 'I. versicolor', type: 'SepalWidth', value: 3.2, _bin: [2, 2.5, 2.8, 3, 3.4] },
19+
{ Species: 'I. versicolor', type: 'PetalLength', value: 4.7, _bin: [3, 4, 4.35, 4.6, 5.1] },
20+
{ Species: 'I. versicolor', type: 'PetalWidth', value: 1.4, _bin: [1, 1.2, 1.3, 1.5, 1.8] },
21+
{ Species: 'I. virginica', type: 'SepalLength', value: 6.3, _bin: [4.9, 6.2, 6.5, 6.9, 7.9] },
22+
{ Species: 'I. virginica', type: 'SepalWidth', value: 3.3, _bin: [2.2, 2.8, 3, 3.2, 3.8] },
23+
{ Species: 'I. virginica', type: 'PetalLength', value: 6, _bin: [4.5, 5.1, 5.55, 5.9, 6.9] },
24+
{ Species: 'I. virginica', type: 'PetalWidth', value: 2.5, _bin: [1.4, 1.8, 2, 2.3, 2.5] },
25+
];
26+
27+
export const outliersData = [
28+
{ x: '职业 A', low: 20000, q1: 26000, median: 27000, q3: 32000, high: 38000, outliers: [50000, 52000] },
29+
{ x: '职业 B', low: 40000, q1: 49000, median: 62000, q3: 73000, high: 88000, outliers: [32000, 29000, 106000] },
30+
{ x: '职业 C', low: 52000, q1: 59000, median: 65000, q3: 74000, high: 83000, outliers: [91000] },
31+
{ x: '职业 D', low: 58000, q1: 96000, median: 130000, q3: 170000, high: 200000, outliers: [42000, 210000, 215000] },
32+
{ x: '职业 E', low: 24000, q1: 28000, median: 32000, q3: 38000, high: 42000, outliers: [48000] },
33+
{ x: '职业 F', low: 47000, q1: 56000, median: 69000, q3: 85000, high: 100000, outliers: [110000, 115000, 32000] },
34+
{ x: '职业 G', low: 64000, q1: 74000, median: 83000, q3: 93000, high: 100000, outliers: [110000] },
35+
{ x: '职业 H', low: 67000, q1: 72000, median: 84000, q3: 95000, high: 110000, outliers: [57000, 54000] },
36+
];

__tests__/unit/plots/box/axis-spec.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Box } from '../../../../src';
2+
import { BOX_RANGE } from '../../../../src/plots/box/constant';
3+
import { boxData } from '../../../data/box';
4+
import { createDiv } from '../../../utils/dom';
5+
6+
describe('box axis', () => {
7+
it('meta', () => {
8+
const box = new Box(createDiv(), {
9+
width: 400,
10+
height: 500,
11+
data: boxData,
12+
xField: 'x',
13+
yField: ['low', 'q1', 'median', 'q3', 'high'],
14+
meta: {
15+
[BOX_RANGE]: {
16+
nice: true,
17+
},
18+
},
19+
});
20+
21+
box.render();
22+
23+
const geometry = box.chart.geometries[0];
24+
// @ts-ignore
25+
expect(geometry.scales[BOX_RANGE].nice).toBe(true);
26+
});
27+
28+
it('xAxis', () => {
29+
const box = new Box(createDiv(), {
30+
width: 400,
31+
height: 500,
32+
data: boxData,
33+
xField: 'x',
34+
yField: ['low', 'q1', 'median', 'q3', 'high'],
35+
xAxis: {
36+
label: {
37+
rotate: -Math.PI / 2,
38+
},
39+
},
40+
});
41+
42+
box.render();
43+
const axisOptions = box.chart.getOptions().axes;
44+
45+
// @ts-ignore
46+
expect(axisOptions.x.label.rotate).toBe(-Math.PI / 2);
47+
});
48+
49+
it('yAxis', () => {
50+
const box = new Box(createDiv(), {
51+
width: 400,
52+
height: 500,
53+
data: boxData,
54+
xField: 'x',
55+
yField: ['low', 'q1', 'median', 'q3', 'high'],
56+
yAxis: {
57+
minLimit: 0,
58+
maxLimit: 50,
59+
nice: true,
60+
},
61+
});
62+
63+
box.render();
64+
65+
const geometry = box.chart.geometries[0];
66+
const axisOptions = box.chart.getOptions().axes;
67+
68+
// @ts-ignore
69+
expect(axisOptions[BOX_RANGE].minLimit).toBe(0);
70+
expect(geometry.scales[BOX_RANGE].minLimit).toBe(0);
71+
expect(geometry.scales[BOX_RANGE].maxLimit).toBe(50);
72+
// @ts-ignore
73+
expect(geometry.scales[BOX_RANGE].nice).toBe(true);
74+
});
75+
});
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Box } from '../../../../src';
2+
import { groupBoxData } from '../../../data/box';
3+
import { createDiv } from '../../../utils/dom';
4+
5+
describe('grouped box', () => {
6+
it('grouped box', () => {
7+
const box = new Box(createDiv('grouped box'), {
8+
width: 400,
9+
height: 500,
10+
data: groupBoxData,
11+
xField: 'type',
12+
yField: '_bin',
13+
groupField: 'Species',
14+
});
15+
16+
box.render();
17+
18+
const geometry = box.chart.geometries[0];
19+
expect(geometry.getAdjust('dodge')).toMatchObject({
20+
xField: 'type',
21+
yField: '_bin',
22+
});
23+
expect(geometry.getAdjust('stack')).toBeUndefined();
24+
expect(geometry.getAttribute('color')?.getFields()).toEqual(['Species']);
25+
});
26+
});
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Box } from '../../../../src';
2+
import { BOX_RANGE } from '../../../../src/plots/box/constant';
3+
import { boxData } from '../../../data/box';
4+
import { createDiv } from '../../../utils/dom';
5+
6+
describe('box', () => {
7+
it('x*range range.min default as 0', () => {
8+
const box = new Box(createDiv('x*range range.min default as 0'), {
9+
width: 400,
10+
height: 500,
11+
data: boxData,
12+
xField: 'x',
13+
yField: ['low', 'q1', 'median', 'q3', 'high'],
14+
});
15+
16+
box.render();
17+
18+
const geometry = box.chart.geometries[0];
19+
const positionFields = geometry.getAttribute('position').getFields();
20+
21+
// 类型
22+
expect(geometry.type).toBe('schema');
23+
// 图形元素个数
24+
expect(box.chart.geometries[0].elements.length).toBe(boxData.length);
25+
// x & range
26+
expect(positionFields).toHaveLength(2);
27+
28+
// range meta default min = 0
29+
// @ts-ignore
30+
expect(geometry.scales[BOX_RANGE].min).toBe(0);
31+
});
32+
});
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Box } from '../../../../src';
2+
import { createDiv } from '../../../utils/dom';
3+
import { groupBoxData } from '../../../data/box';
4+
5+
describe('box legend', () => {
6+
it('legend position', () => {
7+
const box = new Box(createDiv('legend position'), {
8+
width: 400,
9+
height: 500,
10+
data: groupBoxData,
11+
xField: 'type',
12+
yField: '_bin',
13+
groupField: 'Species',
14+
});
15+
16+
box.render();
17+
18+
// @ts-ignore
19+
expect(box.chart.options.legends['Species'].position).toBe('bottom');
20+
21+
box.update({
22+
...box.options,
23+
legend: {
24+
position: 'right',
25+
},
26+
});
27+
// @ts-ignore
28+
expect(box.chart.options.legends['Species'].position).toBe('right');
29+
});
30+
});
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Box } from '../../../../src';
2+
import { outliersData } from '../../../data/box';
3+
import { createDiv } from '../../../utils/dom';
4+
5+
describe('box outliers', () => {
6+
it('with outliersField', () => {
7+
const box = new Box(createDiv('outliers'), {
8+
width: 400,
9+
height: 500,
10+
data: outliersData,
11+
xField: 'x',
12+
yField: ['low', 'q1', 'median', 'q3', 'high'],
13+
outliersField: 'outliers',
14+
});
15+
16+
box.render();
17+
18+
const view = box.chart.views[0];
19+
const geometry = view.geometries[0];
20+
21+
// 类型
22+
expect(geometry.type).toBe('point');
23+
// 图形元素个数
24+
expect(geometry.elements.length).toBe(outliersData.length);
25+
});
26+
27+
it('with outliersField style', () => {
28+
const box = new Box(createDiv('outliers'), {
29+
width: 400,
30+
height: 500,
31+
data: outliersData,
32+
xField: 'x',
33+
yField: ['low', 'q1', 'median', 'q3', 'high'],
34+
outliersField: 'outliers',
35+
outliersStyle: {
36+
fill: '#f6f',
37+
},
38+
});
39+
40+
box.render();
41+
42+
const view = box.chart.views[0];
43+
const elements = view.geometries[0].elements;
44+
45+
// 类型
46+
expect(elements[0].shape.cfg.children[0].attr('fill')).toBe('#f6f');
47+
});
48+
});
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Box } from '../../../../src';
2+
import { boxData } from '../../../data/box';
3+
import { createDiv } from '../../../utils/dom';
4+
5+
describe('box style', () => {
6+
it('style config', () => {
7+
const box = new Box(createDiv('style config'), {
8+
width: 400,
9+
height: 500,
10+
data: boxData,
11+
xField: 'x',
12+
yField: ['low', 'q1', 'median', 'q3', 'high'],
13+
boxStyle: {
14+
stroke: 'black',
15+
lineWidth: 2,
16+
fill: '#1890FF',
17+
},
18+
});
19+
20+
box.render();
21+
22+
const geometry = box.chart.geometries[0];
23+
const elements = geometry.elements;
24+
25+
expect(elements[0].shape.attr('stroke')).toBe('black');
26+
expect(elements[0].shape.attr('lineWidth')).toBe(2);
27+
expect(elements[0].shape.attr('fill')).toBe('#1890FF');
28+
});
29+
30+
it('style callback', () => {
31+
const box = new Box(createDiv('style config'), {
32+
width: 400,
33+
height: 500,
34+
data: boxData,
35+
xField: 'x',
36+
yField: ['low', 'q1', 'median', 'q3', 'high'],
37+
boxStyle: () => {
38+
return {
39+
stroke: 'black',
40+
lineWidth: 2,
41+
fill: '#1890FF',
42+
};
43+
},
44+
});
45+
46+
box.render();
47+
48+
const geometry = box.chart.geometries[0];
49+
const elements = geometry.elements;
50+
expect(elements[0].shape.attr('stroke')).toBe('black');
51+
expect(elements[0].shape.attr('fill')).toBe('#1890FF');
52+
});
53+
});
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Box } from '../../../../src';
2+
import { boxData, groupBoxData } from '../../../data/box';
3+
import { createDiv } from '../../../utils/dom';
4+
5+
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+
});
17+
18+
box.render();
19+
// @ts-ignore
20+
expect(box.chart.options.tooltip.title).toBe('hello world');
21+
22+
box.update({
23+
...box.options,
24+
tooltip: false,
25+
});
26+
// @ts-ignore
27+
expect(box.chart.options.tooltip).toBe(false);
28+
expect(box.chart.getComponents().find((co) => co.type === 'tooltip')).toBe(undefined);
29+
});
30+
31+
it('default toolip', () => {
32+
const box = new Box(createDiv('default tooltip'), {
33+
width: 400,
34+
height: 500,
35+
data: groupBoxData,
36+
xField: 'type',
37+
yField: '_bin',
38+
groupField: 'Species',
39+
});
40+
41+
box.render();
42+
// @ts-ignore
43+
expect(box.chart.options.tooltip.shared).toBe(true);
44+
// @ts-ignore
45+
expect(box.chart.options.tooltip.showCrosshairs).toBe(true);
46+
});
47+
});

examples/box/basic/demo/basic.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
boxStyle: {
21+
stroke: '#545454',
22+
fill: '#1890FF',
23+
fillOpacity: 0.3,
24+
},
25+
animation: false,
26+
});
27+
28+
boxPlot.render();

0 commit comments

Comments
 (0)