From 2c9c7eb0abdc5fb5fe6706c5f6c6c8b583e9daf0 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 27 May 2025 15:34:41 +0200 Subject: [PATCH 001/108] feat: Interactive filters are always defined --- app/charts/area/areas-state.tsx | 4 +- app/charts/area/chart-area.tsx | 4 +- app/charts/bar/bars-stacked-state.tsx | 4 +- app/charts/bar/chart-bar.tsx | 4 +- app/charts/chart-config-ui-options.ts | 10 ++-- app/charts/column/chart-column.tsx | 4 +- app/charts/column/columns-stacked-state.tsx | 4 +- app/charts/index.spec.ts | 8 +-- app/charts/index.ts | 5 +- app/charts/line/chart-lines.tsx | 2 +- app/charts/pie/chart-pie.tsx | 4 +- app/charts/scatterplot/chart-scatterplot.tsx | 2 +- app/charts/shared/brush/index.tsx | 18 +++---- app/charts/shared/chart-data-filters.tsx | 8 +-- app/charts/shared/chart-dimensions.tsx | 6 +-- app/charts/shared/chart-helpers.tsx | 2 +- app/charts/shared/chart-state.ts | 16 +++--- app/charts/shared/containers.tsx | 2 +- .../shared/use-sync-interactive-filters.tsx | 15 +++--- app/components/chart-shared.tsx | 6 +-- app/config-types.ts | 28 +++------- .../components/chart-configurator.tsx | 2 +- .../chart-options-selector/scale-domain.tsx | 2 +- .../components/layout-configurator.tsx | 6 ++- app/configurator/configurator-state/init.tsx | 1 + app/configurator/configurator-state/mocks.ts | 51 ++++++++++++++++-- .../configurator-state/reducer.spec.tsx | 28 +++++++++- .../configurator-state/reducer.tsx | 21 ++++---- .../interactive-filters-config-state.tsx | 6 +-- app/docs/fixtures.ts | 25 ++++++++- app/utils/chart-config/constants.ts | 4 +- app/utils/chart-config/versioning.spec.ts | 4 +- app/utils/chart-config/versioning.ts | 54 +++++++++++++++++++ 33 files changed, 245 insertions(+), 115 deletions(-) diff --git a/app/charts/area/areas-state.tsx b/app/charts/area/areas-state.tsx index 805abde6bb..302769525c 100644 --- a/app/charts/area/areas-state.tsx +++ b/app/charts/area/areas-state.tsx @@ -333,7 +333,7 @@ const useAreasState = ( // When the user can toggle between absolute and relative values, we use the // absolute values to calculate the yScale domain, so that the yScale doesn't // change when the user toggles between absolute and relative values. - if (interactiveFiltersConfig?.calculation.active) { + if (interactiveFiltersConfig.calculation.active) { const scale = getStackedYScale(paddingData, { normalize: false, getX: getXAsString, @@ -359,7 +359,7 @@ const useAreasState = ( customDomain: y.customDomain, }); }, [ - interactiveFiltersConfig?.calculation.active, + interactiveFiltersConfig.calculation.active, paddingData, normalize, getXAsString, diff --git a/app/charts/area/chart-area.tsx b/app/charts/area/chart-area.tsx index 8924369368..5055dd757d 100644 --- a/app/charts/area/chart-area.tsx +++ b/app/charts/area/chart-area.tsx @@ -48,7 +48,7 @@ const ChartAreas = memo((props: ChartProps) => { - {interactiveFiltersConfig?.timeRange.active === true && } + {interactiveFiltersConfig.timeRange.active && } @@ -58,7 +58,7 @@ const ChartAreas = memo((props: ChartProps) => { ) => { dimensionsById={dimensionsById} chartConfig={chartConfig} symbol="square" - interactive={interactiveFiltersConfig?.legend.active} + interactive={interactiveFiltersConfig.legend.active} showTitle={fields.segment.showTitle} /> @@ -115,7 +115,7 @@ const ChartBars = memo((props: ChartProps) => { dimensionsById={dimensionsById} chartConfig={chartConfig} symbol="square" - interactive={interactiveFiltersConfig?.legend.active} + interactive={interactiveFiltersConfig.legend.active} showTitle={fields.segment.showTitle} /> diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts index 0b227a8234..acfa950974 100644 --- a/app/charts/chart-config-ui-options.ts +++ b/app/charts/chart-config-ui-options.ts @@ -655,7 +655,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { if (segment) { const yScale = getStackedYScale(observations, { normalize: - chartConfig.interactiveFiltersConfig?.calculation.type === + chartConfig.interactiveFiltersConfig.calculation.type === "percent", getX, getY, @@ -758,7 +758,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { if (disableStacked(yMeasure)) { setWith(chartConfig, "fields.segment.type", "grouped", Object); - if (chartConfig.interactiveFiltersConfig?.calculation) { + if (chartConfig.interactiveFiltersConfig.calculation) { setWith( chartConfig, "interactiveFiltersConfig.calculation", @@ -799,7 +799,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { if (segment && segment.type === "stacked") { const yScale = getStackedYScale(observations, { normalize: - chartConfig.interactiveFiltersConfig?.calculation.type === + chartConfig.interactiveFiltersConfig.calculation.type === "percent", getX, getY, @@ -959,7 +959,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { if (disableStacked(xMeasure)) { setWith(chartConfig, "fields.segment.type", "grouped", Object); - if (chartConfig.interactiveFiltersConfig?.calculation) { + if (chartConfig.interactiveFiltersConfig.calculation) { setWith( chartConfig, "interactiveFiltersConfig.calculation", @@ -1000,7 +1000,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { if (segment && segment.type === "stacked") { const xScale = getStackedXScale(observations, { normalize: - chartConfig.interactiveFiltersConfig?.calculation.type === + chartConfig.interactiveFiltersConfig.calculation.type === "percent", getX, getY, diff --git a/app/charts/column/chart-column.tsx b/app/charts/column/chart-column.tsx index 8f7c1b5ed8..14873fea83 100644 --- a/app/charts/column/chart-column.tsx +++ b/app/charts/column/chart-column.tsx @@ -83,7 +83,7 @@ const ChartColumns = memo((props: ChartProps) => { dimensionsById={dimensionsById} chartConfig={chartConfig} symbol="square" - interactive={interactiveFiltersConfig?.legend.active} + interactive={interactiveFiltersConfig.legend.active} showTitle={fields.segment.showTitle} /> @@ -115,7 +115,7 @@ const ChartColumns = memo((props: ChartProps) => { dimensionsById={dimensionsById} chartConfig={chartConfig} symbol="square" - interactive={interactiveFiltersConfig?.legend.active} + interactive={interactiveFiltersConfig.legend.active} showTitle={fields.segment.showTitle} /> diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx index c8b969ec5a..c536c28b55 100644 --- a/app/charts/column/columns-stacked-state.tsx +++ b/app/charts/column/columns-stacked-state.tsx @@ -356,7 +356,7 @@ const useColumnsStackedState = ( // When the user can toggle between absolute and relative values, we use the // absolute values to calculate the yScale domain, so that the yScale doesn't // change when the user toggles between absolute and relative values. - if (interactiveFiltersConfig?.calculation.active) { + if (interactiveFiltersConfig.calculation.active) { const scale = getStackedYScale(paddingData, { normalize: false, getX, @@ -380,7 +380,7 @@ const useColumnsStackedState = ( customDomain: y.customDomain, }); }, [ - interactiveFiltersConfig?.calculation.active, + interactiveFiltersConfig.calculation.active, paddingData, normalize, getX, diff --git a/app/charts/index.spec.ts b/app/charts/index.spec.ts index f09718ce15..d1a9eb0ae0 100644 --- a/app/charts/index.spec.ts +++ b/app/charts/index.spec.ts @@ -329,12 +329,12 @@ describe("chart type switch", () => { measures: bathingWaterData.data.dataCubeByIri.measures as Measure[], }); - expect(newConfig.interactiveFiltersConfig?.dataFilters.active).toEqual( + expect(newConfig.interactiveFiltersConfig.dataFilters.active).toEqual( false ); - expect( - newConfig.interactiveFiltersConfig?.dataFilters.componentIds - ).toEqual([]); + expect(newConfig.interactiveFiltersConfig.dataFilters.componentIds).toEqual( + [] + ); }); it("should not carry over not-allowed segment", () => { diff --git a/app/charts/index.ts b/app/charts/index.ts index c01d74f935..c793d6c681 100644 --- a/app/charts/index.ts +++ b/app/charts/index.ts @@ -386,6 +386,7 @@ export const getInitialConfig = ( }; } }), + interactiveFiltersConfig: getInitialInteractiveFiltersConfig(), limits: {}, conversionUnitsByComponentId: {}, activeField: undefined, @@ -507,7 +508,6 @@ export const getInitialConfig = ( return { ...getGenericConfig(makeInitialFiltersForArea(areaDimension)), chartType, - interactiveFiltersConfig: getInitialInteractiveFiltersConfig(), baseLayer: { show: true, locked: false, @@ -542,7 +542,6 @@ export const getInitialConfig = ( return { ...getGenericConfig(), chartType, - interactiveFiltersConfig: getInitialInteractiveFiltersConfig(), fields: { y: { componentId: numericalMeasures[0].id }, segment: { @@ -571,7 +570,6 @@ export const getInitialConfig = ( return { ...getGenericConfig(), chartType: "scatterplot", - interactiveFiltersConfig: getInitialInteractiveFiltersConfig(), fields: { x: { componentId: numericalMeasures[0].id }, y: { @@ -612,7 +610,6 @@ export const getInitialConfig = ( return { ...getGenericConfig(), chartType, - interactiveFiltersConfig: undefined, settings: { showSearch: true, showAllRows: false, diff --git a/app/charts/line/chart-lines.tsx b/app/charts/line/chart-lines.tsx index a58a0752f0..c1e8db18c9 100644 --- a/app/charts/line/chart-lines.tsx +++ b/app/charts/line/chart-lines.tsx @@ -75,7 +75,7 @@ const ChartLines = memo((props: ChartProps) => { dimensionsById={dimensionsById} chartConfig={chartConfig} symbol="line" - interactive={interactiveFiltersConfig?.legend.active} + interactive={interactiveFiltersConfig.legend.active} showTitle={fields.segment?.showTitle} limits={limits} /> diff --git a/app/charts/pie/chart-pie.tsx b/app/charts/pie/chart-pie.tsx index d0cb1850d6..aefabe5a2c 100644 --- a/app/charts/pie/chart-pie.tsx +++ b/app/charts/pie/chart-pie.tsx @@ -55,9 +55,7 @@ const ChartPie = memo((props: ChartProps) => { dimensionsById={dimensionsById} chartConfig={chartConfig} symbol="square" - interactive={ - fields.segment && interactiveFiltersConfig?.legend.active - } + interactive={fields.segment && interactiveFiltersConfig.legend.active} showTitle={fields.segment.showTitle} /> diff --git a/app/charts/scatterplot/chart-scatterplot.tsx b/app/charts/scatterplot/chart-scatterplot.tsx index 71b7fae676..6e18405b13 100644 --- a/app/charts/scatterplot/chart-scatterplot.tsx +++ b/app/charts/scatterplot/chart-scatterplot.tsx @@ -65,7 +65,7 @@ const ChartScatterplot = memo((props: ChartProps) => { dimensionsById={dimensionsById} chartConfig={chartConfig} symbol="circle" - interactive={interactiveFiltersConfig?.legend.active} + interactive={interactiveFiltersConfig.legend.active} showTitle={fields.segment.showTitle} /> )} diff --git a/app/charts/shared/brush/index.tsx b/app/charts/shared/brush/index.tsx index 6371ac2e64..d7d8130d04 100644 --- a/app/charts/shared/brush/index.tsx +++ b/app/charts/shared/brush/index.tsx @@ -43,18 +43,16 @@ const BRUSH_HEIGHT = 4; const HEIGHT = HANDLE_HEIGHT + BRUSH_HEIGHT; export const shouldShowBrush = ( - interactiveFiltersConfig: - | ( - | LineConfig - | ComboLineSingleConfig - | ComboLineDualConfig - | ComboLineColumnConfig - | ColumnConfig - )["interactiveFiltersConfig"] - | undefined, + interactiveFiltersConfig: ( + | LineConfig + | ComboLineSingleConfig + | ComboLineDualConfig + | ComboLineColumnConfig + | ColumnConfig + )["interactiveFiltersConfig"], dashboardTimeRange: DashboardTimeRangeFilter | undefined ) => { - const chartTimeRange = interactiveFiltersConfig?.timeRange; + const chartTimeRange = interactiveFiltersConfig.timeRange; return !dashboardTimeRange?.active && chartTimeRange?.active; }; diff --git a/app/charts/shared/chart-data-filters.tsx b/app/charts/shared/chart-data-filters.tsx index 12b244e5db..bff34015bd 100644 --- a/app/charts/shared/chart-data-filters.tsx +++ b/app/charts/shared/chart-data-filters.tsx @@ -76,10 +76,10 @@ export const useChartDataFiltersState = ({ chartConfig: ChartConfig; dashboardFilters: DashboardFiltersConfig | undefined; }) => { - const dataFiltersConfig = chartConfig.interactiveFiltersConfig?.dataFilters; - const active = dataFiltersConfig?.active; - const defaultOpen = dataFiltersConfig?.defaultOpen; - const componentIds = dataFiltersConfig?.componentIds; + const dataFiltersConfig = chartConfig.interactiveFiltersConfig.dataFilters; + const active = dataFiltersConfig.active; + const defaultOpen = dataFiltersConfig.defaultOpen; + const componentIds = dataFiltersConfig.componentIds; const [open, setOpen] = useState(!!defaultOpen); useEffect(() => { diff --git a/app/charts/shared/chart-dimensions.tsx b/app/charts/shared/chart-dimensions.tsx index 38255d6e9b..e6a4c6c180 100644 --- a/app/charts/shared/chart-dimensions.tsx +++ b/app/charts/shared/chart-dimensions.tsx @@ -54,7 +54,7 @@ const computeChartPadding = ({ // with decimals have greater text length than the extremes. const fakeTicks = yScale.ticks(getTickNumber(height)); const minLeftTickWidth = - !!interactiveFiltersConfig?.calculation.active || normalize + interactiveFiltersConfig.calculation.active || normalize ? getTextWidth("100%", { fontSize: TICK_FONT_SIZE }) + TICK_PADDING : 0; const left = Math.max( @@ -68,7 +68,7 @@ const computeChartPadding = ({ const interactiveBottomElement = !dashboardFilters?.timeRange.active && - !!interactiveFiltersConfig?.timeRange.active; + interactiveFiltersConfig.timeRange.active; let bottom = isFlipped ? 15 // Eyeballed value @@ -80,7 +80,7 @@ const computeChartPadding = ({ 70; } - const top = interactiveFiltersConfig?.calculation.active ? 24 : 0; + const top = interactiveFiltersConfig.calculation.active ? 24 : 0; return isFlipped ? { diff --git a/app/charts/shared/chart-helpers.tsx b/app/charts/shared/chart-helpers.tsx index 3c353a6c2a..ec3620dc0b 100644 --- a/app/charts/shared/chart-helpers.tsx +++ b/app/charts/shared/chart-helpers.tsx @@ -87,7 +87,7 @@ export const prepareCubeQueryFilters = ({ for (const [k, v] of Object.entries(interactiveDataFilters)) { if ( - (interactiveFiltersConfig?.dataFilters.active || + (interactiveFiltersConfig.dataFilters.active || dashboardFilters?.dataFilters.componentIds?.includes(k)) && animationField?.componentId !== k ) { diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts index 7ba082b6e7..77ad065294 100644 --- a/app/charts/shared/chart-state.ts +++ b/app/charts/shared/chart-state.ts @@ -763,7 +763,7 @@ export const useInteractiveFiltersVariables = ( interactiveFiltersConfig: ChartConfig["interactiveFiltersConfig"], { dimensionsById }: { dimensionsById: DimensionsById } ): InteractiveFiltersVariables => { - const id = interactiveFiltersConfig?.timeRange.componentId ?? ""; + const id = interactiveFiltersConfig.timeRange.componentId; const dimension = dimensionsById[id]; const getTimeRangeDate = useTemporalVariable(id); const dimensionValues = dimension?.values ?? []; @@ -897,12 +897,12 @@ export const useChartData = ( const timeSlider = useChartInteractiveFilters((d) => d.timeSlider); // time range - const interactiveTimeRange = interactiveFiltersConfig?.timeRange; - const timeRangeFromTime = interactiveTimeRange?.presets.from - ? parseDate(interactiveTimeRange?.presets.from).getTime() + const interactiveTimeRange = interactiveFiltersConfig.timeRange; + const timeRangeFromTime = interactiveTimeRange.presets.from + ? parseDate(interactiveTimeRange.presets.from).getTime() : undefined; - const timeRangeToTime = interactiveTimeRange?.presets.to - ? parseDate(interactiveTimeRange?.presets.to).getTime() + const timeRangeToTime = interactiveTimeRange.presets.to + ? parseDate(interactiveTimeRange.presets.to).getTime() : undefined; const timeRangeFilters = useMemo(() => { const timeRangeFilter: ValuePredicate | null = @@ -981,7 +981,7 @@ export const useChartData = ( const interactiveLegendFilters = useMemo(() => { const legendItems = Object.keys(categories); const interactiveLegendFilter: ValuePredicate | null = - interactiveFiltersConfig?.legend?.active && getSegmentAbbreviationOrLabel + interactiveFiltersConfig.legend.active && getSegmentAbbreviationOrLabel ? (d: Observation) => { return !legendItems.includes(getSegmentAbbreviationOrLabel(d)); } @@ -991,7 +991,7 @@ export const useChartData = ( }, [ categories, getSegmentAbbreviationOrLabel, - interactiveFiltersConfig?.legend?.active, + interactiveFiltersConfig.legend.active, ]); const chartData = useMemo(() => { diff --git a/app/charts/shared/containers.tsx b/app/charts/shared/containers.tsx index 05b97ac1ce..c660120445 100644 --- a/app/charts/shared/containers.tsx +++ b/app/charts/shared/containers.tsx @@ -93,7 +93,7 @@ export const ChartSvg = ({ children }: { children: ReactNode }) => { width={width} style={{ position: "absolute", left: 0, top: 0 }} > - {interactiveFiltersConfig?.calculation.active && ( + {interactiveFiltersConfig.calculation.active && ( { if (componentIds) { @@ -141,8 +141,9 @@ const useSyncInteractiveFilters = ( ); // Calculation - const calculationActive = interactiveFiltersConfig?.calculation.active; - const calculationType = interactiveFiltersConfig?.calculation.type; + const calculationActive = interactiveFiltersConfig.calculation.active; + const calculationType = interactiveFiltersConfig.calculation.type; + useEffect(() => { if (calculationType) { setCalculationType(calculationType); diff --git a/app/components/chart-shared.tsx b/app/components/chart-shared.tsx index 5448b209a4..1ccd2fc7b4 100644 --- a/app/components/chart-shared.tsx +++ b/app/components/chart-shared.tsx @@ -107,11 +107,11 @@ export const ChartControls = ({ "dataSource" | "chartConfig" | "dashboardFilters" >; }) => { - const chartDataFilters = chartConfig.interactiveFiltersConfig?.dataFilters; + const chartDataFilters = chartConfig.interactiveFiltersConfig.dataFilters; const dashboardDataFilters = dashboardFilters?.dataFilters; const showFilters = - chartDataFilters?.active && - chartDataFilters?.componentIds.some( + chartDataFilters.active && + chartDataFilters.componentIds.some( (id) => !dashboardDataFilters?.componentIds.includes(id) ); const chartFiltersState = useChartDataFiltersState({ diff --git a/app/config-types.ts b/app/config-types.ts index 15312b604d..cdca0aaa22 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -164,15 +164,13 @@ export type InteractiveFiltersCalculation = t.TypeOf< typeof InteractiveFiltersCalculation >; -const InteractiveFiltersConfig = t.union([ - t.type({ - legend: InteractiveFiltersLegend, - timeRange: InteractiveFiltersTimeRange, - dataFilters: InteractiveFiltersDataConfig, - calculation: InteractiveFiltersCalculation, - }), - t.undefined, -]); +const InteractiveFiltersConfig = t.type({ + legend: InteractiveFiltersLegend, + timeRange: InteractiveFiltersTimeRange, + dataFilters: InteractiveFiltersDataConfig, + calculation: InteractiveFiltersCalculation, +}); + export type InteractiveFiltersConfig = t.TypeOf< typeof InteractiveFiltersConfig >; @@ -315,6 +313,7 @@ const GenericChartConfig = t.type({ version: t.string, meta: Meta, cubes: t.array(Cube), + interactiveFiltersConfig: InteractiveFiltersConfig, limits: t.record(t.string, t.array(Limit)), conversionUnitsByComponentId: t.record(t.string, ConversionUnit), activeField: t.union([t.string, t.undefined]), @@ -384,7 +383,6 @@ const ColumnConfig = t.intersection([ t.type( { chartType: t.literal("column"), - interactiveFiltersConfig: InteractiveFiltersConfig, fields: ColumnFields, }, "ColumnConfig" @@ -423,7 +421,6 @@ const BarConfig = t.intersection([ t.type( { chartType: t.literal("bar"), - interactiveFiltersConfig: InteractiveFiltersConfig, fields: BarFields, }, "BarConfig" @@ -472,7 +469,6 @@ const LineConfig = t.intersection([ t.type( { chartType: t.literal("line"), - interactiveFiltersConfig: InteractiveFiltersConfig, fields: LineFields, }, "LineConfig" @@ -518,7 +514,6 @@ const AreaConfig = t.intersection([ t.type( { chartType: t.literal("area"), - interactiveFiltersConfig: InteractiveFiltersConfig, fields: AreaFields, }, "AreaConfig" @@ -551,7 +546,6 @@ const ScatterPlotConfig = t.intersection([ t.type( { chartType: t.literal("scatterplot"), - interactiveFiltersConfig: InteractiveFiltersConfig, fields: ScatterPlotFields, }, "ScatterPlotConfig" @@ -582,7 +576,6 @@ const PieConfig = t.intersection([ t.type( { chartType: t.literal("pie"), - interactiveFiltersConfig: InteractiveFiltersConfig, fields: PieFields, }, "PieConfig" @@ -788,7 +781,6 @@ const TableConfig = t.intersection([ fields: TableFields, settings: TableSettings, sorting: t.array(TableSortingOption), - interactiveFiltersConfig: t.undefined, }, "TableConfig" ), @@ -942,7 +934,6 @@ const MapConfig = t.intersection([ t.type( { chartType: t.literal("map"), - interactiveFiltersConfig: InteractiveFiltersConfig, fields: MapFields, baseLayer: BaseLayer, }, @@ -966,7 +957,6 @@ const ComboLineSingleConfig = t.intersection([ { chartType: t.literal("comboLineSingle"), fields: ComboLineSingleFields, - interactiveFiltersConfig: InteractiveFiltersConfig, }, "ComboLineSingleConfig" ), @@ -989,7 +979,6 @@ const ComboLineDualConfig = t.intersection([ { chartType: t.literal("comboLineDual"), fields: ComboLineDualFields, - interactiveFiltersConfig: InteractiveFiltersConfig, }, "ComboLineDualConfig" ), @@ -1014,7 +1003,6 @@ const ComboLineColumnConfig = t.intersection([ { chartType: t.literal("comboLineColumn"), fields: ComboLineColumnFields, - interactiveFiltersConfig: InteractiveFiltersConfig, }, "ComboLineColumnConfig" ), diff --git a/app/configurator/components/chart-configurator.tsx b/app/configurator/components/chart-configurator.tsx index 94dd3d8e1f..9290b641f2 100644 --- a/app/configurator/components/chart-configurator.tsx +++ b/app/configurator/components/chart-configurator.tsx @@ -743,7 +743,7 @@ export const ChartConfigurator = ({ field={null} path="interactiveFiltersConfig.dataFilters.defaultOpen" disabled={ - !chartConfig.interactiveFiltersConfig?.dataFilters.active + !chartConfig.interactiveFiltersConfig.dataFilters.active } /> diff --git a/app/configurator/components/chart-options-selector/scale-domain.tsx b/app/configurator/components/chart-options-selector/scale-domain.tsx index 357b34d904..dc5d72523e 100644 --- a/app/configurator/components/chart-options-selector/scale-domain.tsx +++ b/app/configurator/components/chart-options-selector/scale-domain.tsx @@ -40,7 +40,7 @@ export const ScaleDomain = ({ | undefined; const checked = !!domain; const disabled = - chartConfig.interactiveFiltersConfig?.calculation.type === "percent"; + chartConfig.interactiveFiltersConfig.calculation.type === "percent"; const defaultDomain = useMemo(() => { return getDefaultDomain({ chartConfig, observations }); diff --git a/app/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index 13d78f8da2..fc802c2e3a 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -387,7 +387,8 @@ const DashboardTimeRangeFilterOptions = ({ const interactiveFiltersState = getInteractiveFiltersState(); const { from, to } = interactiveFiltersState.timeRange; const setTimeRangeFilter = interactiveFiltersState.setTimeRange; - if (from && to && interactiveFiltersConfig?.timeRange.componentId) { + + if (from && to && interactiveFiltersConfig.timeRange.componentId) { setTimeRangeFilter(newDate, to); } } @@ -404,7 +405,8 @@ const DashboardTimeRangeFilterOptions = ({ const interactiveFiltersState = getInteractiveFiltersState(); const { from, to } = interactiveFiltersState.timeRange; const setTimeRangeFilter = interactiveFiltersState.setTimeRange; - if (from && to && interactiveFiltersConfig?.timeRange.componentId) { + + if (from && to && interactiveFiltersConfig.timeRange.componentId) { setTimeRangeFilter(from, newDate); } } diff --git a/app/configurator/configurator-state/init.tsx b/app/configurator/configurator-state/init.tsx index 7bab7a137d..b5d317d53f 100644 --- a/app/configurator/configurator-state/init.tsx +++ b/app/configurator/configurator-state/init.tsx @@ -151,6 +151,7 @@ export const initChartStateFromLocalStorage = async ( try { const rawState = JSON.parse(storedState); const migratedState = await migrateConfiguratorState(rawState); + console.log("migratedState", migratedState); state = decodeConfiguratorState(migratedState); } catch (e) { console.error("Error while parsing stored state", e); diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index 0137c7b3e1..fa335430a7 100644 --- a/app/configurator/configurator-state/mocks.ts +++ b/app/configurator/configurator-state/mocks.ts @@ -30,8 +30,30 @@ export const configStateMock = { chartType: "map", version: CHART_CONFIG_VERSION, meta: {} as ConfiguratorStateConfiguringChart["chartConfigs"][0]["meta"], - interactiveFiltersConfig: - {} as ConfiguratorStateConfiguringChart["chartConfigs"][0]["interactiveFiltersConfig"], + interactiveFiltersConfig: { + legend: { + active: false, + componentId: "", + }, + timeRange: { + active: false, + componentId: "", + presets: { + type: "range", + from: "", + to: "", + }, + }, + dataFilters: { + active: false, + componentIds: [], + defaultOpen: true, + }, + calculation: { + active: false, + type: "identity", + }, + }, baseLayer: {} as Extract< ConfiguratorStateConfiguringChart["chartConfigs"][0], { chartType: "map" } @@ -1160,7 +1182,30 @@ export const configJoinedCubes: Partial< key: "NF9PKwRtOaOI", version: CHART_CONFIG_VERSION, activeField: undefined, - interactiveFiltersConfig: undefined, + interactiveFiltersConfig: { + legend: { + active: false, + componentId: "", + }, + timeRange: { + active: false, + componentId: "", + presets: { + type: "range", + from: "", + to: "", + }, + }, + dataFilters: { + active: false, + componentIds: [], + defaultOpen: true, + }, + calculation: { + active: false, + type: "identity", + }, + }, meta: { title: { en: "", de: "", fr: "", it: "" }, description: { en: "", de: "", fr: "", it: "" }, diff --git a/app/configurator/configurator-state/reducer.spec.tsx b/app/configurator/configurator-state/reducer.spec.tsx index 63dc23b91f..a773ab8e09 100644 --- a/app/configurator/configurator-state/reducer.spec.tsx +++ b/app/configurator/configurator-state/reducer.spec.tsx @@ -608,7 +608,7 @@ describe("deriveFiltersFromFields", () => { "it": "", }, }, - "version": "4.5.0", + "version": "4.6.0", } `); }); @@ -1110,10 +1110,34 @@ describe("colorMapping", () => { filters: {}, }, ], + interactiveFiltersConfig: { + legend: { + active: false, + componentId: "", + }, + timeRange: { + active: false, + componentId: "", + presets: { + type: "range", + from: "", + to: "", + }, + }, + dataFilters: { + active: false, + componentIds: [], + defaultOpen: true, + }, + calculation: { + active: false, + type: "identity", + }, + }, }, ], activeChartKey: "abc", - } as ConfiguratorStateConfiguringChart; + } as unknown as ConfiguratorStateConfiguringChart; handleChartFieldChanged(state, { type: "CHART_FIELD_CHANGED", diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index e0d95a11ea..95d1c0bcfe 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -412,17 +412,16 @@ export const handleChartFieldChanged = ( }); // Remove the component from interactive data filters. - if (chartConfig.interactiveFiltersConfig?.dataFilters) { - const componentIds = - chartConfig.interactiveFiltersConfig.dataFilters.componentIds.filter( - (d) => d !== componentId - ); - const active = componentIds.length > 0; - chartConfig.interactiveFiltersConfig.dataFilters = { - active, - componentIds, - }; - } + + const componentIds = + chartConfig.interactiveFiltersConfig.dataFilters.componentIds.filter( + (d) => d !== componentId + ); + const active = componentIds.length > 0; + chartConfig.interactiveFiltersConfig.dataFilters = { + active, + componentIds, + }; const newConfig = deriveFiltersFromFields(chartConfig, { dimensions }); const index = draft.chartConfigs.findIndex((d) => d.key === chartConfig.key); diff --git a/app/configurator/interactive-filters/interactive-filters-config-state.tsx b/app/configurator/interactive-filters/interactive-filters-config-state.tsx index 7f980051f1..642c2255c5 100644 --- a/app/configurator/interactive-filters/interactive-filters-config-state.tsx +++ b/app/configurator/interactive-filters/interactive-filters-config-state.tsx @@ -14,7 +14,7 @@ export const useInteractiveFiltersToggle = (target: "legend") => { const [state, dispatch] = useConfiguratorState(isConfiguring); const chartConfig = getChartConfig(state); const onChange = useEvent((e: ChangeEvent) => { - if (chartConfig.interactiveFiltersConfig?.[target]) { + if (chartConfig.interactiveFiltersConfig[target]) { const newConfig = produce( chartConfig.interactiveFiltersConfig, (draft) => { @@ -61,7 +61,7 @@ export const useInteractiveDataFilterToggle = (dimensionId: string) => { }); }); const checked = - chartConfig.interactiveFiltersConfig?.dataFilters.componentIds?.includes( + chartConfig.interactiveFiltersConfig.dataFilters.componentIds.includes( dimensionId ); @@ -116,7 +116,7 @@ export const useInteractiveTimeRangeToggle = () => { value: newIFConfig, }); }); - const checked = chartConfig.interactiveFiltersConfig?.timeRange.active; + const checked = chartConfig.interactiveFiltersConfig.timeRange.active; return { checked, toggle }; }; diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index 04548fa925..f281da3e91 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -1112,7 +1112,30 @@ export const tableConfig: TableConfig = { limits: {}, conversionUnitsByComponentId: {}, chartType: "table", - interactiveFiltersConfig: undefined, + interactiveFiltersConfig: { + legend: { + active: false, + componentId: "", + }, + timeRange: { + active: false, + componentId: "", + presets: { + type: "range", + from: "", + to: "", + }, + }, + dataFilters: { + active: false, + componentIds: [], + defaultOpen: true, + }, + calculation: { + active: false, + type: "identity", + }, + }, settings: { showSearch: true, showAllRows: true }, sorting: [ { diff --git a/app/utils/chart-config/constants.ts b/app/utils/chart-config/constants.ts index e5af7176b6..e699855d6c 100644 --- a/app/utils/chart-config/constants.ts +++ b/app/utils/chart-config/constants.ts @@ -1,3 +1,3 @@ -export const CONFIGURATOR_STATE_VERSION = "4.6.0"; +export const CONFIGURATOR_STATE_VERSION = "4.7.0"; -export const CHART_CONFIG_VERSION = "4.5.0"; +export const CHART_CONFIG_VERSION = "4.6.0"; diff --git a/app/utils/chart-config/versioning.spec.ts b/app/utils/chart-config/versioning.spec.ts index da5943b730..9315094221 100644 --- a/app/utils/chart-config/versioning.spec.ts +++ b/app/utils/chart-config/versioning.spec.ts @@ -303,7 +303,7 @@ describe("config migrations", () => { expect(decodedConfig).toBeDefined(); expect( - (decodedConfig as LineConfig).interactiveFiltersConfig?.timeRange + (decodedConfig as LineConfig).interactiveFiltersConfig.timeRange .componentId === lineConfigV1_0_0.fields.x.componentIri ).toBeDefined(); @@ -312,7 +312,7 @@ describe("config migrations", () => { })) as any; expect( - migratedOldConfig.interactiveFiltersConfig?.timeRange.componentIri + migratedOldConfig.interactiveFiltersConfig.timeRange.componentIri ).toEqual(""); }); diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts index bcad674ca6..3937abfaa2 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -1554,6 +1554,54 @@ export const chartConfigMigrations: Migration[] = [ delete newConfig.conversionUnitsByComponentId; } + return newConfig; + }, + }, + { + from: "4.5.0", + to: "4.6.0", + description: `ALL { + interactiveFiltersConfig can't be undefined anymore + }`, + up: (config) => { + const newConfig = { ...config, version: "4.6.0" }; + + if (!newConfig.interactiveFiltersConfig) { + newConfig.interactiveFiltersConfig = { + legend: { + active: false, + componentId: "", + }, + timeRange: { + active: false, + componentId: "", + presets: { + type: "range", + from: "", + to: "", + }, + }, + dataFilters: { + active: false, + componentIds: [], + defaultOpen: true, + }, + calculation: { + active: false, + type: "identity", + }, + }; + } + + return newConfig; + }, + down: (config) => { + const newConfig = { ...config, version: "4.5.0" }; + + if (newConfig.chartType === "table") { + newConfig.interactiveFiltersConfig = undefined; + } + return newConfig; }, }, @@ -2176,6 +2224,12 @@ export const configuratorStateMigrations: Migration[] = [ fromChartConfigVersion: "4.4.0", toChartConfigVersion: "4.5.0", }), + makeBumpChartConfigVersionMigration({ + fromVersion: "4.6.0", + toVersion: "4.7.0", + fromChartConfigVersion: "4.5.0", + toChartConfigVersion: "4.6.0", + }), ]; export const migrateConfiguratorState = makeMigrate( From 175e2a9043bae73f59030576997d265bbad550d4 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 30 Jul 2025 12:29:42 +0200 Subject: [PATCH 002/108] chore: Remove console.log --- app/configurator/configurator-state/init.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/configurator/configurator-state/init.tsx b/app/configurator/configurator-state/init.tsx index 84d1843300..d2d902f7b3 100644 --- a/app/configurator/configurator-state/init.tsx +++ b/app/configurator/configurator-state/init.tsx @@ -151,7 +151,6 @@ export const initChartStateFromLocalStorage = async ( try { const rawState = JSON.parse(storedState); const migratedState = await migrateConfiguratorState(rawState); - console.log("migratedState", migratedState); state = decodeConfiguratorState(migratedState); } catch (e) { console.error("Error while parsing stored state", e); From cbeca9be564c66f0f11385648f3edc157fc1f61d Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 30 Jul 2025 12:48:00 +0200 Subject: [PATCH 003/108] refactor: Rename --- .../components/chart-configurator.tsx | 4 ++-- app/configurator/components/filters.tsx | 2 +- .../interactive-filters-config-state.tsx | 17 +++++++---------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/configurator/components/chart-configurator.tsx b/app/configurator/components/chart-configurator.tsx index 04fb103cae..097c33a1ce 100644 --- a/app/configurator/components/chart-configurator.tsx +++ b/app/configurator/components/chart-configurator.tsx @@ -592,7 +592,7 @@ const useStyles = makeStyles((theme) => ({ })); const InteractiveDataFilterToggle = ({ id }: { id: string }) => { - const { checked, toggle } = useInteractiveDataFilterToggle(id); + const { checked, onChange } = useInteractiveDataFilterToggle(id); return ( { message: "Interactive", })} checked={checked} - onChange={toggle} + onChange={onChange} /> ); }; diff --git a/app/configurator/components/filters.tsx b/app/configurator/components/filters.tsx index 6eaef3bbb1..74006e39c0 100644 --- a/app/configurator/components/filters.tsx +++ b/app/configurator/components/filters.tsx @@ -369,7 +369,7 @@ const MultiFilterContent = ({ segment?.showValuesMapping ?? {} ); - const interactiveFilterProps = useInteractiveFiltersToggle("legend"); + const interactiveFilterProps = useInteractiveFiltersToggle(); const visibleLegendProps = useLegendTitleVisibility(); const chartSymbol = getChartSymbol(chartConfig.chartType); diff --git a/app/configurator/interactive-filters/interactive-filters-config-state.tsx b/app/configurator/interactive-filters/interactive-filters-config-state.tsx index 3a9205c3a1..5df01cd364 100644 --- a/app/configurator/interactive-filters/interactive-filters-config-state.tsx +++ b/app/configurator/interactive-filters/interactive-filters-config-state.tsx @@ -10,15 +10,15 @@ import { } from "@/configurator/configurator-state"; import { useEvent } from "@/utils/use-event"; -export const useInteractiveFiltersToggle = (target: "legend") => { +export const useInteractiveFiltersToggle = () => { const [state, dispatch] = useConfiguratorState(isConfiguring); const chartConfig = getChartConfig(state); const onChange = useEvent((e: ChangeEvent) => { - if (chartConfig.interactiveFiltersConfig[target]) { + if (chartConfig.interactiveFiltersConfig.legend) { const newConfig = produce( chartConfig.interactiveFiltersConfig, (draft) => { - draft[target].active = e.currentTarget.checked; + draft.legend.active = e.currentTarget.checked; } ); @@ -29,14 +29,11 @@ export const useInteractiveFiltersToggle = (target: "legend") => { } }); - const stateValue = get( - chartConfig, - `interactiveFiltersConfig.${target}.active` - ); + const stateValue = get(chartConfig, "interactiveFiltersConfig.legend.active"); const checked = stateValue ? stateValue : false; return { - name: target, + name: "legend", checked, onChange, }; @@ -48,7 +45,7 @@ export const useInteractiveFiltersToggle = (target: "legend") => { export const useInteractiveDataFilterToggle = (dimensionId: string) => { const [state, dispatch] = useConfiguratorState(isConfiguring); const chartConfig = getChartConfig(state); - const toggle = useEvent(() => { + const onChange = useEvent(() => { const { interactiveFiltersConfig } = chartConfig; const newIFConfig = toggleInteractiveFilterDataDimension( interactiveFiltersConfig, @@ -65,7 +62,7 @@ export const useInteractiveDataFilterToggle = (dimensionId: string) => { dimensionId ); - return { checked, toggle }; + return { checked, onChange }; }; // Add or remove a dimension from the interactive data filters dimensions list From 2b977b818373309774b522259449929839fe6b52 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 31 Jul 2025 08:27:24 +0200 Subject: [PATCH 004/108] feat: Extract InteractiveToggle and show it for all MultiFilter component uses, not only segmentation --- app/configurator/components/filters.tsx | 70 ++++++++++++------- .../interactive-filters-config-state.tsx | 6 +- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/app/configurator/components/filters.tsx b/app/configurator/components/filters.tsx index 74006e39c0..c3fe195170 100644 --- a/app/configurator/components/filters.tsx +++ b/app/configurator/components/filters.tsx @@ -72,6 +72,7 @@ import { import { useLegendTitleVisibility } from "@/configurator/configurator-state/segment-config-state"; import { EditorBrush } from "@/configurator/interactive-filters/editor-brush"; import { + useInteractiveDataFilterToggle, useInteractiveFiltersToggle, useInteractiveTimeRangeToggle, } from "@/configurator/interactive-filters/interactive-filters-config-state"; @@ -369,34 +370,23 @@ const MultiFilterContent = ({ segment?.showValuesMapping ?? {} ); - const interactiveFilterProps = useInteractiveFiltersToggle(); + // TODO: separate interactive legend and data filters + const interactiveLegendFilterProps = useInteractiveFiltersToggle(); + const interactiveDataFilterProps = + useInteractiveDataFilterToggle(dimensionId); + const interactiveFilterProps = + chartConfig.activeField === "segment" + ? interactiveLegendFilterProps + : interactiveDataFilterProps; const visibleLegendProps = useLegendTitleVisibility(); const chartSymbol = getChartSymbol(chartConfig.chartType); return ( - {chartConfig.activeField === "segment" ? ( - - - Allow users to change filters - - } - > -
- - Interactive - -
- - } - {...interactiveFilterProps} - /> + + + {chartConfig.activeField === "segment" ? ( - - ) : null} + ) : null} +