Navigation chains
A cell often needs to show something that lives near the row element rather than
on it: the subject of a requirement, the value of a named attribute, the definition
typing a usage. A navigation chain, written in a column’s columnFeature, is how the column walks from the
row element to that related element. This page explains how chains are built. The
exact steps and their parameters are listed in Navigation step reference.
A chain starts from baseViewElement, the row’s own element, and wraps it in
navigation steps. Each step takes an element and navigates to elements related to it,
such as Owner(…) for the owning element or NamedUsage(…, "mass", ...) for a feature
by name. The first argument of a step is what it navigates from, and the remaining
arguments are parameters:
// the "torque" attribute of each row element, looked up by name
:>> columnFeature = NamedUsage(baseViewElement, "torque", NameType::name);
Steps nest, and a chain reads from the inside out. This chain hops from each part
usage to the definition typing it, then on to that definition’s torque:
// the "torque" attribute of the definition typing each row element
:>> columnFeature = NamedUsage(
DefinitionsFirst(baseViewElement),
"torque",
NameType::name
);
Show one element, list several, or split the row
Some navigations can only ever find one element: an element’s owner, a requirement’s
subject, a feature looked up by name. Those come as a single step, such as Owner or
NamedUsage, and the result can always be navigated further.
Other navigations may find several elements: the definitions typing a usage, the supertypes of a definition, the ports of a part. Each of those comes as a family of three steps, and the suffix says what happens to the targets it finds:
Suffix |
What it does with the targets it finds |
|---|---|
|
Expects exactly one target and lets further steps chain onto it. If the navigation finds more than one, the cell shows the first with a warning instead of quietly picking one (the truncated outcome). |
|
Renders every target together in one read-only cell. A list cannot be navigated
onwards, so a |
|
Turns the row into one sub-row per target. Because this changes the shape of
the whole table, a |
HeritageList, HeritageFan, and HeritageFirst navigate identically and take the
same parameters. Only the handling of their targets differs.
Share one chain between columns
A chain does not have to be written out inside the column that uses it. Declaring a named navigation at view level gives a chain a name that any column can start from:
view enginesTable : TVD::TableView {
expose DroneProject::Fleet::*;
engine = NamedUsage(
baseViewElement,
"engine",
NameType::name,
UsageKind::partUsage
);
view 'Engine Type' :> columnViews {
:>> columnFeature = DefinitionsFirst(engine);
attribute :>> featureRepresentation :> ColRep::declaredName;
}
view 'Engine Power' :> columnViews {
:>> columnFeature = NamedUsage(
engine,
"power",
NameType::name,
UsageKind::attributeUsage
);
attribute :>> featureRepresentation :> ColRep::featureValue;
}
view 'Engine Documentation' :> columnViews {
:>> columnFeature = engine;
attribute :>> featureRepresentation :> ColRep::specificDocumentation;
}
}
All three columns are about the engine, but the step that finds it is written once.
Engine Type continues to the definition typing the engine, Engine Power to one of
its attributes, and Engine Documentation uses the navigation directly.
Referencing a named navigation is exactly equivalent to writing its chain out in place, so it changes nothing about the rendered table. It is worth reaching for whenever several columns share a prefix: correcting the shared part later means editing one line rather than every column that repeated it.
A named navigation can hold any chain, with two limits. It has to navigate somewhere:
alias = engine; or root = baseViewElement; only renames something that already
has a name, and is rejected with a hint to reference the original directly. And named
navigations may build on one another, as a fan-out that continues another does, but not in a cycle.
Split a row into sub-rows
Every step so far produces at most one cell per row. A Fan step is different: it
turns one row into several, one per target it finds. A table whose rows are parts can
show one row per port of each part, while still showing each part’s own values
alongside.
Take a model with a pump whose two ports share a definition, where only inlet
sets the maxPressure it declares, and a gauge with no ports at all:
port def FluidPort {
attribute maxPressure;
}
part def Station {
part pump {
port inlet : FluidPort {
attribute :>> maxPressure = 5.0;
}
port outlet : FluidPort;
}
part gauge;
}
The view fans each part’s row out over its ports:
ports = UsageFan(baseViewElement, UsageKind::portUsage);
view Part :> columnViews {
attribute :>> featureRepresentation :> ColRep::declaredName;
}
view Port :> columnViews {
:>> columnFeature = ports;
attribute :>> featureRepresentation :> ColRep::declaredName;
}
view 'Max Pressure' :> columnViews {
:>> columnFeature = NamedUsage(
ports,
"maxPressure",
NameType::name,
UsageKind::attributeUsage
);
attribute :>> featureRepresentation :> ColRep::featureValue;
attribute :>> defaultValue = "(unset)";
}
Over that model, the view renders:
Part Port Max Pressure
─────────────────────────────────
pump inlet 5.0
outlet (unset)
gauge N/A N/A
Three things in that picture are what fanning out means:
The fan-out has a name. Splitting rows affects the whole table rather than one column, so a
Fanstep cannot sit inline in a column. It is declared as a named navigation, hereports, and such a name is called an anchor.PortandMax Pressureboth point at the anchor, which is what puts their cells on the same sub-rows.Partnever references it, so it stays one cell per part, spanning the rows its ports produced.outletandgaugeare empty for different reasons.outletexists but sets no value, so its cell shows the column’sdefaultValue.gaugehas no ports at all, so there is no port for either fanned cell to be about, and those cells shownotApplicableValue(see Cell outcomes).gaugestill has a row. A fan-out never removes rows. A parent whose fan-out found nothing keeps exactly one row, so nothing silently disappears from the table.
Anchors
An anchor is what makes several columns share the same sub-rows, so a few rules follow from that role.
The Fan step is always the outermost call of the anchor, never nested inside
another step. Columns then reference the anchor and apply only non-fanning steps to
it, as Max Pressure does with NamedUsage(ports, …). Columns that share an anchor
are aligned by it: on any given row, their cells describe the same port. Two columns
that fan over the same feature through separate anchors are not aligned, so reusing
one anchor is the way to put several columns on one fanned target.
A table can fan out more than once by starting one anchor from another, for example one row per item of each port:
ports = UsageFan(baseViewElement, UsageKind::portUsage);
items = UsageFan(ports, UsageKind::itemUsage);
Anchors therefore form a chain, each starting from baseViewElement or from exactly
one other anchor. What a table cannot do is fan out in two directions at once. Two
anchors that both start from baseViewElement would make every combination of two
unrelated sets, so the table reports an error instead of rendering (see
The whole table is disabled).
Sub-rows appear in the order their targets are declared in the model, so row order is stable across reloads. In a hierarchical table a fanned row’s sub-rows are siblings at the same tree position, and the node’s children hang off the last of them. The tree column itself may never fan out.
Fanned tables on screen
That is everything needed to write a fan-out. What remains is how a fanned table behaves once it is on screen. Open whichever question comes up.
Merged cells
A cell covering several sub-rows renders once at the start of its run and spans them. How finely a column’s cells split depends only on how many fan-outs its chain contains, not on where the column sits: reordering columns, whether through columnOrder or by dragging, changes no row and no cell value. Merging is by identity, not by matching text: two ports that happen to share a name are never merged into one, and a repeated value on genuinely different targets stays repeated.
Editing a merged cell edits the one element behind it, so it is a single change rather
than one per spanned row. Fanned cells write to their own target: an edit in
Max Pressure lands on that row’s port.
Sorting, filtering, and export
Fanned rows change what these operations mean, in ways that keep the table’s structure intact:
Sorting by a fanned column reorders the sub-rows inside each parent’s run rather than across the whole table, so rows are never torn out from under the cell spanning them.
Filtering applies to the fanned rows. Sub-rows that do not match drop out, and a parent drops too once none of its sub-rows survive. This is distinct from a fan-out that found nothing, which keeps its row.
CSV export writes the flattened table: one line per row, with a spanning value repeated on every line it covered, the way a pivot-table export does.
For the columns most tables need, Common patterns has each chain ready to copy.