From 5e67c4d2098e26575e945982b0f1133d9d034019 Mon Sep 17 00:00:00 2001 From: OlegWock Date: Fri, 17 Oct 2025 12:07:11 +0200 Subject: [PATCH 1/6] feat(charts): Style charts to match editor's color theme --- package-lock.json | 21 +++++ package.json | 1 + .../vega-renderer/ErrorBoundary.tsx | 38 +++++++++ .../vega-renderer/VegaRenderer.tsx | 81 ++++++++++++++++++- .../webview-side/vega-renderer/index.ts | 32 ++++---- 5 files changed, 153 insertions(+), 20 deletions(-) create mode 100644 src/webviews/webview-side/vega-renderer/ErrorBoundary.tsx diff --git a/package-lock.json b/package-lock.json index b982708381..126c7dba4f 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", @@ -15211,6 +15212,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", @@ -30247,6 +30260,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 9c62003e38..1e5c16b3ea 100644 --- a/package.json +++ b/package.json @@ -2195,6 +2195,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..8d014e7fee --- /dev/null +++ b/src/webviews/webview-side/vega-renderer/ErrorBoundary.tsx @@ -0,0 +1,38 @@ +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..2f103ccdd9 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'; export interface VegaRendererProps { spec: Record; 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(() => { + console.log('Observed body change') + 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,48 @@ 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: any) => { + draft.background = backgroundColor; + + if (!draft.config) { + draft.config = {}; + } + + draft.config.background = 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.color = 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) { From ed9b5e2dcd54db7d86f2cebb8033dba5323ed887 Mon Sep 17 00:00:00 2001 From: OlegWock Date: Fri, 17 Oct 2025 12:12:21 +0200 Subject: [PATCH 2/6] Adjust import/no-unresolved rule ignore list --- .eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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', From d216208755f0f040cbefcaf6be1c2d154fae8337 Mon Sep 17 00:00:00 2001 From: OlegWock Date: Fri, 17 Oct 2025 12:16:17 +0200 Subject: [PATCH 3/6] Fix formatting --- src/webviews/webview-side/vega-renderer/ErrorBoundary.tsx | 1 - src/webviews/webview-side/vega-renderer/VegaRenderer.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webviews/webview-side/vega-renderer/ErrorBoundary.tsx b/src/webviews/webview-side/vega-renderer/ErrorBoundary.tsx index 8d014e7fee..8ea2169b1a 100644 --- a/src/webviews/webview-side/vega-renderer/ErrorBoundary.tsx +++ b/src/webviews/webview-side/vega-renderer/ErrorBoundary.tsx @@ -35,4 +35,3 @@ export function ErrorFallback({ error }: FallbackProps) { ); } - diff --git a/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx b/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx index 2f103ccdd9..074a758cd5 100644 --- a/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx +++ b/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx @@ -33,7 +33,7 @@ function useThemeColors(): ThemeColors { useEffect(() => { const observer = new MutationObserver(() => { - console.log('Observed body change') + console.log('Observed body change'); setThemeColors(getThemeColors()); }); From 0781ae70af2590f9d1ae9f81902421c5d14122a8 Mon Sep 17 00:00:00 2001 From: OlegWock Date: Fri, 17 Oct 2025 12:16:54 +0200 Subject: [PATCH 4/6] Remove redundant log --- src/webviews/webview-side/vega-renderer/VegaRenderer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx b/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx index 074a758cd5..5576c96406 100644 --- a/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx +++ b/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx @@ -33,7 +33,6 @@ function useThemeColors(): ThemeColors { useEffect(() => { const observer = new MutationObserver(() => { - console.log('Observed body change'); setThemeColors(getThemeColors()); }); From ad2278a82056ef0a7d0ca3ba83eedab7dd3502c7 Mon Sep 17 00:00:00 2001 From: OlegWock Date: Fri, 17 Oct 2025 12:29:28 +0200 Subject: [PATCH 5/6] Adjust type of spec --- src/webviews/webview-side/vega-renderer/VegaRenderer.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx b/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx index 5576c96406..75e73f8aa8 100644 --- a/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx +++ b/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx @@ -6,9 +6,10 @@ import { produce } from 'immer'; import { numberFormats } from './number-formats'; import { detectBaseTheme } from '../react-common/themeDetector'; +import { TopLevelSpec } from 'vega-lite'; export interface VegaRendererProps { - spec: Record; + spec: TopLevelSpec; renderer?: 'svg' | 'canvas'; } @@ -70,7 +71,7 @@ export const VegaRenderer = memo(function VegaRenderer(props: VegaRendererProps) const { backgroundColor, foregroundColor, isDark } = useThemeColors(); const themedSpec = useMemo(() => { - const patchedSpec = produce(spec, (draft: any) => { + const patchedSpec = produce(spec, (draft) => { draft.background = backgroundColor; if (!draft.config) { From 8d8264b78e89822c777053aabe71a8a5a2315ba0 Mon Sep 17 00:00:00 2001 From: OlegWock Date: Fri, 17 Oct 2025 12:36:03 +0200 Subject: [PATCH 6/6] More type adjustments --- .../webview-side/vega-renderer/VegaRenderer.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx b/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx index 75e73f8aa8..e52eac88fb 100644 --- a/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx +++ b/src/webviews/webview-side/vega-renderer/VegaRenderer.tsx @@ -6,10 +6,10 @@ import { produce } from 'immer'; import { numberFormats } from './number-formats'; import { detectBaseTheme } from '../react-common/themeDetector'; -import { TopLevelSpec } from 'vega-lite'; +import type { Spec as VegaSpec } from 'vega'; export interface VegaRendererProps { - spec: TopLevelSpec; + spec: VegaSpec; renderer?: 'svg' | 'canvas'; } @@ -80,6 +80,16 @@ export const VegaRenderer = memo(function VegaRenderer(props: VegaRendererProps) 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 = {}; } @@ -103,7 +113,7 @@ export const VegaRenderer = memo(function VegaRenderer(props: VegaRendererProps) if (!draft.config.text) { draft.config.text = {}; } - draft.config.text.color = foregroundColor; + draft.config.text.fill = foregroundColor; }); return structuredClone(patchedSpec); // Immer freezes the spec, which doesn't play well with Vega }, [spec, backgroundColor, foregroundColor, isDark]);