From d8a8f9c5710cbfc6d0c8d8156ce573de2a933878 Mon Sep 17 00:00:00 2001 From: "Dae Kun (DK) Kwon" Date: Sat, 24 Jun 2023 22:41:39 -0400 Subject: [PATCH 01/27] Ez time filter component and story --- packages/libs/components/package.json | 1 + .../components/plotControls/EzTimeFilter.tsx | 247 +++++++++++++ .../plotControls/EzTimeFilter.stories.tsx | 326 ++++++++++++++++++ .../src/utils/date-format-change.ts | 9 + yarn.lock | 18 + 5 files changed, 601 insertions(+) create mode 100755 packages/libs/components/src/components/plotControls/EzTimeFilter.tsx create mode 100755 packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx create mode 100644 packages/libs/components/src/utils/date-format-change.ts diff --git a/packages/libs/components/package.json b/packages/libs/components/package.json index 039a1f8448..93ce32fe78 100755 --- a/packages/libs/components/package.json +++ b/packages/libs/components/package.json @@ -12,6 +12,7 @@ "@typescript-eslint/parser": "^5.46.0", "@veupathdb/coreui": "workspace:^", "@visx/axis": "^3.1.0", + "@visx/brush": "^3.0.1", "@visx/gradient": "^1.0.0", "@visx/group": "^1.0.0", "@visx/hierarchy": "^1.0.0", diff --git a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx new file mode 100755 index 0000000000..ac5fb74717 --- /dev/null +++ b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx @@ -0,0 +1,247 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import React, { + useRef, + useState, + useMemo, + useCallback, + useImperativeHandle, + forwardRef, + ForwardedRef, +} from 'react'; +import { scaleTime, scaleLinear } from '@visx/scale'; +import appleStock, { AppleStock } from '@visx/mock-data/lib/mocks/appleStock'; +import { Brush } from '@visx/brush'; +import { Bounds } from '@visx/brush/lib/types'; +import BaseBrush, { + BaseBrushState, + UpdateBrush, +} from '@visx/brush/lib/BaseBrush'; +import { PatternLines } from '@visx/pattern'; +import { Group } from '@visx/group'; +import { max, extent } from 'd3-array'; +import { BrushHandleRenderProps } from '@visx/brush/lib/BrushHandle'; +import { AreaClosed } from '@visx/shape'; +import { AxisBottom } from '@visx/axis'; +import { curveMonotoneX } from '@visx/curve'; +import { millisecondTodate } from '../../utils/date-format-change'; + +export type EZTimeFilterDataProp = { + x: string; + y: number; +}; + +export type EzTimeFilterProps = { + /** Ez time filter data */ + data: EZTimeFilterDataProp[]; + /** current state of selectedRange */ + selectedRange: { start: string; end: string }; + /** update function selectedRange */ + setSelectedRange: (selectedRange: EzTimeFilterProps['selectedRange']) => void; + /** width */ + width?: number; + /** height */ + height?: number; + /** line color of the selected range */ + accentColor?: string; + /** axis tick and tick label color */ + axisColor?: string; +}; + +// using forwardRef +function EzTimeFilter( + props: EzTimeFilterProps, + ref: ForwardedRef<{ handleResetClick: () => void }> +) { + const { + data, + // set default width and height + width = 720, + height = 125, + accentColor = '#4A6BD6', + axisColor = '#000', + selectedRange, + setSelectedRange, + } = props; + + const brushRef = useRef(null); + + // define default values + const margin = { top: 10, bottom: 10, left: 10, right: 10 }; + const PATTERN_ID = 'brush_pattern'; + const selectedBrushStyle = { + fill: `url(#${PATTERN_ID})`, + stroke: accentColor, + }; + + // axis props + const axisBottomTickLabelProps = { + textAnchor: 'middle' as const, + fontFamily: 'Arial', + fontSize: 10, + fill: axisColor, + }; + + // accessors for data + const getXData = (d: EZTimeFilterDataProp) => new Date(d.x); + const getYData = (d: EZTimeFilterDataProp) => d.y; + + const onBrushChange = (domain: Bounds | null) => { + if (!domain) return; + + const { x0, x1 } = domain; + + const selectedDomain = { + // x0 and x1 are millisecond value + start: millisecondTodate(x0), + end: millisecondTodate(x1), + }; + + setSelectedRange(selectedDomain); + }; + + // bounds + const xBrushMax = Math.max(width - margin.left - margin.right, 0); + // take 80 % of given height considering axis tick/tick labels at the bottom + const yBrushMax = Math.max(0.8 * height - margin.top - margin.bottom, 0); + + // scaling + const xBrushScale = useMemo( + () => + scaleTime({ + range: [0, xBrushMax], + domain: + data != null ? (extent(data, getXData) as [Date, Date]) : undefined, + }), + [data, xBrushMax] + ); + + const yBrushScale = useMemo( + () => + scaleLinear({ + range: [yBrushMax, 0], + domain: [0, max(data, getYData) || 0], + nice: true, + }), + [data, yBrushMax] + ); + + // initial selectedRange position + const initialBrushPosition = useMemo( + () => ({ + start: { x: xBrushScale(getXData(data[0])) }, + end: { x: xBrushScale(getXData(data[data.length - 1])) }, + }), + [data, xBrushScale] + ); + + // reset brush position to be initial one + const handleResetClick = () => { + if (brushRef?.current) { + const updater: UpdateBrush = (prevBrush) => { + const newExtent = brushRef.current!.getExtent( + initialBrushPosition.start, + initialBrushPosition.end + ); + + const newState: BaseBrushState = { + ...prevBrush, + start: { y: newExtent.y0, x: newExtent.x0 }, + end: { y: newExtent.y1, x: newExtent.x1 }, + extent: newExtent, + }; + + return newState; + }; + + brushRef.current.updateBrush(updater); + } + }; + + // forwardRef: handleResetClick function to be used at the parent component + useImperativeHandle( + ref, + () => ({ + handleResetClick, + }), + [] + ); + + return ( +
+ + + xBrushScale(getXData(d)) || 0} + y={(d) => yBrushScale(getYData(d)) || 0} + yScale={yBrushScale} + strokeWidth={1} + fill="lightgray" + curve={curveMonotoneX} + /> + 520 ? 10 : 5} + stroke={axisColor} + tickStroke={axisColor} + tickLabelProps={axisBottomTickLabelProps} + /> + + } + /> + + +
+ ); +} + +// define brush handle shape and position +function BrushHandle({ x, height, isBrushActive }: BrushHandleRenderProps) { + const pathWidth = 8; + const pathHeight = 15; + if (!isBrushActive) { + return null; + } + return ( + + + + ); +} + +// forwardRef +export default forwardRef(EzTimeFilter); diff --git a/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx b/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx new file mode 100755 index 0000000000..a1e480cff0 --- /dev/null +++ b/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx @@ -0,0 +1,326 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Story, Meta } from '@storybook/react/types-6-0'; +import { LinePlotProps } from '../../plots/LinePlot'; +import AxisRangeControl from '../../components/plotControls/AxisRangeControl'; +import { NumberOrDateRange } from '../../types/general'; +import { Toggle } from '@veupathdb/coreui'; +import { LinePlotDataSeries } from '../../types/plots'; + +import EzTimeFilter, { + EZTimeFilterDataProp, +} from '../../components/plotControls/EzTimeFilter'; +import { Undo } from '@veupathdb/coreui'; + +export default { + title: 'Plot Controls/EzTimeFilter', + component: EzTimeFilter, +} as Meta; + +// GEMS1 Case Control; x: Enrollment date; y: Weight +const LineplotData = { + series: [ + { + x: [ + '2007-11-01', + '2007-12-01', + '2008-01-01', + '2008-02-01', + '2008-03-01', + '2008-04-01', + '2008-05-01', + '2008-06-01', + '2008-07-01', + '2008-08-01', + '2008-09-01', + '2008-10-01', + '2008-11-01', + '2008-12-01', + '2009-01-01', + '2009-02-01', + '2009-03-01', + '2009-04-01', + '2009-05-01', + '2009-06-01', + '2009-07-01', + '2009-08-01', + '2009-09-01', + '2009-10-01', + '2009-11-01', + '2009-12-01', + '2010-01-01', + '2010-02-01', + '2010-03-01', + '2010-04-01', + '2010-05-01', + '2010-06-01', + '2010-07-01', + '2010-08-01', + '2010-09-01', + '2010-10-01', + '2010-11-01', + '2010-12-01', + '2011-01-01', + '2011-02-01', + '2011-03-01', + ], + y: [ + 9.4667, 8.9338, 8.7881, 8.8275, 8.9666, 8.9188, 8.8984, 8.5207, 8.8826, + 8.8885, 8.918, 9.0428, 9.2326, 9.2678, 9.2778, 9.2791, 9.3129, 9.3575, + 9.547, 9.1614, 8.8183, 9.0783, 9.3669, 9.1692, 9.2234, 9.2269, 9.3905, + 9.3198, 9.0729, 9.4823, 9.2846, 9.2275, 9.0953, 9.2941, 9.2566, 9.6933, + 9.5211, 9.4618, 8.697, 8.718, 7.8882, + ], + binLabel: [ + '2007-11-01 - 2007-12-01', + '2007-12-01 - 2008-01-01', + '2008-01-01 - 2008-02-01', + '2008-02-01 - 2008-03-01', + '2008-03-01 - 2008-04-01', + '2008-04-01 - 2008-05-01', + '2008-05-01 - 2008-06-01', + '2008-06-01 - 2008-07-01', + '2008-07-01 - 2008-08-01', + '2008-08-01 - 2008-09-01', + '2008-09-01 - 2008-10-01', + '2008-10-01 - 2008-11-01', + '2008-11-01 - 2008-12-01', + '2008-12-01 - 2009-01-01', + '2009-01-01 - 2009-02-01', + '2009-02-01 - 2009-03-01', + '2009-03-01 - 2009-04-01', + '2009-04-01 - 2009-05-01', + '2009-05-01 - 2009-06-01', + '2009-06-01 - 2009-07-01', + '2009-07-01 - 2009-08-01', + '2009-08-01 - 2009-09-01', + '2009-09-01 - 2009-10-01', + '2009-10-01 - 2009-11-01', + '2009-11-01 - 2009-12-01', + '2009-12-01 - 2010-01-01', + '2010-01-01 - 2010-02-01', + '2010-02-01 - 2010-03-01', + '2010-03-01 - 2010-04-01', + '2010-04-01 - 2010-05-01', + '2010-05-01 - 2010-06-01', + '2010-06-01 - 2010-07-01', + '2010-07-01 - 2010-08-01', + '2010-08-01 - 2010-09-01', + '2010-09-01 - 2010-10-01', + '2010-10-01 - 2010-11-01', + '2010-11-01 - 2010-12-01', + '2010-12-01 - 2011-01-01', + '2011-01-01 - 2011-02-01', + '2011-02-01 - 2011-03-01', + '2011-03-01 - 2011-04-01', + ], + binSampleSize: [ + { + N: 18, + }, + { + N: 804, + }, + { + N: 1186, + }, + { + N: 1475, + }, + { + N: 1665, + }, + { + N: 1609, + }, + { + N: 1706, + }, + { + N: 1803, + }, + { + N: 1943, + }, + { + N: 1874, + }, + { + N: 1475, + }, + { + N: 1503, + }, + { + N: 1545, + }, + { + N: 1393, + }, + { + N: 1641, + }, + { + N: 1893, + }, + { + N: 1913, + }, + { + N: 1916, + }, + { + N: 1882, + }, + { + N: 1967, + }, + { + N: 1699, + }, + { + N: 1697, + }, + { + N: 1263, + }, + { + N: 1774, + }, + { + N: 1620, + }, + { + N: 1450, + }, + { + N: 1893, + }, + { + N: 1628, + }, + { + N: 1552, + }, + { + N: 1722, + }, + { + N: 1698, + }, + { + N: 1604, + }, + { + N: 1544, + }, + { + N: 1368, + }, + { + N: 1302, + }, + { + N: 1250, + }, + { + N: 1254, + }, + { + N: 751, + }, + { + N: 527, + }, + { + N: 267, + }, + { + N: 51, + }, + ], + name: 'Data', + mode: 'lines+markers', + opacity: 0.7, + marker: { + color: 'rgb(136,34,85)', + symbol: 'circle', + }, + }, + ], +}; + +export const TimeFilter: Story = (args: any) => { + // converting lineplot data to visx format + const timeFilterData: EZTimeFilterDataProp[] = LineplotData.series[0].x.map( + (value, index) => { + return { x: value, y: LineplotData.series[0].y[index] }; + } + ); + + // set initial selectedRange + const [selectedRange, setSelectedRange] = useState({ + start: timeFilterData[0].x, + end: timeFilterData[timeFilterData.length - 1].x, + }); + + // set forwardRef to call handleResetClick function from EzTimeFilter component + const childRef = useRef<{ handleResetClick: () => void }>(null); + + return ( +
+
+ {/* display start to end value */} +
+ {selectedRange?.start} ~ {selectedRange?.end} +
+ {/* button to reset selectedRange */} +
+ + + +
+
+ +
+ ); +}; diff --git a/packages/libs/components/src/utils/date-format-change.ts b/packages/libs/components/src/utils/date-format-change.ts new file mode 100644 index 0000000000..0c71991303 --- /dev/null +++ b/packages/libs/components/src/utils/date-format-change.ts @@ -0,0 +1,9 @@ +// milliseconds to mm/dd/yyyy +export function millisecondTodate(value: number) { + const date = new Date(value); // Date 2011-05-09T06:08:45.178Z + const year = date.getFullYear(); + const month = ('0' + (date.getMonth() + 1)).slice(-2); + const day = ('0' + date.getDate()).slice(-2); + + return `${year}-${month}-${day}`; +} diff --git a/yarn.lock b/yarn.lock index b77a3430a3..d0a4c12b9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7998,6 +7998,7 @@ __metadata: "@veupathdb/react-scripts": "workspace:^" "@veupathdb/tsconfig": "workspace:^" "@visx/axis": ^3.1.0 + "@visx/brush": ^3.0.1 "@visx/gradient": ^1.0.0 "@visx/group": ^1.0.0 "@visx/hierarchy": ^1.0.0 @@ -8991,6 +8992,23 @@ __metadata: languageName: node linkType: hard +"@visx/brush@npm:^3.0.1": + version: 3.0.1 + resolution: "@visx/brush@npm:3.0.1" + dependencies: + "@visx/drag": 3.0.1 + "@visx/event": 3.0.1 + "@visx/group": 3.0.0 + "@visx/scale": 3.0.0 + "@visx/shape": 3.0.0 + classnames: ^2.3.1 + prop-types: ^15.6.1 + peerDependencies: + react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: 7e7d5a4b9663ca8b623245e7ba4dc63eeee912ca28cf0fff9ca15672e24984a2445b45fd8e8281ad309621dac0e7bad5413eedb1a93da66eba9572f0c75bfb03 + languageName: node + linkType: hard + "@visx/clip-path@npm:1.7.0": version: 1.7.0 resolution: "@visx/clip-path@npm:1.7.0" From 8440f05c1defbd13751de1ce387b7c16336b215d Mon Sep 17 00:00:00 2001 From: "Dae Kun (DK) Kwon" Date: Wed, 5 Jul 2023 22:45:14 -0400 Subject: [PATCH 02/27] time filter mockup at SAM --- .../containers/DraggablePanel/index.tsx | 4 +- .../containers/DraggablePanel.stories.tsx | 141 +++++++--- .../lib/map/analysis/DraggableTimeFilter.tsx | 207 ++++++++++++++ .../eda/src/lib/map/analysis/MapAnalysis.tsx | 262 +++++++++++++++++- 4 files changed, 579 insertions(+), 35 deletions(-) create mode 100644 packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx diff --git a/packages/libs/coreui/src/components/containers/DraggablePanel/index.tsx b/packages/libs/coreui/src/components/containers/DraggablePanel/index.tsx index 7206c84a3a..8cdc54e2c6 100644 --- a/packages/libs/coreui/src/components/containers/DraggablePanel/index.tsx +++ b/packages/libs/coreui/src/components/containers/DraggablePanel/index.tsx @@ -1,4 +1,4 @@ -import { CSSProperties, ReactNode, useEffect, useState } from 'react'; +import { CSSProperties, ReactNode, useEffect, useState, useMemo } from 'react'; import Draggable, { DraggableEvent, DraggableData } from 'react-draggable'; import { css } from '@emotion/react'; import useResizeObserver from 'use-resize-observer'; @@ -113,7 +113,7 @@ export default function DraggablePanel({ width: width, }); }, - [height, width, onPanelResize] + [height, width] ); const finalPosition = confineToParentContainer diff --git a/packages/libs/coreui/src/stories/containers/DraggablePanel.stories.tsx b/packages/libs/coreui/src/stories/containers/DraggablePanel.stories.tsx index b4311fd30e..fabf94c21a 100644 --- a/packages/libs/coreui/src/stories/containers/DraggablePanel.stories.tsx +++ b/packages/libs/coreui/src/stories/containers/DraggablePanel.stories.tsx @@ -1,16 +1,16 @@ -import React from "react"; -import { Story, Meta } from "@storybook/react/types-6-0"; -import { useState } from "react"; -import UIThemeProvider from "../../components/theming/UIThemeProvider"; -import { mutedMagenta, gray } from "../../definitions/colors"; +import React from 'react'; +import { Story, Meta } from '@storybook/react/types-6-0'; +import { useState } from 'react'; +import UIThemeProvider from '../../components/theming/UIThemeProvider'; +import { mutedMagenta, gray } from '../../definitions/colors'; import DraggablePanel, { DraggablePanelCoordinatePair, DraggablePanelProps, HeightAndWidthInPixels, -} from "../../components/containers/DraggablePanel"; +} from '../../components/containers/DraggablePanel'; export default { - title: "Containers/DraggablePanel", + title: 'Containers/DraggablePanel', component: DraggablePanel, } as Meta; @@ -21,10 +21,10 @@ interface DraggablePanelStoryProps extends DraggablePanelProps { const Template: Story = (args) => { const panelDefinitionOjects: DraggablePanelStoryProps[] = [ - "Panel 1", - "Panel 2", - "Panel 3", - "Panel 4", + 'Panel 1', + 'Panel 2', + 'Panel 3', + 'Panel 4', ].map((panelTitle, panelIndex) => { return { children: () =>

Panel Contents

, @@ -58,7 +58,9 @@ Default.args = { styleOverrides: {}, }; -type StackOrderingKeeperProps = { draggablePanelProps: DraggablePanelStoryProps[] }; +type StackOrderingKeeperProps = { + draggablePanelProps: DraggablePanelStoryProps[]; +}; function StackOrderingKeeper({ draggablePanelProps, @@ -125,16 +127,16 @@ function StackOrderingKeeper({ return (
    {draggablePanelProps.map((props) => { @@ -146,16 +148,16 @@ function StackOrderingKeeper({ movePanelToTopLayer(props.panelTitle); togglePanelOpen(props.panelTitle); }} - style={{ backgroundColor: isOpen ? "tomato" : "lightgreen" }} + style={{ backgroundColor: isOpen ? 'tomato' : 'lightgreen' }} > - {isOpen ? "Close" : "Open"} {props.panelTitle} + {isOpen ? 'Close' : 'Open'} {props.panelTitle} @@ -201,11 +203,11 @@ function StackOrderingKeeper({ } styleOverrides={{ zIndex, - margin: "0 0 1rem 0", - width: "500px", - height: "200px", - minHeight: "175px", - minWidth: "285px", + margin: '0 0 1rem 0', + width: '500px', + height: '200px', + minHeight: '175px', + minWidth: '285px', ...props.styleOverrides, }} > @@ -215,19 +217,19 @@ function StackOrderingKeeper({ movePanelToTopLayer(props.panelTitle); }} style={{ - padding: "1rem", - fontFamily: "sans-serif", + padding: '1rem', + fontFamily: 'sans-serif', }} >

    {props.panelTitle} Content

    - Panel Dimensions:{" "} + Panel Dimensions:{' '} {JSON.stringify( dimensionByPanelTitleDictionary[props.panelTitle] )}

    - Panel Position:{" "} + Panel Position:{' '} {JSON.stringify( positionByPanelTitleDictionary[props.panelTitle] )} @@ -255,3 +257,80 @@ function StackOrderingKeeper({

); } + +// single panel case +export const SinglePanel: Story = (args) => { + const panelTitle = 'Panel 1'; + + const [panelOpen, setPanelOpen] = useState(true); + + const draggablePanelWidth = 500; + const draggablePanelHeight = 200; + + function handleOnPanelDismiss() { + setPanelOpen(!panelOpen); + } + + return ( +
+
+ +
+ + {/* This is just nonsense to fill the panel with content */} +
+ Lorem ipsum dolor sit, amet consectetur adipisicing elit. Saepe labore + ut quia harum expedita distinctio eius deserunt, officiis inventore + velit. Voluptatibus unde eum animi alias, illum eligendi ullam facilis + consectetur? +
+
+
+ ); +}; diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx new file mode 100644 index 0000000000..9e2be3153f --- /dev/null +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -0,0 +1,207 @@ +import { useState, useRef } from 'react'; +import { DraggablePanel } from '@veupathdb/coreui/lib/components/containers'; +import EzTimeFilter, { + EZTimeFilterDataProp, +} from '@veupathdb/components/lib/components/plotControls/EzTimeFilter'; +import { Undo } from '@veupathdb/coreui'; +import { + InputSpec, + InputVariables, + requiredInputLabelStyle, +} from '../../core/components/visualizations/InputVariables'; +import { VariablesByInputName } from '../../core/utils/data-element-constraints'; +import { AnalysisState } from '../../core'; +import { StudyEntity } from '../../core/types/study'; +import { VariableDescriptor } from '../../core/types/variable'; + +interface Props { + data: any; + zIndex: number; + entities: StudyEntity[]; + // to handle filters in the near future + analysisState: AnalysisState; + // not quite sure yet if configuration is necessary but typed as any for now + configuration: any; + starredVariables: VariableDescriptor[]; + toggleStarredVariable: (targetVariableId: VariableDescriptor) => void; + // constraints: any; +} + +export default function DraggableTimeFilter({ + data, + analysisState, + zIndex, + starredVariables, + entities, + configuration, + toggleStarredVariable, +}: // constraints, +Props) { + // converting lineplot data to visx format + const timeFilterData: EZTimeFilterDataProp[] = data.series[0].x.map( + (value: any, index: any) => { + return { x: value, y: data.series[0].y[index] }; + } + ); + + // set initial selectedRange + const [selectedRange, setSelectedRange] = useState({ + start: timeFilterData[0].x, + end: timeFilterData[timeFilterData.length - 1].x, + }); + + // set forwardRef to call handleResetClick function from EzTimeFilter component + const childRef = useRef<{ handleResetClick: () => void }>(null); + const timeFilterWidth = 750; + + // control panel open/close + const [panelOpen, setPanelOpen] = useState(false); + const panelTitle = 'Time filter'; + function handleOnPanelDismiss() { + // reset time filter + childRef.current?.handleResetClick(); + setPanelOpen(!panelOpen); + } + + // inputVariables onChange function + function handleInputVariablesOnChange(selection: VariablesByInputName) { + if (!selection.overlayVariable) { + console.error( + `Expected overlayVariable to be defined but got ${typeof selection.overlayVariable}` + ); + return; + } + + // temporarily blocked + // onChange({ + // ...configuration, + // selectedVariable: selection.overlayVariable, + // selectedValues: undefined, + // }); + } + + return ( + <> +
+ +
+ + +
+
+ {/* InputVariables does not work yet */} +
+ +
+ {/* display start to end value */} +
+ {selectedRange?.start} ~ {selectedRange?.end} +
+ {/* button to reset selectedRange */} +
+ + + +
+
+ +
+
+ + ); +} diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 14ee42ccb4..ded140751d 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -1,4 +1,11 @@ -import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; +import { + ReactNode, + useCallback, + useEffect, + useMemo, + useState, + useRef, +} from 'react'; import { AnalysisState, @@ -88,6 +95,8 @@ import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; import DonutMarkerComponent from '@veupathdb/components/lib/map/DonutMarker'; import ChartMarkerComponent from '@veupathdb/components/lib/map/ChartMarker'; +import DraggableTimeFilter from './DraggableTimeFilter'; + enum MapSideNavItemLabels { Download = 'Download', Filter = 'Filter', @@ -908,6 +917,240 @@ function MapAnalysisImpl(props: ImplProps) { getZIndexByPanelTitle(DraggablePanelIds.LEGEND_PANEL) + getZIndexByPanelTitle(DraggablePanelIds.VIZ_PANEL); + // Note: temporary test data for Ez time filter + // GEMS1 Case Control; x: Enrollment date; y: Weight + const LineplotData = { + series: [ + { + x: [ + '2007-11-01', + '2007-12-01', + '2008-01-01', + '2008-02-01', + '2008-03-01', + '2008-04-01', + '2008-05-01', + '2008-06-01', + '2008-07-01', + '2008-08-01', + '2008-09-01', + '2008-10-01', + '2008-11-01', + '2008-12-01', + '2009-01-01', + '2009-02-01', + '2009-03-01', + '2009-04-01', + '2009-05-01', + '2009-06-01', + '2009-07-01', + '2009-08-01', + '2009-09-01', + '2009-10-01', + '2009-11-01', + '2009-12-01', + '2010-01-01', + '2010-02-01', + '2010-03-01', + '2010-04-01', + '2010-05-01', + '2010-06-01', + '2010-07-01', + '2010-08-01', + '2010-09-01', + '2010-10-01', + '2010-11-01', + '2010-12-01', + '2011-01-01', + '2011-02-01', + '2011-03-01', + ], + y: [ + 9.4667, 8.9338, 8.7881, 8.8275, 8.9666, 8.9188, 8.8984, 8.5207, + 8.8826, 8.8885, 8.918, 9.0428, 9.2326, 9.2678, 9.2778, 9.2791, 9.3129, + 9.3575, 9.547, 9.1614, 8.8183, 9.0783, 9.3669, 9.1692, 9.2234, 9.2269, + 9.3905, 9.3198, 9.0729, 9.4823, 9.2846, 9.2275, 9.0953, 9.2941, + 9.2566, 9.6933, 9.5211, 9.4618, 8.697, 8.718, 7.8882, + ], + binLabel: [ + '2007-11-01 - 2007-12-01', + '2007-12-01 - 2008-01-01', + '2008-01-01 - 2008-02-01', + '2008-02-01 - 2008-03-01', + '2008-03-01 - 2008-04-01', + '2008-04-01 - 2008-05-01', + '2008-05-01 - 2008-06-01', + '2008-06-01 - 2008-07-01', + '2008-07-01 - 2008-08-01', + '2008-08-01 - 2008-09-01', + '2008-09-01 - 2008-10-01', + '2008-10-01 - 2008-11-01', + '2008-11-01 - 2008-12-01', + '2008-12-01 - 2009-01-01', + '2009-01-01 - 2009-02-01', + '2009-02-01 - 2009-03-01', + '2009-03-01 - 2009-04-01', + '2009-04-01 - 2009-05-01', + '2009-05-01 - 2009-06-01', + '2009-06-01 - 2009-07-01', + '2009-07-01 - 2009-08-01', + '2009-08-01 - 2009-09-01', + '2009-09-01 - 2009-10-01', + '2009-10-01 - 2009-11-01', + '2009-11-01 - 2009-12-01', + '2009-12-01 - 2010-01-01', + '2010-01-01 - 2010-02-01', + '2010-02-01 - 2010-03-01', + '2010-03-01 - 2010-04-01', + '2010-04-01 - 2010-05-01', + '2010-05-01 - 2010-06-01', + '2010-06-01 - 2010-07-01', + '2010-07-01 - 2010-08-01', + '2010-08-01 - 2010-09-01', + '2010-09-01 - 2010-10-01', + '2010-10-01 - 2010-11-01', + '2010-11-01 - 2010-12-01', + '2010-12-01 - 2011-01-01', + '2011-01-01 - 2011-02-01', + '2011-02-01 - 2011-03-01', + '2011-03-01 - 2011-04-01', + ], + binSampleSize: [ + { + N: 18, + }, + { + N: 804, + }, + { + N: 1186, + }, + { + N: 1475, + }, + { + N: 1665, + }, + { + N: 1609, + }, + { + N: 1706, + }, + { + N: 1803, + }, + { + N: 1943, + }, + { + N: 1874, + }, + { + N: 1475, + }, + { + N: 1503, + }, + { + N: 1545, + }, + { + N: 1393, + }, + { + N: 1641, + }, + { + N: 1893, + }, + { + N: 1913, + }, + { + N: 1916, + }, + { + N: 1882, + }, + { + N: 1967, + }, + { + N: 1699, + }, + { + N: 1697, + }, + { + N: 1263, + }, + { + N: 1774, + }, + { + N: 1620, + }, + { + N: 1450, + }, + { + N: 1893, + }, + { + N: 1628, + }, + { + N: 1552, + }, + { + N: 1722, + }, + { + N: 1698, + }, + { + N: 1604, + }, + { + N: 1544, + }, + { + N: 1368, + }, + { + N: 1302, + }, + { + N: 1250, + }, + { + N: 1254, + }, + { + N: 751, + }, + { + N: 527, + }, + { + N: 267, + }, + { + N: 51, + }, + ], + name: 'Data', + mode: 'lines+markers', + opacity: 0.7, + marker: { + color: 'rgb(136,34,85)', + symbol: 'circle', + }, + }, + ], + }; + return ( {(apps: ComputationAppOverview[]) => { @@ -1033,6 +1276,20 @@ function MapAnalysisImpl(props: ImplProps) { + {/* Time filter component: need to adjust props */} + + {/* setIsSubsetPanelOpen(true)} /> - */} + */} + {activeSideMenuId && isMapTypeSubMenuItemSelected() && ( Date: Mon, 17 Jul 2023 16:24:28 -0400 Subject: [PATCH 03/27] address feedbacks, make closer to mockup --- .../components/plotControls/EzTimeFilter.tsx | 84 +++++++++++++++---- .../plotControls/EzTimeFilter.stories.tsx | 40 ++++++++- .../lib/map/analysis/DraggableTimeFilter.tsx | 40 ++++++++- 3 files changed, 144 insertions(+), 20 deletions(-) diff --git a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx index ac5fb74717..86e1a94b9f 100755 --- a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx +++ b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx @@ -1,15 +1,13 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import React, { useRef, - useState, + useEffect, useMemo, - useCallback, useImperativeHandle, forwardRef, ForwardedRef, } from 'react'; import { scaleTime, scaleLinear } from '@visx/scale'; -import appleStock, { AppleStock } from '@visx/mock-data/lib/mocks/appleStock'; import { Brush } from '@visx/brush'; import { Bounds } from '@visx/brush/lib/types'; import BaseBrush, { @@ -20,10 +18,11 @@ import { PatternLines } from '@visx/pattern'; import { Group } from '@visx/group'; import { max, extent } from 'd3-array'; import { BrushHandleRenderProps } from '@visx/brush/lib/BrushHandle'; -import { AreaClosed } from '@visx/shape'; import { AxisBottom } from '@visx/axis'; -import { curveMonotoneX } from '@visx/curve'; import { millisecondTodate } from '../../utils/date-format-change'; +import { Bar } from '@visx/shape'; +import { debounce } from 'lodash'; +import { LineSubject } from '@visx/annotation'; export type EZTimeFilterDataProp = { x: string; @@ -45,6 +44,8 @@ export type EzTimeFilterProps = { accentColor?: string; /** axis tick and tick label color */ axisColor?: string; + /** debounce rate in millisecond */ + debounceRateMs?: number; }; // using forwardRef @@ -61,6 +62,8 @@ function EzTimeFilter( axisColor = '#000', selectedRange, setSelectedRange, + // set a default debounce time in milliseconds + debounceRateMs = 500, } = props; const brushRef = useRef(null); @@ -166,6 +169,30 @@ function EzTimeFilter( [] ); + // compute bar width manually as scaleTime is used for Bar chart + const barWidth = xBrushMax / data.length; + + // after dragging ends + const onBrushEnd = () => { + //TO-DO a sort of submitting action for a filtered range later is required here + console.log('brush dragging ends!!!'); + }; + + // debounce function for onBrushEnd: will be used for submitting filtered range later + const debouncedOnBrushEnd = useMemo( + () => debounce(onBrushEnd, debounceRateMs), + [onBrushEnd] + ); + + const defaultColor = 'lightgray'; + + // Cancel pending onBrushEnd request when this component is unmounted + useEffect(() => { + return () => { + debouncedOnBrushEnd.cancel(); + }; + }, []); + return (
- xBrushScale(getXData(d)) || 0} - y={(d) => yBrushScale(getYData(d)) || 0} - yScale={yBrushScale} - strokeWidth={1} - fill="lightgray" - curve={curveMonotoneX} - /> + {/* use Bar chart */} + {data.map((d, i) => { + const barHeight = yBrushMax - yBrushScale(getYData(d)); + return ( + + barHeight = 0; dataY = 1 -> barHeight = 60 + // Thus, to mimick the bar shape of the ez time filter mockup, + // starting y-coordinate for dataY = 1 sets to be 1/4*yBrushMax. + // And, the height prop is set to be 1/2*yBrushMax so that + // the bottom side of a bar has 1/4*yBrushMax space with respect to the x-axis line + y={barHeight === 0 ? yBrushMax : (1 / 4) * yBrushMax} + height={(1 / 2) * barHeight} + // set the last data's barWidth to be 0 so that it does not overflow to dragging area + width={i === data.length - 1 ? 0 : barWidth} + fill={defaultColor} + /> + + ); + })} } /> + {/* horizontal center line for no data */} + <> + + + +
diff --git a/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx b/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx index a1e480cff0..7466f127b9 100755 --- a/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx +++ b/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx @@ -253,7 +253,8 @@ export const TimeFilter: Story = (args: any) => { // converting lineplot data to visx format const timeFilterData: EZTimeFilterDataProp[] = LineplotData.series[0].x.map( (value, index) => { - return { x: value, y: LineplotData.series[0].y[index] }; + // return { x: value, y: LineplotData.series[0].y[index] }; + return { x: value, y: LineplotData.series[0].y[index] >= 9 ? 1 : 0 }; } ); @@ -266,11 +267,15 @@ export const TimeFilter: Story = (args: any) => { // set forwardRef to call handleResetClick function from EzTimeFilter component const childRef = useRef<{ handleResetClick: () => void }>(null); + // set constant values + const defaultSymbolSize = 0.8; + const defaultColor = 'lightgray'; + return (
= (args: any) => { selectedRange={selectedRange} setSelectedRange={setSelectedRange} width={720} - height={125} + height={100} // line color of the selectedRange accentColor={'#4A6BD6'} // axis tick and tick label color axisColor={'#000'} /> + {/* add a legend */} +
+
+
  Has visible data on the map
+
+
  Has no visible data on the map
+
); }; diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index 9e2be3153f..f355004053 100644 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -38,9 +38,10 @@ export default function DraggableTimeFilter({ }: // constraints, Props) { // converting lineplot data to visx format + // temporarily set to 1 (with data) and 0 (no data) manually for demo purpose const timeFilterData: EZTimeFilterDataProp[] = data.series[0].x.map( - (value: any, index: any) => { - return { x: value, y: data.series[0].y[index] }; + (value: any, index: number) => { + return { x: value, y: data.series[0].y[index] >= 9 ? 1 : 0 }; } ); @@ -80,6 +81,10 @@ Props) { // }); } + // set constant values + const defaultSymbolSize = 0.9; + const defaultColor = 'lightgray'; + return ( <>
+ {/* DKDK add a legend */} +
+
+
  Has visible data on the map
+
+
  Has no visible data on the map
+
From d635f512b8902ff94c8a0925794c2c17f286c3ed Mon Sep 17 00:00:00 2001 From: "Dae Kun (DK) Kwon" Date: Tue, 8 Aug 2023 14:22:39 -0400 Subject: [PATCH 04/27] address feedbacks concerning UI and UX --- .../components/plotControls/EzTimeFilter.tsx | 105 ++----- .../plotControls/EzTimeFilter.stories.tsx | 181 +++++++----- .../lib/map/analysis/DraggableTimeFilter.tsx | 271 ++++++++---------- 3 files changed, 253 insertions(+), 304 deletions(-) diff --git a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx index 86e1a94b9f..b3a84ffe25 100755 --- a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx +++ b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx @@ -1,20 +1,13 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ -import React, { - useRef, - useEffect, - useMemo, - useImperativeHandle, - forwardRef, - ForwardedRef, -} from 'react'; +import React, { useRef, useEffect, useMemo } from 'react'; import { scaleTime, scaleLinear } from '@visx/scale'; import { Brush } from '@visx/brush'; -import { Bounds } from '@visx/brush/lib/types'; +// add ResizeTriggerAreas type +import { Bounds, ResizeTriggerAreas } from '@visx/brush/lib/types'; import BaseBrush, { BaseBrushState, UpdateBrush, } from '@visx/brush/lib/BaseBrush'; -import { PatternLines } from '@visx/pattern'; import { Group } from '@visx/group'; import { max, extent } from 'd3-array'; import { BrushHandleRenderProps } from '@visx/brush/lib/BrushHandle'; @@ -22,7 +15,6 @@ import { AxisBottom } from '@visx/axis'; import { millisecondTodate } from '../../utils/date-format-change'; import { Bar } from '@visx/shape'; import { debounce } from 'lodash'; -import { LineSubject } from '@visx/annotation'; export type EZTimeFilterDataProp = { x: string; @@ -40,28 +32,34 @@ export type EzTimeFilterProps = { width?: number; /** height */ height?: number; - /** line color of the selected range */ - accentColor?: string; + /** color of the selected range */ + brushColor?: string; /** axis tick and tick label color */ axisColor?: string; + /** opacity of selected brush */ + brushOpacity?: number; + /** whether movement of Brush should be disabled */ + disableDraggingSelection?: boolean; + /** disable brush selection */ + resizeTriggerAreas?: ResizeTriggerAreas[]; /** debounce rate in millisecond */ debounceRateMs?: number; }; // using forwardRef -function EzTimeFilter( - props: EzTimeFilterProps, - ref: ForwardedRef<{ handleResetClick: () => void }> -) { +function EzTimeFilter(props: EzTimeFilterProps) { const { data, // set default width and height width = 720, height = 125, - accentColor = '#4A6BD6', + brushColor = 'lightblue', axisColor = '#000', + brushOpacity = 0.4, selectedRange, setSelectedRange, + disableDraggingSelection = false, + resizeTriggerAreas = ['left', 'right'], // set a default debounce time in milliseconds debounceRateMs = 500, } = props; @@ -70,10 +68,12 @@ function EzTimeFilter( // define default values const margin = { top: 10, bottom: 10, left: 10, right: 10 }; - const PATTERN_ID = 'brush_pattern'; const selectedBrushStyle = { - fill: `url(#${PATTERN_ID})`, - stroke: accentColor, + fill: brushColor, + stroke: brushColor, + fillOpacity: brushOpacity, + // need to set this to be 1? + strokeOpacity: 1, }; // axis props @@ -137,38 +137,6 @@ function EzTimeFilter( [data, xBrushScale] ); - // reset brush position to be initial one - const handleResetClick = () => { - if (brushRef?.current) { - const updater: UpdateBrush = (prevBrush) => { - const newExtent = brushRef.current!.getExtent( - initialBrushPosition.start, - initialBrushPosition.end - ); - - const newState: BaseBrushState = { - ...prevBrush, - start: { y: newExtent.y0, x: newExtent.x0 }, - end: { y: newExtent.y1, x: newExtent.x1 }, - extent: newExtent, - }; - - return newState; - }; - - brushRef.current.updateBrush(updater); - } - }; - - // forwardRef: handleResetClick function to be used at the parent component - useImperativeHandle( - ref, - () => ({ - handleResetClick, - }), - [] - ); - // compute bar width manually as scaleTime is used for Bar chart const barWidth = xBrushMax / data.length; @@ -184,7 +152,7 @@ function EzTimeFilter( [onBrushEnd] ); - const defaultColor = 'lightgray'; + const defaultColor = '#333'; // Cancel pending onBrushEnd request when this component is unmounted useEffect(() => { @@ -234,14 +202,6 @@ function EzTimeFilter( tickStroke={axisColor} tickLabelProps={axisBottomTickLabelProps} /> - } /> - {/* horizontal center line for no data */} - <> - - - -
@@ -299,5 +247,4 @@ function BrushHandle({ x, height, isBrushActive }: BrushHandleRenderProps) { ); } -// forwardRef -export default forwardRef(EzTimeFilter); +export default EzTimeFilter; diff --git a/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx b/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx index 7466f127b9..9b251c5bad 100755 --- a/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx +++ b/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx @@ -1,15 +1,10 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState } from 'react'; import { Story, Meta } from '@storybook/react/types-6-0'; import { LinePlotProps } from '../../plots/LinePlot'; -import AxisRangeControl from '../../components/plotControls/AxisRangeControl'; -import { NumberOrDateRange } from '../../types/general'; -import { Toggle } from '@veupathdb/coreui'; -import { LinePlotDataSeries } from '../../types/plots'; - import EzTimeFilter, { EZTimeFilterDataProp, } from '../../components/plotControls/EzTimeFilter'; -import { Undo } from '@veupathdb/coreui'; +import { DraggablePanel } from '@veupathdb/coreui/lib/components/containers'; export default { title: 'Plot Controls/EzTimeFilter', @@ -264,97 +259,127 @@ export const TimeFilter: Story = (args: any) => { end: timeFilterData[timeFilterData.length - 1].x, }); - // set forwardRef to call handleResetClick function from EzTimeFilter component - const childRef = useRef<{ handleResetClick: () => void }>(null); + // set time filter width + const timeFilterWidth = 750; + + // set initial position: shrink + const [defaultPosition, setDefaultPosition] = useState({ + x: window.innerWidth / 2 - timeFilterWidth / 2, + y: 0, + }); + + // set DraggablePanel key + const [key, setKey] = useState(0); + + // set button text + const [buttonText, setButtonText] = useState('Expand'); + + const expandPosition = () => { + setButtonText('Shrink'); + setKey((currentKey) => currentKey + 1); + setDefaultPosition({ + x: window.innerWidth / 2 - timeFilterWidth / 2, + y: 50, + }); + }; + + const resetPosition = () => { + setButtonText('Expand'); + setKey((currentKey) => currentKey + 1); + setDefaultPosition({ + x: window.innerWidth / 2 - timeFilterWidth / 2, + y: 0, + }); + // initialize range + setSelectedRange({ + start: timeFilterData[0].x, + end: timeFilterData[timeFilterData.length - 1].x, + }); + }; // set constant values const defaultSymbolSize = 0.8; - const defaultColor = 'lightgray'; + const defaultColor = '#333'; return ( -
- {/* display start to end value */} -
- {selectedRange?.start} ~ {selectedRange?.end} -
- {/* button to reset selectedRange */}
- - - + {/* display start to end value */} +
+ {selectedRange?.start} ~ {selectedRange?.end} +
-
- - {/* add a legend */} -
-
-
  Has visible data on the map
+ {/* add a Expand or something like that to change position */}
-
  Has no visible data on the map
+ > + {/* reset position to hide panel title */} +
+ +
+
-
+ ); }; diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index f355004053..d316646cbd 100644 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -51,18 +51,44 @@ Props) { end: timeFilterData[timeFilterData.length - 1].x, }); - // set forwardRef to call handleResetClick function from EzTimeFilter component - const childRef = useRef<{ handleResetClick: () => void }>(null); + // set time slider width and y position const timeFilterWidth = 750; + const yPosition = 0; - // control panel open/close - const [panelOpen, setPanelOpen] = useState(false); - const panelTitle = 'Time filter'; - function handleOnPanelDismiss() { - // reset time filter - childRef.current?.handleResetClick(); - setPanelOpen(!panelOpen); - } + // set initial position: shrink + const [defaultPosition, setDefaultPosition] = useState({ + x: window.innerWidth / 2 - timeFilterWidth / 2, + y: yPosition, + }); + + // set DraggablePanel key + const [key, setKey] = useState(0); + + // set button text + const [buttonText, setButtonText] = useState('Expand'); + + const expandSlider = () => { + setButtonText('Shrink'); + setKey((currentKey) => currentKey + 1); + setDefaultPosition({ + x: window.innerWidth / 2 - timeFilterWidth / 2, + y: 100, + }); + }; + + const shrinkSlider = () => { + setButtonText('Expand'); + setKey((currentKey) => currentKey + 1); + setDefaultPosition({ + x: window.innerWidth / 2 - timeFilterWidth / 2, + y: yPosition, + }); + // initialize range + setSelectedRange({ + start: timeFilterData[0].x, + end: timeFilterData[timeFilterData.length - 1].x, + }); + }; // inputVariables onChange function function handleInputVariablesOnChange(selection: VariablesByInputName) { @@ -83,159 +109,110 @@ Props) { // set constant values const defaultSymbolSize = 0.9; - const defaultColor = 'lightgray'; return ( - <> +
- -
- - + {/* InputVariables does not work yet */} +
+ +
+ {/* display start to end value */} +
+ {selectedRange?.start} ~ {selectedRange?.end} +
+
+ + {/* add a button to expand/shrink */}
-
- {/* InputVariables does not work yet */} -
- -
- {/* display start to end value */} -
- {selectedRange?.start} ~ {selectedRange?.end} -
- {/* button to reset selectedRange */} -
+
-
- - {/* DKDK add a legend */} -
-
-
  Has visible data on the map
-
-
  Has no visible data on the map
+ {buttonText === 'Expand' ? ( + + ) : ( + + )} +   {buttonText} +
- - +
+ ); } From 55687c827cf4cb495e1e520d30eed4f865b28044 Mon Sep 17 00:00:00 2001 From: "Dae Kun (DK) Kwon" Date: Tue, 8 Aug 2023 14:56:22 -0400 Subject: [PATCH 05/27] resolve conflict --- packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index d316646cbd..10bdd41e14 100644 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -141,12 +141,12 @@ Props) { {/* InputVariables does not work yet */}
Date: Tue, 29 Aug 2023 23:46:40 -0400 Subject: [PATCH 06/27] update - data request, inputVariables, filtering, etc --- .../components/plotControls/EzTimeFilter.tsx | 40 +- .../lib/map/analysis/DraggableTimeFilter.tsx | 426 ++++++++++++++---- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 254 +---------- 3 files changed, 371 insertions(+), 349 deletions(-) mode change 100644 => 100755 packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx mode change 100644 => 100755 packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx diff --git a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx index b3a84ffe25..2c85e3f0af 100755 --- a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx +++ b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx @@ -122,8 +122,10 @@ function EzTimeFilter(props: EzTimeFilterProps) { () => scaleLinear({ range: [yBrushMax, 0], - domain: [0, max(data, getYData) || 0], - nice: true, + domain: [0, max(data, getYData) || 1], + // set zero: false so that it does not include zero line in the middle of y-axis + // this is useful when all data have zeros + zero: false, }), [data, yBrushMax] ); @@ -140,10 +142,33 @@ function EzTimeFilter(props: EzTimeFilterProps) { // compute bar width manually as scaleTime is used for Bar chart const barWidth = xBrushMax / data.length; - // after dragging ends - const onBrushEnd = () => { - //TO-DO a sort of submitting action for a filtered range later is required here - console.log('brush dragging ends!!!'); + // make an event after dragging ends + const onBrushEnd = () => {}; + + // data bar color + const defaultColor = '#333'; + + // onclick to reset + const handleResetClick = () => { + if (brushRef?.current) { + const updater: UpdateBrush = (prevBrush) => { + const newExtent = brushRef.current!.getExtent( + initialBrushPosition.start, + initialBrushPosition.end + ); + + const newState: BaseBrushState = { + ...prevBrush, + start: { y: newExtent.y0, x: newExtent.x0 }, + end: { y: newExtent.y1, x: newExtent.x1 }, + extent: newExtent, + }; + + return newState; + }; + + brushRef.current.updateBrush(updater); + } }; // debounce function for onBrushEnd: will be used for submitting filtered range later @@ -152,8 +177,6 @@ function EzTimeFilter(props: EzTimeFilterProps) { [onBrushEnd] ); - const defaultColor = '#333'; - // Cancel pending onBrushEnd request when this component is unmounted useEffect(() => { return () => { @@ -215,6 +238,7 @@ function EzTimeFilter(props: EzTimeFilterProps) { brushDirection="horizontal" initialBrushPosition={initialBrushPosition} onChange={onBrushChange} + onClick={handleResetClick} selectedBoxStyle={selectedBrushStyle} useWindowMoveEvents disableDraggingSelection={disableDraggingSelection} diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx old mode 100644 new mode 100755 index 10bdd41e14..cf01670094 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -1,56 +1,185 @@ -import { useState, useRef } from 'react'; +import { useState, useMemo, useEffect, useCallback } from 'react'; import { DraggablePanel } from '@veupathdb/coreui/lib/components/containers'; import EzTimeFilter, { EZTimeFilterDataProp, } from '@veupathdb/components/lib/components/plotControls/EzTimeFilter'; -import { Undo } from '@veupathdb/coreui'; +import { InputVariables } from '../../core/components/visualizations/InputVariables'; import { - InputSpec, - InputVariables, - requiredInputLabelStyle, -} from '../../core/components/visualizations/InputVariables'; -import { VariablesByInputName } from '../../core/utils/data-element-constraints'; -import { AnalysisState } from '../../core'; + VariablesByInputName, + DataElementConstraintRecord, +} from '../../core/utils/data-element-constraints'; +import { AnalysisState, usePromise } from '../../core'; import { StudyEntity } from '../../core/types/study'; import { VariableDescriptor } from '../../core/types/variable'; +import { SubsettingClient } from '../../core/api'; +import Spinner from '@veupathdb/components/lib/components/Spinner'; +import { useFindEntityAndVariable, Filter } from '../../core'; +import { + DateRange, + NumberRange, +} from '@veupathdb/components/lib/types/general'; +import { DateRangeFilter, NumberRangeFilter } from '../../core/types/filter'; +import { Tooltip } from '@material-ui/core'; + interface Props { - data: any; - zIndex: number; + studyId: string; entities: StudyEntity[]; - // to handle filters in the near future + // to handle filters analysisState: AnalysisState; - // not quite sure yet if configuration is necessary but typed as any for now - configuration: any; + subsettingClient: SubsettingClient; + filters: Filter[] | undefined; starredVariables: VariableDescriptor[]; toggleStarredVariable: (targetVariableId: VariableDescriptor) => void; - // constraints: any; } export default function DraggableTimeFilter({ - data, + studyId, analysisState, - zIndex, - starredVariables, entities, - configuration, + subsettingClient, + filters, + starredVariables, toggleStarredVariable, -}: // constraints, -Props) { - // converting lineplot data to visx format - // temporarily set to 1 (with data) and 0 (no data) manually for demo purpose - const timeFilterData: EZTimeFilterDataProp[] = data.series[0].x.map( - (value: any, index: number) => { - return { x: value, y: data.series[0].y[index] >= 9 ? 1 : 0 }; - } +}: Props) { + // filter constraint for time slider inputVariables component + const timeSliderVariableConstraints: DataElementConstraintRecord[] = [ + { + overlayVariable: { + isRequired: true, + minNumVars: 1, + maxNumVars: 1, + // TODO: testing with SCORE S. mansoni Cluster Randomized Trial study + // however, this study does not have date variable, thus temporarily use below for test purpose + // i.e., additionally allowing 'integer' + allowedTypes: ['date', 'integer'], + // TODO: below two are correct ones + // allowedTypes: ['date'], + // isTemporal: true, + }, + }, + ]; + + // find initial variable id for time slider + const defaultTimeSliderVariabeId = useMemo( + () => + entities + .map((data) => { + return data.variables.find( + (variable) => + // TODO temporarily allows integer for test purpose + // no need to allow 'integer' for actual purpose + (variable.type === 'date' || variable.type === 'integer') && + // TODO: thus, below is correct one instead of above + // (variable.type === 'date') && + variable.dataShape === 'continuous' && + variable.isTemporal + ); + }) + .filter((data) => data != null)[0]?.id, + [entities] + ); + + // find initial entity id for time slider + const defaultTimeSliderEntityId = useMemo( + () => + entities + .map((data) => { + if ( + data.variables.find( + (variable) => variable.id === defaultTimeSliderVariabeId + ) + ) + return data; + }) + .filter((data) => data != null)[0]?.id, + [entities] ); - // set initial selectedRange + // set initial variable so that time slider loads data in the beginning + const [timeSliderVariable, setTimeSliderVariable] = + useState({ + entityId: defaultTimeSliderEntityId ?? '', + variableId: defaultTimeSliderVariabeId ?? '', + }); + + // find variable metadata: use timeSliderVariable, not defaultTimeSlider ids + const findEntityAndVariable = useFindEntityAndVariable(); + const timeSliderVariableMetadata = findEntityAndVariable({ + entityId: timeSliderVariable.entityId ?? '', + variableId: timeSliderVariable.variableId ?? '', + }); + + // set initial selectedRange as empty, then change it at useEffect due to data request const [selectedRange, setSelectedRange] = useState({ - start: timeFilterData[0].x, - end: timeFilterData[timeFilterData.length - 1].x, + start: '', + end: '', }); + // data request to distribution for time slider + const getTimeSliderData = usePromise( + useCallback(async () => { + // no data request if no variable is available + if ( + timeSliderVariableMetadata == null || + !('distributionDefaults' in timeSliderVariableMetadata.variable) + ) + return; + + const binSpec = { + displayRangeMin: + timeSliderVariableMetadata.variable.distributionDefaults.rangeMin + + (timeSliderVariableMetadata.variable.type === 'date' + ? 'T00:00:00Z' + : ''), + displayRangeMax: + timeSliderVariableMetadata.variable.distributionDefaults.rangeMax + + (timeSliderVariableMetadata.variable.type === 'date' + ? 'T00:00:00Z' + : ''), + binWidth: + timeSliderVariableMetadata.variable.distributionDefaults.binWidth ?? + 1, + binUnits: + 'binUnits' in timeSliderVariableMetadata.variable.distributionDefaults + ? timeSliderVariableMetadata.variable.distributionDefaults.binUnits + : undefined, + }; + const distributionResponse = await subsettingClient.getDistribution( + studyId, + timeSliderVariable.entityId, + timeSliderVariable.variableId, + { + valueSpec: 'count', + filters: filters ?? [], + binSpec, + } + ); + + return { + x: distributionResponse.histogram.map((d) => d.binStart), + // conditionally set y-values to be 1 (with data) and 0 (no data) + y: distributionResponse.histogram.map((d) => (d.value >= 1 ? 1 : 0)), + }; + }, [ + timeSliderVariableMetadata?.variable, + timeSliderVariable, + subsettingClient, + filters, + ]) + ); + + // converting data to visx format + const timeFilterData: EZTimeFilterDataProp[] = useMemo( + () => + !getTimeSliderData.pending && getTimeSliderData.value != null + ? getTimeSliderData?.value?.x.map((value: string, index: number) => { + return { x: value, y: getTimeSliderData.value!.y[index] }; + }) + : [], + [getTimeSliderData] + ); + // set time slider width and y position const timeFilterWidth = 750; const yPosition = 0; @@ -61,15 +190,15 @@ Props) { y: yPosition, }); - // set DraggablePanel key - const [key, setKey] = useState(0); + // set DraggablePanel key to update time slider + const [draggablePanelKey, setDraggablePanelKey] = useState(0); // set button text const [buttonText, setButtonText] = useState('Expand'); const expandSlider = () => { setButtonText('Shrink'); - setKey((currentKey) => currentKey + 1); + setDraggablePanelKey((currentKey) => currentKey + 1); setDefaultPosition({ x: window.innerWidth / 2 - timeFilterWidth / 2, y: 100, @@ -78,7 +207,7 @@ Props) { const shrinkSlider = () => { setButtonText('Expand'); - setKey((currentKey) => currentKey + 1); + setDraggablePanelKey((currentKey) => currentKey + 1); setDefaultPosition({ x: window.innerWidth / 2 - timeFilterWidth / 2, y: yPosition, @@ -99,34 +228,110 @@ Props) { return; } - // temporarily blocked - // onChange({ - // ...configuration, - // selectedVariable: selection.overlayVariable, - // selectedValues: undefined, - // }); + // set time slider variable + setTimeSliderVariable(selection.overlayVariable); } + // set filter function + const { setFilters } = analysisState; + + const filter = filters?.find( + (f): f is NumberRangeFilter | DateRangeFilter => + f.variableId === timeSliderVariable.variableId && + f.entityId === timeSliderVariable.entityId && + (f.type === 'dateRange' || f.type === 'numberRange') + ); + + // the format of selectedRange at filter is min/max, not start/end + // NOTE: currently, this considers both dateRange and numberRange for test purpose: indeed only dateRange is required + // But perhaps this is just okay even if numberRange parts are redundant + const updateFilter = useCallback( + (selectedRange?: NumberRange | DateRange) => { + const otherFilters = filters?.filter((f) => f !== filter) ?? []; + if (selectedRange == null) { + if (otherFilters.length !== filters?.length) setFilters(otherFilters); + } else { + if ( + filter && + (filter.type === 'dateRange' || filter.type === 'numberRange') && + filter.min === selectedRange.min && + filter.max === selectedRange.max + ) + return; + setFilters( + otherFilters.concat([ + timeSliderVariableMetadata?.variable.type === 'date' + ? { + variableId: timeSliderVariable.variableId, + entityId: timeSliderVariable.entityId, + type: 'dateRange', + ...(selectedRange as DateRange), + } + : { + variableId: timeSliderVariable.variableId, + entityId: timeSliderVariable.entityId, + type: 'numberRange', + ...(selectedRange as NumberRange), + }, + ]) + ); + } + }, + [ + filters, + filter, + setFilters, + timeSliderVariable.entityId, + timeSliderVariable.variableId, + timeSliderVariableMetadata?.variable.type, + ] + ); + + // submit/add filter + const onSubmitTimeSlider = () => { + if (updateFilter != null) + // TODO: below is correct format + // updateFilter({ min: selectedDomain.start, max: selectedDomain.end }) + // since there is no date variable, use number variable for test purpose + // in this case, the value should consider the first value before dash and change it to number, not string + updateFilter({ + min: Number(selectedRange.start.split('-')[0]), + max: Number(selectedRange.end.split('-')[0]), + }); + }; + // set constant values const defaultSymbolSize = 0.9; - return ( + // change selectedRange considering async data request + useEffect(() => { + if (!getTimeSliderData.pending && getTimeSliderData.value != null) { + setSelectedRange({ + start: getTimeSliderData.value.x[0], + end: getTimeSliderData.value.x[getTimeSliderData.value.x.length - 1], + }); + } + }, [getTimeSliderData]); + + // if no variable in a study is suitable to time slider, do not show time slider + return defaultTimeSliderVariabeId != null ? (
- {/* InputVariables does not work yet */}
{/* display start to end value */}
{selectedRange?.start} ~ {selectedRange?.end}
-
- - {/* add a button to expand/shrink */} -
- {/* reset position to hide panel title */} -
- + {/* button to reset selectedRange */} +
+ + +
+ {/* display data loading spinner while requesting data to the backend */} + {getTimeSliderData.pending && ( +
+ +
+ )} + {/* conditional loading for EzTimeFilter */} + {!getTimeSliderData.pending && + getTimeSliderData.value != null && + timeFilterData.length > 0 && ( + <> + + {/* add a button to expand/shrink */} +
+ {/* reset position to hide panel title */} +
+ +
+
+ + )}
- ); + ) : null; } diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx old mode 100644 new mode 100755 index 91c97cffc8..172f554b12 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -1,11 +1,4 @@ -import { - ReactNode, - useCallback, - useEffect, - useMemo, - useState, - useRef, -} from 'react'; +import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; import { AllValuesDefinition, @@ -202,6 +195,7 @@ export function MapAnalysis(props: Props) { const analysisState = useAnalysis(props.analysisId, 'pass'); const appStateAndSetters = useAppState('@@mapApp@@', analysisState); const geoConfigs = useGeoConfig(useStudyEntities()); + if (geoConfigs == null || geoConfigs.length === 0) return ( {(apps: ComputationAppOverview[]) => { @@ -1429,20 +1189,20 @@ function MapAnalysisImpl(props: ImplProps) {
- {/* Time filter component: need to adjust props */} + {/* Time slider component */} + {/* )} */} {/* Date: Tue, 29 Aug 2023 23:59:15 -0400 Subject: [PATCH 07/27] yarn.lock was changed --- yarn.lock | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/yarn.lock b/yarn.lock index 588f2618b8..a73a767f59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9166,6 +9166,20 @@ __metadata: languageName: node linkType: hard +"@visx/drag@npm:3.0.1": + version: 3.0.1 + resolution: "@visx/drag@npm:3.0.1" + dependencies: + "@types/react": "*" + "@visx/event": 3.0.1 + "@visx/point": 3.0.1 + prop-types: ^15.5.10 + peerDependencies: + react: ^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0 + checksum: 256e4a7b649e29b2112f77a57a657cd3aedb76b605ebc8b49e6c3d4b152fea5d81de7820060dc569dda0c81a0e7b6668ce9fe071536198b26cf2ebd655a6e6a9 + languageName: node + linkType: hard + "@visx/drag@npm:3.3.0": version: 3.3.0 resolution: "@visx/drag@npm:3.3.0" @@ -9190,6 +9204,16 @@ __metadata: languageName: node linkType: hard +"@visx/event@npm:3.0.1": + version: 3.0.1 + resolution: "@visx/event@npm:3.0.1" + dependencies: + "@types/react": "*" + "@visx/point": 3.0.1 + checksum: 0cb4dd578bbe54bd428a0cfae9e64db141b039c2c6c1412d1cd1d04e1613d193212b031124948ca3b7eed877cdea87d161cc3a008b973c6f217e21a8a2dd27b0 + languageName: node + linkType: hard + "@visx/event@npm:3.3.0": version: 3.3.0 resolution: "@visx/event@npm:3.3.0" From 3427cf20a47d1953d3fa846f4282fff8ff5fadf6 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 30 Aug 2023 22:19:01 +0100 Subject: [PATCH 08/27] used mostly existing functions to find default variable recursively --- .../lib/map/analysis/DraggableTimeFilter.tsx | 105 +++++++++--------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index cf01670094..2ca24b39f4 100755 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -7,9 +7,15 @@ import { InputVariables } from '../../core/components/visualizations/InputVariab import { VariablesByInputName, DataElementConstraintRecord, + filterVariablesByConstraint, } from '../../core/utils/data-element-constraints'; import { AnalysisState, usePromise } from '../../core'; -import { StudyEntity } from '../../core/types/study'; +import { + DateVariable, + NumberVariable, + StudyEntity, + Variable, +} from '../../core/types/study'; import { VariableDescriptor } from '../../core/types/variable'; import { SubsettingClient } from '../../core/api'; @@ -21,6 +27,7 @@ import { } from '@veupathdb/components/lib/types/general'; import { DateRangeFilter, NumberRangeFilter } from '../../core/types/filter'; import { Tooltip } from '@material-ui/core'; +import { preorder } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; interface Props { studyId: string; @@ -60,59 +67,48 @@ export default function DraggableTimeFilter({ }, ]; - // find initial variable id for time slider - const defaultTimeSliderVariabeId = useMemo( - () => - entities - .map((data) => { - return data.variables.find( - (variable) => - // TODO temporarily allows integer for test purpose - // no need to allow 'integer' for actual purpose - (variable.type === 'date' || variable.type === 'integer') && - // TODO: thus, below is correct one instead of above - // (variable.type === 'date') && - variable.dataShape === 'continuous' && - variable.isTemporal - ); - }) - .filter((data) => data != null)[0]?.id, - [entities] + const temporalVariableTree = filterVariablesByConstraint( + entities[0], + timeSliderVariableConstraints[0]['overlayVariable'] ); - // find initial entity id for time slider - const defaultTimeSliderEntityId = useMemo( - () => - entities - .map((data) => { - if ( - data.variables.find( - (variable) => variable.id === defaultTimeSliderVariabeId - ) - ) - return data; - }) - .filter((data) => data != null)[0]?.id, - [entities] - ); + // take the first suitable variable from the filtered variable tree + + // first find the first entity with some variables that passed the filter + const defaultTimeSliderEntity: StudyEntity | undefined = Array.from( + preorder(temporalVariableTree, (entity) => entity.children ?? []) + ) + // not all `variables` are actually variables, so we filter to be sure + .filter( + (entity) => + entity.variables.filter((variable) => Variable.is(variable)).length > 0 + )[0]; + + // then take the first variable from it + const defaultTimeSliderVariable: Variable | undefined = + defaultTimeSliderEntity.variables.filter((variable): variable is Variable => + Variable.is(variable) + )[0]; // set initial variable so that time slider loads data in the beginning - const [timeSliderVariable, setTimeSliderVariable] = - useState({ - entityId: defaultTimeSliderEntityId ?? '', - variableId: defaultTimeSliderVariabeId ?? '', - }); + const [timeSliderVariable, setTimeSliderVariable] = useState< + VariableDescriptor | undefined + >( + defaultTimeSliderEntity && defaultTimeSliderVariable + ? { + entityId: defaultTimeSliderEntity.id, + variableId: defaultTimeSliderVariable.id, + } + : undefined + ); // find variable metadata: use timeSliderVariable, not defaultTimeSlider ids const findEntityAndVariable = useFindEntityAndVariable(); - const timeSliderVariableMetadata = findEntityAndVariable({ - entityId: timeSliderVariable.entityId ?? '', - variableId: timeSliderVariable.variableId ?? '', - }); + const timeSliderVariableMetadata = findEntityAndVariable(timeSliderVariable); // set initial selectedRange as empty, then change it at useEffect due to data request const [selectedRange, setSelectedRange] = useState({ - start: '', + start: '', // TO DO: use undefined end: '', }); @@ -122,7 +118,11 @@ export default function DraggableTimeFilter({ // no data request if no variable is available if ( timeSliderVariableMetadata == null || - !('distributionDefaults' in timeSliderVariableMetadata.variable) + timeSliderVariable == null || + !( + NumberVariable.is(timeSliderVariableMetadata.variable) || + DateVariable.is(timeSliderVariableMetadata.variable) + ) ) return; @@ -182,7 +182,7 @@ export default function DraggableTimeFilter({ // set time slider width and y position const timeFilterWidth = 750; - const yPosition = 0; + const yPosition = 100; // TEMP: moved it down so I can see it all // set initial position: shrink const [defaultPosition, setDefaultPosition] = useState({ @@ -237,6 +237,7 @@ export default function DraggableTimeFilter({ const filter = filters?.find( (f): f is NumberRangeFilter | DateRangeFilter => + timeSliderVariable != null && f.variableId === timeSliderVariable.variableId && f.entityId === timeSliderVariable.entityId && (f.type === 'dateRange' || f.type === 'numberRange') @@ -251,6 +252,7 @@ export default function DraggableTimeFilter({ if (selectedRange == null) { if (otherFilters.length !== filters?.length) setFilters(otherFilters); } else { + if (timeSliderVariable == null) return; if ( filter && (filter.type === 'dateRange' || filter.type === 'numberRange') && @@ -262,14 +264,12 @@ export default function DraggableTimeFilter({ otherFilters.concat([ timeSliderVariableMetadata?.variable.type === 'date' ? { - variableId: timeSliderVariable.variableId, - entityId: timeSliderVariable.entityId, + ...timeSliderVariable, type: 'dateRange', ...(selectedRange as DateRange), } : { - variableId: timeSliderVariable.variableId, - entityId: timeSliderVariable.entityId, + ...timeSliderVariable, type: 'numberRange', ...(selectedRange as NumberRange), }, @@ -281,8 +281,7 @@ export default function DraggableTimeFilter({ filters, filter, setFilters, - timeSliderVariable.entityId, - timeSliderVariable.variableId, + timeSliderVariable, timeSliderVariableMetadata?.variable.type, ] ); @@ -314,7 +313,7 @@ export default function DraggableTimeFilter({ }, [getTimeSliderData]); // if no variable in a study is suitable to time slider, do not show time slider - return defaultTimeSliderVariabeId != null ? ( + return defaultTimeSliderVariable != null ? ( Date: Wed, 30 Aug 2023 23:08:30 +0100 Subject: [PATCH 09/27] reinstate isTemporal constraint --- packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index 2ca24b39f4..f750ea0564 100755 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -62,7 +62,7 @@ export default function DraggableTimeFilter({ allowedTypes: ['date', 'integer'], // TODO: below two are correct ones // allowedTypes: ['date'], - // isTemporal: true, + isTemporal: true, }, }, ]; From 254112930087b9ee17bb98b45821be4da8f010e0 Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 5 Sep 2023 12:41:12 +0100 Subject: [PATCH 10/27] remove shrink/expand for now --- .../lib/map/analysis/DraggableTimeFilter.tsx | 77 ++----------------- 1 file changed, 7 insertions(+), 70 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index f750ea0564..54ecb9ec1d 100755 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -184,41 +184,15 @@ export default function DraggableTimeFilter({ const timeFilterWidth = 750; const yPosition = 100; // TEMP: moved it down so I can see it all - // set initial position: shrink - const [defaultPosition, setDefaultPosition] = useState({ + const defaultPosition = { x: window.innerWidth / 2 - timeFilterWidth / 2, y: yPosition, - }); + }; // set DraggablePanel key to update time slider + // TO DO: leaving this here for now but may need to remove? const [draggablePanelKey, setDraggablePanelKey] = useState(0); - // set button text - const [buttonText, setButtonText] = useState('Expand'); - - const expandSlider = () => { - setButtonText('Shrink'); - setDraggablePanelKey((currentKey) => currentKey + 1); - setDefaultPosition({ - x: window.innerWidth / 2 - timeFilterWidth / 2, - y: 100, - }); - }; - - const shrinkSlider = () => { - setButtonText('Expand'); - setDraggablePanelKey((currentKey) => currentKey + 1); - setDefaultPosition({ - x: window.innerWidth / 2 - timeFilterWidth / 2, - y: yPosition, - }); - // initialize range - setSelectedRange({ - start: timeFilterData[0].x, - end: timeFilterData[timeFilterData.length - 1].x, - }); - }; - // inputVariables onChange function function handleInputVariablesOnChange(selection: VariablesByInputName) { if (!selection.overlayVariable) { @@ -299,9 +273,6 @@ export default function DraggableTimeFilter({ }); }; - // set constant values - const defaultSymbolSize = 0.9; - // change selectedRange considering async data request useEffect(() => { if (!getTimeSliderData.pending && getTimeSliderData.value != null) { @@ -408,45 +379,11 @@ export default function DraggableTimeFilter({ brushOpacity={0.4} // axis tick and tick label color axisColor={'#000'} - // whether movement of Brush should be disabled - disableDraggingSelection={buttonText === 'Expand'} - // disable brush selection: pass [] - resizeTriggerAreas={ - buttonText === 'Expand' ? [] : ['left', 'right'] - } + // whether movement of Brush should be disabled - false for now + disableDraggingSelection={false} + // if needing to disable brush selection: use [] + resizeTriggerAreas={['left', 'right']} /> - {/* add a button to expand/shrink */} -
- {/* reset position to hide panel title */} -
- -
-
)}
From c326950bd1c1adb58264bd31a6d39ed42c3cf86f Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 5 Sep 2023 12:50:42 +0100 Subject: [PATCH 11/27] remove submit button and updateFilter logic --- .../lib/map/analysis/DraggableTimeFilter.tsx | 74 ------------------- 1 file changed, 74 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index 54ecb9ec1d..f82a81ae7a 100755 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -217,62 +217,6 @@ export default function DraggableTimeFilter({ (f.type === 'dateRange' || f.type === 'numberRange') ); - // the format of selectedRange at filter is min/max, not start/end - // NOTE: currently, this considers both dateRange and numberRange for test purpose: indeed only dateRange is required - // But perhaps this is just okay even if numberRange parts are redundant - const updateFilter = useCallback( - (selectedRange?: NumberRange | DateRange) => { - const otherFilters = filters?.filter((f) => f !== filter) ?? []; - if (selectedRange == null) { - if (otherFilters.length !== filters?.length) setFilters(otherFilters); - } else { - if (timeSliderVariable == null) return; - if ( - filter && - (filter.type === 'dateRange' || filter.type === 'numberRange') && - filter.min === selectedRange.min && - filter.max === selectedRange.max - ) - return; - setFilters( - otherFilters.concat([ - timeSliderVariableMetadata?.variable.type === 'date' - ? { - ...timeSliderVariable, - type: 'dateRange', - ...(selectedRange as DateRange), - } - : { - ...timeSliderVariable, - type: 'numberRange', - ...(selectedRange as NumberRange), - }, - ]) - ); - } - }, - [ - filters, - filter, - setFilters, - timeSliderVariable, - timeSliderVariableMetadata?.variable.type, - ] - ); - - // submit/add filter - const onSubmitTimeSlider = () => { - if (updateFilter != null) - // TODO: below is correct format - // updateFilter({ min: selectedDomain.start, max: selectedDomain.end }) - // since there is no date variable, use number variable for test purpose - // in this case, the value should consider the first value before dash and change it to number, not string - updateFilter({ - min: Number(selectedRange.start.split('-')[0]), - max: Number(selectedRange.end.split('-')[0]), - }); - }; - // change selectedRange considering async data request useEffect(() => { if (!getTimeSliderData.pending && getTimeSliderData.value != null) { @@ -337,24 +281,6 @@ export default function DraggableTimeFilter({
{selectedRange?.start} ~ {selectedRange?.end}
- {/* button to reset selectedRange */} -
- - - -
{/* display data loading spinner while requesting data to the backend */} {getTimeSliderData.pending && ( From 23cbfe0eaf03be8671fb06130ef38619c067034e Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 5 Sep 2023 13:52:17 +0100 Subject: [PATCH 12/27] removed analystate and got bogged down obsessing over typescript and the 'zip' --- .../lib/map/analysis/DraggableTimeFilter.tsx | 34 ++++++++++--------- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 1 - 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index f82a81ae7a..573480c99d 100755 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -28,12 +28,12 @@ import { import { DateRangeFilter, NumberRangeFilter } from '../../core/types/filter'; import { Tooltip } from '@material-ui/core'; import { preorder } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; +import { zip } from 'lodash'; interface Props { studyId: string; entities: StudyEntity[]; // to handle filters - analysisState: AnalysisState; subsettingClient: SubsettingClient; filters: Filter[] | undefined; starredVariables: VariableDescriptor[]; @@ -42,7 +42,6 @@ interface Props { export default function DraggableTimeFilter({ studyId, - analysisState, entities, subsettingClient, filters, @@ -169,13 +168,27 @@ export default function DraggableTimeFilter({ ]) ); + type Data = { x: number[]; y: number[] } | undefined; + const data: Data = + Math.random() < 0.5 ? { x: [1, 2, 3], y: [10, 20, 30] } : undefined; + const z = + data != null + ? data.x.map((value, index) => { + value + data.y[index]; + }) + : []; + // converting data to visx format const timeFilterData: EZTimeFilterDataProp[] = useMemo( () => !getTimeSliderData.pending && getTimeSliderData.value != null - ? getTimeSliderData?.value?.x.map((value: string, index: number) => { - return { x: value, y: getTimeSliderData.value!.y[index] }; - }) + ? zip(getTimeSliderData.value.x, getTimeSliderData.value.y) + .map(([xValue, yValue]) => ({ x: xValue, y: yValue })) + // and a type guard filter to avoid any `!` assertions. + .filter( + (val): val is EZTimeFilterDataProp => + val.x != null && val.y != null + ) : [], [getTimeSliderData] ); @@ -206,17 +219,6 @@ export default function DraggableTimeFilter({ setTimeSliderVariable(selection.overlayVariable); } - // set filter function - const { setFilters } = analysisState; - - const filter = filters?.find( - (f): f is NumberRangeFilter | DateRangeFilter => - timeSliderVariable != null && - f.variableId === timeSliderVariable.variableId && - f.entityId === timeSliderVariable.entityId && - (f.type === 'dateRange' || f.type === 'numberRange') - ); - // change selectedRange considering async data request useEffect(() => { if (!getTimeSliderData.pending && getTimeSliderData.value != null) { diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 9edff8cf8c..c901a6e5c0 100755 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -1305,7 +1305,6 @@ function MapAnalysisImpl(props: ImplProps) { entities={studyEntities} subsettingClient={subsettingClient} filters={filters} - analysisState={analysisState} starredVariables={ analysisState.analysis?.descriptor.starredVariables ?? [] } From 8100c9d5420c7ec62255e70e3e6f89eb7b204b95 Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 5 Sep 2023 14:30:30 +0100 Subject: [PATCH 13/27] remove some test code --- .../eda/src/lib/map/analysis/DraggableTimeFilter.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index 573480c99d..da4aa51f23 100755 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -168,16 +168,6 @@ export default function DraggableTimeFilter({ ]) ); - type Data = { x: number[]; y: number[] } | undefined; - const data: Data = - Math.random() < 0.5 ? { x: [1, 2, 3], y: [10, 20, 30] } : undefined; - const z = - data != null - ? data.x.map((value, index) => { - value + data.y[index]; - }) - : []; - // converting data to visx format const timeFilterData: EZTimeFilterDataProp[] = useMemo( () => From d77a2d4b1dab35e16faa2e45ba7bd6982fc8e55e Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 6 Sep 2023 22:54:19 +0100 Subject: [PATCH 14/27] use appState for slider state, make brush 'controlled' with key trick --- .../components/plotControls/EzTimeFilter.tsx | 10 +- .../lib/map/analysis/DraggableTimeFilter.tsx | 98 +++++++++---------- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 9 ++ .../libs/eda/src/lib/map/analysis/appState.ts | 9 ++ 4 files changed, 70 insertions(+), 56 deletions(-) diff --git a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx index 2c85e3f0af..a3a32e33fb 100755 --- a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx +++ b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx @@ -133,10 +133,10 @@ function EzTimeFilter(props: EzTimeFilterProps) { // initial selectedRange position const initialBrushPosition = useMemo( () => ({ - start: { x: xBrushScale(getXData(data[0])) }, - end: { x: xBrushScale(getXData(data[data.length - 1])) }, + start: { x: xBrushScale(Number(new Date(selectedRange.start))) }, + end: { x: xBrushScale(Number(new Date(selectedRange.end))) }, }), - [data, xBrushScale] + [selectedRange, xBrushScale] ); // compute bar width manually as scaleTime is used for Bar chart @@ -148,7 +148,8 @@ function EzTimeFilter(props: EzTimeFilterProps) { // data bar color const defaultColor = '#333'; - // onclick to reset + // TO DO: this no longer works as intended because initialBrushPosition isn't the "reset position" any more + // but consider a separate reset button anyway? (which would just setSelectedRange to undefined I think) const handleResetClick = () => { if (brushRef?.current) { const updater: UpdateBrush = (prevBrush) => { @@ -226,6 +227,7 @@ function EzTimeFilter(props: EzTimeFilterProps) { tickLabelProps={axisBottomTickLabelProps} /> void; + + variable: AppState['timeSliderVariable']; + setVariable: (newVariable: AppState['timeSliderVariable']) => void; + selectedRange: AppState['timeSliderSelectedRange']; + setSelectedRange: (newRange: AppState['timeSliderSelectedRange']) => void; + active: AppState['timeSliderActive']; + setActive: (newState: AppState['timeSliderActive']) => void; } export default function DraggableTimeFilter({ @@ -47,6 +55,12 @@ export default function DraggableTimeFilter({ filters, starredVariables, toggleStarredVariable, + variable, + setVariable, + selectedRange, + setSelectedRange, + active, // to do - add a toggle to enable/disable + setActive, // the small filter and grey everything out }: Props) { // filter constraint for time slider inputVariables component const timeSliderVariableConstraints: DataElementConstraintRecord[] = [ @@ -61,7 +75,7 @@ export default function DraggableTimeFilter({ allowedTypes: ['date', 'integer'], // TODO: below two are correct ones // allowedTypes: ['date'], - isTemporal: true, + // isTemporal: true, }, }, ]; @@ -89,65 +103,51 @@ export default function DraggableTimeFilter({ Variable.is(variable) )[0]; - // set initial variable so that time slider loads data in the beginning - const [timeSliderVariable, setTimeSliderVariable] = useState< - VariableDescriptor | undefined - >( - defaultTimeSliderEntity && defaultTimeSliderVariable - ? { - entityId: defaultTimeSliderEntity.id, - variableId: defaultTimeSliderVariable.id, - } - : undefined - ); + // sorry, a useEffect for initialising the default variable + useEffect(() => { + if (variable == null && defaultTimeSliderVariable != null) { + setVariable({ + variableId: defaultTimeSliderVariable.id, + entityId: defaultTimeSliderEntity.id, + }); + } + }, [variable, defaultTimeSliderVariable]); // find variable metadata: use timeSliderVariable, not defaultTimeSlider ids const findEntityAndVariable = useFindEntityAndVariable(); - const timeSliderVariableMetadata = findEntityAndVariable(timeSliderVariable); - - // set initial selectedRange as empty, then change it at useEffect due to data request - const [selectedRange, setSelectedRange] = useState({ - start: '', // TO DO: use undefined - end: '', - }); + const variableMetadata = findEntityAndVariable(variable); // data request to distribution for time slider const getTimeSliderData = usePromise( useCallback(async () => { // no data request if no variable is available if ( - timeSliderVariableMetadata == null || - timeSliderVariable == null || + variableMetadata == null || + variable == null || !( - NumberVariable.is(timeSliderVariableMetadata.variable) || - DateVariable.is(timeSliderVariableMetadata.variable) + NumberVariable.is(variableMetadata.variable) || + DateVariable.is(variableMetadata.variable) ) ) return; const binSpec = { displayRangeMin: - timeSliderVariableMetadata.variable.distributionDefaults.rangeMin + - (timeSliderVariableMetadata.variable.type === 'date' - ? 'T00:00:00Z' - : ''), + variableMetadata.variable.distributionDefaults.rangeMin + + (variableMetadata.variable.type === 'date' ? 'T00:00:00Z' : ''), displayRangeMax: - timeSliderVariableMetadata.variable.distributionDefaults.rangeMax + - (timeSliderVariableMetadata.variable.type === 'date' - ? 'T00:00:00Z' - : ''), - binWidth: - timeSliderVariableMetadata.variable.distributionDefaults.binWidth ?? - 1, + variableMetadata.variable.distributionDefaults.rangeMax + + (variableMetadata.variable.type === 'date' ? 'T00:00:00Z' : ''), + binWidth: variableMetadata.variable.distributionDefaults.binWidth ?? 1, binUnits: - 'binUnits' in timeSliderVariableMetadata.variable.distributionDefaults - ? timeSliderVariableMetadata.variable.distributionDefaults.binUnits + 'binUnits' in variableMetadata.variable.distributionDefaults + ? variableMetadata.variable.distributionDefaults.binUnits : undefined, }; const distributionResponse = await subsettingClient.getDistribution( studyId, - timeSliderVariable.entityId, - timeSliderVariable.variableId, + variable.entityId, + variable.variableId, { valueSpec: 'count', filters: filters ?? [], @@ -160,12 +160,7 @@ export default function DraggableTimeFilter({ // conditionally set y-values to be 1 (with data) and 0 (no data) y: distributionResponse.histogram.map((d) => (d.value >= 1 ? 1 : 0)), }; - }, [ - timeSliderVariableMetadata?.variable, - timeSliderVariable, - subsettingClient, - filters, - ]) + }, [variableMetadata?.variable, variable, subsettingClient, filters]) ); // converting data to visx format @@ -192,10 +187,6 @@ export default function DraggableTimeFilter({ y: yPosition, }; - // set DraggablePanel key to update time slider - // TO DO: leaving this here for now but may need to remove? - const [draggablePanelKey, setDraggablePanelKey] = useState(0); - // inputVariables onChange function function handleInputVariablesOnChange(selection: VariablesByInputName) { if (!selection.overlayVariable) { @@ -206,10 +197,13 @@ export default function DraggableTimeFilter({ } // set time slider variable - setTimeSliderVariable(selection.overlayVariable); + setVariable(selection.overlayVariable); + setSelectedRange(undefined); } - // change selectedRange considering async data request + // update selectedRange when the time slider data changes + // TO DO: consider only resetting range when the variable changes + // so probably when selectedRange is nullish useEffect(() => { if (!getTimeSliderData.pending && getTimeSliderData.value != null) { setSelectedRange({ @@ -222,7 +216,6 @@ export default function DraggableTimeFilter({ // if no variable in a study is suitable to time slider, do not show time slider return defaultTimeSliderVariable != null ? ( 0 && ( <> {/* )} */} diff --git a/packages/libs/eda/src/lib/map/analysis/appState.ts b/packages/libs/eda/src/lib/map/analysis/appState.ts index f27e62a375..3c9ea42d4d 100644 --- a/packages/libs/eda/src/lib/map/analysis/appState.ts +++ b/packages/libs/eda/src/lib/map/analysis/appState.ts @@ -98,6 +98,12 @@ export const AppState = t.intersection([ variableId: t.string, }), isSubsetPanelOpen: t.boolean, + timeSliderVariable: VariableDescriptor, + timeSliderSelectedRange: t.type({ + start: t.string, + end: t.string, + }), + timeSliderActive: t.boolean, }), ]); @@ -224,5 +230,8 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { setIsSubsetPanelOpen: useSetter('isSubsetPanelOpen'), setSubsetVariableAndEntity: useSetter('subsetVariableAndEntity'), setViewport: useSetter('viewport'), + setTimeSliderVariable: useSetter('timeSliderVariable'), + setTimeSliderSelectedRange: useSetter('timeSliderSelectedRange'), + setTimeSliderActive: useSetter('timeSliderActive'), }; } From a5bf210482d93d4c70cb7e2bbd102f68f70a3a9c Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 7 Sep 2023 13:18:40 +0100 Subject: [PATCH 15/27] refactored default time variable into appState initialisation and got rid of a useEffect --- .../libs/eda/src/lib/core/hooks/workspace.ts | 68 +++++++++++++++++- .../lib/map/analysis/DraggableTimeFilter.tsx | 72 ++----------------- .../libs/eda/src/lib/map/analysis/appState.ts | 22 +++++- 3 files changed, 91 insertions(+), 71 deletions(-) diff --git a/packages/libs/eda/src/lib/core/hooks/workspace.ts b/packages/libs/eda/src/lib/core/hooks/workspace.ts index 2589da46f9..5949bd0264 100755 --- a/packages/libs/eda/src/lib/core/hooks/workspace.ts +++ b/packages/libs/eda/src/lib/core/hooks/workspace.ts @@ -24,13 +24,20 @@ import { import { ComputeClient } from '../api/ComputeClient'; import { DownloadClient } from '../api'; import { Filter } from '../types/filter'; -import { mapStructure } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; +import { + mapStructure, + preorder, +} from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; import { useFeaturedFieldsFromTree, useFieldTree, useFlattenedFields, } from '../components/variableTrees/hooks'; import { findFirstVariable } from '../../workspace/Utils'; +import { + DataElementConstraintRecord, + filterVariablesByConstraint, +} from '../utils/data-element-constraints'; /** Return the study identifier and a hierarchy of the study entities. */ export function useStudyMetadata(): StudyMetadata { @@ -262,3 +269,62 @@ export function useGetDefaultVariableDescriptor() { [entities, featuredFields, fieldTree] ); } + +export const timeSliderVariableConstraints: DataElementConstraintRecord[] = [ + { + overlayVariable: { + isRequired: true, + minNumVars: 1, + maxNumVars: 1, + // TODO: testing with SCORE S. mansoni Cluster Randomized Trial study + // however, this study does not have date variable, thus temporarily use below for test purpose + // i.e., additionally allowing 'integer' + allowedTypes: ['date', 'integer'], + // TODO: below two are correct ones + // allowedTypes: ['date'], + // isTemporal: true, + }, + }, +]; + +export function useGetDefaultTimeVariableDescriptor() { + const entities = useStudyEntities(); + // filter constraint for time slider inputVariables component + + return useCallback( + function getDefaultTimeVariableDescriptor() { + const temporalVariableTree = filterVariablesByConstraint( + entities[0], + timeSliderVariableConstraints[0]['overlayVariable'] + ); + + // take the first suitable variable from the filtered variable tree + + // first find the first entity with some variables that passed the filter + const defaultTimeSliderEntity: StudyEntity | undefined = Array.from( + preorder(temporalVariableTree, (entity) => entity.children ?? []) + ) + // not all `variables` are actually variables, so we filter to be sure + .filter( + (entity) => + entity.variables.filter((variable) => Variable.is(variable)) + .length > 0 + )[0]; + + // then take the first variable from it + const defaultTimeSliderVariable: Variable | undefined = + defaultTimeSliderEntity.variables.filter( + (variable): variable is Variable => Variable.is(variable) + )[0]; + + return defaultTimeSliderEntity != null && + defaultTimeSliderVariable != null + ? { + entityId: defaultTimeSliderEntity.id, + variableId: defaultTimeSliderVariable.id, + } + : undefined; + }, + [entities, timeSliderVariableConstraints] + ); +} diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index 5403bfaee2..e56afb082d 100755 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -1,33 +1,21 @@ -import { useState, useMemo, useEffect, useCallback } from 'react'; +import { useMemo, useEffect, useCallback } from 'react'; import { DraggablePanel } from '@veupathdb/coreui/lib/components/containers'; import EzTimeFilter, { EZTimeFilterDataProp, } from '@veupathdb/components/lib/components/plotControls/EzTimeFilter'; import { InputVariables } from '../../core/components/visualizations/InputVariables'; -import { - VariablesByInputName, - DataElementConstraintRecord, - filterVariablesByConstraint, -} from '../../core/utils/data-element-constraints'; -import { AnalysisState, usePromise } from '../../core'; +import { VariablesByInputName } from '../../core/utils/data-element-constraints'; +import { timeSliderVariableConstraints, usePromise } from '../../core'; import { DateVariable, NumberVariable, StudyEntity, - Variable, } from '../../core/types/study'; import { VariableDescriptor } from '../../core/types/variable'; import { SubsettingClient } from '../../core/api'; import Spinner from '@veupathdb/components/lib/components/Spinner'; import { useFindEntityAndVariable, Filter } from '../../core'; -import { - DateRange, - NumberRange, -} from '@veupathdb/components/lib/types/general'; -import { DateRangeFilter, NumberRangeFilter } from '../../core/types/filter'; -import { Tooltip } from '@material-ui/core'; -import { preorder } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; import { zip } from 'lodash'; import { AppState } from './appState'; @@ -62,58 +50,6 @@ export default function DraggableTimeFilter({ active, // to do - add a toggle to enable/disable setActive, // the small filter and grey everything out }: Props) { - // filter constraint for time slider inputVariables component - const timeSliderVariableConstraints: DataElementConstraintRecord[] = [ - { - overlayVariable: { - isRequired: true, - minNumVars: 1, - maxNumVars: 1, - // TODO: testing with SCORE S. mansoni Cluster Randomized Trial study - // however, this study does not have date variable, thus temporarily use below for test purpose - // i.e., additionally allowing 'integer' - allowedTypes: ['date', 'integer'], - // TODO: below two are correct ones - // allowedTypes: ['date'], - // isTemporal: true, - }, - }, - ]; - - const temporalVariableTree = filterVariablesByConstraint( - entities[0], - timeSliderVariableConstraints[0]['overlayVariable'] - ); - - // take the first suitable variable from the filtered variable tree - - // first find the first entity with some variables that passed the filter - const defaultTimeSliderEntity: StudyEntity | undefined = Array.from( - preorder(temporalVariableTree, (entity) => entity.children ?? []) - ) - // not all `variables` are actually variables, so we filter to be sure - .filter( - (entity) => - entity.variables.filter((variable) => Variable.is(variable)).length > 0 - )[0]; - - // then take the first variable from it - const defaultTimeSliderVariable: Variable | undefined = - defaultTimeSliderEntity.variables.filter((variable): variable is Variable => - Variable.is(variable) - )[0]; - - // sorry, a useEffect for initialising the default variable - useEffect(() => { - if (variable == null && defaultTimeSliderVariable != null) { - setVariable({ - variableId: defaultTimeSliderVariable.id, - entityId: defaultTimeSliderEntity.id, - }); - } - }, [variable, defaultTimeSliderVariable]); - - // find variable metadata: use timeSliderVariable, not defaultTimeSlider ids const findEntityAndVariable = useFindEntityAndVariable(); const variableMetadata = findEntityAndVariable(variable); @@ -214,7 +150,7 @@ export default function DraggableTimeFilter({ }, [getTimeSliderData]); // if no variable in a study is suitable to time slider, do not show time slider - return defaultTimeSliderVariable != null ? ( + return variable != null ? ( ({ viewport: defaultViewport, mouseMode: 'default', activeMarkerConfigurationType: 'pie', + timeSliderVariable: defaultTimeVariable, markerConfigurations: [ { type: 'pie', @@ -182,11 +188,16 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { ) ); - if (missingMarkerConfigs.length > 0) { + const timeVariableIsMissing = appState.timeSliderVariable == null; + + if (missingMarkerConfigs.length > 0 || timeVariableIsMissing) { setVariableUISettings((prev) => ({ ...prev, [uiStateKey]: { ...appState, + ...(timeVariableIsMissing + ? { timeSliderVariable: defaultTimeVariable } + : {}), markerConfigurations: [ ...appState.markerConfigurations, ...missingMarkerConfigs, @@ -196,7 +207,14 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { } } } - }, [analysis, appState, setVariableUISettings, uiStateKey, defaultAppState]); + }, [ + analysis, + appState, + setVariableUISettings, + uiStateKey, + defaultAppState, + defaultTimeVariable, + ]); function useSetter(key: T) { return useCallback( From 9900b4294b9d0def2aadf8c7abfd286c69ad98df Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 7 Sep 2023 14:43:15 +0100 Subject: [PATCH 16/27] removed onClick and maybe fixed a coordinate issue by removing a Number() conversion --- .../components/plotControls/EzTimeFilter.tsx | 29 ++----------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx index a3a32e33fb..3984a0f983 100755 --- a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx +++ b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx @@ -133,8 +133,8 @@ function EzTimeFilter(props: EzTimeFilterProps) { // initial selectedRange position const initialBrushPosition = useMemo( () => ({ - start: { x: xBrushScale(Number(new Date(selectedRange.start))) }, - end: { x: xBrushScale(Number(new Date(selectedRange.end))) }, + start: { x: xBrushScale(new Date(selectedRange.start)) }, + end: { x: xBrushScale(new Date(selectedRange.end)) }, }), [selectedRange, xBrushScale] ); @@ -148,30 +148,6 @@ function EzTimeFilter(props: EzTimeFilterProps) { // data bar color const defaultColor = '#333'; - // TO DO: this no longer works as intended because initialBrushPosition isn't the "reset position" any more - // but consider a separate reset button anyway? (which would just setSelectedRange to undefined I think) - const handleResetClick = () => { - if (brushRef?.current) { - const updater: UpdateBrush = (prevBrush) => { - const newExtent = brushRef.current!.getExtent( - initialBrushPosition.start, - initialBrushPosition.end - ); - - const newState: BaseBrushState = { - ...prevBrush, - start: { y: newExtent.y0, x: newExtent.x0 }, - end: { y: newExtent.y1, x: newExtent.x1 }, - extent: newExtent, - }; - - return newState; - }; - - brushRef.current.updateBrush(updater); - } - }; - // debounce function for onBrushEnd: will be used for submitting filtered range later const debouncedOnBrushEnd = useMemo( () => debounce(onBrushEnd, debounceRateMs), @@ -240,7 +216,6 @@ function EzTimeFilter(props: EzTimeFilterProps) { brushDirection="horizontal" initialBrushPosition={initialBrushPosition} onChange={onBrushChange} - onClick={handleResetClick} selectedBoxStyle={selectedBrushStyle} useWindowMoveEvents disableDraggingSelection={disableDraggingSelection} From df41bd4466e8959454d734ff9a2992baa8b92cfe Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 7 Sep 2023 17:22:51 +0100 Subject: [PATCH 17/27] more tidy up --- .../src/components/plotControls/EzTimeFilter.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx index 3984a0f983..b2644f24ba 100755 --- a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx +++ b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx @@ -4,10 +4,6 @@ import { scaleTime, scaleLinear } from '@visx/scale'; import { Brush } from '@visx/brush'; // add ResizeTriggerAreas type import { Bounds, ResizeTriggerAreas } from '@visx/brush/lib/types'; -import BaseBrush, { - BaseBrushState, - UpdateBrush, -} from '@visx/brush/lib/BaseBrush'; import { Group } from '@visx/group'; import { max, extent } from 'd3-array'; import { BrushHandleRenderProps } from '@visx/brush/lib/BrushHandle'; @@ -64,8 +60,6 @@ function EzTimeFilter(props: EzTimeFilterProps) { debounceRateMs = 500, } = props; - const brushRef = useRef(null); - // define default values const margin = { top: 10, bottom: 10, left: 10, right: 10 }; const selectedBrushStyle = { @@ -210,7 +204,6 @@ function EzTimeFilter(props: EzTimeFilterProps) { height={yBrushMax} margin={margin} handleSize={8} - innerRef={brushRef} // resize resizeTriggerAreas={resizeTriggerAreas} brushDirection="horizontal" From 1e8fe837e3a5c08ed8533a43c344eb60d0a63932 Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 7 Sep 2023 18:51:21 +0100 Subject: [PATCH 18/27] fix cancel click and debounce the selectedRange update --- .../components/plotControls/EzTimeFilter.tsx | 72 ++++++++++--------- .../plotControls/EzTimeFilter.stories.tsx | 4 +- .../lib/map/analysis/DraggableTimeFilter.tsx | 13 ---- 3 files changed, 42 insertions(+), 47 deletions(-) diff --git a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx index b2644f24ba..04253b7e7f 100755 --- a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx +++ b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx @@ -21,9 +21,11 @@ export type EzTimeFilterProps = { /** Ez time filter data */ data: EZTimeFilterDataProp[]; /** current state of selectedRange */ - selectedRange: { start: string; end: string }; + selectedRange: { start: string; end: string } | undefined; /** update function selectedRange */ - setSelectedRange: (selectedRange: EzTimeFilterProps['selectedRange']) => void; + setSelectedRange: ( + selectedRange: { start: string; end: string } | undefined + ) => void; /** width */ width?: number; /** height */ @@ -82,19 +84,30 @@ function EzTimeFilter(props: EzTimeFilterProps) { const getXData = (d: EZTimeFilterDataProp) => new Date(d.x); const getYData = (d: EZTimeFilterDataProp) => d.y; - const onBrushChange = (domain: Bounds | null) => { - if (!domain) return; + const onBrushChange = useMemo( + () => + debounce((domain: Bounds | null) => { + if (!domain) return; - const { x0, x1 } = domain; + const { x0, x1 } = domain; - const selectedDomain = { - // x0 and x1 are millisecond value - start: millisecondTodate(x0), - end: millisecondTodate(x1), - }; + const selectedDomain = { + // x0 and x1 are millisecond value + start: millisecondTodate(x0), + end: millisecondTodate(x1), + }; - setSelectedRange(selectedDomain); - }; + setSelectedRange(selectedDomain); + }, debounceRateMs), + [setSelectedRange] + ); + + // Cancel pending onBrushEnd request when this component is unmounted + useEffect(() => { + return () => { + onBrushChange.cancel(); + }; + }, []); // bounds const xBrushMax = Math.max(width - margin.left - margin.right, 0); @@ -126,34 +139,27 @@ function EzTimeFilter(props: EzTimeFilterProps) { // initial selectedRange position const initialBrushPosition = useMemo( - () => ({ - start: { x: xBrushScale(new Date(selectedRange.start)) }, - end: { x: xBrushScale(new Date(selectedRange.end)) }, - }), + () => + selectedRange != null + ? { + start: { x: xBrushScale(new Date(selectedRange.start)) }, + end: { x: xBrushScale(new Date(selectedRange.end)) }, + } + : undefined, [selectedRange, xBrushScale] ); // compute bar width manually as scaleTime is used for Bar chart const barWidth = xBrushMax / data.length; - // make an event after dragging ends - const onBrushEnd = () => {}; - // data bar color const defaultColor = '#333'; - // debounce function for onBrushEnd: will be used for submitting filtered range later - const debouncedOnBrushEnd = useMemo( - () => debounce(onBrushEnd, debounceRateMs), - [onBrushEnd] - ); - - // Cancel pending onBrushEnd request when this component is unmounted - useEffect(() => { - return () => { - debouncedOnBrushEnd.cancel(); - }; - }, []); + // this makes/fakes the brush as a controlled component + const brushKey = + initialBrushPosition != null + ? initialBrushPosition.start + ':' + initialBrushPosition.end + : 'no_brush'; return (
setSelectedRange(undefined)} selectedBoxStyle={selectedBrushStyle} useWindowMoveEvents disableDraggingSelection={disableDraggingSelection} - onBrushEnd={debouncedOnBrushEnd} renderBrushHandle={(props) => } /> diff --git a/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx b/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx index 9b251c5bad..4e89271672 100755 --- a/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx +++ b/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx @@ -254,7 +254,9 @@ export const TimeFilter: Story = (args: any) => { ); // set initial selectedRange - const [selectedRange, setSelectedRange] = useState({ + const [selectedRange, setSelectedRange] = useState< + { start: string; end: string } | undefined + >({ start: timeFilterData[0].x, end: timeFilterData[timeFilterData.length - 1].x, }); diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index e56afb082d..ad9a2159b6 100755 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -137,18 +137,6 @@ export default function DraggableTimeFilter({ setSelectedRange(undefined); } - // update selectedRange when the time slider data changes - // TO DO: consider only resetting range when the variable changes - // so probably when selectedRange is nullish - useEffect(() => { - if (!getTimeSliderData.pending && getTimeSliderData.value != null) { - setSelectedRange({ - start: getTimeSliderData.value.x[0], - end: getTimeSliderData.value.x[getTimeSliderData.value.x.length - 1], - }); - } - }, [getTimeSliderData]); - // if no variable in a study is suitable to time slider, do not show time slider return variable != null ? ( 0 && ( <> Date: Thu, 7 Sep 2023 19:43:27 +0100 Subject: [PATCH 19/27] added on/off toggle --- .../components/plotControls/EzTimeFilter.tsx | 2 +- .../lib/map/analysis/DraggableTimeFilter.tsx | 31 ++++++++++++++++--- .../libs/eda/src/lib/map/analysis/appState.ts | 1 + 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx index 04253b7e7f..4d49344a49 100755 --- a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx +++ b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx @@ -102,7 +102,7 @@ function EzTimeFilter(props: EzTimeFilterProps) { [setSelectedRange] ); - // Cancel pending onBrushEnd request when this component is unmounted + // Cancel any pending onBrushChange requests when this component is unmounted useEffect(() => { return () => { onBrushChange.cancel(); diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index ad9a2159b6..5751f12cfb 100755 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -1,5 +1,6 @@ -import { useMemo, useEffect, useCallback } from 'react'; +import { useMemo, useCallback } from 'react'; import { DraggablePanel } from '@veupathdb/coreui/lib/components/containers'; +import { Toggle } from '@veupathdb/coreui'; import EzTimeFilter, { EZTimeFilterDataProp, } from '@veupathdb/components/lib/components/plotControls/EzTimeFilter'; @@ -162,11 +163,12 @@ export default function DraggableTimeFilter({ display: 'grid', gridTemplateColumns: '1fr repeat(1, auto) 1fr', gridColumnGap: '5px', + padding: '0 10px 0 10px', justifyContent: 'center', alignItems: 'center', }} > -
+
- {/* display start to end value */} -
- {selectedRange?.start} ~ {selectedRange?.end} + {/* display start to end value + TO DO: make these date inputs + */} + {selectedRange && ( +
+ {selectedRange?.start} ~ {selectedRange?.end} +
+ )} + +
+
{/* display data loading spinner while requesting data to the backend */} diff --git a/packages/libs/eda/src/lib/map/analysis/appState.ts b/packages/libs/eda/src/lib/map/analysis/appState.ts index d9d81f5d22..3d6da92db4 100644 --- a/packages/libs/eda/src/lib/map/analysis/appState.ts +++ b/packages/libs/eda/src/lib/map/analysis/appState.ts @@ -142,6 +142,7 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { mouseMode: 'default', activeMarkerConfigurationType: 'pie', timeSliderVariable: defaultTimeVariable, + timeSliderActive: true, markerConfigurations: [ { type: 'pie', From edcac9dcfa84fecc2aea5d6358266873fb16927d Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 7 Sep 2023 23:11:40 +0100 Subject: [PATCH 20/27] final wiring of little filters done, hopefully --- .../lib/map/analysis/DraggableTimeFilter.tsx | 2 +- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 99 ++++++++++++++----- 2 files changed, 78 insertions(+), 23 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx index 5751f12cfb..f468206648 100755 --- a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx @@ -133,9 +133,9 @@ export default function DraggableTimeFilter({ return; } - // set time slider variable setVariable(selection.overlayVariable); setSelectedRange(undefined); + setActive(true); } // if no variable in a study is suitable to time slider, do not show time slider diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index e98f80ba9e..c24602236a 100755 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -6,7 +6,11 @@ import { BubbleOverlayConfig, CategoricalVariableDataShape, DEFAULT_ANALYSIS_NAME, + DateRangeFilter, + DateVariable, EntityDiagram, + NumberRangeFilter, + NumberVariable, OverlayConfig, PromiseResult, useAnalysis, @@ -277,6 +281,9 @@ function MapAnalysisImpl(props: ImplProps) { const { variable: overlayVariable, entity: overlayEntity } = findEntityAndVariable(activeMarkerConfiguration?.selectedVariable) ?? {}; + const { variable: timeVariableMetadata } = + findEntityAndVariable(appState.timeSliderVariable) ?? {}; + const outputEntity = useMemo(() => { if (geoConfig == null || geoConfig.entity.id == null) return; @@ -300,32 +307,80 @@ function MapAnalysisImpl(props: ImplProps) { [markerConfigurations, setMarkerConfigurations] ); - const filtersIncludingViewport = useMemo(() => { - const viewportFilters = appState.boundsZoomLevel - ? filtersFromBoundingBox( - appState.boundsZoomLevel.bounds, - { - variableId: geoConfig.latitudeVariableId, - entityId: geoConfig.entity.id, - }, - { - variableId: geoConfig.longitudeVariableId, - entityId: geoConfig.entity.id, - } - ) - : []; + const timeFilter: NumberRangeFilter | DateRangeFilter | undefined = useMemo( + () => + timeVariableMetadata && + appState.timeSliderActive && + appState.timeSliderVariable && + appState.timeSliderSelectedRange + ? DateVariable.is(timeVariableMetadata) + ? { + type: 'dateRange', + ...appState.timeSliderVariable, + min: appState.timeSliderSelectedRange.start, + max: appState.timeSliderSelectedRange.end, + } + : NumberVariable.is(timeVariableMetadata) + ? { + type: 'numberRange', // this is temporary - I think we should NOT handle non-date variables when we roll this out + ...appState.timeSliderVariable, // TO DO: remove number variable handling + min: Number(appState.timeSliderSelectedRange.start.split(/-/)[0]), // just take the year number + max: Number(appState.timeSliderSelectedRange.end.split(/-/)[0]), // from the YYYY-MM-DD returned from the widget + } + : undefined + : undefined, + [ + timeVariableMetadata, + appState.timeSliderActive, + appState.timeSliderVariable, + appState.timeSliderSelectedRange, + ] + ); + + const viewportFilters = useMemo( + () => + appState.boundsZoomLevel + ? filtersFromBoundingBox( + appState.boundsZoomLevel.bounds, + { + variableId: geoConfig.latitudeVariableId, + entityId: geoConfig.entity.id, + }, + { + variableId: geoConfig.longitudeVariableId, + entityId: geoConfig.entity.id, + } + ) + : [], + [ + appState.boundsZoomLevel, + geoConfig.entity.id, + geoConfig.latitudeVariableId, + geoConfig.longitudeVariableId, + ] + ); + + // needed for floaters + const filtersIncludingViewportAndTimeSlider = useMemo(() => { return [ ...(props.analysisState.analysis?.descriptor.subset.descriptor ?? []), ...viewportFilters, + ...(timeFilter != null ? [timeFilter] : []), ]; }, [ - appState.boundsZoomLevel, - geoConfig.entity.id, - geoConfig.latitudeVariableId, - geoConfig.longitudeVariableId, props.analysisState.analysis?.descriptor.subset.descriptor, + viewportFilters, + timeFilter, ]); + // needed for markers + const filtersIncludingTimeSlider = useMemo(() => { + return [ + ...(props.analysisState.analysis?.descriptor.subset.descriptor ?? []), + ...(timeFilter != null ? [timeFilter] : []), + ]; + }, [props.analysisState.analysis?.descriptor.subset.descriptor, timeFilter]); + const allFilteredCategoricalValues = usePromise( useCallback(async (): Promise => { /** @@ -367,7 +422,7 @@ function MapAnalysisImpl(props: ImplProps) { subsettingClient, studyId, overlayVariable, - filters: filtersIncludingViewport, + filters: filtersIncludingViewportAndTimeSlider, // TO DO: decide whether to filter on time slider here }); }, [ overlayVariable, @@ -375,7 +430,7 @@ function MapAnalysisImpl(props: ImplProps) { overlayEntity, subsettingClient, studyId, - filtersIncludingViewport, + filtersIncludingViewportAndTimeSlider, ]) ); @@ -457,7 +512,7 @@ function MapAnalysisImpl(props: ImplProps) { boundsZoomLevel: appState.boundsZoomLevel, geoConfig: geoConfig, studyId, - filters, + filters: filtersIncludingTimeSlider, markerType, selectedOverlayVariable: activeMarkerConfiguration?.selectedVariable, overlayConfig: activeOverlayConfig.value, @@ -1353,7 +1408,7 @@ function MapAnalysisImpl(props: ImplProps) { totalCounts={totalCounts} filteredCounts={filteredCounts} toggleStarredVariable={toggleStarredVariable} - filters={filtersIncludingViewport} + filters={filtersIncludingViewportAndTimeSlider} // onTouch={moveVizToTop} zIndexForStackingContext={getZIndexByPanelTitle( DraggablePanelIds.VIZ_PANEL From 770e64b37c7e11eae1605b36191c721ac7ca78af Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 8 Sep 2023 20:43:45 +0100 Subject: [PATCH 21/27] fix infinite loop issues with appState initialisation, fix little timeFilter formatting --- .../libs/eda/src/lib/core/hooks/workspace.ts | 25 +++++++------------ ...aggableTimeFilter.tsx => EZTimeFilter.tsx} | 0 .../libs/eda/src/lib/map/analysis/appState.ts | 9 +++++-- 3 files changed, 16 insertions(+), 18 deletions(-) rename packages/libs/eda/src/lib/map/analysis/{DraggableTimeFilter.tsx => EZTimeFilter.tsx} (100%) diff --git a/packages/libs/eda/src/lib/core/hooks/workspace.ts b/packages/libs/eda/src/lib/core/hooks/workspace.ts index 5949bd0264..29828f510a 100755 --- a/packages/libs/eda/src/lib/core/hooks/workspace.ts +++ b/packages/libs/eda/src/lib/core/hooks/workspace.ts @@ -279,10 +279,10 @@ export const timeSliderVariableConstraints: DataElementConstraintRecord[] = [ // TODO: testing with SCORE S. mansoni Cluster Randomized Trial study // however, this study does not have date variable, thus temporarily use below for test purpose // i.e., additionally allowing 'integer' - allowedTypes: ['date', 'integer'], + // allowedTypes: ['date', 'integer'], // TODO: below two are correct ones - // allowedTypes: ['date'], - // isTemporal: true, + allowedTypes: ['date'], + // isTemporal: true, }, }, ]; @@ -301,21 +301,14 @@ export function useGetDefaultTimeVariableDescriptor() { // take the first suitable variable from the filtered variable tree // first find the first entity with some variables that passed the filter - const defaultTimeSliderEntity: StudyEntity | undefined = Array.from( - preorder(temporalVariableTree, (entity) => entity.children ?? []) - ) - // not all `variables` are actually variables, so we filter to be sure - .filter( - (entity) => - entity.variables.filter((variable) => Variable.is(variable)) - .length > 0 - )[0]; + const defaultTimeSliderEntity = Array.from( + preorder(temporalVariableTree, (node) => node.children ?? []) + ).find((entity) => entity.variables.some(Variable.is)); // then take the first variable from it - const defaultTimeSliderVariable: Variable | undefined = - defaultTimeSliderEntity.variables.filter( - (variable): variable is Variable => Variable.is(variable) - )[0]; + const defaultTimeSliderVariable = defaultTimeSliderEntity?.variables.find( + Variable.is + ); return defaultTimeSliderEntity != null && defaultTimeSliderVariable != null diff --git a/packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx similarity index 100% rename from packages/libs/eda/src/lib/map/analysis/DraggableTimeFilter.tsx rename to packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx diff --git a/packages/libs/eda/src/lib/map/analysis/appState.ts b/packages/libs/eda/src/lib/map/analysis/appState.ts index 3d6da92db4..89132d1cfc 100644 --- a/packages/libs/eda/src/lib/map/analysis/appState.ts +++ b/packages/libs/eda/src/lib/map/analysis/appState.ts @@ -2,7 +2,7 @@ import { getOrElseW } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/function'; import * as t from 'io-ts'; import { isEqual } from 'lodash'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { AnalysisState, useGetDefaultTimeVariableDescriptor, @@ -169,10 +169,14 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { }, ], }), - [defaultVariable] + [defaultVariable, defaultTimeVariable] ); + // make some backwards compatability updates to the appstate retrieved from the back end + const appStateCheckedRef = useRef(false); + useEffect(() => { + if (appStateCheckedRef.current) return; if (analysis) { if (!appState) { setVariableUISettings((prev) => ({ @@ -207,6 +211,7 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { })); } } + appStateCheckedRef.current = true; } }, [ analysis, From dfc78024439c450f208fe94f0530eef1875a2e66 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 8 Sep 2023 21:31:41 +0100 Subject: [PATCH 22/27] repositioned ez filter under header --- .../eda/src/lib/map/analysis/EZTimeFilter.tsx | 182 ++++++++---------- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 45 ++--- .../eda/src/lib/map/analysis/MapHeader.tsx | 22 +++ 3 files changed, 128 insertions(+), 121 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx index f468206648..a0099a5a06 100755 --- a/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx @@ -1,4 +1,4 @@ -import { useMemo, useCallback } from 'react'; +import { useMemo, useCallback, ReactNode } from 'react'; import { DraggablePanel } from '@veupathdb/coreui/lib/components/containers'; import { Toggle } from '@veupathdb/coreui'; import EzTimeFilter, { @@ -37,7 +37,7 @@ interface Props { setActive: (newState: AppState['timeSliderActive']) => void; } -export default function DraggableTimeFilter({ +export default function EZTimeFilter({ studyId, entities, subsettingClient, @@ -117,12 +117,6 @@ export default function DraggableTimeFilter({ // set time slider width and y position const timeFilterWidth = 750; - const yPosition = 100; // TEMP: moved it down so I can see it all - - const defaultPosition = { - x: window.innerWidth / 2 - timeFilterWidth / 2, - y: yPosition, - }; // inputVariables onChange function function handleInputVariablesOnChange(selection: VariablesByInputName) { @@ -140,109 +134,99 @@ export default function DraggableTimeFilter({ // if no variable in a study is suitable to time slider, do not show time slider return variable != null ? ( -
+
+ +
+ {/* display start to end value + TO DO: make these date inputs + */} + {selectedRange && ( +
+ {selectedRange?.start} ~ {selectedRange?.end} +
+ )} +
-
- -
- {/* display start to end value - TO DO: make these date inputs - */} - {selectedRange && ( -
- {selectedRange?.start} ~ {selectedRange?.end} -
- )} - -
- -
+
- {/* display data loading spinner while requesting data to the backend */} - {getTimeSliderData.pending && ( -
- -
- )} - {/* conditional loading for EzTimeFilter */} - {!getTimeSliderData.pending && - getTimeSliderData.value != null && - timeFilterData.length > 0 && ( - <> - - - )}
-
+ {/* display data loading spinner while requesting data to the backend */} + {getTimeSliderData.pending && ( +
+ +
+ )} + {/* conditional loading for EzTimeFilter */} + {!getTimeSliderData.pending && + getTimeSliderData.value != null && + timeFilterData.length > 0 && ( + <> + + + )} +
) : null; } diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index c24602236a..25cf4e0d86 100755 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -114,7 +114,7 @@ import { getCategoricalValues } from './utils/categoricalValues'; import { DraggablePanelCoordinatePair } from '@veupathdb/coreui/lib/components/containers/DraggablePanel'; import _ from 'lodash'; -import DraggableTimeFilter from './DraggableTimeFilter'; +import EZTimeFilter from './EZTimeFilter'; enum MapSideNavItemLabels { Download = 'Download', @@ -317,8 +317,8 @@ function MapAnalysisImpl(props: ImplProps) { ? { type: 'dateRange', ...appState.timeSliderVariable, - min: appState.timeSliderSelectedRange.start, - max: appState.timeSliderSelectedRange.end, + min: appState.timeSliderSelectedRange.start + 'T00:00:00Z', + max: appState.timeSliderSelectedRange.end + 'T00:00:00Z', } : NumberVariable.is(timeVariableMetadata) ? { @@ -1249,7 +1249,26 @@ function MapAnalysisImpl(props: ImplProps) { totalVisibleEntityCount } overlayActive={overlayVariable != null} - /> + > + {/* child elements will be distributed across, 'hanging' below the header */} + {/* Time slider component */} + +
)} - {/* Time slider component */} - - {/* )} */} {/*
)} + {children} ); } +function HangingTabs({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + type HeaderContentProps = { analysisName?: string; filterList?: ReactNode; From 8b4c75fa0092f8617863c11386e7c4e26433a6b9 Mon Sep 17 00:00:00 2001 From: Bob Date: Sat, 9 Sep 2023 12:26:11 +0100 Subject: [PATCH 23/27] reorganised code as recommended by Dave --- .../libs/eda/src/lib/core/hooks/workspace.ts | 61 +------------------ .../eda/src/lib/map/analysis/EZTimeFilter.tsx | 8 +-- .../libs/eda/src/lib/map/analysis/appState.ts | 2 +- .../lib/map/analysis/config/eztimeslider.ts | 18 ++++++ .../lib/map/analysis/hooks/eztimeslider.ts | 40 ++++++++++++ 5 files changed, 64 insertions(+), 65 deletions(-) create mode 100644 packages/libs/eda/src/lib/map/analysis/config/eztimeslider.ts create mode 100644 packages/libs/eda/src/lib/map/analysis/hooks/eztimeslider.ts diff --git a/packages/libs/eda/src/lib/core/hooks/workspace.ts b/packages/libs/eda/src/lib/core/hooks/workspace.ts index 29828f510a..2589da46f9 100755 --- a/packages/libs/eda/src/lib/core/hooks/workspace.ts +++ b/packages/libs/eda/src/lib/core/hooks/workspace.ts @@ -24,20 +24,13 @@ import { import { ComputeClient } from '../api/ComputeClient'; import { DownloadClient } from '../api'; import { Filter } from '../types/filter'; -import { - mapStructure, - preorder, -} from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; +import { mapStructure } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; import { useFeaturedFieldsFromTree, useFieldTree, useFlattenedFields, } from '../components/variableTrees/hooks'; import { findFirstVariable } from '../../workspace/Utils'; -import { - DataElementConstraintRecord, - filterVariablesByConstraint, -} from '../utils/data-element-constraints'; /** Return the study identifier and a hierarchy of the study entities. */ export function useStudyMetadata(): StudyMetadata { @@ -269,55 +262,3 @@ export function useGetDefaultVariableDescriptor() { [entities, featuredFields, fieldTree] ); } - -export const timeSliderVariableConstraints: DataElementConstraintRecord[] = [ - { - overlayVariable: { - isRequired: true, - minNumVars: 1, - maxNumVars: 1, - // TODO: testing with SCORE S. mansoni Cluster Randomized Trial study - // however, this study does not have date variable, thus temporarily use below for test purpose - // i.e., additionally allowing 'integer' - // allowedTypes: ['date', 'integer'], - // TODO: below two are correct ones - allowedTypes: ['date'], - // isTemporal: true, - }, - }, -]; - -export function useGetDefaultTimeVariableDescriptor() { - const entities = useStudyEntities(); - // filter constraint for time slider inputVariables component - - return useCallback( - function getDefaultTimeVariableDescriptor() { - const temporalVariableTree = filterVariablesByConstraint( - entities[0], - timeSliderVariableConstraints[0]['overlayVariable'] - ); - - // take the first suitable variable from the filtered variable tree - - // first find the first entity with some variables that passed the filter - const defaultTimeSliderEntity = Array.from( - preorder(temporalVariableTree, (node) => node.children ?? []) - ).find((entity) => entity.variables.some(Variable.is)); - - // then take the first variable from it - const defaultTimeSliderVariable = defaultTimeSliderEntity?.variables.find( - Variable.is - ); - - return defaultTimeSliderEntity != null && - defaultTimeSliderVariable != null - ? { - entityId: defaultTimeSliderEntity.id, - variableId: defaultTimeSliderVariable.id, - } - : undefined; - }, - [entities, timeSliderVariableConstraints] - ); -} diff --git a/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx index a0099a5a06..c8704d5bc6 100755 --- a/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx @@ -1,12 +1,11 @@ -import { useMemo, useCallback, ReactNode } from 'react'; -import { DraggablePanel } from '@veupathdb/coreui/lib/components/containers'; +import { useMemo, useCallback } from 'react'; import { Toggle } from '@veupathdb/coreui'; import EzTimeFilter, { EZTimeFilterDataProp, } from '@veupathdb/components/lib/components/plotControls/EzTimeFilter'; import { InputVariables } from '../../core/components/visualizations/InputVariables'; import { VariablesByInputName } from '../../core/utils/data-element-constraints'; -import { timeSliderVariableConstraints, usePromise } from '../../core'; +import { usePromise } from '../../core'; import { DateVariable, NumberVariable, @@ -19,6 +18,7 @@ import Spinner from '@veupathdb/components/lib/components/Spinner'; import { useFindEntityAndVariable, Filter } from '../../core'; import { zip } from 'lodash'; import { AppState } from './appState'; +import { timeSliderVariableConstraints } from './config/eztimeslider'; interface Props { studyId: string; @@ -158,7 +158,7 @@ export default function EZTimeFilter({ inputs={[ { name: 'overlayVariable', - label: 'Variable', + label: '', titleOverride: ' ', isNonNullable: true, }, diff --git a/packages/libs/eda/src/lib/map/analysis/appState.ts b/packages/libs/eda/src/lib/map/analysis/appState.ts index 89132d1cfc..3a11e23603 100644 --- a/packages/libs/eda/src/lib/map/analysis/appState.ts +++ b/packages/libs/eda/src/lib/map/analysis/appState.ts @@ -5,11 +5,11 @@ import { isEqual } from 'lodash'; import { useCallback, useEffect, useMemo, useRef } from 'react'; import { AnalysisState, - useGetDefaultTimeVariableDescriptor, useGetDefaultVariableDescriptor, useStudyMetadata, } from '../../core'; import { VariableDescriptor } from '../../core/types/variable'; +import { useGetDefaultTimeVariableDescriptor } from './hooks/eztimeslider'; const LatLngLiteral = t.type({ lat: t.number, lng: t.number }); diff --git a/packages/libs/eda/src/lib/map/analysis/config/eztimeslider.ts b/packages/libs/eda/src/lib/map/analysis/config/eztimeslider.ts new file mode 100644 index 0000000000..8d0931eb21 --- /dev/null +++ b/packages/libs/eda/src/lib/map/analysis/config/eztimeslider.ts @@ -0,0 +1,18 @@ +import { DataElementConstraintRecord } from '../../../core/utils/data-element-constraints'; + +export const timeSliderVariableConstraints: DataElementConstraintRecord[] = [ + { + overlayVariable: { + isRequired: true, + minNumVars: 1, + maxNumVars: 1, + // TODO: testing with SCORE S. mansoni Cluster Randomized Trial study + // however, this study does not have date variable, thus temporarily use below for test purpose + // i.e., additionally allowing 'integer' + // allowedTypes: ['date', 'integer'], + // TODO: below two are correct ones + allowedTypes: ['date'], + // isTemporal: true, + }, + }, +]; diff --git a/packages/libs/eda/src/lib/map/analysis/hooks/eztimeslider.ts b/packages/libs/eda/src/lib/map/analysis/hooks/eztimeslider.ts new file mode 100644 index 0000000000..714db9070b --- /dev/null +++ b/packages/libs/eda/src/lib/map/analysis/hooks/eztimeslider.ts @@ -0,0 +1,40 @@ +import { useCallback } from 'react'; +import { Variable, useStudyEntities } from '../../../core'; +import { filterVariablesByConstraint } from '../../../core/utils/data-element-constraints'; +import { timeSliderVariableConstraints } from '../config/eztimeslider'; +import { preorder } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; + +export function useGetDefaultTimeVariableDescriptor() { + const entities = useStudyEntities(); + // filter constraint for time slider inputVariables component + + return useCallback( + function getDefaultTimeVariableDescriptor() { + const temporalVariableTree = filterVariablesByConstraint( + entities[0], + timeSliderVariableConstraints[0]['overlayVariable'] + ); + + // take the first suitable variable from the filtered variable tree + + // first find the first entity with some variables that passed the filter + const defaultTimeSliderEntity = Array.from( + preorder(temporalVariableTree, (node) => node.children ?? []) + ).find((entity) => entity.variables.some(Variable.is)); + + // then take the first variable from it + const defaultTimeSliderVariable = defaultTimeSliderEntity?.variables.find( + Variable.is + ); + + return defaultTimeSliderEntity != null && + defaultTimeSliderVariable != null + ? { + entityId: defaultTimeSliderEntity.id, + variableId: defaultTimeSliderVariable.id, + } + : undefined; + }, + [entities, timeSliderVariableConstraints] + ); +} From 8b4b84e798d72688bf4ff7a8a1cfd350881d5c89 Mon Sep 17 00:00:00 2001 From: Bob Date: Sat, 9 Sep 2023 13:15:42 +0100 Subject: [PATCH 24/27] refactored timeSliderConfig --- .../eda/src/lib/map/analysis/EZTimeFilter.tsx | 32 ++++---- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 79 +++++++++---------- .../libs/eda/src/lib/map/analysis/appState.ts | 43 +++++----- 3 files changed, 72 insertions(+), 82 deletions(-) diff --git a/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx index c8704d5bc6..9dc8657479 100755 --- a/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx @@ -29,12 +29,8 @@ interface Props { starredVariables: VariableDescriptor[]; toggleStarredVariable: (targetVariableId: VariableDescriptor) => void; - variable: AppState['timeSliderVariable']; - setVariable: (newVariable: AppState['timeSliderVariable']) => void; - selectedRange: AppState['timeSliderSelectedRange']; - setSelectedRange: (newRange: AppState['timeSliderSelectedRange']) => void; - active: AppState['timeSliderActive']; - setActive: (newState: AppState['timeSliderActive']) => void; + config: NonNullable; + updateConfig: (newConfig: NonNullable) => void; } export default function EZTimeFilter({ @@ -44,14 +40,12 @@ export default function EZTimeFilter({ filters, starredVariables, toggleStarredVariable, - variable, - setVariable, - selectedRange, - setSelectedRange, - active, // to do - add a toggle to enable/disable - setActive, // the small filter and grey everything out + config, + updateConfig, }: Props) { const findEntityAndVariable = useFindEntityAndVariable(); + + const { variable, active, selectedRange } = config; const variableMetadata = findEntityAndVariable(variable); // data request to distribution for time slider @@ -127,9 +121,11 @@ export default function EZTimeFilter({ return; } - setVariable(selection.overlayVariable); - setSelectedRange(undefined); - setActive(true); + updateConfig({ + variable: selection.overlayVariable, + selectedRange: undefined, + active: true, + }); } // if no variable in a study is suitable to time slider, do not show time slider @@ -193,7 +189,7 @@ export default function EZTimeFilter({ label={active ? 'On' : 'Off'} labelPosition="left" value={!!active} - onChange={setActive} + onChange={(active) => updateConfig({ ...config, active })} />
@@ -211,7 +207,9 @@ export default function EZTimeFilter({ + updateConfig({ ...config, selectedRange }) + } width={timeFilterWidth - 30} height={100} // line color of the selectedRange diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index 25cf4e0d86..984f01cd8b 100755 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -254,9 +254,7 @@ function MapAnalysisImpl(props: ImplProps) { setActiveMarkerConfigurationType, setMarkerConfigurations, geoConfigs, - setTimeSliderVariable, - setTimeSliderSelectedRange, - setTimeSliderActive, + setTimeSliderConfig, } = props; const { activeMarkerConfigurationType, markerConfigurations } = appState; const filters = analysisState.analysis?.descriptor.subset.descriptor; @@ -281,9 +279,6 @@ function MapAnalysisImpl(props: ImplProps) { const { variable: overlayVariable, entity: overlayEntity } = findEntityAndVariable(activeMarkerConfiguration?.selectedVariable) ?? {}; - const { variable: timeVariableMetadata } = - findEntityAndVariable(appState.timeSliderVariable) ?? {}; - const outputEntity = useMemo(() => { if (geoConfig == null || geoConfig.entity.id == null) return; @@ -307,35 +302,33 @@ function MapAnalysisImpl(props: ImplProps) { [markerConfigurations, setMarkerConfigurations] ); - const timeFilter: NumberRangeFilter | DateRangeFilter | undefined = useMemo( - () => - timeVariableMetadata && - appState.timeSliderActive && - appState.timeSliderVariable && - appState.timeSliderSelectedRange + const timeFilter: NumberRangeFilter | DateRangeFilter | undefined = + useMemo(() => { + if (appState.timeSliderConfig == null) return undefined; + + const { active, variable, selectedRange } = appState.timeSliderConfig; + + const { variable: timeVariableMetadata } = + findEntityAndVariable(variable) ?? {}; + + return active && variable && selectedRange ? DateVariable.is(timeVariableMetadata) ? { type: 'dateRange', - ...appState.timeSliderVariable, - min: appState.timeSliderSelectedRange.start + 'T00:00:00Z', - max: appState.timeSliderSelectedRange.end + 'T00:00:00Z', + ...variable, + min: selectedRange.start + 'T00:00:00Z', + max: selectedRange.end + 'T00:00:00Z', } : NumberVariable.is(timeVariableMetadata) ? { type: 'numberRange', // this is temporary - I think we should NOT handle non-date variables when we roll this out - ...appState.timeSliderVariable, // TO DO: remove number variable handling - min: Number(appState.timeSliderSelectedRange.start.split(/-/)[0]), // just take the year number - max: Number(appState.timeSliderSelectedRange.end.split(/-/)[0]), // from the YYYY-MM-DD returned from the widget + ...variable, // TO DO: remove number variable handling + min: Number(selectedRange.start.split(/-/)[0]), // just take the year number + max: Number(selectedRange.end.split(/-/)[0]), // from the YYYY-MM-DD returned from the widget } : undefined - : undefined, - [ - timeVariableMetadata, - appState.timeSliderActive, - appState.timeSliderVariable, - appState.timeSliderSelectedRange, - ] - ); + : undefined; + }, [appState.timeSliderConfig, findEntityAndVariable]); const viewportFilters = useMemo( () => @@ -1251,23 +1244,23 @@ function MapAnalysisImpl(props: ImplProps) { overlayActive={overlayVariable != null} > {/* child elements will be distributed across, 'hanging' below the header */} - {/* Time slider component */} - + {/* Time slider component - only if prerequisite variable is available */} + {appState.timeSliderConfig && + appState.timeSliderConfig.variable && ( + + )}
0 || timeVariableIsMissing) { + if (missingMarkerConfigs.length > 0 || timeSliderConfigIsMissing) { setVariableUISettings((prev) => ({ ...prev, [uiStateKey]: { ...appState, - ...(timeVariableIsMissing - ? { timeSliderVariable: defaultTimeVariable } + ...(timeSliderConfigIsMissing + ? { timeSliderConfig: defaultAppState.timeSliderConfig } : {}), markerConfigurations: [ ...appState.markerConfigurations, @@ -213,14 +221,7 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { } appStateCheckedRef.current = true; } - }, [ - analysis, - appState, - setVariableUISettings, - uiStateKey, - defaultAppState, - defaultTimeVariable, - ]); + }, [analysis, appState, setVariableUISettings, uiStateKey, defaultAppState]); function useSetter(key: T) { return useCallback( @@ -254,8 +255,6 @@ export function useAppState(uiStateKey: string, analysisState: AnalysisState) { setIsSubsetPanelOpen: useSetter('isSubsetPanelOpen'), setSubsetVariableAndEntity: useSetter('subsetVariableAndEntity'), setViewport: useSetter('viewport'), - setTimeSliderVariable: useSetter('timeSliderVariable'), - setTimeSliderSelectedRange: useSetter('timeSliderSelectedRange'), - setTimeSliderActive: useSetter('timeSliderActive'), + setTimeSliderConfig: useSetter('timeSliderConfig'), }; } From d33b1b4f88468844868905fdcd4ed79d22bd486f Mon Sep 17 00:00:00 2001 From: Bob Date: Sat, 9 Sep 2023 15:23:58 +0100 Subject: [PATCH 25/27] make h4 section headings optional --- .../visualizations/InputVariables.tsx | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/libs/eda/src/lib/core/components/visualizations/InputVariables.tsx b/packages/libs/eda/src/lib/core/components/visualizations/InputVariables.tsx index e167b1ff75..a9f8333288 100644 --- a/packages/libs/eda/src/lib/core/components/visualizations/InputVariables.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/InputVariables.tsx @@ -40,8 +40,17 @@ export interface InputSpec { * Can be used to override an input role's default title assigned in sectionInfo * when we want the behavior/logic of an existing role but with a different * title. Example: 2x2 mosaic's 'axis' variables. + * Note that the first input (of potentially many) found with this property sets the title. + * See also `noTitle` - because passing `null` to this doesn't get rid of the element. */ titleOverride?: ReactNode; + /** + * To have no title at all. Default false; Same one-to-many issues as titleOverride + */ + noTitle?: boolean; + /** + * apply custom styling to the input container + */ styleOverride?: React.CSSProperties; /** * If an input is pre-populated and cannot be null, set this as true in order to prevent any @@ -222,13 +231,18 @@ export function InputVariables(props: Props) { className={classes.inputGroup} style={{ order: sectionInfo[inputRole ?? 'default'].order }} > -
-

- {inputs.find( - (input) => input.role === inputRole && input.titleOverride - )?.titleOverride ?? sectionInfo[inputRole ?? 'default'].title} -

-
+ {!inputs.find( + (input) => input.role === inputRole && input.noTitle + ) && ( +
+

+ {inputs.find( + (input) => input.role === inputRole && input.titleOverride + )?.titleOverride ?? + sectionInfo[inputRole ?? 'default'].title} +

+
+ )} {inputs .filter((input) => input.role === inputRole) .map((input) => ( From 28a1e001d2ee3fdf0147475359573dc4bfde0854 Mon Sep 17 00:00:00 2001 From: Bob Date: Sat, 9 Sep 2023 16:36:03 +0100 Subject: [PATCH 26/27] final-ish minimize/maximize behaviour --- .../components/plotControls/EzTimeFilter.tsx | 28 ++-- .../plotControls/EzTimeFilter.stories.tsx | 4 +- .../eda/src/lib/map/analysis/EZTimeFilter.tsx | 122 +++++++++--------- 3 files changed, 75 insertions(+), 79 deletions(-) diff --git a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx index 4d49344a49..777f9496d5 100755 --- a/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx +++ b/packages/libs/components/src/components/plotControls/EzTimeFilter.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ -import React, { useRef, useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { scaleTime, scaleLinear } from '@visx/scale'; import { Brush } from '@visx/brush'; // add ResizeTriggerAreas type @@ -36,12 +36,10 @@ export type EzTimeFilterProps = { axisColor?: string; /** opacity of selected brush */ brushOpacity?: number; - /** whether movement of Brush should be disabled */ - disableDraggingSelection?: boolean; - /** disable brush selection */ - resizeTriggerAreas?: ResizeTriggerAreas[]; /** debounce rate in millisecond */ debounceRateMs?: number; + /** all user-interaction disabled */ + disabled?: boolean; }; // using forwardRef @@ -50,23 +48,26 @@ function EzTimeFilter(props: EzTimeFilterProps) { data, // set default width and height width = 720, - height = 125, + height = 100, brushColor = 'lightblue', axisColor = '#000', brushOpacity = 0.4, selectedRange, setSelectedRange, - disableDraggingSelection = false, - resizeTriggerAreas = ['left', 'right'], // set a default debounce time in milliseconds debounceRateMs = 500, + disabled = false, } = props; + const resizeTriggerAreas: ResizeTriggerAreas[] = disabled + ? [] + : ['left', 'right']; + // define default values - const margin = { top: 10, bottom: 10, left: 10, right: 10 }; + const margin = { top: 0, bottom: 10, left: 10, right: 10 }; const selectedBrushStyle = { - fill: brushColor, - stroke: brushColor, + fill: disabled ? 'lightgray' : brushColor, + stroke: disabled ? 'lightgray' : brushColor, fillOpacity: brushOpacity, // need to set this to be 1? strokeOpacity: 1, @@ -166,6 +167,7 @@ function EzTimeFilter(props: EzTimeFilterProps) { style={{ // centering time filter textAlign: 'center', + pointerEvents: disabled ? 'none' : 'all', }} > @@ -215,10 +217,10 @@ function EzTimeFilter(props: EzTimeFilterProps) { brushDirection="horizontal" initialBrushPosition={initialBrushPosition} onChange={onBrushChange} - onClick={() => setSelectedRange(undefined)} + onClick={disabled ? () => {} : () => setSelectedRange(undefined)} selectedBoxStyle={selectedBrushStyle} useWindowMoveEvents - disableDraggingSelection={disableDraggingSelection} + disableDraggingSelection={disabled} renderBrushHandle={(props) => } /> diff --git a/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx b/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx index 4e89271672..2d50ee2eb1 100755 --- a/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx +++ b/packages/libs/components/src/stories/plotControls/EzTimeFilter.stories.tsx @@ -347,9 +347,7 @@ export const TimeFilter: Story = (args: any) => { // axis tick and tick label color axisColor={'#000'} // whether movement of Brush should be disabled - disableDraggingSelection={buttonText === 'Expand'} - // disable brush selection: pass [] - resizeTriggerAreas={buttonText === 'Expand' ? [] : ['left', 'right']} + disabled={buttonText === 'Expand'} /> {/* add a Expand or something like that to change position */}
setMinimized(false)} + onMouseLeave={() => setMinimized(true)} >
-
- +
+
+ {variableMetadata.variable.displayName + + (active && selectedRange + ? ` [${selectedRange?.start} to ${selectedRange?.end}]` + : ' (all dates)')} +
{/* display start to end value - TO DO: make these date inputs - */} + TO DO: make these date inputs? {selectedRange && (
{selectedRange?.start} ~ {selectedRange?.end}
)} - -
+ */} +
0 && ( - <> - - updateConfig({ ...config, selectedRange }) - } - width={timeFilterWidth - 30} - height={100} - // line color of the selectedRange - brushColor={'lightblue'} - // add opacity - brushOpacity={0.4} - // axis tick and tick label color - axisColor={'#000'} - // whether movement of Brush should be disabled - false for now - disableDraggingSelection={false} - // if needing to disable brush selection: use [] - resizeTriggerAreas={['left', 'right']} - /> - + + updateConfig({ ...config, selectedRange }) + } + width={timeFilterWidth - 30} + height={75} + // fill color of the selectedRange + brushColor={'lightpink'} + brushOpacity={0.4} + // axis tick and tick label color + axisColor={'#000'} + // disable user-interaction + disabled={!active} + /> )} + {!minimized && ( +
+ +
+ )}
) : null; } From 589fe1b40c414713a1fe9255596a8825b03c8a96 Mon Sep 17 00:00:00 2001 From: Bob Date: Sat, 9 Sep 2023 16:49:27 +0100 Subject: [PATCH 27/27] moved variable picker to right --- packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx b/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx index e73de52f27..4cc4aaa36a 100755 --- a/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx +++ b/packages/libs/eda/src/lib/map/analysis/EZTimeFilter.tsx @@ -200,7 +200,7 @@ export default function EZTimeFilter({ /> )} {!minimized && ( -
+
)}