向 d3.js 个动画气泡图添加轨迹

Adding traces to d3.js animated bubble chart

我正在尝试构建一个动画时间序列图表,该图表在移动点之后显示 'trace' 或蜗牛轨迹。我一直在尝试集成 KoGor 的 http://bl.ocks.org/KoGor/8163022 但运气不佳 - 我认为问题出在 tweenDash() - 原始功能是为单个跟踪设计的 - 每个公司都有一个。 下面附上了一个工作示例——时间序列清理和可移动数据标签有效,只是跟踪方面无效。

谢谢,

RL

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.10/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<body bgcolor="#000000"> 
<title>BPS</title>
<style>

@import url(style.css);

#chart {
  margin-left: -40px;
  height: 506px;
  display:inline;
}

#buffer {
 width: 100px;
 height:506px;
 float:left;
}
text {
  font: 10px sans-serif;
  color: #ffffff;

}

.dot {
  stroke: #000;
}

.axis path, .axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.label {
  fill: #777;
}

.year.label {
  font: 900 125px "Helvetica Neue";
  fill: #ddd;
}

.year.label.active {
  fill: #aaa;
}

.overlay {
  fill: none;
  pointer-events: all;
  cursor: ew-resize;
}

</style>


<div>
<div id="buffer"></div><div id="chart"></div>
</div>


<script src="d3.v3.min.js"></script>
<script>

var source = '[{"name":"ABCD","AUM":[[2010,1000.6],[2011,1200.6],[2012,1300.1],[2013,1400.5],[2014,1600.0]],"AUA":[[2010,3000.6],[2011,3300.2],[2012,4000.0],[2013,4500.8],[2014,6000.3]],"marketPercentage":[[2010,40.4],[2011,39.7],[2012,38.5],[2013,37.1],[2014,36.5]],"fill":[[2010,0],[2011,-1],[2012,-1],[2013,-1],[2014,-1]],"xOffset":[[2010,5],[2011,5],[2012,5],[2013,5],[2014,5]],"yOffset":[[2010,-30],[2011,-20],[2012,-20],[2013,-20],[2014,-10]]},{"name":"EFGH","AUM":[[2010,32.8],[2011,43.2],[2012,58.3],[2013,78.8],[2014,92]],"AUA":[[2010,327.3],[2011,439.3],[2012,547.0],[2013,710.0],[2014,824.0]],"marketPercentage":[[2010,1.0],[2011,1.2],[2012,1.5],[2013,1.8],[2014,1.9]],"fill":[[2010,0],[2011,1],[2012,1],[2013,1],[2014,1]],"xOffset":[[2010,5],[2011,5],[2012,5],[2013,5],[2014,5]],"yOffset":[[2010,-10],[2011,-10],[2012,-10],[2013,-10],[2014,-10]]},{"name":"HIJK","AUM":[[2010,0.1],[2011,0.5],[2012,1.2],[2013,2.4],[2014,2.6]],"AUA":[[2010,159.6],[2011,176.7],[2012,199.9],[2013,235.1],[2014,269.0]],"marketPercentage":[[2010,0.1],[2011,0.1],[2012,0.1],[2013,0.1],[2014,0.1]],"fill":[[2010,0],[2011,0],[2012,0],[2013,1],[2014,1]],"xOffset":[[2010,5],[2011,5],[2012,5],[2013,5],[2014,5]],"yOffset":[[2010,-10],[2011,-10],[2012,-10],[2013,-10],[2014,-10]]}]';


// Various accessors that specify the four dimensions of data to visualize.
function x(d) { return d.AUM; }
function y(d) { return d.AUA; }
function xo(d) {return d.xOffset; }
function yo(d) {return d.yOffset; }
function radius(d) { return d.marketPercentage; }
function key(d) { return d.name; }

// Chart dimensions.


var margin = {top: 19.5, right: 19.5, bottom: 19.5, left: 39.5},
    width = 960 - margin.right,
    height = 500 - margin.top - margin.bottom;

// Various scales. These domains make assumptions of data, naturally.
var xScale = d3.scale.linear().domain([0, 2000]).range([0, width]),
    yScale = d3.scale.linear().domain([0, 5000]).range([height, 0]),
    radiusScale = d3.scale.sqrt().domain([0, 500]).range([0, 40]),
    colorScale = d3.scale.category10();

// The x & y axes.
var xAxis = d3.svg.axis().orient("bottom").scale(xScale).ticks(12, d3.format(",d")),
    yAxis = d3.svg.axis().scale(yScale).orient("left");

// Create the SVG container and set the origin.
var svg = d3.select("#chart").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// Add the x-axis.
svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
 .style("fill", "#FFFFFF")
    .call(xAxis);

// Add the y-axis.
svg.append("g")
    .attr("class", "y axis")
 .style("fill", "#FFFFFF")
    .call(yAxis);

// Add an x-axis label.
svg.append("text")
    .attr("class", "x label")
    .attr("text-anchor", "end")
 .style("fill", "#FFFFFF") 
    .attr("x", width)
    .attr("y", height - 6);
    //.text("income per capita, inflation-adjusted (dollars)");

