layercake icon indicating copy to clipboard operation
layercake copied to clipboard

Tweened scales / bounds

Open techniq opened this issue 2 years ago • 7 comments

LayerChart currently has a Bounds component that uses a fascade over a d3-scale (see motionScale(), which is an abstraction to use either a tweened() or spring() store. See also tweenedScale() and springScale() for more direct implementations.

Anyways, Bounds is used to "zoom" into a hierarchy chart by adjusting the domain and/or range of these scales and tweening to the value. Examples:

  • https://www.layerchart.com/examples/Partition (domain only)
  • https://www.layerchart.com/examples/Treemap (domain only)
  • https://www.layerchart.com/examples/Sunburst (domain and range)

A similar approach can be found on the d3 zoomable-treemap, Pancake's treemap, and a many other d3 zoomable hierarchies.

Pancake's approach is to allow you to pass the bounds as x1, y1, x2, y2 and building a new scale reactively (which is basically what Bounds does).

Anyways, it would be nice if <LayerCake> provided a way to handle this directly, without an extra component / separate scales. This would also allow to use $xGet / $yGet / etc instead of a second pair of scales to get the tweened value.

I've been thinking on this for a while, and don't have a specific approach on adding this to LayerCake yet, and feel like it would be fairly deep reaching. I've considered forking LayerCake instead LayerChart and seeing how it goes. I mostly created this issue to get your thoughts / feedback, and if you've thought of this. I remember when I saw LayerCake's Reactivity Edition I had hopes this might solve this, but it didn't appear to.

This tweet / thread from a few days ago got me thinking it would be best in LayerCake, or at least at the Chart layer.

https://twitter.com/hamiltonulmer/status/1541548191010590720

Semi-related, I've also ran into cases where derived scales based on the width/height would be nice if handled higher up, for example creating an x1Scale based on the xScale's bandwidth, so it might be helpful to think how both of these use cases could be handled if we do implement a breaking change.

techniq avatar Jul 05 '22 21:07 techniq

Interesting. I'll have to fully digest this some more. In a pre-Svelte3 version of Layer Cake, the scales were all created dynamically. There were some differences in stores in Svelte3's syntax though that meant it was hard to do that in the same way, if I'm remembering correctly. Do you have some ideas for what a syntax would be? That would be a helpful starting point.

mhkeller avatar Jul 10 '22 16:07 mhkeller

Is something like this what you were thinking for a derived scale?

<LayerCake
  x1={$xScale => scaleBand().domain(foo).range([0, $xScale.bandwidth()])}

I don't see how Pancake solves this scale abstraction issue with bounds. In that example, it appears to just be passing in a domain as x1 and x2. You can similarly tween a domain of LayerCake scale like this.

mhkeller avatar Jul 10 '22 16:07 mhkeller

For the derived scale (which in hindsight I should have opened a separate issue not not confuse with the tweened scale use case), I was thinking something like that (but would be the x1Scale, or x1Range, etc.

I'm thinking for each current scale (x, y, z, r) we could have 1 "derived" scales (x1, y1, ...) or maybe even 2 or more (x2, y2, ...) that would take in the original scale. Might be best to start with just the one derived per scale, and then see how we would want to handle a second (should x2 be passed x, or x1, or some config of all of them).

So I'm thinking it would look like this (not really testing it out fully)...

<LayerCake
  xScale={scaleBand()}
  x1Range={$xScale => [0, $xScale.bandwidth()])}

I'm thinking each derived scale would by default be the same scale type as the original, so in this case x1Scale={scaleBand()} would be set since xScale={scaleBand()}, but you could also override it (and you might for scaleBand() to set a different paddingInner or something.

techniq avatar Jul 10 '22 23:07 techniq

As for the tweened scale request, if you notice how Pancake implements the extents, they can be passed as tweened/spring stores. For example, have a look at the Treemap example and specifically...

const extents = tweened(undefined, {
  easing: eases.cubicOut,
  duration: 600
});

$: $extents = {
  x1: selected.x0,
  x2: selected.x1,
  y1: selected.y1,
  y2: selected.y0
};

and

<Pancake.Chart x1={$extents.x1} x2={$extents.x2} y1={$extents.y1} y2={$extents.y2}>

Now I'm asking for more than just the extents to be tweenable, but actually the scales and their domain and range.

I just had a thought (and thus not super thought out or tested), but currently you wrap all the props passed to the LayerCake component in a writable store. What if we check if the value passed was already a store, and just used it (so you could pass a value as tweened() or spring()? This may not work in all use a cases, or at all :).

techniq avatar Jul 10 '22 23:07 techniq

For the derived stuff, I wonder what kind of reactivity issues there are with declaring stores and stuff like that dynamically. Since each $: needs to be top level and all that. If there is a way to do more dynamic stuff with svelte, I would probably refactor a lot of the code base so work off of lists – but I think there may be some reactivity hit you get where svelte doesn't know all the detail about what you get. But interesting to look into to see if there's an implementation that works! I think there's an issue like that around dynamic props too I think. So probably best if you declared like x1, x2, x3 in the library. I wonder how many you would go up to?

For the tweens, what about the current example using a tweened domain is lacking? I haven't done that much tweens in svelte so I may be a bit behind...

mhkeller avatar Jul 11 '22 02:07 mhkeller

Regarding the derived stuff, it might not be worth making them dynamic but instead just expanding the current x, y, z, a r scales to also have x1, y1, z1, and r1?

For tweens, I assume you're referring to the Small multiples. I remember looking at this before, and can't remember why I thought this approach wouldn't work. I might need to revisit. I currently have a component called Bounds (which is badly named and likely to change) which is a wrapper around a scale and allows you to change the domain or range and tween accordingly. It's just on a lot of the hierarchy charts (ex. Treemap).

By tweening at the chart context level, I'm hoping everything animates "automatically" like axis ticks, marks, etc. Currently each mark (ex. Rect and Circle) animates themselves, which works great, and is still needed for more flexibility like staggering different changes.

techniq avatar Jul 11 '22 13:07 techniq

Yeah in the small multiples example, it tweens the domain and everything else then changes. You could change the scale from the context directly but there are some downstream effects you will lose if you do that such as domain padding. I think it would be better to tween the input props to layercake itself so that everything still properly cascades.

mhkeller avatar Jul 21 '22 19:07 mhkeller

I'll close this one for now @techniq since perhaps the existing method of tweening works? For the derived scales, if you still think that is a good idea, let's create a new issue. I'm curious what the use case is. I also wonder about overhead. If you're not using them but the library is creating those extra elements each time, does that affect how much code gets loaded?

mhkeller avatar Sep 06 '22 21:09 mhkeller

Sounds good. TBH I haven't had time to investigate further since the initial discussion. Once I am, I'll report back.

techniq avatar Sep 07 '22 04:09 techniq