diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e8c547d4..d2ae0633c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ You can also check the - Bar charts, dashboard text blocks and Markdown inputs are not hidden behind flags anymore - Fixes + - Added button translations for custom color palette update and create button - Color swatches inside the color picker dynamically adjust to the colors of the selected palette @@ -51,6 +52,8 @@ You can also check the - Fixed errors regarding switching form existing categorical palette to a diverging color palette - Improved filter section styling + - Chart colors stay persistent over chart type changes + - Maintenance - Added authentication method to e2e tests - Added authentication to Vercel previews for easier testing diff --git a/app/charts/bar/bars-state.tsx b/app/charts/bar/bars-state.tsx index 3964d1d91..062111a43 100644 --- a/app/charts/bar/bars-state.tsx +++ b/app/charts/bar/bars-state.tsx @@ -4,6 +4,8 @@ import { scaleBand, ScaleLinear, scaleLinear, + ScaleOrdinal, + scaleOrdinal, scaleTime, } from "d3-scale"; import orderBy from "lodash/orderBy"; @@ -45,6 +47,7 @@ import { useFormatNumber, useTimeFormatUnit, } from "@/formatters"; +import { getPalette } from "@/palettes"; import { getSortingOrders, makeDimensionValueSorters, @@ -62,6 +65,7 @@ export type BarsState = CommonChartState & yScale: ScaleBand; minY: string; getAnnotationInfo: (d: Observation) => TooltipInfo; + colors: ScaleOrdinal; }; const useBarsState = ( @@ -272,6 +276,15 @@ const useBarsState = ( }; }; + const colors = scaleOrdinal(); + + colors.range( + getPalette({ + paletteId: fields.color.paletteId, + colorField: fields.color, + }) + ); + return { chartType: "bar", bounds: { @@ -286,6 +299,7 @@ const useBarsState = ( yScaleInteraction, yScale, getAnnotationInfo, + colors, ...variables, }; }; diff --git a/app/charts/bar/bars.tsx b/app/charts/bar/bars.tsx index 5522a0134..5508bf1c0 100644 --- a/app/charts/bar/bars.tsx +++ b/app/charts/bar/bars.tsx @@ -79,8 +79,16 @@ export const ErrorWhiskers = () => { }; export const Bars = () => { - const { chartData, bounds, getX, xScale, getY, yScale, getRenderingKey } = - useChartState() as BarsState; + const { + chartData, + bounds, + getX, + xScale, + getY, + yScale, + getRenderingKey, + colors, + } = useChartState() as BarsState; const theme = useTheme(); const { margins } = bounds; const ref = useRef(null); @@ -89,10 +97,6 @@ export const Bars = () => { const bandwidth = yScale.bandwidth(); const x0 = xScale(0); const renderData: RenderBarDatum[] = useMemo(() => { - const getColor = (d: number) => { - return d <= 0 ? theme.palette.secondary.main : schemeCategory10[0]; - }; - return chartData.map((d) => { const key = getRenderingKey(d); const yScaled = yScale(getY(d)) as number; @@ -101,7 +105,6 @@ export const Bars = () => { const xScaled = xScale(x); const xRender = xScale(Math.min(x, 0)); const width = Math.max(0, Math.abs(xScaled - x0)); - const color = getColor(x); return { key, @@ -109,7 +112,7 @@ export const Bars = () => { y: yScaled, width, height: bandwidth, - color, + color: colors(d.key as string) ?? schemeCategory10[0], }; }); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts index 28f505868..a6bc05eee 100644 --- a/app/charts/chart-config-ui-options.ts +++ b/app/charts/chart-config-ui-options.ts @@ -837,6 +837,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { options: { showStandardError: {}, showConfidenceInterval: {}, + colorPalette: { + type: "single", + paletteId: "dimension", + color: theme.palette.primary.main, + }, }, }, { diff --git a/app/charts/index.spec.ts b/app/charts/index.spec.ts index 2e1d5aba5..e256f0196 100644 --- a/app/charts/index.spec.ts +++ b/app/charts/index.spec.ts @@ -9,6 +9,7 @@ import { import { Dimension, Measure } from "@/domain/data"; import { stringifyComponentId } from "@/graphql/make-component-id"; import { TimeUnit } from "@/graphql/resolver-types"; +import { DEFAULT_CATEGORICAL_PALETTE_ID } from "@/palettes"; import bathingWaterData from "../test/__fixtures/data/DataCubeMetadataWithComponentValues-bathingWater.json"; import forestAreaData from "../test/__fixtures/data/forest-area-by-production-region.json"; @@ -330,6 +331,7 @@ describe("chart type switch", () => { dimensions: bathingWaterData.data.dataCubeByIri .dimensions as any as Dimension[], measures: bathingWaterData.data.dataCubeByIri.measures as Measure[], + palette: DEFAULT_CATEGORICAL_PALETTE_ID, }); expect(newConfig.interactiveFiltersConfig?.dataFilters.active).toEqual( @@ -470,6 +472,7 @@ describe("chart type switch", () => { id: "https://environment.ld.admin.ch/foen/ubd000502/werteNichtGerundet", }, ] as any as Measure[], + palette: DEFAULT_CATEGORICAL_PALETTE_ID, }) as ColumnConfig; expect(newChartConfig.fields.segment).toBeUndefined(); @@ -512,6 +515,7 @@ describe("chart type switch", () => { dimensions, measures, isAddingNewCube: true, + palette: DEFAULT_CATEGORICAL_PALETTE_ID, }) as ComboLineDualConfig; expect( @@ -527,6 +531,7 @@ describe("chart type switch", () => { dimensions, measures, isAddingNewCube: false, + palette: DEFAULT_CATEGORICAL_PALETTE_ID, }) as ComboLineDualConfig; expect( diff --git a/app/charts/index.ts b/app/charts/index.ts index 5d38bb298..adda8d46f 100644 --- a/app/charts/index.ts +++ b/app/charts/index.ts @@ -27,12 +27,15 @@ import { ChartConfig, ChartSegmentField, ChartType, - ColorField, ColumnSegmentField, ComboChartType, + ComboLineColumnConfig, ComboLineColumnFields, + ComboLineDualConfig, + ComboLineSingleConfig, ComboLineSingleFields, Cube, + CustomPaletteType, Filters, GenericChartConfig, GenericField, @@ -45,6 +48,7 @@ import { isComboLineColumnConfig, isComboLineDualConfig, isComboLineSingleConfig, + isCustomColorPalette, isLineConfig, isMapConfig, isPieConfig, @@ -769,12 +773,14 @@ export const getChartConfigAdjustedToChartType = ({ dimensions, measures, isAddingNewCube, + palette, }: { chartConfig: ChartConfig; newChartType: ChartType; dimensions: Dimension[]; measures: Measure[]; isAddingNewCube?: boolean; + palette: CustomPaletteType | string; }): ChartConfig => { const oldChartType = chartConfig.chartType; const initialConfig = getInitialConfig({ @@ -802,6 +808,7 @@ export const getChartConfigAdjustedToChartType = ({ dimensions, measures, isAddingNewCube, + palette, }); }; @@ -815,6 +822,7 @@ const getAdjustedChartConfig = ({ dimensions, measures, isAddingNewCube, + palette, }: { path: string; field: Object; @@ -825,6 +833,7 @@ const getAdjustedChartConfig = ({ dimensions: Dimension[]; measures: Measure[]; isAddingNewCube?: boolean; + palette: CustomPaletteType | string; }) => { // For filters & segments we can't reach a primitive level as we need to // pass the whole object. Table fields have an [id: TableColumn] structure, @@ -841,6 +850,7 @@ const getAdjustedChartConfig = ({ isSegmentInConfig(newChartConfig) ); case "filters": + case "fields.color": case "fields.segment": case "fields.animation": case "interactiveFiltersConfig.calculation": @@ -876,6 +886,7 @@ const getAdjustedChartConfig = ({ dimensions, measures, isAddingNewCube, + palette, }); } } else { @@ -1014,15 +1025,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }); }, }, - segment: ({ - oldValue, - oldChartConfig, - newChartConfig, - dimensions, - measures, - }) => { + segment: ({ oldValue, oldChartConfig, newChartConfig, measures }) => { let newSegment: ColumnSegmentField | undefined; - let newColor: ColorField | undefined; const yMeasure = measures.find( (d) => d.id === newChartConfig.fields.y.componentId @@ -1030,20 +1034,16 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { // When switching from a table chart, a whole fields object is passed as oldValue. if (oldChartConfig.chartType === "table") { - const maybeSegmentAndColorFields = - convertTableFieldsToSegmentAndColorFields({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentField = convertTableFieldsToSegmentFields({ + fields: oldValue as TableFields, + }); - if (maybeSegmentAndColorFields) { + if (maybeSegmentField) { newSegment = { - ...maybeSegmentAndColorFields.segment, + ...maybeSegmentField, 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 @@ -1063,22 +1063,26 @@ 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 && newColor?.type === "segment") { + if (newSegment) { draft.fields.segment = newSegment; - draft.fields.color = newColor; + } + }); + }, + color: ({ oldValue, newChartConfig }) => { + return produce(newChartConfig, (draft) => { + if (isColorInConfig(newChartConfig)) { + if (oldValue.type !== "measures") { + draft.fields.color = oldValue; + } else { + draft.fields.color = { + type: "single", + paletteId: oldValue.paletteId, + color: Object.values(oldValue.colorMapping)[0], + }; + } } }); }, @@ -1129,15 +1133,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { return newChartConfig; }, }, - segment: ({ - oldValue, - oldChartConfig, - newChartConfig, - dimensions, - measures, - }) => { + segment: ({ oldValue, oldChartConfig, newChartConfig, measures }) => { let newSegment: ColumnSegmentField | undefined; - let newColor: ColorField | undefined; const xMeasure = measures.find( (d) => d.id === newChartConfig.fields.x.componentId @@ -1145,20 +1142,16 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { // When switching from a table chart, a whole fields object is passed as oldValue. if (oldChartConfig.chartType === "table") { - const maybeSegmentAndColorFields = - convertTableFieldsToSegmentAndColorFields({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentField = convertTableFieldsToSegmentFields({ + fields: oldValue as TableFields, + }); - if (maybeSegmentAndColorFields) { + if (maybeSegmentField) { newSegment = { - ...maybeSegmentAndColorFields.segment, + ...maybeSegmentField, 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 @@ -1178,22 +1171,26 @@ 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 && newColor?.type === "segment") { + if (newSegment) { draft.fields.segment = newSegment; - draft.fields.color = newColor; + } + }); + }, + color: ({ oldValue, newChartConfig }) => { + return produce(newChartConfig, (draft) => { + if (isColorInConfig(newChartConfig)) { + if (oldValue.type !== "measures") { + draft.fields.color = oldValue; + } else { + draft.fields.color = { + type: "single", + paletteId: oldValue.paletteId, + color: Object.values(oldValue.colorMapping)[0], + }; + } } }); }, @@ -1250,27 +1247,16 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }); }, }, - segment: ({ - oldValue, - oldChartConfig, - newChartConfig, - dimensions, - measures, - }) => { + segment: ({ oldValue, oldChartConfig, newChartConfig, dimensions }) => { let newSegment: LineSegmentField | undefined; - let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const maybeSegmentAndColorFields = - convertTableFieldsToSegmentAndColorFields({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentField = convertTableFieldsToSegmentFields({ + fields: oldValue as TableFields, + }); - if (maybeSegmentAndColorFields) { - newSegment = maybeSegmentAndColorFields.segment; - newColor = maybeSegmentAndColorFields.color; + if (maybeSegmentField) { + newSegment = maybeSegmentField; } } else { const oldSegment = oldValue as Exclude; @@ -1288,21 +1274,27 @@ 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 && newColor?.type === "segment") { + if (newSegment) { draft.fields.segment = newSegment; - draft.fields.color = newColor; + } + }); + }, + color: ({ oldValue, newChartConfig }) => { + return produce(newChartConfig, (draft) => { + if (isColorInConfig(newChartConfig)) { + if (oldValue.type !== "measures") { + draft.fields.color = oldValue; + } else { + draft.fields.color = { + type: "single", + paletteId: oldValue.paletteId, + color: Object.values(oldValue.colorMapping)[0], + }; + } } }); }, @@ -1361,22 +1353,17 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { } let newSegment: AreaSegmentField | undefined; - let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const maybeSegmentAndColorFields = - convertTableFieldsToSegmentAndColorFields({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentField = convertTableFieldsToSegmentFields({ + fields: oldValue as TableFields, + }); - if (maybeSegmentAndColorFields) { + if (maybeSegmentField) { newSegment = { - ...maybeSegmentAndColorFields.segment, + ...maybeSegmentField, sorting: DEFAULT_SORTING, }; - newColor = maybeSegmentAndColorFields.color; } } else { const oldSegment = oldValue as Exclude; @@ -1393,21 +1380,27 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { 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 && newColor?.type === "segment") { + if (newSegment) { draft.fields.segment = newSegment; - draft.fields.color = newColor; + } + }); + }, + color: ({ oldValue, newChartConfig }) => { + return produce(newChartConfig, (draft) => { + if (isColorInConfig(newChartConfig)) { + if (oldValue.type !== "measures") { + draft.fields.color = oldValue; + } else { + draft.fields.color = { + type: "single", + paletteId: oldValue.paletteId, + color: Object.values(oldValue.colorMapping)[0], + }; + } } }); }, @@ -1443,27 +1436,16 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { return newChartConfig; }, }, - segment: ({ - oldValue, - oldChartConfig, - newChartConfig, - dimensions, - measures, - }) => { + segment: ({ oldValue, oldChartConfig, newChartConfig }) => { let newSegment: ScatterPlotSegmentField | undefined; - let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const maybeSegmentAndColorFields = - convertTableFieldsToSegmentAndColorFields({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentField = convertTableFieldsToSegmentFields({ + fields: oldValue as TableFields, + }); - if (maybeSegmentAndColorFields) { - newSegment = maybeSegmentAndColorFields.segment; - newColor = maybeSegmentAndColorFields.color; + if (maybeSegmentField) { + newSegment = maybeSegmentField; } } else { const oldSegment = oldValue as Exclude; @@ -1473,9 +1455,23 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { } return produce(newChartConfig, (draft) => { - if (newSegment && newColor?.type === "segment") { + if (newSegment) { draft.fields.segment = newSegment; - draft.fields.color = newColor; + } + }); + }, + color: ({ oldValue, newChartConfig }) => { + return produce(newChartConfig, (draft) => { + if (isColorInConfig(newChartConfig)) { + if (oldValue.type !== "measures") { + draft.fields.color = oldValue; + } else { + draft.fields.color = { + type: "single", + paletteId: oldValue.paletteId, + color: Object.values(oldValue.colorMapping)[0], + }; + } } }); }, @@ -1506,30 +1502,19 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }); }, }, - segment: ({ - oldValue, - oldChartConfig, - newChartConfig, - dimensions, - measures, - }) => { + segment: ({ oldValue, oldChartConfig, newChartConfig }) => { let newSegment: PieSegmentField | undefined; - let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const maybeSegmentAndColorFields = - convertTableFieldsToSegmentAndColorFields({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentField = convertTableFieldsToSegmentFields({ + fields: oldValue as TableFields, + }); - if (maybeSegmentAndColorFields) { + if (maybeSegmentField) { newSegment = { - ...maybeSegmentAndColorFields.segment, + ...maybeSegmentField, sorting: DEFAULT_SORTING, }; - newColor = maybeSegmentAndColorFields.color; } } else { const oldSegment = oldValue as Exclude; @@ -1541,22 +1526,52 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { 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 && newColor?.type === "segment") { + if (newSegment) { draft.fields.segment = newSegment; - draft.fields.color = newColor; + } + }); + }, + color: ({ oldValue, newChartConfig, palette, dimensions }) => { + return produce(newChartConfig, (draft) => { + if ( + isColorInConfig(newChartConfig) && + isSegmentInConfig(newChartConfig) + ) { + switch (oldValue.type) { + case "measures": + draft.fields.color = { + ...oldValue, + type: "segment", + }; + break; + case "segment": + draft.fields.color = oldValue; + + break; + case "single": + console.log(); + draft.fields.color = { + type: "segment", + paletteId: oldValue.paletteId, + colorMapping: mapValueIrisToColor({ + paletteId: isCustomColorPalette(palette) + ? palette.paletteId + : palette, + dimensionValues: + dimensions.find( + (d) => + d.id === newChartConfig.fields.segment?.componentId + )?.values || [], + customPalette: isCustomColorPalette(palette) + ? palette + : undefined, + }), + }; + break; + } } }); }, @@ -1720,6 +1735,46 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }); }, }, + color: ({ oldValue, newChartConfig, palette }) => { + return produce(newChartConfig, (draft) => { + if ( + isColorInConfig(newChartConfig) && + isComboChartConfig(newChartConfig) + ) { + switch (oldValue.type) { + case "measures": + draft.fields.color = oldValue; + break; + case "single": + draft.fields.color = { + type: "measures", + paletteId: oldValue.paletteId, + colorMapping: mapValueIrisToColor({ + paletteId: isCustomColorPalette(palette) + ? palette.paletteId + : palette, + dimensionValues: ( + newChartConfig as ComboLineSingleConfig + ).fields.y.componentIds.map((id) => ({ + value: id, + label: id, + })), + customPalette: isCustomColorPalette(palette) + ? palette + : undefined, + }), + }; + break; + case "segment": + draft.fields.color = { + ...oldValue, + type: "measures", + }; + break; + } + } + }); + }, }, interactiveFiltersConfig: interactiveFiltersAdjusters, }, @@ -1841,6 +1896,65 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }; }); }, + color: ({ oldValue, newChartConfig, palette }) => { + return produce(newChartConfig, (draft) => { + if ( + isColorInConfig(newChartConfig) && + isComboChartConfig(newChartConfig) + ) { + const y = (newChartConfig as ComboLineDualConfig).fields.y; + + switch (oldValue.type) { + case "measures": + draft.fields.color = oldValue; + break; + case "single": + draft.fields.color = { + type: "measures", + paletteId: oldValue.paletteId, + colorMapping: mapValueIrisToColor({ + paletteId: isCustomColorPalette(palette) + ? palette.paletteId + : palette, + dimensionValues: [ + y.leftAxisComponentId, + y.rightAxisComponentId, + ].map((id) => ({ + value: id, + label: id, + })), + customPalette: isCustomColorPalette(palette) + ? palette + : undefined, + }), + }; + break; + case "segment": + draft.fields.color = { + paletteId: oldValue.paletteId, + colorMapping: mapValueIrisToColor({ + paletteId: isCustomColorPalette(palette) + ? palette.paletteId + : palette, + dimensionValues: [ + y.leftAxisComponentId, + y.rightAxisComponentId, + ].map((id) => ({ + value: id, + label: id, + })), + + customPalette: isCustomColorPalette(palette) + ? palette + : undefined, + }), + type: "measures", + }; + break; + } + } + }); + }, }, interactiveFiltersConfig: interactiveFiltersAdjusters, }, @@ -1940,6 +2054,48 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }; }); }, + color: ({ oldValue, newChartConfig, palette }) => { + return produce(newChartConfig, (draft) => { + if ( + isColorInConfig(newChartConfig) && + isComboChartConfig(newChartConfig) + ) { + switch (oldValue.type) { + case "measures": + draft.fields.color = oldValue; + break; + case "single": + const y = (newChartConfig as ComboLineColumnConfig).fields.y; + draft.fields.color = { + type: "measures", + paletteId: oldValue.paletteId, + colorMapping: mapValueIrisToColor({ + paletteId: isCustomColorPalette(palette) + ? palette.paletteId + : palette, + dimensionValues: [ + y.lineComponentId, + y.columnComponentId, + ].map((id) => ({ + value: id, + label: id, + })), + customPalette: isCustomColorPalette(palette) + ? palette + : undefined, + }), + }; + break; + case "segment": + draft.fields.color = { + ...oldValue, + type: "measures", + }; + break; + } + } + }); + }, }, interactiveFiltersConfig: interactiveFiltersAdjusters, }, @@ -2576,15 +2732,11 @@ export const getFieldComponentId = ( return (fields as $IntentionalAny)[field]?.componentId; }; -const convertTableFieldsToSegmentAndColorFields = ({ +const convertTableFieldsToSegmentFields = ({ fields, - dimensions, - measures, }: { fields: TableFields; - dimensions: Dimension[]; - measures: Measure[]; -}): { segment: GenericField; color: ColorField } | undefined => { +}): GenericField | undefined => { const groupedColumns = group(Object.values(fields), (d) => d.isGroup) .get(true) ?.filter((d) => SEGMENT_ENABLED_COMPONENTS.includes(d.componentType)) @@ -2595,28 +2747,7 @@ const convertTableFieldsToSegmentAndColorFields = ({ return; } - const { componentId } = component; - const actualComponent = [...dimensions, ...measures].find( - (d) => d.id === componentId - ) as Component; - const paletteId = getDefaultCategoricalPaletteId(actualComponent); - - return { - segment: { - componentId, - }, - color: { - type: "segment", - paletteId: paletteId, - colorMapping: mapValueIrisToColor({ - paletteId: paletteId, - dimensionValues: [componentId].map((id) => ({ - value: id, - label: id, - })), - }), - }, - }; + return component; }; export const getChartSymbol = ( diff --git a/app/config-adjusters.ts b/app/config-adjusters.ts index a927a97e8..4679fd289 100644 --- a/app/config-adjusters.ts +++ b/app/config-adjusters.ts @@ -7,6 +7,7 @@ import { BarFields, BarSegmentField, ChartConfig, + ColorField, ColumnConfig, ColumnFields, ColumnSegmentField, @@ -16,6 +17,7 @@ import { ComboLineDualFields, ComboLineSingleConfig, ComboLineSingleFields, + CustomPaletteType, GenericChartConfig, InteractiveFiltersCalculation, InteractiveFiltersConfig, @@ -47,6 +49,7 @@ export type FieldAdjuster< dimensions: Dimension[]; measures: Measure[]; isAddingNewCube?: boolean; + palette: CustomPaletteType | string; }) => NewChartConfigType; type AssureKeys = { @@ -92,6 +95,7 @@ type ColumnAdjusters = BaseAdjusters & { | PieSegmentField | TableFields >; + color: FieldAdjuster; animation: FieldAdjuster; }; }; @@ -109,6 +113,7 @@ type BarAdjusters = BaseAdjusters & { | PieSegmentField | TableFields >; + color: FieldAdjuster; animation: FieldAdjuster; }; }; @@ -117,6 +122,7 @@ type LineAdjusters = BaseAdjusters & { fields: { x: { componentId: FieldAdjuster }; y: { componentId: FieldAdjuster }; + color: FieldAdjuster; segment: FieldAdjuster< LineConfig, | ColumnSegmentField @@ -133,6 +139,7 @@ type AreaAdjusters = BaseAdjusters & { fields: { x: { componentId: FieldAdjuster }; y: { componentId: FieldAdjuster }; + color: FieldAdjuster; segment: FieldAdjuster< AreaConfig, | ColumnSegmentField @@ -157,6 +164,7 @@ type ScatterPlotAdjusters = BaseAdjusters & { | PieSegmentField | TableFields >; + color: FieldAdjuster; animation: FieldAdjuster; }; }; @@ -173,6 +181,7 @@ type PieAdjusters = BaseAdjusters & { | ScatterPlotSegmentField | TableFields >; + color: FieldAdjuster; animation: FieldAdjuster; }; }; @@ -206,6 +215,7 @@ type ComboLineSingleAdjusters = BaseAdjusters & { fields: { x: { componentId: FieldAdjuster }; y: { componentIds: FieldAdjuster }; + color: FieldAdjuster; }; }; @@ -225,6 +235,7 @@ type ComboLineDualAdjusters = BaseAdjusters & { | ComboLineSingleFields | ComboLineColumnFields >; + color: FieldAdjuster; }; }; @@ -244,6 +255,7 @@ type ComboLineColumnAdjusters = BaseAdjusters & { | ComboLineSingleFields | ComboLineDualFields >; + color: FieldAdjuster; }; }; diff --git a/app/config-types.ts b/app/config-types.ts index e5f7c65e7..679fa98cf 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -1071,10 +1071,17 @@ export const isColorInConfig = ( | ColumnConfig | LineConfig | PieConfig - | ScatterPlotConfig => { + | ScatterPlotConfig + | BarConfig => { return !isTableConfig(chartConfig) && !isMapConfig(chartConfig); }; +export const isCustomColorPalette = ( + palette: CustomPaletteType | string +): palette is CustomPaletteType => { + return typeof palette !== "string"; +}; + export const isNotTableOrMap = (chartConfig: ChartConfig) => { return !isTableConfig(chartConfig) && !isMapConfig(chartConfig); }; diff --git a/app/configurator/components/add-dataset-dialog.tsx b/app/configurator/components/add-dataset-dialog.tsx index 637dbd7f9..5fec2089e 100644 --- a/app/configurator/components/add-dataset-dialog.tsx +++ b/app/configurator/components/add-dataset-dialog.tsx @@ -62,7 +62,11 @@ import { getEnabledChartTypes } from "@/charts"; import Flex from "@/components/flex"; import { Error as ErrorHint, Loading } from "@/components/hint"; import Tag from "@/components/tag"; -import { ConfiguratorStateConfiguringChart, DataSource } from "@/config-types"; +import { + ConfiguratorStateConfiguringChart, + DataSource, + isColorInConfig, +} from "@/config-types"; import { getChartConfig } from "@/config-utils"; import { addDatasetInConfig, @@ -100,9 +104,11 @@ import SvgIcInfo from "@/icons/components/IcInfo"; import SvgIcRemove from "@/icons/components/IcRemove"; import SvgIcSearch from "@/icons/components/IcSearch"; import { useLocale } from "@/locales/use-locale"; +import { DEFAULT_CATEGORICAL_PALETTE_ID } from "@/palettes"; import { useEventEmitter } from "@/utils/eventEmitter"; import useEvent from "@/utils/use-event"; import useLocalState from "@/utils/use-local-state"; +import { useUserPalettes } from "@/utils/use-user-palettes"; const DialogCloseButton = (props: IconButtonProps) => { return ( @@ -1086,6 +1092,7 @@ const useAddDataset = () => { const { type: sourceType, url: sourceUrl } = state.dataSource; const locale = useLocale(); const client = useClient(); + const { data: palettes } = useUserPalettes(); const addDataset = useEventCallback( async ({ otherCube, @@ -1107,6 +1114,13 @@ const useAddDataset = () => { addDatasetInConfig(nextState, addDatasetOptions); const chartConfig = getChartConfig(nextState, state.activeChartKey); + const palette = isColorInConfig(chartConfig) + ? (palettes?.find( + (palette) => + palette.paletteId === chartConfig.fields.color.paletteId + ) ?? chartConfig.fields.color.paletteId) + : DEFAULT_CATEGORICAL_PALETTE_ID; + const res = await executeDataCubesComponentsQuery(client, { locale, sourceType, @@ -1140,6 +1154,7 @@ const useAddDataset = () => { chartKey: state.activeChartKey, chartType: enabledChartTypes[0], isAddingNewCube: true, + palette, }, }); } finally { diff --git a/app/configurator/components/chart-options-selector.tsx b/app/configurator/components/chart-options-selector.tsx index 9dcf7e6aa..67a287d60 100644 --- a/app/configurator/components/chart-options-selector.tsx +++ b/app/configurator/components/chart-options-selector.tsx @@ -1,8 +1,8 @@ import { t, Trans } from "@lingui/macro"; import { Box, - Stack, Switch as MUISwitch, + Stack, Tooltip, Typography, } from "@mui/material"; @@ -55,6 +55,7 @@ import { ImputationType, imputationTypes, isAnimationInConfig, + isBarConfig, isColorInConfig, isComboChartConfig, isTableConfig, @@ -726,10 +727,23 @@ const ChartLayoutOptions = ({ hasSubOptions: boolean; measures: Measure[]; }) => { + const activeField = chartConfig.activeField as EncodingFieldType | undefined; + + if (!activeField) { + return null; + } + const hasColorField = isColorInConfig(chartConfig); const values: { id: string; symbol: LegendSymbol }[] = hasColorField ? chartConfig.fields.color.type === "single" - ? [{ id: chartConfig.fields.y.componentId, symbol: "line" }] + ? [ + { + id: isBarConfig(chartConfig) + ? chartConfig.fields.x.componentId + : chartConfig.fields.y.componentId, + symbol: "line", + }, + ] : Object.keys(chartConfig.fields.color.colorMapping).map((key) => ({ id: key, symbol: "line", @@ -752,7 +766,7 @@ const ChartLayoutOptions = ({ )} <> diff --git a/app/configurator/config-form.tsx b/app/configurator/config-form.tsx index d321708c0..fe8640710 100644 --- a/app/configurator/config-form.tsx +++ b/app/configurator/config-form.tsx @@ -39,10 +39,14 @@ import { Measure, } from "@/domain/data"; import { useLocale } from "@/locales/use-locale"; -import { categoricalPalettes } from "@/palettes"; +import { + categoricalPalettes, + DEFAULT_CATEGORICAL_PALETTE_ID, +} from "@/palettes"; import { bfs } from "@/utils/bfs"; import { isMultiHierarchyNode } from "@/utils/hierarchy"; import useEvent from "@/utils/use-event"; +import { useUserPalettes } from "@/utils/use-user-palettes"; import { mapValueIrisToColor } from "./components/ui-helpers"; @@ -102,6 +106,7 @@ export const useChartFieldField = ({ }): SelectProps => { const [state, dispatch] = useConfiguratorState(); const locale = useLocale(); + const { data: palettes } = useUserPalettes(); const chartConfig = getChartConfig(state); const handleChange = useEvent(async (e: SelectChangeEvent) => { if (e.target.value !== FIELD_VALUE_NONE) { @@ -133,26 +138,33 @@ export const useChartFieldField = ({ }); if (isColorInConfig(chartConfig)) { - const palette = - categoricalPalettes.find( - (p) => p.value === chartConfig.fields.color.paletteId - ) ?? categoricalPalettes[0]; + const paletteId = chartConfig.fields.color.paletteId; + const customColorPalette = palettes?.find( + (palette) => palette.paletteId === paletteId + ); + const defaultColorPalette = categoricalPalettes.find( + (palette) => palette.value === paletteId + ); dispatch({ type: "COLOR_FIELD_SET", value: - chartConfig.fields.color.type === "single" + field === "segment" ? { - type: chartConfig.fields.color.type, - paletteId: palette.value, - color: palette.colors[0], - } - : { - type: chartConfig.fields.color.type, - paletteId: palette.value, + type: "segment", + paletteId, colorMapping: mapValueIrisToColor({ - paletteId: palette.value, + paletteId, dimensionValues: dimension.values, + customPalette: customColorPalette, }), + } + : { + type: "single", + paletteId, + color: + customColorPalette?.colors[0] ?? + defaultColorPalette?.colors[0] ?? + categoricalPalettes[0].colors[0], }, }); } else { @@ -458,6 +470,15 @@ export const useAddOrEditChartType = ( const locale = useLocale(); const [state, dispatch] = useConfiguratorState(); const chartConfig = getChartConfig(state, chartKey); + + const { data: palettes } = useUserPalettes(); + + const palette = isColorInConfig(chartConfig) + ? (palettes?.find( + (palette) => palette.paletteId === chartConfig.fields.color.paletteId + ) ?? chartConfig.fields.color.paletteId) + : DEFAULT_CATEGORICAL_PALETTE_ID; + const addOrEditChartType = useEvent((chartType: ChartType) => { if (type === "edit") { dispatch({ @@ -466,6 +487,7 @@ export const useAddOrEditChartType = ( locale, chartKey, chartType, + palette, }, }); } else { diff --git a/app/configurator/configurator-state/actions.tsx b/app/configurator/configurator-state/actions.tsx index 291755d88..f8d6fda2d 100644 --- a/app/configurator/configurator-state/actions.tsx +++ b/app/configurator/configurator-state/actions.tsx @@ -5,6 +5,7 @@ import { ColorField, ColorMapping, ConfiguratorState, + CustomPaletteType, DashboardFiltersConfig, DataSource, Filters, @@ -59,6 +60,7 @@ export type ConfiguratorStateAction = locale: Locale; chartKey: string; chartType: ChartType; + palette: CustomPaletteType | string; isAddingNewCube?: boolean; }; } diff --git a/app/configurator/configurator-state/reducer.spec.tsx b/app/configurator/configurator-state/reducer.spec.tsx index a63ac33f0..431f6a88a 100644 --- a/app/configurator/configurator-state/reducer.spec.tsx +++ b/app/configurator/configurator-state/reducer.spec.tsx @@ -43,6 +43,7 @@ import { getCachedComponents as getCachedComponentsOriginal } from "@/urql-cache import { getCachedComponentsMock } from "@/urql-cache.mock"; import { assert } from "@/utils/assert"; import { migrateChartConfig } from "@/utils/chart-config/versioning"; +import { DEFAULT_CATEGORICAL_PALETTE_ID } from "@/palettes"; const pristineConfigStateMock = JSON.parse(JSON.stringify(configStateMock)); @@ -1251,6 +1252,7 @@ describe("retainChartConfigWhenSwitchingChartType", () => { newChartType, dimensions, measures, + palette: DEFAULT_CATEGORICAL_PALETTE_ID, }) ); deriveFiltersFromFields(newConfig, { diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index 217545544..8d811db76 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -656,7 +656,8 @@ const reducer_: Reducer = ( case "CHART_TYPE_CHANGED": if (isConfiguring(draft)) { - const { locale, chartKey, chartType, isAddingNewCube } = action.value; + const { locale, chartKey, chartType, isAddingNewCube, palette } = + action.value; const chartConfig = getChartConfig(draft, chartKey); const dataCubesComponents = getCachedComponents({ locale, @@ -677,6 +678,7 @@ const reducer_: Reducer = ( dimensions, measures, isAddingNewCube, + palette, }), { dimensions } );