Skip to content

Commit

Permalink
Put two legends when compare layer is not the same with the base layer (
Browse files Browse the repository at this point in the history
#547)

related to #468 

Sometimes authors need to put two layers with different colormaps to
compare. (ex.
https://www.earthdata.nasa.gov/dashboard/discoveries/urban-heating the
map at the top could be an interactive compare map.) This PR addresses
the need.

Currently, there is one discovery that uses different layers to compare
from a config repo: /discoveries/tws-trends

This PR stacks two legends. I am happy to iterate if there is a better
way to handle multiple legends.
![Screen Shot 2023-06-01 at 3 40 22
PM](https://github.com/NASA-IMPACT/veda-ui/assets/4583806/0b1432d4-c42f-46c2-aeff-dadaa85c33de)
  • Loading branch information
hanbyul-here authored Jun 15, 2023
2 parents 6097ea1 + 1ca1792 commit ce036b3
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 113 deletions.
71 changes: 47 additions & 24 deletions app/scripts/components/common/blocks/scrollytelling/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ import { S_FAILED, S_SUCCEEDED } from '$utils/status';

import { SimpleMap } from '$components/common/mapbox/map';
import Hug from '$styles/hug';
import LayerLegend from '$components/common/mapbox/layer-legend';
import {
LayerLegendContainer,
LayerLegend
} from '$components/common/mapbox/layer-legend';
import MapMessage from '$components/common/mapbox/map-message';
import { MapLoading } from '$components/common/loading-skeleton';
import { HintedError } from '$utils/hinted-error';
Expand Down Expand Up @@ -65,6 +68,9 @@ const TheMap = styled.div<{ topOffset: number }>`
top: ${topOffset}px;
height: calc(100vh - ${topOffset}px);
`}
.mapboxgl-canvas {
height: 100%;
}
`;

const TheChapters = styled(Hug)`
Expand Down Expand Up @@ -392,25 +398,37 @@ function Scrollytelling(props) {
{/*
Map overlay element
Layer legend for the active layer.
The SwitchTransition animated between 2 elements, so when there's no
legend we use an empty div to ensure that there's an out animation.
We also have to set the timeout to 1 because the empty div will not
have transitions defined for it. This causes the transitionend
listener to never fire leading to an infinite wait.
*/}
{activeChapterLayer?.layer.legend && (
<SwitchTransition>
<CSSTransition
key={activeChapterLayer.layer.name}
addEndListener={(node, done) => {
node.addEventListener('transitionend', done, false);
}}
classNames='reveal'
>
<LayerLegend
id={`base-${activeChapterLayer.layer.id}`}
description={activeChapterLayer.layer.description}
title={activeChapterLayer.layer.name}
{...activeChapterLayer.layer.legend}
/>
</CSSTransition>
</SwitchTransition>
)}
<SwitchTransition>
<CSSTransition
key={activeChapterLayer?.layer.name}
timeout={!activeChapterLayer ? 1 : undefined}
addEndListener={(node, done) => {
if (!activeChapterLayer) return;
node?.addEventListener('transitionend', done, false);
}}
classNames='reveal'
>
{activeChapterLayer?.layer.legend ? (
<LayerLegendContainer>
<LayerLegend
id={`base-${activeChapterLayer.layer.id}`}
description={activeChapterLayer.layer.description}
title={activeChapterLayer.layer.name}
{...activeChapterLayer.layer.legend}
/>
</LayerLegendContainer>
) : (
<div />
)}
</CSSTransition>
</SwitchTransition>

<Styles>
<Basemap />
Expand All @@ -419,9 +437,10 @@ function Scrollytelling(props) {
if (!resolvedLayer) return null;

const { runtimeData, Component: LayerCmp, layer } = resolvedLayer;
const isHidden = (!activeChapterLayerId ||
activeChapterLayerId !== runtimeData.id ||
activeChapter.showBaseMap);
const isHidden =
!activeChapterLayerId ||
activeChapterLayerId !== runtimeData.id ||
activeChapter.showBaseMap;

if (!LayerCmp) return null;

Expand All @@ -441,7 +460,7 @@ function Scrollytelling(props) {
sourceParams={layer.sourceParams}
zoomExtent={layer.zoomExtent}
onStatusChange={onLayerLoadSuccess}
idSuffix={'scrolly-'+ lIdx}
idSuffix={'scrolly-' + lIdx}
isHidden={isHidden}
/>
);
Expand All @@ -450,7 +469,11 @@ function Scrollytelling(props) {
className='root'
mapRef={mapRef}
containerRef={mapContainer}
onLoad={() => setMapLoaded(true)}
onLoad={() => {
setMapLoaded(true);
// Fit the map to the container once loaded.
mapRef.current?.resize();
}}
mapOptions={mapOptions}
/>
</Styles>
Expand Down
31 changes: 22 additions & 9 deletions app/scripts/components/common/mapbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { AoiChangeListenerOverload, AoiState } from '../aoi/types';
import { getLayerComponent, resolveConfigFunctions } from './layers/utils';
import { SimpleMap } from './map';
import MapMessage from './map-message';
import LayerLegend from './layer-legend';
import { LayerLegendContainer, LayerLegend } from './layer-legend';
import { useBasemap } from './map-options/use-basemap';
import { DEFAULT_MAP_STYLE_URL } from './map-options/basemaps';
import { Styles } from './layers/styles';
Expand Down Expand Up @@ -365,13 +365,27 @@ function MapboxMapComponent(props: MapboxMapProps, ref) {
Layer legend for the active layer.
*/}
{baseLayerResolvedData?.legend && (
<LayerLegend
id={`base-${baseLayerResolvedData.id}`}
title={baseLayerResolvedData.name}
description={baseLayerResolvedData.description}
{...baseLayerResolvedData.legend}
/>
)}
<LayerLegendContainer>
<LayerLegend
id={`base-${baseLayerResolvedData.id}`}
title={baseLayerResolvedData.name}
description={baseLayerResolvedData.description}
{...baseLayerResolvedData.legend}
/>
{compareLayerResolvedData?.legend &&
isComparing &&
(baseLayerResolvedData.id !== compareLayerResolvedData.id) &&
<LayerLegend
id={`compare-${compareLayerResolvedData.id}`}
title={compareLayerResolvedData.name}
description={compareLayerResolvedData.description}
{...compareLayerResolvedData.legend}
/>}
</LayerLegendContainer>
)}




{/*
Maps container
Expand Down Expand Up @@ -442,7 +456,6 @@ function MapboxMapComponent(props: MapboxMapProps, ref) {
boundariesOption={boundariesOption}
/>
{isMapCompareLoaded &&
isComparing &&
compareLayerResolvedData &&
CompareLayerComponent && (
<CompareLayerComponent
Expand Down
157 changes: 88 additions & 69 deletions app/scripts/components/common/mapbox/layer-legend.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React, { ReactNode, Fragment } from 'react';
import styled from 'styled-components';
import { LayerLegendCategorical, LayerLegendGradient } from 'veda';
import { AccordionFold, AccordionManager } from '@devseed-ui/accordion';
Expand All @@ -22,17 +22,20 @@ import {
WidgetItemHGroup
} from '$styles/panel';


type LayerLegendCommonProps = {
interface LayerLegendCommonProps {
id: string;
title: string;
description: string;
};
}

type LegendSwatchProps = {
interface LegendSwatchProps {
hasHelp?: boolean;
stops: string | string[];
};
}

interface LayerLegendContainerProps {
children: ReactNode | ReactNode[];
}

const makeGradient = (stops: string[]) => {
if (stops.length === 1) return stops[0];
Expand All @@ -44,17 +47,16 @@ const makeGradient = (stops: string[]) => {
const printLegendVal = (val: string | number) =>
typeof val === 'number' ? formatThousands(val, { shorten: true }) : val;

const LayerLegendSelf = styled.div`
export const LegendContainer = styled.div`
position: absolute;
z-index: 8;
bottom: ${variableGlsp()};
right: ${variableGlsp()};
display: flex;
flex-flow: column nowrap;
border-radius: ${themeVal('shape.rounded')};
box-shadow: ${themeVal('boxShadow.elevationB')};
border-radius: ${themeVal('shape.rounded')};
background-color: ${themeVal('color.surface')};
width: 16rem;
&.reveal-enter {
opacity: 0;
Expand All @@ -76,9 +78,23 @@ const LayerLegendSelf = styled.div`
&.reveal-exit-active {
transition: bottom 240ms ease-in-out, opacity 240ms ease-in-out;
}
`;

const LayerLegendSelf = styled.div`
display: flex;
flex-flow: column nowrap;
width: 16rem;
border-bottom: 1px solid ${themeVal('color.base-100')};
${WidgetItemHeader} {
padding: ${variableGlsp(0.5, 0.75)};
padding: ${variableGlsp(0.25, 0.5)};
}
&:only-child {
${WidgetItemHeader} {
padding: ${variableGlsp(0.5)};
}
border-bottom: 0;
}
`;

Expand Down Expand Up @@ -152,78 +168,81 @@ const LegendBody = styled(WidgetItemBodyInner)`
.scroll-inner {
padding: ${variableGlsp(0.5, 0.75)};
}
.shadow-bottom {
border-radius: ${themeVal('shape.rounded')};
}
`;

function LayerLegend(
export function LayerLegend(
props: LayerLegendCommonProps & (LayerLegendGradient | LayerLegendCategorical)
) {
const { id, type, title, description } = props;

return (
<AccordionManager>
<AccordionFold
id={id}
forwardedAs={LayerLegendSelf}
renderHeader={({ isExpanded, toggleExpanded }) => (
<WidgetItemHeader>
<WidgetItemHGroup>
<WidgetItemHeadline>
<LayerLegendTitle>{title}</LayerLegendTitle>
{/* <Subtitle as='p'>Subtitle</Subtitle> */}
</WidgetItemHeadline>
<Toolbar size='small'>
<ToolbarIconButton
variation='base-text'
active={isExpanded}
onClick={toggleExpanded}
>
<CollecticonCircleInformation
title='Information about layer'
meaningful
/>
</ToolbarIconButton>
</Toolbar>
</WidgetItemHGroup>
{type === 'categorical' && (
<LayerCategoricalGraphic type='categorical' stops={props.stops} />
)}
{type === 'gradient' && (
<LayerGradientGraphic
type='gradient'
stops={props.stops}
min={props.min}
max={props.max}
/>
)}
</WidgetItemHeader>
)}
renderBody={() => (
<LegendBody>
<ShadowScrollbar
scrollbarsProps={{
autoHeight: true,
autoHeightMin: 32,
autoHeightMax: 240
}}
>
<div className='scroll-inner'>
{description || (
<p>No info available for this layer.</p>
)}
</div>
</ShadowScrollbar>
</LegendBody>
)}
/>
</AccordionManager>
<AccordionFold
id={id}
forwardedAs={LayerLegendSelf}
renderHeader={({ isExpanded, toggleExpanded }) => (
<WidgetItemHeader>
<WidgetItemHGroup>
<WidgetItemHeadline>
<LayerLegendTitle>{title}</LayerLegendTitle>
{/* <Subtitle as='p'>Subtitle</Subtitle> */}
</WidgetItemHeadline>
<Toolbar size='small'>
<ToolbarIconButton
variation='base-text'
active={isExpanded}
onClick={toggleExpanded}
>
<CollecticonCircleInformation
title='Information about layer'
meaningful
/>
</ToolbarIconButton>
</Toolbar>
</WidgetItemHGroup>
{type === 'categorical' && (
<LayerCategoricalGraphic type='categorical' stops={props.stops} />
)}
{type === 'gradient' && (
<LayerGradientGraphic
type='gradient'
stops={props.stops}
min={props.min}
max={props.max}
/>
)}
</WidgetItemHeader>
)}
renderBody={() => (
<LegendBody>
<ShadowScrollbar
scrollbarsProps={{
autoHeight: true,
autoHeightMin: 32,
autoHeightMax: 120
}}
>
<div className='scroll-inner'>
{description || <p>No info available for this layer.</p>}
</div>
</ShadowScrollbar>
</LegendBody>
)}
/>
);
}

export default LayerLegend;
export function LayerLegendContainer(props: LayerLegendContainerProps) {
return (
<LegendContainer>
<AccordionManager>
{props.children}
</AccordionManager>
</LegendContainer>
);
}

function LayerCategoricalGraphic(props: LayerLegendCategorical) {
const { stops } = props;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ interface MapLayerRasterTimeseriesProps {
zoomExtent?: [number, number];
onStatusChange?: (result: { status: ActionStatus; id: string }) => void;
isHidden: boolean;
idSuffix?: string
idSuffix?: string;
}

export interface StacFeature {
Expand Down
Loading

0 comments on commit ce036b3

Please sign in to comment.