From 31f2607e69c42909c21f5c46bab58bb324582c6f Mon Sep 17 00:00:00 2001 From: Danijel Markov Date: Thu, 24 Apr 2025 14:27:29 +0200 Subject: [PATCH] Update RoundedBarChartRenderer.kt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changelog - RoundedBarChartRenderer Improvements Added: Auto Full Radius Mode Introduced a new setUseAutoFullRadius(true) function that enables automatic calculation of full corner radius for bars. When enabled, the radius dynamically adapts based on the bar’s current screen width and height, ensuring fully rounded corners regardless of zoom level or chart scale. This feature improves visual consistency, especially when zooming in/out, as the rounding remains proportional to each bar’s dimensions. Fixed: Marker Not Showing on Highlight Previously, markers were not appearing when bars were highlighted due to missing positional metadata. This has been resolved by ensuring setHighlightDrawPos(...) is called inside drawHighlighted(), which correctly updates the Highlight object with the marker's draw position. Now, the custom and other markers will be properly displayed above highlighted bars, even when using this renderer. --- .../renderer/RoundedBarChartRenderer.kt | 479 +++++++----------- 1 file changed, 197 insertions(+), 282 deletions(-) diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RoundedBarChartRenderer.kt b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RoundedBarChartRenderer.kt index 62de1edef..d7abc762e 100644 --- a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RoundedBarChartRenderer.kt +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RoundedBarChartRenderer.kt @@ -9,367 +9,282 @@ import com.github.mikephil.charting.animation.ChartAnimator import com.github.mikephil.charting.highlight.Highlight import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider import com.github.mikephil.charting.interfaces.datasets.IBarDataSet +import com.github.mikephil.charting.renderer.BarChartRenderer import com.github.mikephil.charting.utils.Utils import com.github.mikephil.charting.utils.ViewPortHandler +import kotlin.math.abs import kotlin.math.min -/** @noinspection unused - */ -class RoundedBarChartRenderer(chart: BarDataProvider, animator: ChartAnimator?, viewPortHandler: ViewPortHandler?) : - BarChartRenderer(chart, animator, viewPortHandler) { - private val mBarShadowRectBuffer = RectF() - private val mRadius = 20f - private var roundedShadowRadius = 0f +class RoundedBarChartRenderer( + chart: BarDataProvider, + animator: ChartAnimator?, + viewPortHandler: ViewPortHandler? +) : BarChartRenderer(chart, animator, viewPortHandler) { + + private val shadowRect = RectF() + private val tmpPts = FloatArray(4) + private val ovalPath = Path() + + private val defaultRadius = 20f + + private var roundedShadowRadius = 0f private var roundedPositiveDataSetRadius = 0f private var roundedNegativeDataSetRadius = 0f + private var useAutoFullRadius = false - fun setRoundedNegativeDataSetRadius(roundedNegativeDataSet: Float) { - roundedNegativeDataSetRadius = roundedNegativeDataSet + /** If true, corner radii = half the bar’s screen‐pixel width at draw‐time. */ + fun setUseAutoFullRadius(useAuto: Boolean) { + useAutoFullRadius = useAuto } - fun setRoundedShadowRadius(roundedShadow: Float) { - roundedShadowRadius = roundedShadow + fun setRoundedShadowRadius(r: Float) { + roundedShadowRadius = r } - fun setRoundedPositiveDataSetRadius(roundedPositiveDataSet: Float) { - roundedPositiveDataSetRadius = roundedPositiveDataSet + fun setRoundedPositiveDataSetRadius(r: Float) { + roundedPositiveDataSetRadius = r + } + + fun setRoundedNegativeDataSetRadius(r: Float) { + roundedNegativeDataSetRadius = r } override fun drawDataSet(c: Canvas, dataSet: IBarDataSet, index: Int) { initBuffers() - val trans = chart.getTransformer(dataSet.axisDependency) - barBorderPaint.color = dataSet.barBorderColor - barBorderPaint.strokeWidth = Utils.convertDpToPixel(dataSet.barBorderWidth) - shadowPaint.color = dataSet.barShadowColor - val drawBorder = dataSet.barBorderWidth > 0f + val trans = chart.getTransformer(dataSet.axisDependency) ?: return + val handler = viewPortHandler + val phaseX = animator.phaseX val phaseY = animator.phaseY - if (chart.isDrawBarShadowEnabled) { - shadowPaint.color = dataSet.barShadowColor - val barData = chart.barData - val barWidth = barData.barWidth - val barWidthHalf = barWidth / 2.0f - var x: Float - var i = 0 - val count = min((dataSet.entryCount.toFloat() * phaseX).toDouble().toInt().toDouble(), dataSet.entryCount.toDouble()) - while (i < count) { - val e = dataSet.getEntryForIndex(i) - x = e.x - mBarShadowRectBuffer.left = x - barWidthHalf - mBarShadowRectBuffer.right = x + barWidthHalf - trans!!.rectValueToPixel(mBarShadowRectBuffer) - if (!viewPortHandler.isInBoundsLeft(mBarShadowRectBuffer.right)) { - i++ - continue - } - if (!viewPortHandler.isInBoundsRight(mBarShadowRectBuffer.left)) { - break - } - mBarShadowRectBuffer.top = viewPortHandler.contentTop() - mBarShadowRectBuffer.bottom = viewPortHandler.contentBottom() + // 1) auto‐radius? + if (useAutoFullRadius) { + val halfVal = chart.barData.barWidth / 2f + tmpPts[0] = 0f; tmpPts[1] = 0f + tmpPts[2] = halfVal; tmpPts[3] = 0f + trans.pointValuesToPixel(tmpPts) + val pxHalf = abs(tmpPts[2] - tmpPts[0]) + roundedShadowRadius = pxHalf + roundedPositiveDataSetRadius = pxHalf + roundedNegativeDataSetRadius = pxHalf + } + // 2) prep paints + barBorderPaint.color = dataSet.barBorderColor + barBorderPaint.strokeWidth = Utils.convertDpToPixel(dataSet.barBorderWidth) + shadowPaint.color = dataSet.barShadowColor - if (roundedShadowRadius > 0) { - c.drawRoundRect(barRect, roundedShadowRadius, roundedShadowRadius, shadowPaint) - } else { - c.drawRect(mBarShadowRectBuffer, shadowPaint) + // 3) draw shadows + if (chart.isDrawBarShadowEnabled) { + val barWidth = chart.barData.barWidth + val half = barWidth / 2f + val count = min((dataSet.entryCount * phaseX).toInt(), dataSet.entryCount) + for (i in 0 until count) { + dataSet.getEntryForIndex(i)?.let { e -> + val x = e.x + shadowRect.left = x - half + shadowRect.right = x + half + trans.rectValueToPixel(shadowRect) + + if (!handler.isInBoundsLeft(shadowRect.right) || + !handler.isInBoundsRight(shadowRect.left)) return@let + + shadowRect.top = handler.contentTop() + shadowRect.bottom = handler.contentBottom() + + if (roundedShadowRadius > 0f) { + c.drawRoundRect(shadowRect, roundedShadowRadius, roundedShadowRadius, shadowPaint) + } else { + c.drawRect(shadowRect, shadowPaint) + } } - i++ } } - val buffer = barBuffers!![index]!! + // 4) feed & transform + val buffer = barBuffers[index] ?: return buffer.setPhases(phaseX, phaseY) buffer.setDataSet(index) buffer.setInverted(chart.isInverted(dataSet.axisDependency)) buffer.setBarWidth(chart.barData.barWidth) buffer.feed(dataSet) - trans!!.pointValuesToPixel(buffer.buffer) + trans.pointValuesToPixel(buffer.buffer) + + val singleColor = dataSet.colors.size == 1 - // if multiple colors has been assigned to Bar Chart - if (dataSet.colors.size > 1) { + // 5a) multi‐color bars + if (!singleColor) { var j = 0 while (j < buffer.size()) { - if (!viewPortHandler.isInBoundsLeft(buffer.buffer[j + 2])) { - j += 4 - continue - } - - if (!viewPortHandler.isInBoundsRight(buffer.buffer[j])) { - break - } - - if (chart.isDrawBarShadowEnabled) { - if (roundedShadowRadius > 0) { - c.drawRoundRect( - RectF( - buffer.buffer[j], viewPortHandler.contentTop(), - buffer.buffer[j + 2], - viewPortHandler.contentBottom() - ), roundedShadowRadius, roundedShadowRadius, shadowPaint - ) - } else { - c.drawRect( - buffer.buffer[j], viewPortHandler.contentTop(), - buffer.buffer[j + 2], - viewPortHandler.contentBottom(), shadowPaint - ) - } + val left = buffer.buffer[j] + val top = buffer.buffer[j + 1] + val right = buffer.buffer[j + 2] + val bottom = buffer.buffer[j + 3] + + if (!handler.isInBoundsLeft(right)) { j += 4; continue } + // if bar is off‐right, we're past visible, so stop + if (!handler.isInBoundsRight(left)) break + + // shadow + if (chart.isDrawBarShadowEnabled && roundedShadowRadius > 0f) { + ovalPath.reset() + ovalPath.addRoundRect( + RectF(left, handler.contentTop(), right, handler.contentBottom()), + roundedShadowRadius, roundedShadowRadius, Path.Direction.CW + ) + c.drawPath(ovalPath, shadowPaint) } - // Set the color for the currently drawn value. If the index paintRender.color = dataSet.getColor(j / 4) - - if (roundedPositiveDataSetRadius > 0) { - c.drawRoundRect( - RectF( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3] - ), roundedPositiveDataSetRadius, roundedPositiveDataSetRadius, paintRender + if (roundedPositiveDataSetRadius > 0f) { + ovalPath.reset() + ovalPath.addRoundRect( + RectF(left, top, right, bottom), + roundedPositiveDataSetRadius, + roundedPositiveDataSetRadius, + Path.Direction.CW ) + c.drawPath(ovalPath, paintRender) } else { - c.drawRect( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3], paintRender - ) + c.drawRect(left, top, right, bottom, paintRender) } j += 4 } - } else { + } + // 5b) single‐color bars + else { paintRender.color = dataSet.color - var j = 0 while (j < buffer.size()) { - if (!viewPortHandler.isInBoundsLeft(buffer.buffer[j + 2])) { - j += 4 - continue - } - - if (!viewPortHandler.isInBoundsRight(buffer.buffer[j])) { - break - } - - if (chart.isDrawBarShadowEnabled) { - if (roundedShadowRadius > 0) { - c.drawRoundRect( - RectF( - buffer.buffer[j], viewPortHandler.contentTop(), - buffer.buffer[j + 2], - viewPortHandler.contentBottom() - ), roundedShadowRadius, roundedShadowRadius, shadowPaint - ) - } else { - c.drawRect( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3], paintRender - ) - } + val left = buffer.buffer[j] + val top = buffer.buffer[j + 1] + val right = buffer.buffer[j + 2] + val bottom = buffer.buffer[j + 3] + + if (!handler.isInBoundsLeft(right)) { j += 4; continue } + if (!handler.isInBoundsRight(left)) break + + if (chart.isDrawBarShadowEnabled && roundedShadowRadius > 0f) { + ovalPath.reset() + ovalPath.addRoundRect( + RectF(left, handler.contentTop(), right, handler.contentBottom()), + roundedShadowRadius, roundedShadowRadius, Path.Direction.CW + ) + c.drawPath(ovalPath, shadowPaint) } - if (roundedPositiveDataSetRadius > 0) { - c.drawRoundRect( - RectF( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3] - ), roundedPositiveDataSetRadius, roundedPositiveDataSetRadius, paintRender + if (roundedPositiveDataSetRadius > 0f) { + ovalPath.reset() + ovalPath.addRoundRect( + RectF(left, top, right, bottom), + roundedPositiveDataSetRadius, + roundedPositiveDataSetRadius, + Path.Direction.CW ) + c.drawPath(ovalPath, paintRender) } else { - c.drawRect( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3], paintRender - ) + c.drawRect(left, top, right, bottom, paintRender) } j += 4 } } - - val isSingleColor = dataSet.colors.size == 1 - if (isSingleColor) { - paintRender.color = dataSet.getColor(index) - } - + // 6) gradient overlay var j = 0 + if (singleColor) paintRender.color = dataSet.getColor(index) while (j < buffer.size()) { - if (!viewPortHandler.isInBoundsLeft(buffer.buffer[j + 2])) { - j += 4 - continue - } - - if (!viewPortHandler.isInBoundsRight(buffer.buffer[j])) { - break - } - - if (!isSingleColor) { - paintRender.color = dataSet.getColor(j / 4) - } - - paintRender.setShader( - LinearGradient( - buffer.buffer[j], - buffer.buffer[j + 3], - buffer.buffer[j], - buffer.buffer[j + 1], - dataSet.getColor(j / 4), - dataSet.getColor(j / 4), - Shader.TileMode.MIRROR - ) + val left = buffer.buffer[j] + val top = buffer.buffer[j + 1] + val right = buffer.buffer[j + 2] + val bottom = buffer.buffer[j + 3] + + if (!handler.isInBoundsLeft(right)) { j += 4; continue } + if (!handler.isInBoundsRight(left)) break + + if (!singleColor) paintRender.color = dataSet.getColor(j / 4) + paintRender.shader = LinearGradient( + left, bottom, left, top, + paintRender.color, paintRender.color, + Shader.TileMode.MIRROR ) - paintRender.setShader( - LinearGradient( - buffer.buffer[j], - buffer.buffer[j + 3], - buffer.buffer[j], - buffer.buffer[j + 1], - dataSet.getColor(j / 4), - dataSet.getColor(j / 4), - Shader.TileMode.MIRROR + val entryY = dataSet.getEntryForIndex(j / 4)?.y ?: 0f + val radius = if (entryY < 0f) roundedNegativeDataSetRadius else roundedPositiveDataSetRadius + if (radius > 0f) { + ovalPath.reset() + ovalPath.addRoundRect( + RectF(left, top, right, bottom), + radius, radius, Path.Direction.CW ) - ) - - - if ((dataSet.getEntryForIndex(j / 4).y < 0 && roundedNegativeDataSetRadius > 0)) { - val path2 = roundRect( - RectF( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3] - ), roundedNegativeDataSetRadius, roundedNegativeDataSetRadius, true, true, true, true - ) - c.drawPath(path2, paintRender) - } else if ((dataSet.getEntryForIndex(j / 4).y > 0 && roundedPositiveDataSetRadius > 0)) { - val path2 = roundRect( - RectF( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3] - ), roundedPositiveDataSetRadius, roundedPositiveDataSetRadius, true, true, true, true - ) - c.drawPath(path2, paintRender) + c.drawPath(ovalPath, paintRender) } else { - c.drawRect( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3], paintRender - ) + c.drawRect(left, top, right, bottom, paintRender) } - j += 4 } + paintRender.shader = null } override fun drawHighlighted(c: Canvas, indices: Array) { + // 1) early exits + val handler = viewPortHandler val barData = chart.barData - for (high in indices) { - val set = barData.getDataSetByIndex(high.dataSetIndex) - - if (set == null || !set.isHighlightEnabled) { - continue - } - - val e = set.getEntryForXValue(high.x, high.y) - - if (!isInBoundsX(e, set)) { - continue - } - - val trans = chart.getTransformer(set.axisDependency) - - paintHighlight.color = set.highLightColor - paintHighlight.alpha = set.highLightAlpha - - val isStack = high.stackIndex >= 0 && e.isStacked + for (h in indices) { + // 2) only highlight enabled sets + val set = barData.getDataSetByIndex(h.dataSetIndex) ?: continue + if (!set.isHighlightEnabled) continue - val y1: Float - val y2: Float + // 3) find the matching Entry + val e = set.getEntryForXValue(h.x, h.y) ?: continue + if (!isInBoundsX(e, set)) continue - if (isStack) { + // 4) compute the y‐range of the highlight (stack vs. normal) + val isStack = h.stackIndex >= 0 && e.isStacked + val (y1, y2) = if (isStack) { if (chart.isHighlightFullBarEnabled) { - y1 = e.positiveSum - y2 = -e.negativeSum + e.positiveSum to -e.negativeSum } else { - val range = e.ranges[high.stackIndex] - - y1 = range.from - y2 = range.to + val range = e.ranges[h.stackIndex] + range.from to range.to } } else { - y1 = e.y - y2 = 0f + e.y to 0f } - prepareBarHighlight(e.x, y1, y2, barData.barWidth / 2f, trans!!) - - setHighlightDrawPos(high, barRect) - - val path2 = roundRect( - RectF( - barRect.left, barRect.top, barRect.right, - barRect.bottom - ), mRadius, mRadius, true, true, true, true + // 5) transform values to pixel‐rect + val trans = chart.getTransformer(set.axisDependency) ?: continue + prepareBarHighlight( + e.x, + y1, + y2, + barData.barWidth / 2f, + trans ) - c.drawPath(path2, paintHighlight) - } - } + // 5b) record the center/top into the Highlight object so markers can be drawn + setHighlightDrawPos(h, barRect) - private fun roundRect(rect: RectF, rx: Float, ry: Float, tl: Boolean, tr: Boolean, br: Boolean, bl: Boolean): Path { - var rx = rx - var ry = ry - val top = rect.top - val left = rect.left - val right = rect.right - val bottom = rect.bottom - val path = Path() - if (rx < 0) { - rx = 0f - } - if (ry < 0) { - ry = 0f - } - val width = right - left - val height = bottom - top - if (rx > width / 2) { - rx = width / 2 - } - if (ry > height / 2) { - ry = height / 2 - } - val widthMinusCorners = (width - (2 * rx)) - val heightMinusCorners = (height - (2 * ry)) - - path.moveTo(right, top + ry) - if (tr) { - path.rQuadTo(0f, -ry, -rx, -ry) //top-right corner - } else { - path.rLineTo(0f, -ry) - path.rLineTo(-rx, 0f) - } - path.rLineTo(-widthMinusCorners, 0f) - if (tl) { - path.rQuadTo(-rx, 0f, -rx, ry) //top-left corner - } else { - path.rLineTo(-rx, 0f) - path.rLineTo(0f, ry) - } - path.rLineTo(0f, heightMinusCorners) + // 6) clip any highlights that are fully off‐screen + if (!handler.isInBoundsLeft(barRect.right) || + !handler.isInBoundsRight(barRect.left) || + !handler.isInBoundsTop(barRect.bottom) || + !handler.isInBoundsBottom(barRect.top) + ) { + continue + } - if (bl) { - path.rQuadTo(0f, ry, rx, ry) //bottom-left corner - } else { - path.rLineTo(0f, ry) - path.rLineTo(rx, 0f) - } + // 7) choose corner radius + val radius = if (useAutoFullRadius) { + abs((barRect.right - barRect.left) / 2f) + } else { + defaultRadius + } - path.rLineTo(widthMinusCorners, 0f) - if (br) path.rQuadTo(rx, 0f, rx, -ry) //bottom-right corner - else { - path.rLineTo(rx, 0f) - path.rLineTo(0f, -ry) + // 8) draw your rounded highlight + paintHighlight.color = set.highLightColor + paintHighlight.alpha = set.highLightAlpha + c.drawRoundRect(barRect, radius, radius, paintHighlight) } - - path.rLineTo(0f, -heightMinusCorners) - path.close() - return path } }