| ← Lab 2 | Lab 3 | Lab 4 → |
In this exercise, we added two simple interactions with our bar charts for basic interaction with the charts.
Firstly, we implemented a way to highlight the Bar Charts when the mouse cursor was hovered over the bars.
Secondly, we added a simple tooltip text that is displayed to show additional information about the bars when the cursor is hovered over the bars.
barChart.jsexport default class BarChart {
// ...
#updateBars() {
this.bars = this.bars
.data(this.data, (d) => d[0])
// ...
.on("mouseover", (event, datum) => {
// Highlight the bar on cursor hover
d3.select(event.target)
.classed("highlighted", true)
})
.on("mouseout", (event, datum) => {
// Remove the highlight on cursor out
d3.select(event.target)
.classed("highlighted", false)
});
// Add tooltips
// Adds a title element to all bars
this.bars.selectAll("title")
.data(d => [d])
.join("title")
.text(d => `${d[0]}: ${d[1]}`);
}
// ...
}
main.js"use strict";
import BarChart from './visualizations/barChart_tut10.js';
/***** Exercise: Linked Selection and Filters *****/
let data = await d3.csv("data/movies_mock.csv", d => {
return {
year: +d.release_year,
revenues: parseFloat(d.revenues),
genre: d.genre
}
});
let bar1 = new BarChart("#bar1", 800, 400, [10, 40, 65, 10]),
bar2 = new BarChart("#bar2", 800, 400, [10, 40, 65, 10]),
bar3 = new BarChart("#bar3", 800, 400, [10, 40, 65, 10]);
let sortYears = (a, b) => a[0] - b[0];
let yearRevenues = d3.flatRollup(data, v => d3.sum(v, d => d.revenues), d => d.year).sort(sortYears),
yearCount = d3.flatRollup(data, v => v.length, d => d.year).sort(sortYears),
genreCount = d3.flatRollup(data, v => v.length, d => d.genre);
bar1.setLabels("Year", "Total Revenues")
.render(yearRevenues);
bar2.setLabels("Year", "Total Number of Releases")
.render(yearCount);
bar3.setLabels("Genre", "Total Number of Releases")
.render(genreCount);
let highlightYear = (e, d) => {
let year = d[0];
bar1.highlightBars([year]);
bar2.highlightBars([year]);
}
let rmvHighlightYear = (e, d) => {
bar1.highlightBars();
bar2.highlightBars();
}
bar1.setBarHover(highlightYear).setBarOut(rmvHighlightYear);
bar2.setBarHover(highlightYear).setBarOut(rmvHighlightYear);
let filterGenre = (e, d) => {
let genre = d[0];
let filteredData = data.filter(d => d.genre === genre),
yearRevenuesFiltered = d3.flatRollup(filteredData, v => d3.sum(v, d => d.revenues), d => d.year).sort(sortYears),
yearCountFiltered = d3.flatRollup(filteredData, v => v.length, d => d.year).sort(sortYears);
bar1.setLabels("Year", `Revenues: ${genre}`)
.render(yearRevenuesFiltered);
bar2.setLabels("Year", `Number of Releases: ${genre}`)
.render(yearCountFiltered);
}
bar3.setBarClick(filterGenre);
barChart.jsexport default class BarChart {
// Attributes
// ...
// Add Object attributes for storing callback references
barClick = () => {};
barHover = () => {};
barOut = () => {};
// ...
#updateBars() {
this.bars = this.bars
// ...
// ...
this.#updateEvents();
// ...
}
#updateEvents() {
// Rebind these callbacks to events
this.bars
.on("mouseover", this.barHover)
.on("mouseout", this.barOut)
.on("click", (e, d) => {
console.log(`Bar Clicked: ${d}`);
this.barClick(e, d);
});
}
// ...
setBarClick(f = () => {}) {
// Register new callback
this.barClick = f;
// Rebind callback to event
this.#updateEvents();
// Return this for chaining
return this;
}
setBarHover(f = () => {}) {
// Register new callback
this.barHover = f;
// Rebind callback to event
this.#updateEvents();
// Return this for chaining
return this;
}
setBarOut(f = () => {}) {
// Register new callback
this.barOut = f;
// Rebind callback to event
this.#updateEvents();
// Return this for chaining
return this;
}
highlightBars(keys = []) {
// Reset Highlight for all bars
this.bars.classed("highlighted", false);
// Filter bars and set new highlights
this.bars.filter(d => keys.includes(d[0]))
.classed("highlighted", true);
return this; // to allow chaining
}
}
Adds an animated element for the bar chart using D3 Transitions.
There is a visual feedback when bar3 is clicked, showing an animation in bar1 and bar2.
barChart.jsexport default class BarChart {
// ...
#updateBars() {
// Bind and join rectangles to data
this.bars = this.bars
.data(this.data, (d) => d[0])
.join(
// Initial placement of new rectangles
enter => enter.append("rect")
.attr("x", (d) => this.scaleX(d[0]))
.attr("y", (d) => this.scaleY(0)) // Aligned at Bottom
.attr("width", this.scaleX.bandwidth())
.attr("height", 0), // No height
// Leave existing rectangles untouched
update => update,
exit => exit.transition().duration(300)
.attr("y", d => this.scaleY(0)) // Aligned at bottom
.attr("height", 0) // No Height
.remove() // Destroy rectangle when finished
)
.classed("bar", true);
// Animate Placement and sizing (enter + update only)
this.bars.transition().duration(500)
.attr("x", (d) => this.scaleX(d[0]))
.attr("y", (d) => this.scaleY(d[1]))
.attr("width", this.scaleX.bandwidth())
.attr("height", (d) => this.scaleY(0) - this.scaleY(d[1]));
}
}
main.js