Android Tutorial- Introduction to AFreeChart with Sample


business chart showing success

Today’s Android based business apps are incomplete without graphical aids like graphs/charts. Therefore in this tutorial I would be introducing a free charting library called AFreeChart, for Android platform. This library is based on JFreeChart 1.0.13. Although Android AFreeChart project is not being maintained at present, still its very easy to include this library in an Android project and create beautiful graph/charts effortlessly.

This Android tutorial- Introduction to AFreeChart with sample is a part of my series of tutorials on creating chart/graphs on Android. In my previous tutorial I stated that there are many ways to show charts on android but I would be showing only 5 most popular ways to display graphs on Android:

  1. Google Chart Tools- Latest Chart Tools – Android Tutorial- Google Chart Tools
  2. Google Chart Tools: Image Tools- Deprecated – Android Tutorial- Google Chart Tools Image API
  3. AFreeChart- Android Tutorial- Introduction to AFreeChart with Sample
  4. AChartEngine- AChartEngine Tutorial
  5. MPAndroidChart – Android MP Chart Library

As stated earlier this Android tutorial focuses on introduction to AFreeChart with sample. So lets start with a basic introduction to Android AFreeChart library.

AFreeChart library for Android is ported over time from famous JFreeChart 1.0.13 library for java, many leading projects like JBoss, Sports Tracker etc. are using this library. As we all know mobile apps are built with a mindset to provide us information which is absolutely necessary to us when we are on move, therefore Android AFreeChart library is ported with very basic and absolutely necessary charts. Although Android AFreeChart library has the functionality to draw 15 types of charts: Bar Charts, Overlaid Charts, Legends, Multiple Axis Charts, Financial Charts, Pie Charts, Miscellaneous Charts, Markers, Showcase Charts, Scatter Charts, Statistical Charts, Combine Charts, Line Charts, Annotations, and Vectors, but since this tutorial is a basic introduction to Android AFreeChart library we are only going to discuss Bar Charts.

The lowest Andorid OS version supported by AFreeChart library is 2.1. I am building this project for 4.1.2, for this I had to make minor changes in their demo code .I would point them out later in the tutorial. To create an app with Android AFreeChart we have to include a .jar file in our project by configuring the build path. This jar could be downloaded from official Google project page: afreechart-0.0.4.jar.

To draw a graph with Android AFreeChart library we have to create a view by code, in which the graph is actually drawn, and utilize that view in our class. Drawing a view is a very complicated task, way beyond the limits of a beginner Android programmer. This includes calling various methods from the AFreeChart library and android graphics libraries. Hence I took an easier path by including the DemoView.java from the demo code provided by good AFreeChart library people. But since we are compiling this app for later versions of android, two minor changes had to be done in their code, have a look I have highlighted them:

/* ===========================================================
 * AFreeChart : a free chart library for Android(tm) platform.
 *              (based on JFreeChart and JCommon)
 * ===========================================================
 *
 * (C) Copyright 2010, by ICOMSYSTECH Co.,Ltd.
 * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
 *
 * Project Info:
 *    AFreeChart: http://code.google.com/p/afreechart/
 *    JFreeChart: http://www.jfree.org/jfreechart/index.html
 *    JCommon   : http://www.jfree.org/jcommon/index.html
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * [Android is a trademark of Google Inc.]
 *
 * -----------------
 * ChartView.java
 * -----------------
 * (C) Copyright 2010, by ICOMSYSTECH Co.,Ltd.
 *
 * Original Author:  Niwano Masayoshi (for ICOMSYSTECH Co.,Ltd);
 * Contributor(s):   Mohit Gupt;
 *
 * Changes
 * -------
 * 19-Nov-2010 : Version 0.0.1 (NM);
 * 14-Jan-2011 : renamed method name
 * 14-Jan-2011 : Updated API docs
 * 06-Apr-2013 : Updated return types of getScaleX and getScaleY methods
 */ 

package com.truiton.myafreechart;

import org.afree.ui.RectangleInsets;
import org.afree.chart.ChartTouchEvent;
import org.afree.chart.ChartTouchListener;
import org.afree.chart.ChartRenderingInfo;
import org.afree.chart.AFreeChart;
import org.afree.chart.entity.ChartEntity;
import org.afree.chart.entity.EntityCollection;
import org.afree.chart.event.ChartChangeEvent;
import org.afree.chart.event.ChartChangeListener;
import org.afree.chart.event.ChartProgressEvent;
import org.afree.chart.event.ChartProgressListener;
import org.afree.chart.plot.Movable;
import org.afree.chart.plot.Plot;
import org.afree.chart.plot.PlotOrientation;
import org.afree.chart.plot.PlotRenderingInfo;
import org.afree.chart.plot.Zoomable;
import org.afree.graphics.geom.Dimension;
import org.afree.graphics.geom.RectShape;
import org.afree.graphics.SolidColor;

import java.util.EventListener;
import java.util.concurrent.CopyOnWriteArrayList;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PointF;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

public class DemoView extends View 
    implements ChartChangeListener, ChartProgressListener{

    /** The user interface thread handler. */
    private Handler mHandler;

    public DemoView(Context context) {
        super(context);
        mHandler = new Handler();
        this.initialize();
    }

    public DemoView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mHandler = new Handler();
        this.initialize();
    }

    /**
     * initialize parameters
     */
    private void initialize() {
        this.chartMotionListeners = new CopyOnWriteArrayList<ChartTouchListener>();
        this.info = new ChartRenderingInfo();
        this.minimumDrawWidth = DEFAULT_MINIMUM_DRAW_WIDTH;
        this.minimumDrawHeight = DEFAULT_MINIMUM_DRAW_HEIGHT;
        this.maximumDrawWidth = DEFAULT_MAXIMUM_DRAW_WIDTH;
        this.maximumDrawHeight = DEFAULT_MAXIMUM_DRAW_HEIGHT;
        this.moveTriggerDistance = DEFAULT_MOVE_TRIGGER_DISTANCE;
        new SolidColor(Color.BLUE);
        new SolidColor(Color.argb(0, 0, 255, 63));
        new java.util.ArrayList();
    }

    /**
     * Default setting for buffer usage.  The default has been changed to
     * <code>true</code> from version 1.0.13 onwards, because of a severe
     * performance problem with drawing the zoom RectShape using XOR (which
     * now happens only when the buffer is NOT used).
     */
    public static final boolean DEFAULT_BUFFER_USED = true;

    /** The default panel width. */
    public static final int DEFAULT_WIDTH = 680;

    /** The default panel height. */
    public static final int DEFAULT_HEIGHT = 420;

    /** The default limit below which chart scaling kicks in. */
    public static final int DEFAULT_MINIMUM_DRAW_WIDTH = 10;

    /** The default limit below which chart scaling kicks in. */
    public static final int DEFAULT_MINIMUM_DRAW_HEIGHT = 10;

    /** The default limit above which chart scaling kicks in. */
    public static final int DEFAULT_MAXIMUM_DRAW_WIDTH = 1280;

    /** The default limit above which chart scaling kicks in. */
    public static final int DEFAULT_MAXIMUM_DRAW_HEIGHT = 1000;

    /** The minimum size required to perform a zoom on a RectShape */
    public static final int DEFAULT_ZOOM_TRIGGER_DISTANCE = 10;

    /** The minimum size required to perform a move on a RectShape */
    public static final int DEFAULT_MOVE_TRIGGER_DISTANCE = 10;

    /** The chart that is displayed in the panel. */
    private AFreeChart chart;

    /** Storage for registered (chart) touch listeners. */
    private transient CopyOnWriteArrayList<ChartTouchListener> chartMotionListeners;

    /** The drawing info collected the last time the chart was drawn. */
    private ChartRenderingInfo info;

    /** The scale factor used to draw the chart. */
    private double scaleX;

    /** The scale factor used to draw the chart. */
    private double scaleY;

    /** The plot orientation. */
    private PlotOrientation orientation = PlotOrientation.VERTICAL;

    /**
     * The zoom RectShape starting point (selected by the user with touch).
     * This is a point on the screen, not the chart (which may have
     * been scaled up or down to fit the panel).
     */
    private PointF zoomPoint = null;

    /** Controls if the zoom RectShape is drawn as an outline or filled. */
//    private boolean fillZoomRectShape = true;

    private int moveTriggerDistance;

    /** The last touch position during panning. */
//    private Point panLast;

    private RectangleInsets insets = null;

    /**
     * The minimum width for drawing a chart (uses scaling for smaller widths).
     */
    private int minimumDrawWidth;

    /**
     * The minimum height for drawing a chart (uses scaling for smaller
     * heights).
     */
    private int minimumDrawHeight;

    /**
     * The maximum width for drawing a chart (uses scaling for bigger
     * widths).
     */
    private int maximumDrawWidth;

    /**
     * The maximum height for drawing a chart (uses scaling for bigger
     * heights).
     */
    private int maximumDrawHeight;

    private Dimension size = null;

    /** The chart anchor point. */
    private PointF anchor;

    /** A flag that controls whether or not domain moving is enabled. */
    private boolean domainMovable = false;

    /** A flag that controls whether or not range moving is enabled. */
    private boolean rangeMovable = false;

    private double accelX, accelY;
    private double friction = 0.8;
    private boolean inertialMovedFlag = false;
    private PointF lastTouch;

    private float mScale = 1.0f;

    private long mPrevTimeMillis = 0;
    private long mNowTimeMillis = System.currentTimeMillis();

    /**
     * touch event
     */
    public boolean onTouchEvent(MotionEvent ev) {

        super.onTouchEvent(ev);
        int action = ev.getAction();
        int count = ev.getPointerCount();

        this.anchor = new PointF(ev.getX(), ev.getY());

        if (this.info != null) {
            EntityCollection entities = this.info.getEntityCollection();
            if (entities != null) {
            }
        }

        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
                Log.i("TouchEvent", "ACTION_DOWN");
                if(count == 2 && this.multiTouchStartInfo == null) {
                    setMultiTouchStartInfo(ev);
                } else if (count == 1 && this.singleTouchStartInfo == null) {
                    setSingleTouchStartInfo(ev);
                }

                touched(ev);

                break;
            case MotionEvent.ACTION_MOVE:
                Log.i("TouchEvent", "ACTION_MOVE");
                if(count == 1 && this.singleTouchStartInfo != null) {
                    moveAdjustment(ev);
                } else if (count == 2 && this.multiTouchStartInfo != null) {
                    //scaleAdjustment(ev);    
                    zoomAdjustment(ev);
                }

                inertialMovedFlag = false;

                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                Log.i("TouchEvent", "ACTION_UP");
                if(count <= 2) {
                    this.multiTouchStartInfo = null;
                    this.singleTouchStartInfo = null;
                } 
                if (count <= 1) {
                    this.singleTouchStartInfo = null;
                }

                // double click check
                if (count == 1) {
                    mNowTimeMillis = System.currentTimeMillis();
                    if(mNowTimeMillis - mPrevTimeMillis < 400) {
                        if(chart.getPlot() instanceof Movable) {
                            restoreAutoBounds();
                            mScale = 1.0f;
                            inertialMovedFlag = false;
                        }
                    } else {
                        inertialMovedFlag = true;
                    }
                    mPrevTimeMillis = mNowTimeMillis;
                }
                break;
            default:
                break;
        }

        return true;
    }

    /**
     * MultiTouchStartInfo setting
     * @param ev
     */
    private void setMultiTouchStartInfo(MotionEvent ev) {

        if(this.multiTouchStartInfo == null) {
            this.multiTouchStartInfo = new MultiTouchStartInfo();
        }

        // distance
        double distance = Math.sqrt(Math.pow(ev.getX(0) - ev.getX(1),2) + Math.pow(ev.getY(0) - ev.getY(1),2));
        this.multiTouchStartInfo.setDistance(distance);        
    }

    /**
     * SingleTouchStartInfo setting
     * @param ev
     */
    private void setSingleTouchStartInfo(MotionEvent ev) {

        if(this.singleTouchStartInfo == null) {
            this.singleTouchStartInfo = new SingleTouchStartInfo();
        }

        // start point
        this.singleTouchStartInfo.setX(ev.getX(0));
        this.singleTouchStartInfo.setY(ev.getY(0));
    }

    /**
     * Translate MotionEvent as TouchEvent
     * @param ev
     */
    private void moveAdjustment(MotionEvent ev) {

        boolean hMove = false;
        boolean vMove = false;
        if (this.orientation == PlotOrientation.HORIZONTAL) {
            hMove = this.rangeMovable;
            vMove = this.domainMovable;
        }
        else {
            hMove = this.domainMovable;
            vMove = this.rangeMovable;
        }

        boolean moveTrigger1 = hMove && Math.abs(ev.getX(0) - this.singleTouchStartInfo.getX() ) >= this.moveTriggerDistance;
        boolean moveTrigger2 = vMove && Math.abs(ev.getY(0) - this.singleTouchStartInfo.getY()) >= this.moveTriggerDistance;
        if (moveTrigger1 || moveTrigger2) {

            RectShape dataArea = this.info.getPlotInfo().getDataArea();

            double moveBoundX;
            double moveBoundY;
            double dataAreaWidth = dataArea.getWidth();
            double dataAreaHeight = dataArea.getHeight();

            // for touchReleased event, (horizontalZoom || verticalZoom)
            // will be true, so we can just test for either being false;
            // otherwise both are true

            if (!vMove) {
                moveBoundX = this.singleTouchStartInfo.getX() - ev.getX(0);
                moveBoundY = 0;
            }
            else if (!hMove) {
                moveBoundX = 0;
                moveBoundY = this.singleTouchStartInfo.getY() - ev.getY(0);
            }
            else {
                moveBoundX = this.singleTouchStartInfo.getX() - ev.getX(0);
                moveBoundY = this.singleTouchStartInfo.getY() - ev.getY(0);
            }
            accelX = moveBoundX;
            accelY = moveBoundY;

            lastTouch = new PointF(ev.getX(0), ev.getY(0));
            move(lastTouch, moveBoundX,moveBoundY,dataAreaWidth,dataAreaHeight);

        }

        setSingleTouchStartInfo(ev);
    }

    /**
     * 
     * @param moveBoundX
     * @param moveBoundY
     * @param dataAreaWidth
     * @param dataAreaHeight
     */
    private void move(PointF source, double moveBoundX, double moveBoundY,
            double dataAreaWidth, double dataAreaHeight) {

        if (source == null) {
            throw new IllegalArgumentException("Null 'source' argument");
        }

        double hMovePercent = moveBoundX / dataAreaWidth;
        double vMovePercent = - moveBoundY / dataAreaHeight;

        Plot p = this.chart.getPlot();
        if (p instanceof Movable) {
            PlotRenderingInfo info = this.info.getPlotInfo();
            // here we tweak the notify flag on the plot so that only
            // one notification happens even though we update multiple
            // axes...
            //boolean savedNotify = p.isNotify();
            //p.setNotify(false);
            Movable z = (Movable) p;
            if (z.getOrientation() == PlotOrientation.HORIZONTAL) {
                z.moveDomainAxes(vMovePercent, info, source);
                z.moveRangeAxes(hMovePercent, info, source);
            }
            else {
                z.moveDomainAxes(hMovePercent, info, source);
                z.moveRangeAxes(vMovePercent, info, source);
            }
            //p.setNotify(savedNotify);

            //repaint
            invalidate();
        }

    }

    /**
     * Restores the auto-range calculation on both axes.
     */
    public void restoreAutoBounds() {
        Plot plot = this.chart.getPlot();
        if (plot == null) {
            return;
        }
        // here we tweak the notify flag on the plot so that only
        // one notification happens even though we update multiple
        // axes...
        //boolean savedNotify = plot.isNotify();
        //plot.setNotify(false);
        restoreAutoDomainBounds();
        restoreAutoRangeBounds();
        //plot.setNotify(savedNotify);
    }

    /**
     * Restores the auto-range calculation on the domain axis.
     */
    public void restoreAutoDomainBounds() {
        Plot plot = this.chart.getPlot();
        if (plot instanceof Zoomable) {
            Zoomable z = (Zoomable) plot;
            // here we tweak the notify flag on the plot so that only
            // one notification happens even though we update multiple
            // axes...
            //boolean savedNotify = plot.isNotify();
            //plot.setNotify(false);
            // we need to guard against this.zoomPoint being null
            PointF zp = (this.zoomPoint != null
                    ? this.zoomPoint : new PointF());
            z.zoomDomainAxes(0.0, this.info.getPlotInfo(), zp);
            //plot.setNotify(savedNotify);
        }
    }

    /**
     * Restores the auto-range calculation on the range axis.
     */
    public void restoreAutoRangeBounds() {
        Plot plot = this.chart.getPlot();
        if (plot instanceof Zoomable) {
            Zoomable z = (Zoomable) plot;
            // here we tweak the notify flag on the plot so that only
            // one notification happens even though we update multiple
            // axes...
            //boolean savedNotify = plot.isNotify();
            //plot.setNotify(false);
            // we need to guard against this.zoomPoint being null
            PointF zp = (this.zoomPoint != null
                    ? this.zoomPoint : new PointF());
            z.zoomRangeAxes(0.0, this.info.getPlotInfo(), zp);
            //plot.setNotify(savedNotify);
        }
    }

    protected void onSizeChanged(int w, int h, int oldw, int oldh){
        this.insets = new RectangleInsets(0,0,0,0);
        this.size = new Dimension(w, h);
    }

    private RectangleInsets getInsets() {
        return this.insets;
    }

    /**
     * Returns the X scale factor for the chart.  This will be 1.0 if no
     * scaling has been used.
     *
     * @return The scale factor.
     */
    public float getScaleX() {
        return (float) this.scaleX;
    }

    /**
     * Returns the Y scale factory for the chart.  This will be 1.0 if no
     * scaling has been used.
     *
     * @return The scale factor.
     */
    public float getScaleY() {
        return (float) this.scaleY;
    }

    /**
     * Sets the chart that is displayed in the panel.
     *
     * @param chart  the chart (<code>null</code> permitted).
     */
    public void setChart(AFreeChart chart) {

        // stop listening for changes to the existing chart
        if (this.chart != null) {
            this.chart.removeChangeListener(this);
            this.chart.removeProgressListener(this);
        }

        // add the new chart
        this.chart = chart;
        if (chart != null) {
            this.chart.addChangeListener(this);
            this.chart.addProgressListener(this);
            Plot plot = chart.getPlot();
            if (plot instanceof Zoomable) {
                Zoomable z = (Zoomable) plot;
                z.isRangeZoomable();
                this.orientation = z.getOrientation();
            }

            this.domainMovable = false;
            this.rangeMovable = false;
            if (plot instanceof Movable) {
                Movable z = (Movable) plot;
                this.domainMovable = z.isDomainMovable();
                this.rangeMovable = z.isRangeMovable();
                this.orientation = z.getOrientation();
            }
        }
        else {
            this.domainMovable = false;
            this.rangeMovable = false;
        }
//        if (this.useBuffer) {
//            this.refreshBuffer = true;
//        }
        repaint();

    }

    /**
     * Returns the minimum drawing width for charts.
     * <P>
     * If the width available on the panel is less than this, then the chart is
     * drawn at the minimum width then scaled down to fit.
     *
     * @return The minimum drawing width.
     */
    public int getMinimumDrawWidth() {
        return this.minimumDrawWidth;
    }

    /**
     * Sets the minimum drawing width for the chart on this panel.
     * <P>
     * At the time the chart is drawn on the panel, if the available width is
     * less than this amount, the chart will be drawn using the minimum width
     * then scaled down to fit the available space.
     *
     * @param width  The width.
     */
    public void setMinimumDrawWidth(int width) {
        this.minimumDrawWidth = width;
    }

    /**
     * Returns the maximum drawing width for charts.
     * <P>
     * If the width available on the panel is greater than this, then the chart
     * is drawn at the maximum width then scaled up to fit.
     *
     * @return The maximum drawing width.
     */
    public int getMaximumDrawWidth() {
        return this.maximumDrawWidth;
    }

    /**
     * Sets the maximum drawing width for the chart on this panel.
     * <P>
     * At the time the chart is drawn on the panel, if the available width is
     * greater than this amount, the chart will be drawn using the maximum
     * width then scaled up to fit the available space.
     *
     * @param width  The width.
     */
    public void setMaximumDrawWidth(int width) {
        this.maximumDrawWidth = width;
    }

    /**
     * Returns the minimum drawing height for charts.
     * <P>
     * If the height available on the panel is less than this, then the chart
     * is drawn at the minimum height then scaled down to fit.
     *
     * @return The minimum drawing height.
     */
    public int getMinimumDrawHeight() {
        return this.minimumDrawHeight;
    }

    /**
     * Sets the minimum drawing height for the chart on this panel.
     * <P>
     * At the time the chart is drawn on the panel, if the available height is
     * less than this amount, the chart will be drawn using the minimum height
     * then scaled down to fit the available space.
     *
     * @param height  The height.
     */
    public void setMinimumDrawHeight(int height) {
        this.minimumDrawHeight = height;
    }

    /**
     * Returns the maximum drawing height for charts.
     * <P>
     * If the height available on the panel is greater than this, then the
     * chart is drawn at the maximum height then scaled up to fit.
     *
     * @return The maximum drawing height.
     */
    public int getMaximumDrawHeight() {
        return this.maximumDrawHeight;
    }

    /**
     * Sets the maximum drawing height for the chart on this panel.
     * <P>
     * At the time the chart is drawn on the panel, if the available height is
     * greater than this amount, the chart will be drawn using the maximum
     * height then scaled up to fit the available space.
     *
     * @param height  The height.
     */
    public void setMaximumDrawHeight(int height) {
        this.maximumDrawHeight = height;
    }

    /**
     * Returns the chart rendering info from the most recent chart redraw.
     *
     * @return The chart rendering info.
     */
    public ChartRenderingInfo getChartRenderingInfo() {
        return this.info;
    }

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        inertialMove();

        paintComponent(canvas);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * Paints the component by drawing the chart to fill the entire component,
     * but allowing for the insets (which will be non-zero if a border has been
     * set for this component).  To increase performance (at the expense of
     * memory), an off-screen buffer image can be used.
     *
     * @param canvas  the graphics device for drawing on.
     */
    public void paintComponent(Canvas canvas) {

        // first determine the size of the chart rendering area...
        Dimension size = getSize();
        RectangleInsets insets = getInsets();
        RectShape available = new RectShape(insets.getLeft(), insets.getTop(),
                size.getWidth() - insets.getLeft() - insets.getRight(),
                size.getHeight() - insets.getTop() - insets.getBottom());

        double drawWidth = available.getWidth();
        double drawHeight = available.getHeight();
        this.scaleX = 1.0;
        this.scaleY = 1.0;

        if (drawWidth < this.minimumDrawWidth) {
            this.scaleX = drawWidth / this.minimumDrawWidth;
            drawWidth = this.minimumDrawWidth;
        }
        else if (drawWidth > this.maximumDrawWidth) {
            this.scaleX = drawWidth / this.maximumDrawWidth;
            drawWidth = this.maximumDrawWidth;
        }

        if (drawHeight < this.minimumDrawHeight) {
            this.scaleY = drawHeight / this.minimumDrawHeight;
            drawHeight = this.minimumDrawHeight;
        }
        else if (drawHeight > this.maximumDrawHeight) {
            this.scaleY = drawHeight / this.maximumDrawHeight;
            drawHeight = this.maximumDrawHeight;
        }

        RectShape chartArea = new RectShape(0.0, 0.0, drawWidth,
                drawHeight);

        // are we using the chart buffer?
//        if (this.useBuffer) {
//
//            // do we need to resize the buffer?
//            if ((this.chartBuffer == null)
//                    || (this.chartBufferWidth != available.getWidth())
//                    || (this.chartBufferHeight != available.getHeight())) {
//                this.chartBufferWidth = (int) available.getWidth();
//                this.chartBufferHeight = (int) available.getHeight();
//                GraphicsConfiguration gc = canvas.getDeviceConfiguration();
//                this.chartBuffer = gc.createCompatibleImage(
//                        this.chartBufferWidth, this.chartBufferHeight,
//                        Transparency.TRANSLUCENT);
//                this.refreshBuffer = true;
//            }
//
//            // do we need to redraw the buffer?
//            if (this.refreshBuffer) {
//
//                this.refreshBuffer = false; // clear the flag
//
//                RectShape bufferArea = new RectShape(
//                        0, 0, this.chartBufferWidth, this.chartBufferHeight);
//
//                Graphics2D bufferG2 = (Graphics2D)
//                        this.chartBuffer.getGraphics();
//                RectShape r = new RectShape(0, 0, this.chartBufferWidth,
//                        this.chartBufferHeight);
//                bufferG2.setPaint(getBackground());
//                bufferG2.fill(r);
//                if (scale) {
//                    AffineTransform saved = bufferG2.getTransform();
//                    AffineTransform st = AffineTransform.getScaleInstance(
//                            this.scaleX, this.scaleY);
//                    bufferG2.transform(st);
//                    this.chart.draw(bufferG2, chartArea, this.anchor,
//                            this.info);
//                    bufferG2.setTransform(saved);
//                }
//                else {
//                    this.chart.draw(bufferG2, bufferArea, this.anchor,
//                            this.info);
//                }
//
//            }
//
//            // zap the buffer onto the panel...
//            canvas.drawImage(this.chartBuffer, insets.left, insets.top, this);
//
//        }

        // TODO:AffineTransform
        // or redrawing the chart every time...
//        else {

//            AffineTransform saved = canvas.getTransform();
//            canvas.translate(insets.left, insets.top);
//            if (scale) {
//                AffineTransform st = AffineTransform.getScaleInstance(
//                        this.scaleX, this.scaleY);
//                canvas.transform(st);
//            }
//            this.chart.draw(canvas, chartArea, this.anchor, this.info);
//            canvas.setTransform(saved);

//        }
        this.chart.draw(canvas, chartArea, this.anchor, this.info);

//        Iterator iterator = this.overlays.iterator();
//        while (iterator.hasNext()) {
//            Overlay overlay = (Overlay) iterator.next();
//            overlay.paintOverlay(canvas, this);
//        }

        // redraw the zoom RectShape (if present) - if useBuffer is false,
        // we use XOR so we can XOR the RectShape away again without redrawing
        // the chart
        //drawZoomRectShape(canvas, !this.useBuffer);

        //canvas.dispose();

        this.anchor = null;
//        this.verticalTraceLine = null;
//        this.horizontalTraceLine = null;

    }

    public Dimension getSize() {
        return this.size;
    }

    /**
     * Returns the anchor point.
     *
     * @return The anchor point (possibly <code>null</code>).
     */
    public PointF getAnchor() {
        return this.anchor;
    }

    public ChartRenderingInfo getInfo() {
        return info;
    }

    /**
     * Sets the anchor point.  This method is provided for the use of
     * subclasses, not end users.
     *
     * @param anchor  the anchor point (<code>null</code> permitted).
     */
    protected void setAnchor(PointF anchor) {
        this.anchor = anchor;
    }

    /**
     * Information for multi touch start
     * @author ikeda
     *
     */
    private class MultiTouchStartInfo {
        private double distance = 0;

        public double getDistance() {
            return distance;
        }
        public void setDistance(double distance) {
            this.distance = distance;
        }
    }

    private MultiTouchStartInfo multiTouchStartInfo = null;

    /**
     * Information for Single touch start
     * @author ikeda
     *
     */
    private class SingleTouchStartInfo {
        private double x = 0;
        private double y = 0;

        public double getX() {
            return x;
        }
        public void setX(double x) {
            this.x = x;
        }
        public double getY() {
            return y;
        }
        public void setY(double y) {
            this.y = y;
        }
    }

    private SingleTouchStartInfo singleTouchStartInfo = null;

    /**
     * Zoom 
     * @param ev
     */
    private void zoomAdjustment(MotionEvent ev) {
        PointF point = new PointF((ev.getX(0) + ev.getX(1)) / 2, (ev.getY(0) + ev.getY(1)) / 2);
        //end distance
        double endDistance = Math.sqrt(Math.pow(ev.getX(0) - ev.getX(1),2) + Math.pow(ev.getY(0) - ev.getY(1),2));

        // zoom process
        zoom(point, this.multiTouchStartInfo.getDistance(),endDistance);

        // reset start point
        setMultiTouchStartInfo(ev);
    }

    /**
     * zoom
     * @param startDistance
     * @param endDistance
     */
    private void zoom(PointF source, double startDistance, double endDistance) {

        Plot plot = this.chart.getPlot();
        PlotRenderingInfo info = this.info.getPlotInfo();

        if(plot instanceof Zoomable) {
            float scaleDistance = (float)(startDistance / endDistance);

            if (this.mScale * scaleDistance < 10.0f &&
                    this.mScale * scaleDistance > 0.1f) {
                this.mScale *= scaleDistance;
                Zoomable z = (Zoomable) plot;
                z.zoomDomainAxes(scaleDistance, info, source, false);
                z.zoomRangeAxes(scaleDistance, info, source, false);
            }
        }

        // repaint
        invalidate();
    }

    private void inertialMove() {
        if(inertialMovedFlag == true) {
            RectShape dataArea = this.info.getPlotInfo().getDataArea();

            accelX *= friction;
            accelY *= friction;

            double dataAreaWidth = dataArea.getWidth();
            double dataAreaHeight = dataArea.getHeight();

            if (lastTouch != null) {
                move(lastTouch, accelX, accelY, dataAreaWidth, dataAreaHeight);
            }

            if(accelX < 0.1 && accelX > -0.1) {
                accelX = 0;
            }

            if(accelY < 0.1 && accelY > -0.1) {
                accelY = 0;
            }

            if(accelX == 0 && accelY == 0) {
                inertialMovedFlag = false;
            }
        }
    }    /**
     * Receives notification of touch on the panel. These are
     * translated and passed on to any registered {@link ChartTouchListener}s.
     *
     * @param event  Information about the touch event.
     */
    public void touched(MotionEvent event) {

        int x = (int) (event.getX() / this.scaleX);
        int y = (int) (event.getY() / this.scaleY);

        this.anchor = new PointF(x, y);
        if (this.chart == null) {
            return;
        }
        this.chart.setNotify(true);  // force a redraw

        chart.handleClick((int)event.getX(), (int)event.getY(), info);
        inertialMovedFlag = false;

        // new entity code...
        if (this.chartMotionListeners.size() == 0) {
            return;
        }

        ChartEntity entity = null;
        if (this.info != null) {
            EntityCollection entities = this.info.getEntityCollection();
            if (entities != null) {
                entity = entities.getEntity(x, y);
            }
        }
        ChartTouchEvent chartEvent = new ChartTouchEvent(getChart(), event,
                entity);
        for (int i = chartMotionListeners.size() - 1; i >= 0; i--) {
            this.chartMotionListeners.get(i).chartTouched(chartEvent);
        }

    }

    /**
     * Returns the chart contained in the panel.
     *
     * @return The chart (possibly <code>null</code>).
     */
    public AFreeChart getChart() {
        return this.chart;
    }

    /**
     * Adds a listener to the list of objects listening for chart touch events.
     *
     * @param listener  the listener (<code>null</code> not permitted).
     */
    public void addChartTouchListener(ChartTouchListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("Null 'listener' argument.");
        }
        this.chartMotionListeners.add(listener);
    }

    /**
     * Removes a listener from the list of objects listening for chart touch
     * events.
     *
     * @param listener  the listener.
     */
    public void removeChartTouchListener(ChartTouchListener listener) {
        this.chartMotionListeners.remove(listener);
    }

    /**
     * Returns an array of the listeners of the given type registered with the
     * panel.
     *
     * @param listenerType  the listener type.
     *
     * @return An array of listeners.
     */
    public EventListener[] getListeners() {
        return this.chartMotionListeners.toArray(new ChartTouchListener[0]);
    }

    /**
     * Schedule a user interface repaint.
     */
    public void repaint() {
        mHandler.post(new Runnable() {
            public void run() {
                invalidate();
            }
        });
    }

    /**
     * Receives notification of changes to the chart, and redraws the chart.
     *
     * @param event  details of the chart change event.
     */
    public void chartChanged(ChartChangeEvent event) {
//        this.refreshBuffer = true;
        Plot plot = this.chart.getPlot();
        if (plot instanceof Zoomable) {
            Zoomable z = (Zoomable) plot;
            this.orientation = z.getOrientation();
        }
        repaint();
    }

    /**
     * Receives notification of a chart progress event.
     *
     * @param event  the event.
     */
    public void chartProgress(ChartProgressEvent event) {
        // does nothing - override if necessary
    }
}

Have a look at the highlighted lines in this code, the only change that I have done is, typecasting the return type of getScaleX() and getScaleY() methods to Float. Credit for this class goes to good people at ICOMSYSTECH Co.,Ltd. (Niwano Masayoshi). Please note: To get full class, double click on the code box/dialog above.

Now that we have our view ready, we need a class, which can call the createBarChart method from the ChartFactory class of Android AFreeChart library.

/* ===========================================================
 * AFreeChart : a free chart library for Android(tm) platform.
 *              (based on JFreeChart and JCommon)
 * ===========================================================
 * Original Author:  Niwano Masayoshi (for ICOMSYSTECH Co.,Ltd);
 * Contributor(s):   Mohit Gupt;
 *
 * Changes
 * -------
 * 19-Nov-2010 : Version 0.0.1 (NM);
 * 31-Mar-2013 : Restructuring
 */

package com.truiton.myafreechart;

import org.afree.chart.ChartFactory;
import org.afree.chart.AFreeChart;
import org.afree.chart.axis.CategoryAxis;
import org.afree.chart.axis.CategoryLabelPositions;
import org.afree.chart.axis.NumberAxis;
import org.afree.chart.plot.CategoryPlot;
import org.afree.chart.plot.PlotOrientation;
import org.afree.chart.renderer.category.BarRenderer;
import org.afree.data.category.CategoryDataset;
import org.afree.data.category.DefaultCategoryDataset;
import org.afree.graphics.GradientColor;
import org.afree.graphics.SolidColor;

