Demonstrates the general update pattern (exit, update, enter) with d3.transition
and d3.dispatch
. Plus, it's a good example of data binding using nested d3 selections: first we select
a top level element (svg), do a selectAll
on svg groups for each gen type, then do another selectAll
on svg circles for each gen type's year & yield.
The tricky part to figure out here (to me at least) was reselecting each "gen" type's svg group after performing the general update pattern on them. If they are not reselected, then the selection will not contain any new svg groups and no additional circles will be rendered for them!
Forked from syntagmatic's block: Barley Punchcard
With cues taken from Mike Bostock's blocks:
xxxxxxxxxx
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3 Update Pattern –Punchcard Example</title>
<style type="text/css">
body {
font-family: sans-serif;
}
svg {
border:1px solid #d0d0d0;
}
text {
font-size: 16px;
fill: #888;
}
path, line {
stroke: #888;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
// register events to emit & listen for via d3 dispatch
var dispatch = d3.dispatch("load", "statechange");
// the first gen type of our dataset we want the chart to load with
var firstSiteName = "Morris";
// unique years of our dataset to be used by our xScale domain
// computed after data loads
var years = [];
// load our data! When done call our dispatch events with corresponding data
d3.csv('barleyfull.csv', function(err, data) {
if (err) { throw(error) }
data.forEach(function(d) {
d.yield = +d.yield;
d.year = +d.year;
});
// unique years in the data set, note data is already sorted chronologically
years = data.reduce(function(acc, cur) {
if (acc.indexOf(cur.year) === -1) {
acc.push(cur.year);
}
return acc;
}, []);
// max yield for the entire dataset, 75.5
// console.log(d3.max(data, function(d) { return d.yield; }));
var nested = d3.nest()
.key(function(d) { return d.site; })
.key(function(d) { return d.gen; })
.entries(data);
// construct a new d3 map, not as in geographic map, but more like a "hash"
var map = d3.map(nested, function(d) { return d.key; });
// call our dispatch events with `this` context, and corresponding data
dispatch.call("load", this, map);
dispatch.call("statechange", this, map.get(firstSiteName));
});
// register a listener for "load" and create a dropdown / select elem
dispatch.on("load.menu", function(map) {
// create select dropdown with listener to call "statechange"
var select = d3.select("body")
.append("div")
.append("select")
.on("change", function() {
var site = this.value;
dispatch.call(
"statechange",
this,
map.get(site)
);
});
// append options to select dropdown
select.selectAll("option")
.data(map.keys().sort())
.enter().append("option")
.attr("value", function(d) { return d; })
.text(function(d) { return d; });
// set the current dropdown option to value of last statechange
dispatch.on("statechange.menu", function(site) {
select.property("value", site.key);
});
});
// set up our punchcard chart after our data loads
dispatch.on("load.chart", function(map) {
// layout properties
var margin = { top: 20, right: 30, bottom: 30, left: 120 };
var width = 800 - margin.left - margin.right;
var height = 600 - margin.top - margin.bottom;
// scales for axises & circles
var yScale = d3.scalePoint().padding(0.5); // ordinal scale for gen type / category
var xScale = d3.scalePoint().padding(0.3); // ordinal scale for years
var radius = d3.scaleSqrt(); // circle size would be too large if we used raw values, so we compute their square root
var color = d3.scaleOrdinal(d3.schemeCategory20b); // colors used for differentiating "gen" type
// set up yScale with domain of unique gen values
yScale
.range([0, height])
.round(true);
// domain for our x scale is min - 1 & max years of the data set
xScale
.range([0, width])
.domain(years);
// domain of circle radius is from 0 to max d.yield
radius
.range([0, 15])
.domain([0, 76]);
// d3.v4 method of setting up axises: axisLeft, axisBottom, etc.
var yAxis = d3.axisLeft()
.scale(yScale);
var xAxis = d3.axisBottom()
.tickFormat(function(d) { return d; })
.scale(xScale);
// create an svg element to hold our chart parts
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] + ')')
// append svg groups for the axises, then call their corresponding axis function
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// register a callback to be invoked which updates the chart when "statechange" occurs
dispatch.on("statechange.chart", function(site) {
// our transition, will occur over 750 milliseconds
var t = svg.transition().duration(750);
// update our yScale & transition the yAxis, note the xAxis doesn't change
yScale.domain(site.values.map(function(d) { return d.key; }).sort());
yAxis.scale(yScale);
t.select("g.y.axis").call(yAxis);
// bind our new piece of data to our svg element
// could also do `svg.data([site.values]);`
svg.datum(site.values);
// tell d3 we want svg groups for each of our gen categories
// NOTE: the use of 2 accessor functions: the first binds the data, the second sets the data-join's key
var gens = svg.selectAll("g.site")
.data(
function(d) { return d; },
function(d) { return d.key; }
);
// get rid of the old ones we don't need when doing an update
gens.exit().remove();
// update existing ones left over
gens.attr("class", "site")
.transition(t)
.attr("transform", function(d) {
return "translate(0," + yScale(d.key) + ")"
});
// create new ones if our updated dataset has more then the previous
gens.enter().append("g")
.attr("class", "site")
.transition(t)
.attr("transform", function(d) {
return "translate(0," + yScale(d.key) + ")"
});
// reselect the gen groups, so that we get any new ones that were made
// our previous selection would not contain them
gens = svg.selectAll("g.site");
// tell d3 we want some circles!
// NOTE: the use of 2 accessor functions: the first binds the data, the second sets the data join's key
var circles = gens.selectAll("circle")
.data(
function(d) { return d.values; },
function(d) { return d.year; }
);
// get rid of ones we don't need anymore, fade them out
circles.exit()
.transition(t)
.attr('r', 0)
.style("fill", "rgba(255,255,255,0)")
.remove();
// update existing circles, transition size & fill
circles
.attr("cy", 0)
.attr("cx", function(d) { return xScale(d.year); })
.transition(t)
.attr("r", function(d) { return radius(d.yield); })
.attr("fill", function(d) { return color(d.gen); });
// make new circles
circles.enter().append("circle")
.attr("cy", 0)
.attr("cx", function(d) { return xScale(d.year); })
.transition(t)
.attr("r", function(d) { return radius(d.yield); })
.attr("fill", function(d) { return color(d.gen); });
}); // end dispatch statechange.chart
}); // end dispatch load.chart
</script>
</body>
https://d3js.org/d3.v4.min.js