plotly.js
Version:
The open source javascript graphing library that powers plotly
702 lines (604 loc) • 23.1 kB
JavaScript
'use strict';
var isNumeric = require('fast-isnumeric');
var svgSdf = require('svg-path-sdf');
var rgba = require('color-normalize');
var Registry = require('../../registry');
var Lib = require('../../lib');
var isArrayOrTypedArray = Lib.isArrayOrTypedArray;
var Drawing = require('../../components/drawing');
var AxisIDs = require('../../plots/cartesian/axis_ids');
var formatColor = require('../../lib/gl_format_color').formatColor;
var subTypes = require('../scatter/subtypes');
var makeBubbleSizeFn = require('../scatter/make_bubble_size_func');
var helpers = require('./helpers');
var constants = require('./constants');
var DESELECTDIM = require('../../constants/interactions').DESELECTDIM;
var TEXTOFFSETSIGN = {
start: 1, left: 1, end: -1, right: -1, middle: 0, center: 0, bottom: 1, top: -1
};
var appendArrayPointValue = require('../../components/fx/helpers').appendArrayPointValue;
function convertStyle(gd, trace) {
var i;
var opts = {
marker: undefined,
markerSel: undefined,
markerUnsel: undefined,
line: undefined,
fill: undefined,
errorX: undefined,
errorY: undefined,
text: undefined,
textSel: undefined,
textUnsel: undefined
};
var plotGlPixelRatio = gd._context.plotGlPixelRatio;
if(trace.visible !== true) return opts;
if(subTypes.hasText(trace)) {
opts.text = convertTextStyle(gd, trace);
opts.textSel = convertTextSelection(gd, trace, trace.selected);
opts.textUnsel = convertTextSelection(gd, trace, trace.unselected);
}
if(subTypes.hasMarkers(trace)) {
opts.marker = convertMarkerStyle(gd, trace);
opts.markerSel = convertMarkerSelection(gd, trace, trace.selected);
opts.markerUnsel = convertMarkerSelection(gd, trace, trace.unselected);
if(!trace.unselected && isArrayOrTypedArray(trace.marker.opacity)) {
var mo = trace.marker.opacity;
opts.markerUnsel.opacity = new Array(mo.length);
for(i = 0; i < mo.length; i++) {
opts.markerUnsel.opacity[i] = DESELECTDIM * mo[i];
}
}
}
if(subTypes.hasLines(trace)) {
opts.line = {
overlay: true,
thickness: trace.line.width * plotGlPixelRatio,
color: trace.line.color,
opacity: trace.opacity
};
var dashes = (constants.DASHES[trace.line.dash] || [1]).slice();
for(i = 0; i < dashes.length; ++i) {
dashes[i] *= trace.line.width * plotGlPixelRatio;
}
opts.line.dashes = dashes;
}
if(trace.error_x && trace.error_x.visible) {
opts.errorX = convertErrorBarStyle(trace, trace.error_x, plotGlPixelRatio);
}
if(trace.error_y && trace.error_y.visible) {
opts.errorY = convertErrorBarStyle(trace, trace.error_y, plotGlPixelRatio);
}
if(!!trace.fill && trace.fill !== 'none') {
opts.fill = {
closed: true,
fill: trace.fillcolor,
thickness: 0
};
}
return opts;
}
function convertTextStyle(gd, trace) {
var fullLayout = gd._fullLayout;
var count = trace._length;
var textfontIn = trace.textfont;
var textpositionIn = trace.textposition;
var textPos = isArrayOrTypedArray(textpositionIn) ? textpositionIn : [textpositionIn];
var tfc = textfontIn.color;
var tfs = textfontIn.size;
var tff = textfontIn.family;
var tfw = textfontIn.weight;
var tfy = textfontIn.style;
var tfv = textfontIn.variant;
var optsOut = {};
var i;
var plotGlPixelRatio = gd._context.plotGlPixelRatio;
var texttemplate = trace.texttemplate;
if(texttemplate) {
optsOut.text = [];
var d3locale = fullLayout._d3locale;
var isArray = Array.isArray(texttemplate);
var N = isArray ? Math.min(texttemplate.length, count) : count;
var txt = isArray ?
function(i) { return texttemplate[i]; } :
function() { return texttemplate; };
for(i = 0; i < N; i++) {
var d = {i: i};
var labels = trace._module.formatLabels(d, trace, fullLayout);
var pointValues = {};
appendArrayPointValue(pointValues, trace, i);
var meta = trace._meta || {};
optsOut.text.push(Lib.texttemplateString(txt(i), labels, d3locale, pointValues, d, meta));
}
} else {
if(isArrayOrTypedArray(trace.text) && trace.text.length < count) {
// if text array is shorter, we'll need to append to it, so let's slice to prevent mutating
optsOut.text = trace.text.slice();
} else {
optsOut.text = trace.text;
}
}
// pad text array with empty strings
if(isArrayOrTypedArray(optsOut.text)) {
for(i = optsOut.text.length; i < count; i++) {
optsOut.text[i] = '';
}
}
optsOut.opacity = trace.opacity;
optsOut.font = {};
optsOut.align = [];
optsOut.baseline = [];
for(i = 0; i < textPos.length; i++) {
var tp = textPos[i].split(/\s+/);
switch(tp[1]) {
case 'left':
optsOut.align.push('right');
break;
case 'right':
optsOut.align.push('left');
break;
default:
optsOut.align.push(tp[1]);
}
switch(tp[0]) {
case 'top':
optsOut.baseline.push('bottom');
break;
case 'bottom':
optsOut.baseline.push('top');
break;
default:
optsOut.baseline.push(tp[0]);
}
}
if(isArrayOrTypedArray(tfc)) {
optsOut.color = new Array(count);
for(i = 0; i < count; i++) {
optsOut.color[i] = tfc[i];
}
} else {
optsOut.color = tfc;
}
if(
isArrayOrTypedArray(tfs) ||
Array.isArray(tff) ||
isArrayOrTypedArray(tfw) ||
Array.isArray(tfy) ||
Array.isArray(tfv)
) {
// if any textfont param is array - make render a batch
optsOut.font = new Array(count);
for(i = 0; i < count; i++) {
var fonti = optsOut.font[i] = {};
fonti.size = (
Lib.isTypedArray(tfs) ? tfs[i] :
isArrayOrTypedArray(tfs) ? (
isNumeric(tfs[i]) ? tfs[i] : 0
) : tfs
) * plotGlPixelRatio;
fonti.family = Array.isArray(tff) ? tff[i] : tff;
fonti.weight = weightFallBack(isArrayOrTypedArray(tfw) ? tfw[i] : tfw);
fonti.style = Array.isArray(tfy) ? tfy[i] : tfy;
fonti.variant = Array.isArray(tfv) ? tfv[i] : tfv;
}
} else {
// if both are single values, make render fast single-value
optsOut.font = {
size: tfs * plotGlPixelRatio,
family: tff,
weight: weightFallBack(tfw),
style: tfy,
variant: tfv
};
}
return optsOut;
}
// scattergl rendering pipeline has limited support of numeric weight values
// Here we map the numbers to be either bold or normal.
function weightFallBack(w) {
if(w <= 1000) {
return w > 500 ? 'bold' : 'normal';
}
return w;
}
function convertMarkerStyle(gd, trace) {
var count = trace._length;
var optsIn = trace.marker;
var optsOut = {};
var i;
var multiSymbol = isArrayOrTypedArray(optsIn.symbol);
var multiAngle = isArrayOrTypedArray(optsIn.angle);
var multiColor = isArrayOrTypedArray(optsIn.color);
var multiLineColor = isArrayOrTypedArray(optsIn.line.color);
var multiOpacity = isArrayOrTypedArray(optsIn.opacity);
var multiSize = isArrayOrTypedArray(optsIn.size);
var multiLineWidth = isArrayOrTypedArray(optsIn.line.width);
var isOpen;
if(!multiSymbol) isOpen = helpers.isOpenSymbol(optsIn.symbol);
// prepare colors
if(multiSymbol || multiColor || multiLineColor || multiOpacity || multiAngle) {
optsOut.symbols = new Array(count);
optsOut.angles = new Array(count);
optsOut.colors = new Array(count);
optsOut.borderColors = new Array(count);
var symbols = optsIn.symbol;
var angles = optsIn.angle;
var colors = formatColor(optsIn, optsIn.opacity, count);
var borderColors = formatColor(optsIn.line, optsIn.opacity, count);
if(!isArrayOrTypedArray(borderColors[0])) {
var borderColor = borderColors;
borderColors = Array(count);
for(i = 0; i < count; i++) {
borderColors[i] = borderColor;
}
}
if(!isArrayOrTypedArray(colors[0])) {
var color = colors;
colors = Array(count);
for(i = 0; i < count; i++) {
colors[i] = color;
}
}
if(!isArrayOrTypedArray(symbols)) {
var symbol = symbols;
symbols = Array(count);
for(i = 0; i < count; i++) {
symbols[i] = symbol;
}
}
if(!isArrayOrTypedArray(angles)) {
var angle = angles;
angles = Array(count);
for(i = 0; i < count; i++) {
angles[i] = angle;
}
}
optsOut.symbols = symbols;
optsOut.angles = angles;
optsOut.colors = colors;
optsOut.borderColors = borderColors;
for(i = 0; i < count; i++) {
if(multiSymbol) {
isOpen = helpers.isOpenSymbol(optsIn.symbol[i]);
}
if(isOpen) {
borderColors[i] = colors[i].slice();
colors[i] = colors[i].slice();
colors[i][3] = 0;
}
}
optsOut.opacity = trace.opacity;
optsOut.markers = new Array(count);
for(i = 0; i < count; i++) {
optsOut.markers[i] = getSymbolSdf({
mx: optsOut.symbols[i],
ma: optsOut.angles[i]
}, trace);
}
} else {
if(isOpen) {
optsOut.color = rgba(optsIn.color, 'uint8');
optsOut.color[3] = 0;
optsOut.borderColor = rgba(optsIn.color, 'uint8');
} else {
optsOut.color = rgba(optsIn.color, 'uint8');
optsOut.borderColor = rgba(optsIn.line.color, 'uint8');
}
optsOut.opacity = trace.opacity * optsIn.opacity;
optsOut.marker = getSymbolSdf({
mx: optsIn.symbol,
ma: optsIn.angle
}, trace);
}
// prepare sizes
var sizeFactor = 1;
var markerSizeFunc = makeBubbleSizeFn(trace, sizeFactor);
var s;
if(multiSize || multiLineWidth) {
var sizes = optsOut.sizes = new Array(count);
var borderSizes = optsOut.borderSizes = new Array(count);
var sizeTotal = 0;
var sizeAvg;
if(multiSize) {
for(i = 0; i < count; i++) {
sizes[i] = markerSizeFunc(optsIn.size[i]);
sizeTotal += sizes[i];
}
sizeAvg = sizeTotal / count;
} else {
s = markerSizeFunc(optsIn.size);
for(i = 0; i < count; i++) {
sizes[i] = s;
}
}
// See https://github.com/plotly/plotly.js/pull/1781#discussion_r121820798
if(multiLineWidth) {
for(i = 0; i < count; i++) {
borderSizes[i] = optsIn.line.width[i];
}
} else {
s = optsIn.line.width;
for(i = 0; i < count; i++) {
borderSizes[i] = s;
}
}
optsOut.sizeAvg = sizeAvg;
} else {
optsOut.size = markerSizeFunc(optsIn && optsIn.size || 10);
optsOut.borderSizes = markerSizeFunc(optsIn.line.width);
}
return optsOut;
}
function convertMarkerSelection(gd, trace, target) {
var optsIn = trace.marker;
var optsOut = {};
if(!target) return optsOut;
if(target.marker && target.marker.symbol) {
optsOut = convertMarkerStyle(gd, Lib.extendFlat({}, optsIn, target.marker));
} else if(target.marker) {
if(target.marker.size) optsOut.size = target.marker.size;
if(target.marker.color) optsOut.colors = target.marker.color;
if(target.marker.opacity !== undefined) optsOut.opacity = target.marker.opacity;
}
return optsOut;
}
function convertTextSelection(gd, trace, target) {
var optsOut = {};
if(!target) return optsOut;
if(target.textfont) {
var optsIn = {
opacity: 1,
text: trace.text,
texttemplate: trace.texttemplate,
textposition: trace.textposition,
textfont: Lib.extendFlat({}, trace.textfont)
};
if(target.textfont) {
Lib.extendFlat(optsIn.textfont, target.textfont);
}
optsOut = convertTextStyle(gd, optsIn);
}
return optsOut;
}
function convertErrorBarStyle(trace, target, plotGlPixelRatio) {
var optsOut = {
capSize: target.width * 2 * plotGlPixelRatio,
lineWidth: target.thickness * plotGlPixelRatio,
color: target.color
};
if(target.copy_ystyle) {
optsOut = trace.error_y;
}
return optsOut;
}
var SYMBOL_SDF_SIZE = constants.SYMBOL_SDF_SIZE;
var SYMBOL_SIZE = constants.SYMBOL_SIZE;
var SYMBOL_STROKE = constants.SYMBOL_STROKE;
var SYMBOL_SDF = {};
var SYMBOL_SVG_CIRCLE = Drawing.symbolFuncs[0](SYMBOL_SIZE * 0.05);
function getSymbolSdf(d, trace) {
var symbol = d.mx;
if(symbol === 'circle') return null;
var symbolPath, symbolSdf;
var symbolNumber = Drawing.symbolNumber(symbol);
var symbolFunc = Drawing.symbolFuncs[symbolNumber % 100];
var symbolNoDot = !!Drawing.symbolNoDot[symbolNumber % 100];
var symbolNoFill = !!Drawing.symbolNoFill[symbolNumber % 100];
var isDot = helpers.isDotSymbol(symbol);
// until we may handle angles in shader?
if(d.ma) symbol += '_' + d.ma;
// get symbol sdf from cache or generate it
if(SYMBOL_SDF[symbol]) return SYMBOL_SDF[symbol];
var angle = Drawing.getMarkerAngle(d, trace);
if(isDot && !symbolNoDot) {
symbolPath = symbolFunc(SYMBOL_SIZE * 1.1, angle) + SYMBOL_SVG_CIRCLE;
} else {
symbolPath = symbolFunc(SYMBOL_SIZE, angle);
}
symbolSdf = svgSdf(symbolPath, {
w: SYMBOL_SDF_SIZE,
h: SYMBOL_SDF_SIZE,
viewBox: [-SYMBOL_SIZE, -SYMBOL_SIZE, SYMBOL_SIZE, SYMBOL_SIZE],
stroke: symbolNoFill ? SYMBOL_STROKE : -SYMBOL_STROKE
});
SYMBOL_SDF[symbol] = symbolSdf;
return symbolSdf || null;
}
function convertLinePositions(gd, trace, positions) {
var len = positions.length;
var count = len / 2;
var linePositions;
var i;
if(subTypes.hasLines(trace) && count) {
if(trace.line.shape === 'hv') {
linePositions = [];
for(i = 0; i < count - 1; i++) {
if(isNaN(positions[i * 2]) || isNaN(positions[i * 2 + 1])) {
linePositions.push(NaN, NaN, NaN, NaN);
} else {
linePositions.push(positions[i * 2], positions[i * 2 + 1]);
if(!isNaN(positions[i * 2 + 2]) && !isNaN(positions[i * 2 + 3])) {
linePositions.push(positions[i * 2 + 2], positions[i * 2 + 1]);
} else {
linePositions.push(NaN, NaN);
}
}
}
linePositions.push(positions[len - 2], positions[len - 1]);
} else if(trace.line.shape === 'hvh') {
linePositions = [];
for(i = 0; i < count - 1; i++) {
if(isNaN(positions[i * 2]) || isNaN(positions[i * 2 + 1]) || isNaN(positions[i * 2 + 2]) || isNaN(positions[i * 2 + 3])) {
if(!isNaN(positions[i * 2]) && !isNaN(positions[i * 2 + 1])) {
linePositions.push(positions[i * 2], positions[i * 2 + 1]);
} else {
linePositions.push(NaN, NaN);
}
linePositions.push(NaN, NaN);
} else {
var midPtX = (positions[i * 2] + positions[i * 2 + 2]) / 2;
linePositions.push(
positions[i * 2],
positions[i * 2 + 1],
midPtX,
positions[i * 2 + 1],
midPtX,
positions[i * 2 + 3]
);
}
}
linePositions.push(positions[len - 2], positions[len - 1]);
} else if(trace.line.shape === 'vhv') {
linePositions = [];
for(i = 0; i < count - 1; i++) {
if(isNaN(positions[i * 2]) || isNaN(positions[i * 2 + 1]) || isNaN(positions[i * 2 + 2]) || isNaN(positions[i * 2 + 3])) {
if(!isNaN(positions[i * 2]) && !isNaN(positions[i * 2 + 1])) {
linePositions.push(positions[i * 2], positions[i * 2 + 1]);
} else {
linePositions.push(NaN, NaN);
}
linePositions.push(NaN, NaN);
} else {
var midPtY = (positions[i * 2 + 1] + positions[i * 2 + 3]) / 2;
linePositions.push(
positions[i * 2],
positions[i * 2 + 1],
positions[i * 2],
midPtY,
positions[i * 2 + 2],
midPtY
);
}
}
linePositions.push(positions[len - 2], positions[len - 1]);
} else if(trace.line.shape === 'vh') {
linePositions = [];
for(i = 0; i < count - 1; i++) {
if(isNaN(positions[i * 2]) || isNaN(positions[i * 2 + 1])) {
linePositions.push(NaN, NaN, NaN, NaN);
} else {
linePositions.push(positions[i * 2], positions[i * 2 + 1]);
if(!isNaN(positions[i * 2 + 2]) && !isNaN(positions[i * 2 + 3])) {
linePositions.push(positions[i * 2], positions[i * 2 + 3]);
} else {
linePositions.push(NaN, NaN);
}
}
}
linePositions.push(positions[len - 2], positions[len - 1]);
} else {
linePositions = positions;
}
}
// If we have data with gaps, we ought to use rect joins
// FIXME: get rid of this
var hasNaN = false;
for(i = 0; i < linePositions.length; i++) {
if(isNaN(linePositions[i])) {
hasNaN = true;
break;
}
}
var join = (hasNaN || linePositions.length > constants.TOO_MANY_POINTS) ? 'rect' :
subTypes.hasMarkers(trace) ? 'rect' : 'round';
// fill gaps
if(hasNaN && trace.connectgaps) {
var lastX = linePositions[0];
var lastY = linePositions[1];
for(i = 0; i < linePositions.length; i += 2) {
if(isNaN(linePositions[i]) || isNaN(linePositions[i + 1])) {
linePositions[i] = lastX;
linePositions[i + 1] = lastY;
} else {
lastX = linePositions[i];
lastY = linePositions[i + 1];
}
}
}
return {
join: join,
positions: linePositions
};
}
function convertErrorBarPositions(gd, trace, positions, x, y) {
var makeComputeError = Registry.getComponentMethod('errorbars', 'makeComputeError');
var xa = AxisIDs.getFromId(gd, trace.xaxis, 'x');
var ya = AxisIDs.getFromId(gd, trace.yaxis, 'y');
var count = positions.length / 2;
var out = {};
function convertOneAxis(coords, ax) {
var axLetter = ax._id.charAt(0);
var opts = trace['error_' + axLetter];
if(opts && opts.visible && (ax.type === 'linear' || ax.type === 'log')) {
var computeError = makeComputeError(opts);
var pOffset = {x: 0, y: 1}[axLetter];
var eOffset = {x: [0, 1, 2, 3], y: [2, 3, 0, 1]}[axLetter];
var errors = new Float64Array(4 * count);
var minShoe = Infinity;
var maxHat = -Infinity;
for(var i = 0, j = 0; i < count; i++, j += 4) {
var dc = coords[i];
if(isNumeric(dc)) {
var dl = positions[i * 2 + pOffset];
var vals = computeError(dc, i);
var lv = vals[0];
var hv = vals[1];
if(isNumeric(lv) && isNumeric(hv)) {
var shoe = dc - lv;
var hat = dc + hv;
errors[j + eOffset[0]] = dl - ax.c2l(shoe);
errors[j + eOffset[1]] = ax.c2l(hat) - dl;
errors[j + eOffset[2]] = 0;
errors[j + eOffset[3]] = 0;
minShoe = Math.min(minShoe, dc - lv);
maxHat = Math.max(maxHat, dc + hv);
}
}
}
out[axLetter] = {
positions: positions,
errors: errors,
_bnds: [minShoe, maxHat]
};
}
}
convertOneAxis(x, xa);
convertOneAxis(y, ya);
return out;
}
function convertTextPosition(gd, trace, textOpts, markerOpts) {
var count = trace._length;
var out = {};
var i;
// corresponds to textPointPosition from component.drawing
if(subTypes.hasMarkers(trace)) {
var fontOpts = textOpts.font;
var align = textOpts.align;
var baseline = textOpts.baseline;
out.offset = new Array(count);
for(i = 0; i < count; i++) {
var ms = markerOpts.sizes ? markerOpts.sizes[i] : markerOpts.size;
var fs = isArrayOrTypedArray(fontOpts) ? fontOpts[i].size : fontOpts.size;
var a = isArrayOrTypedArray(align) ?
(align.length > 1 ? align[i] : align[0]) :
align;
var b = isArrayOrTypedArray(baseline) ?
(baseline.length > 1 ? baseline[i] : baseline[0]) :
baseline;
var hSign = TEXTOFFSETSIGN[a];
var vSign = TEXTOFFSETSIGN[b];
var xPad = ms ? ms / 0.8 + 1 : 0;
var yPad = -vSign * xPad - vSign * 0.5;
out.offset[i] = [hSign * xPad / fs, yPad / fs];
}
}
return out;
}
module.exports = {
style: convertStyle,
markerStyle: convertMarkerStyle,
markerSelection: convertMarkerSelection,
linePositions: convertLinePositions,
errorBarPositions: convertErrorBarPositions,
textPosition: convertTextPosition
};