// Add a y-axis label.
svg.append("text")
    .attr("class", "y label")
    .attr("text-anchor", "end")
    .attr("y", 6)
    .attr("dy", ".75em")
    .style("fill", "#FFFFFF") 

    .attr("transform", "rotate(-90)")
 //   .text("life expectancy (years)")
  ;

// Add the year label; the value is set on transition.
var label = svg.append("text")
    .attr("class", "year label")
    .attr("text-anchor", "end")
    .attr("y", height - 24)
    .attr("x", width)
    .text(2010);



//d3.json("investments_v04ANON.json", function(companies) {
 
companies = JSON.parse(source)

  // A bisector since many company's data is sparsely-defined.
  var bisect = d3.bisector(function(d) { return d[0]; });

  // Add a dot per company. Initialize the data at 2010, and set the colors.
  var dot = svg.append("g")
      .attr("class", "dots")
    .selectAll(".dot")
      .data(interpolateData(2010))
    .enter().append("circle")
      .attr("class", "dot")
//      .style("fill", function(d) { return colorScale(color(d)); })
      .style("fill", function(d) {return colorScale(interpolateData(2010)) })
      .call(position)
      .sort(order);
   
   
  var lineTraces = svg.append("path")
    .attr("class", "lineTrace")
  .selectAll(".traces")
  .attr("stroke-width", 2)
  .attr("stroke", "grey")
  .data(interpolateData(2010));


   //yields a mouseover label - "title" precludes need for separate mouseover event.
//  dot.append("title")
//   .text(function(d) { return d.name; });
//.text(function(d) {return d.AUM});
   
var theLabel = svg.append("g")
   .attr("class", "texts")
   .selectAll(".theLabel")
   .data(interpolateData(2010))
   .enter().append("text")
   .attr("class", "text")
   .text("hey")
   .call(position2);

  // Add an overlay for the year label.
  var box = label.node().getBBox();

  var overlay = svg.append("rect")
        .attr("class", "overlay")
        .attr("x", box.x)
        .attr("y", box.y)
        .attr("width", box.width)
        .attr("height", box.height)
        .on("mouseover", enableInteraction);

  // Start a transition that interpolates the data based on year.
  svg.transition()
      .duration(30000)
      .ease("linear")
      .tween("year", tweenYear)
   .attrTween("stroke-dasharray", tweenDash)
      .each("end", enableInteraction);

  // Positions the dots based on data.
function position(dot) {
    dot .attr("cx", function(d) { return xScale(x(d)); })
        .attr("cy", function(d) { return yScale(y(d)); })
        .attr("r", function(d) { return radiusScale(radius(d)); })
  .style("fill", function(d) {return d.fill>0 ? "green" : "red"} );//{return d.fill});
  }
    

//function  from: http://bl.ocks.org/KoGor/8163022
  function tweenDash() {
    var i = d3.interpolateString("0," + 5, 5 + "," + 5); // interpolation of stroke-dasharray style attr
 //   var l = path.node().getTotalLength();
//    var i = d3.interpolateString("0," + l, l + "," + l); // interpolation of stroke-dasharray style attr
    
 return function(t) {
      var marker = d3.select(".dots");
//      var p = path.node().getPointAtLength(t * l);
      var p = lineTraces.node().getPointAtLength(t * 5);
      marker.attr("transform", "translate(" + p.x + "," + p.y + ")");//move marker
      return i(t);
    }
  }

 
function position2(theLabel) {
theLabel.attr("x", function(d) { return xScale(x(d)) + xo(d); })
        .attr("y", function(d) { return yScale(y(d)) + yo(d); })
  .attr("text-anchor", "end")
  .style("fill", "#FFFFFF")
  .text(function(d) { return d.name + ": AUM:" + Math.round(d.AUM) + ", AUA: " + Math.round(d.AUA) });//{return d.fill});
  }

  // Defines a sort order so that the smallest dots are drawn on top.
function order(a, b) {
    return radius(b) - radius(a);
  }

  // After the transition finishes, you can mouseover to change the year.
  function enableInteraction() {
    var yearScale = d3.scale.linear()
        .domain([2010, 2014])
        .range([box.x + 10, box.x + box.width - 10])
        .clamp(true);

    // Cancel the current transition, if any.
    svg.transition().duration(0);

    overlay
        .on("mouseover", mouseover)
        .on("mouseout", mouseout)
        .on("mousemove", mousemove)
        .on("touchmove", mousemove);

    function mouseover() {
      label.classed("active", true);
   }

    function mouseout() {
      label.classed("active", true);
      label.classed("active", false);
    }

    function mousemove() {
      displayYear(yearScale.invert(d3.mouse(this)[0]));
    }
  }

  // Tweens the entire chart by first tweening the year, and then the data.
  // For the interpolated data, the dots and label are redrawn.
  function tweenYear() {
    var year = d3.interpolateNumber(2010, 2014);
    return function(t) { displayYear(year(t)); };
  }

  // Updates the display to show the specified year.
  function displayYear(year) {
    dot.data(interpolateData(year), key).call(position).sort(order);
 theLabel.data(interpolateData(year), key).call(position2).sort(order);
    label.text(Math.round(year));
  }

  // Interpolates the dataset for the given (fractional) year.
  function interpolateData(year) {
    return companies.map(function(d) {
      return {
//  name: d.name + ": AUM:" + interpolateValues(d.AUM, year) + ", AUA: " + interpolateValues(d.AUA, year),
//  name: d.name + ": AUM:" + d.AUM + ", AUA: " + d.AUA, 
//        name: interpolateValues(d.AUM, year),
        name: d.name,
        AUM: interpolateValues(d.AUM, year),
        marketPercentage: interpolateValues(d.marketPercentage, year),
        AUA: interpolateValues(d.AUA, year),
  fill: interpolateValues(d.fill, year),
  xOffset: interpolateValues(d.xOffset, year),
  yOffset: interpolateValues(d.yOffset, year)
      };
    });
  }

  // Finds (and possibly interpolates) the value for the specified year.
  function interpolateValues(values, year) {
    var i = bisect.left(values, year, 0, values.length - 1),
        a = values[i];
    if (i > 0) {
      var b = values[i - 1],
          t = (year - a[0]) / (b[0] - a[0]);
      return a[1] * (1 - t) + b[1] * t;
    }
    return a[1];
  };
//});

</script>

马克 - 您构建的第二个版本运行良好。我现在正在尝试解决各个线段。我已经添加了一个属性 'toggleSwitch' 但下面的代码运行 1x 并且只捕获对象的初始状态。

  var lineTraces = svg.append("g")
        .selectAll(".traces")
        .data([0,1,2,4,5,6,7,8,9,10,11,12])
        .enter()
        .append("path")
            .attr("stroke-width", 2)
        .attr("stroke", "grey")
        .attr("class", "lineTrace")
        .attr("d", line)
        .each(function(d,i){
          d3.select(this)
            .datum([someData[i]])
            .attr("nothing", function(i) {console.log(i[0])})
              .attr("d", line)
              .style("stroke-dasharray", function(i) {return (i[0]["toggleSwitch"]<0 ? "0,0": "3,3")})
        });

控制台日志,每个对象一个:

Object { name: "TheName", Impact: 120, bubbleSize: 30.4, YoY: 11, toggleSwitch: 0, xOffset: 5, yOffset: -30 }

您链接到的示例有一个预先建立的路径,然后在其上添加了 "stroke-dasharray"。您的第一个问题是您需要为每个公司建立该路径。然后就可以补间了。

// set up a line to create the path
var line = d3.svg.line()
  .x(function(d) { return xScale(x(d)); })
  .y(function(d) { return yScale(y(d)); })
  .interpolate("basis");

// for each company add the path
var lineTraces = svg.append("g")
  .selectAll(".traces")
  .attr("fill","red")
  .data([0,1,2]) // 3 companies
  .enter()
  .append("path")
  .attr("stroke-width", 2)
  .attr("stroke", "grey")
  .attr("class", "lineTrace")
  .each(function(d,i){
     // get the line data and add path
     var lineData = [interpolateData(2010)[i],interpolateData(2011)[i],
                     interpolateData(2012)[i],interpolateData(2013)[i],interpolateData(2014)[i]];
      d3.select(this)
        .datum(lineData)
        .attr("d", line);
    });

现在在每个路径上设置转换:

lineTraces.each(function(){
  var path = d3.select(this);
  path.transition()
    .duration(30000)
    .ease("linear")
    .attrTween("stroke-dasharray", tweenDash)
});

tweenDash 在哪里:

function tweenDash() {
  var l = lineTraces.node().getTotalLength();
  var i = d3.interpolateString("0," + l, l + "," + l); // interpolation of stroke-dasharray style attr    
  return function(t) {
    var p = lineTraces.node().getPointAtLength(t);
    return i(t);
  }
}

这是一个 example

你会发现它并不完美,时机不对。如果我有更多时间,我会尝试回来理顺它。

编辑

昨晚考虑了一下,我突然意识到有一种更简单、更简洁的方法来添加跟踪。不要预先定义路径,然后 attrTweening "stroke-dasharray",而是边走边构建路径:

var someData = interpolateData(2010);
// add the paths like before
var lineTraces = svg.append("g")
  .selectAll(".traces")
  .data([0,1,2])
  .enter()
  .append("path")
  .attr("stroke-width", 2)
  .attr("stroke", "grey")
  .attr("class", "lineTrace")
  .attr("d", line)
  .each(function(d,i){
    d3.select(this)
      .datum([someData[i]])
      .attr("d", line);
  });

// Tweens the entire chart by first tweening the year, and then the data.
// For the interpolated data, the dots and label are redrawn.
function tweenYear() {
  var year = d3.interpolateNumber(2010, 2014);
  // added "addTrace" function
  return function(t) { addTrace(year(t)); displayYear(year(t)); };
}

// append the data and draw the path
function addTrace(year){
  var thisData = interpolateData(year);
  lineTraces.each(function(d,i){
    var trace = d3.select(this);
    trace.datum().push(thisData[i]);
    trace.attr("d", line);
  });
}

这个produces much better results.