Topic 16.6
Exporting and Printing
In one line
People want the chart in a slide, the numbers in a spreadsheet and the page on paper. An SVG file needs its styles written into it, a PNG goes through a canvas, a CSV needs careful quoting, and print needs its own CSS.
Think of it like this
Lending someone a recipe. You can photocopy the page (a PNG: fixed, easy to paste), hand over the original card (an SVG: sharp at any size, but it needs the same pen colours), or copy the ingredient list on a sheet (a CSV: the numbers, to reuse). Each is right for a different person.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- serialise
- Turn a live object (an SVG element) into text that can be saved.
- Blob
- A chunk of file-like data in memory, with a type such as
text/csv. - object URL
- A temporary address (
blob:…) for a Blob, made byURL.createObjectURL. - CSV
- Comma-separated values: a text table, one row per line.
- byte order mark
- Three bytes at the start of a UTF-8 file (the character
\ufeff) that tell Excel the file is UTF-8. - tainted canvas
- A canvas that has drawn another site's image without permission, so the browser refuses to read it back.
- CORS
- Cross-Origin Resource Sharing: headers by which a server allows other sites to use its files.
Step by step
01Serialise and compare
The first example draws five bars styled by a stylesheet and by var(--chart-accent). It serialises them twice: once plainly, once with svgText. It counts how many bars carry their own fill, and shows both files as pictures, as an image viewer would show them.
Then svgToPng draws the picture on a canvas and returns a PNG blob. The log shows the type, the starting bytes of a PNG file and the size, which is the SVG's size times the scale.
02Make the CSV and the download
The second example prints what csvCell does with ten awkward values, builds a small CSV from real stations plus one row of test text, and shows the exact text with \r\n and the byte order mark made visible.
A stand-in for the link click logs the file name and that the address is a blob: address. In your project the same call starts the download.
03Print
Print CSS goes in the same stylesheet, inside @media print: ink and paper colours, no controls, one column, charts that stay whole. Test it with the browser's print preview (Ctrl+P) or with the Rendering tab's Emulate CSS media type: print.
@media print {
:root:root, :root[data-theme="dark"] { --page-bg: #fff; --chart-bg: #fff; --chart-ink: #000; --chart-muted: #444; }
header button, .presets, .exports, .state button, select { display: none; }
.grid { display: block; }
.box { break-inside: avoid; margin-bottom: 12px; }
}04Meet the tainted canvas
This stage draws an image from the site onto a canvas and tries to read it. It fails with the real browser error: SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data. With crossOrigin = "anonymous" the same read works, because the server allows it.
Try it yourself
- 1
Export at three times the size
In the first example change
svgToPng(svg, 2)tosvgToPng(svg, 3). The log sayspixels 900 x 360: the picture is the SVG's size times the scale. - 2
Drop the byte order mark
In the second example, call
toCsv(rows, columns, { bom: false }). The text no longer starts with\ufeff. Excel on Windows may then show the accents wrongly if a name has one. Keep the mark unless your readers use another tool.
Code & diagrams
The plain file has no fill on its bars, so a viewer paints them black. The second has every style written in. Both pictures are the real exported files, shown as images.
Preview
Console
Nothing logged yet.Expected console output
plain: bars with a fill of their own: 0 of 5
svgText: bars with a fill of their own: 5 of 5
svgText starts with: <?xml version="1.0" encoding="UTF-8"?>
svgText size attributes: true
png: image/png | starts with 137,80,78,71 | pixels 600 x 240
scale 30: The picture would be 9000 x 3600 pixels, which is more than a browser canvas should be asked for. Use a smaller scale.The first three rows are real stations (S01 Quay, S02 Market, S03 Chapel, all in Harbour); the fourth is made-up test text. A formula-looking text gets a ' in front, and a negative number does not.
Preview
Console
Nothing logged yet.Expected console output
"Quay" -> "Quay"
"Old Town, East" -> "\"Old Town, East\""
"He said \"go\"" -> "\"He said \"\"go\"\"\""
"line one\nline two" -> "\"line one\nline two\""
"=SUM(A1:A9)" -> "'=SUM(A1:A9)"
"-5 degrees" -> "'-5 degrees"
-3 -> "-3"
null -> ""
NaN -> ""
Date 2025-06-01T23:30:00.000Z -> "2025-06-01"
"<BOM>Station,Name,District\r\nS01,Quay,Harbour\r\nS02,Market,Harbour\r\nS03,Chapel,Harbour\r\nTEST,\"Test, \"\"quoted\"\" =HYPERLINK(\"\"x\"\")\",\r\n"
download: lumen-rides-2025-06-01-2025-06-30.csv | address starts with blob: true
odd names: lumen-rides-ardenvale-quay-caf-1.svgBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Serialise without the styles
In the first example, delete the from.forEach(…) block inside the pasted svgText (the four lines that copy the computed styles).
Myth vs fact
Myth
A screenshot is a good enough export.
Fact
It is blurry on a big screen, includes the browser, cannot be edited and holds no numbers. Export the SVG, the PNG and the CSV.
Myth
An SVG file looks the same wherever it is opened.
Fact
Only if its styles are inside it. CSS from your page and CSS variables do not travel with serializeToString.
Myth
A CSV is just join(",").
Fact
A comma, quote or line break in a value breaks the columns, and a value that starts with = can run as a formula in a spreadsheet.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A formula-looking value in a CSV (
=HYPERLINK(...)) can make a spreadsheet send data or run a command when someone opens the file (CSV injection). Prefixing a'defuses it. Data from users or other systems must always go throughcsvCell. The cost: a spreadsheet shows the'as a visible character in that cell, so tell readers that a leading apostrophe is a safety mark. - ▸
Fonts: an SVG used as an image cannot fetch web fonts, so a chart that uses your brand font exports in the computer's default font. To keep the font, embed it as base64 in a
<style>inside the SVG, or accept the fallback and name a safe font stack. - ▸
canvas.toBlobis asynchronous and givesnullwhen the canvas is too big (browsers limit the width, height and area). Check the size first, assvgToPngdoes, and tell the person to choose a smaller scale. - ▸
<use href="#icon">points into the page's<defs>. A copy of just the chart has no<defs>, so the icon is missing in the file. Copy the symbols you need into the exported SVG or draw them in place.
Remember this
- 1
The problem this solves: the board wants the chart in a slide, the analyst wants the numbers in Excel, and the office wants a printout. A screenshot is blurry, loses the numbers and takes a person's time. The dashboard can make every one of them with a button.
- 2
SVG to a file.
new XMLSerializer().serializeToString(svg)turns the live element into text. But the text carries only the element's own attributes. The page's stylesheet,var(--chart-accent)and inheritedcurrentColorare left behind, so the file opened alone shows black shapes.svgTextcopies the computedfill,strokeand font values into each element, adds thexmlns, and fixes the width and height (a file withwidth="100%"fills whatever opens it). The first example shows the difference as pictures. - 3
SVG to PNG, through a canvas. Make a Blob of the SVG text, give it an address with
URL.createObjectURL, load it into anImage,drawImageit on a canvas and callcanvas.toBlob. Scale the canvas by 2 for a sharp image on a phone or in a slide, fill the background first (a PNG is transparent otherwise, and dark text on a transparent PNG vanishes on a dark slide), and refuse silly sizes (a canvas has a maximum). An SVG loaded as an image cannot load fonts or pictures from the web, so it uses the fonts the computer already has. - 4
CSV, carefully. A CSV is text with rules: a cell with a comma, a quote or a line break is wrapped in quotes and inner quotes are doubled (
"Quay, ""North"""). Lines end with\r\n. A byte order mark at the start (\ufeff) lets Excel read accents correctly. Empty andNaNvalues become empty cells. And a text that starts with=,+,-or@can run as a formula when opened in a spreadsheet, socsvCellputs a'in front. Numbers are not touched, so-3stays-3. - 5
The download itself. Make a Blob,
URL.createObjectURLit, put it in a link with adownloadname, click the link from code, and release the address a moment later. The name should be safe and tell what is inside, including the filter (lumen-rides-2025-06-01-2025-06-30.csv). A preview cannot download, so the second example logs the name and the text instead. Some browsers ask the person for permission when a page starts several downloads in a row, so offer one file per click. - 6
Export what the person sees. If a filter is on, export the filtered rows and put the filter in the file name. Exporting everything when the screen shows June is a surprise, and exporting a filter with no label is a mystery.
- 7
Print. Paper has no dark theme, no buttons and no hover. A
@media printblock resets the colour tokens to dark ink on white, hides buttons and controls, turns the grid into one column, and usesbreak-inside: avoidso a chart is never cut by a page break. The project'sproduction.cssdoes this, and the Print step below shows the block. Browsers also leave out CSS background colours when printing unless the person ticks "Background graphics", while SVG fills are always drawn: keep meaning in the marks, not in adivbackground (or addprint-color-adjust: exactwhere a background is data). - 8
Tainted canvas. If a canvas draws an image from another origin that did not send permission headers, the browser marks the canvas as tainted and refuses
getImageDataandtoBlob. The last walkthrough stage shows the real errors and the fix: ask for the image withcrossOrigin = "anonymous", and the server must send CORS headers. A chart drawn from your own SVG never taints.
Explain it without notes
Why does an SVG made with serializeToString lose its colours when opened as a file?
How does a chart get from SVG to a PNG file in the browser?
Which values in a CSV need quoting, and what is CSV injection?
What is a tainted canvas and how do you avoid it?
Practice
Write exportName(kind, filter) that gives lumen-rides-saturdays-june.png for kind = "png" and a Saturday filter in June. What does it return for an empty filter?
Make toCsv write dates as 2025-06-01 and numbers with . for decimals, whatever the visitor's language. Why is toLocaleString the wrong tool here?
Trade-offs
- ↔
SVG export stays sharp and editable but depends on fonts and on styles being copied in. PNG is easy to paste everywhere but fixed in size.
- ↔
Writing computed styles into every element makes files bigger. For a chart with thousands of marks, export a PNG or a canvas image instead.
Done when you can
I can export an SVG that keeps its colours and fonts.
I can make a PNG from an SVG at a chosen scale, with a background.
I can write a CSV that survives commas, quotes, line breaks and formulas.
I can start a download and name the file from the filter.
I can write print CSS that hides controls and keeps charts whole.
I can explain a tainted canvas and its fix.