Skip to content

Instantly share code, notes, and snippets.

@timelyportfolio
Last active November 13, 2017 04:02
Show Gist options
  • Save timelyportfolio/05a607c1730f717dc66b6c6db36a9f9f to your computer and use it in GitHub Desktop.
Save timelyportfolio/05a607c1730f717dc66b6c6db36a9f9f to your computer and use it in GitHub Desktop.
partsankey interactive
license: mit
height: 500

This is the version of the partition tree with a sankey flavor and a little interactivity. See post.

Replicate in R

(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_tree <- function() {
  tags$script(HTML(
sprintf(
"
function initialize(hier) {
  d3.partition().size([height,width])(hier);

  var nodes = svg.selectAll('g.node')
    .data(hier.descendants().filter(function(d) {
      return d.height > 0;
    }));
  
  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','0.5em')
    .text(function(d) { return d.data.name});
  
  return nodes;
}

function drawPartSankey(hier, nodes, nodeHeightRatio, duration, delay) {
  nodeHeightRatio = nodeHeightRatio ? nodeHeightRatio : 0.66;
  var nodeHeight = height * nodeHeightRatio;
  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;
  })

  hier.each(function(d) {
    // for the non-leaves also sum
    //  the categories at the leaf level
    //  in the case of Titanic data this will be survival
    if(d.height > 0) {
      var nest = d3.nest()
        .key(d=>d.data.name)
        .rollup(d=>{return d3.sum(d.map(dd=>dd.h))})
      d.sub = nest.entries(d.leaves())
    }
  });

  // 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')
    .style('fill', function(d) {return color(d.data.name)})
    .style('stroke', 'white')
    .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;
    });

  nodes.selectAll('text')
    .attr('y', function(d) {
      if(d.height > 1) {
        return ((d.x1-d.x0)*height-d.h)/2;
      }
      return (d.x1-d.x0)*height/2;
    })
    .attr('x', function(d) {
      if(d.height > 1) {
        return nodeWidth / 2
      }
      return nodeWidth
    })
    .attr('dy', function(d) {
      if(d.height > 1) {
        return '-0.15em'
      }
      return '0.25em'
    })
    .style('text-anchor', function(d) {
      if(d.height > 1) {
        return 'middle'
      }
      return 'start'
    })

  function stack(x) {
    var xobj = {};
    var sum = d3.sum(x.children, function(d) {return d.h} )
    x.children.forEach(function(d) {
      d.sub.forEach(function(dd) {
        xobj[d.data.name + '~' + dd.key] = dd.value/sum;
      })
    })
    return d3.stack().keys(Object.keys(xobj))([xobj]);
  }

  function stackLeaf(x) {
    var xobj = {};
    var sum = d3.sum(x.children, function(d) {return d.h} );
    x.sub.forEach(function(d) {
      xobj[d.key] = d.value/sum;
    })
    return d3.stack().keys(Object.keys(xobj))([xobj]);
  }
  
  nodes.each(function(node) {
    if(node.height <= 1) {return}
    
    // empty array to store links on each node
    node.linkPaths = node.linkPaths ? node.linkPaths : [];

    var st = stack(node);
    st.forEach(function(d,i) {
      var childname = d.key.split('~')[0];
      var leafname = d.key.split('~')[1];
      var child = node.children.filter(function(ch) {
        return ch.data.name === childname
      })[0]
      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');
      }

      var st2 = stackLeaf(child).filter(function(d) {
        return d.key === leafname
      })[0];

      link.datum({
        source: node,
        target: child,
        leaf: leafname
      })
  
      link.attr('d',customLine([
        [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 ) + st2[0][0]*child.h],
        [child.y0 * width, ((child.x0 * height) + ((child.x1-child.x0)*height-child.h)/2 ) + st2[0][1]*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(leafname));
  
      link
        .transition()
        .duration(500)
        .delay(duration+delay)
        .style('opacity', 0.5);

      node.linkPaths = node.linkPaths.concat(link.node());
    })
  });

  // add mouseover that highlights with higher opacity
  //  this will highlight all children with same leaf name
  svg.selectAll('.link').on('mouseover', function(d) {
    d3.select(this).style('opacity', 0.8)
    if(d.target.linkPaths) {
      d.target.linkPaths.forEach(function(lp) {
        var data = d3.select(lp).datum();
        if(data.leaf == d.leaf) {
          d3.select(lp).style('opacity', 0.8)
        }
      })
    }
  })

  svg.selectAll('.link').on('mouseout', function(d) {
    svg.selectAll('.link').style('opacity', 0.5)
  })
}

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('#vis').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);
// rearrange so that survived yes is green
//  and no is red
color.range(
  [color.range()[2]]
    .concat([color.range()[3]])
    .concat(color.range().splice(0,2))
    .concat(color.range().splice(4,10))
)
color.domain(['Yes', 'No'])

var nodes = initialize(tm_h);

drawPartSankey(tm_h, nodes, 0.75, 100,0);
svg.selectAll('rect.rect-outline').remove();
//svg.selectAll('text').remove();
",
titan_nest
)
))
}


### create partition and partition tree ####
browsable(
  tagList(
    d3_dep_v4(offline=FALSE),
    tags$div(id="vis"),
    scr_part_tree()
  )
)

forked from timelyportfolio's block: partsankey static

(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_tree <- function() {
tags$script(HTML(
sprintf(
"
function initialize(hier) {
d3.partition().size([height,width])(hier);
var nodes = svg.selectAll('g.node')
.data(hier.descendants().filter(function(d) {
return d.height > 0;
}));
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','0.5em')
.text(function(d) { return d.data.name});
return nodes;
}
function drawPartSankey(hier, nodes, nodeHeightRatio, duration, delay) {
nodeHeightRatio = nodeHeightRatio ? nodeHeightRatio : 0.66;
var nodeHeight = height * nodeHeightRatio;
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;
})
hier.each(function(d) {
// for the non-leaves also sum
// the categories at the leaf level
// in the case of Titanic data this will be survival
if(d.height > 0) {
var nest = d3.nest()
.key(d=>d.data.name)
.rollup(d=>{return d3.sum(d.map(dd=>dd.h))})
d.sub = nest.entries(d.leaves())
}
});
// 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')
.style('fill', function(d) {return color(d.data.name)})
.style('stroke', 'white')
.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;
});
nodes.selectAll('text')
.attr('y', function(d) {
if(d.height > 1) {
return ((d.x1-d.x0)*height-d.h)/2;
}
return (d.x1-d.x0)*height/2;
})
.attr('x', function(d) {
if(d.height > 1) {
return nodeWidth / 2
}
return nodeWidth
})
.attr('dy', function(d) {
if(d.height > 1) {
return '-0.15em'
}
return '0.25em'
})
.style('text-anchor', function(d) {
if(d.height > 1) {
return 'middle'
}
return 'start'
})
function stack(x) {
var xobj = {};
var sum = d3.sum(x.children, function(d) {return d.h} )
x.children.forEach(function(d) {
d.sub.forEach(function(dd) {
xobj[d.data.name + '~' + dd.key] = dd.value/sum;
})
})
return d3.stack().keys(Object.keys(xobj))([xobj]);
}
function stackLeaf(x) {
var xobj = {};
var sum = d3.sum(x.children, function(d) {return d.h} );
x.sub.forEach(function(d) {
xobj[d.key] = d.value/sum;
})
return d3.stack().keys(Object.keys(xobj))([xobj]);
}
nodes.each(function(node) {
if(node.height <= 1) {return}
// empty array to store links on each node
node.linkPaths = node.linkPaths ? node.linkPaths : [];
var st = stack(node);
st.forEach(function(d,i) {
var childname = d.key.split('~')[0];
var leafname = d.key.split('~')[1];
var child = node.children.filter(function(ch) {
return ch.data.name === childname
})[0]
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');
}
var st2 = stackLeaf(child).filter(function(d) {
return d.key === leafname
})[0];
link.datum({
source: node,
target: child,
leaf: leafname
})
link.attr('d',customLine([
[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 ) + st2[0][0]*child.h],
[child.y0 * width, ((child.x0 * height) + ((child.x1-child.x0)*height-child.h)/2 ) + st2[0][1]*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(leafname));
link
.transition()
.duration(500)
.delay(duration+delay)
.style('opacity', 0.5);
node.linkPaths = node.linkPaths.concat(link.node());
})
});
// add mouseover that highlights with higher opacity
// this will highlight all children with same leaf name
svg.selectAll('.link').on('mouseover', function(d) {
d3.select(this).style('opacity', 0.8)
if(d.target.linkPaths) {
d.target.linkPaths.forEach(function(lp) {
var data = d3.select(lp).datum();
if(data.leaf == d.leaf) {
d3.select(lp).style('opacity', 0.8)
}
})
}
})
svg.selectAll('.link').on('mouseout', function(d) {
svg.selectAll('.link').style('opacity', 0.5)
})
}
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('#vis').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);
// rearrange so that survived yes is green
// and no is red
color.range(
[color.range()[2]]
.concat([color.range()[3]])
.concat(color.range().splice(0,2))
.concat(color.range().splice(4,10))
)
color.domain(['Yes', 'No'])
var nodes = initialize(tm_h);
drawPartSankey(tm_h, nodes, 0.75, 100,0);
svg.selectAll('rect.rect-outline').remove();
//svg.selectAll('text').remove();
",
titan_nest
)
))
}
### create partition and partition tree ####
browsable(
tagList(
d3_dep_v4(offline=FALSE),
tags$div(id="vis"),
scr_part_tree()
)
)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<script src="https://unpkg.com/d3@4.11.0/build/d3.min.js"></script>
</head>
<body style="background-color:white;">
<div id="vis"></div>
<script>
function initialize(hier) {
d3.partition().size([height,width])(hier);
var nodes = svg.selectAll('g.node')
.data(hier.descendants().filter(function(d) {
return d.height > 0;
}));
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','0.5em')
.text(function(d) { return d.data.name});
return nodes;
}
function drawPartSankey(hier, nodes, nodeHeightRatio, duration, delay) {
nodeHeightRatio = nodeHeightRatio ? nodeHeightRatio : 0.66;
var nodeHeight = height * nodeHeightRatio;
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;
})
hier.each(function(d) {
// for the non-leaves also sum
// the categories at the leaf level
// in the case of Titanic data this will be survival
if(d.height > 0) {
var nest = d3.nest()
.key(d=>d.data.name)
.rollup(d=>{return d3.sum(d.map(dd=>dd.h))})
d.sub = nest.entries(d.leaves())
}
});
// 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')
.style('fill', function(d) {return color(d.data.name)})
.style('stroke', 'white')
.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;
});
nodes.selectAll('text')
.attr('y', function(d) {
if(d.height > 1) {
return ((d.x1-d.x0)*height-d.h)/2;
}
return (d.x1-d.x0)*height/2;
})
.attr('x', function(d) {
if(d.height > 1) {
return nodeWidth / 2
}
return nodeWidth
})
.attr('dy', function(d) {
if(d.height > 1) {
return '-0.15em'
}
return '0.25em'
})
.style('text-anchor', function(d) {
if(d.height > 1) {
return 'middle'
}
return 'start'
})
function stack(x) {
var xobj = {};
var sum = d3.sum(x.children, function(d) {return d.h} )
x.children.forEach(function(d) {
d.sub.forEach(function(dd) {
xobj[d.data.name + '~' + dd.key] = dd.value/sum;
})
})
return d3.stack().keys(Object.keys(xobj))([xobj]);
}
function stackLeaf(x) {
var xobj = {};
var sum = d3.sum(x.children, function(d) {return d.h} );
x.sub.forEach(function(d) {
xobj[d.key] = d.value/sum;
})
return d3.stack().keys(Object.keys(xobj))([xobj]);
}
nodes.each(function(node) {
if(node.height <= 1) {return}
// empty array to store links on each node
node.linkPaths = node.linkPaths ? node.linkPaths : [];
var st = stack(node);
st.forEach(function(d,i) {
var childname = d.key.split('~')[0];
var leafname = d.key.split('~')[1];
var child = node.children.filter(function(ch) {
return ch.data.name === childname
})[0]
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');
}
var st2 = stackLeaf(child).filter(function(d) {
return d.key === leafname
})[0];
link.datum({
source: node,
target: child,
leaf: leafname
})
link.attr('d',customLine([
[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 ) + st2[0][0]*child.h],
[child.y0 * width, ((child.x0 * height) + ((child.x1-child.x0)*height-child.h)/2 ) + st2[0][1]*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(leafname));
link
.transition()
.duration(500)
.delay(duration+delay)
.style('opacity', 0.5);
node.linkPaths = node.linkPaths.concat(link.node());
})
});
// add mouseover that highlights with higher opacity
// this will highlight all children with same leaf name
svg.selectAll('.link').on('mouseover', function(d) {
d3.select(this).style('opacity', 0.8)
if(d.target.linkPaths) {
d.target.linkPaths.forEach(function(lp) {
var data = d3.select(lp).datum();
if(data.leaf == d.leaf) {
d3.select(lp).style('opacity', 0.8)
}
})
}
})
svg.selectAll('.link').on('mouseout', function(d) {
svg.selectAll('.link').style('opacity', 0.5)
})
}
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 svg = d3.select('#vis').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);
// rearrange so that survived yes is green
// and no is red
color.range(
[color.range()[2]]
.concat([color.range()[3]])
.concat(color.range().splice(0,2))
.concat(color.range().splice(4,10))
)
color.domain(['Yes', 'No'])
var nodes = initialize(tm_h);
drawPartSankey(tm_h, nodes, 0.75, 100,0);
svg.selectAll('rect.rect-outline').remove();
//svg.selectAll('text').remove();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment