C++ 1kHz+ Real-Time plotting using ROOT

The following example consists of two components:

The server is implemented in server.cpp and the client in main.cpp.

Given an appropriate OpenGL backend, the client will display a real-time plot of the sine wave without noticable delay.

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 real-time plotting window

Plotting client

#include <iostream>
#include <vector>
#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 10 points (100Hz update rate @1kHz)

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 TGraph to hold the data
    auto graph = std::make_unique<TGraph>();
    graph->SetTitle("Real-time Data;Time;Value");
    //graph->SetMarkerStyle(7);  // Small dot marker
    graph->Draw("AL");
    
    // 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<double> 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
            double value;
            memcpy(&value, message.data(), sizeof(double));

            // 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
                graph->Set(times.size());
                for (size_t i = 0; i < times.size(); ++i) {
                    graph->SetPoint(i, times[i], values[i]);
                }
                graph->Draw("AL");

                // 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>

// Constants for sine wave generation
const double PI = 3.14159265358979323846;
const int SAMPLE_RATE = 1000;  // 1kHz sampling rate
const double FREQUENCY = 1.0;  // 1Hz sine wave frequency

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;
    
    while (true) {
        // Generate the next sine wave value
        double time = static_cast<double>(sample_count) / SAMPLE_RATE;  // time in seconds
        double sine_value = std::sin(2.0 * PI * FREQUENCY * time);      // 1Hz sine wave

        // Convert sine value to a message (sending a double)
        zmq::message_t message(sizeof(double));
        memcpy(message.data(), &sine_value, sizeof(double));

        // Publish the sine wave value
        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;
}