Skip to content

Commit 30425bd

Browse files
authored
Merge pull request #1095 from VEuPathDB/timeslider-date-input
test date input for time slider and adjust input element
2 parents 3316bae + 9366590 commit 30425bd

File tree

6 files changed

+398
-86
lines changed

6 files changed

+398
-86
lines changed

packages/libs/components/src/components/plotControls/AxisRangeControl.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface AxisRangeControlProps
2424
logScale?: boolean;
2525
/** specify step for increment/decrement buttons in MUI number inputs; MUI's default is 1 */
2626
step?: number;
27+
/** specify the height of the input element */
28+
inputHeight?: number;
2729
}
2830

2931
export default function AxisRangeControl({
@@ -36,6 +38,7 @@ export default function AxisRangeControl({
3638
disabled = false,
3739
logScale = false,
3840
step = undefined,
41+
inputHeight,
3942
}: AxisRangeControlProps) {
4043
const validator = useCallback(
4144
(
@@ -79,6 +82,7 @@ export default function AxisRangeControl({
7982
validator={validator}
8083
// add disabled prop to disable input fields
8184
disabled={disabled}
85+
inputHeight={inputHeight}
8286
/>
8387
) : (
8488
<NumberRangeInput
@@ -91,6 +95,7 @@ export default function AxisRangeControl({
9195
// add disabled prop to disable input fields
9296
disabled={disabled}
9397
step={step}
98+
inputHeight={inputHeight}
9499
/>
95100
)
96101
) : null;

packages/libs/components/src/components/plotControls/TimeSlider.tsx

+44-44
Original file line numberDiff line numberDiff line change
@@ -91,46 +91,6 @@ function TimeSlider(props: TimeSliderProps) {
9191
const getXData = (d: TimeSliderDataProp) => new Date(d.x);
9292
const getYData = (d: TimeSliderDataProp) => d.y;
9393

94-
const onBrushChange = useMemo(
95-
() =>
96-
debounce((domain: Bounds | null) => {
97-
if (!domain) return;
98-
const { x0, x1 } = domain;
99-
100-
// computing the offset of 2 pixel (SAFE_PIXEL) in domain (milliseconds)
101-
// https://github.com/airbnb/visx/blob/86a851cb3bf622b013b186f02f955bcd6548a87f/packages/visx-brush/src/Brush.tsx#L14
102-
const brushOffset =
103-
xBrushScale.invert(2).getTime() - xBrushScale.invert(0).getTime();
104-
105-
// compensating the offset
106-
// x0 and x1 are millisecond value
107-
const startDate = millisecondTodate(x0 + brushOffset);
108-
const endDate = millisecondTodate(x1 - brushOffset);
109-
110-
setSelectedRange({
111-
// don't let range go outside the xAxisRange, if provided
112-
start: xAxisRange
113-
? startDate < xAxisRange.start
114-
? xAxisRange.start
115-
: startDate
116-
: startDate,
117-
end: xAxisRange
118-
? endDate > xAxisRange.end
119-
? xAxisRange.end
120-
: endDate
121-
: endDate,
122-
});
123-
}, debounceRateMs),
124-
[setSelectedRange, xAxisRange]
125-
);
126-
127-
// Cancel any pending onBrushChange requests when this component is unmounted
128-
useEffect(() => {
129-
return () => {
130-
onBrushChange.cancel();
131-
};
132-
}, []);
133-
13494
// bounds
13595
const xBrushMax = Math.max(width - margin.left - margin.right, 0);
13696
// take 70 % of given height considering axis tick/tick labels at the bottom
@@ -176,10 +136,50 @@ function TimeSlider(props: TimeSliderProps) {
176136
);
177137

178138
// `brushKey` makes/fakes the brush as a controlled component,
179-
const brushKey = 'not_fake_controlled';
180-
// selectedRange != null
181-
// ? selectedRange.start + ':' + selectedRange.end
182-
// : 'no_brush';
139+
const brushKey =
140+
selectedRange != null
141+
? selectedRange.start + ':' + selectedRange.end
142+
: 'no_brush';
143+
144+
const onBrushChange = useMemo(
145+
() =>
146+
debounce((domain: Bounds | null) => {
147+
if (!domain) return;
148+
const { x0, x1 } = domain;
149+
150+
// computing the offset of 2 pixel (SAFE_PIXEL) in domain (milliseconds)
151+
// https://github.com/airbnb/visx/blob/86a851cb3bf622b013b186f02f955bcd6548a87f/packages/visx-brush/src/Brush.tsx#L14
152+
const brushOffset =
153+
xBrushScale.invert(2).getTime() - xBrushScale.invert(0).getTime();
154+
155+
// compensating the offset
156+
// x0 and x1 are millisecond value
157+
const startDate = millisecondTodate(x0 + brushOffset);
158+
const endDate = millisecondTodate(x1 - brushOffset);
159+
160+
setSelectedRange({
161+
// don't let range go outside the xAxisRange, if provided
162+
start: xAxisRange
163+
? startDate < xAxisRange.start
164+
? xAxisRange.start
165+
: startDate
166+
: startDate,
167+
end: xAxisRange
168+
? endDate > xAxisRange.end
169+
? xAxisRange.end
170+
: endDate
171+
: endDate,
172+
});
173+
}, debounceRateMs),
174+
[setSelectedRange, xAxisRange, debounceRateMs, xBrushScale]
175+
);
176+
177+
// Cancel any pending onBrushChange requests when this component is unmounted
178+
useEffect(() => {
179+
return () => {
180+
onBrushChange.cancel();
181+
};
182+
}, [onBrushChange]);
183183

184184
return (
185185
<div

packages/libs/components/src/components/widgets/NumberAndDateInputs.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ type BaseProps<M extends NumberOrDate> = {
3535
disabled?: boolean;
3636
/** Style the Text Field with the warning color and bold stroke */
3737
applyWarningStyles?: boolean;
38+
/** specify the height of the input element */
39+
inputHeight?: number;
3840
};
3941

4042
export type NumberInputProps = BaseProps<number> & { step?: number };
@@ -86,6 +88,8 @@ function BaseInput({
8688
displayRangeViolationWarnings = true,
8789
disabled = false,
8890
applyWarningStyles = false,
91+
// default value is 36.5
92+
inputHeight = 36.5,
8993
...props
9094
}: BaseInputProps) {
9195
if (validator && (required || minValue != null || maxValue != null))
@@ -102,7 +106,7 @@ function BaseInput({
102106

103107
const classes = makeStyles({
104108
root: {
105-
height: 36.5, // default height is 56 and is waaaay too tall
109+
height: inputHeight, // default height is 56 and is waaaay too tall
106110
// 34.5 is the height of the reset button, but 36.5 lines up better
107111
// set width for date
108112
width: valueType === 'date' ? 165 : '',

packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export type BaseProps<M extends NumberOrDateRange> = {
4343
clearButtonLabel?: string;
4444
/** add disabled prop to disable input fields */
4545
disabled?: boolean;
46+
/** specify the height of the input element */
47+
inputHeight?: number;
4648
};
4749

4850
export type NumberRangeInputProps = BaseProps<NumberRange> & { step?: number };
@@ -86,6 +88,7 @@ function BaseInput({
8688
clearButtonLabel = 'Clear',
8789
// add disabled prop to disable input fields
8890
disabled = false,
91+
inputHeight,
8992
...props
9093
}: BaseInputProps) {
9194
if (validator && required)
@@ -175,7 +178,9 @@ function BaseInput({
175178
{label}
176179
</Typography>
177180
)}
178-
<div style={{ display: 'flex', flexDirection: 'row' }}>
181+
<div
182+
style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}
183+
>
179184
{valueType === 'number' ? (
180185
<NumberInput
181186
value={min as number}
@@ -192,6 +197,7 @@ function BaseInput({
192197
// add disabled prop to disable input fields
193198
disabled={disabled}
194199
step={step}
200+
inputHeight={inputHeight}
195201
/>
196202
) : (
197203
<DateInput
@@ -208,6 +214,7 @@ function BaseInput({
208214
}}
209215
// add disabled prop to disable input fields
210216
disabled={disabled}
217+
inputHeight={inputHeight}
211218
/>
212219
)}
213220
<div style={{ display: 'flex', flexDirection: 'row' }}>
@@ -241,6 +248,7 @@ function BaseInput({
241248
// add disabled prop to disable input fields
242249
disabled={disabled}
243250
step={step}
251+
inputHeight={inputHeight}
244252
/>
245253
) : (
246254
<DateInput
@@ -257,6 +265,7 @@ function BaseInput({
257265
}}
258266
// add disabled prop to disable input fields
259267
disabled={disabled}
268+
inputHeight={inputHeight}
260269
/>
261270
)}
262271
{showClearButton && (

packages/libs/components/src/stories/plotControls/TimeSlider.stories.tsx

+92-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { useState } from 'react';
1+
import { useState, useCallback } from 'react';
22
import { Story, Meta } from '@storybook/react/types-6-0';
33
import { LinePlotProps } from '../../plots/LinePlot';
44
import TimeSlider, {
55
TimeSliderDataProp,
66
} from '../../components/plotControls/TimeSlider';
77
import { DraggablePanel } from '@veupathdb/coreui/lib/components/containers';
88

9+
import AxisRangeControl from '../../components/plotControls/AxisRangeControl';
10+
import { NumberOrDateRange } from '../../types/general';
11+
912
export default {
1013
title: 'Plot Controls/TimeSlider',
1114
component: TimeSlider,
@@ -301,7 +304,56 @@ export const TimeFilter: Story<LinePlotProps> = (args: any) => {
301304

302305
// set constant values
303306
const defaultSymbolSize = 0.8;
304-
const defaultColor = '#333';
307+
308+
// control selectedRange
309+
const handleAxisRangeChange = useCallback(
310+
(newRange?: NumberOrDateRange) => {
311+
if (newRange)
312+
setSelectedRange({
313+
start: newRange.min as string,
314+
end: newRange.max as string,
315+
});
316+
},
317+
[setSelectedRange]
318+
);
319+
320+
const handleArrowClick = useCallback(
321+
(arrow: string) => {
322+
// let's assume that selectedRange has the format of 'yyyy-mm-dd'
323+
if (
324+
selectedRange &&
325+
selectedRange.start != null &&
326+
selectedRange.end != null
327+
) {
328+
const selectedRangeArray =
329+
arrow === 'left'
330+
? selectedRange.start.split('-')
331+
: selectedRange.end.split('-');
332+
const addSubtractYear =
333+
arrow === 'left'
334+
? String(Number(selectedRangeArray[0]) - 1)
335+
: String(Number(selectedRangeArray[0]) + 1);
336+
const changeYear =
337+
addSubtractYear +
338+
'-' +
339+
selectedRangeArray[1] +
340+
'-' +
341+
selectedRangeArray[2];
342+
setSelectedRange((prev) => {
343+
return arrow === 'left'
344+
? {
345+
start: changeYear as string,
346+
end: prev?.end as string,
347+
}
348+
: {
349+
start: prev?.start as string,
350+
end: changeYear as string,
351+
};
352+
});
353+
}
354+
},
355+
[selectedRange, setSelectedRange]
356+
);
305357

306358
return (
307359
<DraggablePanel
@@ -322,16 +374,47 @@ export const TimeFilter: Story<LinePlotProps> = (args: any) => {
322374
>
323375
<div
324376
style={{
325-
display: 'grid',
326-
gridTemplateColumns: '1fr repeat(1, auto) 1fr',
327-
gridColumnGap: '5px',
377+
display: 'flex',
378+
flexDirection: 'row',
379+
alignItems: 'center',
328380
justifyContent: 'center',
329-
paddingTop: '1em',
330381
}}
331382
>
332-
{/* display start to end value */}
333-
<div style={{ gridColumnStart: 2 }}>
334-
{selectedRange?.start} ~ {selectedRange?.end}
383+
<div>
384+
<button
385+
style={{ marginRight: '1em' }}
386+
onClick={() => handleArrowClick('left')}
387+
>
388+
<i className="fa fa-arrow-left" aria-hidden="true"></i>
389+
</button>
390+
</div>
391+
{/* add axis range control */}
392+
<AxisRangeControl
393+
range={
394+
selectedRange != null
395+
? {
396+
min: selectedRange.start,
397+
max: selectedRange.end,
398+
}
399+
: undefined
400+
}
401+
onRangeChange={handleAxisRangeChange}
402+
valueType={'date'}
403+
// set maxWidth
404+
containerStyles={{
405+
maxWidth: '350px',
406+
}}
407+
// default height of the input element is 36.5 which may be too high for timeSlider
408+
// thus, introduced new prop to control it
409+
inputHeight={20}
410+
/>
411+
<div>
412+
<button
413+
style={{ marginLeft: '2em' }}
414+
onClick={() => handleArrowClick('right')}
415+
>
416+
<i className="fa fa-arrow-right" aria-hidden="true"></i>
417+
</button>
335418
</div>
336419
</div>
337420
<TimeSlider

0 commit comments

Comments
 (0)