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.nonAxisChart;
034:
035: import org.krysalis.jcharts.Chart;
036: import org.krysalis.jcharts.chartData.interfaces.IPieChartDataSet;
037: import org.krysalis.jcharts.chartData.processors.PieChartDataProcessor;
038: import org.krysalis.jcharts.imageMap.CircleMapArea;
039: import org.krysalis.jcharts.imageMap.ImageMap;
040: import org.krysalis.jcharts.imageMap.PolyMapArea;
041: import org.krysalis.jcharts.properties.ChartProperties;
042: import org.krysalis.jcharts.properties.LegendAreaProperties;
043: import org.krysalis.jcharts.properties.LegendProperties;
044: import org.krysalis.jcharts.properties.PieChart2DProperties;
045: import org.krysalis.jcharts.test.HTMLChartTestable;
046: import org.krysalis.jcharts.test.HTMLGenerator;
047: import org.krysalis.jcharts.types.PieLabelType;
048:
049: import java.awt.*;
050: import java.awt.font.FontRenderContext;
051: import java.awt.geom.Arc2D;
052: import java.awt.geom.Line2D;
053:
054: /*************************************************************************************
055: *
056: * @author Nathaniel Auvil
057: * @version $Id: PieChart2D.java,v 1.7 2003/05/31 18:17:32 nathaniel_auvil Exp $
058: ************************************************************************************/
059: public class PieChart2D extends Chart implements HTMLChartTestable {
060: private float pieX;
061: private float pieY;
062: private float diameter;
063:
064: private IPieChartDataSet iPieChartDataSet;
065: private PieChartDataProcessor pieChartDataProcessor;
066:
067: private PieLabels pieLabels;
068:
069: /************************************************************************************************
070: * Constructor
071: *
072: * @param iPieChartDataSet
073: * @param legendProperties
074: * @param chartProperties general chart properties
075: * @param pixelWidth
076: * @param pixelHeight
077: ************************************************************************************************/
078: public PieChart2D(IPieChartDataSet iPieChartDataSet,
079: LegendProperties legendProperties,
080: ChartProperties chartProperties, int pixelWidth,
081: int pixelHeight) {
082: super (legendProperties, chartProperties, pixelWidth,
083: pixelHeight);
084: this .iPieChartDataSet = iPieChartDataSet;
085: }
086:
087: /************************************************************************************************
088: * Draws the chart
089: *
090: ************************************************************************************************/
091: protected void renderChart() {
092: PieChart2DProperties properties = (PieChart2DProperties) this .iPieChartDataSet
093: .getChartTypeProperties();
094: FontRenderContext fontRenderContext = super .getGraphics2D()
095: .getFontRenderContext();
096:
097: this .pieChartDataProcessor = new PieChartDataProcessor(
098: this .iPieChartDataSet);
099: this .pieChartDataProcessor.processData();
100:
101: //---cache calcs used more than once
102: float edgePaddingTimesTwo = super .getChartProperties()
103: .getEdgePadding() * 2;
104: float halfImageWidth = super .getImageWidth() / 2;
105: float halfImageHeight = super .getImageHeight() / 2;
106:
107: //---render the TITLE. If no title, this will return zero.
108: float chartTitleHeightPlusPadding = super .renderChartTitle(
109: this .iPieChartDataSet.getChartTitle(),
110: fontRenderContext);
111:
112: //---figure out what size is needed to hold all the components of the pie chart, then size the pie accordingly
113: float widthAvailable = super .getImageWidth()
114: - edgePaddingTimesTwo;
115: float heightAvailable = super .getImageHeight()
116: - edgePaddingTimesTwo;
117: heightAvailable -= chartTitleHeightPlusPadding;
118:
119: //---take labels sizing into consideration if needed.
120: if (!properties.getPieLabelType()
121: .equals(PieLabelType.NO_LABELS)) {
122: this .pieLabels = new PieLabels(properties,
123: this .iPieChartDataSet, fontRenderContext);
124:
125: //---if there is only one item in pie, label will be below plot so width is not a concern
126: if (iPieChartDataSet.getNumberOfDataItems() != 1) {
127: widthAvailable -= this .pieLabels
128: .getWidestLabelTimesTwo();
129: widthAvailable -= (properties.getTickLength() * 2);
130: }
131:
132: heightAvailable -= this .pieLabels.getTallestLabelTimesTwo();
133: heightAvailable -= (properties.getTickLength() * 2);
134: }
135:
136: //---if there is a legend...
137: if (this .getLegend() != null) {
138: float legendX = 0f;
139: float legendY = 0f;
140:
141: //---calculate all the legend rendering coordinates and positions.
142: this .getLegend().calculateDrawingValues(iPieChartDataSet);
143:
144: //---adjust width and height based on the Legend size.
145: if ((this .getLegend().getLegendProperties().getPlacement() == LegendAreaProperties.RIGHT)
146: || (this .getLegend().getLegendProperties()
147: .getPlacement() == LegendAreaProperties.LEFT)) {
148: widthAvailable -= this .getLegend().getWidth();
149: widthAvailable -= this .getLegend()
150: .getLegendProperties().getChartPadding();
151:
152: //---diameter of pie will be at least one pixel, even if the legend takes up the whole image.
153: //---this will keep the renderer from blowing up.
154: this .diameter = Math.max(widthAvailable, 1.0f);
155:
156: //---make sure we do not make the pie diameter taller than the image
157: this .diameter = Math
158: .min(this .diameter, heightAvailable);
159:
160: //---calculate the entire width of everything to be drawn so can center everything
161: float plotWidth = this .diameter;
162: plotWidth += this .getLegend().getWidth();
163: plotWidth += this .getLegend().getLegendProperties()
164: .getChartPadding();
165: if (this .pieLabels != null) {
166: plotWidth += (this .pieLabels.getWidestLabel() * 2);
167: plotWidth += (properties.getTickLength() * 2);
168: }
169:
170: if (this .getLegend().getLegendProperties()
171: .getPlacement() == LegendAreaProperties.RIGHT) {
172: //---pie's diameter may not fill image width as may be image height constrained.
173: this .pieX = halfImageWidth - (plotWidth / 2);
174:
175: if (this .pieLabels != null) {
176: this .pieX += this .pieLabels.getWidestLabel();
177: this .pieX += properties.getTickLength();
178: legendX += this .pieLabels.getWidestLabel();
179: legendX += properties.getTickLength();
180: }
181:
182: //---position legend based on the pie position
183: legendX += this .pieX + this .diameter;
184: legendX += this .getLegend().getLegendProperties()
185: .getChartPadding();
186: } else {
187: legendX = halfImageWidth - (plotWidth / 2);
188:
189: if (this .pieLabels != null) {
190: this .pieX = legendX;
191: this .pieX += this .getLegend().getWidth();
192: this .pieX += this .getLegend()
193: .getLegendProperties()
194: .getChartPadding();
195: this .pieX += this .pieLabels.getWidestLabel();
196: this .pieX += properties.getTickLength();
197: }
198: }
199:
200: //---center the legend vertically
201: legendY = halfImageHeight
202: - (this .getLegend().getHeight() / 2);
203:
204: //---center the pie vertically
205: this .pieY = halfImageHeight - (this .diameter / 2);
206: }
207: //---else the legend is either under or on top of the pie
208: else {
209: heightAvailable -= this .getLegend().getHeight();
210: heightAvailable -= this .getLegend()
211: .getLegendProperties().getChartPadding();
212:
213: //---diameter of pie will be at least one pixel, even if the legend takes up the whole image.
214: //---this will keep the renderer from blowing up.
215: this .diameter = Math.max(heightAvailable, 1.0f);
216:
217: //---make sure we do not make the pie diameter wider than the image
218: this .diameter = Math.min(this .diameter, widthAvailable);
219:
220: if (this .getLegend().getLegendProperties()
221: .getPlacement() == LegendAreaProperties.BOTTOM) {
222: this .pieY = super .getChartProperties()
223: .getEdgePadding();
224: this .pieY += chartTitleHeightPlusPadding;
225:
226: legendY += this .diameter;
227:
228: if (this .pieLabels != null) {
229: //---adds label height from top of pie
230: this .pieY += this .pieLabels.getTallestLabel();
231: this .pieY += properties.getTickLength();
232:
233: //---add label hight from bottom of pie
234: legendY += this .pieLabels.getTallestLabel();
235: legendY += properties.getTickLength();
236: }
237:
238: legendY += this .pieY;
239: legendY += this .getLegend().getLegendProperties()
240: .getChartPadding();
241: } else {
242: legendY = super .getChartProperties()
243: .getEdgePadding();
244: legendY += chartTitleHeightPlusPadding;
245:
246: this .pieY = legendY;
247: this .pieY += this .getLegend().getHeight();
248: this .pieY += this .getLegend().getLegendProperties()
249: .getChartPadding();
250:
251: if (this .pieLabels != null) {
252: //---adds label height from top of pie
253: this .pieY += this .pieLabels.getTallestLabel();
254: this .pieY += properties.getTickLength();
255: }
256: }
257:
258: //---center the legend horizontally
259: legendX = halfImageWidth
260: - (this .getLegend().getWidth() / 2);
261:
262: //---center the pie horizontally
263: this .pieX = halfImageWidth - (this .diameter / 2);
264: }
265:
266: super .getLegend().setX(legendX);
267: super .getLegend().setY(legendY);
268: super .getLegend().render();
269:
270: }
271: //---else, the Legend is NULL
272: else {
273: //---if there is no legend, fill the image with the pie
274: this .diameter = Math.min(heightAvailable, widthAvailable);
275:
276: float halfDiameter = this .diameter / 2;
277:
278: //---center the pie horizontally
279: this .pieX = halfImageWidth - halfDiameter;
280:
281: //---center the pie vertically
282: this .pieY = halfImageHeight - halfDiameter;
283: }
284:
285: //---IMAGE MAP setup
286: //---if we are saving all the coordinates for an ImageMap, create the ImageMap Object as we
287: //--- know how many area elements there are.
288: if (super .getGenerateImageMapFlag()) {
289: ImageMap imageMap = new ImageMap(iPieChartDataSet
290: .getNumberOfDataItems());
291: super .setImageMap(imageMap);
292: }
293:
294: PieChart2D.render(this );
295: }
296:
297: /************************************************************************************************
298: * Implement the method to render the Chart.
299: *
300: * @param pieChart2D
301: ************************************************************************************************/
302: static void render(PieChart2D pieChart2D) {
303: Graphics2D g2d = pieChart2D.getGraphics2D();
304: PieChart2DProperties properties = (PieChart2DProperties) pieChart2D.iPieChartDataSet
305: .getChartTypeProperties();
306:
307: //---set the border Stroke
308: properties.getBorderChartStroke().setupGraphics2D(g2d);
309:
310: //---the following only for Image Map-----------------------------
311: //---IMAGE MAP
312: //---number of subdivisions to break each slice into to 'fill' slice
313: int subdivisions = 3;
314: float halfDiameter = 0;
315: float xPieMiddle = 0;
316: float yPieMiddle = 0;
317: float imageMapPoints[][] = null;
318: if (pieChart2D.getImageMap() != null) {
319: halfDiameter = (float) (pieChart2D.diameter / 2.0);
320: xPieMiddle = halfDiameter + pieChart2D.pieX;
321: yPieMiddle = halfDiameter + pieChart2D.pieY;
322: imageMapPoints = new float[pieChart2D.iPieChartDataSet
323: .getNumberOfDataItems()
324: * (subdivisions + 1)][2];
325: }
326:
327: //---get the starting degree
328: float currentDegrees = properties.getZeroDegreeOffset();
329:
330: double percentageOfPie = 0;
331:
332: //---if only one item in chart, just draw border around outside.
333: //---if do a draw of the arc, will get a line in the pie as arc has a start and end.
334: if (pieChart2D.iPieChartDataSet.getNumberOfDataItems() == 1) {
335: Arc2D.Double arc = new Arc2D.Double(pieChart2D.pieX,
336: pieChart2D.pieY, pieChart2D.diameter,
337: pieChart2D.diameter, currentDegrees, 360,
338: Arc2D.OPEN);
339:
340: g2d.setPaint(pieChart2D.iPieChartDataSet.getPaint(0));
341: g2d.fill(arc);
342:
343: properties.getBorderChartStroke().draw(g2d, arc);
344:
345: //---if only a single value use a circle map
346: //---IMAGE MAP
347: if (pieChart2D.getImageMap() != null) {
348: CircleMapArea circleMapArea = new CircleMapArea(
349: xPieMiddle, yPieMiddle,
350: pieChart2D.iPieChartDataSet.getValue(0), null,
351: pieChart2D.iPieChartDataSet.getLegendLabel(0));
352: circleMapArea.setRadius((int) pieChart2D.diameter);
353: pieChart2D.getImageMap().addImageMapArea(circleMapArea);
354: }
355:
356: // System.out.println( pieChart2D.pieLabels.getTextTag( 0 ).getText() );
357:
358: if (pieChart2D.pieLabels != null) {
359: float x = pieChart2D.pieX
360: + (pieChart2D.diameter / 2)
361: - (pieChart2D.pieLabels.getTextTag(0)
362: .getWidth() / 2);
363: float y = pieChart2D.pieY - properties.getTickLength();
364: // System.out.println( "x=" + x );
365: // System.out.println( "y=" + y );
366:
367: properties.getValueLabelFont().setupGraphics2D(g2d);
368: g2d.drawString(pieChart2D.pieLabels.getTextTag(0)
369: .getText(), x, y);
370: }
371: } else {
372: Arc2D.Double arc = new Arc2D.Double(pieChart2D.pieX,
373: pieChart2D.pieY, pieChart2D.diameter,
374: pieChart2D.diameter, currentDegrees, 360, Arc2D.PIE);
375: //---IMAGE MAP
376: int mapCounter = 0;
377:
378: for (int i = 0; i < pieChart2D.iPieChartDataSet
379: .getNumberOfDataItems(); i++) {
380: percentageOfPie = pieChart2D.pieChartDataProcessor
381: .getPercentageOfPie(i);
382:
383: arc.setAngleStart(currentDegrees);
384: arc.setAngleExtent(percentageOfPie);
385:
386: //---set the color, and fill the pie piece.
387: g2d.setPaint(pieChart2D.iPieChartDataSet.getPaint(i));
388: g2d.fill(arc);
389:
390: properties.getBorderChartStroke().draw(g2d, arc);
391:
392: //---if we are going to display labels
393: if (pieChart2D.pieLabels != null) {
394: //---get the angle the center of slice
395: double sliceCenterDegrees = (currentDegrees)
396: + percentageOfPie / 2;
397:
398: if (sliceCenterDegrees > 360) {
399: sliceCenterDegrees -= 360;
400: }
401:
402: double sliceCenterRadians = Math
403: .toRadians(sliceCenterDegrees);
404:
405: //---compute the cos and sin of the label angle.
406: double cosOfLabel = Math.cos(sliceCenterRadians);
407: double sinOfLabel = Math.sin(sliceCenterRadians);
408:
409: halfDiameter = (float) (pieChart2D.diameter / 2.0);
410:
411: //---end point of the label border line.
412: float borderXstart = (float) (cosOfLabel * halfDiameter);
413: float borderYstart = (float) -(sinOfLabel * halfDiameter);
414:
415: //---end point of the label border line.
416: float borderXend = (float) (cosOfLabel * (halfDiameter + properties
417: .getTickLength()));
418: float borderYend = (float) -(sinOfLabel * (halfDiameter + properties
419: .getTickLength()));
420:
421: xPieMiddle = halfDiameter + pieChart2D.pieX;
422: yPieMiddle = halfDiameter + pieChart2D.pieY;
423:
424: properties.getValueLabelFont().setupGraphics2D(g2d);
425:
426: g2d.draw(new Line2D.Double(xPieMiddle
427: + borderXstart, yPieMiddle + borderYstart,
428: xPieMiddle + borderXend, yPieMiddle
429: + borderYend));
430:
431: //System.out.println( pieChart2D.textTagGroup.getTextTag( i ).getText() + " sliceCenterDegrees= " + sliceCenterDegrees );
432:
433: float labelY = yPieMiddle + borderYend;
434: if (sliceCenterDegrees > 60
435: && sliceCenterDegrees < 120) {
436: labelY -= pieChart2D.pieLabels.getTextTag(i)
437: .getFontDescent();
438: } else if (sliceCenterDegrees > 240
439: && sliceCenterDegrees < 300) {
440: labelY += pieChart2D.pieLabels.getTextTag(i)
441: .getFontAscent();
442: }
443:
444: if (sliceCenterDegrees > 90
445: && sliceCenterDegrees < 270) {
446: g2d.drawString(pieChart2D.pieLabels.getTextTag(
447: i).getText(), xPieMiddle
448: + borderXend
449: - pieChart2D.pieLabels.getTextTag(i)
450: .getWidth()
451: - properties.getTickLength(), labelY);
452: } else {
453: g2d.drawString(pieChart2D.pieLabels.getTextTag(
454: i).getText(), xPieMiddle + borderXend
455: + properties.getTickLength(), labelY);
456: }
457: }
458:
459: //---if we are generating an image map...
460: //---IMAGE MAP
461: if (pieChart2D.getImageMap() != null) {
462: //---increment a separate amount to minimize rounding errors.
463: double workDegrees = currentDegrees;
464:
465: //---compute the cos and sin of the bodrder angle.
466: double cosOfBorder;
467: double sinOfBorder;
468: double splitDegree = percentageOfPie / subdivisions;
469:
470: for (int j = 0; j <= subdivisions; j++) {
471: cosOfBorder = Math.cos(Math
472: .toRadians(workDegrees));
473: sinOfBorder = Math.sin(Math
474: .toRadians(workDegrees));
475:
476: //---end point of the slice border line.
477: imageMapPoints[mapCounter][0] = xPieMiddle
478: + (float) (cosOfBorder * halfDiameter);
479: imageMapPoints[mapCounter][1] = yPieMiddle
480: + (float) -(sinOfBorder * halfDiameter);
481:
482: //DEBUG to make sure calculating points correctly
483: //g2d.setPaint( Color.red );
484: //g2d.fillRect( (int) imageMapPoints[ mapCounter ][ 0 ], (int) imageMapPoints[ mapCounter ][ 1 ], 6, 6 );
485:
486: mapCounter++;
487: workDegrees += splitDegree;
488: }
489: }
490:
491: currentDegrees += percentageOfPie;
492: }
493:
494: //---if we are generating an image map...
495: //---IMAGE MAP
496: if (pieChart2D.getImageMap() != null) {
497: //---each slice has 3 + subdivision slices...
498: //int counter= pieChart2D.iPieChartDataSet.getNumberOfDataItems() * ( 3 + subdivisions );
499: int counter = 0;
500:
501: //---for each data item
502: for (int i = 0; i < pieChart2D.iPieChartDataSet
503: .getNumberOfDataItems(); i++) {
504: int coordinateCounter = 0;
505:
506: //---there are three points plus some number of subdivisions...
507: PolyMapArea polyMapArea = new PolyMapArea(
508: 3 + subdivisions,
509: pieChart2D.iPieChartDataSet.getValue(i),
510: null, pieChart2D.iPieChartDataSet
511: .getLegendLabel(i));
512: polyMapArea.addCoordinate(coordinateCounter++,
513: xPieMiddle, yPieMiddle);
514:
515: //---include the first border point, plus the subdivisions
516: for (int h = 0; h <= subdivisions; h++) {
517: polyMapArea.addCoordinate(coordinateCounter++,
518: imageMapPoints[counter][0],
519: imageMapPoints[counter][1]);
520: counter++;
521: }
522:
523: //---if this is the last slice, add the first calculated map point
524: if ((i + 1) == pieChart2D.iPieChartDataSet
525: .getNumberOfDataItems()) {
526: polyMapArea.addCoordinate(coordinateCounter,
527: imageMapPoints[0][0],
528: imageMapPoints[0][1]);
529: }
530: //---else add the next calculated point
531: else {
532: polyMapArea.addCoordinate(coordinateCounter,
533: imageMapPoints[counter][0],
534: imageMapPoints[counter][1]);
535: }
536:
537: pieChart2D.getImageMap().addImageMapArea(
538: polyMapArea);
539: }
540: }
541: }
542: }
543:
544: /**********************************************************************************************
545: * Enables the testing routines to display the contents of this Object. Override Chart
546: * implementation as PieCharts use AreaProperties directly rather than a child.
547: *
548: * @param htmlGenerator
549: * @param imageFileName
550: **********************************************************************************************/
551: public void toHTML(HTMLGenerator htmlGenerator, String imageFileName) {
552: if (this.getLegend() != null) {
553: htmlGenerator.chartTableRowStart();
554: this.getLegend().toHTML(htmlGenerator);
555: htmlGenerator.chartTableRowEnd();
556: }
557:
558: htmlGenerator.chartTableEnd();
559: }
560:
561: }
|