สร้างกราฟ Line Chart บน Android ด้วย AChartEngine

สร้างกราฟ Line Chart บน Android ด้วย AChartEngine Cover Image

บทความสอนเขียนแอพ Android วันนี้ขอนำเสนอเรื่อง การเขียนกราฟ การ plot กราฟ บนแอพพลิเคชัน Android สำหรับบทความนี้จะใช้ Library ที่ชื่อว่า AChartEngine ตัวอย่าง AChartEngine ทำอะไรได้บ้าง ก็ดูได้จากลิงค์นี้ครับ ตัวอย่าง AChartEngine

มาเข้าเรื่องเลยดีกว่า บทความนี้จะเป็นการแสดง Line Chart (กราฟเส้น) แบบง่ายๆให้ดูนะครับ เริ่มต้น ก็สร้างโปรเจ็คแบบปกติ ไฟล์ที่ต้องใช้มีแค่ 2 ไฟล์ครับ คือ ไฟล์ Activity กับ ไฟล์เลเอาท์ xml

บทความนี้ใช้ Eclipse รันบน Ubuntu 14.04 นะครับ

Add Library

ทำการโหลด Library ของ AChartEngine ที่นี่ เลือกดาวน์โหลด achartengine-1.1.0.jar จากนั้นก็ import ไปที่โปรเจ็คของเรา

สำหรับ Eclipse ก็อปปี้ไฟล์ achartengine-1.1.0.jar ไปไว้ที๋โฟลเดอร์ libs ของโปรเจ็ค จากนั้นคลิกขวาที่ไฟล์ เลือก Build Path -> Add to Build Path

สำหรับ Android Studio มีสองทางเลือกครับ คือ ก็อปไปไว้ที่โฟลเดอร์ libs เช่นเดียวกัน จากนั้นแก้ไขไฟล์ build.gradle โดยเพิ่ม dependencies ดังนี้

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

หากใครมีแล้ว ก็ไม่ต้องแก้อะไร (คำสั่งด้านบนเป็นการสั่ง compile ไฟล์ .jar ทั้งหมดที่อยู่ในโฟลเดอร์ libs)

อีกวิธีหนึ่ง สำหรับคนใช้ maven ครับ เปิด build.gradle ที่ root โปรเจ็คนะครับ แล้วเพิ่ม นี้ลงไป

maven {
    url "https://repository-achartengine.forge.cloudbees.com/snapshot/"
}

จะได้เป็นแบบนี้

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.11.+'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        mavenCentral()
        maven {
            url "https://repository-achartengine.forge.cloudbees.com/snapshot/"
        }
    }
}

ส่วนไฟล์ build.gradle ที่อยู่ใน module ก็เพิ่ม dependencies เข้าไปเป็นแบบนี้

dependencies {
    compile 'com.android.support:appcompat-v7:19.+'
    compile 'org.achartengine:achartengine:1.2.0'
}

Getting Started with AChartEngine

ก่อนที่จะไปสร้าง Line Chart (กราฟเส้น) ด้วย AChartEngine เรามาดูภาพรวม แล้วก็แต่ละคลาสที่ใช้กันก่อนครับ

  • XYSeries : ตัวนี้จะเปรียบเสมือนข้อมูลของกราฟเรา เช่น รายได้ จำนวนประชากร เป็นต้น
  • XYSeriesRenderer : ตัวนี้จะเอาไว้ render กราฟเราครับ เช่น รูปทรงกราฟ สี รูปแบบต่างๆ ปรับแต่งได้
  • XYMultipleSeriesDataset : ตัวนี้คือ Dataset มันคือข้อมูลทั้งหมดของกราฟ โดยดึงมาจาก XYSeries
  • XYMultipleSeriesRenderer : ตัวนี้คือตัว render ทั้งกราฟครับ
  • GraphicalView : ตัวนี้คือ View ชนิดหนึ่ง ที่เอาไว้แสดง graph ครับ

