Skip to content

Blocks

The page is a tree of blocks. Layout blocks arrange the page; visualization blocks query the view and render it. Every block is a single-key JSON object: { "@block/<type>": <props> }. Many accept a shorthand (a bare string, array, or child block) in place of the full props object.

The page is always wrapped in an @block/page. You can author the wrapper explicitly, or just provide its content and let Noemata wrap it:

"page": { "@block/text": "Hello" }

This page covers the core blocks. The installed frames under @frames/@opentelemetry/ exercise the full set; .data/schemas/blocks.json lists every block and field.

Layout

  • @block/grid — placed cells. Give it cols and a list of items; each item sets x/y/cols/rows and holds one child. Good for dashboards.
  • @block/column / @block/row — stack children vertically / lay them out horizontally. Both accept the box options (width/height/padding) and the flex options (gap/alignItems). @block/row accepts a bare array shorthand.
  • @block/stack — flex container with direction plus the same box and flex options. Use when you need explicit sizing or alignment.
  • @block/box — single-child container for the box options (width/height/padding).
  • @block/panel — a titled card. title, optional description, one child, plus box options and a gap that separates the header from the body. The standard wrapper around a visualization.
{
"@block/grid": {
"cols": 4,
"default_rows": 1,
"items": [
{
"x": 0,
"y": 0,
"cols": 2,
"rows": 2,
"@block/panel": {
"title": "Latency",
"@block/plot": { "marks": [{ "line": { "y": "DurationP95" } }] }
}
}
]
}
}

Plot

@block/plot renders charts from marks — there is no @block/line or @block/bar. Each mark is a single-key object naming the geometry; its channels (x, y, stroke, fill, …) map to view columns by name.

{
"@block/plot": {
"marks": [{ "line": { "x": "t", "y": "DurationP95", "stroke": "ServiceName" } }]
}
}

Common marks: line, areaY, barY / barX, dot, ruleY. Pass several to overlay them ("marks": [{ "areaY": {…} }, { "line": {…} }]). Axis and color scales are configured with x, y, and color props on the plot.

Plotting over time

A plot with no from queries the frame’s view automatically, bucketed by the table’s timestamp over the active time range. So a metric defined in the view renders as a time series just by naming it:

{ "@block/plot": { "marks": [{ "line": { "y": "DurationP95" } }] } }

For an explicit time-bucketed query, bind the plot’s from to an @expr/timeseries_query (see Querying data).

Table

@block/table renders rows. Bind from to a data expression and list columns as { "title": …, "column": … } (omit columns to show every field):

{
"@block/table": {
"from": {
"@expr/query": {
"from": "otel_traces",
"select": ["ServiceName", { "Requests": "count()" }, { "ErrorRate": "ErrorRate" }],
"group_by": ["ServiceName"],
"order_by": { "Requests": "desc" },
"limit": 20
}
},
"columns": [
{ "title": "Service", "column": "ServiceName" },
{ "title": "Requests", "column": "Requests" },
{ "title": "Error rate", "column": "ErrorRate" }
]
}
}

Stat

@block/stat is a KPI card: a title and a scalar value, with optional format, a sparkline series, and a baseline for delta-vs-previous.

{
"@block/stat": {
"title": "Request rate",
"format": { "rate": "seconds" },
"value": {
"@expr/query": {
"from": "otel_traces",
"select": [{ "Rps": "count()" }],
"where": "IsEntrySpan"
}
}
}
}

Text

  • @block/text — plain text. Shorthand: a bare string. Object form adds size and color (muted, error, …).
  • @block/md — renders a Markdown string to HTML.

Querying data

Data-bearing blocks take a from that is a data expression. The structured @expr/query builds SQL declaratively against the view:

{
"@expr/query": {
"from": "otel_traces",
"select": ["ServiceName", { "Requests": "count()" }],
"where": "IsEntrySpan",
"group_by": ["ServiceName"],
"order_by": { "Requests": "desc" },
"limit": 10
}
}

select items are bare column names or { alias: expression } objects, where the expression may reference any view scalar by name. Use @expr/timeseries_query (same shape) when you want one row per time bucket over the active range.

To correlate two relations, give from an as alias and add a join. The join is a top-level key named for its kind — inner_join, left_join, right_join, full_join, or cross_join — so the query reads like SQL. It carries its own relation from (a table or a nested query) with an optional as, and a condition that is either on (a predicate) or using (shared column names); a cross_join takes no condition. A query may carry at most one join key (as is only valid alongside one); for more, nest a query in from or drop to raw SQL.

{
"@expr/query": {
"from": "otel_traces",
"as": "t",
"left_join": {
"from": "service_owners",
"as": "o",
"on": "t.ServiceName = o.ServiceName"
},
"select": ["t.ServiceName", { "Owner": "o.Team" }]
}
}

For a per-second rate, divide a count() (or sum()) by _time_grain — the time each value covers, in milliseconds, e.g. { "Rps": "count() / nullif(_time_grain / 1000.0, 0)" }. _time_grain resolves to the bucket width inside a timeseries and to the whole window in a scalar query, so the same expression reads correctly as both a per-bucket chart series and a window-mean stat card — the headline equals the mean of its own chart by construction. The bundled Throughput / Rate / HistRate view scalars are defined this way. (Formatting a bare count() as { "rate": "seconds" } without dividing reports the raw count as a per-second rate — wrong by the window’s length.)

Two fixed denominators are injected alongside it for when you need one regardless of context: _time_interval (the bucket width — only in a timeseries) and _time_range (the selected window’s total duration).

Sharing a query across blocks

@block/context binds named values that descendant blocks read by name, so several blocks share one query instead of each re-running it:

{
"@block/context": {
"extend": {
"totals": {
"@expr/query": {
"from": "otel_traces",
"select": ["Throughput", "DurationP95", "EntryErrorRate"],
"where": "IsEntrySpan"
}
}
},
"@block/row": [
{
"@block/stat": {
"title": "Throughput",
"value": { "@expr/get_context": "totals.Throughput" }
}
},
{ "@block/stat": { "title": "p95", "value": { "@expr/get_context": "totals.DurationP95" } } }
]
}
}

Controls

Pages can carry interactive filters that all queries on the page respect:

  • @block/field_filter — a dropdown that filters by a view column ({ "field": "DeploymentEnvironment", "placeholder": "Environment" }).
  • @block/query_bar — a free-text query bar.

See the installed @frames/@opentelemetry/services frame for these in context, then continue to the Cookbook.