lib/zen/src/diagram/ascii/plot.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const root = @import("../root.zig");
4 const canvas_mod = @import("canvas.zig");
5
6 const bounds_mod = root.bounds;
7 const category = root.category;
8 const space = root.space;
9 const spec = root.spec;
10 const Canvas = canvas_mod.Canvas;
11 const roundClamp = canvas_mod.roundClamp;
12
13 pub const Plot = struct {
14 left: usize,
15 right: usize,
16 top: usize,
17 bottom: usize,
18 };
19
20 pub fn area(width: usize, height: usize, frame: spec.Frame) Plot {
21 const top: usize = if (frame.title.len > 0) 2 else 1;
22 const bottom = if (frame.axes and height > 4) height - 4 else height - 2;
23 const left: usize = if (frame.axes) 8 else 1;
24 const right = width - 2;
25 return .{
26 .left = @min(left, width - 3),
27 .right = @max(@min(right, width - 2), @min(left + 1, width - 2)),
28 .top = @min(top, height - 3),
29 .bottom = @max(@min(bottom, height - 2), @min(top + 1, height - 2)),
30 };
31 }
32
33 pub fn drawAxes(
34 canvas: *Canvas,
35 plot: Plot,
36 document: *const spec.Document,
37 plot_bounds: bounds_mod.Bounds,
38 ) !void {
39 var y = plot.top;
40 while (y <= plot.bottom) : (y += 1) canvas.set(plot.left, y, '|');
41 var x = plot.left;
42 while (x <= plot.right) : (x += 1) canvas.set(x, plot.bottom, '-');
43 canvas.set(plot.left, plot.bottom, '+');
44
45 var top_buffer: [32]u8 = undefined;
46 var bottom_buffer: [32]u8 = undefined;
47 canvas.writeText(0, plot.top, formatNumber(&top_buffer, plot_bounds.y_max));
48 canvas.writeText(0, plot.bottom, formatNumber(&bottom_buffer, plot_bounds.y_min));
49
50 const x_label = if (document.scales.x.label.len > 0)
51 document.scales.x.label
52 else
53 document.frame.x_label;
54 if (x_label.len > 0) {
55 canvas.writeCentered(
56 (plot.left + plot.right) / 2,
57 canvas.height - 1,
58 x_label,
59 );
60 }
61
62 const y_label = if (document.scales.y.label.len > 0)
63 document.scales.y.label
64 else
65 document.frame.y_label;
66 if (y_label.len > 0 and plot.top > 0) {
67 canvas.writeText(0, plot.top - 1, y_label);
68 }
69
70 if (!plot_bounds.categorical) {
71 var left_buffer: [32]u8 = undefined;
72 var right_buffer: [32]u8 = undefined;
73 const min_text = formatNumber(&left_buffer, plot_bounds.x_min);
74 const max_text = formatNumber(&right_buffer, plot_bounds.x_max);
75 canvas.writeText(plot.left, plot.bottom + 1, min_text);
76 const right_start = if (plot.right > max_text.len)
77 plot.right - max_text.len + 1
78 else
79 plot.left;
80 canvas.writeText(right_start, plot.bottom + 1, max_text);
81 }
82 }
83
84 pub fn drawCategoryLabels(
85 canvas: *Canvas,
86 plot: Plot,
87 plot_bounds: bounds_mod.Bounds,
88 ) void {
89 if (!plot_bounds.categorical or plot.bottom + 1 >= canvas.height) return;
90 for (plot_bounds.categories.items) |label_value| {
91 const x = roundClamp(
92 category.coordinate(
93 label_value,
94 plot_bounds,
95 @floatFromInt(plot.left),
96 @floatFromInt(plot.right),
97 ),
98 plot.left,
99 plot.right,
100 );
101 canvas.writeCentered(x, plot.bottom + 1, label_value);
102 }
103 }
104
105 pub fn xCell(value: spec.XValue, plot_bounds: bounds_mod.Bounds, plot: Plot) !usize {
106 return roundClamp(
107 try space.xCoordinate(
108 value,
109 plot_bounds,
110 @floatFromInt(plot.left),
111 @floatFromInt(plot.right),
112 ),
113 plot.left,
114 plot.right,
115 );
116 }
117
118 pub fn numberXCell(value: f64, plot_bounds: bounds_mod.Bounds, plot: Plot) !usize {
119 return roundClamp(
120 try space.xNumberCoordinate(
121 value,
122 plot_bounds,
123 @floatFromInt(plot.left),
124 @floatFromInt(plot.right),
125 ),
126 plot.left,
127 plot.right,
128 );
129 }
130
131 pub fn yCell(value: f64, plot_bounds: bounds_mod.Bounds, plot: Plot) !usize {
132 return roundClamp(
133 try space.yCoordinate(
134 value,
135 plot_bounds,
136 @floatFromInt(plot.bottom),
137 @floatFromInt(plot.top),
138 ),
139 plot.top,
140 plot.bottom,
141 );
142 }
143
144 fn formatNumber(buffer: *[32]u8, value: f64) []const u8 {
145 var text = std.fmt.bufPrint(buffer, "{d:.3}", .{value}) catch unreachable;
146 while (text.len > 1 and text[text.len - 1] == '0') {
147 text = text[0 .. text.len - 1];
148 }
149 if (text.len > 1 and text[text.len - 1] == '.') {
150 text = text[0 .. text.len - 1];
151 }
152 if (std.mem.eql(u8, text, "-0")) return "0";
153 return text;
154 }