Skip to content
Merged
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
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;
}