Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: funnel, dynamic-height #2146

Open
wants to merge 9 commits into
base: v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 35 additions & 11 deletions packages/plots/src/core/plots/funnel/adaptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
deepAssign,
flow,
map,
transformOptions,
maxBy,
get,
groupBy,
Expand All @@ -17,6 +16,7 @@ import { FUNNEL_CONVERSATION, FUNNEL_PERCENT, FUNNEL_MAPPING_VALUE, CUSTOM_COMVE
import { Datum } from '../../../interface';
import { compareFunnel } from './compare';
import { facetFunnel } from './facet';
import { dynamicHeightFunnel } from './dynamic-height';

type Params = Adaptor<FunnelOptions>;

Expand Down Expand Up @@ -46,7 +46,9 @@ export function adaptor(params: Params) {
};

const transformData = (params: Params) => {
const { yField, data, compareField, seriesField } = params.options;
const { yField, data, compareField, seriesField, dynamicHeight } = params.options;

if (dynamicHeight) return params;

if (compareField || seriesField) {
const maxCache = {};
Expand Down Expand Up @@ -75,9 +77,22 @@ export function adaptor(params: Params) {
* 图表差异化处理
*/
const init = (params: Params) => {
const { xField, yField, shape, isTransposed, compareField, seriesField, funnelStyle, label } = params.options;

if (compareField) {
const {
xField,
yField,
shape,
isTransposed,
compareField,
seriesField,
funnelStyle,
label,
tooltip,
dynamicHeight,
} = params.options;

if (dynamicHeight) {
dynamicHeightFunnel(params);
} else if (compareField) {
compareFunnel(params);
} else if (seriesField) {
facetFunnel(params);
Expand Down Expand Up @@ -149,13 +164,15 @@ export function adaptor(params: Params) {
tooltip: {
title: false,
items: [
(d) => ({
name: d[xField],
value: d[yField],
}),
(d) =>
isFunction(tooltip?.text)
? tooltip.text(d)
: {
name: d[xField],
value: d[yField],
},
],
},
// labels 对应 xField
labels,
};

Expand All @@ -165,7 +182,14 @@ export function adaptor(params: Params) {
}

// 漏斗图 label、conversionTag 不可被通用处理
params.options = omit(params.options, ['label', CUSTOM_COMVERSION_TAG_CONFIG, 'yField', 'xField', 'seriesField']);
params.options = omit(params.options, [
'label',
'yField',
'xField',
'seriesField',
'dynamicHeight',
CUSTOM_COMVERSION_TAG_CONFIG,
]);

return params;
};
Expand Down
21 changes: 13 additions & 8 deletions packages/plots/src/core/plots/funnel/compare.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Adaptor } from '../../types';
import { flow, map, maxBy, get, merge, conversionTagFormatter, omit, isFunction } from '../../utils';
import { flow, get, merge, conversionTagFormatter, isFunction } from '../../utils';
import type { FunnelOptions } from './type';
import { FUNNEL_CONVERSATION, FUNNEL_MAPPING_VALUE, CUSTOM_COMVERSION_TAG_CONFIG } from './constant';
import { Datum } from '../../../interface';
Expand All @@ -12,7 +12,7 @@ type Params = Adaptor<FunnelOptions>;
*/
export function compareFunnel(params: Params) {
const getBasicFunnel = (params: Params, compareField: string) => {
const { xField, yField, funnelStyle, label, isTransposed } = params.options;
const { xField, yField, funnelStyle, label, isTransposed, tooltip } = params.options;

const conversionTag = get(params.options, CUSTOM_COMVERSION_TAG_CONFIG);

Expand Down Expand Up @@ -63,10 +63,12 @@ export function compareFunnel(params: Params) {
text: (_, index) => {
return index === 0 ? compareField : '';
},
textAlign: isTransposed ? 'right' : undefined,
position: isTransposed ? 'bottom' : 'top',
dx: isTransposed ? 20 : undefined,
fontSize: 14,
position: 'top',
fillOpacity: 1,
dy: -24,
dy: isTransposed ? 100 : -24,
};

const labels = [
Expand Down Expand Up @@ -99,10 +101,13 @@ export function compareFunnel(params: Params) {
tooltip: {
title: false,
items: [
(d) => ({
name: d[xField],
value: d[yField],
}),
(d) =>
isFunction(tooltip?.text)
? tooltip.text(d)
: {
name: d[xField],
value: d[yField],
},
],
},
// labels 对应 xField
Expand Down
8 changes: 5 additions & 3 deletions packages/plots/src/core/plots/funnel/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ export const FUNNEL_CONVERSATION = '__conversion__';
// 漏斗单项占总体和的百分比,用于动态漏斗图计算高度:
// data[n][yField] / sum(data[0-n][yField])
export const FUNNEL_TOTAL_PERCENT = '__totalPercentage__';
// 漏斗多边型 x 坐标
export const PLOYGON_X = '$$x$$';
export const PLOYGON_Y = '$$y$$';

// 动态高度漏斗多边形 x 坐标
export const POLYGON_X = '__x__';
// 动态高度漏斗多边形 y 坐标
export const POLYGON_Y = '__y__';

export const CUSTOM_COMVERSION_TAG_CONFIG = '__conversionTag__';
161 changes: 161 additions & 0 deletions packages/plots/src/core/plots/funnel/dynamic-height.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import type { Adaptor } from '../../types';
import { flow, map, maxBy, get, conversionTagFormatter, isFunction, deepAssign } from '../../utils';
import type { FunnelOptions } from './type';
import {
FUNNEL_CONVERSATION,
FUNNEL_PERCENT,
FUNNEL_TOTAL_PERCENT,
CUSTOM_COMVERSION_TAG_CONFIG,
POLYGON_X,
POLYGON_Y,
} from './constant';
import { Datum } from '../../../interface';

type Params = Adaptor<FunnelOptions>;

/**
* @param chart
* @param options
*/
export function dynamicHeightFunnel(params: Params) {
const transformData = (params: Params) => {
const { options } = params;
const { data = [], yField } = options;

// 计算各数据项所占高度
const sum = data.reduce((total, item) => {
return total + (item[yField] || 0);
}, 0);

const max = maxBy(data, yField)[yField];

const formatData = map(data, (row, index) => {
// 储存四个点 x,y 坐标,方向为顺时针,即 [左上, 右上,右下,左下]
const x = [];
const y = [];

row[FUNNEL_TOTAL_PERCENT] = (row[yField] || 0) / sum;

// 获取左上角,右上角坐标
if (index) {
const preItemX = data[index - 1][POLYGON_X];
const preItemY = data[index - 1][POLYGON_Y];
x[0] = preItemX[3];
y[0] = preItemY[3];
x[1] = preItemX[2];
y[1] = preItemY[2];
} else {
x[0] = -0.5;
y[0] = 1;
x[1] = 0.5;
y[1] = 1;
}

// 获取右下角坐标
y[2] = y[1] - row[FUNNEL_TOTAL_PERCENT];
x[2] = (y[2] + 1) / 4;
y[3] = y[2];
x[3] = -x[2];

// 赋值
row[POLYGON_X] = x;
row[POLYGON_Y] = y;
row[FUNNEL_PERCENT] = (row[yField] || 0) / max;
row[FUNNEL_CONVERSATION] = [get(data, [index - 1, yField]), row[yField]];
return row;
});

options.data = formatData;

return params;
};

const init = (params: Params) => {
const { options } = params;
const { xField, yField, label, isTransposed, tooltip } = options;

const conversionTag = get(params.options, CUSTOM_COMVERSION_TAG_CONFIG);

const basicLabel = [
{
text: (d) => `${d[xField]} ${d[yField]}`,
position: 'inside',
fillOpacity: 1,
...label,
},
];

const rateLabel = [
// coversion 内容,
{
textAlign: 'left',
textBaseline: 'middle',
fill: '#aaa',
fillOpacity: 1,
connector: true,
...conversionTag,
text: (...args: [d: Datum, index: number]) =>
args[1] !== 0
? isFunction(conversionTag?.text)
? `${conversionTag.text(...args)}`
: `Rate: ${conversionTagFormatter(...(args[0][FUNNEL_CONVERSATION] as [number, number]))}`
: '',
...(!isTransposed
? {
position: 'top-right',
dx: 20,
backgroundPadding: [0, 8],
}
: {
position: 'top-left',
dy: -20,
dx: 8, // 与connector 间隙
backgroundPadding: [-8, 8],
}),
},
];

const labels = [...(label === false ? [] : basicLabel), ...(conversionTag === false ? [] : rateLabel)];

const basicFunnel = {
type: 'polygon',
transform: undefined,
encode: {
x: POLYGON_X,
y: POLYGON_Y,
color: xField,
},
labels,
tooltip: {
title: false,
items: [
(d) =>
isFunction(tooltip?.text)
? tooltip.text(d)
: {
name: d[xField],
value: d[yField],
},
],
},
};

params.options.children = map(params.options.children, (child) => {
return deepAssign(child, basicFunnel);
});

deepAssign(params.options, {
paddingLeft: 60,
paddingRight: 60,
legend: false,
// TODO 暂无法实现转置, 缺少 reflect.x
// coordinate: !isTransposed
// ? {
// transform: [{ type: 'transpose' }, { type: 'reflect.x' }],
// }
// : undefined,
});
};

return flow(transformData, init)(params);
}
13 changes: 8 additions & 5 deletions packages/plots/src/core/plots/funnel/facet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type Params = Adaptor<FunnelOptions>;
*/
export function facetFunnel(params: Params) {
const getBasicFunnel = (params: Params) => {
const { xField, yField, shape, legend, label, isTransposed, seriesField } = params.options;
const { xField, yField, shape, legend, label, isTransposed, seriesField, tooltip } = params.options;

const conversionTag = get(params.options, CUSTOM_COMVERSION_TAG_CONFIG);

Expand Down Expand Up @@ -112,10 +112,13 @@ export function facetFunnel(params: Params) {
tooltip: {
title: false,
items: [
(d) => ({
name: d[xField],
value: d[yField],
}),
(d) =>
isFunction(tooltip?.text)
? tooltip.text(d)
: {
name: d[xField],
value: d[yField],
},
],
},
labels,
Expand Down
3 changes: 1 addition & 2 deletions site/examples/statistics/funnel/demo/compare.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ const DemoFunnel = () => {
yField: 'number',
compareField: 'company',
tooltip: {
// fields: ['stage', 'number', 'company'],
formatter: (v) => ({
text: (v) => ({
name: `${v.company}的${v.stage}`,
value: v.number,
}),
Expand Down
24 changes: 24 additions & 0 deletions site/examples/statistics/funnel/demo/dynamic-height.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Funnel } from '@ant-design/plots';

const DemoFunnel = () => {
const data = [
{ stage: '简历筛选', number: 253 },
{ stage: '初试人数', number: 151 },
{ stage: '复试人数', number: 113 },
{ stage: '录取人数', number: 87 },
{ stage: '入职人数', number: 59 },
];

const config = {
data,
xField: 'stage',
yField: 'number',
dynamicHeight: true,
};

return <Funnel {...config} />;
};

ReactDOM.render(<DemoFunnel />, document.getElementById('container'));
8 changes: 8 additions & 0 deletions site/examples/statistics/funnel/demo/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
},
"screenshot": "https://gw.alipayobjects.com/zos/antfincdn/L88CCTUbpo/9127293d-767c-4a57-8852-e7c27fc4df8d.png"
},
{
"filename": "dynamic-height.js",
"title": {
"zh": "动态高度漏斗图",
"en": "Dynamic height funnel plot"
},
"screenshot": "https://gw.alicdn.com/tfs/TB1Tgtyurj1gK0jSZFOXXc7GpXa-786-503.png"
},
{
"filename": "compare.js",
"title": {
Expand Down