Skip to content

Commit 7501785

Browse files
committed
Optimize density-based opacity encoding
1 parent bb62e82 commit 7501785

3 files changed

Lines changed: 22 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Next
22

3+
- Optimize density-based opacity encoding
4+
35
## v0.18.5
46

57
- Fix an issue with the lasso position when the page is scrollable (#50)

src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export const DEFAULT_HEIGHT = 'auto';
7878
export const DEFAULT_GAMMA = 1;
7979

8080
// Default styles
81+
export const MIN_POINT_SIZE = 1;
8182
export const DEFAULT_POINT_SIZE = 6;
8283
export const DEFAULT_POINT_SIZE_SELECTED = 2;
8384
export const DEFAULT_POINT_OUTLINE_WIDTH = 2;

src/index.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import {
6262
DEFAULT_POINT_CONNECTION_INT_POINTS_TOLERANCE,
6363
DEFAULT_SHOW_POINT_CONNECTIONS,
6464
DEFAULT_POINT_OUTLINE_WIDTH,
65+
MIN_POINT_SIZE,
6566
DEFAULT_POINT_SIZE,
6667
DEFAULT_POINT_SIZE_SELECTED,
6768
DEFAULT_POINT_SIZE_MOUSE_DETECTION,
@@ -328,6 +329,8 @@ const createScatterplot = (initialProperties = {}) => {
328329
? [...pointSize]
329330
: [pointSize];
330331

332+
let minPointScale = MIN_POINT_SIZE / pointSize[0];
333+
331334
if (pointConnectionColor === 'inherit') {
332335
pointConnectionColor = [...pointColor];
333336
} else {
@@ -1038,6 +1041,7 @@ const createScatterplot = (initialProperties = {}) => {
10381041

10391042
if (isStrictlyPositiveNumber(+newPointSize)) pointSize = [+newPointSize];
10401043

1044+
minPointScale = MIN_POINT_SIZE / pointSize[0];
10411045
encodingTex = createEncodingTexture();
10421046
computePointSizeMouseDetection();
10431047
};
@@ -1163,8 +1167,16 @@ const createScatterplot = (initialProperties = {}) => {
11631167
const getModel = () => model;
11641168
const getProjectionViewModel = () =>
11651169
mat4.multiply(pvm, projection, mat4.multiply(pvm, camera.view, model));
1166-
const getPointScale = () =>
1167-
1 + Math.log2(max(1.0, camera.scaling)) * window.devicePixelRatio;
1170+
const getPointScale = () => {
1171+
if (camera.scaling > 1)
1172+
return (
1173+
(Math.asinh(max(1.0, camera.scaling)) / Math.asinh(1)) *
1174+
window.devicePixelRatio
1175+
);
1176+
1177+
return max(minPointScale, camera.scaling) * window.devicePixelRatio;
1178+
};
1179+
// 1 + Math.log2(max(1.0, camera.scaling)) * window.devicePixelRatio;
11681180
const getNormalNumPoints = () => numPoints;
11691181
const getIsColoredByZ = () => +(colorBy === 'valueZ');
11701182
const getIsColoredByW = () => +(colorBy === 'valueW');
@@ -1191,22 +1203,22 @@ const createScatterplot = (initialProperties = {}) => {
11911203
// Adopted from the fabulous Ricky Reusser:
11921204
// https://observablehq.com/@rreusser/selecting-the-right-opacity-for-2d-point-clouds
11931205
// Extended with a point-density based approach
1194-
const pointScale = getPointScale();
1195-
const smallestPointSize = pointSize.length === 1 ? pointSize[0] : pointSize;
1196-
const p = smallestPointSize * pointScale;
1206+
const pointScale = getPointScale(true);
1207+
const p = pointSize[0] * pointScale;
11971208

11981209
// Compute the plot's x and y range from the view matrix, though these could come from any source
11991210
const s = (2 / (2 / camera.view[0])) * (2 / (2 / camera.view[5]));
12001211

12011212
// Viewport size, in device pixels
12021213
const H = context.viewportHeight;
1214+
const W = context.viewportWidth;
12031215

12041216
// Adaptation: Instead of using the global number of points, I am using a
12051217
// density-based approach that takes the points in the view into context
12061218
// when zooming in. This ensure that in sparse areas, points are opaque and
12071219
// in dense areas points are more translucent.
12081220
let alpha =
1209-
((opacityByDensityFill * H * H) / (numPointsInView * p * p)) * min(1, s);
1221+
((opacityByDensityFill * W * H) / (numPointsInView * p * p)) * min(1, s);
12101222

12111223
// In performanceMode we use squares, otherwise we use circles, which only
12121224
// take up (pi r^2) of the unit square
@@ -1215,7 +1227,7 @@ const createScatterplot = (initialProperties = {}) => {
12151227
// If the pixels shrink below the minimum permitted size, then we adjust the opacity instead
12161228
// and apply clamping of the point size in the vertex shader. Note that we add 0.5 since we
12171229
// slightly inrease the size of points during rendering to accommodate SDF-style antialiasing.
1218-
const clampedPointDeviceSize = max(smallestPointSize, p) + 0.5;
1230+
const clampedPointDeviceSize = max(MIN_POINT_SIZE, p) + 0.5;
12191231

12201232
// We square this since we're concerned with the ratio of *areas*.
12211233
// eslint-disable-next-line no-restricted-properties

0 commit comments

Comments
 (0)