Skip to content

Instantly share code, notes, and snippets.

@timelyportfolio
Last active April 30, 2019 15:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save timelyportfolio/862dcabf010da04b9b5b to your computer and use it in GitHub Desktop.
Save timelyportfolio/862dcabf010da04b9b5b to your computer and use it in GitHub Desktop.
!function(){
var bP={};
var b=30, bb=150, height=600, buffMargin=1, minHeight=14;
var c1=[-130, 40], c2=[-50, 100], c3=[-10, 140]; //Column positions of labels.
var colors =["#3366CC", "#DC3912", "#FF9900","#109618", "#990099", "#0099C6"];
bP.partData = function(data,p){
var sData={};
sData.keys=[
d3.set(data.map(function(d){ return d[0];})).values().sort(function(a,b){ return ( a<b? -1 : a>b ? 1 : 0);}),
d3.set(data.map(function(d){ return d[1];})).values().sort(function(a,b){ return ( a<b? -1 : a>b ? 1 : 0);})
];
sData.data = [ sData.keys[0].map( function(d){ return sData.keys[1].map( function(v){ return 0; }); }),
sData.keys[1].map( function(d){ return sData.keys[0].map( function(v){ return 0; }); })
];
data.forEach(function(d){
sData.data[0][sData.keys[0].indexOf(d[0])][sData.keys[1].indexOf(d[1])]=d[p];
sData.data[1][sData.keys[1].indexOf(d[1])][sData.keys[0].indexOf(d[0])]=d[p];
});
return sData;
}
function visualize(data){
var vis ={};
function calculatePosition(a, s, e, b, m){
var total=d3.sum(a);
var sum=0, neededHeight=0, leftoverHeight= e-s-2*b*a.length;
var ret =[];
a.forEach(
function(d){
var v={};
v.percent = (total == 0 ? 0 : d/total);
v.value=d;
v.height=Math.max(v.percent*(e-s-2*b*a.length), m);
(v.height==m ? leftoverHeight-=m : neededHeight+=v.height );
ret.push(v);
}
);
var scaleFact=leftoverHeight/Math.max(neededHeight,1), sum=0;
ret.forEach(
function(d){
d.percent = scaleFact*d.percent;
d.height=(d.height==m? m : d.height*scaleFact);
d.middle=sum+b+d.height/2;
d.y=s + d.middle - d.percent*(e-s-2*b*a.length)/2;
d.h= d.percent*(e-s-2*b*a.length);
d.percent = (total == 0 ? 0 : d.value/total);
sum+=2*b+d.height;
}
);
return ret;
}
vis.mainBars = [
calculatePosition( data.data[0].map(function(d){ return d3.sum(d);}), 0, height, buffMargin, minHeight),
calculatePosition( data.data[1].map(function(d){ return d3.sum(d);}), 0, height, buffMargin, minHeight)
];
vis.subBars = [[],[]];
vis.mainBars.forEach(function(pos,p){
pos.forEach(function(bar, i){
calculatePosition(data.data[p][i], bar.y, bar.y+bar.h, 0, 0).forEach(function(sBar,j){
sBar.key1=(p==0 ? i : j);
sBar.key2=(p==0 ? j : i);
vis.subBars[p].push(sBar);
});
});
});
vis.subBars.forEach(function(sBar){
sBar.sort(function(a,b){
return (a.key1 < b.key1 ? -1 : a.key1 > b.key1 ?
1 : a.key2 < b.key2 ? -1 : a.key2 > b.key2 ? 1: 0 )});
});
vis.edges = vis.subBars[0].map(function(p,i){
return {
key1: p.key1,
key2: p.key2,
y1:p.y,
y2:vis.subBars[1][i].y,
h1:p.h,
h2:vis.subBars[1][i].h
};
});
vis.keys=data.keys;
return vis;
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return edgePolygon(i(t));
};
}
function drawPart(data, id, p){
d3.select("#"+id).append("g").attr("class","part"+p)
.attr("transform","translate("+( p*(bb+b))+",0)");
d3.select("#"+id).select(".part"+p).append("g").attr("class","subbars");
d3.select("#"+id).select(".part"+p).append("g").attr("class","mainbars");
var mainbar = d3.select("#"+id).select(".part"+p).select(".mainbars")
.selectAll(".mainbar").data(data.mainBars[p])
.enter().append("g").attr("class","mainbar");
mainbar.append("rect").attr("class","mainrect")
.attr("x", 0).attr("y",function(d){ return d.middle-d.height/2; })
.attr("width",b).attr("height",function(d){ return d.height; })
.style("shape-rendering","auto")
.style("fill-opacity",0).style("stroke-width","0.5")
.style("stroke","black").style("stroke-opacity",0);
mainbar.append("text").attr("class","barlabel")
.attr("x", c1[p]).attr("y",function(d){ return d.middle+5;})
.text(function(d,i){ return data.keys[p][i];})
.attr("text-anchor","start" );
mainbar.append("text").attr("class","barvalue")
.attr("x", c2[p]).attr("y",function(d){ return d.middle+5;})
.text(function(d,i){ return d.value ;})
.attr("text-anchor","end");
mainbar.append("text").attr("class","barpercent")
.attr("x", c3[p]).attr("y",function(d){ return d.middle+5;})
.text(function(d,i){ return "( "+Math.round(100*d.percent)+"%)" ;})
.attr("text-anchor","end").style("fill","grey");
d3.select("#"+id).select(".part"+p).select(".subbars")
.selectAll(".subbar").data(data.subBars[p]).enter()
.append("rect").attr("class","subbar")
.attr("x", 0).attr("y",function(d){ return d.y})
.attr("width",b).attr("height",function(d){ return d.h})
.style("fill",function(d){ return colors[d.key1];});
}
function drawEdges(data, id){
d3.select("#"+id).append("g").attr("class","edges").attr("transform","translate("+ b+",0)");
d3.select("#"+id).select(".edges").selectAll(".edge")
.data(data.edges).enter().append("polygon").attr("class","edge")
.attr("points", edgePolygon).style("fill",function(d){ return colors[d.key1];})
.style("opacity",0.5).each(function(d) { this._current = d; });
}
function drawHeader(header, id){
d3.select("#"+id).append("g").attr("class","header").append("text").text(header[2])
.style("font-size","20").attr("x",108).attr("y",-20).style("text-anchor","middle")
.style("font-weight","bold");
[0,1].forEach(function(d){
var h = d3.select("#"+id).select(".part"+d).append("g").attr("class","header");
h.append("text").text(header[d]).attr("x", (c1[d]-5))
.attr("y", -5).style("fill","grey");
h.append("text").text("Count").attr("x", (c2[d]-10))
.attr("y", -5).style("fill","grey");
h.append("line").attr("x1",c1[d]-10).attr("y1", -2)
.attr("x2",c3[d]+10).attr("y2", -2).style("stroke","black")
.style("stroke-width","1").style("shape-rendering","crispEdges");
});
}
function edgePolygon(d){
return [0, d.y1, bb, d.y2, bb, d.y2+d.h2, 0, d.y1+d.h1].join(" ");
}
function transitionPart(data, id, p){
var mainbar = d3.select("#"+id).select(".part"+p).select(".mainbars")
.selectAll(".mainbar").data(data.mainBars[p]);
mainbar.select(".mainrect").transition().duration(500)
.attr("y",function(d){ return d.middle-d.height/2;})
.attr("height",function(d){ return d.height;});
mainbar.select(".barlabel").transition().duration(500)
.attr("y",function(d){ return d.middle+5;});
mainbar.select(".barvalue").transition().duration(500)
.attr("y",function(d){ return d.middle+5;}).text(function(d,i){ return d.value ;});
mainbar.select(".barpercent").transition().duration(500)
.attr("y",function(d){ return d.middle+5;})
.text(function(d,i){ return "( "+Math.round(100*d.percent)+"%)" ;});
d3.select("#"+id).select(".part"+p).select(".subbars")
.selectAll(".subbar").data(data.subBars[p])
.transition().duration(500)
.attr("y",function(d){ return d.y}).attr("height",function(d){ return d.h});
}
function transitionEdges(data, id){
d3.select("#"+id).append("g").attr("class","edges")
.attr("transform","translate("+ b+",0)");
d3.select("#"+id).select(".edges").selectAll(".edge").data(data.edges)
.transition().duration(500)
.attrTween("points", arcTween)
.style("opacity",function(d){ return (d.h1 ==0 || d.h2 == 0 ? 0 : 0.5);});
}
function transition(data, id){
transitionPart(data, id, 0);
transitionPart(data, id, 1);
transitionEdges(data, id);
}
bP.draw = function(data, svg){
data.forEach(function(biP,s){
svg.append("g")
.attr("id", biP.id)
.attr("transform","translate("+ (550*s)+",0)");
var visData = visualize(biP.data);
drawPart(visData, biP.id, 0);
drawPart(visData, biP.id, 1);
drawEdges(visData, biP.id);
drawHeader(biP.header, biP.id);
[0,1].forEach(function(p){
d3.select("#"+biP.id)
.select(".part"+p)
.select(".mainbars")
.selectAll(".mainbar")
.on("mouseover",function(d, i){ return bP.selectSegment(data, p, i); })
.on("mouseout",function(d, i){ return bP.deSelectSegment(data, p, i); });
});
});
}
bP.selectSegment = function(data, m, s){
data.forEach(function(k){
var newdata = {keys:[], data:[]};
newdata.keys = k.data.keys.map( function(d){ return d;});
newdata.data[m] = k.data.data[m].map( function(d){ return d;});
newdata.data[1-m] = k.data.data[1-m]
.map( function(v){ return v.map(function(d, i){ return (s==i ? d : 0);}); });
transition(visualize(newdata), k.id);
var selectedBar = d3.select("#"+k.id).select(".part"+m).select(".mainbars")
.selectAll(".mainbar").filter(function(d,i){ return (i==s);});
selectedBar.select(".mainrect").style("stroke-opacity",1);
selectedBar.select(".barlabel").style('font-weight','bold');
selectedBar.select(".barvalue").style('font-weight','bold');
selectedBar.select(".barpercent").style('font-weight','bold');
});
}
bP.deSelectSegment = function(data, m, s){
data.forEach(function(k){
transition(visualize(k.data), k.id);
var selectedBar = d3.select("#"+k.id).select(".part"+m).select(".mainbars")
.selectAll(".mainbar").filter(function(d,i){ return (i==s);});
selectedBar.select(".mainrect").style("stroke-opacity",0);
selectedBar.select(".barlabel").style('font-weight','normal');
selectedBar.select(".barvalue").style('font-weight','normal');
selectedBar.select(".barpercent").style('font-weight','normal');
});
}
this.bP = bP;
}();
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg text{
font-size:12px;
}
rect{
shape-rendering:crispEdges;
}
</style>
<body>
<div style = "width:100%;height:100%;">
<svg id = "bipartite_svg" style = "width:90%;height:90%" viewbox = "0 0 1100 610" preserveAspectRatio="xMidYMid">
</svg>
</div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="biPartite.js"></script>
<script>
var sales_data=[
['Lite','AL',16,0],
['Small','AL',1278,4],
['Medium','AL',27,0],
['Plus','AL',58,0],
['Grand','AL',1551,15],
['Elite','AL',141,0],
['Lite','AZ',5453,35],
['Small','AZ',683,1],
['Medium','AZ',862,0],
['Grand','AZ',6228,30],
['Lite','CA',15001,449],
['Small','CA',527,3],
['Medium','CA',836,0],
['Plus','CA',28648,1419],
['Grand','CA',3,0],
['Lite','CO',13,0],
['Small','CO',396,0],
['Medium','CO',362,0],
['Plus','CO',78,10],
['Grand','CO',2473,32],
['Elite','CO',2063,64],
['Medium','DE',203,0],
['Grand','DE',686,2],
['Elite','DE',826,0],
['Lite','FL',1738,110],
['Small','FL',12925,13],
['Medium','FL',15413,0],
['Small','GA',2166,2],
['Medium','GA',86,0],
['Plus','GA',348,3],
['Grand','GA',4244,18],
['Elite','GA',1536,1],
['Small','IA',351,0],
['Grand','IA',405,1],
['Small','IL',914,1],
['Medium','IL',127,0],
['Grand','IL',1470,7],
['Elite','IL',516,1],
['Lite','IN',43,0],
['Small','IN',667,1],
['Medium','IN',172,0],
['Plus','IN',149,1],
['Grand','IN',1380,5],
['Elite','IN',791,23],
['Small','KS',1,0],
['Grand','KS',1,0],
['Small','MD',1070,1],
['Grand','MD',1171,2],
['Elite','MD',33,0],
['Plus','ME',1,0],
['Small','MS',407,0],
['Medium','MS',3,0],
['Grand','MS',457,2],
['Elite','MS',20,0],
['Small','NC',557,0],
['Medium','NC',167,0],
['Plus','NC',95,1],
['Grand','NC',1090,5],
['Elite','NC',676,6],
['Lite','NM',1195,99],
['Small','NM',350,3],
['Medium','NM',212,0],
['Grand','NM',1509,8],
['Lite','NV',3899,389],
['Small','NV',147,0],
['Medium','NV',455,0],
['Plus','NV',1,1],
['Grand','NV',4100,16],
['Lite','OH',12,0],
['Small','OH',634,2],
['Medium','OH',749,0],
['Plus','OH',119,1],
['Grand','OH',3705,19],
['Elite','OH',3456,25],
['Small','PA',828,2],
['Medium','PA',288,0],
['Plus','PA',141,0],
['Grand','PA',2625,7],
['Elite','PA',1920,10],
['Small','SC',1146,2],
['Medium','SC',212,0],
['Plus','SC',223,4],
['Grand','SC',1803,6],
['Elite','SC',761,8],
['Small','TN',527,0],
['Medium','TN',90,0],
['Grand','TN',930,4],
['Elite','TN',395,1],
['Lite','TX',7232,58],
['Small','TX',1272,0],
['Medium','TX',1896,0],
['Plus','TX',1,0],
['Grand','TX',10782,33],
['Elite','TX',1911,3],
['Small','VA',495,0],
['Medium','VA',32,0],
['Plus','VA',7,0],
['Grand','VA',1557,12],
['Elite','VA',24,0],
['Small','WA',460,1],
['Plus','WA',88,3],
['Grand','WA',956,3],
['Small','WV',232,0],
['Medium','WV',71,0],
['Grand','WV',575,2],
['Elite','WV',368,3]
];
var width = 1100, height = 610, margin ={b:0, t:40, l:170, r:50};
var svg = d3.select("#bipartite_svg")//.attr("viewbox","0 0 " + width + " " + height)
.append("g").attr("transform","translate("+ margin.l+","+margin.t+")");
var data = [
{data:bP.partData(sales_data,2), id:'SalesAttempts', header:["Channel","State", "Sales Attempts"]},
{data:bP.partData(sales_data,3), id:'Sales', header:["Channel","State", "Sales"]}
];
bP.draw(data, svg);
</script>
</body>
@timelyportfolio
Copy link
Author

