How to use Chart.js to Plot Data from FrontPanel

There are a few ways to get data from FrontPanel endpoints on the FPGA, the most common being WireOuts and PipeOuts. FrontPanel Alloy allows you to easily read this data and display it in a visually appealing chart utility, like Chart.js. This guide will show how this can be done.

To begin, you’ll want to ensure your environment is set up as explained in the FrontPanel Alloy Application Development guide steps one through three.

Install Library Dependencies

To begin, you will need to install the frontpanel-alloy-core and React-Chartjs-2 library package dependencies.

npm install @opalkelly/frontpanel-alloy-coreCode language: CSS (css)
npm install react-chartjs-2

Create a PlotView Component

Create a React Component that wraps a Line Chart.

import React, { Component, ReactNode } from "react";

import './PlotView.css';

import {
    Chart as ChartJS,
    LinearScale,
    PointElement,
    LineElement,
    Title,
    Legend,
    ChartOptions
} from "chart.js";

import { Line } from "react-chartjs-2";

import { IFrontPanel, WorkQueue } from "@opalkelly/frontpanel-alloy-core";

export type Vector2D = {
    x: number;
    y: number;
};

interface PlotViewProps {
    label: string;
}

interface PlotViewState {
    device: IFrontPanel;
}

class PlotView extends Component<PlotViewProps, PlotViewState>{
    private readonly _WorkQueue: WorkQueue = new WorkQueue();

    private _ChartRef: React.RefObject<ChartJS<"line">>;
    private _ChartOptions: ChartOptions<"line">;
    private _ChartData;

    private readonly _SamplePeriodMilliseconds: number = 15;
    private readonly _SampleCount: number = 256;
    private readonly _SampleWindowLength: number = this._SampleCount * this._SamplePeriodMilliseconds;

    constructor(props: PlotViewProps) {
        super(props);

        ChartJS.register(LinearScale, PointElement, LineElement, Title, Legend);

        this._ChartRef = React.createRef();

        this._ChartOptions = {
            animation: false,
            responsive: true,
            maintainAspectRatio: false,
            scales: {
                x: {
                    type: "linear",            
                    min: 0.0,
                    max: this._SampleWindowLength,                    
                    ticks: { 
                        display: true, 
                        stepSize: 100.0,
                        callback: function(value, index) {
                            // Hide every 2nd tick label
                            return index % 2 === 0 ? (Number(value) / 1000.0).toPrecision(3).toString() : '';
                        },
                    },
                    title: { display: true, text: "Time" },
                },
                y: {
                    type: "linear",
                    min: 0,
                    max: 512,
                    ticks: { display: true, stepSize: 5 },
                    title: { display: true, text: "Value" }
                }
            },
            plugins: {
                legend: {
                    position: "bottom" as const
                },
                title: {
                    display: true,
                    text: props.label
                }
            }
        };

        const channels: Vector2D[][] = Array(2);

        channels[0] = new Array<Vector2D>();
        channels[1] = new Array<Vector2D>();
       
        this._ChartData = {
            datasets: [
                {
                    label: "Channel 1",
                    data: channels[0],
                    pointRadius: 0,
                    borderColor: "rgb(255, 99, 132)",
                    backgroundColor: "rgba(255, 99, 132, 0.5)"
                },
                {
                    label: "Channel 2",
                    data: channels[1],
                    pointRadius: 0,
                    borderColor: "rgb(53, 162, 235)",
                    backgroundColor: "rgba(53, 162, 235, 0.5)"
                }
            ]
        };

        this.state = {
            device: window.FrontPanel
        };
    }

    render(): ReactNode {
        return (
            <div className="PlotViewContainer">
                <Line 
                    ref={this._ChartRef} 
                    options={this._ChartOptions} 
                    data={this._ChartData} />
            </div>
        );
    }
}

export default PlotView;
Code language: JavaScript (javascript)

This defines a React Component named PlotView whose state specifies the label that will be applied as the title for the Line Chart that it renders. The component maintains a reference to the rendered Line Chart. It will use this reference to update the content displayed in the Chart.

Add the PlotView Style Sheet

Create a Stylesheet named ‘PlotView.css’.

.PlotViewContainer {
    width: 100vw; /* Full width of the viewport */
    height: 100vh; /* Full height of the viewport */
    background-color: #ffffff;
    font-size: calc(10px + 2vmin);
    color: white;
    margin: 0; /* Remove default margin */
    padding: 0; /* Remove default padding */
    box-sizing: border-box; /* Include padding and border in the element's size */
  }Code language: CSS (css)

This makes the Plot fill up the window and makes the background color white.

Add method to Update the Chart Data

Add a public method named UpdateChartData to the PlotView.

Using WireOut Data

private _InitialTimeStamp = 0.0;

