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:

TypeScript
/**
 * 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;
}
Rust
/// 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.

JSON
[
  {
    "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.

JSON
{
  "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:

  1. TypeScript and Rust packages produce equivalent charts when their generated schemas are equivalent.
  2. No chart-specific protocol message or payload byte is required.
  3. Chart presentation is atomic: a consumer either validates the complete current contract or presents the existing ordinary data view.
  4. 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:

Text
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:

Text
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:

Text
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:

JSON
{ "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:

Text
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 y band or, when y is continuous Number, a histogram whose y endpoint is zero unless zero=false excludes it from the visible domain;
  • `bar(x,y,y2)` requires a discrete `x` band and continuous `y,y2`; it invents no baseline. With both x2 and y2, 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 on y of a vertical area without y2: 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, and rect(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,radius2 is a radial segment, theta,theta2,radius is 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, or radius2. Its applicable non-positional channels are color, fill, stroke, opacity, fillOpacity, strokeOpacity, strokeWidth, strokeDash, tooltip, detail, order, key, row, and column; text, size, shape, and angle are 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, and radius2 are ordinary numeric Number bindings, not DateOnly numbers 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 R is 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) and y = centerY - R * r * cos(2*pi*t). Scale reversal is applied before this conversion. A polar layer uses this same center and R for 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 arc has 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 radius2 is 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:

TypeScript
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, or LanDevice, 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 String and 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:

TypeScript
/** @LibertasPhysicalUnit("millimeter") */
amount: number;
Rust
#[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:

Text
#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:

Text
#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:

Text
#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:

Text
id,type,min,max,mid,zero,reverse

The grammar is intentionally closed and does not provide an arbitrary option object:

Text
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:

JSON
{
  "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 DateTime explicitly 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 theta and radius positions are quantitative Number channels and cannot use ordinal, band, point, time, or utc. Their explicit scale type is linear, log, symlog, sqrt, or identity; identity values must already lie in 0...1;
  • See scale type compatibility rules. TimeOnly is always continuous local time-of-day and cannot use ordinal, band, or point; explicit bounds must lie in 0..86400 inclusive;
  • See scale scalar rules. DateTime values and explicit bounds must be integral Unix seconds in 0..253402214399 inclusive;
  • See scale type compatibility rules. style and paint channels cannot use time, utc, band, or point;
  • `time` and `utc` are valid only for `DateTime` or a `DateOnly` Number; DateOnly otherwise permits only band or point; log, symlog, and sqrt are valid only for ordinary quantitative Number;
  • `min` and `max` are valid only for continuous quantitative or temporal scales; mid is 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:

Text
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:

Rust
#[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- TimeInterval presentation class, effective Money, PhysicalUnit, and Format values, 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..18 density-independent points) and text-font size (10..32) cannot share an ID or one size guide;
  • See shared-scale compatibility rules. every binding resolves its scale type independently, from its explicit type property or its local inference. Resolved types must match; an omitted type never 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:

Text
target,source,position,title,domain,ticks,labels,grid

The grammar is closed:

Text
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

Text
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):

Text
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:

Text
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:

  1. the use-site or local field;
  2. the same-named inherited table field;
  3. the referenced named type.

It does not union values from several declarations. Therefore a local override must contain the complete channel set:

Text
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:

