From e12f169319bb29d5b5dd69b075f5bcbdd9dcf020 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 5 Apr 2026 02:56:09 -0700 Subject: [PATCH] fix: normalize style with full defaults when chart enters Redux All style sub-objects (title, legend, xAxis, yAxis, dataLabel, background, border, animation) are guaranteed to exist in Redux. This fixes: - Style config panels showing blank (returned null for missing fields) - Style changes not applying (updateStyle now deep-merges sub-objects) - Color palette changes not persisting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/adapters/state/redux/chartSlice.ts | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/frontend/src/adapters/state/redux/chartSlice.ts b/frontend/src/adapters/state/redux/chartSlice.ts index cce66a0..7579952 100644 --- a/frontend/src/adapters/state/redux/chartSlice.ts +++ b/frontend/src/adapters/state/redux/chartSlice.ts @@ -4,6 +4,33 @@ import { type FieldBinding } from '@/domain/entities/FieldBinding'; import { type StyleConfig } from '@/domain/entities/StyleConfig'; import { type SortConfig } from '@/domain/valueObjects/SortOrder'; +const DEFAULT_STYLE: StyleConfig = { + title: { text: '', visible: false, fontSize: 16, fontWeight: 'bold', color: '#333', align: 'left' }, + colors: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4'], + legend: { visible: true, position: 'top', orient: 'horizontal', fontSize: 12, color: '#666' }, + xAxis: { visible: true, labelVisible: true, labelFontSize: 12, labelColor: '#666', labelRotation: 0, titleVisible: false, titleText: '', gridVisible: true, gridColor: '#eee' }, + yAxis: { visible: true, labelVisible: true, labelFontSize: 12, labelColor: '#666', labelRotation: 0, titleVisible: false, titleText: '', gridVisible: true, gridColor: '#eee' }, + dataLabel: { visible: false, fontSize: 12, color: '#333', position: 'outside', format: 'value' }, + background: { color: 'transparent', opacity: 1, borderRadius: 4 }, + border: { visible: false, color: '#ddd', width: 1, style: 'solid' }, + animation: { enabled: true, duration: 500, easing: 'ease-out' }, +}; + +function ensureStyle(style: any): StyleConfig { + if (!style || typeof style !== 'object') return { ...DEFAULT_STYLE }; + return { + title: { ...DEFAULT_STYLE.title, ...(style.title ?? {}) }, + colors: (Array.isArray(style.colors) && style.colors.length > 0) ? style.colors : DEFAULT_STYLE.colors, + legend: { ...DEFAULT_STYLE.legend, ...(style.legend ?? {}) }, + xAxis: { ...DEFAULT_STYLE.xAxis, ...(style.xAxis ?? {}) }, + yAxis: { ...DEFAULT_STYLE.yAxis, ...(style.yAxis ?? {}) }, + dataLabel: { ...DEFAULT_STYLE.dataLabel, ...(style.dataLabel ?? {}) }, + background: { ...DEFAULT_STYLE.background, ...(style.background ?? {}) }, + border: { ...DEFAULT_STYLE.border, ...(style.border ?? {}) }, + animation: { ...DEFAULT_STYLE.animation, ...(style.animation ?? {}) }, + }; +} + interface ChartState { charts: ChartInstance[]; activeChartId: string | null; @@ -19,7 +46,8 @@ const chartSlice = createSlice({ initialState, reducers: { addChart(state, action: PayloadAction) { - state.charts.push(action.payload); + const chart = { ...action.payload, style: ensureStyle(action.payload.style) }; + state.charts.push(chart); }, removeChart(state, action: PayloadAction) { state.charts = state.charts.filter((c) => c.id !== action.payload); @@ -46,7 +74,21 @@ const chartSlice = createSlice({ ) { const chart = state.charts.find((c) => c.id === action.payload.chartId); if (chart) { - chart.style = { ...chart.style, ...action.payload.style }; + // Deep merge each sub-object + const incoming = action.payload.style; + const current = chart.style as StyleConfig; + chart.style = { + ...current, + ...incoming, + title: incoming.title ? { ...current.title, ...incoming.title } : current.title, + legend: incoming.legend ? { ...current.legend, ...incoming.legend } : current.legend, + xAxis: incoming.xAxis ? { ...current.xAxis, ...incoming.xAxis } : current.xAxis, + yAxis: incoming.yAxis ? { ...current.yAxis, ...incoming.yAxis } : current.yAxis, + dataLabel: incoming.dataLabel ? { ...current.dataLabel, ...incoming.dataLabel } : current.dataLabel, + background: incoming.background ? { ...current.background, ...incoming.background } : current.background, + border: incoming.border ? { ...current.border, ...incoming.border } : current.border, + animation: incoming.animation ? { ...current.animation, ...incoming.animation } : current.animation, + }; chart.updatedAt = new Date().toISOString(); } },