From a200aee64015eea26c07928320eefbd9dfa10f7b Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Fri, 21 Apr 2023 18:57:32 +0200 Subject: [PATCH] Improved chart rendering, added export. --- .../running_every_day/data/DateRange.java | 4 + .../running_every_day/view/ChartsPanel.java | 86 +++++----- .../view/RunRecordsPanel.java | 4 - .../view/chart/ChartRenderingPanel.java | 12 +- .../view/chart/DateRangePanel.java | 153 ++++++++++++++++++ .../view/chart/DateSeriesChartRenderer.java | 11 +- .../chart/DateSeriesChartRendererFactory.java | 9 ++ .../view/chart/DateSeriesCharts.java | 56 +++++-- .../view/chart/ExportChartImageDialog.java | 134 +++++++++++++++ .../view/chart/menu/ChartMenuItem.java | 40 +++++ .../chart/menu/DateSeriesChartMenuItem.java | 24 +++ 11 files changed, 468 insertions(+), 65 deletions(-) create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateRangePanel.java create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesChartRendererFactory.java create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ExportChartImageDialog.java create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/menu/ChartMenuItem.java create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/menu/DateSeriesChartMenuItem.java diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/DateRange.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/DateRange.java index c9b2798..21b8e0f 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/DateRange.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/DateRange.java @@ -14,4 +14,8 @@ public record DateRange(LocalDate start, LocalDate end) { public static DateRange unbounded() { return new DateRange(null, null); } + + public static DateRange lastNWeeks(int n) { + return after(LocalDate.now().minusWeeks(n)); + } } 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 75081f1..264b44a 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 @@ -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.db.DataSource; 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.ExportChartImageDialog; +import com.github.andrewlalis.running_every_day.view.chart.menu.DateSeriesChartMenuItem; import javax.swing.*; import java.awt.*; +import java.awt.event.ActionListener; import java.time.LocalDate; public class ChartsPanel extends JPanel { @@ -20,55 +24,47 @@ public class ChartsPanel extends JPanel { this.chartRenderingPanel = new ChartRenderingPanel(); 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(); 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); -// 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; - } } diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java index 4aa9325..0fe786e 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java @@ -97,10 +97,6 @@ public class RunRecordsPanel extends JPanel { paginationPanel.add(lastPageButton); this.add(paginationPanel, BorderLayout.SOUTH); -// SwingUtilities.invokeLater(() -> { -// tableModel.firstPage(); -// updateButtonStates(); -// }); JPanel actionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); JButton addActionButton = new JButton("Add a Record"); 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 index ba0e968..fb97070 100644 --- 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 @@ -3,8 +3,9 @@ package com.github.andrewlalis.running_every_day.view.chart; import javax.swing.*; import java.awt.*; import java.awt.geom.Rectangle2D; +import java.util.function.Consumer; -public class ChartRenderingPanel extends JPanel { +public class ChartRenderingPanel extends JPanel implements Consumer { private ChartRenderer renderer; public void setRenderer(ChartRenderer renderer) { @@ -12,6 +13,10 @@ public class ChartRenderingPanel extends JPanel { this.repaint(); } + public ChartRenderer getRenderer() { + return renderer; + } + @Override protected void paintComponent(Graphics g) { super.paintComponent(g); @@ -23,4 +28,9 @@ public class ChartRenderingPanel extends JPanel { g.drawString("No chart to render", 50, 50); } } + + @Override + public void accept(ChartRenderer chartRenderer) { + setRenderer(chartRenderer); + } } diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateRangePanel.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateRangePanel.java new file mode 100644 index 0000000..c83d262 --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateRangePanel.java @@ -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 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(); + } + } +} diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesChartRenderer.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesChartRenderer.java index 1bf0d81..f4da993 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesChartRenderer.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesChartRenderer.java @@ -12,8 +12,16 @@ import org.jfree.data.xy.XYDataset; import java.awt.*; import java.text.DateFormat; +import java.text.NumberFormat; 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 record Datapoint (double value, LocalDate date) {} @@ -48,11 +56,12 @@ public class DateSeriesChartRenderer extends JFreeChartRenderer { XYDataset dataset = new TimeSeriesCollection(series); DateAxis domainAxis = new DateAxis(); - domainAxis.setVerticalTickLabels(true); + domainAxis.setTickLabelsVisible(true); domainAxis.setDateFormatOverride(DateFormat.getDateInstance()); NumberAxis rangeAxis = new NumberAxis(); rangeAxis.setRangeWithMargins(minValue, maxValue); + rangeAxis.setNumberFormatOverride(NumberFormat.getNumberInstance(Locale.US)); XYPlot plot = new XYPlot(dataset, domainAxis, rangeAxis, new XYLineAndShapeRenderer()); var chart = new JFreeChart(title, plot); diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesChartRendererFactory.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesChartRendererFactory.java new file mode 100644 index 0000000..c4f05d9 --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesChartRendererFactory.java @@ -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); +} diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesCharts.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesCharts.java index 5d9535e..c7b36ba 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesCharts.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/DateSeriesCharts.java @@ -10,15 +10,12 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +/** + * A pre-defined list of chart renderers that can be used. + */ 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; + final String query = applyDateRange("SELECT weight, date FROM run", dateRange) + " ORDER BY date ASC"; return new DateSeriesChartRenderer( "Weight (Kg)", Color.BLUE, @@ -34,13 +31,7 @@ public final class DateSeriesCharts { } 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 String query = applyDateRange("SELECT * FROM run", dateRange) + " ORDER BY date ASC"; final var mapper = new RunRecord.Mapper(); return new DateSeriesChartRenderer( "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 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 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) { if (dateRange == null || (dateRange.start() == null && dateRange.end() == null)) { return null; @@ -72,4 +92,12 @@ public final class DateSeriesCharts { } 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; + } } diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ExportChartImageDialog.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ExportChartImageDialog.java new file mode 100644 index 0000000..f24a998 --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/ExportChartImageDialog.java @@ -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 + ); + } + } +} diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/menu/ChartMenuItem.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/menu/ChartMenuItem.java new file mode 100644 index 0000000..7b9a01b --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/menu/ChartMenuItem.java @@ -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 rendererConsumer; + + public ChartMenuItem(String name, String description, Consumer 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); + } +} diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/menu/DateSeriesChartMenuItem.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/menu/DateSeriesChartMenuItem.java new file mode 100644 index 0000000..cc61369 --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/chart/menu/DateSeriesChartMenuItem.java @@ -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 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())); + }); + } +}