Skip to content

Instantly share code, notes, and snippets.

@emeeks
Last active March 17, 2016 02:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emeeks/58675e9b609295707b9a to your computer and use it in GitHub Desktop.
Save emeeks/58675e9b609295707b9a to your computer and use it in GitHub Desktop.
Ch. 12, Fig. 11 - D3.js in Action

This is the code for Chapter 12, Figure 11 from D3.js in Action which requires a touch interface (or emulator) to see any effect. This implements the final application developed throughout the second half of Chapter 12. This responsive data visualization displays different data visualizations and exposes different functionality depending on whether it is accessed by a small screen (phone-sized) a medium screen (tablet-sized) or a large screen (laptop-sized).

Click open in a new window from each of those screens to see the different views into the same dataset.

You can read more about responsive data visualization here.

<html>
<head>
<title>D3 in Action Chapter 12 - Example 9</title>
<meta charset="utf-8" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="http://d3js.org/colorbrewer.v1.min.js"></script>
</head>
<style>
body, html {
width:100%;
height:100%;
}
#vizcontainer {
width:100%;
height:100%;
}
svg {
width: 100%;
height: 100%;
}
#modal {
position: absolute;
height: 130px;
width: 200px;
background: white;
box-shadow: 2px 2px 0px #888888;
border: 1px #888888 solid;
left: -300px;
top: -300px;
}
#modal > p {
margin: 2px;
padding: 2px;
}
rect.extent {
opacity: .25;
}
g.resize > circle {
fill: lightgray;
stroke: black;
stroke-width: 5px;
}
text.brushLabel {
font-size: 40px;
font-weight: 900;
text-anchor: middle;
opacity: .5;
pointer-events: none;
}
div.viewTitle {
font-size: 54px;
font-weight: 900;
color: darkred;
position: fixed;
top: 0;
width: 100%;
height: 140px;
background: rgba(255,255,255,.95);
text-align: center;
}
div.viewStats {
font-size: 54px;
font-weight: 900;
position: fixed;
bottom: 0;
width: 100%;
height: 140px;
background: rgba(255,255,255,.95);
text-align: center;
}
div.viewStats > div {
width: 100%;
}
</style>
<body>
<div id="vizcontainer">
<svg></svg>
</div>
</body>
<footer>
<script>
d3.json("realestate.json", function(data) {realEstate(data)});
function realEstate(data) {
d3.select("svg").style("width", "80%").style("float", "left");
d3.select("#vizcontainer").append("div").attr("id", "list").style("float", "left").style("height", "90%").style("width", "18%").style("overflow", "auto");
d3.select("#vizcontainer").append("div").attr("id", "modal");
d3.select("#list").append("ol").selectAll("li").data(data).enter().append("li").attr("class", "datapoint").html(function(d) {return d.name})
.on("mouseover", highlightDatapoint)
screenHeight = parseFloat(d3.select("svg").node().clientHeight || d3.select("svg").node().parentNode.clientHeight);
screenWidth = parseFloat(d3.select("svg").node().clientWidth || d3.select("svg").node().parentNode.clientWidth);
sizeExtent = d3.extent(data, function(d) {return d.size})
valueExtent = d3.extent(data, function(d) {return d.value})
xScale = d3.scale.linear().domain(sizeExtent).range([40,screenWidth-40])
yScale = d3.scale.linear().domain(valueExtent).range([screenHeight-40,40])
d3.select("svg").append("g").attr("id", "dataG").selectAll("g.datapoint").data(data, function(d) {return d.name}).enter()
.append("g").attr("class", "datapoint");
locationScale = d3.scale.ordinal().domain(["Rural","Coastal","Suburb","City"]).range(colorbrewer.Reds[4]);
typeShape = {"Spanish": "circle","Craftsman":"cross","Ranch":"square","McMansion":"triangle-down"};
dataG = d3.selectAll("g.datapoint")
.attr("transform", function(d) {return "translate(" + xScale(d.size) + "," + yScale(d.value) +")"})
.each(function(d) {
houseSymbol = d3.svg.symbol().type(typeShape[d.type]).size(64);
d3.select(this).append("path")
.attr("d", houseSymbol)
.style("fill", locationScale(d.location))
.style("stroke", "black")
.style("stroke-width", "1px")
.on("mouseover", highlightDatapoint)
})
xAxis = d3.svg.axis().scale(xScale).orient("top").tickSize(4);
yAxis = d3.svg.axis().scale(yScale).orient("right").tickSize(4);
d3.select("svg").append("g").attr("id", "xAxisG").attr("class", "axis").attr("transform", "translate(0,"+(screenHeight - 20)+")").call(xAxis);
d3.select("svg").append("g").attr("id", "yAxisG").attr("class", "axis").attr("transform", "translate(20,0)").call(yAxis);
// d3.select("body").append("button").html("Tablet").on("click", tabletView)
var screenSize = screen.width;
if (screenSize < 480) {
phoneView();
}
else if (screenSize < 1000) {
tabletView();
}
function highlightDatapoint(d) {
d3.selectAll("li.datapoint").style("font-weight", function(p) {return p == d ? 900 : 100})
d3.selectAll("g.datapoint").select("path").style("stroke-width", function(p) {return p == d ? "3px" : "1px"})
var modal = d3.select("#modal").style("top", yScale(d.value) - 135).style("left", xScale(d.size) - 100);
modal.selectAll("*").remove();
modal.append("p").html(d.name)
modal.append("p").html("Location: " + d.location)
modal.append("p").html("Style: " + d.type)
modal.append("p").html("Size: " + d.size + "Sq. Ft.")
modal.append("p").html("Value: $" + d.value)
}
function tabletView() {
d3.select("svg").style("width", "100%");
screenWidth = parseFloat(d3.select("svg").node().clientWidth || d3.select("svg").node().parentNode.clientWidth);
d3.select("#list").remove();
var cellWidth = screenWidth / 18;
var cellHeight = screenHeight / 14;
sortedData = data.sort(function(a,b) {
if (a.value > b.value)
return 1;
if (a.value < b.value)
return -1;
return 0;
});
d3.selectAll("g.datapoint").data(sortedData, function(d) {return d.name})
.transition()
.duration(1000)
.attr("transform", function(d,i) {return "translate("+((Math.floor(i/6) + .5) * cellWidth)+","+((i%6 + .5)*cellHeight)+")"});
d3.selectAll("g.datapoint").select("path")
.on("mouseover", null)
.each(function(d) {
houseSymbol = d3.svg.symbol().type(typeShape[d.type]).size(512);
d3.select(this).transition().duration(1000).attr("d", houseSymbol);
})
xScale.range([40,screenWidth-40])
xAxis.orient("bottom").scale(xScale);
yScale.range([40,screenWidth-40])
yAxis.orient("bottom").scale(yScale);
sizeBrush = d3.svg.brush()
.x(xScale)
.extent(sizeExtent)
.on("brush", brushed);
valueBrush = d3.svg.brush()
.x(yScale)
.extent(valueExtent)
.on("brush", brushed);
d3.select("#xAxisG").transition().duration(1000).attr("transform", "translate(0,"+(screenHeight - 150)+")").call(xAxis);
d3.select("#yAxisG").transition().duration(1000).attr("transform", "translate(0,"+(screenHeight - 50)+")").call(yAxis);
d3.select("#xAxisG").append("g").attr("class", "brushG").attr("transform", "translate(0,-80)").call(sizeBrush).insert("text", "rect").attr("class", "brushLabel").text("Square Footage").attr("x", screenWidth / 2).attr("y", 50);
d3.select("#yAxisG").append("g").attr("class", "brushG").attr("transform", "translate(0,-80)").call(valueBrush).insert("text", "rect").attr("class", "brushLabel").text("Home Value").attr("x", screenWidth / 2).attr("y", 50);
d3.selectAll(".brushG").selectAll("rect").attr("height", 80);
d3.selectAll(".brushG").selectAll(".resize").append("circle").attr("r", 40).attr("cy", 40);
function brushed() {
d3.selectAll("g.datapoint").each(function(d) {
var color = locationScale(d.location);
if (d.value < valueBrush.extent()[0] || d.value > valueBrush.extent()[1] || d.size < sizeBrush.extent()[0] || d.size > sizeBrush.extent()[1]) {
color = "lightgray";
}
d3.select(this).select("path").style("fill", color);
})
}
}
function phoneView() {
initialLength = 0;
for (x in data) {
data[x].oValue = data[x].value;
}
nestedTweets = d3.nest()
.key(function (d) {return d.location})
.entries(data);
for (x in nestedTweets) {
var subNestedTweets = d3.nest()
.key(function (d) {return d.type})
.entries(nestedTweets[x].values);
nestedTweets[x].values = subNestedTweets;
}
packableTweets = {id: "root", key: "All Real Estate", values: nestedTweets}
d3.select("svg").style("width", "100%")
.on("touchstart", pinchInitial)
.on("touchend", pinchCheck)
;
function pinchInitial() {
var touches = d3.touches(this);
if (touches.length > 2) {
initialLength = Math.sqrt(Math.abs(initialD[0][0] - initialD[1][0]) + Math.abs(initialD[0][1] - initialD[1][1]));
}
}
function pinchCheck() {
var touches = d3.touches(this);
if (touches.length > 2) {
if (initialLength > Math.sqrt(Math.abs(initialD[0][0] - initialD[1][0]) + Math.abs(initialD[0][1] - initialD[1][1]))) {
changeView(packableTweets);
}
}
}
screenWidth = parseFloat(d3.select("svg").node().clientWidth || d3.select("svg").node().parentNode.clientWidth);
d3.select("#list").remove();
d3.selectAll("g.axis").remove();
circleSize = d3.scale.linear().domain(sizeExtent).range([2,10])
circleStroke = d3.scale.linear().domain(valueExtent).range([1,5])
packChart = d3.layout.pack();
packChart.size([screenWidth,screenHeight-200])
.children(function(d) {return d.values})
.value(function(d) {return circleSize(d.size)})
d3.selectAll("g.datapoint").select("path")
.style("pointer-events", "none")
;
d3.select("#dataG")
.attr("transform", "translate(0,100)")
.selectAll("circle")
.data(packChart(packableTweets))
.enter()
.insert("circle","g")
.attr("class", "pack")
.style("fill", "white")
.style("stroke", "black")
.style("stroke-width", function(d) {return circleStroke(d.oValue)})
.on("touchmove", changeView)
.on("click", changeView);
d3.select("#vizcontainer").append("div").attr("class", "viewTitle").html("Current View")
var viewStats = d3.select("#vizcontainer").append("div").attr("class", "viewStats");
viewStats.append("div").attr("id", "viewValue").html("Average Value");
viewStats.append("div").attr("id", "viewSize").html("Average Size");
changeView(packableTweets)
function changeView(d) {
newScale = (screenHeight / 2) / (d.r + 100)
d3.select("#dataG").selectAll("circle").style("fill", function(p) {return p == d ? "lightgray" : "white"})
d3.select("#dataG").selectAll("circle").style("pointer-events", function(p) {return (p.depth == d.depth || p.parent == d) && p != d ? "auto" : "none"})
d3.select("#dataG").transition().duration(1000).attr("transform", "translate(" + ((screenWidth/2)-(d.x * newScale)) + "," + ((screenHeight/2) -(d.y * newScale)) +")");
d3.selectAll("circle.pack")
.transition().duration(1000)
.attr("r", function(d) {return d.r * newScale})
.attr("cx", function(d) {return d.x * newScale})
.attr("cy", function(d) {return d.y * newScale})
symbolSize = d3.scale.linear().domain(sizeExtent).range([100 * newScale,180 * newScale])
d3.selectAll("g.datapoint").transition().duration(1000)
.attr("transform", function(d) {return "translate(" + (d.x * newScale) + "," + (d.y * newScale) +")"})
.select("path")
.each(function(d) {
houseSymbol = d3.svg.symbol().type(typeShape[d.type]).size(symbolSize(d.size));
d3.select(this).transition().duration(1000).attr("d", houseSymbol);
})
calculateStatistics(d);
function calculateStatistics(d) {
if (d.name) {
d3.select("div.viewTitle").html(d.parent.parent.key + " - " + d.parent.key + "<br>" + d.name);
d3.select("#viewValue").html("Value: $" + d.oValue);
d3.select("#viewSize").html("Size: " + d.size + " square feet");
}
else {
var allDatapoints = allChildren(d);
console.log(allDatapoints)
var averageValue = d3.mean(allDatapoints, function(d) {return d.oValue})
var averageSize = d3.mean(allDatapoints, function(d) {return d.size})
d3.select("div.viewTitle").html(d.depth == 2 ? d.parent.key + " - " + d.key : d.key);
d3.select("#viewValue").html("Average Value: $" + d3.format("0,000")(Math.floor(averageValue)));
d3.select("#viewSize").html("Average Size: " + d3.format("0,000")(Math.floor(averageSize)) + " square feet");
}
function allChildren(d) {
var childArray = [];
for (x in d.values) {
if (d.values[x].name) {
childArray.push(d.values[x]);
}
else {
childArray = allChildren(d.values[x]);
}
}
return childArray;
}
}
}
}
}
</script>
</footer>
</html>
[{"type":"Craftsman","size":1680,"location":"Coastal","name":"8106 Generated Ct.","value":200799},{"type":"Ranch","size":2567,"location":"Rural","name":"502 Sample Ct.","value":215445},{"type":"McMansion","size":3193,"location":"Coastal","name":"202 Procedural Ave.","value":280890},{"type":"Spanish","size":1377,"location":"Coastal","name":"2688 Random Blvd.","value":215616},{"type":"McMansion","size":3835,"location":"Suburb","name":"8428 Sample Ln.","value":477235},{"type":"Spanish","size":2445,"location":"Rural","name":"8152 Generated Ave.","value":187891},{"type":"Ranch","size":2021,"location":"Rural","name":"9475 Random St.","value":175445},{"type":"Ranch","size":3523,"location":"City","name":"3369 Generated Ln.","value":763971},{"type":"Craftsman","size":2217,"location":"Coastal","name":"9349 Random Rd.","value":189290},{"type":"Craftsman","size":1331,"location":"City","name":"9399 Random St.","value":271023},{"type":"Craftsman","size":2992,"location":"Suburb","name":"7244 Procedural Ct.","value":244138},{"type":"Spanish","size":1229,"location":"City","name":"1990 Sample Blvd.","value":118704},{"type":"Craftsman","size":3052,"location":"Suburb","name":"2148 Procedural St.","value":371489},{"type":"Craftsman","size":1495,"location":"City","name":"3802 Sample Rd.","value":296708},{"type":"McMansion","size":3725,"location":"Suburb","name":"909 Sample Ct.","value":354316},{"type":"Ranch","size":2233,"location":"Coastal","name":"287 Random Ln.","value":303444},{"type":"Spanish","size":1839,"location":"Suburb","name":"2806 Procedural Ave.","value":208892},{"type":"Spanish","size":1245,"location":"City","name":"412 Procedural Ave.","value":244870},{"type":"Craftsman","size":2557,"location":"City","name":"5380 Sample Ave.","value":292208},{"type":"Craftsman","size":3007,"location":"Coastal","name":"2569 Random Ct.","value":227624},{"type":"Craftsman","size":2427,"location":"City","name":"500 Sample Ct.","value":406172},{"type":"Ranch","size":2632,"location":"Rural","name":"3428 Procedural St.","value":234342},{"type":"Ranch","size":1729,"location":"Suburb","name":"529 Procedural Rd.","value":213404},{"type":"Craftsman","size":3294,"location":"City","name":"284 Procedural Ct.","value":700734},{"type":"Spanish","size":1567,"location":"Coastal","name":"5093 Random Ln.","value":219902},{"type":"Spanish","size":1368,"location":"City","name":"8889 Sample Rd.","value":239413},{"type":"McMansion","size":3309,"location":"City","name":"207 Generated Blvd.","value":649906},{"type":"Craftsman","size":2575,"location":"Suburb","name":"9630 Random Ln.","value":193236},{"type":"Craftsman","size":2745,"location":"City","name":"7062 Generated Rd.","value":331694},{"type":"McMansion","size":3559,"location":"Rural","name":"397 Sample Blvd.","value":284693},{"type":"Ranch","size":1930,"location":"Rural","name":"453 Generated Blvd.","value":173494},{"type":"Spanish","size":1203,"location":"Suburb","name":"9703 Procedural Blvd.","value":129697},{"type":"Spanish","size":1978,"location":"Suburb","name":"412 Sample Rd.","value":198486},{"type":"McMansion","size":3174,"location":"Rural","name":"2365 Generated Blvd.","value":260112},{"type":"Spanish","size":2105,"location":"Coastal","name":"4105 Sample St.","value":276252},{"type":"Craftsman","size":2965,"location":"Coastal","name":"2980 Random Blvd.","value":515928},{"type":"Spanish","size":1774,"location":"Coastal","name":"3416 Procedural Ct.","value":162278},{"type":"Ranch","size":2517,"location":"Suburb","name":"7303 Sample Rd.","value":253801},{"type":"McMansion","size":3644,"location":"Rural","name":"7581 Generated Ave.","value":347807},{"type":"Ranch","size":2968,"location":"City","name":"558 Generated Rd.","value":280624},{"type":"Craftsman","size":2534,"location":"Rural","name":"918 Random Blvd.","value":225456},{"type":"Spanish","size":2109,"location":"City","name":"857 Sample Blvd.","value":363387},{"type":"McMansion","size":3590,"location":"City","name":"632 Procedural Ln.","value":774953},{"type":"McMansion","size":3999,"location":"Suburb","name":"4668 Generated Ave.","value":429727},{"type":"Spanish","size":1758,"location":"Suburb","name":"2984 Generated Blvd.","value":132139},{"type":"Ranch","size":1884,"location":"Coastal","name":"9214 Procedural Ln.","value":174109},{"type":"Craftsman","size":1344,"location":"City","name":"3941 Generated Blvd.","value":280008},{"type":"Craftsman","size":2751,"location":"Suburb","name":"629 Generated Ct.","value":256393},{"type":"Ranch","size":2345,"location":"City","name":"4327 Sample St.","value":367928},{"type":"Craftsman","size":3191,"location":"Coastal","name":"510 Procedural Ct.","value":377104},{"type":"Ranch","size":2679,"location":"City","name":"591 Procedural Ct.","value":559225},{"type":"Ranch","size":2551,"location":"Coastal","name":"3101 Generated Rd.","value":266306},{"type":"Craftsman","size":3160,"location":"Rural","name":"4039 Random Blvd.","value":297746},{"type":"Ranch","size":1629,"location":"Coastal","name":"5566 Generated St.","value":163984},{"type":"Craftsman","size":3111,"location":"Suburb","name":"5364 Random Ln.","value":304653},{"type":"McMansion","size":2563,"location":"City","name":"9281 Random Ln.","value":390477},{"type":"Craftsman","size":3204,"location":"Suburb","name":"681 Procedural Ln.","value":311223},{"type":"Ranch","size":2508,"location":"Rural","name":"693 Sample Rd.","value":213520},{"type":"McMansion","size":3166,"location":"City","name":"6311 Procedural Ln.","value":620220},{"type":"Craftsman","size":1500,"location":"Suburb","name":"8103 Generated Ct.","value":184005},{"type":"Craftsman","size":2510,"location":"Suburb","name":"243 Generated Ave.","value":281330},{"type":"McMansion","size":3704,"location":"City","name":"3636 Procedural Ave.","value":433326},{"type":"Ranch","size":1966,"location":"Suburb","name":"5028 Random Ave.","value":231398},{"type":"McMansion","size":3366,"location":"Suburb","name":"705 Generated Ct.","value":314891},{"type":"McMansion","size":3379,"location":"City","name":"7755 Random Ave.","value":715493},{"type":"Ranch","size":2416,"location":"Coastal","name":"6373 Random Blvd.","value":315894},{"type":"McMansion","size":3611,"location":"City","name":"7505 Procedural Ln.","value":743541},{"type":"McMansion","size":3322,"location":"Coastal","name":"703 Procedural Ln.","value":414291},{"type":"Spanish","size":1931,"location":"City","name":"513 Random Ave.","value":213632},{"type":"Spanish","size":2385,"location":"Suburb","name":"413 Generated Ln.","value":198449},{"type":"Ranch","size":2223,"location":"Suburb","name":"739 Random Blvd.","value":227145},{"type":"McMansion","size":3309,"location":"Coastal","name":"9368 Procedural Blvd.","value":327146},{"type":"Ranch","size":1705,"location":"Rural","name":"6434 Random Rd.","value":152033},{"type":"McMansion","size":3457,"location":"Coastal","name":"4863 Sample Ln.","value":586063},{"type":"McMansion","size":3109,"location":"City","name":"2088 Procedural Ln.","value":264732},{"type":"Spanish","size":1646,"location":"Rural","name":"451 Sample St.","value":143788},{"type":"Spanish","size":2068,"location":"Suburb","name":"569 Generated St.","value":232812},{"type":"Ranch","size":2334,"location":"City","name":"462 Procedural Ct.","value":458074},{"type":"Spanish","size":1417,"location":"Coastal","name":"980 Generated Blvd.","value":154480},{"type":"Ranch","size":2175,"location":"Rural","name":"158 Procedural Blvd.","value":171389},{"type":"McMansion","size":3574,"location":"Coastal","name":"809 Sample St.","value":403264},{"type":"Spanish","size":1361,"location":"Coastal","name":"6734 Procedural Rd.","value":174874},{"type":"McMansion","size":3256,"location":"Suburb","name":"2309 Procedural Ave.","value":405370},{"type":"McMansion","size":3200,"location":"Coastal","name":"9524 Generated Ct.","value":273161},{"type":"Spanish","size":1964,"location":"City","name":"9056 Sample St.","value":413994},{"type":"Spanish","size":1188,"location":"Rural","name":"3249 Sample Ave.","value":115319},{"type":"McMansion","size":3519,"location":"City","name":"8282 Sample St.","value":284846},{"type":"Craftsman","size":1563,"location":"Coastal","name":"770 Procedural Ave.","value":161138},{"type":"Ranch","size":3202,"location":"Suburb","name":"9026 Procedural Rd.","value":249742},{"type":"Craftsman","size":3054,"location":"City","name":"8749 Sample Ln.","value":424731},{"type":"Spanish","size":1724,"location":"Coastal","name":"9611 Random St.","value":247532},{"type":"Ranch","size":2349,"location":"Coastal","name":"276 Generated Ln.","value":298931},{"type":"McMansion","size":3142,"location":"Suburb","name":"9719 Procedural Ln.","value":302821},{"type":"McMansion","size":3981,"location":"Suburb","name":"7524 Random St.","value":339554},{"type":"Ranch","size":3093,"location":"City","name":"4711 Sample Blvd.","value":572275},{"type":"Ranch","size":1651,"location":"Coastal","name":"2441 Sample Ln.","value":281454},{"type":"McMansion","size":2858,"location":"Coastal","name":"1024 Procedural Blvd.","value":316618},{"type":"McMansion","size":3649,"location":"Rural","name":"2639 Procedural Ct.","value":282055},{"type":"Craftsman","size":2591,"location":"City","name":"7870 Generated St.","value":448490},{"type":"Craftsman","size":3091,"location":"Coastal","name":"432 Generated Rd.","value":355961}]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment