Skip to content

Commit 5e67c4d

Browse files
committed
feat(charts): Style charts to match editor's color theme
1 parent f5cec10 commit 5e67c4d

File tree

5 files changed

+153
-20
lines changed

5 files changed

+153
-20
lines changed

package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2195,6 +2195,7 @@
21952195
"react": "^16.5.2",
21962196
"react-data-grid": "^6.0.2-0",
21972197
"react-dom": "^16.5.2",
2198+
"react-error-boundary": "^6.0.0",
21982199
"react-redux": "^7.1.1",
21992200
"react-svg-pan-zoom": "3.9.0",
22002201
"react-svgmt": "1.1.11",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react';
2+
import type { FallbackProps } from 'react-error-boundary';
3+
4+
export function ErrorFallback({ error }: FallbackProps) {
5+
return (
6+
<div
7+
style={{
8+
padding: '16px',
9+
color: 'var(--vscode-errorForeground)',
10+
backgroundColor: 'var(--vscode-inputValidation-errorBackground)',
11+
border: '1px solid var(--vscode-inputValidation-errorBorder)',
12+
borderRadius: '4px',
13+
fontFamily: 'var(--vscode-font-family)',
14+
fontSize: '13px'
15+
}}
16+
>
17+
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}>Error rendering chart</div>
18+
<div style={{ marginBottom: '8px' }}>{error.message}</div>
19+
<details style={{ marginTop: '8px', cursor: 'pointer' }}>
20+
<summary>Stack trace</summary>
21+
<pre
22+
style={{
23+
marginTop: '8px',
24+
padding: '8px',
25+
backgroundColor: 'var(--vscode-editor-background)',
26+
overflow: 'auto',
27+
fontSize: '11px',
28+
whiteSpace: 'pre-wrap',
29+
wordBreak: 'break-word'
30+
}}
31+
>
32+
{error.stack}
33+
</pre>
34+
</details>
35+
</div>
36+
);
37+
}
38+

src/webviews/webview-side/vega-renderer/VegaRenderer.tsx

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,53 @@
11
import { chartColors10, chartColors20, deepnoteBlues } from './colors';
2-
import React, { memo, useLayoutEffect } from 'react';
2+
import React, { memo, useEffect, useLayoutEffect, useMemo, useState } from 'react';
33
import { Vega } from 'react-vega';
44
import { vega } from 'vega-embed';
5+
import { produce } from 'immer';
56

67
import { numberFormats } from './number-formats';
8+
import { detectBaseTheme } from '../react-common/themeDetector';
79

810
export interface VegaRendererProps {
911
spec: Record<string, unknown>;
1012
renderer?: 'svg' | 'canvas';
1113
}
1214

15+
interface ThemeColors {
16+
backgroundColor: string;
17+
foregroundColor: string;
18+
isDark: boolean;
19+
}
20+
21+
const getThemeColors = (): ThemeColors => {
22+
const theme = detectBaseTheme();
23+
const isDark = theme === 'vscode-dark' || theme === 'vscode-high-contrast';
24+
const styles = getComputedStyle(document.body);
25+
const backgroundColor = styles.getPropertyValue('--vscode-editor-background').trim() || 'transparent';
26+
const foregroundColor = styles.getPropertyValue('--vscode-editor-foreground').trim() || '#000000';
27+
28+
return { backgroundColor, foregroundColor, isDark };
29+
};
30+
31+
function useThemeColors(): ThemeColors {
32+
const [themeColors, setThemeColors] = useState(getThemeColors);
33+
34+
useEffect(() => {
35+
const observer = new MutationObserver(() => {
36+
console.log('Observed body change')
37+
setThemeColors(getThemeColors());
38+
});
39+
40+
observer.observe(document.body, {
41+
attributes: true,
42+
attributeFilter: ['class', 'data-vscode-theme-name']
43+
});
44+
45+
return () => observer.disconnect();
46+
}, []);
47+
48+
return themeColors;
49+
}
50+
1351
export const VegaRenderer = memo(function VegaRenderer(props: VegaRendererProps) {
1452
const { renderer, spec } = props;
1553

@@ -31,9 +69,48 @@ export const VegaRenderer = memo(function VegaRenderer(props: VegaRendererProps)
3169
vega.scheme('deepnote_blues', deepnoteBlues);
3270
}, []);
3371

72+
const { backgroundColor, foregroundColor, isDark } = useThemeColors();
73+
const themedSpec = useMemo(() => {
74+
const patchedSpec = produce(spec, (draft: any) => {
75+
draft.background = backgroundColor;
76+
77+
if (!draft.config) {
78+
draft.config = {};
79+
}
80+
81+
draft.config.background = backgroundColor;
82+
83+
if (!draft.config.axis) {
84+
draft.config.axis = {};
85+
}
86+
draft.config.axis.domainColor = foregroundColor;
87+
draft.config.axis.gridColor = isDark ? '#3e3e3e' : '#e0e0e0';
88+
draft.config.axis.tickColor = foregroundColor;
89+
draft.config.axis.labelColor = foregroundColor;
90+
draft.config.axis.titleColor = foregroundColor;
91+
92+
if (!draft.config.legend) {
93+
draft.config.legend = {};
94+
}
95+
draft.config.legend.labelColor = foregroundColor;
96+
draft.config.legend.titleColor = foregroundColor;
97+
98+
if (!draft.config.title) {
99+
draft.config.title = {};
100+
}
101+
draft.config.title.color = foregroundColor;
102+
103+
if (!draft.config.text) {
104+
draft.config.text = {};
105+
}
106+
draft.config.text.color = foregroundColor;
107+
});
108+
return structuredClone(patchedSpec); // Immer freezes the spec, which doesn't play well with Vega
109+
}, [spec, backgroundColor, foregroundColor, isDark]);
110+
34111
return (
35112
<Vega
36-
spec={spec}
113+
spec={themedSpec}
37114
renderer={renderer}
38115
actions={false}
39116
style={{

src/webviews/webview-side/vega-renderer/index.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import React from 'react';
22
import ReactDOM from 'react-dom';
33
import type { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer';
4+
import { ErrorBoundary } from 'react-error-boundary';
45
import { VegaRenderer } from './VegaRenderer';
5-
6-
interface Metadata {
7-
cellId?: string;
8-
cellIndex?: number;
9-
executionCount?: number;
10-
outputType?: string;
11-
}
6+
import { ErrorFallback } from './ErrorBoundary';
127

138
/**
149
* Renderer for Vega charts (application/vnd.vega.v5+json).
@@ -17,25 +12,26 @@ export const activate: ActivationFunction = (_context: RendererContext<unknown>)
1712
const elementsCache: Record<string, HTMLElement | undefined> = {};
1813
return {
1914
renderOutputItem(outputItem: OutputItem, element: HTMLElement) {
20-
console.log(`Vega renderer - rendering output item: ${outputItem.id}`);
2115
try {
2216
const spec = outputItem.json();
23-
24-
console.log(`Vega renderer - received spec with ${Object.keys(spec).length} keys`);
25-
26-
const metadata = outputItem.metadata as Metadata | undefined;
27-
28-
console.log('[VegaRenderer] Full metadata', metadata);
29-
3017
const root = document.createElement('div');
3118
root.style.height = '500px';
3219

3320
element.appendChild(root);
3421
elementsCache[outputItem.id] = root;
3522
ReactDOM.render(
36-
React.createElement(VegaRenderer, {
37-
spec: spec
38-
}),
23+
React.createElement(
24+
ErrorBoundary,
25+
{
26+
FallbackComponent: ErrorFallback,
27+
onError: (error, info) => {
28+
console.error('Vega renderer error:', error, info);
29+
}
30+
},
31+
React.createElement(VegaRenderer, {
32+
spec: spec
33+
})
34+
),
3935
root
4036
);
4137
} catch (error) {

0 commit comments

Comments
 (0)