Generate static png or svg at build time with Framework

In my Framework project I have a javascript function that returns an Observable Plot object. I would like to run the function during build to produce a static png or svg that I can then display on the dashboard at load time. Is a data loader the way to go here? If so how is this done? The docs (Data loaders | Observable Framework) seems to mention this, but has no examples.

Thanks!

Sure! Here’s an example that builds a chart with Plot (in “server-side rendering” mode, using linkedom instead of the browser DOM).

First, add the necessary packages to your project:

yarn add d3 @observablehq/plot linkedom

Then create the file:

import * as Plot from "@observablehq/plot";
import * as d3 from "d3";
import {parseHTML} from "linkedom";

const barley = await d3.csv(
  "https://raw.githubusercontent.com/observablehq/plot/main/test/data/barley.csv",
  d3.autoType
);

function beckerBarley() {
  const {document} = parseHTML("<a>");
  const chart = Plot.plot({
    document,
    marginLeft: 110,
    height: 800,
    grid: true,
    x: {nice: true},
    y: {inset: 5},
    color: {type: "categorical"},
    facet: {marginRight: 90},
    marks: [
      Plot.frame(),
      Plot.dot(barley, {
        x: "yield",
        y: "variety",
        fy: "site",
        stroke: "year",
        sort: {fy: "x", y: "x", reduce: "median", reverse: true}
      })
    ]
  });
  return `${chart}`.replace(
    /^<svg /,
    '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" '
  );
}

process.stdout.write(beckerBarley());

This data loader is written in JavaScript and emits svg, it needs a double extension .svg.js (save it under becker.svg.js).

In a page named becker.md, call it like this:

```js
display(await FileAttachment("becker.svg").image());
```

You should then see:

To output png instead of svg, you need a supplementary step. We’re going to use resvg:

yarn add  @resvg/resvg-j

The data loader must be adapted a little:

import * as Plot from "@observablehq/plot";
import * as d3 from "d3";
import {parseHTML} from "linkedom";
import {Resvg} from "@resvg/resvg-js";

const barley = await d3.csv(
  "https://raw.githubusercontent.com/observablehq/plot/main/test/data/barley.csv",
  d3.autoType
);

function beckerBarley() {
  const {document} = parseHTML("<a>");
  const chart = Plot.plot({
    document,
    marginLeft: 110,
    height: 800,
    grid: true,
    x: {nice: true},
    y: {inset: 5},
    color: {type: "categorical"},
    facet: {marginRight: 90},
    marks: [
      Plot.frame(),
      Plot.dot(barley, {
        x: "yield",
        y: "variety",
        fy: "site",
        stroke: "year",
        sort: {fy: "x", y: "x", reduce: "median", reverse: true}
      })
    ]
  });
  chart.setAttribute("xmlns", "http://www.w3.org/2000/svg");

  return new Resvg(chart.outerHTML, {
    fitTo: {
      mode: "width",
      value: 2 * chart.getAttribute("width")
    }
  })
    .render()
    .asPng();
}

process.stdout.write(beckerBarley());

Save this as becker.png.js then use:

```js
display(await FileAttachment("becker.png").image({style: "max-width: 100%;"}));
```

I understand that this is a lot of code, and we’ll want to make helper functions to make this easier. But… it works!

3 Likes

Thanks!

Was at first baffled by the http links, but I have now learnt about xml namespaces!

1 Like