From e3e9f219015078f83216209c21f6d152ba19493b Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Tue, 26 Mar 2024 15:17:19 +0900 Subject: [PATCH 1/7] Change chart style, tooltip style --- .../components/analysis/define/index.tsx | 2 +- .../components/exploration/atoms/timeline.ts | 2 - .../exploration/components/chart-popover.tsx | 84 ++++++--- .../components/datasets/analysis-metrics.tsx | 9 +- .../components/datasets/dataset-chart.tsx | 168 ++++++++++++------ .../components/datasets/dataset-list-item.tsx | 3 + .../components/timeline/timeline-controls.tsx | 34 +--- .../components/exploration/data-utils.ts | 13 +- app/scripts/styles/theme.ts | 10 +- mock/datasets/no2.data.mdx | 10 +- 10 files changed, 202 insertions(+), 133 deletions(-) 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 From 0e1a52e127f2fa54494c0a6197d7398968f371a2 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Tue, 26 Mar 2024 17:11:50 +0900 Subject: [PATCH 2/7] Separate hardcoded color values --- .../components/exploration/components/chart-popover.tsx | 5 +++-- .../exploration/components/datasets/dataset-chart.tsx | 4 ++-- app/scripts/components/exploration/constants.ts | 6 ++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/scripts/components/exploration/components/chart-popover.tsx b/app/scripts/components/exploration/components/chart-popover.tsx index 011b9f4aa..eb364b8dc 100644 --- a/app/scripts/components/exploration/components/chart-popover.tsx +++ b/app/scripts/components/exploration/components/chart-popover.tsx @@ -18,6 +18,7 @@ import { format } from 'date-fns'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; import { AnalysisTimeseriesEntry, TimeDensity, TimelineDatasetSuccess } from '../types.d.ts'; +import { FADED_TEXT_COLOR, TEXT_TITME_BG_COLOR } from '../constants'; import { DataMetric } from './datasets/analysis-metrics'; import { getNumForChart } from '$components/common/chart/utils'; @@ -74,11 +75,11 @@ const MetricItem = styled.p<{ metricThemeColor: string }>` `; const fadedtext = css` - color: #83868A; + color: ${FADED_TEXT_COLOR}; `; const TitleBox = styled.div` - background-color: #FAFAFA; + background-color: ${TEXT_TITME_BG_COLOR}; ${fadedtext}; padding: ${glsp(0.5)}; font-size: 0.75rem; diff --git a/app/scripts/components/exploration/components/datasets/dataset-chart.tsx b/app/scripts/components/exploration/components/datasets/dataset-chart.tsx index 90bbcc602..e23c27e70 100644 --- a/app/scripts/components/exploration/components/datasets/dataset-chart.tsx +++ b/app/scripts/components/exploration/components/datasets/dataset-chart.tsx @@ -6,7 +6,7 @@ import styled from 'styled-components'; import { CollecticonChartLine, } from '@devseed-ui/collecticons'; -import { RIGHT_AXIS_SPACE } from '../../constants'; +import { RIGHT_AXIS_SPACE, AXIS_BG_COLOR } from '../../constants'; import { DatasetTrackMessage } from './dataset-track-message'; import { DataMetric } from './analysis-metrics'; import LayerChartAnalysisMenu from './layer-chart-analysis-menu'; @@ -42,7 +42,7 @@ const AxisBackground = styled.div<{axisWidth: number}>` top: 0; width: ${props=> props.axisWidth}px; height: 100%; - background-color: #F0F0F0; + background-color: ${AXIS_BG_COLOR}; z-index: -1; `; diff --git a/app/scripts/components/exploration/constants.ts b/app/scripts/components/exploration/constants.ts index bd87d735c..544765f95 100644 --- a/app/scripts/components/exploration/constants.ts +++ b/app/scripts/components/exploration/constants.ts @@ -11,3 +11,9 @@ export const emptyDateRange = { }; export const MAX_QUERY_NUM = 300; + +// @TECH-DEBT As we do not have a new design system that can accommodate the needs for colors +// We put colors here for now +export const AXIS_BG_COLOR = '#F0F0F0'; +export const FADED_TEXT_COLOR = '#83868A'; +export const TEXT_TITME_BG_COLOR = '#FAFAFA'; \ No newline at end of file From 10336f9aef7c014551a678bafc888c61b0173366 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Wed, 27 Mar 2024 13:49:06 +0900 Subject: [PATCH 3/7] Use the data start point, endpoint to show the tooltip when the cursor is still on the timeline but not on the data point anymore --- .../exploration/components/chart-popover.tsx | 60 ++++++++++++++----- .../components/datasets/dataset-list-item.tsx | 9 ++- .../exploration/hooks/use-dataset-hover.ts | 2 +- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/app/scripts/components/exploration/components/chart-popover.tsx b/app/scripts/components/exploration/components/chart-popover.tsx index eb364b8dc..99fc26847 100644 --- a/app/scripts/components/exploration/components/chart-popover.tsx +++ b/app/scripts/components/exploration/components/chart-popover.tsx @@ -2,7 +2,8 @@ import React, { MutableRefObject, forwardRef, useEffect, - useState + useState, + useMemo } from 'react'; import { createPortal } from 'react-dom'; import styled, {css} from 'styled-components'; @@ -18,7 +19,7 @@ import { format } from 'date-fns'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; import { AnalysisTimeseriesEntry, TimeDensity, TimelineDatasetSuccess } from '../types.d.ts'; -import { FADED_TEXT_COLOR, TEXT_TITME_BG_COLOR } from '../constants'; +import { FADED_TEXT_COLOR, TEXT_TITME_BG_COLOR, HEADER_COLUMN_WIDTH } from '../constants'; import { DataMetric } from './datasets/analysis-metrics'; import { getNumForChart } from '$components/common/chart/utils'; @@ -171,6 +172,7 @@ function getClosestDataPoint( data?: AnalysisTimeseriesEntry[], positionDate?: Date ) { + if (!positionDate || !data) return; const dataSorted = sort(data, (a, b) => a.date.getTime() - b.date.getTime()); @@ -209,7 +211,6 @@ interface InteractionDataPointOptions { */ export function getInteractionDataPoint(options: InteractionDataPointOptions) { const { isHovering, xScaled, containerWidth, layerX, data } = options; - if ( !isHovering || !xScaled || @@ -228,13 +229,10 @@ export function getInteractionDataPoint(options: InteractionDataPointOptions) { const closestDataPointPosition = closestDataPoint ? xScaled(closestDataPoint.date) : Infinity; - - const delta = Math.abs(layerX - closestDataPointPosition); - + const inView = closestDataPointPosition >= 0 && - closestDataPointPosition <= containerWidth && - delta <= 80; + closestDataPointPosition <= containerWidth; return inView ? closestDataPoint : undefined; } @@ -243,7 +241,9 @@ interface PopoverHookOptions { x?: number; y?: number; data?: AnalysisTimeseriesEntry; + dataset?: AnalysisTimeseriesEntry[]; enabled?: boolean; + xScaled?: ScaleTime; } /** @@ -253,7 +253,7 @@ interface PopoverHookOptions { * @returns */ export function usePopover(options: PopoverHookOptions) { - const { x, y, data, enabled } = options; + const { x, y, data, xScaled, dataset, enabled } = options; const inView = !!data; @@ -264,8 +264,39 @@ export function usePopover(options: PopoverHookOptions) { const [_isVisible, setVisible] = useState(inView); const isVisible = enabled && _isVisible; + // Do not make tooltip to follow the cursor. + // Instead, show tooltip at the edge of the timeline + // even if the cursor is off from the data timeline. + const datasetMinX = useMemo(() => { + if (!xScaled || !dataset) return; + return (xScaled(dataset[dataset.length-1]?.date) + HEADER_COLUMN_WIDTH); + }, [xScaled, dataset]); + + const datasetMaxX = useMemo(() => { + if (!xScaled || !dataset) return; + return (xScaled(dataset[0]?.date) + HEADER_COLUMN_WIDTH); + }, [xScaled, dataset]); + + const finalClientX = useMemo(() => { + if (!datasetMinX || !datasetMaxX || !x) return; + return x < datasetMinX ? datasetMinX : x > datasetMaxX? datasetMaxX: x; + },[datasetMaxX, datasetMinX, x]); + + // Determine which direction that popover needs to be displayed + const midpointX = useMemo(() => { + if (!xScaled || !dataset) return; + const start = xScaled(dataset[0]?.date) + HEADER_COLUMN_WIDTH; + const end = xScaled(dataset[dataset.length - 1]?.date) + HEADER_COLUMN_WIDTH; + return (start + end) / 2; + }, [xScaled, dataset]); + + const popoverLeft = useMemo(() => { + if (finalClientX === undefined || midpointX === undefined) return true; // Default to true or decide based on your UI needs + return finalClientX < midpointX; + }, [finalClientX, midpointX]); + const floating = useFloating({ - placement: 'left', + placement: popoverLeft ? 'left' : 'right', open: isVisible, onOpenChange: setVisible, middleware: [offset(10), flip(), shift({ padding: 16 })], @@ -273,7 +304,6 @@ export function usePopover(options: PopoverHookOptions) { }); const { refs, floatingStyles } = floating; - // Use a virtual element for the position reference. // https://floating-ui.com/docs/virtual-elements useEffect(() => { @@ -287,17 +317,17 @@ export function usePopover(options: PopoverHookOptions) { return { width: 0, height: 0, - x: x ?? 0, + x: finalClientX ?? 0, y: y ?? 0, top: y ?? 0, - left: x ?? 0, - right: x ?? 0, + left: finalClientX ?? 0, + right: finalClientX ?? 0, bottom: y ?? 0 }; } }); setVisible(true); - }, [refs, inView, x, y]); + }, [refs, inView, finalClientX, y]); return { refs, 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 34e187835..e0f884f63 100644 --- a/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx +++ b/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx @@ -142,15 +142,16 @@ export function DatasetListItem(props: DatasetListItemProps) { midY } = useDatasetHover(); + const timeSeriesData = dataset.analysis.data?.timeseries; + const dataPoint = getInteractionDataPoint({ isHovering, xScaled, containerWidth: width, layerX, - data: dataset.analysis.data?.timeseries + data: timeSeriesData }); - const { refs: popoverRefs, floatingStyles, @@ -159,7 +160,9 @@ export function DatasetListItem(props: DatasetListItemProps) { enabled: isAnalyzing, x: clientX, y: midY, - data: dataPoint + xScaled, + data: dataPoint, + dataset: timeSeriesData }); useAnalysisDataRequest({ datasetAtom }); diff --git a/app/scripts/components/exploration/hooks/use-dataset-hover.ts b/app/scripts/components/exploration/hooks/use-dataset-hover.ts index 6723c69bc..d45a3a76d 100644 --- a/app/scripts/components/exploration/hooks/use-dataset-hover.ts +++ b/app/scripts/components/exploration/hooks/use-dataset-hover.ts @@ -98,7 +98,7 @@ export function useDatasetHover(): DatasetHoverHookReturn { return { ref: elRef, isHovering: false }; } - const isHovering = + const isHovering = clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && From 948cd27e184b1169adad051595529099959a5efc Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Wed, 27 Mar 2024 13:54:28 +0900 Subject: [PATCH 4/7] Use themed zInex for analysis button --- .../exploration/components/datasets/dataset-chart.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scripts/components/exploration/components/datasets/dataset-chart.tsx b/app/scripts/components/exploration/components/datasets/dataset-chart.tsx index e23c27e70..f52e803eb 100644 --- a/app/scripts/components/exploration/components/datasets/dataset-chart.tsx +++ b/app/scripts/components/exploration/components/datasets/dataset-chart.tsx @@ -6,6 +6,7 @@ import styled from 'styled-components'; import { CollecticonChartLine, } from '@devseed-ui/collecticons'; +import { themeVal } from '@devseed-ui/theme-provider'; import { RIGHT_AXIS_SPACE, AXIS_BG_COLOR } from '../../constants'; import { DatasetTrackMessage } from './dataset-track-message'; import { DataMetric } from './analysis-metrics'; @@ -34,7 +35,7 @@ const ChartAnalysisMenu = styled.div<{axisWidth: number}>` display: flex; justify-content: end; margin-right: calc(${props=> props.axisWidth}px + 0.5rem); - z-index: 3000; + z-index: ${themeVal('zIndices.overlay' as any)}; `; const AxisBackground = styled.div<{axisWidth: number}>` position: absolute; From 20c72fbf6bb594e8063a9ad8d93ee3c66ca81bee Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Wed, 27 Mar 2024 14:41:00 +0900 Subject: [PATCH 5/7] Put back hard-coded value --- app/scripts/components/analysis/define/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/components/analysis/define/index.tsx b/app/scripts/components/analysis/define/index.tsx index cd50aa5c8..6cc845f54 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='2023-12-31' + max='2022-12-31' /> From e66d235c8a8d6384be3e415289fbcc01c3fd3482 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Thu, 28 Mar 2024 14:47:20 +0900 Subject: [PATCH 6/7] Add highlight circles for STD, line --- .../components/datasets/dataset-chart.tsx | 78 +++++++++++++++---- .../components/datasets/dataset-list-item.tsx | 1 - .../components/exploration/constants.ts | 3 +- 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/app/scripts/components/exploration/components/datasets/dataset-chart.tsx b/app/scripts/components/exploration/components/datasets/dataset-chart.tsx index f52e803eb..4c7fd08f1 100644 --- a/app/scripts/components/exploration/components/datasets/dataset-chart.tsx +++ b/app/scripts/components/exploration/components/datasets/dataset-chart.tsx @@ -7,7 +7,7 @@ import { CollecticonChartLine, } from '@devseed-ui/collecticons'; import { themeVal } from '@devseed-ui/theme-provider'; -import { RIGHT_AXIS_SPACE, AXIS_BG_COLOR } from '../../constants'; +import { RIGHT_AXIS_SPACE, AXIS_BG_COLOR, HIGHLIGHT_LINE_COLOR } from '../../constants'; import { DatasetTrackMessage } from './dataset-track-message'; import { DataMetric } from './analysis-metrics'; import LayerChartAnalysisMenu from './layer-chart-analysis-menu'; @@ -31,10 +31,10 @@ interface DatasetChartProps { const ChartAnalysisMenu = styled.div<{axisWidth: number}>` width: inherit; - position: relative; + position: absolute; display: flex; justify-content: end; - margin-right: calc(${props=> props.axisWidth}px + 0.5rem); + right: calc(${props=> props.axisWidth}px + 0.5rem); z-index: ${themeVal('zIndices.overlay' as any)}; `; const AxisBackground = styled.div<{axisWidth: number}>` @@ -85,7 +85,6 @@ export function DatasetChart(props: DatasetChartProps) { const chartAnalysisIconTrigger: JSX.Element = ; - return (
    {!activeMetrics.length && ( @@ -97,11 +96,22 @@ export function DatasetChart(props: DatasetChartProps) { + - + + {activeMetrics.length && highlightDate && ( + + )} + - {/* This is where the line is drawn */} {areaMetrics.map( @@ -124,6 +133,7 @@ export function DatasetChart(props: DatasetChartProps) { prop={areaDataKey} data={enhancedTimeseries} color={theme.color?.[metric.themeColor]} + highlightDate={highlightDate} isVisible={isVisible} /> ) @@ -159,29 +169,30 @@ interface DataAreaProps { data: any[]; color: string; isVisible: boolean; + highlightDate?: Date; } interface DateLineProps extends DataAreaProps { - highlightDate?: Date; style?: any; } -interface DataItem { +type AreaDataItem = { date: Date; - [key: string]: [number, number] | Date; -} +} & { + [K in string]: K extends 'date' ? Date : [number, number]; +}; function DataArea(props: DataAreaProps) { - const { x, y, prop, data, color, isVisible } = props; + const { x, y, prop, data, color, highlightDate, 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)); + const areaGenerator = area() + .defined((d) => !!d[prop]) + .x((d) => x(d.date)) + .y0((d) => y(d[prop][0])) + .y1((d) => y(d[prop][1])); return areaGenerator(data); - }, [x, y, data]); // Ensure all variables used are listed in the dependencies + }, [x, y, data, prop]); // Ensure all variables used are listed in the dependencies const maxOpacity = isVisible ? 1 : 0.25; @@ -198,6 +209,39 @@ function DataArea(props: DataAreaProps) { fillOpacity={0.5} stroke={color} /> + {data.map((d) => { + if (typeof d[prop][0] !== 'number' || typeof d[prop][1] !== 'number' ) return false; + + const highlight = + isVisible && highlightDate?.getTime() === d.date.getTime(); + + return highlight? ( + <> + + + + ): false; + })} ); } 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 e0f884f63..44cbbee14 100644 --- a/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx +++ b/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx @@ -79,7 +79,6 @@ const DatasetHeaderInner = styled.div` const DatasetData = styled.div` position: relative; - padding: ${glsp(0.25, 0)}; display: flex; align-items: center; flex-grow: 1; diff --git a/app/scripts/components/exploration/constants.ts b/app/scripts/components/exploration/constants.ts index 544765f95..72d743e42 100644 --- a/app/scripts/components/exploration/constants.ts +++ b/app/scripts/components/exploration/constants.ts @@ -15,5 +15,6 @@ export const MAX_QUERY_NUM = 300; // @TECH-DEBT As we do not have a new design system that can accommodate the needs for colors // We put colors here for now export const AXIS_BG_COLOR = '#F0F0F0'; +export const HIGHLIGHT_LINE_COLOR = '#CCCCCC'; export const FADED_TEXT_COLOR = '#83868A'; -export const TEXT_TITME_BG_COLOR = '#FAFAFA'; \ No newline at end of file +export const TEXT_TITME_BG_COLOR = '#FAFAFA'; From 0e3a458c2b59e6dfb88ad661478b6b92dc65a364 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Fri, 29 Mar 2024 07:18:47 +0900 Subject: [PATCH 7/7] Chart labels, type fix etc. --- .../exploration/components/chart-popover.tsx | 4 ++-- .../components/datasets/analysis-metrics.tsx | 4 ++-- .../exploration/components/datasets/dataset-chart.tsx | 4 ++-- app/scripts/components/exploration/constants.ts | 2 +- mock/datasets/no2.data.mdx | 10 +++++----- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/scripts/components/exploration/components/chart-popover.tsx b/app/scripts/components/exploration/components/chart-popover.tsx index 99fc26847..2475329c2 100644 --- a/app/scripts/components/exploration/components/chart-popover.tsx +++ b/app/scripts/components/exploration/components/chart-popover.tsx @@ -19,7 +19,7 @@ import { format } from 'date-fns'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; import { AnalysisTimeseriesEntry, TimeDensity, TimelineDatasetSuccess } from '../types.d.ts'; -import { FADED_TEXT_COLOR, TEXT_TITME_BG_COLOR, HEADER_COLUMN_WIDTH } from '../constants'; +import { FADED_TEXT_COLOR, TEXT_TITLE_BG_COLOR, HEADER_COLUMN_WIDTH } from '../constants'; import { DataMetric } from './datasets/analysis-metrics'; import { getNumForChart } from '$components/common/chart/utils'; @@ -80,7 +80,7 @@ const fadedtext = css` `; const TitleBox = styled.div` - background-color: ${TEXT_TITME_BG_COLOR}; + background-color: ${TEXT_TITLE_BG_COLOR}; ${fadedtext}; padding: ${glsp(0.5)}; font-size: 0.75rem; diff --git a/app/scripts/components/exploration/components/datasets/analysis-metrics.tsx b/app/scripts/components/exploration/components/datasets/analysis-metrics.tsx index 130c059aa..b0492815b 100644 --- a/app/scripts/components/exploration/components/datasets/analysis-metrics.tsx +++ b/app/scripts/components/exploration/components/datasets/analysis-metrics.tsx @@ -28,7 +28,7 @@ export const DATA_METRICS: DataMetric[] = [ { id: 'mean', label: 'Average', - chartLabel: 'Avg', + chartLabel: 'Average', themeColor: 'infographicB' }, { @@ -41,7 +41,7 @@ export const DATA_METRICS: DataMetric[] = [ { id: 'std', label: 'St Deviation', - chartLabel: 'STD', + chartLabel: 'St Deviation', themeColor: 'infographicD' }, { diff --git a/app/scripts/components/exploration/components/datasets/dataset-chart.tsx b/app/scripts/components/exploration/components/datasets/dataset-chart.tsx index 4c7fd08f1..830b3797e 100644 --- a/app/scripts/components/exploration/components/datasets/dataset-chart.tsx +++ b/app/scripts/components/exploration/components/datasets/dataset-chart.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import { useTheme } from 'styled-components'; import { extent, scaleLinear, ScaleTime, line, area, ScaleLinear } from 'd3'; -import { AnimatePresence, motion } from 'framer-motion'; +import { SVGMotionProps, AnimatePresence, motion } from 'framer-motion'; import styled from 'styled-components'; import { CollecticonChartLine, @@ -172,7 +172,7 @@ interface DataAreaProps { highlightDate?: Date; } interface DateLineProps extends DataAreaProps { - style?: any; + style?: SVGMotionProps; } type AreaDataItem = { diff --git a/app/scripts/components/exploration/constants.ts b/app/scripts/components/exploration/constants.ts index 72d743e42..cbad6abab 100644 --- a/app/scripts/components/exploration/constants.ts +++ b/app/scripts/components/exploration/constants.ts @@ -17,4 +17,4 @@ export const MAX_QUERY_NUM = 300; export const AXIS_BG_COLOR = '#F0F0F0'; export const HIGHLIGHT_LINE_COLOR = '#CCCCCC'; export const FADED_TEXT_COLOR = '#83868A'; -export const TEXT_TITME_BG_COLOR = '#FAFAFA'; +export const TEXT_TITLE_BG_COLOR = '#FAFAFA'; diff --git a/mock/datasets/no2.data.mdx b/mock/datasets/no2.data.mdx index 05710378e..c68f9478a 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