Text
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, and LanDevice labels come from their normal client resolvers, while raw IDs remain category and interaction identity;
  • App-authored dynamic presentation uses `FormattedText`; the App package's StringResources catalog 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 evenOdd fill, 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 original ScriptValue, 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 PhysicalUnit metadata 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 x scale owns a transient output-space viewport over the resolved snapshot domain. Its minimum zoom is 1x (the complete domain) and its maximum zoom is 8192x; 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 1x semantic 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 1x view 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, and reverse=true scales 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 band and point scales. 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 xOffset or yOffset count 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. This 20-point rule is derived from the established 14-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 same E-point content extent, so layered independent scales remain spatially coherent. The visible window is exactly P points and pans continuously within 0...(E-P) without snapping. When E=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 reverse has been applied. Thus reverse=true reverses the visible category order rather than being silently cancelled by initialization. A later size, font, overlay-guide, or rotation change recomputes E and P, 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 below Zc; its maximum is max(8192,Zc). Reset and a replacement snapshot restore the visual top-left category window at Zc, 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 inset 3 points from the bottom for X or right edge for Y, and a proportional thumb with a minimum length of 16 points. 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 area runs 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 horizontal area(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*W vertices, the client paints it unchanged. Otherwise map each vertex X through the resolved scale and viewport, clamp its logical-point bucket to 0...(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. For line, both envelopes are Y. For a vertical area, the lower envelope is min(y,y2) and the upper envelope is max(y,y2); an omitted y2 uses 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 inset 4 points, using the chart surface at 85-percent opacity, a 3-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 yyyyMMdd date rather than a platform-specific partial-day value;
  • a selected mark retains its source `ScriptValue`, allowing existing typed NextRequest and CopyFrom handling 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:

  1. TypeScript and Rust parsers reject invalid native annotation syntax and source resolution/traversal failures while preserving machine-decodable source locations.
  2. 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.
  3. 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.
  • 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.

    TypeScript
    /** @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:

    JSON
    [
      { "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.

    Rust
    #[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,
    }
    
    JSON
    [
      { "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.

    TypeScript
    /** @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.

    Rust
    #[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,
    }
    
    JSON
    [
      { "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:

    1. its owner has explicit `theta` and `theta2`; radial bindings are either both absent, resolving every row to pie radii 0...1, or both radius and radius2 are present;
    2. its position is absent or `auto` and its labels component is enabled;
    3. its target is categorical `fill`, or categorical `color` when the owner has no explicit fill binding; other targets and a color shadowed by explicit fill retain their ordinary guide;
    4. the owner is the pane's only unit with painted rows, and no other resolved guide occupies physical left or right;
    5. every painted row has finite `0<=inner_radius
    6. 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;
    7. 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
    8. the plot has no visible `theta` or `radius` guide.

    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.

    TypeScript
    /** @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.

    TypeScript
    /** @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.

    TypeScript
    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:

    JSON
    {
      "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:

    1. a vertical low-high rule at middle;
    2. a horizontal open rule from left to middle;
    3. a horizontal close rule from middle to right.
    Rust
    #[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:

    • rect: x category, y first quartile, y2 third quartile;
    • rule: category position, y low whisker, y2 high whisker;
    • zero-height rect: the same x band with y=y2 at the median;
    • optional zero-height rect rows at the low/high whiskers for caps;
    • optional point: outliers.

    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:

    • 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 rule and nodes as points;
    • an x-monotonic Sankey link as sampled x,y,y2 area rows and nodes as rectangles;
    • a sampled ribbon, isoband, or other closed region as a polygon;
    • labels as a text layer.

    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.

    Rust
    #[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.

    TypeScript
    /** @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.

    Rust
    #[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:

    • impose the same version-one structural and metadata limits on both platforms:

      composition depth 8, unit charts 64, facets 64, resolved guides 256, candidates per guide 4096, total guide entries 16384, total guide primitives 32768, tooltip fields per row 32, and UTF-8 bytes in one text, guide label, or tooltip value 4096;

    • 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 structural limits use semantic, renderer-independent counts:

    • 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 64 unit-chart budget;
    • a facet grid contains the Cartesian product of its resolved `row` and column domains, with a missing dimension contributing a factor of one. The 64 limit is the sum of panes produced by faceted plot nodes under the outer chart root. Sibling units in one layer count one grid only when their ordered raw row and column domains are identical; otherwise each distinct grid contributes its own product.

    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:

    • 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.

    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:

    • 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.

    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.

Search Libertas