Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
21 changes: 21 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
37 changes: 37 additions & 0 deletions src/webviews/webview-side/vega-renderer/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import type { FallbackProps } from 'react-error-boundary';

export function ErrorFallback({ error }: FallbackProps) {
return (
<div
style={{
padding: '16px',
color: 'var(--vscode-errorForeground)',
backgroundColor: 'var(--vscode-inputValidation-errorBackground)',
border: '1px solid var(--vscode-inputValidation-errorBorder)',
borderRadius: '4px',
fontFamily: 'var(--vscode-font-family)',
fontSize: '13px'
}}
>
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}>Error rendering chart</div>
<div style={{ marginBottom: '8px' }}>{error.message}</div>
<details style={{ marginTop: '8px', cursor: 'pointer' }}>
<summary>Stack trace</summary>
<pre
style={{
marginTop: '8px',
padding: '8px',
backgroundColor: 'var(--vscode-editor-background)',
overflow: 'auto',
fontSize: '11px',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}
>
{error.stack}
</pre>
</details>
</div>
);
}
80 changes: 78 additions & 2 deletions src/webviews/webview-side/vega-renderer/VegaRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,52 @@
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<string, unknown>;
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;

Expand All @@ -31,9 +68,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 (
<Vega
spec={spec}
spec={themedSpec}
renderer={renderer}
actions={false}
style={{
Expand Down
32 changes: 14 additions & 18 deletions src/webviews/webview-side/vega-renderer/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import type { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer';
import { ErrorBoundary } from 'react-error-boundary';
import { VegaRenderer } from './VegaRenderer';

interface Metadata {
cellId?: string;
cellIndex?: number;
executionCount?: number;
outputType?: string;
}
import { ErrorFallback } from './ErrorBoundary';

/**
* Renderer for Vega charts (application/vnd.vega.v5+json).
Expand All @@ -17,25 +12,26 @@ export const activate: ActivationFunction = (_context: RendererContext<unknown>)
const elementsCache: Record<string, HTMLElement | undefined> = {};
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) {
Expand Down