Skip to content

Commit

Permalink
WIP: add Funnel (#2129)
Browse files Browse the repository at this point in the history
* WIP: add Funnel

* feat: 添加静态变量、label定制、尖底漏斗图

* feat: compare, facet

* feat: facet funnel、各种转置适配

* fix: 引入路径修复

---------

Co-authored-by: Joel Alan <[email protected]>
  • Loading branch information
xyuanbuilds and lxfu1 authored Oct 18, 2023
1 parent 02a7ec2 commit 18523bc
Show file tree
Hide file tree
Showing 24 changed files with 1,290 additions and 2 deletions.
11 changes: 11 additions & 0 deletions packages/plots/src/components/funnel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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<FunnelOptions>;

const FunnelChart = (props: FunnelConfig) => <BaseChart {...props} chartType="Funnel" />;

export default Object.assign(FunnelChart, Funnel.getFields());
3 changes: 3 additions & 0 deletions packages/plots/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Area from './area';
import Bar from './bar';
import Column from './column';
import DualAxes from './dual-axes';
import Funnel from './funnel';
import Line from './line';
import Pie from './pie';
import Scatter from './scatter';
Expand All @@ -24,6 +25,7 @@ export type { AreaConfig } from './area';
export type { BarConfig } from './bar';
export type { ColumnConfig } from './column';
export type { DualAxesConfig } from './dual-axes';
export type { FunnelConfig } from './funnel';
export type { LineConfig } from './line';
export type { PieConfig } from './pie';
export type { ScatterConfig } from './scatter';
Expand All @@ -49,6 +51,7 @@ export {
Area,
Bar,
DualAxes,
Funnel,
Scatter,
Radar,
Rose,
Expand Down
7 changes: 5 additions & 2 deletions packages/plots/src/core/base/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import EE from '@antv/event-emitter';
import { Chart, ChartEvent } from '@antv/g2';
import { bind } from 'size-sensor';
import { CHART_OPTIONS, ANNOTATION_LIST } from '../constants';
import { CHART_OPTIONS, ANNOTATION_LIST, SKIP_DEL_CUSTOM_SIGN } from '../constants';
import { merge, omit, pick, deleteCustomKeys, deleteChartOptionKeys } from '../utils';
import { Annotaion } from '../annotation';

Expand Down Expand Up @@ -51,7 +51,10 @@ export abstract class Plot<O extends Options> extends EE {
* G2 options(Spec) 配置
*/
private getSpecOptions() {
if (this.type === 'base') return { ...this.options, ...this.getChartOptions() };
if (this.type === 'base' || this[SKIP_DEL_CUSTOM_SIGN]) {
return { ...this.options, ...this.getChartOptions() };
}

return deleteCustomKeys(omit(this.options, CHART_OPTIONS), true);
}

Expand Down
3 changes: 3 additions & 0 deletions packages/plots/src/core/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export const RESERVED_KEYS = ['data', 'type', 'children'];
/** 特殊标识,用于标识改配置来自于转换逻辑,而非用户配置 */
export const TRANSFORM_SIGN = '__transform__';

/** 特殊标识,用于跳过 删除已转换的配置项 */
export const SKIP_DEL_CUSTOM_SIGN = '__skipDelCustomKeys__';

/**
* @title 字段转换逻辑
* @example
Expand Down
3 changes: 3 additions & 0 deletions packages/plots/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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 { LineOptions } from './plots/line';
export type { PieOptions } from './plots/pie';
export type { ScatterOptions } from './plots/scatter';
Expand Down Expand Up @@ -30,6 +31,7 @@ import { Area } from './plots/area';
import { Bar } from './plots/bar';
import { Column } from './plots/column';
import { DualAxes } from './plots/dual-axes';
import { Funnel } from './plots/funnel';
import { Line } from './plots/line';
import { Pie } from './plots/pie';
import { Scatter } from './plots/scatter';
Expand Down Expand Up @@ -59,6 +61,7 @@ export const Plots = {
Area,
Bar,
DualAxes,
Funnel,
Scatter,
Radar,
Rose,
Expand Down
193 changes: 193 additions & 0 deletions packages/plots/src/core/plots/funnel/adaptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
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<FunnelOptions>;

/**
* @param chart
* @param options
*/
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 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);
}

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 basicFunnel = {
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);
});
}

// 漏斗图 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',
},
},
};

return params;
};

return flow(transformData, init, legend)(params);
}
Loading

0 comments on commit 18523bc

Please sign in to comment.