Add Curved Slices to Pie Chart
Finally added support for rounded slices, and somewhat improved the code for the PieChartRenderer class.
This commit is contained in:
parent
29f4cc5c2c
commit
42cdba535f
6 changed files with 209 additions and 135 deletions
|
@ -210,6 +210,18 @@ public class PieChartActivity extends DemoBase implements OnSeekBarChangeListene
|
|||
chart.invalidate();
|
||||
break;
|
||||
}
|
||||
case R.id.actionToggleCurvedSlices: {
|
||||
boolean toSet = !chart.isDrawRoundedSlicesEnabled() || !chart.isDrawHoleEnabled();
|
||||
chart.setDrawRoundedSlices(toSet);
|
||||
if (toSet && !chart.isDrawHoleEnabled()) {
|
||||
chart.setDrawHoleEnabled(true);
|
||||
}
|
||||
if (toSet && chart.isDrawSlicesUnderHoleEnabled()) {
|
||||
chart.setDrawSlicesUnderHole(false);
|
||||
}
|
||||
chart.invalidate();
|
||||
break;
|
||||
}
|
||||
case R.id.actionDrawCenter: {
|
||||
if (chart.isDrawCenterTextEnabled())
|
||||
chart.setDrawCenterText(false);
|
||||
|
|
|
@ -205,6 +205,18 @@ public class PiePolylineChartActivity extends DemoBase implements OnSeekBarChang
|
|||
chart.invalidate();
|
||||
break;
|
||||
}
|
||||
case R.id.actionToggleCurvedSlices: {
|
||||
boolean toSet = !chart.isDrawRoundedSlicesEnabled() || !chart.isDrawHoleEnabled();
|
||||
chart.setDrawRoundedSlices(toSet);
|
||||
if (toSet && !chart.isDrawHoleEnabled()) {
|
||||
chart.setDrawHoleEnabled(true);
|
||||
}
|
||||
if (toSet && chart.isDrawSlicesUnderHoleEnabled()) {
|
||||
chart.setDrawSlicesUnderHole(false);
|
||||
}
|
||||
chart.invalidate();
|
||||
break;
|
||||
}
|
||||
case R.id.actionDrawCenter: {
|
||||
if (chart.isDrawCenterTextEnabled())
|
||||
chart.setDrawCenterText(false);
|
||||
|
|
|
@ -25,6 +25,10 @@
|
|||
android:id="@+id/actionToggleHole"
|
||||
android:title="@string/actionToggleHole">
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/actionToggleCurvedSlices"
|
||||
android:title="@string/actionToggleCurvedSlices">
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/actionDrawCenter"
|
||||
android:title="@string/actionDrawCenterText">
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
|
||||
<string name="actionTogglePercent">Toggle Percent</string>
|
||||
<string name="actionToggleHole">Toggle Hole</string>
|
||||
<string name="actionToggleCurvedSlices">Toggle Curved Slices</string>
|
||||
<string name="actionDrawCenterText">Draw Center Text</string>
|
||||
<string name="actionToggleHighlightCircle">Toggle Highlight Circle</string>
|
||||
<string name="actionToggleRotation">Toggle Rotation</string>
|
||||
|
|
|
@ -650,6 +650,16 @@ public class PieChart extends PieRadarChartBase<PieData> {
|
|||
((PieChartRenderer) mRenderer).getPaintEntryLabels().setTextSize(Utils.convertDpToPixel(size));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to draw slices in a curved fashion, only works if drawing the hole is enabled
|
||||
* and if the slices are not drawn under the hole.
|
||||
*
|
||||
* @param enabled draw curved ends of slices
|
||||
*/
|
||||
public void setDrawRoundedSlices(boolean enabled) {
|
||||
mDrawRoundedSlices = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the chart is set to draw each end of a pie-slice
|
||||
* "rounded".
|
||||
|
|
|
@ -230,6 +230,9 @@ public class PieChartRenderer extends DataRenderer {
|
|||
final float userInnerRadius = drawInnerArc
|
||||
? radius * (mChart.getHoleRadius() / 100.f)
|
||||
: 0.f;
|
||||
final float roundedRadius = (radius - (radius * mChart.getHoleRadius() / 100f)) / 2f;
|
||||
final RectF roundedCircleBox = new RectF();
|
||||
final boolean drawRoundedSlices = drawInnerArc && mChart.isDrawRoundedSlicesEnabled();
|
||||
|
||||
int visibleAngleCount = 0;
|
||||
for (int j = 0; j < entryCount; j++) {
|
||||
|
@ -249,133 +252,151 @@ public class PieChartRenderer extends DataRenderer {
|
|||
Entry e = dataSet.getEntryForIndex(j);
|
||||
|
||||
// draw only if the value is greater than zero
|
||||
if ((Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) {
|
||||
|
||||
if (!mChart.needsHighlight(j)) {
|
||||
|
||||
final boolean accountForSliceSpacing = sliceSpace > 0.f && sliceAngle <= 180.f;
|
||||
|
||||
mRenderPaint.setColor(dataSet.getColor(j));
|
||||
|
||||
final float sliceSpaceAngleOuter = visibleAngleCount == 1 ?
|
||||
0.f :
|
||||
sliceSpace / (Utils.FDEG2RAD * radius);
|
||||
final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY;
|
||||
float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY;
|
||||
if (sweepAngleOuter < 0.f) {
|
||||
sweepAngleOuter = 0.f;
|
||||
}
|
||||
|
||||
mPathBuffer.reset();
|
||||
|
||||
float arcStartPointX = center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD);
|
||||
float arcStartPointY = center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD);
|
||||
|
||||
if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) {
|
||||
// Android is doing "mod 360"
|
||||
mPathBuffer.addCircle(center.x, center.y, radius, Path.Direction.CW);
|
||||
} else {
|
||||
|
||||
mPathBuffer.moveTo(arcStartPointX, arcStartPointY);
|
||||
|
||||
mPathBuffer.arcTo(
|
||||
circleBox,
|
||||
startAngleOuter,
|
||||
sweepAngleOuter
|
||||
);
|
||||
}
|
||||
|
||||
// API < 21 does not receive floats in addArc, but a RectF
|
||||
mInnerRectBuffer.set(
|
||||
center.x - innerRadius,
|
||||
center.y - innerRadius,
|
||||
center.x + innerRadius,
|
||||
center.y + innerRadius);
|
||||
|
||||
if (drawInnerArc &&
|
||||
(innerRadius > 0.f || accountForSliceSpacing)) {
|
||||
|
||||
if (accountForSliceSpacing) {
|
||||
float minSpacedRadius =
|
||||
calculateMinimumRadiusForSpacedSlice(
|
||||
center, radius,
|
||||
sliceAngle * phaseY,
|
||||
arcStartPointX, arcStartPointY,
|
||||
startAngleOuter,
|
||||
sweepAngleOuter);
|
||||
|
||||
if (minSpacedRadius < 0.f)
|
||||
minSpacedRadius = -minSpacedRadius;
|
||||
|
||||
innerRadius = Math.max(innerRadius, minSpacedRadius);
|
||||
}
|
||||
|
||||
final float sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.f ?
|
||||
0.f :
|
||||
sliceSpace / (Utils.FDEG2RAD * innerRadius);
|
||||
final float startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.f) * phaseY;
|
||||
float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY;
|
||||
if (sweepAngleInner < 0.f) {
|
||||
sweepAngleInner = 0.f;
|
||||
}
|
||||
final float endAngleInner = startAngleInner + sweepAngleInner;
|
||||
|
||||
if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) {
|
||||
// Android is doing "mod 360"
|
||||
mPathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW);
|
||||
} else {
|
||||
|
||||
mPathBuffer.lineTo(
|
||||
center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD),
|
||||
center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD));
|
||||
|
||||
mPathBuffer.arcTo(
|
||||
mInnerRectBuffer,
|
||||
endAngleInner,
|
||||
-sweepAngleInner
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
if (sweepAngleOuter % 360f > Utils.FLOAT_EPSILON) {
|
||||
if (accountForSliceSpacing) {
|
||||
|
||||
float angleMiddle = startAngleOuter + sweepAngleOuter / 2.f;
|
||||
|
||||
float sliceSpaceOffset =
|
||||
calculateMinimumRadiusForSpacedSlice(
|
||||
center,
|
||||
radius,
|
||||
sliceAngle * phaseY,
|
||||
arcStartPointX,
|
||||
arcStartPointY,
|
||||
startAngleOuter,
|
||||
sweepAngleOuter);
|
||||
|
||||
float arcEndPointX = center.x +
|
||||
sliceSpaceOffset * (float) Math.cos(angleMiddle * Utils.FDEG2RAD);
|
||||
float arcEndPointY = center.y +
|
||||
sliceSpaceOffset * (float) Math.sin(angleMiddle * Utils.FDEG2RAD);
|
||||
|
||||
mPathBuffer.lineTo(
|
||||
arcEndPointX,
|
||||
arcEndPointY);
|
||||
|
||||
} else {
|
||||
mPathBuffer.lineTo(
|
||||
center.x,
|
||||
center.y);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mPathBuffer.close();
|
||||
|
||||
mBitmapCanvas.drawPath(mPathBuffer, mRenderPaint);
|
||||
}
|
||||
if (!(Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) {
|
||||
angle += sliceAngle * phaseX;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't draw if it's highlighted, unless the chart uses rounded slices
|
||||
if (mChart.needsHighlight(j) && !drawRoundedSlices) {
|
||||
angle += sliceAngle * phaseX;
|
||||
continue;
|
||||
}
|
||||
|
||||
final boolean accountForSliceSpacing = sliceSpace > 0.f && sliceAngle <= 180.f;
|
||||
|
||||
mRenderPaint.setColor(dataSet.getColor(j));
|
||||
|
||||
final float sliceSpaceAngleOuter = visibleAngleCount == 1 ?
|
||||
0.f :
|
||||
sliceSpace / (Utils.FDEG2RAD * radius);
|
||||
final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY;
|
||||
float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY;
|
||||
if (sweepAngleOuter < 0.f) {
|
||||
sweepAngleOuter = 0.f;
|
||||
}
|
||||
|
||||
mPathBuffer.reset();
|
||||
|
||||
if (drawRoundedSlices) {
|
||||
float x = center.x + (radius - roundedRadius) * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD);
|
||||
float y = center.y + (radius - roundedRadius) * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD);
|
||||
roundedCircleBox.set(x - roundedRadius, y - roundedRadius, x + roundedRadius, y + roundedRadius);
|
||||
}
|
||||
|
||||
float arcStartPointX = center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD);
|
||||
float arcStartPointY = center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD);
|
||||
|
||||
if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) {
|
||||
// Android is doing "mod 360"
|
||||
mPathBuffer.addCircle(center.x, center.y, radius, Path.Direction.CW);
|
||||
} else {
|
||||
|
||||
if (drawRoundedSlices) {
|
||||
mPathBuffer.arcTo(roundedCircleBox, startAngleOuter + 180, -180);
|
||||
}
|
||||
|
||||
mPathBuffer.arcTo(
|
||||
circleBox,
|
||||
startAngleOuter,
|
||||
sweepAngleOuter
|
||||
);
|
||||
}
|
||||
|
||||
// API < 21 does not receive floats in addArc, but a RectF
|
||||
mInnerRectBuffer.set(
|
||||
center.x - innerRadius,
|
||||
center.y - innerRadius,
|
||||
center.x + innerRadius,
|
||||
center.y + innerRadius);
|
||||
|
||||
if (drawInnerArc && (innerRadius > 0.f || accountForSliceSpacing)) {
|
||||
|
||||
if (accountForSliceSpacing) {
|
||||
float minSpacedRadius =
|
||||
calculateMinimumRadiusForSpacedSlice(
|
||||
center, radius,
|
||||
sliceAngle * phaseY,
|
||||
arcStartPointX, arcStartPointY,
|
||||
startAngleOuter,
|
||||
sweepAngleOuter);
|
||||
|
||||
if (minSpacedRadius < 0.f)
|
||||
minSpacedRadius = -minSpacedRadius;
|
||||
|
||||
innerRadius = Math.max(innerRadius, minSpacedRadius);
|
||||
}
|
||||
|
||||
final float sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.f ?
|
||||
0.f :
|
||||
sliceSpace / (Utils.FDEG2RAD * innerRadius);
|
||||
final float startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.f) * phaseY;
|
||||
float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY;
|
||||
if (sweepAngleInner < 0.f) {
|
||||
sweepAngleInner = 0.f;
|
||||
}
|
||||
final float endAngleInner = startAngleInner + sweepAngleInner;
|
||||
|
||||
if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) {
|
||||
// Android is doing "mod 360"
|
||||
mPathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW);
|
||||
} else {
|
||||
|
||||
if (drawRoundedSlices) {
|
||||
float x = center.x + (radius - roundedRadius) * (float) Math.cos(endAngleInner * Utils.FDEG2RAD);
|
||||
float y = center.y + (radius - roundedRadius) * (float) Math.sin(endAngleInner * Utils.FDEG2RAD);
|
||||
roundedCircleBox.set(x - roundedRadius, y - roundedRadius, x + roundedRadius, y + roundedRadius);
|
||||
mPathBuffer.arcTo(roundedCircleBox, endAngleInner, 180);
|
||||
} else
|
||||
mPathBuffer.lineTo(
|
||||
center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD),
|
||||
center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD));
|
||||
|
||||
mPathBuffer.arcTo(
|
||||
mInnerRectBuffer,
|
||||
endAngleInner,
|
||||
-sweepAngleInner
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
if (sweepAngleOuter % 360f > Utils.FLOAT_EPSILON) {
|
||||
if (accountForSliceSpacing) {
|
||||
|
||||
float angleMiddle = startAngleOuter + sweepAngleOuter / 2.f;
|
||||
|
||||
float sliceSpaceOffset =
|
||||
calculateMinimumRadiusForSpacedSlice(
|
||||
center,
|
||||
radius,
|
||||
sliceAngle * phaseY,
|
||||
arcStartPointX,
|
||||
arcStartPointY,
|
||||
startAngleOuter,
|
||||
sweepAngleOuter);
|
||||
|
||||
float arcEndPointX = center.x +
|
||||
sliceSpaceOffset * (float) Math.cos(angleMiddle * Utils.FDEG2RAD);
|
||||
float arcEndPointY = center.y +
|
||||
sliceSpaceOffset * (float) Math.sin(angleMiddle * Utils.FDEG2RAD);
|
||||
|
||||
mPathBuffer.lineTo(
|
||||
arcEndPointX,
|
||||
arcEndPointY);
|
||||
|
||||
} else {
|
||||
mPathBuffer.lineTo(
|
||||
center.x,
|
||||
center.y);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mPathBuffer.close();
|
||||
|
||||
mBitmapCanvas.drawPath(mPathBuffer, mRenderPaint);
|
||||
|
||||
angle += sliceAngle * phaseX;
|
||||
}
|
||||
|
||||
|
@ -396,11 +417,17 @@ public class PieChartRenderer extends DataRenderer {
|
|||
float phaseX = mAnimator.getPhaseX();
|
||||
float phaseY = mAnimator.getPhaseY();
|
||||
|
||||
final float roundedRadius = (radius - (radius * mChart.getHoleRadius() / 100f)) / 2f;
|
||||
final float holeRadiusPercent = mChart.getHoleRadius() / 100.f;
|
||||
float labelRadiusOffset = radius / 10f * 3.6f;
|
||||
|
||||
if (mChart.isDrawHoleEnabled()) {
|
||||
labelRadiusOffset = (radius - (radius * holeRadiusPercent)) / 2f;
|
||||
|
||||
if (!mChart.isDrawSlicesUnderHoleEnabled() && mChart.isDrawRoundedSlicesEnabled()) {
|
||||
// Add curved circle slice and spacing to rotation angle, so that it sits nicely inside
|
||||
rotationAngle += roundedRadius * 360 / (Math.PI * 2 * radius);
|
||||
}
|
||||
}
|
||||
|
||||
final float labelRadius = radius - labelRadiusOffset;
|
||||
|
@ -472,6 +499,7 @@ public class PieChartRenderer extends DataRenderer {
|
|||
float value = mChart.isUsePercentValuesEnabled() ? entry.getY()
|
||||
/ yValueSum * 100f : entry.getY();
|
||||
String formattedValue = formatter.getPieLabel(value, entry);
|
||||
String entryLabel = entry.getLabel();
|
||||
|
||||
final float sliceXBase = (float) Math.cos(transformedAngle * Utils.FDEG2RAD);
|
||||
final float sliceYBase = (float) Math.sin(transformedAngle * Utils.FDEG2RAD);
|
||||
|
@ -552,13 +580,13 @@ public class PieChartRenderer extends DataRenderer {
|
|||
|
||||
drawValue(c, formattedValue, labelPtx, labelPty, dataSet.getValueTextColor(j));
|
||||
|
||||
if (j < data.getEntryCount() && entry.getLabel() != null) {
|
||||
drawEntryLabel(c, entry.getLabel(), labelPtx, labelPty + lineHeight);
|
||||
if (j < data.getEntryCount() && entryLabel != null) {
|
||||
drawEntryLabel(c, entryLabel, labelPtx, labelPty + lineHeight);
|
||||
}
|
||||
|
||||
} else if (drawXOutside) {
|
||||
if (j < data.getEntryCount() && entry.getLabel() != null) {
|
||||
drawEntryLabel(c, entry.getLabel(), labelPtx, labelPty + lineHeight / 2.f);
|
||||
if (j < data.getEntryCount() && entryLabel != null) {
|
||||
drawEntryLabel(c, entryLabel, labelPtx, labelPty + lineHeight / 2.f);
|
||||
}
|
||||
} else if (drawYOutside) {
|
||||
|
||||
|
@ -578,13 +606,13 @@ public class PieChartRenderer extends DataRenderer {
|
|||
|
||||
drawValue(c, formattedValue, x, y, dataSet.getValueTextColor(j));
|
||||
|
||||
if (j < data.getEntryCount() && entry.getLabel() != null) {
|
||||
drawEntryLabel(c, entry.getLabel(), x, y + lineHeight);
|
||||
if (j < data.getEntryCount() && entryLabel != null) {
|
||||
drawEntryLabel(c, entryLabel, x, y + lineHeight);
|
||||
}
|
||||
|
||||
} else if (drawXInside) {
|
||||
if (j < data.getEntryCount() && entry.getLabel() != null) {
|
||||
drawEntryLabel(c, entry.getLabel(), x, y + lineHeight / 2f);
|
||||
if (j < data.getEntryCount() && entryLabel != null) {
|
||||
drawEntryLabel(c, entryLabel, x, y + lineHeight / 2f);
|
||||
}
|
||||
} else if (drawYInside) {
|
||||
drawValue(c, formattedValue, x, y + lineHeight / 2f, dataSet.getValueTextColor(j));
|
||||
|
@ -637,7 +665,6 @@ public class PieChartRenderer extends DataRenderer {
|
|||
|
||||
@Override
|
||||
public void drawExtras(Canvas c) {
|
||||
// drawCircles(c);
|
||||
drawHole(c);
|
||||
c.drawBitmap(mDrawBitmap.get(), 0, 0, null);
|
||||
drawCenterText(c);
|
||||
|
@ -763,6 +790,15 @@ public class PieChartRenderer extends DataRenderer {
|
|||
@Override
|
||||
public void drawHighlighted(Canvas c, Highlight[] indices) {
|
||||
|
||||
/* Skip entirely if using rounded circle slices, because it doesn't make sense to highlight
|
||||
* in this way.
|
||||
* TODO: add support for changing slice color with highlighting rather than only shifting the slice
|
||||
*/
|
||||
|
||||
final boolean drawInnerArc = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled();
|
||||
if (drawInnerArc && mChart.isDrawRoundedSlicesEnabled())
|
||||
return;
|
||||
|
||||
float phaseX = mAnimator.getPhaseX();
|
||||
float phaseY = mAnimator.getPhaseY();
|
||||
|
||||
|
@ -773,7 +809,6 @@ public class PieChartRenderer extends DataRenderer {
|
|||
float[] absoluteAngles = mChart.getAbsoluteAngles();
|
||||
final MPPointF center = mChart.getCenterCircleBox();
|
||||
final float radius = mChart.getRadius();
|
||||
final boolean drawInnerArc = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled();
|
||||
final float userInnerRadius = drawInnerArc
|
||||
? radius * (mChart.getHoleRadius() / 100.f)
|
||||
: 0.f;
|
||||
|
|
Loading…
Add table
Reference in a new issue