block by timelyportfolio e563b1397d6e32bcd823633aa7fb66a8

Working Site Tree - conditional

Full Screen

forked from stephlocke‘s block: Working Site Tree

index.html

<!DOCTYPE html>
<script src="https://unpkg.com/d3@4.12.2/build/d3.min.js"></script>
<script src="./htmlwidgets.js"></script>
<script src="./collapsibleTree.js"></script>
<script src="https://use.fontawesome.com/9f12702d26.js"></script>
<link href="./collapsibleTree.css" rel="stylesheet" />
<link href="./LockeTree.css" rel="stylesheet" />

<script type="application/json" data-for="htmlwidget-219e4959c6412cc5305e">{"x":{"data":{"name":"Locke Data","fa":"fa-home","SizeOfNode":30,"children":[{"name":"For your organisation","fa":"fa-users","SizeOfNode":25,"children":[{"name":"Training","fa":"fa-graduation-cap","SizeOfNode":20,"children":[{"name":"On-site »","fa":"fa-building","SizeOfNode":15,"url":"https://itsalocke.com/company/recommendedtraining"},{"name":"Public »","fa":"fa-ticket","SizeOfNode":15,"url":"https://itsalocke.com/#calendar"},{"name":"Online»","fa":"fa-laptop","SizeOfNode":15,"url":"https://itsalocke.com/company/onlinetraining"}]},{"name":"Support","fa":"fa-headphones","SizeOfNode":20,"children":[{"name":"Remote Guru »","fa":"fa-user-circle","SizeOfNode":15,"url":"https://itsalocke.com/company/remoteguru"}]},{"name":"Consultancy","fa":"fa-user-md","SizeOfNode":20,"children":[{"name":"Data Science Readiness »","fa":"fa-flask","SizeOfNode":15,"url":"https://itsalocke.com/company/readiness"},{"name":"Help on projects »","fa":"fa-gears","SizeOfNode":15,"url":"https://itsalocke.com/company/techleadership"},{"name":"Need a review »","fa":"fa-edit","SizeOfNode":15,"url":"https://itsalocke.com/company/peerreview"},{"name":"Other »","fa":"fa-envelope","SizeOfNode":15,"url":"https://itsalocke.com/#contact"}]}]},{"name":"For you","fa":"fa-user","SizeOfNode":25,"children":[{"name":"Read","fa":"fa-book","SizeOfNode":20,"children":[{"name":"Books »","fa":"fa-book","SizeOfNode":15,"url":"https://itsalocke.com/company/books"},{"name":"Blog »","fa":"fa-newspaper-o","SizeOfNode":15,"url":"https://itsalocke.com/blog"}]},{"name":"Events","fa":"fa-calendar","SizeOfNode":20,"children":[{"name":"Upcoming events »","fa":"fa-ticket","SizeOfNode":15,"url":"https://itsalocke.com/#calendar"},{"name":"Past talks and slides »","fa":"fa-upload","SizeOfNode":15,"url":"https://itsalocke.com/talks"}]},{"name":"Online","fa":"fa-globe","SizeOfNode":20,"children":[{"name":"Learn R by email »","fa":"fa-inbox","SizeOfNode":15,"url":"//eepurl.com/df8me1"},{"name":"Newsletter »","fa":"fa-envelope-open","SizeOfNode":15,"url":"//eepurl.com/df8me1"},{"name":"GitHub »","fa":"fa-github","SizeOfNode":15,"url":"https://github.com/lockedata"},{"name":"Packages »","fa":"fa-cube","SizeOfNode":15,"url":"https://itsalocke.com/oss/packages"},{"name":"Videos »","fa":"fa-youtube-play","SizeOfNode":15,"url":"https://www.youtube.com/channel/UCAg9ZUc9v3YedOuyAd-nDZQ"}]}]}]},"options":{"hierarchy":["L1","L2","L3"],"input":null,"attribute":"leafCount","linkLength":null,"fontSize":24,"tooltip":false,"collapsed":true,"zoomable":true,"margin":{"top":20,"bottom":20,"left":179.05,"right":190}}},"evals":[],"jsHooks":[]}</script>
<script type="application/htmlwidget-sizing" data-for="htmlwidget-219e4959c6412cc5305e">{"viewer":{"width":450,"height":350,"padding":0,"fill":true},"browser":{"width":960,"height":500,"padding":40,"fill":false}}</script>

<div id="htmlwidget_container" style="background-color:#2165B6;">
  <div id="htmlwidget-219e4959c6412cc5305e" style="width:100%;height:500px;" class="collapsibleTree html-widget"></div>
</div>

LockeTree.css

.collapsibleTree .node circle {
  fill: #E8830C;
  stroke: #E8830C;
}

