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 diff --git a/app/charts/column/chart-column.tsx b/app/charts/column/chart-column.tsx index fb32da3d6..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 { sharedFilters } = useDashboardInteractiveFilters(); - + const dashboardFilters = useDashboardInteractiveFilters(); const showTimeBrush = shouldShowBrush( interactiveFiltersConfig, - sharedFilters + 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 388f237f8..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 { sharedFilters } = useDashboardInteractiveFilters(); - + const dashboardFilters = useDashboardInteractiveFilters(); return ( @@ -37,9 +36,10 @@ const ChartComboLineColumn = memo( - {shouldShowBrush(interactiveFiltersConfig, sharedFilters) && ( - - )} + {shouldShowBrush( + interactiveFiltersConfig, + dashboardFilters.timeRange + ) && } diff --git a/app/charts/combo/chart-combo-line-dual.tsx b/app/charts/combo/chart-combo-line-dual.tsx index 469f46c95..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 { sharedFilters } = useDashboardInteractiveFilters(); - + const dashboardFilters = useDashboardInteractiveFilters(); return ( @@ -36,9 +35,10 @@ const ChartComboLineDual = memo((props: ChartProps) => { - {shouldShowBrush(interactiveFiltersConfig, sharedFilters) && ( - - )} + {shouldShowBrush( + interactiveFiltersConfig, + dashboardFilters.timeRange + ) && } diff --git a/app/charts/combo/chart-combo-line-single.tsx b/app/charts/combo/chart-combo-line-single.tsx index cda722f0c..1ce183be3 100644 --- a/app/charts/combo/chart-combo-line-single.tsx +++ b/app/charts/combo/chart-combo-line-single.tsx @@ -26,7 +26,7 @@ const ChartComboLineSingle = memo( (props: ChartProps) => { const { chartConfig } = props; const { interactiveFiltersConfig } = chartConfig; - const { sharedFilters } = useDashboardInteractiveFilters(); + const dashboardFilters = useDashboardInteractiveFilters(); return ( @@ -34,9 +34,10 @@ const ChartComboLineSingle = memo( - {shouldShowBrush(interactiveFiltersConfig, sharedFilters) && ( - - )} + {shouldShowBrush( + interactiveFiltersConfig, + 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 35ea36ebf..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 { - SharedFilter, useChartInteractiveFilters, useInteractiveFiltersGetState, } from "@/stores/interactive-filters"; @@ -44,16 +44,10 @@ export const shouldShowBrush = ( | ColumnConfig )["interactiveFiltersConfig"] | undefined, - sharedFilters: SharedFilter[] | undefined + dashboardTimeRange: DashboardTimeRangeFilter | undefined ) => { const chartTimeRange = interactiveFiltersConfig?.timeRange; - const chartTimeRangeIri = chartTimeRange?.componentIri; - const sharedFilter = sharedFilters?.find( - (x) => x.type === "timeRange" && 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 55494caa9..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,24 +510,11 @@ export const useChartData = ( const interactiveToTime = timeRange.to?.getTime(); const dashboardFilters = useDashboardInteractiveFilters(); const interactiveTimeRangeFilters = useMemo(() => { - const isDashboardFilterActive = !!dashboardFilters.sharedFilters.find( - (f) => { - const timeRangeFilterIri = interactiveTimeRange?.componentIri; - if (f.type !== "timeRange" || !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; @@ -537,13 +523,11 @@ export const useChartData = ( return interactiveTimeRangeFilter ? [interactiveTimeRangeFilter] : []; }, [ - dashboardFilters.sharedFilters, + dashboardFilters.timeRange, getXAsDate, interactiveFromTime, interactiveToTime, interactiveTimeRange?.active, - interactiveTimeRange?.componentIri, - chartConfig, ]); // interactive time slider diff --git a/app/components/chart-filters-list.tsx b/app/components/chart-filters-list.tsx index 631a7e72d..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: cubeQueryFilters.map((filter) => ({ - iri: filter.iri, - componentIris: filter.componentIris, - filters: filter.filters, - joinBy: filter.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(() => { 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) => (
{ 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) + )} { }; const presetToTimeRange = ( - presets: InteractiveFiltersTimeRange["presets"], + presets: Pick, timeUnit: TimeUnit ) => { if (!timeUnit) { @@ -81,43 +74,27 @@ const DashboardTimeRangeSlider = ({ filter, mounted, }: { - filter: Extract; + 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,57 +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 (presets.from && presets.to && timeUnit) { + const parser = timeUnitToParser[timeUnit]; + setTimeRange([ + toUnixSeconds(parser(presets.from)), + toUnixSeconds(parser(presets.to)), + ]); + } + }, [presets.from, presets.to, timeUnit]); + const mountedForSomeTime = useTimeout(500, mounted); - if (!filter || !timeRange || filter.type !== "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} @@ -196,28 +177,17 @@ const DashboardTimeRangeSlider = ({ }; export const DashboardInteractiveFilters = () => { - const dashboardInteractiveFilters = useDashboardInteractiveFilters(); - - return ( - <> - {dashboardInteractiveFilters.sharedFilters.map((filter) => { - if (filter.type !== "timeRange" || !filter.active) { - return null; - } - - return ( - -
- -
-
- ); - })} - - ); + const { timeRange } = useDashboardInteractiveFilters(); + return timeRange?.active ? ( + +
+ +
+
+ ) : null; }; function stepFromTimeUnit(timeUnit: TimeUnit | undefined) { 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..e71b84055 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -1142,26 +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 DashboardFilter = DashboardFilterTimeRange; // Will be replaced by an union later -export type DashboardFilter = t.TypeOf; - const DashboardFiltersConfig = t.type({ - filters: t.array(DashboardFilter), + 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 99ed52e9a..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: { - filters: [], + 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 2f83fd598..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"; @@ -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,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/components/configurator.tsx b/app/configurator/components/configurator.tsx index d93ccc0d2..e547a917d 100644 --- a/app/configurator/components/configurator.tsx +++ b/app/configurator/components/configurator.tsx @@ -192,7 +192,12 @@ const ConfigureChartStep = () => { {chartConfig.chartType === "table" ? ( ) : ( - + // Need to use key to force re-render when switching between charts + // or adding / removing cubes to fix stale data issues + )} 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/configurator/components/layout-configurator.tsx b/app/configurator/components/layout-configurator.tsx index 685be3a53..2124f383e 100644 --- a/app/configurator/components/layout-configurator.tsx +++ b/app/configurator/components/layout-configurator.tsx @@ -8,15 +8,21 @@ import { Typography, useEventCallback, } from "@mui/material"; +import { ascending, descending } from "d3-array"; import capitalize from "lodash/capitalize"; -import keyBy from "lodash/keyBy"; import omit from "lodash/omit"; -import { Fragment, useMemo } from "react"; +import uniqBy from "lodash/uniqBy"; +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, LayoutDashboard } from "@/config-types"; +import { + ChartConfig, + DashboardTimeRangeFilter, + getChartConfig, + LayoutDashboard, +} from "@/config-types"; import { LayoutAnnotator } from "@/configurator/components/annotators"; import { ControlSection, @@ -40,18 +46,17 @@ import { import { canDimensionBeTimeFiltered, Dimension, - isJoinByComponent, + isTemporalDimensionWithTimeUnit, TemporalDimension, TemporalEntityDimension, } from "@/domain/data"; import { useFlag } from "@/flags"; import { useTimeFormatLocale, useTimeFormatUnit } from "@/formatters"; import { useConfigsCubeComponents } from "@/graphql/hooks"; +import { TimeUnit } from "@/graphql/resolver-types"; +import { timeUnitFormats, timeUnitOrder } from "@/rdf/mappings"; import { useLocale } from "@/src"; -import { - SharedFilter, - useDashboardInteractiveFilters, -} from "@/stores/interactive-filters"; +import { useDashboardInteractiveFilters } from "@/stores/interactive-filters"; import { getTimeFilterOptions } from "@/utils/time-filter-options"; export const LayoutConfigurator = () => { @@ -111,7 +116,7 @@ const LayoutLayoutConfigurator = () => { const LayoutSharedFiltersConfigurator = () => { const [state, dispatch] = useConfiguratorState(isLayouting); const { layout } = state; - const { sharedFilters, potentialSharedFilters } = + const { timeRange, potentialTimeRangeFilterIris } = useDashboardInteractiveFilters(); const locale = useLocale(); @@ -122,73 +127,95 @@ const LayoutSharedFiltersConfigurator = () => { }, }); - const dimensionsByIri = useMemo(() => { - const res: Record = {}; - for (const dim of data?.dataCubesComponents.dimensions ?? []) { - res[dim.iri] = dim; - if (isJoinByComponent(dim)) { - for (const o of dim.originalIris) { - res[o.dimensionIri] = dim; - } - } - } - return res; - }, [data?.dataCubesComponents.dimensions]); - - const sharedFiltersByIri = useMemo(() => { - return keyBy(sharedFilters, (x) => x.componentIri); - }, [sharedFilters]); - 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; - } + 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) => { if (checked) { const options = getTimeFilterOptions({ - dimension: dimension, + dimension: combinedDimension, formatLocale, timeFormatUnit, }); const from = options.sortedOptions[0].date; const to = options.sortedOptions.at(-1)?.date; - const dateFormatter = timeUnitToFormatter[dimension.timeUnit]; + const formatDate = timeUnitToFormatter[combinedDimension.timeUnit]; if (!from || !to) { return; } dispatch({ - type: "DASHBOARD_FILTER_ADD", + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", value: { - type: "timeRange", active: true, + timeUnit: combinedDimension.timeUnit, presets: { - type: "range", - from: dateFormatter(from), - to: dateFormatter(to), + from: formatDate(from), + to: formatDate(to), }, - componentIri: componentIri, }, }); } else { dispatch({ - type: "DASHBOARD_FILTER_REMOVE", - value: componentIri, + type: "DASHBOARD_TIME_RANGE_FILTER_REMOVE", }); } } @@ -197,14 +224,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 (!timeRange || potentialTimeRangeFilterIris.length === 0) { return null; } + return ( { - {shownFilters.map((filter) => { - const dimension = dimensionsByIri[filter.componentIri]; - const sharedFilter = sharedFiltersByIri[filter.componentIri]; - return ( - - - - {dimension.label || filter.componentIri} - - - - Shared - - - } - control={ - - } - /> - - + + {combinedDimension.label} + + + + Shared + + + } + control={ + - - ); - })} + } + /> + + {timeRange.active ? ( + + ) : null} @@ -274,18 +286,14 @@ const LayoutSharedFiltersConfigurator = () => { } }; -const SharedFilterOptions = ({ - sharedFilter, +const DashboardFiltersOptions = ({ + timeRangeFilter, dimension, }: { - sharedFilter: SharedFilter; + timeRangeFilter: DashboardTimeRangeFilter | undefined; dimension: Dimension; }) => { - if (!sharedFilter) { - return null; - } - - if (sharedFilter.type !== "timeRange") { + if (!timeRangeFilter) { return null; } @@ -297,25 +305,26 @@ const SharedFilterOptions = ({ } return ( - ); }; -const SharedFilterOptionsTimeRange = ({ - sharedFilter, +const DashboardTimeRangeFilterOptions = ({ + timeRangeFilter, dimension, }: { - sharedFilter: SharedFilter; + timeRangeFilter: DashboardTimeRangeFilter; dimension: TemporalDimension | TemporalEntityDimension; }) => { const { timeUnit, timeFormat } = dimension; 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,61 +333,116 @@ const SharedFilterOptionsTimeRange = ({ }); }, [dimension, parseDate]); + const updateChartStoresFrom = useCallback( + (newDate: Date) => { + 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, state] + ); + + const updateChartStoresTo = useCallback( + (newDate: Date) => { + 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, state] + ); + const handleChangeFromDate: DatePickerFieldProps["onChange"] = (ev) => { + const newDate = parseDate(ev.target.value); + if (!newDate) { + return; + } dispatch({ - type: "DASHBOARD_FILTER_UPDATE", + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", value: { - ...sharedFilter, + ...timeRangeFilter, presets: { - ...sharedFilter.presets, - from: formatDate(new Date(ev.target.value)), + ...timeRangeFilter.presets, + from: formatDate(newDate), }, }, }); + updateChartStoresFrom(newDate); }; const handleChangeFromGeneric: DataFilterGenericDimensionProps["onChange"] = ( ev - ) => + ) => { dispatch({ - type: "DASHBOARD_FILTER_UPDATE", + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", value: { - ...sharedFilter, + ...timeRangeFilter, presets: { - ...sharedFilter.presets, + ...timeRangeFilter.presets, from: ev.target.value as string, }, }, }); + 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", + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", value: { - ...sharedFilter, + ...timeRangeFilter, presets: { - ...sharedFilter.presets, - to: formatDate(new Date(ev.target.value)), + ...timeRangeFilter.presets, + to: formatDate(newDate), }, }, }); + updateChartStoresTo(newDate); + }; const handleChangeToGeneric: DataFilterGenericDimensionProps["onChange"] = ( ev - ) => + ) => { dispatch({ - type: "DASHBOARD_FILTER_UPDATE", + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE", value: { - ...sharedFilter, + ...timeRangeFilter, presets: { - ...sharedFilter.presets, + ...timeRangeFilter.presets, to: ev.target.value as string, }, }, }); + const parsedDate = parseDate(ev.target.value as string); + if (parsedDate) { + updateChartStoresTo(parsedDate); + } + }; return ( - +
{canRenderDatePickerField(timeUnit) ? ( !optionValues.includes(formatDate(d))} + isDateDisabled={(date) => !optionValues.includes(formatDate(date))} timeUnit={timeUnit} dateFormat={formatDate} minDate={minDate} @@ -403,19 +467,19 @@ const SharedFilterOptionsTimeRange = ({ /> ) : ( )} - +
); }; diff --git a/app/configurator/components/ui-helpers.ts b/app/configurator/components/ui-helpers.ts index 7e9414caf..1f8ddcb0e 100644 --- a/app/configurator/components/ui-helpers.ts +++ b/app/configurator/components/ui-helpers.ts @@ -313,16 +313,19 @@ export const canUseAbbreviations = (d?: Component): boolean => { * - 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..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_FILTER_ADD"; - value: DashboardFiltersConfig["filters"][number]; + type: "DASHBOARD_TIME_RANGE_FILTER_UPDATE"; + value: DashboardFiltersConfig["timeRange"]; } | { - type: "DASHBOARD_FILTER_REMOVE"; - value: DashboardFiltersConfig["filters"][number]["componentIri"]; - } - | { - type: "DASHBOARD_FILTER_UPDATE"; - value: DashboardFiltersConfig["filters"][number]; + type: "DASHBOARD_TIME_RANGE_FILTER_REMOVE"; }; 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/configurator/configurator-state/initial.tsx b/app/configurator/configurator-state/initial.tsx index fcfcd3cc7..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: { - filters: [], + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, }, }; }; diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index 4f7c44536..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: { - filters: [], + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, }, }, groupedColumnChart: { @@ -198,7 +205,14 @@ export const configStateMock = { ], activeChartKey: "2of7iJAjccuj", dashboardFilters: { - filters: [], + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, }, }, } satisfies Record; diff --git a/app/configurator/configurator-state/reducer.spec.tsx b/app/configurator/configurator-state/reducer.spec.tsx index c9d446623..ecfde67ce 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( @@ -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"); }); }); diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index 5b9d81878..63f57a570 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, @@ -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( @@ -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}` ); } } @@ -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( @@ -948,7 +947,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, @@ -1057,41 +1058,27 @@ const reducer_: Reducer = ( newDraft.activeChartKey = action.value.chartKey; return newDraft; - case "DASHBOARD_FILTER_ADD": + case "DASHBOARD_TIME_RANGE_FILTER_UPDATE": if (isLayouting(draft)) { - setWith( - draft, - "dashboardFilters.filters", - uniqBy( - [...(draft.dashboardFilters?.filters ?? []), action.value], - (x) => x.componentIri - ), - Object - ); + setWith(draft, "dashboardFilters.timeRange", action.value, Object); } - return draft; - case "DASHBOARD_FILTER_UPDATE": + case "DASHBOARD_TIME_RANGE_FILTER_REMOVE": if (isLayouting(draft)) { - const idx = draft.dashboardFilters?.filters.findIndex( - (f) => f.componentIri === action.value.componentIri - ); - - if (idx !== undefined && idx > -1) { - const newFilters = [...(draft.dashboardFilters?.filters ?? [])]; - newFilters.splice(idx, 1, action.value); - setWith(draft, "dashboardFilters.filters", newFilters, Object); - } - } - return draft; - - case "DASHBOARD_FILTER_REMOVE": - if (isLayouting(draft)) { - const newFilters = draft.dashboardFilters?.filters.filter( - (f) => f.componentIri !== action.value + setWith( + draft, + "dashboardFilters.timeRange", + { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + } as DashboardTimeRangeFilter, + Object ); - setWith(draft, "dashboardFilters.filters", newFilters, Object); } return draft; diff --git a/app/configurator/interactive-filters/time-slider.tsx b/app/configurator/interactive-filters/time-slider.tsx index 49bfeac0f..e2433b5e6 100644 --- a/app/configurator/interactive-filters/time-slider.tsx +++ b/app/configurator/interactive-filters/time-slider.tsx @@ -144,9 +144,7 @@ export const TimeSlider = (props: TimeSliderProps) => { return new Timeline(timelineProps); }, [timelineProps]); - if ( - dashboardFilters.sharedFilters.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 e851369fd..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: { filters: [] }, + dashboardFilters: { + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, + }, }} > @@ -98,8 +107,7 @@ const ColumnsStory = { ), }; -export { ColumnsStory as Columns }; -export { ScatterplotStory as Scatterplot }; +export { ColumnsStory as Columns, ScatterplotStory as Scatterplot }; const ScatterplotStory = { render: () => ( @@ -119,7 +127,16 @@ const ScatterplotStory = { }, chartConfigs: [chartConfig], activeChartKey: "scatterplot", - dashboardFilters: { filters: [] }, + dashboardFilters: { + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, + }, }} > diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index 3907423d0..73b8f5b13 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -89,7 +89,14 @@ export const states: ConfiguratorState[] = [ ], activeChartKey: "column", dashboardFilters: { - filters: [], + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, }, }, ]; diff --git a/app/docs/lines.stories.tsx b/app/docs/lines.stories.tsx index 08a210de6..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: { filters: [] }, + dashboardFilters: { + timeRange: { + active: false, + timeUnit: "", + presets: { + from: "", + to: "", + }, + }, + }, }} > 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, }); 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/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 5e974ba61..3659bcc46 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"; @@ -6,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"; @@ -143,27 +142,16 @@ type InteractiveFiltersContextValue = [ StoreApi, ]; -export type SharedFilter = { - type: "timeRange"; -} & NonNullable["timeRange"]; - -export type PotentialSharedFilter = Pick; - const InteractiveFiltersContext = createContext< | { + potentialTimeRangeFilterIris: string[]; + timeRange: DashboardTimeRangeFilter | undefined; stores: Record; - potentialSharedFilters: PotentialSharedFilter[]; - sharedFilters: SharedFilter[]; } | undefined >(undefined); -/** - * Returns filters that are shared across multiple charts. - */ -const getPotentialSharedFilters = ( - chartConfigs: ChartConfig[] -): PotentialSharedFilter[] => { +const getPotentialTimeRangeFilterIris = (chartConfigs: ChartConfig[]) => { const temporalDimensions = chartConfigs.flatMap((config) => { const chartSpec = getChartSpec(config); const temporalEncodings = chartSpec.encodings.filter((x) => @@ -193,16 +181,7 @@ const getPotentialSharedFilters = ( return chartTemporalDimensions; }); - const sharedTemporalDimensions = Object.values( - groupBy(temporalDimensions, (x) => x.componentIri) - ).filter((x) => x.length > 1); - - return sharedTemporalDimensions.map((x) => { - return { - type: "timeRange", - componentIri: x[0].componentIri, - }; - }); + return temporalDimensions.map((dimension) => dimension.componentIri); }; /** @@ -217,8 +196,8 @@ export const InteractiveFiltersProvider = ({ const [state] = useConfiguratorState(hasChartConfigs); const storeRefs = useRef>>({}); - const potentialSharedFilters = useMemo(() => { - return getPotentialSharedFilters(chartConfigs); + const potentialTimeRangeFilterIris = useMemo(() => { + return getPotentialTimeRangeFilterIris(chartConfigs); }, [chartConfigs]); const stores = useMemo< @@ -239,15 +218,15 @@ export const InteractiveFiltersProvider = ({ ); }, [chartConfigs]); - const sharedFilters = state.dashboardFilters?.filters; + const timeRange = state.dashboardFilters?.timeRange; const ctxValue = useMemo( () => ({ stores, - potentialSharedFilters, - sharedFilters: sharedFilters ?? [], + potentialTimeRangeFilterIris, + timeRange, }), - [stores, potentialSharedFilters, sharedFilters] + [stores, potentialTimeRangeFilterIris, timeRange] ); return ( 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 { diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts index 903d5a1dc..9aa78f40a 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,58 @@ export const configuratorStateMigrations: Migration[] = [ }); }, }, + { + description: "ALL (modify dashboardFilters)", + from: "3.4.0", + to: "3.5.0", + up: (config) => { + const oldTimeRangeFilter = config.dashboardFilters.filters[0]; + const newConfig = { + ...config, + version: "3.5.0", + dashboardFilters: { + 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: [ + { + type: "timeRange", + active: oldTimeRangeFilter.active, + componentIri: "", + presets: { + type: "range", + from: oldTimeRangeFilter.presets.from, + to: oldTimeRangeFilter.presets.to, + }, + }, + ], + }, + }; + return newConfig; + }, + }, ]; export const migrateConfiguratorState = makeMigrate(