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 }