diff --git a/src/main/java/net/berack/upo/valpre/Plot.java b/src/main/java/net/berack/upo/valpre/Plot.java index efa0c83..6e06459 100644 --- a/src/main/java/net/berack/upo/valpre/Plot.java +++ b/src/main/java/net/berack/upo/valpre/Plot.java @@ -1,19 +1,24 @@ package net.berack.upo.valpre; import java.awt.BorderLayout; -import java.awt.GridLayout; +import java.awt.Font; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import javax.swing.BorderFactory; +import javax.swing.Box; import javax.swing.JComboBox; import javax.swing.JFrame; import javax.swing.JLabel; +import javax.swing.JList; import javax.swing.JPanel; +import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; +import org.jfree.chart.axis.CategoryLabelPositions; import org.jfree.chart.plot.PlotOrientation; import org.jfree.data.category.DefaultCategoryDataset; @@ -27,9 +32,9 @@ import net.berack.upo.valpre.sim.stats.Statistics; */ public class Plot { public final ResultSummary summary; - private final ChartPanel chartPanel; + private final ChartPanel panelBarChart; private final JComboBox nodeComboBox; - private final JComboBox statComboBox; + private final JList statList; /** * Create a new plot object. @@ -47,45 +52,71 @@ public class Plot { this.summary = new ResultSummary(results); var nodes = this.summary.getNodes().toArray(new String[0]); - this.chartPanel = new ChartPanel(null); + this.panelBarChart = new ChartPanel(null); + this.nodeComboBox = new JComboBox<>(nodes); - this.statComboBox = new JComboBox<>(Statistics.getOrderOfApply()); + this.nodeComboBox.addActionListener(_ -> update()); + + var order = Statistics.getOrderOfApply(); + var panels = new JListEntry[order.length]; + for (int i = 0; i < order.length; i++) + panels[i] = new JListEntry(order[i]); + + this.statList = new JList<>(panels); + this.statList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + this.statList.addListSelectionListener(_ -> update()); + this.statList.setFixedCellHeight(25); + this.statList.setCellRenderer((list, val, _, selected, _) -> { + var bgColor = list.getBackground(); + var bgSelColor = list.getSelectionBackground(); + val.setBackground(selected ? bgSelColor : bgColor); + + return val; + }); } /** * Show the plot of the results. + * This method creates the GUI and shows the plot of the results. + * The user can select the node and the statistic to show. + * The plot is updated when the user selects a different node or statistic. + * The plot shows the distribution of the runs and the mean and error of the + * statistic. + * The plot is shown in a new window. */ public void show() { SwingUtilities.invokeLater(() -> { - var nodeLabel = new JLabel("Node: "); - var statLabel = new JLabel("Stat: "); - - var filterPanel = new JPanel(); - filterPanel.setLayout(new GridLayout(2, 2)); - filterPanel.add(nodeLabel); - filterPanel.add(nodeComboBox); - filterPanel.add(statLabel); - filterPanel.add(statComboBox); - - nodeComboBox.addActionListener(_ -> updateChart()); - statComboBox.addActionListener(_ -> updateChart()); - - var rootPane = new JPanel(); - rootPane.setLayout(new BorderLayout()); - rootPane.add(filterPanel, BorderLayout.NORTH); - rootPane.add(chartPanel, BorderLayout.CENTER); - - chartPanel.setChart(ChartFactory.createBarChart( - "Title", - "Run", - "Value", + // Create charts with empty data + this.panelBarChart.setChart(ChartFactory.createBarChart( + "Run Distributions", + "", + "", null, PlotOrientation.VERTICAL, - true, + false, true, false)); - updateChart(); + this.panelBarChart.getChart().getCategoryPlot().getDomainAxis() + .setCategoryLabelPositions(CategoryLabelPositions.UP_45); + // Create the GUI with the various layouts and components + var filterPanel = new JPanel(); + filterPanel.setLayout(new BorderLayout()); + filterPanel.add(new JLabel("Node: "), BorderLayout.WEST); + filterPanel.add(this.nodeComboBox, BorderLayout.CENTER); + filterPanel.setBorder(BorderFactory.createEmptyBorder(20, 0, 20, 0)); + + var rootPane = new JPanel(); + rootPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + rootPane.setLayout(new BorderLayout()); + rootPane.add(filterPanel, BorderLayout.NORTH); + rootPane.add(this.statList, BorderLayout.WEST); + rootPane.add(this.panelBarChart, BorderLayout.CENTER); + + // update the charts by triggering the event + this.statList.setSelectedIndex(0); + + // Show the frame var frame = new JFrame("Graph of the Simulation"); frame.add(rootPane); frame.setSize(800, 600); @@ -97,22 +128,30 @@ public class Plot { /** * Update the chart with the selected node and stat. */ - private void updateChart() { + private void update() { try { var node = this.nodeComboBox.getSelectedItem().toString(); - var stat = this.statComboBox.getSelectedItem().toString(); + var stat = this.statList.getSelectedValue().name.getText(); - var summary = this.summary.getSummaryOf(node, stat); - var frequency = summary.getFrequency(20); + var summary = this.summary.getSummaryOf(node); + var statSummary = summary.get(stat); + var frequency = statSummary.getFrequency(15); var dataset = new DefaultCategoryDataset(); + var bucket = (statSummary.max - statSummary.min) / frequency.length; for (int i = 0; i < frequency.length; i++) { - dataset.addValue(frequency[i], "Frequency", Integer.valueOf(i)); + var columnVal = statSummary.min + i * bucket; + var columnKey = String.format("%.3f", columnVal); + dataset.addValue(frequency[i], "Frequency", columnKey); } + this.panelBarChart.getChart().getCategoryPlot().setDataset(dataset); - var chart = chartPanel.getChart(); - chart.getCategoryPlot().setDataset(dataset); - chart.setTitle(String.format("Avg %.3f", summary.average)); + var model = this.statList.getModel(); + for (int i = 0; i < model.getSize(); i++) { + var entry = model.getElementAt(i); + var value = summary.get(entry.name.getText()); + entry.value.setText(String.format("%8.3f ±% 9.3f", value.average, value.error95)); + } } catch (Exception e) { e.printStackTrace(); } @@ -133,4 +172,32 @@ public class Plot { return Parameters.getArgsOrHelper(args, "-", arguments, descriptions); } + + /** + * This class is used to create a panel with a name and a value. + * The name is on the left and the value is on the right. + * The name is in bold and the value is in plain. + */ + private static class JListEntry extends JPanel { + public static final Font fontName = new Font("Consolas", Font.BOLD, 14); + public static final Font fontValue = new Font("Consolas", Font.PLAIN, 12); + + public final JLabel name = new JLabel(); + public final JLabel value = new JLabel(); + + public JListEntry(String text) { + this.name.setText(text); + + this.setLayout(new BorderLayout()); + this.add(this.name, BorderLayout.WEST); + this.add(Box.createHorizontalStrut(100), BorderLayout.CENTER); + this.add(this.value, BorderLayout.EAST); + + this.name.setHorizontalAlignment(JLabel.LEFT); + this.value.setHorizontalAlignment(JLabel.RIGHT); + + this.name.setFont(fontName); + this.value.setFont(fontValue); + } + } } diff --git a/src/main/java/net/berack/upo/valpre/sim/stats/StatisticsSummary.java b/src/main/java/net/berack/upo/valpre/sim/stats/StatisticsSummary.java index 48d9202..432d574 100644 --- a/src/main/java/net/berack/upo/valpre/sim/stats/StatisticsSummary.java +++ b/src/main/java/net/berack/upo/valpre/sim/stats/StatisticsSummary.java @@ -32,14 +32,13 @@ public class StatisticsSummary { Arrays.sort(values); var sum = Arrays.stream(values).sum(); var avg = sum / values.length; - var median = values.length / 2; var varianceSum = Arrays.stream(values).map(value -> Math.pow(value - avg, 2)).sum(); this.name = name; this.values = values; this.average = avg; this.stdDev = Math.sqrt(varianceSum / values.length); - this.median = values.length % 2 == 0 ? (values[median - 1] + values[median]) / 2.0 : values[median]; + this.median = this.getPercentile(0.50); this.min = values[0]; this.max = values[values.length - 1]; this.error95 = this.calcError(0.95); @@ -82,6 +81,17 @@ public class StatisticsSummary { return buckets; } + /** + * Get the percentile of the values in the array. + * + * @param percentile the percentile to calculate + * @return the value at the selected percentile + */ + public double getPercentile(double percentile) { + var index = (int) Math.floor(percentile * (this.values.length - 1)); + return this.values[index]; + } + /** * Get a summary of the statistics. *