import android.content.Context;
import android.graphics.Color;

public class TruitonBarChartView extends DemoView {

    /**
     * constructor
     * @param context
     */
    public TruitonBarChartView(Context context) {
        super(context);

        CategoryDataset dataset = createDataset();
        AFreeChart chart = createChart(dataset);

        setChart(chart);
    }

    /**
     * Returns a sample dataset.
     *
     * @return The dataset.
     */
    private static CategoryDataset createDataset() {

        // row keys...
        String series1 = "Sales";
        String series2 = "Expenses";

        // column keys...
        String category1 = "2010";
        String category2 = "2011";
        String category3 = "2012";
        String category4 = "2013";

        // create the dataset...
        DefaultCategoryDataset dataset = new DefaultCategoryDataset();

        dataset.addValue(1.0, series1, category1);
        dataset.addValue(4.0, series1, category2);
        dataset.addValue(3.0, series1, category3);
        dataset.addValue(8.0, series1, category4);

        dataset.addValue(5.0, series2, category1);
        dataset.addValue(7.0, series2, category2);
        dataset.addValue(6.0, series2, category3);
        dataset.addValue(5.0, series2, category4);

        return dataset;

    }

    /**
     * Creates a sample chart.
     *
     * @param dataset  the dataset.
     *
     * @return The chart.
     */
    private static AFreeChart createChart(CategoryDataset dataset) {

        // create the chart...
        AFreeChart chart = ChartFactory.createBarChart(
            "Truiton's Performance by AFreeChart Bar Chart",      // chart title
            "Year",               // domain axis label
            "Sales/Expenses",         // range axis label
            dataset,                  // data
            PlotOrientation.VERTICAL, // orientation
            true,                     // include legend
            true,                     // tooltips?
            false                     // URLs?
        );

        // NOW DO SOME OPTIONAL CUSTOMISATION OF THE CHART...

        // set the background color for the chart...
        chart.setBackgroundPaintType(new SolidColor(Color.WHITE));

        // get a reference to the plot for further customisation...
        CategoryPlot plot = (CategoryPlot) chart.getPlot();

        // set the range axis to display integers only...
        NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
        rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());

