examples/website/plot/plot-layer/axes-layer.js (300 lines of code) (raw):
import {Layer} from '@deck.gl/core';
import GL from '@luma.gl/constants';
import {Model, Geometry} from '@luma.gl/core';
import {textMatrixToTexture} from './utils';
import fragmentShader from './axes-fragment.glsl';
import gridVertex from './grid-vertex.glsl';
import labelVertex from './label-vertex.glsl';
import labelFragment from './label-fragment.glsl';
/* Constants */
const DEFAULT_FONT_SIZE = 48;
const DEFAULT_TICK_COUNT = 6;
const DEFAULT_TICK_FORMAT = x => x.toFixed(2);
const defaultProps = {
data: [],
fontSize: 12,
xScale: null,
yScale: null,
zScale: null,
xTicks: DEFAULT_TICK_COUNT,
yTicks: DEFAULT_TICK_COUNT,
zTicks: DEFAULT_TICK_COUNT,
xTickFormat: DEFAULT_TICK_FORMAT,
yTickFormat: DEFAULT_TICK_FORMAT,
zTickFormat: DEFAULT_TICK_FORMAT,
padding: 0,
color: [0, 0, 0, 255],
xTitle: 'x',
yTitle: 'y',
zTitle: 'z'
};
/* Utils */
function flatten(arrayOfArrays) {
const flatArray = arrayOfArrays.reduce((acc, arr) => acc.concat(arr), []);
if (Array.isArray(flatArray[0])) {
return flatten(flatArray);
}
return flatArray;
}
function getTicks(props) {
const {axis} = props;
let ticks = props[`${axis}Ticks`];
const scale = props[`${axis}Scale`];
const tickFormat = props[`${axis}TickFormat`];
if (!Array.isArray(ticks)) {
ticks = scale.ticks(ticks);
}
const titleTick = {
value: props[`${axis}Title`],
position: (scale.range()[0] + scale.range()[1]) / 2,
text: props[`${axis}Title`]
};
return [
...ticks.map(t => ({
value: t,
position: scale(t),
text: tickFormat(t, axis)
})),
titleTick
];
}
/*
* @classdesc
* A layer that plots a surface based on a z=f(x,y) equation.
*
* @class
* @param {Object} [props]
* @param {Integer} [props.ticksCount] - number of ticks along each axis, see
https://github.com/d3/d3-axis/blob/master/README.md#axis_ticks
* @param {Number} [props.padding] - amount to set back grids from the plot,
relative to the size of the bounding box
* @param {d3.scale} [props.xScale] - a d3 scale for the x axis
* @param {d3.scale} [props.yScale] - a d3 scale for the y axis
* @param {d3.scale} [props.zScale] - a d3 scale for the z axis
* @param {Number | [Number]} [props.xTicks] - either tick counts or an array of tick values
* @param {Number | [Number]} [props.yTicks] - either tick counts or an array of tick values
* @param {Number | [Number]} [props.zTicks] - either tick counts or an array of tick values
* @param {Function} [props.xTickFormat] - returns a string from value
* @param {Function} [props.yTickFormat] - returns a string from value
* @param {Function} [props.zTickFormat] - returns a string from value
* @param {String} [props.xTitle] - x axis title
* @param {String} [props.yTitle] - y axis title
* @param {String} [props.zTitle] - z axis title
* @param {Number} [props.fontSize] - size of the labels
* @param {Array} [props.color] - color of the gridlines, in [r,g,b,a]
*/
export default class AxesLayer extends Layer {
initializeState() {
const {gl} = this.context;
const attributeManager = this.getAttributeManager();
attributeManager.addInstanced({
instancePositions: {size: 2, update: this.calculateInstancePositions, noAlloc: true},
instanceNormals: {size: 3, update: this.calculateInstanceNormals, noAlloc: true},
instanceIsTitle: {size: 1, update: this.calculateInstanceIsTitle, noAlloc: true}
});
this.setState(
Object.assign(
{
numInstances: 0,
labels: null
},
this._getModels(gl)
)
);
}
updateState({oldProps, props, changeFlags}) {
const attributeManager = this.getAttributeManager();
if (
oldProps.xScale !== props.xScale ||
oldProps.yScale !== props.yScale ||
oldProps.zScale !== props.zScale ||
oldProps.xTicks !== props.xTicks ||
oldProps.yTicks !== props.yTicks ||
oldProps.zTicks !== props.zTicks ||
oldProps.xTickFormat !== props.xTickFormat ||
oldProps.yTickFormat !== props.yTickFormat ||
oldProps.zTickFormat !== props.zTickFormat
) {
const {xScale, yScale, zScale} = props;
const ticks = [
getTicks({...props, axis: 'x'}),
getTicks({...props, axis: 'z'}),
getTicks({...props, axis: 'y'})
];
const xRange = xScale.range();
const yRange = yScale.range();
const zRange = zScale.range();
this.setState({
ticks,
labelTexture: this.renderLabelTexture(ticks),
gridDims: [xRange[1] - xRange[0], zRange[1] - zRange[0], yRange[1] - yRange[0]],
gridCenter: [
(xRange[0] + xRange[1]) / 2,
(zRange[0] + zRange[1]) / 2,
(yRange[0] + yRange[1]) / 2
]
});
attributeManager.invalidateAll();
}
}
draw({uniforms}) {
const {gridDims, gridCenter, modelsByName, labelTexture, numInstances} = this.state;
const {fontSize, color, padding} = this.props;
if (labelTexture) {
const baseUniforms = {
fontSize,
gridDims,
gridCenter,
gridOffset: padding,
strokeColor: color
};
modelsByName.grids.setInstanceCount(numInstances);
modelsByName.labels.setInstanceCount(numInstances);
modelsByName.grids.setUniforms(Object.assign({}, uniforms, baseUniforms)).draw();
modelsByName.labels
.setUniforms(Object.assign({}, uniforms, baseUniforms, labelTexture))
.draw();
}
}
_getModels(gl) {
/* grids:
* for each x tick, draw rectangle on yz plane around the bounding box.
* for each y tick, draw rectangle on zx plane around the bounding box.
* for each z tick, draw rectangle on xy plane around the bounding box.
* show/hide is toggled by the vertex shader
*/
/*
* rectangles are defined in 2d and rotated in the vertex shader
*
* (-1,1) (1,1)
* +-----------+
* | |
* | |
* | |
* | |
* +-----------+
* (-1,-1) (1,-1)
*/
// offset of each corner
const gridPositions = [
// left edge
-1,
-1,
0,
-1,
1,
0,
// top edge
-1,
1,
0,
1,
1,
0,
// right edge
1,
1,
0,
1,
-1,
0,
// bottom edge
1,
-1,
0,
-1,
-1,
0
];
// normal of each edge
const gridNormals = [
// left edge
-1,
0,
0,
-1,
0,
0,
// top edge
0,
1,
0,
0,
1,
0,
// right edge
1,
0,
0,
1,
0,
0,
// bottom edge
0,
-1,
0,
0,
-1,
0
];
const grids = new Model(gl, {
id: `${this.props.id}-grids`,
vs: gridVertex,
fs: fragmentShader,
geometry: new Geometry({
drawMode: GL.LINES,
attributes: {
positions: new Float32Array(gridPositions),
normals: new Float32Array(gridNormals)
},
vertexCount: gridPositions.length / 3
}),
isInstanced: true
});
/* labels
* one label is placed at each end of every grid line
* show/hide is toggled by the vertex shader
*/
let labelTexCoords = [];
let labelPositions = [];
let labelNormals = [];
let labelIndices = [];
for (let i = 0; i < 8; i++) {
/*
* each label is rendered as a rectangle
* 0 2
* +--.+
* | / |
* +'--+
* 1 3
*/
labelTexCoords = labelTexCoords.concat([0, 0, 0, 1, 1, 0, 1, 1]);
labelIndices = labelIndices.concat([
i * 4 + 0,
i * 4 + 1,
i * 4 + 2,
i * 4 + 2,
i * 4 + 1,
i * 4 + 3
]);
// all four vertices of this label's rectangle is anchored at the same grid endpoint
for (let j = 0; j < 4; j++) {
labelPositions = labelPositions.concat(gridPositions.slice(i * 3, i * 3 + 3));
labelNormals = labelNormals.concat(gridNormals.slice(i * 3, i * 3 + 3));
}
}
const labels = new Model(gl, {
id: `${this.props.id}-labels`,
vs: labelVertex,
fs: labelFragment,
geometry: new Geometry({
drawMode: GL.TRIANGLES,
attributes: {
indices: new Uint16Array(labelIndices),
positions: new Float32Array(labelPositions),
texCoords: {size: 2, value: new Float32Array(labelTexCoords)},
normals: new Float32Array(labelNormals)
}
}),
isInstanced: true
});
return {
models: [grids, labels].filter(Boolean),
modelsByName: {grids, labels}
};
}
calculateInstancePositions(attribute) {
const {ticks} = this.state;
const positions = ticks.map(axisTicks => axisTicks.map((t, i) => [t.position, i]));
const value = new Float32Array(flatten(positions));
attribute.value = value;
this.setState({numInstances: value.length / attribute.size});
}
calculateInstanceNormals(attribute) {
const {
ticks: [xTicks, zTicks, yTicks]
} = this.state;
const normals = [
xTicks.map(t => [1, 0, 0]),
zTicks.map(t => [0, 1, 0]),
yTicks.map(t => [0, 0, 1])
];
attribute.value = new Float32Array(flatten(normals));
}
calculateInstanceIsTitle(attribute) {
const {ticks} = this.state;
const isTitle = ticks.map(axisTicks => {
const ticksCount = axisTicks.length - 1;
return axisTicks.map((t, i) => (i < ticksCount ? 0 : 1));
});
attribute.value = new Float32Array(flatten(isTitle));
}
renderLabelTexture(ticks) {
if (this.state.labels) {
this.state.labels.labelTexture.delete();
}
// attach a 2d texture of all the label texts
const textureInfo = textMatrixToTexture(this.context.gl, ticks, DEFAULT_FONT_SIZE);
if (textureInfo) {
// success
const {columnWidths, texture} = textureInfo;
return {
labelHeight: DEFAULT_FONT_SIZE,
labelWidths: columnWidths,
labelTextureDim: [texture.width, texture.height],
labelTexture: texture
};
}
return null;
}
}
AxesLayer.layerName = 'AxesLayer';
AxesLayer.defaultProps = defaultProps;