Skip to content

Commit 2be382e

Browse files
committed
Add the ability to color point connections by their segment via pointConnectionColorBy: 'segment'
1 parent a339745 commit 2be382e

7 files changed

Lines changed: 142 additions & 35 deletions

File tree

CHANGELOG.md

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

3+
## v0.17.0
4+
5+
- Add the ability to color point connections by their segment via `pointConnectionColorBy: 'segment'`
36
- Fix an issue with normalizing RGB(A) values
47

58
## v0.16.2

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,8 +441,11 @@ the forth component use `value` (only for backwards compatibility), `value2`,
441441

442442
In addition to the properties understood by [`colorBy`, etc.](#property-by),
443443
`pointConnectionColorBy`, `pointConnectionOpacityBy`, and `pointConnectionSizeBy`
444-
also understand `"inherit"`. When set to `"inherit"` the value will be inherited
445-
from its point-specific counterpart.
444+
also understand `"inherit"` and `"segment"`. When set to `"inherit"`, the value
445+
will be inherited from its point-specific counterpart. When set to `"segment"`,
446+
each segment of a point connection will be encoded separately. This allows you
447+
to, for instance, color connection by a gradient from the start to the end of
448+
each line.
446449

447450
<a name="property-lassoInitiator" href="#property-lassoInitiator">#</a> <b>lassoInitiator:</b>
448451

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@
3131
"watch": "rollup -cw"
3232
},
3333
"dependencies": {
34-
"@flekschas/utils": "^0.27.0",
34+
"@flekschas/utils": "^0.28.0",
3535
"camera-2d-simple": "~2.2.1",
3636
"dom-2d-camera": "~1.2.3",
3737
"gl-matrix": "~3.3.0",
3838
"kdbush": "~3.0.0",
3939
"lodash-es": "~4.17.21",
4040
"pub-sub-es": "~2.0.1",
4141
"regl": "~1.3.13",
42-
"regl-line": "~0.4.2",
42+
"regl-line": "~0.4.3",
4343
"with-raf": "~1.1.1"
4444
},
4545
"peerDependencies": {

src/index.js

Lines changed: 123 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import createPubSub from 'pub-sub-es';
44
import withRaf from 'with-raf';
55
import { mat4, vec4 } from 'gl-matrix';
66
import createLine from 'regl-line';
7-
import { identity, unionIntegers } from '@flekschas/utils';
7+
import { identity, rangeMap, unionIntegers } from '@flekschas/utils';
88

99
import createLassoManager from './lasso-manager';
1010

@@ -101,6 +101,7 @@ import {
101101
getBBox,
102102
isConditionalArray,
103103
isPositiveNumber,
104+
isStrictlyPositiveNumber,
104105
isMultipleColors,
105106
isPointInPolygon,
106107
isString,
@@ -126,7 +127,11 @@ const checkDeprecations = (properties) => {
126127
});
127128
};
128129

