Martino Ferrari 9aff06acee fix(glfw): route a drag by the row it began in, and drop stale hover state
`controller_at()` routed every pointer event by the row the pixel was in,
ignoring the row a drag started in, so a press on one figure row followed by a
motion and release in another delivered the motion and release to a controller
whose `mode_ == None`. The owning controller kept `mode_`, `drag_region_`,
`drag_cursor_` and `drag_fixed_x_` set for ever: `publish_hot()` kept publishing
the grabbed handle, `grabbed_region()` reported a drag that had ended, and a
later gesture re-entering the row resumed the stale edit from the old anchor.

`feed_pointer_event()` now owns the routing: a press captures the row it landed
in and every motion and the release until the button comes up go to that row
(`pointer_target()`), cleared on release and when another figure is set. The
GLFW callbacks build the event and call it, so a windowless host or a test can
drive the routing too.

Two neighbours in the same seam:

* `sync_figure_crosshair()` called the four-argument overload unconditionally,
  so a documented vertical-only crosshair (`horizontal == false`) was promoted
  to a full cross at the stored y == 0, clamped to the axes edge and made
  permanent; the change detection also keyed on x alone, missing a y-only move
  to another row at the same x (two rows showing one horizontal line, spec 17).
  It now consults `crosshair().horizontal` and compares the hovered row's y.
* Only the row under the pointer republished its hot handles, so a row the
  pointer left kept drawing a hovered marker or bound as if it were still
  under the pointer, and answered `hovered_region()`/`grabbed_cursor()` for
  it. The run loop clears every other row's handles, keeping the captured
  row while a drag owns the button, and reads the frame's one layout once
  instead of recomputing every row five times.

Test: a press on row A's region, a motion into row B, a release in row B
asserts no controller keeps a drag mode, nothing reports a grabbed handle, and
a later press-and-motion that re-enters row A does not resume the old edit.
2026-09-28 22:36:30 +02:00

plot

An embeddable OpenGL time-series plotting library for real-time, high-sample-rate data. The core (plot::core, C++20, OpenGL 3.3 core) has no third-party dependency at all; the host integrations that own a window and an event loop are separate, opt-in targets built on top of it.

Hosts, and what a frame costs

host CMake target source
GLFW plot::glfw adapters/glfw/
Dear ImGui plot::imgui (build-tree only) adapters/imgui/
Qt6 plot::qt6 adapters/qt6/
GTK4 plot::gtk4 adapters/gtk4/

