diff --git a/app/components/debug-search.tsx b/app/components/debug-search.tsx index 0776f6d5b..029fcac1f 100644 --- a/app/components/debug-search.tsx +++ b/app/components/debug-search.tsx @@ -54,13 +54,13 @@ const Search = ({ useEffect(() => { startTimeRef.current = Date.now(); - }, [query, locale]); + }, [query, locale, includeDrafts]); const [cubes] = useDataCubesQuery({ variables: { locale: locale, query: query, - filters: filters.map(({ name, type, value }) => ({ type, value })), + filters: filters.map(({ type, value }) => ({ type, value })), includeDrafts, sourceUrl, sourceType: "sparql", @@ -69,7 +69,6 @@ const Search = ({ useEffect(() => { if (cubes.data) { - console.log("seting end time", query); setEndTime(Date.now()); } }, [cubes.data]); diff --git a/app/configurator/components/ui-helpers.spec.ts b/app/configurator/components/ui-helpers.spec.ts index 70d1eebe1..f75140e1b 100644 --- a/app/configurator/components/ui-helpers.spec.ts +++ b/app/configurator/components/ui-helpers.spec.ts @@ -56,6 +56,14 @@ describe("useDimensionFormatters", () => { isNumerical: true, isKeyDimension: false, } as DimensionMetaDataFragment, + { + iri: "iri-currency", + isNumerical: true, + isKeyDimension: false, + isCurrency: true, + currencyExponent: 1, + __typename: "Measure", + } as DimensionMetaDataFragment, ]) ); return { formatters }; @@ -75,6 +83,11 @@ describe("useDimensionFormatters", () => { const { formatters } = setup(); expect(formatters["iri-number"]("2.33333")).toEqual("2,33"); }); + + it("should work with currencies", () => { + const { formatters } = setup(); + expect(formatters["iri-currency"]("20002.3333")).toEqual("20'002,3"); + }); }); describe("time intervals", () => { diff --git a/app/configurator/components/ui-helpers.ts b/app/configurator/components/ui-helpers.ts index 1ea2e5a9a..c4027242a 100644 --- a/app/configurator/components/ui-helpers.ts +++ b/app/configurator/components/ui-helpers.ts @@ -51,6 +51,7 @@ import { ChartProps } from "../../charts/shared/use-chart-state"; import { Observation } from "../../domain/data"; import { DimensionMetaDataFragment, + Measure, TemporalDimension, TimeUnit, } from "../../graphql/query-hooks"; @@ -149,9 +150,16 @@ const namedNodeFormatter = (d: DimensionMetaDataFragment) => { }; }; +const currencyFormatter = (d: Measure, locale: string) => { + const formatLocale = getD3FormatLocale(locale); + // Use the currency exponent from the dimension, with default 2 + return formatLocale.format(`,.${d.currencyExponent || 2}f`); +}; + export const useDimensionFormatters = ( dimensions: DimensionMetaDataFragment[] ) => { + const locale = useLocale(); const formatNumber = useFormatNumber() as unknown as ( d: number | string ) => string; @@ -161,16 +169,28 @@ export const useDimensionFormatters = ( return useMemo(() => { return Object.fromEntries( dimensions.map((d) => { - return [ - d.iri, - d.__typename === "Measure" || d.isNumerical - ? formatNumber - : d.__typename === "TemporalDimension" - ? dateFormatterFromDimension(d, dateFormatters, formatDateAuto) - : isNamedNodeDimension(d) - ? namedNodeFormatter(d) - : formatIdentity, - ]; + let formatter: (s: any) => string; + if (d.__typename === "Measure") { + if (d.isCurrency) { + formatter = currencyFormatter(d, locale); + } else { + formatter = formatNumber; + } + } else if (d.__typename === "TemporalDimension") { + formatter = dateFormatterFromDimension( + d, + dateFormatters, + formatDateAuto + ); + } else if (isNamedNodeDimension(d)) { + formatter = namedNodeFormatter(d); + } else if (d.isNumerical) { + formatter = formatNumber; + } else { + formatter = formatIdentity; + } + + return [d.iri, formatter]; }) ); }, [dimensions, formatNumber, dateFormatters, formatDateAuto]); diff --git a/app/graphql/queries/data-cubes.graphql b/app/graphql/queries/data-cubes.graphql index 056829163..f6a0ae820 100644 --- a/app/graphql/queries/data-cubes.graphql +++ b/app/graphql/queries/data-cubes.graphql @@ -53,6 +53,10 @@ fragment dimensionMetaData on Dimension { timeUnit timeFormat } + ... on Measure { + isCurrency + currencyExponent + } } query DataCubePreview( diff --git a/app/graphql/query-hooks.ts b/app/graphql/query-hooks.ts index 324e20d37..ebf3d5c0b 100644 --- a/app/graphql/query-hooks.ts +++ b/app/graphql/query-hooks.ts @@ -226,6 +226,8 @@ export type Measure = Dimension & { order?: Maybe; isNumerical: Scalars['Boolean']; isKeyDimension: Scalars['Boolean']; + isCurrency?: Maybe; + currencyExponent?: Maybe; values: Array; related?: Maybe>; hierarchy?: Maybe>; @@ -453,7 +455,7 @@ type DimensionMetaData_GeoCoordinatesDimension_Fragment = { __typename: 'GeoCoor type DimensionMetaData_GeoShapesDimension_Fragment = { __typename: 'GeoShapesDimension', iri: string, label: string, isNumerical: boolean, isKeyDimension: boolean, order?: Maybe, values: Array, unit?: Maybe, related?: Maybe> }; -type DimensionMetaData_Measure_Fragment = { __typename: 'Measure', iri: string, label: string, isNumerical: boolean, isKeyDimension: boolean, order?: Maybe, values: Array, unit?: Maybe, related?: Maybe> }; +type DimensionMetaData_Measure_Fragment = { __typename: 'Measure', isCurrency?: Maybe, currencyExponent?: Maybe, iri: string, label: string, isNumerical: boolean, isKeyDimension: boolean, order?: Maybe, values: Array, unit?: Maybe, related?: Maybe> }; type DimensionMetaData_NominalDimension_Fragment = { __typename: 'NominalDimension', iri: string, label: string, isNumerical: boolean, isKeyDimension: boolean, order?: Maybe, values: Array, unit?: Maybe, related?: Maybe> }; @@ -810,6 +812,10 @@ export const DimensionMetaDataFragmentDoc = gql` timeUnit timeFormat } + ... on Measure { + isCurrency + currencyExponent + } } `; export const HierarchyValueFieldsFragmentDoc = gql` diff --git a/app/graphql/resolver-types.ts b/app/graphql/resolver-types.ts index bd5d5dfa4..ccf5ca06b 100644 --- a/app/graphql/resolver-types.ts +++ b/app/graphql/resolver-types.ts @@ -232,6 +232,8 @@ export type Measure = Dimension & { order?: Maybe; isNumerical: Scalars['Boolean']; isKeyDimension: Scalars['Boolean']; + isCurrency?: Maybe; + currencyExponent?: Maybe; values: Array; related?: Maybe>; hierarchy?: Maybe>; @@ -710,6 +712,8 @@ export type MeasureResolvers, ParentType, ContextType>; isNumerical?: Resolver; isKeyDimension?: Resolver; + isCurrency?: Resolver, ParentType, ContextType>; + currencyExponent?: Resolver, ParentType, ContextType>; values?: Resolver, ParentType, ContextType, RequireFields>; related?: Resolver>, ParentType, ContextType>; hierarchy?: Resolver>, ParentType, ContextType, RequireFields>; diff --git a/app/graphql/resolvers/index.ts b/app/graphql/resolvers/index.ts index c97277701..4d208fd34 100644 --- a/app/graphql/resolvers/index.ts +++ b/app/graphql/resolvers/index.ts @@ -87,7 +87,7 @@ const DataCube: DataCubeResolvers = { }, }; -const mkDimensionResolvers = (debugName: string): Resolvers["Dimension"] => ({ +const mkDimensionResolvers = (type: string): Resolvers["Dimension"] => ({ // TODO: how to pass dataSource here? If it's possible, then we also could have // different resolvers for RDF and SQL. __resolveType({ data: { dataKind, scaleType } }) { @@ -160,7 +160,10 @@ export const resolvers: Resolvers = { getSparqlEditorUrl({ query }), }, Dimension: { - __resolveType({ data: { dataKind, scaleType } }) { + __resolveType({ data: { dataKind, scaleType, isMeasureDimension } }) { + if (isMeasureDimension) { + return "Measure"; + } if (dataKind === "Time") { return "TemporalDimension"; } else if (dataKind === "GeoCoordinates") { @@ -175,25 +178,25 @@ export const resolvers: Resolvers = { }, }, NominalDimension: { - ...mkDimensionResolvers("nominal"), + ...mkDimensionResolvers("NominalDimension"), }, OrdinalDimension: { - ...mkDimensionResolvers("ordinal"), + ...mkDimensionResolvers("OrdinalDimension"), }, TemporalDimension: { - ...mkDimensionResolvers("temporal"), + ...mkDimensionResolvers("TemporalDimension"), timeUnit: ({ data: { timeUnit } }) => timeUnit!, timeFormat: ({ data: { timeFormat } }) => timeFormat!, }, GeoCoordinatesDimension: { - ...mkDimensionResolvers("geocoordinates"), + ...mkDimensionResolvers("GeoCoordinatesDimension"), geoCoordinates: async (parent, args, { setup }, info) => { const { loaders } = await setup(info); return await loaders.geoCoordinates.load(parent); }, }, GeoShapesDimension: { - ...mkDimensionResolvers("geoshapes"), + ...mkDimensionResolvers("GeoShapesDimension"), geoShapes: async (parent, args, { setup }, info) => { const { loaders } = await setup(info); const dimValues = (await loaders.dimensionValues.load( @@ -229,6 +232,12 @@ export const resolvers: Resolvers = { }, }, Measure: { - ...mkDimensionResolvers("measure"), + ...mkDimensionResolvers("Measure"), + isCurrency: ({ data: { isCurrency } }) => { + return isCurrency; + }, + currencyExponent: ({ data: { currencyExponent } }) => { + return currencyExponent || 0; + }, }, }; diff --git a/app/graphql/resolvers/sql.ts b/app/graphql/resolvers/sql.ts index 80a66da6c..3f182464f 100644 --- a/app/graphql/resolvers/sql.ts +++ b/app/graphql/resolvers/sql.ts @@ -89,6 +89,10 @@ const parseSQLDimension = ( iri, name, isLiteral: true, + + // FIXME: Handle currencies in SQL resolvers + isCurrency: false, + currencyExponent: 0, // FIXME: not only measures can be numerical isNumerical: isMeasure, isKeyDimension: true, diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql index b4f02ae6c..c77c19f24 100644 --- a/app/graphql/schema.graphql +++ b/app/graphql/schema.graphql @@ -204,6 +204,8 @@ type Measure implements Dimension { order: Int isNumerical: Boolean! isKeyDimension: Boolean! + isCurrency: Boolean + currencyExponent: Int values( sourceType: String! sourceUrl: String! diff --git a/app/graphql/shared-types.ts b/app/graphql/shared-types.ts index 4af091f7f..a6c671e3b 100644 --- a/app/graphql/shared-types.ts +++ b/app/graphql/shared-types.ts @@ -49,6 +49,8 @@ export type ResolvedDimension = { isNumerical: boolean; isKeyDimension: boolean; isMeasureDimension: boolean; + isCurrency: boolean; + currencyExponent?: number; hasUndefinedValues: boolean; unit?: string; dataType?: string; diff --git a/app/rdf/parse.ts b/app/rdf/parse.ts index 7fb1c6e37..a67b8b81a 100644 --- a/app/rdf/parse.ts +++ b/app/rdf/parse.ts @@ -195,7 +195,10 @@ export const parseCubeDimension = ({ dim: CubeDimension; cube: Cube; locale: string; - units?: Map; + units?: Map< + string, + { iri: Term; label?: Term; isCurrency?: Term; currencyExponent?: Term } + >; }): ResolvedDimension => { const outOpts = { language: getQueryLocales(locale) }; @@ -227,9 +230,8 @@ export const parseCubeDimension = ({ .terms.some((t) => t.equals(ns.cube.MeasureDimension)); const unitTerm = dim.out(ns.qudt.unit).term; - const dimensionUnit = unitTerm - ? units?.get(unitTerm.value)?.label?.value - : undefined; + const dimensionUnit = unitTerm ? units?.get(unitTerm.value) : undefined; + const dimensionUnitLabel = dimensionUnit?.label?.value; const rawOrder = dim.out(ns.sh.order).value; const order = rawOrder !== undefined ? parseInt(rawOrder, 10) : undefined; @@ -247,8 +249,12 @@ export const parseCubeDimension = ({ isKeyDimension, isMeasureDimension, hasUndefinedValues, - unit: dimensionUnit, + unit: dimensionUnitLabel, dataType: dataType?.value, + isCurrency: !!dimensionUnit?.isCurrency?.value, + currencyExponent: dimensionUnit?.currencyExponent?.value + ? parseInt(dimensionUnit?.currencyExponent?.value) + : undefined, name: dim.out(ns.schema.name, outOpts).value ?? dim.path?.value!, order: order, dataKind: dataKindTerm?.equals(ns.time.GeneralDateTimeDescription) diff --git a/app/rdf/queries.ts b/app/rdf/queries.ts index a45e873aa..11e59e0fc 100644 --- a/app/rdf/queries.ts +++ b/app/rdf/queries.ts @@ -39,7 +39,7 @@ import { loadDimensionValues } from "./query-dimension-values"; import { loadResourceLabels } from "./query-labels"; import { loadResourcePositions } from "./query-positions"; import { loadUnversionedResources } from "./query-sameas"; -import { loadUnitLabels } from "./query-unit-labels"; +import { loadUnits } from "./query-unit-labels"; const DIMENSION_VALUE_UNDEFINED = ns.cube.Undefined.value; @@ -173,8 +173,8 @@ export const getCubeDimensions = async ({ return t ? [t] : []; }); - const dimensionUnitLabels = index( - await loadUnitLabels({ + const dimensionUnitIndex = index( + await loadUnits({ ids: dimensionUnits, locale: "en", // No other locales exist yet sparqlClient, @@ -187,7 +187,7 @@ export const getCubeDimensions = async ({ dim, cube, locale, - units: dimensionUnitLabels, + units: dimensionUnitIndex, }); }); } catch (e) { diff --git a/app/rdf/query-unit-labels.ts b/app/rdf/query-unit-labels.ts index acabfe308..858f48f99 100644 --- a/app/rdf/query-unit-labels.ts +++ b/app/rdf/query-unit-labels.ts @@ -10,8 +10,9 @@ interface ResourceLabel { label?: Term; } -const buildUnitLabelsQuery = (values: Term[], locale: string) => { - return SELECT.DISTINCT`?iri ?label`.WHERE` +const buildUnitsQuery = (values: Term[], locale: string) => { + return SELECT.DISTINCT`?iri ?label ?isCurrency ?currencyExponent ?isCurrency` + .WHERE` values ?iri { ${values} } @@ -21,6 +22,9 @@ const buildUnitLabelsQuery = (values: Term[], locale: string) => { OPTIONAL { ?iri ${ns.qudt.ucumCode} ?ucumCode } OPTIONAL { ?iri ${ns.qudt.expression} ?expression } + OPTIONAL { ?iri ?isCurrency ${ns.qudt.CurrencyUnit} } + OPTIONAL { ?iri ${ns.qudt.currencyExponent} ?currencyExponent } + BIND(str(coalesce(str(?symbol), str(?ucumCode), str(?expression), str(?rdfsLabel), "?")) AS ?label) FILTER ( lang(?rdfsLabel) = "${locale}" ) @@ -30,7 +34,7 @@ const buildUnitLabelsQuery = (values: Term[], locale: string) => { /** * Load labels for a list of unit IDs */ -export async function loadUnitLabels({ +export async function loadUnits({ ids, locale = "en", sparqlClient, @@ -42,6 +46,6 @@ export async function loadUnitLabels({ return batchLoad({ ids, sparqlClient, - buildQuery: (values: Term[]) => buildUnitLabelsQuery(values, locale), + buildQuery: (values: Term[]) => buildUnitsQuery(values, locale), }); }