HUIJZER.XYZ

Plotting a CSV file with Typst and CeTZ-Plot

2025-03-18

Whenever I need to plot some data, I usually prefer to have a tool that

gnuplot and matplotlib are popular choices, but I personally don't like the appearance of gnuplot and I usually am not so happy with Python's large amount of dependencies.

For quick plotting, I recently discovered CeTZ-Plot. It's a plotting library inside Typst. Typst is a modern alternative to LaTeX, so it is meant to create full documents, but it's also quite easy to use it to create images.

To do this, you only need to have Typst installed. The easiest way to do this is to have the Rust toolchain installed on your system and then run:

$ cargo install --locked typst-cli

Or see the official installation instructions. In this blog post, I'm using typst version 0.13.1.

As an example, I created some CSV data with the width and height of some apples and pears.

fruit,height,width
apple,9.33,6.5
pear,12.45,8.4
apple,11.37,7.4
pear,13.31,9.4
apple,10.13,7
pear,12.68,9.0
apple,8.98,6.4
pear,11.21,7.8
apple,11.77,8.3
pear,13.3,9.4
apple,8.36,5.6
pear,12.79,8.6

Next, I loaded the data into Typst and used cetz-plot to plot it. Here, Typst will automatically download the cetz and cetz-plot packages when you run this file.

#import "@preview/cetz:0.3.2": canvas, draw
#import "@preview/cetz-plot:0.1.1": plot

#set page(width: auto, height: auto, margin: 0.5cm)

// Load the data from a CSV file.
#let data = csv("data.csv", row-type: dictionary)

// Store the width and height in separate variables.
// This is used to override the default axis limits.
#let widths = data.map(x => float(x.width))
#let heights = data.map(x => float(x.height))

// Store the data for each fruit in a separate variable.
#let apples = data.filter(x => x.fruit == "apple").map(x => (float(x.width), float(x.height)))
#let pears = data.filter(x => x.fruit == "pear").map(x => (float(x.width), float(x.height)))

// Used to turn the plot into a scatter plot.
#let style = (stroke: none)

#let space = 0.3

#canvas({
  import draw: *
  
  plot.plot(
    legend: "inner-north-west",
    x-label: "Width",
    y-label: "Height",
    // Override the default axis limits.
    x-min: calc.min(..widths) - space,
    x-max: calc.max(..widths) + space,
    y-min: calc.min(..heights) - space,
    y-max: calc.max(..heights) + space,
    x-tick-step: 1,
    y-tick-step: 1,
    // The size of the plot. The page is set to auto so it will automatically
    // scale the page to fit the plot.
    size: (12, 8),
    {
        plot.add(
        pears,
        mark: "o",
        label: "Pear",
        style: style
        )
        plot.add(
            apples,
            mark: "x",
            label: "Apple",
            style: style
        )
    }
  )
})

To create a SVG from this Typst file, you can run:

$ typst compile plot.typ plot.svg

Which gives the following SVG file:

A Typst plot from the CSV file with borders

For development, in VS Code or Cursor, you can use the Tinymist Typst extension to get syntax highlighting, a language server, and live previews. With the extension, you can have the plot open in a preview window to see the changes live. Changes are visible almost instantly. Generating this SVG image took 0.2 seconds according to time.

If you don't want to use VS Code or Cursor, then you can use the typst watch command to automatically compile the file when it is saved. To have live-updates, you can also use a PDF viewer than supports live updates such as TeXShop (works on MacOS) or Okular. For faster reloads, you can also output the pages to SVG or PNG and generate a HTML page that refers to the plots. That should work if you disable the cache in the developer tools.

Then finally, let's show some more variations of the plot. Here is a dark version with the "left" axis style:

// Add same preamble as before (everything before the canvas).

#set page(fill: black)
#set text(fill: white)

#canvas({
  import draw: *
  
  set-style(
    stroke: white,
    axes: (tick: (stroke: white))
  )
  plot.plot(
    size: (12, 8),
    x-label: "Width",
    y-label: "Height",
    axis-style: "left",
    legend: "inner-north-west",
    legend-style: (fill: black, stroke: white),
    x-min: calc.min(..widths) - space,
    x-max: calc.max(..widths) + space,
    y-min: calc.min(..heights) - space,
    y-max: calc.max(..heights) + space,
    x-tick-step: 1,
    y-tick-step: 1,
    {
      plot.add(
        pears,
        mark: "o",
        label: "Pear",
        style: style
      )
      plot.add(
        apples,
        mark: "x",
        label: "Apple",
        style: style
      )
    }
  )
})

A Typst plot from the CSV file with a black background

And one without an axis and with the first two colors from the Wong Color Palette:

// Add same preamble as before (everything before the canvas).

#let wong-blue = rgb(0, 114, 178)
#let wong-orange = rgb(230, 159, 0)

#canvas({
  import draw: *
  
  plot.plot(
    size: (8, 8),
    axis-style: none,
    legend: "inner-north-west",
    {
      plot.add(
        pears,
        mark: "o",
        label: "Pear",
        style: style,
        mark-style: (fill: wong-blue)
      )
      plot.add(
        apples,
        mark: "x",
        label: "Apple",
        style: style,
        mark-style: (stroke: wong-orange)
      )
    }
  )
})

A Typst plot from the CSV file with the first two colors from the Wong Color Palette