David
David

Reputation: 492

d3 animate group by path

this is my next question to enhance original question.

So what I am trying is to animate triangle mark and text tooltip along the filling path.

var chart = d3.select("#speedometer");

const arc = d3
  .arc()
  .outerRadius(120)
  .innerRadius(90)
  .startAngle(-Math.PI / 2);

chart
  .append("path")
  .datum({
    endAngle: Math.PI / 2
  })
  .attr("transform", "translate(160, 180)")
  .attr("class", "background")
  .style("fill", "#495270")
  .attr("d", arc);

const mainGroup = chart.append('g')
  .datum({
    endAngle: -Math.PI / 2
  });

const triangle = mainGroup
  .append('path')
  .attr("d", "M3.937,0,7.873,14H0Z")
  .datum({
    endAngle: -Math.PI / 2
  })

const text = mainGroup.append('text')
  .text('hello there')
  .attr('transform', 'rotate(180)')
  .datum({
    endAngle: -Math.PI / 2
  })
  .transition()
  .duration(3000)
  .attrTween("transform", function(d) {
    const topVal = (300 / 2 - 16);
    const interpolate = d3.interpolate(d.endAngle, newAngle);
    return function(t) {
      const angleRadians = interpolate(t);
      const angleDegrees = 360 * angleRadians / (2 * Math.PI);
      return `
            rotate(${angleDegrees})
          `;
    };

  });

const newAngle = (70 / 100) * Math.PI - Math.PI / 2;

const foreground = chart
  .append("path")
  .datum({
    endAngle: -Math.PI / 2
  })
  .style("fill", "rgb(50, 188, 228)")
  .attr("transform", "translate(160, 180)")
  .attr("d", arc);

foreground
  .transition()
  .duration(3000)
  .attrTween("d", function(d) {
    const interpolate = d3.interpolate(d.endAngle, newAngle);
    return function(t) {
      d.endAngle = interpolate(t);
      return arc(d);
    };
  });

mainGroup
  .transition()
  .duration(3000)
  .attrTween("transform", function(d) {
    const topVal = (300 / 2 - 16);
    const interpolate = d3.interpolate(d.endAngle, newAngle);
    return function(t) {
      const angleRadians = interpolate(t);
      const angleDegrees = 360 * angleRadians / (2 * Math.PI);
      return `
            translate(158 176)
            rotate(${angleDegrees + 180} 3.5 7)
            translate(0 ${topVal})
          `;
    };
  });

function pathTween(path) {
  const length = path.node().getTotalLength(); // Get the length of the path
  const r = d3.interpolate(0, length); // Set up interpolation from 0 to the path length
  return function(t) {
    const point = path.node().getPointAtLength(r(t)); // Get the next point along the path
    d3
      .select(this) // Select the circle
      .attr("transform", `translate(${point.x}, ${point.y})`);
  };
}
.main-wrapper {
  max-width: 80%;
  margin: 20px auto;
}

.element {
  display: flex;
  flex-flow: column nowrap;
  margin-bottom: 20px;
  border: 1px solid rgba(0, 0, 0, 0.4);
  padding: 20px;
  border-radius: 6px;
}

.element .title {
  margin-bottom: 4px;
  font-weight: 500;
}

.element .description {
  margin-bottom: 10px;
  color: rgba(0, 0, 0, 0.4);
}

#speedometer {
  width: 300px;
  height: 300px;
  overflow: visible !important;
}

