Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guided tour of exploration page #737

Merged
merged 4 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading