This is the derivative version of the partition sankey, but instead of collapsing leaves, this partsankey
visualizes rpart
or recursive partitioning. See post.
library(htmltools)
library(rpart)
library(d3r)
library(vcdExtra) # for the Titanic dataset used in first post
data("Titanicp", package="vcdExtra")
fit <- rpart(
survived ~ pclass + sex + age,
data = Titanicp
)
fit_json <- d3_party(fit)
scr <- tags$script(HTML(
sprintf(
"
var titanic = %s
var hier = d3.hierarchy(titanic)
var width = 800;
var height = 600;
var nodeWidth = 10;
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(['2', '1'])
var svg = d3.select('#vis').append('svg')
.style('width',width + 20 + 20)
.style('height',height + 20 + 20)
.append('g')
.attr('transform','translate(20,20)');
// n will represent sum at each split
// so don't use hier.sum
// instead add value attribute
hier.each(function(d) {d.value = d.data.wt})
// caution will mutate the object
layout(hier, nodeWidth, 0.75)
// note, this will not follow traditional d3 call form
// if we were to make this official change this to svg.call
var nodes = drawNodes(hier, svg)
var links = drawLinks(nodes, svg)
// 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.descendants().forEach(function(dd) {
if(dd.linkPaths){
dd.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)
})
function layout(hier, nodeWidth, nodeHeightRatio) {
nodeWidth = nodeWidth ? nodeWidth : 10;
nodeHeightRatio = nodeHeightRatio ? nodeHeightRatio : 0.66;
var nodeHeight = height * nodeHeightRatio;
// 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);
}
function drawNodes(hier, svg) {
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 * width + ',' + d.x0 * height + ')'
});
nodes.append('rect')
.classed('rect-part', true);
nodes.append('text')
//.style('text-anchor','end')
.attr('dy','0.5em')
.text(function(d) { return d.data.description});
nodes.selectAll('rect.rect-part')
.style('fill', 'gray')//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 > 0) {
return ((d.x1-d.x0)*height-d.h)/2;
}
return (d.x1-d.x0)*height/2;
})
.attr('x', function(d) {
if(d.height > 0) {
return nodeWidth / 2
}
return nodeWidth
})
.attr('dy', function(d) {
if(d.height > 0) {
return '-0.15em'
}
return '0.25em'
})
.style('text-anchor', function(d) {
if(d.height > 0) {
return 'middle'
}
return 'start'
})
return nodes;
}
function drawLinks(nodes, svg) {
function stackSource(x) {
var xobj = {}
var sum = x.data.wt
x.children.forEach(function(d) {
// sum by response by using leaf data
var leafdat = d.leaves().map(function(lf) {
return lf.data.size.size[0]
})
var sizes = d3.zip(
d3.merge(leafdat.map(function(d){return d.response})),
d3.merge(leafdat.map(function(d){return d.freq}))
)
var sumsResponse = d3.nest()
.key(function(d){return d[0]})
.rollup(function(d){
return d3.sum(d.map(function(dd){return dd[1]}))
})
.entries(sizes)
sumsResponse.forEach(function(dd) {
xobj[d.data.id + '~' + dd.key] = dd.value/sum;
})
})
return d3.stack().keys(Object.keys(xobj))([xobj]);
}
function stackTarget(x) {
// sum by response by using leaf data
var leafdat = x.leaves().map(function(d) {
return d.data.size.size[0]
})
var sizes = d3.zip(
d3.merge(leafdat.map(function(d){return d.response})),
d3.merge(leafdat.map(function(d){return d.freq}))
)
var sumsResponse = d3.nest()
.key(function(d){return d[0]})
.rollup(function(d){
return d3.sum(d.map(function(dd){return dd[1]}))
})
.entries(sizes)
var xobj = {};
var sum = d3.sum(sumsResponse.map(function(d){
return d.value
}))
sumsResponse.forEach(function(d) {
xobj[d.key] = d.value/sum;
})
return d3.stack().keys(Object.keys(xobj))([xobj]);
}
nodes.each(function(node) {
if(node.height == 0) {return}
// empty array to store links on each node
node.linkPaths = node.linkPaths ? node.linkPaths : [];
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 st1 = stackSource(node);
st1.forEach(function(d) {
var childname = d.key.split('~')[0]
var leafname = d.key.split('~')[1]
var child = node.children.filter(function(ch) {
return ch.data.id == childname
})[0]
var st = stackTarget(child).filter(function(d) {
return d.key === leafname
})[0];
var link = svg.append('path')
.classed('link', true)
.style('opacity', 0.000001)
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 ) + st[0][0]*child.h],
[child.y0 * width, ((child.x0 * height) + ((child.x1-child.x0)*height-child.h)/2 ) + st[0][1]*child.h],
[node.y0 * width + nodeWidth, ((node.x0 * height) + ((node.x1-node.x0)*height-node.h)/2 ) + d[0][1]*node.h],
]));
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());
})
})
}
"
,
fit_json
)
))
browsable(
tagList(
d3r::d3_dep_v4(offline=FALSE),
tags$div(id = "vis"),
scr
)
)
forked from timelyportfolio's block: partsankey static
forked from timelyportfolio's block: partsankey interactive
forked from timelyportfolio's block: partsankey interactive angled
https://unpkg.com/d3@4.11.0/build/d3.min.js