@progress/kendo-charts
Version:
Kendo UI platform-independent Charts library
955 lines (782 loc) • 30.8 kB
JavaScript
import { geometry, drawing } from '@progress/kendo-drawing';
import { deepExtend, addClass, Observable, setDefaultOptions } from '../common';
import { calculateSankey, crossesValue } from './calculation';
import { Node, resolveNodeOptions } from './node';
import { Link, resolveLinkOptions } from './link';
import { Label, resolveLabelOptions } from './label';
import { Title } from './title';
import { BLACK, BOTTOM, LEFT, RIGHT, TOP } from '../common/constants';
import { Box, rectToBox } from '../core';
import { Legend } from './legend';
import { defined } from '../drawing-utils';
const LINK = 'link';
const NODE = 'node';
const toRtl = sankey => {
const { nodes, links } = sankey;
const startX = Math.min(...nodes.map(node => node.x0));
const endX = Math.max(...nodes.map(node => node.x1));
const width = endX - startX;
nodes.forEach(node => {
const x0 = width - (node.x1 - 2 * startX);
const x1 = width - (node.x0 - 2 * startX);
node.x0 = x0;
node.x1 = x1;
});
links.forEach(link => {
const x0 = width - (link.x1 - 2 * startX);
const x1 = width - (link.x0 - 2 * startX);
link.x1 = x0;
link.x0 = x1;
});
};
export class Sankey extends Observable {
constructor(element, options, theme) {
super();
this._initTheme(theme);
this._setOptions(options);
this._initElement(element);
this._initSurface();
if (options && options.data) {
this._redraw();
this._initResizeObserver();
this._initNavigation(element);
}
}
destroy() {
this.unbind();
this._destroySurface();
this._destroyResizeObserver();
if (this.element) {
this.element.removeEventListener('keydown', this._keydownHandler);
this.element.removeEventListener('focus', this._focusHandler);
this.element.removeEventListener('mousedown', this._onDownHandler);
this.element.removeEventListener('touchstart', this._onDownHandler);
this.element.removeEventListener('pointerdown', this._onDownHandler);
}
this._focusState = null;
this.element = null;
}
_initElement(element) {
this.element = element;
addClass(element, [ "k-chart", "k-sankey" ]);
element.setAttribute('role', 'graphics-document');
const { title } = this.options;
if (title.text) {
element.setAttribute('aria-label', title.text);
}
if (title.description) {
element.setAttribute("aria-roledescription", title.description);
}
}
_initSurface() {
if (!this.surface) {
this._destroySurface();
this._initSurfaceElement();
this.surface = this._createSurface();
}
}
_initNavigation(element) {
element.tabIndex = element.getAttribute("tabindex") || 0;
if (this.options.disableKeyboardNavigation) {
return;
}
this._keydownHandler = this._keydown.bind(this);
this._focusHandler = this._focus.bind(this);
this._blurHandler = this._blur.bind(this);
this._onDownHandler = this._onDown.bind(this);
element.addEventListener('keydown', this._keydownHandler);
element.addEventListener('focus', this._focusHandler);
element.addEventListener('blur', this._blurHandler);
element.addEventListener('mousedown', this._onDownHandler);
element.addEventListener('touchstart', this._onDownHandler);
element.addEventListener('pointerdown', this._onDownHandler);
this._focusState = {
node: this.firstFocusableNode(),
link: null
};
}
firstFocusableNode() {
return this.columns[0][0];
}
_initResizeObserver() {
const observer = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
if (entry.target !== this.element ||
(this.size && this.size.width === width && this.size.height === height)) {
return;
}
this.size = { width, height };
this.surface.setSize(this.size);
this.resize = true;
this._redraw();
});
});
this._resizeObserver = observer;
observer.observe(this.element);
}
_createSurface() {
return drawing.Surface.create(this.surfaceElement, {
mouseenter: this._mouseenter.bind(this),
mouseleave: this._mouseleave.bind(this),
mousemove: this._mousemove.bind(this),
click: this._click.bind(this)
});
}
_initTheme(theme) {
let currentTheme = theme || this.theme || {};
this.theme = currentTheme;
this.options = deepExtend({}, currentTheme, this.options);
}
setLinksOpacity(opacity) {
this.linksVisuals.forEach(link => {
this.setOpacity(link, opacity, link.linkOptions.opacity);
});
}
setLinksInactivityOpacity(inactiveOpacity) {
this.linksVisuals.forEach(link => {
this.setOpacity(link, inactiveOpacity, link.linkOptions.highlight.inactiveOpacity);
});
}
setOpacity(link, opacity, linkValue) {
link.options.set('stroke', Object.assign({}, link.options.stroke,
{opacity: defined(linkValue) ? linkValue : opacity}));
}
trigger(name, ev) {
let dataItem = ev.element.dataItem;
const targetType = ev.element.type;
const event = Object.assign({}, ev,
{type: name,
targetType,
dataItem: dataItem});
return super.trigger(name, event);
}
_mouseenter(ev) {
const element = ev.element;
const isLink = element.type === LINK;
const isNode = element.type === NODE;
const isLegendItem = Boolean(element.chartElement && element.chartElement.options.node);
if ((isLink && this.trigger('linkEnter', ev)) ||
(isNode && this.trigger('nodeEnter', ev))) {
return;
}
const { highlight } = this.options.links;
if (isLink) {
this.setLinksInactivityOpacity(highlight.inactiveOpacity);
this.setOpacity(element, highlight.opacity, element.linkOptions.highlight.opacity);
} else if (isNode) {
this.highlightLinks(element, highlight);
} else if (isLegendItem) {
const nodeVisual = this.nodesVisuals.get(element.chartElement.options.node.id);
this.highlightLinks(nodeVisual, highlight);
}
}
_mouseleave(ev) {
const element = ev.element;
const isLink = element.type === LINK;
const isNode = element.type === NODE;
const isLegendItem = Boolean(element.chartElement && element.chartElement.options.node);
const target = ev.originalEvent.relatedTarget;
if (isLink && target && target.nodeName === 'text') {
return;
}
if (isLink || isNode) {
if (this.tooltipTimeOut) {
clearTimeout(this.tooltipTimeOut);
this.tooltipTimeOut = null;
}
this.tooltipShown = false;
this.trigger('tooltipHide', ev);
}
if ((isLink && this.trigger('linkLeave', ev)) ||
(isNode && this.trigger('nodeLeave', ev))) {
return;
}
if (isLink || isNode || isLegendItem) {
this.linksVisuals.forEach(link => {
this.setOpacity(link, this.options.links.opacity, link.linkOptions.opacity);
});
}
}
_mousemove(ev) {
const { followPointer, delay } = this.options.tooltip;
const element = ev.element;
const tooltipElType = element.type;
if ((tooltipElType !== LINK && tooltipElType !== NODE) || (this.tooltipShown && !followPointer)) {
return;
}
const mouseEvent = ev.originalEvent;
const rect = this.element.getBoundingClientRect();
const isLeft = mouseEvent.clientX - rect.left < rect.width / 2;
const isTop = mouseEvent.clientY - rect.top < rect.height / 2;
ev.tooltipData = {
popupOffset: {
left: mouseEvent.pageX,
top: mouseEvent.pageY
},
popupAlign: {
horizontal: isLeft ? 'left' : 'right',
vertical: isTop ? 'top' : 'bottom'
}
};
if (tooltipElType === NODE) {
const { sourceLinks, targetLinks } = element.dataItem;
const links = targetLinks.length ? targetLinks : sourceLinks;
ev.nodeValue = links.reduce((acc, link) => acc + link.value, 0);
}
if (this.tooltipTimeOut) {
clearTimeout(this.tooltipTimeOut);
this.tooltipTimeOut = null;
}
const nextDelay = followPointer && this.tooltipShown ? 0 : delay;
this.tooltipTimeOut = setTimeout(() => {
this.trigger('tooltipShow', ev);
this.tooltipShown = true;
this.tooltipTimeOut = null;
}, nextDelay);
}
_click(ev) {
const element = ev.element;
const dataItem = element.dataItem;
const isLink = element.type === LINK;
const isNode = element.type === NODE;
const focusState = this._focusState || {};
if (isNode) {
const focusedNodeClicked = !focusState.link && this.sameNode(focusState.node, dataItem);
if (!focusedNodeClicked) {
this._focusState = { node: dataItem, link: null };
this._focusNode({ highlight: false });
}
this.trigger('nodeClick', ev);
} else if (isLink) {
const link = {
sourceId: dataItem.source.id,
targetId: dataItem.target.id,
value: dataItem.value
};
const focusedLinkClicked = this.sameLink(focusState.link, link);
if (!focusedLinkClicked) {
this._focusState = { node: dataItem.source, link: link };
this._focusLink({ highlight: false });
}
this.trigger('linkClick', ev);
}
}
sameNode(node1, node2) {
return node1 && node2 && node1.id === node2.id;
}
sameLink(link1, link2) {
return link1 && link2 && link1.sourceId === link2.sourceId && link1.targetId === link2.targetId;
}
_focusNode(options) {
this._cleanFocusHighlight();
const nodeData = this._focusState.node;
const node = this.models.map.get(nodeData.id);
node.focus(options);
}
_focusLink(options) {
this._cleanFocusHighlight();
const linkData = this._focusState.link;
const link = this.models.map.get(`${linkData.sourceId}-${linkData.targetId}`);
link.focus(options);
}
_focusNextNode(direction = 1) {
const current = this._focusState.node;
const columnIndex = this.columns.findIndex(column => column.find(n => n.id === current.id));
const columnNodes = this.columns[columnIndex];
const nodeIndex = columnNodes.findIndex(n => n.id === current.id);
const nextNode = columnNodes[nodeIndex + direction];
if (nextNode) {
this._focusState.node = nextNode;
this._focusNode();
}
}
_focusNextLink(direction = 1) {
const node = this._focusState.node;
const link = this._focusState.link;
const sourceLinkIndex = node.sourceLinks.findIndex(l => l.sourceId === link.sourceId && l.targetId === link.targetId);
const targetLinkIndex = node.targetLinks.findIndex(l => l.sourceId === link.sourceId && l.targetId === link.targetId);
if (sourceLinkIndex !== -1) {
const nextLink = node.sourceLinks[sourceLinkIndex + direction];
if (nextLink) {
this._focusState.link = nextLink;
this._focusLink();
}
} else if (targetLinkIndex !== -1) {
const nextLink = node.targetLinks[targetLinkIndex + direction];
if (nextLink) {
this._focusState.link = nextLink;
this._focusLink();
}
}
}
_focusSourceNode() {
const linkData = this._focusState.link;
const sourceNode = this.models.map.get(linkData.sourceId);
this._focusState = { node: sourceNode.options.node, link: null };
this._focusNode();
}
_focusTargetNode() {
const linkData = this._focusState.link;
const targetNode = this.models.map.get(linkData.targetId);
this._focusState = { node: targetNode.options.node, link: null };
this._focusNode();
}
_focusSourceLink() {
const nodeData = this._focusState.node;
const sourceLinks = nodeData.sourceLinks;
const linkData = sourceLinks[0];
if (linkData) {
this._focusState.link = linkData;
this._focusLink();
}
}
_focusTargetLink() {
const nodeData = this._focusState.node;
const targetLinks = nodeData.targetLinks;
const linkData = targetLinks[0];
if (linkData) {
this._focusState.link = linkData;
this._focusLink();
}
}
_focus() {
if (!this._skipFocusHighlight) {
if (this._focusState.link) {
this._focusLink();
} else {
this._focusNode();
}
}
this._skipFocusHighlight = false;
}
_blur() {
this._cleanFocusHighlight();
}
_onDown() {
if (!this._hasFocus()) {
this._skipFocusHighlight = true;
}
}
_hasFocus() {
return this.element.ownerDocument.activeElement === this.element;
}
_cleanFocusHighlight() {
this.models.nodes.forEach(node => node.blur());
this.models.links.forEach(link => link.blur());
}
_keydown(ev) {
let handler = this['on' + ev.key];
const rtl = this.options.rtl;
if (rtl && ev.key === 'ArrowLeft') {
handler = this.onArrowRight;
} else if (rtl && ev.key === 'ArrowRight') {
handler = this.onArrowLeft;
}
if (handler) {
handler.call(this, ev);
}
}
onEscape(ev) {
ev.preventDefault();
this._focusState = { node: this.firstFocusableNode(), link: null };
this._focusNode();
}
onArrowDown(ev) {
ev.preventDefault();
if (this._focusState.link) {
this._focusNextLink(1);
} else {
this._focusNextNode(1);
}
}
onArrowUp(ev) {
ev.preventDefault();
if (this._focusState.link) {
this._focusNextLink(-1);
} else {
this._focusNextNode(-1);
}
}
onArrowLeft(ev) {
ev.preventDefault();
if (this._focusState.link) {
this._focusSourceNode();
} else {
this._focusTargetLink();
}
}
onArrowRight(ev) {
ev.preventDefault();
if (this._focusState.link) {
this._focusTargetNode();
} else {
this._focusSourceLink();
}
}
highlightLinks(node, highlight) {
if (node) {
this.setLinksInactivityOpacity(highlight.inactiveOpacity);
node.links.forEach(link => {
this.setOpacity(link, highlight.opacity, link.linkOptions.highlight.opacity);
});
}
}
_destroySurface() {
if (this.surface) {
this.surface.destroy();
this.surface = null;
this._destroySurfaceElement();
}
}
_destroyResizeObserver() {
if (this._resizeObserver) {
this._resizeObserver.disconnect();
this._resizeObserver = null;
}
}
_initSurfaceElement() {
if (!this.surfaceElement) {
this.surfaceElement = document.createElement('div');
this.element.appendChild(this.surfaceElement);
}
}
_destroySurfaceElement() {
if (this.surfaceElement && this.surfaceElement.parentNode) {
this.surfaceElement.parentNode.removeChild(this.surfaceElement);
this.surfaceElement = null;
}
}
setOptions(options, theme) {
this._setOptions(options);
this._initTheme(theme);
this._initSurface();
this._redraw();
}
_redraw() {
this.surface.clear();
const { width, height } = this._getSize();
this.size = { width, height };
this.surface.setSize(this.size);
this.createVisual();
this.surface.draw(this.visual);
}
_getSize() {
return this.element.getBoundingClientRect();
}
createVisual() {
this.visual = this._render();
}
titleBox(title, drawingRect) {
if (!title || title.visible === false || !title.text) {
return null;
}
const titleElement = new Title(Object.assign({}, {drawingRect}, title));
const titleVisual = titleElement.exportVisual();
return titleVisual.chartElement.box;
}
legendBox(options, nodes, drawingRect) {
if (!options || options.visible === false) {
return null;
}
const legend = new Legend(Object.assign({}, {nodes}, options, {drawingRect}));
const legendVisual = legend.exportVisual();
return legendVisual.chartElement.box;
}
calculateSankey(calcOptions, sankeyOptions) {
const { title, legend, data, nodes, labels, nodeColors, disableAutoLayout, disableKeyboardNavigation, rtl } = sankeyOptions;
const autoLayout = !disableAutoLayout;
const focusHighlightWidth = ((nodes.focusHighlight || {}).border || {}).width || 0;
const padding = disableKeyboardNavigation ? 0 : focusHighlightWidth / 2;
const sankeyBox = new Box(0, 0, calcOptions.width, calcOptions.height);
sankeyBox.unpad(padding);
const titleBox = this.titleBox(title, sankeyBox);
let legendArea = sankeyBox.clone();
if (titleBox) {
const titleHeight = titleBox.height();
if (title.position === TOP) {
sankeyBox.unpad({ top: titleHeight });
legendArea = new Box(0, titleHeight, calcOptions.width, calcOptions.height);
} else {
sankeyBox.shrink(0, titleHeight);
legendArea = new Box(0, 0, calcOptions.width, calcOptions.height - titleHeight);
}
}
const legendBox = this.legendBox(legend, data.nodes, legendArea);
const legendPosition = (legend && legend.position) || Legend.prototype.options.position;
if (legendBox) {
if (legendPosition === LEFT) {
sankeyBox.unpad({ left: legendBox.width() });
}
if (legendPosition === RIGHT) {
sankeyBox.shrink(legendBox.width(), 0);
}
if (legendPosition === TOP) {
sankeyBox.unpad({ top: legendBox.height() });
}
if (legendPosition === BOTTOM) {
sankeyBox.shrink(0, legendBox.height());
}
}
const { nodes: calculatedNodes, circularLinks } = calculateSankey(Object.assign({}, calcOptions, {offsetX: 0, offsetY: 0, width: sankeyBox.width(), height: sankeyBox.height()}));
if (circularLinks) {
console.warn('Circular links detected. Kendo Sankey diagram does not support circular links.');
return { sankey: { nodes: [], links: [], columns: [[]], circularLinks }, legendBox, titleBox };
}
const box = new Box();
const diagramMinX = calculatedNodes.reduce((acc, node) => Math.min(acc, node.x0), Infinity);
const diagramMaxX = calculatedNodes.reduce((acc, node) => Math.max(acc, node.x1), 0);
calculatedNodes.forEach((nodeEl, i) => {
if (rtl) {
const { x0, x1 } = nodeEl;
nodeEl.x0 = diagramMaxX - x1;
nodeEl.x1 = diagramMaxX - x0;
}
const nodeOps = resolveNodeOptions(nodeEl, nodes, nodeColors, i);
const nodeInstance = new Node(nodeOps);
box.wrap(rectToBox(nodeInstance.exportVisual().rawBBox()));
const labelInstance = new Label(resolveLabelOptions(nodeEl, labels, rtl, diagramMinX, diagramMaxX));
const labelVisual = labelInstance.exportVisual();
if (labelVisual) {
box.wrap(rectToBox(labelVisual.rawBBox()));
}
});
let offsetX = sankeyBox.x1;
let offsetY = sankeyBox.y1;
let width = sankeyBox.width() + offsetX;
let height = sankeyBox.height() + offsetY;
width -= box.x2 > sankeyBox.width() ? box.x2 - sankeyBox.width() : 0;
height -= box.y2 > sankeyBox.height() ? box.y2 - sankeyBox.height() : 0;
offsetX += box.x1 < 0 ? -box.x1 : 0;
offsetY += box.y1 < 0 ? -box.y1 : 0;
if (autoLayout === false) {
return {
sankey: calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height, autoLayout: false})),
legendBox,
titleBox
};
}
if (this.resize && autoLayout && this.permutation) {
this.resize = false;
return {
sankey: calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height}, this.permutation)),
legendBox,
titleBox
};
}
const startColumn = 1;
const loops = 2;
const columnsLength = calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height, autoLayout: false})).columns.length;
const results = [];
const permutation = (targetColumnIndex, reverse) => {
let currPerm = calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height, loops: loops, targetColumnIndex, reverse}));
let crosses = crossesValue(currPerm.links);
results.push({
crosses: crosses,
reverse: reverse,
targetColumnIndex: targetColumnIndex
});
return crosses === 0;
};
for (let index = startColumn; index <= columnsLength - 1; index++) {
if (permutation(index, false) || permutation(index, true)) {
break;
}
}
const minCrosses = Math.min.apply(null, results.map(r => r.crosses));
const bestResult = results.find(r => r.crosses === minCrosses);
this.permutation = { targetColumnIndex: bestResult.targetColumnIndex, reverse: bestResult.reverse };
const result = calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height}, this.permutation));
return {
sankey: result,
legendBox,
titleBox
};
}
_render(options, context) {
const sankeyOptions = options || this.options;
const sankeyContext = context || this;
const { labels: labelOptions, nodes: nodesOptions, links: linkOptions, nodeColors, title, legend, rtl, disableKeyboardNavigation } = sankeyOptions;
let data = sankeyOptions.data;
const { width, height } = sankeyContext.size;
const calcOptions = Object.assign({}, data, {width, height, nodesOptions, title, legend});
const { sankey, titleBox, legendBox } = this.calculateSankey(calcOptions, sankeyOptions);
if (rtl) {
toRtl(sankey);
}
const { nodes, links, columns } = sankey;
sankeyContext.columns = columns.map(column => {
const newColumn = column.slice();
newColumn.sort((a, b) => a.y0 - b.y0);
return newColumn;
});
const visual = new drawing.Group({
clip: drawing.Path.fromRect(new geometry.Rect([0, 0], [width, height]))
});
if (titleBox) {
const titleElement = new Title(Object.assign({}, title, {drawingRect: titleBox}));
const titleVisual = titleElement.exportVisual();
visual.append(titleVisual);
}
if (sankey.circularLinks) {
return visual;
}
const visualNodes = new Map();
sankeyContext.nodesVisuals = visualNodes;
const models = {
nodes: [],
links: [],
map: new Map()
};
sankeyContext.models = models;
const focusHighlights = [];
nodes.forEach((node, i) => {
const nodeOps = resolveNodeOptions(node, nodesOptions, nodeColors, i);
nodeOps.root = () => sankeyContext.element;
nodeOps.navigatable = disableKeyboardNavigation !== true;
const nodeInstance = new Node(nodeOps);
const nodeVisual = nodeInstance.exportVisual();
nodeVisual.links = [];
nodeVisual.type = NODE;
node.color = nodeOps.color;
node.opacity = nodeOps.opacity;
nodeVisual.dataItem = Object.assign({}, data.nodes[i],
{color: nodeOps.color,
opacity: nodeOps.opacity,
sourceLinks: node.sourceLinks.map(link => ({ sourceId: link.sourceId, targetId: link.targetId, value: link.value })),
targetLinks: node.targetLinks.map(link => ({ sourceId: link.sourceId, targetId: link.targetId, value: link.value }))});
visualNodes.set(node.id, nodeVisual);
models.nodes.push(nodeInstance);
models.map.set(node.id, nodeInstance);
visual.append(nodeVisual);
nodeInstance.createFocusHighlight();
if (nodeInstance._highlight) {
focusHighlights.push(nodeInstance._highlight);
}
});
const sortedLinks = links.slice().sort((a, b) => b.value - a.value);
const linksVisuals = [];
sankeyContext.linksVisuals = linksVisuals;
sortedLinks.forEach(link => {
const { source, target } = link;
const sourceNode = visualNodes.get(source.id);
const targetNode = visualNodes.get(target.id);
const resolvedOptions = resolveLinkOptions(link, linkOptions, sourceNode, targetNode);
resolvedOptions.root = () => sankeyContext.element;
resolvedOptions.navigatable = disableKeyboardNavigation !== true;
resolvedOptions.rtl = rtl;
const linkInstance = new Link(resolvedOptions);
const linkVisual = linkInstance.exportVisual();
linkVisual.type = LINK;
linkVisual.dataItem = {
source: Object.assign({}, sourceNode.dataItem),
target: Object.assign({}, targetNode.dataItem),
value: link.value
};
linkVisual.linkOptions = resolvedOptions;
linksVisuals.push(linkVisual);
sourceNode.links.push(linkVisual);
targetNode.links.push(linkVisual);
models.links.push(linkInstance);
models.map.set(`${source.id}-${target.id}`, linkInstance);
linkInstance.createFocusHighlight();
if (linkInstance._highlight) {
focusHighlights.push(linkInstance._highlight);
}
visual.append(linkVisual);
});
const diagramMinX = nodes.reduce((acc, node) => Math.min(acc, node.x0), Infinity);
const diagramMaxX = nodes.reduce((acc, node) => Math.max(acc, node.x1), 0);
nodes.forEach((node) => {
const textOps = resolveLabelOptions(node, labelOptions, rtl, diagramMinX, diagramMaxX);
const labelInstance = new Label(textOps);
const labelVisual = labelInstance.exportVisual();
if (labelVisual) {
visual.append(labelVisual);
}
});
if (legendBox) {
const legendElement = new Legend(Object.assign({}, legend, {rtl, drawingRect: legendBox, nodes}));
const legendVisual = legendElement.exportVisual();
visual.append(legendVisual);
}
if (focusHighlights.length !== 0) {
const focusHighlight = new drawing.Group();
focusHighlight.append(...focusHighlights);
visual.append(focusHighlight);
}
return visual;
}
exportVisual(exportOptions) {
const options = (exportOptions && exportOptions.options) ?
deepExtend({}, this.options, exportOptions.options) : this.options;
const context = {
size: {
width: defined(exportOptions && exportOptions.width) ? exportOptions.width : this.size.width,
height: defined(exportOptions && exportOptions.height) ? exportOptions.height : this.size.height
}
};
return this._render(options, context);
}
_setOptions(options) {
this.options = deepExtend({}, this.options, options);
}
}
const highlightOptions = {
opacity: 1,
width: 2,
color: BLACK
};
setDefaultOptions(Sankey, {
title: {
position: TOP, // 'top', 'bottom'
},
labels: {
visible: true,
margin: {
left: 8,
right: 8
},
padding: 0,
border: {
width: 0
},
paintOrder: 'stroke',
stroke: {
lineJoin: "round",
width: 1
},
offset: { left: 0, top: 0 }
},
nodes: {
width: 24,
padding: 16,
opacity: 1,
align: 'stretch', // 'left', 'right', 'stretch'
offset: { left: 0, top: 0 },
focusHighlight: {
border: Object.assign({}, highlightOptions)
},
labels: {
ariaTemplate: ({ node }) => node.label.text
}
},
links: {
colorType: 'static', // 'source', 'target', 'static'
opacity: 0.4,
highlight: {
opacity: 0.8,
inactiveOpacity: 0.2
},
focusHighlight: {
border: Object.assign({}, highlightOptions)
},
labels: {
ariaTemplate: ({ link }) => `${link.source.label.text} to ${link.target.label.text}`
}
},
tooltip: {
followPointer: false,
delay: 200
}
});