Home > front end >  How to fix unwanted circle on / break down of SVG path element for Sankey links with d3?
How to fix unwanted circle on / break down of SVG path element for Sankey links with d3?

Time:06-13

I'm putting together a Sankey diagram via SVG but suddenly under some unknown conditions one of the paths show a circle where there shouldn't be any.

I tried to remove as much as possible from the my SVG but removing anything further makes this no longer reproducible.

Here you can see my reduced SVG image. The black path element is the one having the issue. I manually marked where the path stroke element should end but somehow it shows a circle which seems to have the same radius as the stroke width.

example image showing the issue

<div style="width: 100%; height: 100%;">
  <svg width=100% height=500px>
    <g >
      <g >
        <path d="M107.61971830985915,30C107.61971830985915,74,107.61971830985915,74,107.61971830985915,118" fill="none" stroke="#000000" stroke-opacity="1" stroke-width="200"></path>
        <path d="M251.11267605633802,30C251.11267605633802,192,328.943661971831,192,328.943661971831,354" fill="none" stroke="#00bbff35" stroke-opacity="0.5" stroke-width="71.74647887323944"></path>
        <path d="M478.38028169014075,30C478.38028169014075,192,384.7464788732394,192,384.7464788732394,354" fill="none" stroke="#00bbff35" stroke-opacity="0.5" stroke-width="39.859154929577464"></path>
        <path d="M542.1549295774647,30C542.1549295774647,192,572.1549295774648,192,572.1549295774648,354" fill="none" stroke="#00bbff35" stroke-opacity="0.5" stroke-width="87.69014084507042"></path>
        <path d="M344.8169014084507,30C344.8169014084507,192,452.50704225352115,192,452.50704225352115,354" fill="none" stroke="#00bbff35" stroke-opacity="0.5" stroke-width="95.66197183098592"></path>
        <path d="M143.49295774647888,148C143.49295774647888,251,153.49295774647888,251,153.49295774647888,354" fill="none" stroke="#00bbff35" stroke-opacity="0.5" stroke-width="79.71830985915493"></path>
        <path d="M409.12773522289996,148C409.12773522289996,251,213.28169014084506,251,213.28169014084506,354" fill="none" stroke="#00bbff35" stroke-opacity="0.5" stroke-width="39.859154929577464"></path>
      </g>
    </g>
  </svg>
</div>

Any clue where this comes from and what I can do to avoid it?

Clarification:

The paths are computed by d3. It's not a manually created graphic and I'm looking for a general solution to the problem, not a solution for this particular example.

CodePudding user response:

I changed the first path to <path d="M107.62,30L107.62,118" fill="none" stroke="#000000" stroke-opacity="1" stroke-width="200"></path>. I worked this out by loading it in to Illustrator.

I'm not 100% sure what the issue was, but maybe someone smarter than me will figure it out. The maths of SVGs hurts my brain.

<div style="width: 100%; height: 100%;">
  <svg width=100% height=500px>
    <g >
      <g >
        <path d="M107.62,30L107.62,118" fill="none" stroke="#000000" stroke-opacity="1" stroke-width="200"></path>
        <path d="M251.11267605633802,30C251.11267605633802,192,328.943661971831,192,328.943661971831,354"  fill="none" stroke="#00bbff35" stroke-opacity="0.5" stroke-width="71.74647887323944"></path>
        <path d="M478.38028169014075,30C478.38028169014075,192,384.7464788732394,192,384.7464788732394,354" fill="none" stroke="#00bbff35" stroke-opacity="0.5" stroke-width="39.859154929577464"></path>
        <path d="M542.1549295774647,30C542.1549295774647,192,572.1549295774648,192,572.1549295774648,354" fill="none" stroke="#00bbff35" stroke-opacity="0.5" stroke-width="87.69014084507042"></path>
        <path d="M344.8169014084507,30C344.8169014084507,192,452.50704225352115,192,452.50704225352115,354" fill="none" stroke="#00bbff35" stroke-opacity="0.5" stroke-width="95.66197183098592"></path>
        <path d="M143.49295774647888,148C143.49295774647888,251,153.49295774647888,251,153.49295774647888,354" fill="none" stroke="#00bbff35" stroke-opacity="0.5" stroke-width="79.71830985915493"></path>
        <path d="M409.12773522289996,148C409.12773522289996,251,213.28169014084506,251,213.28169014084506,354" fill="none" stroke="#00bbff35" stroke-opacity="0.5" stroke-width="39.859154929577464"></path>
      </g>
    </g>
  </svg>
</div>

CodePudding user response:

