From 583d57d5bd2e56e8c3922c716059c2ea52a3b936 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Fri, 21 Apr 2023 14:28:42 +0200 Subject: [PATCH] Added more dynamic charts and stuff. --- .../running_every_day/RecorderApp.java | 1 + .../running_every_day/data/DateRange.java | 17 +++++ .../view/AggregateStatisticsPanel.java | 2 +- .../running_every_day/view/ChartsPanel.java | 74 +++++++++++++----- .../view/RunRecordsPanel.java | 8 +- .../view/chart/ChartRenderingPanel.java | 15 ++-- .../view/chart/DateSeriesChartRenderer.java | 12 +-- .../view/chart/DateSeriesCharts.java | 75 +++++++++++++++++++ .../view/chart/DateSeriesDataGenerator.java | 6 ++ .../view/chart/WeightChartRenderer.java | 65 ---------------- .../view/chart/WeightChartRenderer2.java | 30 -------- 11 files changed, 177 insertions(+), 128 deletions(-) create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/data/DateRange.java create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesCharts.java create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesDataGenerator.java delete mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/WeightChartRenderer.java delete mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/WeightChartRenderer2.java diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/RecorderApp.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/RecorderApp.java index be3d4e8..07abea3 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/RecorderApp.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/RecorderApp.java @@ -21,6 +21,7 @@ public class RecorderApp { var window = new RecorderAppWindow(dataSource); System.out.println("Initialized App Window"); window.addWindowListener(new WindowDataSourceCloser(dataSource)); + System.out.println("Added App Window close listener"); window.setVisible(true); System.out.println("Set App Window as visible"); } catch (SQLException e) { diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/DateRange.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/DateRange.java new file mode 100644 index 0000000..c9b2798 --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/DateRange.java @@ -0,0 +1,17 @@ +package com.github.andrewlalis.running_every_day.data; + +import java.time.LocalDate; + +public record DateRange(LocalDate start, LocalDate end) { + public static DateRange after(LocalDate start) { + return new DateRange(start, null); + } + + public static DateRange before(LocalDate end) { + return new DateRange(null, end); + } + + public static DateRange unbounded() { + return new DateRange(null, null); + } +} diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/AggregateStatisticsPanel.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/AggregateStatisticsPanel.java index 3fda96d..673e101 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/AggregateStatisticsPanel.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/AggregateStatisticsPanel.java @@ -34,7 +34,7 @@ public class AggregateStatisticsPanel extends JPanel { controlsPanel.add(refreshButton); this.add(controlsPanel, BorderLayout.NORTH); - SwingUtilities.invokeLater(this::refreshStats); +// SwingUtilities.invokeLater(this::refreshStats); } private void refreshStats() { diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/ChartsPanel.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/ChartsPanel.java index 1e83355..75081f1 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/ChartsPanel.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/ChartsPanel.java @@ -1,34 +1,74 @@ package com.github.andrewlalis.running_every_day.view; +import com.github.andrewlalis.running_every_day.data.DateRange; import com.github.andrewlalis.running_every_day.data.db.DataSource; import com.github.andrewlalis.running_every_day.view.chart.ChartRenderingPanel; -import com.github.andrewlalis.running_every_day.view.chart.WeightChartRenderer; -import com.github.andrewlalis.running_every_day.view.chart.WeightChartRenderer2; -import org.jfree.chart.ChartFactory; -import org.jfree.chart.JFreeChart; -import org.jfree.data.category.DefaultCategoryDataset; -import org.jfree.data.time.Day; -import org.jfree.data.time.TimeSeries; -import org.jfree.data.time.TimeSeriesCollection; -import org.jfree.data.xy.XYDataset; +import com.github.andrewlalis.running_every_day.view.chart.DateSeriesCharts; import javax.swing.*; import java.awt.*; -import java.awt.geom.Rectangle2D; +import java.time.LocalDate; public class ChartsPanel extends JPanel { private final DataSource dataSource; + private final ChartRenderingPanel chartRenderingPanel; + public ChartsPanel(DataSource dataSource) { super(new BorderLayout()); this.dataSource = dataSource; - var drawingPanel = new ChartRenderingPanel(new WeightChartRenderer2(dataSource)); - this.add(drawingPanel, BorderLayout.CENTER); + this.chartRenderingPanel = new ChartRenderingPanel(); + this.add(chartRenderingPanel, BorderLayout.CENTER); - JPanel buttonPanel = new JPanel(); - JButton drawButton = new JButton("Draw"); -// drawButton.addActionListener(e -> draw()); - buttonPanel.add(drawButton); - this.add(buttonPanel, BorderLayout.NORTH); + JPanel chartMenu = new JPanel(); + chartMenu.setLayout(new BoxLayout(chartMenu, BoxLayout.PAGE_AXIS)); + chartMenu.add(buildWeightChartMenuPanel()); + chartMenu.add(buildPaceChartMenuPanel()); + var menuScroll = new JScrollPane(chartMenu, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); +// menuScroll.setPreferredSize(new Dimension(300, -1)); + menuScroll.getVerticalScrollBar().setUnitIncrement(10); + this.add(menuScroll, BorderLayout.EAST); + } + + private JPanel buildBasicChartMenuItem(String name, String description) { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); + panel.setBorder(BorderFactory.createRaisedBevelBorder()); + + JLabel titleLabel = new JLabel(name); + titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD)); + titleLabel.setHorizontalAlignment(SwingConstants.LEFT); + panel.add(titleLabel); + + JTextArea descriptionArea = new JTextArea(); + descriptionArea.setLineWrap(true); + descriptionArea.setWrapStyleWord(true); + descriptionArea.setEditable(false); + descriptionArea.setText(description); + panel.add(descriptionArea); + + return panel; + } + + private JPanel buildWeightChartMenuPanel() { + JPanel panel = buildBasicChartMenuItem("Weight", "A chart that depicts weight change over time."); + + JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton viewButton = new JButton("View"); + viewButton.addActionListener(e -> chartRenderingPanel.setRenderer(DateSeriesCharts.weight(dataSource, DateRange.unbounded()))); + buttonsPanel.add(viewButton); + panel.add(buttonsPanel); + return panel; + } + + private JPanel buildPaceChartMenuPanel() { + JPanel panel = buildBasicChartMenuItem("Average Pace", "A chart that depicts the average pace of each run."); + + JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton viewButton = new JButton("View"); + viewButton.addActionListener(e -> chartRenderingPanel.setRenderer(DateSeriesCharts.pace(dataSource, DateRange.unbounded()))); + buttonsPanel.add(viewButton); + panel.add(buttonsPanel); + return panel; } } diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java index ea27bb8..4aa9325 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java @@ -97,10 +97,10 @@ public class RunRecordsPanel extends JPanel { paginationPanel.add(lastPageButton); this.add(paginationPanel, BorderLayout.SOUTH); - SwingUtilities.invokeLater(() -> { - tableModel.firstPage(); - updateButtonStates(); - }); +// SwingUtilities.invokeLater(() -> { +// tableModel.firstPage(); +// updateButtonStates(); +// }); JPanel actionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); JButton addActionButton = new JButton("Add a Record"); diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ChartRenderingPanel.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ChartRenderingPanel.java index 15ff093..ba0e968 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ChartRenderingPanel.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ChartRenderingPanel.java @@ -5,17 +5,22 @@ import java.awt.*; import java.awt.geom.Rectangle2D; public class ChartRenderingPanel extends JPanel { - private final ChartRenderer renderer; + private ChartRenderer renderer; - public ChartRenderingPanel(ChartRenderer renderer) { + public void setRenderer(ChartRenderer renderer) { this.renderer = renderer; + this.repaint(); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); - Graphics2D g2 = (Graphics2D) g; - Rectangle2D area = new Rectangle2D.Float(0, 0, this.getWidth(), this.getHeight()); - renderer.render(g2, area); + if (renderer != null) { + Graphics2D g2 = (Graphics2D) g; + Rectangle2D area = new Rectangle2D.Float(0, 0, this.getWidth(), this.getHeight()); + renderer.render(g2, area); + } else { + g.drawString("No chart to render", 50, 50); + } } } diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesChartRenderer.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesChartRenderer.java index 42b9980..1bf0d81 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesChartRenderer.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesChartRenderer.java @@ -14,25 +14,25 @@ import java.awt.*; import java.text.DateFormat; import java.time.LocalDate; -public abstract class DateSeriesChartRenderer extends JFreeChartRenderer { - protected record Datapoint (double value, LocalDate date) {} +public class DateSeriesChartRenderer extends JFreeChartRenderer { + public record Datapoint (double value, LocalDate date) {} private final String title; private final Paint linePaint; + private final DateSeriesDataGenerator dataGenerator; - protected DateSeriesChartRenderer(String title, Paint linePaint) { + protected DateSeriesChartRenderer(String title, Paint linePaint, DateSeriesDataGenerator dataGenerator) { this.title = title; this.linePaint = linePaint; + this.dataGenerator = dataGenerator; } - protected abstract Datapoint[] getData() throws Exception; - @Override protected JFreeChart getChart() throws Exception { TimeSeries series = new TimeSeries("Series"); double minValue = Double.MAX_VALUE; double maxValue = Double.MIN_VALUE; - for (var d : getData()) { + for (var d : dataGenerator.generate()) { minValue = Math.min(minValue, d.value()); maxValue = Math.max(maxValue, d.value()); series.add( diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesCharts.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesCharts.java new file mode 100644 index 0000000..5d9535e --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesCharts.java @@ -0,0 +1,75 @@ +package com.github.andrewlalis.running_every_day.view.chart; + +import com.github.andrewlalis.running_every_day.data.DateRange; +import com.github.andrewlalis.running_every_day.data.RunRecord; +import com.github.andrewlalis.running_every_day.data.db.DataSource; +import com.github.andrewlalis.running_every_day.data.db.Queries; + +import java.awt.*; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +public final class DateSeriesCharts { + public static ChartRenderer weight(DataSource dataSource, DateRange dateRange) { + String baseQuery = "SELECT weight, date FROM run"; + String dateRangeConditions = buildDateRangeConditions(dateRange); + if (dateRangeConditions != null) { + baseQuery += " WHERE " + dateRangeConditions; + } + baseQuery += " ORDER BY date ASC"; + final String query = baseQuery; + return new DateSeriesChartRenderer( + "Weight (Kg)", + Color.BLUE, + () -> Queries.findAll( + dataSource.conn(), + query, + rs -> new DateSeriesChartRenderer.Datapoint( + rs.getInt(1) / 1000.0, + LocalDate.parse(rs.getString(2)) + ) + ) + ); + } + + public static ChartRenderer pace(DataSource dataSource, DateRange dateRange) { + String baseQuery = "SELECT * FROM run"; + String dateRangeConditions = buildDateRangeConditions(dateRange); + if (dateRangeConditions != null) { + baseQuery += " WHERE " + dateRangeConditions; + } + baseQuery += " ORDER BY date ASC"; + final String query = baseQuery; + final var mapper = new RunRecord.Mapper(); + return new DateSeriesChartRenderer( + "Pace (Min/Km)", + Color.ORANGE, + () -> Queries.findAll( + dataSource.conn(), + query, + rs -> { + var record = mapper.map(rs); + return new DateSeriesChartRenderer.Datapoint( + record.averagePacePerKm().toMillis() / (1000.0 * 60.0), + record.date() + ); + } + ) + ); + } + + private static String buildDateRangeConditions(DateRange dateRange) { + if (dateRange == null || (dateRange.start() == null && dateRange.end() == null)) { + return null; + } + List conditions = new ArrayList<>(2); + if (dateRange.start() != null) { + conditions.add("date >= '" + dateRange.start() + "'"); + } + if (dateRange.end() != null) { + conditions.add("date <= '" + dateRange.end() + "'"); + } + return String.join(" AND ", conditions); + } +} diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesDataGenerator.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesDataGenerator.java new file mode 100644 index 0000000..ea2d909 --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesDataGenerator.java @@ -0,0 +1,6 @@ +package com.github.andrewlalis.running_every_day.view.chart; + +@FunctionalInterface +public interface DateSeriesDataGenerator { + Iterable generate() throws Exception; +} diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/WeightChartRenderer.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/WeightChartRenderer.java deleted file mode 100644 index cdf18f5..0000000 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/WeightChartRenderer.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.andrewlalis.running_every_day.view.chart; - -import com.github.andrewlalis.running_every_day.data.db.DataSource; -import com.github.andrewlalis.running_every_day.data.db.Queries; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.axis.DateAxis; -import org.jfree.chart.axis.NumberAxis; -import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; -import org.jfree.data.time.Day; -import org.jfree.data.time.TimeSeries; -import org.jfree.data.time.TimeSeriesCollection; - -import java.awt.*; -import java.text.DateFormat; -import java.time.LocalDate; -import java.util.List; - -public class WeightChartRenderer extends JFreeChartRenderer { - private record WeightDatapoint(double weightKg, LocalDate date){} - - private final DataSource dataSource; - - public WeightChartRenderer(DataSource dataSource) { - this.dataSource = dataSource; - } - - @Override - protected JFreeChart getChart() throws Exception { - TimeSeries series = new TimeSeries("Weight", "Date", "Weight (Kg)"); - List datapoints = Queries.findAll( - dataSource.conn(), - "SELECT date, weight FROM run ORDER BY date ASC LIMIT 50", - rs -> new WeightDatapoint( - rs.getInt(2) / 1000.0, - LocalDate.parse(rs.getString(1)) - ) - ); - double minWeight = Double.MAX_VALUE; - double maxWeight = Double.MIN_VALUE; - for (var dp : datapoints) { - minWeight = Math.min(minWeight, dp.weightKg); - maxWeight = Math.max(maxWeight, dp.weightKg); - series.add(new Day(dp.date.getDayOfMonth(), dp.date.getMonthValue(), dp.date.getYear()), dp.weightKg); - } - TimeSeriesCollection dataset = new TimeSeriesCollection(series); - - DateAxis domainAxis = new DateAxis(); - domainAxis.setVerticalTickLabels(true); - domainAxis.setDateFormatOverride(DateFormat.getDateInstance()); - - NumberAxis rangeAxis = new NumberAxis("Weight (Kg)"); - rangeAxis.setRangeWithMargins(minWeight, maxWeight); - - XYPlot plot = new XYPlot(dataset, domainAxis, rangeAxis, new XYLineAndShapeRenderer()); - var chart = new JFreeChart("Weight", plot); - chart.removeLegend(); - return chart; - } - - @Override - protected void applyCustomStyles(JFreeChart chart) { - applyStandardXYLineColor(chart, Color.BLUE); - } -} diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/WeightChartRenderer2.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/WeightChartRenderer2.java deleted file mode 100644 index 54c96fd..0000000 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/WeightChartRenderer2.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.andrewlalis.running_every_day.view.chart; - -import com.github.andrewlalis.running_every_day.data.db.DataSource; -import com.github.andrewlalis.running_every_day.data.db.Queries; - -import java.awt.*; -import java.time.LocalDate; -import java.util.List; - -public class WeightChartRenderer2 extends DateSeriesChartRenderer { - private final DataSource dataSource; - - public WeightChartRenderer2(DataSource dataSource) { - super("Weight (Kg)", Color.BLUE); - this.dataSource = dataSource; - } - - @Override - protected Datapoint[] getData() throws Exception { - List datapoints = Queries.findAll( - dataSource.conn(), - "SELECT weight, date FROM run ORDER BY date ASC LIMIT 50", - rs -> new Datapoint( - rs.getInt(1) / 1000.0, - LocalDate.parse(rs.getString(2)) - ) - ); - return datapoints.toArray(new Datapoint[0]); - } -}