Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grouped Bar Chart #29

Closed
techniq opened this issue Jul 10, 2020 · 14 comments
Closed

Grouped Bar Chart #29

techniq opened this issue Jul 10, 2020 · 14 comments
Labels
discussion Talk about implementation of different chart types.

Comments

@techniq
Copy link
Contributor

techniq commented Jul 10, 2020

Thanks for the awesome library. I'm just beginning to experiment with Svelte (coming from React) and your library has been very useful to learn how to integrate with d3 (I've contributed to the React vx library in the past and considering porting the Text component to Svelte).

Looking at the guide and example, I see you have a xScale, yScale, zScale, and rScale for the scales. For the Stacked Column example, zScale is used to represent the stack.

I was curious, how would this translate to a Grouped Bar Chart or even further with a Grouped Stacked Bar Chart. I know typically the use of x0/x1 and y0/y1 scales are used. I didn't know if it made since to include at least x1Scale and y1Scale in LayerCake.

@mhkeller
Copy link
Owner

mhkeller commented Jul 10, 2020

Thanks for the note!

General info on scales

The four included scales can pretty much be used however you want. The zScale in the Stacked Column example is actually for the color, the stack is determined based on the y-scale after the data is transformed through D3's stack layout. If you wanted an additional scale, you could pass in a scaleOrdinal() to the rScale or zScale layercake prop as well as the proper domain and range for rDomain and rRange or zDomain and zScale and use it that way – there's nothing fixed about having to use the rScale as a scaleSqrt or anything like that.

If it sounds weird to use a value called rScale for something other than a radius, you can also pass in any of your own custom values in the custom prop like so:

<Layecake
 custom={{
    x1: scaleOrdinal().domain(YOUR_DOMAIN).range(YOUR_RANGE)
  }}
  ...
>

The x, y, r and z scales are generally more useful because they are passed in default ranges based on the calculated width and height.

Info on grouped and stacked charts

I've never had occasion to make one of these charts so you'd have to experiment what the best architecture is within the svelte context. Looking briefly at the example, it seems to use the bandwidth of x0 as the rangeRound for x1. That seems like a fairly custom scale setup and not something that would be easily included into the default library in a generalizable way since each of the scales are independent of one another and don't share values – this setup is more of a nested scale.

Instead of creating these scales on the custom prop, you will likely want to create them in a wrapper component similar to the Small Multiple example so you can have access to the calculated width. This isn't a complete example but shows how you can do something like:

<script>
  import { getContext } from 'svelte';
  import { scaleBand } from 'd3-scale';

  const { width, data } = getContext('LayerCake');

  const keys = $data.columns.slice(1);

  const x0 = scaleBand()
    .domain($data.map(d => d[groupKey])) // this should be in some kind of function to determine the `groupKey`
    .rangeRound([0, $width])
    .paddingInner(0.1);
  
  const x1 = scaleBand()
    .domain(keys)
    .rangeRound([0, x0.bandwidth()])
    .padding(0.05)

</script>

<!-- Components go here -->

Hope that helps point you in the right direction. Let me know what you come up with!

@techniq
Copy link
Contributor Author

techniq commented Jul 10, 2020

Thanks @mhkeller! Good information. Initially focused on a grouped (non-stacked) bar chart but was curious how that would transition to including stacking (if needed). Let me play around with LayerCake more and I'll let you know.

@techniq techniq closed this as completed Jul 10, 2020
@mhkeller
Copy link
Owner

Sounds good. The stacking would be easy to add on since the D3 stack layout function builds the stacking information into the structure of your data, as opposed to requiring a separate scale to handle the offset.

@techniq
Copy link
Contributor Author

techniq commented Jul 10, 2020

Btw, I was comparing the difference between Rich's Pancake library and LayerCake. It doesn't seem like Pancake exposes the scales and such like LayerCake does, but I'm still digesting each (mostly in my spare time). Actually ran into an issue with trying to use Pancake with Svelte 3.24.0 that I reported, but likely leaning towards LayerCake as it fits my use cases closer (and less "experimenty" as Rich labeled) :).

