@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.
515 lines (514 loc) • 22.4 kB
JavaScript
import { Tooltip as SVGTooltip } from '@syncfusion/ej2-svg-base';
import { withInBounds } from '../../common/utils/helper';
import { Browser } from '@syncfusion/ej2-base';
/**
* Tooltip rendering module for Sankey Chart.
*/
var SankeyTooltip = /** @class */ (function () {
/**
* Constructor.
*
* @param {Sankey} sankey - Sankey chart instance.
*/
function SankeyTooltip(sankey) {
this.sankey = sankey;
this.wireEvents();
}
/**
* Wires all tooltip-related event listeners to the Sankey chart instance.
*
* This method attaches pointer, touch, mouse, and click events required for
* tooltip rendering and lifecycle management.
*
* @returns {void}
*/
SankeyTooltip.prototype.wireEvents = function () {
var sankeyChart = this.sankey;
if (!sankeyChart || sankeyChart.isDestroyed) {
return;
}
var pointerLeaveEvent = Browser.isPointer ? 'pointerleave' : 'mouseleave';
sankeyChart.on(Browser.touchMoveEvent, this.handlePointerMove, this);
sankeyChart.on(Browser.touchEndEvent, this.handlePointerUp, this);
sankeyChart.on(pointerLeaveEvent, this.handlePointerLeave, this);
sankeyChart.on('click', this.handleChartClick, this);
};
/**
* Unwires all tooltip-related event listeners from the Sankey chart instance.
*
* @returns {void}
*/
SankeyTooltip.prototype.unwireEvents = function () {
var sankeyChart = this.sankey;
if (!sankeyChart || sankeyChart.isDestroyed) {
return;
}
var pointerLeaveEvent = Browser.isPointer ? 'pointerleave' : 'mouseleave';
sankeyChart.off(Browser.touchMoveEvent, this.handlePointerMove);
sankeyChart.off(Browser.touchEndEvent, this.handlePointerUp);
sankeyChart.off(pointerLeaveEvent, this.handlePointerLeave);
sankeyChart.off('click', this.handleChartClick);
};
/**
* Acts as a proxy to forward pointer and touch move events
* to the existing handleMouseMove method.
*
* @param {PointerEvent | TouchEvent} event - The pointer or touch move event.
* @returns {void}
*/
SankeyTooltip.prototype.handlePointerMove = function (event) {
// setMouseXY is already handled before notify, so mouse coordinates are available
this.handleMouseMove(event);
};
/**
* Handles chart click events to hideTooltip the tooltip when fade-out mode is set to click.
*
* @param {Event} event - The click event triggered on the chart.
* @returns {void}
* @private
*/
SankeyTooltip.prototype.handleChartClick = function (event) {
var sankeyChart = this.sankey;
if (sankeyChart.tooltip.fadeOutMode === 'Click') {
this.hideTooltip(0);
}
};
/**
* Listens mouse move events inside the Sankey chart.
*
* @param {PointerEvent} event - The mouse or pointer move event within the chart.
* @returns {void}
* @private
*/
SankeyTooltip.prototype.handleMouseMove = function (event) {
var sankeyChart = this.sankey;
if (!sankeyChart.tooltip.enable || sankeyChart.disableTrackTooltip) {
return;
}
if (withInBounds(sankeyChart.mouseX, sankeyChart.mouseY, sankeyChart.initialClipRect)) {
this.renderTooltip(false, event);
}
else if (sankeyChart.tooltip.fadeOutMode === 'Move') {
this.hideTooltip();
}
};
/**
* Shows tooltip for a given SVG element using the current chart mouse coordinates or a fallback position.
*
* @param {Element} targetElement - The SVG target element to show the tooltip for (node <rect> or link <path>).
* @param {boolean} isInitialRender - Indicates whether the tooltip is being rendered for the first time.
* @param {ChartLocation} [fallbackPosition] - Optional fallback position to place the tooltip when mouse coordinates are not applicable.
* @returns {void}
*
* @private
*/
SankeyTooltip.prototype.showTooltipForElement = function (targetElement, isInitialRender, fallbackPosition) {
if (isInitialRender === void 0) { isInitialRender = false; }
clearTimeout(this.tooltipTimer);
var sankeyChart = this.sankey;
var tooltipSettings = sankeyChart.tooltip;
var POINTER_PADDING = 4;
var location;
// Hit-test
var hitTarget = null;
if (targetElement) {
hitTarget = targetElement; // Use event.target (direct)
}
if (!hitTarget) {
return this.hideTooltip();
}
if (fallbackPosition) {
location = fallbackPosition;
}
else {
// Mouse hover → use current mouse position
var adjustedX = sankeyChart.mouseX - sankeyChart.initialClipRect.x + (POINTER_PADDING * 2);
var adjustedY = sankeyChart.mouseY - sankeyChart.initialClipRect.y + POINTER_PADDING;
location = { x: adjustedX, y: adjustedY };
}
// Detect node or link
var nodeElementIdPrefix = sankeyChart.element.id + "_node_";
var linkCollectionId = sankeyChart.element.id + "_link_collection";
var content = '';
var template = null;
var tooltipData;
var isNodeElement = hitTarget.id.indexOf('_node_level_') > -1 && hitTarget instanceof SVGRectElement;
if (isNodeElement) {
// Node
var nodeId = hitTarget.getAttribute('aria-label');
var nodeAggregates = this.computeNodeAggregates(nodeId, hitTarget.getAttribute('fill'));
tooltipData = {
name: nodeAggregates.name,
value: nodeAggregates.value,
inValue: nodeAggregates.inValue,
outValue: nodeAggregates.outValue
};
template = tooltipSettings.nodeTemplate || null;
if (template) {
if (typeof template === 'string') {
// Interpolate placeholders in string template
content = template
.replace(/\${name}/g, tooltipData.name)
.replace(/\${value}/g, tooltipData.value.toString())
.replace(/\${in}/g, tooltipData.inValue.toString())
.replace(/\${out}/g, tooltipData.outValue.toString());
}
else if (typeof template === 'function') {
// Call function with tooltipData
content = template(tooltipData);
}
}
else {
// Fallback to default format
var defaultFormat = tooltipSettings.nodeFormat || '$name : $value';
defaultFormat = defaultFormat
.replace(/\$\{name\}/g, '$name')
.replace(/\$\{value\}/g, '$value')
.replace(/\$\{in\}/g, '$in')
.replace(/\$\{out\}/g, '$out');
content = defaultFormat
.replace(/\$name/g, nodeAggregates.name)
.replace(/\$value/g, nodeAggregates.value.toString())
.replace(/\$in/g, nodeAggregates.inValue.toString())
.replace(/\$out/g, nodeAggregates.outValue.toString());
}
}
else if (hitTarget.tagName.toLowerCase() === 'path' &&
hitTarget.closest("[id=\"" + linkCollectionId + "\"]")) {
var linkPathElement = hitTarget.closest('path');
var sourceId = linkPathElement.getAttribute('data-source');
var targetId = linkPathElement.getAttribute('data-target');
var valueText = linkPathElement.getAttribute('data-value');
if (!sourceId || !targetId || valueText == null) {
return this.hideTooltip();
}
var value = +valueText;
var sourceAggregates = this.computeNodeAggregates(sourceId);
var targetAggregates = this.computeNodeAggregates(targetId);
tooltipData = {
start: {
name: sourceAggregates.name,
value: sourceAggregates.value,
in: sourceAggregates.inValue,
out: sourceAggregates.outValue
},
target: {
name: targetAggregates.name,
value: targetAggregates.value,
in: targetAggregates.inValue,
out: targetAggregates.outValue
},
value: value
}; // structure used downstream for templating
template = tooltipSettings.linkTemplate || null;
if (template) {
if (typeof template === 'string') {
// Interpolate placeholders in string template
content = template
.replace(/\${start\.name}/g, tooltipData.start.name)
.replace(/\${start\.value}/g, tooltipData.start.value.toString())
.replace(/\${start\.in}/g, tooltipData.start.in.toString())
.replace(/\${start\.out}/g, tooltipData.start.out.toString())
.replace(/\${target\.name}/g, tooltipData.target.name)
.replace(/\${target\.value}/g, tooltipData.target.value.toString())
.replace(/\${target\.in}/g, tooltipData.target.in.toString())
.replace(/\${target\.out}/g, tooltipData.target.out.toString())
.replace(/\${value}/g, tooltipData.value.toString());
}
else if (typeof template === 'function') {
// Call function with tooltipData
content = template(tooltipData);
}
}
else {
// Fallback to default format
var defaultFormat = tooltipSettings.linkFormat || '$start.name → $target.name : $value';
// Support both ${...} and $... placeholders
defaultFormat = defaultFormat
.replace(/\$\{start\.name\}/g, '$start.name')
.replace(/\$\{start\.value\}/g, '$start.value')
.replace(/\$\{start\.in\}/g, '$start.in')
.replace(/\$\{start\.out\}/g, '$start.out')
.replace(/\$\{target\.name\}/g, '$target.name')
.replace(/\$\{target\.value\}/g, '$target.value')
.replace(/\$\{target\.in\}/g, '$target.in')
.replace(/\$\{target\.out\}/g, '$target.out')
.replace(/\$\{value\}/g, '$value');
content = defaultFormat
.replace(/\$start\.name/g, sourceAggregates.name)
.replace(/\$start\.value/g, sourceAggregates.value.toString())
.replace(/\$start\.in/g, sourceAggregates.inValue.toString())
.replace(/\$start\.out/g, sourceAggregates.outValue.toString())
.replace(/\$target\.name/g, targetAggregates.name)
.replace(/\$target\.value/g, targetAggregates.value.toString())
.replace(/\$target\.in/g, targetAggregates.inValue.toString())
.replace(/\$target\.out/g, targetAggregates.outValue.toString())
.replace(/\$value/g, value.toString());
}
}
else {
return this.hideTooltip();
}
// Trigger tooltipRendering event
var eventNode = null;
var eventLink = null;
if (hitTarget.id.indexOf(nodeElementIdPrefix) === 0) {
var nodeId = hitTarget.getAttribute('aria-label');
var matchedNode = null;
var nodesArray = sankeyChart.nodes;
for (var i = 0; i < nodesArray.length; i++) {
var candidate = nodesArray[i];
if (candidate && candidate.id === nodeId) {
matchedNode = candidate;
break;
}
}
eventNode = matchedNode;
}
else if (hitTarget.tagName.toLowerCase() === 'path' && hitTarget.closest("[id=\"" + linkCollectionId + "\"]")) {
var sourceAttr = hitTarget.getAttribute('data-source');
var targetAttr = hitTarget.getAttribute('data-target');
var matchedLink = null;
var linksArray = sankeyChart.links;
for (var i = 0; i < linksArray.length; i++) {
var candidate = linksArray[i];
if (candidate && candidate.sourceId === sourceAttr && candidate.targetId === targetAttr) {
matchedLink = candidate;
break;
}
}
eventLink = matchedLink;
}
var tooltipRenderArgs = {
text: content,
node: eventNode,
link: eventLink
};
sankeyChart.trigger('tooltipRendering', tooltipRenderArgs);
content = tooltipRenderArgs.text;
// Container
var tooltipContainer = document.getElementById(sankeyChart.element.id + "_tooltip_parent");
if (!tooltipContainer) {
tooltipContainer = document.createElement('div');
tooltipContainer.id = sankeyChart.element.id + "_tooltip_parent";
tooltipContainer.style.cssText = 'position:absolute; left:0; top:0; pointer-events:none; z-index:100;';
sankeyChart.element.appendChild(tooltipContainer); // attach to chart root for consistent offsets
}
// Assemble config - Note: Set template to undefined to use content as HTML
var tooltipConfig = {
opacity: tooltipSettings.opacity,
header: '',
content: [content],
fill: tooltipSettings.fill,
location: location,
offset: 0,
enableAnimation: tooltipSettings.enableAnimation,
shared: false,
crosshair: false,
clipBounds: sankeyChart.initialClipRect,
areaBounds: sankeyChart.initialClipRect,
template: undefined,
theme: sankeyChart.theme,
textStyle: tooltipSettings.textStyle,
isCanvas: false,
isFixed: false,
controlName: 'Sankey',
enableRTL: sankeyChart.enableRtl,
arrowPadding: 0,
availableSize: sankeyChart.availableSize
};
// Show or update
if (isInitialRender || !this.svgTooltip) {
this.svgTooltip = new SVGTooltip(tooltipConfig);
this.svgTooltip.appendTo(tooltipContainer);
}
else {
for (var key in tooltipConfig) {
if (Object.prototype.hasOwnProperty.call(tooltipConfig, key)) {
(this.svgTooltip)[key] = (tooltipConfig)[key];
}
}
this.svgTooltip.dataBind();
}
};
/**
* Triggers tooltip rendering logic when a mouse or pointer release
* action occurs inside the Sankey chart series area.
*
* @param {PointerEvent} event - The mouse or pointer up event within the chart.
* @returns {void}
*
* @private
*/
SankeyTooltip.prototype.handlePointerUp = function (event) {
var sankeyChart = this.sankey;
if (!sankeyChart.tooltip.enable) {
return;
}
if (sankeyChart.isTouch && withInBounds(sankeyChart.mouseX, sankeyChart.mouseY, sankeyChart.initialClipRect)) {
this.renderTooltip(true, event);
if (sankeyChart.tooltip.fadeOutMode === 'Move') {
this.hideTooltip(sankeyChart.tooltip.fadeOutDuration);
}
}
else if (sankeyChart.tooltip.fadeOutMode === 'Click') {
this.hideTooltip(0);
}
};
/**
* Resolves the nearest interactive SVG element (node or link) starting from the given element.
*
* @param {string} chartId - The root chart element id used to construct node ids from label ids.
* @param {Element | null} startElement - The starting element to inspect and traverse from.
* @returns {Element | null }} The resolved element and its type ('node' or 'link').
*
* @private
*/
SankeyTooltip.prototype.resolveInteractiveTarget = function (chartId, startElement) {
var currentElement = startElement;
while (currentElement && currentElement !== document.body) {
var elementId = currentElement.getAttribute('id') || '';
if (elementId.indexOf('_node_level_') > -1 && currentElement instanceof SVGRectElement) {
return { element: currentElement, type: 'node' };
}
if (elementId.indexOf('_link_level_') > -1) {
var pathElement = currentElement.closest('path');
return { element: pathElement || currentElement, type: 'link' };
}
if (elementId.indexOf('_label_level_') > -1) {
var idParts = elementId.split('_');
var level = idParts[idParts.length - 2];
var index = idParts[idParts.length - 1];
var nodeId = chartId + "_node_level_" + level + "_" + index;
var nodeElement = document.getElementById(nodeId);
if (nodeElement instanceof SVGRectElement) {
return { element: nodeElement, type: 'node' };
}
}
currentElement = currentElement.parentElement;
}
return { element: null, type: null };
};
/**
* Triggers tooltip hiding if mouse away from chart series area.
*
* @returns {void}
* @private
*/
SankeyTooltip.prototype.handlePointerLeave = function () {
this.hideTooltip(this.sankey.tooltip.fadeOutDuration);
};
/**
* Triggers tooltip rendering logic when a mouse or pointer action
* occurs within the Sankey chart series area.
*
* Determines the nearest interactive Sankey element (node or link)
* based on the event target and renders or hideTooltips the tooltip accordingly.
*
* @param {boolean} isInitialRender - Indicates whether the tooltip is being rendered for the first time.
* @param {PointerEvent} event - The mouse or pointer event occurring inside the chart.
* @returns {void}
* @private
*/
SankeyTooltip.prototype.renderTooltip = function (isInitialRender, event) {
var targetElement = event.target;
if (!targetElement) {
this.hideTooltip();
return;
}
var resolvedElement = this.resolveInteractiveTarget(this.sankey.element.id, targetElement);
var interactiveElement = resolvedElement.element;
if (interactiveElement) {
this.showTooltipForElement(interactiveElement, isInitialRender);
}
else {
// Hide tooltip only when the pointer is outside interactive Sankey element
this.hideTooltip(this.sankey.tooltip.fadeOutDuration);
}
};
/**
* Computes aggregated metrics for a Sankey node to be used in tooltip content.
*
* Calculates total inbound and outbound values for the given node id,
* and resolves its display name and color (if provided).
*
* @param {string} nodeId - The Sankey node identifier to aggregate values for.
* @param {string} [color] - Optional color associated with the node.
* @returns {SankeyNodeAggregates} Aggregated values for the specified node.
* @private
*/
SankeyTooltip.prototype.computeNodeAggregates = function (nodeId, color) {
var inValue = 0;
var outValue = 0;
// Sum inbound and outbound values for the node
for (var _i = 0, _a = this.sankey.links; _i < _a.length; _i++) {
var link = _a[_i];
if (link.targetId === nodeId) {
inValue += link.value;
}
if (link.sourceId === nodeId) {
outValue += link.value;
}
}
// Find matching node metadata (for display name / color)
var matchedNode = null;
var nodesArray = this.sankey.nodes;
for (var i = 0; i < nodesArray.length; i++) {
if (nodesArray[i].id === nodeId) {
matchedNode = nodesArray[i];
break;
}
}
var matchedNodeLayout = this.sankey.nodeLayoutMap[matchedNode && matchedNode.id];
return {
id: nodeId,
name: (matchedNode && matchedNode.label.text) ? matchedNode.label.text : (matchedNodeLayout && matchedNodeLayout.label) ?
matchedNodeLayout.label : nodeId,
inValue: inValue,
outValue: outValue,
value: Math.max(inValue, outValue),
color: color
};
};
/**
* Hides the tooltip after the specified delay.
*
* @param {number} delay - The delay in milliseconds before hiding the tooltip.
* @returns {void}
* @private
*/
SankeyTooltip.prototype.hideTooltip = function (delay) {
var _this = this;
if (delay === void 0) { delay = this.sankey.tooltip.fadeOutDuration; }
clearTimeout(this.tooltipTimer);
if (this.svgTooltip) {
this.tooltipTimer = window.setTimeout(function () {
if (_this.svgTooltip) {
_this.svgTooltip.fadeOut();
}
setTimeout(function () {
_this.svgTooltip = null; // Ensure tooltip reference is cleared after animation
}, 400);
}, delay);
}
};
/**
* Get module name.
*
* @returns {string} - Returns the module name.
*/
SankeyTooltip.prototype.getModuleName = function () {
return 'SankeyTooltip';
};
/**
* To destroy the tooltip.
*
* @returns {void}
* @private
*/
SankeyTooltip.prototype.destroy = function () {
this.unwireEvents(); // ensure detach
};
return SankeyTooltip;
}());
export { SankeyTooltip };