Skip to content

Commit 62683d5

Browse files
authored
feat(v2/column): 添加基础柱形图 (#1316)
* feat(v2/column): add column plot * feat(v2/column): export ColumnOptions & add findGeometry helper * feat(v2/column): add label formatter unit test * feat(v2/column): simplify findGeometry signature * feat(v2/column): changes with cr
1 parent 0b481d5 commit 62683d5

File tree

13 files changed

+507
-7
lines changed

13 files changed

+507
-7
lines changed

__tests__/data/sales.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const salesByArea = [
2+
{ area: '东北', sales: 2681567.469000001 },
3+
{ area: '中南', sales: 4137415.0929999948 },
4+
{ area: '华东', sales: 4684506.442 },
5+
{ area: '华北', sales: 2447301.017000004 },
6+
{ area: '西北', sales: 815039.5959999998 },
7+
{ area: '西南', sales: 1303124.508000002 },
8+
];
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Column } from '../../../../src';
2+
import { salesByArea } from '../../../data/sales';
3+
import { createDiv } from '../../../utils/dom';
4+
5+
describe('column axis', () => {
6+
it('meta', () => {
7+
const formatter = (v) => `${Math.floor(v / 10000)}万`;
8+
const column = new Column(createDiv(), {
9+
width: 400,
10+
height: 300,
11+
data: salesByArea,
12+
xField: 'area',
13+
yField: 'sales',
14+
meta: {
15+
sales: {
16+
nice: true,
17+
formatter,
18+
},
19+
},
20+
});
21+
22+
column.render();
23+
24+
const geometry = column.chart.geometries[0];
25+
// @ts-ignore
26+
expect(geometry.scales.sales.nice).toBe(true);
27+
expect(geometry.scales.sales.formatter).toBe(formatter);
28+
});
29+
30+
it('xAxis', () => {
31+
const column = new Column(createDiv(), {
32+
width: 400,
33+
height: 300,
34+
data: salesByArea,
35+
xField: 'area',
36+
yField: 'sales',
37+
xAxis: {
38+
label: {
39+
rotate: -Math.PI / 2,
40+
},
41+
},
42+
});
43+
44+
column.render();
45+
const axisOptions = column.chart.getOptions().axes;
46+
47+
// @ts-ignore
48+
expect(axisOptions.area.label.rotate).toBe(-Math.PI / 2);
49+
});
50+
51+
it('yAxis', () => {
52+
const column = new Column(createDiv(), {
53+
width: 400,
54+
height: 300,
55+
data: salesByArea,
56+
xField: 'area',
57+
yField: 'sales',
58+
yAxis: {
59+
minLimit: 10000,
60+
nice: true,
61+
},
62+
});
63+
64+
column.render();
65+
66+
const geometry = column.chart.geometries[0];
67+
const axisOptions = column.chart.getOptions().axes;
68+
69+
// @ts-ignore
70+
expect(axisOptions.sales.minLimit).toBe(10000);
71+
expect(geometry.scales.sales.minLimit).toBe(10000);
72+
// @ts-ignore
73+
expect(geometry.scales.sales.nice).toBe(true);
74+
});
75+
});
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Column } from '../../../../src';
2+
import { salesByArea } from '../../../data/sales';
3+
import { createDiv } from '../../../utils/dom';
4+
5+
describe('column', () => {
6+
it('x*y', () => {
7+
const column = new Column(createDiv(), {
8+
width: 400,
9+
height: 300,
10+
data: salesByArea,
11+
xField: 'area',
12+
yField: 'sales',
13+
});
14+
15+
column.render();
16+
17+
const geometry = column.chart.geometries[0];
18+
const positionFields = geometry.getAttribute('position').getFields();
19+
20+
// 类型
21+
expect(geometry.type).toBe('interval');
22+
// 图形元素个数
23+
expect(column.chart.geometries[0].elements.length).toBe(salesByArea.length);
24+
// x & y
25+
expect(positionFields).toHaveLength(2);
26+
expect(positionFields[0]).toBe('area');
27+
expect(positionFields[1]).toBe('sales');
28+
});
29+
30+
it('x*y*color', () => {
31+
const column = new Column(createDiv(), {
32+
width: 400,
33+
height: 300,
34+
data: salesByArea,
35+
xField: 'area',
36+
yField: 'sales',
37+
colorField: 'area',
38+
});
39+
40+
column.render();
41+
42+
const geometry = column.chart.geometries[0];
43+
const colorFields = geometry.getAttribute('color').getFields();
44+
45+
expect(colorFields).toHaveLength(1);
46+
expect(colorFields[0]).toBe('area');
47+
});
48+
49+
it('x*y*color with color', () => {
50+
const palette = ['red', 'yellow', 'green'];
51+
const column = new Column(createDiv(), {
52+
width: 400,
53+
height: 300,
54+
data: salesByArea,
55+
xField: 'area',
56+
yField: 'sales',
57+
colorField: 'area',
58+
color: palette,
59+
});
60+
61+
column.render();
62+
63+
const geometry = column.chart.geometries[0];
64+
const colorAttribute = geometry.getAttribute('color');
65+
const colorFields = colorAttribute.getFields();
66+
67+
expect(colorFields).toHaveLength(1);
68+
expect(colorFields[0]).toBe('area');
69+
geometry.elements.forEach((element, index) => {
70+
const color = element.getModel().color;
71+
expect(color).toBe(palette[index % palette.length]);
72+
});
73+
});
74+
});
+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { Column } from '../../../../src';
2+
import { salesByArea } from '../../../data/sales';
3+
import { createDiv } from '../../../utils/dom';
4+
5+
describe('column label', () => {
6+
it('position: top', () => {
7+
const column = new Column(createDiv(), {
8+
width: 400,
9+
height: 300,
10+
data: salesByArea,
11+
xField: 'area',
12+
yField: 'sales',
13+
meta: {
14+
sales: {
15+
nice: true,
16+
formatter: (v) => `${Math.floor(v / 10000)}万`,
17+
},
18+
},
19+
label: {
20+
position: 'top',
21+
},
22+
});
23+
24+
column.render();
25+
26+
const geometry = column.chart.geometries[0];
27+
const labelGroups = geometry.labelsContainer.getChildren();
28+
29+
// @ts-ignore
30+
expect(geometry.labelOption.cfg).toEqual({
31+
position: 'top',
32+
});
33+
expect(labelGroups).toHaveLength(salesByArea.length);
34+
labelGroups.forEach((label, index) => {
35+
expect(label.get('children')[0].attr('text')).toBe(`${Math.floor(salesByArea[index].sales / 10000)}万`);
36+
});
37+
});
38+
39+
it('label position middle', () => {
40+
const column = new Column(createDiv(), {
41+
width: 400,
42+
height: 300,
43+
data: salesByArea,
44+
xField: 'area',
45+
yField: 'sales',
46+
meta: {
47+
sales: {
48+
nice: true,
49+
formatter: (v) => `${Math.floor(v / 10000)}万`,
50+
},
51+
},
52+
label: {
53+
position: 'middle',
54+
},
55+
});
56+
57+
column.render();
58+
59+
const geometry = column.chart.geometries[0];
60+
61+
// @ts-ignore
62+
expect(geometry.labelOption.cfg).toEqual({ position: 'middle' });
63+
});
64+
65+
it('label position bottom', () => {
66+
const column = new Column(createDiv(), {
67+
width: 400,
68+
height: 300,
69+
data: salesByArea,
70+
xField: 'area',
71+
yField: 'sales',
72+
meta: {
73+
sales: {
74+
nice: true,
75+
formatter: (v) => `${Math.floor(v / 10000)}万`,
76+
},
77+
},
78+
label: {
79+
position: 'bottom',
80+
},
81+
});
82+
83+
column.render();
84+
85+
const geometry = column.chart.geometries[0];
86+
87+
// @ts-ignore
88+
expect(geometry.labelOption.cfg).toEqual({ position: 'bottom' });
89+
});
90+
});
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Column } from '../../../../src';
2+
import { salesByArea } from '../../../data/sales';
3+
import { createDiv } from '../../../utils/dom';
4+
5+
describe('column style', () => {
6+
it('style config', () => {
7+
const column = new Column(createDiv(), {
8+
width: 400,
9+
height: 300,
10+
data: salesByArea,
11+
xField: 'area',
12+
yField: 'sales',
13+
meta: {
14+
sales: {
15+
nice: true,
16+
formatter: (v) => `${Math.floor(v / 10000)}万`,
17+
},
18+
},
19+
columnStyle: {
20+
stroke: 'black',
21+
lineWidth: 2,
22+
},
23+
});
24+
25+
column.render();
26+
27+
const geometry = column.chart.geometries[0];
28+
const elements = geometry.elements;
29+
expect(elements[0].shape.attr('stroke')).toBe('black');
30+
expect(elements[0].shape.attr('lineWidth')).toBe(2);
31+
});
32+
33+
it('style callback', () => {
34+
const column = new Column(createDiv(), {
35+
width: 400,
36+
height: 300,
37+
data: salesByArea,
38+
xField: 'area',
39+
yField: 'sales',
40+
meta: {
41+
sales: {
42+
nice: true,
43+
formatter: (v) => `${Math.floor(v / 10000)}万`,
44+
},
45+
},
46+
columnStyle: (x, y) => {
47+
return {
48+
stroke: 'black',
49+
lineWidth: 2,
50+
};
51+
},
52+
});
53+
54+
column.render();
55+
56+
const geometry = column.chart.geometries[0];
57+
const elements = geometry.elements;
58+
expect(elements[0].shape.attr('stroke')).toBe('black');
59+
expect(elements[0].shape.attr('lineWidth')).toBe(2);
60+
});
61+
});

src/common/helper.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Chart, Geometry } from '@antv/g2';
2+
3+
/**
4+
* 在 Chart 中查找第一个指定 type 类型的 geometry
5+
* @param chart
6+
* @param type
7+
*/
8+
export function findGeometry(chart: Chart, type: string): Geometry {
9+
return chart.geometries.find((g: Geometry) => g.type === type);
10+
}

src/constant.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* 需要从轴配置中提取出来作为 meta 的属性 key 列表
3+
*/
4+
export const AXIS_META_CONFIG_KEYS = [
5+
'tickCount',
6+
'tickInterval',
7+
'min',
8+
'max',
9+
'nice',
10+
'minLimit',
11+
'maxLimit',
12+
'tickMethod',
13+
];

src/core/plot.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import { ChartOptions, Data } from '../types';
88
*/
99
export abstract class Plot<O extends ChartOptions> {
1010
/** plot 类型名称 */
11-
public abstract type: string = 'base';
11+
public abstract readonly type: string = 'base';
1212
/** plot 的 schema 配置 */
1313
public options: O;
1414
/** plot 绘制的 dom */
15-
public container: HTMLElement;
15+
public readonly container: HTMLElement;
1616
/** G2 chart 实例 */
1717
public chart: Chart;
1818
/** resizer unbind */

src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export * from './types';
99
// 折线图及类型定义
1010
export { Line, LineOptions } from './plots/line';
1111

12+
// 柱形图及类型定义
13+
export { Column, ColumnOptions } from './plots/column';
14+
1215
// 饼图及类型定义
1316
export { Pie, PieOptions } from './plots/pie';
1417

0 commit comments

Comments
 (0)