tiny.zen.diagram.spec
Defined in diagram.
API (45)
Actions
Public operations.
Bar.deinitBox.deinitChannelMark.deinitDataRow.deinitDataRow.lookupDataValue.deinitEdge.deinitField.deinitFrame.deinitLineIterator.initLineIterator.nextMark.deinitPoint.deinitRule.deinitScale.deinitScales.deinitStack.deinitText.deinitTransform.deinitXValue.deinitrecordKindrecordKindOf
Types and contracts
Public types and contracts.
AxisBarBoxChannelMarkChannelMarkKindDataRowDataValueDocumentEdgeFieldFrameLineIteratorMarkPointRecordKindRuleScaleScaleKindScalesStackTextTransformXValue
Source
Source: lib/zen/src/diagram/root.zig:11
zig
pub const spec = @import("spec.zig");Source: lib/zen/src/diagram/spec.zig
zig
const std = @import("std");pub const XValue = union(enum) { number: f64, text: []u8, pub fn deinit(self: *XValue, allocator: std.mem.Allocator) void { switch (self.*) { .number => {}, .text => |text| freeNonEmpty(allocator, text), } }};pub const Axis = enum { x, y,};pub const ScaleKind = enum { linear, log,};pub const Scale = struct { kind: ScaleKind = .linear, min: ?f64 = null, max: ?f64 = null, base: f64 = 10, label: []u8 = &.{}, pub fn deinit(self: *Scale, allocator: std.mem.Allocator) void { freeNonEmpty(allocator, self.label); }};pub const Scales = struct { x: Scale = .{}, y: Scale = .{}, pub fn deinit(self: *Scales, allocator: std.mem.Allocator) void { self.x.deinit(allocator); self.y.deinit(allocator); }};pub const Frame = struct { width: f64 = 640, height: f64 = 360, inset: f64 = 44, axes: bool = true, title: []u8 = &.{}, x_label: []u8 = &.{}, y_label: []u8 = &.{}, background: []u8 = &.{}, y_min: ?f64 = null, y_max: ?f64 = null, x_min: ?f64 = null, x_max: ?f64 = null, pub fn deinit(self: *Frame, allocator: std.mem.Allocator) void { freeNonEmpty(allocator, self.title); freeNonEmpty(allocator, self.x_label); freeNonEmpty(allocator, self.y_label); freeNonEmpty(allocator, self.background); }};pub const DataValue = union(enum) { number: f64, text: []u8, boolean: bool, pub fn deinit(self: *DataValue, allocator: std.mem.Allocator) void { switch (self.*) { .number => {}, .text => |text| freeNonEmpty(allocator, text), .boolean => {}, } }};pub const Field = struct { name: []u8, value: DataValue, pub fn deinit(self: *Field, allocator: std.mem.Allocator) void { freeNonEmpty(allocator, self.name); self.value.deinit(allocator); }};pub const DataRow = struct { name: []u8, fields: std.ArrayList(Field) = .empty, pub fn deinit(self: *DataRow, allocator: std.mem.Allocator) void { freeNonEmpty(allocator, self.name); for (self.fields.items) |*field| field.deinit(allocator); self.fields.deinit(allocator); } pub fn lookup(self: *const DataRow, name: []const u8) ?DataValue { for (self.fields.items) |item| { if (std.mem.eql(u8, item.name, name)) return item.value; } return null; }};pub const Bar = struct { x: []u8, y: f64, series: []u8 = &.{}, label: []u8 = &.{}, fill: []u8 = &.{}, pub fn deinit(self: *Bar, allocator: std.mem.Allocator) void { freeNonEmpty(allocator, self.x); freeNonEmpty(allocator, self.series); freeNonEmpty(allocator, self.label); freeNonEmpty(allocator, self.fill); }};pub const Point = struct { x: XValue, y: f64, label: []u8 = &.{}, fill: []u8 = &.{}, radius: f64 = 4, pub fn deinit(self: *Point, allocator: std.mem.Allocator) void { self.x.deinit(allocator); freeNonEmpty(allocator, self.label); freeNonEmpty(allocator, self.fill); }};pub const Rule = struct { x1: f64, y1: f64, x2: f64, y2: f64, stroke: []u8 = &.{}, pub fn deinit(self: *Rule, allocator: std.mem.Allocator) void { freeNonEmpty(allocator, self.stroke); }};pub const Text = struct { x: XValue, y: f64, text: []u8, fill: []u8 = &.{}, pub fn deinit(self: *Text, allocator: std.mem.Allocator) void { self.x.deinit(allocator); freeNonEmpty(allocator, self.text); freeNonEmpty(allocator, self.fill); }};pub const Box = struct { x: f64, y: f64, width: f64, height: f64, text: []u8 = &.{}, fill: []u8 = &.{}, stroke: []u8 = &.{}, pub fn deinit(self: *Box, allocator: std.mem.Allocator) void { freeNonEmpty(allocator, self.text); freeNonEmpty(allocator, self.fill); freeNonEmpty(allocator, self.stroke); }};pub const Edge = struct { x1: f64, y1: f64, x2: f64, y2: f64, label: []u8 = &.{}, stroke: []u8 = &.{}, arrow: bool = true, pub fn deinit(self: *Edge, allocator: std.mem.Allocator) void { freeNonEmpty(allocator, self.label); freeNonEmpty(allocator, self.stroke); }};pub const Stack = struct { mark: []u8 = &.{}, pub fn deinit(self: *Stack, allocator: std.mem.Allocator) void { freeNonEmpty(allocator, self.mark); }};pub const Transform = union(enum) { stack: Stack, pub fn deinit(self: *Transform, allocator: std.mem.Allocator) void { switch (self.*) { .stack => |*transform| transform.deinit(allocator), } }};pub const ChannelMarkKind = enum { bar, point, text,};pub const ChannelMark = struct { kind: ChannelMarkKind, data: []u8, x: []u8, y: []u8, label: []u8 = &.{}, fill: []u8 = &.{}, series: []u8 = &.{}, text: []u8 = &.{}, radius: []u8 = &.{}, pub fn deinit(self: *ChannelMark, allocator: std.mem.Allocator) void { freeNonEmpty(allocator, self.data); freeNonEmpty(allocator, self.x); freeNonEmpty(allocator, self.y); freeNonEmpty(allocator, self.label); freeNonEmpty(allocator, self.fill); freeNonEmpty(allocator, self.series); freeNonEmpty(allocator, self.text); freeNonEmpty(allocator, self.radius); }};pub const Mark = union(enum) { bar: Bar, point: Point, rule: Rule, text: Text, box: Box, edge: Edge, pub fn deinit(self: *Mark, allocator: std.mem.Allocator) void { switch (self.*) { .bar => |*mark| mark.deinit(allocator), .point => |*mark| mark.deinit(allocator), .rule => |*mark| mark.deinit(allocator), .text => |*mark| mark.deinit(allocator), .box => |*mark| mark.deinit(allocator), .edge => |*mark| mark.deinit(allocator), } }};pub const RecordKind = enum { frame, scale, transform, data, mark, bar, point, rule, text, box, edge,};pub fn recordKind(name: []const u8) ?RecordKind { inline for (@typeInfo(RecordKind).@"enum".field_names, std.meta.tags(RecordKind)) |field_name, value| { if (std.mem.eql(u8, name, field_name)) return value; } return null;}pub fn recordKindOf(value: std.json.Value) !RecordKind { const object = switch (value) { .object => |object| object, else => return error.InvalidRecord, }; const kind = objectString(object, "kind") orelse return error.MissingKind; return recordKind(kind) orelse return error.UnknownRecordKind;}pub const LineIterator = struct { lines: std.mem.SplitIterator(u8, .scalar), pub fn init(jsonl: []const u8) LineIterator { return .{ .lines = std.mem.splitScalar(u8, jsonl, '\n') }; } pub fn next(self: *LineIterator) ?[]const u8 { while (self.lines.next()) |raw_line| { const line = std.mem.trim(u8, raw_line, " \t\r\n"); if (line.len == 0) continue; return line; } return null; }};pub const Document = struct { allocator: std.mem.Allocator, frame: Frame = .{}, scales: Scales = .{}, transforms: std.ArrayList(Transform) = .empty, data: std.ArrayList(DataRow) = .empty, marks: std.ArrayList(Mark) = .empty, scratch_high_water: usize = 0, pub fn parse(allocator: std.mem.Allocator, line_scratch: []u8, jsonl: []const u8) !Document { var document = Document{ .allocator = allocator }; errdefer document.deinit(); var scratch = std.heap.FixedBufferAllocator.init(line_scratch); var lines = LineIterator.init(jsonl); while (lines.next()) |line| { scratch.reset(); var parsed = std.json.parseFromSlice(std.json.Value, scratch.allocator(), line, .{}) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return error.InvalidJsonl, }; defer parsed.deinit(); try document.applyRecord(parsed.value); document.scratch_high_water = @max(document.scratch_high_water, scratch.end_index); } try validateDocument(&document); return document; } pub fn deinit(self: *Document) void { self.frame.deinit(self.allocator); self.scales.deinit(self.allocator); for (self.transforms.items) |*transform| transform.deinit(self.allocator); self.transforms.deinit(self.allocator); for (self.data.items) |*row| row.deinit(self.allocator); self.data.deinit(self.allocator); for (self.marks.items) |*mark| mark.deinit(self.allocator); self.marks.deinit(self.allocator); } fn applyRecord(self: *Document, value: std.json.Value) !void { const object = switch (value) { .object => |object| object, else => return error.InvalidRecord, }; switch (try recordKindOf(value)) { .frame => try self.applyFrame(object), .scale => try self.applyScale(object), .transform => try self.applyTransform(object), .data => try self.applyData(object), .mark => try self.applyChannelMark(object), .bar => try self.marks.append(self.allocator, .{ .bar = try parseBar(self.allocator, object) }), .point => try self.marks.append(self.allocator, .{ .point = try parsePoint(self.allocator, object) }), .rule => try self.marks.append(self.allocator, .{ .rule = try parseRule(self.allocator, object) }), .text => try self.marks.append(self.allocator, .{ .text = try parseText(self.allocator, object) }), .box => try self.marks.append(self.allocator, .{ .box = try parseBox(self.allocator, object) }), .edge => try self.marks.append(self.allocator, .{ .edge = try parseEdge(self.allocator, object) }), } } fn applyFrame(self: *Document, object: std.json.ObjectMap) !void { if (objectNumber(object, "width")) |value| self.frame.width = value; if (objectNumber(object, "height")) |value| self.frame.height = value; if (objectNumber(object, "inset")) |value| self.frame.inset = value; if (objectBool(object, "axes")) |value| self.frame.axes = value; if (objectNumber(object, "y_min")) |value| self.frame.y_min = value; if (objectNumber(object, "y_max")) |value| self.frame.y_max = value; if (objectNumber(object, "x_min")) |value| self.frame.x_min = value; if (objectNumber(object, "x_max")) |value| self.frame.x_max = value; try replaceString(self.allocator, &self.frame.title, objectString(object, "title")); try replaceString(self.allocator, &self.frame.x_label, objectString(object, "x_label")); try replaceString(self.allocator, &self.frame.y_label, objectString(object, "y_label")); if (objectString(object, "background")) |background| { if (!validPaint(background)) return error.InvalidColor; try replaceString(self.allocator, &self.frame.background, background); } } fn applyScale(self: *Document, object: std.json.ObjectMap) !void { const axis = try parseAxis(object); const scale = switch (axis) { .x => &self.scales.x, .y => &self.scales.y, }; if (objectString(object, "type")) |value| scale.kind = try parseScaleKind(value); if (objectNumber(object, "min")) |value| scale.min = value; if (objectNumber(object, "max")) |value| scale.max = value; if (objectNumber(object, "base")) |value| scale.base = value; try replaceString(self.allocator, &scale.label, objectString(object, "label")); } fn applyTransform(self: *Document, object: std.json.ObjectMap) !void { var transform = try parseTransform(self.allocator, object); errdefer transform.deinit(self.allocator); try self.transforms.append(self.allocator, transform); } fn applyData(self: *Document, object: std.json.ObjectMap) !void { var row = try parseDataRow(self.allocator, object); errdefer row.deinit(self.allocator); try self.data.append(self.allocator, row); } fn applyChannelMark(self: *Document, object: std.json.ObjectMap) !void { var channel = try parseChannelMark(self.allocator, object); defer channel.deinit(self.allocator); var matched = false; for (self.data.items) |*row| { if (!std.mem.eql(u8, row.name, channel.data)) continue; matched = true; var mark = try markFromChannel(self.allocator, channel, row); var appended = false; errdefer if (!appended) mark.deinit(self.allocator); try self.marks.append(self.allocator, mark); appended = true; } if (!matched) return error.UnknownData; }};fn parseDataRow(allocator: std.mem.Allocator, object: std.json.ObjectMap) !DataRow { var row = DataRow{ .name = try dupeDatasetName(allocator, object) }; errdefer row.deinit(allocator); var iter = object.iterator(); while (iter.next()) |entry| { const key = entry.key_ptr.*; if (dataMetaKey(key)) continue; const name = try allocator.dupe(u8, key); var name_owned = true; errdefer if (name_owned) allocator.free(name); var value = try parseDataValue(allocator, entry.value_ptr.*); var value_owned = true; errdefer if (value_owned) value.deinit(allocator); try row.fields.append(allocator, .{ .name = name, .value = value, }); name_owned = false; value_owned = false; } if (row.fields.items.len == 0) return error.InvalidData; return row;}fn parseDataValue(allocator: std.mem.Allocator, value: std.json.Value) !DataValue { return switch (value) { .string => |string| .{ .text = try allocator.dupe(u8, string) }, .integer => |integer| .{ .number = @floatFromInt(integer) }, .float => |float| .{ .number = float }, .number_string => |string| .{ .number = try std.fmt.parseFloat(f64, string) }, .bool => |boolean| .{ .boolean = boolean }, else => error.InvalidField, };}fn parseChannelMark(allocator: std.mem.Allocator, object: std.json.ObjectMap) !ChannelMark { var channel = ChannelMark{ .kind = try parseChannelMarkKind(objectString(object, "type") orelse return error.MissingField), .data = &.{}, .x = &.{}, .y = &.{}, }; errdefer channel.deinit(allocator); channel.data = try dupeRequiredString(allocator, object, "data"); channel.x = try dupeRequiredString(allocator, object, "x"); channel.y = try dupeRequiredString(allocator, object, "y"); channel.label = try dupeOptionalString(allocator, object, "label"); channel.fill = try dupeOptionalString(allocator, object, "fill"); channel.series = try dupeOptionalString(allocator, object, "series"); channel.text = try dupeOptionalString(allocator, object, "text"); channel.radius = try dupeOptionalString(allocator, object, "radius"); if (channel.kind == .text and channel.text.len == 0) return error.MissingField; return channel;}fn markFromChannel(allocator: std.mem.Allocator, channel: ChannelMark, row: *const DataRow) !Mark { return switch (channel.kind) { .bar => .{ .bar = try barFromChannel(allocator, channel, row) }, .point => .{ .point = try pointFromChannel(allocator, channel, row) }, .text => .{ .text = try textFromChannel(allocator, channel, row) }, };}fn barFromChannel(allocator: std.mem.Allocator, channel: ChannelMark, row: *const DataRow) !Bar { var bar = Bar{ .x = &.{}, .y = 0 }; errdefer bar.deinit(allocator); bar.x = try requiredChannelText(allocator, row, channel.x); bar.y = try requiredChannelNumber(row, channel.y); bar.series = try optionalChannelText(allocator, row, channel.series); bar.label = try optionalChannelText(allocator, row, channel.label); bar.fill = try optionalChannelPaint(allocator, row, channel.fill); return bar;}fn pointFromChannel(allocator: std.mem.Allocator, channel: ChannelMark, row: *const DataRow) !Point { var point = Point{ .x = .{ .number = 0 }, .y = 0 }; errdefer point.deinit(allocator); point.x = try requiredChannelX(allocator, row, channel.x); point.y = try requiredChannelNumber(row, channel.y); point.label = try optionalChannelText(allocator, row, channel.label); point.fill = try optionalChannelPaint(allocator, row, channel.fill); point.radius = if (channel.radius.len == 0) 4 else try requiredChannelNumber(row, channel.radius); return point;}fn textFromChannel(allocator: std.mem.Allocator, channel: ChannelMark, row: *const DataRow) !Text { var text = Text{ .x = .{ .number = 0 }, .y = 0, .text = &.{} }; errdefer text.deinit(allocator); text.x = try requiredChannelX(allocator, row, channel.x); text.y = try requiredChannelNumber(row, channel.y); text.text = try requiredChannelText(allocator, row, channel.text); text.fill = try optionalChannelPaint(allocator, row, channel.fill); return text;}fn parseBar(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Bar { var bar = Bar{ .x = &.{}, .y = 0 }; errdefer bar.deinit(allocator); bar.x = try dupeRequiredString(allocator, object, "x"); bar.y = objectNumber(object, "y") orelse return error.MissingField; bar.series = try dupeOptionalString(allocator, object, "series"); bar.label = try dupeOptionalString(allocator, object, "label"); bar.fill = try dupePaint(allocator, object, "fill"); return bar;}fn parsePoint(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Point { var point = Point{ .x = .{ .number = 0 }, .y = 0 }; errdefer point.deinit(allocator); point.x = try parseX(allocator, object, "x"); point.y = objectNumber(object, "y") orelse return error.MissingField; point.label = try dupeOptionalString(allocator, object, "label"); point.fill = try dupePaint(allocator, object, "fill"); point.radius = objectNumber(object, "radius") orelse 4; return point;}fn parseRule(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Rule { var rule = Rule{ .x1 = 0, .y1 = 0, .x2 = 0, .y2 = 0 }; errdefer rule.deinit(allocator); rule.x1 = objectNumber(object, "x1") orelse return error.MissingField; rule.y1 = objectNumber(object, "y1") orelse return error.MissingField; rule.x2 = objectNumber(object, "x2") orelse return error.MissingField; rule.y2 = objectNumber(object, "y2") orelse return error.MissingField; rule.stroke = try dupePaint(allocator, object, "stroke"); return rule;}fn parseText(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Text { var text = Text{ .x = .{ .number = 0 }, .y = 0, .text = &.{} }; errdefer text.deinit(allocator); text.x = try parseX(allocator, object, "x"); text.y = objectNumber(object, "y") orelse return error.MissingField; text.text = try dupeRequiredString(allocator, object, "text"); text.fill = try dupePaint(allocator, object, "fill"); return text;}fn parseBox(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Box { var box = Box{ .x = 0, .y = 0, .width = 0, .height = 0 }; errdefer box.deinit(allocator); box.x = objectNumber(object, "x") orelse return error.MissingField; box.y = objectNumber(object, "y") orelse return error.MissingField; box.width = objectNumber(object, "width") orelse return error.MissingField; box.height = objectNumber(object, "height") orelse return error.MissingField; if (box.width <= 0 or box.height <= 0) return error.InvalidField; box.text = try dupeOptionalString(allocator, object, "text"); box.fill = try dupePaint(allocator, object, "fill"); box.stroke = try dupePaint(allocator, object, "stroke"); return box;}fn parseEdge(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Edge { var edge = Edge{ .x1 = 0, .y1 = 0, .x2 = 0, .y2 = 0 }; errdefer edge.deinit(allocator); edge.x1 = objectNumber(object, "x1") orelse return error.MissingField; edge.y1 = objectNumber(object, "y1") orelse return error.MissingField; edge.x2 = objectNumber(object, "x2") orelse return error.MissingField; edge.y2 = objectNumber(object, "y2") orelse return error.MissingField; edge.label = try dupeOptionalString(allocator, object, "label"); edge.stroke = try dupePaint(allocator, object, "stroke"); edge.arrow = objectBool(object, "arrow") orelse true; return edge;}fn parseTransform(allocator: std.mem.Allocator, object: std.json.ObjectMap) !Transform { const op = objectString(object, "op") orelse return error.MissingField; if (std.mem.eql(u8, op, "stack")) { var stack = Stack{ .mark = &.{} }; errdefer stack.deinit(allocator); stack.mark = try dupeOptionalString(allocator, object, "mark"); if (stack.mark.len != 0 and !std.mem.eql(u8, stack.mark, "bar")) return error.InvalidTransform; return .{ .stack = stack }; } return error.UnknownTransform;}fn parseAxis(object: std.json.ObjectMap) !Axis { const axis = objectString(object, "axis") orelse return error.MissingField; if (std.mem.eql(u8, axis, "x")) return .x; if (std.mem.eql(u8, axis, "y")) return .y; return error.InvalidScale;}fn parseScaleKind(value: []const u8) !ScaleKind { if (std.mem.eql(u8, value, "linear")) return .linear; if (std.mem.eql(u8, value, "log")) return .log; return error.InvalidScale;}fn parseChannelMarkKind(value: []const u8) !ChannelMarkKind { if (std.mem.eql(u8, value, "bar")) return .bar; if (std.mem.eql(u8, value, "point")) return .point; if (std.mem.eql(u8, value, "text")) return .text; return error.UnknownMark;}fn parseX(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8) !XValue { const value = object.get(key) orelse return error.MissingField; return switch (value) { .string => |string| .{ .text = try allocator.dupe(u8, string) }, .integer => |integer| .{ .number = @floatFromInt(integer) }, .float => |float| .{ .number = float }, .number_string => |string| .{ .number = try std.fmt.parseFloat(f64, string) }, else => error.InvalidField, };}fn requiredChannelValue(row: *const DataRow, field: []const u8) !DataValue { if (field.len == 0) return error.MissingField; return row.lookup(field) orelse error.MissingField;}fn requiredChannelNumber(row: *const DataRow, field: []const u8) !f64 { return try dataValueNumber(try requiredChannelValue(row, field));}fn requiredChannelText(allocator: std.mem.Allocator, row: *const DataRow, field: []const u8) ![]u8 { return try dataValueText(allocator, try requiredChannelValue(row, field));}fn requiredChannelX(allocator: std.mem.Allocator, row: *const DataRow, field: []const u8) !XValue { return try dataValueX(allocator, try requiredChannelValue(row, field));}fn optionalChannelText(allocator: std.mem.Allocator, row: *const DataRow, field: []const u8) ![]u8 { if (field.len == 0) return try allocator.dupe(u8, ""); return try requiredChannelText(allocator, row, field);}fn optionalChannelPaint(allocator: std.mem.Allocator, row: *const DataRow, field: []const u8) ![]u8 { const value = try optionalChannelText(allocator, row, field); errdefer allocator.free(value); if (!validPaint(value)) return error.InvalidColor; return value;}fn dataValueNumber(value: DataValue) !f64 { return switch (value) { .number => |number| number, .text => |text| std.fmt.parseFloat(f64, text) catch error.InvalidField, .boolean => error.InvalidField, };}fn dataValueText(allocator: std.mem.Allocator, value: DataValue) ![]u8 { return switch (value) { .number => |number| try std.fmt.allocPrint(allocator, "{d}", .{number}), .text => |text| try allocator.dupe(u8, text), .boolean => |boolean| try allocator.dupe(u8, if (boolean) "true" else "false"), };}fn dataValueX(allocator: std.mem.Allocator, value: DataValue) !XValue { return switch (value) { .number => |number| .{ .number = number }, .text => |text| .{ .text = try allocator.dupe(u8, text) }, .boolean => |boolean| .{ .text = try allocator.dupe(u8, if (boolean) "true" else "false") }, };}fn objectBool(object: std.json.ObjectMap, key: []const u8) ?bool { const value = object.get(key) orelse return null; return switch (value) { .bool => |boolean| boolean, else => null, };}fn objectString(object: std.json.ObjectMap, key: []const u8) ?[]const u8 { const value = object.get(key) orelse return null; return switch (value) { .string => |string| string, else => null, };}fn objectNumber(object: std.json.ObjectMap, key: []const u8) ?f64 { const value = object.get(key) orelse return null; return switch (value) { .integer => |integer| @floatFromInt(integer), .float => |float| float, .number_string => |string| std.fmt.parseFloat(f64, string) catch null, else => null, };}fn dupeDatasetName(allocator: std.mem.Allocator, object: std.json.ObjectMap) ![]u8 { if (objectString(object, "name")) |name| return try allocator.dupe(u8, name); if (objectString(object, "id")) |id| return try allocator.dupe(u8, id); return error.MissingField;}fn dupeRequiredString(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8) ![]u8 { const value = objectString(object, key) orelse return error.MissingField; return try allocator.dupe(u8, value);}fn dupeOptionalString(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8) ![]u8 { const value = objectString(object, key) orelse return try allocator.dupe(u8, ""); return try allocator.dupe(u8, value);}fn dupePaint(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8) ![]u8 { const value = objectString(object, key) orelse return try allocator.dupe(u8, ""); if (!validPaint(value)) return error.InvalidColor; return try allocator.dupe(u8, value);}fn replaceString(allocator: std.mem.Allocator, target: *[]u8, value: ?[]const u8) !void { const source = value orelse return; freeNonEmpty(allocator, target.*); target.* = try allocator.dupe(u8, source);}fn validateDocument(document: *const Document) !void { try validateFrame(document.frame); try validateScale(document.scales.x); try validateScale(document.scales.y);}fn validateFrame(frame: Frame) !void { if (frame.width < 160 or frame.width > 4000) return error.InvalidFrame; if (frame.height < 120 or frame.height > 3000) return error.InvalidFrame; if (frame.inset < 8 or frame.inset > 400) return error.InvalidFrame; if (frame.y_min != null and frame.y_max != null and frame.y_min.? >= frame.y_max.?) return error.InvalidFrame; if (frame.x_min != null and frame.x_max != null and frame.x_min.? >= frame.x_max.?) return error.InvalidFrame;}fn validateScale(scale: Scale) !void { if (scale.base <= 1) return error.InvalidScale; if (scale.min != null and scale.max != null and scale.min.? >= scale.max.?) return error.InvalidScale; if (scale.kind == .log) { if (scale.min != null and scale.min.? <= 0) return error.InvalidScale; if (scale.max != null and scale.max.? <= 0) return error.InvalidScale; }}fn validPaint(value: []const u8) bool { if (value.len == 0) return true; if (value.len != 4 and value.len != 7) return false; if (value[0] != '#') return false; for (value[1..]) |byte| { if (!std.ascii.isHex(byte)) return false; } return true;}fn dataMetaKey(key: []const u8) bool { return std.mem.eql(u8, key, "kind") or std.mem.eql(u8, key, "name") or std.mem.eql(u8, key, "id");}fn freeNonEmpty(allocator: std.mem.Allocator, bytes: []u8) void { if (bytes.len > 0) allocator.free(bytes);}test "diagram spec parses frame and bars" { const allocator = std.testing.allocator; const input = \\{"kind":"frame","width":500,"height":280,"title":"Coin","x_label":"outcome","y_label":"p","y_min":0,"y_max":1} \\{"kind":"bar","x":"heads","y":0.6,"fill":"#245f8d"} \\{"kind":"bar","x":"tails","y":0.4,"fill":"#8f3a32"} \\ ; var document = try parseForTest(allocator, input); defer document.deinit(); try std.testing.expectEqual(@as(usize, 2), document.marks.items.len); try std.testing.expectEqualStrings("Coin", document.frame.title); try std.testing.expectEqualStrings("heads", document.marks.items[0].bar.x);}test "diagram spec parses scales transforms and series" { const allocator = std.testing.allocator; const input = \\{"kind":"frame","width":500,"height":280,"title":"Counts"} \\{"kind":"scale","axis":"y","type":"log","min":1,"max":1000,"label":"events","base":10} \\{"kind":"transform","op":"stack","mark":"bar"} \\{"kind":"bar","x":"a","y":10,"series":"first","fill":"#245f8d"} \\{"kind":"bar","x":"a","y":40,"series":"second","fill":"#8f3a32"} \\ ; var document = try parseForTest(allocator, input); defer document.deinit(); try std.testing.expectEqual(ScaleKind.log, document.scales.y.kind); try std.testing.expectEqual(@as(f64, 1), document.scales.y.min.?); try std.testing.expectEqual(@as(f64, 1000), document.scales.y.max.?); try std.testing.expectEqualStrings("events", document.scales.y.label); try std.testing.expectEqual(@as(usize, 1), document.transforms.items.len); try std.testing.expectEqualStrings("bar", document.transforms.items[0].stack.mark); try std.testing.expectEqualStrings("second", document.marks.items[1].bar.series);}test "diagram spec expands data marks" { const allocator = std.testing.allocator; const input = \\{"kind":"frame","width":500,"height":280,"title":"Coin","y_min":0,"y_max":1} \\{"kind":"data","name":"coin","outcome":"heads","probability":0.62,"paint":"#245f8d","label":"heads"} \\{"kind":"data","name":"coin","outcome":"tails","probability":0.38,"paint":"#8f3a32","label":"tails"} \\{"kind":"mark","type":"bar","data":"coin","x":"outcome","y":"probability","fill":"paint","label":"label"} \\ ; var document = try parseForTest(allocator, input); defer document.deinit(); try std.testing.expectEqual(@as(usize, 2), document.data.items.len); try std.testing.expectEqual(@as(usize, 2), document.marks.items.len); try std.testing.expectEqualStrings("heads", document.marks.items[0].bar.x); try std.testing.expectEqual(@as(f64, 0.62), document.marks.items[0].bar.y); try std.testing.expectEqualStrings("#8f3a32", document.marks.items[1].bar.fill);}test "diagram spec expands point and text channels" { const allocator = std.testing.allocator; const input = \\{"kind":"frame","width":500,"height":280,"title":"Latency"} \\{"kind":"data","name":"latency","events":10,"micros":120,"label":"p50","paint":"#245f8d","size":5} \\{"kind":"mark","type":"point","data":"latency","x":"events","y":"micros","label":"label","fill":"paint","radius":"size"} \\{"kind":"mark","type":"text","data":"latency","x":"events","y":"micros","text":"label","fill":"paint"} \\ ; var document = try parseForTest(allocator, input); defer document.deinit(); try std.testing.expectEqual(@as(usize, 2), document.marks.items.len); try std.testing.expectEqual(@as(f64, 5), document.marks.items[0].point.radius); try std.testing.expectEqualStrings("p50", document.marks.items[0].point.label); try std.testing.expectEqualStrings("p50", document.marks.items[1].text.text);}test "diagram spec parses boxes and edges" { const allocator = std.testing.allocator; const input = \\{"kind":"frame","width":640,"height":360,"title":"Flow","axes":false,"x_min":0,"x_max":10,"y_min":0,"y_max":10} \\{"kind":"edge","x1":3,"y1":7,"x2":7,"y2":7,"label":"message"} \\{"kind":"box","x":3,"y":7,"width":2.4,"height":1.2,"text":"actor <A>","fill":"#eef5f1","stroke":"#245f8d"} \\{"kind":"box","x":7,"y":7,"width":2.4,"height":1.2,"text":"actor B"} \\ ; var document = try parseForTest(allocator, input); defer document.deinit(); try std.testing.expect(!document.frame.axes); try std.testing.expectEqual(@as(usize, 3), document.marks.items.len); try std.testing.expectEqualStrings("actor <A>", document.marks.items[1].box.text); try std.testing.expect(document.marks.items[0].edge.arrow);}test "diagram spec rejects unknown marks and unsafe colors" { const allocator = std.testing.allocator; try std.testing.expectError(error.UnknownRecordKind, parseForTest(allocator, "{\"kind\":\"arc\"}\n")); try std.testing.expectError(error.InvalidColor, parseForTest(allocator, "{\"kind\":\"bar\",\"x\":\"a\",\"y\":1,\"fill\":\"url(x)\"}\n")); try std.testing.expectError(error.InvalidField, parseForTest(allocator, "{\"kind\":\"box\",\"x\":1,\"y\":1,\"width\":0,\"height\":1}\n")); try std.testing.expectError(error.InvalidScale, parseForTest(allocator, "{\"kind\":\"scale\",\"axis\":\"y\",\"type\":\"log\",\"min\":0}\n")); try std.testing.expectError(error.InvalidTransform, parseForTest(allocator, "{\"kind\":\"transform\",\"op\":\"stack\",\"mark\":\"point\"}\n")); try std.testing.expectError(error.UnknownData, parseForTest(allocator, "{\"kind\":\"mark\",\"type\":\"bar\",\"data\":\"missing\",\"x\":\"x\",\"y\":\"y\"}\n")); 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"));}fn parseForTest(allocator: std.mem.Allocator, jsonl: []const u8) !Document { var scratch: [16 * 1024]u8 = undefined; return Document.parse(allocator, &scratch, jsonl);}Audit
| Definitions | 45 |
|---|---|
| Public names | 82 |
| Members | 97 |
| Version | 26.7.0 |
| Revision | daab053ee433 |