129-
const getEncodingType = (type, defaultValue) => {
130+
const getEncodingType = (
131+
type,
132+
defaultValue,
133+
{ allowSegment = false } = false
134+
) => {
130135
switch (type) {
131136
case 'category':
132137
case 'value1':
@@ -142,6 +147,9 @@ const getEncodingType = (type, defaultValue) => {
142147
case 'w':
143148
return 'valueW'; // W refers to the 4th component of the RGBA value
144149

150+
case 'segment':
151+
return allowSegment ? 'segment' : defaultValue;
152+
145153
default:
146154
return defaultValue;
147155
}
@@ -351,13 +359,25 @@ const createScatterplot = (initialProperties = {}) => {
351359
: [pointConnectionSize];
352360
}
353361

354-
colorBy = getEncodingType(colorBy);
355-
opacityBy = getEncodingType(opacityBy);
356-
sizeBy = getEncodingType(sizeBy);
362+
colorBy = getEncodingType(colorBy, DEFAULT_COLOR_BY);
363+
opacityBy = getEncodingType(opacityBy, DEFAULT_OPACITY_BY);
364+
sizeBy = getEncodingType(sizeBy, DEFAULT_SIZE_BY);
357365

358-
pointConnectionColorBy = getEncodingType(pointConnectionColorBy);
359-
pointConnectionOpacityBy = getEncodingType(pointConnectionOpacityBy);
360-
pointConnectionSizeBy = getEncodingType(pointConnectionSizeBy);
366+
pointConnectionColorBy = getEncodingType(
367+
pointConnectionColorBy,
368+
DEFAULT_POINT_CONNECTION_COLOR_BY,
369+
{ allowSegment: true }
370+
);
371+
pointConnectionOpacityBy = getEncodingType(
372+
pointConnectionOpacityBy,
373+
DEFAULT_POINT_CONNECTION_OPACITY_BY,
374+
{ allowSegment: true }
375+
);
376+
pointConnectionSizeBy = getEncodingType(
377+
pointConnectionSizeBy,
378+
DEFAULT_POINT_CONNECTION_SIZE_BY,
379+
{ allowSegment: true }
380+
);
361381

362382
let stateTex; // Stores the point texture holding x, y, category, and value
363383
let prevStateTex; // Stores the previous point texture. Used for transitions
@@ -959,7 +979,7 @@ const createScatterplot = (initialProperties = {}) => {
959979
if (isConditionalArray(newPointSize, isPositiveNumber, { minLength: 1 }))
960980
pointSize = [...newPointSize];
961981

962-
if (isPositiveNumber(+newPointSize)) pointSize = [+newPointSize];
982+
if (isStrictlyPositiveNumber(+newPointSize)) pointSize = [+newPointSize];
963983

964984
encodingTex = createEncodingTexture();
965985
computePointSizeMouseDetection();
@@ -989,7 +1009,7 @@ const createScatterplot = (initialProperties = {}) => {
9891009
if (isConditionalArray(newOpacity, isPositiveNumber, { minLength: 1 }))
9901010
opacity = [...newOpacity];
9911011

992-
if (isPositiveNumber(+newOpacity)) opacity = [+newOpacity];
1012+
if (isStrictlyPositiveNumber(+newOpacity)) opacity = [+newOpacity];
9931013

9941014
encodingTex = createEncodingTexture();
9951015
};
@@ -1007,14 +1027,14 @@ const createScatterplot = (initialProperties = {}) => {
10071027
}
10081028
};
10091029

1010-
const getEncodingValueToIdx = (type, rangeMap) => {
1030+
const getEncodingValueToIdx = (type, rangeValues) => {
10111031
switch (type) {
10121032
default:
10131033
case 'categorical':
10141034
return identity;
10151035

10161036
case 'continuous':
1017-
return (value) => Math.round(value * (rangeMap.length - 1));
1037+
return (value) => Math.round(value * (rangeValues.length - 1));
10181038
}
10191039
};
10201040

@@ -1030,19 +1050,22 @@ const createScatterplot = (initialProperties = {}) => {
10301050
const setPointConnectionColorBy = (type) => {
10311051
pointConnectionColorBy = getEncodingType(
10321052
type,
1033-
DEFAULT_POINT_CONNECTION_COLOR_BY
1053+
DEFAULT_POINT_CONNECTION_COLOR_BY,
1054+
{ allowSegment: true }
10341055
);
10351056
};
10361057
const setPointConnectionOpacityBy = (type) => {
10371058
pointConnectionOpacityBy = getEncodingType(
10381059
type,
1039-
DEFAULT_POINT_CONNECTION_OPACITY_BY
1060+
DEFAULT_POINT_CONNECTION_OPACITY_BY,
1061+
{ allowSegment: true }
10401062
);
10411063
};
10421064
const setPointConnectionSizeBy = (type) => {
10431065
pointConnectionSizeBy = getEncodingType(
10441066
type,
1045-
DEFAULT_POINT_CONNECTION_SIZE_BY
1067+
DEFAULT_POINT_CONNECTION_SIZE_BY,
1068+
{ allowSegment: true }
10461069
);
10471070
};
10481071

@@ -1437,10 +1460,45 @@ const createScatterplot = (initialProperties = {}) => {
14371460
isInit = true;
14381461
};
14391462

1440-
const getPointConnectionColorIndices = () => {
1463+
const getPointConnectionColorIndices = (curvePoints) => {
14411464
const colorEncoding =
14421465
pointConnectionColorBy === 'inherit' ? colorBy : pointConnectionColorBy;
14431466

1467+
if (colorEncoding === 'segment') {
1468+
const maxColorIdx = pointConnectionColor.length - 1;
1469+
if (maxColorIdx < 1) return [];
1470+
return curvePoints.reduce((colorIndices, curve, index) => {
1471+
let totalLength = 0;
1472+
const segLengths = [];
1473+
// Compute the total length of the line
1474+
for (let i = 2; i < curve.length; i += 2) {
1475+
const segLength = Math.sqrt(
1476+
(curve[i - 2] - curve[i]) ** 2 + (curve[i - 1] - curve[i + 1]) ** 2
1477+
);
1478+
segLengths.push(segLength);
1479+
totalLength += segLength;
1480+
}
1481+
colorIndices[index] = [0];
1482+
let cumLength = 0;
1483+
// Assign the color index based on the cumulative length
1484+
for (let i = 0; i < curve.length / 2 - 1; i++) {
1485+
cumLength += segLengths[i];
1486+
// The `4` comes from the fact that we have 4 color states:
1487+
// normal, active, hover, and background
1488+
colorIndices[index].push(
1489+
Math.floor((cumLength / totalLength) * maxColorIdx) * 4
1490+
);
1491+
}
1492+
// The `4` comes from the fact that we have 4 color states:
1493+
// normal, active, hover, and background
1494+
// colorIndices[index] = rangeMap(
1495+
// curve.length,
1496+
// (i) => Math.floor((i / (curve.length - 1)) * maxColorIdx) * 4
1497+
// );
1498+
return colorIndices;
1499+
}, []);
1500+
}
1501+
14441502
if (colorEncoding) {
14451503
const encodingIdx = getEncodingIdx(colorEncoding);
14461504
const encodingValueToIdx = getEncodingValueToIdx(
@@ -1468,6 +1526,25 @@ const createScatterplot = (initialProperties = {}) => {
14681526
? opacityBy
14691527
: pointConnectionOpacityBy;
14701528

1529+
if (opacityEncoding === 'segment') {
1530+
const maxOpacityIdx = pointConnectionOpacity.length - 1;
1531+
if (maxOpacityIdx < 1) return [];
1532+
return pointConnectionMap.reduce(
1533+
// eslint-disable-next-line no-unused-vars
1534+
(opacities, [index, referencePoint, length]) => {
1535+
opacities[index] = rangeMap(
1536+
length,
1537+
(i) =>
1538+
pointConnectionOpacity[
1539+
Math.floor((i / (length - 1)) * maxOpacityIdx)
1540+
]
1541+
);
1542+
return opacities;
1543+
},
1544+
[]
1545+
);
1546+
}
1547+
14711548
if (opacityEncoding) {
14721549
const encodingIdx = getEncodingIdx(opacityEncoding);
14731550
const encodingRangeMap =
@@ -1492,6 +1569,23 @@ const createScatterplot = (initialProperties = {}) => {
14921569
const sizeEncoding =
14931570
pointConnectionSizeBy === 'inherit' ? sizeBy : pointConnectionSizeBy;
14941571

1572+
if (sizeEncoding === 'segment') {
1573+
const maxSizeIdx = pointConnectionSize.length - 1;
1574+
if (maxSizeIdx < 1) return [];
1575+
return pointConnectionMap.reduce(
1576+
// eslint-disable-next-line no-unused-vars
1577+
(widths, [index, referencePoint, length]) => {
1578+
widths[index] = rangeMap(
1579+
length,
1580+
(i) =>
1581+
pointConnectionSize[Math.floor((i / (length - 1)) * maxSizeIdx)]
1582+
);
1583+
return widths;
1584+
},
1585+
[]
1586+
);
1587+
}
1588+
14951589
if (sizeEncoding) {
14961590
const encodingIdx = getEncodingIdx(sizeEncoding);
14971591
const encodingRangeMap =
@@ -1519,6 +1613,8 @@ const createScatterplot = (initialProperties = {}) => {
15191613
index,
15201614
curvePoints[id].reference,
15211615
curvePoints[id].length / 2,
1616+
// Used for offsetting in the buffer manipulations on
1617+
// hovering and selecting
15221618
cumLinePoints,
15231619
];
15241620
cumLinePoints += curvePoints[id].length / 2;
@@ -1537,10 +1633,11 @@ const createScatterplot = (initialProperties = {}) => {
15371633
tolerance: pointConnectionTolerance,
15381634
}).then((curvePoints) => {
15391635
setPointConnectionMap(curvePoints);
1540-
pointConnections.setPoints(Object.values(curvePoints), {
1541-
colorIndices: getPointConnectionColorIndices(),
1542-
opacities: getPointConnectionOpacities(),
1543-
widths: getPointConnectionWidths(),
1636+
const curvePointValues = Object.values(curvePoints);
1637+
pointConnections.setPoints(curvePointValues, {
1638+
colorIndices: getPointConnectionColorIndices(curvePointValues),
1639+
opacities: getPointConnectionOpacities(curvePointValues),
1640+
widths: getPointConnectionWidths(curvePointValues),
15441641
});
15451642
computingPointConnectionCurves = false;
15461643
resolve();
@@ -1946,10 +2043,13 @@ const createScatterplot = (initialProperties = {}) => {
19462043
if (isConditionalArray(newOpacity, isPositiveNumber, { minLength: 1 }))
19472044
pointConnectionOpacity = [...newOpacity];
19482045

1949-
if (isPositiveNumber(+newOpacity)) pointConnectionOpacity = [+newOpacity];
2046+
if (isStrictlyPositiveNumber(+newOpacity))
2047+
pointConnectionOpacity = [+newOpacity];
19502048

19512049
pointConnectionColor = pointConnectionColor.map((color) => {
1952-
color[3] = pointConnectionOpacity[0];
2050+
color[3] = !Number.isNaN(+pointConnectionOpacity[0])
2051+
? +pointConnectionOpacity[0]
2052+
: color[3];
19532053
return color;
19542054
});
19552055

@@ -1969,7 +2069,7 @@ const createScatterplot = (initialProperties = {}) => {
19692069
)
19702070
pointConnectionSize = [...newPointConnectionSize];
19712071

1972-
if (isPositiveNumber(+newPointConnectionSize))
2072+
if (isStrictlyPositiveNumber(+newPointConnectionSize))
19732073
pointConnectionSize = [+newPointConnectionSize];
19742074

19752075
updatePointConnectionStyle();

src/spline-curve-worker.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ const worker = function worker() {
171171
// is larger than sqTolerance
172172
for (let i = 0; i < numPoints - 1; i++) {
173173
let segmentPoints = [points[i].slice(0, 2)];
174-
// outPoints.push(points[i].slice(0, 2));
175174
prevPoint = points[i];
176175

177176
for (let j = 1; j < maxIntPointsPerSegment; j++) {

src/utils.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,9 @@ export const hexToRgb = (hex, isNormalize = false) =>
105105
export const isConditionalArray = (a, condition, { minLength = 0 } = {}) =>
106106
Array.isArray(a) && a.length >= minLength && a.every(condition);
107107

108-
export const isPositiveNumber = (x) => !Number.isNaN(+x) && +x > 0;
108+
export const isPositiveNumber = (x) => !Number.isNaN(+x) && +x >= 0;
109+
110+
export const isStrictlyPositiveNumber = (x) => !Number.isNaN(+x) && +x > 0;
109111

110112
/**
111113
* Create a function to limit choices to a predefined list

0 commit comments

Comments
 (0)