Integrate ECharts into an Ionic app

Published: February 22, 2017  •  Updated: December 05, 2017  •  ionic

In this post we integrate the ECharts library into an Ionic application. ECharts is an open source, free to use chart library under the (BSD license). The library is developed by Baidu, the chinese search company. ECharts draws diagrams and charts into a canvas element and it added recently a renderer that supports SVG.

The library provides a lot of chart types out of the box. The demo page is a good place to start and see all the diagrams and charts in action: https://ecomfe.github.io/echarts-examples/public/index.html

The documentation page of ECharts describes all the configuration options and methods
Api: https://ecomfe.github.io/echarts-doc/public/en/api.html#echarts
Option: https://ecomfe.github.io/echarts-doc/public/en/option.html


What we want to do in this post is creating an Ionic app and displaying some simple charts. For the following examples I copied the configuration objects directly from the demo page. The application is online at this url: https://demo.rasc.ch/echarts/


Setup

We start with the Ionic command line tool and create a new app based on the tab template.

ionic start echartsapp tabs

Next we install the ECharts library and the ngx-echarts library which provides an Angular directive that allows us to integrate ECharts into an Angular app very easily.

npm install echarts
npm install ngx-echarts

To use the ngx-echarts library we have to import the module into our AppModule. Add NgxEchartsModule to the import section

  imports: [
    BrowserModule,
    NgxEchartsModule,
    IonicModule.forRoot(MyApp)
  ],

src/app/app.module.ts

Then we create a few pages that will host our examples.

ionic g page chart1 --no-module
ionic g page chart2 --no-module
ionic g page chart3 --no-module
ionic g page chart4 --no-module
ionic g page chart5 --no-module
ionic g page chart6 --no-module

You can delete all the other pages that the start command created (about, contact, home). With the --no-module option the generate command does not create the module file and does not add the @IonicPage() decorator. I want to keep the example simple and don't want to use lazy loading. It does not matter for this example, it will work with and without lazy loading.

Next add the new pages to the src/app/app.module.ts file and change the tabs page

<ion-tabs>
  <ion-tab [root]="tab1Root" tabTitle="Chart 1" tabIcon="podium"></ion-tab>
  <ion-tab [root]="tab2Root" tabTitle="Chart 2" tabIcon="podium"></ion-tab>
  <ion-tab [root]="tab3Root" tabTitle="Chart 3" tabIcon="podium"></ion-tab>
  <ion-tab [root]="tab4Root" tabTitle="Chart 4" tabIcon="podium"></ion-tab>
  <ion-tab [root]="tab5Root" tabTitle="Chart 5" tabIcon="podium"></ion-tab>
  <ion-tab [root]="tab6Root" tabTitle="Chart 6" tabIcon="podium"></ion-tab>
</ion-tabs>

src/pages/tabs/tabs.html

import {Component} from '@angular/core';
import {Chart1Page} from "../chart1/chart1";
import {Chart2Page} from "../chart2/chart2";
import {Chart3Page} from "../chart3/chart3";
import {Chart4Page} from "../chart4/chart4";
import {Chart5Page} from "../chart5/chart5";
import {Chart6Page} from "../chart6/chart6";

@Component({
  templateUrl: 'tabs.html'
})
export class TabsPage {
  tab1Root: any = Chart1Page;
  tab2Root: any = Chart2Page;
  tab3Root: any = Chart3Page;
  tab4Root: any = Chart4Page;
  tab5Root: any = Chart5Page;
  tab6Root: any = Chart6Page;
}

src/pages/tabs/tabs.ts


Next we have to change the build process to add the ECharts library to our page. Unfortunately ngx-echarts does not automatically import the EChart library into our app. So we copy the library from the node_modules folder into the www/dist folder and reference it in the index.html page.

Create a new file copy.config.js in the root of your project and insert the following code.

module.exports = {
  copyEcharts: {
    src: ['./node_modules/echarts/dist/echarts-en.min.js'],
    dest: '{{WWW}}/build'
  }
}

copy.config.js

Open package.json and add a new ionic_copy configuration with a value that points to the copy.config.js file.

  "config": {
    "ionic_copy": "./copy.config.js"
  },

package.json

This setup tells the Ionic build scripts to copy the file echarts-en.min.js to the www/builder folder every time we build the application.

Next we have to add a script tag to the src/index.html file to import the library.

<script src="build/echarts-en.min.js"></script>

src/index.html

Not the most elegant solution but it works. Let me know if you find a better solution.

ECharts delivers three different packages of the library. The biggest is echarts-en.min.js (650KB) that contains the code for all the charts. echarts.simple.min.js (262KB) and echarts.common.min.js (400KB) are much smaller but only contain code for a subset of the charts. The simple package contains code for the line, bar and pie charts. The common package adds the scatter chart and components like tooltip, legends, grids, mark points.


Chart 1

The template for the first example is very simple. We add the echarts directive from the ngx-echarts library to a div tag and inject the options object from the TypeScript code into the directive.

<ion-header>
  <ion-navbar>
    <ion-title>Chart 1</ion-title>
  </ion-navbar>
</ion-header>

<ion-content>
  <div echarts [options]="options" class="demo-chart"></div>
</ion-content>

src/pages/chart1/chart1.html

In the TypeScript file we set up the option object.

import {Component} from '@angular/core';

@Component({
  selector: 'page-chart1',
  templateUrl: 'chart1.html'
})
export class Chart1Page {

  options = {
    color: ['#3398DB'],
    grid: {
      left: '3%',
      right: '4%',
      bottom: '3%',
      containLabel: true
    },
    xAxis: [
      {
        type: 'category',
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
        axisTick: {
          alignWithLabel: true
        }
      }
    ],
    yAxis: [
      {
        type: 'value'
      }
    ],
    series: [
      {
        name: 'Test',
        type: 'bar',
        barWidth: '60%',
        data: [10, 52, 200, 334, 390, 330, 220]
      }
    ]
  };

}

src/pages/chart1/chart1.ts

Finally we have to add some styles to the scss file. We need to assign a width and a height to the div element that surrounds the canvas. You will not see the chart when width and height are not set.

page-chart1 {
  .demo-chart {
    width: 100%;
    margin-right: 20px;
    height: 75%;
  }

  .scroll-content {
    height: 100%
  }
}

src/pages/chart1/chart1.scss

Now you can start the app with ionic serve and you should see the following chart on the first tab. Chart1


Chart 2

The second chart is built the same way as the first one. The only difference is the ECharts config object.
Template: src/pages/chart2/chart2.html
TypeScript: src/pages/chart2/chart2.ts

One thing I want to show you with this example is that the charts have some interactivity built in. In this chart you can click or tap on the legend items and the chart will hide or show the corresponding data series.

chart2_1 chart2_2

Chart 3

The third chart demonstrates a way to update the series data of a chart on the fly. When you click or tap the start button the application starts a function with setInterval that updates the series data every two seconds with a random value.

The echarts directive supports the options option that contains the initial configuration and the merge option which allows us to update parts of the options. The value of merge will be used in echartsInstance.setOption() with notMerge = false. See the ECharts documentation for more information.

<ion-header>
  <ion-navbar>
    <ion-title>Chart 3</ion-title>
  </ion-navbar>
</ion-header>

<ion-content>
  <div echarts [options]="options" [merge]="datas" class="demo-chart"></div>
</ion-content>

<ion-footer>
  <button ion-button block [disabled]="running" (click)="start()" color="secondary">Start</button>
  <button ion-button block [disabled]="!running" (click)="stop()" color="danger">Stop</button>
</ion-footer>

src/pages/chart3/chart3.html

It's important that the applications assigns a new object to the datas instance variable each time it wants to set a new value.

this.datas = {
  series: [{
    data: [{value: Number((Math.random() * 100).toFixed(1))}]
  }]
};

It would not work when you only change the value like this
this.datas.series[0].data[0].value = Number((Math.random() * 100).toFixed(1))
The echarts directive only picks up the change when the reference to the datas object changes.

export class Chart3Page {

  running = false;
  private interval = null;

  options = {
    series: [{
      type: 'gauge',
      detail: {formatter: '{value}%'},
      data: [{value: 50, name: 'Sensor'}]
    }]
  };

  datas: any = null;

  start() {
    this.running = true;
    this.interval = setInterval(() => {
      this.datas = {
        series: [{
          data: [{value: Number((Math.random() * 100).toFixed(1))}]
        }]
      };
    }, 2000);
  }

  stop() {
    this.running = false;
    clearInterval(this.interval);
  }

}

src/pages/chart3/chart3.ts

Chart3


Chart 4

The fourth chart displays a simple graph. The template page and TypeScript code are similar to the first two examples.
Template: src/pages/chart4/chart4.html
TypeScript: src/pages/chart4/chart4.ts

This example demonstrates another option of the echarts directive: initOpts

<div echarts [initOpts]="initOpts" [options]="options" class="demo-chart"></div>

The value of initOpts will be used in echarts.init(). It supports the properties: devicePixelRatio, renderer, width and height.
See the ECharts documentation for more details.

In this example we use initOpts for setting the renderer to SVG.

  initOpts = {
    renderer: 'svg'
  };

Chart4


Chart 5

The next chart displays a heatmap. The template page and TypeScript code are again similar to the previous examples. Like in the second example you can interact with the legend.
Template: src/pages/chart5/chart5.html
TypeScript: src/pages/chart5/chart5.ts

Chart5


Chart 6

The last example shows you how to embed two charts into one page. In the template page we add two div tags with the echarts directive.

<ion-header>
  <ion-navbar>
    <ion-title>Chart 6</ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <div echarts [options]="options1" [merge]="updateOptions1" class="demo-chart"></div>
  <div echarts [options]="options2" [merge]="updateOptions2" class="demo-chart"></div>
</ion-content>

src/pages/chart6/chart6.html

In the scss file we use a media query to set the height of the div dependent on the device orientation. If the device is in landscape mode we set the height to 100% percent and in portrait mode we set it to 50%. This way you see both charts in portrait mode and one chart in landscape mode.

page-chart6 {

  @media screen and (orientation: portrait) {
    .demo-chart {
      width: 100%;
      height: 50%;
    }
  }

  @media screen and (orientation: landscape) {
    .demo-chart {
      width: 100%;
      height: 100%;
    }
  }

}

src/pages/chart6/chart6.scss

The TypeScript code defines the two configuration options (options1 and options2). When the user enters the page (ionViewWillEnter) the application starts a job that runs every second and generates random values. Like in the third example we use the merge option to update the chart with the new values.

import {Component} from '@angular/core';

@Component({
  selector: 'page-chart6',
  templateUrl: 'chart6.html',
})
export class Chart6Page {

  data1: any = [];
  data2: any = [];

  private now = new Date(2017, 9, 3);
  private static oneDay = 24 * 3600 * 1000;
  private value = Math.random() * 1000;
  private randomDataInterval: number;

  updateOptions1: any;
  updateOptions2: any;

  options1 = {
    title: {
      text: 'Dynamic Data 1'
    },
    tooltip: {
      trigger: 'axis',
      formatter: function (params) {
        params = params[0];
        const date = new Date(params.name);
        return date.getDate() + '/' + (date.getMonth() + 1) + '/' + date.getFullYear() 
             + ' : ' + params.value[1];
      },
      axisPointer: {
        animation: false
      }
    },
    xAxis: {
      type: 'time',
      splitLine: {
        show: false
      }
    },
    yAxis: {
      type: 'value',
      boundaryGap: [0, '100%'],
      splitLine: {
        show: false
      }
    },
    series: [{
      name: 'Sumulation Data',
      type: 'line',
      showSymbol: false,
      hoverAnimation: false,
      data: this.data1
    }]
  };

  options2 = {
    title: {
      text: 'Dynamic Data 2'
    },
    tooltip: {
      trigger: 'axis',
      formatter: function (params) {
        params = params[0];
        const date = new Date(params.name);
        return date.getDate() + '/' + (date.getMonth() + 1) + '/' + date.getFullYear() 
               + ' : ' + params.value[1];
      },
      axisPointer: {
        animation: false
      }
    },
    xAxis: {
      type: 'time',
      splitLine: {
        show: false
      }
    },
    yAxis: {
      type: 'value',
      boundaryGap: [0, '100%'],
      splitLine: {
        show: false
      }
    },
    series: [{
      name: 'Sumulation Data',
      type: 'line',
      showSymbol: false,
      hoverAnimation: false,
      data: this.data2
    }]
  };

  private randomData(): object {
    this.now = new Date(+this.now + Chart6Page.oneDay);
    this.value = this.value + Math.random() * 21 - 10;
    return {
      name: this.now.toString(),
      value: [
        [this.now.getFullYear(), this.now.getMonth() + 1, this.now.getDate()].join('/'),
        Math.round(this.value)
      ]
    }
  }

  ionViewWillEnter() {
    this.randomDataInterval = setInterval(() => {
      for (let i = 0; i < 5; i++) {
        this.data1.shift();
        this.data1.push(this.randomData());
        this.data2.shift();
        this.data2.push(this.randomData());
      }

      this.updateOptions1 = {
        series: [{
          data: this.data1
        }]
      };

      this.updateOptions2 = {
        series: [{
          data: this.data2
        }]
      };
    }, 1000);
  }

  ionViewWillLeave() {
    clearInterval(this.randomDataInterval);
  }

  constructor() {
    for (let i = 0; i < 1000; i++) {
      this.data1.push(this.randomData());
      this.data2.push(this.randomData());
    }
  }

}

src/pages/chart6/chart6.ts

chart6_1 chart6_2


You find the complete source code for the app on GitHub.