whytheq
whytheq

Reputation: 35605

Add transition to Zoomable Circle Packing

I have this data in flare4.csv:

SplitMth,id,value
Dec,FRUIT,
Dec,FRUIT~Apples,
Dec,FRUIT~Apples~X,100
Dec,FRUIT~Pears,
Dec,FRUIT~Pears~Y,200
Dec,FRUIT~Lemon,
Dec,FRUIT~Lemon~Z,300
Jan,FRUIT,
Jan,FRUIT~Apples,
Jan,FRUIT~Apples~X,300
Jan,FRUIT~Pears,
Jan,FRUIT~Pears~Y,200
Jan,FRUIT~Lemon,
Jan,FRUIT~Lemon~Z,100

I then have the following circle packing visualization , an adaptation of this bl.ock: https://bl.ocks.org/mbostock/7607535

<!DOCTYPE html>
<html>

<head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8">
    <title>ZOOMABLE CIRCLE PACKING</title>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <style>
    .node {
        cursor: pointer;
    }

    .node:hover {
        stroke: #000;
        stroke-width: 1.5px;
    }

    .node--leaf {
        fill: white;
    }

    .label {
        font: 9px "Helvetica Neue", Helvetica, Arial, sans-serif;
        text-anchor: middle;
        text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff, 0 -1px 0 #fff;
    }

    .label,
    .node--root,
    .node--leaf {
        pointer-events: none;
    }

    .clickBut {
        font-family: verdana;
        font-size: 11px;
        /*font-weight: bold;*/
        background-color: #ffa500 !important;
        border-radius: 100px;
        shape-rendering: crispEdges;
    }

    #zoomable {
        position: absolute;
        top: 35px;
        left: 25px;
        width: 250px;
        height: 250px;
    }
    </style>
</head>

<body>
    <div id="zoomable"></div>
    <input id="clickMe" type="button" value="Refresh|Reset" onclick="resetAll();" class="clickBut" />
    <select id="mth-menu"></select>
    <script type="text/javascript">
    var zoomableDataX = "flare4.csv";

    function resetAll() {
        console.log("resetAll pressed") //<< placeholder
    }

    populateMonthCombo();

    function populateMonthCombo() {
        removeOptions(document.getElementById("mth-menu"));

        d3.csv(zoomableDataX, function(rows) {

            //data for the combobox control
            dtaMths = rows.filter(function(row) {
                if ((1 === 1)) { //<< placeholder
                    return true;
                }
            }).map(function(d) {
                return {
                    "SplitMth": d.SplitMth
                };
            })

            var menu = d3.select("#mth-menu");

            menu.selectAll("option")
                .data(d3.map(dtaMths, function(d) {
                    return d.SplitMth;
                }).keys())
                .enter()
                .append("option")
                .property("selected", function(d) {
                    return d === "Jan";
                })
                .text(function(d) {
                    return d;
                });

            menu
                .on("change", function() {
                    changeCircles(this.value)
                });

        });
    };

    function removeOptions(selectbox) {
        var i;
        for (i = selectbox.options.length - 1; i >= 0; i--) {
            selectbox.remove(i);
        }
    };


    var zoomDiv = d3.select("#zoomable")

    var svg = zoomDiv.append("svg").attr("width", "250").attr("height", "250"),
        margin = 20,
        diameter = +svg.attr("width"),
        g = svg.append("g").attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");


    //added>>>>>>
    var format = d3.format(",d");
    //>>>>>>>>>>>

    var color = d3.scaleLinear()
        .domain([-1, 5])
        .range(["hsl(152,80%,80%)", "hsl(228,30%,40%)"])
        .interpolate(d3.interpolateHcl);

    //added>>>>>>
    var stratify = d3.stratify()
        .parentId(function(d) {
            return d.id.substring(0, d.id.lastIndexOf("~"));
        });
    //>>>>>>>>>>>

    var pack = d3.pack()
        .size([diameter - margin, diameter - margin])
        .padding(2);


    function changeCircles(mth) {
        console.log(mth);
        d3.csv(zoomableDataX, function(error, data) {
            if (error) throw error;

            dta = data.filter(function(row) {
                if (row['SplitMth'] === mth) {
                    return true;
                }
            }).map(function(d) {
                return {
                    "id": d.id,
                    "value": +d.value,
                };
            })

            //added>>>>>>
            root = stratify(dta)
                .sum(function(d) {
                    return d.value;
                })
                .sort(function(a, b) {
                    return b.value - a.value;
                });
            //>>>>>>>>>>>


            var focus = root,
                nodes = pack(root).descendants(),
                view;

            var circle = g.selectAll("circle")
                .data(nodes)
                .enter().append("circle")
                .attr("class", function(d) {
                    return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root";
                })
                .style("fill", function(d) {
                    return d.children ? color(d.depth) : null;
                })
                .on("click", function(d) {
                    if (focus !== d) zoom(d), d3.event.stopPropagation();
                });


            var text = g.selectAll("text")
                .data(nodes)
                .enter().append("text")
                .attr("class", "label")
                .style("fill-opacity", function(d) {
                    return d.parent === root ? 1 : 0;
                })
                .style("display", function(d) {
                    return d.parent === root ? "inline" : "none";
                })
                .text(function(d) {
                    return d.id.substring(d.id.lastIndexOf("~") + 1).split(/(?=[A-Z][^A-Z])/g);
                })

            var node = g.selectAll("circle,text");

            node.append("title")
                .text(function(d) {
                    return d.id + "\n" + format(d.value);
                });

            svg
                .on("click", function() {
                    zoom(root);
                });

            zoomTo([root.x, root.y, root.r * 2 + margin]);

            function zoom(d) {
                var focus0 = focus;
                focus = d;

                var transition = d3.transition()
                    .duration(d3.event.altKey ? 7500 : 750)
                    .tween("zoom", function(d) {
                        var i = d3.interpolateZoom(view, [focus.x, focus.y, focus.r * 2 + margin]);
                        return function(t) {
                            zoomTo(i(t));
                        };
                    });

                transition.selectAll("text")
                    .filter(function(d) {
                        return d.parent === focus || this.style.display === "inline";
                    })
                    .style("fill-opacity", function(d) {
                        return d.parent === focus ? 1 : 0;
                    })
                    .on("start", function(d) {
                        if (d.parent === focus) this.style.display = "inline";
                    })
                    .on("end", function(d) {
                        if (d.parent !== focus) this.style.display = "none";
                    });

            }

            function zoomTo(v) {
                var k = diameter / v[2];
                view = v;
                node.attr("transform", function(d) {
                    return "translate(" + (d.x - v[0]) * k + "," + (d.y - v[1]) * k + ")";
                });
                circle.attr("r", function(d) {
                    return d.r * k;
                });
            }

        })
    };

    changeCircles("Jan");
    </script>
</body>

When initially rendered it is as expected and will zoom in a predictable manner.
Live preview here: http://plnkr.co/edit/iPIsldJn09ZyLoc3GVrX?p=preview

I'd like to be able to do two things but I guess the second will need to be an additional question.

How do I amend the code so that when I select "Dec" from the input drop-down the visualization changes accordingly and behaves as expected when clicking to zoom?

(I'd like to make the above change a 5 second transition like this visual but that will be a second question : http://jsfiddle.net/VividD/WDCpq/8/)

Upvotes: 0

Views: 417

Answers (1)

Mark
Mark

Reputation: 108567

You aren't handling the update and exit selections at all. You are only handling the enter. Here's some reading here and here.

Here's how to rewrite you drawing function the correct way:

function changeCircles(mth) {

  // Why download the file each time?

  dta = data.filter(function(row) {
    if (row['SplitMth'] === mth) {
      return true;
    }
  }).map(function(d) {
    return {
      "id": d.id,
      "value": +d.value,
    };
  })

  //added>>>>>>
  root = stratify(dta)
    .sum(function(d) {
      return d.value;
    })
    .sort(function(a, b) {
      return b.value - a.value;
    });
  //>>>>>>>>>>>


  var focus = root,
    nodes = pack(root).descendants(),
    view;

  var circle = g.selectAll("circle")
    .data(nodes, function(d){
      return d.id; //<-- add a key function to uniquely identify a node
    });

  circle.exit().remove(); //<-- those nodes leaving

  circle = circle.enter() //<-- those nodes entering
    .append("circle")
    .attr("class", function(d) {
      return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root";
    })
    .style("fill", function(d) {
      return d.children ? color(d.depth) : null;
    })
    .on("click", function(d) {
      if (focus !== d) zoom(d), d3.event.stopPropagation();
    })
    .merge(circle); //<-- those entering and updating

 ...

Updated plunker.

Upvotes: 3

Related Questions