# 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](embedding.md)). ## Prerequisites - A C++20 compiler. - CMake **3.24 or newer**. `cmake/plot_options.cmake` fails the configure with a `FATAL_ERROR` below that, and the presets in `CMakePresets.json` declare `cmakeMinimumRequired` 3.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 through `plot::gl::caps()` and only used when the driver offers it. - To build the **test suite** you also need EGL: `tests/CMakeLists.txt` fails 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](building-and-testing.md) for the options. ## Build and install The presets live in `CMakePresets.json`: ```sh 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 ```cmake 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: ```cpp #include // core, math, gl, data, render, plot, input — in layer order ``` Adapters are deliberately **not** in the umbrella: their headers are reached as ``, ``, `` and ``, 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: ```sh g++ -std=c++20 -I include main.cpp build/debug/libplot_core.a \ $(pkg-config --libs glfw3) -ldl -lpthread -o plot_quickstart ``` ```cpp // A minimal plot: one window, one axis pair, one trace. #include // the whole public surface, in layer order #include #define GLFW_INCLUDE_NONE // keep GLFW from including and its macros #include #include #include #include #include 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 y(512); for (plot::usize i = 0; i < y.size(); ++i) { y[i] = static_cast(std::sin(0.05 * static_cast(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(fb_w), static_cast(fb_h)}; controller.set_viewport(viewport); renderer.style().ui_scale = 1.0f; // device pixels per logical unit renderer.begin_frame(static_cast(fb_w), static_cast(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, and `Renderer::init()` returns `false` with a `Status` when 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 `Plot` is pure CPU state. When you draw it through a `Renderer`, set colours and metrics on `renderer.style()`, not on the plot — `draw()` copies the style into the plot and hands it the `ui_scale` before laying out. - `ui_scale` is device pixels per logical unit. Here it is `1.0` because the program uses the framebuffer 1:1; a HiDPI host passes the framebuffer/window ratio instead (see [embedding.md](embedding.md)). Next: [architecture.md](architecture.md) for how the pieces fit, [embedding.md](embedding.md) to put a plot into a real host, and [data-and-performance.md](data-and-performance.md) for feeding it a live stream.