From 271dffd69899025bbd6c223047cf62c7f3198af5 Mon Sep 17 00:00:00 2001 From: hustcc Date: Fri, 8 Oct 2021 20:31:18 +0800 Subject: [PATCH 1/4] fix(funnel): do not mutable data --- src/plots/funnel/geometries/dynamic-height.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/plots/funnel/geometries/dynamic-height.ts b/src/plots/funnel/geometries/dynamic-height.ts index 154a33b1ee..3bfae17fc8 100644 --- a/src/plots/funnel/geometries/dynamic-height.ts +++ b/src/plots/funnel/geometries/dynamic-height.ts @@ -39,11 +39,12 @@ function field(params: Params): Params { const max = maxBy(data, yField)[yField]; const formatData = map(data, (row, index) => { + const newRow = { ...row }; // 储存四个点 x,y 坐标,方向为顺时针,即 [左上, 右上,右下,左下] const x = []; const y = []; - row[FUNNEL_TOTAL_PERCENT] = (row[yField] || 0) / sum; + newRow[FUNNEL_TOTAL_PERCENT] = (newRow[yField] || 0) / sum; // 获取左上角,右上角坐标 if (index) { @@ -61,17 +62,17 @@ function field(params: Params): Params { } // 获取右下角坐标 - y[2] = y[1] - row[FUNNEL_TOTAL_PERCENT]; + y[2] = y[1] - newRow[FUNNEL_TOTAL_PERCENT]; x[2] = (y[2] + 1) / 4; y[3] = y[2]; x[3] = -x[2]; // 赋值 - row[PLOYGON_X] = x; - row[PLOYGON_Y] = y; - row[FUNNEL_PERCENT] = (row[yField] || 0) / max; - row[FUNNEL_CONVERSATION] = [get(data, [index - 1, yField]), row[yField]]; - return row; + newRow[PLOYGON_X] = x; + newRow[PLOYGON_Y] = y; + newRow[FUNNEL_PERCENT] = (newRow[yField] || 0) / max; + newRow[FUNNEL_CONVERSATION] = [get(data, [index - 1, yField]), newRow[yField]]; + return newRow; }); chart.data(formatData); From 75467e62e2e7395a18ee05de6ced71140af74bd4 Mon Sep 17 00:00:00 2001 From: hustcc Date: Sat, 9 Oct 2021 11:18:47 +0800 Subject: [PATCH 2/4] revert(funnel): clone data firstly --- src/plots/funnel/adaptor.ts | 6 +++--- src/plots/funnel/geometries/common.ts | 2 +- src/plots/funnel/geometries/dynamic-height.ts | 15 +++++++-------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/plots/funnel/adaptor.ts b/src/plots/funnel/adaptor.ts index 0ee54b9d66..f394d315fb 100644 --- a/src/plots/funnel/adaptor.ts +++ b/src/plots/funnel/adaptor.ts @@ -1,4 +1,4 @@ -import { isFunction } from '@antv/util'; +import { isFunction, clone } from '@antv/util'; import { Params } from '../../core/adaptor'; import { interaction, animation, theme, scale, annotation, tooltip } from '../../adaptor/common'; import { getLocale } from '../../core/locale'; @@ -27,7 +27,7 @@ import { FUNNEL_CONVERSATION, FUNNEL_PERCENT } from './constant'; */ function defaultOptions(params: Params): Params { const { options } = params; - const { compareField, xField, yField, locale, funnelStyle } = options; + const { compareField, xField, yField, locale, funnelStyle, data } = options; const i18n = getLocale(locale); const defaultOption = { @@ -70,7 +70,7 @@ function defaultOptions(params: Params): Params { }; } - return deepAssign({ options: defaultOption }, params, { options: { funnelStyle: style } }); + return deepAssign({ options: defaultOption }, params, { options: { funnelStyle: style, data: clone(data) } }); } /** diff --git a/src/plots/funnel/geometries/common.ts b/src/plots/funnel/geometries/common.ts index 551a7cb8ef..1463e95174 100644 --- a/src/plots/funnel/geometries/common.ts +++ b/src/plots/funnel/geometries/common.ts @@ -1,5 +1,5 @@ import { Types } from '@antv/g2'; -import { isFunction, map, isNumber, maxBy, get } from '@antv/util'; +import { isFunction, map, isNumber, maxBy, get, clone } from '@antv/util'; import { Datum, Data } from '../../../types/common'; import { FUNNEL_PERCENT, FUNNEL_CONVERSATION, FUNNEL_MAPPING_VALUE } from '../constant'; import { Params } from '../../../core/adaptor'; diff --git a/src/plots/funnel/geometries/dynamic-height.ts b/src/plots/funnel/geometries/dynamic-height.ts index 3bfae17fc8..154a33b1ee 100644 --- a/src/plots/funnel/geometries/dynamic-height.ts +++ b/src/plots/funnel/geometries/dynamic-height.ts @@ -39,12 +39,11 @@ function field(params: Params): Params { const max = maxBy(data, yField)[yField]; const formatData = map(data, (row, index) => { - const newRow = { ...row }; // 储存四个点 x,y 坐标,方向为顺时针,即 [左上, 右上,右下,左下] const x = []; const y = []; - newRow[FUNNEL_TOTAL_PERCENT] = (newRow[yField] || 0) / sum; + row[FUNNEL_TOTAL_PERCENT] = (row[yField] || 0) / sum; // 获取左上角,右上角坐标 if (index) { @@ -62,17 +61,17 @@ function field(params: Params): Params { } // 获取右下角坐标 - y[2] = y[1] - newRow[FUNNEL_TOTAL_PERCENT]; + y[2] = y[1] - row[FUNNEL_TOTAL_PERCENT]; x[2] = (y[2] + 1) / 4; y[3] = y[2]; x[3] = -x[2]; // 赋值 - newRow[PLOYGON_X] = x; - newRow[PLOYGON_Y] = y; - newRow[FUNNEL_PERCENT] = (newRow[yField] || 0) / max; - newRow[FUNNEL_CONVERSATION] = [get(data, [index - 1, yField]), newRow[yField]]; - return newRow; + row[PLOYGON_X] = x; + row[PLOYGON_Y] = y; + row[FUNNEL_PERCENT] = (row[yField] || 0) / max; + row[FUNNEL_CONVERSATION] = [get(data, [index - 1, yField]), row[yField]]; + return row; }); chart.data(formatData); From be5167ffc385f9f17f10e343fef06149e4edee12 Mon Sep 17 00:00:00 2001 From: hustcc Date: Sat, 9 Oct 2021 11:19:45 +0800 Subject: [PATCH 3/4] chore: remove unused import --- src/plots/funnel/geometries/common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/funnel/geometries/common.ts b/src/plots/funnel/geometries/common.ts index 1463e95174..551a7cb8ef 100644 --- a/src/plots/funnel/geometries/common.ts +++ b/src/plots/funnel/geometries/common.ts @@ -1,5 +1,5 @@ import { Types } from '@antv/g2'; -import { isFunction, map, isNumber, maxBy, get, clone } from '@antv/util'; +import { isFunction, map, isNumber, maxBy, get } from '@antv/util'; import { Datum, Data } from '../../../types/common'; import { FUNNEL_PERCENT, FUNNEL_CONVERSATION, FUNNEL_MAPPING_VALUE } from '../constant'; import { Params } from '../../../core/adaptor'; From 3c57a9d4211eafeaa3d7bcafebcdc0098bba09a4 Mon Sep 17 00:00:00 2001 From: hustcc Date: Mon, 8 Nov 2021 17:14:48 +0800 Subject: [PATCH 4/4] feat(sankey): node link data supported --- __tests__/unit/plots/sankey/node-link-spec.ts | 38 ++++++++++++ src/plots/sankey/helper.ts | 35 ++++++++--- src/plots/sankey/types.ts | 60 ++++++++++++++++--- 3 files changed, 116 insertions(+), 17 deletions(-) create mode 100644 __tests__/unit/plots/sankey/node-link-spec.ts diff --git a/__tests__/unit/plots/sankey/node-link-spec.ts b/__tests__/unit/plots/sankey/node-link-spec.ts new file mode 100644 index 0000000000..5a83664e5d --- /dev/null +++ b/__tests__/unit/plots/sankey/node-link-spec.ts @@ -0,0 +1,38 @@ +import { Sankey } from '../../../../src'; +import { createDiv } from '../../../utils/dom'; + +describe('sankey', () => { + it('dataType = nodeLink', () => { + const ALIPAY_DATA = { + nodes: [ + { id: 'A', name: 'A' }, + { id: 'B', name: 'B' }, + { id: 'C', name: 'C' }, + { id: 'D', name: 'D' }, + { id: 'E', name: 'E' }, + { id: 'F', name: 'F', fixedValue: 10 }, + ], + links: [ + { source: 0, target: 1, value: 160 }, + { source: 1, target: 2, value: 10 }, + { source: 2, target: 3, value: 8 }, + { source: 4, target: 3, value: 27 }, + ], + }; + + const sankey = new Sankey(createDiv(), { + height: 500, + dataType: 'node-link', + data: ALIPAY_DATA, + }); + + sankey.render(); + + const elements = sankey.chart.views[1].geometries[0].elements; + + // F 元素会现实出来 + expect(elements.some((el) => el.getData().name === 'F')).toBe(true); + + sankey.destroy(); + }); +}); diff --git a/src/plots/sankey/helper.ts b/src/plots/sankey/helper.ts index 9a6d48aa4a..bc42a88d2d 100644 --- a/src/plots/sankey/helper.ts +++ b/src/plots/sankey/helper.ts @@ -1,9 +1,19 @@ import { isRealNumber, pick } from '../../utils'; import { transformDataToNodeLinkData } from '../../utils/data'; -import { sankeyLayout } from './layout'; +import { Data } from '../../types'; +import { sankeyLayout, SankeyLayoutInputData } from './layout'; import { cutoffCircle } from './circle'; import { SankeyOptions } from './types'; +/** + * 是否是 node-link 类型的数据结构 + * @param dataTyp + * @returns + */ +function isNodeLink(dataType: string) { + return dataType === 'node-link'; +} + export function getNodeWidthRatio(nodeWidth: number, nodeWidthRatio: number, width: number) { return isRealNumber(nodeWidth) ? nodeWidth / width : nodeWidthRatio; } @@ -20,6 +30,7 @@ export function getNodePaddingRatio(nodePadding: number, nodePaddingRatio: numbe */ export function transformToViewsData(options: SankeyOptions, width: number, height: number) { const { + dataType, data, sourceField, targetField, @@ -34,13 +45,19 @@ export function transformToViewsData(options: SankeyOptions, width: number, heig rawFields = [], } = options; - const sankeyLayoutInputData = transformDataToNodeLinkData( - cutoffCircle(data, sourceField, targetField), - sourceField, - targetField, - weightField, - rawFields - ); + let sankeyLayoutInputData: unknown; + + if (!isNodeLink(dataType)) { + sankeyLayoutInputData = transformDataToNodeLinkData( + cutoffCircle(data as Data, sourceField, targetField), + sourceField, + targetField, + weightField, + rawFields + ); + } else { + sankeyLayoutInputData = data; + } // 3. layout 之后的数据 const { nodes, links } = sankeyLayout( @@ -51,7 +68,7 @@ export function transformToViewsData(options: SankeyOptions, width: number, heig nodeSort, nodeDepth, }, - sankeyLayoutInputData + sankeyLayoutInputData as SankeyLayoutInputData ); // 4. 生成绘图数据 diff --git a/src/plots/sankey/types.ts b/src/plots/sankey/types.ts index 75ad071592..dce5697ec5 100644 --- a/src/plots/sankey/types.ts +++ b/src/plots/sankey/types.ts @@ -1,20 +1,64 @@ import { Data, Options, State, StyleAttr } from '../../types'; import { NodeDepth, NodeSort } from './layout'; +/** + * node-link 数据类型的结构 + */ +export type NodeLinkData = { + /** + * 节点数据 + */ + readonly nodes: { + /** + * id 唯一即可,一般可以直接等于 name + */ + readonly id: string; + /** + * 节点的名称,用于 UI 上的现实 + */ + readonly name: string; + /** + * 节点的值,不传则节点大小有来源求和决定 + */ + readonly fixedValue?: number; + }[]; + /** + * + */ + readonly links: { + /** + * 来源节点在 nodes 中的 index + */ + readonly source: number; + /** + * 目标节点在 nodes 中的 index + */ + readonly target: number; + /** + * 边的值 + */ + readonly value: number; + }[]; +}; + /** 配置类型定义 */ -export interface SankeyOptions extends Omit { +export interface SankeyOptions extends Omit { + /** + * 数据集的类型,默认为 detail + */ + readonly dataType?: 'node-link' | 'detail'; /** - * 来源字段 + * 来源字段,dataType = 'node-link' 的时候,不用传 */ - readonly sourceField: string; + readonly sourceField?: string; /** - * 去向字段 + * 去向字段,dataType = 'node-link' 的时候,不用传 */ - readonly targetField: string; + readonly targetField?: string; /** - * 权重字段 + * 权重字段,dataType = 'node-link' 的时候,不用传 */ - readonly weightField: string; + readonly weightField?: string; /** * 附加的 元字段 */ @@ -22,7 +66,7 @@ export interface SankeyOptions extends Omit