d3 / d3-chord

Visualizations relationships or network flow with a circular layout.
https://d3js.org/d3-chord
ISC License
124 stars 42 forks source link

Twisted ribbons between nearby nodes #13

Open vasturiano opened 4 years ago

vasturiano commented 4 years ago

This is a fantastic layout module and you can do some really wonderful things with!

There is one issue however that is rather difficult to get around from the consumer's side. When drawing ribbons between nearby nodes, they often get "twisted" as follows:

Screen Shot 2019-11-21 at 1 17 27 PM

There's two factors that make this artifact more apparent:

Afaict, this cannot be counteracted by manipulating any of the ribbon accessor methods.

I believe this is because when computing the two arcs that connect the sides of the ribbon between the source and target, they're both centered at 0,0: https://github.com/d3/d3-chord/blob/d07a3788ef9887adad64ac02138e2f071af05a18/src/ribbon.js#L51 and https://github.com/d3/d3-chord/blob/d07a3788ef9887adad64ac02138e2f071af05a18/src/ribbon.js#L54

So, in certain cases the two arcs actually cross each other because their closest point to the center is happening at different angles. This results in the visual "twist".

Perhaps a resolution would be to shift the arc center of the shorter arc closer to the edge of the diagram, just enough so that the two arcs never cross.

mbostock commented 4 years ago

It would also be nice to have meaningful ribbon widths. For example, if the start and end value are the same, the ribbon width should be constant. I believe this is possible to implement if we compute the Bézier paths ourselves and use graduated curve offsetting, but it’s not trivial.

Short of that, we could use a tension parameter so that rather than always using ⟨0,0⟩ as the control point for the quadratic curve,

https://github.com/d3/d3-chord/blob/d07a3788ef9887adad64ac02138e2f071af05a18/src/ribbon.js#L53

we shift the control point out towards the circumference of the circle at the midangle between the source and target.

vasturiano commented 4 years ago

Ah, I realize now that indeed the issue is with the control point of the quadratic curve, not the arc geometries, those are fine. 👍

Approach # 1 definitely seems ideal, but the math looks tricky.

For # 2 I think we'd only need to adjust one of the quadratic curves (the one with the smallest angular distance).

mbostock commented 4 years ago

This is discussed in the circlize documentation for R:

https://jokergoo.github.io/circlize_book/book/graphics.html#links

vasturiano commented 4 years ago

The article suggests to allow the consumer to specify an h2 for the problematic links.

It happens especially when position of the two ends are too close or the width of one end is extremely large while the width of the other end is too small. In that case, users can manually set height of the top and bottom border by h and h2

What would be convenient is to calculate h2 automatically internally, so the user doesn't have to be bothered with it. It seems to be a factor of the four angles sa0, sa1, ta0 and ta1. Now, what would that expression look like?

Fil commented 4 years ago

Here's a heuristic that seems not too bad: https://observablehq.com/d/07db25f490cf93d1

We look at the differences between the angles, and if that difference is large or one of the angles is really sharp, we push the inner Bézier control points gently towards the border. I tried it on two examples only, so I'm not 100% sure it works everywhere.

PS: I converted the quadratic Bézier to a cubic Bézier—though probably not 100% necessary, I found this made it easier to think — the control points are just 1/3 of the way towards the circumference, and we can nudge them a little more to make the shape smaller.

before

Capture d’écran 2020-07-04 à 01 33 36

after

Capture d’écran 2020-07-04 à 01 33 32

before

Capture d’écran 2020-07-04 à 01 33 27

after

Capture d’écran 2020-07-04 à 01 33 20

Fil commented 4 years ago

I believe PR #16 fixes this in all(?) cases. @vasturiano could you test it on your dataset, or share the notebook so we can see if it works there too?

vasturiano commented 4 years ago

@fil thanks for looking at this!

I've tested the change on my side and it certainly fixes all the twisting cases I could find. 👍 Though it also makes the ribbons generally wider in the middle, which tends to make dense cases look a bit more cluttered.

Here's a few before/after sshots:

before

Screen Shot 2020-07-07 at 12 23 42 AM

after

Screen Shot 2020-07-07 at 12 16 40 AM

before

Screen Shot 2020-07-07 at 12 24 16 AM

after

Screen Shot 2020-07-07 at 12 17 42 AM

before

Screen Shot 2020-07-07 at 12 24 42 AM

after

Screen Shot 2020-07-07 at 12 18 45 AM

before

Screen Shot 2020-07-07 at 12 24 58 AM

after

Screen Shot 2020-07-07 at 12 19 26 AM

I haven't looked at the code changes, but would there be a way to detect and modify only the twisted cases, leaving the ribbons that had no issues untouched?

Fil commented 4 years ago

would there be a way to detect and modify only the twisted cases

I believe it can be done analytically, since it's the intersections of two parabolas.

This PR's approach is also a little bit more consistent with Mike's desire to have…

meaningful ribbon widths. For example, if the start and end value are the same, the ribbon width should be constant

but that will inevitably result in much thicker ribbons in your use case. (If you could share it as data it would be great to experiment).

More research needed :)

vasturiano commented 4 years ago

@fil I added a change request to your notebook with the additional datasets so it can be experimented with. 👍 https://observablehq.com/d/07db25f490cf93d1

Fil commented 4 years ago

New research!

See https://observablehq.com/d/4dcd344d74fcccc8 for the application, and https://observablehq.com/d/1af6b73d28c93927 for the (interactive) research.

Conclusions:

1). the moderation factor must be the same for the inner and outer parts of the path.

why? because in some cases you want parallel ribbons stemming from the same source and flying to nearby targets. If the inner part of the outer ribbon was the only one that got "moderated", then it would overlap the outer part of the inner ribbon. (see example image below)

2). With quadratic Bézier I don't think we can find an analytical solution, in the sense that some overlap will always be present when you have a very small angle inside e.g. a π/4 angle — but with a good heuristic the overlap can be limited to 1 or 2 pixels.

3). As a starting point the following heuristic works quite well: f(x) := max(0, 1 - x) ** 4 * max(0.7, 1 - 6 * x)

Where f(x) controls how much we move the control point from <0,0> towards the midpoint of the angle x.

(We might have to map all the angle pairs (a,b), measure the overlap, and see if we can optimize this function further?)

Capture d’écran 2020-09-24 à 10 28 31