Examples: basic barchart

Author

James Goldie

Published

May 14, 2024

Let’s start with a simple barchart.

This isn’t going to be one of those barcharts you’ve seen on TV, what with tick marks and hover effects and such. This one’s just some bars, some labels and a baseline. It will, however, come with some useful features to help us reuse it.

If you’d like to see how the bar chart works internally, the code for it is shown at the bottom of this page.

But for the purposes of this example, all you need to know about is the chart’s props: updateable options to customise the chart. We’ll cover those in step 2.

Step 1: initialise

First, we need to add Sverto and the bar chart in our document frontmatter:

---
filters:
  - sverto
sverto:
  use:
    - BarChart.svelte
---

Then, the following OJS code initialises our bar chart.

```{ojs}
//| echo: fenced
myBarChart = new BarChart.default({
  target: document.querySelector("#mybarchart")
})
```

We use the name of the Svelte file (without the file extension), then .default(), to create the component. So for BarChart.svelte, we use BarChart.default(). This function is called a constructor.

The constructor needs at least one thing to, uhh, construct the chart: a target, or place to put the chart. You can use document.querySelector() to tell it a place that will exist in the document, and then create that place like this:

:::{#mybarchart}
:::

We made this barchart with Svelte!

Sverto will put the chart inside it.

We’ve also named our chart myBarChart. This is important so that we can customise it!

Step 2: react

The real power of Sverto is that we can customise aspects of our charts and have them update in response to other things.

In Svelte, these customisable options are called props.

This Svelte component accepts a few props:

  • data: a simple array of (up to) 5 numbers
  • height: the height of the chart in pixels
  • width: the width of the chart in pixels
  • barWidth: the width of each bar
  • barColor: the colour of the bars and labels (note the US spelling here) in res

To specify props, use the name of the constructed chart (myBarChart, remember?), then the name of the prop.

Those props can be fixed and unchanging:

```{ojs}
//| echo: fenced
myBarChart.width = 200
```

But with OJS, we can also tie them to things that change, like our data!

We can update any of those values up to OJS code using myBarChart.propName.

Let’s make some controls so that users can animate the chart’s data and colour themselves:

Code
```{ojs}
//| echo: fenced
//| code-fold: true

viewof userData = Inputs.form(
  [
    Inputs.text({type: "number", value: 25, width: 20}),
    Inputs.text({type: "number", value: 35, width: 20}),
    Inputs.text({type: "number", value: 65, width: 20}),
    Inputs.text({type: "number", value: 5, width: 20}),
    Inputs.text({type: "number", value: 50, width: 20})
  ]);

viewof userColor = Inputs.color({ value: "#36a7e9" });
```

Now, we update the props to the value of these controls:

```{ojs}
//| echo: fenced
myBarChart.data = [...userData];
myBarChart.barColor =  userColor;
```

And we’re done!

What about the other props?

Weren’t there other props, like height and barWidth?

Yup! The props in this Svelte component (and many that you’ll use) have default values that are used if you don’t provide one.

You can also include default props as an option when you create the chart, but I generally find it simpler and easier to keep the props away from the constructor.

Summary

How did we get this chart going again?

  1. Add filters: ["sverto"] to our frontmatter, plus the name of our Svelte file to sverto.use
  2. Created the chart with myBarChart = new BarChart.default(), giving it a target to place the chart in
  3. We created the target with a Pandoc div: :::{#mybarchart}
  4. We customised the bar chart by assigning to myBarChart.propName

Challenges

Here’s the code in the Svelte file:

<script>
  /* let's borrow svelte's fly transitions for the circles that need to be
     created or destroyed
     https://svelte.dev/tutorial/transition */
  import { fly } from "svelte/transition";

  /* these are our props and their default values (starting with `export let`).
     in particular, note that data is an array of numbers
     https://svelte.dev/tutorial/declaring-props */
  export let data =  [50, 45, 15, 25, 30];
  export let height = 100;
  export let width = 400;
  export let barWidth = 25;
  export let barColor = "black"

  /* unlike ojs, svelte code isn't "naturally" reactive (except down below in
     the rendered html or svg), but you can prefix a statement with `$:` to
     make it reactive (so it runs every time `data` changes).
     here we're going to normalise our data to 100, and we'll use the normalised
     data in our barchart instead
     https://svelte.dev/tutorial/reactive-statements */
  $: normalisedData = data.map(d => ({
    y: d,
    h: d / Math.max(...data) * height
  }));
  $: console.log(normalisedData);

</script>

<!-- these css styles just apply to our component -->
<style>
  text {
    font-size: smaller;
  }
</style>

<!-- this bar chart is an svg that just includes bars and labels, with a
     single, unlabelled baseline.
     it does, however, use the `height`, `width` and `barWidth`
     props to scale the element sizes, so this can be dynamically
     resized easily. it also uses the `normalisedData`, which is recalculated
     every time the `data` prop changes because it starts with `$:` above -->

<svg width={width} height={height}>
  <!-- for each data element, draw a rectangle and a label -->
  {#each normalisedData as d, i (i)}
  <rect
    in:fly="{{x: 10}}" out:fly="{{x: 10}}"
    style={"transition: all 1s ease-in-out"}
    x="{width / 6 * (i + 0.5) - (barWidth / 2)}px"
    y="{height - d.h}px"
    width="{barWidth}px"
    height="{d.h}px"
    fill="{barColor}"
    >
  </rect>
    <!-- place label either above or below bar,
         depending on its height -->
    <text
      in:fly="{{x: 10}}" out:fly="{{x: 10}}"
      style={"transition: all 1s ease-in-out"}
      text-anchor="middle"
      x="{width / 6 * (i + 0.5)}px"
      y="{d.h > 35 ?
        height - d.h + 16 :
        height - d.h - 6}px"
      fill="{d.h > 35 ? "white" : barColor}"
    >{d.y}</text>
  {/each}
  <!-- and a single x axis baseline -->
  <line x1="0" x2="{width * 5 / 6}" y1="{height}" y2="{height}" stroke="black"></line>
</svg>

If you’d like to start practising your Svelte, start with the official tutorial. Sverto is designed to make using Svelte components in Quarto as easy as working in the tutorial.

This Svelte component’s pretty basic, though. What else is it missing?

Resizing

The height and the width of the chart are configurable using props, and the bars resize in response to them, but the CSS transitions that grow and shrink them are slow to catch up.

Ideally we’d turn those transitions off when the chart as a whole is resizing!

Other barchart elements

We have no axes other than the baseline. That’s fine for a lot of uses, but we might want to add those elements for other uses.

We could add those elements manually, but the d3-axis package has some helpers for creating axes quickly!

Colour scales

The bars are all the same colour. We could write a function that converts each bar’s data value to a colour, and use it for the fill attribute of the <rect>, but the d3-scale-chromatic also has some helpers to do this quickly!

d3 is included with OJS, but if you want to use d3-scale-chromatic (or any other part of d3) in your Svelte components, you’ll have to add it yourself by:

  • running npm install d3-scale-chromatic in the terminal, then
  • adding import XXXX from "d3-scale-chromatic", where XXXX is the name of the thing you want to import (or *).

A more complex example

If you’d like to see an example that addresses some of these shortcomings, check out the time series chart example, which automatically resizes and adds axes that transition!

See the time series chart →