I have some hierarchy examples I've been making in with React / vx / react-spring but likely going to switch over to Svelte and continue that work. Actually started it a few weeks ago but haven't had time to work on it further.

@mhkeller
Copy link
Owner

mhkeller commented Jul 10, 2020

Ya the two libraries are similar but have a few different goals. I've described LayerCake more as a framework for charting recipes but it's not a charting library exactly.

LayerCake is higher-level than Pancake in that it does extent calculation for you, has a D3-like margin convention and has a few chart-minded concepts such as domain padding and bounded ranges. BUT it's much lower level since it doesn't come with any built-in chart types or primitives the way Pancake does.

Another goal of LayerCake is to "layer" canvas / html / svg layers on top of one another, which isn't a goal of Pancake. Pancake is pretty great and since LayerCake 3.0, I've incorporated the neat SVG tricks into the ScaledSvg component!

@techniq
Copy link
Contributor Author

techniq commented Jun 29, 2021

@mhkeller Long time since our last chat :). I'm finally getting around to porting our React/visx grouped and stacked bar charts to LayerCake, and have a few questions / issues

Grouped column example

I've ported the current Stacked column example as a Grouped column example. I ended up using x / xScale for x0 as I needed it in context to use it in AxisX, and used z / zScale for x1 for the "subscale". I did consider the wrapper component (like the Small Multiples examples) but since I wanted xScale in the LayerCake context for Axis (and really both scales will be needed for Tooltips, etc), I felt it should be handled by LayerCake and in context and not as a separate variable.

I did ran into some chicken/egg problems though, as xScale (aka x0) needs it's range set (based on the chart width) which LayerCake handles, but also I could not use xScale.bandwidth() for the zRange until the range of xScale is set. As a workaround, I added the following to ColumnGrouped.svelte to work around the timing...

// Needs set after xScale (x0) has range set by chart width
$: $zScale.range([0, $xScale.bandwidth()])

I wish I could could have set it like...

<script>
  const xScale = scaleBand().paddingInner(0.1);
  const zScale = scaleBand().paddingInner(0.05);
</script>

<LayerCake
  xScale={xScale}

  zScale={zScale}
  zRange={() => [0, xScale.bandwidth()]}
>

as I saw the overload for xRange/zRange which takes in a function with the width/height, but the scales appear to all be initialized at the same time, so xScale.range() hasn't been set yet. Anyways, it feels a little dirty to set the range within the ColumnGrouped component, but it "works".

Some other things I tried... adding the explicit xRange in hopes it would get set sooner on xScale...

<script>
  const xScale = scaleBand().paddingInner(0.1);
  const zScale = scaleBand().paddingInner(0.05);
</script>


<LayerCake
  xScale={xScale}
  xRange={({ width }) => [0, width]}

  zScale={zScale}
  zRange={() => [0, xScale.bandwidth()]}
>

I also tried setting the range directly on xScale in hopes setting it "earlier" on xScale would make it available for zScale but it doesn't appear to help (still needed set in ColumnGrouped as well)

<script>
  const xScale = scaleBand().paddingInner(0.1);
  const zScale = scaleBand().paddingInner(0.05);
</script>


<LayerCake
  xScale={xScale}
  xRange={({ width }) => xScale.range([0, width]).range()}

  zScale={zScale}
  zRange={() => [0, xScale.bandwidth()]}
>

