A real chart in 15 lines of SVG, no library
Set markodenic.tech as your preferred Google source
AI Agents Waste Tokens
Coding agents can waste huge amounts of tokens just finding the code they need.
The client wants a small revenue line next to each metric on the dashboard. Twelve points, no axes, no tooltips. So you install a charting library, ship 200KB to draw it, write a config object with thirty keys, and add a resize listener so the canvas redraws when the sidebar collapses.
An SVG line is a list of coordinates. Turning numbers into coordinates is one `map`.
The fix
function points(values, width, height, pad = 3) {
const min = Math.min(...values);
const max = Math.max(...values);
const range = max - min || 1;
const step = width / (values.length - 1);
const usable = height - pad * 2;
return values
.map((v, i) => `${i * step},${pad + usable - ((v - min) / range) * usable}`)
.join(' ');
}
<svg viewBox="0 0 300 80" class="chart" role="img" aria-label="Revenue, up 40% over 12 months">
<polyline fill="none" stroke="royalblue" stroke-width="2" stroke-linejoin="round" />
</svg>
svg.querySelector('polyline').setAttribute('points', points(data, 300, 80));
That is the chart.
Resize the window and watch it scale. Nothing redraws.
The part that does the work is viewBox
viewBox="0 0 300 80" says the drawing is 300 units wide and 80 tall. Units, not pixels. Your coordinates live in that space forever, and CSS decides how big it appears:
.chart {
width: 100%;
height: auto;
}
The chart now fills a card at 200px or 900px, scales on a phone, stays sharp on a retina screen, and prints correctly. No canvas, no device pixel ratio, no resize listener, no redraw. That is the whole reason to reach for SVG here instead of canvas.
Why the pad
A stroke is centered on the path, so half of a 2px line sits above it. The highest point in your data lands at y=0, and the top half of the line gets clipped by the edge of the viewBox. It looks like a rendering bug, and it is just geometry.
pad keeps the drawing area inset by a few units on the top and bottom. Set it to at least half your stroke width. Add more if you plan to put dots on the points.
Fill the area under it
Same coordinates, plus the two bottom corners, drawn as a polygon behind the line:
const line = points(data, 300, 80);
area.setAttribute('points', `0,80 ${line} 300,80`);
<polygon fill="royalblue" fill-opacity="0.12" />
<polyline fill="none" stroke="royalblue" stroke-width="2" />
Order matters, since SVG paints in document order and has no z-index. The polygon goes first.
Bars are even shorter
const step = width / values.length;
svg.innerHTML = values
.map((v, i) => {
const h = (v / max) * height;
return `<rect x="${i * step}" y="${height - h}" width="${step - 2}" height="${h}" rx="1" />`;
})
.join('');
The - 2 on the width is the gap between bars. rx="1" rounds the corners.
Where this comes up
- Dashboard metric cards: a number, a percentage, and a line showing the shape of it.
- Table cells: a 60×20 sparkline per row, which no charting library enjoys doing 50 times.
- Emails and PDFs: SVG renders in places a canvas never will.
- Anywhere the design is one line: most “charts” in real products are a trend, not an analysis tool.
Two things to get right
Give it a text alternative. A chart with no label is invisible to a screen reader and useless to anyone who cannot see the color:
<svg role="img" aria-label="Revenue, up 40% over 12 months">
Say what the trend is, not that it is a line chart. If the exact numbers matter, put them in a visually hidden table next to it.
Do not stretch it to a fixed height. Setting width: 100%; height: 120px on an SVG that scales uniformly will letterbox or squash it. If you genuinely need it to fill an odd box, add preserveAspectRatio="none" and vector-effect="non-scaling-stroke" on the line, which keeps the stroke 2px while the shape distorts.
When to install the library after all
Axes with sensible tick values, hover tooltips, zoom and pan, stacked series, time scales that know about months. Those are genuinely hard, and that is what you are paying the 200KB for.
The trap is paying it for a twelve-point line in a card.
Why this matters
- 200KB for a line with twelve points: on a dashboard that probably renders eight of them.
- It scales with no code: one
viewBoxreplaces the resize listener, the redraw, and the retina handling that canvas makes you write. - You can read it: it is a list of coordinates in the DOM, so when it looks wrong you inspect it instead of debugging someone’s config object.
Find the smallest chart in your product. If it has no axes and no tooltips, it is a polyline, and you can delete a dependency this afternoon.
Happy coding!
Marko