From 9afb56ec075a07a283f868b61a23b3baf89b7558 Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Sun, 28 Dec 2025 08:44:07 +0100 Subject: [PATCH] Update RoundedBarChartRenderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 | 504 ++++++++---------- .../RoundedHorizontalBarChartRenderer.kt | 6 +- 2 files changed, 213 insertions(+), 297 deletions(-) diff --git a/chartLib/src/main/kotlin/info/appdev/charting/renderer/RoundedBarChartRenderer.kt b/chartLib/src/main/kotlin/info/appdev/charting/renderer/RoundedBarChartRenderer.kt index e81259c46..92defd38a 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/renderer/RoundedBarChartRenderer.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/renderer/RoundedBarChartRenderer.kt @@ -11,357 +11,273 @@ import info.appdev.charting.interfaces.dataprovider.BarDataProvider import info.appdev.charting.interfaces.datasets.IBarDataSet import info.appdev.charting.utils.ViewPortHandler import info.appdev.charting.utils.convertDpToPixel +import kotlin.math.abs import kotlin.math.min +@Suppress("unused") class RoundedBarChartRenderer( dataProvider: BarDataProvider, animator: ChartAnimator, viewPortHandler: ViewPortHandler ) : BarChartRenderer(dataProvider, animator, viewPortHandler) { - private val mBarShadowRectBuffer = RectF() - private val radius = 20f - var roundedShadowRadius = 0f + + private val shadowRect = RectF() + private val tmpPts = FloatArray(4) + private val ovalPath = Path() + + private val defaultRadius = 20f + + private var roundedShadowRadius = 0f var roundedPositiveDataSetRadius = 0f var roundedNegativeDataSetRadius = 0f + /** If true, corner radii = half the bar’s screen‐pixel width at draw‐time. */ + var useAutoFullRadius = false override fun drawDataSet(canvas: Canvas, dataSet: IBarDataSet, index: Int) { initBuffers() - val trans = dataProvider.getTransformer(dataSet.axisDependency) - barBorderPaint.color = dataSet.barBorderColor - barBorderPaint.strokeWidth = dataSet.barBorderWidth.convertDpToPixel() - shadowPaint.color = dataSet.barShadowColor + val transformer = dataProvider.getTransformer(dataSet.axisDependency) ?: return + val handler = viewPortHandler + val phaseX = animator.phaseX val phaseY = animator.phaseY - if (dataProvider.isDrawBarShadowEnabled) { - shadowPaint.color = dataSet.barShadowColor - dataProvider.barData?.let { 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) { - dataSet.getEntryForIndex(i)?.let { barEntry -> - x = barEntry.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() + dataProvider.barData?.let { barData -> + // 1) auto‐radius? + if (useAutoFullRadius) { + val halfVal = barData.barWidth / 2f + tmpPts[0] = 0f; tmpPts[1] = 0f + tmpPts[2] = halfVal; tmpPts[3] = 0f + transformer.pointValuesToPixel(tmpPts) + val pxHalf = abs(tmpPts[2] - tmpPts[0]) + roundedShadowRadius = pxHalf + roundedPositiveDataSetRadius = pxHalf + roundedNegativeDataSetRadius = pxHalf + } + // 2) prep paints + barBorderPaint.color = dataSet.barBorderColor + barBorderPaint.strokeWidth = dataSet.barBorderWidth.convertDpToPixel() + shadowPaint.color = dataSet.barShadowColor - if (roundedShadowRadius > 0) { - canvas.drawRoundRect(barRect, roundedShadowRadius, roundedShadowRadius, shadowPaint) - } else { - canvas.drawRect(mBarShadowRectBuffer, shadowPaint) + // 3) draw shadows + if (dataProvider.isDrawBarShadowEnabled) { + val barWidth = 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 + transformer.rectValueToPixel(shadowRect) + + if (!handler.isInBoundsLeft(shadowRect.right) || + !handler.isInBoundsRight(shadowRect.left) + ) return@let + + shadowRect.top = handler.contentTop() + shadowRect.bottom = handler.contentBottom() + + if (roundedShadowRadius > 0f) { + canvas.drawRoundRect(shadowRect, roundedShadowRadius, roundedShadowRadius, shadowPaint) + } else { + canvas.drawRect(shadowRect, shadowPaint) + } } - i++ } - } - val buffer = barBuffers[index]!! + // 4) feed & transform + val buffer = barBuffers[index] ?: return buffer.setPhases(phaseX, phaseY) buffer.setDataSet(index) buffer.inverted = dataProvider.isInverted(dataSet.axisDependency) - dataProvider.barData?.let { buffer.barWidth = it.barWidth } + buffer.barWidth = barData.barWidth buffer.feed(dataSet) - trans!!.pointValuesToPixel(buffer.buffer) - - // if multiple colors has been assigned to Bar Chart - dataSet.colors.let { - if (it.size > 1) { - var j = 0 - while (j < buffer.size()) { - if (!viewPortHandler.isInBoundsLeft(buffer.buffer[j + 2])) { - j += 4 - continue - } - - if (!viewPortHandler.isInBoundsRight(buffer.buffer[j])) { - break - } + transformer.pointValuesToPixel(buffer.buffer) - if (dataProvider.isDrawBarShadowEnabled) { - if (roundedShadowRadius > 0) { - canvas.drawRoundRect( - RectF( - buffer.buffer[j], viewPortHandler.contentTop(), - buffer.buffer[j + 2], - viewPortHandler.contentBottom() - ), roundedShadowRadius, roundedShadowRadius, shadowPaint - ) - } else { - canvas.drawRect( - buffer.buffer[j], viewPortHandler.contentTop(), - buffer.buffer[j + 2], - viewPortHandler.contentBottom(), shadowPaint - ) - } - } + val singleColor = dataSet.colors.size == 1 - // Set the color for the currently drawn value. If the index - paintRender.color = dataSet.getColorByIndex(j / 4) + // 5a) multi‐color bars + if (!singleColor) { + var j = 0 + while (j < buffer.size()) { + val left = buffer.buffer[j] + val top = buffer.buffer[j + 1] + val right = buffer.buffer[j + 2] + val bottom = buffer.buffer[j + 3] - if (roundedPositiveDataSetRadius > 0) { - canvas.drawRoundRect( - RectF( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3] - ), roundedPositiveDataSetRadius, roundedPositiveDataSetRadius, paintRender - ) - } else { - canvas.drawRect( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3], paintRender - ) - } - j += 4 + 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 (dataProvider.isDrawBarShadowEnabled && roundedShadowRadius > 0f) { + ovalPath.reset() + ovalPath.addRoundRect( + RectF(left, handler.contentTop(), right, handler.contentBottom()), + roundedShadowRadius, roundedShadowRadius, Path.Direction.CW + ) + canvas.drawPath(ovalPath, shadowPaint) } - } 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 (dataProvider.isDrawBarShadowEnabled) { - if (roundedShadowRadius > 0) { - canvas.drawRoundRect( - RectF( - buffer.buffer[j], viewPortHandler.contentTop(), - buffer.buffer[j + 2], - viewPortHandler.contentBottom() - ), roundedShadowRadius, roundedShadowRadius, shadowPaint - ) - } else { - canvas.drawRect( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3], paintRender - ) - } - } - if (roundedPositiveDataSetRadius > 0) { - canvas.drawRoundRect( - RectF( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3] - ), roundedPositiveDataSetRadius, roundedPositiveDataSetRadius, paintRender - ) - } else { - canvas.drawRect( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3], paintRender - ) - } - j += 4 + paintRender.color = dataSet.getColorByIndex(j / 4) + if (roundedPositiveDataSetRadius > 0f) { + ovalPath.reset() + ovalPath.addRoundRect( + RectF(left, top, right, bottom), + roundedPositiveDataSetRadius, + roundedPositiveDataSetRadius, + Path.Direction.CW + ) + canvas.drawPath(ovalPath, paintRender) + } else { + canvas.drawRect(left, top, right, bottom, paintRender) } + j += 4 } } + // 5b) single‐color bars + else { + paintRender.color = dataSet.color + var j = 0 + while (j < buffer.size()) { + 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 - val isSingleColor = dataSet.colors.size == 1 - if (isSingleColor) { - paintRender.color = dataSet.getColorByIndex(index) - } + if (dataProvider.isDrawBarShadowEnabled && roundedShadowRadius > 0f) { + ovalPath.reset() + ovalPath.addRoundRect( + RectF(left, handler.contentTop(), right, handler.contentBottom()), + roundedShadowRadius, roundedShadowRadius, Path.Direction.CW + ) + canvas.drawPath(ovalPath, shadowPaint) + } - var j = 0 - while (j < buffer.size()) { - if (!viewPortHandler.isInBoundsLeft(buffer.buffer[j + 2])) { + if (roundedPositiveDataSetRadius > 0f) { + ovalPath.reset() + ovalPath.addRoundRect( + RectF(left, top, right, bottom), + roundedPositiveDataSetRadius, + roundedPositiveDataSetRadius, + Path.Direction.CW + ) + canvas.drawPath(ovalPath, paintRender) + } else { + canvas.drawRect(left, top, right, bottom, paintRender) + } j += 4 - continue } + } - if (!viewPortHandler.isInBoundsRight(buffer.buffer[j])) { - break - } + // 6) gradient overlay + var j = 0 + if (singleColor) paintRender.color = dataSet.getColorByIndex(index) + while (j < buffer.size()) { + val left = buffer.buffer[j] + val top = buffer.buffer[j + 1] + val right = buffer.buffer[j + 2] + val bottom = buffer.buffer[j + 3] - if (!isSingleColor) { - paintRender.color = dataSet.getColorByIndex(j / 4) + if (!handler.isInBoundsLeft(right)) { + j += 4; continue } + if (!handler.isInBoundsRight(left)) break + if (!singleColor) paintRender.color = dataSet.getColorByIndex(j / 4) paintRender.shader = LinearGradient( - buffer.buffer[j], - buffer.buffer[j + 3], - buffer.buffer[j], - buffer.buffer[j + 1], - dataSet.getColorByIndex(j / 4), - dataSet.getColorByIndex(j / 4), + left, bottom, left, top, + paintRender.color, paintRender.color, Shader.TileMode.MIRROR ) - paintRender.shader = LinearGradient( - buffer.buffer[j], - buffer.buffer[j + 3], - buffer.buffer[j], - buffer.buffer[j + 1], - dataSet.getColorByIndex(j / 4), - dataSet.getColorByIndex(j / 4), - Shader.TileMode.MIRROR - ) - - dataSet.getEntryForIndex(j / 4)?.let { barEntry -> - - if ((barEntry.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, tl = true, tr = true, br = true, bl = true - ) - canvas.drawPath(path2, paintRender) - } else if ((barEntry.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, tl = true, tr = true, br = true, bl = true - ) - canvas.drawPath(path2, paintRender) - } else { - canvas.drawRect( - buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3], paintRender - ) - } + 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 + ) + canvas.drawPath(ovalPath, paintRender) + } else { + canvas.drawRect(left, top, right, bottom, paintRender) } j += 4 } } + paintRender.shader = null } override fun drawHighlighted(canvas: Canvas, indices: Array) { - dataProvider.barData?.let { barData -> - - for (high in indices) { - val set = barData.getDataSetByIndex(high.dataSetIndex) - - if (set == null || !set.isHighlightEnabled) - continue - - - set.getEntryForXValue(high.x, high.y)?.let { barEntry -> - - if (!isInBoundsX(barEntry, set)) { - continue - } - - val trans = dataProvider.getTransformer(set.axisDependency) - - paintHighlight.color = set.highLightColor - paintHighlight.alpha = set.highLightAlpha - - val isStack = high.stackIndex >= 0 && barEntry.isStacked - - val y1: Float - val y2: Float - - if (isStack) { - if (dataProvider.isHighlightFullBarEnabled) { - y1 = barEntry.positiveSum - y2 = -barEntry.negativeSum - } else { - val range = barEntry.ranges[high.stackIndex] - - y1 = range.from - y2 = range.to - } - } else { - y1 = barEntry.y - y2 = 0f - } - - prepareBarHighlight(barEntry.x, y1, y2, barData.barWidth / 2f, trans!!) - - setHighlightDrawPos(high, barRect) - - val path2 = roundRect( - RectF( - barRect.left, barRect.top, barRect.right, - barRect.bottom - ), radius, radius, tl = true, tr = true, br = true, bl = true - ) - - canvas.drawPath(path2, paintHighlight) + // 1) early exits + val handler = viewPortHandler + val barData = dataProvider.barData + + for (h in indices) { + // 2) only highlight enabled sets + val set = barData?.getDataSetByIndex(h.dataSetIndex) ?: continue + if (!set.isHighlightEnabled) continue + + // 3) find the matching Entry + val e = set.getEntryForXValue(h.x, h.y) ?: continue + if (!isInBoundsX(e, set)) continue + + // 4) compute the y‐range of the highlight (stack vs. normal) + val isStack = h.stackIndex >= 0 && e.isStacked + val (y1, y2) = if (isStack) { + if (dataProvider.isHighlightFullBarEnabled) { + e.positiveSum to -e.negativeSum + } else { + val range = e.ranges[h.stackIndex] + range.from to range.to } + } else { + e.y to 0f } - } - } - @Suppress("SameParameterValue") - 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) + // 5) transform values to pixel‐rect + val trans = dataProvider.getTransformer(set.axisDependency) ?: continue + prepareBarHighlight( + e.x, + y1, + y2, + barData.barWidth / 2f, + trans + ) + + // 5b) record the center/top into the Highlight object so markers can be drawn + setHighlightDrawPos(h, barRect) + + // 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 + canvas.drawRoundRect(barRect, radius, radius, paintHighlight) } - - path.rLineTo(0f, -heightMinusCorners) - path.close() - return path } } diff --git a/chartLib/src/main/kotlin/info/appdev/charting/renderer/RoundedHorizontalBarChartRenderer.kt b/chartLib/src/main/kotlin/info/appdev/charting/renderer/RoundedHorizontalBarChartRenderer.kt index c3a2ed380..5a7433522 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/renderer/RoundedHorizontalBarChartRenderer.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/renderer/RoundedHorizontalBarChartRenderer.kt @@ -27,7 +27,7 @@ class RoundedHorizontalBarChartRenderer( override fun drawDataSet(canvas: Canvas, dataSet: IBarDataSet, index: Int) { initBuffers() - val trans = dataProvider.getTransformer(dataSet.axisDependency) + val transformer = dataProvider.getTransformer(dataSet.axisDependency) barBorderPaint.color = dataSet.barBorderColor barBorderPaint.strokeWidth = dataSet.barBorderWidth.convertDpToPixel() shadowPaint.color = dataSet.barShadowColor @@ -48,7 +48,7 @@ class RoundedHorizontalBarChartRenderer( mBarShadowRectBuffer.top = x - barWidthHalf mBarShadowRectBuffer.bottom = x + barWidthHalf } - trans!!.rectValueToPixel(mBarShadowRectBuffer) + transformer!!.rectValueToPixel(mBarShadowRectBuffer) if (!viewPortHandler.isInBoundsTop(mBarShadowRectBuffer.bottom)) { i++ continue @@ -74,7 +74,7 @@ class RoundedHorizontalBarChartRenderer( buffer.inverted = dataProvider.isInverted(dataSet.axisDependency) dataProvider.barData?.let { buffer.barWidth = it.barWidth } buffer.feed(dataSet) - trans!!.pointValuesToPixel(buffer.buffer) + transformer!!.pointValuesToPixel(buffer.buffer) // if multiple colors has been assigned to Bar Chart if (dataSet.colors.size > 1) {