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