Getting started

Install

Node.js 22 or newer. In an existing SvelteKit (or Svelte) app:

bun add @ggsvelte/svelte

Start with a basic plot

Svelte
<script lang="ts">
  import { GeomPoint, GGPlot, Labs, ScaleXContinuous, ScaleYMonthDay } from "@ggsvelte/svelte";
  import { kyotoSakura } from "@ggsvelte/svelte/data";
</script>

<GGPlot
  data={kyotoSakura}
  aes={{ x: "year", y: "bloomDate" }}
>
  <ScaleYMonthDay reverse />
  <ScaleXContinuous labels="d" domain={[800, 2030]} />
  <Labs x="Year" y="Bloom date" />
  <GeomPoint />
</GGPlot>
Output
Peak cherry-blossom dates in Kyoto, 812 to 2026, as a scatter

Add layers

Pick a minimal theme and add a rolling median line

<ThemeTufte />
<ScaleYMonthDay
  reverse
  breaks={["04-05", "04-15", "04-25"]}
  dateLabels="%b %e"
  domain={["05-10", "03-10"]}
/>
<GeomPoint alpha={0.5} size={1.6}
  aes={{ color: { value: "#777777" } }} />
<GeomLine stat="summary_bin" fun="median" binwidth={25}
  curve="step-hv" linewidth={1.8}
  aes={{ color: { value: "#262626" } }} />
Kyoto cherry blossom after step 1: Pick a minimal theme and add a rolling median line

Add epochs

<GeomRect
  data={epochs}
  aes={{
    x: null, y: null,
    xmin: "year", xmax: "until", ymin: "top", ymax: "bottom",
    fill: "epoch",
  }}
  alpha={0.55}
  inspect={false}
/>
<GeomText data={epochNames}
  aes={{ x: "midYear", y: "nameDate", label: "epoch",
         color: { value: "#6b7075" } }} size={11} inspect={false} />
<ScaleFillManual
  domain={["Medieval warm period", "Little Ice Age", "Industrial era"]}
  values={["#f5edc4", "#dce8f2", "#f3dcda"]}
/>
<GuideNone channel="fill" />
Kyoto cherry blossom after step 2: Add epochs

Annotate record years

<GeomRule yintercept="04-15" linewidth={1}
  aes={{ color: { value: "#6b7075" }, linetype: { value: "dashed" } }} />
<GeomSegment data={records}
  aes={{ x: "labelYear", y: "labelDate", xend: "year",
         yend: "bloomDate", color: { value: "#b3452f" } }} linewidth={0.7} />
<GeomText data={records}
  aes={{ x: "labelYear", y: "labelDate", label: "label",
         color: { value: "#b3452f" } }} size={11} anchor="end" dx={-4} />
Kyoto cherry blossom after step 3: Annotate record years

Make it interactive

key="year"
inspect={{ mode: "exact", pin: true }}
Kyoto cherry blossom, finished. Called out: 1323 · May 4, latest on record; 1409 · March 27, earliest for six centuries; 2023 · March 25, earliest in 1,200 years.

The finished file

<script lang="ts">
  import {
    GeomLine,
    GeomPoint,
    GeomRect,
    GeomRule,
    GeomSegment,
    GeomText,
    GGPlot,
    GuideNone,
    Labs,
    ScaleFillManual,
    ScaleXContinuous,
    ScaleYMonthDay,
    ThemeTufte,
  } from "@ggsvelte/svelte";
  import { kyotoSakura } from "@ggsvelte/svelte/data";

  // Bands cover every observation; a strip above holds the epoch names.
  const span = { top: "03-18", bottom: "05-10" };
  const epochs = [
    { epoch: "Medieval warm period", year: 950, until: 1250, ...span },
    { epoch: "Little Ice Age", year: 1300, until: 1850, ...span },
    { epoch: "Industrial era", year: 1850, until: 2026, ...span },
  ];
  const epochNames = [
    { epoch: "Medieval warm period", midYear: 1100, nameDate: "03-14" },
    { epoch: "Little Ice Age", midYear: 1575, nameDate: "03-14" },
    { epoch: "Industrial era", midYear: 1938, nameDate: "03-14" },
  ];
  const records = [
    {
      year: 1323, bloomDate: "05-04",
      labelYear: 1250, labelDate: "05-08",
      label: "1323 · May 4, latest on record",
    },
    {
      year: 1409, bloomDate: "03-27",
      labelYear: 1310, labelDate: "03-24",
      label: "1409 · March 27, earliest for six centuries",
    },
    {
      year: 2023, bloomDate: "03-25",
      labelYear: 2010, labelDate: "03-24",
      label: "2023 · March 25, earliest in 1,200 years",
    },
  ];
</script>

<GGPlot
  data={kyotoSakura}
  aes={{ x: "year", y: "bloomDate" }}
  key="year"
  inspect={{ mode: "exact", pin: true }}
>
  <ThemeTufte />
  <ScaleYMonthDay
    reverse
    breaks={["04-05", "04-15", "04-25"]}
    dateLabels="%b %e"
    domain={["05-10", "03-10"]}
  />
  <ScaleXContinuous labels="d" domain={[800, 2030]} />
  <ScaleFillManual
    domain={["Medieval warm period", "Little Ice Age", "Industrial era"]}
    values={["#f5edc4", "#dce8f2", "#f3dcda"]}
  />
  <GuideNone channel="fill" />
  <Labs x="Year" y="Bloom date" />
  <GeomRect
    data={epochs}
    aes={{
      x: null,
      y: null,
      xmin: "year",
      xmax: "until",
      ymin: "top",
      ymax: "bottom",
      fill: "epoch",
    }}
    alpha={0.55}
    inspect={false}
  />
  <GeomText
    data={epochNames}
    aes={{
      x: "midYear",
      y: "nameDate",
      label: "epoch",
      color: { value: "#6b7075" },
    }}
    size={11}
    inspect={false}
  />
  <GeomPoint
    alpha={0.5}
    size={1.6}
    aes={{ color: { value: "#777777" } }}
  />
  <GeomRule
    yintercept="04-15"
    linewidth={1}
    aes={{ color: { value: "#6b7075" }, linetype: { value: "dashed" } }}
  />
  <GeomLine
    stat="summary_bin"
    fun="median"
    binwidth={25}
    curve="step-hv"
    linewidth={1.8}
    aes={{ color: { value: "#262626" } }}
  />
  <GeomSegment
    data={records}
    aes={{
      x: "labelYear",
      y: "labelDate",
      xend: "year",
      yend: "bloomDate",
      color: { value: "#b3452f" },
    }}
    linewidth={0.7}
    alpha={0.9}
  />
  <GeomText
    data={records}
    aes={{
      x: "labelYear",
      y: "labelDate",
      label: "label",
      color: { value: "#b3452f" },
    }}
    size={11}
    anchor="end"
    dx={-4}
  />
</GGPlot>

Agent JSON spec

{
  "data": {
    "name": "kyotoSakura"
  },
  "aes": {
    "x": {
      "field": "year"
    },
    "y": {
      "field": "bloomDate"
    }
  },
  "layers": [
    {
      "geom": "rect",
      "data": {
        "values": [
          {
            "epoch": "Medieval warm period",
            "year": 950,
            "until": 1250,
            "top": "03-18",
            "bottom": "05-10"
          },
          {
            "epoch": "Little Ice Age",
            "year": 1300,
            "until": 1850,
            "top": "03-18",
            "bottom": "05-10"
          },
          {
            "epoch": "Industrial era",
            "year": 1850,
            "until": 2026,
            "top": "03-18",
            "bottom": "05-10"
          }
        ]
      },
      "aes": {
        "x": null,
        "y": null,
        "xmin": {
          "field": "year"
        },
        "xmax": {
          "field": "until"
        },
        "ymin": {
          "field": "top"
        },
        "ymax": {
          "field": "bottom"
        },
        "fill": {
          "field": "epoch"
        }
      },
      "params": {
        "alpha": 0.55
      },
      "inspect": false
    },
    {
      "geom": "text",
      "data": {
        "values": [
          {
            "epoch": "Medieval warm period",
            "midYear": 1100,
            "nameDate": "03-14"
          },
          {
            "epoch": "Little Ice Age",
            "midYear": 1575,
            "nameDate": "03-14"
          },
          {
            "epoch": "Industrial era",
            "midYear": 1938,
            "nameDate": "03-14"
          }
        ]
      },
      "aes": {
        "x": {
          "field": "midYear"
        },
        "y": {
          "field": "nameDate"
        },
        "label": {
          "field": "epoch"
        },
        "color": {
          "value": "#6b7075"
        }
      },
      "params": {
        "size": 11
      },
      "inspect": false
    },
    {
      "geom": "point",
      "aes": {
        "color": {
          "value": "#777777"
        }
      },
      "params": {
        "alpha": 0.5,
        "size": 1.6
      }
    },
    {
      "geom": "rule",
      "aes": {
        "color": {
          "value": "#6b7075"
        },
        "linetype": {
          "value": "dashed"
        }
      },
      "params": {
        "yintercept": "04-15",
        "linewidth": 1
      }
    },
    {
      "geom": "line",
      "stat": "summary_bin",
      "aes": {
        "color": {
          "value": "#262626"
        }
      },
      "params": {
        "fun": "median",
        "binwidth": 25,
        "curve": "step-hv",
        "linewidth": 1.8
      }
    },
    {
      "geom": "segment",
      "data": {
        "values": [
          {
            "year": 1323,
            "bloomDate": "05-04",
            "label": "1323 · May 4, latest on record",
            "labelYear": 1250,
            "labelDate": "05-08"
          },
          {
            "year": 1409,
            "bloomDate": "03-27",
            "label": "1409 · March 27, earliest for six centuries",
            "labelYear": 1310,
            "labelDate": "03-24"
          },
          {
            "year": 2023,
            "bloomDate": "03-25",
            "label": "2023 · March 25, earliest in 1,200 years",
            "labelYear": 2010,
            "labelDate": "03-24"
          }
        ]
      },
      "aes": {
        "x": {
          "field": "labelYear"
        },
        "y": {
          "field": "labelDate"
        },
        "xend": {
          "field": "year"
        },
        "yend": {
          "field": "bloomDate"
        },
        "color": {
          "value": "#b3452f"
        }
      },
      "params": {
        "linewidth": 0.7,
        "alpha": 0.9
      }
    },
    {
      "geom": "text",
      "data": {
        "values": [
          {
            "year": 1323,
            "bloomDate": "05-04",
            "label": "1323 · May 4, latest on record",
            "labelYear": 1250,
            "labelDate": "05-08"
          },
          {
            "year": 1409,
            "bloomDate": "03-27",
            "label": "1409 · March 27, earliest for six centuries",
            "labelYear": 1310,
            "labelDate": "03-24"
          },
          {
            "year": 2023,
            "bloomDate": "03-25",
            "label": "2023 · March 25, earliest in 1,200 years",
            "labelYear": 2010,
            "labelDate": "03-24"
          }
        ]
      },
      "aes": {
        "x": {
          "field": "labelYear"
        },
        "y": {
          "field": "labelDate"
        },
        "label": {
          "field": "label"
        },
        "color": {
          "value": "#b3452f"
        }
      },
      "params": {
        "size": 11,
        "anchor": "end",
        "dx": -4
      }
    }
  ],
  "scales": {
    "x": {
      "type": "linear",
      "labels": "d",
      "domain": [
        800,
        2030
      ]
    },
    "y": {
      "type": "time",
      "temporalKind": "monthDay",
      "reverse": true,
      "breaks": [
        "04-05",
        "04-15",
        "04-25"
      ],
      "dateLabels": "%b %e",
      "domain": [
        "05-10",
        "03-10"
      ]
    },
    "fill": {
      "type": "manual",
      "domain": [
        "Medieval warm period",
        "Little Ice Age",
        "Industrial era"
      ],
      "range": [
        "#f5edc4",
        "#dce8f2",
        "#f3dcda"
      ]
    }
  },
  "guides": {
    "fill": {
      "type": "none"
    }
  },
  "labs": {
    "x": "Year",
    "y": "Bloom date"
  },
  "theme": "tufte"
}

Where next

Dashed rule on the finished chart: the 1600–1850 median bloom day, 15 April. Kyoto cherry-blossom full-bloom dates, 812-2026 CE. Data: Yasuyuki Aono; Aono & Saito (2010), Aono & Kazui (2008).