Skip to content

Commit 113eda0

Browse files
committed
feat: add forward pyramid diagram
1 parent fdc30a6 commit 113eda0

File tree

8 files changed

+371
-2
lines changed

8 files changed

+371
-2
lines changed
Lines changed: 140 additions & 0 deletions
Loading
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { G2Spec } from '../../../src';
2+
3+
export function alphabetIntervalPyramidReverse(): G2Spec {
4+
return {
5+
type: 'interval',
6+
coordinate: {
7+
transform: [{ type: 'transpose' }],
8+
},
9+
data: [
10+
{ text: '页面', value: 200 },
11+
{ text: '页面1', value: 400 },
12+
{ text: '页面2', value: 600 },
13+
{ text: '页面3', value: 800 },
14+
],
15+
transform: [
16+
{
17+
type: 'symmetryY',
18+
},
19+
],
20+
axis: {
21+
x: false,
22+
y: false,
23+
},
24+
style: {
25+
stroke: '#ff0000',
26+
reverse: true,
27+
},
28+
encode: {
29+
x: 'text',
30+
y: 'value',
31+
color: 'text',
32+
shape: 'pyramid',
33+
},
34+
scale: {
35+
x: { paddingOuter: 0, paddingInner: 0 },
36+
color: { type: 'ordinal', range: ['red', 'green', 'blue', '#e45ca2'] },
37+
},
38+
};
39+
}

__tests__/plots/static/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export { alphabetIntervalLabelOverflowStroke } from './alphabet-interval-label-o
1313
export { alphabetIntervalDataSort } from './alphabet-interval-data-sort';
1414
export { alphabetIntervalFunnel } from './alphabet-interval-funnel';
1515
export { alphabetIntervalPyramid } from './alphabet-interval-pyramid';
16+
export { alphabetIntervalPyramidReverse } from './alphabet-interval-pyramid-reverse';
1617
export { bodyPointScatterPlot } from './body-point-scatter-plot';
1718
export { bodyPointScatterPlotSizeOpacity } from './body-point-scatter-plot-size-opacity';
1819
export { bodyPointScatterPlotOpacity } from './body-point-scatter-plot-opacity';

site/docs/charts/funnel.zh.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,74 @@ chart.render();
275275
- 通过相同的颜色编码对应相同的阶段,便于比较
276276
- 可以清晰地观察到不同渠道在各个阶段的转化效率差异
277277

278+
例子 4:**金字塔模型**
279+
280+
收入金字塔结构,把整个社会按照收入高低划分为几个层级,不同收入群体在总人口中的比例和收入水平的差异,体现出贫富分化的现象。
281+
282+
```js | ob { inject: true }
283+
import { Chart } from '@antv/g2';
284+
285+
const chart = new Chart({
286+
container: 'container',
287+
theme: 'classic',
288+
});
289+
290+
chart.options({
291+
type: 'interval',
292+
coordinate: {
293+
transform: [{ type: 'transpose' }],
294+
},
295+
data: [
296+
{ text: '顶层', value: 5 },
297+
{ text: '中上层', value: 10 },
298+
{ text: '中等', value: 20 },
299+
{ text: '中下层', value: 25 },
300+
{ text: '底层', value: 40 },
301+
],
302+
transform: [
303+
{
304+
type: 'symmetryY',
305+
},
306+
],
307+
axis: {
308+
x: false,
309+
y: false,
310+
},
311+
style: {
312+
reverse: true,
313+
},
314+
encode: {
315+
x: 'text',
316+
y: 'value',
317+
color: 'text',
318+
shape: 'pyramid',
319+
},
320+
scale: {
321+
x: { paddingOuter: 0, paddingInner: 0 },
322+
color: { type: 'ordinal' },
323+
},
324+
labels: [
325+
{
326+
text: (d) => d.text,
327+
position: "inside"
328+
},
329+
{
330+
text: (d) => d.value + "%",
331+
position: "inside",
332+
style: { dy: 15 }
333+
},
334+
]
335+
})
336+
337+
chart.render();
338+
```
339+
340+
**说明**
341+
- 人数比例应逐层递减,反映“底宽顶尖”的结构
342+
- 各层级人口占比清晰标注,便于比较不同群体的数量或比例
343+
- 金字塔形状直观展现阶层分布和分层特点
344+
345+
278346
### 不适合的场景
279347

280348
例子 1: **不适合展示无序或无明显层级关系的数据**

