View Javadoc
1   package nl.tudelft.simulation.dsol.swing.charts.boxwhisker;
2   
3   import java.awt.Color;
4   import java.awt.Font;
5   import java.awt.Graphics2D;
6   import java.awt.font.FontRenderContext;
7   import java.awt.geom.Point2D;
8   import java.awt.geom.Rectangle2D;
9   import java.rmi.RemoteException;
10  import java.text.NumberFormat;
11  import java.util.ArrayList;
12  import java.util.List;
13  
14  import org.djutils.event.Event;
15  import org.djutils.event.EventListener;
16  import org.djutils.event.reference.ReferenceType;
17  import org.djutils.stats.summarizers.Tally;
18  import org.djutils.stats.summarizers.TallyStatistic;
19  import org.djutils.stats.summarizers.WeightedTally;
20  import org.djutils.stats.summarizers.event.EventBasedTally;
21  import org.djutils.stats.summarizers.event.EventBasedTimestampWeightedTally;
22  import org.djutils.stats.summarizers.event.EventBasedWeightedTally;
23  import org.djutils.stats.summarizers.event.StatisticsEvents;
24  import org.jfree.chart.event.PlotChangeEvent;
25  import org.jfree.chart.plot.Plot;
26  import org.jfree.chart.plot.PlotRenderingInfo;
27  import org.jfree.chart.plot.PlotState;
28  
29  /**
30   * The Summary chart class defines a summary chart..
31   * <p>
32   * Copyright (c) 2002-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
33   * for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The DSOL
34   * project is distributed under a three-clause BSD-style license, which can be found at
35   * <a href="https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">DSOL License</a>.
36   * </p>
37   * @author <a href="https://www.linkedin.com/in/peterhmjacobs">Peter Jacobs </a>
38   * @author <a href="mailto:a.verbraeck@tudelft.nl"> Alexander Verbraeck </a>
39   */
40  public class BoxAndWhiskerPlot extends Plot implements EventListener
41  {
42      /** serialversionUId. */
43      private static final long serialVersionUID = 1L;
44  
45      /** BORDER_SIZE refers to the width of the border on the panel. */
46      public static final short BORDER_SIZE = 50;
47  
48      /** PLOT_TYPE refers to the plot type. */
49      public static final String PLOT_TYPE = "SUMMARY_PLOT";
50  
51      /** FONT defines the font of the plot. */
52      public static final Font FONT = new Font("SansSerif", Font.PLAIN, 10);
53  
54      /** TITLE_FONT defines the font of the plot. */
55      public static final Font TITLE_FONT = new Font("SansSerif", Font.BOLD, 15);
56  
57      /** target is the tally to represent. */
58      protected List<TallyStatistic> tallies = new ArrayList<>();
59  
60      /** formatter formats the text. */
61      protected NumberFormat formatter = NumberFormat.getInstance();
62  
63      /** the confidenceInterval. */
64      protected double confidenceInterval = 0.05;
65  
66      /**
67       * constructs a new BoxAndWhiskerPlot.
68       */
69      public BoxAndWhiskerPlot()
70      {
71          super();
72      }
73  
74      /**
75       * adds a tally to the array of targets.
76       * @param tally Tally; the tally to be summarized
77       * @throws RemoteException on network failure
78       */
79      public synchronized void add(final EventBasedTally tally) throws RemoteException
80      {
81          tally.addListener(this, StatisticsEvents.SAMPLE_MEAN_EVENT, ReferenceType.STRONG);
82          this.tallies.add(tally);
83      }
84  
85      /**
86       * adds a tally to the array of targets.
87       * @param tally EventBasedWeightedTally; the tally to be summarized
88       * @throws RemoteException on network failure
89       */
90      public synchronized void add(final EventBasedWeightedTally tally) throws RemoteException
91      {
92          tally.addListener(this, StatisticsEvents.WEIGHTED_SAMPLE_MEAN_EVENT, ReferenceType.STRONG);
93          this.tallies.add(tally);
94      }
95  
96      /**
97       * adds a tally to the array of targets.
98       * @param tally EventBasedTimestampWeightedTally; the tally to be summarized
99       * @throws RemoteException on network failure
100      */
101     public synchronized void add(final EventBasedTimestampWeightedTally tally) throws RemoteException
102     {
103         tally.addListener(this, StatisticsEvents.TIMED_WEIGHTED_SAMPLE_MEAN_EVENT, ReferenceType.STRONG);
104         this.tallies.add(tally);
105     }
106 
107     @Override
108     public String getPlotType()
109     {
110         return PLOT_TYPE;
111     }
112 
113     @Override
114     public void notify(final Event event)
115     {
116         this.notifyListeners(new PlotChangeEvent(this));
117     }
118 
119     /** ************ PRIVATE METHODS *********************** */
120 
121     /**
122      * computes the extent of the targets.
123      * @param tallies Tally[]; the range of tallies
124      * @return double[min,max]
125      */
126     private static double[] extent(final List<TallyStatistic> tallies)
127     {
128         double[] result = {Double.MAX_VALUE, -Double.MAX_VALUE};
129         for (int i = 0; i < tallies.size(); i++)
130         {
131             if (tallies.get(i).getMin() < result[0])
132             {
133                 result[0] = tallies.get(i).getMin();
134             }
135             if (tallies.get(i).getMax() > result[1])
136             {
137                 result[1] = tallies.get(i).getMax();
138             }
139         }
140         return result;
141     }
142 
143     /**
144      * determines the borders on the left and right side of the tally.
145      * @param g2 Graphics2D; the graphics object
146      * @param context FontRenderContext; the context
147      * @param tallyList Tally[]; tallies
148      * @return double[] the extent
149      */
150     private double[] borders(final Graphics2D g2, final FontRenderContext context, final List<TallyStatistic> tallyList)
151     {
152         double[] result = {0, 0};
153         for (int i = 0; i < tallyList.size(); i++)
154         {
155             double left = g2.getFont().getStringBounds(this.formatter.format(tallyList.get(i).getMin()), context).getWidth();
156             double rigth = g2.getFont().getStringBounds(this.formatter.format(tallyList.get(i).getMax()), context).getWidth();
157             if (left > result[0])
158             {
159                 result[0] = left;
160             }
161             if (rigth > result[1])
162             {
163                 result[1] = rigth;
164             }
165         }
166         result[0] = result[0] + 3;
167         result[1] = result[1] + 3;
168         return result;
169     }
170 
171     /**
172      * returns the bounding box.
173      * @param word String; the word
174      * @param context FontRenderContext; the context
175      * @return Rectangle2D the bounds
176      */
177     private Rectangle2D getBounds(final String word, final FontRenderContext context)
178     {
179         return FONT.getStringBounds(word, context);
180     }
181 
182     /**
183      * fills a rectangle.
184      * @param g2 Graphics2D; the graphics object
185      * @param rectangle Rectangle2D; the area
186      * @param color Color; the color
187      */
188     private void fillRectangle(final Graphics2D g2, final Rectangle2D rectangle, final Color color)
189     {
190         g2.setColor(color);
191         g2.fillRect((int) rectangle.getX(), (int) rectangle.getY(), (int) rectangle.getWidth(), (int) rectangle.getHeight());
192     }
193 
194     /**
195      * paints a tally.
196      * @param g2 Graphics2D; the graphics object
197      * @param rectangle Rectangle2D; the rectangle on which to paint
198      * @param tally Tally; the tally
199      * @param leftX double; the lowest real value
200      * @param leftBorder double; the left border
201      * @param scale double; the scale
202      */
203     private void paintTally(final Graphics2D g2, final Rectangle2D rectangle, final TallyStatistic tally, final double leftX,
204             final double leftBorder, final double scale)
205     {
206         this.fillRectangle(g2, rectangle, Color.WHITE);
207         g2.setColor(Color.BLACK);
208         g2.setFont(TITLE_FONT);
209         g2.drawString(tally.getDescription(),
210                 (int) Math.round(leftBorder + 0.5 * (rectangle.getWidth() - leftBorder - 20)
211                         - 0.5 * this.getBounds(tally.getDescription(), g2.getFontRenderContext()).getWidth()),
212                 25 + (int) rectangle.getY());
213         g2.setFont(FONT);
214         g2.drawRect((int) rectangle.getX() - 1, (int) rectangle.getY() - 1, (int) rectangle.getWidth() + 2,
215                 (int) rectangle.getHeight() + 2);
216         int tallyMin = (int) Math.round(rectangle.getX() + (tally.getMin() - leftX) * scale + leftBorder);
217         int tallyMax = (int) Math.round(rectangle.getX() + (tally.getMax() - leftX) * scale + leftBorder);
218         int middle = (int) Math.round(rectangle.getY() + 0.5 * rectangle.getHeight());
219         String label = this.formatter.format(tally.getMin());
220         g2.drawString(label, (int) Math.round(tallyMin - 3 - this.getBounds(label, g2.getFontRenderContext()).getWidth()),
221                 (int) Math.round(middle + 0.5 * this.getBounds(label, g2.getFontRenderContext()).getHeight()));
222         label = this.formatter.format(tally.getMax());
223         g2.drawString(label, tallyMax + 3,
224                 (int) Math.round(middle + 0.5 * this.getBounds(label, g2.getFontRenderContext()).getHeight()));
225         g2.drawLine(tallyMin, middle + 6, tallyMin, middle - 6);
226         g2.drawLine(tallyMin, middle, tallyMax, middle);
227         g2.drawLine(tallyMax, middle + 6, tallyMax, middle - 6);
228         if (tally instanceof Tally) // EventBasedTally extends Tally
229         {
230             Tally unweightedTally = (Tally) tally;
231             double[] confidence = unweightedTally.getConfidenceInterval(this.confidenceInterval);
232             int middleX = (int) Math.round((unweightedTally.getSampleMean() - leftX) * scale + tallyMin);
233             g2.fillRect(middleX, middle - 6, 2, 12);
234             label = this.formatter.format(unweightedTally.getSampleMean());
235             Rectangle2D bounds = this.getBounds(label, g2.getFontRenderContext());
236             g2.drawString(label, (int) Math.round(middleX - 0.5 * bounds.getWidth()), Math.round(middle - 8));
237             if (confidence != null)
238             {
239                 int confX = (int) Math.round((confidence[0] - leftX) * scale + tallyMin);
240                 int confWidth = (int) Math.round((confidence[1] - confidence[0]) * scale);
241                 g2.fillRect(confX, middle - 2, confWidth, 4);
242                 label = this.formatter.format(confidence[0]);
243                 bounds = this.getBounds(label, g2.getFontRenderContext());
244                 g2.drawString(label, (int) Math.round(confX - bounds.getWidth()),
245                         (int) Math.round(middle + 8 + bounds.getHeight()));
246                 label = this.formatter.format(confidence[1]);
247                 bounds = this.getBounds(label, g2.getFontRenderContext());
248                 g2.drawString(label, Math.round(confX + confWidth), (int) Math.round(middle + 8 + bounds.getHeight()));
249             }
250         }
251         else if (tally instanceof WeightedTally) // EventBasedWeightedTally and Timestamp classes extend WeightedTally
252         {
253             WeightedTally weightedTally = (WeightedTally) tally;
254             int middleX = (int) Math.round((weightedTally.getWeightedSampleMean() - leftX) * scale + tallyMin);
255             g2.fillRect(middleX, middle - 6, 2, 12);
256             label = this.formatter.format(weightedTally.getWeightedSampleMean());
257             Rectangle2D bounds = this.getBounds(label, g2.getFontRenderContext());
258             g2.drawString(label, (int) Math.round(middleX - 0.5 * bounds.getWidth()), Math.round(middle - 8));
259         }
260     }
261 
262     @Override
263     public void draw(final Graphics2D g2, final Rectangle2D rectangle, final Point2D point, final PlotState plotState,
264             final PlotRenderingInfo plotRenderingInfo)
265     {
266         g2.setBackground(Color.WHITE);
267         double height = Math.min(rectangle.getHeight() / this.tallies.size() * 1.0, rectangle.getHeight());
268         double[] extent = BoxAndWhiskerPlot.extent(this.tallies);
269         double[] border = this.borders(g2, g2.getFontRenderContext(), this.tallies);
270         double scale = (0.85 * rectangle.getWidth() - 10 - border[0] - border[1]) / ((extent[1] - extent[0]) * 1.0);
271         for (int i = 0; i < this.tallies.size(); i++)
272         {
273             g2.setFont(FONT);
274             Rectangle2D area = new Rectangle2D.Double(rectangle.getX() + 0.15 * rectangle.getWidth(),
275                     rectangle.getY() + i * height + 3, 0.85 * rectangle.getWidth() - 10, 0.75 * height - 3);
276             this.paintTally(g2, area, this.tallies.get(i), extent[0], border[0], scale);
277         }
278     }
279 
280     /**
281      * Returns the confidence interval of the BoxAndWhiskerPlot.
282      * @return the confidence interval of the BoxAndWhiskerPlot
283      */
284     public double getConfidenceInterval()
285     {
286         return this.confidenceInterval;
287     }
288 
289     /**
290      * sets the confidence interval of the plot. The default value = 0.05 (=5%)
291      * @param confidenceInterval double; the confidence interval
292      */
293     public void setConfidenceInterval(final double confidenceInterval)
294     {
295         this.confidenceInterval = confidenceInterval;
296     }
297 
298 }