diff --git a/packages/libs/components/src/components/plotControls/AxisRangeControl.tsx b/packages/libs/components/src/components/plotControls/AxisRangeControl.tsx
index ccffa18c62..0853cdfae3 100755
--- a/packages/libs/components/src/components/plotControls/AxisRangeControl.tsx
+++ b/packages/libs/components/src/components/plotControls/AxisRangeControl.tsx
@@ -24,6 +24,8 @@ export interface AxisRangeControlProps
logScale?: boolean;
/** specify step for increment/decrement buttons in MUI number inputs; MUI's default is 1 */
step?: number;
+ /** specify the height of the input element */
+ inputHeight?: number;
}
export default function AxisRangeControl({
@@ -36,6 +38,7 @@ export default function AxisRangeControl({
disabled = false,
logScale = false,
step = undefined,
+ inputHeight,
}: AxisRangeControlProps) {
const validator = useCallback(
(
@@ -79,6 +82,7 @@ export default function AxisRangeControl({
validator={validator}
// add disabled prop to disable input fields
disabled={disabled}
+ inputHeight={inputHeight}
/>
) : (
)
) : null;
diff --git a/packages/libs/components/src/components/plotControls/TimeSlider.tsx b/packages/libs/components/src/components/plotControls/TimeSlider.tsx
index 4931760d91..6312cdff73 100755
--- a/packages/libs/components/src/components/plotControls/TimeSlider.tsx
+++ b/packages/libs/components/src/components/plotControls/TimeSlider.tsx
@@ -91,46 +91,6 @@ function TimeSlider(props: TimeSliderProps) {
const getXData = (d: TimeSliderDataProp) => new Date(d.x);
const getYData = (d: TimeSliderDataProp) => d.y;
- const onBrushChange = useMemo(
- () =>
- debounce((domain: Bounds | null) => {
- if (!domain) return;
- const { x0, x1 } = domain;
-
- // computing the offset of 2 pixel (SAFE_PIXEL) in domain (milliseconds)
- // https://github.com/airbnb/visx/blob/86a851cb3bf622b013b186f02f955bcd6548a87f/packages/visx-brush/src/Brush.tsx#L14
- const brushOffset =
- xBrushScale.invert(2).getTime() - xBrushScale.invert(0).getTime();
-
- // compensating the offset
- // x0 and x1 are millisecond value
- const startDate = millisecondTodate(x0 + brushOffset);
- const endDate = millisecondTodate(x1 - brushOffset);
-
- setSelectedRange({
- // don't let range go outside the xAxisRange, if provided
- start: xAxisRange
- ? startDate < xAxisRange.start
- ? xAxisRange.start
- : startDate
- : startDate,
- end: xAxisRange
- ? endDate > xAxisRange.end
- ? xAxisRange.end
- : endDate
- : endDate,
- });
- }, debounceRateMs),
- [setSelectedRange, xAxisRange]
- );
-
- // Cancel any pending onBrushChange requests when this component is unmounted
- useEffect(() => {
- return () => {
- onBrushChange.cancel();
- };
- }, []);
-
// bounds
const xBrushMax = Math.max(width - margin.left - margin.right, 0);
// take 70 % of given height considering axis tick/tick labels at the bottom
@@ -176,10 +136,50 @@ function TimeSlider(props: TimeSliderProps) {
);
// `brushKey` makes/fakes the brush as a controlled component,
- const brushKey = 'not_fake_controlled';
- // selectedRange != null
- // ? selectedRange.start + ':' + selectedRange.end
- // : 'no_brush';
+ const brushKey =
+ selectedRange != null
+ ? selectedRange.start + ':' + selectedRange.end
+ : 'no_brush';
+
+ const onBrushChange = useMemo(
+ () =>
+ debounce((domain: Bounds | null) => {
+ if (!domain) return;
+ const { x0, x1 } = domain;
+
+ // computing the offset of 2 pixel (SAFE_PIXEL) in domain (milliseconds)
+ // https://github.com/airbnb/visx/blob/86a851cb3bf622b013b186f02f955bcd6548a87f/packages/visx-brush/src/Brush.tsx#L14
+ const brushOffset =
+ xBrushScale.invert(2).getTime() - xBrushScale.invert(0).getTime();
+
+ // compensating the offset
+ // x0 and x1 are millisecond value
+ const startDate = millisecondTodate(x0 + brushOffset);
+ const endDate = millisecondTodate(x1 - brushOffset);
+
+ setSelectedRange({
+ // don't let range go outside the xAxisRange, if provided
+ start: xAxisRange
+ ? startDate < xAxisRange.start
+ ? xAxisRange.start
+ : startDate
+ : startDate,
+ end: xAxisRange
+ ? endDate > xAxisRange.end
+ ? xAxisRange.end
+ : endDate
+ : endDate,
+ });
+ }, debounceRateMs),
+ [setSelectedRange, xAxisRange, debounceRateMs, xBrushScale]
+ );
+
+ // Cancel any pending onBrushChange requests when this component is unmounted
+ useEffect(() => {
+ return () => {
+ onBrushChange.cancel();
+ };
+ }, [onBrushChange]);
return (
= {
disabled?: boolean;
/** Style the Text Field with the warning color and bold stroke */
applyWarningStyles?: boolean;
+ /** specify the height of the input element */
+ inputHeight?: number;
};
export type NumberInputProps = BaseProps
& { step?: number };
@@ -86,6 +88,8 @@ function BaseInput({
displayRangeViolationWarnings = true,
disabled = false,
applyWarningStyles = false,
+ // default value is 36.5
+ inputHeight = 36.5,
...props
}: BaseInputProps) {
if (validator && (required || minValue != null || maxValue != null))
@@ -102,7 +106,7 @@ function BaseInput({
const classes = makeStyles({
root: {
- height: 36.5, // default height is 56 and is waaaay too tall
+ height: inputHeight, // default height is 56 and is waaaay too tall
// 34.5 is the height of the reset button, but 36.5 lines up better
// set width for date
width: valueType === 'date' ? 165 : '',
diff --git a/packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx b/packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx
index cdc625cb07..d1d6159b0e 100755
--- a/packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx
+++ b/packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx
@@ -43,6 +43,8 @@ export type BaseProps = {
clearButtonLabel?: string;
/** add disabled prop to disable input fields */
disabled?: boolean;
+ /** specify the height of the input element */
+ inputHeight?: number;
};
export type NumberRangeInputProps = BaseProps & { step?: number };
@@ -86,6 +88,7 @@ function BaseInput({
clearButtonLabel = 'Clear',
// add disabled prop to disable input fields
disabled = false,
+ inputHeight,
...props
}: BaseInputProps) {
if (validator && required)
@@ -175,7 +178,9 @@ function BaseInput({
{label}
)}
-
+
{valueType === 'number' ? (
) : (
)}
@@ -241,6 +248,7 @@ function BaseInput({
// add disabled prop to disable input fields
disabled={disabled}
step={step}
+ inputHeight={inputHeight}
/>
) : (
)}
{showClearButton && (
diff --git a/packages/libs/components/src/stories/plotControls/TimeSlider.stories.tsx b/packages/libs/components/src/stories/plotControls/TimeSlider.stories.tsx
index eaf144135b..3a230f73b6 100755
--- a/packages/libs/components/src/stories/plotControls/TimeSlider.stories.tsx
+++ b/packages/libs/components/src/stories/plotControls/TimeSlider.stories.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useCallback } from 'react';
import { Story, Meta } from '@storybook/react/types-6-0';
import { LinePlotProps } from '../../plots/LinePlot';
import TimeSlider, {
@@ -6,6 +6,9 @@ import TimeSlider, {
} from '../../components/plotControls/TimeSlider';
import { DraggablePanel } from '@veupathdb/coreui/lib/components/containers';
+import AxisRangeControl from '../../components/plotControls/AxisRangeControl';
+import { NumberOrDateRange } from '../../types/general';
+
export default {
title: 'Plot Controls/TimeSlider',
component: TimeSlider,
@@ -301,7 +304,56 @@ export const TimeFilter: Story
= (args: any) => {
// set constant values
const defaultSymbolSize = 0.8;
- const defaultColor = '#333';
+
+ // control selectedRange
+ const handleAxisRangeChange = useCallback(
+ (newRange?: NumberOrDateRange) => {
+ if (newRange)
+ setSelectedRange({
+ start: newRange.min as string,
+ end: newRange.max as string,
+ });
+ },
+ [setSelectedRange]
+ );
+
+ const handleArrowClick = useCallback(
+ (arrow: string) => {
+ // let's assume that selectedRange has the format of 'yyyy-mm-dd'
+ if (
+ selectedRange &&
+ selectedRange.start != null &&
+ selectedRange.end != null
+ ) {
+ const selectedRangeArray =
+ arrow === 'left'
+ ? selectedRange.start.split('-')
+ : selectedRange.end.split('-');
+ const addSubtractYear =
+ arrow === 'left'
+ ? String(Number(selectedRangeArray[0]) - 1)
+ : String(Number(selectedRangeArray[0]) + 1);
+ const changeYear =
+ addSubtractYear +
+ '-' +
+ selectedRangeArray[1] +
+ '-' +
+ selectedRangeArray[2];
+ setSelectedRange((prev) => {
+ return arrow === 'left'
+ ? {
+ start: changeYear as string,
+ end: prev?.end as string,
+ }
+ : {
+ start: prev?.start as string,
+ end: changeYear as string,
+ };
+ });
+ }
+ },
+ [selectedRange, setSelectedRange]
+ );
return (
= (args: any) => {
>
- {/* display start to end value */}
-
- {selectedRange?.start} ~ {selectedRange?.end}
+
+
+
+ {/* add axis range control */}
+
+
+
- selectedRange.end
- ? variableMetadata.variable.distributionDefaults.rangeMax
- : selectedRange.end,
- }
- : undefined;
+ const extendedDisplayRange = useMemo(
+ () =>
+ variableMetadata && DateVariable.is(variableMetadata.variable)
+ ? selectedRange == null
+ ? {
+ start: variableMetadata.variable.distributionDefaults.rangeMin,
+ end: variableMetadata.variable.distributionDefaults.rangeMax,
+ }
+ : {
+ start:
+ variableMetadata.variable.distributionDefaults.rangeMin <
+ selectedRange.start
+ ? variableMetadata.variable.distributionDefaults.rangeMin
+ : selectedRange.start,
+ end:
+ variableMetadata.variable.distributionDefaults.rangeMax >
+ selectedRange.end
+ ? variableMetadata.variable.distributionDefaults.rangeMax
+ : selectedRange.end,
+ }
+ : undefined,
+ [variableMetadata, selectedRange]
+ );
// converting old usePromise code to useQuery in an efficient manner
const { enabled, queryKey, queryFn } = useMemo(() => {
@@ -129,12 +140,12 @@ export default function TimeSliderQuickFilter({
},
};
}, [
- variableMetadata?.variable,
variable,
subsettingClient,
filters,
- extendedDisplayRange?.start,
- extendedDisplayRange?.end,
+ extendedDisplayRange,
+ studyId,
+ variableMetadata,
]);
const timeSliderData = useQuery({
@@ -178,9 +189,6 @@ export default function TimeSliderQuickFilter({
});
}
- // (easily) centering the variable picker requires two same-width divs either side
- const sideElementStyle = { width: '70px' };
-
const sliderHeight = minimized ? 50 : 75;
const background =
@@ -235,6 +243,8 @@ export default function TimeSliderQuickFilter({
change the date variable (currently{' '}
{variableMetadata?.variable.displayName})
+ set start and end dates precisely
+ step the window forwards and backwards through the timeline
toggle the temporary time window filter on/off
@@ -249,6 +259,80 @@ export default function TimeSliderQuickFilter({
);
+ // disable arrow button
+ const [disableLeftArrow, setDisableLeftArrow] = useState(false);
+ const [disableRightArrow, setDisableRightArrow] = useState(false);
+
+ // control selectedRange
+ const handleAxisRangeChange = useCallback(
+ (newRange?: NumberOrDateRange) => {
+ if (newRange) {
+ const newSelectedRange = {
+ start: newRange.min as string,
+ end: newRange.max as string,
+ };
+ updateConfig({ ...config, selectedRange: newSelectedRange });
+ }
+ },
+ [config, updateConfig]
+ );
+
+ // step buttons
+ const handleArrowClick = useCallback(
+ (arrow: string) => {
+ if (
+ selectedRange &&
+ selectedRange.start != null &&
+ selectedRange.end != null
+ ) {
+ const newSelectedRange = newArrowRange(selectedRange, arrow);
+ updateConfig({ ...config, selectedRange: newSelectedRange });
+ }
+ },
+ [config, updateConfig, selectedRange]
+ );
+
+ // enabling/disabling date range arrows
+ useEffect(() => {
+ if (extendedDisplayRange && selectedRange) {
+ const diff =
+ new Date(selectedRange.end).getTime() -
+ new Date(selectedRange.start).getTime();
+
+ const expectedStartDate = new Date(
+ new Date(selectedRange.start).getTime() - diff
+ )
+ .toISOString()
+ .split('T')[0];
+
+ const expectedEndDate = new Date(
+ new Date(selectedRange.end).getTime() + diff
+ )
+ .toISOString()
+ .split('T')[0];
+
+ // left arrow
+ if (
+ new Date(expectedStartDate).getTime() <
+ new Date(extendedDisplayRange.start).getTime()
+ ) {
+ setDisableLeftArrow(true);
+ } else {
+ setDisableLeftArrow(false);
+ }
+
+ // right arrow
+ if (
+ new Date(expectedEndDate).getTime() >
+ new Date(extendedDisplayRange.end).getTime()
+ ) {
+ setDisableRightArrow(true);
+ } else {
+ setDisableRightArrow(false);
+ }
+ }
+ }, [extendedDisplayRange, selectedRange]);
+
// if no variable in a study is suitable to time slider, do not show time slider
return variable != null && variableMetadata != null ? (
@@ -292,17 +376,16 @@ export default function TimeSliderQuickFilter({
+
{!minimized && (
<>
-
-
+
+
+
+ {/* add axis range control */}
+
+
+
+
+
) : null;
}
+
+// compute new range by step button
+function newArrowRange(
+ selectedRange: selectedRangeProp | undefined,
+ arrow: string
+) {
+ if (selectedRange) {
+ const diff =
+ new Date(selectedRange.end).getTime() -
+ new Date(selectedRange.start).getTime();
+
+ const diffSign = arrow === 'right' ? diff : -diff;
+
+ const newSelectedRange = {
+ start: new Date(new Date(selectedRange.start).getTime() + diffSign)
+ .toISOString()
+ .split('T')[0],
+ end: new Date(new Date(selectedRange.end).getTime() + diffSign)
+ .toISOString()
+ .split('T')[0],
+ };
+
+ // unit in milliseconds
+ const deltaLeapDaysMS =
+ (countLeapDays(newSelectedRange) - countLeapDays(selectedRange)) *
+ (1000 * 3600 * 24);
+
+ return {
+ start: new Date(
+ new Date(newSelectedRange.start).getTime() -
+ (arrow === 'left' ? deltaLeapDaysMS : 0)
+ )
+ .toISOString()
+ .split('T')[0],
+ end: new Date(
+ new Date(newSelectedRange.end).getTime() +
+ (arrow === 'right' ? deltaLeapDaysMS : 0)
+ )
+ .toISOString()
+ .split('T')[0],
+ };
+ } else {
+ return undefined;
+ }
+}
+
+// check whether leap year is
+function isLeapYear(year: number) {
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+}
+
+// compute the number of days of leap years for a date range
+function countLeapDays(dateRange: selectedRangeProp) {
+ const startDate = new Date(dateRange.start);
+ const endDate = new Date(dateRange.end);
+ const startYear = startDate.getFullYear();
+ const endYear = endDate.getFullYear();
+
+ let leapDayCount = 0;
+
+ for (let year = startYear; year <= endYear; year++) {
+ if (isLeapYear(year)) {
+ const leapDay = new Date(year, 1, 29);
+ if (leapDay >= startDate && leapDay <= endDate) {
+ leapDayCount++;
+ }
+ }
+ }
+
+ return leapDayCount;
+}