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
datasetandencode, 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.
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:
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:
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:
initonce, then manysetOption, anddisposeat the end. Chart.svelteneeds 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
-
$effectmakes 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
$stateof 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
folderof each row goes on the x axis, and the keywordson 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.