Client-side charting for Libertas protocol data
This is the V1 chart contract for schema authors and compatible clients. Schema support and renderer availability are distinct: a client that cannot display a chart follows the chart failure policy. This reference does not certify feature parity for every distributed build.
Presentation uses the schema, exact dependency versions, and locale document associated with the returned value; unrelated installed versions do not redefine that value. See the schema reference.
1. Purpose
A Libertas protocol endpoint can return data that Android and iOS render as a chart instead of only as a generic data tree. The App task remains responsible for all domain calculations—including arbitrary time transforms—and can return an arbitrarily transformed but strictly typed payload. The client is responsible only for scales, layout, primitive marks, guides, accessibility, and standard chart interaction.
This division removes the largest and riskiest parts of a Vega-Lite grammar from the client. There are no client expressions, transforms, joins, data loaders, or arbitrary field paths. The server returns a complete chart-ready snapshot, and the schema describes how direct fields in that snapshot map to visual channels.
This document's two-dimensional schema protocol uses four language-neutral schema attribute families:
| Schema attribute | Placement | Purpose |
|---|---|---|
Chart |
A chart-data List, primitive carrier Table, or composition Table |
Select a primitive mark or composition. A protocol Union variant may instead wrap one such chart field plus direct Hidden fields. |
ChartChannel |
A scalar row field | Map the field to one or more visual roles. |
ChartScale |
A field with a scale-bearing channel | Override automatic scale behavior or share a scale. |
ChartGuide |
A direct scalar chart-row field | Configure, suppress, or source a generic guide. |
V1 also defines the strictly parallel Chart3D, Chart3DChannel,
Chart3DScale, Chart3DGuide, and Chart3DView namespace in the
3D chart contract. Those five attributes do not
extend or change any Chart* token or behavior. A presented result cannot mix
the two namespaces, so an older client never mistakes a 3D result for a 2D
chart and instead follows the chart failure policy in §10.
These attributes target broad chart-family coverage for server-prepared data. They do not provide Vega-Lite syntax compatibility, pixel compatibility, or complete feature parity. In particular, they do not reproduce Vega's runtime language, signals, arbitrary event expressions, client-side transforms, geographic coordinate/projection/topology semantics, raster/image marks, or a general widget system. A server-projected geographic region is ordinary polygon geometry and carries no client-side geographic meaning.
This document defines the complete two-dimensional Chart* contract. The
TypeScript and Rust parsers extract the four source annotations into
language-neutral schema attributes. Libertas Studio's shared validator
canonicalizes those attributes and owns every chart semantic rule decidable
from the extracted schema and its materialized dependencies. The Hub carries
them as ordinary schema attributes
without a renderer, chart-specific message, or chart-specific persistence
path. Rendering clients implement this V1 contract, including the chart failure
policy in §10. The concise attribute
inventory is in the Libertas schema reference,
with source syntax in the TypeScript application manual
and Rust application manual.
2. Three distinct attribute layers
The word "attribute" refers to three related but different things in Libertas. Chart support must keep them separate.
2.1 Source-language annotations
App authors use the native annotation mechanism of their language. The exact spellings are:
/**
* Temperature history
* Recent temperature measurements over time.
* @LibertasChart(line)
*/
export type TemperatureChart = TemperaturePoint[];
/**
* Temperature sample
* One timestamped temperature measurement.
*/
export interface TemperaturePoint {
/**
* Sample time
* The time at which this measurement was taken.
* @LibertasChartChannel(x, tooltip)
* @LibertasChartScale(id=time, kind=utc)
*/
time: LibertasDateTime;
/**
* Temperature
* The measured temperature.
* @LibertasChartChannel(y, tooltip)
* @LibertasChartScale(id=temperature, kind=linear, zero=false)
*/
temperature: number;
}
/// Temperature history
/// Recent temperature measurements over time.
#[libertas_chart(line)]
pub type TemperatureChart = Vec<TemperaturePoint>;
/// Temperature sample
/// One timestamped temperature measurement.
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct TemperaturePoint {
/// Sample time
/// The time at which this measurement was taken.
#[libertas_chart_channel(x, tooltip)]
#[libertas_chart_scale(id = time, kind = utc)]
pub time: LibertasDateTime,
/// Temperature
/// The measured temperature.
#[libertas_chart_channel(y, tooltip)]
#[libertas_chart_scale(
id = temperature,
kind = linear,
zero = false
)]
pub temperature: f64,
}
The TypeScript and Rust spellings do not need to be byte-for-byte identical.
For example, both use the source parameter kind even though the generated
scale property is named type. Each parser owns its native annotation syntax,
resolution, traversal, and language-neutral extraction. Studio's shared
validator owns semantic validation and produces the same canonical final
schema result from equivalent declarations.
2.2 Generated language-neutral schema attributes
After Studio validation, the source examples above produce ordinary Libertas
{Name, Value} schema attributes. Rendering clients and the Hub never need to
parse TypeScript JSDoc or Rust macro syntax.
[
{
"NativeName": "TemperatureChart",
"Type": "List",
"Children": [
{
"NativeName": "",
"Type": "TemperaturePoint",
"BuiltinType": false,
"Children": [],
"Attributes": []
}
],
"Attributes": [
{ "Name": "Chart", "Value": "line" }
]
},
{
"NativeName": "TemperaturePoint",
"Type": "Table",
"Children": [
{
"NativeName": "time",
"Type": "DateTime",
"Attributes": [
{ "Name": "ChartChannel", "Value": "x,tooltip" },
{ "Name": "ChartScale", "Value": "id=time,type=utc" }
]
},
{
"NativeName": "temperature",
"Type": "Number",
"Attributes": [
{ "Name": "ChartChannel", "Value": "y,tooltip" },
{
"Name": "ChartScale",
"Value": "id=temperature,type=linear,zero=false"
}
]
}
],
"Attributes": []
}
]
The selected type nodes are abbreviated; all normal schema fields remain
present. See Display Template rules. The example is the validated parser result. Scale type and guide
position are chart appearance: Studio then compiles them into the package's
Display Template as scale and guide hints and omits them from
the final schema, and clients read them from the template. Every rule in this
specification about those properties applies to their effective value, whether
it comes from source or from a template rule. The canonical schema values use stable, case-sensitive tokens, stable
key ordering, no optional whitespace, and no language-specific spelling. Mark,
composition, and scale-enum tokens are lowercase; multiword channel tokens use
the exact lower-camel-case spelling defined in this document.
2.3 Generated localized document
The same short and long comments generate a separate default-locale document.
The selected entries below are abbreviated; numeric field Type values are
schema-node IDs as in the existing document format.
{
"DocumentVersion": "0.0.2",
"Locale": "en",
"StringResources": {},
"Types": [
{
"Name": "TemperatureChart",
"DisplayName": "Temperature history",
"Description": "Recent temperature measurements over time.",
"List": []
},
{
"Name": "TemperaturePoint",
"DisplayName": "Temperature sample",
"Description": "One timestamped temperature measurement.",
"List": [
{
"Type": 1,
"Name": "Sample time",
"Description": "The time at which this measurement was taken."
},
{
"Type": 2,
"Name": "Temperature",
"Description": "The measured temperature."
}
]
}
]
}
This document contains presentation text, not chart semantics. Its translations
may change independently of the canonical Chart* attributes. Section 8.1
defines the deliberately soft dependency between this existing document model
and chart presentation.
2.4 Client interpretation
Android and iOS build the semantic ChartSpec from only the generated schema
and decoded ScriptValue tree. The presentation layer may then decorate that
model with the already-resolved localized document. A client must not infer
chart behavior from document prose, source-language text, or Task Editor
presentation rows.
This separation has four important consequences:
- TypeScript and Rust packages produce equivalent charts when their generated schemas are equivalent.
- No chart-specific protocol message or payload byte is required.
- Chart presentation is atomic: a consumer either validates the complete current contract or presents the existing ordinary data view.
- Parser, schema, and client conformance can be tested independently.
3. Data contract and placement
3.1 Chart metadata belongs to returned data
Chart attributes belong to a protocol response or subscription-data payload type, not to the Endpoint task parameter. The Endpoint identifies and routes the server. The response schema describes the data returned by a particular protocol contract.
The presentation path checks only the effective type of the ScriptValue
selected as the response or subscription payload. If that effective value does
not carry Chart, the ordinary data view is shown. A client must not recursively
hunt for an arbitrary annotated descendant. For a Union response, the client
first resolves the selected concrete variant and then checks that effective
variant value.
A unit chart is a complete snapshot with either this effective shape:
List<Table Row>
The List carries Chart=<mark>. Direct scalar fields of Row carry
ChartChannel, and optionally ChartScale and ChartGuide. The server may
duplicate values in the row when one logical value needs multiple independent
encodings.
A row field may carry Hidden=true. It is chart data, not presentation: the
client keeps it in the retained row value, so a custom chart request can
CopyFrom it. Chart presentation data is never Hidden: a row field
carrying Hidden=true cannot carry ChartChannel, ChartScale, or
ChartGuide.
Or the mark may be placed on a carrier table:
Table UnitCarrier carrying Chart=<mark> {
data: List<Table Row>
transactionContext: any value carrying Hidden=true
...
}
data is the carrier's exactly one non-hidden direct field and is the real
chart data. It does not repeat Chart; the enclosing table supplies the mark.
Every other direct field must carry Hidden=true. Hidden carrier fields may
use any otherwise valid Libertas type and remain ordinary encoded response
data. They do not become rows, channels, guides, titles, tooltips,
accessibility content, interaction identities, or chart resource usage.
Their primary purpose is request/response transaction context analogous to an
HTTP cookie: the client retains the complete original response tree so a later
typed NextRequest or CopyFrom operation can copy the context back without
displaying it or requiring the App to duplicate it in every chart row.
A composition has this effective shape:
Table Composition {
childA: List<Table RowA> carrying Chart=<mark>
childB: List<Table RowB> carrying Chart=<mark>
transactionContext: any value carrying Hidden=true
...
}
The composition Table carries Chart=layer, hconcat, or vconcat.
It must contain at least one chart-bearing direct child. Chart-bearing children
are traversed in schema order; every direct child that does not independently
carry Chart must carry Hidden=true and is retained only as ordinary carrier
data. The same rule applies recursively to nested compositions.
A Response or SubscriptionData Union variant may also be a carrier table
without carrying Chart itself. In that form it has exactly one direct field
whose effective type independently carries Chart, and every other direct
field carries Hidden=true. The client projects that one chart field while
retaining the complete selected variant as the authoritative transaction
source. A variant that carries Chart itself follows the unit-carrier or
composition rules above. Clients never search deeper or choose among multiple
chart fields; Apps use an explicit composition when one result contains
multiple charts.
For a protocol chart, the selected Response or SubscriptionData variant's
localized name and description supply the overall chart title and description,
including when its chart payload is a child field. Unwrapping the payload does
not replace that wording with the child field's documentation. Charts nested
inside a composition retain their own localized names and descriptions.
3.2 Complete snapshots
Every response, and every complete subscription snapshot, replaces the previous chart snapshot. A subscription report may instead update the working value through the shared SubscriptionData mechanisms: an accepted append report extends the chart-bearing working array, and a reduced SubscriptionData report updates the stable value it copies to. The chart presents the complete resulting value. The client does not otherwise join, patch, or merge chart rows. An empty list is a valid empty chart when the schema is valid.
Existing protocol response and subscription routing is unchanged: the normal response reaches the originating client, and chart metadata arrives with the App schema rather than in a new Hub/client message or in every payload.
If a chart must react to a user choice, the client sends a typed Libertas request and the endpoint returns another complete snapshot. Filtering, aggregation, binning, sorting, stacking, window calculation, regression, projection, and sampling therefore remain ordinary App code.
3.3 Direct row fields only
A ChartChannel applies to a direct scalar field of the chart row. Arbitrary
paths into nested tables are not supported. The endpoint flattens nested domain
data into chart rows before returning it.
This rule keeps channel resolution deterministic, makes validation bounded, and
prevents a second expression/path language from entering the schema. It also
allows a client to retain the exact row ScriptValue for tooltips and the
complete response or selected Union variant—including hidden carrier
fields—for typed follow-up requests.
3.4 The source tree is authoritative
Clients traverse the decoded ScriptValue tree, including the selected concrete
Union variant. They must not build chart data from Task Editor visible rows.
For carrier tables they project only the contract-selected chart child while
retaining the unmodified root tree as the request context. Hidden removes a
carrier field from chart projection even when its referenced effective type
also carries Chart; it does not remove, redact, re-encode, or detach that
value from the response. Features such as the Display Template's promote and flatten can promote,
flatten, or hide presentation rows but do not change the underlying chart shape.
4. Chart
Chart has exactly one lowercase token as its generated Value.
4.1 Primitive marks
| Value | Required channels | Additional geometry/grouping | Notes |
|---|---|---|---|
point |
x,y, or theta,radius |
Cartesian offsets when valid | Cartesian or polar scatterplots and symbol layers. Labels use a separate text layer. |
line |
x,y, or theta,radius |
Path grouping and order | Cartesian or polar paths. Null positional values split a path. |
area |
x,y |
Exactly one of y2 or x2; path grouping and order |
y2 creates a vertical band; x2 creates a horizontal band. |
bar |
x,y, or a ranged form |
x2, y2, xOffset, yOffset where valid |
A semantic rectangle with bar defaults. |
rect |
Ranged coordinates or banded x,y |
x2, y2, xOffset, yOffset where valid |
Heatmaps, ranges, candle bodies, tiles. |
rule |
Cartesian or polar endpoint form | Endpoint roles only | Ranges, ticks, and links. Section 5.3 defines both endpoint forms. |
arc |
theta,theta2 |
Optional pair radius,radius2 |
Pie, donut, radial bar, and polar ranges. |
text |
x,y,text, or theta,radius,text |
Cartesian offsets when valid | Cartesian or polar labels and custom annotations. |
polygon |
x,y, or theta,radius |
Path grouping and order | One or more closed filled subpaths, including radar regions, funnels, projected regions, and server-sampled contours. |
row, column, tooltip, detail, order, and key are available to every
mark under their ordinary facet, grouping, ordering, and interaction rules.
Cartesian offsets remain subject to the discrete parent-band rules in §5.3.
Appearance applicability is closed:
| Mark | Allowed appearance channels |
|---|---|
point |
color,fill,stroke,opacity,fillOpacity,strokeOpacity,size,shape,angle,strokeWidth,strokeDash |
line |
color,stroke,opacity,strokeOpacity,strokeWidth,strokeDash |
area, polygon, bar, rect, arc |
color,fill,stroke,opacity,fillOpacity,strokeOpacity,strokeWidth,strokeDash |
rule |
color,stroke,opacity,strokeOpacity,strokeWidth,strokeDash |
text |
color,fill,opacity,fillOpacity,size,angle |
An appearance channel outside its mark row is invalid rather than inert. Every implicit appearance guide therefore describes an encoding that the mark actually paints.
bar is a convenience. A conforming implementation may lower it to rect
before rendering. This keeps the primitive renderer small without making common
source schemas verbose. Tick-like glyphs use an explicit short rule, so their
length is portable data-space geometry rather than a platform-dependent size.
Point, line, rule, and text also have the polar coordinate forms defined in
Section 5.3. polygon supplies the closed filled-path primitive; line remains
open and arc remains the only primitive whose edges are circular rather than
straight screen-space segments.
The following are deliberately not special marks:
- histogram: server-computed bins rendered as `bar` with `x,x2,y`;
- stacked bar or area: server-computed bounds rendered with `y,y2` or `x,x2`;
- box plot: layered `rect` and `rule`;
- candlestick: layered `rect` and `rule`;
- OHLC: layered `rule` marks;
- error bar or confidence band: layered `rule` or `area` and `line`;
- map, network, or Sankey: server-computed coordinates rendered with primitive polygons, paths, rules, areas, points, and text.
4.2 Compositions
| Value | Meaning |
|---|---|
layer |
Draw every child in schema order in one plot area. Later children are visually above earlier children. |
hconcat |
Place child charts left to right. |
vconcat |
Place child charts top to bottom. |
Faceting is expressed by row and column channels on a unit chart. A repeat
operation is unnecessary: the endpoint emits the explicit chart children or
facet values it wants the client to display. A grid is represented by nested
hconcat and vconcat compositions, avoiding a separate column-count grammar.
Facet-grid equality in resource limits defines only a portable count; it does not merge independent scales or choose one layered child's labels. A renderer that lowers layered children into one physical facet grid may do so only when every facet orientation uses the same nonempty compatible shared scale ID and every resolved raw identity has the same display label. Otherwise it renders the grids and headings independently or follows the chart failure policy in §10. A faceted and an unfaceted child therefore do not silently become one grid.
See composition shape rules.
A composition cannot carry row channels directly. Every non-hidden effective
child must independently form a valid chart. Direct children without Chart
are permitted only when they carry Hidden=true; renderers ignore them while
preserving their values in the source snapshot. An unsupported visible child
or a non-chart child without Hidden invalidates the complete composition.
Fallback behavior is described in
Validation and fallback.
A layer child must be a unit chart or another layer; a direct hconcat or
vconcat child is invalid because a multi-plot layout cannot share the outer
layer's one plot area. Nested layers are flattened depth-first in schema order
at their position in the parent. That flattened order owns painting, guide
ownership, hit-test ties, and accessibility traversal. Concat compositions may
contain units, layers, or further concats, so grids remain freely nestable.
Every effective unit sharing one physical layer plot must use the same
coordinate family. A layer may be entirely Cartesian or entirely polar, but it
must not mix the two. Polar arc, point, line, rule, text, and
polygon children share one center and available plot radius. hconcat and
vconcat children have independent plot areas and may use different coordinate
families. Faceting is valid for either family; every polar facet pane receives
its own equal-aspect circular plot.
5. ChartChannel
The generated value is a comma-separated set of canonical roles. A single field may serve several compatible roles:
{ "Name": "ChartChannel", "Value": "x,tooltip" }
Studio's shared validator deduplicates roles and writes them in the canonical
order shown below.
The nearest ChartChannel declaration is complete; channel values are not
merged through a reference chain.
Each schema node may contain at most one Chart, one ChartChannel, one
ChartScale, and one ChartGuide attribute. Repeating an attribute is invalid;
the client never depends on duplicate-attribute lookup order.
5.1 Channel vocabulary
| Group | Roles | Meaning |
|---|---|---|
| Cartesian position | x, x2, y, y2, xOffset, yOffset |
Primary positions, paired range endpoints, and within-band offsets. |
| Polar position | theta, theta2, radius, radius2 |
Angular and radial positions or ranges. |
| Paint | color, fill, stroke |
Mark-default paint or explicit fill/stroke. |
| Paint strength | opacity, fillOpacity, strokeOpacity |
Overall or component alpha. |
| Symbol and stroke | size, shape, angle, strokeWidth, strokeDash |
Mark geometry and stroke appearance. |
| Annotation | text, tooltip |
Visible text and inspector values. |
| Structure | detail, order, key |
Series grouping, path/z order, and stable row identity. |
| Facet | row, column |
Small-multiple partitioning. |
Canonical role order is:
x,x2,y,y2,xOffset,yOffset,
theta,theta2,radius,radius2,
color,fill,stroke,opacity,fillOpacity,strokeOpacity,
size,shape,angle,strokeWidth,strokeDash,
text,tooltip,detail,order,key,row,column
Whitespace is not present in a generated value. Source-language parsers may
accept their native whitespace and quoting, but ChartChannel roles remain
positional rather than named properties.
5.2 Multiplicity
The positional, paint, symbol, text, order, key, row, and column roles
are singletons within one unit chart. tooltip and detail may occur on
multiple fields. Multiple detail fields form a composite series key in schema
field order.
Positions, facets, paint, opacity, symbol, and stroke roles are scale-bearing.
A field may carry at most one scale-bearing role. It may combine that role with
non-scale roles such as text, tooltip, detail, order, or key. Thus
x,tooltip and color,detail,tooltip are valid, while x,color is ambiguous
and invalid. The endpoint duplicates a value into two row fields when the same
logical value needs two independently scaled roles.
It is not valid for two fields to claim the same singleton role. A field with
only non-scale roles cannot carry ChartScale.
x2 always uses the scale of x; y2 uses y; theta2 uses theta; and
radius2 uses radius. An endpoint role without its primary role is invalid.
A secondary endpoint field cannot carry ChartScale; its type and values are
validated against the primary binding.
5.3 Cartesian and polar geometry
x and y locate the primary point. x2 and y2 are explicit data-space
endpoints. Endpoint order is not semantic: clients render the interval between
the two mapped values, so open > close and reversed ranges are valid. A
zero-length rectangle dimension remains meaningful and is drawn one device
pixel thick, so a doji or median remains visible.
For bar or rect with a continuous x,x2 interval in a discrete y
band, the categorical dimension uses a compact density-independent screen-space
thickness capped at 72 percent of the mapped band. It is not synthesized by
adding and subtracting category-index values. A positive interval shorter than
that lane thickness may receive a paint-only minimum of the same length, while
a zero interval remains one device pixel. A positive floor stays centered on
the mapped source interval and clips at a plot edge; it must not shift inward,
because doing so can collapse distinct adjacent buckets onto one identical
footprint. A true zero at an edge may shift inward only enough to retain its
single device pixel. Clipping, hit testing, and overlap use the same painted
footprint. This responsive presentation never changes source endpoints,
domains, tooltip values, semantic order, or stacking.
The paint-only minimum is not authored geometry. Within one categorical lane,
raw intervals with a positive intersection retain source/order precedence;
a zero interval likewise retains that precedence when it is identical to or
contained by another raw interval. Two positive intervals that merely abut do
not overlap. If otherwise-disjoint raw intervals overlap only after clipping or
minimum-width expansion, the renderer may adjust only their screen z-order so
the containing or wider synthetic footprint paints first and the contained or
narrower footprint remains visible. Equal synthetic footprints retain canonical
order. Any conflict is resolved in favor of authored raw-overlap precedence.
The resulting order and exact clipped footprint are shared by painting and
hit-test ties, while appearance lookup continues to use the row's canonical
source/order identity. Grid and axis strokes paint behind these marks so an
edge-clipped one-device-pixel interval cannot be erased. No part of this
screen-space rule changes scales, raw endpoints, lane assignment, tooltip
values, keys, or stacking.
xOffset requires a discrete x band, and yOffset requires a discrete y
band. The offset field creates a nested band. Enumeration order is used when
available; otherwise the canonical categorical ordering from
Appearance values defines nested-band order.
Offsets are useful for grouped bars. They do not add numeric pixels or bypass
responsive layout.
The primitive geometry rules are:
-
`point` requires exactly one primary coordinate pair: Cartesian `x,y` or
polar
theta,radius; -
`line` requires exactly one primary coordinate pair: Cartesian `x,y` or
polar
theta,radius; - a vertical `area` uses `x,y` and optional `y2`; without `y2`, a vertical `area` has a quantitative y baseline of zero;
- a horizontal `area` uses `y,x,x2`; it is horizontal only when `x2` is present;
- a one-sided `bar` has one discrete position and one quantitative `Number` position whose other endpoint is zero;
- for a `bar` or `rect`, each Cartesian axis independently uses either one discrete band field or an explicit primary/secondary interval;
-
`bar(x,x2,y)` uses continuous `x,x2` and is either a horizontal interval in
a discrete
yband or, whenyis continuousNumber, a histogram whose y endpoint is zero unlesszero=falseexcludes it from the visible domain; -
`bar(x,y,y2)` requires a discrete `x` band and continuous `y,y2`; it invents
no baseline. With both
x2andy2, both primaries are continuous and every endpoint is explicit. A secondary endpoint never pairs with a discrete primary; - `rect` never invents a baseline;
-
a `log` scale is invalid on the quantitative axis of a one-sided `bar`, and
on histogram
y, or onyof a vertical area withouty2: those forms necessarily invent a zero baseline, which a log scale cannot represent. Authors use explicit positive endpoints instead; - consequently,
bar(x,x2,y)is a histogram bin,bar(x,y,y2)is a stacked categorical segment,rect(x,x2,y-band)is an irregular-time heatmap cell, andrect(x-band,y,y2)is a box body; -
a vertical `rule` uses `x,y,y2`, a horizontal rule uses `y,x,x2`, and an
arbitrary segment uses
x,y,x2,y2; plot-spanning endpoints are never implied; -
a polar `rule` requires `theta,radius` plus at least one of
theta2,radius2. An omitted secondary coordinate repeats its primary:theta,radius,radius2is a radial segment,theta,theta2,radiusis a straight equal-radius chord, and all four position an arbitrary chord. These are point endpoints, not an angular or radial interval, so either endpoint order is valid and no plot-spanning endpoint is implied; - `text` requires exactly one of `x,y,text` or `theta,radius,text`;
-
`polygon` requires exactly one of `x,y` or `theta,radius`. It cannot use
x2,y2,xOffset,yOffset,theta2, orradius2. Its applicable non-positional channels arecolor,fill,stroke,opacity,fillOpacity,strokeOpacity,strokeWidth,strokeDash,tooltip,detail,order,key,row, andcolumn;text,size,shape, andangleare invalid on this mark.
Except for facet and non-positional channels, one mark never mixes Cartesian
and polar positions. area, bar, and rect are Cartesian; arc is polar;
point, line, rule, text, and polygon select their coordinate family
from their required pair. The angle appearance channel is separate from
theta. It rotates a point shape or text mark clockwise in screen-space degrees
and never changes its polar position.
Polar geometry is normalized identically on Android and iOS:
- See channel compatibility rules.
theta,theta2,radius, andradius2are ordinary numericNumberbindings, notDateOnlynumbers or categorical values. Clients do not derive angular categories, indicator spacing, or radial normalization from labels; - angular zero is at twelve o'clock and positive angles advance clockwise;
- a linear theta domain maps its minimum to zero turns and maximum to one full turn; an identity theta value is already measured in turns;
- an identity radius is normalized from `0` at the plot center to `1` at the available plot radius; a quantitative radius scale maps its domain into the same normalized interval;
-
after titles, guides, and facet headings reserve their space, the polar plot
is the largest centered circle in the remaining pane. Its physical radius
Ris half the smaller plot dimension, so a polar plot is never stretched into an ellipse; -
for normalized mapped values `t,r`, the screen coordinate is
x = centerX + R * r * sin(2*pi*t)andy = centerY - R * r * cos(2*pi*t). Scale reversal is applied before this conversion. A polar layer uses this same center andRfor every child; -
mapped theta and radius values must be finite and lie in the inclusive
interval
0...1. An out-of-range mapped coordinate is invalid; it is not wrapped or clamped to the circular boundary. An invalid point, rule, or text row is suppressed; an invalid line vertex breaks its path; and an invalid polygon vertex becomes a subpath separator after scale domains are resolved. Its finite raw positions still contribute to automatic domain resolution, but it contributes no geometry, appearance domain, group-style comparison, hit target, or accessibility focus. Tooltip and accessibility output for valid geometry retain the raw source value, not its normalized coordinate; - after valid geometry is constructed, marks and hit regions are clipped to the circular plot boundary. Automatic guide labels and titles occupy their reserved guide space and are not part of that mark clip. Painting and hit testing use the same final clipped footprint;
-
`point`, `line`, `rule`, `text`, and `polygon` use straight screen-space
segments after the conversion above. A provider samples any spiral, circular
edge, geodesic, or other curve it needs. Only
archas intrinsically circular edges; - `arc` requires `theta,theta2`; the server supplies explicit angular bounds;
- before output-range reversal, `theta2` must not precede `theta`, and one arc cannot exceed one full turn;
- omitting both radial channels means inner radius `0` and outer radius `1`;
- when radial channels are present, both `radius,radius2` are required;
-
before output-range reversal, `radius` is the inner source endpoint and
radius2is the outer source endpoint.
After both endpoints are mapped through their shared scale, painted arc
geometry and hit testing use the interval between the two mapped endpoints.
reverse=true may therefore place the mapped primary endpoint above its
secondary endpoint without invalidating the row: the physical angular or
radial interval is mirrored, while source roles and tooltip values remain
unchanged. Both mapped endpoint intervals must remain within 0...1; polar
geometry is not wrapped or clipped across an angular boundary. A source
ordering or mapped-range violation suppresses that arc row as invalid geometry;
it is not repaired by reordering the source values.
For connected polar vertices, mapped theta 0 and 1 are physically
coincident. Polygon closure is implicit, so authors normally use an explicit
theta domain such as 0...1, emit vertices in the half-open interval 0...1,
and do not repeat the first vertex. Repeating it is nevertheless well-defined:
it remains a counted source vertex and creates a zero-length edge. An automatic
theta domain still maps its finite minimum to 0 and maximum to 1; the client
does not invent a cyclic category or an unobserved angular endpoint.
5.4 Default order and grouping
List order is the default path and semantic paint order. An order channel
overrides it. The sole presentation-only exception is the synthetic-overlap
z-order rule in Cartesian and polar geometry,
which never reverses two rows whose authored raw intervals overlap.
For line, area, and polygon marks, rows are first partitioned by facet values
and then by the composite detail key. A color, stroke, or fill category
does not implicitly become a grouping key; the same field should also declare
detail when separate paths or polygons are required:
enum TemperatureSeries {
Indoor,
Outdoor,
}
/** @LibertasChartChannel(color, detail, tooltip) */
series: TemperatureSeries;
Within one polygon facet/detail group, source order first determines subpaths.
A row with a null or non-finite required position is a source separator. After
scale domains are resolved, a polar row whose finite required positions map to
non-finite values or outside 0...1 is a mapped separator. Either kind ends the
current subpath and begins a new one at the next validly mapped vertex. Leading,
trailing, and consecutive separators do not create empty geometry. If order
is present, the client then sorts the validly mapped vertices ascending
within each resulting subpath, retaining source order for equal values;
sorting never moves a vertex across a separator. For polygon, order affects
only vertices within a subpath. Polygon groups paint in order of their first
validly mapped source row and are not reordered by one member's order value.
Each contiguous run of at least three finite mapped vertices is closed by an
implicit last-to-first edge. A shorter run produces no polygon geometry and a
bounded diagnostic. All valid subpaths in one facet/detail group form one
compound polygon. The fill rule is always evenOdd: a test point is filled
when a ray from it crosses the compound boundary an odd number of times. Ring
winding has no meaning, nested rings create holes by parity, and disjoint rings
create islands. Self-intersections, coincident vertices, touching rings, and
crossing subpaths are legal and use the same rule; clients neither repair them
nor impose a platform-native simple-polygon restriction. This permits, among
other valid data, a radar polygon whose zero values collapse several vertices
at the center.
Polygon edges are straight segments in mapped screen coordinates. Their implicit closing edges participate in stroke dashing, clipping, hit testing, and resource accounting. A repeated first vertex is permitted but unnecessary; it remains a source vertex and creates one zero-length edge rather than changing closure or fill semantics. A provider samples a curved boundary explicitly.
The optional key channel identifies a logical row across complete snapshots.
It supports stable accessibility focus and future transitions, but does not
change snapshot replacement semantics.
Position, tooltip, and detail values are not implicit row keys. Any number of
source rows may carry the same x, y, temporal bucket, or other scalar value;
clients preserve those rows independently through projection, paint, tooltip,
and accessibility lowering. This rule is type-independent: a repeated bucket
may be DateOnly, DateTime, numeric, or categorical. Only an authored key
asserts logical row identity, and its documented facet/detail scope still
applies.
Categorical identity is always the typed raw value, never its rendered label.
Enumeration members use their member value. Device, VirtualDevice, User,
Endpoint, and LanDevice use their Libertas reference ID. FormattedText Bytes
use their canonical tuple blob.
Resolved names or localized text may be duplicated or changed without merging
categories, changing focus identity, or reordering a domain.
Numeric raw identity is also closed. An integral Number retains its exact
signed or unsigned typed integer. An f32 is first rounded to its declared
binary32 value and then widened exactly to binary64; an f64 retains its
binary64 value. Floating negative zero is canonicalized to positive zero before
grouping, ordering, key comparison, style constancy, or candidate
deduplication. A nonfinite value is invalid before it can become an identity.
For a shared numeric scale, every contribution is converted to binary64 with
round-to-nearest, ties-to-even; equal converted mathematical values occupy one
domain coordinate even when the original integer widths differ. That geometry
deduplication never merges source rows or rewrites their exact raw tooltip,
key, or request values.
An Enumeration is a closed categorical declaration. For an automatic
categorical color, fill, stroke, shape, or strokeDash scale, the
domain contains every declared member in declaration order, including members
that have no eligible row in the current snapshot. This keeps appearance slots
and legends stable as sparse data appears or disappears; declared-but-absent
members create no marks. The complete declared appearance domain is subject to
the ordinary paint, shape, and dash category limits. Position, offset, and
facet domains remain derived from eligible rows, so a declaration alone never
creates an empty axis band, nested lane, or facet pane. Open categorical types
likewise remain row-derived.
5.5 Type inference
Clients infer the semantic data type from the effective Libertas scalar type:
| Libertas type | Default chart interpretation |
|---|---|
Number |
Quantitative continuous data. |
Number with DateOnly=true |
Timezone-free Gregorian calendar date encoded as integer yyyyMMdd; temporal on positions. |
DateTime |
Temporal data shown in local time unless type=utc is explicit. |
TimeOnly |
Local time-of-day data. |
String |
Genuine literal runtime/domain text, a nominal category whose value is itself literal data, or a literal appearance token. It is never a fixed program vocabulary or a copied object display name. |
Boolean |
Nominal two-value category. |
Enumeration |
Ordered or nominal discrete category with localized labels. |
Device |
Nominal Libertas device reference whose display name is resolved by the client. |
VirtualDevice |
Nominal Libertas virtual-device reference resolved by the client. |
User |
Nominal Libertas user reference whose display name is resolved by the client. |
Endpoint |
Nominal Libertas endpoint reference resolved by the client. |
LanDevice |
Nominal Libertas LAN-device reference resolved by the client. |
Bytes with FormattedText=true |
Nominal canonical resource tuple rendered from the App package catalog with LMF1. |
The String row is a semantic assertion about provenance, not permission to
put untranslated program wording in a payload. Whenever a channel can expose a
value as human-readable text—including position/facet labels, ordinal legends,
text, tooltip, explicit value/span guides, and accessibility—the App must
choose the source type as follows:
- a closed App-defined vocabulary uses an `Enumeration`;
-
an identity that is a Libertas system object remains `Device`,
VirtualDevice,User,Endpoint, orLanDevice, so the ordinary client resolver supplies its display name; - App-authored dynamic or parameterized wording uses a formatted Bytes value and LMF1; and
-
only genuine literal domain/user text, such as an externally supplied proper
name or free-form annotation, remains a plain
Stringand is displayed verbatim.
A small or repeated set of proper names does not become a program vocabulary,
and a token does not become literal data merely because task code writes it at
runtime. A plain String used only by key, or by an identity-scale appearance
channel whose guide is necessarily absent, is not rendered as text and is
outside this presentation-source rule. If that same field also carries
tooltip or another text-emitting role, the rule applies.
The schema validator can enforce the effective scalar, channel, scale, and guide combinations, but schema JSON cannot prove whether task code obtained a String from a user/domain source or selected it from a fixed English token set. Declaring a visible plain String is therefore an author/provider conformance assertion that package review and response fixtures must audit.
DateTime is an instant. An ordinary Number retains its declared
quantitative semantics, including unit or duration metadata. DateOnly=true
is the explicit exception: its integer is the decimal calendar tuple
yyyyMMdd, not epoch time. Clients validate the Gregorian date and construct
chart/calendar components directly, without local- or UTC-offset conversion.
An integral, non-duration Number can opt into ordinal, band, or point
only in the role-specific combinations closed by §6.1. A DateTime can opt
into band or point when sparse epoch
instants are categories rather than a continuous timeline. A Number carrying
the existing TimeInterval attribute is a quantitative duration in seconds. A
string appearance field can opt into identity.
A TimeOnly payload is finite seconds from midnight in 0 <= value < 86400.
An out-of-range value is invalid typed input and never wraps modulo a day for
geometry, grouping, labels, or tooltips.
5.5.1 Physical units
PhysicalUnit is the general Libertas numeric-unit attribute used by charts and
ordinary presentation. It does not change the Number wire type. The source
spellings are exactly one quoted positional canonical identifier:
/** @LibertasPhysicalUnit("millimeter") */
amount: number;
#[libertas_physical_unit("millimeter")]
pub amount: f64,
See PhysicalUnit rules.
Both emit PhysicalUnit=millimeter. Empty, unquoted, named, multiple, aliased,
or invalid identifiers are schema errors. The value may be any canonical CLDR
core unit identifier accepted by the
Unicode UTS #35 unit-identifier grammar.
Libertas does not define a smaller unit catalog based on what one client or ICU
release happens to render.
CLDR core identifiers include simple units such as millimeter, generated
compound units such as millimeter-per-day, products such as
kilowatt-hour, and powered units such as square-meter. These identifiers
are not source field names, localized strings, symbols, legacy UCUM aliases, or
App resource keys. Thus an author declares millimeter, while a supporting
CLDR formatter may visibly produce mm or a localized long name. percent
means that raw 50 is fifty percent and displays as 50%; it never rescales
0.5 into fifty percent. A plain Number with PhysicalUnit=second is a
measured quantity in seconds, while TimeInterval retains its separate
duration semantics; the two attributes cannot coexist.
Schema admission and client rendering support are intentionally separate. The schema always preserves a valid CLDR identifier because that semantic metadata is useful even when a particular client ICU version cannot format it. A client that supports the identifier localizes the complete unit, including a compound numerator and denominator. A client that does not support it may display the numeric value without unit decoration; it must not reject or alter the value.
See PhysicalUnit rules.
PhysicalUnit is valid only on an effective Number and cannot coexist with
DateOnly, TimeInterval, or EnumSource. Effective attribute inheritance
uses ordinary nearest whole-value replacement. The raw value, constraints,
domain, ordering, grouping, tick candidates, geometry, tooltip/request value,
and persistence remain expressed in the declared unit. V1 never performs
automatic unit conversion: a Celsius field remains Celsius in every locale and
a millimeter field remains millimeters. An App that wants converted data emits
the converted value and declares its resulting unit.
A client that can format a unit-bearing visual number first applies that surface's existing deterministic numeric precision rule, then asks its CLDR unit formatter to join the resulting number and the declared provided unit. Visible chart and ordinary-data text uses CLDR short width; accessibility uses CLDR long width with the locale's plural and grammatical rules. A client must not concatenate an English name or ASCII symbol, infer a unit from a native field name or description, convert to a locale-preferred unit, or let the unit formatter independently change the already selected numeric precision. Negative zero remains zero.
Within a chart, the rule applies wherever that exact numeric field is rendered:
ordinary and explicit tooltip values, Cartesian and polar scale-guide ticks,
numeric source=value or source=span guide labels, continuous appearance
legend values, numeric text marks, selection/guide overlays, and accessibility.
A source=scale guide uses its target binding's unit; a value/span guide uses
the declaring label field's unit and never borrows the target's unit. Field and
guide titles remain their independently localized document text and do not
silently gain a unit suffix. Null or suppressed values produce no standalone
unit text.
A unit-bearing chart binding is a continuous physical quantity. If it owns a
scale, its effective type must be linear, log, symlog, or sqrt; it
cannot use identity, ordinal, band, point, time, or utc. A primary
and secondary endpoint pair must have the same effective PhysicalUnit,
including both absent. Bindings sharing one nonempty scale ID must also have the
same effective unit; dimensionally convertible but different declarations such
as meter and millimeter are incompatible because V1 does not convert. An
incompatible pair/shared scale invalidates chart presentation and invokes the
chart failure policy in §10. An unknown received unit, a unit unsupported by
the client's ICU release, or a unit-formatter failure instead omits only unit
decoration everywhere that value is rendered; the numeric scalar and the chart
remain available. Raw unit identifiers are diagnostic-only and never visible.
5.5.2 Scalar display formats
Format uses the same language-neutral Unicode LDML contract as ordinary
Libertas UI presentation: ordinary Number fields use
UTS #35 number format patterns,
while DateOnly, DateTime, and TimeOnly use
UTS #35 date/time format patterns.
The portable field/token subset and placement rules are defined in the
schema specification.
A chart applies the effective Format whenever that field produces visible
text: scale ticks, tooltips, text marks, value/span guide labels, selection
overlays, and accessibility descriptions. It never parses the formatted text
back into data. Domains, tick candidates, sorting, grouping, equality, focus
keys, interpolation, scale transforms, and geometry always use the raw Number,
yyyyMMdd, epoch-second, or seconds-from-midnight value. DateTime formatting
uses the viewer's local timezone even when an explicitly utc scale uses UTC
for geometry; the authored pattern controls presentation, not scale semantics.
See shared-scale compatibility rules.
Two fields sharing one nonempty ChartScale.id must have identical effective
Format values, including both absent, because the shared guide has one tick
label policy. A primary/secondary range pair may use different formats for its
own tooltip values. When PhysicalUnit and Format coexist, Format produces
the numeric portion first and the provided unit is then joined by the existing
CLDR unit rule; % and ‰ in a number pattern are scaling presentation tokens,
whereas PhysicalUnit=percent and permille never rescale the raw value.
A chart DateTime payload is an integral Unix second in the inclusive range
0..253402214399, whose upper endpoint is
9999-12-30T23:59:59Z. This chart-specific bound deliberately reserves a full
day before the end of civil year 9999 so conversion through any configured
time-zone offset whose absolute value is less than 86400 seconds stays within
the proleptic-Gregorian years 0001 through 9999. A nonintegral or out-of-range
row value, explicit bound, unavailable time-zone offset, or conversion outside
that civil range invalidates chart presentation with a bounded diagnostic; it
is never clamped or delegated to a platform formatter's private range.
Client-generated temporal ticks and transient DateTime viewport edges may be
fractional epoch seconds; they are inert derived guide instants, never a
DateTime payload, tooltip, key, or typed request value.
Other Libertas semantic types, tables, lists, unions, functions, nil values,
and plain Bytes are not direct chart scalars in version one. An App
must not flatten an Enumeration or Libertas object reference into a duplicate
display String merely for a chart. Keeping the typed value lets the frontend
apply the same localization and permission-aware identity resolution as every
other Libertas view, without adding configuration burden to the end user.
In the compatibility table below, categorical means a provenance-valid
literal String, Boolean,
Enumeration, Device, VirtualDevice, User, Endpoint, LanDevice, or a
FormattedText Bytes value. A discrete Number is separately allowed only
with an explicit ordinal, band, or point scale.
Scalar compatibility is closed rather than inferred differently by native libraries:
| Channel roles | Permitted effective scalar types |
|---|---|
x, y |
Number (continuous by default or explicitly discrete), DateOnly Number (temporal or band/point), DateTime, TimeOnly, or categorical |
x2, y2 |
Continuous ordinary Number, DateOnly Number, DateTime, or TimeOnly, compatible with the primary |
xOffset, yOffset, row, column |
Categorical, or integral non-duration Number/DateTime/DateOnly with an explicit band scale |
theta, theta2, radius, radius2 |
Number |
color, fill, stroke |
Continuous Number; categorical; or validated identity plain String |
opacity, fillOpacity, strokeOpacity, size, angle, strokeWidth |
Number |
shape, strokeDash |
Categorical; discrete Number; or validated identity plain String |
text, tooltip, detail, order, key |
Any version-one chart scalar listed in the inference table |
See channel compatibility rules.
The categorical types above are intrinsically discrete. A Number is discrete
only with ordinal, band, or point as required by the role; DateTime and
DateOnly are discrete only with band or point. A continuous field cannot facet or offset;
a categorical field cannot supply a numeric
opacity, radius, width, angle, or size. Formatted text can use only categorical
or textual roles and an automatic, ordinal, band, or point scale; it
cannot use a quantitative role, continuous domain, or identity scale. Any
combination outside this table is invalid and falls back instead of relying on
a platform library's coercion.
Nullable positional values create missing points. For line and area marks, a missing position breaks the path; it is not interpolated. For polygon, it separates subpaths as defined in Section 5.4. A missing optional appearance value uses the native mark default. Non-finite numbers receive the same mark-specific suppression or separator behavior and are reported through bounded diagnostics.
5.6 Appearance values
Without an appearance channel, the client uses its accessible theme-native
default. Categorical paint channels use a deterministic Libertas palette.
Literal values use ChartScale(type=identity); their implicit guide is none.
color is the mark's default paint: it supplies stroke for line and rule,
fill for area, bar, rect, arc, text, and polygon, and both fill and
stroke for point. An explicit fill or stroke overrides color for that
component.
Overall opacity multiplies the color alpha and the applicable fillOpacity or
strokeOpacity; missing opacity components default to 1.
An explicit stroke paints the boundary of an area, bar, rect, arc, polygon,
or point and the path of a line or rule. For area, bar, rect, arc, and polygon,
the boundary is absent when no explicit stroke resolves; strokeWidth,
strokeDash, and strokeOpacity alone never synthesize it. Text uses its fill
paint for glyphs and has no outline in version one. Point shape, rotation,
radius, fill, and boundary use the same final geometry in marks, legends, hit
testing, and accessibility.
A resolved stroke width less than or equal to zero paints no path or boundary,
creates no stroke hit region, and must skip the native stroke call. Clients
must not pass semantic zero to Core Graphics, Skia, or another backend that may
interpret zero as a device hairline.
A polygon always has a fill: an explicit fill, otherwise color, otherwise
the accessible theme-native polygon fill. Its boundary stroke is absent unless
an explicit stroke binding resolves to visible paint. strokeWidth,
strokeDash, and strokeOpacity do not synthesize a stroke or a stroke hit
region by themselves.
A polygon facet/detail group has one paint and stroke style, not a per-vertex
gradient. For every group-level appearance binding—color, fill, stroke,
opacity, fillOpacity, strokeOpacity, strokeWidth, and strokeDash—all
finite, validly mapped vertices in all of that group's subpaths must carry the
same typed canonical raw value, or all carry null and use the same mark default.
Comparison uses raw identity, never a localized label, clamped scale output, or
theme-adjusted paint. A mismatch invalidates the complete chart presentation
and invokes the chart failure policy in §10. The renderer fills each compound path with
evenOdd. Within one unit it paints every polygon detail-group fill in group
paint order before painting any of the groups' explicit visible boundary
strokes in that same order. A later translucent fill therefore cannot bury an
earlier series outline. Each stroke remains above every fill along every
subpath boundary, including its implicit closing edge.
For any discrete scale, Enumeration values use declaration order;
Boolean uses false,true; Number and Libertas object references use raw numeric
order; a discrete DateTime uses raw epoch-second order and a DateOnly value uses
its validated chronological yyyyMMdd numeric order; String uses Unicode
scalar-value order of its raw value; and formatted Bytes use canonical blob
order. Resolved display names and formatted output never affect ordering. The
same ordering applies to categorical axes, legends, facets, and nested bands.
An automatic
categorical paint domain is limited to ten values and maps in order to these
stable sRGB base hues:
#4E79A7,#F28E2B,#E15759,#76B7B2,#59A14F,
#EDC948,#B07AA1,#FF9DA7,#9C755F,#BAB0AC
The base hue and category index are semantic and remain stable. At rendering
time, each client minimally adjusts an automatic categorical color toward the
platform theme's semantic foreground when the base color does not provide at
least 3:1 non-text graphical contrast against every active opaque chart
surface. This adaptation may differ between native platform themes, but it must
not reorder categories, rotate hues between categories, use localized labels as
identity, or change while a theme remains unchanged. Legends and marks use the
same adjusted color. Color is never the only category indicator: resolved
legend text and accessibility semantics remain authoritative.
An identity color is literal endpoint data and is never theme-adjusted. An
author who chooses ChartScale(type=identity) owns its contrast on
every supported theme.
An endpoint needing more than ten automatic paint categories supplies a bounded identity-color field or uses facets; clients do not repeat colors and imply false equivalence. Positional bands, facet values, and textual legends are not subject to the paint limit; they remain subject to row, facet, and layout caps.
A continuous numeric paint scale without mid uses these Viridis stops from
domain minimum to maximum:
#440154,#482878,#3E4989,#31688E,#26828E,
#1F9E89,#35B779,#6DCD59,#B4DE2C,#FDE725
With mid, it uses these blue-neutral-red stops with the neutral stop fixed at
the midpoint:
#053061,#2166AC,#4393C3,#92C5DE,#D1E5F0,#F7F7F7,
#FDDBC7,#F4A582,#D6604D,#B2182B,#67001F
Clients interpolate unpremultiplied sRGB components linearly between adjacent stops. These are semantic rendering constants, not extra schema attributes. They make heatmaps, divergent maps, and legends deterministic on both platforms. Authors needing different colors send validated identity values.
Other automatic appearance ranges are also portable: point radius maps to
3..18 density-independent points, text size to 10..32, stroke width to
1..8, opacity to 0.15..1, and angle to 0..360 clockwise degrees. Ordinal
shape uses circle,square,triangle,diamond,cross in order, while ordinal stroke
dash uses solid,dash,dot,dash-dot. Automatic shape and dash domains are
therefore limited to five and four categories respectively. Identity values are
used directly. Size must be finite and at least 0.5, with no schema upper
bound; stroke width is bounded to 0..32, opacity to 0..1, and angle to
finite degrees. A client may saturate an accepted size only while converting
it to the finite native scalar range of its drawing API. Such representational
saturation is not schema validation and must never reject the chart.
Every ordinal appearance output is also deterministic. Let i be a category's
zero-based index in a domain of N values. Ordinal paint uses palette entry
i; shape and stroke dash use entry i in their fixed lists. For opacity,
size, and stroke width, let u=0 when N=1, otherwise u=i/(N-1), and map
that value through the same bounded physical range as the corresponding
continuous scale. Ordinal angle instead uses the half-open slot
360*i/N degrees, with a singleton at zero, so the last category never
duplicates the first at 360 degrees. reverse=true replaces i with
N-1-i for output selection without changing raw domain order. Paint and all
other ordinal appearance domains are limited to ten categories unless the
smaller shape or dash limit applies. A schema-integral, non-TimeInterval
Number may opt into this ordinal behavior; a floating or duration Number may
not use coincidental whole-valued rows as categories.
Recommended literal encodings are:
| Channel | Identity value |
|---|---|
color, fill, stroke |
sRGB #RRGGBB or #RRGGBBAA. |
| Opacity channels | A finite number from 0 through 1. |
shape |
A supported token such as circle, square, triangle, diamond, or cross. |
angle |
Clockwise degrees. |
size |
Point radius or text font size in density-independent points. |
strokeWidth |
Density-independent points. |
strokeDash |
solid, dash, dot, dash-dot, or a bounded space-separated positive-number pattern. |
Unsupported literal values invalidate the chart presentation, not the protocol payload. Arbitrary CSS, URLs, scripts, and platform-specific color objects are not accepted.
5.6.1 Display Template palette
A Display Template palette hint on a
Chart or Chart3D root selects the discrete color list for that chart's
automatic categorical and ordinal paint (color, fill, and stroke) and for
the default colors of series that bind no paint channel:
| Value | Discrete color list |
|---|---|
auto |
The specification default: the ten categorical base hues above. |
categorical |
The same ten categorical base hues. |
sequential |
The ten Viridis stops above, as entries 0 through 9. |
diverging |
The eleven blue-neutral-red stops above, as entries 0 through 10. |
Category i uses entry i of the selected list, the theme contrast adjustment
applies to every list, and the ten-category paint limit applies for every
value. The hint never changes continuous numeric paint, whose stops mid alone
chooses, or identity colors. Studio reports a palette rule whose node has no
Chart or Chart3D, or whose value is not one of the four tokens; every token
is valid with every paint scale.
5.7 Runtime row semantics
List order is stable source order for semantic paint order and hit-test ties. An
order binding sorts ascending by the effective scalar's canonical ordering;
equal values retain list order. The synthetic-overlap exception in
Cartesian and polar geometry changes only
screen z-order and the matching hit-test tie for otherwise-disjoint intervals;
canonical order remains the owner of style fallback and every raw-overlap tie.
Unordered on a chart list does not change chart geometry. A line, area, or
polygon without an order binding follows the received list order, as on any
other list, and polygon separator rows split subpaths in that order. An App
whose path must not depend on the emitted sequence declares order.
A null required position suppresses a point, bar, rect, rule, arc, or text row. For line and area, a null required position breaks the path. For polygon it is a subpath separator under Section 5.4. A non-finite required position has the same mark-specific behavior and produces a bounded diagnostic; it never reaches a native drawing API. Null optional style values use the mark default. Tooltip fields are shown in structural field order and null tooltip values are omitted.
A non-null key must be unique within the tuple of chart unit, facet values,
and detail values. A duplicate key makes the runtime chart invalid and invokes
the chart failure policy in §10 rather than ambiguous interaction. Without key, row
index is identity only for the current complete snapshot.
Layer traversal and row order define hit priority: the last painted eligible
mark wins when distances are equal. A line segment resolves to the nearest of
its originating endpoint rows. Tooltip and request actions use that row's
original ScriptValue; the renderer never synthesizes or joins a source row.
6. ChartScale
ChartScale is optional. Native automatic scales are used when it is absent.
Its type reaches clients through the Display Template
as described above.
Its generated Value is a comma-separated sequence of key=value properties
in this canonical order:
id,type,min,max,mid,zero,reverse
The grammar is intentionally closed and does not provide an arbitrary option object:
scale = property *( "," property )
property = id | type | min | max | mid | zero | reverse
id = "id=" identifier
type = "type=" scale-type
min = "min=" scalar
max = "max=" scalar
mid = "mid=" scalar
zero = "zero=" boolean
reverse = "reverse=" boolean
identifier = ( ALPHA | "_" ) *( ALPHA | DIGIT | "_" | "." | "-" )
boolean = "true" | "false"
scalar = [ "-" ] integer [ fraction ] [ exponent ]
integer = "0" | nonzero-digit *DIGIT
fraction = "." 1*DIGIT
exponent = ( "e" | "E" ) [ "+" | "-" ] 1*DIGIT
Properties appear at most once and in canonical order. The generated form has
no whitespace. A source scalar must match the grammar above, is parsed to
binary64 with round-to-nearest, ties-to-even, and must not overflow to a
nonfinite value. Generated scalar spelling is semantic, not a lexical cleanup:
every numeric zero, including negative zero and underflow, is 0; every other
value uses the shortest decimal that round-trips to the same binary64. It uses
fixed notation when the normalized base-ten exponent is from -6 through 20
inclusive and lowercase scientific notation otherwise. Scientific notation has
one nonzero digit before the decimal point, no + sign or leading exponent
zeros, and no redundant fractional zeros or decimal point. Thus 100e-2 and
1.0e0 become 1, 0e2, -0e-3, and 1e-999 become 0, and a leading-zero
integer such as 001 is invalid. TypeScript and Rust use this same canonical
algorithm before same-ID property comparison.
For DateTime the scalar is an integral Unix
second in the chart range 0..253402214399, for TimeOnly it is seconds from
midnight, and for a Number carrying
TimeInterval it is a duration in seconds, matching their existing runtime
scalar representations. For DateOnly it is the integer yyyyMMdd. No quoting or escaping is needed because every
string-valued property uses a closed token or restricted identifier. A future
property that needs free text must use localized schema documents rather than
extending this grammar with arbitrary strings.
For example:
{
"Name": "ChartScale",
"Value": "id=price,type=linear,min=0,max=100"
}
6.1 Properties
| Property | Values | Meaning |
|---|---|---|
id |
A schema identifier | Share or resolve a scale within the containing composition. |
type |
linear, log, symlog, sqrt, time, utc, ordinal, band, point, identity |
Override the scale inferred from the scalar type and channel. |
min |
A canonical scalar literal | Fix the lower domain bound. |
max |
A canonical scalar literal | Fix the upper domain bound. |
mid |
A canonical scalar literal | Supply a center for a divergent continuous scale. |
zero |
true or false |
Include or exclude zero from an automatic quantitative domain. |
reverse |
true or false |
Reverse the output range. |
Property keys cannot repeat. Unknown keys or invalid combinations are shared
Studio-validation errors before final schema generation and presentation
errors when received from an untrusted or newer schema. Because lookup uses
nearest-value precedence, a
local ChartScale replaces the complete inherited or referenced scale value;
individual scale properties are never merged across declarations.
For DateTime, time formats and groups an epoch value in the device's
configured local time zone and utc uses UTC. For DateOnly, both scale tokens
operate on the same timezone-free calendar components. identity is intended for literal
appearance values and normalized polar geometry; it must not bypass bounds or
type validation.
The effective scale-type matrix is closed. “Integral Number” below means a
field whose schema NumberType is integral; a floating field does not become a
category merely because its current values happen to be whole numbers.
| Target | Effective scalar | Permitted effective scale types |
|---|---|---|
x, y |
Number, including TimeInterval |
linear, log, symlog, sqrt; an integral non-duration Number may explicitly use band or point |
x, y |
DateTime or DateOnly Number |
time, utc, band, point |
x, y |
TimeOnly |
linear |
x, y |
categorical or FormattedText | band, point |
xOffset, yOffset, row, column |
categorical, FormattedText, integral non-duration Number, DateTime, or DateOnly Number |
band |
theta, radius |
non-DateOnly Number, including TimeInterval |
linear, log, symlog, sqrt; a non-duration Number may instead use identity |
See channel compatibility rules. color, fill, stroke |
non-DateOnly Number | linear, log, symlog, sqrt; an integral non-duration Number may explicitly use ordinal |
color, fill, stroke |
categorical or FormattedText | ordinal; a plain String may instead use identity |
See channel compatibility rules. opacity, fillOpacity, strokeOpacity, size, angle, strokeWidth |
non-DateOnly Number | linear, log, symlog, sqrt; a non-duration Number may use identity, and an integral non-duration Number may explicitly use ordinal |
See channel compatibility rules. shape, strokeDash |
categorical, FormattedText, or integral non-duration Number | ordinal; a plain String may instead use identity |
Secondary endpoints inherit their primary scale. text, tooltip, detail,
order, and key do not own scales. A discrete dimension of bar or rect
must resolve to band; point has no band extent from which to construct the
rectangle. Offset and facet roles likewise require band, never point or
ordinal. A combination absent from this matrix is invalid even if a native
chart library would coerce it.
TimeInterval is always a quantitative duration. Wherever its target permits a
quantitative Number it may use linear, log, symlog, or sqrt, but never
identity, ordinal, band, or point.
Scale validation includes these rules:
- `min` must not exceed `max`;
- `mid` is valid only on a compatible continuous divergent scale and must lie within an explicit domain when both bounds are present;
- a `log` scale and every value supplied to it must be strictly positive;
- a `sqrt` scale and every value supplied to it must be non-negative;
- `zero` is valid only for quantitative continuous scales other than `log`;
- `identity` cannot declare `min`, `max`, `mid`, `zero`, or `reverse`, and its values must already use the channel's portable physical or style representation;
- temporal, categorical, and identity scales cannot use a numeric zero baseline;
-
`band` and `point` are valid only for discrete positional or facet channels.
Applying either to
DateTimeexplicitly changes epoch values into sparse temporal categories: identity and order still use raw epoch seconds, while the axis contains only values emitted by the endpoint. Labels use the device's configured local date and time; - See scale type compatibility rules. primary
thetaandradiuspositions are quantitative Number channels and cannot useordinal,band,point,time, orutc. Their explicit scale type islinear,log,symlog,sqrt, oridentity; identity values must already lie in0...1; - See scale type compatibility rules.
TimeOnlyis always continuous local time-of-day and cannot useordinal,band, orpoint; explicit bounds must lie in0..86400inclusive; - See scale scalar rules.
DateTimevalues and explicit bounds must be integral Unix seconds in0..253402214399inclusive; - See scale type compatibility rules. style and paint channels cannot use
time,utc,band, orpoint; -
`time` and `utc` are valid only for `DateTime` or a `DateOnly` Number;
DateOnlyotherwise permits onlybandorpoint;log,symlog, andsqrtare valid only for ordinary quantitativeNumber; -
`min` and `max` are valid only for continuous quantitative or temporal
scales;
midis valid only for a continuous quantitative paint scale; - a secondary endpoint channel cannot declare a scale.
Continuous scale geometry is defined in transformed coordinates so every
client produces the same mapping instead of delegating nonlinear semantics to
a platform library. For a finite raw value v, the canonical transform is:
linear, time, utc: f(v) = v
log: f(v) = ln(v) (v > 0)
symlog: f(v) = sign(v) * ln(1 + abs(v))
sqrt: f(v) = sqrt(v) (v >= 0)
DateOnly is the explicit temporal exception to the first line. Its raw
identity remains the integer yyyyMMdd, but time and utc decode it to the
timezone-free proleptic-Gregorian day ordinal relative to 1970-01-01 before
mapping: f(v)=days(date(v))-days(1970-01-01). The mathematical inverse is a
real day ordinal; only an exact integral ordinal is a typed DateOnly candidate
and encodes back to yyyyMMdd. Clients never round a fractional ordinal or
interpolate through invalid decimal dates when producing a data value, tick, or
interaction value; §9 alone rounds a transient requested viewport boundary to
a typed date. After semantic scalar canonicalization, a generated DateOnly
min or max must be an exact eight-digit valid Gregorian date. An equivalent
source literal such as 2.0240229e7 is accepted and emitted as 20240229;
source spelling never survives into the wire value. Because that
encoding is fixed-width year-month-day order, raw numeric comparison remains
chronological after validation.
The symlog transform therefore uses the fixed portable constant 1 inside
log1p; version one does not expose a configurable constant. Every canonical
transform and inverse result is rounded to binary64 with round-to-nearest,
ties-to-even; clients use a shared correctly-rounded implementation or match
the conformance vectors rather than accepting a platform-libm ULP difference.
For a resolved
raw domain lo..hi, let A=f(lo), B=f(hi), T=f(v), and
q=max(1,abs(A),abs(B)). Clients use the overflow-safe normalization
u = ((T/q) - (A/q)) / ((B/q) - (A/q)). The contract value is the
round-to-nearest, ties-to-even binary64 rounding of that mathematical real
ratio; a conforming implementation uses sufficient intermediate precision if
binary64 division would collapse distinct finite A and B. reverse=true
maps the result to 1-u.
An implementation compares T with the ordered transformed endpoints before
normalization; an out-of-domain value is classified beyond the corresponding
output edge without evaluating a potentially overflowing outside ratio.
Explicit bounds and automatic-domain inference remain raw data values and are
validated before transformation. Values outside an explicit domain are
clipped by the plot; a client does not clamp or rewrite their source values.
When lo == hi, that one transformed value maps to the range midpoint and the
scale-sourced guide exposes one raw-value tick; clients do not invent different
padding domains around it.
Guide candidates and responsive density follow the finite algorithm in §6.4.5;
a nonlinear tick position is always computed through this transform and its
label represents the corresponding raw value. Interaction performs the matching inverse transform: exp(t) for
log, sign(t) * expm1(abs(t)) for symlog, and t * t for sqrt.
Implementations use the numerically stable log1p and expm1 operations so
values close to zero do not collapse during a forward/inverse round trip.
Tooltips, accessibility, explicit bounds, and request values always expose raw
values, never transformed coordinates. The logarithm base does not change a
normalized position; natural logarithms above define the portable inverse.
The same transformed normalization governs continuous numeric appearance
channels. Without mid, u selects the sequential range. With mid, the
resolved raw domain expands to contain the midpoint, lo..mid maps through
f to 0..0.5, and mid..hi maps through f to 0.5..1; the neutral
divergent color is therefore exactly at mid. reverse=true is applied after
that piecewise normalization. Appearance output clamps an out-of-domain value
to the nearest range endpoint; its raw value remains unchanged in tooltips and
accessibility. Appearance guides label raw values.
For ordinal, band, and point scales, reverse=true reverses the output range,
not the canonical raw-identity domain. Axis/facet placement and automatic
appearance ranges therefore run in the opposite direction while grouping,
ordering, keys, labels, and tooltip identity remain unchanged.
6.2 Automatic domains
Default scale selection is deterministic:
| Field and channel | Default scale |
|---|---|
Number on a quantitative channel |
linear |
DateTime on a position |
time |
Number with DateOnly on a position |
time, formatted as a timezone-free calendar date |
TimeOnly on a position |
linear, formatted as local clock time |
Number with TimeInterval |
linear, formatted as a duration |
Discrete value on bar, rect, row, column, or an offset |
band |
| Discrete value on another position | point |
| Discrete value on paint, shape, or stroke dash | ordinal |
| Numeric paint | linear sequential, or divergent when mid is present |
TimeOnly row values use seconds from midnight in the half-open interval
0..86400 and default to the full 0..86400-second scale domain. The upper
domain endpoint is an end-of-day bound sentinel, not a second midnight data
value. Explicit min and max may use the inclusive interval 0..86400.
A DateTime author
uses type=utc when UTC formatting is semantically required; otherwise device
local time is the default. An explicit band or point instead requests a
discrete, sparse DateTime axis and therefore cannot simultaneously request the
utc scale. Endpoints should choose bucket instants whose local labels are
unambiguous for their audience.
6.2.1 Sparse temporal lanes
A server-precomputed water-usage report is a representative sparse temporal
chart. One unit rect chart puts a zone reference on a categorical y band,
so every zone occupies exactly one lane. Each nonempty day or week bucket begins
one server-computed horizontal stack. The App emits Number x,x2 fields named
display_start,display_end on a shared linear scale; they are
synthetic display seconds, not timestamps. The separate Number
bucket_starts_on field carries DateOnly=true; it is the exact timezone-free
UTC calendar bucket and supplies both the tooltip and explicit span guide. Clients present it as a localized
human date with no time or timezone conversion.
Every response uses a 600-second full stack and one response-wide maximum bucket
amount, producing one seconds-per-millimeter scale for every zone and occupied
bucket. The App clips source contributions to the exact requested range before
aggregation. It groups all zones by their real occupied bucket start, assigns
every stack in a group the same display anchor, and uses the greatest rounded
stack extent in that group when placing the next anchor. After a prior extent,
a real gap of 0–59 seconds is retained; a gap of 60 seconds or more is replaced
by a fixed 30-second display gap. The gap is 5% of the full stack and still
leaves capacity for four one-second positive-contributor floors. Within each
stack, rain, irrigation, forecast rain, and scheduled water remain contiguous
in that order and are amount-proportional except for bounded whole-second
allocation needed to keep every positive contributor nondegenerate. The exact
segment amount and real bucket in the tooltip are authoritative.
color,detail distinguishes rain, irrigation, forecast rain, and scheduled
water.
Normative broken-time interval-lane policy
This pattern is for sparse, interval-like quantities whose visual comparison is more useful than a calendar-proportional gap. It makes occupied buckets legible without pretending that empty calendar time is measured. It is not a time series, event timeline, duration chart, or a client-side stacking feature.
The provider owns every geometric decision. Its row schema uses the generic guide attribute explicitly:
#[libertas_chart_channel(x)]
#[libertas_chart_scale(id = usage_display, kind = linear)]
display_start: f64,
#[libertas_chart_channel(x2)]
display_end: f64,
#[libertas_date_only]
#[libertas_chart_channel(tooltip)]
#[libertas_chart_guide(
target = x,
source = span,
position = bottom,
title = none,
grid = none
)]
bucket_starts_on: u32,
#[libertas_chart_channel(tooltip)]
amount: f32,
display_start,display_end must be finite numeric coordinates and every valid
positive contribution must have display_end > display_start. They are
explicitly display coordinates: neither field is an event time, and an
automatic time guide must be absent. The provider supplies the real bucket date
and exact quantity in tooltip fields. Those tooltip values, not the synthetic
coordinate, are the factual date and amount presented to a person, copied to
accessibility output, and used by details or drill-in actions. A bucket value is
never implicit row identity: every same-bucket source contribution remains a
distinct row unless the author provides an explicit key channel.
The explicit ChartGuide is a generic span-label guide; DateOnly has no
special guide status. Within each facet pane it groups eligible rows by the
exact raw bucket_starts_on identity, maps and clips their authored x,x2
intervals, merges overlapping intervals, and places one localized date at each
connected component's midpoint. Its explicit guide replaces the implicit x
axis. Adding, removing, or reordering tooltip fields therefore cannot change
the guide. This is not a time scale: the client does not derive calendar
distance from the labels, move an interval, fill a gap, or expose the synthetic
coordinates.
For a response, group all valid rows by the exact real bucket anchor across all lanes. Sort those anchors ascending, start the first display anchor at zero, and give every lane in a group that same anchor. Use one response-wide quantity denominator. For each lane, allocate the ordered positive contributors from cumulative quantity boundaries, preserving contiguity and reserving a whole synthetic unit for every remaining contributor. The group extent is the largest resulting rounded lane extent. Thus rounding cannot make another lane overlap or cause its next group to relocate.
For the sprinkler V1 contract, the full-scale extent is exactly 600 synthetic
seconds. Let previous_extent be the preceding real bucket's group extent and
let delta be the difference between consecutive real bucket anchors. The next
display anchor is previous_anchor + previous_extent + delta when
0 <= delta < 60, and previous_anchor + previous_extent + 30 when
delta >= 60. Equal anchors are one group, not a zero-gap second group. The
30-unit collapsed gap is five percent of 600 and leaves at least four integer
display units for one-unit positive-contributor floors. A later contract version
may choose different response-wide fixed parameters, but it must document them
as a provider contract; clients must never infer or normalize a gap locally.
The provider uses stable contributor ordering (for sprinkler: rain, irrigation, forecast rain, scheduled water), a deterministic sort by real bucket anchor, and cumulative-boundary rounding rather than independently rounded segment widths. It omits nonfinite or nonpositive inputs. If no valid positive interval can be produced for a lane, it returns the ordinary empty-lane annotation rather than a zero-width mark or fabricated value. Saturating coordinate arithmetic must preserve order where representable; a provider that cannot emit a finite, strictly positive interval omits that invalid row rather than wrapping or relocating unrelated rows.
For example, suppose a first real bucket has a 10-mm zone A stack that rounds
to 600 units and a 5-mm zone B stack that rounds to 300; both start at display
zero. A second real bucket 59 seconds later starts at 600 + 59 = 659 for both
zones. A third bucket 60 seconds after that starts at 659 + 600 + 30 = 1289.
A much later fourth bucket also adds only the preceding maximum extent and 30,
not its calendar-sized gap. Within each stack the ordered segments remain
adjacent, so, under the same denominator, a 6-mm segment is three times the
width of a 2-mm segment except where the documented one-unit floor is required.
The client maps valid endpoints literally into the continuous x range and the categorical lane into its band. Apart from the explicit span guide above, it does not aggregate, sort, stack, renormalize, replace gaps, relabel the x axis as time, or repair provider geometry. It applies the paint-only minimum and matched clipping/hit-testing footprint defined in Section 5.3; that presentation rule never changes this provider geometry. The tooltip and accessibility label must state the real bucket date and exact amount, never synthetic seconds.
The provider owns this precomputed visual-coordinate contract. It must use one
consistent chart-wide mapping and emit a finite, nondegenerate x,x2 interval
for every positive segment; a clipped edge, rounding step, or empty contributor
must not corrupt unrelated geometry. The client maps the supplied endpoints
literally. It does not aggregate, normalize, stack, facet, or repair these rows.
Responsive renderers may guarantee a minimum visible pixel width for a valid
positive interval; that presentation aid is not a substitute for valid provider
geometry and does not change the data-space mapping. The explicit span guide
replaces the misleading numeric axis for synthetic coordinates. This uses the
generic ChartGuide attribute and adds no special bucket, histogram,
stack, or timeline grammar.
6.2.2 Domain contributors and baselines
Every finite point, line, text, or polygon vertex contributes its primary
position values to automatic domains before path lowering. A polygon source
separator whose required position is null or non-finite contributes neither a
position nor an appearance value. A polar polygon vertex whose finite raw
position maps outside 0...1 becomes a separator only after domain resolution:
its raw position still contributes to that resolution, but its appearance
values do not contribute to appearance domains or group-style constancy. A
valid secondary endpoint on a Cartesian or polar rule contributes through its
primary scale; when a polar rule omits one secondary coordinate, repeating the
primary coordinate adds no distinct domain value. A short or fully transparent
polygon subpath therefore does not silently change how its finite authored
values participate in an otherwise visible shared guide.
Automatic continuous domains cover the finite visible data. One-sided
quantitative bar and area baselines include zero by default. Line, point,
explicit area bands, rule, rect, polygon, and ranged bar marks fit their
explicit data by default. zero overrides that choice when the scale type
permits it.
If exactly one explicit bound lies beyond every automatic domain contributor,
or there are no contributors, the unresolved side collapses to that explicit
bound. The result is the degenerate one-tick domain defined above; clients do
not ignore the bound or invent platform-specific padding merely because all
supplied marks are clipped.
A continuous scale-sourced guide is visible when its resolved shared scale has at
least one eligible domain contributor anywhere under the selected chart root,
or when the author supplied a domain anchor (min, max, paint mid, or
zero=true). Thus an empty first binding can still own the guide for data
contributed by a later binding with the same scale ID, and an explicitly
bounded empty snapshot retains its truthful authored guide. A fully automatic
scale with no contributors owns only an internal transform placeholder and
shows no axis or legend. A categorical scale-sourced guide likewise requires at
least one resolved category. A member of a closed Enumeration appearance
domain is resolved even when the current snapshot has no row for that member;
positional and facet guides remain eligible-row-driven.
For a shared scale whose bindings all omit zero, the automatic domain is the
union of every contributing binding's domain contribution, so any one-sided
bar or baseline area contributes zero. An explicit shared zero=false disables
that automatic contribution for the complete shared scale; zero=true forces
it. This rule does not depend on composition traversal order.
NumberMin and NumberMax remain input/data validation constraints. They do
not silently become chart domain bounds. Authors use ChartScale(min=...,max=...)
when a stable visual domain is required.
6.3 Shared and independent scales
A scale id is scoped to the outermost rendered chart root. Compatible bindings
with the same ID share their domain and scale configuration, never their
physical pixel range. Each unit chart maps that shared domain into its own plot
rectangle. Thus vertically concatenated price and volume charts can align in
time without sharing screen coordinates.
An unnamed scale is always local to one unit chart. There is no implicit
cross-layer sharing based on coincidental field names. Different IDs create
independent scales, which supports multi-scale layers without a separate Vega
resolve grammar; §6.4 defines their deterministic guide layout.
For one shared ID:
-
all bindings must use the same compatible channel family (`x/x2`, `y/y2`,
theta/theta2,radius/radius2, or one appearance role), effective scalar type, formatted-versus-plain interpretation, ordinary-number-versus-TimeIntervalpresentation class, effectiveMoney,PhysicalUnit, andFormatvalues, and domain, midpoint, zero, and reverse rules; a duration scale therefore cannot share an ID or guide with an ordinary numeric scale even though both use numeric geometry. Shared Enumeration bindings must also name the same effective Enumeration type. Shared FormattedText bindings already use the same package catalog and remain compatible when their other scalar and scale properties match; -
all bindings must use the same automatic physical output-range class. In
particular, point-radius
size(3..18density-independent points) and text-fontsize(10..32) cannot share an ID or one size guide; - See shared-scale compatibility rules. every binding resolves its scale
typeindependently, from its explicittypeproperty or its local inference. Resolved types must match; an omittedtypenever inherits another binding's explicit type; - domain inference includes the primary and secondary endpoint values from every binding carrying that ID;
- declarations may repeat identical `min`, `max`, `mid`, `zero`, and `reverse` properties, while one of those omitted properties is filled from another binding or inferred automatically;
- continuous domain contributions union across all bindings, including the automatic baseline rule above;
- a conflicting shared property invalidates the containing chart.
Paired endpoint fields inherit their primary field's scale and do not repeat the ID. This is how a candlestick's wick and body share time and price domains. The body still has its own pixel geometry, and an optional volume subplot uses an independent y-scale while sharing only the time ID.
See shared-scale compatibility rules.
PhysicalUnit is part of shared-scale compatibility. Reusing an ID remains the
author's assertion that the values represent one comparable quantity, and every
contributor must carry the same effective unit or no unit. Unit text in a
localized field document is presentation only and cannot satisfy or override
that semantic check.
6.4 ChartGuide: generic guides
ChartGuide is the fourth chart attribute. It separates guide presentation
from ChartScale domain and transform semantics. Its position reaches
clients through the Display Template. Its generated value contains
properties in this canonical order:
target,source,position,title,domain,ticks,labels,grid
The grammar is closed:
guide = target "," source *( "," property )
property = position | title | domain | ticks | labels | grid
target = "target=" primary-role
source = "source=" ( "scale" | "value" | "span" | "none" )
position = "position=" ( "auto" | "top" | "right" | "bottom" | "left" )
title = "title=" component
domain = "domain=" component
ticks = "ticks=" component
labels = "labels=" component
grid = "grid=" component
component = "auto" | "none"
primary-role = "x" | "y" | "xOffset" | "yOffset" |
"theta" | "radius" | "color" | "fill" | "stroke" |
"opacity" | "fillOpacity" | "strokeOpacity" | "size" |
"shape" | "angle" | "strokeWidth" | "strokeDash" |
"row" | "column"
target and source are required, keys cannot repeat, generated values contain
no whitespace, and unspecified optional properties use the defaults below.
Source annotations use
@LibertasChartGuide(target=x, source=scale, ...) and
#[libertas_chart_guide(target = x, source = scale, ...)]; unlike
ChartScale.kind, their tokens already match the generated keys.
6.4.1 Placement and validation
source=scale and source=none are declared on the direct field that owns the
named scale-bearing target; the two tokens respectively request its inferred
generic guide or suppress it. The target must equal that field's one primary
scale-bearing role. Secondary endpoints cannot carry a guide. A field may carry
ChartChannel, ChartScale, and ChartGuide together.
source=value and source=span are declared on a direct scalar guide-label
field. That field is guide-only for scaling: it has no scale-bearing
ChartChannel, carries no ChartScale, and may additionally carry only
non-scale roles such as tooltip, detail, or key. Its typed value supplies
the label; target identifies the position binding in the same unit chart.
value is valid for x, y, theta, or radius and creates one candidate at
each guide-eligible row's mapped primary coordinate. span is valid only for a
continuous Cartesian x or y target with the corresponding x2 or y2
binding. It is not valid for bands, points, polar ranges, or appearance scales.
The valid positions and components are:
| Effective guide | Targets | Positions | Optional properties |
|---|---|---|---|
| Cartesian axis | x, y |
x: auto/top/bottom; y: auto/left/right |
title,domain,ticks,labels,grid |
| Polar axis | theta, radius |
auto |
title,domain,ticks,labels,grid |
| Nested offset tier | xOffset, yOffset |
xOffset: auto/top/bottom; yOffset: auto/left/right |
title,domain,ticks,labels,grid |
| Facet header | row, column |
row: auto/left/right; column: auto/top/bottom |
title,labels |
| See guide placement rules. Appearance legend | appearance targets | auto/top/right/bottom/left |
title,ticks,labels; domain and grid must be omitted |
| Value label rail | x, y |
same side matrix as its Cartesian axis | title,domain,ticks,labels,grid |
| Polar value labels | theta, radius |
auto |
title,domain,ticks,labels,grid |
| Span label rail | continuous x, y |
same side matrix as its Cartesian axis | title,domain,ticks,labels,grid |
| Suppressed | every primary target | no position property |
no component properties |
Any property outside its row is an error; it is never silently ignored.
position=auto resolves to bottom for x-oriented guides, left for y-oriented
guides, top for column headers, bottom for legends, and the intrinsic
perimeter/ray layout for polar guides. Automatic row headers appear above each
row in a single-column facet stack, preserving the plot's horizontal space;
in a multi-column facet grid they appear on the left. Explicit left or right
row-header positions remain on the requested side. domain=auto means the
Cartesian axis baseline or polar boundary/ray. An appearance legend's sample
or ramp is intrinsic and is not an axis domain component. For an offset tier,
the domain and ticks repeat within each parent band, and its grid strokes are
clipped to that parent band. Headers have no domain line. ticks=auto means
axis, data-anchored, offset-tier, or continuous-legend tick strokes;
categorical legends and headers have no ticks. grid=auto means Cartesian grid
lines, theta spokes, radius circles, or repeated offset separators. Defaults are
position=auto and every component supported by the inferred guide is auto;
unsupported components default to none. A value or span guide defaults
title=none and every other supported component to auto; source=none
permits no optional property.
Without an explicit ChartGuide, a non-identity primary binding has the same
effect as source=scale with all defaults. Identity scales and xOffset or
yOffset have an implicit source=none; an author may explicitly request an
offset label tier with source=scale. An offset is always a nested band: its
effective scale type is band, whether inferred or explicit; ordinal and
point are invalid because neither defines a child-band extent. Any explicit
declaration targeting a
role replaces that role's implicit guide. Thus a source=value or
source=span label field replaces, rather than supplements, the implicit axis.
There may be at most one explicit declaration for a target in a unit chart.
The inferred scale guide is an axis for Cartesian or polar positions, a nested
categorical label tier for an offset, a facet header for row or column, and
a legend for an appearance role. There are no bucket-date, radar, map, or other
chart-family guide types.
6.4.2 Ownership, layers, facets, and composition
An unnamed scale and its guide are local to one unit. Within one physical
layered plot, compatible bindings with the same nonempty scale ID and channel
family own one guide. At most one of those bindings may carry an explicit
declaration; it controls the merged guide, including source=none. With no
explicit declaration, their implicit scale guides deduplicate and the first
binding in flattened composition traversal order owns presentation text and
side order. Bindings with different IDs remain independent guides; they never
union domains or borrow one another's labels.
An explicit source=value or source=span guide takes candidates only from
the declaring unit's rows; sharing its target scale with another layer does not
join row data across units. Its output positions nevertheless use the complete
resolved shared domain.
Guide sharing, suppression, and position=auto resolve before layout. Every
surviving guide has an owner key (panel,target,unit): panel is its concat
pane, target uses this fixed guide-layout rank:
xOffset,x,yOffset,y,theta,radius,color,fill,stroke,opacity,fillOpacity,
strokeOpacity,size,shape,angle,strokeWidth,strokeDash,row,column. Offset tiers
precede their parent axes so subgroup labels remain nearest the marks, and
facet headers follow positional and appearance guides so their grid-wide title
stays outermost. unit is depth-first flattened chart traversal order inside
the pane and breaks ties between guides with the same target rank. A
shared-scale guide uses the unit and target carrying its sole explicit
declaration, or the first binding when all declarations are
implicit. A value or span guide uses its declaring unit and target rank, never
the guide-label field's source-field position.
A resolved guide takes its title policy and localized title source only from
that owner. For source=scale the source is the owner's target-binding field;
for source=value or source=span it is the guide-label declaration field.
Documents on other shared-scale contributors are ignored. Matching or differing
rendered title text never changes scale sharing or chart validity.
Within one pane, guides on each resolved physical side sort by (target,unit);
the first is nearest the plot and later guides stack outward. Facet copies
preserve that order. Explicit sides do not move responsively. Automatic x
guides stack at bottom, automatic y guides at left, and automatic legends at
bottom; an author uses position to distribute them. No localized text, runtime
value, measured label width, or native traversal order changes this semantic
ordering. The old compound multi-axis fallback does not exist.
Each concat child owns its guide instances and layout even when its scale domain shares an ID with a sibling; concat sharing never deduplicates guides across panels. A faceted positional axis repeats in each pane because each pane owns a pixel range. A facet header appears once per resolved row or column lane, and an appearance legend appears once for the unit's complete facet grid. Layered facet grids share axes, headers, or legends only when the grid-equality and shared-scale conditions in Section 4.2 hold; otherwise each grid renders its guides independently.
A polar layered pane may expose at most one non-suppressed theta guide and one non-suppressed radius guide. Multiple bindings in one family therefore require one compatible shared nonempty scale ID or explicit suppression of every extra independent guide. Every polar layer shares the same center and radius after the one resolved guide layout reserves space.
Accessibility visits outside sides in the fixed order top, right, bottom, left, using inward-to-outward guide order on each side, then intrinsic polar theta and radius guides in that order, then marks. Concat panes and facet panes retain schema traversal order within those stages.
6.4.3 Cartesian, offset, and facet guide geometry
An x guide's domain is the plot's top or bottom edge selected by position;
its ticks point outward and labels are unrotated and horizontally centered on
their mapped coordinates. The nearest painted edge of an outside x label begins
beyond the tick endpoint with a four-logical-point gap; multiline labels grow
away from the plot edge and never back across the domain or grid. A y guide uses
the selected left or right edge; its
ticks point outward and labels are unrotated and vertically centered.
For either family, a grid component crosses the complete plot perpendicular to
the axis at each selected candidate. Scale reversal changes mapped candidate
positions, never these physical anchors. Cartesian source=value and
source=span guides use the same domain, tick, label, grid, and title geometry.
A Cartesian axis title is complete localized schema context, not changing
chart data, and therefore has zero horizontal plot authority. It may use
otherwise-unused space already reserved by its guide, including a rotated side
or corner placement, provided it adds no left/right inset and overlaps neither
guide values nor marks. Otherwise the renderer places it in scrollable safe
content above or below the complete plot-and-guide frame: a top or left guide
uses the upper rail and a bottom or right guide uses the lower rail. A safe-rail
title is unabridged. Titles in the same rail share a centered line in guide
order, separated by a middle dot (·), and wrap using ordinary locale-aware
text layout only when the combined text exceeds the available width. Their
height extends the outer chart scroll content and never reduces the assigned
charting-region height. Rotation, alignment, and opportunistic placement within
already-reserved guide space are renderer-flexible and have no effect on scale,
mark, hit-testing, or accessibility semantics. An axis title alone can never
cause chart presentation failure.
An xOffset guide repeats its complete resolved child-domain tier inside every parent x band; yOffset does the same inside every parent y band. Each child tick and label is centered on its nested band. The repeated domain segment and grid separators are clipped to that one parent band. The offset title appears once and follows the same zero-horizontal-authority title policy above. Offset tiers are inside their parent axes by the layout rank in §6.4.2; they never create an independent full-plot scale line.
A column facet label is centered over or under each nonempty column lane. An
automatic row facet label is centered above its row in a single-column stack;
other row facet labels are vertically centered beside their row. Facet labels
are changing data and retain their allocated header space. The facet field title is stable
schema context and has the same zero-horizontal-authority policy as an axis
title: it may use already-reserved guide or corner space, including a rotated
placement, only when that adds no left/right inset and overlaps neither labels
nor marks. Otherwise it appears once in the upper safe rail for a top or left
facet guide, or the lower safe rail for a bottom or right facet guide. It may
wrap there and extend only the outer vertical scroll content. A facet title can
never reduce a facet pane, create trailing horizontal blank space, or cause
chart presentation failure. Facet labels remain unrotated. Explicit labels=none
or title=none suppresses that component; collision handling never silently
removes an enabled nonempty lane label.
6.4.4 Polar guide geometry
Let P(t,r) denote the normalized screen mapping in §5.3. A theta guide's
domain=auto is the circle through P(t,1). For each retained theta candidate,
grid=auto draws the center-to-P(t,1) spoke, ticks=auto draws a short
outward radial tick beginning at P(t,1), and labels=auto places an unrotated
label beyond that tick on the same ray. The label box lies wholly outward: its
horizontal alignment follows the sign of the ray's x component and its vertical
alignment follows the sign of its y component, with centered alignment on a
zero component. The theta title is horizontally centered below the complete
circle-and-label envelope.
A radius guide's domain=auto is the t=0 ray from P(0,0) through P(0,1).
For each retained radius candidate, grid=auto draws its concentric circle
except at radius zero, ticks=auto draws a screen-horizontal tick crossing the
zero-angle ray, and labels=auto places an unrotated label on the ray's
clockwise, screen-right side. The radius title is horizontally centered above
the complete circle-and-label envelope. These same anchors govern theta and
radius source=value guides. Theme-native font metrics, tick length, and gaps
may differ, but the semantic anchors, orientations, and component ownership may
not.
Mapped theta zero and one are visually coincident. When both survive candidate generation, they remain distinct raw accessibility entries, but the renderer paints one spoke and tick and displays the mapped-zero label; if no mapped-zero candidate exists, it displays the first coincident candidate in semantic order. A radius-zero candidate likewise retains its tick, label, and accessibility entry but has no visible zero-radius grid circle. Polar guide geometry is inert, outside mark hit testing, and not clipped by the circular mark clip. Collision rules may thin labels only at these anchors; they may not rotate or relocate them.
6.4.5 Scale ticks, labels, and typed formatting
Tick generation creates a deterministic ladder of raw candidate sets before
measuring text. For this subsection, let the guide-cadence transform g equal
log10 with inverse 10^t for a log scale and otherwise equal the canonical
scale transform f and its inverse from §6.1. This guide-only decimal basis
does not change normalized log positions, but it makes aligned log ticks
decimal decades instead of raw exp(n) values. For a nondegenerate continuous
quantitative domain, let lo and hi be the raw bounds of the fixed
cadence-planning window defined below, let a=min(g(lo),g(hi)),
b=max(g(lo),g(hi)), and let P contain zero when
it is in-domain and inside that window, plus an authored mid when
it is inside that window. Let E contain each resolved raw domain endpoint
that is inside the planning window. E bounds candidate generation but is not
added to an ordinary candidate tier: a non-aligned endpoint may carry a binary
arithmetic tail and must not displace a denser aligned interval merely because
it bounds the data. An endpoint appears in an ordinary tier only when the
lattice itself generates that exact binary64 value. A transient viewport edge
is not a data or scale-domain endpoint and is never synthesized as a tick. Candidate
spacings are the strictly increasing set S={1,2,5}*10^k for every integral
k; each mathematical spacing is rounded once to binary64, and duplicate,
zero, or nonfinite results are omitted. For one spacing s, the candidate set is
C(s) = P union { g_inverse(n*s) | n=ceil(a/s)...floor(b/s), inverse is valid }
The client canonicalizes negative zero to zero, deduplicates by binary64 raw
equality, and sorts by mapped output position and then raw value. Before
converting ceil(a/s) or floor(b/s) to an integer or iterating, it computes
the inclusive count with saturating arithmetic capped at 4097; a nonfinite
quotient, an index outside the exactly representable integer interval
-(2^53-1)..(2^53-1), or a count over 4096 skips that spacing without
allocation. If the
binary64 width b-a is nonfinite, or no positive representable member of S
has a bounded count, the client goes directly to the terminal P union E tier.
Otherwise it starts with the smallest bounded s, tests successive
representable spacings, and stops after the last spacing not greater than the
finite width or before the next spacing would be nonfinite. It then tests one
terminal tier containing P union E. A nondegenerate ordinary tier must contain
at least two retained candidates; a one-tick tier cannot win over the terminal
endpoint pair merely because it fits. Cadence probing is a pure arithmetic and
geometry operation. For a horizontal guide, the first bounded tier whose
adjacent mapped anchors are at least 64 density-independent points apart
wins. For a vertical or radial guide, the required pitch is the greater of
40 density-independent points and 1.4 times the effective caption-font line
height. When labels=none, every orientation instead uses a 40-point pitch.
No candidate label is needed to make this decision. A degenerate domain has
only its sole raw value. If even the terminal tier cannot fit, guide layout
falls back as defined below. Candidate tiers are transient; only the selected
tier counts toward guide resource limits. Normalized positions always use the
canonical forward transform, so this one finite algorithm covers linear, log,
symlog, and sqrt scales rather than delegating ticks to a platform library.
For example, a compact linear domain 0...10.199999999999... selects the
aligned tier 0,5,10; it does not force the non-aligned maximum into that tier
and then collapse to two endpoint labels. The raw domain and mark geometry
remain unchanged.
DateTime and DateOnly guides use the same 4096-candidate bound, fixed-pitch
capacity, resolved-domain-endpoint priority, and endpoint-only terminal tier.
Their
ordered cadence ladder is: 1,2,5,10,20,50,100,200,500 milliseconds;
1,2,5,10,15,30 seconds; 1,2,5,10,15,30 minutes; 1,2,3,6,12 hours;
1,2,7 days; 1,2 weeks; 1,3,6 months; then
1,2,5 * 10^k years for integral k >= 0. A cadence producing more than
4096 candidates is detected with a saturating count capped at 4097 and
skipped without allocating them. The cadence probe compares raw width w with
the closed ladder using binary64 arithmetic. For nominal step width d, its
probe count is ceil(w/d)+1; the tier fits capacity when this finite count is
from 2 through the fixed-pitch capacity inclusive. Fixed cadences use their
exact seconds. DateTime calendar cadences use 86400 seconds per day, seven
days per week, 28 days per month, and 365 days per year. DateOnly uses the
same day counts in its raw day-ordinal space. These nominal widths are probe
geometry only; they do not redefine Gregorian boundaries. The probe does not
create a calendar, enumerate civil boundaries, resolve a
time zone, or call ICU. Exact UTC or local calendar candidates are enumerated
only once after the cadence has been selected, while constructing the bounded
visible guide run. DST and calendar variation may make that exact run denser;
the selected-run stride rule handles it without changing cadence. Calendar
conversion there is confined to the chart DateTime
range and civil years defined in §5.5. Year cadences stop after testing the first
step greater than the maximum of one, the absolute endpoint years, and their
year span, or before the next step would overflow; no later step can add a new
interior aligned year. The endpoint-only tier follows immediately, so the
search is finite for every supported civil domain.
For utc, sub-day boundaries align in UTC; Gregorian day ordinals are anchored
at 1970-01-01, week ordinals at Monday 1970-01-05, month indices use
12*year+(month-1), and an aligned year is divisible by its cadence. For
time, the same divisibility rules apply to local civil fields. Every aligned
local civil boundary maps to all valid instants: a daylight-saving gap is
skipped and both instants in an overlap are retained. DateOnly tests only the
day, week, month, and year cadences against its timezone-free Gregorian day
ordinal; time and utc are identical for it and never synthesize sub-day
candidates. TimeOnly tests only the millisecond through hour cadences aligned
from raw second zero at midnight, plus each resolved-domain endpoint inside the
planning window. A Number carrying
TimeInterval is not a calendar: it uses the quantitative lattice in raw
seconds and the existing localized duration formatter.
The TimeOnly bound sentinel 86400 never formats as the same midnight label as
raw zero. It uses the client's localized end-of-day label with a locale-shaped
24:00 representation; if a locale does not conventionally display hour 24,
the localized end-of-day phrase remains visible and distinct. The sentinel is
not a row identity and cannot appear in a tooltip or data-anchored value guide.
Temporal label skeletons follow the selected cadence: sub-second and second tiers include seconds and needed fractional digits; minute/hour tiers use localized time; day/week tiers use localized date; month tiers use month and year; and year tiers use year. A sub-day DateTime guide adds a localized date tier at the first visible candidate and every local or UTC day transition. Repeated local clocks add the localized numeric offset or zone abbreviation. If two retained temporal values still render identically, clients add omitted calendar fields, then fractional seconds, then a locale-shaped shortest round-tripping raw-seconds suffix until the labels differ.
Discrete positional axes, nested offset tiers, and value/span guide rails begin
with every resolved candidate in canonical domain or mapped-position order.
Collision thinning tests strides 1,2,4,8,... anchored at index zero and always
retains the last candidate; the first non-overlapping stride wins. A categorical
label may wrap to two lines before thinning. To choose that wrapping, enumerate
every legal Unicode line-break position in the unabridged label, trim break
whitespace from the two painted lines, and measure each resulting pair. Choose
the pair that minimizes the greater of the two measured line widths; an exact
tie chooses the earliest source break. If there is no legal interior break, the
label remains one unabridged line. This same intrinsic two-line rule applies to
categorical axes, value/span rails, facet labels, and categorical legend-entry
labels; it never changes raw identity or accessibility text. On Android, automatic
row labels above the panes of a single-column facet stack instead use the full
pane width: retain the original label and wrap only when that width requires it.
These headings add their measured height to the vertical stack without reserving
side width or reducing plot height. Side and multi-column facet headers retain
the intrinsic two-line rule. Facet headers and categorical
legends never thin: every nonempty facet lane keeps its header and every legend
keeps every resolved domain entry. They obey the fixed geometry in §6.4.3 and
§6.4.7, and the chart falls back if all entries cannot remain visibly
represented.
After cadence selection, the client constructs and localizes exactly the one bounded visible run. It measures that run's final localized, unabridged text with at least four density-independent points of separation and applies the anchored stride-thinning rule when necessary. Thinning may remove ticks and their grid lines from that visible run; it must not change cadence, inspect a second cadence tier, or cause another cadence probe. Native font metrics may therefore thin a selected run differently, but may not select a different cadence. Ellipsis is a last layout fallback and never changes accessibility text.
Guide text never comes from a ChartGuide property: its grammar contains no
literal or format string. On the exact owner field defined in §6.4.2, a title
resolves ChartGuideTitle, then the localized document Name; title=none
omits it. Boolean values use the client's localized true/false resources while
raw false,true remain identity and order. An ordinary numeric guide candidate
is presentation text, not a lossless wire serialization. A nonempty authored
Format on the exact guide owner is authoritative. Otherwise the complete
retained numeric run owns one display cadence: an authored positive tickStep
when present, or the quantitative cadence selected by the automatic ladder.
For every run, the client also considers the smallest positive adjacent raw gap
so candidates closer together than the selected cadence contribute the needed
fractional scale. It first normalizes that gap to two significant decimal
digits, removing any trailing zeros, so the gap's base-ten order does not
inherit a long binary subtraction tail. Starting precision is the greater of
the cadence-derived and normalized-gap-derived fractional scales. A value/span run with no positive gap
uses its associated scale cadence, while a true singleton with neither uses the
locale-shaped shortest round-tripping decimal.
For a positive decimal cadence q, remove trailing fractional decimal zeros
and let p be the remaining number of digits to the right of the decimal
point, clamped to zero; an integer cadence such as 100 therefore has p=0
and never rounds away integer digits. The normalized gap's contribution to p
is likewise its nonnegative decimal scale after the two-significant-digit
rounding and trailing-zero removal above: 0.1 contributes one fractional
place, while 0.25 contributes two. Round every finite binary64 candidate in the
complete run to p fractional decimal places using round-to-nearest,
ties-to-even, remove insignificant trailing fractional zeros, and then apply
the locale's digit shaping, sign, decimal/grouping, and exponent symbols plus
any existing semantic unit decoration. After rounding, fixed notation is
required when the adjusted base-ten exponent is from -6 through 20
inclusive; scientific notation is required outside that interval. This
cadence-derived guard removes binary arithmetic tails such as
0.6000000000000001 from an otherwise ordinary 0.6 guide tick without
inventing unrelated precision. If distinct retained identities collide, raise
p by one for the whole run and retry, for at most 17 increases; the bounded
terminal fallback is the locale-shaped shortest round-tripping decimal. A
client never chooses precision independently per tick. Negative zero is the one
canonical zero identity and displays as zero. DateOnly, DateTime,
TimeOnly, and duration values use the semantic rules above; a duration collision
is disambiguated with the locale-shaped shortest round-tripping raw seconds.
For a PhysicalUnit Number, selected-visible-run collision testing and label
measurement use the complete CLDR-formatted number-plus-unit strings at the selected precision; the
unit formatter cannot introduce a second rounding pass. Visual text uses the
short unit width and accessibility uses the long width defined in §5.5.1.
Enumeration labels use their member documents. FormattedText Bytes use their declared resource
catalog and arguments; an unresolved or malformed essential formatted value
falls back the chart rather than exposing its resource key or argument
diagnostics. Device, VirtualDevice, User, Endpoint, and LanDevice use
the ordinary permission-aware system object-name resolver.
Resolution never changes raw identity, grouping, ordering, or scale mapping.
An unavailable object uses the resolver's existing localized unavailable-name
result; if that contract supplies no safe display value for an essential
label, the chart falls back instead of exposing a raw ID. Fixed program tokens
must be Enumeration or FormattedText. A value that identifies a Libertas
system object must retain its typed object reference and use the ordinary
resolver; it must not copy the resolved or source-language name into a String.
A plain String is visible only when it is genuine runtime/domain/user text and
is displayed literally. Generic words, overflow
indicators, and accessibility phrases come exclusively from client resources.
Native names, scale IDs, channel tokens, and raw object IDs are never synthetic
user-visible fallbacks.
Stable cadence during pan and zoom
The selected guide spacing depends on the chart snapshot, resolved guide and scale/domain/transform, committed viewport span, settled plot length, guide presentation, component switches, and font-line-height/density metrics. It is independent of pan offset/history, locale, calendar/time zone, selection, tooltips, pointer position, animation, and mark paint state.
Panning retains that cadence and alignment. Only aligned candidates and visible priority candidates appear; moving viewport endpoints are not invented ticks. Wider localized labels use the existing anchored thinning rule rather than changing cadence. Marks and guides move together. Live pinch transforms the current presentation; committing a new zoom span selects the new cadence. Gesture completion always commits the exact terminal viewport. Shared physical facet/layer guides retain the same cadence. No interaction changes raw data, source domains, or localization meaning.
6.4.6 Value and span guide sources
A value/span row is guide-eligible independently of mark eligibility through a
two-phase rule. Before domains resolve, both sources require a non-null typed
label and every authored row/column facet binding needed to place the row;
value additionally requires one finite, type-valid, transform-valid raw target
primary, while span requires both target endpoints. Those raw target values
contribute only to the target scale domain, and the facet identities contribute
only to their facet domains. Unrelated position and appearance fields do not
contribute on behalf of the guide. After all mark and guide contributions have
resolved shared scales and panes, the client maps and clips these provisional
candidates; a mapped-invalid or fully clipped result emits no guide entry.
Another required mark position may be null, so an App task can emit a guide-only row that produces no painted data mark. The row nevertheless keeps its ordinary path meaning: it breaks a line or area run or separates a polygon subpath when the missing role ordinarily does so. This is the explicit generic path for task-computed ticks or intervals after an arbitrary transformation; clients never have to infer the transformation from the label.
A value candidate retains its source row's typed raw label identity and mapped
target coordinate. A null label omits that candidate. Failure to localize or
resolve a non-null label follows the essential-text fallback in §8.1; it never
silently removes a semantic candidate. Within one guide instance and facet pane, rows with the same
typed raw label identity and the same canonical raw target identity collapse
to one candidate whose ordering index is the first row; this does not give the
guide an interactive source-row owner. Equal labels at different target values
remain distinct. Candidates are ordered by mapped output position, then
canonical row order, and use the same collision thinning as a discrete scale.
A value guide with no valid semantic candidate resolves no guide instance: it
draws no title, domain, tick, label, or grid. Authors who need an empty bounded
axis use source=scale.
A span guide groups eligible rows within one unit and facet pane by exact typed raw label identity, never by localized text. For each group it maps and clips every valid authored primary/secondary interval to the current plot viewport, normalizes endpoint order, discards empty intersections, and merges overlapping or touching intervals. It emits one guide entry for each connected component at that component's mapped midpoint. It does not bridge a visible gap, average source coordinates, derive a time distance from the label, or invent any domain coordinate beyond the raw endpoints contributed by the pre-domain rule above. Components order by mapped midpoint and then the first contributing row. Zoom recomputes clipping, components, and collision from the unchanged raw snapshot. The first and last visible candidates remain priority labels.
Each emitted span entry exposes its localized label plus the inverse-mapped raw bounds of that visible clipped connected component, ordered in the target scale's input direction. It does not announce the unclipped authored bounds or an arbitrary contributing row. Original row intervals remain available only through their ordinary data and tooltip presentation. A span guide with no nonempty visible connected component resolves no guide instance.
This is the complete generic rule for sparse or task-transformed interval
labels. A tooltip field can affect a guide only when it explicitly carries
ChartGuide; adding an unrelated tooltip can never change guide semantics.
6.4.7 Legend lowering and layout
Categorical legends contain one entry per resolved domain value in mapped
output order, including reverse. Sample geometry is closed and independent of
runtime text: color and fill use a filled swatch; stroke, strokeDash,
and strokeWidth use a line; shape and size use a point symbol; angle uses
an asymmetric directional needle; opacity and fillOpacity use a filled
swatch over the chart surface; and strokeOpacity uses a line over that
surface. A text mark's size legend instead uses the localized client resource
ChartTextSizeSample rendered at the mapped font size; the resource selects a
representative glyph appropriate to the active writing system. Each sample uses
the same resolved appearance output as its marks for the guide's target
channel. A legend sample isolates that target: every non-target appearance
component uses the accessible theme-native default for the owning mark and
never borrows a value from an arbitrary row or another data-driven binding.
Thus a point-size guide uses the default point shape and paint, a shape guide
uses the default point radius and paint, and a stroke-width guide uses the
default solid, fully opaque semantic foreground stroke. This isolation changes
only sample presentation; it never changes the mark's combined appearance.
Continuous color, fill, and stroke guides use the contract's portable
color ramp. opacity and fillOpacity use a filled alpha ramp;
strokeOpacity uses a line alpha ramp. A point-size guide places the owning
point shape at every selected candidate, a text-size guide places the localized
sample above at every candidate, an angle guide places an asymmetric needle at
every candidate, and a stroke-width guide places a line at every candidate.
Those samples use the mapped physical radius, font size, clockwise angle, or
line width. The continuous rail's scale-output axis is exactly 160 logical
points long in both orientations; Android uses density-independent points and
iOS uses points. Cross-axis swatch thickness, tick length, and component gaps
remain theme-native and measured, but cannot alter that 160-point mapping.
Their selected raw candidates provide ticks and labels. The sample
or rail is intrinsic and remains present independently of title and labels
component switches; accessibility still exposes its target role and unabridged
raw domain.
Every left/right categorical legend is one vertical entry column ordered top
to bottom. An above/below categorical legend uses stable greedy row packing:
measure every complete entry with the final localized text and accessibility
font, retain domain order, place the longest prefix whose entry widths plus
fixed 8-point inter-entry gaps fit the available width, then repeat on the
next row. At least one entry is attempted per row; an individual entry wider
than the available width is an essential-label layout failure. Entries never
split, clip, reorder, or use balancing/look-ahead. After packing, the widest
produced row defines the intrinsic width of the complete above/below guide
group. Center that group within the available chart width, then retain one
shared leading edge for every row inside it; shorter rows are not centered
independently. Multiple blocks on the left or right form outward-stacked
columns. Multiple blocks above or below share a
second stable greedy horizontal flow instead of each claiming a complete row.
First measure each block at its ideal unwrapped width; if that exceeds the
viewport width, remeasure that block at the viewport width using its internal
entry flow above. In resolved guide order, place the longest prefix of complete
blocks whose widths plus fixed 16-point inter-block gaps fit, then continue on
the next row. A block title remains attached above its entries, so two compact
blocks such as Line and Result may sit head-to-head on one row without
mixing their entry identities. There is no balancing or look-ahead at either
packing level. Extra rows increase the chart's outer scroll height instead of
consuming plot width or creating a private legend scroller. A vertical
continuous rail maps normalized
scale output zero at the bottom and one at the top; a horizontal rail maps zero
at the left and one at the right. reverse therefore moves raw values without
changing the legend geometry. A visible categorical or vertical-rail title sits
above its legend; a horizontal-rail title sits at its screen-left leading edge.
It precedes entries in accessibility order. Entries never reorder, wrap into
another row or column, or disappear.
Labels within one entry may use the two-line categorical wrapping rule. A
legend must fit its non-scrolling cross-axis: top/bottom legends must fit the
chart viewport width, while a left/right legend that prevents the minimum plot
width joins the deterministic temporary overlay under §6.4.8. A legend may
exceed the visible viewport vertically under §6.4.8; that is ordinary
outer-scroll overflow, not layout failure. There is no legend-only scroller,
entry clipping, semantic thinning, or maximum legend height.
Categorical entry density is portable rather than platform-native. Entry
labels use the client's 11-point small/caption style and the intrinsic two-line
rule above. The sample-to-label gap is 5 logical points and adjacent vertical
entries have a 4-point gap. Filled swatches are 10x10; stroke samples are
24x10; point-shape, point-size, and angle samples use a 24x24 canvas. The
mapped point-size radius remains 3..18, the shape radius is 5, and the angle
needle radius is 9. A text-size sample remains intrinsic to its mapped
10..32 font size rather than being padded to a fixed sample box. Android uses
density-independent points and its labelSmall style; iOS uses points and its
caption2 style. Native glyph metrics may differ, but clients may not add a
different entry box, row spacing, or sample padding that changes guide fit.
Any explicitly admitted automatic shape-label adapter uses one common inside-first pipeline. This includes a chart-family rule that lowers an automatic categorical guide and may include a future declaration that explicitly associates authored text with one painted shape. Semantic eligibility and stable owner/label identity resolve before screen layout. Localization, intrinsic two-line wrapping, and native glyph measurement then occur exactly once at the explicit text boundary; the pure placement tier must not call ICU, relocalize, reshape, shrink, ellipsize, rotate, or otherwise change that complete label block. Each admitted shape family supplies only its final painted geometry, preferred in-shape anchor, padded containment test, boundary anchor, and permitted outside-label adapter.
Placement is resolved independently in the adapter's specified stable order; a guide-lowering adapter uses resolved category order, including its declared reverse. The complete measured label plus the family rule's padding first attempts the preferred in-shape anchor. A fit comparison permits at most one-half physical pixel of raster error. A fitting label remains inside its own final painted shape; one candidate's failure must never externalize a fitting sibling. Only failed candidates enter the outside collision plan and reserve outside space. That plan starts from the painted shape boundary and moves each label only as much as the family placement and collision rules require. Residual container or screen width is outer slack: it must not stretch the plot-to-label gap, lengthen a leader, resize the already solved mark, or cause a second classification pass. Any native-pixel placement is canonicalized at most once before obstacle and route preflight; paint consumes those identical stored positions rather than independently rounding the plot, labels, or leaders after spending the half-pixel tolerance.
Every automatic label is painted exactly once, whether inside or linked outside, before any preview shell applies its documented outer clip. Those label views are decorative and accessibility-hidden: the existing canonical mark accessibility row, which already includes appearance-channel values, remains the label's single semantic owner. Source presentation changes only after a complete mixed placement succeeds. A guide-lowering adapter then suppresses its ordinary guide and restores that complete guide on failure; a future owner-associated authored-text adapter must define its own literal fallback. If ownership, complete text, containment, collision, clipping, or safe-area requirements cannot produce a plan, opportunistic automatic labels never reject an otherwise renderable chart. An adapter must be explicitly specified. Clients must not infer label ownership from layer order, proximity, matching display text or keys, or a shape that merely looks geographic. Lowering changes guide geometry, not semantic resource ownership. A suppressed guide reports the same entry, title, sample, label, and total primitive usage as its ordinary presentation, so automatic placement cannot bypass a guide or chart resource limit.
HARD INVARIANT: every fullscreen polar family has one orientation-owned
maximum mark radius before mark, label, or legend layout. In portrait, let W
be the cutout-safe physical-axis width expressed in logical coordinates and
let s be the display scale. That width owns the natural outer coordinate
radius after the universal eight-logical-point paint inset on each physical
side: S0=(W-16)/2, equivalently
S0Pixels=(WPixels-16*s)/2. This inset is shared by Radar, pie, donut, ARC,
and every other
portrait polar mark; it is neither a label reservation nor a Cartesian height
cap. A Cartesian nominal-height cap or floor never reduces S0. The
portrait logical composition is W x Hp, where
Hp=max(W,resolvedPolarMinimumHeight,completePolarFootprintHeight). Thus real
vertical host space and a complete automatic-label or title footprint may move
or enlarge the composition around the width-owned mark, but may not substitute
an inscribed label square or unrelated height for the natural-radius owner. An
eligible automatic-label adapter may still select a slightly smaller largest
feasible painted radius through its documented exact bounded planner. In
landscape, the physical height H below the one top bar and above the stable
bottom navigation/safe-area inset owns the natural radius S0=H/2 and the
base mark square is H x H. An outer host that already excludes an inset must
not subtract it again. Radar, pie, donut, annulus, rose/radial ARC, and polar
line/point/rule/text all use this same coordinate-family guard. Preview retains
its established intrinsic polar height and documented outer-shell clip; it
does not enter a fullscreen sizing branch merely because the application is
portrait or landscape. Planner, paint/clip, grid, hit testing, tooltip and
accessibility geometry consume the same radius, composition and origin.
An opportunistic fullscreen-landscape polar end legend replaces that fixed
vertical gap with a measured distribution. For each categorical block, measure
the title and every complete entry intrinsically. The title remains attached to
the first entry by the 4-point minimum gap, and adjacent entries retain that
same minimum. Let N be the entry count and B be the resulting intrinsic
block height. When B <= H, divide the surplus H-B equally among exactly
N+1 flexible spaces: one before the block, one after it, and one between each
pair of entries. Round each absolute placement boundary to the nearest physical
pixel; do not round and accumulate independent gap sizes. All categorical
blocks therefore occupy the same measured charting height while preserving
their own entry counts. A continuous block sharing that rail is vertically
centered. When B > H, rows retain their intrinsic geometry and the automatic
end placement fails the height-fit test below; clients never compress, clip, or
overlap rows to manufacture a fit. This distribution applies only to the
opportunistic end rail. Preview, portrait/bottom flow, and explicitly authored
side guides retain their ordinary intrinsic geometry.
Facet headers and outside guide value lanes reserve space before the plot is
mapped. Cartesian axis titles use only already-reserved guide space or the
vertical safe rails of §6.4.3; they never enlarge a left/right lane. On each
physical side, the first guide in the owner-key order of §6.4.2
is nearest the plot and later guides stack outward. Grid geometry is clipped to
the plot and paints first; axis domains and ticks paint next; data marks paint
in schema order; guide labels and titles paint last outside the mark clip.
In fullscreen, an automatic appearance legend owned by a polar plot uses an
orientation-responsive preferred side: bottom in the physical portrait
viewport and physical screen right in the physical landscape viewport. The
assigned polar square's equal dimensions are never an orientation signal. A
resolved bottom appearance legend also
participates in this fullscreen adaptation; explicitly authored top, left,
and right remain exact. The landscape end choice is stable: a responsive
retry never rewrites it or converts it into the temporary side overlay. The
measured fit exception below is the only automatic end-to-bottom fallback. The
plot uses the complete height below the single top bar. Before any chart-family
layout, W and H exclude platform safe-drawing and physical display-cutout
insets.
This adaptation is guarded by coordinate family as well as presentation and orientation: it is active only for an interactive, fullscreen-filling, landscape polar pane with a positive finite charting height. A client must use one effective-position classification, including resolved-bottom-to-end pinning, for both end-legend preflight and legend rendering. It may not infer end occupancy independently in those two phases. Radar, donut, radial bar, polar point/line/rule/text, and future polar marks all use this same coordinate-family guard; Cartesian, concatenated, faceted, preview, portrait, and transition passes do not enter it.
For an unfaceted landscape polar plot without an automatic outside-label
footprint, the known
charting height H owns polar size before the end legend is measured. The
client selects one guide cadence, measures its actual painted labels, and uses
only the required vertical label/title reservation V to solve the circle as
R=H/2-V. Horizontal label widths never become matching top and bottom insets
and therefore never shrink this height-derived circle. Because the centered
polar renderer applies V on both axes, the complete chart canvas is at least
H wide. If either side's actual painted text extends beyond that base, the
canvas grows symmetrically by the larger excess so width cannot silently
recompute a smaller R through min(width,height).
The client then places this fixed chart footprint immediately to the physical
left of the measured right legend with a 16-point middle gap and centers
their combined group in W, balancing the unused outer space. The right legend
is therefore moved inward toward the chart rather than consuming the whole
residual width. Legend measurement may translate that already-solved group,
but must never determine, widen, narrow, or validate the polar circle.
Physical-end placement is opportunistic. It is used only when the complete
chart footprint + 16-point gap + intrinsic right legend fits W and the
intrinsic, pre-distribution right-legend height fits H, using the universal
half-physical-pixel raster tolerance. A fitting categorical guide is then
distributed through H by the rule above. If either comparison fails, the
automatic appearance legend moves to bottom and uses the ordinary horizontal
greedy flow. The fixed
height-derived chart footprint is not reduced, the guide is not truncated, and
the fit failure is not reported unless the resulting bottom layout also cannot
represent essential content. A viewport, font, locale, or projection change
retries end placement from a fresh measurement. Explicitly authored right
guides do not use this automatic fallback.
Once the plot already consumes that maximum physical height, the 200-point
interactive diameter is a preference rather than a reason by itself to move
the legend; the two-dimensional end-guide fit above may still select bottom.
Positive geometry and a valid polar-guide plan remain mandatory. Preview
retains the ordinary automatic bottom default so the fixed summary does not
depend on application-window orientation.
An eligible ARC partition's automatic outside-label subset from §11.4.1 is nearer the
plot than every automatic appearance legend. It is reserved and measured
first; fitting in-shape labels consume no outside space. The ARC adapter's
labels, leaders, and complete polar frame form one tight chart footprint. That
footprint is the union of final painted bounds, not a proposal or flexible
container, so an outer_radius < 1 cannot turn unused polar-frame space into
leader length. Center the tight footprint in the available safe width without
changing its internal geometry. It then
participates in the two-dimensional opportunistic-end test above. A
lower-priority automatic right legend is placed outside the footprint with the
ordinary 16-point middle gap when the composition fits; otherwise that legend
moves to bottom. It may not hide, overlay, reorder, truncate, or replace inside
or linked labels to make itself fit. Explicit non-bottom legends retain their
authored side and ordinary owner-key order.
Polar grids follow the existing spoke/circle rules, including suppression of a
duplicate theta-one spoke and a zero-radius circle. A renderer may thin, wrap,
or ellipsize only by the rules above. If the resolved lanes cannot leave the
minimum plot width required by §6.4.8 or an essential label cannot be represented,
the complete chart follows the failure policy in §10 rather than overlapping or
relocating an explicit guide.
Guides are semantically inert. They do not compete in mark hit testing, own a
tooltip or source row, trigger a typed request, or become a selection. A value
or span label remains guide-inert even though rows supplied it; authors use a
text mark when the label must be interactive. Accessibility traverses chart
title, guides by physical side and inward-to-outward order, then marks. It
exposes every unabridged guide title and raw candidate through the typed
localized rendering above, including candidates thinned visually; a span entry
also exposes its target interval. Legends expose the appearance role and
sample meaning, and facet headers label the pane they govern.
Guide projection is bounded before native allocation: at most 256 resolved
guide instances under one chart root, 4096 semantic candidates for one guide,
16384 guide entries in total, and 32768 guide primitives in total. A title,
label, tick, grid line/spoke/circle, domain stroke, or legend sample is one
guide primitive. These counts are separate from the data-mark primitive budget;
responsive thinning may reduce native objects but cannot make an over-limit
semantic guide valid. Text retains the existing 4096-byte per-value limit.
6.4.8 Deterministic responsive guide overlay
The containing chart shell first establishes one usable visible viewport after safe areas, system bars, and persistent application chrome. Chart controls use the upper title safe rail and do not reduce or overlay the charting region. Android measures in density- independent points and iOS in points.
Preview height. A preview's charting-region height R follows its pane.
Let Wp be the width assigned to the chart's plot-and-guide frame before any
guide lane, which is the preview shell width Wv for a unit, layer, facet, or
vconcat and the pane width P below for an hconcat child; let Hp be the
visible height of the nearest enclosing pane under the Display Template's
height tokens. The family aspect a
(width over height) is 1 for a polar chart, 4/3 for a Cartesian chart while
the pane's width condition is compact, and 16/9 for every other width
class. The cap share c is 0.5. A Display Template
chart rule on the chart root may replace
a with aspect (square is 1, 4:3, 16:9) and c with max_height
(small 0.25, medium 0.40, large 0.60):
R = max(180, min(Wp / a, c * Hp))
R = 180 when max_height is "thumbnail"
The 180-point floor wins over the cap: a short pane, such as a phone in
landscape, still gets the V1 region and the excess is reached by the
enclosing scroll. The preview viewport keeps the V1 proportion, assigning
three quarters of its height to that region: its height is exactly
4R / 3, so the floor reproduces the V1 240-point viewport. R is
re-evaluated when the pane's width, height, or width class changes; that
re-layout is a new presentation of the same snapshot and never changes the
data. A non-finite or zero Hp leaves only the floor and the width term.
Neither this rule nor chart changes fullscreen sizing, and a Chart3D
preview keeps its own fixed 240-point viewport.
In fullscreen, let W,H be the current usable
viewport width and height. The assigned charting-region height R is:
landscape or square (H <= W): R = 0.75 * H
portrait (H > W): R = min(0.75 * H, W)
The portrait width is the deterministic stand-in for the height visible after
rotation to landscape. This supersedes a fixed 300-point portrait cap while
still preventing a tall phone from stretching one plot arbitrarily. An
unfaceted unit/layer and each concat child use exactly R. For an hconcat,
let Wv be the current usable visible viewport width after shell insets. Every
child receives cross-axis pane width P = max(Wv, 280 points); the client must
never divide Wv by the number of children. Preview keeps the root title and
Chart Explorer control in a shell rail of width Wv, outside composition
clipping, then clips later panes. Fullscreen uses the same per-pane rule and one
horizontal composition scroller, so every complete pane remains reachable in
portrait, landscape, and after rotation. A facet chart assigns R to its
complete pane grid, subtracts authored row gaps, and divides the remainder
equally among facet rows. An established per-pane geometry minimum may increase
the complete grid beyond R; panes never collapse to satisfy the reservation.
A vconcat likewise gives each child its own R, so later children remain
reachable through the same outer vertical viewport.
Chart title and appearance legends are outside that assigned charting region.
They never shrink or stretch it. In particular, a left/right legend taller
than R increases the complete scroll content height while the adjacent plot
remains R; top/bottom legends precede or follow it normally. Preview and
fullscreen have different shell semantics. A preview is a bounded, gesture-neutral
summary: excess legend or composition content is clipped by its 4R / 3
preview viewport and is reached only by opening Chart Explorer. It has no internal
vertical or horizontal scroller. Fullscreen Chart Explorer owns exactly one
vertical chart viewport over the complete title, charting regions, and legends;
its excess content remains reachable through that outer scroll. A renderer must
not add a nested legend scroller, cap legend height, discard entries, or fall
back merely because complete vertical content is taller than either viewport.
Initial presentation is data-focused. Preview places its fixed control rail and first charting region at the leading edge and does not establish mutable scroll state. In fullscreen, starting from semantic scroll position zero, the client makes the minimum nonnegative vertical displacement that fully exposes the first charting region. This leaves up to the permitted first quarter available for preceding legend/title content. If that charting region itself is taller than the viewport, its top aligns with the viewport top. Subsequent measurement, rotation, Dynamic Type changes, and recomposition never reset a fullscreen user's scroll position; a new immutable chart snapshot establishes a new initial presentation.
The preferred minimum usable plot width is 200 logical points: Android uses
density-independent points and iOS uses points. For each plot or facet pane,
let C be the horizontal width assigned to its plot-and-guide frame after
shell padding and concat/facet allocation but before any left/right guide lane
reserves space. Its required usable plot width is M = min(200, C). M is a
post-guide plot measurement, not the outer chart width. A container narrower
than 200 points is therefore valid and the chart must not fall back merely
because its plot cannot reach 200; it must still reclaim guide-reserved width
until it reaches C or exhausts the policy below. Integer-pixel rasterization
may satisfy M at the nearest physical pixel, with at most one-half physical
pixel of error. The renderer measures final localized text with the user's
active accessibility font metrics before choosing presentation. Native font
metrics may therefore change whether M is crossed, but may not change the
policy below.
An interactive polar pane applies that same preference to its equal-aspect
diameter. A bounded, gesture-neutral preview instead requires a strictly positive
post-guide diameter and validates every final polar label against measured
bounds and collisions. It does not reuse the interactive 200-point diameter:
at the 180-point preview floor, any outside polar label would make that
condition structurally impossible before candidate thinning could run, and a
taller responsive R does not reinstate it. This preview rule
never removes a semantic guide candidate; thinned candidates remain in
accessibility, and opening Chart Explorer restores the interactive diameter
preference.
Facet columns are responsive allocations, never intrinsic 200-point widgets.
After subtracting the one resolved row-header lane from the facet grid's
pre-guide width, the renderer divides every remaining point
equally among the nonempty facet columns in source-resolved order. A one-column
facet therefore consumes all remaining width and leaves no trailing blank
column. That equal share is the pane's C; a share below 200 is valid under
M = min(200, C) and cannot itself cause fallback. Column headers are centered
within the same equal shares. Their existing complete-label and two-line
categorical wrapping rules still apply; the renderer does not restore an
intrinsic pane minimum to make a header fit.
Every guide first participates in the ordinary reserving layout in §6.4.7. If
that complete plan leaves a plot narrower than M, every Cartesian
axis, offset tier, value rail, or span rail resolved to the physical left or
right side changes to temporary overlay presentation together, and the client
remeasures. If that stage-one plot is still narrower than M, every
visible appearance legend on the left or right side joins the same overlay,
and the client remeasures a second and final time. A legend never moves when
the axis-only stage already restores the minimum. Top and bottom guides remain
outside because converting them cannot reclaim horizontal width. Facet headers
and polar guides never move or disappear. This is a mandatory runtime policy,
not author metadata: no ChartGuide property enables, disables, or reorders
it. If the final Cartesian plot or interactive polar diameter is still narrower
than M, or any final plot has nonpositive height, the complete chart follows
the failure policy in §10. Apart from the closed gesture-neutral polar-preview
rule above, there is no device-size exception beyond M = min(200, C), partial
conversion within either stage, orientation-specific guess, or additional
platform heuristic.
Exactly one chart-control set belongs to one rendered chart root. A facet pane,
layer unit, or concat child never creates another set; its viewport actions are
coordinated through that root set. In fullscreen, one guide control is present
whenever the runtime policy places at least one resolved guide in overlay mode.
Preview exposes only its Chart Explorer presentation action and never guide or
zoom controls. Every control is a visible 44 by 44 point minimum touch target.
The upper safe rail packs
complete localized guide titles into its leading remainder and the one control
set at its physical trailing edge. When both fit they share a row. When they do
not, the title or controls wrap to additional complete safe-rail rows in reading
order; that vertical growth belongs to the outer chart scroll and can never
reduce, cover, or reject the plot. Controls appear in this fixed physical
left-to-right order when present: Guides, Zoom out, Reset, Zoom in, then
Fullscreen or Close. Hidden/inapplicable controls leave no empty slot. The
guide control's closed and open accessibility names use the localized client
resources ChartShowGuides and ChartHideGuides; no schema token or native
name is shown. Activating it opens or closes every overlay guide atomically.
Guide controls are operable only in fullscreen Chart Explorer. Pointer,
keyboard, switch-control, and screen-reader activation are equivalent.
While open, an overlay guide uses the same resolved candidates, component switches, side, owner order, localization, and accessibility entries as its outside form. Its domain remains on the selected plot edge, but ticks point inward and its label and title lanes mirror inward over the plot instead of reserving exterior space. On one side the first axis, offset, value, or span owner is nearest that edge and later such owners stack farther inward. Side appearance legends retain their ordinary vertical block geometry and authored side, and stack inward after those mirrored lanes in their resolved owner order. Their complete title, samples, labels, and accessibility entries remain unchanged; legend labels do not gain the axis-label ellipsis permission below. Overlay components paint after marks on a theme surface that preserves text contrast and may temporarily obscure data; they never remap, resize, pan, select, or hit-test marks. Existing deterministic collision rules still apply inside the fixed plot bounds. Ellipsis is permitted only for candidate labels in this temporary overlay presentation; outside-guide labels, legend labels, and every guide title remain unabridged. For a horizontal overlay, a candidate's label cell extends horizontally from the midpoint to the preceding retained anchor to the midpoint to the following retained anchor, clipped to the plot; the first and last cells use the corresponding plot edge. Its height is the ordinary one- or two-line label lane. A vertical overlay uses the analogous y interval for cell height and the mirrored outside-guide lane for cell width. Mirrored lanes retain their measured outside widths and owner stacking, then intersect the plot; no equal-width or fixed-width lane is invented. Each cell reserves the ordinary four-point inter-label gap, split equally across its two anchor-axis sides.
A categorical overlay label first wraps to at most two lines. Using extended
grapheme clusters, the renderer puts the longest prefix that fits on the first
line, preferring the last Unicode line-break opportunity within that prefix;
leading and trailing break whitespace is not painted. The remaining text forms
the second line. A noncategorical label remains one line. If a resulting line
does not fit the cell width, the renderer removes trailing extended grapheme clusters
until appending the single Unicode ellipsis character … fits. It never emits
three dots, splits a grapheme cluster, changes the semantic candidate, or uses
the truncated text for collision, grouping, or identity. The complete
unabridged localized value remains the accessibility label. If the required
one- or two-line font height does not fit the cell height, the ellipsis alone
does not fit the cell width, a measured mirrored lane has no nonempty
intersection with the plot, or an unabridged title cannot fit its overlay lane,
the complete chart follows the failure policy in §10.
A side appearance legend keeps its intrinsic vertical extent while overlaid.
If it is taller than the assigned charting region, the legend extends the one
outer vertical chart viewport exactly as it did outside the overlay; the plot
height remains R, and there is still no legend-only scroller or height cap.
The left and right overlay stacks, including their preceding mirrored lanes,
must fit without crossing. Otherwise the complete chart follows the failure
policy in §10. Closing the overlay removes their visible paint but not their guide
resource counts; it also removes their direct accessibility entries until the
localized guide control opens the overlay again.
The overlay remains open until the user activates the guide control again, uses the platform accessibility dismiss action, or taps outside the chart. It has no timeout. A layout change reruns the complete policy and closes the overlay; if the guides now fit outside they return outside, otherwise the closed overlay affordance remains. Overlay mode does not reduce guide candidate or primitive resource counts and cannot rescue a semantically over-limit chart.
7. Why four attribute families are sufficient
Vega-Lite includes data loading, transforms, mark configuration, encoding, scales, guides, composition, selection, and expressions. Libertas already has a typed protocol and an executable App server, so most of those grammar families would duplicate stronger existing mechanisms.
| Vega-Lite concern | Libertas design |
|---|---|
| Data source and loading | The typed protocol response payload. |
| Calculate, filter, lookup, join | App task code. |
| Aggregate, bin, time unit, window | App task code emits final rows. |
| Sort | Server list order or the order channel. |
| Stack | Explicit server-computed x,x2 or y,y2 bounds. |
| Regression and density | Server-computed points. |
| Geographic projection | Server-computed Cartesian geometry. |
| Mark | Chart primitive token. |
| Encoding | ChartChannel on direct scalar fields. |
| Scale and resolution | Automatic defaults plus optional ChartScale. |
| Constant style | A repeated literal data field with an identity scale. |
| Layer, concat, facet | Chart compositions and row/column channels. |
| Axis, legend, facet header, and data-anchored label | ChartGuide with scale, value, or span sources. |
| Parameters and data-changing selection | A typed next protocol request and a new snapshot. |
Consequently, separate ChartTransform, ChartSort, ChartStack,
ChartStyle, ChartAxis, and ChartLegend attributes are intentionally not
part of the minimum design.
Time is not a privileged client-side transform. An App task may emit a native
DateTime/DateOnly position for the ordinary automatic temporal scale, or it may
project time into any bounded numeric coordinate its domain requires. In the
latter case, a typed DateTime, DateOnly, Enumeration, FormattedText, or Libertas
object-reference field can independently supply source=value or
source=span guide labels, including on guide-only rows whose unrelated mark
geometry is null. The client therefore localizes truthful typed labels
without reverse-engineering calendar meaning from synthetic geometry. This
ability to execute arbitrary application code before returning one typed,
chart-ready snapshot is a defining Libertas boundary, not a missing client
transform language.
8. Attribute resolution and references
Libertas attribute lookup returns the nearest declaration for one attribute name. Conceptually, lookup considers:
- the use-site or local field;
- the same-named inherited table field;
- the referenced named type.
It does not union values from several declarations. Therefore a local override must contain the complete channel set:
referenced type: ChartChannel=tooltip
use site: ChartChannel=x,tooltip correct
use site: ChartChannel=x tooltip is intentionally replaced
Different attribute names compose normally. A local ChartChannel can use a
referenced type's ChartScale or ChartGuide when no nearer same-named
attribute replaces it. A local ChartGuide is always a complete declaration;
its properties never merge with an inherited guide.
Foreign compatible types preserve official chart attributes unless the local schema supplies a valid explicit override. Both language parsers must extract the same local declarations and references; Studio's shared validator applies the portable placement, inheritance, canonicalization, and override rules.
8.1 Localization and schema-document dependency
Chart semantics have no schema-document dependency. A client must be able to parse and validate geometry, domains, grouping, and interaction identity from only the language-neutral schema and payload:
schema + ScriptValue snapshot -> semantic ChartSpec
semantic ChartSpec + resolved document? + client resources -> presentation
The second step reuses the same already-resolved Libertas document used by the
ordinary data view, using the existing
localized package document model.
Chart support adds no document fetch, no chart-specific document format, and no
document reference inside Chart, ChartChannel, ChartScale, or
ChartGuide. Document text can never change data meaning or make an otherwise
invalid chart valid.
The presentation dependency is deliberately small:
| Existing localized source | Chart use | Required? |
|---|---|---|
Type DisplayName |
Chart or composition title | No; omit an empty title. |
Type Description |
Long help and accessibility context | No; omit when empty. |
Field/member localized Name |
Guide, legend, and tooltip caption | No for geometry; omit an optional caption when empty. |
Field/member Description |
Tooltip help and accessibility detail | No; omit when empty. |
Enumeration member Name and Description |
Fixed category label and help | Required only when that Enumeration is visibly encoded. |
| Existing Libertas object resolver | Dynamic display name for a Device, VirtualDevice, User, Endpoint, or LanDevice reference |
Required only when that reference is visibly encoded. |
Package document StringResources |
Render a FormattedText Bytes tuple with Libertas Message Format 1 (LMF1) |
Used when supplied; schema validation checks the declared package keys but does not prove every runtime tuple key. |
| Flattened custom document property | Static App-specific chart wording | Never required by the minimum grammar and never a runtime-token map. |
| Client localization catalog | Generic actions, fallback/errors, mark names, and accessibility grammar | Required of the client, not of the App schema. |
Resolution uses the platform's existing locale priority and default-locale
fallback. Enumeration values use their localized member documents. Every
Libertas object type uses its existing dynamic, permission-aware client
resolver and ordinary unavailable-reference behavior; chart code must not
invent a second resolver or require an App-supplied label. Names may change at
runtime without changing the raw reference identity. If an optional localized
value remains empty, the client omits it. It must not synthesize a caption from
NativeName, a scale ID, channel token, Chart value, source-code identifier,
or hard-coded English. If an essential visible category cannot be localized or
resolved after normal fallback, the client follows the chart failure policy in
§10 instead of presenting an inaccessible or misleading chart.
The minimum chart introduces no mandatory custom document keys. Existing
DisplayName, long description, field names/descriptions, and Enumeration
documents are sufficient. An App may optionally provide localized flattened
properties such as ChartGuideTitle, ChartTooltipName, ChartEmpty, or
ChartAccessibilitySummary when the established text is not appropriate. The
corresponding ordinary name/description or a built-in localized client resource
remains the fallback; these properties never affect chart validation. They are
static wording attached to the documented schema node. A client must never use
them as a dictionary for arbitrary plain String payload values.
Formatted runtime text has a separate, closed path. The App schema declares one
package-owned StringResources catalog, and its package document contains the
localized LMF1 templates. The emitting task already identifies that package;
the tuple therefore carries no type owner or resource FQN. Schema validation
checks declared keys and default-document membership but does not prove every
runtime tuple key. At chart
presentation time, an unresolved or malformed optional formatted value is
omitted and an essential one invokes the chart failure policy in §10 for the
complete chart. Resource keys, tuple bytes, and typed argument diagnostics may appear in
bounded developer diagnostics or the ordinary data view's established fallback,
but never as chart presentation text.
A FormattedText Bytes payload is the canonical encoding of one key plus ordered
typed Libertas Message Arguments, including an empty argument list. The client
renders the localized template with its custom LMF1
renderer, including zero-based positions, number/date styles,
plural/select/selectordinal/choice, and Libertas types such as
LibertasDevice and LibertasEnum. The encoded argument type takes precedence
over a conflicting template type, and LibertasEnum uses its style as the
actual Enumeration type name. See
Libertas Message Format.
Formatted chart values never use a String containing Base64. Encoding only to decode again at render time provides no benefit, while the Bytes host creates a sharp distinction from literal text and makes authoring mistakes less likely.
The LMF1 number or date style is the complete formatting instruction for that
message argument; field-level Format and chart scale formatting do not
override it. A locale, translated template, number style, or resolved object or
Enumeration label may change text measurement and therefore label collision,
guide reservation, legend flow, or chart presentation failure. It cannot change
the data-derived plot geometry, domain membership, category ordering, shared
scale compatibility, or interaction identity.
The payload never contains a template. The raw key/blob remains the category, grouping, ordering, and interaction identity. Two values that render to the same text therefore remain distinct.
Every user-visible string obeys one of these rules:
- static App-authored chart wording comes from the resolved document or an optional custom document property;
- generic client wording comes from the client's Libertas string resources;
- localized Enumeration text comes from Enumeration documents;
Device,VirtualDevice,User,Endpoint, andLanDevicelabels come from their normal client resolvers, while raw IDs remain category and interaction identity;-
App-authored dynamic presentation uses `FormattedText`; the App package's
StringResourcescatalog supplies localization; - genuine runtime content, such as an externally supplied product proper name or free-form annotation, may be displayed verbatim as data;
- a plain String payload is always literal data, never an implicit resource key; a fixed localizable program token must instead be an Enumeration or formatted Bytes output, and a Libertas object name must instead come from its typed object reference and normal resolver.
These rules apply uniformly to every primitive and server-projected family,
including direct labels, heatmaps, bubbles, networks, and Sankey-style flows.
Changing geometry does not create an exception for presentation provenance.
Only key-only Strings and guide-less identity appearance Strings are
non-textual. A String that also participates in a tooltip, label, guide,
legend, facet, or accessibility distinction is user-visible and must satisfy
the literal-data assertion above.
Representative response fields therefore resolve as follows:
| Chart family and field | Required semantic source |
|---|---|
Bubble portfolio product |
Plain String only when it is the product's proper name; use an Enumeration for a fixed product class. |
Activity heatmap time_block |
Enumeration for fixed blocks, or formatted Bytes for parameterized wording. |
Direct-label label |
Enumeration, formatted Bytes, typed Libertas object reference, or genuine literal domain/user text according to provenance. |
Network node / connection |
Typed Libertas object references when they identify system objects; otherwise Enumeration for a closed relationship vocabulary or formatted Bytes for dynamic wording. |
Sankey stage |
Enumeration for fixed stages, or formatted Bytes for parameterized wording. |
| Identity-scale colors and key-only identifiers | Plain String is allowed only while the field has no text-emitting role or guide. |
An enum-backed traffic-source arc label already satisfies this rule because the client resolves its member document; it must not be flattened into a String.
Essential chart text means a text mark value, a visible positional, facet, or
appearance category, a retained guide or legend label, or an accessibility name
needed to distinguish rendered items. Chart and guide titles, descriptions,
tooltip captions, and nonessential tooltip values are optional and are omitted
when their localized presentation is unavailable.
Diagnostics may include stable native names in developer logs, but those names are never user-interface fallback text. Accessibility sentences are assembled from localized field/category values with localized client templates; they are not constructed by concatenating English fragments in renderer code. Chart metadata never embeds a format string.
9. Interaction model
The minimum client behavior is:
- a tap or pointer focus must first hit at least one datum's final painted geometry within the platform's normal touch/pointer radius; plot background dismisses. In a Cartesian plot, a directly hit polygon competes with other directly hit geometry by full geometry distance and then reverse paint order, but never joins an X slice. If the winning direct hit is not a polygon, the client resolves exactly one horizontal slice among the remaining Cartesian marks by rendered X distance, then full geometry distance, then reverse paint order. Continuous X distance is measured after the resolved forward transform and current normalized viewport, so zoom increases raw X resolution without changing source data;
- a slice has one X-scale owner and exact source identity. A nonempty compatible scale ID is a shared owner; an unnamed X scale is local to its unit. Point slices use the effective scalar type and exact raw X identity. X/X2 marks use the unordered endpoint pair and never merge with a point slice. Categorical identity is the exact raw key. Pixel epsilon, localized labels, and coincident normalized positions never merge different slices or independent scales;
- after the slice is chosen, the grouped callout contains only distinct source rows in that slice whose final painted vertical geometry is within the platform radius. The winning row appears first; peers are ordered by vertical distance and stable composition/row order. Selection is confined to the current concat child and facet pane, while distinct detail series and layers at that X remain eligible;
- a line or rule may use interpolation only to measure its painted hit geometry. It resolves to the nearer originating source row in output X, choosing the later row at an exact midpoint; tooltip values, typed requests, and selection identity never synthesize an interpolated row. Cartesian intervals use their final clipped/minimum-width footprint for hit testing;
- polar marks retain geometry selection because they have no Cartesian X slice. Point, text, and arc resolve to their source row. A polar line or rule resolves a hit segment to its nearer originating endpoint row in screen distance, choosing the later canonical row at an exact midpoint. Competing polar hits use full geometry distance and then reverse paint order;
-
a polygon hit uses the exact final clipped path: containment uses the
evenOddfill, so a point in a hole is not a fill hit, and stroke proximity applies only when a visible stroke was painted. A boundary hit resolves like a line edge. A fill-interior hit resolves to the nearest finite, validly mapped source vertex in screen distance across every subpath, choosing the later canonical vertex at an exact tie. Overlapping polygons use reverse paint order to establish the winning hit. For a polar polygon, that winning vertex establishes one mapped theta spoke; the callout then includes one finite, valid original source vertex from every polygon detail group in the same pane on that spoke. Theta zero and one are the same spoke. Groups retain stable paint order, and each selected vertex's originalScriptValue, not a synthesized group or intersection, owns its tooltip and typed request; - the Cartesian callout presents at most one common horizontal header, followed by the selected rows' remaining tooltip fields. It promotes and removes only X/X2 source fields present in the authored or default tooltip projection; an explicit omission remains omitted and a distinct tooltip-only field remains. When both endpoints are present, one range header preserves the interval. The callout is anchored at the interaction point, prefers the space above, flips below near the top edge, remains inside the complete physical chart, and stays bounded and internally scrollable. A facet pane confines selection identity but is not the callout's horizontal viewport; sibling facet space remains available to the callout. The desired outer width is the widest complete logical row plus both content insets and exactly one dedicated, stationary close-control slot. The complete physical chart width after edge insets is the only horizontal cap: no smaller fixed or style maximum may force overflow while unused chart width could expose that row. The close control never overlays or scrolls with content. Each localized caption and typed value is an unabridged, non-wrapping token. Only when the complete row plus its reserved chrome exceeds that physical bound does the content scroll horizontally rather than wrap or truncate. It follows layout changes through a normalized interaction anchor and provides both a visible close control and the platform accessibility dismiss action. The ordered selected-source identity owns both scroll axes: replacing one nonempty selection with a different identity resets the callout to top-leading before paint, while anchor movement or a repeated tap on the same ordered identity may retain its scroll offsets. A newly selected datum can therefore never inherit a prior tooltip's scrolled state and mask its first row or leading token;
- a polar or polygon callout presents each selected source row's authored or default tooltip projection without synthesizing a Cartesian horizontal header. It uses the same bounded placement, scrolling, close, and accessibility behavior;
-
accessibility focus exposes the mark, localized series/category, and the
original typed channel values rendered through the localization and
permission-aware name-resolution rules in section 8.1, plus display units
from effective
PhysicalUnitmetadata using the long localized form. Unit prose present only in field documents or descriptions is not semantic unit metadata and is not parsed or synthesized into a value. Polygon separators and invalid vertices are not focusable; polygon vertices traverse in group paint order, subpath order, and canonical vertex order. Polar accessibility exposes the raw theta and radius values and never substitutes normalized turns or screen radii; - native accessibility may virtualize this mark sequence instead of allocating one native accessibility object per admitted source row. A virtualized sequence initially exposes its first eligible mark, resets to that mark only when the complete chart snapshot is replaced, and provides localized previous and next mark actions in the same canonical traversal order. Each action exposes the complete localized semantics of the newly current mark. It may not skip a row because the row is offscreen or visually resampled. This is an accessibility lowering mechanism, not chart grammar, visible chart chrome, selection, or a typed App action;
-
preview charts do not select marks, invoke requests, pan, zoom, or otherwise
mutate a viewport. They retain the deterministic initial discrete-position
window and overflow indicator specified below; Chart Explorer makes every
authored category reachable. An interactive chart with a continuous
xscale owns a transient output-space viewport over the resolved snapshot domain. Its minimum zoom is1x(the complete domain) and its maximum zoom is8192x; zooming and panning clamp to that domain and never rewrite source rows, scale bounds, tooltip values, or task-computed interval endpoints; - version-one polar panes do not pan or zoom. Their complete resolved theta and radius domains remain visible in preview and fullscreen. A Cartesian polygon continues to clip and hit-test through the ordinary continuous-X viewport;
-
for a continuous-only X dimension, fullscreen entry, Reset, and a replacement
snapshot use the complete
1xsemantic X domain. The discrete-position rule below is the only initial-viewport exception: it starts at its category-density minimum rather than compressing authored category lanes; -
pinch zooms around the gesture focus where the platform exposes it. A
horizontal drag pans once zoomed, while vertical drag remains owned by the
surrounding fullscreen scroller. Tap remains selection. Visible localized
zoom-in, zoom-out, and reset controls provide pointer and accessibility
equivalents; Reset and full zoom-out restore the continuous-only
1xview or the discrete-position category-density minimum defined below. Directional ownership is decided before a child chart pan recognizes: ignoring a vertical drag only after recognition is not sufficient because it can still prevent the ancestor scroller from moving. High-frequency pan and pinch updates may be coalesced to the display cadence, but gesture completion commits the exact final viewport rather than dropping the last delta. To avoid invalidating every linked native chart on every pointer sample, the actively manipulated pane may use a gesture-local viewport; panes sharing the same scale ID converge atomically when the gesture commits and must not retain mismatched viewports afterward; -
viewport coordinates are normalized scale-output fractions, so linear,
log,symlog,sqrt, temporal, andreverse=truescales all use the same already-resolved forward/inverse transform. Automatic guide ticks and labels use the stable cadence plan and bounded visible guide run in §6.4.5; viewport movement never reruns cadence selection. A replacement snapshot creates a new initial viewport; -
a Cartesian physical plot preserves every authored category on positional
bandandpointscales. It never samples, aggregates, drops, or replaces a category. Only the App task may intentionally return already-aggregated category rows, which are then ordinary authored chart data; -
the minimum leaf-category pitch is exactly `20` logical points on both
platforms. For one unit and one dimension, the leaf-lane count is the
primary category count multiplied by the matching
xOffsetoryOffsetcount when that band has an offset, otherwise by one. A point scale has no child band. The physical plot's leaf-lane count for that dimension is the maximum required by any contributing unit. This20-point rule is derived from the established14-point interactive lane and the 72-percent band cap (14 / 0.72, rounded upward); it is not a platform theme default; -
let `P` be the post-guide physical plot extent and `L` the leaf-lane count in
one dimension. Its virtual category-content extent is
E=max(P,20*L). Every Cartesian scale in that physical plot maps its complete normalized output through the sameE-point content extent, so layered independent scales remain spatially coherent. The visible window is exactlyPpoints and pans continuously within0...(E-P)without snapping. WhenE=P, that dimension has no category viewport. Concat children resolve independently; facets and flattened layers that share one physical scale share its viewport; -
a new snapshot initially exposes the visual top-left of category content:
the leftmost X output and topmost Y output after
reversehas been applied. Thusreverse=truereverses the visible category order rather than being silently cancelled by initialization. A later size, font, overlay-guide, or rotation change recomputesEandP, preserves the normalized leading content position, and clamps it; it never resets a user's category position; - preview retains the same virtual category extent and deterministic visual top-left window but accepts no pan, zoom, selection-scrub, or accessibility viewport-adjustment action. Its noninteractive position indicator truthfully discloses clipped category content, and the containing endpoint workspace exposes the localized action that opens Chart Explorer. In fullscreen, an overflowing X dimension accepts horizontal movement and an overflowing Y dimension accepts vertical movement. When both overflow, including a two-dimensional heatmap, one drag applies both pointer components simultaneously. When only one overflows, only that component is consumed. At a Y boundary, the exact unconsumed vertical delta chains to the one outer Chart Explorer scroller; horizontal movement never blocks that chaining;
-
one physical dimension owns one normalized viewport even when independent
categorical and continuous scales are layered on it. Its category-density
minimum zoom is
Zc=max(1,E/P). A continuous X interaction may zoom further but never belowZc; its maximum ismax(8192,Zc). Reset and a replacement snapshot restore the visual top-left category window atZc, not a category-compressing full-domain view. A category-only plot adds scrolling and indicators but no zoom buttons. A mixed continuous/category X plot keeps the ordinary zoom controls and uses this same viewport for every X scale. Y category overflow likewise applies one viewport to every Y scale because V1 has no independent continuous-Y zoom; -
each overflowing category dimension paints a noninteractive scroll-position
indicator inside the plot: a
2-point track inset3points from the bottom for X or right edge for Y, and a proportional thumb with a minimum length of16points. The thumb position is(offset/(E-P))over the remaining track. The indicator uses the theme's secondary foreground at 40-percent opacity, paints above marks, is excluded from mark hit testing, and is not the resampling indicator because no data was discarded. The plot accessibility node always exposes the localized visible category range. In fullscreen it additionally exposes forward/back scroll actions that move by one visible window minus one leaf lane, with a minimum movement of one leaf lane. Preview exposes no viewport-adjustment action; its containing endpoint workspace exposes the localized action that opens Chart Explorer; - clients cull offscreen discrete marks and guide entries before allocating native scene objects. Path lowering must retain any boundary-crossing segment needed for the exact clipped result, and culling never changes domains, canonical order, grouping, tooltip/source-row identity, accessibility, or resource validation. Category labels may use the existing deterministic guide thinning and responsive overlay policies, but neither policy removes its category geometry;
-
automatic visual resampling applies only to Cartesian `line` and vertical
arearuns whose X coordinates are monotone after facet/detail partitioning, canonical ordering, invalid-row breaks, and visible-viewport culling. Equal X positions are monotone. A non-monotone line, a horizontalarea(x,x2), every polar path, and every other mark remain exact; category scrolling and offscreen culling are not resampling; -
let `W=max(1,ceil(P))`, where `P` is the final plot width in logical points.
Visible culling first retains every segment needed to cross the viewport
boundary. If one resulting eligible run contains at most
4*Wvertices, the client paints it unchanged. Otherwise map each vertex X through the resolved scale and viewport, clamp its logical-point bucket to0...(W-1), and retain from every nonempty bucket the canonical first row, lower-envelope row, upper-envelope row, and canonical last row, deduplicated and returned in canonical source order. Forline, both envelopes are Y. For a vertical area, the lower envelope ismin(y,y2)and the upper envelope ismax(y,y2); an omittedy2uses the resolved zero baseline. Extrema ties choose the earlier canonical row. This yields no more than four painted vertices per logical plot point while preserving endpoints and visible extrema. Resize, zoom, pan, guide-overlay, and facet-layout changes recompute the result from the immutable full run; a client never resamples an already resampled path; - resampling is a renderer-owned level of detail, not a data transform. Full source rows still determine domains, guides, grouping, structural/guide resource accounting, tooltip values, requests, and the ordinary data view. Hit testing may search the full source path so a retained visual segment resolves to an original endpoint row; it never synthesizes a bucket row. Accessibility announces that automatic resampling is active and retains original typed values;
-
whenever at least one painted run actually loses vertices, the plot paints
one noninteractive resampling indicator above marks at its top-left: a
16-point square inset4points, using the chart surface at 85-percent opacity, a3-point corner radius, and a centered≈glyph in the secondary foreground. The glyph is not localized; its accessibility label is the localized equivalent of “Automatically resampled.” It is absent when the current viewport paints every eligible vertex and is separate from category scroll indicators; -
a continuous DateOnly X viewport is the typed exception to arbitrary
fractional edges. After each requested zoom or pan is converted to Gregorian
day ordinals, both ordered edges round to the nearest integral ordinal with
binary64 ties-to-even and clamp to the resolved domain. Equal snapped edges
are a valid one-date degenerate viewport; further zoom in is a no-op. That
date uses the degenerate-domain rule and maps to the plot midpoint, every
other X value clips outside the viewport, and a zero-width clipped span
intersection remains empty. The
client derives the stored normalized fractions back from those snapped
ordinals. Consequently every visible DateOnly endpoint, regenerated priority
tick, and inverse-mapped clipped span bound remains an exact typed
yyyyMMdddate rather than a platform-specific partial-day value; -
a selected mark retains its source `ScriptValue`, allowing existing typed
NextRequestandCopyFromhandling to be reused where applicable; - chart projection also retains the complete response or selected Union variant. Hidden fields in a unit carrier, composition, or Union wrapper are therefore available to the same typed transaction without becoming visible chart data. A renderer must not clone only the projected List and discard its carrier ancestors;
- a data-changing action sends a protocol request and replaces the chart with the returned snapshot.
Linked brushing, arbitrary cross-filter expressions, and persistent client-side
selections are not in the minimum design. If a future local linked-selection use
case cannot be represented as a typed request, it should be introduced as a
separate, narrowly defined ChartSelection proposal rather than folded into
ChartScale, ChartChannel, or ChartGuide.
10. Validation and fallback
Validation occurs at three boundaries:
- TypeScript and Rust parsers reject invalid native annotation syntax and source resolution/traversal failures while preserving machine-decodable source locations.
- Studio's shared validator rejects invalid chart placement, tokens, arguments, exported shapes, and every other semantic rule decidable from the extracted schema and materialized dependencies.
- Android and iOS validate the effective schema and runtime values because a remote or newer schema is still untrusted input.
At minimum, clients validate:
- chart placement and complete `List` unit shape;
- composition child types;
- layer/concat nesting and nested-layer flattening rules;
- known mark, composition, channel, scale, guide, and property tokens;
- channel multiplicity and mark-required channel combinations;
- exclusive Cartesian/polar coordinate forms and polar-layer compatibility;
- scalar type/channel compatibility;
- paired endpoint and scale compatibility;
- finite numeric values and valid literal styles;
- polygon subpath structure and canonical raw group-style constancy;
- shared-scale compatibility;
- guide placement, source/target matrices, shared ownership, localization, and the single-visible-guide rule for each polar position family;
- resource limits.
-
its owner has explicit `theta` and `theta2`; radial bindings are either both
absent, resolving every row to pie radii
0...1, or bothradiusandradius2are present; - its position is absent or `auto` and its labels component is enabled;
-
its target is categorical `fill`, or categorical `color` when the owner has
no explicit
fillbinding; other targets and a color shadowed by explicit fill retain their ordinary guide; - the owner is the pane's only unit with painted rows, and no other resolved guide occupies physical left or right;
-
every painted row has finite `0<=inner_radius
- the rows' angular open interiors do not materially overlap and their positive authored spans form one complete turn under the portable arrangement tolerance below, so they are a categorical pie/annulus/rose partition rather than occluding radial layers; shared boundaries are allowed, inner and outer radii may vary by row, but a material gap is not eligible for automatic rearrangement;
- every painted row has exactly one target-category identity, every resolved target category owns exactly one painted row, and no two rows own the same identity; and
- the plot has no visible `theta` or `radius` guide.
- a vertical low-high rule at
middle; - a horizontal open rule from
lefttomiddle; - a horizontal close rule from
middletoright. rect:xcategory,yfirst quartile,y2third quartile;rule: category position,ylow whisker,y2high whisker;- zero-height
rect: the same x band withy=y2at the median; - optional zero-height
rectrows at the low/high whiskers for caps; - optional
point: outliers. - map points and boundaries as points or ordered line rows grouped by
detail; - a filled projected region or choropleth region as a polygon detail group, using null-position separators for islands and holes and repeating one canonical raw fill value on all of its vertices;
- a network edge as a sampled line or
ruleand nodes as points; - an x-monotonic Sankey link as sampled
x,y,y2area rows and nodes as rectangles; - a sampled ribbon, isoband, or other closed region as a polygon;
- labels as a text layer.
-
impose the same version-one structural and metadata limits on both platforms:
composition depth
8, unit charts64, facets64, resolved guides256, candidates per guide4096, total guide entries16384, total guide primitives32768, tooltip fields per row32, and UTF-8 bytes in one text, guide label, or tooltip value4096; - use bounded diagnostic logging without dumping complete payloads;
-
prevent non-finite geometry from reaching drawing APIs by applying the
documented row-suppression or path-separator rule, and reject invalid literal style tokens;
-
avoid executable expressions, arbitrary field paths, URLs, embedded HTML,
remote images, and platform-specific style objects;
-
impose no chart-specific numeric ceiling on source rows, eligible source
primitives, or vertices in a source path. The enclosing ScriptValue transport and ordinary-data model already own payload admission; chart presentation must not reject the same admitted snapshot because it crosses an unrelated conservative geometry count;
-
scan source geometry with overflow-checked indices and totals, cancellation
checks, and bounded diagnostics. Projection must not reserve storage from an unchecked row/product count. A superseded scan stops promptly. If a platform allocation or scene-construction API reports a recoverable failure, the client follows the chart failure policy in §10 rather than predicting it with a portable hard-coded row number. Process-level out-of-memory termination is not recoverable on every V1 platform and is therefore not a semantic chart fallback guarantee;
-
apply the specified viewport culling and deterministic resampling without
changing source domains, raw row identity, tooltip values, or accessible data;
- preserve stable layout and exact terminal viewport state during gestures;
- cancel chart construction when its screen or source snapshot is superseded;
- preserve normal protocol authorization and request ownership;
-
expose localized chart, series, category, channel, and tooltip information to
accessibility services;
-
require any meaning encoded through paint to also be exposed through a
localized
tooltip,detail, text, shape, or equivalent accessibility value, so color is not the only carrier of state; -
preserve the underlying response and follow the chart failure policy in §10
when its chart cannot be rendered.
-
the outer selected chart root has composition depth `0`; each composition
edge adds one, so a unit reached through at most eight composition edges is
permitted. Every mark-bearing List leaf is one unit chart; composition
containers do not consume the
64unit-chart budget; -
a facet grid contains the Cartesian product of its resolved `row` and
columndomains, with a missing dimension contributing a factor of one. The64limit is the sum of panes produced by faceted plot nodes under the outer chart root. Sibling units in onelayercount one grid only when their ordered raw row and column domains are identical; otherwise each distinct grid contributes its own product. - schema parsing and validation;
- mark lowering (`bar` to `rect` where desired);
- domain, scale, grouping, ordering, and missing-value rules;
- equal-aspect polar mapping, circular clipping, and straight-chord geometry;
- polygon subpaths, implicit closure, `evenOdd` fill, and fill-before-stroke order;
- polar guide ownership and geometry, polar mark hit testing, and the absence of a polar viewport in version one;
- deterministic category palette assignment;
- composition traversal and layer order;
- generic guide inference and explicit replacement, candidate identity, tick/label selection, typed localization, span components, layout order, and inert interaction semantics;
- tooltip contents and source-row identity;
- structural/metadata limits, visible-work policy, and fallback behavior.
- client-side transform and expression languages;
- arbitrary JSON option blobs;
- client joins, lookups, aggregation, binning, or stack inference;
- arbitrary field paths;
- raw URL or HTML marks;
- client-side geographic projection, topology/ring extraction, force layout, contouring, curve interpolation, or path sampling;
- Vega signals and general event streams;
- persistent crossfilter or brush state;
- chart-specific incremental patch formats;
- special marks whose geometry can be composed from primitives.
Package conformance review and response-fixture tests additionally validate the provenance assertion that cannot be derived from schema JSON: every visible plain String is genuine literal domain/user text. Fixed program vocabularies use Enumeration or FormattedText, and Libertas object labels use their typed system-object reference. This audit covers every role that can emit text and explicitly excludes only key-only and guide-less identity appearance Strings.
Chart failure policy: when a chart cannot be rendered, the implementation may provide a complete ordinary-data fallback or display a bounded localized error message. This choice applies to 2D and 3D charts, including default or automatic previews and Chart Explorer. It covers invalid chart presentation, unsupported features or future tokens, layout failure, and recoverable resource failure.
Replace the failed chart atomically; never silently omit an unsupported layer or display altered data meaning. A presentation failure must not reject an otherwise accepted protocol response, alter its data, crash the screen, or change pending-operation ownership. Diagnostics remain bounded. The endpoint presentation contract owns workspace and navigation continuity. Platforms may choose different failure presentations while preserving these rules.
11. Detailed examples
The examples use the implemented TypeScript or Rust source syntax. The first cross-language example shows ordinary short and long documentation. Later examples omit most comments for geometric clarity, but their native field names never stand in for user-visible labels. Production packages supply localized names and descriptions through the existing document pipeline; optional labels are omitted and essential category failures follow §8.1 and the chart failure policy in §10.
Plain String values in examples are limited to genuine literal runtime/domain
data, key-only identity, or guide-less identity appearance values. Neither
package StringResources nor custom document properties implicitly map them. A closed
localizable category such as rising or heating uses an Enumeration;
App-authored dynamic wording uses formatted Bytes output; and a device,
user, endpoint, or other supported Libertas object remains its typed reference.
Only actual user/domain content such as a proper name or free-form annotation
may remain a visible plain String.
11.1 Multi-series line chart
The server returns long-form rows. series is both the color and path grouping
key. Each measurement is also available in the tooltip.
/** @LibertasChart(line) */
export type SensorHistory = SensorPoint[];
export enum SensorSeries {
Indoor,
Outdoor,
}
export interface SensorPoint {
/**
* @LibertasChartChannel(x, tooltip)
* @LibertasChartScale(id=time, kind=utc)
*/
time: LibertasDateTime;
/**
* @LibertasChartChannel(y, tooltip)
* @LibertasChartScale(id=value, kind=linear, zero=false)
*/
value: number;
/** @LibertasChartChannel(color, detail, tooltip) */
series: SensorSeries;
/** @LibertasChartChannel(key) */
sampleId: string;
}
Example payload:
[
{ "time": 1786536000, "value": 22.4,
"series": 0, "sampleId": "indoor-1200" },
{ "time": 1786536000, "value": 29.1,
"series": 1, "sampleId": "outdoor-1200" },
{ "time": 1786536300, "value": 22.6,
"series": 0, "sampleId": "indoor-1205" },
{ "time": 1786536300, "value": 28.8,
"series": 1, "sampleId": "outdoor-1205" }
]
SensorSeries is a closed program vocabulary, so its encoded Enumeration
member—not the English token—is the category identity. sampleId remains a
plain String because it is key-only and never rendered.
11.2 Histogram
The endpoint computes bins and counts. The client draws explicit bin bounds and does no statistical transformation.
#[libertas_chart(bar)]
pub type LatencyHistogram = Vec<LatencyBin>;
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct LatencyBin {
#[libertas_chart_channel(x, tooltip)]
pub start_ms: f64,
#[libertas_chart_channel(x2, tooltip)]
pub end_ms: f64,
#[libertas_chart_channel(y, tooltip)]
#[libertas_chart_scale(kind = linear, min = 0, zero = true)]
pub count: u64,
}
[
{ "start_ms": 0, "end_ms": 10, "count": 41 },
{ "start_ms": 10, "end_ms": 20, "count": 96 },
{ "start_ms": 20, "end_ms": 50, "count": 33 }
]
11.3 Explicit stacked bar
The endpoint computes cumulative lower and upper bounds. No stack mode or implicit category ordering exists on the client.
/** @LibertasChart(bar) */
export type EnergyBreakdown = EnergySegment[];
export enum EnergySource {
Solar,
Battery,
Grid,
}
export interface EnergySegment {
/**
* @LibertasChartChannel(x, tooltip)
* @LibertasChartScale(kind=band)
* @LibertasDateOnly
* @LibertasNumber(type=u32)
*/
day: number;
/** @LibertasChartChannel(y) */
startKwh: number;
/** @LibertasChartChannel(y2, tooltip) */
endKwh: number;
/** @LibertasChartChannel(color, detail, tooltip) */
source: EnergySource;
}
For one day, the server might emit bounds 0..4.2 for solar, 4.2..6.8 for
battery, and 6.8..9.1 for grid. Reordering or recomputing the stack is an App
server decision.
11.4 Pie, annulus, and categorical ARC partitions
The endpoint converts values to cumulative turn fractions. radius2 supplies
the outer radius and radius supplies the hole.
#[libertas_chart(arc)]
pub type UsageDonut = Vec<UsageSlice>;
#[derive(LibertasExport, LibertasAvroDecode)]
pub enum UsageCategory {
Heating,
Lighting,
Other,
}
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct UsageSlice {
#[libertas_chart_channel(theta)]
#[libertas_chart_scale(kind = linear, min = 0, max = 1)]
#[libertas_chart_guide(target = theta, source = none)]
pub start_turn: f64,
#[libertas_chart_channel(theta2)]
pub end_turn: f64,
#[libertas_chart_channel(radius)]
#[libertas_chart_scale(kind = identity)]
pub inner_radius: f64,
#[libertas_chart_channel(radius2)]
pub outer_radius: f64,
#[libertas_chart_channel(color, detail, tooltip)]
pub category: UsageCategory,
#[libertas_chart_channel(tooltip)]
pub value: f64,
}
[
{ "start_turn": 0.00, "end_turn": 0.55,
"inner_radius": 0.55, "outer_radius": 1.0,
"category": 0, "value": 55 },
{ "start_turn": 0.55, "end_turn": 0.80,
"inner_radius": 0.55, "outer_radius": 1.0,
"category": 1, "value": 25 },
{ "start_turn": 0.80, "end_turn": 1.00,
"inner_radius": 0.55, "outer_radius": 1.0,
"category": 2, "value": 20 }
]
A pie uses inner_radius=0. A radial bar uses server-computed numeric
theta,theta2 turn bounds plus radius=0 and a quantitative radius2; it does
not ask the client to derive an angular band or cumulative boundary.
11.4.1 Automatic in-shape and linked ARC labels
A client may lower one automatic categorical appearance guide owned by an
eligible arc unit into automatic shape labels. This is an ARC-family rule,
not a donut-only rule: a pie with inner_radius=0, an annulus with a positive
inner radius, and a rose/radial categorical partition with row-varying radii
use the same planner. The first qualifying guide in resolved guide order must
meet every condition below:
Failure of any condition keeps the complete ordinary appearance guide and is not a render rejection. In particular, a varying-radius rose is not rejected or forced into common-radius geometry: its row-specific painted outer boundary is the leader anchor and Apache ECharts' common maximum outer radius owns the elbow envelope. A future adapter is required for overlapping ARC layers, translucent compositing, or visible-stroke paint bounds.
For a successful plan, every category appears exactly once as either an in-shape label or a linked outside label. Text is the complete localized category label; clients do not synthesize a value or percentage from tooltip fields. Localization, the intrinsic two-line categorical wrapping rule of §6.4.5, native shaping, and native measurement happen once before geometric search. Automatic labels are never rewrapped, truncated, ellipsized, scaled, rotated, hidden, or semantically thinned.
In-shape labels are centered. Outside labels align toward their leader; labels above or below the plot remain upright and centered. Leaders attach to the owning row's painted boundary, including row-varying radii. They must not cross one another, another label's ink, or a filled sector. Complete labels, leaders, and the guide title fit the cutout-safe composition with the contract's half-physical-pixel raster allowance.
The guide title appears once, inside the common unpainted hole when it fits at the minimum radius, otherwise in the ordinary guide's title-only lane. A pie and annulus with the same outer geometry retain the same natural mark scale; a hole changes title placement, not scale. The minimum painted radius is 100 logical points. The title uses the 11-point guide-title style and four-point clearance. A bottom title lane has an eight-point gap; a physical-end lane has a sixteen-point gap. The title is never omitted to make labels fit.
A complete categorical partition is eligible only when its total span and
adjacent seams differ from one complete turn by no more than
nextUp(1e-9) binary64 turns. The accepted arrangement preserves category order,
identity, and proportions. A material gap or overlap retains authored geometry
and the ordinary guide.
Full-screen portrait uses the complete safe width W and natural radius
(W-16)/2; the composition height includes the full label/title footprint and
is at least W and the polar minimum. Full-screen landscape uses the complete
height H below the title bar and above the stable bottom inset, with an H x H
plot centered in safe width W. Insets are applied once. If W < H, automatic
labels fall back to the ordinary guide. Additional safe width does not lengthen
leaders. The combined plot/title/label footprint is centered without changing
its accepted internal arrangement. An auxiliary guide that does not fit is
available through Show Guides.
Preview retains its intrinsic polar host, sized from the preview's square R.
It applies the 4R / 3 preview viewport clip only after the complete logical
layout succeeds; it has no label scroller.
Chart Explorer reveals the complete footprint. If complete automatic placement
fails, restore the complete ordinary guide atomically. Do not reject an
otherwise valid chart, clip label text, omit categories, or weaken collision
requirements to retain automatic labels.
11.5 Heatmap and facets
Each row is a server-computed cell. Explicit ranges support irregular buckets; band scales support regular categorical cells.
/** @LibertasChart(rect) */
export type OccupancyHeatmap = OccupancyCell[];
export interface OccupancyCell {
/** @LibertasChartChannel(x, tooltip) */
hourStart: LibertasDateTime;
/** @LibertasChartChannel(x2, tooltip) */
hourEnd: LibertasDateTime;
/** @LibertasChartChannel(y, tooltip) */
zone: LibertasDevice;
/** @LibertasChartChannel(color, tooltip) */
occupancy: number;
/** @LibertasChartChannel(row) */
building: LibertasDevice;
}
The client creates one row facet per building. The response remains one flat list; no client lookup or pivot is involved. Both categorical identities remain typed Libertas system-object references, and the client resolves their current, permission-aware display names without the App copying names into Strings.
11.6 Confidence band with a line
An area layer carries lower and upper bounds. A line layer carries the estimate. Both use the same scale IDs.
/** @LibertasChart(area) */
export type ConfidenceBand = ConfidencePoint[];
export interface ConfidencePoint {
/**
* @LibertasChartChannel(x)
* @LibertasChartScale(id=time, kind=utc)
*/
time: LibertasDateTime;
/**
* @LibertasChartChannel(y)
* @LibertasChartScale(id=value, kind=linear, zero=false)
*/
low: number;
/** @LibertasChartChannel(y2) */
high: number;
}
/** @LibertasChart(line) */
export type EstimateLine = EstimatePoint[];
export interface EstimatePoint {
/**
* @LibertasChartChannel(x, tooltip)
* @LibertasChartScale(id=time, kind=utc)
* @LibertasChartGuide(target=x, source=scale)
*/
time: LibertasDateTime;
/**
* @LibertasChartChannel(y, tooltip)
* @LibertasChartScale(id=value, kind=linear, zero=false)
* @LibertasChartGuide(target=y, source=scale)
*/
estimate: number;
}
/** @LibertasChart(layer) */
export interface ForecastChart {
band: ConfidenceBand;
estimate: EstimateLine;
}
11.7 Candlestick chart
Candlestick is a layer of a high-low rule and an open-close rectangle. The App
server computes the candle's horizontal time bounds and its rising/falling
color. There is no special candlestick mark and no special open/high/low/close
channel vocabulary.
export enum MarketDirection {
Rising,
Falling,
}
/** @LibertasChart(rule) */
export type CandleWicks = CandleWick[];
export interface CandleWick {
/**
* @LibertasChartChannel(x, tooltip)
* @LibertasChartScale(id=time, kind=utc)
* @LibertasChartGuide(target=x, source=scale)
*/
time: LibertasDateTime;
/**
* @LibertasChartChannel(y, tooltip)
* @LibertasChartScale(id=price, kind=linear, zero=false)
* @LibertasChartGuide(target=y, source=scale)
*/
low: number;
/** @LibertasChartChannel(y2, tooltip) */
high: number;
/** @LibertasChartChannel(tooltip) */
open: number;
/** @LibertasChartChannel(tooltip) */
close: number;
/** @LibertasChartChannel(tooltip) */
volume: number;
/** @LibertasChartChannel(tooltip) */
direction: MarketDirection;
/** @LibertasChartChannel(key) */
candleId: string;
/**
* @LibertasChartChannel(stroke)
* @LibertasChartScale(kind=identity)
*/
wickColor: string;
}
/** @LibertasChart(rect) */
export type CandleBodies = CandleBody[];
export interface CandleBody {
/**
* @LibertasChartChannel(x)
* @LibertasChartScale(id=time, kind=utc)
*/
start: LibertasDateTime;
/** @LibertasChartChannel(x2) */
end: LibertasDateTime;
/**
* @LibertasChartChannel(y, tooltip)
* @LibertasChartScale(id=price, kind=linear, zero=false)
*/
open: number;
/** @LibertasChartChannel(y2, tooltip) */
close: number;
/** @LibertasChartChannel(tooltip) */
time: LibertasDateTime;
/** @LibertasChartChannel(tooltip) */
low: number;
/** @LibertasChartChannel(tooltip) */
high: number;
/**
* @LibertasChartChannel(fill)
* @LibertasChartScale(kind=identity)
*/
directionColor: string;
/** @LibertasChartChannel(tooltip) */
volume: number;
/** @LibertasChartChannel(tooltip) */
direction: MarketDirection;
/** @LibertasChartChannel(key) */
candleId: string;
}
/** @LibertasChart(layer) */
export interface CandlestickChart {
wicks: CandleWicks;
bodies: CandleBodies;
}
Example rows for one candle:
{
"wicks": [
{
"time": 1786545000,
"low": 101.20,
"high": 108.40,
"open": 102.10,
"close": 106.80,
"volume": 18342,
"direction": 0,
"candleId": "2026-08-12T14:30Z",
"wickColor": "#657184"
}
],
"bodies": [
{
"start": 1786544975,
"end": 1786545025,
"time": 1786545000,
"open": 102.10,
"close": 106.80,
"low": 101.20,
"high": 108.40,
"directionColor": "#178A55",
"volume": 18342,
"direction": 0,
"candleId": "2026-08-12T14:30Z"
}
]
}
Both layers repeat all desired OHLCV tooltip fields because the client does not
join rows across layers, even when their key values match. The key preserves
conceptual identity and focus stability only. Both literal paint fields declare
an identity scale and are not rendered as text. The fixed direction wording is
an Enumeration whose localized member label is used by the tooltip. The
single-pixel zero-height rule from the geometry contract keeps an
open == close doji visible.
An optional volume subplot is a bar chart placed below the candlestick with
Chart=vconcat; both children use id=time so panning and the time guide remain
aligned.
11.8 OHLC chart
An OHLC glyph is three rules. The endpoint computes left, middle, and
right time positions from the sampling interval:
#[derive(LibertasExport, LibertasAvroDecode)]
pub enum MarketDirection {
Rising,
Falling,
}
#[libertas_chart(rule)]
pub type OhlcRanges = Vec<OhlcRange>;
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct OhlcRange {
#[libertas_chart_channel(x, tooltip)]
#[libertas_chart_scale(id = time, kind = utc)]
#[libertas_chart_guide(target = x, source = scale)]
pub middle: LibertasDateTime,
#[libertas_chart_channel(y, tooltip)]
#[libertas_chart_scale(
id = price,
kind = linear,
zero = false
)]
#[libertas_chart_guide(target = y, source = scale)]
pub low: f64,
#[libertas_chart_channel(y2, tooltip)]
pub high: f64,
#[libertas_chart_channel(tooltip)]
pub open: f64,
#[libertas_chart_channel(tooltip)]
pub close: f64,
#[libertas_chart_channel(tooltip)]
pub volume: u64,
#[libertas_chart_channel(tooltip)]
pub direction: MarketDirection,
#[libertas_chart_channel(key)]
pub candle_id: String,
#[libertas_chart_channel(stroke)]
#[libertas_chart_scale(kind = identity)]
pub direction_color: String,
}
#[libertas_chart(rule)]
pub type OhlcOpenTicks = Vec<OhlcOpenTick>;
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct OhlcOpenTick {
#[libertas_chart_channel(x)]
#[libertas_chart_scale(id = time, kind = utc)]
pub left: LibertasDateTime,
#[libertas_chart_channel(x2, tooltip)]
pub middle: LibertasDateTime,
#[libertas_chart_channel(y, tooltip)]
#[libertas_chart_scale(
id = price,
kind = linear,
zero = false
)]
pub open: f64,
#[libertas_chart_channel(tooltip)]
pub low: f64,
#[libertas_chart_channel(tooltip)]
pub high: f64,
#[libertas_chart_channel(tooltip)]
pub close: f64,
#[libertas_chart_channel(tooltip)]
pub volume: u64,
#[libertas_chart_channel(tooltip)]
pub direction: MarketDirection,
#[libertas_chart_channel(key)]
pub candle_id: String,
#[libertas_chart_channel(stroke)]
#[libertas_chart_scale(kind = identity)]
pub direction_color: String,
}
#[libertas_chart(rule)]
pub type OhlcCloseTicks = Vec<OhlcCloseTick>;
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct OhlcCloseTick {
#[libertas_chart_channel(x, tooltip)]
#[libertas_chart_scale(id = time, kind = utc)]
pub middle: LibertasDateTime,
#[libertas_chart_channel(x2)]
pub right: LibertasDateTime,
#[libertas_chart_channel(y, tooltip)]
#[libertas_chart_scale(
id = price,
kind = linear,
zero = false
)]
pub close: f64,
#[libertas_chart_channel(tooltip)]
pub open: f64,
#[libertas_chart_channel(tooltip)]
pub low: f64,
#[libertas_chart_channel(tooltip)]
pub high: f64,
#[libertas_chart_channel(tooltip)]
pub volume: u64,
#[libertas_chart_channel(tooltip)]
pub direction: MarketDirection,
#[libertas_chart_channel(key)]
pub candle_id: String,
#[libertas_chart_channel(stroke)]
#[libertas_chart_scale(kind = identity)]
pub direction_color: String,
}
#[derive(LibertasExport, LibertasAvroDecode)]
#[libertas_chart(layer)]
pub struct OhlcChart {
pub ranges: OhlcRanges,
pub opens: OhlcOpenTicks,
pub closes: OhlcCloseTicks,
}
This representation also supports irregular trading intervals because the server supplies the exact horizontal bounds. A convenience source annotation could expand an OHLC domain type into these primitives in the future, but it would generate instances of the same four canonical attribute families and would not add client semantics. Every layer row repeats the complete OHLCV tooltip set and the same conceptual key because, as in candlestick, the client does not join layer data.
11.9 Box plot and error bars
A box plot uses:
The server computes quartiles and outliers. An error bar is the same pattern
without the box. An error band uses area with y,y2, usually layered under a
line. The one-device-pixel zero-interval rule makes medians and caps visible
without asking the server to know responsive band-edge coordinates.
11.10 Server-projected maps, networks, and flows
The minimum grammar deliberately has no geographic projection, topology, force layout, contour generation, or Sankey transform. The endpoint returns projected and sampled geometry that the primitives can express:
Projected geometry does not relax the presentation-source rule. The
field-by-field audit in §8.1 is normative for bubble product, activity
time_block, direct label, network node and connection, Sankey stage,
and traffic-source values. Apps must not copy an object's current display name
or emit fixed English tokens in a visible plain String. Reusing one of these row
schemas in another response, such as an operations dashboard reusing the
activity heatmap, does not create an exception.
This provides useful projected-map and flow/network charts without embedding a geospatial, contour, or graph runtime in every client. The App remains responsible for projection, antimeridian decisions, topology/ring extraction, force simulation, contouring, and path sampling. A client does not interpret geographic coordinates or topology merely because a polygon happens to depict a map.
The automatic shape-label policy of §6.4.7 is universal, but an authored map
text row is not automatically eligible: its x,y remain literal. Polygon and
text units are independent snapshots, and matching key, detail, localized
text, proximity, or layer order does not establish ownership. A future
projected-region adapter must define an explicit one-label/one-compound-polygon
association, retain the authored text coordinate as its preferred inside
anchor, and specify even-odd containment, hole/island ownership, eligible outer
boundary, outside-side order, segment ties, collision, clipping, and safe-area
fallback before either client may move that label or create a leader. Once
admitted, it must use the same inside-first, half-physical-pixel, exactly-once,
pure-planner, complete-label, and atomic-fallback rules. A shape adapter may
explicitly reuse the §11.4.1 natural-boundary, physical-side partition,
side-local collision, and post-displacement leader-recomputation pipeline when
its ownership and boundary geometry make those stages meaningful; the eligible
ARC-family adapter is the current adapter that does so. Until a projected-region adapter defines
otherwise, failure retains the authored text at its literal x,y; there is no
ordinary guide to restore. Clients may not implement a map-only approximation
based on a bounding box or centroid.
11.11 Scatter and bubble chart
A point chart becomes a bubble chart when it binds size. The endpoint returns
the already calculated x, y, and size measure; the client performs no regression
or domain transform beyond the declared scale. The renderer maps that measure
to its bounded responsive point-radius range.
#[libertas_chart(point)]
pub type ProductPortfolio = Vec<ProductPoint>;
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct ProductPoint {
#[libertas_chart_channel(x, tooltip)]
#[libertas_chart_scale(kind = log)]
pub market_size: f64,
#[libertas_chart_channel(y, tooltip)]
#[libertas_chart_scale(kind = linear, zero = false)]
pub growth_percent: f64,
#[libertas_chart_channel(size, tooltip)]
#[libertas_chart_scale(kind = sqrt, min = 1, max = 100000)]
pub revenue: f64,
#[libertas_chart_channel(color, detail, tooltip)]
pub product: String,
#[libertas_chart_channel(key)]
pub product_key: String,
}
Here min and max constrain the revenue domain; the client chooses a bounded
responsive point-radius range. product is a genuine proper name supplied by
the business domain and may therefore remain literal text even if one snapshot
happens to contain only a small set of names. A fixed product class would use
an Enumeration instead. If the endpoint supplies a literal point radius in
density-independent points, it uses type=identity and must stay within the
client resource limit.
11.12 Grouped bar chart
Grouped bars use a nested discrete band. The server duplicates the series value
because one field cannot own both the independently scaled xOffset and color
roles.
/** @LibertasChart(bar) */
export type GroupedUsage = GroupedUsageRow[];
export enum UsageSeries {
Actual,
Target,
}
export interface GroupedUsageRow {
/** @LibertasChartChannel(x, tooltip) */
room: LibertasDevice;
/** @LibertasChartChannel(xOffset) */
seriesOffset: UsageSeries;
/** @LibertasChartChannel(y, tooltip) */
usageKwh: number;
/** @LibertasChartChannel(color, detail, tooltip) */
series: UsageSeries;
}
For each row, seriesOffset and series contain the same raw category. The
nested-band and color-domain rules give that category stable placement and
paint on both platforms. The room keeps its Libertas device ID as raw identity;
the client resolves the localized, permission-aware display name.
11.13 Filled radar chart
A radar chart is not a special mark. The App assigns every indicator an explicit turn, normalizes that indicator's value against its own documented bound, and emits one polar polygon detail group per series. The original value and indicator remain tooltip data. The client does not infer indicator maxima or evenly space categories.
#[libertas_chart(polygon)]
pub type RadarRegions = Vec<RadarVertex>;
#[derive(LibertasExport, LibertasAvroDecode)]
pub enum RadarIndicator {
SoilMoisture,
Coverage,
Pressure,
Flow,
}
#[derive(LibertasExport, LibertasAvroDecode)]
pub struct RadarVertex {
#[libertas_chart_channel(theta)]
#[libertas_chart_scale(id = radar_theta, kind = identity)]
pub indicator_turn: f64,
#[libertas_chart_channel(radius)]
#[libertas_chart_scale(id = radar_radius, kind = identity)]
pub normalized_value: f64,
#[libertas_chart_channel(color, detail, tooltip)]
pub series: LibertasDevice,
#[libertas_chart_channel(fillOpacity)]
#[libertas_chart_scale(kind = identity)]
pub region_opacity: f64,
#[libertas_chart_channel(order)]
pub indicator_order: u8,
#[libertas_chart_channel(tooltip)]
#[libertas_chart_guide(
target = theta,
source = value,
title = none,
domain = none,
ticks = none,
grid = none
)]
pub indicator: RadarIndicator,
#[libertas_chart_channel(tooltip)]
pub original_value: f64,
#[libertas_chart_channel(key)]
pub vertex_key: String,
}
For four indicators, one series might use turns 0,0.25,0.5,0.75, radii
0.8,0.6,0.9,0.4, orders 0,1,2,3, and the same series and opacity values on
all four rows. It does not repeat the turn-zero row: the polygon closes the
last-to-first edge. A second series is another detail group and may overlap
the first with a different stable palette color. Each series is a typed device
reference, so its current resolved name is presentation and its device ID stays
the grouping and interaction identity.
An optional polar point layer can repeat the vertices for explicit markers
and vertex-sized hit targets. Here the guide on indicator emits one localized
caption from the indicator member's localized document at each numeric theta
position; repeated series rows with the same raw indicator and theta collapse
to one candidate. The server may instead supply
spokes as polar rule(theta,radius,radius2) rows and captions as
text(theta,radius,text) rows when the labels must be in-plot or interactive.
Polygon rings at chosen radii can provide an exact polygonal grid. Such authored
marks remain data geometry, not guides, and their identity scales have no
implicit guide. Per-indicator normalization, label choice, and custom grid
sampling remain server-owned transformations.
12. Security, resource, and accessibility requirements
Chart data and metadata are untrusted even when a parser would normally produce them. Implementations must:
The structural limits use semantic, renderer-independent counts:
Projection rejects a structurally over-limit chart before native lowering. Source geometry size alone is not structural invalidity. Renderers keep the source snapshot available for exact domains, interaction, and ordinary data, while allocating only the culled/resampled visible scene described above.
Remote image marks are intentionally excluded from the initial primitive set. If later required, they need a separate asset identity, size, cache, content type, authorization, and lifetime design rather than a raw URL channel.
13. Cross-platform rendering contract
Android and iOS may use different native drawing libraries, but they must share the semantic behavior defined here:
Theme-native fonts, gesture feel, the fitting density tier selected under the normative collision algorithm, and exact pixel layout may differ. The complete candidate ladder, identities and ordering within each tier, priority rules, data meaning, series grouping, scale domains, and literal colors must not differ; the selected responsive tier may.
14. Deliberate exclusions and future evolution
The minimum design excludes:
New capability should first be tested against the core question: can the App server emit explicit rows and primitive layers instead? If yes, no schema extension is needed. A future attribute is justified only when it represents portable client interaction or presentation that cannot be supplied safely as typed chart-ready data.
15. Design references
The vocabulary and coverage analysis were compared with the official Vega-Lite documentation for encoding channels, layering, concatenation, and the layered candlestick example. Those references motivate familiar terminology only. The Libertas contract in this document is intentionally smaller and has its own normative semantics.