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:
almic 2018-11-12 10:58:17 -07:00
parent 29f4cc5c2c
commit 42cdba535f
No known key found for this signature in database
GPG key ID: 432BBCA8E2FFF871
6 changed files with 209 additions and 135 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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">

View file

@ -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>

View file

@ -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".

View file

@ -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;