plotly.js
Version:
The open source javascript graphing library that powers plotly
677 lines (599 loc) • 21.4 kB
JavaScript
'use strict';
var vertexShaderSource = [
'precision highp float;',
'',
'varying vec4 fragColor;',
'',
'attribute vec4 p01_04, p05_08, p09_12, p13_16,',
' p17_20, p21_24, p25_28, p29_32,',
' p33_36, p37_40, p41_44, p45_48,',
' p49_52, p53_56, p57_60, colors;',
'',
'uniform mat4 dim0A, dim1A, dim0B, dim1B, dim0C, dim1C, dim0D, dim1D,',
' loA, hiA, loB, hiB, loC, hiC, loD, hiD;',
'',
'uniform vec2 resolution, viewBoxPos, viewBoxSize;',
'uniform float maskHeight;',
'uniform float drwLayer; // 0: context, 1: focus, 2: pick',
'uniform vec4 contextColor;',
'uniform sampler2D maskTexture, palette;',
'',
'bool isPick = (drwLayer > 1.5);',
'bool isContext = (drwLayer < 0.5);',
'',
'const vec4 ZEROS = vec4(0.0, 0.0, 0.0, 0.0);',
'const vec4 UNITS = vec4(1.0, 1.0, 1.0, 1.0);',
'',
'float val(mat4 p, mat4 v) {',
' return dot(matrixCompMult(p, v) * UNITS, UNITS);',
'}',
'',
'float axisY(float ratio, mat4 A, mat4 B, mat4 C, mat4 D) {',
' float y1 = val(A, dim0A) + val(B, dim0B) + val(C, dim0C) + val(D, dim0D);',
' float y2 = val(A, dim1A) + val(B, dim1B) + val(C, dim1C) + val(D, dim1D);',
' return y1 * (1.0 - ratio) + y2 * ratio;',
'}',
'',
'int iMod(int a, int b) {',
' return a - b * (a / b);',
'}',
'',
'bool fOutside(float p, float lo, float hi) {',
' return (lo < hi) && (lo > p || p > hi);',
'}',
'',
'bool vOutside(vec4 p, vec4 lo, vec4 hi) {',
' return (',
' fOutside(p[0], lo[0], hi[0]) ||',
' fOutside(p[1], lo[1], hi[1]) ||',
' fOutside(p[2], lo[2], hi[2]) ||',
' fOutside(p[3], lo[3], hi[3])',
' );',
'}',
'',
'bool mOutside(mat4 p, mat4 lo, mat4 hi) {',
' return (',
' vOutside(p[0], lo[0], hi[0]) ||',
' vOutside(p[1], lo[1], hi[1]) ||',
' vOutside(p[2], lo[2], hi[2]) ||',
' vOutside(p[3], lo[3], hi[3])',
' );',
'}',
'',
'bool outsideBoundingBox(mat4 A, mat4 B, mat4 C, mat4 D) {',
' return mOutside(A, loA, hiA) ||',
' mOutside(B, loB, hiB) ||',
' mOutside(C, loC, hiC) ||',
' mOutside(D, loD, hiD);',
'}',
'',
'bool outsideRasterMask(mat4 A, mat4 B, mat4 C, mat4 D) {',
' mat4 pnts[4];',
' pnts[0] = A;',
' pnts[1] = B;',
' pnts[2] = C;',
' pnts[3] = D;',
'',
' for(int i = 0; i < 4; ++i) {',
' for(int j = 0; j < 4; ++j) {',
' for(int k = 0; k < 4; ++k) {',
' if(0 == iMod(',
' int(255.0 * texture2D(maskTexture,',
' vec2(',
' (float(i * 2 + j / 2) + 0.5) / 8.0,',
' (pnts[i][j][k] * (maskHeight - 1.0) + 1.0) / maskHeight',
' ))[3]',
' ) / int(pow(2.0, float(iMod(j * 4 + k, 8)))),',
' 2',
' )) return true;',
' }',
' }',
' }',
' return false;',
'}',
'',
'vec4 position(bool isContext, float v, mat4 A, mat4 B, mat4 C, mat4 D) {',
' float x = 0.5 * sign(v) + 0.5;',
' float y = axisY(x, A, B, C, D);',
' float z = 1.0 - abs(v);',
'',
' z += isContext ? 0.0 : 2.0 * float(',
' outsideBoundingBox(A, B, C, D) ||',
' outsideRasterMask(A, B, C, D)',
' );',
'',
' return vec4(',
' 2.0 * (vec2(x, y) * viewBoxSize + viewBoxPos) / resolution - 1.0,',
' z,',
' 1.0',
' );',
'}',
'',
'void main() {',
' mat4 A = mat4(p01_04, p05_08, p09_12, p13_16);',
' mat4 B = mat4(p17_20, p21_24, p25_28, p29_32);',
' mat4 C = mat4(p33_36, p37_40, p41_44, p45_48);',
' mat4 D = mat4(p49_52, p53_56, p57_60, ZEROS);',
'',
' float v = colors[3];',
'',
' gl_Position = position(isContext, v, A, B, C, D);',
'',
' fragColor =',
' isContext ? vec4(contextColor) :',
' isPick ? vec4(colors.rgb, 1.0) : texture2D(palette, vec2(abs(v), 0.5));',
'}'
].join('\n');
var fragmentShaderSource = [
'precision highp float;',
'',
'varying vec4 fragColor;',
'',
'void main() {',
' gl_FragColor = fragColor;',
'}'
].join('\n');
var maxDim = require('./constants').maxDimensionCount;
var Lib = require('../../lib');
// don't change; otherwise near/far plane lines are lost
var depthLimitEpsilon = 1e-6;
// precision of multiselect is the full range divided into this many parts
var maskHeight = 2048;
var dummyPixel = new Uint8Array(4);
var dataPixel = new Uint8Array(4);
var paletteTextureConfig = {
shape: [256, 1],
format: 'rgba',
type: 'uint8',
mag: 'nearest',
min: 'nearest'
};
function ensureDraw(regl) {
regl.read({
x: 0,
y: 0,
width: 1,
height: 1,
data: dummyPixel
});
}
function clear(regl, x, y, width, height) {
var gl = regl._gl;
gl.enable(gl.SCISSOR_TEST);
gl.scissor(x, y, width, height);
regl.clear({color: [0, 0, 0, 0], depth: 1}); // clearing is done in scissored panel only
}
function renderBlock(regl, glAes, renderState, blockLineCount, sampleCount, item) {
var rafKey = item.key;
function render(blockNumber) {
var count = Math.min(blockLineCount, sampleCount - blockNumber * blockLineCount);
if(blockNumber === 0) {
// stop drawing possibly stale glyphs before clearing
window.cancelAnimationFrame(renderState.currentRafs[rafKey]);
delete renderState.currentRafs[rafKey];
clear(regl, item.scissorX, item.scissorY, item.scissorWidth, item.viewBoxSize[1]);
}
if(renderState.clearOnly) {
return;
}
item.count = 2 * count;
item.offset = 2 * blockNumber * blockLineCount;
glAes(item);
if(blockNumber * blockLineCount + count < sampleCount) {
renderState.currentRafs[rafKey] = window.requestAnimationFrame(function() {
render(blockNumber + 1);
});
}
renderState.drawCompleted = false;
}
if(!renderState.drawCompleted) {
ensureDraw(regl);
renderState.drawCompleted = true;
}
// start with rendering item 0; recursion handles the rest
render(0);
}
function adjustDepth(d) {
// WebGL matrix operations use floats with limited precision, potentially causing a number near a border of [0, 1]
// to end up slightly outside the border. With an epsilon, we reduce the chance that a line gets clipped by the
// near or the far plane.
return Math.max(depthLimitEpsilon, Math.min(1 - depthLimitEpsilon, d));
}
function palette(unitToColor, opacity) {
var result = new Array(256);
for(var i = 0; i < 256; i++) {
result[i] = unitToColor(i / 255).concat(opacity);
}
return result;
}
// Maps the sample index [0...sampleCount - 1] to a range of [0, 1] as the shader expects colors in the [0, 1] range.
// but first it shifts the sample index by 0, 8 or 16 bits depending on rgbIndex [0..2]
// with the end result that each line will be of a unique color, making it possible for the pick handler
// to uniquely identify which line is hovered over (bijective mapping).
// The inverse, i.e. readPixel is invoked from 'parcoords.js'
function calcPickColor(i, rgbIndex) {
return (i >>> 8 * rgbIndex) % 256 / 255;
}
function makePoints(sampleCount, dims, color) {
var points = new Array(sampleCount * (maxDim + 4));
var n = 0;
for(var i = 0; i < sampleCount; i++) {
for(var k = 0; k < maxDim; k++) {
points[n++] = (k < dims.length) ? dims[k].paddedUnitValues[i] : 0.5;
}
points[n++] = calcPickColor(i, 2);
points[n++] = calcPickColor(i, 1);
points[n++] = calcPickColor(i, 0);
points[n++] = adjustDepth(color[i]);
}
return points;
}
function makeVecAttr(vecIndex, sampleCount, points) {
var pointPairs = new Array(sampleCount * 8);
var n = 0;
for(var i = 0; i < sampleCount; i++) {
for(var j = 0; j < 2; j++) {
for(var k = 0; k < 4; k++) {
var q = vecIndex * 4 + k;
var v = points[i * 64 + q];
if(q === 63 && j === 0) {
v *= -1;
}
pointPairs[n++] = v;
}
}
}
return pointPairs;
}
function pad2(num) {
var s = '0' + num;
return s.substr(s.length - 2);
}
function getAttrName(i) {
return (i < maxDim) ? 'p' + pad2(i + 1) + '_' + pad2(i + 4) : 'colors';
}
function setAttributes(attributes, sampleCount, points) {
for(var i = 0; i <= maxDim; i += 4) {
attributes[getAttrName(i)](makeVecAttr(i / 4, sampleCount, points));
}
}
function emptyAttributes(regl) {
var attributes = {};
for(var i = 0; i <= maxDim; i += 4) {
attributes[getAttrName(i)] = regl.buffer({usage: 'dynamic', type: 'float', data: new Uint8Array(0)});
}
return attributes;
}
function makeItem(
model, leftmost, rightmost, itemNumber, i0, i1, x, y, panelSizeX, panelSizeY,
crossfilterDimensionIndex, drwLayer, constraints, plotGlPixelRatio
) {
var dims = [[], []];
for(var k = 0; k < 64; k++) {
dims[0][k] = (k === i0) ? 1 : 0;
dims[1][k] = (k === i1) ? 1 : 0;
}
x *= plotGlPixelRatio;
y *= plotGlPixelRatio;
panelSizeX *= plotGlPixelRatio;
panelSizeY *= plotGlPixelRatio;
var overdrag = model.lines.canvasOverdrag * plotGlPixelRatio;
var domain = model.domain;
var canvasWidth = model.canvasWidth * plotGlPixelRatio;
var canvasHeight = model.canvasHeight * plotGlPixelRatio;
var padL = model.pad.l * plotGlPixelRatio;
var padB = model.pad.b * plotGlPixelRatio;
var layoutHeight = model.layoutHeight * plotGlPixelRatio;
var layoutWidth = model.layoutWidth * plotGlPixelRatio;
var deselectedLinesColor = model.deselectedLines.color;
var deselectedLinesOpacity = model.deselectedLines.opacity;
var itemModel = Lib.extendFlat({
key: crossfilterDimensionIndex,
resolution: [canvasWidth, canvasHeight],
viewBoxPos: [x + overdrag, y],
viewBoxSize: [panelSizeX, panelSizeY],
i0: i0,
i1: i1,
dim0A: dims[0].slice(0, 16),
dim0B: dims[0].slice(16, 32),
dim0C: dims[0].slice(32, 48),
dim0D: dims[0].slice(48, 64),
dim1A: dims[1].slice(0, 16),
dim1B: dims[1].slice(16, 32),
dim1C: dims[1].slice(32, 48),
dim1D: dims[1].slice(48, 64),
drwLayer: drwLayer,
contextColor: [
deselectedLinesColor[0] / 255,
deselectedLinesColor[1] / 255,
deselectedLinesColor[2] / 255,
deselectedLinesOpacity !== 'auto' ?
deselectedLinesColor[3] * deselectedLinesOpacity :
Math.max(1 / 255, Math.pow(1 / model.lines.color.length, 1 / 3))
],
scissorX: (itemNumber === leftmost ? 0 : x + overdrag) + (padL - overdrag) + layoutWidth * domain.x[0],
scissorWidth: (itemNumber === rightmost ? canvasWidth - x + overdrag : panelSizeX + 0.5) + (itemNumber === leftmost ? x + overdrag : 0),
scissorY: y + padB + layoutHeight * domain.y[0],
scissorHeight: panelSizeY,
viewportX: padL - overdrag + layoutWidth * domain.x[0],
viewportY: padB + layoutHeight * domain.y[0],
viewportWidth: canvasWidth,
viewportHeight: canvasHeight
}, constraints);
return itemModel;
}
function expandedPixelRange(bounds) {
var dh = maskHeight - 1;
var a = Math.max(0, Math.floor(bounds[0] * dh), 0);
var b = Math.min(dh, Math.ceil(bounds[1] * dh), dh);
return [
Math.min(a, b),
Math.max(a, b)
];
}
module.exports = function(canvasGL, d) {
// context & pick describe which canvas we're talking about - won't change with new data
var isContext = d.context;
var isPick = d.pick;
var regl = d.regl;
var gl = regl._gl;
var supportedLineWidth = gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE);
// ensure here that plotGlPixelRatio is within supported range; otherwise regl throws error
var plotGlPixelRatio = Math.max(
supportedLineWidth[0],
Math.min(
supportedLineWidth[1],
d.viewModel.plotGlPixelRatio
)
);
var renderState = {
currentRafs: {},
drawCompleted: true,
clearOnly: false
};
// state to be set by update and used later
var model;
var vm;
var initialDims;
var sampleCount;
var attributes = emptyAttributes(regl);
var maskTexture;
var paletteTexture = regl.texture(paletteTextureConfig);
var prevAxisOrder = [];
update(d);
var glAes = regl({
profile: false,
blend: {
enable: isContext,
func: {
srcRGB: 'src alpha',
dstRGB: 'one minus src alpha',
srcAlpha: 1,
dstAlpha: 1 // 'one minus src alpha'
},
equation: {
rgb: 'add',
alpha: 'add'
},
color: [0, 0, 0, 0]
},
depth: {
enable: !isContext,
mask: true,
func: 'less',
range: [0, 1]
},
// for polygons
cull: {
enable: true,
face: 'back'
},
scissor: {
enable: true,
box: {
x: regl.prop('scissorX'),
y: regl.prop('scissorY'),
width: regl.prop('scissorWidth'),
height: regl.prop('scissorHeight')
}
},
viewport: {
x: regl.prop('viewportX'),
y: regl.prop('viewportY'),
width: regl.prop('viewportWidth'),
height: regl.prop('viewportHeight')
},
dither: false,
vert: vertexShaderSource,
frag: fragmentShaderSource,
primitive: 'lines',
lineWidth: plotGlPixelRatio,
attributes: attributes,
uniforms: {
resolution: regl.prop('resolution'),
viewBoxPos: regl.prop('viewBoxPos'),
viewBoxSize: regl.prop('viewBoxSize'),
dim0A: regl.prop('dim0A'),
dim1A: regl.prop('dim1A'),
dim0B: regl.prop('dim0B'),
dim1B: regl.prop('dim1B'),
dim0C: regl.prop('dim0C'),
dim1C: regl.prop('dim1C'),
dim0D: regl.prop('dim0D'),
dim1D: regl.prop('dim1D'),
loA: regl.prop('loA'),
hiA: regl.prop('hiA'),
loB: regl.prop('loB'),
hiB: regl.prop('hiB'),
loC: regl.prop('loC'),
hiC: regl.prop('hiC'),
loD: regl.prop('loD'),
hiD: regl.prop('hiD'),
palette: paletteTexture,
contextColor: regl.prop('contextColor'),
maskTexture: regl.prop('maskTexture'),
drwLayer: regl.prop('drwLayer'),
maskHeight: regl.prop('maskHeight')
},
offset: regl.prop('offset'),
count: regl.prop('count')
});
function update(dNew) {
model = dNew.model;
vm = dNew.viewModel;
initialDims = vm.dimensions.slice();
sampleCount = initialDims[0] ? initialDims[0].values.length : 0;
var lines = model.lines;
var color = isPick ? lines.color.map(function(_, i) {return i / lines.color.length;}) : lines.color;
var points = makePoints(sampleCount, initialDims, color);
setAttributes(attributes, sampleCount, points);
if(!isContext && !isPick) {
paletteTexture = regl.texture(Lib.extendFlat({
data: palette(model.unitToColor, 255)
}, paletteTextureConfig));
}
}
function makeConstraints(isContext) {
var i, j, k;
var limits = [[], []];
for(k = 0; k < 64; k++) {
var p = (!isContext && k < initialDims.length) ?
initialDims[k].brush.filter.getBounds() : [-Infinity, Infinity];
limits[0][k] = p[0];
limits[1][k] = p[1];
}
var len = maskHeight * 8;
var mask = new Array(len);
for(i = 0; i < len; i++) {
mask[i] = 255;
}
if(!isContext) {
for(i = 0; i < initialDims.length; i++) {
var u = i % 8;
var v = (i - u) / 8;
var bitMask = Math.pow(2, u);
var dim = initialDims[i];
var ranges = dim.brush.filter.get();
if(ranges.length < 2) continue; // bail if the bounding box based filter is sufficient
var prevEnd = expandedPixelRange(ranges[0])[1];
for(j = 1; j < ranges.length; j++) {
var nextRange = expandedPixelRange(ranges[j]);
for(k = prevEnd + 1; k < nextRange[0]; k++) {
mask[k * 8 + v] &= ~bitMask;
}
prevEnd = Math.max(prevEnd, nextRange[1]);
}
}
}
var textureData = {
// 8 units x 8 bits = 64 bits, just sufficient for the almost 64 dimensions we support
shape: [8, maskHeight],
format: 'alpha',
type: 'uint8',
mag: 'nearest',
min: 'nearest',
data: mask
};
if(maskTexture) maskTexture(textureData);
else maskTexture = regl.texture(textureData);
return {
maskTexture: maskTexture,
maskHeight: maskHeight,
loA: limits[0].slice(0, 16),
loB: limits[0].slice(16, 32),
loC: limits[0].slice(32, 48),
loD: limits[0].slice(48, 64),
hiA: limits[1].slice(0, 16),
hiB: limits[1].slice(16, 32),
hiC: limits[1].slice(32, 48),
hiD: limits[1].slice(48, 64),
};
}
function renderGLParcoords(panels, setChanged, clearOnly) {
var panelCount = panels.length;
var i;
var leftmost;
var rightmost;
var lowestX = Infinity;
var highestX = -Infinity;
for(i = 0; i < panelCount; i++) {
if(panels[i].dim0.canvasX < lowestX) {
lowestX = panels[i].dim0.canvasX;
leftmost = i;
}
if(panels[i].dim1.canvasX > highestX) {
highestX = panels[i].dim1.canvasX;
rightmost = i;
}
}
if(panelCount === 0) {
// clear canvas here, as the panel iteration below will not enter the loop body
clear(regl, 0, 0, model.canvasWidth, model.canvasHeight);
}
var constraints = makeConstraints(isContext);
for(i = 0; i < panelCount; i++) {
var p = panels[i];
var i0 = p.dim0.crossfilterDimensionIndex;
var i1 = p.dim1.crossfilterDimensionIndex;
var x = p.canvasX;
var y = p.canvasY;
var nextX = x + p.panelSizeX;
var plotGlPixelRatio = p.plotGlPixelRatio;
if(setChanged ||
!prevAxisOrder[i0] ||
prevAxisOrder[i0][0] !== x ||
prevAxisOrder[i0][1] !== nextX
) {
prevAxisOrder[i0] = [x, nextX];
var item = makeItem(
model,
leftmost, rightmost, i, i0, i1, x, y,
p.panelSizeX, p.panelSizeY,
p.dim0.crossfilterDimensionIndex,
isContext ? 0 : isPick ? 2 : 1,
constraints,
plotGlPixelRatio
);
renderState.clearOnly = clearOnly;
var blockLineCount = setChanged ? model.lines.blockLineCount : sampleCount;
renderBlock(
regl, glAes, renderState, blockLineCount, sampleCount, item
);
}
}
}
function readPixel(canvasX, canvasY) {
regl.read({
x: canvasX,
y: canvasY,
width: 1,
height: 1,
data: dataPixel
});
return dataPixel;
}
function readPixels(canvasX, canvasY, width, height) {
var pixelArray = new Uint8Array(4 * width * height);
regl.read({
x: canvasX,
y: canvasY,
width: width,
height: height,
data: pixelArray
});
return pixelArray;
}
function destroy() {
canvasGL.style['pointer-events'] = 'none';
paletteTexture.destroy();
if(maskTexture) maskTexture.destroy();
for(var k in attributes) attributes[k].destroy();
}
return {
render: renderGLParcoords,
readPixel: readPixel,
readPixels: readPixels,
destroy: destroy,
update: update
};
};