From 1ccafba297d3fbffc231b69717485d844c13c0fe Mon Sep 17 00:00:00 2001 From: lxfu1 <954055752@qq.com> Date: Wed, 25 Oct 2023 16:43:57 +0800 Subject: [PATCH] fix: funnel --- packages/plots/package.json | 2 +- .../plots/src/components/funnel/index.tsx | 3 +- packages/plots/src/constants/index.ts | 15 -- packages/plots/src/core/base/index.ts | 2 + packages/plots/src/core/index.ts | 2 +- .../plots/src/core/plots/funnel/adaptor.ts | 219 ++++----------- .../plots/src/core/plots/funnel/compare.ts | 249 ------------------ .../plots/src/core/plots/funnel/constant.ts | 14 - packages/plots/src/core/plots/funnel/facet.ts | 155 ----------- packages/plots/src/core/plots/funnel/index.ts | 43 +-- packages/plots/src/core/plots/funnel/type.ts | 52 ---- packages/plots/src/hooks/useChart.ts | 47 ++-- packages/plots/src/util/get-path-config.ts | 17 -- packages/plots/src/util/index.ts | 5 +- packages/plots/src/util/is-valid-element.ts | 5 + packages/plots/src/util/set-path-config.ts | 21 -- packages/plots/tests/utils/util.test.ts | 41 --- .../statistics/funnel/demo/basic-transpose.js | 42 --- .../funnel/demo/compare-transpose.js | 76 ------ .../statistics/funnel/demo/compare.js | 75 ------ .../statistics/funnel/demo/facet-transpose.js | 69 ----- site/examples/statistics/funnel/demo/facet.js | 68 ----- .../funnel/demo/{basic.js => funnel.js} | 3 + .../examples/statistics/funnel/demo/meta.json | 58 +--- .../statistics/funnel/demo/mirror-funnel.js | 35 +++ .../statistics/funnel/demo/pyramid.js | 48 +++- .../statistics/funnel/demo/static-field.js | 36 --- .../statistics/funnel/demo/transpose.js | 56 ++++ 28 files changed, 241 insertions(+), 1217 deletions(-) delete mode 100644 packages/plots/src/constants/index.ts delete mode 100644 packages/plots/src/core/plots/funnel/compare.ts delete mode 100644 packages/plots/src/core/plots/funnel/constant.ts delete mode 100644 packages/plots/src/core/plots/funnel/facet.ts delete mode 100644 packages/plots/src/util/get-path-config.ts create mode 100644 packages/plots/src/util/is-valid-element.ts delete mode 100644 packages/plots/src/util/set-path-config.ts delete mode 100644 packages/plots/tests/utils/util.test.ts delete mode 100644 site/examples/statistics/funnel/demo/basic-transpose.js delete mode 100644 site/examples/statistics/funnel/demo/compare-transpose.js delete mode 100644 site/examples/statistics/funnel/demo/compare.js delete mode 100644 site/examples/statistics/funnel/demo/facet-transpose.js delete mode 100644 site/examples/statistics/funnel/demo/facet.js rename site/examples/statistics/funnel/demo/{basic.js => funnel.js} (89%) create mode 100644 site/examples/statistics/funnel/demo/mirror-funnel.js delete mode 100644 site/examples/statistics/funnel/demo/static-field.js create mode 100644 site/examples/statistics/funnel/demo/transpose.js diff --git a/packages/plots/package.json b/packages/plots/package.json index 94f733cc9e..ff09ea56b7 100644 --- a/packages/plots/package.json +++ b/packages/plots/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@antv/event-emitter": "^0.1.3", - "@antv/g2": "^5.1.3", + "@antv/g2": "^5.1.6", "size-sensor": "^1.0.1", "@ant-design/charts-util": "workspace:*" }, diff --git a/packages/plots/src/components/funnel/index.tsx b/packages/plots/src/components/funnel/index.tsx index 9ea2ad1c86..e380f2a18d 100644 --- a/packages/plots/src/components/funnel/index.tsx +++ b/packages/plots/src/components/funnel/index.tsx @@ -2,10 +2,9 @@ import React from 'react'; import { FunnelOptions } from '../../core'; import { CommonConfig } from '../../interface'; import { BaseChart } from '../base'; -import { Funnel } from '../../core/plots/funnel'; export type FunnelConfig = CommonConfig; const FunnelChart = (props: FunnelConfig) => ; -export default Object.assign(FunnelChart, Funnel.getFields()); +export default FunnelChart; diff --git a/packages/plots/src/constants/index.ts b/packages/plots/src/constants/index.ts deleted file mode 100644 index 64fca6685a..0000000000 --- a/packages/plots/src/constants/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @description: 需要支持 JSX 的配置项 - * @example - * 1. config: { label: { render: () =>
xxx
} } - */ -export const JSX_TO_STRING = [ - { path: ['label', 'render'] }, - { path: ['style', 'textContent'] }, - { - path: ['interaction', 'tooltip', 'render'], - extra: { - className: 'g2-tooltip', - }, - }, -]; diff --git a/packages/plots/src/core/base/index.ts b/packages/plots/src/core/base/index.ts index f1536583d8..94a6b08613 100644 --- a/packages/plots/src/core/base/index.ts +++ b/packages/plots/src/core/base/index.ts @@ -100,6 +100,8 @@ export abstract class Plot extends EE { if (this.type !== 'base') { this.execAdaptor(); } + // @ts-ignore + console.log(this.getSpecOptions()); // options 转换 this.chart.options(this.getSpecOptions()); diff --git a/packages/plots/src/core/index.ts b/packages/plots/src/core/index.ts index dff5581bdf..53f29717a6 100644 --- a/packages/plots/src/core/index.ts +++ b/packages/plots/src/core/index.ts @@ -3,7 +3,7 @@ export type { AreaOptions } from './plots/area'; export type { BarOptions } from './plots/bar'; export type { ColumnOptions } from './plots/column'; export type { DualAxesOptions } from './plots/dual-axes'; -export type { FunnelOptions } from './plots/funnel'; +export type { FunnelOptions } from './plots/funnel-o'; export type { LineOptions } from './plots/line'; export type { PieOptions } from './plots/pie'; export type { ScatterOptions } from './plots/scatter'; diff --git a/packages/plots/src/core/plots/funnel/adaptor.ts b/packages/plots/src/core/plots/funnel/adaptor.ts index f8b4116f7f..b0494261ed 100644 --- a/packages/plots/src/core/plots/funnel/adaptor.ts +++ b/packages/plots/src/core/plots/funnel/adaptor.ts @@ -1,22 +1,7 @@ +import { flow, transformOptions, set, groupBy } from '../../utils'; +import { mark } from '../../components'; import type { Adaptor } from '../../types'; -import { - deepAssign, - flow, - map, - transformOptions, - maxBy, - get, - groupBy, - conversionTagFormatter, - isNumber, - omit, - isFunction, -} from '../../utils'; import type { FunnelOptions } from './type'; -import { FUNNEL_CONVERSATION, FUNNEL_PERCENT, FUNNEL_MAPPING_VALUE, CUSTOM_COMVERSION_TAG_CONFIG } from './constant'; -import { Datum } from '../../../interface'; -import { compareFunnel } from './compare'; -import { facetFunnel } from './facet'; type Params = Adaptor; @@ -26,168 +11,76 @@ type Params = Adaptor; */ export function adaptor(params: Params) { /** - * @description 数据转换 + * 图表差异化处理 */ - const _transformData = (params: Params, extraMaxValue?: number, customData?: any[]) => { - const { yField, maxSize, minSize, data: originData } = params.options; - const maxYFieldValue = extraMaxValue ?? get(maxBy(originData, yField), [yField]); - const max = isNumber(maxSize) ? maxSize : 1; - const min = isNumber(minSize) ? minSize : 0; - - const curData = customData || originData; - return map(curData, (row, index) => { - const percent = row[yField] === 253 ? 1 : (row[yField] || 0) / maxYFieldValue; - row[FUNNEL_PERCENT] = percent; - row[FUNNEL_MAPPING_VALUE] = (max - min) * percent + min; - // 转化率数据存储前后数据 - row[FUNNEL_CONVERSATION] = [get(curData, [index - 1, yField]), row[yField]]; - return row; - }); + const init = (params: Params) => { + const { options } = params; + const { xField, colorField, labels } = options; + if (!colorField) { + set(options, 'colorField', xField); + } + if (labels) { + set(options, 'label', false); + } + return params; }; - const transformData = (params: Params) => { - const { yField, data, compareField, seriesField } = params.options; - - if (compareField || seriesField) { - const maxCache = {}; - const groupByField = compareField || seriesField; - - const groups = groupBy(data, (d) => () => { - const curKey = d[groupByField]; - const curMax = maxCache[curKey] ?? Number.MIN_SAFE_INTEGER; - maxCache[curKey] = Math.max(curMax, d[yField]); - return curKey; - }); - - const formatData = Object.keys(groups).reduce((res, curKey) => { - return res.concat(_transformData(params, maxCache[curKey], groups[curKey])); - }, []); - - params.options.data = formatData; - } else { - params.options.data = _transformData(params); + const transform = (params: Params) => { + const { options } = params; + const { compareField, transform, isTransposed = true, coordinate } = options; + if (!transform) { + if (compareField) { + set(options, 'transform', []); + } else { + set(options, 'transform', [{ type: 'symmetryY' }]); + } + } + if (!coordinate && isTransposed) { + set(options, 'coordinate', { transform: [{ type: 'transpose' }] }); } - return params; }; - /** - * 图表差异化处理 - */ - const init = (params: Params) => { - const { xField, yField, shape, isTransposed, compareField, seriesField, funnelStyle, label } = params.options; - - if (compareField) { - compareFunnel(params); - } else if (seriesField) { - facetFunnel(params); - } else { - 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 compare = (params: Params) => { + const { options } = params; + const { compareField, seriesField, data, children, yField, isTransposed = true } = options; - const basicFunnel = { + if (compareField || seriesField) { + const groupedData = Object.values(groupBy(data, (item) => item[compareField || seriesField])); + children[0].data = groupedData[0]; + children.push({ type: 'interval', - axis: false, - coordinate: !isTransposed - ? { - transform: [{ type: 'transpose' }], - } - : undefined, - scale: { - x: { - padding: 0, - }, - }, - style: funnelStyle, - encode: { - x: xField, - y: FUNNEL_MAPPING_VALUE, - color: xField, - shape: shape || 'funnel', - }, - animate: { enter: { type: 'fadeIn' } }, - tooltip: { - title: false, - items: [ - (d) => ({ - name: d[xField], - value: d[yField], - }), - ], - }, - // labels 对应 xField - labels, - }; - - params.options.children = map(params.options.children, (child) => { - return deepAssign(child, basicFunnel); + data: groupedData[1], + yField: (item) => -item[yField], }); + delete options['compareField']; + delete options.data; + } + if (seriesField) { + set(options, 'type', 'spaceFlex'); + set(options, 'ratio', [1, 1]); + set(options, 'direction', isTransposed ? 'row' : 'col'); + delete options['seriesField']; } - - // 漏斗图 label、conversionTag 不可被通用处理 - params.options = omit(params.options, ['label', CUSTOM_COMVERSION_TAG_CONFIG, 'yField', 'xField', 'seriesField']); return params; }; - /** - * legend 配置 - * @param params - */ - const legend = (params: Params): Params => { - const { legend } = params.options; - - params.options.legend = legend ?? { - color: { - position: 'bottom', - layout: { - justifyContent: 'center', - }, - }, - }; - + const tooltip = (params) => { + const { options } = params; + const { tooltip, xField, yField } = options; + if (!tooltip) { + set(options, 'tooltip', { + title: false, + items: [ + (d) => { + return { name: d[xField], value: d[yField] }; + }, + ], + }); + } return params; }; - return flow(transformData, init, legend)(params); + return flow(init, transform, compare, tooltip, transformOptions, mark)(params); } diff --git a/packages/plots/src/core/plots/funnel/compare.ts b/packages/plots/src/core/plots/funnel/compare.ts deleted file mode 100644 index 547c3b5b99..0000000000 --- a/packages/plots/src/core/plots/funnel/compare.ts +++ /dev/null @@ -1,249 +0,0 @@ -import type { Adaptor } from '../../types'; -import { flow, map, maxBy, get, merge, conversionTagFormatter, omit, isFunction } from '../../utils'; -import type { FunnelOptions } from './type'; -import { FUNNEL_CONVERSATION, FUNNEL_MAPPING_VALUE, CUSTOM_COMVERSION_TAG_CONFIG } from './constant'; -import { Datum } from '../../../interface'; - -type Params = Adaptor; - -/** - * @param chart - * @param options - */ -export function compareFunnel(params: Params) { - const getBasicFunnel = (params: Params, compareField: string) => { - const { xField, yField, funnelStyle, label, isTransposed } = params.options; - - const conversionTag = get(params.options, CUSTOM_COMVERSION_TAG_CONFIG); - - const basicLabel = [ - { - text: (d) => `${d[yField]}`, - position: 'inside', - fillOpacity: 1, - ...label, - }, - ]; - - const rateLabel = [ - { - textAlign: 'left', - textBaseline: 'middle', - fill: '#aaa', - fillOpacity: 1, - transform: [ - { - type: 'overlapDodgeY', - }, - ], - 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', - dx: 8, - dy: -20, - backgroundPadding: [-8, 8], - }), - }, - ]; - - const compareLabel = { - text: (_, index) => { - return index === 0 ? compareField : ''; - }, - fontSize: 14, - position: 'top', - fillOpacity: 1, - dy: -24, - }; - - const labels = [ - ...(label === false ? [] : basicLabel), - ...(conversionTag === false ? [] : rateLabel), - compareLabel, - ]; - - return { - type: 'interval', - axis: false, - coordinate: !isTransposed - ? { - transform: [{ type: 'transpose' }], - } - : undefined, - scale: { - x: { - padding: 0, - }, - }, - style: funnelStyle, - encode: { - x: xField, - y: FUNNEL_MAPPING_VALUE, - color: xField, - shape: 'funnel', - }, - animate: { enter: { type: 'fadeIn' } }, - tooltip: { - title: false, - items: [ - (d) => ({ - name: d[xField], - value: d[yField], - }), - ], - }, - // labels 对应 xField - labels, - }; - }; - - const getCompareFields = (params: Params) => { - const { data, compareField } = params.options; - const compareFields: string[] = []; - - let i = 0; - while (compareFields.length < 2) { - const curData = data[i][compareField]; - if (curData !== compareFields[0]) { - compareFields.push(curData); - } - if (compareFields.length === 2) return compareFields; - i += 1; - } - - return compareFields; - }; - /** - * 图表差异化处理 - */ - const init = (params: Params) => { - const { yField, isTransposed, legend, conversionTag, label, compareField } = params.options; - - const compareFields = getCompareFields(params); - - params.options.children = [ - compareFields[0] - ? merge(getBasicFunnel(params, compareFields[0]), { - data: { - transform: [ - { - type: 'filter', - callback: (d) => d.company === compareFields[0], - }, - ], - }, - style: { - stroke: '#FFF', - }, - encode: { - y: (d) => -d[FUNNEL_MAPPING_VALUE], - }, - labels: [ - ...(label === false - ? [] - : [ - { - textAlign: isTransposed ? 'center' : 'end', - position: isTransposed ? 'top' : 'right', - dx: isTransposed ? undefined : -8, - dy: isTransposed ? 8 : undefined, - fill: '#FFF', - }, - ]), - ...(conversionTag === false - ? [] - : [ - { - ...(!isTransposed - ? { - position: 'top-left', - dx: -48 * 2, - } - : { - position: 'bottom-left', - dx: 8, - dy: 24, - }), - }, - ]), - isTransposed - ? { - text: (_, index, arr) => { - return index === arr.length - 1 ? compareFields[0] : ''; - }, - textAlign: 'end', - position: 'bottom-right', - textBaseline: 'middle', - dy: 24, - } - : {}, - ], - }) - : null, - compareFields[1] - ? merge(getBasicFunnel(params, compareFields[1]), { - data: { - transform: [ - { - type: 'filter', - callback: (d) => d.company === compareFields[1], - }, - ], - }, - style: { - stroke: '#FFF', - }, - labels: [ - ...(label === false - ? [] - : [ - { - textAlign: isTransposed ? 'center' : 'start', - position: isTransposed ? 'bottom' : 'left', - dx: isTransposed ? undefined : 8, - dy: isTransposed ? -8 : undefined, - fill: '#FFF', - }, - ]), - ...(conversionTag === false ? [] : [{}]), - isTransposed - ? { - text: (_, index, arr) => { - return index === arr.length - 1 ? compareFields[1] : ''; - }, - textAlign: 'end', - position: 'top-right', - textBaseline: 'middle', - dy: -24, - } - : {}, - ], - }) - : null, - ].filter((i) => !!i); - - params.options = merge(params.options, { - // 预留给 compareLabel 的空间 - paddingTop: 24, - paddingLeft: 60, - paddingRight: 60, - }); - - return params; - }; - - return flow(init)(params); -} diff --git a/packages/plots/src/core/plots/funnel/constant.ts b/packages/plots/src/core/plots/funnel/constant.ts deleted file mode 100644 index 21e1068fcb..0000000000 --- a/packages/plots/src/core/plots/funnel/constant.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 漏斗占比: data[n][yField] / data[0][yField] -export const FUNNEL_PERCENT = '__percentage__'; -// 漏斗映射值 -export const FUNNEL_MAPPING_VALUE = '__mappingValue__'; -// 漏斗转化率: data[n][yField] / data[n-1][yField]; -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$$'; - -export const CUSTOM_COMVERSION_TAG_CONFIG = '__conversionTag__'; diff --git a/packages/plots/src/core/plots/funnel/facet.ts b/packages/plots/src/core/plots/funnel/facet.ts deleted file mode 100644 index b158df6e38..0000000000 --- a/packages/plots/src/core/plots/funnel/facet.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { Adaptor } from '../../types'; -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'; - -type Params = Adaptor; - -/** - * @param chart - * @param options - */ -export function facetFunnel(params: Params) { - const getBasicFunnel = (params: Params) => { - const { xField, yField, shape, legend, label, isTransposed, seriesField } = params.options; - - const conversionTag = get(params.options, CUSTOM_COMVERSION_TAG_CONFIG); - - const basicLabel = [ - { - text: (d) => `${d[yField]}`, - position: 'inside', - fillOpacity: 1, - ...label, - }, - ]; - - const rateLabel = [ - { - textAlign: 'left', - textBaseline: 'middle', - fill: '#aaa', - fillOpacity: 1, - connector: true, - ...(!isTransposed - ? { - position: 'top-right', - dx: 20, - backgroundPadding: [0, 8], - } - : { - position: 'top-left', - dy: -20, - dx: 8, // 与connector 间隙 - backgroundPadding: [-8, 8], - }), - ...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]))}` - : '', - }, - ]; - - const facetLabel = { - text: (d, index) => { - return index === 0 ? d[seriesField] : ''; - }, - fontSize: 14, - position: !isTransposed ? 'top' : 'left', - fillOpacity: 1, - dy: -24, - }; - - const labels = [...(label === false ? [] : basicLabel), ...(conversionTag === false ? [] : rateLabel)]; - - if (!isTransposed) { - labels.push(facetLabel); - } - - return { - // 分面中需要单独设置 theme - theme: 'classic', - type: 'interval', - axis: { - x: false, - y: false, - }, - legend, - // facetLabel 空间预留 - marginTop: !isTransposed ? 8 : undefined, - // 分面单元 紧凑 - marginLeft: !isTransposed ? -10 : undefined, - frame: false, - encode: { - x: xField, - y: FUNNEL_MAPPING_VALUE, - color: xField, - shape: shape || 'funnel', - }, - scale: { - x: { - padding: 0, - }, - }, - coordinate: !isTransposed - ? { - transform: [ - { - type: 'transpose', - }, - ], - } - : undefined, - transform: [ - { - type: 'symmetryY', - }, - ], - tooltip: { - title: false, - items: [ - (d) => ({ - name: d[xField], - value: d[yField], - }), - ], - }, - labels, - }; - }; - - /** - * 图表差异化处理 - */ - const init = (params: Params) => { - const { isTransposed, legend, yField, seriesField } = params.options; - - params.options.legend = legend; - - params.options.children = [getBasicFunnel(params)]; - - params.options = merge(params.options, { - type: 'facetRect', - direction: isTransposed ? 'col' : 'row', - frame: false, - encode: { x: seriesField }, - autoFit: true, - paddingLeft: 60, - paddingRight: 60, - axis: { x: false }, - scale: { - [yField]: { - sync: true, - }, - }, - }); - - return params; - }; - - return flow(init)(params); -} diff --git a/packages/plots/src/core/plots/funnel/index.ts b/packages/plots/src/core/plots/funnel/index.ts index 47cbd4d416..e0a9e01293 100644 --- a/packages/plots/src/core/plots/funnel/index.ts +++ b/packages/plots/src/core/plots/funnel/index.ts @@ -1,32 +1,13 @@ import { Plot } from '../../base'; import type { Adaptor } from '../../types'; import { adaptor } from './adaptor'; -import { pick, omit, set } from '../../utils'; import { FunnelOptions } from './type'; -import { FUNNEL_CONVERSATION, FUNNEL_PERCENT, FUNNEL_TOTAL_PERCENT, CUSTOM_COMVERSION_TAG_CONFIG } from './constant'; -import { SKIP_DEL_CUSTOM_SIGN } from '../../constants'; export type { FunnelOptions }; export class Funnel extends Plot { - constructor(container: string | HTMLElement, options: FunnelOptions) { - super(container, omit(options, ['conversionTag'])); - set(this.options, CUSTOM_COMVERSION_TAG_CONFIG, options.conversionTag); - set(this, SKIP_DEL_CUSTOM_SIGN, true); - } /** 图表类型 */ - public type = 'Funnel'; - - /** 漏斗 转化率 字段 */ - static CONVERSATION_FIELD = FUNNEL_CONVERSATION; - /** 漏斗 百分比 字段 */ - static PERCENT_FIELD = FUNNEL_PERCENT; - /** 漏斗 总转换率百分比 字段 */ - static TOTAL_PERCENT_FIELD = FUNNEL_TOTAL_PERCENT; - - static getFields() { - return pick(Funnel, ['CONVERSATION_FIELD', 'PERCENT_FIELD', 'TOTAL_PERCENT_FIELD']); - } + public type = 'column'; /** * 获取 漏斗图 默认配置项 @@ -35,25 +16,19 @@ export class Funnel extends Plot { static getDefaultOptions(): Partial { return { type: 'view', + scale: { x: { padding: 0 } }, + animate: { enter: { type: 'fadeIn' } }, + axis: false, + shapeField: 'funnel', + label: { + position: 'inside', + transform: [{ type: 'contrastReverse' }], + }, children: [ { type: 'interval', - transform: [ - { - type: 'symmetryY', - }, - ], - // 漏斗图默认不需要坐标系 - axis: false, - scale: { - x: { - padding: 0, - }, - }, }, ], - // 漏斗基本动画 - animate: { enter: { type: 'fadeIn' } }, }; } diff --git a/packages/plots/src/core/plots/funnel/type.ts b/packages/plots/src/core/plots/funnel/type.ts index 518414fcc3..3282883e92 100644 --- a/packages/plots/src/core/plots/funnel/type.ts +++ b/packages/plots/src/core/plots/funnel/type.ts @@ -1,59 +1,7 @@ import type { BaseOptions, Options } from '../../types/common'; -import type { Datum } from '../../../interface'; - -type StyleAttr = Record; export type FunnelOptions = Options & BaseOptions & { - /** - * @title x轴字段 - */ - xField: string; - /** - * @title y轴字段 - */ - yField: string; - /** - * @title 对比字段 - * @description 漏斗图将根据此字段转置为对比漏斗图 - */ compareField?: string; - /** - * @title 分组字段 - * @description 漏斗图将根据此字段转置为分面漏斗图 - */ - seriesField?: string; - /** - * @title 是否转置 - * @default false - */ isTransposed?: boolean; - /** - * @title 是否是动态高度 - * @default false - */ - readonly dynamicHeight?: boolean; - /** - * @title 漏斗分面标题 - * @description 是否关闭漏斗的标题展示,适用于存在多组漏斗的情形,如:分组漏斗图、对比漏斗图。 - */ - showFacetTitle?: boolean; - /** - * @title 漏斗图样式 - */ - funnelStyle?: StyleAttr; - /** 可以设置为金字塔 pyramid */ - shape?: 'pyramid'; - label?: { - text?: string | ((datum?: Datum, data?: Datum[]) => string); - }; - /** - * @title 转化率信息 - */ - conversionTag?: - | false - | { - style?: StyleAttr; - text?: string | ((datum?: Datum, data?: Datum[]) => string); - }; }; diff --git a/packages/plots/src/hooks/useChart.ts b/packages/plots/src/hooks/useChart.ts index b7f72c9129..2fb6c4e88a 100644 --- a/packages/plots/src/hooks/useChart.ts +++ b/packages/plots/src/hooks/useChart.ts @@ -1,17 +1,5 @@ import React, { useRef, useEffect } from 'react'; -import { - getPathConfig, - isString, - isNumber, - isElement, - isFunction, - setPathConfig, - isEqual, - get, - createNode, - cloneDeep, -} from '../util'; -import { JSX_TO_STRING } from '../constants'; +import { isFunction, isEqual, get, createNode, cloneDeep, isArray, isObject, isValidElement } from '../util'; import { CommonConfig, Chart } from '../interface'; export default function useChart(ChartClass: T, config: U) { @@ -52,21 +40,20 @@ export default function useChart(ChartC return imageName; }; - const reactDomToString = (source: U, path: string[], extra?: object) => { - const statisticCustomHtml = getPathConfig(source, path); - setPathConfig(source, path, (...arg: any[]) => { - const statisticDom = isFunction(statisticCustomHtml) ? statisticCustomHtml(...arg) : statisticCustomHtml; - if (isString(statisticDom) || isNumber(statisticDom) || isElement(statisticDom)) { - return statisticDom; - } - return createNode(statisticDom, extra); - }); - }; - - const processConfig = () => { - JSX_TO_STRING.forEach(({ path, extra }) => { - if (getPathConfig(config, path)) { - reactDomToString(config, path, extra); + const processConfig = (cfg: object) => { + const keys = Object.keys(cfg); + keys.forEach((key) => { + const current = cfg[key]; + if (isFunction(current) && isValidElement(`${current}`)) { + cfg[key] = (...arg) => createNode(current(...arg)); + } else { + if (isArray(current)) { + current.forEach((item) => { + processConfig(item); + }); + } else if (isObject(current)) { + processConfig(current); + } } }); }; @@ -84,7 +71,7 @@ export default function useChart(ChartC if (changeData) { chart.current.changeData(get(config, 'data')); } else { - processConfig(); + processConfig(config); chart.current.update(config); chart.current.render(); } @@ -98,7 +85,7 @@ export default function useChart(ChartC if (!chartOptions.current) { chartOptions.current = cloneDeep(config); } - processConfig(); + processConfig(config); const chartInstance: T = new (ChartClass as any)(container.current, { ...config, }); diff --git a/packages/plots/src/util/get-path-config.ts b/packages/plots/src/util/get-path-config.ts deleted file mode 100644 index c240abe820..0000000000 --- a/packages/plots/src/util/get-path-config.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @description 存在时返回路径值,不存在时返回 undefined - * @param source 需要获取的对象 - * @param path 路径 - */ -export const getPathConfig = (source: any, path: string[]) => { - let current = source; - for (let i = 0; i < path.length; i += 1) { - if (current?.[path[i]]) { - current = current[path[i]]; - } else { - current = undefined; - break; - } - } - return current; -}; diff --git a/packages/plots/src/util/index.ts b/packages/plots/src/util/index.ts index cb91ef2295..cbe81724b3 100644 --- a/packages/plots/src/util/index.ts +++ b/packages/plots/src/util/index.ts @@ -1,4 +1,3 @@ -export { isEqual, get, isString, isNumber, isFunction, isElement, cloneDeep } from 'lodash-es'; +export { isEqual, get, isString, isNumber, isFunction, isElement, cloneDeep, isArray, isObject } from 'lodash-es'; export { createNode, uuid } from '@ant-design/charts-util'; -export { getPathConfig } from './get-path-config'; -export { setPathConfig } from './set-path-config'; +export { isValidElement } from './is-valid-element'; diff --git a/packages/plots/src/util/is-valid-element.ts b/packages/plots/src/util/is-valid-element.ts new file mode 100644 index 0000000000..131c51b9a8 --- /dev/null +++ b/packages/plots/src/util/is-valid-element.ts @@ -0,0 +1,5 @@ +export const isValidElement = (jsxCode: string): boolean => { + const jsxRegex = /react(.*?).createElement/gi; + + return jsxRegex.test(jsxCode); +}; diff --git a/packages/plots/src/util/set-path-config.ts b/packages/plots/src/util/set-path-config.ts deleted file mode 100644 index 73e210a705..0000000000 --- a/packages/plots/src/util/set-path-config.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @description 内部指定 params ,不考虑复杂情况 - * @param source 需要设置的对象 - * @param path 路径 - * @param value 值 - */ -export const setPathConfig = (source: object, path: string[], value?: any) => { - if (!source) { - return source; - } - let o = source; - path.forEach((key: string, idx: number) => { - // 不是最后一个 - if (idx < path.length - 1) { - o = o[key]; - } else { - o[key] = value; - } - }); - return source; -}; diff --git a/packages/plots/tests/utils/util.test.ts b/packages/plots/tests/utils/util.test.ts deleted file mode 100644 index 2342934eba..0000000000 --- a/packages/plots/tests/utils/util.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getPathConfig, setPathConfig } from '../../src/util'; - -describe('utils', () => { - it('has path', () => { - const config = { - statistic: { - content: { - customHtml: 'html', - }, - title: {}, - }, - }; - expect(getPathConfig(undefined, ['noPath'])).toBeUndefined(); - expect(getPathConfig(config, ['statistic', 'content', 'customHtml'])).toBe('html'); - expect(getPathConfig(config, ['statistic', 'title', 'customHtml'])).toBeUndefined(); - expect(getPathConfig(config, [])).toEqual(config); - }); - - it('set path', () => { - const config = { - statistic: { - content: { - customHtml: 'html', - }, - title: {}, - }, - }; - expect(setPathConfig(undefined, ['noPath'])).toBeUndefined(); - expect(setPathConfig(config, [])).toEqual(config); - expect(setPathConfig(config, ['statistic', 'title', 'customHtml'], 'title')).toEqual({ - statistic: { - content: { - customHtml: 'html', - }, - title: { - customHtml: 'title', - }, - }, - }); - }); -}); diff --git a/site/examples/statistics/funnel/demo/basic-transpose.js b/site/examples/statistics/funnel/demo/basic-transpose.js deleted file mode 100644 index 91b2685c22..0000000000 --- a/site/examples/statistics/funnel/demo/basic-transpose.js +++ /dev/null @@ -1,42 +0,0 @@ -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: data, - xField: 'stage', - yField: 'number', - isTransposed: true, - // minSize: 0.3, - // maxSize: 0.8, - // label: { - // formatter: (datum) => { - // // 提供占比$$percentage$$,转化率$$conversion$$两种格式 - // return `${datum.stage}:${datum.number}`; - // }, - // }, - // conversionTag: { - // formatter: (datum) => { - // return (datum[FUNNEL_CONVERSATION_FIELD][1] / datum[FUNNEL_CONVERSATION_FIELD][0]).toFixed(2); - // }, - // }, - // tooltip: { - // formatter: (datum) => { - // return { name: datum.stage, value: `${datum.number}个` }; - // }, - // }, - }; - - return ; -}; - -ReactDOM.render(, document.getElementById('container')); diff --git a/site/examples/statistics/funnel/demo/compare-transpose.js b/site/examples/statistics/funnel/demo/compare-transpose.js deleted file mode 100644 index 431d43278d..0000000000 --- a/site/examples/statistics/funnel/demo/compare-transpose.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Funnel } from '@ant-design/plots'; - -const DemoFunnel = () => { - const data = [ - { - stage: '简历筛选', - number: 253, - company: 'A公司', - }, - { - stage: '初试人数', - number: 151, - company: 'A公司', - }, - { - stage: '复试人数', - number: 113, - company: 'A公司', - }, - { - stage: '录取人数', - number: 87, - company: 'A公司', - }, - { - stage: '入职人数', - number: 59, - company: 'A公司', - }, - { - stage: '简历筛选', - number: 303, - company: 'B公司', - }, - { - stage: '初试人数', - number: 251, - company: 'B公司', - }, - { - stage: '复试人数', - number: 153, - company: 'B公司', - }, - { - stage: '录取人数', - number: 117, - company: 'B公司', - }, - { - stage: '入职人数', - number: 79, - company: 'B公司', - }, - ]; - const config = { - data, - xField: 'stage', - yField: 'number', - compareField: 'company', - tooltip: { - // fields: ['stage', 'number', 'company'], - formatter: (v) => ({ - name: `${v.company}的${v.stage}`, - value: v.number, - }), - }, - isTransposed: true, - legend: false, - }; - return ; -}; - -ReactDOM.render(, document.getElementById('container')); diff --git a/site/examples/statistics/funnel/demo/compare.js b/site/examples/statistics/funnel/demo/compare.js deleted file mode 100644 index fea6a89a4a..0000000000 --- a/site/examples/statistics/funnel/demo/compare.js +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Funnel } from '@ant-design/plots'; - -const DemoFunnel = () => { - const data = [ - { - stage: '简历筛选', - number: 253, - company: 'A公司', - }, - { - stage: '初试人数', - number: 151, - company: 'A公司', - }, - { - stage: '复试人数', - number: 113, - company: 'A公司', - }, - { - stage: '录取人数', - number: 87, - company: 'A公司', - }, - { - stage: '入职人数', - number: 59, - company: 'A公司', - }, - { - stage: '简历筛选', - number: 303, - company: 'B公司', - }, - { - stage: '初试人数', - number: 251, - company: 'B公司', - }, - { - stage: '复试人数', - number: 153, - company: 'B公司', - }, - { - stage: '录取人数', - number: 117, - company: 'B公司', - }, - { - stage: '入职人数', - number: 79, - company: 'B公司', - }, - ]; - const config = { - data, - xField: 'stage', - yField: 'number', - compareField: 'company', - tooltip: { - // fields: ['stage', 'number', 'company'], - formatter: (v) => ({ - name: `${v.company}的${v.stage}`, - value: v.number, - }), - }, - legend: false, - }; - return ; -}; - -ReactDOM.render(, document.getElementById('container')); diff --git a/site/examples/statistics/funnel/demo/facet-transpose.js b/site/examples/statistics/funnel/demo/facet-transpose.js deleted file mode 100644 index 21eadcb6c2..0000000000 --- a/site/examples/statistics/funnel/demo/facet-transpose.js +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Funnel } from '@ant-design/plots'; - -const DemoFunnel = () => { - const data = [ - { - stage: '简历筛选', - number: 253, - company: 'A公司', - }, - { - stage: '初试人数', - number: 151, - company: 'A公司', - }, - { - stage: '复试人数', - number: 113, - company: 'A公司', - }, - { - stage: '录取人数', - number: 87, - company: 'A公司', - }, - { - stage: '入职人数', - number: 59, - company: 'A公司', - }, - { - stage: '简历筛选', - number: 303, - company: 'B公司', - }, - { - stage: '初试人数', - number: 251, - company: 'B公司', - }, - { - stage: '复试人数', - number: 153, - company: 'B公司', - }, - { - stage: '录取人数', - number: 117, - company: 'B公司', - }, - { - stage: '入职人数', - number: 79, - company: 'B公司', - }, - ]; - const config = { - data, - xField: 'stage', - yField: 'number', - seriesField: 'company', - isTransposed: true, - legend: false, - }; - return ; -}; - -ReactDOM.render(, document.getElementById('container')); diff --git a/site/examples/statistics/funnel/demo/facet.js b/site/examples/statistics/funnel/demo/facet.js deleted file mode 100644 index 9ee03b42ac..0000000000 --- a/site/examples/statistics/funnel/demo/facet.js +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Funnel } from '@ant-design/plots'; - -const DemoFunnel = () => { - const data = [ - { - stage: '简历筛选', - number: 253, - company: 'A公司', - }, - { - stage: '初试人数', - number: 151, - company: 'A公司', - }, - { - stage: '复试人数', - number: 113, - company: 'A公司', - }, - { - stage: '录取人数', - number: 87, - company: 'A公司', - }, - { - stage: '入职人数', - number: 59, - company: 'A公司', - }, - { - stage: '简历筛选', - number: 303, - company: 'B公司', - }, - { - stage: '初试人数', - number: 251, - company: 'B公司', - }, - { - stage: '复试人数', - number: 153, - company: 'B公司', - }, - { - stage: '录取人数', - number: 117, - company: 'B公司', - }, - { - stage: '入职人数', - number: 79, - company: 'B公司', - }, - ]; - const config = { - data, - xField: 'stage', - yField: 'number', - seriesField: 'company', - legend: false, - }; - return ; -}; - -ReactDOM.render(, document.getElementById('container')); diff --git a/site/examples/statistics/funnel/demo/basic.js b/site/examples/statistics/funnel/demo/funnel.js similarity index 89% rename from site/examples/statistics/funnel/demo/basic.js rename to site/examples/statistics/funnel/demo/funnel.js index 0a051d7910..1b05bf79cd 100644 --- a/site/examples/statistics/funnel/demo/basic.js +++ b/site/examples/statistics/funnel/demo/funnel.js @@ -15,6 +15,9 @@ const DemoFunnel = () => { data, xField: 'stage', yField: 'number', + label: { + text: (d) => `${d.stage}\n${d.number}`, + }, }; return ; diff --git a/site/examples/statistics/funnel/demo/meta.json b/site/examples/statistics/funnel/demo/meta.json index 90a33bdc4f..001d4442ac 100644 --- a/site/examples/statistics/funnel/demo/meta.json +++ b/site/examples/statistics/funnel/demo/meta.json @@ -5,68 +5,36 @@ }, "demos": [ { - "filename": "basic.js", + "filename": "funnel.js", "title": { - "zh": "基础漏斗图", - "en": "Basic funnel plot" + "zh": "漏斗图", + "en": "Funnel" }, - "screenshot": "https://gw.alicdn.com/tfs/TB158FxuAT2gK0jSZPcXXcKkpXa-646-500.png" - }, - { - "filename": "static-field.js", - "title": { - "zh": "漏斗图: 使用静态变量", - "en": "Funnel plot - static field" - }, - "screenshot": "https://gw.alicdn.com/tfs/TB158FxuAT2gK0jSZPcXXcKkpXa-646-500.png" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*6syuS6eMD1AAAAAAAAAAAAAADmJ7AQ/original" }, { "filename": "pyramid.js", "title": { - "zh": "尖底漏斗图", - "en": "Pyramid funnel plot" + "zh": "金字塔图", + "en": "Pyramid" }, - "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/L88CCTUbpo/9127293d-767c-4a57-8852-e7c27fc4df8d.png" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*8SagQbsQk6gAAAAAAAAAAAAADmJ7AQ/original" }, { - "filename": "compare.js", + "filename": "mirror-funnel.js", "title": { "zh": "对比漏斗图", - "en": "Compare funnel plot" - }, - "screenshot": "https://gw.alicdn.com/tfs/TB1Y7dtuAL0gK0jSZFtXXXQCXXa-709-495.png" - }, - { - "filename": "facet.js", - "title": { - "zh": "分面漏斗图", - "en": "Facet funnel plot" + "en": "Mirror Funnel" }, - "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*Zk1rRJvD8zwAAAAAAAAAAAAAARQnAQ" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ejYqRJVJ12gAAAAAAAAAAAAADmJ7AQ/original" }, { - "filename": "basic-transpose.js", + "filename": "transpose.js", "title": { - "zh": "基础漏斗图-转置", - "en": "Funnel plot-transpose" + "zh": "转化漏斗图-转置", + "en": "Annotation Funnel transpose" }, "screenshot": "https://gw.alicdn.com/tfs/TB1EgtsuuT2gK0jSZFvXXXnFXXa-958-591.png" - }, - { - "filename": "compare-transpose.js", - "title": { - "zh": "对比漏斗图-转置", - "en": "Compare funnel plot" - }, - "screenshot": "https://gw.alicdn.com/tfs/TB1Hkluurr1gK0jSZR0XXbP8XXa-1095-565.png" - }, - { - "filename": "facet-transpose.js", - "title": { - "zh": "分面漏斗图-转置", - "en": "Facet funnel plot-transpose" - }, - "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*lbtdRJjZ4qsAAAAAAAAAAAAAARQnAQ" } ] } diff --git a/site/examples/statistics/funnel/demo/mirror-funnel.js b/site/examples/statistics/funnel/demo/mirror-funnel.js new file mode 100644 index 0000000000..7049b4dd1a --- /dev/null +++ b/site/examples/statistics/funnel/demo/mirror-funnel.js @@ -0,0 +1,35 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Funnel } from '@ant-design/plots'; + +const DemoFunnel = () => { + const data = [ + { action: '访问', visitor: 500, site: '站点1' }, + { action: '浏览', visitor: 400, site: '站点1' }, + { action: '交互', visitor: 300, site: '站点1' }, + { action: '下单', visitor: 200, site: '站点1' }, + { action: '完成', visitor: 100, site: '站点1' }, + { action: '访问', visitor: 550, site: '站点2' }, + { action: '浏览', visitor: 420, site: '站点2' }, + { action: '交互', visitor: 280, site: '站点2' }, + { action: '下单', visitor: 150, site: '站点2' }, + { action: '完成', visitor: 80, site: '站点2' }, + ]; + + const config = { + data, + xField: 'action', + yField: 'visitor', + compareField: 'site', + style: { + stroke: '#fff', + }, + label: { + text: 'visitor', + }, + legend: false, + }; + return ; +}; + +ReactDOM.render(, document.getElementById('container')); diff --git a/site/examples/statistics/funnel/demo/pyramid.js b/site/examples/statistics/funnel/demo/pyramid.js index 795aaa4a7e..0509ac91a8 100644 --- a/site/examples/statistics/funnel/demo/pyramid.js +++ b/site/examples/statistics/funnel/demo/pyramid.js @@ -4,18 +4,50 @@ 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 }, + { action: '浏览网站', pv: 50000 }, + { action: '放入购物车', pv: 35000 }, + { action: '生成订单', pv: 25000 }, + { action: '支付订单', pv: 15000 }, + { action: '完成交易', pv: 8000 }, ]; const config = { data, - xField: 'stage', - yField: 'number', - shape: 'pyramid', + xField: 'action', + yField: 'pv', + shapeField: 'pyramid', + label: [ + { + text: (d) => d.pv, + position: 'inside', + fontSize: 16, + }, + { + render: ($, _, i) => { + if (i) + return ( +
+ ); + }, + position: 'top-right', + }, + { + text: (d, i, data) => { + if (i) return ((d.pv / data[i - 1].pv) * 100).toFixed(2) + '%'; + }, + position: 'top-right', + textAlign: 'left', + textBaseline: 'middle', + dx: 40, + }, + ], }; return ; diff --git a/site/examples/statistics/funnel/demo/static-field.js b/site/examples/statistics/funnel/demo/static-field.js deleted file mode 100644 index e0ddb7396c..0000000000 --- a/site/examples/statistics/funnel/demo/static-field.js +++ /dev/null @@ -1,36 +0,0 @@ -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: data, - xField: 'stage', - yField: 'number', - legend: false, - label: { - text: (datum) => { - return `${(datum[Funnel.PERCENT_FIELD] * 100).toFixed(2)}%`; - }, - }, - conversionTag: { - text: (datum) => { - return `${((datum[Funnel.CONVERSATION_FIELD][1] / datum[Funnel.CONVERSATION_FIELD][0]) * 100).toFixed(2)}%`; - }, - }, - // 关闭 conversionTag 转化率 展示 - // conversionTag: false, - }; - - return ; -}; - -ReactDOM.render(, document.getElementById('container')); diff --git a/site/examples/statistics/funnel/demo/transpose.js b/site/examples/statistics/funnel/demo/transpose.js new file mode 100644 index 0000000000..a4b32b2d2a --- /dev/null +++ b/site/examples/statistics/funnel/demo/transpose.js @@ -0,0 +1,56 @@ +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: data, + xField: 'stage', + yField: 'number', + isTransposed: false, + label: [ + { + text: (d) => d.number, + position: 'inside', + fontSize: 16, + }, + { + render: ($, _, i) => { + if (i) + return ( +
+ ); + }, + position: 'top-left', + }, + { + text: (d, i, data) => { + if (i) return ((d.number / data[i - 1].number) * 100).toFixed(2) + '%'; + }, + position: 'top-left', + textAlign: 'middle', + textBaseline: 'bottom', + dy: -30, + }, + ], + }; + + return ; +}; + +ReactDOM.render(, document.getElementById('container'));