简体   繁体   中英

Persisting the state of d3 treemap on drilling down to child nodes

I am working on the d3 treemap v5 in which I need to persist the state of the treemap in localstorage on each user click. My code is in https://codesandbox.io/s/d3-treemap-wfbtg When a user clicks the top parent tile it is drilled down to the children tiles. How to persist that in local storage when the user reloads the browser he wants to see the drilled down children tiles.

class Treegraph extends React.Component {
  createTreeChart = () => {
    const width = 550;
    const height = 500;
    var paddingAllowance = 2;
    const format = d3.format(",d");
    const checkLowVal = d => {
      console.log("ChecklowVal", d);
      if (d.value < 2) {
        return true;
      }
    };
    const name = d =>
      d
        .ancestors()
        .reverse()
        .map(d => d.data.name)
        .join(" / ");
    function tile(node, x0, y0, x1, y1) {
      d3.treemapBinary(node, 0, 0, width, height);
      for (const child of node.children) {
        child.x0 = x0 + (child.x0 / width) * (x1 - x0);
        child.x1 = x0 + (child.x1 / width) * (x1 - x0);
        child.y0 = y0 + (child.y0 / height) * (y1 - y0);
        child.y1 = y0 + (child.y1 / height) * (y1 - y0);
      }
    }
    const treemap = data =>
      d3.treemap().tile(tile)(
        d3
          .hierarchy(data)
          .sum(d => d.value)
          .sort((a, b) => b.value - a.value)
      );
    const svg = d3
      .select("#chart")
      .append("svg")
      .attr("viewBox", [0.5, -30.5, width, height + 30])
      .style("font", "16px sans-serif");

    const x = d3.scaleLinear().rangeRound([0, width]);
    const y = d3.scaleLinear().rangeRound([0, height]);

    let group = svg.append("g").call(render, treemap(data));

    function render(group, root) {
      const node = group
        .selectAll("g")
        .data(root.children.concat(root))
        .join("g");

      node
        .filter(d => (d === root ? d.parent : d.children))
        .attr("cursor", "pointer")
        .on("click", d => (d === root ? zoomout(root) : zoomin(d)));

      var tool = d3
        .select("body")
        .append("div")
        .attr("class", "toolTip");

      d3.select(window.frameElement).style("height", height - 20 + "px");
      d3.select(window.frameElement).style("width", width - 20 + "px");
      node
        .append("rect")
        .attr("id", d => (d.leafUid = "leaf"))
        .attr("fill", d =>
          d === root ? "#fff" : d.children ? "#045c79" : "#045c79"
        )
        .attr("stroke", "#fff")
        .on("mousemove", function(d) {
          tool.style("left", d3.event.pageX + 10 + "px");
          tool.style("top", d3.event.pageY - 20 + "px");
          tool.style("display", "inline-block");
          tool.html(`${d.data.name}<br />(${format(d.data.value)})`);
        })
        .on("click", function(d) {
          tool.style("display", "none");
        })
        .on("mouseout", function(d) {
          tool.style("display", "none");
        });
      node
        .append("foreignObject")
        .attr("class", "foreignObject")
        .attr("width", function(d) {
          return d.dx - paddingAllowance;
        })
        .attr("height", function(d) {
          return d.dy - paddingAllowance;
        })
        .append("xhtml:body")
        .attr("class", "labelbody")
        .append("div")
        .attr("class", "label")
        .text(function(d) {
          return d.name;
        })
        .attr("text-anchor", "middle");
      node
        .append("clipPath")
        .attr("id", d => (d.clipUid = "clip"))
        .append("use")
        .attr("xlink:href", d => d.leafUid.href);

      node
        .append("text")
        .attr("clip-path", d => d.clipUid)
        .attr("font-weight", d => (d === root ? "bold" : null))
        .attr("font-size", d => {
          if (d === root) return "0.8em";
          const width = x(d.x1) - x(d.x0),
            height = y(d.y1) - y(d.y0);
          return Math.max(
            Math.min(
              width / 5,
              height / 2,
              Math.sqrt(width * width + height * height) / 25
            ),
            9
          );
        })
        .attr("text-anchor", d => (d === root ? null : "middle"))
        .attr("transform", d =>
          d === root
            ? null
            : `translate(${(x(d.x1) - x(d.x0)) / 2}, ${(y(d.y1) - y(d.y0)) /
                2})`
        )
        .selectAll("tspan")
        .data(d =>
          d === root
            ? name(d).split(/(?=\/)/g)
            : checkLowVal(d)
            ? d.data.name.split(/(\s+)/).concat(format(d.data.value))
            : d.data.name.split(/(\s+)/).concat(format(d.data.value))
        )
        .join("tspan")
        .attr("x", 3)
        .attr(
          "y",
          (d, i, nodes) =>
            `${(i === nodes.length - 1) * 0.3 + (i - nodes.length / 2) * 0.9}em`
        )
        .text(d => d);
      node
        .selectAll("text")
        .classed("text-title", d => d === root)
        .classed("text-tile", d => d !== root)
        .filter(d => d === root)
        .selectAll("tspan")
        .attr("y", "1.1em")
        .attr("x", undefined);
      group.call(position, root);
    }
    function position(group, root) {
      group
        .selectAll("g")
        .attr("transform", d =>
          d === root ? `translate(0,-30)` : `translate(${x(d.x0)},${y(d.y0)})`
        )
        .select("rect")
        .attr("width", d => (d === root ? width : x(d.x1) - x(d.x0)))
        .attr("height", d => (d === root ? 30 : y(d.y1) - y(d.y0)));
    }

    // When zooming in, draw the new nodes on top, and fade them in.
    function zoomin(d) {
      console.log("The zoomin func", d.data);
      x.domain([d.x0, d.x1]);
      y.domain([d.y0, d.y1]);
      const group0 = group.attr("pointer-events", "none");
      const group1 = (group = svg.append("g").call(render, d));
      svg
        .transition()
        .duration(750)
        .call(t =>
          group0
            .transition(t)
            .remove()
            .call(position, d.parent)
        )
        .call(t =>
          group1
            .transition(t)
            .attrTween("opacity", () => d3.interpolate(0, 1))
            .call(position, d)
        );
    }

    // When zooming out, draw the old nodes on top, and fade them out.
    function zoomout(d) {
      console.log("The zoomout func", d.parent.data);
      x.domain([d.parent.x0, d.parent.x1]);
      y.domain([d.parent.y0, d.parent.y1]);
      const group0 = group.attr("pointer-events", "none");
      const group1 = (group = svg.insert("g", "*").call(render, d.parent));
      svg
        .transition()
        .duration(750)
        .call(t =>
          group0
            .transition(t)
            .remove()
            .attrTween("opacity", () => d3.interpolate(1, 0))
            .call(position, d)
        )
        .call(t => group1.transition(t).call(position, d.parent));
    }

    return svg.node();
  };
  componentDidMount() {
    this.createTreeChart();
  }
  render() {
    return (
      <React.Fragment>
        <div id="chart" />
      </React.Fragment>
    );
  }
}

