Skip to content

Instantly share code, notes, and snippets.

@timelyportfolio
Last active October 28, 2017 12:25
Show Gist options
  • Save timelyportfolio/a6f2f931935025b0476ea6180d348c59 to your computer and use it in GitHub Desktop.
Save timelyportfolio/a6f2f931935025b0476ea6180d348c59 to your computer and use it in GitHub Desktop.
parttree on Titanic
license: mit

This example courtesy of Displayr who have generously offered to sponsor a series of independently authored posts about interactive visualization with R and JavaScript. Thank you so much Displayr for this opportunity.


Code in R

Below is the R source code for creating this visualization. You might notice that much of this is actually JavaScript. To see the JavaScript, please inspect index.html.

(function() {
  library(htmltools)
  library(d3r)
  library(dplyr)
  
  titan_nest <<- as.data.frame(Titanic) %>%
    select(-Age) %>%
    group_by(Class, Sex, Survived) %>%
    summarise(Freq = sum(Freq)) %>%
    d3_nest(value_cols="Freq", root="Titanic")
})()


scr_part <- function() {
  tags$script(HTML(
    sprintf(
      "
      function initialize(hier) {
      d3.partition().size([height,width])(hier);
      
      var nodes = svg.selectAll('g.node')
      .data(hier.descendants());
      
      nodes = nodes.merge(
      nodes.enter().append('g')
      .attr('class','node')
      );
      
      nodes.attr('transform', function(d) {
      return 'translate(' + d.y0  + ',' + d.x0 + ')'
      });
      
      nodes.append('rect')
      .classed('rect-part', true);
      
      nodes.append('rect')
      .classed('rect-outline', true)
      .attr('width', function(d) { return d.y1 - d.y0; })
      .attr('height', function(d) { return d.x1 - d.x0; })
      .style('fill', 'none')
      .style('stroke', 'none');
      
      nodes.append('text')
      //.style('text-anchor','end')
      .attr('dy','1em')
      .text(function(d) { return d.data.name});
      
      return nodes;
      }
      
      function drawPart(hier, nodes, duration, delay) {
      d3.partition().size([height,width])(hier);
      
      svg.selectAll('.link')
      .transition()
      .duration(100)
      .style('opacity', 0.00001)
      .remove();
      
      nodes.selectAll('rect.rect-part')
      .transition()
      .duration(duration)
      .delay(delay)
      .attr('width', function(d) { return d.y1 - d.y0; })
      .attr('height', function(d) { return d.x1 - d.x0; })
      .attr('x', 0)
      .attr('y', 0)
      .style('fill', function(d) { return color(d.data.name) || 'gray'; })
      .style('stroke', 'white');
      
      nodes.selectAll('rect.rect-outline')
      .transition()
      .duration(duration)
      .delay(delay)
      .style('stroke', 'white')
      .style('fill', 'none')
      .style('stroke-dasharray', null);
      }
      
      var tm = %s;
      
      var tm_h = d3.hierarchy(tm)
      .sum(function(d){
      return d.Freq || 0;
      });
      
      var width = 400;
      var height = 400;
      
      var svg = d3.select('body').append('svg')
      .style('width',width + 20 + 20)
      .style('height',height + 20 + 20)
      .append('g')
      .attr('transform','translate(20,20)');
      
      var color = d3.scaleOrdinal(d3.schemeCategory10);
      
      var nodes = initialize(tm_h);
      
      drawPart(tm_h, nodes, 0, 0);
      ",
      titan_nest
    )
  ))
  }


