Population pyramid built against live data from the Statistics Iceland API. This experiments with ES6 features so a cutting edge browser is required for viewing.
Last active
March 28, 2020 11:57
-
-
Save borgar/b952bb581923c9993d68 to your computer and use it in GitHub Desktop.
Population pyramid
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
license: mit | |
height: 500 |
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> | |
<meta charset='utf-8'> | |
<style> | |
svg { | |
font: 11px sans-serif; | |
} | |
.axis { | |
shape-rendering: crispEdges; | |
} | |
.axis .domain { | |
fill: none; | |
stroke: #666; | |
stroke-width: 1px; | |
} | |
.axis line { | |
stroke: #000; | |
} | |
rect.male { | |
fill: #3388EE; | |
} | |
rect.female { | |
fill: #EEAACC; | |
} | |
.handle { | |
stroke: #000; | |
fill: #ccc; | |
} | |
</style> | |
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
<script src="px_client.js"></script> | |
<script> | |
var dim_handlers = { | |
'Ár': { | |
'parser': d => +d | |
, 'label': 'year' | |
, 'select': d => true | |
} | |
, 'Aldur': { | |
'parser': d => /^\d+/.test( d ) ? parseInt( d, 10 ) : 0 | |
, 'label': 'age' | |
, 'select': d => !/^\s*Alls\s*$/i.test( d ) | |
} | |
, 'Kyn': { | |
'parser': d => d === 'Karlar' ? 'male' : 'female' | |
, 'label': 'sex' | |
, 'select': d => !/^\s*Alls\s*$/i.test( d ) | |
} | |
}; | |
load_px_data( '//px.hagstofa.is/pxis/api/v1/is/Ibuar/mannfjoldi/1_yfirlit/Yfirlit_mannfjolda/MAN00101.px' | |
, dim_handlers | |
, function ( err, data ) { | |
var margin = { top: 10, right: 10, bottom: 10, left: 10 } | |
, width = 960 - margin.left - margin.right | |
, height = 500 - margin.top - margin.bottom | |
, gutter = 30 | |
, pyramid_h = height - 105 | |
, dom_age = d3.extent( data, d => d.age ) | |
, dom_year = d3.extent( data, d => d.year ) | |
, dom_value = d3.extent( data, d => d.value ) | |
, formatter = d3.format( ',d' ) | |
, barheight = ( pyramid_h / ( dom_age[1] - dom_age[0] ) ) - 0.5 | |
, cx = width / 2 | |
; | |
var svg = d3.select( 'body' ).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 svg_text_m = svg.append( 'text' ) | |
.attr( 'transform', `translate(${cx-250},10)` ) | |
.style( 'font', '15px sans-serif' ) | |
.attr( 'text-anchor', 'start' ); | |
var svg_text_f = svg.append( 'text' ) | |
.attr( 'transform', `translate(${cx+250},10)` ) | |
.style( 'font', '15px sans-serif' ) | |
.attr( 'text-anchor', 'end' ); | |
var svg_text_t = svg.append( 'text' ) | |
.attr( 'transform', `translate(${cx},${pyramid_h+55})` ) | |
.style( 'font', '15px sans-serif' ) | |
.attr( 'text-anchor', 'middle' ); | |
function uptext ( root, lines ) { | |
var lines = this.selectAll( 'tspan' ).data( this.datum() ); | |
lines.text( d => d ); | |
lines.exit().remove(); | |
lines.enter().append( 'tspan' ) | |
.attr( 'x', 0 ) | |
.attr( 'dy', (d,i) => ( i * 1.2 ) + 'em' ) | |
.text( d => d ); | |
return this; | |
} | |
// year axis | |
var s_year = d3.scale.linear() | |
.domain( dom_year ) | |
.range([ 0, 400 ]) | |
.clamp( true ); | |
var ax_year = d3.svg.axis() | |
.scale( s_year ) | |
.orient( 'bottom' ) | |
.ticks( 8 ) | |
.tickFormat( String ); | |
var svg_axis_year = svg.append( 'g' ) | |
.attr( 'class', 'axis year' ) | |
.attr( 'transform', `translate(${cx-200},${pyramid_h+85})` ) | |
.call( ax_year ); | |
// age axis | |
var s_age = d3.scale.linear() | |
.domain( dom_age.concat().reverse() ) | |
.range([ 0, pyramid_h ]); | |
var ax_age_l = d3.svg.axis() | |
.scale( s_age ) | |
.orient( 'left' ) | |
.tickFormat( d => s_age( d ) ? '' + d : '' ); | |
var ax_age_svg = svg.append( 'g' ) | |
.attr( 'class', 'axis age' ) | |
.attr( 'transform', `translate(${cx+gutter/2+10},0)` ) | |
.call( ax_age_l ); | |
ax_age_svg.append( 'text' ) | |
.attr( 'dy', '.32em' ) | |
.text('Age'); | |
ax_age_svg.selectAll( 'text' ) | |
.attr( 'x', -gutter/2-10 ) | |
.style( 'text-anchor', 'middle' ); | |
var ax_age_r = d3.svg.axis() | |
.scale( s_age ) | |
.orient( 'right' ) | |
.tickFormat( d => '' ); | |
svg.append( 'g' ) | |
.attr( 'class', 'axis age' ) | |
.attr( 'transform', `translate(${cx-gutter/2-10},0)` ) | |
.call( ax_age_r ); | |
// population axen | |
var s_value = d3.scale.linear() | |
.domain( dom_value ) | |
.range([ 0, 250 ]); | |
// male population axis | |
var s_male = d3.scale.linear() | |
.domain( dom_value.reverse() ) | |
.range([ 0, 250 ]); | |
var ax_male = d3.svg.axis() | |
.scale( s_male ) | |
.orient( 'bottom' ) | |
.ticks( 5 ) | |
.tickFormat( formatter ); | |
svg.append( 'g' ) | |
.attr( 'class', 'axis population male' ) | |
.attr( 'transform', | |
`translate(${cx-250-gutter},${pyramid_h+5})` ) | |
.call( ax_male ); | |
// female population axis | |
var ax_female = d3.svg.axis() | |
.scale( s_value ) | |
.orient( 'bottom' ) | |
.ticks( 5 ) | |
.tickFormat( formatter ); | |
svg.append( 'g' ) | |
.attr( 'class', 'axis population female' ) | |
.attr( 'transform', `translate(${cx+gutter},${pyramid_h+5})` ) | |
.call( ax_female ); | |
// population bars | |
var bars = svg.append( 'g' ) | |
.attr( 'class', 'pyramid population' ); | |
function update ( current_year, animate ) { | |
var _data = data.filter( d => d.year === current_year ) | |
, isMale = d => d.sex === 'male' | |
, x_pos = d => { | |
return isMale( d ) | |
? cx - gutter - s_value( d.value ) | |
: cx + gutter; | |
} | |
, total = d3.sum( _data, d => d.value ) | |
, m_total = d3.sum( _data, d => isMale( d ) ? d.value : 0 ) | |
; | |
svg_text_m.datum([ 'Males', formatter( m_total ) ]) | |
.call( uptext ); | |
svg_text_f.datum([ 'Females', formatter( total - m_total ) ]) | |
.call( uptext ); | |
svg_text_t.datum([ `Total population of Iceland in | |
${current_year} was ${formatter(total)}` ]) | |
.call( uptext ); | |
var bar = bars.selectAll( '.bar' ).data( _data ); | |
bar.transition().duration( animate ? 450 : 0 ) | |
.attr( 'width', d => s_value( d.value ) ) | |
.attr( 'x', x_pos ); | |
bar.exit().remove(); | |
bar.enter().append( 'rect' ) | |
.attr( 'class', d => 'bar ' + d.sex ) | |
.attr( 'height', barheight ) | |
.attr( 'width', d => s_value( d.value ) ) | |
.attr( 'x', x_pos ) | |
.attr( 'y', d => s_age( d.age ) - barheight/2 ); | |
} | |
// current year | |
var curr_point = d3.svg.symbol() | |
.type( 'triangle-down' ) | |
.size( 100 ); | |
var brush = d3.svg.brush() | |
.x( s_year ) | |
.extent([ dom_year[ 1 ], dom_year[ 1 ] ]) | |
.on( 'brush', onbrush ); | |
var slider = svg_axis_year.append( 'g' ) | |
.attr( 'class', 'slider' ) | |
.call( brush ); | |
slider.selectAll( '.extent,.resize' ).remove(); | |
slider.select( '.background' ) | |
.attr( 'y', -20 ) | |
.attr( 'height', 40 ); | |
var handle = slider.append( 'path' ) | |
.attr( 'class', 'handle' ) | |
.attr( 'd', curr_point ); | |
function onbrush () { | |
var value = brush.extent()[ 0 ] | |
, animate = true; | |
if ( d3.event && d3.event.sourceEvent ) { | |
if ( d3.event.sourceEvent.type === 'mousemove' ) { | |
animate = false; | |
} | |
value = d3.round( s_year.invert( d3.mouse( this )[ 0 ] ) ); | |
brush.extent([ value, value ]); | |
} | |
handle.attr( 'transform', `translate(${s_year(value)},-10)` ); | |
update( value, animate ); | |
} | |
onbrush(); | |
}); | |
</script> |
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
function convert_px_data ( dataset, handlers, callback ) { | |
var dims = dataset.dimension; | |
// id's and sizes are listed in reverse order | |
// for some bizarre reason | |
dims.size = dims.size.reverse(); | |
dims.id = dims.id.reverse(); | |
// allow quick index-to-dimension-value lookups | |
dims.id.forEach( id => { | |
var dim = dims[ id ] | |
, cat = dim.category | |
, handler = handlers[ id ] | |
; | |
dim.lookup = {}; | |
for ( var key in cat.index ) { | |
var index = cat.index[ key ] | |
, label = cat.label[ key ] | |
.trim().replace( /\s+/, ' ' ) | |
; | |
if ( handler && handler.parser ) { | |
label = handler.parser( label ); | |
} | |
dim.lookup[ index ] = label; | |
} | |
}); | |
// convert data stream to 'fact' objects | |
var data = dataset.value.map( (d, i) => { | |
var fact = { 'value': d } | |
, mul = 1 | |
; | |
dims.size.forEach( (size, dim_idx) => { | |
var cat_idx = ~~( i / mul ) % size | |
, dim = dims[ dims.id[ dim_idx ] ] | |
, handler = handlers[ dims.id[ dim_idx ] ] | |
, label = ( handler && handler.label ) || dim.label | |
; | |
fact[ label ] = dim.lookup[ cat_idx ]; | |
mul = mul * size; | |
}); | |
return fact; | |
}); | |
callback( null, data ); | |
} | |
function load_px_data ( data_url, handlers, callback ) { | |
d3.json( data_url, dimspec => { | |
var query = dimspec.variables.map(function ( dim ) { | |
var handler = handlers[ dim.code ] | |
, sel = []; | |
dim.valueTexts.map(function ( val, idx ) { | |
if ( !handler || !handler.select || handler.select( val ) ) { | |
sel.push( dim.values[ idx ] ) | |
} | |
}); | |
return { 'code': dim.code | |
, 'selection': { 'filter': 'item' | |
, 'values': sel } }; | |
}); | |
var post_data = { 'query': query | |
, 'response': { 'format': 'json-stat' } }; | |
d3.xhr( data_url ) | |
.response(function ( req ) { | |
return JSON.parse( req.responseText ); | |
}) | |
.post( JSON.stringify( post_data ), function ( e, d ) { | |
convert_px_data( d.dataset, handlers, callback ); | |
}); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment