Skip to content

Charts with ECharts

Exercise charts-echarts-annex.pdf · PDF

Look at a report table with 36 rows and try to answer: which folder has the most words? You will need between five and ten seconds, hunting through the group footers. Now look at a bar chart of the same data. Same question: under a second.

The numbers did not change. The shape did the reading for you, because your eyes compare lengths before you can read a digit. A table is for looking up a number. A chart is for seeing a pattern. A report needs both, and today yours gets its chart.

What you will learn

  • Choose between bar, line, pie and scatter, or a plain table, from the question the reader asks
  • Tell who drew the first bar and line charts, and when
  • Wrap Apache ECharts in a Svelte 5 component: init, setOption, resize, dispose
  • Feed a chart with the report's own data using dataset and encode, so it reacts to the filters
  • Check a chart against the honest chart list
  • The trap: how one line of code makes a chart lie

A Scot with no data

In 1786, in London, the Scottish engineer William Playfair published The Commercial and Political Atlas. It held 43 charts of England's trade over time, drawn as lines. Money drawn as a line was a new idea.

One more chart was different. For Scotland he only had the figures of a single year, with 17 trading partners. No time, so no line. He drew bars instead. One of the most common charts in the world was born because some data was missing. In 1801, in The Statistical Breviary, he added what is probably the first pie chart.

Why did he not draw Scotland as a line? Because a line needs time along the horizontal axis, and he had categories. He did not choose the chart he liked. The data and the question chose it, and that is still the rule today.

Four tools, four questions

We will keep one picture all the way through this page: a toolbox. A hammer is not better than a screwdriver. It is for a different job. Each chart type is a tool for one kind of question.

The question is about... Tool NoteBox example On your phone
Comparing categories Bar Which folder has the most words? Screen time per app
Change over time Line Notes created per month Battery level during the day
Parts of one whole (2 to 5) Pie / donut Pinned against not pinned Storage: photos, apps, system
Relation between two numbers Scatter Do longer notes get edited more? Sleep hours against steps
Exact values to look up Table Words of each note Your marks

Three rules go with the toolbox:

  • Bars start at zero. The eye compares lengths. Cut the bottom, and the lengths lie.
  • A pie has five slices at most, and they are parts of one whole that adds up to 100%. Twelve slices are twelve bars.
  • No question, no chart. If you cannot write the question, the chart is decoration.

Careful: pie charts are not for percentages

A pie is for parts of a whole. "School has 57% of the words, Work has 22%" is a whole. "Pass rate: 80% in DAM, 75% in DAW" is not, because those two percentages do not add up to anything. That is a bar chart.

Try it before you read on

Choose the tool for each of these, and say why in three words: the temperature of your room over a week; the 30 best-selling games of the month; the exact price of each of 50 items. The handout has more situations to practise with.

ECharts in three steps

Apache ECharts is a free, open-source JavaScript library for drawing charts.

It was born at Baidu in 2013, entered the Apache Software Foundation's incubator in January 2018, and was announced as a top-level Apache project in January 2021. The code on this page was checked on version 6.1. Older tutorials may look different, because version 6 changed the default theme, but the options work the same way.

Using it takes three steps:

const chart = echarts.init(div);   // 1 · a <div> with a height becomes a chart
chart.setOption(option);           // 2 · the option says WHAT to draw
chart.dispose();                   // 3 · when the div goes away, free the chart

Where are the loops and the rectangles? Nowhere. ECharts is declarative: you describe the chart in an object called the option, and the library draws it. You already work like that: a Svelte component describes the page, and it does not draw pixels.

Key of option What it describes
title The text above the chart
dataset The data: { source: [...] }, an array of objects, like our groups
xAxis · yAxis The two axes. type: 'category' for names, type: 'value' for numbers
series What is drawn: type: 'bar', 'line', 'pie', 'scatter'. encode says which key of the data goes to which axis
tooltip The box that appears on hover

You can see every chart type, with its option, at echarts.apache.org/examples.

Chart.svelte: build the workbench once

ECharts knows nothing about Svelte. It wants a real <div>. So you wrap it in a component, once, and never touch echarts.init again.

npm install echarts

New file src/lib/Chart.svelte:

<script>
  import * as echarts from 'echarts';

  let { option, label, height = 320 } = $props();

  let el;
  let chart;

  // Create the chart once the <div> exists; destroy it when the component goes away.
  $effect(() => {
    chart = echarts.init(el);
    const observer = new ResizeObserver(() => chart.resize());
    observer.observe(el);
    return () => {
      observer.disconnect();
      chart.dispose();
    };
  });

  // Runs again every time `option` changes: new data in, same chart.
  $effect(() => {
    chart.setOption(option, { notMerge: true });
  });
</script>

<div bind:this={el} role="img" aria-label={label} style:height="{height}px"></div>

Four questions you should be able to answer about this file:

