Saturday, January 9, 2016

Android bar chart using custom view

Android lacks support for graph view. Here is my implementation of custom view showing bar chart. You can use this source file in your project. The view will appear like this.

/*
 * Copyright (c) 2016, Swapnil Chaudhari. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.poc.customviewchart;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.View;

import java.text.DecimalFormat;

public class BarChartView extends View {

    /**
     * Paint object used to draw things on canvas.
     */
    private Paint mPaint;
    /**
     * Context
     */
    private Context mContext;
    /**
     * Data used to show bar charts
     */
    private BarData[] mDataArray;
    /**
     * Maximum value in the data set
     */
    private float mMaxValueOfData;
    /**
     * width of drawing
     */
    private final int mStrokeWidth = 2;
    /**
     * Font size for legends along X and Y axis in dp.
     */
    private int mAxisFontSize = 14;
    /**
     * Count of legends shown along Y axis
     */
    private int mMaxValueCountOnYAxis = 9;
    /**
     * Distance between Axis and values shown as legend next to it (in px)
     */
    private int mDistanceAxisAndValue;
    /**
     * Maximum width of legends along Y axis
     */
    private int mMaxWidthOfYAxisText;
    /**
     * Maximum width of legends along X axis
     */
    private int mMaxHeightOfXAxisText;

    /**
     * Constuctor.
     *
     * @param context      Context
     * @param attributeSet set of attributes
     */
    public BarChartView(Context context, AttributeSet attributeSet) {

        super(context, attributeSet);
        mContext = context;
        mPaint = new Paint();
        init();
    }

    /**
     * Initialize internal variables
     */
    private void init() {

        mDistanceAxisAndValue = (int) dpToPixels(mContext, 14);
    }

    /**
     * This View will use the given data for drawing bar chart
     *
     * @param barData data to be used for drawing bar chart.
     */
    public void setYAxisData(BarData[] barData) {

        mDataArray = barData;
        mMaxValueOfData = Float.MIN_VALUE;
        for (int index = 0; index < mDataArray.length; index++) {
            if (mMaxValueOfData < mDataArray[index].getValue())
                mMaxValueOfData = mDataArray[index].getValue();
        }
        findMaxWidthOfText(barData);
        invalidate();
    }

    /**
     * Returns the maximum value in the data set.
     *
     * @return Maximum value in the data set.
     */
    public float getMaxValueOfData() {

        return mMaxValueOfData;
    }

    /**
     * Returns maximum width occupied by any of the Y axis values.
     *
     * @return maximum width occupied by any of the Y axis values
     */
    private int getMaxWidthOfYAxisText() {

        return mMaxWidthOfYAxisText;
    }

    /**
     * Calculate the maximum width occupied by any of given bar chart data. Width is calculated
     * based on default font used and size specified in {@link #mAxisFontSize}.
     *
     * @param barDatas data to be used in bar chart
     */
    private void findMaxWidthOfText(BarData[] barDatas) {

        mMaxWidthOfYAxisText = Integer.MIN_VALUE;
        mMaxHeightOfXAxisText = Integer.MIN_VALUE;

        Paint paint = new Paint();
        paint.setTypeface(Typeface.DEFAULT);
        paint.setTextSize(dpToPixels(mContext, mAxisFontSize));

        Rect bounds = new Rect();

        for (int index = 0; index < mDataArray.length; index++) {
            int currentTextWidth =
                    (int) paint.measureText(Float.toString(barDatas[index].getValue()));
            if (mMaxWidthOfYAxisText < currentTextWidth)
                mMaxWidthOfYAxisText = currentTextWidth;

            mPaint.getTextBounds(barDatas[index].getXAxisName(), 0,
                    barDatas[index].getXAxisName().length(), bounds);
            if (mMaxHeightOfXAxisText < bounds.height())
                mMaxHeightOfXAxisText = bounds.height();
        }
    }

    /**
     * Returns the maximum height of X Axis text.
     *
     * @return the maximum height of X Axis text
     */
    public int getMaxHeightOfXAxisText() {

        return mMaxHeightOfXAxisText;
    }

    @Override
    protected void onDraw(Canvas canvas) {

        int usableViewHeight = getHeight() - getPaddingBottom() - getPaddingTop();
        int usableViewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
        Point origin = getOrigin();
        mPaint.setColor(ContextCompat.getColor(mContext, R.color.axesColor));
        mPaint.setStrokeWidth(mStrokeWidth);
        //draw y axis
        canvas.drawLine(origin.x, origin.y, origin.x,
                origin.y - (usableViewHeight - getXAxisLabelAndMargin()), mPaint);
        //draw x axis
        mPaint.setStrokeWidth(mStrokeWidth + 1);
        canvas.drawLine(origin.x, origin.y,
                origin.x + usableViewWidth -
                        (getMaxWidthOfYAxisText() +
                                mDistanceAxisAndValue), origin.y, mPaint);

        if (mDataArray == null || mDataArray.length == 0)
            return;
        //draw bar chart
        int barAndVacantSpaceCount = (mDataArray.length << 1) + 1;
        int widthFactor = (usableViewWidth - getMaxWidthOfYAxisText()) / barAndVacantSpaceCount;
        int x1, x2, y1, y2;
        float maxValue = getMaxValueOfData();
        for (int index = 0; index < mDataArray.length; index++) {
            x1 = origin.x + ((index << 1) + 1) * widthFactor;
            x2 = origin.x + ((index << 1) + 2) * widthFactor;
            int barHeight = (int) ((usableViewHeight - getXAxisLabelAndMargin()) *
                    mDataArray[index].getValue() / maxValue);
            y1 = origin.y - barHeight;
            y2 = origin.y;
            canvas.drawRect(x1, y1, x2, y2, mPaint);
            showXAxisLabel(origin, mDataArray[index].getXAxisName(), x1 + (x2 - x1) / 2, canvas);
        }
        showYAxisLabels(origin, (usableViewHeight - getXAxisLabelAndMargin()), canvas);
    }

    /**
     * Formats the given float value up to one decimal precision point.
     *
     * @param value float which needs to be formatted
     *
     * @return String in the format "0.0" e.g. 100.0, 11.1
     * 

*
TODO make it as interface so that developers can implement interface and use this code. */ private String getFormattedValue(float value) { DecimalFormat precision = new DecimalFormat("0.0"); return precision.format(value); } /** * Draws Y axis labels and marker points along Y axis. * * @param origin coordinates of origin on canvas * @param usableViewHeight view height after removing the padding * @param canvas canvas to draw the chart */ public void showYAxisLabels(Point origin, int usableViewHeight, Canvas canvas) { float maxValueOfData = (int) getMaxValueOfData(); float yAxisValueInterval = usableViewHeight / mMaxValueCountOnYAxis; float dataInterval = maxValueOfData / mMaxValueCountOnYAxis; float valueToBeShown = maxValueOfData; mPaint.setTypeface(Typeface.DEFAULT); mPaint.setTextSize(dpToPixels(mContext, mAxisFontSize)); //draw all texts from top to bottom for (int index = 0; index < mMaxValueCountOnYAxis; index++) { String string = getFormattedValue(valueToBeShown); Rect bounds = new Rect(); mPaint.getTextBounds(string, 0, string.length(), bounds); int y = (int) ((origin.y - usableViewHeight) + yAxisValueInterval * index); canvas.drawLine(origin.x - (mDistanceAxisAndValue >> 1), y, origin.x, y, mPaint); y = y + (bounds.height() >> 1); canvas.drawText(string, origin.x - bounds.width() - mDistanceAxisAndValue, y, mPaint); valueToBeShown = valueToBeShown - dataInterval; } } /** * Draws X axis labels. * * @param origin coordinates of origin on canvas * @param label label to be drawn below a bar along X axis * @param centerX center x coordinate of the given bar * @param canvas canvas to draw the chart */ public void showXAxisLabel(Point origin, String label, int centerX, Canvas canvas) { Rect bounds = new Rect(); mPaint.getTextBounds(label, 0, label.length(), bounds); int y = origin.y + mDistanceAxisAndValue + getMaxHeightOfXAxisText(); int x = centerX - bounds.width() / 2; mPaint.setTextSize(dpToPixels(mContext, mAxisFontSize)); mPaint.setTypeface(Typeface.DEFAULT); canvas.drawText(label, x, y, mPaint); } /** * Returns the X axis' maximum label height and margin between label and the X axis. * * @return the X axis' maximum label height and margin between label and the X axis */ private int getXAxisLabelAndMargin() { return getMaxHeightOfXAxisText() + mDistanceAxisAndValue; } /** * Returns the origin coordinates in canvas' coordinates. * * @return origin's coordinates */ public Point getOrigin() { if (mDataArray != null) { return new Point(getPaddingLeft() + getMaxWidthOfYAxisText() + mDistanceAxisAndValue, getHeight() - getPaddingBottom() - getXAxisLabelAndMargin()); } else { return new Point(getPaddingLeft() + getMaxWidthOfYAxisText() + mDistanceAxisAndValue, getHeight() - getPaddingBottom()); } } /** * Convert dp value to pixels. * * @param context Context * @param dpValue Value in dip * * @return Values in pixels */ public static float dpToPixels(Context context, float dpValue) { if (context != null) { DisplayMetrics metrics = context.getResources().getDisplayMetrics(); return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, metrics); } return 0; } }


This view can be used in layout.xml as shown below.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:paddingBottom="@dimen/activity_vertical_margin"
 android:paddingLeft="@dimen/activity_horizontal_margin"
 android:paddingRight="@dimen/activity_horizontal_margin"
 android:paddingTop="@dimen/activity_vertical_margin"
 app:layout_behavior="@string/appbar_scrolling_view_behavior"
 tools:context="com.poc.customviewchart.MainActivity"
 tools:showIn="@layout/activity_main">
    <com.poc.customviewchart.BarChartView
        android:id="@+id/barChart"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:background="@color/colorPrimaryDark"
        android:padding="16dp"/>
</RelativeLayout>

From activity it can be called as shown below.

BarChartView barChartView = (BarChartView) findViewById(R.id.barChart);
ArrayList arrayList = new ArrayList<>();
BarData barData = new BarData("Jan", 11.1f);
arrayList.add(barData);
barData = new BarData("Feb", 8f);
arrayList.add(barData);
barData = new BarData("Mar", 20f);
arrayList.add(barData);
barData = new BarData("Apr", 43f);
arrayList.add(barData);
barData = new BarData("May", 99f);
arrayList.add(barData);
barData = new BarData("Jun", 12.5f);
arrayList.add(barData);
/*float[] arr =
  new float[]{11.1f, 8f, 100f, 7.5f, 3.5f, 2.5f, 10f*//*, 100f, 7.5f, 3.5f, 2.5f, 10f,
    100f, 7.5f, 3.5f,75f,50f,25f, 7.5f, 3.5f*//*};*/
BarData[] arrayBarData = arrayList.toArray(new BarData[arrayList.size()]);
barChartView.setYAxisData(arrayBarData);

BarData is a simple java bean holding data of graph.
public class BarData {

    private String mXAxisName;
    private float mValue;

    public BarData(String XAxisName, float value) {

        mXAxisName = XAxisName;
        mValue = value;
    }

    public String getXAxisName() {

        return mXAxisName;
    }

    public void setXAxisName(String XAxisName) {

        mXAxisName = XAxisName;
    }

    public float getValue() {

        return mValue;
    }

    public void setValue(float value) {

        mValue = value;
    }
}
Let me know if you need any help customizing it.

2 comments:

  1. hey
    i created a BarView class, BarData and a myActivity class, but I get an Error saying cannot resolve R in the mainactivity and bar view
    i would appreciate any help thanks

    ReplyDelete
  2. R is auto generated if you don't have any other errors. Check all errors and see if they are related to R only, then clean you project. It should generate the R file.

    ReplyDelete

Android aar deployment in Maven - 2022

Introduction If you are working on android library project, you might be wondering how to publish it on Maven like this . Earl...