Skip to content

Commit

Permalink
Guided tour of exploration page
Browse files Browse the repository at this point in the history
  • Loading branch information
danielfdsilva authored Nov 10, 2023
2 parents 2c3f46d + cd20ed2 commit a7d15b4
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export function DatasetListItem(props: DatasetListItemProps) {
onDragEnd={() => {
onDragEnd?.();
}}
data-tour='dataset-list-item'
>
<DatasetItem>
<DatasetHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function TimelineControls(props: TimelineControlsProps) {
setSelectedDay(d.start!);
}}
renderTriggerElement={(props, label) => (
<DatePickerButton {...props} size='small' disabled={!xScaled}>
<DatePickerButton {...props} size='small' disabled={!xScaled} data-tour='date-picker-a'>
<span className='head-reference'>A</span>
<span>{label}</span>
<CollecticonChevronDownSmall />
Expand Down Expand Up @@ -164,7 +164,7 @@ export function TimelineControls(props: TimelineControlsProps) {
)}
</ToolbarGroup>
{selectedInterval && (
<ToolbarGroup>
<ToolbarGroup data-tour='analysis-toolbar'>
<DatePicker
id='date-picker-lr'
value={selectedInterval}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface TimelineHeadBaseProps {
width: number;
onDayChange: (date: Date) => void;
children: React.ReactNode;
'data-tour'?: string;
}

type TimelineHeadProps = Omit<TimelineHeadBaseProps, 'children'> & {
Expand Down Expand Up @@ -84,7 +85,10 @@ export function TimelineHeadBase(props: TimelineHeadBaseProps) {

return (
<TimelineHeadSVG width={width + SVG_PADDING * 2}>
<g transform={`translate(${SVG_PADDING}, 0)`}>
<g
transform={`translate(${SVG_PADDING}, 0)`}
data-tour={props['data-tour']}
>
<line x1={xPos} x2={xPos} y1={0} y2='100%' stroke={theme.color?.base} />
<g transform={`translate(${xPos}, 0)`} ref={rectRef}>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ export default function Timeline(props: TimelineProps) {
<InteractionRect
ref={interactionRef}
style={!shouldRenderTimeline ? { pointerEvents: 'none' } : undefined}
data-tour='timeline-interaction-rect'
/>
<TimelineHeader>
<TimelineDetails>
Expand All @@ -448,6 +449,7 @@ export default function Timeline(props: TimelineProps) {
<>
{selectedDay && (
<TimelineHeadPoint
data-tour='timeline-head-a'
label={selectedCompareDay ? 'A' : undefined}
domain={dataDomain}
xScaled={xScaled}
Expand Down
19 changes: 17 additions & 2 deletions app/scripts/components/exploration/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import React, { useCallback, useState } from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import styled from 'styled-components';
import { useAtomValue } from 'jotai';
import { TourProvider } from '@reactour/tour';
import { themeVal } from '@devseed-ui/theme-provider';

import { MockControls } from './datasets-mock';
import Timeline from './components/timeline/timeline';
import { ExplorationMap } from './components/map';
import { DatasetSelectorModal } from './components/dataset-selector-modal';
import { timelineDatasetsAtom } from './atoms/datasets';
import { PopoverTourComponent, TourManager } from './tour-manager';

import { LayoutProps } from '$components/common/layout-root';
import PageHero from '$components/common/page-hero';
Expand Down Expand Up @@ -56,6 +58,14 @@ const Container = styled.div`
}
`;

const tourProviderStyles = {
popover: (base) => ({
...base,
padding: '0',
background: 'none',
})
};

function Exploration() {
const datasets = useAtomValue(timelineDatasetsAtom);
const [datasetModalRevealed, setDatasetModalRevealed] = useState(
Expand All @@ -66,12 +76,17 @@ function Exploration() {
const closeModal = useCallback(() => setDatasetModalRevealed(false), []);

return (
<>
<TourProvider
steps={[]}
styles={tourProviderStyles}
ContentComponent={PopoverTourComponent}
>
<LayoutProps
title='Exploration'
description='Explore and analyze datasets'
hideFooter
/>
<TourManager />
<PageMainContent>
<PageHero title='Exploration' isHidden />

Expand All @@ -97,7 +112,7 @@ function Exploration() {
close={closeModal}
/>
</PageMainContent>
</>
</TourProvider>
);
}
export default Exploration;
245 changes: 245 additions & 0 deletions app/scripts/components/exploration/tour-manager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import React, { useCallback, useEffect, useState } 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';
import useAois from '$components/common/map/controls/hooks/use-aois';

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 introTourSteps = [
{
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: day, month or year, depending on the dataset's time density."
},
{
title: 'Playhead',
selector: "[data-tour='timeline-head-a']",
content:
'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',
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: () => (
<>
You can zoom in on the timeline by scrolling while pressing the alt key
(or option) and click and drag to pan.
<br />
Go ahead and try it out!
</>
)
},
{
title: 'AOI tools',
selector: '.mapboxgl-ctrl-top-left',
content: () => (
<>
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.
</>
),
stepInteraction: false
}
];

const analysisTourSteps = [
{
title: 'Analysis',
selector: "[data-tour='analysis-message']",
content: () => (
<>
You can now calculate a time series of zonal
statistics for your area of interest.
</>
),
stepInteraction: false
},
{
title: 'Date Range',
selector: "[data-tour='analysis-toolbar']",
content: () => (
<>
Refine the date range to analyze with the data pickers
or handles on the timeline.
<br />
Once you&apos;re happy, press the analyze button to start the calculation.
</>
),
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, 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 (introTourShown && !analysisTourShown && featuresCount > 0) {
// Make the last step of the intro tour mark it as shown.
const steps = addActionAfterLastStep(analysisTourSteps, () => {
setAnalysisTourShown(true);
});
startTour(steps);
}
}, [introTourShown, analysisTourShown, featuresCount, startTour]);

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 (
<Popover>
<CloseButton
variation='base-text'
size='small'
fitting='skinny'
onClick={() => setIsOpen(false)}
>
<CollecticonXmark size='small' meaningful title='Close tour' />
</CloseButton>
<Heading as='strong' size='xsmall'>
{title}
</Heading>
<PopoverBody>
<>
{/* Check if the step.content is a function or a string */}
{typeof content === 'function' ? content({ ...props }) : content}
</>
</PopoverBody>
<PopoverFooter>
<Button
variation='base-text'
size='small'
fitting='skinny'
disabled={currentStep === 0}
onClick={() => {
setCurrentStep((s) => s - 1);
}}
>
<CollecticonChevronLeftSmall meaningful title='Previous step' />
</Button>
<small>
{currentStep + 1} / {steps.length}
</small>
<Button
variation='base-text'
size='small'
fitting='skinny'
disabled={isLastStep}
onClick={() => {
setCurrentStep((s) => s + 1);
}}
>
<CollecticonChevronRightSmall meaningful title='Next step' />
</Button>
</PopoverFooter>
{/* {(currentStep === 0 || isLastStep) && (
<a
href='#'
onClick={(e) => {
e.preventDefault();
setIsOpen(false);
}}
>
Dismiss and do not show again
</a>
)} */}
</Popover>
);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit a7d15b4

Please sign in to comment.