diff --git a/apps/class-solid/src/components/Analysis.tsx b/apps/class-solid/src/components/Analysis.tsx index d7da2178..d307ba2f 100644 --- a/apps/class-solid/src/components/Analysis.tsx +++ b/apps/class-solid/src/components/Analysis.tsx @@ -131,12 +131,8 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) { {/* TODO: get label for yVariable from model config */} - - + + {(d) => Line(d)} diff --git a/apps/class-solid/src/components/plots/Axes.tsx b/apps/class-solid/src/components/plots/Axes.tsx index 732b5726..e624d4cf 100644 --- a/apps/class-solid/src/components/plots/Axes.tsx +++ b/apps/class-solid/src/components/plots/Axes.tsx @@ -1,6 +1,5 @@ // Code generated by AI and checked/modified for correctness -import * as d3 from "d3"; import { For, createEffect } from "solid-js"; import { useChartContext } from "./ChartContainer"; @@ -9,7 +8,6 @@ type AxisProps = { domain?: () => [number, number]; // TODO: is this needed for reactivity? label?: string; tickValues?: number[]; - tickFormat?: (n: number) => string; }; export const AxisBottom = (props: AxisProps) => { @@ -19,7 +17,6 @@ export const AxisBottom = (props: AxisProps) => { props.type && updateChart("scalePropsX", { type: props.type }); }); - const format = () => (props.tickFormat ? props.tickFormat : d3.format(".4")); const ticks = () => props.tickValues || generateTicks(chart.scaleX.domain()); return ( @@ -29,7 +26,7 @@ export const AxisBottom = (props: AxisProps) => { - {format()(tick)} + {chart.formatX(tick)} )} @@ -49,7 +46,6 @@ export const AxisLeft = (props: AxisProps) => { }); const ticks = () => props.tickValues || generateTicks(chart.scaleY.domain()); - const format = () => (props.tickFormat ? props.tickFormat : d3.format(".4")); return ( { - {format()(tick)} + {chart.formatY(tick)} )} diff --git a/apps/class-solid/src/components/plots/ChartContainer.tsx b/apps/class-solid/src/components/plots/ChartContainer.tsx index 2354fba5..57cafa12 100644 --- a/apps/class-solid/src/components/plots/ChartContainer.tsx +++ b/apps/class-solid/src/components/plots/ChartContainer.tsx @@ -1,9 +1,14 @@ import * as d3 from "d3"; import type { JSX } from "solid-js"; -import { createContext, createEffect, useContext } from "solid-js"; +import { + createContext, + createEffect, + createSignal, + useContext, +} from "solid-js"; import { type SetStoreFunction, createStore } from "solid-js/store"; -type SupportedScaleTypes = +export type SupportedScaleTypes = | d3.ScaleLinear | d3.ScaleLogarithmic; const supportedScales = { @@ -27,6 +32,9 @@ interface Chart { scalePropsY: ScaleProps; scaleX: SupportedScaleTypes; scaleY: SupportedScaleTypes; + formatX: (value: number) => string; + formatY: (value: number) => string; + transformX?: (x: number, y: number, scaleY: SupportedScaleTypes) => number; } type SetChart = SetStoreFunction; const ChartContext = createContext<[Chart, SetChart]>(); @@ -55,6 +63,8 @@ export function ChartContainer(props: { scalePropsY: { type: "linear", domain: [0, 1], range: [innerHeight, 0] }, scaleX: initialScale, scaleY: initialScale, + formatX: d3.format(".4"), + formatY: d3.format(".4"), }); createEffect(() => { // Update scaleXInstance when scaleX props change @@ -81,30 +91,63 @@ export function ChartContainer(props: { } /** Container for chart elements such as axes and lines */ -export function Chart(props: { children: JSX.Element; title?: string }) { +export function Chart(props: { + children: JSX.Element; + title?: string; + formatX?: (value: number) => string; + formatY?: (value: number) => string; + transformX?: (x: number, y: number, scaleY: SupportedScaleTypes) => number; +}) { + const [hovering, setHovering] = createSignal(false); + const [coords, setCoords] = createSignal<[number, number]>([0, 0]); const [chart, updateChart] = useChartContext(); const title = props.title || "Default chart"; const [marginTop, _, __, marginLeft] = chart.margin; + if (props.formatX) { + updateChart("formatX", () => props.formatX); + } + if (props.formatY) { + updateChart("formatY", () => props.formatY); + } + if (props.transformX) { + updateChart("transformX", () => props.transformX); + } + + const onMouseMove = (e: MouseEvent) => { + let x = e.offsetX - marginLeft; + const y = e.offsetY - marginTop; + + if (chart.transformX) { + x = chart.transformX(x, y, chart.scaleY); + } + + setCoords([chart.scaleX.invert(x), chart.scaleY.invert(y)]); + }; + + const renderXCoord = () => + hovering() ? `x: ${chart.formatX(coords()[0])}` : ""; + const renderYCoord = () => + hovering() ? `y: ${chart.formatY(coords()[1])}` : ""; + return ( setHovering(true)} + onmousemove={onMouseMove} + onmouseout={() => setHovering(false)} > {title} {props.children} - {/* Line along right edge of plot - */} + + {renderXCoord()} + + + {renderYCoord()} + ); diff --git a/apps/class-solid/src/components/plots/skewTlogP.tsx b/apps/class-solid/src/components/plots/skewTlogP.tsx index f6aca45e..d3f1de08 100644 --- a/apps/class-solid/src/components/plots/skewTlogP.tsx +++ b/apps/class-solid/src/components/plots/skewTlogP.tsx @@ -2,10 +2,9 @@ import * as d3 from "d3"; import { For, createSignal } from "solid-js"; import { AxisBottom, AxisLeft } from "./Axes"; -import type { ChartData } from "./ChartContainer"; +import type { ChartData, SupportedScaleTypes } from "./ChartContainer"; import { Chart, ChartContainer, useChartContext } from "./ChartContainer"; import { Legend } from "./Legend"; - interface SoundingRecord { p: number; T: number; @@ -17,6 +16,10 @@ const tan = Math.tan(55 * deg2rad); const basep = 1050; const topPressure = 100; +function getTempAtCursor(x: number, y: number, scaleY: SupportedScaleTypes) { + return x + 0.5 - (scaleY(basep) - y) / tan; +} + function ClipPath() { const [chart, updateChart] = useChartContext(); @@ -148,18 +151,21 @@ export function SkewTPlot({ return ( - + [-45, 50]} tickValues={temperatureLines} - tickFormat={d3.format(".0d")} label="Temperature [°C]" /> [basep, topPressure]} tickValues={pressureLines} - tickFormat={d3.format(".0d")} label="Pressure [hPa]" />