Skip to content

Commit fbf4d88

Browse files
feat(charts): Style charts to match editor's color theme (#76)
* feat(charts): Style charts to match editor's color theme * Adjust import/no-unresolved rule ignore list * Fix formatting * Remove redundant log * Adjust type of spec * More type adjustments --------- Co-authored-by: James Hobbs <15235276+jamesbhobbs@users.noreply.github.com>
1 parent a50443c commit fbf4d88

File tree

6 files changed

+164
-22
lines changed

6 files changed

+164
-22
lines changed

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ module.exports = {
7474
'import/no-unresolved': [
7575
'error',
7676
{
77-
ignore: ['monaco-editor', 'vscode']
77+
ignore: ['monaco-editor', 'vscode', 'react-error-boundary']
7878
}
7979
],
8080
'import/prefer-default-export': 'off',

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
@@ -2197,6 +2197,7 @@
21972197
"react": "^16.5.2",
21982198
"react-data-grid": "^6.0.2-0",
21992199
"react-dom": "^16.5.2",
2200+
"react-error-boundary": "^6.0.0",
22002201
"react-redux": "^7.1.1",
22012202
"react-svg-pan-zoom": "3.9.0",
22022203
"react-svgmt": "1.1.11",
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
}

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

Lines changed: 90 additions & 3 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';
9+
import type { Spec as VegaSpec } from 'vega';
710

811
export interface VegaRendererProps {
9-
spec: Record<string, unknown>;
12+
spec: VegaSpec;
1013
renderer?: 'svg' | 'canvas';
1114
}
1215

16+
interface ThemeColors {
17+
backgroundColor: string;
18+
foregroundColor: string;
19+
isDark: boolean;
20+
}
21+
22+
const getThemeColors = (): ThemeColors => {
23+
const theme = detectBaseTheme();
24+
const isDark = theme === 'vscode-dark' || theme === 'vscode-high-contrast';
25+
const styles = getComputedStyle(document.body);
26+
const backgroundColor = styles.getPropertyValue('--vscode-editor-background').trim() || 'transparent';
27+
const foregroundColor = styles.getPropertyValue('--vscode-editor-foreground').trim() || '#000000';
28+
29+
return { backgroundColor, foregroundColor, isDark };
30+
};
31+
32+
function useThemeColors(): ThemeColors {
33+
const [themeColors, setThemeColors] = useState(getThemeColors);
34+
35+
useEffect(() => {
36+
const observer = new MutationObserver(() => {
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,58 @@ 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) => {
75+
draft.background = backgroundColor;
76+
77+
if (!draft.config) {
78+
draft.config = {};
79+
}
80+
81+
draft.config.background = backgroundColor;
82+
83+
if (!draft.config.style) {
84+
draft.config.style = {};
85+
}
86+
if (!draft.config.style.cell) {
87+
draft.config.style.cell = {};
88+
}
89+
draft.config.style.cell = {
90+
stroke: backgroundColor
91+
};
92+
93+
if (!draft.config.axis) {
94+
draft.config.axis = {};
95+
}
96+
draft.config.axis.domainColor = foregroundColor;
97+
draft.config.axis.gridColor = isDark ? '#3e3e3e' : '#e0e0e0';
98+
draft.config.axis.tickColor = foregroundColor;
99+
draft.config.axis.labelColor = foregroundColor;
100+
draft.config.axis.titleColor = foregroundColor;
101+
102+
if (!draft.config.legend) {
103+
draft.config.legend = {};
104+
}
105+
draft.config.legend.labelColor = foregroundColor;
106+
draft.config.legend.titleColor = foregroundColor;
107+
108+
if (!draft.config.title) {
109+
draft.config.title = {};
110+
}
111+
draft.config.title.color = foregroundColor;
112+
113+
if (!draft.config.text) {
114+
draft.config.text = {};
115+
}
116+
draft.config.text.fill = foregroundColor;
117+
});
118+
return structuredClone(patchedSpec); // Immer freezes the spec, which doesn't play well with Vega
119+
}, [spec, backgroundColor, foregroundColor, isDark]);
120+
34121
return (
35122
<Vega
36-
spec={spec}
123+
spec={themedSpec}
37124
renderer={renderer}
38125
actions={false}
39126
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)