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