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);
|
||||
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) {
|
||||
|
|
|
@ -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);
|
||||
this.add(controlsPanel, BorderLayout.NORTH);
|
||||
|
||||
SwingUtilities.invokeLater(this::refreshStats);
|
||||
// SwingUtilities.invokeLater(this::refreshStats);
|
||||
}
|
||||
|
||||
private void refreshStats() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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