如何为元素设置动画以跟随 d3 中的圆弧质心?

How to animate an element to follow an arc's centroid in d3?

我有一个简单的饼图,标签如下:

var data = [{
  label: 'Star Wars',
  instances: 20
}, {
  label: 'Lost In Space',
  instances: 32
}, {
  label: 'the Boston Pops',
  instances: 80
}, {
  label: 'Indiana Jones',
  instances: 74
}, {
  label: 'Harry Potter',
  instances: 23
}, {
  label: 'Jaws',
  instances: 10
}, {
  label: 'Lincoln',
  instances: 15
}];

svg = d3.select("svg");
canvas = d3.select("#canvas");
art = d3.select("#art");
labels = d3.select("#labels");

var pie = d3.layout.pie().value(function(d, i) {
    return d.instances;
  })
  .sort(null);

var height = 500,
  width = 500,
  labelRadius = 175;

svg.attr({
  height: height,
  width: width
});

canvas.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")");

arc = d3.svg.arc()
  .innerRadius(50)
  .outerRadius(150);

colors = d3.scale.category10();

var path = art.selectAll(".wedge").data(pie(data)).enter().append("path")
  .attr("class", "wedge")
  .attr("d", arc)
  .style("fill", function(d, i) {
    return colors(i);
  })
  .each(function(d) {
    this._current = d;
  });

enteringLabels = labels.selectAll(".label").data(pie(data)).enter();
labelGroups = enteringLabels.append("g").attr("class", "label");
labelGroups.append("circle").attr({
  x: 0,
  y: 0,
  r: 2,
  fill: "#000",
  transform: function(d, i) {
    centroid = arc.centroid(d);
    return "translate(" + arc.centroid(d) + ")";
  },
  'class': "label-circle"
});

textLines = labelGroups.append("line").attr({
  x1: function(d, i) {
    return arc.centroid(d)[0];
  },
  y1: function(d, i) {
    return arc.centroid(d)[1];
  },
  x2: function(d, i) {
    centroid = arc.centroid(d);
    midAngle = Math.atan2(centroid[1], centroid[0]);
    x = Math.cos(midAngle) * labelRadius;
    return x;
  },
  y2: function(d, i) {
    centroid = arc.centroid(d);
    midAngle = Math.atan2(centroid[1], centroid[0]);
    y = Math.sin(midAngle) * labelRadius;
    return y;
  },
  'class': "label-line"
});

textLabels = labelGroups.append("text").attr({
  x: function(d, i) {
    centroid = arc.centroid(d);
    midAngle = Math.atan2(centroid[1], centroid[0]);
    x = Math.cos(midAngle) * labelRadius;
    sign = (x > 0) ? 1 : -1
    labelX = x + (5 * sign)
    return labelX;
  },
  y: function(d, i) {
    centroid = arc.centroid(d);
    midAngle = Math.atan2(centroid[1], centroid[0]);
    y = Math.sin(midAngle) * labelRadius;
    return y;
  },
  'text-anchor': function(d, i) {
    centroid = arc.centroid(d);
    midAngle = Math.atan2(centroid[1], centroid[0]);
    x = Math.cos(midAngle) * labelRadius;
    return (x > 0) ? "start" : "end";
  },
  'class': 'label-text'
}).text(function(d) {
  return d.data.label
});

alpha = 0.5;
spacing = 12;

function relax() {
  again = false;
  textLabels.each(function(d, i) {
    a = this;
    da = d3.select(a);
    y1 = da.attr("y");
    textLabels.each(function(d, j) {
      b = this;
      // a & b are the same element and don't collide.
      if (a == b) return;
      db = d3.select(b);
      // a & b are on opposite sides of the chart and
      // don't collide
      if (da.attr("text-anchor") != db.attr("text-anchor")) return;
      // Now let's calculate the distance between
      // these elements. 
      y2 = db.attr("y");
      deltaY = y1 - y2;

      // Our spacing is greater than our specified spacing,
      // so they don't collide.
      if (Math.abs(deltaY) > spacing) return;

      // If the labels collide, we'll push each 
      // of the two labels up and down a little bit.
      again = true;
      sign = deltaY > 0 ? 1 : -1;
      adjust = sign * alpha;
      da.attr("y", +y1 + adjust);
      db.attr("y", +y2 - adjust);
    });
  });
  // Adjust our line leaders here
  // so that they follow the labels. 
  if (again) {
    labelElements = textLabels[0];
    textLines.attr("y2", function(d, i) {
      labelForLine = d3.select(labelElements[i]);
      return labelForLine.attr("y");
    });
    setTimeout(relax, 20)
  }
}

