From 6f5f99f2258b576a6efaba625046a9de3112bd7e Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:34:32 +0100 Subject: [PATCH 01/21] refactor: Refactored chart config types --- app/config-types.ts | 59 ++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/app/config-types.ts b/app/config-types.ts index 7822e1af2..ad0e82078 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -211,6 +211,35 @@ export type SortingType = t.TypeOf; 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 NewColorField = t.TypeOf; + const GenericField = t.intersection([ t.type({ componentId: t.string }), t.partial({ useAbbreviations: t.boolean }), @@ -220,15 +249,7 @@ 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, - }), -]); +const GenericSegmentField = GenericField; export type GenericSegmentField = t.TypeOf; const AnimationType = t.union([t.literal("continuous"), t.literal("stepped")]); @@ -297,6 +318,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, @@ -324,6 +346,7 @@ const LineFields = t.intersection([ t.type({ x: GenericField, y: t.intersection([GenericField, UncertaintyFieldExtension]), + color: t.union([SegmentColorField, SingleColorField]), }), t.partial({ segment: LineSegmentField, @@ -361,6 +384,7 @@ const AreaFields = t.intersection([ GenericField, t.partial({ imputationType: ImputationType }), ]), + color: t.union([SegmentColorField, SingleColorField]), }), t.partial({ segment: AreaSegmentField, @@ -387,6 +411,7 @@ const ScatterPlotFields = t.intersection([ t.type({ x: GenericField, y: GenericField, + color: t.union([SegmentColorField, SingleColorField]), }), t.partial({ segment: ScatterPlotSegmentField, @@ -414,6 +439,7 @@ const PieFields = t.intersection([ t.type({ y: GenericField, segment: PieSegmentField, + color: SegmentColorField, }), t.partial({ animation: AnimationField }), ]); @@ -582,7 +608,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 }), @@ -595,7 +621,7 @@ const NumericalColorField = t.intersection([ t.type({ type: t.literal("numerical"), componentId: t.string, - palette: t.union([DivergingPaletteType, SequentialPaletteType]), + paletteId: t.union([DivergingPaletteType, SequentialPaletteType]), }), t.union([ t.type({ @@ -623,6 +649,7 @@ export type ColorField = const MapAreaLayer = t.type({ componentId: t.string, + //FIXME: convert to new color field type color: t.union([CategoricalColorField, NumericalColorField]), }); export type MapAreaLayer = t.TypeOf; @@ -631,6 +658,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; @@ -667,9 +695,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; @@ -691,9 +718,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; @@ -716,9 +742,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; From 4465745feae21eb767555112bdfc1bb59ef404de Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:36:24 +0100 Subject: [PATCH 02/21] refactor: fixed all erros that occured due to chart config refactor --- app/charts/area/areas-state.tsx | 12 +- app/charts/chart-config-ui-options.ts | 59 +++--- app/charts/column/columns-grouped-state.tsx | 10 +- app/charts/column/columns-stacked-state.tsx | 14 +- .../combo/combo-line-column-state-props.ts | 4 +- .../combo/combo-line-dual-state-props.ts | 4 +- .../combo/combo-line-single-state-props.ts | 2 +- app/charts/index.spec.ts | 5 + app/charts/index.ts | 195 ++++++++++++++---- app/charts/line/lines-state.tsx | 9 +- app/charts/pie/pie-state.tsx | 10 +- app/charts/scatterplot/scatterplot-state.tsx | 9 +- app/components/chart-footnotes.tsx | 10 +- .../components/add-dataset-dialog.mock.ts | 5 + .../chart-controls/color-palette.tsx | 5 +- .../configurator-state/actions.tsx | 8 +- app/configurator/configurator-state/mocks.ts | 43 ++-- .../configurator-state/reducer.spec.tsx | 8 +- .../configurator-state/reducer.tsx | 18 +- 19 files changed, 285 insertions(+), 145 deletions(-) diff --git a/app/charts/area/areas-state.tsx b/app/charts/area/areas-state.tsx index 13b7d2154..9d429103c 100644 --- a/app/charts/area/areas-state.tsx +++ b/app/charts/area/areas-state.tsx @@ -248,7 +248,7 @@ const useAreasState = ( const xScaleTimeRange = scaleTime().domain(xScaleTimeRangeDomain); const colors = scaleOrdinal(); - if (segmentDimension && fields.segment?.colorMapping) { + if (segmentDimension && fields.color) { const orderedSegmentLabelsAndColors = allSegments.map((segment) => { const dvIri = segmentsByAbbreviationOrLabel.get(segment)?.value ?? @@ -257,7 +257,10 @@ const useAreasState = ( return { label: segment, - color: fields.segment?.colorMapping![dvIri] ?? schemeCategory10[0], + color: + fields.color.type === "segment" + ? fields.color.colorMapping![dvIri] ?? schemeCategory10[0] + : schemeCategory10[0], }; }); @@ -265,7 +268,7 @@ const useAreasState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range(getPalette(fields.color.paletteId)); } colors.unknown(() => undefined); @@ -276,8 +279,7 @@ const useAreasState = ( xScaleTimeRange, }; }, [ - fields.segment?.palette, - fields.segment?.colorMapping, + fields.color, getX, scalesData, timeRangeData, diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts index 3ae078c9a..e35b07750 100644 --- a/app/charts/chart-config-ui-options.ts +++ b/app/charts/chart-config-ui-options.ts @@ -485,8 +485,8 @@ export const defaultSegmentOnChange: OnEncodingChange< const component = components.find((d) => d.id === id); const palette = getDefaultCategoricalPaletteName( component, - chartConfig.fields.segment && "palette" in chartConfig.fields.segment - ? chartConfig.fields.segment.palette + chartConfig.fields.color && "palette" in chartConfig.fields.color + ? chartConfig.fields.color.paletteId : undefined ); const colorMapping = mapValueIrisToColor({ @@ -496,12 +496,14 @@ export const defaultSegmentOnChange: OnEncodingChange< if (chartConfig.fields.segment && "palette" in 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: palette, colorMapping, }; } @@ -955,12 +957,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(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; }, }, }, @@ -990,11 +992,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], }; }, }, @@ -1002,10 +1004,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], }; }, }, @@ -1036,11 +1039,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 }; @@ -1050,11 +1053,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 }; @@ -1064,7 +1067,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 @@ -1074,9 +1077,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 7c837ecc4..279e912d1 100644 --- a/app/charts/column/columns-grouped-state.tsx +++ b/app/charts/column/columns-grouped-state.tsx @@ -189,7 +189,7 @@ const useColumnsGroupedState = ( } = 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 || @@ -198,7 +198,10 @@ const useColumnsGroupedState = ( return { label: segment, - color: fields.segment?.colorMapping![dvIri] ?? schemeCategory10[0], + color: + fields.color.type === "segment" + ? fields.color.colorMapping![dvIri] ?? schemeCategory10[0] + : schemeCategory10[0], }; }); @@ -206,7 +209,7 @@ const useColumnsGroupedState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range(getPalette(fields.color.paletteId)); } colors.unknown(() => undefined); @@ -278,6 +281,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 99cf9e869..9661bacde 100644 --- a/app/charts/column/columns-stacked-state.tsx +++ b/app/charts/column/columns-stacked-state.tsx @@ -224,11 +224,7 @@ const useColumnsStackedState = ( } = 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 @@ -244,7 +240,10 @@ const useColumnsStackedState = ( return { label: segment, - color: fields.segment?.colorMapping![dvIri] ?? schemeCategory10[0], + color: + fields.color.type === "segment" + ? fields.color.colorMapping![dvIri] ?? schemeCategory10[0] + : schemeCategory10[0], }; }); @@ -252,7 +251,7 @@ const useColumnsStackedState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range(getPalette(fields.color.paletteId)); } colors.unknown(() => undefined); @@ -295,6 +294,7 @@ const useColumnsStackedState = ( }; }, [ fields.segment, + fields.color, fields.x.sorting, fields.x.useAbbreviations, xDimension, diff --git a/app/charts/combo/combo-line-column-state-props.ts b/app/charts/combo/combo-line-column-state-props.ts index d4e53abc9..bb9b37757 100644 --- a/app/charts/combo/combo-line-column-state-props.ts +++ b/app/charts/combo/combo-line-column-state-props.ts @@ -77,7 +77,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 = @@ -94,7 +94,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 0394e5a39..f9ba2ae7e 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 86a3b2e14..731ca89e4 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 cc422c430..fa4355c1a 100644 --- a/app/charts/index.spec.ts +++ b/app/charts/index.spec.ts @@ -275,6 +275,11 @@ describe("chart type switch", () => { y: { componentId: "https://environment.ld.admin.ch/foen/ubd0104/value", }, + color: { + type: "segment", + paletteId: "category10", + colorMapping: {}, + }, }, interactiveFiltersConfig: { legend: { diff --git a/app/charts/index.ts b/app/charts/index.ts index 6827c25a3..5f5d8f0c6 100644 --- a/app/charts/index.ts +++ b/app/charts/index.ts @@ -51,6 +51,7 @@ import { MapConfig, MapSymbolLayer, Meta, + NewColorField, PieSegmentField, RegularChartType, ScatterPlotSegmentField, @@ -398,6 +399,14 @@ export const getInitialConfig = ( fields: { x: { componentId: areaXComponentId }, y: { componentId: numericalMeasures[0].id, imputationType: "none" }, + color: { + type: "segment", + paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + colorMapping: mapValueIrisToColor({ + palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + dimensionValues: dimensions[0].values, + }), + }, }, }; case "column": @@ -422,6 +431,11 @@ export const getInitialConfig = ( sorting: DEFAULT_SORTING, }, y: { componentId: numericalMeasures[0].id }, + color: { + type: "single", + paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + color: dimensions[0].values[0].color || "#000", + }, }, }; case "line": @@ -436,6 +450,11 @@ export const getInitialConfig = ( fields: { x: { componentId: lineXComponentId }, y: { componentId: numericalMeasures[0].id }, + color: { + type: "single", + paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + color: dimensions[0].values[0].color || "#000", + }, }, }; case "map": @@ -487,8 +506,11 @@ 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, dimensionValues: pieSegmentComponent.values, @@ -518,16 +540,25 @@ export const getInitialConfig = ( }, ...(scatterplotSegmentComponent ? { - segment: { - componentId: scatterplotSegmentComponent.id, - palette: scatterplotPalette, + color: { + type: "segment", + paletteId: scatterplotPalette, colorMapping: mapValueIrisToColor({ palette: scatterplotPalette, dimensionValues: scatterplotSegmentComponent.values, }), }, + segment: { + componentId: scatterplotSegmentComponent.id, + }, } - : {}), + : { + color: { + type: "single", + paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + color: dimensions[0].values[0].color || "#000", + }, + }), }, }; case "table": @@ -585,7 +616,10 @@ 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_NAME, colorMapping: mapValueIrisToColor({ palette: DEFAULT_CATEGORICAL_PALETTE_NAME, dimensionValues: yComponentIds.map((id) => ({ @@ -620,7 +654,10 @@ export const getInitialConfig = ( y: { leftAxisComponentId, rightAxisComponentId, - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + }, + color: { + type: "measures", + paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, colorMapping: mapValueIrisToColor({ palette: DEFAULT_CATEGORICAL_PALETTE_NAME, dimensionValues: [leftAxisComponentId, rightAxisComponentId].map( @@ -658,7 +695,10 @@ export const getInitialConfig = ( lineComponentId, lineAxisOrientation: "right", columnComponentId, - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + }, + color: { + type: "measures", + paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, colorMapping: mapValueIrisToColor({ palette: DEFAULT_CATEGORICAL_PALETTE_NAME, dimensionValues: [lineComponentId, columnComponentId].map( @@ -933,6 +973,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: ColumnSegmentField | undefined; + let newColor: NewColorField | undefined; + const yMeasure = measures.find( (d) => d.id === newChartConfig.fields.y.componentId ); @@ -947,10 +989,11 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { if (tableSegment) { newSegment = { - ...tableSegment, + ...tableSegment.segment, sorting: DEFAULT_SORTING, type: disableStacked(yMeasure) ? "grouped" : "stacked", }; + newColor = tableSegment.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 @@ -970,12 +1013,25 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }), type: disableStacked(yMeasure) ? "grouped" : "stacked", }; + newColor = { + type: "segment", + paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + colorMapping: mapValueIrisToColor({ + palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + dimensionValues: + dimensions.find((d) => d.id === oldValue.componentId)?.values || + [], + }), + }; } return produce(newChartConfig, (draft) => { if (newSegment) { draft.fields.segment = newSegment; } + if (newColor?.type === "segment") { + draft.fields.color = newColor; + } }); }, animation: ({ oldValue, newChartConfig }) => { @@ -1027,6 +1083,7 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: LineSegmentField | undefined; + let newColor: NewColorField | undefined; if (oldChartConfig.chartType === "table") { const tableSegment = convertTableFieldsToSegmentField({ @@ -1036,7 +1093,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }); if (tableSegment) { - newSegment = tableSegment; + newSegment = tableSegment.segment; + newColor = tableSegment.color; } } else { const oldSegment = oldValue as Exclude; @@ -1047,8 +1105,6 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { if (!isTemporalDimension(segmentDimension)) { newSegment = { componentId: oldSegment.componentId, - palette: oldSegment.palette, - colorMapping: oldSegment.colorMapping, sorting: "sorting" in oldSegment && oldSegment.sorting && @@ -1056,6 +1112,14 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { ? oldSegment.sorting ?? DEFAULT_FIXED_COLOR_FIELD : DEFAULT_SORTING, }; + newColor = { + type: "segment", + paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + colorMapping: mapValueIrisToColor({ + palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + dimensionValues: segmentDimension?.values || [], + }), + }; } } @@ -1063,6 +1127,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { if (newSegment) { draft.fields.segment = newSegment; } + if (newColor?.type === "segment") { + draft.fields.color = newColor; + } }); }, }, @@ -1115,6 +1182,7 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { } let newSegment: AreaSegmentField | undefined; + let newColor: NewColorField | undefined; if (oldChartConfig.chartType === "table") { const tableSegment = convertTableFieldsToSegmentField({ @@ -1125,9 +1193,10 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { if (tableSegment) { newSegment = { - ...tableSegment, + ...tableSegment.segment, sorting: DEFAULT_SORTING, }; + newColor = tableSegment.color; } } else { const oldSegment = oldValue as Exclude; @@ -1138,14 +1207,20 @@ 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_NAME, + colorMapping: mapValueIrisToColor({ + palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + dimensionValues: segmentDimension?.values || [], + }), + }; } } @@ -1153,6 +1228,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { if (newSegment) { draft.fields.segment = newSegment; } + if (newColor?.type === "segment") { + draft.fields.color = newColor; + } }); }, }, @@ -1190,6 +1268,7 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: ScatterPlotSegmentField | undefined; + let newColor: NewColorField | undefined; if (oldChartConfig.chartType === "table") { const tableSegment = convertTableFieldsToSegmentField({ @@ -1199,14 +1278,13 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }); if (tableSegment) { - newSegment = tableSegment; + newSegment = tableSegment.segment; + newColor = tableSegment.color; } } else { const oldSegment = oldValue as Exclude; newSegment = { componentId: oldSegment.componentId, - palette: oldSegment.palette, - colorMapping: oldSegment.colorMapping, }; } @@ -1214,6 +1292,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { if (newSegment) { draft.fields.segment = newSegment; } + if (newColor?.type === "segment") { + draft.fields.color = newColor; + } }); }, animation: ({ oldValue, newChartConfig }) => { @@ -1246,6 +1327,7 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: PieSegmentField | undefined; + let newColor: NewColorField | undefined; if (oldChartConfig.chartType === "table") { const tableSegment = convertTableFieldsToSegmentField({ @@ -1256,28 +1338,40 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { if (tableSegment) { newSegment = { - ...tableSegment, + ...tableSegment.segment, sorting: DEFAULT_SORTING, }; + newColor = tableSegment.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_NAME, + colorMapping: mapValueIrisToColor({ + palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + dimensionValues: + dimensions.find((d) => d.id === oldSegment.componentId) + ?.values || [], + }), + }; } return produce(newChartConfig, (draft) => { if (newSegment) { draft.fields.segment = newSegment; } + if (newColor?.type === "segment") { + draft.fields.color = newColor; + } }); }, animation: ({ oldValue, newChartConfig }) => { @@ -1405,16 +1499,19 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { .filter((d) => d.unit === unit) .map((d) => d.id); const palette = isSegmentInConfig(oldChartConfig) - ? oldChartConfig.fields.segment?.palette ?? + ? oldChartConfig.fields.color.paletteId ?? DEFAULT_CATEGORICAL_PALETTE_NAME : isComboChartConfig(oldChartConfig) - ? oldChartConfig.fields.y.palette + ? oldChartConfig.fields.color.paletteId : DEFAULT_CATEGORICAL_PALETTE_NAME; return produce(newChartConfig, (draft) => { draft.fields.y = { componentIds, - palette, + }; + draft.fields.color = { + type: "measures", + paletteId: palette, colorMapping: mapValueIrisToColor({ palette, dimensionValues: componentIds.map((id) => ({ @@ -1518,25 +1615,26 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { leftMeasure = getLeftMeasure(leftMeasure.id); const palette = isSegmentInConfig(oldChartConfig) - ? oldChartConfig.fields.segment?.palette ?? + ? oldChartConfig.fields.color.paletteId ?? DEFAULT_CATEGORICAL_PALETTE_NAME : isComboChartConfig(oldChartConfig) - ? oldChartConfig.fields.y.palette + ? oldChartConfig.fields.color.paletteId : DEFAULT_CATEGORICAL_PALETTE_NAME; return produce(newChartConfig, (draft) => { draft.fields.y = { leftAxisComponentId: leftMeasure.id, rightAxisComponentId: rightMeasureId as string, - palette, + }; + draft.fields.color = { + type: "measures", + paletteId: palette, colorMapping: mapValueIrisToColor({ palette, - dimensionValues: [leftMeasure.id, rightMeasureId as string].map( - (id) => ({ - value: id, - label: id, - }) - ), + dimensionValues: [leftMeasure.id, rightMeasureId].map((id) => ({ + value: id, + label: id, + })), }), }; }); @@ -1610,10 +1708,10 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { ).id; const palette = isSegmentInConfig(oldChartConfig) - ? oldChartConfig.fields.segment?.palette ?? + ? oldChartConfig.fields.color.paletteId ?? DEFAULT_CATEGORICAL_PALETTE_NAME : isComboChartConfig(oldChartConfig) - ? oldChartConfig.fields.y.palette + ? oldChartConfig.fields.color.paletteId : DEFAULT_CATEGORICAL_PALETTE_NAME; return produce(newChartConfig, (draft) => { @@ -1621,7 +1719,10 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { columnComponentId: leftMeasure.id, lineComponentId, lineAxisOrientation: "right", - palette, + }; + draft.fields.color = { + type: "measures", + paletteId: palette, colorMapping: mapValueIrisToColor({ palette, dimensionValues: [leftMeasure.id, lineComponentId].map((id) => ({ @@ -2181,7 +2282,7 @@ const convertTableFieldsToSegmentField = ({ fields: TableFields; dimensions: Dimension[]; measures: Measure[]; -}): GenericSegmentField | undefined => { +}): { segment: GenericSegmentField; color: NewColorField } | undefined => { const groupedColumns = group(Object.values(fields), (d) => d.isGroup) .get(true) ?.filter((d) => SEGMENT_ENABLED_COMPONENTS.includes(d.componentType)) @@ -2199,12 +2300,20 @@ const convertTableFieldsToSegmentField = ({ const palette = getDefaultCategoricalPaletteName(actualComponent); return { - componentId, - palette, - colorMapping: mapValueIrisToColor({ - palette, - dimensionValues: actualComponent.values, - }), + segment: { + componentId, + }, + color: { + type: "segment", + paletteId: palette, + colorMapping: mapValueIrisToColor({ + palette: palette, + 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 24f219675..1fed350e7 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,7 @@ const useLinesState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range(getPalette(fields.color.paletteId)); } // Dimensions diff --git a/app/charts/pie/pie-state.tsx b/app/charts/pie/pie-state.tsx index 892ae3dfe..eebbbeb0c 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,7 @@ const usePieState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range(getPalette(fields.color.paletteId)); } // Do not let the scale be implicitly extended by children calling it // on unknown values @@ -133,6 +136,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 6459b6b74..d3deae1cc 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,7 @@ const useScatterplotState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.segment?.palette)); + colors.range(getPalette(fields.color.paletteId)); } // Dimensions const { left, bottom } = useChartPadding({ diff --git a/app/components/chart-footnotes.tsx b/app/components/chart-footnotes.tsx index e3af0fce4..3abe611ae 100644 --- a/app/components/chart-footnotes.tsx +++ b/app/components/chart-footnotes.tsx @@ -197,7 +197,7 @@ const ChartFootnotesComboLineColumn = ({ {firstComponent && ( ; - }; + value: NewColorField; } | { type: "CHART_PALETTE_RESET"; diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index 589c9f9b4..f8e20d4cc 100644 --- a/app/configurator/configurator-state/mocks.ts +++ b/app/configurator/configurator-state/mocks.ts @@ -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 16abe91bc..3d6e3ad9f 100644 --- a/app/configurator/configurator-state/reducer.spec.tsx +++ b/app/configurator/configurator-state/reducer.spec.tsx @@ -700,8 +700,12 @@ describe("colorMapping", () => { chartConfig.fields.segment?.componentId === "mapDataset___newAreaLayerColorIri" ); - expect(chartConfig.fields.segment?.palette === "dimension"); - expect(chartConfig.fields.segment?.colorMapping).toEqual({ + expect(chartConfig.fields.color.paletteId === "dimension"); + expect( + chartConfig.fields.color.type === "single" + ? chartConfig.fields.color.color + : chartConfig.fields.color.colorMapping + ).toEqual({ orange: "rgb(255, 153, 0)", }); }); diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index a3d43629c..f84a4978b 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -657,22 +657,16 @@ const reducer_: Reducer = ( const chartConfig = getChartConfig(draft); setWith( chartConfig, - `fields["${action.value.field}"].${ - action.value.colorConfigPath - ? `${action.value.colorConfigPath}.` - : "" - }palette`, - action.value.palette, + `fields.color.paletteId`, + action.value.paletteId, Object ); setWith( chartConfig, - `fields["${action.value.field}"].${ - action.value.colorConfigPath - ? `${action.value.colorConfigPath}.` - : "" - }colorMapping`, - action.value.colorMapping, + `fields.color.${action.value.type === "single" ? "color" : "colorMapping"}`, + action.value.type === "single" + ? action.value.color + : action.value.colorMapping, Object ); } From 591492b2f26a8eaa193afb4dfa27e51db8f67782 Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:43:44 +0100 Subject: [PATCH 03/21] fix: Fixed silent error --- app/charts/chart-config-ui-options.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts index e35b07750..d5aa0f456 100644 --- a/app/charts/chart-config-ui-options.ts +++ b/app/charts/chart-config-ui-options.ts @@ -485,7 +485,7 @@ export const defaultSegmentOnChange: OnEncodingChange< const component = components.find((d) => d.id === id); const palette = getDefaultCategoricalPaletteName( component, - chartConfig.fields.color && "palette" in chartConfig.fields.color + chartConfig.fields.color && "paletteId" in chartConfig.fields.color ? chartConfig.fields.color.paletteId : undefined ); @@ -494,7 +494,7 @@ export const defaultSegmentOnChange: OnEncodingChange< dimensionValues: component ? component.values : selectedValues, }); - if (chartConfig.fields.segment && "palette" in chartConfig.fields.segment) { + if (chartConfig.fields.segment) { chartConfig.fields.segment.componentId = id; } else { chartConfig.fields.segment = { From c0938dfba15c74a8b571e9b622598e14ba3ed9a6 Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Wed, 4 Dec 2024 06:22:12 +0100 Subject: [PATCH 04/21] fix: Fixed all of bartosz's suggestions --- app/charts/area/areas-state.tsx | 12 +- app/charts/chart-config-ui-options.ts | 14 +- app/charts/column/columns-grouped-state.tsx | 12 +- app/charts/column/columns-stacked-state.tsx | 16 +- app/charts/index.ts | 101 +++-- app/charts/line/lines-state.tsx | 4 +- app/charts/pie/pie-state.tsx | 4 +- app/charts/scatterplot/scatterplot-state.tsx | 4 +- app/charts/shared/legend-color.tsx | 4 +- app/config-types.ts | 30 +- .../chart-controls/color-palette.tsx | 103 +++-- .../chart-controls/color-picker.tsx | 33 +- .../components/chart-options-selector.tsx | 2 +- app/configurator/components/color-picker.css | 28 ++ app/configurator/components/color-picker.tsx | 174 +++++++++ app/configurator/components/field.tsx | 6 +- app/configurator/components/filters.tsx | 4 +- app/configurator/components/ui-helpers.ts | 4 +- .../configurator-state/actions.tsx | 13 +- app/configurator/configurator-state/index.tsx | 2 +- .../configurator-state/reducer.spec.tsx | 8 +- .../configurator-state/reducer.tsx | 28 +- .../table/table-chart-options.tsx | 4 +- app/docs/controls.docs.mdx | 26 +- app/docs/controls.stories.tsx | 4 +- app/package.json | 1 + app/pages/_app.tsx | 6 +- app/palettes.ts | 22 +- yarn.lock | 353 +++++++++++++++--- 29 files changed, 790 insertions(+), 232 deletions(-) create mode 100644 app/configurator/components/color-picker.css create mode 100644 app/configurator/components/color-picker.tsx diff --git a/app/charts/area/areas-state.tsx b/app/charts/area/areas-state.tsx index 9d429103c..70eb281cf 100644 --- a/app/charts/area/areas-state.tsx +++ b/app/charts/area/areas-state.tsx @@ -54,7 +54,7 @@ import { useSize } from "@/charts/shared/use-size"; import { AreaConfig } from "@/configurator"; import { Observation } from "@/domain/data"; import { useFormatNumber, useTimeFormatUnit } from "@/formatters"; -import { getPalette } from "@/palettes"; +import { getPaletteId } from "@/palettes"; import { useChartInteractiveFilters } from "@/stores/interactive-filters"; import { sortByIndex } from "@/utils/array"; import { @@ -248,7 +248,8 @@ const useAreasState = ( const xScaleTimeRange = scaleTime().domain(xScaleTimeRangeDomain); const colors = scaleOrdinal(); - if (segmentDimension && fields.color) { + if (segmentDimension && fields.color.type === "segment") { + const segmentColor = fields.color; const orderedSegmentLabelsAndColors = allSegments.map((segment) => { const dvIri = segmentsByAbbreviationOrLabel.get(segment)?.value ?? @@ -257,10 +258,7 @@ const useAreasState = ( return { label: segment, - color: - fields.color.type === "segment" - ? fields.color.colorMapping![dvIri] ?? schemeCategory10[0] - : schemeCategory10[0], + color: segmentColor.colorMapping[dvIri] ?? schemeCategory10[0], }; }); @@ -268,7 +266,7 @@ const useAreasState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.color.paletteId)); + colors.range(getPaletteId(fields.color.paletteId)); } colors.unknown(() => undefined); diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts index d5aa0f456..f76060c9b 100644 --- a/app/charts/chart-config-ui-options.ts +++ b/app/charts/chart-config-ui-options.ts @@ -19,7 +19,6 @@ import { ChartConfig, ChartSubType, ChartType, - ColorField, ColorScaleType, ColumnConfig, ColumnSegmentField, @@ -29,6 +28,7 @@ import { ComponentType, GenericField, LineConfig, + MapColorField, MapConfig, PaletteType, PieConfig, @@ -56,7 +56,7 @@ import { isTemporalEntityDimension, isTemporalOrdinalDimension, } from "@/domain/data"; -import { getDefaultCategoricalPaletteName, getPalette } from "@/palettes"; +import { getDefaultCategoricalPaletteId, getPaletteId } from "@/palettes"; /** * This module controls chart controls displayed in the UI. @@ -65,7 +65,7 @@ import { getDefaultCategoricalPaletteName, getPalette } from "@/palettes"; type BaseEncodingFieldType = "animation"; type MapEncodingFieldType = "baseLayer" | "areaLayer" | "symbolLayer"; -type XYEncodingFieldType = "x" | "y" | "segment"; +type XYEncodingFieldType = "x" | "y" | "segment" | "color"; export type EncodingFieldType = | BaseEncodingFieldType | MapEncodingFieldType @@ -175,7 +175,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, @@ -192,7 +192,7 @@ const onColorComponentIdChange: OnEncodingOptionChange = ( MULTI_FILTER_ENABLED_COMPONENTS.includes(component.__typename) || isOrdinalMeasure(component) ) { - const palette = getDefaultCategoricalPaletteName(component, colorPalette); + const palette = getDefaultCategoricalPaletteId(component, colorPalette); newField = getDefaultCategoricalColorField({ id, palette, @@ -483,7 +483,7 @@ 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 palette = getDefaultCategoricalPaletteId( component, chartConfig.fields.color && "paletteId" in chartConfig.fields.color ? chartConfig.fields.color.paletteId @@ -958,7 +958,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { const { chartConfig } = options; const { fields } = chartConfig; const { color } = fields; - const palette = getPalette(color.paletteId); + const palette = getPaletteId(color.paletteId); const newColorMapping = Object.fromEntries( ids.map((id, i) => [id, color.colorMapping[i] ?? palette[i]]) ); diff --git a/app/charts/column/columns-grouped-state.tsx b/app/charts/column/columns-grouped-state.tsx index 279e912d1..7e4b63873 100644 --- a/app/charts/column/columns-grouped-state.tsx +++ b/app/charts/column/columns-grouped-state.tsx @@ -44,7 +44,7 @@ import { useSize } from "@/charts/shared/use-size"; import { ColumnConfig } from "@/configurator"; import { Observation } from "@/domain/data"; import { formatNumberWithUnit, useFormatNumber } from "@/formatters"; -import { getPalette } from "@/palettes"; +import { getPaletteId } from "@/palettes"; import { sortByIndex } from "@/utils/array"; import { getSortingOrders, @@ -189,7 +189,8 @@ const useColumnsGroupedState = ( } = useMemo(() => { const colors = scaleOrdinal(); - if (fields.segment && segmentDimension && fields.color) { + if (fields.segment && segmentDimension && fields.color.type === "segment") { + const segmentColor = fields.color; const orderedSegmentLabelsAndColors = allSegments.map((segment) => { const dvIri = segmentsByAbbreviationOrLabel.get(segment)?.value || @@ -198,10 +199,7 @@ const useColumnsGroupedState = ( return { label: segment, - color: - fields.color.type === "segment" - ? fields.color.colorMapping![dvIri] ?? schemeCategory10[0] - : schemeCategory10[0], + color: segmentColor.colorMapping![dvIri] ?? schemeCategory10[0], }; }); @@ -209,7 +207,7 @@ const useColumnsGroupedState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.color.paletteId)); + colors.range(getPaletteId(fields.color.paletteId)); } colors.unknown(() => undefined); diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx index 9661bacde..38a26a6b1 100644 --- a/app/charts/column/columns-stacked-state.tsx +++ b/app/charts/column/columns-stacked-state.tsx @@ -56,7 +56,7 @@ import { useSize } from "@/charts/shared/use-size"; import { ColumnConfig } from "@/configurator"; import { Observation } from "@/domain/data"; import { useFormatNumber } from "@/formatters"; -import { getPalette } from "@/palettes"; +import { getPaletteId } from "@/palettes"; import { useChartInteractiveFilters } from "@/stores/interactive-filters"; import { sortByIndex } from "@/utils/array"; import { @@ -224,7 +224,12 @@ const useColumnsStackedState = ( } = useMemo(() => { const colors = scaleOrdinal(); - if (fields.segment && segmentsByAbbreviationOrLabel && fields.color) { + if ( + fields.segment && + segmentsByAbbreviationOrLabel && + 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 @@ -240,10 +245,7 @@ const useColumnsStackedState = ( return { label: segment, - color: - fields.color.type === "segment" - ? fields.color.colorMapping![dvIri] ?? schemeCategory10[0] - : schemeCategory10[0], + color: segmentColor.colorMapping![dvIri] ?? schemeCategory10[0], }; }); @@ -251,7 +253,7 @@ const useColumnsStackedState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.color.paletteId)); + colors.range(getPaletteId(fields.color.paletteId)); } colors.unknown(() => undefined); diff --git a/app/charts/index.ts b/app/charts/index.ts index 5f5d8f0c6..cf2631e75 100644 --- a/app/charts/index.ts +++ b/app/charts/index.ts @@ -23,6 +23,7 @@ import { ChartConfigsAdjusters, ChartSegmentField, ChartType, + ColorField, ColumnSegmentField, ComboChartType, ComboLineColumnFields, @@ -32,7 +33,6 @@ import { Filters, GenericField, GenericFields, - GenericSegmentField, InteractiveFiltersAdjusters, InteractiveFiltersConfig, isAreaConfig, @@ -51,7 +51,6 @@ import { MapConfig, MapSymbolLayer, Meta, - NewColorField, PieSegmentField, RegularChartType, ScatterPlotSegmentField, @@ -84,8 +83,9 @@ import { import { truthy } from "@/domain/types"; import { DEFAULT_CATEGORICAL_PALETTE_NAME, - getDefaultCategoricalPaletteName, + 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"; @@ -290,7 +290,7 @@ const getInitialAreaLayer = ({ component: GeoShapesDimension; measure: Measure; }): MapAreaLayer => { - const palette = getDefaultCategoricalPaletteName(measure); + const palette = getDefaultCategoricalPaletteId(measure); return { componentId: component.id, @@ -400,12 +400,9 @@ export const getInitialConfig = ( x: { componentId: areaXComponentId }, y: { componentId: numericalMeasures[0].id, imputationType: "none" }, color: { - type: "segment", + type: "single", paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, - colorMapping: mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, - dimensionValues: dimensions[0].values, - }), + color: theme.palette.primary.main, }, }, }; @@ -434,7 +431,7 @@ export const getInitialConfig = ( color: { type: "single", paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, - color: dimensions[0].values[0].color || "#000", + color: theme.palette.primary.main, }, }, }; @@ -453,7 +450,7 @@ export const getInitialConfig = ( color: { type: "single", paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, - color: dimensions[0].values[0].color || "#000", + color: theme.palette.primary.main, }, }, }; @@ -496,7 +493,7 @@ export const getInitialConfig = ( const pieSegmentComponent = getCategoricalDimensions(dimensions)[0] ?? getGeoDimensions(dimensions)[0]; - const piePalette = getDefaultCategoricalPaletteName(pieSegmentComponent); + const piePalette = getDefaultCategoricalPaletteId(pieSegmentComponent); return { ...getGenericConfigProps(), @@ -522,7 +519,7 @@ export const getInitialConfig = ( const scatterplotSegmentComponent = getCategoricalDimensions(dimensions)[0] || getGeoDimensions(dimensions)[0]; - const scatterplotPalette = getDefaultCategoricalPaletteName( + const scatterplotPalette = getDefaultCategoricalPaletteId( scatterplotSegmentComponent ); @@ -556,7 +553,7 @@ export const getInitialConfig = ( color: { type: "single", paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, - color: dimensions[0].values[0].color || "#000", + color: theme.palette.primary.main, }, }), }, @@ -973,7 +970,7 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: ColumnSegmentField | undefined; - let newColor: NewColorField | undefined; + let newColor: ColorField | undefined; const yMeasure = measures.find( (d) => d.id === newChartConfig.fields.y.componentId @@ -981,19 +978,19 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { // When switching from a table chart, a whole fields object is passed as oldValue. if (oldChartConfig.chartType === "table") { - const tableSegment = convertTableFieldsToSegmentField({ + const tableField = convertTableFieldsToSegmentAndColorFields({ fields: oldValue as TableFields, dimensions, measures, }); - if (tableSegment) { + if (tableField) { newSegment = { - ...tableSegment.segment, + ...tableField.segment, sorting: DEFAULT_SORTING, type: disableStacked(yMeasure) ? "grouped" : "stacked", }; - newColor = tableSegment.color; + newColor = tableField.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 @@ -1026,10 +1023,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { } return produce(newChartConfig, (draft) => { - if (newSegment) { + if (newSegment && newColor?.type === "segment") { draft.fields.segment = newSegment; - } - if (newColor?.type === "segment") { draft.fields.color = newColor; } }); @@ -1083,18 +1078,18 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: LineSegmentField | undefined; - let newColor: NewColorField | undefined; + let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const tableSegment = convertTableFieldsToSegmentField({ + const tableField = convertTableFieldsToSegmentAndColorFields({ fields: oldValue as TableFields, dimensions, measures, }); - if (tableSegment) { - newSegment = tableSegment.segment; - newColor = tableSegment.color; + if (tableField) { + newSegment = tableField.segment; + newColor = tableField.color; } } else { const oldSegment = oldValue as Exclude; @@ -1124,10 +1119,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { } return produce(newChartConfig, (draft) => { - if (newSegment) { + if (newSegment && newColor?.type === "segment") { draft.fields.segment = newSegment; - } - if (newColor?.type === "segment") { draft.fields.color = newColor; } }); @@ -1182,21 +1175,21 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { } let newSegment: AreaSegmentField | undefined; - let newColor: NewColorField | undefined; + let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const tableSegment = convertTableFieldsToSegmentField({ + const tableField = convertTableFieldsToSegmentAndColorFields({ fields: oldValue as TableFields, dimensions, measures, }); - if (tableSegment) { + if (tableField) { newSegment = { - ...tableSegment.segment, + ...tableField.segment, sorting: DEFAULT_SORTING, }; - newColor = tableSegment.color; + newColor = tableField.color; } } else { const oldSegment = oldValue as Exclude; @@ -1225,10 +1218,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { } return produce(newChartConfig, (draft) => { - if (newSegment) { + if (newSegment && newColor?.type === "segment") { draft.fields.segment = newSegment; - } - if (newColor?.type === "segment") { draft.fields.color = newColor; } }); @@ -1268,18 +1259,18 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: ScatterPlotSegmentField | undefined; - let newColor: NewColorField | undefined; + let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const tableSegment = convertTableFieldsToSegmentField({ + const tableField = convertTableFieldsToSegmentAndColorFields({ fields: oldValue as TableFields, dimensions, measures, }); - if (tableSegment) { - newSegment = tableSegment.segment; - newColor = tableSegment.color; + if (tableField) { + newSegment = tableField.segment; + newColor = tableField.color; } } else { const oldSegment = oldValue as Exclude; @@ -1289,10 +1280,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { } return produce(newChartConfig, (draft) => { - if (newSegment) { + if (newSegment && newColor?.type === "segment") { draft.fields.segment = newSegment; - } - if (newColor?.type === "segment") { draft.fields.color = newColor; } }); @@ -1327,21 +1316,21 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { measures, }) => { let newSegment: PieSegmentField | undefined; - let newColor: NewColorField | undefined; + let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const tableSegment = convertTableFieldsToSegmentField({ + const tableField = convertTableFieldsToSegmentAndColorFields({ fields: oldValue as TableFields, dimensions, measures, }); - if (tableSegment) { + if (tableField) { newSegment = { - ...tableSegment.segment, + ...tableField.segment, sorting: DEFAULT_SORTING, }; - newColor = tableSegment.color; + newColor = tableField.color; } } else { const oldSegment = oldValue as Exclude; @@ -1366,10 +1355,8 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { } return produce(newChartConfig, (draft) => { - if (newSegment) { + if (newSegment && newColor?.type === "segment") { draft.fields.segment = newSegment; - } - if (newColor?.type === "segment") { draft.fields.color = newColor; } }); @@ -2274,7 +2261,7 @@ export const getFieldComponentId = ( return (fields as $IntentionalAny)[field]?.componentId; }; -const convertTableFieldsToSegmentField = ({ +const convertTableFieldsToSegmentAndColorFields = ({ fields, dimensions, measures, @@ -2282,7 +2269,7 @@ const convertTableFieldsToSegmentField = ({ fields: TableFields; dimensions: Dimension[]; measures: Measure[]; -}): { segment: GenericSegmentField; color: NewColorField } | 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)) @@ -2297,7 +2284,7 @@ const convertTableFieldsToSegmentField = ({ const actualComponent = [...dimensions, ...measures].find( (d) => d.id === componentId ) as Component; - const palette = getDefaultCategoricalPaletteName(actualComponent); + const palette = getDefaultCategoricalPaletteId(actualComponent); return { segment: { diff --git a/app/charts/line/lines-state.tsx b/app/charts/line/lines-state.tsx index 1fed350e7..42089d7ec 100644 --- a/app/charts/line/lines-state.tsx +++ b/app/charts/line/lines-state.tsx @@ -43,7 +43,7 @@ import { useFormatNumber, useTimeFormatUnit, } from "@/formatters"; -import { getPalette } from "@/palettes"; +import { getPaletteId } from "@/palettes"; import { sortByIndex } from "@/utils/array"; import { getSortingOrders, @@ -215,7 +215,7 @@ const useLinesState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.color.paletteId)); + colors.range(getPaletteId(fields.color.paletteId)); } // Dimensions diff --git a/app/charts/pie/pie-state.tsx b/app/charts/pie/pie-state.tsx index eebbbeb0c..b1654e383 100644 --- a/app/charts/pie/pie-state.tsx +++ b/app/charts/pie/pie-state.tsx @@ -26,7 +26,7 @@ import { useSize } from "@/charts/shared/use-size"; import { PieConfig } from "@/configurator"; import { Dimension, Observation } from "@/domain/data"; import { formatNumberWithUnit, useFormatNumber } from "@/formatters"; -import { getPalette } from "@/palettes"; +import { getPaletteId } from "@/palettes"; import { getSortingOrders, makeDimensionValueSorters, @@ -121,7 +121,7 @@ const usePieState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.color.paletteId)); + colors.range(getPaletteId(fields.color.paletteId)); } // Do not let the scale be implicitly extended by children calling it // on unknown values diff --git a/app/charts/scatterplot/scatterplot-state.tsx b/app/charts/scatterplot/scatterplot-state.tsx index d3deae1cc..f3bf9ad38 100644 --- a/app/charts/scatterplot/scatterplot-state.tsx +++ b/app/charts/scatterplot/scatterplot-state.tsx @@ -26,7 +26,7 @@ import { useSize } from "@/charts/shared/use-size"; import { ScatterPlotConfig, SortingField } from "@/configurator"; import { Observation } from "@/domain/data"; import { useFormatNumber } from "@/formatters"; -import { getPalette } from "@/palettes"; +import { getPaletteId } from "@/palettes"; import { getSortingOrders, makeDimensionValueSorters, @@ -147,7 +147,7 @@ const useScatterplotState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.color.paletteId)); + colors.range(getPaletteId(fields.color.paletteId)); } // Dimensions const { left, bottom } = useChartPadding({ diff --git a/app/charts/shared/legend-color.tsx b/app/charts/shared/legend-color.tsx index a8d4a8b79..2b410e024 100644 --- a/app/charts/shared/legend-color.tsx +++ b/app/charts/shared/legend-color.tsx @@ -15,7 +15,7 @@ import { OpenMetadataPanelWrapper } from "@/components/metadata-panel"; import { TooltipTitle } from "@/components/tooltip-utils"; import { ChartConfig, - GenericSegmentField, + GenericField, MapConfig, isSegmentInConfig, useChartConfigFilters, @@ -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/config-types.ts b/app/config-types.ts index ad0e82078..0bcd5567a 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -238,7 +238,7 @@ const ColorField = t.union([ MeasuresColorField, ]); //FIXME: Remove current type called ColorField and replace it with the new one -export type NewColorField = t.TypeOf; +export type ColorField = t.TypeOf; const GenericField = t.intersection([ t.type({ componentId: t.string }), @@ -249,9 +249,6 @@ export type GenericField = t.TypeOf; const GenericFields = t.record(t.string, t.union([GenericField, t.undefined])); export type GenericFields = t.TypeOf; -const GenericSegmentField = GenericField; -export type GenericSegmentField = t.TypeOf; - const AnimationType = t.union([t.literal("continuous"), t.literal("stepped")]); export type AnimationType = t.TypeOf; @@ -300,7 +297,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 }), ]); @@ -339,7 +336,7 @@ const ColumnConfig = t.intersection([ export type ColumnFields = t.TypeOf; export type ColumnConfig = t.TypeOf; -const LineSegmentField = t.intersection([GenericSegmentField, SortingField]); +const LineSegmentField = t.intersection([GenericField, SortingField]); export type LineSegmentField = t.TypeOf; const LineFields = t.intersection([ @@ -366,7 +363,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([ @@ -404,7 +401,7 @@ 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([ @@ -432,7 +429,7 @@ 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([ @@ -642,7 +639,7 @@ const NumericalColorField = t.intersection([ ]); export type NumericalColorField = t.TypeOf; -export type ColorField = +export type MapColorField = | FixedColorField | CategoricalColorField | NumericalColorField; @@ -902,6 +899,19 @@ export const isSegmentInConfig = ( ); }; +export const isColorInConfig = ( + chartConfig: ChartConfig +): chartConfig is + | AreaConfig + | ColumnConfig + | LineConfig + | PieConfig + | ScatterPlotConfig => { + return ["area", "column", "line", "pie", "scatterplot"].includes( + chartConfig.chartType + ); +}; + export const isSortingInConfig = ( chartConfig: ChartConfig ): chartConfig is AreaConfig | ColumnConfig | LineConfig | PieConfig => { diff --git a/app/configurator/components/chart-controls/color-palette.tsx b/app/configurator/components/chart-controls/color-palette.tsx index 4ea39b2a9..4e132393f 100644 --- a/app/configurator/components/chart-controls/color-palette.tsx +++ b/app/configurator/components/chart-controls/color-palette.tsx @@ -2,6 +2,7 @@ import { Trans } from "@lingui/macro"; import { Box, Button, + Divider, MenuItem, Select, SelectProps, @@ -18,6 +19,7 @@ import { Label } from "@/components/form"; import { ConfiguratorStateConfiguringChart, getChartConfig, + isColorInConfig, isConfiguring, useConfiguratorState, } from "@/configurator"; @@ -28,7 +30,7 @@ import { categoricalPalettes, divergingSteppedPalettes, getDefaultCategoricalPalette, - getPalette, + getPaletteId, } from "@/palettes"; import useEvent from "@/utils/use-event"; @@ -76,12 +78,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,18 +95,39 @@ export const ColorPalette = ({ if (!component || !palette) { return; } - - dispatch({ - type: "CHART_PALETTE_CHANGED", - value: { - type: chartConfig.fields.color.type, - paletteId: palette.value, - colorMapping: mapValueIrisToColor({ + if (isColorInConfig(chartConfig)) { + dispatch({ + type: "CHART_PALETTE_CHANGED_NEW", + 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({ + palette: palette.value, + dimensionValues: component.values, + }), + }, + }); + } else { + dispatch({ + type: "CHART_PALETTE_CHANGED", + value: { + field, + colorConfigPath, palette: palette.value, - dimensionValues: component.values, - }), - }, - }); + colorMapping: mapValueIrisToColor({ + palette: palette.value, + dimensionValues: component.values, + }), + }, + }); + } }); return ( @@ -125,13 +150,41 @@ export const ColorPalette = ({ value={currentPalette.value} onChange={handleChangePalette} > + + + + Visualize color palattes + + + {palettes.map((palette, index) => ( - -
+ + {palette.label} -
+ {palette.colors.map((color) => ( ))} -
-
+ +
))} @@ -177,7 +230,7 @@ const useColorSquareStyles = makeStyles((theme: Theme) => ({ }, })); -const ColorSquare = ({ +export const ColorSquare = ({ disabled, color, }: { @@ -240,7 +293,7 @@ const ColorPaletteControls = ({ if (colorMapping) { // Compare palette colors & colorMapping colors - const currentPalette = getPalette(palette); + const currentPalette = getPaletteId(palette); const colorMappingColors = Object.values(colorMapping); const nbMatchedColors = colorMappingColors.length; diff --git a/app/configurator/components/chart-controls/color-picker.tsx b/app/configurator/components/chart-controls/color-picker.tsx index cbd9cc692..8789be8a7 100644 --- a/app/configurator/components/chart-controls/color-picker.tsx +++ b/app/configurator/components/chart-controls/color-picker.tsx @@ -1,13 +1,20 @@ import { Trans } from "@lingui/macro"; import { Box, Button, Popover, styled, Theme, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; +import { HsvaColor } from "@uiw/react-color"; import { color as d3Color } from "d3-color"; -import { MouseEventHandler, useRef } from "react"; +import dynamic from "next/dynamic"; +import { MouseEventHandler, useCallback, useRef, useState } from "react"; import useDisclosure from "@/components/use-disclosure"; import VisuallyHidden from "@/components/visually-hidden"; import { Icon } from "@/icons"; +const CustomColorPicker = dynamic( + () => import("../../components/color-picker"), + { ssr: false } +); + const useStyles = makeStyles(() => ({ swatch: { width: "1.5rem", @@ -119,6 +126,13 @@ export const ColorPickerMenu = (props: Props) => { const { disabled } = props; const { isOpen, open, close } = useDisclosure(); const buttonRef = useRef(null); + const popoverRef = useRef(null); + + const [color, setColor] = useState(undefined); + + const handleColorChange = useCallback((color) => { + setColor(color); + }, []); return ( { - - + + + + ); diff --git a/app/configurator/components/chart-options-selector.tsx b/app/configurator/components/chart-options-selector.tsx index 5172f32c8..732e48b82 100644 --- a/app/configurator/components/chart-options-selector.tsx +++ b/app/configurator/components/chart-options-selector.tsx @@ -1181,7 +1181,7 @@ const ComboChartYColorSection = ({ return ( d.id === id)!.label} symbol={symbol} diff --git a/app/configurator/components/color-picker.css b/app/configurator/components/color-picker.css new file mode 100644 index 000000000..94b980c8c --- /dev/null +++ b/app/configurator/components/color-picker.css @@ -0,0 +1,28 @@ +.w-color-alpha-fill { + width: 10px !important; + height: 10px !important; +} + +.w-color-chrome .w-color-saturation { + display: none !important; +} + +.w-color-chrome { + --github-background-color: transparent !important; + --github-arrow-border-color: transparent !important; + --github-border: none !important; + --github-box-shadow: none !important; + width: auto !important; + background: transparent !important; + padding: 0 !important; +} + +.w-color-chrome div { + padding: 0 !important; + gap: 0 !important; + padding: 4px !important; +} + +.w-color-chrome div div { + display: none !important; +} diff --git a/app/configurator/components/color-picker.tsx b/app/configurator/components/color-picker.tsx new file mode 100644 index 000000000..6d8d3cd41 --- /dev/null +++ b/app/configurator/components/color-picker.tsx @@ -0,0 +1,174 @@ +import { Box } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import { + Chrome, + EditableInput, + hexToHsva, + HsvaColor, + hsvaToHex, + Hue, + Saturation, +} from "@uiw/react-color"; +import { color as d3Color } from "d3-color"; +import { MouseEventHandler, useEffect, useState } from "react"; + +import Flex from "@/components/flex"; + +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 classes = useColorPickerStyles(); + + useEffect(() => { + onChange(hsva); + }, [hsva, onChange]); + + 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) => { + return ( + setHsva(hexToHsva(color))} + /> + ); + })} + + + setHsva(hexToHsva(`#${value}`))} + /> + setHsva(color.hsva)} + /> + + + + ); +}; + +export default CustomColorPicker; + +const useStyles = makeStyles(() => ({ + swatch: { + width: "1rem", + height: "1rem", + borderWidth: 1, + borderStyle: "solid", + borderColor: "transparent", + borderRadius: 1.5, + padding: 0, + cursor: "pointer", + }, +})); + +export const Swatch = ({ + color, + selected, + onClick, +}: { + color: string; + selected: boolean; + onClick: MouseEventHandler; +}) => { + const classes = useStyles(); + const boxShadow = d3Color(color)?.darker().toString(); + + return ( + + ); +}; diff --git a/app/configurator/components/field.tsx b/app/configurator/components/field.tsx index 29c71a280..9aa355dc2 100644 --- a/app/configurator/components/field.tsx +++ b/app/configurator/components/field.tsx @@ -94,7 +94,7 @@ import { import { useTimeFormatLocale } from "@/formatters"; import { TimeUnit } from "@/graphql/query-hooks"; import { useLocale } from "@/locales/use-locale"; -import { getPalette } from "@/palettes"; +import { getPaletteId } from "@/palettes"; import { hierarchyToOptions } from "@/utils/hierarchy"; import { makeDimensionValueSorters } from "@/utils/sorting-values"; import useEvent from "@/utils/use-event"; @@ -710,7 +710,7 @@ const useMultiFilterColorPicker = (value: string) => { ); const palette = useMemo(() => { - return getPalette( + return getPaletteId( get( chartConfig, `fields["${activeField}"].${ @@ -817,7 +817,7 @@ export const ColorPickerField = ({ ); const color = get(chartConfig, `fields["${field}"].${path}`); - const palette = getPalette(get(chartConfig, `fields["${field}"].palette`)); + const palette = getPaletteId(get(chartConfig, `fields["${field}"].palette`)); return ( (palette === "dimension" && d.color) || diff --git a/app/configurator/configurator-state/actions.tsx b/app/configurator/configurator-state/actions.tsx index a52ba33eb..aba1d6bb3 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, ConfiguratorState, DashboardFiltersConfig, DataSource, @@ -9,7 +10,6 @@ import { ImputationType, InteractiveFiltersConfig, Layout, - NewColorField, } from "@/config-types"; import { DataCubeComponents, Dimension, DimensionValue } from "@/domain/data"; import { Locale } from "@/locales/locales"; @@ -91,7 +91,16 @@ export type ConfiguratorStateAction = } | { type: "CHART_PALETTE_CHANGED"; - value: NewColorField; + value: { + field: string; + colorConfigPath?: string; + palette: string; + colorMapping: Record; + }; + } + | { + type: "CHART_PALETTE_CHANGED_NEW"; + value: ColorField; } | { type: "CHART_PALETTE_RESET"; diff --git a/app/configurator/configurator-state/index.tsx b/app/configurator/configurator-state/index.tsx index e96e7e747..1b34850fe 100644 --- a/app/configurator/configurator-state/index.tsx +++ b/app/configurator/configurator-state/index.tsx @@ -89,7 +89,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/reducer.spec.tsx b/app/configurator/configurator-state/reducer.spec.tsx index 3d6e3ad9f..637b39491 100644 --- a/app/configurator/configurator-state/reducer.spec.tsx +++ b/app/configurator/configurator-state/reducer.spec.tsx @@ -705,9 +705,11 @@ describe("colorMapping", () => { chartConfig.fields.color.type === "single" ? chartConfig.fields.color.color : chartConfig.fields.color.colorMapping - ).toEqual({ - orange: "rgb(255, 153, 0)", - }); + ).toEqual( + chartConfig.fields.color.type === "single" + ? "rgb(255, 153, 0)" + : { orange: "rgb(255, 153, 0)" } + ); }); }); diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index f84a4978b..32dc0753e 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -652,7 +652,7 @@ const reducer_: Reducer = ( case "CHART_OPTION_CHANGED": return handleChartOptionChanged(draft, action); - case "CHART_PALETTE_CHANGED": + case "CHART_PALETTE_CHANGED_NEW": if (isConfiguring(draft)) { const chartConfig = getChartConfig(draft); setWith( @@ -671,6 +671,32 @@ const reducer_: Reducer = ( ); } + return draft; + case "CHART_PALETTE_CHANGED": + if (isConfiguring(draft)) { + const chartConfig = getChartConfig(draft); + setWith( + chartConfig, + `fields["${action.value.field}"].${ + action.value.colorConfigPath + ? `${action.value.colorConfigPath}.` + : "" + }palette`, + action.value.palette, + Object + ); + setWith( + chartConfig, + `fields["${action.value.field}"].${ + action.value.colorConfigPath + ? `${action.value.colorConfigPath}.` + : "" + }colorMapping`, + action.value.colorMapping, + Object + ); + } + return draft; case "CHART_PALETTE_RESET": diff --git a/app/configurator/table/table-chart-options.tsx b/app/configurator/table/table-chart-options.tsx index d29087723..5a355571e 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,7 +284,7 @@ export const TableColumnOptions = ({ columnColor: "#fff", }; case "category": - const palette = getDefaultCategoricalPaletteName(component); + const palette = getDefaultCategoricalPaletteId(component); return { type: "category", diff --git a/app/docs/controls.docs.mdx b/app/docs/controls.docs.mdx index bdf9b10f5..2d78acadc 100644 --- a/app/docs/controls.docs.mdx +++ b/app/docs/controls.docs.mdx @@ -14,12 +14,13 @@ import { SectionTitle, } from "@/configurator/components/chart-controls/section"; import { IconButton } from "@/configurator/components/icon-button"; -import { getPalette } from "@/palettes"; +import { getPaletteId } from "@/palettes"; -import { Canvas, Meta } from '@storybook/blocks'; -import * as ControlsStories from './controls.stories'; +import { Canvas, Meta } from "@storybook/blocks"; +import * as ControlsStories from "./controls.stories"; -> Controls are a composition of layout components and form elements, used throughout the application to configurate charts. +> Controls are a composition of layout components and form elements, used +> throughout the application to configurate charts. @@ -29,27 +30,24 @@ import * as ControlsStories from './controls.stories'; ## OnOffControlTab -OnOffControlTab (and OnOffControlTabField) are elements which are supposed to be used on the left panel in the app as category "switches" -(like for BaseLayer in case of maps or InteractiveFilters for... interactive filters). They display either "on" or "off" to indicate component state. +OnOffControlTab (and OnOffControlTabField) are elements which are supposed to be +used on the left panel in the app as category "switches" (like for BaseLayer in +case of maps or InteractiveFilters for... interactive filters). They display +either "on" or "off" to indicate component state. - - - ## Controls section -A section is a styling container, it has a title and a note (displayed on the right). Any component can be given as child component. +A section is a styling container, it has a title and a note (displayed on the +right). Any component can be given as child component. - ## Controls list - - ## Color Picker - +## Color Picker diff --git a/app/docs/controls.stories.tsx b/app/docs/controls.stories.tsx index 3935b9b9a..ca9846c41 100644 --- a/app/docs/controls.stories.tsx +++ b/app/docs/controls.stories.tsx @@ -9,7 +9,7 @@ import { ControlSection, SectionTitle, } from "@/configurator/components/chart-controls/section"; -import { getPalette } from "@/palettes"; +import { getPaletteId } from "@/palettes"; import { IconButton } from "../configurator/components/icon-button"; @@ -175,7 +175,7 @@ const ColorPickerStory = { Current (valid) color: {currentColor} setCurrentColor(color)} /> diff --git a/app/package.json b/app/package.json index e593ab727..9e75b41dc 100644 --- a/app/package.json +++ b/app/package.json @@ -43,6 +43,7 @@ "@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", 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..49f03c26d 100644 --- a/app/palettes.ts +++ b/app/palettes.ts @@ -29,7 +29,7 @@ import { DivergingPaletteType, SequentialPaletteType } from "./config-types"; import { Component } from "./domain/data"; // Colors -export const getDefaultCategoricalPaletteName = ( +export const getDefaultCategoricalPaletteId = ( d?: Component, previousPaletteName?: string ): string => { @@ -53,7 +53,7 @@ export const getDefaultCategoricalPalette = ( } }; -export const getPalette = ( +export const getPaletteId = ( palette?: string, colors?: string[] ): ReadonlyArray => { @@ -96,16 +96,16 @@ export const categoricalPalettes: Array = [ { label: "category10", value: "category10", - colors: getPalette("category10"), + colors: getPaletteId("category10"), }, - { 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: "accent", value: "accent", colors: getPaletteId("accent") }, + { label: "dark2", value: "dark2", colors: getPaletteId("dark2") }, + { label: "paired", value: "paired", colors: getPaletteId("paired") }, + { label: "pastel1", value: "pastel1", colors: getPaletteId("pastel1") }, + { label: "pastel2", value: "pastel2", colors: getPaletteId("pastel2") }, + { label: "set1", value: "set1", colors: getPaletteId("set1") }, + { label: "set2", value: "set2", colors: getPaletteId("set2") }, + { label: "set3", value: "set3", colors: getPaletteId("set3") }, ]; export const DEFAULT_CATEGORICAL_PALETTE_NAME = categoricalPalettes[0].value; diff --git a/yarn.lock b/yarn.lock index 95a66cd8d..2470643fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -230,7 +230,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.21.0", "@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.21.0", "@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== @@ -260,6 +282,17 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.12.5", "@babel/generator@^7.25.9", "@babel/generator@^7.26.0": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.2.tgz#87b75813bec87916210e5e01939a4c823d6bb74f" + integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw== + dependencies: + "@babel/parser" "^7.26.2" + "@babel/types" "^7.26.0" + "@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" @@ -290,17 +323,6 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" -"@babel/generator@^7.25.9", "@babel/generator@^7.26.0": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.2.tgz#87b75813bec87916210e5e01939a4c823d6bb74f" - integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw== - dependencies: - "@babel/parser" "^7.26.2" - "@babel/types" "^7.26.0" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - jsesc "^3.0.2" - "@babel/helper-annotate-as-pure@^7.14.5": version "7.14.5" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz" @@ -538,6 +560,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" @@ -563,15 +594,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" @@ -786,7 +808,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== @@ -840,7 +862,12 @@ 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.2", "@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.12.7", "@babel/parser@^7.14.7", "@babel/parser@^7.15.4", "@babel/parser@^7.20.15", "@babel/parser@^7.20.7", "@babel/parser@^7.21.2", "@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.2" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ== @@ -2500,6 +2527,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" @@ -2527,15 +2563,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" @@ -2566,6 +2593,19 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.12.9", "@babel/traverse@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" + integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/generator" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/template" "^7.25.9" + "@babel/types" "^7.25.9" + 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" @@ -2598,19 +2638,6 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/traverse@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" - integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== - dependencies: - "@babel/code-frame" "^7.25.9" - "@babel/generator" "^7.25.9" - "@babel/parser" "^7.25.9" - "@babel/template" "^7.25.9" - "@babel/types" "^7.25.9" - debug "^4.3.1" - globals "^11.1.0" - "@babel/types@7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz" @@ -2628,6 +2655,14 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" +"@babel/types@^7.12.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" + integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== + 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" @@ -2690,14 +2725,6 @@ "@babel/helper-validator-identifier" "^7.24.5" to-fast-properties "^2.0.0" -"@babel/types@^7.25.9", "@babel/types@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" - integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== - dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@base2/pretty-print-object@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4" @@ -10034,6 +10061,207 @@ "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" +"@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== + "@urql/core@^2.3.2": version "2.3.5" resolved "https://registry.npmjs.org/@urql/core/-/core-2.3.5.tgz" @@ -12479,6 +12707,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" @@ -15641,7 +15879,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== @@ -22328,7 +22566,7 @@ resolve.exports@^1.1.0: resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@1.22.8, resolve@^1.14.2, resolve@^1.22.8: +resolve@1.22.8, resolve@^1.14.2, resolve@^1.22.8, resolve@^1.3.2: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -22676,6 +22914,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" From ac51939f843335ce29ed6106f5ae1dff17063983 Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Thu, 5 Dec 2024 09:21:56 +0100 Subject: [PATCH 05/21] wip --- app/charts/area/areas-state.tsx | 6 +- app/charts/chart-config-ui-options.spec.ts | 4 +- app/charts/chart-config-ui-options.ts | 52 ++++-- app/charts/column/columns-grouped-state.tsx | 10 +- app/charts/column/columns-stacked-state.tsx | 4 +- app/charts/index.ts | 167 +++++++++--------- app/charts/line/lines-state.tsx | 4 +- app/charts/map/constants.ts | 16 +- app/charts/map/map-legend.tsx | 12 +- app/charts/map/map-state.tsx | 12 +- app/charts/pie/pie-state.tsx | 4 +- app/charts/scatterplot/scatterplot-state.tsx | 4 +- app/charts/table/table-state.tsx | 2 +- app/config-types.ts | 69 +++++++- .../chart-controls/color-palette.tsx | 31 ++-- .../chart-controls/color-picker.tsx | 18 +- .../components/chart-controls/color-ramp.tsx | 2 +- .../components/chart-options-selector.tsx | 94 ++++++---- app/configurator/components/color-picker.tsx | 60 +++++-- app/configurator/components/field.tsx | 16 +- app/configurator/components/filters.tsx | 2 +- app/configurator/components/ui-helpers.ts | 10 +- app/configurator/config-form.tsx | 8 +- .../configurator-state/actions.tsx | 6 +- app/configurator/configurator-state/mocks.ts | 2 +- .../configurator-state/reducer.spec.tsx | 4 +- .../configurator-state/reducer.tsx | 28 ++- .../table/table-chart-options.tsx | 8 +- app/docs/columns.mock.ts | 5 + app/docs/controls.docs.mdx | 2 +- app/docs/controls.stories.tsx | 4 +- app/docs/fixtures.ts | 18 +- app/docs/lines.mock.tsx | 21 ++- app/docs/scatterplot.mock.ts | 12 +- app/palettes.ts | 37 ++-- app/utils/chart-config/versioning.ts | 14 +- 36 files changed, 475 insertions(+), 293 deletions(-) diff --git a/app/charts/area/areas-state.tsx b/app/charts/area/areas-state.tsx index 70eb281cf..566fb15fa 100644 --- a/app/charts/area/areas-state.tsx +++ b/app/charts/area/areas-state.tsx @@ -54,7 +54,7 @@ import { useSize } from "@/charts/shared/use-size"; import { AreaConfig } from "@/configurator"; import { Observation } from "@/domain/data"; import { useFormatNumber, useTimeFormatUnit } from "@/formatters"; -import { getPaletteId } from "@/palettes"; +import { getPalette } from "@/palettes"; import { useChartInteractiveFilters } from "@/stores/interactive-filters"; import { sortByIndex } from "@/utils/array"; import { @@ -248,7 +248,7 @@ const useAreasState = ( const xScaleTimeRange = scaleTime().domain(xScaleTimeRangeDomain); const colors = scaleOrdinal(); - if (segmentDimension && fields.color.type === "segment") { + if (segmentDimension && fields.color?.type === "segment") { const segmentColor = fields.color; const orderedSegmentLabelsAndColors = allSegments.map((segment) => { const dvIri = @@ -266,7 +266,7 @@ const useAreasState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPaletteId(fields.color.paletteId)); + colors.range(getPalette(fields.color?.paletteId)); } colors.unknown(() => undefined); diff --git a/app/charts/chart-config-ui-options.spec.ts b/app/charts/chart-config-ui-options.spec.ts index 3649f4abf..c7e29e7b1 100644 --- a/app/charts/chart-config-ui-options.spec.ts +++ b/app/charts/chart-config-ui-options.spec.ts @@ -1,7 +1,7 @@ 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"; +import { DEFAULT_CATEGORICAL_PALETTE_ID } from "@/palettes"; jest.mock("../rdf/extended-cube", () => ({ ExtendedCube: jest.fn(), @@ -45,7 +45,7 @@ describe("defaultSegmentOnChange", () => { expect(chartConfig.fields.segment).toEqual( expect.objectContaining({ componentId, - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + palette: DEFAULT_CATEGORICAL_PALETTE_ID, }) ); }); diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts index f76060c9b..ea26bf00c 100644 --- a/app/charts/chart-config-ui-options.ts +++ b/app/charts/chart-config-ui-options.ts @@ -56,7 +56,7 @@ import { isTemporalEntityDimension, isTemporalOrdinalDimension, } from "@/domain/data"; -import { getDefaultCategoricalPaletteId, getPaletteId } from "@/palettes"; +import { getDefaultCategoricalPaletteId, getPalette } from "@/palettes"; /** * This module controls chart controls displayed in the UI. @@ -192,10 +192,13 @@ const onColorComponentIdChange: OnEncodingOptionChange = ( MULTI_FILTER_ENABLED_COMPONENTS.includes(component.__typename) || isOrdinalMeasure(component) ) { - const palette = getDefaultCategoricalPaletteId(component, colorPalette); + const paletteId = getDefaultCategoricalPaletteId( + component, + colorPalette?.paletteId + ); newField = getDefaultCategoricalColorField({ id, - palette, + paletteId, dimensionValues: component.values, }); } else if (isNumericalMeasure(component)) { @@ -483,14 +486,14 @@ export const defaultSegmentOnChange: OnEncodingChange< > = (id, { chartConfig, dimensions, measures, selectedValues }) => { const components = [...dimensions, ...measures]; const component = components.find((d) => d.id === id); - const palette = getDefaultCategoricalPaletteId( + const paletteId = getDefaultCategoricalPaletteId( component, chartConfig.fields.color && "paletteId" in chartConfig.fields.color ? chartConfig.fields.color.paletteId : undefined ); const colorMapping = mapValueIrisToColor({ - palette, + paletteId, dimensionValues: component ? component.values : selectedValues, }); @@ -503,7 +506,7 @@ export const defaultSegmentOnChange: OnEncodingChange< }; chartConfig.fields.color = { type: "segment", - paletteId: palette, + paletteId: paletteId, colorMapping, }; } @@ -607,7 +610,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { }, options: { calculation: {}, - colorPalette: {}, + colorPalette: { + type: "single", + paletteId: "dimension", + colorMapping: {}, + }, imputation: { shouldShow: (chartConfig, data) => { return isMissingDataPresent(chartConfig, data); @@ -763,7 +770,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { } }, }, - colorPalette: {}, + colorPalette: { + type: "single", + paletteId: "dimension", + colorMapping: {}, + }, useAbbreviations: {}, }, }, @@ -781,6 +792,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { componentTypes: ["NumericalMeasure"], filters: false, options: { + colorPalette: { + type: "single", + paletteId: "dimension", + colorMapping: {}, + }, showStandardError: {}, showConfidenceInterval: {}, }, @@ -801,7 +817,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { sorting: LINE_SEGMENT_SORTING, onChange: defaultSegmentOnChange, options: { - colorPalette: {}, + colorPalette: { + type: "single", + paletteId: "dimension", + colorMapping: {}, + }, useAbbreviations: {}, }, }, @@ -894,7 +914,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { sorting: PIE_SEGMENT_SORTING, onChange: defaultSegmentOnChange, options: { - colorPalette: {}, + colorPalette: { + type: "single", + paletteId: "dimension", + colorMapping: {}, + }, useAbbreviations: {}, }, }, @@ -927,7 +951,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { filters: true, onChange: defaultSegmentOnChange, options: { - colorPalette: {}, + colorPalette: { + type: "single", + paletteId: "dimension", + colorMapping: {}, + }, useAbbreviations: {}, }, }, @@ -958,7 +986,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { const { chartConfig } = options; const { fields } = chartConfig; const { color } = fields; - const palette = getPaletteId(color.paletteId); + const palette = getPalette(color.paletteId); const newColorMapping = Object.fromEntries( ids.map((id, i) => [id, color.colorMapping[i] ?? palette[i]]) ); diff --git a/app/charts/column/columns-grouped-state.tsx b/app/charts/column/columns-grouped-state.tsx index 7e4b63873..841b11c9e 100644 --- a/app/charts/column/columns-grouped-state.tsx +++ b/app/charts/column/columns-grouped-state.tsx @@ -44,7 +44,7 @@ import { useSize } from "@/charts/shared/use-size"; import { ColumnConfig } from "@/configurator"; import { Observation } from "@/domain/data"; import { formatNumberWithUnit, useFormatNumber } from "@/formatters"; -import { getPaletteId } from "@/palettes"; +import { getPalette } from "@/palettes"; import { sortByIndex } from "@/utils/array"; import { getSortingOrders, @@ -189,7 +189,11 @@ const useColumnsGroupedState = ( } = useMemo(() => { const colors = scaleOrdinal(); - if (fields.segment && segmentDimension && fields.color.type === "segment") { + if ( + fields.segment && + segmentDimension && + fields.color?.type === "segment" + ) { const segmentColor = fields.color; const orderedSegmentLabelsAndColors = allSegments.map((segment) => { const dvIri = @@ -207,7 +211,7 @@ const useColumnsGroupedState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPaletteId(fields.color.paletteId)); + colors.range(getPalette(fields.color?.paletteId)); } colors.unknown(() => undefined); diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx index 38a26a6b1..081eac12c 100644 --- a/app/charts/column/columns-stacked-state.tsx +++ b/app/charts/column/columns-stacked-state.tsx @@ -56,7 +56,7 @@ import { useSize } from "@/charts/shared/use-size"; import { ColumnConfig } from "@/configurator"; import { Observation } from "@/domain/data"; import { useFormatNumber } from "@/formatters"; -import { getPaletteId } from "@/palettes"; +import { getPalette } from "@/palettes"; import { useChartInteractiveFilters } from "@/stores/interactive-filters"; import { sortByIndex } from "@/utils/array"; import { @@ -253,7 +253,7 @@ const useColumnsStackedState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPaletteId(fields.color.paletteId)); + colors.range(getPalette(fields.color.paletteId)); } colors.unknown(() => undefined); diff --git a/app/charts/index.ts b/app/charts/index.ts index cf2631e75..f00711925 100644 --- a/app/charts/index.ts +++ b/app/charts/index.ts @@ -82,7 +82,7 @@ import { } from "@/domain/data"; import { truthy } from "@/domain/types"; import { - DEFAULT_CATEGORICAL_PALETTE_NAME, + DEFAULT_CATEGORICAL_PALETTE_ID, getDefaultCategoricalPaletteId, } from "@/palettes"; import { theme } from "@/themes/federal"; @@ -290,7 +290,7 @@ const getInitialAreaLayer = ({ component: GeoShapesDimension; measure: Measure; }): MapAreaLayer => { - const palette = getDefaultCategoricalPaletteId(measure); + const paletteId = getDefaultCategoricalPaletteId(measure); return { componentId: component.id, @@ -300,7 +300,7 @@ const getInitialAreaLayer = ({ }) : getDefaultCategoricalColorField({ id: measure.id, - palette, + paletteId, dimensionValues: measure.values, }), }; @@ -401,7 +401,7 @@ export const getInitialConfig = ( y: { componentId: numericalMeasures[0].id, imputationType: "none" }, color: { type: "single", - paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, color: theme.palette.primary.main, }, }, @@ -430,7 +430,7 @@ export const getInitialConfig = ( y: { componentId: numericalMeasures[0].id }, color: { type: "single", - paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, color: theme.palette.primary.main, }, }, @@ -449,7 +449,7 @@ export const getInitialConfig = ( y: { componentId: numericalMeasures[0].id }, color: { type: "single", - paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, color: theme.palette.primary.main, }, }, @@ -509,7 +509,7 @@ export const getInitialConfig = ( type: "segment", paletteId: piePalette, colorMapping: mapValueIrisToColor({ - palette: piePalette, + paletteId: piePalette, dimensionValues: pieSegmentComponent.values, }), }, @@ -541,7 +541,7 @@ export const getInitialConfig = ( type: "segment", paletteId: scatterplotPalette, colorMapping: mapValueIrisToColor({ - palette: scatterplotPalette, + paletteId: scatterplotPalette, dimensionValues: scatterplotSegmentComponent.values, }), }, @@ -552,7 +552,7 @@ export const getInitialConfig = ( : { color: { type: "single", - paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, color: theme.palette.primary.main, }, }), @@ -616,9 +616,9 @@ export const getInitialConfig = ( }, color: { type: "measures", - paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + 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, @@ -654,9 +654,9 @@ export const getInitialConfig = ( }, color: { type: "measures", - paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, colorMapping: mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, dimensionValues: [leftAxisComponentId, rightAxisComponentId].map( (id) => ({ value: id, @@ -695,9 +695,9 @@ export const getInitialConfig = ( }, color: { type: "measures", - paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, colorMapping: mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, dimensionValues: [lineComponentId, columnComponentId].map( (id) => ({ value: id, @@ -978,19 +978,20 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { // When switching from a table chart, a whole fields object is passed as oldValue. if (oldChartConfig.chartType === "table") { - const tableField = convertTableFieldsToSegmentAndColorFields({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentAndColorFields = + convertTableFieldsToSegmentAndColorFields({ + fields: oldValue as TableFields, + dimensions, + measures, + }); - if (tableField) { + if (maybeSegmentAndColorFields) { newSegment = { - ...tableField.segment, + ...maybeSegmentAndColorFields.segment, sorting: DEFAULT_SORTING, type: disableStacked(yMeasure) ? "grouped" : "stacked", }; - newColor = tableField.color; + 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 @@ -1012,9 +1013,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }; newColor = { type: "segment", - paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, colorMapping: mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, dimensionValues: dimensions.find((d) => d.id === oldValue.componentId)?.values || [], @@ -1081,15 +1082,16 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const tableField = convertTableFieldsToSegmentAndColorFields({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentAndColorFields = + convertTableFieldsToSegmentAndColorFields({ + fields: oldValue as TableFields, + dimensions, + measures, + }); - if (tableField) { - newSegment = tableField.segment; - newColor = tableField.color; + if (maybeSegmentAndColorFields) { + newSegment = maybeSegmentAndColorFields.segment; + newColor = maybeSegmentAndColorFields.color; } } else { const oldSegment = oldValue as Exclude; @@ -1109,9 +1111,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }; newColor = { type: "segment", - paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, colorMapping: mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, dimensionValues: segmentDimension?.values || [], }), }; @@ -1178,18 +1180,19 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const tableField = convertTableFieldsToSegmentAndColorFields({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentAndColorFields = + convertTableFieldsToSegmentAndColorFields({ + fields: oldValue as TableFields, + dimensions, + measures, + }); - if (tableField) { + if (maybeSegmentAndColorFields) { newSegment = { - ...tableField.segment, + ...maybeSegmentAndColorFields.segment, sorting: DEFAULT_SORTING, }; - newColor = tableField.color; + newColor = maybeSegmentAndColorFields.color; } } else { const oldSegment = oldValue as Exclude; @@ -1208,9 +1211,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }; newColor = { type: "segment", - paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, colorMapping: mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, dimensionValues: segmentDimension?.values || [], }), }; @@ -1262,15 +1265,16 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const tableField = convertTableFieldsToSegmentAndColorFields({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentAndColorFields = + convertTableFieldsToSegmentAndColorFields({ + fields: oldValue as TableFields, + dimensions, + measures, + }); - if (tableField) { - newSegment = tableField.segment; - newColor = tableField.color; + if (maybeSegmentAndColorFields) { + newSegment = maybeSegmentAndColorFields.segment; + newColor = maybeSegmentAndColorFields.color; } } else { const oldSegment = oldValue as Exclude; @@ -1319,18 +1323,19 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { let newColor: ColorField | undefined; if (oldChartConfig.chartType === "table") { - const tableField = convertTableFieldsToSegmentAndColorFields({ - fields: oldValue as TableFields, - dimensions, - measures, - }); + const maybeSegmentAndColorFields = + convertTableFieldsToSegmentAndColorFields({ + fields: oldValue as TableFields, + dimensions, + measures, + }); - if (tableField) { + if (maybeSegmentAndColorFields) { newSegment = { - ...tableField.segment, + ...maybeSegmentAndColorFields.segment, sorting: DEFAULT_SORTING, }; - newColor = tableField.color; + newColor = maybeSegmentAndColorFields.color; } } else { const oldSegment = oldValue as Exclude; @@ -1344,9 +1349,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }; newColor = { type: "segment", - paletteId: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, colorMapping: mapValueIrisToColor({ - palette: DEFAULT_CATEGORICAL_PALETTE_NAME, + paletteId: DEFAULT_CATEGORICAL_PALETTE_ID, dimensionValues: dimensions.find((d) => d.id === oldSegment.componentId) ?.values || [], @@ -1485,12 +1490,12 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { const componentIds = numericalMeasures .filter((d) => d.unit === unit) .map((d) => d.id); - const palette = isSegmentInConfig(oldChartConfig) + const paletteId = isSegmentInConfig(oldChartConfig) ? oldChartConfig.fields.color.paletteId ?? - DEFAULT_CATEGORICAL_PALETTE_NAME + DEFAULT_CATEGORICAL_PALETTE_ID : isComboChartConfig(oldChartConfig) ? oldChartConfig.fields.color.paletteId - : DEFAULT_CATEGORICAL_PALETTE_NAME; + : DEFAULT_CATEGORICAL_PALETTE_ID; return produce(newChartConfig, (draft) => { draft.fields.y = { @@ -1498,9 +1503,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }; draft.fields.color = { type: "measures", - paletteId: palette, + paletteId: paletteId, colorMapping: mapValueIrisToColor({ - palette, + paletteId, dimensionValues: componentIds.map((id) => ({ value: id, label: id, @@ -1601,12 +1606,12 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { ).id; leftMeasure = getLeftMeasure(leftMeasure.id); - const palette = isSegmentInConfig(oldChartConfig) + const paletteId = isSegmentInConfig(oldChartConfig) ? oldChartConfig.fields.color.paletteId ?? - DEFAULT_CATEGORICAL_PALETTE_NAME + DEFAULT_CATEGORICAL_PALETTE_ID : isComboChartConfig(oldChartConfig) ? oldChartConfig.fields.color.paletteId - : DEFAULT_CATEGORICAL_PALETTE_NAME; + : DEFAULT_CATEGORICAL_PALETTE_ID; return produce(newChartConfig, (draft) => { draft.fields.y = { @@ -1615,9 +1620,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }; draft.fields.color = { type: "measures", - paletteId: palette, + paletteId: paletteId, colorMapping: mapValueIrisToColor({ - palette, + paletteId, dimensionValues: [leftMeasure.id, rightMeasureId].map((id) => ({ value: id, label: id, @@ -1694,12 +1699,12 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { ) as NumericalMeasure ).id; - const palette = isSegmentInConfig(oldChartConfig) + const paletteId = isSegmentInConfig(oldChartConfig) ? oldChartConfig.fields.color.paletteId ?? - DEFAULT_CATEGORICAL_PALETTE_NAME + DEFAULT_CATEGORICAL_PALETTE_ID : isComboChartConfig(oldChartConfig) ? oldChartConfig.fields.color.paletteId - : DEFAULT_CATEGORICAL_PALETTE_NAME; + : DEFAULT_CATEGORICAL_PALETTE_ID; return produce(newChartConfig, (draft) => { draft.fields.y = { @@ -1709,9 +1714,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { }; draft.fields.color = { type: "measures", - paletteId: palette, + paletteId: paletteId, colorMapping: mapValueIrisToColor({ - palette, + paletteId, dimensionValues: [leftMeasure.id, lineComponentId].map((id) => ({ value: id, label: id, @@ -2284,7 +2289,7 @@ const convertTableFieldsToSegmentAndColorFields = ({ const actualComponent = [...dimensions, ...measures].find( (d) => d.id === componentId ) as Component; - const palette = getDefaultCategoricalPaletteId(actualComponent); + const paletteId = getDefaultCategoricalPaletteId(actualComponent); return { segment: { @@ -2292,9 +2297,9 @@ const convertTableFieldsToSegmentAndColorFields = ({ }, color: { type: "segment", - paletteId: palette, + paletteId: paletteId, colorMapping: mapValueIrisToColor({ - palette: palette, + 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 42089d7ec..1fed350e7 100644 --- a/app/charts/line/lines-state.tsx +++ b/app/charts/line/lines-state.tsx @@ -43,7 +43,7 @@ import { useFormatNumber, useTimeFormatUnit, } from "@/formatters"; -import { getPaletteId } from "@/palettes"; +import { getPalette } from "@/palettes"; import { sortByIndex } from "@/utils/array"; import { getSortingOrders, @@ -215,7 +215,7 @@ const useLinesState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPaletteId(fields.color.paletteId)); + colors.range(getPalette(fields.color.paletteId)); } // 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 06edc915e..f3b2908f4 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]); @@ -234,7 +234,7 @@ const useMapState = ( type AreaLayerColors = | { type: "categorical"; - palette: string; + paletteId: string; component: Dimension; domain: string[]; getValue: (d: Observation) => string; @@ -243,7 +243,7 @@ type AreaLayerColors = } | { type: "continuous"; - palette: string; + paletteId: string; component: Measure; domain: [number, number]; // Needed for the legend. @@ -274,7 +274,7 @@ const getNumericalColorScale = ({ data: Observation[]; dataDomain: [number, number]; }) => { - const interpolator = getColorInterpolator(color.palette); + const interpolator = getColorInterpolator(color.paletteId); switch (color.scaleType) { case "continuous": @@ -365,7 +365,7 @@ const useCategoricalColors = ( return { type: "categorical" as const, - palette: colorSpec.palette, + paletteId: colorSpec.paletteId, component, domain, getValue: colorSpec.useAbbreviations @@ -434,7 +434,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 b1654e383..eebbbeb0c 100644 --- a/app/charts/pie/pie-state.tsx +++ b/app/charts/pie/pie-state.tsx @@ -26,7 +26,7 @@ import { useSize } from "@/charts/shared/use-size"; import { PieConfig } from "@/configurator"; import { Dimension, Observation } from "@/domain/data"; import { formatNumberWithUnit, useFormatNumber } from "@/formatters"; -import { getPaletteId } from "@/palettes"; +import { getPalette } from "@/palettes"; import { getSortingOrders, makeDimensionValueSorters, @@ -121,7 +121,7 @@ const usePieState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPaletteId(fields.color.paletteId)); + colors.range(getPalette(fields.color.paletteId)); } // Do not let the scale be implicitly extended by children calling it // on unknown values diff --git a/app/charts/scatterplot/scatterplot-state.tsx b/app/charts/scatterplot/scatterplot-state.tsx index f3bf9ad38..d3deae1cc 100644 --- a/app/charts/scatterplot/scatterplot-state.tsx +++ b/app/charts/scatterplot/scatterplot-state.tsx @@ -26,7 +26,7 @@ import { useSize } from "@/charts/shared/use-size"; import { ScatterPlotConfig, SortingField } from "@/configurator"; import { Observation } from "@/domain/data"; import { useFormatNumber } from "@/formatters"; -import { getPaletteId } from "@/palettes"; +import { getPalette } from "@/palettes"; import { getSortingOrders, makeDimensionValueSorters, @@ -147,7 +147,7 @@ const useScatterplotState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPaletteId(fields.color.paletteId)); + colors.range(getPalette(fields.color.paletteId)); } // Dimensions const { left, bottom } = useChartPadding({ diff --git a/app/charts/table/table-state.tsx b/app/charts/table/table-state.tsx index 82e02aa14..bc2d27727 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/config-types.ts b/app/config-types.ts index 0bcd5567a..33f93a6d4 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -463,7 +463,6 @@ const DivergingPaletteType = t.union([ t.literal("RdYlBu"), t.literal("RdYlGn"), ]); - export type DivergingPaletteType = t.TypeOf; const SequentialPaletteType = t.union([ @@ -474,10 +473,62 @@ const SequentialPaletteType = t.union([ t.literal("purples"), t.literal("reds"), ]); - export type SequentialPaletteType = t.TypeOf; -export type PaletteType = DivergingPaletteType | SequentialPaletteType; +const CategoricalPaletteType = t.union([ + t.literal("dimension"), + 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, + }) + +// Define the Sequential Palette Structure +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"), @@ -506,13 +557,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"), @@ -618,7 +669,7 @@ const NumericalColorField = t.intersection([ t.type({ type: t.literal("numerical"), componentId: t.string, - paletteId: t.union([DivergingPaletteType, SequentialPaletteType]), + paletteId: t.string, }), t.union([ t.type({ @@ -637,6 +688,7 @@ const NumericalColorField = t.intersection([ ]), ColorFieldOpacity, ]); + export type NumericalColorField = t.TypeOf; export type MapColorField = @@ -907,8 +959,9 @@ export const isColorInConfig = ( | LineConfig | PieConfig | ScatterPlotConfig => { - return ["area", "column", "line", "pie", "scatterplot"].includes( - chartConfig.chartType + return ( + !isTableConfig(chartConfig) && + !isMapConfig(chartConfig) ); }; diff --git a/app/configurator/components/chart-controls/color-palette.tsx b/app/configurator/components/chart-controls/color-palette.tsx index 4e132393f..bc7934a46 100644 --- a/app/configurator/components/chart-controls/color-palette.tsx +++ b/app/configurator/components/chart-controls/color-palette.tsx @@ -26,11 +26,11 @@ import { import { mapValueIrisToColor } from "@/configurator/components/ui-helpers"; import { Component, isNumericalMeasure } from "@/domain/data"; import { - DEFAULT_CATEGORICAL_PALETTE_NAME, + DEFAULT_CATEGORICAL_PALETTE_ID, categoricalPalettes, divergingSteppedPalettes, getDefaultCategoricalPalette, - getPaletteId, + getPalette, } from "@/palettes"; import useEvent from "@/utils/use-event"; @@ -97,7 +97,7 @@ export const ColorPalette = ({ } if (isColorInConfig(chartConfig)) { dispatch({ - type: "CHART_PALETTE_CHANGED_NEW", + type: "COLOR_FIELD_SET", value: chartConfig.fields.color.type === "single" ? { @@ -109,7 +109,7 @@ export const ColorPalette = ({ type: chartConfig.fields.color.type, paletteId: palette.value, colorMapping: mapValueIrisToColor({ - palette: palette.value, + paletteId: palette.value, dimensionValues: component.values, }), }, @@ -120,9 +120,9 @@ export const ColorPalette = ({ value: { field, colorConfigPath, - palette: palette.value, + paletteId: palette.value, colorMapping: mapValueIrisToColor({ - palette: palette.value, + paletteId: palette.value, dimensionValues: component.values, }), }, @@ -248,7 +248,7 @@ export const ColorSquare = ({ ); }; -const ColorPaletteControls = ({ +export const ColorPaletteControls = ({ field, colorConfigPath, component, @@ -262,13 +262,13 @@ const ColorPaletteControls = ({ const [, dispatch] = useConfiguratorState(); const chartConfig = getChartConfig(state); - const palette = get( + const paletteId = isColorInConfig(chartConfig) ? get(chartConfig, `fields.color.paletteId`) : get( chartConfig, - `fields["${field}"].${colorConfigPath ? `${colorConfigPath}.` : ""}palette`, - DEFAULT_CATEGORICAL_PALETTE_NAME + `fields["${field}"].${colorConfigPath ? `${colorConfigPath}.` : ""}paletteId`, + DEFAULT_CATEGORICAL_PALETTE_ID ) as string; - const colorMapping = get( + const colorMapping = isColorInConfig(chartConfig) ? get(chartConfig, `fields.color.colorMapping`) : get( chartConfig, `fields["${field}"].${ colorConfigPath ? `${colorConfigPath}.` : "" @@ -283,24 +283,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 = getPaletteId(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 8789be8a7..e1d6c0006 100644 --- a/app/configurator/components/chart-controls/color-picker.tsx +++ b/app/configurator/components/chart-controls/color-picker.tsx @@ -1,10 +1,10 @@ import { Trans } from "@lingui/macro"; import { Box, Button, Popover, styled, Theme, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; -import { HsvaColor } from "@uiw/react-color"; +import { hexToHsva, hsvaToHex } from "@uiw/react-color"; import { color as d3Color } from "d3-color"; import dynamic from "next/dynamic"; -import { MouseEventHandler, useCallback, useRef, useState } from "react"; +import { MouseEventHandler, useCallback, useRef } from "react"; import useDisclosure from "@/components/use-disclosure"; import VisuallyHidden from "@/components/visually-hidden"; @@ -123,16 +123,17 @@ 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 [color, setColor] = useState(undefined); - - const handleColorChange = useCallback((color) => { - setColor(color); - }, []); + const handleColorChange = useCallback( + (color) => { + onChange?.(hsvaToHex(color)); + }, + [onChange] + ); return ( { > diff --git a/app/configurator/components/chart-controls/color-ramp.tsx b/app/configurator/components/chart-controls/color-ramp.tsx index b96b7155f..0170fa8b5 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 732e48b82..b827412a6 100644 --- a/app/configurator/components/chart-options-selector.tsx +++ b/app/configurator/components/chart-options-selector.tsx @@ -525,18 +525,17 @@ const EncodingOptionsPanel = ({ {isComboChartConfig(chartConfig) && encoding.field === "y" && ( )} - {fieldComponent && - encoding.field === "segment" && - (hasSubOptions || hasColorPalette) && ( - - )} + {fieldComponent && (hasSubOptions || hasColorPalette) && ( + + )} {encoding.options?.imputation?.shouldShow(chartConfig, observations) && ( )} @@ -609,6 +608,7 @@ const ChartLayoutOptions = ({ components, hasColorPalette, hasSubOptions, + measures, }: { encoding: EncodingSpec; component: Component | undefined; @@ -616,7 +616,13 @@ const ChartLayoutOptions = ({ components: Component[]; hasColorPalette: boolean; hasSubOptions: boolean; + measures: Measure[]; }) => { + const { fields } = chartConfig; + const values: { id: string; symbol: LegendSymbol }[] = [ + { id: fields.y.componentId, symbol: "line" }, + ]; + return encoding.options || hasColorPalette ? ( @@ -632,11 +638,39 @@ const ChartLayoutOptions = ({ /> )} {hasColorPalette && ( - + <> + ({ + value: id, + label: id, + })), + } as any as Component + } + /> + + {values.map(({ id, symbol }) => { + return ( + + d.id === id)!.label} + symbol={symbol} + /> + + ); + })} + + )} @@ -860,7 +894,7 @@ const ChartComboLineSingleYField = ({ } dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field: "y", @@ -888,7 +922,7 @@ const ChartComboLineSingleYField = ({ if (id !== FIELD_VALUE_NONE) { dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field: "y", @@ -903,7 +937,7 @@ const ChartComboLineSingleYField = ({ )} - ({ id, symbol: "line" }))} measures={measures} /> @@ -977,7 +1011,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", @@ -1002,7 +1036,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", @@ -1015,7 +1049,7 @@ const ChartComboLineDualYField = ({ /> - { const newId = e.target.value as string; dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field: "y", @@ -1117,7 +1151,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", @@ -1130,7 +1164,7 @@ const ChartComboLineColumnYField = ({ /> - ( ({ sortingType, sortingOrder }) => { dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field, @@ -2010,7 +2044,7 @@ const ChartMapBaseLayerSettings = ({ if (chartConfig.baseLayer.locked) { if (map !== null) { dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field: null, @@ -2022,7 +2056,7 @@ const ChartMapBaseLayerSettings = ({ } } else { dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field: null, diff --git a/app/configurator/components/color-picker.tsx b/app/configurator/components/color-picker.tsx index 6d8d3cd41..6f6aef4d4 100644 --- a/app/configurator/components/color-picker.tsx +++ b/app/configurator/components/color-picker.tsx @@ -1,8 +1,7 @@ +"use client"; import { Box } from "@mui/material"; import { makeStyles } from "@mui/styles"; import { - Chrome, - EditableInput, hexToHsva, HsvaColor, hsvaToHex, @@ -10,10 +9,22 @@ import { Saturation, } from "@uiw/react-color"; import { color as d3Color } from "d3-color"; +import dynamic from "next/dynamic"; import { MouseEventHandler, useEffect, useState } from "react"; import Flex from "@/components/flex"; +const ChromePicker = dynamic( + () => import("@uiw/react-color").then((mod) => ({ default: mod.Chrome })), + { ssr: false } +); + +const EditableInputComponent = dynamic( + () => + import("@uiw/react-color").then((mod) => ({ default: mod.EditableInput })), + { ssr: false } +); + const useColorPickerStyles = makeStyles(() => ({ swatches: { gridTemplateColumns: "repeat(auto-fill, minmax(1rem, 1fr))", @@ -34,6 +45,7 @@ const CustomColorPicker = ({ colorSwatches, defaultSelection = { h: 0, s: 0, v: 68, a: 1 }, }: CustomColorPickerProps) => { + // Remove the isBrowser check since we're using client-side only rendering const [hsva, setHsva] = useState(defaultSelection); const classes = useColorPickerStyles(); @@ -41,6 +53,27 @@ const CustomColorPicker = ({ onChange(hsva); }, [hsva, onChange]); + // Wrap the entire component in a client-side only check + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + if (!isClient) { + // Return a placeholder while server-side rendering + return ( + + ); + } + return ( - {colorSwatches?.map((color) => { - return ( - setHsva(hexToHsva(color))} - /> - ); - })} + {colorSwatches?.map((color) => ( + setHsva(hexToHsva(color))} + /> + ))} - setHsva(hexToHsva(`#${value}`))} /> - + import("./chart-controls/color-picker").then((mod) => mod.ColorPickerMenu), + { ssr: false } +); + const useStyles = makeStyles((theme) => ({ root: { display: "flex", @@ -710,7 +716,7 @@ const useMultiFilterColorPicker = (value: string) => { ); const palette = useMemo(() => { - return getPaletteId( + return getPalette( get( chartConfig, `fields["${activeField}"].${ @@ -805,7 +811,7 @@ export const ColorPickerField = ({ const updateColor = useCallback( (value: string) => dispatch({ - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale, field, @@ -817,7 +823,7 @@ export const ColorPickerField = ({ ); const color = get(chartConfig, `fields["${field}"].${path}`); - const palette = getPaletteId(get(chartConfig, `fields["${field}"].palette`)); + const palette = getPalette(get(chartConfig, `fields["${field}"].paletteId`)); return ( Selected filters - {hasColorMapping && colorConfig?.palette === "dimension" && ( + {hasColorMapping && colorConfig?.paletteId === "dimension" && ( diff --git a/app/configurator/components/ui-helpers.ts b/app/configurator/components/ui-helpers.ts index e851f7883..42c1d0c63 100644 --- a/app/configurator/components/ui-helpers.ts +++ b/app/configurator/components/ui-helpers.ts @@ -24,7 +24,7 @@ import { import { RelatedDimensionType, TimeUnit } from "@/graphql/query-hooks"; import { IconName } from "@/icons"; import { getTimeInterval } from "@/intervals"; -import { getPaletteId } from "@/palettes"; +import { getPalette } from "@/palettes"; // FIXME: We should cover more time formats export const timeUnitToParser: Record< @@ -268,11 +268,11 @@ export const getIconName = (name: string): IconName => { const randomComparator = () => (Math.random() > 0.5 ? 1 : -1); export const mapValueIrisToColor = ({ - palette, + paletteId, dimensionValues, random, }: { - palette: string; + paletteId: string; dimensionValues: DimensionValue[]; random?: boolean; }) => { @@ -280,10 +280,10 @@ export const mapValueIrisToColor = ({ return {}; } - const paletteValues = getPaletteId(palette); + const paletteValues = getPalette(paletteId); const colors = dimensionValues.map( (d, i) => - (palette === "dimension" && d.color) || + (paletteId === "dimension" && d.color) || paletteValues[i % paletteValues.length] ); const colorScale = scaleOrdinal() diff --git a/app/configurator/config-form.tsx b/app/configurator/config-form.tsx index 1ed7013be..3b4f17db6 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 aba1d6bb3..4319d0ede 100644 --- a/app/configurator/configurator-state/actions.tsx +++ b/app/configurator/configurator-state/actions.tsx @@ -74,7 +74,7 @@ export type ConfiguratorStateAction = }; } | { - type: "CHART_OPTION_CHANGED"; + type: "COLOR_MAPPING_UPDATED"; value: { locale: Locale; path: string; @@ -94,12 +94,12 @@ export type ConfiguratorStateAction = value: { field: string; colorConfigPath?: string; - palette: string; + paletteId: string; colorMapping: Record; }; } | { - type: "CHART_PALETTE_CHANGED_NEW"; + type: "COLOR_FIELD_SET"; value: ColorField; } | { diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index f8e20d4cc..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", diff --git a/app/configurator/configurator-state/reducer.spec.tsx b/app/configurator/configurator-state/reducer.spec.tsx index 637b39491..e37ce9d99 100644 --- a/app/configurator/configurator-state/reducer.spec.tsx +++ b/app/configurator/configurator-state/reducer.spec.tsx @@ -1102,7 +1102,7 @@ describe("handleChartOptionChanged", () => { } as unknown as ConfiguratorStateConfiguringChart; handleChartOptionChanged(state, { - type: "CHART_OPTION_CHANGED", + type: "COLOR_MAPPING_UPDATED", value: { locale: "en", field: "areaLayer", @@ -1162,7 +1162,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 32dc0753e..df082bc8d 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -413,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; @@ -474,7 +474,7 @@ export const updateColorMapping = ( if (fieldValue) { colorMapping = mapValueIrisToColor({ - palette: fieldValue.palette, + paletteId: fieldValue.paletteId, dimensionValues: values, random, }); @@ -487,7 +487,7 @@ export const updateColorMapping = ( if (fieldValue?.componentId === dimensionId) { colorMapping = mapValueIrisToColor({ - palette: fieldValue.palette, + paletteId: fieldValue.palette, dimensionValues: values, random, }); @@ -649,10 +649,10 @@ const reducer_: Reducer = ( return draft; - case "CHART_OPTION_CHANGED": + case "COLOR_MAPPING_UPDATED": return handleChartOptionChanged(draft, action); - case "CHART_PALETTE_CHANGED_NEW": + case "COLOR_FIELD_SET": if (isConfiguring(draft)) { const chartConfig = getChartConfig(draft); setWith( @@ -675,23 +675,19 @@ const reducer_: Reducer = ( 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 5a355571e..028b93f96 100644 --- a/app/configurator/table/table-chart-options.tsx +++ b/app/configurator/table/table-chart-options.tsx @@ -284,14 +284,14 @@ export const TableColumnOptions = ({ columnColor: "#fff", }; case "category": - const palette = getDefaultCategoricalPaletteId(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.docs.mdx b/app/docs/controls.docs.mdx index 2d78acadc..f9e5ec9e5 100644 --- a/app/docs/controls.docs.mdx +++ b/app/docs/controls.docs.mdx @@ -14,7 +14,7 @@ import { SectionTitle, } from "@/configurator/components/chart-controls/section"; import { IconButton } from "@/configurator/components/icon-button"; -import { getPaletteId } from "@/palettes"; +import { getPalette } from "@/palettes"; import { Canvas, Meta } from "@storybook/blocks"; import * as ControlsStories from "./controls.stories"; diff --git a/app/docs/controls.stories.tsx b/app/docs/controls.stories.tsx index ca9846c41..3935b9b9a 100644 --- a/app/docs/controls.stories.tsx +++ b/app/docs/controls.stories.tsx @@ -9,7 +9,7 @@ import { ControlSection, SectionTitle, } from "@/configurator/components/chart-controls/section"; -import { getPaletteId } from "@/palettes"; +import { getPalette } from "@/palettes"; import { IconButton } from "../configurator/components/icon-button"; @@ -175,7 +175,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/palettes.ts b/app/palettes.ts index 49f03c26d..48a95606e 100644 --- a/app/palettes.ts +++ b/app/palettes.ts @@ -25,7 +25,7 @@ import { } from "d3-scale-chromatic"; import { hasDimensionColors } from "./charts/shared/colors"; -import { DivergingPaletteType, SequentialPaletteType } from "./config-types"; +import { DivergingPaletteType, PaletteType, SequentialPaletteType } from "./config-types"; import { Component } from "./domain/data"; // Colors @@ -36,7 +36,7 @@ export const getDefaultCategoricalPaletteId = ( const hasColors = hasDimensionColors(d); return hasColors ? "dimension" - : previousPaletteName || DEFAULT_CATEGORICAL_PALETTE_NAME; + : previousPaletteName || DEFAULT_CATEGORICAL_PALETTE_ID; }; export const getDefaultCategoricalPalette = ( @@ -53,7 +53,7 @@ export const getDefaultCategoricalPalette = ( } }; -export const getPaletteId = ( +export const getPalette = ( palette?: string, colors?: string[] ): ReadonlyArray => { @@ -96,19 +96,19 @@ export const categoricalPalettes: Array = [ { label: "category10", value: "category10", - colors: getPaletteId("category10"), + colors: getPalette("category10"), }, - { label: "accent", value: "accent", colors: getPaletteId("accent") }, - { label: "dark2", value: "dark2", colors: getPaletteId("dark2") }, - { label: "paired", value: "paired", colors: getPaletteId("paired") }, - { label: "pastel1", value: "pastel1", colors: getPaletteId("pastel1") }, - { label: "pastel2", value: "pastel2", colors: getPaletteId("pastel2") }, - { label: "set1", value: "set1", colors: getPaletteId("set1") }, - { label: "set2", value: "set2", colors: getPaletteId("set2") }, - { label: "set3", value: "set3", colors: getPaletteId("set3") }, + { 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") }, ]; -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 +136,7 @@ const sequentialPaletteKeys: SequentialPaletteType[] = [ "purples", "reds", ]; -const interpolatorByName = { +const interpolatorByName: Record string> = { RdBu: interpolateRdBu, RdYlBu: interpolateRdYlBu, RdYlGn: interpolateRdYlGn, @@ -154,14 +154,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/versioning.ts b/app/utils/chart-config/versioning.ts index cae7468a2..bc96f6f78 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -15,7 +15,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 +676,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 +696,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, From 544d51e766ab83391933e9dd921a563df3b9d0d4 Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Fri, 6 Dec 2024 07:27:16 +0100 Subject: [PATCH 06/21] refactore: Refactored getPalette --- app/charts/area/areas-state.tsx | 7 +- app/charts/chart-config-ui-options.ts | 27 +++-- app/charts/column/columns-grouped-state.tsx | 7 +- app/charts/column/columns-stacked-state.tsx | 1 + app/charts/line/lines-state.tsx | 7 +- app/charts/pie/pie-state.tsx | 7 +- app/charts/scatterplot/scatterplot-state.tsx | 7 +- app/configurator/components/field.tsx | 5 +- app/configurator/components/ui-helpers.ts | 2 +- app/docs/controls.stories.tsx | 2 +- app/palettes.ts | 112 ++++++++++++------- 11 files changed, 130 insertions(+), 54 deletions(-) diff --git a/app/charts/area/areas-state.tsx b/app/charts/area/areas-state.tsx index 566fb15fa..b26d31057 100644 --- a/app/charts/area/areas-state.tsx +++ b/app/charts/area/areas-state.tsx @@ -266,7 +266,12 @@ const useAreasState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.color?.paletteId)); + colors.range( + getPalette({ + paletteId: fields.color?.paletteId, + colorField: fields.color, + }) + ); } colors.unknown(() => undefined); diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts index ea26bf00c..fc4408030 100644 --- a/app/charts/chart-config-ui-options.ts +++ b/app/charts/chart-config-ui-options.ts @@ -57,6 +57,7 @@ import { isTemporalOrdinalDimension, } from "@/domain/data"; import { getDefaultCategoricalPaletteId, getPalette } from "@/palettes"; +import { theme } from "@/themes/federal"; /** * This module controls chart controls displayed in the UI. @@ -559,6 +560,13 @@ const chartConfigOptionsUISpec: ChartSpecs = { delete chartConfig.fields.segment; } }, + options: { + colorPalette: { + type: "single", + paletteId: "dimension", + color: theme.palette.primary.main, + }, + }, }, { field: "x", @@ -613,7 +621,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { colorPalette: { type: "single", paletteId: "dimension", - colorMapping: {}, + color: theme.palette.primary.main, }, imputation: { shouldShow: (chartConfig, data) => { @@ -654,6 +662,11 @@ const chartConfigOptionsUISpec: ChartSpecs = { } }, options: { + colorPalette: { + type: "single", + paletteId: "dimension", + color: theme.palette.primary.main, + }, showStandardError: {}, showConfidenceInterval: {}, }, @@ -773,7 +786,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { colorPalette: { type: "single", paletteId: "dimension", - colorMapping: {}, + color: theme.palette.primary.main, }, useAbbreviations: {}, }, @@ -795,7 +808,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { colorPalette: { type: "single", paletteId: "dimension", - colorMapping: {}, + color: theme.palette.primary.main, }, showStandardError: {}, showConfidenceInterval: {}, @@ -820,7 +833,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { colorPalette: { type: "single", paletteId: "dimension", - colorMapping: {}, + color: theme.palette.primary.main, }, useAbbreviations: {}, }, @@ -917,7 +930,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { colorPalette: { type: "single", paletteId: "dimension", - colorMapping: {}, + color: theme.palette.primary.main, }, useAbbreviations: {}, }, @@ -954,7 +967,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { colorPalette: { type: "single", paletteId: "dimension", - colorMapping: {}, + color: theme.palette.primary.main, }, useAbbreviations: {}, }, @@ -986,7 +999,7 @@ const chartConfigOptionsUISpec: ChartSpecs = { const { chartConfig } = options; const { fields } = chartConfig; const { color } = fields; - const palette = getPalette(color.paletteId); + const palette = getPalette({ paletteId: color.paletteId }); const newColorMapping = Object.fromEntries( ids.map((id, i) => [id, color.colorMapping[i] ?? palette[i]]) ); diff --git a/app/charts/column/columns-grouped-state.tsx b/app/charts/column/columns-grouped-state.tsx index 841b11c9e..2a1fb5b54 100644 --- a/app/charts/column/columns-grouped-state.tsx +++ b/app/charts/column/columns-grouped-state.tsx @@ -211,7 +211,12 @@ const useColumnsGroupedState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.color?.paletteId)); + colors.range( + getPalette({ + paletteId: fields.color?.paletteId, + colorField: fields.color, + }) + ); } colors.unknown(() => undefined); diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx index 081eac12c..d90c9e90c 100644 --- a/app/charts/column/columns-stacked-state.tsx +++ b/app/charts/column/columns-stacked-state.tsx @@ -254,6 +254,7 @@ const useColumnsStackedState = ( } else { colors.domain(allSegments); colors.range(getPalette(fields.color.paletteId)); + colors.range( } colors.unknown(() => undefined); diff --git a/app/charts/line/lines-state.tsx b/app/charts/line/lines-state.tsx index 1fed350e7..7a94ca9db 100644 --- a/app/charts/line/lines-state.tsx +++ b/app/charts/line/lines-state.tsx @@ -215,7 +215,12 @@ const useLinesState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.color.paletteId)); + colors.range( + getPalette({ + paletteId: fields.color?.paletteId, + colorField: fields.color, + }) + ); } // Dimensions diff --git a/app/charts/pie/pie-state.tsx b/app/charts/pie/pie-state.tsx index eebbbeb0c..b350e765b 100644 --- a/app/charts/pie/pie-state.tsx +++ b/app/charts/pie/pie-state.tsx @@ -121,7 +121,12 @@ const usePieState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.color.paletteId)); + 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 diff --git a/app/charts/scatterplot/scatterplot-state.tsx b/app/charts/scatterplot/scatterplot-state.tsx index d3deae1cc..c7974de65 100644 --- a/app/charts/scatterplot/scatterplot-state.tsx +++ b/app/charts/scatterplot/scatterplot-state.tsx @@ -147,7 +147,12 @@ const useScatterplotState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.color.paletteId)); + colors.range( + getPalette({ + paletteId: fields.color.paletteId, + colorField: fields.color, + }) + ); } // Dimensions const { left, bottom } = useChartPadding({ diff --git a/app/configurator/components/field.tsx b/app/configurator/components/field.tsx index f4e871a20..9c34f1c34 100644 --- a/app/configurator/components/field.tsx +++ b/app/configurator/components/field.tsx @@ -823,7 +823,10 @@ export const ColorPickerField = ({ ); const color = get(chartConfig, `fields["${field}"].${path}`); - const palette = getPalette(get(chartConfig, `fields["${field}"].paletteId`)); + const palette = getPalette({ + paletteId: get(chartConfig, `fields["${field}"].paletteId`), + }); + return ( (paletteId === "dimension" && d.color) || diff --git a/app/docs/controls.stories.tsx b/app/docs/controls.stories.tsx index 3935b9b9a..bbd094ae1 100644 --- a/app/docs/controls.stories.tsx +++ b/app/docs/controls.stories.tsx @@ -175,7 +175,7 @@ const ColorPickerStory = { Current (valid) color: {currentColor} setCurrentColor(color)} /> diff --git a/app/palettes.ts b/app/palettes.ts index 48a95606e..e36f0a606 100644 --- a/app/palettes.ts +++ b/app/palettes.ts @@ -25,7 +25,12 @@ import { } from "d3-scale-chromatic"; import { hasDimensionColors } from "./charts/shared/colors"; -import { DivergingPaletteType, PaletteType, SequentialPaletteType } from "./config-types"; +import { + ColorField, + DivergingPaletteType, + PaletteType, + SequentialPaletteType, +} from "./config-types"; import { Component } from "./domain/data"; // Colors @@ -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,16 +110,36 @@ 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_ID = categoricalPalettes[0].value; From 8c0f353ddde74a37221d63fcae22988716c961f1 Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Fri, 6 Dec 2024 07:27:35 +0100 Subject: [PATCH 07/21] refactor: Refactored getPalette --- app/charts/column/columns-stacked-state.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/charts/column/columns-stacked-state.tsx b/app/charts/column/columns-stacked-state.tsx index d90c9e90c..fd0f38475 100644 --- a/app/charts/column/columns-stacked-state.tsx +++ b/app/charts/column/columns-stacked-state.tsx @@ -253,8 +253,12 @@ const useColumnsStackedState = ( colors.range(orderedSegmentLabelsAndColors.map((s) => s.color)); } else { colors.domain(allSegments); - colors.range(getPalette(fields.color.paletteId)); colors.range( + getPalette({ + paletteId: fields.color.paletteId, + colorField: fields.color, + }) + ); } colors.unknown(() => undefined); From 04b816138bdc800aaa9dafca564077c97fe58fea Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Fri, 6 Dec 2024 07:28:06 +0100 Subject: [PATCH 08/21] refactor: improved formating --- app/config-types.ts | 53 +++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/app/config-types.ts b/app/config-types.ts index 33f93a6d4..b83328099 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -489,43 +489,43 @@ const CategoricalPaletteType = t.union([ t.literal("tableau10"), ]); -export type CategoricalPaletteType = t.TypeOf;; +export type CategoricalPaletteType = t.TypeOf; -const DivergingPalette = - t.type({ - type: t.literal("diverging"), - paletteId: DivergingPaletteType, - name: DivergingPaletteType, - }) +const DivergingPalette = t.type({ + type: t.literal("diverging"), + paletteId: DivergingPaletteType, + name: DivergingPaletteType, +}); // Define the Sequential Palette Structure -const SequentialPalette = - t.type({ - type: t.literal("sequential"), - paletteId: SequentialPaletteType, - name: SequentialPaletteType, - }) +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 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")]), +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 + CustomPalette, ]); export type PaletteType = t.TypeOf; @@ -959,10 +959,7 @@ export const isColorInConfig = ( | LineConfig | PieConfig | ScatterPlotConfig => { - return ( - !isTableConfig(chartConfig) && - !isMapConfig(chartConfig) - ); + return !isTableConfig(chartConfig) && !isMapConfig(chartConfig); }; export const isSortingInConfig = ( From f1229b38d80c7b5b0d98ad77479e2b210c444f8c Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Fri, 6 Dec 2024 07:28:36 +0100 Subject: [PATCH 09/21] refactor: extended color palettes --- .../chart-controls/color-palette.tsx | 28 +++++++++++-------- .../components/chart-options-selector.tsx | 18 ++++++++---- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/app/configurator/components/chart-controls/color-palette.tsx b/app/configurator/components/chart-controls/color-palette.tsx index bc7934a46..1993349a2 100644 --- a/app/configurator/components/chart-controls/color-palette.tsx +++ b/app/configurator/components/chart-controls/color-palette.tsx @@ -262,18 +262,22 @@ export const ColorPaletteControls = ({ const [, dispatch] = useConfiguratorState(); const chartConfig = getChartConfig(state); - const paletteId = isColorInConfig(chartConfig) ? get(chartConfig, `fields.color.paletteId`) : get( - chartConfig, - `fields["${field}"].${colorConfigPath ? `${colorConfigPath}.` : ""}paletteId`, - DEFAULT_CATEGORICAL_PALETTE_ID - ) 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 = isColorInConfig(chartConfig) ? get(chartConfig, `fields.color.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( () => @@ -292,7 +296,7 @@ export const ColorPaletteControls = ({ ); if (colorMapping) { - const currentPalette = getPalette(paletteId); + const currentPalette = getPalette({ paletteId }); const colorMappingColors = Object.values(colorMapping); const nbMatchedColors = colorMappingColors.length; diff --git a/app/configurator/components/chart-options-selector.tsx b/app/configurator/components/chart-options-selector.tsx index b827412a6..c42e2c69a 100644 --- a/app/configurator/components/chart-options-selector.tsx +++ b/app/configurator/components/chart-options-selector.tsx @@ -619,9 +619,13 @@ const ChartLayoutOptions = ({ measures: Measure[]; }) => { const { fields } = chartConfig; - const values: { id: string; symbol: LegendSymbol }[] = [ - { id: fields.y.componentId, symbol: "line" }, - ]; + const values: { id: string; symbol: LegendSymbol }[] = + chartConfig.fields.color.type === "single" + ? [{ id: fields.y.componentId, symbol: "line" }] + : Object.keys(chartConfig.fields.color.colorMapping).map((key) => ({ + id: key, + symbol: "line", + })); return encoding.options || hasColorPalette ? ( @@ -662,8 +666,12 @@ const ChartLayoutOptions = ({ d.id === id)!.label} + path={ + chartConfig.fields.color.type === "single" + ? "color" + : `colorMapping["${id}"]` + } + label={measures.find((d) => d.id === id)?.label ?? ""} symbol={symbol} /> From 9f4dc02a767542d371d72b2d843ab1972b01fb16 Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:57:13 +0100 Subject: [PATCH 10/21] fix: Changed Type name --- app/charts/chart-config-ui-options.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts index fc4408030..9b94e5e00 100644 --- a/app/charts/chart-config-ui-options.ts +++ b/app/charts/chart-config-ui-options.ts @@ -66,11 +66,11 @@ import { theme } from "@/themes/federal"; type BaseEncodingFieldType = "animation"; type MapEncodingFieldType = "baseLayer" | "areaLayer" | "symbolLayer"; -type XYEncodingFieldType = "x" | "y" | "segment" | "color"; +type RegularChartEncodingType = "x" | "y" | "segment" | "color"; export type EncodingFieldType = | BaseEncodingFieldType | MapEncodingFieldType - | XYEncodingFieldType; + | RegularChartEncodingType; type OnEncodingOptionChange = ( value: V, From 9684d6f22477f1bfdbf38d8e1cec6c2aa8641389 Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:58:20 +0100 Subject: [PATCH 11/21] fix: Changed column color handling to conform to other Chart types --- app/charts/column/columns-state.tsx | 19 +++++++++++++++++++ app/charts/column/columns.tsx | 21 ++++++++++++--------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app/charts/column/columns-state.tsx b/app/charts/column/columns-state.tsx index 97bc98aeb..42be7d60d 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 ed51e6f0e..06030bd47 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"; @@ -82,8 +81,16 @@ export const ErrorWhiskers = () => { }; export const Columns = () => { - const { chartData, bounds, getX, xScale, getY, yScale, getRenderingKey } = - useChartState() as ColumnsState; + const { + chartData, + bounds, + getX, + xScale, + getY, + yScale, + getRenderingKey, + colors, + } = useChartState() as ColumnsState; const theme = useTheme(); const { margins } = bounds; const ref = useRef(null); @@ -92,10 +99,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 +107,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 +126,7 @@ export const Columns = () => { xScale, yScale, y0, - theme.palette.secondary.main, + colors, getRenderingKey, ]); From bb8272ab86f71c5b7bb8a6901c6b5bc8249742fe Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:05:39 +0100 Subject: [PATCH 12/21] fix: removed unnecessary client check --- app/configurator/components/color-picker.tsx | 21 -------------------- 1 file changed, 21 deletions(-) diff --git a/app/configurator/components/color-picker.tsx b/app/configurator/components/color-picker.tsx index 6f6aef4d4..6b639ab33 100644 --- a/app/configurator/components/color-picker.tsx +++ b/app/configurator/components/color-picker.tsx @@ -53,27 +53,6 @@ const CustomColorPicker = ({ onChange(hsva); }, [hsva, onChange]); - // Wrap the entire component in a client-side only check - const [isClient, setIsClient] = useState(false); - - useEffect(() => { - setIsClient(true); - }, []); - - if (!isClient) { - // Return a placeholder while server-side rendering - return ( - - ); - } - return ( Date: Wed, 11 Dec 2024 15:06:13 +0100 Subject: [PATCH 13/21] fix: improved state for color picker changes --- .../components/chart-controls/color-picker.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/configurator/components/chart-controls/color-picker.tsx b/app/configurator/components/chart-controls/color-picker.tsx index e1d6c0006..adcd2840b 100644 --- a/app/configurator/components/chart-controls/color-picker.tsx +++ b/app/configurator/components/chart-controls/color-picker.tsx @@ -4,7 +4,7 @@ import { makeStyles } from "@mui/styles"; import { hexToHsva, hsvaToHex } from "@uiw/react-color"; import { color as d3Color } from "d3-color"; import dynamic from "next/dynamic"; -import { MouseEventHandler, useCallback, useRef } from "react"; +import { MouseEventHandler, useCallback, useMemo, useRef } from "react"; import useDisclosure from "@/components/use-disclosure"; import VisuallyHidden from "@/components/visually-hidden"; @@ -128,11 +128,19 @@ export const ColorPickerMenu = (props: Props) => { const buttonRef = useRef(null); const popoverRef = useRef(null); + const initalSelected = useMemo( + () => hexToHsva(selectedColor), + [selectedColor] + ); + const handleColorChange = useCallback( (color) => { - onChange?.(hsvaToHex(color)); + const newHex = hsvaToHex(color); + if (newHex !== selectedColor) { + onChange?.(newHex); + } }, - [onChange] + [onChange, selectedColor] ); return ( @@ -165,7 +173,7 @@ export const ColorPickerMenu = (props: Props) => { > From f20ac398e79e9cd1d93856fa77f786607501e98d Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:19:19 +0100 Subject: [PATCH 14/21] refactor: refactored old way of handling colors --- .../components/chart-options-selector.tsx | 79 ++++++++----------- app/configurator/components/field.tsx | 33 ++++---- app/configurator/components/filters.tsx | 29 +++---- 3 files changed, 62 insertions(+), 79 deletions(-) diff --git a/app/configurator/components/chart-options-selector.tsx b/app/configurator/components/chart-options-selector.tsx index c42e2c69a..62e3fccc8 100644 --- a/app/configurator/components/chart-options-selector.tsx +++ b/app/configurator/components/chart-options-selector.tsx @@ -48,6 +48,7 @@ import { ImputationType, imputationTypes, isAnimationInConfig, + isColorInConfig, isComboChartConfig, isConfiguring, isTableConfig, @@ -314,6 +315,10 @@ const EncodingOptionsPanel = ({ 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", @@ -618,14 +623,15 @@ const ChartLayoutOptions = ({ hasSubOptions: boolean; measures: Measure[]; }) => { - const { fields } = chartConfig; - const values: { id: string; symbol: LegendSymbol }[] = - chartConfig.fields.color.type === "single" - ? [{ id: fields.y.componentId, symbol: "line" }] + const withColorField = isColorInConfig(chartConfig); + const values: { id: string; symbol: LegendSymbol }[] = withColorField + ? 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 ? ( @@ -641,45 +647,30 @@ const ChartLayoutOptions = ({ disabled={!component} /> )} - {hasColorPalette && ( - <> - ({ - value: id, - label: id, - })), - } as any as Component - } + <> + ({ + value: id, + label: id, + })), + } as any as Component + } + /> + {withColorField && chartConfig.fields.color.type === "single" && ( + d.id === values[0].id)!.label} /> - - {values.map(({ id, symbol }) => { - return ( - - d.id === id)?.label ?? ""} - symbol={symbol} - /> - - ); - })} - - - )} + )} + ) : null; @@ -1405,7 +1396,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"} /> ) )} diff --git a/app/configurator/components/field.tsx b/app/configurator/components/field.tsx index 9c34f1c34..2256e7e72 100644 --- a/app/configurator/components/field.tsx +++ b/app/configurator/components/field.tsx @@ -42,6 +42,7 @@ import useDisclosure from "@/components/use-disclosure"; import { ChartConfig, getChartConfig, + isColorInConfig, useChartConfigFilters, } from "@/config-types"; import { @@ -691,14 +692,17 @@ const useMultiFilterColorPicker = (value: string) => { const filters = useChartConfigFilters(chartConfig); const { dimensionId, colorConfigPath } = useMultiFilterContext(); const { activeField } = chartConfig; + + const colorField = isColorInConfig(chartConfig) ? "color" : activeField; + const onChange = useCallback( (color: string) => { - if (activeField) { + if (colorField) { dispatch({ type: "CHART_COLOR_CHANGED", value: { - field: activeField, - colorConfigPath, + field: colorField, + colorConfigPath: isColorInConfig(chartConfig) ? "" : colorField, color, value, }, @@ -706,25 +710,23 @@ const useMultiFilterColorPicker = (value: string) => { } }, - [colorConfigPath, dispatch, activeField, value] + [dispatch, colorField, value, chartConfig] ); const path = colorConfigPath ? `${colorConfigPath}.` : ""; - const color = get( - chartConfig, - `fields["${activeField}"].${path}colorMapping["${value}"]` - ); + + const color = get(chartConfig, `fields["${colorField}"].${path}["${value}"]`); const palette = useMemo(() => { - return getPalette( - get( + return getPalette({ + paletteId: get( chartConfig, - `fields["${activeField}"].${ + `fields["${colorField}"].${ colorConfigPath ? `${colorConfigPath}.` : "" - }palette` - ) - ); - }, [chartConfig, colorConfigPath, activeField]); + }paletteId` + ), + }); + }, [chartConfig, colorConfigPath, colorField]); const checkedState = dimensionId ? isMultiFilterFieldChecked(filters, dimensionId, value) @@ -827,7 +829,6 @@ export const ColorPickerField = ({ paletteId: get(chartConfig, `fields["${field}"].paletteId`), }); - return ( { return joinParents(node?.parents); }; -const getColorConfig = ( - chartConfig: ChartConfig, - colorConfigPath: string | undefined -) => { - if (!chartConfig.activeField) { - return; - } - - const path = colorConfigPath - ? [chartConfig.activeField, colorConfigPath] - : [chartConfig.activeField]; - - return get(chartConfig.fields, path) as GenericField | undefined; +const getColorConfig = (chartConfig: ChartConfig) => { + return get(chartConfig.fields, ["color"]) as ColorField | undefined; }; const FilterControls = ({ @@ -322,15 +311,17 @@ 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]); const interactiveFilterProps = useInteractiveFiltersToggle("legend"); const chartSymbol = getChartSymbol(chartConfig.chartType); From 5c386ff63bd3e2b491d88445fd2c1146be6a9e69 Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:19:39 +0100 Subject: [PATCH 15/21] chore: Added comment for dynamic import reasoning --- app/configurator/components/chart-controls/color-picker.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/configurator/components/chart-controls/color-picker.tsx b/app/configurator/components/chart-controls/color-picker.tsx index adcd2840b..4903bc0b5 100644 --- a/app/configurator/components/chart-controls/color-picker.tsx +++ b/app/configurator/components/chart-controls/color-picker.tsx @@ -10,6 +10,7 @@ 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 } From 1e2147d33d3381fe5ea7e7d270541e4090a263ff Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:20:26 +0100 Subject: [PATCH 16/21] chore: Added color control translations --- app/locales/de/messages.po | 4 ++++ app/locales/en/messages.po | 4 ++++ app/locales/fr/messages.po | 4 ++++ app/locales/it/messages.po | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/app/locales/de/messages.po b/app/locales/de/messages.po index 60d57fc13..20eba0e0f 100644 --- a/app/locales/de/messages.po +++ b/app/locales/de/messages.po @@ -945,6 +945,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 19bffe116..a471ca390 100644 --- a/app/locales/en/messages.po +++ b/app/locales/en/messages.po @@ -945,6 +945,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 7c0ac3b0f..89fb07d00 100644 --- a/app/locales/fr/messages.po +++ b/app/locales/fr/messages.po @@ -945,6 +945,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 30781c816..0d13bcaee 100644 --- a/app/locales/it/messages.po +++ b/app/locales/it/messages.po @@ -945,6 +945,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" From adec4ab5bf9f2ac5a96180b025ab1765ac19f51e Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:47:12 +0100 Subject: [PATCH 17/21] fix: Added chartConfig fields type check --- app/charts/chart-config-ui-options.ts | 3 ++- app/config-types.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/charts/chart-config-ui-options.ts b/app/charts/chart-config-ui-options.ts index 9b94e5e00..07a1576b0 100644 --- a/app/charts/chart-config-ui-options.ts +++ b/app/charts/chart-config-ui-options.ts @@ -36,6 +36,7 @@ import { SortingOrder, SortingType, TableConfig, + fieldHasComponentId, getAnimationField, isSortingInConfig, makeMultiFilter, @@ -415,7 +416,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]) ); diff --git a/app/config-types.ts b/app/config-types.ts index b83328099..c80d4b817 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -867,6 +867,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]: { componentId: string; useAbbreviations?: boolean } } + ); + + return validFields as { + [s: string]: { componentId: string } & { + useAbbreviations?: boolean | undefined; + }; + }; +}; + export const isAreaConfig = ( chartConfig: ChartConfig ): chartConfig is AreaConfig => { From d25212f995c4cc855a3aca2ffd3adda0f7829a45 Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:52:18 +0100 Subject: [PATCH 18/21] fix: Added missing getColorLabel type --- app/charts/shared/chart-state.ts | 2 +- app/configurator/components/color-picker.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/charts/shared/chart-state.ts b/app/charts/shared/chart-state.ts index fcad2d852..55c2c52ef 100644 --- a/app/charts/shared/chart-state.ts +++ b/app/charts/shared/chart-state.ts @@ -86,7 +86,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/configurator/components/color-picker.tsx b/app/configurator/components/color-picker.tsx index 6b639ab33..61dc8842e 100644 --- a/app/configurator/components/color-picker.tsx +++ b/app/configurator/components/color-picker.tsx @@ -45,7 +45,6 @@ const CustomColorPicker = ({ colorSwatches, defaultSelection = { h: 0, s: 0, v: 68, a: 1 }, }: CustomColorPickerProps) => { - // Remove the isBrowser check since we're using client-side only rendering const [hsva, setHsva] = useState(defaultSelection); const classes = useColorPickerStyles(); From 9070012ea3d59c2c6b4993eeaa7e14a37c7d5164 Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:01:19 +0100 Subject: [PATCH 19/21] fix: Fixed reverting segmented colors back to single colors --- app/configurator/configurator-state/reducer.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/configurator/configurator-state/reducer.tsx b/app/configurator/configurator-state/reducer.tsx index df082bc8d..c55eadf6b 100644 --- a/app/configurator/configurator-state/reducer.tsx +++ b/app/configurator/configurator-state/reducer.tsx @@ -38,6 +38,7 @@ import { GenericFields, getChartConfig, isAreaConfig, + isColorInConfig, isTableConfig, makeMultiFilter, } from "@/config-types"; @@ -48,6 +49,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"; @@ -310,7 +312,6 @@ const transitionStepPrevious = ( if (draft.state === "INITIAL" || draft.state === "SELECTING_DATASET") { return draft; } - switch (stepTo) { case "SELECTING_DATASET": return { @@ -644,6 +645,12 @@ 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; + } + } } } From c5e117ef1ad4ae64b025578b085f41b37999723f Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:04:37 +0100 Subject: [PATCH 20/21] fix: removed exsesive theme import --- app/charts/column/columns.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/charts/column/columns.tsx b/app/charts/column/columns.tsx index 06030bd47..52c0da6d3 100644 --- a/app/charts/column/columns.tsx +++ b/app/charts/column/columns.tsx @@ -12,7 +12,6 @@ import { renderWhiskers, } from "@/charts/shared/rendering-utils"; import { useTransitionStore } from "@/stores/transition"; -import { useTheme } from "@/themes"; export const ErrorWhiskers = () => { const { @@ -91,7 +90,6 @@ export const Columns = () => { getRenderingKey, colors, } = useChartState() as ColumnsState; - const theme = useTheme(); const { margins } = bounds; const ref = useRef(null); const enableTransition = useTransitionStore((state) => state.enable); From f128f592a5f1e10d7e33e9f912f10fae561a50c1 Mon Sep 17 00:00:00 2001 From: Noah Onyejese <129368606+noahonyejese@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:31:32 +0100 Subject: [PATCH 21/21] fix: Fixed Type issues --- app/charts/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/charts/index.ts b/app/charts/index.ts index f00711925..e50870cf9 100644 --- a/app/charts/index.ts +++ b/app/charts/index.ts @@ -65,6 +65,7 @@ import { Component, Dimension, DimensionType, + DimensionValue, GeoCoordinatesDimension, GeoShapesDimension, getCategoricalDimensions, @@ -622,7 +623,7 @@ export const getInitialConfig = ( dimensionValues: yComponentIds.map((id) => ({ value: id, label: id, - })), + })) as DimensionValue[], }), }, }, @@ -1626,7 +1627,7 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { dimensionValues: [leftMeasure.id, rightMeasureId].map((id) => ({ value: id, label: id, - })), + })) as DimensionValue[], }), }; }); @@ -1720,7 +1721,7 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = { dimensionValues: [leftMeasure.id, lineComponentId].map((id) => ({ value: id, label: id, - })), + })) as DimensionValue[], }), }; });