Added more dynamic charts and stuff.

This commit is contained in:
Andrew Lalis 2023-04-21 14:28:42 +02:00
parent a67c6641f9
commit 583d57d5bd
11 changed files with 177 additions and 128 deletions

View File

@ -21,6 +21,7 @@ public class RecorderApp {
var window = new RecorderAppWindow(dataSource); var window = new RecorderAppWindow(dataSource);
System.out.println("Initialized App Window"); System.out.println("Initialized App Window");
window.addWindowListener(new WindowDataSourceCloser(dataSource)); window.addWindowListener(new WindowDataSourceCloser(dataSource));
System.out.println("Added App Window close listener");
window.setVisible(true); window.setVisible(true);
System.out.println("Set App Window as visible"); System.out.println("Set App Window as visible");
} catch (SQLException e) { } catch (SQLException e) {

View File

@ -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);
}
}

View File

@ -34,7 +34,7 @@ public class AggregateStatisticsPanel extends JPanel {
controlsPanel.add(refreshButton); controlsPanel.add(refreshButton);
this.add(controlsPanel, BorderLayout.NORTH); this.add(controlsPanel, BorderLayout.NORTH);
SwingUtilities.invokeLater(this::refreshStats); // SwingUtilities.invokeLater(this::refreshStats);
} }
private void refreshStats() { private void refreshStats() {

View File

@ -1,34 +1,74 @@
package com.github.andrewlalis.running_every_day.view; 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.data.db.DataSource;
import com.github.andrewlalis.running_every_day.view.chart.ChartRenderingPanel; 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.DateSeriesCharts;
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 javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.geom.Rectangle2D; import java.time.LocalDate;
public class ChartsPanel extends JPanel { public class ChartsPanel extends JPanel {
private final DataSource dataSource; private final DataSource dataSource;
private final ChartRenderingPanel chartRenderingPanel;
public ChartsPanel(DataSource dataSource) { public ChartsPanel(DataSource dataSource) {
super(new BorderLayout()); super(new BorderLayout());
this.dataSource = dataSource; this.dataSource = dataSource;
var drawingPanel = new ChartRenderingPanel(new WeightChartRenderer2(dataSource)); this.chartRenderingPanel = new ChartRenderingPanel();
this.add(drawingPanel, BorderLayout.CENTER); this.add(chartRenderingPanel, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel(); JPanel chartMenu = new JPanel();
JButton drawButton = new JButton("Draw"); chartMenu.setLayout(new BoxLayout(chartMenu, BoxLayout.PAGE_AXIS));
// drawButton.addActionListener(e -> draw()); chartMenu.add(buildWeightChartMenuPanel());
buttonPanel.add(drawButton); chartMenu.add(buildPaceChartMenuPanel());
this.add(buttonPanel, BorderLayout.NORTH); 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;
} }
} }

View File

@ -97,10 +97,10 @@ public class RunRecordsPanel extends JPanel {
paginationPanel.add(lastPageButton); paginationPanel.add(lastPageButton);
this.add(paginationPanel, BorderLayout.SOUTH); this.add(paginationPanel, BorderLayout.SOUTH);
SwingUtilities.invokeLater(() -> { // SwingUtilities.invokeLater(() -> {
tableModel.firstPage(); // tableModel.firstPage();
updateButtonStates(); // updateButtonStates();
}); // });
JPanel actionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); JPanel actionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton addActionButton = new JButton("Add a Record"); JButton addActionButton = new JButton("Add a Record");

View File

@ -5,17 +5,22 @@ import java.awt.*;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
public class ChartRenderingPanel extends JPanel { 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.renderer = renderer;
this.repaint();
} }
@Override @Override
protected void paintComponent(Graphics g) { protected void paintComponent(Graphics g) {
super.paintComponent(g); super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g; if (renderer != null) {
Rectangle2D area = new Rectangle2D.Float(0, 0, this.getWidth(), this.getHeight()); Graphics2D g2 = (Graphics2D) g;
renderer.render(g2, area); Rectangle2D area = new Rectangle2D.Float(0, 0, this.getWidth(), this.getHeight());
renderer.render(g2, area);
} else {
g.drawString("No chart to render", 50, 50);
}
} }
} }

View File

@ -14,25 +14,25 @@ import java.awt.*;
import java.text.DateFormat; import java.text.DateFormat;
import java.time.LocalDate; import java.time.LocalDate;
public abstract class DateSeriesChartRenderer extends JFreeChartRenderer { public class DateSeriesChartRenderer extends JFreeChartRenderer {
protected record Datapoint (double value, LocalDate date) {} public record Datapoint (double value, LocalDate date) {}
private final String title; private final String title;
private final Paint linePaint; 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.title = title;
this.linePaint = linePaint; this.linePaint = linePaint;
this.dataGenerator = dataGenerator;
} }
protected abstract Datapoint[] getData() throws Exception;
@Override @Override
protected JFreeChart getChart() throws Exception { protected JFreeChart getChart() throws Exception {
TimeSeries series = new TimeSeries("Series"); TimeSeries series = new TimeSeries("Series");
double minValue = Double.MAX_VALUE; double minValue = Double.MAX_VALUE;
double maxValue = Double.MIN_VALUE; double maxValue = Double.MIN_VALUE;
for (var d : getData()) { for (var d : dataGenerator.generate()) {
minValue = Math.min(minValue, d.value()); minValue = Math.min(minValue, d.value());
maxValue = Math.max(maxValue, d.value()); maxValue = Math.max(maxValue, d.value());
series.add( series.add(

View File

@ -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<String> 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);
}
}

View File

@ -0,0 +1,6 @@
package com.github.andrewlalis.running_every_day.view.chart;
@FunctionalInterface
public interface DateSeriesDataGenerator {
Iterable<DateSeriesChartRenderer.Datapoint> generate() throws Exception;
}

View File

@ -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<WeightDatapoint> 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);
}
}

View File

@ -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<Datapoint> 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]);
}
}