Apperently a browser related bug you should report.

As a "post-processing" workaround you could use this helper I've once created to convert flat curves to L commands. (based on Sankey Link Failing

While the issue the blog post focuses on looks slightly different, I'm sure it's the exact same issue I'm experiencing.

In some sense, I feel like the underlying issue here is a misuse of the path element. Setting fill to none and increasing the stroke width to draw the line seems to not work properly when the line is shorter than the stroke width.

The blog post proposes to draw custom Bézier curves instead. This removes the need for the stroke property and instead we can just use the normal fill. This feels like a proper solution to me. Let's look into the code.

Before this question I did not really look into the graphical side of Sankey diagrams. While trying to understand the solution proposed in the blog post (I need to change the code to work vertically instead of horizontally), I struggled to fully graphs what coordinates are referring to what. The d3 docs are not really helpful on that. Here an info graphic I made for myself:

Sankey Diagram Coordinates

The code from the blog post uses a slightly different coordinate naming. I adapted this, added some comments and tried to simplify it.

function sankeyLinkPath(link) {
    /**
     * This function is a drop in replacement for d3.sankeyLinkHorizontal().
     * Except any accessors/options.
     */
    // Start and end of the link
    let sx1 = link.source.x1;
    let tx0 = link.target.x0   1;

    // All four outer corners of the link
    // where e.g. lsy0 is the upper corner of the link on the source side
    let lsy0 = link.y0 - link.width / 2;
    let lsy1 = link.y0   link.width / 2;
    let lty0 = link.y1 - link.width / 2;
    let lty1 = link.y1   link.width / 2;

    // Center (x) of the link
    let lcx = sx1   (tx0 - sx1) / 2;

    // Define outline of link as path
    let path = d3.path();
    path.moveTo(sx1, lsy0);
    path.bezierCurveTo(lcx, lsy0, lcx, lty0, tx0, lty0);
    path.lineTo(tx0, lty1);
    path.bezierCurveTo(lcx, lty1, lcx, lsy1, sx1, lsy1);
    path.lineTo(sx1, lsy0);
    return path.toString();
}

What this does is, that it draws a path of the outline of the link.

Steps to draw outline of link.

Which reduces my code to:

<path
    d={sankeyLinkPath(link)}
    fill='#333'
/>

Adjust for Vertical Plotting of Sankey

Finally, I adjusted the code to work vertically instead of horizontally:

function sankeyLinkPath(link) {
    /**
     * This function is a drop in replacement for d3.sankeyLinkVertical().
     * Except any accessors/options.
     */
    // Start and end of the link
    let sy1 = link.source.x1;
    let ty0 = link.target.x0   1;

    // All four outer corners of the link
    // where e.g. lsx0 is the right corner of the link on the source side
    let lsx0 = link.y0 - (link.width / 2) * linkWidth;
    let lsx1 = link.y0   (link.width / 2) * linkWidth;
    let ltx0 = link.y1 - (link.width / 2) * linkWidth;
    let ltx1 = link.y1   (link.width / 2) * linkWidth;

    // Center (y) of the link
    let lcy = sy1   (ty0 - sy1) / 2;

    // Define outline of link as path
    let path = d3.path();
    path.moveTo(lsx0, sy1);
    path.bezierCurveTo(lsx0, lcy, ltx0, lcy, ltx0, ty0);
    path.lineTo(ltx1, ty0);
    path.bezierCurveTo(ltx1, lcy, lsx1, lcy, lsx1, sy1);
    path.lineTo(lsx0, sy1);
    return path.toString();
}

Which now yields this absolutely fine working Sankey plot:

