@syncfusion/ej2-charts
Version:
Feature-rich chart control with built-in support for over 25 chart types, technical indictors, trendline, zooming, tooltip, selection, crosshair and trackball.
1,146 lines • 69.4 kB
JavaScript
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Component, Property, NotifyPropertyChanges, Internationalization, Complex, Browser, EventHandler, Collection, Event } from '@syncfusion/ej2-base';
import { Margin, Border, Accessibility, Animation } from '../common/model/base';
import { Rect, Size, SvgRenderer, TextOption, measureText } from '@syncfusion/ej2-svg-base';
import { appendChildElement, createSvg, getTextAnchor, getTitle, textElement, titlePositionX, ImageOption, redrawElement } from '../common/utils/helper';
import { RectOption, removeElement } from '../common/utils/helper';
import { SankeySeries } from './series/series';
import { SankeyLabelSettings, SankeyLinkSettings, SankeyNodeSettings, SankeyTitleStyle, SankeyLegendSettings, SankeyTooltipSettings, SankeyNode, SankeyLink } from './model/sankey-base';
import { SankeyTooltip } from './user-interaction/tooltip';
import { PrintUtils } from '../common/utils/print';
import { SankeyHighlight } from './user-interaction/highlight';
import { getSankeyThemeColor } from './model/sankey-theme';
/**
* Represents the Sankey diagram control for visualizing flow quantities between entities.
*
* const sankey = new Sankey({
* nodes: [
* { id: 'A', label: 'Source A' },
* { id: 'B', label: 'Process B' },
* { id: 'C', label: 'Destination C' }
* ],
* links: [
* { source: 'A', target: 'B', value: 40 },
* { source: 'B', target: 'C', value: 35 },
* { source: 'A', target: 'C', value: 5 }
* ],
* orientation: 'Horizontal,
* tooltip: { enabled: true }
* });
*
* @public
*/
var Sankey = /** @class */ (function (_super) {
__extends(Sankey, _super);
/**
* Initializes the Sankey chart instance and its core modules.
*
* @param {SankeyModel} [options] - The Sankey configuration model used to initialize the component.
* @param {string | HTMLElement} [element] - The host element or element id used to render the component.
* @private
*/
function Sankey(options, element) {
var _this = _super.call(this, options, element) || this;
/** @private */
_this.previousTargetId = '';
/** @private */
_this.currentGroup = null;
/** @private */
_this.currentNodeIndex = 0;
/** @private */
_this.currentLinkIndex = 0;
/** @private */
_this.currentLegendIndex = 0;
/** @private */
_this.resizeBound = null;
/**
* Holds the currently hovered Sankey node instance.
*
* @private
*/
_this.hoveredNode = null;
/**
* Holds the currently hovered Sankey link instance.
*
* @private
*/
_this.hoveredLink = null;
_this.sankeySeriesModule = new SankeySeries(_this);
return _this;
}
/**
* Prepares the Sankey component for rendering by wiring events, applying culture/RTL settings, and computing available size.
*
* @returns {void}
* @private
*/
Sankey.prototype.preRender = function () {
this.allowServerDataBinding = false;
this.unWireEvents();
this.setCulture();
this.wireEvents();
if (this.element && this.element.className.indexOf('e-sankey') === -1) {
this.element.classList.add('e-sankey');
}
this.element.setAttribute('dir', this.enableRtl ? 'rtl' : 'ltr');
this.element.style.outline = 'none';
this.animateSeries = true;
this.calculateAvailableSize();
};
/**
* Renders the Sankey component by initializing SVG, computing nodes/legend/bounds, and drawing visual elements.
*
* @returns {void}
* @private
*/
Sankey.prototype.render = function () {
var _this = this;
var loadEventData = {
chart: this, theme: this.theme, cancel: false
};
this.element.setAttribute('role', this.accessibility.accessibilityRole || 'region');
this.element.setAttribute('tabindex', this.accessibility.focusable ? String(this.accessibility.tabIndex) : '-1');
var regionLabel = this.accessibility.accessibilityDescription
|| (this.title ? this.title + ". Interactive Sankey diagram." : 'Interactive Sankey diagram.');
this.element.setAttribute('aria-label', regionLabel);
// Trigger `load` event and proceed with rendering inside the callback
this.trigger('load', loadEventData, function () {
_this.renderer = new SvgRenderer(_this.element.id);
_this.setTheme();
_this.calculateAvailableSize();
_this.createChartSvg();
if (!_this.sankeySeriesModule) {
_this.sankeySeriesModule = new SankeySeries(_this);
}
if (_this.sankeyHighlightModule) {
_this.sankeyHighlightModule.destroy();
_this.sankeyHighlightModule = null;
}
if (_this.linkStyle && (_this.linkStyle.highlightOpacity !== _this.linkStyle.opacity
|| _this.linkStyle.inactiveOpacity !== _this.linkStyle.opacity) ||
(_this.nodeStyle && (_this.nodeStyle.highlightOpacity !== _this.nodeStyle.opacity
|| _this.nodeStyle.inactiveOpacity !== _this.nodeStyle.opacity))) {
_this.sankeyHighlightModule = new SankeyHighlight(_this);
}
_this.nodeLayoutMap = _this.sankeySeriesModule.buildNodes(_this.links, _this);
if (_this.sankeyLegendModule && _this.legendSettings.visible) {
_this.sankeyLegendModule.getLegendOptions(_this);
}
_this.calculateBounds();
_this.renderElements();
_this.allowServerDataBinding = true;
});
};
/**
* Creates the chart SVG by removing the existing SVG and initializing a new one.
*
* @returns {void}
* @private
*/
Sankey.prototype.createChartSvg = function () { this.removeSvg(); createSvg(this); };
/**
* Removes the existing SVG and tooltip parent element from the DOM.
*
* @returns {void}
* @private
*/
Sankey.prototype.removeSvg = function () {
removeElement(this.element.id + '_tooltip_parent');
if (this.svgObject) {
while (this.svgObject.childNodes.length) {
this.svgObject.removeChild(this.svgObject.firstChild);
}
}
};
/**
* Creates the secondary tooltip container element used for rendering tooltip content.
*
* @returns {void}
* @private
*/
Sankey.prototype.createSecondaryElement = function () {
if (this.element.tagName !== 'g') {
var tooltipDiv = redrawElement(false, this.element.id + '_Secondary_Element') ||
this.createElement('div');
tooltipDiv.id = this.element.id + '_Secondary_Element';
tooltipDiv.style.cssText = 'position: relative';
var tooltipHostElement = document.getElementById(this.element.id + '_tooltip_parent');
if (!tooltipHostElement) {
tooltipHostElement = document.createElement('div');
tooltipHostElement.id = this.element.id + '_tooltip_parent';
tooltipHostElement.style.cssText = " position: absolute; left: 0; top: 0; pointer-events: none;\n z-index: 100; overflow: hidden; contain: layout style paint; ";
appendChildElement(false, tooltipDiv, tooltipHostElement, false);
}
appendChildElement(false, this.element, tooltipDiv, false);
}
};
/**
* Positions and applies required styles to the secondary tooltip container element.
*
* @returns {void}
*/
Sankey.prototype.positionSecondaryElement = function () {
var secondaryElement = document.getElementById((this.element ? this.element.id : '') + '_tooltip_parent');
if (!secondaryElement || !this.svgObject) {
return;
}
secondaryElement.style.left = '0px';
secondaryElement.style.top = '0px';
secondaryElement.style.position = 'absolute';
secondaryElement.style.pointerEvents = 'none';
secondaryElement.style.zIndex = '1';
secondaryElement.style.overflow = 'hidden';
};
/**
* Calculates the initial clip rect and legend bounds based on margins, borders, titles, and available size.
*
* @returns {void}
*/
Sankey.prototype.calculateBounds = function () {
this.calculateAvailableSize();
var margin = this.margin;
var titleHeight = 0;
var subTitleHeight = 0;
var titlePadding = 10;
var borderWidth = (this.border && this.border.width);
var clipLeft = (margin.left) + borderWidth;
var clipWidth = this.availableSize.width - clipLeft - (margin.right) - borderWidth;
var clipTop = (margin.top) + borderWidth;
var clipHeight = this.availableSize.height - clipTop - borderWidth - (margin.bottom);
var titleCollection = this.title ? getTitle(this.title, this.titleStyle, clipWidth, this.enableRtl, this.themeStyle.chartTitleFont) : [];
if (this.title) {
titleHeight = (measureText(this.title, this.titleStyle, this.themeStyle.chartTitleFont).height
* titleCollection.length) + titlePadding;
if (this.subTitle) {
var subTitleCollection = getTitle(this.subTitle, this.subTitleStyle, clipWidth, this.enableRtl, this.themeStyle.chartSubTitleFont);
// use consistent 10px gap between title and subtitle lines
subTitleHeight = (measureText(this.subTitle, this.subTitleStyle, this.themeStyle.chartSubTitleFont).height
* subTitleCollection.length) + 10;
}
}
var totalTitlesHeight = titleHeight + subTitleHeight;
clipTop += totalTitlesHeight;
clipHeight -= totalTitlesHeight; // place titles on top by default
this.initialClipRect = new Rect(clipLeft, clipTop, Math.max(0, clipWidth), Math.max(0, clipHeight));
if (this.sankeyLegendModule && this.legendSettings.visible) {
this.sankeyLegendModule.calculateLegendBounds(this.initialClipRect, this.availableSize, null);
}
};
/**
* Renders all visual elements of the Sankey chart including border, title, series, legend, and accessibility features.
*
* @returns {void}
*/
Sankey.prototype.renderElements = function () {
this.renderBorder();
this.renderTitle();
this.createSecondaryElement();
if (!this.element.contains(this.svgObject)) {
this.element.appendChild(this.svgObject);
}
if (this.sankeySeriesModule && this.initialClipRect && this.links && this.links.length > 0) {
this.sankeySeriesModule.render(this);
}
this.renderLegend();
this.positionSecondaryElement();
this.initKeyboardTabOrder();
this.trigger('loaded', { chart: this, theme: this.theme });
};
/**
* Renders the chart border and background (including optional background image) into the SVG.
*
* @returns {void}
*/
Sankey.prototype.renderBorder = function () {
var padding = this.border.width;
var borderRectOptions = new RectOption(this.element.id + '_border', this.background || this.themeStyle.background, this.border, 1, new Rect(padding / 2, padding / 2, this.availableSize.width - padding, this.availableSize.height - padding), 0, 0, '', this.border.dashArray);
var borderElement = this.renderer.drawRectangle(borderRectOptions);
borderElement.setAttribute('aria-hidden', 'true');
appendChildElement(false, this.svgObject, borderElement, false);
if (this.backgroundImage) {
var backgroundImageOptions = new ImageOption(this.availableSize.height - padding, this.availableSize.width - padding, this.backgroundImage, 0, 0, this.element.id + '_background', 'visible', 'none');
var backgroundImageElement = this.renderer.drawImage(backgroundImageOptions);
backgroundImageElement.setAttribute('aria-hidden', 'true');
appendChildElement(false, this.svgObject, backgroundImageElement, false);
}
};
/**
* Renders the chart title and subtitle text elements with accessibility attributes applied.
*
* @returns {void}
*/
Sankey.prototype.renderTitle = function () {
if (!this.title && !this.subTitle) {
return;
}
var margin = this.margin;
var titleRect = new Rect(margin.left, 0, (this.availableSize.width - (margin.left) - (margin.right)), 0);
var titleTextSize = this.title ? measureText(this.title, this.titleStyle, this.themeStyle.chartTitleFont) : new Size(0, 0);
var titleCollection = this.title
? getTitle(this.title, this.titleStyle, titleRect.width, this.enableRtl, this.themeStyle.chartTitleFont)
: [];
var titleX = titlePositionX(titleRect, this.titleStyle);
var titleY = (margin.top) + ((titleTextSize.height) * 3 / 4);
if (this.title) {
var titleTextOptions = new TextOption(this.element.id + '_title', titleX, titleY, getTextAnchor(this.titleStyle.textAlignment, this.enableRtl), titleCollection, undefined, 'auto');
var titleTextElement = textElement(this.renderer, titleTextOptions, this.titleStyle, this.titleStyle.color || this.themeStyle.chartTitleFont.color, this.svgObject, null, null, null, null, null, null, null, null, false, null, this.themeStyle.chartTitleFont);
titleTextElement.setAttribute('role', 'heading');
titleTextElement.setAttribute('aria-level', '1');
titleTextElement.setAttribute('tabindex', '-1'); // Readable but non-focusable
titleTextElement.setAttribute('aria-label', this.title);
}
if (this.subTitle) {
var subTitleCollection = getTitle(this.subTitle, this.subTitleStyle, titleRect.width, this.enableRtl, this.themeStyle.chartSubTitleFont);
var subTitleLineSize = measureText(this.subTitle, this.subTitleStyle, this.themeStyle.chartSubTitleFont);
var titleLines = this.title ? (titleCollection.length || 1) : 0;
var subTitleY = this.title
? (margin.top + (titleTextSize.height * 3 / 4) + (titleTextSize.height * titleLines) + 10)
: (margin.top + (subTitleLineSize.height * 3 / 4));
var subAlign = (this.subTitleStyle && this.subTitleStyle.textAlignment) ? this.subTitleStyle.textAlignment : 'Center';
var subTitleX = titlePositionX(titleRect, { textAlignment: subAlign });
var subTitleTextOptions = new TextOption(this.element.id + '_subtitle', subTitleX, subTitleY, getTextAnchor(subAlign, this.enableRtl), subTitleCollection, undefined, 'auto');
var subTitleTextElement = textElement(this.renderer, subTitleTextOptions, this.subTitleStyle, this.subTitleStyle.color || this.themeStyle.chartSubTitleFont.color, this.svgObject, null, null, null, null, null, null, null, null, false, null, this.themeStyle.chartSubTitleFont);
subTitleTextElement.setAttribute('role', 'heading');
subTitleTextElement.setAttribute('aria-level', '2');
subTitleTextElement.setAttribute('tabindex', '-1');
subTitleTextElement.setAttribute('aria-label', this.subTitle);
}
};
/**
* Renders the Sankey legend if the legend module is available and legend visibility is enabled.
*
* @returns {void}
*/
Sankey.prototype.renderLegend = function () {
if (this.sankeyLegendModule && this.sankeyLegendModule.legendCollections.length &&
this.legendSettings.visible) {
this.sankeyLegendModule.calTotalPage = true;
var legendBounds = this.sankeyLegendModule.legendBounds;
this.sankeyLegendModule.renderLegend(this, this.legendSettings, legendBounds);
}
};
/**
* Finds and returns a user-defined node by id from the nodes collection.
*
* @param {string} id - The node id used to locate the node definition.
* @returns {SankeyNode | null} returns node if present, else null.
* @private
*/
Sankey.prototype.findNode = function (id) {
var nodesArray = Array.isArray(this.nodes) ? this.nodes : [];
for (var nodeIndex = 0; nodeIndex < nodesArray.length; nodeIndex++) {
if (nodesArray[nodeIndex] && nodesArray[nodeIndex].id === id) {
return nodesArray[nodeIndex];
}
}
return null;
};
// helper: find link by source/target
/**
* Finds and returns a user-defined link by source and target ids from the links collection.
*
* @param {string} sourceId - The source node id used to locate the link definition.
* @param {string} targetId - The target node id used to locate the link definition.
* @returns {SankeyLink | null} returns link if present, else null.
*
* @private
*/
Sankey.prototype.findLink = function (sourceId, targetId) {
var linksArray = Array.isArray(this.links) ? this.links : [];
for (var linkIndex = 0; linkIndex < linksArray.length; linkIndex++) {
var currentLink = linksArray[linkIndex];
if (currentLink && currentLink.sourceId === sourceId &&
currentLink.targetId === targetId) {
return currentLink;
}
}
return null;
};
/**
* Initializes the internationalization instance used by the component.
*
* @returns {void}
*/
Sankey.prototype.setCulture = function () { this.intl = new Internationalization(); };
/**
* Applies theme styles for the current chart theme.
*
* @returns {void}
*/
Sankey.prototype.setTheme = function () { this.themeStyle = getSankeyThemeColor(this.theme); };
/**
* Calculates the available rendering size based on configured width/height or element client size.
*
* @returns {void}
*
* @private
*/
Sankey.prototype.calculateAvailableSize = function () {
var _this = this;
// Use given width/height or fall back to parent/client size, defaulting to 600x450 when zero
var parseSize = function (v, fallback, client) {
if (!v) {
return client > 0 ? client : fallback;
}
var sizeText = v.toString();
if (sizeText.indexOf('%') > -1) {
var percent = parseFloat(sizeText);
var base = (_this.element && _this.element.parentElement) ? _this.element.parentElement.clientWidth : client;
var value = (isNaN(percent) ? 100 : percent) / 100 * base;
return value > 0 ? value : fallback;
}
var pixelValue = parseFloat(sizeText);
return (pixelValue > 0 ? pixelValue : (client > 0 ? client : fallback));
};
var clientWidth = this.element ? this.element.clientWidth : 0;
var clientHeight = this.element ? this.element.clientHeight : 0;
var computedWidth = parseSize(this.width, 600, clientWidth);
var computedHeight = parseSize(this.height, 450, clientHeight);
this.availableSize = new Size(computedWidth, computedHeight);
};
/**
* Unbinds all DOM and window event listeners attached for Sankey interactions.
*
* @returns {void}
*
* @private
*/
Sankey.prototype.unWireEvents = function () {
var touchStartEvent = Browser.touchStartEvent;
var touchMoveEvent = Browser.touchMoveEvent;
var touchEndEvent = Browser.touchEndEvent;
var pointerLeaveOrMouseLeaveEvent = Browser.isPointer ? 'pointerleave' : 'mouseleave';
/** UnBind the Event handler */
EventHandler.remove(this.element, touchStartEvent, this.handleMouseDown);
EventHandler.remove(this.element, touchMoveEvent, this.mouseMove);
EventHandler.remove(this.element, touchEndEvent, this.mouseEnd);
EventHandler.remove(this.element, 'click', this.handleMouseClick);
EventHandler.remove(this.element, pointerLeaveOrMouseLeaveEvent, this.mouseLeave);
EventHandler.remove(this.element, 'keydown', this.handleKeyDown);
EventHandler.remove(document.body, 'keydown', this.handleDocumentKeyDown);
EventHandler.remove(this.element, 'keyup', this.handleKeyUp);
EventHandler.remove(this.element, 'focusin', this.handleFocusIn);
window.removeEventListener((Browser.isTouch && ('orientation' in window && 'onorientationchange' in window)) ? 'orientationchange' : 'resize', this.resizeBound);
};
/**
* Binds all required DOM and window event listeners for Sankey interactions.
*
* @returns {void}
*
* @private
*/
Sankey.prototype.wireEvents = function () {
/**
* To fix react timeout destroy issue.
*/
if (!this.element) {
return;
}
/** Find the Events type */
var pointerLeaveOrMouseLeaveEvent = Browser.isPointer ? 'pointerleave' : 'mouseleave';
/** Bind the Event handler */
EventHandler.add(this.element, Browser.touchStartEvent, this.handleMouseDown, this);
EventHandler.add(this.element, Browser.touchMoveEvent, this.mouseMove, this);
EventHandler.add(this.element, Browser.touchEndEvent, this.mouseEnd, this);
EventHandler.add(this.element, 'click', this.handleMouseClick, this);
EventHandler.add(this.element, pointerLeaveOrMouseLeaveEvent, this.mouseLeave, this);
this.resizeBound = this.chartResize.bind(this);
window.addEventListener((Browser.isTouch && ('orientation' in window && 'onorientationchange' in window)) ? 'orientationchange' : 'resize', this.resizeBound);
EventHandler.add(this.element, 'keydown', this.handleKeyDown, this);
EventHandler.add(document.body, 'keydown', this.handleDocumentKeyDown, this); // optional Alt+J
EventHandler.add(this.element, 'keyup', this.handleKeyUp, this);
EventHandler.add(this.element, 'focusin', this.handleFocusIn, this);
this.element.addEventListener('pointermove', this.handlePointerMove.bind(this));
this.element.addEventListener('pointerup', this.handlePointerUp.bind(this));
this.element.addEventListener('pointerleave', this.handlePointerLeave.bind(this));
this.setStyle(this.element);
};
/**
* Applying styles for sankey chart element.
*
* @param {HTMLElement} element - Specifies the element.
* @returns {void}
*/
Sankey.prototype.setStyle = function (element) {
element.style.touchAction = 'element';
element.style.msTouchAction = 'element';
element.style.msContentZooming = 'none';
element.style.msUserSelect = 'none';
element.style.webkitUserSelect = 'none';
element.style.position = 'relative';
element.style.display = 'block';
element.style.overflow = 'hidden';
element.style.height = (element.style.height || (this.height && this.height.indexOf('%') === -1)) ? element.style.height : 'inherit';
};
/**
* Handles document-level keyboard shortcuts for the Sankey chart.
*
* Moves focus to the chart container when the Alt + J key combination is pressed
* and applies the container navigation focus styling.
*
* @param {KeyboardEvent} keyboardEvent - The keyboard event triggered on the document.
* @returns {void}
*
* @private
*/
Sankey.prototype.handleDocumentKeyDown = function (keyboardEvent) {
if (keyboardEvent.altKey && keyboardEvent.key.toLowerCase() === 'j' && this.element) {
this.element.focus();
this.clearNavigationStyles();
this.setContainerNavigationStyle();
}
};
/**
* Applies keyboard navigation focus styling to the Sankey chart container.
*
* @returns {void}
*
* @private
*/
Sankey.prototype.setContainerNavigationStyle = function () {
var chartElement = this.element;
var focusColor = this.focusBorderColor || this.themeStyle.tabColor;
chartElement.style.setProperty('outline', this.focusBorderWidth + "px solid " + focusColor);
chartElement.style.setProperty('outline-offset', this.focusBorderMargin + "px");
};
/**
* Handles pointer or mouse move events on the chart area.
*
* Updates the internal mouse position used for hit-testing
* and tooltip positioning.
*
* @param {PointerEvent | MouseEvent} event - The pointer or mouse move event.
* @returns {boolean} Returns false to prevent further propagation.
*/
Sankey.prototype.handlePointerMove = function (event) {
this.updateMousePosition(event);
this.notify(Browser.touchMoveEvent, event);
return false;
};
/**
* Handles pointer or mouse up events on the chart area.
*
* Updates the internal mouse position used for interaction handling.
*
* @param {PointerEvent | MouseEvent} event - The pointer or mouse up event.
* @returns {boolean} Returns false to prevent further propagation.
*/
Sankey.prototype.handlePointerUp = function (event) {
this.updateMousePosition(event);
this.notify(Browser.touchEndEvent, event);
return false;
};
/**
* Handles pointer or mouse leave events on the chart area.
*
* Updates the mouse position (if available) and resets
* the internal pointer movement state.
*
* @param {PointerEvent | MouseEvent} [event] - The optional pointer or mouse leave event.
* @returns {boolean} Returns false to prevent further propagation.
*
* @private
*/
Sankey.prototype.handlePointerLeave = function (event) {
if (event) {
this.updateMousePosition(event);
this.notify(Browser.isPointer ? 'pointerleave' : 'mouseleave', event);
}
this.startMove = false;
return false;
};
/**
* Handles window resize by recalculating available size, triggering sizeChanged, and re-rendering only when dimensions differ.
*
* @returns {boolean} returns boolean based on chart resize actions.
*
* @private
*/
Sankey.prototype.chartResize = function () {
this.animateSeries = false;
var previousAvailableSize = new Size(this.availableSize.width, this.availableSize.height);
// Recompute available size first
this.calculateAvailableSize();
// If size hasn't changed, skip a full re-render but still update secondary elements and emit sizeChanged
var isSizeChanged = (previousAvailableSize.width !== this.availableSize.width) ||
(previousAvailableSize.height !== this.availableSize.height);
// Trigger sizeChanged event regardless
var sizeChangedEventArgs = {
previousSize: previousAvailableSize,
currentSize: this.availableSize
};
this.trigger('sizeChanged', sizeChangedEventArgs);
// If the chart size actually changed, recreate svg and re-render all elements
if (isSizeChanged) {
// recreate renderer + svg to ensure correct dimensions
this.renderer = new SvgRenderer(this.element.id);
this.setTheme();
this.createChartSvg();
this.nodeLayoutMap = this.sankeySeriesModule.buildNodes(this.links, this);
if (this.sankeyLegendModule && this.legendSettings.visible) {
this.sankeyLegendModule.getLegendOptions(this);
}
this.calculateBounds();
this.renderElements();
}
else {
// Update secondary DOM positions and clear tooltip state
this.positionSecondaryElement();
if (this.tooltipModule) {
this.tooltipModule.svgTooltip = null;
}
}
return false;
};
/**
* Sets the mouse coordinates relative to the chart SVG for pointer and mouse events.
*
* @param {PointerEvent | MouseEvent} event - Mouse/pointer event with client coordinates.
* @returns {void}
*
* @private
*/
Sankey.prototype.updateMousePosition = function (event) {
var svgHostElement = this.svgObject;
if (!svgHostElement || !event) {
return;
}
var svgClientRect = svgHostElement.getBoundingClientRect();
var clientX = event.clientX;
var clientY = event.clientY;
this.mouseX = clientX - svgClientRect.left;
this.mouseY = clientY - svgClientRect.top;
};
/**
* Tracks pointer/touch movement, updates chart-relative mouse coordinates, and forwards the move event to the chart handler.
*
* @param {(PointerEvent | TouchEvent)} event - Pointer or touch move event containing client coordinates.
* @returns {boolean} returns boolean based on mouse actions.
*
* @private
*/
Sankey.prototype.mouseMove = function (event) {
var clientX;
var clientY;
if (event.type === 'touchmove') {
this.isTouch = true;
var touchMoveEvent = event;
clientX = touchMoveEvent.changedTouches[0].clientX;
clientY = touchMoveEvent.changedTouches[0].clientY;
}
else {
var pointerMoveEvent = event;
this.isTouch = (pointerMoveEvent.pointerType === 'touch' || pointerMoveEvent.pointerType === '2') || this.isTouch;
clientX = pointerMoveEvent.clientX;
clientY = pointerMoveEvent.clientY;
}
this.updateMousePosition({ clientX: clientX, clientY: clientY });
if (this.handleMouseMove) {
this.handleMouseMove(event);
}
return false;
};
/**
* Tracks pointer/touch end, updates chart-relative mouse coordinates, and forwards the end event to the mouse-leave handler.
*
* @param {(PointerEvent | TouchEvent)} event - Pointer or touch end event containing client coordinates.
* @returns {boolean} returns boolean based on mouse actions.
*
* @private
*/
Sankey.prototype.mouseEnd = function (event) {
var clientX;
var clientY;
if (event.type === 'touchend') {
var touchEndEvent = event;
clientX = touchEndEvent.changedTouches[0].clientX;
clientY = touchEndEvent.changedTouches[0].clientY;
this.isTouch = true;
}
else {
var pointerEndEvent = event;
clientX = pointerEndEvent.clientX;
clientY = pointerEndEvent.clientY;
this.isTouch = (pointerEndEvent.pointerType === 'touch' || pointerEndEvent.pointerType === '2');
}
this.updateMousePosition({ clientX: clientX, clientY: clientY });
if (event.type === 'touchend') {
this.notify(Browser.touchEndEvent, event);
}
return false;
};
/**
* Handles mouse move interactions over the Sankey chart.
*
* Detects whether the pointer is over a node or a link, triggers the corresponding
* enter/leave events, and maintains the current hovered node/link state.
*
* @param {MouseEvent} event - The mouse move event dispatched on the chart.
* @returns {boolean} Always returns false to prevent default propagation in the control.
*
* @private
*/
Sankey.prototype.handleMouseMove = function (event) {
this.updateMousePosition(event);
var targetElement = event.target;
var nodeIdPrefix = this.element.id + "_node_";
var linkGroupId = this.element.id + "_link_collection";
// Node hit
if (targetElement && targetElement.id && targetElement.id.indexOf(nodeIdPrefix) === 0) {
var nodeId = targetElement.getAttribute('aria-label');
if (!this.hoveredNode || (this.hoveredNode && this.hoveredNode.id !== nodeId)) {
if (this.hoveredNode) {
this.trigger('nodeLeave', { node: this.hoveredNode });
this.hoveredNode = null;
}
var nodeObject = this.findNode(nodeId);
if (nodeObject) {
this.trigger('nodeEnter', { node: nodeObject });
this.hoveredNode = nodeObject;
}
if (this.hoveredLink) {
this.trigger('linkLeave', { link: this.hoveredLink });
this.hoveredLink = null;
}
}
}
// Link hit
else if (targetElement && targetElement.tagName.toLowerCase() === 'path' && targetElement.closest("[id=\"" + linkGroupId + "\"]")) {
var sourceId = targetElement.getAttribute('data-source');
var targetId = targetElement.getAttribute('data-target');
if (!this.hoveredLink || (this.hoveredLink && (this.hoveredLink.sourceId !== sourceId
|| this.hoveredLink.targetId !== targetId))) {
if (this.hoveredLink) {
this.trigger('linkLeave', { link: this.hoveredLink });
this.hoveredLink = null;
}
var linkObject = this.findLink(sourceId, targetId);
if (linkObject) {
this.trigger('linkEnter', { link: linkObject });
this.hoveredLink = linkObject;
}
if (this.hoveredNode) {
this.trigger('nodeLeave', { node: this.hoveredNode });
this.hoveredNode = null;
}
}
}
else {
if (this.hoveredNode) {
this.trigger('nodeLeave', { node: this.hoveredNode });
this.hoveredNode = null;
}
if (this.hoveredLink) {
this.trigger('linkLeave', { link: this.hoveredLink });
this.hoveredLink = null;
}
}
this.notify(Browser.touchMoveEvent, event);
return false;
};
/**
* Notifies the chart about the pointer/touch start event to initiate interaction handling.
*
* @param {PointerEvent} event - Pointer down event raised on the chart surface.
* @returns {boolean} returns boolean based on mouse actions.
*
* @private
*/
Sankey.prototype.handleMouseDown = function (event) {
this.notify(Browser.touchStartEvent, event);
return false;
};
/**
* Handles mouse leave by clearing hover states, triggering leave events, and notifying the chart about interaction end.
*
* @param {MouseEvent} event - Mouse leave event raised when the pointer exits the chart surface.
* @returns {boolean} returns boolean based on mouse actions.
*
* @private
*/
Sankey.prototype.mouseLeave = function (event) {
this.updateMousePosition(event);
if (this.hoveredNode) {
this.trigger('nodeLeave', { node: this.hoveredNode });
this.hoveredNode = null;
}
if (this.hoveredLink) {
this.trigger('linkLeave', { link: this.hoveredLink });
this.hoveredLink = null;
}
this.notify(Browser.touchEndEvent, event);
return false;
};
/**
* Handles the mouse click on the chart.
*
* @param {PointerEvent | TouchEvent} e - The mouse or touch event.
* @returns {boolean} - Return false.
* @private
*
*/
Sankey.prototype.handleMouseClick = function (e) {
this.updateMousePosition(e);
var target = e.target;
// Check if node was clicked
var nodePrefix = this.element.id + "_node_";
if (target.id && target.id.indexOf(nodePrefix) === 0) {
var nodeId = target.getAttribute('aria-label');
var node = this.findNode(nodeId);
if (node) {
var nodeClickArgs = { node: node };
this.trigger('nodeClick', nodeClickArgs);
}
}
// Check if link was clicked
var linkGroupId = this.element.id + "_link_collection";
if (target.tagName.toLowerCase() === 'path' && target.closest("[id=\"" + linkGroupId + "\"]")) {
var sourceID = target.getAttribute('data-source');
var targetID = target.getAttribute('data-target');
var link = this.findLink(sourceID, targetID);
if (link) {
var linkClickArgs = { link: link };
this.trigger('linkClick', linkClickArgs);
}
}
this.notify('click', e);
return false;
};
/**
* Export method for the chart.
*
* @param {ExportType} type - Specifies the type of the export.
* @param {string} fileName - Specifies the file name of the exported file.
* @returns {void}
*
* @private
*/
Sankey.prototype.export = function (type, fileName) {
if (this.sankeyExportModule) {
this.sankeyExportModule.export(type, fileName);
if (this.afterExport) {
this.sankeyExportModule.getDataUrl(this);
}
}
};
/**
* Prints the chart in the page.
*
* @param {string[] | string | Element} id - The id of the chart to be printed on the page.
* @returns {void}
*
* @private
*/
Sankey.prototype.print = function (id) {
var argsData = {
cancel: false, htmlContent: this.svgObject
};
this.trigger('beforePrint', argsData);
if (!argsData.cancel) {
var printChart = new PrintUtils(this);
printChart.print(id);
}
};
/**
* Returns the persisted state of the component.
*
* @returns {string} the persisted state.
*
* @private
*/
Sankey.prototype.getPersistData = function () { return ''; };
/**
* Handles property updates and refreshes the Sankey rendering based on the changed properties.
*
* @param {SankeyModel} newProp - Newly updated property values.
* @param {SankeyModel} _oldProp - Previously existing property values.
* @returns {void}
*
* @private
*/
Sankey.prototype.onPropertyChanged = function (newProp, _oldProp) {
var renderer = false;
var refreshBounds = false;
this.animateSeries = false;
for (var _i = 0, _a = Object.keys(newProp); _i < _a.length; _i++) {
var prop = _a[_i];
switch (prop) {
case 'width':
case 'height':
this.createChartSvg();
refreshBounds = true;
break;
case 'title':
case 'subTitle':
refreshBounds = true;
break;
case 'titleStyle':
case 'subTitleStyle':
renderer = true;
break;
case 'orientation':
case 'nodes':
case 'links':
case 'labelSettings':
case 'nodeStyle':
case 'linkStyle':
case 'margin':
case 'border':
case 'background':
case 'backgroundImage':
case 'legendSettings':
refreshBounds = true;
break;
case 'enableExport':
renderer = true;
break;
case 'tooltip':
renderer = true;
break;
case 'animation':
renderer = true;
break;
case 'enableRtl':
case 'locale':
this.refresh();
break;
case 'theme':
this.animateSeries = true;
renderer = true;
break;
default:
renderer = true;
break;
}
}
if (refreshBounds) {
this.nodeLayoutMap = this.sankeySeriesModule.buildNodes(this.links, this);
if (this.sankeyLegendModule && this.legendSettings.visible) {
this.sankeyLegendModule.getLegendOptions(this);
}
this.calculateAvailableSize();
this.createChartSvg();
if (this.isReact) {
this.clearTemplate();
}
this.calculateBounds();
this.renderElements();
}
else if (renderer) {
this.removeSvg();
if (this.isReact) {
this.clearTemplate();
}
this.renderElements();
}
};
/**
* Handles focus-in events within the Sankey chart to manage keyboard navigation,
* highlighting, and tooltip behavior based on the focused element group.
*
* @param {FocusEvent} event - The focus-in event dispatched on the chart.
* @returns {void}
* @private
*/
Sankey.prototype.handleFocusIn = function (event) {
var targetElement = event.target;
if (!targetElement || !targetElement.id) {
return;
}
var canUseMatches = !!(targetElement).matches && typeof (targetElement).matches === 'function';
if (canUseMatches && !(targetElement).matches(':focus-visible')) {
this.clearNavigationStyles();
var groupName = this.getGroupOf(targetElement.id);
if (groupName) {
this.currentGroup = groupName;
}
this.previousTargetId = targetElement.id;
return;
}
this.clearNavigationStyles();
this.applyNavigationStyle(targetElement.id);
var group = this.getGroupOf(targetElement.id);
if (group === 'nodes' || group === 'links') {
if (this.ensureTooltip()) {
var focusedElement = document.activeElement || targetElement;
this.applyHighlightForElement(focusedElement);
var centerPosition = void 0;
if (!centerPosition) {
centerPosition = this.getElementCenterInChartCoords(focusedElement);
}
if (centerPosition) {
this.tooltipModule.showTooltipForElement(focusedElement, !this.tooltipModule.svgTooltip, centerPosition);
}
}
}
else {
if (this.tooltipModule) {
this.tooltipModule.hideTooltip(0);
}
if (group === 'legend' && this.sankeyHighlightModule) {
var label = targetElement.getAttribute('aria-label');
if (label) {
this.sankeyHighlightModule.highlightForNode(label);
}
}
}
if (group) {
this.currentGroup = group;
}
this.previousTargetId = targetElement.id;
};
/**
* Initializes the keyboard tab order for focusable groups within the Sankey chart.
*
* The order is: Title → Subtitle → Nodes → Links → Legend.
* Only the first element of each group is tabbable by default; others use roving tabindex.
*
* @returns {void}
*/
Sankey.prototype.initKeyboardTabOrder = function () {
// Title → Subtitle → Nodes → Links → Legend (first element in each group is tabbable)
var titleElement = document.getElementById(this.element.id + "_title");
var subtitleElement = document.getElementById(this.element.id + "_subtitle");
var nodeElements = this.getNodeElements();
var linkElements = this.getLinkElements();
var legendItems = this.getLegendItems();
// Set tabindex states
if (titleElement) {
titleElement.setAttribute('tabindex', '0');
}
if (subtitleElement) {
subtitleElement.setAttribute('tabindex', '0');
} // requirement
if (nodeElements.length) {
nodeElements[0].setAttribute('tabindex', '0');
}
if (linkElements.length) {
linkElements[0].setAttribute('tabindex', '0');
}
if (legendItems.length) {
legendItems[0].setAttribute('tabindex', '0');
}
// Force all other legend <g> to -1 (avoid tabindex="")
for (var i = 1; i < legendItems.length; i++) {
var legendGroup = legendItems[i];
if (legendGroup instanceof HTMLElement || legendGroup instanceof SVGElement) {
legendGroup.setAttribute('tabindex', '-1');
}
}
// Reset roving indexes to 0 for Arrow navigation
this.currentNodeIndex = 0;
this.currentLinkIndex = 0;
this.currentLegendIndex = 0;
// Do not set currentGroup by default; we will infer from focused element.
this.currentGroup = null;
this.previousTargetId = '';
};
/**
* Updates tabindex so the previous element becomes unfocusable and the next element becomes focusable.
*
* @param {Element | null} [previousElement] - The element to make unfocusable.
* @param {Element | null} [nextElement] - The element to make focusable.
* @returns {void}
*
* @private
*/
Sankey.prototype.updateTabIndex = function (previousElement, nextElement) {
if (previousElement instanceof HTMLElement || previousElement instanceof SVGElement) {
previousElement.setAttribute('tabindex', '-1');
}
if (nextElement instanceof HTMLElement || nextElement instanceof SVGElement) {
nextElement.setAttribute('tabindex', '0');
}
};
/**
* Wraps an index into the valid range [0, total - 1].
*
* @param {number} index - The index to normalize.
* @param {number} total - The total number of items in the collection.
* @returns {number} The normalized index within bounds.
*
* @private
*/
Sankey.prototype.normalizeIndex = function (index, total) {
return index > total - 1 ? 0 : (index < 0 ? total - 1 : index);
};
/**
* Finds the index of an element by id within a list of elements.
*
* @param {Element[]} elements - The collection to search.
* @param {string} targetId - The id of the element to find.
* @returns {number} The index of the matching element, or 0 if not found.
*
* @private
*/
Sankey.prototype.indexOfElementById = function (elements, targetId) {
for (var i = 0; i < elements.length; i++) {
if (elements[i].id === targetId) {
return i;
}
}
return 0;
};
/**
* Removes the focused CSS class from elements currently marked as focused.
*
* @returns {void}
*/
Sankey.prototype.clearFocusedClasses = function () {
var focusedElements = this.element.querySelectorAll('.e-sankey-focused');
focusedElements.forEach(function (element) {
element.classList.remove('e-sankey-focused');
});
};
/**
* Focuses the specified element, applies the focused class, and ensures it is tabbable.
*
* @param {Element} element - The element to focus.
* @returns {string} The id of the focused element, or an empty string if focus was not applied.
*
* @private
*/
Sankey.prototype.focusElement = function (element) {
if (!(element instanceof HTMLElement || element instanceof SVGElement)) {
return '';
}
this.clearFocusedClasses();
element.setAttribute('tabindex', '0');
element.classList.add('e-sankey-focused');
element.focus();
return element.id;
};
/**
* Applies keyboard navigation outline styling to the specified target element by id.
*
* @param {string} targetId - The id of the element to style.
* @returns {void}
*
* @private
*/
Sankey.prototype.applyNavigationStyle = function (targetId) {
var targetElement = document.getElementById(targetId);
if (!targetElement) {
return;
}
var focusColor = this.focusBorderColor || this.themeStyle.tabColor;
targetElement.style.setProperty('outline', this.focusBorderWidth + "px solid " + focusColor);
targetElement.style.setProperty('margin', this.focusBorderMargin + "