lib/zen/src/diagram/ascii/mark.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const root = @import("../root.zig");
  2 const canvas_mod = @import("canvas.zig");
  3 const plot_mod = @import("plot.zig");
  4 
  5 const bounds_mod = root.bounds;
  6 const category = root.category;
  7 const space = root.space;
  8 const stack = root.stack;
  9 const spec = root.spec;
 10 const Canvas = canvas_mod.Canvas;
 11 const Plot = plot_mod.Plot;
 12 const distance = canvas_mod.distance;
 13 const roundClamp = canvas_mod.roundClamp;
 14 
 15 pub fn drawAll(
 16     canvas: *Canvas,
 17     plot: Plot,
 18     plot_bounds: bounds_mod.Bounds,
 19     document: *const spec.Document,
 20 ) !void {
 21     const stacked = stack.has(document);
 22     for (document.marks.items, 0..) |mark, index| {
 23         switch (mark) {
 24             .bar => |bar_mark| try drawBar(
 25                 canvas,
 26                 plot,
 27                 plot_bounds,
 28                 document.marks.items[0..index],
 29                 bar_mark,
 30                 stacked,
 31             ),
 32             .point => |point_mark| try drawPoint(
 33                 canvas,
 34                 plot,
 35                 plot_bounds,
 36                 point_mark,
 37             ),
 38             .rule => |rule_mark| try drawRule(
 39                 canvas,
 40                 plot,
 41                 plot_bounds,
 42                 rule_mark,
 43             ),
 44             .text => |text_mark| try drawText(
 45                 canvas,
 46                 plot,
 47                 plot_bounds,
 48                 text_mark,
 49             ),
 50             .box => |box_mark| try drawDiagramBox(
 51                 canvas,
 52                 plot,
 53                 plot_bounds,
 54                 box_mark,
 55             ),
 56             .edge => |edge_mark| try drawEdge(
 57                 canvas,
 58                 plot,
 59                 plot_bounds,
 60                 edge_mark,
 61             ),
 62         }
 63     }
 64 }
 65 
 66 fn drawBar(
 67     canvas: *Canvas,
 68     plot: Plot,
 69     plot_bounds: bounds_mod.Bounds,
 70     previous: []const spec.Mark,
 71     mark: spec.Bar,
 72     stacked: bool,
 73 ) !void {
 74     const count = @max(plot_bounds.categories.items.len, 1);
 75     const slot = @as(f64, @floatFromInt(plot.right - plot.left + 1)) /
 76         @as(f64, @floatFromInt(count));
 77     const bar_width = @max(
 78         @as(usize, 1),
 79         @as(usize, @intFromFloat(@max(1, @floor(slot * 0.55)))),
 80     );
 81     const center = roundClamp(
 82         category.coordinate(
 83             mark.x,
 84             plot_bounds,
 85             @floatFromInt(plot.left),
 86             @floatFromInt(plot.right),
 87         ),
 88         plot.left,
 89         plot.right,
 90     );
 91     const base = if (stacked)
 92         stack.base(previous, mark.x, mark.y)
 93     else
 94         bounds_mod.barBaseline(plot_bounds);
 95     const y0 = try plot_mod.yCell(base, plot_bounds, plot);
 96     const y1 = try plot_mod.yCell(base + mark.y, plot_bounds, plot);
 97     const x0 = if (center > bar_width / 2)
 98         center - bar_width / 2
 99     else
100         plot.left;
101     const x1 = @min(plot.right, x0 + bar_width - 1);
102     canvas.fillRect(x0, y0, x1, y1, '#');
103     const label_text = if (mark.label.len > 0) mark.label else mark.series;
104     if (label_text.len > 0) {
105         const label_y = if (@min(y0, y1) > 0)
106             @min(y0, y1) - 1
107         else
108             @min(y0, y1);
109         canvas.writeCentered(center, label_y, label_text);
110     }
111 }
112 
113 fn drawPoint(
114     canvas: *Canvas,
115     plot: Plot,
116     plot_bounds: bounds_mod.Bounds,
117     mark: spec.Point,
118 ) !void {
119     const x = try plot_mod.xCell(mark.x, plot_bounds, plot);
120     const y = try plot_mod.yCell(mark.y, plot_bounds, plot);
121     canvas.set(x, y, '*');
122     if (mark.label.len > 0) {
123         canvas.writeCentered(x, if (y > 0) y - 1 else y, mark.label);
124     }
125 }
126 
127 fn drawRule(
128     canvas: *Canvas,
129     plot: Plot,
130     plot_bounds: bounds_mod.Bounds,
131     mark: spec.Rule,
132 ) !void {
133     const x0 = try plot_mod.numberXCell(mark.x1, plot_bounds, plot);
134     const y0 = try plot_mod.yCell(mark.y1, plot_bounds, plot);
135     const x1 = try plot_mod.numberXCell(mark.x2, plot_bounds, plot);
136     const y1 = try plot_mod.yCell(mark.y2, plot_bounds, plot);
137     canvas.drawLine(x0, y0, x1, y1, lineGlyph(x0, y0, x1, y1));
138 }
139 
140 fn drawText(
141     canvas: *Canvas,
142     plot: Plot,
143     plot_bounds: bounds_mod.Bounds,
144     mark: spec.Text,
145 ) !void {
146     const x = try plot_mod.xCell(mark.x, plot_bounds, plot);
147     const y = try plot_mod.yCell(mark.y, plot_bounds, plot);
148     canvas.writeCentered(x, y, mark.text);
149 }
150 
151 fn drawDiagramBox(
152     canvas: *Canvas,
153     plot: Plot,
154     plot_bounds: bounds_mod.Bounds,
155     mark: spec.Box,
156 ) !void {
157     const x = try plot_mod.numberXCell(mark.x, plot_bounds, plot);
158     const y = try plot_mod.yCell(mark.y, plot_bounds, plot);
159     const width = @max(
160         @as(usize, 4),
161         roundDistance(space.xDistance(
162             mark.width,
163             plot_bounds,
164             @floatFromInt(plot.left),
165             @floatFromInt(plot.right),
166         )),
167     );
168     const height = @max(
169         @as(usize, 2),
170         roundDistance(space.yDistance(
171             mark.height,
172             plot_bounds,
173             @floatFromInt(plot.bottom),
174             @floatFromInt(plot.top),
175         )),
176     );
177     const x0 = if (x > width / 2) x - width / 2 else plot.left;
178     const y0 = if (y > height / 2) y - height / 2 else plot.top;
179     const x1 = @min(plot.right, x0 + width);
180     const y1 = @min(plot.bottom, y0 + height);
181     canvas.drawBox(x0, y0, x1, y1);
182     if (mark.text.len > 0 and y0 + 1 < y1) {
183         canvas.writeCentered((x0 + x1) / 2, (y0 + y1) / 2, mark.text);
184     }
185 }
186 
187 fn drawEdge(
188     canvas: *Canvas,
189     plot: Plot,
190     plot_bounds: bounds_mod.Bounds,
191     mark: spec.Edge,
192 ) !void {
193     const x0 = try plot_mod.numberXCell(mark.x1, plot_bounds, plot);
194     const y0 = try plot_mod.yCell(mark.y1, plot_bounds, plot);
195     const x1 = try plot_mod.numberXCell(mark.x2, plot_bounds, plot);
196     const y1 = try plot_mod.yCell(mark.y2, plot_bounds, plot);
197     canvas.drawLine(x0, y0, x1, y1, lineGlyph(x0, y0, x1, y1));
198     if (mark.arrow) canvas.set(x1, y1, arrowGlyph(x0, y0, x1, y1));
199     if (mark.label.len > 0) {
200         const mid_x = (x0 + x1) / 2;
201         const mid_y = (y0 + y1) / 2;
202         canvas.writeCentered(
203             mid_x,
204             if (mid_y > 0) mid_y - 1 else mid_y,
205             mark.label,
206         );
207     }
208 }
209 
210 fn lineGlyph(x0: usize, y0: usize, x1: usize, y1: usize) u8 {
211     if (y0 == y1) return '-';
212     if (x0 == x1) return '|';
213     const rising = (x1 > x0 and y1 < y0) or (x1 < x0 and y1 > y0);
214     return if (rising) '/' else '\\';
215 }
216 
217 fn arrowGlyph(x0: usize, y0: usize, x1: usize, y1: usize) u8 {
218     const dx = distance(x0, x1);
219     const dy = distance(y0, y1);
220     if (dx >= dy) return if (x1 >= x0) '>' else '<';
221     return if (y1 >= y0) 'v' else '^';
222 }
223 
224 fn roundDistance(value: f64) usize {
225     if (value <= 0) return 0;
226     return @intFromFloat(@round(value));
227 }