mhkeller / layercake.graphics

Website for Layer Cake
https://layercake.graphics
MIT License
8 stars 6 forks source link

Add Sankey example #17

Closed jurb closed 4 years ago

jurb commented 4 years ago

What is the best way to suggest a chart to the library?

I implemented a Sankey chart like so

<script>
  import { getContext } from "svelte";
  import * as Sankey from "d3-sankey";

  const { data, width, height, padding, custom } = getContext("LayerCake");

  $: sankey = Sankey.sankey()
    .nodeAlign(Sankey.sankeyLeft)
    .nodeWidth(5)
    .nodePadding(10)
    .size([$width, $height - $padding.top])
    .linkSort(null);

  $: sankeyData = sankey($data);

  $: link = Sankey.sankeyLinkHorizontal();

  $: colorLinks = $custom.colorLinks;

  $: fontSize = $width <= 320 ? "8px" : "12px";
</script>

<style>
</style>

<g transform={`translate(0,${$padding.top})`}>
  <g class="link-group">
    {#each sankeyData.links as d}
      <path
        d={link(d)}
        fill={'none'}
        stroke={colorLinks(d)}
        stroke-opacity={0.5}
        stroke-width={d.width} />
    {/each}
  </g>
  <g class="rect-group">
    {#each sankeyData.nodes as d, i}
      <rect
        x={d.x0}
        y={d.y0}
        height={d.y1 - d.y0}
        width={d.x1 - d.x0}
        fill={'#09299490'} />
      <text
        x={d.x0 < $width / 4 ? d.x1 + 6 : d.x0 - 6}
        y={(d.y1 + d.y0) / 2}
        style={`fill: ${'#263238'};
                alignment-baseline: "middle";
                font-size: ${fontSize};
                text-anchor: ${d.x0 < $width / 4 ? 'start' : 'end'};
                pointer-events: "none";
                user-select: "none";`}>
        {d.name}
      </text>
    {/each}
  </g>
</g>
mhkeller commented 4 years ago

Thanks @jurb! I think making a pull request on the website repo at http://github.com/mhkeller/layercake.graphics would be best. I'll transfer the issue to that repo.

mhkeller commented 4 years ago

@jurb Do you have the data you used for this? I can put it into an example – I just finished updating the site.

jurb commented 4 years ago

Sure! The data I used was private client work, but this should work:

    const data = {
        nodes: [
            { id: "A1" },
            { id: "A2" },
            { id: "A3" },
            { id: "B1" },
            { id: "B2" },
            { id: "B3" },
            { id: "B4" },
            { id: "C1" },
            { id: "C2" },
            { id: "C3" },
            { id: "D1" },
            { id: "D2" }
        ],
        links: [
            { source: "A1", target: "B1", value: 27 },
            { source: "A1", target: "B2", value:  9 },
            { source: "A2", target: "B2", value:  5 },
            { source: "A2", target: "B3", value: 11 },
            { source: "A3", target: "B2", value: 12 },
            { source: "A3", target: "B4", value:  7 },
            { source: "B1", target: "C1", value: 13 },
            { source: "B1", target: "C2", value: 10 },
            { source: "B4", target: "C2", value:  5 },
            { source: "B4", target: "C3", value:  2 },
            { source: "B1", target: "D1", value:  4 },
            { source: "C3", target: "D1", value:  1 },
            { source: "C3", target: "D2", value:  1 }
        ]
    }

Excited for the 3.0 release! Hope I can find some time soon to play around with it.

techniq commented 4 years ago

@jurb @mhkeller Something like... https://svelte.dev/repl/e5a640408459450bb6073e7962b1b770?version=3.24.0. Note: I had to add .nodeId(d => d.id) to the sankey builder based on the example data, and just hard coded the colorLinks

I have a non-LayerCake Svelte version here which is mostly a port this React/vx version which includes link/node hovering, layout and padding props, etc. I like the measurement and such being handled by LayerCake. I plan to migrate the rest of the vx hierarchy examples to Svelte/LayerCake and finish the interactive bits (edit / provide own data, etc).

mhkeller commented 4 years ago

Very cool thanks @techniq! I've been slow to add this one. Maybe including colorLinks as an export on the Sankey component itself may be good or is it useful to have that function shared across components?

I could also see it being useful to have an HTML layer that's just the labels in case someone wants that option. If the paths look good using ScaledSvg then that could be a good place to use it!

techniq commented 4 years ago

Handling the coloring within the Sankey is what I did in my React example, albeit in a hacky way, as I colored the nodes based on depth.

Those all sound like good suggestions and something I'll experiment with.

mhkeller commented 4 years ago

I added the example here with a few minor changes. Let me know what you think.

  1. The color and sizing options are exports with the existing values as defaults so users can customize more
  2. I wasn't sure why there was $height - $padding.top and then a translate of $padding.top on the <g> element. Seems like it would decrease the height by double the padding.top and then translate it? Maybe @jurb can explain what that could be used for.
  3. Moved the data into its own file to be consistent with the other examples and make it easier to share data with an SSR example

Doing an SSR version would be pretty easy. You would change [$width, $height] in the sankey to [100, 100] and then you would divide nodeWidth and nodePadding by the width. I started on a version here. It needs an HTML text layer. I'm open to thoughts on this but I'm thinking the best way to do the SSR version would be:

  1. Have a wrapper component that creates the D3 sankey object
  2. Pass that as a prop to a ScaledSvg component that draws the links
  3. Pass that as a prop to an HTML component that draws the node rects and the text labels

Having the nodes in an HTML component where you can define an actual pixel width instead of dividing to get a percent seems a bit nicer to me as long as the pixels line up.

The Sankey configuration could be all in the top-level component, really, since it doesn't really require context values apart from $data. Having it in a wrapper could just make it easier to drop into multiple parts of a project though and generally more transportable.

jurb commented 4 years ago

I wasn't sure why there was $height - $padding.top and then a translate of $padding.top on the element. Seems like it would decrease the height by double the padding.top and then translate it? Maybe @jurb can explain what that could be used for.

@mhkeller This was just shoddy coding on my part! Sorry :)

mhkeller commented 4 years ago

No worries! I hadn't done a sankey in a while so wasn't sure if it was necessary to have some extra padding to handle overflow like how you have to do with axes.

jurb commented 4 years ago

@mhkeller The version I made initially had extra text labels at the top, and I forgot to take out the accommodating padding for this example.

techniq commented 4 years ago

@mhkeller Since it's just an example the user is free to do as they please, but might be useful to show exporting nodeAlign (defaulting to sankeyLeft) and nodeId (default to d => d.id, but sometimes is more convenient to use d.name like on Flow-o-matic) to show how it can be more configurable. My React component exposes all of the sankey generator props... which once again the user can do as much or as little as desired...

Thanks again for the awesome library and great examples. I plan to create more of these hierarchy components using LayerCake (Tree, Icicle, Sunburst, Treemap, etc) with animated versions (probably be a few weeks time before I can get around to it). Rich has the zoomable Treemap example on Pancake which I have as well with React / react-spring, and plan to implement it with LayerCake (and feel out using Svelte's transitions and motion/spring)

mhkeller commented 4 years ago

Got it. Maybe exporting nodeSort would be good too. On nodeAlign, I wonder if that affects bundle size / makes it harder to tree-shake unused alignments.

mhkeller commented 4 years ago

And great I'm looking forward to seeing what you come up with! I'll probably add the SSR Sankey over the next few days unless someone else wanted to take a crack at it.

mhkeller commented 4 years ago

I started doing an SSR one but it's a little tricky since the nodeWidth and nodePadding are specified in the top-level Sankey object so it makes it tricky to then specify those in pixels on the HTML layer. Could be I just wasn't thinking about it hard enough.

mhkeller commented 4 years ago

If anyone comes up with a generalized template for doing an ssr sankey, feel free to PR it! Maybe the best way is you keep everything as percents and change the nodeWidth based on the $width.

mhkeller commented 4 years ago

Here's where I got but something's up with the vertical height: https://svelte.dev/repl/f3d14380fd7949bbbc9c36077b6a7d84?version=3.24.0

Screen Shot 2020-07-19 at 8 54 45 PM
mhkeller commented 4 years ago

The issue has to do with vector-effects: non-scaling-stroke;. If you set it to none. It looks better

Screen Shot 2020-07-19 at 8 55 14 PM

But you can sometimes end up with strokes that don't have a consistent width. See the one from B4 to C2. So I would say that Sankeys aren't a great choice for SSR.

mhkeller commented 4 years ago

I'll close this for now but let me know if you have any ideas!