skip to content
==> src/chart.c <==
1/* ratatui-widgets 0.3.2's chart.rs, ported: layout (labels, axes, titles, legend), then the x and
2 * y labels, the axes, the canvas, the titles and the legend. */
3#include "tui/chart.h"
4
5#include <string.h>
6
7tui_chart tui_chart_new(const tui_dataset *datasets, size_t n) {
8    tui_chart c = {0};
9    c.x_axis = tui_axis_new();
10    c.y_axis = tui_axis_new();
11    c.datasets = datasets;
12    c.ndatasets = n;
13    c.hidden_legend[0] = TUI_RATIO(1, 4);
14    c.hidden_legend[1] = TUI_RATIO(1, 4);
15    c.has_legend = true;
16    c.legend_position = TUI_LEGEND_TOP_RIGHT;
17    return c;
18}
19
20static uint16_t lw(const tui_line *l) {
21    size_t w = tui_line_width(l);
22    return (uint16_t)(w > 0xFFFF ? 0xFFFF : w);
23}
24
25typedef struct {
26    bool has_title_x, has_title_y, has_label_x, has_label_y, has_axis_x, has_axis_y, has_legend;
27    uint16_t title_x_x, title_x_y, title_y_x, title_y_y, label_x, label_y, axis_x, axis_y;
28    tui_rect legend, graph;
29} chart_layout;
30
31static bool legend_layout(tui_legend_position p, tui_rect area, uint16_t lw_, uint16_t lh, uint16_t xtw, uint16_t ytw,
32                          tui_rect *out) {
33    int32_t margin = (int32_t)(uint16_t)(area.height - lh); /* u16 subtraction, as ratatui's i32::from(a - b) */
34    if (xtw) margin--;
35    if (ytw) margin--;
36    if (margin < 0) return false;
37    uint16_t x = 0, y = 0;
38    switch (p) {
39    case TUI_LEGEND_TOP_RIGHT:
40        x = (uint16_t)(tui_rect_right(area) - lw_);
41        y = lw_ + ytw > area.width ? (uint16_t)(area.y + 1) : area.y;
42        break;
43    case TUI_LEGEND_TOP_LEFT:
44        x = area.x;
45        y = ytw ? (uint16_t)(area.y + 1) : area.y;
46        break;
47    case TUI_LEGEND_TOP: {
48        uint16_t off = (uint16_t)((area.width - lw_) / 2);
49        x = (uint16_t)(area.x + off);
50        y = area.x + ytw > off ? (uint16_t)(area.y + 1) : area.y;
51        break;
52    }
53    case TUI_LEGEND_LEFT:
54    case TUI_LEGEND_RIGHT: {
55        uint16_t yy = (uint16_t)((area.height - lh) / 2);
56        if (ytw) yy++;
57        if (xtw) yy = tui_sat_sub(yy, 1);
58        x = p == TUI_LEGEND_LEFT ? area.x : (uint16_t)(tui_rect_right(area) - lw_);
59        y = (uint16_t)(area.y + yy);
60        break;
61    }
62    case TUI_LEGEND_BOTTOM_LEFT:
63        x = area.x;
64        y = xtw + lw_ > area.width ? (uint16_t)(tui_rect_bottom(area) - lh - 1) : (uint16_t)(tui_rect_bottom(area) - lh);
65        break;
66    case TUI_LEGEND_BOTTOM_RIGHT:
67        x = (uint16_t)(tui_rect_right(area) - lw_);
68        y = xtw ? (uint16_t)(tui_rect_bottom(area) - lh - 1) : (uint16_t)(tui_rect_bottom(area) - lh);
69        break;
70    case TUI_LEGEND_BOTTOM: {
71        x = (uint16_t)(area.x + (area.width - lw_) / 2);
72        y = x + lw_ > tui_rect_right(area) - xtw ? (uint16_t)(tui_rect_bottom(area) - lh - 1)
73                                                 : (uint16_t)(tui_rect_bottom(area) - lh);
74        break;
75    }
76    }
77    *out = tui_rect_new(x, y, lw_, lh);
78    return true;
79}
80
81static uint16_t max_label_width_left(const tui_chart *c, tui_rect area, bool has_y_axis) {
82    uint16_t m = 0;
83    for (size_t i = 0; i < c->y_axis.nlabels; i++) m = tui_max16(m, lw(&c->y_axis.labels[i]));
84    if (c->x_axis.nlabels) {
85        uint16_t first = lw(&c->x_axis.labels[0]), left;
86        switch (c->x_axis.labels_alignment) {
87        case TUI_ALIGN_CENTER: left = (uint16_t)(first / 2); break;
88        case TUI_ALIGN_RIGHT: left = 0; break;
89        default: left = tui_sat_sub(first, has_y_axis ? 1 : 0); break;
90        }
91        m = tui_max16(m, left);
92    }
93    return tui_min16(m, (uint16_t)(area.width / 3));
94}
95
96static bool layout(const tui_chart *c, tui_rect area, chart_layout *L) {
97    if (area.height == 0 || area.width == 0) return false;
98    memset(L, 0, sizeof *L);
99    uint16_t x = area.x, y = (uint16_t)(tui_rect_bottom(area) - 1);
100    if (c->x_axis.nlabels && y > area.y) {
101        L->has_label_x = true;
102        L->label_x = y;
103        y--;
104    }
105    if (c->y_axis.nlabels) {
106        L->has_label_y = true;
107        L->label_y = x;
108    }
109    x = (uint16_t)(x + max_label_width_left(c, area, c->y_axis.nlabels > 0));
110    if (c->x_axis.nlabels && y > area.y) {
111        L->has_axis_x = true;
112        L->axis_x = y;
113        y--;
114    }
115    if (c->y_axis.nlabels && x + 1 < tui_rect_right(area)) {
116        L->has_axis_y = true;
117        L->axis_y = x;
118        x++;
119    }
120    uint16_t gw = tui_sat_sub(tui_rect_right(area), x);
121    uint16_t gh = tui_sat_add(tui_sat_sub(y, area.y), 1);
122    L->graph = tui_rect_new(x, area.y, gw, gh);
123    if (c->x_axis.has_title) {
124        uint16_t w = lw(&c->x_axis.title);
125        if (w < L->graph.width && L->graph.height > 2) {
126            L->has_title_x = true;
127            L->title_x_x = (uint16_t)(x + L->graph.width - w);
128            L->title_x_y = y;
129        }
130    }
131    if (c->y_axis.has_title) {
132        uint16_t w = lw(&c->y_axis.title);
133        if (w + 1 < L->graph.width && L->graph.height > 2) {
134            L->has_title_y = true;
135            L->title_y_x = x;
136            L->title_y_y = area.y;
137        }
138    }
139    if (c->has_legend) {
140        uint16_t inner = 0;
141        size_t count = 0;
142        for (size_t i = 0; i < c->ndatasets; i++)
143            if (c->datasets[i].has_name) {
144                inner = count ? tui_max16(inner, lw(&c->datasets[i].name)) : lw(&c->datasets[i].name);
145                count++;
146            }
147        if (count) {
148            uint16_t legend_w = (uint16_t)(inner + 2), legend_h = (uint16_t)(count + 2);
149            tui_rect mw, mh;
150            tui_layout hl = {.vertical = false, .constraints = &c->hidden_legend[0], .n = 1, .flex = TUI_FLEX_START};
151            tui_layout vl = {.vertical = true, .constraints = &c->hidden_legend[1], .n = 1, .flex = TUI_FLEX_START};
152            tui_layout_split(&hl, L->graph, &mw, NULL);
153            tui_layout_split(&vl, L->graph, &mh, NULL);
154            if (inner > 0 && legend_w <= mw.width && legend_h <= mh.height) {
155                uint16_t xtw = L->has_title_x ? lw(&c->x_axis.title) : 0;
156                uint16_t ytw = L->has_title_y ? lw(&c->y_axis.title) : 0;
157                L->has_legend = legend_layout(c->legend_position, L->graph, legend_w, legend_h, xtw, ytw, &L->legend);
158            }
159        }
160    }
161    return true;
162}
163
164static void render_label(tui_arena *a, tui_buffer *buf, const tui_line *label, tui_rect area, int8_t align) {
165    tui_line l = *label;
166    l.align = align;
167    tui_line_render(a, &l, area, buf, TUI_ALIGN_NONE);
168}
169
170static void x_labels(tui_arena *a, const tui_chart *c, tui_buffer *buf, const chart_layout *L, tui_rect chart_area) {
171    if (!L->has_label_x) return;
172    uint16_t y = L->label_x;
173    size_t n = c->x_axis.nlabels;
174    if (n < 2) return;
175    tui_rect g = L->graph;
176    uint16_t between = (uint16_t)(g.width / n);
177    uint16_t first_w = lw(&c->x_axis.labels[0]);
178    uint16_t min_x, max_x;
179    switch (c->x_axis.labels_alignment) {
180    case TUI_ALIGN_CENTER:
181        min_x = chart_area.x;
182        max_x = (uint16_t)(g.x + tui_min16(between, first_w));
183        break;
184    case TUI_ALIGN_RIGHT:
185        min_x = tui_sat_sub(g.x, 1);
186        max_x = (uint16_t)(g.x + between);
187        break;
188    default:
189        min_x = chart_area.x;
190        max_x = g.x;
191        break;
192    }
193    int8_t first_align = c->x_axis.labels_alignment == TUI_ALIGN_CENTER ? TUI_ALIGN_CENTER
194                         : c->x_axis.labels_alignment == TUI_ALIGN_RIGHT ? TUI_ALIGN_LEFT
195                                                                         : TUI_ALIGN_RIGHT;
196    render_label(a, buf, &c->x_axis.labels[0], tui_rect_new(min_x, y, (uint16_t)(max_x - min_x), 1), first_align);
197    for (size_t i = 1; i + 1 < n; i++) {
198        uint16_t x = (uint16_t)(g.x + i * between + 1);
199        render_label(a, buf, &c->x_axis.labels[i], tui_rect_new(x, y, tui_sat_sub(between, 1), 1), TUI_ALIGN_CENTER);
200    }
201    uint16_t x = (uint16_t)(tui_rect_right(g) - between);
202    render_label(a, buf, &c->x_axis.labels[n - 1], tui_rect_new(x, y, between, 1), TUI_ALIGN_RIGHT);
203}
204
205static void y_labels(tui_arena *a, const tui_chart *c, tui_buffer *buf, const chart_layout *L, tui_rect chart_area) {
206    if (!L->has_label_y) return;
207    size_t n = c->y_axis.nlabels;
208    if (n < 2) return;
209    tui_rect g = L->graph;
210    for (size_t i = 0; i < n; i++) {
211        uint16_t dy = (uint16_t)(i * (uint16_t)(g.height - 1) / (n - 1));
212        if (dy < tui_rect_bottom(g)) {
213            tui_rect r = tui_rect_new(L->label_y, (uint16_t)(tui_sat_sub(tui_rect_bottom(g), 1) - dy),
214                                      tui_sat_sub((uint16_t)(g.x - chart_area.x), 1), 1);
215            render_label(a, buf, &c->y_axis.labels[i], r, c->y_axis.labels_alignment);
216        }
217    }
218}
219
220void tui_chart_render(tui_arena *a, const tui_chart *c, tui_rect area, tui_buffer *buf) {
221    tui_buffer_set_style(buf, area, c->style);
222    if (c->block) tui_block_render(a, c->block, area, buf);
223    tui_rect chart_area = tui_block_inner_opt(c->block, area);
224    chart_layout L;
225    if (!layout(c, chart_area, &L)) return;
226    tui_rect g = L.graph;
227    const tui_cell *corner = tui_buffer_cell_c(buf, area.x, area.y);
228    tui_style original = corner ? tui_cell_style(corner) : tui_style_new();
229    x_labels(a, c, buf, &L, chart_area);
230    y_labels(a, c, buf, &L, chart_area);
231    if (L.has_axis_x)
232        for (uint16_t x = g.x; x < tui_rect_right(g); x++) {
233            tui_cell *cl = tui_buffer_cell(buf, x, L.axis_x);
234            tui_cell_set_str(cl, "─");
235            tui_cell_set_style(cl, c->x_axis.style);
236        }
237    if (L.has_axis_y)
238        for (uint16_t y = g.y; y < tui_rect_bottom(g); y++) {
239            tui_cell *cl = tui_buffer_cell(buf, L.axis_y, y);
240            tui_cell_set_str(cl, "│");
241            tui_cell_set_style(cl, c->y_axis.style);
242        }
243    if (L.has_axis_x && L.has_axis_y) {
244        tui_cell *cl = tui_buffer_cell(buf, L.axis_y, L.axis_x);
245        tui_cell_set_str(cl, "└");
246        tui_cell_set_style(cl, c->x_axis.style);
247    }
248    /* The canvas: one marker change per dataset, points, then its graph. */
249    tui_canvas *cv = tui_canvas_new(a, g.width, g.height, c->x_axis.bounds, c->y_axis.bounds, TUI_MARKER_BRAILLE);
250    for (size_t i = 0; i < c->ndatasets; i++) {
251        const tui_dataset *d = &c->datasets[i];
252        tui_canvas_marker(cv, d->marker);
253        tui_color color = d->style.has_fg ? d->style.fg : TUI_RESET;
254        tui_canvas_points(cv, d->data, d->n, color);
255        switch (d->graph_type) {
256        case TUI_GRAPH_LINE:
257            for (size_t k = 0; k + 1 < d->n; k++)
258                tui_canvas_line(cv, d->data[k].x, d->data[k].y, d->data[k + 1].x, d->data[k + 1].y, color);
259            break;
260        case TUI_GRAPH_BAR:
261            for (size_t k = 0; k < d->n; k++) tui_canvas_line(cv, d->data[k].x, 0.0, d->data[k].x, d->data[k].y, color);
262            break;
263        case TUI_GRAPH_AREA:
264            for (size_t k = 0; k + 1 < d->n; k++)
265                tui_canvas_filled_line(cv, d->data[k].x, d->data[k].y, d->data[k + 1].x, d->data[k + 1].y, d->fill_to_y, color);
266            break;
267        case TUI_GRAPH_SCATTER:
268            break;
269        }
270    }
271    tui_canvas_render(a, cv, NULL, c->style.has_bg ? c->style.bg : TUI_RESET, g, buf);
272    if (L.has_title_x) {
273        uint16_t w = tui_min16(tui_sat_sub(tui_rect_right(g), L.title_x_x), lw(&c->x_axis.title));
274        tui_buffer_set_style(buf, (tui_rect){L.title_x_x, L.title_x_y, w, 1}, original);
275        tui_buffer_set_line(buf, L.title_x_x, L.title_x_y, &c->x_axis.title, w);
276    }
277    if (L.has_title_y) {
278        uint16_t w = tui_min16(tui_sat_sub(tui_rect_right(g), L.title_y_x), lw(&c->y_axis.title));
279        tui_buffer_set_style(buf, (tui_rect){L.title_y_x, L.title_y_y, w, 1}, original);
280        tui_buffer_set_line(buf, L.title_y_x, L.title_y_y, &c->y_axis.title, w);
281    }
282    if (L.has_legend) {
283        tui_buffer_set_style(buf, L.legend, original);
284        tui_block lb = tui_block_bordered();
285        tui_block_render(a, &lb, L.legend, buf);
286        size_t i = 0;
287        for (size_t k = 0; k < c->ndatasets; k++) {
288            const tui_dataset *d = &c->datasets[k];
289            if (!d->has_name) continue;
290            tui_line name = d->name;
291            name.style = tui_style_patch(name.style, d->style);
292            tui_line_render(a, &name, (tui_rect){(uint16_t)(L.legend.x + 1), (uint16_t)(L.legend.y + 1 + i), (uint16_t)(L.legend.width - 2), 1},
293                            buf, TUI_ALIGN_NONE);
294            i++;
295        }
296    }
297}