From 567b7cb76fcb780dbc97593238c3869e154dc339 Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Sun, 6 Apr 2025 08:51:14 +0200 Subject: [PATCH] Some renderer in Kotlin --- .../charting/renderer/BarChartRenderer.java | 553 ----------- .../charting/renderer/BarChartRenderer.kt | 560 +++++++++++ .../renderer/CombinedChartRenderer.java | 5 +- .../renderer/HorizontalBarChartRenderer.java | 50 +- .../charting/renderer/LineChartRenderer.java | 881 ------------------ .../charting/renderer/LineChartRenderer.kt | 812 ++++++++++++++++ .../renderer/RoundedBarChartRenderer.java | 46 +- .../RoundedHorizontalBarChartRenderer.java | 34 +- 8 files changed, 1439 insertions(+), 1502 deletions(-) delete mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.kt delete mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.java create mode 100644 MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.kt diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.java deleted file mode 100644 index 0c158bd999..0000000000 --- a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.java +++ /dev/null @@ -1,553 +0,0 @@ - -package com.github.mikephil.charting.renderer; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.RectF; -import android.graphics.drawable.Drawable; - -import com.github.mikephil.charting.animation.ChartAnimator; -import com.github.mikephil.charting.buffer.BarBuffer; -import com.github.mikephil.charting.data.BarData; -import com.github.mikephil.charting.data.BarEntry; -import com.github.mikephil.charting.highlight.Highlight; -import com.github.mikephil.charting.highlight.Range; -import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider; -import com.github.mikephil.charting.interfaces.datasets.IBarDataSet; -import com.github.mikephil.charting.utils.Fill; -import com.github.mikephil.charting.utils.MPPointF; -import com.github.mikephil.charting.utils.Transformer; -import com.github.mikephil.charting.utils.Utils; -import com.github.mikephil.charting.utils.ViewPortHandler; - -import java.util.List; - -public class BarChartRenderer extends BarLineScatterCandleBubbleRenderer { - - protected BarDataProvider mChart; - - /** - * the rect object that is used for drawing the bars - */ - protected RectF mBarRect = new RectF(); - - protected BarBuffer[] mBarBuffers; - - protected Paint mShadowPaint; - protected Paint mBarBorderPaint; - - /** - * if set to true, the bar chart's bars would be round on all corners instead of rectangular - */ - private boolean mDrawRoundedBars; - - /** - * the radius of the rounded bar chart bars - */ - private float mRoundedBarRadius = 0f; - - public BarChartRenderer(BarDataProvider chart, ChartAnimator animator, - ViewPortHandler viewPortHandler) { - super(animator, viewPortHandler); - this.mChart = chart; - - mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mHighlightPaint.setStyle(Paint.Style.FILL); - mHighlightPaint.setColor(Color.rgb(0, 0, 0)); - // set alpha after color - mHighlightPaint.setAlpha(120); - - mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mShadowPaint.setStyle(Paint.Style.FILL); - - mBarBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mBarBorderPaint.setStyle(Paint.Style.STROKE); - } - - public BarChartRenderer(BarDataProvider chart, ChartAnimator animator, - ViewPortHandler viewPortHandler, boolean mDrawRoundedBars, float mRoundedBarRadius) { - this(chart, animator, viewPortHandler); - this.mDrawRoundedBars = mDrawRoundedBars; - this.mRoundedBarRadius = mRoundedBarRadius; - } - - @Override - public void initBuffers() { - - BarData barData = mChart.getBarData(); - mBarBuffers = new BarBuffer[barData.getDataSetCount()]; - - for (int i = 0; i < mBarBuffers.length; i++) { - IBarDataSet set = barData.getDataSetByIndex(i); - mBarBuffers[i] = new BarBuffer(set.getEntryCount() * 4 * (set.isStacked() ? set.getStackSize() : 1), - barData.getDataSetCount(), set.isStacked()); - } - } - - @Override - public void drawData(Canvas c) { - - if (mBarBuffers == null) { - initBuffers(); - } - - BarData barData = mChart.getBarData(); - - for (int i = 0; i < barData.getDataSetCount(); i++) { - - IBarDataSet set = barData.getDataSetByIndex(i); - - if (set.isVisible()) { - drawDataSet(c, set, i); - } - } - } - - private RectF mBarShadowRectBuffer = new RectF(); - - protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { - - Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); - - mBarBorderPaint.setColor(dataSet.getBarBorderColor()); - mBarBorderPaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getBarBorderWidth())); - - final boolean drawBorder = dataSet.getBarBorderWidth() > 0.f; - - float phaseX = mAnimator.getPhaseX(); - float phaseY = mAnimator.getPhaseY(); - - // draw the bar shadow before the values - if (mChart.isDrawBarShadowEnabled()) { - mShadowPaint.setColor(dataSet.getBarShadowColor()); - - BarData barData = mChart.getBarData(); - - final float barWidth = barData.getBarWidth(); - final float barWidthHalf = barWidth / 2.0f; - float x; - - for (int i = 0, count = Math.min((int) (Math.ceil((float) (dataSet.getEntryCount()) * phaseX)), dataSet.getEntryCount()); - i < count; - i++) { - - BarEntry e = dataSet.getEntryForIndex(i); - - x = e.getX(); - - mBarShadowRectBuffer.left = x - barWidthHalf; - mBarShadowRectBuffer.right = x + barWidthHalf; - - trans.rectValueToPixel(mBarShadowRectBuffer); - - if (!mViewPortHandler.isInBoundsLeft(mBarShadowRectBuffer.right)) { - continue; - } - - if (!mViewPortHandler.isInBoundsRight(mBarShadowRectBuffer.left)) { - break; - } - - mBarShadowRectBuffer.top = mViewPortHandler.contentTop(); - mBarShadowRectBuffer.bottom = mViewPortHandler.contentBottom(); - - if (mDrawRoundedBars) { - c.drawRoundRect(mBarShadowRectBuffer, mRoundedBarRadius, mRoundedBarRadius, mShadowPaint); - } else { - c.drawRect(mBarShadowRectBuffer, mShadowPaint); - } - } - } - - // initialize the buffer - BarBuffer buffer = mBarBuffers[index]; - buffer.setPhases(phaseX, phaseY); - buffer.setDataSet(index); - buffer.setInverted(mChart.isInverted(dataSet.getAxisDependency())); - buffer.setBarWidth(mChart.getBarData().getBarWidth()); - - buffer.feed(dataSet); - - trans.pointValuesToPixel(buffer.buffer); - - final boolean isCustomFill = dataSet.getFills() != null && !dataSet.getFills().isEmpty(); - final boolean isSingleColor = dataSet.getColors().size() == 1; - final boolean isInverted = mChart.isInverted(dataSet.getAxisDependency()); - - if (isSingleColor) { - mRenderPaint.setColor(dataSet.getColor()); - } - - for (int j = 0, pos = 0; j < buffer.size(); j += 4, pos++) { - - if (!mViewPortHandler.isInBoundsLeft(buffer.buffer[j + 2])) { - continue; - } - - if (!mViewPortHandler.isInBoundsRight(buffer.buffer[j])) { - break; - } - - if (!isSingleColor) { - // Set the color for the currently drawn value. If the index - // is out of bounds, reuse colors. - mRenderPaint.setColor(dataSet.getColor(pos)); - } - - if (isCustomFill) { - dataSet.getFill(pos) - .fillRect( - c, mRenderPaint, - buffer.buffer[j], - buffer.buffer[j + 1], - buffer.buffer[j + 2], - buffer.buffer[j + 3], - isInverted ? Fill.Direction.DOWN : Fill.Direction.UP, - mRoundedBarRadius); - } else { - if (mDrawRoundedBars) { - c.drawRoundRect(new RectF(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3]), mRoundedBarRadius, mRoundedBarRadius, mRenderPaint); - } else { - c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3], mRenderPaint); - } - } - - if (drawBorder) { - if (mDrawRoundedBars) { - c.drawRoundRect(new RectF(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3]), mRoundedBarRadius, mRoundedBarRadius, mBarBorderPaint); - } else { - c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3], mBarBorderPaint); - } - } - } - } - - protected void prepareBarHighlight(float x, float y1, float y2, float barWidthHalf, Transformer trans) { - - float left = x - barWidthHalf; - float right = x + barWidthHalf; - float top = y1; - float bottom = y2; - - mBarRect.set(left, top, right, bottom); - - trans.rectToPixelPhase(mBarRect, mAnimator.getPhaseY()); - } - - @Override - public void drawValues(Canvas c) { - - // if values are drawn - if (isDrawingValuesAllowed(mChart)) { - - List dataSets = mChart.getBarData().getDataSets(); - - final float valueOffsetPlus = Utils.convertDpToPixel(4.5f); - float posOffset = 0f; - float negOffset = 0f; - boolean drawValueAboveBar = mChart.isDrawValueAboveBarEnabled(); - - for (int i = 0; i < mChart.getBarData().getDataSetCount(); i++) { - - IBarDataSet dataSet = dataSets.get(i); - if (dataSet.getEntryCount() == 0) { - continue; - } - if (!shouldDrawValues(dataSet)) { - continue; - } - - // apply the text-styling defined by the DataSet - applyValueTextStyle(dataSet); - - boolean isInverted = mChart.isInverted(dataSet.getAxisDependency()); - - // calculate the correct offset depending on the draw position of - // the value - float valueTextHeight = Utils.calcTextHeight(mValuePaint, "8"); - posOffset = (drawValueAboveBar ? -valueOffsetPlus : valueTextHeight + valueOffsetPlus); - negOffset = (drawValueAboveBar ? valueTextHeight + valueOffsetPlus : -valueOffsetPlus); - - if (isInverted) { - posOffset = -posOffset - valueTextHeight; - negOffset = -negOffset - valueTextHeight; - } - - // get the buffer - BarBuffer buffer = mBarBuffers[i]; - - final float phaseY = mAnimator.getPhaseY(); - - MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); - iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); - iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); - - // if only single values are drawn (sum) - if (!dataSet.isStacked()) { - - for (int j = 0; j < buffer.buffer.length * mAnimator.getPhaseX(); j += 4) { - - float x = (buffer.buffer[j] + buffer.buffer[j + 2]) / 2f; - - if (!mViewPortHandler.isInBoundsRight(x)) { - break; - } - - if (!mViewPortHandler.isInBoundsY(buffer.buffer[j + 1]) - || !mViewPortHandler.isInBoundsLeft(x)) { - continue; - } - - BarEntry entry = dataSet.getEntryForIndex(j / 4); - float val = entry.getY(); - - if (dataSet.isDrawValuesEnabled()) { - drawValue(c, dataSet.getValueFormatter(), val, entry, i, x, - val >= 0 ? - (buffer.buffer[j + 1] + posOffset) : - (buffer.buffer[j + 3] + negOffset), - dataSet.getValueTextColor(j / 4)); - } - - if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { - - Drawable icon = entry.getIcon(); - - float px = x; - float py = val >= 0 ? - (buffer.buffer[j + 1] + posOffset) : - (buffer.buffer[j + 3] + negOffset); - - px += iconsOffset.x; - py += iconsOffset.y; - - Utils.drawImage( - c, - icon, - (int) px, - (int) py, - icon.getIntrinsicWidth(), - icon.getIntrinsicHeight()); - } - } - - // if we have stacks - } else { - - Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); - - int bufferIndex = 0; - int index = 0; - - while (index < dataSet.getEntryCount() * mAnimator.getPhaseX()) { - - BarEntry entry = dataSet.getEntryForIndex(index); - - float[] vals = entry.getYVals(); - float x = (buffer.buffer[bufferIndex] + buffer.buffer[bufferIndex + 2]) / 2f; - - int color = dataSet.getValueTextColor(index); - - // we still draw stacked bars, but there is one - // non-stacked - // in between - if (vals == null) { - - if (!mViewPortHandler.isInBoundsRight(x)) { - break; - } - - if (!mViewPortHandler.isInBoundsY(buffer.buffer[bufferIndex + 1]) - || !mViewPortHandler.isInBoundsLeft(x)) { - continue; - } - - if (dataSet.isDrawValuesEnabled()) { - drawValue(c, dataSet.getValueFormatter(), entry.getY(), entry, i, x, - buffer.buffer[bufferIndex + 1] + - (entry.getY() >= 0 ? posOffset : negOffset), - color); - } - - if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { - - Drawable icon = entry.getIcon(); - - float px = x; - float py = buffer.buffer[bufferIndex + 1] + - (entry.getY() >= 0 ? posOffset : negOffset); - - px += iconsOffset.x; - py += iconsOffset.y; - - Utils.drawImage( - c, - icon, - (int) px, - (int) py, - icon.getIntrinsicWidth(), - icon.getIntrinsicHeight()); - } - - // draw stack values - } else { - - float[] transformed = new float[vals.length * 2]; - - float posY = 0f; - float negY = -entry.getNegativeSum(); - - for (int k = 0, idx = 0; k < transformed.length; k += 2, idx++) { - - float value = vals[idx]; - float y; - - if (value == 0.0f && (posY == 0.0f || negY == 0.0f)) { - // Take care of the situation of a 0.0 value, which overlaps a non-zero bar - y = value; - } else if (value >= 0.0f) { - posY += value; - y = posY; - } else { - y = negY; - negY -= value; - } - - transformed[k + 1] = y * phaseY; - } - - trans.pointValuesToPixel(transformed); - - for (int k = 0; k < transformed.length; k += 2) { - - final float val = vals[k / 2]; - final boolean drawBelow = - (val == 0.0f && negY == 0.0f && posY > 0.0f) || - val < 0.0f; - float y = transformed[k + 1] - + (drawBelow ? negOffset : posOffset); - - if (!mViewPortHandler.isInBoundsRight(x)) { - break; - } - - if (!mViewPortHandler.isInBoundsY(y) - || !mViewPortHandler.isInBoundsLeft(x)) { - continue; - } - - if (dataSet.isDrawValuesEnabled()) { - drawValue(c, - dataSet.getValueFormatter(), - vals[k / 2], - entry, - i, - x, - y, - color); - } - - if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { - - Drawable icon = entry.getIcon(); - - Utils.drawImage( - c, - icon, - (int) (x + iconsOffset.x), - (int) (y + iconsOffset.y), - icon.getIntrinsicWidth(), - icon.getIntrinsicHeight()); - } - } - } - - bufferIndex = vals == null ? bufferIndex + 4 : bufferIndex + 4 * vals.length; - index++; - } - } - - MPPointF.recycleInstance(iconsOffset); - } - } - } - - @Override - public void drawHighlighted(Canvas c, Highlight[] indices) { - - BarData barData = mChart.getBarData(); - - for (Highlight high : indices) { - - IBarDataSet set = barData.getDataSetByIndex(high.getDataSetIndex()); - - if (set == null || !set.isHighlightEnabled()) { - continue; - } - - BarEntry e = set.getEntryForXValue(high.getX(), high.getY()); - - if (!isInBoundsX(e, set)) { - continue; - } - - Transformer trans = mChart.getTransformer(set.getAxisDependency()); - - mHighlightPaint.setColor(set.getHighLightColor()); - mHighlightPaint.setAlpha(set.getHighLightAlpha()); - - boolean isStack = (high.getStackIndex() >= 0 && e.isStacked()) ? true : false; - - final float y1; - final float y2; - - if (isStack) { - - if (mChart.isHighlightFullBarEnabled()) { - - y1 = e.getPositiveSum(); - y2 = -e.getNegativeSum(); - - } else { - - Range range = e.getRanges()[high.getStackIndex()]; - - y1 = range.from; - y2 = range.to; - } - - } else { - y1 = e.getY(); - y2 = 0.f; - } - - prepareBarHighlight(e.getX(), y1, y2, barData.getBarWidth() / 2f, trans); - - setHighlightDrawPos(high, mBarRect); - - if (mDrawRoundedBars) { - c.drawRoundRect(new RectF(mBarRect), mRoundedBarRadius, mRoundedBarRadius, mHighlightPaint); - } else { - c.drawRect(mBarRect, mHighlightPaint); - } - } - } - - /** - * Sets the drawing position of the highlight object based on the riven bar-rect. - * - * @param high - */ - protected void setHighlightDrawPos(Highlight high, RectF bar) { - high.setDraw(bar.centerX(), bar.top); - } - - @Override - public void drawExtras(Canvas c) { - } -} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.kt b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.kt new file mode 100644 index 0000000000..6dd78ea8c0 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/BarChartRenderer.kt @@ -0,0 +1,560 @@ +package com.github.mikephil.charting.renderer + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import com.github.mikephil.charting.animation.ChartAnimator +import com.github.mikephil.charting.buffer.BarBuffer +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.utils.Fill +import com.github.mikephil.charting.utils.MPPointF +import com.github.mikephil.charting.utils.Transformer +import com.github.mikephil.charting.utils.Utils +import com.github.mikephil.charting.utils.ViewPortHandler +import kotlin.math.ceil +import kotlin.math.min + +open class BarChartRenderer( + @JvmField var chart: BarDataProvider, animator: ChartAnimator?, + viewPortHandler: ViewPortHandler? +) : BarLineScatterCandleBubbleRenderer(animator, viewPortHandler) { + /** + * the rect object that is used for drawing the bars + */ + @JvmField + protected var barRect: RectF = RectF() + + @JvmField + protected var barBuffers: Array? = null + + @JvmField + protected var shadowPaint: Paint + @JvmField + protected var barBorderPaint: Paint + + /** + * if set to true, the bar chart's bars would be round on all corners instead of rectangular + */ + private var mDrawRoundedBars = false + + /** + * the radius of the rounded bar chart bars + */ + private var mRoundedBarRadius = 0f + + constructor( + chart: BarDataProvider, animator: ChartAnimator?, + viewPortHandler: ViewPortHandler?, mDrawRoundedBars: Boolean, mRoundedBarRadius: Float + ) : this(chart, animator, viewPortHandler) { + this.mDrawRoundedBars = mDrawRoundedBars + this.mRoundedBarRadius = mRoundedBarRadius + } + + override fun initBuffers() { + val barData = chart.barData + barBuffers = arrayOfNulls(barData.dataSetCount) + + for (i in barBuffers!!.indices) { + val set = barData.getDataSetByIndex(i) + barBuffers!![i] = BarBuffer( + set.entryCount * 4 * (if (set.isStacked) set.stackSize else 1), + barData.dataSetCount, set.isStacked + ) + } + } + + override fun drawData(c: Canvas) { + if (barBuffers == null) { + initBuffers() + } + + val barData = chart.barData + + for (i in 0.. 0f + + val phaseX = mAnimator.phaseX + val phaseY = mAnimator.phaseY + + // draw the bar shadow before the values + 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((ceil(((dataSet.entryCount).toFloat() * phaseX).toDouble())).toInt().toDouble(), dataSet.entryCount.toDouble()).toInt() + while (i < count + ) { + val e = dataSet.getEntryForIndex(i) + + x = e.x + + mBarShadowRectBuffer.left = x - barWidthHalf + mBarShadowRectBuffer.right = x + barWidthHalf + + trans!!.rectValueToPixel(mBarShadowRectBuffer) + + if (!mViewPortHandler.isInBoundsLeft(mBarShadowRectBuffer.right)) { + i++ + continue + } + + if (!mViewPortHandler.isInBoundsRight(mBarShadowRectBuffer.left)) { + break + } + + mBarShadowRectBuffer.top = mViewPortHandler.contentTop() + mBarShadowRectBuffer.bottom = mViewPortHandler.contentBottom() + + if (mDrawRoundedBars) { + c.drawRoundRect(mBarShadowRectBuffer, mRoundedBarRadius, mRoundedBarRadius, shadowPaint) + } else { + c.drawRect(mBarShadowRectBuffer, shadowPaint) + } + i++ + } + } + + // initialize the buffer + val buffer = barBuffers!![index] + 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) + + val isCustomFill = dataSet.fills != null && dataSet.fills.isNotEmpty() + val isSingleColor = dataSet.colors.size == 1 + val isInverted = chart.isInverted(dataSet.axisDependency) + + if (isSingleColor) { + mRenderPaint.color = dataSet.color + } + + var j = 0 + var pos = 0 + while (j < buffer.size()) { + if (!mViewPortHandler.isInBoundsLeft(buffer.buffer[j + 2])) { + j += 4 + pos++ + continue + } + + if (!mViewPortHandler.isInBoundsRight(buffer.buffer[j])) { + break + } + + if (!isSingleColor) { + // Set the color for the currently drawn value. If the index + // is out of bounds, reuse colors. + mRenderPaint.color = dataSet.getColor(pos) + } + + if (isCustomFill) { + dataSet.getFill(pos) + .fillRect( + c, mRenderPaint, + buffer.buffer[j], + buffer.buffer[j + 1], + buffer.buffer[j + 2], + buffer.buffer[j + 3], + if (isInverted) Fill.Direction.DOWN else Fill.Direction.UP, + mRoundedBarRadius + ) + } else { + if (mDrawRoundedBars) { + c.drawRoundRect( + RectF( + buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], + buffer.buffer[j + 3] + ), mRoundedBarRadius, mRoundedBarRadius, mRenderPaint + ) + } else { + c.drawRect( + buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], + buffer.buffer[j + 3], mRenderPaint + ) + } + } + + if (drawBorder) { + if (mDrawRoundedBars) { + c.drawRoundRect( + RectF( + buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], + buffer.buffer[j + 3] + ), mRoundedBarRadius, mRoundedBarRadius, barBorderPaint + ) + } else { + c.drawRect( + buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], + buffer.buffer[j + 3], barBorderPaint + ) + } + } + j += 4 + pos++ + } + } + + protected open fun prepareBarHighlight(x: Float, y1: Float, y2: Float, barWidthHalf: Float, trans: Transformer) { + val left = x - barWidthHalf + val right = x + barWidthHalf + val top = y1 + val bottom = y2 + + barRect[left, top, right] = bottom + + trans.rectToPixelPhase(barRect, mAnimator.phaseY) + } + + override fun drawValues(c: Canvas) { + // if values are drawn + + if (isDrawingValuesAllowed(chart)) { + val dataSets = chart.barData.dataSets + + val valueOffsetPlus = Utils.convertDpToPixel(4.5f) + var posOffset: Float + var negOffset: Float + val drawValueAboveBar = chart.isDrawValueAboveBarEnabled + + for (i in 0..= 0) (buffer.buffer[j + 1] + posOffset) else (buffer.buffer[j + 3] + negOffset), + dataSet.getValueTextColor(j / 4) + ) + } + + if (entry.icon != null && dataSet.isDrawIconsEnabled) { + val icon = entry.icon + + var px = x + var py = if (`val` >= 0) (buffer.buffer[j + 1] + posOffset) else (buffer.buffer[j + 3] + negOffset) + + px += iconsOffset.x + py += iconsOffset.y + + Utils.drawImage( + c, + icon, + px.toInt(), + py.toInt(), + icon!!.intrinsicWidth, + icon.intrinsicHeight + ) + } + j += 4 + } + + // if we have stacks + } else { + val trans = chart.getTransformer(dataSet.axisDependency) + + var bufferIndex = 0 + var index = 0 + + while (index < dataSet.entryCount * mAnimator.phaseX) { + val entry = dataSet.getEntryForIndex(index) + + val vals = entry.yVals + val x = (buffer!!.buffer[bufferIndex] + buffer.buffer[bufferIndex + 2]) / 2f + + val color = dataSet.getValueTextColor(index) + + // we still draw stacked bars, but there is one + // non-stacked + // in between + if (vals == null) { + if (!mViewPortHandler.isInBoundsRight(x)) { + break + } + + if (!mViewPortHandler.isInBoundsY(buffer.buffer[bufferIndex + 1]) + || !mViewPortHandler.isInBoundsLeft(x) + ) { + continue + } + + if (dataSet.isDrawValuesEnabled) { + drawValue( + c, dataSet.valueFormatter, entry.y, entry, i, x, + buffer.buffer[bufferIndex + 1] + + (if (entry.y >= 0) posOffset else negOffset), + color + ) + } + + if (entry.icon != null && dataSet.isDrawIconsEnabled) { + val icon = entry.icon + + var px = x + var py = buffer.buffer[bufferIndex + 1] + + (if (entry.y >= 0) posOffset else negOffset) + + px += iconsOffset.x + py += iconsOffset.y + + Utils.drawImage( + c, + icon, + px.toInt(), + py.toInt(), + icon!!.intrinsicWidth, + icon.intrinsicHeight + ) + } + + // draw stack values + } else { + val transformed = FloatArray(vals.size * 2) + + var posY = 0f + var negY = -entry.negativeSum + + run { + var k = 0 + var idx = 0 + while (k < transformed.size) { + val value = vals[idx] + val y: Float + + if (value == 0.0f && (posY == 0.0f || negY == 0.0f)) { + // Take care of the situation of a 0.0 value, which overlaps a non-zero bar + y = value + } else if (value >= 0.0f) { + posY += value + y = posY + } else { + y = negY + negY -= value + } + + transformed[k + 1] = y * phaseY + k += 2 + idx++ + } + } + + trans!!.pointValuesToPixel(transformed) + + var k = 0 + while (k < transformed.size) { + val `val` = vals[k / 2] + val drawBelow = + (`val` == 0.0f && negY == 0.0f && posY > 0.0f) || + `val` < 0.0f + val y = (transformed[k + 1] + + (if (drawBelow) negOffset else posOffset)) + + if (!mViewPortHandler.isInBoundsRight(x)) { + break + } + + if (!mViewPortHandler.isInBoundsY(y) + || !mViewPortHandler.isInBoundsLeft(x) + ) { + k += 2 + continue + } + + if (dataSet.isDrawValuesEnabled) { + drawValue( + c, + dataSet.valueFormatter, + vals[k / 2], + entry, + i, + x, + y, + color + ) + } + + if (entry.icon != null && dataSet.isDrawIconsEnabled) { + val icon = entry.icon + + Utils.drawImage( + c, + icon, + (x + iconsOffset.x).toInt(), + (y + iconsOffset.y).toInt(), + icon!!.intrinsicWidth, + icon.intrinsicHeight + ) + } + k += 2 + } + } + + bufferIndex = if (vals == null) bufferIndex + 4 else bufferIndex + 4 * vals.size + index++ + } + } + + MPPointF.recycleInstance(iconsOffset) + } + } + } + + override fun drawHighlighted(c: Canvas, indices: Array) { + 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) + + mHighlightPaint.color = set.highLightColor + mHighlightPaint.alpha = set.highLightAlpha + + val isStack = if (high.stackIndex >= 0 && e.isStacked) true else false + + val y1: Float + val y2: Float + + if (isStack) { + if (chart.isHighlightFullBarEnabled) { + y1 = e.positiveSum + y2 = -e.negativeSum + } else { + val range = e.ranges[high.stackIndex] + + y1 = range.from + y2 = range.to + } + } else { + y1 = e.y + y2 = 0f + } + + prepareBarHighlight(e.x, y1, y2, barData.barWidth / 2f, trans!!) + + setHighlightDrawPos(high, barRect) + + if (mDrawRoundedBars) { + c.drawRoundRect(RectF(barRect), mRoundedBarRadius, mRoundedBarRadius, mHighlightPaint) + } else { + c.drawRect(barRect, mHighlightPaint) + } + } + } + + /** + * Sets the drawing position of the highlight object based on the riven bar-rect. + * + * @param high + */ + protected open fun setHighlightDrawPos(high: Highlight, bar: RectF) { + high.setDraw(bar.centerX(), bar.top) + } + + override fun drawExtras(c: Canvas) { + } +} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CombinedChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CombinedChartRenderer.java index 6d0d4d3da0..6dd8ad93d9 100644 --- a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CombinedChartRenderer.java +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/CombinedChartRenderer.java @@ -9,7 +9,6 @@ import com.github.mikephil.charting.data.ChartData; import com.github.mikephil.charting.data.CombinedData; import com.github.mikephil.charting.highlight.Highlight; -import com.github.mikephil.charting.interfaces.dataprovider.BarLineScatterCandleBubbleDataProvider; import com.github.mikephil.charting.utils.ViewPortHandler; import java.lang.ref.WeakReference; @@ -115,9 +114,9 @@ public void drawHighlighted(Canvas c, Highlight[] indices) { ChartData data = null; if (renderer instanceof BarChartRenderer) - data = ((BarChartRenderer)renderer).mChart.getBarData(); + data = ((BarChartRenderer)renderer).chart.getBarData(); else if (renderer instanceof LineChartRenderer) - data = ((LineChartRenderer)renderer).mChart.getLineData(); + data = ((LineChartRenderer)renderer).chart.getLineData(); else if (renderer instanceof CandleStickChartRenderer) data = ((CandleStickChartRenderer)renderer).mChart.getCandleData(); else if (renderer instanceof ScatterChartRenderer) diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/HorizontalBarChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/HorizontalBarChartRenderer.java index e6ec91cdc9..23909ba14b 100644 --- a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/HorizontalBarChartRenderer.java +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/HorizontalBarChartRenderer.java @@ -41,12 +41,12 @@ public HorizontalBarChartRenderer(BarDataProvider chart, ChartAnimator animator, @Override public void initBuffers() { - BarData barData = mChart.getBarData(); - mBarBuffers = new HorizontalBarBuffer[barData.getDataSetCount()]; + BarData barData = chart.getBarData(); + barBuffers = new HorizontalBarBuffer[barData.getDataSetCount()]; - for (int i = 0; i < mBarBuffers.length; i++) { + for (int i = 0; i < barBuffers.length; i++) { IBarDataSet set = barData.getDataSetByIndex(i); - mBarBuffers[i] = new HorizontalBarBuffer(set.getEntryCount() * 4 * (set.isStacked() ? set.getStackSize() : 1), + barBuffers[i] = new HorizontalBarBuffer(set.getEntryCount() * 4 * (set.isStacked() ? set.getStackSize() : 1), barData.getDataSetCount(), set.isStacked()); } } @@ -56,10 +56,10 @@ public void initBuffers() { @Override protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { - Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + Transformer trans = chart.getTransformer(dataSet.getAxisDependency()); - mBarBorderPaint.setColor(dataSet.getBarBorderColor()); - mBarBorderPaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getBarBorderWidth())); + barBorderPaint.setColor(dataSet.getBarBorderColor()); + barBorderPaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getBarBorderWidth())); final boolean drawBorder = dataSet.getBarBorderWidth() > 0.f; @@ -67,10 +67,10 @@ protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { float phaseY = mAnimator.getPhaseY(); // draw the bar shadow before the values - if (mChart.isDrawBarShadowEnabled()) { - mShadowPaint.setColor(dataSet.getBarShadowColor()); + if (chart.isDrawBarShadowEnabled()) { + shadowPaint.setColor(dataSet.getBarShadowColor()); - BarData barData = mChart.getBarData(); + BarData barData = chart.getBarData(); final float barWidth = barData.getBarWidth(); final float barWidthHalf = barWidth / 2.0f; @@ -100,16 +100,16 @@ protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { mBarShadowRectBuffer.left = mViewPortHandler.contentLeft(); mBarShadowRectBuffer.right = mViewPortHandler.contentRight(); - c.drawRect(mBarShadowRectBuffer, mShadowPaint); + c.drawRect(mBarShadowRectBuffer, shadowPaint); } } // initialize the buffer - BarBuffer buffer = mBarBuffers[index]; + BarBuffer buffer = barBuffers[index]; buffer.setPhases(phaseX, phaseY); buffer.setDataSet(index); - buffer.setInverted(mChart.isInverted(dataSet.getAxisDependency())); - buffer.setBarWidth(mChart.getBarData().getBarWidth()); + buffer.setInverted(chart.isInverted(dataSet.getAxisDependency())); + buffer.setBarWidth(chart.getBarData().getBarWidth()); buffer.feed(dataSet); @@ -117,7 +117,7 @@ protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { final boolean isCustomFill = dataSet.getFills() != null && !dataSet.getFills().isEmpty(); final boolean isSingleColor = dataSet.getColors().size() == 1; - final boolean isInverted = mChart.isInverted(dataSet.getAxisDependency()); + final boolean isInverted = chart.isInverted(dataSet.getAxisDependency()); if (isSingleColor) { mRenderPaint.setColor(dataSet.getColor()); @@ -156,7 +156,7 @@ protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { if (drawBorder) { c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], - buffer.buffer[j + 3], mBarBorderPaint); + buffer.buffer[j + 3], barBorderPaint); } } } @@ -164,16 +164,16 @@ protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { @Override public void drawValues(Canvas c) { // if values are drawn - if (isDrawingValuesAllowed(mChart)) { + if (isDrawingValuesAllowed(chart)) { - List dataSets = mChart.getBarData().getDataSets(); + List dataSets = chart.getBarData().getDataSets(); final float valueOffsetPlus = Utils.convertDpToPixel(5f); float posOffset = 0f; float negOffset = 0f; - final boolean drawValueAboveBar = mChart.isDrawValueAboveBarEnabled(); + final boolean drawValueAboveBar = chart.isDrawValueAboveBarEnabled(); - for (int i = 0; i < mChart.getBarData().getDataSetCount(); i++) { + for (int i = 0; i < chart.getBarData().getDataSetCount(); i++) { IBarDataSet dataSet = dataSets.get(i); if (dataSet.getEntryCount() == 0) { @@ -183,7 +183,7 @@ public void drawValues(Canvas c) { continue; } - boolean isInverted = mChart.isInverted(dataSet.getAxisDependency()); + boolean isInverted = chart.isInverted(dataSet.getAxisDependency()); // apply the text-styling defined by the DataSet applyValueTextStyle(dataSet); @@ -192,7 +192,7 @@ public void drawValues(Canvas c) { IValueFormatter formatter = dataSet.getValueFormatter(); // get the buffer - BarBuffer buffer = mBarBuffers[i]; + BarBuffer buffer = barBuffers[i]; final float phaseY = mAnimator.getPhaseY(); @@ -265,7 +265,7 @@ public void drawValues(Canvas c) { // if each value of a potential stack should be drawn } else { - Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); + Transformer trans = chart.getTransformer(dataSet.getAxisDependency()); int bufferIndex = 0; int index = 0; @@ -441,9 +441,9 @@ protected void prepareBarHighlight(float x, float y1, float y2, float barWidthHa float left = y1; float right = y2; - mBarRect.set(left, top, right, bottom); + barRect.set(left, top, right, bottom); - trans.rectToPixelPhaseHorizontal(mBarRect, mAnimator.getPhaseY()); + trans.rectToPixelPhaseHorizontal(barRect, mAnimator.getPhaseY()); } @Override diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.java deleted file mode 100644 index ed92714b57..0000000000 --- a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.java +++ /dev/null @@ -1,881 +0,0 @@ -package com.github.mikephil.charting.renderer; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.drawable.Drawable; - -import com.github.mikephil.charting.animation.ChartAnimator; -import com.github.mikephil.charting.charts.LineChart; -import com.github.mikephil.charting.data.Entry; -import com.github.mikephil.charting.data.LineData; -import com.github.mikephil.charting.data.LineDataSet; -import com.github.mikephil.charting.highlight.Highlight; -import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider; -import com.github.mikephil.charting.interfaces.datasets.IDataSet; -import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; -import com.github.mikephil.charting.utils.ColorTemplate; -import com.github.mikephil.charting.utils.MPPointD; -import com.github.mikephil.charting.utils.MPPointF; -import com.github.mikephil.charting.utils.Transformer; -import com.github.mikephil.charting.utils.Utils; -import com.github.mikephil.charting.utils.ViewPortHandler; - -import java.lang.ref.WeakReference; -import java.util.HashMap; -import java.util.List; - -public class LineChartRenderer extends LineRadarRenderer { - - protected LineDataProvider mChart; - - /** - * paint for the inner circle of the value indicators - */ - protected Paint mCirclePaintInner; - - /** - * Bitmap object used for drawing the paths (otherwise they are too long if - * rendered directly on the canvas) - */ - protected WeakReference mDrawBitmap; - - /** - * on this canvas, the paths are rendered, it is initialized with the - * pathBitmap - */ - protected Canvas mBitmapCanvas; - - /** - * the bitmap configuration to be used - */ - protected Bitmap.Config mBitmapConfig = Bitmap.Config.ARGB_8888; - - protected Path cubicPath = new Path(); - protected Path cubicFillPath = new Path(); - - public LineChartRenderer(LineDataProvider chart, ChartAnimator animator, - ViewPortHandler viewPortHandler) { - super(animator, viewPortHandler); - mChart = chart; - - mCirclePaintInner = new Paint(Paint.ANTI_ALIAS_FLAG); - mCirclePaintInner.setStyle(Paint.Style.FILL); - mCirclePaintInner.setColor(Color.WHITE); - } - - @Override - public void initBuffers() { - } - - @Override - public void drawData(Canvas c) { - - int width = (int) mViewPortHandler.getChartWidth(); - int height = (int) mViewPortHandler.getChartHeight(); - - Bitmap drawBitmap = mDrawBitmap == null ? null : mDrawBitmap.get(); - - if (drawBitmap == null - || (drawBitmap.getWidth() != width) - || (drawBitmap.getHeight() != height)) { - - if (width > 0 && height > 0) { - drawBitmap = Bitmap.createBitmap(width, height, mBitmapConfig); - mDrawBitmap = new WeakReference<>(drawBitmap); - mBitmapCanvas = new Canvas(drawBitmap); - } else - return; - } - - drawBitmap.eraseColor(Color.TRANSPARENT); - - LineData lineData = mChart.getLineData(); - - for (ILineDataSet set : lineData.getDataSets()) { - - if (set.isVisible()) - drawDataSet(c, set); - } - - c.drawBitmap(drawBitmap, 0, 0, mRenderPaint); - } - - protected void drawDataSet(Canvas c, ILineDataSet dataSet) { - - if (dataSet.getEntryCount() < 1) - return; - - mRenderPaint.setStrokeWidth(dataSet.getLineWidth()); - mRenderPaint.setPathEffect(dataSet.getDashPathEffect()); - - switch (dataSet.getMode()) { - default: - case LINEAR: - case STEPPED: - drawLinear(c, dataSet); - break; - - case CUBIC_BEZIER: - drawCubicBezier(dataSet); - break; - - case HORIZONTAL_BEZIER: - drawHorizontalBezier(dataSet); - break; - } - - mRenderPaint.setPathEffect(null); - } - - protected void drawHorizontalBezier(ILineDataSet dataSet) { - - float phaseY = mAnimator.getPhaseY(); - - Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); - - mXBounds.set(mChart, dataSet); - - cubicPath.reset(); - - if (mXBounds.range >= 1) { - - Entry prev = dataSet.getEntryForIndex(mXBounds.min); - Entry cur = prev; - - // let the spline start - cubicPath.moveTo(cur.getX(), cur.getY() * phaseY); - - for (int j = mXBounds.min + 1; j <= mXBounds.range + mXBounds.min; j++) { - - prev = cur; - cur = dataSet.getEntryForIndex(j); - - final float cpx = (prev.getX()) - + (cur.getX() - prev.getX()) / 2.0f; - - cubicPath.cubicTo( - cpx, prev.getY() * phaseY, - cpx, cur.getY() * phaseY, - cur.getX(), cur.getY() * phaseY); - } - } - - // if filled is enabled, close the path - if (dataSet.isDrawFilledEnabled()) { - - cubicFillPath.reset(); - cubicFillPath.addPath(cubicPath); - // create a new path, this is bad for performance - drawCubicFill(mBitmapCanvas, dataSet, cubicFillPath, trans, mXBounds); - } - - mRenderPaint.setColor(dataSet.getColor()); - - mRenderPaint.setStyle(Paint.Style.STROKE); - - trans.pathValueToPixel(cubicPath); - - mBitmapCanvas.drawPath(cubicPath, mRenderPaint); - - mRenderPaint.setPathEffect(null); - } - - protected void drawCubicBezier(ILineDataSet dataSet) { - - float phaseY = mAnimator.getPhaseY(); - - Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); - - mXBounds.set(mChart, dataSet); - - float intensity = dataSet.getCubicIntensity(); - - cubicPath.reset(); - - if (mXBounds.range >= 1) { - - float prevDx = 0f; - float prevDy = 0f; - float curDx = 0f; - float curDy = 0f; - - // Take an extra point from the left, and an extra from the right. - // That's because we need 4 points for a cubic bezier (cubic=4), otherwise we get lines moving and doing weird stuff on the edges of the chart. - // So in the starting `prev` and `cur`, go -2, -1 - // And in the `lastIndex`, add +1 - - final int firstIndex = mXBounds.min + 1; - final int lastIndex = mXBounds.min + mXBounds.range; - - Entry prevPrev; - Entry prev = dataSet.getEntryForIndex(Math.max(firstIndex - 2, 0)); - Entry cur = dataSet.getEntryForIndex(Math.max(firstIndex - 1, 0)); - Entry next = cur; - int nextIndex = -1; - - if (cur == null) return; - - // let the spline start - cubicPath.moveTo(cur.getX(), cur.getY() * phaseY); - - for (int j = mXBounds.min + 1; j <= mXBounds.range + mXBounds.min; j++) { - - prevPrev = prev; - prev = cur; - cur = nextIndex == j ? next : dataSet.getEntryForIndex(j); - - nextIndex = j + 1 < dataSet.getEntryCount() ? j + 1 : j; - next = dataSet.getEntryForIndex(nextIndex); - - prevDx = (cur.getX() - prevPrev.getX()) * intensity; - prevDy = (cur.getY() - prevPrev.getY()) * intensity; - curDx = (next.getX() - prev.getX()) * intensity; - curDy = (next.getY() - prev.getY()) * intensity; - - cubicPath.cubicTo(prev.getX() + prevDx, (prev.getY() + prevDy) * phaseY, - cur.getX() - curDx, - (cur.getY() - curDy) * phaseY, cur.getX(), cur.getY() * phaseY); - } - } - - // if filled is enabled, close the path - if (dataSet.isDrawFilledEnabled()) { - - cubicFillPath.reset(); - cubicFillPath.addPath(cubicPath); - - drawCubicFill(mBitmapCanvas, dataSet, cubicFillPath, trans, mXBounds); - } - - mRenderPaint.setColor(dataSet.getColor()); - - mRenderPaint.setStyle(Paint.Style.STROKE); - - trans.pathValueToPixel(cubicPath); - - mBitmapCanvas.drawPath(cubicPath, mRenderPaint); - - mRenderPaint.setPathEffect(null); - } - - protected void drawCubicFill(Canvas c, ILineDataSet dataSet, Path spline, Transformer trans, XBounds bounds) { - - float fillMin = dataSet.getFillFormatter() - .getFillLinePosition(dataSet, mChart); - - spline.lineTo(dataSet.getEntryForIndex(bounds.min + bounds.range).getX(), fillMin); - spline.lineTo(dataSet.getEntryForIndex(bounds.min).getX(), fillMin); - spline.close(); - - trans.pathValueToPixel(spline); - - final Drawable drawable = dataSet.getFillDrawable(); - if (drawable != null) { - - drawFilledPath(c, spline, drawable); - } else { - - drawFilledPath(c, spline, dataSet.getFillColor(), dataSet.getFillAlpha()); - } - } - - private float[] mLineBuffer = new float[4]; - - /** - * Draws a normal line. - * - * @param c - * @param dataSet - */ - protected void drawLinear(Canvas c, ILineDataSet dataSet) { - - int entryCount = dataSet.getEntryCount(); - - final boolean isDrawSteppedEnabled = dataSet.isDrawSteppedEnabled(); - final int pointsPerEntryPair = isDrawSteppedEnabled ? 4 : 2; - - Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); - - float phaseY = mAnimator.getPhaseY(); - - mRenderPaint.setStyle(Paint.Style.STROKE); - - Canvas canvas = null; - - // if the data-set is dashed, draw on bitmap-canvas - if (dataSet.isDashedLineEnabled()) { - canvas = mBitmapCanvas; - } else { - canvas = c; - } - - mXBounds.set(mChart, dataSet); - - // if drawing filled is enabled - if (dataSet.isDrawFilledEnabled() && entryCount > 0) { - drawLinearFill(c, dataSet, trans, mXBounds); - } - - // more than 1 color - if (dataSet.getColors().size() > 1) { - - int numberOfFloats = pointsPerEntryPair * 2; - - if (mLineBuffer.length <= numberOfFloats) - mLineBuffer = new float[numberOfFloats * 2]; - - int max = mXBounds.min + mXBounds.range; - - for (int j = mXBounds.min; j < max; j++) { - - Entry e = dataSet.getEntryForIndex(j); - if (e == null) continue; - - mLineBuffer[0] = e.getX(); - mLineBuffer[1] = e.getY() * phaseY; - - if (j < mXBounds.max) { - - e = dataSet.getEntryForIndex(j + 1); - - if (e == null) break; - - if (isDrawSteppedEnabled) { - mLineBuffer[2] = e.getX(); - mLineBuffer[3] = mLineBuffer[1]; - mLineBuffer[4] = mLineBuffer[2]; - mLineBuffer[5] = mLineBuffer[3]; - mLineBuffer[6] = e.getX(); - mLineBuffer[7] = e.getY() * phaseY; - } else { - mLineBuffer[2] = e.getX(); - mLineBuffer[3] = e.getY() * phaseY; - } - - } else { - mLineBuffer[2] = mLineBuffer[0]; - mLineBuffer[3] = mLineBuffer[1]; - } - - // Determine the start and end coordinates of the line, and make sure they differ. - float firstCoordinateX = mLineBuffer[0]; - float firstCoordinateY = mLineBuffer[1]; - float lastCoordinateX = mLineBuffer[numberOfFloats - 2]; - float lastCoordinateY = mLineBuffer[numberOfFloats - 1]; - - if (firstCoordinateX == lastCoordinateX && - firstCoordinateY == lastCoordinateY) - continue; - - trans.pointValuesToPixel(mLineBuffer); - - if (!mViewPortHandler.isInBoundsRight(firstCoordinateX)) - break; - - // make sure the lines don't do shitty things outside - // bounds - if (!mViewPortHandler.isInBoundsLeft(lastCoordinateX) || - !mViewPortHandler.isInBoundsTop(Math.max(firstCoordinateY, lastCoordinateY)) || - !mViewPortHandler.isInBoundsBottom(Math.min(firstCoordinateY, lastCoordinateY))) - continue; - - // get the color that is set for this line-segment - mRenderPaint.setColor(dataSet.getColor(j)); - - canvas.drawLines(mLineBuffer, 0, pointsPerEntryPair * 2, mRenderPaint); - } - - } else { // only one color per dataset - - if (mLineBuffer.length < Math.max((entryCount) * pointsPerEntryPair, pointsPerEntryPair) * 2) - mLineBuffer = new float[Math.max((entryCount) * pointsPerEntryPair, pointsPerEntryPair) * 4]; - - Entry e1, e2; - - e1 = dataSet.getEntryForIndex(mXBounds.min); - - if (e1 != null) { - - int j = 0; - for (int x = mXBounds.min; x <= mXBounds.range + mXBounds.min; x++) { - - e1 = dataSet.getEntryForIndex(x == 0 ? 0 : (x - 1)); - e2 = dataSet.getEntryForIndex(x); - - if (e1 == null || e2 == null) continue; - - mLineBuffer[j++] = e1.getX(); - mLineBuffer[j++] = e1.getY() * phaseY; - - if (isDrawSteppedEnabled) { - mLineBuffer[j++] = e2.getX(); - mLineBuffer[j++] = e1.getY() * phaseY; - mLineBuffer[j++] = e2.getX(); - mLineBuffer[j++] = e1.getY() * phaseY; - } - - mLineBuffer[j++] = e2.getX(); - mLineBuffer[j++] = e2.getY() * phaseY; - } - - if (j > 0) { - trans.pointValuesToPixel(mLineBuffer); - - final int size = Math.max((mXBounds.range + 1) * pointsPerEntryPair, pointsPerEntryPair) * 2; - - mRenderPaint.setColor(dataSet.getColor()); - - canvas.drawLines(mLineBuffer, 0, size, mRenderPaint); - } - } - } - - mRenderPaint.setPathEffect(null); - } - - protected Path mGenerateFilledPathBuffer = new Path(); - - /** - * Draws a filled linear path on the canvas. - * - * @param c - * @param dataSet - * @param trans - * @param bounds - */ - protected void drawLinearFill(Canvas c, ILineDataSet dataSet, Transformer trans, XBounds bounds) { - - final Path filled = mGenerateFilledPathBuffer; - - final int startingIndex = bounds.min; - final int endingIndex = bounds.range + bounds.min; - final int indexInterval = 128; - - int currentStartIndex = 0; - int currentEndIndex = indexInterval; - int iterations = 0; - - // Doing this iteratively in order to avoid OutOfMemory errors that can happen on large bounds sets. - do { - currentStartIndex = startingIndex + (iterations * indexInterval); - currentEndIndex = currentStartIndex + indexInterval; - currentEndIndex = Math.min(currentEndIndex, endingIndex); - - if (currentStartIndex <= currentEndIndex) { - final Drawable drawable = dataSet.getFillDrawable(); - - int startIndex = currentStartIndex; - int endIndex = currentEndIndex; - - // Add a little extra to the path for drawables, larger data sets were showing space between adjacent drawables - if (drawable != null) { - - startIndex = Math.max(0, currentStartIndex - 1); - endIndex = Math.min(endingIndex, currentEndIndex + 1); - } - - generateFilledPath(dataSet, startIndex, endIndex, filled); - - trans.pathValueToPixel(filled); - - if (drawable != null) { - - drawFilledPath(c, filled, drawable); - } else { - - drawFilledPath(c, filled, dataSet.getFillColor(), dataSet.getFillAlpha()); - } - } - - iterations++; - - } while (currentStartIndex <= currentEndIndex); - - } - - /** - * Generates a path that is used for filled drawing. - * - * @param dataSet The dataset from which to read the entries. - * @param startIndex The index from which to start reading the dataset - * @param endIndex The index from which to stop reading the dataset - * @param outputPath The path object that will be assigned the chart data. - * @return - */ - private void generateFilledPath(final ILineDataSet dataSet, final int startIndex, final int endIndex, final Path outputPath) { - - final float fillMin = dataSet.getFillFormatter().getFillLinePosition(dataSet, mChart); - final float phaseY = mAnimator.getPhaseY(); - final boolean isDrawSteppedEnabled = dataSet.getMode() == LineDataSet.Mode.STEPPED; - - final Path filled = outputPath; - filled.reset(); - - final Entry entry = dataSet.getEntryForIndex(startIndex); - - filled.moveTo(entry.getX(), fillMin); - filled.lineTo(entry.getX(), entry.getY() * phaseY); - - // create a new path - Entry currentEntry = null; - Entry previousEntry = entry; - for (int x = startIndex + 1; x <= endIndex; x++) { - - currentEntry = dataSet.getEntryForIndex(x); - - if (currentEntry != null) { - if (isDrawSteppedEnabled) { - filled.lineTo(currentEntry.getX(), previousEntry.getY() * phaseY); - } - - filled.lineTo(currentEntry.getX(), currentEntry.getY() * phaseY); - - previousEntry = currentEntry; - } - } - - // close up - if (currentEntry != null) { - filled.lineTo(currentEntry.getX(), fillMin); - } - - filled.close(); - } - - @Override - public void drawValues(Canvas c) { - - if (isDrawingValuesAllowed(mChart)) { - - List dataSets = mChart.getLineData().getDataSets(); - - for (int i = 0; i < dataSets.size(); i++) { - - ILineDataSet dataSet = dataSets.get(i); - if (dataSet.getEntryCount() == 0) { - continue; - } - if (!shouldDrawValues(dataSet) || dataSet.getEntryCount() < 1) { - continue; - } - - // apply the text-styling defined by the DataSet - applyValueTextStyle(dataSet); - - Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); - - // make sure the values do not interfear with the circles - int valOffset = (int) (dataSet.getCircleRadius() * 1.75f); - - if (!dataSet.isDrawCirclesEnabled()) - valOffset = valOffset / 2; - - mXBounds.set(mChart, dataSet); - - float[] positions = trans.generateTransformedValuesLine(dataSet, mAnimator.getPhaseX(), mAnimator - .getPhaseY(), mXBounds.min, mXBounds.max); - - MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); - iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); - iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); - - for (int j = 0; j < positions.length; j += 2) { - - float x = positions[j]; - float y = positions[j + 1]; - - if (!mViewPortHandler.isInBoundsRight(x)) - break; - - if (!mViewPortHandler.isInBoundsLeft(x) || !mViewPortHandler.isInBoundsY(y)) - continue; - - Entry entry = dataSet.getEntryForIndex(j / 2 + mXBounds.min); - - if (entry != null) { - if (dataSet.isDrawValuesEnabled()) { - drawValue(c, dataSet.getValueFormatter(), entry.getY(), entry, i, x, - y - valOffset, dataSet.getValueTextColor(j / 2)); - } - - if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { - - Drawable icon = entry.getIcon(); - - Utils.drawImage( - c, - icon, - (int)(x + iconsOffset.x), - (int)(y + iconsOffset.y), - icon.getIntrinsicWidth(), - icon.getIntrinsicHeight()); - } - } - } - - MPPointF.recycleInstance(iconsOffset); - } - } - } - - @Override - public void drawExtras(Canvas c) { - drawCircles(c); - } - - /** - * cache for the circle bitmaps of all datasets - */ - private final HashMap mImageCaches = new HashMap<>(); - - /** - * buffer for drawing the circles - */ - private final float[] mCirclesBuffer = new float[2]; - - protected void drawCircles(Canvas c) { - - mRenderPaint.setStyle(Paint.Style.FILL); - - float phaseY = mAnimator.getPhaseY(); - - mCirclesBuffer[0] = 0; - mCirclesBuffer[1] = 0; - - List dataSets = mChart.getLineData().getDataSets(); - - for (int i = 0; i < dataSets.size(); i++) { - - ILineDataSet dataSet = dataSets.get(i); - - if (!dataSet.isVisible() || !dataSet.isDrawCirclesEnabled() || - dataSet.getEntryCount() == 0) - continue; - - mCirclePaintInner.setColor(dataSet.getCircleHoleColor()); - - Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); - - mXBounds.set(mChart, dataSet); - - float circleRadius = dataSet.getCircleRadius(); - float circleHoleRadius = dataSet.getCircleHoleRadius(); - boolean drawCircleHole = dataSet.isDrawCircleHoleEnabled() && - circleHoleRadius < circleRadius && - circleHoleRadius > 0.f; - boolean drawTransparentCircleHole = drawCircleHole && - dataSet.getCircleHoleColor() == ColorTemplate.COLOR_NONE; - - DataSetImageCache imageCache; - - if (mImageCaches.containsKey(dataSet)) { - imageCache = mImageCaches.get(dataSet); - } else { - imageCache = new DataSetImageCache(); - mImageCaches.put(dataSet, imageCache); - } - - boolean changeRequired = imageCache.init(dataSet); - - // only fill the cache with new bitmaps if a change is required - if (changeRequired) { - imageCache.fill(dataSet, drawCircleHole, drawTransparentCircleHole); - } - - int boundsRangeCount = mXBounds.range + mXBounds.min; - - for (int j = mXBounds.min; j <= boundsRangeCount; j++) { - - Entry e = dataSet.getEntryForIndex(j); - - if (e == null) break; - - mCirclesBuffer[0] = e.getX(); - mCirclesBuffer[1] = e.getY() * phaseY; - - trans.pointValuesToPixel(mCirclesBuffer); - - if (!mViewPortHandler.isInBoundsRight(mCirclesBuffer[0])) - break; - - if (!mViewPortHandler.isInBoundsLeft(mCirclesBuffer[0]) || - !mViewPortHandler.isInBoundsY(mCirclesBuffer[1])) - continue; - - Bitmap circleBitmap = imageCache.getBitmap(j); - - if (circleBitmap != null) { - c.drawBitmap(circleBitmap, mCirclesBuffer[0] - circleRadius, mCirclesBuffer[1] - circleRadius, null); - } - } - } - } - - @Override - public void drawHighlighted(Canvas c, Highlight[] indices) { - - LineData lineData = mChart.getLineData(); - - for (Highlight high : indices) { - - ILineDataSet set = lineData.getDataSetByIndex(high.getDataSetIndex()); - - if (set == null || !set.isHighlightEnabled()) - continue; - - Entry e = set.getEntryForXValue(high.getX(), high.getY()); - - if (!isInBoundsX(e, set)) - continue; - - MPPointD pix = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(e.getX(), e.getY() * mAnimator - .getPhaseY()); - - high.setDraw((float) pix.x, (float) pix.y); - - // draw the lines - drawHighlightLines(c, (float) pix.x, (float) pix.y, set); - } - } - - /** - * Sets the Bitmap.Config to be used by this renderer. - * Default: Bitmap.Config.ARGB_8888 - * Use Bitmap.Config.ARGB_4444 to consume less memory. - * - * @param config - */ - public void setBitmapConfig(Bitmap.Config config) { - mBitmapConfig = config; - releaseBitmap(); - } - - /** - * Returns the Bitmap.Config that is used by this renderer. - * - * @return - */ - public Bitmap.Config getBitmapConfig() { - return mBitmapConfig; - } - - /** - * Releases the drawing bitmap. This should be called when {@link LineChart#onDetachedFromWindow()}. - */ - public void releaseBitmap() { - if (mBitmapCanvas != null) { - mBitmapCanvas.setBitmap(null); - mBitmapCanvas = null; - } - if (mDrawBitmap != null) { - Bitmap drawBitmap = mDrawBitmap.get(); - if (drawBitmap != null) { - drawBitmap.recycle(); - } - mDrawBitmap.clear(); - mDrawBitmap = null; - } - } - - private class DataSetImageCache { - - private final Path mCirclePathBuffer = new Path(); - - private Bitmap[] circleBitmaps; - - /** - * Sets up the cache, returns true if a change of cache was required. - * - * @param set - * @return - */ - protected boolean init(ILineDataSet set) { - - int size = set.getCircleColorCount(); - boolean changeRequired = false; - - if (circleBitmaps == null) { - circleBitmaps = new Bitmap[size]; - changeRequired = true; - } else if (circleBitmaps.length != size) { - circleBitmaps = new Bitmap[size]; - changeRequired = true; - } - - return changeRequired; - } - - /** - * Fills the cache with bitmaps for the given dataset. - * - * @param set - * @param drawCircleHole - * @param drawTransparentCircleHole - */ - protected void fill(ILineDataSet set, boolean drawCircleHole, boolean drawTransparentCircleHole) { - - int colorCount = set.getCircleColorCount(); - float circleRadius = set.getCircleRadius(); - float circleHoleRadius = set.getCircleHoleRadius(); - - for (int i = 0; i < colorCount; i++) { - - Bitmap.Config conf = Bitmap.Config.ARGB_4444; - Bitmap circleBitmap = Bitmap.createBitmap((int) (circleRadius * 2.1), (int) (circleRadius * 2.1), conf); - - Canvas canvas = new Canvas(circleBitmap); - circleBitmaps[i] = circleBitmap; - mRenderPaint.setColor(set.getCircleColor(i)); - - if (drawTransparentCircleHole) { - // Begin path for circle with hole - mCirclePathBuffer.reset(); - - mCirclePathBuffer.addCircle( - circleRadius, - circleRadius, - circleRadius, - Path.Direction.CW); - - // Cut hole in path - mCirclePathBuffer.addCircle( - circleRadius, - circleRadius, - circleHoleRadius, - Path.Direction.CCW); - - // Fill in-between - canvas.drawPath(mCirclePathBuffer, mRenderPaint); - } else { - - canvas.drawCircle( - circleRadius, - circleRadius, - circleRadius, - mRenderPaint); - - if (drawCircleHole) { - canvas.drawCircle( - circleRadius, - circleRadius, - circleHoleRadius, - mCirclePaintInner); - } - } - } - } - - /** - * Returns the cached Bitmap at the given index. - * - * @param index - * @return - */ - protected Bitmap getBitmap(int index) { - return circleBitmaps[index % circleBitmaps.length]; - } - } -} diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.kt b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.kt new file mode 100644 index 0000000000..fa9e88fec9 --- /dev/null +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/LineChartRenderer.kt @@ -0,0 +1,812 @@ +package com.github.mikephil.charting.renderer + +import android.graphics.Bitmap +import android.graphics.Bitmap.createBitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import com.github.mikephil.charting.animation.ChartAnimator +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.data.LineDataSet +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider +import com.github.mikephil.charting.interfaces.datasets.IDataSet +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet +import com.github.mikephil.charting.utils.ColorTemplate +import com.github.mikephil.charting.utils.MPPointF +import com.github.mikephil.charting.utils.Transformer +import com.github.mikephil.charting.utils.Utils +import com.github.mikephil.charting.utils.ViewPortHandler +import java.lang.ref.WeakReference +import kotlin.math.max +import kotlin.math.min + +class LineChartRenderer( + @JvmField var chart: LineDataProvider, animator: ChartAnimator?, + viewPortHandler: ViewPortHandler? +) : LineRadarRenderer(animator, viewPortHandler) { + /** + * paint for the inner circle of the value indicators + */ + protected var circlePaintInner: Paint = Paint(Paint.ANTI_ALIAS_FLAG) + + /** + * Bitmap object used for drawing the paths (otherwise they are too long if + * rendered directly on the canvas) + */ + protected var drawBitmap: WeakReference? = null + + /** + * on this canvas, the paths are rendered, it is initialized with the + * pathBitmap + */ + protected var bitmapCanvas: Canvas? = null + + /** + * the bitmap configuration to be used + */ + protected var mBitmapConfig: Bitmap.Config = Bitmap.Config.ARGB_8888 + + protected var cubicPath: Path = Path() + protected var cubicFillPath: Path = Path() + + override fun initBuffers() { + } + + override fun drawData(c: Canvas) { + val width = mViewPortHandler.chartWidth.toInt() + val height = mViewPortHandler.chartHeight.toInt() + + var drawBitmapLocal = if (drawBitmap == null) null else drawBitmap!!.get() + + if (drawBitmapLocal == null || (drawBitmapLocal.width != width) + || (drawBitmapLocal.height != height) + ) { + if (width > 0 && height > 0) { + drawBitmapLocal = createBitmap(width, height, mBitmapConfig) + this.drawBitmap = WeakReference(drawBitmapLocal) + bitmapCanvas = Canvas(drawBitmapLocal) + } else return + } + + drawBitmapLocal.eraseColor(Color.TRANSPARENT) + + val lineData = chart.lineData + + for (set in lineData.dataSets) { + if (set.isVisible) drawDataSet(c, set) + } + + c.drawBitmap(drawBitmapLocal, 0f, 0f, mRenderPaint) + } + + protected fun drawDataSet(c: Canvas?, dataSet: ILineDataSet) { + if (dataSet.entryCount < 1) return + + mRenderPaint.strokeWidth = dataSet.lineWidth + mRenderPaint.setPathEffect(dataSet.dashPathEffect) + + when (dataSet.mode) { + LineDataSet.Mode.LINEAR, LineDataSet.Mode.STEPPED -> drawLinear(c, dataSet) + LineDataSet.Mode.CUBIC_BEZIER -> drawCubicBezier(dataSet) + LineDataSet.Mode.HORIZONTAL_BEZIER -> drawHorizontalBezier(dataSet) + else -> drawLinear(c, dataSet) + } + + mRenderPaint.setPathEffect(null) + } + + protected fun drawHorizontalBezier(dataSet: ILineDataSet) { + val phaseY = mAnimator.phaseY + + val trans = chart.getTransformer(dataSet.axisDependency) + + mXBounds[chart] = dataSet + + cubicPath.reset() + + if (mXBounds.range >= 1) { + var prev = dataSet.getEntryForIndex(mXBounds.min) + var cur = prev + + // let the spline start + cubicPath.moveTo(cur.x, cur.y * phaseY) + + for (j in mXBounds.min + 1..mXBounds.range + mXBounds.min) { + prev = cur + cur = dataSet.getEntryForIndex(j) + + val cpx = ((prev.x) + + (cur.x - prev.x) / 2.0f) + + cubicPath.cubicTo( + cpx, prev.y * phaseY, + cpx, cur.y * phaseY, + cur.x, cur.y * phaseY + ) + } + } + + // if filled is enabled, close the path + if (dataSet.isDrawFilledEnabled) { + cubicFillPath.reset() + cubicFillPath.addPath(cubicPath) + // create a new path, this is bad for performance + drawCubicFill(bitmapCanvas, dataSet, cubicFillPath, trans!!, mXBounds) + } + + mRenderPaint.color = dataSet.color + + mRenderPaint.style = Paint.Style.STROKE + + trans!!.pathValueToPixel(cubicPath) + + bitmapCanvas!!.drawPath(cubicPath, mRenderPaint) + + mRenderPaint.setPathEffect(null) + } + + protected fun drawCubicBezier(dataSet: ILineDataSet) { + val phaseY = mAnimator.phaseY + + val trans = chart.getTransformer(dataSet.axisDependency) + + mXBounds[chart] = dataSet + + val intensity = dataSet.cubicIntensity + + cubicPath.reset() + + if (mXBounds.range >= 1) { + var prevDx: Float + var prevDy: Float + var curDx: Float + var curDy: Float + + // Take an extra point from the left, and an extra from the right. + // That's because we need 4 points for a cubic bezier (cubic=4), otherwise we get lines moving and doing weird stuff on the edges of the chart. + // So in the starting `prev` and `cur`, go -2, -1 + // And in the `lastIndex`, add +1 + val firstIndex = mXBounds.min + 1 + val lastIndex = mXBounds.min + mXBounds.range + + var prevPrev: Entry? + var prev = dataSet.getEntryForIndex(max((firstIndex - 2).toDouble(), 0.0).toInt()) + var cur = dataSet.getEntryForIndex(max((firstIndex - 1).toDouble(), 0.0).toInt()) + var next = cur + var nextIndex = -1 + + if (cur == null) return + + // let the spline start + cubicPath.moveTo(cur.x, cur.y * phaseY) + + for (j in mXBounds.min + 1..mXBounds.range + mXBounds.min) { + prevPrev = prev + prev = cur + cur = if (nextIndex == j) next else dataSet.getEntryForIndex(j) + + nextIndex = if (j + 1 < dataSet.entryCount) j + 1 else j + next = dataSet.getEntryForIndex(nextIndex) + + prevDx = (cur!!.x - prevPrev!!.x) * intensity + prevDy = (cur.y - prevPrev.y) * intensity + curDx = (next.x - prev!!.x) * intensity + curDy = (next.y - prev.y) * intensity + + cubicPath.cubicTo( + prev.x + prevDx, (prev.y + prevDy) * phaseY, + cur.x - curDx, + (cur.y - curDy) * phaseY, cur.x, cur.y * phaseY + ) + } + } + + // if filled is enabled, close the path + if (dataSet.isDrawFilledEnabled) { + cubicFillPath.reset() + cubicFillPath.addPath(cubicPath) + + drawCubicFill(bitmapCanvas, dataSet, cubicFillPath, trans!!, mXBounds) + } + + mRenderPaint.color = dataSet.color + + mRenderPaint.style = Paint.Style.STROKE + + trans!!.pathValueToPixel(cubicPath) + + bitmapCanvas!!.drawPath(cubicPath, mRenderPaint) + + mRenderPaint.setPathEffect(null) + } + + protected fun drawCubicFill(c: Canvas?, dataSet: ILineDataSet, spline: Path, trans: Transformer, bounds: XBounds) { + val fillMin = dataSet.fillFormatter + .getFillLinePosition(dataSet, chart) + + spline.lineTo(dataSet.getEntryForIndex(bounds.min + bounds.range).x, fillMin) + spline.lineTo(dataSet.getEntryForIndex(bounds.min).x, fillMin) + spline.close() + + trans.pathValueToPixel(spline) + + val drawable = dataSet.fillDrawable + if (drawable != null) { + drawFilledPath(c, spline, drawable) + } else { + drawFilledPath(c, spline, dataSet.fillColor, dataSet.fillAlpha) + } + } + + private var mLineBuffer = FloatArray(4) + + /** + * Draws a normal line. + * + * @param c + * @param dataSet + */ + protected fun drawLinear(c: Canvas?, dataSet: ILineDataSet) { + val entryCount = dataSet.entryCount + + val isDrawSteppedEnabled = dataSet.isDrawSteppedEnabled + val pointsPerEntryPair = if (isDrawSteppedEnabled) 4 else 2 + + val trans = chart.getTransformer(dataSet.axisDependency) + + val phaseY = mAnimator.phaseY + + mRenderPaint.style = Paint.Style.STROKE + + // if the data-set is dashed, draw on bitmap-canvas + val canvas: Canvas? = if (dataSet.isDashedLineEnabled) { + bitmapCanvas + } else { + c + } + + mXBounds[chart] = dataSet + + // if drawing filled is enabled + if (dataSet.isDrawFilledEnabled && entryCount > 0) { + drawLinearFill(c, dataSet, trans!!, mXBounds) + } + + // more than 1 color + if (dataSet.colors.size > 1) { + val numberOfFloats = pointsPerEntryPair * 2 + + if (mLineBuffer.size <= numberOfFloats) mLineBuffer = FloatArray(numberOfFloats * 2) + + val max = mXBounds.min + mXBounds.range + + for (j in mXBounds.min.. 0) { + trans!!.pointValuesToPixel(mLineBuffer) + + val size = (max(((mXBounds.range + 1) * pointsPerEntryPair).toDouble(), pointsPerEntryPair.toDouble()) * 2).toInt() + + mRenderPaint.color = dataSet.color + + canvas!!.drawLines(mLineBuffer, 0, size, mRenderPaint) + } + } + } + + mRenderPaint.setPathEffect(null) + } + + protected var mGenerateFilledPathBuffer: Path = Path() + + /** + * Draws a filled linear path on the canvas. + * + * @param c + * @param dataSet + * @param trans + * @param bounds + */ + protected fun drawLinearFill(c: Canvas?, dataSet: ILineDataSet, trans: Transformer, bounds: XBounds) { + val filled = mGenerateFilledPathBuffer + + val startingIndex = bounds.min + val endingIndex = bounds.range + bounds.min + val indexInterval = 128 + + var currentStartIndex: Int + var currentEndIndex: Int + var iterations = 0 + + // Doing this iteratively in order to avoid OutOfMemory errors that can happen on large bounds sets. + do { + currentStartIndex = startingIndex + (iterations * indexInterval) + currentEndIndex = currentStartIndex + indexInterval + currentEndIndex = min(currentEndIndex.toDouble(), endingIndex.toDouble()).toInt() + + if (currentStartIndex <= currentEndIndex) { + val drawable = dataSet.fillDrawable + + var startIndex = currentStartIndex + var endIndex = currentEndIndex + + // Add a little extra to the path for drawables, larger data sets were showing space between adjacent drawables + if (drawable != null) { + startIndex = max(0.0, (currentStartIndex - 1).toDouble()).toInt() + endIndex = min(endingIndex.toDouble(), (currentEndIndex + 1).toDouble()).toInt() + } + + generateFilledPath(dataSet, startIndex, endIndex, filled) + + trans.pathValueToPixel(filled) + + if (drawable != null) { + drawFilledPath(c, filled, drawable) + } else { + drawFilledPath(c, filled, dataSet.fillColor, dataSet.fillAlpha) + } + } + + iterations++ + } while (currentStartIndex <= currentEndIndex) + } + + /** + * Generates a path that is used for filled drawing. + * + * @param dataSet The dataset from which to read the entries. + * @param startIndex The index from which to start reading the dataset + * @param endIndex The index from which to stop reading the dataset + * @param outputPath The path object that will be assigned the chart data. + * @return + */ + private fun generateFilledPath(dataSet: ILineDataSet, startIndex: Int, endIndex: Int, outputPath: Path) { + val fillMin = dataSet.fillFormatter.getFillLinePosition(dataSet, chart) + val phaseY = mAnimator.phaseY + val isDrawSteppedEnabled = dataSet.mode == LineDataSet.Mode.STEPPED + + val filled = outputPath + filled.reset() + + val entry = dataSet.getEntryForIndex(startIndex) + + filled.moveTo(entry.x, fillMin) + filled.lineTo(entry.x, entry.y * phaseY) + + // create a new path + var currentEntry: Entry? = null + var previousEntry = entry + for (x in startIndex + 1..endIndex) { + currentEntry = dataSet.getEntryForIndex(x) + + if (currentEntry != null) { + if (isDrawSteppedEnabled) { + filled.lineTo(currentEntry.x, previousEntry.y * phaseY) + } + + filled.lineTo(currentEntry.x, currentEntry.y * phaseY) + + previousEntry = currentEntry + } + } + + // close up + if (currentEntry != null) { + filled.lineTo(currentEntry.x, fillMin) + } + + filled.close() + } + + override fun drawValues(c: Canvas) { + if (isDrawingValuesAllowed(chart)) { + val dataSets = chart.lineData.dataSets + + for (i in dataSets.indices) { + val dataSet = dataSets[i] + if (dataSet.entryCount == 0) { + continue + } + if (!shouldDrawValues(dataSet) || dataSet.entryCount < 1) { + continue + } + + // apply the text-styling defined by the DataSet + applyValueTextStyle(dataSet) + + val trans = chart.getTransformer(dataSet.axisDependency) + + // make sure the values do not interfear with the circles + var valOffset = (dataSet.circleRadius * 1.75f).toInt() + + if (!dataSet.isDrawCirclesEnabled) valOffset = valOffset / 2 + + mXBounds[chart] = dataSet + + val positions = trans!!.generateTransformedValuesLine( + dataSet, mAnimator.phaseX, mAnimator + .phaseY, mXBounds.min, mXBounds.max + ) + + val iconsOffset = MPPointF.getInstance(dataSet.iconsOffset) + iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x) + iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y) + + var j = 0 + while (j < positions.size) { + val x = positions[j] + val y = positions[j + 1] + + if (!mViewPortHandler.isInBoundsRight(x)) break + + if (!mViewPortHandler.isInBoundsLeft(x) || !mViewPortHandler.isInBoundsY(y)) { + j += 2 + continue + } + + val entry = dataSet.getEntryForIndex(j / 2 + mXBounds.min) + + if (entry != null) { + if (dataSet.isDrawValuesEnabled) { + drawValue( + c, dataSet.valueFormatter, entry.y, entry, i, x, + y - valOffset, dataSet.getValueTextColor(j / 2) + ) + } + + if (entry.icon != null && dataSet.isDrawIconsEnabled) { + val icon = entry.icon + + Utils.drawImage( + c, + icon, + (x + iconsOffset.x).toInt(), + (y + iconsOffset.y).toInt(), + icon!!.intrinsicWidth, + icon.intrinsicHeight + ) + } + } + j += 2 + } + + MPPointF.recycleInstance(iconsOffset) + } + } + } + + override fun drawExtras(c: Canvas) { + drawCircles(c) + } + + /** + * cache for the circle bitmaps of all datasets + */ + private val mImageCaches = HashMap, DataSetImageCache>() + + /** + * buffer for drawing the circles + */ + private val mCirclesBuffer = FloatArray(2) + + init { + circlePaintInner.style = Paint.Style.FILL + circlePaintInner.color = Color.WHITE + } + + protected fun drawCircles(c: Canvas) { + mRenderPaint.style = Paint.Style.FILL + + val phaseY = mAnimator.phaseY + + mCirclesBuffer[0] = 0f + mCirclesBuffer[1] = 0f + + val dataSets = chart.lineData.dataSets + + for (i in dataSets.indices) { + val dataSet = dataSets[i] + + if (!dataSet.isVisible || !dataSet.isDrawCirclesEnabled || dataSet.entryCount == 0) continue + + circlePaintInner.color = dataSet.circleHoleColor + + val trans = chart.getTransformer(dataSet.axisDependency) + + mXBounds[chart] = dataSet + + val circleRadius = dataSet.circleRadius + val circleHoleRadius = dataSet.circleHoleRadius + val drawCircleHole = dataSet.isDrawCircleHoleEnabled && circleHoleRadius < circleRadius && circleHoleRadius > 0f + val drawTransparentCircleHole = drawCircleHole && + dataSet.circleHoleColor == ColorTemplate.COLOR_NONE + + val imageCache: DataSetImageCache? + + if (mImageCaches.containsKey(dataSet)) { + imageCache = mImageCaches[dataSet] + } else { + imageCache = DataSetImageCache() + mImageCaches[dataSet] = imageCache + } + + val changeRequired = imageCache!!.init(dataSet) + + // only fill the cache with new bitmaps if a change is required + if (changeRequired) { + imageCache.fill(dataSet, drawCircleHole, drawTransparentCircleHole) + } + + val boundsRangeCount = mXBounds.range + mXBounds.min + + for (j in mXBounds.min..boundsRangeCount) { + val e = dataSet.getEntryForIndex(j) ?: break + + mCirclesBuffer[0] = e.x + mCirclesBuffer[1] = e.y * phaseY + + trans!!.pointValuesToPixel(mCirclesBuffer) + + if (!mViewPortHandler.isInBoundsRight(mCirclesBuffer[0])) break + + if (!mViewPortHandler.isInBoundsLeft(mCirclesBuffer[0]) || + !mViewPortHandler.isInBoundsY(mCirclesBuffer[1]) + ) continue + + val circleBitmap = imageCache.getBitmap(j) + + if (circleBitmap != null) { + c.drawBitmap(circleBitmap, mCirclesBuffer[0] - circleRadius, mCirclesBuffer[1] - circleRadius, null) + } + } + } + } + + override fun drawHighlighted(c: Canvas, indices: Array) { + val lineData = chart.lineData + + for (high in indices) { + val set = lineData.getDataSetByIndex(high.dataSetIndex) + + if (set == null || !set.isHighlightEnabled) continue + + val e = set.getEntryForXValue(high.x, high.y) + + if (!isInBoundsX(e, set)) continue + + val pix = chart.getTransformer(set.axisDependency)!!.getPixelForValues( + e.x, e.y * mAnimator + .phaseY + ) + + high.setDraw(pix.x.toFloat(), pix.y.toFloat()) + + // draw the lines + drawHighlightLines(c, pix.x.toFloat(), pix.y.toFloat(), set) + } + } + + var bitmapConfig: Bitmap.Config + /** + * Returns the Bitmap.Config that is used by this renderer. + * + * @return + */ + get() = mBitmapConfig + /** + * Sets the Bitmap.Config to be used by this renderer. + * Default: Bitmap.Config.ARGB_8888 + * Use Bitmap.Config.ARGB_4444 to consume less memory. + * + * @param config + */ + set(config) { + mBitmapConfig = config + releaseBitmap() + } + + /** + * Releases the drawing bitmap. This should be called when [LineChart.onDetachedFromWindow]. + */ + fun releaseBitmap() { + if (bitmapCanvas != null) { + bitmapCanvas!!.setBitmap(null) + bitmapCanvas = null + } + if (drawBitmap != null) { + val drawBitmap = drawBitmap!!.get() + drawBitmap?.recycle() + this.drawBitmap!!.clear() + this.drawBitmap = null + } + } + + private inner class DataSetImageCache { + private val mCirclePathBuffer = Path() + + private var circleBitmaps: Array? = null + + /** + * Sets up the cache, returns true if a change of cache was required. + * + * @param set + * @return + */ + fun init(set: ILineDataSet): Boolean { + val size = set.circleColorCount + var changeRequired = false + + if (circleBitmaps == null) { + circleBitmaps = arrayOfNulls(size) + changeRequired = true + } else if (circleBitmaps!!.size != size) { + circleBitmaps = arrayOfNulls(size) + changeRequired = true + } + + return changeRequired + } + + /** + * Fills the cache with bitmaps for the given dataset. + * + * @param set + * @param drawCircleHole + * @param drawTransparentCircleHole + */ + fun fill(set: ILineDataSet, drawCircleHole: Boolean, drawTransparentCircleHole: Boolean) { + val colorCount = set.circleColorCount + val circleRadius = set.circleRadius + val circleHoleRadius = set.circleHoleRadius + + for (i in 0.. 0f; float phaseX = mAnimator.getPhaseX(); float phaseY = mAnimator.getPhaseY(); - if (mChart.isDrawBarShadowEnabled()) { - mShadowPaint.setColor(dataSet.getBarShadowColor()); - BarData barData = mChart.getBarData(); + if (chart.isDrawBarShadowEnabled()) { + shadowPaint.setColor(dataSet.getBarShadowColor()); + BarData barData = chart.getBarData(); float barWidth = barData.getBarWidth(); float barWidthHalf = barWidth / 2.0f; float x; @@ -80,19 +80,19 @@ protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { if (roundedShadowRadius > 0) { - c.drawRoundRect(mBarRect, roundedShadowRadius, roundedShadowRadius, mShadowPaint); + c.drawRoundRect(barRect, roundedShadowRadius, roundedShadowRadius, shadowPaint); } else { - c.drawRect(mBarShadowRectBuffer, mShadowPaint); + c.drawRect(mBarShadowRectBuffer, shadowPaint); } i++; } } - BarBuffer buffer = mBarBuffers[index]; + BarBuffer buffer = barBuffers[index]; buffer.setPhases(phaseX, phaseY); buffer.setDataSet(index); - buffer.setInverted(mChart.isInverted(dataSet.getAxisDependency())); - buffer.setBarWidth(mChart.getBarData().getBarWidth()); + buffer.setInverted(chart.isInverted(dataSet.getAxisDependency())); + buffer.setBarWidth(chart.getBarData().getBarWidth()); buffer.feed(dataSet); trans.pointValuesToPixel(buffer.buffer); @@ -109,15 +109,15 @@ protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { break; } - if (mChart.isDrawBarShadowEnabled()) { + if (chart.isDrawBarShadowEnabled()) { if (roundedShadowRadius > 0) { c.drawRoundRect(new RectF(buffer.buffer[j], mViewPortHandler.contentTop(), buffer.buffer[j + 2], - mViewPortHandler.contentBottom()), roundedShadowRadius, roundedShadowRadius, mShadowPaint); + mViewPortHandler.contentBottom()), roundedShadowRadius, roundedShadowRadius, shadowPaint); } else { c.drawRect(buffer.buffer[j], mViewPortHandler.contentTop(), buffer.buffer[j + 2], - mViewPortHandler.contentBottom(), mShadowPaint); + mViewPortHandler.contentBottom(), shadowPaint); } } @@ -146,11 +146,11 @@ protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { break; } - if (mChart.isDrawBarShadowEnabled()) { + if (chart.isDrawBarShadowEnabled()) { if (roundedShadowRadius > 0) { c.drawRoundRect(new RectF(buffer.buffer[j], mViewPortHandler.contentTop(), buffer.buffer[j + 2], - mViewPortHandler.contentBottom()), roundedShadowRadius, roundedShadowRadius, mShadowPaint); + mViewPortHandler.contentBottom()), roundedShadowRadius, roundedShadowRadius, shadowPaint); } else { c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], buffer.buffer[j + 3], mRenderPaint); @@ -228,7 +228,7 @@ protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { @Override public void drawHighlighted(Canvas c, Highlight[] indices) { - BarData barData = mChart.getBarData(); + BarData barData = chart.getBarData(); for (Highlight high : indices) { @@ -244,7 +244,7 @@ public void drawHighlighted(Canvas c, Highlight[] indices) { continue; } - Transformer trans = mChart.getTransformer(set.getAxisDependency()); + Transformer trans = chart.getTransformer(set.getAxisDependency()); mHighlightPaint.setColor(set.getHighLightColor()); mHighlightPaint.setAlpha(set.getHighLightAlpha()); @@ -256,7 +256,7 @@ public void drawHighlighted(Canvas c, Highlight[] indices) { if (isStack) { - if (mChart.isHighlightFullBarEnabled()) { + if (chart.isHighlightFullBarEnabled()) { y1 = e.getPositiveSum(); y2 = -e.getNegativeSum(); @@ -276,10 +276,10 @@ public void drawHighlighted(Canvas c, Highlight[] indices) { prepareBarHighlight(e.getX(), y1, y2, barData.getBarWidth() / 2f, trans); - setHighlightDrawPos(high, mBarRect); + setHighlightDrawPos(high, barRect); - Path path2 = roundRect(new RectF(mBarRect.left, mBarRect.top, mBarRect.right, - mBarRect.bottom), mRadius, mRadius, true, true, true, true); + Path path2 = roundRect(new RectF(barRect.left, barRect.top, barRect.right, + barRect.bottom), mRadius, mRadius, true, true, true, true); c.drawPath(path2, mHighlightPaint); } diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RoundedHorizontalBarChartRenderer.java b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RoundedHorizontalBarChartRenderer.java index 5c113ebedc..638e328b1f 100644 --- a/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RoundedHorizontalBarChartRenderer.java +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/renderer/RoundedHorizontalBarChartRenderer.java @@ -46,17 +46,17 @@ public RoundedHorizontalBarChartRenderer(BarDataProvider chart, ChartAnimator an @Override protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { initBuffers(); - Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); - mBarBorderPaint.setColor(dataSet.getBarBorderColor()); - mBarBorderPaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getBarBorderWidth())); - mShadowPaint.setColor(dataSet.getBarShadowColor()); + Transformer trans = chart.getTransformer(dataSet.getAxisDependency()); + barBorderPaint.setColor(dataSet.getBarBorderColor()); + barBorderPaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getBarBorderWidth())); + shadowPaint.setColor(dataSet.getBarShadowColor()); boolean drawBorder = dataSet.getBarBorderWidth() > 0f; float phaseX = mAnimator.getPhaseX(); float phaseY = mAnimator.getPhaseY(); - if (mChart.isDrawBarShadowEnabled()) { - mShadowPaint.setColor(dataSet.getBarShadowColor()); - BarData barData = mChart.getBarData(); + if (chart.isDrawBarShadowEnabled()) { + shadowPaint.setColor(dataSet.getBarShadowColor()); + BarData barData = chart.getBarData(); float barWidth = barData.getBarWidth(); float barWidthHalf = barWidth / 2.0f; float x; @@ -79,19 +79,19 @@ protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { mBarShadowRectBuffer.right = mViewPortHandler.contentRight(); if (roundedShadowRadius > 0) { - c.drawRoundRect(mBarRect, roundedShadowRadius, roundedShadowRadius, mShadowPaint); + c.drawRoundRect(barRect, roundedShadowRadius, roundedShadowRadius, shadowPaint); } else { - c.drawRect(mBarShadowRectBuffer, mShadowPaint); + c.drawRect(mBarShadowRectBuffer, shadowPaint); } i++; } } - BarBuffer buffer = mBarBuffers[index]; + BarBuffer buffer = barBuffers[index]; buffer.setPhases(phaseX, phaseY); buffer.setDataSet(index); - buffer.setInverted(mChart.isInverted(dataSet.getAxisDependency())); - buffer.setBarWidth(mChart.getBarData().getBarWidth()); + buffer.setInverted(chart.isInverted(dataSet.getAxisDependency())); + buffer.setBarWidth(chart.getBarData().getBarWidth()); buffer.feed(dataSet); trans.pointValuesToPixel(buffer.buffer); @@ -106,15 +106,15 @@ protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { break; } - if (mChart.isDrawBarShadowEnabled()) { + if (chart.isDrawBarShadowEnabled()) { if (roundedShadowRadius > 0) { c.drawRoundRect(new RectF(buffer.buffer[j], mViewPortHandler.contentTop(), buffer.buffer[j + 2], - mViewPortHandler.contentBottom()), roundedShadowRadius, roundedShadowRadius, mShadowPaint); + mViewPortHandler.contentBottom()), roundedShadowRadius, roundedShadowRadius, shadowPaint); } else { c.drawRect(buffer.buffer[j], mViewPortHandler.contentTop(), buffer.buffer[j + 2], - mViewPortHandler.contentBottom(), mShadowPaint); + mViewPortHandler.contentBottom(), shadowPaint); } } @@ -141,11 +141,11 @@ protected void drawDataSet(Canvas c, IBarDataSet dataSet, int index) { break; } - if (mChart.isDrawBarShadowEnabled()) { + if (chart.isDrawBarShadowEnabled()) { if (roundedShadowRadius > 0) { c.drawRoundRect(new RectF(buffer.buffer[j], mViewPortHandler.contentTop(), buffer.buffer[j + 2], - mViewPortHandler.contentBottom()), roundedShadowRadius, roundedShadowRadius, mShadowPaint); + mViewPortHandler.contentBottom()), roundedShadowRadius, roundedShadowRadius, shadowPaint); } else { c.drawRect(buffer.buffer[j], buffer.buffer[j + 1], buffer.buffer[j + 2], buffer.buffer[j + 3], mRenderPaint);