Skip to content

Commit

Permalink
feat: custom plot axis titles
Browse files Browse the repository at this point in the history
- backend: add axis titles to the simulation plot model.
- frontend: add custom axis titles to plots, falling back to the old default values when empty.
  • Loading branch information
eatyourgreens committed Mar 4, 2025
1 parent 3d045a6 commit e88a689
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 69 deletions.
44 changes: 20 additions & 24 deletions frontend-v2/src/app/backendApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2739,9 +2739,7 @@ export type Inference = {
/** If executing, this is the celery task id */
task_id?: string | null;
/** metadata for inference */
metadata?: {
[key: string]: any;
};
metadata?: any;
/** Project that "owns" this inference object */
project: number;
/** algorithm used to perform the inference */
Expand Down Expand Up @@ -2775,9 +2773,7 @@ export type InferenceRead = {
/** If executing, this is the celery task id */
task_id?: string | null;
/** metadata for inference */
metadata?: {
[key: string]: any;
};
metadata?: any;
/** Project that "owns" this inference object */
project: number;
/** algorithm used to perform the inference */
Expand Down Expand Up @@ -2810,9 +2806,7 @@ export type PatchedInference = {
/** If executing, this is the celery task id */
task_id?: string | null;
/** metadata for inference */
metadata?: {
[key: string]: any;
};
metadata?: any;
/** Project that "owns" this inference object */
project?: number;
/** algorithm used to perform the inference */
Expand Down Expand Up @@ -2846,9 +2840,7 @@ export type PatchedInferenceRead = {
/** If executing, this is the celery task id */
task_id?: string | null;
/** metadata for inference */
metadata?: {
[key: string]: any;
};
metadata?: any;
/** Project that "owns" this inference object */
project?: number;
/** algorithm used to perform the inference */
Expand Down Expand Up @@ -3204,9 +3196,7 @@ export type ResultsTable = {
* `intervals` - Time intervals. */
columns: ColumnsEnum;
/** Filters to apply to the table. */
filters?: {
[key: string]: any;
} | null;
filters?: any | null;
/** Project that this table belongs to. */
project?: number | null;
};
Expand All @@ -3229,9 +3219,7 @@ export type ResultsTableRead = {
* `intervals` - Time intervals. */
columns: ColumnsEnum;
/** Filters to apply to the table. */
filters?: {
[key: string]: any;
} | null;
filters?: any | null;
/** Project that this table belongs to. */
project?: number | null;
};
Expand All @@ -3253,9 +3241,7 @@ export type PatchedResultsTable = {
* `intervals` - Time intervals. */
columns?: ColumnsEnum;
/** Filters to apply to the table. */
filters?: {
[key: string]: any;
} | null;
filters?: any | null;
/** Project that this table belongs to. */
project?: number | null;
};
Expand All @@ -3278,9 +3264,7 @@ export type PatchedResultsTableRead = {
* `intervals` - Time intervals. */
columns?: ColumnsEnum;
/** Filters to apply to the table. */
filters?: {
[key: string]: any;
} | null;
filters?: any | null;
/** Project that this table belongs to. */
project?: number | null;
};
Expand Down Expand Up @@ -3338,6 +3322,12 @@ export type SimulationPlot = {
* `lg10` - Log10
* `ln` - Ln */
y2_scale?: Y2ScaleEnum;
/** label for x axis */
x_label?: string;
/** label for y axis */
y_label?: string;
/** label for rhs y axis */
y2_label?: string;
/** lower bound for the y axis */
min?: number | null;
/** upper bound for the y axis */
Expand Down Expand Up @@ -3380,6 +3370,12 @@ export type SimulationPlotRead = {
* `lg10` - Log10
* `ln` - Ln */
y2_scale?: Y2ScaleEnum;
/** label for x axis */
x_label?: string;
/** label for y axis */
y_label?: string;
/** label for rhs y axis */
y2_label?: string;
/** lower bound for the y axis */
min?: number | null;
/** upper bound for the y axis */
Expand Down
47 changes: 36 additions & 11 deletions frontend-v2/src/app/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";
import { Control, FieldPath, FieldValues, useFormState } from "react-hook-form";
import {
Control,
DeepPartial,
FieldPath,
FieldValues,
useFormState,
} from "react-hook-form";
import { useEffect, useState } from "react";

// Use throughout your app instead of plain `useDispatch` and `useSelector`
Expand All @@ -10,26 +16,45 @@ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
type Props<T extends FieldValues> = {
name: FieldPath<T>;
control: Control<T>;
defaultValue?: string | number;
};

export function useFieldState<T extends FieldValues>({
name,
/**
* Subscribe to the stored default value for a given path within a form control object.
* @param props.control A form control object.
* @param props.name Path of a field within the control object.
* @returns Default value for name.
*/
export function useDefaultValue<T extends FieldValues>({
control,
name,
}: Props<T>) {
const { defaultValues } = useFormState({ control, name });
const keys = name.split(".");
const defaultValue = keys.reduce((acc, key) => {
if (acc && typeof acc === "object") {
return acc[key];
let nextValue = defaultValues;
keys.forEach((key) => {
if (typeof nextValue === "object") {
nextValue = nextValue[key];
}
return undefined;
}, defaultValues);
});
return nextValue as string | number | undefined;
}

export function useFieldState<T extends FieldValues>({
name,
control,
defaultValue,
}: Props<T>) {
const defaultFromFormKey = useDefaultValue({ control, name });
const initialValue = defaultValue
? defaultFromFormKey || defaultValue
: defaultFromFormKey;

const [fieldValue, setFieldValue] = useState<any>(defaultValue);
const [fieldValue, setFieldValue] = useState<any>(initialValue);

useEffect(() => {
setFieldValue(defaultValue);
}, [defaultValue]);
setFieldValue(initialValue);
}, [initialValue]);

return [fieldValue, setFieldValue];
}
25 changes: 14 additions & 11 deletions frontend-v2/src/components/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,26 @@ type Props<T extends FieldValues> = {
autoShrink?: boolean;
size?: "small" | "medium";
sx?: SxProps;
defaultValue?: string;
};

function TextField<T extends FieldValues>({
label,
name,
control,
rules,
mode,
mode = "onBlur",
size = "medium",
textFieldProps,
autoShrink,
sx,
defaultValue = "",
}: Props<T>): ReactElement {
const [fieldValue, setFieldValue] = useFieldState({ name, control });

if (mode === undefined) {
mode = "onBlur";
}
const [fieldValue, setFieldValue] = useFieldState({
name,
control,
defaultValue,
});

return (
<Controller
Expand All @@ -47,8 +49,11 @@ function TextField<T extends FieldValues>({
fieldState: { error },
}) => {
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
if (mode === "onBlur" && e.target.value !== value) {
onChange(e);
// save the default value, if the text field is empty.
const newValue = e.target.value || defaultValue;
setFieldValue(newValue);
if (mode === "onBlur" && newValue !== value) {
onChange({ target: { value: newValue } });
}
onBlur();
};
Expand All @@ -74,9 +79,7 @@ function TextField<T extends FieldValues>({
autoShrink !== undefined ? { shrink: autoShrink } : {}
}
variant="outlined"
value={
fieldValue === undefined || fieldValue === null ? "" : fieldValue
}
value={fieldValue}
onChange={handleChange}
onBlur={handleBlur}
error={!!error}
Expand Down
35 changes: 35 additions & 0 deletions frontend-v2/src/features/simulation/SimulationPlotForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import FloatField from "../../components/FloatField";
import { useSelector } from "react-redux";
import { RootState } from "../../app/store";
import { selectIsProjectShared } from "../login/loginSlice";
import { getDefaultAxisTitles } from "./utils";

interface SimulationPlotFormProps {
index: number;
Expand Down Expand Up @@ -124,6 +125,19 @@ const SimulationPlotForm: FC<SimulationPlotFormProps> = ({
baseY2UnitId = undefined;
}

const yAxisVariables = lhs_y_axes.map(
(y) => variables.find((v) => v.id === y.variable)?.name,
);
const y2AxisVariables = rhs_y_axes.map(
(y) => variables.find((v) => v.id === y.variable)?.name,
);
const { xAxisTitle, yAxisTitle, y2AxisTitle } = getDefaultAxisTitles({
plot,
units,
yAxisVariables,
y2AxisVariables,
});

const commonAddYAxis = (
variableId: number,
first: boolean,
Expand Down Expand Up @@ -235,6 +249,13 @@ const SimulationPlotForm: FC<SimulationPlotFormProps> = ({
control={control}
selectProps={defaultProps}
/>
<TextField
label="X Axis Label"
name={`plots.${index}.x_label`}
control={control}
textFieldProps={defaultProps}
defaultValue={xAxisTitle}
/>
</Stack>
<Divider sx={{ margin: 2 }} />
<Typography sx={{ fontWeight: "bold", paddingBottom: "1rem" }}>
Expand Down Expand Up @@ -262,6 +283,13 @@ const SimulationPlotForm: FC<SimulationPlotFormProps> = ({
baseUnit={units.find((u) => u.id === baseYUnitId)}
selectProps={{ disabled: lhs_y_axes.length === 0 || isSharedWithMe }}
/>
<TextField
label="Y Axis Label"
name={`plots.${index}.y_label`}
control={control}
textFieldProps={defaultProps}
defaultValue={yAxisTitle}
/>
<SelectField
label="Y Axis Scale"
name={`plots.${index}.y_scale`}
Expand Down Expand Up @@ -379,6 +407,13 @@ const SimulationPlotForm: FC<SimulationPlotFormProps> = ({
baseUnit={units.find((u) => u.id === baseY2UnitId)}
selectProps={{ disabled: rhs_y_axes.length === 0 || isSharedWithMe }}
/>
<TextField
label="Y2 Axis Label"
name={`plots.${index}.y2_label`}
control={control}
textFieldProps={defaultProps}
defaultValue={y2AxisTitle}
/>
<SelectField
label="Y2 Axis Scale"
name={`plots.${index}.y2_scale`}
Expand Down
22 changes: 16 additions & 6 deletions frontend-v2/src/features/simulation/SimulationPlotView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ import {
generateScatterPlots,
genIcLines,
getICLineShapes,
getAxisTitles,
getPlotDimensions,
getPlotLayout,
getYRanges,
ScatterDataWithVariable,
getPlotAxes,
getDefaultAxisTitles,
} from "./utils";
import { useConfig } from "./config";

Expand Down Expand Up @@ -141,10 +141,20 @@ const SimulationPlotView: FC<SimulationPlotProps> = ({
const maxX = Math.max(...convertedTime);
const icLineShapes = getICLineShapes({ icLines, minX, maxX, plot });

const { xAxisTitle, yAxisTitle, y2AxisTitle } = getAxisTitles({
const yAxisVariables = plotData
.filter((d) => !d.yaxis)
.map((d) => d.variable)
.filter(Boolean);
const y2AxisVariables = plotData
.filter((d) => d.yaxis)
.map((d) => d.variable)
.filter(Boolean);

const defaultAxisTitles = getDefaultAxisTitles({
plot,
plotData,
units,
yAxisVariables,
y2AxisVariables,
});

const plotDimensions = getPlotDimensions({
Expand All @@ -156,9 +166,9 @@ const SimulationPlotView: FC<SimulationPlotProps> = ({

const plotAxes: Partial<Layout> = getPlotAxes({
plot,
xAxisTitle,
yAxisTitle,
y2AxisTitle,
xAxisTitle: plot.x_label || defaultAxisTitles.xAxisTitle,
yAxisTitle: plot.y_label || defaultAxisTitles.yAxisTitle,
y2AxisTitle: plot.y2_label || defaultAxisTitles.y2AxisTitle,
yRanges,
});

Expand Down
14 changes: 5 additions & 9 deletions frontend-v2/src/features/simulation/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,21 +358,17 @@ export const getYRanges = ({
return { minY, maxY, minY2, maxY2 };
};

export const getAxisTitles = ({
export const getDefaultAxisTitles = ({
plot,
plotData,
units,
yAxisVariables,
y2AxisVariables,
}: {
plot: FieldArrayWithId<Simulation, "plots", "id">;
plotData: Partial<ScatterDataWithVariable>[];
units: UnitRead[];
yAxisVariables: (string | undefined)[];
y2AxisVariables: (string | undefined)[];
}) => {
const yAxisVariables = plotData
.filter((d) => !d.yaxis)
.map((d) => d.variable);
const y2AxisVariables = plotData
.filter((d) => d.yaxis)
.map((d) => d.variable);
let yAxisTitle = [...new Set(yAxisVariables)].join(", ");
let y2AxisTitle = [...new Set(y2AxisVariables)].join(", ");
let xAxisTitle = "Time";
Expand Down
Loading

0 comments on commit e88a689

Please sign in to comment.