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() {
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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<ChartRenderer> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.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);
|
||||
|
|
|
@ -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.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<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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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