D3 条形图缩放不居中

D3 Bar Chart Zoom Not Centered

我有一个带缩放功能的条形图。问题是,缩放实际上并未居中。如果,我将光标放在一个条上并缩放,光标下方的条会移动而不是停留在那里,但是,如果我设置 MARGIN.LEFT = 0,那么问题就会得到纠正,无论是什么条我有我的光标,当我缩放时,该条停留在那里,就在下方。谁能帮我解决这个问题?

工作代码在这里: https://codesandbox.io/s/d3-zoom-not-centered-sfziyk

D3代码:

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");
});

当您 call 在 svg 上 zoom 时,所有缩放行为都是相对于 svg 的。

假设您的 x-axis 处于长度为 100 的初始缩放级别,表示域 [0, 100]。所以 x-scale 有 range([0, 100])domain([0, 100])。添加 10 的左边距。

如果您在轴的中点 x=50 处按比例 2 进行缩放,您会期望在缩放后得到以下行为:

  • 中点不动。
  • 区间[25, 75]可见。

但是,由于在 svg 上调用了缩放,因此您必须考虑 10 的左边距。缩放不会发生在中点,而是发生在 x = 10 + 50 = 60 处。因此变换是 x - > x * k + t 其中 k = 2 且 t = -60。这导致

  • x = 50 -> 2 * 50 - 60 = 40,
  • x = 80 -> 2 * 80 - 60 = 100,
  • x = 30 -> 2 * 30 - 60 = 0.

放大后可见区间[30, 80],x=50点左移

这是您在图表中观察到的内容。

为了获得预期的行为,您可以做两件事:

一个。遵循 bar chart example,其中 x-scale 的范围不是从 0 开始,而是从左边距开始。 margin.leftmargin.top翻译过来的g这里也省略了。相反,轴的范围直接包含边距。

b。添加一个 rectfill: none; pointer-events: all; 到图表大小的 svg,没有边距。然后在该矩形上调用 zoom,就像在 example.

中所做的那样

请注意,ObservableHQ 上的所有新示例都遵循需要较少标记的模式“a”。