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

CARTO: HeatmapTileLayer full colorRange #9068

Merged
merged 11 commits into from
Aug 6, 2024
Merged
68 changes: 65 additions & 3 deletions modules/carto/src/layers/heatmap-tile-layer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import type {ShaderModule} from '@luma.gl/shadertools';
import {getResolution} from 'quadbin';

import {Accessor, CompositeLayer, CompositeLayerProps, DefaultProps, Layer} from '@deck.gl/core';
import {
Accessor,
Color,
CompositeLayer,
CompositeLayerProps,
DefaultProps,
Layer,
UpdateParameters
} from '@deck.gl/core';
import {SolidPolygonLayer} from '@deck.gl/layers';

import {HeatmapProps, heatmap} from './heatmap';
Expand All @@ -10,7 +18,22 @@ import QuadbinTileLayer, {QuadbinTileLayerProps} from './quadbin-tile-layer';
import {TilejsonPropType} from './utils';
import {TilejsonResult} from '../sources';
import {_Tile2DHeader as Tile2DHeader} from '@deck.gl/geo-layers';

import {Texture, TextureProps} from '@luma.gl/core';
import {
defaultColorRange,
colorRangeToFlatArray
} from '../../../aggregation-layers/src/utils/color-utils';
felixpalmer marked this conversation as resolved.
Show resolved Hide resolved

