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 }