Skip to content

Commit 2c0f56d

Browse files
authored
feat: 迁移转化率组件 (#1772)
* feat: support conversion tag for column and bar * feat: add conversion tag demo * feat: add conversion tag demo * fix: add unit test for update
1 parent 8d4b729 commit 2c0f56d

File tree

13 files changed

+661
-20
lines changed

13 files changed

+661
-20
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import { IGroup } from '@antv/g2/lib/dependents';
2+
import { Column, ColumnOptions, Bar, BarOptions } from '../../../src';
3+
import { createDiv } from '../../utils/dom';
4+
import { delay } from '../../utils/delay';
5+
import { near } from '../../utils/number';
6+
7+
const DATA = [
8+
{ x: 'A', y: 100 },
9+
{ x: 'B', y: 120 },
10+
{ x: 'C', y: 80 },
11+
];
12+
13+
const DATA_WITH_ZERO = [
14+
{ action: '浏览网站', pv: 12000000 },
15+
{ action: '放入购物车', pv: 8000000 },
16+
{ action: '生成订单', pv: 6000000 },
17+
{ action: '支付订单', pv: 0 },
18+
{ action: '完成交易', pv: 0 },
19+
];
20+
21+
describe('column conversion tag', () => {
22+
const container = createDiv();
23+
24+
const options: ColumnOptions = {
25+
data: DATA,
26+
autoFit: false,
27+
width: 600,
28+
height: 400,
29+
xField: 'x',
30+
yField: 'y',
31+
// label: {},
32+
yAxis: {
33+
nice: true,
34+
},
35+
conversionTag: {},
36+
};
37+
const plot = new Column(container, options);
38+
39+
it('render', async () => {
40+
plot.render();
41+
await delay(500);
42+
43+
const shapes = plot.chart.geometries[0].getShapes();
44+
const shapeBBoxes = shapes.map((shape) => shape.getBBox());
45+
const totalWidth = shapeBBoxes[1].minX - shapeBBoxes[0].maxX;
46+
const foreground = plot.chart.foregroundGroup;
47+
48+
// 整体
49+
const group: IGroup = foreground.findAllByName('conversion-tag-group')[0] as IGroup;
50+
expect(group).toBeDefined();
51+
// 每两条数据之间一个转化率标记,每一个标记有 text/arrow 两个 shape
52+
expect(group.getChildren()).toHaveLength((DATA.length - 1) * 2);
53+
54+
// 文本
55+
const texts = group.findAllByName('conversion-tag-text');
56+
expect(texts).toHaveLength(DATA.length - 1);
57+
DATA.forEach((datum, idx) => {
58+
if (idx > 0) {
59+
expect(texts[idx - 1].get('type')).toBe('text');
60+
expect(texts[idx - 1].attr('text')).toBe(((DATA[idx].y / DATA[idx - 1].y) * 100).toFixed(2) + '%');
61+
}
62+
});
63+
64+
// 箭头
65+
const arrows = group.findAllByName('conversion-tag-arrow');
66+
expect(arrows).toHaveLength(DATA.length - 1);
67+
arrows.forEach((arrow) => {
68+
const bbox = arrow.getBBox();
69+
// spacing: 8
70+
expect(near(bbox.width, totalWidth - 8 * 2)).toBeTruthy();
71+
// size: 32
72+
expect(near(bbox.height, 32)).toBeTruthy();
73+
});
74+
});
75+
76+
it('update', async () => {
77+
plot.update({
78+
...options,
79+
conversionTag: {
80+
spacing: 12,
81+
size: 40,
82+
},
83+
});
84+
plot.render();
85+
86+
await delay(500);
87+
88+
const shapes = plot.chart.geometries[0].getShapes();
89+
const shapeBBoxes = shapes.map((shape) => shape.getBBox());
90+
const totalWidth = shapeBBoxes[1].minX - shapeBBoxes[0].maxX;
91+
const foreground = plot.chart.foregroundGroup;
92+
93+
// 整体
94+
const group: IGroup = foreground.findAllByName('conversion-tag-group')[0] as IGroup;
95+
expect(group).toBeDefined();
96+
// 每两条数据之间一个转化率标记,每一个标记有 text/arrow 两个 shape
97+
expect(group.getChildren()).toHaveLength((DATA.length - 1) * 2);
98+
99+
// 文本
100+
const texts = group.findAllByName('conversion-tag-text');
101+
expect(texts).toHaveLength(DATA.length - 1);
102+
DATA.slice()
103+
.reverse()
104+
.forEach((datum, idx) => {
105+
if (idx > 0) {
106+
expect(texts[idx - 1].get('type')).toBe('text');
107+
expect(texts[idx - 1].attr('text')).toBe(((DATA[idx].y / DATA[idx - 1].y) * 100).toFixed(2) + '%');
108+
}
109+
});
110+
111+
// 箭头
112+
const arrows = group.findAllByName('conversion-tag-arrow');
113+
expect(arrows).toHaveLength(DATA.length - 1);
114+
arrows.forEach((arrow) => {
115+
const bbox = arrow.getBBox();
116+
// spacing: 12
117+
expect(near(bbox.width, totalWidth - 12 * 2)).toBeTruthy();
118+
// size: 40
119+
expect(near(bbox.height, 40)).toBeTruthy();
120+
});
121+
});
122+
123+
it('clear', async () => {
124+
plot.update({
125+
...options,
126+
conversionTag: false,
127+
});
128+
plot.render();
129+
130+
await delay(500);
131+
132+
const foreground = plot.chart.foregroundGroup;
133+
const group: IGroup = foreground.findAllByName('conversion-tag-group')[0] as IGroup;
134+
expect(group).toBeUndefined();
135+
});
136+
});
137+
138+
describe('bar conversion tag', () => {
139+
const container = createDiv();
140+
141+
const options: BarOptions = {
142+
data: DATA,
143+
autoFit: false,
144+
width: 600,
145+
height: 400,
146+
xField: 'y',
147+
yField: 'x',
148+
label: {},
149+
yAxis: {
150+
nice: true,
151+
},
152+
conversionTag: {},
153+
animation: false,
154+
};
155+
const plot = new Bar(container, options);
156+
const DATA_REVERSED = DATA.slice().reverse();
157+
158+
it('render', async () => {
159+
plot.render();
160+
161+
await delay(500);
162+
163+
const shapes = plot.chart.geometries[0].getShapes();
164+
const shapeBBoxes = shapes.map((shape) => shape.getBBox());
165+
const totalHeight = shapeBBoxes[0].minY - shapeBBoxes[1].maxY;
166+
const foreground = plot.chart.foregroundGroup;
167+
168+
// 整体
169+
const group: IGroup = foreground.findAllByName('conversion-tag-group')[0] as IGroup;
170+
expect(group).toBeDefined();
171+
// 每两条数据之间一个转化率标记,每一个标记有 text/arrow 两个 shape
172+
expect(group.getChildren()).toHaveLength((DATA.length - 1) * 2);
173+
174+
// 文本
175+
const texts = group.findAllByName('conversion-tag-text');
176+
expect(texts).toHaveLength(DATA.length - 1);
177+
DATA_REVERSED.forEach((datum, idx) => {
178+
if (idx > 0) {
179+
expect(texts[idx - 1].get('type')).toBe('text');
180+
expect(texts[idx - 1].attr('text')).toBe(
181+
((DATA_REVERSED[idx].y / DATA_REVERSED[idx - 1].y) * 100).toFixed(2) + '%'
182+
);
183+
}
184+
});
185+
186+
// 箭头
187+
const arrows = group.findAllByName('conversion-tag-arrow');
188+
expect(arrows).toHaveLength(DATA.length - 1);
189+
arrows.forEach((arrow) => {
190+
const bbox = arrow.getBBox();
191+
// spacing: 12
192+
expect(near(bbox.height, totalHeight - 12 * 2)).toBeTruthy();
193+
// size: 80
194+
expect(near(bbox.width, 80)).toBeTruthy();
195+
});
196+
});
197+
198+
it('update', async () => {
199+
plot.update({
200+
...options,
201+
conversionTag: {
202+
size: 100,
203+
spacing: 24,
204+
},
205+
});
206+
plot.render();
207+
208+
await delay(500);
209+
210+
const shapes = plot.chart.geometries[0].getShapes();
211+
const shapeBBoxes = shapes.map((shape) => shape.getBBox());
212+
const totalHeight = shapeBBoxes[0].minY - shapeBBoxes[1].maxY;
213+
const foreground = plot.chart.foregroundGroup;
214+
215+
// 整体
216+
const group: IGroup = foreground.findAllByName('conversion-tag-group')[0] as IGroup;
217+
expect(group).toBeDefined();
218+
// 每两条数据之间一个转化率标记,每一个标记有 text/arrow 两个 shape
219+
expect(group.getChildren()).toHaveLength((DATA.length - 1) * 2);
220+
221+
// 文本
222+
const texts = group.findAllByName('conversion-tag-text');
223+
expect(texts).toHaveLength(DATA.length - 1);
224+
DATA_REVERSED.forEach((datum, idx) => {
225+
if (idx > 0) {
226+
expect(texts[idx - 1].get('type')).toBe('text');
227+
expect(texts[idx - 1].attr('text')).toBe(
228+
((DATA_REVERSED[idx].y / DATA_REVERSED[idx - 1].y) * 100).toFixed(2) + '%'
229+
);
230+
}
231+
});
232+
233+
// 箭头
234+
const arrows = group.findAllByName('conversion-tag-arrow');
235+
expect(arrows).toHaveLength(DATA.length - 1);
236+
arrows.forEach((arrow) => {
237+
const bbox = arrow.getBBox();
238+
// spacing: 24
239+
expect(near(bbox.height, totalHeight - 24 * 2)).toBeTruthy();
240+
// size: 100
241+
expect(near(bbox.width, 100)).toBeTruthy();
242+
});
243+
});
244+
245+
it('clear', async () => {
246+
plot.update({
247+
...options,
248+
conversionTag: false,
249+
});
250+
plot.render();
251+
252+
await delay(500);
253+
254+
const foreground = plot.chart.foregroundGroup;
255+
const group: IGroup = foreground.findAllByName('conversion-tag-group')[0] as IGroup;
256+
expect(group).toBeUndefined();
257+
});
258+
});
259+
260+
describe('zero data no NaN', () => {
261+
const container = createDiv();
262+
263+
const DATA_REVERSED = DATA_WITH_ZERO.slice().reverse();
264+
const plot = new Bar(container, {
265+
data: DATA_REVERSED,
266+
autoFit: false,
267+
width: 600,
268+
height: 400,
269+
xField: 'pv',
270+
yField: 'action',
271+
label: {},
272+
yAxis: {
273+
nice: true,
274+
},
275+
conversionTag: {},
276+
});
277+
plot.render();
278+
279+
it('text', () => {
280+
const foreground = plot.chart.foregroundGroup;
281+
282+
// 整体
283+
const group: IGroup = foreground.findAllByName('conversion-tag-group')[0] as IGroup;
284+
// 文本
285+
const texts = group.findAllByName('conversion-tag-text');
286+
expect(texts).toHaveLength(DATA_WITH_ZERO.length - 1);
287+
DATA_WITH_ZERO.forEach((datum, idx) => {
288+
if (idx > 0) {
289+
const prev = DATA_WITH_ZERO[idx - 1].pv;
290+
const next = DATA_WITH_ZERO[idx].pv;
291+
let v;
292+
if (prev === next) {
293+
v = '0.00%';
294+
} else if (prev === 0) {
295+
v = '∞';
296+
} else if (next === 0) {
297+
v = '-∞';
298+
} else {
299+
v = ((next / prev) * 100).toFixed(2) + '%';
300+
}
301+
expect(texts[idx - 1].get('type')).toBe('text');
302+
expect(texts[idx - 1].attr('text')).toBe(v);
303+
}
304+
});
305+
});
306+
});

__tests__/unit/core/index-spec.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { Line, G2, Pie } from '../../../src';
1+
import LineAnnotation from '@antv/component/lib/annotation/line';
2+
import { deepMix, isEqual, clone } from '@antv/util';
3+
import { Line, G2, Pie, line } from '../../../src';
24
import { partySupport } from '../../data/party-support';
35
import { salesByArea } from '../../data/sales';
46
import { createDiv } from '../../utils/dom';
@@ -37,6 +39,26 @@ describe('core', () => {
3739
expect(line.chart.width).toBe(400);
3840
});
3941

42+
it('update mix with default options', () => {
43+
const options = {
44+
width: 400,
45+
height: 300,
46+
data: partySupport.filter((o) => o.type === 'FF'),
47+
xField: 'date',
48+
yField: 'value',
49+
};
50+
const line = new Line(createDiv(), options);
51+
52+
line.render();
53+
const curOptions = clone(line.options);
54+
55+
line.update({ ...options, width: 500 });
56+
57+
line.render();
58+
59+
expect(isEqual(line.options, deepMix(curOptions, { ...options, width: 500 }))).toBeTruthy();
60+
});
61+
4062
it('localRefresh', () => {
4163
const line = new Line(createDiv(), {
4264
width: 400,
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Bar } from '@antv/g2plot';
2+
3+
const data = [
4+
{ action: '完成交易', pv: 8500 },
5+
{ action: '支付订单', pv: 15000 },
6+
{ action: '生成订单', pv: 25000 },
7+
{ action: '放入购物车', pv: 35000 },
8+
{ action: '浏览网站', pv: 50000 },
9+
];
10+
11+
const barPlot = new Bar('container', {
12+
data,
13+
xField: 'pv',
14+
yField: 'action',
15+
barWidthRatio: 1 / 3,
16+
conversionTag: {},
17+
});
18+
19+
barPlot.render();

examples/bar/basic/demo/meta.json

+8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@
3535
"en": "Bar chart Bar width"
3636
},
3737
"screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*uSKqQo2lCPoAAAAAAAAAAABkARQnAQ"
38+
},
39+
{
40+
"filename": "conversion-tag.ts",
41+
"title": {
42+
"zh": "基础条形图 - 转化率",
43+
"en": "Bar chart conversion tag"
44+
},
45+
"screenshot": "https://gw.alicdn.com/tfs/TB107ZbvKT2gK0jSZFvXXXnFXXa-1344-828.png"
3846
}
3947
]
4048
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Column } from '@antv/g2plot';
2+
3+
const data = [
4+
{ action: '浏览网站', pv: 50000 },
5+
{ action: '放入购物车', pv: 35000 },
6+
{ action: '生成订单', pv: 25000 },
7+
{ action: '支付订单', pv: 15000 },
8+
{ action: '完成交易', pv: 8500 },
9+
];
10+
11+
const columnPlot = new Column('container', {
12+
data,
13+
xField: 'action',
14+
yField: 'pv',
15+
columnWidthRatio: 1 / 3,
16+
conversionTag: {},
17+
});
18+
19+
columnPlot.render();

0 commit comments

Comments
 (0)