==> 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}