diff --git a/draftlogs/7725_fix.md b/draftlogs/7725_fix.md new file mode 100644 index 00000000000..45b670d124e --- /dev/null +++ b/draftlogs/7725_fix.md @@ -0,0 +1 @@ + - Fix sankey nodes being clipped at the bottom edge when using user-positioned nodes (`node.x`/`node.y`) near `y=1.0` or with `arrangement="snap"` collision resolution [[#7725](https://github.com/plotly/plotly.js/pull/7725)] diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index a07d18a9c9e..b1e2ff3f54b 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -200,6 +200,17 @@ function sankeyModel(layout, d, traceIndex) { } y = node.y1 + nodePad; } + // If the last node extends past the bottom, shift the whole column up + if(n > 0) { + var lastNode = nodes[n - 1]; + if(lastNode.y1 > height) { + dy = lastNode.y1 - height; + for(i = 0; i < n; ++i) { + nodes[i].y0 -= dy; + nodes[i].y1 -= dy; + } + } + } }); } @@ -251,8 +262,12 @@ function sankeyModel(layout, d, traceIndex) { graph.nodes[i].x1 = pos[0] + nodeThickness / 2; var nodeHeight = graph.nodes[i].y1 - graph.nodes[i].y0; - graph.nodes[i].y0 = pos[1] - nodeHeight / 2; - graph.nodes[i].y1 = pos[1] + nodeHeight / 2; + var yCenter = pos[1]; + // Clamp so node doesn't extend past bottom or top + yCenter = Math.max(yCenter, nodeHeight / 2); + yCenter = Math.min(yCenter, height - nodeHeight / 2); + graph.nodes[i].y0 = yCenter - nodeHeight / 2; + graph.nodes[i].y1 = yCenter + nodeHeight / 2; } } if(trace.arrangement === 'snap') { diff --git a/test/image/mocks/sankey_x_y_bottom_clipping.json b/test/image/mocks/sankey_x_y_bottom_clipping.json new file mode 100644 index 00000000000..bbc2f58bd74 --- /dev/null +++ b/test/image/mocks/sankey_x_y_bottom_clipping.json @@ -0,0 +1,23 @@ +{ + "data": [ + { + "type": "sankey", + "arrangement": "snap", + "node": { + "label": ["A", "B", "C", "D"], + "x": [0.1, 0.1, 0.5, 0.9], + "y": [0.5, 0.95, 0.95, 0.95] + }, + "link": { + "source": [0, 0, 1, 2], + "target": [1, 2, 3, 3], + "value": [5, 3, 5, 3] + } + } + ], + "layout": { + "title": { "text": "Sankey with bottom-edge nodes" }, + "width": 600, + "height": 400 + } +} diff --git a/test/jasmine/tests/sankey_test.js b/test/jasmine/tests/sankey_test.js index 76fa0e9f27c..1dece998400 100644 --- a/test/jasmine/tests/sankey_test.js +++ b/test/jasmine/tests/sankey_test.js @@ -578,6 +578,33 @@ describe('sankey tests', function() { .then(done, done.fail); }); + it('prevents nodes from being clipped at the bottom edge', function(done) { + var mockBottom = require('../../image/mocks/sankey_x_y_bottom_clipping.json'); + var mockCopy = Lib.extendDeep({}, mockBottom); + + Plotly.newPlot(gd, mockCopy) + .then(function() { + var nodeRects = document.querySelectorAll('.sankey-node .node-rect'); + var sankeyLayer = document.querySelector('.sankey'); + var sankeyRect = sankeyLayer.getBoundingClientRect(); + + for(var i = 0; i < nodeRects.length; i++) { + var rect = nodeRects[i].getBoundingClientRect(); + // Every node's bottom edge must be within the sankey area + expect(rect.bottom).not.toBeGreaterThan( + sankeyRect.bottom + 1, // 1px tolerance + 'node ' + i + ' extends past the bottom edge' + ); + // Every node's top edge must be within the sankey area + expect(rect.top).not.toBeLessThan( + sankeyRect.top - 1, // 1px tolerance + 'node ' + i + ' extends past the top edge' + ); + } + }) + .then(done, done.fail); + }); + it('resets each subplot to its initial view (ie. x, y groups) via modebar button', function(done) { var mockCopy = Lib.extendDeep({}, require('../../image/mocks/sankey_subplots_circular'));