Improved chart rendering, added export.
This commit is contained in:
parent
583d57d5bd
commit
a200aee640
|
@ -14,4 +14,8 @@ public record DateRange(LocalDate start, LocalDate end) {
|
||||||
public static DateRange unbounded() {
|
public static DateRange unbounded() {
|
||||||
return new DateRange(null, null);
|
return new DateRange(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DateRange lastNWeeks(int n) {
|
||||||
|
return after(LocalDate.now().minusWeeks(n));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,14 @@ 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.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.DateRangePanel;
|
||||||
import com.github.andrewlalis.running_every_day.view.chart.DateSeriesCharts;
|
import com.github.andrewlalis.running_every_day.view.chart.DateSeriesCharts;
|
||||||
|
import com.github.andrewlalis.running_every_day.view.chart.ExportChartImageDialog;
|
||||||
|
import com.github.andrewlalis.running_every_day.view.chart.menu.DateSeriesChartMenuItem;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.awt.event.ActionListener;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
||||||
public class ChartsPanel extends JPanel {
|
public class ChartsPanel extends JPanel {
|
||||||
|
@ -20,55 +24,47 @@ public class ChartsPanel extends JPanel {
|
||||||
this.chartRenderingPanel = new ChartRenderingPanel();
|
this.chartRenderingPanel = new ChartRenderingPanel();
|
||||||
this.add(chartRenderingPanel, BorderLayout.CENTER);
|
this.add(chartRenderingPanel, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||||
|
JButton exportButton = new JButton("Export to Image");
|
||||||
|
exportButton.addActionListener(e -> {
|
||||||
|
var dialog = new ExportChartImageDialog(this, chartRenderingPanel.getRenderer());
|
||||||
|
dialog.setVisible(true);
|
||||||
|
});
|
||||||
|
topPanel.add(exportButton);
|
||||||
|
this.add(topPanel, BorderLayout.NORTH);
|
||||||
|
|
||||||
JPanel chartMenu = new JPanel();
|
JPanel chartMenu = new JPanel();
|
||||||
chartMenu.setLayout(new BoxLayout(chartMenu, BoxLayout.PAGE_AXIS));
|
chartMenu.setLayout(new BoxLayout(chartMenu, BoxLayout.PAGE_AXIS));
|
||||||
chartMenu.add(buildWeightChartMenuPanel());
|
|
||||||
chartMenu.add(buildPaceChartMenuPanel());
|
chartMenu.add(new DateSeriesChartMenuItem(
|
||||||
|
"Weight",
|
||||||
|
"A chart that depicts weight change over time.",
|
||||||
|
chartRenderingPanel,
|
||||||
|
dataSource,
|
||||||
|
DateRange.lastNWeeks(2),
|
||||||
|
DateSeriesCharts::weight
|
||||||
|
));
|
||||||
|
chartMenu.add(new DateSeriesChartMenuItem(
|
||||||
|
"Pace",
|
||||||
|
"A chart that depicts average pace in minutes per kilometer.",
|
||||||
|
chartRenderingPanel,
|
||||||
|
dataSource,
|
||||||
|
DateRange.lastNWeeks(2),
|
||||||
|
DateSeriesCharts::pace
|
||||||
|
));
|
||||||
|
chartMenu.add(new DateSeriesChartMenuItem(
|
||||||
|
"Total Distance",
|
||||||
|
"A chart showing the total distance accumulated over time, in kilometers.",
|
||||||
|
chartRenderingPanel,
|
||||||
|
dataSource,
|
||||||
|
DateRange.lastNWeeks(4),
|
||||||
|
DateSeriesCharts::totalDistance
|
||||||
|
));
|
||||||
|
|
||||||
|
chartMenu.add(Box.createVerticalGlue());
|
||||||
|
|
||||||
var menuScroll = new JScrollPane(chartMenu, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
var menuScroll = new JScrollPane(chartMenu, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||||
// menuScroll.setPreferredSize(new Dimension(300, -1));
|
|
||||||
menuScroll.getVerticalScrollBar().setUnitIncrement(10);
|
menuScroll.getVerticalScrollBar().setUnitIncrement(10);
|
||||||
this.add(menuScroll, BorderLayout.EAST);
|
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,6 @@ public class RunRecordsPanel extends JPanel {
|
||||||
paginationPanel.add(lastPageButton);
|
paginationPanel.add(lastPageButton);
|
||||||
|
|
||||||
this.add(paginationPanel, BorderLayout.SOUTH);
|
this.add(paginationPanel, BorderLayout.SOUTH);
|
||||||
// SwingUtilities.invokeLater(() -> {
|
|
||||||
// tableModel.firstPage();
|
|
||||||
// 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");
|
||||||
|
|
|
@ -3,8 +3,9 @@ package com.github.andrewlalis.running_every_day.view.chart;
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.geom.Rectangle2D;
|
import java.awt.geom.Rectangle2D;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class ChartRenderingPanel extends JPanel {
|
public class ChartRenderingPanel extends JPanel implements Consumer<ChartRenderer> {
|
||||||
private ChartRenderer renderer;
|
private ChartRenderer renderer;
|
||||||
|
|
||||||
public void setRenderer(ChartRenderer renderer) {
|
public void setRenderer(ChartRenderer renderer) {
|
||||||
|
@ -12,6 +13,10 @@ public class ChartRenderingPanel extends JPanel {
|
||||||
this.repaint();
|
this.repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ChartRenderer getRenderer() {
|
||||||
|
return renderer;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void paintComponent(Graphics g) {
|
protected void paintComponent(Graphics g) {
|
||||||
super.paintComponent(g);
|
super.paintComponent(g);
|
||||||
|
@ -23,4 +28,9 @@ public class ChartRenderingPanel extends JPanel {
|
||||||
g.drawString("No chart to render", 50, 50);
|
g.drawString("No chart to render", 50, 50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(ChartRenderer chartRenderer) {
|
||||||
|
setRenderer(chartRenderer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
package com.github.andrewlalis.running_every_day.view.chart;
|
||||||
|
|
||||||
|
import com.github.andrewlalis.running_every_day.data.DateRange;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interactive panel for selecting a date range, with multiple different options.
|
||||||
|
*/
|
||||||
|
public class DateRangePanel extends JPanel {
|
||||||
|
private record DateRangeChoice(DateRange range, String label) {
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final JTabbedPane tabbedPane = new JTabbedPane();
|
||||||
|
|
||||||
|
private final JTextField startDateField = new JTextField();
|
||||||
|
private final JCheckBox startDateEnabledCheckbox = new JCheckBox();
|
||||||
|
private final JTextField endDateField = new JTextField();
|
||||||
|
private final JCheckBox endDateEnabledCheckbox = new JCheckBox();
|
||||||
|
|
||||||
|
private final DefaultComboBoxModel<DateRangeChoice> dateRangeChoiceModel = new DefaultComboBoxModel<>();
|
||||||
|
|
||||||
|
public DateRangePanel(DateRange defaultRange) {
|
||||||
|
super(new BorderLayout());
|
||||||
|
tabbedPane.addTab("Exact Date", buildManualDatePanel(defaultRange));
|
||||||
|
tabbedPane.addTab("Preset Date", buildPresetDatePanel(defaultRange));
|
||||||
|
this.add(tabbedPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
JLabel titleLabel = new JLabel("Date Range Selector");
|
||||||
|
titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD));
|
||||||
|
this.add(titleLabel, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
this.setMinimumSize(new Dimension(300, 120));
|
||||||
|
this.setPreferredSize(this.getMinimumSize());
|
||||||
|
this.setBorder(BorderFactory.createLoweredBevelBorder());
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildManualDatePanel(DateRange defaultRange) {
|
||||||
|
JPanel manualDatePanel = new JPanel(new GridBagLayout());
|
||||||
|
GridBagConstraints c = new GridBagConstraints();
|
||||||
|
|
||||||
|
c.gridx = 0;
|
||||||
|
c.gridy = 0;
|
||||||
|
c.anchor = GridBagConstraints.WEST;
|
||||||
|
c.weightx = 0;
|
||||||
|
c.gridwidth = 1;
|
||||||
|
c.insets = new Insets(3, 3, 3, 3);
|
||||||
|
manualDatePanel.add(new JLabel("Start"), c);
|
||||||
|
|
||||||
|
c.gridy = 1;
|
||||||
|
manualDatePanel.add(new JLabel("End"), c);
|
||||||
|
|
||||||
|
c.gridx = 1;
|
||||||
|
c.gridy = 0;
|
||||||
|
c.anchor = GridBagConstraints.EAST;
|
||||||
|
c.weightx = 1;
|
||||||
|
c.fill = GridBagConstraints.HORIZONTAL;
|
||||||
|
manualDatePanel.add(startDateField, c);
|
||||||
|
|
||||||
|
c.gridy = 1;
|
||||||
|
manualDatePanel.add(endDateField, c);
|
||||||
|
|
||||||
|
c.gridx = 2;
|
||||||
|
c.gridy = 0;
|
||||||
|
c.anchor = GridBagConstraints.CENTER;
|
||||||
|
c.weightx = 0;
|
||||||
|
manualDatePanel.add(startDateEnabledCheckbox, c);
|
||||||
|
c.gridy = 1;
|
||||||
|
manualDatePanel.add(endDateEnabledCheckbox, c);
|
||||||
|
|
||||||
|
startDateEnabledCheckbox.addActionListener(e -> {
|
||||||
|
startDateField.setEnabled(startDateEnabledCheckbox.isSelected());
|
||||||
|
});
|
||||||
|
endDateEnabledCheckbox.addActionListener(e -> {
|
||||||
|
endDateField.setEnabled(endDateEnabledCheckbox.isSelected());
|
||||||
|
});
|
||||||
|
startDateEnabledCheckbox.setSelected(false);
|
||||||
|
endDateEnabledCheckbox.setSelected(false);
|
||||||
|
if (defaultRange != null) {
|
||||||
|
if (defaultRange.start() != null) {
|
||||||
|
startDateField.setText(defaultRange.start().toString());
|
||||||
|
startDateEnabledCheckbox.setSelected(true);
|
||||||
|
}
|
||||||
|
if (defaultRange.end() != null) {
|
||||||
|
endDateField.setText(defaultRange.end().toString());
|
||||||
|
endDateEnabledCheckbox.setSelected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startDateField.setEnabled(startDateEnabledCheckbox.isSelected());
|
||||||
|
endDateField.setEnabled(endDateEnabledCheckbox.isSelected());
|
||||||
|
|
||||||
|
return manualDatePanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildPresetDatePanel(DateRange defaultRange) {
|
||||||
|
JPanel panel = new JPanel();
|
||||||
|
var choices = List.of(
|
||||||
|
new DateRangeChoice(DateRange.lastNWeeks(1), "Last Week"),
|
||||||
|
new DateRangeChoice(DateRange.lastNWeeks(2), "Last 2 Weeks"),
|
||||||
|
new DateRangeChoice(DateRange.after(LocalDate.now().minusMonths(1)), "Last Month"),
|
||||||
|
new DateRangeChoice(DateRange.after(LocalDate.now().minusMonths(3)), "Last 3 Months"),
|
||||||
|
new DateRangeChoice(DateRange.after(LocalDate.now().minusYears(1)), "Last Year"),
|
||||||
|
new DateRangeChoice(DateRange.unbounded(), "All")
|
||||||
|
);
|
||||||
|
dateRangeChoiceModel.addAll(choices);
|
||||||
|
boolean assigned = false;
|
||||||
|
for (var choice : choices) {
|
||||||
|
if (choice.range().equals(defaultRange)) {
|
||||||
|
dateRangeChoiceModel.setSelectedItem(choice);
|
||||||
|
assigned = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!assigned) {
|
||||||
|
dateRangeChoiceModel.setSelectedItem(choices.get(0));
|
||||||
|
}
|
||||||
|
panel.add(new JComboBox<>(dateRangeChoiceModel));
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateRange getSelectedDateRange() {
|
||||||
|
if (tabbedPane.getSelectedIndex() == 0) {
|
||||||
|
LocalDate start = null;
|
||||||
|
LocalDate end = null;
|
||||||
|
if (startDateEnabledCheckbox.isSelected()) {
|
||||||
|
try {
|
||||||
|
start = LocalDate.parse(startDateField.getText());
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (endDateEnabledCheckbox.isSelected()) {
|
||||||
|
try {
|
||||||
|
end = LocalDate.parse(endDateField.getText());
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new DateRange(start, end);
|
||||||
|
} else {
|
||||||
|
DateRangeChoice choice = (DateRangeChoice) dateRangeChoiceModel.getSelectedItem();
|
||||||
|
return choice.range();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,8 +12,16 @@ import org.jfree.data.xy.XYDataset;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
|
import java.text.NumberFormat;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extension of the generic JFreeChartRenderer which handles formatting for
|
||||||
|
* date-series charts, which are a common type of chart that we use. This way,
|
||||||
|
* you only need to specify a title, paint for the line, and a data generator
|
||||||
|
* that produces data points.
|
||||||
|
*/
|
||||||
public class DateSeriesChartRenderer extends JFreeChartRenderer {
|
public class DateSeriesChartRenderer extends JFreeChartRenderer {
|
||||||
public record Datapoint (double value, LocalDate date) {}
|
public record Datapoint (double value, LocalDate date) {}
|
||||||
|
|
||||||
|
@ -48,11 +56,12 @@ public class DateSeriesChartRenderer extends JFreeChartRenderer {
|
||||||
XYDataset dataset = new TimeSeriesCollection(series);
|
XYDataset dataset = new TimeSeriesCollection(series);
|
||||||
|
|
||||||
DateAxis domainAxis = new DateAxis();
|
DateAxis domainAxis = new DateAxis();
|
||||||
domainAxis.setVerticalTickLabels(true);
|
domainAxis.setTickLabelsVisible(true);
|
||||||
domainAxis.setDateFormatOverride(DateFormat.getDateInstance());
|
domainAxis.setDateFormatOverride(DateFormat.getDateInstance());
|
||||||
|
|
||||||
NumberAxis rangeAxis = new NumberAxis();
|
NumberAxis rangeAxis = new NumberAxis();
|
||||||
rangeAxis.setRangeWithMargins(minValue, maxValue);
|
rangeAxis.setRangeWithMargins(minValue, maxValue);
|
||||||
|
rangeAxis.setNumberFormatOverride(NumberFormat.getNumberInstance(Locale.US));
|
||||||
|
|
||||||
XYPlot plot = new XYPlot(dataset, domainAxis, rangeAxis, new XYLineAndShapeRenderer());
|
XYPlot plot = new XYPlot(dataset, domainAxis, rangeAxis, new XYLineAndShapeRenderer());
|
||||||
var chart = new JFreeChart(title, plot);
|
var chart = new JFreeChart(title, plot);
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
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.db.DataSource;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface DateSeriesChartRendererFactory {
|
||||||
|
ChartRenderer getRenderer(DataSource dataSource, DateRange range);
|
||||||
|
}
|
|
@ -10,15 +10,12 @@ import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pre-defined list of chart renderers that can be used.
|
||||||
|
*/
|
||||||
public final class DateSeriesCharts {
|
public final class DateSeriesCharts {
|
||||||
public static ChartRenderer weight(DataSource dataSource, DateRange dateRange) {
|
public static ChartRenderer weight(DataSource dataSource, DateRange dateRange) {
|
||||||
String baseQuery = "SELECT weight, date FROM run";
|
final String query = applyDateRange("SELECT weight, date FROM run", dateRange) + " ORDER BY date ASC";
|
||||||
String dateRangeConditions = buildDateRangeConditions(dateRange);
|
|
||||||
if (dateRangeConditions != null) {
|
|
||||||
baseQuery += " WHERE " + dateRangeConditions;
|
|
||||||
}
|
|
||||||
baseQuery += " ORDER BY date ASC";
|
|
||||||
final String query = baseQuery;
|
|
||||||
return new DateSeriesChartRenderer(
|
return new DateSeriesChartRenderer(
|
||||||
"Weight (Kg)",
|
"Weight (Kg)",
|
||||||
Color.BLUE,
|
Color.BLUE,
|
||||||
|
@ -34,13 +31,7 @@ public final class DateSeriesCharts {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ChartRenderer pace(DataSource dataSource, DateRange dateRange) {
|
public static ChartRenderer pace(DataSource dataSource, DateRange dateRange) {
|
||||||
String baseQuery = "SELECT * FROM run";
|
final String query = applyDateRange("SELECT * FROM run", dateRange) + " ORDER BY date ASC";
|
||||||
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();
|
final var mapper = new RunRecord.Mapper();
|
||||||
return new DateSeriesChartRenderer(
|
return new DateSeriesChartRenderer(
|
||||||
"Pace (Min/Km)",
|
"Pace (Min/Km)",
|
||||||
|
@ -59,6 +50,35 @@ public final class DateSeriesCharts {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ChartRenderer totalDistance(DataSource dataSource, DateRange dateRange) {
|
||||||
|
final String query = applyDateRange("SELECT distance, date FROM run", dateRange) + " ORDER BY date ASC";
|
||||||
|
return new DateSeriesChartRenderer(
|
||||||
|
"Total Distance (Km)",
|
||||||
|
Color.GREEN,
|
||||||
|
() -> {
|
||||||
|
List<DateSeriesChartRenderer.Datapoint> items = Queries.findAll(
|
||||||
|
dataSource.conn(),
|
||||||
|
query,
|
||||||
|
rs -> new DateSeriesChartRenderer.Datapoint(
|
||||||
|
rs.getInt(1) / 1000.0,
|
||||||
|
LocalDate.parse(rs.getString(2))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
double total = 0;
|
||||||
|
// If a start date is specified, first compute the total distance ran up until then.
|
||||||
|
if (dateRange != null && dateRange.start() != null) {
|
||||||
|
total = Queries.getLong(dataSource.conn(), "SELECT SUM(distance) FROM run WHERE date < '" + dateRange.start() + "'") / 1000.0;
|
||||||
|
}
|
||||||
|
List<DateSeriesChartRenderer.Datapoint> finalItems = new ArrayList<>(items.size());
|
||||||
|
for (var d : items) {
|
||||||
|
total += d.value();
|
||||||
|
finalItems.add(new DateSeriesChartRenderer.Datapoint(total, d.date()));
|
||||||
|
}
|
||||||
|
return finalItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private static String buildDateRangeConditions(DateRange dateRange) {
|
private static String buildDateRangeConditions(DateRange dateRange) {
|
||||||
if (dateRange == null || (dateRange.start() == null && dateRange.end() == null)) {
|
if (dateRange == null || (dateRange.start() == null && dateRange.end() == null)) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -72,4 +92,12 @@ public final class DateSeriesCharts {
|
||||||
}
|
}
|
||||||
return String.join(" AND ", conditions);
|
return String.join(" AND ", conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String applyDateRange(String query, DateRange range) {
|
||||||
|
String rangeConditions = buildDateRangeConditions(range);
|
||||||
|
if (rangeConditions != null) {
|
||||||
|
return query + " WHERE " + rangeConditions;
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
package com.github.andrewlalis.running_every_day.view.chart;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class ExportChartImageDialog extends JDialog {
|
||||||
|
private final ChartRenderer chartRenderer;
|
||||||
|
|
||||||
|
private final SpinnerNumberModel widthSpinnerModel = new SpinnerNumberModel(1920, 10, 10000, 1);
|
||||||
|
private final SpinnerNumberModel heightSpinnerModel = new SpinnerNumberModel(1080, 10, 10000, 1);
|
||||||
|
private final JTextField filePathField = new JTextField();
|
||||||
|
private Path currentFilePath = Path.of(".").toAbsolutePath().resolve("chart.png");
|
||||||
|
|
||||||
|
public ExportChartImageDialog(Component parent, ChartRenderer chartRenderer) {
|
||||||
|
super(SwingUtilities.getWindowAncestor(parent), "Export Chart to Image", ModalityType.APPLICATION_MODAL);
|
||||||
|
this.chartRenderer = chartRenderer;
|
||||||
|
|
||||||
|
this.setLayout(new BorderLayout());
|
||||||
|
|
||||||
|
JPanel formPanel = new JPanel(new GridBagLayout());
|
||||||
|
GridBagConstraints c = new GridBagConstraints();
|
||||||
|
|
||||||
|
c.gridx = 0;
|
||||||
|
c.gridy = 0;
|
||||||
|
c.anchor = GridBagConstraints.WEST;
|
||||||
|
c.weightx = 0;
|
||||||
|
c.insets = new Insets(5, 5, 5, 5);
|
||||||
|
formPanel.add(new JLabel("Width (px)"), c);
|
||||||
|
c.gridy = 1;
|
||||||
|
formPanel.add(new JLabel("Height (px)"), c);
|
||||||
|
c.gridy = 2;
|
||||||
|
formPanel.add(new JLabel("File"), c);
|
||||||
|
c.gridy = 3;
|
||||||
|
JButton selectFileButton = new JButton("Select File");
|
||||||
|
selectFileButton.addActionListener(e -> browseForFilePath());
|
||||||
|
formPanel.add(selectFileButton, c);
|
||||||
|
|
||||||
|
c.gridx = 1;
|
||||||
|
c.gridy = 0;
|
||||||
|
c.anchor = GridBagConstraints.EAST;
|
||||||
|
c.weightx = 1;
|
||||||
|
c.fill = GridBagConstraints.HORIZONTAL;
|
||||||
|
JSpinner widthSpinner = new JSpinner(widthSpinnerModel);
|
||||||
|
widthSpinner.setLocale(Locale.US);
|
||||||
|
formPanel.add(widthSpinner, c);
|
||||||
|
c.gridy = 1;
|
||||||
|
JSpinner heightSpinner = new JSpinner(heightSpinnerModel);
|
||||||
|
heightSpinner.setLocale(Locale.US);
|
||||||
|
formPanel.add(heightSpinner, c);
|
||||||
|
|
||||||
|
c.gridy = 2;
|
||||||
|
filePathField.setEditable(false);
|
||||||
|
filePathField.setText(currentFilePath.toAbsolutePath().toString());
|
||||||
|
filePathField.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
browseForFilePath();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
formPanel.add(filePathField, c);
|
||||||
|
this.add(formPanel, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
|
JButton exportButton = new JButton("Export");
|
||||||
|
exportButton.addActionListener(e -> {
|
||||||
|
exportChart();
|
||||||
|
});
|
||||||
|
buttonPanel.add(exportButton);
|
||||||
|
JButton cancelButton = new JButton("Cancel");
|
||||||
|
cancelButton.addActionListener(e -> this.dispose());
|
||||||
|
buttonPanel.add(cancelButton);
|
||||||
|
this.add(buttonPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
this.setPreferredSize(new Dimension(600, 300));
|
||||||
|
this.pack();
|
||||||
|
this.setLocationRelativeTo(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void browseForFilePath() {
|
||||||
|
JFileChooser fileChooser = new JFileChooser(currentFilePath.toFile());
|
||||||
|
fileChooser.setFileFilter(new FileNameExtensionFilter("PNG Images", "png"));
|
||||||
|
fileChooser.setMultiSelectionEnabled(false);
|
||||||
|
int result = fileChooser.showDialog(this, "Save");
|
||||||
|
if (result == JFileChooser.APPROVE_OPTION) {
|
||||||
|
currentFilePath = fileChooser.getSelectedFile().toPath();
|
||||||
|
filePathField.setText(currentFilePath.toAbsolutePath().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportChart() {
|
||||||
|
if (Files.exists(currentFilePath)) {
|
||||||
|
int result = JOptionPane.showConfirmDialog(
|
||||||
|
this,
|
||||||
|
"The file " + currentFilePath + " already exists.\nAre you sure you want to overwrite it?",
|
||||||
|
"Confirm Overwrite",
|
||||||
|
JOptionPane.OK_CANCEL_OPTION,
|
||||||
|
JOptionPane.WARNING_MESSAGE
|
||||||
|
);
|
||||||
|
if (result != JOptionPane.OK_OPTION) return;
|
||||||
|
}
|
||||||
|
int width = (int) widthSpinnerModel.getValue();
|
||||||
|
int height = (int) heightSpinnerModel.getValue();
|
||||||
|
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
|
||||||
|
chartRenderer.render(img.createGraphics(), new Rectangle2D.Float(0, 0, width, height));
|
||||||
|
try {
|
||||||
|
ImageIO.write(img, "png", currentFilePath.toFile());
|
||||||
|
JOptionPane.showMessageDialog(
|
||||||
|
this,
|
||||||
|
"Export complete!",
|
||||||
|
"Export Complete",
|
||||||
|
JOptionPane.PLAIN_MESSAGE
|
||||||
|
);
|
||||||
|
this.dispose();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
JOptionPane.showMessageDialog(
|
||||||
|
this,
|
||||||
|
"An error occurred:\n" + e.getMessage(),
|
||||||
|
"Export Error",
|
||||||
|
JOptionPane.ERROR_MESSAGE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.github.andrewlalis.running_every_day.view.chart.menu;
|
||||||
|
|
||||||
|
import com.github.andrewlalis.running_every_day.view.chart.ChartRenderer;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.ActionListener;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class ChartMenuItem extends JPanel {
|
||||||
|
private final JButton viewButton = new JButton("View");
|
||||||
|
protected final Consumer<ChartRenderer> rendererConsumer;
|
||||||
|
|
||||||
|
public ChartMenuItem(String name, String description, Consumer<ChartRenderer> rendererConsumer) {
|
||||||
|
this.rendererConsumer = rendererConsumer;
|
||||||
|
this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
|
||||||
|
|
||||||
|
JPanel headerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||||
|
JLabel titleLabel = new JLabel(name);
|
||||||
|
titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD));
|
||||||
|
titleLabel.setHorizontalAlignment(SwingConstants.LEFT);
|
||||||
|
headerPanel.add(titleLabel);
|
||||||
|
this.add(headerPanel);
|
||||||
|
|
||||||
|
JTextArea descriptionArea = new JTextArea(0, 0);
|
||||||
|
descriptionArea.setLineWrap(true);
|
||||||
|
descriptionArea.setWrapStyleWord(true);
|
||||||
|
descriptionArea.setEditable(false);
|
||||||
|
descriptionArea.setText(description);
|
||||||
|
this.add(descriptionArea);
|
||||||
|
|
||||||
|
JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||||
|
buttonsPanel.add(viewButton);
|
||||||
|
this.add(buttonsPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setupViewAction(ActionListener l) {
|
||||||
|
viewButton.addActionListener(l);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.github.andrewlalis.running_every_day.view.chart.menu;
|
||||||
|
|
||||||
|
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.ChartRenderer;
|
||||||
|
import com.github.andrewlalis.running_every_day.view.chart.DateRangePanel;
|
||||||
|
import com.github.andrewlalis.running_every_day.view.chart.DateSeriesChartRendererFactory;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class DateSeriesChartMenuItem extends ChartMenuItem {
|
||||||
|
private final DateRange defaultDateRange;
|
||||||
|
private final DateRangePanel dateRangePanel;
|
||||||
|
|
||||||
|
public DateSeriesChartMenuItem(String name, String description, Consumer<ChartRenderer> rendererConsumer, DataSource dataSource, DateRange defaultRange, DateSeriesChartRendererFactory rendererFactory) {
|
||||||
|
super(name, description, rendererConsumer);
|
||||||
|
this.defaultDateRange = defaultRange;
|
||||||
|
this.dateRangePanel = new DateRangePanel(defaultRange);
|
||||||
|
this.add(dateRangePanel, 2);
|
||||||
|
setupViewAction(e -> {
|
||||||
|
rendererConsumer.accept(rendererFactory.getRenderer(dataSource, dateRangePanel.getSelectedDateRange()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue