'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 };