diff --git a/app/scripts/components/common/browse-controls/index.tsx b/app/scripts/components/common/browse-controls/index.tsx index f26201e24..4dfccb835 100644 --- a/app/scripts/components/common/browse-controls/index.tsx +++ b/app/scripts/components/common/browse-controls/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Taxonomy } from 'veda'; import { Overline } from '@devseed-ui/typography'; @@ -14,7 +14,6 @@ import { Actions, FilterOption, optionAll, - sortDirOptions, useBrowserControls } from './use-browse-controls'; @@ -34,15 +33,12 @@ const BrowseControlsWrapper = styled.div` const SearchWrapper = styled.div` display: flex; gap: ${variableGlsp(0.5)}; - width: 100%; - max-width: 70rem; + flex-wrap: no-wrap; `; -const TaxonomyWrapper = styled.div` +const FilterOptionsWrapper = styled.div` display: flex; - flex-flow: row wrap; gap: ${variableGlsp(0.5)}; - > * { flex-shrink: 0; } @@ -58,15 +54,11 @@ const DropButton = styled(Button)` flex-shrink: 0; } `; -const MainDropButton = styled(DropButton)` - width: 15rem; - max-width: 15rem; -`; -const ShowMorebutton = styled(Button)` - width: 10rem; - max-width: 10rem; - text-decoration: underline; +const MainDropButton = styled(DropButton)` + > * { + flex-shrink: 0; + } `; const ButtonPrefix = styled(Overline).attrs({ as: 'small' })` @@ -76,28 +68,35 @@ const ButtonPrefix = styled(Overline).attrs({ as: 'small' })` interface BrowseControlsProps extends ReturnType { taxonomiesOptions: Taxonomy[]; - sortOptions: FilterOption[]; - showMoreButtonOpt?: boolean; } function BrowseControls(props: BrowseControlsProps) { const { taxonomiesOptions, taxonomies, - sortOptions, search, - showMoreButtonOpt, - sortField, - sortDir, onAction, ...rest } = props; - const [ showFilters, setShowFilters ] = useState(showMoreButtonOpt ? false : true); - - const currentSortField = sortOptions.find((s) => s.id === sortField)!; - const { isLargeUp } = useMediaQuery(); + const filterWrapConstant = 4; + const wrapTaxonomies = taxonomiesOptions.length > filterWrapConstant; // wrap list of taxonomies when more then 4 filter options + + const createFilterList = (filterList: Taxonomy[]) => ( + filterList.map(({ name, values }) => ( + { + onAction(Actions.TAXONOMY, { key: name, value: v }); + }} + size={isLargeUp ? 'large' : 'medium'} + /> + )) + ); return ( @@ -109,83 +108,17 @@ function BrowseControls(props: BrowseControlsProps) { value={search ?? ''} onChange={(v) => onAction(Actions.SEARCH, v)} /> - ( - - Sort by - {currentSortField.name}{' '} - {active ? ( - - ) : ( - - )} - - )} - > - Options - - {/* { @NOTE: Display the sort option labels only when there is more than one otherwise it already defaults to the button title} */} - {sortOptions.length > 1 && sortOptions.map((t) => ( -
  • - onAction(Actions.SORT_FIELD, t.id)} - > - {t.name} - -
  • - ))} -
    - - {sortDirOptions.map((t) => ( -
  • - onAction(Actions.SORT_DIR, t.id)} - > - {t.name} - -
  • - ))} -
    -
    - { - showMoreButtonOpt && ( - {setShowFilters(value => !value);}} - > - {showFilters ? 'Hide filters' : 'Show filters'} - - ) - } + + {createFilterList(taxonomiesOptions.slice(0, filterWrapConstant))} + - {showFilters && - - {taxonomiesOptions.map(({ name, values }) => ( - { - onAction(Actions.TAXONOMY, { key: name, value: v }); - }} - size={isLargeUp ? 'large' : 'medium'} - /> - ))} - } + { + wrapTaxonomies && ( + + {createFilterList(taxonomiesOptions.slice(filterWrapConstant, taxonomiesOptions.length))} + + ) + }
    ); } @@ -206,13 +139,13 @@ function DropdownOptions(props: DropdownOptionsProps) { const { size, items, currentId, onChange, prefix } = props; const currentItem = items.find((d) => d.id === currentId); - + return ( ( - )} - + )} > Options diff --git a/app/scripts/components/common/browse-controls/use-browse-controls.ts b/app/scripts/components/common/browse-controls/use-browse-controls.ts index 30259e684..702d2639b 100644 --- a/app/scripts/components/common/browse-controls/use-browse-controls.ts +++ b/app/scripts/components/common/browse-controls/use-browse-controls.ts @@ -18,6 +18,12 @@ export interface FilterOption { name: string; } +export interface TaxonomyFilterOption { + taxonomyType: string; + value: string; + exclusion?: string; +} + interface BrowseControlsHookParams { sortOptions: FilterOption[]; } diff --git a/app/scripts/components/data-catalog/index.tsx b/app/scripts/components/data-catalog/index.tsx index 68a60f019..22272dbfc 100644 --- a/app/scripts/components/data-catalog/index.tsx +++ b/app/scripts/components/data-catalog/index.tsx @@ -201,7 +201,6 @@ function DataCatalog() { diff --git a/app/scripts/components/exploration/components/dataset-selector-modal/content.tsx b/app/scripts/components/exploration/components/dataset-selector-modal/content.tsx index e45d3a767..5080384b4 100644 --- a/app/scripts/components/exploration/components/dataset-selector-modal/content.tsx +++ b/app/scripts/components/exploration/components/dataset-selector-modal/content.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import styled, { css } from 'styled-components'; import { glsp, themeVal, media } from '@devseed-ui/theme-provider'; import { @@ -20,7 +21,7 @@ import { import TextHighlight from '$components/common/text-highlight'; import { CardSourcesList } from '$components/common/card-sources'; import { CollecticonDatasetLayers } from '$components/common/icons/dataset-layers'; -import { getDatasetPath } from '$utils/routes'; +import { DATASETS_PATH, getDatasetPath } from '$utils/routes'; import { getTaxonomy, TAXONOMY_SOURCE, @@ -72,6 +73,11 @@ const DatasetIntro = styled.div` padding: ${glsp(1)} 0; `; +const EmptyInfoDiv = styled.div` + width: 70%; + text-align: center; +`; + export const ParentDatasetTitle = styled.h2<{size?: string}>` color: ${themeVal('color.primary')}; text-align: left; @@ -94,20 +100,26 @@ export const ParentDatasetTitle = styled.h2<{size?: string}>` } `; +const WarningPill = styled(Pill)` + margin-left: 8px; +`; + interface ModalContentComponentProps { search: string; selectedIds: string[]; - displayDatasets: (DatasetData & { + displayDatasets?: (DatasetData & { countSelectedLayers: number; })[]; - onCheck: (id: string) => void; + onCheck: (id: string, currentDataset?: DatasetData & {countSelectedLayers: number}) => void; } export default function ModalContentComponent(props:ModalContentComponentProps) { const { search, selectedIds, displayDatasets, onCheck } = props; + const exclusiveSourceWarning = "Can only be analyzed with layers from the same source"; + return( - {displayDatasets.length ? ( + {displayDatasets?.length ? (
    {displayDatasets.map(currentDataset => ( @@ -115,6 +127,13 @@ export default function ModalContentComponent(props:ModalContentComponentProps) {currentDataset.name} + { + currentDataset.sourceExclusive && ( + + {exclusiveSourceWarning} + + ) + } {currentDataset.countSelectedLayers > 0 && {currentDataset.countSelectedLayers} selected } @@ -136,7 +155,7 @@ export default function ModalContentComponent(props:ModalContentComponentProps) layer={datasetLayer} parent={currentDataset} selected={selectedIds.includes(datasetLayer.id)} - onDatasetClick={() => onCheck(datasetLayer.id)} + onDatasetClick={() => onCheck(datasetLayer.id, currentDataset)} /> ); @@ -147,7 +166,10 @@ export default function ModalContentComponent(props:ModalContentComponentProps)
    ) : ( - There are no datasets to show with the selected filters. + +

    There are no datasets to show with the selected filters.


    +

    This tool allows the exploration and analysis of time-series datasets in raster format. For a comprehensive list of available datasets, please visit the Data Catalog.

    +
    )}
    @@ -220,6 +242,7 @@ function DatasetLayerCard(props: DatasetLayerCardProps) { const { parent, layer, onDatasetClick, selected, searchTerm } = props; const topics = getTaxonomy(parent, TAXONOMY_TOPICS)?.values; + const sources = getTaxonomy(parent, TAXONOMY_SOURCE)?.values; return ( } diff --git a/app/scripts/components/exploration/components/dataset-selector-modal/footer.tsx b/app/scripts/components/exploration/components/dataset-selector-modal/footer.tsx index 662d3bc6f..bdf02453d 100644 --- a/app/scripts/components/exploration/components/dataset-selector-modal/footer.tsx +++ b/app/scripts/components/exploration/components/dataset-selector-modal/footer.tsx @@ -30,6 +30,7 @@ const LayerResult = styled.div` export default function ModalFooterRender (props:ModalFooterComponentProps) { const { selectedIds, close, onConfirm } = props; + return ( <> diff --git a/app/scripts/components/exploration/components/dataset-selector-modal/header.tsx b/app/scripts/components/exploration/components/dataset-selector-modal/header.tsx index 4f037f283..79c88cbd6 100644 --- a/app/scripts/components/exploration/components/dataset-selector-modal/header.tsx +++ b/app/scripts/components/exploration/components/dataset-selector-modal/header.tsx @@ -1,7 +1,5 @@ import React from 'react'; import styled from 'styled-components'; -import { Link } from 'react-router-dom'; -import { media } from '@devseed-ui/theme-provider'; import { datasetTaxonomies } from 'veda'; import { ModalHeadline @@ -10,34 +8,33 @@ import { Heading } from '@devseed-ui/typography'; import BrowseControls from '$components/common/browse-controls'; import { + Actions, + TaxonomyFilterOption, useBrowserControls } from '$components/common/browse-controls/use-browse-controls'; import { sortOptions } from '$components/data-catalog'; -import { DATASETS_PATH } from '$utils/routes'; -const StyledModalHeadline = styled(ModalHeadline)``; -const ModalIntro = styled.div` - ${media.largeUp` - width: 66%; - `} +const StyledModalHeadline = styled(ModalHeadline)` + width: 100%; `; -export default function RenderModalHeader () { +export default function RenderModalHeader ({defaultSelect}: {defaultSelect?: TaxonomyFilterOption}) { const controlVars = useBrowserControls({ sortOptions }); + + React.useEffect(() => { + if(defaultSelect) { + controlVars.onAction(Actions.TAXONOMY, { key: defaultSelect.taxonomyType, value: defaultSelect.value }); + } + }, [defaultSelect]); + return( Data layers - -

    This tool allows the exploration and analysis of time-series datasets in raster format. For a comprehensive list of available datasets, please visit the Data Catalog. -

    -
    ); diff --git a/app/scripts/components/exploration/components/dataset-selector-modal/index.tsx b/app/scripts/components/exploration/components/dataset-selector-modal/index.tsx index aff3a9713..d3b5f172a 100644 --- a/app/scripts/components/exploration/components/dataset-selector-modal/index.tsx +++ b/app/scripts/components/exploration/components/dataset-selector-modal/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import { useAtom } from 'jotai'; @@ -16,7 +16,8 @@ import { timelineDatasetsAtom } from '../../atoms/datasets'; import { allDatasetsWithEnhancedLayers as allDatasets, reconcileDatasets, - datasetLayers + datasetLayers, + findParentDataset } from '../../data-utils'; import RenderModalHeader from './header'; import ModalContentRender from './content'; @@ -24,9 +25,12 @@ import ModalFooterRender from './footer'; import { Actions, + TaxonomyFilterOption, useBrowserControls } from '$components/common/browse-controls/use-browse-controls'; import { prepareDatasets, sortOptions } from '$components/data-catalog'; +import { TAXONOMY_SOURCE, getTaxonomy } from '$utils/veda-data'; +import { usePreviousValue } from '$utils/use-effect-previous'; const DatasetModal = styled(Modal)` @@ -90,17 +94,40 @@ export function DatasetSelectorModal(props: DatasetSelectorModalProps) { const { revealed, close } = props; const [timelineDatasets, setTimelineDatasets] = useAtom(timelineDatasetsAtom); + const [datasetsToDisplay, setDatasetsToDisplay] = useState<(DatasetData & { + countSelectedLayers: number; + })[] | undefined>(); + const [defaultSelectFilter, setDefaultSelectFilter] = useState(); // Store a list of selected datasets and only confirm on save. const [selectedIds, setSelectedIds] = useState( timelineDatasets.map((dataset) => dataset.data.id) ); + const prevSelectedIds = usePreviousValue(selectedIds); + + const [exclusionSelected, setExclusionSelected] = useState(false); + useEffect(() => { setSelectedIds(timelineDatasets.map((dataset) => dataset.data.id)); }, [timelineDatasets]); - const onCheck = useCallback((id: string) => { + const onCheck = useCallback((id: string, currentDataset?: DatasetData & {countSelectedLayers: number}) => { + if (currentDataset) { + // This layer is part of a dataset that is exclusive + const exclusiveSource = currentDataset.sourceExclusive?.toLowerCase(); + const sources = getTaxonomy(currentDataset, TAXONOMY_SOURCE)?.values; + const sourceIds = sources?.map(source => source.id); + + if (exclusiveSource && sourceIds?.includes(exclusiveSource)) { + setDefaultSelectFilter({taxonomyType: TAXONOMY_SOURCE, value: exclusiveSource}); + setExclusionSelected(true); + } else { + setDefaultSelectFilter(undefined); + setExclusionSelected(false); + } + } + setSelectedIds((ids) => ids.includes(id) ? ids.filter((i) => i !== id) : [...ids, id] ); @@ -123,6 +150,29 @@ export function DatasetSelectorModal(props: DatasetSelectorModalProps) { // Clear filters when the modal is revealed. const firstRevealRef = React.useRef(true); + useEffect(() => { + if(selectedIds && selectedIds !== prevSelectedIds) { + let relevantIds: string[] | undefined = undefined; + + const selectedIdsWithParentData = selectedIds.map((selectedId) => { + const parentData = findParentDataset(selectedId); + const exclusiveSource = parentData?.sourceExclusive; + const parentDataSourceValues = parentData?.taxonomy.filter((x) => x.name === 'Source')?.[0]?.values?.map((value) => value.id); + return {id: selectedId, values: parentDataSourceValues, sourceExclusive: exclusiveSource?.toLowerCase() || ''}; + }); + + if (exclusionSelected) { + relevantIds = selectedIdsWithParentData.filter((x) => x.values?.includes(x.sourceExclusive)).map((x) => x.id); + } else { + relevantIds = selectedIdsWithParentData.filter((x) => !x.values?.includes(x.sourceExclusive)).map((x) => x.id); + } + + setSelectedIds((ids) => + ids.filter((id) => relevantIds?.includes(id)) + ); + } + }, [exclusionSelected]); + useEffect(() => { if (revealed) { if (firstRevealRef.current) { @@ -133,24 +183,22 @@ export function DatasetSelectorModal(props: DatasetSelectorModalProps) { } }, [revealed]); - // Filtered datasets for modal display - const displayDatasets = useMemo<(DatasetData & {countSelectedLayers: number})[]>( - () => - // TODO: Move function from data-catalog once that page is removed. - prepareDatasets(allDatasets, { - search, - taxonomies, - sortField, - sortDir, - filterLayers: true - }) - .map(dataset => ({ - ...dataset, - countSelectedLayers: countOverlap(dataset.layers.map(l => l.id), selectedIds) - })), - [search, taxonomies, sortField, sortDir, selectedIds] - ); - + useEffect(() => { + const datasets = prepareDatasets(allDatasets, { + search, + taxonomies, + sortField, + sortDir, + filterLayers: true + }) + .map(dataset => ({ + ...dataset, + countSelectedLayers: countOverlap(dataset.layers.map(l => l.id), selectedIds) + })); + + setDatasetsToDisplay(datasets); + }, [search, taxonomies, sortField, sortDir, selectedIds]); + return ( ( - + )} content={ } diff --git a/app/scripts/components/stories/hub/index.tsx b/app/scripts/components/stories/hub/index.tsx index e399e7409..91f09fbd8 100644 --- a/app/scripts/components/stories/hub/index.tsx +++ b/app/scripts/components/stories/hub/index.tsx @@ -188,7 +188,6 @@ function StoriesHub() { diff --git a/app/scripts/styles/pill.tsx b/app/scripts/styles/pill.tsx index 7d4a5dc4c..ae50082c0 100644 --- a/app/scripts/styles/pill.tsx +++ b/app/scripts/styles/pill.tsx @@ -8,7 +8,11 @@ const renderPillVariation = ({ variation }: PillProps) => { color: ${themeVal('color.surface')}; background: ${themeVal('color.surface-100a')}; `; - + case 'warning': + return css` + color: ${themeVal('color.danger-500')}; + background: ${themeVal('color.danger-200a')}; + `; case 'primary': default: return css` @@ -19,7 +23,7 @@ const renderPillVariation = ({ variation }: PillProps) => { }; interface PillProps { - variation?: 'primary' | 'achromic'; + variation?: 'primary' | 'achromic' | 'warning'; } export const Pill = styled.span` display: inline-flex; diff --git a/docs/content/CONTENT.md b/docs/content/CONTENT.md index 18a072055..6e8dec475 100644 --- a/docs/content/CONTENT.md +++ b/docs/content/CONTENT.md @@ -51,6 +51,7 @@ thematics: string[] sources: string[] featured: boolean disableExplore: boolean +sourceExclusive: string layers: Layer[] related: Related[] @@ -115,6 +116,10 @@ Whether this dataset is featured `boolean` When set to true, the 'explore data' section won't be available for this dataset. +**sourceExclusive** +`string` +When a source value is provided, this marks the dataset layers as can only be analyzed with layers from the same source + **layers** `Layer[]` diff --git a/mock/datasets/no2.data.mdx b/mock/datasets/no2.data.mdx index 49fc9ae11..f64ab7e9a 100644 --- a/mock/datasets/no2.data.mdx +++ b/mock/datasets/no2.data.mdx @@ -2,6 +2,7 @@ id: no2 name: 'Nitrogen Dioxide' featured: true +sourceExclusive: Mock description: "Since the outbreak of the novel coronavirus, atmospheric concentrations of nitrogen dioxide have changed by as much as 60% in some regions." usage: - url: 'https://nasa-impact.github.io/veda-documentation/timeseries-stac-api.html' @@ -210,7 +211,7 @@ NASA has observed subsequent rebounds in nitrogen dioxide levels as the lockdown ## Scientific research [Ongoing research](https://airquality.gsfc.nasa.gov/) by scientists in the Atmospheric Chemistry and Dynamics Laboratory at NASA’s Goddard Space Flight Center and [new research](https://science.nasa.gov/earth-science/rrnes-awards) funded by NASA's Rapid Response and Novel research in the Earth Sciences (RRNES) program element seek to better understand the atmospheric effects of the COVID-19 shutdowns. -For nitrogen dioxide levels related to COVID-19, NASA uses data collected by the joint NASA-Royal Netherlands Meteorological Institute (KNMI) [Ozone Monitoring Instrument (OMI)](https://aura.gsfc.nasa.gov/omi.html) aboard the Aura satellite, as well as data collected by the Tropospheric Monitoring Instrument (TROPOMI) aboard the European Commission’s Copernicus Sentinel-5P satellite, built by the European Space Agency. +For nitrogen dioxide levels related to COVID-19, NASA uses data collected by the joint NASA-Royal Netherlands Meteorological Institute (KNMI) [Ozone Monitoring Instrument (OMI)](https://aura.gsfc.nasa.gov/omi.html) aboard the Aura satellite, as well as data collected by the Tropospheric Monitoring Instrument (TROPOMI) aboard the European Commission’s Copernicus Sentinel-5P satellite, built by the European Space Agency. OMI, which launched in 2004, preceded TROPOMI, which launched in 2017. While TROPOMI provides higher resolution information, the longer OMI data record provides context for the TROPOMI observations. @@ -262,4 +263,4 @@ Interpreting satellite NO2 data must be done carefully, as the quantity observed * [Tropospheric Emissions: Monitoring of Pollution (TEMPO)](http://tempo.si.edu/outreach.html) * [Pandora Project](https://pandora.gsfc.nasa.gov/) - + \ No newline at end of file diff --git a/parcel-resolver-veda/index.d.ts b/parcel-resolver-veda/index.d.ts index 7b28fce3c..09a4bc92d 100644 --- a/parcel-resolver-veda/index.d.ts +++ b/parcel-resolver-veda/index.d.ts @@ -178,6 +178,7 @@ export interface LayerInfo { */ export interface DatasetData { featured?: boolean; + sourceExclusive?: string; id: string; name: string; infoDescription?: string;