From 0459625df2550da0cc6453d2597f5e13f5a252a6 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Tue, 17 Oct 2023 15:55:54 +0100 Subject: [PATCH 1/2] Implement controls for dataset compare Fix #523 --- .../components/exploration/atoms/atoms.ts | 4 +- .../exploration/components/map/index.tsx | 12 +- .../components/timeline/timeline-controls.tsx | 79 ++++++++++-- .../components/timeline/timeline-head.tsx | 44 ++++--- .../components/timeline/timeline.tsx | 121 ++++++++++-------- app/scripts/components/exploration/index.tsx | 9 +- 6 files changed, 172 insertions(+), 97 deletions(-) diff --git a/app/scripts/components/exploration/atoms/atoms.ts b/app/scripts/components/exploration/atoms/atoms.ts index 3a42d8df0..2f3300213 100644 --- a/app/scripts/components/exploration/atoms/atoms.ts +++ b/app/scripts/components/exploration/atoms/atoms.ts @@ -5,8 +5,10 @@ import { DateRange, TimelineDataset, ZoomTransformPlain } from '../types.d.ts'; // Datasets to show on the timeline and their settings export const timelineDatasetsAtom = atom([]); -// Main timeline date. This date defines the datasets shown on the map. +// Main timeline date. This is the date for the datasets shown on the map. export const selectedDateAtom = atom(null); +// Compare date. This is the compare date for the datasets shown on the map. +export const selectedCompareDateAtom = atom(null); // Date range for L&R playheads. export const selectedIntervalAtom = atom(null); // Zoom transform for the timeline. Values as object instead of d3.ZoomTransform diff --git a/app/scripts/components/exploration/components/map/index.tsx b/app/scripts/components/exploration/components/map/index.tsx index 12a14ee51..307d092af 100644 --- a/app/scripts/components/exploration/components/map/index.tsx +++ b/app/scripts/components/exploration/components/map/index.tsx @@ -1,10 +1,9 @@ import React, { useState } from 'react'; import { useAtomValue } from 'jotai'; -import { addMonths } from 'date-fns'; import { Feature, Polygon } from 'geojson'; import { useStacMetadataOnDatasets } from '../../hooks/use-stac-metadata-datasets'; -import { selectedDateAtom, timelineDatasetsAtom } from '../../atoms/atoms'; +import { selectedCompareDateAtom, selectedDateAtom, timelineDatasetsAtom } from '../../atoms/atoms'; import { TimelineDatasetStatus, TimelineDatasetSuccess @@ -24,7 +23,7 @@ import DrawControl from '$components/common/map/controls/aoi'; import useAois from '$components/common/map/controls/hooks/use-aois'; import CustomAoIControl from '$components/common/map/controls/aoi/custom-aoi-control'; -export function ExplorationMap(props: { comparing: boolean }) { +export function ExplorationMap() { const [projection, setProjection] = useState(projectionDefault); const { @@ -39,6 +38,9 @@ export function ExplorationMap(props: { comparing: boolean }) { const datasets = useAtomValue(timelineDatasetsAtom); const selectedDay = useAtomValue(selectedDateAtom); + const selectedCompareDay = useAtomValue(selectedCompareDateAtom); + + const comparing = !!selectedCompareDay; // Reverse the datasets order to have the "top" layer, list-wise, at the "top" layer, z-order wise // Disabled eslint rule as slice() creates a shallow copy @@ -105,7 +107,7 @@ export function ExplorationMap(props: { comparing: boolean }) { - {props.comparing && ( + {comparing && ( // Compare map layers ))} diff --git a/app/scripts/components/exploration/components/timeline/timeline-controls.tsx b/app/scripts/components/exploration/components/timeline/timeline-controls.tsx index b4cd11c15..0af9b24e1 100644 --- a/app/scripts/components/exploration/components/timeline/timeline-controls.tsx +++ b/app/scripts/components/exploration/components/timeline/timeline-controls.tsx @@ -7,8 +7,10 @@ import { scaleTime, ScaleTime } from 'd3'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; import { CollecticonChevronDownSmall, + CollecticonPlusSmall, CollecticonResizeIn, - CollecticonResizeOut + CollecticonResizeOut, + CollecticonTrashBin } from '@devseed-ui/collecticons'; import { Button } from '@devseed-ui/button'; import { DatePicker } from '@devseed-ui/date-picker'; @@ -24,6 +26,7 @@ import { DateAxis } from './date-axis'; import { analysisControllerAtom, isExpandedAtom, + selectedCompareDateAtom, selectedDateAtom, selectedIntervalAtom } from '$components/exploration/atoms/atoms'; @@ -67,6 +70,9 @@ export function TimelineControls(props: TimelineControlsProps) { const { xScaled, width } = props; const [selectedDay, setSelectedDay] = useAtom(selectedDateAtom); + const [selectedCompareDay, setSelectedCompareDay] = useAtom( + selectedCompareDateAtom + ); const [selectedInterval, setSelectedInterval] = useAtom(selectedIntervalAtom); const { isAnalyzing } = useAtomValue(analysisControllerAtom); const [isExpanded, setExpanded] = useAtom(isExpandedAtom); @@ -83,20 +89,65 @@ export function TimelineControls(props: TimelineControlsProps) { - { - setSelectedDay(d.start!); - }} - renderTriggerElement={(props, label) => ( - - P - {label} - - + + { + setSelectedDay(d.start!); + }} + renderTriggerElement={(props, label) => ( + + A + {label} + + + )} + /> + + {selectedCompareDay ? ( + <> + { + setSelectedCompareDay(d.start!); + }} + renderTriggerElement={(props, label) => ( + + B + {label} + + + )} + /> + { + setSelectedCompareDay(null); + }} + > + + + + ) : ( + { + setSelectedCompareDay(selectedDay); + }} + > + + )} - /> + ; selectedDay: Date; @@ -32,7 +32,11 @@ interface TimelineHeadProps { children: React.ReactNode; } -export function TimelineHead(props: TimelineHeadProps) { +type TimelineHeadProps = Omit & { + label?: string; +}; + +export function TimelineHeadBase(props: TimelineHeadBaseProps) { const { domain, xScaled, selectedDay, width, onDayChange, children } = props; const theme = useTheme(); @@ -81,13 +85,7 @@ export function TimelineHead(props: TimelineHeadProps) { return ( - + {children} @@ -96,11 +94,12 @@ export function TimelineHead(props: TimelineHeadProps) { ); } -export function TimelineHeadP(props: Omit) { +export function TimelineHeadPoint(props: TimelineHeadProps) { const theme = useTheme(); + const { label, ...rest } = props; return ( - + ) { }} /> - P + {label ?? 'P'} - + ); } -export function TimelineHeadL(props: Omit) { +export function TimelineHeadIn(props: TimelineHeadProps) { const theme = useTheme(); + const { label, ...rest } = props; return ( - + ) { }} /> - L + {label ?? 'L'} - + ); } -export function TimelineHeadR(props: Omit) { +export function TimelineHeadOut(props: TimelineHeadProps) { const theme = useTheme(); + const { label, ...rest } = props; + return ( - + ) { dy='1em' textAnchor='end' > - R + {label ?? 'R'} - + ); } diff --git a/app/scripts/components/exploration/components/timeline/timeline.tsx b/app/scripts/components/exploration/components/timeline/timeline.tsx index c45058fe0..c4a46d1e7 100644 --- a/app/scripts/components/exploration/components/timeline/timeline.tsx +++ b/app/scripts/components/exploration/components/timeline/timeline.tsx @@ -21,14 +21,15 @@ import { DatasetList } from '../datasets/dataset-list'; import { applyTransform, isEqualTransform, rescaleX } from './timeline-utils'; import { TimelineControls } from './timeline-controls'; import { - TimelineHeadL, - TimelineHeadP, - TimelineHeadR, + TimelineHeadIn, + TimelineHeadPoint, + TimelineHeadOut, TimelineRangeTrack } from './timeline-head'; import { DateGrid } from './date-axis'; import { + selectedCompareDateAtom, selectedDateAtom, selectedIntervalAtom, timelineDatasetsAtom, @@ -158,6 +159,9 @@ export default function Timeline(props: TimelineProps) { const { contentWidth: width } = useAtomValue(timelineSizesAtom); const [selectedDay, setSelectedDay] = useAtom(selectedDateAtom); + const [selectedCompareDay, setSelectedCompareDay] = useAtom( + selectedCompareDateAtom + ); const [selectedInterval, setSelectedInterval] = useAtom(selectedIntervalAtom); const { setObsolete } = useAnalysisController(); @@ -403,59 +407,72 @@ export default function Timeline(props: TimelineProps) { - {shouldRenderTimeline && selectedDay ? ( - - ) : ( - false - )} - {shouldRenderTimeline && selectedInterval && ( + {shouldRenderTimeline && ( <> - { - setSelectedInterval((interval) => { - const prevDay = sub(interval!.end, { days: 1 }); - return { - end: interval!.end, - start: isAfter(d, prevDay) ? prevDay : d - }; - }); - }} - selectedDay={selectedInterval.start} - width={width} - /> - { - setSelectedInterval((interval) => { - const nextDay = add(interval!.start, { days: 1 }); - return { - start: interval!.start, - end: isBefore(d, nextDay) ? nextDay : d - }; - }); - }} - selectedDay={selectedInterval.end} - width={width} - /> - + {selectedDay && ( + + )} + {selectedCompareDay && ( + + )} + {selectedInterval && ( + <> + { + setSelectedInterval((interval) => { + const prevDay = sub(interval!.end, { days: 1 }); + return { + end: interval!.end, + start: isAfter(d, prevDay) ? prevDay : d + }; + }); + }} + selectedDay={selectedInterval.start} + width={width} + /> + { + setSelectedInterval((interval) => { + const nextDay = add(interval!.start, { days: 1 }); + return { + start: interval!.start, + end: isBefore(d, nextDay) ? nextDay : d + }; + }); + }} + selectedDay={selectedInterval.end} + width={width} + /> + + + )} + + )} - {shouldRenderTimeline && } - diff --git a/app/scripts/components/exploration/index.tsx b/app/scripts/components/exploration/index.tsx index 06b834ca0..80d8f5c07 100644 --- a/app/scripts/components/exploration/index.tsx +++ b/app/scripts/components/exploration/index.tsx @@ -55,7 +55,6 @@ const Container = styled.div` `; function Exploration() { - const [compare, setCompare] = useState(false); const [datasetModalRevealed, setDatasetModalRevealed] = useState(true); const openModal = useCallback(() => setDatasetModalRevealed(true), []); @@ -74,10 +73,12 @@ function Exploration() { - + setCompare((v) => !v)} + comparing={false} + onCompareClick={() => { + /* noop */ + }} /> From 6721546004ff38bbac40eb9e44e3a6c57342ba47 Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Wed, 18 Oct 2023 16:21:30 +0200 Subject: [PATCH 2/2] Fixed an error that caused the compare map to not be displayed properly --- app/scripts/components/common/map/controls/coords.tsx | 2 +- app/scripts/components/common/map/maps.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scripts/components/common/map/controls/coords.tsx b/app/scripts/components/common/map/controls/coords.tsx index c6ed2a4c7..9b0a2d4ad 100644 --- a/app/scripts/components/common/map/controls/coords.tsx +++ b/app/scripts/components/common/map/controls/coords.tsx @@ -34,7 +34,7 @@ const getCoords = (mapInstance?: MapRef) => { }; }; -export default function MapCoords() { +export default function MapCoordsControl() { const { main } = useMaps(); const [position, setPosition] = useState(getCoords(main)); diff --git a/app/scripts/components/common/map/maps.tsx b/app/scripts/components/common/map/maps.tsx index b1a2cfbfe..00f340b9e 100644 --- a/app/scripts/components/common/map/maps.tsx +++ b/app/scripts/components/common/map/maps.tsx @@ -97,10 +97,10 @@ function Maps({ children, projection }: MapsProps) { acc.compareGenerators = Children.toArray( child.props.children ) as ReactElement[]; - } else if (['Basemap', 'RasterTimeseries'].includes(componentName)) { - acc.generators = [...acc.generators, child]; - } else { + } else if (componentName.endsWith('Control')) { acc.controls = [...acc.controls, child]; + } else { + acc.generators = [...acc.generators, child]; } return acc; },