Skip to content

Commit ec463bb

Browse files
authored
Add skew-T log-p diagrams (#75)
* Start porting https://github.com/rsobash/d3-skewt/ to solidjs * Make axis ticks and tick format settable + polish code * Add and plot dummy data, looks like a real sounding now * Add title to card and rename to thermodynamic diagram * Change soundingdata format * Convert CLASS output to soundingdata format (proof of concept) * Split skewT background from lines * Plot thermodynamic diagrams for all experiments/permutations * Highlight lines on hover using either tailwind or solid event * Change size of lineplots * Consistent font * Move legend out of lineplot * Reuse legend component in thermodynamic diagram * Make sure all plots share same margins etc. + enable axis labels on skewT * Use analysis also in index.tsx, and turn off final height for production * Start refactoring plotting code * disable failing test as final height is no longer available
1 parent f8648be commit ec463bb

File tree

11 files changed

+584
-212
lines changed

11 files changed

+584
-212
lines changed

apps/class-solid/src/components/Analysis.tsx

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { For, Match, Show, Switch, createMemo, createUniqueId } from "solid-js";
2-
import { getVerticalProfiles } from "~/lib/profiles";
2+
import { getThermodynamicProfiles, getVerticalProfiles } from "~/lib/profiles";
33
import { type Analysis, deleteAnalysis, experiments } from "~/lib/store";
4-
import LinePlot from "./LinePlot";
54
import { MdiCog, MdiContentCopy, MdiDelete, MdiDownload } from "./icons";
5+
import LinePlot from "./plots/LinePlot";
6+
import { SkewTPlot } from "./plots/skewTlogP";
67
import { Button } from "./ui/button";
78
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
89

@@ -37,19 +38,25 @@ export function TimeSeriesPlot() {
3738
.map((perm, j) => {
3839
return {
3940
label: `${e.name}/${perm.name}`,
40-
y: perm.output?.h ?? [],
41-
x: perm.output?.t ?? [],
4241
color: colors[(j + 1) % 10],
4342
linestyle: linestyles[i % 5],
43+
data:
44+
perm.output?.t.map((tVal, i) => ({
45+
x: tVal,
46+
y: perm.output?.h[i] || Number.NaN,
47+
})) || [],
4448
};
4549
});
4650
return [
4751
{
48-
y: experimentOutput?.h ?? [],
49-
x: experimentOutput?.t ?? [],
5052
label: e.name,
5153
color: colors[0],
5254
linestyle: linestyles[i],
55+
data:
56+
experimentOutput?.t.map((tVal, i) => ({
57+
x: tVal,
58+
y: experimentOutput?.h[i] || Number.NaN,
59+
})) || [],
5360
},
5461
...permutationRuns,
5562
];
@@ -79,7 +86,7 @@ export function VerticalProfilePlot() {
7986
color: colors[(j + 1) % 10],
8087
linestyle: linestyles[i % 5],
8188
label: `${e.name}/${p.name}`,
82-
...getVerticalProfiles(p.output, p.config, variable, time),
89+
data: getVerticalProfiles(p.output, p.config, variable, time),
8390
};
8491
});
8592

@@ -88,7 +95,7 @@ export function VerticalProfilePlot() {
8895
label: e.name,
8996
color: colors[0],
9097
linestyle: linestyles[i],
91-
...getVerticalProfiles(
98+
data: getVerticalProfiles(
9299
e.reference.output ?? {
93100
t: [],
94101
h: [],
@@ -113,6 +120,39 @@ export function VerticalProfilePlot() {
113120
);
114121
}
115122

123+
export function ThermodynamicPlot() {
124+
const time = -1;
125+
const skewTData = createMemo(() => {
126+
return experiments.flatMap((e, i) => {
127+
const permutations = e.permutations.map((p, j) => {
128+
// TODO get additional config info from reference
129+
// permutations probably usually don't have gammaq/gammatetha set?
130+
return {
131+
color: colors[(j + 1) % 10],
132+
linestyle: linestyles[i % 5],
133+
label: `${e.name}/${p.name}`,
134+
data: getThermodynamicProfiles(p.output, p.config, time),
135+
};
136+
});
137+
138+
return [
139+
{
140+
label: e.name,
141+
color: colors[0],
142+
linestyle: linestyles[i],
143+
data: getThermodynamicProfiles(
144+
e.reference.output,
145+
e.reference.config,
146+
time,
147+
),
148+
},
149+
...permutations,
150+
];
151+
});
152+
});
153+
return <SkewTPlot data={skewTData} />;
154+
}
155+
116156
/** Simply show the final height for each experiment that has output */
117157
function FinalHeights() {
118158
return (
@@ -154,7 +194,7 @@ function FinalHeights() {
154194
export function AnalysisCard(analysis: Analysis) {
155195
const id = createUniqueId();
156196
return (
157-
<Card class="w-[500px]" role="article" aria-labelledby={id}>
197+
<Card class="min-w-[500px]" role="article" aria-labelledby={id}>
158198
<CardHeader class="flex-row items-center justify-between py-2 pb-6">
159199
{/* TODO: make name & description editable */}
160200
<CardTitle id={id}>{analysis.name}</CardTitle>
@@ -183,6 +223,7 @@ export function AnalysisCard(analysis: Analysis) {
183223
</CardHeader>
184224
<CardContent class="min-h-[450px]">
185225
<Switch fallback={<p>Unknown analysis type</p>}>
226+
{/* @ts-ignore: kept for developers, but not included in production */}
186227
<Match when={analysis.type === "finalheight"}>
187228
<FinalHeights />
188229
</Match>
@@ -192,6 +233,9 @@ export function AnalysisCard(analysis: Analysis) {
192233
<Match when={analysis.type === "profiles"}>
193234
<VerticalProfilePlot />
194235
</Match>
236+
<Match when={analysis.type === "skewT"}>
237+
<ThermodynamicPlot />
238+
</Match>
195239
</Switch>
196240
</CardContent>
197241
</Card>

apps/class-solid/src/components/LinePlot.tsx

Lines changed: 0 additions & 126 deletions
This file was deleted.

apps/class-solid/src/components/Axes.tsx renamed to apps/class-solid/src/components/plots/Axes.tsx

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,26 @@ interface AxisProps {
99
transform?: string;
1010
tickCount?: number;
1111
label?: string;
12+
tickValues?: number[];
13+
tickFormat?: (n: number | { valueOf(): number }) => string;
14+
decreasing?: boolean;
1215
}
1316

14-
export const AxisBottom = (props: AxisProps) => {
15-
const ticks = () => {
16-
const domain = props.scale.domain();
17-
const tickCount = props.tickCount || 5;
17+
const ticks = (props: AxisProps) => {
18+
const domain = props.scale.domain();
19+
const generateTicks = (domain = [0, 1], tickCount = 5) => {
1820
const step = (domain[1] - domain[0]) / (tickCount - 1);
19-
20-
return [...Array(tickCount).keys()].map((i) => {
21-
const value = domain[0] + i * step;
22-
return {
23-
value,
24-
position: props.scale(value),
25-
};
26-
});
21+
return [...Array(10).keys()].map((i) => domain[0] + i * step);
2722
};
2823

24+
const values = props.tickValues
25+
? props.tickValues.filter((x) => x >= domain[0] && x <= domain[1])
26+
: generateTicks(domain, props.tickCount);
27+
return values.map((value) => ({ value, position: props.scale(value) }));
28+
};
29+
30+
export const AxisBottom = (props: AxisProps) => {
31+
const format = props.tickFormat ? props.tickFormat : d3.format(".3g");
2932
return (
3033
<g transform={props.transform}>
3134
<line
@@ -35,12 +38,12 @@ export const AxisBottom = (props: AxisProps) => {
3538
y2="0"
3639
stroke="currentColor"
3740
/>
38-
<For each={ticks()}>
41+
<For each={ticks(props)}>
3942
{(tick) => (
4043
<g transform={`translate(${tick.position}, 0)`}>
4144
<line y2="6" stroke="currentColor" />
4245
<text y="9" dy="0.71em" text-anchor="middle">
43-
{d3.format(".3g")(tick.value)}
46+
{format(tick.value)}
4447
</text>
4548
</g>
4649
)}
@@ -53,21 +56,8 @@ export const AxisBottom = (props: AxisProps) => {
5356
};
5457

5558
export const AxisLeft = (props: AxisProps) => {
56-
const ticks = () => {
57-
const domain = props.scale.domain();
58-
const tickCount = props.tickCount || 5;
59-
const step = (domain[1] - domain[0]) / (tickCount - 1);
60-
61-
return [...Array(tickCount).keys()].map((i) => {
62-
const value = domain[0] + i * step;
63-
return {
64-
value,
65-
position: props.scale(value),
66-
};
67-
});
68-
};
69-
70-
const labelpos = props.scale.range().reduce((a, b) => a + b) / 2;
59+
const format = props.tickFormat ? props.tickFormat : d3.format(".0f");
60+
const yAnchor = props.decreasing ? 0 : 1;
7161
return (
7262
<g transform={props.transform}>
7363
<line
@@ -77,23 +67,39 @@ export const AxisLeft = (props: AxisProps) => {
7767
y2={props.scale.range()[1]}
7868
stroke="currentColor"
7969
/>
80-
<For each={ticks()}>
70+
<For each={ticks(props)}>
8171
{(tick) => (
8272
<g transform={`translate(0, ${tick.position})`}>
8373
<line x2="-6" stroke="currentColor" />
8474
<text x="-9" dy="0.32em" text-anchor="end">
85-
{tick.value.toFixed()}
75+
{format(tick.value)}
8676
</text>
8777
</g>
8878
)}
8979
</For>
9080
<text
91-
y={props.scale.range()[1]}
81+
y={props.scale.range()[yAnchor]}
9282
text-anchor="end"
93-
transform="translate(-65, 20) rotate(-90)"
83+
transform="translate(-45, 0) rotate(-90)"
9484
>
9585
{props.label}
9686
</text>
9787
</g>
9888
);
9989
};
90+
91+
/**
92+
* Calculate a "nice" step size by rounding up to the nearest power of 10
93+
* Snap the min and max to the nearest multiple of step
94+
*/
95+
export function getNiceAxisLimits(data: number[]): [number, number] {
96+
const max = Math.max(...data);
97+
const min = Math.min(...data);
98+
const range = max - min;
99+
const step = 10 ** Math.floor(Math.log10(range));
100+
101+
const niceMin = Math.floor(min / step) * step;
102+
const niceMax = Math.ceil(max / step) * step;
103+
104+
return [niceMin, niceMax];
105+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export interface ChartData<T> {
2+
label: string;
3+
color: string;
4+
linestyle: string;
5+
data: T[];
6+
}
7+
8+
// TODO: would be nice to create a chartContainer/context that manages logic like
9+
// width/height/margins etc. that should be consistent across different plots.

0 commit comments

Comments
 (0)