Search code examples
svgd3.jscss-transforms

How to properly transition when reflecting an element with d3?


I have a path element that I am updating with new data. It is then repositioned and also reflected. All of that is working correctly. The part that is not working correctly is the transition. Due to the way I am reflecting (using scale and translate), it moves past the correct position before returning.

How can I transition from the initial x-axis position to the new x-axis position without moving past the new position?

jsFiddle

HTML:

<button id="reflect">reflect</button>
<div id="container"></div>

JS:

import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

const width = 500;
const height = 200;

const x = d3.scaleLinear()
  .domain([0, 100])
  .range([0, width]);

const y = d3.scaleLinear()
  .domain([0, 50])
  .range([height, 0]);

const svg = d3.create("svg")
  .attr("width", width)
  .attr("height", height);

svg.append("g")
  .attr("transform", `translate(0,${height})`)
  .call(d3.axisBottom(x));

svg.append("g")
  .call(d3.axisLeft(y));

const data = [{
  x: 60,
  y: 20
}];
const reflectedData = [{
  x: 30,
  y: 20
}];

svg.selectAll('arrow')
  .data(data)
  .join('path')
  .attr('class', 'arrow')
  .attr('d', 'M0,0 L80,0 L80, 50z')

d3.select('#reflect')
  .on('click', () => {
    svg.selectAll('.arrow')
      .data(reflectedData)
    updateArrow(2000)
  })

updateArrow(0);

function updateArrow(duration) {
  const midpoint = x.domain()[1] / 2

  svg.selectAll('.arrow')
    .transition()
    .duration(duration)
    .style('scale', d => d.x < midpoint ? '-1 1' : '1 1')
    .style('transform', d => {
      const translateX = d.x < midpoint ? -x(d.x) : x(d.x);
      return `translate(${translateX}px, ${y(d.y)}px)`;
    })
}

container.append(svg.node());

Solution

  • Two problems:

    1. transform doesn't really play nice with other transform-like attributes like scale. (At least, it's very hard to reason about.) Much easier to just do everything in transform.
    2. The translation shouldn't change depending on whether d.x < midpoint; only the scale should.
    function updateArrow(duration) {
      const midpoint = x.domain()[1] / 2
    
      svg.selectAll('.arrow')
        .transition()
        .duration(duration)
        .style('transform', d =>
          `translate(${x(d.x)}px, ${y(d.y)}px) scaleX(${d.x < midpoint ? -1 : 1})`
        )
    }
    

    The triangle flips over the vertical as it moves from right to left.