Jason S
Jason S

Reputation: 189626

Using adjacent data values in d3

I am using D3 to visualize some data that updates with time, and I have a list of (x,y) coordinates; for example:

[[0.1,0.2],
 [0.3,0.4],
 [0.5,0.4],
 [0.7,0.2]]

I would like to draw triangles from (0,0) to each of the pairs of adjacent coordinates, for example, the first triangle would have coordinates (0,0), (0.1,0.2), (0.3,0.4) and the second triangle would have coordinates (0,0), (0.3,0.4), (0.5,0.4) and so on.

My question is whether there is a way to access "neighboring" values in D3; the D3 paradigm seems to be to pass in a function that gets access to each data value separately. So I was able to do this, but only by explicitly constructing a new data set of the triangle coordinates from the entire data set of the individual points:

var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 500 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;
    
// add the graph canvas to the body of the webpage
var svg = d3.select("div#plot1").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom);
var axis = svg.append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    
var xsc = d3.scaleLinear()
          .domain([-2, 2])  // the range of the values to plot
          .range([ 0, width ]);        // the pixel range of the x-axis

var ysc = d3.scaleLinear()
          .domain([-2, 2])
          .range([ height, 0 ]);
var closedLine = d3.line()
   .x(function(d){ return xsc(d[0]); })
   .y(function(d){ return ysc(d[1]); })
   .curve(d3.curveLinearClosed);

function attrfunc(f,attr) {
  return function(d) {
    return f(d[attr]);
  };
}


function doit(data)
{
  var items = axis.selectAll("path.item")
    .data(data);
  items.enter()
      .append("path")
        .attr("class", "item")
      .merge(items)
        .attr("d", attrfunc(closedLine, "xy"))
        .attr("stroke", "gray")
        .attr("stroke-width", 1)
        .attr("stroke-opacity", function(d) { return 1-d.age;})
        .attr("fill", "gray")
        .attr("fill-opacity", function(d) {return 1-d.age;});
  items.exit().remove();
}

var state = {
  t: 0,
  theta: 0,
  omega: 0.5,
  A: 1.0,
  N: 60,
  history: []
}
d3.timer(function(elapsed)
{
  var S = state;
  if (S.history.length > S.N)
    S.history.shift();
  var dt = Math.min(0.1, elapsed*1e-3);
  S.t += dt;
  S.theta += S.omega * dt;
  var sample = {
    t: S.t,
    x: S.A*(Math.cos(S.theta)+0.1*Math.cos(6*S.theta)),
    y: S.A*(Math.sin(S.theta)+0.1*Math.sin(6*S.theta))
  }
  S.history.push(sample);

  // Create triangular regions
  var data = [];
  for (var k = 0; k < S.history.length-1; ++k)
  {
     var pt1 = S.history[k];
     var pt2 = S.history[k+1];
		 data.push({age: (S.history.length-1-k)/S.N,
                xy:
                 [[0,0],
                  [pt1.x,pt1.y],
                  [pt2.x,pt2.y]]
               });
  }
  doit(data);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.min.js"></script>
<div id="plot1">
</div>

Upvotes: 0

Views: 87

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

You can get any point in the data array using the second argument, which is the index.

When you pass a datum to any D3 method, traditionally using a parameter named d (for datum), you are in fact using data[i], i being the current index. You can change this index to get data points before or after the current datum.

Thus, in any D3 method:

.attr("foo", function(d, i){
    console.log(d)//this is the current datum
    console.log(data[i])//this is the same current datum!
    console.log(data[i + 1])//this is the next (adjacent) datum
});

Here is a simple snippet showing this:

var data = ["foo", "bar", "baz", "foobar", "foobaz"];

var foo = d3.selectAll("foo")
  .data(data)
  .enter()
  .append("foo")
  .attr("foo", function(d, i) {
    if (data[i + 1]) {
      console.log("this datum is " + d + ", the next datum is " + data[i + 1]);
    }
  })
<script src="https://d3js.org/d3.v4.min.js"></script>

Have a look at the if statement: we have to check if there is a data[i + 1] because, of course, the last data point has no adjacent data after it.

Here is a demo using your data array:

var svg = d3.select("svg");

var scale = d3.scaleLinear()
  .domain([-1, 1])
  .range([0, 150]);

var data = [
  [0.1, 0.2],
  [0.3, 0.4],
  [0.5, 0.4],
  [0.7, 0.2]
];

var triangles = svg.selectAll("foo")
  .data(data)
  .enter()
  .append("polygon");

triangles.attr("stroke", "teal")
  .attr("stroke-width", 2)
  .attr("fill", "none")
  .attr("points", function(d, i) {
    if (data[i + 1]) {
      return scale(0) + "," + scale(0) + " " + scale(data[i][0]) + "," + scale(data[i][1]) + " " + scale(data[i + 1][0]) + "," + scale(data[i + 1][1]) + " " + scale(0) + "," + scale(0);
    }
  })
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="150" height="150"></svg>

PS: I'm not using your snippet because I'm drawing the triangles using polygons, but the principle is the same.

Upvotes: 1

Related Questions