I’m trying to employ d3-axis
in a Svelte dashboard I’m working on. But I don’t understand how to “extract” a <g>
element out of the axis
object that I can add to my <svg>
element. I wanted to explore this as alternative to creating it manually, like I did for my time axis.
import { scaleLinear } from 'd3-scale';
import { axisLeft} from 'd3-axis';
... snip...
const countAxis = scaleLinear().domain([0, maxCount]).range([0, baseline - margin.top]);
const axis = axisLeft(countAxis);
</script>
<svg height="{chartSize.height}" width="{chartSize.width}">
<line x1="{margin.left}" y1="{baseline}"
x2="{margin.right}" y2="{baseline}" stroke="grey" />
{#each timeTicks as tick}
<line x1="{tick[0]}" y1="{baseline}" x2="{tick[0]}" y2="{baseline+8}" stroke="grey"/>
<text x="{tick[0]}" y="{margin.bottom}"
text-anchor="middle" dominant-baseline="bottom" stroke="grey"> {tick[1]} </text>
{/each}
{axis} // How do I make this into a <g> that I can transform?
...
</svg>
The D3 API provides a sample using the call function.
d3.append('g').call(axis);
But it wasn’t clear to me how this maps to an inline inclusion.
I think you could do roughly the same thing Mike does in the first bar chart here, which is a pattern I hadn’t seen before: create a G element, call the axis on it, and then interpolate it into some templated HTML — in that case, using HTL:
htl.html`<svg viewBox="0 0 ${width} ${height}" style="max-width: ${width}px; font: 10px sans-serif;">
${d3.select(htl.svg`<g transform="translate(${margin.left},0)">`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
.node()}
</svg>`
That’s not Svelte, though. This is pretty much the first Svelte I’ve ever written lol (it’s great, I know!), so there may be a better way, but I think you could say:
const axis = d3.create("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(countAxis))
And then, in the SVG:
<svg>
{@html axis.node().outerHTML}
</svg>
The steps here are:
-
d3.axisLeft(countAxis)
returns a function that takes a D3 selection of a G element and draws an axis in it
-
d3.create("g")
creates a D3 selection of a G element (detached, not yet inserted into the DOM)
- We transform that G element to give it a margin and call d3.axisLeft on it
- Then, in the HTML, we call
.node()
on it to get a DOM node instead of a D3 selection, and then call .outerHTML
on it to get a string of HTML, which we interpolate into Svelte using @html
Here’s an example in the Svelte repl, forking a D3 Svelte example Mike made, with updating on mousemove.
Does Svelte have a way to interpolate DOM elements into the the HTML? That’d probably be better than stringifying the axis with .outerHTML
.
Some other approaches:
My preference is to use bind:this
and then a reactive statement $:
to apply the axis:
<script>
import * as d3 from 'd3';
export let data;
export let width = 640;
export let height = 400;
export let marginTop = 20;
export let marginRight = 20;
export let marginBottom = 30;
export let marginLeft = 40;
let gx;
let gy;
$: x = d3.scaleLinear([0, data.length - 1], [marginLeft, width - marginRight]);
$: y = d3.scaleLinear(d3.extent(data), [height - marginBottom, marginTop]);
$: line = d3.line((d, i) => x(i), y);
$: d3.select(gy).call(d3.axisLeft(y));
$: d3.select(gx).call(d3.axisBottom(x));
</script>
<svg width={width} height={height}>
<g bind:this={gx} transform="translate(0,{height - marginBottom})" />
<g bind:this={gy} transform="translate({marginLeft},0)" />
<path fill="none" stroke="currentColor" stroke-width="1.5" d={line(data)} />
<g fill="white" stroke="currentColor" stroke-width="1.5">
{#each data as d, i}
<circle key={i} cx={x(i)} cy={y(d)} r="2.5" />
{/each}
</g>
</svg>
Repl: D3 + Svelte (Axes) • REPL • Svelte
4 Likes
This is great. It really should work for me (it obviously works in the Svelte REPL link you included). But I seem to have a problem with the @html
tag. If I include
<svg ...>
{axis.node().outerHTML}
</svg>
I see the node tree as a big string containing the nodes I expect. It doesn’t render; but I can see everything I want in the inspector quoted as a string.
If I change to
<svg ...>
{@html axis.node().outerHTML}
</svg>
there is nothing; not even in the inspector.
Mike’s post below with node binding is working for me for now. But I’ll keep digging. I don’t want to take up your time. I really appreciate the example and the REPL link.
This works great, thanks, Mike! It also feels like a very Svelte-y way to do it.
1 Like