forked from ndobie's block: Bar Chart and Line Chart on Same D3 Plot
xxxxxxxxxx
<html>
<head>
<title>New Mexico Employment in NAICS Sector 21</title>
<meta charset="utf-8" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<style>
@import url('//fonts.googleapis.com/css?family=Lato:400,400italic,700,700italic');
body {
font-family: 'Lato', Arial, Helvetica, sans-serif;
font-size: 16px;
position: relative;
}
h3 {
margin-left: 4px;
}
/* .arc text {
text-anchor: middle;
}
.arc path {
stroke: #fff;
}
#pie-chart {
background-color: #ffffff;
height: 370px;
text-shadow: none;
margin-right: 0;
}
#pie-chart .total {
font-size: 18px;
font-weight: bold;
}
#pie-chart .units {
fill: gray;
font-size: 12px;
}
#pie-chart .label {
fill: #CCC;
font-size: 12px;
font-weight: bold;
}
#pie-chart .value {
font-size: 18px;
font-weight: bold;
} */
.toolTip {
position: absolute;
display: none;
width: auto;
height: auto;
background: none repeat scroll 0 0 white;
border: 0 none;
border-radius: 8px 8px 8px 8px;
box-shadow: -3px 3px 15px #888888;
color: black;
font: 12px sans-serif;
padding: 5px;
text-align: center;
}
.bar text {
font-size: 1em;
font-weight: 300;
cursor: default;
}
.axis line {
stroke: #AAA;
stroke-width: 1;
}
line.median {
stroke: #777;
stroke-width: 1;
}
.grid .tick {
stroke: lightgrey;
opacity: 0.7;
}
.grid path {
stroke-width: 0;
}
.bar {
fill: #74c476;
}
.bar:hover {
fill: #a1d99b;
}
.axisR {
font: 0.625em sans-serif;
}
.axisL {
font: 0.625em sans-serif;
}
.axis {
font: 0.625em sans-serif;
}
.axisR path,
.axisR line,
.axisL path,
.axisL line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
.chartLine {
fill: red;
stroke: red;
}
.chartLineText {
font: 0.625em sans-serif;
pointer-events: none;
}
.barText {
font: 0.7em sans-serif;
font-weight: 500;
color: lightgray;
visibility: hidden;
}
a {
color: dimgray;
}
.source {
font-size: 0.625em;
color: grey;
line-height: 80%;
}
.prepaired {
font-size: 0.625em;
color: grey;
line-height: 80%;
}
.update {
font-size: 0.625em;
color: grey;
line-height: 80%;
}
.notes {
font-size: 0.625em;
color: grey;
line-height: 80%;
}
.def {
font-size: .75em;
color: grey;
}
</style>
</head>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
<script src="https://code.jquery.com/jquery-1.9.1.js"></script>
<script src="https://code.jquery.com/ui/1.10.3/jquery-ui.js"></script>
<form>
<span style="color:dimgrey" id="title">Oil & Gas + Mining Employment: </span><select id="geo" style="width:200px;">
<option value='00'>New Mexico</option>
<option value='01'>Bernalillo</option>
<option value='03'>Catron</option>
<option value='05'>Chaves</option>
<option value='06'>Cibola</option>
<option value='07'>Colfax</option>
<option disabled value='09'>Curry</option>
<option disabled value='11'>De Baca</option>
<option value='13'>Doña Ana</option>
<option value='15'>Eddy</option>
<option value='17'>Grant</option>
<option disabled value='19'>Guadalupe</option>
<option disabled value='21'>Harding</option>
<option disabled value='23'>Hidalgo</option>
<option value='25'>Lea</option>
<option value='27'>Lincoln</option>
<option disabled value='28'>Los Alamos</option>
<option value='29'>Luna</option>
<option value='31'>Mckinley</option>
<option disabled value='33'>Mora</option>
<option value='35'>Otero</option>
<option disabled value='37'>Quay</option>
<option value='39'>Rio Arriba</option>
<option disabled value='41'>Roosevelt</option>
<option value='43'>Sandoval</option>
<option value='45'>San Juan</option>
<option value='47'>San Miguel</option>
<option value='49'>Santa Fe</option>
<option value='51'>Sierra</option>
<option disabled value='53'>Socorro</option>
<option value='55'>Taos</option>
<option disabled value='57'>Torrance</option>
<option disabled value='59'>Union</option>
<option value='61'>Valencia</option>
</select>
</form>
<div id="wrap" style="max-width:960px;">
<div id="pie-chart" style="float: left; width:510px;margin:0;"></div>
<div id="graph" style="float: left; width:450px;margin:0;"></div>
<span class="prepaired" style='color:grey;font-size:0.625em;'>Prepared by: Nathan Dobie for UNM Bureau of Business and Economic Research</span>
<br />
<span id='source' style='display:block;color:grey;font-size:0.625em;'>Source: Annual 2014 Quarterly Census of Employment and Wages, New Mexico Department of Workforce Solutions. Numbers reflect only persons covered under unemployment insurance, not all employment is covered under unemployment insurance law. Change y/y calculated from data.</span>
</div>
<script type="text/javascript">
d3.select(self.frameElement).attr("height", "480px")
var div = d3.select("body").append("div").attr("class", "toolTip");
var w = 800;
var h = 390;
var r = 130;
var ir = 78;
var textOffset = 24;
var tweenDuration = 1050;
//OBJECTS TO BE POPULATED WITH DATA LATER
var lines, valueLabels, nameLabels;
var pieData = [];
var oldPieData = [];
var filteredPieData = [];
// //D3 helper function to populate pie slice parameters from array data
// var donut = d3.layout.pie().value(function (d) {
// return d.emp;
// }).sort(null);
// ////D3 helper function to create colors from an ordinal scale
// //var color = d3.scale.category20c();
// var color = d3.scale.ordinal()
// .range(['#6baed6', '#aaa']);
// //D3 helper function to draw arcs, populates parameter "d" in path object
// var arc = d3.svg.arc()
// .startAngle(function (d) { return d.startAngle; })
// .endAngle(function (d) { return d.endAngle; })
// .innerRadius(ir)
// .outerRadius(r);
function formatMoney(num) {
return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,")
};
// var vis = d3.select("#pie-chart").append("svg:svg")
// .attr("width", w)
// .attr("height", h);
// //GROUP FOR ARCS/PATHS
// var arc_group = vis.append("svg:g")
// .attr("class", "arc")
// .attr("transform", "translate(" + (w / 2) + "," + (h / 2) + ")");
// //GROUP FOR LABELS
// var label_group = vis.append("svg:g")
// .attr("class", "label_group")
// .attr("transform", "translate(" + (w / 2) + "," + (h / 2) + ")");
// //GROUP FOR CENTER TEXT
// var center_group = vis.append("svg:g")
// .attr("class", "center_group")
// .attr("transform", "translate(" + (w / 2) + "," + (h / 2) + ")");
// //WHITE CIRCLE BEHIND LABELS
// var whiteCircle = center_group.append("svg:circle")
// .attr("fill", "white")
// .attr("r", ir);
// function type(d) {
// d.wage = +d.wage;
// d.emp = +d.emp;
// d.gdp = +d.gdp;
// return d;
// };
/*
d3.csv("sector_21.csv", type, function (error, dataI) {
d3.selectAll("#geo")
.on("change", update);
function update() {
dataI.value = dataI.emp
var I = document.getElementById('geo').selectedIndex
, select = document.getElementById('geo')[I].value;
data = dataI.filter(function (d) { return d.area == select });
oldPieData = filteredPieData;
pieData = donut(data);
var sliceProportion = 0; //size of this slice
filteredPieData = pieData.filter(filterData);
function filterData(element, index, array) {
element.emp = data[index].emp;
element.wage = data[index].wage;
element.ind = data[index].ind;
element.label = data[index].label;
sliceProportion += element.emp;
return (element.wage >= 0)
}
//DRAW ARC PATHS
paths = arc_group.selectAll("path").data(filteredPieData);
paths.enter().append("svg:path")
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.attr("fill", function (d, i) { return color(i); })
.transition()
.duration(tweenDuration)
.attrTween("d", pieTween);
paths
.transition()
.duration(tweenDuration)
.attrTween("d", pieTween);
paths.exit()
.transition()
.duration(tweenDuration)
.attrTween("d", removePieTween)
.remove();
paths.on("mousemove", function (d) {
div.style("left", d3.event.pageX + 10 + "px");
div.style("top", d3.event.pageY - 25 + "px");
div.style("display", "inline-block");
div.html(d.label + ': ' + (d.ind) + "<br>" + 'Covered Employment: ' + (formatMoney(d.emp)));
});
paths.on("mouseout", function (d) {
div.style("display", "none");
});
//DRAW TICK MARK LINES FOR LABELS
lines = label_group.selectAll("line").data(filteredPieData);
lines.enter().append("svg:line")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", -r - 3)
.attr("y2", -r - 15)
.attr("stroke", "gray")
.attr("transform", function (d) {
return "rotate(" + (d.startAngle + d.endAngle) / 2 * (180 / Math.PI) + ")";
});
lines.transition()
.duration(tweenDuration)
.attr("transform", function (d) {
return "rotate(" + (d.startAngle + d.endAngle) / 2 * (180 / Math.PI) + ")";
});
lines.exit().remove();
//DRAW LABELS WITH PERCENTAGE VALUES
valueLabels = label_group.selectAll("text.value").data(filteredPieData)
.attr("dy", function (d) {
if ((d.startAngle + d.endAngle) / 2 > Math.PI / 2 && (d.startAngle + d.endAngle) / 2 < Math.PI * 1.5) {
return 5;
} else {
return -7;
}
})
.attr("text-anchor", function (d) {
if ((d.startAngle + d.endAngle) / 2 < Math.PI) {
return "beginning";
} else {
return "end";
}
})
.text(function (d) {
var percentage = (d.value / sliceProportion) * 100;
return percentage.toFixed(1) + "%";
});
valueLabels.enter().append("svg:text")
.attr("class", "value")
.attr("transform", function (d) {
return "translate(" + Math.cos(((d.startAngle + d.endAngle - Math.PI) / 2)) * (r + textOffset) + "," + Math.sin((d.startAngle + d.endAngle - Math.PI) / 2) * (r + textOffset) + ")";
})
.attr("dy", function (d) {
if ((d.startAngle + d.endAngle) / 2 > Math.PI / 2 && (d.startAngle + d.endAngle) / 2 < Math.PI * 1.5) {
return 5;
} else {
return -7;
}
})
.attr("text-anchor", function (d) {
if ((d.startAngle + d.endAngle) / 2 < Math.PI) {
return "beginning";
} else {
return "end";
}
}).text(function (d) {
var percentage = (d.value / sliceProportion) * 100;
return percentage.toFixed(1) + "%";
});
valueLabels.transition().duration(tweenDuration).attrTween("transform", textTween);
valueLabels.exit().remove();
//DRAW LABELS WITH ENTITY NAMES
nameLabels = label_group.selectAll("text.units").data(filteredPieData)
.attr("dy", function (d) {
if ((d.startAngle + d.endAngle) / 2 > Math.PI / 2 && (d.startAngle + d.endAngle) / 2 < Math.PI * 1.5) {
return 17;
} else {
return 5;
}
})
.attr("text-anchor", function (d) {
if ((d.startAngle + d.endAngle) / 2 < Math.PI) {
return "beginning";
} else {
return "end";
}
}).text(function (d) {
return d.ind;
});
nameLabels.enter().append("svg:text")
.attr("class", "units")
.attr("transform", function (d) {
return "translate(" + Math.cos(((d.startAngle + d.endAngle - Math.PI) / 2)) * (r + textOffset) + "," + Math.sin((d.startAngle + d.endAngle - Math.PI) / 2) * (r + textOffset) + ")";
})
.attr("dy", function (d) {
if ((d.startAngle + d.endAngle) / 2 > Math.PI / 2 && (d.startAngle + d.endAngle) / 2 < Math.PI * 1.5) {
return 17;
} else {
return 5;
}
})
.attr("text-anchor", function (d) {
if ((d.startAngle + d.endAngle) / 2 < Math.PI) {
return "beginning";
} else {
return "end";
}
}).text(function (d) {
return d.ind;
});
nameLabels.transition().duration(tweenDuration).attrTween("transform", textTween);
nameLabels.exit().remove();
updateBars(); //Notice the bar update gets called here and later, this actually works to create the neat into animation that only occurs on page load, easier than righting a custom delay function
}
// Interpolate the arcs in data space.
function pieTween(d, i) {
var s0;
var e0;
if (oldPieData[i]) {
s0 = oldPieData[i].startAngle;
e0 = oldPieData[i].endAngle;
} else if (!(oldPieData[i]) && oldPieData[i - 1]) {
s0 = oldPieData[i - 1].endAngle;
e0 = oldPieData[i - 1].endAngle;
} else if (!(oldPieData[i - 1]) && oldPieData.length > 0) {
s0 = oldPieData[oldPieData.length - 1].endAngle;
e0 = oldPieData[oldPieData.length - 1].endAngle;
} else {
s0 = 0;
e0 = 0;
}
var i = d3.interpolate({ startAngle: s0, endAngle: e0 }, { startAngle: d.startAngle, endAngle: d.endAngle });
return function (t) {
var b = i(t);
return arc(b);
};
}
function removePieTween(d, i) {
s0 = 2 * Math.PI;
e0 = 2 * Math.PI;
var i = d3.interpolate({ startAngle: d.startAngle, endAngle: d.endAngle }, { startAngle: s0, endAngle: e0 });
return function (t) {
var b = i(t);
return arc(b);
};
}
function textTween(d, i) {
var a;
if (oldPieData[i]) {
a = (oldPieData[i].startAngle + oldPieData[i].endAngle - Math.PI) / 2;
} else if (!(oldPieData[i]) && oldPieData[i - 1]) {
a = (oldPieData[i - 1].startAngle + oldPieData[i - 1].endAngle - Math.PI) / 2;
} else if (!(oldPieData[i - 1]) && oldPieData.length > 0) {
a = (oldPieData[oldPieData.length - 1].startAngle + oldPieData[oldPieData.length - 1].endAngle - Math.PI) / 2;
} else {
a = 0;
}
var b = (d.startAngle + d.endAngle - Math.PI) / 2;
var fn = d3.interpolateNumber(a, b);
return function (t) {
var val = fn(t);
return "translate(" + Math.cos(val) * (r + textOffset) + "," + Math.sin(val) * (r + textOffset) + ")";
};
}
update();//calls the above code to run,
});
*/
//Start the bar graph code
var margin = { top: 10, right: 40, bottom: 20, left: 50 },
width = 900 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .2);
var y = d3.scale.linear()
.range([height, 0]);
var y2 = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5);
var y2Axis = d3.svg.axis()
.scale(y2)
.orient("right")
.ticks(4, "%");
var svg = d3.select("#graph").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var line = d3.svg.line()
.x(function (d) { return x2(d.Feature); })
.y(function (d) { return y2(d.FeatureCountAvg); });
function roundToOne(num) {//Ensures numbers always display the given number of digits after the decimal
return +(Math.round(num + "e+1") + "e-1");
};
updateBars();
function updateBars() {
d3.csv("bar.csv", type, function (error, data) {
if (error) throw error;
var I = document.getElementById('geo').selectedIndex
, select = document.getElementById('geo')[I].value
//, data = data.filter(function (d) { return d.area == select })
, min = (d3.min(data, function (d) { return d.FeatureCount; }) - (.2 * d3.mean(data, function (d) { return d.FeatureCount; })))
, min2 = 1.2 * d3.min(data, function (d) { return d.FeatureCountAvg; })
, max2 = 1.2 * d3.max(data, function (d) { return d.FeatureCountAvg; })
if (min < 1) {//sets axis automatically, but uses zero if the min would be close to 0 anyway
var min = 0
};
if (min2 >= 0) {
var min2 = -.01
};
if (max2 <= 0) {
var max2 = .01
};
//Tufte would be proud of this, if he ever saw it. The economic data
//of y/y change in employment can take on negitive values, thus we need to
// ensure the axis always include negitive and positive values
x.domain(data.map(function (d) { return d.Feature; }));
y2.domain([min2, max2]);
y.domain([min, ((d3.max(data, function (d) { return d.FeatureCount / 100; }) * 100) + (.11 * d3.mean(data, function (d) { return d.FeatureCount; })))]);
//Sets the domain based on the average, making the max .11 times the average greater than the max value, for asthetics only essentually
if (svg.selectAll(".x.axis")[0].length < 1) {//This stuff only occurs the first time the code is looped through, not subsiquent calls
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append('g')
.attr("class", "y axisL")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 4)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Annual Avg. Employment");
svg.append("g")
.attr("class", "y axisR")
.call(y2Axis)
.attr("transform", "translate(" + width + ",0)")
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -11)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Y/Y Change");
svg.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function (d) { return x(d.Feature); })
.attr("width", x.rangeBand())
.attr("y", function (d) { return y(10e-6); })
.attr("height", function (d) { return y(10e-6) })
.transition()
.duration(1050)
.attr("y", function (d) { return y(d.FeatureCount); })
.attr("height", function (d) { return height - y(d.FeatureCount); });
svg.selectAll(".chartLine")
.data(data)
.enter()
.append("rect")
.attr("class", "chartLine")
.attr("transform", "translate(" + 20 + ",0)")
.attr("width", x.rangeBand() - 40)
.attr("x", function (d) { return x(d.Feature); })
.attr("y", function (d) { return d3.min(data, function (d) { return y2(d.FeatureCountAvg); }) })
.attr("height", function (d) { return 1 })
.transition()
.duration(1050)
.attr("y", function (d) { return y2(d.FeatureCountAvg); });
svg.selectAll(".chartLineText")
.data(data)
.enter()
.append('text')
.attr("class", "chartLineText")
.style("color", "#000")
.style("text-anchor", "middle")
.attr("transform", "translate(" + 30 + ",-2)")
.attr("x", function (d) { return x(d.Feature); })
.attr("y", function (d) { return d3.min(data, function (d) { return y2(d.FeatureCountAvg); }) })
.text(function (d) { return roundToOne(d.FeatureCountAvg * 100) + "%" })
.transition()
.duration(1050)
.attr("y", function (d) { return y2(d.FeatureCountAvg); });
svg.selectAll(".barText")
.data(data)
.enter()
.append("text")
.attr("class", "barText")
.attr("text-anchor", "middle")
.attr("transform", "translate(" + x.rangeBand() / 2 + ",-3)")
.attr("x", function (d) { return x(d.Feature); })
.text(function (d) { return formatMoney(+d.FeatureCount) })
.attr("y", function (d) { return y(d.FeatureCount) - 10; });
} else {//This is what is called when the code is called to update by a change in the drop downs.
svg.selectAll(".y.axisL").transition().duration(1500).call(yAxis);
svg.selectAll(".y.axisR").transition().duration(1500).call(y2Axis);
svg.selectAll(".chartLineText").
data(data)
.transition()
.duration(1500)
.attr("y", function (d) { return y2(d.FeatureCountAvg); })
.text(function (d) { return roundToOne(d.FeatureCountAvg * 100) + "%" });
svg.selectAll(".chartLine")
.data(data)
.transition()
.duration(1500)
.attr("y", function (d) { return y2(d.FeatureCountAvg); });
svg.selectAll(".bar")
.data(data)
.transition()
.duration(1500)
.delay(function (d) { return 100 + (d.Feature - 2010) * 100 })
//Notice this delay function, it allows the eye to see all of the changes in the graph without needing to run the animation again,
//but it's suttle enough it's hard to notice, but it really makes a difference, your mind is fast but only holds so many things in
//short term memory, this delay helps avoid mental hurt, but also by limiting the years to 4 makes it much better
// Also the green bars draw in the focus while changing more than the small red lines, this is to mentally make it easier again. (I read to many theory books on this stuff, sorry those that are color blind)
.attr("y", function (d) { return y(d.FeatureCount); })
.attr("height", function (d) { return height - y(d.FeatureCount); });
svg.selectAll(".barText")
.data(data)
.transition()
.duration(500)
.attr("y", function (d) { return y(d.FeatureCount); })
.text(function (d) { return formatMoney(+d.FeatureCount) });
};
d3.selectAll(".bar")
.on("mouseover", function (d) {
d3.selectAll(".barText").transition()
.duration(500)
.delay(80)
//This makes the text lables mostly prevent overlap, it's not perfect, but solves more cases than it breaks, and the values are generally always visable
.attr("y", function (d) { if (Math.abs(y2(d.FeatureCountAvg) - y(d.FeatureCount)) < 10) { if (y(d.FeatureCount) - 10 - (y2(d.FeatureCountAvg)) >= 2) { return y(d.FeatureCount) - 10 } else { return y(d.FeatureCount) + 15 } } else { return y(d.FeatureCount) } })
.style("visibility", "visible");
})
.on("mouseout", function (d) {
d3.selectAll(".barText").transition()
.duration(500)
.delay(150)
.attr("y", function (d) { return y(d.FeatureCount) - 10; })
.style("visibility", "hidden");
})
});
function type(d) {
// d.Feature = d.Feature;
d.FeatureCountAvg = +d.FeatureCountAvg / 100;
d.FeatureCount = +d.FeatureCount;
return d;
}
};
</script>
</body>
</html>
Modified http://code.jquery.com/jquery-1.9.1.js to a secure url
Modified http://code.jquery.com/ui/1.10.3/jquery-ui.js to a secure url
https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js
https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js
https://d3js.org/d3.v3.min.js
https://code.jquery.com/jquery-1.9.1.js
https://code.jquery.com/ui/1.10.3/jquery-ui.js