Sankey: Display Link Values Permanently?

I’m working with Plotly’s Sankey diagrams in Python and I would like to display the values on the links permanently, not just when hovering. I’ve looked through the documentation and source code but couldn’t find a property to achieve this.

Current behavior:

  • Values only show when hovering over links

I’ve tried:

I know it’s a different site, but on sankeyart, it’s displayed nicely and allow easy toggle on-off (which should be doable with dash)

Is there a way to achieve this in Plotly?

EDIT:
So, I looked at the plotly.js github issues, and it seems like there has been this issue open for a couple of years: add optional text on Sankey links · Issue #4746 · plotly/plotly.js · GitHub

I guess it’s not possible…

1 Like

Use this js code to add link values permanently, I tested it in my R program, it works excellent.

htmlwidgets::onRender(x=p, jsCode=’
function(el) {
//reference code: https://stackoverflow.com/questions/67291003/add-text-to-svg-path-dynamically
//reference code: https://stackoverflow.com/questions/9281199/adding-text-to-svg-document-in-javascript
//reference code: https://stackoverflow.com/questions/52335837/event-when-clicking-a-name-in-the-legend-of-a-plotlys-graph-in-r-shiny
//reference code: https://stackoverflow.com/questions/42280913/troubleshoot-javascript-code-in-onrender-function-of-htmlwidgets
function addLabelText(bgPath, labelText) {
let bbox = bgPath.getBBox();
let x = bbox.x + bbox.width / 2;
let y = bbox.y + bbox.height / 2;

     let textElem = document.createElementNS("http://www.w3.org/2000/svg", "text");
     textElem.setAttribute("x", x);
     textElem.setAttribute("y", y);
     textElem.setAttribute("text-anchor", "middle");
     textElem.setAttribute("font-size", "10px");
     textElem.setAttribute("fill", "black");
     textElem.textContent = labelText;

     bgPath.parentNode.appendChild(textElem);
     console.log(bgPath.parentNode)
   }

   let myGraph = document.getElementById(el.id);
   let nodes = document.querySelectorAll("g.sankey-node");
   let links = document.querySelectorAll(".sankey-link");
   console.log(myGraph)
   console.log(nodes[1])
   console.log(links[1])      

   for (let i = 0; i < links.length; i++) {
     addLabelText(links[i], links[i].__data__.link.value)           
   }

}')

Can you explain how to integrate this in Python?

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)