private async UpdateChartData(): Promise<void> {
    await this._WorkQueue.Post(async () => {
        // Retrieve the latest data from the device
        await this.state.device.updateWireOuts();

        const currentTimeStamp = performance.now();

        const data = await this.state.device.getWireOutValue(0x20) & 0xff;

        // Set the initial time stamp when the first data element is received
        if(this._ChartData.datasets[0].data.length === 0) {
            this._InitialTimeStamp = currentTimeStamp;
        }

        // Append the new data element
        this._ChartData.datasets[0].data.push({ x: currentTimeStamp - this._InitialTimeStamp, y: data });

        // Remove the first data element and adjust the x-axis range when the data extends past the maximum
        if(currentTimeStamp >= (this._ChartData.datasets[0].data[0].x + this._SampleWindowLength)) {
            this._ChartData.datasets[0].data.shift();

            if(this._ChartRef.current != null) {
                this._ChartRef.current.options.scales!.x!.min = this._ChartData.datasets[0].data[0].x;
                this._ChartRef.current.options.scales!.x!.max = this._ChartData.datasets[0].data[0].x + this._SampleWindowLength;
            }
        }
    });

    // Update the Chart
    if (this._ChartRef.current != null) {
        this._ChartRef.current.update();
    }
}Code language: JavaScript (javascript)

This method retrieves a WireOut data value at a specific point in time and plots it using a horizontal axis representing time. Note that the operations that depend on the data being retrieved are placed entirely in the function submitted to the WorkQueue.

Using PipeOut Data

A more efficient and faster way to acquire data is by using a PipeOut.

private _SampleSize = 4; // 4 bytes per sample

private async UpdateChartData(): Promise<void> {
    await this._WorkQueue.Post(async () => {
        const outputData: ArrayBuffer = await this.state.device.readFromPipeOut(
            0xa0,
            this._SampleCount * this._SampleSize // 1024 byte read, 256 samples at 4 bytes per sample
        );

        const output: DataView = new DataView(outputData);

        for(let sampleIndex = 0; sampleIndex < this._SampleCount; sampleIndex++) {
            const num = output.getUint32(sampleIndex * this._SampleSize, false);

            this._ChartData.datasets[0].data[sampleIndex].y = num;
            this._ChartData.datasets[0].data[sampleIndex].x = sampleIndex;
        }
    });

    // Update the Chart
    if (this._ChartRef.current != null) {
        this._ChartRef.current.update();
    }
}
Code language: JavaScript (javascript)

This method reads a sequence of samples from a PipeOut endpoint and plots them all using a horizontal axis representing the index of the samples. Note that the operations that depend on the data being retrieved are placed entirely in the function submitted to the WorkQueue.

Add a Method to Periodically Update the Chart Content

Add a public method named ContinuousChartUpdateLoop to the PlotView.

private _IsStopPending: boolean = false;
private _TimeoutId?: number;
private _CurrentOperation?: Promise<void>;

private async ContinuousChartUpdateLoop(): Promise<void> {
    const start: number = performance.now();

    await this.UpdateChartData();

    const elapsed: number = performance.now() - start;

    if (!this._IsStopPending) {
        const delay: number = this._SamplePeriodMilliseconds - elapsed;

        this._TimeoutId = window.setTimeout(
            () => {
                this._CurrentOperation = this.ContinuousChartUpdateLoop();
            },
            delay > 0 ? delay : 0
        );
    }
}Code language: JavaScript (javascript)

This method will cause the operation that retrieves and plots the current data to occur periodically. It attempts to ensure that an update operation is initiated every 15 milliseconds. Recall this._SamplePeriodMilliseconds = 15; in our constructor.

Periodic updates are accomplished using the JavaScript window.setTimeout function. After the operation to update the chart is complete, the next iteration is scheduled so that it will occur after a specified number of milliseconds have passed.

Configure the Chart Update Loop to Start and Stop

Add the React Component lifecycle componentDidMount and componentWillUnmount methods.

public componentDidMount(): void {
    this._CurrentOperation = this.ContinuousChartUpdateLoop();
}

async componentWillUnmount(): Promise<void> {
    this._IsStopPending = true;
    
    if (this._TimeoutId !== undefined) {
        clearTimeout(this._TimeoutId);
        this._TimeoutId = undefined;
    }
    
    await this._CurrentOperation;
    
    this._IsStopPending = false;
}Code language: JavaScript (javascript)

These methods will cause the ContinuousChartUpdateLoop to start automatically when the React Component is mounted and stop it when the component will unmount.

Complete Example

PlotView.tsx

import React, { Component, ReactNode } from "react";

import './PlotView.css';

import {
    Chart as ChartJS,
    LinearScale,
    PointElement,
    LineElement,
    Title,
    Legend,
    ChartOptions
} from "chart.js";

import { Line } from "react-chartjs-2";

import { IFrontPanel, WorkQueue } from "@opalkellytech/frontpanel-chromium-core";

export type Vector2D = {
    x: number;
    y: number;
};

interface PlotViewProps {
    label: string;
}

interface PlotViewState {
    device: IFrontPanel;
}

