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
2 changes: 2 additions & 0 deletions client/component/src/HeatmapPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function HeatmapVisCanvas({ xValues, yValues, values }: Props) {
aspect,
batonProps,
canSelect,
selectionMax,
selectionType,
updateSelection,
selections,
Expand Down Expand Up @@ -90,6 +91,7 @@ export function HeatmapVisCanvas({ xValues, yValues, values }: Props) {
<SelectionComponent
modifierKey={[] as ModifierKey[]}
disabled={mode !== InteractionModeType.selectRegion}
selectionMax={selectionMax}
selectionType={selectionType}
batonProps={batonProps}
updateSelection={updateSelection}
Expand Down
2 changes: 2 additions & 0 deletions client/component/src/ImagePlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function ImageVisCanvas({ xValues, yValues, values }: Props) {
aspect,
batonProps,
canSelect,
selectionMax,
selectionType,
updateSelection,
selections,
Expand Down Expand Up @@ -56,6 +57,7 @@ export function ImageVisCanvas({ xValues, yValues, values }: Props) {
<SelectionComponent
modifierKey={[] as ModifierKey[]}
disabled={mode !== InteractionModeType.selectRegion}
selectionMax={selectionMax}
selectionType={selectionType}
batonProps={batonProps}
updateSelection={updateSelection}
Expand Down
37 changes: 21 additions & 16 deletions client/component/src/InteractionModeToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface InteractionModeToggleProps {
onModeChange: (value: InteractionModeType) => void;
/** If client holds baton */
hasBaton: boolean;
canSelect: boolean;
}

/**
Expand All @@ -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) {
Expand Down Expand Up @@ -54,21 +55,25 @@ function InteractionModeToggle(props: InteractionModeToggleProps) {
icon={TbZoomInArea as IIconType}
value={InteractionModeType.selectToZoom}
/>
<div // wrapper hack to add tooltip (note corners are not correctly drawn for this last child)
style={{
pointerEvents: hasBaton ? 'inherit' : 'auto',
display: 'inline-flex',
}}
title={hasBaton ? '' : 'need baton'}
>
<ToggleGroup.Btn
label="select region"
iconOnly
icon={IoShapesOutline as IIconType}
value={InteractionModeType.selectRegion}
disabled={!hasBaton}
/>
</div>
{canSelect ? (
<div // wrapper hack to add tooltip (note corners are not correctly drawn for this last child)
style={{
pointerEvents: hasBaton ? 'inherit' : 'auto',
display: 'inline-flex',
}}
title={hasBaton ? '' : 'need baton'}
>
<ToggleGroup.Btn
label="select region"
iconOnly
icon={IoShapesOutline as IIconType}
value={InteractionModeType.selectRegion}
disabled={!hasBaton}
/>
</div>
) : (
<></>
)}
</ToggleGroup>
</>
);
Expand Down
2 changes: 2 additions & 0 deletions client/component/src/LinePlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export function LineVisCanvas(props: Props) {
setCurrentLineKey,
batonProps,
canSelect,
selectionMax,
selectionType,
updateSelection,
selections,
Expand Down Expand Up @@ -209,6 +210,7 @@ export function LineVisCanvas(props: Props) {
modifierKey={[] as ModifierKey[]}
batonProps={batonProps}
disabled={mode !== InteractionModeType.selectRegion}
selectionMax={selectionMax}
selectionType={selectionType}
updateSelection={updateSelection}
selections={selections}
Expand Down
46 changes: 42 additions & 4 deletions client/component/src/PlotCustomizationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 */
Expand All @@ -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 */
Expand Down Expand Up @@ -170,8 +182,9 @@ export function PlotCustomizationContextProvider(
InteractionModeType.panAndWheelZoom
);
const [selectionType, setSelectionType] = useState<SelectionType>(
SelectionType.line
SelectionType.unknown
);
const [selectionMax, setSelectionMax] = useState<number>(0);
const [xCustomDomain, setXCustomDomain] = useState<CustomDomain>([
null,
null,
Expand Down Expand Up @@ -218,6 +231,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,
Expand Down Expand Up @@ -333,8 +366,10 @@ export function PlotCustomizationContextProvider(
dDomain,
dCustomDomain,
setDCustomDomain,
selectionOptions: props.selectionOptions,
selectionMax,
selectionType,
setSelectionType,
setSelectionType: newSetSelectionType,
setHistogram,
selections,
setSelections,
Expand Down Expand Up @@ -374,8 +409,11 @@ export function PlotCustomizationContextProvider(
invertColourMap,
mode,
newUpdateSelection,
newSetSelectionType,
plotType,
props.selectionOptions,
scatterPointSize,
selectionMax,
selectionType,
selections,
setSelections,
Expand Down
45 changes: 36 additions & 9 deletions client/component/src/PlotToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -178,15 +183,36 @@ function PlotToolbar(props: PropsWithChildren): JSX.Element {

const bareModals: 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(
<SelectionTypeDropdown
key="Selection type"
value={value.selectionType}
onSelectionTypeChange={value.setSelectionType}
disabled={value.mode !== InteractionModeType.selectRegion}
/>
);
let selectionType = value.selectionType;
if (
selectionType === SelectionType.unknown &&
dropdownOptions === undefined
) {
selectionType = SelectionType.line;
}
if (canAddSelection) {
bareModals.push(
<SelectionTypeDropdown
key="Selection type"
value={selectionType}
onSelectionTypeChange={value.setSelectionType}
disabled={value.mode !== InteractionModeType.selectRegion}
options={dropdownOptions}
/>
);
}
}

const allLineParams = value.allLineParams;
Expand Down Expand Up @@ -320,6 +346,7 @@ function PlotToolbar(props: PropsWithChildren): JSX.Element {
value={value.mode}
onModeChange={value.setMode}
hasBaton={selectBaton}
canSelect={canAddSelection}
/>
{canSelect && <Separator key="Interaction separator" />}
{bareModals}
Expand Down
2 changes: 2 additions & 0 deletions client/component/src/ScatterPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function ScatterVisCanvas({ x, y, values }: Props) {
mode,
batonProps,
canSelect,
selectionMax,
selectionType,
updateSelection,
selections,
Expand Down Expand Up @@ -91,6 +92,7 @@ export function ScatterVisCanvas({ x, y, values }: Props) {
<SelectionComponent
modifierKey={[] as ModifierKey[]}
disabled={mode !== InteractionModeType.selectRegion}
selectionMax={selectionMax}
selectionType={selectionType}
batonProps={batonProps}
updateSelection={updateSelection}
Expand Down
71 changes: 47 additions & 24 deletions client/component/src/SelectionComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import MulticlickSelectionTool from './MulticlickSelectionTool';
import {
SelectionType,
getClicks,
getSelectionType,
makeShapes,
pointsToSelection,
pointsToShape,
Expand All @@ -18,6 +19,8 @@ import {
* Props for the `SelectionComponent` component.
*/
interface SelectionComponentProps extends PlotSelectionProps {
/** Maximum number of current type */
selectionMax: number;
/** The selection type (optional) */
selectionType?: SelectionType;
/** The modifier key(s) */
Expand All @@ -34,6 +37,7 @@ interface SelectionComponentProps extends PlotSelectionProps {
function SelectionComponent(props: SelectionComponentProps) {
const {
disabled = false,
selectionMax = 0,
selectionType = SelectionType.rectangle,
selections = [],
updateSelection,
Expand Down Expand Up @@ -61,33 +65,52 @@ function SelectionComponent(props: SelectionComponentProps) {
return [v.x < 0, v.y < 0] as [boolean, boolean];
}, [camera, dataToHtml]);

const clicks = getClicks(selectionType);
const maxReached = useMemo(() => {
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 && (
<MulticlickSelectionTool
modifierKey={props.modifierKey}
validate={({ html }) => 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,
)
}
</MulticlickSelectionTool>
)}
{updateSelection &&
(batonProps?.hasBaton ?? true) &&
!disabled &&
!maxReached && (
<MulticlickSelectionTool
modifierKey={props.modifierKey}
validate={({ html }) => 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,
)
}
</MulticlickSelectionTool>
)}
{shapes}
</>
);
Expand Down
Loading