Skip to content

Commit 94cf09e

Browse files
authored
Merge pull request #6276 from Andy2003/feature/sanky-with-arrows
Add support for sankey links with arrows
2 parents a154ab9 + 15b06b9 commit 94cf09e

11 files changed

+145
-32
lines changed

draftlogs/6276_add.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Add support for sankey links with arrows [[#6276](https://github.com/plotly/plotly.js/pull/6276)],
2+
with thanks to @Andy2003 for the contribution!

src/traces/sankey/attributes.js

+8
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ var attrs = module.exports = overrideAll({
168168
},
169169

170170
link: {
171+
arrowlen: {
172+
valType: 'number',
173+
min: 0,
174+
dflt: 0,
175+
description: [
176+
'Sets the length (in px) of the links arrow, if 0 no arrow will be drawn.'
177+
].join(' ')
178+
},
171179
label: {
172180
valType: 'data_array',
173181
dflt: [],

src/traces/sankey/defaults.js

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
5252
return Lib.coerce(linkIn, linkOut, attributes.link, attr, dflt);
5353
}
5454
coerceLink('label');
55+
coerceLink('arrowlen');
5556
coerceLink('source');
5657
coerceLink('target');
5758
coerceLink('value');

src/traces/sankey/render.js

+43-32
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ function sankeyModel(layout, d, traceIndex) {
271271
nodeLineWidth: trace.node.line.width,
272272
linkLineColor: trace.link.line.color,
273273
linkLineWidth: trace.link.line.width,
274+
linkArrowLength: trace.link.arrowlen,
274275
valueFormat: trace.valueformat,
275276
valueSuffix: trace.valuesuffix,
276277
textFont: trace.textfont,
@@ -309,6 +310,7 @@ function linkModel(d, l, i) {
309310
linkPath: linkPath,
310311
linkLineColor: d.linkLineColor,
311312
linkLineWidth: d.linkLineWidth,
313+
linkArrowLength: d.linkArrowLength,
312314
valueFormat: d.valueFormat,
313315
valueSuffix: d.valueSuffix,
314316
sankey: d.sankey,
@@ -318,7 +320,7 @@ function linkModel(d, l, i) {
318320
};
319321
}
320322

321-
function createCircularClosedPathString(link) {
323+
function createCircularClosedPathString(link, arrowLen) {
322324
// Using coordinates computed by d3-sankey-circular
323325
var pathString = '';
324326
var offset = link.width / 2;
@@ -328,17 +330,17 @@ function createCircularClosedPathString(link) {
328330
pathString =
329331
// start at the left of the target node
330332
'M ' +
331-
coords.targetX + ' ' + (coords.targetY + offset) + ' ' +
333+
(coords.targetX - arrowLen) + ' ' + (coords.targetY + offset) + ' ' +
332334
'L' +
333-
coords.rightInnerExtent + ' ' + (coords.targetY + offset) +
335+
(coords.rightInnerExtent - arrowLen) + ' ' + (coords.targetY + offset) +
334336
'A' +
335337
(coords.rightLargeArcRadius + offset) + ' ' + (coords.rightSmallArcRadius + offset) + ' 0 0 1 ' +
336-
(coords.rightFullExtent - offset) + ' ' + (coords.targetY - coords.rightSmallArcRadius) +
338+
(coords.rightFullExtent - offset - arrowLen) + ' ' + (coords.targetY - coords.rightSmallArcRadius) +
337339
'L' +
338-
(coords.rightFullExtent - offset) + ' ' + coords.verticalRightInnerExtent +
340+
(coords.rightFullExtent - offset - arrowLen) + ' ' + coords.verticalRightInnerExtent +
339341
'A' +
340342
(coords.rightLargeArcRadius + offset) + ' ' + (coords.rightLargeArcRadius + offset) + ' 0 0 1 ' +
341-
coords.rightInnerExtent + ' ' + (coords.verticalFullExtent - offset) +
343+
(coords.rightInnerExtent - arrowLen) + ' ' + (coords.verticalFullExtent - offset) +
342344
'L' +
343345
coords.leftInnerExtent + ' ' + (coords.verticalFullExtent - offset) +
344346
'A' +
@@ -366,34 +368,35 @@ function createCircularClosedPathString(link) {
366368
(coords.leftLargeArcRadius - offset) + ' ' + (coords.leftLargeArcRadius - offset) + ' 0 0 0 ' +
367369
coords.leftInnerExtent + ' ' + (coords.verticalFullExtent + offset) +
368370
'L' +
369-
coords.rightInnerExtent + ' ' + (coords.verticalFullExtent + offset) +
371+
(coords.rightInnerExtent - arrowLen) + ' ' + (coords.verticalFullExtent + offset) +
370372
'A' +
371373
(coords.rightLargeArcRadius - offset) + ' ' + (coords.rightLargeArcRadius - offset) + ' 0 0 0 ' +
372-
(coords.rightFullExtent + offset) + ' ' + coords.verticalRightInnerExtent +
374+
(coords.rightFullExtent + offset - arrowLen) + ' ' + coords.verticalRightInnerExtent +
373375
'L' +
374-
(coords.rightFullExtent + offset) + ' ' + (coords.targetY - coords.rightSmallArcRadius) +
376+
(coords.rightFullExtent + offset - arrowLen) + ' ' + (coords.targetY - coords.rightSmallArcRadius) +
375377
'A' +
376378
(coords.rightLargeArcRadius - offset) + ' ' + (coords.rightSmallArcRadius - offset) + ' 0 0 0 ' +
377-
coords.rightInnerExtent + ' ' + (coords.targetY - offset) +
379+
(coords.rightInnerExtent - arrowLen) + ' ' + (coords.targetY - offset) +
378380
'L' +
379-
coords.targetX + ' ' + (coords.targetY - offset) +
381+
(coords.targetX - arrowLen) + ' ' + (coords.targetY - offset) +
382+
(arrowLen > 0 ? 'L' + coords.targetX + ' ' + (coords.targetY) : '') +
380383
'Z';
381384
} else {
382385
// Bottom path
383386
pathString =
384387
// start at the left of the target node
385388
'M ' +
386-
coords.targetX + ' ' + (coords.targetY - offset) + ' ' +
389+
(coords.targetX - arrowLen) + ' ' + (coords.targetY - offset) + ' ' +
387390
'L' +
388-
coords.rightInnerExtent + ' ' + (coords.targetY - offset) +
391+
(coords.rightInnerExtent - arrowLen) + ' ' + (coords.targetY - offset) +
389392
'A' +
390393
(coords.rightLargeArcRadius + offset) + ' ' + (coords.rightSmallArcRadius + offset) + ' 0 0 0 ' +
391-
(coords.rightFullExtent - offset) + ' ' + (coords.targetY + coords.rightSmallArcRadius) +
394+
(coords.rightFullExtent - offset - arrowLen) + ' ' + (coords.targetY + coords.rightSmallArcRadius) +
392395
'L' +
393-
(coords.rightFullExtent - offset) + ' ' + coords.verticalRightInnerExtent +
396+
(coords.rightFullExtent - offset - arrowLen) + ' ' + coords.verticalRightInnerExtent +
394397
'A' +
395398
(coords.rightLargeArcRadius + offset) + ' ' + (coords.rightLargeArcRadius + offset) + ' 0 0 0 ' +
396-
coords.rightInnerExtent + ' ' + (coords.verticalFullExtent + offset) +
399+
(coords.rightInnerExtent - arrowLen) + ' ' + (coords.verticalFullExtent + offset) +
397400
'L' +
398401
coords.leftInnerExtent + ' ' + (coords.verticalFullExtent + offset) +
399402
'A' +
@@ -421,17 +424,18 @@ function createCircularClosedPathString(link) {
421424
(coords.leftLargeArcRadius - offset) + ' ' + (coords.leftLargeArcRadius - offset) + ' 0 0 1 ' +
422425
coords.leftInnerExtent + ' ' + (coords.verticalFullExtent - offset) +
423426
'L' +
424-
coords.rightInnerExtent + ' ' + (coords.verticalFullExtent - offset) +
427+
(coords.rightInnerExtent - arrowLen) + ' ' + (coords.verticalFullExtent - offset) +
425428
'A' +
426429
(coords.rightLargeArcRadius - offset) + ' ' + (coords.rightLargeArcRadius - offset) + ' 0 0 1 ' +
427-
(coords.rightFullExtent + offset) + ' ' + coords.verticalRightInnerExtent +
430+
(coords.rightFullExtent + offset - arrowLen) + ' ' + coords.verticalRightInnerExtent +
428431
'L' +
429-
(coords.rightFullExtent + offset) + ' ' + (coords.targetY + coords.rightSmallArcRadius) +
432+
(coords.rightFullExtent + offset - arrowLen) + ' ' + (coords.targetY + coords.rightSmallArcRadius) +
430433
'A' +
431434
(coords.rightLargeArcRadius - offset) + ' ' + (coords.rightSmallArcRadius - offset) + ' 0 0 1 ' +
432-
coords.rightInnerExtent + ' ' + (coords.targetY + offset) +
435+
(coords.rightInnerExtent - arrowLen) + ' ' + (coords.targetY + offset) +
433436
'L' +
434-
coords.targetX + ' ' + (coords.targetY + offset) +
437+
(coords.targetX - arrowLen) + ' ' + (coords.targetY + offset) +
438+
(arrowLen > 0 ? 'L' + coords.targetX + ' ' + (coords.targetY) : '') +
435439
'Z';
436440
}
437441
return pathString;
@@ -440,27 +444,34 @@ function createCircularClosedPathString(link) {
440444
function linkPath() {
441445
var curvature = 0.5;
442446
function path(d) {
447+
var arrowLen = d.linkArrowLength;
443448
if(d.link.circular) {
444-
return createCircularClosedPathString(d.link);
449+
return createCircularClosedPathString(d.link, arrowLen);
445450
} else {
451+
var maxArrowLength = Math.abs((d.link.target.x0 - d.link.source.x1) / 2);
452+
if(arrowLen > maxArrowLength) {
453+
arrowLen = maxArrowLength;
454+
}
446455
var x0 = d.link.source.x1;
447-
var x1 = d.link.target.x0;
456+
var x1 = d.link.target.x0 - arrowLen;
448457
var xi = interpolateNumber(x0, x1);
449458
var x2 = xi(curvature);
450459
var x3 = xi(1 - curvature);
451460
var y0a = d.link.y0 - d.link.width / 2;
452461
var y0b = d.link.y0 + d.link.width / 2;
453462
var y1a = d.link.y1 - d.link.width / 2;
454463
var y1b = d.link.y1 + d.link.width / 2;
455-
return 'M' + x0 + ',' + y0a +
456-
'C' + x2 + ',' + y0a +
457-
' ' + x3 + ',' + y1a +
458-
' ' + x1 + ',' + y1a +
459-
'L' + x1 + ',' + y1b +
460-
'C' + x3 + ',' + y1b +
461-
' ' + x2 + ',' + y0b +
462-
' ' + x0 + ',' + y0b +
463-
'Z';
464+
var start = 'M' + x0 + ',' + y0a;
465+
var upperCurve = 'C' + x2 + ',' + y0a +
466+
' ' + x3 + ',' + y1a +
467+
' ' + x1 + ',' + y1a;
468+
var lowerCurve = 'C' + x3 + ',' + y1b +
469+
' ' + x2 + ',' + y0b +
470+
' ' + x0 + ',' + y0b;
471+
472+
var rightEnd = arrowLen > 0 ? 'L' + (x1 + arrowLen) + ',' + (y1a + d.link.width / 2) : '';
473+
rightEnd += 'L' + x1 + ',' + y1b;
474+
return start + upperCurve + rightEnd + lowerCurve + 'Z';
464475
}
465476
}
466477
return path;
Loading
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"data": [
3+
{
4+
"type": "sankey",
5+
"node": {
6+
"pad": 5,
7+
"label": ["0", "1", "2", "3", "4", "5", "6"]
8+
},
9+
"link": {
10+
"arrowlen": 20,
11+
"source": [
12+
0, 0, 1, 2, 5, 4, 3
13+
],
14+
"target": [
15+
5, 3, 4, 3, 0, 2, 2
16+
],
17+
"value": [
18+
1, 2, 1, 1, 1, 1, 1
19+
]
20+
}
21+
}],
22+
"layout": {
23+
"title": {"text": "Sankey with circular data and arrows"},
24+
"width": 800,
25+
"height": 800
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"data": [
3+
{
4+
"type": "sankey",
5+
"orientation": "v",
6+
"node": {
7+
"pad": 5,
8+
"label": ["0", "1", "2", "3", "4", "5", "6"]
9+
},
10+
"link": {
11+
"arrowlen": 20,
12+
"source": [
13+
0, 0, 1, 2, 5, 4, 3
14+
],
15+
"target": [
16+
5, 3, 4, 3, 0, 2, 2
17+
],
18+
"value": [
19+
1, 2, 1, 1, 1, 1, 1
20+
]
21+
}
22+
}],
23+
"layout": {
24+
"title": {"text": "Sankey with circular data and arrows"},
25+
"width": 800,
26+
"height": 800
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"data": [
3+
{
4+
"type": "sankey",
5+
"arrangement": "freeform",
6+
"node": {
7+
"label": ["0", "1", "2"],
8+
"x": [1, 2, 3],
9+
"y": [0,0,0]
10+
},
11+
"link": {
12+
"arrowlen": 40,
13+
"source": [
14+
0, 1, 2
15+
],
16+
"target": [
17+
1, 2, 0
18+
],
19+
"value": [
20+
10, 9, 1
21+
]
22+
}
23+
}],
24+
"layout": {
25+
"title": {"text": "Sankey with little space"},
26+
"width": 400,
27+
"height": 400
28+
}
29+
}

test/plot-schema.json

+7
Original file line numberDiff line numberDiff line change
@@ -42056,6 +42056,13 @@
4205642056
"valType": "number"
4205742057
},
4205842058
"link": {
42059+
"arrowlen": {
42060+
"description": "Sets the length (in px) of the links arrow, if 0 no arrow will be drawn.",
42061+
"dflt": 0,
42062+
"editType": "calc",
42063+
"min": 0,
42064+
"valType": "number"
42065+
},
4205942066
"color": {
4206042067
"arrayOk": true,
4206142068
"description": "Sets the `link` color. It can be a single value, or an array for specifying color for each `link`. If `link.color` is omitted, then by default, a translucent grey link will be used.",

0 commit comments

Comments
 (0)