Skip to content

Commit 4238bb4

Browse files
authored
Variable picker (#92)
1 parent 4337353 commit 4238bb4

File tree

10 files changed

+423
-103
lines changed

10 files changed

+423
-103
lines changed

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

Lines changed: 141 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,36 @@
1-
import { For, Match, Show, Switch, createMemo, createUniqueId } from "solid-js";
1+
import { BmiClass } from "@classmodel/class/bmi";
2+
import {
3+
type Accessor,
4+
For,
5+
Match,
6+
type Setter,
7+
Show,
8+
Switch,
9+
createMemo,
10+
createSignal,
11+
createUniqueId,
12+
} from "solid-js";
213
import { getThermodynamicProfiles, getVerticalProfiles } from "~/lib/profiles";
3-
import { type Analysis, deleteAnalysis, experiments } from "~/lib/store";
14+
import {
15+
type Analysis,
16+
type ProfilesAnalysis,
17+
type TimeseriesAnalysis,
18+
deleteAnalysis,
19+
experiments,
20+
updateAnalysis,
21+
} from "~/lib/store";
422
import { MdiCog, MdiContentCopy, MdiDelete, MdiDownload } from "./icons";
523
import LinePlot from "./plots/LinePlot";
624
import { SkewTPlot } from "./plots/skewTlogP";
725
import { Button } from "./ui/button";
826
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
27+
import {
28+
Select,
29+
SelectContent,
30+
SelectItem,
31+
SelectTrigger,
32+
SelectValue,
33+
} from "./ui/select";
934

1035
/** https://github.com/d3/d3-scale-chromatic/blob/main/src/categorical/Tableau10.js */
1136
const colors = [
@@ -27,7 +52,11 @@ const linestyles = ["none", "5,5", "10,10", "15,5,5,5", "20,10,5,5,5,10"];
2752
/** Very rudimentary plot showing time series of each experiment globally available
2853
* It only works if the time axes are equal
2954
*/
30-
export function TimeSeriesPlot() {
55+
export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
56+
const xVariableOptions = ["t"]; // TODO: separate plot types for timeseries and x-vs-y? Use time axis?
57+
// TODO: add nice description from config as title and dropdown option for the variable picker.
58+
const yVariableOptions = new BmiClass().get_output_var_names();
59+
3160
const chartData = createMemo(() => {
3261
return experiments
3362
.filter((e) => e.running === false) // Skip running experiments
@@ -41,9 +70,13 @@ export function TimeSeriesPlot() {
4170
color: colors[(j + 1) % 10],
4271
linestyle: linestyles[i % 5],
4372
data:
44-
perm.output?.t.map((tVal, i) => ({
45-
x: tVal,
46-
y: perm.output?.h[i] || Number.NaN,
73+
perm.output?.t.map((tVal, ti) => ({
74+
x: perm.output
75+
? perm.output[analysis.xVariable][ti]
76+
: Number.NaN,
77+
y: perm.output
78+
? perm.output[analysis.yVariable][ti]
79+
: Number.NaN,
4780
})) || [],
4881
};
4982
});
@@ -53,9 +86,13 @@ export function TimeSeriesPlot() {
5386
color: colors[0],
5487
linestyle: linestyles[i],
5588
data:
56-
experimentOutput?.t.map((tVal, i) => ({
57-
x: tVal,
58-
y: experimentOutput?.h[i] || Number.NaN,
89+
experimentOutput?.t.map((tVal, ti) => ({
90+
x: experimentOutput
91+
? experimentOutput[analysis.xVariable][ti]
92+
: Number.NaN,
93+
y: experimentOutput
94+
? experimentOutput[analysis.yVariable][ti]
95+
: Number.NaN,
5996
})) || [],
6097
},
6198
...permutationRuns,
@@ -64,17 +101,50 @@ export function TimeSeriesPlot() {
64101
});
65102

66103
return (
67-
<LinePlot
68-
data={chartData}
69-
xlabel="Time [s]"
70-
ylabel="Mixed-layer height [m]"
71-
/>
104+
<>
105+
{/* TODO: get label for yVariable from model config */}
106+
<LinePlot
107+
data={chartData}
108+
xlabel={() => "Time [s]"}
109+
ylabel={() => analysis.yVariable}
110+
/>
111+
<div class="flex justify-around">
112+
<Picker
113+
value={() => analysis.xVariable}
114+
setValue={(v) => updateAnalysis(analysis, { xVariable: v })}
115+
options={xVariableOptions}
116+
label="x-axis"
117+
/>
118+
<Picker
119+
value={() => analysis.yVariable}
120+
setValue={(v) => updateAnalysis(analysis, { yVariable: v })}
121+
options={yVariableOptions}
122+
label="y-axis"
123+
/>
124+
</div>
125+
</>
72126
);
73127
}
74128

75-
export function VerticalProfilePlot() {
76-
const variable = "theta";
77-
const time = -1;
129+
export function VerticalProfilePlot({
130+
analysis,
131+
}: { analysis: ProfilesAnalysis }) {
132+
const [variable, setVariable] = createSignal("Potential temperature [K]");
133+
134+
// TODO also check time of permutations.
135+
const timeOptions = experiments
136+
.filter((e) => e.running === false)
137+
.flatMap((e) => (e.reference.output ? e.reference.output.t : []));
138+
const variableOptions = {
139+
"Potential temperature [K]": "theta",
140+
"Specific humidity [kg/kg]": "q",
141+
};
142+
const classVariable = () =>
143+
variableOptions[analysis.variable as keyof typeof variableOptions];
144+
145+
// TODO: refactor this? We could have a function that creates shared ChartData
146+
// props (linestyle, color, label) generic for each plot type, and custom data
147+
// formatting as required by specific chart
78148
const profileData = createMemo(() => {
79149
return experiments
80150
.filter((e) => e.running === false) // Skip running experiments
@@ -86,7 +156,12 @@ export function VerticalProfilePlot() {
86156
color: colors[(j + 1) % 10],
87157
linestyle: linestyles[i % 5],
88158
label: `${e.name}/${p.name}`,
89-
data: getVerticalProfiles(p.output, p.config, variable, time),
159+
data: getVerticalProfiles(
160+
p.output,
161+
p.config,
162+
classVariable(),
163+
analysis.time,
164+
),
90165
};
91166
});
92167

@@ -103,20 +178,59 @@ export function VerticalProfilePlot() {
103178
dtheta: [],
104179
},
105180
e.reference.config,
106-
variable,
107-
time,
181+
classVariable(),
182+
analysis.time,
108183
),
109184
},
110185
...permutations,
111186
];
112187
});
113188
});
114189
return (
115-
<LinePlot
116-
data={profileData}
117-
xlabel="Potential temperature [K]"
118-
ylabel="Height [m]"
119-
/>
190+
<>
191+
<LinePlot
192+
data={profileData}
193+
xlabel={variable}
194+
ylabel={() => "Height [m]"}
195+
/>
196+
<Picker
197+
value={() => analysis.variable}
198+
setValue={(v) => updateAnalysis(analysis, { variable: v })}
199+
options={Object.keys(variableOptions)}
200+
label="variable: "
201+
/>
202+
</>
203+
);
204+
}
205+
206+
type PickerProps = {
207+
value: Accessor<string>;
208+
setValue: Setter<string>;
209+
options: string[];
210+
label?: string;
211+
};
212+
213+
function Picker(props: PickerProps) {
214+
return (
215+
<div class="flex items-center gap-2">
216+
<p>{props.label}</p>
217+
<Select
218+
class="whitespace-nowrap"
219+
value={props.value()}
220+
disallowEmptySelection={true}
221+
onChange={props.setValue}
222+
options={props.options}
223+
placeholder="Select value..."
224+
itemComponent={(props) => (
225+
<SelectItem item={props.item}>{props.item.rawValue}</SelectItem>
226+
)}
227+
>
228+
<SelectTrigger aria-label="Variable" class="min-w-[100px]">
229+
<SelectValue<string>>{(state) => state.selectedOption()}</SelectValue>
230+
</SelectTrigger>
231+
<SelectContent />
232+
</Select>
233+
</div>
120234
);
121235
}
122236

@@ -228,10 +342,10 @@ export function AnalysisCard(analysis: Analysis) {
228342
<FinalHeights />
229343
</Match>
230344
<Match when={analysis.type === "timeseries"}>
231-
<TimeSeriesPlot />
345+
<TimeSeriesPlot analysis={analysis as TimeseriesAnalysis} />
232346
</Match>
233347
<Match when={analysis.type === "profiles"}>
234-
<VerticalProfilePlot />
348+
<VerticalProfilePlot analysis={analysis as ProfilesAnalysis} />
235349
</Match>
236350
<Match when={analysis.type === "skewT"}>
237351
<ThermodynamicPlot />

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

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Code generated by AI and checked/modified for correctness
22

33
import * as d3 from "d3";
4-
import { For } from "solid-js";
4+
import { For, createEffect } from "solid-js";
55
import { useChartContext } from "./ChartContainer";
66

77
type AxisProps = {
@@ -14,25 +14,22 @@ type AxisProps = {
1414

1515
export const AxisBottom = (props: AxisProps) => {
1616
const [chart, updateChart] = useChartContext();
17-
props.domain && chart.scaleX.domain(props.domain());
17+
createEffect(() => {
18+
props.domain && updateChart("scalePropsX", { domain: props.domain() });
19+
props.type && updateChart("scalePropsX", { type: props.type });
20+
});
1821

19-
if (props.type === "log") {
20-
const range = chart.scaleX.range();
21-
const domain = chart.scaleX.range();
22-
updateChart("scaleX", d3.scaleLog().domain(domain).range(range));
23-
}
24-
25-
const format = props.tickFormat ? props.tickFormat : d3.format(".3g");
26-
const ticks = props.tickValues || generateTicks(chart.scaleX.domain());
22+
const format = () => (props.tickFormat ? props.tickFormat : d3.format(".3g"));
23+
const ticks = () => props.tickValues || generateTicks(chart.scaleX.domain());
2724
return (
2825
<g transform={`translate(0,${chart.innerHeight - 0.5})`}>
2926
<line x1="0" x2={chart.innerWidth} y1="0" y2="0" stroke="currentColor" />
30-
<For each={ticks}>
27+
<For each={ticks()}>
3128
{(tick) => (
3229
<g transform={`translate(${chart.scaleX(tick)}, 0)`}>
3330
<line y2="6" stroke="currentColor" />
3431
<text y="9" dy="0.71em" text-anchor="middle">
35-
{format(tick)}
32+
{format()(tick)}
3633
</text>
3734
</g>
3835
)}
@@ -46,16 +43,13 @@ export const AxisBottom = (props: AxisProps) => {
4643

4744
export const AxisLeft = (props: AxisProps) => {
4845
const [chart, updateChart] = useChartContext();
49-
props.domain && chart.scaleY.domain(props.domain());
50-
51-
if (props.type === "log") {
52-
const range = chart.scaleY.range();
53-
const domain = chart.scaleY.domain();
54-
updateChart("scaleY", () => d3.scaleLog().range(range).domain(domain));
55-
}
46+
createEffect(() => {
47+
props.domain && updateChart("scalePropsY", { domain: props.domain() });
48+
props.type && updateChart("scalePropsY", { type: props.type });
49+
});
5650

57-
const ticks = props.tickValues || generateTicks(chart.scaleY.domain());
58-
const format = props.tickFormat ? props.tickFormat : d3.format(".0f");
51+
const ticks = () => props.tickValues || generateTicks(chart.scaleY.domain());
52+
const format = () => (props.tickFormat ? props.tickFormat : d3.format(".0f"));
5953
return (
6054
<g transform="translate(-0.5,0)">
6155
<line
@@ -65,12 +59,12 @@ export const AxisLeft = (props: AxisProps) => {
6559
y2={chart.scaleY.range()[1]}
6660
stroke="currentColor"
6761
/>
68-
<For each={ticks}>
62+
<For each={ticks()}>
6963
{(tick) => (
7064
<g transform={`translate(0, ${chart.scaleY(tick)})`}>
7165
<line x2="-6" stroke="currentColor" />
7266
<text x="-9" dy="0.32em" text-anchor="end">
73-
{format(tick)}
67+
{format()(tick)}
7468
</text>
7569
</g>
7670
)}

apps/class-solid/src/components/plots/ChartContainer.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
11
import * as d3 from "d3";
22
import type { JSX } from "solid-js";
3-
import { createContext, useContext } from "solid-js";
3+
import { createContext, createEffect, useContext } from "solid-js";
44
import { type SetStoreFunction, createStore } from "solid-js/store";
55

6+
type SupportedScaleTypes =
7+
| d3.ScaleLinear<number, number, never>
8+
| d3.ScaleLogarithmic<number, number, never>;
9+
const supportedScales = {
10+
linear: d3.scaleLinear<number, number, never>,
11+
log: d3.scaleLog<number, number, never>,
12+
};
13+
14+
type ScaleProps = {
15+
domain: [number, number];
16+
range: [number, number];
17+
type: keyof typeof supportedScales;
18+
};
19+
620
interface Chart {
721
width: number;
822
height: number;
923
margin: [number, number, number, number];
1024
innerWidth: number;
1125
innerHeight: number;
12-
scaleX: d3.ScaleLinear<number, number> | d3.ScaleLogarithmic<number, number>;
13-
scaleY: d3.ScaleLinear<number, number> | d3.ScaleLogarithmic<number, number>;
26+
scalePropsX: ScaleProps;
27+
scalePropsY: ScaleProps;
28+
scaleX: SupportedScaleTypes;
29+
scaleY: SupportedScaleTypes;
1430
}
1531
type SetChart = SetStoreFunction<Chart>;
1632
const ChartContext = createContext<[Chart, SetChart]>();
@@ -28,14 +44,34 @@ export function ChartContainer(props: {
2844
const [marginTop, marginRight, marginBottom, marginLeft] = margin;
2945
const innerHeight = height - marginTop - marginBottom;
3046
const innerWidth = width - marginRight - marginLeft;
47+
const initialScale = d3.scaleLinear<number, number, never>();
3148
const [chart, updateChart] = createStore<Chart>({
3249
width,
3350
height,
3451
margin,
3552
innerHeight,
3653
innerWidth,
37-
scaleX: d3.scaleLinear().range([0, innerWidth]),
38-
scaleY: d3.scaleLinear().range([innerHeight, 0]),
54+
scalePropsX: { type: "linear", domain: [0, 1], range: [0, innerWidth] },
55+
scalePropsY: { type: "linear", domain: [0, 1], range: [innerHeight, 0] },
56+
scaleX: initialScale,
57+
scaleY: initialScale,
58+
});
59+
createEffect(() => {
60+
// Update scaleXInstance when scaleX props change
61+
const scaleX = supportedScales[chart.scalePropsX.type]()
62+
.range(chart.scalePropsX.range)
63+
.domain(chart.scalePropsX.domain);
64+
// .nice(); // TODO: could use this instead of getNiceAxisLimits
65+
updateChart("scaleX", () => scaleX);
66+
});
67+
68+
createEffect(() => {
69+
// Update scaleYInstance when scaleY props change
70+
const scaleY = supportedScales[chart.scalePropsY.type]()
71+
.range(chart.scalePropsY.range)
72+
.domain(chart.scalePropsY.domain);
73+
// .nice();
74+
updateChart("scaleY", () => scaleY);
3975
});
4076
return (
4177
<ChartContext.Provider value={[chart, updateChart]}>

0 commit comments

Comments
 (0)