Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 59 additions & 5 deletions apps/class-solid/src/components/Analysis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {
For,
Match,
type Setter,
Show,
Switch,
createEffect,
createMemo,
createUniqueId,
} from "solid-js";
import { createStore } from "solid-js/store";
import type { Observation } from "~/lib/experiment_config";
import {
getThermodynamicProfiles,
Expand Down Expand Up @@ -142,15 +144,34 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
};
});

const [toggles, setToggles] = createStore<Record<string, boolean>>({});

// Initialize all lines as visible
createEffect(() => {
for (const d of chartData()) {
setToggles(d.label, true);
}
});

function toggleLine(label: string, value: boolean) {
setToggles(label, value);
}

return (
<>
{/* TODO: get label for yVariable from model config */}
<ChartContainer>
<Legend entries={chartData} />
<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} />
<For each={chartData()}>{(d) => Line(d)}</For>
<For each={chartData()}>
{(d) => (
<Show when={toggles[d.label]}>
<Line {...d} />
</Show>
)}
</For>
</Chart>
</ChartContainer>
<div class="flex justify-around">
Expand Down Expand Up @@ -223,16 +244,49 @@ export function VerticalProfilePlot({
};
});

function chartData() {
return [...profileData(), ...observations()];
}

const [toggles, setToggles] = createStore<Record<string, boolean>>({});

// Initialize all lines as visible
createEffect(() => {
for (const d of chartData()) {
setToggles(d.label, true);
}
});

function toggleLine(label: string, value: boolean) {
setToggles(label, value);
}

