@@ -47,7 +47,7 @@ const PRECIP_BAR_COLOR = "rgba(127, 184, 224, 0.65)"; // dusty rain blue
4747const BAND_FILL = " rgba(232, 130, 107, 0.16)" ; // coral, low alpha — ±1σ band
4848const BAND_SWATCH = " rgba(232, 130, 107, 0.45)" ; // more visible coral for the legend chip
4949const TRUTH_AREA = " rgba(245, 185, 66, 0.12)" ; // sodium, low alpha — precip truth fill
50- const NIGHT_FILL = " rgba(10, 16, 24 , 0.55 )" ;
50+ const NIGHT_FILL = " rgba(120, 140, 200 , 0.12 )" ; // cool marine wash — reads as night against the warm theme
5151const MODEL_PALETTE = [" #6dc6c2" , " #9bb87a" , " #bfa9d6" , " #f0a285" , " #7fb8e0" , " #d99a1e" , " #e8826b" , " #9ddad6" , " #c7b69a" , " #a8c182" , " #b88c8c" ];
5252const MODEL_OPACITY = 0.55 ;
5353
@@ -63,7 +63,10 @@ const WINDOW_CHOICES = [
6363] as const ;
6464
6565// ---- UI state ---------------------------------------------------------------
66- const view = ref <ChartViewId >(props .variables [0 ] ?? " temperature_2m" );
66+ // Forecast (no truth) opens on the combined temperature + precipitation view;
67+ // verify keeps per-variable views so each can show its own ERA5 truth line.
68+ const canCombineTempPrecip = ! props .data .truth && props .variables .includes (" temperature_2m" ) && props .variables .includes (" precipitation" );
69+ const view = ref <ChartViewId >(canCombineTempPrecip ? " temp_precip" : (props .variables [0 ] ?? " temperature_2m" ));
6770const hoursWindow = ref <number >(props .defaultWindow );
6871
6972// Visibility toggles. Kept OUT of `option` so toggling them patches the chart
@@ -97,14 +100,47 @@ function selectView(v: ChartViewId): void {
97100 view .value = v ;
98101}
99102
100- // Variable dropdown (replaces the old full-width button rail; sits next to the
101- // window selector in the chart header).
103+ // Variable picker: an expanded rail on desktop, a dropdown on mobile.
102104const varOpen = ref (false );
103105const varRoot = ref <HTMLElement | null >(null );
104106onClickOutside (varRoot , () => (varOpen .value = false ));
105107
106- function selectVariable(v : ChartViewId ): void {
107- selectView (v );
108+ /** Whether a picker entry reads as "active". Temperature and precipitation are
109+ * a combinable pair on the forecast page: either is active in the composite. */
110+ function isVarActive(vid : ChartViewId ): boolean {
111+ if (canCombineTempPrecip ) {
112+ if (vid === " temperature_2m" ) return view .value === " temp_precip" || view .value === " temperature_2m" ;
113+ if (vid === " precipitation" ) return view .value === " temp_precip" || view .value === " precipitation" ;
114+ }
115+ return view .value === vid ;
116+ }
117+
118+ function selectVariable(vid : ChartViewId ): void {
119+ // Temperature & precipitation toggle independently (dual-axis), so the two
120+ // can be shown together — that combination *is* the composite view. The
121+ // remaining variables are exclusive single-axis views.
122+ if (canCombineTempPrecip && (vid === " temperature_2m" || vid === " precipitation" )) {
123+ const inPair = view .value === " temp_precip" || view .value === " temperature_2m" || view .value === " precipitation" ;
124+ let tempOn = view .value === " temp_precip" || view .value === " temperature_2m" ;
125+ let precipOn = view .value === " temp_precip" || view .value === " precipitation" ;
126+ if (! inPair ) {
127+ // Coming from an exclusive view (wind / cloud / prob) → focus the click.
128+ tempOn = vid === " temperature_2m" ;
129+ precipOn = vid === " precipitation" ;
130+ } else if (vid === " temperature_2m" ) {
131+ tempOn = ! tempOn ;
132+ } else {
133+ precipOn = ! precipOn ;
134+ }
135+ // Never leave the pair empty: toggling off the last one is a no-op.
136+ if (! tempOn && ! precipOn ) {
137+ tempOn = vid === " temperature_2m" ;
138+ precipOn = vid === " precipitation" ;
139+ }
140+ selectView (tempOn && precipOn ? " temp_precip" : tempOn ? " temperature_2m" : " precipitation" );
141+ } else {
142+ selectView (vid );
143+ }
108144 varOpen .value = false ;
109145}
110146
@@ -224,6 +260,17 @@ function selectNoModels(): void {
224260 applyModelVisibility ();
225261}
226262
263+ // Single "All" toggle: active only when every available model is enabled;
264+ // clicking flips between all-on and all-off.
265+ const allModelsActive = computed (() => {
266+ const available = allModels .value .filter ((m ) => modelHasData .value [m .id ]);
267+ return available .length > 0 && available .every ((m ) => enabledModels .value .has (m .id ));
268+ });
269+ function toggleAllModels(): void {
270+ if (allModelsActive .value ) selectNoModels ();
271+ else selectAllModels ();
272+ }
273+
227274// ---- Tooltip formatting -----------------------------------------------------
228275function fmtVar(dv : DataVarId , base : number | null | undefined ): string {
229276 if (dv === " temperature_2m" ) return formatTemp .value (base , 1 );
@@ -281,15 +328,32 @@ const option = computed<EChartsOption>(() => {
281328 }
282329 : undefined ;
283330
331+ // Night shading lives on its own zero-z background series so it always sits
332+ // *behind* the spread band and lines (it used to ride the band-base series,
333+ // which drew it on top of the spread).
334+ if (markArea ) {
335+ series .push ({
336+ id: " night" ,
337+ type: " line" ,
338+ yAxisIndex: 0 ,
339+ data: Array .from ({ length: n }, () => null ),
340+ symbol: " none" ,
341+ lineStyle: { opacity: 0 },
342+ silent: true ,
343+ z: 0 ,
344+ tooltip: { show: false },
345+ markArea ,
346+ });
347+ }
348+
284349 // --- helper: push an aggregate line + band for a line variable -------------
285350 const pushLineAggregate = (dv : DataVarId , axisIndex : number , attachMarks : boolean ): void => {
286351 const pts = (props .data .aggregate [dv ] ?? []).slice (0 , n );
287352 const values = pts .map ((p ) => convertVar (p .value , dv , units .value ));
288353 const smooth = dv === " temperature_2m" ;
289354
290355 // Band (±1σ). Always built — even in spaghetti mode — so the spread can be
291- // toggled independently and stays visible behind the per-model lines. The
292- // invisible band-base carries the night-shading markArea.
356+ // toggled independently and stays visible behind the per-model lines.
293357 const lower = pts .map ((p ) => convertVar (p .value - p .stdDev , dv , units .value ));
294358 const delta = pts .map ((p ) => (Number .isFinite (p .stdDev ) ? convertDelta (p .stdDev * 2 , dv , units .value ) : 0 ));
295359 series .push ({
@@ -303,7 +367,7 @@ const option = computed<EChartsOption>(() => {
303367 itemStyle: { opacity: 0 },
304368 tooltip: { show: false },
305369 data: lower ,
306- ... ( attachMarks && markArea ? { markArea } : {}) ,
370+ z: 1 ,
307371 });
308372 series .push ({
309373 id: " band-delta" ,
@@ -315,7 +379,7 @@ const option = computed<EChartsOption>(() => {
315379 areaStyle: { color: BAND_FILL },
316380 tooltip: { show: false },
317381 data: delta ,
318- z: 0 ,
382+ z: 1 ,
319383 });
320384
321385 series .push ({
@@ -346,8 +410,7 @@ const option = computed<EChartsOption>(() => {
346410 data: values ,
347411 itemStyle: { color: PRECIP_BAR_COLOR },
348412 barWidth: " 60%" ,
349- z: 1 ,
350- ... (attachMarks && markArea ? { markArea } : {}),
413+ z: 2 ,
351414 ... (attachMarks && markLine ? { markLine } : {}),
352415 });
353416 const truth = props .data .truth ?.precipitation ;
@@ -552,8 +615,8 @@ const hasBand = computed(() => view.value !== "precipitation");
552615 v-for =" vid in variables"
553616 :key =" vid"
554617 class =" border-ink-700 border-r px-2.5 py-1 whitespace-nowrap transition-colors last:border-r-0"
555- :class =" view === vid ? 'bg-sodium-300/15 text-sodium-200' : 'text-paper-300 hover:bg-ink-800 hover:text-paper-50'"
556- @click =" selectView (vid)"
618+ :class =" isVarActive( vid) ? 'bg-sodium-300/15 text-sodium-200' : 'text-paper-300 hover:bg-ink-800 hover:text-paper-50'"
619+ @click =" selectVariable (vid)"
557620 >
558621 {{ CHART_VIEWS[vid].label }}
559622 </button >
@@ -583,9 +646,9 @@ const hasBand = computed(() => view.value !== "precipitation");
583646 :key =" vid"
584647 type =" button"
585648 role =" menuitemradio"
586- :aria-checked =" view === vid"
649+ :aria-checked =" isVarActive( vid) "
587650 class =" block w-full px-3 py-2 text-left font-mono text-xs tracking-wide transition-colors"
588- :class =" view === vid ? 'bg-ink-800 text-sodium-200' : 'text-paper-200 hover:bg-ink-800 hover:text-sodium-200'"
651+ :class =" isVarActive( vid) ? 'bg-ink-800 text-sodium-200' : 'text-paper-200 hover:bg-ink-800 hover:text-sodium-200'"
589652 @click =" selectVariable(vid)"
590653 >
591654 {{ CHART_VIEWS[vid].label }}
@@ -617,10 +680,9 @@ const hasBand = computed(() => view.value !== "precipitation");
617680 </div >
618681
619682 <!-- Legend / filter strip -------------------------------------------- -
620- Two labelled sections — SERIES (always-on toggles: aggregate, truth)
621- and MODELS (per-model spaghetti chips). Replaces the old "Show
622- contributing models" checkbox: enabling any model chip turns the
623- spaghetti on; "none" turns it back off. -->
683+ Two labelled sections — SERIES (aggregate / spread / truth toggles)
684+ and MODELS (an "All" toggle plus per-model spaghetti chips). Enabling
685+ any model chip turns the spaghetti on; "All" flips every model at once. -->
624686 <div class =" border-ink-700/60 mt-4 space-y-2.5 border-t pt-3 font-mono text-[11px] tracking-wide" >
625687 <!-- SERIES -->
626688 <div class =" flex flex-wrap items-center gap-1.5" >
@@ -657,24 +719,16 @@ const hasBand = computed(() => view.value !== "precipitation");
657719 <!-- MODELS -->
658720 <div v-if =" hasModels" class =" flex flex-wrap items-center gap-1.5" >
659721 <span class =" text-paper-400 mr-2 w-14 shrink-0" >Models</span >
660- <div class =" flex items-center gap-px" >
661- <button
662- type =" button"
663- class =" border-ink-700 hover:border-sodium-300/60 text-paper-300 hover:text-sodium-200 bg-ink-950 border px-2 py-1 transition-colors"
664- title =" Enable all models"
665- @click =" selectAllModels"
666- >
667- all
668- </button >
669- <button
670- type =" button"
671- class =" border-ink-700 hover:border-sodium-300/60 text-paper-300 hover:text-sodium-200 bg-ink-950 -ml-px border px-2 py-1 transition-colors"
672- title =" Disable all models"
673- @click =" selectNoModels"
674- >
675- none
676- </button >
677- </div >
722+ <button
723+ type =" button"
724+ class =" border px-2 py-1 transition-colors"
725+ :class =" allModelsActive ? 'border-ink-600 bg-ink-800 text-paper-50' : 'border-ink-700 bg-ink-950 text-paper-400 hover:text-paper-200'"
726+ :aria-pressed =" allModelsActive"
727+ :title =" allModelsActive ? 'Disable all models' : 'Enable all models'"
728+ @click =" toggleAllModels"
729+ >
730+ All
731+ </button >
678732 <span class =" bg-ink-700 mx-1 hidden h-4 w-px sm:inline-block" aria-hidden =" true" />
679733 <button
680734 v-for =" m in allModels"
0 commit comments