|
|
|
@@ -0,0 +1,290 @@
|
|
|
|
|
/*
|
|
|
|
|
* 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
|