Skip to content

Commit

Permalink
Merge pull request #1667 from silx-kit/dyn-deb
Browse files Browse the repository at this point in the history
Decrease debounce delay dynamically on slices that are already cached
  • Loading branch information
axelboc authored Jun 18, 2024
2 parents b5c0353 + d7aec97 commit 14028fb
Show file tree
Hide file tree
Showing 20 changed files with 197 additions and 76 deletions.
27 changes: 13 additions & 14 deletions packages/app/src/dimension-mapper/AxisMapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ import type { DimensionMapping } from './models';

interface Props {
axis: Axis;
rawDims: number[];
axisLabels: AxisMapping<string> | undefined;
mapperState: DimensionMapping;
dimMapping: DimensionMapping;
onChange: (mapperState: DimensionMapping) => void;
}

function AxisMapper(props: Props) {
const { axis, rawDims, axisLabels, mapperState, onChange } = props;
const selectedDim = mapperState.indexOf(axis);
const { axis, axisLabels, dimMapping, onChange } = props;
const selectedDim = dimMapping.indexOf(axis);

if (selectedDim === -1) {
return null;
Expand All @@ -31,23 +30,23 @@ function AxisMapper(props: Props) {
onChange={(val) => {
const newDim = Number(val);
if (selectedDim !== newDim) {
const newMapperState = [...mapperState];
const newMapping = [...dimMapping];

// Invert mappings or reset slicing index of previously selected dimension
newMapperState[selectedDim] =
typeof mapperState[newDim] === 'number' ? 0 : mapperState[newDim];
newMapperState[newDim] = axis; // assign axis to newly selected dimension
newMapping[selectedDim] =
typeof dimMapping[newDim] === 'number' ? 0 : dimMapping[newDim];
newMapping[newDim] = axis; // assign axis to newly selected dimension

onChange(newMapperState);
onChange(newMapping);
}
}}
>
{Object.keys(rawDims).map((dimKey, index) => (
{dimMapping.map((_, i) => (
<ToggleGroup.Btn
key={dimKey}
label={`D${dimKey}`}
value={dimKey}
hint={axisLabels?.[index]}
key={i} // eslint-disable-line react/no-array-index-key
label={`D${i}`}
value={i.toString()}
hint={axisLabels?.[i]}
/>
))}
</ToggleGroup>
Expand Down
40 changes: 24 additions & 16 deletions packages/app/src/dimension-mapper/DimensionMapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import type { DimensionMapping } from './models';
import SlicingSlider from './SlicingSlider';

interface Props {
rawDims: number[];
dims: number[];
axisLabels?: AxisMapping<string>;
mapperState: DimensionMapping;
dimMapping: DimensionMapping;
isCached?: (dimMapping: DimensionMapping) => boolean;
onChange: (d: DimensionMapping) => void;
}

function DimensionMapper(props: Props) {
const { rawDims, axisLabels, mapperState, onChange } = props;
const { dims, axisLabels, dimMapping, isCached, onChange } = props;
const mappableDims = dims.slice(0, dimMapping.length);

return (
<div className={styles.mapper}>
Expand All @@ -22,41 +24,47 @@ function DimensionMapper(props: Props) {
<span className={styles.dimsLabel}>
<abbr title="Number of elements in each dimension">n</abbr>
</span>
{rawDims.map((d, i) => (
{mappableDims.map((d, i) => (
// eslint-disable-next-line react/no-array-index-key
<span key={`${i}${d}`} className={styles.dimSize}>
<span key={i} className={styles.dimSize}>
{' '}
{d}
</span>
))}
</div>
<AxisMapper
axis="x"
rawDims={rawDims}
axisLabels={axisLabels}
mapperState={mapperState}
dimMapping={dimMapping}
onChange={onChange}
/>
<AxisMapper
axis="y"
rawDims={rawDims}
axisLabels={axisLabels}
mapperState={mapperState}
dimMapping={dimMapping}
onChange={onChange}
/>
</div>
<div className={styles.sliders}>
{mapperState.map((val, index) =>
{dimMapping.map((val, index) =>
typeof val === 'number' ? (
<SlicingSlider
key={`${index}`} // eslint-disable-line react/no-array-index-key
key={index} // eslint-disable-line react/no-array-index-key
dimension={index}
maxIndex={rawDims[index] - 1}
length={dims[index]}
initialValue={val}
onChange={(newVal: number) => {
const newMapperState = [...mapperState];
newMapperState[index] = newVal;
onChange(newMapperState);
isFastSlice={
isCached &&
((newVal) => {
const newMapping = [...dimMapping];
newMapping[index] = newVal;
return isCached(newMapping);
})
}
onChange={(newVal) => {
const newMapping = [...dimMapping];
newMapping[index] = newVal;
onChange(newMapping);
}}
/>
) : undefined,
Expand Down
23 changes: 13 additions & 10 deletions packages/app/src/dimension-mapper/SlicingSlider.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
import { useDebouncedCallback, useMeasure } from '@react-hookz/web';
import { useMeasure } from '@react-hookz/web';
import { useState } from 'react';
import ReactSlider from 'react-slider';

import styles from './SlicingSlider.module.css';
import { useDynamicDebouncedCallback } from './utils';

const ID = 'h5w-slider';
const MIN_HEIGHT_PER_MARK = 25;
const SLICING_DEBOUNCE_DELAY = 250;
const SHORT_DELAY = 20;
const LONG_DELAY = 250;

interface Props {
dimension: number;
maxIndex: number;
length: number;
initialValue: number;
isFastSlice?: (value: number) => boolean;
onChange: (value: number) => void;
}

function SlicingSlider(props: Props) {
const { dimension, maxIndex, initialValue, onChange } = props;
const { dimension, length, initialValue, isFastSlice, onChange } = props;

const [value, setValue] = useState(initialValue);
const onDebouncedChange = useDebouncedCallback(
const onDebouncedChange = useDynamicDebouncedCallback(
onChange,
[onChange],
SLICING_DEBOUNCE_DELAY,
(val) => (isFastSlice?.(val) ? SHORT_DELAY : LONG_DELAY),
);

const [containerSize, containerRef] = useMeasure<HTMLDivElement>();
Expand All @@ -33,17 +36,17 @@ function SlicingSlider(props: Props) {
<span id={sliderLabelId} className={styles.label}>
D{dimension}
</span>
<span className={styles.sublabel}>0:{maxIndex}</span>
{maxIndex > 0 ? (
<span className={styles.sublabel}>0:{length - 1}</span>
{length > 1 ? (
<ReactSlider
className={styles.slider}
ariaLabelledby={sliderLabelId}
min={0}
max={maxIndex}
max={length - 1}
step={1}
marks={
containerSize &&
containerSize.height / (maxIndex + 1) >= MIN_HEIGHT_PER_MARK
containerSize.height / length >= MIN_HEIGHT_PER_MARK
}
markClassName={styles.mark}
orientation="vertical"
Expand Down
57 changes: 57 additions & 0 deletions packages/app/src/dimension-mapper/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,62 @@
import type { Axis } from '@h5web/shared/vis-models';
import { useSyncedRef, useUnmountEffect } from '@react-hookz/web';
import type { DependencyList } from 'react';
import { useEffect, useMemo, useRef } from 'react';

export function isAxis(elem: number | Axis): elem is Axis {
return typeof elem !== 'number';
}

// Debounced callback with a delay that can be adjusted on every invocation
export function useDynamicDebouncedCallback<
Fn extends (...args: any[]) => void, // eslint-disable-line @typescript-eslint/no-explicit-any
>(
callback: Fn,
deps: DependencyList,
getDelay: (...args: Parameters<Fn>) => number,
): (...args: Parameters<Fn>) => void {
const timeout = useRef<ReturnType<typeof setTimeout>>();
const cb = useRef(callback);
const getDelayRef = useSyncedRef(getDelay);
const lastCallArgs = useRef<Parameters<Fn>>();

function clear() {
if (timeout.current) {
clearTimeout(timeout.current);
timeout.current = undefined;
}
}

// Cancel scheduled execution on unmount
useUnmountEffect(clear);

useEffect(() => {
cb.current = callback;
}, deps); // eslint-disable-line react-hooks/exhaustive-deps

return useMemo(() => {
function execute() {
clear();

if (!lastCallArgs.current) {
return;
}

const context = lastCallArgs.current;
lastCallArgs.current = undefined;

cb.current(...context);
}

return (...args) => {
if (timeout.current) {
clearTimeout(timeout.current);
}

lastCallArgs.current = args;

// Plan regular execution
timeout.current = setTimeout(execute, getDelayRef.current(...args));
};
}, deps); // eslint-disable-line react-hooks/exhaustive-deps
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import { useValuesInCache } from '../hooks';
import { useLineConfig } from '../line/config';
import { getSliceSelection } from '../utils';
import ValueFetcher from '../ValueFetcher';
Expand All @@ -30,8 +31,9 @@ function ComplexLineVisContainer(props: VisContainerProps) {
return (
<>
<DimensionMapper
rawDims={dims}
mapperState={dimMapping}
dims={dims}
dimMapping={dimMapping}
isCached={useValuesInCache(entity)}
onChange={setDimMapping}
/>
<VisBoundary resetKey={dimMapping} isSlice={selection !== undefined}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useDimMappingState } from '../../../dimension-mapper/hooks';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import { useHeatmapConfig } from '../heatmap/config';
import { useValuesInCache } from '../hooks';
import { getSliceSelection } from '../utils';
import ValueFetcher from '../ValueFetcher';
import { useComplexConfig } from './config';
Expand All @@ -32,8 +33,9 @@ function ComplexVisContainer(props: VisContainerProps) {
return (
<>
<DimensionMapper
rawDims={dims}
mapperState={dimMapping}
dims={dims}
dimMapping={dimMapping}
isCached={useValuesInCache(entity)}
onChange={setDimMapping}
/>
<VisBoundary resetKey={dimMapping} isSlice={selection !== undefined}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import { useValuesInCache } from '../hooks';
import { useMatrixConfig } from '../matrix/config';
import { getSliceSelection } from '../utils';
import ValueFetcher from '../ValueFetcher';
Expand All @@ -30,8 +31,9 @@ function CompoundMatrixVisContainer(props: VisContainerProps) {
return (
<>
<DimensionMapper
rawDims={dims}
mapperState={dimMapping}
dims={dims}
dimMapping={dimMapping}
isCached={useValuesInCache(entity)}
onChange={setDimMapping}
/>
<VisBoundary isSlice={selection !== undefined}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import { useIgnoreFillValue } from '../hooks';
import { useIgnoreFillValue, useValuesInCache } from '../hooks';
import { getSliceSelection } from '../utils';
import ValueFetcher from '../ValueFetcher';
import { useHeatmapConfig } from './config';
Expand All @@ -27,14 +27,15 @@ function HeatmapVisContainer(props: VisContainerProps) {

const config = useHeatmapConfig();

const ignoreValue = useIgnoreFillValue(entity);
const selection = getSliceSelection(dimMapping);
const ignoreValue = useIgnoreFillValue(entity);

return (
<>
<DimensionMapper
rawDims={dims}
mapperState={dimMapping}
dims={dims}
dimMapping={dimMapping}
isCached={useValuesInCache(entity)}
onChange={setDimMapping}
/>
<VisBoundary resetKey={dimMapping} isSlice={selection !== undefined}>
Expand Down
19 changes: 18 additions & 1 deletion packages/app/src/vis-packs/core/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,27 @@ import type { DimensionMapping } from '../../dimension-mapper/models';
import { isAxis } from '../../dimension-mapper/utils';
import { useDataContext } from '../../providers/DataProvider';
import { typedArrayFromDType } from '../../providers/utils';
import { applyMapping, getBaseArray, toNumArray } from './utils';
import {
applyMapping,
getBaseArray,
getSliceSelection,
toNumArray,
} from './utils';

export const useToNumArray = createMemo(toNumArray);

export function useValuesInCache(
...datasets: (Dataset<ArrayShape> | undefined)[]
): (dimMapping: DimensionMapping) => boolean {
const { valuesStore } = useDataContext();
return (dimMapping) => {
const selection = getSliceSelection(dimMapping);
return datasets.every(
(dataset) => !dataset || valuesStore.has({ dataset, selection }),
);
};
}

export function usePrefetchValues(
datasets: (Dataset<ScalarShape | ArrayShape> | undefined)[],
selection?: string,
Expand Down
7 changes: 4 additions & 3 deletions packages/app/src/vis-packs/core/line/LineVisContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import { useDimMappingState } from '../../../dimension-mapper/hooks';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import { useIgnoreFillValue } from '../hooks';
import { useIgnoreFillValue, useValuesInCache } from '../hooks';
import { getSliceSelection } from '../utils';
import ValueFetcher from '../ValueFetcher';
import { useLineConfig } from './config';
Expand All @@ -30,8 +30,9 @@ function LineVisContainer(props: VisContainerProps) {
return (
<>
<DimensionMapper
rawDims={dims}
mapperState={dimMapping}
dims={dims}
dimMapping={dimMapping}
isCached={useValuesInCache(entity)}
onChange={setDimMapping}
/>
<VisBoundary resetKey={dimMapping} isSlice={selection !== undefined}>
Expand Down
Loading

0 comments on commit 14028fb

Please sign in to comment.