Repository: holoviz/holoviews

Issue:
Update Sankey algorithm to match latest d3-sankey
Michael Bostock has made some major improvements to the overlap avoidance algorithms in d3-sankey over the last week. Since the code in holoviews is a fairly straightforward Python port of that code it should be reasonably straightforward to update our code to match. [Here](https://github.com/d3/d3-sankey/compare/v0.7.1...master)'s a link to the diff for all the updates.


Discussion:
I'd like to work on this. Are there any good examples where the improvements after the update can be observed?
@philippjfr @jbednar 

Hi! I am almost done with the implementation, just want to double check if there is still interest in it.
@Yura52 Amazing! We are definitely still interested. Would love to see a PR.
Sorry for the delay. The implementation is finished. However, I [cannot find data](https://github.com/holoviz/holoviews/issues/3548#issuecomment-718082283) where improvements can be observed. Do you have any examples?

The screenshot is based on [this](https://holoviews.org/gallery/demos/bokeh/energy_sankey.html#demos-bokeh-gallery-energy-sankey) example. 1.13.5. on the left, my implementation on the right. The first row shows [nodes](https://github.com/holoviz/holoviews/blob/a684a09bfab538e798351dd21db941903833fb77/holoviews/element/sankey.py#L66), the second row shows [edges](https://github.com/holoviz/holoviews/blob/a684a09bfab538e798351dd21db941903833fb77/holoviews/element/sankey.py#L67). The "problem" is that the final sankey plot is absolutely the same in both cases and looks like in the gallery example.

So it is either my mistake or "bad" data. The only thing that my implementation changes is how the graph is filled (lines 48-52 [here](https://github.com/holoviz/holoviews/blob/a684a09bfab538e798351dd21db941903833fb77/holoviews/element/sankey.py#L48)), maybe this is not enough.

<img width="799" alt="Screenshot 2020-11-17 at 23 23 18" src="https://user-images.githubusercontent.com/8861742/99445307-6da9a180-292e-11eb-87ac-a0d8cac9944b.png">

@philippjfr I can create a PR, but I would prefer to make sure that the results I see are expected. Any additional infomation will be helpful.
Sorry for not getting back to you before. I'd say open the PR and then I can play around with it and give feedback.

Resolving patch:
diff --git a/holoviews/element/sankey.py b/holoviews/element/sankey.py
--- a/holoviews/element/sankey.py
+++ b/holoviews/element/sankey.py
@@ -1,4 +1,4 @@
-from collections import Counter
+import math
 from functools import cmp_to_key
 from itertools import cycle
 
@@ -8,17 +8,23 @@
 from ..core.dimension import Dimension
 from ..core.data import Dataset
 from ..core.operation import Operation
-from ..core.util import OrderedDict, get_param_values, unique_array
+from ..core.util import get_param_values, unique_array
 from .graphs import Graph, Nodes, EdgePaths
 from .util import quadratic_bezier
 
 
+_Y_N_DECIMAL_DIGITS = 6
+_Y_EPS = 10 ** -_Y_N_DECIMAL_DIGITS
+
+
 class _layout_sankey(Operation):
     """
     Computes a Sankey diagram from a Graph element for internal use in
     the Sankey element constructor.
 
     Adapted from d3-sankey under BSD-3 license.
+
+    Source: https://github.com/d3/d3-sankey/tree/v0.12.3
     """
 
     bounds = param.NumericTuple(default=(0, 0, 1000, 500))
@@ -46,6 +52,7 @@ def layout(self, element, **params):
         self.computeNodeLinks(element, graph)
         self.computeNodeValues(graph)
         self.computeNodeDepths(graph)
+        self.computeNodeHeights(graph)
         self.computeNodeBreadths(graph)
         self.computeLinkBreadths(graph)
         paths = self.computePaths(graph)
@@ -65,60 +72,6 @@ def layout(self, element, **params):
         edges = element.edge_type(paths)
         return nodes, edges, graph
 
-
-    def computePaths(self, graph):
-        paths = []
-        for link in graph['links']:
-            source, target = link['source'], link['target']
-            x0, y0 = source['x1'], link['y0']
-            x1, y1 = target['x0'], link['y1']
-            start = np.array([(x0, link['width']+y0),
-                              (x0, y0)])
-            src = (x0, y0)
-            ctr1 = ((x0+x1)/2., y0)
-            ctr2 = ((x0+x1)/2., y1)
-            tgt = (x1, y1)
-            bottom = quadratic_bezier(src, tgt, ctr1, ctr2)
-            mid = np.array([(x1, y1),
-                            (x1, y1+link['width'])])
-
-            xmid = (x0+x1)/2.
-            y0 = y0+link['width']
-            y1 = y1+link['width']
-            src = (x1, y1)
-            ctr1 = (xmid, y1)
-            ctr2 = (xmid, y0)
-            tgt = (x0, y0)
-            top = quadratic_bezier(src, tgt, ctr1, ctr2)
-            spline = np.concatenate([start, bottom, mid, top])
-            paths.append(spline)
-        return paths
-
-
-    @classmethod
-    def weightedSource(cls, link):
-        return cls.nodeCenter(link['source']) * link['value']
-
-    @classmethod
-    def weightedTarget(cls, link):
-        return cls.nodeCenter(link['target']) * link['value']
-
-    @classmethod
-    def nodeCenter(cls, node):
-        return (node['y0'] + node['y1']) / 2
-
-    @classmethod
-    def ascendingBreadth(cls, a, b):
-        return int(a['y0'] - b['y0'])
-
-    @classmethod
-    def ascendingSourceBreadth(cls, a, b):
-        return cls.ascendingBreadth(a['source'], b['source']) | a['index'] - b['index']
-
-    @classmethod
-    def ascendingTargetBreadth(cls, a, b):
-        return cls.ascendingBreadth(a['target'], b['target']) | a['index'] - b['index']
-
     @classmethod
     def computeNodeLinks(cls, element, graph):
         """
@@ -155,13 +108,8 @@ def computeNodeValues(cls, graph):
             target_val = np.sum([l['value'] for l in node['targetLinks']])
             node['value'] = max([source_val, target_val])
 
-    def computeNodeDepths(self, graph):
-        """
-        Iteratively assign the depth (x-position) for each node.
-        Nodes are assigned the maximum depth of incoming neighbors plus one;
-        nodes with no incoming links are assigned depth zero, while
-        nodes with no outgoing links are assigned the maximum depth.
-        """
+    @classmethod
+    def computeNodeDepths(cls, graph):
         nodes = graph['nodes']
         depth = 0
         while nodes:
@@ -169,126 +117,234 @@ def computeNodeDepths(self, graph):
             for node in nodes:
                 node['depth'] = depth
                 for link in node['sourceLinks']:
-                    if not any(link['target'] is node for node in next_nodes):
-                        next_nodes.append(link['target'])
+                    next_nodes.append(link['target'])
             nodes = next_nodes
             depth += 1
-            if depth > 10000:
+            if depth > len(graph['nodes']):
                 raise RecursionError('Sankey diagrams only support acyclic graphs.')
+        return depth
 
+    @classmethod
+    def computeNodeHeights(cls, graph):
         nodes = graph['nodes']
-        depth = 0
+        height = 0
         while nodes:
             next_nodes = []
             for node in nodes:
-                node['height'] = depth
+                node['height'] = height
                 for link in node['targetLinks']:
-                    if not any(link['source'] is node for node in next_nodes):
-                        next_nodes.append(link['source'])
+                    next_nodes.append(link['source'])
             nodes = next_nodes
-            depth += 1
-            if depth > 10000:
+            height += 1
+            if height > len(graph['nodes']):
                 raise RecursionError('Sankey diagrams only support acyclic graphs.')
+        return height
 
-        x0, _, x1, _ = self.p.bounds
+    def computeNodeColumns(self, graph):
+        depth_upper_bound = max(x['depth'] for x in graph['nodes']) + 1
+        x0, x1 = self.p.bounds[0], self.p.bounds[2]
         dx = self.p.node_width
-        kx = (x1 - x0 - dx) / (depth - 1)
+        kx = (x1 - x0 - dx) / (depth_upper_bound - 1)
+        columns = [[] for _ in range(depth_upper_bound)]
         for node in graph['nodes']:
-            d  = node['depth'] if node['sourceLinks'] else depth - 1
-            node['x0'] = x0 + max([0, min([depth-1, np.floor(d)]) * kx])
+            node['column'] = max(
+                0,
+                min(
+                    depth_upper_bound - 1,
+                    math.floor(
+                        node['depth']
+                        if node['sourceLinks']
+                        else depth_upper_bound - 1
+                    )
+                )
+            )
+            node['x0'] = x0 + node['column'] * kx
             node['x1'] = node['x0'] + dx
+            columns[node['column']].append(node)
+        return columns
 
-    def computeNodeBreadths(self, graph):
-        node_map = OrderedDict()
-        depths = Counter()
-        for n in graph['nodes']:
-            if n['x0'] not in node_map:
-                node_map[n['x0']] = []
-            node_map[n['x0']].append(n)
-            depths[n['depth']] += 1
+    @classmethod
+    def ascendingBreadth(cls, a, b):
+        return int(a['y0'] - b['y0'])
+
+    @classmethod
+    def ascendingSourceBreadth(cls, a, b):
+        return (
+            (
+                cls.ascendingBreadth(a['source'], b['source'])
+                if 'y0' in a['source'] and 'y0' in b['source']
+                else None
+            )
+            or a['index'] - b['index']
+        )
+
+    @classmethod
+    def ascendingTargetBreadth(cls, a, b):
+        return (
+            (
+                cls.ascendingBreadth(a['target'], b['target'])
+                if 'y0' in a['target'] and 'y0' in b['target']
+                else None
+            )
+            or a['index'] - b['index']
+        )
 
+    @classmethod
+    def reorderLinks(cls, nodes):
+        for x in nodes:
+            x['sourceLinks'].sort(key=cmp_to_key(cls.ascendingTargetBreadth))
+            x['targetLinks'].sort(key=cmp_to_key(cls.ascendingSourceBreadth))
+
+    def initializeNodeBreadths(self, columns, py):
         _, y0, _, y1 = self.p.bounds
-        py = self.p.node_padding
-        if py is None:
-            max_depth = max(depths.values()) - 1 if depths else 1
-            height = self.p.bounds[3] - self.p.bounds[1]
-            py = min((height * 0.1) / max_depth, 20) if max_depth else 20
-
-        def initializeNodeBreadth():
-            kys = []
-            for nodes in node_map.values():
-                nsum = np.sum([node['value'] for node in nodes])
-                ky = (y1 - y0 - (len(nodes)-1) * py) / nsum
-                kys.append(ky)
-            ky = np.min(kys) if len(kys) else np.nan
-
-            for nodes in node_map.values():
-                for i, node in enumerate(nodes):
-                    node['y0'] = i
-                    node['y1'] = i + node['value'] * ky
-
-            for link in graph['links']:
-                link['width'] = link['value'] * ky
-
-        def relaxLeftToRight(alpha):
-            for nodes in node_map.values():
-                for node in nodes:
-                    if not node['targetLinks']:
-                        continue
-                    weighted = sum([self.weightedSource(l) for l in node['targetLinks']])
-                    tsum = sum([l['value'] for l in node['targetLinks']])
-                    center = self.nodeCenter(node)
-                    dy = (weighted/tsum - center)*alpha
-                    node['y0'] += dy
-                    node['y1'] += dy
-
-
-        def relaxRightToLeft(alpha):
-            for nodes in list(node_map.values())[::-1]:
-                for node in nodes:
-                    if not node['sourceLinks']:
-                        continue
-                    weighted = sum([self.weightedTarget(l) for l in node['sourceLinks']])
-                    tsum = sum([l['value'] for l in node['sourceLinks']])
-                    center = self.nodeCenter(node)
-                    dy = (weighted/tsum - center)*alpha
-                    node['y0'] += dy
-                    node['y1'] += dy
-
-
-        def resolveCollisions():
-            for nodes in node_map.values():
-                y = y0
-                if self.p.node_sort:
-                    nodes.sort(key=cmp_to_key(self.ascendingBreadth))
-                for node in nodes:
-                    dy = y-node['y0']
-                    if dy > 0:
-                        node['y0'] += dy
-                        node['y1'] += dy
-                    y = node['y1'] + py
-
-                dy = y-py-y1
-                if dy > 0:
-                    node['y0'] -= dy
-                    node['y1'] -= dy
-                    y = node['y0']
-                    for node in nodes[:-1][::-1]:
-                        dy = node['y1'] + py - y;
-                        if dy>0:
-                            node['y0'] -= dy
-                            node['y1'] -= dy
-                        y = node['y0']
-
-        initializeNodeBreadth()
-        resolveCollisions()
-        alpha = 1
-        for _ in range(self.p.iterations):
-            alpha = alpha*0.99
-            relaxRightToLeft(alpha)
-            resolveCollisions()
-            relaxLeftToRight(alpha)
-            resolveCollisions()
+        ky = min(
+            (y1 - y0 - (len(c) - 1) * py) / sum(node['value'] for node in c)
+            for c in columns
+        )
+        for nodes in columns:
+            y = y0
+            for node in nodes:
+                node['y0'] = y
+                node['y1'] = y + node['value'] * ky
+                y = node['y1'] + py
+                for link in node['sourceLinks']:
+                    link['width'] = link['value'] * ky
+            y = (y1 - y + py) / (len(nodes) + 1)
+            for i, node in enumerate(nodes):
+                node['y0'] += y * (i + 1)
+                node['y1'] += y * (i + 1)
+            self.reorderLinks(nodes)
+
+    @classmethod
+    def sourceTop(cls, source, target, py):
+        y = target['y0'] - (len(target['targetLinks']) - 1) * py / 2
+        for link in target['targetLinks']:
+            if link['source'] is source:
+                break
+            y += link['width'] + py
+        for link in source['sourceLinks']:
+            if link['target'] is target:
+                break
+            y -= link['width']
+        return y
+
+    @classmethod
+    def targetTop(cls, source, target, py):
+        y = source['y0'] - (len(source['sourceLinks']) - 1) * py / 2
+        for link in source['sourceLinks']:
+            if link['target'] is target:
+                break
+            y += link['width'] + py
+        for link in target['targetLinks']:
+            if link['source'] is source:
+                break
+            y -= link['width']
+        return y
+
+    @classmethod
+    def resolveCollisionsTopToBottom(cls, nodes, y, i, alpha, py):
+        for node in nodes[i:]:
+            dy = (y - node['y0']) * alpha
+            if dy > _Y_EPS:
+                node['y0'] += dy
+                node['y1'] += dy
+            y = node['y1'] + py
+
+    @classmethod
+    def resolveCollisionsBottomToTop(cls, nodes, y, i, alpha, py):
+        # NOTE: don't change the `while` loop to `for`
+        while i >= 0:
+            node = nodes[i]
+            dy = (node['y1'] - y) * alpha
+            if dy > _Y_EPS:
+                node['y0'] -= dy
+                node['y1'] -= dy
+            y = node['y0'] - py
+            i -= 1
+
+    def resolveCollisions(self, nodes, alpha, py):
+        _, y0, _, y1 = self.p.bounds
+        i = len(nodes) // 2
+        subject = nodes[i]
+        self.resolveCollisionsBottomToTop(nodes, subject['y0'] - py, i - 1, alpha, py)
+        self.resolveCollisionsTopToBottom(nodes, subject['y1'] + py, i + 1, alpha, py)
+        self.resolveCollisionsBottomToTop(nodes, y1, len(nodes) - 1, alpha, py)
+        self.resolveCollisionsTopToBottom(nodes, y0, 0, alpha, py)
+
+    @classmethod
+    def reorderNodeLinks(cls, node):
+        for link in node['targetLinks']:
+            link['source']['sourceLinks'].sort(
+                key=cmp_to_key(cls.ascendingTargetBreadth)
+            )
+        for link in node['sourceLinks']:
+            link['target']['targetLinks'].sort(
+                key=cmp_to_key(cls.ascendingSourceBreadth)
+            )
+
+    def relaxLeftToRight(self, columns, alpha, beta, py):
+        for column in columns[1:]:
+            for target in column:
+                y = 0
+                w = 0
+                for link in target['targetLinks']:
+                    source = link['source']
+                    v = link['value'] * (target['column'] - source['column'])
+                    y += self.targetTop(source, target, py) * v
+                    w += v
+                if w <= 0:
+                    continue
+                dy = (y / w - target['y0']) * alpha
+                target['y0'] += dy
+                target['y1'] += dy
+                self.reorderNodeLinks(target)
+            if self.p.node_sort:
+                # TODO is the comparison operator valid?
+                column.sort(key=cmp_to_key(self.ascendingBreadth))
+            self.resolveCollisions(column, beta, py)
+
+    def relaxRightToLeft(self, columns, alpha, beta, py):
+        """Reposition each node based on its outgoing (source) links."""
+        for column in columns[-2::-1]:
+            for source in column:
+                y = 0
+                w = 0
+                for link in source['sourceLinks']:
+                    target = link['target']
+                    v = link['value'] * (target['column'] - source['column'])
+                    y += self.sourceTop(source, target, py) * v
+                    w += v
+                if w <= 0:
+                    continue
+                dy = (y / w - source['y0']) * alpha
+                source['y0'] += dy
+                source['y1'] += dy
+                self.reorderNodeLinks(source)
+            if self.p.node_sort:
+                column.sort(key=cmp_to_key(self.ascendingBreadth))
+            self.resolveCollisions(column, beta, py)
+
+    def computeNodeBreadths(self, graph):
+        columns = self.computeNodeColumns(graph)
+        _, y0, _, y1 = self.p.bounds
+        max_column_size = max(map(len, columns))
+        # NOTE: the `max_default_padding` thing is a holoviews-specific hack
+        max_default_padding = 20
+        py = (
+            self.p.node_padding
+            if self.p.node_padding is not None
+            else min((y1 - y0) / (max_column_size - 1), max_default_padding)
+            if max_column_size > 1
+            else max_default_padding
+        )
+        self.initializeNodeBreadths(columns, py)
+        for i in range(self.p.iterations):
+            alpha = 0.99 ** i
+            beta = max(1 - alpha, (i + 1) / self.p.iterations)
+            self.relaxRightToLeft(columns, alpha, beta, py)
+            self.relaxLeftToRight(columns, alpha, beta, py)
+        for node in graph['nodes']:
+            node['y1'] = round(node['y1'], _Y_N_DECIMAL_DIGITS)
 
     @classmethod
     def computeLinkBreadths(cls, graph):
@@ -297,14 +353,50 @@ def computeLinkBreadths(cls, graph):
             node['targetLinks'].sort(key=cmp_to_key(cls.ascendingSourceBreadth))
 
         for node in graph['nodes']:
-            y0 = y1 = node['y0']
+            y0 = node['y0']
+            y1 = y0
             for link in node['sourceLinks']:
-                link['y0'] = y0
+                link['y0'] = y0 + link['width'] / 2
                 y0 += link['width']
             for link in node['targetLinks']:
-                link['y1'] = y1
+                link['y1'] = y1 + link['width'] / 2
                 y1 += link['width']
 
+    def computePaths(self, graph):
+        paths = []
+        for link in graph['links']:
+            source, target = link['source'], link['target']
+            x0 = source['x1']
+            x1 = target['x0']
+            xmid = (x0 + x1) / 2
+            y0_upper = link['y0'] + link['width'] / 2
+            y0_lower = link['y0'] - link['width'] / 2
+            y1_upper = link['y1'] + link['width'] / 2
+            y1_lower = link['y1'] - link['width'] / 2
+
+            start = np.array([
+                [x0, y0_upper],
+                [x0, y0_lower],
+            ])
+            bottom = quadratic_bezier(
+                (x0, y0_lower),
+                (x1, y1_lower),
+                (xmid, y0_lower),
+                (xmid, y1_lower),
+            )
+            mid = np.array([
+                [x1, y1_lower],
+                [x1, y1_upper],
+            ])
+            top = quadratic_bezier(
+                (x1, y1_upper),
+                (x0, y0_upper),
+                (xmid, y1_upper),
+                (xmid, y0_upper),
+            )
+            spline = np.concatenate([start, bottom, mid, top])
+            paths.append(spline)
+        return paths
 
 
 class Sankey(Graph):


Test patch:
diff --git a/holoviews/tests/element/test_graphelement.py b/holoviews/tests/element/test_graphelement.py
--- a/holoviews/tests/element/test_graphelement.py
+++ b/holoviews/tests/element/test_graphelement.py
@@ -314,6 +314,7 @@ def test_single_edge_sankey(self):
                 'value': 1,
                 'depth': 0,
                 'height': 1,
+                'column': 0,
                 'x0': 0,
                 'x1': 15,
                 'y0': 0.0,
@@ -325,13 +326,14 @@ def test_single_edge_sankey(self):
                 'value': 1,
                 'depth': 1,
                 'height': 0,
+                'column': 1,
                 'x0': 985.0,
                 'x1': 1000.0,
                 'y0': 0.0,
                 'y1': 500.0},
             'value': 1,
             'width': 500.0,
-            'y0': 0.0,
-            'y1': 0.0
+            'y0': 250.0,
+            'y1': 250.0
         }
         self.assertEqual(links[0], link)
diff --git a/holoviews/tests/plotting/bokeh/test_sankey.py b/holoviews/tests/plotting/bokeh/test_sankey.py
--- a/holoviews/tests/plotting/bokeh/test_sankey.py
+++ b/holoviews/tests/plotting/bokeh/test_sankey.py
@@ -23,8 +23,10 @@ def test_sankey_simple(self):
         self.assertEqual(scatter_source.data['index'], scatter_index)
 
         text_data = {'x': np.array([18.75, 18.75, 1003.75, 1003.75, 1003.75]),
-                     'y': np.array([145.454545, 395.454545, 48.787879, 229.090909, 430.30303]),
+                     'y': np.array([125.454545, 375.454545,  48.787879, 229.090909, 430.30303 ]),
                      'text': ['A - 18', 'B - 15', 'X - 7', 'Y - 16', 'Z - 10']}
+        with open('a.txt', 'w') as f:
+            f.write(str(text_source.data) + '\n')
         for k in text_data:
             self.assertEqual(text_source.data[k], text_data[k])
 
@@ -32,9 +34,8 @@ def test_sankey_simple(self):
             'index': scatter_index,
             'x0': [0, 0, 985.0, 985.0, 985.0],
             'x1': [15, 15, 1000.0, 1000.0, 1000.0],
-            'y0': [19.999999999999588, 290.90909090909054, 2.842170943040401e-14,
-                   117.57575757575768, 360.60606060606057],
-            'y1': [270.90909090909054, 500.0, 97.57575757575768, 340.60606060606057, 500.0]
+            'y0': [0.0, 270.9090909090908, -7.105427357601002e-15, 117.57575757575756, 360.6060606060606],
+            'y1': [250.909091, 480.0, 97.575758, 340.606061, 500.0]
         }
         for k in quad_data:
             self.assertEqual(quad_source.data[k], quad_data[k])
@@ -65,7 +66,7 @@ def test_sankey_label_index(self):
         self.assertEqual(scatter_source.data['index'], scatter_index)
 
         text_data = {'x': np.array([18.75, 18.75, 1003.75, 1003.75, 1003.75]),
-                     'y': np.array([145.454545, 395.454545, 48.787879, 229.090909, 430.30303]),
+                     'y': np.array([125.454545, 375.454545,  48.787879, 229.090909, 430.30303 ]),
                      'text': ['A - 18', 'B - 15', 'X - 7', 'Y - 16', 'Z - 10']}
         for k in text_data:
             self.assertEqual(text_source.data[k], text_data[k])
@@ -82,4 +83,3 @@ def test_sankey_label_index(self):
         graph_renderer = plot.handles['glyph_renderer']
         self.assertTrue(renderers.index(graph_renderer)<renderers.index(quad_renderer))
         self.assertTrue(renderers.index(quad_renderer)<renderers.index(text_renderer))
-
diff --git a/holoviews/tests/plotting/matplotlib/test_sankey.py b/holoviews/tests/plotting/matplotlib/test_sankey.py
--- a/holoviews/tests/plotting/matplotlib/test_sankey.py
+++ b/holoviews/tests/plotting/matplotlib/test_sankey.py
@@ -19,7 +19,7 @@ def test_sankey_simple(self):
         labels = plot.handles['labels']
 
         text_data = {'x': np.array([18.75, 18.75, 1003.75, 1003.75, 1003.75]),
-                     'y': np.array([145.454545, 395.454545,  48.787879, 229.090909, 430.30303]),
+                     'y': np.array([125.454545, 375.454545,  48.787879, 229.090909, 430.30303 ]),
                      'text': ['A - 18', 'B - 15', 'X - 7', 'Y - 16', 'Z - 10']}
         for i, text in enumerate(labels):
             self.assertEqual(text.xy[0], text_data['x'][i])
@@ -29,9 +29,8 @@ def test_sankey_simple(self):
         quad_data = {
             'x0': [0, 0, 985.0, 985.0, 985.0],
             'x1': [15, 15, 1000.0, 1000.0, 1000.0],
-            'y0': [19.999999999999588, 290.90909090909054, 2.842170943040401e-14,
-                   117.57575757575768, 360.60606060606057],
-            'y1': [270.90909090909054, 500.0, 97.57575757575768, 340.60606060606057, 500.0]
+            'y0': [0.0, 270.9090909090908, -7.105427357601002e-15, 117.57575757575756, 360.6060606060606],
+            'y1': [250.909091, 480.0, 97.575758, 340.606061, 500.0]
         }
         for i, rect in enumerate(rects.get_paths()):
             x0, x1, y0, y1 = [quad_data[c][i] for c in ('x0', 'x1', 'y0', 'y1')]
@@ -49,7 +48,7 @@ def test_sankey_label_index(self):
         labels = plot.handles['labels']
 
         text_data = {'x': np.array([18.75, 18.75, 1003.75, 1003.75, 1003.75]),
-                     'y': np.array([145.454545, 395.454545,  48.787879, 229.090909, 430.30303]),
+                     'y': np.array([125.454545, 375.454545,  48.787879, 229.090909, 430.30303 ]),
                      'text': ['A - 18', 'B - 15', 'X - 7', 'Y - 16', 'Z - 10']}
         for i, text in enumerate(labels):
             self.assertEqual(text.xy[0], text_data['x'][i])