รายละเอียดคร่าวๆ ก็เป็นดังนี้ หากใครงง (แน่นอนว่าต้องงง ถ้าเพิ่งเคยทำ ต้องลองดูตัวอย่าง แล้วจะเข้าใจมากขึ้นครับ)

เริ่มสร้างกราฟ

เป้าหมายคือ ต้องการสร้างกราฟข้อมูลยอดขาย ของ 3 บริษัท ในปีๆหนึ่ง ฉะนั้นกราฟก็จะมี 3 เส้น แบบนี้

Mockup

ให้เปิดคลาส MainActivity.java ขึ้นมา โค๊ดเริ่มต้นผมเป็นแบบนี้

package com.devahoy.sample.achartengine;

import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.ActionBar;
import android.support.v4.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.os.Build;

public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(R.id.container, new PlaceholderFragment())
                    .commit();
        }
    }

    /**
     * A placeholder fragment containing a simple view.
     */
    public static class PlaceholderFragment extends Fragment {

        public PlaceholderFragment() {}

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_main, container, false);
            return rootView;
        }
    }

}

ใครใช้ fragment ไม่เป็น แนะนำให้อ่าน Android Fragment คือ? จริงๆควรจะใช้ fragment ในทุกๆโปรเจ็คนะครับ

ก่อนสร้างกราฟ เราต้องมีข้อมูลก่อน ฉะนั้น สร้างข้อมูลจำลองขึ้นมา โดยสร้างเมธอดใหม่ ชื่อ initData() แบบนี้

private void initData() {
    String[] months = {
            "JAN", "FEB", "MAR", "APR", "MAY", "JUN",
            "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"
    };

    int[] index = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    int[] incomeA = {4000, 5500, 2300, 2100, 2500, 2900, 3200, 2400, 1800, 2100, 3500, 5900};
    int[] incomeB = {3600, 4500, 3200, 3600, 2800, 1800, 2100, 2900, 2200, 2500, 4000, 3500};
    int[] incomeC = {4300, 4000, 3000, 3200, 2400, 2500, 2600, 3400, 3900, 4500, 5000, 4500};
}

โค๊ดด้านบน months หมายถึง ข้อมูลที่่จะแสดงบนแกน x ระบุว่าแต่ละเดือนยอดขายเป็นยังไง ส่วน index คือตำแหน่งของข้อมูล โดยใช้ข้อมูล 3 บริษัท ได้แก่ incomeA, incomeB และ incomeC เมื่อมีข้อมูลดิบแล้ว ขั้นตอนต่อมาทำอย่างไร?

ใช้ XYSeries เพื่อเก็บข้อมูลที่ของเราแบบนี้ (ยังอยู่ในเมธอด initData() นะ)

XYSeries seriesA = new XYSeries("Googla");
XYSeries seriesB = new XYSeries("Microsa");
XYSeries seriesC = new XYSeries("Appla");

int length = index.length;
for (int i = 0; i < length; i++) {
    seriesA.add(index[i], incomeA[i]);
    seriesB.add(index[i], incomeB[i]);
    seriesC.add(index[i], incomeC[i]);
}

ต่อมาใช้ XYSeriesRenderer เพื่อจะกำหนดให้กราฟออกมาลักษณะไหน

XYSeriesRenderer rendererA = new XYSeriesRenderer();
rendererA.setPointStyle(PointStyle.CIRCLE);
rendererA.setColor(Color.RED);
rendererA.setLineWidth(2);

ด้านบนเป็นการกำหนด ให้แสดง กราฟเป็นจุดและเป็นสีแดง

ต่อมาสร้าง dataset ขึ้นมาโดยใช้ XYMultipleSeriesDataset แล้วทำการเพิ่ม XYSeries ทั้ง 3 ที่สร้างไว้ก่อนหน้านี้ ดังนี้

XYMultipleSeriesDataset dataset = new XYMultipleSeriesDataset();
dataset.addSeries(seriesA);
dataset.addSeries(seriesB);
dataset.addSeries(seriesC);

ต่อมาใช้ XYMultipleSeriesRenderer เพื่อที่จะแสดงกราฟทั้งหมด ขึ้นตอนนี้คือการปรับแต่ง และตั้งค่ากราฟก่อนครับ

XYMultipleSeriesRenderer multipleSeriesRenderer
    = new XYMultipleSeriesRenderer();

for (int i = 0; i < length; i++) {
    multipleSeriesRenderer.addXTextLabel(i + 1, months[i]);
}
multipleSeriesRenderer.setChartTitle("ตัวอย่างกราฟเส้น (Line Chart)");
multipleSeriesRenderer.setYTitle("ยอดขายรวม(สตางค์)");
multipleSeriesRenderer.setXTitle("ปี พ.ศ. 2600");
multipleSeriesRenderer.setZoomButtonsVisible(true);

จะเห็นว่าด้านบน เป็นการกำหนด ข้อความที่จะแสดงในแกน x คือแสดงเดือน ที่เก็บไว้ใน months ส่วนอันอื่นก็ดูตามชื่อเมธอดเลยครับ มัน make sense อยู่แล้ว สุดท้าย ก็ต้องเพิ่ม XYSeriesRenderer ไปที่ XYMultipleSeriesRenderer ด้วย

multipleSeriesRenderer.addSeriesRenderer(rendererA);
multipleSeriesRenderer.addSeriesRenderer(rendererA);
multipleSeriesRenderer.addSeriesRenderer(rendererA);

ข้อควรระวัง dataset และ XYSeriesRenderer ต้องมีจำนวนเท่ากันนะครับ dataset เราเพิ่ม Series ไปเท่าไหร่ XYMultipleSeriesRenderer ก็ต้องเพิ่ม XYSeriesRenderer จำนวนเดียวกันด้วยครับ

ต่อมาเพิ่มอีกหนึ่งบรรทัด เป็นอันจบเมธอด initData()

drawChart(dataset, multipleSeriesRenderer);

ปล่อยให้ error ไว้ก่อนครับ เนื่องจากเรายังไม่ได้ทำการสร้าง เมธอด drawChart() ให้ไปสร้างเลเอาท์ xml ก่อน

สร้างไฟล์ XML

การสร้างเลเอาท์ xml ไม่มีอะไรมากครับ แค่สร้าง ViewGroup ให้มันซักตัว จะเป็น RelativeLayout หรือ LinearLayout ก็ได้ อย่างในบทความ ใช้ RelativeLayout โดยใช้ไฟล์ res/layout/fragment_main.xml จะได้ดังนี้

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/graph_container"
    tools:context="com.devahoy.sample.achartengine.MainActivity$PlaceholderFragment" >

</RelativeLayout>

ต่อมาที่คลาส PlachHolderFragment ภายในคลาส MainActivity ก็เพ่ิมนี้ลงไป

private View mView;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
    View rootView = inflater.inflate(R.layout.fragment_main, container, false);
    mView = rootView;
    initData();
    return rootView;
}

เพื่อจะเอา View ที่สร้างจาก onCreateView() ไปใช้ในการสร้าง graph ครับ เลยต้องประกาศเป็นตัวแปร global ไว้

ต่อมาได้เวลาเพิ่มเมธอด drawChart() ซักที ภายในเมธอดก็มีโค๊ดแบบนี้

private void drawChart(XYMultipleSeriesDataset dataset,
                       XYMultipleSeriesRenderer renderer) {
    GraphicalView graphView =
            ChartFactory.getLineChartView(getActivity(), dataset, renderer);

    RelativeLayout container =
            (RelativeLayout) mView.findViewById(R.id.graph_container);

    container.addView(graphView);
}

เราใช้ GraphicalView ซึ่งมันก็คือ View นั่นแหละ เหมือนๆกับ TextView, EditText, Button ทั่วๆไป จากนั้น ก็ใช้ RelativeLayout ที่เราได้สร้างไว้ใน fragment_main.xml ที่ชื่อว่า graph_container ทำการ addView() ซะ เป็นอันจบ

เมื่อเปิดโปรแกรมขึ้นมา จะได้ดังภาพ

AChart Engine

!!! แต่เดี๋ยวก่อน ในเมื่อมีกราฟ ถึง 3 อัน ซึ่งสีเหมือนกัน มันดุยาก เราอยากจะปรับเปลี่ยนสีทำยังไงละ ? แน่นอนครับ ก็ใช้ XYSeriesRenderer เข้ามาช่วย ฉะนั้นผมสร้าง XYSeriesRenderer เพิ่ม 2 ตัว ในเมธอด initData() ดังนี้

XYSeriesRenderer rendererB = new XYSeriesRenderer();
rendererB.setPointStyle(PointStyle.X);
rendererB.setColor(Color.BLUE);
rendererB.setLineWidth(2);

XYSeriesRenderer rendererC = new XYSeriesRenderer();
rendererC.setPointStyle(PointStyle.DIAMOND);
rendererC.setColor(Color.GREEN);
rendererC.setLineWidth(2);

แล้ว XYMultipleSeriesRenderer ก็เพิ่ม XYSeries ให้มันซะ จาก

multipleSeriesRenderer.addSeriesRenderer(rendererA);
multipleSeriesRenderer.addSeriesRenderer(rendererA);
multipleSeriesRenderer.addSeriesRenderer(rendererA);

ก็จะได้เป็น

multipleSeriesRenderer.addSeriesRenderer(rendererA);
multipleSeriesRenderer.addSeriesRenderer(rendererB);
multipleSeriesRenderer.addSeriesRenderer(rendererC);

ลองรันโปรแกรมใหม่ ผลลัพธ์เป็นแบบนี้

AChart 2

ยังไม่จบ เรายังสามารถปรับแต่งกราฟได้อีก โดยผมปรับแต่ง XYMultipleSeriesRenderer อีกเป็นแบบนี้

multipleSeriesRenderer.setXLabels(0);
multipleSeriesRenderer.setBackgroundColor(Color.WHITE);
multipleSeriesRenderer.setApplyBackgroundColor(true);
multipleSeriesRenderer.setMarginsColor(Color.WHITE);
multipleSeriesRenderer.setLabelsColor(Color.BLACK);
multipleSeriesRenderer.setAxesColor(Color.GRAY);
multipleSeriesRenderer.setYLabelsColor(0, Color.BLACK);
multipleSeriesRenderer.setXLabelsColor(Color.BLACK);

ข้างบนคือปรับสี background ปรับสี แกน x, y ปรับตัวหนังสือ และที่สำคัญ ปรับค่า x ให้เป็น 0 เนื่องจากมันจะไปทับกับค่า เดือน ที่เรากำหนดไว้นั่นเอง

สุดท้าย ก็ได้กราฟออกมาเป็นแบบลักษณะดังรูป

AChart Final

สรุป

ตัวอย่างนี้ก็เป็นตัวอย่างการใช้ AChartEngine โดยการวาด Line Chart (กราฟแท่ง) แบบง่ายๆนั้นเอง หวังว่าผู้อ่านทุกท่าน สามารถนำไปประยุกต์ ปรับแต่ง ใช้งานให้เข้ากับแอพของท่านได้นะครับ

สุดท้าย โค๊ดตัวอย่าง อัพลง Github เรียบร้อยแล้ว ไปดูได้ที่นี่ครับ

Source Code

Chai

Chai Phonbopit : Developer แห่งหนึ่ง • ผู้ชายธรรมดาๆ ที่ชื่นชอบ Node.js, JavaScript และ Open Source มีงานอดิเรกเป็น Acoustic Guitar และ Football

บทความล่าสุด