Customizing Vizzu Charts - Real-time data model

simzer

Simon L谩szl贸

Posted on April 27, 2023

Customizing Vizzu Charts - Real-time data model

Have you ever wondered how to make your charts come to life with real-time data updates? Well, good news! Connecting a real-time data model to a Vizzu chart is easier than you might think. Vizzu is a data visualization tool you can easily use in web applications. Its simple API lets you update your charts in real time as your data changes.

This how-to guide will have two parts. The first part will show you how to connect an arbitrary real-time data source to a Vizzu chart. In the second part, we will build a real-time mechanical model of bouncing balls. We will then visualize the movements of these balls in real time on the Vizzu chart.

Bouncing Balls

So, let's dive into how you can connect your real-time data model to a Vizzu chart!

Part 1: Creating a Vizzu Chart and Connecting it to a Realtime Data Source

The first step is to import the Vizzu library into your Javascript file. We will use the latest version from CDN:

import Vizzu from 'https://cdn.jsdelivr.net/npm/vizzu@latest/dist/vizzu.min.js';
Enter fullscreen mode Exit fullscreen mode

The next step is to create a Vizzu chart. You can do this by creating a new instance of the Vizzu class and passing in the ID of the container element placed in your HTML file where you want to display the chart:

<div id="vizzu-container" style="width: 500px; height: 350px"></div>
Enter fullscreen mode Exit fullscreen mode
let vizzu = new Vizzu('vizzu-container');
Enter fullscreen mode Exit fullscreen mode

You need to define an update function to update the chart with real-time data. This function will be called repeatedly to update the chart with new data:

function update(model, chart) 
{
  const timeStep = 0.01;
  model.update(timeStep);
  return chart.animate({ 
    data: getDataFromModel(model),
    config: getChartConfig() 
  }, timeStep)
  .then(chart => update(model, chart));
}
Enter fullscreen mode Exit fullscreen mode

This update function takes two parameters: the real-time data model and the Vizzu chart. It uses the model.update function to update the data model and then uses the chart.animate method to update the chart with the new data.

Before updating the chart, you have to wait for the chart to initialize. This can be done using the initializing property of the Vizzu instance:

vizzu.initializing.then(chart => {
  let model = new Model(chart);
  return update(model, chart);
});
Enter fullscreen mode Exit fullscreen mode

Now we have a template for an infinitely updating chart. What we have to do in the next part is to implement the data model
and the 'getDataFromModel()' and 'getChartConfig()' functions which will get the data from the model in the Vizzu required format and describe the chart we want to show the data.

Part 2: Building the Data Source, a Mechanical Model of Bouncing Balls

For the model, we define three classes:

  • Vector - a simple 2D vector class to help with vector arithmetics,
  • BouncingBall - representing the mechanical model of a ball, and
  • Model - which will contain a set of balls bouncing around.

The Vector class

class Vector {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static Random() {
    return new Vector(Math.random(), Math.random());
  }

  mul(multiplier) {
    return new Vector(
      this.x * (multiplier?.x || multiplier), 
      this.y * (multiplier?.y || multiplier));
  }

  add(other) {
    return new Vector(this.x + other.x, this.y + other.y);
  }
}
Enter fullscreen mode Exit fullscreen mode

We will need addition and scalar multiplication of 2D vectors, and we added a factory method to
create vectors with random coordinates.

The BouncingBall class

class BouncingBall {

  static lastIndex = 0;

  constructor(massToSize) {
    this.index = BouncingBall.lastIndex++;
    this.position = Vector.Random();
    this.speed = (Vector.Random()).mul(new Vector(3, 5));
    this.mass = Math.random();
    this.radius = Math.sqrt(this.mass)*massToSize;
  }
Enter fullscreen mode Exit fullscreen mode

In the constructor, we give each instance a random position, speed, and mass and assign a unique index for each. We also calculate a radius with a factor that we extract from the chart to be able to bounce the balls back from the walls when
their side touches the walls.

  update(timeStep) {
    const g = 9.81;
    const friction = 0.5;
    let acceleration = this.speed.mul(-friction * this.mass).add(new Vector(0, -g));
    this.speed = this.speed.add(acceleration.mul(timeStep));
    this.position = this.position.add(this.speed.mul(timeStep));
    this.collision('y', v => v);
    this.collision('x', v => v);
    this.collision('x', v => 1 - v);
  }
Enter fullscreen mode Exit fullscreen mode

Next, we calculate the ball's position for the next time point. We will assume a gravitational field and air friction.
We also check in each cycle if the balls collide with the side walls or the bottom, and we bounce them back if so.

  collision(coordinate, conversion) {
    const collisionDumping = 0.6;
    let side = conversion(this.position[coordinate]) - this.radius;
    if (side < 0) { 
      this.position[coordinate] = conversion(- side + this.radius);
      this.speed[coordinate] *= -collisionDumping;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

In the collision detection method, we check if the ball went "outside" and if so, we switch the direction of its speed with a dumping factor, and move it back to the "right side" of the wall.

The Model class

class Model {
  constructor(chart, ballCount = 150) {
    let massToSize = chart.getComputedStyle().plot.marker.circleMaxRadius;
    this.time = 0;
    this.balls = [];
    for (let i = 0; i < ballCount; i++)
      this.balls.push(new BouncingBall(massToSize));
  }
Enter fullscreen mode Exit fullscreen mode

The model class is simply containing the specified number of balls independently, which we initialize in the constructor, then in the update method, we go through them and update
their positions:

  update(timeStep) 
  {
    for (let ball of this.balls) ball.update(timeStep);
    this.time += timeStep;
  }
}
Enter fullscreen mode Exit fullscreen mode

Connecting the Model to the Chart

First, we need to write a function that will convert the data from the model to the format required by the Vizzu chart:

function getDataFromModel(model) {
  return {
    series: [
      { name: 'index', type: 'dimension', values: model.balls.map(ball => `${ball.index}`) },
      { name: 'x', type: 'measure', values: model.balls.map(ball => ball.position.x) },
      { name: 'y', type: 'measure', values: model.balls.map(ball => ball.position.y) },
      { name: 'size', type: 'measure', values: model.balls.map(ball => ball.mass) }
    ]
  };
}
Enter fullscreen mode Exit fullscreen mode

We will need their coordinates and mass to visualize the balls, which we will use as size. We also include their index and convert it to string to have a categorical data series
to distinguish between the balls.

Now that we have the data, we can connect its data series to the chart configuration:

function getChartConfig() {
  return {
    x: { set: 'x', range: { min: 0, max: 1} },
    y: { set: 'y', range: { min: 0, max: 1} },
    color: 'index',
    size: 'size',
    geometry: 'circle',
    legend: null
  };
}
Enter fullscreen mode Exit fullscreen mode

Here we define a scatterplot by setting the geometry to circle and linking the coordinates and size to the x and y axis and size channel, respectively. Of course, we must add the categorical index series to have a separate marker for all the balls. For that, we use the color channel. We also switched off the legend as it would not add meaningful information to our visualization.

Now that we put together the whole code, we can run it and watch the balls bouncing around the chart!

馃挅 馃挭 馃檯 馃毄
simzer
Simon L谩szl贸

Posted on April 27, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related