Repository navigation
Expand file tree
/
Copy pathmodels.html
More file actions
4530 lines (4497 loc) · 383 KB
/
Copy pathmodels.html
File metadata and controls
4530 lines (4497 loc) · 383 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
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
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Short-Range Model Maps: HRRR, RRFS and RDPS · Shawn Corvec</title>
<meta name="description" content="Zoomable maps of the newest HRRR, RRFS and RDPS runs: radar and precipitation type, snow bands, clouds, temperature, wind, ECAPE, smoke and sunshine.">
<link rel="canonical" href="https://scorvec.com/models.html">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<meta property="og:type" content="website">
<meta property="og:title" content="Short-range model maps: HRRR, RRFS and RDPS">
<meta property="og:description" content="Zoomable maps of the newest HRRR, RRFS and RDPS runs: radar and precipitation type, clouds and visibility, temperature, wind, ECAPE, smoke and sunshine, with hover values.">
<meta property="og:url" content="https://scorvec.com/models.html">
<meta property="og:image" content="https://scorvec.com/hero-supercell-1280.webp">
<meta property="og:site_name" content="Shawn Corvec — Weather & Climate Monitors">
<meta name="twitter:card" content="summary_large_image">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "Short-range model maps: HRRR, RRFS and RDPS",
"url": "https://scorvec.com/models.html",
"description": "Zoomable maps of the newest HRRR, RRFS and RDPS runs: radar and precipitation type, snow bands, clouds, temperature, wind, ECAPE, smoke and sunshine.",
"image": "https://scorvec.com/hero-supercell-1280.webp",
"isPartOf": {
"@type": "WebSite",
"name": "Shawn Corvec",
"url": "https://scorvec.com/"
},
"author": {
"@id": "https://scorvec.com/#person"
}
}
</script>
<link rel="preconnect" href="https://scorvec-store.b-cdn.net" crossorigin>
<link rel="preconnect" href="https://frames.scorvec.com" crossorigin>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,500;0,8..60,600;1,8..60,400&family=Source+Sans+3:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
<script src="/assets/site.js?v=6a624e5f1e" defer></script>
<script src="/assets/frames_root.js?v=bdced09224"></script>
<link rel="stylesheet" href="/assets/site.css?v=08996a48dc">
<style>
/* models.html: the page draws the data itself - scripts/models/models.py ships one 8-bit lossless WebP
per field and hour on the model's native grid, and the WebGL code below colours, projects and reads it. */
:root {--map: #e9ebe6;}
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--ink); font-family: var(--sans); line-height: 1.5; }
/* full width on any screen */
main.mv { max-width: none; margin: 0 auto; padding: 1.2rem clamp(0.75rem, 3vw, 3.5rem) 3rem; }
.kicker { font-size: 0.88rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); margin: 0.2rem 0 0; }
h1 { font-family: var(--serif); font-weight: 500; font-size: var(--t-h1); line-height: 1.15; margin: 0.25rem 0 0.4rem; }
h2 { font-family: var(--serif); font-weight: 500; font-size: 1.3rem; margin: 1.8rem 0 0.5rem; }
/* the intro sits in the site's lede box (site.css "main > .lede") */
main.mv > .lede { color: var(--ink); font-size: 1.16rem; max-width: 80ch; margin: 0.7rem auto 1.1rem; }
.lede a, .mv-about a { color: var(--accent); }
.mv-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.45rem 0.9rem; margin: 0 0 0.55rem; }
.seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--rule); border-radius: 7px; background: var(--panel); overflow: hidden; }
/* controls: large, bold type */
.seg button { font: inherit; font-size: 1.14rem; font-weight: 600; border: 0; border-right: 1px solid var(--rule); background: transparent; color: var(--ink);
padding: 0.62rem 1.05rem; min-height: 46px; cursor: pointer; white-space: nowrap; }
.seg button:last-child { border-right: 0; }
.seg button[aria-pressed="true"] { background: var(--accent); color: #fff; }
.seg button:disabled { color: #8a8f8a; cursor: not-allowed; background: repeating-linear-gradient(135deg, transparent 0 5px, rgba(0,0,0,0.035) 5px 10px); }
.seg button:not(:disabled):not([aria-pressed="true"]):hover { background: var(--accent-soft); }
.mv-fields { flex: 1 1 100%; display: flex; flex-wrap: wrap; gap: 0.35rem 0.8rem; align-items: flex-end; }
/* a group is one row, its name at the left: seven groups take three rows, not six and six captions, so the map
opens near the top of the window */
.mv-grp { display: flex; align-items: center; gap: 0.45rem; }
.mv-fields .seg button { font-size: 1rem; padding: 0.38rem 0.7rem; min-height: 36px; }
.mv-grp > span { font-size: 0.78rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted); padding-left: 2px; }
.seg button small { font-size: 0.78em; opacity: 0.8; margin-left: 0.25em; }
.runsel.mv-fsel { display: none; }
/* min-width 0: a wrapping row is as wide as the bar, never as wide as its longest line (it ran 100 px past a 320 px phone) */
.mv-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.45rem 0.9rem; flex: 1 1 100%; min-width: 0; }
.mv-show { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 1.3rem; margin: 0.45rem 0 0; font-size: 1.12rem; color: var(--ink); }
.mv-show > b { font-weight: 600; color: var(--ink); }
.mv-keys { margin-left: auto; font-size: 0.92rem; color: var(--muted); }
.mv-keys kbd { font: inherit; font-size: 0.78rem; border: 1px solid var(--rule); border-bottom-width: 2px; border-radius: 4px; padding: 0 0.3em; background: var(--panel); }
.mv-legT { margin: 0.1rem 0 0; font-size: 0.97rem; color: var(--muted); }
.mv-acc { display: flex; flex-wrap: wrap; gap: 0.45rem 0.9rem; align-items: center; flex: 1 1 100%; min-width: 0; }
.mv-acc[hidden] { display: none; }
/* a picker never runs past the window: its select shrinks (the longest option names are wider than a 320 px phone) */
.runsel { display: inline-flex; align-items: center; gap: 0.45rem; font-size: 1.12rem; font-weight: 600; color: var(--ink); min-width: 0; max-width: 100%; }
.runsel select { font: inherit; font-size: 1.12rem; font-weight: 600; padding: 0.55rem 0.6rem; min-height: 46px; border: 1px solid var(--rule); border-radius: 6px; background: var(--panel); color: var(--ink);
flex: 0 1 auto; min-width: 0; max-width: 100%; text-overflow: ellipsis; }
/* the meteogram's keyboard way in: an airport or city by code or name */
.mv-find { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0.6rem; margin: 0.45rem 0 0; font-size: 1.04rem; color: var(--ink); }
.mv-find label { font-weight: 600; }
.mv-find input { font: inherit; padding: 0.45rem 0.6rem; min-height: 44px; width: 17rem; max-width: 100%; border: 1px solid var(--rule); border-radius: 6px; background: var(--panel); color: var(--ink); }
.mv-find button { font: inherit; font-weight: 600; padding: 0.45rem 0.95rem; min-height: 44px; border: 1px solid var(--rule); border-radius: 6px; background: var(--panel); color: var(--ink); cursor: pointer; }
.mv-find output { color: var(--ink2); }
.chk[hidden] { display: none; }
.chk { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 1.12rem; font-weight: 600; color: var(--ink); cursor: pointer; user-select: none; min-height: 40px; }
.chk input { width: 1.25rem; height: 1.25rem; accent-color: var(--accent); margin: 0; }
/* time bar + map + colour legend fit in the window together once scrolled to (sticky header ~64 px, time bar ~62,
legend ~70): no 880 px cap */
/* touch: one finger scrolls the page, two move and zoom the map (the map is most of a phone's screen; a one-finger
pan there left no way to scroll past it) */
/* the map fills the window under its controls with its Show row still on screen (site.js sets --fit-top and
--fit-below), never shorter than 420 px */
.mv-map { position: relative; width: 100%; height: max(420px, calc(100vh - var(--fit-top, 260px) - var(--fit-below, 0px) - 14px)); background: var(--map); border: 1px solid var(--rule);
border-radius: 8px; overflow: hidden; touch-action: pan-x pan-y; cursor: grab;
-webkit-touch-callout: none; -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; }
.mv-map.drag { cursor: grabbing; }
.mv-map:focus { outline: none; }
.mv-map:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
/* how to zoom or pan when a gesture went to the page instead (scroll without Ctrl, one finger on a touch screen) */
.mv-coop { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 5; pointer-events: none; max-width: min(26rem, 90%);
background: rgba(20, 24, 22, 0.85); color: #fff; border-radius: 8px; padding: 0.6rem 0.95rem; font-size: 1.04rem; text-align: center;
opacity: 0; transition: opacity 0.2s; }
.mv-coop.on { opacity: 1; }
.mv-map canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
/* the line overlay is moved by the compositor during a gesture (CSS transform, no canvas redraw); the label canvas is
hidden while the map moves and redrawn once it settles */
.mv-map canvas#ov { transform-origin: 0 0; will-change: transform; }
.mv-map canvas#lab { pointer-events: none; }
.mv-zoom { position: absolute; right: 8px; top: 8px; display: flex; flex-direction: column; gap: 4px; z-index: 3; }
.mv-zoom button { width: 48px; height: 48px; font: 700 1.5rem var(--sans); border: 1px solid var(--rule); border-radius: 6px;
background: rgba(255,255,255,0.92); color: var(--ink); cursor: pointer; }
.mv-zoom button.txt { font-size: 0.98rem; }
.mv-badge { position: absolute; left: 8px; top: 8px; z-index: 3; background: rgba(255,255,255,0.93); border: 1px solid var(--rule); border-radius: 6px;
padding: 0.3rem 0.6rem; font-size: 0.95rem; color: var(--ink); max-width: calc(100% - 60px); pointer-events: none; }
.mv-badge b { font-weight: 600; }
.mv-load { position: absolute; left: 8px; bottom: 8px; z-index: 3; background: rgba(255,255,255,0.93); border: 1px solid var(--rule); border-radius: 6px;
padding: 0.2rem 0.5rem; font-size: 0.82rem; color: var(--ink2); pointer-events: none; }
.mv-load:empty { display: none; }
/* sounding prompt on the map: says how to get a sounding where it is seen (a hint in the Show row would be small, at
the far right, hidden on phones and absent while a run's soundings are still being computed). It sits just under the
title badge (placeSndHint sets the exact top as the badge wraps): at the map's foot it was below the fold on a
laptop. Dismissible; the dismissal is remembered per browser. */
.mv-sndhint { position: absolute; left: 8px; top: 52px; z-index: 3; display: flex; align-items: center; gap: 0.5rem;
max-width: calc(100% - 16px); background: #1f3b5c; color: #fff; border-radius: 8px; padding: 0.42rem 0.45rem 0.42rem 0.75rem;
font: 500 0.98rem/1.3 var(--sans); box-shadow: 0 2px 10px rgba(0,0,0,0.25); }
.mv-sndhint[hidden] { display: none; }
.mv-sndhint.wait { background: rgba(255,255,255,0.95); color: var(--ink); border: 1px solid var(--rule); box-shadow: none; }
.mv-sndhint b { font-weight: 700; }
.mv-sndhint button { flex: 0 0 auto; border: 0; background: rgba(255,255,255,0.18); color: inherit; border-radius: 5px;
width: 30px; height: 30px; font: 600 1.1rem var(--sans); cursor: pointer; }
.mv-sndhint.wait button { background: var(--panel); }
.mv-msg { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 2; max-width: min(34rem, 90%); text-align: center;
background: rgba(255,255,255,0.96); border: 1.5px dashed #b9beb6; border-radius: 8px; padding: 1.1rem 1.3rem; font-size: 0.97rem; color: var(--ink2); }
.mv-msg:empty { display: none; }
.mv-tip { position: absolute; z-index: 4; pointer-events: none; background: rgba(20, 24, 22, 0.9); color: #fff; font-size: 0.84rem; line-height: 1.35;
padding: 0.3rem 0.5rem; border-radius: 5px; white-space: nowrap; transform: translate(12px, 12px); display: none; }
.mv-tip .v { font-weight: 600; font-size: 0.95rem; }
.mv-tip .ll { color: #cfd6cf; }
/* time bar above the map with a thick slider, easy to grab */
.mv-time { display: flex; align-items: center; gap: 0.5rem; margin: 0.2rem 0 0.55rem; flex-wrap: wrap; }
.mv-time button { font: inherit; font-size: 1.35rem; font-weight: 700; min-width: 3.3rem; height: 3rem; border: 1px solid var(--rule); border-radius: 6px; background: var(--panel);
color: var(--ink); cursor: pointer; }
.mv-time input[type=range] { flex: 1 1 14rem; min-width: 10rem; height: 3rem; margin: 0; background: transparent; cursor: pointer;
-webkit-appearance: none; appearance: none; touch-action: manipulation; }
.mv-time input[type=range]::-webkit-slider-runnable-track { height: 14px; border-radius: 7px; background: #cfd5cc; border: 1px solid #b8beb5; }
.mv-time input[type=range]::-moz-range-track { height: 14px; border-radius: 7px; background: #cfd5cc; border: 1px solid #b8beb5; }
.mv-time input[type=range]::-moz-range-progress { height: 14px; border-radius: 7px; background: var(--accent); }
.mv-time input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 30px; height: 30px; margin-top: -9px;
border-radius: 50%; background: var(--accent); border: 3px solid #fff; box-shadow: 0 0 0 1px var(--accent), 0 1px 4px rgba(0,0,0,0.35); }
.mv-time input[type=range]::-moz-range-thumb { width: 26px; height: 26px; border-radius: 50%; background: var(--accent); border: 3px solid #fff;
box-shadow: 0 0 0 1px var(--accent), 0 1px 4px rgba(0,0,0,0.35); }
.mv-time input[type=range]:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 8px; }
.mv-valid { font-size: 1.05rem; color: var(--ink); min-width: 15rem; line-height: 1.3; }
.mv-valid .vt { font-size: 1.22rem; font-weight: 600; }
.mv-valid .loc { color: var(--ink2); }
.mv-valid .fh { display: inline-block; margin-left: 0.35rem; padding: 0 0.4rem; border-radius: 4px; background: var(--accent-soft); color: var(--accent); font-weight: 600; font-size: 0.9rem; }
/* controls in one tinted panel; colour bar vertical on the right of the map */
.mv-panel { --muted: var(--muted-tint); background: #e6edf4; border: 1px solid #c9d6e3; border-radius: 10px; padding: 0.7rem 0.85rem 0.15rem; margin: 0 0 0.6rem; }
.mv-panel .mv-bar { margin-bottom: 0.4rem; }
.mv-mapwrap { display: flex; gap: 8px; align-items: stretch; position: relative; }
.mv-mapwrap > .mv-map { flex: 1 1 auto; min-width: 0; }
/* the canvas is taken out of the flow: its backing store is sized from its own box x devicePixelRatio, and in flow that
intrinsic height feeds back into the stretched row height in some browsers (Safari), so every redraw would make the
row - and the bar - taller */
.mv-legend { flex: 0 0 96px; margin: 0; position: relative; }
.mv-legend canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.mv-legT { margin-top: 0.3rem; }
.mv-note { margin: 0.5rem 0 0; padding: 0.55rem 0.8rem; background: var(--panel); border: 1px solid var(--rule); border-left: 4px solid var(--accent);
border-radius: 4px; font-size: 1.02rem; color: var(--ink2); max-width: 80rem; }
.mv-note:empty { display: none; }
/* data-use warning for metered connections; sizes measured on the 2026-10-10 18Z cycles */
.mv-data { max-width: calc(80ch * 1.16 / 0.98); margin: -0.4rem auto 1rem; padding: 0.55rem 1.1rem; background: #fdf6e7; border: 1px solid #ecdcb8;
border-left: 4px solid #b26b00; border-radius: 8px; font-size: 0.98rem; color: var(--ink2); } /* width = the lede box (80ch at 1.16rem) */
.mv-data b { color: var(--ink); font-weight: 600; }
.mv-note b { color: var(--ink); font-weight: 600; }
.mv-about { max-width: 80rem; color: var(--ink2); font-size: 1.04rem; margin-top: 1.4rem; border-top: 1px solid var(--rule); padding-top: 0.6rem; }
.mv-about > summary { cursor: pointer; font-family: var(--serif); font-size: 1.25rem; color: var(--ink); padding: 0.3rem 0; }
.mv-about h2 { font-size: 1.12rem; margin: 1.1rem 0 0.4rem; }
.mv-about table { border-collapse: collapse; width: 100%; font-size: 0.98rem; margin: 0.4rem 0 0.8rem; background: var(--panel); }
.mv-about th, .mv-about td { border: 1px solid var(--rule); padding: 0.35rem 0.5rem; text-align: left; vertical-align: top; }
.mv-about th { background: #eef1ec; font-weight: 600; color: var(--ink); }
.mv-about td.na { color: #6f6b64; font-style: italic; }
.mv-about .tbl { overflow-x: auto; }
/* snow-band case studies: the site's loop player in an iframe */
.mv-cases { display: flex; flex-wrap: wrap; gap: 0.5rem 1.2rem; margin: 0.4rem 0 0.6rem; }
.mv-casef iframe { display: block; width: 100%; border: 1px solid var(--rule); border-radius: 8px; background: #fcfcfa; }
.mv-runs { font-size: 0.88rem; color: var(--muted); margin: 0.2rem 0 0; }
/* meteogram (click an airport or city): plain canvases, drawn by the page, in a dock - a large panel centred on the
window, 2 vh from the top and bottom, with the map still visible (and clickable) either side on a wide screen (click
another airport to switch, click a chart to send the map to that hour); "Full window" takes the whole width; phones
always full. */
.mv-mg { position: fixed; top: 2vh; bottom: 2vh; left: 50%; transform: translateX(-50%); width: min(2400px, 94vw);
z-index: 1300; margin: 0; background: var(--panel); border: 1px solid var(--rule); border-radius: 12px;
box-shadow: 0 18px 60px rgba(20, 24, 22, 0.28); overflow: hidden; display: flex; flex-direction: column; }
.mv-mg.full { top: 0; bottom: 0; width: 100vw; border: 0; border-radius: 0; }
.mv-mg[hidden] { display: none; }
.mv-mg:focus { outline: none; }
/* forecast sounding dock: the meteogram's frame around the sounding explorer in an iframe. On a wide screen it sits
at the right and leaves the map's left third visible, so another spot can be clicked without closing it (centred
at 94 vw it left a 3 vw sliver either side, and a click there opened an airport's meteogram over it instead). */
@media (min-width: 1300px) { .mv-snd { left: auto; right: 1.5vw; transform: none; width: min(2000px, 68vw); } }
.mv-snd iframe { flex: 1 1 auto; min-height: 0; width: 100%; border: 0; display: block; background: #0b0b12; }
.mv-snd .mv-mgtop { padding-bottom: 0.55rem; }
.mv-mgtop { flex: none; padding: 0.75rem 1.3rem 0.65rem; border-bottom: 1px solid var(--rule); background: var(--panel); }
.mv-mgbody { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 0.3rem 1.3rem 1.5rem; }
.mv-mgh { display: flex; flex-wrap: wrap; align-items: center; gap: 0.45rem 1rem; }
.mv-mgh h2 { margin: 0; font-size: clamp(1.75rem, 1rem + 0.9vw, 2.6rem); line-height: 1.1; }
.mv-mgh .sub { color: var(--ink2); font-size: clamp(1.1rem, 0.6rem + 0.55vw, 1.5rem); }
.mv-mgbtns { margin-left: auto; display: inline-flex; gap: 0.5rem; }
.mv-mgh .x { font: inherit; font-size: 1.05rem; font-weight: 600; border: 1px solid #c3c8c0; border-radius: 7px; background: var(--panel);
padding: 0.45rem 0.95rem; min-height: 44px; cursor: pointer; color: var(--ink); }
.mv-mgh .x:hover { border-color: var(--accent); }
.mv-mgh .x kbd { font: 600 0.78rem 'Source Sans 3', sans-serif; color: var(--muted); border: 1px solid var(--rule); border-radius: 4px; padding: 0 0.3rem; margin-left: 0.3rem; }
.mv-key { display: inline-flex; flex-wrap: wrap; gap: 0.3rem 1.1rem; font-size: 1.08rem; color: var(--ink2); }
.mv-key i { display: inline-block; width: 1.6em; height: 0; border-top: 4px solid; vertical-align: middle; margin-right: 0.35em; }
.mv-key label { cursor: pointer; user-select: none; }
.mv-key input { width: 1.05em; height: 1.05em; vertical-align: -0.12em; margin-right: 0.3em; }
.mv-mgr { margin: 0.55rem 0 0; font-variant-numeric: tabular-nums; }
.mgr-when { font-size: 1.12rem; color: var(--ink); }
.mgr-when span { color: var(--muted); font-size: 0.98rem; }
.mgr-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(15.5rem, 1fr)); gap: 0.5rem; margin-top: 0.4rem; }
.mgr-tile { border: 1px solid var(--rule); border-left: 5px solid var(--c); border-radius: 7px; padding: 0.4rem 0.7rem; background: #fbfcfa; }
.mgr-tile .hd { font-weight: 700; color: var(--c); font-size: clamp(1rem, 0.5rem + 0.5vw, 1.4rem); }
.mgr-tile .hd span { color: var(--muted); font-weight: 500; margin-left: 0.35rem; }
.mgr-tile .big { font-family: var(--serif); font-size: clamp(1.7rem, 0.9rem + 0.9vw, 2.6rem); line-height: 1.15; color: var(--ink); }
.mgr-tile .big small { font-family: 'Source Sans 3', sans-serif; font-size: 0.95rem; color: var(--ink2); margin-left: 0.3rem; }
.mgr-tile .ln { font-size: clamp(1.02rem, 0.55rem + 0.5vw, 1.4rem); color: var(--ink2); line-height: 1.4; }
.mgc { margin: 0.9rem 0 0; }
.mgc p { margin: 0 0 0.15rem; font-size: clamp(1.18rem, 0.55rem + 0.7vw, 1.75rem); font-weight: 600; color: var(--ink); }
.mgc p small { color: var(--muted); font-size: clamp(0.98rem, 0.5rem + 0.45vw, 1.3rem); font-weight: 400; margin-left: 0.5em; }
.mgc canvas { display: block; width: 100%; touch-action: pan-y; cursor: crosshair; }
#mgN { font-size: 0.98rem; margin-top: 1rem; }
@media (max-width: 900px) {
.mv-mg { top: 0; bottom: 0; width: 100vw; border: 0; border-radius: 0; }
#mgW { display: none; }
.mv-mgtop { padding: 0.6rem 0.8rem 0.5rem; }
.mv-mgbody { padding: 0.2rem 0.8rem 1.2rem; }
.mv-mgh h2 { font-size: 1.45rem; }
.mgr-tiles { grid-template-columns: 1fr; }
.mgr-tile .big { font-size: 1.4rem; }
/* phones: the whole sheet scrolls (a fixed readout of three tiles would leave the charts a sliver) */
.mv-mg { overflow-y: auto; overscroll-behavior: contain; display: block; }
.mv-mg.mv-snd:not([hidden]) { display: flex; overflow: hidden; }
.mv-mgbody { overflow: visible; }
.mgr-tile .ln { font-size: 0.95rem; }
}
@media (max-width: 640px) {
main.mv { padding-top: 0.6rem; }
main.mv > .lede { font-size: 1.04rem; padding: 0.75rem 0.9rem; margin: 0.4rem 0 0.8rem; }
.mv-data { font-size: 0.97rem; margin: -0.2rem 0 0.8rem; }
.seg button { padding: 0.55rem 0.72rem; font-size: 1.06rem; min-height: 46px; }
.runsel, .runsel select, .chk { font-size: 1.04rem; }
.mv-time button { min-width: 2.9rem; }
.mv-fields { display: none; }
.runsel.mv-fsel { display: inline-flex; flex: 1 1 12rem; }
.mv-fsel select { flex: 1 1 auto; min-width: 0; }
/* phones: the map first (the introduction and the data note move under it), a taller map, smaller map buttons */
main.mv { display: flex; flex-direction: column; }
main.mv > .lede, main.mv > .mv-data { order: 1; }
main.mv > .mv-mapwrap ~ * { order: 2; }
main.mv > .lede { margin-top: 0.9rem; }
.mv-map { height: clamp(340px, calc(100svh - 180px), 700px); }
.mv-zoom button { width: 36px; height: 36px; font-size: 1.15rem; }
.mv-zoom button.txt { font-size: 0.82rem; }
.mv-time { margin: 0 0 0.35rem; padding: 0.2rem 0 0.25rem; }
.mv-panel { padding: 0.5rem 0.55rem 0.05rem; }
.mv-mapwrap { flex-direction: column; gap: 0.3rem; }
.mv-sndhint { position: static; max-width: none; font-size: 0.92rem; padding: 0.35rem 0.4rem 0.35rem 0.65rem; } /* phones: under the map, not over it */
.mv-legend { flex: 0 0 auto; }
.mv-legend canvas { position: static; height: 38px; }
.mv-valid { min-width: 0; flex: 1 1 100%; font-size: 0.95rem; order: -1; }
.mv-valid .vt { font-size: 1.08rem; }
.mv-time input[type=range] { flex: 1 1 5rem; min-width: 5rem; }
.mv-keys { display: none; }
.kicker { display: none; }
.mv-badge { font-size: 0.86rem; padding: 0.22rem 0.5rem; }
.mv-mg { padding: 0.55rem 0.55rem 0.7rem; }
/* phones: the three model tiles side by side and compact, so the charts start on the first screen */
.mgr-tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.3rem; }
.mgr-tile { padding: 0.3rem 0.4rem; border-left-width: 4px; }
.mgr-tile .hd { font-size: 0.92rem; }
.mgr-tile .big { font-size: 1.3rem; }
.mgr-tile .big small { display: block; margin: 0; font-size: 0.8rem; }
.mgr-tile .ln { font-size: 0.78rem; line-height: 1.3; }
.mv-mgtop { position: relative; }
.mv-mgh { padding-right: 5.6rem; }
.mv-mgbtns { position: absolute; top: 0.55rem; right: 0.6rem; }
}
/* the controls scale with the window (--ui, set at the end of the head): full size on a large screen, smaller on a
laptop, so the buttons leave the map most of the window; phones (<= 640 px) have their own compact layout */
@media (min-width: 641px) { .mv-bar, .mv-time, .mv-show, .mv-find, .mv-zoom { zoom: var(--ui, 1); } }
/* above 640 px the viewer opens the page: the introduction and the data note go under the map, its legend caption
and Show row; a short window gets a compact title, and the map picker is a menu */
@media (min-width: 641px) {
main.mv { display: flex; flex-direction: column; }
main.mv > .mv-mapwrap ~ * { order: 3; }
main.mv > .mv-mapwrap ~ :is(.mv-legT, .mv-show, .mv-find) { order: 1; }
main.mv > :is(.lede, .mv-data) { order: 2; margin-top: 0.9rem; }
}
@media (min-width: 641px) and (max-height: 900px) {
main.mv { padding-top: 0.5rem; }
main.mv > h1 { font-size: var(--t-h1-compact); margin: 0 0 0.45rem; }
.mv-fields { display: none; }
.runsel.mv-fsel { display: inline-flex; flex: 0 1 26rem; }
.mv-fsel select { flex: 1 1 auto; min-width: 0; }
}
/* full screen, or the whole window (the map's corner button) */
main.mv.full { position: fixed; inset: 0; z-index: 1000; display: flex; flex-direction: column; overflow: auto;
padding: 0.5rem 0.75rem; background: var(--bg); }
main.mv.full > :not(.mv-panel, .mv-mapwrap, .mv-show, .mv-mg) { display: none; }
main.mv.full > * { flex: none; }
main.mv.full .mv-map { height: calc(100vh - var(--tbh, 70px)); }
html.mv-full { overflow: hidden; }
.mv-zoom svg { display: block; margin: auto; }
</style>
<script>
// --ui: the scale the viewer's controls are drawn at (CSS zoom): 1 in a window about 1000 px tall and 1500 px wide or
// larger, less in a smaller one, never under 0.72 (0.85 on a touch screen, so its buttons stay big enough to tap)
(function () {
var touch = matchMedia("(pointer: coarse)").matches;
function ui() {
var s = Math.min(1, innerHeight / 1000, innerWidth / 1500);
document.documentElement.style.setProperty("--ui", Math.max(touch ? 0.85 : 0.72, s).toFixed(3));
}
ui();
addEventListener("resize", ui);
})();
</script>
<script data-goatcounter="https://scorvec.goatcounter.com/count" async src="https://gc.zgo.at/count.v4.js" integrity="sha384-nRw6qfbWyJha9LhsOtSb2YJDyZdKvvCFh0fJYlkquSFjUxp9FVNugbfy8q1jdxI+" crossorigin="anonymous"></script>
</head>
<body>
<!-- sh:start -->
<a class="skip-link" href="#main">Skip to main content</a>
<header class="sh" id="site-header">
<div class="sh-in">
<a class="sh-brand" href="/">Shawn Corvec</a>
<button class="sh-find" type="button" aria-haspopup="dialog" aria-keyshortcuts="Control+K Meta+K" title="Find a plot (Ctrl K)"><svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true" focusable="false"><circle cx="8.5" cy="8.5" r="5.5" style="fill:none;stroke:currentColor"/><path d="M12.6 12.6 17 17" style="fill:none;stroke:currentColor"/></svg><span class="sh-find-t">Find a plot</span><kbd>Ctrl K</kbd></button>
<button class="sh-toggle" type="button" aria-expanded="false" aria-controls="sh-menu">Menu</button>
<nav class="sh-nav" id="sh-menu" aria-label="Site">
<ul class="sh-list">
<li class="sh-item sh-has-menu">
<button class="sh-link sh-menubtn" type="button" aria-expanded="false" aria-controls="sh-g0" aria-current="page">Forecasts</button>
<div class="sh-menu sh-menu--cols" id="sh-g0">
<div class="sh-col">
<div>
<h3>Model maps</h3>
<ul>
<li><a href="/models.html" aria-current="page"><span class="sh-t">North America, short range</span><span class="sh-d">HRRR, RRFS and RDPS: radar, snow, storms</span></a></li>
<li><a href="/samerica-models.html"><span class="sh-t">South America, to day 10</span><span class="sh-d">IFS control and GDPS, with soundings</span></a></li>
<li><a href="/ens-anomalies.html"><span class="sh-t">World ensembles, to day 15</span><span class="sh-d">Four ensembles: anomalies and regimes</span></a></li>
</ul>
</div>
</div>
<div class="sh-col">
<div>
<h3>Days 1–15</h3>
<ul>
<li><a href="/ar.html"><span class="sh-t">Atmospheric rivers</span><span class="sh-d">Moisture plumes, landfall and their odds</span></a></li>
<li><a href="/circulation.html#dt"><span class="sh-t">Jet stream and tropopause</span><span class="sh-d">Tropopause, Pacific jet and wave trains</span></a></li>
<li><a href="/circulation.html#telecon15"><span class="sh-t">Teleconnections</span><span class="sh-d">NAO, PNA, EPO and more, calibrated</span></a></li>
<li><a href="/circulation.html#tc"><span class="sh-t">Tropical cyclones</span><span class="sh-d">Tracks and recurvature from 100 members</span></a></li>
<li><a href="/recon.html"><span class="sh-t">Hurricane reconnaissance</span><span class="sh-d">Flights, dropsondes and vortex fixes</span></a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="sh-item sh-has-menu">
<button class="sh-link sh-menubtn" type="button" aria-expanded="false" aria-controls="sh-g1">Tropics</button>
<div class="sh-menu sh-menu--cols" id="sh-g1">
<div class="sh-col">
<div>
<h3>ENSO and the MJO</h3>
<ul>
<li><a href="/enso.html"><span class="sh-t">El Niño monitor</span><span class="sh-d">ONI, RONI, the subsurface and winds</span></a></li>
<li><a href="/mjo.html"><span class="sh-t">MJO forecast</span><span class="sh-d">Four ensembles on one phase diagram</span></a></li>
<li><a href="/tropical-waves.html"><span class="sh-t">Tropical waves</span><span class="sh-d">Kelvin and Rossby waves, now and ahead</span></a></li>
<li><a href="/circulation.html#mslp"><span class="sh-t">Pacific pressure and satellite</span><span class="sh-d">Pressure, wind and simulated IR</span></a></li>
</ul>
</div>
</div>
<div class="sh-col">
<div>
<h3>Circulation</h3>
<ul>
<li><a href="/circulation.html#walker"><span class="sh-t">Walker and Hadley cells</span><span class="sh-d">The overturning, its outflow and descent</span></a></li>
<li><a href="/circulation.html#jets"><span class="sh-t">Jets and angular momentum</span><span class="sh-d">Jet cores, momentum and torques</span></a></li>
<li><a href="/circulation.html#rainfc"><span class="sh-t">Rain forecast, weeks 1–2</span><span class="sh-d">Four ensembles, bias-adjusted, blended</span></a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="sh-item sh-has-menu">
<button class="sh-link sh-menubtn" type="button" aria-expanded="false" aria-controls="sh-g2">Stratosphere</button>
<div class="sh-menu sh-menu--cols" id="sh-g2">
<div class="sh-col">
<div>
<h3>Forecasts</h3>
<ul>
<li><a href="/ssw.html"><span class="sh-t">SSW outlook</span><span class="sh-d">Is a sudden warming coming?</span></a></li>
<li><a href="/stratosphere.html#vortex"><span class="sh-t">The next two weeks</span><span class="sh-d">The vortex, its waves and the surface</span></a></li>
<li><a href="/stratosphere.html#vortex35"><span class="sh-t">Weeks 3–5 and the season</span><span class="sh-d">GEPS, GEFS, C3S and SEAS5</span></a></li>
<li><a href="/strat-models.html"><span class="sh-t">Stratosphere model maps</span><span class="sh-d">Polar maps at every level, zoomable</span></a></li>
</ul>
</div>
</div>
<div class="sh-col">
<div>
<h3>Analyses and history</h3>
<ul>
<li><a href="/stratosphere.html#qbo"><span class="sh-t">The QBO and today's analyses</span><span class="sh-d">QBO, momentum and the Brewer–Dobson</span></a></li>
<li><a href="/stratosphere.html#shwinters"><span class="sh-t">History and studies</span><span class="sh-d">Every winter and warming since 1980</span></a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="sh-item sh-has-menu">
<button class="sh-link sh-menubtn" type="button" aria-expanded="false" aria-controls="sh-g3">Weeks to seasons</button>
<div class="sh-menu sh-menu--cols" id="sh-g3">
<div class="sh-col">
<div>
<h3>Extended range</h3>
<ul>
<li><a href="/subseasonal.html"><span class="sh-t">GEPS, to day 35</span><span class="sh-d">Canada's extended ensemble</span></a></li>
<li><a href="/gefs.html"><span class="sh-t">GEFS, to day 35</span><span class="sh-d">NOAA's extended ensemble</span></a></li>
<li><a href="/sfs-subseasonal.html"><span class="sh-t">SFS, to six weeks</span><span class="sh-d">NOAA's experimental new system</span></a></li>
</ul>
</div>
</div>
<div class="sh-col">
<div>
<h3>Months and seasons</h3>
<ul>
<li><a href="/monthly-outlook.html"><span class="sh-t">Claude's long-range forecast</span><span class="sh-d">The month and three seasons ahead</span></a></li>
<li><a href="/seasonal.html"><span class="sh-t">Seasonal model forecasts</span><span class="sh-d">Seven C3S systems and NOAA's SFS</span></a></li>
<li><a href="/enso-forecasts.html"><span class="sh-t">ENSO forecasts</span><span class="sh-d">Niño-3.4 from five centres</span></a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="sh-item sh-has-menu">
<button class="sh-link sh-menubtn" type="button" aria-expanded="false" aria-controls="sh-g4">Studies and tools</button>
<div class="sh-menu sh-menu--cols" id="sh-g4">
<div class="sh-col">
<div>
<h3>Studies</h3>
<ul>
<li><a href="/enso.html#imp_reg"><span class="sh-t">ENSO impacts on the Americas</span><span class="sh-d">CMIP6 and the observed record</span></a></li>
<li><a href="/mjo.html#mi"><span class="sh-t">MJO impacts</span><span class="sh-d">Each phase, season by season</span></a></li>
<li><a href="/aifs-aam-budget.html"><span class="sh-t">AI models and conservation laws</span><span class="sh-d">Does the AIFS keep the budgets?</span></a></li>
<li><a href="/torque-enso.html"><span class="sh-t">El Niño and angular momentum</span><span class="sh-d">Why the atmosphere spins up</span></a></li>
<li><a href="/topics/"><span class="sh-t">Explainers</span><span class="sh-d">The physics behind the products</span></a></li>
</ul>
</div>
</div>
<div class="sh-col">
<div>
<h3>Verification</h3>
<ul>
<li><a href="/verification.html"><span class="sh-t">Global models against observations</span><span class="sh-d">Scorecards, maps and the database</span></a></li>
<li><a href="/aifs-verify.html"><span class="sh-t">AIFS single versus ensemble control</span><span class="sh-d">Two AIFS configurations compared</span></a></li>
</ul>
</div>
<div>
<h3>Tools</h3>
<ul>
<li><a href="/catalog.html"><span class="sh-t">Every plot on the site</span><span class="sh-d">Search and filter every figure</span></a></li>
<li><a href="/skewt/"><span class="sh-t">Sounding explorer</span><span class="sh-d">Radiosondes worldwide, live</span></a></li>
<li><a href="/asos5.html"><span class="sh-t">Five-minute airport observations</span><span class="sh-d">22 major US airports, live</span></a></li>
<li><a href="/climate.html"><span class="sh-t">US climate trends</span><span class="sh-d">Every county and month since 1895</span></a></li>
<li><a href="/fci.html"><span class="sh-t">US credit conditions</span><span class="sh-d">Financial conditions, weekly since 1990</span></a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="sh-item sh-item--special"><a class="sh-special" href="/midterms/">2026 Midterms</a></li>
<li class="sh-item sh-has-menu sh-item--me">
<button class="sh-link sh-menubtn sh-me" type="button" aria-expanded="false" aria-controls="sh-me">About</button>
<div class="sh-menu sh-menu--list sh-menu--me" id="sh-me">
<ul>
<li><a href="/about.html">About me</a></li>
<li><a href="/resume.html">Resume</a></li>
<li><a href="/research.html">Publications</a></li>
<li class="sh-me-ext"><a href="https://github.com/scorvec">GitHub</a></li>
<li><a href="https://scholar.google.com/citations?user=EYLRCJIAAAAJ&hl=en">Google Scholar</a></li>
<li><a href="https://www.linkedin.com/in/shawn-corvec-35895b231/">LinkedIn</a></li>
</ul>
</div>
</li>
</ul>
</nav>
</div>
</header>
<!-- sh:end -->
<main id="main" class="mv">
<p class="kicker">Newest run of each model · updated four times a day</p>
<h1>Short-range model maps</h1>
<p class="lede">Zoomable maps of the newest HRRR, RRFS and RDPS forecasts, two to three and a half days ahead: hover or tap for values, click an airport for its forecast.</p>
<p class="mv-data" role="note"><b>Heavy on data:</b> a single hour is small, but a playing loop downloads every hour (5–30 MB, more with Minus).</p>
<div class="mv-panel">
<div class="mv-bar">
<div class="mv-row">
<div class="seg" id="models" role="group" aria-label="Model"></div>
<label class="runsel">Run <select id="run" aria-label="Model run"></select><span id="runT"></span></label>
<label class="runsel mv-fsel">Map <select id="fieldSel" aria-label="Map"></select></label>
</div>
<div class="mv-fields" id="fields" role="group" aria-label="Map"></div>
<div class="mv-acc" id="accBar" hidden>
<div class="seg" id="accType" role="group" aria-label="Precipitation type"></div>
<label class="runsel">Window <select id="accWin" aria-label="Accumulation window"></select></label>
</div>
<div class="mv-acc" id="ecBar" hidden><div class="seg" id="ecSub" role="group" aria-label="ECAPE field"></div></div>
<div class="mv-acc" id="subBar" hidden><div class="seg" id="subSeg" role="group" aria-label="Map variant"></div></div>
</div>
<div class="mv-time">
<button type="button" id="play" aria-label="Play">▶</button>
<button type="button" id="back" aria-label="Previous hour">‹</button>
<input type="range" id="hour" min="0" max="18" value="0" aria-label="Forecast hour">
<button type="button" id="fwd" aria-label="Next hour">›</button>
<span class="mv-valid" id="valid" aria-live="polite"></span>
</div>
</div>
<div class="mv-mapwrap">
<div class="mv-map" id="map" data-fit-below=".mv-legT, .mv-show" tabindex="0" role="application" aria-roledescription="map" aria-label="Model map" aria-describedby="badge mapKeys">
<canvas id="gl" aria-hidden="true"></canvas>
<canvas id="ov" aria-hidden="true"></canvas>
<canvas id="lab" aria-hidden="true"></canvas>
<div class="mv-coop" id="coop" aria-hidden="true"></div>
<div class="mv-badge" id="badge"></div>
<div class="mv-zoom"><button type="button" id="zin" aria-label="Zoom in">+</button><button type="button" id="zout" aria-label="Zoom out">−</button><button type="button" class="txt" id="zfit" aria-label="Reset view">fit</button><button type="button" id="zfull" aria-label="Full screen" title="Full screen"><svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true"><path d="M3 8V3h5M12 3h5v5M17 12v5h-5M8 17H3v-5" fill="none" stroke="currentColor" stroke-width="2"/></svg></button></div>
<div class="mv-msg" id="msg" role="status"></div>
<div class="mv-load" id="load"></div>
<div class="mv-tip" id="tip"></div>
</div>
<div class="mv-legend"><canvas id="leg" role="img" aria-label="Colour scale" aria-describedby="legT"></canvas></div>
<div class="mv-sndhint" id="sndH" hidden role="note"><span id="sndHT"></span><button type="button" id="sndHX" aria-label="Hide this tip" title="Hide this tip">×</button></div>
</div>
<p class="mv-legT" id="legT"></p>
<div class="mv-show"><b>Show</b>
<label class="chk" id="contL" hidden title="Labelled contours drawn from the same data (isotherms, flight-category thresholds, isobars, CAPE)"><input type="checkbox" id="cont" checked> Contours</label>
<label class="chk" title="Airports with scheduled service in the US and Canada (OurAirports) and a few city centres; hover for the model's value there, click or tap for the forecast meteogram"><input type="checkbox" id="sta" checked> Airports</label>
<label class="chk" id="tdnL" title="The model's 2 m temperature (red) and dewpoint (green), °F, at the hour shown, above each labelled airport (temperature and dewpoint maps only)"><input type="checkbox" id="tdn" checked> Temp / dew</label>
<label class="chk" id="diffL" hidden title="The other 3 km model's value subtracted at the same valid time, on the shared grid (HRRR and RRFS use the identical grid)"><input type="checkbox" id="diff"> <span id="diffT">Difference</span></label>
<label class="chk" id="farmsL" hidden title="US wind projects (USGS Wind Turbine Database) or large-scale solar plants (USGS Solar PV Database), sized by capacity"><input type="checkbox" id="farms" checked> <span id="farmsT">Wind farms</span></label>
<span class="mv-keys" id="sndL" hidden><b>Sounding:</b> double-click the map (on a phone, double-tap or press and hold) for that spot's HRRR forecast sounding</span>
<span class="mv-keys" id="mapKeys">Click the map, then: arrows pan · <kbd>+</kbd> <kbd>−</kbd> zoom · <kbd>0</kbd> reset · <kbd>,</kbd> <kbd>.</kbd> hour · <kbd>Space</kbd> play · <kbd>Ctrl</kbd> + scroll zooms</span>
</div>
<form class="mv-find" id="mgFind" role="search">
<label for="mgQ">Forecast for an airport or city</label>
<input id="mgQ" type="text" list="mgList" autocomplete="off" autocapitalize="characters" spellcheck="false" placeholder="ICAO code or name, e.g. KORD">
<datalist id="mgList"></datalist>
<button type="submit">Open the meteogram</button>
<output id="mgFindMsg" aria-live="polite"></output>
</form>
<p class="mv-note" id="note"></p>
<section class="mv-mg" id="mg" hidden role="dialog" aria-label="Forecast meteogram" tabindex="-1">
<div class="mv-mgtop">
<div class="mv-mgh"><h2 id="mgT"></h2><span class="sub" id="mgS"></span><span class="mv-key" id="mgK"></span>
<span class="mv-mgbtns"><button type="button" class="x" id="mgSnd" hidden title="The HRRR forecast sounding at this station">Sounding</button><button type="button" class="x" id="mgW" title="Use the whole window (the map stays underneath)">Full window</button><button type="button" class="x" id="mgX">Close<kbd>Esc</kbd></button></span></div>
<div class="mv-mgr" id="mgR"></div>
</div>
<div class="mv-mgbody" id="mgB">
<div id="mgC"></div>
<p class="mv-legT" id="mgN"></p>
</div>
</section>
<section class="mv-mg mv-snd" id="snd" hidden role="dialog" aria-label="Forecast sounding">
<div class="mv-mgtop">
<div class="mv-mgh"><h2 id="sndT">Forecast sounding</h2><span class="sub" id="sndS"></span>
<span class="mv-mgbtns"><button type="button" class="x" id="sndX">Close<kbd>Esc</kbd></button></span></div>
</div>
<iframe id="sndF" title="Forecast sounding: skew-T, hodograph and parameters"></iframe>
</section>
<details class="mv-about">
<summary>About these maps: fields, models, timing and methods</summary>
<p class="mv-runs" id="runs"></p>
<h2>What each map is, model by model</h2>
<div class="tbl"><table>
<tr><th>Field</th><th>HRRR v4 (NOAA, 3 km)</th><th>RRFS v1 (NOAA, 3 km)</th><th>RDPS (ECCC, 10 km)</th></tr>
<tr><td>Simulated IR</td><td>Brightness temperature a GOES-East 10.3 µm imager would see (UPP field SBT124)</td><td>GOES-16 ABI band 13, 10.3 µm (SBTA1613)</td><td>No simulated band exists in the RDPS output. Shown instead: the <b>OLR-derived brightness temperature</b>, T<sub>b</sub> = (OLR/σ)<sup>¼</sup>, from the instantaneous outgoing longwave radiation at the top of the atmosphere. It is a proxy for IR, not a satellite band: it integrates the whole longwave spectrum, so clear skies read 20–30 K colder than a window channel, while cloud tops come out close.</td></tr>
<tr><td>Radar + precipitation type</td><td>Simulated reflectivity 1 km above ground, coloured by the categorical precipitation type at the surface (freezing rain > sleet > snow > rain; rain and snow together shown as mix); isobars from MSLP (MAPS reduction) every 2 hPa</td><td>As HRRR; isobars from MSLP (MSLET reduction)</td><td>No simulated reflectivity exists. Shown instead: the <b>precipitation rate</b> (mm/h) coloured by the model's precipitation type, on the radar colour scale; isobars from MSLP</td></tr>
<tr><td>Precipitation accumulation</td><td>Total (APCP); snow = the HRRR's own snowfall (ASNOW, variable density); freezing rain = the model's accumulated freezing rain (FRZR, liquid equivalent, not ice accretion); sleet = APCP accumulated in hours whose precipitation type is sleet (liquid equivalent)</td><td>As HRRR, except freezing rain: the operational RRFS writes no FRZR, so it is APCP accumulated in periods whose precipitation type is freezing rain, like sleet (both 3-hourly past 36 h)</td><td>Total; snow = <b>10:1</b> from the accumulated snow water equivalent (not a model snowfall); freezing rain and sleet = the model's own liquid-equivalent accumulations</td></tr>
<tr><td>Winter accumulations<br><small>computed here</small></td><td><b>Ice (FRAM)</b>: radial ice on power lines and branches from the Freezing Rain Accumulation Model (Sanders and Barjenbruch 2016): each hour’s ice-to-liquid ratio from the freezing-rain rate, the 2 m wet-bulb temperature (Stull 2011) and the 10 m wind, times the freezing-rain liquid, × 0.394 for radial ice. <b>Snow (Cobb ratio)</b>: the snow-phase liquid (APCP where the surface type is snow, half where it is a rain-snow mix) times a column snow-to-liquid ratio (Cobb and Waldstreicher 2005, in the NBM v4.1 form): layers saturated over ice (RH<sub>ice</sub> ≥ 90 %) every 25 hPa from 925 to 300 hPa, weighted by upward motion and humidity, limited to 3–30, 10:1 where no layer qualifies. <b>Snow 10:1</b>: ten times the same snow-phase liquid. <b>Snow-depth increase</b>: the sum of the hourly increases of the model’s snow depth (SNOD); melting and settling are not subtracted, and over existing snow small up-and-down changes in the depth add up, so totals there run high</td><td>As HRRR, except: no Cobb ratio (HRRR only); FRAM from its freezing-rain liquid as in the row above; 3-hourly past 36 h</td><td>FRAM from FreezingRain-Accum; snow-depth increase from SnowDepth; no Cobb ratio, and no separate 10:1 map (its Snow map is already 10:1)</td></tr>
<tr><td>Cloud ceiling</td><td>Height of the lowest broken or overcast layer above the ground (the model's ceiling height minus the terrain)</td><td>As HRRR</td><td class="na">Not in the RDPS output</td></tr>
<tr><td>Visibility</td><td>Surface visibility; above 10 mi shown as “10+”, as a METAR reports it</td><td>As HRRR</td><td class="na">Not in the RDPS output</td></tr>
<tr><td>2 m temperature, dewpoint</td><td colspan="3">In °F, stored to 0.5 °F</td></tr>
<tr><td>Smoke<br><small>April–September only</small></td><td>Vertically integrated smoke (column mass density, COLMD), mg m<sup>−2</sup></td><td>RRFS-SD column smoke (organic particulate matter, COLMD), mg m<sup>−2</sup></td><td>From ECCC's <b>RAQDPS-FireWork</b> air-quality run (wildfire-smoke PM2.5 column), 00 and 12 UTC to 72 h; a 06 or 18 UTC RDPS run shows the FireWork run 6 h older, matched on valid time</td></tr>
<tr><td>80 m wind</td><td colspan="3">Speed shaded in m/s (the wind-energy convention; the other wind maps are in knots), wind barbs in knots (half barb 5, full 10, pennant 50), from the model's u and v at 80 m above ground</td></tr>
<tr><td>10 m wind</td><td colspan="3">As the 80 m wind, from the model's u and v at 10 m above ground, shown in knots (stored to 0.4 m/s, about 0.8 kt)</td></tr>
<tr><td>Wind gust</td><td>The model's instantaneous surface gust (GUST, a diagnosis of the wind that boundary-layer mixing can bring down), not an hourly maximum; barbs show the 10 m wind</td><td>As HRRR</td><td>The model's instantaneous 10 m gust (WindGust); barbs show the 10 m wind</td></tr>
<tr><td>Sea-level pressure</td><td>MSLP (MAPS reduction, MSLMA) in 4 hPa colour classes, isobars every 2 hPa, highs and lows; stored on every 4th grid point (12 km) and interpolated. Faded where the ground is above ~1,500 m (surface pressure below 850 hPa): the reduction to sea level is unreliable there and no H or L is placed</td><td>As HRRR, MSLET reduction</td><td>As HRRR, every 2nd grid point (20 km)</td></tr>
<tr><td>ECAPE and CAPE</td><td>Entraining CAPE (Peters et al. 2023) and the undiluted CAPE for the most-unstable and mixed-layer parcels, computed with SHARPlib on every column of the HRRR's native levels, hourly to 18 h and every 3 hours to 48 h; the ECAPE/CAPE ratio is shown where CAPE exceeds 100 J/kg</td><td class="na" colspan="2">Computed for the HRRR only</td></tr>
<tr><td>Updraft helicity</td><td>MXUPHL 2–5 km and 0–3 km above ground, the maximum over each hour, and the run swath (the running maximum since the run started); classes from 25 m²/s², black outline at 75</td><td>As HRRR (past 36 h the files are 3-hourly and each covers its last hour only)</td><td class="na">Not in the RDPS output</td></tr>
<tr><td>Max updraft</td><td>MAXUVV: the hourly maximum upward vertical velocity between 100 and 1000 hPa, and its run swath</td><td>As HRRR</td><td class="na">Not in the RDPS output</td></tr>
<tr><td>Echo tops</td><td>RETOP: the 18.3 dBZ echo top, thousands of feet above sea level, in 5,000 ft classes (under Radar)</td><td>As HRRR</td><td class="na">Not in the RDPS output</td></tr>
<tr><td>PBL height</td><td>HPBL, metres above ground, every hour, smoothed to 6 km</td><td>As HRRR</td><td>PlanetaryBoundaryLayerHeight, smoothed to 20 km</td></tr>
<tr><td>Potential vorticity</td><td>Ertel PV computed here on 22 pressure levels (fields smoothed to 12 km): the 300–250 hPa mean with 250 hPa barbs and heights and the 2-PVU line, PV on 315/320/330/340 K with the wind and the pressure of the surface, and the 850–700 hPa mean with 850 hPa barbs; every 3 hours</td><td>As HRRR</td><td>As HRRR, on 19 levels of its rotated grid, 20 km</td></tr>
<tr><td>925 and 250 hPa wind</td><td>Wind speed (knots) with barbs and the height of the pressure level (contours every 30 m at 925 hPa, 120 m at 250 hPa), from the pressure-level file (wrfprs), every 3 hours, on every 2nd grid point (6 km); 925 hPa left blank where the ground is above that level</td><td>As HRRR (prslev)</td><td>As HRRR, on the 10 km grid</td></tr>
<tr><td>500 hPa vorticity</td><td>Absolute vorticity computed from the 500 hPa wind smoothed to 12 km (with the grid's map factor), heights every 30 m (60 m bold), 500 hPa wind barbs</td><td>As HRRR</td><td>The model's own absolute vorticity, smoothed to 20 km</td></tr>
<tr><td>Dynamic tropopause</td><td>Potential temperature and wind on the 2-PVU surface, computed here: Ertel PV on 19 pressure levels (100–700 hPa, fields smoothed to 12 km), the 2-PVU crossing searched downward from 100 hPa; 100 hPa values where PV is below 2 PVU even there; colours as the site's dynamic-tropopause maps</td><td>As HRRR</td><td>The model's own 2-PVU pressure (Pressure_PVU-2), with potential temperature and wind interpolated to it from the pressure levels; 20 km</td></tr>
<tr><td>Snow bands<br><small>1 October–30 April</small></td><td>From the pressure-level file every 3 hours from +3 h, smoothed (Gaussian, σ = 18 km) on the 3 km grid and evaluated on 12 km: 700 hPa Petterssen frontogenesis with θ, height and the axes of dilatation; EPV* (750–500 hPa, where saturated over ice) with upright CI hatched; the strongest lift in the saturated −12 to −18 °C layer; and where those three overlap, with the model’s own snow (≥ 25 dBZ composite reflectivity, snow type) outlined. Pressure levels 950–450 hPa every 25 hPa</td><td>As HRRR; vertical motion from the model's geometric vertical velocity (DZDT), converted to ω hydrostatically</td><td>As HRRR, on its own 10 km grid, levels 950–450 hPa (25–50 hPa apart); vertical velocity exists only at 850, 700 and 500 hPa, so the dendritic-zone lift is interpolated between them; the model’s snow outline is 1 mm or more in the hour where its type is snow</td></tr>
<tr><td>Incoming shortwave</td><td>Downward shortwave at the surface, instantaneous at the valid time</td><td>As HRRR (instantaneous)</td><td>Mean over the hour ending at the valid time (the difference of the model's accumulated flux)</td></tr>
</table></div>
<h2>Runs and timing</h2>
<p>Airports: large and medium airports with scheduled service in the US and Canada from <a href="https://ourairports.com/data/">OurAirports</a> (public domain); the large hubs show at national scale and the rest as you zoom in. Hover or tap one for the model's value there.</p>
<p>Wind and solar plants (shown on the 80 m wind and shortwave maps; at national scale only the larger ones): the <a href="https://energy.usgs.gov/uswtdb/">U.S. Wind Turbine Database</a> v8.3 (USGS, LBNL and ACP; turbines grouped into projects) and the <a href="https://energy.usgs.gov/uspvdb/">U.S. Large-Scale Solar PV Database</a> v4.0 (USGS and LBNL), both public domain. Circles and squares scale with capacity; hover for name, capacity and year.</p>
<p>HRRR: the 00, 06, 12 and 18 UTC runs, every hour to 48 h (the hourly runs that stop at 18 h are not shown). RRFS v1: the 00, 06, 12 and 18 UTC runs to 84 h, every hour to 36 h and every 3 hours after (its other hourly runs write only 15-minute surface files to 18 h and are not shown). RDPS: 00, 06, 12 and 18 UTC, every hour to 84 h. Only the newest run of each model is kept. A run appears once its last forecast hour is published; GitHub runs the jobs, so expect it about 2–3 hours after its start time for the HRRR and 4–5 hours for the RRFS and RDPS.</p>
<h2>Meteograms and model differences</h2>
<p>Click or tap any airport (or one of a few city centres such as New York Central Park) for a meteogram from every model's newest run, every forecast hour: temperature and dewpoint, 10 m wind and gusts and 80 m wind with barbs, ceiling and visibility against the flight-category bands, simulated radar and precipitation type, hourly and accumulated precipitation by type, ECAPE and CAPE (HRRR), smoke, sunshine and sea-level pressure, with a line at the current time. The values are read from the model grids when the run is processed: bilinear between the four nearest grid points for smooth fields, the nearest point for ceiling, visibility, reflectivity, precipitation type and CAPE.</p>
<p>“Minus RRFS” / “Minus HRRR” subtracts the other 3 km model at the same valid time; the HRRR and RRFS share an identical grid, so the difference is cell by cell (temperature, dewpoint, 10 m and 80 m wind speed, gusts, IR and shortwave).</p>
<h2>How the page works</h2>
<p><b>Data use.</b> <b>Heavy on data, especially on mobile.</b> Opening a map loads only the hour on screen (0.05–0.6 MB). Once you play, step or drag the time slider, the viewer downloads every hour of that map so the loop never stalls: typically 5–30 MB a map, twice that with <b>Minus</b>, and 100 MB or more after a dozen maps. It holds back to the next few hours only on a slow connection, not on a good 4G or 5G signal. On a capped data plan, look at single hours or wait for Wi‑Fi.</p>
<p>Zoomable maps of the newest HRRR, RRFS and RDPS forecasts, out to two to three and a half days, for aviation, severe and winter weather (including snow-band diagnostics), and wind and solar power. Pick a map, scrub through the hours, and hover or tap anywhere for the model's value. <b>Click or tap an airport</b> for its forecast from all three models, <b>double-click</b> the HRRR map (on a phone, double-tap or press and hold) for a forecast sounding there, and compare the HRRR and RRFS with <b>Minus</b>.</p>
<p>Nothing on this page is a picture. Each field and hour is shipped as the model's own numbers, rounded to 256 levels (0.625 K for infrared, 0.5 °F for temperature and dewpoint, 0.4 m/s for wind, 0.25 m/s for gusts, 0.5 hPa for sea-level pressure, 0.5 m/s for the 925 hPa and 0.8 m/s for the 250 hPa wind, a few metres for heights, 0.6 K for the dynamic tropopause, 5 W m<sup>−2</sup> for shortwave, 2–3 % steps on logarithmic scales for ceiling, visibility and smoke, and a square-root scale for ECAPE and CAPE: within ±4 J/kg at 100, ±11 at 1,000 and ±20 at 3,000) on the native model grid, and your browser colours, projects, contours and reads it: zooming shows the model's grid cells, and the hover value is the stored one, within those steps. Maps are drawn on the HRRR's Lambert conformal projection for all three models, so switching model keeps the view. Contours (every 10 °F with the freezing line in blue; dewpoint every 10 °F with 60 and 70 emphasised; 80 m wind 10/15/20 m/s; 10 m wind 20/30/40 kt; gusts 30/40/50 kt; isobars every 2 hPa, 4 hPa zoomed out, on the radar, pressure, 10 m wind and gust maps) are drawn by your graphics card in the same pass as the colours, from the same numbers after a light smoothing (to about 6 km on the 3 km grids), so they show the pattern, not every 3 km wiggle, and move with the map; their labels and the highs and lows are placed once the map is at rest. Boundaries: Natural Earth; US states and counties: US Census via us-atlas.</p>
<h2 id="snowbands">Snow bands: what the four maps are</h2>
<p>A <b>mesoscale snow band</b> is a stripe of heavy snow, typically 20–100 km wide and hundreds of kilometres long, that can drop 2–4 inches an hour for several hours while places 50 km away get a fraction of that. Most cold-season bands form on the poleward side of a cyclone where the middle troposphere is <b>frontogenetic</b> (the flow packs the temperature gradient together), the air above is <b>weakly stable to slantwise motion</b>, and the column is saturated through the <b>dendritic growth zone</b> (Nicosia and Grumm 1999; Waldstreicher 2001; Novak et al. 2004). The four maps under <b>Winter: snow bands</b> show those ingredients and their overlap; the model’s own bands are on the <b>Radar + type</b> map (snow in blue).</p>
<p><b>How they are computed.</b> Every field is smoothed with a Gaussian filter (σ = 18 km) on the model’s own grid before any derivative, and the diagnostics are evaluated on a 12 km grid (10 km for the RDPS): frontogenesis and EPV are second order in the wind and temperature, and on a raw 3 km grid they are dominated by convective-scale noise. Derivatives use grid-relative winds and the true grid spacing; levels within 25 hPa of the ground are masked. Frontogenesis: the rate of change of |∇θ| following the flow, from the 2-D kinematic (Petterssen 1936) form, K per 100 km per 3 h. EPV*: saturated equivalent potential vorticity in pressure coordinates with the full wind and θ<sub>es</sub> from Bolton (1980), averaged over 750–500 hPa where that layer’s relative humidity (over ice below 0 °C) is 80 % or more; hatched where θ<sub>es</sub> at 500 hPa is lower than at 750 hPa. DGZ lift: the column sampled every 5 hPa, the strongest −ω (10 µb/s ≈ 10 cm/s) where it is −12 to −18 °C and saturated over ice. Ingredients: frontogenesis ≥ 4, EPV* ≤ 0.25 PVU in saturated air, DGZ lift ≥ 10 µb/s — round numbers in the range reported for northeast US banded storms, loose enough to keep the weakly stable cases a strict EPV* < 0 rule would miss (Schultz and Schumacher 1999). The axes of dilatation are drawn where the total deformation is in the top 15 % of the map in view and at least 6 × 10<sup>−5</sup> s<sup>−1</sup>. The code is the same as the site’s former snow-band page’s (scripts/snowband/snowband.py), now run on the whole model domain. Checked against the former page’s own output on the same HRRR hours (the Binghamton storm, 16 December 2020 18Z +3 h, and the 27 September 2026 nor’easter): every field agrees to r ≥ 0.99 on the snowstorm (0.95–1.00 on the rain case, where frontogenesis was weak), the peaks within about 5 %, and the ingredient count matches in 99 % of cells; the small differences come from the 12 km grid being offset by one 3 km cell and from the stored precision.</p>
<p><b>Known limits.</b> These are only as good as the model’s placement of the system, and band placement errors of 50–100 km are normal: on the Binghamton case below the HRRR’s one-hour frontogenesis and lift axis sat 35–75 km south of the observed band, and its 12Z run’s band was 80–150 km south 14–16 hours out. Negative EPV* is necessary, not sufficient (it also flags upright and inertial instability and means nothing in unsaturated air): read it as “the atmosphere would not resist a band here”, never “a band is here”. The ingredient overlap is not a probability and has not been calibrated against observed bands. The HRRR’s analysis hour has no usable vertical motion, so the maps start at +3 h. Computed 1 October to 30 April; outside that the maps say so.</p>
<p class="mv-runs">Petterssen, S., 1936: Contribution to the theory of frontogenesis. Geofys. Publ. 11(6), 1–27 · Bolton, D., 1980, Mon. Wea. Rev. 108, 1046–1053 · Nicosia, D. J., and R. H. Grumm, 1999, Wea. Forecasting 14, 346–368 · Schultz, D. M., and P. N. Schumacher, 1999, Mon. Wea. Rev. 127, 2709–2732 · Waldstreicher, J. S., 2001: The importance of snow microphysics for large snowfalls. Third Northeast Operational Workshop · Novak, D. R., L. F. Bosart, D. Keyser and J. S. Waldstreicher, 2004, Wea. Forecasting 19, 993–1010 · Peters, J. M., D. R. Chavas, C.-Y. Su, H. Morrison and B. E. Coffer, 2023, J. Atmos. Sci. 80, 2165–2186 · Cobb, D. K., and J. S. Waldstreicher, 2005: A simple physically based snowfall algorithm. 21st Conf. on Weather Analysis and Forecasting / 17th Conf. on NWP, Amer. Meteor. Soc., 2A.2 · Sanders, K. J., and B. Barjenbruch, 2016, Wea. Forecasting 31, 1041–1060</p>
<h2 id="snowband-cases">Snow-band case studies, against the radar</h2>
<p>Every diagnostic from the HRRR’s one-hour forecast of each hourly cycle (f01, the first hour with usable vertical motion) beside the NEXRAD base-reflectivity mosaic at the same hour, so the diagnosed band axis can be checked against the bands the radar saw with almost no forecast error in the large-scale pattern. The <b>16–17 December 2020</b> nor’easter put a nearly stationary band over the Southern Tier of New York for about ten hours: Binghamton recorded <b>40.2 inches</b>, its biggest snowstorm on record, while places 60 km south got under a foot. The frontogenesis maximum, the weakly stable EPV* and the dendritic-zone lift line up along a southwest–northeast axis about 35–75 km south of the observed band, and the HRRR’s own band sits with them. The <b>27 September 2026</b> nor’easter (rain) is the method on a warm-season storm: the model’s reflectivity matched the radar (r 0.71–0.87) and its lift through the −12 to −18 °C layer aloft correlated with the radar at r 0.5–0.7, while 700 hPa frontogenesis was not aligned with the bands — they were not mid-level frontal bands. (Rendered by the former snow-band page in September 2026; figures from the same code as the maps above.)</p>
<div class="mv-cases"><label class="runsel">Case <select id="sbCase" aria-label="Case study"><option value="c2020121613">Binghamton band, 16–17 December 2020</option><option value="c2026092700">Nor'easter, 27 September 2026 (rain)</option></select></label>
<label class="runsel">View <select id="sbView" aria-label="Case study view"><option value="ov">Overview: the four diagnostics</option><option value="radar">Against NEXRAD</option><option value="ing">Band ingredients</option><option value="fg">Frontogenesis</option><option value="epv">EPV* and CSI</option><option value="dgz">Lift in the DGZ</option><option value="bands">The HRRR's own bands</option></select></label></div>
<div class="mv-casef" id="sbCaseW"></div>
<p>Data: NOAA HRRR and RRFS (AWS open data), ECCC RDPS and RAQDPS-FireWork (Data Source: Environment and Climate Change Canada (MSC Datamart), under the <a href="https://eccc-msc.github.io/open-data/licence/readme_en/">ECCC Data Servers End-use Licence</a>). Model output as published, uncorrected.</p>
</details>
</main>
<script>
(function () {
"use strict";
// ── configuration ─────────────────────────────────────────────────────────────────────────────────────────────
var LOCAL = /[?&]local=1/.test(location.search); // testing: data from this origin
var MIRROR = "https://cdn.jsdelivr.net/gh/scorvec/scorvec.github.io@";
var D2R = Math.PI / 180;
var MODELS = { hrrr: "HRRR", rrfs: "RRFS", rdps: "RDPS" };
var MODEL_ORDER = ["hrrr", "rrfs", "rdps"];
// colour stops: [value in display units, "#rrggbb", alpha]
function S(v, c, a) { return [v, parseInt(c.slice(1, 3), 16), parseInt(c.slice(3, 5), 16), parseInt(c.slice(5, 7), 16), a == null ? 1 : a]; }
var KT = 1.943844; // knots per m/s
var FIELDS = {
ir: { label: "Simulated IR", enc: "lin", off: 170, step: 0.625, unit: "°C",
disp: function (k) { return k - 273.15; }, lines: "rgba(255,226,110,0.9)",
stops: [S(45, "#0c0c0c"), S(20, "#3b3b3b"), S(0, "#727272"), S(-20, "#b1b1b1"), S(-30, "#e2e2e2"), S(-31, "#a6dcf2"),
S(-40, "#3f92d2"), S(-48, "#1f58a6"), S(-50, "#2aa047"), S(-58, "#9fd23a"), S(-62, "#f2e23a"), S(-67, "#f39a2b"),
S(-72, "#d7301f"), S(-78, "#8b0a36"), S(-84, "#e8b8ec"), S(-95, "#ffffff")],
ticks: [40, 20, 0, -20, -30, -40, -50, -60, -70, -80],
fmt: function (v, k) { return v.toFixed(1) + " °C (" + k.toFixed(1) + " K)"; } },
ceil: { label: "Cloud ceiling", enc: "log", lo: 50, hi: 50000, unit: "ft",
ticks: [100, 200, 500, 1000, 2000, 3000, 5000, 12000, 30000],
fmt: function (v) { return v < 1000 ? Math.round(v / 10) * 10 + " ft" : Math.round(v / 100) * 100 + " ft"; } },
vis: { label: "Visibility", enc: "log", lo: 0.03, hi: 10, unit: "mi", ticks: [0.1, 0.25, 0.5, 1, 2, 3, 5, 10],
fmt: function (v) { return (v < 1 ? v.toFixed(2) : v < 3 ? v.toFixed(1) : v.toFixed(1)) + " mi"; } },
t2m: { label: "Temperature", enc: "linf", unit: "°F",
// high-contrast ramp: a hue change every 10 F through the common range, saturated red from 100 F and a
// hard step to purple at 110 F
stops: [S(-40, "#f3eef6"), S(-30, "#dcc4e8"), S(-20, "#b98fd6"), S(-10, "#8a55bd"), S(0, "#5c2fa3"), S(10, "#2f3fae"),
S(20, "#2a6fd6"), S(32, "#5cb8ef"), S(40, "#3cb8a8"), S(50, "#4aaa45"), S(60, "#a6cf3c"), S(65, "#ecdf3a"),
S(70, "#f8c23a"), S(75, "#f7a031"), S(80, "#f07f2a"), S(90, "#e44f22"), S(97, "#d92a1f"), S(100, "#ff1414"),
S(109.9, "#d6001c"), S(110, "#a32bc4"), S(118, "#76169f"), S(125, "#4b0a6e")],
ticks: [-20, 0, 20, 32, 40, 50, 60, 70, 80, 90, 100, 110, 120],
fmt: function (v) { return v.toFixed(1) + " °F (" + ((v - 32) / 1.8).toFixed(1) + " °C)"; } },
td2m: { label: "Dewpoint", enc: "linf", unit: "°F",
stops: [S(-30, "#4d3b29"), S(0, "#7b5e3d"), S(20, "#a8875a"), S(30, "#c9ae84"), S(40, "#e4d8b3"), S(45, "#d8e8bf"),
S(50, "#abd8a2"), S(55, "#70c07e"), S(60, "#3aa05d"), S(65, "#1f7f4a"), S(70, "#1b6f7a"), S(75, "#245199"), S(82, "#4a2f8a")],
ticks: [0, 20, 30, 40, 50, 55, 60, 65, 70, 75, 80],
fmt: function (v) { return v.toFixed(1) + " °F (" + ((v - 32) / 1.8).toFixed(1) + " °C)"; } },
smoke: { label: "Smoke", enc: "log", lo: 0.1, hi: 3000, unit: "mg/m²",
stops: [S(2, "#cfcabc", 0), S(4, "#d6ccad", 0.55), S(10, "#dcb97c", 0.8), S(20, "#d6934f", 0.9), S(50, "#bf6233"),
S(100, "#96402e"), S(200, "#652c3c"), S(500, "#3d1f42"), S(1500, "#1b0f22")],
ticks: [4, 10, 20, 50, 100, 200, 500, 1000],
fmt: function (v) { return (v < 10 ? v.toFixed(1) : Math.round(v)) + " mg/m²"; } },
wind80: { label: "80 m wind", enc: "lin2", off: -50.8, step: 0.4, unit: "m/s", smax: 40,
stops: [S(0, "#f6f6f4"), S(3, "#d3e4f1"), S(6, "#9cc5e3"), S(9, "#5c9fcd"), S(12, "#3f9b6a"), S(15, "#8cc24a"),
S(18, "#e6d93a"), S(21, "#f8a93a"), S(25, "#e4552f"), S(30, "#a8124f"), S(40, "#4a0f6a")],
ticks: [0, 5, 10, 15, 20, 25, 30, 35, 40] },
// 10 m wind and the surface gust: 10 m wind encoded as the 80 m one; gust = 0.25 m/s steps from 0 (to 63.5 m/s),
// shaded with the 10 m wind's barbs, shown in knots; stored and coloured in m/s,
// so `kt` only changes what is displayed: legend ticks (ticksKt), contours, hover and the model difference
wind10: { label: "10 m wind", enc: "lin2", off: -50.8, step: 0.4, unit: "kt", kt: true, smax: 40,
stops: [S(0, "#f6f6f4"), S(3, "#d3e4f1"), S(6, "#9cc5e3"), S(9, "#5c9fcd"), S(12, "#3f9b6a"), S(15, "#8cc24a"),
S(18, "#e6d93a"), S(21, "#f8a93a"), S(25, "#e4552f"), S(30, "#a8124f"), S(40, "#4a0f6a")],
ticks: [0, 5, 10, 15, 20, 25, 30, 35, 40], ticksKt: [0, 10, 20, 30, 40, 50, 60, 70] },
gust: { label: "Wind gust", enc: "lin", off: 0, step: 0.25, unit: "kt", kt: true,
stops: [S(0, "#f4f4f0"), S(5, "#d6e6f0"), S(10, "#94c3e2"), S(13, "#559fd2"), S(16, "#3a9e6c"), S(19, "#98ca46"), S(22, "#eed73a"),
S(25, "#f6a535"), S(28, "#e8682d"), S(32, "#c92a37"), S(38, "#981062"), S(45, "#5a0f78"), S(55, "#2c0640")],
ticks: [0, 5, 10, 15, 20, 25, 30, 40, 50], ticksKt: [0, 10, 20, 30, 40, 50, 60, 80, 100],
fmt: function (v) { return Math.round(v * 1.943844) + " kt · " + v.toFixed(1) + " m/s · " + Math.round(v * 2.236936) + " mph"; } },
sw: { label: "Shortwave", enc: "lin", off: 0, step: 5, unit: "W/m²", lines: "rgba(255,255,255,0.75)",
stops: [S(0, "#16101f"), S(50, "#2e1a4a"), S(150, "#5b2270"), S(300, "#983068"), S(450, "#d14c4f"), S(600, "#ef7f3a"),
S(750, "#f8b440"), S(900, "#fbe06a"), S(1050, "#fffbd0")],
ticks: [0, 100, 200, 400, 600, 800, 1000],
fmt: function (v) { return Math.round(v) + " W/m²"; } }
};
// simulated reflectivity (1 km AGL) coloured by precipitation type; RDPS: precipitation rate (manifest enc "rate2")
FIELDS.refl = { label: "Radar + type", enc: "refl2", unit: "dBZ" };
// MSLP: the isobars on the radar map, and a map of its own. The file is the existing
// 4x/2x-subsampled grid (12 km HRRR/RRFS, 20 km RDPS; R = 940 + 0.5 hPa, G = 1 where the surface pressure is below 850 hPa):
// the shader interpolates it bilinearly, so the 4 hPa colour classes have smooth edges; high terrain (G) is drawn faded.
FIELDS.mslp = { label: "Sea-level pressure", enc: "lin", off: 940, step: 0.5, unit: "hPa",
fmt: function (v) { return v.toFixed(1) + " hPa (" + (v * 0.02953).toFixed(2) + " inHg)"; } };
// 4 hPa classes centred on 1012 hPa: blues and purples below, pale through 1008-1016, oranges to dark red above
var MSLP_LEVELS = [900, 976, 980, 984, 988, 992, 996, 1000, 1004, 1008, 1012, 1016, 1020, 1024, 1028, 1032, 1036, 1040, 1044];
var MSLP_COLORS = ["#3b0f5c", "#56258a", "#6a3fb0", "#4c50c4", "#3769c9", "#3f86cf", "#5aa2d8", "#80bce2", "#a9d3ea", "#d3e7ef",
"#f1ead6", "#f6d8a4", "#f3be78", "#ec9c52", "#df7638", "#c9512b", "#a53024", "#7c1a20", "#560e1d"];
var PT_NAMES = ["no surface type", "rain", "snow", "freezing rain", "sleet", "rain/snow mix"];
// radar-style ramps per type, stops in dBZ. Rain = the traditional NWS reflectivity table in discrete 5-dBZ bins; each
// bin is a pair of equal stops so interp() steps instead of blending.
var NWS_RAIN = (function () {
var bins = [[5, "#04e9e7"], [10, "#019ff4"], [15, "#0300f4"], [20, "#02fd02"], [25, "#01c501"], [30, "#008e00"], [35, "#fdf802"],
[40, "#e5bc00"], [45, "#fd9500"], [50, "#fd0000"], [55, "#d40000"], [60, "#bc0000"], [65, "#f800fd"], [70, "#9854c6"], [75, "#fdfdfd"]];
var out = [];
bins.forEach(function (b, i) { out.push(S(b[0], b[1])); out.push(S(i + 1 < bins.length ? bins[i + 1][0] - 0.001 : 95, b[1])); });
return out;
})();
var PT_RAMPS = [
null,
NWS_RAIN,
[S(5, "#cfe9fb"), S(15, "#8cc3f0"), S(25, "#4b8fe0"), S(33, "#1f5cc2"), S(40, "#12398c"), S(52, "#241766")],
[S(5, "#f9d3e5"), S(15, "#f09fc6"), S(25, "#e0639f"), S(35, "#c02b7b"), S(48, "#7e0e4a")],
[S(5, "#fde3bd"), S(15, "#f8bb72"), S(25, "#ef8c35"), S(35, "#d45f16"), S(48, "#8f3406")],
[S(5, "#e6d3f7"), S(15, "#c09ae8"), S(25, "#9660d2"), S(35, "#6c35b0"), S(48, "#40157a")]];
PT_RAMPS[0] = PT_RAMPS[1]; // echo with no surface type flag: drawn as rain, hover says so
// rain rate -> reflectivity on the NWS colour table (the same table the observed radar products use)
function rateEq(mmh) { return 10 * Math.log10(200 * Math.pow(Math.max(mmh, 1e-6), 1.6)); } // colour index only (Marshall-Palmer)
// precipitation accumulation, with a selector for the precipitation types: the files hold the total since the run
// started (16-bit, 0.01 in / snow 0.1 in); the page subtracts two hours for the window and bins the result on a
// square-root scale into the 8-bit code the shader colours (vmax per type)
var ACC_TYPES = {
apcp: { label: "Total", vmax: 20, unitLabel: "inches of liquid", ticks: [0.01, 0.1, 0.25, 0.5, 1, 2, 3, 5, 10], lo: 0.01, hi: 15, levels: [0.5, 1, 2],
stops: [S(0.01, "#d6f0c4", 0.9), S(0.1, "#9bd685"), S(0.25, "#4cb24a"), S(0.5, "#1e873a"), S(0.75, "#2c72b8"), S(1, "#1d409f"),
S(1.5, "#7a4fb7"), S(2, "#ae3bb0"), S(3, "#d42a3b"), S(4, "#ef6a25"), S(5, "#f8a62c"), S(7, "#fbe46c"), S(10, "#fff6cf"), S(15, "#ffffff")] },
asnow: { label: "Snow", vmax: 100, unitLabel: "inches of snow", ticks: [0.1, 1, 2, 4, 6, 8, 12, 18, 24, 36], lo: 0.1, hi: 48, levels: [1, 6, 12],
stops: [S(0.1, "#e2effc", 0.9), S(1, "#a9cdf2"), S(2, "#6fa8e6"), S(3, "#3d7fd4"), S(4, "#2152b0"), S(6, "#1d2f8f"), S(8, "#5a2a96"),
S(12, "#9a3fb0"), S(18, "#cf62c6"), S(24, "#f29ad8"), S(36, "#f9d2ea"), S(48, "#b07040")] },
afzra: { label: "Freezing rain", vmax: 5, unitLabel: "inches of liquid falling as freezing rain", ticks: [0.01, 0.05, 0.1, 0.25, 0.5, 1], lo: 0.01, hi: 1.5, levels: [0.1, 0.25, 0.5],
stops: [S(0.01, "#fbd8e8", 0.9), S(0.05, "#f4a6c8"), S(0.1, "#e872a8"), S(0.25, "#d23d86"), S(0.5, "#a51e66"), S(1, "#6a0d44")] },
aip: { label: "Sleet", vmax: 5, unitLabel: "inches of liquid falling as sleet", ticks: [0.01, 0.05, 0.1, 0.25, 0.5, 1], lo: 0.01, hi: 1.5, levels: [0.25, 0.5, 1],
stops: [S(0.01, "#fde6c8", 0.9), S(0.05, "#f9c27e"), S(0.1, "#f39a45"), S(0.25, "#d9681c"), S(0.5, "#a8460c"), S(1, "#6e2a04")] } };
// winter accumulations (scripts/models/winter.py): FRAM radial ice, snowfall with the Cobb column snow ratio (HRRR),
// snowfall at 10:1 (HRRR, RRFS), the snow-depth increase. Same files and window differencing as the others.
var SNOW_STOPS = ACC_TYPES.asnow.stops;
ACC_TYPES.as10 = { derived: true, label: "Snow 10:1", vmax: 100, unitLabel: "inches of snow at 10:1 from the snow-phase liquid", ticks: ACC_TYPES.asnow.ticks, lo: 0.1, hi: 48, levels: [1, 6, 12], stops: SNOW_STOPS, snow: true };
ACC_TYPES.acobb = { derived: true, label: "Snow (Cobb ratio)", vmax: 100, unitLabel: "inches of snow with the Cobb column snow-to-liquid ratio (limited to 3–30)", ticks: ACC_TYPES.asnow.ticks, lo: 0.1, hi: 48, levels: [1, 6, 12], stops: SNOW_STOPS, snow: true };
ACC_TYPES.asnod = { derived: true, label: "Snow depth increase", vmax: 100, unitLabel: "inches: the sum of the hourly increases of the model's snow depth (melt and settling not subtracted; over existing snow, small up-and-down changes in the depth add up, so totals there run high)", ticks: ACC_TYPES.asnow.ticks, lo: 0.1, hi: 48, levels: [1, 6, 12], stops: SNOW_STOPS, snow: true };
ACC_TYPES.afram = { derived: true, label: "Ice (FRAM)", vmax: 3, unitLabel: "inches of RADIAL ice on power lines and branches (FRAM), not the liquid amount · 0.25 in bold (a common ice-storm-warning criterion)", ticks: [0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 1], lo: 0.01, hi: 1.5, levels: [0.1, 0.5], bold: 0.25,
stops: [S(0.01, "#e4ecf8", 0.9), S(0.05, "#b8cbef"), S(0.1, "#8aa5e2"), S(0.25, "#6a5ccb"), S(0.5, "#8e2db1"), S(0.75, "#c3197b"), S(1, "#6e0b3a")] };
ACC_TYPES.asnow.snow = true;
var ACC_ORDER = ["apcp", "asnow", "acobb", "as10", "asnod", "afzra", "afram", "aip"], ACC_WINS = [0, 1, 3, 6, 12, 24, 48];
var ACC_NA = { acobb: { rrfs: "Snowfall with the Cobb snow ratio is computed for HRRR only, from its hourly temperature, humidity and vertical-motion profiles. For the RRFS, use Snow 10:1 or the model's own snowfall.",
rdps: "Snowfall with the Cobb snow ratio is computed for HRRR only: it needs hourly temperature, humidity and vertical-motion profiles, which the RDPS publishes at three levels only." },
as10: { rdps: "The RDPS's Snow map is already 10:1 (ten times its accumulated snow water); a separate 10:1 map would be the same." } };
FIELDS.acc = { label: "Accumulation", enc: "accd", unit: "in" };
// ECAPE: HRRR only, computed by the
// ECAPE job (scripts/ecape/to_models.py) from the same runs; files ecape_mu / ecape_ml / mucape / mlcape, code = round(K*sqrt(J/kg)),
// listed in hrrr.json["ecape"][cycle] and joined to the cycle on load. The ratio is divided here (masked where CAPE <= 100 J/kg,
// as render_ecape.py does). Colour tables: one bright hue per class (below).
var EC_SUBS = {
ecape_mu: { label: "MU ECAPE", long: "entraining CAPE, most-unstable parcel", kind: "cape", files: ["ecape_mu"] },
ecape_ml: { label: "ML ECAPE", long: "entraining CAPE, mixed-layer parcel", kind: "cape", files: ["ecape_ml"] },
ratio_mu: { label: "ECAPE/CAPE, MU", long: "ECAPE / CAPE, most-unstable parcel", kind: "ratio", files: ["ecape_mu", "mucape"] },
ratio_ml: { label: "ECAPE/CAPE, ML", long: "ECAPE / CAPE, mixed-layer parcel", kind: "ratio", files: ["ecape_ml", "mlcape"] },
mucape: { label: "MU CAPE", long: "undiluted CAPE, most-unstable parcel", kind: "cape", files: ["mucape"] },
mlcape: { label: "ML CAPE", long: "undiluted CAPE, mixed-layer parcel", kind: "cape", files: ["mlcape"] } };
var EC_ORDER = ["ecape_mu", "ecape_ml", "ratio_mu", "ratio_ml", "mucape", "mlcape"];
var CAPE_LEVELS = [0, 100, 250, 500, 750, 1000, 1500, 2000, 2500, 3000, 4000, 5000];
// one distinct hue per class, so differences are easy to see - blues for marginal, greens to yellow for moderate,
// orange-red for large, magenta-purple for extreme
var CAPE_COLORS = ["#ffffff", "#bfe0f2", "#72b6e6", "#2f7fd0", "#1fa463", "#8fd043", "#f3e228", "#f6a623", "#ea5a1f", "#c8172d", "#d63fa8", "#7b2fc4"];
var RATIO_LEVELS = [0.0, 0.15, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.25, 1.5];
var RATIO_COLORS = ["#4a2377", "#6b3fbf", "#3d64d8", "#249cdd", "#20b7ad", "#37b658", "#97cf37", "#eadf27", "#f7b329", "#f27a20", "#df3a24", "#c0175d", "#6e0a30"];
function stepColour(levels, colors, v) { // matplotlib BoundaryNorm, extend="max": bin i = [L[i], L[i+1])
if (v === null || !(v >= levels[0])) return null;
var i = levels.length - 1; while (i > 0 && v < levels[i]) i--;
var h = colors[i]; return [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16), 1];
}
function capeOf(c, k) { return c === 255 ? null : (c / k) * (c / k); }
FIELDS.ecape = { label: "ECAPE / CAPE", enc: "sqrt", unit: "J/kg" };
// ── upper air (925 and 250 hPa wind, 500 hPa vorticity, dynamic tropopause): scripts/models/upper.py; every 3rd
// hour (the slider walks those hours on these maps). Files: 3 channels.
// ua925 / ua250: R, G = earth-relative u, v (off + step * code), B = height (per-frame q, like t2m), on the 6 km grid
// (RDPS 10 km); dt: R = theta on 2 PVU (250 K + 0.6 K), G, B = the wind on it; z500: R = absolute vorticity, G = height;
// dt and z500 on the 12 km grid (RDPS 20 km). All drawn with bilinear interpolation (shader modes 4 and 5).
function kts(list) { return list.map(function (a) { return S(a[0] / KT, a[1], a[2]); }); }
FIELDS.ua925 = { label: "925 hPa wind", enc: "lin2", off: -63.5, step: 0.5, unit: "kt", kt: true, smax: 48, bil: true, ch: 3, ua: true, barbs: true, zc: 2,
stops: kts([[0, "#f6f6f4"], [10, "#d3e4f1"], [20, "#9cc5e3"], [30, "#5c9fcd"], [40, "#3f9b6a"], [50, "#8cc24a"], [60, "#e6d93a"], [70, "#f8a93a"], [80, "#e4552f"], [90, "#a8124f"]]),
ticksKt: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90] };
// the jet: nothing below ~45 kt, then the usual blues, greens, yellow, orange, red, purple to 190 kt
FIELDS.ua250 = { label: "250 hPa wind", enc: "lin2", off: -101.6, step: 0.8, unit: "kt", kt: true, smax: 100, bil: true, ch: 3, ua: true, barbs: true, zc: 2,
stops: kts([[40, "#e8f1f7", 0], [50, "#c4def0"], [70, "#86bce5"], [90, "#4b8bd0"], [110, "#2f9e5b"], [130, "#e6d636"], [150, "#f3972b"], [170, "#d7301f"], [190, "#8b0a6e"]]),
ticksKt: [50, 70, 90, 110, 130, 150, 170, 190] };
FIELDS.dt = { label: "Dynamic tropopause", enc: "lin", off: 250, step: 0.6, unit: "K", bil: true, ch: 3, ua: true, barbs: true, uvc: 1, uv: [-101.6, 0.8],
fmt: function (v) { return v.toFixed(1) + " K"; } };
// the z500 map's barbs: companion frame w500, u, v as 250 hPa, same grid; never a map of its own
FIELDS.w500 = { label: "500 hPa wind", enc: "lin2", off: -101.6, step: 0.8, unit: "kt", kt: true, smax: 100, ch: 2, barbs: true, companion: true };
FIELDS.z500 = { label: "500 hPa vorticity", enc: "lin", off: -30, step: 0.5, unit: "10⁻⁵ s⁻¹", bil: true, ch: 2, ua: true, zc: 1,
stops: [S(10, "#fff3b0", 0), S(12, "#fff1a8", 0.55), S(16, "#fde26a"), S(20, "#f9b23c"), S(24, "#f07a2a"), S(28, "#dc3d24"), S(32, "#b3172f"), S(40, "#7a0f5a"), S(50, "#4b0a6e")],
ticks: [12, 16, 20, 24, 28, 32, 40, 50] };
// theta on the dynamic tropopause: the site's DT maps (scripts/mjo/src/dt_charts.py): 5 K classes 260-400 K, colours from
// its 19 anchors spread evenly over the 30 classes (matplotlib LinearSegmentedColormap + BoundaryNorm, extend both)
var DT_LEV = range(260, 400, 5);
var DT_ANCH = ["#f2c6e6", "#c77fc9", "#8e3fa8", "#5d2a8a", "#3b3f9e", "#3f6fcf", "#3fa9df", "#4fc6cd", "#6fd6a8",
"#a0e07f", "#d6e76a", "#f6d54c", "#f8ae3d", "#ef8231", "#dc552a", "#bd3029", "#93192b", "#5a0d24", "#3a0a14"];
var DT_COLS = (function () { var n = DT_LEV.length + 1, out = [];
for (var i = 0; i < n; i++) { var t = i / (n - 1) * (DT_ANCH.length - 1), k = Math.min(DT_ANCH.length - 2, Math.floor(t)), f = t - k, a = DT_ANCH[k], b = DT_ANCH[k + 1], c = "#";
for (var q = 0; q < 3; q++) { var x = Math.round(parseInt(a.slice(1 + 2 * q, 3 + 2 * q), 16) * (1 - f) + parseInt(b.slice(1 + 2 * q, 3 + 2 * q), 16) * f); c += (x < 16 ? "0" : "") + x.toString(16); }
out.push(c); } return out; })();
function dtColour(v) { var i = 0; while (i < DT_LEV.length && v >= DT_LEV[i]) i++; var h = DT_COLS[i];
return [parseInt(h.slice(1, 3), 16), parseInt(h.slice(3, 5), 16), parseInt(h.slice(5, 7), 16), 1]; }
// ── convection and the boundary layer (updraft helicity and its swaths, echo tops, PBL height, maximum updraft speed;
// no other severe fields). HRRR/RRFS; RDPS: PBL only.
// uh*: code = round(254 sqrt(UH / 400)), 0 below 1 m2/s2; uvv: 0.25 m/s steps, 0 below 1 m/s; "s" = run swath (running
// max since the run started, stored per hour); etop: kft, 255 = no echo; hpbl: 25 m steps. All 1-channel.
var UH_LEVELS = [25, 50, 75, 100, 150, 200, 300], UH_COLORS = ["#9ad0f0", "#3a9be0", "#2fa24a", "#f2d23a", "#f08a24", "#d7261e", "#a0157a"];
var ET_LEVELS = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50], ET_COLORS = ["#d9eef5", "#a9d8ea", "#6db7dc", "#3b8fcc", "#2c9d63", "#8cc63f", "#f1d63a", "#f39b2d", "#e5532a", "#c01f3e", "#7c1d6f"];
function uhOf(c) { return c === 0 ? 0 : 400 * (c / 254) * (c / 254); }
["uh25", "uh25s", "uh03", "uh03s"].forEach(function (f) {
var swath = /s$/.test(f), lay = f.indexOf("uh25") === 0 ? "2–5 km" : "0–3 km";
FIELDS[f] = { label: "Updraft helicity", sub: lay + (swath ? ", run swath" : ", hourly max"), enc: "uh", unit: "m²/s²", own: true, conv: true, swath: swath,
fmt: function (v) { return Math.round(v) + " m²/s²"; } }; });
FIELDS.uvv = { label: "Max updraft", sub: "hourly max", enc: "uvv", unit: "m/s", own: true, conv: true, fmt: function (v) { return v.toFixed(2) + " m/s"; },
stops: [S(1.9, "#e6f0f7", 0), S(2, "#cfe3f1", 0.85), S(5, "#8cc3e6"), S(10, "#3f8fd2"), S(15, "#2f9e5b"), S(20, "#c7d63a"), S(25, "#f2a830"), S(30, "#e0482a"), S(40, "#a8124f"), S(50, "#4a0f6a")],
ticks: [2, 5, 10, 15, 20, 25, 30, 40, 50] };
FIELDS.uvvs = Object.assign({}, FIELDS.uvv, { sub: "run swath", swath: true });
FIELDS.etop = { label: "Echo tops", sub: "echo tops", enc: "etop", unit: "kft", conv: true, fmt: function (v) { return Math.round(v) + ",000 ft (" + (v * 0.3048).toFixed(1) + " km) above sea level"; } };
FIELDS.hpbl = { label: "PBL height", enc: "lin", off: 0, step: 25, unit: "m", bil: true,
stops: [S(0, "#f4efe6"), S(250, "#e9dcc0"), S(500, "#d9c08d"), S(1000, "#a9c97a"), S(1500, "#5fb07a"), S(2000, "#3c8fb8"), S(2500, "#4a64b8"), S(3000, "#6a45a8"), S(4000, "#8a2a8a"), S(5000, "#5a0d4a")],
ticks: [0, 500, 1000, 1500, 2000, 2500, 3000, 4000, 5000], fmt: function (v) { return Math.round(v).toLocaleString("en-US") + " m (" + Math.round(v * 3.28084).toLocaleString("en-US") + " ft) above ground"; } };
// ── potential vorticity: scripts/models/upper.py, 12 km
// (RDPS 20 km), every 3 h. PV code = (PVU + 2) / 0.0625 (255 = masked). pvu 1 channel (barbs and heights from the ua250
// frame); pvl R = PV, G, B = 850 hPa wind; pv315..pv340 R = PV, G = the isentrope's pressure 100 + 2.5 hPa (wind: w315..)
function pvOf(c) { return -2 + 0.0625 * c; }
// the site's PV colours (scripts/mjo/src/dt_charts.py PV_LEV / PV_COLS): warm below 2 PVU, blue to purple above
var PV_LEV = [0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 12], PV_COLS = ["#fbe9b0", "#f8d27e", "#f2ad57", "#e5803d", "#b9d8ee", "#86b9e3", "#5a96d3", "#3e74c0", "#3654a6", "#3c3a8c", "#4a2675", "#5b1560"];
var PVL_LEV = [0.5, 0.75, 1, 1.5, 2, 3], PVL_COLS = ["#fde7c8", "#f9c58b", "#f19a4f", "#de6a2c", "#b8402a", "#7a1f3a"];
FIELDS.pvu = { label: "Upper-level PV", enc: "pv", unit: "PVU", bil: true, ch: 1, ua: true, pv: true };
FIELDS.pvl = { label: "Low-level PV", enc: "pv", unit: "PVU", bil: true, ch: 3, ua: true, pv: true, barbs: true, uvc: 1, uv: [-63.5, 0.5] };
[315, 320, 330, 340].forEach(function (t) {
FIELDS["pv" + t] = { label: "PV on " + t + " K", sub: t + " K", enc: "pv", unit: "PVU", bil: true, ch: 2, ua: true, pv: true, theta: t };
if (t === 330) FIELDS.pv330.btn = "Isentropic PV";
FIELDS["w" + t] = { label: "Wind on " + t + " K", enc: "lin2", off: -101.6, step: 0.8, unit: "kt", kt: true, smax: 100, ch: 2, barbs: true, companion: true }; });
// ── snow-band diagnostics (they replace snowbands.html). Computed in models.py by scripts/snowband/snowband.py itself (smooth_diagnose, ingredients):
// every field Gaussian-smoothed (sigma 18 km) on the model's grid before any derivative, evaluated on 12 km (HRRR, the
// "sub" grid) or 10 km (RDPS), every 3 h from +3 h, 1 October - 30 April. Colours = snowband.py's matplotlib classes.
// sbfg R FG (-20.125 + 0.25), G theta700 (240.2 + 0.4 K), B z700 (2402.5 + 5 m; offsets half a step so no code equals a contour level); sbdef R axis (2 deg, from east), G deformation
// (2e-6 s-1); sbepv R EPV* where saturated (-2.5 + 0.025 PVU), G RH % (+128 = upright CI), B 800-600 hPa FG; sbdgz R lift
// (-20 + 0.5 ub/s), G DGZ depth (5 hPa); sbing R 60 x count, G flags (1 FG, 2 EPV*, 4 lift), B 250 x model-snow fraction
var SB = { fg: 4.0, epv: 0.25, lift: 10.0, dmin: 6e-5, // snowband.py ING; the axes' floor
FG_LEV: [0.5, 1, 2, 3, 4, 6, 8, 10, 12, 15, 20], FG_COL: ["#ffffcc", "#ffefa5", "#fede80", "#febf5a", "#fd9e43", "#fd7234", "#f43d25", "#db141e", "#b70026", "#800026", "#800026"],
EPV_LEV: [-1e9, -1.0, -0.5, -0.25, -0.1, 0, 0.1, 0.25, 0.5, 1.0, 1.5],
EPV_COL: ["#5b2a86", "#5b2a86", "#8a4fb8", "#b889d6", "#e3d0f0", "#f4f1e8", "#d9e8d2", "#a8cfa0", "#6aa36a", "#3c7040", "#3c7040"],
DGZ_LEV: [2, 5, 10, 15, 20, 30, 40, 60], DGZ_COL: ["#fff7fb", "#e3e0ee", "#b4c4df", "#75a9cf", "#2685bb", "#046299", "#023858", "#023858"],
ING_COL: ["#f4d9a6", "#c2410c"], GROUND: [217, 214, 207, 0.85] };
var SB_FIELDS = ["sbing", "sbfg", "sbepv", "sbdgz"];
FIELDS.sbfg = { label: "700 hPa frontogenesis", enc: "sbfg", unit: "K/100 km/3 h", bil: true, ch: 3, sb: true, own: true, conv: true, lines: "rgba(28,30,28,0.7)" };
FIELDS.sbepv = { label: "EPV* / CSI", enc: "sbepv", unit: "PVU", bil: true, ch: 3, sb: true, own: true, conv: true };
FIELDS.sbdgz = { label: "DGZ lift", enc: "sbdgz", unit: "µb/s", bil: true, ch: 2, sb: true, own: true, conv: true };
FIELDS.sbing = { label: "Band ingredients", enc: "sbing", unit: "of 3", bil: true, ch: 3, sb: true, own: true, conv: true };
FIELDS.sbdef = { label: "Deformation", enc: "sbdef", ch: 2, companion: true };
function sbVal(enc, c) { // R channel code -> value (null = masked)
if (c === 255) return null;
return enc === "sbfg" ? -20.125 + 0.25 * c : enc === "sbepv" ? -2.5 + 0.025 * c : enc === "sbdgz" ? -20 + 0.5 * c : enc === "sbing" ? c / 60 : null;
}
function sbColour(name, v) {
if (name === "sbfg") return stepColour(SB.FG_LEV, SB.FG_COL, v);
if (name === "sbepv") return stepColour(SB.EPV_LEV, SB.EPV_COL, v);
if (name === "sbdgz") return stepColour(SB.DGZ_LEV, SB.DGZ_COL, v);
if (name === "sbing") return v >= 2.5 ? stepColour([0], [SB.ING_COL[1]], 1) : v >= 1.5 ? stepColour([0], [SB.ING_COL[0]], 1) : null;
return null;
}
// sub-options of one button (like ECAPE's): the button shows the family head; the bar under the field picker switches member
var SUBF = { pv330: ["pv315", "pv320", "pv330", "pv340"], refl: ["refl", "etop"], uh25: ["uh25", "uh25s", "uh03", "uh03s"], uvv: ["uvv", "uvvs"] };
var FAM = {}; Object.keys(SUBF).forEach(function (h) { SUBF[h].forEach(function (f) { FAM[f] = h; }); });
function famOf(f) { return FAM[f] || f; }
var famLast = {}; // the member last shown per family
// the field picker, grouped by what people use the maps for; units shown on the buttons
var FIELD_GROUPS = [["Clouds & visibility", ["ir", "ceil", "vis"]], ["Surface", ["t2m", "td2m", "mslp", "wind10", "gust"]], ["Precipitation", ["refl", "acc"]],
["Winter: snow bands", SB_FIELDS],
["Convection", ["ecape", "uh25", "uvv"]], ["Upper air", ["ua925", "ua250", "z500", "dt", "pvu", "pv330", "pvl"]], ["Air quality & energy", ["wind80", "smoke", "sw", "hpbl"]]];
var FIELD_ORDER = [].concat.apply([], FIELD_GROUPS.map(function (g) { return g[1]; }));
var FIELD_UNIT = { ir: "°C", ceil: "ft", vis: "mi", t2m: "°F", td2m: "°F", mslp: "hPa", wind10: "kt", gust: "kt", wind80: "m/s", refl: "dBZ", acc: "in", ecape: "J/kg", smoke: "mg/m²", sw: "W/m²",
ua925: "kt", ua250: "kt", z500: "10⁻⁵/s", dt: "K", uh25: "m²/s²", uvv: "m/s", hpbl: "m", etop: "kft", pvu: "PVU", pv330: "PVU", pvl: "PVU",
sbing: "of 3", sbfg: "K/100 km/3 h", sbepv: "PVU", sbdgz: "µb/s" };
function encOf(f, fm) { return (fm && fm.enc) || FIELDS[f].enc; }
function accValue(type, c) { var vm = ACC_TYPES[type].vmax, t = c / 254; return vm * t * t; }
function chOf(f) { if (FIELDS[f] && FIELDS[f].ch) return FIELDS[f].ch; return f === "wind80" || f === "wind10" || f === "refl" || f === "mslp" || !!ACC_TYPES[f] ? 2 : 1; }
var NOTES = {
rdps: { refl: "<b>RDPS has no simulated reflectivity.</b> Shown: the model's <b>precipitation rate</b> (mm/h, instantaneous) coloured by its precipitation type, on the radar colour scale (colours placed with the Marshall–Palmer Z–R relation, so 1 mm/h looks like light rain on radar; it is a rate, not dBZ). Isobars: MSLP every 2 hPa.",
ir: "<b>RDPS has no simulated satellite band.</b> Shown: the OLR-derived brightness temperature (proxy for IR), T<sub>b</sub> = (OLR/σ)<sup>¼</sup> from the instantaneous outgoing longwave at the top of the atmosphere. Cloud tops come out close to a 10.3 µm image; clear skies read 20–30 K colder than one, because OLR includes the water-vapour bands.",
sw: "RDPS shortwave is the <b>mean over the hour ending at the valid time</b> (difference of the accumulated flux), not an instantaneous value as for HRRR and RRFS.",
smoke: "Smoke over the RDPS domain comes from ECCC's RAQDPS-FireWork air-quality model: column-integrated wildfire-smoke PM2.5. It runs at 00 and 12 UTC to 72 h, so its hours end before the RDPS's." },
hrrr: { refl: "HRRR simulated reflectivity 1 km above ground (what the lowest radar scans see, and the level the precipitation type describes; composite reflectivity would paint elevated echo in surface-type colours), coloured by the model's categorical precipitation type at the surface: priority freezing rain > sleet > snow > rain, rain and snow together = mix; echo with no type flag is drawn as rain. Isobars: MSLP (MAPS reduction, MSLMA) every 2 hPa.",
ir: "HRRR simulated brightness temperature for the GOES-East 10.3 µm window band (UPP field SBT124), computed with the CRTM from the model's clouds and water vapour.",
ceil: "Height above ground of the lowest broken-or-overcast layer. Colours follow the aviation flight categories: magenta LIFR (< 500 ft), red IFR (500–999), blue MVFR (1,000–3,000), green VFR; no shading = no ceiling.",
vis: "Colours follow the aviation flight categories: magenta < 1 mi, red 1–3, blue 3–5, green 5–10; no shading = 10 mi or more.",
smoke: "HRRR-Smoke vertically integrated smoke (column mass density), from satellite-detected fires and the model's plume rise and transport.",
sw: "Downward shortwave radiation at the surface, instantaneous at the valid time.",
gust: "HRRR surface wind gust (GUST), instantaneous at the valid time: the model's estimate of the wind that boundary-layer mixing can bring to the surface, not an hourly maximum. Barbs: the 10 m wind (knots).",
mslp: "HRRR sea-level pressure (MAPS reduction, MSLMA), stored every 4th grid point (12 km) and interpolated. Faded where the ground is above ~1,500 m: the reduction to sea level invents highs and lows there, so none are marked." },
rrfs: { ir: "RRFS simulated GOES-16 ABI band 13 (10.3 µm) brightness temperature.",
refl: "RRFS simulated reflectivity 1 km above ground, coloured by the model's precipitation type at the surface (priority freezing rain > sleet > snow > rain; rain and snow together = mix). Isobars: MSLP (NCEP's MSLET reduction) every 2 hPa.",
smoke: "RRFS-SD smoke: the column mass density of the smoke (organic particulate) tracer." }
};
NOTES.rrfs.gust = NOTES.hrrr.gust.replace("HRRR", "RRFS");
NOTES.rrfs.mslp = "RRFS sea-level pressure (NCEP's MSLET reduction; the 2-D file has no PRMSL), stored every 4th grid point (12 km) and interpolated. Faded where the ground is above ~1,500 m, where no highs or lows are marked.";
NOTES.rdps.gust = "RDPS 10 m wind gust (WindGust), instantaneous at the valid time (not the hourly maximum). Barbs: the 10 m wind (knots).";
NOTES.rdps.mslp = "RDPS sea-level pressure, stored every 2nd grid point (20 km) and interpolated. Faded where the ground is above ~1,500 m, where no highs or lows are marked.";
// upper air
NOTES.hrrr.ua925 = "HRRR 925 hPa wind and height (wrfprs, AWS), every 3 hours, on every 2nd grid point (6 km). Left blank where the ground rises above the 925 hPa level (surface pressure below 925 hPa): the model's values there are extrapolated below ground.";
NOTES.hrrr.ua250 = "HRRR 250 hPa wind and height (wrfprs, AWS), every 3 hours, on every 2nd grid point (6 km).";
NOTES.hrrr.z500 = "HRRR 500 hPa height and absolute vorticity, every 3 hours, on a 12 km grid. The vorticity is computed here from the 500 hPa wind smoothed to 12 km (with the Lambert grid's map factor), plus the Coriolis parameter. Barbs: the 500 hPa wind (knots), from the same smoothed field.";
NOTES.hrrr.dt = "Computed here every 3 hours from the HRRR's temperature and wind on 19 pressure levels (100–700 hPa), smoothed to 12 km: Ertel potential vorticity on the pressure levels, then the 2-PVU surface found by searching <b>downward from 100 hPa</b> (so the low-level PV made by storms and fronts cannot fake a low tropopause), with potential temperature and wind interpolated to it. Where PV is still below 2 PVU at 100 hPa (tropical air) the 100 hPa values are shown. The HRRR's top is at 50 hPa, its domain small: the larger-scale view is on the site's <a href=\"/circulation.html#dt\">dynamic tropopause maps</a>.";
["ua925", "ua250", "z500", "dt"].forEach(function (f) { NOTES.rrfs[f] = NOTES.hrrr[f].replace(/HRRR/g, "RRFS").replace("wrfprs, AWS", "prslev, AWS").replace("RRFS's top is at 50 hPa", "RRFS's top is high"); });
NOTES.rdps.ua925 = "RDPS 925 hPa wind and height, every 3 hours, on its 10 km grid. Left blank where the ground rises above the 925 hPa level.";
NOTES.rdps.ua250 = "RDPS 250 hPa wind and height, every 3 hours, on its 10 km grid.";
NOTES.rdps.z500 = "RDPS 500 hPa height and the model's own absolute vorticity, every 3 hours, smoothed to 20 km. Barbs: the 500 hPa wind (knots).";
NOTES.rdps.dt = "The RDPS's own 2-PVU surface (the pressure of the 2 PVU level, Pressure_PVU-2 on the MSC Datamart), with potential temperature and wind interpolated to it from 16 and 9 pressure levels, every 3 hours, smoothed to 20 km.";
// convection
NOTES.hrrr.uh25 = "HRRR updraft helicity 2–5 km above ground (MXUPHL): the maximum over the hour ending at the time shown, from every model time step, so a storm's whole hour of track is in it. Values above ~75 m²/s² mark rotating updrafts (supercells) in a 3 km model; tracks are a few grid cells wide.";
NOTES.hrrr.uh25s = NOTES.hrrr.uh25.replace("the maximum over the hour ending at the time shown", "the run swath - the largest of the hourly maxima since the run started (stored hour by hour)");
NOTES.hrrr.uh03 = NOTES.hrrr.uh25.replace("2–5 km", "0–3 km") + " The 0–3 km layer picks up low-level rotation (tornado-prone storms, QLCS mesovortices).";
NOTES.hrrr.uh03s = NOTES.hrrr.uh03.replace("the maximum over the hour ending at the time shown", "the run swath - the largest of the hourly maxima since the run started");
NOTES.hrrr.uvv = "HRRR maximum upward vertical velocity in the hour ending at the time shown, between 100 and 1000 hPa (MAXUVV): the strongest updraft in each column. Deep convection shows 10–40 m/s; 1–2 m/s is ordinary rising air.";
NOTES.hrrr.uvvs = NOTES.hrrr.uvv.replace("in the hour ending at the time shown", "since the run started (run swath of the hourly maxima)");
NOTES.hrrr.etop = "HRRR echo top (RETOP): the highest level where the simulated reflectivity reaches 18.3 dBZ, in thousands of feet above sea level, instantaneous at the valid time. Tops above ~40,000 ft mark strong, deep convection.";
NOTES.hrrr.hpbl = "HRRR boundary-layer height (HPBL, from the MYNN scheme), metres above ground, instantaneous, smoothed to 6 km: how deep the air mixed from the surface is. Deep afternoon mixing brings momentum (gusts) down and dilutes smoke; shallow layers at night trap it.";
["uh25", "uh25s", "uh03", "uh03s", "uvv", "uvvs", "etop", "hpbl"].forEach(function (f) { NOTES.rrfs[f] = NOTES.hrrr[f].replace(/HRRR/g, "RRFS"); });
["uh25", "uh25s", "uh03", "uh03s", "uvv", "uvvs"].forEach(function (f) { NOTES.rrfs[f] += " Past +36 h the RRFS is published every 3 hours and each file's maximum covers only its last hour, so swaths there miss two hours in three."; });
// PV charts
var PVN = "Ertel potential vorticity computed here every 3 hours from the {M}'s temperature and wind on {L} pressure levels smoothed to {K} km (the same PV as the dynamic-tropopause map). ";
NOTES.hrrr.pvu = PVN.replace("{M}", "HRRR").replace("{L}", "22").replace("{K}", "12") + "Mean of 250, 275 and 300 hPa (the layer mean is less noisy than one level and spans where the jet-level tropopause usually sits). Air with more than 2 PVU (blue) is stratospheric: tropopause folds, troughs and the jet's cold side.";
NOTES.hrrr.pvl = PVN.replace("{M}", "HRRR").replace("{L}", "22").replace("{K}", "12") + "Mean of 700, 750, 800 and 850 hPa: low-level PV is made by latent heating, so it marks convective systems, mesoscale vortices, fronts and tropical cyclones. Blank where the surface pressure is below 850 hPa.";
[315, 320, 330, 340].forEach(function (t) { NOTES.hrrr["pv" + t] = PVN.replace("{M}", "HRRR").replace("{L}", "22").replace("{K}", "12") + "Interpolated to the " + t + " K isentrope (linear in θ, the first crossing above 700 hPa); blank where " + t + " K lies outside 100–700 hPa. Lower isentropes (315–320 K) suit the cold season, 330–340 K the warm season and the subtropical jet."; });
["pvu", "pvl", "pv315", "pv320", "pv330", "pv340"].forEach(function (f) { NOTES.rrfs[f] = NOTES.hrrr[f].replace(/HRRR/g, "RRFS"); });
["pvu", "pvl", "pv315", "pv320", "pv330", "pv340"].forEach(function (f) { NOTES.rdps[f] = NOTES.hrrr[f].replace(/HRRR/g, "RDPS").replace("on 22 pressure levels smoothed to 12 km", "on 19 pressure levels (100–850 hPa, on its rotated latitude-longitude grid) smoothed to 20 km"); });
NOTES.rdps.hpbl = "RDPS boundary-layer height (PlanetaryBoundaryLayerHeight), metres above ground, instantaneous, smoothed to 20 km.";
NOTES.rrfs.ceil = NOTES.hrrr.ceil; NOTES.rrfs.vis = NOTES.hrrr.vis; NOTES.rrfs.sw = NOTES.hrrr.sw;
var NA = { rdps: { ceil: "The RDPS output has no cloud ceiling.", vis: "The RDPS output has no visibility." } };
["uh25", "uh25s", "uh03", "uh03s", "uvv", "uvvs", "etop"].forEach(function (f) { NA.rdps[f] = (FIELDS[f].label) + " is computed for the HRRR and the RRFS only: the RDPS output has no " + (f === "etop" ? "echo top." : FIELDS[f].enc === "uh" ? "updraft helicity." : "hourly maximum vertical velocity."); });
// snow-band diagnostics
var SBN = "Smoothed with a Gaussian filter (σ = 18 km) on the {M}'s own grid before any derivative and evaluated on {K} km, every 3 hours from +3 h: the same computation as the site's former snow-band page (the code is shared). ";
NOTES.hrrr.sbfg = "<b>700 hPa frontogenesis</b> (2-D kinematic, Petterssen), K per 100 km per 3 h: where the flow is packing the temperature gradient together. Bands form on the warm side of a mid-level frontogenesis maximum, under the rising branch of the circulation it forces (Nicosia and Grumm 1999; Novak et al. 2004); 2–10 is typical of banded snowstorms. Brown lines: 700 hPa θ every 2 K; black: 700 hPa height every 30 m (dam); short navy segments: the axes of dilatation where the total deformation is in the top 15 % of the map in view (and at least 6 × 10⁻⁵ s⁻¹) - isentropes at a small angle to them are being packed together. Grey: 700 hPa within 25 hPa of the ground. " + SBN;
NOTES.hrrr.sbepv = "<b>Saturated equivalent potential vorticity</b> (EPV*), 750–500 hPa mean, PVU, drawn only where that layer is saturated (relative humidity 80 % or more, over ice below 0 °C; grey outline). Negative EPV* in saturated air means a slantwise parcel would keep accelerating: conditional symmetric instability (CSI), under which a frontal circulation collapses into a narrow, strong band. <b>Hatched</b>: θ<sub>es</sub> falls with height, so the negative EPV* there is ordinary upright instability, not CSI. Magenta lines: 800–600 hPa frontogenesis 2, 4, 8, 16. Caveat (Schultz and Schumacher 1999): negative EPV* is necessary, not sufficient - read purple as \"the atmosphere would not resist a band here\", never \"a band is here\". " + SBN;
NOTES.hrrr.sbdgz = "<b>Lift in the dendritic growth zone</b>: the strongest upward motion (−ω, microbars per second; 10 µb/s is about 10 cm/s) anywhere in the column where it is −12 to −18 °C and saturated with respect to ice. Strong lift through a deep saturated DGZ is the \"crosshair\" (Waldstreicher 2001) behind 2–4 in/h snowfall rates. Brown lines: the depth of the −12 to −18 °C layer, 50 / 100 / 150 hPa. The column is sampled every 5 hPa from the model's 25 hPa levels. " + SBN;
NOTES.hrrr.sbing = "<b>Band ingredients</b>: where 700 hPa frontogenesis ≥ 4 K/100 km/3 h, EPV* ≤ 0.25 PVU in saturated air (750–500 hPa) and DGZ lift ≥ 10 µb/s overlap - pale orange two of the three, dark orange all three. Navy outline: the model's own snow at 25 dBZ or more (composite reflectivity where its precipitation type is snow); grey lines: 700 hPa height. <b>An overlap of ingredients, not a probability</b>: not calibrated against observed bands, says nothing about rain or snow, and placement errors of 50–100 km are normal. " + SBN;
SB_FIELDS.forEach(function (f) {
NOTES.hrrr[f] = NOTES.hrrr[f].replace("{M}", "HRRR").replace("{K}", "12");
NOTES.rrfs[f] = NOTES.hrrr[f].replace(/HRRR/g, "RRFS") + (f === "sbdgz" || f === "sbing" ? " The RRFS writes geometric vertical velocity (DZDT); it is converted to ω here hydrostatically." : "");
NOTES.rdps[f] = NOTES.hrrr[f].replace(/HRRR/g, "RDPS").replace("evaluated on 12 km", "evaluated on its 10 km grid")
.replace("from the model's 25 hPa levels", "from the RDPS's 25–50 hPa levels; it publishes vertical velocity only at 850, 700 and 500 hPa, so the lift is interpolated linearly in pressure between them and cannot show a narrow maximum between levels")
.replace("the model's own snow at 25 dBZ or more (composite reflectivity where its precipitation type is snow)", "the model's own snow at 1 mm or more in the hour (the RDPS has no reflectivity)");
});
// ── display projection: the HRRR's Lambert conformal (tangent at 38.5 N, 97.5 W, sphere 6371229 m) ────────────────
var P = (function () {
var R = 6371229, phi = 38.5 * D2R, lon0 = -97.5 * D2R;
var n = Math.sin(phi), t = Math.tan(Math.PI / 4 + phi / 2);
var F = Math.cos(phi) * Math.pow(t, n) / n, RF = R * F, rho0 = RF / Math.pow(t, n);
return { n: n, RF: RF, rho0: rho0, lon0: lon0,
fwd: function (lon, lat) {
var rho = RF / Math.pow(Math.tan(Math.PI / 4 + lat * D2R / 2), n);
var d = lon * D2R - lon0; d = Math.atan2(Math.sin(d), Math.cos(d));
var th = n * d;
return [rho * Math.sin(th), rho0 - rho * Math.cos(th)];
},
inv: function (x, y) {
var dy = rho0 - y, rho = Math.sign(n) * Math.sqrt(x * x + dy * dy), th = Math.atan2(x, dy);
return [(lon0 + th / n) / D2R, (2 * Math.atan(Math.pow(RF / rho, 1 / n)) - Math.PI / 2) / D2R];
} };
})();
// grid index (fractional) of a display point, identical to the shader
function gridIJ(g, x, y) {
if (g.type === "lcc") return [(x - g._x1) / g.dx, (y - g._y1) / g.dy];
var ll = P.inv(x, y), lon = ll[0] * D2R, lat = ll[1] * D2R;
var lo = lon - g.splon * D2R, cx = Math.cos(lat) * Math.cos(lo), cy = Math.cos(lat) * Math.sin(lo), cz = Math.sin(lat);
var th = (90 + g.splat) * D2R, xr = Math.cos(th) * cx + Math.sin(th) * cz, zr = -Math.sin(th) * cx + Math.cos(th) * cz;
var lr = Math.atan2(cy, xr) / D2R, pr = Math.asin(zr) / D2R;