Skip to content

Commit 1418d46

Browse files
author
wfr
committed
Restructure permanent link-label rendering: own sankey-link-label-set group (fixes z-order with entering link paths), thread the label selection through updateShapes/attachDragHandler/startForce instead of DOM-querying every drag frame, and build each link's model once via a shared linkModels cache instead of duplicating it for paths and labels; exiting labels now fade out like links, and per-link texttemplate lookups use pointNumber instead of the loop index. Also switch link.texttemplate to the shared texttemplateAttrs() helper for a standard description and %{meta} support. More details in draftlog.
1 parent 9f85f6e commit 1418d46

11 files changed

Lines changed: 50 additions & 42 deletions

draftlogs/7888_add.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
- Add permanent on-diagram link labels to the Sankey trace via new `link.textinfo`, `link.texttemplate`, `link.textfont`, `link.valueformat`, and `link.valuesuffix` attributes [[#7888](https://github.com/plotly/plotly.js/pull/7888)].
1+
- Add permanent on-diagram link labels to the Sankey trace via new `link.textinfo`, `link.texttemplate`, `link.texttemplatefallback`, `link.textfont`, `link.valueformat`, and `link.valuesuffix` attributes [[#7888](https://github.com/plotly/plotly.js/pull/7888)].
22
- `link.textinfo` shows `label` and/or `value` directly on each link, opt-in and off by default
3-
- `link.texttemplate` allows full custom formatting, with `%{label}`, `%{value}`, `%{valueLabel}`, `%{source}`, `%{target}`, and `%{customdata}` as available variables
3+
- `link.texttemplate` allows full custom formatting, with `%{label}`, `%{value}`, `%{valueLabel}`, `%{source}`, `%{target}`, `%{customdata}` and `%{meta}` as available variables, resolved using the layout locale
4+
- `link.texttemplatefallback`, `link.textfont`, `link.valueformat` and `link.valuesuffix` control the fallback text for missing template variables, the label font, and value formatting/suffix respectively, each falling back to the corresponding trace-level attribute when unset

src/traces/sankey/attributes.js

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ var baseAttrs = require('../../plots/attributes');
55
var colorAttrs = require('../../components/color/attributes');
66
var fxAttrs = require('../../components/fx/attributes');
77
var domainAttrs = require('../../plots/domain').attributes;
8-
const { hovertemplateAttrs, templatefallbackAttrs } = require('../../plots/template_attributes');
8+
const { hovertemplateAttrs, texttemplateAttrs, templatefallbackAttrs } = require('../../plots/template_attributes');
99
var colorAttributes = require('../../components/colorscale/attributes');
1010
var templatedArray = require('../../plot_api/plot_template').templatedArray;
1111
var descriptionOnlyNumbers = require('../../plots/cartesian/axis_format_attributes').descriptionOnlyNumbers;
@@ -220,19 +220,13 @@ var attrs = (module.exports = overrideAll(
220220
'Any combination of *label* and *value* joined with a *+* OR *none*.'
221221
].join(' ')
222222
},
223-
texttemplate: {
224-
valType: 'string',
225-
dflt: '',
226-
arrayOk: true,
223+
texttemplate: texttemplateAttrs({editType: 'calc'}, {
227224
description: [
228-
'Template string used for rendering the information text that appears',
229-
'permanently on the links. Note that this will override `textinfo`.',
230-
'Variables are inserted using %{variable}, for example',
231-
'*%{label}: %{value}*. Available variables are `label`, `value`,',
232-
'`valueLabel` (the value formatted with `valueformat`/`valuesuffix`),',
233-
'`source`, `target`, `customdata` and `meta`.'
234-
].join(' ')
235-
},
225+
'*%{label}: %{valueLabel}* renders the link label and its formatted',
226+
'value; `valueLabel` is the value formatted with `valueformat`/`valuesuffix`.'
227+
].join(' '),
228+
keys: ['label', 'value', 'valueLabel', 'source', 'target', 'customdata', 'meta']
229+
}),
236230
texttemplatefallback: templatefallbackAttrs(),
237231
textfont: fontAttrs({
238232
autoShadowDflt: 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+
sankeyLinkLabelSet: 'sankey-link-label-set',
1617
sankeyLinkLabel: 'sankey-link-label',
1718
sankeyNodeSet: 'sankey-node-set',
1819
sankeyNode: 'sankey-node',

src/traces/sankey/render.js

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,17 @@ function linkModel(d, l, i) {
346346
};
347347
}
348348

349+
// The link paths and their permanent labels are two joins over the same links,
350+
// so the models are built once per draw and shared between both selections.
351+
function linkModels(d) {
352+
if(!d._linkModels) {
353+
d._linkModels = d.graph.links
354+
.filter(function(l) {return l.value;})
355+
.map(linkModel.bind(null, d));
356+
}
357+
return d._linkModels;
358+
}
359+
349360
function createCircularClosedPathString(link, arrowLen) {
350361
// Using coordinates computed by d3-sankey-circular
351362
var pathString = '';
@@ -704,14 +715,11 @@ function updateNodeShapes(sankeyNode) {
704715
sankeyNode.call(updateNodePositions);
705716
}
706717

707-
function updateShapes(sankeyNode, sankeyLink) {
718+
function updateShapes(sankeyNode, sankeyLink, sankeyLinkLabel) {
708719
sankeyNode.call(updateNodeShapes);
709720
sankeyLink.attr('d', linkPath());
710-
var linkNode = sankeyLink.node();
711-
if(linkNode) {
712-
d3.select(linkNode.parentNode).selectAll('.' + c.cn.sankeyLinkLabel)
713-
.attr('transform', linkLabelTransform);
714-
}
721+
// empty selection when the permanent labels are not opted in
722+
sankeyLinkLabel.attr('transform', linkLabelTransform);
715723
}
716724

717725
function sizeNode(rect) {
@@ -774,7 +782,7 @@ function attachPointerEvents(selection, sankey, eventSet) {
774782
});
775783
}
776784

777-
function attachDragHandler(sankeyNode, sankeyLink, callbacks, gd) {
785+
function attachDragHandler(sankeyNode, sankeyLink, sankeyLinkLabel, callbacks, gd) {
778786
var dragBehavior = d3.behavior.drag()
779787
.origin(function(d) {
780788
return {
@@ -803,7 +811,7 @@ function attachDragHandler(sankeyNode, sankeyLink, callbacks, gd) {
803811
} else { // make a forceLayout if needed
804812
attachForce(sankeyNode, forceKey, d, gd);
805813
}
806-
startForce(sankeyNode, sankeyLink, d, forceKey, gd);
814+
startForce(sankeyNode, sankeyLink, sankeyLinkLabel, d, forceKey, gd);
807815
}
808816
})
809817

@@ -829,7 +837,7 @@ function attachDragHandler(sankeyNode, sankeyLink, callbacks, gd) {
829837
saveCurrentDragPosition(d.node);
830838
if(d.arrangement !== 'snap') {
831839
d.sankey.update(d.graph);
832-
updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink);
840+
updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink, sankeyLinkLabel);
833841
}
834842
})
835843

@@ -865,7 +873,7 @@ function attachForce(sankeyNode, forceKey, d, gd) {
865873
.stop();
866874
}
867875

868-
function startForce(sankeyNode, sankeyLink, d, forceKey, gd) {
876+
function startForce(sankeyNode, sankeyLink, sankeyLinkLabel, d, forceKey, gd) {
869877
window.requestAnimationFrame(function faster() {
870878
var i;
871879
for(i = 0; i < c.forceTicksPerFrame; i++) {
@@ -876,7 +884,7 @@ function startForce(sankeyNode, sankeyLink, d, forceKey, gd) {
876884
switchToSankeyFormat(nodes);
877885

878886
d.sankey.update(d.graph);
879-
updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink);
887+
updateShapes(sankeyNode.filter(sameLayer(d)), sankeyLink, sankeyLinkLabel);
880888

881889
if(d.forceLayouts[forceKey].alpha() > 0) {
882890
window.requestAnimationFrame(faster);
@@ -1042,12 +1050,7 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
10421050
.style('fill', 'none');
10431051

10441052
var sankeyLink = sankeyLinks.selectAll('.' + c.cn.sankeyLink)
1045-
.data(function(d) {
1046-
var links = d.graph.links;
1047-
return links
1048-
.filter(function(l) {return l.value;})
1049-
.map(linkModel.bind(null, d));
1050-
}, keyFun);
1053+
.data(linkModels, keyFun);
10511054

10521055
sankeyLink
10531056
.enter().append('path')
@@ -1074,16 +1077,26 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
10741077
.style('opacity', 0)
10751078
.remove();
10761079

1077-
var sankeyLinkLabel = sankeyLinks.selectAll('.' + c.cn.sankeyLinkLabel)
1080+
// Own group, appended between the links and the nodes: entering link paths
1081+
// of a later draw must not end up on top of already rendered labels.
1082+
var sankeyLinkLabelSet = sankey.selectAll('.' + c.cn.sankeyLinkLabelSet)
1083+
.data(repeat, keyFun);
1084+
1085+
sankeyLinkLabelSet.enter()
1086+
.append('g')
1087+
.classed(c.cn.sankeyLinkLabelSet, true)
1088+
.style('pointer-events', 'none');
1089+
1090+
var sankeyLinkLabel = sankeyLinkLabelSet.selectAll('.' + c.cn.sankeyLinkLabel)
10781091
.data(function(d) {
10791092
var getText = linkTextGetter(gd, d.trace);
10801093
if(!getText) return [];
10811094
var out = [];
1082-
d.graph.links.forEach(function(l, i) {
1083-
if(!l.value) return;
1084-
var txt = getText(l, i);
1095+
linkModels(d).forEach(function(m) {
1096+
// pointNumber indexes the input arrays, so it is the one an
1097+
// arrayOk texttemplate has to be looked up with
1098+
var txt = getText(m.link, m.pointNumber);
10851099
if(!txt) return;
1086-
var m = linkModel(d, l, i);
10871100
m.linkLabelText = txt;
10881101
out.push(m);
10891102
});
@@ -1093,8 +1106,7 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
10931106
sankeyLinkLabel.enter()
10941107
.append('text')
10951108
.classed(c.cn.sankeyLinkLabel, true)
1096-
.attr('text-anchor', 'middle')
1097-
.style('pointer-events', 'none');
1109+
.attr('text-anchor', 'middle');
10981110

10991111
sankeyLinkLabel
11001112
.attr('data-notex', 1)
@@ -1143,7 +1155,7 @@ module.exports = function(gd, svg, calcData, layout, callbacks) {
11431155

11441156
sankeyNode
11451157
.call(attachPointerEvents, sankey, callbacks.nodeEvents)
1146-
.call(attachDragHandler, sankeyLink, callbacks, gd); // has to be here as it binds sankeyLink
1158+
.call(attachDragHandler, sankeyLink, sankeyLinkLabel, callbacks, gd); // has to be here as it binds sankeyLink
11471159

11481160
sankeyNode
11491161
.transition()

src/types/generated/schema.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7010,7 +7010,7 @@ export interface SankeyData {
70107010
* @default 'none'
70117011
*/
70127012
textinfo?: 'label' | 'value' | 'none' | (string & {});
7013-
/** 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`, `customdata` and `meta`. */
7013+
/** Template string used for rendering the information text that appears on points. Note that this will override `textinfo`. Variables are inserted using %{variable}, for example "y: %{y}". Numbers are formatted using d3-format's syntax %{variable:d3-format}, for example "Price: %{y:$.2f}". https://github.com/d3/d3-format/tree/v1.4.5#d3-format for details on the formatting syntax. Dates are formatted using d3-time-format's syntax %{variable|d3-time-format}, for example "Day: %{2019-01-01|%A}". https://github.com/d3/d3-time-format/tree/v2.2.3#locale_format for details on the date formatting syntax. Variables that can't be found will be replaced with the specifier. For example, a template of "data: %{x}, %{y}" will result in a value of "data: 1, %{y}" if x is 1 and y is missing. Variables with an undefined value will be replaced with the fallback value. All attributes that can be specified per-point (the ones that are `arrayOk: true`) are available. Finally, the template string has access to variables `label`, `value`, `valueLabel`, `source`, `target`, `customdata` and `meta`. */
70147014
texttemplate?: string | string[];
70157015
/**
70167016
* Fallback string that's displayed when a variable referenced in a template is missing. If the boolean value 'false' is passed in, the specifier with the missing variable will be displayed.
-289 Bytes
Loading
-31 Bytes
Loading
-5 Bytes
Loading
7 Bytes
Loading
24 Bytes
Loading

0 commit comments

Comments
 (0)