diff --git a/app/scripts/components/analysis/define/index.tsx b/app/scripts/components/analysis/define/index.tsx index 6cc845f54..cd50aa5c8 100644 --- a/app/scripts/components/analysis/define/index.tsx +++ b/app/scripts/components/analysis/define/index.tsx @@ -431,7 +431,7 @@ export default function Analysis() { value={end ? dateToInputFormat(end) : ''} onChange={onEndDateChange} min={dateToInputFormat(start)} - max='2022-12-31' + max='2023-12-31' /> diff --git a/app/scripts/components/exploration/atoms/timeline.ts b/app/scripts/components/exploration/atoms/timeline.ts index 0e559d424..9c1026afd 100644 --- a/app/scripts/components/exploration/atoms/timeline.ts +++ b/app/scripts/components/exploration/atoms/timeline.ts @@ -30,5 +30,3 @@ export const timelineSizesAtom = atom((get) => { }; }); -// Whether or not the dataset rows are expanded. -export const isExpandedAtom = atom(true); diff --git a/app/scripts/components/exploration/components/chart-popover.tsx b/app/scripts/components/exploration/components/chart-popover.tsx index 6476f1a5d..011b9f4aa 100644 --- a/app/scripts/components/exploration/components/chart-popover.tsx +++ b/app/scripts/components/exploration/components/chart-popover.tsx @@ -5,7 +5,7 @@ import React, { useState } from 'react'; import { createPortal } from 'react-dom'; -import styled from 'styled-components'; +import styled, {css} from 'styled-components'; import { useFloating, autoUpdate, @@ -14,12 +14,10 @@ import { shift } from '@floating-ui/react'; import { bisector, ScaleTime, sort } from 'd3'; -import { useAtomValue } from 'jotai'; import { format } from 'date-fns'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; -import { AnalysisTimeseriesEntry, TimeDensity } from '../types.d.ts'; -import { isExpandedAtom } from '../atoms/timeline'; +import { AnalysisTimeseriesEntry, TimeDensity, TimelineDatasetSuccess } from '../types.d.ts'; import { DataMetric } from './datasets/analysis-metrics'; import { getNumForChart } from '$components/common/chart/utils'; @@ -48,12 +46,16 @@ const MetricList = styled.ul` list-style: none; margin: 0 -${glsp()}; padding: 0; + padding-top: ${glsp(0.25)}; gap: ${glsp(0.25)}; - > li { padding: ${glsp(0, 1)}; } `; +const MetricLi = styled.li` + display: flex; + justify-content: space-between; +`; const MetricItem = styled.p<{ metricThemeColor: string }>` display: flex; @@ -71,46 +73,74 @@ const MetricItem = styled.p<{ metricThemeColor: string }>` } `; -type DivProps = JSX.IntrinsicElements['div']; +const fadedtext = css` + color: #83868A; +`; + +const TitleBox = styled.div` + background-color: #FAFAFA; + ${fadedtext}; + padding: ${glsp(0.5)}; + font-size: 0.75rem; +`; + +const ContentBox = styled.div` + padding: ${glsp(0.5)}; + font-size: 0.75rem; +`; +const MetaBox = styled.div` + display: flex; + align-items: center; + gap: ${glsp(1)}; +`; + +const UnitBox = styled.div` + ${fadedtext}; +`; +type DivProps = JSX.IntrinsicElements['div']; interface DatasetPopoverProps extends DivProps { data: AnalysisTimeseriesEntry; activeMetrics: DataMetric[]; timeDensity: TimeDensity; + dataset: TimelineDatasetSuccess; } function DatasetPopoverComponent( props: DatasetPopoverProps, ref: MutableRefObject ) { - const { data, activeMetrics, timeDensity, ...rest } = props; - - const isExpanded = useAtomValue(isExpandedAtom); - + const { data, dataset, activeMetrics, timeDensity, style, ...rest } = props; + // Check if there is no data to show const hasData = activeMetrics.some( (metric) => typeof data[metric.id] === 'number' ); - if (!isExpanded || !hasData) return null; + if (!hasData) return null; return createPortal( -
- {timeDensityFormat(data.date, timeDensity)} - - {activeMetrics.map((metric) => { - const dataPoint = data[metric.id]; - - return typeof dataPoint !== 'number' ? null : ( -
  • - - {metric.chartLabel}: - {getNumForChart(dataPoint)} - -
  • - ); - })} -
    +
    + {dataset.data.name} + + + {timeDensityFormat(data.date, timeDensity)} + {dataset.data.info?.unit} + + + {activeMetrics.map((metric) => { + const dataPoint = data[metric.id]; + return typeof dataPoint !== 'number' ? null : ( + + + {metric.chartLabel} + + {getNumForChart(dataPoint)} + + ); + })} + +
    , document.body ); diff --git a/app/scripts/components/exploration/components/datasets/analysis-metrics.tsx b/app/scripts/components/exploration/components/datasets/analysis-metrics.tsx index 2fd62c072..130c059aa 100644 --- a/app/scripts/components/exploration/components/datasets/analysis-metrics.tsx +++ b/app/scripts/components/exploration/components/datasets/analysis-metrics.tsx @@ -14,6 +14,7 @@ export interface DataMetric { | 'infographicC' | 'infographicD' | 'infographicE'; + style?: Record } export const DATA_METRICS: DataMetric[] = [ @@ -21,7 +22,8 @@ export const DATA_METRICS: DataMetric[] = [ id: 'min', label: 'Min', chartLabel: 'Min', - themeColor: 'infographicA' + themeColor: 'infographicA', + style: { "strokeDasharray": "2 2"} }, { id: 'mean', @@ -33,7 +35,8 @@ export const DATA_METRICS: DataMetric[] = [ id: 'max', label: 'Max', chartLabel: 'Max', - themeColor: 'infographicC' + themeColor: 'infographicC', + style: { "strokeDasharray": "2 2"} }, { id: 'std', @@ -49,6 +52,8 @@ export const DATA_METRICS: DataMetric[] = [ } ]; +export const DEFAULT_DATA_METRICS: DataMetric[] = DATA_METRICS.filter(metric => metric.id ==='mean' || metric.id==='std'); + const MetricList = styled(DropMenu)` display: flex; flex-flow: column; diff --git a/app/scripts/components/exploration/components/datasets/dataset-chart.tsx b/app/scripts/components/exploration/components/datasets/dataset-chart.tsx index 35482afc8..90bbcc602 100644 --- a/app/scripts/components/exploration/components/datasets/dataset-chart.tsx +++ b/app/scripts/components/exploration/components/datasets/dataset-chart.tsx @@ -1,13 +1,11 @@ import React, { useMemo } from 'react'; import { useTheme } from 'styled-components'; -import { extent, scaleLinear, ScaleTime, line, ScaleLinear } from 'd3'; -import { useAtomValue } from 'jotai'; +import { extent, scaleLinear, ScaleTime, line, area, ScaleLinear } from 'd3'; import { AnimatePresence, motion } from 'framer-motion'; import styled from 'styled-components'; import { - CollecticonCog, + CollecticonChartLine, } from '@devseed-ui/collecticons'; -import { isExpandedAtom } from '../../atoms/timeline'; import { RIGHT_AXIS_SPACE } from '../../constants'; import { DatasetTrackMessage } from './dataset-track-message'; import { DataMetric } from './analysis-metrics'; @@ -30,48 +28,63 @@ interface DatasetChartProps { onUpdateSettings: (type: string, m: DataMetric[]) => void; } -const ChartAnalysisMenu = styled.div` +const ChartAnalysisMenu = styled.div<{axisWidth: number}>` width: inherit; position: relative; display: flex; justify-content: end; - margin-right: 2.3rem; + margin-right: calc(${props=> props.axisWidth}px + 0.5rem); + z-index: 3000; +`; +const AxisBackground = styled.div<{axisWidth: number}>` + position: absolute; + right: 0; + top: 0; + width: ${props=> props.axisWidth}px; + height: 100%; + background-color: #F0F0F0; + z-index: -1; `; export function DatasetChart(props: DatasetChartProps) { - const { xScaled, width, isVisible, dataset, activeMetrics, highlightDate, onUpdateSettings } = - props; - + const { xScaled, width, isVisible, dataset, activeMetrics, highlightDate, onUpdateSettings } = props; const analysisData = dataset.analysis as TimelineDatasetAnalysisSuccess; const timeseries = analysisData.data.timeseries; - const theme = useTheme(); + const areaDataKey = 'stdArea'; + const height = 180; - const isExpanded = useAtomValue(isExpandedAtom); + // Simplifying the enhancedTimeseries mapping + const enhancedTimeseries = timeseries.map(e => ({ + ...e, + [areaDataKey]: [e.mean - e.std, e.mean + e.std], + })); - const height = isExpanded ? 180 : 70; + // Filter line and area metrics once, avoiding separate filter calls + const { lineMetrics, areaMetrics } = activeMetrics.reduce<{lineMetrics: DataMetric[], areaMetrics: DataMetric[]}>((acc, metric:DataMetric) => { + metric.id === 'std' ? acc.areaMetrics.push(metric) : acc.lineMetrics.push(metric); + return acc; + }, { lineMetrics: [], areaMetrics: [] }); - const yExtent = useMemo( - () => - extent( - // Extent of all active metrics. - timeseries.flatMap((d) => extent(activeMetrics.map((m) => d[m.id]))) - ) as [undefined, undefined] | [number, number], - [timeseries, activeMetrics] - ); + + const yExtent = useMemo(() => { + const extents = [ + ...enhancedTimeseries.flatMap(d => extent(lineMetrics.map(m => d[m.id]))), + ...(areaMetrics.length ? enhancedTimeseries.flatMap(d => extent([d[areaDataKey]].flat())) : []) + ].filter(Boolean); // Filter out falsey values + return extent(extents.flat()) as [undefined, undefined] | [number, number]; + }, [enhancedTimeseries, lineMetrics, areaMetrics]); const y = useMemo(() => { const [min = 0, max = 0] = yExtent; - return ( - scaleLinear() - // Add 5% buffer - .domain([min * 0.95, max * 1.05]) - .range([height - CHART_MARGIN * 2, 0]) - ); + return scaleLinear() + .domain([(min * 0.95 > 0) ? 0 : min * 0.95, max * 1.05]) + .range([height - CHART_MARGIN * 2, 0]); }, [yExtent, height]); - const chartAnalysisIconTrigger: JSX.Element = ; + const chartAnalysisIconTrigger: JSX.Element = ; + return (
    {!activeMetrics.length && ( @@ -79,9 +92,10 @@ export function DatasetChart(props: DatasetChartProps) { There are no active metrics to visualize. )} - + + @@ -94,24 +108,37 @@ export function DatasetChart(props: DatasetChartProps) { width={width} height={height} isVisible={isVisible} - isExpanded={isExpanded} /> - + {/* This is where the line is drawn */} - {activeMetrics.map( + {areaMetrics.map( + (metric) => + enhancedTimeseries.some((d) => !isNaN(d[metric.id])) && ( + + ) + )} + {lineMetrics.map( (metric) => - timeseries.some((d) => !isNaN(d[metric.id])) && ( + enhancedTimeseries.some((d) => !isNaN(d[metric.id])) && ( ) @@ -123,19 +150,59 @@ export function DatasetChart(props: DatasetChartProps) { ); } -interface DateLineProps { + +interface DataAreaProps { x: ScaleTime; y: ScaleLinear; prop: string; data: any[]; color: string; isVisible: boolean; - isExpanded: boolean; +} +interface DateLineProps extends DataAreaProps { highlightDate?: Date; + style?: any; +} + +interface DataItem { + date: Date; + [key: string]: [number, number] | Date; +} + +function DataArea(props: DataAreaProps) { + const { x, y, prop, data, color, isVisible } = props; + + const path = useMemo(() => { + const areaGenerator = area() + .defined((d) => d[prop] !== null) + .x((d) => x(d.date ?? 0)) + .y0((d) => y(d[prop] ? d[prop][0] : 0)) + .y1((d) => y(d[prop] ? d[prop][1] : 0)); + + return areaGenerator(data); + }, [x, y, data]); // Ensure all variables used are listed in the dependencies + + const maxOpacity = isVisible ? 1 : 0.25; + + if (!path) return null; + + return ( + + + + ); } function DataLine(props: DateLineProps) { - const { x, y, prop, data, color, isVisible, isExpanded, highlightDate } = + const { x, y, prop, data, color, style, isVisible, highlightDate } = props; const path = useMemo( @@ -154,12 +221,14 @@ function DataLine(props: DateLineProps) { return ( {data.map((d) => { if (typeof d[prop] !== 'number') return false; @@ -167,19 +236,19 @@ function DataLine(props: DateLineProps) { const highlight = isVisible && highlightDate?.getTime() === d.date.getTime(); - return ( + return highlight? ( - ); + ): false; })} ); @@ -191,28 +260,24 @@ interface AxisGridProps { height: number; yLabel?: string; isVisible: boolean; - isExpanded: boolean; } function AxisGrid(props: AxisGridProps) { - const { y, width, height, isVisible, isExpanded, yLabel } = props; + const { y, width, height, isVisible, yLabel } = props; const theme = useTheme(); - const ticks = y.ticks(5); - return ( - {isExpanded && ( - + > {yLabel && ( ))} - - )} + ); } diff --git a/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx b/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx index ad61a39ca..34e187835 100644 --- a/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx +++ b/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx @@ -150,6 +150,7 @@ export function DatasetListItem(props: DatasetListItemProps) { data: dataset.analysis.data?.timeseries }); + const { refs: popoverRefs, floatingStyles, @@ -180,6 +181,7 @@ export function DatasetListItem(props: DatasetListItemProps) { [dataset] ); + const onDragging = (e) => { controls.start(e); }; @@ -274,6 +276,7 @@ export function DatasetListItem(props: DatasetListItemProps) { - - - - - { - setExpanded((v) => !v); - }} - > - {isExpanded ? ( - - ) : ( - - )} - ) : ( diff --git a/app/scripts/components/exploration/data-utils.ts b/app/scripts/components/exploration/data-utils.ts index 2d18bbf93..7a21a2e94 100644 --- a/app/scripts/components/exploration/data-utils.ts +++ b/app/scripts/components/exploration/data-utils.ts @@ -16,7 +16,8 @@ import { } from './types.d.ts'; import { DataMetric, - DATA_METRICS + DATA_METRICS, + DEFAULT_DATA_METRICS } from './components/datasets/analysis-metrics'; import { utcString2userTzDate } from '$utils/date'; @@ -71,17 +72,17 @@ export const datasetLayers = Object.values(datasets) */ function getInitialMetrics(data: DatasetLayer): DataMetric[] { const metricsIds = data.analysis?.metrics ?? []; - + + if (!metricsIds.length) { + return DEFAULT_DATA_METRICS; + } + const foundMetrics = metricsIds .map((metric: string) => { return DATA_METRICS.find((m) => m.id === metric)!; }) .filter(Boolean); - if (!foundMetrics.length) { - return DATA_METRICS; - } - return foundMetrics; } diff --git a/app/scripts/styles/theme.ts b/app/scripts/styles/theme.ts index c6c642702..1af0827af 100644 --- a/app/scripts/styles/theme.ts +++ b/app/scripts/styles/theme.ts @@ -22,11 +22,11 @@ export const VEDA_OVERRIDE_THEME = { base: '#2c3e50', primary: '#2276ac', danger: '#FC3D21', - infographicA: '#fcab10', - infographicB: '#f4442e', - infographicC: '#b62b6e', - infographicD: '#2ca58d', - infographicE: '#2276ac' + infographicA: '#83868A', // Min + infographicB: '#6138BE', // Ave + infographicC: '#83868A', // Max + infographicD: '#F2F2F2', // STD + infographicE: '#3094E3' // Median }, type: { base: { diff --git a/mock/datasets/no2.data.mdx b/mock/datasets/no2.data.mdx index c68f9478a..05710378e 100644 --- a/mock/datasets/no2.data.mdx +++ b/mock/datasets/no2.data.mdx @@ -99,11 +99,11 @@ layers: - "#461070" - "#050308" analysis: - metrics: - - min - - max - # dummy value to make sure non existent values are sagfely discarded - - non-existent + # metrics: + # - min + # - max + # # dummy value to make sure non existent values are sagfely discarded + # - non-existent info: source: NASA spatialExtent: Global