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
77 changes: 76 additions & 1 deletion apps/visr/src/components/spectroscopy/SpectroscopyControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,37 @@ import {
FormControlLabel,
Radio,
RadioGroup,
Tooltip,
Typography,
} from "@mui/material";
import { DataSource } from "./dataSource";
import {
POSITIONS,
READBACK_ONLY_HELP,
SHAPES,
type Positions,
type Shape,
} from "./plotKinds";

interface Props {
onSetDataSource: (event: React.ChangeEvent<HTMLInputElement>) => void;
dataSource: DataSource;
positions: Positions;
onSetPositions: (positions: Positions) => void;
/** The shape for readbacks, kept while the setpoint grid is shown */
shape: Shape;
onSetShape: (shape: Shape) => void;
}

export default function Controls({ onSetDataSource, dataSource }: Props) {
export default function Controls({
onSetDataSource,
dataSource,
positions,
onSetPositions,
shape,
onSetShape,
}: Props) {
const onGrid = positions === "setpoints";
return (
<Box
sx={{
Expand Down Expand Up @@ -43,6 +64,60 @@ export default function Controls({ onSetDataSource, dataSource }: Props) {
/>
</RadioGroup>
</Box>

<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="body2" color="primary">
Positions
</Typography>
<RadioGroup
row
value={positions}
onChange={event => onSetPositions(event.target.value as Positions)}
>
{POSITIONS.map(({ value, label, help }) => (
<Tooltip key={value} title={help} describeChild>
<FormControlLabel
value={value}
control={<Radio />}
label={label}
/>
</Tooltip>
))}
</RadioGroup>
</Box>

<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="body2" color="primary">
Plot Type
</Typography>
<RadioGroup
row
// the setpoint grid's cells are rectangles
value={onGrid ? "rectangles" : shape}
onChange={event => onSetShape(event.target.value as Shape)}
>
{SHAPES.map(({ value, label, help }) => {
const disabled = onGrid && value !== "rectangles";
return (
<Tooltip
key={value}
title={disabled ? READBACK_ONLY_HELP : help}
describeChild
>
{/* a span, so the tooltip still shows on a disabled option */}
<span>
<FormControlLabel
value={value}
control={<Radio />}
label={label}
disabled={disabled}
/>
</span>
</Tooltip>
);
})}
</RadioGroup>
</Box>
</Box>
);
}
210 changes: 183 additions & 27 deletions apps/visr/src/components/spectroscopy/SpectroscopyPlots.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,35 @@
import { Box, Typography } from "@mui/material";
import {
Box,
CircularProgress,
Fade,
Typography,
useColorScheme,
} from "@mui/material";
import { ImagePlot } from "@diamondlightsource/davidia";
import { ReactGridLayout, useContainerWidth } from "react-grid-layout";
import { useLayoutEffect, useMemo, useState, type ComponentProps } from "react";
import { useSpectroscopyData, type DataChannels } from "./useSpectroscopyData";
import {
useEffect,
useLayoutEffect,
useMemo,
useState,
type ComponentProps,
} from "react";
import {
DEFAULT_STRIPES,
stripesOn,
useSpectroscopyData,
type DataChannels,
type PlotKind,
type Rgb,
type StripeColours,
} from "./useSpectroscopyData";
import { beamWarning } from "./plotKinds";
import { DataSource } from "./dataSource";
import {
SCAN_STATUS_BUSY,
SCAN_STATUS_TEXT,
type ScanStatus,
} from "./scanStatus";

const CHANNELS = [
{ key: "red", label: "Red channel" },
Expand All @@ -21,7 +47,7 @@ export type SpectroscopyData = Partial<Record<ChannelKey, PlotValues>>;

const SOURCE_CHANNELS: Record<
DataSource,
Record<ChannelKey, keyof DataChannels>
Record<ChannelKey, `${"camera" | "diodes"}${"Red" | "Green" | "Blue"}`>
> = {
[DataSource.Camera]: {
red: "cameraRed",
Expand All @@ -48,27 +74,145 @@ function selectChannels(
};
}

// ---------------------------------------------------------------------------
// Scan status: one quiet line under the plots, at the right
// ---------------------------------------------------------------------------

const STATUS_COLOUR: Record<ScanStatus, string> = {
scanning: "text.secondary",
loadingLastPoints: "text.secondary",
complete: "text.secondary",
lastPointsMissing: "warning.main",
failed: "error.main",
};

/** "Scan complete" fades out after this long; problems stay */
const COMPLETE_SHOWN_MS = 3000;
const FADE_MS = 1000;

/** One status's text; remounted when the status changes, so a fade starts afresh */
function ScanStatusText({ status }: { status: ScanStatus }) {
const [shown, setShown] = useState(true);
useEffect(() => {
if (status !== "complete") return;
const timer = setTimeout(() => setShown(false), COMPLETE_SHOWN_MS);
return () => clearTimeout(timer);
}, [status]);
return (
<Fade in={shown} timeout={FADE_MS} appear={false}>
<Box sx={{ display: "flex", alignItems: "center", gap: 0.75 }}>
{SCAN_STATUS_BUSY.has(status) && (
<CircularProgress size={10} thickness={5} color="inherit" />
)}
<Typography variant="caption" color={STATUS_COLOUR[status]}>
{SCAN_STATUS_TEXT[status]}
</Typography>
</Box>
</Fade>
);
}

function ScanStatusLine({
status,
note,
}: {
status: ScanStatus | null;
/** A remark about the plots themselves, on the left */
note?: string | null;
}) {
return (
<Box
role="status"
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
minHeight: 20,
px: 1,
flexShrink: 0,
}}
>
{note && (
<Typography
variant="caption"
color="text.secondary"
sx={{ mr: "auto", pr: 2 }}
>
{note}
</Typography>
)}
{status && <ScanStatusText key={status} status={status} />}
</Box>
);
}

// ---------------------------------------------------------------------------
// Empty-cell stripes in the colours of the page behind the plots
// ---------------------------------------------------------------------------

/** The first opaque background colour at or above `element`, if any */
function backgroundBehind(element: HTMLElement | null): Rgb | null {
for (let el = element; el; el = el.parentElement) {
const parts = getComputedStyle(el).backgroundColor.match(/[\d.]+/g);
if (parts && parts.length >= 3 && (parts.length < 4 || +parts[3] > 0)) {
return [+parts[0], +parts[1], +parts[2]];
}
}
return null;
}

/**
* Stripes matching the background behind `element`, read again when the
* colour scheme changes (after the browser has applied it)
*/
function useStripesBehind(
element: React.RefObject<HTMLElement | null>,
): StripeColours {
const { mode, systemMode } = useColorScheme();
const scheme = mode === "system" ? systemMode : mode;
const [background, setBackground] = useState<string | null>(null);
useEffect(() => {
const frame = requestAnimationFrame(() => {
const found = backgroundBehind(element.current);
if (found) setBackground(found.join(","));
});
return () => cancelAnimationFrame(frame);
}, [element, scheme]);
return useMemo(
() =>
background
? stripesOn(background.split(",").map(Number) as unknown as Rgb)
: DEFAULT_STRIPES,
[background],
);
}

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

interface SpectroscopyPlotsProps {
dataSource: DataSource;
plotKind: PlotKind;
expanded: boolean;
plotAspectRatio: number | "auto" | "equal";
}

function SpectroscopyPlots({
dataSource,
plotKind,
expanded,
plotAspectRatio,
}: SpectroscopyPlotsProps) {
const { data } = useSpectroscopyData();
const { width, containerRef, mounted } = useContainerWidth();
const stripes = useStripesBehind(
containerRef as React.RefObject<HTMLElement | null>,
);
const { data, status } = useSpectroscopyData(plotKind, stripes);
const channels = useMemo(
() => selectChannels(data, dataSource),
[data, dataSource],
);
const { width, containerRef, mounted } = useContainerWidth();

// -------------------------------------------------------------------------
// Grid sizing
Expand Down Expand Up @@ -126,7 +270,7 @@ function SpectroscopyPlots({
return (
<ImagePlot
aspect={plotAspectRatio}
plotConfig={{}}
plotConfig={{ xValues: data.xValues, yValues: data.yValues }}
customToolbarChildren={null}
values={values}
//tightAxes //requires Davidia 1.1.0
Expand Down Expand Up @@ -183,32 +327,44 @@ function SpectroscopyPlots({
// -------------------------------------------------------------------------
return (
<Box
ref={containerRef! as React.RefObject<HTMLDivElement>}
sx={{
flex: "1 1 0",
display: "flex",
flexDirection: "column",
width: "100%",
minWidth: 0,
height: gridHeight,
minHeight: minimumGridHeight,
overflow: "visible",
minHeight: 0,
}}
>
{mounted && (
<ReactGridLayout
layout={layout}
width={width}
style={{ height: gridHeight }}
gridConfig={{
cols: expanded ? 2 : 3,
rowHeight: rowHeight,
margin: [0, 0],
}}
autoSize
onLayoutChange={() => {}}
>
{plots}
</ReactGridLayout>
)}
<Box
ref={containerRef! as React.RefObject<HTMLDivElement>}
sx={{
flex: "1 1 0",
width: "100%",
minWidth: 0,
height: gridHeight,
minHeight: minimumGridHeight,
overflow: "visible",
}}
>
{mounted && (
<ReactGridLayout
layout={layout}
width={width}
style={{ height: gridHeight }}
gridConfig={{
cols: expanded ? 2 : 3,
rowHeight: rowHeight,
margin: [0, 0],
}}
autoSize
onLayoutChange={() => {}}
>
{plots}
</ReactGridLayout>
)}
</Box>
<ScanStatusLine status={status} note={beamWarning(plotKind, data.beam)} />
</Box>
);
}
Expand Down
Loading
Loading