Anyways, not terrible, but would be nice if this could be handled within LayerCake initialization. Maybe if there was an on:init={...} event that you could access all the LayerCake context (and be able to update it), or maybe if the scales were initialized in a defined order (maybe alphabetic (r, x, y, z), or set r last (x, y, z, r). Or maybe the function calls are calls after all the non-function calls (xRange={[0, 100] is setup before zRange={() => ...}). With a defined order some of the above examples might work.

Thoughts?

Issue when swapping chart data

I then moved to support switching between stacked and grouped examples and ran into an issue. If you check out this REPL, you can manually change layout in App.svelte on line 16 and either works, but trying to change it dynamically with the radio inputs causes errors. It appears data / flatData are responsive but within x={d => ...} it's still getting the previous data values (see around line 60 with the console.log). I could restructure chartData to contain both stackData and groupData as keys (chartData[layout]....} but that complicates the accessors and such even more.

I'm still thinking what data structure I might build to support both use cases (or maybe move the stack / group data building to within ColumnGrouped / ColumnStacked and pass in the original data, but not sure that will work great with the accessors, axises, tooltips, etcs.

I was working to port this Stacked-to-Grouped Bars d3 example with transitions and such but hit this road block.

I also plan to create a Grouped Stacked Bar Chart example as well, but was working thorough the 2 items above first before going deeper down the rabbit hole.

As always, thanks for all your help.

@techniq techniq reopened this Jun 29, 2021
@techniq
Copy link
Contributor Author

techniq commented Jun 29, 2021

@mhkeller I ended up feeling out another approach where I pass in the original data to LayerCake and then handle the specific stack/group data management directly within ColumnStacked and ColumnGrouped. This feels cleaner but with a few tradeoffs:

  • Still need to construct flatten versions of stackData and groupData within App to calculate extents for scales/axis/etc. I could make this a little cleaner by exposing a module-level function exports within ColumnStacked and ColumnGrouped that returns the extents of that data, but I think it would still need to generate the data twice since module-level can not access Svelte context. Still exploring / refining the approach.
  • The accessors (y={...}, r={...}) turn into identity functions as the determination for data is done at usage (basically using $yGet() vs $yScale() is the same. This isn't terrible, but feels a little wrong. I wonder too if the identity function should be the defaults for these accessors

I'm still refining / feeling out this approach. I'm not sure how it will work with tooltips and such, but it seems like it might even be cleaner since the data doesn't change (just the visual representation). I'm also considering taking this a step forward and having these layouts be consolidated into a single component so the control of the bars/rects are shared, to allow transitions between the states, etc. Maybe something like...

<LayerCake ...>
  <Layout stacked let:bars>
    {#each bars as bar}
      <rect {...bar} />
    {/each}
  <Layout>
</LayerCake>

@mhkeller
Copy link
Owner

Thanks for doing such a thorough review of what you've been doing. I want to go more deeply into what you've done so far and definitely take a look at that old data issue. One initial thought on the initialization problem is I think it might be tricky since your second scale would need to be initialized after an onMount right? Would on:mount that just exposed an optional onMount callback inside the LayerCake.svelte component do the trick? I think I need to step through all of what you've done though to see if there is a more elegant solution. But also, what you've done seems fine even if it's not the most concise putting some of that scale logic inside a component and not at the top level – these types of charts are pretty complicated so it's not surprising that nested scale logic doesn't easily fit the mental model.

I really like the idea you have here in your second post of putting some kind of data transform in a component and then exposing that transform via a let. Maybe we have two different ideas but I wonder if that transform makes more sense as a parent to the <LayerCake> component and then you would pass the transformed data in the normal way. I could see those being handy functions you could to do avoid the boilerplate of using d3.stack() yourself.

@techniq
Copy link
Contributor Author

techniq commented Jun 30, 2021

I made more progress towards replicating the Stacked-to-Grouped Bars d3 example by consolidating the ColumnStacked and ColumnGrouped components into a single Bar component and building a data structure up front so it can be iterated and keyed in the same {#each} regardless of the layout, which allows for keyed Rect components to be tweened (single they won't be unmounted / recreated). Need to play around with adding a staggered delay based on the group index to truly match the d3 example, but I'm pretty happy with how this is coming along.

I still plan to feel out a combined grouped and stack layout (as I need an additional dimension to visualize in some cases), but all in all, I'm liking how it's coming along. I plan to integrate it into my project with a common Tooltip/Axis/etc and see how it all fits together. I might also experiment creating layout components like I mentioned earlier (which are similar to visx and Victory), but not sure its worth it (and it seems like it will break the tween capabilities from a single keyed iteration, and don't know if something like crossfade would even work with svg to allowed keyed components from different iterations).

Anyways, I'll keep posting anymore exploration / progress here if you want (and figure adding a Grouped and/or Grouped/Stacked example to LayerCake would be useful).

One of the next areas I plan to explore after this is hierarchy components such as these visx examples I worked on a while back. I know pancake has a nice zoomable treemap example that shouldn't be too hard to port, as a starter.

@techniq
Copy link
Contributor Author

techniq commented Jul 7, 2021

Iterated more in the past week. Finally starting to figure out the data structure to make grouped & stacked work at the same time (see 3c+), but still fighting through some issues/regressions on each iteration.

  • Grouped and Stacked column - Single Animated Bar (take 3a)
    • Starts with wide/stack-friendly data, pivots to group or stack version of chart data based on layout
    • Supports Grouped, Stacked, Seperated (ie. stacked with small multiples offset), or Percent (i.e. stacked with stackOffsetExpand offset)
    • Single data array to iterate when enables transitioning between states
  • Grouped and Stacked column - Single Animated Bar - Flat data (take 3b)
    • Starts with tall data and pivots to stack-friendly data
    • Mostly a stop-gap solution from 3a to 3c
  • Grouped and Stacked column - Single Animated Bar - Flat data (take 3c)
    • Supports Grouped and Stacked at the same time
    • Refined API by passing groupBy (string[]) and stackBy (string) props to Bar instead of generic keys
    • TODO: Needs to calculate extents (hard coded)
    • Regression: Separate no longer aligned across groups
    • TODO: Transitions need refined based on before/after layout
  • Grouped and Stacked column - Single Animated Bar - Flat data (take 3d)
    • Attempted to pass data (i.e. pivoted / processed chartData) and flatData (i.e. tall source data) to LayerCake instead of building in Bar in hopes to calculate extents
    • More of the same issues/regressions as in 3c, but a lot of the transitions have issues
      • From Stacked to Grouped (delays are wrong)
      • from/to Grouped & Stacked
      • Maybe due to replacing data / flatData like I mentioned earlier in the thread.

I'll keep tinkering as I get more time, but thought I'd share my progress in case you have any thoughts / improvements. End goal is to support Bar, Stacked Bar, Grouped Bar, and Grouped and Stacked Bar, and variations using different stacked offsets. I'm getting pretty close if you ignore some of the transitions, and still trying to find the right API. I go back and forth with whether the data built for the chart should be done at the start and passed to Bar vs Bar doing the work itself. Mostly with how it will be consumed in other components that read it from the context (Tooltips, etc).

I have a lot of shared components I've built at this point that I might look into open sourcing at some point (considering calling it Layer Charts) to provide a high level of abstraction above LayerCake, but can always "break glass" and add your own layers, etc.

@mhkeller
Copy link
Owner

It's looking pretty neat! I think even when I was doing a lot of D3 charts, I didn't dabble much in transition between chart types so you're probably much more expert at this than I am. Having some higher-level components would be really neat and I'd be happy to take a look when you publish them!

@techniq
Copy link
Contributor Author

techniq commented Jul 13, 2021

Any ideas why swapping our data and flatData can give odd results?

@mhkeller
Copy link
Owner

mhkeller commented Jul 13, 2021 via email

@techniq
Copy link
Contributor Author

techniq commented Dec 9, 2021

I've been implementing LayerChart as a reusable component library built upon LayerCake, and it has examples for Grouped/Stacked bar charts (and related variants).

@techniq techniq closed this as completed Dec 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Talk about implementation of different chart types.
Projects
None yet
Development

No branches or pull requests

2 participants