.opacity-40 {
    opacity: 0.4;
}
<div width="100%" height="100%">
    <svg width="648" height="384">
        <g >
            <g >
                <path
                    
                    d="M11.370422535211262,40C11.370422535211262,77.83333333333334,11.370422535211262,77.83333333333334,11.370422535211262,115.66666666666667L216.03802816901413,115.66666666666667C216.03802816901413,77.83333333333334,216.03802816901413,77.83333333333334,216.03802816901413,40L11.370422535211262,40"
                    fill="#0011ff"
                ></path>
            </g>
            <g >
                <path
                    
                    d="M5.47464788732394,154.66666666666669C5.47464788732394,249.83333333333334,5.47464788732394,249.83333333333334,5.47464788732394,345L104.01830985915494,345C104.01830985915494,249.83333333333334,104.01830985915494,249.83333333333334,104.01830985915494,154.66666666666669L5.47464788732394,154.66666666666669"
                    fill="#00bbff"
                ></path>
            </g>
            <g >
                <path
                    
                    d="M318.26478873239444,40C318.26478873239444,192.5,312.9971830985916,192.5,312.9971830985916,345L403.9605633802817,345C403.9605633802817,192.5,409.22816901408453,192.5,409.22816901408453,40L318.26478873239444,40"
                    fill="#0011ff"
                ></path>
            </g>
            <g >
                <path
                    
                    d="M487.8718309859155,40C487.8718309859155,192.5,559.9845070422538,192.5,559.9845070422538,345L643.367605633803,345C643.367605633803,192.5,571.2549295774647,192.5,571.2549295774647,40L487.8718309859155,40"
                    fill="#00bbff"
                ></path>
            </g>
            <g >
                <path
                    
                    d="M113.70422535211267,154.66666666666669C113.70422535211267,249.83333333333334,123.70422535211273,249.83333333333334,123.70422535211273,345L199.50704225352118,345C199.50704225352118,249.83333333333334,189.50704225352112,249.83333333333334,189.50704225352112,154.66666666666669L113.70422535211267,154.66666666666669"
                    fill="#00bbff"
                ></path>
            </g>
            <g >
                <path
                    
                    d="M231.1985915492958,40C231.1985915492958,192.5,412.8042253521127,192.5,412.8042253521127,345L481.0267605633804,345C481.0267605633804,192.5,299.4211267605634,192.5,299.4211267605634,40L231.1985915492958,40"
                    fill="#0011ff"
                ></path>
            </g>
            <g >
                <path
                    
                    d="M417.22957746478875,40C417.22957746478875,77.83333333333334,413.3159790754746,77.83333333333334,413.3159790754746,115.66666666666667L466.37795090646057,115.66666666666667C466.37795090646057,77.83333333333334,470.2915492957747,77.83333333333334,470.2915492957747,40L417.22957746478875,40"
                    fill="#00bbff"
                ></path>
            </g>
            <g >
                <path
                    
                    d="M577.9929577464789,40C577.9929577464789,192.5,486.92253521126764,192.5,486.92253521126764,345L524.8239436619718,345C524.8239436619718,192.5,615.894366197183,192.5,615.894366197183,40L577.9929577464789,40"
                    fill="#0011ff"
                ></path>
            </g>
            <g >
                <path
                    
                    d="M412.4737255543479,154.66666666666669C412.4737255543479,249.83333333333334,205.82394366197187,249.83333333333334,205.82394366197187,345L243.72535211267612,345C243.72535211267612,249.83333333333334,450.37513400505213,249.83333333333334,450.37513400505213,154.66666666666669L412.4737255543479,154.66666666666669"
                    fill="#00bbff"
                ></path>
            </g>
            <g >
                <path
                    
                    d="M195.40281690140844,154.66666666666669C195.40281690140844,249.83333333333334,257.5154929577465,249.83333333333334,257.5154929577465,345L287.8366197183099,345C287.8366197183099,249.83333333333334,225.72394366197184,249.83333333333334,225.72394366197184,154.66666666666669L195.40281690140844,154.66666666666669"
                    fill="#00bbff"
                ></path>
            </g>
            <g >
                <path
                    
                    d="M453.32302132899576,154.66666666666669C453.32302132899576,192.5,457.2918698867473,192.5,457.2918698867473,230.33333333333334L472.452433267029,230.33333333333334C472.452433267029,192.5,468.48358470927747,192.5,468.48358470927747,154.66666666666669L453.32302132899576,154.66666666666669"
                    fill="#00bbff"
                ></path>
            </g>
            <g >
                <path
                    
                    d="M465.2932783374515,269.33333333333337C465.2932783374515,307.1666666666667,289.94225352112676,307.1666666666667,289.94225352112676,345L297.5225352112676,345C297.5225352112676,307.1666666666667,472.87356002759236,307.1666666666667,472.87356002759236,269.33333333333337L465.2932783374515,269.33333333333337"
                    fill="#00bbff"
                ></path>
            </g>
            <g >
                <path
                    
                    d="M456.8707431261839,269.33333333333337C456.8707431261839,307.1666666666667,537.3507042253524,307.1666666666667,537.3507042253524,345L544.9309859154931,345C544.9309859154931,307.1666666666667,464.4510248163248,307.1666666666667,464.4510248163248,269.33333333333337L456.8707431261839,269.33333333333337"
                    fill="#00bbff"
                ></path>
            </g>
        </g>
    </svg>
</div>

I gained quite some insight into working with SVGs and d3 with this. I hope the solution helps someone else in the future.

  • Related