relax();

d3.selectAll("#randomize")
  .on("click", function() {
    data = _(data).map(function(v) {
      v.instances = Math.floor((Math.random() * 100) + 1);
      return v;
    });

    pie.value(function(d) {
      return d.instances;
    });

    path = path
      .data(pie(data));

    path.transition().duration(750)
      .attrTween("d", function(d) {
        var interpolate = d3.interpolate(this._current, d);
        this._current = interpolate(0);
        return function(t) {
          return arc(interpolate(t));
        };
      });
  });
.label-text {
  alignment-baseline: middle;
  font-size: 12px;
  font-family: arial, helvetica, "sans-serif";
  fill: #393939;
}
.label-line {
  stroke-width: 1;
  stroke: #393939;
}
.label-circle {
  fill: #393939;
}
#randomize {
  position: absolute;
  padding: 10px;
  z-index: 100;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore.js"></script>
<button id="randomize">Randomize</button>
<svg>
  <g id="canvas">
    <g id="art" />
    <g id="labels" /></g>
</svg>

我可以 update/randomize 使弧长动画化的数据,但我不知道如何使标签 "follow" 成为弧的质心。我知道我可能需要沿着圆弧 centroid/path 插入标签,但我似乎无法弄清楚如何开始。

click 函数中,您需要重置绑定到各种 DOM 的数据:

//update the groups with the new data
enteringLabels = labels.selectAll(".label").data(pie(data));
//update the circle with the new data
labelGroups = d3.selectAll(".label-circle").data(pie(data));
//update the lines with the new data
textLines = d3.selectAll(".label-line").data(pie(data));
//update the text with the data
textLabels = d3.selectAll(".label-text").data(pie(data));
//for transition   
labelGroups.transition().duration(1000).attr({
  transform: function(d, i) {
    centroid = arc.centroid(d);
    return "translate(" + arc.centroid(d) + ")";
  },
  'class': "label-circle"
});   
//for transition   
textLines.transition().duration(1000).attr({
  x1: function(d, i) {
    return arc.centroid(d)[0];
  },
  y1: function(d, i) {
    return arc.centroid(d)[1];
  },
  x2: function(d, i) {
    centroid = arc.centroid(d);
    midAngle = Math.atan2(centroid[1], centroid[0]);
    x = Math.cos(midAngle) * labelRadius;
    return x;
  },
  y2: function(d, i) {
    centroid = arc.centroid(d);
    midAngle = Math.atan2(centroid[1], centroid[0]);
    y = Math.sin(midAngle) * labelRadius;
    return y;
  },
  'class': "label-line"
});

textLabels.transition().duration(1000).attr({
  x: function(d, i) {
    centroid = arc.centroid(d);
    midAngle = Math.atan2(centroid[1], centroid[0]);
    x = Math.cos(midAngle) * labelRadius;
    sign = (x > 0) ? 1 : -1
    labelX = x + (5 * sign)
    return labelX;
  },
  y: function(d, i) {
    centroid = arc.centroid(d);
    midAngle = Math.atan2(centroid[1], centroid[0]);
    y = Math.sin(midAngle) * labelRadius;
    return y;
  },
  'text-anchor': function(d, i) {
    centroid = arc.centroid(d);
    midAngle = Math.atan2(centroid[1], centroid[0]);
    x = Math.cos(midAngle) * labelRadius;
    return (x > 0) ? "start" : "end";
  },
  'class': 'label-text'
}).text(function(d) {
  return d.data.label
});

工作代码here

这使得 circle/dot 遵循圆弧的质心:

label_circle = label_circle.data(pie(data));

label_circle
    .transition()
        .duration(750)
        .attrTween("transform", function(d) {
            var interpolate = d3.interpolate(this._current, d);
            this._current = interpolate(0);
            return function(t) {
                return "translate(" + arc.centroid(interpolate(t)) + ")";
            };
        });