- Plot class to use JList for statistics selection and improve chart update logic
This commit is contained in:
2025-02-02 21:56:22 +01:00
parent 7a5b2cc078
commit 214705b675
2 changed files with 116 additions and 39 deletions

View File

@@ -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<String> nodeComboBox;
private final JComboBox<String> statComboBox;
private final JList<JListEntry> 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);
}
}
}

View File

@@ -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.
*