charts

Tags for numbers. Below is the year of Ember Hill, a small and entirely made-up coffee roastery, told as a short report. Every chart draws itself as you reach it, and the data is plain HTML.

The year in one line

Ember Hill opened its web shop in January with one roast and a hand-labelled bag. Orders were slow through the winter, then a subscription launched in April and the line never looked back.

<motion-chart> a line that draws in when it scrolls into view
Show the codeHide the code
<script type="module">
  import "motion-components/motion-chart";
</script>

<motion-chart
  style="--mc-chart-1: #2563eb"
  type="line"
  label="Orders per month in 2026"
  values="310, 280, 340, 520, 610, 690, 760, 740, 700, 780, 920, 1240"
  labels="Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec"
></motion-chart>

Small lines, big stories

Subscribers grew from 40 to 310 while café walk-ins stayed almost flat and returns kept falling .

<motion-sparkline> a trend the size of a word, drawn inside a sentence

Show the codeHide the code
<script type="module">
  import "motion-components/motion-sparkline";
</script>

<p>
  Subscribers grew from 40 to 310
  <motion-sparkline
    style="--mc-sparkline-color: #2563eb"
    values="40, 52, 61, 98, 140, 171, 198, 214, 232, 251, 280, 310"
    label="Subscribers per month, from 40 to 310"
    area
  ></motion-sparkline>
  while café walk-ins stayed almost flat
  <motion-sparkline
    style="--mc-sparkline-color: #60608a"
    values="210, 198, 220, 214, 230, 236, 241, 228, 219, 224, 232, 240"
    label="Café walk-ins per week, from 210 to 240"
  ></motion-sparkline>
  and returns kept falling
  <motion-sparkline
    style="--mc-sparkline-color: #a16207"
    values="9, 8, 8, 6, 6, 5, 4, 4, 3, 3, 2, 2"
    label="Returned orders per month, from 9 to 2"
  ></motion-sparkline>.
</p>

Where the orders came from

The shop counter carried the first quarter. By summer, online orders had passed it, and two local restaurants started buying beans wholesale in the autumn.

<motion-chart type="bar"> three series, bars that grow in one after another
Show the codeHide the code
<script type="module">
  import "motion-components/motion-chart";
</script>

<motion-chart
  style="--mc-chart-1: #2563eb; --mc-chart-2: #a16207; --mc-chart-3: #60608a"
  type="bar"
  label="Orders by channel per quarter"
  values="620, 460, 480, 510; 380, 1320, 2200, 2940; 0, 0, 180, 420"
  labels="Q1, Q2, Q3, Q4"
  series="Shop counter, Online, Wholesale"
></motion-chart>

Hot coffee, cold brew

Summer turned the menu upside down. Cold brew overtook hot drinks in June and handed the lead back in September, right on schedule.

<motion-chart> two lines with a legend, a tooltip and arrow-key navigation
Show the codeHide the code
<script type="module">
  import "motion-components/motion-chart";
</script>

<motion-chart
  style="--mc-chart-1: #2563eb; --mc-chart-2: #a16207"
  type="line"
  label="Drinks sold per month, hot and cold"
  values="1900, 1820, 1760, 1500, 1240, 980, 860, 900, 1180, 1560, 1840, 2050; 120, 140, 260, 520, 890, 1260, 1480, 1390, 1010, 540, 260, 150"
  labels="Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec"
  series="Hot drinks, Cold brew"
></motion-chart>

Parts of the whole

The roaster ran 2,400 kilos of green coffee this year. Most of it went into the house blend. A bag sells for 16 euros, and the beans are the biggest part of that price.

<motion-pie donut> sweeps in, with the total in the middle
Show the codeHide the code
<script type="module">
  import "motion-components/motion-pie";
</script>

<motion-pie
  style="--mc-chart-1: #2563eb; --mc-chart-2: #a16207; --mc-chart-3: #60608a; --mc-chart-4: #18181e"
  donut
  label="Coffee roasted in 2026, in kilos"
  values="1320, 540, 360, 180"
  labels="House blend, Single origin, Decaf, Espresso"
  total-label="kilos roasted"
></motion-pie>
<motion-pie> hover a slice and it springs outward
Show the codeHide the code
<script type="module">
  import "motion-components/motion-pie";
</script>

<motion-pie
  style="--mc-chart-1: #2563eb; --mc-chart-2: #a16207; --mc-chart-3: #60608a; --mc-chart-4: #18181e; --mc-chart-5: #9db7f4"
  label="Where the price of a bag goes"
  values="6.2, 3.1, 2.4, 2.1, 2.2"
  labels="Green beans, Wages, Rent, Packaging, Profit"
  format="currency:EUR"
></motion-pie>

This year against last

Switch the year. The bars don't redraw from zero. Each one springs from its current height to the new value, so you can watch every month grow.

<motion-chart> new values spring in from wherever the bars are
Show the codeHide the code
<script type="module">
  import "motion-components/motion-chart";
</script>

<div role="group" aria-label="Year shown in the revenue chart">
  <button type="button" data-year="2025" aria-pressed="false">2025</button>
  <button type="button" data-year="2026" aria-pressed="true">2026</button>
</div>

<motion-chart
  id="revenue"
  style="--mc-chart-1: #2563eb"
  type="bar"
  label="Revenue per month"
  values="8100, 7300, 8400, 11200, 12600, 14100, 15800, 15100, 13900, 14800, 17200, 21500"
  labels="Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec"
  format="currency:EUR"
  data-2025="6200, 5400, 6100, 7000, 7600, 8100, 8800, 8300, 7900, 8600, 9900, 12400"
  data-2026="8100, 7300, 8400, 11200, 12600, 14100, 15800, 15100, 13900, 14800, 17200, 21500"
></motion-chart>

<script type="module">
  const chart = document.getElementById("revenue");
  const buttons = [...document.querySelectorAll("[data-year]")];
  for (const button of buttons) {
    button.addEventListener("click", () => {
      chart.values = chart.getAttribute("data-" + button.dataset.year);
      for (const b of buttons) b.setAttribute("aria-pressed", String(b === button));
    });
  }
</script>

Revenue for the year came to €160,000, two thirds more than the year before.