From ad472a4f3726e6f0af6403331bcd2ed5ac02d2cb Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 27 Jun 2024 11:34:14 +0200 Subject: [PATCH 01/14] fix: Remove unnecessary row gaps in subgrid layout --- app/components/chart-panel-layout-tall.tsx | 3 ++- app/components/chart-preview.tsx | 17 +++++++---------- app/components/chart-published.tsx | 17 +++++++---------- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/app/components/chart-panel-layout-tall.tsx b/app/components/chart-panel-layout-tall.tsx index d20dea7fe..825d4cbee 100644 --- a/app/components/chart-panel-layout-tall.tsx +++ b/app/components/chart-panel-layout-tall.tsx @@ -48,7 +48,8 @@ const ChartPanelLayoutTallRow = (props: ChartPanelLayoutTallRowProps) => { xs: "1fr", md: "calc(50% - 8px) calc(50% - 8px)", }, - gap: "16px", + gridAutoRows: "min-content", + columnGap: 4, }} > {row.chartConfigs.map(row.renderChart)} diff --git a/app/components/chart-preview.tsx b/app/components/chart-preview.tsx index 708fd1875..bf352eacf 100644 --- a/app/components/chart-preview.tsx +++ b/app/components/chart-preview.tsx @@ -461,6 +461,7 @@ const ChartPreviewInner = (props: ChartPreviewInnerProps) => { > { : undefined } /> - ) : // We need to have a span here to keep the space between the - // title and the chart (subgrid layout) - state.layout.type === "dashboard" ? ( -   ) : ( - + // We need to have a span here to keep the space between the + // title and the chart (subgrid layout) + )} { : undefined } /> - ) : // We need to have a span here to keep the space between the - // title and the chart (subgrid layout) - state.layout.type === "dashboard" ? ( -   ) : ( - + // We need to have a span here to keep the space between the + // title and the chart (subgrid layout) + )} { { text={meta.title[locale]} smaller={state.layout.type === "dashboard"} /> - ) : // We need to have a span here to keep the space between the - // title and the chart (subgrid layout) - state.layout.type === "dashboard" ? ( -   ) : ( - + // We need to have a span here to keep the space between the + // title and the chart (subgrid layout) + )} { text={meta.description[locale]} smaller={state.layout.type === "dashboard"} /> - ) : // We need to have a span here to keep the space between the - // title and the chart (subgrid layout) - state.layout.type === "dashboard" ? ( -   ) : ( - + // We need to have a span here to keep the space between the + // title and the chart (subgrid layout) + )} Date: Thu, 27 Jun 2024 12:31:08 +0200 Subject: [PATCH 02/14] fix: Update chart interactive stores when updating base time range presets in shared filters --- .../dashboard-interactive-filters.tsx | 14 +++- .../components/layout-configurator.tsx | 82 +++++++++++++++++-- 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index 3881ccea7..99655b699 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -150,9 +150,7 @@ const DashboardTimeRangeSlider = ({ for (const [_getState, _useStore, store] of Object.values( dashboardInteractiveFilters.stores )) { - store.setState({ - timeRange: newTimeRange, - }); + store.setState({ timeRange: newTimeRange }); setTimeRange([value[0], value[1]]); } } @@ -173,6 +171,16 @@ const DashboardTimeRangeSlider = ({ [timeRange, timeUnit, presets, handleChangeSlider, filter.componentIri] ); + useEffect(() => { + if (filter.presets.from && filter.presets.to && timeUnit) { + const parser = timeUnitToParser[timeUnit]; + setTimeRange([ + toUnixSeconds(parser(filter.presets.from)), + toUnixSeconds(parser(filter.presets.to)), + ]); + } + }, [filter.presets.from, filter.presets.to, timeUnit]); + const mountedForSomeTime = useTimeout(500, mounted); if (!filter || !timeRange || filter.type !== "timeRange" || !filter.active) { diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index 685be3a53..f602683e7 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -11,12 +11,12 @@ import { import capitalize from "lodash/capitalize"; import keyBy from "lodash/keyBy"; import omit from "lodash/omit"; -import { Fragment, useMemo } from "react"; +import { Fragment, useCallback, useMemo } from "react"; import { DataFilterGenericDimensionProps } from "@/charts/shared/chart-data-filters"; import { Select } from "@/components/form"; import { generateLayout } from "@/components/react-grid"; -import { ChartConfig, LayoutDashboard } from "@/config-types"; +import { ChartConfig, getChartConfig, LayoutDashboard } from "@/config-types"; import { LayoutAnnotator } from "@/configurator/components/annotators"; import { ControlSection, @@ -315,7 +315,8 @@ const SharedFilterOptionsTimeRange = ({ const formatLocale = useTimeFormatLocale(); const formatDate = formatLocale.format(timeFormat); const parseDate = formatLocale.parse(timeFormat); - const [, dispatch] = useConfiguratorState(); + const [state, dispatch] = useConfiguratorState(); + const dashboardInteractiveFilters = useDashboardInteractiveFilters(); const { minDate, maxDate, optionValues, options } = useMemo(() => { return extractDataPickerOptionsFromDimension({ @@ -324,22 +325,71 @@ const SharedFilterOptionsTimeRange = ({ }); }, [dimension, parseDate]); + const updateChartStoresFrom = useCallback( + (newDate: Date) => { + const sharedFilterIri = sharedFilter.componentIri; + Object.entries(dashboardInteractiveFilters.stores).forEach( + ([chartKey, [getInteractiveFiltersState]]) => { + const { interactiveFiltersConfig } = getChartConfig(state, chartKey); + const interactiveFiltersState = getInteractiveFiltersState(); + const { from, to } = interactiveFiltersState.timeRange; + const setTimeRangeFilter = interactiveFiltersState.setTimeRange; + if ( + from && + to && + interactiveFiltersConfig?.timeRange.componentIri === sharedFilterIri + ) { + setTimeRangeFilter(newDate, to); + } + } + ); + }, + [dashboardInteractiveFilters.stores, sharedFilter.componentIri, state] + ); + + const updateChartStoresTo = useCallback( + (newDate: Date) => { + const sharedFilterIri = sharedFilter.componentIri; + Object.entries(dashboardInteractiveFilters.stores).forEach( + ([chartKey, [getInteractiveFiltersState]]) => { + const { interactiveFiltersConfig } = getChartConfig(state, chartKey); + const interactiveFiltersState = getInteractiveFiltersState(); + const { from, to } = interactiveFiltersState.timeRange; + const setTimeRangeFilter = interactiveFiltersState.setTimeRange; + if ( + from && + to && + interactiveFiltersConfig?.timeRange.componentIri === sharedFilterIri + ) { + setTimeRangeFilter(from, newDate); + } + } + ); + }, + [dashboardInteractiveFilters.stores, sharedFilter.componentIri, state] + ); + const handleChangeFromDate: DatePickerFieldProps["onChange"] = (ev) => { + const newDate = parseDate(ev.target.value); + if (!newDate) { + return; + } dispatch({ type: "DASHBOARD_FILTER_UPDATE", value: { ...sharedFilter, presets: { ...sharedFilter.presets, - from: formatDate(new Date(ev.target.value)), + from: formatDate(newDate), }, }, }); + updateChartStoresFrom(newDate); }; const handleChangeFromGeneric: DataFilterGenericDimensionProps["onChange"] = ( ev - ) => + ) => { dispatch({ type: "DASHBOARD_FILTER_UPDATE", value: { @@ -350,22 +400,33 @@ const SharedFilterOptionsTimeRange = ({ }, }, }); + const parsedDate = parseDate(ev.target.value as string); + if (parsedDate) { + updateChartStoresFrom(parsedDate); + } + }; - const handleChangeToDate: DatePickerFieldProps["onChange"] = (ev) => + const handleChangeToDate: DatePickerFieldProps["onChange"] = (ev) => { + const newDate = parseDate(ev.target.value); + if (!newDate) { + return; + } dispatch({ type: "DASHBOARD_FILTER_UPDATE", value: { ...sharedFilter, presets: { ...sharedFilter.presets, - to: formatDate(new Date(ev.target.value)), + to: formatDate(newDate), }, }, }); + updateChartStoresTo(newDate); + }; const handleChangeToGeneric: DataFilterGenericDimensionProps["onChange"] = ( ev - ) => + ) => { dispatch({ type: "DASHBOARD_FILTER_UPDATE", value: { @@ -376,6 +437,11 @@ const SharedFilterOptionsTimeRange = ({ }, }, }); + const parsedDate = parseDate(ev.target.value as string); + if (parsedDate) { + updateChartStoresTo(parsedDate); + } + }; return ( From 4fbed3c732daac71f66fe3d21f13d15df8d7be7f Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 27 Jun 2024 13:51:26 +0200 Subject: [PATCH 03/14] style: Add a bit of top margin for the table view --- app/components/chart-footnotes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/chart-footnotes.tsx b/app/components/chart-footnotes.tsx index f09603147..449a50672 100644 --- a/app/components/chart-footnotes.tsx +++ b/app/components/chart-footnotes.tsx @@ -78,7 +78,7 @@ export const ChartFootnotes = ({ const formatLocale = useTimeFormatLocale(); return ( - :not(:last-child)": { mb: 3 } }}> + :not(:last-child)": { mb: 3 } }}> {data?.dataCubesMetadata.map((metadata) => (
Date: Thu, 27 Jun 2024 14:46:28 +0200 Subject: [PATCH 04/14] feat: Only one shared time range filter per time unit --- app/charts/column/chart-column.tsx | 4 +- app/charts/combo/chart-combo-line-column.tsx | 9 +- app/charts/combo/chart-combo-line-dual.tsx | 9 +- app/charts/combo/chart-combo-line-single.tsx | 10 +- app/charts/shared/brush/index.tsx | 8 +- app/charts/shared/chart-state.ts | 11 +- .../dashboard-interactive-filters.tsx | 40 +- app/components/metadata-panel.tsx | 4 +- app/config-types.ts | 5 +- .../components/add-dataset-dialog.mock.ts | 2 +- .../components/layout-configurator.tsx | 352 +++++++++++------- app/configurator/components/ui-helpers.ts | 19 +- .../configurator-state/actions.tsx | 12 +- .../configurator-state/initial.tsx | 2 +- app/configurator/configurator-state/mocks.ts | 4 +- .../configurator-state/reducer.tsx | 27 +- .../interactive-filters/time-slider.tsx | 4 +- app/docs/charts.stories.tsx | 7 +- app/docs/fixtures.ts | 2 +- app/docs/lines.stories.tsx | 2 +- app/locales/de/messages.po | 4 + app/locales/en/messages.po | 4 + app/locales/fr/messages.po | 4 + app/locales/it/messages.po | 4 + app/stores/interactive-filters.tsx | 41 +- app/utils/chart-config/versioning.ts | 27 +- 26 files changed, 367 insertions(+), 250 deletions(-) diff --git a/app/charts/column/chart-column.tsx b/app/charts/column/chart-column.tsx index fb32da3d6..8575823d2 100644 --- a/app/charts/column/chart-column.tsx +++ b/app/charts/column/chart-column.tsx @@ -40,11 +40,11 @@ const ChartColumns = memo((props: ChartProps) => { const { chartConfig, dimensions } = props; const { fields, interactiveFiltersConfig } = chartConfig; const filters = useChartConfigFilters(chartConfig); - const { sharedFilters } = useDashboardInteractiveFilters(); + const { sharedTimeRangeFilters } = useDashboardInteractiveFilters(); const showTimeBrush = shouldShowBrush( interactiveFiltersConfig, - sharedFilters + sharedTimeRangeFilters ); return ( diff --git a/app/charts/combo/chart-combo-line-column.tsx b/app/charts/combo/chart-combo-line-column.tsx index 388f237f8..1d766cb25 100644 --- a/app/charts/combo/chart-combo-line-column.tsx +++ b/app/charts/combo/chart-combo-line-column.tsx @@ -26,7 +26,7 @@ const ChartComboLineColumn = memo( (props: ChartProps) => { const { chartConfig } = props; const { interactiveFiltersConfig } = chartConfig; - const { sharedFilters } = useDashboardInteractiveFilters(); + const { sharedTimeRangeFilters } = useDashboardInteractiveFilters(); return ( @@ -37,9 +37,10 @@ const ChartComboLineColumn = memo( - {shouldShowBrush(interactiveFiltersConfig, sharedFilters) && ( - - )} + {shouldShowBrush( + interactiveFiltersConfig, + sharedTimeRangeFilters + ) && } diff --git a/app/charts/combo/chart-combo-line-dual.tsx b/app/charts/combo/chart-combo-line-dual.tsx index 469f46c95..d91202cf5 100644 --- a/app/charts/combo/chart-combo-line-dual.tsx +++ b/app/charts/combo/chart-combo-line-dual.tsx @@ -25,7 +25,7 @@ export const ChartComboLineDualVisualization = ( const ChartComboLineDual = memo((props: ChartProps) => { const { chartConfig } = props; const { interactiveFiltersConfig } = chartConfig; - const { sharedFilters } = useDashboardInteractiveFilters(); + const { sharedTimeRangeFilters } = useDashboardInteractiveFilters(); return ( @@ -36,9 +36,10 @@ const ChartComboLineDual = memo((props: ChartProps) => { - {shouldShowBrush(interactiveFiltersConfig, sharedFilters) && ( - - )} + {shouldShowBrush( + interactiveFiltersConfig, + sharedTimeRangeFilters + ) && } diff --git a/app/charts/combo/chart-combo-line-single.tsx b/app/charts/combo/chart-combo-line-single.tsx index cda722f0c..b2dc0d4da 100644 --- a/app/charts/combo/chart-combo-line-single.tsx +++ b/app/charts/combo/chart-combo-line-single.tsx @@ -26,7 +26,8 @@ const ChartComboLineSingle = memo( (props: ChartProps) => { const { chartConfig } = props; const { interactiveFiltersConfig } = chartConfig; - const { sharedFilters } = useDashboardInteractiveFilters(); + const { sharedTimeRangeFilters } = useDashboardInteractiveFilters(); + return ( @@ -34,9 +35,10 @@ const ChartComboLineSingle = memo( - {shouldShowBrush(interactiveFiltersConfig, sharedFilters) && ( - - )} + {shouldShowBrush( + interactiveFiltersConfig, + sharedTimeRangeFilters + ) && } diff --git a/app/charts/shared/brush/index.tsx b/app/charts/shared/brush/index.tsx index 35ea36ebf..895fa3f0c 100644 --- a/app/charts/shared/brush/index.tsx +++ b/app/charts/shared/brush/index.tsx @@ -22,7 +22,7 @@ import { import { Observation } from "@/domain/data"; import { useFormatFullDateAuto } from "@/formatters"; import { - SharedFilter, + SharedTimeRangeFilter, useChartInteractiveFilters, useInteractiveFiltersGetState, } from "@/stores/interactive-filters"; @@ -44,12 +44,12 @@ export const shouldShowBrush = ( | ColumnConfig )["interactiveFiltersConfig"] | undefined, - sharedFilters: SharedFilter[] | undefined + sharedTimeRangeFilters: SharedTimeRangeFilter[] | undefined ) => { const chartTimeRange = interactiveFiltersConfig?.timeRange; const chartTimeRangeIri = chartTimeRange?.componentIri; - const sharedFilter = sharedFilters?.find( - (x) => x.type === "timeRange" && x.componentIri === chartTimeRangeIri + const sharedFilter = sharedTimeRangeFilters?.find( + (x) => x.componentIri === chartTimeRangeIri ); return ( (chartTimeRange?.active && !sharedFilter) || sharedFilter?.active == false diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts index 55494caa9..15e6eceb4 100644 --- a/app/charts/shared/chart-state.ts +++ b/app/charts/shared/chart-state.ts @@ -511,10 +511,10 @@ export const useChartData = ( const interactiveToTime = timeRange.to?.getTime(); const dashboardFilters = useDashboardInteractiveFilters(); const interactiveTimeRangeFilters = useMemo(() => { - const isDashboardFilterActive = !!dashboardFilters.sharedFilters.find( - (f) => { + const isDashboardFilterActive = + !!dashboardFilters.sharedTimeRangeFilters.find((f) => { const timeRangeFilterIri = interactiveTimeRange?.componentIri; - if (f.type !== "timeRange" || !timeRangeFilterIri) { + if (!timeRangeFilterIri) { return false; } return isJoinById(timeRangeFilterIri) @@ -522,8 +522,7 @@ export const useChartData = ( f.componentIri ) : f.componentIri === timeRangeFilterIri; - } - ); + }); const interactiveTimeRangeFilter: ValuePredicate | null = getXAsDate && interactiveFromTime && @@ -537,7 +536,7 @@ export const useChartData = ( return interactiveTimeRangeFilter ? [interactiveTimeRangeFilter] : []; }, [ - dashboardFilters.sharedFilters, + dashboardFilters.sharedTimeRangeFilters, getXAsDate, interactiveFromTime, interactiveToTime, diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index 99655b699..5c7daf537 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -22,7 +22,7 @@ import { useConfigsCubeComponents } from "@/graphql/hooks"; import { TimeUnit } from "@/graphql/query-hooks"; import { useLocale } from "@/src"; import { - SharedFilter, + SharedTimeRangeFilter, useDashboardInteractiveFilters, } from "@/stores/interactive-filters"; import { assert } from "@/utils/assert"; @@ -81,7 +81,7 @@ const DashboardTimeRangeSlider = ({ filter, mounted, }: { - filter: Extract; + filter: SharedTimeRangeFilter; mounted: boolean; }) => { const classes = useStyles(); @@ -183,7 +183,7 @@ const DashboardTimeRangeSlider = ({ const mountedForSomeTime = useTimeout(500, mounted); - if (!filter || !timeRange || filter.type !== "timeRange" || !filter.active) { + if (!filter || !timeRange || !filter.active) { return null; } @@ -208,22 +208,24 @@ export const DashboardInteractiveFilters = () => { return ( <> - {dashboardInteractiveFilters.sharedFilters.map((filter) => { - if (filter.type !== "timeRange" || !filter.active) { - return null; - } - - return ( - -
- -
-
- ); - })} + {dashboardInteractiveFilters.sharedTimeRangeFilters + .slice(1) + .map((filter) => { + if (!filter.active) { + return null; + } + + return ( + +
+ +
+
+ ); + })} ); }; diff --git a/app/components/metadata-panel.tsx b/app/components/metadata-panel.tsx index 4b87c19a4..0611c902d 100644 --- a/app/components/metadata-panel.tsx +++ b/app/components/metadata-panel.tsx @@ -475,7 +475,7 @@ const DataPanel = ({ }[] => { if (isJoinByComponent(component)) { return (component.originalIris ?? []).map((x) => ({ - label: getComponentLabel(component, x.cubeIri), + label: getComponentLabel(component, { cubeIri: x.cubeIri }), value: { ...omit(component, "originalIris"), cubeIri: x.cubeIri, @@ -674,7 +674,7 @@ const ComponentTabPanel = ({ const classes = useOtherStyles(); const { setSelectedDimension } = useMetadataPanelStoreActions(); const label = useMemo( - () => getComponentLabel(component, cubeIri), + () => getComponentLabel(component, { cubeIri }), [cubeIri, component] ); const description = useMemo(() => { diff --git a/app/config-types.ts b/app/config-types.ts index 601549598..160a2d9f1 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -1157,11 +1157,8 @@ export type DashboardFilterTimeRange = t.TypeOf< typeof DashboardFilterTimeRange >; -const DashboardFilter = DashboardFilterTimeRange; // Will be replaced by an union later -export type DashboardFilter = t.TypeOf; - const DashboardFiltersConfig = t.type({ - filters: t.array(DashboardFilter), + timeRangeFilters: t.array(DashboardFilterTimeRange), }); export type DashboardFiltersConfig = t.TypeOf; diff --git a/app/configurator/components/add-dataset-dialog.mock.ts b/app/configurator/components/add-dataset-dialog.mock.ts index 99ed52e9a..d6c50e430 100644 --- a/app/configurator/components/add-dataset-dialog.mock.ts +++ b/app/configurator/components/add-dataset-dialog.mock.ts @@ -101,6 +101,6 @@ export const photovoltaikChartStateMock: ConfiguratorStateConfiguringChart = { ], activeChartKey: "8-5RW138pTDA", dashboardFilters: { - filters: [], + timeRangeFilters: [], }, }; diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index f602683e7..e22c42fbd 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -9,8 +9,9 @@ import { useEventCallback, } from "@mui/material"; import capitalize from "lodash/capitalize"; -import keyBy from "lodash/keyBy"; import omit from "lodash/omit"; +import uniq from "lodash/uniq"; +import uniqBy from "lodash/uniqBy"; import { Fragment, useCallback, useMemo } from "react"; import { DataFilterGenericDimensionProps } from "@/charts/shared/chart-data-filters"; @@ -41,15 +42,18 @@ import { canDimensionBeTimeFiltered, Dimension, isJoinByComponent, + isTemporalDimensionWithTimeUnit, TemporalDimension, TemporalEntityDimension, } from "@/domain/data"; +import { truthy } from "@/domain/types"; import { useFlag } from "@/flags"; import { useTimeFormatLocale, useTimeFormatUnit } from "@/formatters"; import { useConfigsCubeComponents } from "@/graphql/hooks"; +import { timeUnitFormats } from "@/rdf/mappings"; import { useLocale } from "@/src"; import { - SharedFilter, + SharedTimeRangeFilter, useDashboardInteractiveFilters, } from "@/stores/interactive-filters"; import { getTimeFilterOptions } from "@/utils/time-filter-options"; @@ -111,7 +115,7 @@ const LayoutLayoutConfigurator = () => { const LayoutSharedFiltersConfigurator = () => { const [state, dispatch] = useConfiguratorState(isLayouting); const { layout } = state; - const { sharedFilters, potentialSharedFilters } = + const { sharedTimeRangeFilters, potentialSharedTimeRangeFilters } = useDashboardInteractiveFilters(); const locale = useLocale(); @@ -122,74 +126,87 @@ const LayoutSharedFiltersConfigurator = () => { }, }); - const dimensionsByIri = useMemo(() => { - const res: Record = {}; - for (const dim of data?.dataCubesComponents.dimensions ?? []) { - res[dim.iri] = dim; + const { dimensions, dimensionsByIri } = useMemo(() => { + const dimensions = data?.dataCubesComponents.dimensions ?? []; + const dimensionsByIri: Record = {}; + for (const dim of dimensions) { + dimensionsByIri[dim.iri] = dim; if (isJoinByComponent(dim)) { for (const o of dim.originalIris) { - res[o.dimensionIri] = dim; + dimensionsByIri[o.dimensionIri] = dim; } } } - return res; + return { dimensions, dimensionsByIri }; }, [data?.dataCubesComponents.dimensions]); - const sharedFiltersByIri = useMemo(() => { - return keyBy(sharedFilters, (x) => x.componentIri); - }, [sharedFilters]); + const shownFilters = potentialSharedTimeRangeFilters.filter((filter) => { + const dimension = dimensionsByIri[filter.componentIri]; + return dimension && canDimensionBeTimeFiltered(dimension); + }); + const timeUnits = uniq( + shownFilters + .map((filter) => { + const dimension = dimensionsByIri[filter.componentIri]; + return isTemporalDimensionWithTimeUnit(dimension) + ? dimension.timeUnit + : undefined; + }) + .filter(truthy) + ); const formatLocale = useTimeFormatLocale(); const timeFormatUnit = useTimeFormatUnit(); const handleToggle: SwitchProps["onChange"] = useEventCallback( - (event, checked) => { - const componentIri = event.currentTarget.dataset.componentIri; - const dimension = componentIri - ? dimensionsByIri[componentIri] - : undefined; - - if ( - !componentIri || - !dimension || - !canDimensionBeTimeFiltered(dimension) - ) { - return; - } + (_, checked) => { + for (const { componentIri } of shownFilters) { + const dimension = componentIri + ? dimensionsByIri[componentIri] + : undefined; + + if ( + !componentIri || + !dimension || + !canDimensionBeTimeFiltered(dimension) + ) { + return; + } - if (checked) { - const options = getTimeFilterOptions({ - dimension: dimension, - formatLocale, - timeFormatUnit, - }); + if (checked) { + const options = getTimeFilterOptions({ + dimension: dimension, + formatLocale, + timeFormatUnit, + }); - const from = options.sortedOptions[0].date; - const to = options.sortedOptions.at(-1)?.date; - const dateFormatter = timeUnitToFormatter[dimension.timeUnit]; + const from = options.sortedOptions[0].date; + const to = options.sortedOptions.at(-1)?.date; + const dateFormatter = timeUnitToFormatter[dimension.timeUnit]; - if (!from || !to) { - return; - } + if (!from || !to) { + return; + } - dispatch({ - type: "DASHBOARD_FILTER_ADD", - value: { - type: "timeRange", - active: true, - presets: { - type: "range", - from: dateFormatter(from), - to: dateFormatter(to), + dispatch({ + type: "DASHBOARD_TIME_RANGE_FILTER_ADD", + value: { + type: "timeRange", + active: true, + presets: { + type: "range", + from: dateFormatter(from), + to: dateFormatter(to), + }, + componentIri: componentIri, }, - componentIri: componentIri, - }, - }); - } else { - dispatch({ - type: "DASHBOARD_FILTER_REMOVE", - value: componentIri, - }); + }); + } else { + dispatch({ + type: "DASHBOARD_TIME_RANGE_FILTER_REMOVE", + value: componentIri, + }); + } } } ); @@ -197,14 +214,10 @@ const LayoutSharedFiltersConfigurator = () => { switch (layout.type) { case "tab": case "dashboard": - const shownFilters = potentialSharedFilters.filter((filter) => { - const dimension = dimensionsByIri[filter.componentIri]; - return dimension && canDimensionBeTimeFiltered(dimension); - }); - - if (!shownFilters.length) { + if (!timeUnits.length) { return null; } + return ( { - {shownFilters.map((filter) => { - const dimension = dimensionsByIri[filter.componentIri]; - const sharedFilter = sharedFiltersByIri[filter.componentIri]; + {timeUnits.map((timeUnit) => { + const timeUnitDimensions = dimensions.filter( + (dimension) => + isTemporalDimensionWithTimeUnit(dimension) && + dimension.timeUnit === timeUnit + ) as TemporalDimension[]; + const timeFormat = timeUnitFormats.get(timeUnit); + + if (!timeUnitDimensions.length || !timeFormat) { + return null; + } + + const values = timeUnitDimensions.flatMap( + (dimension) => dimension.values + ); + const combinedDimension: TemporalDimension = { + __typename: "TemporalDimension", + cubeIri: "all", + iri: "combined", + label: t({ + id: "controls.section.shared-filters.date", + message: "Date", + }), + isKeyDimension: true, + isNumerical: false, + values: uniqBy(values, "value").sort((a, b) => + (a.value as string).localeCompare(b.value as string) + ), + timeUnit, + timeFormat, + }; + return ( - + - {dimension.label || filter.componentIri} + {combinedDimension.label} { } control={ } /> ); @@ -275,17 +317,13 @@ const LayoutSharedFiltersConfigurator = () => { }; const SharedFilterOptions = ({ - sharedFilter, + sharedFilters, dimension, }: { - sharedFilter: SharedFilter; + sharedFilters: SharedTimeRangeFilter[]; dimension: Dimension; }) => { - if (!sharedFilter) { - return null; - } - - if (sharedFilter.type !== "timeRange") { + if (!sharedFilters.length) { return null; } @@ -298,17 +336,17 @@ const SharedFilterOptions = ({ return ( ); }; const SharedFilterOptionsTimeRange = ({ - sharedFilter, + sharedFilters, dimension, }: { - sharedFilter: SharedFilter; + sharedFilters: SharedTimeRangeFilter[]; dimension: TemporalDimension | TemporalEntityDimension; }) => { const { timeUnit, timeFormat } = dimension; @@ -327,46 +365,58 @@ const SharedFilterOptionsTimeRange = ({ const updateChartStoresFrom = useCallback( (newDate: Date) => { - const sharedFilterIri = sharedFilter.componentIri; - Object.entries(dashboardInteractiveFilters.stores).forEach( - ([chartKey, [getInteractiveFiltersState]]) => { - const { interactiveFiltersConfig } = getChartConfig(state, chartKey); - const interactiveFiltersState = getInteractiveFiltersState(); - const { from, to } = interactiveFiltersState.timeRange; - const setTimeRangeFilter = interactiveFiltersState.setTimeRange; - if ( - from && - to && - interactiveFiltersConfig?.timeRange.componentIri === sharedFilterIri - ) { - setTimeRangeFilter(newDate, to); + sharedFilters.forEach((sharedFilter) => { + const sharedFilterIri = sharedFilter.componentIri; + Object.entries(dashboardInteractiveFilters.stores).forEach( + ([chartKey, [getInteractiveFiltersState]]) => { + const { interactiveFiltersConfig } = getChartConfig( + state, + chartKey + ); + const interactiveFiltersState = getInteractiveFiltersState(); + const { from, to } = interactiveFiltersState.timeRange; + const setTimeRangeFilter = interactiveFiltersState.setTimeRange; + if ( + from && + to && + interactiveFiltersConfig?.timeRange.componentIri === + sharedFilterIri + ) { + setTimeRangeFilter(newDate, to); + } } - } - ); + ); + }); }, - [dashboardInteractiveFilters.stores, sharedFilter.componentIri, state] + [dashboardInteractiveFilters.stores, sharedFilters, state] ); const updateChartStoresTo = useCallback( (newDate: Date) => { - const sharedFilterIri = sharedFilter.componentIri; - Object.entries(dashboardInteractiveFilters.stores).forEach( - ([chartKey, [getInteractiveFiltersState]]) => { - const { interactiveFiltersConfig } = getChartConfig(state, chartKey); - const interactiveFiltersState = getInteractiveFiltersState(); - const { from, to } = interactiveFiltersState.timeRange; - const setTimeRangeFilter = interactiveFiltersState.setTimeRange; - if ( - from && - to && - interactiveFiltersConfig?.timeRange.componentIri === sharedFilterIri - ) { - setTimeRangeFilter(from, newDate); + sharedFilters.forEach((sharedFilter) => { + const sharedFilterIri = sharedFilter.componentIri; + Object.entries(dashboardInteractiveFilters.stores).forEach( + ([chartKey, [getInteractiveFiltersState]]) => { + const { interactiveFiltersConfig } = getChartConfig( + state, + chartKey + ); + const interactiveFiltersState = getInteractiveFiltersState(); + const { from, to } = interactiveFiltersState.timeRange; + const setTimeRangeFilter = interactiveFiltersState.setTimeRange; + if ( + from && + to && + interactiveFiltersConfig?.timeRange.componentIri === + sharedFilterIri + ) { + setTimeRangeFilter(from, newDate); + } } - } - ); + ); + }); }, - [dashboardInteractiveFilters.stores, sharedFilter.componentIri, state] + [dashboardInteractiveFilters.stores, sharedFilters, state] ); const handleChangeFromDate: DatePickerFieldProps["onChange"] = (ev) => { @@ -374,15 +424,18 @@ const SharedFilterOptionsTimeRange = ({ if (!newDate) { return; } - dispatch({ - type: "DASHBOARD_FILTER_UPDATE", - value: { - ...sharedFilter, - presets: { - ...sharedFilter.presets, - from: formatDate(newDate), + sharedFilters.forEach((sharedFilter) => { + dispatch({ + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", + value: { + type: "timeRange", + ...sharedFilter, + presets: { + ...sharedFilter.presets, + from: formatDate(newDate), + }, }, - }, + }); }); updateChartStoresFrom(newDate); }; @@ -390,15 +443,18 @@ const SharedFilterOptionsTimeRange = ({ const handleChangeFromGeneric: DataFilterGenericDimensionProps["onChange"] = ( ev ) => { - dispatch({ - type: "DASHBOARD_FILTER_UPDATE", - value: { - ...sharedFilter, - presets: { - ...sharedFilter.presets, - from: ev.target.value as string, + sharedFilters.forEach((sharedFilter) => { + dispatch({ + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", + value: { + type: "timeRange", + ...sharedFilter, + presets: { + ...sharedFilter.presets, + from: ev.target.value as string, + }, }, - }, + }); }); const parsedDate = parseDate(ev.target.value as string); if (parsedDate) { @@ -411,15 +467,18 @@ const SharedFilterOptionsTimeRange = ({ if (!newDate) { return; } - dispatch({ - type: "DASHBOARD_FILTER_UPDATE", - value: { - ...sharedFilter, - presets: { - ...sharedFilter.presets, - to: formatDate(newDate), + sharedFilters.forEach((sharedFilter) => { + dispatch({ + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", + value: { + type: "timeRange", + ...sharedFilter, + presets: { + ...sharedFilter.presets, + to: formatDate(newDate), + }, }, - }, + }); }); updateChartStoresTo(newDate); }; @@ -427,15 +486,18 @@ const SharedFilterOptionsTimeRange = ({ const handleChangeToGeneric: DataFilterGenericDimensionProps["onChange"] = ( ev ) => { - dispatch({ - type: "DASHBOARD_FILTER_UPDATE", - value: { - ...sharedFilter, - presets: { - ...sharedFilter.presets, - to: ev.target.value as string, + sharedFilters.forEach((sharedFilter) => { + dispatch({ + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", + value: { + type: "timeRange", + ...sharedFilter, + presets: { + ...sharedFilter.presets, + to: ev.target.value as string, + }, }, - }, + }); }); const parsedDate = parseDate(ev.target.value as string); if (parsedDate) { @@ -443,6 +505,8 @@ const SharedFilterOptionsTimeRange = ({ } }; + const sharedFilter = sharedFilters[0]; + return ( { * - Temporal dimensions will get labelled via their time unit * - If you need the dimension label in the context of a cube, pass the cube iri */ -export const getComponentLabel = (dim: Component, cubeIri?: string) => { - if (isJoinByComponent(dim)) { +export const getComponentLabel = ( + component: Component, + { cubeIri }: { cubeIri?: string } = {} +) => { + if (isJoinByComponent(component)) { const original = - cubeIri && dim.originalIris.find((i) => i.cubeIri === cubeIri); + cubeIri && component.originalIris.find((i) => i.cubeIri === cubeIri); if (original) { return original.label; } - if (dim.__typename === "TemporalDimension") { - switch (dim.timeUnit) { + if (component.__typename === "TemporalDimension") { + switch (component.timeUnit) { case TimeUnit.Year: return t({ id: `time-units.Year`, message: "Year" }); case TimeUnit.Month: @@ -339,9 +342,11 @@ export const getComponentLabel = (dim: Component, cubeIri?: string) => { return t({ id: `time-units.Second`, message: "Second" }); } } - return dim.originalIris[0].label ?? "NO LABEL"; + + return component.originalIris[0].label ?? "NO LABEL"; } - return dim.label; + + return component.label; }; /** diff --git a/app/configurator/configurator-state/actions.tsx b/app/configurator/configurator-state/actions.tsx index 9aa4feaaa..729a3e591 100644 --- a/app/configurator/configurator-state/actions.tsx +++ b/app/configurator/configurator-state/actions.tsx @@ -270,14 +270,14 @@ export type ConfiguratorStateAction = }; } | { - type: "DASHBOARD_FILTER_ADD"; - value: DashboardFiltersConfig["filters"][number]; + type: "DASHBOARD_TIME_RANGE_FILTER_ADD"; + value: DashboardFiltersConfig["timeRangeFilters"][number]; } | { - type: "DASHBOARD_FILTER_REMOVE"; - value: DashboardFiltersConfig["filters"][number]["componentIri"]; + type: "DASHBOARD_TIME_RANGE_FILTER_REMOVE"; + value: DashboardFiltersConfig["timeRangeFilters"][number]["componentIri"]; } | { - type: "DASHBOARD_FILTER_UPDATE"; - value: DashboardFiltersConfig["filters"][number]; + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE"; + value: DashboardFiltersConfig["timeRangeFilters"][number]; }; diff --git a/app/configurator/configurator-state/initial.tsx b/app/configurator/configurator-state/initial.tsx index fcfcd3cc7..b4d48dfdf 100644 --- a/app/configurator/configurator-state/initial.tsx +++ b/app/configurator/configurator-state/initial.tsx @@ -38,7 +38,7 @@ export const getInitialConfiguringConfigBasedOnCube = (props: { chartConfigs: [chartConfig], activeChartKey: chartConfig.key, dashboardFilters: { - filters: [], + timeRangeFilters: [], }, }; }; diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index 4f7c44536..db43c55bd 100644 --- a/app/configurator/configurator-state/mocks.ts +++ b/app/configurator/configurator-state/mocks.ts @@ -62,7 +62,7 @@ export const configStateMock = { ], activeChartKey: "abc", dashboardFilters: { - filters: [], + timeRangeFilters: [], }, }, groupedColumnChart: { @@ -198,7 +198,7 @@ export const configStateMock = { ], activeChartKey: "2of7iJAjccuj", dashboardFilters: { - filters: [], + timeRangeFilters: [], }, }, } satisfies Record; diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index 5b9d81878..e5c26553a 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -1057,13 +1057,13 @@ const reducer_: Reducer = ( newDraft.activeChartKey = action.value.chartKey; return newDraft; - case "DASHBOARD_FILTER_ADD": + case "DASHBOARD_TIME_RANGE_FILTER_ADD": if (isLayouting(draft)) { setWith( draft, - "dashboardFilters.filters", + "dashboardFilters.timeRangeFilters", uniqBy( - [...(draft.dashboardFilters?.filters ?? []), action.value], + [...(draft.dashboardFilters?.timeRangeFilters ?? []), action.value], (x) => x.componentIri ), Object @@ -1072,26 +1072,33 @@ const reducer_: Reducer = ( return draft; - case "DASHBOARD_FILTER_UPDATE": + case "DASHBOARD_TIME_RANGE_FILTER_UPDATE": if (isLayouting(draft)) { - const idx = draft.dashboardFilters?.filters.findIndex( + const idx = draft.dashboardFilters?.timeRangeFilters.findIndex( (f) => f.componentIri === action.value.componentIri ); if (idx !== undefined && idx > -1) { - const newFilters = [...(draft.dashboardFilters?.filters ?? [])]; + const newFilters = [ + ...(draft.dashboardFilters?.timeRangeFilters ?? []), + ]; newFilters.splice(idx, 1, action.value); - setWith(draft, "dashboardFilters.filters", newFilters, Object); + setWith( + draft, + "dashboardFilters.timeRangeFilters", + newFilters, + Object + ); } } return draft; - case "DASHBOARD_FILTER_REMOVE": + case "DASHBOARD_TIME_RANGE_FILTER_REMOVE": if (isLayouting(draft)) { - const newFilters = draft.dashboardFilters?.filters.filter( + const newFilters = draft.dashboardFilters?.timeRangeFilters.filter( (f) => f.componentIri !== action.value ); - setWith(draft, "dashboardFilters.filters", newFilters, Object); + setWith(draft, "dashboardFilters.timeRangeFilters", newFilters, Object); } return draft; diff --git a/app/configurator/interactive-filters/time-slider.tsx b/app/configurator/interactive-filters/time-slider.tsx index 49bfeac0f..bfe726f98 100644 --- a/app/configurator/interactive-filters/time-slider.tsx +++ b/app/configurator/interactive-filters/time-slider.tsx @@ -145,7 +145,9 @@ export const TimeSlider = (props: TimeSliderProps) => { }, [timelineProps]); if ( - dashboardFilters.sharedFilters.find((x) => x.componentIri === componentIri) + dashboardFilters.sharedTimeRangeFilters.find( + (x) => x.componentIri === componentIri + ) ) { return null; } diff --git a/app/docs/charts.stories.tsx b/app/docs/charts.stories.tsx index e851369fd..4926104bd 100644 --- a/app/docs/charts.stories.tsx +++ b/app/docs/charts.stories.tsx @@ -68,7 +68,7 @@ const ColumnsStory = { }, chartConfigs: [chartConfig], activeChartKey: "scatterplot", - dashboardFilters: { filters: [] }, + dashboardFilters: { timeRangeFilters: [] }, }} > @@ -98,8 +98,7 @@ const ColumnsStory = { ), }; -export { ColumnsStory as Columns }; -export { ScatterplotStory as Scatterplot }; +export { ColumnsStory as Columns, ScatterplotStory as Scatterplot }; const ScatterplotStory = { render: () => ( @@ -119,7 +118,7 @@ const ScatterplotStory = { }, chartConfigs: [chartConfig], activeChartKey: "scatterplot", - dashboardFilters: { filters: [] }, + dashboardFilters: { timeRangeFilters: [] }, }} > diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index 3907423d0..8a2f406dc 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -89,7 +89,7 @@ export const states: ConfiguratorState[] = [ ], activeChartKey: "column", dashboardFilters: { - filters: [], + timeRangeFilters: [], }, }, ]; diff --git a/app/docs/lines.stories.tsx b/app/docs/lines.stories.tsx index 08a210de6..61b2d5b52 100644 --- a/app/docs/lines.stories.tsx +++ b/app/docs/lines.stories.tsx @@ -46,7 +46,7 @@ const LineChartStory = () => ( }, chartConfigs: [chartConfig], activeChartKey: "line", - dashboardFilters: { filters: [] }, + dashboardFilters: { timeRangeFilters: [] }, }} > diff --git a/app/locales/de/messages.po b/app/locales/de/messages.po index b54424c29..515088965 100644 --- a/app/locales/de/messages.po +++ b/app/locales/de/messages.po @@ -814,6 +814,10 @@ msgstr "Layout-Optionen" msgid "controls.section.shared-filters" msgstr "Geteilte Filter" +#: app/configurator/components/layout-configurator.tsx +msgid "controls.section.shared-filters.date" +msgstr "Datum" + #: app/configurator/components/layout-configurator.tsx msgid "controls.section.shared-filters.shared-switch" msgstr "Geteilt" diff --git a/app/locales/en/messages.po b/app/locales/en/messages.po index d0f620f4d..8be5cc5d8 100644 --- a/app/locales/en/messages.po +++ b/app/locales/en/messages.po @@ -814,6 +814,10 @@ msgstr "Layout Options" msgid "controls.section.shared-filters" msgstr "Shared filters" +#: app/configurator/components/layout-configurator.tsx +msgid "controls.section.shared-filters.date" +msgstr "Date" + #: app/configurator/components/layout-configurator.tsx msgid "controls.section.shared-filters.shared-switch" msgstr "Shared" diff --git a/app/locales/fr/messages.po b/app/locales/fr/messages.po index feb429b96..8e2052702 100644 --- a/app/locales/fr/messages.po +++ b/app/locales/fr/messages.po @@ -814,6 +814,10 @@ msgstr "Options de présentation" msgid "controls.section.shared-filters" msgstr "Filtres partagés" +#: app/configurator/components/layout-configurator.tsx +msgid "controls.section.shared-filters.date" +msgstr "Date" + #: app/configurator/components/layout-configurator.tsx msgid "controls.section.shared-filters.shared-switch" msgstr "Partagé" diff --git a/app/locales/it/messages.po b/app/locales/it/messages.po index e1017c571..bc8c6ef84 100644 --- a/app/locales/it/messages.po +++ b/app/locales/it/messages.po @@ -814,6 +814,10 @@ msgstr "Opzioni di layout" msgid "controls.section.shared-filters" msgstr "Filtri condivisi" +#: app/configurator/components/layout-configurator.tsx +msgid "controls.section.shared-filters.date" +msgstr "Data" + #: app/configurator/components/layout-configurator.tsx msgid "controls.section.shared-filters.shared-switch" msgstr "Condiviso" diff --git a/app/stores/interactive-filters.tsx b/app/stores/interactive-filters.tsx index 5e974ba61..0b25eb8c9 100644 --- a/app/stores/interactive-filters.tsx +++ b/app/stores/interactive-filters.tsx @@ -1,4 +1,3 @@ -import groupBy from "lodash/groupBy"; import React, { createContext, useContext, useMemo, useRef } from "react"; import create, { StateCreator, StoreApi, UseBoundStore } from "zustand"; @@ -143,27 +142,29 @@ type InteractiveFiltersContextValue = [ StoreApi, ]; -export type SharedFilter = { - type: "timeRange"; -} & NonNullable["timeRange"]; +export type SharedTimeRangeFilter = + NonNullable["timeRange"]; -export type PotentialSharedFilter = Pick; +export type PotentialSharedTimeRangeFilter = Pick< + SharedTimeRangeFilter, + "componentIri" +>; const InteractiveFiltersContext = createContext< | { stores: Record; - potentialSharedFilters: PotentialSharedFilter[]; - sharedFilters: SharedFilter[]; + potentialSharedTimeRangeFilters: PotentialSharedTimeRangeFilter[]; + sharedTimeRangeFilters: SharedTimeRangeFilter[]; } | undefined >(undefined); /** - * Returns filters that are shared across multiple charts. + * Returns time range filters that are shared across multiple charts. */ -const getPotentialSharedFilters = ( +const getPotentialSharedTimeRangeFilters = ( chartConfigs: ChartConfig[] -): PotentialSharedFilter[] => { +): PotentialSharedTimeRangeFilter[] => { const temporalDimensions = chartConfigs.flatMap((config) => { const chartSpec = getChartSpec(config); const temporalEncodings = chartSpec.encodings.filter((x) => @@ -193,14 +194,10 @@ const getPotentialSharedFilters = ( return chartTemporalDimensions; }); - const sharedTemporalDimensions = Object.values( - groupBy(temporalDimensions, (x) => x.componentIri) - ).filter((x) => x.length > 1); - - return sharedTemporalDimensions.map((x) => { + return temporalDimensions.map((dimension) => { return { type: "timeRange", - componentIri: x[0].componentIri, + componentIri: dimension.componentIri, }; }); }; @@ -217,8 +214,8 @@ export const InteractiveFiltersProvider = ({ const [state] = useConfiguratorState(hasChartConfigs); const storeRefs = useRef>>({}); - const potentialSharedFilters = useMemo(() => { - return getPotentialSharedFilters(chartConfigs); + const potentialSharedTimeRangeFilters = useMemo(() => { + return getPotentialSharedTimeRangeFilters(chartConfigs); }, [chartConfigs]); const stores = useMemo< @@ -239,15 +236,15 @@ export const InteractiveFiltersProvider = ({ ); }, [chartConfigs]); - const sharedFilters = state.dashboardFilters?.filters; + const sharedTimeRangeFilters = state.dashboardFilters?.timeRangeFilters; const ctxValue = useMemo( () => ({ stores, - potentialSharedFilters, - sharedFilters: sharedFilters ?? [], + potentialSharedTimeRangeFilters, + sharedTimeRangeFilters: sharedTimeRangeFilters ?? [], }), - [stores, potentialSharedFilters, sharedFilters] + [stores, potentialSharedTimeRangeFilters, sharedTimeRangeFilters] ); return ( diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts index 903d5a1dc..a1477f1ca 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -931,7 +931,7 @@ export const migrateChartConfig = makeMigrate( } ); -export const CONFIGURATOR_STATE_VERSION = "3.4.0"; +export const CONFIGURATOR_STATE_VERSION = "3.5.0"; export const configuratorStateMigrations: Migration[] = [ { @@ -1232,6 +1232,31 @@ export const configuratorStateMigrations: Migration[] = [ }); }, }, + { + description: "ALL (modify dashboardFilters)", + from: "3.4.0", + to: "3.5.0", + up: (config) => { + const newConfig = { + ...config, + version: "3.5.0", + dashboardFilters: { + timeRangeFilters: config.dashboardFilters.filters, + }, + }; + return newConfig; + }, + down: (config) => { + const newConfig = { + ...config, + version: "3.4.0", + dashboardFilters: { + filters: config.dashboardFilters.timeRangeFilters, + }, + }; + return newConfig; + }, + }, ]; export const migrateConfiguratorState = makeMigrate( From 4d310a4ecb984f6bf1afebc085882b631aa99dcd Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 28 Jun 2024 11:52:26 +0200 Subject: [PATCH 05/14] fix: Datasets are tied to a chart, not shared across all charts --- .../components/add-dataset-dialog.tsx | 5 +- app/configurator/configurator-state/index.tsx | 55 +++++++++---------- app/graphql/hooks.ts | 28 ---------- 3 files changed, 30 insertions(+), 58 deletions(-) diff --git a/app/configurator/components/add-dataset-dialog.tsx b/app/configurator/components/add-dataset-dialog.tsx index 2f83fd598..f140dab4f 100644 --- a/app/configurator/components/add-dataset-dialog.tsx +++ b/app/configurator/components/add-dataset-dialog.tsx @@ -903,10 +903,10 @@ export const DatasetDialog = ({ const handleSubmit = (ev: FormEvent) => { ev.preventDefault(); - const formdata = Object.fromEntries( + const formData = Object.fromEntries( new FormData(ev.currentTarget).entries() ); - setQuery(formdata.search as string); + setQuery(formData.search as string); }; const handleClose: DialogProps["onClose"] = useEventCallback((ev, reason) => { @@ -1257,6 +1257,7 @@ const useAddDataset = () => { JSON.stringify(state) ) as ConfiguratorStateConfiguringChart; addDatasetInConfig(nextState, addDatasetOptions); + console.log("nextState", nextState); const allCubes = uniqBy( nextState.chartConfigs.flatMap((x) => x.cubes), diff --git a/app/configurator/configurator-state/index.tsx b/app/configurator/configurator-state/index.tsx index 79bba1441..a723475c0 100644 --- a/app/configurator/configurator-state/index.tsx +++ b/app/configurator/configurator-state/index.tsx @@ -264,35 +264,34 @@ export const addDatasetInConfig = function ( }; } ) { + const chartConfig = getChartConfig(config, config.activeChartKey); const { iri, joinBy } = options; - for (const chartConfig of config.chartConfigs) { - chartConfig.cubes[0].joinBy = joinBy.left; - chartConfig.cubes.push({ - iri: iri, - publishIri: iri, - joinBy: joinBy.right, - filters: {}, - }); - - // Need to go over fields, and replace any IRI part of the joinBy by "joinBy" - const { encodings } = getChartSpec(chartConfig); - const encodingAndFields = encodings.map( - (e) => - [ - e, - chartConfig.fields[e.field as keyof typeof chartConfig.fields] as any, - ] as const - ); - for (const [encoding, field] of encodingAndFields) { - if (!field) { - continue; - } - for (const iriAttribute of encoding.iriAttributes) { - const value = get(field, iriAttribute); - const index = joinBy.left.indexOf(value) ?? joinBy.right.indexOf(value); - if (index > -1) { - set(field, iriAttribute, mkJoinById(index)); - } + chartConfig.cubes[0].joinBy = joinBy.left; + chartConfig.cubes.push({ + iri: iri, + publishIri: iri, + joinBy: joinBy.right, + filters: {}, + }); + + // Need to go over fields, and replace any IRI part of the joinBy by "joinBy" + const { encodings } = getChartSpec(chartConfig); + const encodingAndFields = encodings.map( + (e) => + [ + e, + chartConfig.fields[e.field as keyof typeof chartConfig.fields] as any, + ] as const + ); + for (const [encoding, field] of encodingAndFields) { + if (!field) { + continue; + } + for (const iriAttribute of encoding.iriAttributes) { + const value = get(field, iriAttribute); + const index = joinBy.left.indexOf(value) ?? joinBy.right.indexOf(value); + if (index > -1) { + set(field, iriAttribute, mkJoinById(index)); } } } diff --git a/app/graphql/hooks.ts b/app/graphql/hooks.ts index 95c32fa73..5d6ad323a 100644 --- a/app/graphql/hooks.ts +++ b/app/graphql/hooks.ts @@ -183,14 +183,6 @@ export const executeDataCubesComponentsQuery = async ( onFetching?: () => void ) => { const { locale, sourceType, sourceUrl, cubeFilters } = variables; - - if (cubeFilters.length > 1 && !cubeFilters.every((f) => f.joinBy)) { - console.log({ cubeFilters }); - throw new Error( - "When fetching data from multiple cubes, all cube filters must have joinBy property set." - ); - } - const queries = await Promise.all( cubeFilters.map((cubeFilter) => { const cubeVariables = { @@ -255,7 +247,6 @@ export const executeDataCubesComponentsQuery = async ( dataCubeComponents !== undefined, "Undefined dataCubeComponents" ); - assert(joinBy, "Undefined joinBy"); return { dataCubeComponents, joinBy, @@ -279,16 +270,6 @@ export const useDataCubesComponentsQuery = makeUseQuery< DataCubesComponentsOptions, DataCubesComponentsData >({ - check: (variables: DataCubesComponentsOptions["variables"]) => { - const { cubeFilters } = variables; - if (cubeFilters.length > 1 && !cubeFilters.every((f) => f.joinBy)) { - console.log({ cubeFilters }); - throw new Error( - "When fetching data from multiple cubes, all cube filters must have joinBy property set." - ); - } - }, - fetch: executeDataCubesComponentsQuery, }); @@ -371,15 +352,6 @@ export const useDataCubesObservationsQuery = makeUseQuery< DataCubesObservationsOptions, DataCubesObservationsData >({ - check: (variables) => { - const { cubeFilters } = variables; - - if (cubeFilters.length > 1 && !cubeFilters.every((f) => f.joinBy)) { - throw new Error( - "When fetching data from multiple cubes, all cube filters must have joinBy property set." - ); - } - }, fetch: executeDataCubesObservationsQuery, }); From 4b49f8bc9d37d254e852143a4209ca965dc37847 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 28 Jun 2024 11:52:47 +0200 Subject: [PATCH 06/14] fix: Stale data issues --- app/configurator/components/configurator.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/configurator/components/configurator.tsx b/app/configurator/components/configurator.tsx index d93ccc0d2..fe161c60e 100644 --- a/app/configurator/components/configurator.tsx +++ b/app/configurator/components/configurator.tsx @@ -192,7 +192,9 @@ const ConfigureChartStep = () => { {chartConfig.chartType === "table" ? ( ) : ( - + // Need to use key to force re-render when switching between charts + // to fix stale data issues + )} From 08a3ea5eb49b6fac0e84507259fde3a5f5f9ab95 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 28 Jun 2024 12:44:47 +0200 Subject: [PATCH 07/14] feat: Introduce shared time range filters for all charts in the dashboard mode --- app/charts/column/chart-column.tsx | 6 +- app/charts/combo/chart-combo-line-column.tsx | 5 +- app/charts/combo/chart-combo-line-dual.tsx | 5 +- app/charts/combo/chart-combo-line-single.tsx | 5 +- app/charts/line/chart-lines.tsx | 10 +- app/charts/shared/brush/index.tsx | 12 +- app/charts/shared/chart-state.ts | 19 +- .../dashboard-interactive-filters.tsx | 128 ++--- app/config-types.ts | 12 +- .../components/add-dataset-dialog.mock.ts | 9 +- .../components/add-dataset-dialog.tsx | 1 - .../components/layout-configurator.tsx | 470 ++++++++---------- .../configurator-state/actions.tsx | 9 +- .../configurator-state/initial.tsx | 9 +- app/configurator/configurator-state/mocks.ts | 18 +- .../configurator-state/reducer.tsx | 49 +- .../interactive-filters/time-slider.tsx | 6 +- app/docs/charts.stories.tsx | 22 +- app/docs/fixtures.ts | 9 +- app/docs/lines.stories.tsx | 11 +- app/rdf/mappings.ts | 10 + app/stores/interactive-filters.tsx | 42 +- app/utils/chart-config/versioning.ts | 31 +- 23 files changed, 407 insertions(+), 491 deletions(-) diff --git a/app/charts/column/chart-column.tsx b/app/charts/column/chart-column.tsx index 8575823d2..c3e261a50 100644 --- a/app/charts/column/chart-column.tsx +++ b/app/charts/column/chart-column.tsx @@ -40,13 +40,11 @@ const ChartColumns = memo((props: ChartProps) => { const { chartConfig, dimensions } = props; const { fields, interactiveFiltersConfig } = chartConfig; const filters = useChartConfigFilters(chartConfig); - const { sharedTimeRangeFilters } = useDashboardInteractiveFilters(); - + const dashboardFilters = useDashboardInteractiveFilters(); const showTimeBrush = shouldShowBrush( interactiveFiltersConfig, - sharedTimeRangeFilters + dashboardFilters.timeRange ); - return ( <> {fields.segment?.componentIri && fields.segment.type === "stacked" ? ( diff --git a/app/charts/combo/chart-combo-line-column.tsx b/app/charts/combo/chart-combo-line-column.tsx index 1d766cb25..f6b8d5c99 100644 --- a/app/charts/combo/chart-combo-line-column.tsx +++ b/app/charts/combo/chart-combo-line-column.tsx @@ -26,8 +26,7 @@ const ChartComboLineColumn = memo( (props: ChartProps) => { const { chartConfig } = props; const { interactiveFiltersConfig } = chartConfig; - const { sharedTimeRangeFilters } = useDashboardInteractiveFilters(); - + const dashboardFilters = useDashboardInteractiveFilters(); return ( @@ -39,7 +38,7 @@ const ChartComboLineColumn = memo( {shouldShowBrush( interactiveFiltersConfig, - sharedTimeRangeFilters + dashboardFilters.timeRange ) && } diff --git a/app/charts/combo/chart-combo-line-dual.tsx b/app/charts/combo/chart-combo-line-dual.tsx index d91202cf5..f8245b0bf 100644 --- a/app/charts/combo/chart-combo-line-dual.tsx +++ b/app/charts/combo/chart-combo-line-dual.tsx @@ -25,8 +25,7 @@ export const ChartComboLineDualVisualization = ( const ChartComboLineDual = memo((props: ChartProps) => { const { chartConfig } = props; const { interactiveFiltersConfig } = chartConfig; - const { sharedTimeRangeFilters } = useDashboardInteractiveFilters(); - + const dashboardFilters = useDashboardInteractiveFilters(); return ( @@ -38,7 +37,7 @@ const ChartComboLineDual = memo((props: ChartProps) => { {shouldShowBrush( interactiveFiltersConfig, - sharedTimeRangeFilters + dashboardFilters.timeRange ) && } diff --git a/app/charts/combo/chart-combo-line-single.tsx b/app/charts/combo/chart-combo-line-single.tsx index b2dc0d4da..1ce183be3 100644 --- a/app/charts/combo/chart-combo-line-single.tsx +++ b/app/charts/combo/chart-combo-line-single.tsx @@ -26,8 +26,7 @@ const ChartComboLineSingle = memo( (props: ChartProps) => { const { chartConfig } = props; const { interactiveFiltersConfig } = chartConfig; - const { sharedTimeRangeFilters } = useDashboardInteractiveFilters(); - + const dashboardFilters = useDashboardInteractiveFilters(); return ( @@ -37,7 +36,7 @@ const ChartComboLineSingle = memo( {shouldShowBrush( interactiveFiltersConfig, - sharedTimeRangeFilters + dashboardFilters.timeRange ) && } diff --git a/app/charts/line/chart-lines.tsx b/app/charts/line/chart-lines.tsx index 6d50f3817..712fd94df 100644 --- a/app/charts/line/chart-lines.tsx +++ b/app/charts/line/chart-lines.tsx @@ -30,8 +30,7 @@ export const ChartLinesVisualization = ( const ChartLines = memo((props: ChartProps) => { const { chartConfig } = props; const { fields, interactiveFiltersConfig } = chartConfig; - const { sharedFilters } = useDashboardInteractiveFilters(); - + const dashboardFilters = useDashboardInteractiveFilters(); return ( @@ -40,9 +39,10 @@ const ChartLines = memo((props: ChartProps) => { - {shouldShowBrush(interactiveFiltersConfig, sharedFilters) && ( - - )} + {shouldShowBrush( + interactiveFiltersConfig, + dashboardFilters.timeRange + ) && } diff --git a/app/charts/shared/brush/index.tsx b/app/charts/shared/brush/index.tsx index 895fa3f0c..8e174c5c5 100644 --- a/app/charts/shared/brush/index.tsx +++ b/app/charts/shared/brush/index.tsx @@ -17,12 +17,12 @@ import { ComboLineColumnConfig, ComboLineDualConfig, ComboLineSingleConfig, + DashboardTimeRangeFilter, LineConfig, } from "@/configurator"; import { Observation } from "@/domain/data"; import { useFormatFullDateAuto } from "@/formatters"; import { - SharedTimeRangeFilter, useChartInteractiveFilters, useInteractiveFiltersGetState, } from "@/stores/interactive-filters"; @@ -44,16 +44,10 @@ export const shouldShowBrush = ( | ColumnConfig )["interactiveFiltersConfig"] | undefined, - sharedTimeRangeFilters: SharedTimeRangeFilter[] | undefined + dashboardTimeRange: DashboardTimeRangeFilter | undefined ) => { const chartTimeRange = interactiveFiltersConfig?.timeRange; - const chartTimeRangeIri = chartTimeRange?.componentIri; - const sharedFilter = sharedTimeRangeFilters?.find( - (x) => x.componentIri === chartTimeRangeIri - ); - return ( - (chartTimeRange?.active && !sharedFilter) || sharedFilter?.active == false - ); + return chartTimeRange?.active && !dashboardTimeRange?.active; }; export const BrushTime = () => { diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts index 15e6eceb4..66fb03403 100644 --- a/app/charts/shared/chart-state.ts +++ b/app/charts/shared/chart-state.ts @@ -56,7 +56,6 @@ import { isTemporalEntityDimension, } from "@/domain/data"; import { Has } from "@/domain/types"; -import { getOriginalIris, isJoinById } from "@/graphql/join"; import { ScaleType, TimeUnit } from "@/graphql/resolver-types"; import { useChartInteractiveFilters, @@ -511,23 +510,11 @@ export const useChartData = ( const interactiveToTime = timeRange.to?.getTime(); const dashboardFilters = useDashboardInteractiveFilters(); const interactiveTimeRangeFilters = useMemo(() => { - const isDashboardFilterActive = - !!dashboardFilters.sharedTimeRangeFilters.find((f) => { - const timeRangeFilterIri = interactiveTimeRange?.componentIri; - if (!timeRangeFilterIri) { - return false; - } - return isJoinById(timeRangeFilterIri) - ? getOriginalIris(timeRangeFilterIri, chartConfig).includes( - f.componentIri - ) - : f.componentIri === timeRangeFilterIri; - }); const interactiveTimeRangeFilter: ValuePredicate | null = getXAsDate && interactiveFromTime && interactiveToTime && - (interactiveTimeRange?.active || isDashboardFilterActive) + (interactiveTimeRange?.active || dashboardFilters.timeRange?.active) ? (d: Observation) => { const time = getXAsDate(d).getTime(); return time >= interactiveFromTime && time <= interactiveToTime; @@ -536,13 +523,11 @@ export const useChartData = ( return interactiveTimeRangeFilter ? [interactiveTimeRangeFilter] : []; }, [ - dashboardFilters.sharedTimeRangeFilters, + dashboardFilters.timeRange, getXAsDate, interactiveFromTime, interactiveToTime, interactiveTimeRange?.active, - interactiveTimeRange?.componentIri, - chartConfig, ]); // interactive time slider diff --git a/app/components/dashboard-interactive-filters.tsx b/app/components/dashboard-interactive-filters.tsx index 5c7daf537..666b06f00 100644 --- a/app/components/dashboard-interactive-filters.tsx +++ b/app/components/dashboard-interactive-filters.tsx @@ -9,22 +9,15 @@ import { makeStyles } from "@mui/styles"; import { useEffect, useMemo, useState } from "react"; import { - hasChartConfigs, + DashboardTimeRangeFilter, InteractiveFiltersTimeRange, - useConfiguratorState, } from "@/configurator"; import { timeUnitToFormatter, timeUnitToParser, } from "@/configurator/components/ui-helpers"; -import { canDimensionBeTimeFiltered } from "@/domain/data"; -import { useConfigsCubeComponents } from "@/graphql/hooks"; import { TimeUnit } from "@/graphql/query-hooks"; -import { useLocale } from "@/src"; -import { - SharedTimeRangeFilter, - useDashboardInteractiveFilters, -} from "@/stores/interactive-filters"; +import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; import { assert } from "@/utils/assert"; import { useTimeout } from "../hooks/use-timeout"; @@ -57,7 +50,7 @@ const valueToTimeRange = (value: number[]) => { }; const presetToTimeRange = ( - presets: InteractiveFiltersTimeRange["presets"], + presets: Pick, timeUnit: TimeUnit ) => { if (!timeUnit) { @@ -81,43 +74,27 @@ const DashboardTimeRangeSlider = ({ filter, mounted, }: { - filter: SharedTimeRangeFilter; + filter: DashboardTimeRangeFilter; mounted: boolean; }) => { const classes = useStyles(); - const dashboardInteractiveFilters = useDashboardInteractiveFilters(); - - const [state] = useConfiguratorState(hasChartConfigs); - const locale = useLocale(); - - const [data] = useConfigsCubeComponents({ - variables: { - state, - locale, - }, - }); - - const timeUnit = useMemo(() => { - const dim = data?.data?.dataCubesComponents?.dimensions?.find( - (d) => d.iri === filter.componentIri - ); - return canDimensionBeTimeFiltered(dim) ? dim.timeUnit : undefined; - }, [data?.data?.dataCubesComponents?.dimensions, filter.componentIri]); - const presets = filter.presets; assert( presets, "Filter presets should be defined when time range filter is rendered" ); + 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 || !presets) { + if (!timeUnit) { return { min: 0, max: 0 }; } const parser = timeUnitToParser[timeUnit]; @@ -133,65 +110,61 @@ const DashboardTimeRangeSlider = ({ if (!timeUnit) { return ""; } - const d = new Date(value * 1000); - return timeUnitToFormatter[timeUnit](d); + const date = new Date(value * 1000); + return timeUnitToFormatter[timeUnit](date); }); - const handleChangeSlider = useEventCallback( - (componentIri: string, value: number | number[]) => { - assert(Array.isArray(value), "Value should be an array of two numbers"); - if (!componentIri || !timeUnit) { - return; - } - const newTimeRange = valueToTimeRange(value); - if (!newTimeRange) { - return; - } - for (const [_getState, _useStore, store] of Object.values( - dashboardInteractiveFilters.stores - )) { - store.setState({ timeRange: newTimeRange }); - setTimeRange([value[0], value[1]]); - } + const handleChangeSlider = useEventCallback((value: number | number[]) => { + assert(Array.isArray(value), "Value should be an array of two numbers"); + if (!timeUnit) { + return; } - ); + const newTimeRange = valueToTimeRange(value); + if (!newTimeRange) { + return; + } + for (const [_getState, _useStore, store] of Object.values( + dashboardInteractiveFilters.stores + )) { + store.setState({ timeRange: newTimeRange }); + setTimeRange([value[0], value[1]]); + } + }); useEffect( function initTimeRangeAfterDataFetch() { if (timeRange || !timeUnit) { return; } - const parser = timeUnitToParser[timeUnit]; - handleChangeSlider(filter.componentIri, [ + handleChangeSlider([ toUnixSeconds(parser(presets.from)), toUnixSeconds(parser(presets.to)), ]); }, - [timeRange, timeUnit, presets, handleChangeSlider, filter.componentIri] + [timeRange, timeUnit, presets, handleChangeSlider] ); useEffect(() => { - if (filter.presets.from && filter.presets.to && timeUnit) { + if (presets.from && presets.to && timeUnit) { const parser = timeUnitToParser[timeUnit]; setTimeRange([ - toUnixSeconds(parser(filter.presets.from)), - toUnixSeconds(parser(filter.presets.to)), + toUnixSeconds(parser(presets.from)), + toUnixSeconds(parser(presets.to)), ]); } - }, [filter.presets.from, filter.presets.to, timeUnit]); + }, [presets.from, presets.to, timeUnit]); const mountedForSomeTime = useTimeout(500, mounted); - if (!filter || !timeRange || !filter.active) { + if (!timeRange || !filter.active) { return null; } return ( handleChangeSlider(filter.componentIri, value)} + onChange={(_ev, value) => handleChangeSlider(value)} min={min} max={max} valueLabelFormat={valueLabelFormat} @@ -204,30 +177,17 @@ const DashboardTimeRangeSlider = ({ }; export const DashboardInteractiveFilters = () => { - const dashboardInteractiveFilters = useDashboardInteractiveFilters(); - - return ( - <> - {dashboardInteractiveFilters.sharedTimeRangeFilters - .slice(1) - .map((filter) => { - if (!filter.active) { - return null; - } - - return ( - -
- -
-
- ); - })} - - ); + const { timeRange } = useDashboardInteractiveFilters(); + return timeRange?.active ? ( + +
+ +
+
+ ) : null; }; function stepFromTimeUnit(timeUnit: TimeUnit | undefined) { diff --git a/app/config-types.ts b/app/config-types.ts index 160a2d9f1..e71b84055 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -1142,23 +1142,21 @@ export type Layout = t.TypeOf; export type LayoutType = Layout["type"]; export type LayoutDashboard = Extract; -const DashboardFilterTimeRange = t.type({ - type: t.literal("timeRange"), +const DashboardTimeRangeFilter = t.type({ active: t.boolean, - componentIri: t.string, + timeUnit: t.string, presets: t.type({ - type: t.literal("range"), from: t.string, to: t.string, }), }); -export type DashboardFilterTimeRange = t.TypeOf< - typeof DashboardFilterTimeRange +export type DashboardTimeRangeFilter = t.TypeOf< + typeof DashboardTimeRangeFilter >; const DashboardFiltersConfig = t.type({ - timeRangeFilters: t.array(DashboardFilterTimeRange), + timeRange: DashboardTimeRangeFilter, }); export type DashboardFiltersConfig = t.TypeOf; diff --git a/app/configurator/components/add-dataset-dialog.mock.ts b/app/configurator/components/add-dataset-dialog.mock.ts index d6c50e430..c36b35ed4 100644 --- a/app/configurator/components/add-dataset-dialog.mock.ts +++ b/app/configurator/components/add-dataset-dialog.mock.ts @@ -101,6 +101,13 @@ export const photovoltaikChartStateMock: ConfiguratorStateConfiguringChart = { ], activeChartKey: "8-5RW138pTDA", dashboardFilters: { - timeRangeFilters: [], + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, }, }; diff --git a/app/configurator/components/add-dataset-dialog.tsx b/app/configurator/components/add-dataset-dialog.tsx index f140dab4f..76eb7741c 100644 --- a/app/configurator/components/add-dataset-dialog.tsx +++ b/app/configurator/components/add-dataset-dialog.tsx @@ -1257,7 +1257,6 @@ const useAddDataset = () => { JSON.stringify(state) ) as ConfiguratorStateConfiguringChart; addDatasetInConfig(nextState, addDatasetOptions); - console.log("nextState", nextState); const allCubes = uniqBy( nextState.chartConfigs.flatMap((x) => x.cubes), diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index e22c42fbd..2124f383e 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -8,16 +8,21 @@ import { Typography, useEventCallback, } from "@mui/material"; +import { ascending, descending } from "d3-array"; import capitalize from "lodash/capitalize"; import omit from "lodash/omit"; -import uniq from "lodash/uniq"; import uniqBy from "lodash/uniqBy"; -import { Fragment, useCallback, useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { DataFilterGenericDimensionProps } from "@/charts/shared/chart-data-filters"; import { Select } from "@/components/form"; import { generateLayout } from "@/components/react-grid"; -import { ChartConfig, getChartConfig, LayoutDashboard } from "@/config-types"; +import { + ChartConfig, + DashboardTimeRangeFilter, + getChartConfig, + LayoutDashboard, +} from "@/config-types"; import { LayoutAnnotator } from "@/configurator/components/annotators"; import { ControlSection, @@ -41,21 +46,17 @@ import { import { canDimensionBeTimeFiltered, Dimension, - isJoinByComponent, isTemporalDimensionWithTimeUnit, TemporalDimension, TemporalEntityDimension, } from "@/domain/data"; -import { truthy } from "@/domain/types"; import { useFlag } from "@/flags"; import { useTimeFormatLocale, useTimeFormatUnit } from "@/formatters"; import { useConfigsCubeComponents } from "@/graphql/hooks"; -import { timeUnitFormats } from "@/rdf/mappings"; +import { TimeUnit } from "@/graphql/resolver-types"; +import { timeUnitFormats, timeUnitOrder } from "@/rdf/mappings"; import { useLocale } from "@/src"; -import { - SharedTimeRangeFilter, - useDashboardInteractiveFilters, -} from "@/stores/interactive-filters"; +import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; import { getTimeFilterOptions } from "@/utils/time-filter-options"; export const LayoutConfigurator = () => { @@ -115,7 +116,7 @@ const LayoutLayoutConfigurator = () => { const LayoutSharedFiltersConfigurator = () => { const [state, dispatch] = useConfiguratorState(isLayouting); const { layout } = state; - const { sharedTimeRangeFilters, potentialSharedTimeRangeFilters } = + const { timeRange, potentialTimeRangeFilterIris } = useDashboardInteractiveFilters(); const locale = useLocale(); @@ -126,87 +127,96 @@ const LayoutSharedFiltersConfigurator = () => { }, }); - const { dimensions, dimensionsByIri } = useMemo(() => { - const dimensions = data?.dataCubesComponents.dimensions ?? []; - const dimensionsByIri: Record = {}; - for (const dim of dimensions) { - dimensionsByIri[dim.iri] = dim; - if (isJoinByComponent(dim)) { - for (const o of dim.originalIris) { - dimensionsByIri[o.dimensionIri] = dim; - } - } - } - return { dimensions, dimensionsByIri }; - }, [data?.dataCubesComponents.dimensions]); - - const shownFilters = potentialSharedTimeRangeFilters.filter((filter) => { - const dimension = dimensionsByIri[filter.componentIri]; - return dimension && canDimensionBeTimeFiltered(dimension); - }); - const timeUnits = uniq( - shownFilters - .map((filter) => { - const dimension = dimensionsByIri[filter.componentIri]; - return isTemporalDimensionWithTimeUnit(dimension) - ? dimension.timeUnit - : undefined; - }) - .filter(truthy) - ); - const formatLocale = useTimeFormatLocale(); const timeFormatUnit = useTimeFormatUnit(); + const combinedDimension = useMemo(() => { + const dimensions = data?.dataCubesComponents.dimensions ?? []; + 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; + }, [ + data?.dataCubesComponents.dimensions, + formatLocale, + potentialTimeRangeFilterIris, + ]); + const handleToggle: SwitchProps["onChange"] = useEventCallback( (_, checked) => { - for (const { componentIri } of shownFilters) { - const dimension = componentIri - ? dimensionsByIri[componentIri] - : undefined; - - if ( - !componentIri || - !dimension || - !canDimensionBeTimeFiltered(dimension) - ) { - return; - } - - if (checked) { - const options = getTimeFilterOptions({ - dimension: dimension, - formatLocale, - timeFormatUnit, - }); + if (checked) { + const options = getTimeFilterOptions({ + dimension: combinedDimension, + formatLocale, + timeFormatUnit, + }); - const from = options.sortedOptions[0].date; - const to = options.sortedOptions.at(-1)?.date; - const dateFormatter = timeUnitToFormatter[dimension.timeUnit]; + const from = options.sortedOptions[0].date; + const to = options.sortedOptions.at(-1)?.date; + const formatDate = timeUnitToFormatter[combinedDimension.timeUnit]; - if (!from || !to) { - return; - } + if (!from || !to) { + return; + } - dispatch({ - type: "DASHBOARD_TIME_RANGE_FILTER_ADD", - value: { - type: "timeRange", - active: true, - presets: { - type: "range", - from: dateFormatter(from), - to: dateFormatter(to), - }, - componentIri: componentIri, + dispatch({ + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", + value: { + active: true, + timeUnit: combinedDimension.timeUnit, + presets: { + from: formatDate(from), + to: formatDate(to), }, - }); - } else { - dispatch({ - type: "DASHBOARD_TIME_RANGE_FILTER_REMOVE", - value: componentIri, - }); - } + }, + }); + } else { + dispatch({ + type: "DASHBOARD_TIME_RANGE_FILTER_REMOVE", + }); } } ); @@ -214,7 +224,7 @@ const LayoutSharedFiltersConfigurator = () => { switch (layout.type) { case "tab": case "dashboard": - if (!timeUnits.length) { + if (!timeRange || potentialTimeRangeFilterIris.length === 0) { return null; } @@ -233,80 +243,40 @@ const LayoutSharedFiltersConfigurator = () => { - {timeUnits.map((timeUnit) => { - const timeUnitDimensions = dimensions.filter( - (dimension) => - isTemporalDimensionWithTimeUnit(dimension) && - dimension.timeUnit === timeUnit - ) as TemporalDimension[]; - const timeFormat = timeUnitFormats.get(timeUnit); - - if (!timeUnitDimensions.length || !timeFormat) { - return null; - } - - const values = timeUnitDimensions.flatMap( - (dimension) => dimension.values - ); - const combinedDimension: TemporalDimension = { - __typename: "TemporalDimension", - cubeIri: "all", - iri: "combined", - label: t({ - id: "controls.section.shared-filters.date", - message: "Date", - }), - isKeyDimension: true, - isNumerical: false, - values: uniqBy(values, "value").sort((a, b) => - (a.value as string).localeCompare(b.value as string) - ), - timeUnit, - timeFormat, - }; - - return ( - - - - {combinedDimension.label} - - - - Shared - - - } - control={ - - } - /> - - + + {combinedDimension.label} + + + + Shared + + + } + control={ + - - ); - })} + } + /> + + {timeRange.active ? ( + + ) : null}
@@ -316,14 +286,14 @@ const LayoutSharedFiltersConfigurator = () => { } }; -const SharedFilterOptions = ({ - sharedFilters, +const DashboardFiltersOptions = ({ + timeRangeFilter, dimension, }: { - sharedFilters: SharedTimeRangeFilter[]; + timeRangeFilter: DashboardTimeRangeFilter | undefined; dimension: Dimension; }) => { - if (!sharedFilters.length) { + if (!timeRangeFilter) { return null; } @@ -335,18 +305,18 @@ const SharedFilterOptions = ({ } return ( - ); }; -const SharedFilterOptionsTimeRange = ({ - sharedFilters, +const DashboardTimeRangeFilterOptions = ({ + timeRangeFilter, dimension, }: { - sharedFilters: SharedTimeRangeFilter[]; + timeRangeFilter: DashboardTimeRangeFilter; dimension: TemporalDimension | TemporalEntityDimension; }) => { const { timeUnit, timeFormat } = dimension; @@ -365,58 +335,36 @@ const SharedFilterOptionsTimeRange = ({ const updateChartStoresFrom = useCallback( (newDate: Date) => { - sharedFilters.forEach((sharedFilter) => { - const sharedFilterIri = sharedFilter.componentIri; - Object.entries(dashboardInteractiveFilters.stores).forEach( - ([chartKey, [getInteractiveFiltersState]]) => { - const { interactiveFiltersConfig } = getChartConfig( - state, - chartKey - ); - const interactiveFiltersState = getInteractiveFiltersState(); - const { from, to } = interactiveFiltersState.timeRange; - const setTimeRangeFilter = interactiveFiltersState.setTimeRange; - if ( - from && - to && - interactiveFiltersConfig?.timeRange.componentIri === - sharedFilterIri - ) { - setTimeRangeFilter(newDate, to); - } + Object.entries(dashboardInteractiveFilters.stores).forEach( + ([chartKey, [getInteractiveFiltersState]]) => { + const { interactiveFiltersConfig } = getChartConfig(state, chartKey); + const interactiveFiltersState = getInteractiveFiltersState(); + const { from, to } = interactiveFiltersState.timeRange; + const setTimeRangeFilter = interactiveFiltersState.setTimeRange; + if (from && to && interactiveFiltersConfig?.timeRange.componentIri) { + setTimeRangeFilter(newDate, to); } - ); - }); + } + ); }, - [dashboardInteractiveFilters.stores, sharedFilters, state] + [dashboardInteractiveFilters.stores, state] ); const updateChartStoresTo = useCallback( (newDate: Date) => { - sharedFilters.forEach((sharedFilter) => { - const sharedFilterIri = sharedFilter.componentIri; - Object.entries(dashboardInteractiveFilters.stores).forEach( - ([chartKey, [getInteractiveFiltersState]]) => { - const { interactiveFiltersConfig } = getChartConfig( - state, - chartKey - ); - const interactiveFiltersState = getInteractiveFiltersState(); - const { from, to } = interactiveFiltersState.timeRange; - const setTimeRangeFilter = interactiveFiltersState.setTimeRange; - if ( - from && - to && - interactiveFiltersConfig?.timeRange.componentIri === - sharedFilterIri - ) { - setTimeRangeFilter(from, newDate); - } + Object.entries(dashboardInteractiveFilters.stores).forEach( + ([chartKey, [getInteractiveFiltersState]]) => { + const { interactiveFiltersConfig } = getChartConfig(state, chartKey); + const interactiveFiltersState = getInteractiveFiltersState(); + const { from, to } = interactiveFiltersState.timeRange; + const setTimeRangeFilter = interactiveFiltersState.setTimeRange; + if (from && to && interactiveFiltersConfig?.timeRange.componentIri) { + setTimeRangeFilter(from, newDate); } - ); - }); + } + ); }, - [dashboardInteractiveFilters.stores, sharedFilters, state] + [dashboardInteractiveFilters.stores, state] ); const handleChangeFromDate: DatePickerFieldProps["onChange"] = (ev) => { @@ -424,18 +372,15 @@ const SharedFilterOptionsTimeRange = ({ if (!newDate) { return; } - sharedFilters.forEach((sharedFilter) => { - dispatch({ - type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", - value: { - type: "timeRange", - ...sharedFilter, - presets: { - ...sharedFilter.presets, - from: formatDate(newDate), - }, + dispatch({ + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", + value: { + ...timeRangeFilter, + presets: { + ...timeRangeFilter.presets, + from: formatDate(newDate), }, - }); + }, }); updateChartStoresFrom(newDate); }; @@ -443,18 +388,15 @@ const SharedFilterOptionsTimeRange = ({ const handleChangeFromGeneric: DataFilterGenericDimensionProps["onChange"] = ( ev ) => { - sharedFilters.forEach((sharedFilter) => { - dispatch({ - type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", - value: { - type: "timeRange", - ...sharedFilter, - presets: { - ...sharedFilter.presets, - from: ev.target.value as string, - }, + dispatch({ + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", + value: { + ...timeRangeFilter, + presets: { + ...timeRangeFilter.presets, + from: ev.target.value as string, }, - }); + }, }); const parsedDate = parseDate(ev.target.value as string); if (parsedDate) { @@ -467,18 +409,15 @@ const SharedFilterOptionsTimeRange = ({ if (!newDate) { return; } - sharedFilters.forEach((sharedFilter) => { - dispatch({ - type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", - value: { - type: "timeRange", - ...sharedFilter, - presets: { - ...sharedFilter.presets, - to: formatDate(newDate), - }, + dispatch({ + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", + value: { + ...timeRangeFilter, + presets: { + ...timeRangeFilter.presets, + to: formatDate(newDate), }, - }); + }, }); updateChartStoresTo(newDate); }; @@ -486,18 +425,15 @@ const SharedFilterOptionsTimeRange = ({ const handleChangeToGeneric: DataFilterGenericDimensionProps["onChange"] = ( ev ) => { - sharedFilters.forEach((sharedFilter) => { - dispatch({ - type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", - value: { - type: "timeRange", - ...sharedFilter, - presets: { - ...sharedFilter.presets, - to: ev.target.value as string, - }, + dispatch({ + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", + value: { + ...timeRangeFilter, + presets: { + ...timeRangeFilter.presets, + to: ev.target.value as string, }, - }); + }, }); const parsedDate = parseDate(ev.target.value as string); if (parsedDate) { @@ -505,10 +441,8 @@ const SharedFilterOptionsTimeRange = ({ } }; - const sharedFilter = sharedFilters[0]; - return ( - +
{canRenderDatePickerField(timeUnit) ? ( !optionValues.includes(formatDate(d))} + isDateDisabled={(date) => !optionValues.includes(formatDate(date))} timeUnit={timeUnit} dateFormat={formatDate} minDate={minDate} @@ -533,19 +467,19 @@ const SharedFilterOptionsTimeRange = ({ /> ) : ( )} - +
); }; diff --git a/app/configurator/configurator-state/actions.tsx b/app/configurator/configurator-state/actions.tsx index 729a3e591..61fec710e 100644 --- a/app/configurator/configurator-state/actions.tsx +++ b/app/configurator/configurator-state/actions.tsx @@ -270,14 +270,9 @@ export type ConfiguratorStateAction = }; } | { - type: "DASHBOARD_TIME_RANGE_FILTER_ADD"; - value: DashboardFiltersConfig["timeRangeFilters"][number]; + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE"; + value: DashboardFiltersConfig["timeRange"]; } | { type: "DASHBOARD_TIME_RANGE_FILTER_REMOVE"; - value: DashboardFiltersConfig["timeRangeFilters"][number]["componentIri"]; - } - | { - type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE"; - value: DashboardFiltersConfig["timeRangeFilters"][number]; }; diff --git a/app/configurator/configurator-state/initial.tsx b/app/configurator/configurator-state/initial.tsx index b4d48dfdf..a2a060f0d 100644 --- a/app/configurator/configurator-state/initial.tsx +++ b/app/configurator/configurator-state/initial.tsx @@ -38,7 +38,14 @@ export const getInitialConfiguringConfigBasedOnCube = (props: { chartConfigs: [chartConfig], activeChartKey: chartConfig.key, dashboardFilters: { - timeRangeFilters: [], + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, }, }; }; diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index db43c55bd..c3d53a91b 100644 --- a/app/configurator/configurator-state/mocks.ts +++ b/app/configurator/configurator-state/mocks.ts @@ -62,7 +62,14 @@ export const configStateMock = { ], activeChartKey: "abc", dashboardFilters: { - timeRangeFilters: [], + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, }, }, groupedColumnChart: { @@ -198,7 +205,14 @@ export const configStateMock = { ], activeChartKey: "2of7iJAjccuj", dashboardFilters: { - timeRangeFilters: [], + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, }, }, } satisfies Record; diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index e5c26553a..41b6aad01 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -4,7 +4,6 @@ import get from "lodash/get"; import isEqual from "lodash/isEqual"; import setWith from "lodash/setWith"; import sortBy from "lodash/sortBy"; -import uniqBy from "lodash/uniqBy"; import unset from "lodash/unset"; import { Reducer } from "use-immer"; @@ -26,6 +25,7 @@ import { ColorMapping, ColumnStyleCategory, ConfiguratorState, + DashboardTimeRangeFilter, enableLayouting, Filters, GenericField, @@ -1057,48 +1057,27 @@ const reducer_: Reducer = ( newDraft.activeChartKey = action.value.chartKey; return newDraft; - case "DASHBOARD_TIME_RANGE_FILTER_ADD": - if (isLayouting(draft)) { - setWith( - draft, - "dashboardFilters.timeRangeFilters", - uniqBy( - [...(draft.dashboardFilters?.timeRangeFilters ?? []), action.value], - (x) => x.componentIri - ), - Object - ); - } - - return draft; - case "DASHBOARD_TIME_RANGE_FILTER_UPDATE": if (isLayouting(draft)) { - const idx = draft.dashboardFilters?.timeRangeFilters.findIndex( - (f) => f.componentIri === action.value.componentIri - ); - - if (idx !== undefined && idx > -1) { - const newFilters = [ - ...(draft.dashboardFilters?.timeRangeFilters ?? []), - ]; - newFilters.splice(idx, 1, action.value); - setWith( - draft, - "dashboardFilters.timeRangeFilters", - newFilters, - Object - ); - } + setWith(draft, "dashboardFilters.timeRange", action.value, Object); } return draft; case "DASHBOARD_TIME_RANGE_FILTER_REMOVE": if (isLayouting(draft)) { - const newFilters = draft.dashboardFilters?.timeRangeFilters.filter( - (f) => f.componentIri !== action.value + setWith( + draft, + "dashboardFilters.timeRange", + { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + } as DashboardTimeRangeFilter, + Object ); - setWith(draft, "dashboardFilters.timeRangeFilters", newFilters, Object); } return draft; diff --git a/app/configurator/interactive-filters/time-slider.tsx b/app/configurator/interactive-filters/time-slider.tsx index bfe726f98..e2433b5e6 100644 --- a/app/configurator/interactive-filters/time-slider.tsx +++ b/app/configurator/interactive-filters/time-slider.tsx @@ -144,11 +144,7 @@ export const TimeSlider = (props: TimeSliderProps) => { return new Timeline(timelineProps); }, [timelineProps]); - if ( - dashboardFilters.sharedTimeRangeFilters.find( - (x) => x.componentIri === componentIri - ) - ) { + if (dashboardFilters.timeRange?.active) { return null; } diff --git a/app/docs/charts.stories.tsx b/app/docs/charts.stories.tsx index 4926104bd..e77d14103 100644 --- a/app/docs/charts.stories.tsx +++ b/app/docs/charts.stories.tsx @@ -68,7 +68,16 @@ const ColumnsStory = { }, chartConfigs: [chartConfig], activeChartKey: "scatterplot", - dashboardFilters: { timeRangeFilters: [] }, + dashboardFilters: { + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, + }, }} > @@ -118,7 +127,16 @@ const ScatterplotStory = { }, chartConfigs: [chartConfig], activeChartKey: "scatterplot", - dashboardFilters: { timeRangeFilters: [] }, + dashboardFilters: { + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, + }, }} > diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index 8a2f406dc..73b8f5b13 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -89,7 +89,14 @@ export const states: ConfiguratorState[] = [ ], activeChartKey: "column", dashboardFilters: { - timeRangeFilters: [], + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, }, }, ]; diff --git a/app/docs/lines.stories.tsx b/app/docs/lines.stories.tsx index 61b2d5b52..ba872ebd3 100644 --- a/app/docs/lines.stories.tsx +++ b/app/docs/lines.stories.tsx @@ -46,7 +46,16 @@ const LineChartStory = () => ( }, chartConfigs: [chartConfig], activeChartKey: "line", - dashboardFilters: { timeRangeFilters: [] }, + dashboardFilters: { + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, + }, }} > diff --git a/app/rdf/mappings.ts b/app/rdf/mappings.ts index a23dce18b..839a64a23 100644 --- a/app/rdf/mappings.ts +++ b/app/rdf/mappings.ts @@ -32,3 +32,13 @@ export const timeUnitFormats = new Map([ [TimeUnit.Minute, "%Y-%m-%dT%H:%M"], [TimeUnit.Second, "%Y-%m-%dT%H:%M:%S"], ]); + +export const timeUnitOrder = new Map([ + [TimeUnit.Year, 0], + [TimeUnit.Month, 1], + [TimeUnit.Week, 2], + [TimeUnit.Day, 3], + [TimeUnit.Hour, 4], + [TimeUnit.Minute, 5], + [TimeUnit.Second, 6], +]); diff --git a/app/stores/interactive-filters.tsx b/app/stores/interactive-filters.tsx index 0b25eb8c9..3659bcc46 100644 --- a/app/stores/interactive-filters.tsx +++ b/app/stores/interactive-filters.tsx @@ -5,17 +5,17 @@ import { getChartSpec } from "@/charts/chart-config-ui-options"; import { CalculationType, ChartConfig, + DashboardTimeRangeFilter, FilterValueSingle, hasChartConfigs, - InteractiveFiltersConfig, useConfiguratorState, } from "@/configurator"; import { truthy } from "@/domain/types"; import { getOriginalIris, isJoinById } from "@/graphql/join"; import { - createBoundUseStoreWithSelector, ExtractState, UseBoundStoreWithSelector, + createBoundUseStoreWithSelector, } from "@/stores/utils"; import { assert } from "@/utils/assert"; @@ -142,29 +142,16 @@ type InteractiveFiltersContextValue = [ StoreApi, ]; -export type SharedTimeRangeFilter = - NonNullable["timeRange"]; - -export type PotentialSharedTimeRangeFilter = Pick< - SharedTimeRangeFilter, - "componentIri" ->; - const InteractiveFiltersContext = createContext< | { + potentialTimeRangeFilterIris: string[]; + timeRange: DashboardTimeRangeFilter | undefined; stores: Record; - potentialSharedTimeRangeFilters: PotentialSharedTimeRangeFilter[]; - sharedTimeRangeFilters: SharedTimeRangeFilter[]; } | undefined >(undefined); -/** - * Returns time range filters that are shared across multiple charts. - */ -const getPotentialSharedTimeRangeFilters = ( - chartConfigs: ChartConfig[] -): PotentialSharedTimeRangeFilter[] => { +const getPotentialTimeRangeFilterIris = (chartConfigs: ChartConfig[]) => { const temporalDimensions = chartConfigs.flatMap((config) => { const chartSpec = getChartSpec(config); const temporalEncodings = chartSpec.encodings.filter((x) => @@ -194,12 +181,7 @@ const getPotentialSharedTimeRangeFilters = ( return chartTemporalDimensions; }); - return temporalDimensions.map((dimension) => { - return { - type: "timeRange", - componentIri: dimension.componentIri, - }; - }); + return temporalDimensions.map((dimension) => dimension.componentIri); }; /** @@ -214,8 +196,8 @@ export const InteractiveFiltersProvider = ({ const [state] = useConfiguratorState(hasChartConfigs); const storeRefs = useRef>>({}); - const potentialSharedTimeRangeFilters = useMemo(() => { - return getPotentialSharedTimeRangeFilters(chartConfigs); + const potentialTimeRangeFilterIris = useMemo(() => { + return getPotentialTimeRangeFilterIris(chartConfigs); }, [chartConfigs]); const stores = useMemo< @@ -236,15 +218,15 @@ export const InteractiveFiltersProvider = ({ ); }, [chartConfigs]); - const sharedTimeRangeFilters = state.dashboardFilters?.timeRangeFilters; + const timeRange = state.dashboardFilters?.timeRange; const ctxValue = useMemo( () => ({ stores, - potentialSharedTimeRangeFilters, - sharedTimeRangeFilters: sharedTimeRangeFilters ?? [], + potentialTimeRangeFilterIris, + timeRange, }), - [stores, potentialSharedTimeRangeFilters, sharedTimeRangeFilters] + [stores, potentialTimeRangeFilterIris, timeRange] ); return ( diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts index a1477f1ca..9aa78f40a 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -1237,21 +1237,48 @@ export const configuratorStateMigrations: Migration[] = [ from: "3.4.0", to: "3.5.0", up: (config) => { + const oldTimeRangeFilter = config.dashboardFilters.filters[0]; const newConfig = { ...config, version: "3.5.0", dashboardFilters: { - timeRangeFilters: config.dashboardFilters.filters, + timeRange: { + active: oldTimeRangeFilter?.active ?? false, + timeUnit: "", + presets: { + from: oldTimeRangeFilter?.from ?? "", + to: oldTimeRangeFilter?.to ?? "", + }, + } ?? { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, }, }; return newConfig; }, down: (config) => { + const oldTimeRangeFilter = config.dashboardFilters.timeRange; const newConfig = { ...config, version: "3.4.0", dashboardFilters: { - filters: config.dashboardFilters.timeRangeFilters, + filters: [ + { + type: "timeRange", + active: oldTimeRangeFilter.active, + componentIri: "", + presets: { + type: "range", + from: oldTimeRangeFilter.presets.from, + to: oldTimeRangeFilter.presets.to, + }, + }, + ], }, }; return newConfig; From aa57c056f0518178356299bbb5430f7b31ff9794 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 28 Jun 2024 12:50:49 +0200 Subject: [PATCH 08/14] docs: Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffb422bfe..2cdad6e70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,11 @@ You can also check the eggs in the application - Published charts now have a "more options" button with two actions - "Share" and "Table view" + - Shared dashboard time range filters are now rendered as one `Date` filter + that can be used to filter all charts in the dashboard. Date range is + combined from all charts in the dashboard and the "lowest" time unit is used + in the time slider (e.g. if one chart has a year resolution and another has + a month resolution, the time slider will show months) - Fixes - Fixed using a time range brush in column charts when X dimension is a `TemporalEntityDimension` @@ -49,6 +54,9 @@ You can also check the several charts e.g. in a dashboard - Sub-theme filters now work correctly both in the search page and in the dataset selection modal when adding a new chart based on another cube + - Changing dashboard time range filter presets now correctly updates the + charts + - Merged cubes are now done on a chart basis and are not shared between charts - Style - Cleaned up the chart footnotes and moved most of them to the metadata panel – it means that the footnotes don't look broken anymore for charts based on @@ -58,6 +66,8 @@ You can also check the them - Updated style of the buttons that open metadata panel - Data download menu now opens on click, not hover + - Removed unnecessary row gaps in the dashboard layout when e.g. title or + description is missing # [4.6.1] - 2024-06-05 From 69fb5ed578b464f1b08dabe0c1fcbebdfbc6a030 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 28 Jun 2024 13:37:05 +0200 Subject: [PATCH 09/14] fix: Removing datasets --- app/configurator/components/configurator.tsx | 7 +++++-- app/configurator/configurator-state/reducer.tsx | 2 +- app/graphql/hooks.ts | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/configurator/components/configurator.tsx b/app/configurator/components/configurator.tsx index fe161c60e..e547a917d 100644 --- a/app/configurator/components/configurator.tsx +++ b/app/configurator/components/configurator.tsx @@ -193,8 +193,11 @@ const ConfigureChartStep = () => { ) : ( // Need to use key to force re-render when switching between charts - // to fix stale data issues - + // or adding / removing cubes to fix stale data issues + )} diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index 41b6aad01..7589b09c8 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -761,7 +761,7 @@ const reducer_: Reducer = ( }; } else { console.warn( - `Could not set filter, no cube in chat config was found with iri ${cubeIri}` + `Could not set filter, no cube in chart config was found with iri ${cubeIri}` ); } } diff --git a/app/graphql/hooks.ts b/app/graphql/hooks.ts index 5d6ad323a..c57e2e936 100644 --- a/app/graphql/hooks.ts +++ b/app/graphql/hooks.ts @@ -224,7 +224,7 @@ export const executeDataCubesComponentsQuery = async ( } const { dimensions: firstDimensions = [], measures: firstMeasures = [] } = - queries[0].data?.dataCubeComponents || {}; + queries[0]?.data?.dataCubeComponents || {}; assert(firstDimensions !== undefined, "Undefined dimensions"); assert(firstMeasures !== undefined, "Undefined measures"); @@ -331,7 +331,7 @@ export const executeDataCubesObservationsQuery = async ( queries.length > 1 ? mergeObservations(queries) : // If we are fetching data from a single cube, we can just return the data - queries[0].data?.dataCubeObservations?.data!; + queries[0]?.data?.dataCubeObservations?.data!; return { data: { From 027d56178eaa2a713be7047e8b90785fddb43310 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 28 Jun 2024 15:19:09 +0200 Subject: [PATCH 10/14] feat: Keep old chart type when possible after removing a cube --- app/configurator/configurator-state/reducer.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index 7589b09c8..7ab152af6 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -948,7 +948,9 @@ const reducer_: Reducer = ( cubeCount: iris.length, }); const initialConfig = getInitialConfig({ - chartType: possibleChartTypes[0], + chartType: possibleChartTypes.includes(chartConfig.chartType) + ? chartConfig.chartType + : possibleChartTypes[0], iris, dimensions, measures, From 793347ceb313b7e2a67131e6bf43295e3eb8f1fb Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 28 Jun 2024 17:02:53 +0200 Subject: [PATCH 11/14] fix: Pre-load cache per active chart, not every chart --- .../components/add-dataset-dialog.tsx | 15 +++-- .../configurator-state/reducer.spec.tsx | 8 +-- .../configurator-state/reducer.tsx | 61 +++++++++---------- app/urql-cache.tsx | 18 +++--- 4 files changed, 52 insertions(+), 50 deletions(-) diff --git a/app/configurator/components/add-dataset-dialog.tsx b/app/configurator/components/add-dataset-dialog.tsx index 76eb7741c..989e5c027 100644 --- a/app/configurator/components/add-dataset-dialog.tsx +++ b/app/configurator/components/add-dataset-dialog.tsx @@ -48,7 +48,6 @@ import groupBy from "lodash/groupBy"; import keyBy from "lodash/keyBy"; import maxBy from "lodash/maxBy"; import uniq from "lodash/uniq"; -import uniqBy from "lodash/uniqBy"; import { FormEvent, useCallback, useEffect, useMemo, useState } from "react"; import { useClient } from "urql"; @@ -61,6 +60,7 @@ import { addDatasetInConfig, ConfiguratorStateConfiguringChart, DataSource, + getChartConfig, isConfiguring, useConfiguratorState, } from "@/configurator"; @@ -1257,18 +1257,16 @@ const useAddDataset = () => { JSON.stringify(state) ) as ConfiguratorStateConfiguringChart; addDatasetInConfig(nextState, addDatasetOptions); + const chartConfig = getChartConfig(nextState, state.activeChartKey); - const allCubes = uniqBy( - nextState.chartConfigs.flatMap((x) => x.cubes), - (x) => x.iri - ); const res = await executeDataCubesComponentsQuery(client, { - locale: locale, + locale, sourceType, sourceUrl, - cubeFilters: allCubes.map((cube) => ({ + cubeFilters: chartConfig.cubes.map((cube) => ({ iri: cube.iri, joinBy: cube.joinBy, + componentIris: undefined, loadValues: true, })), }); @@ -1276,6 +1274,7 @@ const useAddDataset = () => { if (res.error || !res.data) { throw new Error("Could not fetch dimensions and measures"); } + dispatch({ type: "DATASET_ADD", value: addDatasetOptions, @@ -1284,7 +1283,7 @@ const useAddDataset = () => { const possibleType = getPossibleChartTypes({ dimensions: dimensions, measures: measures, - cubeCount: allCubes.length, + cubeCount: chartConfig.cubes.length, }); dispatch({ type: "CHART_TYPE_CHANGED", diff --git a/app/configurator/configurator-state/reducer.spec.tsx b/app/configurator/configurator-state/reducer.spec.tsx index c9d446623..0606cc87c 100644 --- a/app/configurator/configurator-state/reducer.spec.tsx +++ b/app/configurator/configurator-state/reducer.spec.tsx @@ -127,12 +127,12 @@ describe("add dataset", () => { addAction ) as ConfiguratorStatePublishing; - getCachedComponents.mockImplementation((_, cubes) => { + getCachedComponents.mockImplementation((options) => { // TODO Cubes join by need to be reset at the moment, will change // when we have more than 2 cubes - expect(cubes.map((x) => x.joinBy).every((x) => x === undefined)).toBe( - true - ); + expect( + options.cubeFilters.map((x) => x.joinBy).every((x) => x === undefined) + ).toBe(true); return getCachedComponentsMock.electricyPricePerCantonDimensions; }); const newState2 = runReducer( diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index 7ab152af6..63f57a570 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -361,14 +361,14 @@ export const handleChartFieldChanged = ( selectedValues: actionSelectedValues, } = action.value; const f = get(chartConfig.fields, field); - const dataCubesComponents = getCachedComponents( - draft.dataSource, - chartConfig.cubes.map((cube) => ({ + const dataCubesComponents = getCachedComponents({ + locale, + dataSource: draft.dataSource, + cubeFilters: chartConfig.cubes.map((cube) => ({ iri: cube.iri, joinBy: cube.joinBy, })), - locale - ); + }); const dimensions = dataCubesComponents?.dimensions ?? []; const measures = dataCubesComponents?.measures ?? []; const components = [...dimensions, ...measures]; @@ -418,14 +418,14 @@ export const handleChartOptionChanged = ( const { locale, path, field, value } = action.value; const chartConfig = getChartConfig(draft); const updatePath = field === null ? path : `fields["${field}"].${path}`; - const dataCubesComponents = getCachedComponents( - draft.dataSource, - chartConfig.cubes.map((cube) => ({ + const dataCubesComponents = getCachedComponents({ + locale, + dataSource: draft.dataSource, + cubeFilters: chartConfig.cubes.map((cube) => ({ iri: cube.iri, joinBy: cube.joinBy, })), - locale - ); + }); const dimensions = dataCubesComponents?.dimensions ?? []; const measures = dataCubesComponents?.measures ?? []; @@ -575,14 +575,14 @@ const reducer_: Reducer = ( if (isConfiguring(draft)) { const { locale, chartKey, chartType } = action.value; const chartConfig = getChartConfig(draft, chartKey); - const dataCubesComponents = getCachedComponents( - draft.dataSource, - chartConfig.cubes.map((cube) => ({ + const dataCubesComponents = getCachedComponents({ + locale, + dataSource: draft.dataSource, + cubeFilters: chartConfig.cubes.map((cube) => ({ iri: cube.iri, joinBy: cube.joinBy, })), - locale - ); + }); const dimensions = dataCubesComponents?.dimensions; const measures = dataCubesComponents?.measures; @@ -621,14 +621,14 @@ const reducer_: Reducer = ( if (isConfiguring(draft)) { const chartConfig = getChartConfig(draft); delete (chartConfig.fields as GenericFields)[action.value.field]; - const dataCubesComponents = getCachedComponents( - draft.dataSource, - chartConfig.cubes.map((cube) => ({ + const dataCubesComponents = getCachedComponents({ + locale: action.value.locale, + dataSource: draft.dataSource, + cubeFilters: chartConfig.cubes.map((cube) => ({ iri: cube.iri, joinBy: cube.joinBy, })), - action.value.locale - ); + }); const dimensions = dataCubesComponents?.dimensions ?? []; const newConfig = deriveFiltersFromFields(chartConfig, { dimensions }); const index = draft.chartConfigs.findIndex( @@ -872,14 +872,14 @@ const reducer_: Reducer = ( if (isConfiguring(draft)) { const chartConfig = createDraft(action.value.chartConfig) ?? getChartConfig(draft); - const dataCubesComponents = getCachedComponents( - draft.dataSource, - chartConfig.cubes.map((cube) => ({ + const dataCubesComponents = getCachedComponents({ + locale: action.value.locale, + dataSource: draft.dataSource, + cubeFilters: chartConfig.cubes.map((cube) => ({ iri: cube.iri, joinBy: cube.joinBy, })), - action.value.locale - ); + }); if (dataCubesComponents) { const cubes = current(chartConfig.cubes); @@ -922,15 +922,14 @@ const reducer_: Reducer = ( const newCubes = chartConfig.cubes.filter( (c) => c.iri !== removedCubeIri ); - const dataCubesComponents = getCachedComponents( - draft.dataSource, - newCubes.map((cube) => ({ + const dataCubesComponents = getCachedComponents({ + locale, + dataSource: draft.dataSource, + cubeFilters: newCubes.map((cube) => ({ iri: cube.iri, - // Only keep joinBy while we have more than one cube joinBy: newCubes.length > 1 ? cube.joinBy : undefined, })), - locale - ); + }); if (!dataCubesComponents) { throw new Error( diff --git a/app/urql-cache.tsx b/app/urql-cache.tsx index e43e4c811..58901bbf4 100644 --- a/app/urql-cache.tsx +++ b/app/urql-cache.tsx @@ -15,27 +15,31 @@ import { Locale } from "@/locales/locales"; * Reads components from client cache, reading cube by cube. * Components are not joined in cache, but transformed here. */ -export const getCachedComponents = ( - dataSource: DataSource, - cubeFilters: DataCubeComponentFilter[], - locale: Locale -): DataCubeComponents | undefined => { +export const getCachedComponents = ({ + locale, + dataSource, + cubeFilters, +}: { + locale: Locale; + dataSource: DataSource; + cubeFilters: DataCubeComponentFilter[]; +}): DataCubeComponents | undefined => { const queries = cubeFilters .map((cubeFilter) => { return client.readQuery< DataCubeComponentsQuery, DataCubeComponentsQueryVariables >(DataCubeComponentsDocument, { + locale, sourceType: dataSource.type, sourceUrl: dataSource.url, - locale, cubeFilter: { iri: cubeFilter.iri, componentIris: undefined, joinBy: cubeFilter.joinBy, loadValues: true, }, - })!; + }); }) .filter(truthy); return { From 1368cfdd5f5d27cbbd0ec1f1cd2e7978b12c9bb4 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 28 Jun 2024 17:24:04 +0200 Subject: [PATCH 12/14] fix: Properly handle removal of datasets --- app/components/chart-filters-list.tsx | 10 ++--- .../components/dataset-control-section.tsx | 44 +++++++++++++++---- app/graphql/hooks.ts | 4 +- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/app/components/chart-filters-list.tsx b/app/components/chart-filters-list.tsx index 631a7e72d..c0b2a50e1 100644 --- a/app/components/chart-filters-list.tsx +++ b/app/components/chart-filters-list.tsx @@ -52,11 +52,11 @@ export const ChartFiltersList = ({ sourceType: dataSource.type, sourceUrl: dataSource.url, locale, - cubeFilters: cubeQueryFilters.map((filter) => ({ - iri: filter.iri, - componentIris: filter.componentIris, - filters: filter.filters, - joinBy: filter.joinBy, + cubeFilters: chartConfig.cubes.map((cube) => ({ + iri: cube.iri, + componentIris: extractChartConfigComponentIris({ chartConfig }), + filters: cubeQueryFilters.find((f) => f.iri === cube.iri)?.filters, + joinBy: cube.joinBy, loadValues: true, })), }, diff --git a/app/configurator/components/dataset-control-section.tsx b/app/configurator/components/dataset-control-section.tsx index f2a1287a5..72e8f9d8c 100644 --- a/app/configurator/components/dataset-control-section.tsx +++ b/app/configurator/components/dataset-control-section.tsx @@ -12,9 +12,11 @@ import { makeStyles } from "@mui/styles"; import clsx from "clsx"; import uniqBy from "lodash/uniqBy"; import { useEffect, useMemo, useRef, useState } from "react"; +import { useClient } from "urql"; import { useMetadataPanelStoreActions } from "@/components/metadata-panel-store"; import useDisclosure from "@/components/use-disclosure"; +import { getChartConfig } from "@/configurator"; import { DatasetDialog } from "@/configurator/components/add-dataset-dialog"; import { DatasetsBadge } from "@/configurator/components/badges"; import { BetaTag } from "@/configurator/components/beta-tag"; @@ -29,7 +31,10 @@ import { } from "@/configurator/configurator-state"; import { DataCubeMetadata } from "@/domain/data"; import useFlag from "@/flags/useFlag"; -import { useDataCubesMetadataQuery } from "@/graphql/hooks"; +import { + executeDataCubesComponentsQuery, + useDataCubesMetadataQuery, +} from "@/graphql/hooks"; import SvgIcAdd from "@/icons/components/IcAdd"; import SvgIcChecked from "@/icons/components/IcChecked"; import SvgIcTrash from "@/icons/components/IcTrash"; @@ -82,8 +87,10 @@ const DatasetRow = ({ }) => { const ref = useRef(null); const locale = useLocale(); + const client = useClient(); const classes = useStyles(); - const [, dispatch] = useConfiguratorState(isConfiguring); + const [state, dispatch] = useConfiguratorState(isConfiguring); + const [loading, setLoading] = useState(false); useEffect(() => { if (added && ref.current) { @@ -136,12 +143,33 @@ const DatasetRow = ({
{canRemove ? ( - dispatch({ - type: "DATASET_REMOVE", - value: { locale, iri: cube.iri }, - }) - } + disabled={loading} + onClick={async () => { + try { + const chartConfig = getChartConfig(state); + const newCubes = chartConfig.cubes.filter( + (c) => c.iri !== cube.iri + ); + await executeDataCubesComponentsQuery(client, { + locale, + sourceUrl: state.dataSource.url, + sourceType: state.dataSource.type, + cubeFilters: newCubes.map((cube) => ({ + iri: cube.iri, + joinBy: newCubes.length > 1 ? cube.joinBy : undefined, + loadValues: true, + })), + }); + dispatch({ + type: "DATASET_REMOVE", + value: { locale, iri: cube.iri }, + }); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }} > diff --git a/app/graphql/hooks.ts b/app/graphql/hooks.ts index c57e2e936..5d6ad323a 100644 --- a/app/graphql/hooks.ts +++ b/app/graphql/hooks.ts @@ -224,7 +224,7 @@ export const executeDataCubesComponentsQuery = async ( } const { dimensions: firstDimensions = [], measures: firstMeasures = [] } = - queries[0]?.data?.dataCubeComponents || {}; + queries[0].data?.dataCubeComponents || {}; assert(firstDimensions !== undefined, "Undefined dimensions"); assert(firstMeasures !== undefined, "Undefined measures"); @@ -331,7 +331,7 @@ export const executeDataCubesObservationsQuery = async ( queries.length > 1 ? mergeObservations(queries) : // If we are fetching data from a single cube, we can just return the data - queries[0]?.data?.dataCubeObservations?.data!; + queries[0].data?.dataCubeObservations?.data!; return { data: { From 126bdb356929f80216a732117ac7e8689a6261bf Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 28 Jun 2024 17:29:26 +0200 Subject: [PATCH 13/14] fix: Test --- app/configurator/configurator-state/reducer.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/configurator/configurator-state/reducer.spec.tsx b/app/configurator/configurator-state/reducer.spec.tsx index 0606cc87c..ecfde67ce 100644 --- a/app/configurator/configurator-state/reducer.spec.tsx +++ b/app/configurator/configurator-state/reducer.spec.tsx @@ -141,7 +141,7 @@ describe("add dataset", () => { ) as ConfiguratorStatePublishing; const config = newState2.chartConfigs[0] as MapConfig; expect(config.cubes.length).toBe(1); - expect(config.chartType).toEqual("column"); + expect(config.chartType).toEqual("map"); }); }); From 4cf7dc57e34dd2926e4f3e3724cda5f3ed388c07 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Fri, 28 Jun 2024 17:44:11 +0200 Subject: [PATCH 14/14] fix: Filters list --- app/components/chart-filters-list.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/components/chart-filters-list.tsx b/app/components/chart-filters-list.tsx index c0b2a50e1..85075282c 100644 --- a/app/components/chart-filters-list.tsx +++ b/app/components/chart-filters-list.tsx @@ -52,13 +52,16 @@ export const ChartFiltersList = ({ sourceType: dataSource.type, sourceUrl: dataSource.url, locale, - cubeFilters: chartConfig.cubes.map((cube) => ({ - iri: cube.iri, - componentIris: extractChartConfigComponentIris({ chartConfig }), - filters: cubeQueryFilters.find((f) => f.iri === cube.iri)?.filters, - joinBy: cube.joinBy, - loadValues: true, - })), + cubeFilters: chartConfig.cubes.map((cube) => { + const f = cubeQueryFilters.find((f) => f.iri === cube.iri); + return { + iri: f?.iri ?? cube.iri, + componentIris: f?.componentIris, + filters: f?.filters, + joinBy: f?.joinBy, + loadValues: true, + }; + }), }, }); const allFilters = useMemo(() => {