Files

291 lines
8.6 KiB
C++

/*
* graph.hpp
* Montauk Toolkit graph helpers
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include "gui/mtk/widgets.hpp"
namespace gui::mtk {
struct GraphHistory {
uint64_t* samples;
int capacity;
int count;
int head;
};
struct GraphStyle {
Color background;
Color border;
Color plot_bg;
Color grid;
Color label;
Color value;
Color line;
Color fill;
int radius;
int padding;
int grid_x;
int grid_y;
int line_thickness;
bool fill_area;
};
inline GraphStyle make_graph_style(const Theme& theme, Color line) {
return {
theme.window_bg,
theme.border,
theme.surface_alt,
mix(theme.surface_alt, line, 18),
theme.text_subtle,
theme.text,
line,
lighten(line, 218),
theme.radius_md,
8,
4,
4,
2,
true,
};
}
inline void graph_history_init(GraphHistory* history, uint64_t* samples, int capacity) {
if (!history) return;
history->samples = samples;
history->capacity = capacity;
history->count = 0;
history->head = 0;
}
inline void graph_history_clear(GraphHistory* history) {
if (!history) return;
history->count = 0;
history->head = 0;
}
inline void graph_history_push(GraphHistory* history, uint64_t value) {
if (!history || !history->samples || history->capacity <= 0) return;
history->samples[history->head] = value;
history->head = (history->head + 1) % history->capacity;
if (history->count < history->capacity)
history->count++;
}
inline uint64_t graph_history_at(const GraphHistory& history, int index) {
if (!history.samples || history.capacity <= 0 ||
index < 0 || index >= history.count) {
return 0;
}
int start = history.head - history.count;
while (start < 0) start += history.capacity;
return history.samples[(start + index) % history.capacity];
}
inline uint64_t graph_history_max(const GraphHistory& history) {
uint64_t max_value = 0;
for (int i = 0; i < history.count; i++) {
uint64_t value = graph_history_at(history, i);
if (value > max_value) max_value = value;
}
return max_value;
}
inline int graph_sample_x(const Rect& plot, int index, int count) {
if (count <= 1) return plot.x + plot.w - 1;
return plot.x + (int)(((int64_t)index * (plot.w - 1)) / (count - 1));
}
inline int graph_sample_y(const Rect& plot, uint64_t sample, uint64_t max_value) {
if (plot.h <= 1) return plot.y;
if (max_value == 0) max_value = 1;
if (sample > max_value) sample = max_value;
uint64_t scaled = (sample * (uint64_t)(plot.h - 1)) / max_value;
return plot.y + plot.h - 1 - (int)scaled;
}
inline void graph_draw_point(Canvas& c,
const Rect& clip,
int x,
int y,
Color color,
int thickness) {
thickness = gui_max(thickness, 1);
int radius = thickness / 2;
for (int py = y - radius; py <= y - radius + thickness - 1; py++) {
for (int px = x - radius; px <= x - radius + thickness - 1; px++) {
if (clip.contains(px, py))
c.put_pixel(px, py, color);
}
}
}
inline void graph_draw_line(Canvas& c,
const Rect& clip,
int x0,
int y0,
int x1,
int y1,
Color color,
int thickness) {
int dx = gui_abs(x1 - x0);
int sx = x0 < x1 ? 1 : -1;
int dy = -gui_abs(y1 - y0);
int sy = y0 < y1 ? 1 : -1;
int err = dx + dy;
for (;;) {
graph_draw_point(c, clip, x0, y0, color, thickness);
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 >= dy) {
err += dy;
x0 += sx;
}
if (e2 <= dx) {
err += dx;
y0 += sy;
}
}
}
inline void graph_fill_segment(Canvas& c,
const Rect& clip,
int x0,
int y0,
int x1,
int y1,
Color color) {
int bottom = clip.y + clip.h - 1;
if (x0 == x1) {
int y = gui_min(y0, y1);
if (clip.contains(x0, y))
c.vline(x0, y, bottom - y + 1, color);
return;
}
if (x1 < x0) {
int tx = x0; x0 = x1; x1 = tx;
int ty = y0; y0 = y1; y1 = ty;
}
for (int x = x0; x <= x1; x++) {
if (x < clip.x || x >= clip.x + clip.w) continue;
int y = y0 + (int)(((int64_t)(y1 - y0) * (x - x0)) / (x1 - x0));
if (y < clip.y) y = clip.y;
if (y > bottom) y = bottom;
c.vline(x, y, bottom - y + 1, color);
}
}
inline void draw_line_graph(Canvas& c,
const Rect& bounds,
const GraphHistory& history,
uint64_t max_value,
const char* title,
const char* value_label,
const Theme& theme,
const GraphStyle& style) {
if (bounds.empty()) return;
draw_rounded_frame(c, bounds, style.radius, style.background, style.border);
int fh = system_font_height();
int pad = gui_max(style.padding, 4);
int title_y = bounds.y + pad;
int title_x = bounds.x + pad;
if (title && title[0])
c.text(title_x, title_y, title, style.label);
if (value_label && value_label[0]) {
int value_w = text_width(value_label);
int value_x = bounds.x + bounds.w - pad - value_w;
if (value_x > title_x)
c.text(value_x, title_y, value_label, style.value);
}
Rect plot = {
bounds.x + pad,
bounds.y + pad + fh + 7,
bounds.w - pad * 2,
bounds.h - pad * 2 - fh - 7
};
if (plot.empty()) return;
c.fill_rect(plot.x, plot.y, plot.w, plot.h, style.plot_bg);
int sample_count = history.count;
if (sample_count > 0) {
if (max_value == 0)
max_value = graph_history_max(history);
if (max_value == 0)
max_value = 1;
if (style.fill_area && sample_count > 1) {
int prev_x = graph_sample_x(plot, 0, sample_count);
int prev_y = graph_sample_y(plot, graph_history_at(history, 0), max_value);
for (int i = 1; i < sample_count; i++) {
int x = graph_sample_x(plot, i, sample_count);
int y = graph_sample_y(plot, graph_history_at(history, i), max_value);
graph_fill_segment(c, plot, prev_x, prev_y, x, y, style.fill);
prev_x = x;
prev_y = y;
}
}
}
if (style.grid_y > 0) {
for (int i = 1; i < style.grid_y; i++) {
int y = plot.y + (plot.h * i) / style.grid_y;
c.hline(plot.x, y, plot.w, style.grid);
}
}
if (style.grid_x > 0) {
for (int i = 1; i < style.grid_x; i++) {
int x = plot.x + (plot.w * i) / style.grid_x;
c.vline(x, plot.y, plot.h, style.grid);
}
}
c.rect(plot.x, plot.y, plot.w, plot.h, style.border);
if (sample_count == 1) {
int x = graph_sample_x(plot, 0, sample_count);
int y = graph_sample_y(plot, graph_history_at(history, 0), max_value);
graph_draw_point(c, plot, x, y, style.line, style.line_thickness + 1);
} else if (sample_count > 1) {
int prev_x = graph_sample_x(plot, 0, sample_count);
int prev_y = graph_sample_y(plot, graph_history_at(history, 0), max_value);
for (int i = 1; i < sample_count; i++) {
int x = graph_sample_x(plot, i, sample_count);
int y = graph_sample_y(plot, graph_history_at(history, i), max_value);
graph_draw_line(c, plot, prev_x, prev_y, x, y,
style.line, style.line_thickness);
prev_x = x;
prev_y = y;
}
}
(void)theme;
}
inline void draw_line_graph(Canvas& c,
const Rect& bounds,
const GraphHistory& history,
uint64_t max_value,
const char* title,
const char* value_label,
const Theme& theme,
Color line) {
GraphStyle style = make_graph_style(theme, line);
draw_line_graph(c, bounds, history, max_value, title, value_label, theme, style);
}
} // namespace gui::mtk