package.src.traces.parcoords.lines.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of plotly.js Show documentation
Show all versions of plotly.js Show documentation
The open source javascript graphing library that powers plotly
The newest version!
'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
};
};