@antv/g2
Version:
the Grammar of Graphics in Javascript
166 lines (152 loc) • 3.95 kB
text/typescript
import { deepMix } from '@antv/util';
import { CompositeMarkComponent as CC } from '../runtime';
import { SankeyMark } from '../spec';
import { Sankey as SankeyTransform } from '../data/sankey';
import { omitPrefixObject, subObject } from '../utils/helper';
import { subTooltip, maybeAnimation } from '../utils/mark';
import { field, initializeData } from './utils';
const DEFAULT_LAYOUT_OPTIONS = {
nodeId: (d) => d.key,
nodeWidth: 0.02,
nodePadding: 0.02,
};
const DEFAULT_NODE_OPTIONS = {
type: 'polygon',
axis: false,
legend: false,
encode: {
shape: 'polygon',
x: 'x',
y: 'y',
},
scale: {
x: { type: 'identity' },
y: { type: 'identity' },
},
style: {
stroke: '#000',
},
};
const DEFAULT_LINK_OPTIONS = {
type: 'polygon',
axis: false,
legend: false,
encode: {
shape: 'ribbon',
x: 'x',
y: 'y',
},
style: {
fillOpacity: 0.5,
stroke: undefined,
},
};
const DEFAULT_LABEL_OPTIONS = {
textAlign: (d) => (d.x[0] < 0.5 ? 'start' : 'end'),
position: (d) => (d.x[0] < 0.5 ? 'right' : 'left'),
fontSize: 10,
};
export type SankeyOptions = Omit<SankeyMark, 'type'>;
/**
* @todo Add interaction
* @todo Add source-link color mode
*/
export const Sankey: CC<SankeyOptions> = (options) => {
const {
data,
encode = {},
scale,
style = {},
layout = {},
nodeLabels = [],
linkLabels = [],
animate = {},
tooltip = {},
interaction,
state = {},
} = options;
// Initialize data, generating nodes by link if is not specified.
const { links, nodes } = initializeData(data, encode);
// Extract encode for node and link.
const nodeEncode = subObject(encode, 'node');
const linkEncode = subObject(encode, 'link');
const { key: nodeKey = (d) => d.key, color = nodeKey } = nodeEncode;
// Transform data, using nodeKey as nodeId.
const { links: linkData, nodes: nodeData } = SankeyTransform({
...DEFAULT_LAYOUT_OPTIONS,
nodeId: field(nodeKey),
...layout,
})({ links, nodes });
// Extract label style and apply defaults.
const {
text = nodeKey,
spacing = 5,
...labelStyle
} = subObject(style, 'label');
const key1 = field(nodeKey);
const nodeTooltip = subTooltip(
tooltip,
'node',
{
title: key1,
items: [{ field: 'value' }],
},
true,
);
const linkTooltip = subTooltip(tooltip, 'link', {
title: '',
items: [
(d) => ({ name: 'source', value: key1(d.source) }),
(d) => ({ name: 'target', value: key1(d.target) }),
],
});
// Extract node and link state.
const [nodeState, linkState] = Object.entries(state).reduce(
(acc, [stateName, styleObj]) => {
const commonState = omitPrefixObject(styleObj, 'node', 'link');
const nodeState = subObject(styleObj, 'node');
acc[0][stateName] = { ...commonState, ...nodeState };
const linkState = subObject(styleObj, 'link');
acc[1][stateName] = { ...commonState, ...linkState };
return acc;
},
[{}, {}],
);
return [
deepMix({}, DEFAULT_NODE_OPTIONS, {
data: nodeData,
encode: { ...nodeEncode, color },
scale,
style: subObject(style, 'node'),
labels: [
{
...DEFAULT_LABEL_OPTIONS,
text,
dx: (d) => (d.x[0] < 0.5 ? spacing : -spacing),
...labelStyle,
},
...nodeLabels,
],
tooltip: nodeTooltip,
animate: maybeAnimation(animate, 'node'),
axis: false,
interaction,
state: nodeState,
}),
deepMix({}, DEFAULT_LINK_OPTIONS, {
data: linkData,
encode: linkEncode,
labels: linkLabels,
style: {
fill: linkEncode.color ? undefined : '#aaa',
lineWidth: 0,
...subObject(style, 'link'),
},
tooltip: linkTooltip,
animate: maybeAnimation(animate, 'link'),
interaction,
state: linkState,
}),
];
};
Sankey.props = {};