diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c8d399f4..1efd280b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,10 @@ You can also check the [release page](https://github.com/visualize-admin/visuali # Unreleased +- Features + - Added TemporalEntityDimension support for global dashboard filters - Fixes + - Fixed using a time range brush in column charts when X dimension is a TemporalEntityDimension - Whiskers should now display correctly at the initial render # [4.6.1] - 2024-06-05 diff --git a/app/charts/area/areas-state-props.ts b/app/charts/area/areas-state-props.ts index 7f8308fd8..4dba98b97 100644 --- a/app/charts/area/areas-state-props.ts +++ b/app/charts/area/areas-state-props.ts @@ -6,12 +6,14 @@ import { usePlottableData } from "@/charts/shared/chart-helpers"; import { BaseVariables, ChartStateData, + InteractiveFiltersVariables, NumericalYVariables, SegmentVariables, SortingVariables, TemporalXVariables, useBaseVariables, useChartData, + useInteractiveFiltersVariables, useNumericalYVariables, useSegmentVariables, useTemporalXVariables, @@ -22,7 +24,8 @@ export type AreasStateVariables = BaseVariables & SortingVariables & TemporalXVariables & NumericalYVariables & - SegmentVariables; + SegmentVariables & + InteractiveFiltersVariables; export const useAreasStateVariables = ( props: ChartProps @@ -42,6 +45,10 @@ export const useAreasStateVariables = ( dimensionsByIri, observations, }); + const interactiveFiltersVariables = useInteractiveFiltersVariables( + chartConfig.interactiveFiltersConfig, + { dimensionsByIri } + ); const { getX } = temporalXVariables; const sortData: AreasStateVariables["sortData"] = useCallback( @@ -59,6 +66,7 @@ export const useAreasStateVariables = ( ...temporalXVariables, ...numericalYVariables, ...segmentVariables, + ...interactiveFiltersVariables, }; }; @@ -67,7 +75,13 @@ export const useAreasStateData = ( variables: AreasStateVariables ): ChartStateData => { const { chartConfig, observations } = chartProps; - const { sortData, getX, getY, getSegmentAbbreviationOrLabel } = variables; + const { + sortData, + getX, + getY, + getSegmentAbbreviationOrLabel, + getTimeRangeDate, + } = variables; const plottableData = usePlottableData(observations, { getX, getY, @@ -79,6 +93,7 @@ export const useAreasStateData = ( chartConfig, getXAsDate: getX, getSegmentAbbreviationOrLabel, + getTimeRangeDate, }); return { diff --git a/app/charts/column/columns-grouped-state-props.ts b/app/charts/column/columns-grouped-state-props.ts index 2c5264a1f..79160b425 100644 --- a/app/charts/column/columns-grouped-state-props.ts +++ b/app/charts/column/columns-grouped-state-props.ts @@ -7,6 +7,7 @@ import { BandXVariables, BaseVariables, ChartStateData, + InteractiveFiltersVariables, NumericalYErrorVariables, NumericalYVariables, RenderingVariables, @@ -15,12 +16,14 @@ import { useBandXVariables, useBaseVariables, useChartData, + useInteractiveFiltersVariables, useNumericalYErrorVariables, useNumericalYVariables, useSegmentVariables, } from "@/charts/shared/chart-state"; import { useRenderingKeyVariable } from "@/charts/shared/rendering-utils"; import { ColumnConfig, useChartConfigFilters } from "@/configurator"; +import { Observation, isTemporalEntityDimension } from "@/domain/data"; import { sortByIndex } from "@/utils/array"; import { ChartProps } from "../shared/ChartProps"; @@ -31,7 +34,8 @@ export type ColumnsGroupedStateVariables = BaseVariables & NumericalYVariables & NumericalYErrorVariables & SegmentVariables & - RenderingVariables; + RenderingVariables & + InteractiveFiltersVariables; export const useColumnsGroupedStateVariables = ( props: ChartProps @@ -46,6 +50,7 @@ export const useColumnsGroupedStateVariables = ( } = props; const { fields, interactiveFiltersConfig } = chartConfig; const { x, y, segment, animation } = fields; + const xDimension = dimensionsByIri[x.componentIri]; const filters = useChartConfigFilters(chartConfig); const baseVariables = useBaseVariables(chartConfig); @@ -65,31 +70,38 @@ export const useColumnsGroupedStateVariables = ( dimensionsByIri, observations, }); + const interactiveFiltersVariables = useInteractiveFiltersVariables( + interactiveFiltersConfig, + { dimensionsByIri } + ); - const { getX } = bandXVariables; + const { getX, getXAsDate } = bandXVariables; const { getY } = numericalYVariables; const sortData: ColumnsGroupedStateVariables["sortData"] = useCallback( (data) => { const { sortingOrder, sortingType } = x.sorting ?? {}; + const xGetter = isTemporalEntityDimension(xDimension) + ? (d: Observation) => getXAsDate(d).getTime().toString() + : getX; const order = [ ...rollup( data, (v) => sum(v, (d) => getY(d)), - (d) => getX(d) + (d) => xGetter(d) ), ] .sort((a, b) => ascending(a[1], b[1])) .map((d) => d[0]); if (sortingType === "byDimensionLabel") { - return orderBy(data, getX, sortingOrder); + return orderBy(data, xGetter, sortingOrder); } else if (sortingType === "byMeasure") { - return sortByIndex({ data, order, getCategory: getX, sortingOrder }); + return sortByIndex({ data, order, getCategory: xGetter, sortingOrder }); } else { - return orderBy(data, getX, "asc"); + return orderBy(data, xGetter, "asc"); } }, - [getX, getY, x.sorting] + [getX, getXAsDate, getY, x.sorting, xDimension] ); const getRenderingKey = useRenderingKeyVariable( @@ -106,6 +118,7 @@ export const useColumnsGroupedStateVariables = ( ...numericalYVariables, ...numericalYErrorVariables, ...segmentVariables, + ...interactiveFiltersVariables, getRenderingKey, }; }; @@ -115,8 +128,13 @@ export const useColumnsGroupedStateData = ( variables: ColumnsGroupedStateVariables ): ChartStateData => { const { chartConfig, observations } = chartProps; - const { sortData, getXAsDate, getY, getSegmentAbbreviationOrLabel } = - variables; + const { + sortData, + getXAsDate, + getY, + getSegmentAbbreviationOrLabel, + getTimeRangeDate, + } = variables; const plottableData = usePlottableData(observations, { getY, }); @@ -127,6 +145,7 @@ export const useColumnsGroupedStateData = ( chartConfig, getXAsDate, getSegmentAbbreviationOrLabel, + getTimeRangeDate, }); return { diff --git a/app/charts/column/columns-stacked-state-props.ts b/app/charts/column/columns-stacked-state-props.ts index 20b6b5b86..48d2adb6f 100644 --- a/app/charts/column/columns-stacked-state-props.ts +++ b/app/charts/column/columns-stacked-state-props.ts @@ -6,6 +6,7 @@ import { BandXVariables, BaseVariables, ChartStateData, + InteractiveFiltersVariables, NumericalYVariables, RenderingVariables, SegmentVariables, @@ -13,12 +14,13 @@ import { useBandXVariables, useBaseVariables, useChartData, + useInteractiveFiltersVariables, useNumericalYVariables, useSegmentVariables, } from "@/charts/shared/chart-state"; import { useRenderingKeyVariable } from "@/charts/shared/rendering-utils"; import { ColumnConfig, useChartConfigFilters } from "@/configurator"; -import { Observation } from "@/domain/data"; +import { Observation, isTemporalEntityDimension } from "@/domain/data"; import { sortByIndex } from "@/utils/array"; import { ChartProps } from "../shared/ChartProps"; @@ -28,7 +30,8 @@ export type ColumnsStackedStateVariables = BaseVariables & BandXVariables & NumericalYVariables & SegmentVariables & - RenderingVariables; + RenderingVariables & + InteractiveFiltersVariables; export const useColumnsStackedStateVariables = ( props: ChartProps @@ -42,6 +45,7 @@ export const useColumnsStackedStateVariables = ( } = props; const { fields, interactiveFiltersConfig } = chartConfig; const { x, y, segment, animation } = fields; + const xDimension = dimensionsByIri[x.componentIri]; const filters = useChartConfigFilters(chartConfig); const baseVariables = useBaseVariables(chartConfig); @@ -56,31 +60,38 @@ export const useColumnsStackedStateVariables = ( dimensionsByIri, observations, }); + const interactiveFiltersVariables = useInteractiveFiltersVariables( + interactiveFiltersConfig, + { dimensionsByIri } + ); - const { getX } = bandXVariables; + const { getX, getXAsDate } = bandXVariables; const sortData: ColumnsStackedStateVariables["sortData"] = useCallback( (data, { plottableDataWide }) => { const { sortingOrder, sortingType } = x.sorting ?? {}; + const xGetter = isTemporalEntityDimension(xDimension) + ? (d: Observation) => getXAsDate(d).getTime().toString() + : getX; const xOrder = plottableDataWide .sort((a, b) => ascending(a.total ?? undefined, b.total ?? undefined)) - .map(getX); + .map(xGetter); if (sortingOrder === "desc" && sortingType === "byDimensionLabel") { - return [...data].sort((a, b) => descending(getX(a), getX(b))); + return [...data].sort((a, b) => descending(xGetter(a), xGetter(b))); } else if (sortingOrder === "asc" && sortingType === "byDimensionLabel") { - return [...data].sort((a, b) => ascending(getX(a), getX(b))); + return [...data].sort((a, b) => ascending(xGetter(a), xGetter(b))); } else if (sortingType === "byMeasure") { return sortByIndex({ data, order: xOrder, - getCategory: getX, + getCategory: xGetter, sortingOrder, }); } else { - return [...data].sort((a, b) => ascending(getX(a), getX(b))); + return [...data].sort((a, b) => ascending(xGetter(a), xGetter(b))); } }, - [getX, x.sorting] + [getX, getXAsDate, x.sorting, xDimension] ); const getRenderingKey = useRenderingKeyVariable( @@ -96,6 +107,7 @@ export const useColumnsStackedStateVariables = ( ...bandXVariables, ...numericalYVariables, ...segmentVariables, + ...interactiveFiltersVariables, getRenderingKey, }; }; @@ -118,6 +130,7 @@ export const useColumnsStackedStateData = ( getY, getSegment, getSegmentAbbreviationOrLabel, + getTimeRangeDate, } = variables; const plottableData = usePlottableData(observations, { getY, @@ -142,6 +155,7 @@ export const useColumnsStackedStateData = ( chartConfig, getXAsDate, getSegmentAbbreviationOrLabel, + getTimeRangeDate, }); return { diff --git a/app/charts/column/columns-state-props.ts b/app/charts/column/columns-state-props.ts index 0ed544c59..a2dd15b28 100644 --- a/app/charts/column/columns-state-props.ts +++ b/app/charts/column/columns-state-props.ts @@ -6,6 +6,7 @@ import { BandXVariables, BaseVariables, ChartStateData, + InteractiveFiltersVariables, NumericalYErrorVariables, NumericalYVariables, RenderingVariables, @@ -13,11 +14,13 @@ import { useBandXVariables, useBaseVariables, useChartData, + useInteractiveFiltersVariables, useNumericalYErrorVariables, useNumericalYVariables, } from "@/charts/shared/chart-state"; import { useRenderingKeyVariable } from "@/charts/shared/rendering-utils"; import { ColumnConfig, useChartConfigFilters } from "@/configurator"; +import { isTemporalEntityDimension } from "@/domain/data"; import { ChartProps } from "../shared/ChartProps"; @@ -26,7 +29,8 @@ export type ColumnsStateVariables = BaseVariables & BandXVariables & NumericalYVariables & NumericalYErrorVariables & - RenderingVariables; + RenderingVariables & + InteractiveFiltersVariables; export const useColumnsStateVariables = ( props: ChartProps @@ -41,6 +45,7 @@ export const useColumnsStateVariables = ( } = props; const { fields, interactiveFiltersConfig } = chartConfig; const { x, y, animation } = fields; + const xDimension = dimensionsByIri[x.componentIri]; const filters = useChartConfigFilters(chartConfig); const baseVariables = useBaseVariables(chartConfig); @@ -56,16 +61,21 @@ export const useColumnsStateVariables = ( dimensions, measures, }); + const interactiveFiltersVariables = useInteractiveFiltersVariables( + interactiveFiltersConfig, + { dimensionsByIri } + ); - const { getX } = bandXVariables; + const { getX, getXAsDate } = bandXVariables; const { getY } = numericalYVariables; const sortData: ColumnsStateVariables["sortData"] = useCallback( (data) => { const { sortingOrder, sortingType } = x.sorting ?? {}; + const xGetter = isTemporalEntityDimension(xDimension) ? getXAsDate : getX; if (sortingOrder === "desc" && sortingType === "byDimensionLabel") { - return [...data].sort((a, b) => descending(getX(a), getX(b))); + return [...data].sort((a, b) => descending(xGetter(a), xGetter(b))); } else if (sortingOrder === "asc" && sortingType === "byDimensionLabel") { - return [...data].sort((a, b) => ascending(getX(a), getX(b))); + return [...data].sort((a, b) => ascending(xGetter(a), xGetter(b))); } else if (sortingOrder === "desc" && sortingType === "byMeasure") { return [...data].sort((a, b) => descending(getY(a) ?? -1, getY(b) ?? -1) @@ -75,10 +85,10 @@ export const useColumnsStateVariables = ( ascending(getY(a) ?? -1, getY(b) ?? -1) ); } else { - return [...data].sort((a, b) => ascending(getX(a), getX(b))); + return [...data].sort((a, b) => ascending(xGetter(a), xGetter(b))); } }, - [getX, getY, x.sorting] + [getX, getXAsDate, getY, x.sorting, xDimension] ); const getRenderingKey = useRenderingKeyVariable( @@ -94,6 +104,7 @@ export const useColumnsStateVariables = ( ...bandXVariables, ...numericalYVariables, ...numericalYErrorVariables, + ...interactiveFiltersVariables, getRenderingKey, }; }; @@ -103,7 +114,7 @@ export const useColumnsStateData = ( variables: ColumnsStateVariables ): ChartStateData => { const { chartConfig, observations } = chartProps; - const { sortData, getXAsDate, getY } = variables; + const { sortData, getXAsDate, getY, getTimeRangeDate } = variables; const plottableData = usePlottableData(observations, { getY, }); @@ -113,6 +124,7 @@ export const useColumnsStateData = ( const data = useChartData(sortedPlottableData, { chartConfig, getXAsDate, + getTimeRangeDate, }); return { diff --git a/app/charts/combo/combo-line-column-state-props.ts b/app/charts/combo/combo-line-column-state-props.ts index f8ef969b5..dc4cf5262 100644 --- a/app/charts/combo/combo-line-column-state-props.ts +++ b/app/charts/combo/combo-line-column-state-props.ts @@ -10,12 +10,14 @@ import { BandXVariables, BaseVariables, ChartStateData, + InteractiveFiltersVariables, RenderingVariables, SortingVariables, shouldUseDynamicMinScaleValue, useBandXVariables, useBaseVariables, useChartData, + useInteractiveFiltersVariables, } from "@/charts/shared/chart-state"; import { useRenderingKeyVariable } from "@/charts/shared/rendering-utils"; import { ComboLineColumnConfig, useChartConfigFilters } from "@/configurator"; @@ -38,7 +40,8 @@ export type ComboLineColumnStateVariables = BaseVariables & SortingVariables & BandXVariables & NumericalYComboLineColumnVariables & - RenderingVariables; + RenderingVariables & + InteractiveFiltersVariables; export const useComboLineColumnStateVariables = ( props: ChartProps @@ -59,6 +62,10 @@ export const useComboLineColumnStateVariables = ( dimensionsByIri, observations, }); + const interactiveFiltersVariables = useInteractiveFiltersVariables( + chartConfig.interactiveFiltersConfig, + { dimensionsByIri } + ); const lineIri = chartConfig.fields.y.lineComponentIri; const lineAxisOrientation = chartConfig.fields.y.lineAxisOrientation; @@ -135,6 +142,7 @@ export const useComboLineColumnStateVariables = ( sortData, ...bandXVariables, ...numericalYVariables, + ...interactiveFiltersVariables, getRenderingKey, }; }; @@ -144,7 +152,7 @@ export const useComboLineColumnStateData = ( variables: ComboLineColumnStateVariables ): ChartStateData => { const { chartConfig, observations } = chartProps; - const { sortData, getX, getXAsDate, y } = variables; + const { sortData, getX, getXAsDate, y, getTimeRangeDate } = variables; const plottableData = usePlottableData(observations, { getX, getY: (d) => { @@ -163,6 +171,7 @@ export const useComboLineColumnStateData = ( const data = useChartData(sortedPlottableData, { chartConfig, getXAsDate, + getTimeRangeDate, }); return { diff --git a/app/charts/combo/combo-line-dual-state-props.ts b/app/charts/combo/combo-line-dual-state-props.ts index d9cfaaaa9..d93ac5bca 100644 --- a/app/charts/combo/combo-line-dual-state-props.ts +++ b/app/charts/combo/combo-line-dual-state-props.ts @@ -9,11 +9,13 @@ import { import { BaseVariables, ChartStateData, + InteractiveFiltersVariables, SortingVariables, TemporalXVariables, shouldUseDynamicMinScaleValue, useBaseVariables, useChartData, + useInteractiveFiltersVariables, useTemporalXVariables, } from "@/charts/shared/chart-state"; import { ComboLineDualConfig } from "@/configurator"; @@ -30,7 +32,8 @@ type NumericalYComboLineDualVariables = { export type ComboLineDualStateVariables = BaseVariables & SortingVariables & TemporalXVariables & - NumericalYComboLineDualVariables; + NumericalYComboLineDualVariables & + InteractiveFiltersVariables; export const useComboLineDualStateVariables = ( props: ChartProps @@ -43,6 +46,10 @@ export const useComboLineDualStateVariables = ( const temporalXVariables = useTemporalXVariables(x, { dimensionsByIri, }); + const interactiveFiltersVariables = useInteractiveFiltersVariables( + chartConfig.interactiveFiltersConfig, + { dimensionsByIri } + ); const leftIri = chartConfig.fields.y.leftAxisComponentIri; const rightIri = chartConfig.fields.y.rightAxisComponentIri; @@ -102,6 +109,7 @@ export const useComboLineDualStateVariables = ( sortData, ...temporalXVariables, ...numericalYVariables, + ...interactiveFiltersVariables, }; }; @@ -110,7 +118,7 @@ export const useComboLineDualStateData = ( variables: ComboLineDualStateVariables ): ChartStateData => { const { chartConfig, observations } = chartProps; - const { sortData, getX, y } = variables; + const { sortData, getX, y, getTimeRangeDate } = variables; const plottableData = usePlottableData(observations, { getX, getY: (d) => { @@ -129,6 +137,7 @@ export const useComboLineDualStateData = ( const data = useChartData(sortedPlottableData, { chartConfig, getXAsDate: getX, + getTimeRangeDate, }); return { diff --git a/app/charts/combo/combo-line-single-state-props.ts b/app/charts/combo/combo-line-single-state-props.ts index ecff5b894..3e7f8140b 100644 --- a/app/charts/combo/combo-line-single-state-props.ts +++ b/app/charts/combo/combo-line-single-state-props.ts @@ -5,11 +5,13 @@ import { BaseYGetter, sortComboData } from "@/charts/combo/combo-state-props"; import { BaseVariables, ChartStateData, + InteractiveFiltersVariables, SortingVariables, TemporalXVariables, shouldUseDynamicMinScaleValue, useBaseVariables, useChartData, + useInteractiveFiltersVariables, useTemporalXVariables, } from "@/charts/shared/chart-state"; import { ComboLineSingleConfig } from "@/configurator"; @@ -26,7 +28,8 @@ type NumericalYComboLineSingleVariables = { export type ComboLineSingleStateVariables = BaseVariables & SortingVariables & TemporalXVariables & - NumericalYComboLineSingleVariables; + NumericalYComboLineSingleVariables & + InteractiveFiltersVariables; export const useComboLineSingleStateVariables = ( props: ChartProps @@ -39,6 +42,10 @@ export const useComboLineSingleStateVariables = ( const temporalXVariables = useTemporalXVariables(x, { dimensionsByIri, }); + const interactiveFiltersVariables = useInteractiveFiltersVariables( + chartConfig.interactiveFiltersConfig, + { dimensionsByIri } + ); const numericalYVariables: NumericalYComboLineSingleVariables = { y: { @@ -74,6 +81,7 @@ export const useComboLineSingleStateVariables = ( sortData, ...temporalXVariables, ...numericalYVariables, + ...interactiveFiltersVariables, }; }; @@ -82,7 +90,7 @@ export const useComboLineSingleStateData = ( variables: ComboLineSingleStateVariables ): ChartStateData => { const { chartConfig, observations } = chartProps; - const { sortData, getX, y } = variables; + const { sortData, getX, y, getTimeRangeDate } = variables; const plottableData = usePlottableData(observations, { getX, getY: (d) => { @@ -101,6 +109,7 @@ export const useComboLineSingleStateData = ( const data = useChartData(sortedPlottableData, { chartConfig, getXAsDate: getX, + getTimeRangeDate, }); return { diff --git a/app/charts/line/lines-state-props.ts b/app/charts/line/lines-state-props.ts index c99e7fbe9..813dc0254 100644 --- a/app/charts/line/lines-state-props.ts +++ b/app/charts/line/lines-state-props.ts @@ -5,12 +5,14 @@ import { usePlottableData } from "@/charts/shared/chart-helpers"; import { BaseVariables, ChartStateData, + InteractiveFiltersVariables, NumericalYVariables, SegmentVariables, SortingVariables, TemporalXVariables, useBaseVariables, useChartData, + useInteractiveFiltersVariables, useNumericalYVariables, useSegmentVariables, useTemporalXVariables, @@ -23,7 +25,8 @@ export type LinesStateVariables = BaseVariables & SortingVariables & TemporalXVariables & NumericalYVariables & - SegmentVariables; + SegmentVariables & + InteractiveFiltersVariables; export const useLinesStateVariables = ( props: ChartProps @@ -43,6 +46,10 @@ export const useLinesStateVariables = ( dimensionsByIri, observations, }); + const interactiveFiltersVariables = useInteractiveFiltersVariables( + chartConfig.interactiveFiltersConfig, + { dimensionsByIri } + ); const { getX } = temporalXVariables; const sortData: LinesStateVariables["sortData"] = useCallback( @@ -60,6 +67,7 @@ export const useLinesStateVariables = ( ...temporalXVariables, ...numericalYVariables, ...segmentVariables, + ...interactiveFiltersVariables, }; }; @@ -70,7 +78,13 @@ export const useLinesStateData = ( variables: LinesStateVariables ): ChartStateData => { const { chartConfig, observations } = chartProps; - const { sortData, getX, getY, getSegmentAbbreviationOrLabel } = variables; + const { + sortData, + getX, + getY, + getSegmentAbbreviationOrLabel, + getTimeRangeDate, + } = variables; const plottableData = usePlottableData(observations, { getY, }); @@ -81,6 +95,7 @@ export const useLinesStateData = ( chartConfig, getXAsDate: getX, getSegmentAbbreviationOrLabel, + getTimeRangeDate, }); return { diff --git a/app/charts/shared/brush/index.tsx b/app/charts/shared/brush/index.tsx index 8cbda728d..de33b22a4 100644 --- a/app/charts/shared/brush/index.tsx +++ b/app/charts/shared/brush/index.tsx @@ -105,7 +105,6 @@ export const BrushTime = () => { () => brushWidthScale.domain().map((d) => d.getTime()), [brushWidthScale] ); - const getClosestObservationFromRangeDates = useCallback( ([from, to]: [Date, Date]): [Date, Date] => { const getClosestDatesFromDateRange = makeGetClosestDatesFromDateRange( diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts index 145d4a3b4..c68d20a2b 100644 --- a/app/charts/shared/chart-state.ts +++ b/app/charts/shared/chart-state.ts @@ -177,6 +177,9 @@ export const useBandXVariables = ( }); const getXAsDate = useTemporalVariable(x.componentIri); + const getXTemporalEntity = useTemporalEntityVariable( + dimensionsByIri[x.componentIri].values + )(x.componentIri); return { xDimension, @@ -184,7 +187,9 @@ export const useBandXVariables = ( getXLabel, getXAbbreviationOrLabel, xTimeUnit, - getXAsDate, + getXAsDate: isTemporalDimension(xDimension) + ? getXAsDate + : getXTemporalEntity, }; }; @@ -402,6 +407,28 @@ export const useSegmentVariables = ( }; }; +export type InteractiveFiltersVariables = { + getTimeRangeDate: (d: Observation) => Date; +}; + +export const useInteractiveFiltersVariables = ( + interactiveFiltersConfig: ChartConfig["interactiveFiltersConfig"], + { dimensionsByIri }: { dimensionsByIri: DimensionsByIri } +): InteractiveFiltersVariables => { + const iri = interactiveFiltersConfig?.timeRange.componentIri ?? ""; + const dimension = dimensionsByIri[iri]; + const getTimeRangeDate = useTemporalVariable(iri); + const getTimeRangeEntityDate = useTemporalEntityVariable( + dimension?.values ?? [] + )(iri); + + return { + getTimeRangeDate: isTemporalDimension(dimension) + ? getTimeRangeDate + : getTimeRangeEntityDate, + }; +}; + export type AreaLayerVariables = { areaLayerDimension: GeoShapesDimension | undefined; }; @@ -446,10 +473,12 @@ export const useChartData = ( chartConfig, getXAsDate, getSegmentAbbreviationOrLabel, + getTimeRangeDate, }: { chartConfig: ChartConfig; getXAsDate?: (d: Observation) => Date; getSegmentAbbreviationOrLabel?: (d: Observation) => string; + getTimeRangeDate?: (d: Observation) => Date; } ): Omit => { const { interactiveFiltersConfig } = chartConfig; @@ -458,10 +487,7 @@ export const useChartData = ( const timeSlider = useChartInteractiveFilters((d) => d.timeSlider); // time range - const timeRangeFilterComponentIri = - interactiveFiltersConfig?.timeRange.componentIri ?? ""; const interactiveTimeRange = interactiveFiltersConfig?.timeRange; - const getTimeRangeTime = useTemporalVariable(timeRangeFilterComponentIri); const timeRangeFromTime = interactiveTimeRange?.presets.from ? parseDate(interactiveTimeRange?.presets.from).getTime() : undefined; @@ -470,15 +496,15 @@ export const useChartData = ( : undefined; const timeRangeFilters = useMemo(() => { const timeRangeFilter: ValuePredicate | null = - timeRangeFromTime && timeRangeToTime + getTimeRangeDate && timeRangeFromTime && timeRangeToTime ? (d: Observation) => { - const time = getTimeRangeTime(d).getTime(); + const time = getTimeRangeDate(d).getTime(); return time >= timeRangeFromTime && time <= timeRangeToTime; } : null; return timeRangeFilter ? [timeRangeFilter] : []; - }, [timeRangeFromTime, timeRangeToTime, getTimeRangeTime]); + }, [timeRangeFromTime, timeRangeToTime, getTimeRangeDate]); // interactive time range const interactiveFromTime = timeRange.from?.getTime(); diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index f03844653..e3dde53fe 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -18,7 +18,7 @@ import { timeUnitToFormatter, timeUnitToParser, } from "@/configurator/components/ui-helpers"; -import { isTemporalDimension } from "@/domain/data"; +import { canDimensionBeTimeFiltered } from "@/domain/data"; import { useDataCubesComponentsQuery } from "@/graphql/hooks"; import { DataCubeComponentFilter, TimeUnit } from "@/graphql/query-hooks"; import { useLocale } from "@/src"; @@ -117,7 +117,7 @@ const DashboardTimeRangeSlider = ({ const timeUnit = useMemo(() => { const dim = data?.data?.dataCubesComponents?.dimensions?.[0]; - return isTemporalDimension(dim) ? dim.timeUnit : undefined; + return canDimensionBeTimeFiltered(dim) ? dim.timeUnit : undefined; }, [data?.data?.dataCubesComponents?.dimensions]); const presets = filter.presets; @@ -200,7 +200,7 @@ const DashboardTimeRangeSlider = ({ step={step} valueLabelDisplay={mountedForSomeTime ? "on" : "off"} value={timeRange} - marks + marks={(max - min) / (step ?? 1) < 50} /> ); }; @@ -229,10 +229,12 @@ export const DashboardInteractiveFilters = () => { ); }; + function stepFromTimeUnit(timeUnit: TimeUnit | undefined) { if (!timeUnit) { return 0; } + switch (timeUnit) { case "Year": return 1 * 60 * 60 * 24 * 365; diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index d6fcf4b4e..301b7f971 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -41,8 +41,9 @@ import { import { Dimension, TemporalDimension, + TemporalEntityDimension, + canDimensionBeTimeFiltered, isJoinByComponent, - isTemporalDimension, } from "@/domain/data"; import { useFlag } from "@/flags"; import { useTimeFormatLocale, useTimeFormatUnit } from "@/formatters"; @@ -162,8 +163,14 @@ const LayoutSharedFiltersConfigurator = () => { ? dimensionsByIri[componentIri] : undefined; - if (!componentIri || !dimension || !isTemporalDimension(dimension)) + if ( + !componentIri || + !dimension || + !canDimensionBeTimeFiltered(dimension) + ) { return; + } + if (checked) { const options = getTimeFilterOptions({ dimension: dimension, @@ -222,7 +229,7 @@ const LayoutSharedFiltersConfigurator = () => { const dimension = dimensionsByIri[filter.componentIri]; const sharedFilter = sharedFiltersByIri[filter.componentIri]; - if (!dimension || !isTemporalDimension(dimension)) { + if (!dimension || !canDimensionBeTimeFiltered(dimension)) { return null; } return ( @@ -290,8 +297,9 @@ const SharedFilterOptions = ({ if (sharedFilter.type !== "timeRange") { return null; } + if ( - !isTemporalDimension(dimension) || + !canDimensionBeTimeFiltered(dimension) || !canRenderDatePickerField(dimension.timeUnit) ) { return null; @@ -310,7 +318,7 @@ const SharedFilterOptionsTimeRange = ({ dimension, }: { sharedFilter: SharedFilter; - dimension: TemporalDimension; + dimension: TemporalDimension | TemporalEntityDimension; }) => { const { timeUnit, timeFormat } = dimension; const formatLocale = useTimeFormatLocale(); @@ -325,7 +333,7 @@ const SharedFilterOptionsTimeRange = ({ }); }, [dimension, parseDate]); - const handleChangeFromDate: DatePickerFieldProps["onChange"] = (ev) => + const handleChangeFromDate: DatePickerFieldProps["onChange"] = (ev) => { dispatch({ type: "DASHBOARD_FILTER_UPDATE", value: { @@ -336,6 +344,7 @@ const SharedFilterOptionsTimeRange = ({ }, }, }); + }; const handleChangeFromGeneric: DataFilterGenericDimensionProps["onChange"] = ( ev diff --git a/app/configurator/components/ui-helpers.ts b/app/configurator/components/ui-helpers.ts index 31ee706eb..1eb85e4c2 100644 --- a/app/configurator/components/ui-helpers.ts +++ b/app/configurator/components/ui-helpers.ts @@ -4,6 +4,7 @@ import { scaleOrdinal } from "d3-scale"; import { CountableTimeInterval } from "d3-time"; import { timeFormat, TimeLocaleObject, timeParse } from "d3-time-format"; import { useMemo } from "react"; +import { match } from "ts-pattern"; import type { BaseChartProps } from "@/charts/shared/ChartProps"; import { TableColumn, TableFields } from "@/config-types"; @@ -13,8 +14,11 @@ import { Dimension, DimensionValue, isJoinByComponent, + isTemporalDimension, Measure, Observation, + TemporalDimension, + TemporalEntityDimension, } from "@/domain/data"; import { TimeUnit } from "@/graphql/query-hooks"; import { IconName } from "@/icons"; @@ -360,7 +364,7 @@ export const extractDataPickerOptionsFromDimension = ({ dimension, parseDate, }: { - dimension: Dimension; + dimension: TemporalDimension | TemporalEntityDimension; parseDate: (dateStr: string) => Date | null; }) => { const { isKeyDimension, label, values } = dimension; @@ -368,16 +372,36 @@ export const extractDataPickerOptionsFromDimension = ({ const noneLabel = "None"; if (values.length) { - const options = values.map((d) => { - return { - label: `${d.value}`, - value: `${d.value}`, - }; - }); + const [minValue, maxValue] = isTemporalDimension(dimension) + ? [values[0].value as string, values[values.length - 1].value as string] + : [ + values[0].position as string, + values[values.length - 1].position as string, + ]; + const dimensionType = dimension.__typename; + const options = match(dimensionType) + .with("TemporalDimension", () => { + return values.map((d) => { + const stringifiedValue = `${d.value}`; + return { + label: stringifiedValue, + value: stringifiedValue, + }; + }); + }) + .with("TemporalEntityDimension", () => { + return values.map((d) => { + return { + label: `${d.label}`, + value: `${d.position}`, + }; + }); + }) + .exhaustive(); return { - minDate: parseDate(values[0].value as string) as Date, - maxDate: parseDate(values[values.length - 1].value as string) as Date, + minDate: parseDate(minValue) as Date, + maxDate: parseDate(maxValue) as Date, options: isKeyDimension ? options : [ diff --git a/app/domain/data.ts b/app/domain/data.ts index 1c2f4f64b..d10dc4750 100644 --- a/app/domain/data.ts +++ b/app/domain/data.ts @@ -580,6 +580,12 @@ export const isTemporalEntityDimension = ( return dimension?.__typename === "TemporalEntityDimension"; }; +export const canDimensionBeTimeFiltered = ( + dimension?: Component | null +): dimension is TemporalDimension | TemporalEntityDimension => { + return isTemporalDimension(dimension) || isTemporalEntityDimension(dimension); +}; + export const isTemporalOrdinalDimension = ( dimension?: Component | null ): dimension is TemporalOrdinalDimension => { diff --git a/app/stores/interactive-filters.tsx b/app/stores/interactive-filters.tsx index c1f9da6f4..cbfc8eb90 100644 --- a/app/stores/interactive-filters.tsx +++ b/app/stores/interactive-filters.tsx @@ -167,7 +167,9 @@ export const getPotentialSharedFilters = ( const temporalDimensions = chartConfigs.flatMap((config) => { const chartSpec = getChartSpec(config); const temporalEncodings = chartSpec.encodings.filter((x) => - x.componentTypes.some((x) => x === "TemporalDimension") + x.componentTypes.some( + (x) => x === "TemporalDimension" || x === "TemporalEntityDimension" + ) ); const chartTemporalDimensions = temporalEncodings .map((encoding) => {