package.src.plots.gl2d.scene2d.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 Registry = require('../../registry');
var Axes = require('../../plots/cartesian/axes');
var Fx = require('../../components/fx');
var createPlot2D = require('../../../stackgl_modules').gl_plot2d;
var createSpikes = require('../../../stackgl_modules').gl_spikes2d;
var createSelectBox = require('../../../stackgl_modules').gl_select_box;
var getContext = require('webgl-context');
var createOptions = require('./convert');
var createCamera = require('./camera');
var showNoWebGlMsg = require('../../lib/show_no_webgl_msg');
var axisConstraints = require('../cartesian/constraints');
var enforceAxisConstraints = axisConstraints.enforce;
var cleanAxisConstraints = axisConstraints.clean;
var doAutoRange = require('../cartesian/autorange').doAutoRange;
var dragHelpers = require('../../components/dragelement/helpers');
var drawMode = dragHelpers.drawMode;
var selectMode = dragHelpers.selectMode;
var AXES = ['xaxis', 'yaxis'];
var STATIC_CANVAS, STATIC_CONTEXT;
var SUBPLOT_PATTERN = require('../cartesian/constants').SUBPLOT_PATTERN;
function Scene2D(options, fullLayout) {
this.container = options.container;
this.graphDiv = options.graphDiv;
this.pixelRatio = options.plotGlPixelRatio || window.devicePixelRatio;
this.id = options.id;
this.staticPlot = !!options.staticPlot;
this.scrollZoom = this.graphDiv._context._scrollZoom.cartesian;
this.fullData = null;
this.updateRefs(fullLayout);
this.makeFramework();
if(this.stopped) return;
// update options
this.glplotOptions = createOptions(this);
this.glplotOptions.merge(fullLayout);
// create the plot
this.glplot = createPlot2D(this.glplotOptions);
// create camera
this.camera = createCamera(this);
// trace set
this.traces = {};
// create axes spikes
this.spikes = createSpikes(this.glplot);
this.selectBox = createSelectBox(this.glplot, {
innerFill: false,
outerFill: true
});
// last button state
this.lastButtonState = 0;
// last pick result
this.pickResult = null;
// is the mouse over the plot?
// it's OK if this says true when it's not, so long as
// when we get a mouseout we set it to false before handling
this.isMouseOver = true;
// flag to stop render loop
this.stopped = false;
// redraw the plot
this.redraw = this.draw.bind(this);
this.redraw();
}
module.exports = Scene2D;
var proto = Scene2D.prototype;
proto.makeFramework = function() {
// create canvas and gl context
if(this.staticPlot) {
if(!STATIC_CONTEXT) {
STATIC_CANVAS = document.createElement('canvas');
STATIC_CONTEXT = getContext({
canvas: STATIC_CANVAS,
preserveDrawingBuffer: false,
premultipliedAlpha: true,
antialias: true
});
if(!STATIC_CONTEXT) {
throw new Error('Error creating static canvas/context for image server');
}
}
this.canvas = STATIC_CANVAS;
this.gl = STATIC_CONTEXT;
} else {
var liveCanvas = this.container.querySelector('.gl-canvas-focus');
var gl = getContext({
canvas: liveCanvas,
preserveDrawingBuffer: true,
premultipliedAlpha: true
});
if(!gl) {
showNoWebGlMsg(this);
this.stopped = true;
return;
}
this.canvas = liveCanvas;
this.gl = gl;
}
// position the canvas
var canvas = this.canvas;
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.position = 'absolute';
canvas.style.top = '0px';
canvas.style.left = '0px';
canvas.style['pointer-events'] = 'none';
this.updateSize(canvas);
// create SVG container for hover text
var svgContainer = this.svgContainer = document.createElementNS(
'http://www.w3.org/2000/svg',
'svg');
svgContainer.style.position = 'absolute';
svgContainer.style.top = svgContainer.style.left = '0px';
svgContainer.style.width = svgContainer.style.height = '100%';
svgContainer.style['z-index'] = 20;
svgContainer.style['pointer-events'] = 'none';
// create div to catch the mouse event
var mouseContainer = this.mouseContainer = document.createElement('div');
mouseContainer.style.position = 'absolute';
mouseContainer.style['pointer-events'] = 'auto';
this.pickCanvas = this.container.querySelector('.gl-canvas-pick');
// append canvas, hover svg and mouse div to container
var container = this.container;
container.appendChild(svgContainer);
container.appendChild(mouseContainer);
var self = this;
mouseContainer.addEventListener('mouseout', function() {
self.isMouseOver = false;
self.unhover();
});
mouseContainer.addEventListener('mouseover', function() {
self.isMouseOver = true;
});
};
proto.toImage = function(format) {
if(!format) format = 'png';
this.stopped = true;
if(this.staticPlot) this.container.appendChild(STATIC_CANVAS);
// update canvas size
this.updateSize(this.canvas);
// grab context and yank out pixels
var gl = this.glplot.gl;
var w = gl.drawingBufferWidth;
var h = gl.drawingBufferHeight;
// force redraw
gl.clearColor(1, 1, 1, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
this.glplot.setDirty();
this.glplot.draw();
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
var pixels = new Uint8Array(w * h * 4);
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
// flip pixels
for(var j = 0, k = h - 1; j < k; ++j, --k) {
for(var i = 0; i < w; ++i) {
for(var l = 0; l < 4; ++l) {
var tmp = pixels[4 * (w * j + i) + l];
pixels[4 * (w * j + i) + l] = pixels[4 * (w * k + i) + l];
pixels[4 * (w * k + i) + l] = tmp;
}
}
}
var canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
var context = canvas.getContext('2d', {willReadFrequently: true});
var imageData = context.createImageData(w, h);
imageData.data.set(pixels);
context.putImageData(imageData, 0, 0);
var dataURL;
switch(format) {
case 'jpeg':
dataURL = canvas.toDataURL('image/jpeg');
break;
case 'webp':
dataURL = canvas.toDataURL('image/webp');
break;
default:
dataURL = canvas.toDataURL('image/png');
}
if(this.staticPlot) this.container.removeChild(STATIC_CANVAS);
return dataURL;
};
proto.updateSize = function(canvas) {
if(!canvas) canvas = this.canvas;
var pixelRatio = this.pixelRatio;
var fullLayout = this.fullLayout;
var width = fullLayout.width;
var height = fullLayout.height;
var pixelWidth = Math.ceil(pixelRatio * width) |0;
var pixelHeight = Math.ceil(pixelRatio * height) |0;
// check for resize
if(canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
canvas.width = pixelWidth;
canvas.height = pixelHeight;
}
return canvas;
};
proto.computeTickMarks = function() {
this.xaxis.setScale();
this.yaxis.setScale();
var nextTicks = [
Axes.calcTicks(this.xaxis),
Axes.calcTicks(this.yaxis)
];
for(var j = 0; j < 2; ++j) {
for(var i = 0; i < nextTicks[j].length; ++i) {
// coercing tick value (may not be a string) to a string
nextTicks[j][i].text = nextTicks[j][i].text + '';
}
}
return nextTicks;
};
function compareTicks(a, b) {
for(var i = 0; i < 2; ++i) {
var aticks = a[i];
var bticks = b[i];
if(aticks.length !== bticks.length) return true;
for(var j = 0; j < aticks.length; ++j) {
if(aticks[j].x !== bticks[j].x) return true;
}
}
return false;
}
proto.updateRefs = function(newFullLayout) {
this.fullLayout = newFullLayout;
var spmatch = this.id.match(SUBPLOT_PATTERN);
var xaxisName = 'xaxis' + spmatch[1];
var yaxisName = 'yaxis' + spmatch[2];
this.xaxis = this.fullLayout[xaxisName];
this.yaxis = this.fullLayout[yaxisName];
};
proto.relayoutCallback = function() {
var graphDiv = this.graphDiv;
var xaxis = this.xaxis;
var yaxis = this.yaxis;
var layout = graphDiv.layout;
// make a meaningful value to be passed on to possible 'plotly_relayout' subscriber(s)
var update = {};
var xrange = update[xaxis._name + '.range'] = xaxis.range.slice();
var yrange = update[yaxis._name + '.range'] = yaxis.range.slice();
update[xaxis._name + '.autorange'] = xaxis.autorange;
update[yaxis._name + '.autorange'] = yaxis.autorange;
Registry.call('_storeDirectGUIEdit', graphDiv.layout, graphDiv._fullLayout._preGUI, update);
// update the input layout
var xaIn = layout[xaxis._name];
xaIn.range = xrange;
xaIn.autorange = xaxis.autorange;
var yaIn = layout[yaxis._name];
yaIn.range = yrange;
yaIn.autorange = yaxis.autorange;
// lastInputTime helps determine which one is the latest input (if async)
update.lastInputTime = this.camera.lastInputTime;
graphDiv.emit('plotly_relayout', update);
};
proto.cameraChanged = function() {
var camera = this.camera;
this.glplot.setDataBox(this.calcDataBox());
var nextTicks = this.computeTickMarks();
var curTicks = this.glplotOptions.ticks;
if(compareTicks(nextTicks, curTicks)) {
this.glplotOptions.ticks = nextTicks;
this.glplotOptions.dataBox = camera.dataBox;
this.glplot.update(this.glplotOptions);
this.handleAnnotations();
}
};
proto.handleAnnotations = function() {
var gd = this.graphDiv;
var annotations = this.fullLayout.annotations;
for(var i = 0; i < annotations.length; i++) {
var ann = annotations[i];
if(ann.xref === this.xaxis._id && ann.yref === this.yaxis._id) {
Registry.getComponentMethod('annotations', 'drawOne')(gd, i);
}
}
};
proto.destroy = function() {
if(!this.glplot) return;
var traces = this.traces;
if(traces) {
Object.keys(traces).map(function(key) {
traces[key].dispose();
delete traces[key];
});
}
this.glplot.dispose();
this.container.removeChild(this.svgContainer);
this.container.removeChild(this.mouseContainer);
this.fullData = null;
this.glplot = null;
this.stopped = true;
this.camera.mouseListener.enabled = false;
this.mouseContainer.removeEventListener('wheel', this.camera.wheelListener);
this.camera = null;
};
proto.plot = function(fullData, calcData, fullLayout) {
var glplot = this.glplot;
this.updateRefs(fullLayout);
this.xaxis.clearCalc();
this.yaxis.clearCalc();
this.updateTraces(fullData, calcData);
this.updateFx(fullLayout.dragmode);
var width = fullLayout.width;
var height = fullLayout.height;
this.updateSize(this.canvas);
var options = this.glplotOptions;
options.merge(fullLayout);
options.screenBox = [0, 0, width, height];
var mockGraphDiv = {_fullLayout: {
_axisConstraintGroups: fullLayout._axisConstraintGroups,
xaxis: this.xaxis,
yaxis: this.yaxis,
_size: fullLayout._size
}};
cleanAxisConstraints(mockGraphDiv, this.xaxis);
cleanAxisConstraints(mockGraphDiv, this.yaxis);
var size = fullLayout._size;
var domainX = this.xaxis.domain;
var domainY = this.yaxis.domain;
options.viewBox = [
size.l + domainX[0] * size.w,
size.b + domainY[0] * size.h,
(width - size.r) - (1 - domainX[1]) * size.w,
(height - size.t) - (1 - domainY[1]) * size.h
];
this.mouseContainer.style.width = size.w * (domainX[1] - domainX[0]) + 'px';
this.mouseContainer.style.height = size.h * (domainY[1] - domainY[0]) + 'px';
this.mouseContainer.height = size.h * (domainY[1] - domainY[0]);
this.mouseContainer.style.left = size.l + domainX[0] * size.w + 'px';
this.mouseContainer.style.top = size.t + (1 - domainY[1]) * size.h + 'px';
var ax, i;
for(i = 0; i < 2; ++i) {
ax = this[AXES[i]];
ax._length = options.viewBox[i + 2] - options.viewBox[i];
doAutoRange(this.graphDiv, ax);
ax.setScale();
}
enforceAxisConstraints(mockGraphDiv);
options.ticks = this.computeTickMarks();
options.dataBox = this.calcDataBox();
options.merge(fullLayout);
glplot.update(options);
// force redraw so that promise is returned when rendering is completed
this.glplot.draw();
};
proto.calcDataBox = function() {
var xaxis = this.xaxis;
var yaxis = this.yaxis;
var xrange = xaxis.range;
var yrange = yaxis.range;
var xr2l = xaxis.r2l;
var yr2l = yaxis.r2l;
return [xr2l(xrange[0]), yr2l(yrange[0]), xr2l(xrange[1]), yr2l(yrange[1])];
};
proto.setRanges = function(dataBox) {
var xaxis = this.xaxis;
var yaxis = this.yaxis;
var xl2r = xaxis.l2r;
var yl2r = yaxis.l2r;
xaxis.range = [xl2r(dataBox[0]), xl2r(dataBox[2])];
yaxis.range = [yl2r(dataBox[1]), yl2r(dataBox[3])];
};
proto.updateTraces = function(fullData, calcData) {
var traceIds = Object.keys(this.traces);
var i, j, fullTrace;
this.fullData = fullData;
// remove empty traces
traceIdLoop:
for(i = 0; i < traceIds.length; i++) {
var oldUid = traceIds[i];
var oldTrace = this.traces[oldUid];
for(j = 0; j < fullData.length; j++) {
fullTrace = fullData[j];
if(fullTrace.uid === oldUid && fullTrace.type === oldTrace.type) {
continue traceIdLoop;
}
}
oldTrace.dispose();
delete this.traces[oldUid];
}
// update / create trace objects
for(i = 0; i < fullData.length; i++) {
fullTrace = fullData[i];
var calcTrace = calcData[i];
var traceObj = this.traces[fullTrace.uid];
if(traceObj) traceObj.update(fullTrace, calcTrace);
else {
traceObj = fullTrace._module.plot(this, fullTrace, calcTrace);
this.traces[fullTrace.uid] = traceObj;
}
}
// order object per traces
this.glplot.objects.sort(function(a, b) {
return a._trace.index - b._trace.index;
});
};
proto.updateFx = function(dragmode) {
// switch to svg interactions in lasso/select mode & shape drawing
if(selectMode(dragmode) || drawMode(dragmode)) {
this.pickCanvas.style['pointer-events'] = 'none';
this.mouseContainer.style['pointer-events'] = 'none';
} else {
this.pickCanvas.style['pointer-events'] = 'auto';
this.mouseContainer.style['pointer-events'] = 'auto';
}
// set proper cursor
if(dragmode === 'pan') {
this.mouseContainer.style.cursor = 'move';
} else if(dragmode === 'zoom') {
this.mouseContainer.style.cursor = 'crosshair';
} else {
this.mouseContainer.style.cursor = null;
}
};
proto.emitPointAction = function(nextSelection, eventType) {
var uid = nextSelection.trace.uid;
var ptNumber = nextSelection.pointIndex;
var trace;
for(var i = 0; i < this.fullData.length; i++) {
if(this.fullData[i].uid === uid) {
trace = this.fullData[i];
}
}
var pointData = {
x: nextSelection.traceCoord[0],
y: nextSelection.traceCoord[1],
curveNumber: trace.index,
pointNumber: ptNumber,
data: trace._input,
fullData: this.fullData,
xaxis: this.xaxis,
yaxis: this.yaxis
};
Fx.appendArrayPointValue(pointData, trace, ptNumber);
this.graphDiv.emit(eventType, {points: [pointData]});
};
proto.draw = function() {
if(this.stopped) return;
requestAnimationFrame(this.redraw);
var glplot = this.glplot;
var camera = this.camera;
var mouseListener = camera.mouseListener;
var mouseUp = this.lastButtonState === 1 && mouseListener.buttons === 0;
var fullLayout = this.fullLayout;
this.lastButtonState = mouseListener.buttons;
this.cameraChanged();
var x = mouseListener.x * glplot.pixelRatio;
var y = this.canvas.height - glplot.pixelRatio * mouseListener.y;
var result;
if(camera.boxEnabled && fullLayout.dragmode === 'zoom') {
this.selectBox.enabled = true;
var selectBox = this.selectBox.selectBox = [
Math.min(camera.boxStart[0], camera.boxEnd[0]),
Math.min(camera.boxStart[1], camera.boxEnd[1]),
Math.max(camera.boxStart[0], camera.boxEnd[0]),
Math.max(camera.boxStart[1], camera.boxEnd[1])
];
// 1D zoom
for(var i = 0; i < 2; i++) {
if(camera.boxStart[i] === camera.boxEnd[i]) {
selectBox[i] = glplot.dataBox[i];
selectBox[i + 2] = glplot.dataBox[i + 2];
}
}
glplot.setDirty();
} else if(!camera.panning && this.isMouseOver) {
this.selectBox.enabled = false;
var size = fullLayout._size;
var domainX = this.xaxis.domain;
var domainY = this.yaxis.domain;
result = glplot.pick(
(x / glplot.pixelRatio) + size.l + domainX[0] * size.w,
(y / glplot.pixelRatio) - (size.t + (1 - domainY[1]) * size.h)
);
var nextSelection = result && result.object._trace.handlePick(result);
if(nextSelection && mouseUp) {
this.emitPointAction(nextSelection, 'plotly_click');
}
if(result && result.object._trace.hoverinfo !== 'skip' && fullLayout.hovermode) {
if(nextSelection && (
!this.lastPickResult ||
this.lastPickResult.traceUid !== nextSelection.trace.uid ||
this.lastPickResult.dataCoord[0] !== nextSelection.dataCoord[0] ||
this.lastPickResult.dataCoord[1] !== nextSelection.dataCoord[1])
) {
var selection = nextSelection;
this.lastPickResult = {
traceUid: nextSelection.trace ? nextSelection.trace.uid : null,
dataCoord: nextSelection.dataCoord.slice()
};
this.spikes.update({ center: result.dataCoord });
selection.screenCoord = [
((glplot.viewBox[2] - glplot.viewBox[0]) *
(result.dataCoord[0] - glplot.dataBox[0]) /
(glplot.dataBox[2] - glplot.dataBox[0]) + glplot.viewBox[0]) /
glplot.pixelRatio,
(this.canvas.height - (glplot.viewBox[3] - glplot.viewBox[1]) *
(result.dataCoord[1] - glplot.dataBox[1]) /
(glplot.dataBox[3] - glplot.dataBox[1]) - glplot.viewBox[1]) /
glplot.pixelRatio
];
// this needs to happen before the next block that deletes traceCoord data
// also it's important to copy, otherwise data is lost by the time event data is read
this.emitPointAction(nextSelection, 'plotly_hover');
var trace = this.fullData[selection.trace.index] || {};
var ptNumber = selection.pointIndex;
var hoverinfo = Fx.castHoverinfo(trace, fullLayout, ptNumber);
if(hoverinfo && hoverinfo !== 'all') {
var parts = hoverinfo.split('+');
if(parts.indexOf('x') === -1) selection.traceCoord[0] = undefined;
if(parts.indexOf('y') === -1) selection.traceCoord[1] = undefined;
if(parts.indexOf('z') === -1) selection.traceCoord[2] = undefined;
if(parts.indexOf('text') === -1) selection.textLabel = undefined;
if(parts.indexOf('name') === -1) selection.name = undefined;
}
Fx.loneHover({
x: selection.screenCoord[0],
y: selection.screenCoord[1],
xLabel: this.hoverFormatter('xaxis', selection.traceCoord[0]),
yLabel: this.hoverFormatter('yaxis', selection.traceCoord[1]),
zLabel: selection.traceCoord[2],
text: selection.textLabel,
name: selection.name,
color: Fx.castHoverOption(trace, ptNumber, 'bgcolor') || selection.color,
borderColor: Fx.castHoverOption(trace, ptNumber, 'bordercolor'),
fontFamily: Fx.castHoverOption(trace, ptNumber, 'font.family'),
fontSize: Fx.castHoverOption(trace, ptNumber, 'font.size'),
fontColor: Fx.castHoverOption(trace, ptNumber, 'font.color'),
nameLength: Fx.castHoverOption(trace, ptNumber, 'namelength'),
textAlign: Fx.castHoverOption(trace, ptNumber, 'align')
}, {
container: this.svgContainer,
gd: this.graphDiv
});
}
}
}
// Remove hover effects if we're not over a point OR
// if we're zooming or panning (in which case result is not set)
if(!result) {
this.unhover();
}
glplot.draw();
};
proto.unhover = function() {
if(this.lastPickResult) {
this.spikes.update({});
this.lastPickResult = null;
this.graphDiv.emit('plotly_unhover');
Fx.loneUnhover(this.svgContainer);
}
};
proto.hoverFormatter = function(axisName, val) {
if(val === undefined) return undefined;
var axis = this[axisName];
return Axes.tickText(axis, axis.c2l(val), 'hover').text;
};