I am pursuing this somewhat, but the approach is not super ideal as it requires waiting and when users interact with the app by it drag and drop a node and thereby changing links’ labels’ positions or selecting different data to pass into the figure, the process would need to get repeated (in the case of dragging, it seems crazy to me to run checks all the time). Does anyone have a better idea than the below?
import plotly.graph_objects as go
from dash import Dash, dcc, html, Input, Output, clientside_callback
fig = go.Figure(data=[go.Sankey(
node = dict(
pad = 15,
thickness = 20,
line = dict(color = "black", width = 0.5),
label = \["Node A", "Node B", "Node C", "Node D"\],
),
link = dict(
source = \[0, 1, 0, 2\],
target = \[2, 3, 0, 3\],
value = \[8, 4, 2, 8\],
label = \["A to C: 8", "B to D: 4", "A to D: 2", "C to D: 8"\],
hovertemplate='%{label}<extra></extra>',
)
)])
app = Dash(_name_)
app.layout = html.Div([
dcc.Graph(id='sankey-graph', figure=fig),
dcc.Interval(id='interval', interval=2000, n_intervals=0, max_intervals=1),
html.Div(id='dummy-output')
])
# Clientside callback - no external file needed
app.clientside_callback(
"""
function(n_intervals) {
if (n_intervals === 1) {
const graphDiv = document.getElementById('sankey-graph');
const links = document.querySelectorAll('.sankey-link');
const hoverLayer = document.querySelector('.hoverlayer');
// Create permanent layer
const permanentLayer = document.createElementNS(' http://www.w3.org/2000/svg ', 'g');
permanentLayer.setAttribute('class', 'permanent-labels');
hoverLayer.parentNode.appendChild(permanentLayer);
let processedCount = 0;
const totalLinks = links.length;
// Process all links with minimal stagger (20ms)
links.forEach((link, index) => {
setTimeout(() => {
const bbox = link.getBBox();
const ctm = link.getCTM();
const centerX = bbox.x + bbox.width / 2;
const centerY = bbox.y + bbox.height / 2;
const point = link.ownerSVGElement.createSVGPoint();
point.x = centerX;
point.y = centerY;
const screenPoint = point.matrixTransform(ctm);
link.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true,
cancelable: true,
view: window,
clientX: screenPoint.x,
clientY: screenPoint.y
}));
// Very short wait before cloning
setTimeout(() => {
if (hoverLayer.children.length > 0) {
const hoverElement = hoverLayer.lastElementChild;
const clone = hoverElement.cloneNode(true);
clone.classList.add('permanent-label-item');
permanentLayer.appendChild(clone);
processedCount++;
// Clear the temporary hover immediately
link.dispatchEvent(new MouseEvent('mouseout', {bubbles: true}));
}
}, 30); // Very fast
}, index \* 50); // Much faster sequencing
});
setTimeout(() => {
console.log(\`${processedCount}/${totalLinks} labels created\`);
}, totalLinks \* 50 + 100);
}
return '';
}
""",
Output('dummy-output', 'children'),
Input('interval', 'n_intervals')
)
if _name_ == ‘_main_’:
app.run(port=7878, debug=True)