Repository: flekschas/regl-scatterplot (js)

Issue:
Split up performance mode options
Right now there is a single "performanceMode" option. This both makes the points render as squares (easier on the fragment shader), and disables blending. Our application depends on having alpha blending, but point shapes aren't nearly as important. Some of our users only have a low-tier integrated GPU and we have ~11m points, so we could greatly benefit from just having the blending instead of both blend mode and shader change. Would it be possible to separate those two?

With kind regards & thanks in advance!

Pull request:
feat: add `renderPointsAsSquares` and `disableAlphaBlending`
Expose `renderPointsAsSquares` and `disableAlphaBlending` to allow finer control over performance increasing settings.

## Description

> What was changed in this pull request?

Added two props:
1. `renderPointsAsSquares`
2. `disableAlphaBlending`

Note that if `performanceMode` is `true`, both props are automatically true.

> Why is it necessary?

Fixes #206 

> Examples

**Normal**

![Screenshot 2025-01-12 at 2 19 21 PM](https://github.com/user-attachments/assets/80718150-1b66-4b38-81a7-1b15aeb0bed0)

**No Alpha Blending**

![Screenshot 2025-01-12 at 2 19 39 PM](https://github.com/user-attachments/assets/b3d69349-b72e-4b9f-ab20-bf930479b8c0)

**Squared Points** 

![Screenshot 2025-01-12 at 2 19 57 PM](https://github.com/user-attachments/assets/8e746399-50bf-489d-a729-28526259d297)

## Checklist

- [x] Provided a concise title as a [semantic commit message](https://www.conventionalcommits.org) (e.g. "fix: correctly handle undefined properties")
- [x] `CHANGELOG.md` updated
- [x] Tests added or updated
- [x] Documentation in `README.md` added or updated
- [ ] Example(s) added or updated
- [x] Screenshot, gif, or video attached for visual changes


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9d96c2..974ba6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
+## 1.13.0
+
+- Feat: expose `renderPointsAsSquares` and `disableAlphaBlending` to allow finer control over performance increasing settings ([#206](https://github.com/flekschas/regl-scatterplot/issues/206))
+
 ## 1.12.1
 
+- Fix: prevent drawing points if all are filtered out ([#201](https://github.com/flekschas/regl-scatterplot/issues/201))
 - Fix: destroy the encoding texture before recreating it
 - Fix: reject `set()` calls if the instance was destroyed
 - Fix: ensures unnecessary color and encoding texture updates are avoided
diff --git a/README.md b/README.md
index 73630c4..080dab4 100644
--- a/README.md
+++ b/README.md
@@ -823,6 +823,8 @@ can be read and written via [`scatterplot.get()`](#scatterplot.get) and [`scatte
 | annotationHVLineLimit                 | number                                       | `1000`                              | the extent of horizontal or vertical lines                      | `true`   | `false`     |
 | antiAliasing                          | number                                       | `0.5`                               | higher values result in more blurry points                      | `true`   | `false`     |
 | pixelAligned                          | number                                       | `false`                             | if true, points are aligned with the pixel grid                 | `true`   | `false`     |
+| renderPointsAsSquares                 | boolean                                      | `false`                             | true of `performanceMode` is true. can only be set on init!     | `true`   | `false`     |
+| disableAlphaBlending                  | boolean                                      | `false`                             | true of `performanceMode` is true. can only be set on init!     | `true`   | `false`     |
 
 <a name="property-notes" href="#property-notes">#</a> <b>Notes:</b>
 
@@ -863,10 +865,12 @@ can be read and written via [`scatterplot.get()`](#scatterplot.get) and [`scatte
 - If you need to draw more than 2 million points, you might want to set
   `performanceMode` to `true` during the initialization to boost the
   performance. In performance mode, points will be drawn as simple squares and
-  color blending is disabled. This should allow you to draw up to 20 million
+  alpha blending is disabled. This should allow you to draw up to 20 million
   points (or more depending on your hardware). Make sure to reduce the
   `pointSize` as you render more and more points (e.g., `0.25` for 20 million
-  works for me) to ensure good performance.
+  works for me) to ensure good performance. You can also enable squared points
+  and disable alpha blending individually via `renderPointsAsSquares` and
+  `disableAlphaBlending` respectively.
 
 <a name="property-by" href="#property-by">#</a> <b>colorBy, opacityBy, sizeBy:</b>
 
diff --git a/package-lock.json b/package-lock.json
index 1a1ef9f..f1aa602 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "regl-scatterplot",
-  "version": "1.12.0",
+  "version": "1.12.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "regl-scatterplot",
-      "version": "1.12.0",
+      "version": "1.12.1",
       "license": "MIT",
       "dependencies": {
         "@flekschas/utils": "^0.32.2",
diff --git a/package.json b/package.json
index b43763d..372ec4b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "regl-scatterplot",
-  "version": "1.12.0",
+  "version": "1.12.1",
   "description": "A WebGL-Powered Scalable Interactive Scatter Plot Library",
   "author": "Fritz Lekschas",
   "keywords": [
diff --git a/src/index.js b/src/index.js
index b505567..2ec147b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -310,6 +310,13 @@ const createScatterplot = (
     spatialIndexUseWorker = DEFAULT_SPATIAL_INDEX_USE_WORKER,
   } = initialProperties;
 
+  const renderPointsAsSquares = Boolean(
+    initialProperties.renderPointsAsSquares || performanceMode,
+  );
+  const disableAlphaBlending = Boolean(
+    initialProperties.disableAlphaBlending || performanceMode,
+  );
+
   mouseMode = limit(MOUSE_MODES, MOUSE_MODE_PANZOOM)(mouseMode);
 
   if (!renderer) {
@@ -1596,9 +1603,9 @@ const createScatterplot = (
     let alpha =
       ((opacityByDensityFill * W * H) / (numPointsInView * p * p)) * min(1, s);
 
-    // In performanceMode we use squares, otherwise we use circles, which only
-    // take up (pi r^2) of the unit square
-    alpha *= performanceMode ? 1 : 1 / (0.25 * Math.PI);
+    // Unless `renderPointsAsSquares` is true, we use circles, which only take
+    // up (pi r^2) of the unit square
+    alpha *= renderPointsAsSquares ? 1 : 1 / (0.25 * Math.PI);
 
     // If the pixels shrink below the minimum permitted size, then we adjust the opacity instead
     // and apply clamping of the point size in the vertex shader. Note that we add 0.5 since we
@@ -1641,11 +1648,11 @@ const createScatterplot = (
     getPointOpacityScale = getPointOpacityScaleBase,
   ) =>
     renderer.regl({
-      frag: performanceMode ? POINT_SIMPLE_FS : POINT_FS,
+      frag: renderPointsAsSquares ? POINT_SIMPLE_FS : POINT_FS,
       vert: createVertexShader(globalState),
 
       blend: {
-        enable: !performanceMode,
+        enable: !disableAlphaBlending,
         func: {
           // biome-ignore lint/style/useNamingConvention: Regl specific
           srcRGB: 'src alpha',
@@ -2270,7 +2277,7 @@ const createScatterplot = (
     const filteredSelectedPoints = [];
 
     for (const pointIdx of pointIdxsArray) {
-      if (pointIdx < 0 || pointIdx >= numPoints) {
+      if (!Number.isFinite(pointIdx) || pointIdx < 0 || pointIdx >= numPoints) {
         // Skip invalid filtered points
         continue;
       }
@@ -3611,6 +3618,14 @@ const createScatterplot = (
       return performanceMode;
     }
 
+    if (property === 'renderPointsAsSquares') {
+      return renderPointsAsSquares;
+    }
+
+    if (property === 'disableAlphaBlending') {
+      return disableAlphaBlending;
+    }
+
     if (property === 'gamma') {
       return renderer.gamma;
     }
@@ -4322,7 +4337,8 @@ const createScatterplot = (
         });
       }
 
-      if (isPointsDrawn) {
+      const numPoints = getNormalNumPoints();
+      if (isPointsDrawn && numPoints > 0) {
         drawPointBodies();
       }
 
diff --git a/src/types.d.ts b/src/types.d.ts
index 59b7fbf..78bf430 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -210,6 +210,8 @@ export type Properties = {
   lassoLongPressIndicatorParentElement: HTMLElement;
   camera: Camera2D;
   performanceMode: boolean;
+  renderPointsAsSquares: boolean;
+  disableAlphaBlending: boolean;
   opacityByDensityDebounceTime: number;
   spatialIndex: ArrayBuffer;
   spatialIndexUseWorker: undefined | boolean;


Test patch:
diff --git a/tests/get-set.test.js b/tests/get-set.test.js
index b5bbfc9..e80b24c 100644
--- a/tests/get-set.test.js
+++ b/tests/get-set.test.js
@@ -728,7 +728,6 @@ test('get("isDrawing")', async () => {
   scatterplot.destroy();
 });
 
-
 test('set() after destroy', async () => {
   const scatterplot = createScatterplot({ canvas: createCanvas() });
 
@@ -738,3 +737,57 @@ test('set() after destroy', async () => {
 
   await expect(whenSet).rejects.toThrow(ERROR_INSTANCE_IS_DESTROYED);
 });
+
+
+test('get() and set() performance properties', async () => {
+  const scatterplotA = createScatterplot({ canvas: createCanvas() });
+
+  expect(scatterplotA.get('performanceMode')).toBe(false);
+  expect(scatterplotA.get('renderPointsAsSquares')).toBe(false);
+  expect(scatterplotA.get('disableAlphaBlending')).toBe(false);
+
+  scatterplotA.set({
+    'performanceMode': true,
+    'renderPointsAsSquares': true,
+    'disableAlphaBlending': true,
+  });
+
+  expect(scatterplotA.get('performanceMode')).toBe(false);
+  expect(scatterplotA.get('renderPointsAsSquares')).toBe(false);
+  expect(scatterplotA.get('disableAlphaBlending')).toBe(false);
+
+  scatterplotA.destroy();
+
+  const scatterplotB = createScatterplot({
+    canvas: createCanvas(),
+    performanceMode: true,
+  });
+
+  expect(scatterplotB.get('performanceMode')).toBe(true);
+  expect(scatterplotB.get('renderPointsAsSquares')).toBe(true);
+  expect(scatterplotB.get('disableAlphaBlending')).toBe(true);
+
+  scatterplotB.destroy();
+
+  const scatterplotC = createScatterplot({
+    canvas: createCanvas(),
+    renderPointsAsSquares: true,
+  });
+
+  expect(scatterplotC.get('performanceMode')).toBe(false);
+  expect(scatterplotC.get('renderPointsAsSquares')).toBe(true);
+  expect(scatterplotC.get('disableAlphaBlending')).toBe(false);
+
+  scatterplotC.destroy();
+
+  const scatterplotD = createScatterplot({
+    canvas: createCanvas(),
+    disableAlphaBlending: true,
+  });
+
+  expect(scatterplotD.get('performanceMode')).toBe(false);
+  expect(scatterplotD.get('renderPointsAsSquares')).toBe(false);
+  expect(scatterplotD.get('disableAlphaBlending')).toBe(true);
+
+  scatterplotD.destroy();
+});