Last active
August 29, 2015 13:57
-
-
Save louh/9731914 to your computer and use it in GitHub Desktop.
World Bank Indicator Test 01
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ | |
{ | |
"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é." | |
] | |
} | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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