///|
/// Explicit bounds for structured-data layout and painting.
pub struct Limits {
nodes : Int32
depth : Int32
work : Int32
/// The widest value column before text wraps, in SVG units.
width : Int32
output_units : Int32
minimum_approach : Double
} derive(Eq, Debug)
///|
pub extend Limits with Eq::{equal, not_equal}
///|
pub extend Limits with @debug.Debug::{to_repr}
///|
pub fn Limits::new(
nodes~ : Int32 = 10000,
depth~ : Int32 = 256,
work~ : Int32 = 1000000,
width~ : Int32 = 360,
output_units~ : Int32 = 16777216,
minimum_approach~ : Double = 24.0,
) -> Limits {
{ minimum_approach, nodes, depth, work, width, output_units, }
}
///|
pub(all) struct Point {
x : Double
y : Double
} derive(Eq, Debug)
///|
pub extend Point with Eq::{equal, not_equal}
///|
pub extend Point with @debug.Debug::{to_repr}
///|
pub(all) struct Rect {
x : Double
y : Double
width : Double
height : Double
} derive(Eq, Debug)
///|
pub extend Rect with Eq::{equal, not_equal}
///|
pub extend Rect with @debug.Debug::{to_repr}
///|
pub fn Rect::contains(
self : Rect,
other : Rect,
tolerance~ : Double = 0.0,
) -> Bool {
other.x >= self.x - tolerance &&
other.y >= self.y - tolerance &&
other.x + other.width <= self.x + self.width + tolerance &&
other.y + other.height <= self.y + self.height + tolerance
}
///|
pub fn Rect::overlaps(self : Rect, other : Rect) -> Bool {
self.x < other.x + other.width &&
other.x < self.x + self.width &&
self.y < other.y + other.height &&
other.y < self.y + self.height
}
///|
pub fn Rect::right(self : Rect) -> Double {
self.x + self.width
}
///|
pub fn Rect::bottom(self : Rect) -> Double {
self.y + self.height
}
///|
/// The kind of a value, as written in the `data-type` attribute.
pub(all) enum Kind {
Object
Array
String
Number
Boolean
Null
} derive(Eq, Debug)
///|
pub extend Kind with Eq::{equal, not_equal}
///|
pub extend Kind with @debug.Debug::{to_repr}
///|
pub fn Kind::name(self : Kind) -> String {
match self {
Object => "object"
Array => "array"
String => "string"
Number => "number"
Boolean => "boolean"
Null => "null"
}
}
///|
pub fn kind_of(value : @model.Value) -> Kind {
match value {
Object(_) => Object
Array(_) => Array
Text(_) => String
Number(_) => Number
Boolean(_) => Boolean
Null => Null
}
}
///|
/// The ink of a label. Each scalar kind has its own ink.
pub(all) enum Ink {
Key
Index
Text
Number
Boolean
Null
/// The summary of a nested container and the mark of an empty one.
Container
Title
} derive(Eq, Debug)
///|
pub extend Ink with Eq::{equal, not_equal}
///|
pub extend Ink with @debug.Debug::{to_repr}
///|
/// The theme token of the ink: `text`, `muted` or a family ink.
pub fn Ink::token(self : Ink) -> String {
match self {
Key | Title => "text"
Index | Null | Container => "muted"
Text => "backend-ink"
Number => "frontend-ink"
Boolean => "database-ink"
}
}
///|
pub fn Ink::color(self : Ink, palette : @theme.Palette) -> @theme.Color {
match self {
Key | Title => palette.text
Index | Null | Container => palette.muted
Text => palette.backend.ink
Number => palette.frontend.ink
Boolean => palette.database.ink
}
}
///|
/// The ink of a scalar or container value.
pub fn ink_of(kind : Kind) -> Ink {
match kind {
Object | Array => Container
String => Text
Number => Number
Boolean => Boolean
Null => Null
}
}
///|
/// The visible text of a value. Text and number lexemes are exact. Booleans
/// and null have a mark, so they do not look like text with the same letters.
/// A row of a nested container shows `{…}` or `[…]`, or `{}` or `[]` when the
/// container is empty.
pub fn display_text(value : @model.Value) -> String {
match value {
Object(members) => if members.is_empty() { "{}" } else { "{…}" }
Array(members) => if members.is_empty() { "[]" } else { "[…]" }
Text(text) => text
Number(lexeme) => lexeme
Boolean(true) => "☑ true"
Boolean(false) => "☐ false"
Null => "␀ null"
}
}
///|
/// The background of a highlighted row: the cloud family fill over the
/// box fill.
pub fn highlight_background(palette : @theme.Palette) -> @theme.Color {
@theme.over(palette.cloud.fill, palette.mask)
}
///|
/// The palette for data views. Each ink that a data label uses gets a shade
/// with 4.5:1 contrast on the box fill and on the highlight background.
pub fn palette(p : @theme.Palette) -> @theme.Palette {
let backgrounds = [p.mask, highlight_background(p), p.bg]
fn shade(c : @theme.Color) -> @theme.Color {
@theme.text_shade(c, backgrounds)
}
fn family(f : @theme.FamilyColors) -> @theme.FamilyColors {
{ ..f, ink: shade(f.ink), }
}
{
..p,
text: shade(p.text),
muted: shade(p.muted),
frontend: family(p.frontend),
backend: family(p.backend),
database: family(p.database),
}
}
///|
/// One measured label. `lines` are the wrapped lines; with the line breaks
/// that wrapping replaced, they give the complete text.
pub struct Label {
lines : ReadOnlyArray[String]
box : Rect
size : Double
weight : Int32
ink : Ink
} derive(Eq, Debug)
///|
pub extend Label with Eq::{equal, not_equal}
///|
pub extend Label with @debug.Debug::{to_repr}
///|
/// One member row of a container box.
pub struct Row {
/// The member identity.
entry : String
/// The value node of the member.
node : String
kind : Kind
box : Rect
/// The member name, or the muted position `[i]` in an array.
key : Label?
/// The scalar text, or the summary of a nested container.
value : Label?
highlighted : Bool
/// The start of the connector to a nested container.
port : Point?
} derive(Eq, Debug)
///|
pub extend Row with Eq::{equal, not_equal}
///|
pub extend Row with @debug.Debug::{to_repr}
///|
/// One box: a container with its rows, or a scalar root.
pub struct Box {
node : String
kind : Kind
box : Rect
/// The x position of the line between the key and value columns.
divider : Double?
rows : ReadOnlyArray[Row]
/// The value of a scalar root, or the mark of an empty container.
label : Label?
highlighted : Bool
depth : Int32
} derive(Eq, Debug)
///|
pub extend Box with Eq::{equal, not_equal}
///|
pub extend Box with @debug.Debug::{to_repr}
///|
/// A drawn connector from a row to the box of its nested container. The
/// points are the start, the end of the straight part, two curve controls
/// and the end.
pub struct Connector {
/// The member identity of the row that owns the connector.
entry : String
source : String
target : String
points : ReadOnlyArray[Point]
} derive(Eq, Debug)
///|
pub extend Connector with Eq::{equal, not_equal}
///|
pub extend Connector with @debug.Debug::{to_repr}
///|
/// The SVG path data of the connector.
pub fn Connector::path(self : Connector) -> String {
let n = svg_number
let p = self.points
guard p.length() == 5 else {
let out = StringBuilder()
for i, point in p {
out.write_string(if i == 0 { "M" } else { " L" })
out.write_string("\{n(point.x)} \{n(point.y)}")
}
return out.to_string()
}
let head = "M\{n(p[0].x)} \{n(p[0].y)}"
let straight = if p[1].x != p[0].x { " H\{n(p[1].x)}" } else { "" }
head +
straight +
" C\{n(p[2].x)} \{n(p[2].y)} \{n(p[3].x)} \{n(p[3].y)} \{n(p[4].x)} \{n(p[4].y)}"
}
///|
/// Sample points along the connector, for checks.
pub fn Connector::samples(
self : Connector,
count~ : Int32 = 16,
) -> Array[Point] {
let p = self.points
guard p.length() == 5 else { return p.iter().to_array() }
let out = [p[0], p[1]]
for i in 1..<=count {
let t = i.to_double() / count.to_double()
let s = 1.0 - t
let a = s * s * s
let b = 3.0 * s * s * t
let c = 3.0 * s * t * t
let d = t * t * t
out.push({
x: a * p[1].x + b * p[2].x + c * p[3].x + d * p[4].x,
y: a * p[1].y + b * p[2].y + c * p[3].y + d * p[4].y,
})
}
out
}
///|
/// The measured geometry of one data view.
pub struct Geometry {
tree : @model.DataTree
width : Int32
height : Int32
title : Label?
boxes : ReadOnlyArray[Box]
connectors : ReadOnlyArray[Connector]
} derive(Eq, Debug)
///|
pub extend Geometry with Eq::{equal, not_equal}
///|
pub extend Geometry with @debug.Debug::{to_repr}
///|
pub struct Result {
geometry : Geometry?
diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
}
///|
/// Canonical coordinates have at most three fractional digits and never use
/// exponent notation.
pub fn svg_number(value : Double) -> String {
let scaled = (value * 1000.0).round().to_int64()
if scaled == 0L {
return "0"
}
let negative = scaled < 0L
let magnitude = if negative { -scaled } else { scaled }
let whole = magnitude / 1000L
let fraction = (magnitude % 1000L).to_int()
let prefix = if negative { "-" } else { "" }
if fraction == 0 {
return "\{prefix}\{whole}"
}
let digits = "\{if fraction < 100 { "0" } else { "" }}\{if fraction < 10 { "0" } else { "" }}\{fraction}"
"\{prefix}\{whole}." + digits.trim_end(chars="0").to_owned()
}
///|
pub fn Limits::with_minimum_approach(
self : Limits,
minimum_approach : Double,
) -> Limits {
{ ..self, minimum_approach, }
}