return (
<>
<div class="flex flex-col gap-2">
<ChartContainer>
<Legend entries={() => [...profileData(), ...observations()]} />
<Legend
entries={() => [...profileData(), ...observations()]}
toggles={toggles}
onChange={toggleLine}
/>
<Chart title="Vertical profile plot">
<AxisBottom domain={xLim} label={analysis.variable} />
<AxisLeft domain={yLim} label="Height[m]" />
<For each={profileData()}>{(d) => Line(d)}</For>
<For each={observations()}>{(d) => Line(d)}</For>
<For each={profileData()}>
{(d) => (
<Show when={toggles[d.label]}>
<Line {...d} />
</Show>
)}
</For>
<For each={observations()}>
{(d) => (
<Show when={toggles[d.label]}>
<Line {...d} />
</Show>
)}
</For>
</Chart>
</ChartContainer>
<Picker
Expand Down
51 changes: 24 additions & 27 deletions apps/class-solid/src/components/plots/Legend.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,42 @@
import { For } from "solid-js";
import { cn } from "~/lib/utils";
import { createUniqueId } from "solid-js";
import type { ChartData } from "./ChartContainer";
import { useChartContext } from "./ChartContainer";

export interface LegendProps<T> {
entries: () => ChartData<T>[];
toggles: Record<string, boolean>;
onChange: (key: string, value: boolean) => void;
}

export function Legend<T>(props: LegendProps<T>) {
const [chart, updateChart] = useChartContext();

return (
<div
class={cn(
"flex flex-wrap justify-end text-sm tracking-tight",
`w-[${chart.width}px]`,
)}
class={"flex flex-wrap justify-end gap-2 text-sm tracking-tight"}
style={`max-width: ${chart.width}px;`}
>
<For each={props.entries()}>
{(d) => (
<>
<span class="flex items-center">
<svg
width="1.5rem"
height="1rem"
overflow="visible"
viewBox="0 0 50 20"
>
<title>legend</title>
<path
fill="none"
stroke={d.color}
stroke-dasharray={d.linestyle}
stroke-width="4"
d="M 0 12 L 45 12"
/>
</svg>
<p style={`color: ${d.color}`}>{d.label}</p>
</span>
</>
)}
{(d) => {
const id = createUniqueId();
return (
<div
class="flex items-center gap-1"
style={`color: ${d.color}; accent-color: ${d.color}`}
>
<input
type="checkbox"
checked={props.toggles[d.label]}
onChange={(v) =>
props.onChange(d.label, v.currentTarget.checked)
}
id={id}
/>
<label for={id}>{d.label}</label>
</div>
);
}}
</For>
</div>
);
Expand Down
40 changes: 33 additions & 7 deletions apps/class-solid/src/components/plots/skewTlogP.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Code modified from https://github.com/rsobash/d3-skewt/ (MIT license)
import * as d3 from "d3";
import { For, createSignal } from "solid-js";
import { For, Show, createEffect, createSignal } from "solid-js";
import { createStore } from "solid-js/store";
import { AxisBottom, AxisLeft } from "./Axes";
import type { ChartData, SupportedScaleTypes } from "./ChartContainer";
import {
Expand Down Expand Up @@ -149,9 +150,7 @@ function Sounding(data: ChartData<SoundingRecord>) {

// Note: using temperatures in Kelvin as that's easiest to get from CLASS, but
// perhaps not the most interoperable with other sounding data sources.
export function SkewTPlot({
data,
}: { data: () => ChartData<SoundingRecord>[] }) {
export function SkewTPlot(props: { data: () => ChartData<SoundingRecord>[] }) {
const pressureLines = [1000, 850, 700, 500, 300, 200, 100];
const temperatureLines = d3.range(-100, 45, 10);

Expand All @@ -161,9 +160,30 @@ export function SkewTPlot({
pressureGrid.map((pressure) => [pressure, temperature]),
);

const [toggles, setToggles] = createStore<Record<string, boolean>>({});

// Initialize all lines as visible
createEffect(() => {
for (const d of props.data()) {
setToggles(d.label, true);
}
});

function toggleLine(label: string, value: boolean) {
setToggles(label, value);
}

function showSounding(i: number) {
const cd = props.data()[i];
if (!toggles || !cd) {
return true;
}
return toggles[cd.label];
}

return (
<ChartContainer>
<Legend entries={data} />
<Legend entries={props.data} toggles={toggles} onChange={toggleLine} />
<Chart
title="Thermodynamic diagram"
formatX={d3.format(".0d")}
Expand All @@ -184,8 +204,14 @@ export function SkewTPlot({
<ClipPath />
<For each={temperatureLines}>{(t) => SkewTGridLine(t)}</For>
<For each={pressureLines}>{(p) => LogPGridLine(p)}</For>
<For each={dryAdiabats}>{(d) => DryAdiabat(d)}</For>
<For each={data()}>{(d) => Sounding(d)}</For>
<For each={dryAdiabats}>{(d) => <DryAdiabat {...d} />}</For>
<For each={props.data()}>
{(d, i) => (
<Show when={showSounding(i())}>
<Sounding {...d} />
</Show>
)}
</For>
</Chart>
</ChartContainer>
);
Expand Down
62 changes: 62 additions & 0 deletions apps/class-solid/src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { ValidComponent } from "solid-js";
import { Match, Switch, splitProps } from "solid-js";

import * as CheckboxPrimitive from "@kobalte/core/checkbox";
import type { PolymorphicProps } from "@kobalte/core/polymorphic";

import { cn } from "~/lib/utils";

type CheckboxRootProps<T extends ValidComponent = "div"> =
CheckboxPrimitive.CheckboxRootProps<T> & { class?: string | undefined };

const Checkbox = <T extends ValidComponent = "div">(
props: PolymorphicProps<T, CheckboxRootProps<T>>,
) => {
const [local, others] = splitProps(props as CheckboxRootProps, ["class"]);
return (
<CheckboxPrimitive.Root
class={cn("items-top group relative flex space-x-2", local.class)}
{...others}
>
<CheckboxPrimitive.Input class="peer" />
<CheckboxPrimitive.Control class="size-4 shrink-0 rounded-sm border border-primary ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2 data-[checked]:border-none data-[indeterminate]:border-none data-[checked]:bg-primary data-[indeterminate]:bg-primary data-[checked]:text-primary-foreground data-[indeterminate]:text-primary-foreground">
<CheckboxPrimitive.Indicator>
<Switch>
<Match when={!others.indeterminate}>
{/* biome-ignore lint/a11y/noSvgWithoutTitle: want to use tooltip for something else */}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M5 12l5 5l10 -10" />
</svg>
</Match>
<Match when={others.indeterminate}>
{/* biome-ignore lint/a11y/noSvgWithoutTitle: want to use tooltip for something else */}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M5 12l14 0" />
</svg>
</Match>
</Switch>
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Control>
</CheckboxPrimitive.Root>
);
};

export { Checkbox };