7.1 KiB
Getting started
plot is a C++20 library for real-time, high-sample-rate time-series plots drawn with
OpenGL 3.3 core. The core library — the CMake target plot::core — has no third-party
dependency at all; the host adapters for GLFW, Dear ImGui, Qt6 and GTK4 are separate,
opt-in targets on top of it (see embedding.md).
Prerequisites
- A C++20 compiler.
- CMake 3.24 or newer.
cmake/plot_options.cmakefails the configure with aFATAL_ERRORbelow that, and the presets inCMakePresets.jsondeclarecmakeMinimumRequired3.24. - A driver exposing an OpenGL 3.3 core profile.
plot::gl::load()resolves only the 3.3 entry points the library actually calls; anything newer (persistent buffer mapping, sync objects) is optional, probed throughplot::gl::caps()and only used when the driver offers it. - To build the test suite you also need EGL:
tests/CMakeLists.txtfails the configure without it, because the GL tests create a real headless EGL context. The library itself does not need EGL. - Each adapter needs its own toolkit (GLFW, Dear ImGui downloaded at configure time, Qt6 or GTK4) — see building-and-testing.md for the options.
Build and install
The presets live in CMakePresets.json:
cmake --preset linux-release # or linux-debug, linux-asan, linux-coverage
cmake --build build/release
cmake --install build/release --prefix /where/you/want
Install is on by default (PLOT_INSTALL, ON). It installs the plot_core archive, the
public headers under include/plot/, and a CMake package config under
lib/cmake/plot/. Adapters join the install/export set only when their toolkit can be
re-found on a consumer's machine; the configure log records which ones were exported and
why.
Consuming it
find_package(plot 0.1 REQUIRED)
target_link_libraries(my_app PRIVATE plot::core)
plot::core is the alias of the plot_core target (EXPORT_NAME core, so the installed
name matches the build-tree name). It carries the C++20 requirement and the include
directory as its public interface and nothing else. The exported adapters are named
plot::glfw, plot::qt6, plot::gtk4 and (when built) plot::font_ttf;
plot::imgui is a build-tree target only, because ImGui is supplied by FetchContent and
there is no ImGui package for an installed config file to point at.
Every public component is reachable through the umbrella header:
#include <plot/plot.hpp> // core, math, gl, data, render, plot, input — in layer order
Adapters are deliberately not in the umbrella: their headers are reached as
<glfw/glfw_host.hpp>, <imgui/imgui_plot.hpp>, <qt6/qt_plot_widget.hpp> and
<gtk4/gtk_plot_area.hpp>, so including the umbrella never drags a toolkit in.
A minimal GLFW program
The program below opens a window, creates a 3.3 core context, loads the GL entry points,
and draws one trace. It uses the GLFW toolkit for the window and plot::core directly —
it does not use the shipped plot::glfw::Host adapter, so it also shows the pieces the
adapter hides (window, context, loader, renderer, controller, frame loop).
It was compiled and linked as part of writing this page:
g++ -std=c++20 -I include main.cpp build/debug/libplot_core.a \
$(pkg-config --libs glfw3) -ldl -lpthread -o plot_quickstart
// A minimal plot: one window, one axis pair, one trace.
#include <plot/plot.hpp> // the whole public surface, in layer order
#include <plot/gl/loader.hpp>
#define GLFW_INCLUDE_NONE // keep GLFW from including <GL/gl.h> and its macros
#include <GLFW/glfw3.h>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
namespace {
// GLFW hands back `void (*)(void)`; plot::gl::ProcLoader wants `void*`.
void* resolve_gl(const char* name) {
GLFWglproc p = glfwGetProcAddress(name);
void* out = nullptr;
static_assert(sizeof(out) == sizeof(p));
std::memcpy(&out, &p, sizeof(out));
return out;
}
} // namespace
int main() {
if (glfwInit() != GLFW_TRUE) return 1;
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
GLFWwindow* window = glfwCreateWindow(800, 480, "plot quick-start", nullptr, nullptr);
if (window == nullptr) { glfwTerminate(); return 1; }
glfwMakeContextCurrent(window);
glfwSwapInterval(1);
plot::Status st;
if (!plot::gl::load(&resolve_gl)) { std::fprintf(stderr, "gl::load failed\n"); return 1; }
plot::Renderer renderer;
if (!renderer.init(&st)) { std::fprintf(stderr, "init failed: %s\n", st.message); return 1; }
// One trace: 512 samples of a sine, x = sample index.
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);
plot::Plot plot;
const plot::u32 x_axis = plot.add_x_axis({"x", {0.0, 511.0}, true});
const plot::u32 y_axis =
plot.add_y_axis({"signal", {-1.0, 1.0}, true, plot::AxisPlace::Left});
plot.add_trace({&series, plot::Color::hex(0x4FA3FF), 1.5f, x_axis, y_axis, "sine"});
plot::input::Controller controller(plot);
while (glfwWindowShouldClose(window) == GLFW_FALSE) {
glfwPollEvents();
int fb_w = 0, fb_h = 0;
glfwGetFramebufferSize(window, &fb_w, &fb_h);
if (fb_w == 0 || fb_h == 0) continue;
const plot::Rect viewport{0.0f, 0.0f, static_cast<plot::f32>(fb_w),
static_cast<plot::f32>(fb_h)};
controller.set_viewport(viewport);
renderer.style().ui_scale = 1.0f; // device pixels per logical unit
renderer.begin_frame(static_cast<plot::u32>(fb_w), static_cast<plot::u32>(fb_h), viewport);
renderer.draw(plot);
renderer.end_frame();
glfwSwapBuffers(window);
}
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
Things worth noticing, because they are the embedding contract in miniature:
plot::gl::load()must run on the thread that owns the current context, andRenderer::init()returnsfalsewith aStatuswhen the load failed or the driver refused a shader/buffer. The renderer never creates, binds or resizes a framebuffer.begin_frame(fb_w, fb_h, viewport_px)takes the framebuffer size (for the projection) and the top-left-origin rectangle the plot occupies;draw()fills exactly that region;end_frame()restores the GL state it changed.- The
Plotis pure CPU state. When you draw it through aRenderer, set colours and metrics onrenderer.style(), not on the plot —draw()copies the style into the plot and hands it theui_scalebefore laying out. ui_scaleis device pixels per logical unit. Here it is1.0because the program uses the framebuffer 1:1; a HiDPI host passes the framebuffer/window ratio instead (see embedding.md).
Next: architecture.md for how the pieces fit, embedding.md to put a plot into a real host, and data-and-performance.md for feeding it a live stream.