Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for linear and constant point scaling via a new property called pointScaleMode #195

Merged
merged 8 commits into from
Sep 28, 2024
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.11.0

- Feat: add support for linear and constant point scaling via a new property called `pointScaleMode`.

## 1.10.4

- Fix: restrict the renderer's canvas to be at most as large as the screen. Previously we had the canvas size bound to `window.innerWidth` and `window.innerHeight`. However, in VSCode it was possible that `window.innerHeight` was muuuuch larger than the actual screen, which in turn caused a WebGL error (invalid renderbuffer size). This issues was first reported at https://github.com/flekschas/jupyter-scatter/issues/37.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,7 @@ can be read and written via [`scatterplot.get()`](#scatterplot.get) and [`scatte
| pointConnectionSizeBy | string | `null` | See [data encoding](#property-point-conntection-by) | `true` | `false` |
| pointConnectionMaxIntPointsPerSegment | int | `100` | | `true` | `false` |
| pointConnectionTolerance | float | `0.002` | | `true` | `false` |
| pointScale | string | `'asinh'` | `'asinh'`, `'linear'`, or `'constant'` | `true` | `false` |
| lassoColor | quadruple | rgba(0, 0.667, 1, 1) | hex, rgb, rgba | `true` | `false` |
| lassoLineWidth | float | 2 | >= 1 | `true` | `false` |
| lassoMinDelay | int | 15 | >= 0 | `true` | `false` |
Expand Down
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export const DEFAULT_GAMMA = 1;

// Default styles
export const MIN_POINT_SIZE = 1;
export const DEFAULT_POINT_SCALE_MODE = 'asinh';
export const DEFAULT_POINT_SIZE = 6;
export const DEFAULT_POINT_SIZE_SELECTED = 2;
export const DEFAULT_POINT_OUTLINE_WIDTH = 2;
Expand Down
49 changes: 45 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
DEFAULT_POINT_CONNECTION_SIZE_ACTIVE,
DEFAULT_POINT_CONNECTION_SIZE_BY,
DEFAULT_POINT_OUTLINE_WIDTH,
DEFAULT_POINT_SCALE_MODE,
DEFAULT_POINT_SIZE,
DEFAULT_POINT_SIZE_MOUSE_DETECTION,
DEFAULT_POINT_SIZE_SELECTED,
Expand Down Expand Up @@ -278,6 +279,7 @@ const createScatterplot = (
opacityInactiveMax = DEFAULT_OPACITY_INACTIVE_MAX,
opacityInactiveScale = DEFAULT_OPACITY_INACTIVE_SCALE,
sizeBy = DEFAULT_SIZE_BY,
pointScaleMode = DEFAULT_POINT_SCALE_MODE,
height = DEFAULT_HEIGHT,
width = DEFAULT_WIDTH,
annotationLineColor = DEFAULT_ANNOTATION_LINE_COLOR,
Expand Down Expand Up @@ -1475,16 +1477,27 @@ const createScatterplot = (
const getModel = () => model;
const getModelViewProjection = () =>
mat4.multiply(pvm, projection, mat4.multiply(pvm, camera.view, model));
const getPointScale = () => {
const getConstantPointScale = () => {
return window.devicePixelRatio;
};
const getLinearPointScale = () => {
return max(minPointScale, camera.scaling[0]) * window.devicePixelRatio;
};
const getAsinhPointScale = () => {
if (camera.scaling[0] > 1) {
return (
(Math.asinh(max(1.0, camera.scaling[0])) / Math.asinh(1)) *
window.devicePixelRatio
);
}

return max(minPointScale, camera.scaling[0]) * window.devicePixelRatio;
};
let getPointScale = getAsinhPointScale;
if (pointScaleMode === 'linear') {
getPointScale = getLinearPointScale;
} else if (pointScaleMode === 'constant') {
getPointScale = getConstantPointScale;
}
const getNormalNumPoints = () =>
isPointsFiltered ? filteredPointsSet.size : numPoints;
const getSelectedNumPoints = () => selectedPoints.length;
Expand Down Expand Up @@ -1525,7 +1538,7 @@ const createScatterplot = (
// Adopted from the fabulous Ricky Reusser:
// https://observablehq.com/@rreusser/selecting-the-right-opacity-for-2d-point-clouds
// Extended with a point-density based approach
const pointScale = getPointScale(true);
const pointScale = getPointScale();
const p = pointSize[0] * pointScale;

// Compute the plot's x and y range from the view matrix, though these could come from any source
Expand Down Expand Up @@ -1615,7 +1628,7 @@ const createScatterplot = (
resolution: getResolution,
modelViewProjection: getModelViewProjection,
devicePixelRatio: getDevicePixelRatio,
pointScale: getPointScale,
pointScale: () => getPointScale(),
encodingTex: getEncodingTex,
encodingTexRes: getEncodingTexRes,
encodingTexEps: getEncodingTexEps,
Expand Down Expand Up @@ -3186,6 +3199,26 @@ const createScatterplot = (
computePointSizeMouseDetection();
};

const setPointScaleMode = (newPointScaleMode) => {
switch (newPointScaleMode) {
case 'linear': {
pointScaleMode = newPointScaleMode;
getPointScale = getLinearPointScale;
break;
}
case 'constant': {
pointScaleMode = newPointScaleMode;
getPointScale = getConstantPointScale;
break;
}
default: {
pointScaleMode = 'asinh';
getPointScale = getAsinhPointScale;
break;
}
}
};

const setOpacityByDensityFill = (newOpacityByDensityFill) => {
opacityByDensityFill = +newOpacityByDensityFill;
};
Expand Down Expand Up @@ -3457,6 +3490,10 @@ const createScatterplot = (
return pointConnectionTolerance;
}

if (property === 'pointScaleMode') {
return pointScaleMode;
}

if (property === 'reticleColor') {
return reticleColor;
}
Expand Down Expand Up @@ -3661,6 +3698,10 @@ const createScatterplot = (
setPointConnectionTolerance(properties.pointConnectionTolerance);
}

if (properties.pointScaleMode !== undefined) {
setPointScaleMode(properties.pointScaleMode);
}

if (properties.opacityBy !== undefined) {
setOpacityBy(properties.opacityBy);
}
Expand Down
3 changes: 3 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type KeyMap = Record<'alt' | 'cmd' | 'ctrl' | 'meta' | 'shift', KeyAction>;

type MouseMode = 'panZoom' | 'lasso' | 'rotate';

type PointScaleMode = 'constant' | 'asinh' | 'linear';

// biome-ignore lint/style/useNamingConvention: ZWData are three words, z, w, and data
type ZWDataType = 'continuous' | 'categorical';

Expand Down Expand Up @@ -159,6 +161,7 @@ interface BaseOptions {
opacityBy: null | DataEncoding;
xScale: null | Scale;
yScale: null | Scale;
pointScaleMode: PointScaleMode;
}

// biome-ignore lint/style/useNamingConvention: KDBush is a library name
Expand Down
80 changes: 80 additions & 0 deletions tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
DEFAULT_POINT_CONNECTION_OPACITY_ACTIVE,
DEFAULT_POINT_CONNECTION_SIZE,
DEFAULT_POINT_CONNECTION_SIZE_ACTIVE,
DEFAULT_POINT_SCALE_MODE,
DEFAULT_GAMMA,
KEY_ACTION_LASSO,
KEY_ACTION_ROTATE,
Expand Down Expand Up @@ -3302,6 +3303,85 @@ test('regl-scatterplot', async (t2) => {
scatterplot.destroy();
})
);

await t2.test(
'pointScaleMode',
catchError(async (t) => {
const dim = 100;
const scatterplot = createScatterplot({
canvas: createCanvas(dim, dim),
width: dim,
height: dim,
pointSize: 10,
});

t.equal(
scatterplot.get('pointScaleMode'),
DEFAULT_POINT_SCALE_MODE,
`The default point scale mode should be ${DEFAULT_POINT_SCALE_MODE}`
);

await scatterplot.draw([[0, 0]]);

const initialImage = scatterplot.export();
const initialPixelSum = getPixelSum(initialImage, 0, dim, 0, dim);

t.ok(initialPixelSum > 0, 'The point should be drawn');

// Zoom in a bit
await scatterplot.zoomToLocation([0, 0], 0.5);

const asinhImage = scatterplot.export();
const asinhPixelSum = getPixelSum(asinhImage, 0, dim, 0, dim);

t.ok(asinhPixelSum > initialPixelSum, 'The point should be larger');

// Zoom back to the origin
await scatterplot.zoomToLocation([0, 0], 1);

scatterplot.set({ pointScaleMode: 'constant' });
t.equal(
scatterplot.get('pointScaleMode'),
'constant',
'The new point scale mode should be constant'
);

// Zoom in a bit
await scatterplot.zoomToLocation([0, 0], 0.5);

const constantImage = scatterplot.export();
const constantPixelSum = getPixelSum(constantImage, 0, dim, 0, dim);

t.ok(constantPixelSum === initialPixelSum, 'The point should be unchanged');

// Zoom back to the origin
await scatterplot.zoomToLocation([0, 0], 1);

scatterplot.set({ pointScaleMode: 'linear' });
t.equal(
scatterplot.get('pointScaleMode'),
'linear',
'The new point scale mode should be linear'
);

// Zoom in a bit
await scatterplot.zoomToLocation([0, 0], 0.5);

const linearImage = scatterplot.export();
const linearPixelSum = getPixelSum(linearImage, 0, dim, 0, dim);

t.ok(linearPixelSum > asinhPixelSum, 'The point should be larger');

scatterplot.set({ pointScaleMode: 'nonsense' });
t.equal(
scatterplot.get('pointScaleMode'),
'asinh',
'The point scale mode should default to "asinh" for invalid values'
);

scatterplot.destroy();
})
);
});

/* --------------------------------- Utils ---------------------------------- */
Expand Down