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
31
32
33
34
35
36
37
38
39
40
41 public class BoxAndWhiskerPlot extends Plot implements EventListener
42 {
43
44 private static final long serialVersionUID = 1L;
45
46
47 public static final short BORDER_SIZE = 50;
48
49
50 public static final String PLOT_TYPE = "SUMMARY_PLOT";
51
52
53 public static final Font FONT = new Font("SansSerif", Font.PLAIN, 10);
54
55
56 public static final Font TITLE_FONT = new Font("SansSerif", Font.BOLD, 15);
57
58
59 protected List<TallyStatistic> tallies = new ArrayList<>();
60
61
62 protected NumberFormat formatter = NumberFormat.getInstance();
63
64
65 protected double confidenceInterval = 0.05;
66
67
68
69
70 public BoxAndWhiskerPlot()
71 {
72 super();
73 }
74
75
76
77
78
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
88
89
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
99
100
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
109 @Override
110 public String getPlotType()
111 {
112 return PLOT_TYPE;
113 }
114
115
116 @Override
117 public void notify(final Event event)
118 {
119 this.notifyListeners(new PlotChangeEvent(this));
120 }
121
122
123
124
125
126
127
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
148
149
150
151
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
176
177
178
179
180 private Rectangle2D getBounds(final String word, final FontRenderContext context)
181 {
182 return FONT.getStringBounds(word, context);
183 }
184
185
186
187
188
189
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
199
200
201
202
203
204
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)
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)
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
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
286
287
288 public double getConfidenceInterval()
289 {
290 return this.confidenceInterval;
291 }
292
293
294
295
296
297 public void setConfidenceInterval(final double confidenceInterval)
298 {
299 this.confidenceInterval = confidenceInterval;
300 }
301
302 }