ROOT Real-Time plotting with multiple lines in a single plot

In our previous example we showed how to make a high performance realtime-updating plot with a ZMQ based datasource.

This example expands on that by not plotting one but five separate lines in a single plot. It expects to receive a double[5] array via ZMQ, which is distributed to the 5 lines.

Compile using

g++ -o plot main.cpp `root-config --cflags --glibs` -lzmq
g++ -o server server.cpp `root-config --cflags --glibs` -lzmq

then run ./server and ./plot in separate shells.

ROOT five lineplots

Plotting client

#include <iostream>
#include <vector>
#include <array>
#include <zmq.hpp>
#include <TApplication.h>
#include <TGraph.h>
#include <TCanvas.h>
#include <TSystem.h>

using namespace std;

// Maximum number of data points to display
const int MAX_POINTS = 10000;
const int BATCH_SIZE = 10;  // Only update plot every 100 points
const int NUM_SINEWAVES = 5;

int main(int argc, char* argv[]) {
    // Initialize ROOT TApplication for event handling and GUI
    TApplication app("app", &argc, argv);
    
    // Create a canvas to display the graph
    TCanvas *c1 = new TCanvas("c1", "Real-time Plot", 800, 600);
    c1->SetGrid();

    // Create a vector of TGraphs to hold the data
    std::vector<std::unique_ptr<TGraph>> graphs(NUM_SINEWAVES);
    for (int i = 0; i < NUM_SINEWAVES; ++i) {
        graphs[i] = std::make_unique<TGraph>();
        graphs[i]->SetTitle("Real-time Data;Time;Value");
        graphs[i]->SetLineColor(i + 1); // Different color for each line
        if (i == 0) {
            graphs[i]->Draw("AL");
        } else {
            graphs[i]->Draw("L SAME");
        }
    }
    
    // Setup ZeroMQ context and SUB socket
    zmq::context_t context(1);
    zmq::socket_t socket(context, zmq::socket_type::sub);
    
    // Connect to the publisher socket (modify "tcp://localhost:5555" as needed)
    socket.connect("tcp://localhost:5555");
    
    // Subscribe to all messages (empty string filter)
    socket.set(zmq::sockopt::subscribe, "");

    // Variables to store data
    double time = 0.0;
    std::vector<double> times;
    std::vector<std::array<double, NUM_SINEWAVES>> values;

    int batch_counter = 0;

    while (true) {
        // Poll for incoming data with a timeout of 100 ms
        zmq::message_t message;
        if (socket.recv(message, zmq::recv_flags::none)) {
            // Extract double value from the message
            std::array<double, NUM_SINEWAVES> value;
            memcpy(&value, message.data(), sizeof(value));

            // Update the time (or you could use an external clock/timestamp)
            time += 1.0 / 1000.0;  // Assuming a 1kHz sample rate

            // Add the new point to the vectors
            times.push_back(time);
            values.push_back(value);

            // If we exceed the maximum number of points, remove the oldest point
            if (times.size() > MAX_POINTS) {
                times.erase(times.begin());
                values.erase(values.begin());
            }

            // Only update the graph every BATCH_SIZE points
            batch_counter++;
            if (batch_counter >= BATCH_SIZE) {
                batch_counter = 0;

                // Update the TGraph with the new data
                for (size_t i = 0; i < NUM_SINEWAVES; i++)
                {
                    graphs[i]->Set(times.size());
                    for (size_t j = 0; j < times.size(); ++j) {
                        graphs[i]->SetPoint(j, times[j], values[j][i]);
                    }
                }

                // Redraw the graph
                c1->Modified();
                c1->Update();
            }
        }

        // Handle ROOT events (this keeps the GUI responsive)
        gSystem->ProcessEvents();
        
        // Introduce a small delay (optional)
        gSystem->Sleep(1);  // 1 ms sleep for smooth handling
    }

    // Enter the ROOT event loop (never reached in this case, but necessary for completeness)
    app.Run();

    return 0;
}

Data generation server

#include <iostream>
#include <zmq.hpp>
#include <chrono>
#include <thread>
#include <cmath>
#include <array>

// Constants for sine wave generation
const double PI = 3.14159265358979323846;
const double SAMPLE_RATE = 1000.0; // Sample rate in Hz
const double FREQUENCY = 1.0;  // 1Hz sine wave frequency
const int NUM_SINE_WAVES = 100; // Number of sine waves

int main() {
    // Setup ZeroMQ context and PUB socket
    zmq::context_t context(1);
    zmq::socket_t socket(context, zmq::socket_type::pub);
    
    // Bind to a port (e.g., tcp://*:5555)
    socket.bind("tcp://*:5555");

    // Start sine wave generation and publishing
    int sample_count = 0;
    std::array<double, NUM_SINE_WAVES> sine_values;
    
    while (true) {
        // Generate the next sine wave values
        for (int i = 0; i < NUM_SINE_WAVES; ++i) {
            double time = static_cast<double>(sample_count) / SAMPLE_RATE;  // time in seconds
            sine_values[i] = std::sin(2.0 * PI * (FREQUENCY + i) * time);  // Different frequency for each sine wave
        }

        // Convert sine values to a message (sending an array of doubles)
        zmq::message_t message(sine_values.size() * sizeof(double));
        memcpy(message.data(), sine_values.data(), sine_values.size() * sizeof(double));

        // Publish the sine wave values
        socket.send(message, zmq::send_flags::none);

        // Increment the sample count
        sample_count++;

        // Sleep to maintain 1kHz sample rate (1000 samples per second)
        std::this_thread::sleep_for(std::chrono::microseconds(1000));  // 1 millisecond delay
        if(sample_count % 1000 == 0) {
            std::cout << "Sample count: " << sample_count << std::endl;
        }
    }

    return 0;
}