diff --git a/.github/workflows/npm-publish.yaml b/.github/workflows/npm-publish.yaml index b9348ecc..0c0e1332 100644 --- a/.github/workflows/npm-publish.yaml +++ b/.github/workflows/npm-publish.yaml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 with: @@ -28,9 +28,9 @@ jobs: args: [ --force ] - name: Setup node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: '20.x' + node-version: '24' registry-url: https://registry.npmjs.org/ scope: '@diamondlightsource' cache: pnpm diff --git a/client/component/src/AxisConfigModal.tsx b/client/component/src/AxisConfigModal.tsx index 04bf087d..b43f7f9c 100644 --- a/client/component/src/AxisConfigModal.tsx +++ b/client/component/src/AxisConfigModal.tsx @@ -51,8 +51,8 @@ interface AxisConfigModalProps { domain?: Domain; /** The custom domain for the axis (optional) */ customDomain?: CustomDomain; - /** Histogram params */ - histogram?: HistogramParams; + /** Histogram params getter */ + histogramGetter?: () => HistogramParams; /** The function to call when the custom domain is updated (optional) */ setCustomDomain?: (value: CustomDomain) => void; /** Point size for scatter plot (optional) */ @@ -106,7 +106,7 @@ function AxisConfigModal( ); const domainSelector = props.domain && - props.histogram && + props.histogramGetter && props.customDomain && props.setCustomDomain && ( ( customDomain={props.customDomain} scaleType={props.scaleType as ColorScaleType | undefined} onCustomDomainChange={props.setCustomDomain} - histogram={props.histogram} + histogramGetter={props.histogramGetter} /> ); diff --git a/client/component/src/DomainConfig.tsx b/client/component/src/DomainConfig.tsx index 207101fb..c74c3f28 100644 --- a/client/component/src/DomainConfig.tsx +++ b/client/component/src/DomainConfig.tsx @@ -73,7 +73,7 @@ interface DomainConfigProps { /** Handles custom domain change */ onCustomDomainChange: (domain: CustomDomain) => void; /** Histogram params */ - histogram?: HistogramParams; + histogramGetter?: () => HistogramParams; } /** @@ -83,11 +83,16 @@ interface DomainConfigProps { */ function DomainConfig(props: DomainConfigProps) { const { dataDomain, customDomain, scaleType } = props; - const { onCustomDomainChange, histogram } = props; + const { onCustomDomainChange, histogramGetter } = props; const [lockDomain, setLockDomain] = useState(false); const visDomain = useVisDomain(customDomain, dataDomain); + const histogram = useMemo( + () => histogramGetter && histogramGetter(), + [histogramGetter] + ); + const memoizedCustomDomain = useMemo(() => { if (lockDomain) { return customDomain; diff --git a/client/component/src/HeatmapPlot.tsx b/client/component/src/HeatmapPlot.tsx index 204a4573..a5274c40 100644 --- a/client/component/src/HeatmapPlot.tsx +++ b/client/component/src/HeatmapPlot.tsx @@ -8,7 +8,7 @@ import { } from '@h5web/lib'; import { - calculateHistogramCounts, + createHistogramParams, createInteractionsConfig, InteractionModeType, } from './utils'; @@ -20,7 +20,7 @@ import { usePlotCustomizationContext, } from './PlotCustomizationContext'; import { AnyToolbar } from './PlotToolbar'; -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; interface Props { xValues?: NDT; @@ -38,6 +38,7 @@ export function HeatmapVisCanvas({ xValues, yValues, values }: Props) { aspect, batonProps, canSelect, + selectionMax, selectionType, updateSelection, selections, @@ -46,24 +47,15 @@ export function HeatmapVisCanvas({ xValues, yValues, values }: Props) { colourMap, invertColourMap, dScaleType, - setHistogram, + updateHistogramGetter, } = usePlotCustomizationContext(); const interactionsConfig = createInteractionsConfig(mode); - const histogram = useMemo( - () => calculateHistogramCounts(values.data, dDomain), - [dDomain, values] - ); - useEffect(() => { - if (histogram) { - setHistogram({ - ...histogram, - colorMap: colourMap, - invertColorMap: invertColourMap, - }); - } - }, [colourMap, invertColourMap, histogram, setHistogram]); + const hg = () => + createHistogramParams(values.data, dDomain, colourMap, invertColourMap); + updateHistogramGetter(hg); + }, [values.data, dDomain, colourMap, invertColourMap, updateHistogramGetter]); return ( void; /** If client holds baton */ hasBaton: boolean; + canSelect: boolean; } /** @@ -24,7 +25,7 @@ interface InteractionModeToggleProps { * @returns {JSX.Element} The rendered component. */ function InteractionModeToggle(props: InteractionModeToggleProps) { - const { value, onModeChange, hasBaton } = props; + const { value, onModeChange, hasBaton, canSelect } = props; useEffect(() => { if (!hasBaton && value === InteractionModeType.selectRegion) { @@ -54,21 +55,25 @@ function InteractionModeToggle(props: InteractionModeToggleProps) { icon={TbZoomInArea as IIconType} value={InteractionModeType.selectToZoom} /> -
- -
+ {canSelect ? ( +
+ +
+ ) : ( + <> + )} ); diff --git a/client/component/src/LinePlot.tsx b/client/component/src/LinePlot.tsx index 467dfc9c..3a5f920e 100644 --- a/client/component/src/LinePlot.tsx +++ b/client/component/src/LinePlot.tsx @@ -144,6 +144,7 @@ export function LineVisCanvas(props: Props) { setCurrentLineKey, batonProps, canSelect, + selectionMax, selectionType, updateSelection, selections, @@ -213,6 +214,7 @@ export function LineVisCanvas(props: Props) { modifierKey={[] as ModifierKey[]} batonProps={batonProps} disabled={mode !== InteractionModeType.selectRegion} + selectionMax={selectionMax} selectionType={selectionType} updateSelection={updateSelection} selections={selections} diff --git a/client/component/src/PlotCustomizationContext.tsx b/client/component/src/PlotCustomizationContext.tsx index a016342b..07cd393d 100644 --- a/client/component/src/PlotCustomizationContext.tsx +++ b/client/component/src/PlotCustomizationContext.tsx @@ -21,8 +21,16 @@ import { import { defaultBatonProps, type BatonProps } from './models'; import { InteractionModeType } from './utils'; -import type { SelectionBase, SelectionHandler } from './selections/utils'; -import { SelectionType, useSelections } from './selections/utils'; +import type { + SelectionBase, + SelectionHandler, + SelectionOptions, +} from './selections/utils'; +import { + SelectionType, + toSelectionType, + useSelections, +} from './selections/utils'; import { LineParams, LinePlotCustomizationProps } from './LinePlot'; import { ImagePlotCustomizationProps } from './ImagePlot'; import { HeatmapPlotCustomizationProps } from './HeatmapPlot'; @@ -84,6 +92,8 @@ export interface PlotCustomizationContextValue { aspect: Aspect; /** A function that sets the aspect value */ setAspect: (a: Aspect) => void; + /** Selection upper limit (on current type) */ + selectionMax: number; /** A selection type */ selectionType: SelectionType; /** A function that sets the selection type */ @@ -106,6 +116,8 @@ export interface PlotCustomizationContextValue { invertColourMap: boolean; /** A function that toggles the color map inversion */ toggleInvertColourMap: () => void; + /** Selection options */ + selectionOptions?: SelectionOptions; /** Selections */ selections: SelectionBase[]; /** A function that sets the selections */ @@ -130,10 +142,11 @@ export interface PlotCustomizationContextValue { showPoints: boolean; /** A function that toggles the points */ toggleShowPoints: () => void; - /** Histogram params */ - histogram?: HistogramParams; - /** Set heatmap histogram */ - setHistogram: (histogram: HistogramParams) => void; + /** Get heatmap histogram */ + histogramGetter?: () => HistogramParams; + /** Update heatmap histogram getter */ + updateHistogramGetter: (getter: () => HistogramParams) => void; + /** Can select */ canSelect: boolean; /** Type of plot */ @@ -170,8 +183,9 @@ export function PlotCustomizationContextProvider( InteractionModeType.panAndWheelZoom ); const [selectionType, setSelectionType] = useState( - SelectionType.line + SelectionType.unknown ); + const [selectionMax, setSelectionMax] = useState(0); const [xCustomDomain, setXCustomDomain] = useState([ null, null, @@ -200,9 +214,15 @@ export function PlotCustomizationContextProvider( new Map() ); const [currentLineKey, setCurrentLineKey] = useState(null); - const [histogram, setHistogram] = useState(); + const [histogramGetter, setHistogramGetter] = + useState<() => HistogramParams>(); const [scatterPointSize, setScatterPointSize] = useState(10); + const updateHistogramGetter = useCallback( + (g: () => HistogramParams) => setHistogramGetter(() => g), + [setHistogramGetter] + ); + useEffect(() => { if (props.plotConfig.title) setTitle(props.plotConfig.title); }, [props.plotConfig.title, setTitle]); @@ -218,6 +238,26 @@ export function PlotCustomizationContextProvider( useEffect(() => { if (props.plotConfig.yScale) setYScaleType(props.plotConfig.yScale); }, [props.plotConfig.yScale, setYScaleType]); + useEffect(() => { + const entries = Object.entries(props.selectionOptions ?? {}); + const [k, v] = entries[0] ?? []; + if (k !== undefined && v !== undefined) { + setSelectionType(toSelectionType(k)); + setSelectionMax(v); + } + }, [props.selectionOptions, setSelectionType, setSelectionMax]); + + const newSetSelectionType = useCallback( + (t: SelectionType) => { + setSelectionType(t); + const selectionOptions = props.selectionOptions ?? {}; + const m = selectionOptions[t]; + if (m !== undefined) { + setSelectionMax(m); + } + }, + [setSelectionType, setSelectionMax, props.selectionOptions] + ); const batonProps = useMemo( () => props.batonProps ?? defaultBatonProps, @@ -333,9 +373,12 @@ export function PlotCustomizationContextProvider( dDomain, dCustomDomain, setDCustomDomain, + selectionOptions: props.selectionOptions, + selectionMax, selectionType, - setSelectionType, - setHistogram, + setSelectionType: newSetSelectionType, + histogramGetter, + updateHistogramGetter, selections, setSelections, updateSelection: newUpdateSelection, @@ -352,7 +395,6 @@ export function PlotCustomizationContextProvider( setColourMap, invertColourMap, toggleInvertColourMap, - histogram, scatterPointSize, setScatterPointSize, showPoints, @@ -370,12 +412,16 @@ export function PlotCustomizationContextProvider( dCustomDomain, dDomain, dScaleType, - histogram, + histogramGetter, + updateHistogramGetter, invertColourMap, mode, newUpdateSelection, + newSetSelectionType, plotType, + props.selectionOptions, scatterPointSize, + selectionMax, selectionType, selections, setSelections, diff --git a/client/component/src/PlotToolbar.tsx b/client/component/src/PlotToolbar.tsx index 15490c4f..aed48643 100644 --- a/client/component/src/PlotToolbar.tsx +++ b/client/component/src/PlotToolbar.tsx @@ -30,7 +30,12 @@ import LineKeyDropdown from './LineKeyDropdown'; import type { IIconType } from './Modal'; import Modal from './Modal'; import SelectionTypeDropdown from './SelectionTypeDropdown'; -import { undashSelection, dashSelection } from './selections/utils'; +import { + undashSelection, + dashSelection, + toSelectionType, + SelectionType, +} from './selections/utils'; import SelectionConfig from './SelectionConfig'; import SelectionIDDropdown from './SelectionIDDropdown'; import { InteractionModeType } from './utils'; @@ -178,15 +183,36 @@ function PlotToolbar(props: PropsWithChildren): React.JSX.Element { const bareModals: React.JSX.Element[] = []; + const dropdownOptions = useMemo(() => { + const selectionOptions = value.selectionOptions; + if (selectionOptions === undefined) { + return undefined; + } + return Object.keys(selectionOptions).map((k) => toSelectionType(k)); + }, [value.selectionOptions]); + + const canAddSelection = + dropdownOptions === undefined || dropdownOptions.length !== 0; + if (canSelect && value.selectionType !== undefined) { - bareModals.push( - - ); + let selectionType = value.selectionType; + if ( + selectionType === SelectionType.unknown && + dropdownOptions === undefined + ) { + selectionType = SelectionType.line; + } + if (canAddSelection) { + bareModals.push( + + ); + } } const allLineParams = value.allLineParams; @@ -227,7 +253,7 @@ function PlotToolbar(props: PropsWithChildren): React.JSX.Element { domain: value.dDomain, customDomain: value.dCustomDomain, setCustomDomain: value.setDCustomDomain, - histogram: value.histogram, + histogramGetter: value.histogramGetter, scatterPointSize: isScatter ? value.scatterPointSize : undefined, setScatterPointSize: value.setScatterPointSize, hasBaton, @@ -320,8 +346,11 @@ function PlotToolbar(props: PropsWithChildren): React.JSX.Element { value={value.mode} onModeChange={value.setMode} hasBaton={selectBaton} + canSelect={canAddSelection} /> - {canSelect && } + {canSelect && canAddSelection && ( + + )} {bareModals} {canSelect && ( {selectionConfig} diff --git a/client/component/src/ScatterPlot.tsx b/client/component/src/ScatterPlot.tsx index 73cee899..bebb5094 100644 --- a/client/component/src/ScatterPlot.tsx +++ b/client/component/src/ScatterPlot.tsx @@ -8,7 +8,7 @@ import { import SelectionComponent from './SelectionComponent'; import { - calculateHistogramCounts, + createHistogramParams, createInteractionsConfig, InteractionModeType, } from './utils'; @@ -18,7 +18,7 @@ import { usePlotCustomizationContext, } from './PlotCustomizationContext'; import { AnyToolbar } from './PlotToolbar'; -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; interface Props { x: NDT; @@ -37,6 +37,7 @@ export function ScatterVisCanvas({ x, y, values }: Props) { mode, batonProps, canSelect, + selectionMax, selectionType, updateSelection, selections, @@ -46,24 +47,15 @@ export function ScatterVisCanvas({ x, y, values }: Props) { invertColourMap, dScaleType, scatterPointSize, - setHistogram, + updateHistogramGetter, } = usePlotCustomizationContext(); const interactionsConfig = createInteractionsConfig(mode); - const histogram = useMemo( - () => calculateHistogramCounts(values.data, dDomain), - [dDomain, values] - ); - useEffect(() => { - if (histogram) { - setHistogram({ - ...histogram, - colorMap: colourMap, - invertColorMap: invertColourMap, - }); - } - }, [colourMap, invertColourMap, histogram, setHistogram]); + const hg = () => + createHistogramParams(values.data, dDomain, colourMap, invertColourMap); + updateHistogramGetter(hg); + }, [values.data, dDomain, colourMap, invertColourMap, updateHistogramGetter]); return ( { + if (selectionMax === 0) { + return false; + } + const n = selections.reduce( + (a, v) => a + (getSelectionType(v) === selectionType ? 1 : 0), + 0 + ); + return n >= selectionMax; + }, [selectionMax, selectionType, selections]); + + const clicks = useMemo(() => getClicks(selectionType), [selectionType]); return ( <> - {updateSelection && (batonProps?.hasBaton ?? true) && !disabled && ( - validateHtml(html, selectionType)} - onValidSelection={({ data }) => { - const s = pointsToSelection(selections, selectionType, data, alpha); - return updateSelection(s); - }} - minPoints={clicks[0]} - maxPoints={clicks[1]} - > - {({ html }, _, isValid) => - pointsToShape( - selectionType, - html, - isFlipped, - alpha, - size, - isValid ? undefined : '#cc6677' // orangered, - ) - } - - )} + {updateSelection && + (batonProps?.hasBaton ?? true) && + !disabled && + !maxReached && ( + validateHtml(html, selectionType)} + onValidSelection={({ data }) => { + const s = pointsToSelection( + selections, + selectionType, + data, + alpha + ); + return updateSelection(s); + }} + minPoints={clicks[0]} + maxPoints={clicks[1]} + > + {({ html }, _, isValid) => + pointsToShape( + selectionType, + html, + isFlipped, + alpha, + size, + isValid ? undefined : '#cc6677' // orangered, + ) + } + + )} {shapes} ); diff --git a/client/component/src/SurfacePlot.tsx b/client/component/src/SurfacePlot.tsx index ef52463f..02de4e82 100644 --- a/client/component/src/SurfacePlot.tsx +++ b/client/component/src/SurfacePlot.tsx @@ -14,7 +14,7 @@ import { } from './PlotCustomizationContext'; import { AnyToolbar } from './PlotToolbar'; import { PropsWithChildren, useEffect, useMemo } from 'react'; -import { calculateHistogramCounts } from './utils'; +import { createHistogramParams } from './utils'; interface Props { values: NDT; @@ -30,7 +30,7 @@ export function SurfaceVisCanvas(props: PropsWithChildren) { invertColourMap, dScaleType, showPoints, - setHistogram, + updateHistogramGetter, } = usePlotCustomizationContext(); const visDomain = useMemo( @@ -39,18 +39,10 @@ export function SurfaceVisCanvas(props: PropsWithChildren) { ); useEffect(() => { - const histogram = calculateHistogramCounts(values.data, dDomain); - - if (histogram) { - console.log('Set histogram:', histogram, colourMap, invertColourMap); - - setHistogram({ - ...histogram, - colorMap: colourMap, - invertColorMap: invertColourMap, - }); - } - }, [dDomain, values, colourMap, invertColourMap, setHistogram]); + const hg = () => + createHistogramParams(values.data, dDomain, colourMap, invertColourMap); + updateHistogramGetter(hg); + }, [values.data, dDomain, colourMap, invertColourMap, updateHistogramGetter]); return ( { Line + Line (selection option) + Line (selection options) + Line (no selection options) Heatmap Heatmap (selection) @@ -178,6 +181,27 @@ class AppMain extends React.Component { + +
+ +
+
+ +
+ +
+
+ +
+ +
+