/*
* Copyright 2013 AndroidPlot.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.androidplot.pie;
import android.graphics.*;
import com.androidplot.exception.PlotRenderException;
import com.androidplot.ui.SeriesRenderer;
import java.util.Set;
public class PieRenderer extends SeriesRenderer<PieChart, Segment, SegmentFormatter> {
// starting angle to use when drawing the first radial line of the first segment.
@SuppressWarnings("FieldCanBeLocal")
private float startDeg = 0;
private float endDeg = 360;
// TODO: express donut in units other than px.
private float donutSize = 0.5f;
private DonutMode donutMode = DonutMode.PERCENT;
public enum DonutMode {
PERCENT,
DP,
PIXELS
}
public PieRenderer(PieChart plot) {
super(plot);
}
public float getRadius(RectF rect) {
return rect.width() < rect.height() ? rect.width() / 2 : rect.height() / 2;
}
@Override
public void onRender(Canvas canvas, RectF plotArea) throws PlotRenderException {
float radius = getRadius(plotArea);
PointF origin = new PointF(plotArea.centerX(), plotArea.centerY());
double[] values = getValues();
double scale = calculateScale(values);
float offset = startDeg;
Set<Segment> segments = getPlot().getSeriesSet();
//PointF lastRadial = calculateLineEnd(origin, radius, offset);
RectF rec = new RectF(origin.x - radius, origin.y - radius, origin.x + radius, origin.y + radius);
int i = 0;
for (Segment segment : segments) {
float lastOffset = offset;
float sweep = (float) (scale * (values[i]) * 360);
offset += sweep;
//PointF radial = calculateLineEnd(origin, radius, offset);
drawSegment(canvas, rec, segment, getPlot().getFormatter(segment, getClass()),
radius, lastOffset, sweep);
//lastRadial = radial;
i++;
}
}
protected void drawSegment(Canvas canvas, RectF bounds, Segment seg, SegmentFormatter f,
float rad, float startAngle, float sweep) {
canvas.save();
float cx = bounds.centerX();
float cy = bounds.centerY();
float donutSizePx;
switch(donutMode) {
case PERCENT:
donutSizePx = donutSize * rad;
break;
case PIXELS:
donutSizePx = (donutSize > 0)?donutSize:(rad + donutSize);
break;
default:
throw new UnsupportedOperationException("Not yet implemented.");
}
// do we have a pie chart of less than 100%
if(Math.abs(sweep - 360f) > Float.MIN_VALUE) {
// vertices of the first radial:
PointF r1Outer = calculateLineEnd(cx, cy, rad, startAngle);
PointF r1Inner = calculateLineEnd(cx, cy, donutSizePx, startAngle);
// vertices of the second radial:
PointF r2Outer = calculateLineEnd(cx, cy, rad, startAngle + sweep);
PointF r2Inner = calculateLineEnd(cx, cy, donutSizePx, startAngle + sweep);
Path clip = new Path();
//float outerStroke = f.getOuterEdgePaint().getStrokeWidth();
//float halfOuterStroke = outerStroke / 2;
// leave plenty of room on the outside for stroked borders;
// necessary because the clipping border is ugly
// and cannot be easily anti aliased. Really we only care about masking off the
// radial edges.
clip.arcTo(new RectF(bounds.left - rad,
bounds.top - rad,
bounds.right + rad,
bounds.bottom + rad),
startAngle, sweep);
clip.lineTo(cx, cy);
clip.close();
canvas.clipPath(clip);
Path p = new Path();
p.arcTo(bounds, startAngle, sweep);
p.lineTo(r2Inner.x, r2Inner.y);
// sweep back to original angle:
p.arcTo(new RectF(
cx - donutSizePx,
cy - donutSizePx,
cx + donutSizePx,
cy + donutSizePx),
startAngle + sweep, -sweep);
p.close();
// fill segment:
canvas.drawPath(p, f.getFillPaint());
// draw radial lines
canvas.drawLine(r1Inner.x, r1Inner.y, r1Outer.x, r1Outer.y, f.getRadialEdgePaint());
canvas.drawLine(r2Inner.x, r2Inner.y, r2Outer.x, r2Outer.y, f.getRadialEdgePaint());
}
else {
canvas.save(Canvas.CLIP_SAVE_FLAG);
Path chart = new Path();
chart.addCircle(cx, cy, rad, Path.Direction.CW);
Path inside = new Path();
inside.addCircle(cx, cy, donutSizePx, Path.Direction.CW);
canvas.clipPath(inside, Region.Op.DIFFERENCE);
canvas.drawPath(chart, f.getFillPaint());
canvas.restore();
}
// draw inner line:
canvas.drawCircle(cx, cy, donutSizePx, f.getInnerEdgePaint());
// draw outer line:
canvas.drawCircle(cx, cy, rad, f.getOuterEdgePaint());
canvas.restore();
PointF labelOrigin = calculateLineEnd(cx, cy,
(rad-((rad- donutSizePx)/2)), startAngle + (sweep/2));
// TODO: move segment labelling outside the segment drawing loop
// TODO: so that the labels will not be clipped by the edge of the next
// TODO: segment being drawn.
drawSegmentLabel(canvas, labelOrigin, seg, f);
}
protected void drawSegmentLabel(Canvas canvas, PointF origin,
Segment seg, SegmentFormatter f) {
canvas.drawText(seg.getTitle(), origin.x, origin.y, f.getLabelPaint());
}
@Override
protected void doDrawLegendIcon(Canvas canvas, RectF rect, SegmentFormatter formatter) {
throw new UnsupportedOperationException("Not yet implemented.");
}
/**
* Determines how many counts there are per cent of whatever the
* pie chart is displaying as a fraction, 1 being 100%.
*/
protected double calculateScale(double[] values) {
double total = 0;
for (int i = 0; i < values.length; i++) {
total += values[i];
}
return (1d / total);
}
protected double[] getValues() {
Set<Segment> segments = getPlot().getSeriesSet();
double[] result = new double[segments.size()];
int i = 0;
for (Segment seg : getPlot().getSeriesSet()) {
result[i] = seg.getValue().doubleValue();
i++;
}
return result;
}
protected PointF calculateLineEnd(float x, float y, float rad, float deg) {
return calculateLineEnd(new PointF(x, y), rad, deg);
}
protected PointF calculateLineEnd(PointF origin, float rad, float deg) {
double radians = deg * Math.PI / 180F;
double x = rad * Math.cos(radians);
double y = rad * Math.sin(radians);
// convert to screen space:
return new PointF(origin.x + (float) x, origin.y + (float) y);
}
public void setDonutSize(float size, DonutMode mode) {
switch(mode) {
case PERCENT:
if(size < 0 || size > 1) {
throw new IllegalArgumentException(
"Size parameter must be between 0 and 1 when operating in PERCENT mode.");
}
break;
case PIXELS:
break;
default:
throw new UnsupportedOperationException("Not yet implemented.");
}
donutMode = mode;
donutSize = size;
}
/**
* Retrieve the segment containing the specified point. This current implementation
* only matches against angle; clicks outside of the pie/donut inner/outer boundaries
* will still trigger a match on the segment whose begining and ending angle contains
* the angle of the line drawn between the pie chart's center point and the clicked point.
* @param point The clicked point
* @return Segment containing the clicked point.
*/
public Segment getContainingSegment(PointF point) {
RectF plotArea = getPlot().getPieWidget().getWidgetDimensions().marginatedRect;
// figure out the angle in degrees of the line between the clicked point
// and the origin of the plotArea:
PointF origin = new PointF(plotArea.centerX(), plotArea.centerY());
float dx = point.x - origin.x;
float dy = point.y - origin.y;
double theta = Math.atan2(dy, dx);
double angle = (theta * (180f/Math.PI));
if(angle < 0) {
// convert angle to 0-360 range with 0 being in the
// traditional "westerly" orientation:
angle += 360;
}
// find the segment whose starting and ending angle (degs) contains
// the angle calculated above
Set<Segment> segments = getPlot().getSeriesSet();
int i = 0;
double[] values = getValues();
double scale = calculateScale(values);
float offset = startDeg;
for (Segment segment : segments) {
float lastOffset = offset;
float sweep = (float) (scale * (values[i]) * 360);
offset += sweep;
if(angle >= lastOffset && angle <= offset) {
return segment;
}
i++;
}
return null;
}
public void setStartDeg(float deg) {
startDeg = deg;
}
public float getStartDeg() {
return startDeg;
}
public void setEndDeg(float deg) {
endDeg = deg;
}
public float getEndDeg() {
return endDeg;
}
}