Repository: deephaven/deephaven-plugins
Pull request #1147: fix: Detect if webgl is supported
Author: login: jnumainville
Created: 2025-03-21T17:49:09Z | Merged: 2025-03-26T17:51:24Z

Headless firefox mode doesn't work properly with webgl, so we need to detect if webgl is allowed.
This was causing issues for https://github.com/deephaven/deephaven-plugins/pull/1126


Diff:
diff --git a/plugins/plotly-express/src/js/src/PlotlyExpressChartModel.test.ts b/plugins/plotly-express/src/js/src/PlotlyExpressChartModel.test.ts
index 54cfbf4fd..a246856fe 100644
--- a/plugins/plotly-express/src/js/src/PlotlyExpressChartModel.test.ts
+++ b/plugins/plotly-express/src/js/src/PlotlyExpressChartModel.test.ts
@@ -271,6 +271,9 @@ describe('PlotlyExpressChartModel', () => {
       jest.fn()
     );
 
+    // Assume WebGL is supported in case it's false in the test environment
+    chartModel.isWebGlSupported = true;
+
     const mockSubscribe = jest.fn();
     await chartModel.subscribe(mockSubscribe);
     await new Promise(process.nextTick); // Subscribe is async
@@ -285,6 +288,31 @@ describe('PlotlyExpressChartModel', () => {
     expect(mockSubscribe).toHaveBeenCalledTimes(0);
   });
 
+  it('should swap replaceable WebGL traces without blocker events if WebGL is not supported', async () => {
+    const mockWidget = createMockWidget([SMALL_TABLE], 'scattergl');
+    const chartModel = new PlotlyExpressChartModel(
+      mockDh,
+      mockWidget,
+      jest.fn()
+    );
+
+    // Assume WebGL is not supported in case it's true in the test environment
+    chartModel.isWebGlSupported = false;
+
+    const mockSubscribe = jest.fn();
+    await chartModel.subscribe(mockSubscribe);
+    await new Promise(process.nextTick); // Subscribe is async
+    chartModel.setRenderOptions({ webgl: true });
+    expect(chartModel.plotlyData[0].type).toBe('scatter');
+    chartModel.setRenderOptions({ webgl: false });
+    expect(chartModel.plotlyData[0].type).toBe('scatter');
+    chartModel.setRenderOptions({ webgl: true });
+    expect(chartModel.plotlyData[0].type).toBe('scatter');
+
+    // No events should be emitted since the trace is replaceable
+    expect(mockSubscribe).toHaveBeenCalledTimes(0);
+  });
+
   it('should emit blocker events only if unreplaceable WebGL traces are present and WebGL is disabled, then blocker clear events when reenabled', async () => {
     const mockWidget = createMockWidget([SMALL_TABLE], 'scatter3d');
     const chartModel = new PlotlyExpressChartModel(
diff --git a/plugins/plotly-express/src/js/src/PlotlyExpressChartModel.ts b/plugins/plotly-express/src/js/src/PlotlyExpressChartModel.ts
index 2f0a61f3c..14314600f 100644
--- a/plugins/plotly-express/src/js/src/PlotlyExpressChartModel.ts
+++ b/plugins/plotly-express/src/js/src/PlotlyExpressChartModel.ts
@@ -18,6 +18,7 @@ import {
   removeColorsFromData,
   setWebGlTraceType,
   hasUnreplaceableWebGlTraces,
+  IS_WEBGL_SUPPORTED,
 } from './PlotlyExpressChartUtils';
 
 const log = Log.module('@deephaven/js-plugin-plotly-express.ChartModel');
@@ -120,6 +121,8 @@ export class PlotlyExpressChartModel extends ChartModel {
 
   isDownsamplingDisabled = false;
 
+  isWebGlSupported = IS_WEBGL_SUPPORTED;
+
   /**
    * Set of traces that are originally WebGL and can be replaced with non-WebGL traces.
    * These need to be replaced if WebGL is disabled and re-enabled if WebGL is enabled again.
@@ -236,7 +239,11 @@ export class PlotlyExpressChartModel extends ChartModel {
    * @param prevWebgl The previous WebGL value
    */
   handleWebGlAllowed(webgl = true, prevWebgl = true): void {
-    setWebGlTraceType(this.plotlyData, webgl, this.webGlTraceIndices);
+    setWebGlTraceType(
+      this.plotlyData,
+      webgl && this.isWebGlSupported,
+      this.webGlTraceIndices
+    );
 
     const needsBlocker = hasUnreplaceableWebGlTraces(this.plotlyData);
 
diff --git a/plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts b/plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts
index 009f46e8c..f2720b236 100644
--- a/plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts
+++ b/plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts
@@ -288,3 +288,27 @@ export function setWebGlTraceType(
     }
   });
 }
+
+/**
+ * Check if WebGL is supported in the current environment.
+ * Most modern browsers do support WebGL, but it's possible to disable it and it is also not available
+ * in some headless environments, which can affect e2e tests.
+ *
+ * https://github.com/microsoft/playwright/issues/13146
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1375585
+ *
+ * @returns True if WebGL is supported, false otherwise
+ */
+export function isWebGLSupported(): boolean {
+  try {
+    // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/By_example/Detect_WebGL
+    const canvas = document.createElement('canvas');
+    const gl =
+      canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+    return gl != null && gl instanceof WebGLRenderingContext;
+  } catch (e) {
+    return false;
+  }
+}
+
+export const IS_WEBGL_SUPPORTED = isWebGLSupported();
diff --git a/tests/app.d/express.py b/tests/app.d/express.py
index bcb120097..5004c3517 100644
--- a/tests/app.d/express.py
+++ b/tests/app.d/express.py
@@ -4,7 +4,7 @@
 import plotly.express as px
 
 
-# Test a basic deephaven plot
+# Test basic deephaven plots
 express_source = new_table(
     [
         string_col("Categories", ["A", "B", "C"]),
@@ -14,6 +14,8 @@
 )
 express_fig = dx.bar(table=express_source, x="Categories", y="Values")
 
+scatter_fig = dx.scatter(express_source, x="Values", y="Values2")
+
 # Test a basic plotly chart
 plotly_fig = px.scatter(x=[0, 1, 2, 3, 4], y=[0, 1, 4, 9, 16])
 
diff --git a/tests/express.spec.ts b/tests/express.spec.ts
index 266382940..81814f74e 100644
--- a/tests/express.spec.ts
+++ b/tests/express.spec.ts
@@ -26,7 +26,13 @@ test('Ticking loads', async ({ page }) => {
 });
 
 test('Partitioned loads', async ({ page }) => {
-    await gotoPage(page, '');
-    await openPanel(page, 'partitioned_fig', '.js-plotly-plot');
-    await expect(page.locator('.iris-chart-panel')).toHaveScreenshot();
-});
\ No newline at end of file
+  await gotoPage(page, '');
+  await openPanel(page, 'partitioned_fig', '.js-plotly-plot');
+  await expect(page.locator('.iris-chart-panel')).toHaveScreenshot();
+});
+
+test('Figure with scatter loads', async ({ page }) => {
+  await gotoPage(page, '');
+  await openPanel(page, 'scatter_fig', '.js-plotly-plot');
+  await expect(page.locator('.iris-chart-panel')).toHaveScreenshot();
+});
diff --git a/tests/express.spec.ts-snapshots/Figure-with-scatter-loads-1-chromium-linux.png b/tests/express.spec.ts-snapshots/Figure-with-scatter-loads-1-chromium-linux.png
new file mode 100644
index 000000000..806abb6d0
Binary files /dev/null and b/tests/express.spec.ts-snapshots/Figure-with-scatter-loads-1-chromium-linux.png differ
diff --git a/tests/express.spec.ts-snapshots/Figure-with-scatter-loads-1-firefox-linux.png b/tests/express.spec.ts-snapshots/Figure-with-scatter-loads-1-firefox-linux.png
new file mode 100644
index 000000000..ff0697b87
Binary files /dev/null and b/tests/express.spec.ts-snapshots/Figure-with-scatter-loads-1-firefox-linux.png differ
diff --git a/tests/express.spec.ts-snapshots/Figure-with-scatter-loads-1-webkit-linux.png b/tests/express.spec.ts-snapshots/Figure-with-scatter-loads-1-webkit-linux.png
new file mode 100644
index 000000000..766b41250
Binary files /dev/null and b/tests/express.spec.ts-snapshots/Figure-with-scatter-loads-1-webkit-linux.png differ


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces detection for WebGL support, ensuring that charts behave correctly in environments where WebGL may be disabled or unsupported. It also updates the chart model to leverage this detection and adds/updates tests for both replaceable and unreplaceable WebGL traces.
- Added a utility function to detect WebGL support.
- Updated the chart model to conditionally adjust trace types based on WebGL availability.
- Expanded test coverage by adding tests for both WebGL-supported and unsupported scenarios.

### Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts | Adds isWebGLSupported() and exports the constant, used for WebGL detection. |
| plugins/plotly-express/src/js/src/PlotlyExpressChartModel.ts | Updates to use IS_WEBGL_SUPPORTED and adjust trace types accordingly. |
| plugins/plotly-express/src/js/src/PlotlyExpressChartModel.test.ts | Adds tests for handling replaceable and unreplaceable WebGL traces. |
| tests/express.spec.ts | Adds a new E2E test for a scatter figure. |
| tests/app.d/express.py | Updates comments and adds a plotly scatter figure test. |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T17:49:51Z
user: login: mattrunyon
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T22:16:22Z
user: login: jnumainville
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T15:08:50Z
user: login: jnumainville
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T15:08:53Z
user: login: mattrunyon
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-26T17:39:31Z

Review comments:
diff_hunk: @@ -288,3 +288,23 @@ export function setWebGlTraceType(
     }
   });
 }
+
+/**
+ * Check if WebGL is supported in the current environment.
+ * Most modern browsers do support WebGL, but it's possible to disable it and it is also not available
+ * in some headless environments, which can affect e2e tests.
+ *
+ * https://github.com/microsoft/playwright/issues/13146
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1375585
+ *
+ * @returns True if Web GL is supported, false otherwise
path: plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts
user: login: Copilot
body: Consider updating 'Web GL' to 'WebGL' for consistency with the rest of the code comments and naming conventions.
```suggestion
 * @returns True if WebGL is supported, false otherwise
```
created_at: 2025-03-21T17:49:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -285,6 +288,28 @@ describe('PlotlyExpressChartModel', () => {
     expect(mockSubscribe).toHaveBeenCalledTimes(0);
   });
 
+  it('should swap replaceable WebGL traces without blocker events if WebGL is not supported', async () => {
+    const mockWidget = createMockWidget([SMALL_TABLE], 'scattergl');
+    const chartModel = new PlotlyExpressChartModel(
+      mockDh,
+      mockWidget,
+      jest.fn()
+    );
+
path: plugins/plotly-express/src/js/src/PlotlyExpressChartModel.test.ts
user: login: mattrunyon
body: I see the comment above about adding the `isWebGlSupported = true`. Even though it's the default, can you set it to false here? Should just make the test a little more resilient/less dependent on the actual supported value in the environment. For example, we _could_ run Jest with a canvas mock which could make this default to true.
created_at: 2025-03-24T22:07:59Z
author_association: COLLABORATOR
diff_hunk: @@ -288,3 +288,23 @@ export function setWebGlTraceType(
     }
   });
 }
+
+/**
+ * Check if WebGL is supported in the current environment.
+ * Most modern browsers do support WebGL, but it's possible to disable it and it is also not available
+ * in some headless environments, which can affect e2e tests.
+ *
+ * https://github.com/microsoft/playwright/issues/13146
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1375585
+ *
+ * @returns True if WebGL is supported, false otherwise
+ */
+export function isWebGLSupported(): boolean {
+  // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/By_example/Detect_WebGL
+  const canvas = document.createElement('canvas');
+  const gl =
+    canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+  return gl != null && gl instanceof WebGLRenderingContext;
+}
path: plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts
user: login: mattrunyon
body: Wrap this in a try catch that returns false in the exception in case for some reason we're in some environment that has document, but not canvas. The `getContext` would throw if `createElement('canvas')` is just an arbitrary element and not an actual canvas element.

