Public
Edited
Jun 26, 2023
1 star
Insert cell
Insert cell
Insert cell
Insert cell
brush_chart = {
const svg_brush = d3.create("svg")
.attr("class", "show-up")
.attr("viewBox", [0, 0, width, brushHeight])

svg_brush.append("g")
.call(xAxisBrush);

// HIDDEN X-AXIS
svg_brush.append("g")
.attr("class", "hidden-date-axis")
.style("visibility", "hidden")
.call(d3.axisBottom(xDateAxis));
// HIDDEN X-AXIS

// ADD YEARLY DATA PLACEHOLDERS
svg_brush.append("g")
.selectAll("g")
// Enter in data = loop group per group
.data(data)
.enter()
.append("g")
.attr("class", "barGroup")
.attr("transform", function(d) { return "translate(" + x(d.group) + ",0)"; })
// ADD YEARLY DATA PLACEHOLDERS
// MONTHLY AXES
const monthlyAxes = getMonthlyAxes(svg_brush, brushHeight, false);
// MONTHLY AXES

// CATEGORY AXES - MONTHS
const xSubgroupMonths = getXSubgroupMonths(monthlyAxes);
// CATEGORY AXES - MONTHS

// ADD MONTHLY DATA
generateBarsMonth(svg_brush, monthlyAxes, xSubgroupMonths, yBrush)
// ADD MONTHLY DATA

// BRUSH FUNCTIONALITY
svg_brush.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, x.range());

d3.selectAll('.handle').remove();
d3.selectAll('.resize').remove();
d3.selectAll('.background').remove();
d3.selectAll('.overlay').remove();
// BRUSH FUNCTIONALITY


return svg_brush.node();
}
Insert cell
Insert cell
Insert cell
function generateBarsMonthZoomed(svg, monthlyAxes, y) {
svg.selectAll(".bar").remove();
const xSubgroupMonths = getXSubgroupMonths(monthlyAxes);
generateBarsMonth(svg, monthlyAxes, xSubgroupMonths, y)
}
Insert cell
brush = d3.brushX()
.extent([[0, 0], [width, height]])
.on("brush", brushed);
Insert cell
function brushed(event) {
if (
(event.sourceEvent && event.sourceEvent.type !== "zoom")
) {
dispatchBrush.call("brushCallingZoom", event);
}
}
Insert cell
// This makes sure we're not zooming in too much or too little depending on the number of years
zoomScale = x.domain().length * 1.2;
Insert cell
extent = [[margin.left, margin.top], [width - margin.right, height - margin.top]];
Insert cell
zoom = d3.zoom()
.scaleExtent([1, zoomScale])
.translateExtent(extent)
.extent(extent)
.on("zoom", zoomedCallback)
Insert cell
function zoomedCallback(event) {
/* bit of a hack here: need to manually disable hovered data when the user zooms */
d3.selectAll('.svg1-tooltip').style("visibility", 'hidden');
/* hack end */

xSubgroupYear.rangeRound([0, x.bandwidth()]);
x.range([0, width].map(d => event.transform.applyX(d)));
svg.select(".x-axis").call(d3.axisBottom(x));
svg.selectAll(".barGroup").attr("transform", (d) => "translate(" + x(d.group) + ",0)");

if (
(event.sourceEvent && event.sourceEvent.type) !== "brush"
) {
dispatchZoom.call("zoomCallingBrush", event);
}
/**
* the transform value we get from the brush-triggered zoom event is
* not accurate upto 6/7 decimal places, so we need to round it
* example: we get 8.99999998 instead of 9
* so we need to round it off to 9 ONLY if the zoom event is triggered by the brush
*/
const transformValue = Math.abs(event.transform.k - zoomScale);
if (transformValue < 0.00001) {
if (event.sourceEvent && event.sourceEvent.type !== "brush") {
// if the secondary axes have already been rendered then just go back
if (svg.selectAll("[class*='x-axis-2']").empty()) {
const monthlyAxes = getMonthlyAxes(svg, height, true);
if (svg.selectAll(".barGroup-Secondary").empty()) {
generateBarsMonthZoomed(svg, monthlyAxes, y);
}
}
}
} else {
svg.selectAll(".bar")
.attr("x", function(d) { return xSubgroupYear(d.key);})
.attr("width", xSubgroupYear.bandwidth());
if (!svg.selectAll(".barGroup-Secondary").empty()) {
generateBarsYear(svg, x, y, height, color);
svg.selectAll("[class*='x-axis-2']").remove();
}
}
// showDataOnHover(svg);
}

