From 090c16e0cadef92a2f9e23162c6bf2b2c748082e Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Tue, 28 Feb 2023 15:24:02 +0100 Subject: [PATCH] Fix PieChart values overlap --- MPChartLib/src/main/res/values/attrs.xml | 11 + .../info/appdev/charting/charts/PieChart2.kt | 732 ++++++++++++++++ .../renderer/PieChartRendererFixCover.kt | 783 ++++++++++++++++++ 3 files changed, 1526 insertions(+) create mode 100644 MPChartLib/src/main/res/values/attrs.xml create mode 100644 chartLib/src/main/kotlin/info/appdev/charting/charts/PieChart2.kt create mode 100644 chartLib/src/main/kotlin/info/appdev/charting/renderer/PieChartRendererFixCover.kt diff --git a/MPChartLib/src/main/res/values/attrs.xml b/MPChartLib/src/main/res/values/attrs.xml new file mode 100644 index 0000000000..b41e435222 --- /dev/null +++ b/MPChartLib/src/main/res/values/attrs.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/chartLib/src/main/kotlin/info/appdev/charting/charts/PieChart2.kt b/chartLib/src/main/kotlin/info/appdev/charting/charts/PieChart2.kt new file mode 100644 index 0000000000..90565ba148 --- /dev/null +++ b/chartLib/src/main/kotlin/info/appdev/charting/charts/PieChart2.kt @@ -0,0 +1,732 @@ +package info.appdev.charting.charts + +import android.content.Context +import android.content.res.TypedArray +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.Typeface +import android.text.TextUtils +import android.util.AttributeSet +import info.appdev.charting.components.XAxis +import info.appdev.charting.data.PieData +import info.appdev.charting.data.PieEntry +import info.appdev.charting.highlight.Highlight +import info.appdev.charting.highlight.PieHighlighter +import info.appdev.charting.interfaces.datasets.IPieDataSet +import info.appdev.charting.renderer.PieChartRenderer +import info.appdev.charting.renderer.PieChartRendererFixCover +import info.appdev.charting.utils.PointF +import info.appdev.charting.utils.Utils +import info.appdev.charting.utils.getNormalizedAngle +import java.util.Locale +import kotlin.math.abs +import kotlin.math.cos +import kotlin.math.min +import kotlin.math.sin + +/** + * View that represents a pie chart. Draws cake like slices. + */ +class PieChart2 : PieRadarChartBase { + private var mode: String? = null + + /** + * rect object that represents the bounds of the piechart, needed for + * drawing the circle + */ + private val mCircleBox: RectF = RectF() + + /** + * Returns true if drawing the entry labels is enabled, false if not. + * + * @return + */ + /** + * flag indicating if entry labels should be drawn or not + */ + var isDrawEntryLabelsEnabled: Boolean = true + private set + + /** + * returns an integer array of all the different angles the chart slices + * have the angles in the returned array determine how much space (of 360°) + * each slice takes + * + * @return + */ + /** + * array that holds the width of each pie-slice in degrees + */ + var drawAngles: FloatArray = FloatArray(1) + private set + + /** + * returns the absolute angles of the different chart slices (where the + * slices end) + * + * @return + */ + /** + * array that holds the absolute angle in degrees of each slice + */ + var absoluteAngles: FloatArray = FloatArray(1) + private set + + /** + * returns true if the hole in the center of the pie-chart is set to be + * visible, false if not + * + * @return + */ + /** + * set this to true to draw the pie center empty + * + * @param enabled + */ + /** + * if true, the white hole inside the chart will be drawn + */ + var isDrawHoleEnabled: Boolean = true + + /** + * Returns true if the inner tips of the slices are visible behind the hole, + * false if not. + * + * @return true if slices are visible behind the hole. + */ + /** + * if true, the hole will see-through to the inner tips of the slices + */ + var isDrawSlicesUnderHoleEnabled: Boolean = false + private set + + /** + * Returns true if using percentage values is enabled for the chart. + * + * @return + */ + /** + * if true, the values inside the piechart are drawn as percent values + */ + var isUsePercentValuesEnabled: Boolean = false + private set + + /** + * Returns true if the chart is set to draw each end of a pie-slice + * "rounded". + * + * @return + */ + /** + * if true, the slices of the piechart are rounded + */ + var isDrawRoundedSlicesEnabled: Boolean = false + private set + + /** + * variable for the text that is drawn in the center of the pie-chart + */ + private var mCenterText: CharSequence? = "" + + private val mCenterTextOffset: PointF = PointF.getInstance(0F, 0F) + + /** + * Returns the size of the hole radius in percent of the total radius. + * + * @return + */ + /** + * sets the radius of the hole in the center of the piechart in percent of + * the maximum radius (max = the radius of the whole chart), default 50% + * + * @param percent + */ + /** + * indicates the size of the hole in the center of the piechart, default: + * radius / 2 + */ + var holeRadius: Float = 50f + + /** + * sets the radius of the transparent circle that is drawn next to the hole + * in the piechart in percent of the maximum radius (max = the radius of the + * whole chart), default 55% -> means 5% larger than the center-hole by + * default + * + * @param percent + */ + /** + * the radius of the transparent circle next to the chart-hole in the center + */ + var transparentCircleRadius: Float = 55f + + /** + * returns true if drawing the center text is enabled + * + * @return + */ + /** + * if enabled, centertext is drawn + */ + var isDrawCenterTextEnabled: Boolean = true + private set + + /** + * the rectangular radius of the bounding box for the center text, as a percentage of the pie + * hole + * default 1.f (100%) + */ + /** + * the rectangular radius of the bounding box for the center text, as a percentage of the pie + * hole + * default 1.f (100%) + */ + var centerTextRadiusPercent: Float = 100f + + protected var mMaxAngle: Float = 360f + + /** + * Minimum angle to draw slices, this only works if there is enough room for all slices to have + * the minimum angle, default 0f. + */ + private var mMinAngleForSlices = 0f + + constructor(context: Context?) : super(context) + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + + constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) { + getAttrs(attrs) + } + + private fun getAttrs(attrs: AttributeSet?) { + if (attrs != null) { + val a: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.PieChart) + mode = a.getString(R.styleable.PieChart_mp_chart_out_value_place_mode) + a.recycle() + } + (mRenderer as PieChartRendererFixCover).mode = mode + } + + + override fun init() { + super.init() + + mRenderer = PieChartRendererFixCover(this, mAnimator, viewPortHandler) + + highlighter = PieHighlighter(this) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (mData == null) return + + mRenderer.drawData(canvas) + + if (valuesToHighlight()) + mRenderer?.drawHighlighted(canvas, indicesToHighlight) + + mRenderer?.drawExtras(canvas) + + mRenderer?.drawValues(canvas) + + legendRenderer?.renderLegend(canvas) + + drawDescription(canvas) + + drawMarkers(canvas) + } + + override fun calculateOffsets() { + super.calculateOffsets() + + // prevent null pointer when no data set + if (mData == null) + return + + val radius = diameter / 2f + + val c: PointF = getCenterOffsets() + + val shift: Float = mData.getDataSet().getSelectionShift() + + // create the circle box that will contain the pie-chart (the bounds of + // the pie-chart) + mCircleBox.set( + c.x - radius + shift, + c.y - radius + shift, + c.x + radius - shift, + c.y + radius - shift + ) + + PointF.recycleInstance(c) + } + + override fun calcMinMax() { + calcAngles() + } + + override fun getMarkerPosition(high: Highlight): FloatArray { + val center: PointF = this.centerCircleBox + var r = this.radius + + var off = r / 10f * 3.6f + + if (this.isDrawHoleEnabled) { + off = (r - (r / 100f * this.holeRadius)) / 2f + } + + r -= off // offset to keep things inside the chart + + val entryIndex = high.x.toInt() + + // offset needed to center the drawn text in the slice + val offset = this.drawAngles[entryIndex] / 2 + + // calculate the text position + val x = (r + * cos( + Math.toRadians( + ((rotationAngle + this.absoluteAngles[entryIndex] - offset) + * mAnimator.phaseY).toDouble() + ) + ) + center.x).toFloat() + val y = (r + * sin( + Math.toRadians( + ((rotationAngle + this.absoluteAngles[entryIndex] - offset) + * mAnimator.phaseY).toDouble() + ) + ) + center.y).toFloat() + + PointF.recycleInstance(center) + return floatArrayOf(x, y) + } + + /** + * calculates the needed angles for the chart slices + */ + private fun calcAngles() { + val entryCount: Int = mData!!.entryCount + + if (drawAngles.size != entryCount) { + this.drawAngles = FloatArray(entryCount) + } else { + for (i in 0.. a) return i + } + + return -1 // return -1 if no index found + } + + /** + * Returns the index of the DataSet this x-index belongs to. + * + * @param xIndex + * @return + */ + fun getDataSetIndexForIndex(xIndex: Int): Int { + val dataSets: MutableList = mData.getDataSets() + + for (i in dataSets.indices) { + if (dataSets.get(i).getEntryForXValue(xIndex, Float.NaN) != null) return i + } + + return -1 + } + + /** + * Sets the color for the hole that is drawn in the center of the PieChart + * (if enabled). + * + * @param color + */ + fun setHoleColor(color: Int) { + (mRenderer as PieChartRenderer).getPaintHole().setColor(color) + } + + /** + * Enable or disable the visibility of the inner tips of the slices behind the hole + */ + fun setDrawSlicesUnderHole(enable: Boolean) { + this.isDrawSlicesUnderHoleEnabled = enable + } + + var centerText: CharSequence? + /** + * returns the text that is drawn in the center of the pie-chart + * + * @return + */ + get() = mCenterText + /** + * Sets the text String that is displayed in the center of the PieChart. + * + * @param text + */ + set(text) { + if (text == null) mCenterText = "" + else mCenterText = text + } + + /** + * set this to true to draw the text that is displayed in the center of the + * pie chart + * + * @param enabled + */ + fun setDrawCenterText(enabled: Boolean) { + this.isDrawCenterTextEnabled = enabled + } + + val requiredLegendOffset: Float + get() = mLegendRenderer.getLabelPaint().getTextSize() * 2f + + val requiredBaseOffset: Float + get() = 0f + + val radius: Float + get() { + if (mCircleBox == null) return 0f + else return min(mCircleBox.width() / 2f, mCircleBox.height() / 2f) + } + + val circleBox: RectF + /** + * returns the circlebox, the boundingbox of the pie-chart slices + * + * @return + */ + get() = mCircleBox + + val centerCircleBox: PointF + /** + * returns the center of the circlebox + * + * @return + */ + get() = PointF.getInstance(mCircleBox.centerX(), mCircleBox.centerY()) + + /** + * sets the typeface for the center-text paint + * + * @param t + */ + fun setCenterTextTypeface(t: Typeface?) { + (mRenderer as PieChartRenderer).getPaintCenterText().setTypeface(t) + } + + /** + * Sets the size of the center text of the PieChart in dp. + * + * @param sizeDp + */ + fun setCenterTextSize(sizeDp: Float) { + (mRenderer as PieChartRenderer).getPaintCenterText().setTextSize( + convertDpToPixel(sizeDp) + ) + } + + /** + * Sets the size of the center text of the PieChart in pixels. + * + * @param sizePixels + */ + fun setCenterTextSizePixels(sizePixels: Float) { + (mRenderer as PieChartRenderer).getPaintCenterText().setTextSize(sizePixels) + } + + /** + * Sets the offset the center text should have from it's original position in dp. Default x = 0, y = 0 + * + * @param x + * @param y + */ + fun setCenterTextOffset(x: Float, y: Float) { + mCenterTextOffset.x = convertDpToPixel(x) + mCenterTextOffset.y = convertDpToPixel(y) + } + + val centerTextOffset: PointF + /** + * Returns the offset on the x- and y-axis the center text has in dp. + * + * @return + */ + get() = PointF.getInstance(mCenterTextOffset.x, mCenterTextOffset.y) + + /** + * Sets the color of the center text of the PieChart. + * + * @param color + */ + fun setCenterTextColor(color: Int) { + (mRenderer as PieChartRenderer).getPaintCenterText().setColor(color) + } + + /** + * Sets the color the transparent-circle should have. + * + * @param color + */ + fun setTransparentCircleColor(color: Int) { + val p: Paint = (mRenderer as PieChartRenderer).getPaintTransparentCircle() + val alpha = p.alpha + p.color = color + p.alpha = alpha + } + + /** + * Sets the amount of transparency the transparent circle should have 0 = fully transparent, + * 255 = fully opaque. + * Default value is 100. + * + * @param alpha 0-255 + */ + fun setTransparentCircleAlpha(alpha: Int) { + (mRenderer as PieChartRenderer).getPaintTransparentCircle().setAlpha(alpha) + } + + /** + * Set this to true to draw the entry labels into the pie slices (Provided by the getLabel() method of the PieEntry class). + * Deprecated -> use setDrawEntryLabels(...) instead. + * + * @param enabled + */ + @Deprecated("") + fun setDrawSliceText(enabled: Boolean) { + this.isDrawEntryLabelsEnabled = enabled + } + + /** + * Set this to true to draw the entry labels into the pie slices (Provided by the getLabel() method of the PieEntry class). + * + * @param enabled + */ + fun setDrawEntryLabels(enabled: Boolean) { + this.isDrawEntryLabelsEnabled = enabled + } + + /** + * Sets the color the entry labels are drawn with. + * + * @param color + */ + fun setEntryLabelColor(color: Int) { + (mRenderer as PieChartRenderer).getPaintEntryLabels().setColor(color) + } + + /** + * Sets a custom Typeface for the drawing of the entry labels. + * + * @param tf + */ + fun setEntryLabelTypeface(tf: Typeface?) { + (mRenderer as PieChartRenderer).getPaintEntryLabels().setTypeface(tf) + } + + /** + * Sets the size of the entry labels in dp. Default: 13dp + * + * @param size + */ + fun setEntryLabelTextSize(size: Float) { + (mRenderer as PieChartRenderer).getPaintEntryLabels().setTextSize(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 + */ + fun setDrawRoundedSlices(enabled: Boolean) { + this.isDrawRoundedSlicesEnabled = enabled + } + + /** + * If this is enabled, values inside the PieChart are drawn in percent and + * not with their original value. Values provided for the IValueFormatter to + * format are then provided in percent. + * + * @param enabled + */ + fun setUsePercentValues(enabled: Boolean) { + this.isUsePercentValuesEnabled = enabled + } + + var maxAngle: Float + get() = mMaxAngle + /** + * Sets the max angle that is used for calculating the pie-circle. 360f means + * it's a full PieChart, 180f results in a half-pie-chart. Default: 360f + * + * @param maxangle min 90, max 360 + */ + set(maxangle) { + var maxangle = maxangle + if (maxangle > 360) maxangle = 360f + + if (maxangle < 90) maxangle = 90f + + this.mMaxAngle = maxangle + } + + var minAngleForSlices: Float + /** + * The minimum angle slices on the chart are rendered with, default is 0f. + * + * @return minimum angle for slices + */ + get() = mMinAngleForSlices + /** + * Set the angle to set minimum size for slices, you must call [.notifyDataSetChanged] + * and [.invalidate] when changing this, only works if there is enough room for all + * slices to have the minimum angle. + * + * @param minAngle minimum 0, maximum is half of [.setMaxAngle] + */ + set(minAngle) { + var minAngle = minAngle + if (minAngle > (mMaxAngle / 2f)) minAngle = mMaxAngle / 2f + else if (minAngle < 0) minAngle = 0f + + this.mMinAngleForSlices = minAngle + } + + override fun onDetachedFromWindow() { + // releases the bitmap in the renderer to avoid oom error + if (mRenderer != null && mRenderer is PieChartRenderer) { + (mRenderer as PieChartRenderer).releaseBitmap() + } + super.onDetachedFromWindow() + } + + val accessibilityDescription: String + get() { + val pieData: PieData? = getData() + + var entryCount = 0 + if (pieData != null) entryCount = pieData.getEntryCount() + + val builder = StringBuilder() + + builder.append(String.format(Locale.getDefault(), "The pie chart has %d entries.", entryCount)) + + for (i in 0..= 0) { + rightToLeftCount++ + } else if (rotationAngle % 360 in 90.0..270.0 && rotationAngle != 270f && angle * phaseY % 360.0 > 180.0 && angle * phaseY % 360.0 < 360.0) { + leftToLeftCount++ + } + } else { + rightCount++ + if (rotationAngle != 270f && angle * phaseY % 360.0 > 180.0 && angle * phaseY % 360.0 < 360.0) { + leftToRightCount++ + } else if (rotationAngle % 360 in 90.0..270.0 && rotationAngle != 270f && angle * phaseY % 360.0 <= 180.0 && angle * phaseY % 360.0 >= 0) { + rightToRightCount++ + } + } + } + xIndex++ + } + xIndex = 0 + val measuredHeight = chart.measuredHeight + val topAndBottomSpace = measuredHeight - radius * 2 + val rightSpace = radius * 2 / (rightCount - 1) + val leftSpace = radius * 2 / (leftCount - 1) + var tempRightIndex = 0 + var tempLeftIndex = 0 + var tempLeftToRightIndex = 0 + var tempRightToRightIndex = 0 + var tempRightToLeftIndex = 0 + var tempLeftToLeftIndex = 0 + for (j in 0 until entryCount) { + dataSet.getEntryForIndex(j)?.let { entry -> + angle = if (xIndex == 0) 0f else absoluteAngles[xIndex - 1] * phaseX + val sliceAngle = drawAngles[xIndex] + val sliceSpaceMiddleAngle = sliceSpace / (Utils.FDEG2RAD * labelRadius) + + // offset needed to center the drawn text in the slice + val angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2f) / 2f + angle += angleOffset + val transformedAngle = rotationAngle + angle * phaseY + val value: Float = if (chart.isUsePercentValuesEnabled) + entry.y / yValueSum * 100f + else + entry.y + val sliceXBase = cos((transformedAngle * Utils.FDEG2RAD).toDouble()).toFloat() + val sliceYBase = sin((transformedAngle * Utils.FDEG2RAD).toDouble()).toFloat() + val drawXOutside = drawEntryLabels && + xValuePosition == ValuePosition.OUTSIDE_SLICE + val drawYOutside = drawValues && + yValuePosition == ValuePosition.OUTSIDE_SLICE + val drawXInside = drawEntryLabels && + xValuePosition == ValuePosition.INSIDE_SLICE + val drawYInside = drawValues && + yValuePosition == ValuePosition.INSIDE_SLICE + if (drawXOutside || drawYOutside) { + val valueLineLength1 = dataSet.valueLinePart1Length + val valueLineLength2 = dataSet.valueLinePart2Length + val valueLinePart1OffsetPercentage = dataSet.valueLinePart1OffsetPercentage / 100f + var pt2x: Float + var pt2y: Float + var labelPtx: Float + var labelPty: Float + val line1Radius: Float = if (chart.isDrawHoleEnabled) (radius - radius * holeRadiusPercent + * valueLinePart1OffsetPercentage) + radius * holeRadiusPercent else radius * valueLinePart1OffsetPercentage + if (dataSet.isValueLineVariableLength) labelRadius * valueLineLength2 * + abs(sin((transformedAngle * Utils.FDEG2RAD).toDouble())).toFloat() + else + labelRadius * valueLineLength2 + val pt0x = line1Radius * sliceXBase + center.x + val pt0y = line1Radius * sliceYBase + center.y + val pt1x = labelRadius * (1 + valueLineLength1) * sliceXBase + center.x + val pt1y = labelRadius * (1 + valueLineLength1) * sliceYBase + center.y + if (transformedAngle % 360.0 in 90.0..270.0) { + pt2x = center.x - radius - 5 + if (rotationAngle != 270f && angle * phaseY % 360.0 <= 180.0 && angle * phaseY % 360.0 >= 0) { + pt2y = measuredHeight - topAndBottomSpace / 2 - leftSpace * (tempRightToLeftIndex + leftToLeftCount) + tempRightToLeftIndex++ + tempLeftIndex++ + } else if (rotationAngle % 360 in 90.0..270.0 && rotationAngle != 270f && angle * phaseY % 360.0 > 180.0 && angle * phaseY % 360.0 < 360.0) { + pt2y = measuredHeight - topAndBottomSpace / 2 - leftSpace * tempLeftToLeftIndex + tempLeftToLeftIndex++ + } else { + pt2y = measuredHeight - topAndBottomSpace / 2 - leftSpace * (tempLeftIndex + leftToLeftCount) + tempLeftIndex++ + } + paintValues.textAlign = Paint.Align.RIGHT + if (drawXOutside) paintEntryLabels.textAlign = Paint.Align.RIGHT + labelPtx = pt2x - offset + labelPty = pt2y + } else { + pt2x = center.x + radius + 5 + if (rotationAngle != 270f && angle * phaseY % 360.0 > 180.0 && angle * phaseY % 360.0 < 360.0) { + pt2y = topAndBottomSpace / 2 + rightSpace * (tempLeftToRightIndex + rightToRightCount) + tempLeftToRightIndex++ + tempRightIndex++ + } else if (rotationAngle % 360 in 90.0..270.0 && rotationAngle != 270f && angle * phaseY % 360.0 <= 180.0 && angle * phaseY % 360.0 >= 0) { + pt2y = topAndBottomSpace / 2 + rightSpace * tempRightToRightIndex + tempRightIndex++ + tempRightToRightIndex++ + } else { + pt2y = topAndBottomSpace / 2 + rightSpace * (tempRightIndex + leftToRightCount + rightToRightCount) + tempRightIndex++ + } + paintValues.textAlign = Paint.Align.LEFT + if (drawXOutside) paintEntryLabels.textAlign = Paint.Align.LEFT + labelPtx = pt2x + offset + labelPty = pt2y + } + if (dataSet.valueLineColor != ColorTemplate.COLOR_NONE) { + canvas.drawLine(pt0x, pt0y, pt1x, pt1y, valueLinePaint) + canvas.drawLine(pt1x, pt1y, pt2x, pt2y, valueLinePaint) + } + + // draw everything, depending on settings + if (drawXOutside && drawYOutside) { + drawValue( + canvas, + formatter, + value, + entry, + 0, + labelPtx, + labelPty, + dataSet.getValueTextColor(j) + ) + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(canvas, entry.label!!, labelPtx, labelPty + lineHeight) + } + } else if (drawXOutside) { + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(canvas, entry.label!!, labelPtx, labelPty + lineHeight / 2f) + } + } else if (drawYOutside) { + drawValue( + canvas, formatter, value, entry, 0, labelPtx, labelPty + lineHeight / 2f, dataSet + .getValueTextColor(j) + ) + } + } + if (drawXInside || drawYInside) { + // calculate the text position + val x = labelRadius * sliceXBase + center.x + val y = labelRadius * sliceYBase + center.y + paintValues.textAlign = Paint.Align.CENTER + + // draw everything, depending on settings + if (drawXInside && drawYInside) { + drawValue(canvas, formatter, value, entry, 0, x, y, dataSet.getValueTextColor(j)) + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(canvas, entry.label!!, x, y + lineHeight) + } + } else if (drawXInside) { + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(canvas, entry.label!!, x, y + lineHeight / 2f) + } + } else if (drawYInside) { + drawValue(canvas, formatter, value, entry, 0, x, y + lineHeight / 2f, dataSet.getValueTextColor(j)) + } + } + if (entry.icon != null && dataSet.isDrawIcons) { + val icon = entry.icon + val x = (labelRadius + iconsOffset.y) * sliceXBase + center.x + var y = (labelRadius + iconsOffset.y) * sliceYBase + center.y + y += iconsOffset.x + icon?.let { ic -> + canvas.drawImage( + ic, x.toInt(), y.toInt(), + ) + } + } + } + xIndex++ + } + PointF.recycleInstance(iconsOffset) + } + } + PointF.recycleInstance(center) + canvas.restore() + } + + private fun drawValuesTopAlign(c: Canvas) { + val rect = Rect() + paintEntryLabels.getTextBounds(text, 0, text.length, rect) + val textHeight = rect.height() + val center = chart.centerCircleBox + + // get whole the radius + val radius = chart.radius + val rotationAngle = chart.rotationAngle + val drawAngles = chart.drawAngles + val absoluteAngles = chart.absoluteAngles + val phaseX = animator.phaseX + val phaseY = animator.phaseY + val holeRadiusPercent = chart.holeRadius / 100f + var labelRadiusOffset = radius / 10f * 3.6f + if (chart.isDrawHoleEnabled) { + labelRadiusOffset = radius - radius * holeRadiusPercent / 2f + } + val labelRadius = radius - labelRadiusOffset + val data = chart.getData() + val dataSets = data?.dataSets + val yValueSum = data?.yValueSum ?: 0F + val drawEntryLabels = chart.isDrawEntryLabelsEnabled + var angle: Float + var xIndex = 0 + c.save() + val offset = 5f.convertDpToPixel() + dataSets?.let { + for (i in it.indices) { + val dataSet = dataSets[i] + val drawValues = dataSet.isDrawValues + if (!drawValues && !drawEntryLabels) continue + val xValuePosition = dataSet.xValuePosition + val yValuePosition = dataSet.yValuePosition + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet) + val lineHeight = (paintValues.calcTextHeight("Q") + + 4f.convertDpToPixel()) + val formatter = dataSet.valueFormatter + val entryCount = dataSet.entryCount + valueLinePaint.color = dataSet.valueLineColor + valueLinePaint.strokeWidth = dataSet.valueLineWidth.convertDpToPixel() + val sliceSpace = getSliceSpace(dataSet) + val iconsOffset = PointF.getInstance(dataSet.iconsOffset) + iconsOffset.x = iconsOffset.x.convertDpToPixel() + iconsOffset.y = iconsOffset.y.convertDpToPixel() + var lastPositionOfLeft = 0f + var lastPositionOfRight = 0f + for (j in 0 until entryCount) { + dataSet.getEntryForIndex(j)?.let { entry -> + angle = if (xIndex == 0) 0f else absoluteAngles[xIndex - 1] * phaseX + val sliceAngle = drawAngles[xIndex] + val sliceSpaceMiddleAngle = sliceSpace / (Utils.FDEG2RAD * labelRadius) + + // offset needed to center the drawn text in the slice + val angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2f) / 2f + angle += angleOffset + val transformedAngle = rotationAngle + angle * phaseY + val value: Float = if (chart.isUsePercentValuesEnabled) + entry.y / yValueSum * 100f + else + entry.y + val sliceXBase = cos((transformedAngle * Utils.FDEG2RAD).toDouble()).toFloat() + val sliceYBase = sin((transformedAngle * Utils.FDEG2RAD).toDouble()).toFloat() + val drawXOutside = drawEntryLabels && + xValuePosition == ValuePosition.OUTSIDE_SLICE + val drawYOutside = drawValues && + yValuePosition == ValuePosition.OUTSIDE_SLICE + val drawXInside = drawEntryLabels && + xValuePosition == ValuePosition.INSIDE_SLICE + val drawYInside = drawValues && + yValuePosition == ValuePosition.INSIDE_SLICE + if (drawXOutside || drawYOutside) { + val valueLineLength1 = dataSet.valueLinePart1Length + val valueLineLength2 = dataSet.valueLinePart2Length + val valueLinePart1OffsetPercentage = dataSet.valueLinePart1OffsetPercentage / 100f + var pt2x: Float + var pt2y: Float + var labelPtx: Float + var labelPty: Float + val line1Radius: Float = if (chart.isDrawHoleEnabled) (radius - radius * holeRadiusPercent + * valueLinePart1OffsetPercentage) + radius * holeRadiusPercent else radius * valueLinePart1OffsetPercentage + if (dataSet.isValueLineVariableLength) labelRadius * valueLineLength2 * + abs(sin((transformedAngle * Utils.FDEG2RAD).toDouble())).toFloat() + else + labelRadius * valueLineLength2 + val pt0x = line1Radius * sliceXBase + center.x + val pt0y = line1Radius * sliceYBase + center.y + val pt1x = labelRadius * (1 + valueLineLength1) * sliceXBase + center.x + val pt1y = labelRadius * (1 + valueLineLength1) * sliceYBase + center.y + if (transformedAngle % 360.0 in 90.0..270.0) { + break + } else { + pt2x = center.x + radius + 5 + pt2y = if (lastPositionOfRight == 0f) { + pt1y + } else { + if (pt1y - lastPositionOfRight < textHeight) { + pt1y + (textHeight - (pt1y - lastPositionOfRight)) + } else { + pt1y + } + } + lastPositionOfRight = pt2y + paintValues.textAlign = Paint.Align.LEFT + if (drawXOutside) paintEntryLabels.textAlign = Paint.Align.LEFT + labelPtx = pt2x + offset + labelPty = pt2y + } + if (dataSet.valueLineColor != ColorTemplate.COLOR_NONE) { + c.drawLine(pt0x, pt0y, pt1x, pt1y, valueLinePaint) + c.drawLine(pt1x, pt1y, pt2x, pt2y, valueLinePaint) + } + + // draw everything, depending on settings + if (drawXOutside && drawYOutside) { + drawValue( + c, + formatter, + value, + entry, + 0, + labelPtx, + labelPty, + dataSet.getValueTextColor(j) + ) + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(c, entry.label!!, labelPtx, labelPty + lineHeight) + } + } else if (drawXOutside) { + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(c, entry.label!!, labelPtx, labelPty + lineHeight / 2f) + } + } else if (drawYOutside) { + drawValue(c, formatter, value, entry, 0, labelPtx, labelPty + lineHeight / 2f, dataSet.getValueTextColor(j)) + } + } + if (drawXInside || drawYInside) { + // calculate the text position + val x = labelRadius * sliceXBase + center.x + val y = labelRadius * sliceYBase + center.y + paintValues.textAlign = Paint.Align.CENTER + + // draw everything, depending on settings + if (drawXInside && drawYInside) { + drawValue(c, formatter, value, entry, 0, x, y, dataSet.getValueTextColor(j)) + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(c, entry.label!!, x, y + lineHeight) + } + } else if (drawXInside) { + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(c, entry.label!!, x, y + lineHeight / 2f) + } + } else if (drawYInside) { + drawValue(c, formatter, value, entry, 0, x, y + lineHeight / 2f, dataSet.getValueTextColor(j)) + } + } + if (entry.icon != null && dataSet.isDrawIcons) { + val icon = entry.icon + val x = (labelRadius + iconsOffset.y) * sliceXBase + center.x + var y = (labelRadius + iconsOffset.y) * sliceYBase + center.y + y += iconsOffset.x + icon?.let { ic -> + c.drawImage( + ic, x.toInt(), y.toInt() + ) + } + } + } + xIndex++ + } + + //画左边 + xIndex = entryCount - 1 + for (j in entryCount - 1 downTo 0) { + dataSet.getEntryForIndex(j)?.let { entry -> + angle = if (xIndex == 0) 0f else absoluteAngles[xIndex - 1] * phaseX + val sliceAngle = drawAngles[xIndex] + val sliceSpaceMiddleAngle = sliceSpace / (Utils.FDEG2RAD * labelRadius) + + // offset needed to center the drawn text in the slice + val angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2f) / 2f + angle += angleOffset + val transformedAngle = rotationAngle + angle * phaseY + val value: Float = if (chart.isUsePercentValuesEnabled) + entry.y / yValueSum * 100f + else + entry.y + val sliceXBase = cos((transformedAngle * Utils.FDEG2RAD).toDouble()).toFloat() + val sliceYBase = sin((transformedAngle * Utils.FDEG2RAD).toDouble()).toFloat() + val drawXOutside = drawEntryLabels && + xValuePosition == ValuePosition.OUTSIDE_SLICE + val drawYOutside = drawValues && + yValuePosition == ValuePosition.OUTSIDE_SLICE + val drawXInside = drawEntryLabels && + xValuePosition == ValuePosition.INSIDE_SLICE + val drawYInside = drawValues && + yValuePosition == ValuePosition.INSIDE_SLICE + if (drawXOutside || drawYOutside) { + val valueLineLength1 = dataSet.valueLinePart1Length + val valueLineLength2 = dataSet.valueLinePart2Length + val valueLinePart1OffsetPercentage = dataSet.valueLinePart1OffsetPercentage / 100f + var pt2x: Float + var pt2y: Float + var labelPtx: Float + var labelPty: Float + val line1Radius: Float = if (chart.isDrawHoleEnabled) (radius - radius * holeRadiusPercent + * valueLinePart1OffsetPercentage) + radius * holeRadiusPercent else radius * valueLinePart1OffsetPercentage + if (dataSet.isValueLineVariableLength) labelRadius * valueLineLength2 * + abs(sin((transformedAngle * Utils.FDEG2RAD).toDouble())).toFloat() + else + labelRadius * valueLineLength2 + val pt0x = line1Radius * sliceXBase + center.x + val pt0y = line1Radius * sliceYBase + center.y + val pt1x = labelRadius * (1 + valueLineLength1) * sliceXBase + center.x + val pt1y = labelRadius * (1 + valueLineLength1) * sliceYBase + center.y + if (transformedAngle % 360.0 in 90.0..270.0) { + pt2x = center.x - radius - 5 + pt2y = if (lastPositionOfLeft == 0f) { + pt1y + } else { + if (pt1y - lastPositionOfLeft < textHeight) { + pt1y + (textHeight - (pt1y - lastPositionOfLeft)) + } else { + pt1y + } + } + lastPositionOfLeft = pt2y + paintValues.textAlign = Paint.Align.RIGHT + if (drawXOutside) paintEntryLabels.textAlign = Paint.Align.RIGHT + labelPtx = pt2x - offset + labelPty = pt2y + } else { + continue + } + if (dataSet.valueLineColor != ColorTemplate.COLOR_NONE) { + c.drawLine(pt0x, pt0y, pt1x, pt1y, valueLinePaint) + c.drawLine(pt1x, pt1y, pt2x, pt2y, valueLinePaint) + } + + // draw everything, depending on settings + if (drawXOutside && drawYOutside) { + drawValue( + c, + formatter, + value, + entry, + 0, + labelPtx, + labelPty, + dataSet.getValueTextColor(j) + ) + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(c, entry.label!!, labelPtx, labelPty + lineHeight) + } + } else if (drawXOutside) { + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(c, entry.label!!, labelPtx, labelPty + lineHeight / 2f) + } + } else if (drawYOutside) { + drawValue( + c, formatter, value, entry, 0, labelPtx, labelPty + lineHeight / 2f, dataSet + .getValueTextColor(j) + ) + } + } + if (drawXInside || drawYInside) { + // calculate the text position + val x = labelRadius * sliceXBase + center.x + val y = labelRadius * sliceYBase + center.y + paintValues.textAlign = Paint.Align.CENTER + + // draw everything, depending on settings + if (drawXInside && drawYInside) { + drawValue(c, formatter, value, entry, 0, x, y, dataSet.getValueTextColor(j)) + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(c, entry.label!!, x, y + lineHeight) + } + } else if (drawXInside) { + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(c, entry.label!!, x, y + lineHeight / 2f) + } + } else if (drawYInside) { + drawValue(c, formatter, value, entry, 0, x, y + lineHeight / 2f, dataSet.getValueTextColor(j)) + } + } + if (entry.icon != null && dataSet.isDrawIcons) { + val icon = entry.icon + val x = (labelRadius + iconsOffset.y) * sliceXBase + center.x + var y = (labelRadius + iconsOffset.y) * sliceYBase + center.y + y += iconsOffset.x + icon?.let { ic -> + c.drawImage( + ic, x.toInt(), y.toInt() + ) + } + } + } + xIndex-- + } + PointF.recycleInstance(iconsOffset) + } + } + PointF.recycleInstance(center) + c.restore() + } + + private fun drawValuesNotTopAlign(c: Canvas) { + val rect = Rect() + paintEntryLabels.getTextBounds(text, 0, text.length, rect) + val textHeight = rect.height() + val center = chart.centerCircleBox + + // get whole the radius + val radius = chart.radius + val rotationAngle = chart.rotationAngle + val drawAngles = chart.drawAngles + val absoluteAngles = chart.absoluteAngles + val phaseX = animator.phaseX + val phaseY = animator.phaseY + val holeRadiusPercent = chart.holeRadius / 100f + var labelRadiusOffset = radius / 10f * 3.6f + if (chart.isDrawHoleEnabled) { + labelRadiusOffset = radius - radius * holeRadiusPercent / 2f + } + val labelRadius = radius - labelRadiusOffset + val data = chart.getData() + val dataSets = data?.dataSets + val yValueSum = data?.yValueSum ?: 0F + val drawEntryLabels = chart.isDrawEntryLabelsEnabled + var angle: Float + var xIndex = 0 + c.save() + val offset = 5f.convertDpToPixel() + dataSets?.let { + for (i in it.indices) { + val dataSet = dataSets[i] + val drawValues = dataSet.isDrawValues + if (!drawValues && !drawEntryLabels) continue + val xValuePosition = dataSet.xValuePosition + val yValuePosition = dataSet.yValuePosition + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet) + val lineHeight = (paintValues.calcTextHeight("Q") + + 4f.convertDpToPixel()) + val formatter = dataSet.valueFormatter + val entryCount = dataSet.entryCount + valueLinePaint.color = dataSet.valueLineColor + valueLinePaint.strokeWidth = dataSet.valueLineWidth.convertDpToPixel() + val sliceSpace = getSliceSpace(dataSet) + val iconsOffset = PointF.getInstance(dataSet.iconsOffset) + iconsOffset.x = iconsOffset.x.convertDpToPixel() + iconsOffset.y = iconsOffset.y.convertDpToPixel() + var lastPositionOfLeft = 0f + var lastPositionOfRight = 0f + for (j in 0 until entryCount) { + dataSet.getEntryForIndex(j)?.let { entry -> + angle = if (xIndex == 0) 0f else absoluteAngles[xIndex - 1] * phaseX + val sliceAngle = drawAngles[xIndex] + val sliceSpaceMiddleAngle = sliceSpace / (Utils.FDEG2RAD * labelRadius) + + // offset needed to center the drawn text in the slice + val angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2f) / 2f + angle += angleOffset + val transformedAngle = rotationAngle + angle * phaseY + val value: Float = if (chart.isUsePercentValuesEnabled) + entry.y / yValueSum * 100f + else + entry.y + val sliceXBase = cos((transformedAngle * Utils.FDEG2RAD).toDouble()).toFloat() + val sliceYBase = sin((transformedAngle * Utils.FDEG2RAD).toDouble()).toFloat() + val drawXOutside = drawEntryLabels && + xValuePosition == ValuePosition.OUTSIDE_SLICE + val drawYOutside = drawValues && + yValuePosition == ValuePosition.OUTSIDE_SLICE + val drawXInside = drawEntryLabels && + xValuePosition == ValuePosition.INSIDE_SLICE + val drawYInside = drawValues && + yValuePosition == ValuePosition.INSIDE_SLICE + if (drawXOutside || drawYOutside) { + val valueLineLength1 = dataSet.valueLinePart1Length + dataSet.valueLinePart2Length + val valueLinePart1OffsetPercentage = dataSet.valueLinePart1OffsetPercentage / 100f + var pt2x: Float + var pt2y: Float + var labelPtx: Float + var labelPty: Float + val line1Radius: Float = if (chart.isDrawHoleEnabled) (radius - radius * holeRadiusPercent + * valueLinePart1OffsetPercentage) + radius * holeRadiusPercent else radius * valueLinePart1OffsetPercentage + + val pt0x = line1Radius * sliceXBase + center.x + val pt0y = line1Radius * sliceYBase + center.y + val pt1x = labelRadius * (1 + valueLineLength1) * sliceXBase + center.x + val pt1y = labelRadius * (1 + valueLineLength1) * sliceYBase + center.y + if (transformedAngle % 360.0 in 90.0..270.0) { + pt2x = center.x - radius - 5 + pt2y = if (lastPositionOfLeft == 0f) { + pt1y + } else { + if (lastPositionOfLeft - pt1y < textHeight) { + pt1y - (textHeight - (lastPositionOfLeft - pt1y)) + } else { + pt1y + } + } + lastPositionOfLeft = pt2y + paintValues.textAlign = Paint.Align.RIGHT + if (drawXOutside) paintEntryLabels.textAlign = Paint.Align.RIGHT + labelPtx = pt2x - offset + labelPty = pt2y + } else { + pt2x = center.x + radius + 5 + pt2y = if (lastPositionOfRight == 0f) { + pt1y + } else { + if (pt1y - lastPositionOfRight < textHeight) { + pt1y + (textHeight - (pt1y - lastPositionOfRight)) + } else { + pt1y + } + } + lastPositionOfRight = pt2y + paintValues.textAlign = Paint.Align.LEFT + if (drawXOutside) paintEntryLabels.textAlign = Paint.Align.LEFT + labelPtx = pt2x + offset + labelPty = pt2y + } + if (dataSet.valueLineColor != ColorTemplate.COLOR_NONE) { + c.drawLine(pt0x, pt0y, pt1x, pt1y, valueLinePaint) + c.drawLine(pt1x, pt1y, pt2x, pt2y, valueLinePaint) + } + + // draw everything, depending on settings + if (drawXOutside && drawYOutside) { + drawValue( + c, + formatter, + value, + entry, + 0, + labelPtx, + labelPty, + dataSet.getValueTextColor(j) + ) + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(c, entry.label!!, labelPtx, labelPty + lineHeight) + } + } else if (drawXOutside) { + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(c, entry.label!!, labelPtx, labelPty + lineHeight / 2f) + } + } else if (drawYOutside) { + drawValue( + c, formatter, value, entry, 0, labelPtx, labelPty + lineHeight / 2f, dataSet + .getValueTextColor(j) + ) + } + } + if (drawXInside || drawYInside) { + // calculate the text position + val x = labelRadius * sliceXBase + center.x + val y = labelRadius * sliceYBase + center.y + paintValues.textAlign = Paint.Align.CENTER + + // draw everything, depending on settings + if (drawXInside && drawYInside) { + drawValue(c, formatter, value, entry, 0, x, y, dataSet.getValueTextColor(j)) + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(c, entry.label!!, x, y + lineHeight) + } + } else if (drawXInside) { + if (j < data.entryCount && entry.label != null) { + drawEntryLabel(c, entry.label!!, x, y + lineHeight / 2f) + } + } else if (drawYInside) { + drawValue(c, formatter, value, entry, 0, x, y + lineHeight / 2f, dataSet.getValueTextColor(j)) + } + } + if (entry.icon != null && dataSet.isDrawIcons) { + val icon = entry.icon + val x = (labelRadius + iconsOffset.y) * sliceXBase + center.x + var y = (labelRadius + iconsOffset.y) * sliceYBase + center.y + y += iconsOffset.x + icon?.let { ic -> + c.drawImage( + ic, x.toInt(), y.toInt() + ) + } + } + } + xIndex++ + } + PointF.recycleInstance(iconsOffset) + } + } + PointF.recycleInstance(center) + c.restore() + } +} \ No newline at end of file