Per-frame rendering cost is O(visible pixels + samples produced since the last frame), not O(samples): a trace denser than one sample per pixel is drawn from a min/max LOD pyramid rather than from raw samples. The plot_multiplot_demo run stages 18,651 GPU segments to draw ~4.5 million visible samples across six traces — the frame is bounded by the pixels it lights, not by the sample count behind them. That figure is from a linux-release build under xvfb-run on llvmpipe (Mesa's software rasteriser); no run touched a GPU, so read it as an upper bound and a record of the software path.

Features

  • grid with independent major/minor line classes (Style::grid_major_visible, grid_minor_visible);
  • x and y axes, with a vertical (bottom-to-top) y-axis title;
  • cursors in both orientations — vertical markers and horizontal rulers;
  • draggable regions and measurement bands with exact interval statistics;
  • a signal legend anchored to an axes corner;
  • a hover crosshair with a per-signal, interpolated readout;
  • a multi-plot Figure of stacked rows sharing one synchronised x axis;
  • live follow (XFollow::Live) with relative (multiplicative) zoom and mouse panning;
  • DPI-consistent sizing: metrics are logical units converted once through Style::ui_scale;
  • an anti-aliased embedded monospaced font, plus an optional runtime TrueType backend (plot::font_ttf).

Build and install

Needs a C++20 compiler and CMake 3.24+. Presets live in CMakePresets.json (linux-debug, linux-release, linux-asan, linux-coverage).

cmake --preset linux-release
cmake --build build/release
cmake --install build/release --prefix /where/you/want

Install is on by default (PLOT_INSTALL=ON) and installs the plot_core archive, the public headers under include/plot/, and a CMake package config under lib/cmake/plot/. A consumer then needs only:

find_package(plot 0.1 REQUIRED)
target_link_libraries(my_app PRIVATE plot::core)

Embedding

A host owns the window, the GL context and the event loop; plot owns none of them. This is the whole of a minimal GLFW program — the adapter opens the window, runs the loop and feeds the controller, and the callback is where live data would be pushed:

#include <glfw/glfw_host.hpp>   // the optional GLFW adapter
#include <plot/plot.hpp>

#include <cmath>
#include <vector>

int main() {
  plot::glfw::Host host;
  plot::Plot& p = host.plot();

  std::vector<plot::f32> y(512);
  for (plot::usize i = 0; i < y.size(); ++i)
    y[i] = static_cast<plot::f32>(std::sin(0.05 * static_cast<double>(i)));
  plot::data::ArraySeries series(std::move(y), 0.0, 1.0);

  const plot::u32 x = p.add_x_axis({"x", {0.0, 511.0}, true});
  const plot::u32 sig = p.add_y_axis({"signal", {-1.0, 1.0}, true, plot::AxisPlace::Left});
  p.add_trace({&series, plot::Color::hex(0x4FA3FF), 1.5f, x, sig, "sine"});

  plot::Status st;
  return host.run({800, 480, "plot"}, [](plot::Plot&, plot::f64) {}, &st) ? 0 : 1;
}

It compiles and links against a built tree (see docs/getting-started.md for the same program written against plot::core directly, with no adapter):

g++ -std=c++20 -I include -I adapters readme_main.cpp \
    build/debug/adapters/glfw/libplot_glfw.a build/debug/libplot_core.a \
    $(pkg-config --libs glfw3) -ldl -lpthread -o plot_readme

Documentation

The user guide lives under docs/; docs/superpowers/ is the design record, not user documentation.

  • Getting started — prerequisites, build/install/consume, and a minimal GLFW program against plot::core.
  • Architecture — the layers (core/math → gl → data → render → plot → input), what each owns, and the dependency rules.
  • Embedding into a host — the four adapters, the ui_scale/DPI rule per host, and the headless demo invocations.
  • Data and performance — the sample stores, the single-producer/single-consumer contract, the LOD pyramid, and the measured frame times.
  • Interaction — cursors, regions, the measurement band, the legend, the crosshair, the gesture table, and live follow.
  • Styling — every Style field with its default and unit, the grid classes, and installing a text backend.
  • Building and testing — presets, PLOT_BUILD_* options, the suite, the headless demo modes, and the coverage command.
  • Coverage — the coverage method and the measured per-file table.

Testing and coverage

cmake --preset linux-debug
cmake --build build/debug
ctest --test-dir build/debug --output-on-failure   # 283 cases, 0 failed

tools/coverage.sh                                  # configure+build+run+capture+floor check

The linux-debug suite runs 283 cases; the linux-coverage preset builds 272 of them, because its adapters are off and 11 cases need an adapter target (8 in test_imgui_widget.cpp, 3 in test_glfw_host.cpp). Coverage is enforced by tools/coverage.sh against tools/coverage_floor.py: 80% per file under src/ and 90% aggregate. The current measurement is an aggregate 95.3% (3284/3447 lines) over 23 files, every file at or above 80%; the per-file table is in docs/coverage.md, which the command regenerates.

Licence and third-party provenance

  • This library is MIT licensed: the full text is in LICENSE, and every first-party file (C++, Python and shell) carries its SPDX identifier in the file header — // SPDX-License-Identifier: MIT — so the grant travels with each source file, not just the repository root.
  • Adwaita Mono (the embedded font, render::EmbeddedMonoFont) is under SIL OFL 1.1, committed verbatim at licences/OFL-1.1.txt. The backend class is named neutrally (EmbeddedMonoFont, not "Adwaita Mono") because OFL reserves the font name for modified redistributions. The baked atlas is generated at authoring time by tools/fonts/gen_atlas.py and committed as data, so plot_core rasterises nothing at build or run time.
  • stb_truetype v1.26 (public domain, Sean Barrett / RAD Game Tools) is vendored at third_party/stb_truetype.h and compiled only by the optional plot_font_ttf target; its include directory is PRIVATE, so no consumer and no other target can reach it. PLOT_BUILD_TTF_FONT defaults on exactly when the vendored header is present.
  • plot_core is third-party-free. It links only ${CMAKE_DL_LIBS}, and no public header under include/plot/ includes a third-party header. The header self-containment and umbrella checks in tests/CMakeLists.txt fail the build if either rule breaks.

Limitations

  • Timings are llvmpipe under Xvfb (tools/coverage.sh, the demo summaries). No measurement here used a GPU; there are no hardware numbers.
  • macOS and Windows are untested. Development and CI are Linux/EGL + X11 only.
  • Fractional-scale compositing and touchpad scroll calibration are unverified — the ui_scale sources are correct per host, but no run exercised a real fractional DPI or a calibrated scroll device.
  • Untrusted font files are out of scope for the runtime plot::font_ttf backend: stb_truetype does no range checking, so from_file() must only be handed files the host trusts.
  • render ↔ plot is not a strict include chain. Renderer includes plot/plot.hpp because drawing is being handed a Plot; the plot layer names only render's value types (Style, TextBackend), never Renderer. The cycle and its direction are documented in docs/architecture.md.
S
Description
No description provided
Readme MIT
2.3 MiB
0 Stars 1 Watchers 0 Forks
Languages
C++ 95.7%
CMake 2.4%
Python 1.7%
Shell 0.2%