From d3747be6569cd903f80d81a6422984e50cf8404d Mon Sep 17 00:00:00 2001 From: giuseppe straziota Date: Fri, 15 Nov 2024 14:42:41 +0100 Subject: [PATCH 1/3] Fix: Improvements to avoid distortions in sankey diagram links loops. Issue #7132 --- src/traces/sankey/render.js | 265 +++++++++++++++++++++--------------- 1 file changed, 156 insertions(+), 109 deletions(-) diff --git a/src/traces/sankey/render.js b/src/traces/sankey/render.js index adb0b100f26..a07d18a9c9e 100644 --- a/src/traces/sankey/render.js +++ b/src/traces/sankey/render.js @@ -335,118 +335,165 @@ function createCircularClosedPathString(link, arrowLen) { var pathString = ''; var offset = link.width / 2; var coords = link.circularPathData; - if(link.circularLinkType === 'top') { - // Top path + var isSourceBeforeTarget = coords.sourceX + coords.verticalBuffer < coords.targetX; + var isPathOverlapped = (coords.rightFullExtent - coords.rightLargeArcRadius - arrowLen) <= (coords.leftFullExtent - offset) + var diff = Math.abs(coords.rightFullExtent- coords.leftFullExtent - offset) < offset ; + if (link.circularLinkType === 'top') { pathString = - // start at the left of the target node - 'M ' + - (coords.targetX - arrowLen) + ' ' + (coords.targetY + offset) + ' ' + - 'L' + - (coords.rightInnerExtent - arrowLen) + ' ' + (coords.targetY + offset) + - 'A' + - (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightSmallArcRadius + offset) + ' 0 0 1 ' + - (coords.rightFullExtent - offset - arrowLen) + ' ' + (coords.targetY - coords.rightSmallArcRadius) + - 'L' + - (coords.rightFullExtent - offset - arrowLen) + ' ' + coords.verticalRightInnerExtent + - 'A' + - (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightLargeArcRadius + offset) + ' 0 0 1 ' + - (coords.rightInnerExtent - arrowLen) + ' ' + (coords.verticalFullExtent - offset) + - 'L' + - coords.leftInnerExtent + ' ' + (coords.verticalFullExtent - offset) + - 'A' + - (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftLargeArcRadius + offset) + ' 0 0 1 ' + - (coords.leftFullExtent + offset) + ' ' + coords.verticalLeftInnerExtent + - 'L' + - (coords.leftFullExtent + offset) + ' ' + (coords.sourceY - coords.leftSmallArcRadius) + - 'A' + - (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftSmallArcRadius + offset) + ' 0 0 1 ' + - coords.leftInnerExtent + ' ' + (coords.sourceY + offset) + - 'L' + - coords.sourceX + ' ' + (coords.sourceY + offset) + - - // Walking back - 'L' + - coords.sourceX + ' ' + (coords.sourceY - offset) + - 'L' + - coords.leftInnerExtent + ' ' + (coords.sourceY - offset) + - 'A' + - (coords.leftLargeArcRadius - offset) + ' ' + (coords.leftSmallArcRadius - offset) + ' 0 0 0 ' + - (coords.leftFullExtent - offset) + ' ' + (coords.sourceY - coords.leftSmallArcRadius) + - 'L' + - (coords.leftFullExtent - offset) + ' ' + coords.verticalLeftInnerExtent + - 'A' + - (coords.leftLargeArcRadius - offset) + ' ' + (coords.leftLargeArcRadius - offset) + ' 0 0 0 ' + - coords.leftInnerExtent + ' ' + (coords.verticalFullExtent + offset) + - 'L' + - (coords.rightInnerExtent - arrowLen) + ' ' + (coords.verticalFullExtent + offset) + - 'A' + - (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightLargeArcRadius - offset) + ' 0 0 0 ' + - (coords.rightFullExtent + offset - arrowLen) + ' ' + coords.verticalRightInnerExtent + - 'L' + - (coords.rightFullExtent + offset - arrowLen) + ' ' + (coords.targetY - coords.rightSmallArcRadius) + - 'A' + - (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightSmallArcRadius - offset) + ' 0 0 0 ' + - (coords.rightInnerExtent - arrowLen) + ' ' + (coords.targetY - offset) + - 'L' + - (coords.targetX - arrowLen) + ' ' + (coords.targetY - offset) + - (arrowLen > 0 ? 'L' + coords.targetX + ' ' + (coords.targetY) : '') + - 'Z'; + // start at the left of the target node + 'M ' + + (coords.targetX - arrowLen) + ' ' + (coords.targetY + offset) + ' ' + + 'L ' + + (coords.rightInnerExtent - arrowLen) + ' ' + (coords.targetY + offset) + + 'A ' + + (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightSmallArcRadius + offset) + ' 0 0 1 ' + + (coords.rightFullExtent - offset - arrowLen) + ' ' + (coords.targetY - coords.rightSmallArcRadius) + + 'L ' + (coords.rightFullExtent - offset - arrowLen) + ' ' + coords.verticalRightInnerExtent; + + if (isSourceBeforeTarget && isPathOverlapped) { + pathString += ' A ' + + (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightLargeArcRadius + offset) + ' 0 0 1 ' + + (coords.rightFullExtent + offset - arrowLen - (coords.rightLargeArcRadius - offset)) + ' ' + + (coords.verticalRightInnerExtent - (coords.rightLargeArcRadius + offset)) + + ' L ' + + (coords.rightFullExtent + offset - (coords.rightLargeArcRadius - offset) - arrowLen) + ' ' + + (coords.verticalRightInnerExtent - (coords.rightLargeArcRadius + offset)) + + ' A ' + + (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftLargeArcRadius + offset) + ' 0 0 1 ' + + (coords.leftFullExtent + offset) + ' ' + coords.verticalRightInnerExtent; + } else if (isSourceBeforeTarget) { + pathString += ' A ' + + (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightLargeArcRadius - offset) + ' 0 0 0 ' + + (coords.rightFullExtent - offset - arrowLen - (coords.rightLargeArcRadius - offset)) + ' ' + + (coords.verticalRightInnerExtent - (coords.rightLargeArcRadius - offset)) + + ' L ' + + (coords.leftFullExtent + offset + (coords.rightLargeArcRadius - offset)) + ' ' + + (coords.verticalRightInnerExtent - (coords.rightLargeArcRadius - offset)) + + ' A ' + + (coords.leftLargeArcRadius - offset) + ' ' + (coords.leftLargeArcRadius - offset) + ' 0 0 0 ' + + (coords.leftFullExtent + offset) + ' ' + coords.verticalLeftInnerExtent; + } else { + pathString += ' A ' + + (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightLargeArcRadius + offset) + ' 0 0 1 ' + + (coords.rightInnerExtent - arrowLen) + ' ' + (coords.verticalFullExtent - offset) + + ' L ' + + coords.leftInnerExtent + ' ' + (coords.verticalFullExtent - offset) + + ' A ' + + (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftLargeArcRadius + offset) + ' 0 0 1 ' + + (coords.leftFullExtent + offset) + ' ' + coords.verticalLeftInnerExtent; + } + + pathString += ' L ' + + (coords.leftFullExtent + offset) + ' ' + (coords.sourceY - coords.leftSmallArcRadius) + + ' A ' + + (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftSmallArcRadius + offset) + ' 0 0 1 ' + + coords.leftInnerExtent + ' ' + (coords.sourceY + offset) + + ' L ' + + coords.sourceX + ' ' + (coords.sourceY + offset) + + + // Walking back + ' L ' + + coords.sourceX + ' ' + (coords.sourceY - offset) + + ' L ' + + coords.leftInnerExtent + ' ' + (coords.sourceY - offset) + + ' A ' + + (coords.leftLargeArcRadius - offset) + ' ' + (coords.leftSmallArcRadius - offset) + ' 0 0 0 ' + + (coords.leftFullExtent - offset) + ' ' + (coords.sourceY - coords.leftSmallArcRadius) + + ' L ' + + (coords.leftFullExtent - offset) + ' ' + coords.verticalLeftInnerExtent; + + if (isSourceBeforeTarget && isPathOverlapped) { + pathString += ' A ' + + (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftSmallArcRadius + offset) + ' 0 0 0 ' + + (coords.leftFullExtent - offset) + ' ' + (coords.verticalFullExtent + offset) + + 'L' + (coords.rightFullExtent + offset - arrowLen) + ' ' + (coords.verticalFullExtent + offset) + + ' A ' + + (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftSmallArcRadius + offset) + ' 0 0 0 ' + + (coords.rightFullExtent + offset - arrowLen) + ' ' + coords.verticalRightInnerExtent; + } else if (isSourceBeforeTarget) { + pathString += ' A ' + + (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftSmallArcRadius + offset) + ' 0 0 1 ' + + (coords.leftFullExtent + offset) + ' ' + (coords.verticalFullExtent - offset) + + ' L ' + + (coords.rightFullExtent - offset - arrowLen) + ' ' + (coords.verticalFullExtent - offset) + + ' A ' + + (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftSmallArcRadius + offset) + ' 0 0 1 ' + + (coords.rightFullExtent + offset - arrowLen) + ' ' + coords.verticalRightInnerExtent; + } else { + pathString += ' A ' + + (coords.leftLargeArcRadius - offset) + ' ' + (coords.leftLargeArcRadius - offset) + ' 0 0 0 ' + + coords.leftInnerExtent + ' ' + (coords.verticalFullExtent + offset) + + ' L ' + + (coords.rightInnerExtent - arrowLen) + ' ' + (coords.verticalFullExtent + offset) + + ' A ' + + (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightLargeArcRadius - offset) + ' 0 0 0 ' + + (coords.rightFullExtent + offset - arrowLen) + ' ' + coords.verticalRightInnerExtent; + } + + pathString += ' L ' + + (coords.rightFullExtent + offset - arrowLen) + ' ' + (coords.targetY - coords.rightSmallArcRadius) + + ' A ' + + (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightSmallArcRadius - offset) + ' 0 0 0 ' + + (coords.rightInnerExtent - arrowLen) + ' ' + (coords.targetY - offset) + + ' L ' + + (coords.targetX - arrowLen) + ' ' + (coords.targetY - offset) + + (arrowLen > 0 ? ' L ' + coords.targetX + ' ' + coords.targetY : '') + + 'Z'; } else { - // Bottom path pathString = - // start at the left of the target node - 'M ' + - (coords.targetX - arrowLen) + ' ' + (coords.targetY - offset) + ' ' + - 'L' + - (coords.rightInnerExtent - arrowLen) + ' ' + (coords.targetY - offset) + - 'A' + - (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightSmallArcRadius + offset) + ' 0 0 0 ' + - (coords.rightFullExtent - offset - arrowLen) + ' ' + (coords.targetY + coords.rightSmallArcRadius) + - 'L' + - (coords.rightFullExtent - offset - arrowLen) + ' ' + coords.verticalRightInnerExtent + - 'A' + - (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightLargeArcRadius + offset) + ' 0 0 0 ' + - (coords.rightInnerExtent - arrowLen) + ' ' + (coords.verticalFullExtent + offset) + - 'L' + - coords.leftInnerExtent + ' ' + (coords.verticalFullExtent + offset) + - 'A' + - (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftLargeArcRadius + offset) + ' 0 0 0 ' + - (coords.leftFullExtent + offset) + ' ' + coords.verticalLeftInnerExtent + - 'L' + - (coords.leftFullExtent + offset) + ' ' + (coords.sourceY + coords.leftSmallArcRadius) + - 'A' + - (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftSmallArcRadius + offset) + ' 0 0 0 ' + - coords.leftInnerExtent + ' ' + (coords.sourceY - offset) + - 'L' + - coords.sourceX + ' ' + (coords.sourceY - offset) + - - // Walking back - 'L' + - coords.sourceX + ' ' + (coords.sourceY + offset) + - 'L' + - coords.leftInnerExtent + ' ' + (coords.sourceY + offset) + - 'A' + - (coords.leftLargeArcRadius - offset) + ' ' + (coords.leftSmallArcRadius - offset) + ' 0 0 1 ' + - (coords.leftFullExtent - offset) + ' ' + (coords.sourceY + coords.leftSmallArcRadius) + - 'L' + - (coords.leftFullExtent - offset) + ' ' + coords.verticalLeftInnerExtent + - 'A' + - (coords.leftLargeArcRadius - offset) + ' ' + (coords.leftLargeArcRadius - offset) + ' 0 0 1 ' + - coords.leftInnerExtent + ' ' + (coords.verticalFullExtent - offset) + - 'L' + - (coords.rightInnerExtent - arrowLen) + ' ' + (coords.verticalFullExtent - offset) + - 'A' + - (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightLargeArcRadius - offset) + ' 0 0 1 ' + - (coords.rightFullExtent + offset - arrowLen) + ' ' + coords.verticalRightInnerExtent + - 'L' + - (coords.rightFullExtent + offset - arrowLen) + ' ' + (coords.targetY + coords.rightSmallArcRadius) + - 'A' + - (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightSmallArcRadius - offset) + ' 0 0 1 ' + - (coords.rightInnerExtent - arrowLen) + ' ' + (coords.targetY + offset) + - 'L' + - (coords.targetX - arrowLen) + ' ' + (coords.targetY + offset) + - (arrowLen > 0 ? 'L' + coords.targetX + ' ' + (coords.targetY) : '') + - 'Z'; + 'M ' + (coords.targetX - arrowLen) + ' ' + (coords.targetY - offset) + ' ' + + ' L ' + (coords.rightInnerExtent - arrowLen) + ' ' + (coords.targetY - offset) + + ' A ' + (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightSmallArcRadius + offset) + ' 0 0 0 ' + (coords.rightFullExtent - offset - arrowLen) + ' ' + (coords.targetY + coords.rightSmallArcRadius) + + ' L ' + (coords.rightFullExtent - offset - arrowLen) + ' ' + coords.verticalRightInnerExtent; + + if (isSourceBeforeTarget && isPathOverlapped) { + pathString += ' A ' + (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightLargeArcRadius + offset) + ' 0 0 0 ' + + (coords.rightInnerExtent - offset - arrowLen) + ' ' + (coords.verticalFullExtent + offset) + + ' L ' + (coords.rightFullExtent + offset - arrowLen - (coords.rightLargeArcRadius - offset)) + ' ' + (coords.verticalFullExtent + offset) + + ' A ' + (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightLargeArcRadius + offset) + ' 0 0 0 ' + + (coords.leftFullExtent + offset) + ' ' + coords.verticalLeftInnerExtent; + } else if (isSourceBeforeTarget) { + pathString += ' A ' + (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightSmallArcRadius - offset) + ' 0 0 1 ' + + (coords.rightFullExtent - arrowLen - offset - (coords.rightLargeArcRadius - offset)) + ' ' + (coords.verticalFullExtent - offset) + + ' L ' + (coords.leftFullExtent + offset + (coords.rightLargeArcRadius - offset)) + ' ' + (coords.verticalFullExtent - offset) + + ' A ' + (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightSmallArcRadius - offset) + ' 0 0 1 ' + + (coords.leftFullExtent + offset) + ' ' + coords.verticalLeftInnerExtent; + } else { + pathString += ' A ' + (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightLargeArcRadius + offset) + ' 0 0 0 ' + (coords.rightInnerExtent - arrowLen) + ' ' + (coords.verticalFullExtent + offset) + + ' L ' + coords.leftInnerExtent + ' ' + (coords.verticalFullExtent + offset) + + ' A ' + (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftLargeArcRadius + offset) + ' 0 0 0 ' + (coords.leftFullExtent + offset) + ' ' + coords.verticalLeftInnerExtent; + } + + pathString += ' L ' + (coords.leftFullExtent + offset) + ' ' + (coords.sourceY + coords.leftSmallArcRadius) + + ' A ' + (coords.leftLargeArcRadius + offset) + ' ' + (coords.leftSmallArcRadius + offset) + ' 0 0 0 ' + coords.leftInnerExtent + ' ' + (coords.sourceY - offset) + + ' L ' + coords.sourceX + ' ' + (coords.sourceY - offset) + + + // Walking back + ' L ' + coords.sourceX + ' ' + (coords.sourceY + offset) + + ' L ' + coords.leftInnerExtent + ' ' + (coords.sourceY + offset) + + ' A ' + (coords.leftLargeArcRadius - offset) + ' ' + (coords.leftSmallArcRadius - offset) + ' 0 0 1 ' + (coords.leftFullExtent - offset) + ' ' + (coords.sourceY + coords.leftSmallArcRadius) + + ' L ' + (coords.leftFullExtent - offset) + ' ' + coords.verticalLeftInnerExtent; + + if (isSourceBeforeTarget && isPathOverlapped) { + pathString += + ' A ' + (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightSmallArcRadius - offset) + ' 0 0 1 ' + + (coords.leftFullExtent - offset - (coords.rightLargeArcRadius - offset)) + ' ' + (coords.verticalFullExtent - offset) + + ' L ' + (coords.rightFullExtent + offset - arrowLen + (coords.rightLargeArcRadius - offset)) + ' ' + (coords.verticalFullExtent - offset) + + ' A ' + (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightSmallArcRadius - offset) + ' 0 0 1 ' + + (coords.rightFullExtent + offset - arrowLen) + ' ' + coords.verticalRightInnerExtent; + } else if (isSourceBeforeTarget) { + pathString += ' A ' + (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightLargeArcRadius + offset) + ' 0 0 0 ' + (coords.leftFullExtent + offset) + ' ' + (coords.verticalFullExtent + offset) + + ' L ' + (coords.rightFullExtent - arrowLen - offset) + ' ' + (coords.verticalFullExtent + offset) + + ' A ' + (coords.rightLargeArcRadius + offset) + ' ' + (coords.rightLargeArcRadius + offset) + ' 0 0 0 ' + (coords.rightFullExtent + offset - arrowLen) + ' ' + coords.verticalRightInnerExtent; + } else { + pathString += ' A ' + (coords.leftLargeArcRadius - offset) + ' ' + (coords.leftLargeArcRadius - offset) + ' 0 0 1 ' + coords.leftInnerExtent + ' ' + (coords.verticalFullExtent - offset) + + ' L ' + (coords.rightInnerExtent - arrowLen) + ' ' + (coords.verticalFullExtent - offset) + + ' A ' + (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightLargeArcRadius - offset) + ' 0 0 1 ' + (coords.rightFullExtent + offset - arrowLen) + ' ' + coords.verticalRightInnerExtent; + } + + pathString += ' L ' + (coords.rightFullExtent + offset - arrowLen) + ' ' + (coords.targetY + coords.rightSmallArcRadius) + + ' A ' + (coords.rightLargeArcRadius - offset) + ' ' + (coords.rightSmallArcRadius - offset) + ' 0 0 1 ' + (coords.rightInnerExtent - arrowLen) + ' ' + (coords.targetY + offset) + + ' L ' + (coords.targetX - arrowLen) + ' ' + (coords.targetY + offset) + (arrowLen > 0 ? ' L ' + coords.targetX + ' ' + coords.targetY : '') + 'Z'; } return pathString; } From 869548cac513992596ce6a160dbb3975a4e2fc6a Mon Sep 17 00:00:00 2001 From: giuseppe straziota Date: Fri, 15 Nov 2024 16:11:01 +0100 Subject: [PATCH 2/3] Chore: add changelog entry for PR #7272 --- draftlogs/7272_fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/7272_fix.md diff --git a/draftlogs/7272_fix.md b/draftlogs/7272_fix.md new file mode 100644 index 00000000000..f6f73172951 --- /dev/null +++ b/draftlogs/7272_fix.md @@ -0,0 +1 @@ +- Fix: Avoid distortions in sankey diagram links loops [[#7272](https://github.com/plotly/plotly.js/pull/7272)] \ No newline at end of file From 9898921e0d3b0b52583bbf97d6e914586008e601 Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi <33888540+archmoj@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:59:17 -0500 Subject: [PATCH 3/3] Update draftlogs/7272_fix.md --- draftlogs/7272_fix.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/draftlogs/7272_fix.md b/draftlogs/7272_fix.md index f6f73172951..0fa68b50a3c 100644 --- a/draftlogs/7272_fix.md +++ b/draftlogs/7272_fix.md @@ -1 +1,2 @@ -- Fix: Avoid distortions in sankey diagram links loops [[#7272](https://github.com/plotly/plotly.js/pull/7272)] \ No newline at end of file +- Fix distortions in sankey diagram links loops [[#7272](https://github.com/plotly/plotly.js/pull/7272)], + with thanks to @giuseppe-straziota for the contribution! \ No newline at end of file