rafaeelneto
rafaeelneto

Reputation: 43

How to proper zoom and pan only the y-axis in D3 v5 y-axis

I`m developing visualization with d3 v6 that's basically use a set of entries to show horizontal layers. The visualization looks like this: example of the visualization

The caveat is that as number of layers grows some layers become impossible to see due scale decreasing. So I want to make a zoom (and pan) feature, but it has to work only on the Y axis. I've search a lot and found this tutorial (https://observablehq.com/@d3/x-y-zoom) from where I've made some changes to adapt to my visualization:

      const svgHeight: any = d3.select(svgContainer.current).attr('height');
      const yScaleGlobal = d3
        .scaleLinear()
        .domain([0, maxYValues])
        .range([0, svgHeight - MARGINS.TOP - MARGINS.BOTTOM]);

      const yAxis = d3.axisLeft(yScaleGlobal).tickFormat((d: any) => `${d} m`);

      const gY = layerGroup.append('g').attr('class', 'y-axis');

      // z holds a copy of the previous transform, so we can track its changes
      let z = d3.zoomIdentity;

      // set up the ancillary zooms and an accessor for their transforms
      const zoomY = d3.zoom().scaleExtent([0.2, 5]);
      const ty = () => d3.zoomTransform(gY.node());

      gY.call(zoomY).attr('pointer-events', 'none');

      drawViz();

      const zoom = d3.zoom().on('zoom', function (e) {
        const t = e.transform;
        const k = t.k / z.k;
        const point = center(e, this);

        if (k === 1) {
          // pure translation?
          gY.call(zoomY.translateBy, 0, (t.y - z.y) / ty().k);
        } else {
          // if not, we're zooming on a fixed point
          gY.call(zoomY.scaleBy, k, point);
        }

        z = t;

        drawViz();
      });

      svg.call(zoom).call(zoom.transform, d3.zoomIdentity.scale(0.8)).node();

      function drawViz() {
        const yr = ty().rescaleY(yScaleGlobal);
        yAxis.scale(yr);
        gY.call(yAxis, yr);
        if (data) updateLayers(data, yr);
      }

      function center(event, target) {
        if (event.sourceEvent) {
          const p = d3.pointers(event, target);
          return [d3.mean(p, (d) => d[0]), d3.mean(p, (d) => d[1])];
        }
        return [svgWidth / 2, svgHeight / 2];
      }

The updateLayers function is something like this:

     const updateLayers = (data: LAYER_COMPONENT_TYPE[], yScale) => {
        const layerFill = getLayerFill(data);

        const rects = layerGroup.selectAll('rect').data(data);

        rects.exit().remove();

        const newLayers = rects
          .enter()
          .append('rect')
          .attr('x', 10)
          .style('opacity', 0.5)
          .attr('width', 300)
          .style('stroke', '#101010')
          .style('stroke-width', '1px');

        newLayers
          .merge(rects)
          .style('fill', (d: LAYER_COMPONENT_TYPE) => {
            if (!layerFill[`${d.fgdc_texture}.${d.from}`].url) {
              return layerFill[`${d.fgdc_texture}.${d.from}`];
            }
            svg.call(layerFill[`${d.fgdc_texture}.${d.from}`]);
            return layerFill[`${d.fgdc_texture}.${d.from}`].url();
          })
          .attr('y', (d: LAYER_COMPONENT_TYPE, i) => {
            if (i === 0) return yScale(d.from);
            return yScale(data[i - 1].to);
          })
          .attr('height', (d: LAYER_COMPONENT_TYPE, i) => {
            return yScale(d.to - d.from);
          });
      };

The zoom and pan works fine and the whole visualization too, but the yScale have a strange behavior. The height of the rectangles changes out of scale at zoom or pan (i've realized that the anomaly of the height is equal to the distance of "0 m" point at yScale) here you can view the problem

Anyways, I can figure out what's wrong with the code or what's causing the anomaly. So i want some help with this problem please

Edit 1: I'm using react btw

Upvotes: 1

Views: 564

Answers (1)

rafaeelneto
rafaeelneto

Reputation: 43

I've already came out with a solution (with a better and cleaner approach). This solution is based the TBD's d3 timeline implementation. Using his solution I just adapted to the y axis and to my code

    const maxYValues = d3.max(maxValues) || 0;

    const yScaleGlobal = d3
      .scaleLinear()
      .domain([0, maxYValues])
      .range([0, svgHeight - MARGINS.TOP - MARGINS.BOTTOM]);

    const yAxis = d3.axisLeft(yScaleGlobal).tickFormat((d: any) => `${d} m`);

    const gY = litoligicalGroup.select(`.${styles.yAxis}`).call(yAxis);

    const spanY = (d) => {
      if (d.thickness) return yScaleGlobal(0) - 10;
      return yScaleGlobal(d.from);
    };

    const spanH = (d) => {
      if (d.thickness) return 10;
      return yScaleGlobal(d.to - d.from);
    };

    const zoom = d3
      .zoom()
      .scaleExtent([0.2, 5])
      .on('zoom', (e) => {
        const transform = e.transform;

        gY.call(yAxis.scale(transform.rescaleY(yScaleGlobal)));
        // the viz group is where all the layers are attached
        vizGroup
          .selectAll('rect')
          .attr('y', (d) => {
            if (!d) return null;
            return transform.applyY(spanY(d));
          })
          .attr('height', (d) => {
            if (!d) return null;
            return transform.k * spanH(d);
          });
      });

    drawProfile();

    svg.call(zoom);

    function drawProfile() {
      if (geologicData) updateGeology(geologicData, yScaleGlobal);
      if (constructionData) updatePoco(constructionData, yScaleGlobal);
    }

Upvotes: 1

Related Questions