From 2858b83a1aef4e2e02dda712b7891c89c4650f70 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Thu, 20 Apr 2023 15:21:04 +0200 Subject: [PATCH] Added weight chart renderer, infrastructure for more dynamic charts. --- .../running_every_day/data/db/Queries.java | 14 +++++ .../running_every_day/view/ChartsPanel.java | 31 +++-------- .../view/RecorderAppWindow.java | 2 +- .../view/chart/ChartRenderer.java | 8 +++ .../view/chart/ChartRenderingPanel.java | 21 +++++++ .../view/chart/JFreeChartRenderer.java | 24 ++++++++ .../view/chart/WeightChartRenderer.java | 55 +++++++++++++++++++ 7 files changed, 131 insertions(+), 24 deletions(-) create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ChartRenderer.java create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ChartRenderingPanel.java create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/JFreeChartRenderer.java create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/WeightChartRenderer.java diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/db/Queries.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/db/Queries.java index 54a3069..495d670 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/db/Queries.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/db/Queries.java @@ -2,6 +2,8 @@ package com.github.andrewlalis.running_every_day.data.db; import java.sql.Connection; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; public final class Queries { @@ -66,6 +68,18 @@ public final class Queries { return pageCount; } + public static List findAll(Connection c, String query, ResultSetMapper mapper) throws SQLException { + try (var stmt = c.prepareStatement(query)) { + try (var rs = stmt.executeQuery()) { + List items = new ArrayList<>(); + while (rs.next()) { + items.add(mapper.map(rs)); + } + return items; + } + } + } + public static int update(Connection c, String query) throws SQLException { try (var stmt = c.prepareStatement(query)) { return stmt.executeUpdate(); 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 b23af7f..243fe0d 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,5 +1,8 @@ package com.github.andrewlalis.running_every_day.view; +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 org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.data.category.DefaultCategoryDataset; @@ -13,36 +16,18 @@ import java.awt.*; import java.awt.geom.Rectangle2D; public class ChartsPanel extends JPanel { - private final JPanel drawingPanel = new JPanel(); + private final DataSource dataSource; - public ChartsPanel() { + public ChartsPanel(DataSource dataSource) { super(new BorderLayout()); + this.dataSource = dataSource; + var drawingPanel = new ChartRenderingPanel(new WeightChartRenderer(dataSource)); this.add(drawingPanel, BorderLayout.CENTER); JPanel buttonPanel = new JPanel(); JButton drawButton = new JButton("Draw"); - drawButton.addActionListener(e -> draw()); +// drawButton.addActionListener(e -> draw()); buttonPanel.add(drawButton); this.add(buttonPanel, BorderLayout.NORTH); } - - public void draw() { - TimeSeriesCollection ds = new TimeSeriesCollection(); - TimeSeries ts = new TimeSeries("Data"); - ts.add(new Day(16, 4, 2023), 45); - ts.add(new Day(17, 4, 2023), 50); - ts.add(new Day(18, 4, 2023), 52); - ts.add(new Day(19, 4, 2023), 65); - ds.addSeries(ts); - - JFreeChart chart = ChartFactory.createXYLineChart( - "Test XY Line Chart", - "Date", - "Value", - ds - ); - Graphics2D g2 = (Graphics2D) drawingPanel.getGraphics(); - Rectangle2D area = new Rectangle2D.Float(0, 0, drawingPanel.getWidth(), drawingPanel.getHeight()); - chart.draw(g2, area); - } } diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RecorderAppWindow.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RecorderAppWindow.java index 9177be4..dc241d0 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RecorderAppWindow.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RecorderAppWindow.java @@ -19,7 +19,7 @@ public class RecorderAppWindow extends JFrame { JTabbedPane tabbedPane = new JTabbedPane(); tabbedPane.addTab("Run Records", new RunRecordsPanel(dataSource)); tabbedPane.addTab("Aggregate Statistics", new AggregateStatisticsPanel(dataSource)); - tabbedPane.addTab("Charts", new ChartsPanel()); + tabbedPane.addTab("Charts", new ChartsPanel(dataSource)); return tabbedPane; } } diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ChartRenderer.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ChartRenderer.java new file mode 100644 index 0000000..8643634 --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ChartRenderer.java @@ -0,0 +1,8 @@ +package com.github.andrewlalis.running_every_day.view.chart; + +import java.awt.*; +import java.awt.geom.Rectangle2D; + +public interface ChartRenderer { + void render(Graphics2D graphics, Rectangle2D area); +} 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 new file mode 100644 index 0000000..15ff093 --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ChartRenderingPanel.java @@ -0,0 +1,21 @@ +package com.github.andrewlalis.running_every_day.view.chart; + +import javax.swing.*; +import java.awt.*; +import java.awt.geom.Rectangle2D; + +public class ChartRenderingPanel extends JPanel { + private final ChartRenderer renderer; + + public ChartRenderingPanel(ChartRenderer renderer) { + this.renderer = renderer; + } + + @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); + } +} diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/JFreeChartRenderer.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/JFreeChartRenderer.java new file mode 100644 index 0000000..cd97c52 --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/JFreeChartRenderer.java @@ -0,0 +1,24 @@ +package com.github.andrewlalis.running_every_day.view.chart; + +import org.jfree.chart.JFreeChart; + +import java.awt.*; +import java.awt.geom.Rectangle2D; + +public abstract class JFreeChartRenderer implements ChartRenderer { + protected abstract JFreeChart getChart() throws Exception; + + @Override + public void render(Graphics2D graphics, Rectangle2D area) { + try { + var chart = getChart(); + // Apply theme to the chart. + chart.draw(graphics, area); + } catch (Exception e) { + graphics.setColor(Color.BLACK); + graphics.setBackground(Color.WHITE); + graphics.fill(area); + graphics.drawString("Error: " + e.getMessage(), 20, 40); + } + } +} 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 new file mode 100644 index 0000000..0fdb6df --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/WeightChartRenderer.java @@ -0,0 +1,55 @@ +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.XYItemRenderer; +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.text.DateFormat; +import java.time.LocalDate; +import java.util.List; + +public class WeightChartRenderer extends JFreeChartRenderer { + private record WeightDatapoint(int weightGrams, 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", + rs -> new WeightDatapoint( + rs.getInt(2), + LocalDate.parse(rs.getString(1)) + ) + ); + for (var dp : datapoints) { + series.add(new Day(dp.date.getDayOfMonth(), dp.date.getMonthValue(), dp.date.getYear()), dp.weightGrams); + } + TimeSeriesCollection dataset = new TimeSeriesCollection(series); + + DateAxis domainAxis = new DateAxis("Date"); + domainAxis.setVerticalTickLabels(true); + domainAxis.setDateFormatOverride(DateFormat.getDateInstance()); + NumberAxis rangeAxis = new NumberAxis("Weight (Kg)"); + rangeAxis.setAutoRange(true); + XYItemRenderer renderer = new XYLineAndShapeRenderer(true, false); + + XYPlot plot = new XYPlot(dataset, domainAxis, rangeAxis, renderer); + + return new JFreeChart(plot); + } +}