class PlotView extends Component<PlotViewProps, PlotViewState>{
    private readonly _WorkQueue: WorkQueue = new WorkQueue();

    private _ChartRef: React.RefObject<ChartJS<"line">>;
    private _ChartOptions: ChartOptions<"line">;
    private _ChartData;

    private readonly _SamplePeriodMilliseconds: number = 15;
    private readonly _SampleCount: number = 500;
    private readonly _SampleWindowLength: number = this._SampleCount * this._SamplePeriodMilliseconds;

    private _InitialTimeStamp = 0.0;

    private _IsStopPending: boolean = false;
    private _TimeoutId?: number;
    private _CurrentOperation?: Promise<void>;

    constructor(props: PlotViewProps) {
        super(props);

        ChartJS.register(LinearScale, PointElement, LineElement, Title, Legend);

        this._ChartRef = React.createRef();

        this._ChartOptions = {
            animation: false,
            responsive: true,
            maintainAspectRatio: false,
            scales: {
                x: {
                    type: "linear",            
                    min: 0.0,
                    max: this._SampleWindowLength,                    
                    ticks: { 
                        display: true, 
                        stepSize: 100.0,
                        callback: function(value, index) {
                            // Hide every 2nd tick label
                            return index % 2 === 0 ? (Number(value) / 1000.0).toPrecision(3).toString() : '';
                        },
                    },
                    title: { display: true, text: "Time" },
                },
                y: {
                    type: "linear",
                    min: 0,
                    max: 512,
                    ticks: { display: true, stepSize: 5 },
                    title: { display: true, text: "Value" }
                }
            },
            plugins: {
                legend: {
                    position: "bottom" as const
                },
                title: {
                    display: true,
                    text: props.label
                }
            }
        };

        const channels: Vector2D[][] = Array(2);

        channels[0] = new Array<Vector2D>();
        channels[1] = new Array<Vector2D>();
       
        this._ChartData = {
            datasets: [
                {
                    label: "Channel 1",
                    data: channels[0],
                    pointRadius: 0,
                    borderColor: "rgb(255, 99, 132)",
                    backgroundColor: "rgba(255, 99, 132, 0.5)"
                },
                {
                    label: "Channel 2",
                    data: channels[1],
                    pointRadius: 0,
                    borderColor: "rgb(53, 162, 235)",
                    backgroundColor: "rgba(53, 162, 235, 0.5)"
                }
            ]
        };

        this.state = {
            device: window.FrontPanel
        };
    }

    render(): ReactNode {
        return (
            <div className="PlotViewContainer">
                <Line 
                    ref={this._ChartRef} 
                    options={this._ChartOptions} 
                    data={this._ChartData} />
            </div>
        );
    }

    private async UpdateChartData(): Promise<void> {
        await this._WorkQueue.Post(async () => {
            // Retrieve the latest data from the device
            await this.state.device.updateWireOuts();
    
            const currentTimeStamp = performance.now();

            const data = await this.state.device.getWireOutValue(0x20) & 0xff;

            // Set the initial time stamp when the first data element is received
            if(this._ChartData.datasets[0].data.length === 0) {
                this._InitialTimeStamp = currentTimeStamp;
            }
    
            // Append the new data element
            this._ChartData.datasets[0].data.push({ x: currentTimeStamp - this._InitialTimeStamp, y: data });
    
            // Remove the first data element and adjust the x-axis range when the data extends past the maximum
            if(currentTimeStamp >= (this._ChartData.datasets[0].data[0].x + this._SampleWindowLength)) {
                this._ChartData.datasets[0].data.shift();
    
                if(this._ChartRef.current != null) {
                    this._ChartRef.current.options.scales!.x!.min = this._ChartData.datasets[0].data[0].x;
                    this._ChartRef.current.options.scales!.x!.max = this._ChartData.datasets[0].data[0].x + this._SampleWindowLength;
                }
            }
        });
    
        // Update the Chart
        if (this._ChartRef.current != null) {
            this._ChartRef.current.update();
        }
    }

    private async ContinuousChartUpdateLoop(): Promise<void> {
        const start: number = performance.now();
    
        await this.UpdateChartData();
    
        const elapsed: number = performance.now() - start;
    
        if (!this._IsStopPending) {
            const delay: number = this._SamplePeriodMilliseconds - elapsed;
    
            this._TimeoutId = window.setTimeout(
                () => {
                    this._CurrentOperation = this.ContinuousChartUpdateLoop();
                },
                delay > 0 ? delay : 0
            );
        }
    }
    
    public componentDidMount(): void {
        this._CurrentOperation = this.ContinuousChartUpdateLoop();
    }
    
    async componentWillUnmount(): Promise<void> {
        this._IsStopPending = true;
        
        if (this._TimeoutId !== undefined) {
            clearTimeout(this._TimeoutId);
            this._TimeoutId = undefined;
        }
        
        await this._CurrentOperation;
        
        this._IsStopPending = false;
    }
}

export default PlotView;
Code language: JavaScript (javascript)