site/examples/general/funnel/demo/meta.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@
2828
},
2929
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*VpuRSZGqnlUAAAAAAAAAAAAADmJ7AQ/original"
3030
},
31+
{
32+
"filename": "pyramid-forward.ts",
33+
"title": {
34+
"zh": "正向金字塔图",
35+
"en": "pyramid-forward"
36+
},
37+
"screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*oKnDQINKBvAAAAAAQpAAAAgAemJ7AQ/original"
38+
},
3139
{
3240
"filename": "mirror-funnel.ts",
3341
"title": {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Chart } from '@antv/g2';
2+
3+
const data = [
4+
{ text: '顶层', value: 5 },
5+
{ text: '中上层', value: 10 },
6+
{ text: '中等', value: 20 },
7+
{ text: '中下层', value: 25 },
8+
{ text: '底层', value: 40 },
9+
];
10+
11+
const chart = new Chart({
12+
container: 'container',
13+
autoFit: true,
14+
});
15+
16+
chart.coordinate({
17+
transform: [{ type: 'transpose' }],
18+
});
19+
20+
chart.data(data);
21+
22+
chart
23+
.interval()
24+
.encode('x', 'text')
25+
.encode('y', 'value')
26+
.encode('color', 'text')
27+
.encode('shape', 'pyramid')
28+
.transform({ type: 'symmetryY' })
29+
.scale('x', { paddingOuter: 0, paddingInner: 0 })
30+
.scale('color', { type: 'ordinal' })
31+
.label({
32+
text: (d) => d.text,
33+
position: 'inside',
34+
})
35+
.label({
36+
text: (d) => d.value + '%',
37+
position: 'inside',
38+
style: { dy: 15 },
39+
})
40+
.style('reverse', true)
41+
.axis(false);
42+
43+
chart.render();

src/shape/interval/funnel.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ export type FunnelOptions = {
99
adjustPoints?: (
1010
points: Vector2[],
1111
nextPoints: Vector2[],
12+
previousPoints: Vector2[],
1213
coordinate: Coordinate,
14+
reverse: boolean,
1315
) => Vector2[];
1416
[key: string]: any;
1517
};
@@ -20,15 +22,45 @@ export type FunnelOptions = {
2022
function getFunnelPoints(
2123
points: Vector2[],
2224
nextPoints: Vector2[],
25+
previousPoints: Vector2[],
2326
coordinate: Coordinate,
27+
reverse: boolean,
2428
) {
2529
const [p0, p1, p2, p3] = points;
26-
2730
if (isTranspose(coordinate)) {
31+
if (reverse) {
32+
const newP0: Vector2 = [
33+
previousPoints ? previousPoints[1][0] : p0[0],
34+
p0[1],
35+
];
36+
37+
const newP3: Vector2 = [
38+
previousPoints ? previousPoints[2][0] : p3[0],
39+
p3[1],
40+
];
41+
42+
return [newP0, p1, p2, newP3];
43+
}
44+
2845
const newP1: Vector2 = [nextPoints ? nextPoints[0][0] : p1[0], p1[1]];
2946
const newP2: Vector2 = [nextPoints ? nextPoints[3][0] : p2[0], p2[1]];
3047
return [p0, newP1, newP2, p3];
3148
}
49+
50+
if (reverse) {
51+
const newP0: Vector2 = [
52+
p0[0],
53+
previousPoints ? previousPoints[1][1] : p0[1],
54+
];
55+
56+
const newP3: Vector2 = [
57+
p3[0],
58+
previousPoints ? previousPoints[2][1] : p3[1],
59+
];
60+
61+
return [newP0, p1, p2, newP3];
62+
}
63+
3264
const newP1: Vector2 = [p1[0], nextPoints ? nextPoints[0][1] : p1[1]];
3365
const newP2: Vector2 = [p2[0], nextPoints ? nextPoints[3][1] : p2[1]];
3466
return [p0, newP1, newP2, p3];
@@ -125,7 +157,14 @@ export const Funnel: SC<FunnelOptions> = (options, context) => {
125157
const { index } = value;
126158
const { color: defaultColor, ...rest } = defaults;
127159
const nextPoints = point2d[index + 1];
128-
const funnelPoints = adjustPoints(points, nextPoints, coordinate);
160+
const previousPoints = point2d[index - 1];
161+
const funnelPoints = adjustPoints(
162+
points,
163+
nextPoints,
164+
previousPoints,
165+
coordinate,
166+
style.reverse,
167+
);
129168
const tpShape = !!isTranspose(coordinate);
130169
const [p0, p1, p2, p3] = tpShape ? reorder(funnelPoints) : funnelPoints;
131170
const { color = defaultColor, opacity } = value;

0 commit comments

Comments
 (0)