Permalink
Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
630 lines (517 sloc) 17.8 KB
{% javascript d3-queue.js %}
<style>
input[type=range] {
background-color: #fff;
width: 200px;
height:20px;
}
input[type="range"]::-webkit-slider-thumb {
background-color: #fff;
opacity: 0.5;
width: 10px;
height: 26px;
}
#circle circle {
fill: none;
pointer-events: all;
}
.group path {
fill-opacity: .5;
}
.group text{
fill:#aaa;
font-weight: normal;
font-size: 9pt;
cursor: pointer;
}
.group text.highlighted {
fill: #2171B5;
font-size: 9pt;
font-weight: bold;
}
.group text:hover, .group text.selected {
fill: #000;
font-size: 10pt;
font-weight: bold;
}
path.chord {
stroke-width: 0.1;
}
.chord.source {
stroke-width: 1;
stroke-opacity:.5;
cursor:pointer;
}
#circle:hover path.fade {
display: none;
}
#binnenwanderung svg {
font-size: 9pt;
font-family: "Titillium Web",sans-serif;
}
div.binnenwanderung_tooltip {
position: absolute;
text-align: center;
padding: 0px;
margin: 10px;
font: 10pt "Titillium Web",sans-serif;
background: #FEFEFE;
border: 1px solid #333;
border-radius: 5px;
}
</style>
<div style="width: 25%; float: left">
<!--<a href="javascript:void(0)" id="umzugsmatrix_showall" class="btn btn-simple" role="button">Saldo</a>-->
<a href="javascript:void(0)" id="umzugsmatrix_received" class="btn btn-simple active" role="button">Zuzüge</a>
<a href="javascript:void(0)" id="umzugsmatrix_sent" class="btn btn-simple" role="button">Wegzüge</a>
</div>
<div style="width: 75%; float: left">
<span style="float: left;">2004 </span> <input id="slider" type="range" min="2004" max="2013" value="2013" style="width: 75%; float: left" step="1" /> <span style="float: left;"> 2013</span>
<div class="clearfix">&nbsp;</div>
<div class="text-center" style="width: 75%">
Ausgewähltes Jahr: <span id="range">2013</span>
</div>
<script type="text/javascript">
onload = function() {
var $ = function(id) { return document.getElementById(id); };
$('slider').oninput = function() { $('range').innerHTML = this.value; };
$('slider').oninput();
};
</script>
</div>
<div id="binnenwanderung" style="width: 80%; margin: 0 auto;"></div>
<script>
//
// Concept taken from http://stackoverflow.com/a/21923560
//
// Define parameters and tools
var chord_margin = {top: 10, left: 10, bottom: 10, right: 10},
chord_width = parseInt(d3.select('#binnenwanderung').style('width')),
chord_width = chord_width - chord_margin.left - chord_margin.right,
chord_height = chord_width,
outerRadius = Math.min(chord_width, chord_height) / 2 - 130,
innerRadius = outerRadius - 18;
//create number formatting functions
var formatPercent = d3.format("%");
var numberWithCommas = d3.format("0,f");
//create the arc path data generator for the groups
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
//create the chord path data generator for the chords
var path = d3.svg.chord()
.radius(innerRadius);
/**
* define the default chord layout parameters within a function that returns a new layout object;
* that way, you can create multiple chord layouts that are the same except for the data.
*
* @return {Datentyp} default d3 chord layout
*/
function getDefaultLayout() {
return d3.layout.chord()
.padding(0.03)
.sortSubgroups(d3.descending)
.sortChords(d3.ascending);
}
var last_layout; //store layout between updates
var neighborhoods; //store neighbourhood data outside data-reading function
var neighborhoodColors = {'Mitte': '#e41a1c','Nordost':'#377eb8','Ost':'#4daf4a','Südost':'#984ea3','Süd':'#ff7f00','Südwest':'#B2B200','West':'#a65628','Alt-West':'#f781bf','Nordwest':'#999','Nord':'#000'};
// Create tooltip div
var binnenwanderung_tooltip = d3.select("body").append("div")
.attr("class", "binnenwanderung_tooltip")
.style("opacity", 0);
/*** Initialize the visualization ***/
var g = d3.select("#binnenwanderung").append("svg")
.attr("width", chord_width)
.attr("height", chord_height)
.append("g")
.attr("id", "circle")
.attr("transform",
"translate(" + chord_width / 2 + "," + chord_height / 2 + ")");
//the entire graphic will be drawn within this <g> element,
//so all coordinates will be relative to the center of the circle
g.append("circle")
.attr("r", outerRadius);
//this circle is set in CSS to be transparent but to respond to mouse events
//It will ensure that the <g> responds to all mouse events within
//the area, even after chords are faded out.
/*** Read in the neighbourhoods data and update with initial data matrix ***/
var indexByName = {},
nameByIndex = {},
matrix = [],
neighborhoods = [],
dataMatrix = [],
outcomingMatrix = [],
incomingMatrix = [],
saldoMatrix = [],
colorByIndex = [],
mode = 'received',
year = '2013';
/**
* Prepare the Data
*
* @param {} year Jahre
* @param {} callback
*/
function prepareData(year, callback)
{
d3.text("{% asset_path umzugsmatrix/stadtteile.csv %}", function(imports){
neighborhoods = d3.csv.parse(imports);
d3.text("/assets/umzugsmatrix/ortsteildaten" + year + ".csv", function(imports) {
var csv_values = d3.csv.parseRows(imports);
// read data
for (var i = 1; i < csv_values.length; i ++){
dataMatrix[i-1] = [];
var name = csv_values[i][0];
nameByIndex[i-1] = name;
indexByName[name] = i-1;
for (var j = 1 ; j < csv_values[i].length; j ++)
{
// "ausgepunktete" and empty vars are 0
if (!csv_values[i][j] || csv_values[i][j] == ".")
{
csv_values[i][j] = 0;
}
var value = csv_values[i][j].toString().replace(',','');
dataMatrix[i-1][j-1] = parseFloat(value);
}
}
// find out colors
for (var j = 0; j < neighborhoods.length; j++)
{
//var found = false;
for (var i = 0; i < dataMatrix.length; i++)
{
if (neighborhoods[j].country == nameByIndex[i])
{
colorByIndex[i] = neighborhoodColors[neighborhoods[j].continent];
};
};
};
//transposing matrix for remittances received and received totals calculation
for (var i = 0; i < dataMatrix.length; i++){
outcomingMatrix[i] = [];
for (var j = 0; j < dataMatrix[i].length; j++){
outcomingMatrix[i][j] = dataMatrix[j][i];
};
};
mode == 'sent' ? callback(null, dataMatrix) : callback(null, outcomingMatrix);
});
});
}
d3_queue.queue()
.defer(prepareData, year)
.await(updateChords);
/**
* Create OR update a chord layout from a data matrix
*
* @param {} error Beschreibung
* @param {} matrix Bechreibung
*/
function updateChords( error, matrix ) {
/* Compute chord layout. */
layout = getDefaultLayout(); //create a new layout object
layout.matrix(matrix);
/* Create/update "group" elements */
var groupG = g.selectAll("g.group")
.data(layout.groups(), function (d) {
return d.index;
//use a key function in case the
//groups are sorted differently between updates
});
groupG.exit()
.transition()
.duration(500)
.attr("opacity", 0)
.remove(); //remove after transitions are complete
var newGroups = groupG.enter().append("g")
.attr("class", "group");
//the enter selection is stored in a variable so we can
//enter the <path>, <text>, and <title> elements as well
//Create the title tooltip for the new groups
newGroups
.on("mouseover", function(p) {
binnenwanderung_tooltip.transition()
.duration(200)
.style("opacity", .9);
binnenwanderung_tooltip
// TOOLTIP CONTENT
.html(function (foo) {
return parseInt(p.value)
+ " Personen sind aus/nach "
+ nameByIndex[p.index] + " weggezogen/zugezogen";
})
// -TOOLTIP CONTENT
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(p) {
binnenwanderung_tooltip.transition()
.duration(500)
.style("opacity", 0);
});
//create the arc paths and set the constant attributes
//(those based on the group index, not on the value)
newGroups.append("path")
.attr("id", function (d) {
return "group" + d.index;
//using d.index and not i to maintain consistency
//even if groups are sorted
})
.style("fill", function (d) {
return colorByIndex[d.index];
});
//update the paths to match the layout
groupG.select("path")
.transition()
.duration(500)
.attr("opacity", 0.5) //optional, just to observe the transition
.attrTween("d", arcTween( last_layout ))
.transition().duration(100).attr("opacity", 1) //reset opacity
;
//create the group labels
newGroups.append("text")
.attr("dy", ".35em")
.attr("color", "#fff")
.text(function (d) {
return nameByIndex[d.index];
});
//position group labels to match layout
groupG.select("text")
// remove selection and highlight
.classed("selected highlighted", false)
// do transition
.transition()
.duration(500)
.attr("transform", function(d) {
d.angle = (d.startAngle + d.endAngle) / 2;
//store the midpoint angle in the data object
return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" +
" translate(" + (innerRadius + 26) + ")" +
(d.angle > Math.PI ? " rotate(180)" : " rotate(0)");
//include the rotate zero so that transforms can be interpolated
})
.attr("text-anchor", function (d) {
return d.angle > Math.PI ? "end" : "begin";
});
/* Create/update the chord paths */
var chordPaths = g.selectAll("path.chord")
.data(layout.chords(), chordKey );
//specify a key function to match chords
//between updates
//create the new chord paths
var newChords = chordPaths.enter()
.append("path")
.attr("class", "chord");
//handle exiting paths:
chordPaths.exit().transition()
.duration(500)
.attr("opacity", 0)
.remove();
//update the path shape
chordPaths.transition()
.duration(500)
.attr("opacity", 0.5) //optional, just to observe the transition
.style("fill", function (d) {
return colorByIndex[d.source.index];
})
.style("stroke", function(d) {
return colorByIndex[d.source.index];
})
.style("visibility", "hidden")
.attrTween("d", chordTween(last_layout))
.transition().duration(100).attr("opacity", 1) //reset opacity
;
// add the highlight function if a neighborhood is clicked
groupG
.on("click", function(d) {
// reset the circle: make all chords invisible and remove selected and highlighted classes from text
chordPaths
.classed("source", false)
.style("visibility", "hidden")
.on("mouseover", null)
.on("mouseout", null);
groupG
.selectAll("text")
.classed("selected highlighted", false);
// add selected class to clicked neighborhood label
d3.select(this)
.select("text")
.classed("selected", true);
// make visible all relevant chords
var sent = chordPaths
.filter(function(p)
{
// show all chords in relation to selected neighborhood (regardless of target and source)
return ((p.source.index == d.index && p.source.value != 0) || (p.target.index == d.index && p.target.value != 0));
})
.classed("source", true)
.style("visibility", "visible")
// update colors on click
.style("fill", function (p)
{
return colorByIndex[nameByIndex[d.index] == nameByIndex[p.target.index] ? p.source.index : p.target.index];
})
.style("stroke", function (p)
{
return colorByIndex[nameByIndex[d.index] == nameByIndex[p.target.index] ? p.source.index : p.target.index];
})
.on("mouseover", function(p) {
binnenwanderung_tooltip.transition()
.duration(200)
.style("opacity", .9);
binnenwanderung_tooltip
// TOOLTIP CONTENT
.html(function (foo) {
if (mode == 'sent')
{
// matrix data is accessed directly to bypass confusing d3.js source/target-handling
var toolTipContent = nameByIndex[d.index] + " nach " + (nameByIndex[d.index] == nameByIndex[p.target.index] ? nameByIndex[p.source.index] : nameByIndex[p.target.index]) + ": "
+ matrix[d.index][(nameByIndex[d.index] == nameByIndex[p.target.index] ? p.source.index : p.target.index)];
}
if (mode == 'received')
{
// matrix data is accessed directly to bypass confusing d3.js source/target-handling
var toolTipContent = (nameByIndex[d.index] == nameByIndex[p.target.index] ? nameByIndex[p.source.index] : nameByIndex[p.target.index]) + " nach " + nameByIndex[d.index] + ": "
+ matrix[d.index][(nameByIndex[d.index] == nameByIndex[p.target.index] ? p.source.index : p.target.index)];
}
return toolTipContent;
})
// -TOOLTIP CONTENT
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(p) {
binnenwanderung_tooltip.transition()
.duration(500)
.style("opacity", 0);
});
});
last_layout = layout; //save for next update
}
/**
* this function will be called once per update cycle
*
* @return {} Beschreibung
* @return {} Beschreibung
*/
function arcTween(oldLayout) {
//Create a key:value version of the old layout's groups array
//so we can easily find the matching group
//even if the group index values don't match the array index
//(because of sorting)
var oldGroups = {};
if (oldLayout) {
oldLayout.groups().forEach( function(groupData) {
oldGroups[ groupData.index ] = groupData;
});
}
return function (d, i) {
var tween;
var old = oldGroups[d.index];
if (old) { //there's a matching old group
tween = d3.interpolate(old, d);
}
else {
//create a zero-width arc object
var emptyArc = {startAngle:d.startAngle,
endAngle:d.startAngle};
tween = d3.interpolate(emptyArc, d);
}
return function (t) {
return arc( tween(t) );
};
};
}
/**
* create a key that will represent the relationship between these two groups *regardless* of which group is called 'source' and which 'target'
*
* @param {} data Beschreibung
* @return {} Beschreibung
*/
function chordKey(data) {
return (data.source.index < data.target.index) ?
data.source.index + "-" + data.target.index:
data.target.index + "-" + data.source.index;
}
/**
* this function will be called once per update cycle
*
* @param {} oldLayout Beschreibung
* @return {} Beschreibung
*/
function chordTween(oldLayout) {
//Create a key:value version of the old layout's chords array
//so we can easily find the matching chord
//(which may not have a matching index)
var oldChords = {};
if (oldLayout) {
oldLayout.chords().forEach( function(chordData) {
oldChords[ chordKey(chordData) ] = chordData;
});
}
return function (d, i) {
//this function will be called for each active chord
var tween;
var old = oldChords[ chordKey(d) ];
if (old) {
//old is not undefined, i.e.
//there is a matching old chord value
//check whether source and target have been switched:
if (d.source.index != old.source.index ){
//swap source and target to match the new data
old = {
source: old.target,
target: old.source
};
}
tween = d3.interpolate(old, d);
}
else {
//create a zero-width chord object
var emptyChord = {
source: { startAngle: d.source.startAngle,
endAngle: d.source.startAngle},
target: { startAngle: d.target.startAngle,
endAngle: d.target.startAngle}
};
tween = d3.interpolate( emptyChord, d );
}
return function (t) {
//this function calculates the intermediary shapes
return path(tween(t));
};
};
}
/* Activate the buttons and link to data sets */
d3.select("#umzugsmatrix_showall").on("click", function(){
d3.select("#umzugsmatrix_received").classed('active',false);
d3.select("#umzugsmatrix_sent").classed('active',false);
d3.select(this).classed('active',true);
updateChords(saldoMatrix);
});
d3.select("#umzugsmatrix_sent").on("click", function(){
d3.select("#umzugsmatrix_received").classed('active',false);
d3.select("#umzugsmatrix_showall").classed('active',false);
d3.select(this).classed('active',true);
mode = 'sent';
d3_queue.queue()
.defer(prepareData, year)
.await(updateChords);
});
d3.select("#umzugsmatrix_received").on("click", function(){
d3.select("#umzugsmatrix_sent").classed('active',false);
d3.select("#umzugsmatrix_showall").classed('active',false);
d3.select(this).classed('active',true);
mode = 'received';
d3_queue.queue()
.defer(prepareData, year)
.await(updateChords);
});
d3.select('#slider').on('change', function change() {
var year = this.value;
d3_queue.queue()
.defer(prepareData, year)
.await(updateChords);
});
</script>