How to draw multiple d3js zoomable treemap

Viewed 864

I'm trying to draw multiple d3js zoomable treemaps.

I succeeded in drawing one based on this example https://codepen.io/moktc/pen/XMGgwP . When I reuse the drawing function the first chart shows blank like if it need the data to display.

The drawing function is

function drawTreeMap(tree) {


        treemap = d3.treemap()
                .tile(d3.treemapSquarify.ratio(height / width * 0.69 *(1 + Math.sqrt(5))))
                .size([width, height])
                .round(false)
                .paddingInner(1);

        var id = "#" +tree;
      svg = d3.select(id).append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.bottom + margin.top)
      .style("margin-left", -margin.left + "px")
      .style("margin.right", -margin.right + "px")
        .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .style("shape-rendering", "crispEdges");

      grandparent = svg.append("g")
      .attr("class", "grandparent");

      grandparent.append("rect")
      .attr("y", -margin.top)
      .attr("width", width)
      .attr("height", margin.top);

      grandparent.append("text")
      .attr("x", 6)
      .attr("y", 6 - margin.top)
      .attr("dy", ".75em");
};

When I use this function twite to display a second treemap with the same data of the first one. the first one turns blank

1 Answers

svg, grandparent and treemap are all objects that are declared outside of your function drawTreemap() (this isn't evident from the code that you have posted, but can be seen in the full code example on CodePen), so calling the function twice will act upon the same objects twice. Since these objects form part of the treemap, you need separate copies of these objects for the second treemap, so they should be declared inside drawTreemap().

Furthermore, note that the function display() contains a reference to grandparent, which is not passed to display() as an argument, so two copies of display() need to be declared separately for each copy of grandparent. Hence, display() should also be declared inside drawTreemap() (note that Javascript functions are closures, i.e. their definition includes references to all variables that were in scope at the time when the function was declared, so it is possible to declare functions that refer to variables that are not in global scope without passing them into the function as arguments).

Thirdly, the functions initialize(), accumulate(), layout(), treemap() and display() need to be called separately for each treemap, so it makes sense to include these calls inside drawTreemap().

Therefore you should declare svg, grandparent,treemap and display() locally inside drawTreemap() and include the calls to initialize(), accumulate(), layout(), treemap() and display(), which can be done as follows:

function drawTreemap(treeId) {

    var id = "#" + treeId;
    var svg = d3.select(id).append("svg")
        .attr("width", width - margin.left - margin.right)
        .attr("height", height - margin.bottom - margin.top)
        .style("margin-left", -margin.left + "px")
        .style("margin.right", -margin.right + "px")
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
        .style("shape-rendering", "crispEdges");

    var grandparent = svg.append("g")
        .attr("class", "grandparent");

    grandparent.append("rect")
        .attr("y", -margin.top)
        .attr("width", width)
        .attr("height", margin.top);

    grandparent.append("text")
        .attr("x", 6)
        .attr("y", 6 - margin.top)
        .attr("dy", ".75em");

    var treemap = d3.treemap()
        //.tile(d3.treemapResquarify)
        .size([width, height])
        .round(false)
        .paddingInner(1);

    initialize(root);
    accumulate(root);
    layout(root);
    treemap(root);
    display(root);

    function display(d) {

        //...
    }

}

Here is the full code, based on the example on CodePen that you referenced:

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta charset="utf-8" />
    <style>
        text {
          pointer-events: none;
        }

        .grandparent text {
          font-weight: bold;
        }

        rect {
          fill: none;
          stroke: #fff;
        }

        rect.parent,
        .grandparent rect {
          stroke-width: 2px;
        }

        .grandparent rect {
          fill: orange;
        }

        .grandparent:hover rect {
          fill: #ee9700;
        }

        .children rect.parent,
        .grandparent rect {
          cursor: pointer;
        }

        .children rect.parent {
          fill: #bbb;
          fill-opacity: .5;
        }

        .children:hover rect.child {
          fill: #bbb;
        }
    </style>
</head>
<body>
    <p id="chart">
        Sample zoomable treemap
    </p>
    <div id="tree1"></div>
    <div id="tree2"></div>

    <script src="js/d3.v4.js"></script>
    <script>
        var data = {
            "name": 'Sample',
            "shortName": 'Sample',
            "children": [
              {
                "name": "6.1 Identify and plan learning needs",
                "shortName": "AITSL-A61",
                "size": null,
                "children": [
                  {
                    "name": "Analyse the Standards for.",
                    "shortName": "AITSL-A61-H",
                    "size": 59,
                    "children": [

                    ]
                  },
                  {
                    "name": "Demonstrate an  of the role of the",
                    "shortName": "AITSL-A61-G",
                    "size": 448,
                    "children": [

                    ]
                  },
                  {
                    "name": "Use  knowledge of the Standards for ",
                    "shortName": "AITSL-A61-L",
                    "size": 59,
                    "children": [

                    ]
                  },
                  {
                    "name": "Use the  plan learning needs.",
                    "shortName": "AITSL-A61-P",
                    "size": 101,
                    "children": [

                    ]
                  }
                ]
              },
              {
                "name": "6.2 Engage in improve practice",
                "shortName": "AITSL-A62",
                "size": null,
                "children": [
                  {
                    "name": "Participate in to update knowledge .",
                    "shortName": "AITSL-A62-P",
                    "size": 92,
                    "children": [

                    ]
                  },
                  {
                    "name": "Understand appropriate sources of .",
                    "shortName": "AITSL-A62-G",
                    "size": 405,
                    "children": [

                    ]
                  },
                  {
                    "name": "Plan for  and critiquing ",
                    "shortName": "AITSL-A62-H",
                    "size": 49,
                    "children": [

                    ]
                  },
                  {
                    "name": "Initiate to expand opportunities.",
                    "shortName": "AITSL-A62-L",
                    "size": 47,
                    "children": [

                    ]
                  }
                ]
              },
              {
                "name": "6.3 Engage with  and improve practice",
                "shortName": "AITSL-A63",
                "size": null,
                "children": [
                  {
                    "name": "Contribute to collegial  and apply.",
                    "shortName": "AITSL-A63-P",
                    "size": 84,
                    "children": [

                    ]
                  },
                  {
                    "name": "Initiate and engage in  discussions.",
                    "shortName": "AITSL-A63-H",
                    "size": 51,
                    "children": [

                    ]
                  },
                  {
                    "name": "Seek and feedback from .",
                    "shortName": "AITSL-A63-G",
                    "size": 458,
                    "children": [

                    ]
                  },
                  {
                    "name": "Implement  dialogue within   by .",
                    "shortName": "AITSL-A63-L",
                    "size": 40,
                    "children": [

                    ]
                  }
                ]
              },
              {
                "name": "6.4 Apply  improve learning",
                "shortName": "AITSL-A64",
                "size": null,
                "children": [
                  {
                    "name": "Undertake  .",
                    "shortName": "AITSL-A64-P",
                    "size": 76,
                    "children": [

                    ]
                  },
                  {
                    "name": "Demonstrate an  of the rationale.",
                    "shortName": "AITSL-A64-G",
                    "size": 426,
                    "children": [

                    ]
                  },
                  {
                    "name": "Engage with  to evaluate the .",
                    "shortName": "AITSL-A64-H",
                    "size": 54,
                    "children": [

                    ]
                  },
                  {
                    "name": "Advocate,  in and lead high-quality .",
                    "shortName": "AITSL-A64-L",
                    "size": 43,
                    "children": [

                    ]
                  }
                ]
              }
            ]
        };

        var margin = { top: 20, right: 0, bottom: 0, left: 0 },
            width = 640, //640
            height = 530,
            formatNumber = d3.format(",d"),
            transitioning;

        var x = d3.scaleLinear()
            .domain([0, width])
            .range([0, width]);

        var y = d3.scaleLinear()
            .domain([0, height - margin.top - margin.bottom])
            .range([0, height - margin.top - margin.bottom]);


        var color = d3.scaleOrdinal()
            .range(d3.schemeCategory10
            .map(function (c) { c = d3.rgb(c); c.opacity = 0.6; return c; }));

        var fader = function (color) { return d3.interpolateRgb(color, "#fff")(0.2); };

        var format = d3.format(",d");

        //var treemap;

        //var svg, grandparent;


        var root = d3.hierarchy(data)
            .eachBefore(function (d) { d.id = (d.parent ? d.parent.id + "." : "") + d.data.shortName; })
            .sum(function (d) { return d.size; })  // access the numeric attribute of the data
            .sort(function (a, b) {
                console.log('initial root sort a ' + a.value + ' b ' + b.value);
                return b.height - a.height || b.value - a.value;
            });

        function updateDrillDown() {

            drawTreemap("tree1");
            drawTreemap("tree2");

        };


        function drawTreemap(treeId) {

            var id = "#" + treeId;
            var svg = d3.select(id).append("svg")
                .attr("width", width - margin.left - margin.right)
                .attr("height", height - margin.bottom - margin.top)
                .style("margin-left", -margin.left + "px")
                .style("margin.right", -margin.right + "px")
                .append("g")
                .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
                .style("shape-rendering", "crispEdges");

            var grandparent = svg.append("g")
                .attr("class", "grandparent");

            grandparent.append("rect")
                .attr("y", -margin.top)
                .attr("width", width)
                .attr("height", margin.top);

            grandparent.append("text")
                .attr("x", 6)
                .attr("y", 6 - margin.top)
                .attr("dy", ".75em");

            var treemap = d3.treemap()
                //.tile(d3.treemapResquarify)
                .size([width, height])
                .round(false)
                .paddingInner(1);

            initialize(root);
            accumulate(root);
            layout(root);
            treemap(root);
            display(root);



            function display(d) {

                grandparent
                    .datum(d.parent)
                    .on("click", transition)
                  .select("text")
                    .text(name(d));

                var g1 = svg.insert("g", ".grandparent")
                    .datum(d)
                    .attr("class", "depth");

                var g = g1.selectAll("g")
                    .data(d._children)
                  .enter().append("g");

                g.filter(function (d) { return d._children; })
                    .classed("children", true)
                    .on("click", transition);

                var children = g.selectAll(".child")
                    .data(function (d) { return d._children || [d]; })
                  .enter().append("g");

                children.append("rect")
                    .attr("class", "child")
                    .call(rect)
                  .append("title")
                    .text(function (d) { return d.data.shortName + " (" + formatNumber(d.value) + ")"; });

                children.append("text")
                    .attr("class", "ctext")
                    .text(function (d) { return d.data.shortName; })
                    .call(text2);

                g.append("rect")
                    .attr("class", "parent")
                    .call(rect);


                var t = g.append("text")
                  .attr("class", "ptext")
                  .attr("dy", ".75em")

                t.append("tspan")
                  .text(function (d) { return d.data.shortName; });
                t.append("tspan")
                  .attr("dy", "1.0em")
                  .text(function (d) { return formatNumber(d.value); });
                t.call(text);

                g.selectAll("rect")
                  .style("fill", function (d) { return color(d.data.shortName); });



                function transition(d) {
                    if (transitioning || !d) return;
                    transitioning = true;

                    var g2 = display(d),
                        t1 = g1.transition().duration(750),
                        t2 = g2.transition().duration(750);


                    // Update the domain only after entering new elements.
                    x.domain([d.x0, d.x0 + d.x1]);
                    y.domain([d.y0, d.y0 + d.y1]);

                    // Enable anti-aliasing during the transition.
                    svg.style("shape-rendering", null);

                    // Draw child nodes on top of parent nodes.
                    svg.selectAll(".depth").sort(function (a, b) {
                        console.log('.depth sort a ' + a.depth + ' b ' + b.depth);
                        return a.depth - b.depth;
                    });

                    // Fade-in entering text.
                    g2.selectAll("text").style("fill-opacity", 0);

                    // Transition to the new view.
                    t1.selectAll("text").call(text).style("fill-opacity", 0);
                    t2.selectAll("text").call(text).style("fill-opacity", 1);
                    t1.selectAll("rect").call(rect);
                    t2.selectAll("rect").call(rect);

                    // Remove the old node when the transition is finished.
                    t1.remove().on("end", function () {
                        svg.style("shape-rendering", "crispEdges");
                        transitioning = false;
                    });
                }

                return g;
            }


        }  // end of drawTreemap()



        function initialize(root) {
            root.x = root.y = 0;
            root.x1 = width;
            root.y1 = height;
            root.depth = 0;
        }

        // Aggregate the values for internal nodes. This is normally done by the
        // treemap layout, but not here because of our custom implementation.
        // We also take a snapshot of the original children (_children) to avoid
        // the children being overwritten when when layout is computed.
        function accumulate(d) {
            console.log('accumulate called ' + d.data.name);
            return (d._children = d.children)
                ? d.value = d.children.reduce(function (p, v) { return p + accumulate(v); }, 0)
                : d.value;
        }

        // Compute the treemap layout recursively such that each group of siblings
        // uses the same size (1×1) rather than the dimensions of the parent cell.
        // This optimizes the layout for the current zoom state. Note that a wrapper
        // object is created for the parent node for each group of siblings so that
        // the parent’s dimensions are not discarded as we recurse. Since each group
        // of sibling was laid out in 1×1, we must rescale to fit using absolute
        // coordinates. This lets us use a viewport to zoom.
        function layout(d) {
            if (d._children) {
                //    treemap.nodes({_children: d._children});
                //    treemap(d);
                d._children.forEach(function (c) {
                    c.x0 = d.x0 + c.x0 * d.x1;
                    c.y0 = d.y0 + c.y0 * d.y1;
                    c.x1 *= d.x1;
                    c.y1 *= d.y1;
                    c.parent = d;
                    layout(c);
                });
            }
        }


        function text(text) {
            text.selectAll("tspan")
                .attr("x", function (d) { return x(d.x0) + 6; })
            text.attr("x", function (d) { return x(d.x0) + 6; })
                .attr("y", function (d) { return y(d.y0) + 10; })
                .style("opacity", function (d) {
                    console.log("text opacity setting textlength " + this.getComputedTextLength() + " d size " + (x(d.x0 + d.x1) - x(d.x0)));
                    return this.getComputedTextLength() < x(d.x0 + d.x1) - x(d.x0) ? 1 : 0;
                });
        }

        function text2(text) {
            text.attr("x", function (d) { return x(d.x0 + d.x1) - this.getComputedTextLength() - 6; })
                .attr("y", function (d) { return y(d.y0 + d.y1) - 6; })
                .style("opacity", function (d) { return this.getComputedTextLength() < x(d.x0 + d.x1) - x(d.x0) ? 1 : 0; });
        }

        function rect(rect) {
            rect.attr("x", function (d) { return x(d.x0); })
                .attr("y", function (d) { return y(d.y0); })
                .attr("width", function (d) {
                    console.log('id ' + d.id + ' rect width ' + (d.x1 - d.x0));
                    return x(d.x0 + d.x1) - x(d.x0);
                    //return (d.x1 -d.x0);

                })
                .attr("height", function (d) {
                    console.log('id ' + d.id + ' rect height ' + (d.y1 - d.y0) + ' ordinal ' + (y(d.y1 + d.y0) - y(d.y0)));
                    return y(d.y0 + d.y1) - y(d.y0);
                    //return y(d.y1 - d.y0);

                });
        }

        function name(d) {
            return d.parent
                ? name(d.parent) + " / " + d.data.shortName + " (" + formatNumber(d.value) + ")"
                : d.data.shortName + " (" + formatNumber(d.value) + ")";
        }

        //$(function () {  // I have commented this out because I have not included jQuery.js
            updateDrillDown();
        //});

    </script>
</body>
</html>

Edit to explain how to display only one chart at once:

One way to display only one of the charts at a time, depending on the value of a <select> box, is to draw both charts but hide one of them using the display style property. For example:

    <!--Add this HTML-->
    <select id="chart_selector" onchange="toggleChartVisibility();">
        <option value="1" selected="selected">Chart 1</option>
        <option value="2">Chart 2</option>
    </select>
    <br />
    <div id="tree1"></div>
    <div id="tree2" style="display:none;"></div><!--Add this style to the second <div>-->

    <!-- ... -->
    <script>
        function toggleChartVisibility() {
            var chosen = document.getElementById("chart_selector").value;
            if (chosen == "1") {
                document.getElementById("tree1").style.display = "block";
                document.getElementById("tree2").style.display = "none";
            } else {
                document.getElementById("tree1").style.display = "none";
                document.getElementById("tree2").style.display = "block";
            }
        }
    </script>

Using display to hide one of the charts has the advantage that the charts are not redrawn from scratch each time the user changes the selected chart.

Alternatively, if you wish to have only a single chart drawn in the document at any given time, and to draw a chart each time the user changes the selected chart, you can do so by triggering updateDrillDown() directly from changes to the <select> box and including code in updateDrillDown() that clears the old chart before drawing the new one:

    <select id="chart_selector" onchange="updateDrillDown();">
        <option value="1" selected="selected">Chart 1</option>
        <option value="2">Chart 2</option>
    </select>
    <br />
    <div id="tree1"></div>
    <div id="tree2"></div>

and

        function updateDrillDown() {

            var chosen = document.getElementById("chart_selector").value;
            if (chosen == "1") {
                document.getElementById("tree2").innerHTML = "";
                drawTreemap("tree1");
            } else {
                document.getElementById("tree1").innerHTML = "";
                drawTreemap("tree2");
            }
        };

If you have different data for each chart, you should also include the code to create root inside drawTreemap() and add a condition on treeId that determines which data to use.

Related