@@ -13,6 +13,7 @@ use maud::PreEscaped;
1313use maud:: html;
1414
1515use super :: render:: escape_json_for_script;
16+ use super :: render:: filter_icon;
1617use super :: summary:: summary_markup;
1718use super :: toolbar:: per_chart_toolbar;
1819use super :: toolbar:: range_strip;
@@ -48,7 +49,7 @@ pub(super) struct LandingGroup {
4849/// Render the landing-page body — one `<section>` per group, each wrapping a
4950/// `<details>` disclosure. The `chart-data-N` script ids are globally
5051/// indexed so `chart-init.js` can find every payload by integer.
51- pub ( super ) fn landing_body ( groups : & [ LandingGroup ] ) -> Markup {
52+ pub ( super ) fn landing_body ( groups : & [ LandingGroup ] , universe : & api :: FilterUniverse ) -> Markup {
5253 if groups. is_empty ( ) {
5354 return html ! {
5455 p. empty { "No data ingested yet." }
@@ -73,6 +74,7 @@ pub(super) fn landing_body(groups: &[LandingGroup]) -> Markup {
7374 }
7475 }
7576 ( summary_markup( group. summary. as_ref( ) ) )
77+ ( per_group_toolbar( universe) )
7678 div. chart-grid {
7779 @for ( chart_idx, link) in group. chart_links. iter( ) . enumerate( ) {
7880 @let idx = idx_iter. next( ) . expect( "indices match charts" ) ;
@@ -88,6 +90,102 @@ pub(super) fn landing_body(groups: &[LandingGroup]) -> Markup {
8890 }
8991}
9092
93+ /// Render the per-group toolbar that lets the user override the global filter
94+ /// and Y-axis scale across every chart in the group. The toolbar is a sibling
95+ /// of `.chart-grid`; CSS hides it when the enclosing `<details>` is closed,
96+ /// mirroring the rule that hides `.chart-grid` itself.
97+ ///
98+ /// Layout: Y-axis buttons on the left, a centered "Filter series" dropdown
99+ /// trigger, and a Reset button on the right. The dropdown panel contains
100+ /// engine and format macro chips (which expand to "toggle every series with
101+ /// this engine/format") plus a series row whose chips are populated by JS as
102+ /// charts in the group hydrate and surface their `payload.series_meta`.
103+ ///
104+ /// Resolution layering (driven by `chart-init.js`):
105+ /// - Per-card legend overrides win over everything.
106+ /// - The per-group filter (`hiddenSeries`) hides next.
107+ /// - The global filter hides last.
108+ /// - The Y-axis pass skips charts where the user previously clicked the
109+ /// per-chart Y toolbar (`canvas.__bench_y_user_set`).
110+ fn per_group_toolbar ( universe : & api:: FilterUniverse ) -> Markup {
111+ html ! {
112+ section. group-toolbar data-role="group-toolbar" {
113+ div. toolbar-group. group-toolbar-y role="group" aria-label="Group Y-axis scale" {
114+ span. toolbar-label { "Y" }
115+ // Linear is the resting default (matches each chart's
116+ // own default) so it ships highlighted; the JS keeps it
117+ // lit while the per-group Y is unset or explicitly linear.
118+ button. toolbar-btn. toolbar-btn--active
119+ type ="button" data-group-y="linear" { "linear" }
120+ button. toolbar-btn type ="button" data-group-y="log" { "log" }
121+ }
122+ div. group-filter-dropdown data-role="group-filter-dropdown" {
123+ button. control-btn. filter-trigger. group-filter-trigger
124+ type ="button"
125+ data-role="group-filter-trigger"
126+ aria-haspopup="true"
127+ aria-expanded="false" {
128+ ( filter_icon( ) )
129+ span { "Filter series" }
130+ }
131+ div. filter-panel. group-filter-panel data-role="group-filter-panel" hidden {
132+ ( group_macro_row( "Engine" , "engine" , & universe. engines) )
133+ ( group_macro_row( "Format" , "format" , & universe. formats) )
134+ div. global-filter-row. group-series-row {
135+ span. global-filter-label { "Series" }
136+ button. filter-chip. filter-chip--all
137+ type ="button"
138+ data-group-filter="series"
139+ data-value="*"
140+ aria-pressed="false" {
141+ "all"
142+ }
143+ // Series chips hydrate client-side once a chart in this
144+ // group exposes its `payload.series_meta`. Until then
145+ // the row only shows the Engine/Format macros above.
146+ div. group-series-chips data-role="group-series-chips" { }
147+ }
148+ }
149+ }
150+ button. group-toolbar-reset
151+ type ="button"
152+ data-role="group-toolbar-reset" {
153+ "Reset group"
154+ }
155+ }
156+ }
157+ }
158+
159+ /// Render an engine/format macro row inside the per-group filter panel. The
160+ /// macro chip click bulk-toggles every known series whose `engine`/`format`
161+ /// matches; the chip's active state reflects "every matching series is
162+ /// currently visible". `data-group-filter` distinguishes these chips from the
163+ /// global filter's `data-filter` ones so the click handler can route them to
164+ /// the per-group state.
165+ fn group_macro_row ( label : & str , dim : & str , universe : & [ String ] ) -> Markup {
166+ html ! {
167+ div. global-filter-row. group-macro-row {
168+ span. global-filter-label { ( label) }
169+ button. filter-chip. filter-chip--all
170+ type ="button"
171+ data-group-filter=( dim)
172+ data-value="*"
173+ aria-pressed="false" {
174+ "all"
175+ }
176+ @for value in universe {
177+ button. filter-chip. filter-chip--active
178+ type ="button"
179+ data-group-filter=( dim)
180+ data-value=( value)
181+ aria-pressed="true" {
182+ ( value)
183+ }
184+ }
185+ }
186+ }
187+ }
188+
91189/// Render the small ⓘ info icon that surfaces the group's editorial
92190/// description on hover and on focus. The CSS-only tooltip uses a
93191/// `data-tooltip` attribute so it shows below the icon (see `style.css`'s
0 commit comments