Files
plotter/docs/getting-started.md
T

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.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 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, 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).

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.