Question Answer
Why inside $effect, and not at the top of the script? The top of the script also runs on the server, where there is no <div>. $effect runs only in the browser, after the <div> exists
What is the function the first effect returns? The cleanup. Without it, the chart stays in memory after you leave the page
Why two effects? The first does not read option, so it runs once. The second does, so it runs again whenever the option changes. Creating a new chart on every filter change would flicker
What is role="img" for? A canvas is a black box for a screen reader. The label is the chart's accessible name

{ notMerge: true } makes the new option replace the old one, instead of merging into it.

Careful: the chart does not resize by itself

ECharts draws on a canvas with a fixed pixel size. Without the ResizeObserver, the chart keeps its first width when the window changes.

The bar chart, from the groups

The groups you built in the previous lesson are already the data of the chart. In +page.svelte, add to the script:

import Chart from '#lib/Chart.svelte';

// Chart 1 · comparison between categories → bar
let wordsOption = $derived({
  title: { text: 'Words per folder' },
  color: ['#1f5fd1'],
  tooltip: {},
  dataset: { source: groups.map((g) => ({ folder: g.folder, words: g.words })) },
  xAxis: { type: 'category' },
  yAxis: { type: 'value', name: 'words' },
  series: [{ type: 'bar', encode: { x: 'folder', y: 'words' }, label: { show: true, position: 'top' } }]
});

And in the markup, above the table and inside the {:else} (no rows, no chart):

<Chart option={wordsOption}
  label="Bar chart: total words per folder. The table below has the same numbers." />

Now change the folder filter to School: one bar, 5217. Which line of your code redrew the chart? None of them did it directly:

flowchart LR
    F["<b>Filters</b><br>$state"] --> R["<b>rows, groups</b><br>$derived"]
    R --> O["<b>wordsOption</b><br>$derived"]
    O --> E["<b>Second effect</b><br>of Chart.svelte"]
    E --> C["<b>setOption</b><br>the chart redraws"]

The second effect is the bridge. ECharts does not read your $state by itself: the effect hands it each new option.

Read the option back and match each key with what it produced:

On the screen Key
The text on top title.text
The word "words" on the axis yAxis.name
The numbers over the bars series[0].label
Which column runs along the horizontal axis series[0].encode.x

The line chart, on your own

Your second chart: notes created per month, a line. You need a new grouping and a new option. Here is what the function must return:

// report.js
export function countByMonth(rows) { /* ... */ }
// → [{ month: '2026-09', notes: 9 }, { month: '2026-10', notes: 8 }, { month: '2026-11', notes: 9 },
//    { month: '2026-12', notes: 7 }, { month: '2027-01', notes: 3 }]

Three hints, in order. groupByFolder groups by n.folder: what do you group by now? The month is the first seven characters of created. And to sort by month, remember that ISO text sorts like the calendar.

Solution, after you have tried
export function countByMonth(rows) {
  const groups = Object.groupBy(rows, (n) => n.created.slice(0, 7));
  return Object.entries(groups)
    .map(([month, notes]) => ({ month, notes: notes.length }))
    .sort((a, b) => a.month.localeCompare(b.month));
}
// Chart 2 · change over time → line
let monthsOption = $derived({
  title: { text: 'Notes created per month' },
  color: ['#d92b1c'],
  tooltip: { trigger: 'axis' },
  dataset: { source: countByMonth(rows) },
  xAxis: { type: 'category' },
  yAxis: { type: 'value', name: 'notes', minInterval: 1 },
  series: [{ type: 'line', encode: { x: 'month', y: 'notes' }, label: { show: true } }]
});

minInterval: 1 keeps the axis in whole numbers, because nobody writes 2.5 notes.

Put both charts in one row with a CSS grid. In a narrow window they stack by themselves:

.charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
One more: a donut, if you finish early

Pinned against not pinned is 9 and 27 with no filter. A pie has no axes, and radius with two values makes the hole of the donut. encode maps the data to slices with itemName and value.

let pinnedOption = $derived({
  title: { text: 'Pinned notes' },
  tooltip: {},
  dataset: {
    source: [
      { name: 'Pinned', value: rows.filter((n) => n.pinned).length },
      { name: 'Not pinned', value: rows.filter((n) => !n.pinned).length }
    ]
  },
  series: [{ type: 'pie', radius: ['40%', '70%'], encode: { itemName: 'name', value: 'value' } }]
});

An honest chart

Add one small thing to the bar chart:

yAxis: { type: 'value', name: 'words', min: 1500 },

School now towers six times over Work, and Shopping has vanished. How many times more words does School really have than Work? 5217 ÷ 2070 = 2.5. It is the same data and the same library, telling a different story. Now take min out again. News charts do this all the time, and now you know where to look first: the bottom of the value axis.

Here is a subtler lie. Your line chart says 9, 8, 9, 7 and then 3. Did the user stop writing in January? No: the data ends on 12 January. Half a month looks like a fall. An honest report says so in its header ("data until 12/01/2027"), or leaves the unfinished period out.

Tick this list for every chart you make:

  • The title says what is measured
  • The value axis has a name
  • Bars start from zero
  • The values are labelled
  • It has an accessible name
  • The table with the same numbers is on the page

Careful: the chart does not replace the table

A chart shows the pattern and hides the exact values, and a screen reader gets only its name. The table underneath is the detail and the accessible version. Keep both.

Why does npm run build warn about a big chunk?

import * as echarts from 'echarts' loads the whole library, so the build warns that a chunk is larger than 500 kB. That is fine for this course. The production fix is to import only what you use from echarts/core, echarts/charts, echarts/components and echarts/renderers, and to register it with echarts.use([...]).

A chart for your report

Open your wireframe. Before any code, write one sentence:

My chart answers the question "...", so the tool is a ...

A missing question means no chart yet. Then copy Chart.svelte into your project, write the grouping in your report.js and the option in your page. Your chart must react to your filter and pass the honest chart list.

Key takeaways

  • The question chooses the chart: compare → bar, time → line, parts of a whole → pie, relation → scatter, look up → table.
  • William Playfair drew the line and the bar in 1786, and he chose by the data he had.
  • ECharts is declarative: init once, then many setOption, and dispose at the end.
  • Chart.svelte needs two effects: one creates the chart, and the other hands it each new option. Because the option is $derived, the chart reacts to the filters.
  • Bars start at zero, and an unfinished period must say so.
  • A chart needs an accessible name, and the table stays on the page.

Quick check

1. A reader asks: how has the number of visits to my shop changed, week by week, this year? Which tool fits?
  • A line, because the question is about change over time
  • A pie, because the weeks are all parts of the same year
  • A scatter, because the question uses two different numbers
  • A donut, because it shows a whole year in a single circle

Time along the horizontal axis means a line. Fifty-two slices would be impossible to compare, and a scatter is for the relation between two numbers, not for change over time.

2. Why did William Playfair draw Scotland's trade as bars, and not as a line?
  • He only had the figures of one year, so there was no time to draw along an axis
  • Scotland had far more trading partners than England, so a line would not fit
  • Printing lines was more expensive than printing bars, so he saved on that plate
  • He wanted to show that Scotland was less important than England in world trade

A line needs time on the horizontal axis, and he had only categories: 17 countries in one year. The data and the question chose the chart.

3. Why is echarts.init inside $effect, and not at the top of the script?
  • The top of the script also runs on the server, where there is no <div>
  • ECharts can only be imported from inside an $effect, never from a script
  • The top of the script runs once per second, which would create many charts
  • $effect makes the chart draw faster, because it uses a second processor

$effect runs only in the browser, after the <div> exists. Its cleanup function then disposes the chart when the component goes away.

4. What is the job of the second $effect in Chart.svelte?
  • It reads option, so it hands each new option to the existing chart
  • It destroys the old chart and creates a new one on every filter change
  • It measures the <div> and resizes the chart when the window changes
  • It reads the $state of the page and writes the new rows into the table

ECharts knows nothing about Svelte, so this effect is the bridge. One init, many setOption. Creating a chart on each change would flicker, and resizing is the job of the ResizeObserver.

5. In a bar series, what does encode: { x: 'folder', y: 'words' } mean?
  • The key folder of each row goes on the x axis, and the key words on the y axis
  • The words of the chart must be encrypted before being sent to the folder
  • The chart saves its picture to a folder as a file with that name and size
  • The axis titles are set to the text "folder" and the text "words" on the screen

Each key of the objects in dataset.source is a dimension, and encode maps dimensions to axes. The title of an axis is set separately, with yAxis.name.

6. A bar chart has min: 1500 on its value axis. What does that do to the reader?
  • It exaggerates the differences, because the bars no longer start at zero
  • It hides nothing, because the numbers on the axis tell the same exact story
  • It makes the comparison more accurate, because the bars become much taller
  • It adds 1500 to every value, so all the totals on the chart become incorrect

The eye compares lengths. Cutting the bottom turns a real 2.5 times into an apparent 6 times. The first thing to check on any bar chart is the bottom of the value axis.

7. Why does an accessible report keep the table under its chart?
  • The chart hides exact values, and a screen reader only gets the chart's name
  • ECharts refuses to draw a chart unless the same data is in a table on the page
  • The table is what makes the chart react each time the user changes a filter
  • Browsers print only the table, so the chart would be lost on the paper version

A chart shows the pattern. The table gives the exact values, and it is the accessible version of the same numbers.

8. A fitness app shows a yearly summary. Pick the two good choices.
  • A line for the kilometres run per month of the year
  • A sorted bar chart to compare the 15 routes you ran most
  • A pie chart with one slice for each of the 15 routes
  • A scatter chart for the total kilometres of the year

Months are time, so a line fits. Fifteen items are too many slices, so a sorted bar chart compares them better. A single total is one number: a chart has nothing to show.

The same report can now be read at a glance on a screen. Next it goes on paper, in a PDF nobody draws by hand: PDF reports with Typst.