Home | History | Annotate | Download | only in pie
      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 
     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