Skip to content

Commit 1f3192f

Browse files
committed
add color mapping utility functions to @rapidsai/deck.gl
1 parent 7b54e83 commit 1f3192f

3 files changed

Lines changed: 199 additions & 1 deletion

File tree

modules/deck.gl/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
export * from './buffer';
1616
export * from './layers/graph';
1717
export * from './layers/point-cloud';
18+
export * from './utils/series-color-utils';
1819

1920
export {Deck as DeckSSR} from './ssr/deck';
2021
export {AnimationLoop as AnimationLoopSSR} from './ssr/animation-loop';
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) 2022, NVIDIA CORPORATION.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {IndexType, Series, Uint32, Uint32Series} from '@rapidsai/cudf';
16+
17+
function getSize(col: Series<IndexType>|number) {
18+
return col instanceof Series ? col.length : undefined;
19+
}
20+
21+
function validateSeries(col: Series<IndexType>) {
22+
if (col.min() < 0 || col.max() > 255) {
23+
throw new RangeError('invalid column, contains values outside of the range [0,255]');
24+
}
25+
return col;
26+
}
27+
28+
function hexToInteger(value: string) {
29+
value = value.replace('#', '0x');
30+
31+
return parseInt(`${value}ff`, 16);
32+
}
33+
/**
34+
* Convert 4 individual RGBA Series or values to a RGBA Integer Series to be consumed directly by
35+
* color buffers
36+
* @param r Series<IndexType> or number for the color red (valid values [0,255])
37+
* @param g Series<IndexType> or number for the color red (valid values [0,255])
38+
* @param b Series<IndexType> or number for the color red (valid values [0,255])
39+
* @param a Series<IndexType> or number for the color red (valid values [0,255])
40+
* @param size optional size of the series, if all of the r,g,b,a arguments are numbers. default is
41+
* 1
42+
* @returns Uint32Series containing RGBA integer values
43+
*/
44+
export function RGBASeriestoIntSeries(r: Series<IndexType>|number,
45+
g: Series<IndexType>|number,
46+
b: Series<IndexType>|number,
47+
a: Series<IndexType>|number,
48+
size = 1): Uint32Series {
49+
size = getSize(r) || getSize(g) || getSize(b) || getSize(a) || size;
50+
r = r instanceof Series ? validateSeries(r)
51+
: Series.sequence({type: new Uint32, size: size, init: r, step: 0});
52+
g = g instanceof Series ? validateSeries(g)
53+
: Series.sequence({type: new Uint32, size: size, init: g, step: 0});
54+
b = b instanceof Series ? validateSeries(b)
55+
: Series.sequence({type: new Uint32, size: size, init: b, step: 0});
56+
a = a instanceof Series ? validateSeries(a)
57+
: Series.sequence({type: new Uint32, size: size, init: a, step: 0});
58+
59+
return r.shiftLeft(BigInt(24))
60+
.bitwiseOr(g.shiftLeft(BigInt(16)))
61+
.bitwiseOr(b.shiftLeft(BigInt(8)))
62+
.bitwiseOr(a)
63+
.cast(new Uint32);
64+
}
65+
/**
66+
* Convert rgba to 32 bit signed integer
67+
* @param rgba array of rgba value (a value is optional, default is 255)
68+
* @returns rgba 32 bit signed integer value
69+
*/
70+
export function RGBAtoInt(rgba: number[]): number {
71+
rgba.forEach((value) => {
72+
if (value < 0 || value > 255) { throw new RangeError('rgba values expected within [0,255]'); }
73+
});
74+
if (rgba.length < 3 || rgba.length > 4) {
75+
throw new Error('invalid array length, provide values for r,g,b,a(optional)');
76+
}
77+
if (rgba.length == 3) { rgba = rgba.concat(255); } // if only rgb values are provided
78+
return (rgba[0] << 24 >>> 0) + (rgba[1] << 16 >>> 0) + (rgba[2] << 8 >>> 0) + rgba[3];
79+
}
80+
81+
/**
82+
* Helper function for quickly creating a cudf color Series based on color bins provided
83+
* @param values
84+
* @param domain
85+
* @param colors
86+
* possible input values:
87+
* - array of rgba arrays
88+
* - array of rgb arrays (a defaults to 255)
89+
* - array of 32bit rgba integers
90+
* - array of hex color strings
91+
* @param nullColor
92+
*/
93+
export function mapValuesToColorSeries(
94+
values: Series<IndexType>,
95+
domain: number[],
96+
colors: number[][]|number[]|string[],
97+
nullColor: number[]|number = [204, 204, 204, 255]): Uint32Series {
98+
// validate colors and domain lengths
99+
if (colors.length < 1 || domain.length < 1) {
100+
throw new Error('colors and domain must be arrays of length 1 or greater');
101+
}
102+
103+
// convert RGBA values to Integers accepted by deck gpu buffers
104+
const nullColorInteger = Array.isArray(nullColor) ? RGBAtoInt(nullColor) : nullColor;
105+
const colorsInteger: number[] =
106+
colors.map(value => Array.isArray(value) ? RGBAtoInt(value)
107+
: typeof value == 'string' ? hexToInteger(value)
108+
: value);
109+
let colorSeries =
110+
Series.sequence({type: new Uint32, init: colorsInteger[0], step: 0, size: values.length});
111+
const colorIndices = Series.sequence({type: values.type, init: 0, step: 1, size: values.length});
112+
113+
if (domain.length == 1) {
114+
const boolMask = values.ge(domain[0]);
115+
const indices = colorIndices.filter(boolMask);
116+
colorSeries =
117+
colorSeries.scatter(colorsInteger[1] || colorsInteger[colors.length - 1], indices);
118+
} else {
119+
for (let i = 0; i < domain.length - 1; i++) {
120+
const boolMask = values.ge(domain[i]).logicalAnd(values.lt(domain[i + 1]));
121+
const indices = colorIndices.filter(boolMask);
122+
colorSeries =
123+
colorSeries.scatter(colorsInteger[i] || colorsInteger[colors.length - 1], indices);
124+
}
125+
}
126+
// handle nulls
127+
if (values.countNonNulls() !== values.length) { // contains null values
128+
const indices = colorIndices.filter(values.isNull());
129+
colorSeries = colorSeries.scatter(nullColorInteger, indices);
130+
}
131+
132+
return colorSeries;
133+
}
Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,65 @@
1-
test('nothing', () => {});
1+
// Copyright (c) 2022, NVIDIA CORPORATION.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {Series, Uint32, Uint8} from '@rapidsai/cudf';
16+
import {mapValuesToColorSeries, RGBASeriestoIntSeries, RGBAtoInt} from '@rapidsai/deck.gl';
17+
18+
describe('DeckGL-color-utils', () => {
19+
test('test RGBAtoInt', () => {
20+
expect(RGBAtoInt([255, 255, 255, 255])).toEqual(4294967295);
21+
expect(RGBAtoInt([167, 0, 125, 0])).toEqual(2801827072);
22+
expect(RGBAtoInt([123, 123, 123, 125])).toEqual(2071690109);
23+
expect(() => {RGBAtoInt([123, 123, 256, 125])}).toThrow(RangeError);
24+
});
25+
26+
test('test RGBASeriesToIntSeries', () => {
27+
const r = Series.new({type: new Uint32, data: [255, 167, 123]});
28+
const g = Series.new({type: new Uint32, data: [255, 0, 123]});
29+
const b = Series.new({type: new Uint32, data: [255, 125, 123]});
30+
const a = Series.new({type: new Uint32, data: [255, 0, 125]});
31+
const badR = Series.new({type: new Uint32, data: [-1, 167, 256]});
32+
33+
const expectedA = Series.new({type: new Uint32, data: [4294967295, 2801827072, 2071690109]});
34+
const expectedB = Series.new({type: new Uint32, data: [4294967295, 2801827327, 2071690239]});
35+
36+
expect([...RGBASeriestoIntSeries(r, g, b, a)]).toEqual([...expectedA]);
37+
expect([...RGBASeriestoIntSeries(r, g, b, 255)]).toEqual([...expectedB]);
38+
expect(() => {[...RGBASeriestoIntSeries(badR, g, b, 255)]}).toThrow(RangeError);
39+
});
40+
41+
test('test mapValuesToColorSeries', () => {
42+
const values = Series.new({type: new Uint8, data: [1, 5, 19, 24, 23, 32, 50, null]});
43+
const domain = [1, 10, 30, 40];
44+
// Integer colors: 4278190335, 4227793151, 1835007, 16712447, 4278243071
45+
const colors = [[255, 0, 0], [251, 255, 0], [0, 27, 255], [0, 255, 2], [255, 0, 206]];
46+
const colorsHex = ['#ff0000', '#fbff00', '#001bff', '00ff02', 'ff00ce'];
47+
48+
const resultColors = Series.new({
49+
type: new Uint32,
50+
data: [
51+
4278190335,
52+
4278190335,
53+
4227793151,
54+
4227793151,
55+
4227793151,
56+
1835007,
57+
4278190335,
58+
3435973887 // default null Color
59+
]
60+
})
61+
62+
expect([...mapValuesToColorSeries(values, domain, colors)]).toEqual([...resultColors]);
63+
expect([...mapValuesToColorSeries(values, domain, colorsHex)]).toEqual([...resultColors]);
64+
})
65+
});

0 commit comments

Comments
 (0)