Reputation: 1521
In my code below I've managed to make the brush move in unison with the zoom event, but I'm now stuck with trying to make the bar chart zoom properly when you use the brush.
I've based my brushed
function on this answer but was unable to convert the logic to my own code.
I get the error: Cannot read property 'transform' of undefined and I have no idea why.
Here's all my code:
const data = [
{month: "jan", value: 12},
{month: "feb", value: 25},
{month: "mar", value: 10},
{month: "apr", value: 15}
];
var svg = d3.select("svg"),
margin = {top: 10, right: 35, bottom: 10, left: 35},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var g = svg.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// === Bar ===
var x = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)]).nice()
.range([margin.left * 2, width])
var y = d3.scaleBand()
.domain(data.map(d => d.month))
.range([height, 0])
.padding(0.1)
var yAxis = g => g
.attr("transform", `translate(${margin.left * 2},0)`)
.call(d3.axisLeft(y).tickSizeOuter(0))
g.append("g").selectAll(".bar")
.data(data).enter().append("rect")
.attr("fill", "steelblue")
.attr("class", "bar")
.attr("x", x(0))
.attr("y", d => y(d.month))
.attr("width", d => x(d.value) - x(0))
.attr("height", y.bandwidth());
g.append("g")
.attr("class", "y-axis")
.call(yAxis);
// === Brush ===
var xB = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, margin.left]);
var yB = d3.scaleBand()
.domain(data.map(d => d.month))
.range([height, 0])
.padding(0.1);
var brush = d3.brushY()
.extent([[0, 0],[margin.left, height]])
.on("start brush", brushed);
var yAxisB = g => g
.call(d3.axisLeft(yB).tickSizeOuter(0))
g.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, yB.range().reverse())
g.append("g").selectAll(".brushBar")
.data(data).enter().append("rect")
.attr("fill", "steelblue")
.attr("class", "brushBar")
.attr("x", xB(0))
.attr("y", d => yB(d.month))
.attr("width", d => xB(d.value) - xB(0))
.attr("height", yB.bandwidth());
g.append("g")
.call(yAxisB);
// === Brush & Zoom ===
var bExtent = [[0, 0], [width, height]]
var zoom = d3.zoom()
.scaleExtent([1, 2])
.translateExtent(bExtent)
.extent(bExtent)
.on("zoom", zoomed)
g.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
function zoomed() {
if (d3.event.sourceEvent &&
d3.event.sourceEvent.type === "brush") return;
var t = d3.event.transform;
y.range([height, 0]
.map(d => d3.event.transform.applyY(d)));
g.selectAll(".bar")
.attr("y", d => y(d.month))
.attr("height", y.bandwidth());
g.selectAll(".y-axis").call(yAxis);
g.select(".brush").call(brush.move,
yB.range().reverse().map(t.invertY, t))
}
function brushed() {
if (d3.event.sourceEvent &&
d3.event.sourceEvent.type === "zoom") return;
var s = d3.event.selection,
nD = [];
yB.domain().forEach((d) => {
var pos = yB(d) + yB.bandwidth() / 2;
if (pos > s[0] && pos < s[1]){
nD.push(d);
}
});
y.domain(nD);
g.selectAll(".y-axis").call(yAxis);
g.selectAll(".bar")
.attr("y", d => y(d.month))
.attr("height", y.bandwidth());
//g.select(".zoom").call(zoom.transform, d3.zoomIdentity
// .scale(2)
// .translate(-s[0], 0));
}
.zoom {
cursor: move;
fill: none;
pointer-events: all;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="540" height="120"></svg>
Upvotes: 1
Views: 509
Reputation: 102194
First of all, I'm not getting the error you described.
The problem in your snippet seems to be that the brushed out bars are piling up at the top of the SVG. This is due to the fact that you're changing the y
scale's domain, and because of that some bars get undefined
for y(d.month)
. Therefore, instead of nicely disappearing from the view, they are simply painted at 0
in the y
position.
The best alternative is rethinking all your brush/zoom code. However, for minimal changes in the code, a quick and dirty solution is simply turning the bars that have undefined
for the y
position transparent:
.style("opacity", d => y(d.month) ? 1 : 0)
Also, make the pointer-events: none;
for the bars in the brush, that makes a better user experience:
.attr("pointer-events", "none")
Here is your code with those change:
const data = [{
month: "jan",
value: 12
},
{
month: "feb",
value: 25
},
{
month: "mar",
value: 10
},
{
month: "apr",
value: 15
}
];
var svg = d3.select("svg"),
margin = {
top: 10,
right: 35,
bottom: 10,
left: 35
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var g = svg.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// === Bar ===
var x = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)]).nice()
.range([margin.left * 2, width])
var y = d3.scaleBand()
.domain(data.map(d => d.month))
.range([height, 0])
.padding(0.1)
var yAxis = g => g
.attr("transform", `translate(${margin.left * 2},0)`)
.call(d3.axisLeft(y).tickSizeOuter(0))
g.append("g").selectAll(".bar")
.data(data).enter().append("rect")
.attr("fill", "steelblue")
.attr("class", "bar")
.attr("x", x(0))
.attr("y", d => y(d.month))
.attr("width", d => x(d.value) - x(0))
.attr("height", y.bandwidth());
g.append("g")
.attr("class", "y-axis")
.call(yAxis);
// === Brush ===
var xB = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, margin.left]);
var yB = d3.scaleBand()
.domain(data.map(d => d.month))
.range([height, 0])
.padding(0.1);
var brush = d3.brushY()
.extent([
[0, 0],
[margin.left, height]
])
.on("start brush", brushed);
var yAxisB = g => g
.call(d3.axisLeft(yB).tickSizeOuter(0))
g.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, yB.range().reverse())
g.append("g").selectAll(".brushBar")
.data(data).enter().append("rect")
.attr("fill", "steelblue")
.attr("class", "brushBar")
.attr("pointer-events", "none")
.attr("x", xB(0))
.attr("y", d => yB(d.month))
.attr("width", d => xB(d.value) - xB(0))
.attr("height", yB.bandwidth());
g.append("g")
.call(yAxisB);
// === Brush & Zoom ===
var bExtent = [
[0, 0],
[width, height]
]
var zoom = d3.zoom()
.scaleExtent([1, 2])
.translateExtent(bExtent)
.extent(bExtent)
.on("zoom", zoomed)
g.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
function zoomed() {
if (d3.event.sourceEvent &&
d3.event.sourceEvent.type === "brush") return;
var t = d3.event.transform;
y.range([height, 0]
.map(d => d3.event.transform.applyY(d)));
g.selectAll(".bar")
.attr("y", d => y(d.month))
.attr("height", y.bandwidth());
g.selectAll(".y-axis").call(yAxis);
g.select(".brush").call(brush.move,
yB.range().reverse().map(t.invertY, t))
}
function brushed() {
if (d3.event.sourceEvent &&
d3.event.sourceEvent.type === "zoom") return;
var s = d3.event.selection,
nD = [];
yB.domain().forEach((d) => {
var pos = yB(d) + yB.bandwidth() / 2;
if (pos > s[0] && pos < s[1]) {
nD.push(d);
}
});
y.domain(nD);
g.selectAll(".y-axis").call(yAxis);
g.selectAll(".bar")
.attr("y", d => y(d.month))
.style("opacity", d => y(d.month) ? 1 : 0)
.attr("height", y.bandwidth());
//g.select(".zoom").call(zoom.transform, d3.zoomIdentity
// .scale(2)
// .translate(-s[0], 0));
}
.zoom {
cursor: move;
fill: none;
pointer-events: all;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="540" height="120"></svg>
Upvotes: 1