.collapsibleTree .node text {
  fill: white;
}

.collapsibleTree .link {
  stroke-opacity: 0.5;
  stroke: white;
}

.collapsibleTree .node  a {
  fill: white;
  text-decoration: underline;
}

.fa {
  color: white;
  font-size: 3em;
}

collapsibleTree.css

.collapsibleTree .node {
  cursor: pointer;
}

.collapsibleTree .node circle {
  fill: #fff;
  stroke: #000;
}

.collapsibleTree .node text {
  font: 10px sans-serif;
}

.collapsibleTree .link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}

.collapsibleTree div.tooltip {
    position: absolute;
    text-align: center;
    padding: 3px;
    font: 10px sans-serif;
    background: #fff;
    border: 2px;
    border-radius: 8px;
    border-style: solid;
    pointer-events: none;
}

collapsibleTree.js

HTMLWidgets.widget({

  name: 'collapsibleTree',
  type: 'output',

  factory: function(el, width, height) {

    var i = 0,
    duration = 750,
    root = {},
    options = {},
    treemap;

    // Optionally enable zooming, and limit to 1/5x or 5x of the original viewport
    var zoom = d3.zoom()
    .scaleExtent([1/5, 5])
    .on('zoom', function () {
      if (options.zoomable) svg.attr('transform', d3.event.transform)
    })

    // create our tree object and bind it to the element
    // appends a 'group' element to 'svg'
    // moves the 'group' element to the top left margin
    var svg = d3.select(el).append('svg')
    .attr('width', width)
    .attr('height', height)
    .call(zoom)
    .append('g');

    // Define the div for the tooltip
    var tooltip = d3.select(el).append('div')
    .attr('class', 'tooltip')
    .style('opacity', 0);

    function update(source) {

      // Assigns the x and y position for the nodes
      var treeData = treemap(root);

      // Compute the new tree layout.
      var nodes = treeData.descendants(),
      links = treeData.descendants().slice(1);

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

      // ****************** Nodes section ***************************

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

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

      // Add tooltips, if specified in options
      if (options.tooltip) {
        nodeEnter = nodeEnter
        .on('mouseover', mouseover)
        .on('mouseout', mouseout);
      }

      // Add Circle for the nodes
      nodeEnter.append('circle')
      .attr('class', 'node')
      .attr('r', 1e-6)
      .style('fill', function(d) {
        return d.data.fill || (d._children ? options.fill : '#E8830C');
      })
      .style('stroke-width', function(d) {
        return d._children ? 3 : 1;
      });
      // Add fontawesome node circles
      nodeEnter.append('svg:foreignObject')
      .attr('class', 'handle')
      .html(function(d) { return d.data.url ? '<a href="'+d.data.url+'"><i class="fa ' + d.data.fa + '"></i></a>': '<i class="fa ' + d.data.fa + '"></i>'; });


      // Add links for the nodes with no url
      nodeEnter.filter(function(d) {return typeof(d.data.url) === "undefined"})
				.append("text")
        .attr('text-anchor', 'end')
        .style('font-size', options.fontSize + 'px')
        .text(function(d) { return d.data.name; });

      
      // Add links for the nodes with url
      nodeEnter.filter(function(d) {return d.data.url})
				.append("text")
        .append("a")
        .attr("href", function(d) {return d.data.url})
        .attr('text-anchor', 'end')
        .style('font-size', options.fontSize + 'px')
        .text(function(d) { return d.data.name; });


      // UPDATE
      var nodeUpdate = nodeEnter.merge(node);

      // Transition to the proper position for the node
      nodeUpdate.transition()
      .duration(duration)
      .attr('transform', function(d) {
        return 'translate(' + d.y + ',' + d.x + ')';
      });

      // Update the node attributes and style
      nodeUpdate.select('circle.node')
      .attr('r', function(d) {
        return d.data.SizeOfNode || 10; // default radius is 10
      })
      .style('fill', function(d) {
        return d.data.fill || (d._children ? options.fill : '#E8830C');
      })
      .style('stroke-width', function(d) {
        return d._children ? 3 : 1;
      })
      .attr('cursor', 'pointer');


      // Remove any exiting nodes
      var nodeExit = node.exit().transition()
      .duration(duration)
      .attr('transform', function(d) {
        return 'translate(' + source.y + ',' + source.x + ')';
      })
      .remove();

      // On exit reduce the node circles size to 0
      nodeExit.select('circle')
      .attr('r', 1e-6);

      // On exit reduce the opacity of text labels
      nodeExit.select('text')
      .style('fill-opacity', 1e-6);

      // ****************** links section ***************************

      // Update the links...
      var link = svg.selectAll('path.link')
      .data(links, function(d) { return d.id; });

      // Enter any new links at the parent's previous position.
      var linkEnter = link.enter().insert('path', 'g')
      .attr('class', 'link')
      // Potentially, this may one day be mappable
      // .style('stroke-width', function(d) { return d.data.linkWidth || 1 })
      .attr('d', function(d){
        var o = { x: source.x0, y: source.y0 }
        return diagonal(o, o)
      });

      // UPDATE
      var linkUpdate = linkEnter.merge(link);

      // Transition back to the parent element position
      linkUpdate.transition()
      .duration(duration)
      .attr('d', function(d){ return diagonal(d, d.parent) });

      // Remove any exiting links
      var linkExit = link.exit().transition()
      .duration(duration)
      .attr('d', function(d) {
        var o = {x: source.x, y: source.y}
        return diagonal(o, o)
      })
      .remove();

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

      // Creates a curved (diagonal) path from parent to the child nodes
      function diagonal(s, d) {

        path = 'M ' + s.y + ' ' + s.x + ' C ' +
        (s.y + d.y) / 2 + ' ' + s.x + ', ' +
        (s.y + d.y) / 2 + ' ' + d.x + ', ' +
        d.y + ' ' + d.x;

        return path
      }

      // Toggle children on click.
      function click(d) {
        if (d.children) {
          d._children = d.children;
          d.children = null;
        } else {
          d.children = d._children;
          d._children = null;
        }
        update(d);
        // Hide the tooltip after clicking
        tooltip.transition()
        .duration(100)
        .style('opacity', 0)
        // Update Shiny inputs, if applicable
        if (options.input) {
          var nest = {},
          obj = d;
          // Navigate up the list and recursively find parental nodes
          for (var n = d.depth; n > 0; n--) {
            nest[options.hierarchy[n-1]] = obj.data.name
            obj = obj.parent
          }
          Shiny.onInputChange(options.input, nest)
        }
      }

      // Show tooltip on mouseover
      function mouseover(d) {
        tooltip.transition()
        .duration(200)
        .style('opacity', .9);

        // Show either a constructed tooltip, or override with one from the data
        tooltip.html(
          d.data.tooltip || d.data.name + '<br>' +
          options.attribute + ': ' + d.data.WeightOfNode
        )
        // Make the tooltip font size just a little bit bigger
        .style('font-size', (options.fontSize + 1) + 'px')
        .style('left', (d3.event.layerX) + 'px')
        .style('top', (d3.event.layerY - 30) + 'px');
      }
      // Hide tooltip on mouseout
      function mouseout(d) {
        tooltip.transition()
        .duration(500)
        .style('opacity', 0);
      }
    }

    return {
      renderValue: function(x) {
        // Assigns parent, children, height, depth
        root = d3.hierarchy(x.data, function(d) { return d.children; });
        root.x0 = height / 2;
        root.y0 = 0;

        // Attach options as a property of the instance
        options = x.options;

        // Update the canvas with the new dimensions
        svg = svg.attr('transform', 'translate('
        + options.margin.left + ',' + options.margin.top + ')')

        // width and height, corrected for margins
        var heightMargin = height - options.margin.top - options.margin.bottom,
        widthMargin = width - options.margin.left - options.margin.right;
        // declares a tree layout and assigns the size
        treemap = d3.tree().size([heightMargin, widthMargin]);

        // Calculate a reasonable link length, if not otherwise specified
        if (!options.linkLength) {
          options.linkResponsive = true
          options.linkLength = widthMargin / options.hierarchy.length
          if (options.linkLength < 10) {
            options.linkLength = 10 // Offscreen or too short
          }
        }

        // Optionally collapse after the second level
        if (options.collapsed) root.children.forEach(collapse);
        update(root);

        // Collapse the node and all it's children
        function collapse(d) {
          if(d.children) {
            d._children = d.children
            d._children.forEach(collapse)
            d.children = null
          }
        }
      },

      resize: function(width, height) {
        // Resize the canvas
        d3.select(el).select('svg')
        .attr('width', width)
        .attr('height', height);

        // width and height, corrected for margins
        var heightMargin = height - options.margin.top - options.margin.bottom,
        widthMargin = width - options.margin.left - options.margin.right;

        // Calculate a reasonable link length, if not originally specified
        if (options.linkResponsive) {
          options.linkLength = widthMargin / options.hierarchy.length
          if (options.linkLength < 10) {
            options.linkLength = 10 // Offscreen or too short
          }
        }
        // Update the treemap to fit the new canvas size
        treemap = d3.tree().size([heightMargin, widthMargin]);
        update(root)
      },
      // Make the instance properties available as a property of the widget
      svg: svg,
      root: root,
      options: options
    };
  }
});