Skip to content

Commit

Permalink
feat: rectangle and brush type lasso selection (#212)
Browse files Browse the repository at this point in the history
* feat: add `brush` and `rectangle` lasso types

Fix #186

* feat: add tweakpane to examples for better customizations

* feat: allow removing selected points

Fix #105

* refactor: rename `keyMap` to `actionKeyMap`

To allow triggering multiple actions with the same key.

* test: rectangle and brush lasso selections

* docs: update changelog

* docs: link ticket
  • Loading branch information
flekschas authored Jan 19, 2025
1 parent 3b3f28e commit 2bd9ac8
Show file tree
Hide file tree
Showing 31 changed files with 1,148 additions and 1,176 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## 1.13.0

- Feat: add support for two new lasso types: `'rectangle'` and `'brush'`. The lasso type can be changed via `lassoType`. Additionally, for the brush lasso, you can adjust the brush size via `lassoBrushSize`. The default lasso type is `'freeform'`. ([#186](/~https://github.com/flekschas/regl-scatterplot/issues/186))
- Feat: replace `keyMap` with `actionKeyMap` to allow triggering multiple actions with the same modifier key.
- Feat: add `'remove'` key action to allow removing selecting points. By default, to rgitemove selected points hold down `ALT` and then lasso around selected points. ([#105](/~https://github.com/flekschas/regl-scatterplot/issues/105))
- 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
Expand Down
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,7 @@ can be read and written via [`scatterplot.get()`](#scatterplot.get) and [`scatte
| pointConnectionMaxIntPointsPerSegment | int | `100` | | `true` | `false` |
| pointConnectionTolerance | float | `0.002` | | `true` | `false` |
| pointScaleMode | string | `'asinh'` | `'asinh'`, `'linear'`, or `'constant'` | `true` | `false` |
| lassoType | string | `'freeform'` | `'freeform'`, `'rectangle'`, or `'brush'` | `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 All @@ -807,11 +808,12 @@ can be read and written via [`scatterplot.get()`](#scatterplot.get) and [`scatte
| lassoLongPressAfterEffectTime | int | `500` | | `true` | `false` |
| lassoLongPressEffectDelay | int | `100` | | `true` | `false` |
| lassoLongPressRevertEffectTime | int | `250` | | `true` | `false` |
| lassoBrushSize | int | `24` | | `true` | `false` |
| showReticle | boolean | `false` | `true` or `false` | `true` | `false` |
| reticleColor | quadruple | rgba(1, 1, 1, .5) | hex, rgb, rgba | `true` | `false` |
| xScale | function | `null` | must follow the D3 scale API | `true` | `true` |
| yScale | function | `null` | must follow the D3 scale API | `true` | `true` |
| keyMap | object | `{ alt: 'rotate', shift: 'lasso' }` | See the notes below | `true` | `false` |
| actionKeyMap | object | `{ remove: 'alt': rotate: 'alt', merge: 'cmd', lasso: 'shift' }` | See the notes below | `true` | `false` |
| mouseMode | string | `'panZoom'` | `'panZoom'`, `'lasso'`, or `'rotate'` | `true` | `false` |
| performanceMode | boolean | `false` | can only be set during initialization! | `true` | `false` |
| gamma | float | `1` | to control the opacity blending | `true` | `false` |
Expand Down Expand Up @@ -911,15 +913,16 @@ You don't like the look of the lasso initiator? No problem. Simple get the DOM
element via `scatterplot.get('lassoInitiatorElement')` and adjust the style
via JavaScript. E.g.: `scatterplot.get('lassoInitiatorElement').style.background = 'green'`.

<a name="property-keymap" href="#property-keymap">#</a> <b>KeyMap:</b>
<a name="property-keymap" href="#property-keymap">#</a> <b>ActionKeyMap:</b>

The `keyMap` property is an object defining which actions are enabled when
holding down which modifier key. E.g.: `{ shift: 'lasso' }`. Acceptable
modifier keys are `alt`, `cmd`, `ctrl`, `meta`, `shift`. Acceptable actions
are `lasso`, `rotate`, and `merge` (for selecting multiple items by merging a series of lasso or click selections).
The `actionKeyMap` property is an object defining which actions are enabled when
holding down which modifier key. E.g.: `{ lasso: 'shift' }`. Acceptable actions
are `lasso`, `rotate`, `merge` (for selecting multiple items by merging a series
of lasso or click selections), and `remove` (for removing selected points).
Acceptable modifier keys are `alt`, `cmd`, `ctrl`, `meta`, `shift`.

You can also use the `keyMap` option to disable the lasso selection and rotation
by setting `keyMap` to an empty object.
You can also use the `actionKeyMap` option to disable the lasso selection and
rotation by setting `actionKeyMap` to an empty object.

<a name="property-examples" href="#property-examples">#</a> <b>Examples:</b>

Expand Down
83 changes: 7 additions & 76 deletions example/annotations.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@
/* eslint no-console: 0 */

import createScatterplot from '../src';
import { saveAsPng, checkSupport } from './utils';
import createMenu from './menu';
import { checkSupport } from './utils';

const canvas = document.querySelector('#canvas');
const numPointsEl = document.querySelector('#num-points');
const numPointsValEl = document.querySelector('#num-points-value');
const pointSizeEl = document.querySelector('#point-size');
const pointSizeValEl = document.querySelector('#point-size-value');
const opacityEl = document.querySelector('#opacity');
const opacityValEl = document.querySelector('#opacity-value');
const clickLassoInitiatorEl = document.querySelector('#click-lasso-initiator');
const resetEl = document.querySelector('#reset');
const exportEl = document.querySelector('#export');
const exampleEl = document.querySelector('#example-annotations');

exampleEl.setAttribute('class', 'active');
exampleEl.removeAttribute('href');

let points = { x: [], y: [], z: [], w: [] };
let numPoints = 100000;
Expand Down Expand Up @@ -60,15 +46,14 @@ const scatterplot = createScatterplot({
lassoMinDelay,
lassoMinDist,
pointSize,
opacity,
showReticle,
reticleColor,
lassoOnLongPress: true,
});

checkSupport(scatterplot);

exportEl.addEventListener('click', () => saveAsPng(scatterplot));

console.log(`Scatterplot v${scatterplot.get('version')}`);

scatterplot.subscribe('select', selectHandler);
Expand Down Expand Up @@ -122,74 +107,20 @@ const generatePoints = (l) => {
return { x, y, z, w };
};

const setNumPoint = (newNumPoints) => {
numPoints = newNumPoints;
numPointsEl.value = numPoints;
numPointsValEl.innerHTML = numPoints;
points = generatePoints(numPoints);
const setNumPoints = (newNumPoints) => {
points = generatePoints(newNumPoints);
scatterplot.draw(points);
};

const numPointsInputHandler = (event) => {
numPointsValEl.innerHTML = `${+event.target
.value} <em>release to redraw</em>`;
};

numPointsEl.addEventListener('input', numPointsInputHandler);

const numPointsChangeHandler = (event) => setNumPoint(+event.target.value);

numPointsEl.addEventListener('change', numPointsChangeHandler);

const setPointSize = (newPointSize) => {
pointSize = newPointSize;
pointSizeEl.value = pointSize;
pointSizeValEl.innerHTML = pointSize;
scatterplot.set({ pointSize });
};

const pointSizeInputHandler = (event) => setPointSize(+event.target.value);

pointSizeEl.addEventListener('input', pointSizeInputHandler);

const setOpacity = (newOpacity) => {
opacity = newOpacity;
opacityEl.value = opacity;
opacityValEl.innerHTML = opacity;
scatterplot.set({ opacity });
};

const opacityInputHandler = (event) => setOpacity(+event.target.value);

opacityEl.addEventListener('input', opacityInputHandler);

const clickLassoInitiatorChangeHandler = (event) => {
scatterplot.set({
lassoInitiator: event.target.checked,
});
};

clickLassoInitiatorEl.addEventListener(
'change',
clickLassoInitiatorChangeHandler
);
clickLassoInitiatorEl.checked = scatterplot.get('lassoInitiator');

const resetClickHandler = () => {
scatterplot.reset();
};

resetEl.addEventListener('click', resetClickHandler);
createMenu({ scatterplot, setNumPoints });

const colorsCat = ['#3a78aa', '#aa3a99'];
scatterplot.set({ colorBy: 'category', pointColor: colorsCat });

const colorsScale = ['#009E73', '#CC79A7', '#56B4E9', '#F0E442'];
scatterplot.set({ colorBy: 'z', pointColor: colorsScale });

setPointSize(pointSize);
setOpacity(opacity);
setNumPoint(numPoints);
setNumPoints(numPoints);

scatterplot.drawAnnotations([
{ x: 0, lineColor: [1, 1, 1, 0.1], lineWidth: 1 },
Expand Down
84 changes: 7 additions & 77 deletions example/axes.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
/* eslint no-console: 0 */

import { axisBottom, axisRight } from 'd3-axis';
import { scaleLinear } from 'd3-scale';
import { select } from 'd3-selection';

import createScatterplot from '../src';
import { saveAsPng, checkSupport } from './utils';
import createMenu from './menu';
import { checkSupport } from './utils';

const parentWrapper = document.querySelector('#parent-wrapper');
const canvasWrapper = document.querySelector('#canvas-wrapper');
const canvas = document.querySelector('#canvas');
const numPointsEl = document.querySelector('#num-points');
const numPointsValEl = document.querySelector('#num-points-value');
const pointSizeEl = document.querySelector('#point-size');
const pointSizeValEl = document.querySelector('#point-size-value');
const opacityEl = document.querySelector('#opacity');
const opacityValEl = document.querySelector('#opacity-value');
const clickLassoInitiatorEl = document.querySelector('#click-lasso-initiator');
const resetEl = document.querySelector('#reset');
const exportEl = document.querySelector('#export');
const exampleBg = document.querySelector('#example-axes');

exampleBg.setAttribute('class', 'active');
exampleBg.removeAttribute('href');

const xDomain = [0, 42];
const yDomain = [0, 4.2];
Expand Down Expand Up @@ -80,6 +66,7 @@ const deselectHandler = () => {
const scatterplot = createScatterplot({
canvas,
pointSize,
opacity,
xScale,
yScale,
showReticle: true,
Expand All @@ -88,8 +75,6 @@ const scatterplot = createScatterplot({

checkSupport(scatterplot);

exportEl.addEventListener('click', () => saveAsPng(scatterplot));

console.log(`Scatterplot v${scatterplot.get('version')}`);

scatterplot.subscribe('select', selectHandler);
Expand Down Expand Up @@ -130,65 +115,12 @@ const generatePoints = (num) =>
Math.random(), // value
]);

const setNumPoint = (newNumPoints) => {
numPoints = newNumPoints;
numPointsEl.value = numPoints;
numPointsValEl.innerHTML = numPoints;
points = generatePoints(numPoints);

const setNumPoints = (newNumPoints) => {
points = generatePoints(newNumPoints);
scatterplot.draw(points);
};

const numPointsInputHandler = (event) => {
numPointsValEl.innerHTML = `${+event.target
.value} <em>release to redraw</em>`;
};

numPointsEl.addEventListener('input', numPointsInputHandler);

const numPointsChangeHandler = (event) => setNumPoint(+event.target.value);

numPointsEl.addEventListener('change', numPointsChangeHandler);

const setPointSize = (newPointSize) => {
pointSize = newPointSize;
pointSizeEl.value = pointSize;
pointSizeValEl.innerHTML = pointSize;
scatterplot.set({ pointSize });
};

const pointSizeInputHandler = (event) => setPointSize(+event.target.value);

pointSizeEl.addEventListener('input', pointSizeInputHandler);

const setOpacity = (newOpacity) => {
opacity = newOpacity;
opacityEl.value = opacity;
opacityValEl.innerHTML = opacity;
scatterplot.set({ opacity });
};

const opacityInputHandler = (event) => setOpacity(+event.target.value);

opacityEl.addEventListener('input', opacityInputHandler);

const clickLassoInitiatorChangeHandler = (event) => {
scatterplot.set({
lassoInitiator: event.target.checked,
});
};

clickLassoInitiatorEl.addEventListener(
'change',
clickLassoInitiatorChangeHandler
);
clickLassoInitiatorEl.checked = scatterplot.get('lassoInitiator');

const resetClickHandler = () => {
scatterplot.reset();
};

resetEl.addEventListener('click', resetClickHandler);
createMenu({ scatterplot, setNumPoints });

scatterplot.set({
colorBy: 'category',
Expand All @@ -203,6 +135,4 @@ scatterplot.set({
],
});

setPointSize(pointSize);
setOpacity(opacity);
setNumPoint(numPoints);
setNumPoints(numPoints);
Loading

0 comments on commit 2bd9ac8

Please sign in to comment.