3
votes

I am trying to implement both zooming and brushing in my d3 (v4) chart.

I have got them both working separately, but my problem comes when I try to implement both features on the same chart.

My scenario is the following: 1. The user uses the brush to show a specific region of the chart. 2. They then zoom/pan, but this causes the view to jump back to the old location, because the stored zoom transform is not aware of the changes made by the brushing.

My understanding is that the current zoom transform (scale+translation) is stored inside the DOM element in an internal __zoom attribute. The zoom plugin automatically adjusts this whenever you interact with the element (e.g. by scrolling the mouse wheel).

I see that you can use d3.zoomTransform to get the current zoom transform for an element.

How can I reset/remove the stored zoom transform (e.g. after panning, so that any subsequent zooming carries on from where the brushing left off)?

Note: I don't want to have to change the zoom, but rather just update the stored zoom transform to treat that new scale as the "identity". This is important because I want to be able to smoothly transition from one scale to another when brushing etc.

1
Do you solved? I have just the same problem! - Aral Roca
@AralRoca I've added an answer which shows what I ended up doing - Matt Wilson

1 Answers

4
votes

The way I got around this in the end is:

  1. in the zoom handler, use transform.rescaleX() to get a new transformed scale
  2. Update the main scale's domain based on the transformed scale
  3. Update the x-axis based on the scale
  4. Reset the transform on the element to d3.zoomIdentity.

The key thing here is that after the scale has been updated, the stored transform on the DOM element is always put back to identity (i.e. scale=1, translate=0,0).

That means that we don't need to worry about brushing/zooming or any programatic changes to the scale on different elements won't conflict or have different values from each other. We effectively just keep applying very small scale factors to the element.

In terms of a code example, here are the relevant parts from my working chart:

// class contains:
// this.xScale - stored scale for x-axis
// this.xAxis - a d3 Axis
// this.xAxisElement - a d3 selection for the element on which the x-axis is drawn
// this.zoomX - a d3 ZoomBehavior
// this.chartElement - a d3 selection for the element on which the zooming is added

protected setupZooming(): void {
  this.zoomX = d3.zoom().on('zoom', () => { this.onZoomX(); });

  this.zoomXElement = this.xAxisElement
    .append('rect')
      .attr('fill', 'none')
      .style('pointer-events', 'all')
      .attr('width', this.width)
      .attr('height', this.margin.bottom)
      .call(this.zoomX);
}

onZoomX(): void {
  const transform: d3.ZoomTransform = d3.event.transform;

  if (transform.k === 1 && transform.x === 0 && transform.y === 0) {
    return;
  }

  const transformedXScale = transform.rescaleX<any>(this.xScale);

  const from = transformedXScale.domain()[0];
  const to = transformedXScale.domain()[1];

  this.zoomXTo(from, to, false);

  this.chartElement.call(this.zoomX.transform, d3.zoomIdentity);
}

zoomXTo(x0: Date, x1: Date, animate: boolean): void {
  const transitionSpeed = animate ? 750 : 0;

  this.xScale.domain([x0, x1]);

  this.xAxisElement.transition().duration(transitionSpeed).call(this.xAxis);
  this.updateData(transitionSpeed);
}

updateData(transitionSpeed: number): void {
  // ...
}

Apologies if this extract isn't easy to follow outside of the context of the rest of my code, but hopefully it is still helpful.