I have created a LineChart
using the library MPAndroidChart and everything works great.
Now what I want to do is show a drawable (image) instead of the
I also came across this question, but had a bit more specific requirements:
Finally, I managed to achieve this:
Where each circle is actually a regular drawable and can be replaced with anything else.
Solved it in a next way:
1.Create an Entry subclass which takes a drawable as a parameter.
/**
* Represents an [Entry] which is able to use drawables (including different drawables for different points) instead of the circle.
* For the points where you don't need points use a regular [Entry].
*/
class DrawableCircleEntry @JvmOverloads constructor(
@DrawableRes val circleDrawableRes: Int,
x: Float,
y: Float,
icon: Drawable? = null,
data: Any? = null
) : Entry(x, y, icon, data)
2.Create a custom rendered, which
draws the drawable instead of the circle in case entry is a type of DrawableCircleEntry.
Doesn't draw the circle in case try is a regular Entry.
internal class LineChartCustomCirclesRenderer(private val context: Context, lineChart: LineChart
) : LineChartRenderer(lineChart, lineChart.animator, lineChart.viewPortHandler) {
// Contains (left, top) coordinates of the next circle which has to be drawn
private val circleCoordinates = FloatArray(2)
// Cached drawables
private val drawablesCache = SparseArray()
override fun drawCircles(canvas: Canvas) {
val phaseY = mAnimator.phaseY
circleCoordinates[0] = 0f
circleCoordinates[1] = 0f
val dataSets = mChart.lineData.dataSets
dataSets.forEach { dataSet ->
if (!dataSet.isVisible || !dataSet.isDrawCirclesEnabled || dataSet.entryCount == 0)
return@forEach
val transformer = mChart.getTransformer(dataSet.axisDependency)
mXBounds[mChart] = dataSet
val boundsRangeCount = mXBounds.range + mXBounds.min
for (i in mXBounds.min..boundsRangeCount) {
// don't do anything in case entry is not type of DrawableCircleEntry
val entry = dataSet.getEntryForIndex(i) as? DrawableCircleEntry
?: continue
circleCoordinates[0] = entry.x
circleCoordinates[1] = entry.y * phaseY
transformer.pointValuesToPixel(circleCoordinates)
if (!mViewPortHandler.isInBoundsRight(circleCoordinates[0])) break
if (!mViewPortHandler.isInBoundsLeft(circleCoordinates[0]) || !mViewPortHandler.isInBoundsY(circleCoordinates[1])) continue
// Drawable radius is taken as `dataSet.circleRadius`
val radius = dataSet.circleRadius
// Retrieve the drawable, center it and draw on canvas
getDrawable(entry.circleDrawableRes)?.run {
setBounds((circleCoordinates[0] - radius).roundToInt(), (circleCoordinates[1] - radius).roundToInt(),
(circleCoordinates[0] + radius).roundToInt(), (circleCoordinates[1] + radius).roundToInt())
draw(canvas)
}
}
}
}
private fun getDrawable(@DrawableRes drawableRes: Int): Drawable? {
drawablesCache[drawableRes]?.let {
return it
}
return ContextCompat.getDrawable(context, drawableRes)
.also { drawablesCache.append(drawableRes, it) }
}
}
3.Enable circles for the dataset and set the needed radius. The drawable's size will be radius*2
dataSet.setDrawCircles(true)
dataSet.circleRadius = 3f
4.When constructing Entries, create either normal Entry for a point where you don't need to draw a circle and a DrawableCircleEntry when you need one. For instance,
...
val entry = when {
someCondition -> DrawableCircleEntry(R.drawable.your_awesome_drawable, floatIndex, floatValue)
anotherCondition -> DrawableCircleEntry(R.drawable.your_another_drawable, floatIndex, floatValue)
else -> Entry(floatIndex, floatValue)
}
...
One of the drawables in my case looks like:
-
But it can be any other.
Enjoy.