### create partition ####
browsable(
  tagList(
    d3_dep_v4(offline=FALSE),
    scr_part()
  )
)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<script src="https://unpkg.com/d3@4.9.1/build/d3.min.js"></script>
</head>
<body style="background-color:white;">
<script>
function initialize(hier) {
d3.partition().size([height,width])(hier);
var nodes = svg.selectAll('g.node')
.data(hier.descendants());
nodes = nodes.merge(
nodes.enter().append('g')
.attr('class','node')
);
nodes.attr('transform', function(d) {
return 'translate(' + d.y0 + ',' + d.x0 + ')'
});
nodes.append('rect')
.classed('rect-part', true);
nodes.append('rect')
.classed('rect-outline', true)
.attr('width', function(d) { return d.y1 - d.y0; })
.attr('height', function(d) { return d.x1 - d.x0; })
.style('fill', 'none')
.style('stroke', 'none');
nodes.append('text')
//.style('text-anchor','end')
.attr('dy','1em')
.text(function(d) { return d.data.name});
return nodes;
}
function drawPart(hier, nodes, duration, delay) {
d3.partition().size([height,width])(hier);
svg.selectAll('.link')
.transition()
.duration(100)
.style('opacity', 0.00001)
.remove();
nodes.selectAll('rect.rect-part')
.transition()
.duration(duration)
.delay(delay)
.attr('width', function(d) { return d.y1 - d.y0; })
.attr('height', function(d) { return d.x1 - d.x0; })
.attr('x', 0)
.attr('y', 0)
.style('fill', function(d) { return color(d.data.name) || 'gray'; })
.style('stroke', 'white');
nodes.selectAll('rect.rect-outline')
.transition()
.duration(duration)
.delay(delay)
.style('stroke', 'white')
.style('fill', 'none')
.style('stroke-dasharray', null);
}
</script>
<script>
function drawPartTree(hier, nodes, duration, delay) {
var nodeHeight = height * 0.66;
var nodeWidth = 10;
// run treemap slice to get heights
// note, this will overwrite the x0,x1,y0,y1
d3.treemap()
.size([nodeWidth, nodeHeight])
.tile(d3.treemapSlice)(hier);
// record treemap heights and widths
// since partition will overwrite
hier.each(function(d) {
d.h = d.y1 - d.y0;
d.w = d.x1 - d.x0;
});
// now run partition with no size
// so will be in range [0,1]
d3.partition()(hier);
nodes.selectAll('rect.rect-outline')
.transition()
.duration(duration)
.delay(delay)
.style('stroke', 'gray')
.style('fill', 'none')
.style('stroke-dasharray','2,2');
nodes.selectAll('rect.rect-part')
.transition()
.duration(duration)
.delay(delay)
.attr('y', function(d) {
return ((d.x1-d.x0)*height-d.h)/2;
})
.attr('width', function(d) { return nodeWidth; })
.attr('height', function(d) {
return d.h;
});
function stack(x) {
var xobj = {};
var sum = d3.sum(x.children, function(d) {return d.h} )
x.children.forEach(function(d) {
xobj[d.data.name] = d.h/sum;
})
return d3.stack().keys(Object.keys(xobj))([xobj]);
}
nodes.each(function(node) {
if(!node.children) {return}
var st = stack(node);
st.forEach(function(d,i) {
var child = node.children[i]
var link = svg.append('path')
.classed('link', true)
.style('opacity', 0.000001);
function customLine(pts) {
var ld = d3.linkHorizontal()
.source(function(d){return d[0]})
.target(function(d){return d[1]})
.x(function(d){return d[0]})
.y(function(d){return d[1]});
return [
ld([pts[0],pts[1]]),
pts[1] + ',' + pts[2],
ld([pts[2],pts[3]]).slice(1)
].join('L');
}
link.attr('d',d3.line()([
[node.y0 * width + nodeWidth, ((node.x0 * height) + ((node.x1-node.x0)*height-node.h)/2 ) + d[0][0]*node.h],
[child.y0 * width, ((child.x0 * height) + ((child.x1-child.x0)*height-child.h)/2 )],
[child.y0 * width, ((child.x0 * height) + ((child.x1-child.x0)*height-child.h)/2 ) + child.h],
[node.y0 * width + nodeWidth, ((node.x0 * height) + ((node.x1-node.x0)*height-node.h)/2 ) + d[0][1]*node.h],
//[node.y0 * width + nodeWidth, ((node.x0 * height) + ((node.x1-node.x0)*height-node.h)/2 )]
]));
link
.style('stroke', 'white')
.style('fill', color(child.data.name));
link
.transition()
.duration(500)
.delay(duration+delay)
.style('opacity', 0.7);
})
});
}
var tm = {"children":[{"name":"1st","children":[{"name":"Male","children":[{"name":"No","Freq":118,"colname":"Survived"},{"name":"Yes","Freq":62,"colname":"Survived"}],"colname":"Sex"},{"name":"Female","children":[{"name":"No","Freq":4,"colname":"Survived"},{"name":"Yes","Freq":141,"colname":"Survived"}],"colname":"Sex"}],"colname":"Class"},{"name":"2nd","children":[{"name":"Male","children":[{"name":"No","Freq":154,"colname":"Survived"},{"name":"Yes","Freq":25,"colname":"Survived"}],"colname":"Sex"},{"name":"Female","children":[{"name":"No","Freq":13,"colname":"Survived"},{"name":"Yes","Freq":93,"colname":"Survived"}],"colname":"Sex"}],"colname":"Class"},{"name":"3rd","children":[{"name":"Male","children":[{"name":"No","Freq":422,"colname":"Survived"},{"name":"Yes","Freq":88,"colname":"Survived"}],"colname":"Sex"},{"name":"Female","children":[{"name":"No","Freq":106,"colname":"Survived"},{"name":"Yes","Freq":90,"colname":"Survived"}],"colname":"Sex"}],"colname":"Class"},{"name":"Crew","children":[{"name":"Male","children":[{"name":"No","Freq":670,"colname":"Survived"},{"name":"Yes","Freq":192,"colname":"Survived"}],"colname":"Sex"},{"name":"Female","children":[{"name":"No","Freq":3,"colname":"Survived"},{"name":"Yes","Freq":20,"colname":"Survived"}],"colname":"Sex"}],"colname":"Class"}],"name":"Titanic"};
var tm_h = d3.hierarchy(tm)
.sum(function(d){
return d.Freq || 0;
});
var width = 400;
var height = 400;
var convert_btn = d3.select('body').append('div')
.append('button')
.text('convert')
.attr('disabled', '')
.on('click', function(d) {
if(!svg.node().state) {return}
if(svg.node().state === 'partition') {
drawPartTree(tm_h, nodes, 2000, 0);
svg.node().state = 'partitionTree'
} else {
drawPart(tm_h, nodes, 2000, 0);
svg.node().state = 'partition'
}
})
var svg = d3.select('body').append('svg')
.style('width',width + 20 + 20)
.style('height',height + 20 + 20)
.append('g')
.attr('transform','translate(20,20)');
var color = d3.scaleOrdinal(d3.schemeCategory10);
var nodes = initialize(tm_h);
drawPart(tm_h, nodes, 0, 0);
drawPartTree(tm_h, nodes, 5000, 2000);
convert_btn.attr('disabled', null);
svg.node().state = 'partitiontree'
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment