lib/zen/src/diagram/spec.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 pub const XValue = union(enum) {
  4     number: f64,
  5     text: []u8,
  6 
  7     pub fn deinit(self: *XValue, allocator: std.mem.Allocator) void {
  8         switch (self.*) {
  9             .number => {},
 10             .text => |text| freeNonEmpty(allocator, text),
 11         }
 12     }
 13 };
 14 
 15 pub const Axis = enum {
 16     x,
 17     y,
 18 };
 19 
 20 pub const ScaleKind = enum {
 21     linear,
 22     log,
 23 };
 24 
 25 pub const Scale = struct {
 26     kind: ScaleKind = .linear,
 27     min: ?f64 = null,
 28     max: ?f64 = null,
 29     base: f64 = 10,
 30     label: []u8 = &.{},
 31 
 32     pub fn deinit(self: *Scale, allocator: std.mem.Allocator) void {
 33         freeNonEmpty(allocator, self.label);
 34     }
 35 };
 36 
 37 pub const Scales = struct {
 38     x: Scale = .{},
 39     y: Scale = .{},
 40 
 41     pub fn deinit(self: *Scales, allocator: std.mem.Allocator) void {
 42         self.x.deinit(allocator);
 43         self.y.deinit(allocator);
 44     }
 45 };
 46 
 47 pub const Frame = struct {
 48     width: f64 = 640,
 49     height: f64 = 360,
 50     inset: f64 = 44,
 51     axes: bool = true,
 52     title: []u8 = &.{},
 53     x_label: []u8 = &.{},
 54     y_label: []u8 = &.{},
 55     background: []u8 = &.{},
 56     y_min: ?f64 = null,
 57     y_max: ?f64 = null,
 58     x_min: ?f64 = null,
 59     x_max: ?f64 = null,
 60 
 61     pub fn deinit(self: *Frame, allocator: std.mem.Allocator) void {
 62         freeNonEmpty(allocator, self.title);
 63         freeNonEmpty(allocator, self.x_label);
 64         freeNonEmpty(allocator, self.y_label);
 65         freeNonEmpty(allocator, self.background);
 66     }
 67 };
 68 
 69 pub const DataValue = union(enum) {
 70     number: f64,
 71     text: []u8,
 72     boolean: bool,
 73 
 74     pub fn deinit(self: *DataValue, allocator: std.mem.Allocator) void {
 75         switch (self.*) {
 76             .number => {},
 77             .text => |text| freeNonEmpty(allocator, text),
 78             .boolean => {},
 79         }
 80     }
 81 };
 82 
 83 pub const Field = struct {
 84     name: []u8,
 85     value: DataValue,
 86 
 87     pub fn deinit(self: *Field, allocator: std.mem.Allocator) void {
 88         freeNonEmpty(allocator, self.name);
 89         self.value.deinit(allocator);
 90     }
 91 };
 92 
 93 pub const DataRow = struct {
 94     name: []u8,
 95     fields: std.ArrayList(Field) = .empty,
 96 
 97     pub fn deinit(self: *DataRow, allocator: std.mem.Allocator) void {
 98         freeNonEmpty(allocator, self.name);
 99         for (self.fields.items) |*field| field.deinit(allocator);
100         self.fields.deinit(allocator);
101     }
102 
103     pub fn lookup(self: *const DataRow, name: []const u8) ?DataValue {
104         for (self.fields.items) |item| {
105             if (std.mem.eql(u8, item.name, name)) return item.value;
106         }
107         return null;
108     }
109 };
110 
111 pub const Bar = struct {
112     x: []u8,
113     y: f64,
114     series: []u8 = &.{},
115     label: []u8 = &.{},
116     fill: []u8 = &.{},
117 
118     pub fn deinit(self: *Bar, allocator: std.mem.Allocator) void {
119         freeNonEmpty(allocator, self.x);
120         freeNonEmpty(allocator, self.series);
121         freeNonEmpty(allocator, self.label);
122         freeNonEmpty(allocator, self.fill);
123     }
124 };
125 
126 pub const Point = struct {
127     x: XValue,
128     y: f64,
129     label: []u8 = &.{},
130     fill: []u8 = &.{},
131     radius: f64 = 4,
132 
133     pub fn deinit(self: *Point, allocator: std.mem.Allocator) void {
134         self.x.deinit(allocator);
135         freeNonEmpty(allocator, self.label);
136         freeNonEmpty(allocator, self.fill);
137     }
138 };
139 
140 pub const Rule = struct {
141     x1: f64,
142     y1: f64,
143     x2: f64,
144     y2: f64,
145     stroke: []u8 = &.{},
146 
147     pub fn deinit(self: *Rule, allocator: std.mem.Allocator) void {
148         freeNonEmpty(allocator, self.stroke);
149     }
150 };
151 
152 pub const Text = struct {
153     x: XValue,
154     y: f64,
155     text: []u8,
156     fill: []u8 = &.{},
157 
158     pub fn deinit(self: *Text, allocator: std.mem.Allocator) void {
159         self.x.deinit(allocator);
160         freeNonEmpty(allocator, self.text);
161         freeNonEmpty(allocator, self.fill);
162     }
163 };
164 
165 pub const Box = struct {
166     x: f64,
167     y: f64,
168     width: f64,
169     height: f64,
170     text: []u8 = &.{},
171     fill: []u8 = &.{},
172     stroke: []u8 = &.{},
173 
174     pub fn deinit(self: *Box, allocator: std.mem.Allocator) void {
175         freeNonEmpty(allocator, self.text);
176         freeNonEmpty(allocator, self.fill);
177         freeNonEmpty(allocator, self.stroke);
178     }
179 };
180 
181 pub const Edge = struct {
182     x1: f64,
183     y1: f64,
184     x2: f64,
185     y2: f64,
186     label: []u8 = &.{},
187     stroke: []u8 = &.{},
188     arrow: bool = true,
189 
190     pub fn deinit(self: *Edge, allocator: std.mem.Allocator) void {
191         freeNonEmpty(allocator, self.label);
192         freeNonEmpty(allocator, self.stroke);
193     }
194 };
195 
196 pub const Stack = struct {
197     mark: []u8 = &.{},
198 
199     pub fn deinit(self: *Stack, allocator: std.mem.Allocator) void {
200         freeNonEmpty(allocator, self.mark);
201     }
202 };
203 
204 pub const Transform = union(enum) {
205     stack: Stack,
206 
207     pub fn deinit(self: *Transform, allocator: std.mem.Allocator) void {
208         switch (self.*) {
209             .stack => |*transform| transform.deinit(allocator),
210         }
211     }
212 };
213 
214 pub const ChannelMarkKind = enum {
215     bar,
216     point,
217     text,
218 };
219 
220 pub const ChannelMark = struct {
221     kind: ChannelMarkKind,
222     data: []u8,
223     x: []u8,
224     y: []u8,
225     label: []u8 = &.{},
226     fill: []u8 = &.{},
227     series: []u8 = &.{},
228     text: []u8 = &.{},
229     radius: []u8 = &.{},
230 
231     pub fn deinit(self: *ChannelMark, allocator: std.mem.Allocator) void {
232         freeNonEmpty(allocator, self.data);
233         freeNonEmpty(allocator, self.x);
234         freeNonEmpty(allocator, self.y);
235         freeNonEmpty(allocator, self.label);
236         freeNonEmpty(allocator, self.fill);
237         freeNonEmpty(allocator, self.series);
238         freeNonEmpty(allocator, self.text);
239         freeNonEmpty(allocator, self.radius);
240     }
241 };
242 
243 pub const Mark = union(enum) {
244     bar: Bar,
245     point: Point,
246     rule: Rule,
247     text: Text,
248     box: Box,
249     edge: Edge,
250 
251     pub fn deinit(self: *Mark, allocator: std.mem.Allocator) void {
252         switch (self.*) {
253             .bar => |*mark| mark.deinit(allocator),
254             .point => |*mark| mark.deinit(allocator),
255             .rule => |*mark| mark.deinit(allocator),
256             .text => |*mark| mark.deinit(allocator),
257             .box => |*mark| mark.deinit(allocator),
258             .edge => |*mark| mark.deinit(allocator),
259         }
260     }
261 };
262 
263 pub const RecordKind = enum {
264     frame,
265     scale,
266     transform,
267     data,
268     mark,
269     bar,
270     point,
271     rule,
272     text,
273     box,
274     edge,
275 };
276 
277 pub fn recordKind(name: []const u8) ?RecordKind {
278     inline for (@typeInfo(RecordKind).@"enum".field_names, std.meta.tags(RecordKind)) |field_name, value| {
279         if (std.mem.eql(u8, name, field_name)) return value;
280     }
281     return null;
282 }
283 
284 pub fn recordKindOf(value: std.json.Value) !RecordKind {
285     const object = switch (value) {
286         .object => |object| object,
287         else => return error.InvalidRecord,
288     };
289     const kind = objectString(object, "kind") orelse return error.MissingKind;
290     return recordKind(kind) orelse return error.UnknownRecordKind;
291 }
292 
293 pub const LineIterator = struct {
294     lines: std.mem.SplitIterator(u8, .scalar),
295 
296     pub fn init(jsonl: []const u8) LineIterator {
297         return .{ .lines = std.mem.splitScalar(u8, jsonl, '\n') };
298     }
299 
300     pub fn next(self: *LineIterator) ?[]const u8 {
301         while (self.lines.next()) |raw_line| {
302             const line = std.mem.trim(u8, raw_line, " \t\r\n");
303             if (line.len == 0) continue;
304             return line;
305         }
306         return null;
307     }
308 };
309 
310 pub const Document = struct {
311     allocator: std.mem.Allocator,
312     frame: Frame = .{},
313     scales: Scales = .{},
314     transforms: std.ArrayList(Transform) = .empty,
315     data: std.ArrayList(DataRow) = .empty,
316     marks: std.ArrayList(Mark) = .empty,
317     scratch_high_water: usize = 0,
318 
319     pub fn parse(allocator: std.mem.Allocator, line_scratch: []u8, jsonl: []const u8) !Document {
320         var document = Document{ .allocator = allocator };
321         errdefer document.deinit();
322 
323         var scratch = std.heap.FixedBufferAllocator.init(line_scratch);
324         var lines = LineIterator.init(jsonl);
325         while (lines.next()) |line| {
326             scratch.reset();
327             var parsed = std.json.parseFromSlice(std.json.Value, scratch.allocator(), line, .{}) catch |err| switch (err) {
328                 error.OutOfMemory => return error.OutOfMemory,
329                 else => return error.InvalidJsonl,
330             };
331             defer parsed.deinit();
332             try document.applyRecord(parsed.value);
333             document.scratch_high_water = @max(document.scratch_high_water, scratch.end_index);
334         }
335         try validateDocument(&document);
336         return document;
337     }
338 
339     pub fn deinit(self: *Document) void {
340         self.frame.deinit(self.allocator);
341         self.scales.deinit(self.allocator);
342         for (self.transforms.items) |*transform| transform.deinit(self.allocator);
343         self.transforms.deinit(self.allocator);
344         for (self.data.items) |*row| row.deinit(self.allocator);
345         self.data.deinit(self.allocator);
346         for (self.marks.items) |*mark| mark.deinit(self.allocator);
347         self.marks.deinit(self.allocator);
348     }
349 
350     fn applyRecord(self: *Document, value: std.json.Value) !void {
351         const object = switch (value) {
352             .object => |object| object,
353             else => return error.InvalidRecord,
354         };
355         switch (try recordKindOf(value)) {
356             .frame => try self.applyFrame(object),
357             .scale => try self.applyScale(object),
358             .transform => try self.applyTransform(object),
359             .data => try self.applyData(object),
360             .mark => try self.applyChannelMark(object),
361             .bar => try self.marks.append(self.allocator, .{ .bar = try parseBar(self.allocator, object) }),
362             .point => try self.marks.append(self.allocator, .{ .point = try parsePoint(self.allocator, object) }),
363             .rule => try self.marks.append(self.allocator, .{ .rule = try parseRule(self.allocator, object) }),
364             .text => try self.marks.append(self.allocator, .{ .text = try parseText(self.allocator, object) }),
365             .box => try self.marks.append(self.allocator, .{ .box = try parseBox(self.allocator, object) }),
366             .edge => try self.marks.append(self.allocator, .{ .edge = try parseEdge(self.allocator, object) }),
367         }
368     }
369 
370     fn applyFrame(self: *Document, object: std.json.ObjectMap) !void {
371         if (objectNumber(object, "width")) |value| self.frame.width = value;
372         if (objectNumber(object, "height")) |value| self.frame.height = value;
373         if (objectNumber(object, "inset")) |value| self.frame.inset = value;
374         if (objectBool(object, "axes")) |value| self.frame.axes = value;
375         if (objectNumber(object, "y_min")) |value| self.frame.y_min = value;
376         if (objectNumber(object, "y_max")) |value| self.frame.y_max = value;
377         if (objectNumber(object, "x_min")) |value| self.frame.x_min = value;
378         if (objectNumber(object, "x_max")) |value| self.frame.x_max = value;
379         try replaceString(self.allocator, &self.frame.title, objectString(object, "title"));
380         try replaceString(self.allocator, &self.frame.x_label, objectString(object, "x_label"));
381         try replaceString(self.allocator, &self.frame.y_label, objectString(object, "y_label"));
382         if (objectString(object, "background")) |background| {
383             if (!validPaint(background)) return error.InvalidColor;
384             try replaceString(self.allocator, &self.frame.background, background);
385         }
386     }
387 
388     fn applyScale(self: *Document, object: std.json.ObjectMap) !void {
389         const axis = try parseAxis(object);
390         const scale = switch (axis) {
391             .x => &self.scales.x,
392             .y => &self.scales.y,
393         };
394         if (objectString(object, "type")) |value| scale.kind = try parseScaleKind(value);
395         if (objectNumber(object, "min")) |value| scale.min = value;
396         if (objectNumber(object, "max")) |value| scale.max = value;
397         if (objectNumber(object, "base")) |value| scale.base = value;
398         try replaceString(self.allocator, &scale.label, objectString(object, "label"));
399     }
400 
401     fn applyTransform(self: *Document, object: std.json.ObjectMap) !void {
402         var transform = try parseTransform(self.allocator, object);
403         errdefer transform.deinit(self.allocator);
404         try self.transforms.append(self.allocator, transform);
405     }
406 
407     fn applyData(self: *Document, object: std.json.ObjectMap) !void {
408         var row = try parseDataRow(self.allocator, object);
409         errdefer row.deinit(self.allocator);
410         try self.data.append(self.allocator, row);
411     }
412 
413     fn applyChannelMark(self: *Document, object: std.json.ObjectMap) !void {
414         var channel = try parseChannelMark(self.allocator, object);
415         defer channel.deinit(self.allocator);
416         var matched = false;
417         for (self.data.items) |*row| {
418             if (!std.mem.eql(u8, row.name, channel.data)) continue;
419             matched = true;
420             var mark = try markFromChannel(self.allocator, channel, row);
421             var appended = false;
422             errdefer if (!appended) mark.deinit(self.allocator);
423             try self.marks.append(self.allocator, mark);
424             appended = true;
425         }
426         if (!matched) return error.UnknownData;
427     }
428 };
429 
430 fn parseDataRow(allocator: std.mem.Allocator, object: std.json.ObjectMap) !DataRow {
431     var row = DataRow{ .name = try dupeDatasetName(allocator, object) };
432     errdefer row.deinit(allocator);
433     var iter = object.iterator();
434     while (iter.next()) |entry| {
435         const key = entry.key_ptr.*;
436         if (dataMetaKey(key)) continue;
437         const name = try allocator.dupe(u8, key);
438         var name_owned = true;
439         errdefer if (name_owned) allocator.free(name);
440         var value = try parseDataValue(allocator, entry.value_ptr.*);
441         var value_owned = true;
442         errdefer if (value_owned) value.deinit(allocator);
443         try row.fields.append(allocator, .{
444             .name = name,
445             .value = value,
446         });
447         name_owned = false;
448         value_owned = false;
449     }
450     if (row.fields.items.len == 0) return error.InvalidData;
451     return row;
452 }
453 
454 fn parseDataValue(allocator: std.mem.Allocator, value: std.json.Value) !DataValue {
455     return switch (value) {
456         .string => |string| .{ .text = try allocator.dupe(u8, string) },
457         .integer => |integer| .{ .number = @floatFromInt(integer) },
458         .float => |float| .{ .number = float },
459         .number_string => |string| .{ .number = try std.fmt.parseFloat(f64, string) },
460         .bool => |boolean| .{ .boolean = boolean },
461         else => error.InvalidField,
462     };
463 }
464 
465 fn parseChannelMark(allocator: std.mem.Allocator, object: std.json.ObjectMap) !ChannelMark {
466     var channel = ChannelMark{
467         .kind = try parseChannelMarkKind(objectString(object, "type") orelse return error.MissingField),
468         .data = &.{},
469         .x = &.{},
470         .y = &.{},
471     };
472     errdefer channel.deinit(allocator);
473     channel.data = try dupeRequiredString(allocator, object, "data");
474     channel.x = try dupeRequiredString(allocator, object, "x");
475     channel.y = try dupeRequiredString(allocator, object, "y");
476     channel.label = try dupeOptionalString(allocator, object, "label");
477     channel.fill = try dupeOptionalString(allocator, object, "fill");
478     channel.series = try dupeOptionalString(allocator, object, "series");
479     channel.text = try dupeOptionalString(allocator, object, "text");
480     channel.radius = try dupeOptionalString(allocator, object, "radius");
481     if (channel.kind == .text and channel.text.len == 0) return error.MissingField;
482     return channel;
483 }
484 
485 fn markFromChannel(allocator: std.mem.Allocator, channel: ChannelMark, row: *const DataRow) !Mark {
486     return switch (channel.kind) {
487         .bar => .{ .bar = try barFromChannel(allocator, channel, row) },
488         .point => .{ .point = try pointFromChannel(allocator, channel, row) },
489         .text => .{ .text = try textFromChannel(allocator, channel, row) },
490     };
491 }
492 
493 fn barFromChannel(allocator: std.mem.Allocator, channel: ChannelMark, row: *const DataRow) !Bar {
494     var bar = Bar{ .x = &.{}, .y = 0 };
495     errdefer bar.deinit(allocator);
496     bar.x = try requiredChannelText(allocator, row, channel.x);
497     bar.y = try requiredChannelNumber(row, channel.y);
498     bar.series = try optionalChannelText(allocator, row, channel.series);
499     bar.label = try optionalChannelText(allocator, row, channel.label);
500     bar.fill = try optionalChannelPaint(allocator, row, channel.fill);
501     return bar;
502 }
503 
504 fn pointFromChannel(allocator: std.mem.Allocator, channel: ChannelMark, row: *const DataRow) !Point {
505     var point = Point{ .x = .{ .number = 0 }, .y = 0 };
506     errdefer point.deinit(allocator);
507     point.x = try requiredChannelX(allocator, row, channel.x);
508     point.y = try requiredChannelNumber(row, channel.y);
509     point.label = try optionalChannelText(allocator, row, channel.label);
510     point.fill = try optionalChannelPaint(allocator, row, channel.fill);
511     point.radius = if (channel.radius.len == 0) 4 else try requiredChannelNumber(row, channel.radius);
512     return point;
513 }
514 
515 fn textFromChannel(allocator: std.mem.Allocator, channel: ChannelMark, row: *const DataRow) !Text {
516     var text = Text{ .x = .{ .number = 0 }, .y = 0, .text = &.{} };
517     errdefer text.deinit(allocator);
518     text.x = try requiredChannelX(allocator, row, channel.x);
519     text.y = try requiredChannelNumber(row, channel.y);
520     text.text = try requiredChannelText(allocator, row, channel.text);
521     text.fill = try optionalChannelPaint(allocator, row, channel.fill);
522     return text;
523 }
524 
525 fn parseBar(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Bar {
526     var bar = Bar{ .x = &.{}, .y = 0 };
527     errdefer bar.deinit(allocator);
528     bar.x = try dupeRequiredString(allocator, object, "x");
529     bar.y = objectNumber(object, "y") orelse return error.MissingField;
530     bar.series = try dupeOptionalString(allocator, object, "series");
531     bar.label = try dupeOptionalString(allocator, object, "label");
532     bar.fill = try dupePaint(allocator, object, "fill");
533     return bar;
534 }
535 
536 fn parsePoint(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Point {
537     var point = Point{ .x = .{ .number = 0 }, .y = 0 };
538     errdefer point.deinit(allocator);
539     point.x = try parseX(allocator, object, "x");
540     point.y = objectNumber(object, "y") orelse return error.MissingField;
541     point.label = try dupeOptionalString(allocator, object, "label");
542     point.fill = try dupePaint(allocator, object, "fill");
543     point.radius = objectNumber(object, "radius") orelse 4;
544     return point;
545 }
546 
547 fn parseRule(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Rule {
548     var rule = Rule{ .x1 = 0, .y1 = 0, .x2 = 0, .y2 = 0 };
549     errdefer rule.deinit(allocator);
550     rule.x1 = objectNumber(object, "x1") orelse return error.MissingField;
551     rule.y1 = objectNumber(object, "y1") orelse return error.MissingField;
552     rule.x2 = objectNumber(object, "x2") orelse return error.MissingField;
553     rule.y2 = objectNumber(object, "y2") orelse return error.MissingField;
554     rule.stroke = try dupePaint(allocator, object, "stroke");
555     return rule;
556 }
557 
558 fn parseText(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Text {
559     var text = Text{ .x = .{ .number = 0 }, .y = 0, .text = &.{} };
560     errdefer text.deinit(allocator);
561     text.x = try parseX(allocator, object, "x");
562     text.y = objectNumber(object, "y") orelse return error.MissingField;
563     text.text = try dupeRequiredString(allocator, object, "text");
564     text.fill = try dupePaint(allocator, object, "fill");
565     return text;
566 }
567 
568 fn parseBox(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Box {
569     var box = Box{ .x = 0, .y = 0, .width = 0, .height = 0 };
570     errdefer box.deinit(allocator);
571     box.x = objectNumber(object, "x") orelse return error.MissingField;
572     box.y = objectNumber(object, "y") orelse return error.MissingField;
573     box.width = objectNumber(object, "width") orelse return error.MissingField;
574     box.height = objectNumber(object, "height") orelse return error.MissingField;
575     if (box.width <= 0 or box.height <= 0) return error.InvalidField;
576     box.text = try dupeOptionalString(allocator, object, "text");
577     box.fill = try dupePaint(allocator, object, "fill");
578     box.stroke = try dupePaint(allocator, object, "stroke");
579     return box;
580 }
581 
582 fn parseEdge(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Edge {
583     var edge = Edge{ .x1 = 0, .y1 = 0, .x2 = 0, .y2 = 0 };
584     errdefer edge.deinit(allocator);
585     edge.x1 = objectNumber(object, "x1") orelse return error.MissingField;
586     edge.y1 = objectNumber(object, "y1") orelse return error.MissingField;
587     edge.x2 = objectNumber(object, "x2") orelse return error.MissingField;
588     edge.y2 = objectNumber(object, "y2") orelse return error.MissingField;
589     edge.label = try dupeOptionalString(allocator, object, "label");
590     edge.stroke = try dupePaint(allocator, object, "stroke");
591     edge.arrow = objectBool(object, "arrow") orelse true;
592     return edge;
593 }
594 
595 fn parseTransform(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Transform {
596     const op = objectString(object, "op") orelse return error.MissingField;
597     if (std.mem.eql(u8, op, "stack")) {
598         var stack = Stack{ .mark = &.{} };
599         errdefer stack.deinit(allocator);
600         stack.mark = try dupeOptionalString(allocator, object, "mark");
601         if (stack.mark.len != 0 and !std.mem.eql(u8, stack.mark, "bar")) return error.InvalidTransform;
602         return .{ .stack = stack };
603     }
604     return error.UnknownTransform;
605 }
606 
607 fn parseAxis(object: std.json.ObjectMap) !Axis {
608     const axis = objectString(object, "axis") orelse return error.MissingField;
609     if (std.mem.eql(u8, axis, "x")) return .x;
610     if (std.mem.eql(u8, axis, "y")) return .y;
611     return error.InvalidScale;
612 }
613 
614 fn parseScaleKind(value: []const u8) !ScaleKind {
615     if (std.mem.eql(u8, value, "linear")) return .linear;
616     if (std.mem.eql(u8, value, "log")) return .log;
617     return error.InvalidScale;
618 }
619 
620 fn parseChannelMarkKind(value: []const u8) !ChannelMarkKind {
621     if (std.mem.eql(u8, value, "bar")) return .bar;
622     if (std.mem.eql(u8, value, "point")) return .point;
623     if (std.mem.eql(u8, value, "text")) return .text;
624     return error.UnknownMark;
625 }
626 
627 fn parseX(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8) !XValue {
628     const value = object.get(key) orelse return error.MissingField;
629     return switch (value) {
630         .string => |string| .{ .text = try allocator.dupe(u8, string) },
631         .integer => |integer| .{ .number = @floatFromInt(integer) },
632         .float => |float| .{ .number = float },
633         .number_string => |string| .{ .number = try std.fmt.parseFloat(f64, string) },
634         else => error.InvalidField,
635     };
636 }
637 
638 fn requiredChannelValue(row: *const DataRow, field: []const u8) !DataValue {
639     if (field.len == 0) return error.MissingField;
640     return row.lookup(field) orelse error.MissingField;
641 }
642 
643 fn requiredChannelNumber(row: *const DataRow, field: []const u8) !f64 {
644     return try dataValueNumber(try requiredChannelValue(row, field));
645 }
646 
647 fn requiredChannelText(allocator: std.mem.Allocator, row: *const DataRow, field: []const u8) ![]u8 {
648     return try dataValueText(allocator, try requiredChannelValue(row, field));
649 }
650 
651 fn requiredChannelX(allocator: std.mem.Allocator, row: *const DataRow, field: []const u8) !XValue {
652     return try dataValueX(allocator, try requiredChannelValue(row, field));
653 }
654 
655 fn optionalChannelText(allocator: std.mem.Allocator, row: *const DataRow, field: []const u8) ![]u8 {
656     if (field.len == 0) return try allocator.dupe(u8, "");
657     return try requiredChannelText(allocator, row, field);
658 }
659 
660 fn optionalChannelPaint(allocator: std.mem.Allocator, row: *const DataRow, field: []const u8) ![]u8 {
661     const value = try optionalChannelText(allocator, row, field);
662     errdefer allocator.free(value);
663     if (!validPaint(value)) return error.InvalidColor;
664     return value;
665 }
666 
667 fn dataValueNumber(value: DataValue) !f64 {
668     return switch (value) {
669         .number => |number| number,
670         .text => |text| std.fmt.parseFloat(f64, text) catch error.InvalidField,
671         .boolean => error.InvalidField,
672     };
673 }
674 
675 fn dataValueText(allocator: std.mem.Allocator, value: DataValue) ![]u8 {
676     return switch (value) {
677         .number => |number| try std.fmt.allocPrint(allocator, "{d}", .{number}),
678         .text => |text| try allocator.dupe(u8, text),
679         .boolean => |boolean| try allocator.dupe(u8, if (boolean) "true" else "false"),
680     };
681 }
682 
683 fn dataValueX(allocator: std.mem.Allocator, value: DataValue) !XValue {
684     return switch (value) {
685         .number => |number| .{ .number = number },
686         .text => |text| .{ .text = try allocator.dupe(u8, text) },
687         .boolean => |boolean| .{ .text = try allocator.dupe(u8, if (boolean) "true" else "false") },
688     };
689 }
690 
691 fn objectBool(object: std.json.ObjectMap, key: []const u8) ?bool {
692     const value = object.get(key) orelse return null;
693     return switch (value) {
694         .bool => |boolean| boolean,
695         else => null,
696     };
697 }
698 
699 fn objectString(object: std.json.ObjectMap, key: []const u8) ?[]const u8 {
700     const value = object.get(key) orelse return null;
701     return switch (value) {
702         .string => |string| string,
703         else => null,
704     };
705 }
706 
707 fn objectNumber(object: std.json.ObjectMap, key: []const u8) ?f64 {
708     const value = object.get(key) orelse return null;
709     return switch (value) {
710         .integer => |integer| @floatFromInt(integer),
711         .float => |float| float,
712         .number_string => |string| std.fmt.parseFloat(f64, string) catch null,
713         else => null,
714     };
715 }
716 
717 fn dupeDatasetName(allocator: std.mem.Allocator, object: std.json.ObjectMap) ![]u8 {
718     if (objectString(object, "name")) |name| return try allocator.dupe(u8, name);
719     if (objectString(object, "id")) |id| return try allocator.dupe(u8, id);
720     return error.MissingField;
721 }
722 
723 fn dupeRequiredString(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8) ![]u8 {
724     const value = objectString(object, key) orelse return error.MissingField;
725     return try allocator.dupe(u8, value);
726 }
727 
728 fn dupeOptionalString(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8) ![]u8 {
729     const value = objectString(object, key) orelse return try allocator.dupe(u8, "");
730     return try allocator.dupe(u8, value);
731 }
732 
733 fn dupePaint(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8) ![]u8 {
734     const value = objectString(object, key) orelse return try allocator.dupe(u8, "");
735     if (!validPaint(value)) return error.InvalidColor;
736     return try allocator.dupe(u8, value);
737 }
738 
739 fn replaceString(allocator: std.mem.Allocator, target: *[]u8, value: ?[]const u8) !void {
740     const source = value orelse return;
741     freeNonEmpty(allocator, target.*);
742     target.* = try allocator.dupe(u8, source);
743 }
744 
745 fn validateDocument(document: *const Document) !void {
746     try validateFrame(document.frame);
747     try validateScale(document.scales.x);
748     try validateScale(document.scales.y);
749 }
750 
751 fn validateFrame(frame: Frame) !void {
752     if (frame.width < 160 or frame.width > 4000) return error.InvalidFrame;
753     if (frame.height < 120 or frame.height > 3000) return error.InvalidFrame;
754     if (frame.inset < 8 or frame.inset > 400) return error.InvalidFrame;
755     if (frame.y_min != null and frame.y_max != null and frame.y_min.? >= frame.y_max.?) return error.InvalidFrame;
756     if (frame.x_min != null and frame.x_max != null and frame.x_min.? >= frame.x_max.?) return error.InvalidFrame;
757 }
758 
759 fn validateScale(scale: Scale) !void {
760     if (scale.base <= 1) return error.InvalidScale;
761     if (scale.min != null and scale.max != null and scale.min.? >= scale.max.?) return error.InvalidScale;
762     if (scale.kind == .log) {
763         if (scale.min != null and scale.min.? <= 0) return error.InvalidScale;
764         if (scale.max != null and scale.max.? <= 0) return error.InvalidScale;
765     }
766 }
767 
768 fn validPaint(value: []const u8) bool {
769     if (value.len == 0) return true;
770     if (value.len != 4 and value.len != 7) return false;
771     if (value[0] != '#') return false;
772     for (value[1..]) |byte| {
773         if (!std.ascii.isHex(byte)) return false;
774     }
775     return true;
776 }
777 
778 fn dataMetaKey(key: []const u8) bool {
779     return std.mem.eql(u8, key, "kind") or std.mem.eql(u8, key, "name") or std.mem.eql(u8, key, "id");
780 }
781 
782 fn freeNonEmpty(allocator: std.mem.Allocator, bytes: []u8) void {
783     if (bytes.len > 0) allocator.free(bytes);
784 }
785 
786 test "diagram spec parses frame and bars" {
787     const allocator = std.testing.allocator;
788     const input =
789         \\{"kind":"frame","width":500,"height":280,"title":"Coin","x_label":"outcome","y_label":"p","y_min":0,"y_max":1}
790         \\{"kind":"bar","x":"heads","y":0.6,"fill":"#245f8d"}
791         \\{"kind":"bar","x":"tails","y":0.4,"fill":"#8f3a32"}
792         \\
793     ;
794     var document = try parseForTest(allocator, input);
795     defer document.deinit();
796     try std.testing.expectEqual(@as(usize, 2), document.marks.items.len);
797     try std.testing.expectEqualStrings("Coin", document.frame.title);
798     try std.testing.expectEqualStrings("heads", document.marks.items[0].bar.x);
799 }
800 
801 test "diagram spec parses scales transforms and series" {
802     const allocator = std.testing.allocator;
803     const input =
804         \\{"kind":"frame","width":500,"height":280,"title":"Counts"}
805         \\{"kind":"scale","axis":"y","type":"log","min":1,"max":1000,"label":"events","base":10}
806         \\{"kind":"transform","op":"stack","mark":"bar"}
807         \\{"kind":"bar","x":"a","y":10,"series":"first","fill":"#245f8d"}
808         \\{"kind":"bar","x":"a","y":40,"series":"second","fill":"#8f3a32"}
809         \\
810     ;
811     var document = try parseForTest(allocator, input);
812     defer document.deinit();
813     try std.testing.expectEqual(ScaleKind.log, document.scales.y.kind);
814     try std.testing.expectEqual(@as(f64, 1), document.scales.y.min.?);
815     try std.testing.expectEqual(@as(f64, 1000), document.scales.y.max.?);
816     try std.testing.expectEqualStrings("events", document.scales.y.label);
817     try std.testing.expectEqual(@as(usize, 1), document.transforms.items.len);
818     try std.testing.expectEqualStrings("bar", document.transforms.items[0].stack.mark);
819     try std.testing.expectEqualStrings("second", document.marks.items[1].bar.series);
820 }
821 
822 test "diagram spec expands data marks" {
823     const allocator = std.testing.allocator;
824     const input =
825         \\{"kind":"frame","width":500,"height":280,"title":"Coin","y_min":0,"y_max":1}
826         \\{"kind":"data","name":"coin","outcome":"heads","probability":0.62,"paint":"#245f8d","label":"heads"}
827         \\{"kind":"data","name":"coin","outcome":"tails","probability":0.38,"paint":"#8f3a32","label":"tails"}
828         \\{"kind":"mark","type":"bar","data":"coin","x":"outcome","y":"probability","fill":"paint","label":"label"}
829         \\
830     ;
831     var document = try parseForTest(allocator, input);
832     defer document.deinit();
833     try std.testing.expectEqual(@as(usize, 2), document.data.items.len);
834     try std.testing.expectEqual(@as(usize, 2), document.marks.items.len);
835     try std.testing.expectEqualStrings("heads", document.marks.items[0].bar.x);
836     try std.testing.expectEqual(@as(f64, 0.62), document.marks.items[0].bar.y);
837     try std.testing.expectEqualStrings("#8f3a32", document.marks.items[1].bar.fill);
838 }
839 
840 test "diagram spec expands point and text channels" {
841     const allocator = std.testing.allocator;
842     const input =
843         \\{"kind":"frame","width":500,"height":280,"title":"Latency"}
844         \\{"kind":"data","name":"latency","events":10,"micros":120,"label":"p50","paint":"#245f8d","size":5}
845         \\{"kind":"mark","type":"point","data":"latency","x":"events","y":"micros","label":"label","fill":"paint","radius":"size"}
846         \\{"kind":"mark","type":"text","data":"latency","x":"events","y":"micros","text":"label","fill":"paint"}
847         \\
848     ;
849     var document = try parseForTest(allocator, input);
850     defer document.deinit();
851     try std.testing.expectEqual(@as(usize, 2), document.marks.items.len);
852     try std.testing.expectEqual(@as(f64, 5), document.marks.items[0].point.radius);
853     try std.testing.expectEqualStrings("p50", document.marks.items[0].point.label);
854     try std.testing.expectEqualStrings("p50", document.marks.items[1].text.text);
855 }
856 
857 test "diagram spec parses boxes and edges" {
858     const allocator = std.testing.allocator;
859     const input =
860         \\{"kind":"frame","width":640,"height":360,"title":"Flow","axes":false,"x_min":0,"x_max":10,"y_min":0,"y_max":10}
861         \\{"kind":"edge","x1":3,"y1":7,"x2":7,"y2":7,"label":"message"}
862         \\{"kind":"box","x":3,"y":7,"width":2.4,"height":1.2,"text":"actor <A>","fill":"#eef5f1","stroke":"#245f8d"}
863         \\{"kind":"box","x":7,"y":7,"width":2.4,"height":1.2,"text":"actor B"}
864         \\
865     ;
866     var document = try parseForTest(allocator, input);
867     defer document.deinit();
868     try std.testing.expect(!document.frame.axes);
869     try std.testing.expectEqual(@as(usize, 3), document.marks.items.len);
870     try std.testing.expectEqualStrings("actor <A>", document.marks.items[1].box.text);
871     try std.testing.expect(document.marks.items[0].edge.arrow);
872 }
873 
874 test "diagram spec rejects unknown marks and unsafe colors" {
875     const allocator = std.testing.allocator;
876     try std.testing.expectError(error.UnknownRecordKind, parseForTest(allocator, "{\"kind\":\"arc\"}\n"));
877     try std.testing.expectError(error.InvalidColor, parseForTest(allocator, "{\"kind\":\"bar\",\"x\":\"a\",\"y\":1,\"fill\":\"url(x)\"}\n"));
878     try std.testing.expectError(error.InvalidField, parseForTest(allocator, "{\"kind\":\"box\",\"x\":1,\"y\":1,\"width\":0,\"height\":1}\n"));
879     try std.testing.expectError(error.InvalidScale, parseForTest(allocator, "{\"kind\":\"scale\",\"axis\":\"y\",\"type\":\"log\",\"min\":0}\n"));
880     try std.testing.expectError(error.InvalidTransform, parseForTest(allocator, "{\"kind\":\"transform\",\"op\":\"stack\",\"mark\":\"point\"}\n"));
881     try std.testing.expectError(error.UnknownData, parseForTest(allocator, "{\"kind\":\"mark\",\"type\":\"bar\",\"data\":\"missing\",\"x\":\"x\",\"y\":\"y\"}\n"));
882     try std.testing.expectError(error.InvalidColor, parseForTest(allocator, "{\"kind\":\"data\",\"name\":\"d\",\"x\":\"a\",\"y\":1,\"paint\":\"url(x)\"}\n{\"kind\":\"mark\",\"type\":\"bar\",\"data\":\"d\",\"x\":\"x\",\"y\":\"y\",\"fill\":\"paint\"}\n"));
883 }
884 
885 fn parseForTest(allocator: std.mem.Allocator, jsonl: []const u8) !Document {
886     var scratch: [16 * 1024]u8 = undefined;
887     return Document.parse(allocator, &scratch, jsonl);
888 }