diff --git a/CHANGELOG.md b/CHANGELOG.md index 2089b2802..028126f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ You can also check the - Features - Implemented Content Security Policy (CSP) - It's now possible to export charts as images + - Added support for custom colors in single and multi-colored charts, enabling enhanced visual customization. + - Introduced a new Color Picker, offering greater flexibility and precision in chart color selection. - Added Footer to the Profile Page - Fixes - Addressed security flaw allowing the injection of arbitrary URLs diff --git a/app/charts/area/areas-state.tsx b/app/charts/area/areas-state.tsx index 2246fef23..397746ca0 100644 --- a/app/charts/area/areas-state.tsx +++ b/app/charts/area/areas-state.tsx @@ -248,7 +248,8 @@ const useAreasState = ( const xScaleTimeRange = scaleTime().domain(xScaleTimeRangeDomain); const colors = scaleOrdinal(); - if (segmentDimension && fields.segment?.colorMapping) { + if (segmentDimension && fields.color?.type === "segment") { + const segmentColor = fields.color; const orderedSegmentLabelsAndColors = allSegments.map((segment) => { const dvIri = segmentsByAbbreviationOrLabel.get(segment)?.value ?? @@ -257,7 +258,7 @@ const useAreasState = ( return { label: segment, - color: fields.segment?.colorMapping![dvIri] ?? schemeCategory10[0], + color: segmentColor.colorMapping[dvIri] ?? schemeCategory10[0], }; }); @@ -265,7 +266,12 @@ const useAreasState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range( + getPalette({ + paletteId: fields.color?.paletteId, + colorField: fields.color, + }) + ); } colors.unknown(() => undefined); @@ -276,8 +282,7 @@ const useAreasState = ( xScaleTimeRange, }; }, [ - fields.segment?.palette, - fields.segment?.colorMapping, + fields.color, getX, scalesData, timeRangeData, diff --git a/app/charts/bar/bars-grouped-state.tsx b/app/charts/bar/bars-grouped-state.tsx index c4feaaed7..aa3238a43 100644 --- a/app/charts/bar/bars-grouped-state.tsx +++ b/app/charts/bar/bars-grouped-state.tsx @@ -188,7 +188,7 @@ const useBarsGroupedState = ( } = useMemo(() => { const colors = scaleOrdinal(); - if (fields.segment && segmentDimension && fields.segment.colorMapping) { + if (fields.segment && segmentDimension && fields.color) { const orderedSegmentLabelsAndColors = allSegments.map((segment) => { const dvIri = segmentsByAbbreviationOrLabel.get(segment)?.value || @@ -197,7 +197,10 @@ const useBarsGroupedState = ( return { label: segment, - color: fields.segment?.colorMapping![dvIri] ?? schemeCategory10[0], + color: + fields.color.type === "segment" + ? fields.color.colorMapping![dvIri] ?? schemeCategory10[0] + : schemeCategory10[0], }; }); @@ -205,7 +208,12 @@ const useBarsGroupedState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range( + getPalette({ + paletteId: fields.color.paletteId, + colorField: fields.color, + }) + ); } colors.unknown(() => undefined); @@ -276,6 +284,7 @@ const useBarsGroupedState = ( yTimeRangeDomainLabels, }; }, [ + fields.color, fields.segment, fields.y?.sorting, fields.y?.useAbbreviations, diff --git a/app/charts/bar/bars-stacked-state.tsx b/app/charts/bar/bars-stacked-state.tsx index d37f9e6a8..83ebff75d 100644 --- a/app/charts/bar/bars-stacked-state.tsx +++ b/app/charts/bar/bars-stacked-state.tsx @@ -227,11 +227,7 @@ const useBarsStackedState = ( } = useMemo(() => { const colors = scaleOrdinal(); - if ( - fields.segment && - segmentsByAbbreviationOrLabel && - fields.segment.colorMapping - ) { + if (fields.segment && segmentsByAbbreviationOrLabel && fields.color) { const orderedSegmentLabelsAndColors = allSegments.map((segment) => { // FIXME: Labels in observations can differ from dimension values because the latter can be concatenated to only appear once per value // See https://github.com/visualize-admin/visualization-tool/issues/97 @@ -247,7 +243,10 @@ const useBarsStackedState = ( return { label: segment, - color: fields.segment?.colorMapping![dvIri] ?? schemeCategory10[0], + color: + fields.color.type === "segment" + ? fields.color.colorMapping![dvIri] ?? schemeCategory10[0] + : schemeCategory10[0], }; }); @@ -255,7 +254,12 @@ const useBarsStackedState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range( + getPalette({ + paletteId: fields.color.paletteId, + colorField: fields.color, + }) + ); } colors.unknown(() => undefined); @@ -297,6 +301,7 @@ const useBarsStackedState = ( yScaleInteraction, }; }, [ + fields.color, fields.segment, fields.y.sorting, fields.y.useAbbreviations, diff --git a/app/charts/chart-config-ui-options.spec.ts b/app/charts/chart-config-ui-options.spec.ts index 3649f4abf..e9b84a3f2 100644 --- a/app/charts/chart-config-ui-options.spec.ts +++ b/app/charts/chart-config-ui-options.spec.ts @@ -1,7 +1,6 @@ import { defaultSegmentOnChange } from "@/charts/chart-config-ui-options"; import { ColumnConfig, ScatterPlotConfig } from "@/configurator"; import { stringifyComponentId } from "@/graphql/make-component-id"; -import { DEFAULT_CATEGORICAL_PALETTE_NAME } from "@/palettes"; jest.mock("../rdf/extended-cube", () => ({ ExtendedCube: jest.fn(), @@ -41,11 +40,14 @@ describe("defaultSegmentOnChange", () => { initializing: false, selectedValues: [], }); - expect(Object.keys(chartConfig.fields)).toEqual(["segment"]); + expect(Object.keys(chartConfig.fields)).toEqual(["segment", "color"]); expect(chartConfig.fields.segment).toEqual( expect.objectContaining({ componentId, - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + sorting: { + sortingOrder: "asc", + sortingType: "byAuto", + }, }) ); }); diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts index c2edb795a..f4cb07da1 100644 --- a/app/charts/chart-config-ui-options.ts +++ b/app/charts/chart-config-ui-options.ts @@ -1,5 +1,6 @@ import { t } from "@lingui/macro"; import { group } from "d3-array"; +import { schemeCategory10 } from "d3-scale-chromatic"; import get from "lodash/get"; import setWith from "lodash/setWith"; import unset from "lodash/unset"; @@ -20,7 +21,6 @@ import { ChartConfig, ChartSubType, ChartType, - ColorField, ColorScaleType, ColumnConfig, ColumnSegmentField, @@ -28,10 +28,12 @@ import { ComboLineDualConfig, ComboLineSingleConfig, ComponentType, + fieldHasComponentId, GenericField, getAnimationField, isSortingInConfig, LineConfig, + MapColorField, MapConfig, PaletteType, PieConfig, @@ -57,7 +59,8 @@ import { Observation, SEGMENT_ENABLED_COMPONENTS, } from "@/domain/data"; -import { getDefaultCategoricalPaletteName, getPalette } from "@/palettes"; +import { getDefaultCategoricalPaletteId, getPalette } from "@/palettes"; +import { theme } from "@/themes/federal"; /** * This module controls chart controls displayed in the UI. @@ -66,11 +69,11 @@ import { getDefaultCategoricalPaletteName, getPalette } from "@/palettes"; type BaseEncodingFieldType = "animation"; type MapEncodingFieldType = "baseLayer" | "areaLayer" | "symbolLayer"; -type XYEncodingFieldType = "x" | "y" | "segment"; +type RegularChartEncodingType = "x" | "y" | "segment" | "color"; export type EncodingFieldType = | BaseEncodingFieldType | MapEncodingFieldType - | XYEncodingFieldType; + | RegularChartEncodingType; type OnEncodingOptionChange = ( value: V, @@ -176,7 +179,7 @@ const onColorComponentIdChange: OnEncodingOptionChange = ( ) => { const basePath = `fields["${field}"]`; const components = [...dimensions, ...measures]; - let newField: ColorField = DEFAULT_FIXED_COLOR_FIELD; + let newField: MapColorField = DEFAULT_FIXED_COLOR_FIELD; const component = components.find((d) => d.id === id); const currentColorComponentId = get( chartConfig, @@ -193,10 +196,13 @@ const onColorComponentIdChange: OnEncodingOptionChange = ( MULTI_FILTER_ENABLED_COMPONENTS.includes(component.__typename) || isOrdinalMeasure(component) ) { - const palette = getDefaultCategoricalPaletteName(component, colorPalette); + const paletteId = getDefaultCategoricalPaletteId( + component, + colorPalette?.paletteId + ); newField = getDefaultCategoricalColorField({ id, - palette, + paletteId, dimensionValues: component.values, }); } else if (isNumericalMeasure(component)) { @@ -414,7 +420,7 @@ export const ANIMATION_FIELD_SPEC: EncodingSpec< } const fieldComponentsMap = Object.fromEntries( - Object.entries(chartConfig.fields) + Object.entries(fieldHasComponentId(chartConfig)) .filter((d) => d[0] !== "animation") .map(([k, v]) => [v.componentId, k]) ); @@ -487,25 +493,27 @@ export const defaultSegmentOnChange: OnEncodingChange< > = (id, { chartConfig, dimensions, measures, selectedValues }) => { const components = [...dimensions, ...measures]; const component = components.find((d) => d.id === id); - const palette = getDefaultCategoricalPaletteName( + const paletteId = getDefaultCategoricalPaletteId( component, - chartConfig.fields.segment && "palette" in chartConfig.fields.segment - ? chartConfig.fields.segment.palette + chartConfig.fields.color && "paletteId" in chartConfig.fields.color + ? chartConfig.fields.color.paletteId : undefined ); const colorMapping = mapValueIrisToColor({ - palette, + paletteId, dimensionValues: component ? component.values : selectedValues, }); - if (chartConfig.fields.segment && "palette" in chartConfig.fields.segment) { + if (chartConfig.fields.segment) { chartConfig.fields.segment.componentId = id; - chartConfig.fields.segment.colorMapping = colorMapping; } else { chartConfig.fields.segment = { componentId: id, - palette, sorting: DEFAULT_SORTING, + }; + chartConfig.fields.color = { + type: "segment", + paletteId: paletteId, colorMapping, }; } @@ -558,6 +566,13 @@ const chartConfigOptionsUISpec: ChartSpecs = { delete chartConfig.fields.segment; } }, + options: { + colorPalette: { + type: "single", + paletteId: "schemaCategory10", + color: schemeCategory10[0], + }, + }, }, { field: "x", @@ -609,7 +624,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { }, options: { calculation: {}, - colorPalette: {}, + colorPalette: { + type: "single", + paletteId: "dimension", + color: theme.palette.primary.main, + }, imputation: { shouldShow: (chartConfig, data) => { return isMissingDataPresent(chartConfig, data); @@ -649,6 +668,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { } }, options: { + colorPalette: { + type: "single", + paletteId: "dimension", + color: theme.palette.primary.main, + }, showStandardError: {}, showConfidenceInterval: {}, }, @@ -765,7 +789,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { } }, }, - colorPalette: {}, + colorPalette: { + type: "single", + paletteId: "dimension", + color: theme.palette.primary.main, + }, useAbbreviations: {}, }, }, @@ -935,6 +963,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { componentTypes: ["NumericalMeasure"], filters: false, options: { + colorPalette: { + type: "single", + paletteId: "dimension", + color: theme.palette.primary.main, + }, showStandardError: {}, showConfidenceInterval: {}, }, @@ -955,7 +988,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { sorting: LINE_SEGMENT_SORTING, onChange: defaultSegmentOnChange, options: { - colorPalette: {}, + colorPalette: { + type: "single", + paletteId: "dimension", + color: theme.palette.primary.main, + }, useAbbreviations: {}, }, }, @@ -1048,7 +1085,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { sorting: PIE_SEGMENT_SORTING, onChange: defaultSegmentOnChange, options: { - colorPalette: {}, + colorPalette: { + type: "single", + paletteId: "dimension", + color: theme.palette.primary.main, + }, useAbbreviations: {}, }, }, @@ -1081,7 +1122,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { filters: true, onChange: defaultSegmentOnChange, options: { - colorPalette: {}, + colorPalette: { + type: "single", + paletteId: "dimension", + color: theme.palette.primary.main, + }, useAbbreviations: {}, }, }, @@ -1111,12 +1156,12 @@ const chartConfigOptionsUISpec: ChartSpecs = { onChange: (ids, options) => { const { chartConfig } = options; const { fields } = chartConfig; - const { y } = fields; - const palette = getPalette(y.palette); + const { color } = fields; + const palette = getPalette({ paletteId: color.paletteId }); const newColorMapping = Object.fromEntries( - ids.map((id, i) => [id, y.colorMapping[i] ?? palette[i]]) + ids.map((id, i) => [id, color.colorMapping[i] ?? palette[i]]) ); - chartConfig.fields.y.colorMapping = newColorMapping; + chartConfig.fields.color.colorMapping = newColorMapping; }, }, }, @@ -1146,11 +1191,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { onChange: (id, options) => { const { chartConfig } = options; const { fields } = chartConfig; - const { y } = fields; - chartConfig.fields.y.colorMapping = { - [id]: y.colorMapping[y.leftAxisComponentId], + const { y, color } = fields; + chartConfig.fields.color.colorMapping = { + [id]: color.colorMapping[y.leftAxisComponentId], [y.rightAxisComponentId]: - y.colorMapping[y.rightAxisComponentId], + color.colorMapping[y.rightAxisComponentId], }; }, }, @@ -1158,10 +1203,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { onChange: (id, options) => { const { chartConfig } = options; const { fields } = chartConfig; - const { y } = fields; - chartConfig.fields.y.colorMapping = { - [y.leftAxisComponentId]: y.colorMapping[y.leftAxisComponentId], - [id]: y.colorMapping[y.rightAxisComponentId], + const { y, color } = fields; + chartConfig.fields.color.colorMapping = { + [y.leftAxisComponentId]: + color.colorMapping[y.leftAxisComponentId], + [id]: color.colorMapping[y.rightAxisComponentId], }; }, }, @@ -1192,11 +1238,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { onChange: (id, options) => { const { chartConfig } = options; const { fields } = chartConfig; - const { y } = fields; - const lineColor = y.colorMapping[y.lineComponentId]; - const columnColor = y.colorMapping[y.columnComponentId]; + const { y, color } = fields; + const lineColor = color.colorMapping[y.lineComponentId]; + const columnColor = color.colorMapping[y.columnComponentId]; - chartConfig.fields.y.colorMapping = + chartConfig.fields.color.colorMapping = y.lineAxisOrientation === "left" ? { [id]: lineColor, [y.columnComponentId]: columnColor } : { [y.columnComponentId]: columnColor, [id]: lineColor }; @@ -1206,11 +1252,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { onChange: (id, options) => { const { chartConfig } = options; const { fields } = chartConfig; - const { y } = fields; - const columnColor = y.colorMapping[y.columnComponentId]; - const lineColor = y.colorMapping[y.lineComponentId]; + const { y, color } = fields; + const columnColor = color.colorMapping[y.columnComponentId]; + const lineColor = color.colorMapping[y.lineComponentId]; - chartConfig.fields.y.colorMapping = + chartConfig.fields.color.colorMapping = y.lineAxisOrientation === "left" ? { [y.lineComponentId]: lineColor, [id]: columnColor } : { [id]: columnColor, [y.lineComponentId]: lineColor }; @@ -1220,7 +1266,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { onChange: (_, options) => { const { chartConfig } = options; const { fields } = chartConfig; - const { y } = fields; + const { y, color } = fields; const lineAxisLeft = y.lineAxisOrientation === "left"; // Need the correct order to not enable "Reset color palette" button. const firstId = lineAxisLeft @@ -1230,9 +1276,9 @@ const chartConfigOptionsUISpec: ChartSpecs = { ? y.lineComponentId : y.columnComponentId; - chartConfig.fields.y.colorMapping = { - [firstId]: y.colorMapping[secondId], - [secondId]: y.colorMapping[firstId], + chartConfig.fields.color.colorMapping = { + [firstId]: color.colorMapping[secondId], + [secondId]: color.colorMapping[firstId], }; }, }, diff --git a/app/charts/column/columns-grouped-state.tsx b/app/charts/column/columns-grouped-state.tsx index 736281268..5ad4b5569 100644 --- a/app/charts/column/columns-grouped-state.tsx +++ b/app/charts/column/columns-grouped-state.tsx @@ -189,7 +189,12 @@ const useColumnsGroupedState = ( } = useMemo(() => { const colors = scaleOrdinal(); - if (fields.segment && segmentDimension && fields.segment.colorMapping) { + if ( + fields.segment && + segmentDimension && + fields.color?.type === "segment" + ) { + const segmentColor = fields.color; const orderedSegmentLabelsAndColors = allSegments.map((segment) => { const dvIri = segmentsByAbbreviationOrLabel.get(segment)?.value || @@ -198,7 +203,7 @@ const useColumnsGroupedState = ( return { label: segment, - color: fields.segment?.colorMapping![dvIri] ?? schemeCategory10[0], + color: segmentColor.colorMapping![dvIri] ?? schemeCategory10[0], }; }); @@ -206,7 +211,12 @@ const useColumnsGroupedState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range( + getPalette({ + paletteId: fields.color?.paletteId, + colorField: fields.color, + }) + ); } colors.unknown(() => undefined); @@ -278,6 +288,7 @@ const useColumnsGroupedState = ( }; }, [ fields.segment, + fields.color, fields.x?.sorting, fields.x?.useAbbreviations, segmentDimension, diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx index e87ec0887..c1d0205e6 100644 --- a/app/charts/column/columns-stacked-state.tsx +++ b/app/charts/column/columns-stacked-state.tsx @@ -227,8 +227,9 @@ const useColumnsStackedState = ( if ( fields.segment && segmentsByAbbreviationOrLabel && - fields.segment.colorMapping + fields.color.type === "segment" ) { + const segmentColor = fields.color; const orderedSegmentLabelsAndColors = allSegments.map((segment) => { // FIXME: Labels in observations can differ from dimension values because the latter can be concatenated to only appear once per value // See https://github.com/visualize-admin/visualization-tool/issues/97 @@ -244,7 +245,7 @@ const useColumnsStackedState = ( return { label: segment, - color: fields.segment?.colorMapping![dvIri] ?? schemeCategory10[0], + color: segmentColor.colorMapping![dvIri] ?? schemeCategory10[0], }; }); @@ -252,7 +253,12 @@ const useColumnsStackedState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range( + getPalette({ + paletteId: fields.color.paletteId, + colorField: fields.color, + }) + ); } colors.unknown(() => undefined); @@ -295,6 +301,7 @@ const useColumnsStackedState = ( }; }, [ fields.segment, + fields.color, fields.x.sorting, fields.x.useAbbreviations, xDimension, diff --git a/app/charts/column/columns-state.tsx b/app/charts/column/columns-state.tsx index a6efece3d..1b78fdd8d 100644 --- a/app/charts/column/columns-state.tsx +++ b/app/charts/column/columns-state.tsx @@ -4,6 +4,8 @@ import { scaleBand, ScaleLinear, scaleLinear, + ScaleOrdinal, + scaleOrdinal, scaleTime, } from "d3-scale"; import orderBy from "lodash/orderBy"; @@ -41,6 +43,7 @@ import { useFormatNumber, useTimeFormatUnit, } from "@/formatters"; +import { getPalette } from "@/palettes"; import { getSortingOrders, makeDimensionValueSorters, @@ -56,6 +59,7 @@ export type ColumnsState = CommonChartState & xScale: ScaleBand; xScaleInteraction: ScaleBand; yScale: ScaleLinear; + colors: ScaleOrdinal; getAnnotationInfo: (d: Observation) => TooltipInfo; }; @@ -98,12 +102,24 @@ const useColumnsState = ( const { xScale, + colors, yScale, paddingYScale, xScaleTimeRange, xScaleInteraction, xTimeRangeDomainLabels, } = useMemo(() => { + const colors = scaleOrdinal(); + + if (fields.color?.type === "single") { + colors.range( + getPalette({ + paletteId: fields.color?.paletteId, + colorField: fields.color, + }) + ); + } + const sorters = makeDimensionValueSorters(xDimension, { sorting: fields.x.sorting, measureBySegment: sumsByX, @@ -162,6 +178,7 @@ const useColumnsState = ( .nice(); return { + colors, xScale, yScale, paddingYScale, @@ -170,6 +187,7 @@ const useColumnsState = ( xTimeRangeDomainLabels, }; }, [ + fields.color, getX, getXLabel, getXAsDate, @@ -261,6 +279,7 @@ const useColumnsState = ( }; return { + colors, chartType: "column", bounds, chartData, diff --git a/app/charts/column/columns.tsx b/app/charts/column/columns.tsx index d9448159a..99ccec97c 100644 --- a/app/charts/column/columns.tsx +++ b/app/charts/column/columns.tsx @@ -1,4 +1,3 @@ -import { schemeCategory10 } from "d3-scale-chromatic"; import { useEffect, useMemo, useRef } from "react"; import { ColumnsState } from "@/charts/column/columns-state"; @@ -13,7 +12,6 @@ import { renderVerticalWhiskers, } from "@/charts/shared/rendering-utils"; import { useTransitionStore } from "@/stores/transition"; -import { useTheme } from "@/themes"; export const ErrorWhiskers = () => { const { @@ -82,9 +80,16 @@ export const ErrorWhiskers = () => { }; export const Columns = () => { - const { chartData, bounds, getX, xScale, getY, yScale, getRenderingKey } = - useChartState() as ColumnsState; - const theme = useTheme(); + const { + chartData, + bounds, + getX, + xScale, + getY, + yScale, + getRenderingKey, + colors, + } = useChartState() as ColumnsState; const { margins } = bounds; const ref = useRef(null); const enableTransition = useTransitionStore((state) => state.enable); @@ -92,10 +97,6 @@ export const Columns = () => { const bandwidth = xScale.bandwidth(); const y0 = yScale(0); const renderData: RenderColumnDatum[] = useMemo(() => { - const getColor = (d: number) => { - return d <= 0 ? theme.palette.secondary.main : schemeCategory10[0]; - }; - return chartData.map((d) => { const key = getRenderingKey(d); const xScaled = xScale(getX(d)) as number; @@ -104,7 +105,7 @@ export const Columns = () => { const yScaled = yScale(y); const yRender = yScale(Math.max(y, 0)); const height = Math.max(0, Math.abs(yScaled - y0)); - const color = getColor(y); + const color = colors(key); return { key, @@ -123,7 +124,7 @@ export const Columns = () => { xScale, yScale, y0, - theme.palette.secondary.main, + colors, getRenderingKey, ]); diff --git a/app/charts/combo/combo-line-column-state-props.ts b/app/charts/combo/combo-line-column-state-props.ts index 2c5ec08e2..4e46f15c2 100644 --- a/app/charts/combo/combo-line-column-state-props.ts +++ b/app/charts/combo/combo-line-column-state-props.ts @@ -78,7 +78,7 @@ export const useComboLineColumnStateVariables = ( dimension: measuresById[lineId], id: lineId, label: getLabelWithUnit(measuresById[lineId]), - color: fields.y.colorMapping[lineId], + color: fields.color.colorMapping[lineId], getY: (d) => (d[lineId] !== null ? Number(d[lineId]) : null), getMinY: (data) => { const minY = @@ -95,7 +95,7 @@ export const useComboLineColumnStateVariables = ( dimension: measuresById[columnId], id: columnId, label: getLabelWithUnit(measuresById[columnId]), - color: fields.y.colorMapping[columnId], + color: fields.color.colorMapping[columnId], getY: (d) => (d[columnId] !== null ? Number(d[columnId]) : null), getMinY: (data) => { const minY = diff --git a/app/charts/combo/combo-line-dual-state-props.ts b/app/charts/combo/combo-line-dual-state-props.ts index 337401642..2c589f78d 100644 --- a/app/charts/combo/combo-line-dual-state-props.ts +++ b/app/charts/combo/combo-line-dual-state-props.ts @@ -60,7 +60,7 @@ export const useComboLineDualStateVariables = ( dimension: measuresById[leftId], id: leftId, label: getLabelWithUnit(measuresById[leftId]), - color: fields.y.colorMapping[leftId], + color: fields.color.colorMapping[leftId], getY: (d) => (d[leftId] !== null ? Number(d[leftId]) : null), getMinY: (data) => { const minY = @@ -77,7 +77,7 @@ export const useComboLineDualStateVariables = ( dimension: measuresById[rightId], id: rightId, label: getLabelWithUnit(measuresById[rightId]), - color: fields.y.colorMapping[rightId], + color: fields.color.colorMapping[rightId], getY: (d) => (d[rightId] !== null ? Number(d[rightId]) : null), getMinY: (data) => { const minY = diff --git a/app/charts/combo/combo-line-single-state-props.ts b/app/charts/combo/combo-line-single-state-props.ts index 135da612f..533136937 100644 --- a/app/charts/combo/combo-line-single-state-props.ts +++ b/app/charts/combo/combo-line-single-state-props.ts @@ -53,7 +53,7 @@ export const useComboLineSingleStateVariables = ( dimension: measuresById[id], id, label: measuresById[id].label, - color: fields.y.colorMapping[id], + color: fields.color.colorMapping[id], getY: (d) => (d[id] !== null ? Number(d[id]) : null), getMinY: (data) => { const minY = diff --git a/app/charts/index.spec.ts b/app/charts/index.spec.ts index 289421ce1..b8168856e 100644 --- a/app/charts/index.spec.ts +++ b/app/charts/index.spec.ts @@ -1,3 +1,5 @@ +import { schemeCategory10 } from "d3-scale-chromatic"; + import { ColumnConfig, ComboLineDualConfig, @@ -288,6 +290,11 @@ describe("chart type switch", () => { y: { componentId: "https://environment.ld.admin.ch/foen/ubd0104/value", }, + color: { + type: "segment", + paletteId: "category10", + colorMapping: {}, + }, }, interactiveFiltersConfig: { legend: { @@ -401,7 +408,7 @@ describe("chart type switch", () => { }, segment: { componentId: "https://environment.ld.admin.ch/foen/ubd000502/jahr", - palette: "category10", + paletteId: "category10", sorting: { sortingType: "byAuto", sortingOrder: "asc", @@ -481,6 +488,11 @@ describe("chart type switch", () => { y: { componentId: "A_M1", }, + color: { + paletteId: "category10", + color: schemeCategory10[0], + type: "single", + }, }, } as any as ColumnConfig; const dimensions = [ diff --git a/app/charts/index.ts b/app/charts/index.ts index 4d415b903..61038221d 100644 --- a/app/charts/index.ts +++ b/app/charts/index.ts @@ -27,6 +27,7 @@ import { ChartConfig, ChartSegmentField, ChartType, + ColorField, ColumnSegmentField, ComboChartType, ComboLineColumnFields, @@ -35,9 +36,9 @@ import { Filters, GenericField, GenericFields, - GenericSegmentField, InteractiveFiltersConfig, isAreaConfig, + isColorInConfig, isColumnConfig, isComboChartConfig, isComboLineColumnConfig, @@ -67,6 +68,7 @@ import { Component, Dimension, DimensionType, + DimensionValue, GeoCoordinatesDimension, GeoShapesDimension, getCategoricalDimensions, @@ -84,9 +86,10 @@ import { } from "@/domain/data"; import { truthy } from "@/domain/types"; import { - DEFAULT_CATEGORICAL_PALETTE_NAME, - getDefaultCategoricalPaletteName, + DEFAULT_CATEGORICAL_PALETTE_ID, + getDefaultCategoricalPaletteId, } from "@/palettes"; +import { theme } from "@/themes/federal"; import { bfs } from "@/utils/bfs"; import { CHART_CONFIG_VERSION } from "@/utils/chart-config/constants"; import { createChartId } from "@/utils/create-chart-id"; @@ -294,7 +297,7 @@ const getInitialAreaLayer = ({ component: GeoShapesDimension; measure: Measure; }): MapAreaLayer => { - const palette = getDefaultCategoricalPaletteName(measure); + const paletteId = getDefaultCategoricalPaletteId(measure); return { componentId: component.id, @@ -304,7 +307,7 @@ const getInitialAreaLayer = ({ }) : getDefaultCategoricalColorField({ id: measure.id, - palette, + paletteId, dimensionValues: measure.values, }), }; @@ -403,6 +406,11 @@ export const getInitialConfig = ( fields: { x: { componentId: areaXComponentId }, y: { componentId: numericalMeasures[0].id, imputationType: "none" }, + color: { + type: "single", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + color: theme.palette.primary.main, + }, }, }; case "column": @@ -427,6 +435,11 @@ export const getInitialConfig = ( sorting: DEFAULT_SORTING, }, y: { componentId: numericalMeasures[0].id }, + color: { + type: "single", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + color: theme.palette.primary.main, + }, }, }; @@ -452,6 +465,11 @@ export const getInitialConfig = ( sorting: DEFAULT_SORTING, }, x: { componentId: numericalMeasures[0].id }, + color: { + type: "single", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + color: theme.palette.primary.main, + }, }, }; case "line": @@ -466,6 +484,11 @@ export const getInitialConfig = ( fields: { x: { componentId: lineXComponentId }, y: { componentId: numericalMeasures[0].id }, + color: { + type: "single", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + color: theme.palette.primary.main, + }, }, }; case "map": @@ -507,7 +530,7 @@ export const getInitialConfig = ( const pieSegmentComponent = getCategoricalDimensions(dimensions)[0] ?? getGeoDimensions(dimensions)[0]; - const piePalette = getDefaultCategoricalPaletteName(pieSegmentComponent); + const piePalette = getDefaultCategoricalPaletteId(pieSegmentComponent); return { ...getGenericConfigProps(), @@ -517,10 +540,13 @@ export const getInitialConfig = ( y: { componentId: numericalMeasures[0].id }, segment: { componentId: pieSegmentComponent.id, - palette: piePalette, sorting: { sortingType: "byMeasure", sortingOrder: "asc" }, + }, + color: { + type: "segment", + paletteId: piePalette, colorMapping: mapValueIrisToColor({ - palette: piePalette, + paletteId: piePalette, dimensionValues: pieSegmentComponent.values, }), }, @@ -530,7 +556,7 @@ export const getInitialConfig = ( const scatterplotSegmentComponent = getCategoricalDimensions(dimensions)[0] || getGeoDimensions(dimensions)[0]; - const scatterplotPalette = getDefaultCategoricalPaletteName( + const scatterplotPalette = getDefaultCategoricalPaletteId( scatterplotSegmentComponent ); @@ -548,16 +574,25 @@ export const getInitialConfig = ( }, ...(scatterplotSegmentComponent ? { - segment: { - componentId: scatterplotSegmentComponent.id, - palette: scatterplotPalette, + color: { + type: "segment", + paletteId: scatterplotPalette, colorMapping: mapValueIrisToColor({ - palette: scatterplotPalette, + paletteId: scatterplotPalette, dimensionValues: scatterplotSegmentComponent.values, }), }, + segment: { + componentId: scatterplotSegmentComponent.id, + }, } - : {}), + : { + color: { + type: "single", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + color: theme.palette.primary.main, + }, + }), }, }; case "table": @@ -615,13 +650,16 @@ export const getInitialConfig = ( // Use all measures with the most common unit. y: { componentIds: yComponentIds, - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + }, + color: { + type: "measures", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, colorMapping: mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, dimensionValues: yComponentIds.map((id) => ({ value: id, label: id, - })), + })) as DimensionValue[], }), }, }, @@ -650,9 +688,12 @@ export const getInitialConfig = ( y: { leftAxisComponentId, rightAxisComponentId, - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + }, + color: { + type: "measures", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, colorMapping: mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, dimensionValues: [leftAxisComponentId, rightAxisComponentId].map( (id) => ({ value: id, @@ -688,9 +729,12 @@ export const getInitialConfig = ( lineComponentId, lineAxisOrientation: "right", columnComponentId, - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + }, + color: { + type: "measures", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, colorMapping: mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, dimensionValues: [lineComponentId, columnComponentId].map( (id) => ({ value: id, @@ -963,24 +1007,28 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: ColumnSegmentField | undefined; + let newColor: ColorField | undefined; + const yMeasure = measures.find( (d) => d.id === newChartConfig.fields.y.componentId ); // When switching from a table chart, a whole fields object is passed as oldValue. if (oldChartConfig.chartType === "table") { - const tableSegment = convertTableFieldsToSegmentField({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentAndColorFields = + convertTableFieldsToSegmentAndColorFields({ + fields: oldValue as TableFields, + dimensions, + measures, + }); - if (tableSegment) { + if (maybeSegmentAndColorFields) { newSegment = { - ...tableSegment, + ...maybeSegmentAndColorFields.segment, sorting: DEFAULT_SORTING, type: disableStacked(yMeasure) ? "grouped" : "stacked", }; + newColor = maybeSegmentAndColorFields.color; } // Otherwise we are dealing with a segment field. We shouldn't take // the segment from oldValue if the component has already been used as @@ -1000,11 +1048,22 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }), type: disableStacked(yMeasure) ? "grouped" : "stacked", }; + newColor = { + type: "segment", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + colorMapping: mapValueIrisToColor({ + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + dimensionValues: + dimensions.find((d) => d.id === oldValue.componentId)?.values || + [], + }), + }; } return produce(newChartConfig, (draft) => { - if (newSegment) { + if (newSegment && newColor?.type === "segment") { draft.fields.segment = newSegment; + draft.fields.color = newColor; } }); }, @@ -1058,24 +1117,28 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: ColumnSegmentField | undefined; + let newColor: ColorField | undefined; + const xMeasure = measures.find( (d) => d.id === newChartConfig.fields.x.componentId ); // When switching from a table chart, a whole fields object is passed as oldValue. if (oldChartConfig.chartType === "table") { - const tableSegment = convertTableFieldsToSegmentField({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentAndColorFields = + convertTableFieldsToSegmentAndColorFields({ + fields: oldValue as TableFields, + dimensions, + measures, + }); - if (tableSegment) { + if (maybeSegmentAndColorFields) { newSegment = { - ...tableSegment, + ...maybeSegmentAndColorFields.segment, sorting: DEFAULT_SORTING, type: disableStacked(xMeasure) ? "grouped" : "stacked", }; + newColor = maybeSegmentAndColorFields.color; } // Otherwise we are dealing with a segment field. We shouldn't take // the segment from oldValue if the component has already been used as @@ -1095,11 +1158,22 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }), type: disableStacked(xMeasure) ? "grouped" : "stacked", }; + newColor = { + type: "segment", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + colorMapping: mapValueIrisToColor({ + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + dimensionValues: + dimensions.find((d) => d.id === oldValue.componentId)?.values || + [], + }), + }; } return produce(newChartConfig, (draft) => { - if (newSegment) { + if (newSegment && newColor?.type === "segment") { draft.fields.segment = newSegment; + draft.fields.color = newColor; } }); }, @@ -1159,16 +1233,19 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: LineSegmentField | undefined; + let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const tableSegment = convertTableFieldsToSegmentField({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentAndColorFields = + convertTableFieldsToSegmentAndColorFields({ + fields: oldValue as TableFields, + dimensions, + measures, + }); - if (tableSegment) { - newSegment = tableSegment; + if (maybeSegmentAndColorFields) { + newSegment = maybeSegmentAndColorFields.segment; + newColor = maybeSegmentAndColorFields.color; } } else { const oldSegment = oldValue as Exclude; @@ -1179,8 +1256,6 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { if (!isTemporalDimension(segmentDimension)) { newSegment = { componentId: oldSegment.componentId, - palette: oldSegment.palette, - colorMapping: oldSegment.colorMapping, sorting: "sorting" in oldSegment && oldSegment.sorting && @@ -1188,12 +1263,21 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { ? (oldSegment.sorting ?? DEFAULT_FIXED_COLOR_FIELD) : DEFAULT_SORTING, }; + newColor = { + type: "segment", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + colorMapping: mapValueIrisToColor({ + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + dimensionValues: segmentDimension?.values || [], + }), + }; } } return produce(newChartConfig, (draft) => { - if (newSegment) { + if (newSegment && newColor?.type === "segment") { draft.fields.segment = newSegment; + draft.fields.color = newColor; } }); }, @@ -1247,19 +1331,22 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { } let newSegment: AreaSegmentField | undefined; + let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const tableSegment = convertTableFieldsToSegmentField({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentAndColorFields = + convertTableFieldsToSegmentAndColorFields({ + fields: oldValue as TableFields, + dimensions, + measures, + }); - if (tableSegment) { + if (maybeSegmentAndColorFields) { newSegment = { - ...tableSegment, + ...maybeSegmentAndColorFields.segment, sorting: DEFAULT_SORTING, }; + newColor = maybeSegmentAndColorFields.color; } } else { const oldSegment = oldValue as Exclude; @@ -1270,20 +1357,27 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { if (!isTemporalDimension(segmentDimension)) { newSegment = { componentId: oldSegment.componentId, - palette: oldSegment.palette, - colorMapping: oldSegment.colorMapping, sorting: adjustSegmentSorting({ segment: oldSegment, acceptedValues: AREA_SEGMENT_SORTING.map((d) => d.sortingType), defaultValue: "byTotalSize", }), }; + newColor = { + type: "segment", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + colorMapping: mapValueIrisToColor({ + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + dimensionValues: segmentDimension?.values || [], + }), + }; } } return produce(newChartConfig, (draft) => { - if (newSegment) { + if (newSegment && newColor?.type === "segment") { draft.fields.segment = newSegment; + draft.fields.color = newColor; } }); }, @@ -1322,29 +1416,31 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: ScatterPlotSegmentField | undefined; + let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const tableSegment = convertTableFieldsToSegmentField({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentAndColorFields = + convertTableFieldsToSegmentAndColorFields({ + fields: oldValue as TableFields, + dimensions, + measures, + }); - if (tableSegment) { - newSegment = tableSegment; + if (maybeSegmentAndColorFields) { + newSegment = maybeSegmentAndColorFields.segment; + newColor = maybeSegmentAndColorFields.color; } } else { const oldSegment = oldValue as Exclude; newSegment = { componentId: oldSegment.componentId, - palette: oldSegment.palette, - colorMapping: oldSegment.colorMapping, }; } return produce(newChartConfig, (draft) => { - if (newSegment) { + if (newSegment && newColor?.type === "segment") { draft.fields.segment = newSegment; + draft.fields.color = newColor; } }); }, @@ -1378,37 +1474,49 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: PieSegmentField | undefined; + let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const tableSegment = convertTableFieldsToSegmentField({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentAndColorFields = + convertTableFieldsToSegmentAndColorFields({ + fields: oldValue as TableFields, + dimensions, + measures, + }); - if (tableSegment) { + if (maybeSegmentAndColorFields) { newSegment = { - ...tableSegment, + ...maybeSegmentAndColorFields.segment, sorting: DEFAULT_SORTING, }; + newColor = maybeSegmentAndColorFields.color; } } else { const oldSegment = oldValue as Exclude; newSegment = { componentId: oldSegment.componentId, - palette: oldSegment.palette, - colorMapping: oldSegment.colorMapping, sorting: adjustSegmentSorting({ segment: oldSegment, acceptedValues: PIE_SEGMENT_SORTING.map((d) => d.sortingType), defaultValue: "byMeasure", }), }; + newColor = { + type: "segment", + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + colorMapping: mapValueIrisToColor({ + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, + dimensionValues: + dimensions.find((d) => d.id === oldSegment.componentId) + ?.values || [], + }), + }; } return produce(newChartConfig, (draft) => { - if (newSegment) { + if (newSegment && newColor?.type === "segment") { draft.fields.segment = newSegment; + draft.fields.color = newColor; } }); }, @@ -1536,19 +1644,22 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { const componentIds = numericalMeasures .filter((d) => d.unit === unit) .map((d) => d.id); - const palette = isSegmentInConfig(oldChartConfig) - ? (oldChartConfig.fields.segment?.palette ?? - DEFAULT_CATEGORICAL_PALETTE_NAME) + const paletteId = isColorInConfig(oldChartConfig) + ? oldChartConfig.fields.color.paletteId ?? + DEFAULT_CATEGORICAL_PALETTE_ID : isComboChartConfig(oldChartConfig) - ? oldChartConfig.fields.y.palette - : DEFAULT_CATEGORICAL_PALETTE_NAME; + ? oldChartConfig.fields.color.paletteId + : DEFAULT_CATEGORICAL_PALETTE_ID; return produce(newChartConfig, (draft) => { draft.fields.y = { componentIds, - palette, + }; + draft.fields.color = { + type: "measures", + paletteId: paletteId, colorMapping: mapValueIrisToColor({ - palette, + paletteId, dimensionValues: componentIds.map((id) => ({ value: id, label: id, @@ -1649,26 +1760,27 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { ).id; leftMeasure = getLeftMeasure(leftMeasure.id); - const palette = isSegmentInConfig(oldChartConfig) - ? (oldChartConfig.fields.segment?.palette ?? - DEFAULT_CATEGORICAL_PALETTE_NAME) + const paletteId = isColorInConfig(oldChartConfig) + ? oldChartConfig.fields.color.paletteId ?? + DEFAULT_CATEGORICAL_PALETTE_ID : isComboChartConfig(oldChartConfig) - ? oldChartConfig.fields.y.palette - : DEFAULT_CATEGORICAL_PALETTE_NAME; + ? oldChartConfig.fields.color.paletteId + : DEFAULT_CATEGORICAL_PALETTE_ID; return produce(newChartConfig, (draft) => { draft.fields.y = { leftAxisComponentId: leftMeasure.id, rightAxisComponentId: rightMeasureId as string, - palette, + }; + draft.fields.color = { + type: "measures", + paletteId: paletteId, colorMapping: mapValueIrisToColor({ - palette, - dimensionValues: [leftMeasure.id, rightMeasureId as string].map( - (id) => ({ - value: id, - label: id, - }) - ), + paletteId, + dimensionValues: [leftMeasure.id, rightMeasureId].map((id) => ({ + value: id, + label: id, + })) as DimensionValue[], }), }; }); @@ -1741,25 +1853,28 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { ) as NumericalMeasure ).id; - const palette = isSegmentInConfig(oldChartConfig) - ? (oldChartConfig.fields.segment?.palette ?? - DEFAULT_CATEGORICAL_PALETTE_NAME) + const paletteId = isColorInConfig(oldChartConfig) + ? oldChartConfig.fields.color.paletteId ?? + DEFAULT_CATEGORICAL_PALETTE_ID : isComboChartConfig(oldChartConfig) - ? oldChartConfig.fields.y.palette - : DEFAULT_CATEGORICAL_PALETTE_NAME; + ? oldChartConfig.fields.color.paletteId + : DEFAULT_CATEGORICAL_PALETTE_ID; return produce(newChartConfig, (draft) => { draft.fields.y = { columnComponentId: leftMeasure.id, lineComponentId, lineAxisOrientation: "right", - palette, + }; + draft.fields.color = { + type: "measures", + paletteId: paletteId, colorMapping: mapValueIrisToColor({ - palette, + paletteId, dimensionValues: [leftMeasure.id, lineComponentId].map((id) => ({ value: id, label: id, - })), + })) as DimensionValue[], }), }; }); @@ -2400,7 +2515,7 @@ export const getFieldComponentId = ( return (fields as $IntentionalAny)[field]?.componentId; }; -const convertTableFieldsToSegmentField = ({ +const convertTableFieldsToSegmentAndColorFields = ({ fields, dimensions, measures, @@ -2408,7 +2523,7 @@ const convertTableFieldsToSegmentField = ({ fields: TableFields; dimensions: Dimension[]; measures: Measure[]; -}): GenericSegmentField | undefined => { +}): { segment: GenericField; color: ColorField } | undefined => { const groupedColumns = group(Object.values(fields), (d) => d.isGroup) .get(true) ?.filter((d) => SEGMENT_ENABLED_COMPONENTS.includes(d.componentType)) @@ -2423,15 +2538,23 @@ const convertTableFieldsToSegmentField = ({ const actualComponent = [...dimensions, ...measures].find( (d) => d.id === componentId ) as Component; - const palette = getDefaultCategoricalPaletteName(actualComponent); + const paletteId = getDefaultCategoricalPaletteId(actualComponent); return { - componentId, - palette, - colorMapping: mapValueIrisToColor({ - palette, - dimensionValues: actualComponent.values, - }), + segment: { + componentId, + }, + color: { + type: "segment", + paletteId: paletteId, + colorMapping: mapValueIrisToColor({ + paletteId: paletteId, + dimensionValues: [componentId].map((id) => ({ + value: id, + label: id, + })), + }), + }, }; }; diff --git a/app/charts/line/lines-state.tsx b/app/charts/line/lines-state.tsx index db6ba4f5a..86c856c44 100644 --- a/app/charts/line/lines-state.tsx +++ b/app/charts/line/lines-state.tsx @@ -195,7 +195,7 @@ const useLinesState = ( // Map ordered segments to colors const colors = scaleOrdinal(); - if (fields.segment && segmentDimension && fields.segment.colorMapping) { + if (fields.segment && segmentDimension && fields.color) { const orderedSegmentLabelsAndColors = allSegments.map((segment) => { const dvIri = segmentsByAbbreviationOrLabel.get(segment)?.value || @@ -204,7 +204,10 @@ const useLinesState = ( return { label: segment, - color: fields.segment?.colorMapping![dvIri] ?? schemeCategory10[0], + color: + fields.color.type === "segment" + ? fields.color.colorMapping![dvIri] ?? schemeCategory10[0] + : schemeCategory10[0], }; }); @@ -212,7 +215,12 @@ const useLinesState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range( + getPalette({ + paletteId: fields.color?.paletteId, + colorField: fields.color, + }) + ); } // Dimensions diff --git a/app/charts/map/constants.ts b/app/charts/map/constants.ts index f9b520b3b..301968332 100644 --- a/app/charts/map/constants.ts +++ b/app/charts/map/constants.ts @@ -21,18 +21,18 @@ export const DEFAULT_FIXED_COLOR_FIELD: FixedColorField = { export const getDefaultCategoricalColorField = ({ id, - palette, + paletteId, dimensionValues, }: { id: string; - palette: string; + paletteId: string; dimensionValues: DimensionValue[]; }): CategoricalColorField => ({ type: "categorical", componentId: id, - palette, + paletteId, colorMapping: mapValueIrisToColor({ - palette, + paletteId, dimensionValues, }), opacity: DEFAULT_OTHER_COLOR_FIELD_OPACITY, @@ -40,14 +40,18 @@ export const getDefaultCategoricalColorField = ({ export const getDefaultNumericalColorField = ({ id, - colorPalette = "oranges", + colorPalette = { + type: "sequential", + paletteId: "oranges", + name: "oranges", + }, }: { id: string; colorPalette?: PaletteType; }): NumericalColorField => ({ type: "numerical", componentId: id, - palette: colorPalette, + paletteId: colorPalette.paletteId, scaleType: "continuous", interpolationType: "linear", opacity: 100, diff --git a/app/charts/map/map-legend.tsx b/app/charts/map/map-legend.tsx index 0f8155aa9..ef54358f8 100644 --- a/app/charts/map/map-legend.tsx +++ b/app/charts/map/map-legend.tsx @@ -21,7 +21,7 @@ import { useChartTheme } from "@/charts/shared/use-chart-theme"; import { useInteraction } from "@/charts/shared/use-interaction"; import { useSize } from "@/charts/shared/use-size"; import Flex from "@/components/flex"; -import { MapConfig } from "@/configurator"; +import { MapConfig, PaletteType } from "@/configurator"; import { ColorRamp } from "@/configurator/components/chart-controls/color-ramp"; import { Observation } from "@/domain/data"; import { truthy } from "@/domain/types"; @@ -115,7 +115,7 @@ export const MapLegend = ({ {areaLayer.colors.type === "continuous" ? ( areaLayer.colors.interpolationType === "linear" ? ( {symbolLayer.colors.interpolationType === "linear" ? ( { // @ts-ignore @@ -645,12 +645,12 @@ const QuantizeColorLegend = ({ }; const ContinuousColorLegend = ({ - palette, + paletteId, domain, getValue, valueFormatter, }: { - palette: string; + paletteId: PaletteType["paletteId"]; domain: [number, number]; getValue: (d: Observation) => number | null; valueFormatter: (v: Observation[string]) => string; @@ -677,7 +677,7 @@ const ContinuousColorLegend = ({ diff --git a/app/charts/map/map-state.tsx b/app/charts/map/map-state.tsx index ff969fc8e..2dccb4821 100644 --- a/app/charts/map/map-state.tsx +++ b/app/charts/map/map-state.tsx @@ -129,7 +129,7 @@ const useMapState = ( data: areaLayerState.data, dataDomain: areaLayerState.dataDomain, getLabel: areaLayerState.getLabel, - colors: areaColors as AreaLayerColors, + colors: areaColors as unknown as AreaLayerColors, }; }, [areaColors, areaLayer, areaLayerState]); @@ -245,7 +245,7 @@ const useMapState = ( type AreaLayerColors = | { type: "categorical"; - palette: string; + paletteId: string; component: Dimension; domain: string[]; getValue: (d: Observation) => string; @@ -254,7 +254,7 @@ type AreaLayerColors = } | { type: "continuous"; - palette: string; + paletteId: string; component: Measure; domain: [number, number]; // Needed for the legend. @@ -285,7 +285,7 @@ const getNumericalColorScale = ({ data: Observation[]; dataDomain: [number, number]; }) => { - const interpolator = getColorInterpolator(color.palette); + const interpolator = getColorInterpolator(color.paletteId); switch (color.scaleType) { case "continuous": @@ -376,7 +376,7 @@ const useCategoricalColors = ( return { type: "categorical" as const, - palette: colorSpec.palette, + paletteId: colorSpec.paletteId, component, domain, getValue: colorSpec.useAbbreviations @@ -445,7 +445,7 @@ const useNumericalColors = ( return { type: "continuous" as const, - palette: colorSpec.palette, + paletteId: colorSpec.paletteId, component, scale: colorScale, interpolationType: colorSpec.interpolationType, diff --git a/app/charts/pie/pie-state.tsx b/app/charts/pie/pie-state.tsx index 2e6b7b8de..bd44a30d5 100644 --- a/app/charts/pie/pie-state.tsx +++ b/app/charts/pie/pie-state.tsx @@ -101,7 +101,7 @@ const usePieState = ( ); const segments = allSegments.filter((d) => uniqueSegments.includes(d)); - if (fields.segment && segmentDimension && fields.segment.colorMapping) { + if (fields.segment && segmentDimension && fields.color) { const orderedSegmentLabelsAndColors = allSegments.map((segment) => { const dvIri = segmentsByAbbreviationOrLabel.get(segment)?.value || @@ -110,7 +110,10 @@ const usePieState = ( return { label: segment, - color: fields.segment?.colorMapping![dvIri] ?? schemeCategory10[0], + color: + fields.color.type === "segment" + ? fields.color.colorMapping![dvIri] ?? schemeCategory10[0] + : schemeCategory10[0], }; }); @@ -118,7 +121,12 @@ const usePieState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range( + getPalette({ + paletteId: fields.color.paletteId, + colorField: fields.color, + }) + ); } // Do not let the scale be implicitly extended by children calling it // on unknown values @@ -133,6 +141,7 @@ const usePieState = ( ySum, }; }, [ + fields.color, fields.segment, getSegment, getY, diff --git a/app/charts/scatterplot/scatterplot-state.tsx b/app/charts/scatterplot/scatterplot-state.tsx index 5eeebb8b2..2371a9997 100644 --- a/app/charts/scatterplot/scatterplot-state.tsx +++ b/app/charts/scatterplot/scatterplot-state.tsx @@ -127,7 +127,7 @@ const useScatterplotState = ( // Map ordered segments to colors const colors = scaleOrdinal(); - if (fields.segment && segmentDimension && fields.segment.colorMapping) { + if (fields.segment && segmentDimension && fields.color) { const orderedSegmentLabelsAndColors = allSegments.map((segment) => { const dvIri = segmentsByAbbreviationOrLabel.get(segment)?.value || @@ -136,7 +136,10 @@ const useScatterplotState = ( return { label: segment, - color: fields.segment!.colorMapping![dvIri] ?? schemeCategory10[0], + color: + fields.color.type === "segment" + ? fields.color.colorMapping![dvIri] ?? schemeCategory10[0] + : schemeCategory10[0], }; }); @@ -144,7 +147,12 @@ const useScatterplotState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range( + getPalette({ + paletteId: fields.color.paletteId, + colorField: fields.color, + }) + ); } // Dimensions const { left, bottom } = useChartPadding({ diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts index 69a8ccd91..07a3dca3e 100644 --- a/app/charts/shared/chart-state.ts +++ b/app/charts/shared/chart-state.ts @@ -92,7 +92,7 @@ export type CommonChartState = { interactiveFiltersConfig: InteractiveFiltersConfig; }; -export type ColorsChartState = Has; +export type ColorsChartState = Has; export const ChartContext = createContext(undefined); export const useChartState = () => { diff --git a/app/charts/shared/legend-color.tsx b/app/charts/shared/legend-color.tsx index 2c22ce86e..f28eac391 100644 --- a/app/charts/shared/legend-color.tsx +++ b/app/charts/shared/legend-color.tsx @@ -16,7 +16,7 @@ import { TooltipTitle } from "@/components/tooltip-utils"; import { useChartConfigFilters } from "@/config-utils"; import { ChartConfig, - GenericSegmentField, + GenericField, isSegmentInConfig, MapConfig, useReadOnlyConfiguratorState, @@ -147,7 +147,7 @@ const useLegendGroups = ({ const segmentField = ( isSegmentInConfig(chartConfig) ? chartConfig.fields.segment : null - ) as GenericSegmentField | null | undefined; + ) as GenericField | null | undefined; const segmentFilters = segmentField?.componentId ? filters[segmentField.componentId] : null; diff --git a/app/charts/table/table-state.tsx b/app/charts/table/table-state.tsx index d22b47983..7003f5f16 100644 --- a/app/charts/table/table-state.tsx +++ b/app/charts/table/table-state.tsx @@ -358,7 +358,7 @@ const useTableState = ( ) || 1; const maxAbsoluteValue = Math.max(absMinValue, absMaxValue); const colorScale = scaleDiverging( - getColorInterpolator((columnStyle as ColumnStyleHeatmap).palette) + getColorInterpolator((columnStyle as ColumnStyleHeatmap).paletteId) ).domain([-maxAbsoluteValue, 0, maxAbsoluteValue]); return { ...common, diff --git a/app/components/chart-footnotes.tsx b/app/components/chart-footnotes.tsx index c82ab13d6..c6442877f 100644 --- a/app/components/chart-footnotes.tsx +++ b/app/components/chart-footnotes.tsx @@ -197,7 +197,7 @@ const ChartFootnotesComboLineColumn = ({ {firstComponent && ( ; const ColorMapping = t.record(t.string, t.string); export type ColorMapping = t.TypeOf; +const SingleColorField = t.type({ + type: t.literal("single"), + paletteId: t.string, + color: t.string, +}); +export type SingleColorField = t.TypeOf; + +const SegmentColorField = t.type({ + type: t.literal("segment"), + paletteId: t.string, + colorMapping: ColorMapping, +}); +export type SegmentColorField = t.TypeOf; + +const MeasuresColorField = t.type({ + type: t.literal("measures"), + paletteId: t.string, + colorMapping: ColorMapping, +}); +export type MeasuresColorField = t.TypeOf; + +const ColorField = t.union([ + SingleColorField, + SegmentColorField, + MeasuresColorField, +]); +//FIXME: Remove current type called ColorField and replace it with the new one +export type ColorField = t.TypeOf; + const GenericField = t.intersection([ t.type({ componentId: t.string }), t.partial({ useAbbreviations: t.boolean }), @@ -195,17 +224,6 @@ export type GenericField = t.TypeOf; const GenericFields = t.record(t.string, t.union([GenericField, t.undefined])); export type GenericFields = t.TypeOf; -const GenericSegmentField = t.intersection([ - GenericField, - t.type({ - palette: t.string, - }), - t.partial({ - colorMapping: ColorMapping, - }), -]); -export type GenericSegmentField = t.TypeOf; - const AnimationType = t.union([t.literal("continuous"), t.literal("stepped")]); export type AnimationType = t.TypeOf; @@ -254,7 +272,7 @@ const ChartSubType = t.union([t.literal("stacked"), t.literal("grouped")]); export type ChartSubType = t.TypeOf; const ColumnSegmentField = t.intersection([ - GenericSegmentField, + GenericField, SortingField, t.type({ type: ChartSubType }), ]); @@ -272,6 +290,7 @@ const ColumnFields = t.intersection([ t.type({ x: t.intersection([GenericField, SortingField]), y: t.intersection([GenericField, UncertaintyFieldExtension]), + color: t.union([SegmentColorField, SingleColorField]), }), t.partial({ segment: ColumnSegmentField, @@ -292,8 +311,10 @@ const ColumnConfig = t.intersection([ export type ColumnFields = t.TypeOf; export type ColumnConfig = t.TypeOf; +const LineSegmentField = t.intersection([GenericField, SortingField]); + const BarSegmentField = t.intersection([ - GenericSegmentField, + GenericField, SortingField, t.type({ type: ChartSubType }), ]); @@ -303,6 +324,7 @@ const BarFields = t.intersection([ t.type({ x: GenericField, y: t.intersection([GenericField, SortingField]), + color: t.union([SegmentColorField, SingleColorField]), }), t.partial({ segment: BarSegmentField, @@ -323,13 +345,13 @@ const BarConfig = t.intersection([ export type BarFields = t.TypeOf; export type BarConfig = t.TypeOf; -const LineSegmentField = t.intersection([GenericSegmentField, SortingField]); export type LineSegmentField = t.TypeOf; const LineFields = t.intersection([ t.type({ x: GenericField, y: t.intersection([GenericField, UncertaintyFieldExtension]), + color: t.union([SegmentColorField, SingleColorField]), }), t.partial({ segment: LineSegmentField, @@ -349,7 +371,7 @@ const LineConfig = t.intersection([ export type LineFields = t.TypeOf; export type LineConfig = t.TypeOf; -const AreaSegmentField = t.intersection([GenericSegmentField, SortingField]); +const AreaSegmentField = t.intersection([GenericField, SortingField]); export type AreaSegmentField = t.TypeOf; const ImputationType = t.union([ @@ -367,6 +389,7 @@ const AreaFields = t.intersection([ GenericField, t.partial({ imputationType: ImputationType }), ]), + color: t.union([SegmentColorField, SingleColorField]), }), t.partial({ segment: AreaSegmentField, @@ -386,13 +409,14 @@ const AreaConfig = t.intersection([ export type AreaFields = t.TypeOf; export type AreaConfig = t.TypeOf; -const ScatterPlotSegmentField = GenericSegmentField; +const ScatterPlotSegmentField = GenericField; export type ScatterPlotSegmentField = t.TypeOf; const ScatterPlotFields = t.intersection([ t.type({ x: GenericField, y: GenericField, + color: t.union([SegmentColorField, SingleColorField]), }), t.partial({ segment: ScatterPlotSegmentField, @@ -413,13 +437,14 @@ const ScatterPlotConfig = t.intersection([ export type ScatterPlotFields = t.TypeOf; export type ScatterPlotConfig = t.TypeOf; -const PieSegmentField = t.intersection([GenericSegmentField, SortingField]); +const PieSegmentField = t.intersection([GenericField, SortingField]); export type PieSegmentField = t.TypeOf; const PieFields = t.intersection([ t.type({ y: GenericField, segment: PieSegmentField, + color: SegmentColorField, }), t.partial({ animation: AnimationField }), ]); @@ -446,7 +471,6 @@ const DivergingPaletteType = t.union([ t.literal("RdYlBu"), t.literal("RdYlGn"), ]); - export type DivergingPaletteType = t.TypeOf; const SequentialPaletteType = t.union([ @@ -457,10 +481,62 @@ const SequentialPaletteType = t.union([ t.literal("purples"), t.literal("reds"), ]); - export type SequentialPaletteType = t.TypeOf; -export type PaletteType = DivergingPaletteType | SequentialPaletteType; +const DimensionPaletteType = t.literal("dimension"); +const CategoricalPaletteType = t.union([ + DimensionPaletteType, + t.literal("accent"), + t.literal("category10"), + t.literal("dark2"), + t.literal("paired"), + t.literal("pastel1"), + t.literal("pastel2"), + t.literal("set1"), + t.literal("set2"), + t.literal("set3"), + t.literal("tableau10"), +]); + +export type CategoricalPaletteType = t.TypeOf; + +const DivergingPalette = t.type({ + type: t.literal("diverging"), + paletteId: DivergingPaletteType, + name: DivergingPaletteType, +}); + +const SequentialPalette = t.type({ + type: t.literal("sequential"), + paletteId: SequentialPaletteType, + name: SequentialPaletteType, +}); + +const CategoricalPalette = t.type({ + type: t.literal("categorical"), + paletteId: CategoricalPaletteType, + name: CategoricalPaletteType, +}); + +const CustomPalette = t.type({ + type: t.union([ + t.literal("diverging"), + t.literal("sequential"), + t.literal("categorical"), + ]), + paletteId: t.string, + name: t.string, + colors: t.array(t.string), +}); + +export const PaletteType = t.union([ + DivergingPalette, + SequentialPalette, + CategoricalPalette, + CustomPalette, +]); + +export type PaletteType = t.TypeOf; const ColorScaleType = t.union([ t.literal("continuous"), @@ -489,13 +565,13 @@ const ColumnStyleText = t.type({ const ColumnStyleCategory = t.type({ type: t.literal("category"), textStyle: ColumnTextStyle, - palette: t.string, + paletteId: t.string, colorMapping: ColorMapping, }); const ColumnStyleHeatmap = t.type({ type: t.literal("heatmap"), textStyle: ColumnTextStyle, - palette: DivergingPaletteType, + paletteId: DivergingPaletteType, }); const ColumnStyleBar = t.type({ type: t.literal("bar"), @@ -588,7 +664,7 @@ const CategoricalColorField = t.intersection([ t.type({ type: t.literal("categorical"), componentId: t.string, - palette: t.string, + paletteId: t.string, colorMapping: ColorMapping, }), t.partial({ useAbbreviations: t.boolean }), @@ -601,7 +677,7 @@ const NumericalColorField = t.intersection([ t.type({ type: t.literal("numerical"), componentId: t.string, - palette: t.union([DivergingPaletteType, SequentialPaletteType]), + paletteId: t.string, }), t.union([ t.type({ @@ -620,15 +696,17 @@ const NumericalColorField = t.intersection([ ]), ColorFieldOpacity, ]); + export type NumericalColorField = t.TypeOf; -export type ColorField = +export type MapColorField = | FixedColorField | CategoricalColorField | NumericalColorField; const MapAreaLayer = t.type({ componentId: t.string, + //FIXME: convert to new color field type color: t.union([CategoricalColorField, NumericalColorField]), }); export type MapAreaLayer = t.TypeOf; @@ -637,6 +715,7 @@ const MapSymbolLayer = t.type({ componentId: t.string, /** symbol radius (size) */ measureId: t.string, + //FIXME: convert to new color field type color: t.union([FixedColorField, CategoricalColorField, NumericalColorField]), }); export type MapSymbolLayer = t.TypeOf; @@ -673,9 +752,8 @@ const ComboLineSingleFields = t.type({ x: GenericField, y: t.type({ componentIds: t.array(t.string), - palette: t.string, - colorMapping: ColorMapping, }), + color: MeasuresColorField, }); export type ComboLineSingleFields = t.TypeOf; @@ -697,9 +775,8 @@ const ComboLineDualFields = t.type({ y: t.type({ leftAxisComponentId: t.string, rightAxisComponentId: t.string, - palette: t.string, - colorMapping: ColorMapping, }), + color: MeasuresColorField, }); export type ComboLineDualFields = t.TypeOf; @@ -722,9 +799,8 @@ const ComboLineColumnFields = t.type({ lineComponentId: t.string, lineAxisOrientation: t.union([t.literal("left"), t.literal("right")]), columnComponentId: t.string, - palette: t.string, - colorMapping: ColorMapping, }), + color: MeasuresColorField, }); export type ComboLineColumnFields = t.TypeOf; @@ -801,6 +877,24 @@ export const isComboChartConfig = ( ); }; +export const fieldHasComponentId = (chartConfig: ChartConfig) => { + const validFields = Object.entries(chartConfig.fields).reduce( + (acc, [key, field]) => { + if (field && typeof field.componentId === "string") { + acc[key] = field; + } + return acc; + }, + {} as { [key: string]: GenericField } + ); + + return validFields as { + [s: string]: { componentId: string } & { + useAbbreviations?: boolean | undefined; + }; + }; +}; + export const isAreaConfig = ( chartConfig: ChartConfig ): chartConfig is AreaConfig => { @@ -895,6 +989,21 @@ export const isSegmentInConfig = ( ); }; +export const isColorInConfig = ( + chartConfig: ChartConfig +): chartConfig is + | AreaConfig + | ColumnConfig + | LineConfig + | PieConfig + | ScatterPlotConfig => { + return !isTableConfig(chartConfig) && !isMapConfig(chartConfig); +}; + +export const isNotTableOrMap = (chartConfig: ChartConfig) => { + return !isTableConfig(chartConfig) && !isMapConfig(chartConfig); +}; + export const isSortingInConfig = ( chartConfig: ChartConfig ): chartConfig is diff --git a/app/configurator/components/add-dataset-dialog.mock.ts b/app/configurator/components/add-dataset-dialog.mock.ts index bb7347187..e13d00b51 100644 --- a/app/configurator/components/add-dataset-dialog.mock.ts +++ b/app/configurator/components/add-dataset-dialog.mock.ts @@ -106,6 +106,11 @@ export const photovoltaikChartStateMock: ConfiguratorStateConfiguringChart = { componentId: "https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/AnzahlAnlagen", }, + color: { + type: "single", + paletteId: "category10", + color: "#1f77b4", + }, }, }, ], diff --git a/app/configurator/components/chart-controls/color-palette.tsx b/app/configurator/components/chart-controls/color-palette.tsx index e880d79ab..f051c52d1 100644 --- a/app/configurator/components/chart-controls/color-palette.tsx +++ b/app/configurator/components/chart-controls/color-palette.tsx @@ -18,6 +18,7 @@ import { Label } from "@/components/form"; import { getChartConfig } from "@/config-utils"; import { ConfiguratorStateConfiguringChart, + isColorInConfig, isConfiguring, useConfiguratorState, } from "@/configurator"; @@ -25,7 +26,7 @@ import { mapValueIrisToColor } from "@/configurator/components/ui-helpers"; import { Component, isNumericalMeasure } from "@/domain/data"; import { categoricalPalettes, - DEFAULT_CATEGORICAL_PALETTE_NAME, + DEFAULT_CATEGORICAL_PALETTE_ID, divergingSteppedPalettes, getDefaultCategoricalPalette, getPalette, @@ -76,12 +77,14 @@ export const ColorPalette = ({ ? [defaultPalette, ...categoricalPalettes] : categoricalPalettes; - const currentPaletteName = get( - chartConfig, - `fields["${chartConfig.activeField}"].${ - colorConfigPath ? `${colorConfigPath}.` : "" - }palette` - ); + const currentPaletteName = isColorInConfig(chartConfig) + ? get(chartConfig, `fields.color.paletteId`) + : get( + chartConfig, + `fields["${chartConfig.activeField}"].${ + colorConfigPath ? `${colorConfigPath}.` : "" + }palette` + ); const currentPalette = palettes.find((p) => p.value === currentPaletteName) ?? palettes[0]; @@ -91,19 +94,39 @@ export const ColorPalette = ({ if (!component || !palette) { return; } - - dispatch({ - type: "CHART_PALETTE_CHANGED", - value: { - field, - colorConfigPath, - palette: palette.value, - colorMapping: mapValueIrisToColor({ - palette: palette.value, - dimensionValues: component.values, - }), - }, - }); + if (isColorInConfig(chartConfig)) { + dispatch({ + type: "COLOR_FIELD_SET", + value: + chartConfig.fields.color.type === "single" + ? { + type: chartConfig.fields.color.type, + paletteId: palette.value, + color: palette.colors[0], + } + : { + type: chartConfig.fields.color.type, + paletteId: palette.value, + colorMapping: mapValueIrisToColor({ + paletteId: palette.value, + dimensionValues: component.values, + }), + }, + }); + } else { + dispatch({ + type: "CHART_PALETTE_CHANGED", + value: { + field, + colorConfigPath, + paletteId: palette.value, + colorMapping: mapValueIrisToColor({ + paletteId: palette.value, + dimensionValues: component.values, + }), + }, + }); + } }); return ( @@ -127,12 +150,16 @@ export const ColorPalette = ({ onChange={handleChangePalette} > {palettes.map((palette, index) => ( - -
+ + {palette.label} -
+ {palette.colors.map((color) => ( ))} -
-
+ +
))} @@ -210,18 +237,22 @@ const ColorPaletteControls = ({ const [, dispatch] = useConfiguratorState(); const chartConfig = getChartConfig(state); - const palette = get( - chartConfig, - `fields["${field}"].${colorConfigPath ? `${colorConfigPath}.` : ""}palette`, - DEFAULT_CATEGORICAL_PALETTE_NAME - ) as string; + const paletteId = isColorInConfig(chartConfig) + ? get(chartConfig, `fields.color.paletteId`) + : (get( + chartConfig, + `fields["${field}"].${colorConfigPath ? `${colorConfigPath}.` : ""}paletteId`, + DEFAULT_CATEGORICAL_PALETTE_ID + ) as string); - const colorMapping = get( - chartConfig, - `fields["${field}"].${ - colorConfigPath ? `${colorConfigPath}.` : "" - }colorMapping` - ) as Record | undefined; + const colorMapping = isColorInConfig(chartConfig) + ? get(chartConfig, `fields.color.colorMapping`) + : (get( + chartConfig, + `fields["${field}"].${ + colorConfigPath ? `${colorConfigPath}.` : "" + }colorMapping` + ) as Record | undefined); const resetColorPalette = useCallback( () => @@ -231,24 +262,23 @@ const ColorPaletteControls = ({ field, colorConfigPath, colorMapping: mapValueIrisToColor({ - palette, + paletteId, dimensionValues: component.values, }), }, }), - [colorConfigPath, component, dispatch, field, palette] + [colorConfigPath, component, dispatch, field, paletteId] ); if (colorMapping) { - // Compare palette colors & colorMapping colors - const currentPalette = getPalette(palette); + const currentPalette = getPalette({ paletteId }); const colorMappingColors = Object.values(colorMapping); const nbMatchedColors = colorMappingColors.length; const matchedColorsInPalette = currentPalette.slice(0, nbMatchedColors); const same = matchedColorsInPalette.every((d, i) => d === colorMappingColors[i]) || - palette === "dimension"; + paletteId === "dimension"; return ( diff --git a/app/configurator/components/chart-controls/color-picker.tsx b/app/configurator/components/chart-controls/color-picker.tsx index cbd9cc692..8d392c0c4 100644 --- a/app/configurator/components/chart-controls/color-picker.tsx +++ b/app/configurator/components/chart-controls/color-picker.tsx @@ -1,17 +1,25 @@ import { Trans } from "@lingui/macro"; import { Box, Button, Popover, styled, Theme, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; +import { hexToHsva, hsvaToHex } from "@uiw/react-color"; import { color as d3Color } from "d3-color"; -import { MouseEventHandler, useRef } from "react"; +import dynamic from "next/dynamic"; +import { MouseEventHandler, useCallback, useMemo, useRef } from "react"; import useDisclosure from "@/components/use-disclosure"; import VisuallyHidden from "@/components/visually-hidden"; import { Icon } from "@/icons"; +//have to import dynamically to avoid @uiw/react-color dependency issues with the server +const CustomColorPicker = dynamic( + () => import("../../components/color-picker"), + { ssr: false } +); + const useStyles = makeStyles(() => ({ swatch: { - width: "1.5rem", - height: "1.5rem", + width: "1rem", + height: "1rem", borderWidth: 1, borderStyle: "solid", borderColor: "transparent", @@ -21,7 +29,7 @@ const useStyles = makeStyles(() => ({ }, })); -const Swatch = ({ +export const Swatch = ({ color, selected, onClick, @@ -116,9 +124,25 @@ const ColorPickerBox = styled(Box)({ }); export const ColorPickerMenu = (props: Props) => { - const { disabled } = props; + const { disabled, onChange, selectedColor } = props; const { isOpen, open, close } = useDisclosure(); const buttonRef = useRef(null); + const popoverRef = useRef(null); + + const initialSelected = useMemo( + () => hexToHsva(selectedColor), + [selectedColor] + ); + + const handleColorChange = useCallback( + (color) => { + const newHex = hsvaToHex(color); + if (newHex !== selectedColor) { + onChange?.(newHex); + } + }, + [onChange, selectedColor] + ); return ( { - - + + + + ); diff --git a/app/configurator/components/chart-controls/color-ramp.tsx b/app/configurator/components/chart-controls/color-ramp.tsx index 1a84194c1..523bd9c75 100644 --- a/app/configurator/components/chart-controls/color-ramp.tsx +++ b/app/configurator/components/chart-controls/color-ramp.tsx @@ -106,7 +106,7 @@ export const ColorRampField = (props: ColorRampFieldProps) => { const value = ev.target.value as (typeof currentPalette)["value"]; if (value) { dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field, diff --git a/app/configurator/components/chart-options-selector.tsx b/app/configurator/components/chart-options-selector.tsx index 9d48ac7d9..87eb1d476 100644 --- a/app/configurator/components/chart-options-selector.tsx +++ b/app/configurator/components/chart-options-selector.tsx @@ -47,6 +47,7 @@ import { ImputationType, imputationTypes, isAnimationInConfig, + isColorInConfig, isComboChartConfig, isTableConfig, MapConfig, @@ -319,6 +320,10 @@ const EncodingOptionsPanel = (props: EncodingOptionsPanelProps) => { id: "controls.select.measure", message: "Select a measure", }), + color: t({ + id: "controls.select.color", + message: "Select a color", + }), segment: t({ id: "controls.select.dimension", message: "Select a dimension", @@ -529,18 +534,17 @@ const EncodingOptionsPanel = (props: EncodingOptionsPanelProps) => { {isComboChartConfig(chartConfig) && encoding.field === "y" && ( )} - {fieldComponent && - encoding.field === "segment" && - (hasSubOptions || hasColorPalette) && ( - - )} + {fieldComponent && (hasSubOptions || hasColorPalette) && ( + + )} {encoding.options?.imputation?.shouldShow(chartConfig, observations) && ( )} @@ -613,6 +617,7 @@ const ChartLayoutOptions = ({ components, hasColorPalette, hasSubOptions, + measures, }: { encoding: EncodingSpec; component: Component | undefined; @@ -620,7 +625,18 @@ const ChartLayoutOptions = ({ components: Component[]; hasColorPalette: boolean; hasSubOptions: boolean; + measures: Measure[]; }) => { + const hasColorField = isColorInConfig(chartConfig); + const values: { id: string; symbol: LegendSymbol }[] = hasColorField + ? chartConfig.fields.color.type === "single" + ? [{ id: chartConfig.fields.y.componentId, symbol: "line" }] + : Object.keys(chartConfig.fields.color.colorMapping).map((key) => ({ + id: key, + symbol: "line", + })) + : []; + return encoding.options || hasColorPalette ? ( @@ -635,13 +651,30 @@ const ChartLayoutOptions = ({ disabled={!component} /> )} - {hasColorPalette && ( + <> ({ + value: id, + label: id, + })), + } as any as Component + } /> - )} + {hasColorField && chartConfig.fields.color.type === "single" && ( + d.id === values[0].id)!.label} + /> + )} + ) : null; @@ -864,7 +897,7 @@ const ChartComboLineSingleYField = ({ } dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field: "y", @@ -892,7 +925,7 @@ const ChartComboLineSingleYField = ({ if (id !== FIELD_VALUE_NONE) { dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field: "y", @@ -907,7 +940,7 @@ const ChartComboLineSingleYField = ({ )} - ({ id, symbol: "line" }))} measures={measures} /> @@ -981,7 +1014,7 @@ const ChartComboLineDualYField = ({ onChange={(e) => { const newId = e.target.value as string; dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field: "y", @@ -1006,7 +1039,7 @@ const ChartComboLineDualYField = ({ onChange={(e) => { const newId = e.target.value as string; dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field: "y", @@ -1019,7 +1052,7 @@ const ChartComboLineDualYField = ({ /> - { const newId = e.target.value as string; dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field: "y", @@ -1121,7 +1154,7 @@ const ChartComboLineColumnYField = ({ onChange={(e) => { const newId = e.target.value as string; dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field: "y", @@ -1134,7 +1167,7 @@ const ChartComboLineColumnYField = ({ /> - d.id === id)!.label} symbol={symbol} @@ -1367,7 +1400,7 @@ const ChartFieldMultiFilter = ({ colorComponent={colorComponent ?? component} // If colorType is defined, we are dealing with color field and // not segment. - colorConfigPath={colorType ? "color" : undefined} + colorConfigPath={colorType ? "color" : "colorMapping"} /> ) )} @@ -1519,7 +1552,7 @@ const ChartFieldSorting = ({ >( ({ sortingType, sortingOrder }) => { dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field, @@ -1837,7 +1870,7 @@ const ChartFieldColorComponent = ({
import("@uiw/react-color").then((mod) => ({ default: mod.Chrome })), + { ssr: false } +); + +const useColorPickerStyles = makeStyles(() => ({ + swatches: { + gridTemplateColumns: "repeat(auto-fill, minmax(1rem, 1fr))", + gap: 2, + marginBottom: 2, + marginTop: 8, + }, +})); + +type CustomColorPickerProps = { + onChange: (color: HsvaColor) => void; + colorSwatches?: readonly string[]; + defaultSelection?: HsvaColor; +}; + +const CustomColorPicker = ({ + onChange, + colorSwatches, + defaultSelection = { h: 0, s: 0, v: 68, a: 1 }, +}: CustomColorPickerProps) => { + const [hsva, setHsva] = useState(defaultSelection); + const [hexInput, setHexInput] = useState(hsvaToHex(defaultSelection)); + const classes = useColorPickerStyles(); + + useEffect(() => { + onChange(hsva); + }, [hsva, onChange]); + + const updateColorInput = useCallback< + (e: ChangeEvent) => void + >((e) => { + const value = e.target.value; + if (String(value).length <= 7) { + setHexInput(value); + if (String(value).length >= 3) { + setHsva(hexToHsva(`${value}`)); + } + } + }, []); + + return ( + + + + setHsva((ps) => ({ ...ps, ...newColor, a: ps.a })) + } + style={{ width: "100%", height: "90px" }} + /> + + + setHsva((ps) => ({ ...ps, ...newHue }))} + style={{ width: "100%", height: "8px" }} + /> + + + + {colorSwatches?.map((color) => ( + setHsva(hexToHsva(color))} + /> + ))} + + + + setHsva(color.hsva)} + /> + + + + ); +}; + +export default CustomColorPicker; diff --git a/app/configurator/components/field.tsx b/app/configurator/components/field.tsx index 4b4fb12ac..6d7150be3 100644 --- a/app/configurator/components/field.tsx +++ b/app/configurator/components/field.tsx @@ -13,6 +13,7 @@ import { TimeLocaleObject } from "d3-time-format"; import get from "lodash/get"; import orderBy from "lodash/orderBy"; import pick from "lodash/pick"; +import dynamic from "next/dynamic"; import React, { ChangeEvent, ComponentProps, @@ -38,9 +39,8 @@ import { import { OpenMetadataPanelWrapper } from "@/components/metadata-panel"; import SelectTree from "@/components/select-tree"; import useDisclosure from "@/components/use-disclosure"; -import { ChartConfig } from "@/config-types"; +import { ChartConfig, isColorInConfig } from "@/config-types"; import { getChartConfig, useChartConfigFilters } from "@/config-utils"; -import { ColorPickerMenu } from "@/configurator/components/chart-controls/color-picker"; import { AnnotatorTab, AnnotatorTabProps, @@ -96,6 +96,12 @@ import { hierarchyToOptions } from "@/utils/hierarchy"; import { makeDimensionValueSorters } from "@/utils/sorting-values"; import useEvent from "@/utils/use-event"; +const ColorPickerMenu = dynamic( + () => + import("./chart-controls/color-picker").then((mod) => mod.ColorPickerMenu), + { ssr: false } +); + const useStyles = makeStyles((theme) => ({ root: { display: "flex", @@ -682,14 +688,18 @@ const useMultiFilterColorPicker = (value: string) => { const filters = useChartConfigFilters(chartConfig); const { dimensionId, colorConfigPath } = useMultiFilterContext(); const { activeField } = chartConfig; + + const hasColorField = isColorInConfig(chartConfig); + const colorField = hasColorField ? "color" : activeField; + const onChange = useCallback( (color: string) => { - if (activeField) { + if (colorField) { dispatch({ type: "CHART_COLOR_CHANGED", value: { - field: activeField, - colorConfigPath, + field: colorField, + colorConfigPath: hasColorField ? "" : colorField, color, value, }, @@ -697,25 +707,28 @@ const useMultiFilterColorPicker = (value: string) => { } }, - [colorConfigPath, dispatch, activeField, value] + [dispatch, colorField, value, hasColorField] ); - const path = colorConfigPath ? `${colorConfigPath}.` : ""; + const path = colorConfigPath ? colorConfigPath : ""; + const color = get( chartConfig, - `fields["${activeField}"].${path}colorMapping["${value}"]` + `fields["${colorField}"].${path}${!hasColorField ? ".colorMapping" : ""}["${value}"]` ); const palette = useMemo(() => { - return getPalette( - get( + return getPalette({ + paletteId: get( chartConfig, - `fields["${activeField}"].${ - colorConfigPath ? `${colorConfigPath}.` : "" - }palette` - ) - ); - }, [chartConfig, colorConfigPath, activeField]); + `fields["${colorField}"].${ + colorConfigPath + ? `${colorConfigPath}${!hasColorField ? ".colorMapping" : ""}` + : "" + }paletteId` + ), + }); + }, [chartConfig, colorConfigPath, colorField]); const checkedState = dimensionId ? isMultiFilterFieldChecked(filters, dimensionId, value) @@ -802,7 +815,7 @@ export const ColorPickerField = ({ const updateColor = useCallback( (value: string) => dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field, @@ -814,7 +827,9 @@ export const ColorPickerField = ({ ); const color = get(chartConfig, `fields["${field}"].${path}`); - const palette = getPalette(get(chartConfig, `fields["${field}"].palette`)); + const palette = getPalette({ + paletteId: get(chartConfig, `fields["${field}"].paletteId`), + }); return ( { return joinParents(node?.parents); }; -const getColorConfig = ( - chartConfig: ChartConfig, - colorConfigPath: string | undefined -) => { - if (!chartConfig.activeField) { - return; +const getColorConfig = (chartConfig: ChartConfig) => { + if (isColorInConfig(chartConfig)) { + return get(chartConfig.fields, ["color"]) as ColorField | undefined; + } else { + return get(chartConfig.fields, [ + chartConfig.activeField ?? "symbolLayer", + "color", + ]) as ColorField | undefined; } - - const path = colorConfigPath - ? [chartConfig.activeField, colorConfigPath] - : [chartConfig.activeField]; - - return get(chartConfig.fields, path) as GenericSegmentField | undefined; }; const FilterControls = ({ @@ -322,19 +319,22 @@ const MultiFilterContent = ({ }); const colorConfig = useMemo(() => { - return getColorConfig(chartConfig, colorConfigPath); - }, [chartConfig, colorConfigPath]); + return getColorConfig(chartConfig); + }, [chartConfig]); const hasColorMapping = useMemo(() => { - return ( - !!colorConfig?.colorMapping && + return !!( + (colorConfig?.type === "single" + ? colorConfig.color + : colorConfig?.colorMapping) && (colorComponent !== undefined ? dimensionId === colorComponent.id : true) ); - }, [colorConfig?.colorMapping, dimensionId, colorComponent]); + }, [colorConfig, dimensionId, colorComponent]); useEnsureUpToDateColorMapping({ colorComponentValues: colorComponent?.values, - colorMapping: colorConfig?.colorMapping, + colorMapping: + colorConfig?.type !== "single" ? colorConfig?.colorMapping : undefined, }); const interactiveFilterProps = useInteractiveFiltersToggle("legend"); @@ -386,7 +386,7 @@ const MultiFilterContent = ({ Selected filters - {hasColorMapping && colorConfig?.palette === "dimension" && ( + {hasColorMapping && colorConfig?.paletteId === "dimension" && ( @@ -470,8 +470,8 @@ const MultiFilterContent = ({ * and contains new values in the color dimension. * */ const useEnsureUpToDateColorMapping = ({ - colorComponentValues, - colorMapping, + colorComponentValues = [], + colorMapping = {}, }: { colorComponentValues?: DimensionValue[]; colorMapping?: ColorMapping; @@ -481,22 +481,33 @@ const useEnsureUpToDateColorMapping = ({ const { dimensionId, colorConfigPath } = useMultiFilterContext(); const { activeField } = chartConfig; - if ( - activeField && - colorComponentValues?.some((v) => !colorMapping?.[v.value]) - ) { - dispatch({ - type: "CHART_CONFIG_UPDATE_COLOR_MAPPING", - value: { - field: activeField, - dimensionId, - colorConfigPath, - colorMapping, - values: colorComponentValues, - random: false, - }, - }); - } + const hasOutdatedMapping = useMemo(() => { + return colorComponentValues.some((value) => !colorMapping[value.value]); + }, [colorComponentValues, colorMapping]); + + useEffect(() => { + if (activeField && hasOutdatedMapping) { + dispatch({ + type: "CHART_CONFIG_UPDATE_COLOR_MAPPING", + value: { + dimensionId, + colorConfigPath, + colorMapping, + field: activeField, + values: colorComponentValues, + random: false, + }, + }); + } + }, [ + hasOutdatedMapping, + dispatch, + dimensionId, + colorConfigPath, + colorMapping, + colorComponentValues, + activeField, + ]); }; const useBreadcrumbStyles = makeStyles({ diff --git a/app/configurator/components/ui-helpers.ts b/app/configurator/components/ui-helpers.ts index 85f66c471..e9cc50838 100644 --- a/app/configurator/components/ui-helpers.ts +++ b/app/configurator/components/ui-helpers.ts @@ -268,12 +268,12 @@ export const getIconName = (name: string): IconName => { const randomComparator = () => (Math.random() > 0.5 ? 1 : -1); export const mapValueIrisToColor = ({ - palette, + paletteId, dimensionValues, colorMapping: oldColorMapping, random, }: { - palette: string; + paletteId: string; dimensionValues: DimensionValue[]; colorMapping?: ColorMapping; random?: boolean; @@ -282,10 +282,10 @@ export const mapValueIrisToColor = ({ return {}; } - const paletteValues = getPalette(palette); + const paletteValues = getPalette({ paletteId }); const colors = dimensionValues.map( (d, i) => - (palette === "dimension" && d.color) || + (paletteId === "dimension" && d.color) || oldColorMapping?.[`${d.value}`] || paletteValues[i % paletteValues.length] ); diff --git a/app/configurator/config-form.tsx b/app/configurator/config-form.tsx index 24418fb5e..31795ad34 100644 --- a/app/configurator/config-form.tsx +++ b/app/configurator/config-form.tsx @@ -177,7 +177,7 @@ export const useChartOptionSelectField = ( (e) => { const value = e.target.value as string; dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field, @@ -228,7 +228,7 @@ export const useChartOptionSliderField = ({ if (isValidNumber) { dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field, @@ -264,7 +264,7 @@ export const useChartOptionRadioField = ( const [state, dispatch] = useConfiguratorState(isConfiguring); const handleChange = useCallback(() => { dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field, @@ -299,7 +299,7 @@ export const useChartOptionBooleanField = ({ const onChange = useCallback<(e: ChangeEvent) => void>( (e) => { dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, path, diff --git a/app/configurator/configurator-state/actions.tsx b/app/configurator/configurator-state/actions.tsx index 7ec7b29a0..a83520ef4 100644 --- a/app/configurator/configurator-state/actions.tsx +++ b/app/configurator/configurator-state/actions.tsx @@ -2,6 +2,7 @@ import { EncodingFieldType } from "@/charts/chart-config-ui-options"; import { ChartConfig, ChartType, + ColorField, ColorMapping, ConfiguratorState, DashboardFiltersConfig, @@ -74,7 +75,7 @@ export type ConfiguratorStateAction = }; } | { - type: "CHART_OPTION_CHANGED"; + type: "COLOR_MAPPING_UPDATED"; value: { locale: Locale; path: string; @@ -94,10 +95,14 @@ export type ConfiguratorStateAction = value: { field: string; colorConfigPath?: string; - palette: string; + paletteId: string; colorMapping: Record; }; } + | { + type: "COLOR_FIELD_SET"; + value: ColorField; + } | { type: "CHART_PALETTE_RESET"; value: { diff --git a/app/configurator/configurator-state/index.tsx b/app/configurator/configurator-state/index.tsx index a7bf15cb1..6551d6453 100644 --- a/app/configurator/configurator-state/index.tsx +++ b/app/configurator/configurator-state/index.tsx @@ -91,7 +91,7 @@ const getNonGenericFieldValues = (chartConfig: ChartConfig): string[] => { /** Get all filters by mapping status. * * We need to handle some fields differently due to the way the chart config - * is structured at the moment (colorField) is a subfield of areaLayer and + * is structured at the moment (MapColorField) is a subfield of areaLayer and * symbolLayer fields. */ export const getFiltersByMappingStatus = ( diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index 589c9f9b4..ceb3bb339 100644 --- a/app/configurator/configurator-state/mocks.ts +++ b/app/configurator/configurator-state/mocks.ts @@ -43,7 +43,7 @@ export const configStateMock = { color: { type: "categorical", componentId: "year-period-1", - palette: "dimension", + paletteId: "dimension", colorMapping: { red: "green", green: "blue", @@ -170,14 +170,9 @@ export const configStateMock = { componentId: "https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/anzahlanlagen", }, - segment: { - componentId: - "https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/Kanton", - palette: "category10", - sorting: { - sortingType: "byAuto", - sortingOrder: "asc", - }, + color: { + type: "segment", + paletteId: "category10", colorMapping: { "https://ld.admin.ch/canton/1": "#1f77b4", "https://ld.admin.ch/canton/10": "#ff7f0e", @@ -206,6 +201,16 @@ export const configStateMock = { "https://ld.admin.ch/canton/8": "#9467bd", "https://ld.admin.ch/canton/9": "#8c564b", }, + }, + segment: { + componentId: + "https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/Kanton", + + sorting: { + sortingType: "byAuto", + sortingOrder: "asc", + }, + type: "stacked", }, }, @@ -1619,15 +1624,9 @@ export const configJoinedCubes: Partial< "https://energy.ld.admin.ch/sfoe/bfe_ogd84_einmalverguetung_fuer_photovoltaikanlagen/AnzahlAnlagen", }), }, - segment: { - componentId: stringifyComponentId({ - unversionedCubeIri: - "https://energy.ld.admin.ch/elcom/electricityprice-canton", - unversionedComponentIri: - "https://energy.ld.admin.ch/elcom/electricityprice/dimension/category", - }), - palette: "category10", - sorting: { sortingType: "byMeasure", sortingOrder: "asc" }, + color: { + type: "segment", + paletteId: "category10", colorMapping: { "https://energy.ld.admin.ch/elcom/electricityprice/category/C1": "#1f77b4", @@ -1661,6 +1660,16 @@ export const configJoinedCubes: Partial< "#9467bd", }, }, + segment: { + componentId: stringifyComponentId({ + unversionedCubeIri: + "https://energy.ld.admin.ch/elcom/electricityprice-canton", + unversionedComponentIri: + "https://energy.ld.admin.ch/elcom/electricityprice/dimension/category", + }), + + sorting: { sortingType: "byMeasure", sortingOrder: "asc" }, + }, }, }, }; diff --git a/app/configurator/configurator-state/reducer.spec.tsx b/app/configurator/configurator-state/reducer.spec.tsx index 2c713fd02..d6db37c6b 100644 --- a/app/configurator/configurator-state/reducer.spec.tsx +++ b/app/configurator/configurator-state/reducer.spec.tsx @@ -509,7 +509,7 @@ describe("deriveFiltersFromFields", () => { }, ], "fields": Object { - "segment": Object { + "color": Object { "colorMapping": Object { "https://energy.ld.admin.ch/elcom/electricityprice/category/C1": "#1f77b4", "https://energy.ld.admin.ch/elcom/electricityprice/category/C2": "#ff7f0e", @@ -527,8 +527,11 @@ describe("deriveFiltersFromFields", () => { "https://energy.ld.admin.ch/elcom/electricityprice/category/H7": "#d62728", "https://energy.ld.admin.ch/elcom/electricityprice/category/H8": "#9467bd", }, + "paletteId": "category10", + "type": "segment", + }, + "segment": Object { "componentId": "https://energy.ld.admin.ch/elcom/electricityprice-canton(VISUALIZE.ADMIN_COMPONENT_ID_SEPARATOR)https://energy.ld.admin.ch/elcom/electricityprice/dimension/category", - "palette": "category10", "sorting": Object { "sortingOrder": "asc", "sortingType": "byMeasure", @@ -582,7 +585,7 @@ describe("deriveFiltersFromFields", () => { "it": "", }, }, - "version": "4.0.0", + "version": "4.1.0", } `); }); @@ -702,10 +705,16 @@ describe("colorMapping", () => { chartConfig.fields.segment?.componentId === "mapDataset(VISUALIZE.ADMIN_COMPONENT_ID_SEPARATOR)newAreaLayerColorIri" ); - expect(chartConfig.fields.segment?.palette === "dimension"); - expect(chartConfig.fields.segment?.colorMapping).toEqual({ - orange: "rgb(255, 153, 0)", - }); + expect(chartConfig.fields.color.paletteId === "dimension"); + expect( + chartConfig.fields.color.type === "single" + ? chartConfig.fields.color.color + : chartConfig.fields.color.colorMapping + ).toEqual( + chartConfig.fields.color.type === "single" + ? "rgb(255, 153, 0)" + : { orange: "rgb(255, 153, 0)" } + ); }); }); @@ -1098,7 +1107,7 @@ describe("handleChartOptionChanged", () => { } as unknown as ConfiguratorStateConfiguringChart; handleChartOptionChanged(state, { - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale: "en", field: "areaLayer", @@ -1158,7 +1167,7 @@ describe("handleChartOptionChanged", () => { } as unknown as ConfiguratorStateConfiguringChart; handleChartOptionChanged(state, { - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale: "en", field: "areaLayer", diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index 341cfe03d..36b62d845 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -37,6 +37,7 @@ import { GenericField, GenericFields, isAreaConfig, + isColorInConfig, isTableConfig, } from "@/config-types"; import { getChartConfig, makeMultiFilter } from "@/config-utils"; @@ -47,6 +48,7 @@ import { Dimension, isGeoDimension, isJoinByComponent } from "@/domain/data"; import { getOriginalDimension, isJoinByCube } from "@/graphql/join"; import { PossibleFilterValue } from "@/graphql/query-hooks"; import { findInHierarchy } from "@/rdf/tree-utils"; +import { theme } from "@/themes/federal"; import { getCachedComponents } from "@/urql-cache"; import { assert } from "@/utils/assert"; import { unreachableError } from "@/utils/unreachable"; @@ -309,7 +311,6 @@ const transitionStepPrevious = ( if (draft.state === "INITIAL" || draft.state === "SELECTING_DATASET") { return draft; } - switch (stepTo) { case "SELECTING_DATASET": return { @@ -412,7 +413,7 @@ export const handleChartFieldChanged = ( export const handleChartOptionChanged = ( draft: ConfiguratorState, - action: Extract + action: Extract ) => { if (isConfiguring(draft)) { const { locale, path, field, value } = action.value; @@ -479,21 +480,19 @@ export const updateColorMapping = ( if (fieldValue) { colorMapping = mapValueIrisToColor({ - palette: fieldValue.palette, + paletteId: fieldValue.paletteId, dimensionValues: values, colorMapping: oldColorMapping, random, }); } } else { - const fieldValue: (GenericField & { palette: string }) | undefined = get( - chartConfig, - path - ); + const fieldValue: (GenericField & { paletteId: string }) | undefined = + get(chartConfig, path); if (fieldValue?.componentId === dimensionId) { colorMapping = mapValueIrisToColor({ - palette: fieldValue.palette, + paletteId: fieldValue.paletteId, dimensionValues: values, colorMapping: oldColorMapping, random, @@ -651,34 +650,56 @@ const reducer_: Reducer = ( ) { chartConfig.interactiveFiltersConfig.calculation.active = false; chartConfig.interactiveFiltersConfig.calculation.type = "identity"; + if (isColorInConfig(chartConfig)) { + chartConfig.fields.color.type = "single"; + if (chartConfig.fields.color.type === "single") { + chartConfig.fields.color.color = theme.palette.primary.main; + } + } } } return draft; - case "CHART_OPTION_CHANGED": + case "COLOR_MAPPING_UPDATED": return handleChartOptionChanged(draft, action); + case "COLOR_FIELD_SET": + if (isConfiguring(draft)) { + const chartConfig = getChartConfig(draft); + setWith( + chartConfig, + `fields.color.paletteId`, + action.value.paletteId, + Object + ); + setWith( + chartConfig, + `fields.color.${action.value.type === "single" ? "color" : "colorMapping"}`, + action.value.type === "single" + ? action.value.color + : action.value.colorMapping, + Object + ); + } + + return draft; case "CHART_PALETTE_CHANGED": if (isConfiguring(draft)) { const chartConfig = getChartConfig(draft); + const commonPath = `fields["${action.value.field}"].${ + action.value.colorConfigPath ? `${action.value.colorConfigPath}.` : "" + }`; + setWith( chartConfig, - `fields["${action.value.field}"].${ - action.value.colorConfigPath - ? `${action.value.colorConfigPath}.` - : "" - }palette`, - action.value.palette, + `${commonPath}palette`, + action.value.paletteId, Object ); setWith( chartConfig, - `fields["${action.value.field}"].${ - action.value.colorConfigPath - ? `${action.value.colorConfigPath}.` - : "" - }colorMapping`, + `${commonPath}colorMapping`, action.value.colorMapping, Object ); diff --git a/app/configurator/table/table-chart-options.tsx b/app/configurator/table/table-chart-options.tsx index faa3c5287..31881ba6f 100644 --- a/app/configurator/table/table-chart-options.tsx +++ b/app/configurator/table/table-chart-options.tsx @@ -51,7 +51,7 @@ import { } from "@/domain/data"; import { getDefaultCategoricalPalette, - getDefaultCategoricalPaletteName, + getDefaultCategoricalPaletteId, getDefaultDivergingSteppedPalette, } from "@/palettes"; @@ -284,14 +284,14 @@ export const TableColumnOptions = ({ columnColor: "#fff", }; case "category": - const palette = getDefaultCategoricalPaletteName(component); + const paletteId = getDefaultCategoricalPaletteId(component); return { type: "category", textStyle: "regular", - palette, + paletteId, colorMapping: mapValueIrisToColor({ - palette, + paletteId, dimensionValues: component.values, }), }; @@ -299,7 +299,7 @@ export const TableColumnOptions = ({ return { type: "heatmap", textStyle: "regular", - palette: getDefaultDivergingSteppedPalette().value, + paletteId: getDefaultDivergingSteppedPalette().value, }; case "bar": return { diff --git a/app/docs/columns.mock.ts b/app/docs/columns.mock.ts index df031b4b8..cfb14136c 100644 --- a/app/docs/columns.mock.ts +++ b/app/docs/columns.mock.ts @@ -11,6 +11,11 @@ export const columnFields = { componentId: "http://environment.ld.admin.ch/foen/px/0703010000_103/measure/1", }, + color: { + type: "segment" as const, + paletteId: "dimension", + colorMapping: {}, + } }; export const chartConfig: ColumnConfig = { diff --git a/app/docs/controls.stories.tsx b/app/docs/controls.stories.tsx index 5ff039edb..4b7a15acb 100644 --- a/app/docs/controls.stories.tsx +++ b/app/docs/controls.stories.tsx @@ -127,7 +127,7 @@ const ColorPickerStory = { Current (valid) color: {currentColor} setCurrentColor(color)} /> diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index e151d4323..8079c3d5c 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -64,6 +64,11 @@ export const states: ConfiguratorState[] = [ y: { componentId: "foo", }, + color: { + type: "single", + paletteId: "oranges", + color: "#ff7f0e", + } }, interactiveFiltersConfig: { legend: { @@ -170,6 +175,11 @@ export const fields: ColumnFields = { "http://environment.ld.admin.ch/foen/px/0703030000_124/measure/0", }), }, + color: { + type: "single", + paletteId: "oranges", + color: "#ff7f0e", + } }; export const dimensions: Dimension[] = [ @@ -1091,7 +1101,7 @@ export const tableConfig: TableConfig = { componentType: "NominalDimension", columnStyle: { type: "category", - palette: "set3", + paletteId: "set3", textStyle: "bold", colorMapping: { "http://environment.ld.admin.ch/foen/px/0703010000_105/dimension/1/0": @@ -1174,7 +1184,7 @@ export const tableConfig: TableConfig = { componentType: "NumericalMeasure", columnStyle: { type: "heatmap", - palette: "BrBG", + paletteId: "BrBG", textStyle: "regular", }, }, @@ -1229,7 +1239,7 @@ export const tableConfig: TableConfig = { isGroup: false, isHidden: false, componentType: "NumericalMeasure", - columnStyle: { type: "heatmap", palette: "PRGn", textStyle: "regular" }, + columnStyle: { type: "heatmap", paletteId: "PRGn", textStyle: "regular" }, }, "http://environment.ld.admin.ch/foen/px/0703010000_105/measure/6": { componentId: @@ -1241,7 +1251,7 @@ export const tableConfig: TableConfig = { columnStyle: { type: "heatmap", textStyle: "regular", - palette: "PiYG", + paletteId: "PiYG", }, }, "http://environment.ld.admin.ch/foen/px/0703010000_105/measure/7": { diff --git a/app/docs/lines.mock.tsx b/app/docs/lines.mock.tsx index c2e59a327..5328e4f90 100644 --- a/app/docs/lines.mock.tsx +++ b/app/docs/lines.mock.tsx @@ -34,15 +34,9 @@ export const fields = { componentId: "http://environment.ld.admin.ch/foen/px/0703010000_103/measure/0", }, - segment: { - componentId: - "http://environment.ld.admin.ch/foen/px/0703010000_103/dimension/3", - palette: "category10", - type: "stacked", - sorting: { - sortingType: "byDimensionLabel", - sortingOrder: "asc", - } as SortingField["sorting"], + color: { + type: "segment" as const, + paletteId: "dimension", colorMapping: { "http://environment.ld.admin.ch/foen/px/0703010000_103/dimension/3/0": "#1f77b4", @@ -62,6 +56,15 @@ export const fields = { "#7f7f7f", }, }, + segment: { + componentId: + "http://environment.ld.admin.ch/foen/px/0703010000_103/dimension/3", + type: "stacked", + sorting: { + sortingType: "byDimensionLabel", + sortingOrder: "asc", + } as SortingField["sorting"], + }, }; export const chartConfig: LineConfig = { diff --git a/app/docs/scatterplot.mock.ts b/app/docs/scatterplot.mock.ts index e01c1603f..7f10b0b96 100644 --- a/app/docs/scatterplot.mock.ts +++ b/app/docs/scatterplot.mock.ts @@ -35,10 +35,9 @@ export const scatterplotFields: ScatterPlotFields = { componentId: "http://environment.ld.admin.ch/foen/px/0703010000_103/measure/1", }, - segment: { - componentId: - "http://environment.ld.admin.ch/foen/px/0703010000_103/dimension/1", - palette: "category10", + color: { + type: "segment", + paletteId: "category10", colorMapping: { "http://environment.ld.admin.ch/foen/px/0703010000_103/dimension/1/0": "#1f77b4", @@ -54,6 +53,11 @@ export const scatterplotFields: ScatterPlotFields = { "#8c564b", }, }, + segment: { + componentId: + "http://environment.ld.admin.ch/foen/px/0703010000_103/dimension/1", + + }, }; export const chartConfig: ScatterPlotConfig = { diff --git a/app/locales/de/messages.po b/app/locales/de/messages.po index a8f92d315..3b9f28189 100644 --- a/app/locales/de/messages.po +++ b/app/locales/de/messages.po @@ -949,6 +949,10 @@ msgstr "Visualisierungsmodus" msgid "controls.select.chart.type" msgstr "Diagrammtyp" +#: app/configurator/components/chart-options-selector.tsx +msgid "controls.select.color" +msgstr "Farbe auswählen" + #: app/configurator/components/chart-options-selector.tsx msgid "controls.select.column.layout" msgstr "Spaltenstil" diff --git a/app/locales/en/messages.po b/app/locales/en/messages.po index 74056298f..afd48ba1c 100644 --- a/app/locales/en/messages.po +++ b/app/locales/en/messages.po @@ -949,6 +949,10 @@ msgstr "Chart mode" msgid "controls.select.chart.type" msgstr "Chart Type" +#: app/configurator/components/chart-options-selector.tsx +msgid "controls.select.color" +msgstr "Select a color" + #: app/configurator/components/chart-options-selector.tsx msgid "controls.select.column.layout" msgstr "Column layout" diff --git a/app/locales/fr/messages.po b/app/locales/fr/messages.po index 15e57cd85..f2a467985 100644 --- a/app/locales/fr/messages.po +++ b/app/locales/fr/messages.po @@ -949,6 +949,10 @@ msgstr "Mode de visualisation" msgid "controls.select.chart.type" msgstr "Type de graphique" +#: app/configurator/components/chart-options-selector.tsx +msgid "controls.select.color" +msgstr "Sélectionner une couleur" + #: app/configurator/components/chart-options-selector.tsx msgid "controls.select.column.layout" msgstr "Mise en forme de la colonne" diff --git a/app/locales/it/messages.po b/app/locales/it/messages.po index 1f0e0c63b..afe80d3ef 100644 --- a/app/locales/it/messages.po +++ b/app/locales/it/messages.po @@ -949,6 +949,10 @@ msgstr "Modalità di visualizzazione" msgid "controls.select.chart.type" msgstr "Tipo di grafico" +#: app/configurator/components/chart-options-selector.tsx +msgid "controls.select.color" +msgstr "Seleziona un colore" + #: app/configurator/components/chart-options-selector.tsx msgid "controls.select.column.layout" msgstr "Layout a colonne" diff --git a/app/login/components/profile-content-tabs.tsx b/app/login/components/profile-content-tabs.tsx index 2073408f8..907276782 100644 --- a/app/login/components/profile-content-tabs.tsx +++ b/app/login/components/profile-content-tabs.tsx @@ -51,7 +51,6 @@ export const ProfileContentTabs = (props: ProfileContentTabsProps) => { const handleChange = useEvent((_: React.SyntheticEvent, v: string) => { setValue(v); }); - const rootClasses = useRootStyles(); const classes = useStyles(); @@ -69,7 +68,7 @@ export const ProfileContentTabs = (props: ProfileContentTabsProps) => { return ( - + diff --git a/app/login/utils.ts b/app/login/utils.ts index a1071bfb3..2d732e66d 100644 --- a/app/login/utils.ts +++ b/app/login/utils.ts @@ -28,7 +28,6 @@ export const useRootStyles = makeStyles((theme) => ({ margin: "0 auto", }, noTooltip: { - // Disable native tooltip in Safari "&::after": { content: "''", display: "block", diff --git a/app/package.json b/app/package.json index 88e876861..8f729c8fd 100644 --- a/app/package.json +++ b/app/package.json @@ -41,10 +41,12 @@ "@reach/auto-id": "^0.15.3", "@sentry/nextjs": "^7.112.2", "@storybook/nextjs": "^8.1.0", + "@testing-library/react": "^16.1.0", "@testing-library/react-hooks": "^8.0.1", "@tpluscode/rdf-ns-builders": "2.0.1", "@tpluscode/sparql-builder": "^0.3.31", "@types/react-grid-layout": "^1.3.5", + "@uiw/react-color": "^2.3.2", "@urql/devtools": "^2.0.3", "@visx/group": "^2.10.0", "@visx/text": "^2.12.2", @@ -166,7 +168,6 @@ "@playwright-testing-library/test": "^4.5.0", "@playwright/test": "^1.49.1", "@svgr/cli": "^5.5.0", - "@testing-library/react": "^14.1.2", "@types/autosuggest-highlight": "^3.2.0", "@types/clownface": "^2.0.7", "@types/cors": "^2.8.8", diff --git a/app/pages/_app.tsx b/app/pages/_app.tsx index 2a544b170..5687cadaf 100644 --- a/app/pages/_app.tsx +++ b/app/pages/_app.tsx @@ -10,19 +10,21 @@ import Head from "next/head"; import { useRouter } from "next/router"; import { useEffect } from "react"; +import { SnackbarProvider } from "@/components/snackbar"; import { PUBLIC_URL } from "@/domain/env"; import { flag } from "@/flags/flag"; import { GraphqlProvider } from "@/graphql/GraphqlProvider"; import { i18n, parseLocaleString } from "@/locales/locales"; import { LocaleProvider } from "@/locales/use-locale"; import * as federalTheme from "@/themes/federal"; +import { EventEmitterProvider } from "@/utils/eventEmitter"; import Flashes from "@/utils/flashes"; import { analyticsPageView } from "@/utils/googleAnalytics"; import AsyncLocalizationProvider from "@/utils/l10n-provider"; import "@/utils/nprogress.css"; import { useNProgress } from "@/utils/use-nprogress"; -import { EventEmitterProvider } from "@/utils/eventEmitter"; -import { SnackbarProvider } from "@/components/snackbar"; + +import "@/configurator/components/color-picker.css"; const GQLDebugPanel = dynamic(() => import("@/gql-flamegraph/devtool"), { ssr: false, diff --git a/app/palettes.ts b/app/palettes.ts index f8d151a76..e36f0a606 100644 --- a/app/palettes.ts +++ b/app/palettes.ts @@ -25,18 +25,23 @@ import { } from "d3-scale-chromatic"; import { hasDimensionColors } from "./charts/shared/colors"; -import { DivergingPaletteType, SequentialPaletteType } from "./config-types"; +import { + ColorField, + DivergingPaletteType, + PaletteType, + SequentialPaletteType, +} from "./config-types"; import { Component } from "./domain/data"; // Colors -export const getDefaultCategoricalPaletteName = ( +export const getDefaultCategoricalPaletteId = ( d?: Component, previousPaletteName?: string ): string => { const hasColors = hasDimensionColors(d); return hasColors ? "dimension" - : previousPaletteName || DEFAULT_CATEGORICAL_PALETTE_NAME; + : previousPaletteName || DEFAULT_CATEGORICAL_PALETTE_ID; }; export const getDefaultCategoricalPalette = ( @@ -53,36 +58,45 @@ export const getDefaultCategoricalPalette = ( } }; -export const getPalette = ( - palette?: string, - colors?: string[] -): ReadonlyArray => { - switch (palette) { - case "dimension": - return getDefaultCategoricalPalette(colors).colors; - case "accent": - return schemeAccent; - case "category10": - return schemeCategory10; - case "dark2": - return schemeDark2; - case "paired": - return schemePaired; - case "pastel1": - return schemePastel1; - case "pastel2": - return schemePastel2; - case "set1": - return schemeSet1; - case "set2": - return schemeSet2; - case "set3": - return schemeSet3; - case "tableau10": - return schemeTableau10; +export const getPalette = ({ + paletteId, + colorField, + colors, +}: { + paletteId?: string; + colorField?: ColorField; + colors?: string[]; +}): ReadonlyArray => { + if (colorField?.type === "single") { + return [colorField.color]; + } else { + switch (paletteId) { + case "dimension": + return getDefaultCategoricalPalette(colors).colors; + case "accent": + return schemeAccent; + case "category10": + return schemeCategory10; + case "dark2": + return schemeDark2; + case "paired": + return schemePaired; + case "pastel1": + return schemePastel1; + case "pastel2": + return schemePastel2; + case "set1": + return schemeSet1; + case "set2": + return schemeSet2; + case "set3": + return schemeSet3; + case "tableau10": + return schemeTableau10; - default: - return schemeCategory10; + default: + return schemeCategory10; + } } }; @@ -96,19 +110,39 @@ export const categoricalPalettes: Array = [ { label: "category10", value: "category10", - colors: getPalette("category10"), + colors: getPalette({ paletteId: "category10" }), + }, + { + label: "accent", + value: "accent", + colors: getPalette({ paletteId: "accent" }), + }, + { + label: "dark2", + value: "dark2", + colors: getPalette({ paletteId: "dark2" }), + }, + { + label: "paired", + value: "paired", + colors: getPalette({ paletteId: "paired" }), + }, + { + label: "pastel1", + value: "pastel1", + colors: getPalette({ paletteId: "pastel1" }), + }, + { + label: "pastel2", + value: "pastel2", + colors: getPalette({ paletteId: "pastel2" }), }, - { label: "accent", value: "accent", colors: getPalette("accent") }, - { label: "dark2", value: "dark2", colors: getPalette("dark2") }, - { label: "paired", value: "paired", colors: getPalette("paired") }, - { label: "pastel1", value: "pastel1", colors: getPalette("pastel1") }, - { label: "pastel2", value: "pastel2", colors: getPalette("pastel2") }, - { label: "set1", value: "set1", colors: getPalette("set1") }, - { label: "set2", value: "set2", colors: getPalette("set2") }, - { label: "set3", value: "set3", colors: getPalette("set3") }, + { label: "set1", value: "set1", colors: getPalette({ paletteId: "set1" }) }, + { label: "set2", value: "set2", colors: getPalette({ paletteId: "set2" }) }, + { label: "set3", value: "set3", colors: getPalette({ paletteId: "set3" }) }, ]; -export const DEFAULT_CATEGORICAL_PALETTE_NAME = categoricalPalettes[0].value; +export const DEFAULT_CATEGORICAL_PALETTE_ID = categoricalPalettes[0].value; export type Palette = { label: string; @@ -136,7 +170,7 @@ const sequentialPaletteKeys: SequentialPaletteType[] = [ "purples", "reds", ]; -const interpolatorByName = { +const interpolatorByName: Record string> = { RdBu: interpolateRdBu, RdYlBu: interpolateRdYlBu, RdYlGn: interpolateRdYlGn, @@ -154,14 +188,13 @@ const interpolatorByName = { const defaultInterpolator = interpolatorByName["oranges"]; export const getColorInterpolator = ( - palette: SequentialPaletteType | DivergingPaletteType + paletteId: PaletteType["paletteId"] ): ((t: number) => string) => { - const interpolator = interpolatorByName[palette] ?? defaultInterpolator; + const interpolator = interpolatorByName[paletteId] ?? defaultInterpolator; // If the palette is sequential, we artificially clamp the value not to display too // white a value - const isSequential = palette - ? // @ts-ignore - sequentialPaletteKeys.includes(palette) + const isSequential = paletteId + ? sequentialPaletteKeys.includes(paletteId as any) : false; return isSequential ? (n: number) => interpolator(n * 0.8 + 0.2) diff --git a/app/utils/chart-config/constants.ts b/app/utils/chart-config/constants.ts index 4079da244..df7256f7a 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.0.0"; +export const CONFIGURATOR_STATE_VERSION = "4.1.0"; -export const CHART_CONFIG_VERSION = "4.0.0"; +export const CHART_CONFIG_VERSION = "4.1.0"; diff --git a/app/utils/chart-config/versioning.spec.ts b/app/utils/chart-config/versioning.spec.ts index 29ccc6227..b38d21b80 100644 --- a/app/utils/chart-config/versioning.spec.ts +++ b/app/utils/chart-config/versioning.spec.ts @@ -124,11 +124,10 @@ describe("config migrations", () => { const migratedConfig = await migrateChartConfig(oldMapConfig, { migrationProps: CONFIGURATOR_STATE, }); - const decodedConfig = decodeChartConfig(migratedConfig); - expect(decodedConfig).toBeDefined(); + expect(migratedConfig).toBeDefined(); - const migratedOldConfig = (await migrateChartConfig(decodedConfig, { + const migratedOldConfig = (await migrateChartConfig(migratedConfig, { toVersion: "1.0.0", })) as any; expect(migratedOldConfig.version).toEqual("1.0.0"); @@ -182,9 +181,8 @@ describe("config migrations", () => { toVersion: "4.0.0", migrationProps: CONFIGURATOR_STATE, }); - const decodedConfig = decodeChartConfig(migratedConfig); - expect(decodedConfig).toBeDefined(); - expect((decodedConfig as any).fields.y.colorMapping).toMatchObject({ + expect(migratedConfig).toBeDefined(); + expect((migratedConfig as any).fields.y.colorMapping).toMatchObject({ [stringifyComponentId({ unversionedCubeIri: "https://energy.ld.admin.ch/sfoe/bfe_ogd18_gebaeudeprogramm_co2wirkung/4", diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts index cae7468a2..b8f26c23a 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -1,3 +1,4 @@ +import { schemeCategory10 } from "d3-scale-chromatic"; import stringSimilarity from "string-similarity-js"; import { DEFAULT_OTHER_COLOR_FIELD_OPACITY } from "@/charts/map/constants"; @@ -15,7 +16,7 @@ import { parseComponentId, stringifyComponentId, } from "@/graphql/make-component-id"; -import { DEFAULT_CATEGORICAL_PALETTE_NAME } from "@/palettes"; +import { DEFAULT_CATEGORICAL_PALETTE_ID } from "@/palettes"; import { CHART_CONFIG_VERSION, CONFIGURATOR_STATE_VERSION, @@ -676,17 +677,17 @@ export const chartConfigMigrations: Migration[] = [ const newConfig = { ...config, version: "2.2.0" }; if (newConfig.chartType === "comboLineSingle") { - newConfig.fields.y.palette = DEFAULT_CATEGORICAL_PALETTE_NAME; + newConfig.fields.y.palette = DEFAULT_CATEGORICAL_PALETTE_ID; newConfig.fields.y.colorMapping = mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, dimensionValues: newConfig.fields.y.componentIris.map( (iri: string) => ({ value: iri, label: iri }) ), }); } else if (newConfig.chartType === "comboLineDual") { - newConfig.fields.y.palette = DEFAULT_CATEGORICAL_PALETTE_NAME; + newConfig.fields.y.palette = DEFAULT_CATEGORICAL_PALETTE_ID; newConfig.fields.y.colorMapping = mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, dimensionValues: [ newConfig.fields.y.leftAxisComponentIri, newConfig.fields.y.rightAxisComponentIri, @@ -696,9 +697,9 @@ export const chartConfigMigrations: Migration[] = [ })), }); } else if (newConfig.chartType === "comboLineColumn") { - newConfig.fields.y.palette = DEFAULT_CATEGORICAL_PALETTE_NAME; + newConfig.fields.y.palette = DEFAULT_CATEGORICAL_PALETTE_ID; newConfig.fields.y.colorMapping = mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, dimensionValues: [ newConfig.fields.y.lineComponentIri, newConfig.fields.y.columnComponentIri, @@ -1287,6 +1288,129 @@ export const chartConfigMigrations: Migration[] = [ return newConfig; }, }, + { + description: `ALL { + fields { + segment { + - colorMapping + }, + y { + - colorMapping + } + + color { + + type + + paletteId + + colorMapping / color + } + } + }`, + from: "4.0.0", + to: "4.1.0", + + up: (config) => { + const newConfig = { + ...config, + version: "4.1.0", + }; + + if (newConfig.chartType === "table" || newConfig.chartType === "map") { + return newConfig; + } + + // Only set default color if no existing color configurations + if (!newConfig.fields?.color) { + const hasNoColorMapping = !( + newConfig.fields?.y?.colorMapping || + newConfig.fields?.segment?.colorMapping + ); + + if (hasNoColorMapping) { + newConfig.fields = { + ...newConfig.fields, + color: { + type: "single", + paletteId: "category10", + color: schemeCategory10[0], + }, + }; + } + } + + if (newConfig.fields?.segment?.colorMapping) { + newConfig.fields = { + ...newConfig.fields, + color: { + type: "segment", + paletteId: newConfig.fields.segment.palette || "category10", + colorMapping: { ...newConfig.fields.segment.colorMapping }, + }, + segment: { + ...newConfig.fields.segment, + }, + }; + delete newConfig.fields.segment.colorMapping; + delete newConfig.fields.segment.palette; + } + + if (newConfig.fields?.y?.colorMapping) { + newConfig.fields = { + ...newConfig.fields, + color: { + type: "measures", + paletteId: newConfig.fields.y.palette || "category10", + colorMapping: { ...newConfig.fields.y.colorMapping }, + }, + y: { + ...newConfig.fields.y, + }, + }; + delete newConfig.fields.y.colorMapping; + delete newConfig.fields.y.palette; + } + + return newConfig; + }, + down: (config) => { + const oldConfig = { + ...config, + version: "4.0.0", + }; + + if (oldConfig.chartType === "table" || oldConfig.chartType === "map") { + return oldConfig; + } + + if (oldConfig.fields?.color) { + if (oldConfig.fields.color.type === "segment") { + oldConfig.fields = { + ...oldConfig.fields, + segment: { + ...oldConfig.fields.segment, + colorMapping: { ...oldConfig.fields.color.colorMapping }, + palette: oldConfig.fields.color.paletteId, + }, + }; + } + + if (oldConfig.fields.color.type === "measures") { + oldConfig.fields = { + ...oldConfig.fields, + y: { + ...oldConfig.fields.y, + colorMapping: { ...oldConfig.fields.color.colorMapping }, + palette: oldConfig.fields.color.paletteId, + }, + }; + } + + // Create a new fields object without the color property + const { color, ...fieldsWithoutColor } = oldConfig.fields; + oldConfig.fields = fieldsWithoutColor; + } + + return oldConfig; + }, + }, ]; export const migrateChartConfig = makeMigrate( @@ -1868,6 +1992,43 @@ export const configuratorStateMigrations: Migration[] = [ delete dataFilters.componentIds; newConfig.dashboardFilters.dataFilters = dataFilters; + return newConfig; + }, + }, + { + description: "ALL (bump ChartConfig version)", + from: "4.0.0", + to: "4.1.0", + up: async (config) => { + const newConfig = { ...config, version: "4.1.0" }; + const chartConfigs: any[] = []; + + for (const chartConfig of newConfig.chartConfigs) { + const migratedChartConfig = await migrateChartConfig(chartConfig, { + migrationProps: newConfig, + toVersion: "4.1.0", + }); + chartConfigs.push(migratedChartConfig); + } + + newConfig.chartConfigs = chartConfigs; + + return newConfig; + }, + down: async (config) => { + const newConfig = { ...config, version: "4.0.0" }; + const chartConfigs: any[] = []; + + for (const chartConfig of newConfig.chartConfigs) { + const migratedChartConfig = await migrateChartConfig(chartConfig, { + migrationProps: newConfig, + toVersion: "4.0.0", + }); + chartConfigs.push(migratedChartConfig); + } + + newConfig.chartConfigs = chartConfigs; + return newConfig; }, }, diff --git a/yarn.lock b/yarn.lock index bbacc1192..7f47e3c01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -257,7 +257,7 @@ "@babel/highlight" "^7.24.2" picocolors "^1.0.0" -"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0": +"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== @@ -286,7 +286,29 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e" integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg== -"@babel/core@7.12.9", "@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.10.5", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.12.9", "@babel/core@^7.14.6", "@babel/core@^7.18.9", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.24.4", "@babel/core@^7.7.2", "@babel/core@^7.7.5", "@babel/core@^7.7.7": +"@babel/core@7.12.9": + version "7.12.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" + integrity sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.5" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helpers" "^7.12.5" + "@babel/parser" "^7.12.7" + "@babel/template" "^7.12.7" + "@babel/traverse" "^7.12.9" + "@babel/types" "^7.12.7" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.10.5", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.12.9", "@babel/core@^7.18.9", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.24.4", "@babel/core@^7.7.2", "@babel/core@^7.7.5", "@babel/core@^7.7.7": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== @@ -316,6 +338,17 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.12.5", "@babel/generator@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.5.tgz#e44d4ab3176bbcaf78a5725da5f1dc28802a9458" + integrity sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw== + dependencies: + "@babel/parser" "^7.26.5" + "@babel/types" "^7.26.5" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + "@babel/generator@^7.20.14": version "7.23.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" @@ -594,6 +627,15 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" +"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" + integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== + dependencies: + "@babel/helper-module-imports" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/helper-module-transforms@^7.14.5": version "7.15.8" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz" @@ -619,15 +661,6 @@ "@babel/helper-split-export-declaration" "^7.22.6" "@babel/helper-validator-identifier" "^7.22.20" -"@babel/helper-module-transforms@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" - integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== - dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/traverse" "^7.25.9" - "@babel/helper-optimise-call-expression@^7.14.5", "@babel/helper-optimise-call-expression@^7.15.4": version "7.15.4" resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz" @@ -837,7 +870,7 @@ "@babel/template" "^7.22.15" "@babel/types" "^7.22.19" -"@babel/helpers@^7.26.0": +"@babel/helpers@^7.12.5", "@babel/helpers@^7.26.0": version "7.26.0" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4" integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== @@ -882,13 +915,25 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@7.12.16", "@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.14.6", "@babel/parser@^7.14.7", "@babel/parser@^7.15.4", "@babel/parser@^7.20.15", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0", "@babel/parser@^7.24.0", "@babel/parser@^7.24.4", "@babel/parser@^7.24.5", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.2", "@babel/parser@^7.7.2": +"@babel/parser@7.12.16": + version "7.12.16" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.16.tgz#cc31257419d2c3189d394081635703f549fc1ed4" + integrity sha512-c/+u9cqV6F0+4Hpq01jnJO+GLp2DdT63ppz9Xa+6cHaajM9VFzK/iDXiKK65YtpeVwu+ctfS6iqlMqRgQRzeCw== + +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.14.7", "@babel/parser@^7.15.4", "@babel/parser@^7.20.15", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0", "@babel/parser@^7.24.0", "@babel/parser@^7.24.4", "@babel/parser@^7.24.5", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.2", "@babel/parser@^7.7.2": version "7.26.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== dependencies: "@babel/types" "^7.26.3" +"@babel/parser@^7.12.7", "@babel/parser@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.5.tgz#6fec9aebddef25ca57a935c86dbb915ae2da3e1f" + integrity sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw== + dependencies: + "@babel/types" "^7.26.5" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.5.tgz#4c3685eb9cd790bcad2843900fe0250c91ccf895" @@ -2535,6 +2580,15 @@ resolved "https://registry.npmjs.org/@babel/standalone/-/standalone-7.14.6.tgz" integrity sha512-oAoSp82jhJFnXKybKTOj5QF04XxiDRyiiqrFToiU1udlBXuZoADlPmmnOcuqBrZxSNNUjzJIVK8vt838Qoqjxg== +"@babel/template@^7.12.7", "@babel/template@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/template@^7.15.4", "@babel/template@^7.3.3": version "7.15.4" resolved "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz" @@ -2562,15 +2616,6 @@ "@babel/parser" "^7.24.0" "@babel/types" "^7.24.0" -"@babel/template@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" - integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== - dependencies: - "@babel/code-frame" "^7.25.9" - "@babel/parser" "^7.25.9" - "@babel/types" "^7.25.9" - "@babel/traverse@7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz" @@ -2601,6 +2646,19 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.12.9": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.5.tgz#6d0be3e772ff786456c1a37538208286f6e79021" + integrity sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.5" + "@babel/parser" "^7.26.5" + "@babel/template" "^7.25.9" + "@babel/types" "^7.26.5" + debug "^4.3.1" + globals "^11.1.0" + "@babel/traverse@^7.18.9", "@babel/traverse@^7.23.2": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.0.tgz#4a408fbf364ff73135c714a2ab46a5eab2831b1e" @@ -2663,6 +2721,14 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" +"@babel/types@^7.12.7", "@babel/types@^7.26.5": + version "7.26.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.5.tgz#7a1e1c01d28e26d1fe7f8ec9567b3b92b9d07747" + integrity sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@babel/types@^7.16.7": version "7.17.0" resolved "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz" @@ -8141,20 +8207,6 @@ lz-string "^1.4.4" pretty-format "^26.6.2" -"@testing-library/dom@^9.0.0": - version "9.3.3" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.3.tgz#108c23a5b0ef51121c26ae92eb3179416b0434f5" - integrity sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.12.5" - "@types/aria-query" "^5.0.1" - aria-query "5.1.3" - chalk "^4.1.0" - dom-accessibility-api "^0.5.9" - lz-string "^1.5.0" - pretty-format "^27.0.2" - "@testing-library/dom@^9.3.4": version "9.3.4" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.4.tgz#50696ec28376926fec0a1bf87d9dbac5e27f60ce" @@ -8191,14 +8243,12 @@ "@babel/runtime" "^7.12.5" react-error-boundary "^3.1.0" -"@testing-library/react@^14.1.2": - version "14.1.2" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.1.2.tgz#a2b9e9ee87721ec9ed2d7cfc51cc04e474537c32" - integrity sha512-z4p7DVBTPjKM5qDZ0t5ZjzkpSNb+fZy1u6bzO7kk8oeGagpPCAtgh4cx1syrfp7a+QWkM021jGqjJaxJJnXAZg== +"@testing-library/react@^16.1.0": + version "16.1.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.1.0.tgz#aa0c61398bac82eaf89776967e97de41ac742d71" + integrity sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg== dependencies: "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^9.0.0" - "@types/react-dom" "^18.0.0" "@testing-library/user-event@^14.5.2": version "14.5.2" @@ -9103,13 +9153,6 @@ dependencies: "@types/react" "^17" -"@types/react-dom@^18.0.0": - version "18.2.17" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.17.tgz#375c55fab4ae671bd98448dcfa153268d01d6f64" - integrity sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg== - dependencies: - "@types/react" "*" - "@types/react-grid-layout@^1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz#f4b52bf27775290ee0523214be0987be14e66823" @@ -9536,6 +9579,207 @@ "@typescript-eslint/types" "8.19.1" eslint-visitor-keys "^4.2.0" +"@uiw/color-convert@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/color-convert/-/color-convert-2.3.2.tgz#1cb7931d6ec3bda8ecadca4ae65a382bec3813a2" + integrity sha512-Txs0oAcOGhvM15yi7NqDJSws6htpuGx75EblFlZmh4h4AyUYXaeN2HNcOAUt835M3SN1j7rqMC+XERIE4r776Q== + +"@uiw/react-color-alpha@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-alpha/-/react-color-alpha-2.3.2.tgz#61a3bac06bd426c4c1cc1893a536b24de31881b6" + integrity sha512-+yh+KEpNKjxNFFODQrB3Lki2hu6kznoSCngHgptlWBUtAC3e/e7tIiTTedSpCGr7fwIpC0CWrKwxENA3tyY/2Q== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-drag-event-interactive" "2.3.2" + +"@uiw/react-color-block@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-block/-/react-color-block-2.3.2.tgz#e5afd883f40650cf34173cc46073d0c385e36261" + integrity sha512-eic08WG6IFBEWrsE/U9aMuZxW9gSdX4s5iD5TyZMlHlUiGIneGXEOOSHNqlIfA7Dxbs1STYQbEQU/aSx6APYLw== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-editable-input" "2.3.2" + "@uiw/react-color-swatch" "2.3.2" + +"@uiw/react-color-chrome@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-chrome/-/react-color-chrome-2.3.2.tgz#6d1f0a52fc6419ddb93cec998e69d6b1db1964da" + integrity sha512-WvA8dg2y+vgoyy8GFBM3B1+SeJ29ov5OVEei1kACMKxThADPdI4w3RRmhYIMnSeFGVW3bGuBMq6JimjIKZirCQ== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-alpha" "2.3.2" + "@uiw/react-color-editable-input" "2.3.2" + "@uiw/react-color-editable-input-hsla" "2.3.2" + "@uiw/react-color-editable-input-rgba" "2.3.2" + "@uiw/react-color-github" "2.3.2" + "@uiw/react-color-hue" "2.3.2" + "@uiw/react-color-saturation" "2.3.2" + +"@uiw/react-color-circle@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-circle/-/react-color-circle-2.3.2.tgz#9f279924cfb30750c285eb269208868cf1eefea8" + integrity sha512-lndeyFmvKNZ5MBwL9BqkfceuhOnIGQW3gB9wKRPxiKmux9maxllirTwWNvv/6vgGFMnfaR89NO7pjTyf7mNhOA== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-swatch" "2.3.2" + +"@uiw/react-color-colorful@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-colorful/-/react-color-colorful-2.3.2.tgz#118a3893f892c5ef681919512b32a2a65a641ee0" + integrity sha512-Rr1qa4Uo588CZjOx6OZIXjo7+CNcnqogU0Nel6L5zzwL+3CNBC3GfS4Or4db/ZxFhN49qt6NqsQa/ykA2/ixpg== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-alpha" "2.3.2" + "@uiw/react-color-hue" "2.3.2" + "@uiw/react-color-saturation" "2.3.2" + +"@uiw/react-color-compact@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-compact/-/react-color-compact-2.3.2.tgz#c5ec1f1917f6fd1a25dfd1c18315596419ec4987" + integrity sha512-3/eQkndGBIBr91mkPoOgS8NwTxpz5wEt5UK6CSvbC+zOQtEzZaWq+XpB7ieoUjla3yiKgdBauu3iYTwo1LOFEw== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-editable-input" "2.3.2" + "@uiw/react-color-editable-input-rgba" "2.3.2" + "@uiw/react-color-swatch" "2.3.2" + +"@uiw/react-color-editable-input-hsla@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-editable-input-hsla/-/react-color-editable-input-hsla-2.3.2.tgz#749116a1842f71eb786b40bc28e0ef82890c5557" + integrity sha512-lLO8K+Zv5L9HKBgM3zYSqeLKisBkpXCxlQmF8iCKYcIgeRilM26qqylskA4n6pVixfSooL0hyL/HwfNmbCIrrg== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-editable-input-rgba" "2.3.2" + +"@uiw/react-color-editable-input-rgba@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-editable-input-rgba/-/react-color-editable-input-rgba-2.3.2.tgz#711d4e85183f5c3f5fabaf1bf4e7ca6a6dbe7d7c" + integrity sha512-HV0+5zzpaNG5v6EyVgmPfInd9UzYknQI7gdsVmmXKzB13L3RFhiv77r6o+q3IZMEnoDZ3U92uX4FeRaM1NrwYw== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-editable-input" "2.3.2" + +"@uiw/react-color-editable-input@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-editable-input/-/react-color-editable-input-2.3.2.tgz#1df092ede3b301d29c12a8aba63e1eed1dcf7daa" + integrity sha512-DDl9pCN7hH5Q+OB6LiFGFmkqIQw81EDIEvDi6rr6G9U+k+oqZ9JCBKSZ9qNKSa4MqnuISOkFv0vL3Jh8fSyqcg== + +"@uiw/react-color-github@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-github/-/react-color-github-2.3.2.tgz#8ee1d1992b5293f69e6f68c98f6f2c1bd15515cd" + integrity sha512-3QxpEOKYXbbV/L1cYsf7nhoOnl19/zWTpRRgda8LOe3SuRhFrFM3ZLa+UJUEXgO1cg9h64gxSKINh2st/FawpQ== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-swatch" "2.3.2" + +"@uiw/react-color-hue@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-hue/-/react-color-hue-2.3.2.tgz#6cf5d821461a1047a7d1770bf150cf1c6997b342" + integrity sha512-aAveo++GAghw09Ngc8Zzwxhj9mGaJfw8q40fDGFrVNxdrwrAjySIKHzlOSg5kw6WnEp4tUjhkMXDfCZWUhqmPA== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-alpha" "2.3.2" + +"@uiw/react-color-material@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-material/-/react-color-material-2.3.2.tgz#f0a780a5221af0369af5d38236ad82447b83bda3" + integrity sha512-fb+bVCwRoeb4INAFEEYU26GWU7+/695DFz7C/dA2RLUa279NhVtNaOtISULT+u+Aerf2dR6GrjBk5wdgNqRqPg== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-editable-input" "2.3.2" + "@uiw/react-color-editable-input-rgba" "2.3.2" + +"@uiw/react-color-name@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-name/-/react-color-name-2.3.2.tgz#d520077d501f879742d6e33dbde5e8d6a81302cf" + integrity sha512-ZLt6ypbsGbo48wSgtFa5t+egZ57VmWIriyW/6rMNK5nMB8Y9Da7tqT2dL4WfwPzrG7I/97qafinfnGzPAuHdsw== + dependencies: + colors-named "^1.0.1" + colors-named-hex "^1.0.1" + +"@uiw/react-color-saturation@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-saturation/-/react-color-saturation-2.3.2.tgz#8987a142f59d19180739711a49c24c71065c52c9" + integrity sha512-aDKMhjylBUb4dH4oTQYz+U4mhpOebbQ2J0Y8y5aX1tfZ3fZuBqnXtWzu7Ffj3ksdKwkQHPddxT5IDTbAChF/og== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-drag-event-interactive" "2.3.2" + +"@uiw/react-color-shade-slider@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-shade-slider/-/react-color-shade-slider-2.3.2.tgz#056e6ad5aa320a2a70bb07c1e8aad249b5cf9feb" + integrity sha512-nM8AJPpq9UnC7LWjQEZ28bIm8gMRqRzk3dXfGQ4X3t3040d11o4sbl23pTmWTjn6P+3+MP/L2FhmvLSTUkMp/g== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-alpha" "2.3.2" + +"@uiw/react-color-sketch@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-sketch/-/react-color-sketch-2.3.2.tgz#c9b680360e658bd7f474a7d9d039f9525a0af044" + integrity sha512-CMCbzarRVEqZLCi84AE4RQrHapvZQTXFylm7A7mfA28qInFFyF3jZsqRu6Y5sc33rPmL4AeSURbgPljmolxQ1g== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-alpha" "2.3.2" + "@uiw/react-color-editable-input" "2.3.2" + "@uiw/react-color-editable-input-rgba" "2.3.2" + "@uiw/react-color-hue" "2.3.2" + "@uiw/react-color-saturation" "2.3.2" + "@uiw/react-color-swatch" "2.3.2" + +"@uiw/react-color-slider@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-slider/-/react-color-slider-2.3.2.tgz#67219865df3f3e275b516617d7f995a73797fa9d" + integrity sha512-Mf9w9YwI+nYxagoTPsxzEHX+/EkVTH0Ak84V6CgcmHVxM3zGESdpalZ+9B6NFWjy9nP7Oa2lK1i4cyZr8D7v2g== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-alpha" "2.3.2" + +"@uiw/react-color-swatch@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-swatch/-/react-color-swatch-2.3.2.tgz#f5acac4c73da10d88075cf3c67f4fb7022f5430f" + integrity sha512-AjkEcSdlpxiFm9yull4WDujuHr0tD9/+XkLtcus/MH768zSQbb+rj6cY1nZ8L8FI6LRDGRaVJqFaXm4ZOAaIZw== + dependencies: + "@uiw/color-convert" "2.3.2" + +"@uiw/react-color-wheel@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color-wheel/-/react-color-wheel-2.3.2.tgz#066ec384da3ea4e5728de3973bf9be27ca2670f6" + integrity sha512-AwZZusWq+nlNNEjif6ruryrgc/cVuQ0x6XbdIVUbiQekfHFv+LunHnMS4EtmX+yPiOVihTvBp8NpfrdN2jJ8hw== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-drag-event-interactive" "2.3.2" + +"@uiw/react-color@^2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-color/-/react-color-2.3.2.tgz#a8a17cbdba00980f4f0d0886b54b9ae50ad5d1b6" + integrity sha512-PfyzYFIa16MKVd2FLFmV/+moznibDCDZB6AUwV6lrL7Mz+/jjcW1JDDdCRWMHC8e7uoC3EmnYAmnpj2eF0w2vw== + dependencies: + "@uiw/color-convert" "2.3.2" + "@uiw/react-color-alpha" "2.3.2" + "@uiw/react-color-block" "2.3.2" + "@uiw/react-color-chrome" "2.3.2" + "@uiw/react-color-circle" "2.3.2" + "@uiw/react-color-colorful" "2.3.2" + "@uiw/react-color-compact" "2.3.2" + "@uiw/react-color-editable-input" "2.3.2" + "@uiw/react-color-editable-input-hsla" "2.3.2" + "@uiw/react-color-editable-input-rgba" "2.3.2" + "@uiw/react-color-github" "2.3.2" + "@uiw/react-color-hue" "2.3.2" + "@uiw/react-color-material" "2.3.2" + "@uiw/react-color-name" "2.3.2" + "@uiw/react-color-saturation" "2.3.2" + "@uiw/react-color-shade-slider" "2.3.2" + "@uiw/react-color-sketch" "2.3.2" + "@uiw/react-color-slider" "2.3.2" + "@uiw/react-color-swatch" "2.3.2" + "@uiw/react-color-wheel" "2.3.2" + +"@uiw/react-drag-event-interactive@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@uiw/react-drag-event-interactive/-/react-drag-event-interactive-2.3.2.tgz#852447609d3d8c6b2a4a5822be58d6cb73f19756" + integrity sha512-lG5pJCtqbYBv7Dj0r12PE9q9yg7P2CzlQodw5ZHPY9GCSZVXHJc0g4lGvCbe/4Y8HYqM8aU4CYS8LplpX+mIQw== + "@ungap/structured-clone@^1.2.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.1.tgz#28fa185f67daaf7b7a1a8c1d445132c5d979f8bd" @@ -12059,6 +12303,16 @@ colorette@^2.0.10: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +colors-named-hex@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/colors-named-hex/-/colors-named-hex-1.0.2.tgz#353165cc548ef0fbd770280bf441ec2dfc1bb386" + integrity sha512-k6kq1e1pUCQvSVwIaGFq2l0LrkAPQZWyeuZn1Z8nOiYSEZiKoFj4qx690h2Kd34DFl9Me0gKS6MUwAMBJj8nuA== + +colors-named@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/colors-named/-/colors-named-1.0.2.tgz#362dd6b520c08da8d9a77250174f0d5f2cfc5b81" + integrity sha512-2ANq2r393PV9njYUD66UdfBcxR1slMqRA3QRTWgCx49JoCJ+kOhyfbQYxKJbPZQIhZUcNjVOs5AlyY1WwXec3w== + colors@1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz" @@ -15173,7 +15427,7 @@ fwd-stream@^1.0.4: dependencies: readable-stream "~1.0.26-4" -gensync@^1.0.0-beta.2: +gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== @@ -22108,7 +22362,7 @@ resolve@^1.21.0, resolve@^1.22.1: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.22.4: +resolve@^1.22.4, resolve@^1.3.2: version "1.22.10" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== @@ -22432,6 +22686,11 @@ semver@7.x, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: dependencies: lru-cache "^6.0.0" +semver@^5.4.1: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz"