Universal axis¶
Technical specification of the bar every result module is built from.
Docs: Universal axis | Design: Figma

Scope¶
One presentational component. It renders a value on a track and nothing else - no scoring, no data access, no interaction, no state. Every module that shows a score composes it: single axis, double axis, multi axis, horizontal bar, Nolan chart, archetype, the checkpoint cards, and the generated result image.
Inputs¶
| Input | Data | Meaning | Rules |
|---|---|---|---|
| Start entry | Orientation and value | The orientation anchored to the left cap | Optional |
| End entry | Orientation and value | The orientation anchored to the right cap | Optional; its presence is what makes the bar double-sided |
| Comparison | Orientation and value | The other party on the same track | Optional; exactly one, never a list |
| Marker | Number, 0-100 | Position of the reference line | Defaults to 50, can be disabled |
| Labels | Boolean | Whether orientation names show under the caps | Off by default |
An orientation carries an identifier, a display name, an image and a colour. Anything with points is one - an ideology, a party, a candidate, an archetype, a trait, and a friend in comparison mode. A friend is passed as an orientation like any other, their avatar being the image, so the component never distinguishes a person from an ideology.
Colour and image come from quiz configuration or from a user profile, so both are untrusted data rather than design tokens.
A value is a share of the track from 0 to 100. The component never normalises: whatever proportion the caller passes is what gets drawn.
Modes¶
| Mode | Condition |
|---|---|
| Empty | Neither entry present |
| One-sided | One entry present, anchored to its own cap |
| Double-sided | Both entries present |
Each entry is anchored to the cap that belongs to it, so an end entry alone renders as a one-sided bar filling from the right.
The mode follows which entries are passed, not which have values. An entry without a value means the orientation is known and its score is not: it keeps its cap, so a double-sided bar with one unknown side stays double-sided. It draws like a value of zero and is announced as missing.
Fill geometry¶
- Each fill starts at its own cap and extends by its value as a share of the track width.
- Any remainder stays unfilled. On a double-sided bar with values that do not reach 100 between them, the gap sits in the middle.
- A cap is rendered only for an entry that is present.
- Fills never overlap the opposite cap.
Value label cases¶
| Case | Rendering |
|---|---|
| One-sided, value at or above the fit threshold | Value inside the fill |
| One-sided, value below the fit threshold | Value immediately after the fill, in the orientation colour against the track |
| One-sided, value is zero | No value shown |
| Double-sided, side at or above its fit threshold | Value inside that side's fill |
| Double-sided, side below its fit threshold | No value for that side; the other side still shows its own |
| Entry absent | No value shown |
| Value labels switched off - the checkpoint cards | No value shown on or next to any fill, and none in the description. See axis closeness, single axis puzzle and double axis puzzle |
The fit thresholds are fixed percentages, one per mode, not measured from the rendered text. Measuring would make the same bar render differently depending on which font loaded, the locale's digits and the environment doing the drawing, and this bar has to look identical in the app, on a checkpoint card and inside the generated image. Fixed numbers also cost nothing on a screen holding dozens of bars. The trade is that each threshold is tuned once for the widest realistic label rather than being exactly right for every one.
Values are displayed rounded. Layout always uses the exact value, so rounding never moves a fill.
With a comparison¶
A comparison does not remove the numbers. A side keeps its value wherever the band and the other party's image leave it uncovered.
| Case | Rendering |
|---|---|
| The other party's position is clear of the side's number | The value is shown by the rules above |
| The other party's position is within the clearance of the cap the number sits at | No value for that side; the other side of a double-sided bar still shows its own |
| Value that would be written after the fill | No value shown |
| The whole track is hatched, because the taker has no entry or no value | No value shown on either side |
The clearance is a fixed percentage, one per mode, a little above the fit threshold so that it also leaves room for the other party's image. It is tuned once and never measured, for the same reasons as the thresholds. A number is never drawn under hatching or under the image; a hidden value is still carried by the description for assistive technology.
The frame at the top of this page still draws its comparison states without numbers. It predates this rule and is to be redrawn; until then the table above is what holds.
The numbers stay because modules that colour a bar by something other than the orientation - a match band, for one - rely on the number to say what the colour says.
Marker cases¶
| Case | Rendering |
|---|---|
| Default | Line at the midpoint |
| Configured | Line at the given position |
| Disabled | Not drawn |
| At 0 or 100 | Drawn at the edge, fully inside the track |
The marker sits above the fills and below the comparison layer, and is drawn whether or not any entry is present.
Comparison cases¶
Comparison carries exactly one other party. Group comparison is not a case this component handles - a screen comparing several people composes several bars.
Hatching exists for comparison, with one exception named below. Without a comparison the bar has no hatched area at all, and with one the hatched band is exactly the span between the two values - the difference is the thing being drawn.
The exception is the masked state of the single axis puzzle: while the taker is guessing, the whole track is hatched and no fill is drawn, so the bar gives nothing away. It ends with the guess.
| Case | Rendering |
|---|---|
| Other is ahead | Hatched band from the taker's value to theirs, continuing past the fill |
| Other is behind | Hatched band from their value to the taker's, drawn over the fill |
| Values are equal | No band; the image alone marks the shared position |
| Taker has no entry, or an entry without a value | The whole track is hatched and only the other party's image is positioned |
| Other is at 0 or 100 | The image is clamped so it stays fully inside the track |
| Double-sided bar | The band is measured against the start entry on the same shared track |
The band always ends at the taker's fill as it is drawn. When two values exceed the track and the fills are scaled, the band follows the scaled fill, not the value as given, so it never runs past the fill it belongs to. Values that exceed the track are already wrong input, and what is drawn then is a degraded bar, not a correct one: the other party's position is not scaled with the fills, so the band can end up on the other side of the fill than the values as given would put it.
One hatching pattern serves both directions. Direction is conveyed by where the band sits, never by colour, because both cases must be recognisable wherever the bar is drawn. A wider band means a bigger disagreement, which is what makes a screen of bars scannable for where two people actually differ.
Draw order is fills, then marker, then band, then the other party's image, which is always topmost. Which values stay visible next to the band is set in value label cases.
Label cases¶
- A label sits under the cap of the entry it names, on one line, truncated when it does not fit.
- The full name remains available to assistive technology even when the visible text is truncated.
- When labels are enabled their row is reserved whether or not a name is present, so enabling them never shifts a neighbouring bar.
Invalid and edge input¶
| Input | Behaviour |
|---|---|
| Value below 0 or above 100 | Clamped into range |
| Values that together exceed the track | Both fills scaled proportionally so they meet without overlapping |
| Value missing or not a number | The entry stays present: its cap and label are drawn, with no fill and no number |
| Orientation without an image | Cap renders with colour only |
| Orientation without a colour | Falls back to a neutral colour |
| Orientation name longer than the track | Truncated visually, preserved for assistive technology |
Nothing here throws. Quiz configuration is author-supplied and can be wrong; a broken bar must degrade rather than take a result screen down.
Rendering contexts¶
The same component renders in the app, on checkpoint cards mid-quiz, and inside the generated result image, at one size in all of them. There is no compact variant: the bar fills the width it is given and everything on it - caps, type, marker, hatching - keeps its proportions. That is also what makes identical output across the three contexts achievable, and it rules out anything depending on hover, viewport, animation state, measured text or fonts that are not embedded.
Accessibility¶
- The bar is not interactive and exposes no focusable elements.
- It is announced as a single image whose description carries the orientation names, their values, and the comparison value when present.
- Colour is never the only carrier of meaning: values are written, comparison uses a pattern, and the marker is a shape.
- Value text must meet contrast against the fill it sits on, which is why a small value moves out of the fill rather than shrinking - the type size is the same everywhere.
Performance¶
A single screen can hold dozens of instances, and an opened multi axis group can add dozens more at once. The component does no measurement work per instance beyond layout, and the entry animation runs on first paint only, disabled under a reduced-motion preference.