1/* 2 * Copyright 2013 AndroidPlot.com 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.androidplot.pie; 18 19import android.graphics.*; 20 21import com.androidplot.exception.PlotRenderException; 22import com.androidplot.ui.SeriesRenderer; 23 24import java.util.Set; 25 26public class PieRenderer extends SeriesRenderer<PieChart, Segment, SegmentFormatter> { 27 28 // starting angle to use when drawing the first radial line of the first segment. 29 @SuppressWarnings("FieldCanBeLocal") 30 private float startDeg = 0; 31 private float endDeg = 360; 32 33 // TODO: express donut in units other than px. 34 private float donutSize = 0.5f; 35 private DonutMode donutMode = DonutMode.PERCENT; 36 37 public enum DonutMode { 38 PERCENT, 39 DP, 40 PIXELS 41 } 42 43 public PieRenderer(PieChart plot) { 44 super(plot); 45 } 46 47 public float getRadius(RectF rect) { 48 return rect.width() < rect.height() ? rect.width() / 2 : rect.height() / 2; 49 } 50 51 @Override 52 public void onRender(Canvas canvas, RectF plotArea) throws PlotRenderException { 53 54 float radius = getRadius(plotArea); 55 PointF origin = new PointF(plotArea.centerX(), plotArea.centerY()); 56 57 double[] values = getValues(); 58 double scale = calculateScale(values); 59 float offset = startDeg; 60 Set<Segment> segments = getPlot().getSeriesSet(); 61 62 //PointF lastRadial = calculateLineEnd(origin, radius, offset); 63 64 RectF rec = new RectF(origin.x - radius, origin.y - radius, origin.x + radius, origin.y + radius); 65 66 int i = 0; 67 for (Segment segment : segments) { 68 float lastOffset = offset; 69 float sweep = (float) (scale * (values[i]) * 360); 70 offset += sweep; 71 //PointF radial = calculateLineEnd(origin, radius, offset); 72 drawSegment(canvas, rec, segment, getPlot().getFormatter(segment, PieRenderer.class), 73 radius, lastOffset, sweep); 74 //lastRadial = radial; 75 i++; 76 } 77 } 78 79 protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentFormatter f, 80 float rad, float startAngle, float sweep) { 81 canvas.save(); 82 83 float cx = bounds.centerX(); 84 float cy = bounds.centerY(); 85 86 float donutSizePx; 87 switch(donutMode) { 88 case PERCENT: 89 donutSizePx = donutSize * rad; 90 break; 91 case PIXELS: 92 donutSizePx = (donutSize > 0)?donutSize:(rad + donutSize); 93 break; 94 default: 95 throw new UnsupportedOperationException("Not yet implemented."); 96 } 97 98 // vertices of the first radial: 99 PointF r1Outer = calculateLineEnd(cx, cy, rad, startAngle); 100 PointF r1Inner = calculateLineEnd(cx, cy, donutSizePx, startAngle); 101 102 // vertices of the second radial: 103 PointF r2Outer = calculateLineEnd(cx, cy, rad, startAngle + sweep); 104 PointF r2Inner = calculateLineEnd(cx, cy, donutSizePx, startAngle + sweep); 105 106 Path clip = new Path(); 107 108 //float outerStroke = f.getOuterEdgePaint().getStrokeWidth(); 109 //float halfOuterStroke = outerStroke / 2; 110 111 // leave plenty of room on the outside for stroked borders; 112 // necessary because the clipping border is ugly 113 // and cannot be easily anti aliased. Really we only care about masking off the 114 // radial edges. 115 clip.arcTo(new RectF(bounds.left - rad, 116 bounds.top - rad, 117 bounds.right + rad, 118 bounds.bottom + rad), 119 startAngle, sweep); 120 clip.lineTo(cx, cy); 121 clip.close(); 122 canvas.clipPath(clip); 123 124 Path p = new Path(); 125 p.arcTo(bounds, startAngle, sweep); 126 p.lineTo(r2Inner.x, r2Inner.y); 127 128 // sweep back to original angle: 129 p.arcTo(new RectF( 130 cx - donutSizePx, 131 cy - donutSizePx, 132 cx + donutSizePx, 133 cy + donutSizePx), 134 startAngle + sweep, -sweep); 135 136 p.close(); 137 138 // fill segment: 139 canvas.drawPath(p, f.getFillPaint()); 140 141 // draw radial lines 142 canvas.drawLine(r1Inner.x, r1Inner.y, r1Outer.x, r1Outer.y, f.getRadialEdgePaint()); 143 canvas.drawLine(r2Inner.x, r2Inner.y, r2Outer.x, r2Outer.y, f.getRadialEdgePaint()); 144 145 // draw inner line: 146 canvas.drawCircle(cx, cy, donutSizePx, f.getInnerEdgePaint()); 147 148 // draw outer line: 149 canvas.drawCircle(cx, cy, rad, f.getOuterEdgePaint()); 150 canvas.restore(); 151 152 PointF labelOrigin = calculateLineEnd(cx, cy, 153 (rad-((rad- donutSizePx)/2)), startAngle + (sweep/2)); 154 155 // TODO: move segment labelling outside the segment drawing loop 156 // TODO: so that the labels will not be clipped by the edge of the next 157 // TODO: segment being drawn. 158 drawSegmentLabel(canvas, labelOrigin, seg, f); 159 } 160 161 protected void drawSegmentLabel(Canvas canvas, PointF origin, 162 Segment seg, SegmentFormatter f) { 163 canvas.drawText(seg.getTitle(), origin.x, origin.y, f.getLabelPaint()); 164 165 } 166 167 @Override 168 protected void doDrawLegendIcon(Canvas canvas, RectF rect, SegmentFormatter formatter) { 169 throw new UnsupportedOperationException("Not yet implemented."); 170 } 171 172 /** 173 * Determines how many counts there are per cent of whatever the 174 * pie chart is displaying as a fraction, 1 being 100%. 175 */ 176 private double calculateScale(double[] values) { 177 double total = 0; 178 for (int i = 0; i < values.length; i++) { 179 total += values[i]; 180 } 181 182 return (1d / total); 183 } 184 185 private double[] getValues() { 186 Set<Segment> segments = getPlot().getSeriesSet(); 187 double[] result = new double[segments.size()]; 188 int i = 0; 189 for (Segment seg : getPlot().getSeriesSet()) { 190 result[i] = seg.getValue().doubleValue(); 191 i++; 192 } 193 return result; 194 } 195 196 private PointF calculateLineEnd(float x, float y, float rad, float deg) { 197 return calculateLineEnd(new PointF(x, y), rad, deg); 198 } 199 200 private PointF calculateLineEnd(PointF origin, float rad, float deg) { 201 202 double radians = deg * Math.PI / 180F; 203 double x = rad * Math.cos(radians); 204 double y = rad * Math.sin(radians); 205 206 // convert to screen space: 207 return new PointF(origin.x + (float) x, origin.y + (float) y); 208 } 209 210 public void setDonutSize(float size, DonutMode mode) { 211 switch(mode) { 212 case PERCENT: 213 if(size < 0 || size > 1) { 214 throw new IllegalArgumentException( 215 "Size parameter must be between 0 and 1 when operating in PERCENT mode."); 216 } 217 break; 218 case PIXELS: 219 break; 220 default: 221 throw new UnsupportedOperationException("Not yet implemented."); 222 } 223 donutMode = mode; 224 donutSize = size; 225 } 226 227 public void setStartDeg(float deg) { 228 startDeg = deg; 229 } 230 231 public void setEndDeg(float deg) { 232 endDeg = deg; 233 } 234} 235