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 }