Skip to content

Commit

Permalink
feat(sankey): remove the circle data in sankey (#2178)
Browse files Browse the repository at this point in the history
* feat(sankey): remove the circle data in sankey

* test(sankey): add plot test of sankey with circle

* chore: add warn log for remove data

* fix(sankey): when node has multi parent node
  • Loading branch information
hustcc authored Jan 7, 2021
1 parent 383d8f7 commit e248c8e
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 1 deletion.
94 changes: 94 additions & 0 deletions __tests__/unit/plots/sankey/circle-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { cutoffCircle } from '../../../../src/plots/sankey/circle';
import { ENERGY_RELATIONS } from '../../../data/sankey-energy';

describe('sankey ', () => {
it('cutoffCircle', () => {
let data = [
{ source: 'a', target: 'b' },
{ source: 'b', target: 'c' },
{ source: 'a', target: 'c' },
{ source: 'c', target: 'd' },
];

// 不成环
expect(cutoffCircle(data, 'source', 'target')).toEqual([
{ source: 'a', target: 'b' },
{ source: 'b', target: 'c' },
{ source: 'a', target: 'c' },
{ source: 'c', target: 'd' },
]);

// 两节点环
data = [
{ source: 'a', target: 'b' },
{ source: 'b', target: 'a' },
];

expect(cutoffCircle(data, 'source', 'target')).toEqual([{ source: 'a', target: 'b' }]);

// 三节点环
data = [
{ source: 'a', target: 'b' },
{ source: 'b', target: 'c' },
{ source: 'c', target: 'a' },
];

expect(cutoffCircle(data, 'source', 'target')).toEqual([
{ source: 'a', target: 'b' },
{ source: 'b', target: 'c' },
]);

// 多个环
data = [
{ source: 'a', target: 'b' },
{ source: 'b', target: 'c' },
{ source: 'c', target: 'a' },
{ source: 'a', target: 'd' },
{ source: 'd', target: 'e' },
{ source: 'e', target: 'a' },
];

expect(cutoffCircle(data, 'source', 'target')).toEqual([
{ source: 'a', target: 'b' },
{ source: 'b', target: 'c' },
{ source: 'a', target: 'd' },
{ source: 'd', target: 'e' },
]);

// 一条边产生两个环
data = [
{ source: 'a', target: 'b' },
{ source: 'b', target: 'c' },
{ source: 'c', target: 'a' }, // 它带来两个环
{ source: 'a', target: 'd' },
{ source: 'd', target: 'c' },
];

expect(cutoffCircle(data, 'source', 'target')).toEqual([
{ source: 'a', target: 'b' },
{ source: 'b', target: 'c' },
{ source: 'a', target: 'd' },
{ source: 'd', target: 'c' },
]);

// 节点多个父
data = [
{ source: 'a', target: 'c' },
{ source: 'b', target: 'c' },
{ source: 'c', target: 'a' },
];

expect(cutoffCircle(data, 'source', 'target')).toEqual([
{ source: 'a', target: 'c' },
{ source: 'b', target: 'c' },
]);

// 稍微正式一点的数据
expect(cutoffCircle(ENERGY_RELATIONS, 'source', 'target')).toEqual(ENERGY_RELATIONS);
expect(cutoffCircle(ENERGY_RELATIONS, 'source', 'target')).not.toBe(ENERGY_RELATIONS);

// 空数据
expect(cutoffCircle(null, 'source', 'target')).toEqual([]);
expect(cutoffCircle(undefined, 'source', 'target')).toEqual([]);
});
});
23 changes: 23 additions & 0 deletions __tests__/unit/plots/sankey/index-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,27 @@ describe('sankey', () => {

sankey.destroy();
});

it('sankey circle', () => {
const DATA = [
{ source: 'a', target: 'b', value: 160 },
{ source: 'b', target: 'c', value: 40 },
{ source: 'c', target: 'd', value: 10 },
{ source: 'd', target: 'a', value: 10 },
];

const sankey = new Sankey(createDiv(), {
data: DATA,
sourceField: 'source',
targetField: 'target',
weightField: 'value',
});

sankey.render();

// 被去掉环
expect(sankey.chart.views[1].getOptions().data.length).toBe(3);

sankey.destroy();
});
});
8 changes: 7 additions & 1 deletion src/plots/sankey/adaptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { polygon, edge } from '../../adaptor/geometries';
import { transformDataToNodeLinkData } from '../../utils/data';
import { SankeyOptions } from './types';
import { X_FIELD, Y_FIELD, COLOR_FIELD } from './constant';
import { cutoffCircle } from './circle';

/**
* geometry 处理
Expand Down Expand Up @@ -35,7 +36,12 @@ function geometry(params: Params<SankeyOptions>): Params<SankeyOptions> {
chart.axis(false);

// 2. 转换出 layout 前数据
const sankeyLayoutInputData = transformDataToNodeLinkData(data, sourceField, targetField, weightField);
const sankeyLayoutInputData = transformDataToNodeLinkData(
cutoffCircle(data, sourceField, targetField),
sourceField,
targetField,
weightField
);

// 3. layout 之后的数据
const { nodes, links } = sankeyLayout(
Expand Down
57 changes: 57 additions & 0 deletions src/plots/sankey/circle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { each, size } from '@antv/util';
import { Data, Datum } from '../../types';

/**
* 是否有环的判断依据是,当前 source 对应的 target 是 source 的父节点
* @param circleCache
* @param source
* @param target
*/
function hasCircle(circleCache: Map<string, string[]>, source: string[], target: string): boolean {
// 父元素为空,则表示已经到头了!
if (size(source) === 0) return false;
// target 在父元素路径上,所以形成环
if (source.includes(target)) return true;

// 递归
return source.some((s: string) => hasCircle(circleCache, circleCache.get(s), target));
}

/**
* 切断桑基图数据中的环(会丢失数据),保证顺序
* @param data
* @param sourceField
* @param targetField
*/
export function cutoffCircle(data: Data, sourceField: string, targetField: string): Data {
const dataWithoutCircle = [];
const removedData = [];

/** 存储父子关系的链表关系,具体是 子 -> 父 数组 */
const circleCache = new Map<string, string[]>();

each(data, (d: Datum) => {
const source = d[sourceField] as string;
const target = d[targetField] as string;

// 当前数据,不成环
if (!hasCircle(circleCache, [source], target)) {
// 保留数据
dataWithoutCircle.push(d);
// 存储关系链表
if (!circleCache.has(target)) {
circleCache.set(target, []);
}
circleCache.get(target).push(source);
} else {
// 保存起来用于打印 log
removedData.push(d);
}
});

if (removedData.length !== 0) {
console.warn(`sankey data contains circle, ${removedData.length} records removed.`, removedData);
}

return dataWithoutCircle;
}

0 comments on commit e248c8e

Please sign in to comment.