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
30
31
32
33
34
35
36
37
38
39 public class BoxAndWhiskerPlot extends Plot implements EventListener
40 {
41
42 private static final long serialVersionUID = 1L;
43
44
45 public static final short BORDER_SIZE = 50;
46
47
48 public static final String PLOT_TYPE = "SUMMARY_PLOT";
49
50
51 public static final Font FONT = new Font("SansSerif", Font.PLAIN, 10);
52
53
54 public static final Font TITLE_FONT = new Font("SansSerif", Font.BOLD, 15);
55
56
57 protected List<TallyStatistic> tallies = new ArrayList<>();
58
59
60 protected NumberFormat formatter = NumberFormat.getInstance();
61
62
63 protected double confidenceInterval = 0.05;
64
65
66
67
68 public BoxAndWhiskerPlot()
69 {
70 super();
71 }
72
73
74
75
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
85
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
95
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
116
117
118
119
120
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
141
142
143
144
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
169
170
171
172
173 private Rectangle2D getBounds(final String word, final FontRenderContext context)
174 {
175 return FONT.getStringBounds(word, context);
176 }
177
178
179
180
181
182
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
192
193
194
195
196
197
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)
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)
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
278
279
280 public double getConfidenceInterval()
281 {
282 return this.confidenceInterval;
283 }
284
285
286
287
288
289 public void setConfidenceInterval(final double confidenceInterval)
290 {
291 this.confidenceInterval = confidenceInterval;
292 }
293
294 }