001: /***********************************************************************************************
002: * Copyright 2002 (C) Nathaniel G. Auvil. All Rights Reserved.
003: *
004: * Redistribution and use of this software and associated documentation ("Software"), with or
005: * without modification, are permitted provided that the following conditions are met:
006: *
007: * 1. Redistributions of source code must retain copyright statements and notices.
008: * Redistributions must also contain a copy of this document.
009: *
010: * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
011: * conditions and the following disclaimer in the documentation and/or other materials
012: * provided with the distribution.
013: *
014: * 3. The name "jCharts" or "Nathaniel G. Auvil" must not be used to endorse or promote
015: * products derived from this Software without prior written permission of Nathaniel G.
016: * Auvil. For written permission, please contact nathaniel_auvil@users.sourceforge.net
017: *
018: * 4. Products derived from this Software may not be called "jCharts" nor may "jCharts" appear
019: * in their names without prior written permission of Nathaniel G. Auvil. jCharts is a
020: * registered trademark of Nathaniel G. Auvil.
021: *
022: * 5. Due credit should be given to the jCharts Project (http://jcharts.sourceforge.net/).
023: *
024: * THIS SOFTWARE IS PROVIDED BY Nathaniel G. Auvil AND CONTRIBUTORS ``AS IS'' AND ANY
025: * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
026: * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
027: * jCharts OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
028: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
029: * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
030: * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,STRICT LIABILITY, OR TORT
031: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
032: * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
033: ************************************************************************************************/package org.krysalis.jcharts;
034:
035: import java.awt.Graphics2D;
036: import java.awt.Paint;
037: import java.awt.Shape;
038: import java.awt.geom.AffineTransform;
039: import java.awt.geom.Line2D;
040: import java.awt.geom.Rectangle2D;
041: import java.io.Serializable;
042: import java.util.ArrayList;
043: import java.util.Iterator;
044:
045: import org.krysalis.jcharts.Chart;
046: import org.krysalis.jcharts.chartData.interfaces.IAxisDataSeries;
047: import org.krysalis.jcharts.chartData.interfaces.IAxisPlotDataSet;
048: import org.krysalis.jcharts.chartData.interfaces.IData;
049: import org.krysalis.jcharts.chartData.interfaces.IPieChartDataSet;
050: import org.krysalis.jcharts.chartData.processors.TextProcessor;
051: import org.krysalis.jcharts.properties.LegendAreaProperties;
052: import org.krysalis.jcharts.properties.LegendProperties;
053: import org.krysalis.jcharts.properties.LineChartProperties;
054: import org.krysalis.jcharts.properties.PointChartProperties;
055: import org.krysalis.jcharts.test.HTMLGenerator;
056: import org.krysalis.jcharts.test.HTMLTestable;
057: import org.krysalis.jcharts.types.ChartType;
058:
059: /*************************************************************************************
060: *
061: * @author Nathaniel Auvil, Sandor Dornbush, Sundar Balasubramanian
062: * @version $Id: Legend.java,v 1.10 2004/05/31 16:26:13 nathaniel_auvil Exp $
063: ************************************************************************************/
064: final public class Legend implements HTMLTestable, Serializable {
065: private Chart chart;
066: private LegendProperties legendProperties;
067:
068: private float iconSide;
069:
070: //---derived values
071: private float widestLabelAndColumnPadding;
072: private int numColumns;
073: private int numRows;
074:
075: private TextProcessor textProcessor;
076:
077: private float x;
078: private float y;
079: private float width = 0;
080: private float height = 0;
081:
082: //---used to extract the legendLabels and paints from the data set and make them easy to loop through
083: private ArrayList labels;
084: private ArrayList paints;
085: private ArrayList shapes = new ArrayList();
086: private ArrayList fillPointsFlags = new ArrayList();
087: private ArrayList pointOutlinePaints = new ArrayList();
088: private ChartType chartType;
089: private PointChartProperties pointChartProperties;
090: private LineChartProperties lineChartProperties;
091:
092: /*********************************************************************************************
093: *
094: * @param chart
095: * @deprecated
096: **********************************************************************************************/
097: public Legend(Chart chart) {
098: this .chart = chart;
099: }
100:
101: /*********************************************************************************************
102: *
103: * @param chart
104: * @param legendProperties
105: **********************************************************************************************/
106: public Legend(Chart chart, LegendProperties legendProperties) {
107: this .chart = chart;
108: this .legendProperties = legendProperties;
109: }
110:
111: public void setX(float x) {
112: this .x = x;
113: }
114:
115: public void setY(float y) {
116: this .y = y;
117: }
118:
119: /*****************************************************************************************
120: *
121: * @param iAxisDataSeries
122: * @param chartTitleHeight
123: ****************************************************************************************/
124: public void computeLegendXY(IAxisDataSeries iAxisDataSeries,
125: float chartTitleHeight) {
126: //---PROCESS the size needed for drawing the legend.
127: this .calculateDrawingValues(iAxisDataSeries);
128:
129: if ((this .getLegendProperties().getPlacement() == LegendAreaProperties.RIGHT)
130: || (this .getLegendProperties().getPlacement() == LegendAreaProperties.LEFT)) {
131: if (this .getHeight() > this .chart.getImageHeight()
132: - this .chart.getChartProperties().getEdgePadding()
133: * 2) {
134: this .setY(this .chart.getChartProperties()
135: .getEdgePadding());
136: } else {
137: this .setY((this .chart.getImageHeight() / 2)
138: - (this .getHeight() / 2));
139: }
140:
141: if (this .getLegendProperties().getPlacement() == LegendAreaProperties.RIGHT) {
142: this .setX(this .chart.getImageWidth()
143: - this .getWidth()
144: - this .chart.getChartProperties()
145: .getEdgePadding());
146: } else //---else, LegendAreaProperties.LEFT
147: {
148: this .setX(this .chart.getChartProperties()
149: .getEdgePadding());
150: }
151: } else //---LegendAreaProperties.BOTTOM, OR LegendAreaProperties.TOP
152: {
153: if (this .getWidth()
154: + this .chart.getChartProperties().getEdgePadding()
155: * 2 > this .chart.getImageWidth()) {
156: this .setX(this .chart.getChartProperties()
157: .getEdgePadding());
158: } else {
159: this .setX((this .chart.getImageWidth() / 2)
160: - (this .getWidth() / 2));
161: }
162:
163: if (this .getLegendProperties().getPlacement() == LegendAreaProperties.BOTTOM) {
164: this .setY(this .chart.getImageHeight()
165: - this .getHeight()
166: - this .chart.getChartProperties()
167: .getEdgePadding());
168: } else //---else, LegendAreaProperties.TOP
169: {
170: this .setY(this .chart.getChartProperties()
171: .getEdgePadding()
172: + chartTitleHeight);
173: }
174: }
175: }
176:
177: /**********************************************************************************************
178: * Central method for processing data; try to minimize looping.
179: * 1) calculate the maximum height of labels
180: * 2) find the maximum label width
181: *
182: * @param iAxisDataSeries
183: **********************************************************************************************/
184: private void processData(IAxisDataSeries iAxisDataSeries) {
185: this .textProcessor = new TextProcessor();
186:
187: Iterator iterator = iAxisDataSeries
188: .getIAxisPlotDataSetIterator();
189:
190: //LOOP
191: while (iterator.hasNext()) {
192: this
193: .processLegendLabels((IAxisPlotDataSet) iterator
194: .next());
195: }
196: }
197:
198: /**********************************************************************************************
199: * Central method for processing data; try to minimize looping.
200: * 1) calculate the maximum height of labels
201: * 2) find the maximum label width
202: *
203: * @param iPieChartDataSet
204: **********************************************************************************************/
205: private void processData(IPieChartDataSet iPieChartDataSet) {
206: this .textProcessor = new TextProcessor();
207: this .processLegendLabels(iPieChartDataSet);
208: }
209:
210: /**********************************************************************************************
211: * Method for processing data for AxisPlot datasets; try to minimize
212: * looping.
213: * 1) calculate the maximum height of labels
214: * 2) find the maximum label width
215: *
216: * @param iAxisPlotDataSet
217: * *********************************************************************************************/
218: private void processLegendLabels(IAxisPlotDataSet iAxisPlotDataSet) {
219: for (int i = 0; i < iAxisPlotDataSet.getNumberOfLegendLabels(); i++) {
220: //---StockChartDataSets could have NULLs depending on the data
221: if (iAxisPlotDataSet.getLegendLabel(i) != null) {
222: this .textProcessor.addLabel(iAxisPlotDataSet
223: .getLegendLabel(i), this .legendProperties
224: .getChartFont().getFont(), this .chart
225: .getGraphics2D().getFontRenderContext());
226:
227: //---pair labels with paints to get around ugly piechart vs axischart data structure mess
228: this .labels.add(iAxisPlotDataSet.getLegendLabel(i));
229: this .paints.add(iAxisPlotDataSet.getPaint(i));
230:
231: if (iAxisPlotDataSet.getChartType().equals(
232: ChartType.POINT)) {
233: this .chartType = ChartType.POINT;
234: this .pointChartProperties = (PointChartProperties) iAxisPlotDataSet
235: .getChartTypeProperties();
236: this .shapes.add(pointChartProperties.getShape(i));
237: this .fillPointsFlags.add(new Boolean(
238: pointChartProperties.getFillPointsFlag(i)));
239: this .pointOutlinePaints.add(pointChartProperties
240: .getPointOutlinePaints(i));
241: }
242: if (iAxisPlotDataSet.getChartType().equals(
243: ChartType.LINE)) {
244: this .chartType = ChartType.LINE;
245: this .lineChartProperties = (LineChartProperties) iAxisPlotDataSet
246: .getChartTypeProperties();
247: if (lineChartProperties.getShapes() != null) {
248: this .shapes
249: .add(lineChartProperties.getShapes()[i]);
250: }
251: }
252: }
253: }
254: }
255:
256: /**********************************************************************************************
257: * Method for processing data for PieCharts; try to minimize looping.
258: * 1) calculate the maximum height of labels
259: * 2) find the maximum label width
260: * @param iPieChartDataSet
261: * ********************************************************************************************/
262: private void processLegendLabels(IPieChartDataSet iPieChartDataSet) {
263: for (int i = 0; i < iPieChartDataSet.getNumberOfLegendLabels(); i++) {
264: //---StockChartDataSets could have NULLs depending on the data
265: if (iPieChartDataSet.getLegendLabel(i) != null) {
266: this .textProcessor.addLabel(iPieChartDataSet
267: .getLegendLabel(i), this .legendProperties
268: .getChartFont().getFont(), this .chart
269: .getGraphics2D().getFontRenderContext());
270:
271: //---pair labels with paints to get around ugly piechart vs axischart data structure mess
272: this .labels.add(iPieChartDataSet.getLegendLabel(i));
273: this .paints.add(iPieChartDataSet.getPaint(i));
274: }
275: }
276: }
277:
278: /************************************************************************************************
279: *
280: *************************************************************************************************/
281: public LegendProperties getLegendProperties() {
282: return this .legendProperties;
283: }
284:
285: /************************************************************************************************
286: * Calculates the width and height needed to display the Legend. Use the getWidth() and
287: * getHeight() methods to extract this information.
288: *
289: * @param iData can pass either the IPieChartDataSet or the IChartDataSeries to this.
290: ************************************************************************************************/
291: public void calculateDrawingValues(IData iData) {
292: int numberOfLabels;
293: this .labels = new ArrayList();
294: this .paints = new ArrayList();
295:
296: if (iData instanceof IAxisDataSeries) {
297: IAxisDataSeries iAxisDataSeries = (IAxisDataSeries) iData;
298: this .processData(iAxisDataSeries);
299: numberOfLabels = iAxisDataSeries.getTotalNumberOfDataSets();
300: } else {
301: IPieChartDataSet iPieChartDataSet = (IPieChartDataSet) iData;
302: this .processData(iPieChartDataSet);
303: numberOfLabels = iPieChartDataSet.getNumberOfLegendLabels();
304: }
305:
306: //---make the icon proportional to the Font being used.
307: this .iconSide = (float) .50
308: * this .textProcessor.getTallestLabel();
309: //---for POINT and LINE charts, set iconSide to max width of legend shapes
310: if ((chartType == ChartType.POINT)
311: || (chartType == ChartType.LINE)) {
312: //for( int i = 0; i < numberOfLabels; i++ )
313: for (int i = 0; i < this .shapes.size(); i++) {
314: //---get the bounds of the shape
315: try {
316: Double shapeWidthDouble = new Double(
317: (((Shape) this .shapes.get(i)).getBounds2D()
318: .getWidth()));
319: float shapeWidth = shapeWidthDouble.floatValue();
320: this .iconSide = Math.max(this .iconSide, shapeWidth);
321: } catch (NullPointerException npe) {
322: // Looks like in 0.74 it was quite acceptable to make shape = null
323: // we should probably catch all these and render a "null" shape to the legend
324: System.err.println("Warning: legend shape is null");
325: npe.printStackTrace();
326: }
327: }
328: }
329:
330: this .determineWidthAndHeight(numberOfLabels);
331: }
332:
333: /********************************************************************************************
334: *
335: ********************************************************************************************/
336: public float getWidth() {
337: return this .width;
338: }
339:
340: /********************************************************************************************
341: *
342: ********************************************************************************************/
343: public int getHeight() {
344: //why not return a float here?
345: return ((int) Math.ceil(this .height));
346: }
347:
348: /**********************************************************************************************
349: * Determines the dimensions needed for the Legend and creates the image for it.
350: *
351: **********************************************************************************************/
352: private void determineWidthAndHeight(int numberOfLabels) {
353: //---start with the padding no matter how many columns or specified width
354: width = this .legendProperties.getEdgePadding() * 2;
355: height = width;
356:
357: //---if don't care how many columns or the number of labels is less than num columns specified, all in one row.
358: if (this .legendProperties.getNumColumns() == LegendAreaProperties.COLUMNS_AS_MANY_AS_NEEDED
359: || this .legendProperties.getNumColumns() >= numberOfLabels) {
360: this .numColumns = numberOfLabels;
361: width += this .textProcessor.getTotalLabelWidths();
362:
363: this .numRows = 1;
364: }
365: //---else, more than one row
366: else {
367: //---one less addition to do when looping.
368: this .widestLabelAndColumnPadding = this .textProcessor
369: .getWidestLabel()
370: + this .legendProperties.getColumnPadding();
371:
372: if (legendProperties.getNumColumns() == LegendAreaProperties.COLUMNS_FIT_TO_IMAGE) {
373: // calculate that the columns match exactly
374: float actualWidth = legendProperties.getSize().width;
375:
376: float widestLabelColumnAndIcon = widestLabelAndColumnPadding
377: + iconSide
378: + legendProperties.getIconPadding()
379: + legendProperties.getColumnPadding();
380:
381: numColumns = (int) (actualWidth / widestLabelColumnAndIcon);
382: numColumns = Math.min(numColumns, numberOfLabels);
383: } else {
384: numColumns = this .legendProperties.getNumColumns();
385: }
386:
387: width += this .textProcessor.getWidestLabel()
388: * this .numColumns;
389:
390: this .numRows = (int) Math.ceil((double) numberOfLabels
391: / (double) this .numColumns);
392: }
393:
394: //---account for icons
395: width += (this .iconSide + this .legendProperties
396: .getIconPadding())
397: * this .numColumns;
398:
399: //---account for space between each column
400: width += this .legendProperties.getColumnPadding()
401: * (this .numColumns - 1);
402:
403: //---account for lineStrokes for LINE charts
404: if (chartType == ChartType.LINE) {
405: width += this .legendProperties.getIconLineStrokeLength()
406: * 2 * this .numColumns;
407: }
408:
409: //---account for each row
410: height += (this .textProcessor.getTallestLabel() * this .numRows);
411:
412: //---account for each row padding
413: height += (this .legendProperties.getRowPadding() * (this .numRows - 1));
414: }
415:
416: /**********************************************************************************************
417: * Renders the legend.
418: *
419: **********************************************************************************************/
420: public void render() {
421: Graphics2D g2d = this .chart.getGraphics2D();
422:
423: //---get the bounds of the image
424: Rectangle2D.Float rectangle = new Rectangle2D.Float(this .x,
425: this .y, width - 1, this .height - 1);
426:
427: //---fill the background of the Legend with the specified Paint
428: if (this .legendProperties.getBackgroundPaint() != null) {
429: g2d.setPaint(this .legendProperties.getBackgroundPaint());
430: g2d.fill(rectangle);
431: }
432:
433: //---draw Legend border
434: if (this .legendProperties.getBorderStroke() != null) {
435: this .legendProperties.getBorderStroke()
436: .draw(g2d, rectangle);
437: }
438:
439: //---dont think we want this so text will be clean but leave commented out.
440: //g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF );
441:
442: //---set the font and text color.
443: g2d.setFont(this .legendProperties.getChartFont().getFont());
444:
445: //---icon coordinates
446: rectangle.y += this .legendProperties.getEdgePadding()
447: + (this .textProcessor.getTallestLabel() / 2)
448: - (this .iconSide / 2);
449: rectangle.width = this .iconSide;
450: rectangle.height = this .iconSide;
451:
452: float posX = this .x + this .legendProperties.getEdgePadding();
453: float fontY = rectangle.y + rectangle.height;
454:
455: //---pre calculate utility values
456: float yIncrement = this .textProcessor.getTallestLabel()
457: + this .legendProperties.getRowPadding();
458: float iconAndPaddingWidth = this .iconSide
459: + this .legendProperties.getIconPadding();
460:
461: int labelIndex = 0;
462:
463: //LOOP
464: for (int j = 0; j < this .numRows; j++) {
465: //LOOP
466: for (int i = 0; i < this .numColumns; i++) {
467: rectangle.x = posX;
468:
469: //---display icon
470: g2d.setPaint((Paint) this .paints.get(labelIndex));
471:
472: // only Point and Line Charts will have shapes drawn
473:
474: if (this .shapes.size() > 0
475: && this .shapes.size() > labelIndex) {
476: //Shape shape = (Shape)this.shapes.get( labelIndex);
477:
478: //---get the original transform so can reset it
479: AffineTransform affineTransform = g2d
480: .getTransform();
481:
482: //---translate the Shape into position
483: g2d.translate(rectangle.x, rectangle.y);
484:
485: if (this .fillPointsFlags.size() > 0) {
486: if (((Boolean) this .fillPointsFlags
487: .get(labelIndex)).booleanValue()) {
488: g2d.fill((Shape) this .shapes
489: .get(labelIndex));
490:
491: //---if we are filling the points, see if we should outline the Shape
492: //---applicable only to POINt charts
493: if (this .pointOutlinePaints.get(labelIndex) != null) {
494: g2d
495: .setPaint((Paint) this .pointOutlinePaints
496: .get(labelIndex));
497: g2d.draw((Shape) this .shapes
498: .get(labelIndex));
499: }
500: }
501: } else {
502: // for Point Charts, only draw shape
503: if (chartType == ChartType.POINT) {
504: g2d.draw((Shape) this .shapes
505: .get(labelIndex));
506: } else
507: // chartType == ChartType.LINE
508: // for Line Charts, fill the shape
509: {
510: //---get the bounds of the shape
511: Rectangle2D shapeBounds = ((Shape) this .shapes
512: .get(labelIndex)).getBounds2D();
513: double XOffset = shapeBounds.getWidth() / 2;
514: double YOffset = shapeBounds.getHeight() / 2;
515:
516: g2d.setStroke(this .lineChartProperties
517: .getLineStrokes()[labelIndex]);
518:
519: Line2D.Double line = new Line2D.Double(0,
520: YOffset, this .legendProperties
521: .getIconLineStrokeLength(),
522: YOffset);
523: g2d.draw(line);
524: // move posX to account for the lineStroke before the shape. for example, ---o
525: posX += this .legendProperties
526: .getIconLineStrokeLength();
527: //---translate the Shape to adjust for the IconLineStrokeLength
528: g2d.translate(this .legendProperties
529: .getIconLineStrokeLength()
530: - XOffset, 0);
531:
532: line.x1 = XOffset;
533: g2d.draw(line);
534:
535: g2d.fill((Shape) this .shapes
536: .get(labelIndex));
537:
538: //---border around icon
539: if (this .legendProperties
540: .getIconBorderStroke() != null
541: && this .pointOutlinePaints.size() != 0) {
542: if (this .pointOutlinePaints != null) {
543: g2d.setStroke(this .legendProperties
544: .getIconBorderStroke());
545: g2d
546: .setPaint((Paint) this .pointOutlinePaints
547: .get(labelIndex));
548: g2d.draw((Shape) this .shapes
549: .get(labelIndex));
550: }
551: }
552:
553: // move posX to account for the lineStroke after the shape. for example, o---
554: posX += this .legendProperties
555: .getIconLineStrokeLength();
556: }
557: }
558: //---reset original transform
559: g2d.setTransform(affineTransform);
560: }
561: // for other charts, just draw a rectangle
562: else {
563: g2d.fill(rectangle);
564:
565: //---border around icon
566: if (this .legendProperties.getIconBorderStroke() != null) {
567: g2d.setStroke(this .legendProperties
568: .getIconBorderStroke());
569: g2d.setPaint(this .legendProperties
570: .getIconBorderPaint());
571: g2d.draw(rectangle);
572: }
573: }
574:
575: //---draw the label
576: g2d.setPaint(this .legendProperties.getChartFont()
577: .getPaint());
578: posX += iconAndPaddingWidth;
579: g2d.drawString((String) this .labels.get(labelIndex),
580: posX, fontY);
581:
582: if (this .legendProperties.getNumColumns() == LegendAreaProperties.COLUMNS_AS_MANY_AS_NEEDED
583: || this .legendProperties.getNumColumns() >= this .labels
584: .size()) {
585: //---each column is as wide as it needs to be
586: posX += this .textProcessor.getTextTag(labelIndex)
587: .getWidth()
588: + this .legendProperties.getColumnPadding();
589: } else {
590: //---all columns have the same width
591: posX += this .widestLabelAndColumnPadding;
592: }
593:
594: labelIndex++;
595:
596: //---if no more labels, we are done.
597: if (labelIndex == this .labels.size())
598: break;
599: }
600:
601: posX = this .x + this .legendProperties.getEdgePadding();
602: fontY += yIncrement;
603: rectangle.y += yIncrement;
604: }
605: }
606:
607: /*********************************************************************************************
608: * Enables the testing routines to display the contents of this Object.
609: *
610: * @param htmlGenerator
611: **********************************************************************************************/
612: public void toHTML(HTMLGenerator htmlGenerator) {
613: htmlGenerator.legendTableStart();
614:
615: htmlGenerator.addTableRow("Width", Float.toString(this .width));
616: htmlGenerator
617: .addTableRow("Height", Float.toString(this .height));
618: htmlGenerator.addTableRow("Icon Side", Float
619: .toString(this.iconSide));
620:
621: htmlGenerator.innerTableRowStart();
622: this.legendProperties.toHTML(htmlGenerator);
623: htmlGenerator.innerTableRowEnd();
624:
625: htmlGenerator.legendTableEnd();
626: }
627:
628: }
|