Skip to content

Instantly share code, notes, and snippets.

@saifuddin778
Last active December 23, 2019 09:31
Show Gist options
  • Save saifuddin778/e87a365d7b467baeb1156eb6b924e6e8 to your computer and use it in GitHub Desktop.
Save saifuddin778/e87a365d7b467baeb1156eb6b924e6e8 to your computer and use it in GitHub Desktop.
A Day At Our Park

We have a park with some shops in it. People visit our park and shop. We track visitors.

  • We plot visitors as dots, and color code them by their ethnicity/race.
  • We divide visitors in quadrants of gender and age (i.e. male-young, female-young, female-old, male-old).
  • We compare visitors' family-size vs their number of purchases (in some lost hope) [sub plot # 1]
  • We view visitors' purchasing history, looking for how much they have shopped in the last 5 months with us. [sub plot # 2]
  • We classify visitors' purchases by product categories during last 5 months. [sub plot # 3]
Boring description aside, the main purpose of this plot is to demonstrate action binds (and resulting transforms) between different containers having independent svg spaces. Also uses Ziggy Jonsson's d3.legend for quick legend drawing.
// d3.legend.js
// (C) 2012 ziggy.jonsson.nyc@gmail.com
// MIT licence
(function() {
d3.legend = function(g) {
g.each(function() {
var g= d3.select(this),
items = {},
svg = d3.select(g.property("nearestViewportElement")),
legendPadding = g.attr("data-style-padding") || 5,
lb = g.selectAll(".legend-box").data([true]),
li = g.selectAll(".legend-items").data([true])
lb.enter().append("rect").style("fill", "none").classed("legend-box", true) //(saif) some addition here
li.enter().append("g").style("font-size", "10px").style("font-family", "helvetica").classed("legend-items",true) //(saif) some addition here too
svg.selectAll("[data-legend]").each(function() {
var self = d3.select(this)
items[self.attr("data-legend")] = {
pos : self.attr("data-legend-pos") || this.getBBox().y,
color : self.attr("data-legend-color") != undefined ? self.attr("data-legend-color") : self.style("fill") != 'none' ? self.style("fill") : self.style("stroke")
}
})
items = d3.entries(items).sort(function(a,b) { return a.value.pos-b.value.pos})
li.selectAll("text")
.data(items,function(d) { return d.key})
.call(function(d) { d.enter().append("text")})
.call(function(d) { d.exit().remove()})
.attr("y",function(d,i) { return i+"em"})
.attr("x","1em")
.text(function(d) { ;return d.key})
li.selectAll("circle")
.data(items,function(d) { return d.key})
.call(function(d) { d.enter().append("circle")})
.call(function(d) { d.exit().remove()})
.attr("cy",function(d,i) { return i-0.25+"em"})
.attr("cx",0)
.attr("r","0.4em")
.style("fill",function(d) { /*console.log(d.value.color);*/return d.value.color}); //saif commenting it out for jonsson
// Reposition and resize the box
var lbbox = li[0][0].getBBox()
lb.attr("x",(lbbox.x-legendPadding))
.attr("y",(lbbox.y-legendPadding))
.attr("height",(lbbox.height+2*legendPadding))
.attr("width",(lbbox.width+2*legendPadding))
})
return g
}
})()
var data_gen = new function(){
this.rand = function(){
return Math.random();
},
this.ceil = function(val){
return Math.ceil(val);
}
this.data_gen = function(){
var self = this;
var genders = ["male", "female"];
var last_n_months = ["03-15", "04-15", "05-15", "06-15", "07-15"];
var purchase_items = ["FOOD", "CLTHNG", "ELCTR", "JWLRY"];
var races = {
"ASIAN": "rgba(233,212,96,1)",
"HISPANIC": "rgba(38, 194, 129, 1)",
"CAUCASIAN": "rgba(37, 116, 169, 1)",
"BLACK": "crimson"
};
var rk = Object.keys(races);
var data = d3.range(2000).map(function(d){
var item = {};
item.age = self.rand() * 90;
item.gender = genders[self.ceil(self.rand() * genders.length-1)];
item.name = self.rand().toString(36).substr(2, 20);
item.young = ((item.age < 40) ? true : false);
item.old = ((item.age > 40) ? true : false);
item.family_size = self.ceil(self.rand() * 6);
item.race = rk[self.ceil(self.rand() * (rk.length)- (1) )];
item.fill = races[item.race];
item.purchases_per_visit = self.ceil(self.rand() * 6);
item.radius = 3;
item.last_n_months = d3.range(last_n_months.length).map(function(d, i){
return {month: last_n_months[d], visits: self.ceil( self.rand() * 25) };
});
item.purchase_history = d3.range(purchase_items.length).map(function(d, i){
return {item: purchase_items[d], count: self.ceil(self.rand() * (200 * self.rand())) };
});
return item;
});
return data;
}
}
function add_axes(scx, scy, dh, dw, space){
var x_axis = d3.svg.axis()
.scale(scx)
.orient("top")
.innerTickSize(2)
.outerTickSize(0)
.tickPadding(2)
.ticks(5);
var y_axis = d3.svg.axis()
.scale(scy)
.orient("right")
.innerTickSize(2)
.outerTickSize(0)
.tickPadding(2)
.ticks(5);
space.append("g")
.attr("class", "axis")
.attr("transform", "translate(0,"+dh+")")
.call(x_axis);
space.append("g")
.attr("class", "axis")
.call(y_axis);
return
}
var helpers = {
div_a: new function(){
this.getquadrants = function(dw, dh) {
return{
"male-young": {minw: 0, maxw: dw/2, minh: 0, maxh: dh/2, title: "male-young"},
"male-old": {minw: dw/2, maxw: dw, minh: 0, maxh: dh/2, title: "male-old"},
"female-young": {minw: 0, maxw: dw/2, minh: dh/2, maxh: dh, title: "female-young"},
"female-old": {minw: dw/2, maxw: dw, minh: dh/2, maxh: dh, title: "female-old"}
};
},
this.addquads = function(space, dw, dh){
//draw quadrants (hor and vert)
var linespc = space.append("g");
linespc.append("line")
.attr("x1", 0)
.attr("y1", dh/2)
.attr("x2", dw)
.attr("y2", dh/2)
.attr("class", "quadrant");
linespc.append("line")
.attr("x1", dw/2)
.attr("y1", 0)
.attr("x2", dw/2)
.attr("y2", dh)
.attr("class", "quadrant");
return;
},
this.addlabels = function(space, dw, dh){
var xmid = dw/2;
var ymid = dh/2;
var xr = xmid/2;
var yr = ymid/2;
var labelspc = space.append("g");
labelspc.append("text")
.attr("class", "quadlabel")
.attr("x", xmid - xr)
.attr("y", 20)
.text("YOUNG");
labelspc.append("text")
.attr("class", "quadlabel")
.attr("x", xmid + xr)
.attr("y", 20)
.text("OLD");
labelspc.append("text")
.attr("class", "quadlabel")
.attr("x", -(xmid - 1.9*yr))
.attr("y", 20)
.attr("dy", ".35em")
.attr("transform", "rotate(-90)")
.style("text-anchor", "start")
.text("MALE");
labelspc.append("text")
.attr("class", "quadlabel")
.attr("x", -(ymid + 1.3*yr))
.attr("y", 20)
.attr("dy", ".35em")
.attr("transform", "rotate(-90)")
.style("text-anchor", "start")
.text("FEMALE");
return;
},
this.addlegend = function(space, dw, dh){
space.append("g")
.attr("class", "legend")
.attr("transform", "translate(30,30)")
.style("font-size", "11px")
.call(d3.legend);
return;
},
this.get_q = function(quadrants, d){
var qk = d.gender + "-" + ((d.young == true) ? "young" : "old");
return quadrants[qk];
},
this.interact = function(d, helpers){
helpers.div_ba.put(d);
helpers.div_bb.put(d);
helpers.div_bc.put(d);
},
this.mouseleave = function(){
d3.select(this).transition().duration(200).attr("r", 3);
},
this.pick_sign = function(v, r){
var rand = Math.cos(Math.random()) - Math.cos(Math.random());
if (rand < 0.6){
return v - (r * rand);
}
else{
return v + (r * rand);
}
}
},
div_ba: new function() {
this.getscales = function(dw, dh) {
return {
x_scale: d3.scale.linear().domain([0, this.max_x]).range([dw*0.2, dw*0.9]),
y_scale: d3.scale.linear().domain([0, this.max_y]).range([dh*0.9, dh*0.2])
};
},
this.addaxes = function(scales, space, dw, dh){
add_axes(scales.x_scale, scales.y_scale, dh, dw, space);
space.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 10)
.attr("x",0 - (dh / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("# of purchases");
space.append("text")
.attr("y", dh*0.85)
.attr("x", dw/2)
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("family-size");
return;
},
this.point = null,
this.put = function(d){
var family_size = this.scales.x_scale(d.family_size);
var purchases_per_visit = this.scales.y_scale(d.purchases_per_visit);
if (! (this.point) ){
this.point = this.space.append("circle").attr("cx", family_size).attr("cy", purchases_per_visit).attr("r", 8).style("opacity", 0.5);
}
else{
this.point.transition().attr("cx", family_size).attr("cy", purchases_per_visit);
}
}
},
div_bb: new function(){
this.getscales = function(dw, dh) {
return {
x_scale: d3.scale.ordinal().domain(["03-15", "04-15", "05-15", "06-15", "07-15"]).rangePoints([dw*0.2, dw*0.9]),
y_scale: d3.scale.linear().domain([0, this.max_y]).range([dh*0.9, dh*0.1])
};
},
this.addaxes = function(scales, space, dw, dh){
add_axes(scales.x_scale, scales.y_scale, dh, dw, space);
space.append("text")
.attr("x", 50)
.attr("y", 20)
.text("VISITS (LAST 5 MONTHS)");
return;
},
this.path = null,
this.put = function(d){
var scales = this.scales;
var last_n_months = d.last_n_months;
var valueline = d3.svg.line()
.x(function(d) { return scales.x_scale(d.month); })
.y(function(d) { return scales.y_scale(d.visits); })
.interpolate("cardinal");
if (!this.path){
this.path = this.space.append("path")
.attr("class", "line")
.style("fill", "none")
.style("stroke", "rgba(231, 76, 60,1.0)")
.style("stroke-width", 2)
.style("opacity", 0.6)
.attr("d", valueline(last_n_months));
}
else{
this.path.transition()
.attr("d", valueline(last_n_months));
}
return;
}
},
div_bc: new function(){
this.getscales = function(dw, dh) {
return {
x_scale: d3.scale.ordinal().domain(["FOOD", "CLTHNG", "ELCTR", "JWLRY"]).rangeRoundBands([dw*0.1, dw], 0.05),
y_scale: d3.scale.linear().domain([0, this.max_y]).range([dh*0.9, dh*0.1])
};
},
this.addaxes = function(scales, space, dw, dh){
add_axes(scales.x_scale, scales.y_scale, dh, dw, space);
space.append("text")
.attr("x", 50)
.attr("y", 20)
.text("PURCHASE HISTORY");
return;
},
this.set = null,
this.put = function(d){
var scales = this.scales;
var purchase_history = d.purchase_history;
var dw = this.dw;
var dh = this.dh;
if (!this.set){
this.set = this.space.selectAll()
.data(purchase_history)
.enter()
.append("rect")
.attr("x", function(d) { return scales.x_scale(d.item); })
.attr("y", function(d) { return scales.y_scale(d.count); })
.attr("width", (dw * 0.8)/purchase_history.length)
.attr("height", function(d) { return (dh*0.9) - scales.y_scale(d.count)*0.99 ; })
.style("fill", "#87D37C")
.style("stroke", "gray")
.style("stroke-width", 0.6);
}
else{
this.set
.data(purchase_history)
.transition()
.attr("height", function(d) { return (dh*0.9) - scales.y_scale(d.count)*0.99 ; })
.attr("y", function(d) { return scales.y_scale(d.count); });
}
}
}
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.major_div {
width: 940px;
height: 450px;
position: relative;
display: block;
padding: 0;
margin: 0;
}
.div_a {
display: inline-block;
width: 79%;
height: 100%;
margin: 0;
}
.div_b {
display: inline-block;
width: 19%;
height: 100%;
position: relative;
margin: 0;
float: right;
}
.div_bsub {
width: 100%;
display: block;
border-radius: 3px;
border-style: solid;
border-color: black;
margin-bottom: 5px;
border-width: 1px;
margin-top: 0px;
}
.div_ba {
background: rgba(255, 255, 5, 0.1);
}
.div_bb {
background: rgba(135, 211, 124, 0.2);
}
.div_bc {
background: whitesmoke;
}
.quadrant {
stroke: gray;
stroke-width: 0.6;
}
.quadlabel {
font-size: 13px;
fill: #34495e;
font-family: helvetica;
}
.div_bsub .axis path, .div_bsub .axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.div_bsub text {
font-family: sans-serif;
font-size: 9px;
}
.div_bsub .div_ba_hover_line {
stroke: gray;
stroke-width: 0.7;
}
</style>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="d3.legend.js"></script>
<script src="data_gen.js"></script>
<script src="helpers.js"></script>
<body>
<script>
glob = null;
var width = 940;
var height = 450;
var plot_maps = {
div_a: plot_div_a,
div_ba: plot_div_ba,
div_bb: plot_div_bb,
div_bc: plot_div_bc
};
function plot_div_bc(data, div_name, div_, _skeleton_, extra_params){
margin = {top: 8, right: 5, bottom: 5, left: 8};
var dw = div_[0][0].clientWidth;
var dh = div_[0][0].clientHeight;
var helper = helpers[div_name];
helper.dh = dh;
helper.dw = dw;
helper.margin = margin;
helper.min_y = 0;
helper.max_y = 200;
var space = div_.append("svg").attr("width", dw).attr("height", dh);
var scales = helpers[div_name].getscales(dw, dh);
helper.addaxes(scales, space, dw, dh);
helper.space = space.append("g");
helper.scales = scales;
return;
}
function plot_div_bb(data, div_name, div_, _skeleton_, extra_params){
margin = {top: 8, right: 5, bottom: 5, left: 8};
var dw = div_[0][0].clientWidth;
var dh = div_[0][0].clientHeight;
var helper = helpers[div_name];
helper.dh = dh;
helper.dw = dw;
helper.margin = margin;
helper.min_x = "03-15"
helper.max_x = "07-15";
helper.min_y = 0;
helper.max_y = 30;
var space = div_.append("svg").attr("width", dw).attr("height", dh);
var scales = helpers[div_name].getscales(dw, dh);
helper.addaxes(scales, space, dw, dh);
helper.space = space.append("g");
helper.scales = scales;
return;
}
function plot_div_ba(data, div_name, div_, _skeleton_, extra_params){
margin = {top: 8, right: 5, bottom: 5, left: 8};
var dw = div_[0][0].clientWidth;
var dh = div_[0][0].clientHeight;
var helper = helpers[div_name];
helper.dh = dh;
helper.dw = dw;
helper.margin = margin;
helper.min_x = d3.min(data, function(d){return d.family_size});
helper.max_x = d3.max(data, function(d){return d.family_size});
helper.min_y = d3.min(data, function(d){return d.purchases_per_visit});
helper.max_y = d3.max(data, function(d){return d.purchases_per_visit});
var space = div_.append("svg").attr("width", dw).attr("height", dh);
var scales = helpers[div_name].getscales(dw, dh);
helper.addaxes(scales, space, dw, dh);
helper.space = space.append("g");
helper.scales = scales;
return;
}
function plot_div_a(data, div_name, div_, _skeleton_, extra_params){
//plots the major quadrants plot
var dw = div_[0][0].clientWidth;
var dh = div_[0][0].clientHeight;
var helper = helpers[div_name];
var quadrants = helper.getquadrants(dw, dh);
var space = div_.append("svg").attr("width", dw).attr("height", dh);
space.append("g").selectAll()
.data(data)
.enter()
.append("circle")
.attr("cx", function(d, i){
var qv = helpers[div_name].get_q(quadrants, d);
var cx = (qv.minw+qv.maxw)/2;
return helper.pick_sign(cx, 200);
})
.attr("cy", function(d, i){
var qv = helpers[div_name].get_q(quadrants, d);
var cy = (qv.minh+qv.maxh)/2;
return helper.pick_sign(cy, 150);
})
.attr("r", function(d){ return d.radius})
.style("fill", function(d){ return d.fill })
.attr("data-legend", function(d) { return d.race })
.attr("class", "div_a_p")
.on("mouseenter", function(d){
d3.select(this).transition().duration(200).attr("r", 9);
helper.interact(d, helpers);
})
.on("mouseleave", helper.mouseleave);
//add quadrants
helper.addquads(space, dw, dh);
//add labels
helper.addlabels(space, dw, dh);
//add legend
helper.addlegend(space, dw, dh);
}
function _build_skeleton(w, h){
var sk = {};
sk.div_major = d3.select("body").append("div").attr("class", "major_div");
sk.div_a = sk.div_major.append("div").attr("class", "div_a");
sk.div_b = sk.div_major.append("div").attr("class", "div_b");
sk.div_ba = sk.div_b.append("div").attr("class", "div_bsub div_ba").style("height", (h/3) + "px");
sk.div_bb = sk.div_b.append("div").attr("class", "div_bsub div_bb").style("height", (h/3) + "px");
sk.div_bc = sk.div_b.append("div").attr("class", "div_bsub div_bc").style("height", (h/3) + "px");
return sk;
}
var data = data_gen.data_gen();
var _skeleton_ = _build_skeleton(width, height);
for (var plt in plot_maps){
plot_maps[plt](data, plt, _skeleton_[plt], _skeleton_, {});
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment