LineChartPainter

LineChart Painter

LineChart Painter allow chart data to be rendered within a Canvas.

The class is imported using the following:

var LineChartPainter = require('paraviewweb/src/Rendering/Painter/LineChartPainter'),
instance = new LineChartPainter("Display data along x: {x}");

constructor(title, markerColor=”#0000FF”, colors=[“#e1002a”, “#417dc0”, “#1d9a57”, “#e9bc2f”, “#9b3880”])

Creates an instance of a LineChartPainter.

updateData(data)

Update the data to render and emit a PainterReady event.
The data format should be as follow:

{
xRange: [ 0 , 100],
fields: [
{ name: 'Temperature', data: [y0, y1, ..., yn]},
...
]
}

setTitle(title)

Update the title that should be rendered within the Line Chart.

If a String {x} is embedded inside the title, it will be replaced with the
marker location value.

setMarkerLocation(xRatio)

Set a line marker for the line chart. The value provided should be a fraction
of the range along X. Its value should be within the following range [0.0, 1.0].

enableMarker(show)

Choose to show or hide the marker.

isReady() : Boolean

Method that can be used to see if the Painter is ready for painting the data.

paint(ctx, location)

Method called by the renderer to let the painter fill the given location of the
canvas context.

The location object should be defined as follow:

var location = {
x: 5,
y: 10,
width: 100,
height: 200
};

onPainterReady(callback)

Method used to register the callback function that should be called when the
painter is ready to be used.

getControlWidgets() : Array[String…]

Return the list of widgets that should be used to control this painter.
(None for this painter)

Source

index.js
import Monologue from 'monologue.js';

const PAINTER_READY = 'painter-ready';

// ----------------------------------------------------------------------------
// Helpers
// ----------------------------------------------------------------------------

function paintField(ctx, location, field, range) {
let count;
let min = Number.MAX_VALUE;
let max = Number.MIN_VALUE;

const xOffset = location.x;
const yOffset = location.y;
const width = location.width;
const height = location.height;
const values = field.data;
const size = values.length;
const xValues = new Uint16Array(size);

// Compute xValues and min/max
count = size;
while (count) {
count -= 1;
const value = values[count];
min = Math.min(min, value);
max = Math.max(max, value);
xValues[count] = xOffset + Math.floor(width * (count / (size - 1)));
}

// Update range if any provided
if (range) {
min = range[0];
max = range[1];
}

const scaleY = height / (max - min);

function getY(idx) {
let value = values[idx];
value = value > min ? (value < max ? value : max) : min;
return yOffset + height - Math.floor((value - min) * scaleY);
}

// Draw line
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = field.color;
ctx.moveTo(xValues[0], getY(0));
for (let idx = 1; idx < size; idx++) {
if (Number.isNaN(values[idx])) {
if (idx + 1 < size && !Number.isNaN(values[idx + 1])) {
ctx.moveTo(xValues[idx + 1], getY(idx + 1));
}
} else {
ctx.lineTo(xValues[idx], getY(idx));
}
}
ctx.stroke();
}

// ----------------------------------------------------------------------------

function paintMarker(ctx, location, xRatio, color) {
if (xRatio < 0 || xRatio > 1) {
return;
}

const y1 = location.y;
const y2 = y1 + location.height;
const x = location.x + Math.floor(xRatio * location.width);

ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = color;
ctx.moveTo(x, y1);
ctx.lineTo(x, y2);
ctx.stroke();
}

// ----------------------------------------------------------------------------

function paintText(ctx, location, xOffset, yOffset, text, color = '#000000') {
ctx.fillStyle = color;
ctx.font = '20px serif';
ctx.textBaseline = 'top';
ctx.fillText(text, location.x + xOffset, location.y + yOffset);
}

// ----------------------------------------------------------------------------

// function interpolate(values, xRatio) {
// var size = values.length,
// idx = size * xRatio,
// a = values[Math.floor(idx)],
// b = values[Math.ceil(idx)],
// ratio = idx - Math.floor(idx);
// return ((b-a)*ratio + a).toFixed(5);
// }

// ----------------------------------------------------------------------------

export default class LineChartPainter {
constructor(
title,
markerColor = '#0000FF',
colors = ['#e1002a', '#417dc0', '#1d9a57', '#e9bc2f', '#9b3880']
) {
this.data = null;
this.colors = colors;
this.markerColor = markerColor;
this.markerLocation = -1;
this.showMarker = true;
this.title = title;
this.fillBackground = null;
this.controlWidgets = [];
}

// ----------------------------------------------------------------------------
// Expected data structure
// {
// xRange: [ 0 , 100],
// fields: [
// { name: 'Temperature', data: [y0, y1, ..., yn], range: [0, 1]},
// ...
// ]
// }

updateData(data) {
let colorIdx = 0;

// Keep data
this.data = data;

// Assign color if no color
data.fields.forEach((field) => {
if (!field.color) {
field.color = this.colors[colorIdx % this.colors.length];
colorIdx += 1;
}
});

this.emit(PAINTER_READY, this);
}

// ----------------------------------------------------------------------------

setBackgroundColor(color) {
this.fillBackground = color;
}

// ----------------------------------------------------------------------------

setTitle(title) {
this.title = title;
this.emit(PAINTER_READY, this);
}

// ----------------------------------------------------------------------------

setMarkerLocation(xRatio) {
this.markerLocation = xRatio;

this.emit(PAINTER_READY, this);
}

// ----------------------------------------------------------------------------

enableMarker(show) {
if (this.showMarker !== show) {
this.showMarker = show;
this.emit(PAINTER_READY, this);
}
}

// ----------------------------------------------------------------------------

isReady() {
return this.data !== null;
}

// ----------------------------------------------------------------------------

paint(ctx, location) {
let xValue = '?';

if (!this.data) {
return;
}

// Empty content
ctx.clearRect(
location.x - 1,
location.y - 1,
location.width + 2,
location.height + 2
);

if (this.fillBackground) {
ctx.fillStyle = this.fillBackground;
ctx.fillRect(location.x, location.y, location.width, location.height);
}

// Paint each field
this.data.fields.forEach((field) => {
if (field.active === undefined || field.active) {
paintField(ctx, location, field, field.range);
}
});

// Paint marker if any
if (this.showMarker) {
paintMarker(ctx, location, this.markerLocation, this.markerColor);
}

// Paint tile if any
if (this.title) {
if (
this.data.xRange &&
this.data.xRange.length === 2 &&
!Number.isNaN(Number(this.markerLocation))
) {
xValue =
(this.data.xRange[1] - this.data.xRange[0]) * this.markerLocation +
this.data.xRange[0];
if (xValue.toFixed) {
xValue = xValue.toFixed(5);
}
}
paintText(ctx, location, 10, 10, this.title.replace(/{x}/g, `${xValue}`));
}
}

// ----------------------------------------------------------------------------

onPainterReady(callback) {
return this.on(PAINTER_READY, callback);
}

// ----------------------------------------------------------------------------
// Method meant to be used with the WidgetFactory

getControlWidgets() {
return this.controlWidgets;
}
}

Monologue.mixInto(LineChartPainter);