Skip to content

Instantly share code, notes, and snippets.

@tenderlove
Created January 20, 2013 05:13
Show Gist options
  • Select an option

  • Save tenderlove/4576780 to your computer and use it in GitHub Desktop.

Select an option

Save tenderlove/4576780 to your computer and use it in GitHub Desktop.

Revisions

  1. tenderlove created this gist Jan 20, 2013.
    31 changes: 31 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,31 @@
    <!DOCTYPE html>
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
    <link type="text/css" rel="stylesheet" href="style.css"/>
    <script src="http://d3js.org/d3.v3.min.js"></script>
    <style>
    .node circle {
    cursor: pointer;
    fill: #fff;
    stroke: steelblue;
    stroke-width: 1.5px;
    }

    .node text {
    font-size: 11px;
    }

    path.link {
    fill: none;
    stroke: #ccc;
    stroke-width: 1.5px;
    }
    </style>
    </head>
    <body>
    <div id="body">
    </div>
    <script src="tree.js"></script>
    </body>
    </html>
    37 changes: 37 additions & 0 deletions test.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,37 @@
    require 'webrick'
    require 'json'
    require 'objspace'

    ROOT = [
    Object.new,
    1,
    { "foo" => :bar, 2.2 => [1,2,3,4] }
    ]

    def leaves o
    ObjectSpace.reachable_objects_from(o).map do |obj|
    node obj
    end
    end

    def node o
    {
    'name' => o.class,
    'child_link' => "/leaves.json?id=#{o.object_id}"
    }
    end

    server = WEBrick::HTTPServer.new Port: 8000, DocumentRoot: '.'

    server.mount_proc '/leaves.json' do |req, res|
    id = req.query['id'].to_i
    res.body = JSON.dump leaves ObjectSpace._id2ref id
    end

    server.mount_proc '/root.json' do |req, res|
    res.body = JSON.dump node ROOT
    end

    trap 'INT' do server.shutdown end

    server.start
    147 changes: 147 additions & 0 deletions tree.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,147 @@
    var m = [20, 120, 20, 120],
    w = 1280 - m[1] - m[3],
    h = 800 - m[0] - m[2],
    i = 0,
    root;

    var tree = d3.layout.tree()
    .size([h, w]);

    var diagonal = d3.svg.diagonal()
    .projection(function(d) { return [d.y, d.x]; });

    var vis = d3.select("#body").append("svg:svg")
    .attr("width", w + m[1] + m[3])
    .attr("height", h + m[0] + m[2])
    .append("svg:g")
    .attr("transform", "translate(" + m[3] + "," + m[0] + ")");

    d3.json("root.json", function(json) {
    root = json;
    root.x0 = h / 2;
    root.y0 = 0;

    function toggleAll(d) {
    if (d.children) {
    d.children.forEach(toggleAll);
    toggle(d);
    }
    }

    update(root);
    });

    function click(d) {
    toggle(d);

    if (d.seen) {
    update(d);
    } else {
    d3.json(d.child_link, function(json) {
    d.children = json;
    d.seen = true;
    update(d);
    });
    }
    }

    function update(source) {
    var duration = d3.event && d3.event.altKey ? 5000 : 500;

    // Compute the new tree layout.
    var nodes = tree.nodes(root).reverse();

    // Normalize for fixed-depth.
    nodes.forEach(function(d) { d.y = d.depth * 180; });

    // Update the nodes…
    var node = vis.selectAll("g.node")
    .data(nodes, function(d) { return d.id || (d.id = ++i); });

    // Enter any new nodes at the parent's previous position.
    var nodeEnter = node.enter().append("svg:g")
    .attr("class", "node")
    .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
    .on("click", click);

    nodeEnter.append("svg:circle")
    .attr("r", 1e-6)
    .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

    nodeEnter.append("svg:text")
    .attr("x", function(d) { return d.children || d._children ? -10 : 10; })
    .attr("dy", ".35em")
    .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
    .text(function(d) { return d.name; })
    .style("fill-opacity", 1e-6);

    // Transition nodes to their new position.
    var nodeUpdate = node.transition()
    .duration(duration)
    .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });

    nodeUpdate.select("circle")
    .attr("r", 4.5)
    .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

    nodeUpdate.select("text")
    .style("fill-opacity", 1);

    // Transition exiting nodes to the parent's new position.
    var nodeExit = node.exit().transition()
    .duration(duration)
    .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
    .remove();

    nodeExit.select("circle")
    .attr("r", 1e-6);

    nodeExit.select("text")
    .style("fill-opacity", 1e-6);

    // Update the links…
    var link = vis.selectAll("path.link")
    .data(tree.links(nodes), function(d) { return d.target.id; });

    // Enter any new links at the parent's previous position.
    link.enter().insert("svg:path", "g")
    .attr("class", "link")
    .attr("d", function(d) {
    var o = {x: source.x0, y: source.y0};
    return diagonal({source: o, target: o});
    })
    .transition()
    .duration(duration)
    .attr("d", diagonal);

    // Transition links to their new position.
    link.transition()
    .duration(duration)
    .attr("d", diagonal);

    // Transition exiting nodes to the parent's new position.
    link.exit().transition()
    .duration(duration)
    .attr("d", function(d) {
    var o = {x: source.x, y: source.y};
    return diagonal({source: o, target: o});
    })
    .remove();

    // Stash the old positions for transition.
    nodes.forEach(function(d) {
    d.x0 = d.x;
    d.y0 = d.y;
    });
    }

    // Toggle children.
    function toggle(d) {
    if (d.children) {
    d._children = d.children;
    d.children = null;
    } else {
    d.children = d._children;
    d._children = null;
    }
    }