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 }