Export Data Visualizations as CSV, HTML, SVG, and PNG Without Losing Meaning
Export is not a checkbox feature in a data visualization tool.
Different users want different outputs. A spreadsheet user needs CSV. A reviewer needs a portable HTML report. A maintainer wants SVG or PNG for documentation and pull requests. If all formats are treated as screenshots, the exported data loses meaning.
Use a format strategy, not one generic export button.
CSV is for analysis
CSV should contain the data behind the view, not pixels from the view.
For Excel compatibility, include a UTF-8 BOM:
function exportCsv(rows: string[][], filename: string) {
const csv = rows
.map((row) => row.map(escapeCsvCell).join(","))
.join("\r\n");
downloadBlob(
new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8" }),
filename,
);
}
function escapeCsvCell(value: string) {
if (/[",\r\n]/.test(value)) {
return `"${value.replaceAll('"', '""')}"`;
}
return value;
}
The BOM helps Excel detect UTF-8, which matters when labels include non-ASCII text.
HTML is for sharing
An HTML export should open directly from disk without your app.
The practical shape is:
- inline CSS
- embedded JSON data
- small JavaScript renderer
- optional D3 from a CDN
Example skeleton:
<!doctype html>
<meta charset="utf-8" />
<title>Visualization Export</title>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script type="application/json" id="data">
{"nodes":[],"edges":[]}
</script>
<script>
const data = JSON.parse(document.getElementById("data").textContent);
// render visualization
</script>
This is useful for sending a report to someone who does not have your desktop app.
SVG is for editable documents
SVG export should preserve labels, shapes, and colors.
If the app uses CSS variables, clone the SVG and inline computed styles before serializing:
function inlineSvgStyles(svg: SVGSVGElement) {
const clone = svg.cloneNode(true) as SVGSVGElement;
for (const el of clone.querySelectorAll<SVGElement>("*")) {
const computed = getComputedStyle(el);
el.style.fill = computed.fill;
el.style.stroke = computed.stroke;
el.style.color = computed.color;
}
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
clone.setAttribute("width", String(svg.clientWidth));
clone.setAttribute("height", String(svg.clientHeight));
return new XMLSerializer().serializeToString(clone);
}
Without this, the exported SVG may look correct in the app and wrong in another viewer.
PNG is for quick paste
PNG export is useful for issue comments, slide decks, and chat.
Render the SVG into a canvas at 2x scale:
async function svgToPngBlob(svgText: string, width: number, height: number) {
const img = new Image();
const url = URL.createObjectURL(new Blob([svgText], { type: "image/svg+xml" }));
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = reject;
img.src = url;
});
const canvas = document.createElement("canvas");
canvas.width = width * 2;
canvas.height = height * 2;
const ctx = canvas.getContext("2d")!;
ctx.scale(2, 2);
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);
return await new Promise<Blob>((resolve) => {
canvas.toBlob((blob) => resolve(blob!), "image/png");
});
}
The 2x scale keeps text readable on high-density screens.
Use one download helper
Keep browser download mechanics in one helper:
function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
That keeps format code focused on producing correct content.
Verification checklist
Check that:
- CSV opens in Excel with non-ASCII labels intact
- HTML opens from disk without a dev server
- SVG keeps colors outside the app
- PNG text is readable in a document
- filenames include context and timestamp
- exported data matches active filters