Added more dynamic charts and stuff.
This commit is contained in:
parent
a67c6641f9
commit
583d57d5bd
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.github.andrewlalis.running_every_day.view.chart;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface DateSeriesDataGenerator {
|
||||||
|
Iterable<DateSeriesChartRenderer.Datapoint> generate() throws Exception;
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue