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 public class BoxAndWhiskerPlot extends Plot implements EventListener
41 {
42
43 private static final long serialVersionUID = 1L;
44
45
46 public static final short BORDER_SIZE = 50;
47
48
49 public static final String PLOT_TYPE = "SUMMARY_PLOT";
50
51
52 public static final Font FONT = new Font("SansSerif", Font.PLAIN, 10);
53
54
55 public static final Font TITLE_FONT = new Font("SansSerif", Font.BOLD, 15);
56
57
58 protected List<TallyStatistic> tallies = new ArrayList<>();
59
60
61 protected NumberFormat formatter = NumberFormat.getInstance();
62
63
64 protected double confidenceInterval = 0.05;
65
66
67
68
69 public BoxAndWhiskerPlot()
70 {
71 super();
72 }
73
74
75
76
77
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
87
88
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
98
99
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
120
121
122
123
124
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
145
146
147
148
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
173
174
175
176
177 private Rectangle2D getBounds(final String word, final FontRenderContext context)
178 {
179 return FONT.getStringBounds(word, context);
180 }
181
182
183
184
185
186
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
196
197
198
199
200
201
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)
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)
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
282
283
284 public double getConfidenceInterval()
285 {
286 return this.confidenceInterval;
287 }
288
289
290
291
292
293 public void setConfidenceInterval(final double confidenceInterval)
294 {
295 this.confidenceInterval = confidenceInterval;
296 }
297
298 }