diff --git a/draftlogs/7873_add.md b/draftlogs/7873_add.md new file mode 100644 index 00000000000..6875cfa3d43 --- /dev/null +++ b/draftlogs/7873_add.md @@ -0,0 +1 @@ + - Add `sort` option to Sankey nodes [[#7873](https://github.com/plotly/plotly.js/pull/7873)] diff --git a/src/traces/sankey/attributes.js b/src/traces/sankey/attributes.js index a2ee49f0083..c5bdc77508a 100644 --- a/src/traces/sankey/attributes.js +++ b/src/traces/sankey/attributes.js @@ -170,6 +170,16 @@ var attrs = (module.exports = overrideAll( dflt: 'justify', description: 'Sets the alignment method used to position the nodes along the horizontal axis.' }, + sort: { + valType: 'enumerated', + values: ['auto', 'input'], + dflt: 'auto', + description: [ + 'If the value is `auto` (the default), the vertical order of nodes will be determined automatically', + 'by the layout.', + 'If the value is `input`, the vertical order is kept the same as the order in the input node array' + ].join(' ') + }, description: 'The nodes of the Sankey plot.' }, diff --git a/src/traces/sankey/defaults.js b/src/traces/sankey/defaults.js index 4f6eb92d1c4..b6efaa55350 100644 --- a/src/traces/sankey/defaults.js +++ b/src/traces/sankey/defaults.js @@ -34,6 +34,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout handleHoverLabelDefaults(nodeIn, nodeOut, coerceNode, hoverlabelDefault); coerceNode('hovertemplate'); coerceNode('align'); + coerceNode('sort'); var colors = layout.colorway; diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index ab00a51223e..f98c8a3d931 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -40,6 +40,7 @@ function sankeyModel(layout, d, traceIndex) { right: d3Sankey.sankeyRight, center: d3Sankey.sankeyCenter }[trace.node.align]; + var input_sort = trace.node.sort === 'input'; var width = layout.width * (domain.x[1] - domain.x[0]); var height = layout.height * (domain.y[1] - domain.y[0]); @@ -48,6 +49,10 @@ function sankeyModel(layout, d, traceIndex) { var links = calcData._links; var circular = calcData.circular; + if(circular && input_sort) { + Lib.error('Circular Sankey diagrams do not support the "input" node.sort mode; falling back to the default sort.'); + } + // Select Sankey generator var sankey; if(circular) { @@ -70,6 +75,11 @@ function sankeyModel(layout, d, traceIndex) { .nodes(nodes) .links(links); + // d3-sankey-circular does not support the nodeSort method + if(!circular) { + sankey.nodeSort(input_sort ? null : undefined); + } + var graph = sankey(); if(sankey.nodePadding() < nodePad) { diff --git a/src/types/generated/schema.d.ts b/src/types/generated/schema.d.ts index 15fae6cbd87..9f88a45d590 100644 --- a/src/types/generated/schema.d.ts +++ b/src/types/generated/schema.d.ts @@ -6837,6 +6837,11 @@ export interface SankeyData { * Minimum: 0 */ pad?: number; + /** + * If the value is `auto` (the default), the vertical order of nodes will be determined automatically by the layout. If the value is `input`, the vertical order is kept the same as the order in the input node array + * @default 'auto' + */ + sort?: 'auto' | 'input'; /** * Sets the thickness (in px) of the `nodes`. * @default 20 diff --git a/test/image/baselines/sankey_auto_sort.png b/test/image/baselines/sankey_auto_sort.png new file mode 100644 index 00000000000..adf20ce19e3 Binary files /dev/null and b/test/image/baselines/sankey_auto_sort.png differ diff --git a/test/image/baselines/sankey_input_sort.png b/test/image/baselines/sankey_input_sort.png new file mode 100644 index 00000000000..6e9c96e78db Binary files /dev/null and b/test/image/baselines/sankey_input_sort.png differ diff --git a/test/image/mocks/sankey_auto_sort.json b/test/image/mocks/sankey_auto_sort.json new file mode 100644 index 00000000000..96f2fa202cf --- /dev/null +++ b/test/image/mocks/sankey_auto_sort.json @@ -0,0 +1,22 @@ +{ + "data": [ + { + "type": "sankey", + "node": { + "label": ["0", "1", "2", "3", "4", "5"], + "align": "center", + "sort": "auto" + }, + "link": { + "source": [0, 0, 0, 0, 1, 4], + "target": [1, 2, 3, 4, 5, 5], + "value": [1, 2, 1, 1, 1, 1] + } + } + ], + "layout": { + "title": { "text": "Sankey with automatic vertical node ordering" }, + "width": 800, + "height": 800 + } +} diff --git a/test/image/mocks/sankey_input_sort.json b/test/image/mocks/sankey_input_sort.json new file mode 100644 index 00000000000..f3cf9249ba0 --- /dev/null +++ b/test/image/mocks/sankey_input_sort.json @@ -0,0 +1,22 @@ +{ + "data": [ + { + "type": "sankey", + "node": { + "label": ["0", "1", "2", "3", "4", "5"], + "align": "center", + "sort": "input" + }, + "link": { + "source": [0, 0, 0, 0, 1, 4], + "target": [1, 2, 3, 4, 5, 5], + "value": [1, 2, 1, 1, 1, 1] + } + } + ], + "layout": { + "title": { "text": "Sankey with fixed vertical node ordering" }, + "width": 800, + "height": 800 + } +} diff --git a/test/jasmine/tests/sankey_test.js b/test/jasmine/tests/sankey_test.js index d9bd928148c..38a50e85eab 100644 --- a/test/jasmine/tests/sankey_test.js +++ b/test/jasmine/tests/sankey_test.js @@ -501,6 +501,27 @@ describe('sankey tests', function () { .then(done, done.fail); }); + it('falls back to the default sort for circular Sankey with node.sort set', function (done) { + var errors = []; + spyOn(Lib, 'error').and.callFake(function (msg) { + errors.push(msg); + }); + + var mockCircularCopy = Lib.extendDeep({}, mockCircular); + mockCircularCopy.data[0].node.sort = 'input'; + + Plotly.newPlot(gd, mockCircularCopy) + .then(function () { + // The plot renders successfully + expect(gd.calcdata[0][0].circular).toBe(true); + expect(d3SelectAll('.sankey .node-rect').size()).toBeGreaterThan(0); + + // An error is logged about the fallback + expect(errors.length).toBe(1); + }) + .then(done, done.fail); + }); + it('can create groups, restyle groups and properly update DOM', function (done) { var mockCircularCopy = Lib.extendDeep({}, mockCircular); var firstGroup = [ diff --git a/test/plot-schema.json b/test/plot-schema.json index 6156b2ef890..d62b0ca6a61 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -50746,6 +50746,16 @@ "valType": "number" }, "role": "object", + "sort": { + "description": "If the value is `auto` (the default), the vertical order of nodes will be determined automatically by the layout. If the value is `input`, the vertical order is kept the same as the order in the input node array", + "dflt": "auto", + "editType": "calc", + "valType": "enumerated", + "values": [ + "auto", + "input" + ] + }, "thickness": { "arrayOk": false, "description": "Sets the thickness (in px) of the `nodes`.",