DistributionPlotExtendedData.java
package org.opentrafficsim.animation.graphs;
import java.util.Optional;
import org.djunits.value.vdouble.scalar.Duration;
import org.djutils.exceptions.Throw;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.data.DomainOrder;
import org.jfree.data.xy.IntervalXYDataset;
import org.opentrafficsim.animation.graphs.AbstractPlot.PaintState;
import org.opentrafficsim.animation.graphs.DistributionPlotExtendedData.DistributionPaintState;
import org.opentrafficsim.animation.graphs.GraphPath.Section;
import org.opentrafficsim.kpi.interfaces.GtuData;
import org.opentrafficsim.kpi.interfaces.LaneData;
import org.opentrafficsim.kpi.sampling.SamplerData;
import org.opentrafficsim.kpi.sampling.Trajectory;
import org.opentrafficsim.kpi.sampling.TrajectoryGroup;
import org.opentrafficsim.kpi.sampling.data.ExtendedDataType;
/**
* Distribution data plot for any numerical extended trajectory data. Data is collected cumulatively; each interval adds to the
* totals of the previous interval.
* <p>
* Copyright (c) 2026-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
* BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
* </p>
* @author Wouter Schakel
* @param <G> GTU data type that needs to be consistent between the sampler data and the extended data type
* @param <Z> value type that needs to be consistent between extended data type and label data
*/
public class DistributionPlotExtendedData<G extends GtuData, Z extends Number> extends AbstractPlot<DistributionPaintState>
implements IntervalXYDataset
{
/** Sampler data. */
private final SamplerData<G> samplerData;
/** Lanes registered in the sampler. */
private final GraphPath<? extends LaneData<?>> path;
/** Data type. */
private final ExtendedDataType<Z, ?, ?, ?> dataType;
/** Time of most recent update. */
private double lastUpdateTime = Double.NEGATIVE_INFINITY;
/** X-values. */
private final double[] x;
/** Step size. */
private final double dx;
/** Y-values. */
private final long[] y;
/**
* Constructor.
* @param samplerData sampler data
* @param path path
* @param dataType data type
* @param plotScheduler plot scheduler
* @param labelData label data
*/
public DistributionPlotExtendedData(final SamplerData<G> samplerData, final GraphPath<? extends LaneData<?>> path,
final ExtendedDataType<Z, ?, ?, G> dataType, final PlotScheduler plotScheduler, final LabelData<Z> labelData)
{
super(plotScheduler, labelData.caption(), Duration.ofSI(10.0), Duration.ZERO);
int n = 1 + (int) Math.round(
(labelData.maximum().doubleValue() - labelData.minimum().doubleValue()) / labelData.step().doubleValue());
this.x = new double[n];
for (int i = 0; i < n; i++)
{
this.x[i] = labelData.minimum().doubleValue() + i * labelData.step().doubleValue();
}
this.dx = labelData.step().doubleValue();
this.y = new long[n];
this.samplerData = Throw.whenNull(samplerData, "samplerData");
this.path = Throw.whenNull(path, "path");
this.dataType = Throw.whenNull(dataType, "dataType");
setChart(createChart(labelData.xLabel()));
}
/**
* Create a chart.
* @param xLabel label on x-axis
* @return JFreeChart; chart
*/
private JFreeChart createChart(final String xLabel)
{
NumberAxis xAxis = new NumberAxis(xLabel);
xAxis.setRange(this.x[0], this.x[this.x.length - 1]);
NumberAxis yAxis = new NumberAxis("Count [-]");
yAxis.setAutoRangeIncludesZero(true);
XYBarRenderer renderer = new XYBarRenderer();
XYPlot plot = new XYPlot(this, xAxis, yAxis, renderer);
return new JFreeChart(getCaption(), JFreeChart.DEFAULT_TITLE_FONT, plot, false);
}
@Override
public int getSeriesCount()
{
return 1;
}
@SuppressWarnings("rawtypes")
@Override
public Comparable getSeriesKey(final int series)
{
return Integer.valueOf(1);
}
@SuppressWarnings("rawtypes")
@Override
public int indexOf(final Comparable seriesKey)
{
return 0;
}
@Override
public DomainOrder getDomainOrder()
{
return DomainOrder.ASCENDING;
}
@Override
public int getItemCount(final int series)
{
return this.x.length - 1;
}
@Override
public Number getX(final int series, final int item)
{
return this.x[item];
}
@Override
public double getXValue(final int series, final int item)
{
return this.x[item];
}
@Override
public Number getY(final int series, final int item)
{
return this.y[item];
}
@Override
public double getYValue(final int series, final int item)
{
return this.y[item];
}
@Override
public Number getStartX(final int series, final int item)
{
return this.x[item];
}
@Override
public double getStartXValue(final int series, final int item)
{
return this.x[item];
}
@Override
public Number getEndX(final int series, final int item)
{
return this.x[item + 1];
}
@Override
public double getEndXValue(final int series, final int item)
{
return this.x[item + 1];
}
@Override
public Number getStartY(final int series, final int item)
{
return getYValue(series, item);
}
@Override
public double getStartYValue(final int series, final int item)
{
return getYValue(series, item);
}
@Override
public Number getEndY(final int series, final int item)
{
return getYValue(series, item);
}
@Override
public double getEndYValue(final int series, final int item)
{
return getYValue(series, item);
}
@Override
public GraphType getGraphType()
{
return GraphType.OTHER;
}
@Override
public String getStatusLabel(final double domainValue, final double rangeValue)
{
return " ";
}
@Override
protected DistributionPaintState emptyPaintState()
{
return new DistributionPaintState(Duration.ZERO);
}
@Override
protected void calculatePaintState(final Duration time)
{
for (Section<? extends LaneData<?>> section : this.path.getSections())
{
for (LaneData<?> lane : section.sections())
{
processLane(lane);
}
}
this.lastUpdateTime = time.si;
offerPaintState(new DistributionPaintState(time));
}
/**
* Processes a single lane while updating the time.
* @param lane lane
*/
private void processLane(final LaneData<?> lane)
{
Optional<TrajectoryGroup<G>> trajectories = this.samplerData.getTrajectoryGroup(lane);
if (trajectories.isPresent())
{
for (Trajectory<G> trajectory : trajectories.get())
{
int n = trajectory.size() - 1;
while (n >= 0 && trajectory.getT(n) > this.lastUpdateTime)
{
Z z = trajectory.getExtendedData(this.dataType, n);
double value = z == null ? Double.NaN : z.doubleValue();
if (!Double.isNaN(value) && value >= this.x[0] && value < this.x[this.x.length - 1])
{
int index = (int) Math.floor((value - this.x[0]) / this.dx);
this.y[index]++;
}
n--;
}
}
}
}
@Override
public void setAutoBoundDomain(final XYPlot plot)
{
// don't need to do anything, override to prevent exception thrown in super
}
@Override
public void setAutoBoundRange(final XYPlot plot)
{
// don't need to do anything, override to prevent exception thrown in super
}
/**
* Input container for label input.
* @param <Z> value type
* @param caption caption
* @param xLabel label on x-axis
* @param minimum minimum x-value
* @param step step value
* @param maximum maximum x-value
*/
public record LabelData<Z extends Number>(String caption, String xLabel, Z minimum, Z step, Z maximum)
{
/**
* Constructor.
* @param caption caption
* @param xLabel label on x-axis
* @param minimum minimum x-value
* @param step step value
* @param maximum maximum x-value
*/
public LabelData
{
Throw.when(maximum.doubleValue() <= minimum.doubleValue(), IllegalArgumentException.class,
"maximum must be greater than minimum");
Throw.when(step.doubleValue() <= 0.0, IllegalArgumentException.class, "step must be greater than 0");
}
}
/**
* Only contains time. The y-values are cumulative so direct internal storage suffices.
* @param getAvailableTime available time
*/
public record DistributionPaintState(Duration getAvailableTime) implements PaintState
{
}
}