View Javadoc

1   /* ============================================================
2    * JRobin : Pure java implementation of RRDTool's functionality
3    * ============================================================
4    *
5    * Project Info:  http://www.jrobin.org
6    * Project Lead:  Sasa Markovic (saxon@jrobin.org)
7    *
8    * Developers:    Sasa Markovic (saxon@jrobin.org)
9    *
10   *
11   * (C) Copyright 2003-2005, by Sasa Markovic.
12   *
13   * This library is free software; you can redistribute it and/or modify it under the terms
14   * of the GNU Lesser General Public License as published by the Free Software Foundation;
15   * either version 2.1 of the License, or (at your option) any later version.
16   *
17   * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
18   * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
19   * See the GNU Lesser General Public License for more details.
20   *
21   * You should have received a copy of the GNU Lesser General Public License along with this
22   * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
23   * Boston, MA 02111-1307, USA.
24   */
25  package org.jrobin.graph;
26  
27  import org.jrobin.core.RrdException;
28  import org.jrobin.core.Util;
29  import org.jrobin.data.DataProcessor;
30  
31  import javax.swing.*;
32  import java.awt.*;
33  import java.io.IOException;
34  
35  /**
36   * Class which actually creates JRobin graphs (does the hard work).
37   */
38  public class RrdGraph implements RrdGraphConstants {
39  	RrdGraphDef gdef;
40  	ImageParameters im = new ImageParameters();
41  	DataProcessor dproc;
42  	ImageWorker worker;
43  	Mapper mapper;
44  	RrdGraphInfo info = new RrdGraphInfo();
45  	private String signature;
46  
47  	/**
48  	 * Creates graph from the corresponding {@link RrdGraphDef} object.
49  	 *
50  	 * @param gdef Graph definition
51  	 * @throws IOException  Thrown in case of I/O error
52  	 * @throws RrdException Thrown in case of JRobin related error
53  	 */
54  	public RrdGraph(RrdGraphDef gdef) throws IOException, RrdException {
55  		this.gdef = gdef;
56  		signature = gdef.getSignature();
57  		worker = new ImageWorker(100, 100); // Dummy worker, just to start with something
58  		try {
59  			createGraph();
60  		}
61  		finally {
62  			worker.dispose();
63  			worker = null;
64  			dproc = null;
65  		}
66  	}
67  
68  	/**
69  	 * Returns complete graph information in a single object.
70  	 *
71  	 * @return Graph information (width, height, filename, image bytes, etc...)
72  	 */
73  	public RrdGraphInfo getRrdGraphInfo() {
74  		return info;
75  	}
76  
77  	private void createGraph() throws RrdException, IOException {
78  		boolean lazy = lazyCheck();
79  		if (!lazy || gdef.printStatementCount() != 0) {
80  			fetchData();
81  			resolveTextElements();
82  			if (gdef.shouldPlot() && !lazy) {
83  				calculatePlotValues();
84  				findMinMaxValues();
85  				identifySiUnit();
86  				expandValueRange();
87  				removeOutOfRangeRules();
88  				initializeLimits();
89  				placeLegends();
90  				createImageWorker();
91  				drawBackground();
92  				drawData();
93  				drawGrid();
94  				drawAxis();
95  				drawText();
96  				drawLegend();
97  				drawRules();
98  				gator();
99  				drawOverlay();
100 				saveImage();
101 			}
102 		}
103 		collectInfo();
104 	}
105 
106 	private void collectInfo() {
107 		info.filename = gdef.filename;
108 		info.width = im.xgif;
109 		info.height = im.ygif;
110 		for (CommentText comment : gdef.comments) {
111 			if (comment instanceof PrintText) {
112 				PrintText pt = (PrintText) comment;
113 				if (pt.isPrint()) {
114 					info.addPrintLine(pt.resolvedText);
115 				}
116 			}
117 		}
118 		if (gdef.imageInfo != null) {
119 			info.imgInfo = Util.sprintf(gdef.imageInfo, gdef.filename, im.xgif, im.ygif);
120 		}
121 	}
122 
123 	private void saveImage() throws IOException {
124 		if (!gdef.filename.equals("-")) {
125 			info.bytes = worker.saveImage(gdef.filename, gdef.imageFormat, gdef.imageQuality);
126 		}
127 		else {
128 			info.bytes = worker.getImageBytes(gdef.imageFormat, gdef.imageQuality);
129 		}
130 	}
131 
132 	private void drawOverlay() throws IOException {
133 		if (gdef.overlayImage != null) {
134 			worker.loadImage(gdef.overlayImage);
135 		}
136 	}
137 
138 	private void gator() {
139 		if (!gdef.onlyGraph && gdef.showSignature) {
140 			Font font = gdef.getSmallFont().deriveFont(Font.PLAIN, 9);
141 			int x = (int) (im.xgif - 2 - worker.getFontAscent(font));
142 			int y = 4;
143 			worker.transform(x, y, Math.PI / 2);
144 			worker.drawString(signature, 0, 0, font, Color.LIGHT_GRAY);
145 			worker.reset();
146 		}
147 	}
148 
149 	private void drawRules() {
150 		worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2);
151 		for (PlotElement pe : gdef.plotElements) {
152 			if (pe instanceof HRule) {
153 				HRule hr = (HRule) pe;
154 				if (hr.value >= im.minval && hr.value <= im.maxval) {
155 					int y = mapper.ytr(hr.value);
156 					worker.drawLine(im.xorigin, y, im.xorigin + im.xsize, y, hr.color, new BasicStroke(hr.width));
157 				}
158 			}
159 			else if (pe instanceof VRule) {
160 				VRule vr = (VRule) pe;
161 				if (vr.timestamp >= im.start && vr.timestamp <= im.end) {
162 					int x = mapper.xtr(vr.timestamp);
163 					worker.drawLine(x, im.yorigin, x, im.yorigin - im.ysize, vr.color, new BasicStroke(vr.width));
164 				}
165 			}
166 		}
167 		worker.reset();
168 	}
169 
170 	private void drawText() {
171 		if (!gdef.onlyGraph) {
172 			if (gdef.title != null) {
173 				int x = im.xgif / 2 - (int) (worker.getStringWidth(gdef.title, gdef.largeFont) / 2);
174 				int y = PADDING_TOP + (int) worker.getFontAscent(gdef.largeFont);
175 				worker.drawString(gdef.title, x, y, gdef.largeFont, gdef.colors[COLOR_FONT]);
176 			}
177 			if (gdef.verticalLabel != null) {
178 				int x = PADDING_LEFT;
179 				int y = im.yorigin - im.ysize / 2 + (int) worker.getStringWidth(gdef.verticalLabel, gdef.getSmallFont()) / 2;
180 				int ascent = (int) worker.getFontAscent(gdef.smallFont);
181 				worker.transform(x, y, -Math.PI / 2);
182 				worker.drawString(gdef.verticalLabel, 0, ascent, gdef.smallFont, gdef.colors[COLOR_FONT]);
183 				worker.reset();
184 			}
185 		}
186 	}
187 
188 	private void drawGrid() {
189 		if (!gdef.onlyGraph) {
190 			Paint shade1 = gdef.colors[COLOR_SHADEA], shade2 = gdef.colors[COLOR_SHADEB];
191 			Stroke borderStroke = new BasicStroke(1);
192 			worker.drawLine(0, 0, im.xgif - 1, 0, shade1, borderStroke);
193 			worker.drawLine(1, 1, im.xgif - 2, 1, shade1, borderStroke);
194 			worker.drawLine(0, 0, 0, im.ygif - 1, shade1, borderStroke);
195 			worker.drawLine(1, 1, 1, im.ygif - 2, shade1, borderStroke);
196 			worker.drawLine(im.xgif - 1, 0, im.xgif - 1, im.ygif - 1, shade2, borderStroke);
197 			worker.drawLine(0, im.ygif - 1, im.xgif - 1, im.ygif - 1, shade2, borderStroke);
198 			worker.drawLine(im.xgif - 2, 1, im.xgif - 2, im.ygif - 2, shade2, borderStroke);
199 			worker.drawLine(1, im.ygif - 2, im.xgif - 2, im.ygif - 2, shade2, borderStroke);
200 			if (gdef.drawXGrid) {
201 				new TimeAxis(this).draw();
202 			}
203 			if (gdef.drawYGrid) {
204 				boolean ok;
205 				if (gdef.altYMrtg) {
206 					ok = new ValueAxisMrtg(this).draw();
207 				}
208 				else if (gdef.logarithmic) {
209 					ok = new ValueAxisLogarithmic(this).draw();
210 				}
211 				else {
212 					ok = new ValueAxis(this).draw();
213 				}
214 				if (!ok) {
215 					String msg = "No Data Found";
216 					worker.drawString(msg,
217 							im.xgif / 2 - (int) worker.getStringWidth(msg, gdef.largeFont) / 2,
218 							(2 * im.yorigin - im.ysize) / 2,
219 							gdef.largeFont, gdef.colors[COLOR_FONT]);
220 				}
221 			}
222 		}
223 	}
224 
225 	private void drawData() throws RrdException {
226 		worker.setAntiAliasing(gdef.antiAliasing);
227 		worker.clip(im.xorigin + 1, im.yorigin - gdef.height - 1, gdef.width - 1, gdef.height + 2);
228 		double areazero = mapper.ytr((im.minval > 0.0) ? im.minval : (im.maxval < 0.0) ? im.maxval : 0.0);
229 		double[] x = xtr(dproc.getTimestamps()), lastY = null;
230 		// draw line, area and stack
231 		for (PlotElement plotElement : gdef.plotElements) {
232 			if (plotElement instanceof SourcedPlotElement) {
233 				SourcedPlotElement source = (SourcedPlotElement) plotElement;
234 				double[] y = ytr(source.getValues());
235 				if (source instanceof Line) {
236 					worker.drawPolyline(x, y, source.color, new BasicStroke(((Line) source).width));
237 				}
238 				else if (source instanceof Area) {
239 					worker.fillPolygon(x, areazero, y, source.color);
240 				}
241 				else if (source instanceof Stack) {
242 					Stack stack = (Stack) source;
243 					float width = stack.getParentLineWidth();
244 					if (width >= 0F) {
245 						// line
246 						worker.drawPolyline(x, y, stack.color, new BasicStroke(width));
247 					}
248 					else {
249 						// area
250 						worker.fillPolygon(x, lastY, y, stack.color);
251 						worker.drawPolyline(x, lastY, stack.getParentColor(), new BasicStroke(0));
252 					}
253 				}
254 				else {
255 					// should not be here
256 					throw new RrdException("Unknown plot source: " + source.getClass().getName());
257 				}
258 				lastY = y;
259 			}
260 		}
261 		worker.reset();
262 		worker.setAntiAliasing(false);
263 	}
264 
265 	private void drawAxis() {
266 		if (!gdef.onlyGraph) {
267 			Paint gridColor = gdef.colors[COLOR_GRID];
268 			Paint fontColor = gdef.colors[COLOR_FONT];
269 			Paint arrowColor = gdef.colors[COLOR_ARROW];
270 			Stroke stroke = new BasicStroke(1);
271 			worker.drawLine(im.xorigin + im.xsize, im.yorigin, im.xorigin + im.xsize, im.yorigin - im.ysize,
272 					gridColor, stroke);
273 			worker.drawLine(im.xorigin, im.yorigin - im.ysize, im.xorigin + im.xsize, im.yorigin - im.ysize,
274 					gridColor, stroke);
275 			worker.drawLine(im.xorigin - 4, im.yorigin, im.xorigin + im.xsize + 4, im.yorigin,
276 					fontColor, stroke);
277 			worker.drawLine(im.xorigin, im.yorigin, im.xorigin, im.yorigin - im.ysize,
278 					gridColor, stroke);
279 			worker.drawLine(im.xorigin + im.xsize + 4, im.yorigin - 3, im.xorigin + im.xsize + 4, im.yorigin + 3,
280 					arrowColor, stroke);
281 			worker.drawLine(im.xorigin + im.xsize + 4, im.yorigin - 3, im.xorigin + im.xsize + 9, im.yorigin,
282 					arrowColor, stroke);
283 			worker.drawLine(im.xorigin + im.xsize + 4, im.yorigin + 3, im.xorigin + im.xsize + 9, im.yorigin,
284 					arrowColor, stroke);
285 		}
286 	}
287 
288 	private void drawBackground() throws IOException {
289 		worker.fillRect(0, 0, im.xgif, im.ygif, gdef.colors[COLOR_BACK]);
290 		if (gdef.backgroundImage != null) {
291 			worker.loadImage(gdef.backgroundImage);
292 		}
293 		worker.fillRect(im.xorigin, im.yorigin - im.ysize, im.xsize, im.ysize, gdef.colors[COLOR_CANVAS]);
294 	}
295 
296 	private void createImageWorker() {
297 		worker.resize(im.xgif, im.ygif);
298 	}
299 
300 	private void placeLegends() {
301 		if (!gdef.noLegend && !gdef.onlyGraph) {
302 			int border = (int) (getSmallFontCharWidth() * PADDING_LEGEND);
303 			LegendComposer lc = new LegendComposer(this, border, im.ygif, im.xgif - 2 * border);
304 			im.ygif = lc.placeComments() + PADDING_BOTTOM;
305 		}
306 	}
307 
308 	private void initializeLimits() throws RrdException {
309 		im.xsize = gdef.width;
310 		im.ysize = gdef.height;
311 		im.unitslength = gdef.unitsLength;
312 		if (gdef.onlyGraph) {
313 			if (im.ysize > 64) {
314 				throw new RrdException("Cannot create graph only, height too big");
315 			}
316 			im.xorigin = 0;
317 		}
318 		else {
319 			im.xorigin = (int) (PADDING_LEFT + im.unitslength * getSmallFontCharWidth());
320 		}
321 		if (gdef.verticalLabel != null) {
322 			im.xorigin += getSmallFontHeight();
323 		}
324 		if (gdef.onlyGraph) {
325 			im.yorigin = im.ysize;
326 		}
327 		else {
328 			im.yorigin = PADDING_TOP + im.ysize;
329 		}
330 		mapper = new Mapper(this);
331 		if (gdef.title != null) {
332 			im.yorigin += getLargeFontHeight() + PADDING_TITLE;
333 		}
334 		if (gdef.onlyGraph) {
335 			im.xgif = im.xsize;
336 			im.ygif = im.yorigin;
337 		}
338 		else {
339 			im.xgif = PADDING_RIGHT + im.xsize + im.xorigin;
340 			im.ygif = im.yorigin + (int) (PADDING_PLOT * getSmallFontHeight());
341 		}
342 	}
343 
344 	private void removeOutOfRangeRules() {
345 		for (PlotElement plotElement : gdef.plotElements) {
346 			if (plotElement instanceof HRule) {
347 				((HRule) plotElement).setLegendVisibility(im.minval, im.maxval, gdef.forceRulesLegend);
348 			}
349 			else if (plotElement instanceof VRule) {
350 				((VRule) plotElement).setLegendVisibility(im.start, im.end, gdef.forceRulesLegend);
351 			}
352 		}
353 	}
354 
355 	private void expandValueRange() {
356 		im.ygridstep = (gdef.valueAxisSetting != null) ? gdef.valueAxisSetting.gridStep : Double.NaN;
357 		im.ylabfact = (gdef.valueAxisSetting != null) ? gdef.valueAxisSetting.labelFactor : 0;
358 		if (!gdef.rigid && !gdef.logarithmic) {
359 			double sensiblevalues[] = {
360 					1000.0, 900.0, 800.0, 750.0, 700.0, 600.0, 500.0, 400.0, 300.0, 250.0, 200.0, 125.0, 100.0,
361 					90.0, 80.0, 75.0, 70.0, 60.0, 50.0, 40.0, 30.0, 25.0, 20.0, 10.0,
362 					9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.5, 3.0, 2.5, 2.0, 1.8, 1.5, 1.2, 1.0,
363 					0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0, -1
364 			};
365 			double scaled_min, scaled_max, adj;
366 			if (Double.isNaN(im.ygridstep)) {
367 				if (gdef.altYMrtg) { /* mrtg */
368 					im.decimals = Math.ceil(Math.log10(Math.max(Math.abs(im.maxval), Math.abs(im.minval))));
369 					im.quadrant = 0;
370 					if (im.minval < 0) {
371 						im.quadrant = 2;
372 						if (im.maxval <= 0) {
373 							im.quadrant = 4;
374 						}
375 					}
376 					switch (im.quadrant) {
377 						case 2:
378 							im.scaledstep = Math.ceil(50 * Math.pow(10, -(im.decimals)) * Math.max(Math.abs(im.maxval),
379 									Math.abs(im.minval))) * Math.pow(10, im.decimals - 2);
380 							scaled_min = -2 * im.scaledstep;
381 							scaled_max = 2 * im.scaledstep;
382 							break;
383 						case 4:
384 							im.scaledstep = Math.ceil(25 * Math.pow(10,
385 									-(im.decimals)) * Math.abs(im.minval)) * Math.pow(10, im.decimals - 2);
386 							scaled_min = -4 * im.scaledstep;
387 							scaled_max = 0;
388 							break;
389 						default: /* quadrant 0 */
390 							im.scaledstep = Math.ceil(25 * Math.pow(10, -(im.decimals)) * im.maxval) *
391 									Math.pow(10, im.decimals - 2);
392 							scaled_min = 0;
393 							scaled_max = 4 * im.scaledstep;
394 							break;
395 					}
396 					im.minval = scaled_min;
397 					im.maxval = scaled_max;
398 				}
399 				else if (gdef.altAutoscale) {
400 					/* measure the amplitude of the function. Make sure that
401 					   graph boundaries are slightly higher then max/min vals
402 					   so we can see amplitude on the graph */
403 					double delt, fact;
404 
405 					delt = im.maxval - im.minval;
406 					adj = delt * 0.1;
407 					fact = 2.0 * Math.pow(10.0,
408 							Math.floor(Math.log10(Math.max(Math.abs(im.minval), Math.abs(im.maxval)))) - 2);
409 					if (delt < fact) {
410 						adj = (fact - delt) * 0.55;
411 					}
412 					im.minval -= adj;
413 					im.maxval += adj;
414 				}
415 				else if (gdef.altAutoscaleMax) {
416 					/* measure the amplitude of the function. Make sure that
417 					   graph boundaries are slightly higher than max vals
418 					   so we can see amplitude on the graph */
419 					adj = (im.maxval - im.minval) * 0.1;
420 					im.maxval += adj;
421 				}
422 				else {
423 					scaled_min = im.minval / im.magfact;
424 					scaled_max = im.maxval / im.magfact;
425 					for (int i = 1; sensiblevalues[i] > 0; i++) {
426 						if (sensiblevalues[i - 1] >= scaled_min && sensiblevalues[i] <= scaled_min) {
427 							im.minval = sensiblevalues[i] * im.magfact;
428 						}
429 						if (-sensiblevalues[i - 1] <= scaled_min && -sensiblevalues[i] >= scaled_min) {
430 							im.minval = -sensiblevalues[i - 1] * im.magfact;
431 						}
432 						if (sensiblevalues[i - 1] >= scaled_max && sensiblevalues[i] <= scaled_max) {
433 							im.maxval = sensiblevalues[i - 1] * im.magfact;
434 						}
435 						if (-sensiblevalues[i - 1] <= scaled_max && -sensiblevalues[i] >= scaled_max) {
436 							im.maxval = -sensiblevalues[i] * im.magfact;
437 						}
438 					}
439 				}
440 			}
441 			else {
442 				im.minval = (double) im.ylabfact * im.ygridstep *
443 						Math.floor(im.minval / ((double) im.ylabfact * im.ygridstep));
444 				im.maxval = (double) im.ylabfact * im.ygridstep *
445 						Math.ceil(im.maxval / ((double) im.ylabfact * im.ygridstep));
446 			}
447 
448 		}
449 	}
450 
451 	private void identifySiUnit() {
452 		im.unitsexponent = gdef.unitsExponent;
453 		im.base = gdef.base;
454 		if (!gdef.logarithmic) {
455 			final char symbol[] = {'a', 'f', 'p', 'n', 'u', 'm', ' ', 'k', 'M', 'G', 'T', 'P', 'E'};
456 			int symbcenter = 6;
457 			double digits;
458 			if (im.unitsexponent != Integer.MAX_VALUE) {
459 				digits = Math.floor(im.unitsexponent / 3);
460 			}
461 			else {
462 				digits = Math.floor(Math.log(Math.max(Math.abs(im.minval), Math.abs(im.maxval))) / Math.log(im.base));
463 			}
464 			im.magfact = Math.pow(im.base, digits);
465 			if (((digits + symbcenter) < symbol.length) && ((digits + symbcenter) >= 0)) {
466 				im.symbol = symbol[(int) digits + symbcenter];
467 			}
468 			else {
469 				im.symbol = '?';
470 			}
471 		}
472 	}
473 
474 	private void findMinMaxValues() {
475 		double minval = Double.NaN, maxval = Double.NaN;
476 		for (PlotElement pe : gdef.plotElements) {
477 			if (pe instanceof SourcedPlotElement) {
478 				minval = Util.min(((SourcedPlotElement) pe).getMinValue(), minval);
479 				maxval = Util.max(((SourcedPlotElement) pe).getMaxValue(), maxval);
480 			}
481 		}
482 		if (Double.isNaN(minval)) {
483 			minval = 0D;
484 		}
485 		if (Double.isNaN(maxval)) {
486 			maxval = 1D;
487 		}
488 		im.minval = gdef.minValue;
489 		im.maxval = gdef.maxValue;
490 		/* adjust min and max values */
491 		if (Double.isNaN(im.minval) || ((!gdef.logarithmic && !gdef.rigid) && im.minval > minval)) {
492 			im.minval = minval;
493 		}
494 		if (Double.isNaN(im.maxval) || (!gdef.rigid && im.maxval < maxval)) {
495 			if (gdef.logarithmic) {
496 				im.maxval = maxval * 1.1;
497 			}
498 			else {
499 				im.maxval = maxval;
500 			}
501 		}
502 		/* make sure min is smaller than max */
503 		if (im.minval > im.maxval) {
504 			im.minval = 0.99 * im.maxval;
505 		}
506 		/* make sure min and max are not equal */
507 		if (im.minval == im.maxval) {
508 			im.maxval *= 1.01;
509 			if (!gdef.logarithmic) {
510 				im.minval *= 0.99;
511 			}
512 			/* make sure min and max are not both zero */
513 			if (im.maxval == 0.0) {
514 				im.maxval = 1.0;
515 			}
516 		}
517 	}
518 
519 	private void calculatePlotValues() throws RrdException {
520 		for (PlotElement pe : gdef.plotElements) {
521 			if (pe instanceof SourcedPlotElement) {
522 				((SourcedPlotElement) pe).assignValues(dproc);
523 			}
524 		}
525 	}
526 
527 	private void resolveTextElements() throws RrdException {
528 		ValueScaler valueScaler = new ValueScaler(gdef.base);
529 		for (CommentText comment : gdef.comments) {
530 			comment.resolveText(dproc, valueScaler);
531 		}
532 	}
533 
534 	private void fetchData() throws RrdException, IOException {
535 		dproc = new DataProcessor(gdef.startTime, gdef.endTime);
536 		dproc.setPoolUsed(gdef.poolUsed);
537 		if (gdef.step > 0) {
538 			dproc.setStep(gdef.step);
539 		}
540 		for (Source src : gdef.sources) {
541 			src.requestData(dproc);
542 		}
543 		dproc.processData();
544 		//long[] t = dproc.getTimestamps();
545 		//im.start = t[0];
546 		//im.end = t[t.length - 1];
547 		im.start = gdef.startTime;
548 		im.end = gdef.endTime;
549 	}
550 
551 	private boolean lazyCheck() {
552 		// redraw if lazy option is not set or file does not exist
553 		if (!gdef.lazy || !Util.fileExists(gdef.filename)) {
554 			return false; // 'false' means 'redraw'
555 		}
556 		// redraw if not enough time has passed
557 		long secPerPixel = (gdef.endTime - gdef.startTime) / gdef.width;
558 		long elapsed = Util.getTimestamp() - Util.getLastModified(gdef.filename);
559 		return elapsed <= secPerPixel;
560 	}
561 
562 	private void drawLegend() {
563 		if (!gdef.onlyGraph && !gdef.noLegend) {
564 			int ascent = (int) worker.getFontAscent(gdef.smallFont);
565 			int box = (int) getBox(), boxSpace = (int) (getBoxSpace());
566 			for (CommentText c : gdef.comments) {
567 				if (c.isValidGraphElement()) {
568 					int x = c.x, y = c.y + ascent;
569 					if (c instanceof LegendText) {
570 						// draw with BOX
571 						worker.fillRect(x, y - box, box, box, gdef.colors[COLOR_FRAME]);
572 						worker.fillRect(x + 1, y - box + 1, box - 2, box - 2, ((LegendText) c).legendColor);
573 						worker.drawString(c.resolvedText, x + boxSpace, y, gdef.smallFont, gdef.colors[COLOR_FONT]);
574 					}
575 					else {
576 						worker.drawString(c.resolvedText, x, y, gdef.smallFont, gdef.colors[COLOR_FONT]);
577 					}
578 				}
579 			}
580 		}
581 	}
582 
583 	// helper methods
584 
585 	double getSmallFontHeight() {
586 		return worker.getFontHeight(gdef.smallFont);
587 	}
588 
589 	private double getLargeFontHeight() {
590 		return worker.getFontHeight(gdef.largeFont);
591 	}
592 
593 	private double getSmallFontCharWidth() {
594 		return worker.getStringWidth("a", gdef.smallFont);
595 	}
596 
597 	double getInterlegendSpace() {
598 		return getSmallFontCharWidth() * LEGEND_INTERSPACING;
599 	}
600 
601 	double getLeading() {
602 		return getSmallFontHeight() * LEGEND_LEADING;
603 	}
604 
605 	double getSmallLeading() {
606 		return getSmallFontHeight() * LEGEND_LEADING_SMALL;
607 	}
608 
609 	double getBoxSpace() {
610 		return Math.ceil(getSmallFontHeight() * LEGEND_BOX_SPACE);
611 	}
612 
613 	private double getBox() {
614 		return getSmallFontHeight() * LEGEND_BOX;
615 	}
616 
617 	double[] xtr(long[] timestamps) {
618 		/*
619 		double[] timestampsDev = new double[timestamps.length];
620 		for (int i = 0; i < timestamps.length; i++) {
621 			timestampsDev[i] = mapper.xtr(timestamps[i]);
622 		}
623 		return timestampsDev;
624 		*/
625 		double[] timestampsDev = new double[2 * timestamps.length - 1];
626 		for (int i = 0, j = 0; i < timestamps.length; i += 1, j += 2) {
627 			timestampsDev[j] = mapper.xtr(timestamps[i]);
628 			if (i < timestamps.length - 1) {
629 				timestampsDev[j + 1] = timestampsDev[j];
630 			}
631 		}
632 		return timestampsDev;
633 	}
634 
635 	double[] ytr(double[] values) {
636 		/*
637 		double[] valuesDev = new double[values.length];
638 		for (int i = 0; i < values.length; i++) {
639 			if (Double.isNaN(values[i])) {
640 				valuesDev[i] = Double.NaN;
641 			}
642 			else {
643 				valuesDev[i] = mapper.ytr(values[i]);
644 			}
645 		}
646 		return valuesDev;
647 		*/
648 		double[] valuesDev = new double[2 * values.length - 1];
649 		for (int i = 0, j = 0; i < values.length; i += 1, j += 2) {
650 			if (Double.isNaN(values[i])) {
651 				valuesDev[j] = Double.NaN;
652 			}
653 			else {
654 				valuesDev[j] = mapper.ytr(values[i]);
655 			}
656 			if (j > 0) {
657 				valuesDev[j - 1] = valuesDev[j];
658 			}
659 		}
660 		return valuesDev;
661 	}
662 
663 	/**
664 	 * Renders this graph onto graphing device
665 	 *
666 	 * @param g Graphics handle
667 	 */
668 	public void render(Graphics g) {
669 		byte[] imageData = getRrdGraphInfo().getBytes();
670 		ImageIcon image = new ImageIcon(imageData);
671 		image.paintIcon(null, g, 0, 0);
672 	}
673 }