/* * 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