Skip to content

Commit

Permalink
Add dataset options menu
Browse files Browse the repository at this point in the history
Includes opacity and dataset remove button
  • Loading branch information
danielfdsilva committed Aug 18, 2023
1 parent 59b7dd1 commit e1eef89
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 13 deletions.
1 change: 1 addition & 0 deletions app/scripts/components/exploration/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface TimelineDataset {
settings: {
// user defined settings like visibility, opacity
isVisible?: boolean;
opacity?: number;
};
}

Expand Down
2 changes: 2 additions & 0 deletions app/scripts/components/exploration/dataset-list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
} from './dataset-list-item-status';
import { DatasetChart } from './dataset-chart';
import { activeAnalysisMetricsAtom, isAnalysisAtom } from './atoms';
import DatasetOptions from './dataset-options';

import { LayerGradientGraphic } from '$components/common/mapbox/layer-legend';

Expand Down Expand Up @@ -170,6 +171,7 @@ export function DatasetListItem(props: DatasetListItemProps) {
{dataset.data.title}
</Heading>
<Toolbar size='small'>
<DatasetOptions datasetAtom={datasetAtom} />
{!isError ? (
<ToolbarIconButton onClick={() => setVisible((v) => !v)}>
{isVisible ? (
Expand Down
100 changes: 100 additions & 0 deletions app/scripts/components/exploration/dataset-options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';
import { PrimitiveAtom, useAtomValue, useSetAtom } from 'jotai';
import 'react-range-slider-input/dist/style.css';
import styled from 'styled-components';
import { glsp, themeVal } from '@devseed-ui/theme-provider';
import { Dropdown, DropMenu, DropTitle } from '@devseed-ui/dropdown';
import { Button } from '@devseed-ui/button';
import { CollecticonCog, CollecticonTrashBin } from '@devseed-ui/collecticons';
import { Overline } from '@devseed-ui/typography';

import { useTimelineDatasetSettings } from './hooks';
import { TimelineDataset } from './constants';
import { timelineDatasetsAtom } from './atoms';

import DropMenuItemButton from '$styles/drop-menu-item-button';
import { SliderInput, SliderInputProps } from '$styles/range-slider';

interface DatasetOptionsProps {
datasetAtom: PrimitiveAtom<TimelineDataset>;
}

export default function DatasetOptions(props: DatasetOptionsProps) {
const { datasetAtom } = props;

const setDatasets = useSetAtom(timelineDatasetsAtom);
const dataset = useAtomValue(datasetAtom);
const [getSettings, setSetting] = useTimelineDatasetSettings(datasetAtom);

const opacity = (getSettings('opacity') ?? 100) as number;

return (
<Dropdown
alignment='right'
triggerElement={(props) => (
<Button variation='base-text' size='small' fitting='skinny' {...props}>
<CollecticonCog meaningful title='View dataset options' />
</Button>
)}
>
<DropTitle>View options</DropTitle>
<DropMenu>
<li>
<OpacityControl
value={opacity}
onInput={(v) => setSetting('opacity', v)}
/>
</li>
</DropMenu>
<DropMenu>
<li>
<DropMenuItemButton
variation='danger'
onClick={() => {
setDatasets((datasets) =>
datasets.filter((d) => d.data.id !== dataset.data.id)
);
}}
>
<CollecticonTrashBin /> Remove dataset
</DropMenuItemButton>
</li>
</DropMenu>
</Dropdown>
);
}

const OpacityControlWrapper = styled.div`
padding: ${glsp(0.5, 1)};
display: flex;
flex-flow: column;
gap: ${glsp(0.25)};
`;

const OpacityControlElements = styled.div`
display: flex;
gap: ${glsp(0.5)};
align-items: center;
`;

const OpacityValue = styled.span`
font-size: 0.75rem;
font-weight: ${themeVal('type.base.regular')};
color: ${themeVal('color.base-400')};
width: 2rem;
text-align: right;
`;

function OpacityControl(props: SliderInputProps) {
const { value, onInput } = props;

return (
<OpacityControlWrapper>
<Overline>Opacity</Overline>
<OpacityControlElements>
<SliderInput value={value} onInput={onInput} />
<OpacityValue>{value}</OpacityValue>
</OpacityControlElements>
</OpacityControlWrapper>
);
}
54 changes: 53 additions & 1 deletion app/scripts/components/exploration/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { extent, scaleTime } from 'd3';
import { PrimitiveAtom, useAtom, useAtomValue } from 'jotai';
import { focusAtom } from 'jotai-optics';
Expand Down Expand Up @@ -109,6 +109,58 @@ export function useTimelineDatasetAtom(id: string) {
return datasetAtom as PrimitiveAtom<TimelineDataset>;
}

type TimelineDatasetSettingsReturn = [
(
prop: keyof TimelineDataset['settings']
) => TimelineDataset['settings'][keyof TimelineDataset['settings']],
(
prop: keyof TimelineDataset['settings'],
value:
| TimelineDataset['settings'][keyof TimelineDataset['settings']]
| ((
prev: TimelineDataset['settings'][keyof TimelineDataset['settings']]
) => TimelineDataset['settings'][keyof TimelineDataset['settings']])
) => void
];

/**
* Hook to get/set the settings of a dataset.
*
* @param datasetAtom Single dataset atom.
* @returns State getter/setter for the dataset settings.
*
* @example
* const [get, set] = useTimelineDatasetSettings(datasetAtom);
* const isVisible = get('isVisible');
* set('isVisible', !isVisible);
* set('isVisible', (prev) => !prev);
*
*/
export function useTimelineDatasetSettings(
datasetAtom: PrimitiveAtom<TimelineDataset>
): TimelineDatasetSettingsReturn {
const settingsAtom = useMemo(() => {
return focusAtom(datasetAtom, (optic) => optic.prop('settings'));
}, [datasetAtom]);

const [value, set] = useAtom(settingsAtom);

const setter = useCallback(
(prop, value) => {
set((prev) => {
const currValue = prev[prop];
const newValue = typeof value === 'function' ? value(currValue) : value;
return { ...prev, [prop]: newValue };
});
},
[set]
);

const getter = useCallback((prop) => value[prop], [value]);

return [getter, setter];
}

/**
* Hook to get/set the visibility of a dataset.
* @param datasetAtom Single dataset atom.
Expand Down
49 changes: 37 additions & 12 deletions app/scripts/styles/drop-menu-item-button.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,52 @@
import styled, { css } from 'styled-components';
import { DropMenuItem } from '@devseed-ui/dropdown';
import { rgba, themeVal } from '@devseed-ui/theme-provider';
import { DropMenuItem, DropMenuItemProps } from '@devseed-ui/dropdown';
import { themeVal } from '@devseed-ui/theme-provider';

const rgbaFixed = rgba as any;
interface DropMenuItemButtonProps extends DropMenuItemProps {
variation?:
| 'base'
| 'primary'
| 'secondary'
| 'danger'
| 'success'
| 'warning'
| 'info';
}

const DropMenuItemButton = styled(DropMenuItem).attrs({
as: 'button',
'data-dropdown': 'click.close'
})`
})<DropMenuItemButtonProps>`
background: none;
border: none;
width: 100%;
cursor: pointer;
text-align: left;
${({ active }) =>
active &&
css`
&,
&:visited {
background-color: ${rgbaFixed(themeVal('color.link'), 0.08)};
}
`}
${(props) => renderVariation(props)}
`;

export default DropMenuItemButton;

function renderVariation(props: DropMenuItemButtonProps) {
const { variation = 'base', active } = props;

return css`
color: ${themeVal(`color.${variation}`)};
&:hover {
color: ${themeVal(`color.${variation}`)};
background-color: ${themeVal(`color.${variation}-50a`)};
}
/* Print & when prop is passed */
${active && '&,'}
&.active {
background-color: ${themeVal(`color.${variation}-100a`)};
}
&:focus-visible {
outline-color: ${themeVal(`color.${variation}-200a`)};
}
`;
}
73 changes: 73 additions & 0 deletions app/scripts/styles/range-slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import styled from 'styled-components';
import RangeSlider from 'react-range-slider-input';
import 'react-range-slider-input/dist/style.css';

import { themeVal } from '@devseed-ui/theme-provider';

export interface RangeSliderInputProps {
id?: string;
className?: string;
min?: number;
max?: number;
step?: number;
defaultValue?: [number, number];
value: [number, number];
onInput: (v: [number, number]) => void;
// onThumbDragStart;
// onThumbDragEnd;
// onRangeDragStart;
// onRangeDragEnd;
disabled?: boolean;
rangeSlideDisabled?: boolean;
thumbsDisabled?: [boolean, boolean];
orientation?: 'horizontal' | 'vertical';
}

export const RangeSliderInput = styled(RangeSlider)<RangeSliderInputProps>`
&& {
background: ${themeVal('color.base-200')};
border-radius: ${themeVal('shape.rounded')};
.range-slider__range {
border-radius: ${themeVal('shape.rounded')};
background: ${themeVal('color.primary')};
}
.range-slider__thumb {
transition: background 160ms ease-in-out;
background: ${themeVal('color.surface')};
box-shadow: ${themeVal('boxShadow.elevationD')};
width: 1.25rem;
height: 1.25rem;
&:hover {
background: ${themeVal('color.base-50')};
}
}
.range-slider__thumb[data-lower] {
width: 0;
}
}
`;

export interface SliderInputProps
extends Omit<RangeSliderInputProps, 'value' | 'onInput'> {
value: number;
onInput: (v: number) => void;
}

export function SliderInput(props: SliderInputProps) {
const { value, onInput, ...rest } = props;

return (
<RangeSliderInput
{...rest}
value={[0, value]}
onInput={(v) => onInput(v[1])}
thumbsDisabled={[true, false]}
rangeSlideDisabled
/>
);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
"react-indiana-drag-scroll": "^2.2.0",
"react-lazyload": "^3.2.0",
"react-nl2br": "^1.0.2",
"react-range-slider-input": "^3.0.7",
"react-resizable-panels": "^0.0.45",
"react-router": "^6.0.0",
"react-router-dom": "^6.0.0",
Expand Down
13 changes: 13 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4905,6 +4905,11 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1:
browserslist "^4.21.1"
semver "7.0.0"

core-js@^3.22.4:
version "3.32.0"
resolved "http://verdaccio.ds.io:4873/core-js/-/core-js-3.32.0.tgz#7643d353d899747ab1f8b03d2803b0312a0fb3b6"
integrity sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww==

core-util-is@~1.0.0:
version "1.0.3"
resolved "http://verdaccio.ds.io:4873/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
Expand Down Expand Up @@ -10661,6 +10666,14 @@ react-nl2br@^1.0.2:
resolved "http://verdaccio.ds.io:4873/react-nl2br/-/react-nl2br-1.0.4.tgz#20079e2660b9e9a5293b115466e3749abeed6d87"
integrity sha512-KQ+uwmjYk3B04xDl0hus2OeGoqkR8qkoyDBtsIqVacOUMNeaP0W+r/+anded2ehii/FlgFqEuu0T72CJLWFp4A==

react-range-slider-input@^3.0.7:
version "3.0.7"
resolved "http://verdaccio.ds.io:4873/react-range-slider-input/-/react-range-slider-input-3.0.7.tgz#88ceb118b33d7eb0550cec1f77fc3e60e0f880f9"
integrity sha512-yAJDDMUNkILOcZSCLCVbwgnoAM3v0AfdDysTCMXDwY/+ZRNRlv98TyHbVCwPFEd7qiI8Ca/stKb0GAy//NybYw==
dependencies:
clsx "^1.1.1"
core-js "^3.22.4"

react-refresh@^0.9.0:
version "0.9.0"
resolved "http://verdaccio.ds.io:4873/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf"
Expand Down

0 comments on commit e1eef89

Please sign in to comment.