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 17 package com.androidplot.pie; 18 19 import android.graphics.*; 20 21 import com.androidplot.exception.PlotRenderException; 22 import com.androidplot.ui.SeriesRenderer; 23 24 import java.util.Set; 25 26 public 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 PieRenderer(PieChart plot)43 public PieRenderer(PieChart plot) { 44 super(plot); 45 } 46 getRadius(RectF rect)47 public float getRadius(RectF rect) { 48 return rect.width() < rect.height() ? rect.width() / 2 : rect.height() / 2; 49 } 50 51 @Override onRender(Canvas canvas, RectF plotArea)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 drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentFormatter f, float rad, float startAngle, float sweep)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 drawSegmentLabel(Canvas canvas, PointF origin, Segment seg, SegmentFormatter f)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 doDrawLegendIcon(Canvas canvas, RectF rect, SegmentFormatter formatter)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 */ calculateScale(double[] values)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 getValues()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 calculateLineEnd(float x, float y, float rad, float deg)196 private PointF calculateLineEnd(float x, float y, float rad, float deg) { 197 return calculateLineEnd(new PointF(x, y), rad, deg); 198 } 199 calculateLineEnd(PointF origin, float rad, float deg)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 setDonutSize(float size, DonutMode mode)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 setStartDeg(float deg)227 public void setStartDeg(float deg) { 228 startDeg = deg; 229 } 230 setEndDeg(float deg)231 public void setEndDeg(float deg) { 232 endDeg = deg; 233 } 234 } 235