Skip to content

Embedded json schema #139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
15b1562
Implement basic zoom; TODO: fix angle in skewT
Peter9192 Apr 11, 2025
4b9625c
Add logarithmic zoom for skew-T diagram; TODO: T calculations should …
Peter9192 Apr 11, 2025
89f028b
Fix skew-T lines responding to original extent instead of actual; now…
Peter9192 Apr 11, 2025
8e29a5a
Add panning effect, but it is stroboscopic and doesn't work for skewT…
Peter9192 Apr 11, 2025
53f2cb1
combine side-effects for both axes in a single callback
Peter9192 Apr 11, 2025
23f4dae
Merge remote-tracking branch 'origin/main' into pan-zoom
Peter9192 Apr 18, 2025
ca46f1f
Remove animationframe
Peter9192 Apr 18, 2025
8be9635
Use produce to update both scales in a single call
Peter9192 Apr 18, 2025
859557b
Don't update panstart; this fixes the jittering
Peter9192 Apr 18, 2025
9c32384
Also work in log space
Peter9192 Apr 18, 2025
d3fae43
Make consistent for x-direction
Peter9192 Apr 18, 2025
ea696fc
zoom towards cursor
Peter9192 Apr 18, 2025
b65f516
Add reset plot button
Peter9192 Apr 18, 2025
69e93d4
Thinner lines
Peter9192 Apr 18, 2025
1ca0f68
Higher resolution plot
Peter9192 Apr 18, 2025
8953c43
Round time to steps of 10 minutes
Peter9192 Apr 18, 2025
0d72ef1
Use runClass from package, skipping BMI altogher + rich metadata for …
Peter9192 Apr 18, 2025
d072e56
Fix hanging issue: wrap only once...
Peter9192 Apr 18, 2025
cd10da8
Make sure initial state is included in output
Peter9192 Apr 18, 2025
f085554
Reset pan/zoom when variable changes
Peter9192 Apr 18, 2025
202d144
formatting
Peter9192 Apr 18, 2025
1e1399a
Use output metadata in plot labels and variable pickers
Peter9192 Apr 18, 2025
eeb62df
ditch BMI
Peter9192 Apr 18, 2025
8e9dddf
Fix xlabel in timeseries plot; fix axes extent for non-time on x-axis
Peter9192 Apr 22, 2025
638ccfc
Remove config.JSON everywhere; only keep the embedded version
Peter9192 Apr 22, 2025
5faf60c
Change units to use superscript
Peter9192 Apr 22, 2025
8bb3259
Add `pnpx @classmodel/class schema` cli subcommand
sverhoeven Apr 24, 2025
0951d69
Merge remote-tracking branch 'origin/main' into embedded-json-schema
Peter9192 Apr 24, 2025
3904050
Update lockfile
Peter9192 Apr 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,8 @@ by [Kobalte](https://kobalte.dev/docs/core/overview/introduction) and
application and tweaked further as seen fit. It can also do charts, using
[chart.js](https://www.chartjs.org/), though we might deviate from that later.

To expose the model in a standard way we use the [Basic Model Interface (BMI)](https://bmi.readthedocs.io/).

To prevent the user interface from getting blocked by running the model we use a [Web worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker) to run the computation in a background task/thread.
A Web worker uses messages passing to communicate, this does not fit with the Basic Model Interface so we use [comlink](https://github.com/GoogleChromeLabs/comlink) to wrap the Web Worker in a BMI class.
We use [comlink](https://github.com/GoogleChromeLabs/comlink) to wrap the Web Worker so it behaves the same as if the runner was used directly inside the main thread.

To format and lint the code, we use [biome](https://biomejs.dev/) as it combines eslint, prettier in one package.

Expand Down
2 changes: 1 addition & 1 deletion apps/class-solid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ This allows you to trigger tests from the [playwright ui](https://playwright.dev
An experiment can get started from a preset.

The presets are stored in the `src/lib/presets/` directory.
The format is JSON with content adhering to the [JSON schema](https://github.com/classmodel/class-web/blob/main/packages/class/src/config.json).
The format is JSON with content adhering to the [JSON schema](https://github.com/classmodel/class-web/blob/main/packages/class/src/config.ts).

The `src/lib/presets.ts` is used as an index of presets.
If you add a preset the `src/lib/presets.ts` file needs to be updated.
Expand Down
87 changes: 65 additions & 22 deletions apps/class-solid/src/components/Analysis.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BmiClass } from "@classmodel/class/bmi";
import type { Config } from "@classmodel/class/config";
import type { ClassOutput } from "@classmodel/class/runner";
import { type ClassOutput, outputVariables } from "@classmodel/class/runner";
import * as d3 from "d3";
import { saveAs } from "file-saver";
import { toBlob } from "html-to-image";
import {
Expand All @@ -12,6 +12,7 @@ import {
Switch,
createEffect,
createMemo,
createSignal,
createUniqueId,
} from "solid-js";
import { createStore } from "solid-js/store";
Expand All @@ -31,7 +32,7 @@ import {
experiments,
updateAnalysis,
} from "~/lib/store";
import { MdiCamera, MdiDelete } from "./icons";
import { MaterialSymbolsLightResetImage, MdiCamera, MdiDelete } from "./icons";
import { AxisBottom, AxisLeft, getNiceAxisLimits } from "./plots/Axes";
import { Chart, ChartContainer } from "./plots/ChartContainer";
import { Legend } from "./plots/Legend";
Expand Down Expand Up @@ -114,9 +115,15 @@ const uniqueTimes = () => [...new Set(_allTimes())].sort((a, b) => a - b);

// TODO: could memoize all reactive elements here, would it make a difference?
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 symbols = Object.fromEntries(
outputVariables.map((v) => [v.key, v.symbol]),
);
const getKey = Object.fromEntries(
outputVariables.map((v) => [v.symbol, v.key]),
);
const labels = Object.fromEntries(
outputVariables.map((v) => [v.key, `${v.symbol} [${v.unit}]`]),
);

const allX = () =>
flatExperiments().flatMap((e) =>
Expand All @@ -127,7 +134,8 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
e.output ? e.output[analysis.yVariable] : [],
);

const xLim = () => getNiceAxisLimits(allX());
const granularity = () => (analysis.xVariable === "t" ? 600 : undefined);
const xLim = () => getNiceAxisLimits(allX(), 0, granularity());
const yLim = () => getNiceAxisLimits(allY());

const chartData = () =>
Expand Down Expand Up @@ -157,14 +165,34 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
setToggles(label, value);
}

const setXVar = (symbol: string) => {
updateAnalysis(analysis, { xVariable: getKey[symbol] });
setResetPlot(analysis.id);
};

const setYVar = (symbol: string) => {
updateAnalysis(analysis, { yVariable: getKey[symbol] });
setResetPlot(analysis.id);
};

const formatX = () =>
analysis.xVariable === "t" ? formatSeconds : d3.format(".4");
const formatY = () =>
analysis.yVariable === "t" ? formatSeconds : d3.format(".4");

return (
<>
{/* TODO: get label for yVariable from model config */}
<ChartContainer>
<Legend entries={chartData} toggles={toggles} onChange={toggleLine} />
<Chart title="Timeseries plot" formatX={formatSeconds}>
<AxisBottom domain={xLim} label="Time [s]" />
<AxisLeft domain={yLim} label={analysis.yVariable} />
<Chart
id={analysis.id}
title="Timeseries plot"
formatX={formatX}
formatY={formatY}
>
<AxisBottom domain={xLim} label={labels[analysis.xVariable]} />
<AxisLeft domain={yLim} label={labels[analysis.yVariable]} />
<For each={chartData()}>
{(d) => (
<Show when={toggles[d.label]}>
Expand All @@ -176,15 +204,15 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
</ChartContainer>
<div class="flex justify-around">
<Picker
value={() => analysis.xVariable}
setValue={(v) => updateAnalysis(analysis, { xVariable: v })}
options={xVariableOptions}
value={() => symbols[analysis.xVariable]}
setValue={(v) => setXVar(v)}
options={Object.values(symbols)}
label="x-axis"
/>
<Picker
value={() => analysis.yVariable}
setValue={(v) => updateAnalysis(analysis, { yVariable: v })}
options={yVariableOptions}
value={() => symbols[analysis.yVariable]}
setValue={(v) => setYVar(v)}
options={Object.values(symbols)}
label="y-axis"
/>
</div>
Expand Down Expand Up @@ -260,6 +288,11 @@ export function VerticalProfilePlot({
setToggles(label, value);
}

function changeVar(v: string) {
updateAnalysis(analysis, { variable: v });
setResetPlot(analysis.id);
}

return (
<>
<div class="flex flex-col gap-2">
Expand All @@ -269,7 +302,7 @@ export function VerticalProfilePlot({
toggles={toggles}
onChange={toggleLine}
/>
<Chart title="Vertical profile plot">
<Chart id={analysis.id} title="Vertical profile plot">
<AxisBottom domain={xLim} label={analysis.variable} />
<AxisLeft domain={yLim} label="Height[m]" />
<For each={profileData()}>
Expand All @@ -290,7 +323,7 @@ export function VerticalProfilePlot({
</ChartContainer>
<Picker
value={() => analysis.variable}
setValue={(v) => updateAnalysis(analysis, { variable: v })}
setValue={(v) => changeVar(v)}
options={Object.keys(variableOptions)}
label="variable: "
/>
Expand All @@ -306,7 +339,7 @@ export function VerticalProfilePlot({

type PickerProps = {
value: Accessor<string>;
setValue: Setter<string>;
setValue: (value: string) => void;
options: string[];
label?: string;
};
Expand Down Expand Up @@ -394,7 +427,10 @@ export function ThermodynamicPlot({ analysis }: { analysis: SkewTAnalysis }) {

return (
<>
<SkewTPlot data={() => [...skewTData(), ...observations()]} />
<SkewTPlot
id={analysis.id}
data={() => [...skewTData(), ...observations()]}
/>
{TimeSlider(
() => analysis.time,
uniqueTimes,
Expand All @@ -414,8 +450,9 @@ async function takeScreenshot(event: MouseEvent, analyse: Analysis) {
return;
}

// TODO Make screenshot bigger than the original?
const scale = 1;
// Make screenshot bigger than the original
const scale = 10;

// Can not use toSvg as legend is written in HTML
// generated svg document contains foreignObject with html tag
// which can only be rendered using web browser, not Inkscape or PowerPoint
Expand Down Expand Up @@ -443,6 +480,9 @@ async function takeScreenshot(event: MouseEvent, analyse: Analysis) {
saveAs(file);
}

// Emit a signal when plot reset button is pressed
export const [resetPlot, setResetPlot] = createSignal("", { equals: false });

export function AnalysisCard(analysis: Analysis) {
const id = createUniqueId();
return (
Expand All @@ -451,6 +491,9 @@ export function AnalysisCard(analysis: Analysis) {
{/* TODO: make name & description editable */}
<CardTitle id={id}>{analysis.name}</CardTitle>
<div class="flex gap-1">
<Button variant="outline" onclick={() => setResetPlot(analysis.id)}>
<MaterialSymbolsLightResetImage />
</Button>
<Button
variant="outline"
onClick={(e: MouseEvent) => takeScreenshot(e, analysis)}
Expand Down
21 changes: 21 additions & 0 deletions apps/class-solid/src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,24 @@ export function MdiCamera(props: JSX.IntrinsicElements["svg"]) {
</svg>
);
}

export function MaterialSymbolsLightResetImage(
props: JSX.IntrinsicElements["svg"],
) {
// Icon from Material Symbols Light by Google - https://github.com/google/material-design-icons/blob/master/LICENSE
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<title>Reset plot</title>
<path
fill="currentColor"
d="M4 9.77V5h1v3.235q1-1.896 2.851-3.066T12 4q2.739 0 4.849 1.627t2.824 4.142h-1.06q-.696-2.108-2.486-3.438T12 5Q9.979 5 8.36 6.044T5.909 8.77H8.77v1zm3.5 7.73h9.154l-2.827-3.77l-2.615 3.308l-1.75-2.115zM5.616 21q-.672 0-1.144-.472T4 19.385v-6.77h1v6.77q0 .269.173.442t.443.173h12.769q.269 0 .442-.173t.173-.442v-6.77h1v6.77q0 .67-.472 1.143q-.472.472-1.143.472z"
/>
</svg>
);
}
3 changes: 2 additions & 1 deletion apps/class-solid/src/components/plots/Axes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const AxisLeft = (props: AxisProps) => {
export function getNiceAxisLimits(
data: number[],
extraMargin = 0,
roundTo?: number, // Optional rounding step, e.g. 600 for 10 minutes
): [number, number] {
const max = Math.max(...data);
const min = Math.min(...data);
Expand All @@ -87,7 +88,7 @@ export function getNiceAxisLimits(
// Avoid NaNs on axis for constant values
if (range === 0) return [min - 1, max + 1];

const step = 10 ** Math.floor(Math.log10(range));
const step = roundTo ?? 10 ** Math.floor(Math.log10(range));

const niceMin = Math.floor(min / step) * step - extraMargin * step;
const niceMax = Math.ceil(max / step) * step + extraMargin * step;
Expand Down
Loading