Monday, March 16, 2015

Integrasi Radar Chart (Chart.js) dengan Primefaces

Beberapa waktu yang lalu saat mengerjakan project on-site, tim kami diminta untuk me-modernisasikan sistem dokumen report yg sudah ada menjadi sebuah aplikasi web yang nantinya dapat di integrasikan ke dalam portal internal client. Dokumen report yg sudah ada saat ini dibuat dengan Microsoft Excel, dimana dokumen ini memperlihatkan rekap data beserta total per kelompok barang menggunakan radar chart.

Tantangannya adalah tim harus menggunakan Primefaces karena karena sistem portal yang sudah ada saat ini dibangun dengan menggunakan Primefaces sementara Primefaces sendiri tidak menyediakan component Radar Chart.

Saya memutuskan untuk menggunakan Chart.js (http://www.chartjs.org/docs/#radar-chart) dimana library javascript ini sudah mem-fasilitasi untuk dapat membuat radar chart. Saya akan coba sharing cara mengintegrasikannya dengan Primefaces.

Kita akan membuat membuat aplikasi sederhana yang menyajikan kepada user summary penjualan tahunan per tiga-bulan, dalam bentuk tabel dan radar chart.

Mari kita mulai dengan membuat contoh data:


Untuk membuat contoh data diatas anda dapat membuat JUnit test sederhana seperti berikut:

 public class AppTest extends TestCase {  
   public AppTest(String testName){  
     super( testName );    
   }  

   public static Test suite() {  
     return new TestSuite( AppTest.class );    
   }  

   private static SessionFactory factory;    
   private static Session session;  

   private static double generateRandomAmount() {  
     return ((double)new Random().nextInt(500))/  
         (new Random().nextInt(1) == 0 ? 2 : 3);    
   }  

   public void testApp() {  
     try {  
       factory = new AnnotationConfiguration()  
           .configure()  
           .addPackage("com.nostratech.model")  
           .addAnnotatedClass(QuarterlySales.class)  
           .buildSessionFactory();      
     } catch (Throwable ex) {  
       System.err.println("Failed to create sessionFactory object." + ex);  
       throw new ExceptionInInitializerError(ex);      
     }  

     Session session = factory.openSession();      
     Transaction tx = null;      

     try{  
       tx = session.beginTransaction();  

       QuarterlySales[] sales = new QuarterlySales[] {  
         new QuarterlySales("Rice"),          
         new QuarterlySales("Vegetable"),          
         new QuarterlySales("Fruit"),          
         new QuarterlySales("Snack"),          
         new QuarterlySales("Ketchup"),          
         new QuarterlySales("Milk")  
       };  

       for (QuarterlySales sale : sales) {  
         sale.setAmountQ1(generateRandomAmount());          
         sale.setAmountQ2(generateRandomAmount());          
         sale.setAmountQ3(generateRandomAmount());          
         sale.setAmountQ4(generateRandomAmount());  
         session.save(sale);        
       }  

       assert session.createCriteria(QuarterlySales.class).list().size() > 0;  

       tx.commit();      

      } catch (HibernateException e) {  
       if (tx!=null) tx.rollback();        
       e.printStackTrace();      
     } finally {  
       session.close();      
     }  
   }  
 }  

Dimana entity QuarterlySales adalah sebagai berikut:

 @Entity  
 public class QuarterlySales {  
   @Id  
   @GeneratedValue(strategy = GenerationType.AUTO)  
   private Long id;  

   private String item;  
   private double amountQ1;    
   private double amountQ2;    
   private double amountQ3;    
   private double amountQ4;  

   public QuarterlySales() {  
   }  

   public QuarterlySales(String item) {  
     this.item = item;    
   }  
   public String getItem() {  
     return item;    
   }  
   public void setItem(String item) {  
     this.item = item;    
   }  
   public double getAmountQ1() {  
     return amountQ1;    
   }  
   public void setAmountQ1(double amountQ1) {  
     this.amountQ1 = amountQ1;    
   }  
   public double getAmountQ2() {  
     return amountQ2;    
   }  
   public void setAmountQ2(double amountQ2) {  
     this.amountQ2 = amountQ2;    
   }  
   public double getAmountQ3() {  
     return amountQ3;    
   }  
   public void setAmountQ3(double amountQ3) {  
     this.amountQ3 = amountQ3;    
   }  
   public double getAmountQ4() {  
     return amountQ4;    
   }  
   public void setAmountQ4(double amountQ4) {  
     this.amountQ4 = amountQ4;    
   }  
 }  

Jika anda perhatikan, saya menggunakan Hibernate sebagai ORM nya. Berikut konfigurasi hibernate yang saya gunakan:

 <?xml version="1.0" encoding="utf-8"?>  
     <!DOCTYPE hibernate-configuration SYSTEM  
         "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">  
 <hibernate-configuration>  
   <session-factory>  
     <property name="hibernate.dialect">  
       org.hibernate.dialect.MySQLDialect  
     </property>  
     <property name="hibernate.connection.driver_class">  
       com.mysql.jdbc.Driver  
     </property>  
     <property name="hibernate.connection.url">  
       jdbc:mysql://localhost:8889/nostra_db  
     </property>  
     <property name="hibernate.connection.username">  
       root  
     </property>  
     <property name="hibernate.connection.password">  
       root  
     </property>  
     <property name="show_sql">true</property>  
     <property name="hbm2ddl.auto">create</property>  
   </session-factory>  
 </hibernate-configuration>  

Sebelum kita integrasikan Chart.js mari terlebih dahulu kita membuat UI untuk menampilkan data yang sudah kita buat sebelumnya dengan menggunakan Primefaces datatable

 <?xml version="1.0" encoding="UTF-8"?>  
 <!DOCTYPE html>  
 <html xmlns="http://www.w3.org/1999/xhtml"  
    xmlns:h="http://java.sun.com/jsf/html"  
    xmlns:p="http://primefaces.org/ui">  
 <h:head>  
 </h:head>  
 <h:body>  
   <h:form>  
     <p:commandButton value="Show Data"  
              actionListener="#{radarChartBean.search}"  
              update="tbl"  
              oncomplete="fetchChartData()"/>  
     <br/>  
     <br/>  
     <p:dataTable id="tbl" value="#{radarChartBean.data}" var="model">  
       <p:column headerText="Item">  
         <h:outputText value="#{model.item}"/>  
       </p:column>  
       <p:column headerText="Q1">  
         <h:outputText value="#{model.amountQ1}"/>  
       </p:column>  
       <p:column headerText="Q2">  
         <h:outputText value="#{model.amountQ2}"/>  
       </p:column>  
       <p:column headerText="Q3">  
         <h:outputText value="#{model.amountQ3}"/>  
       </p:column>  
       <p:column headerText="Q4">  
         <h:outputText value="#{model.amountQ4}"/>  
       </p:column>  
     </p:dataTable>  
   </h:form>  
 </h:body>  
 </html>  

Managed Bean sebagai controller. Untuk demo ini, saya menggunakan @ViewScoped karena data untuk untuk radar (akan dijelaskan selebihnya nanti...) dapat merentang lebih dari satu HTTP Request. Namun demikian, anda bebas untuk mengatur ulang organisasi kodenya agar tidak memerlukan @ViewScoped (saya tinggalkan sebagai latihan...)

 @ManagedBean  
 @ViewScoped  
 public class RadarChartBean {  
   private List<QuarterlySales> data;  
   private RadarChartService service;  
   
   @PostConstruct  
   public void init() {  
     service = new RadarChartService();  
   }  
   
   public void search() {  
     data = service.getData();  
   }
  
   public List<QuarterlySales> getData() {  
     return data;  
   }  
 }  

Untuk mengambil data dengan hibernate kita buat class baru RadarChartService. Disini kita lakukan logical partitioning pada aplikasi sesuai fungsi sehingga mudah dimengerti.

 public class RadarChartService {  
   private SessionFactory factory;  
   private Session session;  
   public RadarChartService() {  
     factory = new AnnotationConfiguration()  
         .configure()  
         .addPackage("com.nostratech.model")  
         .addAnnotatedClass(QuarterlySales.class)  
         .buildSessionFactory();  
     session = factory.openSession();  
   }  
   public List<QuarterlySales> getData() {  
     return session.createCriteria(QuarterlySales.class).list();  
   }  
 }  

Struktur aplikasi kurang lebih menjadi seperti berikut:


Saat anda jalankan, maka tampilan aplikasi kita untuk sementara akan menjadi seperti di bawah:


Mari selanjutnya kita tambahkan radar chart untuk me-representasikan data dari table tersebut.

Chart.js mengharapkan format data dalam bentuk JSON seperti contoh di bawah (saya ambil dari dokumentasi Chart.js: http://www.chartjs.org/docs/#radar-chart):

 var data = {  
   labels: ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"],  
   datasets: [  
     {  
       label: "My First dataset",  
       fillColor: "rgba(220,220,220,0.2)",  
       strokeColor: "rgba(220,220,220,1)",  
       pointColor: "rgba(220,220,220,1)",  
       pointStrokeColor: "#fff",  
       pointHighlightFill: "#fff",  
       pointHighlightStroke: "rgba(220,220,220,1)",  
       data: [65, 59, 90, 81, 56, 55, 40]  
     },  
     {  
       label: "My Second dataset",  
       fillColor: "rgba(151,187,205,0.2)",  
       strokeColor: "rgba(151,187,205,1)",  
       pointColor: "rgba(151,187,205,1)",  
       pointStrokeColor: "#fff",  
       pointHighlightFill: "#fff",  
       pointHighlightStroke: "rgba(151,187,205,1)",  
       data: [28, 48, 40, 19, 96, 27, 100]  
     }  
   ]  
 };  

Pada kasus kita, labels akan menjadi:

labels: ["Rice",  "Vegetable", "Fruit", "Snack", "Ketchup", "Milk"]

untuk masing-masing item. Dan datasets merupakan data sales (Q1, Q2, Q3, dan Q4) masing-masing item mengikuti urutan labels. Contoh:

datasets: {
    {
        label: "Q1",
        fillColor: "rgba(120, 120, 120, 1)",
        ....
        ....
        data: [75, 43, 80, 120, 20, 55, 310],
    },
    {
        label: "Q2",
        ....
        ....

Untuk membantu kita membuat data JSON dengan format seperti di atas, buat sebuah View Object seperti berikut:

 public class VoRadarDataset {  
   private static final DecimalFormat df = new DecimalFormat("###.#");  
   private static VoRadarDataset[] dataset = new VoRadarDataset[] {  
       new VoRadarDataset("Q1", "rgba(192, 80, 77, 0.2)", "rgba(192, 80, 77, 1)"),  
       new VoRadarDataset("Q2", "rgba(155, 187, 89, 0.2)", "rgba(155, 187, 89, 1)"),  
       new VoRadarDataset("Q3", "rgba(187, 174, 45, 0.2)", "rgba(187, 174, 45, 1)"),  
       new VoRadarDataset("Q4", "rgba(85, 134, 191, 0.2)", "rgba(85, 134, 191, 1)")  
   };  
   public static void resetData() {  
     for (int i = 0; i < dataset.length; ++i) {  
       dataset[i].data = null;  
       dataset[i].data = new ArrayList<String>();  
     }  
   }  
   public static void addData(int i, double d) {  
     dataset[i].data.add(df.format(d));  
   }  
   public static void addDataQ1(double d) {  
     addData(0, d);  
   }  
   public static void addDataQ2(double d) {  
     addData(1, d);  
   }  
   public static void addDataQ3(double d) {  
     addData(2, d);  
   }  
   public static void addDataQ4(double d) {  
     addData(3, d);  
   }  
   public static VoRadarDataset[] get() {  
     return dataset;  
   }  
   private final String pointStrokeColor = "#fff";  
   private final String pointHighlightFill = "#fff";  
   private String label;  
   private String fillColor;  
   private String strokeColor;  
   private String pointColor;  
   private String pointHighlightStroke;  
   private List<String> data;  
   private VoRadarDataset(String label, String fillColor, String strokeColor) {  
     this.label = label;  
     this.fillColor = fillColor;  
     this.strokeColor = strokeColor;  
     this.pointColor = strokeColor;  
     this.pointHighlightStroke = strokeColor;  
     this.data = new ArrayList<String>();  
   }  
   public String getPointHighlightFill() {  
     return pointHighlightFill;  
   }  
   public String getPointHighlightStroke() {  
     return pointHighlightStroke;  
   }  
   public String getPointStrokeColor() {  
     return pointStrokeColor;  
   }  
   public String getLabel() {  
     return label;  
   }  
   public String getFillColor() {  
     return fillColor;  
   }  
   public String getStrokeColor() {  
     return strokeColor;  
   }  
   public String getPointColor() {  
     return pointColor;  
   }  
   public List<String> getData() {  
     return data;  
   }  
 }  

Kita akan lihat bagaimana cara menggunakan class ini nanti, sekarang mari kita tambahkan placeholder untuk Radar Chart, posisikan tepat di bawah table:

 </p:dataTable>  
     <div style="margin: 10px">  
       <canvas id="radar" width="400" height="400"></canvas>  
       <div id="radarLegend" style="width: 400px;"></div>  
     </div>  

Tambahkan kode javascript untuk membuat radar chart:

 <script>  
       //<![CDATA[  
       var chart = null;  
       function createRadarChart(data) {  
         var ctx = $("#radar")[0].getContext("2d");  
         chart = new Chart(ctx).Radar(data, {  
           legendTemplate: '' +  
           '<ul style="width: 230px; margin: 0 auto;">' +  
           '  <% for (var i=0; i<datasets.length; i++){ %>' +  
           '  <li style="float: left; margin-left: 30px; color: <%= datasets[i].strokeColor %>">' +  
           '    <span style="color: #333; font-family: Arial; font-size: 0.8em;">' +  
           '      <%= datasets[i].label %>' +  
           '    </span>' +  
           '  </li>' +  
           '  <% } %>' +  
           '</ul>'  
         });  
         $("#radarLegend").html(chart.generateLegend());  
       }  
       function stringFromChars(chars) {  
         var s = "";  
         for(var i=0; i<chars.length; i++) {  
           s += String.fromCharCode(chars[i]);  
         }  
         return s;  
       }  
       function parseLabels(chartData) {  
         var labels = [];  
         for(var i=0; i<chartData.labels.length; i++) {  
           var chars = chartData.labels[i].label;  
           labels.push(stringFromChars(chars));  
         }  
         return labels;  
       }  
       function doCreateRadarChart(xhr, status, args) {  
         if (chart) {  
           chart.destroy();  
           $("#radarLegend").html('');  
         }  
         if (args.chartData) {  
           var chartData = args.chartData;  
           chartData.labels = parseLabels(chartData);  
           createRadarChart(chartData);  
         }  
       }  
       //]]>  
     </script>  

dan tentunya tambahkan referensi ke Chart.js

 <h:head>  
   <script src="js/Chart.min.js" type="text/javascript"/>  
 </h:head>  

Kita akan menggunakan button "Show Data" sebegai trigger untuk menampilkan chart. Untuk itu, tambahkan event oncomplete pada button tersebut:

 <p:commandButton value="Show Data"  
              actionListener="#{radarChartBean.search}"  
              update="tbl"  
              oncomplete="fetchChartData()"/>  

Lalu apakah fungsi fetchChartData() ini ?. Fungsi ini di buat dengan menggunakan component Primefaces yang bernama remoteCommand. remoteCommand ini digunakan untuk memanggil method pada managed bean guna menyiapkan data untuk membuat radar chart.

Kita dapat men-declare nya seperti berikut (anda dapat meletakkannya dimana saja selama masih di dalam <h:form>


 <p:remoteCommand name="fetchChartData"  
              action="#{radarChartBean.prepareChartData}"  
              oncomplete="doCreateRadarChart(xhr, status, args);"/>  

Berikut adalah method prepareChartData() pada managed bean:

 public void prepareChartData() {  
     RequestContext reqCtx = RequestContext.getCurrentInstance();  
     reqCtx.addCallbackParam("chartData", radarChartData);  
   }  

radarChartData adalah instance dari class RadarChartData pada managed bean yang saya gunakan sebagai helper untuk membentuk data dengan format seperti yang di harapkan oleh ChartJS. class RadarChartData juga menggunakan VoRadarDataset yang kita buat sebelumnya untuk membentuk "datasets".

 public static class RadarChartData {  
     private List<LabelWrapper> labels;  
     private VoRadarDataset[] datasets;  
     public RadarChartData() {  
       labels = new ArrayList<LabelWrapper>();  
     }  
     public void addLabel(String label) {  
       labels.add(new LabelWrapper(label));  
     }  
     public List<LabelWrapper> getLabels() {  
       return labels;  
     }  
     public VoRadarDataset[] getDatasets() {  
       return datasets;  
     }  
     public void setDatasets(VoRadarDataset[] datasets) {  
       this.datasets = datasets;  
     }  
     public static class LabelWrapper {  
       private byte[] label;  
       public LabelWrapper(String label) {  
         this.label = label.getBytes();  
       }  
       public byte[] getLabel() {  
         return label;  
       }  
     }  
   }  

Jika anda perhatikan dengan seksama, saya tidak melakukan apapun untuk mengembalikan data ke UI dalam format JSON. Ini karena method addCallbackParam(), akan otomatis meng-convert POJO (dalam kasus ini object dari RadarChartData) ke dalam format JSON.

Lakukan initialisasi pada method search() sebagai berikut:

 public void search() {  
     data = service.getData();  
     prepareChartData(data);  
   }  
   public List<QuarterlySales> getData() {  
     return data;  
   }  
   private void prepareChartData(List<QuarterlySales> radarData) {  
     if (radarData != null && radarData.size() > 0) {  
       VoRadarDataset.resetData();  
       radarChartData = new RadarChartData();  
       for (int i = 0; i < radarData.size(); ++i) {  
         radarChartData.addLabel(radarData.get(i).getItem());  
         VoRadarDataset.addDataQ1(radarData.get(i).getAmountQ1());  
         VoRadarDataset.addDataQ2(radarData.get(i).getAmountQ2());  
         VoRadarDataset.addDataQ3(radarData.get(i).getAmountQ3());  
         VoRadarDataset.addDataQ4(radarData.get(i).getAmountQ4());  
       }  
       radarChartData.setDatasets(VoRadarDataset.get());  
     }  
   }  

Struktur final aplikasi:


Sekian cara mengintegrasikan Chart.js dengan Primefaces. Please share jika anda mempunyai solusi yang mirip atau bahkan lebih baik dari cara yang saya gunakan disini. :)


Terima kasih, semoga bermanfaat !


No comments:

Post a Comment