Prototype of a new visualization I wanted to build for PowerBI.
Last active
February 16, 2016 17:57
-
-
Save dankronstal/d19411677a272954b9b2 to your computer and use it in GitHub Desktop.
Clustered Progression Chart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<head> | |
<title>Stuff</title> | |
<style> | |
* {font-family: Verdana, Geneva, sans-serif; font-size: 12px;} | |
.node {opacity:.75} | |
.nodeSelected {font-weight:bold} | |
body { background-color:#eee;} | |
path { stroke: 1px; } | |
</style> | |
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
</head> | |
<body> | |
<div class="content"></div> | |
<script> | |
// | |
// parameters | |
// | |
var viewport = {width:0, height:500, "middle":0, "margins":{"top":10, "bottom":10, "left":10, "right":10}}; | |
var config = {"backgroundColor": "#ccc", | |
"colors":d3.scale.category10(), | |
"group":{"float":.75, "yPos":50, "barHeight":10, "barOpacity":.7, "space":10, "textColor":"#ccc"}, | |
"node":{"radius":5}, | |
"entry":{"arm":150, "height":100, "radius":15, "armOffset":35, "yPos":4}, | |
"tooltip":{"borderColor":"#000#","fillColor":"#ccc","opacity":.7}}; | |
// | |
// helper functions | |
// | |
function sortByDateDescending(a, b) { | |
return b.eventDate - a.eventDate; | |
} | |
function getGroupSize(n) | |
{ | |
var size = 0; | |
events.forEach(function(d1, i1){ | |
d1.groups.forEach(function(d2,i2){ | |
if(d2.name == n) size += d2.value + config.entry.radius; | |
}); | |
}); | |
return size; | |
} | |
function getGroupCount(n) | |
{ | |
var c = 0; | |
events.forEach(function(d1, i1){ | |
d1.groups.forEach(function(d2,i2){ | |
if(d2.name == n) c ++; | |
}); | |
}); | |
return c; | |
} | |
function getGroupNames(n){ | |
names = ""; | |
n.groups.forEach(function(d,i){ | |
names += " "+d.name; | |
}); | |
return names; | |
} | |
Date.prototype.yyyymmdd = function() { | |
var yyyy = this.getFullYear().toString(); | |
var mm = (this.getMonth()+1).toString(); // getMonth() is zero-based | |
var dd = this.getDate().toString(); | |
return yyyy + (mm[1]?mm:"0"+mm[0]) + (dd[1]?dd:"0"+dd[0]); // padding | |
}; | |
function tt_Bar(e){ | |
tooltip.selectAll("h1").remove(); | |
tooltip.selectAll("p").remove(); | |
tooltip.append("h1").text(e.name + " : " + e.ename); | |
tooltip.append("p").html("<pre><b>Date: </b>"+e.date.yyyymmdd()+"<br /><b>Value: </b>"+e.value+"</pre>"); | |
} | |
function tt_Node(e){ | |
tooltip.selectAll("h1").remove(); | |
tooltip.selectAll("p").remove(); | |
tooltip.append("h1").text(e.name); | |
tooltip.append("p").html("<pre><b>Groups: </b>"+e.groups.length+"</pre>"); | |
e.groups.forEach(function(d){ | |
tooltip.append("p").html("<pre><b>- Name: </b>"+d.name+"<br /> <b>Value: </b>"+d.value+"</pre>"); | |
}); | |
} | |
function tt_Group(e){ | |
tooltip.selectAll("h1").remove(); | |
tooltip.selectAll("p").remove(); | |
tooltip.append("h1").text(e); | |
tooltip.append("p").html("<pre><b>Events: </b>"+groups[e].count+"<br /><b>Value: </b>"+groups[e].size+"</pre>"); | |
} | |
// | |
// set up data | |
// | |
var events = [{"name":"eventOne", "eventDate":new Date("2015-01-01"), "color":"black", "groups":[{"name":"catOne", "value":1},{"name":"catFour", "value":1}]}, | |
{"name":"eventTwo", "eventDate":new Date("2013-01-01"), "color":"black", "groups":[{"name":"catOne", "value":10}, {"name":"catThree", "value":100}, {"name":"catFour", "value":1}, {"name":"catFive", "value":11}]}, | |
{"name":"eventThree", "eventDate":new Date("2014-07-01"), "color":"black", "groups":[{"name":"catOne", "value":1}, {"name":"catTwo", "value":20}]}, | |
{"name":"eventFour", "eventDate":new Date("2014-09-01"), "color":"black", "groups":[{"name":"catOne", "value":30}, {"name":"catTwo", "value":50}]}, | |
{"name":"eventFive", "eventDate":new Date("2012-09-01"), "color":"black", "groups":[{"name":"catOne", "value":30}, {"name":"catTwo", "value":50}]}, | |
{"name":"eventSix", "eventDate":new Date("2011-01-01"), "color":"black", "groups":[{"name":"catThree", "value":10}]}, | |
{"name":"eventSeven", "eventDate":new Date("2010-01-01"), "color":"black", "groups":[{"name":"catTwo", "value":10}, {"name":"catFour", "value":10}]},]; | |
events = events.sort(sortByDateDescending); | |
var yScale = d3.time.scale() | |
.domain([ | |
d3.min(events, function(d) { return d.eventDate;} ), | |
d3.max(events, function(d) { return d.eventDate;} ) | |
]) | |
.range([viewport.height, config.entry.height]); | |
var groups = {"catOne":{"color":"black", "size":0, "count":0, "offset":0, "side":"right"}, | |
"catTwo":{"color":"black", "size":0, "count":0, "offset":0, "side":"right"}, | |
"catThree":{"color":"black", "size":0, "count":0, "offset":0, "side":"right"}, | |
"catFour":{"color":"black", "size":0, "count":0, "offset":0, "side":"left"}, | |
"catFive":{"color":"black", "size":0, "count":0, "offset":0, "side":"left"}}; | |
var eventsByGroup = []; | |
var groupNames = []; | |
groupTotal = {"left":config.entry.arm-10, "right":config.entry.arm}; | |
var groupCurrent = Object.keys(groups)[0]; //get first group | |
for(x in groups){ | |
groups[x].size = getGroupSize(x); | |
groups[x].count = getGroupCount(x); | |
groupNames[groupNames.length]=x; | |
groups[x].color = config.colors(groupNames.length); | |
events.forEach(function(d,i){ | |
var e = d; | |
e.groups.forEach(function(d,i){ | |
if(x.localeCompare(d.name)==0){ | |
if(d.name != groupCurrent){ | |
groupCurrent = d.name; | |
groupTotal[groups[d.name].side] += config.group.space; | |
} | |
eventsByGroup[eventsByGroup.length] = {"name":d.name, "value":d.value, "date":e.eventDate, "ename":e.name, "offset":groupTotal[groups[d.name].side]}; | |
var entry = d.value + config.entry.radius; | |
groupTotal[groups[d.name].side] += entry; | |
viewport.width += entry+1; //not sure what the extra 1 is for... | |
} | |
}); | |
}); | |
viewport.middle = groupTotal["left"]; | |
} | |
viewport.width += (config.entry.arm*2)+(config.group.space*2)+config.node.radius; | |
var nestByGroup = d3.nest().key(function(d) { return d.name; }); | |
var c = nestByGroup.entries(eventsByGroup); | |
var firsts = c.map(function(entry) { return entry.values[0]; }); | |
firsts.forEach(function(d,i){ | |
groups[d.name].offset = d.offset; | |
}); | |
var s = d3.sum(eventsByGroup, function(d, i){ return d.value + config.entry.radius; }); | |
eventsByGroup = eventsByGroup.reverse(); //stack from bottom to top (to hide lines) | |
// | |
// canvas & tooltip | |
// | |
var svg = d3.select(".content").append("svg") | |
.style("background-color",config.backgroundColor) | |
.attr("width", viewport.width + viewport.margins.left + viewport.margins.right) | |
.attr("height", viewport.height+config.node.radius + viewport.margins.top + viewport.margins.bottom); | |
var tooltip = d3.select("body") | |
.append("div") | |
.style("position", "absolute") | |
.style("z-index", "10") | |
.style("visibility", "hidden") | |
.style("background-color",config.tooltip.fillColor) | |
.style("border","1px solid black") | |
.style("padding","1em") | |
.style("opacity",config.tooltip.opacity); | |
// | |
// progression node texts | |
// | |
var eventSvgs = svg.selectAll("g").data(events).enter().append("g"); | |
eventSvgs.each(function(d, i){ | |
d3.select(this).append("text") | |
.attr("x",viewport.middle+viewport.margins.left) | |
.attr("y",function(d,i){return yScale(d.eventDate)+viewport.margins.top+config.entry.yPos;}) | |
.classed("node",true) | |
.attr("event",function(d,i){ return d.name; }) | |
.attr("groups", function(d, i){ | |
return getGroupNames(d); | |
}) | |
.attr("text-anchor","middle").attr("text-baseline","middle") | |
.text(function(d,i){ return d.eventDate.yyyymmdd(); }) | |
d3.select(this) | |
.on("mouseover", function(d, i){ | |
svg.selectAll(".p").style("opacity",.2); | |
d3.selectAll("."+d.name).style("opacity",1); | |
tt_Node(d); | |
tooltip.style("visibility", "visible"); | |
d3.select(this.childNodes[0]).classed("nodeSelected",true); | |
}) | |
.on("mouseout", function(d, i){ | |
d3.selectAll(".p").style("opacity",1); | |
tooltip.style("visibility", "hidden"); | |
d3.select(this.childNodes[0]).classed("nodeSelected",false); | |
}) | |
.on("mousemove", function(d, i){ | |
return tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+25)+"px"); | |
}); | |
}); | |
// | |
// progression node paths | |
// | |
var eventsByGroupSvg = svg.selectAll("g.catLines").data(eventsByGroup).enter().append("g") | |
.append("path") | |
.style("opacity",1) | |
.style("fill", function(d,i){ return groups[d.name].color;}) | |
.attr("class", function(d){ return "p "+d.name+" "+d.ename; }) | |
.attr("d",function(d,i){ | |
if(groups[d.name].side == "right") | |
return "M"+d.offset+" -"+(yScale(d.date)-(config.group.yPos+config.group.barHeight+config.group.float))+",L"+d.offset+" -"+config.entry.radius+", a "+config.entry.radius+" "+config.entry.radius+", 0,0,1, -"+config.entry.radius+" "+config.entry.radius+", L"+config.entry.armOffset+" 0, L"+(d.offset+d.value)+" 0, L"+(d.offset+d.value)+" 0, a "+config.entry.radius+" "+config.entry.radius+", 0,0,0, "+config.entry.radius+", -"+config.entry.radius+", L"+(d.offset+d.value+config.entry.radius)+" -"+(yScale(d.date)-(config.group.yPos+config.group.barHeight+config.group.float)); | |
else | |
return "M-"+d.offset+" -"+(yScale(d.date)-(config.group.yPos+config.group.barHeight+config.group.float))+",L-"+d.offset+" -"+config.entry.radius+", a "+config.entry.radius+" -"+config.entry.radius+", 1,0,0, "+config.entry.radius+" "+config.entry.radius+", L-"+config.entry.armOffset+" 0, L-"+(d.offset+d.value)+" 0, L-"+(d.offset+d.value)+" 0, a "+config.entry.radius+" "+config.entry.radius+", 0,0,1, -"+config.entry.radius+", -"+config.entry.radius+", L-"+(d.offset+d.value+config.entry.radius)+" -"+(yScale(d.date)-(config.group.yPos+config.group.barHeight+config.group.float)); | |
}) | |
.attr("transform", function(d,i){ return "translate(" + (viewport.middle+viewport.margins.left) + "," + (yScale(d.date)+viewport.margins.top) + ")";}) | |
.attr("stroke","black") | |
.attr("stroke-width", .25) | |
.on("mouseover", function(d, i){ | |
d3.selectAll(".p").style("opacity",.2); | |
d3.select(this).style("opacity",1); | |
that = this; | |
d3.selectAll(".node") | |
.classed("nodeSelected", function(d, i){ return that.classList.contains(d3.select(this).attr("event")); }); | |
tt_Bar(d); | |
tooltip.style("visibility", "visible"); | |
}) | |
.on("mouseout", function(d, i){ | |
d3.selectAll(".p").style("opacity",1); | |
d3.selectAll(".node").classed("nodeSelected", false); | |
tooltip.style("visibility", "hidden"); | |
}) | |
.on("mousemove", function(d, i){ | |
return tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+25)+"px"); | |
}); | |
// | |
// cluster rects | |
// | |
svg.selectAll("g.group").data(groupNames).enter() | |
.append("g") | |
.append("rect") | |
.attr("x",function(d,i){ | |
if(groups[d].side == "right") | |
return groups[d].offset + viewport.middle + viewport.margins.left; | |
else | |
return viewport.middle - (groups[d].offset + groups[d].size) + viewport.margins.left; | |
}) | |
.attr("y",config.group.yPos + + viewport.margins.top) | |
.attr("width",function(d){ | |
return groups[d].size; | |
}) | |
.attr("height",config.group.barHeight) | |
.style("fill",function(d){ return groups[d].color; }) | |
.style("opacity", config.group.barOpacity) | |
.attr("group", function(d) { return d; }) | |
.on("mouseover", function(d, i){ | |
svg.selectAll(".p").style("opacity",.2); | |
d3.selectAll("."+d).style("opacity",1); | |
that = this; | |
d3.selectAll(".node") | |
.classed("nodeSelected", function(d, i){ | |
return d3.select(this).attr("groups").indexOf(d3.select(that).attr("group")) > 0; | |
}); | |
tt_Group(d); | |
tooltip.style("visibility", "visible"); | |
}) | |
.on("mouseout", function(d, i){ | |
d3.selectAll(".p").style("opacity",1); | |
d3.selectAll(".node").classed("nodeSelected", false); | |
tooltip.style("visibility", "hidden"); | |
}) | |
.on("mousemove", function(d, i){ | |
return tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+25)+"px"); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment