Skip to main content

No project description provided

Project description

Logo of the project

rtplot — real-time plotting over ZMQ

rtplot lets a Python script push live data to a plot window — locally, or across the network — with a few lines of code on the sender side. The plot window can be a traditional Qt application or a modern browser UI, and it also supports interactive controls (buttons, sliders, dials, text and numeric displays) that feed values back into the sending script in real time.

Typical use: a robot or data-acquisition script runs on a Raspberry Pi or microcontroller host, and you watch live signals and tweak gains from a laptop on the same network.


Table of contents


Highlights

  • Fast. 500+ fps on a single trace on a modern laptop. Binary WebSocket deltas on the browser server; raw Qt rendering on the desktop server.
  • Two frontends. A new browser-based server (aiohttp + uPlot) and the original pyqtgraph desktop server. Both speak the same ZMQ protocol, so client code is identical.
  • Remote-friendly. Either the sender or the plot host can bind — pick whichever fits your network. Works across LAN, WSL, and SSH tunnels.
  • Plot config lives with the data. The sender declares the plot layout, so a Pi running your experiment owns the look of its own dashboards.
  • Interactive controls (browser server only). Declare buttons, sliders, dials, numeric/text displays in the same initialize_plots call. Poll from your tight loop; no threads, no callbacks.
  • Save to Parquet with a single button click or client.save_plot() call.

Install

Minimum install — just the client (send data only):

pip install better-rtplot

Add the browser server (recommended):

pip install "better-rtplot[browser]"

Add the Qt/pyqtgraph server instead:

pip install "better-rtplot[server]"

The browser extra pulls aiohttp + pandas + pyarrow; the server extra pulls pyqtgraph + PySide6 + pandas + pyarrow. If you only pip install better-rtplot and try to launch a server, rtplot will print a friendly message telling you which extra to add.

WSL users: the browser server works out of the box — open the URL it prints in your Windows browser. The Qt server needs an X server such as VcXsrv.


60-second quickstart

Terminal 1 — start a plot window:

python -m rtplot.server_browser        # browser UI at http://localhost:8050
# or
python -m rtplot.server                 # desktop Qt window

Terminal 2 — send data:

from rtplot import client
import numpy as np, time

client.local_plot()                     # send to the server on 127.0.0.1
client.initialize_plots(["sin", "cos"]) # one plot with two named traces

for i in range(10000):
    t = i * 0.01
    client.send_array([np.sin(t), np.cos(t)])
    time.sleep(0.01)

That's it. Open http://localhost:8050 if you used the browser server; the Qt server will pop up its own window.


Choosing a server: browser vs. Qt

Browser server (rtplot.server_browser) Qt server (rtplot.server)
Frontend aiohttp + uPlot in any modern browser pyqtgraph + PySide6 desktop window
Extra [browser] [server]
Works over SSH Yes (just forward the HTTP port) No (needs X forwarding)
Interactive controls Yes — buttons, sliders, dials, displays No
Typical frame rate 60 Hz render, 1000 Hz data push cap 500+ fps
Saves to Parquet Yes Yes

If you're on WSL, running remotely, or you want interactive controls, use the browser server. The Qt server is still available for local desktop use and for legacy setups.


Interactive controls

Browser server only. Declare a control row inline in your plot layout:

from rtplot import client
import numpy as np, time

client.local_plot()
client.initialize_plots([
    {"names": ["signal"], "yrange": [-6, 6]},
    {"controls": [
        {"type": "button", "id": "reset", "label": "Reset"},
        {"type": "button", "id": "pause", "label": "Pause"},
        {"type": "slider", "id": "gain",  "label": "Gain",
         "min": 0, "max": 5, "value": 1.0, "step": 0.1, "format": "{:.2f}"},
    ]},
    {"controls": [
        {"type": "dial",    "id": "freq", "label": "Freq (Hz)",
         "min": 0.1, "max": 5.0, "value": 1.0, "step": 0.05,
         "sensitivity": 0.5, "format": "{:.2f}"},
        {"type": "display", "id": "t",    "label": "t (s)", "format": "{:.2f}"},
        {"type": "text",    "id": "msg",  "label": "Status",
         "value": "running"},
    ]},
])

running = True
t0 = time.time()
while True:
    ctrl = client.poll_controls()
    for btn in ctrl.buttons:
        if btn == "reset": t0 = time.time()
        if btn == "pause": running = not running

    gain = ctrl.values.get("gain", 1.0)
    freq = ctrl.values.get("freq", 1.0)
    t = time.time() - t0
    amp = gain * np.sin(2 * np.pi * freq * t) if running else 0.0

    client.set_display("t", t)
    client.set_display("msg", "paused" if not running else "running")
    client.send_array(amp)
    time.sleep(0.01)

Reading controls from Python

ctrl = client.poll_controls()           # non-blocking, cheap to call every loop
gain = ctrl.values.get("gain", 1.0)     # latest slider/dial value
for btn_id in ctrl.buttons:             # list of buttons fired since last poll
    handle(btn_id)

poll_controls() returns a ControlState(values, buttons) namedtuple:

  • values — a dict of {element_id: float} for every slider and dial the server has told the client about. Defaults declared in initialize_plots are pre-seeded so the first call already sees them.
  • buttons — a list of button ids fired since the previous poll, in order. The list is cleared on return, so each event is delivered exactly once.

Call it from your tight loop before computing the next sample. No threads, no callbacks, no missed events.

Pushing values into displays

client.set_display("t", 12.34)       # numeric display box
client.set_display("msg", "running") # text field

set_display() accepts either a number (for type: "display" elements) or a string (for type: "text" elements). Updates are coalesced on the server and rebroadcast to every connected browser at ~30 Hz.

Element reference

Type Purpose Notable fields
button Fires a discrete event when clicked id, label
slider Scalar input via horizontal range id, label, min, max, value, step, format
dial Scalar input via rotational drag same as slider, plus sensitivity (full turns per range sweep; default 1.0)
display Read-only numeric readout id, label, format
text Read-only text field (prompts, status) id, label, value

Slider and dial widgets both render as [widget] [−] [number input] [+], so you can drag, type a value directly, or nudge by step. The dial accepts "round and round" circular drag — each full rotation walks the value through (max − min) × sensitivity, so sensitivity: 0.25 gives you four rotations per sweep for fine control.

The format field accepts Python-style {:.Nf} strings (e.g. "{:.2f}").


Plot configuration

Each entry in initialize_plots is one of:

  • an integerclient.initialize_plots(3) → one plot with 3 anonymous traces
  • a stringclient.initialize_plots("torque") → one plot with one named trace
  • a list of strings — one plot, one trace per name
  • a list of lists of strings — one plot per sublist
  • a dict — one plot, with full styling options (below)
  • a list of dicts — multiple plots with full styling

A styled plot dict accepts any of:

Key Meaning
names Required. List of trace names.
colors List of per-trace colors. Single letter (r g b c m y k w) or any CSS color string.
line_style "-" for dashed, "" (or anything else) for solid, per trace.
line_width Per-trace line width in pixels.
title Plot title.
xlabel / ylabel Axis labels.
yrange [ymin, ymax] — pins the Y axis and significantly speeds up rendering.
xrange Integer number of samples visible at once (default 200).

Special row entries (not plots themselves):

  • {"controls": [...]} — a row of interactive controls (browser server only)
  • {"non_plot_labels": ["name1", "name2"]} — extra scalar names that ride along with send_array and get saved into the output Parquet file, but aren't rendered as traces

Sending data

client.send_array(scalar)           # float
client.send_array([a, b, c])        # 1-D list: one sample per trace
client.send_array(np.array([...]))  # 1-D numpy array: one sample per trace
client.send_array(np.array([[...]]))# 2-D (num_traces, N): N samples at once

Passing a 2-D array with N > 1 lets you push a batch of samples per send_array call, which is the fastest way to get many samples through without dropping frames.


Saving data

The server saves every sample it has received since the latest initialize_plots call to a Parquet file, including any non_plot_labels data that rode along with your normal data.

Trigger a save from either side:

  • Browser UI: click the Save Plot button.
  • Python: client.save_plot("my_run")

Control where things get written:

python -m rtplot.server_browser -sd ./saved_plots -sn experiment1
  • -sd / --save-dir — target directory
  • -sn / --save-name — filename prefix (a timestamp is always appended)

Save non-plot signals alongside the plotted ones

client.initialize_plots([
    {"names": ["hip_angle", "knee_angle"]},
    {"non_plot_labels": ["battery", "cpu_temp", "loop_latency"]},
])

Send battery, cpu_temp and loop_latency as extra rows after the plotted traces in each send_array call; they won't be drawn but they will land in the Parquet file.


Networking modes

rtplot uses ZMQ, so either the sender or the plot host can be the one that binds a socket. Pick whichever works for your network and firewalls.

Mode A — plot host binds, sender connects (typical for lab laptops)

# on the plot host (e.g. your laptop)
python -m rtplot.server_browser
# on the sender (e.g. the Pi)
from rtplot import client
client.configure_ip("192.168.1.42")   # the laptop's LAN IP

Mode B — sender binds, plot host connects (typical when the sender has a static IP and the viewer roams around)

# on the plot host
python -m rtplot.server_browser -p 192.168.1.50   # the sender's IP
# on the sender
from rtplot import client
# no configure_ip call needed — the default behavior binds

If you pass -p host:port to the server, rtplot also derives the control return-channel endpoint from that same host/port (it uses port+1). This means sliders, buttons, and dials work transparently in both modes with no extra config.


Performance tuning

If you start running out of frames, try these, in roughly this order:

  1. Pin the Y range. {"yrange": [-2, 2]} on each plot lets the renderer skip autoscaling work and gives the single biggest win.
  2. Batch your samples. Pass a 2-D numpy array to send_array so N samples ship per call.
  3. Shrink the window. Fewer pixels to redraw per frame.
  4. Reduce line_width. Thicker lines cost more to rasterize.
  5. Use the -s N / --skip N server flag to push every Nth sample batch to the browser instead of every one. Add -a / --adaptable to let the server tune N to your data rate automatically.
  6. Increase xrange. Counterintuitively, a longer visible history can be cheaper than a short one because the browser ring-buffers the data and only replaces the tail on each push.

CLI reference

Browser server (python -m rtplot.server_browser):

Flag Default Meaning
-p HOST[:PORT] (bind) Connect to a sender at this address instead of binding
--host HOST 0.0.0.0 HTTP bind interface
--port N 8050 HTTP port
--no-browser off Don't try to open a browser on startup
--rate N 1000 Max WebSocket push rate (Hz)
-n N / --skip N 1 Push every Nth sample batch
-a / --adaptable off Auto-tune skip rate to data rate
-c / --column row Lay plots out in columns instead of rows
-d / --debug off Extra debug logging
-sd DIR / --save-dir DIR cwd Where to write .parquet saves
-sn NAME / --save-name NAME Prefix for saved filenames

Qt server (python -m rtplot.server): same -p, -n, -a, -c, -d, -sd, -sn flags as above, plus:

Flag Meaning
-b / --bigscreen Pre-configure for the neurobionics lab big-screen display
-t FILE / --plot_config FILE Load a plot configuration from a file on startup

Examples

  • rtplot/example_code.py — a walk through every initialize_plots signature, plus a controls demo at the bottom.

  • rtplot/interactive_test.py — a guided end-to-end test that walks you through clicking buttons, dragging sliders, typing into the number input, using the ± nudge arrows, and spinning the dial. Good for smoke-testing a fresh install.

    python -m rtplot.server_browser &
    python -m rtplot.interactive_test
    

Qt server example 1 Qt server example 2

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

better_rtplot-0.2.1.tar.gz (77.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

better_rtplot-0.2.1-py3-none-any.whl (76.0 kB view details)

Uploaded Python 3

File details

Details for the file better_rtplot-0.2.1.tar.gz.

File metadata

  • Download URL: better_rtplot-0.2.1.tar.gz
  • Upload date:
  • Size: 77.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.2 CPython/3.12.3 Linux/6.6.87.2-microsoft-standard-WSL2

File hashes

Hashes for better_rtplot-0.2.1.tar.gz
Algorithm Hash digest
SHA256 285f5b258e0a587ba4c1423b24388dae21802a9dc577da6ba3b95f7c9eeb8d10
MD5 b0b1ceaaf4ab9291e9584a17faf7158f
BLAKE2b-256 2e170af15d738e3f425271ef1f6c2aae33b20bc28d4fd27d5892473e1f683b5a

See more details on using hashes here.

File details

Details for the file better_rtplot-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: better_rtplot-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 76.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.2 CPython/3.12.3 Linux/6.6.87.2-microsoft-standard-WSL2

File hashes

Hashes for better_rtplot-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8c1d64fb04f45a6b82438944c007dab94a838b00a5ab201e50911c63a70d054d
MD5 0f9fd0bbba155631093b6ffd5e677867
BLAKE2b-256 69f41d11c45f58f1a89653f228fc2e357c26bada025bbe9ab3653f3eea82528e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page