Skip to content

Commit a6cbccb

Browse files
committed
Surface BigNumber; ARIA direction uses delta sign
Expose BigNumber via AI surface and fix its ARIA wording. Updates: - BigNumber component: buildAriaSentence now uses the numeric delta sign (positive/negative) to pick the direction word ("up"/"down") instead of relying on visual sentiment, avoiding awkward screen-reader phrasing like "up −5" for lower-is-better metrics. Passes delta through to BigNumberInner. - Tests: add accessibility test to assert the ARIA direction follows the delta sign and that visual sentiment styling remains independent. - Scripts: include the `value` subpath in the semiotic/ai export regex and adjust expected export logic/logging since value charts (e.g. BigNumber) are now re-exported. - Docs: correct ThemeProvider import path and update ScaleAwarePage copy to mention BigNumber (with a link to /charts/big-number) and future composition suggestions. Motivation: properly surface BigNumber across the AI/MCP surface and ensure screen readers read factual direction words while keeping sentiment-based visuals separate.
1 parent 3a4dbf8 commit a6cbccb

5 files changed

Lines changed: 66 additions & 16 deletions

File tree

docs/src/pages/charts/BigNumberPage.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect, useRef, useState } from "react"
22
import { BigNumber } from "semiotic/value"
3-
import { ThemeProvider } from "semiotic"
3+
import { ThemeProvider } from "semiotic/utils"
44
import { THEME_PRESETS } from "semiotic/themes"
55
// These chart families are imported *only here in the docs* to demo
66
// dropping a real Semiotic chart into BigNumber's `trendSlot` (wide /

docs/src/pages/features/ScaleAwarePage.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useEffect, useMemo, useState } from "react"
2+
import { Link } from "react-router-dom"
23
import {
34
suggestCharts,
45
suggestChartsGrouped,
@@ -588,12 +589,26 @@ export const ForceDirectedGraphCapability: ChartCapability = {
588589
should — bend to your shop's conventions.
589590
</p>
590591

591-
<h2>The single-value gap</h2>
592+
<h2>The single-value gap (now filled)</h2>
592593
<p>
593-
At <strong>tiny</strong> scale, the engine's catalog currently has one chart:
594-
<code>GaugeChart</code>. Single-value displays (BigNumber, KPI cards, scorecards) aren't
595-
shipped yet — the suggestion engine is honest about wanting them. The roadmap entry for{" "}
596-
<code>SingleValueFrame</code> sketches that gap and the design tradeoffs around filling it.
594+
At <strong>tiny</strong> scale, the engine used to only have{" "}
595+
<code>GaugeChart</code>, and a gauge is misleading without an explicit
596+
min / max. <code>BigNumber</code> (under <code>semiotic/value</code>)
597+
ships as the honest answer for unbounded single-value data — and the
598+
capability layer wires it into <code>suggestCharts</code> /{" "}
599+
<code>suggestChartsGrouped</code> with a <code>scaleFit</code> boost
600+
that puts it ahead of <code>GaugeChart</code> at the{" "}
601+
<strong>tiny</strong> band. See{" "}
602+
<Link to="/charts/big-number">/charts/big-number</Link> for the full
603+
component, and the roadmap entry for <code>SingleValueFrame</code> for
604+
what a future frame-backed version would inherit.
605+
</p>
606+
<p>
607+
Next iteration: <strong>composition suggestions</strong> — sets of N
608+
charts where a <code>BigNumber</code> can host the others via its{" "}
609+
<code>trendSlot</code> / <code>chartSlot</code> rather than the
610+
dashboard rendering them as peer cards. Documented in the roadmap;
611+
not part of this PR.
597612
</p>
598613
</PageLayout>
599614
)

scripts/check-surface-parity.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function parseSchemaComponents() {
7070
function parseSemioticAIChartExports() {
7171
const source = read(files.semioticAI)
7272
const names = new Set()
73-
const exportRegex = /export\s+\{([^}]+)\}\s+from\s+"\.\/charts\/(?:xy|ordinal|network|realtime)\//g
73+
const exportRegex = /export\s+\{([^}]+)\}\s+from\s+"\.\/charts\/(?:xy|ordinal|network|realtime|value)\//g
7474
for (const match of source.matchAll(exportRegex)) {
7575
for (const raw of match[1].split(",")) {
7676
const name = raw.trim().split(/\s+as\s+/)[0].trim()
@@ -135,8 +135,12 @@ const metadataRenderable = new Set(
135135
.map(component => component.name)
136136
)
137137

138+
// Value charts now DO get re-exported from `semiotic/ai` (BigNumber is
139+
// surfaced for the intelligence demo pages), so they're expected in the
140+
// AI exports set. Geo charts stay excluded because they ship under a
141+
// separate subpath and aren't re-exported from `semiotic/ai`.
138142
const expectedAIExports = new Set(
139-
[...validation].filter(name => !geoCharts.has(name) && !valueCharts.has(name))
143+
[...validation].filter(name => !geoCharts.has(name))
140144
)
141145
const expectedMCPRegistry = new Set(
142146
[...validation].filter(name => !realtimeCharts.has(name) && !valueCharts.has(name))
@@ -198,7 +202,7 @@ if (errors.length) {
198202

199203
console.log("AI/MCP surface parity check passed")
200204
console.log(` ${validation.size} validation/schema components`)
201-
console.log(` ${semioticAI.size} semiotic/ai chart exports (${geoCharts.size} geo + ${valueCharts.size} value chart(s) intentionally excluded)`)
205+
console.log(` ${semioticAI.size} semiotic/ai chart exports (${geoCharts.size} geo chart(s) intentionally excluded)`)
202206
console.log(` ${mcpRegistry.size} MCP-renderable components (${realtimeCharts.size} realtime + ${valueCharts.size} value chart(s) intentionally excluded)`)
203207
console.log(` ${metadataComponents.size} shared AI metadata components`)
204208
console.log(` ${serverConfigs.size} server render configs (+ ${SERVER_CONFIG_EXCLUDED.size} documented HOC-SSR exclusions)`)

src/components/charts/value/BigNumber.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,30 @@ describe("BigNumber — accessibility", () => {
359359
expect(aria).toContain("of weekly goal")
360360
})
361361

362+
it("ARIA direction word follows the delta sign, not sentiment (lower-is-better)", () => {
363+
// Latency went DOWN (good under lower-is-better) — sentiment is
364+
// positive but the screen reader sentence should still report
365+
// "down 60 ms" (factual direction), not "up 60 ms".
366+
const { container } = render(
367+
<BigNumber
368+
value={340}
369+
label="P99 latency"
370+
suffix=" ms"
371+
comparison={{ value: 400, label: "vs last week", direction: "lower-is-better" }}
372+
/>
373+
)
374+
const aria = container
375+
.querySelector("[data-chart='BigNumber']")
376+
?.getAttribute("aria-label") ?? ""
377+
expect(aria).toContain("down")
378+
expect(aria).not.toContain("up")
379+
// Sentiment-positive (good) coloring still applies — direction word
380+
// and visual sentiment are independent concerns.
381+
expect(
382+
container.querySelector("[data-sentiment='positive']")
383+
).not.toBeNull()
384+
})
385+
362386
it("uses description prop as the ARIA override when provided", () => {
363387
const { container } = render(
364388
<BigNumber value={1} description="custom description for this card" />

src/components/charts/value/BigNumber.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,9 @@ function buildAriaSentence(args: {
286286
formattedValue: string
287287
unit?: string
288288
comparisonLabel?: string
289+
delta?: number | null
289290
deltaFormatted?: string | null
290291
deltaPercent?: string | null
291-
sentiment: "positive" | "negative" | "neutral"
292292
targetLabel?: string
293293
targetPercent?: string | null
294294
stale: boolean
@@ -300,12 +300,19 @@ function buildAriaSentence(args: {
300300
args.unit ? `${args.formattedValue} ${args.unit}` : args.formattedValue
301301
)
302302
if (args.deltaFormatted) {
303+
// Direction word follows the SIGN of the delta, not the sentiment.
304+
// Sentiment is a styling concern ("good" / "bad" colour) that flips
305+
// when `direction: "lower-is-better"` — but for a screen reader the
306+
// factual phrasing must remain "up" for +5 and "down" for −5
307+
// regardless. Avoids the previous "up −5" reading on lower-is-better
308+
// metrics.
309+
const d = args.delta
303310
const dirWord =
304-
args.sentiment === "positive"
305-
? "up"
306-
: args.sentiment === "negative"
307-
? "down"
308-
: "change"
311+
d != null && Number.isFinite(d) && d !== 0
312+
? d > 0
313+
? "up"
314+
: "down"
315+
: "change"
309316
const pct = args.deltaPercent ? ` (${args.deltaPercent})` : ""
310317
const comp = args.comparisonLabel ? ` from ${args.comparisonLabel}` : ""
311318
parts.push(`${dirWord} ${args.deltaFormatted}${pct}${comp}`)
@@ -652,9 +659,9 @@ const BigNumberInner = (
652659
: "",
653660
unit,
654661
comparisonLabel: comparison?.label,
662+
delta: computedDelta,
655663
deltaFormatted,
656664
deltaPercent,
657-
sentiment,
658665
targetLabel: target?.label,
659666
targetPercent,
660667
stale,

0 commit comments

Comments
 (0)