Skip to content

Commit 758b4e3

Browse files
authored
feat(v2/column): support grouped and stacked column (#1333)
1 parent e7f408a commit 758b4e3

File tree

6 files changed

+272
-16
lines changed

6 files changed

+272
-16
lines changed

__tests__/data/sales.ts

+93
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,96 @@ export const salesByArea = [
66
{ area: '西北', sales: 815039.5959999998 },
77
{ area: '西南', sales: 1303124.508000002 },
88
];
9+
10+
export const subSalesByArea = [
11+
{
12+
area: '东北',
13+
series: '消费者',
14+
sales: 1323985.6069999991,
15+
},
16+
{
17+
area: '东北',
18+
series: '小型企业',
19+
sales: 522739.0349999995,
20+
},
21+
{
22+
area: '东北',
23+
series: '公司',
24+
sales: 834842.827,
25+
},
26+
{
27+
area: '中南',
28+
series: '消费者',
29+
sales: 2057936.7620000008,
30+
},
31+
{
32+
area: '中南',
33+
series: '小型企业',
34+
sales: 743813.0069999992,
35+
},
36+
{
37+
area: '中南',
38+
series: '公司',
39+
sales: 1335665.3239999984,
40+
},
41+
{
42+
area: '华东',
43+
series: '消费者',
44+
sales: 2287358.261999998,
45+
},
46+
{
47+
area: '华东',
48+
series: '小型企业',
49+
sales: 942432.3720000006,
50+
},
51+
{
52+
area: '华东',
53+
series: '公司',
54+
sales: 1454715.807999998,
55+
},
56+
{
57+
area: '华北',
58+
series: '消费者',
59+
sales: 1220430.5610000012,
60+
},
61+
{
62+
area: '华北',
63+
series: '小型企业',
64+
sales: 422100.9870000001,
65+
},
66+
{
67+
area: '华北',
68+
series: '公司',
69+
sales: 804769.4689999995,
70+
},
71+
{
72+
area: '西北',
73+
series: '消费者',
74+
sales: 458058.1039999998,
75+
},
76+
{
77+
area: '西北',
78+
series: '小型企业',
79+
sales: 103523.308,
80+
},
81+
{
82+
area: '西北',
83+
series: '公司',
84+
sales: 253458.1840000001,
85+
},
86+
{
87+
area: '西南',
88+
series: '消费者',
89+
sales: 677302.8919999995,
90+
},
91+
{
92+
area: '西南',
93+
series: '小型企业',
94+
sales: 156479.9319999999,
95+
},
96+
{
97+
area: '西南',
98+
series: '公司',
99+
sales: 469341.684,
100+
},
101+
];

__tests__/unit/plots/column/index-spec.ts

+45-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Column } from '../../../../src';
2-
import { salesByArea } from '../../../data/sales';
2+
import { salesByArea, subSalesByArea } from '../../../data/sales';
33
import { createDiv } from '../../../utils/dom';
44

