Home > other >  Download Image of d3 visualization in Svelte?
Download Image of d3 visualization in Svelte?

Time:01-07

I'm trying to download an image of a visualization created in d3, in Svelte. I've created a repl to show what I've tried so far. It's just a throwaway chart-- so don't mind how ugly and useless it is, this is just an attempt to get a simple working example.

Any idea what's going wrong? I'm not getting any errors in the console. Help very much appreciated.

App.svelte:

            <script>
                import Chart from "./Chart.svelte";
                import saveAs from "./FileSaver.min.js";

                async function downloadChart() {
                    const chart = document.querySelector("#chart");
                    const svgString = new XMLSerializer().serializeToString(chart);
                    const canvas = document.createElement("canvas");
                    const ctx = canvas.getContext("2d");
                    const data = new Blob([svgString], {type: "image/svg xml"});
                    const url = URL.createObjectURL(data);
                    const img = new Image();
                    img.onload = () => {
                        ctx.drawImage(img, 0, 0);
                        canvas.toBlob((blob) => {
                            saveAs(blob, "chart.jpg");
                        });
                    };
                    img.src = url;
                }
            </script>
            <Chart/>
            <button on:click={downloadChart}>Download Image</button>

Chart.svelte

            <script>
                import { onMount } from "svelte";
                import * as d3 from "d3";

                let data = [10, 20, 30, 40, 50];

                onMount(() => {
                    const svg = d3
                        .select("#chart")
                        .append("svg")
                        .attr("width", 500)
                        .attr("height", 500);

                    svg
                        .selectAll("rect")
                        .data(data)
                        .enter()
                        .append("rect")
                        .attr("x", (d, i) => i * 50)
                        .attr("y", (d, i) => 500 - d)
                        .attr("width", 50)
                        .attr("height", (d) => d)
                        .attr("fill", "teal");
                });
            </script>

            <div id="chart" />

FileSaver.min.js is from https://cdn.rawgit.com/eligrey/FileSaver.js/e9d941381475b5df8b7d7691013401e171014e89/FileSaver.min.js

CodePudding user response:

There are several issues.

You serialize a div rather than an SVG, which then cannot be loaded in the image. I would recommend adding an onerror handler to the image to be notified of such issues.

Generally one should not query the DOM when using Svelte, it is prone to causing errors and violates the independence of the components. Use prop bindings and bind:this or use:action to interact with DOM nodes.

Here, as the SVG is created by D3, it can be exposed as a property of Chart, e.g.

<!-- App.svelte -->
<script>
  let chart;
  //...
</script>
<Chart bind:chart />
<!-- Chart.svelte -->
<script>
  export let chart;

  onMount(() => {
    const svg = d3
      .select("#chart")
      .append("svg")
      .attr("width", 500)
      .attr("height", 500);

    // ...
        
    chart = svg.node();
  });
</script>

The canvas size should be set to the image size:

canvas.width = chart.getAttribute('width');
canvas.height = chart.getAttribute('height');

toBlob returns a PNG by default, a second parameter has to be added to specify 'image/jpeg' to get a JPG blob. (Would recommend using PNG, though. It has transparency and is a more suitable format for charts.)

FileSaver is on NPM and can be imported like this:

import FileSaver from 'file-saver';

(Even if you keep the file as a copy, the import gives you the module, not the saveAs function.)

It also will not work in the REPL, because that does not allow opening new tabs. To test whether the logic works you can add a a element with a link to the file blob URL instead. The link then can be opened manually via the right click menu.

Updated REPL

  • Related