Skip to content

Commit 2aed4a9

Browse files
AtishayMsftatisjaiclaude
authored
Create vega test app (#715)
* Add first vega set for test validation * Fix invalid Vega-Lite schema type declarations in 35 files - 16 schemas: Remove bin+quantitative on string data, change to nominal (e.g., data_400 had bin:true on "dog"/"cat" strings) - 19 schemas: Change quantitative to temporal for date string fields (e.g., data_940 had quantitative on "2000-01" date strings) Detected via E2E screenshot testing of VegaDeclarativeChart component across all 1,056 schemas. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix vega schemas * Fix schemas * Add vega test app --------- Co-authored-by: Atishay Jain <atisjai@microsoft.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 928a07e commit 2aed4a9

9 files changed

Lines changed: 1121 additions & 194 deletions

File tree

.claude/settings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(git fetch:*)",
5+
"Bash(git merge:*)",
6+
"Bash(yarn install:*)",
7+
"Bash(npx tsc:*)",
8+
"Bash(npm run build:*)",
9+
"Bash(npm view:*)",
10+
"Bash(yarn link:*)",
11+
"Bash(yarn unlink:*)",
12+
"Bash(npm start)"
13+
]
14+
}
15+
}

apps/plotly_examples/config/paths.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ module.exports = {
6969
appWebpackCache: resolveApp('node_modules/.cache'),
7070
appTsBuildInfoFile: resolveApp('node_modules/.cache/tsconfig.tsbuildinfo'),
7171
swSrc: resolveModule(resolveApp, 'src/service-worker'),
72+
vegaData: resolveApp('../../vega_data'),
7273
publicUrlOrPath,
7374
};
7475

apps/plotly_examples/config/webpack.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,9 @@ module.exports = function (webpackEnv) {
319319
'scheduler/tracing': 'scheduler/tracing-profiling',
320320
}),
321321
...(modules.webpackAliases || {}),
322+
// Force a single React instance when using yarn link
323+
react: path.resolve(paths.appNodeModules, 'react'),
324+
'react-dom': path.resolve(paths.appNodeModules, 'react-dom'),
322325
},
323326
plugins: [
324327
// Prevents users from importing files from outside of src/ (or node_modules/).
@@ -333,6 +336,7 @@ module.exports = function (webpackEnv) {
333336
babelRuntimeEntry,
334337
babelRuntimeEntryHelpers,
335338
babelRuntimeRegenerator,
339+
paths.vegaData,
336340
]),
337341
],
338342
fallback: {

apps/plotly_examples/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"dependencies": {
66
"@babel/core": "^7.16.0",
77
"@fluentui/react-charting": "^5.25.6",
8-
"@fluentui/react-charts": "0.0.0-nightly-20260212-0406.1",
8+
"@fluentui/react-charts": "0.0.0-nightly-20260302-0516.1",
99
"@fluentui/react-components": "^9.72.2",
1010
"@fluentui/react-menu": "^9.20.1",
1111
"@fluentui/react-portal-compat": "9.0.176",
@@ -40,6 +40,9 @@
4040
"jest-watch-typeahead": "^1.0.0",
4141
"mini-css-extract-plugin": "^2.4.5",
4242
"plotly.js": "^2.35.3",
43+
"vega": "^5.30.0",
44+
"vega-embed": "^6.26.0",
45+
"vega-lite": "^5.21.0",
4346
"postcss": "^8.4.4",
4447
"postcss-flexbugs-fixes": "^5.0.2",
4548
"postcss-loader": "^6.2.1",

apps/plotly_examples/src/App.tsx

Lines changed: 77 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,56 @@ import {
1010
Subtitle2,
1111
Body2,
1212
Switch,
13-
Slider
13+
Slider,
14+
TabList,
15+
Tab,
16+
SelectTabEvent,
17+
SelectTabData
1418
} from '@fluentui/react-components';
1519
import { PortalCompatProvider } from '@fluentui/react-portal-compat';
1620
import { ChartWrapper } from './components/ChartWrapper';
21+
import { VegaChartWrapper } from './components/VegaChartWrapper';
1722
import { getSelection, saveSelection } from './components/utils';
1823
import { setRTL } from '@fluentui/react/lib/Utilities';
1924
import { SliderOnChangeData } from '@fluentui/react-components';
2025

26+
type AppView = 'plotly' | 'vega';
27+
28+
function getInitialView(): AppView {
29+
const hash = window.location.hash.replace('#', '');
30+
if (hash === 'vega') return 'vega';
31+
return 'plotly';
32+
}
33+
2134
const App: React.FC = () => {
35+
const [activeView, setActiveView] = React.useState<AppView>(getInitialView);
2236
const [value, setValue] = React.useState(getSelection("Theme", "Light"));
2337
const [isRTL, setisRTL] = React.useState(getSelection("RTL", "false") === "true");
2438
const [isDimensionSlidersEnabled, setIsDimensionSlidersEnabled] = React.useState(getSelection("DimensionSlidersEnabled", "true") === "true");
25-
const [isV9ChartFirst, setIsV9ChartFirst] = React.useState(getSelection("isV9ChartFirst", "false") === "true");
2639
const [labelRTLMode, setLabelRTLMode] = React.useState("Enable RTL");
2740
const [labelDimensionSwitch, setLabelDimensionSwitch] = React.useState("Disable Dimension sliders");
28-
const [labelChartOrderSwitch] = React.useState("Show v9 first");
2941
const [chartWidth, setChartWidth] = React.useState<number>(Number(getSelection("ChartWidth", window.innerWidth.toString())));
3042
const [chartHeight, setChartHeight] = React.useState<number>(Number(getSelection("ChartHeight", "400")));
3143

44+
React.useEffect(() => {
45+
const onHashChange = () => setActiveView(getInitialView());
46+
window.addEventListener('hashchange', onHashChange);
47+
return () => window.removeEventListener('hashchange', onHashChange);
48+
}, []);
49+
3250
setRTL(isRTL);
51+
3352
const onOptionSelect = (event: SelectionEvents, data: OptionOnSelectData): void => {
3453
setValue(data.optionText ?? "Light");
3554
saveSelection("Theme", data.optionText ?? "Light");
3655
};
3756

57+
const handleTabSelect = (_event: SelectTabEvent, data: SelectTabData) => {
58+
const view = data.value as AppView;
59+
setActiveView(view);
60+
window.location.hash = view;
61+
};
62+
3863
const handleRTLSwitchChange = () => {
3964
const newIsRTL = !isRTL;
4065
setisRTL(newIsRTL);
@@ -50,12 +75,6 @@ const App: React.FC = () => {
5075
saveSelection("DimensionSlidersEnabled", newIsEnabled.toString());
5176
};
5277

53-
const handleChartOrderSwitchChange = () => {
54-
const newIsV9ChartFirst = !isV9ChartFirst;
55-
setIsV9ChartFirst(newIsV9ChartFirst);
56-
saveSelection("isV9ChartFirst", newIsV9ChartFirst.toString());
57-
};
58-
5978
const handleSliderChange = (event: React.ChangeEvent<HTMLInputElement>, data: SliderOnChangeData) => {
6079
setChartWidth(Number(data.value));
6180
saveSelection("ChartWidth", data.value.toString());
@@ -66,41 +85,44 @@ const App: React.FC = () => {
6685
saveSelection("ChartHeight", data.value.toString());
6786
};
6887

88+
const chartWidthProp = isDimensionSlidersEnabled ? chartWidth : undefined;
89+
const chartHeightProp = isDimensionSlidersEnabled ? chartHeight : undefined;
90+
6991
return (
7092
<div>
7193
<FluentProvider theme={value === "Light" ? webLightTheme : webDarkTheme} targetDocument={document}>
7294
<PortalCompatProvider>
73-
<Subtitle2> Theme:</Subtitle2>&nbsp;&nbsp;
74-
<Dropdown
75-
value={value}
76-
onOptionSelect={onOptionSelect}
77-
>
78-
<Option>Light</Option>
79-
<Option>Dark</Option>
80-
</Dropdown>
81-
&nbsp;&nbsp;&nbsp;
82-
<Switch
83-
data-testid="rtl_switch"
84-
checked={isRTL}
85-
onChange={handleRTLSwitchChange}
86-
label={labelRTLMode}
87-
/>
88-
<Switch
89-
checked={isDimensionSlidersEnabled}
90-
onChange={handleDimensionSlidersChange}
91-
label={labelDimensionSwitch}
92-
/>
93-
<Switch
94-
checked={isV9ChartFirst}
95-
onChange={handleChartOrderSwitchChange}
96-
label={labelChartOrderSwitch}
97-
/>
98-
99-
&nbsp;&nbsp;<Body2>@fluentui/react-charting &nbsp;</Body2><Subtitle2>v5.25.6</Subtitle2>
100-
&nbsp;&nbsp;<Body2>@fluentui/react-charts &nbsp;</Body2><Subtitle2>0.0.0-nightly-20260212-0406.1</Subtitle2>
101-
<br />
102-
{isDimensionSlidersEnabled && (<>
103-
<div style={{ display: 'flex', flexDirection: isRTL ? 'row-reverse' : 'row', alignItems: 'center', gap: '20px' }}>
95+
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '8px', padding: '4px 8px' }}>
96+
<TabList selectedValue={activeView} onTabSelect={handleTabSelect}>
97+
<Tab value="plotly">Plotly Schemas</Tab>
98+
<Tab value="vega">Vega-Lite Schemas</Tab>
99+
</TabList>
100+
<Subtitle2> Theme:</Subtitle2>&nbsp;
101+
<Dropdown
102+
value={value}
103+
onOptionSelect={onOptionSelect}
104+
>
105+
<Option>Light</Option>
106+
<Option>Dark</Option>
107+
</Dropdown>
108+
<Switch
109+
data-testid="rtl_switch"
110+
checked={isRTL}
111+
onChange={handleRTLSwitchChange}
112+
label={labelRTLMode}
113+
/>
114+
<Switch
115+
checked={isDimensionSlidersEnabled}
116+
onChange={handleDimensionSlidersChange}
117+
label={labelDimensionSwitch}
118+
/>
119+
</div>
120+
<div style={{ padding: '0 8px' }}>
121+
<Body2>@fluentui/react-charting &nbsp;</Body2><Subtitle2>v5.25.6</Subtitle2>
122+
&nbsp;&nbsp;<Body2>@fluentui/react-charts &nbsp;</Body2><Subtitle2>0.0.0-nightly-20260302-0516.1</Subtitle2>
123+
</div>
124+
{isDimensionSlidersEnabled && (
125+
<div style={{ display: 'flex', flexDirection: isRTL ? 'row-reverse' : 'row', alignItems: 'center', gap: '20px', padding: '4px 8px' }}>
104126
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
105127
<Subtitle2>Chart Width:</Subtitle2>
106128
<Slider
@@ -120,13 +142,21 @@ const App: React.FC = () => {
120142
/>
121143
</div>
122144
</div>
123-
</>)}
124-
<ChartWrapper
125-
width={isDimensionSlidersEnabled ? chartWidth : undefined}
126-
height={isDimensionSlidersEnabled ? chartHeight : undefined}
127-
isReversedOrder={isV9ChartFirst}
128-
isRTL={isRTL}
129-
/>
145+
)}
146+
{activeView === 'plotly' ? (
147+
<ChartWrapper
148+
width={chartWidthProp}
149+
height={chartHeightProp}
150+
isReversedOrder={true}
151+
isRTL={isRTL}
152+
/>
153+
) : (
154+
<VegaChartWrapper
155+
width={chartWidthProp}
156+
height={chartHeightProp}
157+
isReversedOrder={true}
158+
/>
159+
)}
130160
</PortalCompatProvider>
131161
</FluentProvider>
132162
</div>
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import * as React from 'react';
2+
import embed, { Result } from 'vega-embed';
3+
4+
interface VegaChartProps {
5+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6+
spec: any;
7+
width?: number;
8+
height?: number;
9+
theme?: 'light' | 'dark';
10+
}
11+
12+
function VegaChartInner({ spec, width, height, theme = 'light' }: VegaChartProps) {
13+
const containerRef = React.useRef<HTMLDivElement>(null);
14+
const viewRef = React.useRef<Result | null>(null);
15+
const lastSpecRef = React.useRef<string>('');
16+
const lastThemeRef = React.useRef<string>('');
17+
18+
const specString = React.useMemo(() => JSON.stringify(spec), [spec]);
19+
20+
React.useEffect(() => {
21+
const container = containerRef.current;
22+
if (!container || !spec) return;
23+
24+
const specChanged = lastSpecRef.current !== specString;
25+
const themeChanged = lastThemeRef.current !== theme;
26+
27+
if (!specChanged && !themeChanged && viewRef.current) {
28+
const view = viewRef.current.view;
29+
if (width) view.width(width);
30+
if (height) view.height(height);
31+
view.runAsync();
32+
return;
33+
}
34+
35+
lastSpecRef.current = specString;
36+
lastThemeRef.current = theme;
37+
38+
let isCancelled = false;
39+
40+
if (viewRef.current) {
41+
viewRef.current.finalize();
42+
viewRef.current = null;
43+
}
44+
45+
const specWithDimensions = {
46+
...spec,
47+
...(width && { width }),
48+
...(height && { height }),
49+
autosize: { type: 'fit', contains: 'padding' },
50+
};
51+
52+
const embedOptions = {
53+
actions: false,
54+
renderer: 'svg' as const,
55+
config: theme === 'dark' ? {
56+
background: 'rgb(17,17,17)',
57+
title: { color: 'white' },
58+
axis: {
59+
labelColor: 'white',
60+
titleColor: 'white',
61+
gridColor: '#444'
62+
},
63+
legend: { labelColor: 'white', titleColor: 'white' }
64+
} : undefined
65+
};
66+
67+
embed(container, specWithDimensions, embedOptions)
68+
.then((result) => {
69+
if (!isCancelled) {
70+
viewRef.current = result;
71+
} else {
72+
result.finalize();
73+
}
74+
})
75+
.catch((err: Error) => {
76+
if (!isCancelled) {
77+
console.error('Vega embed error:', err);
78+
}
79+
});
80+
81+
return () => {
82+
isCancelled = true;
83+
};
84+
// eslint-disable-next-line react-hooks/exhaustive-deps
85+
}, [specString, width, height, theme]);
86+
87+
React.useEffect(() => {
88+
return () => {
89+
if (viewRef.current) {
90+
viewRef.current.finalize();
91+
viewRef.current = null;
92+
}
93+
};
94+
}, []);
95+
96+
return (
97+
<div
98+
ref={containerRef}
99+
style={{
100+
height: height ? `${height}px` : '360px',
101+
width: width ? `${width}px` : '500px',
102+
overflow: 'hidden'
103+
}}
104+
/>
105+
);
106+
}
107+
108+
const VegaChart = React.memo(VegaChartInner, (prevProps, nextProps) => {
109+
return (
110+
JSON.stringify(prevProps.spec) === JSON.stringify(nextProps.spec) &&
111+
prevProps.width === nextProps.width &&
112+
prevProps.height === nextProps.height &&
113+
prevProps.theme === nextProps.theme
114+
);
115+
});
116+
117+
export default VegaChart;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import VegaDeclarativeChartExample from './VegaDeclarativeChartExample';
3+
4+
interface VegaChartWrapperProps {
5+
width: number | undefined;
6+
height: number | undefined;
7+
isReversedOrder?: boolean;
8+
}
9+
10+
export function VegaChartWrapper({ width, height, isReversedOrder }: VegaChartWrapperProps) {
11+
return (
12+
<div style={{ marginLeft: '25px' }}>
13+
<VegaDeclarativeChartExample width={width} height={height} isReversedOrder={isReversedOrder} />
14+
</div>
15+
);
16+
}

0 commit comments

Comments
 (0)