Stack a bar's readings, and stop widgets taking the phone sideways
A bar drew its nested reading on top of the outer one in --chart-5, which measures 2.53:1 against --primary and lost the 3:1 guideline for non-text. The readings now partition the fill end to end, up to three of them, in a token of their own: --primary-nested, the primary hue a few steps deeper, 3.14:1 light and 3.12:1 dark. It cannot also clear 3:1 against --muted — in dark those two are 5.82:1 apart and a colour 3:1 from both would need a 9:1 gap — so a segment is drawn inside a gutter of outer fill rather than ever bordering the track, which is what separates neighbours too, and what caps the count at three. A nested value larger than its outer used to spill onto the track; it is clamped. `inner` still reads as a single binding, so no dashboard needs migrating. On a phone, .widget-grid took its width from the widest thing any widget held — a truncating flex item still offers its whole unwrapped line as a min-content contribution — and a handful of widgets had no floor of their own: the uPlot legend is a table, a fieldset carries min-inline-size: min-content from the UA sheet, and buttons are whitespace-nowrap. Each is capped now. A widget's body scrolls rather than clipping, so long text stops painting over the title. Gauges and bars move between readings instead of jumping, and a segmented control slides one thumb rather than recolouring cells. The gauge arc is drawn whole and revealed by its dash, because `d` cannot be transitioned. UplotChart pushed new readings only when the point count changed, so once a rolling window was full a refetch left the old values on screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs
This commit is contained in:
@@ -30,6 +30,12 @@ DASHBOARD_DIR = "_dashboards"
|
||||
#: A chart cannot ask for an unbounded series; this is the ceiling.
|
||||
HISTORY_CAP = 5000
|
||||
|
||||
#: How many readings a bar may nest inside its own. The limit is contrast, not
|
||||
#: layout: the segments share one fill token, because no slot of the chart ramp
|
||||
#: clears 3:1 against the outer one, and a fourth could not be told from its
|
||||
#: neighbour. Mirrored in the client (``BarWidget.tsx``).
|
||||
BAR_SEGMENTS = 3
|
||||
|
||||
#: Resolved out here on purpose: the store has a ``list`` method, which
|
||||
#: shadows the builtin for any annotation written inside the class.
|
||||
Bindings = list[dict[str, Any]]
|
||||
@@ -119,6 +125,23 @@ class WidgetDef(BaseModel):
|
||||
"""A chart that asks a flow for its series instead of reading the ring."""
|
||||
return self.type == "chart" and self.config.get("source") == "query"
|
||||
|
||||
@property
|
||||
def inner_bindings(self) -> Bindings:
|
||||
"""A bar's nested readings, in either shape a document may carry them.
|
||||
|
||||
One binding beside ``inner_dtype``, as a bar was written before it
|
||||
stacked, or an ordered list of ``{message, dtype}`` — so an older
|
||||
dashboard keeps drawing without being migrated first.
|
||||
"""
|
||||
inner = self.config.get("inner")
|
||||
if isinstance(inner, list):
|
||||
return [s for s in inner[:BAR_SEGMENTS] if isinstance(s, dict)]
|
||||
dtype = self.config.get("inner_dtype")
|
||||
# A recorded type with nothing bound is still a type to be held to.
|
||||
if inner or dtype:
|
||||
return [{"message": inner or "", "dtype": dtype}]
|
||||
return []
|
||||
|
||||
@property
|
||||
def messages(self) -> list[str]:
|
||||
"""Every message name this widget reads."""
|
||||
@@ -132,8 +155,9 @@ class WidgetDef(BaseModel):
|
||||
if series.get("message")
|
||||
]
|
||||
name = self.config.get("message")
|
||||
inner = self.config.get("inner") # only a bar nests a second reading
|
||||
return [str(value) for value in (name, inner) if value]
|
||||
# Only a bar nests further readings inside the one it draws.
|
||||
nested = [s.get("message") for s in self.inner_bindings]
|
||||
return [str(value) for value in (name, *nested) if value]
|
||||
|
||||
@property
|
||||
def target(self) -> str:
|
||||
@@ -170,7 +194,10 @@ class WidgetDef(BaseModel):
|
||||
str(series.get("dtype") or "")
|
||||
for series in self.config.get("series") or []
|
||||
]
|
||||
return [str(self.config.get(key) or "") for key in ("dtype", "inner_dtype")]
|
||||
return [
|
||||
str(self.config.get("dtype") or ""),
|
||||
*(str(s.get("dtype") or "") for s in self.inner_bindings),
|
||||
]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_binding(self) -> WidgetDef:
|
||||
@@ -184,6 +211,10 @@ class WidgetDef(BaseModel):
|
||||
)
|
||||
return self
|
||||
|
||||
inner = self.config.get("inner")
|
||||
if isinstance(inner, list) and len(inner) > BAR_SEGMENTS:
|
||||
raise ValueError(f"a bar nests at most {BAR_SEGMENTS} readings")
|
||||
|
||||
allowed = WIDGET_DTYPES.get(self.type)
|
||||
if not allowed:
|
||||
return self
|
||||
@@ -510,6 +541,7 @@ def default_dashboard(name: str) -> DashboardDef:
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BAR_SEGMENTS",
|
||||
"DASHBOARD_DIR",
|
||||
"HISTORY_CAP",
|
||||
"INPUT_WIDGETS",
|
||||
|
||||
Reference in New Issue
Block a user