I have a bar chart with zoom function. The issue is, the zooming isn't actually centered. If, I place the cursor, on a bar and zoom, the bar underneath the cursor moves away as opposed to staying there, However, if I set the MARGIN.LEFT
= 0, then the issue is rectified and No matter what bar I have my cursor on, when I zoom the bar stays there, right underneath. Could anyone help me with this?
Working Code Here: https://codesandbox.io/s/d3-zoom-not-centered-sfziyk
D3 Code:
const MARGIN = {
LEFT: 60,
RIGHT: 40,
TOP: 10,
BOTTOM: 130
};
// total width incl margin
const VIEWPORT_WIDTH = 1140;
// total height incl margin
const VIEWPORT_HEIGHT = 400;
const WIDTH = VIEWPORT_WIDTH - MARGIN.LEFT - MARGIN.RIGHT;
const HEIGHT = VIEWPORT_HEIGHT - MARGIN.TOP - MARGIN.BOTTOM;
const svg = d3
.select(".chart-container")
.append("svg")
.attr("width", WIDTH MARGIN.LEFT MARGIN.RIGHT)
.attr("height", HEIGHT MARGIN.TOP MARGIN.BOTTOM);
const g = svg
.append("g")
.attr("transform", `translate(${MARGIN.LEFT}, ${MARGIN.TOP})`);
g.append("text")
.attr("class", "x axis-label")
.attr("x", WIDTH / 2)
.attr("y", HEIGHT 110)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.text("Month");
g.append("text")
.attr("class", "y axis-label")
.attr("x", -(HEIGHT / 2))
.attr("y", -60)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.text("");
const zoom = d3.zoom().scaleExtent([0.5, 10]).on("zoom", zoomed);
svg.call(zoom);
function zoomed(event) {
x.range([0, WIDTH].map((d) => event.transform.applyX(d)));
barsGroup
.selectAll("rect.profit")
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth());
barsGroup
.selectAll("rect.revenue")
.attr("x", (d) => x(d.month) 0.5 * x.bandwidth())
.attr("width", 0.5 * x.bandwidth());
xAxisGroup.call(xAxisCall);
}
const x = d3.scaleBand().range([0, WIDTH]).paddingInner(0.3).paddingOuter(0.2);
const y = d3.scaleLinear().range([HEIGHT, 0]);
const xAxisGroup = g
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${HEIGHT})`);
const yAxisGroup = g.append("g").attr("class", "y axis");
const xAxisCall = d3.axisBottom(x);
const yAxisCall = d3
.axisLeft(y)
.ticks(3)
.tickFormat((d) => "$" d);
const defs = svg.append("defs");
const barsClipPath = defs
.append("clipPath")
.attr("id", "bars-clip-path")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", WIDTH)
.attr("height", 400);
const barsGroup = g.append("g");
const zoomGroup = barsGroup.append("g");
barsGroup.attr("class", "bars");
zoomGroup.attr("class", "zoom");
barsGroup.attr("clip-path", "url(#bars-clip-path)");
xAxisGroup.attr("clip-path", "url(#bars-clip-path)");
d3.csv("data.csv").then((data) => {
data.forEach((d) => {
d.profit = Number(d.profit);
d.revenue = Number(d.revenue);
d.month = d.month;
});
var y0 = d3.max(data, (d) => d.profit);
var y1 = d3.max(data, (d) => d.revenue);
var maxdomain = y1;
if (y0 > y1) var maxdomain = y0;
x.domain(data.map((d) => d.month));
y.domain([0, maxdomain]);
xAxisGroup
.call(xAxisCall)
.selectAll("text")
.attr("y", "10")
.attr("x", "-5")
.attr("text-anchor", "end")
.attr("transform", "rotate(-40)");
yAxisGroup.call(yAxisCall);
const rects = zoomGroup.selectAll("rect").data(data);
rects.exit().remove();
rects
.attr("y", (d) => y(d.profit))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.profit));
rects
.enter()
.append("rect")
.attr("class", "profit")
.attr("y", (d) => y(d.profit))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.profit))
.attr("fill", "grey");
const rects_revenue = zoomGroup.selectAll("rect.revenue").data(data);
rects_revenue.exit().remove();
rects_revenue
.attr("y", (d) => y(d.revenue))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.revenue));
rects_revenue
.enter()
.append("rect")
.attr("class", "revenue")
.style("fill", "red")
.attr("y", (d) => y(d.revenue))
.attr("x", (d) => x(d.month) 0.5 * x.bandwidth())
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.revenue))
.attr("fill", "grey");
});
CodePudding user response:
When you call
the zoom
on the svg, all zoom behaviour is relative to the svg.
Imagine that your x-axis is at initial zoom level of length 100 representing the domain [0, 100]. So the x-scale has range([0, 100])
and domain([0, 100])
. Add a left margin of 10.
If you zoom by scale 2 at the midpoint of your axis at x=50 you would expect to get the following behaviour after the zoom:
- The midpoint does not move.
- The interval [25, 75] is visible.
However, since the zoom is called on the svg you have to account for the left margin of 10. The zoom does not occur at the midpoint but at x = 10 50 = 60. The transform is thus x -> x * k t with k = 2 and t = -60. This results in
- x = 50 -> 2 * 50 - 60 = 40,
- x = 80 -> 2 * 80 - 60 = 100,
- x = 30 -> 2 * 30 - 60 = 0.
Visible after the zoom is the interval [30, 80] and the point x = 50 is shifted to the left.
This is what you observe in your chart.
In order to get the expected behaviour, you can do two things:
a. Follow the bar chart example where the range of the x-scale does not start at 0 but at the left margin. The g
which is translated by margin.left
and margin.top
is also omitted here. Instead, the ranges of the axes incorporate the margins directly.
b. Add a rect
with fill: none; pointer-events: all;
to the svg that is of the size of the chart without the margins. Then call the zoom
on that rectangle, as done in this example.
Note that all the new examples on ObservableHQ follow the pattern "a" that needs fewer markup.