Use d3-axis in Svelte application

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