Home > OS >  React application breaks after downloading SVG through d3_save_svg
React application breaks after downloading SVG through d3_save_svg

Time:07-01

I am prototyping a simple React App where the user is able to interact with an SVG generated with d3 and download the SVG that results from their interaction (though React isn't especially useful at the prototyping stage, it will be required eventually once the prototype is expanded upon).

I stumbled upon the d3-save-svg library (https://github.com/edeno/d3-save-svg) which is really handy and I am testing it in my prototype to see if I can rely on it looking forward.

The prototype is working "well" in the sense that the user is able to:

  1. Interact with the SVG, and
  2. Download the SVG resulting from their interaction.

However downloading the SVG, which relies on d3-save-svg, appears to break further interactivity with the SVG, which is unintended. The user is still able to redownload the SVG but cannot interact with the SVG anymore.

Can anyone:

  • Help me understand what's going on, and/or
  • Propose a fix that would allow the user to, pass the first download, interact further with the SVG and redownload the result of their further interactions?

Here is the prototype code (https://codesandbox.io/s/goofy-hellman-qcppyd):

import { useRef, useEffect } from "react";
import * as d3 from "d3";
import d3_save_svg from "d3-save-svg";

export default function App() {
  const ref = useRef();

  useEffect(() => {
    const svg = d3
      .select(ref.current)
      .append("svg")
      .attr("width", 500)
      .attr("height", 500);

    svg
      .append("circle")
      .attr("cx", 400)
      .attr("cy", 300)
      .attr("r", 100)
      .attr("fill", "blue");

    svg
      .append("text")
      .text("Move")
      .attr("x", 100)
      .attr("y", 100)
      .attr("cursor", "pointer")
      .on("click", function () {
        svg.selectAll("circle").attr("cx", function () {
          return d3.select(this).attr("cx") - 10;
        });
      });

    svg
      .append("path")
      .attr(
        "d",
        "M896 672q119 0 203.5 84.5t84.5 203.5-84.5 203.5-203.5 84.5-203.5-84.5-84.5-203.5 84.5-203.5 203.5-84.5zm704-416q106 0 181 75t75 181v896q0 106-75 181t-181 75h-1408q-106 0-181-75t-75-181v-896q0-106 75-181t181-75h224l51-136q19-49 69.5-84.5t103.5-35.5h512q53 0 103.5 35.5t69.5 84.5l51 136h224zm-704 1152q185 0 316.5-131.5t131.5-316.5-131.5-316.5-316.5-131.5-316.5 131.5-131.5 316.5 131.5 316.5 316.5 131.5z"
      )
      .attr("transform", "translate(200 100) scale(0.02)")
      .attr("class", "snap-button")
      .attr("fill", "#1976d2")
      .attr("cursor", "pointer")
      /* .html(iconHtml) */
      .on("click", function () {
        var config = {
          filename: "customFileName"
        };

        d3_save_svg.save(d3.select("svg").node(), config);
      });
  });

  return <div ref={ref} />;
}

CodePudding user response:

Turns out d3_save_svg modifies the DOM before it exports the svg, adding a number of things to it including styles. It seems that, since this happens outside of react's rendering cycle, React looses its refs and looses track of the SVG's desired implicit state.

A solution is to base the export on a clone version of the SVG which you can then remove right after the export, thereby guaranteeing the copy is never actually visible to the user (if desired, there can be other ways to do ensure the user never sees the copy, such as locating the copy outside of the viewbox).

import { useRef, useEffect } from "react";
import * as d3 from "d3";
import d3_save_svg from "d3-save-svg";

export default function App() {
  const ref = useRef();

  useEffect(() => {
    const svg = d3
      .select(ref.current)
      .append("svg")
      .attr("width", 500)
      .attr("height", 500);

    svg
      .append("circle")
      .attr("cx", 400)
      .attr("cy", 300)
      .attr("r", 100)
      .attr("fill", "blue");

    svg
      .append("text")
      .text("Move")
      .attr("x", 100)
      .attr("y", 100)
      .attr("cursor", "pointer")
      .on("click", function () {
        svg.selectAll("circle").attr("cx", function () {
          return d3.select(this).attr("cx") - 10;
        });
      });

    svg
      .append("path")
      .attr(
        "d",
        "M896 672q119 0 203.5 84.5t84.5 203.5-84.5 203.5-203.5 84.5-203.5-84.5-84.5-203.5 84.5-203.5 203.5-84.5zm704-416q106 0 181 75t75 181v896q0 106-75 181t-181 75h-1408q-106 0-181-75t-75-181v-896q0-106 75-181t181-75h224l51-136q19-49 69.5-84.5t103.5-35.5h512q53 0 103.5 35.5t69.5 84.5l51 136h224zm-704 1152q185 0 316.5-131.5t131.5-316.5-131.5-316.5-316.5-131.5-316.5 131.5-131.5 316.5 131.5 316.5 316.5 131.5z"
      )
      .attr("transform", "translate(200 100) scale(0.02)")
      .attr("class", "snap-button")
      .attr("fill", "#1976d2")
      .attr("cursor", "pointer")
      /* .html(iconHtml) */
      .on("click", function () {
        var config = {
          filename: "customFileName"
        };

         let svgExport = d3.select(".svg").clone(true)
         d3_save_svg.save(svgExport.node(), config);
         svgExport.remove()
      });
  });

  return <div ref={ref} />;
}
  • Related