const TEXTURE_PROPS: TextureProps = {
format: 'rgba8unorm',
mipmaps: false,
sampler: {
minFilter: 'linear',
magFilter: 'linear',
addressModeU: 'clamp-to-edge',
addressModeV: 'clamp-to-edge'
}
};
/**
* Computes the unit density (inverse of cell area)
*/
Expand Down Expand Up @@ -86,6 +109,7 @@ const defaultProps: DefaultProps<HeatmapTileLayerProps> = {
getWeight: {type: 'accessor', value: 1},
onMaxDensityChange: {type: 'function', optional: true, value: null},
colorDomain: {type: 'array', value: [0, 1]},
colorRange: defaultColorRange,
intensity: {type: 'number', value: 1},
radiusPixels: {type: 'number', min: 0, max: 100, value: 20}
};
Expand All @@ -99,6 +123,13 @@ export type HeatmapTileLayerProps<DataT = unknown> = _HeatmapTileLayerProps<Data
/** Properties added by HeatmapTileLayer. */
type _HeatmapTileLayerProps<DataT> = QuadbinTileLayerProps<DataT> &
HeatmapProps & {
/**
* Specified as an array of colors [color1, color2, ...].
*
* @default `6-class YlOrRd` - [colorbrewer](http://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=6)
*/
colorRange: Color[];

/**
* The weight of each object.
*
Expand All @@ -117,6 +148,7 @@ class HeatmapTileLayer<DataT = any, ExtraProps extends {} = {}> extends Composit
static defaultProps = defaultProps;

state!: {
colorTexture?: Texture;
isLoaded: boolean;
tiles: Set<Tile2DHeader>;
viewportChanged?: boolean;
Expand All @@ -135,6 +167,14 @@ class HeatmapTileLayer<DataT = any, ExtraProps extends {} = {}> extends Composit
return changeFlags.somethingChanged;
}

updateState(opts: UpdateParameters<this>) {
const {props, oldProps} = opts;
super.updateState(opts);
if (props.colorRange !== oldProps.colorRange) {
this._updateColorTexture(opts);
}
}

renderLayers(): Layer {
const {
data,
Expand Down Expand Up @@ -199,12 +239,14 @@ class HeatmapTileLayer<DataT = any, ExtraProps extends {} = {}> extends Composit

colorDomain,

colorRange,
// colorRange,
felixpalmer marked this conversation as resolved.
Show resolved Hide resolved
radiusPixels,
intensity,
_subLayerProps: subLayerProps,
refinementStrategy: 'no-overlap',

colorTexture: this.state.colorTexture,

// Disable line rendering
extruded: false,
stroked: false,
Expand Down Expand Up @@ -247,6 +289,26 @@ class HeatmapTileLayer<DataT = any, ExtraProps extends {} = {}> extends Composit
})
);
}

_updateColorTexture(opts) {
const {colorRange} = opts.props;
let {colorTexture} = this.state;
const colors = colorRangeToFlatArray(colorRange, false, Uint8Array as any);

if (colorTexture && colorTexture?.width === colorRange.length) {
// TODO(v9): Unclear whether `setSubImageData` is a public API, or what to use if not.
(colorTexture as any).setSubImageData({data: colors});
} else {
colorTexture?.destroy();
colorTexture = this.context.device.createTexture({
...TEXTURE_PROPS,
data: colors,
width: colorRange.length,
height: 1
});
}
this.setState({colorTexture});
}
}

export default HeatmapTileLayer;
128 changes: 33 additions & 95 deletions modules/carto/src/layers/heatmap.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,29 @@
import type {ShaderPass} from '@luma.gl/shadertools';
import {Texture} from '@luma.gl/core';
const glsl = (s: TemplateStringsArray) => `${s}`;

/**
* @filter Heatmap
* @param radiusPixels Blur radius in pixels, controls smoothness of heatmap
* @param colorDomain Domain to apply to values prior to applying color scale
* @param color1-6 Colors to use in color scale
* @param colorTexture 1D RGB lookup texture for color scale
* @param intensity Multiplier to apply to value
* @param opacity Output opacity
*/

const fs = glsl`\
uniform heatmapUniforms {
vec2 delta;
float radiusPixels;
vec2 colorDomain;
vec3 color1;
vec3 color2;
vec3 color3;
vec3 color4;
vec3 color5;
vec3 color6;
vec2 delta;
float intensity;
float opacity;
float radiusPixels;
} heatmap;

const vec4 STOPS = vec4(0.2, 0.4, 0.6, 0.8);
uniform sampler2D colorTexture;

vec3 colorGradient(float value) {
vec3 c1;
vec3 c2;
vec2 range;
if (value < STOPS.x) {
range = vec2(0.0, STOPS.x);
c1 = heatmap.color1; c2 = heatmap.color2;
} else if (value < STOPS.y) {
range = STOPS.xy;
c1 = heatmap.color2; c2 = heatmap.color3;
} else if (value < STOPS.z) {
range = STOPS.yz;
c1 = heatmap.color3; c2 = heatmap.color4;
} else if (value < STOPS.w) {
range = STOPS.zw;
c1 = heatmap.color4; c2 = heatmap.color5;
} else {
range = vec2(STOPS.w, 1.0);
c1 = heatmap.color5; c2 = heatmap.color6;
}

float f = (clamp(value, 0.0, 1.0) - range.x) / (range.y - range.x);
return mix(c1, c2, f) / 255.0;
return texture(colorTexture, vec2(value, 0.5)).rgb;
}

const vec3 SHIFT = vec3(1.0, 256.0, 256.0 * 256.0);
Expand Down Expand Up @@ -114,15 +90,6 @@ vec4 heatmap_sampleColor(sampler2D source, vec2 texSize, vec2 texCoord) {
}
`;

const defaultColorRange: [number, number, number][] = [
[255, 255, 178],
[254, 217, 118],
[254, 178, 76],
[253, 141, 60],
[240, 59, 32],
[189, 0, 38]
];

export type HeatmapProps = {
/**
* Radius of the heatmap blur in pixels, to which the weight of a cell is distributed.
Expand All @@ -131,93 +98,64 @@ export type HeatmapProps = {
*/
radiusPixels?: number;
/**
* Controls how weight values are mapped to the `colorRange`, as an array of two numbers [`minValue`, `maxValue`].
* Controls how weight values are mapped to the colors in `colorTexture`, as an array of two numbers [`minValue`, `maxValue`].
*
* @default [0, 1]
*/
colorDomain?: [number, number];
/**
* Specified as an array of colors [color1, color2, ...].
*
* @default `6-class YlOrRd` - [colorbrewer](http://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=6)
*/
colorRange: [number, number, number][];
/**
* Value that is multiplied with the total weight at a pixel to obtain the final weight. A value larger than 1 biases the output color towards the higher end of the spectrum, and a value less than 1 biases the output color towards the lower end of the spectrum.
*/
intensity?: number;
/**
* Color LUT for color gradient
*/
colorTexture: Texture;
opacity: number;
};

export type HeatmapUniforms = {
delta?: [number, number];
radiusPixels?: number;
colorDomain?: [number, number];
color1?: [number, number, number];
color2?: [number, number, number];
color3?: [number, number, number];
color4?: [number, number, number];
color5?: [number, number, number];
color6?: [number, number, number];
intensity: number;
opacity?: number;
type PassProps = {
delta: [number, number];
};

export const heatmap: ShaderPass<HeatmapProps, HeatmapUniforms> = {
export const heatmap = {
name: 'heatmap',
uniformPropTypes: {
delta: {value: [0, 1]},
radiusPixels: {value: 20, min: 0, softMax: 100},
colorDomain: {value: [0, 1]},
color1: {value: [0, 0, 0]},
color2: {value: [0, 0, 0]},
color3: {value: [0, 0, 0]},
color4: {value: [0, 0, 0]},
color5: {value: [0, 0, 0]},
color6: {value: [0, 0, 0]},
delta: {value: [0, 1]},
intensity: {value: 1, min: 0.1, max: 10},
opacity: {value: 1, min: 0, max: 1}
opacity: {value: 1, min: 0, max: 1},
radiusPixels: {value: 20, min: 0, softMax: 100}
},
uniformTypes: {
delta: 'vec2<f32>',
radiusPixels: 'f32',
colorDomain: 'vec2<f32>',
color1: 'vec3<f32>',
color2: 'vec3<f32>',
color3: 'vec3<f32>',
color4: 'vec3<f32>',
color5: 'vec3<f32>',
color6: 'vec3<f32>',
delta: 'vec2<f32>',
intensity: 'f32',
opacity: 'f32'
opacity: 'f32',
radiusPixels: 'f32'
},
getUniforms: opts => {
if (!opts) return {};
const {
delta = [1, 0],
colorRange = defaultColorRange,
radiusPixels = 20,
colorDomain = [0, 1],
colorTexture,
delta = [1, 0],
intensity = 1,
opacity = 1
} = opts as HeatmapProps & {delta: [number, number]};
const [color1, color2, color3, color4, color5, color6] = colorRange;
opacity = 1,
radiusPixels = 20
} = opts;
return {
delta,
color1,
color2,
color3,
color4,
color5,
color6,
radiusPixels,
colorDomain,
colorTexture,
delta,
intensity,
opacity
opacity,
radiusPixels
};
},
fs,
passes: [
{sampler: true, uniforms: {delta: [1, 0]}},
{sampler: true, uniforms: {delta: [0, 1]}}
]
};
} as const satisfies ShaderPass<HeatmapProps & PassProps>;
Loading