DEV Community

Cover image for Data visualization: Using amCharts with Perl and Mojo
Gaurav Rai
Gaurav Rai

Posted on • Updated on

Data visualization: Using amCharts with Perl and Mojo

In my previous article, I talked about Chart::Plotly. Today we will looking at creating the similar chart using another javascript library amCharts.

I have the opportunity to work on both v3 and v4 of amCharts. v3 is currently in maintenance mode. v4 is rewritten in typescript. One good thing about the library is there are lot of documentation and examples available on there website. Also you can use it in plain Javascript or integrate them into various application frameworks - React, Angular2+, Ember, Vue.js etc.
Also you don't need to be javascript expert to use it. It is highly configurable. You can use any syntax for configuration - TypeScript/ES6, JavaScript or JSON. For more details have a look at there excellent documentation.

Without further delay lets get started.

Creating the data config

We will use the exact same example as in previous article and try to create a multi line chart. But this time we will tweak the data format a little bit.

{
    "title": "Number of automobiles sold per day by manufacturer",
    "label": {
        "domainAxis": "Date",
        "rangeAxis": "Numbers of automobiles sold"
    },
    "data": [
        {
            "Date": "2020-04-15",
            "Honda": 10,
            "Toyota": 20,
            "Ford": 6,
            "Renault": 16
        },
        {
            "Date": "2020-04-16",
            "Honda": 3,
            "Toyota": 15,
            "Ford": 19,
            "Renault": 10
        },
        {
            "Date": "2020-04-17",
            "Honda": 5,
            "Toyota": 8,
            "Ford": 12,
            "Renault": 6
        },
        {
            "Date": "2020-04-18",
            "Honda": 9,
            "Toyota": 10,
            "Ford": 4,
            "Renault": 12
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

The reason we are using this format is because amCharts use array of objects to create chart where each object in the array represents a single data point. More info here.
We can use any data format but ultimately we have to convert it as array of object before creating chart which doesn't make sense (especially if you are doing it at time of page loading). So why not to create the data in the format which we can use easily.

Creating the mojo app

We we will be using the Mojolicious framework for server side. You can install it using single command as mentioned on website -

$ curl -L https://cpanmin.us | perl - -M https://cpan.metacpan.org -n Mojolicious
Enter fullscreen mode Exit fullscreen mode

It also have excellent documentation. Have a look at it to learn more.
The version I am using for this article is 9.14.
We will go ahead and create an app from command line.

$  mojo generate app MojoApp
Enter fullscreen mode Exit fullscreen mode

This command will generate a example application with proper directory structure for a MVC application. Easy peasy

📦mojo_app
┣ 📂lib
┃ ┣ 📂MojoApp
┃ ┃ ┗ 📂Controller
┃ ┃ ┃ ┗ 📜Example.pm
┃ ┗ 📜MojoApp.pm
┣ 📂public
┃ ┗ 📜index.html
┣ 📂script
┃ ┗ 📜mojo_app
┣ 📂t
┃ ┗ 📜basic.t
┣ 📂templates
┃ ┣ 📂example
┃ ┃ ┗ 📜welcome.html.ep
┃ ┗ 📂layouts
┃ ┃ ┗ 📜default.html.ep
┗ 📜mojo_app.yml

Now go inside the dir and try to run this app.

$ morbo ./script/mojo_app
Web application available at http://127.0.0.1:3000
Enter fullscreen mode Exit fullscreen mode

Open the browser and hit http://localhost:3000/ and you can see the welcome page.
If you open and look into MojoApp.pm you can see - get request on /(home page) is redirected to example controller (Example.pm) and function welcome is called inside that controller to fulfill the request. You can also see the template example/welcome.html.ep is rendered inside that function which you are seeing when you hit the http://localhost:3000/

We will be adding/modifying some parts of this dir structure to suit our need.

  1. We will be creating a 'mojo_app/etc/' dir to put our 'input_data.json' created previously.
  2. We will be renaming the default controller example to something meaningful
  3. Also we will be modifying the layouts\default.html.ep template.
  4. And we will be adding amCharts javascript library in template.

Update MojoApp.pm with the following changes in startup-

    # Normal route to controller
    $r->get('/')->to('charts#create_multi_line_chart');
Enter fullscreen mode Exit fullscreen mode

Create new or rename Example.pm to Charts.pm in Controller and update it with -

package MojoApp::Controller::Charts;
use Mojo::Base 'Mojolicious::Controller', -signatures;
use Mojo::JSON qw(decode_json encode_json);

sub read_json_file ($self, $json_file) {

    open(my $in, '<', $json_file) or $self->app->log->error("Unable to open file $json_file : $!");
    my $json_text = do { local $/ = undef; <$in>; };
    close($in) or $self->app->log->error("Unable to close file : $!");

    my $config_data = decode_json($json_text);
    return $config_data;
}

sub create_multi_line_chart ($self) {
    my $data_in_json = $self->read_json_file( "etc/input_data.json");

    $self->render(template => 'charts/multi_line_chart', chart_data => encode_json($data_in_json));
}

1;
Enter fullscreen mode Exit fullscreen mode

Here we are just reading the input json file and rendering the template with the chart data. Please note that create_multi_line_chart will be called at every load of page. Here I am reading the file every time. You can optimize it by reading it once at the start or caching it in case your input data doesn't change that often.
The JSON file is just an example. You can get this data from a database also.
Since we are talking about MVC framwork, why not move this data logic to Model.
Create lib\MojoApp\Model\Data.pm and update it with

package MojoApp::Model::Data;

use strict;
use warnings;
use experimental qw(signatures);
use Mojo::JSON qw(decode_json);

sub new ($class) {
    my $self = {};
    bless $self, $class;
    return $self;
}

sub _read_json_file ($self, $json_file) {
    open(my $in, '<', $json_file) or $self->app->log->error("Unable to open file $json_file : $!");
    my $json_text = do { local $/ = undef; <$in>; };
    close($in) or $self->app->log->error("Unable to close file : $!");

    my $config_data = decode_json($json_text);
    return $config_data;
}

sub get_data ($self) {
    my $data_in_json = $self->_read_json_file("etc/input_data.json");

    return $data_in_json;
}

1;
Enter fullscreen mode Exit fullscreen mode

Again, you can connect to DB and generate this data. For simplicity I am just getting the data from JSON file. (This data is actually generated from CouchDB :P).
Lets update our startup in MojoApp.pm

use MojoApp::Model::Data;

sub startup ($self) {

...
    # Helper to lazy initialize and store our model object
    $self->helper(
        model => sub ($c) {
            state $data = MojoApp::Model::Data->new();
            return $data;
        }
    );
...

}
Enter fullscreen mode Exit fullscreen mode

Lets remove the extra thing from controller Charts.pm and use this helper.

package MojoApp::Controller::Charts;
use Mojo::Base 'Mojolicious::Controller', -signatures;
use Mojo::JSON qw(encode_json);

sub create_multi_line_chart ($self) {
    my $data_in_json = $self->model->get_data();

    $self->render(template => 'charts/multi_line_chart', chart_data => encode_json($data_in_json));
}

1;
Enter fullscreen mode Exit fullscreen mode

We updated the controller to use the model for data and render the template.
Now lets go to template section and update/create a folder name charts in which we will be creating template multi_line_chart.html.ep.
Also lets update the default.html.ep template a little bit.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title><%= title %></title>

        %= content 'head'
    </head>
    <body>
        <div>
            %= content
        </div>
        %= content 'end'
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

This is our layout template and we will be using it at our every page throughout the website. There are different placeholders where we will genrating data for different pages. For more details have a look at Mojolicious::Guides::Rendering and Mojo::Template
In multi_line_chart.html.ep

% layout 'default';
% title 'Charts';

% content_for 'head' => begin
    <link rel="stylesheet" type="text/css" href="css/charts.css">
% end

<div id="chartdiv"></div>

% content_for 'end' => begin
    %= javascript "https://cdn.amcharts.com/lib/4/core.js"
    %= javascript "https://cdn.amcharts.com/lib/4/charts.js"
    %= javascript "https://cdn.amcharts.com/lib/4/themes/animated.js"

    %= javascript "js/multi_line_chart.js"

    %= javascript begin
        createMultiLineChart(<%== $chart_data %>);
    % end
% end
Enter fullscreen mode Exit fullscreen mode

In simple language, we are saying here - use the default.html.ep template, update the title of the page to 'Charts', append the head section with the css for this page, in the page body create a 'div' with 'id' chartdiv and in the end of the body add the mentioned javascripts file.
The $chart_data which we are using in javascript, gets passed from server side while rendering the template in create_multi_line_chart method. It is encoded in JSON for which we are decoding on client side.
The top 3 javascript included are amCharts library.
Now lets create charts.css and multi_line_chart.js which we are referencing here. These will be automatically served from 'public' dir.
In public/css/charts.css

#chartdiv {
    width: 850px;
    height: 550px;
}
Enter fullscreen mode Exit fullscreen mode

Its very small css where we just setting the dimensions of the chart.
In public/js/multi_line_chart.js

function createSeries(chart, axis, field, name) {
    // Create series
    var series = chart.series.push(new am4charts.LineSeries());
    series.dataFields.dateX = "Date";
    series.dataFields.valueY = field;
    series.strokeWidth = 2;
    series.xAxis = axis;
    series.name = name;
    series.tooltipText = "{name}: [bold]{valueY}[/]";

    var bullet = series.bullets.push(new am4charts.CircleBullet());

    return series;
}

function createMultiLineChart(chartData) {
    // Themes begin
    am4core.useTheme(am4themes_animated);

    var chart = am4core.create("chartdiv", am4charts.XYChart);

    // Increase contrast by taking every second color
    chart.colors.step = 2;
    // Add title to chart
    var title = chart.titles.create();
    title.text = chartData["title"];

    // Add data to chart
    chart.data = chartData["data"];

    // Create axes
    var dateAxis = chart.xAxes.push(new am4charts.DateAxis());
    dateAxis.title.text = chartData["label"]["domainAxis"];

    var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
    valueAxis.title.text = chartData["label"]["rangeAxis"];

    //var single_data_item = chartData["data"][0];
    var series1 = createSeries(chart, dateAxis, "Toyota", "Toyota");
    var series2 = createSeries(chart, dateAxis, "Ford", "Ford");
    var series3 = createSeries(chart, dateAxis, "Honda", "Honda");
    var series4 = createSeries(chart, dateAxis, "Renault", "Renault");

    // Add legend
    chart.legend = new am4charts.Legend();

    // Add cursor
    chart.cursor = new am4charts.XYCursor();
    chart.cursor.xAxis = dateAxis;

    // Add scrollbar
    chart.scrollbarX = new am4core.Scrollbar();

    // Add export menu
    chart.exporting.menu = new am4core.ExportMenu();
}
Enter fullscreen mode Exit fullscreen mode

I have added the comments for the description. You can look at reference and xy-chart for more details.
The function createMultiLineChart created here is the one which we are calling in multi_line_chart.html.ep.

Save it and refresh the home page.
Alt Text
I have tried to use mostly the default configuration. The screenshot above is not doing the justice to the actual dynamic chart. For that you have to run and see it for yourself.

Now lets try to modify the public/js/multi_line_chart.js with some more configuration. As I mentioned before it is highly configurable and its difficult to cover each and every thing so I will try to cover whatever I can.

function createSeries(chart, axis, field, name) {
    // Create series
    var series = chart.series.push(new am4charts.LineSeries());
    series.dataFields.dateX = "Date";
    series.dataFields.valueY = field;
    //series.dataFields.categoryX = "Date";
    series.strokeWidth = 2;
    series.xAxis = axis;
    series.name = name;
    series.tooltipText = "{name}: [bold]{valueY}[/]";
    //series.fillOpacity = 0.8;

    // For curvey lines
    series.tensionX = 0.8;
    series.tensionY = 1;

    // Multiple bullet options - circle, triangle, rectangle etc.
    var bullet = series.bullets.push(new am4charts.CircleBullet());
    bullet.fill = new am4core.InterfaceColorSet().getFor("background");
    bullet.fillOpacity = 1;
    bullet.strokeWidth = 2;
    bullet.circle.radius = 4;

    return series;
}

function createMultiLineChart(chartData) {
    // Themes begin
    am4core.useTheme(am4themes_animated);

    var chart = am4core.create("chartdiv", am4charts.XYChart);

    // Increase contrast by taking every second color
    chart.colors.step = 3;
    //chart.hiddenState.properties.opacity = 0; // this creates initial fade-in

    // Add title to chart
    var title = chart.titles.create();
    title.text = chartData["title"];
    title.fontSize = 25;
    title.marginBottom = 15;

    chart.data = chartData["data"];

    // Create axes - for normal Axis
    // var categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis());
    // categoryAxis.dataFields.category = "Date";
    // categoryAxis.renderer.grid.template.location = 0;

    // Create axes - for Date Axis
    var dateAxis = chart.xAxes.push(new am4charts.DateAxis());
    //dateAxis.dataFields.category = "Date";
    dateAxis.renderer.grid.template.location = 0;
    dateAxis.renderer.minGridDistance = 50;
    dateAxis.title.text = chartData["label"]["domainAxis"];

    var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
    //valueAxis.renderer.line.strokeOpacity = 1;
    //valueAxis.renderer.line.strokeWidth = 2;
    valueAxis.title.text = chartData["label"]["rangeAxis"];

    var series1 = createSeries(chart, dateAxis, "Toyota", "Toyota");
    var series2 = createSeries(chart, dateAxis, "Ford", "Ford");
    var series3 = createSeries(chart, dateAxis, "Honda", "Honda");
    var series4 = createSeries(chart, dateAxis, "Renault", "Renault");

    // Add legend
    chart.legend = new am4charts.Legend();

    // Add cursor
    chart.cursor = new am4charts.XYCursor();
    chart.cursor.xAxis = dateAxis;

    // Add scrollbar
    chart.scrollbarX = new am4core.Scrollbar();

    // Add export menu
    chart.exporting.menu = new am4core.ExportMenu();
}
Enter fullscreen mode Exit fullscreen mode

Now we will try to see the output again -
Alt Text
Somewhat better than the previous one. The three dots on the top right corner gives to more options to interact like - downloading the image as png or svg, getting the data in JSON or CSV format, printing the chart etc.
Also there are certain plugins available which you can use to enhance the experience. More details at Plugins.

As I mentioned there are lot of config options and I haven't
covered all of them. But I will try to cover it in my next installment where I will create the same chart in React.js using Typescript/ES6. Also the above js file can be modified a little bit to make it generalized for any type of multi line chart(especially the 'createSeries' call). I will leave that as an exercise.

The above example is available at github.

Perl onion logo taken from here
Mojolicious logo taken from here
amCharts logo taken form here

Top comments (5)

Collapse
 
brxfork profile image
Philippe Bricout • Edited

Thank you for your post.
Here are some changes to make this app compatible with Mojolicious 8.12 (debian) :

diff --git a/amCharts/mojo_app/lib/MojoApp.pm b/amCharts/mojo_app/lib/MojoApp.pm
index 6858520..aec2e47 100644
--- a/amCharts/mojo_app/lib/MojoApp.pm
+++ b/amCharts/mojo_app/lib/MojoApp.pm
@@ -6,7 +6,7 @@ use MojoApp::Model::Data;
 sub startup ($self) {

     # Load configuration from config file
-    my $config = $self->plugin('NotYAMLConfig');
+    my $config = $self->plugin('Config');

     # Configure the application
     $self->secrets($config->{secrets});
diff --git a/amCharts/mojo_app/script/mojo_app b/amCharts/mojo_app/script/mojo_app
index b097e09..5c1edb5 100644
--- a/amCharts/mojo_app/script/mojo_app
+++ b/amCharts/mojo_app/script/mojo_app
@@ -3,8 +3,10 @@
 use strict;
 use warnings;

-use Mojo::File qw(curfile);
-use lib curfile->dirname->sibling('lib')->to_string;
+use FindBin;
+BEGIN { unshift @INC, "$FindBin::Bin/../lib" }
+#use Mojo::File qw(curfile);
+#use lib curfile->dirname->sibling('lib')->to_string;
 use Mojolicious::Commands;

 # Start command line interface for application
Enter fullscreen mode Exit fullscreen mode

... and create mojo_app.conf (beside mojo_app.yml ) :

{
  secrets => ['7c121257b1db016af25743be1e09177f634ba5e5'],
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
raigaurav profile image
Gaurav Rai

Thanks. I forgot to mention the current code is written in mojo version 9.14.
The YAML config were introduced in 8.57 and curfile in 8.25.
I must say, I liked the JSON more than YAML. But since most of cloud deployment tools are using YAML so they changed it to that. Maybe it will take some time to get accustom to it. :)

Collapse
 
grinnz profile image
Dan Book

I would recommend lib::relative if you don't have access to curfile yet:

use lib::relative '../lib';
Enter fullscreen mode Exit fullscreen mode
Collapse
 
slobo profile image
Slobodan Mišković

Thanks for the article.

What is the reasoning of this line - does it cover some edge case?

var chartData = JSON.parse(JSON.stringify((<%== $chart_data %>)))
Enter fullscreen mode Exit fullscreen mode

From what I understand, $chart_data is already encoded as json, so you should be able to just do

var chartData = <%== $chart_data %>)
Enter fullscreen mode Exit fullscreen mode

Or skip the assignment altogether, and pass it in right away:

createMultiLineChart(<%== $chart_data %>);
Enter fullscreen mode Exit fullscreen mode

Cheers

Collapse
 
raigaurav profile image
Gaurav Rai

Thanks for the catch. The JSON parsing is absolutely not needed on JS side.
I remember I was doing some hit and trail before encoding it as JSON on server side and forgot it to remove later. Since everything is working fine, I didn't catch it. That is an extra performance overhead on JS side by cloning it.
I will remove it. Thank you.