Sepia: named after the cuttlefish (
Sepia officinalis), which releases ink, here we "ink" pixels onto the plots.
A native C++20 plotting framework for publication-quality 2D visualizations. Single-Header and Zero external dependencies.
Handles datasets from a few points to tens of millions through built-in LTTB (Largest-Triangle-Three-Buckets) decimation.
- 2D line plots with multiple series, colors, line styles, markers, and fill
- Automatic axis generation with "nice" tick marks and grid rendering
- Built-in 5x7 bitmap font; no FreeType or external font libraries
- Xiaolin Wu anti-aliased line drawing
- LTTB Level-of-Detail decimation for mega-datasets
- Cache-line aligned memory buffers (SIMD-friendly)
- PPM image output; no libpng, no external image libraries
- Single-Header
git clone https://github.com/MoonFlowww/Sepia.git
cd SepiaSepia is Single-Header. Only need to copy-paste sepia.hpp into your project.
#include "sepia.hpp"
#include <cmath>
#include <vector>
int main() {
const size_t N = 200;
std::vector<Sepia::f64> x(N), y(N);
for (size_t i = 0; i < N; ++i) { // generate dummy data
x[i] = static_cast<double>(i) * 0.05;
y[i] = std::sin(x[i]);
}
Sepia::plot2d::Figure figure(700.0, 450.0); // Plot Init
figure.set_title("Sine Wave");
figure.set_xlabel("Time");
figure.set_ylabel("Amplitude");
figure.plot(x.data(), y.data(), N)
.data({.color = Sepia::Color::blue(), .width = 2.0, .label = "sin(x)"}); // optional parameters
figure.grid({.show = true, .major_color = {210, 210, 210}}); // optional parameters
figure.render(); // plot
figure.save_ppm("basic.ppm"); // save image
return 0;
}The central user-facing class. Create one, configure it, plot data, render, and save.
Sepia::plot2d::Figure figure(width, height); // dimensions in pixels
// Titles and labels
figure.set_title("My Plot");
figure.set_xlabel("X Axis");
figure.set_ylabel("Y Axis");
// Style configuration (all use aggregate initialization)
figure.grid({.show = true, .show_minor = true});
figure.axis({.show = true, .tick_size = 5.0});
figure.legend({.show = true, .position = "top-right"});
figure.layout({.margin_left = 80.0, .background = Sepia::Color::white()});
figure.text({.color = Sepia::Color::black(), .font_size = 12.0});
figure.perf({.lod_enable = true, .lod_target_points = 2000});
// Render and export
figure.render();
figure.save_ppm("output.ppm");Three ways to add data to a figure:
// 1. Owning (copies data into aligned storage)
figure.plot(x_ptr, y_ptr, count)
.data({.color = Sepia::Color::red(), .width = 2.0, .label = "my curve"});
// 2. Owning (move a pre-built Series)
figure.plot(sepia::data::Series(std::move(x_buf), std::move(y_buf)))
.data({.color = Sepia::Color::blue()});
// 3. Non-owning (zero-copy reference to your memory)
figure.plot_ref(x_ptr, y_ptr, count)
.data({.color = Sepia::Color::green()});figure.plot(...) returns a PlotCommand that supports chaining:
figure.plot(x, y, n)
.color(Sepia::Color::orange())
.width(2.5)
.alpha(0.8)
.label("signal")
.line(Sepia::LineStyle::Dashed)
.marker(Sepia::MarkerStyle::Circle, 3.0)
.fill(true, Sepia::Color::orange().with_alpha(50));Or pass everything at once via DataStyle:
figure.plot(x, y, n)
.data({
.color = Sepia::Color::red(),
.width = 1.5,
.alpha = 0.9,
.line_style = Sepia::LineStyle::Solid,
.marker = Sepia::MarkerStyle::Circle,
.marker_size = 4.0,
.fill = false,
.label = "my data"
});| Struct | Key fields |
|---|---|
DataStyle |
color, width, alpha, line_style, marker, marker_size, fill, fill_color, label |
GridStyle |
show, major_color, minor_color, major_width, minor_width, show_minor |
AxisStyle |
show, color, width, tick_size, x_min/x_max/y_min/y_max (-1=no default) |
LegendStyle |
show, bg_color, border, padding, position |
LayoutStyle |
margin_top/bottom/left/right, background |
TextStyle |
color, font_size, font_face |
PerfParams |
lod_enable, lod_target_points |
Built-in presets:
Sepia::Color::black() Sepia::Color::white()
Sepia::Color::red() Sepia::Color::blue()
Sepia::Color::green() Sepia::Color::orange()
Sepia::Color::purple() Sepia::Color::gray()
// Custom
Sepia::Color(r, g, b); // opaque
Sepia::Color(r, g, b, a); // with alpha
color.with_alpha(128); // semi-transparent variantLineStyle: Solid, Dashed, Dotted, DashDot, None
MarkerStyle: None, Circle, Square, Triangle, Cross, Diamond
Sepia offers three ownership models depending on your performance and lifetime needs:
| Method | Ownership | Copy? | When to use |
|---|---|---|---|
figure.plot(ptr, ptr, n) |
Figure owns | Yes, data is copied into cache-aligned AlignedBuffer |
Default. Safe after your arrays go out of scope. |
figure.plot(Series&&) |
Figure owns | No, moved in | You already have a Series or AlignedBuffer. Zero-copy transfer. |
figure.plot_ref(ptr, ptr, n) |
You own | No, zero-copy view | Performance-critical. You must keep your arrays alive until after render(). |
Sepia::AlignedBuffer<T> is a cache-line aligned (64-byte) contiguous buffer. Used internally for all owned data. SIMD-friendly.
Sepia::AlignedBuffer<sepia::f64> x(N), y(N);
// Fill x, y...
auto series = Sepia::data::Series(std::move(x), std::move(y));
figure.plot(std::move(series));When enable_lod is true (the default) automatically downsampled to lod_target_points using the Largest-Triangle-Three-Buckets algorithm before rendering. This preserves visual shape while keeping render times constant regardless of input size.
// Defaults (in PerfParams):
// lod_enable = true
// lod_target_points = 2000
// Custom tuning
figure.perf({.lod_enable = true, .lod_target_points = 5000});
// Disable decimation entirely
figure.perf({.enable_lod = false});The decimation is lazy, it happens during render() and does not modify your original data.
Sepia outputs PPM (Portable Pixmap) files. To convert to PNG:
# Using ImageMagick
convert output.ppm output.png
# Using ffmpeg
ffmpeg -i output.ppm output.png- C++17 compiler (GCC 7+, Clang 5+, MSVC 2017+)
- No external libraries
[cmd]: g++ -std=c++20 -O3 -mavx2 -mfma -ffast-math -fno-trapping-math -fno-math-errno -march=native stress/stresstest.cpp -o ./bin/stress
[cmd]: time ./a.out
Dataset Size With LTTB[2k] (ms) Without LTTB (ms)
------------ ------------------ -----------------
100 0.13 0.14
500 0.18 0.18
1000 0.23 0.22
5000 0.26 0.31
10000 0.26 0.41
50000 0.30 1.45
100000 0.34 2.80
500000 0.65 13.56
1000000 1.03 27.01
5000000 4.66 129.34
10000000 8.47 235.06
50000000 40.65 1176.12
100000000 81.09 2347.70
500000000 405.73 11741.75
1000000000 822.99 23520.82
Results plotted to stresstest_results.ppm
--- Summary ---
At 1B points:
With LTTB[2k]: 822.99 ms
Without LTTB: 23520.82 ms
Speedup: 28.6x
./bin/stress 169,42s user 1,76s system 99% cpu 2:51,36 total
Log Log scale