Insert cell
dispatchZoom.on("zoomCallingBrush", function(data) {
/**
* we need to use a reference scale here so that zooming
* is relative to the original zoom state and not the last zoom state
* explanation: https://stackoverflow.com/a/50185040/6051241
* */
xDateAxis.domain(this.transform.rescaleX(xDateAxisReference).domain());
const [startPoint, endPoint] = xDateAxis.domain();
d3.select(".brush")
.call(
brush.move,
[xDateAxisReference(startPoint), xDateAxisReference(endPoint)]
);
});
Insert cell
dispatchBrush.on("brushCallingZoom", function(data) {
const s = this.selection;
d3.select(".zoom").call(zoom.transform, d3.zoomIdentity
.scale(width / (s[1] - s[0]))
.translate(-s[0], 0));
});
Insert cell
dispatchBrush = d3.dispatch("brushCallingZoom");
Insert cell
dispatchZoom = d3.dispatch("zoomCallingBrush");
Insert cell
Insert cell
Insert cell
x = d3.scaleBand()
.domain(years)
.range([0, width])
.padding([0.2])
Insert cell
y = d3.scaleLog()
.base(2)
.domain([1, 1500])
.range([ height - margin.bottom, margin.top ]);
Insert cell
yBrush = d3.scaleLog()
.base(2)
.domain([1, 1500]) // !! if you update the data you'll need to update this too!
.range([ brushHeight - margin.bottom, margin.top ]);
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.style("font-size", "12px")
.style("font-weight", "bold")
.attr("class", "x-axis")
.call(d3.axisBottom(x))
Insert cell
xAxisBrush = g => g
.attr("transform", `translate(0,${brushHeight - margin.bottom})`)
.style("font-size", "12px")
.style("font-weight", "bold")
.call(d3.axisBottom(x.copy()))
Insert cell
xDateAxis = d3.scaleTime()
.domain([ new Date(d3.min(years)), new Date(d3.max(years)) ])
.range([0, width])
Insert cell
xDateAxisReference = xDateAxis.copy();
Insert cell
xSubgroupYear = d3.scaleBand()
.domain(subgroups)
.rangeRound([0, x.bandwidth()])
.padding([0.05]);
Insert cell
function getXSubgroupMonths(monthlyAxes) {
return d3.scaleBand()
.domain(subgroups)
.rangeRound([0, monthlyAxes[0].bandwidth()])
.padding([0.05]);
}
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.attr("class", "y-axis")
.call(d3.axisLeft(y))
Insert cell
getMonthlyAxes = (svg, height, showTicks) => {
const x2s = []
const ticks = svg
.selectAll(".barGroup")
.append("g")
.attr("transform", "translate(0," + height*0.9 + ")")
.style("font-size", "10px")
.style("font-weight", "bold")
.attr("class",(_, i) => `x-axis-2-${i}`)
.each((_, i) => {
const barGroup = svg.select(`.barGroup:nth-child(${i+1})`);
const currentYear = barGroup.data()[0]?.group;
const x2 = d3.scaleBand()
.domain(months.map(month => `${currentYear}-${month}`))
.range([0, x.bandwidth()])
.padding(0);
x2s.push(x2);
});
if(showTicks) {
x2s.forEach((x2, i) =>
svg.selectAll(`.x-axis-2-${i}`)
.call(
d3.axisBottom(x2)
.tickFormat(d => d.split('-')[1])
)
)
}
ticks.selectAll("text").attr("transform", "translate(-10, 10), rotate(-65)");
return x2s;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more