Here is a quick idea, store the name as ID on local storage. Hopefully name is unique, otherwise make sure you have an unique ID for each node.

https://codesandbox.io/s/d3-treemap-4ehbf?file=/src/treegraph.js

  • first load localstorage for lastSelection with last selected node name
  componentDidMount() {
    const lastSelection = localStorage.getItem("lastSelection");
    console.log("lastSelection:", lastSelection);
    this.createTreeChart(lastSelection);
  }
  • add a parameter to you createTreeChart so when it loads and there is a lastaSelection you have to reload that state
  createTreeChart = (lastSelection = null) => {
  • decouple your treemap in a variable, filter the lastSelection node and zoomin into it. this step could be improved, not sure if the refreshing zoomin is interesting.
    const tree = treemap(data);
    let group = svg.append("g").call(render, tree);
    if (lastSelection !== null) {
      const lastNode = tree
        .descendants()
        .find(e => e.data.name === lastSelection);
      zoomin(lastNode);
    }
  • finally update localStorage on node click.
      node
        .filter(d => (d === root ? d.parent : d.children))
        .attr("cursor", "pointer")
        .on("click", d => {
          if (d === root) {
           localStorage.setItem("lastSelection", null);
            zoomout(root);
          } else {
           localStorage.setItem("lastSelection", d.data.name);
            zoomin(d);
          }
        });

This could be improved, but it's a starting point.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM