An early look at the Dart programming language

To content | To menu | To search

Simplot: A 2D Canvas Plotting Library for Dart

When writing code, I often find myself working with arrays of numbers that I need to have some way of visualizing. Since I spend a large part of my time these days coding with the Dart programming language, it seemed logical to build some sort of data visualizer in Dart. Specifically, I wanted an easy way to view waveforms with Dart that was at least somewhat similar to the basic plot capabilities of Matlab or one of the Matlab clones. Knowing that Dart already had an extensive and easy to use API for working with the HTML canvas, I figured that would be the most direct path to creating a simple data visualizer.

Introduction


When writing code, I often find myself working with arrays of numbers that I need to have some way of visualizing. Since I spend a large part of my time these days coding with the Dart programming language [1], it seemed logical to build some sort of data visualizer in Dart. Specifically, I wanted an easy way to view waveforms with Dart that was at least somewhat similar to the basic plot capabilities of Matlab or one of the Matlab clones. Knowing that Dart already had an extensive and easy to use API for working with the HTML canvas [2], I figured that would be the most direct path to creating a simple data visualizer. In this article, we'll take a look at some of the capabilities of a library that takes an array of numbers and plots them to a browser window. We'll look at a few examples, including one that makes use of websockets to move data that resides in a file to our simplot library for plotting to an HTML window. If you are interested in the underlying code, the simplot library is available on Github [3].

Using the Simplot Library


My goal in creating this library was to be able to create plots similar to the one shown below. Each plot should be able to handle multiple sets of data and multiple plots should be able to be grouped together either vertically or in a matrix.

simplot-example-3.png

We'll take a look at how each of these plots was created in just a moment, but let's start with the simplest usage of the library itself. To get started, simply add the following to your pubspec.yaml file:

simplot:
  git: git://github.com/scribeGriff/simplot.git

Then import the library to your app:

import 'package:simplot/simplot.dart';

Let's plot some arbitrary data:

library plottest;

import 'dart:html';
import 'package:simplot/simplot.dart';

void main() {
  var simpleData = [2, 17, 16,2, -0.5, 47, -12, 3, 8, 23.2, 67, 14, -7.5, 0, 31];
  plot(simpleData);
}

The y axis data is the only required parameter and the limits of the y axis are calculated using the provided data. The limits of the x axis are also calculated, either based on the default x axis data generated internally or from a List provided as an optional named parameter. Using the save() method of the Plot2D class brings up a PNG image in a separate browser window to allow for saving the image for further use:

plot(simpleData).save();

The saved PNG image is below.

simplot-simple1.png

The resulting plot is not much to look at, but the library comes with a range of optional named parameters and configurable methods to dress things up a bit. Let's take a look at that next.

Configuring the plot() Command


The plot() command accepts a number of optional named parameters to allow plotting up to 4 sets of data for a given set of x and y axes.

Optional Named Parameters for plot() Function
Parameter Type Description Default Value
xdata List x axis array derived from ydata
y2 List additional y plot on existing axis null
y3 List additional y plot on existing axis null
y4 List additional y plot on existing axis null
style1 String type of plot
(data, line, points, linepts, curve, curvepts)
linepts
style2 String type of plot
(data, line, points, linepts, curve, curvepts)
style1
style3 String type of plot
(data, line, points, linepts, curve, curvepts)
style1
style4 String type of plot
(data, line, points, linepts, curve, curvepts)
style1
color1 String color of first plotted data black
color2 String color of second plotted data forest green
color3 String color of third plotted data navy
color4 String color of fourth plotted data fire brick
linewidth int stroke width of plotted data 2px
range int indicates total number of separate subplot canvas elements 1
index int a reference to a particular subplot's canvas element 1
container String ID of container holding all subplot canvases #simPlotQuad

There are also a number of configurable public methods available to the plot() function:

Public Methods of the plot() Function
Method Required Parameters Optional Named Parameters
grid none none
xlabel String xlabelName String color, String font
ylabel String ylabelName String color, String font
title String title String color, String font
date none bool short
legend none String l1, l2, l3, l4
String font, bool top
xmarker num xval bool annotate, String color, var width
ymarker num yval String color, var width
save none String name

The library also contains two top level functions, saveAll() and requestDataWS(). The saveAll() command accepts an array of plots and generates a PNG image of the plots arranged in either a quad or linear format. The requestDataWS() receives an array of data from a server and plots it to a browser window. We'll look at an example of using requestDataWS() near the end of this article.

Supporting Top Level Functions of simplot Library
Function Required Parameters Optional Named Parameters
saveAll List plots num scale, bool quad
requestDataWS String host, int port String message, Element display

The following examples illustrate the creation of a variety of plot styles using many of the features just described.

Examples


The first subplot in the quad plot above examines the partial sums of the Fourier series for two cycles of a square wave. For this example, we need to import the ConvoLab library [4] to have access to the waveform generator and the Fourier series algorithm. So first we need to add the library to our pubspec.yaml file:

convolab:
    git: git://github.com/scribeGriff/ConvoLab.git

The fsps() function returns the solutions to the partial sums of the Fourier series in a map data structure. Before we can plot the data, therefore, we need to map each partial series to a List(). Also, by default our waveform generator generates a vector 512 points long and, as such, our solutions are also 512 points long. But before plotting, we crop our square wave waveform to 500 points. Since this cropped waveform is passed to the plot() command first and we are not passing a defined x axis array, its length determines the length of the x axis. All subsequent sets of data are then plotted against this same x axis, regardless of the length of the data sets.

library plotExamples;

import 'dart:html';
import 'dart:math';
import 'package:simplot/simplot.dart';
import 'package:convolab/convolab.dart';

void main() {
  var allPlots = new List();
  List waveform = square(2);
  var kvals = [2, 8, 32];
  var fourier = fsps(waveform, kvals);
  var square2 = waveform.sublist(0, 500);
  var f0 = fourier.psums[kvals[0]].map((x) => x.real).toList();
  var f1 = fourier.psums[kvals[1]].map((x) => x.real).toList();
  var f2 = fourier.psums[kvals[2]].map((x) => x.real).toList();

  var fspsCurve = plot(square2, y2:f0, y3:f1, y4:f2, style1:'curve',
      range:4, index:1);
  fspsCurve
    ..grid()
    ..title('Partial Sums of Fourier Series', color:'DarkSlateGray')
    ..legend(l1:'original', l2:'k = 2', l3:'k = 8', l4:'k = 32', top:false)
    ..xlabel('samples(n)')
    ..ylabel('signal amplitude')
    ..save(name:'fspsPlot');
  allPlots.add(fspsCurve);
}

We have set the range of the plot() function to 4 in anticipation of the 3 remaining plots. This has the effect of scaling each plot to a fraction of the size of a single plot. The index has been defined as 1 for the first subplot, which sets the canvas id of this subplot to #simPlot1. All subplot canvases share a common class of .simPlot. We also add this plot to the growable array allPlots so we can save all the plots to a single PNG image when we're finished. Executing the above code results in the following image when saved:

simplot-fourier-series.png

Our next example investigates the relationship between two forms of the sinc(x) function and the cos(x) function. Let's add the plot() commands to our plotExamples library that we started above:

var x = new List.generate(501, (var index) => (index - 250) / 10, growable:false);
var sincx = new List(x.length);
var sincpix = new List(x.length);
var cosx = new List(x.length);
for (var i = 0; i < x.length; i++) {
  if (x[i] != 0) {
    sincx[i] = sin(x[i]) / x[i];
    sincpix[i] = sin(x[i] * PI) / (x[i] * PI);
    cosx[i] = cos(x[i]);
  } else {
    sincx[i] = 1;
    sincpix[i] = 1;
    cosx[i] = cos(x[i]);
  }
}

var sincCurve = plot(cosx, xdata:x, y2:sincpix, y3:sincx, color1: 'LightBlue',
    color2: 'IndianRed', style1:'curve', range:4, index:2);
sincCurve
  ..grid()
  ..xlabel('x')
  ..ylabel('f(x)')
  ..xmarker(0)
  ..ymarker(0)
  ..legend(l1:'cos(x)', l2:'sinc(pi*x)', l3:'sinc(x)')
  ..title('Sinc Function (normalized and unnormalized)', color:'MidnightBlue')
  ..save(name:'sincPlot');
allPlots.add(sincCurve);

For this example we have defined a specific x axis that specifies x values from -25 to 25. We then added an xmarker() and ymarker() at the 0 value on the x axis and y axis respectively. The saved image looks like the following:

simplot-sinc-cosine.png

The next example plots the resistance, inductance and capacitance of a path network. We add several xmarker()s and set the annotate parameter to true to display the data on the plot where the marker crosses each plot. Note that we have also been passing the optional name parameter to the save() method. This is only necessary when saving multiple individual plots to prevent each subsequent save() from overwriting the previous image. If you are only saving a single image, just call the save() command with no parameters.

var resistance = [77.98, 104.23, 107.9, 74.61, 73.54, 91.63, 100.54, 85.19,
                  81.46, 87.64, 69.26, 90.86, 100.15, 95.24, 72.26, 74.86,
                  84.68, 93.61, 102.54, 103.18, 94.03, 87.13, 85.03, 66.59,
                  82.45, 81.66, 81.4, 81.58, 84.71];

