vis4.net

Hi, I'm Gregor, welcome to my blog where I mostly write about data visualization, cartography, colors, data journalism and some of my open source software projects.

Observable Plot + Svelte = SveltePlot?

#datavis#svelte#opensource

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 or stroke. Each mark comes with its own set of channels although a few channels are universal, such as opacity.
  • The channels then get mapped to shared scales. So the fill and stroke channels will be mapped to the color scale. This means you don’t have to map the fill 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.


  1. I still don’t fully understand how Plot transforms work, as the data is not even passed to the transform functions…
  2. Apologies for skipping Rich’s very own Pancake framework here
  3. The author of LayerCake asked me to clarify that LayerCake itself doesn’t include any layer components like AxisX or Bar. 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.