11import type { Config } from "@classmodel/class/config" ;
2- import { calculatePlume , transposePlumeData } from "@classmodel/class/fire" ;
32import {
4- type ClassOutput ,
53 type OutputVariableKey ,
6- getOutputAtTime ,
74 outputVariables ,
85} from "@classmodel/class/output" ;
9- import {
10- type ClassProfile ,
11- NoProfile ,
12- generateProfiles ,
13- } from "@classmodel/class/profiles" ;
6+ import type { ClassProfile } from "@classmodel/class/profiles" ;
7+ import type { ClassData } from "@classmodel/class/runner" ;
148import * as d3 from "d3" ;
159import { saveAs } from "file-saver" ;
1610import { toBlob } from "html-to-image" ;
@@ -43,7 +37,7 @@ import { MdiCamera, MdiDelete, MdiImageFilterCenterFocus } from "./icons";
4337import { AxisBottom , AxisLeft , getNiceAxisLimits } from "./plots/Axes" ;
4438import { Chart , ChartContainer , type ChartData } from "./plots/ChartContainer" ;
4539import { Legend } from "./plots/Legend" ;
46- import { Line , type Point } from "./plots/Line" ;
40+ import { Line } from "./plots/Line" ;
4741import { SkewTPlot , type SoundingRecord } from "./plots/skewTlogP" ;
4842import { Button } from "./ui/button" ;
4943import { Card , CardContent , CardHeader , CardTitle } from "./ui/card" ;
@@ -76,7 +70,7 @@ interface FlatExperiment {
7670 color : string ;
7771 linestyle : string ;
7872 config : Config ;
79- output ?: ClassOutput ;
73+ output ?: ClassData ;
8074}
8175
8276// Create a derived store for looping over all outputs:
@@ -117,7 +111,7 @@ const flatObservations: () => Observation[] = createMemo(() => {
117111} ) ;
118112
119113const _allTimes = ( ) =>
120- new Set ( flatExperiments ( ) . flatMap ( ( e ) => e . output ?. utcTime ?? [ ] ) ) ;
114+ new Set ( flatExperiments ( ) . flatMap ( ( e ) => e . output ?. timeseries . utcTime ?? [ ] ) ) ;
121115const uniqueTimes = ( ) => [ ...new Set ( _allTimes ( ) ) ] . sort ( ( a , b ) => a - b ) ;
122116
123117// TODO: could memoize all reactive elements here, would it make a difference?
@@ -131,11 +125,15 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
131125
132126 const allX = ( ) =>
133127 flatExperiments ( ) . flatMap ( ( e ) =>
134- e . output ? e . output [ analysis . xVariable as OutputVariableKey ] : [ ] ,
128+ e . output
129+ ? e . output . timeseries [ analysis . xVariable as OutputVariableKey ]
130+ : [ ] ,
135131 ) ;
136132 const allY = ( ) =>
137133 flatExperiments ( ) . flatMap ( ( e ) =>
138- e . output ? e . output [ analysis . yVariable as OutputVariableKey ] : [ ] ,
134+ e . output
135+ ? e . output . timeseries [ analysis . yVariable as OutputVariableKey ]
136+ : [ ] ,
139137 ) ;
140138
141139 const granularities : Record < string , number | undefined > = {
@@ -155,12 +153,12 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
155153 ...formatting ,
156154 data :
157155 // Zip x[] and y[] into [x, y][]
158- output ?. t . map ( ( _ , t ) => ( {
156+ output ?. timeseries . t . map ( ( _ , t ) => ( {
159157 x : output
160- ? output [ analysis . xVariable as OutputVariableKey ] [ t ]
158+ ? output . timeseries [ analysis . xVariable as OutputVariableKey ] [ t ]
161159 : Number . NaN ,
162160 y : output
163- ? output [ analysis . yVariable as OutputVariableKey ] [ t ]
161+ ? output . timeseries [ analysis . yVariable as OutputVariableKey ] [ t ]
164162 : Number . NaN ,
165163 } ) ) || [ ] ,
166164 } ;
@@ -258,87 +256,90 @@ export function VerticalProfilePlot({
258256 variableOptions [ analysis . variable as keyof typeof variableOptions ] ;
259257
260258 type PlumeVariable = "theta" | "qt" | "thetav" | "T" | "Td" | "rh" | "w" ;
259+
261260 function isPlumeVariable ( v : string ) : v is PlumeVariable {
262261 return [ "theta" , "qt" , "thetav" , "T" , "Td" , "rh" , "w" ] . includes ( v ) ;
263262 }
264263
265- const showPlume = createMemo ( ( ) => isPlumeVariable ( classVariable ( ) ) ) ;
264+ type LineSet = {
265+ label : string ;
266+ color : string ;
267+ linestyle : string ;
268+ data : { x : number ; y : number } [ ] ;
269+ } ;
266270
267- const observations = ( ) =>
268- flatObservations ( ) . map ( ( o ) => observationsForProfile ( o , classVariable ( ) ) ) ;
271+ function getLinesForExperiment (
272+ e : FlatExperiment ,
273+ variable : string ,
274+ type : "profiles" | "plumes" ,
275+ timeVal : number ,
276+ ) : LineSet {
277+ const { label, color, linestyle, output } = e ;
269278
270- const profileData = ( ) =>
271- flatExperiments ( ) . map ( ( e ) => {
272- const { config, output, ...formatting } = e ;
273- const t = output ?. utcTime . indexOf ( uniqueTimes ( ) [ analysis . time ] ) ;
274- if ( config . sw_ml && output && t !== undefined && t !== - 1 ) {
275- const outputAtTime = getOutputAtTime ( output , t ) ;
276- return { ...formatting , data : generateProfiles ( config , outputAtTime ) } ;
277- }
278- return { ...formatting , data : NoProfile } ;
279- } ) ;
279+ if ( ! output ) return { label, color, linestyle, data : [ ] } ;
280280
281- const firePlumes = ( ) =>
282- flatExperiments ( ) . map ( ( e , i ) => {
283- const { config, output, ...formatting } = e ;
284- if ( config . sw_fire && isPlumeVariable ( classVariable ( ) ) ) {
285- const plume = transposePlumeData (
286- calculatePlume ( config , profileData ( ) [ i ] . data ) ,
287- ) ;
288- return {
289- ...formatting ,
290- linestyle : "4" ,
291- data : plume . z . map ( ( z , i ) => ( {
292- x : plume [ classVariable ( ) as PlumeVariable ] [ i ] ,
293- y : z ,
294- } ) ) ,
295- } ;
296- }
297- return { ...formatting , data : [ ] } ;
298- } ) ;
281+ const profile = output [ type ] ;
282+ if ( ! profile ) return { label, color, linestyle, data : [ ] } ;
299283
300- // TODO: There should be a way that this isn't needed.
301- const profileDataForPlot = ( ) =>
302- profileData ( ) . map ( ( { data, label, color, linestyle } ) => ( {
303- label,
284+ // Find experiment-specific time index
285+ const tIndex = output . timeseries ?. utcTime ?. indexOf ( timeVal ) ;
286+ if ( tIndex === undefined || tIndex === - 1 )
287+ return { label, color, linestyle, data : [ ] } ;
288+
289+ const linesAtTime = profile [ variable ] ?. [ tIndex ] ?? [ ] ;
290+
291+ return {
292+ label : type === "plumes" ? `${ label } - plume` : label ,
304293 color,
305- linestyle,
306- data : data . z . map ( ( z , i ) => ( {
307- x : data [ classVariable ( ) ] [ i ] ,
308- y : z ,
309- } ) ) ,
310- } ) ) as ChartData < Point > [ ] ;
311-
312- const allX = ( ) => [
313- ...firePlumes ( ) . flatMap ( ( p ) => p . data . map ( ( d ) => d . x ) ) ,
314- ...profileDataForPlot ( ) . flatMap ( ( p ) => p . data . map ( ( d ) => d . x ) ) ,
315- ...observations ( ) . flatMap ( ( obs ) => obs . data . map ( ( d ) => d . x ) ) ,
316- ] ;
317- const allY = ( ) => [
318- ...firePlumes ( ) . flatMap ( ( p ) => p . data . map ( ( d ) => d . y ) ) ,
319- ...profileDataForPlot ( ) . flatMap ( ( p ) => p . data . map ( ( d ) => d . y ) ) ,
320- ...observations ( ) . flatMap ( ( obs ) => obs . data . map ( ( d ) => d . y ) ) ,
321- ] ;
322-
323- // TODO: better to include jump at top in extent calculation rather than adding random margin.
324- const xLim = ( ) => getNiceAxisLimits ( allX ( ) , 1 ) ;
325- const yLim = ( ) => [ 0 , getNiceAxisLimits ( allY ( ) , 0 ) [ 1 ] ] as [ number , number ] ;
294+ linestyle : type === "plumes" ? "4" : linestyle ,
295+ data : linesAtTime . flat ( ) ,
296+ } ;
297+ }
326298
327- function chartData ( ) {
328- return [ ...profileData ( ) , ...observations ( ) ] ;
299+ /** Collect all lines across experiments for a given type */
300+ function collectLines ( type : "profiles" | "plumes" ) : LineSet [ ] {
301+ const variable = classVariable ( ) ;
302+ return flatExperiments ( ) . map ( ( e ) =>
303+ getLinesForExperiment ( e , variable , type , uniqueTimes ( ) [ analysis . time ] ) ,
304+ ) ;
329305 }
330306
331- const [ toggles , setToggles ] = createStore < Record < string , boolean > > ( { } ) ;
307+ /** Lines to plot */
308+ const profileLines = ( ) => collectLines ( "profiles" ) ;
309+ // Only collect plumes for experiments that actually have plume output
310+ const plumeLines = ( ) =>
311+ flatExperiments ( )
312+ . filter ( ( e ) => e . output ?. plumes ) // only show plume when firemodel enabled
313+ . filter ( ( e ) => isPlumeVariable ( classVariable ( ) ) ) // only show plume for plume vars
314+ . map ( ( e ) =>
315+ getLinesForExperiment (
316+ e ,
317+ classVariable ( ) ,
318+ "plumes" ,
319+ uniqueTimes ( ) [ analysis . time ] ,
320+ ) ,
321+ ) ;
322+ const obsLines = ( ) =>
323+ flatObservations ( ) . map ( ( o ) => observationsForProfile ( o , classVariable ( ) ) ) ;
324+ const allLines = ( ) => [ ...profileLines ( ) , ...plumeLines ( ) , ...obsLines ( ) ] ;
332325
333- // Initialize all lines as visible
334- for ( const d of chartData ( ) ) {
335- setToggles ( d . label , true ) ;
336- }
326+ /** Global axes extents across all experiments, times, and observations */
327+ const allX = ( ) => allLines ( ) . flatMap ( ( d ) => d . data . map ( ( p ) => p . x ) ) ;
328+ const allY = ( ) => allLines ( ) . flatMap ( ( d ) => d . data . map ( ( p ) => p . y ) ) ;
329+
330+ const xLim = ( ) => getNiceAxisLimits ( allX ( ) , 1 ) ;
331+ const yLim = ( ) => [ 0 , getNiceAxisLimits ( allY ( ) , 0 ) [ 1 ] ] as [ number , number ] ;
337332
333+ /** Initialize toggles for legend */
334+ const [ toggles , setToggles ] = createStore < Record < string , boolean > > ( { } ) ;
335+ for ( const line of allLines ( ) ) {
336+ setToggles ( line . label , true ) ;
337+ }
338338 function toggleLine ( label : string , value : boolean ) {
339339 setToggles ( label , value ) ;
340340 }
341341
342+ /** Change variable handler */
342343 function changeVar ( v : string ) {
343344 updateAnalysis ( analysis , { variable : v } ) ;
344345 setResetPlot ( analysis . id ) ;
@@ -348,39 +349,20 @@ export function VerticalProfilePlot({
348349 < >
349350 < div class = "flex flex-col gap-2" >
350351 < ChartContainer >
351- < Legend
352- entries = { ( ) => [ ...profileData ( ) , ...observations ( ) ] }
353- toggles = { toggles }
354- onChange = { toggleLine }
355- />
352+ < Legend entries = { allLines } toggles = { toggles } onChange = { toggleLine } />
356353 < Chart id = { analysis . id } title = "Vertical profile plot" >
357354 < AxisBottom domain = { xLim } label = { analysis . variable } />
358355 < AxisLeft domain = { yLim } label = "Height[m]" />
359- < For each = { profileDataForPlot ( ) } >
360- { ( d ) => (
361- < Show when = { toggles [ d . label ] } >
362- < Line { ...d } />
363- </ Show >
364- ) }
365- </ For >
366- < For each = { observations ( ) } >
356+ < For each = { allLines ( ) } >
367357 { ( d ) => (
368358 < Show when = { toggles [ d . label ] } >
369359 < Line { ...d } />
370360 </ Show >
371361 ) }
372362 </ For >
373- < For each = { firePlumes ( ) } >
374- { ( d ) => (
375- < Show when = { toggles [ d . label ] } >
376- < Show when = { showPlume ( ) } >
377- < Line { ...d } />
378- </ Show >
379- </ Show >
380- ) }
381- </ For >
382363 </ Chart >
383364 </ ChartContainer >
365+
384366 < Picker
385367 value = { ( ) => analysis . variable }
386368 setValue = { ( v ) => changeVar ( v ) }
@@ -473,47 +455,71 @@ function Picker(props: PickerProps) {
473455}
474456
475457export function ThermodynamicPlot ( { analysis } : { analysis : SkewTAnalysis } ) {
476- const profileData = ( ) =>
458+ /** Extract profile lines from CLASS output at the current time index */
459+ const profileDataForPlot = ( ) =>
477460 flatExperiments ( ) . map ( ( e ) => {
478- const { config, output, ...formatting } = e ;
479- const t = output ?. utcTime . indexOf ( uniqueTimes ( ) [ analysis . time ] ) ;
480- if ( config . sw_ml && output && t !== undefined && t !== - 1 ) {
481- const outputAtTime = getOutputAtTime ( output , t ) ;
482- return { ...formatting , data : generateProfiles ( config , outputAtTime ) } ;
483- }
484- return { ...formatting , data : NoProfile } ;
485- } ) ;
461+ const { output, label, color, linestyle } = e ;
462+ if ( ! output ?. profiles ) return { label, color, linestyle, data : [ ] } ;
463+
464+ const tIndex = output . timeseries ?. utcTime ?. indexOf (
465+ uniqueTimes ( ) [ analysis . time ] ,
466+ ) ;
467+ if ( tIndex === undefined || tIndex === - 1 )
468+ return { label, color, linestyle, data : [ ] } ;
469+
470+ // Make sure each variable exists and has data at this time
471+ const pLine = output . profiles . p ?. [ tIndex ] ?? [ ] ;
472+ const TLine = output . profiles . T ?. [ tIndex ] ?? [ ] ;
473+ const TdLine = output . profiles . Td ?. [ tIndex ] ?? [ ] ;
474+
475+ // If any line is empty, return empty data
476+ if ( ! pLine . length || ! TLine . length || ! TdLine . length )
477+ return { label, color, linestyle, data : [ ] } ;
478+
479+ const data : SoundingRecord [ ] = pLine . map ( ( _ , i ) => ( {
480+ p : pLine [ i ] . x / 100 ,
481+ T : TLine [ i ] . x ,
482+ Td : TdLine [ i ] . x ,
483+ } ) ) ;
484+
485+ return { label, color, linestyle, data } ;
486+ } ) as ChartData < SoundingRecord > [ ] ;
486487
487488 const firePlumes = ( ) =>
488- flatExperiments ( ) . map ( ( e , i ) => {
489- const { config, output, ...formatting } = e ;
490- if ( config . sw_fire ) {
489+ flatExperiments ( )
490+ . map ( ( e ) => {
491+ const output = e . output ;
492+ if ( ! output ?. plumes ) return null ; // skip if no plume
493+
494+ const tIndex = output . timeseries ?. utcTime ?. indexOf (
495+ uniqueTimes ( ) [ analysis . time ] ,
496+ ) ;
497+ if ( tIndex === undefined || tIndex === - 1 ) return null ;
498+
499+ const pLine = output . plumes . p ?. [ tIndex ] ?? [ ] ;
500+ const TLine = output . plumes . T ?. [ tIndex ] ?? [ ] ;
501+ const TdLine = output . plumes . Td ?. [ tIndex ] ?? [ ] ;
502+
503+ if ( ! pLine . length || ! TLine . length || ! TdLine . length ) return null ;
504+
505+ const data : SoundingRecord [ ] = pLine . map ( ( _ , i ) => ( {
506+ p : pLine [ i ] . x ,
507+ T : TLine [ i ] . x ,
508+ Td : TdLine [ i ] . x ,
509+ } ) ) ;
510+
491511 return {
492- ... formatting ,
512+ label : ` ${ e . label } - fire plume` ,
493513 color : "#ff0000" ,
494- label : ` ${ formatting . label } - fire plume` ,
495- data : calculatePlume ( config , profileData ( ) [ i ] . data ) ,
514+ linestyle : "4" ,
515+ data,
496516 } ;
497- }
498- return { ...formatting , data : [ ] } ;
499- } ) as ChartData < SoundingRecord > [ ] ;
517+ } )
518+ . filter ( ( d ) : d is ChartData < SoundingRecord > => d !== null ) ;
500519
501520 const observations = ( ) =>
502521 flatObservations ( ) . map ( ( o ) => observationsForSounding ( o ) ) ;
503522
504- // TODO: There should be a way that this isn't needed.
505- const profileDataForPlot = ( ) =>
506- profileData ( ) . map ( ( { data, label, color, linestyle } ) => ( {
507- label,
508- color,
509- linestyle,
510- data : data . p . map ( ( p , i ) => ( {
511- p : p / 100 ,
512- T : data . T [ i ] ,
513- Td : data . Td [ i ] ,
514- } ) ) ,
515- } ) ) as ChartData < SoundingRecord > [ ] ;
516-
517523 return (
518524 < >
519525 < SkewTPlot
0 commit comments