lib/zen/src/diagram/bounds.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const category = @import("category.zig");
4 const domain = @import("domain.zig");
5 const model = @import("model.zig");
6 const root = @import("root.zig");
7 const stack = @import("stack.zig");
8
9 const spec = root.spec;
10 pub const Bounds = model.Bounds;
11
12 pub const StackTotal = struct {
13 x: []const u8,
14 positive: f64 = 0,
15 negative: f64 = 0,
16 };
17
18 const StackSpan = struct {
19 start: f64,
20 end: f64,
21 };
22
23 pub const ScratchPlan = struct {
24 categories: usize,
25 stack_totals: usize,
26
27 pub fn inspect(document: *const spec.Document) ScratchPlan {
28 var categories: usize = 0;
29 var stack_totals: usize = 0;
30 const stacked = stack.has(document);
31 for (document.marks.items, 0..) |mark, index| {
32 if (categoryValue(mark)) |label_value| {
33 if (!hasCategory(document.marks.items[0..index], label_value)) categories += 1;
34 }
35 if (stacked) switch (mark) {
36 .bar => |bar| if (!hasBar(document.marks.items[0..index], bar.x)) {
37 stack_totals += 1;
38 },
39 else => {},
40 };
41 }
42 return .{
43 .categories = categories,
44 .stack_totals = stack_totals,
45 };
46 }
47 };
48
49 pub fn compute(allocator: std.mem.Allocator, document: *const spec.Document) !Bounds {
50 const plan = ScratchPlan.inspect(document);
51 var bounds = Bounds{};
52 errdefer bounds.deinit(allocator);
53 try bounds.categories.ensureTotalCapacityPrecise(allocator, plan.categories);
54 var stack_totals: std.ArrayList(StackTotal) = .empty;
55 defer stack_totals.deinit(allocator);
56 try stack_totals.ensureTotalCapacityPrecise(allocator, plan.stack_totals);
57 return computePrepared(&bounds, &stack_totals, document);
58 }
59
60 pub fn computeIn(
61 categories: [][]const u8,
62 stack_totals: []StackTotal,
63 document: *const spec.Document,
64 ) !Bounds {
65 const plan = ScratchPlan.inspect(document);
66 std.debug.assert(categories.len >= plan.categories);
67 std.debug.assert(stack_totals.len >= plan.stack_totals);
68 var bounds = Bounds{
69 .categories = .{
70 .items = categories[0..0],
71 .capacity = categories.len,
72 },
73 .category_slots_owned = false,
74 };
75 var totals = std.ArrayList(StackTotal){
76 .items = stack_totals[0..0],
77 .capacity = stack_totals.len,
78 };
79 return computePrepared(&bounds, &totals, document);
80 }
81
82 fn computePrepared(
83 bounds: *Bounds,
84 stack_totals: *std.ArrayList(StackTotal),
85 document: *const spec.Document,
86 ) !Bounds {
87 bounds.x_kind = document.scales.x.kind;
88 bounds.y_kind = document.scales.y.kind;
89 bounds.x_base = document.scales.x.base;
90 bounds.y_base = document.scales.y.base;
91 const x_min = document.scales.x.min orelse document.frame.x_min;
92 const x_max = document.scales.x.max orelse document.frame.x_max;
93 const y_min = document.scales.y.min orelse document.frame.y_min;
94 const y_max = document.scales.y.max orelse document.frame.y_max;
95 bounds.y_min = y_min orelse if (bounds.y_kind == .log) 1 else 0;
96 bounds.y_max = y_max orelse if (bounds.y_kind == .log) 10 else 1;
97 bounds.x_min = x_min orelse if (bounds.x_kind == .log) 1 else 0;
98 bounds.x_max = x_max orelse if (bounds.x_kind == .log) 10 else 1;
99 var saw_y = y_min != null or y_max != null;
100 var saw_x = x_min != null or x_max != null;
101 const stack_bars = stack.has(document);
102 if (stack_bars and bounds.y_kind == .log) return error.InvalidScale;
103
104 for (document.marks.items) |mark| switch (mark) {
105 .bar => |bar| {
106 bounds.categorical = true;
107 category.appendAssumeCapacity(bounds, bar.x);
108 if (stack_bars) {
109 const span = advanceStack(stack_totals, bar.x, bar.y);
110 includeY(bounds, &saw_y, span.start);
111 includeY(bounds, &saw_y, span.end);
112 } else {
113 includeY(bounds, &saw_y, bar.y);
114 if (bounds.y_kind == .linear) includeY(bounds, &saw_y, 0);
115 }
116 },
117 .point => |point| {
118 includeXValue(bounds, &saw_x, point.x);
119 includeY(bounds, &saw_y, point.y);
120 },
121 .rule => |rule| {
122 includeX(bounds, &saw_x, rule.x1);
123 includeX(bounds, &saw_x, rule.x2);
124 includeY(bounds, &saw_y, rule.y1);
125 includeY(bounds, &saw_y, rule.y2);
126 },
127 .text => |text| {
128 includeXValue(bounds, &saw_x, text.x);
129 includeY(bounds, &saw_y, text.y);
130 },
131 .box => |box| {
132 includeX(bounds, &saw_x, box.x - box.width / 2);
133 includeX(bounds, &saw_x, box.x + box.width / 2);
134 includeY(bounds, &saw_y, box.y - box.height / 2);
135 includeY(bounds, &saw_y, box.y + box.height / 2);
136 },
137 .edge => |edge| {
138 includeX(bounds, &saw_x, edge.x1);
139 includeX(bounds, &saw_x, edge.x2);
140 includeY(bounds, &saw_y, edge.y1);
141 includeY(bounds, &saw_y, edge.y2);
142 },
143 };
144 if (!saw_y) {
145 bounds.y_min = if (bounds.y_kind == .log) 1 else 0;
146 bounds.y_max = if (bounds.y_kind == .log) 10 else 1;
147 }
148 if (!saw_x) {
149 bounds.x_min = if (bounds.x_kind == .log) 1 else 0;
150 bounds.x_max = if (bounds.x_kind == .log) 10 else 1;
151 }
152 domain.expand(&bounds.y_min, &bounds.y_max, bounds.y_kind, bounds.y_base);
153 domain.expand(&bounds.x_min, &bounds.x_max, bounds.x_kind, bounds.x_base);
154 try domain.validate(bounds.*);
155 return bounds.*;
156 }
157
158 fn advanceStack(
159 totals: *std.ArrayList(StackTotal),
160 x: []const u8,
161 y: f64,
162 ) StackSpan {
163 for (totals.items) |*total| {
164 if (!std.mem.eql(u8, total.x, x)) continue;
165 return advanceTotal(total, y);
166 }
167 totals.appendAssumeCapacity(.{ .x = x });
168 return advanceTotal(&totals.items[totals.items.len - 1], y);
169 }
170
171 fn advanceTotal(total: *StackTotal, y: f64) StackSpan {
172 if (y >= 0) {
173 const start = total.positive;
174 total.positive += y;
175 return .{ .start = start, .end = total.positive };
176 }
177 const start = total.negative;
178 total.negative += y;
179 return .{ .start = start, .end = total.negative };
180 }
181
182 fn includeXValue(bounds: *Bounds, saw_x: *bool, value: spec.XValue) void {
183 switch (value) {
184 .number => |number| includeX(bounds, saw_x, number),
185 .text => |text| {
186 bounds.categorical = true;
187 category.appendAssumeCapacity(bounds, text);
188 },
189 }
190 }
191
192 fn includeX(bounds: *Bounds, saw_x: *bool, value: f64) void {
193 if (!saw_x.*) {
194 bounds.x_min = value;
195 bounds.x_max = value;
196 saw_x.* = true;
197 return;
198 }
199 bounds.x_min = @min(bounds.x_min, value);
200 bounds.x_max = @max(bounds.x_max, value);
201 }
202
203 fn includeY(bounds: *Bounds, saw_y: *bool, value: f64) void {
204 if (!saw_y.*) {
205 bounds.y_min = value;
206 bounds.y_max = value;
207 saw_y.* = true;
208 return;
209 }
210 bounds.y_min = @min(bounds.y_min, value);
211 bounds.y_max = @max(bounds.y_max, value);
212 }
213
214 fn categoryValue(mark: spec.Mark) ?[]const u8 {
215 return switch (mark) {
216 .bar => |bar| bar.x,
217 .point => |point| switch (point.x) {
218 .text => |text| text,
219 .number => null,
220 },
221 .text => |text_mark| switch (text_mark.x) {
222 .text => |text| text,
223 .number => null,
224 },
225 else => null,
226 };
227 }
228
229 fn hasCategory(marks: []const spec.Mark, label_value: []const u8) bool {
230 for (marks) |mark| {
231 const candidate = categoryValue(mark) orelse continue;
232 if (std.mem.eql(u8, candidate, label_value)) return true;
233 }
234 return false;
235 }
236
237 fn hasBar(marks: []const spec.Mark, x: []const u8) bool {
238 for (marks) |mark| switch (mark) {
239 .bar => |bar| if (std.mem.eql(u8, bar.x, x)) return true,
240 else => {},
241 };
242 return false;
243 }
244
245 pub fn barBaseline(bounds: Bounds) f64 {
246 return if (bounds.y_kind == .log) bounds.y_min else 0;
247 }
248
249 test "bounds scratch plan counts unique categories and stacked totals" {
250 const marks = [_]spec.Mark{
251 .{ .bar = .{ .x = @constCast("a"), .y = 1 } },
252 .{ .bar = .{ .x = @constCast("a"), .y = 2 } },
253 .{ .point = .{ .x = .{ .text = @constCast("b") }, .y = 3 } },
254 };
255 var transforms = [_]spec.Transform{
256 .{ .stack = .{} },
257 };
258 var document = spec.Document{
259 .allocator = std.testing.allocator,
260 .marks = .{ .items = @constCast(&marks), .capacity = marks.len },
261 .transforms = .{ .items = &transforms, .capacity = transforms.len },
262 };
263 const plan = ScratchPlan.inspect(&document);
264 try std.testing.expectEqual(@as(usize, 2), plan.categories);
265 try std.testing.expectEqual(@as(usize, 1), plan.stack_totals);
266 }