Questions / suggestions

  1. why do the bars not contain the labels /columns? Seems like this would be more efficient use of space, but also more clearly convey the connection between labels and rectangles.
  2. why are part 1 and part 2 separated and not connected? Seems like an obvious connection opportunity and also could eliminate one or really two column(s) so more width available.
  3. if not 2, then should these be separate, so a bipartite is just part 1 or part 2 not both? Then, we could allow interactivity/syncing between both.
  4. Alex brings up a good point on twitter. These could be great legend-type elements.
  5. why not change the order of the columns so the inner are the same and the outer the same?
  6. what are the limits to number of nodes?
  7. why are the columns not ordered, or why is there not an option to sort by a column? Seems like ordering would more clearly communicate relationships or reveal structure.
  8. why is there no dragging like sankeys?
  9. why not just add the bipartite extra interactivity to sankeys? Seems like a sankey with path.curvature(0).
  10. on hover, why not retain gross aggregate / total, so can see both within the group hovered and also within total?
  11. what about a marimekko?

Other thoughts

In effect, these are just stacked bars with an extra layer of interactivity.

@abresler
Copy link

Agreed on all points this library has so much potential though.

@abresler
Copy link

As a related aside I have been trying to figure out a solution in R but haven't found one that works; any idea how to export to JSON Array in JSONlite? If you try to recreate this by taking the data from a dataframe I haven't found a way to do it cleanly {now I export data to csv and use a csv to json conversion tool on the web that does arrays}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment