Skip to content

Commit 02916a1

Browse files
authored
ui: Tidy up chart theming (#4905)
- Centralize theming in EchartView to avoid duplication. - Avoid depending on `pf-theme-provider` and remove fallback to documentElement as this won't have the CSS variables set. - Fixes reactivity in scroll position chart in chrome scroll plugin - Use themes rather than options to specify colors, which avoids having to fiddle around with injecting theme colors into options.
1 parent 728eb56 commit 02916a1

File tree

14 files changed

+162
-167
lines changed

14 files changed

+162
-167
lines changed

ui/src/base/assert.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ export function assertDefined<T>(value: T | undefined, optMsg?: string): T {
4141
return value;
4242
}
4343

44+
// Asserts that the value is an instance of the given class. Returns the value
45+
// with a narrowed type if the assertion passes, otherwise throws an error.
4446
export function assertIsInstance<T>(
4547
value: unknown,
46-
clazz: Function,
48+
clazz: abstract new (...args: never[]) => T,
4749
optMsg?: string,
4850
): T {
4951
assertTrue(

ui/src/components/widgets/charts/bar_chart.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
buildBrushOption,
2323
buildTooltipOption,
2424
} from './chart_option_builder';
25-
import {getChartThemeColors} from './chart_theme';
2625

2726
/**
2827
* A single bar in the bar chart.
@@ -168,8 +167,6 @@ function buildBarOption(
168167
} = attrs;
169168
const fmtDimension = formatDimension ?? String;
170169
const fmtMeasure = formatMeasure ?? formatNumber;
171-
172-
const theme = getChartThemeColors();
173170
const horizontal = orientation === 'horizontal';
174171
const labels = data.items.map((item) => fmtDimension(item.label));
175172

@@ -209,7 +206,6 @@ function buildBarOption(
209206

210207
const option: Record<string, unknown> = {
211208
animation: false,
212-
color: [...theme.chartColors],
213209
grid: buildGridOption({
214210
bottom: dimensionLabel && !horizontal ? 45 : 25,
215211
}),
@@ -228,11 +224,10 @@ function buildBarOption(
228224
type: 'bar',
229225
data: data.items.map((item) => item.value),
230226
itemStyle: barColor !== undefined ? {color: barColor} : undefined,
231-
emphasis: {
232-
itemStyle: {
233-
color: barHoverColor ?? theme.accentColor,
234-
},
235-
},
227+
emphasis:
228+
barHoverColor !== undefined
229+
? {itemStyle: {color: barHoverColor}}
230+
: undefined,
236231
},
237232
],
238233
};
@@ -247,7 +242,7 @@ function buildBarOption(
247242
option.toolbox = {show: false};
248243
}
249244

250-
return option as EChartsCoreOption;
245+
return option;
251246
}
252247

253248
function buildBarEventHandlers(

ui/src/components/widgets/charts/boxplot.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
buildGridOption,
2222
buildTooltipOption,
2323
} from './chart_option_builder';
24-
import {getChartThemeColors} from './chart_theme';
2524

2625
/**
2726
* A single box in a boxplot chart.
@@ -132,7 +131,6 @@ function buildBoxplotOption(
132131
} = attrs;
133132
const fmtVal = formatValue ?? formatNumber;
134133
const horizontal = orientation === 'horizontal';
135-
const theme = getChartThemeColors();
136134

137135
const showXAxisGrid = gridLines === 'vertical' || gridLines === 'both';
138136
const showYAxisGrid = gridLines === 'horizontal' || gridLines === 'both';
@@ -177,7 +175,6 @@ function buildBoxplotOption(
177175

178176
const option: Record<string, unknown> = {
179177
animation: false,
180-
color: [...theme.chartColors],
181178
grid: buildGridOption({
182179
bottom: horizontal ? 25 : categoryLabel ? 40 : 25,
183180
}),

ui/src/components/widgets/charts/chart_option_builder.ts

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
/**
16+
* Chart option builder utilities.
17+
*
18+
* These functions build ECharts option objects with a consistent structure.
19+
* Theme colors are NOT included here - EChartView applies theme colors
20+
* automatically by reading CSS variables from the DOM element.
21+
*/
22+
1523
import type {EChartsCoreOption} from 'echarts/core';
16-
import {getChartThemeColors} from './chart_theme';
1724

1825
/**
1926
* Configuration for an axis in a chart.
@@ -48,35 +55,28 @@ export interface BrushConfig {
4855

4956
/**
5057
* Build an axis option from config.
51-
* Explicitly includes theme colors because ECharts doesn't deep merge
52-
* option objects with theme objects - setting axisLabel overrides the theme's
53-
* axisLabel entirely, so we must include colors here.
58+
* Theme colors are applied by EChartView, so we omit color settings here.
5459
*/
5560
export function buildAxisOption(
5661
config: AxisConfig,
5762
isXAxis: boolean,
5863
): Record<string, unknown> {
59-
const theme = getChartThemeColors();
6064
const axis: Record<string, unknown> = {
6165
type: config.type,
6266
name: config.name,
6367
nameLocation: isXAxis ? ('middle' as const) : ('end' as const),
6468
nameGap: config.nameGap ?? (isXAxis ? 25 : 10),
65-
nameTextStyle: {fontSize: 11, color: theme.textColor},
69+
nameTextStyle: {fontSize: 11},
6670
axisLabel: {
6771
fontSize: 10,
68-
color: theme.textColor,
6972
...(config.formatter !== undefined && {formatter: config.formatter}),
7073
...(config.labelOverflow !== undefined && {
7174
overflow: config.labelOverflow,
7275
}),
7376
...(config.labelWidth !== undefined && {width: config.labelWidth}),
7477
},
75-
axisTick: {lineStyle: {color: theme.borderColor}},
76-
axisLine: {lineStyle: {color: theme.borderColor}},
7778
splitLine: {
7879
show: config.showSplitLine ?? false,
79-
lineStyle: {color: theme.borderColor},
8080
},
8181
};
8282

@@ -100,7 +100,7 @@ export function buildAxisOption(
100100
}
101101

102102
/**
103-
* Build a themed grid option.
103+
* Build a grid option.
104104
*/
105105
export function buildGridOption(opts?: {
106106
top?: number;
@@ -119,28 +119,23 @@ export function buildGridOption(opts?: {
119119
}
120120

121121
/**
122-
* Build a themed tooltip option.
123-
* Explicitly includes theme colors so tooltips adapt when the theme changes.
122+
* Build a tooltip option.
123+
* Theme colors are applied by EChartView.
124124
* Extra options are merged in (e.g. trigger, formatter).
125125
*/
126126
export function buildTooltipOption(
127127
extra?: Record<string, unknown>,
128128
): Record<string, unknown> {
129-
const theme = getChartThemeColors();
130129
return {
131-
backgroundColor: theme.backgroundColor,
132-
borderColor: theme.borderColor,
133-
textStyle: {color: theme.textColor},
134130
...extra,
135131
};
136132
}
137133

138134
/**
139135
* Build a brush configuration.
140-
* Uses accent color from theme (ECharts doesn't theme brush colors).
136+
* Theme colors are applied by EChartView.
141137
*/
142138
export function buildBrushOption(config: BrushConfig): Record<string, unknown> {
143-
const theme = getChartThemeColors();
144139
return {
145140
...(config.xAxisIndex !== undefined && {xAxisIndex: config.xAxisIndex}),
146141
...(config.yAxisIndex !== undefined && {yAxisIndex: config.yAxisIndex}),
@@ -149,7 +144,6 @@ export function buildBrushOption(config: BrushConfig): Record<string, unknown> {
149144
brushStyle: {
150145
borderWidth: 1,
151146
color: 'rgba(0, 0, 0, 0.1)',
152-
borderColor: theme.accentColor,
153147
},
154148
throttleType: 'debounce' as const,
155149
throttleDelay: 100,
@@ -158,13 +152,11 @@ export function buildBrushOption(config: BrushConfig): Record<string, unknown> {
158152

159153
/**
160154
* Build a legend option.
161-
* Explicitly includes theme colors because ECharts doesn't deep merge
162-
* option objects with theme objects.
155+
* Theme colors are applied by EChartView.
163156
*/
164157
export function buildLegendOption(
165158
position: 'top' | 'right' = 'top',
166159
): Record<string, unknown> {
167-
const theme = getChartThemeColors();
168160
if (position === 'right') {
169161
return {
170162
show: true,
@@ -178,7 +170,6 @@ export function buildLegendOption(
178170
width: 120,
179171
overflow: 'truncate',
180172
ellipsis: '\u2026',
181-
color: theme.textColor,
182173
},
183174
tooltip: {show: true},
184175
pageButtonPosition: 'end',
@@ -187,17 +178,16 @@ export function buildLegendOption(
187178
return {
188179
show: true,
189180
top: 0,
190-
textStyle: {fontSize: 10, color: theme.textColor},
181+
textStyle: {fontSize: 10},
191182
};
192183
}
193184

194185
/**
195186
* Build a complete base chart option with grid, axes, and optional
196187
* tooltip/brush/legend. Charts add their own `series` on top.
197188
*
198-
* The `color` array (series colors) and tooltip theme colors are included
199-
* so that charts update automatically when the Mithril component re-renders
200-
* after a theme change — without needing to reinitialize the ECharts instance.
189+
* Theme colors (series colors, text colors, etc.) are applied by EChartView
190+
* which reads CSS variables from the DOM element.
201191
*/
202192
export function buildChartOption(config: {
203193
readonly grid?: Parameters<typeof buildGridOption>[0];
@@ -208,11 +198,9 @@ export function buildChartOption(config: {
208198
readonly legend?: Record<string, unknown>;
209199
}): EChartsCoreOption {
210200
const {grid, xAxis, yAxis, tooltip, brush, legend} = config;
211-
const theme = getChartThemeColors();
212201

213202
const option: Record<string, unknown> = {
214203
animation: false,
215-
color: [...theme.chartColors],
216204
grid: buildGridOption(grid),
217205
xAxis: buildAxisOption(xAxis, true),
218206
yAxis: buildAxisOption(yAxis, false),

ui/src/components/widgets/charts/chart_theme.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,11 @@ export interface ChartThemeColors {
4343

4444
/**
4545
* Returns the current theme colors by reading CSS variables from the given
46-
* element. Falls back to .pf-theme-provider then document.documentElement so
47-
* charts work in embedded builds that omit the theme provider.
46+
* element. The element must be within the DOM tree that has the theme CSS
47+
* variables defined (typically a child of .pf-theme-provider).
4848
*/
49-
export function getChartThemeColors(el?: Element): ChartThemeColors {
50-
const elem =
51-
el ??
52-
document.querySelector('.pf-theme-provider') ??
53-
document.documentElement;
54-
const style = getComputedStyle(elem);
49+
export function getChartThemeColors(el: Element): ChartThemeColors {
50+
const style = getComputedStyle(el);
5551

5652
const chartColors: string[] = [];
5753
for (let i = 1; i <= 8; i++) {

0 commit comments

Comments
 (0)