Tushar H
Tushar H

Reputation: 805

Show image instead of circle on LineChart

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 default circle for every entry on the chart.

I have tried so many options from the API but no luck.

Can anyone tell me how can I do this?

Upvotes: 1

Views: 2854

Answers (3)

Leo DroidCoder
Leo DroidCoder

Reputation: 15046

I also came across this question, but had a bit more specific requirements:

  1. Draw the circles only at specific points on the chart.
  2. Have the flexibility to use different drawables in different cases.

Finally, I managed to achieve this:

enter image description here

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<Drawable>()
    
      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)
    }
    ...
  1. One of the drawables in my case looks like:

<item>
    <shape
        android:shape="ring"
        android:thickness="@dimen/chart_circle_stroke_thickness"
        android:useLevel="false">
        <solid android:color="#497EFF" />
    </shape>
</item>

But it can be any other.

Enjoy.

Upvotes: 1

Eugene Shtoka
Eugene Shtoka

Reputation: 1208

You can simply create entry with drawable, and it will be drawn instead of circle on graph.

new Entry(i, value, drawable)

Upvotes: 3

Tushar H
Tushar H

Reputation: 805

And finally after trying so many things, with the help of @David Rawson's suggestion and this post MPAndroidChart LineChart custom highlight drawable

I have managed to create a custom renderer, which replaces the default circle image in chart with the provided image.

Following is the code snippet of solution.

class ImageLineChartRenderer extends LineChartRenderer {
private final LineChart lineChart;
private final Bitmap image;


ImageLineChartRenderer(LineChart chart, ChartAnimator animator, ViewPortHandler viewPortHandler, Bitmap image) {
    super(chart, animator, viewPortHandler);
    this.lineChart = chart;
    this.image = image;
}

private float[] mCirclesBuffer = new float[2];

@Override
protected void drawCircles(Canvas c) {
    mRenderPaint.setStyle(Paint.Style.FILL);
    float phaseY = mAnimator.getPhaseY();
    mCirclesBuffer[0] = 0;
    mCirclesBuffer[1] = 0;
    List<ILineDataSet> dataSets = mChart.getLineData().getDataSets();

    //Draw bitmap image for every data set with size as radius * 10, and store it in scaled bitmaps array
    Bitmap[] scaledBitmaps = new Bitmap[dataSets.size()];
    float[] scaledBitmapOffsets = new float[dataSets.size()];
    for (int i = 0; i < dataSets.size(); i++) {
        float imageSize = dataSets.get(i).getCircleRadius() * 10;
        scaledBitmapOffsets[i] = imageSize / 2f;
        scaledBitmaps[i] = scaleImage((int) imageSize);
    }

    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);


        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;

            if (scaledBitmaps[i] != null) {
                c.drawBitmap(scaledBitmaps[i],
                        mCirclesBuffer[0] - scaledBitmapOffsets[i],
                        mCirclesBuffer[1] - scaledBitmapOffsets[i],
                        mRenderPaint);
            }
        }
    }

}


private Bitmap scaleImage(int radius) {
    return Bitmap.createScaledBitmap(image, radius, radius, false);
}

Hope this helps someone.

Upvotes: 5

Related Questions