diff --git a/app/charts/shared/legend-color.tsx b/app/charts/shared/legend-color.tsx index 467eed1b0..13a9b4afe 100644 --- a/app/charts/shared/legend-color.tsx +++ b/app/charts/shared/legend-color.tsx @@ -1,6 +1,7 @@ import { Theme, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; import clsx from "clsx"; +import { ascending } from "d3"; import React, { memo, useMemo } from "react"; import { @@ -11,6 +12,7 @@ import { useInteractiveFilters } from "@/charts/shared/use-interactive-filters"; import Flex from "@/components/flex"; import { Checkbox } from "@/components/form"; import { + DataSource, isSegmentInConfig, useReadOnlyConfiguratorState, } from "@/configurator"; @@ -141,6 +143,33 @@ export const InteractiveLegendColor = () => { ); }; +const useDimension = ({ + dataset, + dataSource, + locale, + dimensionIri, +}: { + dataset: string; + dataSource: DataSource; + locale: string; + dimensionIri?: string; +}) => { + const [{ data: cubeMetadata }] = useDataCubeMetadataWithComponentValuesQuery({ + variables: { + iri: dataset, + sourceType: dataSource.type, + sourceUrl: dataSource.url, + locale: locale, + }, + pause: !dimensionIri, + }); + return useMemo(() => { + return cubeMetadata?.dataCubeByIri?.dimensions.find( + (d) => d.iri === dimensionIri + ); + }, [cubeMetadata?.dataCubeByIri?.dimensions, dimensionIri]); +}; + const useLegendGroups = ({ labels, title, @@ -160,27 +189,23 @@ const useLegendGroups = ({ } const locale = useLocale(); + // FIXME: should color field also be included here? - const segment = isSegmentInConfig(configState.chartConfig) + const segmentField = isSegmentInConfig(configState.chartConfig) ? configState.chartConfig.fields.segment : null; - const { dataSet, dataSource } = configState; - const [{ data: cubeMetadata }] = useDataCubeMetadataWithComponentValuesQuery({ - variables: { - iri: dataSet, - sourceType: dataSource.type, - sourceUrl: dataSource.url, - locale: locale, - }, + + const { dataSet: dataset, dataSource } = configState; + const segmentDimension = useDimension({ + dataset, + dataSource: dataSource, + locale: locale, + dimensionIri: segmentField?.componentIri, }); - const segmentDimension = useMemo(() => { - return cubeMetadata?.dataCubeByIri?.dimensions.find( - (d) => d.iri === segment?.componentIri - ); - }, [cubeMetadata?.dataCubeByIri?.dimensions, segment?.componentIri]); + const [hierarchyResp] = useDimensionHierarchyQuery({ variables: { - cubeIri: dataSet, + cubeIri: dataset, dimensionIri: segmentDimension?.iri!, sourceType: dataSource.type, sourceUrl: dataSource.url, @@ -192,22 +217,33 @@ const useLegendGroups = ({ hierarchyResp?.data?.dataCubeByIri?.dimensionByIri?.hierarchy; const groups = useMemo(() => { + const groupsMap = new Map(); if (!hierarchy) { - return new Map([[title ? [{ label: title }] : [], labels]]); - } - const labelSet = new Set(labels); - const groups = new Map(); - [ - ...dfs(hierarchy, (node, { parents }) => { + groupsMap.set(title ? [{ label: title } as HierarchyValue] : [], labels); + } else { + const labelSet = new Set(labels); + dfs(hierarchy, (node, { parents }) => { if (!labelSet.has(node.label)) { return; } - groups.set(parents, groups.get(parents) || []); - groups.get(parents)?.push(node.label); - }), - ]; + groupsMap.set(parents, groupsMap.get(parents) || []); + groupsMap.get(parents)?.push(node.label); + }); + } + + const groups = Array.from(groupsMap.entries()); + if (segmentField && "sorting" in segmentField) { + // Re-sort hierarchy groups against the label order that we have received + const labelOrder = Object.fromEntries( + labels.map((x, i) => [x, i] as const) + ); + groups.forEach(([_groupName, entries]) => { + entries.sort((a, b) => ascending(labelOrder[a], labelOrder[b])); + }); + } + return groups; - }, [hierarchy, labels, title]); + }, [hierarchy, labels, segmentField, title]); return groups; }; @@ -276,16 +312,16 @@ const LegendColorContent = ({ symbol: LegendSymbol; }) => { const classes = useStyles(); - const groupList = Array.from(groups.entries()); + return ( {groups - ? groupList.map(([g, colorValues]) => { + ? groups.map(([g, colorValues]) => { const headerLabelsArray = g.map((n) => n.label); return ( @@ -328,5 +364,9 @@ export const LegendItem = ({ symbol: LegendSymbol; }) => { const classes = useItemStyles({ symbol, color }); - return {item}; + return ( + + {item} + + ); }; diff --git a/app/graphql/resolvers/rdf.ts b/app/graphql/resolvers/rdf.ts index ab73d5cf0..074f44b51 100644 --- a/app/graphql/resolvers/rdf.ts +++ b/app/graphql/resolvers/rdf.ts @@ -200,7 +200,12 @@ export const dataCubeDimensionByIri: NonNullable< > = async ({ cube, locale }, { iri }, { setup }, info) => { const { sparqlClient } = await setup(info); const dimension = ( - await getCubeDimensions({ cube, locale, sparqlClient }) + await getCubeDimensions({ + cube, + locale, + sparqlClient, + dimensionIris: [iri], + }) ).find((d) => iri === d.data.iri); return dimension ?? null; }; diff --git a/app/pages/_document.tsx b/app/pages/_document.tsx index 310d65d08..770cf42c1 100644 --- a/app/pages/_document.tsx +++ b/app/pages/_document.tsx @@ -7,6 +7,7 @@ class MyDocument extends Document { return ( + {/* eslint-disable-next-line @next/next/no-sync-scripts */} {GA_TRACKING_ID && ( <> @@ -24,7 +25,7 @@ class MyDocument extends Document {
- + diff --git a/app/rdf/queries.ts b/app/rdf/queries.ts index 37884f5ec..b4e843b3b 100644 --- a/app/rdf/queries.ts +++ b/app/rdf/queries.ts @@ -158,13 +158,19 @@ export const getCubeDimensions = async ({ cube, locale, sparqlClient, + dimensionIris, }: { cube: Cube; locale: string; sparqlClient: ParsingClient; + dimensionIris?: string[]; }): Promise => { try { - const dimensions = cube.dimensions.filter(isObservationDimension); + const dimensions = cube.dimensions + .filter(isObservationDimension) + .filter((x) => + dimensionIris ? dimensionIris.includes(x.path.value) : true + ); const dimensionUnits = dimensions.flatMap(getDimensionUnits); const dimensionUnitIndex = index( diff --git a/e2e/actions.ts b/e2e/actions.ts index 04923b13d..da5e4ef81 100644 --- a/e2e/actions.ts +++ b/e2e/actions.ts @@ -38,9 +38,7 @@ export const createActions = ({ chartLoadedOptions?: Parameters[0] ) => { await page.goto( - `en/browse/create/new?cube=${encodeURIComponent( - iri - )}&dataSource=${dataSource}` + `en/create/new?cube=${encodeURIComponent(iri)}&dataSource=${dataSource}` ); await selectors.chart.loaded(chartLoadedOptions); diff --git a/e2e/sorting.spec.ts b/e2e/sorting.spec.ts index f36fc7475..bdf2ec01b 100644 --- a/e2e/sorting.spec.ts +++ b/e2e/sorting.spec.ts @@ -77,3 +77,55 @@ test("Segment sorting", async ({ await actions.mui.selectOption("Automatic"); } }); + +test("Segment sorting with hierarchy", async ({ + actions, + selectors, + screen, + within, + page, +}) => { + await actions.chart.createFrom( + "https://environment.ld.admin.ch/foen/nfi/49-19-44/cube/1", + "Int" + ); + await actions.editor.selectActiveField("Color"); + + // Wait for color section to be ready + await selectors.edition.controlSection("Color").waitFor(); + + await within(selectors.edition.controlSection("Color")) + .getByText("None") + .click(); + + await actions.mui.selectOption("production region"); + await selectors.chart.loaded(); + + await selectors.edition.filtersLoaded(); + await selectors.chart.colorLegend(undefined, { setTimeout: 5_000 }); + + await within(await selectors.chart.colorLegend()).findByText( + "Southern Alps", + undefined, + { timeout: 5000 } + ); + + const legendItems = await selectors.chart.colorLegendItems(); + + expect(await legendItems.allInnerTexts()).toEqual([ + "Jura", + "Plateau", + "Pre-Alps", + "Alps", + "Southern Alps", + ]); + + await screen.getByText("Z → A").click(); + expect(await legendItems.allInnerTexts()).toEqual([ + "Southern Alps", + "Alps", + "Pre-Alps", + "Plateau", + "Jura", + ]); +});