diff --git a/CHANGELOG.md b/CHANGELOG.md index 46f5dcbd6..8e87a7512 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,8 @@ You can also check the - Renaming of charts through user profile now works correctly - Manually entering dates in date pickers works correctly again - Improved scrolling behavior in the chart tabs + - Shared time range filter now correctly positions the marks in the time + slider - Style - Aligned editor and layouting page layouts - Removed dimension selection from modal when merging cubes diff --git a/app/charts/shared/use-combined-temporal-dimension.ts b/app/charts/shared/use-combined-temporal-dimension.ts new file mode 100644 index 000000000..d499a9c36 --- /dev/null +++ b/app/charts/shared/use-combined-temporal-dimension.ts @@ -0,0 +1,88 @@ +import { t } from "@lingui/macro"; +import { ascending, descending } from "d3-array"; +import uniqBy from "lodash/uniqBy"; +import { useMemo } from "react"; + +import { + hasChartConfigs, + useConfiguratorState, +} from "@/configurator/configurator-state"; +import { + isTemporalDimensionWithTimeUnit, + TemporalDimension, + TemporalEntityDimension, +} from "@/domain/data"; +import { useTimeFormatLocale } from "@/formatters"; +import { useConfigsCubeComponents } from "@/graphql/hooks"; +import { TimeUnit } from "@/graphql/query-hooks"; +import { useLocale } from "@/locales/use-locale"; +import { timeUnitFormats, timeUnitOrder } from "@/rdf/mappings"; +import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; + +/** Hook to get combined temporal dimension. Useful for shared dashboard filters. */ +export const useCombinedTemporalDimension = () => { + const locale = useLocale(); + const formatLocale = useTimeFormatLocale(); + const [state] = useConfiguratorState(hasChartConfigs); + const { potentialTimeRangeFilterIris } = useDashboardInteractiveFilters(); + const [{ data }] = useConfigsCubeComponents({ + variables: { + state, + locale, + }, + }); + return useMemo(() => { + const timeUnitDimensions = ( + data?.dataCubesComponents.dimensions ?? [] + ).filter( + (dimension) => + isTemporalDimensionWithTimeUnit(dimension) && + potentialTimeRangeFilterIris.includes(dimension.iri) + ) as (TemporalDimension | TemporalEntityDimension)[]; + // We want to use lowest time unit for combined dimension filtering, + // so in case we have year and day, we'd filter both by day + const timeUnit = timeUnitDimensions.sort((a, b) => + descending( + timeUnitOrder.get(a.timeUnit) ?? 0, + timeUnitOrder.get(b.timeUnit) ?? 0 + ) + )[0]?.timeUnit as TimeUnit; + const timeFormat = timeUnitFormats.get(timeUnit) as string; + const values = timeUnitDimensions.flatMap((dimension) => { + const formatDate = formatLocale.format(timeFormat); + const parseDate = formatLocale.parse(dimension.timeFormat); + // Standardize values to have same date format + return dimension.values.map((dimensionValue) => { + const date = parseDate(`${dimensionValue.value}`) as Date; + const dateString = formatDate(date); + return { + ...dimensionValue, + value: dateString, + label: dateString, + }; + }); + }); + const combinedDimension: TemporalDimension = { + __typename: "TemporalDimension", + cubeIri: "all", + iri: "combined-date-filter", + label: t({ + id: "controls.section.shared-filters.date", + message: "Date", + }), + isKeyDimension: true, + isNumerical: false, + values: uniqBy(values, "value").sort((a, b) => + ascending(a.value, b.value) + ), + timeUnit, + timeFormat, + }; + + return combinedDimension; + }, [ + data?.dataCubesComponents.dimensions, + formatLocale, + potentialTimeRangeFilterIris, + ]); +}; diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index dd1afabcd..cd4e503db 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -16,6 +16,7 @@ import { DataFilterHierarchyDimension, DataFilterTemporalDimension, } from "@/charts/shared/chart-data-filters"; +import { useCombinedTemporalDimension } from "@/charts/shared/use-combined-temporal-dimension"; import { ChartConfig, DashboardTimeRangeFilter, @@ -25,6 +26,7 @@ import { useConfiguratorState, } from "@/configurator"; import { + parseDate, timeUnitToFormatter, timeUnitToParser, } from "@/configurator/components/ui-helpers"; @@ -140,26 +142,11 @@ const DashboardTimeRangeSlider = ({ ); const timeUnit = filter.timeUnit as TimeUnit; - - // In Unix timestamp const [timeRange, setTimeRange] = useState(() => // timeUnit can still be an empty string timeUnit ? presetToTimeRange(presets, timeUnit) : undefined ); - const { min, max } = useMemo(() => { - if (!timeUnit) { - return { min: 0, max: 0 }; - } - const parser = timeUnitToParser[timeUnit]; - return { - min: toUnixSeconds(parser(presets.from)), - max: toUnixSeconds(parser(presets.to)), - }; - }, [timeUnit, presets]); - - const step = stepFromTimeUnit(timeUnit); - const valueLabelFormat = useEventCallback((value: number) => { if (!timeUnit) { return ""; @@ -211,8 +198,23 @@ const DashboardTimeRangeSlider = ({ }, [presets.from, presets.to, timeUnit]); const mountedForSomeTime = useTimeout(500, mounted); + const combinedTemporalDimension = useCombinedTemporalDimension(); + const sliderRange = useMemo(() => { + const { values } = combinedTemporalDimension; + const min = values[0]?.value; + const max = values[values.length - 1]?.value; - if (!timeRange || !filter.active) { + if (!min || !max) { + return; + } + + return [ + toUnixSeconds(parseDate(min as string)), + toUnixSeconds(parseDate(max as string)), + ]; + }, [combinedTemporalDimension]); + + if (!timeRange || !filter.active || !sliderRange) { return null; } @@ -221,40 +223,19 @@ const DashboardTimeRangeSlider = ({ className={classes.slider} onChange={(_ev, value) => handleChangeSlider(value)} onChangeCommitted={() => setEnableTransition(true)} - min={min} - max={max} valueLabelFormat={valueLabelFormat} - step={step} + step={null} + min={sliderRange[0]} + max={sliderRange[1]} valueLabelDisplay={mountedForSomeTime ? "on" : "off"} value={timeRange} - marks={(max - min) / (step ?? 1) < 50} + marks={combinedTemporalDimension.values.map(({ value }) => ({ + value: toUnixSeconds(parseDate(value as string)), + }))} /> ); }; -function stepFromTimeUnit(timeUnit: TimeUnit | undefined) { - if (!timeUnit) { - return 0; - } - - switch (timeUnit) { - case "Year": - return 1 * 60 * 60 * 24 * 365; - case "Month": - return 1 * 60 * 60 * 24 * 30; - case "Week": - return 1 * 60 * 60 * 24 * 7; - case "Day": - return 1 * 60 * 60 * 24; - case "Hour": - return 1 * 60 * 60; - case "Minute": - return 1 * 60; - case "Second": - return 1; - } -} - const useDataFilterStyles = makeStyles((theme: Theme) => ({ wrapper: { display: "grid", diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index 8729a87c6..c22d8722b 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -7,13 +7,12 @@ import { Typography, useEventCallback, } from "@mui/material"; -import { ascending, descending } from "d3-array"; import capitalize from "lodash/capitalize"; import omit from "lodash/omit"; -import uniqBy from "lodash/uniqBy"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { DataFilterGenericDimensionProps } from "@/charts/shared/chart-data-filters"; +import { useCombinedTemporalDimension } from "@/charts/shared/use-combined-temporal-dimension"; import { Select } from "@/components/form"; import { generateLayout } from "@/components/react-grid"; import { @@ -46,15 +45,12 @@ import { import { canDimensionBeTimeFiltered, Dimension, - isTemporalDimensionWithTimeUnit, TemporalDimension, TemporalEntityDimension, } from "@/domain/data"; import { useFlag } from "@/flags"; import { useTimeFormatLocale, useTimeFormatUnit } from "@/formatters"; import { useConfigsCubeComponents } from "@/graphql/hooks"; -import { TimeUnit } from "@/graphql/resolver-types"; -import { timeUnitFormats, timeUnitOrder } from "@/rdf/mappings"; import { useLocale } from "@/src"; import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; import { getTimeFilterOptions } from "@/utils/time-filter-options"; @@ -122,7 +118,7 @@ const LayoutSharedFiltersConfigurator = () => { const [{ data, fetching }] = useConfigsCubeComponents({ variables: { state, - locale: locale, + locale, }, }); const dimensions = useMemo( @@ -133,68 +129,21 @@ const LayoutSharedFiltersConfigurator = () => { const formatLocale = useTimeFormatLocale(); const timeFormatUnit = useTimeFormatUnit(); - const combinedDimension = useMemo(() => { - const timeUnitDimensions = dimensions.filter( - (dimension) => - isTemporalDimensionWithTimeUnit(dimension) && - potentialTimeRangeFilterIris.includes(dimension.iri) - ) as (TemporalDimension | TemporalEntityDimension)[]; - // We want to use lowest time unit for combined dimension filtering, - // so in case we have year and day, we'd filter both by day - const timeUnit = timeUnitDimensions.sort((a, b) => - descending( - timeUnitOrder.get(a.timeUnit) ?? 0, - timeUnitOrder.get(b.timeUnit) ?? 0 - ) - )[0]?.timeUnit as TimeUnit; - const timeFormat = timeUnitFormats.get(timeUnit) as string; - const values = timeUnitDimensions.flatMap((dimension) => { - const formatDate = formatLocale.format(timeFormat); - const parseDate = formatLocale.parse(dimension.timeFormat); - // Standardize values to have same date format - return dimension.values.map((dimensionValue) => { - const value = formatDate( - parseDate(dimensionValue.value as string) as Date - ); - return { - ...dimensionValue, - value, - label: value, - }; - }); - }); - const combinedDimension: TemporalDimension = { - __typename: "TemporalDimension", - cubeIri: "all", - iri: "combined-date-filter", - label: t({ - id: "controls.section.shared-filters.date", - message: "Date", - }), - isKeyDimension: true, - isNumerical: false, - values: uniqBy(values, "value").sort((a, b) => - ascending(a.value, b.value) - ), - timeUnit, - timeFormat, - }; - - return combinedDimension; - }, [dimensions, formatLocale, potentialTimeRangeFilterIris]); + const combinedTemporalDimension = useCombinedTemporalDimension(); const handleTimeRangeFilterToggle: SwitchProps["onChange"] = useEventCallback( (_, checked) => { if (checked) { const options = getTimeFilterOptions({ - dimension: combinedDimension, + dimension: combinedTemporalDimension, formatLocale, timeFormatUnit, }); const from = options.sortedOptions[0]?.date; const to = options.sortedOptions.at(-1)?.date; - const formatDate = timeUnitToFormatter[combinedDimension.timeUnit]; + const formatDate = + timeUnitToFormatter[combinedTemporalDimension.timeUnit]; if (!from || !to) { return; @@ -204,7 +153,7 @@ const LayoutSharedFiltersConfigurator = () => { type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", value: { active: true, - timeUnit: combinedDimension.timeUnit, + timeUnit: combinedTemporalDimension.timeUnit, presets: { from: formatDate(from), to: formatDate(to), @@ -295,7 +244,7 @@ const LayoutSharedFiltersConfigurator = () => { {/* TODO: allow TemporalOrdinalDimensions to work here */} - {timeRange && combinedDimension.values.length ? ( + {timeRange && combinedTemporalDimension.values.length ? ( <> { }} > - {combinedDimension.label} + {combinedTemporalDimension.label} { {timeRange.active ? ( ) : null}