I’m building a custom Sankey widget as a web component. I’m using plotly.js and the plot renders correctly, but hover tooltips don’t appear at all. Does plotly.js currently support hover tooltips for Sankey traces inside Shadow DOM? I tried global CSS styling, but this did not solve the problem. Another thing is that the nodes are not clickable, so when i click a node it says "link clicked". Here is a minimal example of the problem:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title></title>
<script src="https://cdn.plot.ly/plotly-2.30.1.min.js"></script>
<style>
html, body {
height: 100%;
margin: 0;
}
sankey-sd {
display: block;
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<sankey-sd></sankey-sd>
<script>
window.addEventListener('DOMContentLoaded', () => {
class SankeySD extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const chartDiv = document.createElement('div');
chartDiv.id = 'chart';
chartDiv.style.width = '100%';
chartDiv.style.height = '100%';
chartDiv.style.minWidth = '200px';
chartDiv.style.minHeight = '200px';
this.shadowRoot.appendChild(chartDiv);
const labels = ["Start", "Middle", "Begin", "End", "Final"];
const labelIndex = new Map(labels.map((label, i) => [label, i]));
const links = [
{ source: "Start", target: "Middle", value: 5, label: "Test" },
{ source: "Start", target: "Middle", value: 3, label: "Test2" },
{ source: "Middle", target: "Start", value: 1, label: "" },
{ source: "Start", target: "End", value: 2, label: "" },
{ source: "Begin", target: "Middle", value: 5, label: "Test" },
{ source: "Middle", target: "End", value: 3, label: "" },
{ source: "Final", target: "Final", value: 0.0001, label: "" }
];
const sources = links.map(link => labelIndex.get(link.source));
const targets = links.map(link => labelIndex.get(link.target));
const values = links.map(link => link.value);
const customData = links.map(link => [link.source, link.target, link.value]);
const trace = {
type: "sankey",
orientation: "h",
arrangement: "fixed",
node: {
label: labels,
pad: 15,
thickness: 20,
line: { color: "black", width: 0.5 },
hoverlabel: {
bgcolor: "white",
bordercolor: "darkgrey",
font: {
color: "black",
family: "Open Sans, Arial",
size: 14
}
},
hovertemplate: '%{label}<extra></extra>',
color: ["#a6cee3", "#1f78b4", "#b2df8a", "#a9b1b9", "#a9b1b9" ]
},
link: {
source: sources,
target: targets,
value: values,
arrowlen: 20,
pad: 20,
thickness: 20,
line: { color: "black", width: 0.2 },
color: sources.map(i => ["#a6cee3", "#1f78b4", "#b2df8a", "#a9b1b9", "#a9b1b9"][i]),
customdata: customData,
hoverlabel: {
bgcolor: "white",
bordercolor: "darkgrey",
font: {
color: "black",
family: "Open Sans, Arial",
size: 14
}
},
hovertemplate:
'<b>%{customdata[0]}</b> → <b>%{customdata[1]}</b><br>' +
'Flow Value: <b>%{customdata[2]}</b><extra></extra>'
}
};
const layout = {
font: { size: 14 },
margin: { t: 20, l: 10, r: 10, b: 10 },
hovermode: 'closest'
};
Plotly.newPlot(chartDiv, [trace], layout, { responsive: true, displayModeBar: true })
.then((plot) => {
chartDiv.on('plotly_click', function(eventData) {
if (!eventData || !eventData.points || !eventData.points.length) return;
const point = eventData.points[0];
if (typeof point.pointIndex === "number") {
const nodeLabel = point.label;
alert("Node clicked: " + nodeLabel + "\nNode index: " + point.pointIndex);
console.log("Node clicked:", point);
} else if (typeof point.pointNumber === "number") {
const linkIdx = point.pointNumber;
const linkData = customData[linkIdx];
alert(
"Link clicked: " +
linkData[0] + " → " + linkData[1] +
"\nValue: " + linkData[2] +
"\nLink index: " + linkIdx
);
console.log("Link clicked:", point);
} else {
console.log("Clicked background", point);
}
});
});
}
}
customElements.define('sankey-sd', SankeySD);
});
</script>
</body>
</html>
Plotly.js creates a global stylesheet that is used to show the tooltip (called in plotly.js hoverbox/hoverlayer/hoverlabel), as well as for other features - for instance, you can see the "modelbar" (the icon menu that's by default at the top-right of the plot div) is misplaced in your shadow-dom version.
The issue is thus the fact that the global stylesheets are not applied to the shadow DOM. Based on the approach in this Medium article by EisenbergEffect I applied the global stylesheets to the shadow root of your sankey-sd
, using the function:
function addGlobalStylesToShadowRoot(shadowRoot) {
const globalSheets = Array.from(document.styleSheets)
.map(x => {
const sheet = new CSSStyleSheet();
const css = Array.from(x.cssRules).map(rule => rule.cssText).join(' ');
sheet.replaceSync(css);
return sheet;
});
shadowRoot.adoptedStyleSheets.push(
...globalSheets
);
}
applied in the constructor of class SankeySD
:
class SankeySD extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
addGlobalStylesToShadowRoot(this.shadowRoot); // new line
}
// ............... other methods
}
and it did enable the tooltip and corrected the position of the modelbar.
Here's a stack snippet demo, based on your original code:
//from https://eisenbergeffect.medium.com/using-global-styles-in-shadow-dom-5b80e802e89d
function addGlobalStylesToShadowRoot(shadowRoot) {
const globalSheets = Array.from(document.styleSheets)
.map(x => {
const sheet = new CSSStyleSheet();
const css = Array.from(x.cssRules).map(rule => rule.cssText).join(' ');
sheet.replaceSync(css);
return sheet;
});
shadowRoot.adoptedStyleSheets.push(
...globalSheets
);
}
window.addEventListener('DOMContentLoaded', () => {
class SankeySD extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
addGlobalStylesToShadowRoot(this.shadowRoot);
}
connectedCallback() {
const chartDiv = document.createElement('div');
chartDiv.id = 'chart';
chartDiv.style.width = '100%';
chartDiv.style.height = '100%';
chartDiv.style.minWidth = '500px';
chartDiv.style.minHeight = '400px';
this.shadowRoot.appendChild(chartDiv);
const labels = ["Start", "Middle", "Begin", "End", "Final"];
const labelIndex = new Map(labels.map((label, i) => [label, i]));
const links = [
{ source: "Start", target: "Middle", value: 5, label: "Test" },
{ source: "Start", target: "Middle", value: 3, label: "Test2" },
{ source: "Middle", target: "Start", value: 1, label: "" },
{ source: "Start", target: "End", value: 2, label: "" },
{ source: "Begin", target: "Middle", value: 5, label: "Test" },
{ source: "Middle", target: "End", value: 3, label: "" },
{ source: "Final", target: "Final", value: 0.0001, label: "" }
];
const sources = links.map(link => labelIndex.get(link.source));
const targets = links.map(link => labelIndex.get(link.target));
const values = links.map(link => link.value);
const customData = links.map(link => [link.source, link.target, link.value]);
const trace = {
type: "sankey",
orientation: "h",
arrangement: "fixed",
node: {
label: labels,
pad: 15,
thickness: 20,
line: { color: "black", width: 0.5 },
hoverlabel: {
bgcolor: "white",
bordercolor: "darkgrey",
font: {
color: "black",
family: "Open Sans, Arial",
size: 14
}
},
hovertemplate: '%{label}<extra></extra>',
color: ["#a6cee3", "#1f78b4", "#b2df8a", "#a9b1b9", "#a9b1b9" ]
},
link: {
source: sources,
target: targets,
value: values,
arrowlen: 20,
pad: 20,
thickness: 20,
line: { color: "black", width: 0.2 },
color: sources.map(i => ["#a6cee3", "#1f78b4", "#b2df8a", "#a9b1b9", "#a9b1b9"][i]),
customdata: customData,
hoverlabel: {
bgcolor: "white",
bordercolor: "darkgrey",
font: {
color: "black",
family: "Open Sans, Arial",
size: 14
}
},
hovertemplate:
'<b>%{customdata[0]}</b> → <b>%{customdata[1]}</b><br>' +
'Flow Value: <b>%{customdata[2]}</b><extra></extra>'
}
};
const layout = {
font: { size: 14 },
//margin: { t: 20, l: 10, r: 10, b: 10 },
//hovermode: 'closest'
};
Plotly.newPlot(chartDiv, [trace], layout, { responsive: true, displayModeBar: true })
.then((plot) => {
chartDiv.on('plotly_click', function(eventData) {
console.log(eventData);
if (!eventData || !eventData.points || !eventData.points.length) return;
const point = eventData.points[0];
if (typeof point.pointIndex === "number") {
const nodeLabel = point.label;
alert("Node clicked: " + nodeLabel + "\nNode index: " + point.pointIndex);
console.log("Node clicked:", point);
} else if (typeof point.pointNumber === "number") {
const linkIdx = point.pointNumber;
const linkData = customData[linkIdx];
alert(
"Link clicked: " +
linkData[0] + " → " + linkData[1] +
"\nValue: " + linkData[2] +
"\nLink index: " + linkIdx
);
console.log("Link clicked:", point);
} else {
console.log("Clicked background", point);
}
});
});
}
}
customElements.define('sankey-sd', SankeySD);
});
html, body {
height: 100%;
margin: 0;
}
sankey-sd {
display: block;
width: 100vw;
height: 100vh;
}
<sankey-sd></sankey-sd>
<script src="https://cdn.plot.ly/plotly-3.0.1.min.js" charset="utf-8"></script>
<!-- also works with v 2.30.1-->
The click feature is not caused by the shadow DOM; in this fiddle that uses the same plot configuration, but without the shadow DOM, the behaviour is the same - there's always a point.pointNumber
and never a point.pointIndex
.
I can't find the code you have used, can you please show the version that works? This might become another question, as there should not be multiple issues per post, if their solutions are unrelated.