55
describe('column', () => {
66
it('x*y', () => {
7-
const column = new Column(createDiv(), {
7+
const column = new Column(createDiv('x*y'), {
88
width: 400,
99
height: 300,
1010
data: salesByArea,
@@ -28,7 +28,7 @@ describe('column', () => {
2828
});
2929

3030
it('x*y*color', () => {
31-
const column = new Column(createDiv(), {
31+
const column = new Column(createDiv('x*y*color'), {
3232
width: 400,
3333
height: 300,
3434
data: salesByArea,
@@ -48,7 +48,7 @@ describe('column', () => {
4848

4949
it('x*y*color with color', () => {
5050
const palette = ['red', 'yellow', 'green'];
51-
const column = new Column(createDiv(), {
51+
const column = new Column(createDiv('x*y*color with color'), {
5252
width: 400,
5353
height: 300,
5454
data: salesByArea,
@@ -71,4 +71,45 @@ describe('column', () => {
7171
expect(color).toBe(palette[index % palette.length]);
7272
});
7373
});
74+
75+
it('grouped column', () => {
76+
const column = new Column(createDiv('grouped column'), {
77+
width: 400,
78+
height: 300,
79+
data: subSalesByArea,
80+
xField: 'area',
81+
yField: 'sales',
82+
colorField: 'series',
83+
});
84+
85+
column.render();
86+
87+
const geometry = column.chart.geometries[0];
88+
expect(geometry.getAdjust('dodge')).toMatchObject({
89+
xField: 'area',
90+
yField: 'sales',
91+
});
92+
expect(geometry.getAdjust('stack')).toBeUndefined();
93+
});
94+
95+
it('stacked column', () => {
96+
const column = new Column(createDiv('stacked column'), {
97+
width: 400,
98+
height: 300,
99+
data: subSalesByArea,
100+
xField: 'area',
101+
yField: 'sales',
102+
colorField: 'series',
103+
isStack: true,
104+
});
105+
106+
column.render();
107+
108+
const geometry = column.chart.geometries[0];
109+
expect(geometry.getAdjust('dodge')).toBeUndefined();
110+
expect(geometry.getAdjust('stack')).toMatchObject({
111+
xField: 'area',
112+
yField: 'sales',
113+
});
114+
});
74115
});

__tests__/unit/plots/column/label-spec.ts

+112-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Column } from '../../../../src';
2-
import { salesByArea } from '../../../data/sales';
2+
import { salesByArea, subSalesByArea } from '../../../data/sales';
33
import { createDiv } from '../../../utils/dom';
44

55
describe('column label', () => {
6-
it('position: top', () => {
7-
const column = new Column(createDiv(), {
6+
it('position top', () => {
7+
const column = new Column(createDiv('position top'), {
88
width: 400,
99
height: 300,
1010
data: salesByArea,
@@ -36,8 +36,8 @@ describe('column label', () => {
3636
});
3737
});
3838

39-
it('label position middle', () => {
40-
const column = new Column(createDiv(), {
39+
it('position middle', () => {
40+
const column = new Column(createDiv('position middle'), {
4141
width: 400,
4242
height: 300,
4343
data: salesByArea,
@@ -62,8 +62,8 @@ describe('column label', () => {
6262
expect(geometry.labelOption.cfg).toEqual({ position: 'middle' });
6363
});
6464

65-
it('label position bottom', () => {
66-
const column = new Column(createDiv(), {
65+
it('position bottom', () => {
66+
const column = new Column(createDiv('position bottom'), {
6767
width: 400,
6868
height: 300,
6969
data: salesByArea,
@@ -87,4 +87,109 @@ describe('column label', () => {
8787
// @ts-ignore
8888
expect(geometry.labelOption.cfg).toEqual({ position: 'bottom' });
8989
});
90+
91+
it('group column position top', () => {
92+
const column = new Column(createDiv('group column position top'), {
93+
width: 400,
94+
height: 300,
95+
data: subSalesByArea,
96+
xField: 'area',
97+
yField: 'sales',
98+
colorField: 'series',
99+
meta: {
100+
sales: {
101+
nice: true,
102+
formatter: (v) => `${Math.floor(v / 10000)}万`,
103+
},
104+
},
105+
label: {
106+
position: 'top',
107+
},
108+
});
109+
110+
column.render();
111+
112+
const geometry = column.chart.geometries[0];
113+
const labelGroups = geometry.labelsContainer.getChildren();
114+
115+
// @ts-ignore
116+
expect(geometry.labelOption.cfg).toEqual({
117+
position: 'top',
118+
});
119+
expect(labelGroups).toHaveLength(subSalesByArea.length);
120+
labelGroups.forEach((label) => {
121+
const origin = label.get('origin')._origin;
122+
expect(label.get('children')[0].attr('text')).toBe(`${Math.floor(origin.sales / 10000)}万`);
123+
});
124+
});
125+
126+
it('group column position middle', () => {
127+
const column = new Column(createDiv('group column position middle'), {
128+
width: 400,
129+
height: 300,
130+
data: subSalesByArea,
131+
xField: 'area',
132+
yField: 'sales',
133+
colorField: 'series',
134+
meta: {
135+
sales: {
136+
nice: true,
137+
formatter: (v) => `${Math.floor(v / 10000)}万`,
138+
},
139+
},
140+
label: {
141+
position: 'middle',
142+
},
143+
});
144+
145+
column.render();
146+
147+
const geometry = column.chart.geometries[0];
148+
const labelGroups = geometry.labelsContainer.getChildren();
149+
150+
// @ts-ignore
151+
expect(geometry.labelOption.cfg).toEqual({
152+
position: 'middle',
153+
});
154+
expect(labelGroups).toHaveLength(subSalesByArea.length);
155+
labelGroups.forEach((label) => {
156+
const origin = label.get('origin')._origin;
157+
expect(label.get('children')[0].attr('text')).toBe(`${Math.floor(origin.sales / 10000)}万`);
158+
});
159+
});
160+
161+
it('group column position bottom', () => {
162+
const column = new Column(createDiv('group column position bottom'), {
163+
width: 400,
164+
height: 300,
165+
data: subSalesByArea,
166+
xField: 'area',
167+
yField: 'sales',
168+
colorField: 'series',
169+
meta: {
170+
sales: {
171+
nice: true,
172+
formatter: (v) => `${Math.floor(v / 10000)}万`,
173+
},
174+
},
175+
label: {
176+
position: 'bottom',
177+
},
178+
});
179+
180+
column.render();
181+
182+
const geometry = column.chart.geometries[0];
183+
const labelGroups = geometry.labelsContainer.getChildren();
184+
185+
// @ts-ignore
186+
expect(geometry.labelOption.cfg).toEqual({
187+
position: 'bottom',
188+
});
189+
expect(labelGroups).toHaveLength(subSalesByArea.length);
190+
labelGroups.forEach((label) => {
191+
const origin = label.get('origin')._origin;
192+
expect(label.get('children')[0].attr('text')).toBe(`${Math.floor(origin.sales / 10000)}万`);
193+
});
194+
});
90195
});

__tests__/utils/dom.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
/**
22
* 创建一个 div 节点,并放到 container,默认放到 body 上
3+
* @param title
34
* @param container
45
*/
5-
export function createDiv(container: HTMLElement = document.body): HTMLElement {
6+
export function createDiv(title: string = '', container: HTMLElement = document.body): HTMLElement {
67
const div = document.createElement('div');
78

9+
if (title) {
10+
const titleDiv = document.createElement('div').appendChild(document.createTextNode(title));
11+
container.appendChild(titleDiv);
12+
}
13+
814
container.appendChild(div);
915

1016
return div;

src/plots/column/adaptor.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Geometry, Chart } from '@antv/g2';
22
import { deepMix, isFunction } from '@antv/util';
33
import { Params } from '../../core/adaptor';
44
import { findGeometry } from '../../common/helper';
5+
import { tooltip, interaction, animation, theme } from '../../common/adaptor';
56
import { flow, pick } from '../../utils';
67
import { ColumnOptions } from './types';
78
import { AXIS_META_CONFIG_KEYS } from '../../constant';
@@ -12,7 +13,7 @@ import { AXIS_META_CONFIG_KEYS } from '../../constant';
1213
*/
1314
function field(params: Params<ColumnOptions>): Params<ColumnOptions> {
1415
const { chart, options } = params;
15-
const { data, xField, yField, colorField, color } = options;
16+
const { data, xField, yField, colorField, color, isStack } = options;
1617

1718
chart.data(data);
1819
const geometry = chart.interval().position(`${xField}*${yField}`);
@@ -21,6 +22,10 @@ function field(params: Params<ColumnOptions>): Params<ColumnOptions> {
2122
geometry.color(colorField, color);
2223
}
2324

25+
if (colorField && ![xField, yField].includes(colorField)) {
26+
geometry.adjust(isStack ? 'stack' : 'dodge');
27+
}
28+
2429
return params;
2530
}
2631

@@ -129,5 +134,5 @@ function label(params: Params<ColumnOptions>): Params<ColumnOptions> {
129134
* @param params
130135
*/
131136
export function adaptor(params: Params<ColumnOptions>) {
132-
return flow(field, meta, axis, legend, style, label)(params);
137+
return flow(field, meta, axis, legend, tooltip, theme, style, label, interaction, animation)(params);
133138
}

src/plots/column/types.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ import { ShapeStyle } from '../../types/style';
33

44
export interface ColumnOptions extends Options {
55
/** x 轴字段 */
6-
readonly xField?: string;
6+
readonly xField: string;
77
/** y 轴字段 */
8-
readonly yField?: string;
8+
readonly yField: string;
99
/** 颜色字段,可选 */
1010
readonly colorField?: string;
11+
/** 是否 堆积柱状图, 默认 分组柱状图 */
12+
readonly isStack?: boolean;
13+
/** 柱子宽度占比 [0-1] */
14+
readonly marginRatio?: number;
15+
/** 分组或堆叠内部的间距,像素值 */
16+
readonly innerPadding?: number;
1117
/** 柱子样式配置,可选 */
1218
readonly columnStyle?: ShapeStyle | ((x: any, y: any, color?: any) => ShapeStyle);
1319
}

0 commit comments

Comments
 (0)