renderGenres = function() {
const stackGenres = genreXPosition === "stack";
const genres = Array.from(genresMap.keys());
const genresList = Array.from(genresMap.entries());
const mostGenresPerFilm = d3.max(filteredData.map(d => d[ATTR_GENRES]).map(d => d.length));
const mostFilmsPerGenre = d3.max(Array.from(genresMap.values()).map(d => d.length));
const DEFAULT_OPACITY = 0.5;
const genreScaleX = d3.scaleBand(
stackGenres
? d3.range(0, mostFilmsPerGenre + 1)
: genreXPosition === ATTR_X
? xDomain
: genreXPosition === ATTR_RATING
? d3.range(0, 10.1, 0.1).map(d => Math.floor(d * 10) / 10)
: yDomain,
[2*MARGIN, width - MARGIN]
).padding(0.15);
const genreScaleY = d3.scaleBand(genres, [GENRE_HEIGHT - MARGIN, MARGIN]).padding(0.15);
let hoveredFilm = null;
let hoveredGenre = null;
function getHoveredFilms() {
const hoveredFilms = hoveredGenre === null || hoveredFilm === null
? []
: stackGenres
? [hoveredFilm]
: hoveredGenre[1].filter(d => +d[genreXPosition] === +hoveredFilm[genreXPosition]);
return hoveredFilms;
}
function updateHoveredFilm() {
const hoveredFilms = getHoveredFilms();
genre.selectAll("rect.film")
.attr("fill-opacity", d => {
if (hoveredFilms.length === 0) {
return DEFAULT_OPACITY;
}
if (hoveredFilms.indexOf(d) > -1) {
return DEFAULT_OPACITY;
}
return DEFAULT_OPACITY / 2;
})
.attr("stroke", d => hoveredFilm === d ? "#000" : "#fff");
}
function updateTooltip(e) {
const hoveredFilms = getHoveredFilms();
const parent = genreSvg.node().getBoundingClientRect();
tooltip
.attr("display", hoveredFilm === null ? "none" : "block")
.attr("transform", `translate(${e.clientX - parent.left + 10},${e.clientY - parent.top + FONT_SIZE * 0.75})`);
tooltip.select("text").selectAll("tspan").data(hoveredFilms)
.join("tspan")
.attr("x", 0)
.attr("y", (d, i) => i * FONT_SIZE * 1.05)
.text(d => `${d[ATTR_TITLE]} (${d[ATTR_X]})`);
tooltip.select("rect")
.attr("width", tooltip.select("text").node().getBBox().width)
.attr("height", tooltip.select("text").node().getBBox().height);
}
const genreSvg = d3.create("svg")
.attr("width", width)
.attr("height", GENRE_HEIGHT)
.on("mousemove", updateTooltip);
const genreLinearScaleX = stackGenres || genreXPosition === ATTR_RATING
? d3.scaleLinear(d3.extent(genreScaleX.domain()), genreScaleX.range())
: d3.scaleTime(d3.extent(genreScaleX.domain()).map(d => new Date(`${d}-01-01`)), genreScaleX.range());
const axisX = d3.axisBottom(genreLinearScaleX);
genreSvg.append("g")
.attr("class", "axis x")
.attr("transform", `translate(0, ${genreScaleY.range()[0]})`)
.call(axisX);
const barchart = genreSvg.append("g")
.attr("class", "genre-barchart");
const genre = barchart.selectAll("g.bar").data(genresList).join("g")
.attr("class", d => d[0])
.attr("transform", d => `translate(0,${genreScaleY(d[0])})`)
.on("mouseenter", (e, d) => {
hoveredGenre = d;
updateHoveredFilm();
})
.on("mouseleave", () => {
hoveredGenre = null;
updateHoveredFilm();
});
genre.selectAll("rect.film").data(d => d[1]).join("rect")
.attr("class", "film")
.attr("x", (d, i) => stackGenres ? genreScaleX(i) : genreScaleX(+d[genreXPosition]))
.attr("width", genreScaleX.bandwidth())
.attr("height", genreScaleY.bandwidth())
.attr("fill", "steelblue")
.attr("fill-opacity", DEFAULT_OPACITY)
.attr("stroke-width", stackGenres ? 0 : 1)
.on("mouseenter", (e, d) => {
hoveredFilm = d;
updateHoveredFilm();
})
.on("mouseleave", (e, d) => {
hoveredFilm = null;
updateHoveredFilm();
});
genre.append("text")
.attr("x", d => d3.min(genreScaleX.range()) - 10)
.attr("y", genreScaleY.bandwidth() / 2)
.attr("text-anchor", "end")
.attr("font-family", "sans-serif")
.attr("font-size", FONT_SIZE)
.text(d => d[0]);
const tooltip = genreSvg.append("g").attr("class", "tooltip");
tooltip.append("rect")
.attr("x", 0)
.attr("y", -FONT_SIZE)
.attr("fill", "white")
.attr("fill-opacity", 0.73);
tooltip.append("text")
.attr("font-family", "sans-serif")
.attr("font-size", FONT_SIZE);
updateHoveredFilm();
return genreSvg.node();
}