        // disable bar outlines...
        BarRenderer renderer = (BarRenderer) plot.getRenderer();
        renderer.setDrawBarOutline(false);

        // set up gradient paints for series...
        GradientColor gp0 = new GradientColor(Color.BLUE, Color.rgb(51, 102, 204));
        GradientColor gp1 = new GradientColor(Color.RED, Color.rgb(255, 0, 0));
        renderer.setSeriesPaintType(0, gp0);
        renderer.setSeriesPaintType(1, gp1);

        CategoryAxis domainAxis = plot.getDomainAxis();
        domainAxis.setCategoryLabelPositions(
                CategoryLabelPositions.createUpRotationLabelPositions(
                        Math.PI / 6.0));
        // OPTIONAL CUSTOMISATION COMPLETED.

        return chart;

    }
}

The purpose of this TruitonBarChartView.java is to create a bar chart by proving all the required arguments. In this class we can create a data set for graph/chart, set colors and set various other properties for chart.

We can now move on to our main activity class TruitonMainActivity.java which would finally display bar chart from Android AFreeChart library.

package com.truiton.myafreechart;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.view.Window;

public class TruitonMainActivity extends Activity {

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 requestWindowFeature(Window.FEATURE_NO_TITLE);
 TruitonBarChartView mView = new TruitonBarChartView(this);
 setContentView(mView);
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
 // Inflate the menu; this adds items to the action bar if it is present.
 getMenuInflater().inflate(R.menu.truiton_main, menu);
 return true;
 }

}

The code in this class needs no explanation, we are just creating a view from TruitonBarChartView class and setting it as content view. With this done our graph/chart from Android AFreeChart library would be drawn as:

The major advantages of Android AFreeChart over Google Chart Tools is that, no internet connection is required to render chart/graphs and graphs are live i.e. they are scrollable, more interactive which leads to better user experience. To get a better understanding of Android AFreeChart please have a look at the chart gallery and AFreeChart javadocs. Next I would be writing a tutorial about AChartEngine which would conclude my series on charts on Android. If this tutorial helped you please like n share it on Facebook, and connect with us on by liking our Facebook page.

About Mohit Gupt

An android enthusiast, and an iPhone user with a keen interest in development of innovative applications.


Leave a comment

Your email address will not be published. Required fields are marked *

7 thoughts on “Android Tutorial- Introduction to AFreeChart with Sample

  • Akhil

    I cannot find the org.afree.graphics.GradientColor

    i was looking for another graph engine achartengine, but stumble upon this
    that does not support gradient color vertical, can you provide source code for org.afree.graphics.GradientColor; or any jar for org.afree

  • Tim

    Mohit,

    I have been trying to use AFreechart to create dial charts without success. The dial classes are subtly different to the ones in JFreeChart.

    Can you direct me to some examples?

    Kind regards,

    Tim.

  • Hemant

    Mohit,
    i am creating an app in which i have created a activity.xml file now i want to add some textviews & imageviews along with this bar chart. But using this code
    i can only generate bar chart in a different view(“mview in this code”). Is there any way to use this view inside my activity.xml file, so that i can have my textviews, imageviews and bar chat together

  • Diego Chalderonier

    Hello, your post helps me a lot. But, it’s the same procedure to create a Line chart? Like an equation with X and Y values. And joint the points X and Y.
    Thank You!