From eff3eb9ade612ca27bf0a7bc4f56cf36783e16f1 Mon Sep 17 00:00:00 2001 From: wfr Date: Fri, 3 Jul 2026 08:52:25 +0200 Subject: [PATCH 1/3] Add permanent text labels to Sankey links --- src/traces/sankey/attributes.js | 44 ++++++++++++++++ src/traces/sankey/constants.js | 1 + src/traces/sankey/defaults.js | 9 ++++ src/traces/sankey/render.js | 91 +++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+) diff --git a/src/traces/sankey/attributes.js b/src/traces/sankey/attributes.js index a2ee49f0083..b77b3369a79 100644 --- a/src/traces/sankey/attributes.js +++ b/src/traces/sankey/attributes.js @@ -185,6 +185,50 @@ var attrs = (module.exports = overrideAll( dflt: [], description: 'The shown name of the link.' }, + textinfo: { + valType: 'flaglist', + flags: ['label', 'value'], + extras: ['none'], + dflt: 'none', + description: [ + 'Determines which trace information appears permanently on the links.', + 'Any combination of *label* and *value* joined with a *+* OR *none*.' + ].join(' ') + }, + texttemplate: { + valType: 'string', + dflt: '', + arrayOk: true, + description: [ + 'Template string used for rendering the information text that appears', + 'permanently on the links. Note that this will override `textinfo`.', + 'Variables are inserted using %{variable}, for example', + '*%{label}: %{value}*. Available variables are `label`, `value`,', + '`valueLabel` (the value formatted with `valueformat`/`valuesuffix`),', + '`source`, `target` and `customdata`.' + ].join(' ') + }, + textfont: fontAttrs({ + autoShadowDflt: true, + description: 'Sets the font for the permanent link labels.' + }), + valueformat: { + valType: 'string', + dflt: '', + description: [ + 'Sets the value formatting rule for the permanent link labels,', + 'using d3 formatting mini-languages. Falls back to the trace-level', + '`valueformat` when empty.' + ].join(' ') + }, + valuesuffix: { + valType: 'string', + dflt: '', + description: [ + 'Adds a unit to follow the value in the permanent link labels.', + 'Falls back to the trace-level `valuesuffix` when empty.' + ].join(' ') + }, color: { valType: 'color', arrayOk: true, diff --git a/src/traces/sankey/constants.js b/src/traces/sankey/constants.js index c7eda8ca65b..da8c8c4ee62 100644 --- a/src/traces/sankey/constants.js +++ b/src/traces/sankey/constants.js @@ -13,6 +13,7 @@ module.exports = { sankey: 'sankey', sankeyLinks: 'sankey-links', sankeyLink: 'sankey-link', + sankeyLinkLabel: 'sankey-link-label', sankeyNodeSet: 'sankey-node-set', sankeyNode: 'sankey-node', nodeRect: 'node-rect', diff --git a/src/traces/sankey/defaults.js b/src/traces/sankey/defaults.js index d9ca15fea65..d611251a2ae 100644 --- a/src/traces/sankey/defaults.js +++ b/src/traces/sankey/defaults.js @@ -108,6 +108,15 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout Lib.coerceFont(coerce, 'textfont', layout.font, { autoShadowDflt: true }); + // permanent link text labels (opt-in via link.textinfo / link.texttemplate) + var linkTextInfo = coerceLink('textinfo'); + var linkTextTemplate = coerceLink('texttemplate'); + if(linkTextInfo !== 'none' || linkTextTemplate) { + Lib.coerceFont(coerceLink, 'textfont', traceOut.textfont, { autoShadowDflt: true }); + coerceLink('valueformat', traceOut.valueformat); + coerceLink('valuesuffix', traceOut.valuesuffix); + } + // disable 1D transforms - arrays here are 1D but their lengths/meanings // don't match, between nodes and links traceOut._length = null; diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index a07d18a9c9e..a1f8dc89a42 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -534,6 +534,59 @@ function linkPath() { return path; } +// Builds the permanent-label string for a link from textinfo / texttemplate. +function linkTextGetter(trace) { + var linkAttr = trace.link; + var textinfo = linkAttr.textinfo; + var texttemplate = linkAttr.texttemplate; + var vFmt = linkAttr.valueformat; + var vSuf = linkAttr.valuesuffix; + var flags = (textinfo && textinfo !== 'none') ? textinfo.split('+') : []; + + return function(l, i) { + var valueLabel = Lib.numberFormat(vFmt)(l.value) + vSuf; + + var tt = Array.isArray(texttemplate) ? texttemplate[i] : texttemplate; + if(tt) { + return Lib.texttemplateString({ + template: tt, + labels: {valueLabel: valueLabel}, + data: [{ + label: l.label, + value: l.value, + source: l.source.label, + target: l.target.label, + customdata: l.customdata + }] + }); + } + + if(!flags.length) return ''; + var parts = []; + if(flags.indexOf('label') !== -1 && l.label) parts.push(l.label); + if(flags.indexOf('value') !== -1) parts.push(valueLabel); + return parts.join('
'); + }; +} + +// Positions a permanent link label at the link midpoint (layout frame) and keeps +// the glyphs upright. +function linkLabelTransform(d) { + var l = d.link; + var midX, midY; + if(l.circular) { + // same anchor as the hover label (see hoverCenterPosition in plot.js) + midX = (l.circularPathData.leftInnerExtent + l.circularPathData.rightInnerExtent) / 2; + midY = l.circularPathData.verticalFullExtent; + } else { + midX = (l.source.x1 + l.target.x0) / 2; + midY = (l.y0 + l.y1) / 2; + } + var p = d.parent; + var flip = p.horizontal ? '' : ('scale(-1,1)' + strRotate(90)); + return strTranslate(midX, midY) + flip; +} + function nodeModel(d, n) { var tc = tinycolor(n.color); var zoneThicknessPad = c.nodePadAcross; @@ -607,6 +660,11 @@ function updateNodeShapes(sankeyNode) { function updateShapes(sankeyNode, sankeyLink) { sankeyNode.call(updateNodeShapes); sankeyLink.attr('d', linkPath()); + var linkNode = sankeyLink.node(); + if(linkNode) { + d3.select(linkNode.parentNode).selectAll('.' + c.cn.sankeyLinkLabel) + .attr('transform', linkLabelTransform); + } } function sizeNode(rect) { @@ -966,6 +1024,39 @@ module.exports = function(gd, svg, calcData, layout, callbacks) { .style('opacity', 0) .remove(); + var sankeyLinkLabel = sankeyLinks.selectAll('.' + c.cn.sankeyLinkLabel) + .data(function(d) { + var getText = linkTextGetter(d.trace); + var out = []; + d.graph.links.forEach(function(l, i) { + if(!l.value) return; + var txt = getText(l, i); + if(!txt) return; + var m = linkModel(d, l, i); + m.linkLabelText = txt; + out.push(m); + }); + return out; + }, keyFun); + + sankeyLinkLabel.enter() + .append('text') + .classed(c.cn.sankeyLinkLabel, true) + .attr('text-anchor', 'middle') + .style('pointer-events', 'none'); + + sankeyLinkLabel + .attr('data-notex', 1) + .text(function(d) { return d.linkLabelText; }) + .each(function(d) { + var e = d3.select(this); + Drawing.font(e, d.link.trace.link.textfont); + svgTextUtils.convertToTspans(e, gd); + }) + .attr('transform', linkLabelTransform); + + sankeyLinkLabel.exit().remove(); + var sankeyNodeSet = sankey.selectAll('.' + c.cn.sankeyNodeSet) .data(repeat, keyFun); From e7ca79cf47c540ce48381141793cd6d28c395e92 Mon Sep 17 00:00:00 2001 From: wfr Date: Fri, 3 Jul 2026 09:11:10 +0200 Subject: [PATCH 2/3] Minor fixes --- src/traces/sankey/render.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index a1f8dc89a42..36061b00a23 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -554,6 +554,7 @@ function linkTextGetter(trace) { data: [{ label: l.label, value: l.value, + valueLabel: valueLabel, source: l.source.label, target: l.target.label, customdata: l.customdata @@ -1043,6 +1044,7 @@ module.exports = function(gd, svg, calcData, layout, callbacks) { .append('text') .classed(c.cn.sankeyLinkLabel, true) .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'central') .style('pointer-events', 'none'); sankeyLinkLabel From d7f7dea4df633abaa1dda7774d6873a0e293911a Mon Sep 17 00:00:00 2001 From: wfr Date: Fri, 3 Jul 2026 09:45:57 +0200 Subject: [PATCH 3/3] Add draftlog --- draftlogs/7888_add.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/7888_add.md diff --git a/draftlogs/7888_add.md b/draftlogs/7888_add.md new file mode 100644 index 00000000000..9f982bfdb72 --- /dev/null +++ b/draftlogs/7888_add.md @@ -0,0 +1 @@ + - Add permanent on-diagram link labels to `sankey` traces via `link.textinfo`, `link.texttemplate`, `link.textfont`, `link.valueformat` and `link.valuesuffix` [[#7888](https://github.com/plotly/plotly.js/pull/7888)] \ No newline at end of file