plot β A Minimal C++ Plotting Library
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.