Observable Plot + Svelte = SveltePlot?
Ok, before anyone gets too excited, there is nothing here, yet š . The goal of this blog post is to share an idea Iāve been pondering for the past months in order to start a discussion. For now, SveltePlot is nothing more than an experiment.
Alright, so what is the idea? Remember how last year I wrote a review about Observable Plot? I liked it a lot, and Iāve been using it steadily since. But Iām not using it inside Observable notebooks ā where itās supposed to be used ā but mostly in Svelte projects. This works using a wrapper component, but a few pain points remain.
What is Plot?
But before we dive into this, letās quickly summarize the basic concept of a plot. Feel free to skip this section if youāre already familiar with Observable Plot.
- Plots are made of marks that can be stacked on top of each other. The library comes with a huge set of ready-to-use marks, that display data, for instance, the dot or line marks.
- To customize the marks, the user defines how the provided data maps to the mark channels, such as
x
,y1
,fill
orstroke
. Each mark comes with its own set of channels although a few channels are universal, such asopacity
. - The channels then get mapped to shared scales. So the
fill
andstroke
channels will be mapped to thecolor
scale. This means you donāt have to map thefill
channel to color values directly but Plot will automatically try to use a meaningful color scale, depending on the data values you mapped to the channel.
The impressive range of marks and the automatic mapping to shared scales are among the best features of Plot. It allows creating a plot with a super minimal code footprint. Please make sure to check out the official introduction as well.
Why SveltePlot?
My biggest problem with Plot is that itās internally written using a lot of d3.select().append()
and thus follows a fire-and-forget logic: You call Plot.plot()
with your configuration, and it returns an SVG element with the chart. After that, the contents of the plot remain a black box that is hard to do anything with, other than re-rendering the whole thing.
Svelte is a great framework for interactive visualizations, and arguably, itās the reason why it was created in the first place. In Svelte (like other reactive frameworks), interactive applications are broken down into stateful components. Once the state changes, the component updates its DOM, etc.
Now imagine we had a <Plot />
component inside which we could add our mark components. So instead of this:
Plot.plot({
title: 'Apple stock',
marks: [Plot.line(aapl, { x: 'Date', y: 'Close' })]
})
ā¦we would write this
<Plot title="Apple stock">
<Line data={aapl} x="Date" y="Close" />
</Plot>
and get a nice line chart in return:
In this example, the title
is the state of the Plot
component, and the data and x and y channel accessors for the line mark are just the state of the Line
component that we pass on as props. Once we update them, the line should re-render, without other parts of the plot having to re-render as well.
If we need multiple marks, we just define multiple components, like this:
<Plot grid>
<Area data={aapl} x1="Date" y1={0} y2="Close" opacity={0.25} />
<Line data={aapl} x="Date" y="Close" />
<RuleY data={[0]} />
</Plot>
And the marks are layered on top of each other in the order we defined them:
Of course, since weāre writing Svelte code here, we could just throw any SVG code into the plot body! If we wanted, we could wrap the line mark in a separate <g>
group, or put a watermark behind, etc. (Btw, if you want, you can play around with these examples on StackBlitz.)
And, of course, the Plot being declared in Svelte means we can add event handlers to individual marks (and SveltePlot passes them on to the <rect>
SVG elements for us)!
<Plot title={clicked ? 'Click the bars' : `You clicked ${d}`}>
<BarY
data={[-2,-1,2,4,6,9,5]}
onclick={(d) => clicked = d}
opacity={(d) => (!clicked || clicked === d ? 1 : 0.5)} />
</Plot>
Another roadblock I was running into when using Plot are the built-in tooltips. They are kind of cute, but not very easy to customize. You can let them show custom (unformatted) text, but thatās it.
In Svelte, Iād love to provide my own tooltip code as custom component or slot (or soon, snippet). And it turns out, itās not that hard! We can even use HTML tooltips, if we wanted.
<Plot>
<Dot data={penguins} x="culmen_length_mm" y="culmen_depth_mm" />
{#snippet overlay()}
<!-- this is placed outside the <svg> root -->
<HTMLTooltip
data={penguins}
x="culmen_length_mm"
y="culmen_depth_mm"
let:datum>
<!-- tooltip content here -->
{datum.species}
</HTMLTooltip>
{/snippet}
</Plot>
What about transforms?
So far we only talked about the marks, channels, and scales, but Plot also comes with so-called transforms that allow to reshape a dataset to fit the needs of the plot marks.
In Plot, transforms are sort of magic functions1 that you throw into the Plot configuration and they will change the data and channel accessors for you. Hereās a simple example of the stackY
transform that calculates stacking offsets for each year.
Plot.area(sales, Plot.stackY({
x: "year",
y: "revenue",
z: "group"
}))
This groups the data and turns the single y
channel into an y1
and y2
channel for the lower and upper revenue bounds which are then visualized as stacked area paths.
So what transforms are doing is modifying the dataset and the channel mapping, and all we need is a function that takes { data, ...channels }
as the first argument and returns the result as { data, ...channels }
. In SveltePlot, this would work similarly, thanks to Svelteās props spreading operator.
<Plot title="Stack transform" color={{ legend: true }}>
<Area fill="group" {...stackY({
data,
x: 'year',
y: 'revenue',
z: 'format'
})} />
</Plot>
ā¦et voilĆ !
Of course, like Observable Plot, SveltePlot would also support implicit transforms, since stacking areas on top of each other is more common than not stacking them. So the previous example could also be simplified as
<Plot title="Stack transform" color={{ legend: true }}>
<AreaY {data} x="year" y="revenue" z="format" fill="group" />
</Plot>
But do we really need yet another visualization framework?
I think, SveltePlot would be a pure joy to use, but do we really need yet another Svelte-based visualization framework? Donāt we have a few already? Letās take a look at two examples UnoVis and LayerCake2.
Yes, at first glance the syntax looks a bit similar but thereās a crucial difference! In both LayerCake and UnoVis the data is defined once for the entire chart and then shared between all layers. This is what a typical3 bar chart in LayerCake looks like:
<LayerCake {data} x="value" y="year" yScale={scaleBand()}>
<Svg>
<AxisX gridlines baseline snapTicks />
<AxisY gridlines={false} />
<Bar/>
</Svg>
</LayerCake>
In UnoVis, it would look something like this:
<VisXYContainer {data}>
<VisStackedBar x={(d) => d.value} y={(d) => d.year} />
<VisAxis type="x"/>
<VisAxis type="y"/>
</VisXYContainer>
In SveltePlot, like Observable Plot, the data is defined per mark. So different marks (read layers) can show different datasets while still sharing the same scales. This is very useful if you want to use marks for annotations, like in the next example we would add vertical rules at the values 1000 and 4000:
<Plot y={{ type: 'band' }}>
<BarX {data} x="value" y="year" />
<RuleX data=[{1000,4000}] />
</Plot>
Another difference between SveltePlot and existing frameworks would be the smart defaults we have in Observable Plot. The main idea is to make it easy to create plots. Why force the users to import and add a VisAxes
component every time they need axes in a chart (which is almost always, right)? Plot and SveltePlot would add these marks automatically. To enable a grid, you just write <Plot grid>
and SveltePlot will add the GridX
and GridY
marks for you.
Alright, Iām sold! When can I use it???
Iām happy you like the idea as much as I do. But as I wrote above, at this point SveltePlot is just an idea with a very early prototype right now. You can play around with the examples on StackBlitz, but before this gets anywhere near production, we need to talk about a few challenges.
First of all, Observable Plot is a huge framework with tons of features and smart defaults built into it. Porting all of this over to Svelte is going to take a while!
Also, itās worth keeping in mind that Svelte still feels like a young framework that is regularly embracing new ideas and making major changes to its core. Or, as Rich Harris put it in his recent talk:
When other frameworks introduce new ideas, like Signals or server components, we look at them with interest and jealousy, and try to work out how we can incorporate the good ideas instead of resting on our laurels.
That means whoever takes on implementing a library of this size needs to be prepared to rewrite major parts every other year or so. At the same time, older Svelte versions need to be supported as well to increase adoption of SveltePlot.
The prototype Iāve created for this blog post is based on Svelte 5 (mainly because I wanted to use this opportunity to learn it). But it may make sense to invest in a Svelte 4 version, too, to make SveltePlot easier to use for developers who canāt just migrate their stacks to Svelte 5 right after its release.
Finally, Observable Plot not only comes with an impressive set of features; but also includes very detailed documentation with hundreds of example charts. This would have to be done for SveltePlot as well.
All this makes me think that this project is far too big for a single developer, so pleaseā¦
Get in touch!
As I said, the main purpose of this blog post is to try to find out who else would be interested in building a framework like SveltePlot, because itās impossible to do it alone.
If you think this is a stupid idea, or you know that thereās already a much better solution, or if you feel crazy enough to work on a project of this size, āļø contact me or leave a comment below.
- I still donāt fully understand how Plot transforms work, as the data is not even passed to the transform functionsā¦ā©
- Apologies for skipping Richās very own Pancake framework hereā©
- The author of LayerCake asked me to clarify that LayerCake itself doesnāt include any layer components like
AxisX
orBar
. So users can create their own components that accept independent data. Hereās a REPL he provided that shows how this could look like. However, you would still need to provide a āmergedā dataset to the LayerCake component to make sure the scale extent fits all layer data sources, or alternatively manually compute the scale domains.ā©