'use strict'; var calc = require('./calc'); var setGroupPositions = require('../bar/cross_trace_calc').setGroupPositions; function groupCrossTraceCalc(gd, plotinfo) { var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; var fullLayout = gd._fullLayout; var fullTraces = gd._fullData; var calcTraces = gd.calcdata; var calcTracesHorz = []; var calcTracesVert = []; for(var i = 0; i < fullTraces.length; i++) { var fullTrace = fullTraces[i]; if( fullTrace.visible === true && fullTrace.type === 'scatter' && fullTrace.xaxis === xa._id && fullTrace.yaxis === ya._id ) { if(fullTrace.orientation === 'h') { calcTracesHorz.push(calcTraces[i]); } else if(fullTrace.orientation === 'v') { // check for v since certain scatter traces may not have an orientation calcTracesVert.push(calcTraces[i]); } } } var opts = { mode: fullLayout.scattermode, gap: fullLayout.scattergap }; setGroupPositions(gd, xa, ya, calcTracesVert, opts); setGroupPositions(gd, ya, xa, calcTracesHorz, opts); } /* * Scatter stacking & normalization calculations * runs per subplot, and can handle multiple stacking groups */ module.exports = function crossTraceCalc(gd, plotinfo) { if(gd._fullLayout.scattermode === 'group') { groupCrossTraceCalc(gd, plotinfo); } var xa = plotinfo.xaxis; var ya = plotinfo.yaxis; var subplot = xa._id + ya._id; var subplotStackOpts = gd._fullLayout._scatterStackOpts[subplot]; if(!subplotStackOpts) return; var calcTraces = gd.calcdata; var i, j, k, i2, cd, cd0, posj, sumj, norm; var groupOpts, interpolate, groupnorm, posAttr, valAttr; var hasAnyBlanks; for(var stackGroup in subplotStackOpts) { groupOpts = subplotStackOpts[stackGroup]; var indices = groupOpts.traceIndices; // can get here with no indices if the stack axis is non-numeric if(!indices.length) continue; interpolate = groupOpts.stackgaps === 'interpolate'; groupnorm = groupOpts.groupnorm; if(groupOpts.orientation === 'v') { posAttr = 'x'; valAttr = 'y'; } else { posAttr = 'y'; valAttr = 'x'; } hasAnyBlanks = new Array(indices.length); for(i = 0; i < hasAnyBlanks.length; i++) { hasAnyBlanks[i] = false; } // Collect the complete set of all positions across ALL traces. // Start with the first trace, then interleave items from later traces // as needed. // Fill in mising items as we go. cd0 = calcTraces[indices[0]]; var allPositions = new Array(cd0.length); for(i = 0; i < cd0.length; i++) { allPositions[i] = cd0[i][posAttr]; } for(i = 1; i < indices.length; i++) { cd = calcTraces[indices[i]]; for(j = k = 0; j < cd.length; j++) { posj = cd[j][posAttr]; for(; posj > allPositions[k] && k < allPositions.length; k++) { // the current trace is missing a position from some previous trace(s) insertBlank(cd, j, allPositions[k], i, hasAnyBlanks, interpolate, posAttr); j++; } if(posj !== allPositions[k]) { // previous trace(s) are missing a position from the current trace for(i2 = 0; i2 < i; i2++) { insertBlank(calcTraces[indices[i2]], k, posj, i2, hasAnyBlanks, interpolate, posAttr); } allPositions.splice(k, 0, posj); } k++; } for(; k < allPositions.length; k++) { insertBlank(cd, j, allPositions[k], i, hasAnyBlanks, interpolate, posAttr); j++; } } var serieslen = allPositions.length; // stack (and normalize)! for(j = 0; j < cd0.length; j++) { sumj = cd0[j][valAttr] = cd0[j].s; for(i = 1; i < indices.length; i++) { cd = calcTraces[indices[i]]; cd[0].trace._rawLength = cd[0].trace._length; cd[0].trace._length = serieslen; sumj += cd[j].s; cd[j][valAttr] = sumj; } if(groupnorm) { norm = ((groupnorm === 'fraction') ? sumj : (sumj / 100)) || 1; for(i = 0; i < indices.length; i++) { var cdj = calcTraces[indices[i]][j]; cdj[valAttr] /= norm; cdj.sNorm = cdj.s / norm; } } } // autorange for(i = 0; i < indices.length; i++) { cd = calcTraces[indices[i]]; var trace = cd[0].trace; var ppad = calc.calcMarkerSize(trace, trace._rawLength); var arrayPad = Array.isArray(ppad); if((ppad && hasAnyBlanks[i]) || arrayPad) { var ppadRaw = ppad; ppad = new Array(serieslen); for(j = 0; j < serieslen; j++) { ppad[j] = cd[j].gap ? 0 : (arrayPad ? ppadRaw[cd[j].i] : ppadRaw); } } var x = new Array(serieslen); var y = new Array(serieslen); for(j = 0; j < serieslen; j++) { x[j] = cd[j].x; y[j] = cd[j].y; } calc.calcAxisExpansion(gd, trace, xa, ya, x, y, ppad); // while we're here (in a loop over all traces in the stack) // record the orientation, so hover can find it easily cd[0].t.orientation = groupOpts.orientation; } } }; function insertBlank(calcTrace, index, position, traceIndex, hasAnyBlanks, interpolate, posAttr) { hasAnyBlanks[traceIndex] = true; var newEntry = { i: null, gap: true, s: 0 }; newEntry[posAttr] = position; calcTrace.splice(index, 0, newEntry); // Even if we're not interpolating, if one trace has multiple // values at the same position and this trace only has one value there, // we just duplicate that one value rather than insert a zero. // We also make it look like a real point - because it's ambiguous which // one really is the real one! if(index && position === calcTrace[index - 1][posAttr]) { var prevEntry = calcTrace[index - 1]; newEntry.s = prevEntry.s; // TODO is it going to cause any problems to have multiple // calcdata points with the same index? newEntry.i = prevEntry.i; newEntry.gap = prevEntry.gap; } else if(interpolate) { newEntry.s = getInterp(calcTrace, index, position, posAttr); } if(!index) { // t and trace need to stay on the first cd entry calcTrace[0].t = calcTrace[1].t; calcTrace[0].trace = calcTrace[1].trace; delete calcTrace[1].t; delete calcTrace[1].trace; } } function getInterp(calcTrace, index, position, posAttr) { var pt0 = calcTrace[index - 1]; var pt1 = calcTrace[index + 1]; if(!pt1) return pt0.s; if(!pt0) return pt1.s; return pt0.s + (pt1.s - pt0.s) * (position - pt0[posAttr]) / (pt1[posAttr] - pt0[posAttr]); }