The model is also used in the docs snapshot tool which is a Node environment and has no browser stuff like `document`, so updating in the docs tool would then run this and throw on the `createElement` call.
created_at: 2025-03-24T22:14:36Z
author_association: COLLABORATOR
diff_hunk: @@ -288,3 +288,23 @@ export function setWebGlTraceType(
     }
   });
 }
+
+/**
+ * Check if WebGL is supported in the current environment.
+ * Most modern browsers do support WebGL, but it's possible to disable it and it is also not available
+ * in some headless environments, which can affect e2e tests.
+ *
+ * https://github.com/microsoft/playwright/issues/13146
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1375585
+ *
+ * @returns True if WebGL is supported, false otherwise
+ */
+export function isWebGLSupported(): boolean {
+  // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/By_example/Detect_WebGL
+  const canvas = document.createElement('canvas');
+  const gl =
+    canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+  return gl != null && gl instanceof WebGLRenderingContext;
+}
path: plugins/plotly-express/src/js/src/PlotlyExpressChartUtils.ts
user: login: jnumainville
body: done
created_at: 2025-03-26T15:08:50Z
author_association: COLLABORATOR
diff_hunk: @@ -285,6 +288,28 @@ describe('PlotlyExpressChartModel', () => {
     expect(mockSubscribe).toHaveBeenCalledTimes(0);
   });
 
+  it('should swap replaceable WebGL traces without blocker events if WebGL is not supported', async () => {
+    const mockWidget = createMockWidget([SMALL_TABLE], 'scattergl');
+    const chartModel = new PlotlyExpressChartModel(
+      mockDh,
+      mockWidget,
+      jest.fn()
+    );
+
path: plugins/plotly-express/src/js/src/PlotlyExpressChartModel.test.ts
user: login: jnumainville
body: done
created_at: 2025-03-26T15:08:53Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-21T17:51:48Z
body: [plotly-express docs preview](https://preview.deephaven.io/core/plotly/pr-1147/docs) (Available for 14 days)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-24T15:09:41Z
body: [plotly-express docs preview](https://preview.deephaven.io/core/plotly/pr-1147/docs) (Available for 14 days)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-24T15:57:05Z
body: [plotly-express docs preview](https://preview.deephaven.io/core/plotly/pr-1147/docs) (Available for 14 days)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-26T15:11:25Z
body: [plotly-express docs preview](https://preview.deephaven.io/core/plotly/pr-1147/docs) (Available for 14 days)
author_association: NONE