Skip to content

Instantly share code, notes, and snippets.

@dankronstal
Last active February 16, 2016 17:57
Show Gist options
  • Save dankronstal/d19411677a272954b9b2 to your computer and use it in GitHub Desktop.
Save dankronstal/d19411677a272954b9b2 to your computer and use it in GitHub Desktop.
Clustered Progression Chart

Prototype of a new visualization I wanted to build for PowerBI.

<!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