diff --git a/src/com/stuypulse/stuylib/util/plot/DataSeries.java b/src/com/stuypulse/stuylib/util/plot/DataSeries.java new file mode 100644 index 00000000..963ab271 --- /dev/null +++ b/src/com/stuypulse/stuylib/util/plot/DataSeries.java @@ -0,0 +1,50 @@ +/* Copyright (c) 2022 StuyPulse Robotics. All rights reserved. */ +/* This work is licensed under the terms of the MIT license */ +/* found in the root directory of this project. */ + +package com.stuypulse.stuylib.util.plot; + +import java.util.ArrayList; +import java.util.List; + +import com.stuypulse.stuylib.math.Vector2D; + +public class DataSeries extends Series { + + private List xValues; + private List yValues; + + public DataSeries(String label, Vector2D... points) { + super(new Config(label, points.length), false); + + xValues = new ArrayList(); + yValues = new ArrayList(); + + for (Vector2D point : points) { + xValues.add(point.x); + yValues.add(point.y); + } + } + + @Override + public int size() { + return yValues.size(); + } + + @Override + protected List getSafeXValues() { + return xValues; + } + + @Override + protected List getSafeYValues() { + return yValues; + } + + @Override + protected void pop() {} + + @Override + protected void poll() {} + +} diff --git a/src/com/stuypulse/stuylib/util/plot/Playground.java b/src/com/stuypulse/stuylib/util/plot/Playground.java index d032a6d8..916898d1 100644 --- a/src/com/stuypulse/stuylib/util/plot/Playground.java +++ b/src/com/stuypulse/stuylib/util/plot/Playground.java @@ -25,8 +25,8 @@ public interface Constants { String X_AXIS = "x-axis"; String Y_AXIS = "y-axis"; - int WIDTH = 800; - int HEIGHT = 600; + int WIDTH = 640; + int HEIGHT = 480; double MIN_X = 0.0; double MAX_X = 1.0; @@ -36,11 +36,17 @@ public interface Constants { Settings SETTINGS = new Settings() - .setSize(WIDTH, HEIGHT) .setAxes(TITLE, X_AXIS, Y_AXIS) .setXRange(MIN_X, MAX_X) .setYRange(MIN_Y, MAX_Y); + public static Settings settings(String title) { + return new Settings() + .setAxes(title, X_AXIS, Y_AXIS) + .setXRange(MIN_X, MAX_X) + .setYRange(MIN_Y, MAX_Y); + } + public static Series make(String id, IFilter function) { return new FuncSeries(new Config(id, CAPACITY), new Domain(MIN_X, MAX_X), function); } @@ -59,39 +65,45 @@ public static Series make(String id, BStream series) { } public static void main(String[] args) throws InterruptedException { - Plot plot = new Plot(Constants.SETTINGS); - - plot.addSeries(Constants.make("y=x", x -> x)) - .addSeries( - Constants.make( - "interp", - new LinearInterpolator( - new Vector2D(0.0, 0.43), - new Vector2D(0.2, 0.56), - new Vector2D(0.4, 0.72), - new Vector2D(0.6, 0.81), - new Vector2D(0.8, 0.02), - new Vector2D(1.0, 0.11)))) - .addSeries(Constants.make("mouse y", IStream.create(plot::getMouseY))) - .addSeries( - Constants.make( - "lpf", - IStream.create(plot::getMouseY).filtered(new LowPassFilter(0.2)))) - .addSeries( - Constants.make("mouse bool", BStream.create(() -> plot.getMouseY() > 0.5))) - .addSeries( - Constants.make( - "debounced", - BStream.create(() -> plot.getMouseY() > 0.5) - .filtered(new BDebounce.Both(1.0)))) - .addSeries(Constants.make("mouse position", VStream.create(plot::getMouse))) - .addSeries( - Constants.make( - "jerk limit", - VStream.create(plot::getMouse) - .filtered(new VJerkLimit(10.0, 5.0)))); - - while (plot.isRunning()) { + Plot plot = new Plot(); + + plot.addTab(Constants.settings("Functions")) + .addSeries(Constants.make("y=x", x -> x)) + .addSeries( + Constants.make( + "interp", + new LinearInterpolator( + new Vector2D(0.0, 0.43), + new Vector2D(0.2, 0.56), + new Vector2D(0.4, 0.72), + new Vector2D(0.6, 0.81), + new Vector2D(0.8, 0.02), + new Vector2D(1.0, 0.11)))) + + .addTab(Constants.settings("Filters")) + .addSeries(Constants.make("mouse y", IStream.create(plot::getMouseY))) + .addSeries( + Constants.make( + "lpf", + IStream.create(plot::getMouseY).filtered(new LowPassFilter(0.2)))) + .addSeries( + Constants.make("mouse bool", BStream.create(() -> plot.getMouseY() > 0.5))) + .addSeries( + Constants.make( + "debounced", + BStream.create(() -> plot.getMouseY() > 0.5) + .filtered(new BDebounce.Both(1.0)))) + + .addTab(Constants.settings("XY Graph")) + .addSeries(Constants.make("mouse position", VStream.create(plot::getMouse))) + .addSeries(Constants.make( + "jerk limit", + VStream.create(plot::getMouse) + .filtered(new VJerkLimit(10.0, 5.0)))) + + .build(Constants.TITLE, Constants.WIDTH, Constants.HEIGHT); + + for (;;) { plot.update(); Thread.sleep(20); } diff --git a/src/com/stuypulse/stuylib/util/plot/Plot.java b/src/com/stuypulse/stuylib/util/plot/Plot.java index 45439231..5ddb1c15 100644 --- a/src/com/stuypulse/stuylib/util/plot/Plot.java +++ b/src/com/stuypulse/stuylib/util/plot/Plot.java @@ -7,13 +7,13 @@ import com.stuypulse.stuylib.math.Vector2D; import java.awt.Dimension; -import java.awt.Toolkit; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; + import javax.swing.JFrame; -import org.knowm.xchart.XChartPanel; -import org.knowm.xchart.XYChart; -import org.knowm.xchart.XYChartBuilder; +import javax.swing.JTabbedPane; /** * A plot contains and manages the window to which any data is drawn. @@ -25,72 +25,57 @@ */ public class Plot { - /** A collection of Series to be graphed */ - private List plots; + private List tabs; + private Map names; - /** The window that is created */ private JFrame frame; - - /** A reference to the XChart library */ - private XYChart instance; - - private XChartPanel panel; + private JTabbedPane pane; /** A utility for finding mouse positions */ private MouseTracker mouse; - /** A boolean to ensure the plot is updated at least once */ - private boolean runOnce; - /** * Creates a configured plot * * @param settings plot and window settings */ - public Plot(Settings settings) { - // Setup series - plots = new ArrayList<>(); - - // Setup window - frame = new JFrame(settings.getTitle()); + public Plot() { + tabs = new ArrayList(); + names = new HashMap(); + } + + public void build(String title, int width, int height) { + pane = new JTabbedPane(); + frame = new JFrame(title); + frame.getContentPane() - .setPreferredSize(new Dimension(settings.getWidth(), settings.getHeight())); + .setPreferredSize(new Dimension(width, height)); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + + frame.setResizable(false); - // Create XYChart using settings - instance = - new XYChartBuilder() - .title(settings.getTitle()) - .xAxisTitle(settings.getXAxis()) - .yAxisTitle(settings.getYAxis()) - .build(); - - instance.getStyler().setYAxisMin(settings.getYMin()); - instance.getStyler().setYAxisMax(settings.getYMax()); + for (Tab tab : tabs) { + pane.addTab(tab.panel.getName(), tab.panel); + } - instance.getStyler().setXAxisMin(settings.getXMin()); - instance.getStyler().setXAxisMax(settings.getXMax()); + frame.getContentPane().add(pane); - panel = new XChartPanel(instance); - panel.setName(settings.getTitle()); + frame.pack(); - mouse = new MouseTracker(panel); - - frame.getContentPane().add(panel); - frame.setResizable(false); - - frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + + mouse = new MouseTracker(frame); + } - frame.setLocationRelativeTo(null); - frame.setVisible(true); + public Plot addTab(Settings settings) { + Tab tab = new Tab(settings); - runOnce = true; - } + tabs.add(tab); + names.put(settings.getTitle(), tabs.size() - 1); - /** Creates a plot with default settings */ - public Plot() { - this(new Settings()); + return this; } /** @return mouse position */ @@ -114,44 +99,25 @@ public double getMouseX() { * @param series series to add * @return reference to self */ - public Plot addSeries(Series... series) { - for (Series e : series) plots.add(e); - return this; - } - - /** allows the series to update the XYChart */ - public void updateSeries() { - for (Series plot : plots) { - plot.update(instance); + public Plot addSeries(String tabId, Series... series) { + if (!names.containsKey(tabId)) { + System.err.println("Invalid tab ID \"" + tabId + "\" given"); + return this; } - } - /** repaints the screen */ - public void display() { - Toolkit.getDefaultToolkit().sync(); - panel.revalidate(); - panel.repaint(); + tabs.get(names.get(tabId)).addSeries(series); + return this; } - public void update() { - updateSeries(); - display(); + public Plot addSeries(Series... series) { + tabs.get(tabs.size() - 1).addSeries(series); + return this; } - /** - * Checks if any series are polling to see if the plot should still update. - * - * @return if the plot should still run - */ - public boolean isRunning() { - if (runOnce) { - runOnce = false; - return true; - } + public void update() { + int selected = pane.getSelectedIndex(); - for (Series e : plots) { - if (e.isPolling()) return true; - } - return false; + if (tabs.get(selected).isRunning()) + tabs.get(selected).update(); } } diff --git a/src/com/stuypulse/stuylib/util/plot/Settings.java b/src/com/stuypulse/stuylib/util/plot/Settings.java index 4290a204..b4a96a31 100644 --- a/src/com/stuypulse/stuylib/util/plot/Settings.java +++ b/src/com/stuypulse/stuylib/util/plot/Settings.java @@ -39,11 +39,6 @@ public Axes(String title, String x, String y) { private String xAxis; private String yAxis; - /** window size */ - private int width; - - private int height; - /** x-axis bounds */ private double xMin; @@ -60,9 +55,6 @@ public Settings() { xAxis = "x"; yAxis = "y"; - width = 640; - height = 480; - xMin = 0.0; xMax = 1.0; @@ -85,16 +77,6 @@ public String getYAxis() { return yAxis; } - /** @return width of window */ - public int getWidth() { - return width; - } - - /** @return height of window */ - public int getHeight() { - return height; - } - /** @return lower bound of x-axis */ public double getXMin() { return xMin; @@ -115,7 +97,7 @@ public double getYMax() { return yMax; } - /** + /** * Sets title of plot * * @param title title @@ -242,39 +224,4 @@ public Settings setYRange(double min, double max) { setYMin(min); return this; } - - /** - * Sets width of window - * - * @param width width of window - * @return reference to self for chaining methods - */ - public Settings setWidth(int width) { - this.width = width; - return this; - } - - /** - * Sets height of window - * - * @param height height of window - * @return reference to self for chaining methods - */ - public Settings setHeight(int height) { - this.height = height; - return this; - } - - /** - * Sets size of window - * - * @param width width of window - * @param height height of window - * @return reference to self for chaining methods - */ - public Settings setSize(int width, int height) { - setWidth(width); - setHeight(height); - return this; - } } diff --git a/src/com/stuypulse/stuylib/util/plot/Tab.java b/src/com/stuypulse/stuylib/util/plot/Tab.java new file mode 100644 index 00000000..a0a54e2d --- /dev/null +++ b/src/com/stuypulse/stuylib/util/plot/Tab.java @@ -0,0 +1,90 @@ +package com.stuypulse.stuylib.util.plot; + +import java.awt.Toolkit; +import java.util.ArrayList; +import java.util.List; + +import org.knowm.xchart.XChartPanel; +import org.knowm.xchart.XYChart; +import org.knowm.xchart.XYChartBuilder; +import org.knowm.xchart.style.Styler.LegendPosition; + +public class Tab { + + /** A collection of Series to be graphed */ + private List plots; + + private XYChart instance; + + protected XChartPanel panel; + + /** A boolean to ensure the plot is updated at least once */ + private boolean runOnce; + + public Tab(Settings settings) { + // Setup series + plots = new ArrayList<>(); + + // Create XYChart using settings + instance = + new XYChartBuilder() + .title(settings.getTitle()) + .xAxisTitle(settings.getXAxis()) + .yAxisTitle(settings.getYAxis()) + .build(); + + instance.getStyler().setYAxisMin(settings.getYMin()); + instance.getStyler().setYAxisMax(settings.getYMax()); + + instance.getStyler().setXAxisMin(settings.getXMin()); + instance.getStyler().setXAxisMax(settings.getXMax()); + + instance.getStyler().setLegendPosition(LegendPosition.InsideNW); + + panel = new XChartPanel(instance); + panel.setName(settings.getTitle()); + + runOnce = true; + } + + public void addSeries(Series... series) { + for (Series s : series) plots.add(s); + } + + /** allows the series to update the XYChart */ + public void updateSeries() { + for (Series plot : plots) { + plot.update(instance); + } + } + + /** repaints the screen */ + public void display() { + Toolkit.getDefaultToolkit().sync(); + panel.revalidate(); + panel.repaint(); + } + + public void update() { + updateSeries(); + display(); + } + + /** + * Checks if any series are polling to see if the plot should still update. + * + * @return if the plot should still run + */ + public boolean isRunning() { + if (runOnce) { + runOnce = false; + return true; + } + + for (Series e : plots) { + if (e.isPolling()) return true; + } + return false; + } + +}