Skip to content

Instantly share code, notes, and snippets.

@louh
Last active August 29, 2015 13:57
Show Gist options
  • Save louh/9731914 to your computer and use it in GitHub Desktop.
Save louh/9731914 to your computer and use it in GitHub Desktop.
World Bank Indicator Test 01
[
{
"indicator_name": "Pregnant women receiving antenatal care",
"project_name": "AF: Strengthening Health Activities",
"project_id": "P112446",
"theme": "Health",
"status": "Active",
"units": "number",
"baseline": {
"date": "2009-07-01",
"value": "70353"
},
"target": {
"date": "2013-09-30",
"value": "119127"
},
"measurements": [
{
"date": "2009-10-10",
"value": "79146"
},
{
"date": "2011-12-15",
"value": "131911"
}
],
"description": [
"Jujubes sesame snaps chupa chups muffin sweet cake. Liquorice gingerbread cake dragée. Danish cake chupa chups topping pudding lemon drops. Lollipop tootsie roll soufflé cotton candy. Powder caramels soufflé danish oat cake. Liquorice jujubes biscuit biscuit candy oat cake sugar plum soufflé. Soufflé chocolate caramels. Oat cake bear claw gingerbread ice cream marshmallow macaroon. Biscuit bonbon cotton candy tootsie roll toffee cheesecake.",
"Cheesecake tiramisu cake bonbon. Lollipop applicake candy donut sugar plum wafer icing wafer cookie. Pudding lemon drops dessert jelly muffin tootsie roll croissant marshmallow jujubes. Carrot cake danish sugar plum tootsie roll muffin cupcake biscuit tart. Jelly beans carrot cake oat cake. Croissant cupcake tart toffee topping liquorice pastry."
]
},
{
"indicator_name": "Number of consultations (per person)",
"project_name": "AF: Strengthening Health Activities",
"project_id": "P112446",
"theme": "Health",
"status": "Active",
"units": "per person per year",
"baseline": {
"date": "2007-12-31",
"value": "0.9"
},
"target": {
"date": "2013-09-30",
"value": "1.4"
},
"measurements": [
{
"date": "2011-05-22",
"value": "1.2"
},
{
"date": "2011-12-15",
"value": "1.23"
},
{
"date": "2012-10-15",
"value": "1.5"
},
{
"date": "2013-06-05",
"value": "1.55"
}
],
"description": [
"Dragée tootsie roll chocolate cake chocolate gingerbread. Marzipan tart cake lemon drops marshmallow topping. Cupcake jelly gummi bears cheesecake icing sweet roll jelly topping tart. Pudding cheesecake oat cake biscuit toffee bonbon apple pie candy canes bonbon. Croissant powder chocolate bar chocolate cake. Sweet dessert topping liquorice marshmallow pie sugar plum.",
"Dessert pie muffin lemon drops toffee donut. Icing cheesecake chocolate bar cupcake oat cake gummi bears gummi bears bonbon. Icing jujubes fruitcake croissant. Donut cheesecake croissant. Sugar plum candy canes tiramisu biscuit jujubes. Dragée gummies wafer bonbon carrot cake bonbon. Lollipop liquorice halvah halvah liquorice. Candy cake donut gummi bears cotton candy gummies fruitcake gummies."
]
},
{
"indicator_name": "Number of female scholarship recipients in Teacher Training Colleges",
"project_name": "Second Education Quality Improvement Program",
"project_id": "P106259",
"theme": "Education",
"status": "Active",
"units": "number",
"baseline": {
"date": "2008-01-31",
"value": "0"
},
"target": {
"date": "2014-08-15",
"value": "5000"
},
"measurements": [
{
"date": "2010-10-01",
"value": "584"
},
{
"date": "2011-04-30",
"value": "1500"
},
{
"date": "2012-06-27",
"value": "3328"
},
{
"date": "2013-09-06",
"value": "6234"
}
],
"description": [
"Pudding liquorice donut wafer cake macaroon sugar plum. Chupa chups biscuit bear claw topping pastry muffin jujubes brownie pastry. Sesame snaps cotton candy jujubes. Candy halvah marshmallow dragée muffin lollipop apple pie sweet tootsie roll. Jujubes tiramisu macaroon pastry sesame snaps pie marshmallow powder. Tart liquorice gummies tart chocolate bar sugar plum candy canes soufflé donut. Sesame snaps chocolate cake applicake candy canes tootsie roll chocolate cookie bear claw.",
"Pudding marshmallow gummi bears. Dessert powder muffin donut gummi bears. Biscuit chocolate powder cotton candy icing powder. Powder jujubes chupa chups chocolate cake sweet roll bear claw marshmallow biscuit. Halvah gummies cookie apple pie gingerbread bear claw tiramisu powder. Chocolate bar candy canes cake cupcake tiramisu. Applicake jelly powder. Topping sugar plum dragée tiramisu oat cake wafer lollipop."
]
},
{
"indicator_name": "Percentage of active female clients",
"project_name": "Microfinance for Poverty Reduction",
"project_id": "P091264",
"theme": "Livelihood",
"status": "Active",
"units": "percentage",
"baseline": {
"date": "2006-12-06",
"value": "70"
},
"target": {
"date": "2010-06-30",
"value": "65"
},
"measurements": [
{
"date": "2007-12-31",
"value": "68"
},
{
"date": "2010-06-30",
"value": "60"
}
],
"description": [
"Cake pudding sweet pie fruitcake marzipan chocolate cake. Cookie tootsie roll apple pie jelly beans oat cake jelly beans soufflé jelly-o candy. Sweet roll gummies pie lollipop halvah gummies candy. Marshmallow caramels chocolate cake donut pastry caramels brownie dragée. Toffee halvah topping donut. Cake toffee donut jelly beans. Wafer pudding carrot cake jujubes jelly beans. Danish cheesecake jelly-o liquorice.",
"Gingerbread cookie croissant. Pastry ice cream chocolate cake. Liquorice soufflé macaroon. Wafer applicake lemon drops gingerbread halvah tootsie roll apple pie. Liquorice jujubes gingerbread pastry gingerbread. Muffin chocolate bar ice cream jujubes oat cake tiramisu lollipop chocolate cake. Pastry cake tootsie roll jelly beans cotton candy cake cupcake brownie. Croissant croissant sweet jelly beans. Gummi bears dessert gummies powder carrot cake halvah halvah."
]
},
{
"indicator_name": "Number of female owned Enterprise groups",
"project_name": "AF: Rural Enterprise Devt Program",
"project_id": "P110407",
"theme": "Livelihood",
"status": "Active",
"units": "number",
"baseline": {
"date": "2010-06-14",
"value": "0"
},
"target": {
"date": "2015-01-01",
"value": "2275"
},
"measurements": [
{
"date": "2011-05-22",
"value": "40"
},
{
"date": "2012-05-22",
"value": "60"
},
{
"date": "2013-12-04",
"value": "440"
}
],
"description": [
"Chocolate bar chocolate bar halvah cheesecake dessert sugar plum powder oat cake marshmallow. Sugar plum sugar plum chupa chups jujubes pie. Ice cream marshmallow powder apple pie gummies cheesecake topping marzipan chupa chups. Sweet roll powder sweet roll. Oat cake jelly-o muffin marshmallow apple pie tart muffin cotton candy dessert. Marshmallow muffin tiramisu dessert marzipan pudding dragée powder tiramisu. Powder gummi bears lollipop powder dragée jujubes bear claw chocolate bar gummies.",
"Tart jelly beans topping applicake. Jujubes marzipan pudding. Soufflé bonbon marshmallow. Sweet roll icing liquorice pie fruitcake cheesecake. Cookie pudding pudding jelly-o jujubes lemon drops biscuit. Brownie sesame snaps sweet. Wafer brownie lollipop cotton candy soufflé chupa chups pie danish apple pie. Biscuit pastry carrot cake jelly-o tiramisu. Tart gummies marshmallow jujubes. Tart danish pie cotton candy caramels soufflé."
]
}
]
<!DOCTYPE html>
<head>
<title>World Bank Indicator Viz Test 01</title>
<link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600' rel='stylesheet' type='text/css'>
<style type='text/css'>
body {
font-family: 'Open Sans', 'Helvetica Neue', Arial, sans-serif;
font-size: 0.8em;
line-height: 1.6em;
}
.container {
width: 695px;
}
.row {
margin-bottom: 20px;
}
.column {
display: inline-block;
}
.column:not(:first-of-type) {
margin-left: 10px;
}
h2 {
font-size: 22px;
}
label {
display: block;
}
select {
margin-left: 0;
}
svg {
/* force Firefox to display properly - why is this bug occurring? */
overflow: visible;
/* TEMP: HARD CODE */
height: 350px;
}
.axis path, .axis line {
fill: none;
stroke: #aaa;
shape-rendering: crispEdges;
}
.axis line {
stroke: #e6e7e8;
}
.tick:nth-of-type(2n+1) line {
stroke: #aaa;
}
.tick text {
font-size: 0.9em;
fill: #999;
}
g.indicator.active {
cursor: pointer;
}
.background {
fill: white;
fill-opacity: 0;
}
g.indicator.active .background {
fill: whitesmoke;
fill-opacity: 1;
}
.indicator {
cursor: pointer;
}
text.label {
/* cursor: pointer; */
text-align: right;
}
text.value {
}
.label-today {
font-size: 0.7em;
font-weight: bold;
fill: #888;
display: none;
}
svg .circle-measured, svg .circle-baseline {
stroke-width: 1;
stroke: white;
}
svg circle:hover {
cursor: pointer;
}
svg circle.highlight {
}
svg .circle-targeted {
fill: white;
fill-opacity: 0;
stroke-width: 1.5;
stroke-opacity: 1;
stroke: white;
}
svg .circle-subtargets {
fill-opacity: 0;
stroke-width: 1;
stroke-opacity: 0.3;
stroke: white;
display: none;
}
svg .circle-latest {
fill-opacity: 0.5;
display: none;
}
svg .circle-subtargets.show,
svg .circle-latest.show,
g.indicator.active circle {
display: block;
}
svg .today line {
stroke: red;
stroke-width: 1;
stroke-opacity: 0.2;
stroke-dasharray: 5, 4;
}
rect.hoverable {
cursor: pointer;
fill: white;
fill-opacity: 0;
}
.indicator-line {
stroke: #e1e1e1;
stroke-width: 1;
}
.tooltip {
border-radius: 3px;
box-shadow: 0 0 5px rgba(0,0,0,0.2);
padding: 5px;
color: #333;
background-color: white;
text-align: center;
}
.tooltip .date {
font-size: 0.85em;
color: gray;
}
</style>
</head>
<body>
<div class='container'>
<div id='filters'>
<form id='filter'>
<p><strong>Filter by</strong></p>
<div class='row'>
<div class='column'>
<label for='filter-sectors'>Sector</label>
<select id='filter-sectors'>
<option value='' default>(all sectors)</option>
</select>
</div>
<div class='column'>
<label for='filter-themes'>Theme</label>
<select id='filter-themes'>
<option value='' default>(all themes)</option>
</select>
</div>
<div class='column'>
<label for='filter-projects'>Project</label>
<select id='filter-projects'>
<option value='' default>(all projects)</option>
</select>
</div>
<div class='column'>
<button type='reset' id='filter-reset'>Clear filters</button>
</div>
</div>
</form>
</div>
<div id='viz'></div>
<div id='legend'>
<h4>Legend (placeholder)</h4>
<img src='legend.png' width='650' height='80' style='margin-top: -10px;'>
</div>
<div id='info'>
<h2 id='info-title'>More information</h2>
<div id='info-description'>
<p>Select an indicator to learn more.</p>
</div>
</div>
</div>
<script src='//cdnjs.cloudflare.com/ajax/libs/jquery/1.11.0/jquery.js'></script>
<script src='//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js'></script>
<script src='//cdnjs.cloudflare.com/ajax/libs/d3/3.4.2/d3.min.js'></script>
<script src='tooltip.js'></script>
<script src='//cdnjs.cloudflare.com/ajax/libs/moment.js/2.6.0/moment.min.js'></script>
<script>
// Get data first
var dataEndpoint = 'data.json',
data =[];
$.get(dataEndpoint, function (response) {
data = response;
// Parse dates and values
for (var k = 0; k < data.length; k++) {
data[k].baseline.date = new Date(Date.parse(data[k].baseline.date));
data[k].baseline.dateRounded = _roundDateToHalfYear(data[k].baseline.date);
data[k].baseline.value = parseFloat(data[k].baseline.value);
data[k].target.date = new Date(Date.parse(data[k].target.date));
data[k].target.dateRounded = _roundDateToHalfYear(data[k].target.date);
data[k].target.value = parseFloat(data[k].target.value);
for (var j = 0; j < data[k].measurements.length; j++) {
var item = data[k].measurements[j];
item.date = new Date(Date.parse(item.date));
item.dateRounded = _roundDateToHalfYear(item.date);
item.value = parseFloat(item.value);
}
}
// Get dropdown options
var themes = _getArrayOfDataType(data, 'theme');
var projects = _getArrayOfDataType(data, 'project_name');
var sectors = _getArrayOfDataType(data, 'sector');
// Populate dropdowns
for (var j = 0; j < projects.length; j++) {
if (_.isUndefined(projects[j]) === true) continue;
$('#filter-projects').append('<option value="' + projects[j] + '">' + projects[j] + '</option>')
}
for (var j = 0; j < themes.length; j++) {
if (_.isUndefined(themes[j]) === true) continue;
$('#filter-themes').append('<option value="' + themes[j] + '">' + themes[j] + '</option>')
}
for (var j = 0; j < sectors.length; j++) {
if (_.isUndefined(sectors[j]) === true) continue;
$('#filter-sectors').append('<option value="' + sectors[j] + '">' + sectors[j] + '</option>')
}
// Create SVG viz
createViz(data)
});
$(document).ready(function () {
$('#filter select').on('change', function (e) {
var project = $('#filter-projects').val();
var theme = $('#filter-themes').val();
var sector = $('#filter-sectors').val();
var filter = {};
if (project) filter['project_name'] = project;
if (theme) filter['theme'] = theme;
if (sector) filter['sector'] = sector;
var newData = _.where(data, filter);
// If nothing is filtered, get the whole thing
//if (newData.length < 1) newData = data;
if (!project && !theme && !sector) newData = data;
createViz(newData);
})
/*
$('#filter-projects').on('change', function (e) {
// Resets the other filters (hacky)
document.getElementById('filter-themes').selectedIndex = 0;
document.getElementById('filter-sectors').selectedIndex = 0;
// Get the filter
var value = $('#filter-projects').val();
var newData = _.where(data, { project_name: value });
// If nothing is filtered, get the whole thing
if (newData.length < 1) newData = data;
createViz(newData);
});
$('#filter-themes').on('change', function (e) {
// Resets the other filters (hacky)
document.getElementById('filter-projects').selectedIndex = 0;
document.getElementById('filter-sectors').selectedIndex = 0;
// Get the filter
var value = $('#filter-themes').val();
var newData = _.where(data, { theme: value });
// If nothing is filtered, get the whole thing
if (newData.length < 1) newData = data;
createViz(newData);
});
$('#filter-sectors').on('change', function (e) {
// Resets the other filters (hacky)
document.getElementById('filter-projects').selectedIndex = 0;
document.getElementById('filter-themes').selectedIndex = 0;
// Get the filter
var value = $('#filter-sectors').val();
var newData = _.where(data, { sector: value });
// If nothing is filtered, get the whole thing
if (newData.length < 1) newData = data;
createViz(newData);
});
*/
$('#filter-reset').on('click', function (e) {
// Resets viz
createViz(data);
})
});
function _getArrayOfDataType (data, field) {
// Uses underscore.js chaining.
// (1) Selects all the values of a given `field`
// (2) Sorts alphabetically - TODO: Guarantee alphabetic sort
// (3) and filters by unique values.
return _.chain(data)
.pluck(field)
.sort()
.uniq(true)
.value();
}
// Massive SVG creation function
function createViz (data) {
// If there is a previous SVG, remove it
d3.select('#viz').select('svg').remove();
// SVG overall dimensions
var margin = {top: 25, right: 25, bottom: 0, left: 0},
// width = 960;
width = 695; // ARTF.af two-column layout maximum width
var rowSpacing = 60;
var viewportWidth = width - margin.right - margin.left,
labelWidth = viewportWidth / 4,
chartWidth = viewportWidth * (3/4);
var mockupColor = '#27a9e1',
baselineCircleColor = '#c34040',
measuredCircleColor = mockupColor,
targetCircleColor = baselineCircleColor;
// color scale
// TODO: Set colors based on category, not per line
// c(i) where i = index -> returns a color
var c = d3.scale.category10();
// Set up SVG display area
var svg = d3.select('#viz').append('svg')
.attr('width', width)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// If we don't have a data, display a notice
if (data.length < 1) {
// Labels for each indicator
svg.append('text')
.attr('x', viewportWidth / 2)
.attr('y', rowSpacing)
.attr('text-anchor', 'middle')
.text('No indicators match your filter criteria.')
.style('fill', 'gray');
return false;
}
var startDate = _getStartDate(data),
endDate = _getEndDate(data);
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// * * * AXIS FORMATTING * * * //
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// Set up the domain and range for the X-axis scale, based on the data we have
var xScale = d3.time.scale()
.domain([startDate, endDate])
.nice(d3.time.year)
.range([0, chartWidth]);
// Set up the X-axis itself
// Tick formatting labels only the year, with empty strings for months
var xAxis = d3.svg.axis()
.scale(xScale)
.ticks(d3.time.month, 6)
.tickFormat(d3.time.format.multi([
['%Y', function (d) { return (d.getMonth() === 0) ? true : false }],
['', function (d) { return true }]
]))
.orient('top');
// Further axis formatting on the SVG element
var xAxisG = svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(' + labelWidth + ',' + 0 + ')')
.call(xAxis)
.selectAll('text')
.attr('y', -14)
.attr('x', -2)
.style('text-anchor', 'start');
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// * * * DATA FORMATTING * * * //
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// Data and formatting specific to each data set
for (var j = 0; j < data.length; j++) {
var indicator = data[j]
// Position and radius of circle
var yPos = (j + 1) * rowSpacing - 5; // The -5 is a vertical centering hack
// Create group for each indicator
var g = svg.append('g').attr('class', 'indicator');
var gBg = g.append('g').append('rect')
.classed('background', true)
.attr('x', 0)
.attr('y', yPos - (rowSpacing / 2))
.attr('width', width)
.attr('height', rowSpacing);
// Add horizontal line
var gLine = g.append('line')
.attr('class', 'indicator-line')
.attr('x1', labelWidth)
.attr('x2', viewportWidth)
.attr('y1', yPos)
.attr('y2', yPos);
// Special rect shape for interaction hover area
var gHoverArea = g.append('rect')
.classed('hoverable', true)
.attr('x', 0)
.attr('y', yPos - (rowSpacing / 2))
.attr('width', viewportWidth)
.attr('height', rowSpacing)
.on('mouseover.indicator', _indicatorMouseover)
.on('mouseout.indicator', _indicatorMouseout)
.on('click.indicator', _clickToDiveDeep);
// Baseline data group
var gBaselineCircle = g.append('g').attr('class', 'indicator-baseline');
var baselineCircle = gBaselineCircle.selectAll('circle')
.data([indicator.baseline])
.enter()
.append('circle')
.attr('class', 'circle-baseline')
.style('fill', baselineCircleColor)
.call(d3.helper.tooltip()
.attr({ class: 'tooltip' })
.text(function (d, i) { return '<strong>' + d.value + ' units</strong><br><span class="date">' + moment(d.date).format('MMMM D, YYYY') + '</span>'; })
)
.on('mouseover', function (d, i) { d3.select(this).classed('highlight', true); })
.on('mouseout', function (d, i) { d3.select(this).classed('highlight', false); })
.on('mouseover.indicator', _indicatorMouseover)
.on('mouseout.indicator', _indicatorMouseout)
.on('click.indicator', _clickToDiveDeep);
// Subtarget group
var gSubtargets = g.append('g').attr('class', 'indicator-subtargets');
var subtargetCircles = gSubtargets.selectAll('circle')
.data(_makeSubtargets(indicator))
.enter()
.append('circle')
.classed('circle-subtargets', true)
.style('stroke', targetCircleColor)
.on('mouseover.indicator', _indicatorMouseover)
.on('mouseout.indicator', _indicatorMouseout)
.on('click.indicator', _clickToDiveDeep);
// Measured data group
var gValues = g.append('g').attr('class', 'indicator-measured');
var circles = gValues.selectAll('circle')
.data(indicator.measurements)
.enter()
.append('circle')
.attr('class', 'circle-measured')
.style('fill', measuredCircleColor)
.call(d3.helper.tooltip()
.attr({ class: 'tooltip' })
.text(function (d, i) { return '<strong>' + d.value + ' units</strong><br><span class="date">' + moment(d.date).format('MMMM D, YYYY') + '</span>'; })
)
.on('mouseover', function (d, i) { d3.select(this).classed('highlight', true); })
.on('mouseout', function (d, i) { d3.select(this).classed('highlight', false); })
.on('mouseover.indicator', _indicatorMouseover)
.on('mouseout.indicator', _indicatorMouseout)
.on('click.indicator', _clickToDiveDeep);
// Latest measured data group
var gLatestCircle = g.append('g').attr('class', 'indicator-latest');
var latestCircle = gLatestCircle.selectAll('circle')
.data([indicator.measurements[indicator.measurements.length - 1]])
.enter()
.append('circle')
.attr('class', 'circle-latest')
.style('fill', measuredCircleColor)
.call(d3.helper.tooltip()
.attr({ class: 'tooltip' })
.text(function (d, i) { return '<strong>' + d.value + ' units</strong><br><span class="date">' + moment(d.date).format('MMMM D, YYYY') + '</span>'; })
)
.on('mouseover', function (d, i) { d3.select(this).classed('highlight', true); })
.on('mouseout', function (d, i) { d3.select(this).classed('highlight', false); })
.on('mouseover.indicator', _indicatorMouseover)
.on('mouseout.indicator', _indicatorMouseout)
.on('click.indicator', _clickToDiveDeep);
// Target group
var gTarget = g.append('g').attr('class', 'indicator-targeted');
var targetCircle = gTarget.selectAll('circle')
.data([indicator.target])
.enter()
.append('circle')
.classed('circle-targeted', true)
.style('stroke', targetCircleColor)
.call(d3.helper.tooltip()
.attr({ class: 'tooltip' })
.text(function (d, i) { return '<strong>' + d.value + ' units</strong><br><span class="date">' + moment(d.date).format('MMMM D, YYYY') + '</span>'; })
)
.on('mouseover', function (d, i) { d3.select(this).classed('highlight', true); })
.on('mouseout', function (d, i) { d3.select(this).classed('highlight', false); })
.on('mouseover.indicator', _indicatorMouseover)
.on('mouseout.indicator', _indicatorMouseout)
.on('click.indicator', _clickToDiveDeep);
/*
var text = g.selectAll('text')
.data(indicator['articles'])
.enter()
.append('text');
*/
// Radius scale for circle
// If baseline measurement is lower than the target, it should increase on the X-axis
// Flip the range if baseline is higher than the target measurement.
if (indicator.baseline.value < indicator.target.value) {
var rScale = d3.scale.linear()
.domain([indicator.baseline.value, indicator.target.value])
.range([4, 14]);
} else {
var rScale = d3.scale.linear()
.domain([indicator.baseline.value, indicator.target.value])
.range([14, 4]);
}
// Baseline circle
baselineCircle
.attr('cx', function (d) {
return labelWidth + xScale(d.dateRounded);
})
.attr('cy', yPos)
.attr('r', function (d) { return rScale(d.value); });
// Measured circles
circles
.attr('cx', function (d, i) {
return labelWidth + xScale(d.dateRounded);
})
.attr('cy', yPos)
.attr('r', function (d) {
// If the target is decreasing, and a measurement overshoots it, the
// linear scale could result in a r-value less than 0. This would cause
// an error because a radius can't be less than 0. For sake of
// readability, we'll clamp the minimum radius to 2 pixels.
return (rScale(d.value) >= 2) ? rScale(d.value) : 2;
});
// Latest measurement circle
latestCircle
.attr('cx', function (d, i) {
return labelWidth + xScale(indicator.target.dateRounded);
})
.attr('cy', yPos)
.attr('r', function (d) {
// If the target is decreasing, and a measurement overshoots it, the
// linear scale could result in a r-value less than 0. This would cause
// an error because a radius can't be less than 0. For sake of
// readability, we'll clamp the minimum radius to 2 pixels.
return (rScale(d.value) >= 2) ? rScale(d.value) : 2;
});
// Target circle
targetCircle
.attr('cx', function (d) {
return labelWidth + xScale(d.dateRounded);
})
.attr('cy', yPos)
.attr('r', function (d) { return rScale(d.value); });
// Subtarget circles
subtargetCircles
.attr('cx', function (d) {
return labelWidth + xScale(d.date);
})
.attr('cy', yPos)
.attr('r', function (d) { return rScale(d.value); });
/*
text
.attr('y', j*20+25)
.attr('x',function (d, i) { return xScale(d[0])-5; })
.attr('class','value')
.text(function (d){ return d[1]; })
.style('fill', mockupColor)
.style('display','none');
*/
// Labels for each indicator
g.append('text')
.attr('x', labelWidth - 30)
.attr('y', (j + 1) * rowSpacing)
.attr('text-anchor', 'end')
.text(indicator['indicator_name'])
.style('fill', mockupColor)
.on('mouseover', _indicatorMouseover)
.on('mouseout', _indicatorMouseout)
.on('click', _clickToDiveDeep);
};
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// * * * TODAY INDICATOR * * * //
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
var gToday = svg.append('g').attr('class', 'today');
var todayYStart = -30,
todayYEnd = 320,
todayYHeight = todayYEnd - todayYStart
// Create the today line
var today = new Date(),
todayXPos = labelWidth + xScale(_roundDateToHalfYear(today)),
todayLine = gToday.selectAll('line')
.data([today])
.enter()
.append('line')
.attr('x1', todayXPos)
.attr('x2', todayXPos)
.attr('y1', todayYStart)
.attr('y2', todayYEnd); // TODO: Don't hardcode the end point
// Special rect shape for interaction hover area
var todayHoverRect = gToday.append('rect')
.classed('hoverable', true)
.attr('x', todayXPos - 5)
.attr('y', todayYStart)
.attr('width', '10')
.attr('height', todayYHeight)
.on('mouseover.today', todayMouseover)
.on('mouseout.today', todayMouseout);
// Add a label for the indicator that appears on hover
var todayLabel = gToday.append('text')
.attr('x', todayXPos)
.attr('y', todayYEnd)
.attr('text-anchor', 'middle')
.text('Now')
.classed('label-today', true);
$('circle').on('hover', function (e) {
console.log('test')
})
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// * * * FUNCTIONS * * * //
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
function _indicatorMouseover (p) {
var g = d3.select(this).node().parentNode.parentNode;
d3.select(g).selectAll('.circle-subtargets').classed('show', true);
d3.select(g).selectAll('.circle-latest').classed('show', true);
}
function _indicatorMouseout (p) {
var g = d3.select(this).node().parentNode.parentNode;
d3.select(g).selectAll('.circle-subtargets').classed('show', false);
d3.select(g).selectAll('.circle-latest').classed('show', false);
}
function todayMouseover (p) {
var g = d3.select(this).node().parentNode;
d3.select(g).select('text').style('display', 'block');
}
function todayMouseout (p) {
var g = d3.select(this).node().parentNode;
d3.select(g).select('text').style('display', 'none');
}
function _clickToDiveDeep () {
var g = d3.select(this).node().parentNode.parentNode;
var gAll = d3.select(g).node().parentNode;
//if ($(this).hasClas)
// Deselect all indicators
d3.select(gAll).selectAll('.active').classed('active', false);
d3.select(gAll).selectAll('.circle-subtargets').classed('show', false);
d3.select(gAll).selectAll('.circle-latest').classed('show', false);
// Clear info box
$('#info-title').text('');
$('#info-description').empty();
// Just show the one clicked
d3.select(g).classed('active', true);
d3.select(g).selectAll('.circle-subtargets').classed('show', true);
d3.select(g).selectAll('.circle-latest').classed('show', true);
// Display the infos below
var title = $(this).closest('.indicator').find('text').text();
$('#info-title').text(title);
var indicator = _.findWhere(data, { indicator_name: title });
$('#info-description').append('<p><strong>Project:</strong> ' + indicator.project_name + '</p>');
$('#info-description').append('<p><strong>Theme:</strong> ' + indicator.theme + '</p>');
$('#info-description').append('<p><strong>Baseline measurement:</strong> ' + indicator.baseline.value + ' ' + indicator.units + '</p>');
$('#info-description').append('<p><strong>Target goal:</strong> ' + indicator.target.value + ' ' + indicator.units + '</p>');
for (var i = 0; i < indicator.description.length; i++) {
var paragraph = indicator.description[i];
$('#info-description').append('<p>' + paragraph + '</p>');
}
$('#info-description').append('<p><a href="#">View the raw data behind this indicator</a></p>');
}
}
// UTILITY FUNCTIONS
// Gets the earliest start date for the visualization, based on earliest baseline date.
function _getStartDate (data) {
var date;
for (var i = 0; i < data.length; i++) {
var test = data[i].baseline.dateRounded;
if (!date || test.getTime() < date.getTime()) {
date = test;
}
}
return date;
}
// Gets the latest end date for the visualization, based on latest target date.
function _getEndDate (data) {
var date;
// Assumes that earliest start date for visualization is a baseline date.
for (var i = 0; i < data.length; i++) {
var test = data[i].target.dateRounded;
if (!date || test.getTime() > date.getTime()) {
date = test;
}
}
return date;
}
// Round a given Date object to the nearest 6 months, using D3.
function _roundDateToHalfYear (date) {
// TODO: Verify that this is returning optimal rounding
// TODO: What happens if a rounded date is the same as another measurement?
var lowerRange = d3.time.month.offset(date, -3);
var upperRange = d3.time.month.offset(date, 3);
return d3.time.month.range(lowerRange, upperRange, 6)[0];
}
// Returns an object of subtarget dates and values between baseline and target
function _makeSubtargets (indicator) {
var baseline = indicator.baseline;
var target = indicator.target;
// Get all the dates between baseline (inclusive) and target (exclusive)
var interval = d3.time.month.range(baseline.dateRounded, target.dateRounded, 6);
var subtargets = [],
divisor = interval.length;
// Significant digits
//Function: getSigFigFromNum( num ), provides the significant digits of a number.
//@num must be a number (base 10) that is a string. example "01"
var getSigFigFromNum = function( num ){
if( isNaN( +num ) ){
throw new Error( "getSigFigFromNum(): num (" + num + ") is not a number." );
}
// We need to get rid of the leading zeros for the numbers.
num = num.toString();
num = num.replace( /^0+/, '');
// re is a RegExp to get the numbers from first non-zero to last non-zero
var re = /[^0](\d*[^0])?/;
return ( /\./.test( num ) )? num.length - 1 : (num.match( re ) || [''])[0].length;
};
var baselineSigfig = getSigFigFromNum(baseline.value);
var targetSigfig = getSigFigFromNum(target.value);
var sigfig = Math.max(baselineSigfig, targetSigfig);
// don't use sig 0
if (sigfig < 1) sigfig = 1;
// Start making a new subtarget array.
// Note: we start counting at 1 because position 0 of the interval array
// is the baseline date, which we don't need again.
for (var i = 1; i < interval.length; i++) {
if (target.value > baseline.value) {
// If target is increasing
var difference = target.value - baseline.value;
var delta = (difference / interval.length) * i;
var subvalue = baseline.value + delta;
} else {
// If target is decreasing, flip the calculations
var difference = baseline.value - target.value;
var delta = (difference / interval.length) * i;
var subvalue = baseline.value - delta;
}
subtargets.push({
date: interval[i],
value: parseFloat(subvalue.toPrecision(sigfig))
});
}
return subtargets;
}
</script>
</body>
</html>
d3.helper = {};
d3.helper.tooltip = function(){
var tooltipDiv;
var bodyNode = d3.select('body').node();
var attrs = {};
var text = '';
var styles = {};
function tooltip (selection) {
selection.on('mouseover.tooltip', function(pD, pI){
var name, value;
// Clean up lost tooltips
d3.select('body').selectAll('div.tooltip').remove();
// Append tooltip
tooltipDiv = d3.select('body').append('div');
tooltipDiv.attr(attrs);
tooltipDiv.style(styles);
var absoluteMousePos = d3.mouse(bodyNode);
var leftPos = (absoluteMousePos[0] - 50) + 'px';
var topPos = (absoluteMousePos[1] - 45) + 'px';
tooltipDiv.style({
left: leftPos,
top: topPos,
position: 'absolute',
'z-index': 1001,
width: '200px'
});
// Add text using the accessor function, Crop text arbitrarily
tooltipDiv.style('width', function(d, i){ return (text(pD, pI).length > 80) ? '300px' : null; })
.html(function(d, i){return text(pD, pI);});
})
.on('mousemove.tooltip', function(pD, pI){
// Move tooltip
var absoluteMousePos = d3.mouse(bodyNode);
var leftPos = (absoluteMousePos[0] - 50) + 'px';
var topPos = (absoluteMousePos[1] - 45) + 'px';
tooltipDiv.style({
left: leftPos,
top: topPos
});
// Keep updating the text, it could change according to position
tooltipDiv.html(function(d, i){ return text(pD, pI); });
})
.on('mouseout.tooltip', function(pD, pI){
// Remove tooltip
tooltipDiv.remove();
});
}
tooltip.attr = function (_x) {
if (!arguments.length) return attrs;
attrs = _x;
return this;
};
tooltip.style = function (_x) {
if (!arguments.length) return styles;
styles = _x;
return this;
};
tooltip.text = function (_x) {
if (!arguments.length) return text;
text = d3.functor(_x);
return this;
};
return tooltip;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment