Working through the excellent Interactive Data Visualization for the Web book and have created (a monstrosity of a) script to create an interactive bar chart that:
- Adds a new bar to the end when clicking on the svg element
- Generates a new set of 50 bars when clicking on the p element
I have added a mouseover event listener to change the color of the bars when hovering over. The problem is that bars added via 1. above are not changing color. As far as I can tell, the bars are getting selected properly, but for whatever reason, the mouseover event is never being fired for these bars:
svg.select(".bars").selectAll("rect")
.on("mouseover", function() {
d3.select(this)
.transition()
.attr("fill", "red");
})
Thanks in advance for your help, it is always greatly appreciated.
Here is the full code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Interactive Data Visualization for the Web Program-Along</title>
<style>
/* axes are made up of path, line, and text elements */
.axis path,
.axis line {
fill: none;
stroke: navy;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 11px;
/* color is CSS property, but need SVG property fill to change color */
fill: navy;
}
</style>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<p>Click on this text to update the chart with new data values.</p>
<script type="text/javascript">
var n = 50;
var domain = Math.random() * 1000;
function gen_data(n, domain) {
var d = [];
for (var i = 0; i < n; i++) {
d.push(
{ id: i, val: Math.random() * domain }
);
}
return d;
}
// define key function once for use in .data calls
var key = function(d) {
return d.id;
};
var dataset = gen_data(n, domain);
// define graphic dimensions
var w = 500, h = 250, pad = 30;
// get input domains
var ylim = d3.extent(dataset, function(d) {
return d.val;
});
// define scales
var x_scale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([0, w - pad], 0.15);
var y_scale = d3.scale.linear()
.domain([ylim[0], ylim[1] + pad]) // could have ylim[0] instead
// range must be backward [upper, lower] to accommodate svg y inversion
.range([h, 0]); // tolerance to avoid clipping points
var color_scale = d3.scale.linear()
.domain([ylim[0], ylim[1]])
.range([0, 255]);
// create graphic
var svg = d3.select("body").append("div").append("svg")
.attr("width", w)
.attr("height", h);
svg.append("g")
.attr("class", "bars")
.selectAll(".bars rect")
.data(dataset)
.enter()
.append("rect")
.attr({
x: function(d, i) {
return x_scale(i) + pad;
},
y: function(d) {
return y_scale(d.val);
},
width: x_scale.rangeBand(), // calculates width automatically
height: function(d) { return h - y_scale(d.val); },
opacity: 0.6,
fill: function(d) {
return "rgb(50, 0, " + Math.floor(color_scale(d.val)) + ")";
}
});
// add axes
var yAxis = d3.svg.axis()
.scale(y_scale) // must be passed data-to-pixel mapping (scale)
.ticks(3) // optional (d3 can assign ticks automatically)
.orient("left");
// since function, must be called
// create <g> to keep things tidy, to style via CSS, & to adjust placement
svg.append("g")
.attr({
class: "axis",
transform: "translate(" + pad + ",0)"
})
.call(yAxis);
// add event listener for clearing/adding all new values
d3.select("p")
.on("click", function() {
// generate new dataset
dataset = gen_data(n, domain);
// remove extra bars
d3.selectAll(".bars rect")
.data(dataset, function(d, i) { if (i < 50) { return d; }})
.exit()
.transition()
.attr("opacity", 0)
.remove();
// update scales
x_scale.domain(d3.range(dataset.length))
.rangeRoundBands([0, w - pad], 0.15);
ylim = d3.extent(dataset, function(d) {
return d.val;
});
y_scale.domain([ylim[0], ylim[1] + pad]);
// update bar values & colors
d3.selectAll(".bars rect")
.data(dataset)
.transition()
.duration(500)
.attr("x", function(d, i) { return x_scale(i) + pad; })
.transition() // yes, it's really this easy...feels like cheating
.delay(function(d, i) { return i * (1000 / dataset.length); }) // set dynamically
.duration(1000) // optional: control transition duration in ms
.each("start", function() {
// "start" results in immediate effect (no nesting transitions)
d3.select(this) // this to select each element (ie, rect)
.attr("fill", "magenta")
.attr("opacity", 0.2);
})
.attr({
y: function(d) { return y_scale(d.val); },
height: function(d) { return h - y_scale(d.val); }
})
.each("end", function() {
d3.selectAll(".bars rect")
.transition()
// needs delay or may interrupt previous transition
.delay(700)
.attr("fill", function(d) {
return "rgb(50, 0, " + Math.floor(color_scale(d.val)) + ")";
})
.attr("opacity", 0.6)
.transition()
.duration(100)
.attr("fill", "red")
.transition()
.duration(100)
.attr("fill", function(d) {
return "rgb(50, 0, " + Math.floor(color_scale(d.val)) + ")";
});
});
// update axis (no need to update axis-generator function)
svg.select(".axis")
.transition()
.duration(1000)
.call(yAxis);
});
// extend dataset by 1 for each click on svg
svg.on("click", function() {
// extend dataset & update x scale
dataset.push({ id: dataset.length, val: Math.random() * domain });
x_scale.domain(d3.range(dataset.length));
// add this datum to the bars <g> tag as a rect
var bars = svg.select(".bars")
.selectAll("rect")
.data(dataset, key);
bars.enter() // adds new data point(s)
.append("rect")
.attr({
x: w,
y: function(d) {
return y_scale(d.val);
},
width: x_scale.rangeBand(), // calculates width automatically
height: function(d) { return h - y_scale(d.val); },
opacity: 0.6,
fill: function(d) {
return "rgb(50, 0, " + Math.floor(color_scale(d.val)) + ")";
}
});
// how does this move all the other bars!?
// because the entire dataset is mapped to bars
bars.transition()
.duration(500)
.attr("x", function(d, i) {
return x_scale(i) + pad;
});
});
// add mouseover color change transition using d3 (vs CSS)
svg.select(".bars").selectAll("rect")
.on("mouseover", function() {
d3.select(this)
.transition()
.attr("fill", "red");
})
.on("mouseout", function(d) {
d3.select(this)
.transition()
.attr("fill", function() {
return "rgb(50, 0, " + Math.floor(color_scale(d.val)) + ")";
})
.attr("opacity", 0.6);
})
// print to console when clicking on bar = good for debugging
.on("click", function(d) { console.log(d); });
</script>
</body>
</html>
UPDATE:
Thanks to Miroslav's suggestion, I started playing around with different ways to resolve the issue and came across Makyen's answer to this related SO post.
While I imagine there is a more performant way to handle this, I have decided to rebind the mouseover event listener each time the mouse enters the svg element using the following code:
svg.on("mouseover", mouse_over_highlight);
// add mouseover color change transition using d3 (vs CSS)
function mouse_over_highlight() {
d3.selectAll("rect")
.on("mouseover", function () {
d3.select(this)
.transition()
.attr("fill", "red");
})
.on("mouseout", function (d) {
d3.select(this)
.transition()
.attr("fill", function () {
return "rgb(50, 0, " + Math.floor(color_scale(d.val)) + ")";
})
.attr("opacity", 0.6);
})
// print to console when clicking on bar = good for debugging
.on("click", function (d) {
console.log(d);
});
}