From 895aafc256a31d6b3a0cd4643a78245ae6f35a70 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Thu, 9 Nov 2023 16:51:09 +0000 Subject: [PATCH 1/4] Add draft implementation of react tour to exploration --- .../components/datasets/dataset-list-item.tsx | 1 + .../components/timeline/timeline-controls.tsx | 2 +- .../components/timeline/timeline-head.tsx | 6 +- .../components/timeline/timeline.tsx | 2 + app/scripts/components/exploration/index.tsx | 19 +- .../components/exploration/tour-manager.tsx | 176 ++++++++++++++++++ package.json | 1 + yarn.lock | 41 ++++ 8 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 app/scripts/components/exploration/tour-manager.tsx diff --git a/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx b/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx index 2332400a7..c2abe2a19 100644 --- a/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx +++ b/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx @@ -211,6 +211,7 @@ export function DatasetListItem(props: DatasetListItemProps) { onDragEnd={() => { onDragEnd?.(); }} + data-tour='dataset-list-item' > diff --git a/app/scripts/components/exploration/components/timeline/timeline-controls.tsx b/app/scripts/components/exploration/components/timeline/timeline-controls.tsx index c84219a34..77603fc02 100644 --- a/app/scripts/components/exploration/components/timeline/timeline-controls.tsx +++ b/app/scripts/components/exploration/components/timeline/timeline-controls.tsx @@ -96,7 +96,7 @@ export function TimelineControls(props: TimelineControlsProps) { setSelectedDay(d.start!); }} renderTriggerElement={(props, label) => ( - + A {label} diff --git a/app/scripts/components/exploration/components/timeline/timeline-head.tsx b/app/scripts/components/exploration/components/timeline/timeline-head.tsx index cf985e901..4a3a94111 100644 --- a/app/scripts/components/exploration/components/timeline/timeline-head.tsx +++ b/app/scripts/components/exploration/components/timeline/timeline-head.tsx @@ -30,6 +30,7 @@ interface TimelineHeadBaseProps { width: number; onDayChange: (date: Date) => void; children: React.ReactNode; + 'data-tour'?: string; } type TimelineHeadProps = Omit & { @@ -84,7 +85,10 @@ export function TimelineHeadBase(props: TimelineHeadBaseProps) { return ( - + {children} diff --git a/app/scripts/components/exploration/components/timeline/timeline.tsx b/app/scripts/components/exploration/components/timeline/timeline.tsx index a250d8e32..383fd24a2 100644 --- a/app/scripts/components/exploration/components/timeline/timeline.tsx +++ b/app/scripts/components/exploration/components/timeline/timeline.tsx @@ -422,6 +422,7 @@ export default function Timeline(props: TimelineProps) { @@ -448,6 +449,7 @@ export default function Timeline(props: TimelineProps) { <> {selectedDay && ( ({ + ...base, + padding: '0', + background: 'none', + }) +}; + function Exploration() { const datasets = useAtomValue(timelineDatasetsAtom); const [datasetModalRevealed, setDatasetModalRevealed] = useState( @@ -66,12 +76,17 @@ function Exploration() { const closeModal = useCallback(() => setDatasetModalRevealed(false), []); return ( - <> + + @@ -97,7 +112,7 @@ function Exploration() { close={closeModal} /> - + ); } export default Exploration; diff --git a/app/scripts/components/exploration/tour-manager.tsx b/app/scripts/components/exploration/tour-manager.tsx new file mode 100644 index 000000000..08da40b48 --- /dev/null +++ b/app/scripts/components/exploration/tour-manager.tsx @@ -0,0 +1,176 @@ +import React, { useEffect } from 'react'; +import { useTour, PopoverContentProps, StepType } from '@reactour/tour'; +import { useAtomValue } from 'jotai'; +import styled from 'styled-components'; +import { glsp, themeVal } from '@devseed-ui/theme-provider'; +import { Button } from '@devseed-ui/button'; +import { Heading } from '@devseed-ui/typography'; +import { + CollecticonChevronLeftSmall, + CollecticonChevronRightSmall, + CollecticonXmark +} from '@devseed-ui/collecticons'; + +import { timelineDatasetsAtom } from './atoms/datasets'; + +import { usePreviousValue } from '$utils/use-effect-previous'; + +const Popover = styled.div` + position: relative; + background: ${themeVal('color.surface')}; + padding: ${glsp(1, 2, 1, 2)}; + border-radius: ${themeVal('shape.rounded')}; + display: flex; + flex-direction: column; + gap: ${glsp()}; +`; + +const CloseButton = styled(Button)` + position: absolute; + right: ${glsp(0.5)}; + top: ${glsp(0.5)}; +`; + +const PopoverBody = styled.div``; + +const PopoverFooter = styled.div` + display: flex; + justify-content: center; + align-items: center; + gap: ${glsp()}; + font-weight: ${themeVal('type.base.bold')}; +`; + +const tourSteps = [ + { + title: 'Welcome to Exploration', + selector: "[data-tour='dataset-list-item']", + content: + "Each timeline entry represents a dataset, and each of the gray segments represents a data unit in the dataset. This data unit can be a day, month or year, depending on the dataset's time density." + }, + { + title: 'Playhead', + selector: "[data-tour='timeline-head-a']", + content: + 'This is the main timeline playhead which can be used to select the date to view on the map. You can drag it around to change the date.' + }, + { + title: 'Date picker', + selector: "[data-tour='date-picker-a']", + content: 'Alternatively you can also select a date through the date picker.' + }, + { + title: 'Timeline', + selector: "[data-tour='timeline-interaction-rect']", + content: () => ( + <> + To navigate the timeline you can zoom in by pressing the alt key (or + option) + the scroll wheel, and click and drag to pan the timeline. +
+ Clicking on the timeline will also move the playhead to that date. +
+ Go ahead and try it out! + + ) + }, + { + title: 'AOI tools', + selector: '.mapboxgl-ctrl-top-left', + content: () => ( + <> + These tools allow you to draw or upload an AOI to run an analysis on the + selected datasets. + + ), + stepInteraction: false + } +]; + +export function TourManager() { + const { setIsOpen, setSteps } = useTour(); + const datasets = useAtomValue(timelineDatasetsAtom); + + const datasetCount = datasets.length; + const prevDatasetCount = usePreviousValue(datasetCount); + + useEffect(() => { + if (!prevDatasetCount && datasetCount > 0) { + setSteps?.(tourSteps); + setTimeout(() => { + setIsOpen(true); + }, 1000); + } + }, [prevDatasetCount, datasetCount, setIsOpen]); + + return null; +} + +interface ExtendedPopoverContentProps extends PopoverContentProps { + steps: (StepType & { title: string })[]; +} + +export function PopoverTourComponent(props: ExtendedPopoverContentProps) { + const { currentStep, steps, setIsOpen, setCurrentStep } = props; + + const isLastStep = currentStep === steps.length - 1; + const { content, title } = steps[currentStep]; + return ( + + setIsOpen(false)} + > + + + + {title} + + + <> + {/* Check if the step.content is a function or a string */} + {typeof content === 'function' ? content({ ...props }) : content} + + + + + + {currentStep + 1} / {steps.length} + + + + {(currentStep === 0 || isLastStep) && ( + { + e.preventDefault(); + setIsOpen(false); + }} + > + Dismiss and do not show again + + )} + + ); +} diff --git a/package.json b/package.json index 5142fa132..e924b694e 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "@mapbox/mapbox-gl-draw": "^1.3.0", "@mapbox/mapbox-gl-geocoder": "^5.0.1", "@parcel/transformer-raw": "~2.7.0", + "@reactour/tour": "^3.6.1", "@tanstack/react-query": "^4.3.9", "@tanstack/react-query-devtools": "^4.3.9", "@tanstack/react-table": "^8.9.3", diff --git a/yarn.lock b/yarn.lock index 25e0c8902..eb0f5ee5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2962,6 +2962,42 @@ resolved "http://verdaccio.ds.io:4873/@reach%2fobserve-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ== +"@reactour/mask@*": + version "1.1.0" + resolved "http://verdaccio.ds.io:4873/@reactour%2fmask/-/mask-1.1.0.tgz#e327306585ee3510e80169a7fa811e9d0b9448bb" + integrity sha512-GkJMLuTs3vTsm4Ryq2uXcE4sMzRP1p4xSd6juSOMqbHa7IVD/UiLCLqJWHR9xGSQPbYhpZAZAORUG5cS0U5tBA== + dependencies: + "@reactour/utils" "*" + +"@reactour/popover@*": + version "1.1.1" + resolved "http://verdaccio.ds.io:4873/@reactour%2fpopover/-/popover-1.1.1.tgz#c9b05ee31b8677d874f1479d724204b936e1128f" + integrity sha512-BouulO0sXfmuHSPX8FwCYI0fMeT+VsWqk7UTao3NQcUC5H903ZeKOV2GYpwSJtRUQhsyNEu1Q8cEruGRf1SOXQ== + dependencies: + "@reactour/utils" "*" + +"@reactour/tour@^3.6.1": + version "3.6.1" + resolved "http://verdaccio.ds.io:4873/@reactour%2ftour/-/tour-3.6.1.tgz#7537c8faa48546fe4e312f125113459b7a19fa13" + integrity sha512-vzmgbm4T7n5gh0cjc4Zi4G3K29dXQyEdi/o7ZYLpNcisJ0hwP5jNKH7BgckrHWEGldBxYSWl34tsRmHcyxporQ== + dependencies: + "@reactour/mask" "*" + "@reactour/popover" "*" + "@reactour/utils" "*" + +"@reactour/utils@*": + version "0.5.0" + resolved "http://verdaccio.ds.io:4873/@reactour%2futils/-/utils-0.5.0.tgz#8886872d78839187fd53399834a1b9688e98d754" + integrity sha512-yQs5Nm/Dg1xRM7d/S/UILBV5OInrTgrjGzgc81/RP5khqdO5KnpOaC46yF83kDtCalte8X3RCwp+F2YA509k1w== + dependencies: + "@rooks/use-mutation-observer" "^4.11.2" + resize-observer-polyfill "^1.5.1" + +"@rooks/use-mutation-observer@^4.11.2": + version "4.11.2" + resolved "http://verdaccio.ds.io:4873/@rooks%2fuse-mutation-observer/-/use-mutation-observer-4.11.2.tgz#a0466c4338e0a4487ea19253c86bcd427c29f4af" + integrity sha512-vpsdrZdr6TkB1zZJcHx+fR1YC/pHs2BaqcuYiEGjBVbwY5xcC49+h0hAUtQKHth3oJqXfIX/Ng8S7s5HFHdM/A== + "@sinclair/typebox@^0.24.1": version "0.24.20" resolved "http://verdaccio.ds.io:4873/@sinclair%2ftypebox/-/typebox-0.24.20.tgz#11a657875de6008622d53f56e063a6347c51a6dd" @@ -11321,6 +11357,11 @@ require-main-filename@^1.0.1: resolved "http://verdaccio.ds.io:4873/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "http://verdaccio.ds.io:4873/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^3.0.0: version "3.0.0" resolved "http://verdaccio.ds.io:4873/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" From e31d2054421574f103c46b96fbc6dc5ff9beabcf Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Wed, 8 Nov 2023 17:03:31 +0000 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Jonas --- .../components/exploration/tour-manager.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/scripts/components/exploration/tour-manager.tsx b/app/scripts/components/exploration/tour-manager.tsx index 08da40b48..003b020d5 100644 --- a/app/scripts/components/exploration/tour-manager.tsx +++ b/app/scripts/components/exploration/tour-manager.tsx @@ -46,13 +46,13 @@ const tourSteps = [ title: 'Welcome to Exploration', selector: "[data-tour='dataset-list-item']", content: - "Each timeline entry represents a dataset, and each of the gray segments represents a data unit in the dataset. This data unit can be a day, month or year, depending on the dataset's time density." + "Each row represents a dataset, and each of the boxes on the timeline represents a data unit in the dataset. This data unit can be a day, month or year, depending on the dataset's time density." }, { title: 'Playhead', selector: "[data-tour='timeline-head-a']", content: - 'This is the main timeline playhead which can be used to select the date to view on the map. You can drag it around to change the date.' + 'Move this playhead to select a date to view on the map. You can drag it around or click on the timeline to place it.' }, { title: 'Date picker', @@ -64,10 +64,8 @@ const tourSteps = [ selector: "[data-tour='timeline-interaction-rect']", content: () => ( <> - To navigate the timeline you can zoom in by pressing the alt key (or - option) + the scroll wheel, and click and drag to pan the timeline. -
- Clicking on the timeline will also move the playhead to that date. + You can zoom in on the timeline by scrolling while pressing the alt key (or + option) and click and drag to pan.
Go ahead and try it out! @@ -78,8 +76,8 @@ const tourSteps = [ selector: '.mapboxgl-ctrl-top-left', content: () => ( <> - These tools allow you to draw or upload an AOI to run an analysis on the - selected datasets. + You can calculate a time series (zonal statistics) for your area of interest (AOI). + Start that process here by drawing or uploading an AOI. ), stepInteraction: false From 49081e523d68cd7d9a4e0db675abe9c462d0cb53 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Thu, 9 Nov 2023 16:46:55 +0000 Subject: [PATCH 3/4] Add tour for analysis step --- .../map/analysis-message-control.tsx | 4 +- .../components/timeline/timeline-controls.tsx | 2 +- .../components/exploration/tour-manager.tsx | 105 +++++++++++++++--- 3 files changed, 93 insertions(+), 18 deletions(-) diff --git a/app/scripts/components/exploration/components/map/analysis-message-control.tsx b/app/scripts/components/exploration/components/map/analysis-message-control.tsx index 119d48427..53c144938 100644 --- a/app/scripts/components/exploration/components/map/analysis-message-control.tsx +++ b/app/scripts/components/exploration/components/map/analysis-message-control.tsx @@ -22,7 +22,9 @@ import { useAnalysisController } from '$components/exploration/hooks/use-analysi import useThemedControl from '$components/common/map/controls/hooks/use-themed-control'; import { AoIFeature } from '$components/common/map/types'; -const AnalysisMessageWrapper = styled.div` +const AnalysisMessageWrapper = styled.div.attrs({ + 'data-tour': 'analysis-message' +})` background-color: ${themeVal('color.base-400a')}; color: ${themeVal('color.surface')}; border-radius: ${themeVal('shape.rounded')}; diff --git a/app/scripts/components/exploration/components/timeline/timeline-controls.tsx b/app/scripts/components/exploration/components/timeline/timeline-controls.tsx index 77603fc02..db3130158 100644 --- a/app/scripts/components/exploration/components/timeline/timeline-controls.tsx +++ b/app/scripts/components/exploration/components/timeline/timeline-controls.tsx @@ -164,7 +164,7 @@ export function TimelineControls(props: TimelineControlsProps) { )} {selectedInterval && ( - + ( <> - You can zoom in on the timeline by scrolling while pressing the alt key (or - option) and click and drag to pan. + You can zoom in on the timeline by scrolling while pressing the alt key + (or option) and click and drag to pan.
Go ahead and try it out! @@ -76,29 +78,100 @@ const tourSteps = [ selector: '.mapboxgl-ctrl-top-left', content: () => ( <> - You can calculate a time series (zonal statistics) for your area of interest (AOI). - Start that process here by drawing or uploading an AOI. + You can calculate a time series (zonal statistics) for your area of + interest (AOI). Start that process here by drawing or uploading an AOI. ), stepInteraction: false } ]; +const analysisTourSteps = [ + { + title: 'Analysis', + selector: "[data-tour='analysis-message']", + content: () => ( + <> + Now that you have an AOI, you can calculate a time series (zonal + statistics) for it. + + ), + stepInteraction: false + }, + { + title: 'Date Range', + selector: "[data-tour='analysis-toolbar']", + content: () => ( + <> + Through the date picker (or the new handles in the timeline) you can + select a date range to analyze. +
+ A date range is preselected for you, but you can change it if you want. +
+ Once you're happy press the analyze button. + + ), + stepInteraction: false + } +]; + +/** + * Helper function to add an action after the last step of a tour. + * @param steps The steps to add the action to + * @param action The action to add to the last step + * @returns steps with the action added to the last step + */ +function addActionAfterLastStep(steps: StepType[], action: () => void) { + const lastStep = steps[steps.length - 1]; + const lastStepWithAction = { + ...lastStep, + actionAfter: action + }; + return [...steps.slice(0, -1), lastStepWithAction]; +} + export function TourManager() { - const { setIsOpen, setSteps } = useTour(); - const datasets = useAtomValue(timelineDatasetsAtom); + const { setIsOpen, setSteps, setCurrentStep } = useTour(); + const startTour = useCallback( + (steps) => { + setCurrentStep(0); + setSteps?.(steps); + setIsOpen(true); + }, + [setIsOpen, setSteps, setCurrentStep] + ); + + // Control states for the different tours. + const [introTourShown, setIntroTourShown] = useState(false); + const [analysisTourShown, setAnalysisTourShown] = useState(false); + + // Variables that cause tour 1 to start. + const datasets = useAtomValue(timelineDatasetsAtom); const datasetCount = datasets.length; const prevDatasetCount = usePreviousValue(datasetCount); + useEffect(() => { + if (!introTourShown && !prevDatasetCount && datasetCount > 0) { + // Make the last step of the intro tour mark it as shown. + const steps = addActionAfterLastStep(introTourSteps, () => { + setIntroTourShown(true); + }); + startTour(steps); + } + }, [introTourShown, prevDatasetCount, datasetCount, startTour]); + // Variables that cause tour 2 to start. + const { features } = useAois(); + const featuresCount = features.length; useEffect(() => { - if (!prevDatasetCount && datasetCount > 0) { - setSteps?.(tourSteps); - setTimeout(() => { - setIsOpen(true); - }, 1000); + if (introTourShown && !analysisTourShown && featuresCount > 0) { + // Make the last step of the intro tour mark it as shown. + const steps = addActionAfterLastStep(analysisTourSteps, () => { + setAnalysisTourShown(true); + }); + startTour(steps); } - }, [prevDatasetCount, datasetCount, setIsOpen]); + }, [introTourShown, analysisTourShown, featuresCount, startTour]); return null; } @@ -158,7 +231,7 @@ export function PopoverTourComponent(props: ExtendedPopoverContentProps) { - {(currentStep === 0 || isLastStep) && ( + {/* {(currentStep === 0 || isLastStep) && ( { @@ -168,7 +241,7 @@ export function PopoverTourComponent(props: ExtendedPopoverContentProps) { > Dismiss and do not show again - )} + )} */} ); } From cd20ed20421300e699db8bb882b3c2f8c9ec5e5f Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 10 Nov 2023 14:34:08 +0000 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Jonas --- .../components/exploration/tour-manager.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/app/scripts/components/exploration/tour-manager.tsx b/app/scripts/components/exploration/tour-manager.tsx index 5a15cde5c..a90858357 100644 --- a/app/scripts/components/exploration/tour-manager.tsx +++ b/app/scripts/components/exploration/tour-manager.tsx @@ -44,11 +44,11 @@ const PopoverFooter = styled.div` const introTourSteps = [ { - title: 'Welcome to Exploration', + title: 'Map layer selection', selector: "[data-tour='dataset-list-item']", mutationObservables: ["[data-tour='dataset-list-item']"], content: - "Each row represents a dataset, and each of the boxes on the timeline represents a data unit in the dataset. This data unit can be a day, month or year, depending on the dataset's time density." + "Each row represents a dataset, and each of the boxes on the timeline represents a data unit: day, month or year, depending on the dataset's time density." }, { title: 'Playhead', @@ -78,7 +78,7 @@ const introTourSteps = [ selector: '.mapboxgl-ctrl-top-left', content: () => ( <> - You can calculate a time series (zonal statistics) for your area of + You can calculate a time series of zonal statistics for your area of interest (AOI). Start that process here by drawing or uploading an AOI. ), @@ -92,8 +92,8 @@ const analysisTourSteps = [ selector: "[data-tour='analysis-message']", content: () => ( <> - Now that you have an AOI, you can calculate a time series (zonal - statistics) for it. + You can now calculate a time series of zonal + statistics for your area of interest. ), stepInteraction: false @@ -103,12 +103,10 @@ const analysisTourSteps = [ selector: "[data-tour='analysis-toolbar']", content: () => ( <> - Through the date picker (or the new handles in the timeline) you can - select a date range to analyze. + Refine the date range to analyze with the data pickers + or handles on the timeline.
- A date range is preselected for you, but you can change it if you want. -
- Once you're happy press the analyze button. + Once you're happy, press the analyze button to start the calculation. ), stepInteraction: false