diff --git a/apps/class-solid/src/components/Analysis.tsx b/apps/class-solid/src/components/Analysis.tsx
index 8ac6f291..528d805f 100644
--- a/apps/class-solid/src/components/Analysis.tsx
+++ b/apps/class-solid/src/components/Analysis.tsx
@@ -1,11 +1,36 @@
-import { For, Match, Show, Switch, createMemo, createUniqueId } from "solid-js";
+import { BmiClass } from "@classmodel/class/bmi";
+import {
+ type Accessor,
+ For,
+ Match,
+ type Setter,
+ Show,
+ Switch,
+ createMemo,
+ createSignal,
+ createUniqueId,
+} from "solid-js";
import { getThermodynamicProfiles, getVerticalProfiles } from "~/lib/profiles";
-import { type Analysis, deleteAnalysis, experiments } from "~/lib/store";
+import {
+ type Analysis,
+ type ProfilesAnalysis,
+ type TimeseriesAnalysis,
+ deleteAnalysis,
+ experiments,
+ updateAnalysis,
+} from "~/lib/store";
import { MdiCog, MdiContentCopy, MdiDelete, MdiDownload } from "./icons";
import LinePlot from "./plots/LinePlot";
import { SkewTPlot } from "./plots/skewTlogP";
import { Button } from "./ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "./ui/select";
/** https://github.com/d3/d3-scale-chromatic/blob/main/src/categorical/Tableau10.js */
const colors = [
@@ -27,7 +52,11 @@ const linestyles = ["none", "5,5", "10,10", "15,5,5,5", "20,10,5,5,5,10"];
/** Very rudimentary plot showing time series of each experiment globally available
* It only works if the time axes are equal
*/
-export function TimeSeriesPlot() {
+export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
+ const xVariableOptions = ["t"]; // TODO: separate plot types for timeseries and x-vs-y? Use time axis?
+ // TODO: add nice description from config as title and dropdown option for the variable picker.
+ const yVariableOptions = new BmiClass().get_output_var_names();
+
const chartData = createMemo(() => {
return experiments
.filter((e) => e.running === false) // Skip running experiments
@@ -41,9 +70,13 @@ export function TimeSeriesPlot() {
color: colors[(j + 1) % 10],
linestyle: linestyles[i % 5],
data:
- perm.output?.t.map((tVal, i) => ({
- x: tVal,
- y: perm.output?.h[i] || Number.NaN,
+ perm.output?.t.map((tVal, ti) => ({
+ x: perm.output
+ ? perm.output[analysis.xVariable][ti]
+ : Number.NaN,
+ y: perm.output
+ ? perm.output[analysis.yVariable][ti]
+ : Number.NaN,
})) || [],
};
});
@@ -53,9 +86,13 @@ export function TimeSeriesPlot() {
color: colors[0],
linestyle: linestyles[i],
data:
- experimentOutput?.t.map((tVal, i) => ({
- x: tVal,
- y: experimentOutput?.h[i] || Number.NaN,
+ experimentOutput?.t.map((tVal, ti) => ({
+ x: experimentOutput
+ ? experimentOutput[analysis.xVariable][ti]
+ : Number.NaN,
+ y: experimentOutput
+ ? experimentOutput[analysis.yVariable][ti]
+ : Number.NaN,
})) || [],
},
...permutationRuns,
@@ -64,17 +101,50 @@ export function TimeSeriesPlot() {
});
return (
-
+ <>
+ {/* TODO: get label for yVariable from model config */}
+ "Time [s]"}
+ ylabel={() => analysis.yVariable}
+ />
+
+
analysis.xVariable}
+ setValue={(v) => updateAnalysis(analysis, { xVariable: v })}
+ options={xVariableOptions}
+ label="x-axis"
+ />
+ analysis.yVariable}
+ setValue={(v) => updateAnalysis(analysis, { yVariable: v })}
+ options={yVariableOptions}
+ label="y-axis"
+ />
+
+ >
);
}
-export function VerticalProfilePlot() {
- const variable = "theta";
- const time = -1;
+export function VerticalProfilePlot({
+ analysis,
+}: { analysis: ProfilesAnalysis }) {
+ const [variable, setVariable] = createSignal("Potential temperature [K]");
+
+ // TODO also check time of permutations.
+ const timeOptions = experiments
+ .filter((e) => e.running === false)
+ .flatMap((e) => (e.reference.output ? e.reference.output.t : []));
+ const variableOptions = {
+ "Potential temperature [K]": "theta",
+ "Specific humidity [kg/kg]": "q",
+ };
+ const classVariable = () =>
+ variableOptions[analysis.variable as keyof typeof variableOptions];
+
+ // TODO: refactor this? We could have a function that creates shared ChartData
+ // props (linestyle, color, label) generic for each plot type, and custom data
+ // formatting as required by specific chart
const profileData = createMemo(() => {
return experiments
.filter((e) => e.running === false) // Skip running experiments
@@ -86,7 +156,12 @@ export function VerticalProfilePlot() {
color: colors[(j + 1) % 10],
linestyle: linestyles[i % 5],
label: `${e.name}/${p.name}`,
- data: getVerticalProfiles(p.output, p.config, variable, time),
+ data: getVerticalProfiles(
+ p.output,
+ p.config,
+ classVariable(),
+ analysis.time,
+ ),
};
});
@@ -103,8 +178,8 @@ export function VerticalProfilePlot() {
dtheta: [],
},
e.reference.config,
- variable,
- time,
+ classVariable(),
+ analysis.time,
),
},
...permutations,
@@ -112,11 +187,50 @@ export function VerticalProfilePlot() {
});
});
return (
-
+ <>
+ "Height [m]"}
+ />
+ analysis.variable}
+ setValue={(v) => updateAnalysis(analysis, { variable: v })}
+ options={Object.keys(variableOptions)}
+ label="variable: "
+ />
+ >
+ );
+}
+
+type PickerProps = {
+ value: Accessor;
+ setValue: Setter;
+ options: string[];
+ label?: string;
+};
+
+function Picker(props: PickerProps) {
+ return (
+
+
{props.label}
+
+
);
}
@@ -228,10 +342,10 @@ export function AnalysisCard(analysis: Analysis) {
-
+
-
+
diff --git a/apps/class-solid/src/components/plots/Axes.tsx b/apps/class-solid/src/components/plots/Axes.tsx
index 5a2c39e3..cc354f21 100644
--- a/apps/class-solid/src/components/plots/Axes.tsx
+++ b/apps/class-solid/src/components/plots/Axes.tsx
@@ -1,7 +1,7 @@
// Code generated by AI and checked/modified for correctness
import * as d3 from "d3";
-import { For } from "solid-js";
+import { For, createEffect } from "solid-js";
import { useChartContext } from "./ChartContainer";
type AxisProps = {
@@ -14,25 +14,22 @@ type AxisProps = {
export const AxisBottom = (props: AxisProps) => {
const [chart, updateChart] = useChartContext();
- props.domain && chart.scaleX.domain(props.domain());
+ createEffect(() => {
+ props.domain && updateChart("scalePropsX", { domain: props.domain() });
+ props.type && updateChart("scalePropsX", { type: props.type });
+ });
- if (props.type === "log") {
- const range = chart.scaleX.range();
- const domain = chart.scaleX.range();
- updateChart("scaleX", d3.scaleLog().domain(domain).range(range));
- }
-
- const format = props.tickFormat ? props.tickFormat : d3.format(".3g");
- const ticks = props.tickValues || generateTicks(chart.scaleX.domain());
+ const format = () => (props.tickFormat ? props.tickFormat : d3.format(".3g"));
+ const ticks = () => props.tickValues || generateTicks(chart.scaleX.domain());
return (
-
+
{(tick) => (
- {format(tick)}
+ {format()(tick)}
)}
@@ -46,16 +43,13 @@ export const AxisBottom = (props: AxisProps) => {
export const AxisLeft = (props: AxisProps) => {
const [chart, updateChart] = useChartContext();
- props.domain && chart.scaleY.domain(props.domain());
-
- if (props.type === "log") {
- const range = chart.scaleY.range();
- const domain = chart.scaleY.domain();
- updateChart("scaleY", () => d3.scaleLog().range(range).domain(domain));
- }
+ createEffect(() => {
+ props.domain && updateChart("scalePropsY", { domain: props.domain() });
+ props.type && updateChart("scalePropsY", { type: props.type });
+ });
- const ticks = props.tickValues || generateTicks(chart.scaleY.domain());
- const format = props.tickFormat ? props.tickFormat : d3.format(".0f");
+ const ticks = () => props.tickValues || generateTicks(chart.scaleY.domain());
+ const format = () => (props.tickFormat ? props.tickFormat : d3.format(".0f"));
return (
{
y2={chart.scaleY.range()[1]}
stroke="currentColor"
/>
-
+
{(tick) => (
- {format(tick)}
+ {format()(tick)}
)}
diff --git a/apps/class-solid/src/components/plots/ChartContainer.tsx b/apps/class-solid/src/components/plots/ChartContainer.tsx
index 724a9400..eddea6a7 100644
--- a/apps/class-solid/src/components/plots/ChartContainer.tsx
+++ b/apps/class-solid/src/components/plots/ChartContainer.tsx
@@ -1,16 +1,32 @@
import * as d3 from "d3";
import type { JSX } from "solid-js";
-import { createContext, useContext } from "solid-js";
+import { createContext, createEffect, useContext } from "solid-js";
import { type SetStoreFunction, createStore } from "solid-js/store";
+type SupportedScaleTypes =
+ | d3.ScaleLinear
+ | d3.ScaleLogarithmic;
+const supportedScales = {
+ linear: d3.scaleLinear,
+ log: d3.scaleLog,
+};
+
+type ScaleProps = {
+ domain: [number, number];
+ range: [number, number];
+ type: keyof typeof supportedScales;
+};
+
interface Chart {
width: number;
height: number;
margin: [number, number, number, number];
innerWidth: number;
innerHeight: number;
- scaleX: d3.ScaleLinear | d3.ScaleLogarithmic;
- scaleY: d3.ScaleLinear | d3.ScaleLogarithmic;
+ scalePropsX: ScaleProps;
+ scalePropsY: ScaleProps;
+ scaleX: SupportedScaleTypes;
+ scaleY: SupportedScaleTypes;
}
type SetChart = SetStoreFunction;
const ChartContext = createContext<[Chart, SetChart]>();
@@ -28,14 +44,34 @@ export function ChartContainer(props: {
const [marginTop, marginRight, marginBottom, marginLeft] = margin;
const innerHeight = height - marginTop - marginBottom;
const innerWidth = width - marginRight - marginLeft;
+ const initialScale = d3.scaleLinear();
const [chart, updateChart] = createStore({
width,
height,
margin,
innerHeight,
innerWidth,
- scaleX: d3.scaleLinear().range([0, innerWidth]),
- scaleY: d3.scaleLinear().range([innerHeight, 0]),
+ scalePropsX: { type: "linear", domain: [0, 1], range: [0, innerWidth] },
+ scalePropsY: { type: "linear", domain: [0, 1], range: [innerHeight, 0] },
+ scaleX: initialScale,
+ scaleY: initialScale,
+ });
+ createEffect(() => {
+ // Update scaleXInstance when scaleX props change
+ const scaleX = supportedScales[chart.scalePropsX.type]()
+ .range(chart.scalePropsX.range)
+ .domain(chart.scalePropsX.domain);
+ // .nice(); // TODO: could use this instead of getNiceAxisLimits
+ updateChart("scaleX", () => scaleX);
+ });
+
+ createEffect(() => {
+ // Update scaleYInstance when scaleY props change
+ const scaleY = supportedScales[chart.scalePropsY.type]()
+ .range(chart.scalePropsY.range)
+ .domain(chart.scalePropsY.domain);
+ // .nice();
+ updateChart("scaleY", () => scaleY);
});
return (
diff --git a/apps/class-solid/src/components/plots/LinePlot.tsx b/apps/class-solid/src/components/plots/LinePlot.tsx
index cb816298..012c1a27 100644
--- a/apps/class-solid/src/components/plots/LinePlot.tsx
+++ b/apps/class-solid/src/components/plots/LinePlot.tsx
@@ -37,7 +37,11 @@ export default function LinePlot({
data,
xlabel,
ylabel,
-}: { data: () => ChartData[]; xlabel?: string; ylabel?: string }) {
+}: {
+ data: () => ChartData[];
+ xlabel?: () => string;
+ ylabel?: () => string;
+}) {
const xLim = () =>
getNiceAxisLimits(data().flatMap((d) => d.data.flatMap((d) => d.x)));
const yLim = () =>
@@ -46,8 +50,8 @@ export default function LinePlot({
-
-
+
+
{(d) => Line(d)}
diff --git a/apps/class-solid/src/components/plots/skewTlogP.tsx b/apps/class-solid/src/components/plots/skewTlogP.tsx
index fe09debf..f6aca45e 100644
--- a/apps/class-solid/src/components/plots/skewTlogP.tsx
+++ b/apps/class-solid/src/components/plots/skewTlogP.tsx
@@ -29,8 +29,8 @@ function ClipPath() {
function SkewTGridLine(temperature: number) {
const [chart, updateChart] = useChartContext();
- const x = chart.scaleX;
- const y = chart.scaleY;
+ const x = (temp: number) => chart.scaleX(temp);
+ const y = (pres: number) => chart.scaleY(pres);
return (
chart.scaleX(temp);
+ const y = (pres: number) => chart.scaleY(pres);
return (
chart.scaleX(temp);
+ const y = (pres: number) => chart.scaleY(pres);
const dryline = d3
.line()
@@ -92,8 +92,8 @@ function Sounding(data: ChartData) {
const [hovered, setHovered] = createSignal(false);
// Scales and axes. Note the inverted domain for the y-scale: bigger is up!
- const x = chart.scaleX;
- const y = chart.scaleY;
+ const x = (temp: number) => chart.scaleX(temp);
+ const y = (pres: number) => chart.scaleY(pres);
const temperatureLine = d3
.line()
diff --git a/apps/class-solid/src/components/ui/select.tsx b/apps/class-solid/src/components/ui/select.tsx
new file mode 100644
index 00000000..8f59ad98
--- /dev/null
+++ b/apps/class-solid/src/components/ui/select.tsx
@@ -0,0 +1,124 @@
+import type { JSX, ValidComponent } from "solid-js";
+import { splitProps } from "solid-js";
+
+import type { PolymorphicProps } from "@kobalte/core/polymorphic";
+import * as SelectPrimitive from "@kobalte/core/select";
+
+import { cn } from "~/lib/utils";
+
+const Select = SelectPrimitive.Root;
+const SelectValue = SelectPrimitive.Value;
+const SelectHiddenSelect = SelectPrimitive.HiddenSelect;
+
+type SelectTriggerProps =
+ SelectPrimitive.SelectTriggerProps & {
+ class?: string | undefined;
+ children?: JSX.Element;
+ };
+
+const SelectTrigger = (
+ props: PolymorphicProps>,
+) => {
+ const [local, others] = splitProps(props as SelectTriggerProps, [
+ "class",
+ "children",
+ ]);
+ return (
+
+ {local.children}
+
+
+
+
+
+ );
+};
+
+type SelectContentProps =
+ SelectPrimitive.SelectContentProps & { class?: string | undefined };
+
+const SelectContent = (
+ props: PolymorphicProps>,
+) => {
+ const [local, others] = splitProps(props as SelectContentProps, ["class"]);
+ return (
+
+
+
+
+
+ );
+};
+
+type SelectItemProps =
+ SelectPrimitive.SelectItemProps & {
+ class?: string | undefined;
+ children?: JSX.Element;
+ };
+
+const SelectItem = (
+ props: PolymorphicProps>,
+) => {
+ const [local, others] = splitProps(props as SelectItemProps, [
+ "class",
+ "children",
+ ]);
+ return (
+
+
+ {/* biome-ignore lint/a11y/noSvgWithoutTitle: generated code */}
+
+
+ {local.children}
+
+ );
+};
+
+export {
+ Select,
+ SelectValue,
+ SelectHiddenSelect,
+ SelectTrigger,
+ SelectContent,
+ SelectItem,
+};
diff --git a/apps/class-solid/src/lib/profiles.ts b/apps/class-solid/src/lib/profiles.ts
index 6519c56c..2bc10fc1 100644
--- a/apps/class-solid/src/lib/profiles.ts
+++ b/apps/class-solid/src/lib/profiles.ts
@@ -6,7 +6,7 @@ import type { Point } from "~/components/plots/LinePlot";
export function getVerticalProfiles(
output: ClassOutput | undefined,
config: PartialConfig,
- variable: "theta" | "q" = "theta",
+ variable = "theta",
t = -1,
): Point[] {
// Guard against undefined output
diff --git a/apps/class-solid/src/lib/runner.ts b/apps/class-solid/src/lib/runner.ts
index dec59e69..be66133b 100644
--- a/apps/class-solid/src/lib/runner.ts
+++ b/apps/class-solid/src/lib/runner.ts
@@ -1,4 +1,4 @@
-import type { BmiClass } from "@classmodel/class/bmi";
+import { BmiClass } from "@classmodel/class/bmi";
import type { Config } from "@classmodel/class/config";
import type { ClassOutput } from "@classmodel/class/runner";
import { type PartialConfig, parse } from "@classmodel/class/validate";
@@ -15,7 +15,7 @@ export async function runClass(config: PartialConfig): Promise {
const model = await new AsyncBmiClass();
await model.initialize(parsedConfig);
const output = await model.run({
- var_names: ["h", "theta", "q", "dtheta", "dq"],
+ var_names: new BmiClass().get_output_var_names(),
});
return output;
} catch (error) {
diff --git a/apps/class-solid/src/lib/store.ts b/apps/class-solid/src/lib/store.ts
index 94709c11..c940dabf 100644
--- a/apps/class-solid/src/lib/store.ts
+++ b/apps/class-solid/src/lib/store.ts
@@ -80,8 +80,8 @@ export async function runExperiment(id: number) {
// If no analyis are set then add all of them
if (analyses.length === 0) {
- for (const key of Object.keys(analysisNames) as AnalysisType[]) {
- addAnalysis(key);
+ for (const name of analysisNames) {
+ addAnalysis(name);
}
}
}
@@ -260,33 +260,88 @@ export async function loadStateFromString(rawState: string): Promise {
await Promise.all(loadedExperiments.map((_, i) => runExperiment(i)));
}
-export const analysisNames = {
- profiles: "Vertical profiles",
- timeseries: "Timeseries",
- skewT: "Thermodynamic diagram",
- // finalheight: "Final height", // keep for development but not in production
-} as const;
-export type AnalysisType = keyof typeof analysisNames;
-
export interface Analysis {
- name: string;
- description: string;
id: string;
- experiments: Experiment[] | undefined;
- type: AnalysisType;
+ description: string;
+ type: string;
+ name: string;
}
-export function addAnalysis(type: AnalysisType) {
- const name = analysisNames[type];
+export type TimeseriesAnalysis = Analysis & {
+ xVariable: string;
+ yVariable: string;
+};
+
+export type ProfilesAnalysis = Analysis & {
+ variable: string;
+ time: number;
+};
+
+export type SkewTAnalysis = Analysis & {
+ time: number;
+};
+
+export type AnalysisType =
+ | TimeseriesAnalysis
+ | ProfilesAnalysis
+ | SkewTAnalysis;
+export const analysisNames = [
+ "Vertical profiles",
+ "Timeseries",
+ "Thermodynamic diagram",
+];
+
+export function addAnalysis(name: string) {
+ let newAnalysis: Analysis;
+
+ switch (name) {
+ case "Timeseries":
+ newAnalysis = {
+ id: createUniqueId(),
+ description: "",
+ type: "timeseries",
+ name: "Timeseries",
+ xVariable: "t",
+ yVariable: "h",
+ } as TimeseriesAnalysis;
+ break;
+ case "Vertical profiles":
+ newAnalysis = {
+ id: createUniqueId(),
+ description: "",
+ type: "profiles",
+ name: "Vertical profiles",
+ variable: "Potential temperature [K]",
+ time: -1,
+ } as ProfilesAnalysis;
+ break;
+ case "Thermodynamic diagram":
+ newAnalysis = {
+ id: createUniqueId(),
+ description: "",
+ type: "skewT",
+ name: "Thermodynamic diagram",
+ time: -1,
+ } as SkewTAnalysis;
+ break;
+ default:
+ throw new Error(`Unknown analysis type: ${name}`);
+ }
- setAnalyses(analyses.length, {
- name,
- id: createUniqueId(),
- experiments: experiments,
- type,
- });
+ setAnalyses(analyses.length, newAnalysis);
}
export function deleteAnalysis(analysis: Analysis) {
setAnalyses(analyses.filter((ana) => ana.id !== analysis.id));
}
+
+export function updateAnalysis(analysis: Analysis, newData: object) {
+ setAnalyses(
+ produce((analyses) => {
+ const currentAnalysis = analyses.find((a) => a.id === analysis.id);
+ if (currentAnalysis) {
+ Object.assign(currentAnalysis, newData);
+ }
+ }),
+ );
+}
diff --git a/apps/class-solid/src/routes/index.tsx b/apps/class-solid/src/routes/index.tsx
index 39089655..c94937da 100644
--- a/apps/class-solid/src/routes/index.tsx
+++ b/apps/class-solid/src/routes/index.tsx
@@ -16,12 +16,7 @@ import { Flex } from "~/components/ui/flex";
import { Toaster } from "~/components/ui/toast";
import { onPageLoad } from "~/lib/state";
-import {
- type AnalysisType,
- addAnalysis,
- analysisNames,
- experiments,
-} from "~/lib/store";
+import { addAnalysis, analysisNames, experiments } from "~/lib/store";
import { analyses } from "~/lib/store";
export default function Home() {
@@ -65,12 +60,10 @@ export default function Home() {
Add analysis
-
- {([key, value]) => (
- addAnalysis(key)}>
- {value}
+
+ {(name) => (
+ addAnalysis(name)}>
+ {name}
)}