Skip to content

Commit eff3eb9

Browse files
author
wfr
committed
Add permanent text labels to Sankey links
1 parent c23d3b3 commit eff3eb9

4 files changed

Lines changed: 145 additions & 0 deletions

File tree

src/traces/sankey/attributes.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,50 @@ var attrs = (module.exports = overrideAll(
185185
dflt: [],
186186
description: 'The shown name of the link.'
187187
},
188+
textinfo: {
189+
valType: 'flaglist',
190+
flags: ['label', 'value'],
191+
extras: ['none'],
192+
dflt: 'none',
193+
description: [
194+
'Determines which trace information appears permanently on the links.',
195+
'Any combination of *label* and *value* joined with a *+* OR *none*.'
196+
].join(' ')
197+
},
198+
texttemplate: {
199+
valType: 'string',
200+
dflt: '',
201+
arrayOk: true,
202+
description: [
203+
'Template string used for rendering the information text that appears',
204+
'permanently on the links. Note that this will override `textinfo`.',
205+
'Variables are inserted using %{variable}, for example',
206+
'*%{label}: %{value}*. Available variables are `label`, `value`,',
207+
'`valueLabel` (the value formatted with `valueformat`/`valuesuffix`),',
208+
'`source`, `target` and `customdata`.'
209+
].join(' ')
210+
},
211+
textfont: fontAttrs({
212+
autoShadowDflt: true,
213+
description: 'Sets the font for the permanent link labels.'
214+
}),
215+
valueformat: {
216+
valType: 'string',
217+
dflt: '',
218+
description: [
219+
'Sets the value formatting rule for the permanent link labels,',
220+
'using d3 formatting mini-languages. Falls back to the trace-level',
221+
'`valueformat` when empty.'
222+
].join(' ')
223+
},
224+
valuesuffix: {
225+
valType: 'string',
226+
dflt: '',
227+
description: [
228+
'Adds a unit to follow the value in the permanent link labels.',
229+
'Falls back to the trace-level `valuesuffix` when empty.'
230+
].join(' ')
231+
},
188232
color: {
189233
valType: 'color',
190234
arrayOk: true,

src/traces/sankey/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
sankey: 'sankey',
1414
sankeyLinks: 'sankey-links',
1515
sankeyLink: 'sankey-link',
16+
sankeyLinkLabel: 'sankey-link-label',
1617
sankeyNodeSet: 'sankey-node-set',
1718
sankeyNode: 'sankey-node',
1819
nodeRect: 'node-rect',

src/traces/sankey/defaults.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
108108

109109
Lib.coerceFont(coerce, 'textfont', layout.font, { autoShadowDflt: true });
110110

111+
// permanent link text labels (opt-in via link.textinfo / link.texttemplate)
112+
var linkTextInfo = coerceLink('textinfo');
113+
var linkTextTemplate = coerceLink('texttemplate');
114+
if(linkTextInfo !== 'none' || linkTextTemplate) {
115+
Lib.coerceFont(coerceLink, 'textfont', traceOut.textfont, { autoShadowDflt: true });
116+
coerceLink('valueformat', traceOut.valueformat);
117+
coerceLink('valuesuffix', traceOut.valuesuffix);
118+
}
119+
111120
// disable 1D transforms - arrays here are 1D but their lengths/meanings
112121
// don't match, between nodes and links
113122
traceOut._length = null;

src/traces/sankey/render.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,59 @@ function linkPath() {
534534
return path;
535535
}
536536

537+
// Builds the permanent-label string for a link from textinfo / texttemplate.
538+
function linkTextGetter(trace) {
539+
var linkAttr = trace.link;
540+
var textinfo = linkAttr.textinfo;
541+
var texttemplate = linkAttr.texttemplate;
542+
var vFmt = linkAttr.valueformat;
543+
var vSuf = linkAttr.valuesuffix;
544+
var flags = (textinfo && textinfo !== 'none') ? textinfo.split('+') : [];
545+
546+
return function(l, i) {
547+
var valueLabel = Lib.numberFormat(vFmt)(l.value) + vSuf;
548+
549+
var tt = Array.isArray(texttemplate) ? texttemplate[i] : texttemplate;
550+
if(tt) {
551+
return Lib.texttemplateString({
552+
template: tt,
553+
labels: {valueLabel: valueLabel},
554+
data: [{
555+
label: l.label,
556+
value: l.value,
557+
source: l.source.label,
558+
target: l.target.label,
559+
customdata: l.customdata
560+
}]
561+
});
562+
}
563+
564+
if(!flags.length) return '';
565+
var parts = [];
566+
if(flags.indexOf('label') !== -1 && l.label) parts.push(l.label);
567+
if(flags.indexOf('value') !== -1) parts.push(valueLabel);
568+
return parts.join('<br>');
569+
};
570+
}
571+
572+
// Positions a permanent link label at the link midpoint (layout frame) and keeps
573+
// the glyphs upright.
574+
function linkLabelTransform(d) {
575+
var l = d.link;
576+
var midX, midY;
577+
if(l.circular) {
578+
// same anchor as the hover label (see hoverCenterPosition in plot.js)
579+
midX = (l.circularPathData.leftInnerExtent + l.circularPathData.rightInnerExtent) / 2;
580+
midY = l.circularPathData.verticalFullExtent;
581+
} else {
582+
midX = (l.source.x1 + l.target.x0) / 2;
583+
midY = (l.y0 + l.y1) / 2;
584+
}
585+
var p = d.parent;
586+
var flip = p.horizontal ? '' : ('scale(-1,1)' + strRotate(90));
587+
return strTranslate(midX, midY) + flip;
588+
}
589+
537590
function nodeModel(d, n) {
538591
var tc = tinycolor(n.color);
539592
var zoneThicknessPad = c.nodePadAcross;
@@ -607,6 +660,11 @@ function updateNodeShapes(sankeyNode) {
607660
function updateShapes(sankeyNode, sankeyLink) {
608661
sankeyNode.call(updateNodeShapes);
609662
sankeyLink.attr('d', linkPath());
663+
var linkNode = sankeyLink.node();
664+
if(linkNode) {
665+
d3.select(linkNode.parentNode).selectAll('.' + c.cn.sankeyLinkLabel)
666+
.attr('transform', linkLabelTransform);
667+
}
610668
}
611669

612670
function sizeNode(rect) {
@@ -966,6 +1024,39 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
9661024
.style('opacity', 0)
9671025
.remove();
9681026

1027+
var sankeyLinkLabel = sankeyLinks.selectAll('.' + c.cn.sankeyLinkLabel)
1028+
.data(function(d) {
1029+
var getText = linkTextGetter(d.trace);
1030+
var out = [];
1031+
d.graph.links.forEach(function(l, i) {
1032+
if(!l.value) return;
1033+
var txt = getText(l, i);
1034+
if(!txt) return;
1035+
var m = linkModel(d, l, i);
1036+
m.linkLabelText = txt;
1037+
out.push(m);
1038+
});
1039+
return out;
1040+
}, keyFun);
1041+
1042+
sankeyLinkLabel.enter()
1043+
.append('text')
1044+
.classed(c.cn.sankeyLinkLabel, true)
1045+
.attr('text-anchor', 'middle')
1046+
.style('pointer-events', 'none');
1047+
1048+
sankeyLinkLabel
1049+
.attr('data-notex', 1)
1050+
.text(function(d) { return d.linkLabelText; })
1051+
.each(function(d) {
1052+
var e = d3.select(this);
1053+
Drawing.font(e, d.link.trace.link.textfont);
1054+
svgTextUtils.convertToTspans(e, gd);
1055+
})
1056+
.attr('transform', linkLabelTransform);
1057+
1058+
sankeyLinkLabel.exit().remove();
1059+
9691060
var sankeyNodeSet = sankey.selectAll('.' + c.cn.sankeyNodeSet)
9701061
.data(repeat, keyFun);
9711062

0 commit comments

Comments
 (0)