From cfe26f7cc52b0d4a37b350d392de3b59fd63fcf6 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Fri, 8 Sep 2023 16:34:44 +0100 Subject: [PATCH 1/3] Lump dataset blocks together when zooming out --- .../components/datasets/block-utils.ts | 81 +++++++++++++++++ .../components/datasets/dataset-list-item.tsx | 91 +++++++++---------- .../components/exploration/datasets-mock.tsx | 1 + .../exploration/hooks/scales-hooks.ts | 3 +- .../hooks/use-stac-metadata-datasets.ts | 53 ++++++----- 5 files changed, 158 insertions(+), 71 deletions(-) create mode 100644 app/scripts/components/exploration/components/datasets/block-utils.ts diff --git a/app/scripts/components/exploration/components/datasets/block-utils.ts b/app/scripts/components/exploration/components/datasets/block-utils.ts new file mode 100644 index 000000000..7325ad9e4 --- /dev/null +++ b/app/scripts/components/exploration/components/datasets/block-utils.ts @@ -0,0 +1,81 @@ +import { + endOfDay, + endOfMonth, + endOfYear, + startOfDay, + startOfMonth, + startOfYear, +} from 'date-fns'; +import { TimeDensity } from '$components/exploration/types.d.ts'; + +/** + * Calculate the start and end of a block of time, given a date and a time + * density + * @param date Starting date + * @param timeDensity Dataset time density + * + * @returns Array of two dates, the first being the start of the block and the + * second being the end of the block. + */ +export function getBlockBoundaries(date: Date, timeDensity: TimeDensity) { + switch (timeDensity) { + case TimeDensity.MONTH: + return [startOfMonth(date), endOfMonth(date)]; + case TimeDensity.YEAR: + return [startOfYear(date), endOfYear(date)]; + } + + return [startOfDay(date), endOfDay(date)]; +} + +/** + * Lumps blocks of time together if they are too small to be seen on the chart. + * + * @param params.domain The dataset domain from which to calculate the blocks + * @param params.xScaled The xScaled function from the chart + * @param params.timeDensity The dataset time density + * @param params.minBlockSize The minimum size of a block + * + * @returns The blocks and whether or not any lumping happened. + */ +export function lumpBlocks({ domain, xScaled, timeDensity, minBlockSize = 4 }) { + // How big would a block be? + const [start, end] = getBlockBoundaries(domain[0], timeDensity); + + const blockWidth = xScaled(end) - xScaled(start); + + if (blockWidth >= minBlockSize) { + return { + blocks: domain.map((d) => getBlockBoundaries(d, timeDensity)), + wasLumped: false + }; + } + + let blocks: Date[][] = []; + let startBoundary = start; + let endBoundary = end; + + for (let i = 0; i < domain.length; i++) { + if (i === domain.length - 1) { + blocks = [...blocks, [startBoundary, endBoundary]]; + break; + } + + const nextDate = domain[i + 1]; + const [startNext, endNext] = getBlockBoundaries(nextDate, timeDensity); + + // Distance between the end of the current block and the start of the next. + const distance = xScaled(startNext) - xScaled(endBoundary); + + if (distance < minBlockSize / 2) { + endBoundary = endNext; + continue; + } + + blocks = [...blocks, [startBoundary, endBoundary]]; + startBoundary = startNext; + endBoundary = endNext; + } + + return { blocks, wasLumped: true }; +} \ No newline at end of file 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 a5805e264..bd8cb4135 100644 --- a/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx +++ b/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx @@ -2,17 +2,7 @@ import React, { useCallback, useMemo } from 'react'; import { useAtomValue } from 'jotai'; import { Reorder, useDragControls } from 'framer-motion'; import styled, { useTheme } from 'styled-components'; -import { - addDays, - subDays, - endOfDay, - endOfMonth, - endOfYear, - startOfDay, - startOfMonth, - startOfYear, - areIntervalsOverlapping -} from 'date-fns'; +import { addDays, subDays, areIntervalsOverlapping } from 'date-fns'; import { useQueryClient } from '@tanstack/react-query'; import { ScaleTime } from 'd3'; import { @@ -35,13 +25,13 @@ import { } from './dataset-list-item-status'; import { DatasetChart } from './dataset-chart'; import DatasetOptions from './dataset-options'; +import { getBlockBoundaries, lumpBlocks } from './block-utils'; import { LayerCategoricalGraphic, LayerGradientGraphic } from '$components/common/mapbox/layer-legend'; import { - TimeDensity, TimelineDatasetStatus, TimelineDatasetSuccess } from '$components/exploration/types.d.ts'; @@ -59,17 +49,6 @@ import { isAnalysisAtom } from '$components/exploration/atoms/atoms'; -function getBlockBoundaries(date: Date, timeDensity: TimeDensity) { - switch (timeDensity) { - case TimeDensity.MONTH: - return [startOfMonth(date), endOfMonth(date)]; - case TimeDensity.YEAR: - return [startOfYear(date), endOfYear(date)]; - } - - return [startOfDay(date), endOfDay(date)]; -} - const DatasetItem = styled.article` width: 100%; display: flex; @@ -139,6 +118,11 @@ const DatasetData = styled.div` flex-grow: 1; `; +const DatasetGLabel = styled.text` + font-weight: ${themeVal('type.base.bold')}; + font-size: 0.75rem; +`; + interface DatasetListItemProps { datasetId: string; width: number; @@ -367,16 +351,32 @@ function DatasetTrack(props: DatasetTrackProps) { }); }, [xScaled, dataset]); + const { blocks, wasLumped } = lumpBlocks({ + domain: domainToRender, + xScaled, + timeDensity: dataset.data.timeDensity + }); + return ( - {domainToRender.map((date) => ( - + {blocks.map(([blockStart, blockEnd]) => ( + + + {wasLumped && ( + + G + + )} + ))} ); @@ -384,34 +384,31 @@ function DatasetTrack(props: DatasetTrackProps) { interface DatasetTrackBlockProps { xScaled: ScaleTime; - date: Date; - dataset: TimelineDatasetSuccess; + startDate: Date; + endDate: Date; isVisible: boolean; } function DatasetTrackBlock(props: DatasetTrackBlockProps) { - const { xScaled, date, dataset, isVisible } = props; + const { xScaled, startDate, endDate, isVisible } = props; const theme = useTheme(); - const [start, end] = getBlockBoundaries(date, dataset.data.timeDensity); - const s = xScaled(start); - const e = xScaled(end); + const s = xScaled(startDate); + const e = xScaled(endDate); const fill = isVisible ? theme.color?.['base-400'] : theme.color?.['base-200']; return ( - - - + ); } diff --git a/app/scripts/components/exploration/datasets-mock.tsx b/app/scripts/components/exploration/datasets-mock.tsx index b8f28a53c..11b096f3c 100644 --- a/app/scripts/components/exploration/datasets-mock.tsx +++ b/app/scripts/components/exploration/datasets-mock.tsx @@ -373,6 +373,7 @@ function makeDataset( analysis = makeAnalysis({}, {}) ) { return { + mocked: true, status, data, error: status === TimelineDatasetStatus.ERROR ? new Error('Mock error') : null, diff --git a/app/scripts/components/exploration/hooks/scales-hooks.ts b/app/scripts/components/exploration/hooks/scales-hooks.ts index 164da9643..c004ccbc6 100644 --- a/app/scripts/components/exploration/hooks/scales-hooks.ts +++ b/app/scripts/components/exploration/hooks/scales-hooks.ts @@ -25,7 +25,8 @@ export function useScaleFactors() { const domainDays = differenceInCalendarDays(dataDomain[1], dataDomain[0]); return { - k0: Math.max(1, DAY_SIZE_MIN / (contentWidth / domainDays)), + // k0: Math.max(1, DAY_SIZE_MIN / (contentWidth / domainDays)), + k0: 1, k1: DAY_SIZE_MAX / (contentWidth / domainDays) }; }, [contentWidth, dataDomain]); diff --git a/app/scripts/components/exploration/hooks/use-stac-metadata-datasets.ts b/app/scripts/components/exploration/hooks/use-stac-metadata-datasets.ts index 8e038e0ca..2d5ffa008 100644 --- a/app/scripts/components/exploration/hooks/use-stac-metadata-datasets.ts +++ b/app/scripts/components/exploration/hooks/use-stac-metadata-datasets.ts @@ -135,7 +135,9 @@ export function useStacMetadataOnDatasets() { const [datasets, setDatasets] = useAtom(timelineDatasetsAtom); const datasetsQueryData = useQueries({ - queries: datasets.map((dataset) => makeQueryObject(dataset)) + queries: datasets + .filter((d) => !(d as any).mocked) + .map((dataset) => makeQueryObject(dataset)) }); useEffectPrevious<[typeof datasetsQueryData, TimelineDataset[]]>( @@ -143,28 +145,33 @@ export function useStacMetadataOnDatasets() { const prevQueryData = prev[0]; if (!prevQueryData) return; - const { changed, data: updatedDatasets } = datasets.reduce<{ - changed: boolean; - data: TimelineDataset[]; - }>( - (acc, dataset, idx) => { - const curr = datasetsQueryData[idx]; - - if (didDataChange(curr, prevQueryData[idx])) { - // Changed - return { - changed: true, - data: [...acc.data, reconcileQueryDataWithDataset(curr, dataset)] - }; - } else { - return { - ...acc, - data: [...acc.data, dataset] - }; - } - }, - { changed: false, data: [] } - ); + const { changed, data: updatedDatasets } = datasets + .filter((d) => !(d as any).mocked) + .reduce<{ + changed: boolean; + data: TimelineDataset[]; + }>( + (acc, dataset, idx) => { + const curr = datasetsQueryData[idx]; + + if (didDataChange(curr, prevQueryData[idx])) { + // Changed + return { + changed: true, + data: [ + ...acc.data, + reconcileQueryDataWithDataset(curr, dataset) + ] + }; + } else { + return { + ...acc, + data: [...acc.data, dataset] + }; + } + }, + { changed: false, data: [] } + ); if (changed as boolean) { setDatasets(updatedDatasets); From 3177feb9cf9f14424b8a7c8a1427241d3db072dc Mon Sep 17 00:00:00 2001 From: Erik Escoffier Date: Thu, 14 Sep 2023 09:46:16 +0200 Subject: [PATCH 2/3] Fixed linting error --- app/scripts/components/exploration/hooks/scales-hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/components/exploration/hooks/scales-hooks.ts b/app/scripts/components/exploration/hooks/scales-hooks.ts index c004ccbc6..012b34d0d 100644 --- a/app/scripts/components/exploration/hooks/scales-hooks.ts +++ b/app/scripts/components/exploration/hooks/scales-hooks.ts @@ -4,7 +4,7 @@ import { useAtomValue } from 'jotai'; import { differenceInCalendarDays } from 'date-fns'; import { timelineSizesAtom, zoomTransformAtom } from '../atoms/atoms'; -import { DAY_SIZE_MAX, DAY_SIZE_MIN } from '../constants'; +import { DAY_SIZE_MAX } from '../constants'; import { useTimelineDatasetsDomain } from '../atoms/hooks'; import { rescaleX } from '../components/timeline/timeline-utils'; From 7a1b27baba0323fea9a775eb2dfb947e74bb2186 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Mon, 18 Sep 2023 09:48:53 +0100 Subject: [PATCH 3/3] Improve dataset track stacked look --- .../components/datasets/dataset-list-item.tsx | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) 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 bd8cb4135..690764527 100644 --- a/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx +++ b/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx @@ -118,11 +118,6 @@ const DatasetData = styled.div` flex-grow: 1; `; -const DatasetGLabel = styled.text` - font-weight: ${themeVal('type.base.bold')}; - font-size: 0.75rem; -`; - interface DatasetListItemProps { datasetId: string; width: number; @@ -358,25 +353,16 @@ function DatasetTrack(props: DatasetTrackProps) { }); return ( - + {blocks.map(([blockStart, blockEnd]) => ( - - - {wasLumped && ( - - G - - )} - + ))} ); @@ -387,28 +373,44 @@ interface DatasetTrackBlockProps { startDate: Date; endDate: Date; isVisible: boolean; + isGroup: boolean; } function DatasetTrackBlock(props: DatasetTrackBlockProps) { - const { xScaled, startDate, endDate, isVisible } = props; + const { xScaled, startDate, endDate, isVisible, isGroup } = props; const theme = useTheme(); - const s = xScaled(startDate); - const e = xScaled(endDate); + const xStart = xScaled(startDate); + const xEnd = xScaled(endDate); const fill = isVisible ? theme.color?.['base-400'] : theme.color?.['base-200']; return ( - + + {isGroup && ( + + )} + + ); }