Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3js - Getting max value from d3.line() for a specific domain

I am making a multi-line chart and I have implemented a brush to be able to zoom into a specific domain on the x-axis. However, when I zoom in I want the y-axis to scale along so that its domain goes from [0, maxY], where maxY is the maximum y-value for the current selection on the x-axis. To generate the lines I am using d3.line() (which has the connection between the x and y values). This is how I currently calculate the maxY value:

//Find max and min values in data to set the domain of the y-axis
var maxArray = updatedData.map(function(variable){
  //First map the values in the array to a new array
  var valuesArray = variable.values.map(function(d){
    return d.value;
  })
  //Find max value in array
  return Math.max(...valuesArray);
});
var maxY = Math.max(...maxArray);

And here is where I set the scales and create the d3.line():

var xScale = d3.scaleTime()
  .range([0, chartWidth]);
var yScale = d3.scaleLinear()
  .domain([0, maxY])
  .range([chartHeight, 0]);

var brush = d3.brushX()
  .on("end", brushend);

var line = d3.line()
  .curve(d3.curveBasis)
  .x(function(d) {return xScale(d.date)})
  .y(function(d) {return yScale(d.value)})

//Save this to be able to zoom back out
var originalDomain = [new Date(data[0].Timestamp), new Date(data[data.length-1].Timestamp)];
xScale.domain(originalDomain);

Here is the code where I set the new xScale.domain() and zoom in on that interval (which is called when the brushing is ended):

function brushend(){
  //sourceEvent - the underlying input event, such as mousemove or touchmove.
  if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
  var brushInterval = d3.event.selection; //The interval of the current brushed selection
  //If the function is called with no selection: ignore
  if(!brushInterval) return;
  //Enable reset button
  resetButton.attr("disabled", null)
    .on("click", resetAxis);

  var newDomain = brushInterval.map(xScale.invert, xScale);

  //TODO: Find max and min values in data to set the domain of the y-axis

  xScale.domain(newDomain);
  chart.selectAll(".line")
    .transition()
    .duration(1000)
      .attr("d", function(d){ return line(d.values)});
  chart.select(".x-axis")
    .transition()
    .duration(1000)
      .call(xAxis);
  //Remove the visual brush
  d3.select(".brush").call(brush.move, null);
}

What I would like to do is to find the maximum y-value in the currently selected domain. I know that I can filter the data values to remove the ones that are not in the currently selected domain and then calculate the maximum value from them (like I did for the original domain). But it seems like there should be an easier solution to this. I didn't find any function in the documentation for d3.line() that could calculate max values.

Is there any easy way to calculate max value from d3.line()?

Thanks

like image 620
martin36 Avatar asked Oct 18 '22 08:10

martin36


1 Answers

There is not really an easier solution to this as you somehow have to filter the values to only take into account the ones which are in your selected x domain. However, using two nested calls to d3.max() you can at least give it a pleasant look and spare some iterations by avoiding an additional call to .filter(). Since d3.max() will ignore null values you can use it to filter your values by returning null if the current datum is outside of the x domain's boundaries. To get the maximum value you can use something like the following:

const maxY = xDomain => d3.max(updatedData, variable => 
  d3.max(
    variable.values,
    v => v.Timestamp >= xDomain[0] && v.Timestamp <= xDomain[1] ? v.value : null
  )
);

Have a look at the following snippet for a working demo:

var updatedData = [{
  values: [{Timestamp:0, value:1},{Timestamp:1, value:5},{Timestamp:2, value:10},{Timestamp:3, value:3},{Timestamp:4, value:30}]
}, {
  values: [{Timestamp:0, value:19},{Timestamp:1, value:12},{Timestamp:2, value:13},{Timestamp:3, value:8},{Timestamp:4, value:50}]
}];

const maxY = xDomain => d3.max(updatedData, variable => 
  d3.max(
    variable.values,
    v => (!xDomain || v.Timestamp >= xDomain[0] && v.Timestamp <= xDomain[1]) ? v.value : null
  )
);

console.log(maxY());       // Default, check all values: max 50
console.log(maxY([1,3]));  // Max 13
console.log(maxY([0,3]));  // Max 19
<script src="https://d3js.org/d3.v4.js"></script>
like image 114
altocumulus Avatar answered Oct 21 '22 03:10

altocumulus