Repository navigation
Commit cd50bc4
feat(ai): theme charts, and describe them to a screen reader (#10)
* feat(ai): theme charts, and describe them to a screen reader
Adds AITheme — a ThemeExtension a host registers on ThemeData.extensions —
with ChartThemeData as its first component theme, plus a ChartTheme
InheritedTheme for subtree overrides and a `theme` argument on both chart
widgets. Thirteen nullable fields cover the palette, the plot height, the
scatter and bubble radii, the histogram's bucket count, the axis, pie-label
and title styles, the grid line colour and the heatmap's three ramp stops.
An unset field derives from the ambient ColorScheme, so overriding the
palette leaves everything else following the app in both brightnesses.
There was nothing to hang a chart theme on: no ThemeExtension and no
InheritedWidget existed here outside 2.3's translations scope. The shape
mirrors stream_core_flutter's StreamTheme (<Component>ThemeData plus an
InheritedTheme, defaults resolved in the widget's build) so a host of both
SDKs finds what it expects, but AITheme carries no brightness, colour scheme
or typography of its own — every widget here already resolves from the
ambient ColorScheme, and a second brightness would be a second source of
truth able to disagree with Theme.of. copyWith/merge/lerp/== are hand-written:
one component theme does not justify a codegen dependency in a package whose
premise is a short dependency list.
In lerp, a field set on only one side swaps at t == 0.5 instead of
interpolating. MaterialApp animates its ThemeData, so lerp runs on every
light/dark switch, and null here means "derive from the theme" rather than
"transparent" or "zero" — Color.lerp(null, c, t) would fade a grid line
through transparency mid-transition and lerpDouble(null, 220, t) would grow a
chart up from zero height. Palettes of different lengths interpolate by
wrapping the shorter one, the way it is cycled at paint time, so no series
loses its colour partway through.
Neither AIMarkdownBody nor StreamingMessageView gained a parameter, so the
fence widget cache's key is untouched: a chart resolves the theme from its own
BuildContext and the inherited-dependency mechanism marks the cached element
dirty regardless of widget identity. ai_markdown_body_test asserts it — one
identical widget instance, two palettes. A chartTheme constructor argument
left out of that global key would instead let two differently-themed bodies
share one palette.
Charts also had no Semantics at all, fl_chart painting to a canvas, so a
ChartView was an empty box to a screen reader. Each now carries one node
summarising the kind, title, axes, series, counts and value range, with a
sentence per USpecKind. ChartSemantics.fromSpec gathers the facts and formats
the numbers for speech; AITranslations.chartSemanticsLabel composes the
sentence, so it is translatable like every other string — one method rather
than a dozen phrase-sized ones, because a sentence's word order varies far
more between languages than a tooltip's does. The node excludes its subtree:
the tick labels and a heatmap's row, column and legend labels are real Text
widgets, and walking them yields a run of bare numbers with nothing saying
which axis they belong to. This is also the first code here to read
USpec.xLabel and USpec.yLabel, which the parsers had been filling in for
nobody.
Two visible changes. Pie slice labels pick black or white per slice instead of
always white, which disappears on any light slice a host palette might carry;
under the default palette that flips every label dark. And USpecParser leaves
an unnamed series' name empty instead of substituting 'Series' or 'Pie' —
2.3 deferred those here, since a parser has no BuildContext — with the name
resolved at render time from AITranslations.unnamedChartSeries. The 'Pie'
variant is dropped rather than translated: it reached the screen nowhere.
All ten committed CI goldens are unchanged, defaults having been preserved
verbatim.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ai): describe only the chart that is drawn, and format its numbers
Two defects in the chart accessibility summary, both reaching the screen
reader as the only description a chart has — its Semantics node excludes
the painted axis labels, so there is nothing else to fall back on.
The trailing-zero strip in `_formatValue` ran on the whole formatted
number instead of the fraction, so anything rounding to `x.00` kept a
dangling point: 99.999 became '100.', read aloud as "one hundred point".
Values below 0.005 collapsed to '0.' at both ends of the range, losing
the range entirely.
`ChartSemantics.fromSpec` also flattened every series, while `ChartView`
plots only the first for a pie and a histogram — so a two-series pie
showing four slices announced eight, and computed the largest slice's
share against a total it never draws. `USpecParser` accepts a multi-series
pie from model output, so nothing unusual was needed to hit it.
Deriving the described series list up front also removes an unguarded
`reduce` over an empty first series, which threw `StateError` inside
`build` and took the whole `AIMarkdownBody` down with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(ai): cover the chart theme's dark half, and correct the docs
Adds the coverage the theming work was missing, each case checked by
breaking the code it guards and watching it fail:
- No test set a dark Brightness, so `_kDarkScale` and the three ramp
fallbacks never ran. Delete the brightness check and the suite stayed
green while every dark-mode heatmap inverted the intensity it encodes.
- The ramp test matched with `contains` over an unordered bag of fills,
so its own `reason` strings ("the minimum cell sits at the low stop")
were unchecked and a reversed ramp passed. Now positional, and the mid
stop is asserted too.
- `copyWith`/`merge`/`==`/`hashCode` are four hand-written 13-line
tables, spot-checked on two fields. A loop over all thirteen asserts
each one round-trips and that nothing else moves, which is what catches
a crossed line such as `scatterRadius: bubbleMinRadius ?? ...`.
- `ChartTheme.wrap` had no coverage at all, though carrying the scope
across a route is the reason the class is an InheritedTheme. Mirrors
the AITranslationsScope route test.
Doc corrections:
- `USeries.name` said "shown in legends". This package draws no legend,
and the field is now `''` for an unnamed series, which the doc never
mentioned.
- The README described `ChartView` as "line, bar, or pie ... powered by
fl_chart" (it handles eight kinds, and the heatmap is a widget grid),
listed two parser formats of seven, and said unset theme fields derive
from the ambient ColorScheme — five of them fall back to constants.
- The README's Dutch `chartSemanticsLabel` example labelled every chart
a bar chart; it now switches on `chart.kind` and handles `isEmpty`.
- The example's pie comment pointed at an amber slice the four-slice
chart never reached, and the ROADMAP claimed the example showed the
unnamed-row fallback while every series was named. Both are now true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(ai): add goldens for the themed and dark chart
The eight chart goldens all render the default theme, so they show that
defaults didn't move and nothing else — a feature whose whole point is
visual had no visual evidence of what it does, which is why running
update_goldens produced no new files: there were no new cases to render.
Four cases covering the two things a host actually changes:
- `chart_view_themed_bar` — a host palette and grid colour reaching the
paint.
- `chart_view_themed_heatmap` — a host three-stop ramp.
- `chart_view_dark_line` — the default line chart under a dark theme.
- `chart_view_dark_heatmap` — the ramp inversion `_kDarkScale` exists
for, which until now had no coverage of any kind and which the
luminance test can only describe.
Scope, noted at the group: CI goldens obscure text with an opaque paint,
so these pin fills, grid lines and the ramp. Label colours — including
the per-slice pie contrast — are invisible to every golden here and stay
covered by the widget tests.
The committed `goldens/ci/` PNGs have to be rendered on Linux, so these
four fail locally until the update_goldens workflow generates them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: Update Goldens
* docs(ai): compact the chart theming docs
The ChartThemeData docs argued their design at length where a caller only
needs to know what a field does and what it falls back to. Every field doc
is now one or two lines, and the class doc keeps the three things a host
acts on — null means derive, how the three levels compose, copyWith can't
unset — while the essay on why layout isn't themeable is down to the one
consequence a host can see.
Rationale comments survive only where they stop a wrong change: _swap's
reason not to interpolate a null, _lerpPalette's wrapping, and AITheme's
hand-written == that ThemeData compares with mapEquals. The rest of the
argument lives in the ROADMAP entry, which is where it belongs.
Three docs were also wrong, not just long:
- `ChartView.semanticsLabel` said `''` adds "no semantics at all". It
removes this widget's node and so puts the excluded subtree labels back
in reach — the opposite of hiding the chart.
- `ChartSemantics.seriesNames` claimed axis order; it is series order.
- `rowCount`/`columnCount` read as heatmap-only but are populated for
every kind, which a translator reading them could not have known.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ai): describe the chart that is drawn, and pick a readable slice label
Five review findings on roadmap 2.4, all confirmed against the code.
Pie slice labels no longer go through ThemeData.estimateBrightnessForColor,
which compares its 0.15 threshold to (luminance + 0.05)² and so switches at
0.34 — leaving white on mid blue (3.34:1), orange (2.85:1) and red (3.82:1),
under the 4.5:1 an 11px label needs. The label is now whichever of white and
black87 has the better WCAG contrast against the slice, composited first.
Five of the six default colors flip dark; purple keeps white, which reads
better on it. The widget test asserts the ratio over kDefaultChartSeriesColors
rather than a navy/yellow pair any threshold would pass — the CI goldens can't
cover this, since alchemist blocks fl_chart's canvas-painted slice titles.
ChartSemantics._formatValue rounds before it decides how to render. It caught
exact -0.0 only, so every value in (-0.005, 0) formatted as '-0.00' and was
stripped to '-0', voiced as "minus zero". The guarding test used y: -0, the
one input that hit the short-circuit.
A size range is gathered only for a bubble chart. Every other kind draws its
points at one flat radius, but the parsers fill UPoint.size in regardless of
the mark, so a Vega-Lite {"mark": "point", "encoding": {"size": …}} — a
scatter — announced "sizes 1000000 to 9000000" over identical dots. The gate
is in fromSpec, so no translation can reintroduce it.
A heatmap names its rows and columns instead of only counting them. It rarely
carries axis labels, the row and column are what identify a cell, and the
painted labels are excluded from the tree — so "2 rows by 4 columns" was the
whole description. Rows come from seriesNames, columns from the new
ChartSemantics.columnLabels. Counts are singularised too ("1 slice").
ChartTheme.merge layers onto an enclosing scope, since `of` reads the nearest
one only and nesting otherwise shadows the outer palette. Kept IconTheme's
behaviour and added its escape hatch; the README's "the three compose" claim
now says which three.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(ai): correct three claims in the new chart comments
Self-audit of the comments added in the previous commit, since adding
prose to fix a prose-accuracy problem is the same failure mode.
- ChartSemantics.columnLabels said a heatmap draws them "down the side
and along the bottom". It draws them along the bottom; the side is the
rows, from seriesNames (heatmap_chart_view.dart:61-132).
- ChartView._onSlice punctuated a paraphrase as a quotation. Flutter's
comment reads "appears to bias more towards", not "biases more
towards".
- ChartTheme.merge claimed folding AITheme in would leave a widget
outliving a change to it. It wouldn't — the Builder depends on AITheme
through 'of' and rebuilds. The reason that holds is that it keeps
'data' the overrides its own doc describes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(ai): cut the chart comments back to their point
The previous two commits added 64 lines of comment against 20 of code.
Trimmed to what a reader needs and can't infer: that
estimateBrightnessForColor switches at 0.34 rather than the 0.15 it
names, that black87 is composited before measuring, that only a bubble
draws UPoint.size, that _formatValue rounds before deciding, and that
nesting a ChartTheme replaces. Dropped the measured contrast tables, the
ChartTheme.merge sample and the restated rationale — the ROADMAP entry
carries all of it at length.
Net comment lines across the review round: +64 -> +30.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(ai): tighten the chart theming and accessibility docs
A pass over the doc comments 2.4 added, not only the ones this review
round touched.
Cut the bug archaeology, which git blame holds better than a doc
comment does: how the scatter axis used to index per series, how a
Chart.js null gap shifted points left, how clamping raw bubble sizes
flattened the encoding. Each is now one clause naming the trap rather
than a paragraph recounting it. Also cut the restated rationale in
AITheme, ChartThemeData, HeatmapChartView and ResolvedChartTheme — the
ROADMAP entry carries all of it.
Kept every 'don't simplify this back' note: why lerp swaps a
one-sided field instead of interpolating, why the row-label gutter
isn't themeable, why the pie label style may carry no colour, why the
heatmap grid keys rows by label.
Comment density across 2.4's lib additions: 38% -> 32%.
AITheme's doc-only import of ai_translations.dart went with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(ai): make ChartSemantics compare by value
Follows #11, which gave USpec, USeries and UPoint value equality.
ChartSemantics is derived from them and exported alongside them, so a
host memoizing on one or asserting against one in a test was the odd
case out.
Matches uspec.dart's shape: identical-first, ListEquality for the two
string lists, Object.hashAll for their deep hashes.
The test pairs two specs per field, each differing in exactly one
derived fact, and asserts both != and a different hashCode. A set of
distinct variants would not do: a field dropped from == alone still
hashes apart and lands in another bucket, so the collision never
happens. Verified by removing three fields from == and three from
hashCode and watching the matching test fail each time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: renefloor <15101411+renefloor@users.noreply.github.com>1 parent a5d5cd7 commit cd50bc4
23 files changed
Lines changed: 3470 additions & 281 deletions
File tree
- packages/stream_chat_flutter_ai
- example/lib
- lib
- src
- chart
- localization
- theme
- components
- test/src
- chart
- goldens/ci
- theme
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
13 | 30 | | |
14 | 31 | | |
15 | 32 | | |
| |||
69 | 86 | | |
70 | 87 | | |
71 | 88 | | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
72 | 145 | | |
73 | 146 | | |
74 | 147 | | |
| |||
285 | 358 | | |
286 | 359 | | |
287 | 360 | | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
288 | 384 | | |
289 | 385 | | |
290 | 386 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
170 | 170 | | |
171 | 171 | | |
172 | 172 | | |
173 | | - | |
174 | | - | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
175 | 176 | | |
176 | 177 | | |
177 | | - | |
| 178 | + | |
| 179 | + | |
178 | 180 | | |
179 | 181 | | |
180 | 182 | | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
181 | 226 | | |
182 | 227 | | |
183 | 228 | | |
| |||
610 | 655 | | |
611 | 656 | | |
612 | 657 | | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
613 | 690 | | |
614 | 691 | | |
615 | 692 | | |
| |||
0 commit comments