plotly.js
Version:
The open source javascript graphing library that powers plotly
368 lines (327 loc) • 13.6 kB
JavaScript
/**
* Copyright 2012-2020, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
var createScatter = require('regl-scatter2d');
var createLine = require('regl-line2d');
var createError = require('regl-error2d');
var Text = require('gl-text');
var Lib = require('../../lib');
var selectMode = require('../../components/dragelement/helpers').selectMode;
var prepareRegl = require('../../lib/prepare_regl');
var subTypes = require('../scatter/subtypes');
var linkTraces = require('../scatter/link_traces');
var styleTextSelection = require('./edit_style').styleTextSelection;
function getViewport(fullLayout, xaxis, yaxis) {
var gs = fullLayout._size;
var width = fullLayout.width;
var height = fullLayout.height;
return [
gs.l + xaxis.domain[0] * gs.w,
gs.b + yaxis.domain[0] * gs.h,
(width - gs.r) - (1 - xaxis.domain[1]) * gs.w,
(height - gs.t) - (1 - yaxis.domain[1]) * gs.h
];
}
module.exports = function plot(gd, subplot, cdata) {
if(!cdata.length) return;
var fullLayout = gd._fullLayout;
var scene = subplot._scene;
var xaxis = subplot.xaxis;
var yaxis = subplot.yaxis;
var i, j;
// we may have more subplots than initialized data due to Axes.getSubplots method
if(!scene) return;
var success = prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']);
if(!success) {
scene.init();
return;
}
var count = scene.count;
var regl = fullLayout._glcanvas.data()[0].regl;
// that is needed for fills
linkTraces(gd, subplot, cdata);
if(scene.dirty) {
// make sure scenes are created
if(scene.error2d === true) {
scene.error2d = createError(regl);
}
if(scene.line2d === true) {
scene.line2d = createLine(regl);
}
if(scene.scatter2d === true) {
scene.scatter2d = createScatter(regl, { constPointSize: true });
}
if(scene.fill2d === true) {
scene.fill2d = createLine(regl);
}
if(scene.glText === true) {
scene.glText = new Array(count);
for(i = 0; i < count; i++) {
scene.glText[i] = new Text(regl);
}
}
// update main marker options
if(scene.glText) {
if(count > scene.glText.length) {
// add gl text marker
var textsToAdd = count - scene.glText.length;
for(i = 0; i < textsToAdd; i++) {
scene.glText.push(new Text(regl));
}
} else if(count < scene.glText.length) {
// remove gl text marker
var textsToRemove = scene.glText.length - count;
var removedTexts = scene.glText.splice(count, textsToRemove);
removedTexts.forEach(function(text) { text.destroy(); });
}
for(i = 0; i < count; i++) {
scene.glText[i].update(scene.textOptions[i]);
}
}
if(scene.line2d) {
scene.line2d.update(scene.lineOptions);
scene.lineOptions = scene.lineOptions.map(function(lineOptions) {
if(lineOptions && lineOptions.positions) {
var srcPos = lineOptions.positions;
var firstptdef = 0;
while(firstptdef < srcPos.length && (isNaN(srcPos[firstptdef]) || isNaN(srcPos[firstptdef + 1]))) {
firstptdef += 2;
}
var lastptdef = srcPos.length - 2;
while(lastptdef > firstptdef && (isNaN(srcPos[lastptdef]) || isNaN(srcPos[lastptdef + 1]))) {
lastptdef -= 2;
}
lineOptions.positions = srcPos.slice(firstptdef, lastptdef + 2);
}
return lineOptions;
});
scene.line2d.update(scene.lineOptions);
}
if(scene.error2d) {
var errorBatch = (scene.errorXOptions || []).concat(scene.errorYOptions || []);
scene.error2d.update(errorBatch);
}
if(scene.scatter2d) {
scene.scatter2d.update(scene.markerOptions);
}
// fill requires linked traces, so we generate it's positions here
scene.fillOrder = Lib.repeat(null, count);
if(scene.fill2d) {
scene.fillOptions = scene.fillOptions.map(function(fillOptions, i) {
var cdscatter = cdata[i];
if(!fillOptions || !cdscatter || !cdscatter[0] || !cdscatter[0].trace) return;
var cd = cdscatter[0];
var trace = cd.trace;
var stash = cd.t;
var lineOptions = scene.lineOptions[i];
var last, j;
var fillData = [];
if(trace._ownfill) fillData.push(i);
if(trace._nexttrace) fillData.push(i + 1);
if(fillData.length) scene.fillOrder[i] = fillData;
var pos = [];
var srcPos = (lineOptions && lineOptions.positions) || stash.positions;
var firstptdef, lastptdef;
if(trace.fill === 'tozeroy') {
firstptdef = 0;
while(firstptdef < srcPos.length && isNaN(srcPos[firstptdef + 1])) {
firstptdef += 2;
}
lastptdef = srcPos.length - 2;
while(lastptdef > firstptdef && isNaN(srcPos[lastptdef + 1])) {
lastptdef -= 2;
}
if(srcPos[firstptdef + 1] !== 0) {
pos = [srcPos[firstptdef], 0];
}
pos = pos.concat(srcPos.slice(firstptdef, lastptdef + 2));
if(srcPos[lastptdef + 1] !== 0) {
pos = pos.concat([srcPos[lastptdef], 0]);
}
} else if(trace.fill === 'tozerox') {
firstptdef = 0;
while(firstptdef < srcPos.length && isNaN(srcPos[firstptdef])) {
firstptdef += 2;
}
lastptdef = srcPos.length - 2;
while(lastptdef > firstptdef && isNaN(srcPos[lastptdef])) {
lastptdef -= 2;
}
if(srcPos[firstptdef] !== 0) {
pos = [0, srcPos[firstptdef + 1]];
}
pos = pos.concat(srcPos.slice(firstptdef, lastptdef + 2));
if(srcPos[lastptdef] !== 0) {
pos = pos.concat([ 0, srcPos[lastptdef + 1]]);
}
} else if(trace.fill === 'toself' || trace.fill === 'tonext') {
pos = [];
last = 0;
for(j = 0; j < srcPos.length; j += 2) {
if(isNaN(srcPos[j]) || isNaN(srcPos[j + 1])) {
pos = pos.concat(srcPos.slice(last, j));
pos.push(srcPos[last], srcPos[last + 1]);
last = j + 2;
}
}
pos = pos.concat(srcPos.slice(last));
if(last) {
pos.push(srcPos[last], srcPos[last + 1]);
}
} else {
var nextTrace = trace._nexttrace;
if(nextTrace) {
var nextOptions = scene.lineOptions[i + 1];
if(nextOptions) {
var nextPos = nextOptions.positions;
if(trace.fill === 'tonexty') {
pos = srcPos.slice();
for(i = Math.floor(nextPos.length / 2); i--;) {
var xx = nextPos[i * 2];
var yy = nextPos[i * 2 + 1];
if(isNaN(xx) || isNaN(yy)) continue;
pos.push(xx, yy);
}
fillOptions.fill = nextTrace.fillcolor;
}
}
}
}
// detect prev trace positions to exclude from current fill
if(trace._prevtrace && trace._prevtrace.fill === 'tonext') {
var prevLinePos = scene.lineOptions[i - 1].positions;
// FIXME: likely this logic should be tested better
var offset = pos.length / 2;
last = offset;
var hole = [last];
for(j = 0; j < prevLinePos.length; j += 2) {
if(isNaN(prevLinePos[j]) || isNaN(prevLinePos[j + 1])) {
hole.push(j / 2 + offset + 1);
last = j + 2;
}
}
pos = pos.concat(prevLinePos);
fillOptions.hole = hole;
}
fillOptions.fillmode = trace.fill;
fillOptions.opacity = trace.opacity;
fillOptions.positions = pos;
return fillOptions;
});
scene.fill2d.update(scene.fillOptions);
}
}
// form batch arrays, and check for selected points
var dragmode = fullLayout.dragmode;
var isSelectMode = selectMode(dragmode);
var clickSelectEnabled = fullLayout.clickmode.indexOf('select') > -1;
for(i = 0; i < count; i++) {
var cd0 = cdata[i][0];
var trace = cd0.trace;
var stash = cd0.t;
var index = stash.index;
var len = trace._length;
var x = stash.x;
var y = stash.y;
if(trace.selectedpoints || isSelectMode || clickSelectEnabled) {
if(!isSelectMode) isSelectMode = true;
// regenerate scene batch, if traces number changed during selection
if(trace.selectedpoints) {
var selPts = scene.selectBatch[index] = Lib.selIndices2selPoints(trace);
var selDict = {};
for(j = 0; j < selPts.length; j++) {
selDict[selPts[j]] = 1;
}
var unselPts = [];
for(j = 0; j < len; j++) {
if(!selDict[j]) unselPts.push(j);
}
scene.unselectBatch[index] = unselPts;
}
// precalculate px coords since we are not going to pan during select
// TODO, could do better here e.g.
// - spin that in a webworker
// - compute selection from polygons in data coordinates
// (maybe just for linear axes)
var xpx = stash.xpx = new Array(len);
var ypx = stash.ypx = new Array(len);
for(j = 0; j < len; j++) {
xpx[j] = xaxis.c2p(x[j]);
ypx[j] = yaxis.c2p(y[j]);
}
} else {
stash.xpx = stash.ypx = null;
}
}
if(isSelectMode) {
// create scatter instance by cloning scatter2d
if(!scene.select2d) {
scene.select2d = createScatter(fullLayout._glcanvas.data()[1].regl);
}
// use unselected styles on 'context' canvas
if(scene.scatter2d) {
var unselOpts = new Array(count);
for(i = 0; i < count; i++) {
unselOpts[i] = scene.selectBatch[i].length || scene.unselectBatch[i].length ?
scene.markerUnselectedOptions[i] :
{};
}
scene.scatter2d.update(unselOpts);
}
// use selected style on 'focus' canvas
if(scene.select2d) {
scene.select2d.update(scene.markerOptions);
scene.select2d.update(scene.markerSelectedOptions);
}
if(scene.glText) {
cdata.forEach(function(cdscatter) {
var trace = ((cdscatter || [])[0] || {}).trace || {};
if(subTypes.hasText(trace)) {
styleTextSelection(cdscatter);
}
});
}
} else {
// reset 'context' scatter2d opts to base opts,
// thus unsetting markerUnselectedOptions from selection
if(scene.scatter2d) {
scene.scatter2d.update(scene.markerOptions);
}
}
// provide viewport and range
var vpRange0 = {
viewport: getViewport(fullLayout, xaxis, yaxis),
// TODO do we need those fallbacks?
range: [
(xaxis._rl || xaxis.range)[0],
(yaxis._rl || yaxis.range)[0],
(xaxis._rl || xaxis.range)[1],
(yaxis._rl || yaxis.range)[1]
]
};
var vpRange = Lib.repeat(vpRange0, scene.count);
// upload viewport/range data to GPU
if(scene.fill2d) {
scene.fill2d.update(vpRange);
}
if(scene.line2d) {
scene.line2d.update(vpRange);
}
if(scene.error2d) {
scene.error2d.update(vpRange.concat(vpRange));
}
if(scene.scatter2d) {
scene.scatter2d.update(vpRange);
}
if(scene.select2d) {
scene.select2d.update(vpRange);
}
if(scene.glText) {
scene.glText.forEach(function(text) { text.update(vpRange0); });
}
};