jpc
jpc

Reputation: 139

How to avoid axes moving out of bounds when zooming and panning

I am using React with D3 (v7) to make a stacked bar chart. I've added the ability to zoom and pan around the chart in both X and Y directions.

When zooming and panning, elements would be moved outside of the bounds of the chart. I added a clipPath which works with the bars themselves, so when zooming/panning the bars do not get drawn outside of the axes. However, the axes still get drawn out of bounds (see the images below for an example).

Correct position here -

Axes in correct position without zoom or pan

As compared to how they are moved after zooming and panning

Axes extending past each other while bars are appropriately clipped

How can I avoid this happening? I've tried using rescaleY which breaks the zoom functionality, and rescaleX does not work for band scales.

Here is my abbreviated code:

const BarChart = ({
  data,
  dimensions = {
    chartOffset: {
      left: 10,
    },
    margin: {
      top: 50,
      right: 50,
      bottom: 50,
      left: 50,
    },
    width: 800,
    height: 500,
  },
  subgroups,
  colors,
  displayNames,
}) => {
  const svgRef = React.useRef(null);
  const {
    width, height, margin, chartOffset,
  } = dimensions;
  const svgWidth = width + margin.left + margin.right;
  const svgHeight = height + margin.top + margin.bottom;
  const xScale = myChartUtils.getXScale(); //scaleBand
  const yScale = myChartUtils.getYScale(); //scaleLinear

  function zoom() {
    const extent = [[0, 0], [width, height]];
    const svgEl = d3.select(svgRef.current);

    svgEl.call(d3.zoom()
      .scaleExtent([1, 20])
      .translateExtent(extent)
      .extent(extent)
      .on("zoom", zoomed));

      function zoomed(e) {
        const xAxis = d3.axisBottom(xScale);
        const yAxis = d3.axisLeft(yScale).ticks(5).tickSize(5);

        xScale.range([0, width - margin.right].map(d => e.transform.applyX(d)));
        yScale.range([height, 0].map(d => e.transform.applyY(d)));

        svgEl
          .selectAll(".bars rect")
          .attr('x', d => xScale(d.data.user))
          .attr('width', xScale.bandwidth())
          .attr('y', d => yScale(d[1]))
          .attr('height', d => yScale(d[0]) - yScale(d[1]))
        svgEl.selectAll('.x-axis').call(xAxis)
        svgEl.selectAll('.y-axis').call(yAxis)
      }
  }
React.useEffect(() => {
    // generate stacked bars
    const stackedGroups = d3
      .stack()
      .keys(subgroups)
      .value((obj, key) => obj[key].length)(data);
    const svgEl = d3.select(svgRef.current);
    const svg = svgEl
      .append('g')
      .attr("id", "plot")
      .attr(
        'transform',
        `translate(${margin.left + chartOffset.left},${
          margin.top + chartOffset.top
        })`,
      );

    svg.append("defs").append("clipPath")
      .attr("id", "clip")
      .append("rect")
      .attr("width", width)
      .attr("height", height);

    const plotArea = svg.append("g")
      .attr("clip-path", "url(#clip)");

    myChartUtils.drawXAxis(xScale, svg, height, width, margin, 'title');
    myChartUtils.drawYAxis(yScale, svg, height, 'title');
    
    plotArea
      .append('g')
      .attr("class", "bars")
      .selectAll('g')
      .data(stackedGroups, (d) => d.user)
      .enter()
      .append('g')
      .attr('fill', (d) => colors[d.key])
      .selectAll('rect')
      .data((d) => d)
      .enter()
      .append('rect')
      .attr('x', (d) => xScale(d.data.user))
      .attr('y', (d) => yScale(d[1]))
      .attr('height', (d) => yScale(d[0]) - yScale(d[1]))
      .attr('width', xScale.bandwidth())
      .style('opacity', 0.5)
      .call(zoom);
    
    // Clear svg content before adding new elements
    return function cleanup() {
      svgEl.selectAll('rect').remove();
    };
  }, [data, margin, colors]);

Upvotes: 1

Views: 325

Answers (1)

yuyu yu
yuyu yu

Reputation: 1

const deviation = 0.001;
let k = event.transform.k;
xScale.range([0, width].map(d => event.transform.applyX(d)));
if (Math.abs(k - 1) < deviation) {
  xScale.range([0, width]);
}

I wrote this solution to the problem.

Upvotes: 0

Related Questions