diff --git a/.eslintrc.js b/.eslintrc.js
index 6b459fd61b..0b72d4b386 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -74,7 +74,7 @@ module.exports = {
'import/no-unresolved': [
'error',
{
- ignore: ['monaco-editor', 'vscode']
+ ignore: ['monaco-editor', 'vscode', 'react-error-boundary']
}
],
'import/prefer-default-export': 'off',
diff --git a/package-lock.json b/package-lock.json
index f78241cc48..15c748e392 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -53,6 +53,7 @@
"react": "^16.5.2",
"react-data-grid": "^6.0.2-0",
"react-dom": "^16.5.2",
+ "react-error-boundary": "^6.0.0",
"react-redux": "^7.1.1",
"react-svg-pan-zoom": "3.9.0",
"react-svgmt": "1.1.11",
@@ -16525,6 +16526,18 @@
"react": "^16.14.0"
}
},
+ "node_modules/react-error-boundary": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz",
+ "integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -32556,6 +32569,14 @@
"scheduler": "^0.19.1"
}
},
+ "react-error-boundary": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz",
+ "integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==",
+ "requires": {
+ "@babel/runtime": "^7.12.5"
+ }
+ },
"react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
diff --git a/package.json b/package.json
index 575cc95d56..d9bffeb461 100644
--- a/package.json
+++ b/package.json
@@ -2197,6 +2197,7 @@
"react": "^16.5.2",
"react-data-grid": "^6.0.2-0",
"react-dom": "^16.5.2",
+ "react-error-boundary": "^6.0.0",
"react-redux": "^7.1.1",
"react-svg-pan-zoom": "3.9.0",
"react-svgmt": "1.1.11",
diff --git a/src/webviews/webview-side/vega-renderer/ErrorBoundary.tsx b/src/webviews/webview-side/vega-renderer/ErrorBoundary.tsx
new file mode 100644
index 0000000000..8ea2169b1a
--- /dev/null
+++ b/src/webviews/webview-side/vega-renderer/ErrorBoundary.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import type { FallbackProps } from 'react-error-boundary';
+
+export function ErrorFallback({ error }: FallbackProps) {
+ return (
+
+
Error rendering chart
+
{error.message}
+
+ Stack trace
+
+ {error.stack}
+
+
+
+ );
+}
diff --git a/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx b/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx
index 8077285108..e52eac88fb 100644
--- a/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx
+++ b/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx
@@ -1,15 +1,53 @@
import { chartColors10, chartColors20, deepnoteBlues } from './colors';
-import React, { memo, useLayoutEffect } from 'react';
+import React, { memo, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { Vega } from 'react-vega';
import { vega } from 'vega-embed';
+import { produce } from 'immer';
import { numberFormats } from './number-formats';
+import { detectBaseTheme } from '../react-common/themeDetector';
+import type { Spec as VegaSpec } from 'vega';
export interface VegaRendererProps {
- spec: Record;
+ spec: VegaSpec;
renderer?: 'svg' | 'canvas';
}
+interface ThemeColors {
+ backgroundColor: string;
+ foregroundColor: string;
+ isDark: boolean;
+}
+
+const getThemeColors = (): ThemeColors => {
+ const theme = detectBaseTheme();
+ const isDark = theme === 'vscode-dark' || theme === 'vscode-high-contrast';
+ const styles = getComputedStyle(document.body);
+ const backgroundColor = styles.getPropertyValue('--vscode-editor-background').trim() || 'transparent';
+ const foregroundColor = styles.getPropertyValue('--vscode-editor-foreground').trim() || '#000000';
+
+ return { backgroundColor, foregroundColor, isDark };
+};
+
+function useThemeColors(): ThemeColors {
+ const [themeColors, setThemeColors] = useState(getThemeColors);
+
+ useEffect(() => {
+ const observer = new MutationObserver(() => {
+ setThemeColors(getThemeColors());
+ });
+
+ observer.observe(document.body, {
+ attributes: true,
+ attributeFilter: ['class', 'data-vscode-theme-name']
+ });
+
+ return () => observer.disconnect();
+ }, []);
+
+ return themeColors;
+}
+
export const VegaRenderer = memo(function VegaRenderer(props: VegaRendererProps) {
const { renderer, spec } = props;
@@ -31,9 +69,58 @@ export const VegaRenderer = memo(function VegaRenderer(props: VegaRendererProps)
vega.scheme('deepnote_blues', deepnoteBlues);
}, []);
+ const { backgroundColor, foregroundColor, isDark } = useThemeColors();
+ const themedSpec = useMemo(() => {
+ const patchedSpec = produce(spec, (draft) => {
+ draft.background = backgroundColor;
+
+ if (!draft.config) {
+ draft.config = {};
+ }
+
+ draft.config.background = backgroundColor;
+
+ if (!draft.config.style) {
+ draft.config.style = {};
+ }
+ if (!draft.config.style.cell) {
+ draft.config.style.cell = {};
+ }
+ draft.config.style.cell = {
+ stroke: backgroundColor
+ };
+
+ if (!draft.config.axis) {
+ draft.config.axis = {};
+ }
+ draft.config.axis.domainColor = foregroundColor;
+ draft.config.axis.gridColor = isDark ? '#3e3e3e' : '#e0e0e0';
+ draft.config.axis.tickColor = foregroundColor;
+ draft.config.axis.labelColor = foregroundColor;
+ draft.config.axis.titleColor = foregroundColor;
+
+ if (!draft.config.legend) {
+ draft.config.legend = {};
+ }
+ draft.config.legend.labelColor = foregroundColor;
+ draft.config.legend.titleColor = foregroundColor;
+
+ if (!draft.config.title) {
+ draft.config.title = {};
+ }
+ draft.config.title.color = foregroundColor;
+
+ if (!draft.config.text) {
+ draft.config.text = {};
+ }
+ draft.config.text.fill = foregroundColor;
+ });
+ return structuredClone(patchedSpec); // Immer freezes the spec, which doesn't play well with Vega
+ }, [spec, backgroundColor, foregroundColor, isDark]);
+
return (
)
const elementsCache: Record = {};
return {
renderOutputItem(outputItem: OutputItem, element: HTMLElement) {
- console.log(`Vega renderer - rendering output item: ${outputItem.id}`);
try {
const spec = outputItem.json();
-
- console.log(`Vega renderer - received spec with ${Object.keys(spec).length} keys`);
-
- const metadata = outputItem.metadata as Metadata | undefined;
-
- console.log('[VegaRenderer] Full metadata', metadata);
-
const root = document.createElement('div');
root.style.height = '500px';
element.appendChild(root);
elementsCache[outputItem.id] = root;
ReactDOM.render(
- React.createElement(VegaRenderer, {
- spec: spec
- }),
+ React.createElement(
+ ErrorBoundary,
+ {
+ FallbackComponent: ErrorFallback,
+ onError: (error, info) => {
+ console.error('Vega renderer error:', error, info);
+ }
+ },
+ React.createElement(VegaRenderer, {
+ spec: spec
+ })
+ ),
root
);
} catch (error) {