io.fair_acc.sample.chart.TimeAxisNonLinearSample Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of samples Show documentation
Show all versions of samples Show documentation
Small sample applications to showcase the features of the chart-fx library.
The newest version!
package io.fair_acc.sample.chart;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import io.fair_acc.chartfx.XYChart;
import io.fair_acc.chartfx.axes.AxisLabelOverlapPolicy;
import io.fair_acc.chartfx.axes.spi.AxisRange;
import io.fair_acc.chartfx.axes.spi.DefaultNumericAxis;
import io.fair_acc.chartfx.axes.spi.format.DefaultTimeFormatter;
import io.fair_acc.chartfx.plugins.DataPointTooltip;
import io.fair_acc.chartfx.plugins.EditAxis;
import io.fair_acc.chartfx.plugins.XValueIndicator;
import io.fair_acc.chartfx.plugins.Zoomer;
import io.fair_acc.chartfx.renderer.spi.ErrorDataSetRenderer;
import io.fair_acc.chartfx.utils.FXUtils;
import io.fair_acc.dataset.spi.DoubleDataSet;
import io.fair_acc.dataset.spi.LimitedIndexedTreeDataSet;
import io.fair_acc.dataset.spi.fastutil.DoubleArrayList;
import io.fair_acc.dataset.utils.ProcessingProfiler;
public class TimeAxisNonLinearSample extends ChartSample {
private static final Timer timer = new Timer("TimeAxisNonLinearSampleTimer", true);
@Override
public Node getChartPanel(final Stage primaryStage) {
ProcessingProfiler.setVerboseOutputState(true);
ProcessingProfiler.setLoggerOutputState(true);
ProcessingProfiler.setDebugState(false);
final var root = new BorderPane();
final var xAxis1 = new NonLinearTimeAxis("time", "iso");
xAxis1.setThreshold(0.6);
xAxis1.setWeight(0.975);
final var yAxis1 = new DefaultNumericAxis("y-axis", "a.u.");
final var chart = new XYChart(xAxis1, yAxis1);
chart.legendVisibleProperty().set(true);
chart.getPlugins().add(new Zoomer());
chart.getPlugins().add(new EditAxis());
chart.getPlugins().add(new DataPointTooltip());
// set them false to make the plot faster
chart.setAnimated(false);
((ErrorDataSetRenderer) (chart.getRenderers().get(0))).setAllowNaNs(true);
((ErrorDataSetRenderer) (chart.getRenderers().get(0))).setPointReduction(false);
yAxis1.setAutoRangeRounding(true);
final var dataSet = new LimitedIndexedTreeDataSet("TestData", 100_000, 60);
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
final double now = System.currentTimeMillis() / 1000.0 + 1; // N.B. '+1'
dataSet.add(now - (dataSet.getMaxLength()), Double.NaN, 0., 0.); // first point for long-term history
dataSet.add(now, 100 * Math.cos(2.0 * Math.PI * now), 0., 0.);
}
}, 0, 40);
long startTime = ProcessingProfiler.getTimeStamp();
chart.getDatasets().add(dataSet);
ProcessingProfiler.getTimeDiff(startTime, "adding data to chart");
startTime = ProcessingProfiler.getTimeStamp();
final var spThreshold = new Slider(0.0, 1.0, xAxis1.getThreshold());
spThreshold.setMajorTickUnit(0.1);
spThreshold.setSnapToTicks(true);
spThreshold.setShowTickLabels(true);
spThreshold.setShowTickMarks(true);
HBox.setHgrow(spThreshold, Priority.ALWAYS);
spThreshold.valueProperty().bindBidirectional(xAxis1.thresholdProperty());
final var xValueIndicator = new XValueIndicator(xAxis1, xAxis1.getWidth() * xAxis1.getThreshold(), "long-short");
xValueIndicator.setEditable(false);
dataSet.getBitState().addInvalidateListener(FXUtils.runOnFxThread((src, bits) -> {
final var locator = xAxis1.getValueForDisplay(xAxis1.getThreshold() * xAxis1.getWidth());
xValueIndicator.setValue(locator);
}));
chart.getPlugins().add(xValueIndicator);
final var spWeight = new Slider(0.0, 1.0, xAxis1.getWeight());
spWeight.setMajorTickUnit(0.1);
spWeight.setShowTickLabels(true);
spWeight.setSnapToTicks(true);
spWeight.setShowTickMarks(true);
HBox.setHgrow(spWeight, Priority.ALWAYS);
spWeight.valueProperty().bindBidirectional(xAxis1.weightProperty());
root.setTop(new VBox(new HBox(new Label("threshold: "), spThreshold), new HBox(new Label("weight: "), spWeight)));
root.setCenter(chart);
ProcessingProfiler.getTimeDiff(startTime, "adding chart into StackPane");
final var diagChart = new XYChart();
diagChart.setMaxHeight(300);
diagChart.getPlugins().addAll(new Zoomer(), new EditAxis(), new DataPointTooltip());
final var function = new DoubleDataSet("function");
final var inverse = new DoubleDataSet("inverse");
final var identity = new DoubleDataSet("identity");
diagChart.getDatasets().addAll(function, inverse, identity);
final var nSamples = 1000;
Runnable updateFunction = () -> {
function.clearData();
inverse.clearData();
identity.clearData();
for (var i = 0; i < nSamples; i++) {
final double x = (double) i / (nSamples - 1);
function.add(x, NonLinearTimeAxis.forwardTransform(x, xAxis1.getThreshold(), xAxis1.getWeight()));
inverse.add(x, NonLinearTimeAxis.backwardTransform(x, xAxis1.getThreshold(), xAxis1.getWeight()));
identity.add(x, NonLinearTimeAxis.backwardTransform(NonLinearTimeAxis.forwardTransform(x, xAxis1.getThreshold(), xAxis1.getWeight()), xAxis1.getThreshold(), xAxis1.getWeight()));
}
};
updateFunction.run();
spThreshold.valueProperty().addListener(evt -> updateFunction.run());
spWeight.valueProperty().addListener(evt -> updateFunction.run());
root.setBottom(diagChart);
return root;
}
/**
* @param args the command line arguments
*/
public static void main(final String[] args) {
Application.launch(args);
}
public static class NonLinearTimeAxis extends DefaultNumericAxis { // NOPMD NOSONAR -- inheritance depth of 8 vs. desired 5 (unavoidable with JavaFX)
private final transient DoubleProperty threshold = new SimpleDoubleProperty(this, "threshold", 0.6); // 0.6
private final transient DoubleProperty weight = new SimpleDoubleProperty(this, "weight", 0.9);
private final transient DefaultTimeFormatter lowerFormat = new DefaultTimeFormatter();
private final transient DefaultTimeFormatter upperFormat = new DefaultTimeFormatter();
NonLinearTimeAxis(final String axisLabel, final String unit) {
super(axisLabel, unit);
setOverlapPolicy(AxisLabelOverlapPolicy.SKIP_ALT);
setAutoRangeRounding(false);
super.setTimeAxis(true);
}
@Override
public double getDisplayPosition(final double value) {
final double diffMin = value - getMin();
final double range = Math.abs(getMax() - getMin());
final double relPos = diffMin / range;
return forwardTransform(relPos, getThreshold(), getWeight()) * getWidth();
}
public double getThreshold() {
return threshold.get();
}
public void setThreshold(final double threshold) {
this.threshold.set(threshold);
}
@Override
public double getValueForDisplay(final double displayPosition) {
final double relPosition = displayPosition / getWidth();
final double range = Math.abs(getMax() - getMin());
return getMin() + backwardTransform(relPosition, getThreshold(), getWeight()) * range;
}
public double getWeight() {
return weight.get();
}
public void setWeight(final double weight) {
this.weight.set(weight);
}
public DoubleProperty thresholdProperty() {
return threshold;
}
public DoubleProperty weightProperty() {
return weight;
}
@Override
protected void calculateMajorTickValues(final AxisRange axisRange, DoubleArrayList tickValues) {
// TODO:
// axisLength used to be passed in, but the ticks should be computed before the length is available.
// this example is the only one that depends on the length, but there is already a dependency on
// getWidth() in getValueForDisplay(), so we might as well use it here. Overall, the example could
// probably be refactored to work on the range directly.
final double axisLength = getWidth();
final int nTicks = getMaxMajorTickLabelCount();
tickValues.ensureCapacity(nTicks);
final double nTicksHalf1 = nTicks * getThreshold();
final var lower = new DoubleArrayList((int) nTicksHalf1);
final double min = getValueForDisplay(0.01 * axisLength);
lower.add(min);
tickValues.add(min);
for (var i = 1; i < nTicksHalf1; i++) {
final var axisPos = (double) i / nTicksHalf1 * getThreshold() * axisLength;
final double value = (getValueForDisplay(axisPos));
tickValues.add(value);
lower.add(value);
}
final double nTicksHalf2 = nTicks * (1.0 - getThreshold());
final var upper = new DoubleArrayList((int) nTicksHalf2);
final double atThreshold = getValueForDisplay(getThreshold() * axisLength);
tickValues.add(atThreshold); // fixed limit at threshold boundary
upper.add(atThreshold);
lower.add(atThreshold);
lowerFormat.updateFormatter(lower, 1.0);
for (var i = 1; i < nTicksHalf2 - 2; i++) {
final var axisPos = (1.0 + (double) i / nTicksHalf2) * getThreshold() * axisLength;
final double value = (getValueForDisplay(axisPos));
tickValues.add(value);
upper.add(value);
}
upperFormat.updateFormatter(upper, 1.0);
}
@Override
public String getTickMarkLabel(final double value) {
final Double boxedValue = value;
if (getDisplayPosition(value) < getThreshold() * getWidth()) {
return (getWeight() > getThreshold() ? lowerFormat : upperFormat).toString(boxedValue); // large values
}
return (getWeight() > getThreshold() ? upperFormat : lowerFormat).toString(boxedValue); // small values
}
public static double backwardTransform(final double x, final double threshold, final double weight) {
if (x < threshold) {
return weight * x / threshold;
}
return weight + (1.0 - weight) / (1.0 - threshold) * (x - threshold);
}
public static double forwardTransform(final double x, final double threshold, final double weight) {
if (x < weight) {
return threshold * x / weight;
}
return threshold + (1.0 - threshold) / (1.0 - weight) * (x - weight);
}
}
}