plot β€” A Minimal C++ Plotting Library

plot β€” A Minimal C++ Plotting Library

🌐 Auf Deutsch lesen
πŸ“… 2026-06-28 ✏️ 2026-10-18 ✍️ Andreas Wittmann πŸ‘οΈ ... c++ sdl2 graphics

plot is a small C++ library that wraps SDL2 into a minimal drawing API. It manages the window, renderer, and event loop β€” you only implement three callbacks.

Download

File Description
plot.hpp Header β€” include this in your program
plot.cpp Implementation β€” compile alongside your code
vera_ttf.c Embedded font (included automatically by plot.cpp)
demo.cpp Example: animated sine wave with keyboard control
plot_server.cpp Socket server exposing plot over a text protocol (see below)
Makefile Build file for the demo and the socket server

Dependencies

SDL2, SDL2_ttf, SDL2_image. On Debian/Ubuntu:

apt install libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev

Building

Compile plot.cpp together with your source file:

g++ -std=c++11 -o demo demo.cpp plot.cpp $(sdl2-config --cflags --libs) -lSDL2_ttf -lSDL2_image

Or use the included Makefile:

make

To build a different source file:

make SRC=myprogram.cpp OUT=myprogram

Usage

Include plot.hpp and implement the callbacks. Only frame() is required β€” all others are optional.

#include "plot.hpp"

void setup() {
    PlotCanvas(640, 480, PLOT_MODE_CROSS);
}

void frame() {
    PlotBackground(255, 255, 255);
    PlotColor(200, 0, 0);
    PlotLine(-100, 0, 100, 0);   // horizontal line through origin
    PlotLine(0, -100, 0, 100);   // vertical line through origin
    PlotText(5, 5, "Frame: %d", g_current_frame);
}

bool event(SDL_Event* e) {
    return false; // return true if event was handled
}

Canvas Modes

PlotCanvas(width, height, mode) sets up the drawing area. The mode controls where the origin is and which direction y increases:

Mode constant Origin Y direction
PLOT_MODE_DEFAULT top-left down
PLOT_MODE_CROSS center up
PLOT_MODE_MATH bottom-left up

API Reference

// Setup
void PlotCanvas(int width, int height, int mode = PLOT_MODE_DEFAULT);
void PlotMode(int mode);

// Drawing
void PlotBackground(Uint8 r, Uint8 g, Uint8 b);
void PlotColor(Uint8 r, Uint8 g, Uint8 b);
void PlotColor(Uint8 r, Uint8 g, Uint8 b, Uint8 a);
void PlotCross();                                        // draws x/y axes
void PlotPixel(double x, double y);
void PlotLine(double x1, double y1, double x2, double y2);
void PlotTriangle(PlotPoint a, PlotPoint b, PlotPoint c);
void PlotRectangle(double x1, double y1, double x2, double y2);
void PlotFilledRectangle(double x1, double y1, double x2, double y2);
void PlotText(double x, double y, const char* format, ...);  // printf-style

// Timing
void PlotSetFPS(int fps);
void PlotSetFrameDurationMS(Uint32 ms);
void PlotSetFrameLimit(int frames);
uint32_t PlotGetTicks();

// Global state (read-only)
extern int plot_width, plot_height;
extern uint64_t g_current_frame;

All draw functions also accept PlotPoint structs (double x, y) as an alternative to separate coordinates.

Driving plot from outside C++

plot.hpp's API is a direct function call interface: setup(), frame(), and event() run inside the same process, driven by plot's own main loop. That works well for a C++ program, but it means any other language that wants to draw with plot would have to embed and call into that C++ code directly.

plot_server.cpp takes a different approach: it links against the same plot.cpp/plot.hpp (with plot's own main renamed out of the way) and wraps the canvas in a long-running process that listens on a Unix domain socket instead of calling back into frame(). Any process that can open a socket and write lines of text can drive the canvas β€” no C++, no linking, no shared memory.

Build it with the dedicated Makefile target:

make plot-server

Run it, pointing at a socket path and a canvas size:

./plot-server /tmp/plot.sock 800 600

It prints READY <path> once the socket is accepting connections. The window is resizable, so the size given on the command line is only the initial size β€” see SIZE below for reading the current one back.

Protocol

Each line sent to the socket is one command; the server replies with a single OK or ERR line per command, except SIZE, whose reply carries the current window dimensions.

Command Arguments Effect
CLEAR r g b PlotBackground(r, g, b)
COLOR r g b PlotColor(r, g, b)
COLORA r g b a PlotColor(r, g, b, a) β€” with alpha
PIXEL x y PlotPixel(x, y)
LINE x1 y1 x2 y2 PlotLine(x1, y1, x2, y2)
RECT x1 y1 x2 y2 PlotRectangle(x1, y1, x2, y2)
FILLRECT x1 y1 x2 y2 PlotFilledRectangle(x1, y1, x2, y2)
TRIANGLE x1 y1 x2 y2 x3 y3 PlotTriangle(...)
CROSS β€” PlotCross() β€” draws x/y axes
TEXT x y hex PlotText(x, y, "%s", text) β€” hex is the UTF-8 text, hex-encoded (- for an empty string)
MODE 0, 1, or 2 PlotMode(mode)
SIZE β€” Replies OK <width> <height> instead of plain OK
PRESENT β€” Flushes the frame to the window
AUTOPRESENT 0 or 1 Automatically flush after every draw command
QUIT β€” Shuts the server down

TEXT's text argument is hex-encoded rather than sent as plain words because the protocol is line-based β€” a literal space would be read as another argument, and a literal newline would end the command early. Encoding the UTF-8 bytes as hex keeps arbitrary text, including spaces, on a single line.

Nothing is drawn to the window until PRESENT is sent β€” draw commands only update an internal renderer state, the same way they would inside a single frame() call. A minimal session from the shell, using socat to talk to the socket directly:

printf 'CLEAR 255 255 255\nCOLOR 0 0 0\nPIXEL 10 10\nPRESENT\nQUIT\n' \
    | socat - UNIX-CONNECT:/tmp/plot.sock

Each line gets its own OK back, and the window shows a single black pixel at (10, 10) on a white background once PRESENT arrives.

Sending AUTOPRESENT 1 flips that around: every draw command (anything except COLOR, COLORA, MODE, PRESENT, AUTOPRESENT, and QUIT itself) presents automatically afterward, so an interactive session doesn't need an explicit PRESENT after each individual shape. Turn it back off with AUTOPRESENT 0 for a batch of draw commands that should appear together.

This protocol is intentionally minimal β€” enough to let an external, possibly long-lived process (a REPL, say) hold a persistent connection and issue draw commands interactively, without recompiling or restarting anything between commands.

A Common Lisp client already exists

A small Common Lisp wrapper around this protocol lives in the common-lisp/ directory alongside the server β€” an ASDF system that connects over sb-bsd-sockets, auto-starting plot-server if none is running yet, and exposes Lisp functions like plot:pixel, plot:line, and a plot:with-frame macro:

File Description
package.lisp Package definition and exports
plot.lisp The client: socket connection and plot:* functions
plot.asd ASDF system definition

It's enough to drive the canvas interactively from an SBCL REPL, which is a genuinely different way to use this library than anything covered above. It's out of scope for this C++-focused article, though β€” it gets its own dedicated write-up.

Sample