var inductance = [97.993, 136.77, 142.215, 93.1, 90.956, 117.34, 131.299,
                  108.633, 103.196, 112.219, 85.533, 116.96, 130.688,
                  123.414, 89.781, 93.508, 107.893, 121.004, 134.263, 135.15,
                  121.547, 111.277, 108.154, 81.526, 104.312, 103.11, 102.674,
                  102.915, 107.367];

var capacitance = [88.52, 123.02, 114.13, 79.69, 78.06, 98.84, 100.09, 79.69,
                   75.69, 82.13, 63.36, 85.74, 97.07, 93.29, 74.33, 74.98,
                   78.84, 103.8, 109.18, 111.45, 107.04, 94.02, 93.01, 67.72,
                   89.42, 83.06, 79.6, 83.1, 87.73];

var rlcLines = plot(resistance, y2:inductance, y3:capacitance, linewidth:1, 
    range:4, index:3);
rlcLines
  ..grid()
  ..xlabel('Network (n)')
  ..ylabel('Impedance')
  ..title('RLC Impedance Values for 29 Path Network')
  ..legend(l1:'R (mOhms)', l2:'L (10^-2 nH)', l3: 'C (10^-3 pF)')
  ..xmarker(2, annotate:true)
  ..xmarker(20, annotate:true)
  ..save(name:'rlcPlot');
allPlots.add(rlcLines);

We've set the linewidth of the line plots to 1px and added xmarker()s at x = 2 and x = 20. Note that we have not specified a style so the plots are drawn using the default style of lines with points.

simplot-rlc-network.png

The last example is commonly referred to as a scatter or xy plot. Let's assume that we have some possibly correlated data in either a two-dimensional array or as an array of sets. We first separate out the pairs of points into an x array and a y array. We then generate a List that corresponds to the best fit for our data. Finally, we set the style1 parameter to 'points' for a point plot and the style2 parameter to a 'line'.

var fat_calories = [[9, 260],
                    [13, 320],
                    [21, 420],
                    [30, 530],
                    [31, 560],
                    [31, 550],
                    [34, 590],
                    [25, 500],
                    [28, 560],
                    [20, 440],
                    [5, 300]];

var xscatter = fat_calories.map((x) => x.elementAt(0)).toList();
var yscatter = fat_calories.map((y) => y.elementAt(1)).toList();
var bestFit = xscatter.map((x) => (11.7313 * x) + 193.852).toList();

var scatterPoints = plot(yscatter, xdata:xscatter, style1:'points', color1:'#3C3D36',
    y2:bestFit, style2:'line', color2: '#90AB76', range:4, index:4);
scatterPoints
  ..grid()
  ..xlabel('total fat (g)', color: '#3C3D36')
  ..ylabel('total calories', color: '#3C3D36')
  ..legend(l1: 'Calories from fat', l2: 'best fit: 11.7x + 193', top:false)
  ..date()
  ..title('Correlation of Fat and Calories in Fast Food', color: 'black')
  ..save(name:'scatterPlot');
allPlots.add(scatterPoints);

Since we expect this to be our last plot for our quad arrangement of subplots, we add a date() stamp which will appear just below the graph in the lower right side. Here's the final plot:

simplot-scatter.png

For each plot, we have been adding the plot object to our allPlots array. To save all plots to a single image, we can make use of the saveAll() function as follows:

WindowBase myPlotWindow = saveAll(allPlots);

Executing this command opens a new browser window with the image that we showed at the beginning of the article. By default, the plots are arranged in a quad orientation, but a linear arrangement in the vertical direction is also available by setting the named optional parameter quad to false, ie, quad:false.

Setting up the HTML and CSS


Displaying the plots in a web browser is left largely up to the user. All that is needed at a minimum is a div with either an ID of #simPlotQuad (the default value of the optional container parameter), or an ID of the user's choosing that is passed as the container parameter to plot() and a class called .simPlot. A simple example might look like the following:

The HTML:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>A Simple Plot Example</title>
    <link rel="stylesheet" href="simple_plot.css">
  </head>
  <body>
    <h1>SimPlot</h1>
    <p>A 2D canvas plotting tool in Dart.</p>
    <div id="container">
      <div id="simPlotQuad"></div>
    </div>
    <script type="application/dart" src="simple_plot.dart"></script>
    <script src="packages/browser/dart.js"></script>
  </body>
</html>

The CSS:

body {
  background-color: #F8F8F8;
  font-family: 'Open Sans', sans-serif;
  font-size: 14px;
  font-weight: normal;
  line-height: 1.2em;
  margin: 15px;
}
#container {
  width: 100%;
  position: relative;
  border: 1px solid #ccc;
  background-color: #fff;
  overflow:hidden;
}
#simPlotQuad {
   width: 1400px;
   margin: 0 auto;
   overflow: hidden;
}
.simPlot {
  margin: 30px;
  float: left;
}

This example will place multiple plots in a quad arrangement as in the example image above.

Working with Server Side Data


Of course, much of the data we'd like to plot resides in files either on a local machine or a server. Reading files in this way requires using the dart:io library, which is not compatible with the dart:html library that we use with simplot. We can easily get around this limitation by using a websocket, which has full API support for both client and server side Dart apps [5] [6]. The simplot library contains a top level function, requestDataWS(), which interacts with a server to retrieve data for plotting. Let's set up a simple example which reads a file into a list and then opens a websocket connection to send the data on to a client.

import 'dart:async';
import 'dart:io';
import 'dart:convert';

void main() {
  //Path to external file.
  String filename = '../../lib/external/data/sound4.txt';
  var host = 'local';
  var port = 8080;
  List data = [];
  Stream stream = new File(filename).openRead();
  stream
      .transform(UTF8.decoder)
      .transform(new LineSplitter())
      .listen((String line) {
        if (line.isNotEmpty) {
          data.add(double.parse(line.trim()));
        }
      },
      onDone: () {
        //connect with ws://localhost:8080/ws
        if (host == 'local') host = '127.0.0.1';
        HttpServer.bind(host, port).then((server) {
          print('Opening connection at $host:$port');
          server.transform(new WebSocketTransformer()).listen((WebSocket webSocket) {
            webSocket.listen((message) {
              var msg = JSON.decode(message);
              print("Received the following message: \n"
                  "${msg["request"]}\n${msg["date"]}");
              webSocket.add(JSON.encode(data));
            },
            onDone: () {
              print('Connection closed by client: Status - ${webSocket.closeCode}'
              ' : Reason - ${webSocket.closeReason}');
              server.close();
            });
          });
        });
      },
      onError: (e) {
        print('There was an error: $e');
      });
}

If you were to execute this code in the Dart Editor, for example, you would see the following:

Opening connection at 127.0.0.1:8080.

The data is now available to the client, which we can retrieve using the requestDataWS() function from the simplot library and then plot:

import 'dart:html';
import 'dart:async';
import 'package:simplot/simplot.dart';

void main() {
  String host = 'local';
  int port = 8080;
  var myDisplay = querySelector('#console');
  var myMessage = 'Send data request';
  Future reqData = requestDataWS(host, port, message:myMessage,
      display:myDisplay);
  reqData.then((data) {
    var sndLength = data.length;
    var sndRate = 22050;
    var sndSample = sndLength / sndRate * 1e3;
    var xtime = new List.generate(sndLength, (var index) =>
        index / sndRate * 1e3, growable:false);

    var wsCurve = plot(data, xdata:xtime, style1:'curve', color1:'green',
        linewidth:3);
    wsCurve
      ..grid()
      ..title('Sound Sample from Server')
      ..xlabel('time (ms)')
      ..ylabel('amplitude')
      ..save();
  });
}

The server prints the following information:

Received the following message:
Send data request
2013-06-14 14:34:28.502
Connection closed by client: Status - 1000 : Reason - Got the data. Thanks!

If you provide an optional display parameter to the requestDataWS(), the client prints the following message:

Opening connection at 127.0.0.1:8080
Successfully received data from the server.
Connection closed satisfactorily.

Finally, the data, which originated from a file on our local machine, is plotted to a canvas in the browser window:

simplot-websocket.png

Conclusion


The simplot library is a simple, lightweight tool for taking virtually any data that can be stored in a List and plotting that data to a canvas element in a browser window.


Works Cited

[1] The Dart Programming Language: Building Structured Web Apps
[2] Dart CanvasRenderingContext2D API
[3] The simplot library on Github
[4] The ConvoLab library on Github
[5] Dart Client Side Websocket API
[6] Dart Server Side Websocket API

Comments

1. On Saturday, February 8 2014, 18:17 by Richard

I would like to add a vertical line to a plot with another graph on it. There doesn't seem to be a way to do this with just simPlot, is that correct? I am aware I can do it with the default graphics api.

2. On Saturday, February 8 2014, 21:48 by Richard

@Richard: I think what you are asking for is to be able to have multiple y axes, likely because of data that needs different scales but want to share a common x axis. Simplot does not currently handle multiple y axes on the same plot unfortunately, but I have added it as a feature request since it would be great to be able to do that. Thanks for the feedback.