empires.js | |
---|---|
SummaryThis D3 visualization is by Edward Lee, with a few minor modifications by BFL The (slightly modified) visualization can be viewed here Notes by BFL The basic structure of this example could probably serve as a nice template for many D3 visualizations, having the following basic components: | |
| |
| |
Variables | |
The current width and height of the visualization; this will change if the window size changes | var wid, hei; |
How long the transitions should last, in milliseconds | var transitionDuration = 800; |
The data that is loaded from the file | var data = []; |
| var scales = {}; |
| var totals = {}; |
| var vis; |
Set some "comfortable" padding around the visualization | var padding = {
top : 40,
right : 140,
bottom : 30,
left : 30
} |
The initial barHeight for the bars; this will be changed based on the actual data and window height | var barHeight = 10; |
If the percentage of total population is not defined for a given empire, then use this default value | var defaultPopPercent = .08; |
The | |
| var controls = {
display : "aligned",
height : "fixed"
} |
| |
Initialization | |
This is the jQuery callback called when the DOM is fully loaded | $(document).ready(function() { |
This will create the
| |
Note that D3 can handle this particular case if you just use "svg" instead of "svg:svg" | vis = d3.select("body")
.append("svg:svg")
.attr("class", "vis"); |
Set the size of the div based on the current size of the window; this method is called every time the window is resized, too | setVisSize(); |
Load the DataInternally, the | d3.csv("Empires_Data.csv", function(d) {
data = d; |
Parse the dataEach line of the csv file results in an element in the | for ( i = 0; i < data.length; i++) {
d = data[i]; |
Do a little bit of cleanup/converting | for (prop in d) {
if (!isNaN(d[prop])) {
d[prop] = parseFloat(d[prop]);
} else if (d[prop] == "Yes") {
d[prop] = true;
} else if (d[prop] == "No") {
d[prop] = false;
}
}
} |
Do sort and calculate totalsThis is not in | |
Sort the data by start year for the empire, using the d3 | data.sort(function(a, b) {
return d3.ascending(a.Start, b.Start);
}); |
Calculate the total area of all empires, using the d3 | totals.area = d3.sum(data, function(d) {
return d.Land_area_million_km2;
}); |
Calculate the total population of all empires, using the d3 | totals.population = d3.sum(data, function(d) {
return d.Estimated_Population;
}); |
Calculate the sum of the population percentage over of all empires, using the
d3 | totals.popPercent = d3.sum(data, function(d) {
if (isNaN(d.Percent_World_Population))
return defaultPopPercent;
else
return d.Percent_World_Population;
}); |
Process data for scales, etc. (see notes below) | processData(); |
Some more initialization (these methods only called once; see notes below) | drawStarting();
addInteractionEvents(); |
Set the initial options for controls.display and controls.height, after a wait of 500ms (for a bit of a dramatic effect :) ) | setTimeout(function() { |
Set the initial "controls.display" option to "timeline", and do not redraw | setControl($("#controls #layoutControls #layout-timeline"), "display", "timeline", false); |
Set the initial "controls.height" option to "area", and redraw | setControl($("#controls #heightControls #height-area"), "height", "area", true);
}, 500);
});
}); |
| |
Set the Width and Height of the Visualization | |
These calculations are based on the current window size | function setVisSize() {
wid = $(window).width() - 4;
hei = $(window).height() - 25 - $("#controls").height();
vis.attr("width", wid).attr("height", hei); //works in firefox;
/*
The original $(".vis").attr("width", wid);//does not work in firefox
*/
$(".vis .background").attr("width", wid).attr("height", hei);
} |
Note: You need to be careful how you set the width/height attributes, because the jQuery selectors might not work correctly on Firefox. | |
| |
Hook into Window Resize Event | |
This uses jQuery's resize to get notified when the window's size is changed | $(window).resize(function() {
setVisSize();
processData();
redraw();
}); |
| |
Recalculate Scales, etc. based on Current Window Size | /************************************************************
* Process the data once it's imported
***********************************************************/ |
| function processData() { |
| barHeight = (hei - padding.top - padding.bottom) / data.length; |
Configure the | scales.years = d3.scale.linear()
.domain([d3.min(data, function(d) {return d.Start;}),
d3.max(data, function(d) {return d.End;})])
.range([padding.left, wid - padding.right]);
scales.indexes = d3.scale.linear()
.domain([0, data.length - 1])
.range([padding.top, hei - padding.bottom - barHeight]);
scales.areas = function(a) {
var percentage = a / totals.area;
var range = hei - padding.top - padding.bottom;
return range * percentage;
}
scales.popPercents = function(a) {
if (isNaN(a))
a = defaultPopPercent;
var percentage = a / totals.popPercent;
var range = hei - padding.top - padding.bottom;
return range * percentage;
} |
Determine the y location for each bar for the case where height of each bar is proportional to the area of the corresponding empire | var y_area = padding.top;
for ( i = 0; i < data.length; i++) {
d = data[i];
d.area_y = y_area;
y_area += scales.areas(d.Land_area_million_km2);
} |
Determine the y location for each bar for the case where height of each bar is proportional to the population percentage | var y_popPercent = padding.top;
for ( i = 0; i < data.length; i++) {
d = data[i];
d.popPercent_y = y_popPercent;
if (isNaN(d.Percent_World_Population))
y_popPercent += scales.popPercents(defaultPopPercent);
else
y_popPercent += scales.popPercents(d.Percent_World_Population);
}
} |
| |
Initial Render (called only once) | /************************************************************
* Initial rendering of the vis
***********************************************************/
function drawStarting() { |
The Main rect Containing the Visualization | |
| vis.append("svg:rect")
.attr("class", "background")
.attr("x", 0)
.attr("y", 0)
.attr("width", wid)
.attr("height", hei); |
Initialize the Year Ticks
|
vis.selectAll("line")
.data(scales.years.ticks(10))
.enter()
.append("svg:line")
.attr("class", "tickLine")
.attr("x1", padding.left)
.attr("x2", padding.left)
.attr("y1", padding.top)
.attr("y2", hei - padding.bottom); |
Empire Containers
| vis.selectAll("g")
.data(data)
.enter()
.append("svg:g")
.attr("class", "barGroup")
.attr("index", function(d, i) {return i;})
.attr("transform", function(d, i) {
return "translate(" + padding.left + ", " + scales.indexes(i) + ")";
}); |
Bars
| vis.selectAll("g.barGroup")
.append("svg:rect")
.attr("class", "bar")
.attr("x", 0)
.attr("y", 0)
.attr("width", function(d) {
return scales.years(d.End) - scales.years(d.Start);
})
.attr("height", barHeight); |
Empire Peak Lines
| vis.selectAll("g.barGroup")
.append("svg:line")
.attr("class", "peakLine")
.attr("x1", function(d) {
return scales.years(d.Peak) - scales.years(d.Start);
})
.attr("x2", function(d) {
return scales.years(d.Peak) - scales.years(d.Start);
})
.attr("y1", 0)
.attr("y2", barHeight); |
Bar Labels
| vis.selectAll("g.barGroup")
.append("svg:text")
.attr("class", "barLabel")
.attr("x", function(d) {
return scales.years(d.End) - scales.years(d.Start);
})
.attr("y", 0)
.attr("dx", 5)
.attr("dy", ".35em")
.style("fill", function(d) {
if (d.Contiguous === false)
return "#0ff";
})
.text(function(d) {
return d.Name;
}); |
Tick Labels
| vis.selectAll("text.rule")
.data(scales.years.ticks(10))
.enter()
.append("svg:text")
.attr("class", "rule")
.attr("x", padding.left)
.attr("y", 20)
.attr("dy", 0)
.attr("text-anchor", "middle")
.text(function(d) {
return formatYear(d);
})
.style("fill-opacity",0);
} |
| |
redraw | /************************************************************
* Redraw the vis with transition
***********************************************************/ |
Redraw everything, using a transition for each thing rendered every time | |
Things are rendered from back to front: | |
| function redraw() {
$("#infobox").hide(); |
Calculate the horizontal center of the rendering area for later use | var visCenter = (wid - padding.left - padding.right) / 2 + padding.left; |
redraw the Year TicksThe ticklines are shown either at the specified tick intervals, bunched up in the middle, or all the way to the left; the top and bottom are the height of the container minus the padding | vis.selectAll("line.tickLine")
.transition().duration(transitionDuration)
.attr("x1", function(d, i) {
if (controls.display == "timeline")
return scales.years(d);
else if (controls.display == "centered")
return visCenter;
else
return padding.left;
})
.attr("x2", function(d) {
if (controls.display == "timeline")
return scales.years(d);
else if (controls.display == "centered")
return visCenter;
else
return padding.left;
})
.attr("y1", padding.top)
.attr("y2", hei - padding.bottom); |
redraw the Empire Containers | vis.selectAll("g")
.transition().duration(transitionDuration)
.style("fill-opacity", function(d) {
if (controls.height == "population" && isNaN(d.Percent_World_Population))
return .4;
else
return 1;
})
.attr("transform", function(d, i) {
var tx, ty;
if (controls.display == "timeline")
tx = scales.years(d.Start);
else if (controls.display == "centered")
tx = visCenter - (scales.years(d.Peak) - scales.years(d.Start));
else
tx = padding.left;
if (controls.height == "area")
ty = d.area_y;
else if (controls.height == "population")
ty = d.popPercent_y;
else
ty = scales.indexes(i);
return "translate(" + tx + ", " + ty + ")";
}); |
redraw the Bars | vis.selectAll("g.barGroup rect.bar")
.transition().duration(transitionDuration)
.style("fill-opacity", function(d) {
if (controls.height == "population" && isNaN(d.Percent_World_Population))
return .25;
else
return .75;
})
.attr("height", function(d) {
if (controls.height == "area")
return scales.areas(d.Land_area_million_km2);
else if (controls.height == "population")
return scales.popPercents(d.Percent_World_Population);
else
return barHeight;
}); |
redraw the Bar Labels | var labelHeight = 0;
vis.selectAll("g.barGroup text.barLabel")
.transition().duration(transitionDuration)
.attr("y", function(d) {
if (controls.height == "area")
return scales.areas(d.Land_area_million_km2) / 2 - labelHeight;
else if (controls.height == "population")
return scales.popPercents(d.Percent_World_Population) / 2 - labelHeight;
else
return barHeight / 2 - labelHeight;
}); |
redraw the Peak Lines | vis.selectAll("g.barGroup line.peakLine")
.transition().duration(transitionDuration)
.attr("y2", function(d) {
if (controls.height == "area")
return scales.areas(d.Land_area_million_km2);
else if (controls.height == "population")
return scales.popPercents(d.Percent_World_Population);
else
return barHeight;
}); |
redraw the Tick Labels | vis.selectAll("text.rule")
.transition().duration(transitionDuration)
.attr("x", function(d) {
if (controls.display == "timeline")
return scales.years(d);
else if (controls.display == "centered")
return visCenter;
else
return padding.left;
})
.attr("y", 20)
.attr("dy", 0)
.style("fill-opacity", function(d) {
if (controls.display == "timeline") {
return 1;
}
else {
return 0;
}
});
} |
| |
Configure Interaction Events (called only once) | /************************************************************
* Add interaction events after initial drawing
***********************************************************/
function addInteractionEvents() { |
This will set up the click event on every single bar that was drawn, with the result that if the user clicks on one of the bars, the InfoBox specific to that bar will be shown | $("g.barGroup").click(function(e) {
showInfoBox(e, $(this).attr("index"));
}); |
Configure so that a click that is NOT on a bar will (ultimately) hide the InfoBox | $(".vis .background, .vis .mouseLine").click(function(e) {
showInfoBox(e, null);
}); |
Note: if this will be used on mobile devices, it is probably worth checking out hooking up to touch events rather than click events for responsiveness purposes. | } |
Show InfoBox | /************************************************************
* Display info box for data index i, at mouse
***********************************************************/ |
Show the InfoBox for a particular empire, or simply hide the InfoBox
if | function showInfoBox(e, i) {
if (i == null)
$("#infobox").hide();
else {
var d = data[i]; |
Build up the html for the InfoBox | var info = "<span class='title'>" + d.Name + "</span>";
info += "<br />" + formatYear(d.Start) + " - " + formatYear(d.End);
if (!isNaN(d.Land_area_million_km2))
info += "<br />" + " Peak (" + formatYear(d.Peak) + "): " + d.Land_area_million_km2 + " million sq km";
if (!isNaN(d.Estimated_Population))
info += "<br />" + d.Estimated_Population + " million people in " + formatYear(d.Population_Year);
else
info += "<br />" + "no population data available";
if (!isNaN(d.Percent_World_Population))
info += "<br />" + "(" + Math.round(d.Percent_World_Population * 100) + "% of world population)";
if (d.Contiguous === false)
info += "<br />" + "non-contiguous"; |
Determine where the InfoBox will be going on the screen; if the bar clicked is in the top half, use the [mouse coordinates] where the user clicked, otherwise shift back to the left and up a little big (in case the box is near the bottom of the screen) | var infoPos;
if (i <= data.length / 2)
infoPos = {
left : e.pageX,
top : e.pageY
};
else
infoPos = {
left : e.pageX - 200,
top : e.pageY - 80
}; |
| $("#infobox").html(info).css(infoPos).show();
}
} |
| |
Set Current View Options | |
This sets either controls.display or controls.height; it is called from the initialization routine and from the script in the html itself | function setControl(elem, con, val, re) { |
Remove the CSS "active" class for any sibling elements, by:
| $(elem).parents(".controlGroup").find("a").removeClass("active"); |
Make sure the passed in element has its CSS class set to "active", using the jQuery addClass() function | $(elem).addClass("active"); |
Set controls.display or controls.height (could be any property, but only "display" and "height" are passed in for "con" in this example) | controls[con] = val; |
If the caller wants us to redraw now, do so | if (re == true)
redraw();
} |
| |
This function doesn't seem to be called anywhere | function parseTransform(s) {
if (s.substr(0, 10) == "translate(") {
s = s.substring(10, s.length - 1);
s = s.split(",");
var v1 = parseFloat($.trim(s[0]));
var v2 = parseFloat($.trim(s[1]));
}
return {
val1 : v1,
val2 : v2
};
} |
Date Format Helper | |
Helper function to format dates that are BCE | function formatYear(y) {
if (y <= 0)
return y * -1 + " BCE";
else
return y;
}
|