canvas {
  width: 100%;
  height: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<div class="main-wrapper">
  <section class="ui-section">
    <div class="element">
      <div class="title">
        Speedometr
      </div>

      <div class="content">
        <svg id="speedometer" viewbox="0 0 300 300"></svg>
      </div>
    </div>
  </section>
</div>

So the main problem here is rotating text. It supposes to be in a normal position. And how should animation be in this case? How I can animate the whole group and control each node??

Upvotes: 3

Views: 104

Answers (1)

Ruben Helsloot
Ruben Helsloot

Reputation: 13129

Firstly, you can offset the turning by using 180 - angleDegrees instead of angleDegrees for the text.

Secondly, you probably want the text to be at about the same distance from the arc at all times, not too close so it overlaps, or too far away because it looks weird. For this, one solution is to make the node text-anchor: middle, and then position it during the transition.

var chart = d3.select("#speedometer");

const arc = d3
  .arc()
  .outerRadius(120)
  .innerRadius(90)
  .startAngle(-Math.PI / 2);

chart
  .append("path")
  .datum({
    endAngle: Math.PI / 2
  })
  .attr("transform", "translate(160, 180)")
  .attr("class", "background")
  .style("fill", "#495270")
  .attr("d", arc);

const mainGroup = chart.append('g')
  .datum({
    endAngle: -Math.PI / 2
  });

const triangle = mainGroup
  .append('path')
  .attr("d", "M3.937,0,7.873,14H0Z")
  .datum({
    endAngle: -Math.PI / 2
  })

const text = mainGroup.append('text')
  .text('hello there')
  .datum({
    endAngle: -Math.PI / 2
  })
  .attr("text-anchor", "middle") // to more easily position the text
  .transition()
  .duration(3000)
  .attrTween("transform", function(d) {
    const topVal = (300 / 2 - 16);
    const interpolateAngle = d3.interpolate(d.endAngle, newAngle);

    // We want to add some offset so the text is always easily visible
    // Think about what happens when the following functions are called with
    // angles -90, -45, 0, 45, 90
    const textWidth = this.getBBox().width;
    const offsetX = function(angle) {
      return (angle / 90) * textWidth / 2;
    };

    const offsetY = function(angle) {
      if (angle < 0) {
        return offsetY(-angle);
      }
      
      // The -4 and -3 are a little bit trial and error, and can be
      // tweaked further to
      return -4 + (1 - angle / 90) * -3;
    };

    return function(t) {
      const angleRadians = interpolateAngle(t);
      const angleDegrees = 360 * angleRadians / (2 * Math.PI);

      return `
        translate(0 15)
        rotate(${180 - angleDegrees})
        translate(${offsetX(angleDegrees)} ${offsetY(angleDegrees)})
      `;
    };
  });

const newAngle = (70 / 100) * Math.PI - Math.PI / 2;

const foreground = chart
  .append("path")
  .datum({
    endAngle: -Math.PI / 2
  })
  .style("fill", "rgb(50, 188, 228)")
  .attr("transform", "translate(160, 180)")
  .attr("d", arc);

foreground
  .transition()
  .duration(3000)
  .attrTween("d", function(d) {
    const interpolate = d3.interpolate(d.endAngle, newAngle);
    return function(t) {
      d.endAngle = interpolate(t);
      return arc(d);
    };
  });

mainGroup
  .transition()
  .duration(3000)
  .attrTween("transform", function(d) {
    const topVal = (300 / 2 - 16);
    const interpolate = d3.interpolate(d.endAngle, newAngle);
    return function(t) {
      const angleRadians = interpolate(t);
      const angleDegrees = 360 * angleRadians / (2 * Math.PI);
      return `
            translate(158 176)
            rotate(${angleDegrees + 180} 3.5 7)
            translate(0 ${topVal})
          `;
    };
  });

function pathTween(path) {
  const length = path.node().getTotalLength(); // Get the length of the path
  const r = d3.interpolate(0, length); // Set up interpolation from 0 to the path length
  return function(t) {
    const point = path.node().getPointAtLength(r(t)); // Get the next point along the path
    d3
      .select(this) // Select the circle
      .attr("transform", `translate(${point.x}, ${point.y})`);
  };
}
.main-wrapper {
  max-width: 80%;
  margin: 20px auto;
}

.element {
  display: flex;
  flex-flow: column nowrap;
  margin-bottom: 20px;
  border: 1px solid rgba(0, 0, 0, 0.4);
  padding: 20px;
  border-radius: 6px;
}

.element .title {
  margin-bottom: 4px;
  font-weight: 500;
}

.element .description {
  margin-bottom: 10px;
  color: rgba(0, 0, 0, 0.4);
}

#speedometer {
  width: 300px;
  height: 300px;
  overflow: visible !important;
}

canvas {
  width: 100%;
  height: 100%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<div class="main-wrapper">
  <section class="ui-section">
    <div class="element">
      <div class="title">
        Speedometr
      </div>

      <div class="content">
        <svg id="speedometer" viewbox="0 0 300 300"></svg>
      </div>
    </div>
  </section>
</div>

Upvotes: 3

Related Questions