From 1e923c925f4d83ab2353c2bdf5f55ea45f75cf90 Mon Sep 17 00:00:00 2001 From: etpinard Date: Tue, 15 Dec 2015 18:36:13 -0500 Subject: [PATCH 01/28] break up heatmap index: - the supply-defaults, calc, plot, style and hoverPoints steps each get a file of their own. - helpers convertColumnXYZ, hasColumns and maxRowLength are now modules required into module files. --- src/traces/heatmap/calc.js | 431 +++++++++ src/traces/heatmap/convert_column_xyz.js | 57 ++ src/traces/heatmap/defaults.js | 130 +++ src/traces/heatmap/has_columns.js | 14 + src/traces/heatmap/hover.js | 109 +++ src/traces/heatmap/index.js | 1044 +--------------------- src/traces/heatmap/max_row_length.js | 20 + src/traces/heatmap/plot.js | 384 ++++++++ src/traces/heatmap/style.js | 20 + test/jasmine/tests/heatmap_test.js | 6 +- 10 files changed, 1177 insertions(+), 1038 deletions(-) create mode 100644 src/traces/heatmap/calc.js create mode 100644 src/traces/heatmap/convert_column_xyz.js create mode 100644 src/traces/heatmap/defaults.js create mode 100644 src/traces/heatmap/has_columns.js create mode 100644 src/traces/heatmap/hover.js create mode 100644 src/traces/heatmap/max_row_length.js create mode 100644 src/traces/heatmap/plot.js create mode 100644 src/traces/heatmap/style.js diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js new file mode 100644 index 00000000000..6ddebcb4797 --- /dev/null +++ b/src/traces/heatmap/calc.js @@ -0,0 +1,431 @@ +/** +* Copyright 2012-2015, 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 isNumeric = require('fast-isnumeric'); + +var Plotly = require('../../plotly'); +var Lib = require('../../lib'); + +var hasColumns = require('./has_columns'); +var convertColumnXYZ = require('./convert_column_xyz'); +var maxRowLength = require('./max_row_length'); + + +module.exports = function calc(gd, trace) { + Lib.markTime('start convert x&y'); + + // prepare the raw data + // run makeCalcdata on x and y even for heatmaps, in case of category mappings + var xa = Plotly.Axes.getFromId(gd, trace.xaxis||'x'), + ya = Plotly.Axes.getFromId(gd, trace.yaxis||'y'), + isContour = Plotly.Plots.traceIs(trace, 'contour'), + isHist = Plotly.Plots.traceIs(trace, 'histogram'), + zsmooth = isContour ? 'best' : trace.zsmooth, + x, + x0, + dx, + y, + y0, + dy, + z, + i; + + // cancel minimum tick spacings (only applies to bars and boxes) + xa._minDtick = 0; + ya._minDtick = 0; + + Lib.markTime('done convert x&y'); + + if(isHist) { + var binned = Plotly.Histogram.calc2d(gd, trace); + x = binned.x; + x0 = binned.x0; + dx = binned.dx; + y = binned.y; + y0 = binned.y0; + dy = binned.dy; + z = binned.z; + } + else { + if(hasColumns(trace)) convertColumnXYZ(trace, xa, ya); + + x = trace.x ? xa.makeCalcdata(trace, 'x') : []; + y = trace.y ? ya.makeCalcdata(trace, 'y') : []; + x0 = trace.x0 || 0; + dx = trace.dx || 1; + y0 = trace.y0 || 0; + dy = trace.dy || 1; + + z = cleanZ(trace); + + if(isContour || trace.connectgaps) { + trace._emptypoints = findEmpties(z); + trace._interpz = interp2d(z, trace._emptypoints, trace._interpz); + } + } + + function noZsmooth(msg) { + zsmooth = trace._input.zsmooth = trace.zsmooth = false; + Lib.notifier('cannot fast-zsmooth: ' + msg); + } + + // check whether we really can smooth (ie all boxes are about the same size) + if(zsmooth === 'fast') { + if(xa.type==='log' || ya.type==='log') { + noZsmooth('log axis found'); + } + else if(!isHist) { + if(x.length) { + var avgdx = (x[x.length-1]-x[0]) / (x.length-1), + maxErrX = Math.abs(avgdx/100); + for(i=0; imaxErrX) { + noZsmooth('x scale is not linear'); + break; + } + } + } + if(y.length && zsmooth === 'fast') { + var avgdy = (y[y.length-1]-y[0])/(y.length-1), + maxErrY = Math.abs(avgdy/100); + for(i=0; imaxErrY) { + noZsmooth('y scale is not linear'); + break; + } + } + } + } + } + + // create arrays of brick boundaries, to be used by autorange and heatmap.plot + var xlen = maxRowLength(z), + xIn = trace.xtype==='scaled' ? '' : trace.x, + xArray = makeBoundArray(trace, xIn, x0, dx, xlen, xa), + yIn = trace.ytype==='scaled' ? '' : trace.y, + yArray = makeBoundArray(trace, yIn, y0, dy, z.length, ya); + + Plotly.Axes.expand(xa, xArray); + Plotly.Axes.expand(ya, yArray); + + var cd0 = {x: xArray, y: yArray, z: z}; + + // auto-z and autocolorscale if applicable + Plotly.Colorscale.calc(trace, z, '', 'z'); + + if(isContour && trace.contours && trace.contours.coloring==='heatmap') { + var hmType = trace.type === 'contour' ? 'heatmap' : 'histogram2d'; + cd0.xfill = makeBoundArray(hmType, xIn, x0, dx, xlen, xa); + cd0.yfill = makeBoundArray(hmType, yIn, y0, dy, z.length, ya); + } + + return [cd0]; +}; + + +function cleanZ(trace) { + var zOld = trace.z; + + var rowlen, collen, getCollen, old2new, i, j; + + function cleanZvalue(v) { + if(!isNumeric(v)) return undefined; + return +v; + } + + if(trace.transpose) { + rowlen = 0; + for(i = 0; i < zOld.length; i++) rowlen = Math.max(rowlen, zOld[i].length); + if(rowlen === 0) return false; + getCollen = function(zOld) { return zOld.length; }; + old2new = function(zOld, i, j) { return zOld[j][i]; }; + } + else { + rowlen = zOld.length; + getCollen = function(zOld, i) { return zOld[i].length; }; + old2new = function(zOld, i, j) { return zOld[i][j]; }; + } + + var zNew = new Array(rowlen); + + for(i = 0; i < rowlen; i++) { + collen = getCollen(zOld, i); + zNew[i] = new Array(collen); + for(j = 0; j < collen; j++) zNew[i][j] = cleanZvalue(old2new(zOld, i, j)); + } + + return zNew; +} + +function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, ax) { + var arrayOut = [], + isContour = Plotly.Plots.traceIs(trace, 'contour'), + isHist = Plotly.Plots.traceIs(trace, 'histogram'), + v0, + dv, + i; + if(Array.isArray(arrayIn) && !isHist && (ax.type!=='category')) { + arrayIn = arrayIn.map(ax.d2c); + var len = arrayIn.length; + + // given vals are brick centers + // hopefully length==numbricks, but use this method even if too few are supplied + // and extend it linearly based on the last two points + if(len <= numbricks) { + // contour plots only want the centers + if(isContour) arrayOut = arrayIn.slice(0, numbricks); + else if(numbricks === 1) arrayOut = [arrayIn[0]-0.5,arrayIn[0]+0.5]; + else { + arrayOut = [1.5*arrayIn[0]-0.5*arrayIn[1]]; + for(i=1; i INTERPTHRESHOLD; i++) { + maxFractionalChange = iterateInterp2d(z, emptyPoints, + correctionOvershoot(maxFractionalChange)); + } + if(maxFractionalChange > INTERPTHRESHOLD) { + console.log('interp2d didn\'t converge quickly', maxFractionalChange); + } + + return z; +} + +function findEmpties(z) { + // return a list of empty points in 2D array z + // each empty point z[i][j] gives an array [i, j, neighborCount] + // neighborCount is the count of 4 nearest neighbors that DO exist + // this is to give us an order of points to evaluate for interpolation. + // if no neighbors exist, we iteratively look for neighbors that HAVE + // neighbors, and add a fractional neighborCount + var empties = [], + neighborHash = {}, + noNeighborList = [], + nextRow = z[0], + row = [], + blank = [0, 0, 0], + rowLength = maxRowLength(z), + prevRow, + i, + j, + thisPt, + p, + neighborCount, + newNeighborHash, + foundNewNeighbors; + + for(i = 0; i < z.length; i++) { + prevRow = row; + row = nextRow; + nextRow = z[i + 1] || []; + for(j = 0; j < rowLength; j++) { + if(row[j]===undefined) { + neighborCount = (row[j - 1] !== undefined ? 1 : 0) + + (row[j + 1] !== undefined ? 1 : 0) + + (prevRow[j] !== undefined ? 1 : 0) + + (nextRow[j] !== undefined ? 1 : 0); + + if(neighborCount) { + // for this purpose, don't count off-the-edge points + // as undefined neighbors + if(i === 0) neighborCount++; + if(j === 0) neighborCount++; + if(i === z.length - 1) neighborCount++; + if(j === row.length - 1) neighborCount++; + + // if all neighbors that could exist do, we don't + // need this for finding farther neighbors + if(neighborCount < 4) { + neighborHash[[i,j]] = [i, j, neighborCount]; + } + + empties.push([i, j, neighborCount]); + } + else noNeighborList.push([i, j]); + } + } + } + + while(noNeighborList.length) { + newNeighborHash = {}; + foundNewNeighbors = false; + + // look for cells that now have neighbors but didn't before + for(p = noNeighborList.length - 1; p >= 0; p--) { + thisPt = noNeighborList[p]; + i = thisPt[0]; + j = thisPt[1]; + + neighborCount = ((neighborHash[[i - 1, j]] || blank)[2] + + (neighborHash[[i + 1, j]] || blank)[2] + + (neighborHash[[i, j - 1]] || blank)[2] + + (neighborHash[[i, j + 1]] || blank)[2])/20; + + if(neighborCount) { + newNeighborHash[thisPt] = [i, j, neighborCount]; + noNeighborList.splice(p, 1); + foundNewNeighbors = true; + } + } + + if(!foundNewNeighbors) { + throw 'findEmpties iterated with no new neighbors'; + } + + // put these new cells into the main neighbor list + for(thisPt in newNeighborHash) { + neighborHash[thisPt] = newNeighborHash[thisPt]; + empties.push(newNeighborHash[thisPt]); + } + } + + // sort the full list in descending order of neighbor count + return empties.sort(function(a, b) { return b[2] - a[2]; }); +} + +function iterateInterp2d(z, emptyPoints, overshoot) { + var maxFractionalChange = 0, + thisPt, + i, + j, + p, + q, + neighborShift, + neighborRow, + neighborVal, + neighborCount, + neighborSum, + initialVal, + minNeighbor, + maxNeighbor; + + for(p = 0; p < emptyPoints.length; p++) { + thisPt = emptyPoints[p]; + i = thisPt[0]; + j = thisPt[1]; + initialVal = z[i][j]; + neighborSum = 0; + neighborCount = 0; + + for (q = 0; q < 4; q++) { + neighborShift = NEIGHBORSHIFTS[q]; + neighborRow = z[i + neighborShift[0]]; + if(!neighborRow) continue; + neighborVal = neighborRow[j + neighborShift[1]]; + if(neighborVal !== undefined) { + if(neighborSum === 0) { + minNeighbor = maxNeighbor = neighborVal; + } + else { + minNeighbor = Math.min(minNeighbor, neighborVal); + maxNeighbor = Math.max(maxNeighbor, neighborVal); + } + neighborCount++; + neighborSum += neighborVal; + } + } + + if(neighborCount === 0) { + throw 'iterateInterp2d order is wrong: no defined neighbors'; + } + + // this is the laplace equation interpolation: + // each point is just the average of its neighbors + // note that this ignores differential x/y scaling + // which I think is the right approach, since we + // don't know what that scaling means + z[i][j] = neighborSum / neighborCount; + + if(initialVal === undefined) { + if(neighborCount < 4) maxFractionalChange = 1; + } + else { + // we can make large empty regions converge faster + // if we overshoot the change vs the previous value + z[i][j] = (1 + overshoot) * z[i][j] - overshoot * initialVal; + + if(maxNeighbor > minNeighbor) { + maxFractionalChange = Math.max(maxFractionalChange, + Math.abs(z[i][j] - initialVal) / (maxNeighbor - minNeighbor)); + } + } + } + + return maxFractionalChange; +} diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js new file mode 100644 index 00000000000..0b884d399a0 --- /dev/null +++ b/src/traces/heatmap/convert_column_xyz.js @@ -0,0 +1,57 @@ +/** +* Copyright 2012-2015, 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 Lib = require('../../lib'); + + +module.exports = function convertColumnXYZ(trace, xa, ya) { + var xCol = trace.x.slice(), + yCol = trace.y.slice(), + zCol = trace.z, + textCol = trace.text, + colLen = Math.min(xCol.length, yCol.length, zCol.length), + hasColumnText = (textCol!==undefined && !Array.isArray(textCol[0])); + + var i; + + if(colLen < xCol.length) xCol = xCol.slice(0, colLen); + if(colLen < yCol.length) yCol = yCol.slice(0, colLen); + + for(i = 0; i < colLen; i++) { + xCol[i] = xa.d2c(xCol[i]); + yCol[i] = ya.d2c(yCol[i]); + } + + var xColdv = Lib.distinctVals(xCol), + x = xColdv.vals, + yColdv = Lib.distinctVals(yCol), + y = yColdv.vals, + z = Lib.init2dArray(y.length, x.length); + + var ix, iy, text; + + if(hasColumnText) text = Lib.init2dArray(y.length, x.length); + + for(i = 0; i < colLen; i++) { + ix = Lib.findBin(xCol[i] + xColdv.minDiff / 2, x); + iy = Lib.findBin(yCol[i] + yColdv.minDiff / 2, y); + + z[iy][ix] = zCol[i]; + if(hasColumnText) text[iy][ix] = textCol[i]; + } + + trace.x = x; + trace.y = y; + trace.z = z; + if(hasColumnText) trace.text = text; +}; + + diff --git a/src/traces/heatmap/defaults.js b/src/traces/heatmap/defaults.js new file mode 100644 index 00000000000..b2129035589 --- /dev/null +++ b/src/traces/heatmap/defaults.js @@ -0,0 +1,130 @@ +/** +* Copyright 2012-2015, 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 isNumeric = require('fast-isnumeric'); + +var Plotly = require('../../plotly'); +var Plots = require('../../plots/plots'); +var Lib = require('../../lib'); +var Colorscale = require('../../components/colorscale'); + +var attributes = require('./attributes'); +var hasColumns = require('./has_columns'); + + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + var isContour = Plots.traceIs(traceOut, 'contour'); + + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + if(!isContour) coerce('zsmooth'); + + if(Plots.traceIs(traceOut, 'histogram')) { + // x, y, z, marker.color, and x0, dx, y0, dy are coerced + // in Histogram.supplyDefaults + // (along with histogram-specific attributes) + Plotly.Histogram.supplyDefaults(traceIn, traceOut); + if(traceOut.visible === false) return; + } + else { + var len = handleXYZDefaults(traceIn, traceOut, coerce); + if(!len) { + traceOut.visible = false; + return; + } + + coerce('text'); + + var _hasColumns = hasColumns(traceOut); + + if(!_hasColumns) coerce('transpose'); + coerce('connectgaps', _hasColumns && + (isContour || traceOut.zsmooth !== false)); + } + + if(!isContour || (traceOut.contours || {}).coloring!=='none') { + Colorscale.handleDefaults( + traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} + ); + } +}; + +function handleXYZDefaults(traceIn, traceOut, coerce) { + var z = coerce('z'); + var x, y; + + if(z===undefined || !z.length) return 0; + + if(hasColumns(traceIn)) { + x = coerce('x'); + y = coerce('y'); + + // column z must be accompanied by 'x' and 'y' arrays + if(!x || !y) return 0; + } + else { + x = coordDefaults('x', coerce); + y = coordDefaults('y', coerce); + + // TODO put z validation elsewhere + if(!isValidZ(z)) return 0; + } + + return traceOut.z.length; +} + +function coordDefaults(coordStr, coerce) { + var coord = coerce(coordStr), + coordType = coord ? + coerce(coordStr + 'type', 'array') : + 'scaled'; + + if(coordType === 'scaled') { + coerce(coordStr + '0'); + coerce('d' + coordStr); + } + + return coord; +} + +function isValidZ(z) { + var allRowsAreArrays = true, + oneRowIsFilled = false, + hasOneNumber = false, + zi; + + /* + * Without this step: + * + * hasOneNumber = false breaks contour but not heatmap + * allRowsAreArrays = false breaks contour but not heatmap + * oneRowIsFilled = false breaks both + */ + + for(var i = 0; i < z.length; i++) { + zi = z[i]; + if(!Array.isArray(zi)) { + allRowsAreArrays = false; + break; + } + if(zi.length > 0) oneRowIsFilled = true; + for(var j = 0; j < zi.length; j++) { + if(isNumeric(zi[j])) { + hasOneNumber = true; + break; + } + } + } + + return (allRowsAreArrays && oneRowIsFilled && hasOneNumber); +} diff --git a/src/traces/heatmap/has_columns.js b/src/traces/heatmap/has_columns.js new file mode 100644 index 00000000000..48e49d6c3d1 --- /dev/null +++ b/src/traces/heatmap/has_columns.js @@ -0,0 +1,14 @@ +/** +* Copyright 2012-2015, 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'; + +module.exports = function(trace) { + return !Array.isArray(trace.z[0]); +}; diff --git a/src/traces/heatmap/hover.js b/src/traces/heatmap/hover.js new file mode 100644 index 00000000000..19eea0a4a98 --- /dev/null +++ b/src/traces/heatmap/hover.js @@ -0,0 +1,109 @@ +/** +* Copyright 2012-2015, 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 Plotly = require('../../plotly'); +var Lib = require('../../lib'); + + +module.exports = function hoverPoints(pointData, xval, yval, hovermode, contour) { + // never let a heatmap override another type as closest point + if(pointData.distance < Plotly.Fx.MAXDIST) return; + + var cd0 = pointData.cd[0], + trace = cd0.trace, + xa = pointData.xa, + ya = pointData.ya, + x = cd0.x, + y = cd0.y, + z = cd0.z, + zmask = cd0.zmask, + x2 = x, + y2 = y, + xl, + yl, + nx, + ny; + if(pointData.index!==false) { + try { + nx = Math.round(pointData.index[1]); + ny = Math.round(pointData.index[0]); + } + catch(e) { + console.log('Error hovering on heatmap, ' + + 'pointNumber must be [row,col], found:', pointData.index); + return; + } + if(nx<0 || nx>=z[0].length || ny<0 || ny>z.length) { + return; + } + } + else if(Plotly.Fx.inbox(xval-x[0], xval-x[x.length-1])>Plotly.Fx.MAXDIST || + Plotly.Fx.inbox(yval-y[0], yval-y[y.length-1])>Plotly.Fx.MAXDIST) { + return; + } + else { + if(contour) { + x2 = [2*x[0]-x[1]]; + for(var i2=1; i2 0) oneRowIsFilled = true; - for(var j = 0; j < zi.length; j++) { - if(isNumeric(zi[j])) { - hasOneNumber = true; - break; - } - } - } - - return (allRowsAreArrays && oneRowIsFilled && hasOneNumber); -} - -heatmap.hasColumns = function(trace) { - return !Array.isArray(trace.z[0]); -}; - -heatmap.convertColumnXYZ = function(trace, xa, ya) { - var xCol = trace.x.slice(), - yCol = trace.y.slice(), - zCol = trace.z, - textCol = trace.text, - colLen = Math.min(xCol.length, yCol.length, zCol.length), - hasColumnText = (textCol!==undefined && !Array.isArray(textCol[0])); - - var i; - - if(colLen < xCol.length) xCol = xCol.slice(0, colLen); - if(colLen < yCol.length) yCol = yCol.slice(0, colLen); - - for(i = 0; i < colLen; i++) { - xCol[i] = xa.d2c(xCol[i]); - yCol[i] = ya.d2c(yCol[i]); - } - - var xColdv = Plotly.Lib.distinctVals(xCol), - x = xColdv.vals, - yColdv = Plotly.Lib.distinctVals(yCol), - y = yColdv.vals, - z = Plotly.Lib.init2dArray(y.length, x.length); - - var ix, iy, text; - - if(hasColumnText) text = Plotly.Lib.init2dArray(y.length, x.length); - - for(i = 0; i < colLen; i++) { - ix = Plotly.Lib.findBin(xCol[i] + xColdv.minDiff / 2, x); - iy = Plotly.Lib.findBin(yCol[i] + yColdv.minDiff / 2, y); - - z[iy][ix] = zCol[i]; - if(hasColumnText) text[iy][ix] = textCol[i]; - } - - trace.x = x; - trace.y = y; - trace.z = z; - if(hasColumnText) trace.text = text; -}; - -heatmap.calc = function(gd, trace) { - // prepare the raw data - // run makeCalcdata on x and y even for heatmaps, in case of category mappings - Plotly.Lib.markTime('start convert x&y'); - var xa = Plotly.Axes.getFromId(gd, trace.xaxis||'x'), - ya = Plotly.Axes.getFromId(gd, trace.yaxis||'y'), - isContour = Plotly.Plots.traceIs(trace, 'contour'), - isHist = Plotly.Plots.traceIs(trace, 'histogram'), - zsmooth = isContour ? 'best' : trace.zsmooth, - x, - x0, - dx, - y, - y0, - dy, - z, - i; - - // cancel minimum tick spacings (only applies to bars and boxes) - xa._minDtick = 0; - ya._minDtick = 0; - - Plotly.Lib.markTime('done convert x&y'); - - if(isHist) { - var binned = Plotly.Histogram.calc2d(gd, trace); - x = binned.x; - x0 = binned.x0; - dx = binned.dx; - y = binned.y; - y0 = binned.y0; - dy = binned.dy; - z = binned.z; - } - else { - if(heatmap.hasColumns(trace)) heatmap.convertColumnXYZ(trace, xa, ya); - - x = trace.x ? xa.makeCalcdata(trace, 'x') : []; - y = trace.y ? ya.makeCalcdata(trace, 'y') : []; - x0 = trace.x0 || 0; - dx = trace.dx || 1; - y0 = trace.y0 || 0; - dy = trace.dy || 1; - - z = heatmap.cleanZ(trace); - - if(isContour || trace.connectgaps) { - trace._emptypoints = findEmpties(z); - trace._interpz = interp2d(z, trace._emptypoints, trace._interpz); - } - } - - function noZsmooth(msg) { - zsmooth = trace._input.zsmooth = trace.zsmooth = false; - Plotly.Lib.notifier('cannot fast-zsmooth: ' + msg); - } - - // check whether we really can smooth (ie all boxes are about the same size) - if(zsmooth === 'fast') { - if(xa.type==='log' || ya.type==='log') { - noZsmooth('log axis found'); - } - else if(!isHist) { - if(x.length) { - var avgdx = (x[x.length-1]-x[0]) / (x.length-1), - maxErrX = Math.abs(avgdx/100); - for(i=0; imaxErrX) { - noZsmooth('x scale is not linear'); - break; - } - } - } - if(y.length && zsmooth === 'fast') { - var avgdy = (y[y.length-1]-y[0])/(y.length-1), - maxErrY = Math.abs(avgdy/100); - for(i=0; imaxErrY) { - noZsmooth('y scale is not linear'); - break; - } - } - } - } - } - - // create arrays of brick boundaries, to be used by autorange and heatmap.plot - var xlen = heatmap.maxRowLength(z), - xIn = trace.xtype==='scaled' ? '' : trace.x, - xArray = makeBoundArray(trace, xIn, x0, dx, xlen, xa), - yIn = trace.ytype==='scaled' ? '' : trace.y, - yArray = makeBoundArray(trace, yIn, y0, dy, z.length, ya); - - Plotly.Axes.expand(xa, xArray); - Plotly.Axes.expand(ya, yArray); - - var cd0 = {x: xArray, y: yArray, z: z}; - - // auto-z and autocolorscale if applicable - Plotly.Colorscale.calc(trace, z, '', 'z'); - - if(isContour && trace.contours && trace.contours.coloring==='heatmap') { - var hmType = trace.type === 'contour' ? 'heatmap' : 'histogram2d'; - cd0.xfill = makeBoundArray(hmType, xIn, x0, dx, xlen, xa); - cd0.yfill = makeBoundArray(hmType, yIn, y0, dy, z.length, ya); - } - - return [cd0]; -}; - -function cleanZvalue(v) { - if(!isNumeric(v)) return undefined; - return +v; -} - -heatmap.cleanZ = function(trace) { - var zOld = trace.z; - - var rowlen, collen, getCollen, old2new, i, j; - - if(trace.transpose) { - rowlen = 0; - for(i = 0; i < zOld.length; i++) rowlen = Math.max(rowlen, zOld[i].length); - if(rowlen === 0) return false; - getCollen = function(zOld) { return zOld.length; }; - old2new = function(zOld, i, j) { return zOld[j][i]; }; - } - else { - rowlen = zOld.length; - getCollen = function(zOld, i) { return zOld[i].length; }; - old2new = function(zOld, i, j) { return zOld[i][j]; }; - } - - var zNew = new Array(rowlen); - - for(i = 0; i < rowlen; i++) { - collen = getCollen(zOld, i); - zNew[i] = new Array(collen); - for(j = 0; j < collen; j++) zNew[i][j] = cleanZvalue(old2new(zOld, i, j)); - } - - return zNew; -}; - -function makeBoundArray(trace, arrayIn, v0In, dvIn, numbricks, ax) { - var arrayOut = [], - isContour = Plotly.Plots.traceIs(trace, 'contour'), - isHist = Plotly.Plots.traceIs(trace, 'histogram'), - v0, - dv, - i; - if(Array.isArray(arrayIn) && !isHist && (ax.type!=='category')) { - arrayIn = arrayIn.map(ax.d2c); - var len = arrayIn.length; - - // given vals are brick centers - // hopefully length==numbricks, but use this method even if too few are supplied - // and extend it linearly based on the last two points - if(len <= numbricks) { - // contour plots only want the centers - if(isContour) arrayOut = arrayIn.slice(0, numbricks); - else if(numbricks === 1) arrayOut = [arrayIn[0]-0.5,arrayIn[0]+0.5]; - else { - arrayOut = [1.5*arrayIn[0]-0.5*arrayIn[1]]; - for(i=1; i INTERPTHRESHOLD; i++) { - maxFractionalChange = iterateInterp2d(z, emptyPoints, - correctionOvershoot(maxFractionalChange)); - } - if(maxFractionalChange > INTERPTHRESHOLD) { - console.log('interp2d didn\'t converge quickly', maxFractionalChange); - } - - return z; -} - -heatmap.maxRowLength = function(z) { - var len = 0; - for(var i = 0; i < z.length; i++) { - len = Math.max(len, z[i].length); - } - return len; -}; - -function findEmpties(z) { - // return a list of empty points in 2D array z - // each empty point z[i][j] gives an array [i, j, neighborCount] - // neighborCount is the count of 4 nearest neighbors that DO exist - // this is to give us an order of points to evaluate for interpolation. - // if no neighbors exist, we iteratively look for neighbors that HAVE - // neighbors, and add a fractional neighborCount - var empties = [], - neighborHash = {}, - noNeighborList = [], - nextRow = z[0], - row = [], - blank = [0, 0, 0], - rowLength = heatmap.maxRowLength(z), - prevRow, - i, - j, - thisPt, - p, - neighborCount, - newNeighborHash, - foundNewNeighbors; - - for(i = 0; i < z.length; i++) { - prevRow = row; - row = nextRow; - nextRow = z[i + 1] || []; - for(j = 0; j < rowLength; j++) { - if(row[j]===undefined) { - neighborCount = (row[j - 1] !== undefined ? 1 : 0) + - (row[j + 1] !== undefined ? 1 : 0) + - (prevRow[j] !== undefined ? 1 : 0) + - (nextRow[j] !== undefined ? 1 : 0); - - if(neighborCount) { - // for this purpose, don't count off-the-edge points - // as undefined neighbors - if(i === 0) neighborCount++; - if(j === 0) neighborCount++; - if(i === z.length - 1) neighborCount++; - if(j === row.length - 1) neighborCount++; - - // if all neighbors that could exist do, we don't - // need this for finding farther neighbors - if(neighborCount < 4) { - neighborHash[[i,j]] = [i, j, neighborCount]; - } - - empties.push([i, j, neighborCount]); - } - else noNeighborList.push([i, j]); - } - } - } - - while(noNeighborList.length) { - newNeighborHash = {}; - foundNewNeighbors = false; - - // look for cells that now have neighbors but didn't before - for(p = noNeighborList.length - 1; p >= 0; p--) { - thisPt = noNeighborList[p]; - i = thisPt[0]; - j = thisPt[1]; - - neighborCount = ((neighborHash[[i - 1, j]] || blank)[2] + - (neighborHash[[i + 1, j]] || blank)[2] + - (neighborHash[[i, j - 1]] || blank)[2] + - (neighborHash[[i, j + 1]] || blank)[2])/20; - - if(neighborCount) { - newNeighborHash[thisPt] = [i, j, neighborCount]; - noNeighborList.splice(p, 1); - foundNewNeighbors = true; - } - } - - if(!foundNewNeighbors) { - throw 'findEmpties iterated with no new neighbors'; - } - - // put these new cells into the main neighbor list - for(thisPt in newNeighborHash) { - neighborHash[thisPt] = newNeighborHash[thisPt]; - empties.push(newNeighborHash[thisPt]); - } - } - - // sort the full list in descending order of neighbor count - return empties.sort(function(a, b) { return b[2] - a[2]; }); -} - -function iterateInterp2d(z, emptyPoints, overshoot) { - var maxFractionalChange = 0, - thisPt, - i, - j, - p, - q, - neighborShift, - neighborRow, - neighborVal, - neighborCount, - neighborSum, - initialVal, - minNeighbor, - maxNeighbor; - - for(p = 0; p < emptyPoints.length; p++) { - thisPt = emptyPoints[p]; - i = thisPt[0]; - j = thisPt[1]; - initialVal = z[i][j]; - neighborSum = 0; - neighborCount = 0; - - for (q = 0; q < 4; q++) { - neighborShift = NEIGHBORSHIFTS[q]; - neighborRow = z[i + neighborShift[0]]; - if(!neighborRow) continue; - neighborVal = neighborRow[j + neighborShift[1]]; - if(neighborVal !== undefined) { - if(neighborSum === 0) { - minNeighbor = maxNeighbor = neighborVal; - } - else { - minNeighbor = Math.min(minNeighbor, neighborVal); - maxNeighbor = Math.max(maxNeighbor, neighborVal); - } - neighborCount++; - neighborSum += neighborVal; - } - } - - if(neighborCount === 0) { - throw 'iterateInterp2d order is wrong: no defined neighbors'; - } - - // this is the laplace equation interpolation: - // each point is just the average of its neighbors - // note that this ignores differential x/y scaling - // which I think is the right approach, since we - // don't know what that scaling means - z[i][j] = neighborSum / neighborCount; - - if(initialVal === undefined) { - if(neighborCount < 4) maxFractionalChange = 1; - } - else { - // we can make large empty regions converge faster - // if we overshoot the change vs the previous value - z[i][j] = (1 + overshoot) * z[i][j] - overshoot * initialVal; - - if(maxNeighbor > minNeighbor) { - maxFractionalChange = Math.max(maxFractionalChange, - Math.abs(z[i][j] - initialVal) / (maxNeighbor - minNeighbor)); - } - } - } - - return maxFractionalChange; -} - -// From http://www.xarg.org/2010/03/generate-client-side-png-files-using-javascript/ -heatmap.plot = function(gd, plotinfo, cdheatmaps) { - cdheatmaps.forEach(function(cd) { plotOne(gd, plotinfo, cd); }); -}; - -function plotOne(gd, plotinfo, cd) { - Plotly.Lib.markTime('in Heatmap.plot'); - var trace = cd[0].trace, - uid = trace.uid, - xa = plotinfo.x(), - ya = plotinfo.y(), - fullLayout = gd._fullLayout, - id = 'hm' + uid, - cbId = 'cb' + uid; - - fullLayout._paper.selectAll('.contour' + uid).remove(); // in case this used to be a contour map - - if(trace.visible !== true) { - fullLayout._paper.selectAll('.' + id).remove(); - fullLayout._paper.selectAll('.' + cbId).remove(); - return; - } - - var z = cd[0].z, - min = trace.zmin, - max = trace.zmax, - scl = Plotly.Colorscale.getScale(trace.colorscale), - x = cd[0].x, - y = cd[0].y, - isContour = Plotly.Plots.traceIs(trace, 'contour'), - zsmooth = isContour ? 'best' : trace.zsmooth, - - // get z dims - m = z.length, - n = heatmap.maxRowLength(z), - xrev = false, - left, - right, - temp, - yrev = false, - top, - bottom, - i; - - // TODO: if there are multiple overlapping categorical heatmaps, - // or if we allow category sorting, then the categories may not be - // sequential... may need to reorder and/or expand z - - // Get edges of png in pixels (xa.c2p() maps axes coordinates to pixel coordinates) - // figure out if either axis is reversed (y is usually reversed, in pixel coords) - // also clip the image to maximum 50% outside the visible plot area - // bigger image lets you pan more naturally, but slows performance. - // TODO: use low-resolution images outside the visible plot for panning - // these while loops find the first and last brick bounds that are defined - // (in case of log of a negative) - i = 0; - while(left === undefined && i < x.length - 1) { - left = xa.c2p(x[i]); - i++; - } - i = x.length - 1; - while(right === undefined && i > 0) { - right = xa.c2p(x[i]); - i--; - } - - if(right < left) { - temp = right; - right = left; - left = temp; - xrev = true; - } - - i = 0; - while(top === undefined && i < y.length - 1) { - top = ya.c2p(y[i]); - i++; - } - i = y.length - 1; - while(bottom === undefined && i > 0) { - bottom = ya.c2p(y[i]); - i--; - } - - if(bottom < top) { - temp = top; - top = bottom; - bottom = temp; - yrev = true; - } - - // for contours with heatmap fill, we generate the boundaries based on - // brick centers but then use the brick edges for drawing the bricks - if(isContour) { - // TODO: for 'best' smoothing, we really should use the given brick - // centers as well as brick bounds in calculating values, in case of - // nonuniform brick sizes - x = cd[0].xfill; - y = cd[0].yfill; - } - - // make an image that goes at most half a screen off either side, to keep - // time reasonable when you zoom in. if zsmooth is true/fast, don't worry - // about this, because zooming doesn't increase number of pixels - // if zsmooth is best, don't include anything off screen because it takes too long - if(zsmooth !== 'fast') { - var extra = zsmooth === 'best' ? 0 : 0.5; - left = Math.max(-extra * xa._length, left); - right = Math.min((1 + extra) * xa._length, right); - top = Math.max(-extra * ya._length, top); - bottom = Math.min((1 + extra) * ya._length, bottom); - } - - var imageWidth = Math.round(right - left), - imageHeight = Math.round(bottom - top); - - // now redraw - - // if image is entirely off-screen, don't even draw it - if(imageWidth <= 0 || imageHeight <= 0) return; - - var canvasW, canvasH; - if(zsmooth === 'fast') { - canvasW = n; - canvasH = m; - } else { - canvasW = imageWidth; - canvasH = imageHeight; - } - - var canvas = document.createElement('canvas'); - canvas.width = canvasW; - canvas.height = canvasH; - var context = canvas.getContext('2d'); - - // interpolate for color scale - // use an array instead of color strings, so we preserve alpha - var s = d3.scale.linear() - .domain(scl.map(function(si){ return si[0]; })) - .range(scl.map(function(si){ - var c = tinycolor(si[1]).toRgb(); - return [c.r, c.g, c.b, c.a]; - })) - .clamp(true); - - // map brick boundaries to image pixels - var xpx, - ypx; - if(zsmooth === 'fast') { - xpx = xrev ? - function(index) { return n - 1 - index; } : - Plotly.Lib.identity; - ypx = yrev ? - function(index) { return m - 1 - index; } : - Plotly.Lib.identity; - } - else { - xpx = function(index){ - return Plotly.Lib.constrain(Math.round(xa.c2p(x[index]) - left), - 0, imageWidth); - }; - ypx = function(index){ - return Plotly.Lib.constrain(Math.round(ya.c2p(y[index]) - top), - 0, imageHeight); - }; - } - - // get interpolated bin value. Returns {bin0:closest bin, frac:fractional dist to next, bin1:next bin} - function findInterp(pixel, pixArray) { - var maxbin = pixArray.length - 2, - bin = Plotly.Lib.constrain(Plotly.Lib.findBin(pixel, pixArray), 0, maxbin), - pix0 = pixArray[bin], - pix1 = pixArray[bin + 1], - interp = Plotly.Lib.constrain(bin + (pixel - pix0) / (pix1 - pix0) - 0.5, 0, maxbin), - bin0 = Math.round(interp), - frac = Math.abs(interp - bin0); - - if(!interp || interp === maxbin || !frac) { - return { - bin0: bin0, - bin1: bin0, - frac: 0 - }; - } - return { - bin0: bin0, - frac: frac, - bin1: Math.round(bin0 + frac / (interp - bin0)) - }; - } - - function setColor(v, pixsize) { - if(v !== undefined) { - var c = s((v - min) / (max - min)); - c[0] = Math.round(c[0]); - c[1] = Math.round(c[1]); - c[2] = Math.round(c[2]); - - pixcount += pixsize; - rcount += c[0] * pixsize; - gcount += c[1] * pixsize; - bcount += c[2] * pixsize; - return c; - } - return [0, 0, 0, 0]; - } - - function putColor(pixels, pxIndex, c) { - pixels[pxIndex] = c[0]; - pixels[pxIndex + 1] = c[1]; - pixels[pxIndex + 2] = c[2]; - pixels[pxIndex + 3] = Math.round(c[3] * 255); - } - - function interpColor(r0, r1, xinterp, yinterp) { - var z00 = r0[xinterp.bin0]; - if(z00 === undefined) return setColor(undefined, 1); - - var z01 = r0[xinterp.bin1], - z10 = r1[xinterp.bin0], - z11 = r1[xinterp.bin1], - dx = (z01 - z00) || 0, - dy = (z10 - z00) || 0, - dxy; - - // the bilinear interpolation term needs different calculations - // for all the different permutations of missing data - // among the neighbors of the main point, to ensure - // continuity across brick boundaries. - if(z01 === undefined) { - if(z11 === undefined) dxy = 0; - else if(z10 === undefined) dxy = 2 * (z11 - z00); - else dxy = (2 * z11 - z10 - z00) * 2/3; - } - else if(z11 === undefined) { - if(z10 === undefined) dxy = 0; - else dxy = (2 * z00 - z01 - z10) * 2/3; - } - else if(z10 === undefined) dxy = (2 * z11 - z01 - z00) * 2/3; - else dxy = (z11 + z00 - z01 - z10); - - return setColor(z00 + xinterp.frac * dx + yinterp.frac * (dy + xinterp.frac * dxy)); - } - - Plotly.Lib.markTime('done init png'); - // build the pixel map brick-by-brick - // cruise through z-matrix row-by-row - // build a brick at each z-matrix value - var yi = ypx(0), - yb = [yi, yi], - xbi = xrev ? 0 : 1, - ybi = yrev ? 0 : 1, - // for collecting an average luminosity of the heatmap - pixcount = 0, - rcount = 0, - gcount = 0, - bcount = 0, - xb, - j, - xi, - v, - row, - c; - - if(zsmooth) { // best or fast, works fastest with imageData - var pxIndex = 0, - pixels = new Uint8Array(imageWidth * imageHeight * 4); - - if(zsmooth === 'best') { - var xPixArray = new Array(x.length), - yPixArray = new Array(y.length), - xinterpArray = new Array(imageWidth), - yinterp, - r0, - r1; - - // first make arrays of x and y pixel locations of brick boundaries - for(i = 0; i < x.length; i++) xPixArray[i] = Math.round(xa.c2p(x[i]) - left); - for(i = 0; i < y.length; i++) yPixArray[i] = Math.round(ya.c2p(y[i]) - top); - - // then make arrays of interpolations - // (bin0=closest, bin1=next, frac=fractional dist.) - for(i = 0; i < imageWidth; i++) xinterpArray[i] = findInterp(i, xPixArray); - - // now do the interpolations and fill the png - for(j = 0; j < imageHeight; j++) { - yinterp = findInterp(j, yPixArray); - r0 = z[yinterp.bin0]; - r1 = z[yinterp.bin1]; - for(i = 0; i < imageWidth; i++, pxIndex += 4) { - c = interpColor(r0, r1, xinterpArray[i], yinterp); - putColor(pixels, pxIndex, c); - } - } - } - else { // zsmooth = fast - for(j = 0; j < m; j++) { - row = z[j]; - yb = ypx(j); - for(i = 0; i < n; i++) { - c = setColor(row[i],1); - pxIndex = (yb * imageWidth + xpx(i)) * 4; - putColor(pixels, pxIndex, c); - } - } - } - - var imageData = context.createImageData(imageWidth, imageHeight); - imageData.data.set(pixels); - context.putImageData(imageData, 0, 0); - } else { // zsmooth = false -> filling potentially large bricks works fastest with fillRect - for(j = 0; j < m; j++) { - row = z[j]; - yb.reverse(); - yb[ybi] = ypx(j + 1); - if(yb[0] === yb[1] || yb[0] === undefined || yb[1] === undefined) { - continue; - } - xi = xpx(0); - xb = [xi, xi]; - for(i = 0; i < n; i++) { - // build one color brick! - xb.reverse(); - xb[xbi] = xpx(i + 1); - if(xb[0] === xb[1] || xb[0] === undefined || xb[1] === undefined) { - continue; - } - v = row[i]; - c = setColor(v, (xb[1] - xb[0]) * (yb[1] - yb[0])); - context.fillStyle = 'rgba(' + c.join(',') + ')'; - context.fillRect(xb[0], yb[0], (xb[1] - xb[0]), (yb[1] - yb[0])); - } - } - } - - Plotly.Lib.markTime('done filling png'); - - rcount = Math.round(rcount / pixcount); - gcount = Math.round(gcount/ pixcount); - bcount = Math.round(bcount / pixcount); - var avgColor = tinycolor('rgb(' + rcount + ',' + gcount + ',' + bcount + ')'); - - gd._hmpixcount = (gd._hmpixcount||0) + pixcount; - gd._hmlumcount = (gd._hmlumcount||0) + pixcount * avgColor.getLuminance(); - - // put this right before making the new image, to minimize flicker - fullLayout._paper.selectAll('.'+id).remove(); - plotinfo.plot.select('.maplayer').append('svg:image') - .classed(id, true) - .datum(cd[0]) - .attr({ - xmlns: 'http://www.w3.org/2000/svg', - 'xlink:xlink:href': canvas.toDataURL('image/png'), // odd d3 quirk, need namespace twice - height: imageHeight, - width: imageWidth, - x: left, - y: top, - preserveAspectRatio: 'none' - }); - - Plotly.Lib.markTime('done showing png'); -} - -heatmap.colorbar = Plotly.Colorbar.traceColorbar; - -heatmap.style = function(gd) { - d3.select(gd).selectAll('image').style('opacity',function(d){ return d.trace.opacity; }); -}; +exports.attributes = require('./attributes'); -heatmap.hoverPoints = function(pointData, xval, yval, hovermode, contour) { - // never let a heatmap override another type as closest point - if(pointData.distance=z[0].length || ny<0 || ny>z.length) { - return; - } - } - else if(Plotly.Fx.inbox(xval-x[0], xval-x[x.length-1])>Plotly.Fx.MAXDIST || - Plotly.Fx.inbox(yval-y[0], yval-y[y.length-1])>Plotly.Fx.MAXDIST) { - return; - } - else { - if(contour) { - x2 = [2*x[0]-x[1]]; - for(var i2=1; i2 0) { + right = xa.c2p(x[i]); + i--; + } + + if(right < left) { + temp = right; + right = left; + left = temp; + xrev = true; + } + + i = 0; + while(top === undefined && i < y.length - 1) { + top = ya.c2p(y[i]); + i++; + } + i = y.length - 1; + while(bottom === undefined && i > 0) { + bottom = ya.c2p(y[i]); + i--; + } + + if(bottom < top) { + temp = top; + top = bottom; + bottom = temp; + yrev = true; + } + + // for contours with heatmap fill, we generate the boundaries based on + // brick centers but then use the brick edges for drawing the bricks + if(isContour) { + // TODO: for 'best' smoothing, we really should use the given brick + // centers as well as brick bounds in calculating values, in case of + // nonuniform brick sizes + x = cd[0].xfill; + y = cd[0].yfill; + } + + // make an image that goes at most half a screen off either side, to keep + // time reasonable when you zoom in. if zsmooth is true/fast, don't worry + // about this, because zooming doesn't increase number of pixels + // if zsmooth is best, don't include anything off screen because it takes too long + if(zsmooth !== 'fast') { + var extra = zsmooth === 'best' ? 0 : 0.5; + left = Math.max(-extra * xa._length, left); + right = Math.min((1 + extra) * xa._length, right); + top = Math.max(-extra * ya._length, top); + bottom = Math.min((1 + extra) * ya._length, bottom); + } + + var imageWidth = Math.round(right - left), + imageHeight = Math.round(bottom - top); + + // now redraw + + // if image is entirely off-screen, don't even draw it + if(imageWidth <= 0 || imageHeight <= 0) return; + + var canvasW, canvasH; + if(zsmooth === 'fast') { + canvasW = n; + canvasH = m; + } else { + canvasW = imageWidth; + canvasH = imageHeight; + } + + var canvas = document.createElement('canvas'); + canvas.width = canvasW; + canvas.height = canvasH; + var context = canvas.getContext('2d'); + + // interpolate for color scale + // use an array instead of color strings, so we preserve alpha + var s = d3.scale.linear() + .domain(scl.map(function(si){ return si[0]; })) + .range(scl.map(function(si){ + var c = tinycolor(si[1]).toRgb(); + return [c.r, c.g, c.b, c.a]; + })) + .clamp(true); + + // map brick boundaries to image pixels + var xpx, + ypx; + if(zsmooth === 'fast') { + xpx = xrev ? + function(index) { return n - 1 - index; } : + Lib.identity; + ypx = yrev ? + function(index) { return m - 1 - index; } : + Lib.identity; + } + else { + xpx = function(index){ + return Lib.constrain(Math.round(xa.c2p(x[index]) - left), + 0, imageWidth); + }; + ypx = function(index){ + return Lib.constrain(Math.round(ya.c2p(y[index]) - top), + 0, imageHeight); + }; + } + + // get interpolated bin value. Returns {bin0:closest bin, frac:fractional dist to next, bin1:next bin} + function findInterp(pixel, pixArray) { + var maxbin = pixArray.length - 2, + bin = Lib.constrain(Lib.findBin(pixel, pixArray), 0, maxbin), + pix0 = pixArray[bin], + pix1 = pixArray[bin + 1], + interp = Lib.constrain(bin + (pixel - pix0) / (pix1 - pix0) - 0.5, 0, maxbin), + bin0 = Math.round(interp), + frac = Math.abs(interp - bin0); + + if(!interp || interp === maxbin || !frac) { + return { + bin0: bin0, + bin1: bin0, + frac: 0 + }; + } + return { + bin0: bin0, + frac: frac, + bin1: Math.round(bin0 + frac / (interp - bin0)) + }; + } + + function setColor(v, pixsize) { + if(v !== undefined) { + var c = s((v - min) / (max - min)); + c[0] = Math.round(c[0]); + c[1] = Math.round(c[1]); + c[2] = Math.round(c[2]); + + pixcount += pixsize; + rcount += c[0] * pixsize; + gcount += c[1] * pixsize; + bcount += c[2] * pixsize; + return c; + } + return [0, 0, 0, 0]; + } + + function putColor(pixels, pxIndex, c) { + pixels[pxIndex] = c[0]; + pixels[pxIndex + 1] = c[1]; + pixels[pxIndex + 2] = c[2]; + pixels[pxIndex + 3] = Math.round(c[3] * 255); + } + + function interpColor(r0, r1, xinterp, yinterp) { + var z00 = r0[xinterp.bin0]; + if(z00 === undefined) return setColor(undefined, 1); + + var z01 = r0[xinterp.bin1], + z10 = r1[xinterp.bin0], + z11 = r1[xinterp.bin1], + dx = (z01 - z00) || 0, + dy = (z10 - z00) || 0, + dxy; + + // the bilinear interpolation term needs different calculations + // for all the different permutations of missing data + // among the neighbors of the main point, to ensure + // continuity across brick boundaries. + if(z01 === undefined) { + if(z11 === undefined) dxy = 0; + else if(z10 === undefined) dxy = 2 * (z11 - z00); + else dxy = (2 * z11 - z10 - z00) * 2/3; + } + else if(z11 === undefined) { + if(z10 === undefined) dxy = 0; + else dxy = (2 * z00 - z01 - z10) * 2/3; + } + else if(z10 === undefined) dxy = (2 * z11 - z01 - z00) * 2/3; + else dxy = (z11 + z00 - z01 - z10); + + return setColor(z00 + xinterp.frac * dx + yinterp.frac * (dy + xinterp.frac * dxy)); + } + + Lib.markTime('done init png'); + + // build the pixel map brick-by-brick + // cruise through z-matrix row-by-row + // build a brick at each z-matrix value + var yi = ypx(0), + yb = [yi, yi], + xbi = xrev ? 0 : 1, + ybi = yrev ? 0 : 1, + // for collecting an average luminosity of the heatmap + pixcount = 0, + rcount = 0, + gcount = 0, + bcount = 0, + xb, + j, + xi, + v, + row, + c; + + if(zsmooth) { // best or fast, works fastest with imageData + var pxIndex = 0, + pixels = new Uint8Array(imageWidth * imageHeight * 4); + + if(zsmooth === 'best') { + var xPixArray = new Array(x.length), + yPixArray = new Array(y.length), + xinterpArray = new Array(imageWidth), + yinterp, + r0, + r1; + + // first make arrays of x and y pixel locations of brick boundaries + for(i = 0; i < x.length; i++) xPixArray[i] = Math.round(xa.c2p(x[i]) - left); + for(i = 0; i < y.length; i++) yPixArray[i] = Math.round(ya.c2p(y[i]) - top); + + // then make arrays of interpolations + // (bin0=closest, bin1=next, frac=fractional dist.) + for(i = 0; i < imageWidth; i++) xinterpArray[i] = findInterp(i, xPixArray); + + // now do the interpolations and fill the png + for(j = 0; j < imageHeight; j++) { + yinterp = findInterp(j, yPixArray); + r0 = z[yinterp.bin0]; + r1 = z[yinterp.bin1]; + for(i = 0; i < imageWidth; i++, pxIndex += 4) { + c = interpColor(r0, r1, xinterpArray[i], yinterp); + putColor(pixels, pxIndex, c); + } + } + } + else { // zsmooth = fast + for(j = 0; j < m; j++) { + row = z[j]; + yb = ypx(j); + for(i = 0; i < n; i++) { + c = setColor(row[i],1); + pxIndex = (yb * imageWidth + xpx(i)) * 4; + putColor(pixels, pxIndex, c); + } + } + } + + var imageData = context.createImageData(imageWidth, imageHeight); + imageData.data.set(pixels); + context.putImageData(imageData, 0, 0); + } else { // zsmooth = false -> filling potentially large bricks works fastest with fillRect + for(j = 0; j < m; j++) { + row = z[j]; + yb.reverse(); + yb[ybi] = ypx(j + 1); + if(yb[0] === yb[1] || yb[0] === undefined || yb[1] === undefined) { + continue; + } + xi = xpx(0); + xb = [xi, xi]; + for(i = 0; i < n; i++) { + // build one color brick! + xb.reverse(); + xb[xbi] = xpx(i + 1); + if(xb[0] === xb[1] || xb[0] === undefined || xb[1] === undefined) { + continue; + } + v = row[i]; + c = setColor(v, (xb[1] - xb[0]) * (yb[1] - yb[0])); + context.fillStyle = 'rgba(' + c.join(',') + ')'; + context.fillRect(xb[0], yb[0], (xb[1] - xb[0]), (yb[1] - yb[0])); + } + } + } + + Lib.markTime('done filling png'); + + rcount = Math.round(rcount / pixcount); + gcount = Math.round(gcount/ pixcount); + bcount = Math.round(bcount / pixcount); + var avgColor = tinycolor('rgb(' + rcount + ',' + gcount + ',' + bcount + ')'); + + gd._hmpixcount = (gd._hmpixcount||0) + pixcount; + gd._hmlumcount = (gd._hmlumcount||0) + pixcount * avgColor.getLuminance(); + + // put this right before making the new image, to minimize flicker + fullLayout._paper.selectAll('.'+id).remove(); + plotinfo.plot.select('.maplayer').append('svg:image') + .classed(id, true) + .datum(cd[0]) + .attr({ + xmlns: 'http://www.w3.org/2000/svg', + 'xlink:xlink:href': canvas.toDataURL('image/png'), // odd d3 quirk, need namespace twice + height: imageHeight, + width: imageWidth, + x: left, + y: top, + preserveAspectRatio: 'none' + }); + + Lib.markTime('done showing png'); +} diff --git a/src/traces/heatmap/style.js b/src/traces/heatmap/style.js new file mode 100644 index 00000000000..07ec3645d2a --- /dev/null +++ b/src/traces/heatmap/style.js @@ -0,0 +1,20 @@ +/** +* Copyright 2012-2015, 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 d3 = require('d3'); + +module.exports = function style(gd) { + d3.select(gd).selectAll('image') + .style('opacity', function(d) { + return d.trace.opacity; + }); +}; + diff --git a/test/jasmine/tests/heatmap_test.js b/test/jasmine/tests/heatmap_test.js index d67a235ba98..59946744f1a 100644 --- a/test/jasmine/tests/heatmap_test.js +++ b/test/jasmine/tests/heatmap_test.js @@ -1,4 +1,7 @@ var Plotly = require('@src/plotly'); +var Heatmap = require('@src/traces/heatmap'); +var convertColumnXYZ = require('@src/traces/heatmap/convert_column_xyz'); + describe('Test heatmap', function () { 'use strict'; @@ -12,7 +15,7 @@ describe('Test heatmap', function () { font: Plotly.Plots.layoutAttributes.font }; - var supplyDefaults = Plotly.Heatmap.supplyDefaults; + var supplyDefaults = Heatmap.supplyDefaults; beforeEach(function() { traceOut = {}; @@ -82,7 +85,6 @@ describe('Test heatmap', function () { }); describe('convertColumnXYZ', function() { - var convertColumnXYZ = Plotly.Heatmap.convertColumnXYZ; var trace; function makeMockAxis() { From 6b8885ac228149b0a2bb038769b286c714fefff5 Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 15:32:57 -0500 Subject: [PATCH 02/28] split parts of the scatter index: - colorbar and handleXYDefaults get their own files. --- src/traces/scatter/colorbar.js | 51 +++++++++++++++++++++++ src/traces/scatter/index.js | 68 ++----------------------------- src/traces/scatter/xy_defaults.js | 43 +++++++++++++++++++ 3 files changed, 97 insertions(+), 65 deletions(-) create mode 100644 src/traces/scatter/colorbar.js create mode 100644 src/traces/scatter/xy_defaults.js diff --git a/src/traces/scatter/colorbar.js b/src/traces/scatter/colorbar.js new file mode 100644 index 00000000000..2092907bd2d --- /dev/null +++ b/src/traces/scatter/colorbar.js @@ -0,0 +1,51 @@ +/** +* Copyright 2012-2015, 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 d3 = require('d3'); +var isNumeric = require('fast-isnumeric'); + +var Plotly = require('../../plotly'); +var Lib = require('../../lib'); + + +module.exports = function colorbar(gd, cd) { + var trace = cd[0].trace, + marker = trace.marker, + cbId = 'cb' + trace.uid; + + gd._fullLayout._infolayer.selectAll('.' + cbId).remove(); + + // TODO unify Scatter.colorbar and Heatmap.colorbar + // TODO make Plotly[module].colorbar support multiple colorbar per trace + + if(marker===undefined || !marker.showscale){ + Plotly.Plots.autoMargin(gd, cbId); + return; + } + + var scl = Plotly.Colorscale.getScale(marker.colorscale), + vals = marker.color, + cmin = marker.cmin, + cmax = marker.cmax; + + if(!isNumeric(cmin)) cmin = Plotly.Lib.aggNums(Math.min, null, vals); + if(!isNumeric(cmax)) cmax = Plotly.Lib.aggNums(Math.max, null, vals); + + var cb = cd[0].t.cb = Plotly.Colorbar(gd, cbId); + + cb.fillcolor(d3.scale.linear() + .domain(scl.map(function(v){ return cmin + v[0] * (cmax - cmin); })) + .range(scl.map(function(v){ return v[1]; }))) + .filllevels({start: cmin, end: cmax, size: (cmax - cmin) / 254}) + .options(marker.colorbar)(); + + Lib.markTime('done colorbar'); +}; diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js index 7f3dd0e2497..438dc47b693 100644 --- a/src/traces/scatter/index.js +++ b/src/traces/scatter/index.js @@ -32,44 +32,14 @@ scatter.PTS_LINESONLY = 20; scatter.attributes = require('./attributes'); -scatter.handleXYDefaults = function(traceIn, traceOut, coerce) { - var len, - x = coerce('x'), - y = coerce('y'); - - if(x) { - if(y) { - len = Math.min(x.length, y.length); - // TODO: not sure we should do this here... but I think - // the way it works in calc is wrong, because it'll delete data - // which could be a problem eg in streaming / editing if x and y - // come in at different times - // so we need to revisit calc before taking this out - if(len Date: Wed, 16 Dec 2015 15:34:12 -0500 Subject: [PATCH 03/28] split up bars index --- src/traces/bars/arrays_to_calcdata.js | 26 ++ src/traces/bars/calc.js | 55 +++ src/traces/bars/defaults.js | 64 +++ src/traces/bars/hover.js | 88 +++++ src/traces/bars/index.js | 550 +------------------------- src/traces/bars/layout_defaults.js | 58 +++ src/traces/bars/plot.js | 105 +++++ src/traces/bars/set_positions.js | 201 ++++++++++ src/traces/bars/style.js | 74 ++++ 9 files changed, 683 insertions(+), 538 deletions(-) create mode 100644 src/traces/bars/arrays_to_calcdata.js create mode 100644 src/traces/bars/calc.js create mode 100644 src/traces/bars/defaults.js create mode 100644 src/traces/bars/hover.js create mode 100644 src/traces/bars/layout_defaults.js create mode 100644 src/traces/bars/plot.js create mode 100644 src/traces/bars/set_positions.js create mode 100644 src/traces/bars/style.js diff --git a/src/traces/bars/arrays_to_calcdata.js b/src/traces/bars/arrays_to_calcdata.js new file mode 100644 index 00000000000..6820efb4739 --- /dev/null +++ b/src/traces/bars/arrays_to_calcdata.js @@ -0,0 +1,26 @@ +/** +* Copyright 2012-2015, 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 mergeArray = require('../../lib').mergeArray; + + +// arrayOk attributes, merge them into calcdata array +module.exports = function arraysToCalcdata(cd) { + var trace = cd[0].trace, + marker = trace.marker, + markerLine = marker.line; + + mergeArray(trace.text, cd, 'tx'); + mergeArray(marker.opacity, cd, 'mo'); + mergeArray(marker.color, cd, 'mc'); + mergeArray(markerLine.color, cd, 'mlc'); + mergeArray(markerLine.width, cd, 'mlw'); +}; diff --git a/src/traces/bars/calc.js b/src/traces/bars/calc.js new file mode 100644 index 00000000000..0ebdf4b10df --- /dev/null +++ b/src/traces/bars/calc.js @@ -0,0 +1,55 @@ +/** +* Copyright 2012-2015, 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 isNumeric = require('fast-isnumeric'); + +var Plotly = require('../../plotly'); + + +module.exports = function calc(gd, trace) { + // depending on bar direction, set position and size axes + // and data ranges + // note: this logic for choosing orientation is + // duplicated in graph_obj->setstyles + + var xa = Plotly.Axes.getFromId(gd, trace.xaxis||'x'), + ya = Plotly.Axes.getFromId(gd, trace.yaxis||'y'), + orientation = trace.orientation || ((trace.x && !trace.y) ? 'h' : 'v'), + pos, size, i; + + if(orientation==='h') { + size = xa.makeCalcdata(trace, 'x'); + pos = ya.makeCalcdata(trace, 'y'); + } + else { + size = ya.makeCalcdata(trace, 'y'); + pos = xa.makeCalcdata(trace, 'x'); + } + + // create the "calculated data" to plot + var serieslen = Math.min(pos.length, size.length), + cd = []; + for(i=0; isetstyles - var xa = Plotly.Axes.getFromId(gd, trace.xaxis||'x'), - ya = Plotly.Axes.getFromId(gd, trace.yaxis||'y'), - orientation = trace.orientation || ((trace.x && !trace.y) ? 'h' : 'v'), - pos, size, i; - if(orientation==='h') { - size = xa.makeCalcdata(trace, 'x'); - pos = ya.makeCalcdata(trace, 'y'); - } - else { - size = ya.makeCalcdata(trace, 'y'); - pos = xa.makeCalcdata(trace, 'x'); - } - - // create the "calculated data" to plot - var serieslen = Math.min(pos.length, size.length), - cd = []; - for(i=0; i sMax + tiny) { - padded = true; - sMax = barEnd; - } - } - } - } - } - - Plotly.Axes.expand(sa, [sMin, sMax], {tozero: true, padded: padded}); - } - else { - // for grouped or overlaid bars, just make sure zero is - // included, along with the tops of each bar, and store - // these bar tops in calcdata - var fs = function(v){ v[sLetter] = v.s; return v.s; }; - - for(i=0; i=2 ? roundWithLine(v) : - // but if it's very thin, expand it so it's - // necessarily visible, even if it might overlap - // its neighbor - (v>vc ? Math.ceil(v) : Math.floor(v)); - } - if(!gd._context.staticPlot) { - // if bars are not fully opaque or they have a line - // around them, round to integer pixels, mainly for - // safari so we prevent overlaps from its expansive - // pixelation. if the bars ARE fully opaque and have - // no line, expand to a full pixel to make sure we - // can see them - var op = Plotly.Color.opacity( - di.mc || trace.marker.color), - fixpx = (op<1 || lw>0.01) ? - roundWithLine : expandToVisible; - x0 = fixpx(x0,x1); - x1 = fixpx(x1,x0); - y0 = fixpx(y0,y1); - y1 = fixpx(y1,y0); - } - d3.select(this).attr('d', - 'M'+x0+','+y0+'V'+y1+'H'+x1+'V'+y0+'Z'); - }); - }); -}; - -bars.style = function(gd) { - var s = d3.select(gd).selectAll('g.trace.bars'), - barcount = s.size(), - fullLayout = gd._fullLayout; - - // trace styling - s.style('opacity',function(d){ return d[0].trace.opacity; }) - - // for gapless (either stacked or neighboring grouped) bars use - // crispEdges to turn off antialiasing so an artificial gap - // isn't introduced. - .each(function(d){ - if((fullLayout.barmode==='stack' && barcount>1) || - (fullLayout.bargap===0 && - fullLayout.bargroupgap===0 && - !d[0].trace.marker.line.width)){ - d3.select(this).attr('shape-rendering','crispEdges'); - } - }); - - // then style the individual bars - s.selectAll('g.points').each(function(d){ - var trace = d[0].trace, - marker = trace.marker, - markerLine = marker.line, - markerIn = (trace._input||{}).marker||{}, - markerScale = Plotly.Drawing.tryColorscale(marker, markerIn, ''), - lineScale = Plotly.Drawing.tryColorscale(marker, markerIn, 'line.'); - - d3.select(this).selectAll('path').each(function(d) { - // allow all marker and marker line colors to be scaled - // by given max and min to colorscales - var fillColor, - lineColor, - lineWidth = (d.mlw+1 || markerLine.width+1) - 1, - p = d3.select(this); - - if('mc' in d) fillColor = d.mcc = markerScale(d.mc); - else if(Array.isArray(marker.color)) fillColor = Plotly.Color.defaultLine; - else fillColor = marker.color; - - p.style('stroke-width', lineWidth + 'px') - .call(Plotly.Color.fill, fillColor); - if(lineWidth) { - if('mlc' in d) lineColor = d.mlcc = lineScale(d.mlc); - // weird case: array wasn't long enough to apply to every point - else if(Array.isArray(markerLine.color)) lineColor = Plotly.Color.defaultLine; - else lineColor = markerLine.color; - - p.call(Plotly.Color.stroke, lineColor); - } - }); - // TODO: text markers on bars, either extra text or just bar values - // d3.select(this).selectAll('text') - // .call(Plotly.Drawing.textPointStyle,d.t||d[0].t); - }); -}; - -bars.hoverPoints = function(pointData, xval, yval, hovermode) { - var cd = pointData.cd, - trace = cd[0].trace, - t = cd[0].t, - xa = pointData.xa, - ya = pointData.ya, - barDelta = (hovermode==='closest') ? - t.barwidth/2 : t.dbar*(1-xa._td._fullLayout.bargap)/2, - barPos; - if(hovermode!=='closest') barPos = function(di) { return di.p; }; - else if(trace.orientation==='h') barPos = function(di) { return di.y; }; - else barPos = function(di) { return di.x; }; - - var dx, dy; - if(trace.orientation==='h') { - dx = function(di){ - // add a gradient so hovering near the end of a - // bar makes it a little closer match - return Plotly.Fx.inbox(di.b-xval, di.x-xval) + (di.x-xval)/(di.x-di.b); - }; - dy = function(di){ - var centerPos = barPos(di) - yval; - return Plotly.Fx.inbox(centerPos - barDelta, centerPos + barDelta); - }; - } - else { - dy = function(di){ - return Plotly.Fx.inbox(di.b-yval, di.y-yval) + (di.y-yval)/(di.y-di.b); - }; - dx = function(di){ - var centerPos = barPos(di) - xval; - return Plotly.Fx.inbox(centerPos - barDelta, centerPos + barDelta); - }; - } +exports.layoutAttributes = require('./layout_attributes'); - var distfn = Plotly.Fx.getDistanceFunction(hovermode, dx, dy); - Plotly.Fx.getClosest(cd, distfn, pointData); +exports.supplyDefaults = require('./defaults'); - // skip the rest (for this trace) if we didn't find a close point - if(pointData.index===false) return; +exports.supplyLayoutDefaults = require('./layout_defaults'); - // the closest data point - var di = cd[pointData.index], - mc = di.mcc || trace.marker.color, - mlc = di.mlcc || trace.marker.line.color, - mlw = di.mlw || trace.marker.line.width; - if(Plotly.Color.opacity(mc)) pointData.color = mc; - else if(Plotly.Color.opacity(mlc) && mlw) pointData.color = mlc; +exports.calc = require('./calc'); - if(trace.orientation==='h') { - pointData.x0 = pointData.x1 = xa.c2p(di.x, true); - pointData.xLabelVal = di.s; +exports.setPositions = require('./set_positions'); - pointData.y0 = ya.c2p(barPos(di) - barDelta, true); - pointData.y1 = ya.c2p(barPos(di) + barDelta, true); - pointData.yLabelVal = di.p; - } - else { - pointData.y0 = pointData.y1 = ya.c2p(di.y,true); - pointData.yLabelVal = di.s; +exports.colorbar = require('../scatter/colorbar'); - pointData.x0 = xa.c2p(barPos(di) - barDelta, true); - pointData.x1 = xa.c2p(barPos(di) + barDelta, true); - pointData.xLabelVal = di.p; - } +exports.arraysToCalcdata = require('./arrays_to_calcdata'); - if(di.tx) pointData.text = di.tx; +exports.plot = require('./plot'); - Plotly.ErrorBars.hoverInfo(di, trace, pointData); +exports.style = require('./style'); - return [pointData]; -}; +exports.hoverPoints = require('./hover'); diff --git a/src/traces/bars/layout_defaults.js b/src/traces/bars/layout_defaults.js new file mode 100644 index 00000000000..d5882b951f2 --- /dev/null +++ b/src/traces/bars/layout_defaults.js @@ -0,0 +1,58 @@ +/** +* Copyright 2012-2015, 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 Plotly = require('../../plotly'); +var Lib = require('../../lib'); + +var layoutAttributes = require('./layout_attributes'); + + +module.exports = function(layoutIn, layoutOut, fullData) { + function coerce(attr, dflt) { + return Lib.coerce(layoutIn, layoutOut, layoutAttributes, attr, dflt); + } + + var hasBars = false, + shouldBeGapless = false, + gappedAnyway = false, + usedSubplots = {}, + i, + trace, + subploti; + + for(i = 0; i < fullData.length; i++) { + trace = fullData[i]; + if(Plotly.Plots.traceIs(trace, 'bar')) hasBars = true; + else continue; + + // if we have at least 2 grouped bar traces on the same subplot, + // we should default to a gap anyway, even if the data is histograms + if(layoutIn.barmode !== 'overlay' && layoutIn.barmode !== 'stack') { + subploti = trace.xaxis + trace.yaxis; + if(usedSubplots[subploti]) gappedAnyway = true; + usedSubplots[subploti] = true; + } + + if(trace.visible && trace.type==='histogram') { + var pa = Plotly.Axes.getFromId({_fullLayout:layoutOut}, + trace[trace.orientation==='v' ? 'xaxis' : 'yaxis']); + if(pa.type!=='category') shouldBeGapless = true; + } + } + + if(!hasBars) return; + + var mode = coerce('barmode'); + if(mode!=='overlay') coerce('barnorm'); + + coerce('bargap', shouldBeGapless && !gappedAnyway ? 0 : 0.2); + coerce('bargroupgap'); +}; diff --git a/src/traces/bars/plot.js b/src/traces/bars/plot.js new file mode 100644 index 00000000000..8314e63cd84 --- /dev/null +++ b/src/traces/bars/plot.js @@ -0,0 +1,105 @@ +/** +* Copyright 2012-2015, 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 d3 = require('d3'); +var isNumeric = require('fast-isnumeric'); + +var Lib = require('../../lib'); +var Color = require('../../components/color'); + +var arraysToCalcdata = require('./arrays_to_calcdata'); + + +module.exports = function(gd, plotinfo, cdbar) { + var xa = plotinfo.x(), + ya = plotinfo.y(), + fullLayout = gd._fullLayout; + + var bartraces = plotinfo.plot.select('.barlayer') + .selectAll('g.trace.bars') + .data(cdbar) + .enter().append('g') + .attr('class','trace bars'); + + bartraces.append('g') + .attr('class','points') + .each(function(d){ + var t = d[0].t, + trace = d[0].trace; + + arraysToCalcdata(d); + + d3.select(this).selectAll('path') + .data(Lib.identity) + .enter().append('path') + .each(function(di){ + // now display the bar + // clipped xf/yf (2nd arg true): non-positive + // log values go off-screen by plotwidth + // so you see them continue if you drag the plot + var x0,x1,y0,y1; + if(trace.orientation==='h') { + y0 = ya.c2p(t.poffset+di.p, true); + y1 = ya.c2p(t.poffset+di.p+t.barwidth, true); + x0 = xa.c2p(di.b, true); + x1 = xa.c2p(di.s+di.b, true); + } + else { + x0 = xa.c2p(t.poffset+di.p, true); + x1 = xa.c2p(t.poffset+di.p+t.barwidth, true); + y1 = ya.c2p(di.s+di.b, true); + y0 = ya.c2p(di.b, true); + } + + if(!isNumeric(x0) || !isNumeric(x1) || + !isNumeric(y0) || !isNumeric(y1) || + x0===x1 || y0===y1) { + d3.select(this).remove(); + return; + } + var lw = (di.mlw+1 || trace.marker.line.width+1 || + (di.trace ? di.trace.marker.line.width : 0)+1)-1, + offset = d3.round((lw/2)%1,2); + function roundWithLine(v) { + // if there are explicit gaps, don't round, + // it can make the gaps look crappy + return (fullLayout.bargap===0 && fullLayout.bargroupgap===0) ? + d3.round(Math.round(v)-offset, 2) : v; + } + function expandToVisible(v,vc) { + // if it's not in danger of disappearing entirely, + // round more precisely + return Math.abs(v-vc)>=2 ? roundWithLine(v) : + // but if it's very thin, expand it so it's + // necessarily visible, even if it might overlap + // its neighbor + (v>vc ? Math.ceil(v) : Math.floor(v)); + } + if(!gd._context.staticPlot) { + // if bars are not fully opaque or they have a line + // around them, round to integer pixels, mainly for + // safari so we prevent overlaps from its expansive + // pixelation. if the bars ARE fully opaque and have + // no line, expand to a full pixel to make sure we + // can see them + var op = Color.opacity(di.mc || trace.marker.color), + fixpx = (op<1 || lw>0.01) ? + roundWithLine : expandToVisible; + x0 = fixpx(x0,x1); + x1 = fixpx(x1,x0); + y0 = fixpx(y0,y1); + y1 = fixpx(y1,y0); + } + d3.select(this).attr('d', + 'M'+x0+','+y0+'V'+y1+'H'+x1+'V'+y0+'Z'); + }); + }); +}; diff --git a/src/traces/bars/set_positions.js b/src/traces/bars/set_positions.js new file mode 100644 index 00000000000..217886dd3ce --- /dev/null +++ b/src/traces/bars/set_positions.js @@ -0,0 +1,201 @@ +/** +* Copyright 2012-2015, 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 isNumeric = require('fast-isnumeric'); + +var Plotly = require('../../plotly'); +var Lib = require('../../lib'); + +/* + * Bar chart stacking/grouping positioning and autoscaling calculations + * for each direction separately calculate the ranges and positions + * note that this handles histograms too + * now doing this one subplot at a time + */ + +module.exports = function setPositions(gd, plotinfo) { + var fullLayout = gd._fullLayout, + xa = plotinfo.x(), + ya = plotinfo.y(), + i, j; + + ['v','h'].forEach(function(dir){ + var bl = [], + pLetter = {v:'x',h:'y'}[dir], + sLetter = {v:'y',h:'x'}[dir], + pa = plotinfo[pLetter](), + sa = plotinfo[sLetter](); + + gd._fullData.forEach(function(trace,i) { + if(trace.visible === true && + Plotly.Plots.traceIs(trace, 'bar') && + trace.orientation === dir && + trace.xaxis === xa._id && + trace.yaxis === ya._id) { + bl.push(i); + } + }); + if(!bl.length) return; + + // bar position offset and width calculation + // bl1 is a list of traces (in calcdata) to look at together + // to find the maximum size bars that won't overlap + // for stacked or grouped bars, this is all vertical or horizontal + // bars for overlaid bars, call this individually on each trace. + function barposition(bl1) { + // find the min. difference between any points + // in any traces in bl1 + var pvals=[]; + bl1.forEach(function(i){ + gd.calcdata[i].forEach(function(v){ pvals.push(v.p); }); + }); + var dv = Lib.distinctVals(pvals), + pv2 = dv.vals, + barDiff = dv.minDiff; + + // check if all the traces have only independent positions + // if so, let them have full width even if mode is group + var overlap = false, + comparelist = []; + if(fullLayout.barmode==='group') { + bl1.forEach(function(i) { + if(overlap) return; + gd.calcdata[i].forEach(function(v) { + if(overlap) return; + comparelist.forEach(function(cp) { + if(Math.abs(v.p-cp) < barDiff) overlap = true; + }); + }); + if(overlap) return; + gd.calcdata[i].forEach(function(v) { + comparelist.push(v.p); + }); + }); + } + + // check forced minimum dtick + Plotly.Axes.minDtick(pa, barDiff, pv2[0], overlap); + + // position axis autorange - always tight fitting + Plotly.Axes.expand(pa, pv2, {vpad: barDiff/2}); + + // bar widths and position offsets + barDiff *= 1-fullLayout.bargap; + if(overlap) barDiff/=bl.length; + + var barCenter; + function setBarCenter(v) { v[pLetter] = v.p + barCenter; } + + for(var i=0; i sMax + tiny) { + padded = true; + sMax = barEnd; + } + } + } + } + } + + Plotly.Axes.expand(sa, [sMin, sMax], {tozero: true, padded: padded}); + } + else { + // for grouped or overlaid bars, just make sure zero is + // included, along with the tops of each bar, and store + // these bar tops in calcdata + var fs = function(v){ v[sLetter] = v.s; return v.s; }; + + for(i=0; i1) || + (fullLayout.bargap===0 && + fullLayout.bargroupgap===0 && + !d[0].trace.marker.line.width)){ + d3.select(this).attr('shape-rendering','crispEdges'); + } + }); + + // then style the individual bars + s.selectAll('g.points').each(function(d){ + var trace = d[0].trace, + marker = trace.marker, + markerLine = marker.line, + markerIn = (trace._input||{}).marker||{}, + markerScale = Plotly.Drawing.tryColorscale(marker, markerIn, ''), + lineScale = Plotly.Drawing.tryColorscale(marker, markerIn, 'line.'); + + d3.select(this).selectAll('path').each(function(d) { + // allow all marker and marker line colors to be scaled + // by given max and min to colorscales + var fillColor, + lineColor, + lineWidth = (d.mlw+1 || markerLine.width+1) - 1, + p = d3.select(this); + + if('mc' in d) fillColor = d.mcc = markerScale(d.mc); + else if(Array.isArray(marker.color)) fillColor = Color.defaultLine; + else fillColor = marker.color; + + p.style('stroke-width', lineWidth + 'px') + .call(Color.fill, fillColor); + if(lineWidth) { + if('mlc' in d) lineColor = d.mlcc = lineScale(d.mlc); + // weird case: array wasn't long enough to apply to every point + else if(Array.isArray(markerLine.color)) lineColor = Color.defaultLine; + else lineColor = markerLine.color; + + p.call(Color.stroke, lineColor); + } + }); + // TODO: text markers on bars, either extra text or just bar values + // d3.select(this).selectAll('text') + // .call(Plotly.Drawing.textPointStyle,d.t||d[0].t); + }); +}; From 503fd93d5b513ac13072114ff4cb1f67f60bfe98 Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 15:35:33 -0500 Subject: [PATCH 04/28] mv traceColorbar from colorbar/ to heatmap/ - similar to scatter colorbar --- src/components/colorbar/index.js | 26 -------------------- src/traces/choropleth/index.js | 2 +- src/traces/heatmap/colorbar.js | 42 ++++++++++++++++++++++++++++++++ src/traces/heatmap/index.js | 2 +- src/traces/mesh3d/index.js | 2 +- src/traces/surface/index.js | 2 +- 6 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 src/traces/heatmap/colorbar.js diff --git a/src/components/colorbar/index.js b/src/components/colorbar/index.js index 91a5b897ca1..63b327768af 100644 --- a/src/components/colorbar/index.js +++ b/src/components/colorbar/index.js @@ -575,29 +575,3 @@ colorbar.supplyDefaults = function(containerIn, containerOut, layout) { Plotly.Lib.coerceFont(coerce, 'titlefont', layout.font); coerce('titleside'); }; - -colorbar.traceColorbar = function(gd, cd) { - var trace = cd[0].trace, - cbId = 'cb' + trace.uid, - scl = Plotly.Colorscale.getScale(trace.colorscale), - zmin = trace.zmin, - zmax = trace.zmax; - - if(!isNumeric(zmin)) zmin = Plotly.Lib.aggNums(Math.min, null, trace.z); - if(!isNumeric(zmax)) zmax = Plotly.Lib.aggNums(Math.max, null, trace.z); - - gd._fullLayout._infolayer.selectAll('.'+cbId).remove(); - if(!trace.showscale){ - Plotly.Plots.autoMargin(gd, cbId); - return; - } - - var cb = cd[0].t.cb = colorbar(gd, cbId); - cb.fillcolor(d3.scale.linear() - .domain(scl.map(function(v){ return zmin + v[0]*(zmax-zmin); })) - .range(scl.map(function(v){ return v[1]; }))) - .filllevels({start: zmin, end: zmax, size: (zmax-zmin)/254}) - .options(trace.colorbar)(); - - Plotly.Lib.markTime('done colorbar'); -}; diff --git a/src/traces/choropleth/index.js b/src/traces/choropleth/index.js index 1adc5857818..c66037d950f 100644 --- a/src/traces/choropleth/index.js +++ b/src/traces/choropleth/index.js @@ -26,7 +26,7 @@ Choropleth.attributes = require('./attributes'); Choropleth.supplyDefaults = require('./defaults'); -Choropleth.colorbar = Plotly.Colorbar.traceColorbar; +Choropleth.colorbar = require('../heatmap/colorbar'); Choropleth.calc = function(gd, trace) { diff --git a/src/traces/heatmap/colorbar.js b/src/traces/heatmap/colorbar.js new file mode 100644 index 00000000000..4fec7470c38 --- /dev/null +++ b/src/traces/heatmap/colorbar.js @@ -0,0 +1,42 @@ +/** +* Copyright 2012-2015, 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 d3 = require('d3'); +var isNumeric = require('fast-isnumeric'); + +var Plotly = require('../../plotly'); +var Lib = require('../../lib'); + +module.exports = function colorbar(gd, cd) { + var trace = cd[0].trace, + cbId = 'cb' + trace.uid, + scl = Plotly.Colorscale.getScale(trace.colorscale), + zmin = trace.zmin, + zmax = trace.zmax; + + if(!isNumeric(zmin)) zmin = Plotly.Lib.aggNums(Math.min, null, trace.z); + if(!isNumeric(zmax)) zmax = Plotly.Lib.aggNums(Math.max, null, trace.z); + + gd._fullLayout._infolayer.selectAll('.'+cbId).remove(); + if(!trace.showscale){ + Plotly.Plots.autoMargin(gd, cbId); + return; + } + + var cb = cd[0].t.cb = Plotly.Colorbar(gd, cbId); + cb.fillcolor(d3.scale.linear() + .domain(scl.map(function(v){ return zmin + v[0]*(zmax-zmin); })) + .range(scl.map(function(v){ return v[1]; }))) + .filllevels({start: zmin, end: zmax, size: (zmax-zmin)/254}) + .options(trace.colorbar)(); + + Lib.markTime('done colorbar'); +}; diff --git a/src/traces/heatmap/index.js b/src/traces/heatmap/index.js index 6a8c17c0b8b..db8f6923665 100644 --- a/src/traces/heatmap/index.js +++ b/src/traces/heatmap/index.js @@ -46,7 +46,7 @@ exports.calc = require('./calc'); exports.plot = require('./plot'); -exports.colorbar = Plotly.Colorbar.traceColorbar; +exports.colorbar = require('./colorbar'); exports.style = require('./style'); diff --git a/src/traces/mesh3d/index.js b/src/traces/mesh3d/index.js index d81cf6bc8b9..601b52b453f 100644 --- a/src/traces/mesh3d/index.js +++ b/src/traces/mesh3d/index.js @@ -28,4 +28,4 @@ Mesh3D.attributes = require('./attributes'); Mesh3D.supplyDefaults = require('./defaults'); -Mesh3D.colorbar = Plotly.Colorbar.traceColorbar; +Mesh3D.colorbar = require('../heatmap/colorbar'); diff --git a/src/traces/surface/index.js b/src/traces/surface/index.js index a45a700e4aa..b8520b0dba3 100644 --- a/src/traces/surface/index.js +++ b/src/traces/surface/index.js @@ -30,7 +30,7 @@ Surface.attributes = require('./attributes'); Surface.supplyDefaults = require('./defaults'); -Surface.colorbar = Plotly.Colorbar.traceColorbar; +Surface.colorbar = require('../heatmap/colorbar'); Surface.calc = function(gd, trace) { From 7f1801eede19682d01a5da0400b97118a6438759 Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 15:36:24 -0500 Subject: [PATCH 05/28] split up contour index --- src/traces/contour/calc.js | 47 ++ src/traces/contour/colorbar.js | 91 ++++ src/traces/contour/defaults.js | 43 ++ src/traces/contour/hover.js | 17 + src/traces/contour/index.js | 883 +-------------------------------- src/traces/contour/plot.js | 706 ++++++++++++++++++++++++++ src/traces/contour/style.js | 55 ++ 7 files changed, 967 insertions(+), 875 deletions(-) create mode 100644 src/traces/contour/calc.js create mode 100644 src/traces/contour/colorbar.js create mode 100644 src/traces/contour/defaults.js create mode 100644 src/traces/contour/hover.js create mode 100644 src/traces/contour/plot.js create mode 100644 src/traces/contour/style.js diff --git a/src/traces/contour/calc.js b/src/traces/contour/calc.js new file mode 100644 index 00000000000..a0bef873449 --- /dev/null +++ b/src/traces/contour/calc.js @@ -0,0 +1,47 @@ +/** +* Copyright 2012-2015, 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 Plotly = require('../../plotly'); +var heatmapCalc = require('../heatmap/calc'); + + +module.exports = function calc(gd, trace) { + // most is the same as heatmap calc, then adjust it + // though a few things inside heatmap calc still look for + // contour maps, because the makeBoundArray calls are too entangled + var cd = heatmapCalc(gd, trace), + contours = trace.contours; + + // check if we need to auto-choose contour levels + if(trace.autocontour!==false) { + var dummyAx = { + type: 'linear', + range: [trace.zmin, trace.zmax] + }; + Plotly.Axes.autoTicks(dummyAx, + (trace.zmax - trace.zmin) / (trace.ncontours||15)); + contours.start = Plotly.Axes.tickFirst(dummyAx); + contours.size = dummyAx.dtick; + dummyAx.range.reverse(); + contours.end = Plotly.Axes.tickFirst(dummyAx); + + if(contours.start===trace.zmin) contours.start += contours.size; + if(contours.end===trace.zmax) contours.end -= contours.size; + + // so rounding errors don't cause us to miss the last contour + contours.end += contours.size/100; + + // copy auto-contour info back to the source data. + trace._input.contours = contours; + } + + return cd; +}; diff --git a/src/traces/contour/colorbar.js b/src/traces/contour/colorbar.js new file mode 100644 index 00000000000..20e72141786 --- /dev/null +++ b/src/traces/contour/colorbar.js @@ -0,0 +1,91 @@ +/** +* Copyright 2012-2015, 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 d3 = require('d3'); + +var Plotly = require('../../plotly'); + + +module.exports = function colorbar(gd, cd) { + var trace = cd[0].trace, + cbId = 'cb'+trace.uid; + + gd._fullLayout._infolayer.selectAll('.'+cbId).remove(); + + if(trace.showscale===false){ + Plotly.Plots.autoMargin(gd, cbId); + return; + } + + // instantiate the colorbar (will be drawn and styled in contour.style) + var cb = Plotly.Colorbar(gd, cbId); + cd[0].t.cb = cb; + + var contours = trace.contours, + line = trace.line, + cs = contours.size||1, + nc = Math.floor((contours.end + cs/10 - contours.start)/cs)+1, + scl = Plotly.Colorscale.getScale(trace.colorscale), + extraLevel = contours.coloring==='lines' ? 0 : 1, + colormap = d3.scale.linear().interpolate(d3.interpolateRgb), + colorDomain = scl.map(function(si){ + return (si[0]*(nc+extraLevel-1)-(extraLevel/2)) * cs + + contours.start; + }), + colorRange = scl.map(function(si){ return si[1]; }); + + // colorbar fill and lines + if(contours.coloring==='heatmap') { + if(trace.zauto && trace.autocontour===false) { + trace.zmin = contours.start-cs/2; + trace.zmax = trace.zmin+nc*cs; + } + cb.filllevels({ + start: trace.zmin, + end: trace.zmax, + size: (trace.zmax-trace.zmin)/254 + }); + colorDomain = scl.map(function(si){ + return si[0]*(trace.zmax-trace.zmin) + trace.zmin; + }); + + // do the contours extend beyond the colorscale? + // if so, extend the colorscale with constants + var zRange = d3.extent([trace.zmin, trace.zmax, contours.start, + contours.start + cs*(nc-1)]), + zmin = zRange[trace.zmin0 - CHOOSESADDLE = { - 104: [4, 1], - 208: [2, 8], - 713: [7, 13], - 1114: [11, 14] - }, - // after one index has been used for a saddle, which do we - // substitute to be used up later? - SADDLEREMAINDER = {1: 4, 2: 8, 4: 1, 7: 13, 8: 2, 11: 14, 13: 7, 14: 11}; - -function plotOne(gd, plotinfo, cd) { - Plotly.Lib.markTime('in Contour.plot'); - var trace = cd[0].trace, - x = cd[0].x, - y = cd[0].y, - contours = trace.contours, - uid = trace.uid, - xa = plotinfo.x(), - ya = plotinfo.y(), - fullLayout = gd._fullLayout, - id = 'contour' + uid, - pathinfo = emptyPathinfo(contours, plotinfo, cd[0]); - - if(trace.visible !== true) { - fullLayout._paper.selectAll('.'+id+',.cb'+uid+',.hm'+uid).remove(); - return; - } - - // use a heatmap to fill - draw it behind the lines - if(contours.coloring==='heatmap') { - if(trace.zauto && trace.autocontour===false) { - trace._input.zmin = trace.zmin = - contours.start - contours.size/2; - trace._input.zmax = trace.zmax = - trace.zmin + pathinfo.length * contours.size; - } - Plotly.Heatmap.plot(gd, plotinfo, [cd]); - } - // in case this used to be a heatmap (or have heatmap fill) - else fullLayout._paper.selectAll('.hm'+uid).remove(); - - makeCrossings(pathinfo); - findAllPaths(pathinfo); - - var leftedge = xa.c2p(x[0], true), - rightedge = xa.c2p(x[x.length-1], true), - bottomedge = ya.c2p(y[0], true), - topedge = ya.c2p(y[y.length-1], true), - perimeter = [ - [leftedge, topedge], - [rightedge, topedge], - [rightedge, bottomedge], - [leftedge, bottomedge] - ]; - - // draw everything - var plotGroup = makeContourGroup(plotinfo, cd, id); - makeBackground(plotGroup, perimeter, contours); - makeFills(plotGroup, pathinfo, perimeter, contours); - makeLines(plotGroup, pathinfo, contours); - clipGaps(plotGroup, plotinfo, cd[0], perimeter); - - Plotly.Lib.markTime('done Contour.plot'); -} - -function emptyPathinfo(contours, plotinfo, cd0) { - var cs = contours.size || 1, - pathinfo = []; - for(var ci = contours.start; ci < contours.end + cs/10; ci += cs) { - pathinfo.push({ - level:ci, - // all the cells with nontrivial marching index - crossings:{}, - // starting points on the edges of the lattice for each contour - starts:[], - // all unclosed paths (may have less items than starts, - // if a path is closed by rounding) - edgepaths:[], - // all closed paths - paths:[], - // store axes so we can convert to px - xaxis: plotinfo.x(), - yaxis: plotinfo.y(), - // full data arrays to use for interpolation - x: cd0.x, - y: cd0.y, - z: cd0.z, - smoothing: cd0.trace.line.smoothing - }); - } - return pathinfo; -} - -// modified marching squares algorithm, -// so we disambiguate the saddle points from the start -// and we ignore the cases with no crossings -// the index I'm using is based on: -// http://en.wikipedia.org/wiki/Marching_squares -// except that the saddles bifurcate and I represent them -// as the decimal combination of the two appropriate -// non-saddle indices -function getMarchingIndex(val,corners) { - var mi = (corners[0][0] > val ? 0 : 1) + - (corners[0][1] > val ? 0 : 2) + - (corners[1][1] > val ? 0 : 4) + - (corners[1][0] > val ? 0 : 8); - if(mi === 5 || mi === 10) { - var avg = (corners[0][0] + corners[0][1] + - corners[1][0] + corners[1][1]) / 4; - // two peaks with a big valley - if(val > avg) return (mi === 5) ? 713 : 1114; - // two valleys with a big ridge - return (mi === 5) ? 104 : 208; - } - return (mi === 15) ? 0 : mi; -} - -// Calculate all the marching indices, for ALL levels at once. -// since we want to be exhaustive we'll check for contour crossings -// at every intersection, rather than just following a path -// TODO: shorten the inner loop to only the relevant levels -function makeCrossings(pathinfo) { - var z = pathinfo[0].z, - m = z.length, - n = z[0].length, // we already made sure z isn't ragged in interp2d - twoWide = m===2 || n===2, - xi, - yi, - startIndices, - ystartIndices, - label, - corners, - mi, - pi, - i; - - for(yi = 0; yi20) { - mi = CHOOSESADDLE[mi][(marchStep[0]||marchStep[1])<0 ? 0 : 1]; - pi.crossings[locStr] = SADDLEREMAINDER[mi]; - } - else { - delete pi.crossings[locStr]; - } - - marchStep = NEWDELTA[mi]; - if(!marchStep) { - console.log('found bad marching index', mi, loc, pi.level); - break; - } - - // find the crossing a half step forward, and then take the full step - pts.push(getInterpPx(pi, loc, marchStep)); - loc[0] += marchStep[0]; - loc[1] += marchStep[1]; - - // don't include the same point multiple times - if(equalPts(pts[pts.length-1], pts[pts.length-2])) pts.pop(); - locStr = loc.join(','); - - // have we completed a loop, or reached an edge? - if( (locStr===startLocStr && marchStep.join(',')===startStepStr) || - (edgeflag && ( - (marchStep[0] && (loc[0]<0 || loc[0]>n-2)) || - (marchStep[1] && (loc[1]<0 || loc[1]>m-2))))) { - break; - } - mi = pi.crossings[locStr]; - } - - if(cnt===10000) { - console.log('Infinite loop in contour?'); - } - var closedpath = equalPts(pts[0], pts[pts.length-1]), - totaldist = 0, - distThresholdFactor = 0.2 * pi.smoothing, - alldists = [], - cropstart = 0, - distgroup, - cnt2, - cnt3, - newpt, - ptcnt, - ptavg, - thisdist; - - // check for points that are too close together (<1/5 the average dist, - // less if less smoothed) and just take the center (or avg of center 2) - // this cuts down on funny behavior when a point is very close to a contour level - for(cnt=1; cnt=cropstart; cnt--) { - distgroup = alldists[cnt]; - if(distgroup=cropstart; cnt2--) { - if(distgroup+alldists[cnt2]20 && edgeflag) { - // these saddles start at +/- x - if(mi===208 || mi===1114) { - // if we're starting at the left side, we must be going right - dx = loc[0]===0 ? 1 : -1; - } - else { - // if we're starting at the bottom, we must be going up - dy = loc[1]===0 ? 1 : -1; - } - } - else if(BOTTOMSTART.indexOf(mi)!==-1) dy = 1; - else if(LEFTSTART.indexOf(mi)!==-1) dx = 1; - else if(TOPSTART.indexOf(mi)!==-1) dy = -1; - else dx = -1; - return [dx, dy]; -} - -function equalPts(pt1, pt2) { - return Math.abs(pt1[0] - pt2[0]) < 0.01 && - Math.abs(pt1[1] - pt2[1]) < 0.01; -} - -function ptDist(pt1, pt2) { - var dx = pt1[0] - pt2[0], - dy = pt1[1] - pt2[1]; - return Math.sqrt(dx*dx + dy*dy); -} - -function getInterpPx(pi, loc, step) { - var locx = loc[0] + Math.max(step[0], 0), - locy = loc[1] + Math.max(step[1], 0), - zxy = pi.z[locy][locx], - xa = pi.xaxis, - ya = pi.yaxis; - - if(step[1]) { - var dx = (pi.level - zxy) / (pi.z[locy][locx+1] - zxy); - return [xa.c2p((1-dx) * pi.x[locx] + dx * pi.x[locx+1], true), - ya.c2p(pi.y[locy], true)]; - } - else { - var dy = (pi.level - zxy) / (pi.z[locy+1][locx] - zxy); - return [xa.c2p(pi.x[locx], true), - ya.c2p((1-dy) * pi.y[locy] + dy * pi.y[locy+1], true)]; - } -} - -function makeContourGroup(plotinfo, cd, id) { - var plotgroup = plotinfo.plot.select('.maplayer') - .selectAll('g.contour.'+id) - .data(cd); - plotgroup.enter().append('g') - .classed('contour',true) - .classed(id,true); - plotgroup.exit().remove(); - return plotgroup; -} - -function makeBackground(plotgroup, perimeter, contours) { - var bggroup = plotgroup.selectAll('g.contourbg').data([0]); - bggroup.enter().append('g').classed('contourbg',true); - - var bgfill = bggroup.selectAll('path') - .data(contours.coloring==='fill' ? [0] : []); - bgfill.enter().append('path'); - bgfill.exit().remove(); - bgfill - .attr('d','M'+perimeter.join('L')+'Z') - .style('stroke','none'); -} - -function makeFills(plotgroup, pathinfo, perimeter, contours) { - var fillgroup = plotgroup.selectAll('g.contourfill') - .data([0]); - fillgroup.enter().append('g') - .classed('contourfill',true); - - var fillitems = fillgroup.selectAll('path') - .data(contours.coloring==='fill' ? pathinfo : []); - fillitems.enter().append('path'); - fillitems.exit().remove(); - fillitems.each(function(pi){ - // join all paths for this level together into a single path - // first follow clockwise around the perimeter to close any open paths - // if the whole perimeter is above this level, start with a path - // enclosing the whole thing. With all that, the parity should mean - // that we always fill everything above the contour, nothing below - var fullpath = joinAllPaths(pi, perimeter); - - if(!fullpath) d3.select(this).remove(); - else d3.select(this).attr('d',fullpath).style('stroke', 'none'); - }); -} - -function joinAllPaths(pi, perimeter) { - var fullpath = (pi.edgepaths.length || pi.z[0][0] < pi.level) ? - '' : ('M'+perimeter.join('L')+'Z'), - i = 0, - startsleft = pi.edgepaths.map(function(v,i){ return i; }), - newloop = true, - endpt, - newendpt, - cnt, - nexti, - possiblei, - addpath; - - function istop(pt) { return Math.abs(pt[1] - perimeter[0][1]) < 0.01; } - function isbottom(pt) { return Math.abs(pt[1] - perimeter[2][1]) < 0.01; } - function isleft(pt) { return Math.abs(pt[0] - perimeter[0][0]) < 0.01; } - function isright(pt) { return Math.abs(pt[0] - perimeter[2][0]) < 0.01; } - - while(startsleft.length) { - addpath = Plotly.Drawing.smoothopen(pi.edgepaths[i], pi.smoothing); - fullpath += newloop ? addpath : addpath.replace(/^M/, 'L'); - startsleft.splice(startsleft.indexOf(i), 1); - endpt = pi.edgepaths[i][pi.edgepaths[i].length-1]; - nexti = -1; - - //now loop through sides, moving our endpoint until we find a new start - for(cnt=0; cnt<4; cnt++) { // just to prevent infinite loops - if(!endpt) { - console.log('missing end?',i,pi); - break; - } - - if(istop(endpt) && !isright(endpt)) newendpt = perimeter[1]; // right top - else if(isleft(endpt)) newendpt = perimeter[0]; // left top - else if(isbottom(endpt)) newendpt = perimeter[3]; // right bottom - else if(isright(endpt)) newendpt = perimeter[2]; // left bottom - - for(possiblei=0; possiblei < pi.edgepaths.length; possiblei++) { - var ptNew = pi.edgepaths[possiblei][0]; - // is ptNew on the (horz. or vert.) segment from endpt to newendpt? - if(Math.abs(endpt[0]-newendpt[0]) < 0.01) { - if(Math.abs(endpt[0]-ptNew[0]) < 0.01 && - (ptNew[1]-endpt[1]) * (newendpt[1]-ptNew[1]) >= 0) { - newendpt = ptNew; - nexti = possiblei; - } - } - else if(Math.abs(endpt[1]-newendpt[1]) < 0.01) { - if(Math.abs(endpt[1]-ptNew[1]) < 0.01 && - (ptNew[0]-endpt[0]) * (newendpt[0]-ptNew[0]) >= 0) { - newendpt = ptNew; - nexti = possiblei; - } - } - else { - console.log('endpt to newendpt is not vert. or horz.', - endpt, newendpt, ptNew); - } - } - - endpt = newendpt; - - if(nexti>=0) break; - fullpath += 'L'+newendpt; - } - - if(nexti === pi.edgepaths.length) { - console.log('unclosed perimeter path'); - break; - } - - i = nexti; - - // if we closed back on a loop we already included, - // close it and start a new loop - newloop = (startsleft.indexOf(i)===-1); - if(newloop) { - i = startsleft[0]; - fullpath += 'Z'; - } - } - - // finally add the interior paths - for(i = 0; i < pi.paths.length; i++) { - fullpath += Plotly.Drawing.smoothclosed(pi.paths[i], pi.smoothing); - } - - return fullpath; -} - -function makeLines(plotgroup, pathinfo, contours) { - var smoothing = pathinfo[0].smoothing; - - var linegroup = plotgroup.selectAll('g.contourlevel') - .data(contours.showlines===false ? [] : pathinfo); - linegroup.enter().append('g') - .classed('contourlevel',true); - linegroup.exit().remove(); - - var opencontourlines = linegroup.selectAll('path.openline') - .data(function(d){ return d.edgepaths; }); - opencontourlines.enter().append('path') - .classed('openline',true); - opencontourlines.exit().remove(); - opencontourlines - .attr('d', function(d){ - return Plotly.Drawing.smoothopen(d, smoothing); - }) - .style('stroke-miterlimit',1); - - var closedcontourlines = linegroup.selectAll('path.closedline') - .data(function(d){ return d.paths; }); - closedcontourlines.enter().append('path') - .classed('closedline',true); - closedcontourlines.exit().remove(); - closedcontourlines - .attr('d', function(d){ - return Plotly.Drawing.smoothclosed(d, smoothing); - }) - .style('stroke-miterlimit',1); -} - -function clipGaps(plotGroup, plotinfo, cd0, perimeter) { - var clipId = 'clip' + cd0.trace.uid; - - var defs = plotinfo.plot.selectAll('defs') - .data([0]); - defs.enter().append('defs'); - - var clipPath = defs.selectAll('#' + clipId) - .data(cd0.trace.connectgaps ? [] : [0]); - clipPath.enter().append('clipPath').attr('id', clipId); - clipPath.exit().remove(); - - if(cd0.trace.connectgaps === false) { - var clipPathInfo = { - // fraction of the way from missing to present point - // to draw the boundary. - // if you make this 1 (or 1-epsilon) then a point in - // a sea of missing data will disappear entirely. - level: 0.9, - crossings: {}, - starts: [], - edgepaths: [], - paths: [], - xaxis: plotinfo.x(), - yaxis: plotinfo.y(), - x: cd0.x, - y: cd0.y, - // 0 = no data, 1 = data - z: makeClipMask(cd0), - smoothing: 0 - }; - - makeCrossings([clipPathInfo]); - findAllPaths([clipPathInfo]); - var fullpath = joinAllPaths(clipPathInfo, perimeter); - - var path = clipPath.selectAll('path') - .data([0]); - path.enter().append('path'); - path.attr('d', fullpath); - } - else clipId = null; - - plotGroup.call(Plotly.Drawing.setClipUrl, clipId); - plotinfo.plot.selectAll('.hm' + cd0.trace.uid) - .call(Plotly.Drawing.setClipUrl, clipId); -} - -function makeClipMask(cd0) { - var empties = cd0.trace._emptypoints, - z = [], - m = cd0.z.length, - n = cd0.z[0].length, - i, - row = [], - emptyPoint; - - for(i = 0; i < n; i++) row.push(1); - for(i = 0; i < m; i++) z.push(row.slice()); - for(i = 0; i < empties.length; i++) { - emptyPoint = empties[i]; - z[emptyPoint[0]][emptyPoint[1]] = 0; - } - // save this mask to determine whether to show this data in hover - cd0.zmask = z; - return z; -} - -contour.style = function(gd) { - d3.select(gd).selectAll('g.contour') - .style('opacity',function(d){ return d.trace.opacity; }) - .each(function(d) { - var c = d3.select(this), - trace = d.trace, - contours = trace.contours, - line = trace.line, - colorLines = contours.coloring==='lines', - cs = contours.size||1, - nc = Math.floor((contours.end + cs/10 - contours.start)/cs) + 1, - scl = Plotly.Colorscale.getScale(trace.colorscale), - extraLevel = colorLines ? 0 : 1, - colormap = d3.scale.linear() - .domain(scl.map(function(si){ - return (si[0]*(nc+extraLevel-1)-(extraLevel/2)) * cs + - contours.start; - })) - .interpolate(d3.interpolateRgb) - .range(scl.map(function(si){ return si[1]; })); - - c.selectAll('g.contourlevel').each(function(d, i) { - d3.select(this).selectAll('path') - .call(Plotly.Drawing.lineGroupStyle, - line.width, - colorLines ? colormap(contours.start+i*cs) : line.color, - line.dash); - }); - c.selectAll('g.contourbg path') - .style('fill', colormap(contours.start - cs/2)); - c.selectAll('g.contourfill path') - .style('fill',function(d, i){ - return colormap(contours.start + (i+0.5)*cs); - }); - }); - Plotly.Heatmap.style(gd); -}; - -contour.colorbar = function(gd, cd) { - var trace = cd[0].trace, - cbId = 'cb'+trace.uid; - - gd._fullLayout._infolayer.selectAll('.'+cbId).remove(); - if(trace.showscale===false){ - Plotly.Plots.autoMargin(gd, cbId); - return; - } - - // instantiate the colorbar (will be drawn and styled in contour.style) - var cb = Plotly.Colorbar(gd, cbId); - cd[0].t.cb = cb; +exports.attributes = require('./attributes'); - var contours = trace.contours, - line = trace.line, - cs = contours.size||1, - nc = Math.floor((contours.end + cs/10 - contours.start)/cs)+1, - scl = Plotly.Colorscale.getScale(trace.colorscale), - extraLevel = contours.coloring==='lines' ? 0 : 1, - colormap = d3.scale.linear().interpolate(d3.interpolateRgb), - colorDomain = scl.map(function(si){ - return (si[0]*(nc+extraLevel-1)-(extraLevel/2)) * cs + - contours.start; - }), - colorRange = scl.map(function(si){ return si[1]; }); +exports.supplyDefaults = require('./defaults'); - // colorbar fill and lines - if(contours.coloring==='heatmap') { - if(trace.zauto && trace.autocontour===false) { - trace.zmin = contours.start-cs/2; - trace.zmax = trace.zmin+nc*cs; - } - cb.filllevels({ - start: trace.zmin, - end: trace.zmax, - size: (trace.zmax-trace.zmin)/254 - }); - colorDomain = scl.map(function(si){ - return si[0]*(trace.zmax-trace.zmin) + trace.zmin; - }); +exports.calc = require('./calc'); - // do the contours extend beyond the colorscale? - // if so, extend the colorscale with constants - var zRange = d3.extent([trace.zmin, trace.zmax, contours.start, - contours.start + cs*(nc-1)]), - zmin = zRange[trace.zmin0 + CHOOSESADDLE = { + 104: [4, 1], + 208: [2, 8], + 713: [7, 13], + 1114: [11, 14] + }, + // after one index has been used for a saddle, which do we + // substitute to be used up later? + SADDLEREMAINDER = {1: 4, 2: 8, 4: 1, 7: 13, 8: 2, 11: 14, 13: 7, 14: 11}; + +function plotOne(gd, plotinfo, cd) { + Lib.markTime('in Contour.plot'); + var trace = cd[0].trace, + x = cd[0].x, + y = cd[0].y, + contours = trace.contours, + uid = trace.uid, + xa = plotinfo.x(), + ya = plotinfo.y(), + fullLayout = gd._fullLayout, + id = 'contour' + uid, + pathinfo = emptyPathinfo(contours, plotinfo, cd[0]); + + if(trace.visible !== true) { + fullLayout._paper.selectAll('.'+id+',.cb'+uid+',.hm'+uid).remove(); + return; + } + + // use a heatmap to fill - draw it behind the lines + if(contours.coloring==='heatmap') { + if(trace.zauto && trace.autocontour===false) { + trace._input.zmin = trace.zmin = + contours.start - contours.size/2; + trace._input.zmax = trace.zmax = + trace.zmin + pathinfo.length * contours.size; + } + + heatmapPlot(gd, plotinfo, [cd]); + } + // in case this used to be a heatmap (or have heatmap fill) + else fullLayout._paper.selectAll('.hm'+uid).remove(); + + makeCrossings(pathinfo); + findAllPaths(pathinfo); + + var leftedge = xa.c2p(x[0], true), + rightedge = xa.c2p(x[x.length-1], true), + bottomedge = ya.c2p(y[0], true), + topedge = ya.c2p(y[y.length-1], true), + perimeter = [ + [leftedge, topedge], + [rightedge, topedge], + [rightedge, bottomedge], + [leftedge, bottomedge] + ]; + + // draw everything + var plotGroup = makeContourGroup(plotinfo, cd, id); + makeBackground(plotGroup, perimeter, contours); + makeFills(plotGroup, pathinfo, perimeter, contours); + makeLines(plotGroup, pathinfo, contours); + clipGaps(plotGroup, plotinfo, cd[0], perimeter); + + Plotly.Lib.markTime('done Contour.plot'); +} + +function emptyPathinfo(contours, plotinfo, cd0) { + var cs = contours.size || 1, + pathinfo = []; + for(var ci = contours.start; ci < contours.end + cs/10; ci += cs) { + pathinfo.push({ + level:ci, + // all the cells with nontrivial marching index + crossings:{}, + // starting points on the edges of the lattice for each contour + starts:[], + // all unclosed paths (may have less items than starts, + // if a path is closed by rounding) + edgepaths:[], + // all closed paths + paths:[], + // store axes so we can convert to px + xaxis: plotinfo.x(), + yaxis: plotinfo.y(), + // full data arrays to use for interpolation + x: cd0.x, + y: cd0.y, + z: cd0.z, + smoothing: cd0.trace.line.smoothing + }); + } + return pathinfo; +} + +// modified marching squares algorithm, +// so we disambiguate the saddle points from the start +// and we ignore the cases with no crossings +// the index I'm using is based on: +// http://en.wikipedia.org/wiki/Marching_squares +// except that the saddles bifurcate and I represent them +// as the decimal combination of the two appropriate +// non-saddle indices +function getMarchingIndex(val,corners) { + var mi = (corners[0][0] > val ? 0 : 1) + + (corners[0][1] > val ? 0 : 2) + + (corners[1][1] > val ? 0 : 4) + + (corners[1][0] > val ? 0 : 8); + if(mi === 5 || mi === 10) { + var avg = (corners[0][0] + corners[0][1] + + corners[1][0] + corners[1][1]) / 4; + // two peaks with a big valley + if(val > avg) return (mi === 5) ? 713 : 1114; + // two valleys with a big ridge + return (mi === 5) ? 104 : 208; + } + return (mi === 15) ? 0 : mi; +} + +// Calculate all the marching indices, for ALL levels at once. +// since we want to be exhaustive we'll check for contour crossings +// at every intersection, rather than just following a path +// TODO: shorten the inner loop to only the relevant levels +function makeCrossings(pathinfo) { + var z = pathinfo[0].z, + m = z.length, + n = z[0].length, // we already made sure z isn't ragged in interp2d + twoWide = m===2 || n===2, + xi, + yi, + startIndices, + ystartIndices, + label, + corners, + mi, + pi, + i; + + for(yi = 0; yi20) { + mi = CHOOSESADDLE[mi][(marchStep[0]||marchStep[1])<0 ? 0 : 1]; + pi.crossings[locStr] = SADDLEREMAINDER[mi]; + } + else { + delete pi.crossings[locStr]; + } + + marchStep = NEWDELTA[mi]; + if(!marchStep) { + console.log('found bad marching index', mi, loc, pi.level); + break; + } + + // find the crossing a half step forward, and then take the full step + pts.push(getInterpPx(pi, loc, marchStep)); + loc[0] += marchStep[0]; + loc[1] += marchStep[1]; + + // don't include the same point multiple times + if(equalPts(pts[pts.length-1], pts[pts.length-2])) pts.pop(); + locStr = loc.join(','); + + // have we completed a loop, or reached an edge? + if( (locStr===startLocStr && marchStep.join(',')===startStepStr) || + (edgeflag && ( + (marchStep[0] && (loc[0]<0 || loc[0]>n-2)) || + (marchStep[1] && (loc[1]<0 || loc[1]>m-2))))) { + break; + } + mi = pi.crossings[locStr]; + } + + if(cnt===10000) { + console.log('Infinite loop in contour?'); + } + var closedpath = equalPts(pts[0], pts[pts.length-1]), + totaldist = 0, + distThresholdFactor = 0.2 * pi.smoothing, + alldists = [], + cropstart = 0, + distgroup, + cnt2, + cnt3, + newpt, + ptcnt, + ptavg, + thisdist; + + // check for points that are too close together (<1/5 the average dist, + // less if less smoothed) and just take the center (or avg of center 2) + // this cuts down on funny behavior when a point is very close to a contour level + for(cnt=1; cnt=cropstart; cnt--) { + distgroup = alldists[cnt]; + if(distgroup=cropstart; cnt2--) { + if(distgroup+alldists[cnt2]20 && edgeflag) { + // these saddles start at +/- x + if(mi===208 || mi===1114) { + // if we're starting at the left side, we must be going right + dx = loc[0]===0 ? 1 : -1; + } + else { + // if we're starting at the bottom, we must be going up + dy = loc[1]===0 ? 1 : -1; + } + } + else if(BOTTOMSTART.indexOf(mi)!==-1) dy = 1; + else if(LEFTSTART.indexOf(mi)!==-1) dx = 1; + else if(TOPSTART.indexOf(mi)!==-1) dy = -1; + else dx = -1; + return [dx, dy]; +} + +function equalPts(pt1, pt2) { + return Math.abs(pt1[0] - pt2[0]) < 0.01 && + Math.abs(pt1[1] - pt2[1]) < 0.01; +} + +function ptDist(pt1, pt2) { + var dx = pt1[0] - pt2[0], + dy = pt1[1] - pt2[1]; + return Math.sqrt(dx*dx + dy*dy); +} + +function getInterpPx(pi, loc, step) { + var locx = loc[0] + Math.max(step[0], 0), + locy = loc[1] + Math.max(step[1], 0), + zxy = pi.z[locy][locx], + xa = pi.xaxis, + ya = pi.yaxis; + + if(step[1]) { + var dx = (pi.level - zxy) / (pi.z[locy][locx+1] - zxy); + return [xa.c2p((1-dx) * pi.x[locx] + dx * pi.x[locx+1], true), + ya.c2p(pi.y[locy], true)]; + } + else { + var dy = (pi.level - zxy) / (pi.z[locy+1][locx] - zxy); + return [xa.c2p(pi.x[locx], true), + ya.c2p((1-dy) * pi.y[locy] + dy * pi.y[locy+1], true)]; + } +} + +function makeContourGroup(plotinfo, cd, id) { + var plotgroup = plotinfo.plot.select('.maplayer') + .selectAll('g.contour.'+id) + .data(cd); + plotgroup.enter().append('g') + .classed('contour',true) + .classed(id,true); + plotgroup.exit().remove(); + return plotgroup; +} + +function makeBackground(plotgroup, perimeter, contours) { + var bggroup = plotgroup.selectAll('g.contourbg').data([0]); + bggroup.enter().append('g').classed('contourbg',true); + + var bgfill = bggroup.selectAll('path') + .data(contours.coloring==='fill' ? [0] : []); + bgfill.enter().append('path'); + bgfill.exit().remove(); + bgfill + .attr('d','M'+perimeter.join('L')+'Z') + .style('stroke','none'); +} + +function makeFills(plotgroup, pathinfo, perimeter, contours) { + var fillgroup = plotgroup.selectAll('g.contourfill') + .data([0]); + fillgroup.enter().append('g') + .classed('contourfill',true); + + var fillitems = fillgroup.selectAll('path') + .data(contours.coloring==='fill' ? pathinfo : []); + fillitems.enter().append('path'); + fillitems.exit().remove(); + fillitems.each(function(pi){ + // join all paths for this level together into a single path + // first follow clockwise around the perimeter to close any open paths + // if the whole perimeter is above this level, start with a path + // enclosing the whole thing. With all that, the parity should mean + // that we always fill everything above the contour, nothing below + var fullpath = joinAllPaths(pi, perimeter); + + if(!fullpath) d3.select(this).remove(); + else d3.select(this).attr('d',fullpath).style('stroke', 'none'); + }); +} + +function joinAllPaths(pi, perimeter) { + var fullpath = (pi.edgepaths.length || pi.z[0][0] < pi.level) ? + '' : ('M'+perimeter.join('L')+'Z'), + i = 0, + startsleft = pi.edgepaths.map(function(v,i){ return i; }), + newloop = true, + endpt, + newendpt, + cnt, + nexti, + possiblei, + addpath; + + function istop(pt) { return Math.abs(pt[1] - perimeter[0][1]) < 0.01; } + function isbottom(pt) { return Math.abs(pt[1] - perimeter[2][1]) < 0.01; } + function isleft(pt) { return Math.abs(pt[0] - perimeter[0][0]) < 0.01; } + function isright(pt) { return Math.abs(pt[0] - perimeter[2][0]) < 0.01; } + + while(startsleft.length) { + addpath = Plotly.Drawing.smoothopen(pi.edgepaths[i], pi.smoothing); + fullpath += newloop ? addpath : addpath.replace(/^M/, 'L'); + startsleft.splice(startsleft.indexOf(i), 1); + endpt = pi.edgepaths[i][pi.edgepaths[i].length-1]; + nexti = -1; + + //now loop through sides, moving our endpoint until we find a new start + for(cnt=0; cnt<4; cnt++) { // just to prevent infinite loops + if(!endpt) { + console.log('missing end?',i,pi); + break; + } + + if(istop(endpt) && !isright(endpt)) newendpt = perimeter[1]; // right top + else if(isleft(endpt)) newendpt = perimeter[0]; // left top + else if(isbottom(endpt)) newendpt = perimeter[3]; // right bottom + else if(isright(endpt)) newendpt = perimeter[2]; // left bottom + + for(possiblei=0; possiblei < pi.edgepaths.length; possiblei++) { + var ptNew = pi.edgepaths[possiblei][0]; + // is ptNew on the (horz. or vert.) segment from endpt to newendpt? + if(Math.abs(endpt[0]-newendpt[0]) < 0.01) { + if(Math.abs(endpt[0]-ptNew[0]) < 0.01 && + (ptNew[1]-endpt[1]) * (newendpt[1]-ptNew[1]) >= 0) { + newendpt = ptNew; + nexti = possiblei; + } + } + else if(Math.abs(endpt[1]-newendpt[1]) < 0.01) { + if(Math.abs(endpt[1]-ptNew[1]) < 0.01 && + (ptNew[0]-endpt[0]) * (newendpt[0]-ptNew[0]) >= 0) { + newendpt = ptNew; + nexti = possiblei; + } + } + else { + console.log('endpt to newendpt is not vert. or horz.', + endpt, newendpt, ptNew); + } + } + + endpt = newendpt; + + if(nexti>=0) break; + fullpath += 'L'+newendpt; + } + + if(nexti === pi.edgepaths.length) { + console.log('unclosed perimeter path'); + break; + } + + i = nexti; + + // if we closed back on a loop we already included, + // close it and start a new loop + newloop = (startsleft.indexOf(i)===-1); + if(newloop) { + i = startsleft[0]; + fullpath += 'Z'; + } + } + + // finally add the interior paths + for(i = 0; i < pi.paths.length; i++) { + fullpath += Plotly.Drawing.smoothclosed(pi.paths[i], pi.smoothing); + } + + return fullpath; +} + +function makeLines(plotgroup, pathinfo, contours) { + var smoothing = pathinfo[0].smoothing; + + var linegroup = plotgroup.selectAll('g.contourlevel') + .data(contours.showlines===false ? [] : pathinfo); + linegroup.enter().append('g') + .classed('contourlevel',true); + linegroup.exit().remove(); + + var opencontourlines = linegroup.selectAll('path.openline') + .data(function(d){ return d.edgepaths; }); + opencontourlines.enter().append('path') + .classed('openline',true); + opencontourlines.exit().remove(); + opencontourlines + .attr('d', function(d){ + return Plotly.Drawing.smoothopen(d, smoothing); + }) + .style('stroke-miterlimit',1); + + var closedcontourlines = linegroup.selectAll('path.closedline') + .data(function(d){ return d.paths; }); + closedcontourlines.enter().append('path') + .classed('closedline',true); + closedcontourlines.exit().remove(); + closedcontourlines + .attr('d', function(d){ + return Plotly.Drawing.smoothclosed(d, smoothing); + }) + .style('stroke-miterlimit',1); +} + +function clipGaps(plotGroup, plotinfo, cd0, perimeter) { + var clipId = 'clip' + cd0.trace.uid; + + var defs = plotinfo.plot.selectAll('defs') + .data([0]); + defs.enter().append('defs'); + + var clipPath = defs.selectAll('#' + clipId) + .data(cd0.trace.connectgaps ? [] : [0]); + clipPath.enter().append('clipPath').attr('id', clipId); + clipPath.exit().remove(); + + if(cd0.trace.connectgaps === false) { + var clipPathInfo = { + // fraction of the way from missing to present point + // to draw the boundary. + // if you make this 1 (or 1-epsilon) then a point in + // a sea of missing data will disappear entirely. + level: 0.9, + crossings: {}, + starts: [], + edgepaths: [], + paths: [], + xaxis: plotinfo.x(), + yaxis: plotinfo.y(), + x: cd0.x, + y: cd0.y, + // 0 = no data, 1 = data + z: makeClipMask(cd0), + smoothing: 0 + }; + + makeCrossings([clipPathInfo]); + findAllPaths([clipPathInfo]); + var fullpath = joinAllPaths(clipPathInfo, perimeter); + + var path = clipPath.selectAll('path') + .data([0]); + path.enter().append('path'); + path.attr('d', fullpath); + } + else clipId = null; + + plotGroup.call(Plotly.Drawing.setClipUrl, clipId); + plotinfo.plot.selectAll('.hm' + cd0.trace.uid) + .call(Plotly.Drawing.setClipUrl, clipId); +} + +function makeClipMask(cd0) { + var empties = cd0.trace._emptypoints, + z = [], + m = cd0.z.length, + n = cd0.z[0].length, + i, + row = [], + emptyPoint; + + for(i = 0; i < n; i++) row.push(1); + for(i = 0; i < m; i++) z.push(row.slice()); + for(i = 0; i < empties.length; i++) { + emptyPoint = empties[i]; + z[emptyPoint[0]][emptyPoint[1]] = 0; + } + // save this mask to determine whether to show this data in hover + cd0.zmask = z; + return z; +} diff --git a/src/traces/contour/style.js b/src/traces/contour/style.js new file mode 100644 index 00000000000..f9e382a7a04 --- /dev/null +++ b/src/traces/contour/style.js @@ -0,0 +1,55 @@ +/** +* Copyright 2012-2015, 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 d3 = require('d3'); + +var Plotly = require('../../plotly'); +var heatmapStyle = require('../heatmap/style'); + + +module.exports = function style(gd) { + d3.select(gd).selectAll('g.contour') + .style('opacity',function(d){ return d.trace.opacity; }) + .each(function(d) { + var c = d3.select(this), + trace = d.trace, + contours = trace.contours, + line = trace.line, + colorLines = contours.coloring==='lines', + cs = contours.size||1, + nc = Math.floor((contours.end + cs/10 - contours.start)/cs) + 1, + scl = Plotly.Colorscale.getScale(trace.colorscale), + extraLevel = colorLines ? 0 : 1, + colormap = d3.scale.linear() + .domain(scl.map(function(si){ + return (si[0]*(nc+extraLevel-1)-(extraLevel/2)) * cs + + contours.start; + })) + .interpolate(d3.interpolateRgb) + .range(scl.map(function(si){ return si[1]; })); + + c.selectAll('g.contourlevel').each(function(d, i) { + d3.select(this).selectAll('path') + .call(Plotly.Drawing.lineGroupStyle, + line.width, + colorLines ? colormap(contours.start+i*cs) : line.color, + line.dash); + }); + c.selectAll('g.contourbg path') + .style('fill', colormap(contours.start - cs/2)); + c.selectAll('g.contourfill path') + .style('fill',function(d, i){ + return colormap(contours.start + (i+0.5)*cs); + }); + }); + + heatmapStyle(gd); +}; From 2ba8f80e9fb75e16f904e92a28d8726bfd60b9db Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 15:39:25 -0500 Subject: [PATCH 06/28] split up histogram index --- src/traces/histogram/average.js | 24 +++ src/traces/histogram/bin_functions.js | 72 +++++++ src/traces/histogram/calc.js | 128 ++++++++++++ src/traces/histogram/defaults.js | 68 ++++++ src/traces/histogram/index.js | 273 ------------------------- src/traces/histogram/norm_functions.js | 33 +++ 6 files changed, 325 insertions(+), 273 deletions(-) create mode 100644 src/traces/histogram/average.js create mode 100644 src/traces/histogram/bin_functions.js create mode 100644 src/traces/histogram/calc.js create mode 100644 src/traces/histogram/defaults.js create mode 100644 src/traces/histogram/norm_functions.js diff --git a/src/traces/histogram/average.js b/src/traces/histogram/average.js new file mode 100644 index 00000000000..2190e3f3172 --- /dev/null +++ b/src/traces/histogram/average.js @@ -0,0 +1,24 @@ +/** +* Copyright 2012-2015, 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'; + + +module.exports = function doAvg(size, counts) { + var nMax = size.length, + total = 0; + for(var i=0; iv) { + size[n] = v; + return v-size[n]; + } + } + return 0; + }, + + max: function(n, i, size, counterData) { + var v = counterData[i]; + if(isNumeric(v)) { + v = Number(v); + if(!isNumeric(size[n])) { + size[n] = v; + return v; + } + else if(size[n]setstyles + var pos = [], + size = [], + i, + pa = Plotly.Axes.getFromId(gd, + trace.orientation==='h' ? (trace.yaxis || 'y') : (trace.xaxis || 'x')), + maindata = trace.orientation==='h' ? 'y' : 'x', + counterdata = {x: 'y', y: 'x'}[maindata]; + + // prepare the raw data + var pos0 = pa.makeCalcdata(trace, maindata); + // calculate the bins + if((trace['autobin' + maindata]!==false) || !(maindata + 'bins' in trace)) { + trace[maindata + 'bins'] = Plotly.Axes.autoBin(pos0, pa, trace['nbins' + maindata]); + + // copy bin info back to the source data. + trace._input[maindata + 'bins'] = trace[maindata + 'bins']; + } + + var binspec = trace[maindata + 'bins'], + allbins = typeof binspec.size === 'string', + bins = allbins ? [] : binspec, + // make the empty bin array + i2, + binend, + n, + inc = [], + counts = [], + total = 0, + norm = trace.histnorm, + func = trace.histfunc, + densitynorm = norm.indexOf('density')!==-1, + extremefunc = func==='max' || func==='min', + sizeinit = extremefunc ? null : 0, + binfunc = binFunctions.count, + normfunc = normFunctions[norm], + doavg = false, + rawCounterData; + + if(Array.isArray(trace[counterdata]) && func!=='count') { + rawCounterData = trace[counterdata]; + doavg = func==='avg'; + binfunc = binFunctions[func]; + } + + // create the bins (and any extra arrays needed) + // assume more than 5000 bins is an error, so we don't crash the browser + i = binspec.start; + // decrease end a little in case of rounding errors + binend = binspec.end + + (binspec.start - Plotly.Axes.tickIncrement(binspec.start, binspec.size)) / 1e6; + while(i=0 && nfirstNonzero; i--) { + if(size[i]) { + lastNonzero = i; + break; + } + } + + // create the "calculated data" to plot + for(i=firstNonzero; i<=lastNonzero; i++) { + if((isNumeric(pos[i]) && isNumeric(size[i]))) { + cd.push({p: pos[i], s: size[i], b: 0}); + } + } + + return cd; +}; diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js new file mode 100644 index 00000000000..1138561121c --- /dev/null +++ b/src/traces/histogram/defaults.js @@ -0,0 +1,68 @@ +/** +* Copyright 2012-2015, 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 Plotly = require('../../plotly'); +var Lib = require('../../lib'); + +var attributes = require('./attributes'); + + +module.exports = function(traceIn, traceOut) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var binDirections = ['x'], + hasAggregationData, + x = coerce('x'), + y = coerce('y'); + + if(Plotly.Plots.traceIs(traceOut, '2dMap')) { + // we could try to accept x0 and dx, etc... + // but that's a pretty weird use case. + // for now require both x and y explicitly specified. + if(!(x && x.length && y && y.length)) { + traceOut.visible = false; + return; + } + + // if marker.color is an array, we can use it in aggregation instead of z + hasAggregationData = coerce('z') || coerce('marker.color'); + + binDirections = ['x','y']; + } else { + var orientation = coerce('orientation', (y && !x) ? 'h' : 'v'), + sample = traceOut[orientation==='v' ? 'x' : 'y']; + + if(!(sample && sample.length)) { + traceOut.visible = false; + return; + } + + if(orientation==='h') binDirections = ['y']; + + hasAggregationData = traceOut[orientation==='h' ? 'x' : 'y']; + } + + if(hasAggregationData) coerce('histfunc'); + coerce('histnorm'); + + binDirections.forEach(function(binDirection) { + // data being binned - note that even though it's a little weird, + // it's possible to have bins without data, if there's inferred data + var binstrt = coerce(binDirection + 'bins.start'), + binend = coerce(binDirection + 'bins.end'), + autobin = coerce('autobin' + binDirection, !(binstrt && binend)); + + if(autobin) coerce('nbins' + binDirection); + else coerce(binDirection + 'bins.size'); + }); +}; diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js index cdad060f9f6..6eb6928001b 100644 --- a/src/traces/histogram/index.js +++ b/src/traces/histogram/index.js @@ -10,14 +10,12 @@ 'use strict'; var Plotly = require('../../plotly'); -var isNumeric = require('fast-isnumeric'); /** * Histogram has its own calc function, * but uses Bars.plot to display * and Bars.setPositions for stacking and grouping */ -var histogram = module.exports = {}; /** * histogram errorBarsOK is debatable, but it's put in for backward compat. @@ -25,7 +23,6 @@ var histogram = module.exports = {}; * constant and % work but they're not so meaningful. I guess it could be cool * to allow quadrature combination of errors in summed histograms... */ -Plotly.Plots.register(Plotly.Bars, 'histogram', ['cartesian', 'bar', 'histogram', 'oriented', 'errorBarsOK', 'showLegend'], { description: [ 'The sample data from which statistics are computed is set in `x`', @@ -37,276 +34,6 @@ Plotly.Plots.register(Plotly.Bars, 'histogram', ].join(' ') }); -Plotly.Plots.register(Plotly.Heatmap, 'histogram2d', - ['cartesian', '2dMap', 'histogram'], { - hrName: 'histogram_2d', - description: [ - 'The sample data from which statistics are computed is set in `x`', - 'and `y` (where `x` and `y` represent marginal distributions,', - 'binning is set in `xbins` and `ybins` in this case)', - 'or `z` (where `z` represent the 2D distribution and binning set,', - 'binning is set by `x` and `y` in this case).', - 'The resulting distribution is visualized as a heatmap.' - ].join(' ') -}); - -histogram.attributes = require('./attributes'); - -histogram.supplyDefaults = function(traceIn, traceOut) { - function coerce(attr, dflt) { - return Plotly.Lib.coerce(traceIn, traceOut, histogram.attributes, attr, dflt); - } - - var binDirections = ['x'], - hasAggregationData, - x = coerce('x'), - y = coerce('y'); - - if(Plotly.Plots.traceIs(traceOut, '2dMap')) { - // we could try to accept x0 and dx, etc... - // but that's a pretty weird use case. - // for now require both x and y explicitly specified. - if(!(x && x.length && y && y.length)) { - traceOut.visible = false; - return; - } - - // if marker.color is an array, we can use it in aggregation instead of z - hasAggregationData = coerce('z') || coerce('marker.color'); - - binDirections = ['x','y']; - } else { - var orientation = coerce('orientation', (y && !x) ? 'h' : 'v'), - sample = traceOut[orientation==='v' ? 'x' : 'y']; - - if(!(sample && sample.length)) { - traceOut.visible = false; - return; - } - - if(orientation==='h') binDirections = ['y']; - - hasAggregationData = traceOut[orientation==='h' ? 'x' : 'y']; - } - - if(hasAggregationData) coerce('histfunc'); - coerce('histnorm'); - - binDirections.forEach(function(binDirection){ - // data being binned - note that even though it's a little weird, - // it's possible to have bins without data, if there's inferred data - var binstrt = Plotly.Lib.coerce(traceIn, traceOut, histogram.attributes, binDirection + 'bins.start'), - binend = Plotly.Lib.coerce(traceIn, traceOut, histogram.attributes, binDirection + 'bins.end'), - autobin = binstrt && binend ? - coerce('autobin' + binDirection, false) : - coerce('autobin' + binDirection); - - if(autobin) coerce('nbins' + binDirection); - else coerce(binDirection + 'bins.size'); - }); -}; - -var binFunctions = { - count: function(n, i, size) { - size[n]++; - return 1; - }, - - sum: function(n, i, size, counterData) { - var v = counterData[i]; - if(isNumeric(v)) { - v = Number(v); - size[n] += v; - return v; - } - return 0; - }, - - avg: function(n, i, size, counterData, counts) { - var v = counterData[i]; - if(isNumeric(v)) { - v = Number(v); - size[n] += v; - counts[n]++; - } - return 0; - }, - - min: function(n, i, size, counterData) { - var v = counterData[i]; - if(isNumeric(v)) { - v = Number(v); - if(!isNumeric(size[n])) { - size[n] = v; - return v; - } - else if(size[n]>v) { - size[n] = v; - return v-size[n]; - } - } - return 0; - }, - - max: function(n, i, size, counterData) { - var v = counterData[i]; - if(isNumeric(v)) { - v = Number(v); - if(!isNumeric(size[n])) { - size[n] = v; - return v; - } - else if(size[n]setstyles - var pos = [], - size = [], - i, - pa = Plotly.Axes.getFromId(gd, - trace.orientation==='h' ? (trace.yaxis || 'y') : (trace.xaxis || 'x')), - maindata = trace.orientation==='h' ? 'y' : 'x', - counterdata = {x: 'y', y: 'x'}[maindata]; - - // prepare the raw data - var pos0 = pa.makeCalcdata(trace, maindata); - // calculate the bins - if((trace['autobin' + maindata]!==false) || !(maindata + 'bins' in trace)) { - trace[maindata + 'bins'] = Plotly.Axes.autoBin(pos0, pa, trace['nbins' + maindata]); - - // copy bin info back to the source data. - trace._input[maindata + 'bins'] = trace[maindata + 'bins']; - } - - var binspec = trace[maindata + 'bins'], - allbins = typeof binspec.size === 'string', - bins = allbins ? [] : binspec, - // make the empty bin array - i2, - binend, - n, - inc = [], - counts = [], - total = 0, - norm = trace.histnorm, - func = trace.histfunc, - densitynorm = norm.indexOf('density')!==-1, - extremefunc = func==='max' || func==='min', - sizeinit = extremefunc ? null : 0, - binfunc = binFunctions.count, - normfunc = normFunctions[norm], - doavg = false, - rawCounterData; - - if(Array.isArray(trace[counterdata]) && func!=='count') { - rawCounterData = trace[counterdata]; - doavg = func==='avg'; - binfunc = binFunctions[func]; - } - - // create the bins (and any extra arrays needed) - // assume more than 5000 bins is an error, so we don't crash the browser - i = binspec.start; - // decrease end a little in case of rounding errors - binend = binspec.end + - (binspec.start - Plotly.Axes.tickIncrement(binspec.start, binspec.size)) / 1e6; - while(i=0 && nfirstNonzero; i--) { - if(size[i]) { - lastNonzero = i; - break; - } - } - - // create the "calculated data" to plot - for(i=firstNonzero; i<=lastNonzero; i++) { - if((isNumeric(pos[i]) && isNumeric(size[i]))) { - cd.push({p: pos[i], s: size[i], b: 0}); - } - } - - return cd; -}; histogram.calc2d = function(gd, trace) { var xa = Plotly.Axes.getFromId(gd, trace.xaxis||'x'), diff --git a/src/traces/histogram/norm_functions.js b/src/traces/histogram/norm_functions.js new file mode 100644 index 00000000000..6a27e7e47b1 --- /dev/null +++ b/src/traces/histogram/norm_functions.js @@ -0,0 +1,33 @@ +/** +* Copyright 2012-2015, 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'; + + +module.exports = { + percent: function(size, total) { + var nMax = size.length, + norm = 100/total; + for(var n=0; n Date: Wed, 16 Dec 2015 15:39:48 -0500 Subject: [PATCH 07/28] mv histogram calc2d to histogram2d/ --- src/traces/histogram/index.js | 149 ----------------------------- src/traces/histogram2d/calc.js | 168 +++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 149 deletions(-) create mode 100644 src/traces/histogram2d/calc.js diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js index 6eb6928001b..45343a9c168 100644 --- a/src/traces/histogram/index.js +++ b/src/traces/histogram/index.js @@ -35,152 +35,3 @@ var Plotly = require('../../plotly'); }); -histogram.calc2d = function(gd, trace) { - var xa = Plotly.Axes.getFromId(gd, trace.xaxis||'x'), - x = trace.x ? xa.makeCalcdata(trace, 'x') : [], - ya = Plotly.Axes.getFromId(gd, trace.yaxis||'y'), - y = trace.y ? ya.makeCalcdata(trace, 'y') : [], - x0, - dx, - y0, - dy, - z, - i; - - var serieslen = Math.min(x.length, y.length); - if(x.length>serieslen) x.splice(serieslen, x.length-serieslen); - if(y.length>serieslen) y.splice(serieslen, y.length-serieslen); - - Plotly.Lib.markTime('done convert data'); - - // calculate the bins - if(trace.autobinx || !('xbins' in trace)) { - trace.xbins = Plotly.Axes.autoBin(x, xa, trace.nbinsx, '2d'); - if(trace.type==='histogram2dcontour') { - trace.xbins.start -= trace.xbins.size; - trace.xbins.end += trace.xbins.size; - } - - // copy bin info back to the source data. - trace._input.xbins = trace.xbins; - } - if(trace.autobiny || !('ybins' in trace)) { - trace.ybins = Plotly.Axes.autoBin(y,ya,trace.nbinsy,'2d'); - if(trace.type==='histogram2dcontour') { - trace.ybins.start -= trace.ybins.size; - trace.ybins.end += trace.ybins.size; - } - trace._input.ybins = trace.ybins; - } - Plotly.Lib.markTime('done autoBin'); - - // make the empty bin array & scale the map - z = []; - var onecol = [], - zerocol = [], - xbins = (typeof(trace.xbins.size)==='string') ? [] : trace.xbins, - ybins = (typeof(trace.xbins.size)==='string') ? [] : trace.ybins, - total = 0, - n, - m, - counts=[], - norm = trace.histnorm, - func = trace.histfunc, - densitynorm = (norm.indexOf('density')!==-1), - extremefunc = (func==='max' || func==='min'), - sizeinit = (extremefunc ? null : 0), - binfunc = binFunctions.count, - normfunc = normFunctions[norm], - doavg = false, - xinc = [], - yinc = []; - - // set a binning function other than count? - // for binning functions: check first for 'z', - // then 'mc' in case we had a colored scatter plot - // and want to transfer these colors to the 2D histo - // TODO: this is why we need a data picker in the popover... - var rawCounterData = ('z' in trace) ? - trace.z : - (('marker' in trace && Array.isArray(trace.marker.color)) ? - trace.marker.color : ''); - if(rawCounterData && func!=='count') { - doavg = func==='avg'; - binfunc = binFunctions[func]; - } - - // decrease end a little in case of rounding errors - var binspec = trace.xbins, - binend = binspec.end + - (binspec.start - Plotly.Axes.tickIncrement(binspec.start, binspec.size)) / 1e6; - - for(i=binspec.start; i=0 && n=0 && mserieslen) x.splice(serieslen, x.length-serieslen); + if(y.length>serieslen) y.splice(serieslen, y.length-serieslen); + + Lib.markTime('done convert data'); + + // calculate the bins + if(trace.autobinx || !('xbins' in trace)) { + trace.xbins = Plotly.Axes.autoBin(x, xa, trace.nbinsx, '2d'); + if(trace.type==='histogram2dcontour') { + trace.xbins.start -= trace.xbins.size; + trace.xbins.end += trace.xbins.size; + } + + // copy bin info back to the source data. + trace._input.xbins = trace.xbins; + } + if(trace.autobiny || !('ybins' in trace)) { + trace.ybins = Plotly.Axes.autoBin(y,ya,trace.nbinsy,'2d'); + if(trace.type==='histogram2dcontour') { + trace.ybins.start -= trace.ybins.size; + trace.ybins.end += trace.ybins.size; + } + trace._input.ybins = trace.ybins; + } + Lib.markTime('done autoBin'); + + // make the empty bin array & scale the map + z = []; + var onecol = [], + zerocol = [], + xbins = (typeof(trace.xbins.size)==='string') ? [] : trace.xbins, + ybins = (typeof(trace.xbins.size)==='string') ? [] : trace.ybins, + total = 0, + n, + m, + counts=[], + norm = trace.histnorm, + func = trace.histfunc, + densitynorm = (norm.indexOf('density')!==-1), + extremefunc = (func==='max' || func==='min'), + sizeinit = (extremefunc ? null : 0), + binfunc = binFunctions.count, + normfunc = normFunctions[norm], + doavg = false, + xinc = [], + yinc = []; + + // set a binning function other than count? + // for binning functions: check first for 'z', + // then 'mc' in case we had a colored scatter plot + // and want to transfer these colors to the 2D histo + // TODO: this is why we need a data picker in the popover... + var rawCounterData = ('z' in trace) ? + trace.z : + (('marker' in trace && Array.isArray(trace.marker.color)) ? + trace.marker.color : ''); + if(rawCounterData && func!=='count') { + doavg = func==='avg'; + binfunc = binFunctions[func]; + } + + // decrease end a little in case of rounding errors + var binspec = trace.xbins, + binend = binspec.end + + (binspec.start - Plotly.Axes.tickIncrement(binspec.start, binspec.size)) / 1e6; + + for(i=binspec.start; i=0 && n=0 && m Date: Wed, 16 Dec 2015 15:40:37 -0500 Subject: [PATCH 08/28] register histogram module, require parts for Bars in it. --- src/traces/histogram/index.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js index 45343a9c168..aae23b5fa84 100644 --- a/src/traces/histogram/index.js +++ b/src/traces/histogram/index.js @@ -23,6 +23,8 @@ var Plotly = require('../../plotly'); * constant and % work but they're not so meaningful. I guess it could be cool * to allow quadrature combination of errors in summed histograms... */ + +Plotly.Plots.register(exports, 'histogram', ['cartesian', 'bar', 'histogram', 'oriented', 'errorBarsOK', 'showLegend'], { description: [ 'The sample data from which statistics are computed is set in `x`', @@ -34,4 +36,22 @@ var Plotly = require('../../plotly'); ].join(' ') }); +exports.attributes = require('./attributes'); + +exports.layoutAttributes = require('../bars/layout_attributes'); + +exports.supplyDefaults = require('../bars/defaults'); + +exports.supplyLayoutDefaults = require('../bars/layout_defaults'); + +exports.calc = require('./calc'); + +exports.setPositions = require('../bars/set_positions'); + +exports.plot = require('../bars/plot'); + +exports.style = require('../bars/style'); + +exports.colorbar = require('../scatter/colorbar'); +exports.hoverPoints = require('../bars/hover'); From ab7ff1dda48c98a90785f9fab2bcd2b6c9104a98 Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 15:41:22 -0500 Subject: [PATCH 09/28] register histogram2d module, require parts of heatmap in it. --- src/traces/histogram2d/index.js | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/traces/histogram2d/index.js diff --git a/src/traces/histogram2d/index.js b/src/traces/histogram2d/index.js new file mode 100644 index 00000000000..c86ee285125 --- /dev/null +++ b/src/traces/histogram2d/index.js @@ -0,0 +1,39 @@ +/** +* Copyright 2012-2015, 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 Plotly = require('../../plotly'); + +Plotly.Plots.register(exports, 'histogram2d', + ['cartesian', '2dMap', 'histogram'], { + hrName: 'histogram_2d', + description: [ + 'The sample data from which statistics are computed is set in `x`', + 'and `y` (where `x` and `y` represent marginal distributions,', + 'binning is set in `xbins` and `ybins` in this case)', + 'or `z` (where `z` represent the 2D distribution and binning set,', + 'binning is set by `x` and `y` in this case).', + 'The resulting distribution is visualized as a heatmap.' + ].join(' ') +}); + +exports.attributes = require('../heatmap/attributes'); + +exports.supplyDefaults = require('../heatmap/defaults'); + +exports.calc = require('../heatmap/calc'); + +exports.plot = require('../heatmap/plot'); + +exports.colorbar = require('../heatmap/colorbar'); + +exports.style = require('../heatmap/style'); + +exports.hoverPoints = require('../heatmap/hover'); From 1a909686765cdaa15f76f90dc577602986b0de46 Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 15:41:47 -0500 Subject: [PATCH 10/28] register histogram2dcontour, require pars of contour in it. --- src/traces/histogram2dcontour/index.js | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/traces/histogram2dcontour/index.js diff --git a/src/traces/histogram2dcontour/index.js b/src/traces/histogram2dcontour/index.js new file mode 100644 index 00000000000..6d97e38d9e6 --- /dev/null +++ b/src/traces/histogram2dcontour/index.js @@ -0,0 +1,39 @@ +/** +* Copyright 2012-2015, 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 Plotly = require('../../plotly'); + +Plotly.Plots.register(exports, 'histogram2dcontour', + ['cartesian', '2dMap', 'contour', 'histogram'], { + hrName: 'histogram_2d_contour', + description: [ + 'The sample data from which statistics are computed is set in `x`', + 'and `y` (where `x` and `y` represent marginal distributions,', + 'binning is set in `xbins` and `ybins` in this case)', + 'or `z` (where `z` represent the 2D distribution and binning set,', + 'binning is set by `x` and `y` in this case).', + 'The resulting distribution is visualized as a contour plot.' + ].join(' ') +}); + +exports.attributes = require('../contour/attributes'); + +exports.supplyDefaults = require('../contour/defaults'); + +exports.calc = require('../contour/calc'); + +exports.plot = require('../contour/plot'); + +exports.style = require('../contour/style'); + +exports.colorbar = require('../contour/colorbar'); + +exports.hoverPoints = require('../contour/hover'); From 2f3bd84d9152c74fc8bc3a79bc8495ef4a6a17a9 Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 15:43:30 -0500 Subject: [PATCH 11/28] require histogram2d/calc in heatmap/calc, - histogram2d and heatmap share the same calc function for now --- src/traces/heatmap/calc.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/traces/heatmap/calc.js b/src/traces/heatmap/calc.js index 6ddebcb4797..d2f4b7a7e80 100644 --- a/src/traces/heatmap/calc.js +++ b/src/traces/heatmap/calc.js @@ -14,6 +14,7 @@ var isNumeric = require('fast-isnumeric'); var Plotly = require('../../plotly'); var Lib = require('../../lib'); +var histogram2dCalc = require('../histogram2d/calc'); var hasColumns = require('./has_columns'); var convertColumnXYZ = require('./convert_column_xyz'); var maxRowLength = require('./max_row_length'); @@ -45,7 +46,7 @@ module.exports = function calc(gd, trace) { Lib.markTime('done convert x&y'); if(isHist) { - var binned = Plotly.Histogram.calc2d(gd, trace); + var binned = histogram2dCalc(gd, trace); x = binned.x; x0 = binned.x0; dx = binned.dx; From bb542abc96965234461338b532a09c8b37c87d59 Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 15:44:34 -0500 Subject: [PATCH 12/28] require histogram/defaults in heatmap/defaults - histogram2d and heatmap share the same supplyDefaults step --- src/traces/heatmap/defaults.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/traces/heatmap/defaults.js b/src/traces/heatmap/defaults.js index b2129035589..0900cebc0d7 100644 --- a/src/traces/heatmap/defaults.js +++ b/src/traces/heatmap/defaults.js @@ -11,11 +11,11 @@ var isNumeric = require('fast-isnumeric'); -var Plotly = require('../../plotly'); var Plots = require('../../plots/plots'); var Lib = require('../../lib'); var Colorscale = require('../../components/colorscale'); +var histogramSupplyDefaults = require('../histogram/defaults'); var attributes = require('./attributes'); var hasColumns = require('./has_columns'); @@ -33,7 +33,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout // x, y, z, marker.color, and x0, dx, y0, dy are coerced // in Histogram.supplyDefaults // (along with histogram-specific attributes) - Plotly.Histogram.supplyDefaults(traceIn, traceOut); + histogramSupplyDefaults(traceIn, traceOut); if(traceOut.visible === false) return; } else { From 0d6fecc787ecaa1eedf37370d62899713566908e Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 15:45:03 -0500 Subject: [PATCH 13/28] require xy defaults + errorbar defaults in scattergl defaults --- src/traces/scattergl/defaults.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/traces/scattergl/defaults.js b/src/traces/scattergl/defaults.js index 3a235b6188a..414624d693f 100644 --- a/src/traces/scattergl/defaults.js +++ b/src/traces/scattergl/defaults.js @@ -9,18 +9,20 @@ 'use strict'; -var Plotly = require('../../plotly'); -var ScatterGl = require('./'); +var Lib = require('../../lib'); +var Scatter = require('../scatter'); +var handleXYDefaults = require('../scatter/xy_defaults'); +var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); +var attributes = require('./attributes'); -module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - var Scatter = Plotly.Scatter; +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { - return Plotly.Lib.coerce(traceIn, traceOut, ScatterGl.attributes, attr, dflt); + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var len = Scatter.handleXYDefaults(traceIn, traceOut, coerce); + var len = handleXYDefaults(traceIn, traceOut, coerce); if(!len) { traceOut.visible = false; return; @@ -42,6 +44,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout Scatter.fillColorDefaults(traceIn, traceOut, defaultColor, coerce); } - Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); - Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); + errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); + errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); }; From 67ae5aa811e96a227c9bdaebd4823c74ecd27a12 Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 15:45:17 -0500 Subject: [PATCH 14/28] adapt jasmine tests --- test/jasmine/tests/bars_test.js | 4 ++-- test/jasmine/tests/histogram_test.js | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/test/jasmine/tests/bars_test.js b/test/jasmine/tests/bars_test.js index c5115726070..e86aa58e2ff 100644 --- a/test/jasmine/tests/bars_test.js +++ b/test/jasmine/tests/bars_test.js @@ -1,4 +1,4 @@ -var Plotly = require('@src/plotly'); +var Bars = require('@src/traces/bars'); describe('Test bars', function () { 'use strict'; @@ -9,7 +9,7 @@ describe('Test bars', function () { var defaultColor = '#444'; - var supplyDefaults = Plotly.Bars.supplyDefaults; + var supplyDefaults = Bars.supplyDefaults; beforeEach(function() { traceOut = {}; diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index 796de42a837..80600680aa7 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -1,4 +1,4 @@ -var Plotly = require('@src/plotly'); +var supplyDefaults = require('@src/traces/histogram/defaults'); describe('Test histogram', function () { 'use strict'; @@ -7,8 +7,6 @@ describe('Test histogram', function () { var traceIn, traceOut; - var supplyDefaults = Plotly.Histogram.supplyDefaults; - beforeEach(function() { traceOut = {}; }); From 1d18222d54c86d96a05c113e1ab2155b05a5b50b Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 15:46:16 -0500 Subject: [PATCH 15/28] require in new trace modules, - so that the new trace modules are registered via Plots.register --- src/plotly.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plotly.js b/src/plotly.js index 368af2d31a9..c3c9b1cdf49 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -61,6 +61,8 @@ exports.Bars = require('./traces/bars'); exports.Boxes = require('./traces/boxes'); exports.Heatmap = require('./traces/heatmap'); exports.Histogram = require('./traces/histogram'); +exports.Histogram2d = require('./traces/histogram2d'); +exports.Histogram2dContour = require('./traces/histogram2dcontour'); exports.Pie = require('./traces/pie'); exports.Contour = require('./traces/contour'); exports.Scatter3D = require('./traces/scatter3d'); From c42a6d182cbb6cd5e754c5d4950cd5ca7e66bb83 Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 16:23:42 -0500 Subject: [PATCH 16/28] rm trailing lines --- src/traces/bars/set_positions.js | 1 - src/traces/heatmap/convert_column_xyz.js | 2 -- src/traces/heatmap/style.js | 1 - 3 files changed, 4 deletions(-) diff --git a/src/traces/bars/set_positions.js b/src/traces/bars/set_positions.js index 217886dd3ce..f9411cd23cf 100644 --- a/src/traces/bars/set_positions.js +++ b/src/traces/bars/set_positions.js @@ -198,4 +198,3 @@ module.exports = function setPositions(gd, plotinfo) { } }); }; - diff --git a/src/traces/heatmap/convert_column_xyz.js b/src/traces/heatmap/convert_column_xyz.js index 0b884d399a0..769d5821b5e 100644 --- a/src/traces/heatmap/convert_column_xyz.js +++ b/src/traces/heatmap/convert_column_xyz.js @@ -53,5 +53,3 @@ module.exports = function convertColumnXYZ(trace, xa, ya) { trace.z = z; if(hasColumnText) trace.text = text; }; - - diff --git a/src/traces/heatmap/style.js b/src/traces/heatmap/style.js index 07ec3645d2a..8db38832257 100644 --- a/src/traces/heatmap/style.js +++ b/src/traces/heatmap/style.js @@ -17,4 +17,3 @@ module.exports = function style(gd) { return d.trace.opacity; }); }; - From 1f0b92df0342f00d9f362f56f638b69a494c2806 Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 17:15:36 -0500 Subject: [PATCH 17/28] rename bars / Bars --> bar / Bar consistently with trace type --- src/plotly.js | 2 +- src/traces/{bars => bar}/arrays_to_calcdata.js | 0 src/traces/{bars => bar}/attributes.js | 0 src/traces/{bars => bar}/calc.js | 0 src/traces/{bars => bar}/defaults.js | 0 src/traces/{bars => bar}/hover.js | 0 src/traces/{bars => bar}/index.js | 0 src/traces/{bars => bar}/layout_attributes.js | 0 src/traces/{bars => bar}/layout_defaults.js | 0 src/traces/{bars => bar}/plot.js | 0 src/traces/{bars => bar}/set_positions.js | 0 src/traces/{bars => bar}/style.js | 0 src/traces/histogram/attributes.js | 2 +- src/traces/histogram/index.js | 18 +++++++++--------- .../tests/{bars_test.js => bar_test.js} | 6 +++--- test/jasmine/tests/plot_api_test.js | 8 ++++---- 16 files changed, 18 insertions(+), 18 deletions(-) rename src/traces/{bars => bar}/arrays_to_calcdata.js (100%) rename src/traces/{bars => bar}/attributes.js (100%) rename src/traces/{bars => bar}/calc.js (100%) rename src/traces/{bars => bar}/defaults.js (100%) rename src/traces/{bars => bar}/hover.js (100%) rename src/traces/{bars => bar}/index.js (100%) rename src/traces/{bars => bar}/layout_attributes.js (100%) rename src/traces/{bars => bar}/layout_defaults.js (100%) rename src/traces/{bars => bar}/plot.js (100%) rename src/traces/{bars => bar}/set_positions.js (100%) rename src/traces/{bars => bar}/style.js (100%) rename test/jasmine/tests/{bars_test.js => bar_test.js} (92%) diff --git a/src/plotly.js b/src/plotly.js index c3c9b1cdf49..c01c2d438eb 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -57,7 +57,7 @@ exports.ModeBar = require('./components/modebar'); // traces exports.Scatter = require('./traces/scatter'); -exports.Bars = require('./traces/bars'); +exports.Bar = require('./traces/bar'); exports.Boxes = require('./traces/boxes'); exports.Heatmap = require('./traces/heatmap'); exports.Histogram = require('./traces/histogram'); diff --git a/src/traces/bars/arrays_to_calcdata.js b/src/traces/bar/arrays_to_calcdata.js similarity index 100% rename from src/traces/bars/arrays_to_calcdata.js rename to src/traces/bar/arrays_to_calcdata.js diff --git a/src/traces/bars/attributes.js b/src/traces/bar/attributes.js similarity index 100% rename from src/traces/bars/attributes.js rename to src/traces/bar/attributes.js diff --git a/src/traces/bars/calc.js b/src/traces/bar/calc.js similarity index 100% rename from src/traces/bars/calc.js rename to src/traces/bar/calc.js diff --git a/src/traces/bars/defaults.js b/src/traces/bar/defaults.js similarity index 100% rename from src/traces/bars/defaults.js rename to src/traces/bar/defaults.js diff --git a/src/traces/bars/hover.js b/src/traces/bar/hover.js similarity index 100% rename from src/traces/bars/hover.js rename to src/traces/bar/hover.js diff --git a/src/traces/bars/index.js b/src/traces/bar/index.js similarity index 100% rename from src/traces/bars/index.js rename to src/traces/bar/index.js diff --git a/src/traces/bars/layout_attributes.js b/src/traces/bar/layout_attributes.js similarity index 100% rename from src/traces/bars/layout_attributes.js rename to src/traces/bar/layout_attributes.js diff --git a/src/traces/bars/layout_defaults.js b/src/traces/bar/layout_defaults.js similarity index 100% rename from src/traces/bars/layout_defaults.js rename to src/traces/bar/layout_defaults.js diff --git a/src/traces/bars/plot.js b/src/traces/bar/plot.js similarity index 100% rename from src/traces/bars/plot.js rename to src/traces/bar/plot.js diff --git a/src/traces/bars/set_positions.js b/src/traces/bar/set_positions.js similarity index 100% rename from src/traces/bars/set_positions.js rename to src/traces/bar/set_positions.js diff --git a/src/traces/bars/style.js b/src/traces/bar/style.js similarity index 100% rename from src/traces/bars/style.js rename to src/traces/bar/style.js diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js index ed5a6c54c71..54ee5e64452 100644 --- a/src/traces/histogram/attributes.js +++ b/src/traces/histogram/attributes.js @@ -9,7 +9,7 @@ 'use strict'; -var barAttrs = require('../bars/attributes'); +var barAttrs = require('../bar/attributes'); module.exports = { diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js index aae23b5fa84..391e96c77f3 100644 --- a/src/traces/histogram/index.js +++ b/src/traces/histogram/index.js @@ -13,8 +13,8 @@ var Plotly = require('../../plotly'); /** * Histogram has its own calc function, - * but uses Bars.plot to display - * and Bars.setPositions for stacking and grouping + * but uses bar's plot to display + * and bar's setPositions for stacking and grouping */ /** @@ -38,20 +38,20 @@ Plotly.Plots.register(exports, 'histogram', exports.attributes = require('./attributes'); -exports.layoutAttributes = require('../bars/layout_attributes'); +exports.layoutAttributes = require('../bar/layout_attributes'); -exports.supplyDefaults = require('../bars/defaults'); +exports.supplyDefaults = require('../bar/defaults'); -exports.supplyLayoutDefaults = require('../bars/layout_defaults'); +exports.supplyLayoutDefaults = require('../bar/layout_defaults'); exports.calc = require('./calc'); -exports.setPositions = require('../bars/set_positions'); +exports.setPositions = require('../bar/set_positions'); -exports.plot = require('../bars/plot'); +exports.plot = require('../bar/plot'); -exports.style = require('../bars/style'); +exports.style = require('../bar/style'); exports.colorbar = require('../scatter/colorbar'); -exports.hoverPoints = require('../bars/hover'); +exports.hoverPoints = require('../bar/hover'); diff --git a/test/jasmine/tests/bars_test.js b/test/jasmine/tests/bar_test.js similarity index 92% rename from test/jasmine/tests/bars_test.js rename to test/jasmine/tests/bar_test.js index e86aa58e2ff..72621ff3d37 100644 --- a/test/jasmine/tests/bars_test.js +++ b/test/jasmine/tests/bar_test.js @@ -1,6 +1,6 @@ -var Bars = require('@src/traces/bars'); +var Bar = require('@src/traces/bar'); -describe('Test bars', function () { +describe('Test bar', function () { 'use strict'; describe('supplyDefaults', function() { @@ -9,7 +9,7 @@ describe('Test bars', function () { var defaultColor = '#444'; - var supplyDefaults = Bars.supplyDefaults; + var supplyDefaults = Bar.supplyDefaults; beforeEach(function() { traceOut = {}; diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 68ff9cd1e02..700c5d8523c 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -8,7 +8,7 @@ describe('Test graph_obj', function () { spyOn(Plotly.Plots, 'previousPromises'); spyOn(Plotly, 'plot'); spyOn(Plotly.Scatter, 'arraysToCalcdata'); - spyOn(Plotly.Bars, 'arraysToCalcdata'); + spyOn(Plotly.Bar, 'arraysToCalcdata'); spyOn(Plotly.Plots, 'style'); spyOn(Plotly.Legend, 'draw'); }); @@ -28,14 +28,14 @@ describe('Test graph_obj', function () { mockDefaultsAndCalc(gd); Plotly.restyle(gd, {'marker.color': 'red'}); expect(Plotly.Scatter.arraysToCalcdata).toHaveBeenCalled(); - expect(Plotly.Bars.arraysToCalcdata).not.toHaveBeenCalled(); + expect(Plotly.Bar.arraysToCalcdata).not.toHaveBeenCalled(); expect(Plotly.Plots.style).toHaveBeenCalled(); expect(Plotly.plot).not.toHaveBeenCalled(); // "docalc" deletes gd.calcdata - make sure this didn't happen expect(gd.calcdata).toBeDefined(); }); - it('calls Bars.arraysToCalcdata and Plots.style on bar styling', function() { + it('calls Bar.arraysToCalcdata and Plots.style on bar styling', function() { var gd = { data: [{x: [1,2,3], y: [1,2,3], type: 'bar'}], layout: {} @@ -43,7 +43,7 @@ describe('Test graph_obj', function () { mockDefaultsAndCalc(gd); Plotly.restyle(gd, {'marker.color': 'red'}); expect(Plotly.Scatter.arraysToCalcdata).not.toHaveBeenCalled(); - expect(Plotly.Bars.arraysToCalcdata).toHaveBeenCalled(); + expect(Plotly.Bar.arraysToCalcdata).toHaveBeenCalled(); expect(Plotly.Plots.style).toHaveBeenCalled(); expect(Plotly.plot).not.toHaveBeenCalled(); expect(gd.calcdata).toBeDefined(); From db469d895ec778248ce499373bd4e2c80d314e6a Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 17:16:27 -0500 Subject: [PATCH 18/28] rename boxes / Boxes --> box / Box consistently with trace type --- src/plotly.js | 2 +- src/traces/{boxes => box}/attributes.js | 0 src/traces/{boxes => box}/index.js | 0 src/traces/{boxes => box}/layout_attributes.js | 0 test/jasmine/tests/{boxes_test.js => box_test.js} | 4 ++-- 5 files changed, 3 insertions(+), 3 deletions(-) rename src/traces/{boxes => box}/attributes.js (100%) rename src/traces/{boxes => box}/index.js (100%) rename src/traces/{boxes => box}/layout_attributes.js (100%) rename test/jasmine/tests/{boxes_test.js => box_test.js} (95%) diff --git a/src/plotly.js b/src/plotly.js index c01c2d438eb..34e05d7e14e 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -58,7 +58,7 @@ exports.ModeBar = require('./components/modebar'); // traces exports.Scatter = require('./traces/scatter'); exports.Bar = require('./traces/bar'); -exports.Boxes = require('./traces/boxes'); +exports.Box = require('./traces/box'); exports.Heatmap = require('./traces/heatmap'); exports.Histogram = require('./traces/histogram'); exports.Histogram2d = require('./traces/histogram2d'); diff --git a/src/traces/boxes/attributes.js b/src/traces/box/attributes.js similarity index 100% rename from src/traces/boxes/attributes.js rename to src/traces/box/attributes.js diff --git a/src/traces/boxes/index.js b/src/traces/box/index.js similarity index 100% rename from src/traces/boxes/index.js rename to src/traces/box/index.js diff --git a/src/traces/boxes/layout_attributes.js b/src/traces/box/layout_attributes.js similarity index 100% rename from src/traces/boxes/layout_attributes.js rename to src/traces/box/layout_attributes.js diff --git a/test/jasmine/tests/boxes_test.js b/test/jasmine/tests/box_test.js similarity index 95% rename from test/jasmine/tests/boxes_test.js rename to test/jasmine/tests/box_test.js index 5821a519064..76acc2fb0be 100644 --- a/test/jasmine/tests/boxes_test.js +++ b/test/jasmine/tests/box_test.js @@ -1,4 +1,4 @@ -var Plotly = require('@src/plotly'); +var Box = require('@src/traces/box'); describe('Test boxes', function () { 'use strict'; @@ -9,7 +9,7 @@ describe('Test boxes', function () { var defaultColor = '#444'; - var supplyDefaults = Plotly.Boxes.supplyDefaults; + var supplyDefaults = Box.supplyDefaults; beforeEach(function() { traceOut = {}; From b043ec3a311af0d39cad34e1ed7883db9a9febe6 Mon Sep 17 00:00:00 2001 From: etpinard Date: Wed, 16 Dec 2015 17:16:55 -0500 Subject: [PATCH 19/28] update layout module definition with Bar and Box --- src/plots/plots.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index f83e424769b..45c6177f608 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -653,7 +653,7 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData) { var moduleLayoutDefaults = [ 'Axes', 'Annotations', 'Shapes', 'Fx', - 'Bars', 'Boxes', 'Gl3dLayout', 'GeoLayout', 'Pie', 'Legend' + 'Bar', 'Box', 'Gl3dLayout', 'GeoLayout', 'Pie', 'Legend' ]; var i, module; From de92572c26f19ec209829fc1c955223a3a25b4f7 Mon Sep 17 00:00:00 2001 From: etpinard Date: Thu, 17 Dec 2015 12:08:53 -0500 Subject: [PATCH 20/28] make bar and histogram have distinct attribute object, - no more 'composed module' logic. --- src/traces/bar/attributes.js | 5 +-- src/traces/histogram/attributes.js | 52 +++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/traces/bar/attributes.js b/src/traces/bar/attributes.js index 6e1373cd2b5..9bebf75a3c5 100644 --- a/src/traces/bar/attributes.js +++ b/src/traces/bar/attributes.js @@ -51,12 +51,9 @@ module.exports = { } }, - r: scatterAttrs.r, // FIXME this shouldn't get included in 'histogram' + r: scatterAttrs.r, t: scatterAttrs.t, - _composedModules: { // composed module coupling - 'histogram': 'Histogram' - }, _nestedModules: { // nested module coupling 'error_y': 'ErrorBars', 'error_x': 'ErrorBars', diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js index 54ee5e64452..2622590527d 100644 --- a/src/traces/histogram/attributes.js +++ b/src/traces/histogram/attributes.js @@ -10,6 +10,10 @@ 'use strict'; var barAttrs = require('../bar/attributes'); +var extendFlat = require('../../lib').extendFlat; + +var barMarkerAttrs = barAttrs.marker; +var barMarkerLineAttrs = barMarkerAttrs.line; module.exports = { @@ -25,17 +29,10 @@ module.exports = { 'Sets the sample data to be binned on the y axis.' ].join(' ') }, - z: { - valType: 'data_array', - description: 'Sets the aggregation data.' - }, - marker: { - color: { // FIXME this overrides 'bar' - valType: 'data_array', - arrayOk: undefined - } - }, + + text: barAttrs.text, orientation: barAttrs.orientation, + histfunc: { valType: 'enumerated', values: ['count', 'sum', 'avg', 'min', 'max'], @@ -78,6 +75,7 @@ module.exports = { '(here, the sum of all bin area equals 1).' ].join(' ') }, + autobinx: { valType: 'boolean', dflt: true, @@ -95,6 +93,7 @@ module.exports = { description: 'Sets the number of x axis bins.' }, xbins: makeBinsAttr('x'), + autobiny: { valType: 'boolean', dflt: true, @@ -111,7 +110,38 @@ module.exports = { role: 'style', description: 'Sets the number of y axis bins.' }, - ybins: makeBinsAttr('y') + ybins: makeBinsAttr('y'), + + marker: { + color: barMarkerAttrs.color, + colorscale: barMarkerAttrs.colorscale, + cauto: barMarkerAttrs.cauto, + cmax: barMarkerAttrs.cmax, + cmin: barMarkerAttrs.cmin, + autocolorscale: barMarkerAttrs.autocolorscale, + reversescale: barMarkerAttrs.reversescale, + showscale: barMarkerAttrs.showscale, + line: { + color: barMarkerLineAttrs.color, + colorscale: barMarkerLineAttrs.colorscale, + cauto: barMarkerLineAttrs.cauto, + cmax: barMarkerLineAttrs.cmax, + cmin: barMarkerLineAttrs.cmin, + autocolorscale: barMarkerLineAttrs.autocolorscale, + reversescale: barMarkerLineAttrs.reversescale, + width: extendFlat({}, barMarkerLineAttrs.width, {dflt: 0}) + } + }, + + _nestedModules: { + 'error_y': 'ErrorBars', + 'error_x': 'ErrorBars', + 'marker.colorbar': 'Colorbar' + }, + + _deprecated: { + bardir: barAttrs._deprecated.bardir + } }; function makeBinsAttr(axLetter) { From 9c90e0a625a4722a081e3892f8e6f568671f7cac Mon Sep 17 00:00:00 2001 From: etpinard Date: Thu, 17 Dec 2015 12:09:35 -0500 Subject: [PATCH 21/28] make contour and heatmap have distinct attribute objects --- src/traces/contour/attributes.js | 34 +++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/traces/contour/attributes.js b/src/traces/contour/attributes.js index 71c7e8192d2..2e83c2fc9b2 100644 --- a/src/traces/contour/attributes.js +++ b/src/traces/contour/attributes.js @@ -7,16 +7,35 @@ */ +var heatmapAttrs = require('../heatmap/attributes'); var scatterAttrs = require('../scatter/attributes'); var extendFlat = require('../../lib/extend').extendFlat; var scatterLineAttrs = scatterAttrs.line; module.exports = { - _composedModules: { // composed module coupling - 'contour': 'Heatmap', - 'histogram2dcontour': 'Heatmap' - }, + z: heatmapAttrs.z, + x: heatmapAttrs.x, + x0: heatmapAttrs.x0, + dx: heatmapAttrs.dx, + y: heatmapAttrs.y, + y0: heatmapAttrs.y0, + dy: heatmapAttrs.dy, + text: heatmapAttrs.text, + transpose: heatmapAttrs.transpose, + xtype: heatmapAttrs.xtype, + ytype: heatmapAttrs.ytype, + + zauto: heatmapAttrs.zauto, + zmin: heatmapAttrs.zmin, + zmax: heatmapAttrs.zmax, + colorscale: heatmapAttrs.colorscale, + autocolorscale: heatmapAttrs.autocolorscale, + reversescale: heatmapAttrs.reversescale, + showscale: heatmapAttrs.showscale, + + connectgaps: heatmapAttrs.connectgaps, + autocontour: { valType: 'boolean', dflt: true, @@ -34,6 +53,7 @@ module.exports = { role: 'style', description: 'Sets the number of contour levels.' }, + contours: { start: { valType: 'number', @@ -77,6 +97,7 @@ module.exports = { ].join(' ') } }, + line: { color: extendFlat({}, scatterLineAttrs.color, { description: [ @@ -92,6 +113,9 @@ module.exports = { 'where *0* corresponds to no smoothing.' ].join(' ') }) + }, + + _nestedModules: { + 'colorbar': 'Colorbar' } }; - From f4f1a9315612d85e0a2e7745781401d8fe61826d Mon Sep 17 00:00:00 2001 From: etpinard Date: Thu, 17 Dec 2015 12:10:07 -0500 Subject: [PATCH 22/28] make histogram2d and histogram2dcontour have distinct attribute objs --- src/traces/heatmap/attributes.js | 7 +- src/traces/heatmap/defaults.js | 116 ++------------------ src/traces/histogram2d/attributes.js | 50 +++++++++ src/traces/histogram2dcontour/attributes.js | 43 ++++++++ 4 files changed, 107 insertions(+), 109 deletions(-) create mode 100644 src/traces/histogram2d/attributes.js create mode 100644 src/traces/histogram2dcontour/attributes.js diff --git a/src/traces/heatmap/attributes.js b/src/traces/heatmap/attributes.js index 21997574f39..49ddb19ae9f 100644 --- a/src/traces/heatmap/attributes.js +++ b/src/traces/heatmap/attributes.js @@ -83,11 +83,8 @@ module.exports = { 'in the `z` data are filled in.' ].join(' ') }, - _nestedModules: { // nested module coupling + + _nestedModules: { 'colorbar': 'Colorbar' - }, - _composedModules: { // composed module coupling - 'histogram2d': 'Histogram', - 'histogram2dcontour': 'Histogram' } }; diff --git a/src/traces/heatmap/defaults.js b/src/traces/heatmap/defaults.js index 0900cebc0d7..39a4d9802c4 100644 --- a/src/traces/heatmap/defaults.js +++ b/src/traces/heatmap/defaults.js @@ -9,122 +9,30 @@ 'use strict'; -var isNumeric = require('fast-isnumeric'); - -var Plots = require('../../plots/plots'); var Lib = require('../../lib'); var Colorscale = require('../../components/colorscale'); -var histogramSupplyDefaults = require('../histogram/defaults'); -var attributes = require('./attributes'); var hasColumns = require('./has_columns'); +var handleXYZDefaults = require('./xyz_defaults'); +var attributes = require('./attributes'); module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { - var isContour = Plots.traceIs(traceOut, 'contour'); - function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - if(!isContour) coerce('zsmooth'); - - if(Plots.traceIs(traceOut, 'histogram')) { - // x, y, z, marker.color, and x0, dx, y0, dy are coerced - // in Histogram.supplyDefaults - // (along with histogram-specific attributes) - histogramSupplyDefaults(traceIn, traceOut); - if(traceOut.visible === false) return; + var len = handleXYZDefaults(traceIn, traceOut, coerce); + if(!len) { + traceOut.visible = false; + return; } - else { - var len = handleXYZDefaults(traceIn, traceOut, coerce); - if(!len) { - traceOut.visible = false; - return; - } - - coerce('text'); - var _hasColumns = hasColumns(traceOut); + coerce('text'); + coerce('zsmooth'); + coerce('connectgaps', hasColumns(traceOut) && (traceOut.zsmooth !== false)); - if(!_hasColumns) coerce('transpose'); - coerce('connectgaps', _hasColumns && - (isContour || traceOut.zsmooth !== false)); - } - - if(!isContour || (traceOut.contours || {}).coloring!=='none') { - Colorscale.handleDefaults( - traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} - ); - } + Colorscale.handleDefaults( + traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} + ); }; - -function handleXYZDefaults(traceIn, traceOut, coerce) { - var z = coerce('z'); - var x, y; - - if(z===undefined || !z.length) return 0; - - if(hasColumns(traceIn)) { - x = coerce('x'); - y = coerce('y'); - - // column z must be accompanied by 'x' and 'y' arrays - if(!x || !y) return 0; - } - else { - x = coordDefaults('x', coerce); - y = coordDefaults('y', coerce); - - // TODO put z validation elsewhere - if(!isValidZ(z)) return 0; - } - - return traceOut.z.length; -} - -function coordDefaults(coordStr, coerce) { - var coord = coerce(coordStr), - coordType = coord ? - coerce(coordStr + 'type', 'array') : - 'scaled'; - - if(coordType === 'scaled') { - coerce(coordStr + '0'); - coerce('d' + coordStr); - } - - return coord; -} - -function isValidZ(z) { - var allRowsAreArrays = true, - oneRowIsFilled = false, - hasOneNumber = false, - zi; - - /* - * Without this step: - * - * hasOneNumber = false breaks contour but not heatmap - * allRowsAreArrays = false breaks contour but not heatmap - * oneRowIsFilled = false breaks both - */ - - for(var i = 0; i < z.length; i++) { - zi = z[i]; - if(!Array.isArray(zi)) { - allRowsAreArrays = false; - break; - } - if(zi.length > 0) oneRowIsFilled = true; - for(var j = 0; j < zi.length; j++) { - if(isNumeric(zi[j])) { - hasOneNumber = true; - break; - } - } - } - - return (allRowsAreArrays && oneRowIsFilled && hasOneNumber); -} diff --git a/src/traces/histogram2d/attributes.js b/src/traces/histogram2d/attributes.js new file mode 100644 index 00000000000..1acd01f75b6 --- /dev/null +++ b/src/traces/histogram2d/attributes.js @@ -0,0 +1,50 @@ +/** +* Copyright 2012-2015, 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. +*/ + + +var histogramAttrs = require('../histogram/attributes'); +var heatmapAttrs = require('../heatmap/attributes'); + +module.exports = { + x: histogramAttrs.x, + y: histogramAttrs.y, + + z: { + valType: 'data_array', + description: 'Sets the aggregation data.' + }, + marker: { + color: { + valType: 'data_array', + description: 'Sets the aggregation data.' + } + }, + + histnorm: histogramAttrs.histnorm, + histfunc: histogramAttrs.histfunc, + autobinx:histogramAttrs.autobinx, + nbinsx: histogramAttrs.nbinsx, + xbins: histogramAttrs.xbins, + autobiny: histogramAttrs.autobiny, + nbinsy: histogramAttrs.nbinsy, + ybins: histogramAttrs.ybins, + + zauto: heatmapAttrs.zauto, + zmin: heatmapAttrs.zmin, + zmax: heatmapAttrs.zmax, + colorscale: heatmapAttrs.colorscale, + autocolorscale: heatmapAttrs.autocolorscale, + reversescale: heatmapAttrs.reversescale, + showscale: heatmapAttrs.showscale, + + zsmooth: heatmapAttrs.zsmooth, + + _nestedModules: { + 'colorbar': 'Colorbar' + } +}; diff --git a/src/traces/histogram2dcontour/attributes.js b/src/traces/histogram2dcontour/attributes.js new file mode 100644 index 00000000000..b9796e0786f --- /dev/null +++ b/src/traces/histogram2dcontour/attributes.js @@ -0,0 +1,43 @@ +/** +* Copyright 2012-2015, 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. +*/ + +var histogram2dAttrs = require('../histogram2d/attributes'); +var contourAttrs = require('../contour/attributes'); + +module.exports = { + x: histogram2dAttrs.x, + y: histogram2dAttrs.y, + z: histogram2dAttrs.z, + marker: histogram2dAttrs.marker, + + histnorm: histogram2dAttrs.histnorm, + histfunc: histogram2dAttrs.histfunc, + autobinx:histogram2dAttrs.autobinx, + nbinsx: histogram2dAttrs.nbinsx, + xbins: histogram2dAttrs.xbins, + autobiny: histogram2dAttrs.autobiny, + nbinsy: histogram2dAttrs.nbinsy, + ybins: histogram2dAttrs.ybins, + + zauto: contourAttrs.zauto, + zmin: contourAttrs.zmin, + zmax: contourAttrs.zmax, + colorscale: contourAttrs.colorscale, + autocolorscale: contourAttrs.autocolorscale, + reversescale: contourAttrs.reversescale, + showscale: contourAttrs.showscale, + + autocontour: contourAttrs.autocontour, + ncontours: contourAttrs.ncontours, + contours: contourAttrs.contours, + line: contourAttrs.line, + + _nestedModules: { + 'colorbar': 'Colorbar' + } +}; From 7bc3e75e2efbe1d458edacb99923d0903b0792bc Mon Sep 17 00:00:00 2001 From: etpinard Date: Thu, 17 Dec 2015 12:12:25 -0500 Subject: [PATCH 23/28] make bar and histogram have distinct defaults step: - split bar style defaults logic into own file to be reused by histogram defaults - split histogram bin defaults logic into own file to be reused by histogram2d and histogram2dcontour defaults --- src/traces/bar/defaults.js | 42 +++++--------------- src/traces/bar/style_defaults.js | 34 ++++++++++++++++ src/traces/histogram/bin_defaults.js | 28 ++++++++++++++ src/traces/histogram/defaults.js | 58 ++++++++++------------------ 4 files changed, 91 insertions(+), 71 deletions(-) create mode 100644 src/traces/bar/style_defaults.js create mode 100644 src/traces/histogram/bin_defaults.js diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js index 20ad5015e10..22a40a19539 100644 --- a/src/traces/bar/defaults.js +++ b/src/traces/bar/defaults.js @@ -9,55 +9,31 @@ 'use strict'; -var Plotly = require('../../plotly'); var Lib = require('../../lib'); var Color = require('../../components/color'); -var histogramSupplyDefaults = require('../histogram/defaults'); var handleXYDefaults = require('../scatter/xy_defaults'); +var handleStyleDefaults = require('../bar/style_defaults'); var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); - var attributes = require('./attributes'); -module.exports = function(traceIn, traceOut, defaultColor, layout) { +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - if(traceOut.type === 'histogram') { - // x, y, and orientation are coerced in the histogram supplyDefaults - // (along with histogram-specific attributes) - histogramSupplyDefaults(traceIn, traceOut); - if(!traceOut.visible) return; - } - else { - var len = handleXYDefaults(traceIn, traceOut, coerce); - if(!len) { - traceOut.visible = false; - return; - } - - coerce('orientation', (traceOut.x && !traceOut.y) ? 'h' : 'v'); - } - - coerce('marker.color', defaultColor); - if(Plotly.Colorscale.hasColorscale(traceIn, 'marker')) { - Plotly.Colorscale.handleDefaults( - traceIn, traceOut, layout, coerce, {prefix: 'marker.', cLetter: 'c'} - ); - } - - coerce('marker.line.color', Plotly.Color.defaultLine); - if(Plotly.Colorscale.hasColorscale(traceIn, 'marker.line')) { - Plotly.Colorscale.handleDefaults( - traceIn, traceOut, layout, coerce, {prefix: 'marker.line.', cLetter: 'c'} - ); + var len = handleXYDefaults(traceIn, traceOut, coerce); + if(!len) { + traceOut.visible = false; + return; } - coerce('marker.line.width', 0); + coerce('orientation', (traceOut.x && !traceOut.y) ? 'h' : 'v'); coerce('text'); + handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); + // override defaultColor for error bars with defaultLine errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, {axis: 'y'}); errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, {axis: 'x', inherit: 'y'}); diff --git a/src/traces/bar/style_defaults.js b/src/traces/bar/style_defaults.js new file mode 100644 index 00000000000..e4a0065ade8 --- /dev/null +++ b/src/traces/bar/style_defaults.js @@ -0,0 +1,34 @@ +/** +* Copyright 2012-2015, 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 Color = require('../../components/color'); +var Colorscale = require('../../components/colorscale'); + + +module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout) { + coerce('marker.color', defaultColor); + + if(Colorscale.hasColorscale(traceIn, 'marker')) { + Colorscale.handleDefaults( + traceIn, traceOut, layout, coerce, {prefix: 'marker.', cLetter: 'c'} + ); + } + + coerce('marker.line.color', Color.defaultLine); + + if(Colorscale.hasColorscale(traceIn, 'marker.line')) { + Colorscale.handleDefaults( + traceIn, traceOut, layout, coerce, {prefix: 'marker.line.', cLetter: 'c'} + ); + } + + coerce('marker.line.width'); +}; diff --git a/src/traces/histogram/bin_defaults.js b/src/traces/histogram/bin_defaults.js new file mode 100644 index 00000000000..a08731ac4ee --- /dev/null +++ b/src/traces/histogram/bin_defaults.js @@ -0,0 +1,28 @@ +/** +* Copyright 2012-2015, 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'; + + +module.exports = function handleBinDefaults(traceIn, traceOut, coerce, binDirections) { + coerce('histnorm'); + + binDirections.forEach(function(binDirection) { + // data being binned - note that even though it's a little weird, + // it's possible to have bins without data, if there's inferred data + var binstrt = coerce(binDirection + 'bins.start'), + binend = coerce(binDirection + 'bins.end'), + autobin = coerce('autobin' + binDirection, !(binstrt && binend)); + + if(autobin) coerce('nbins' + binDirection); + else coerce(binDirection + 'bins.size'); + }); + + return traceOut; +}; diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index 1138561121c..51ac309ed12 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -9,60 +9,42 @@ 'use strict'; -var Plotly = require('../../plotly'); var Lib = require('../../lib'); +var Color = require('../../components/color'); +var handleBinDefaults = require('./bin_defaults'); +var handleStyleDefaults = require('../bar/style_defaults'); +var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); var attributes = require('./attributes'); -module.exports = function(traceIn, traceOut) { +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } - var binDirections = ['x'], - hasAggregationData, - x = coerce('x'), + var x = coerce('x'), y = coerce('y'); - if(Plotly.Plots.traceIs(traceOut, '2dMap')) { - // we could try to accept x0 and dx, etc... - // but that's a pretty weird use case. - // for now require both x and y explicitly specified. - if(!(x && x.length && y && y.length)) { - traceOut.visible = false; - return; - } + coerce('text'); - // if marker.color is an array, we can use it in aggregation instead of z - hasAggregationData = coerce('z') || coerce('marker.color'); + var orientation = coerce('orientation', (y && !x) ? 'h' : 'v'), + sample = traceOut[orientation==='v' ? 'x' : 'y']; - binDirections = ['x','y']; - } else { - var orientation = coerce('orientation', (y && !x) ? 'h' : 'v'), - sample = traceOut[orientation==='v' ? 'x' : 'y']; - - if(!(sample && sample.length)) { - traceOut.visible = false; - return; - } - - if(orientation==='h') binDirections = ['y']; - - hasAggregationData = traceOut[orientation==='h' ? 'x' : 'y']; + if(!(sample && sample.length)) { + traceOut.visible = false; + return; } + var hasAggregationData = traceOut[orientation==='h' ? 'x' : 'y']; if(hasAggregationData) coerce('histfunc'); - coerce('histnorm'); - binDirections.forEach(function(binDirection) { - // data being binned - note that even though it's a little weird, - // it's possible to have bins without data, if there's inferred data - var binstrt = coerce(binDirection + 'bins.start'), - binend = coerce(binDirection + 'bins.end'), - autobin = coerce('autobin' + binDirection, !(binstrt && binend)); + var binDirections = (orientation==='h') ? ['y'] : ['x']; + handleBinDefaults(traceIn, traceOut, coerce, binDirections); + + handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); - if(autobin) coerce('nbins' + binDirection); - else coerce(binDirection + 'bins.size'); - }); + // override defaultColor for error bars with defaultLine + errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, {axis: 'y'}); + errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, {axis: 'x', inherit: 'y'}); }; From 03b1557a6d273bf2f6226220fb1038c69d9521d7 Mon Sep 17 00:00:00 2001 From: etpinard Date: Thu, 17 Dec 2015 12:14:32 -0500 Subject: [PATCH 24/28] make contour and heatmap have distinct defaults step: - split up xyz heatmap defaults logic to be reused by contour defaults - split up contour style defaults logic to be resued by histogram2d and histogram2dcontour --- src/traces/contour/defaults.js | 27 +++++---- src/traces/contour/style_defaults.js | 34 +++++++++++ src/traces/heatmap/xyz_defaults.js | 87 ++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 src/traces/contour/style_defaults.js create mode 100644 src/traces/heatmap/xyz_defaults.js diff --git a/src/traces/contour/defaults.js b/src/traces/contour/defaults.js index a4767d92b49..05136b9bf78 100644 --- a/src/traces/contour/defaults.js +++ b/src/traces/contour/defaults.js @@ -10,8 +10,10 @@ 'use strict'; var Lib = require('../../lib'); -var heatmapSupplyDefaults = require('../heatmap/defaults'); +var hasColumns = require('../heatmap/has_columns'); +var handleXYZDefaults = require('../heatmap/xyz_defaults'); +var handleStyleDefaults = require('../contour/style_defaults'); var attributes = require('./attributes'); @@ -20,6 +22,15 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); } + var len = handleXYZDefaults(traceIn, traceOut, coerce); + if(!len) { + traceOut.visible = false; + return; + } + + coerce('text'); + coerce('connectgaps', hasColumns(traceOut)); + var contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'), contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'), autocontour = coerce('autocontour', !(contourStart && contourEnd)); @@ -27,17 +38,5 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout if(autocontour) coerce('ncontours'); else coerce('contours.size'); - var coloring = coerce('contours.coloring'); - - if(coloring === 'fill') coerce('contours.showlines'); - - if(traceOut.contours.showlines!==false) { - if(coloring !== 'lines') coerce('line.color', '#000'); - coerce('line.width', 0.5); - coerce('line.dash'); - } - - coerce('line.smoothing'); - - heatmapSupplyDefaults(traceIn, traceOut, defaultColor, layout); + handleStyleDefaults(traceIn, traceOut, coerce, layout); }; diff --git a/src/traces/contour/style_defaults.js b/src/traces/contour/style_defaults.js new file mode 100644 index 00000000000..f02bc7b5819 --- /dev/null +++ b/src/traces/contour/style_defaults.js @@ -0,0 +1,34 @@ +/** +* Copyright 2012-2015, 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 Colorscale = require('../../components/colorscale'); + + +module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, layout) { + var coloring = coerce('contours.coloring'); + + var showLines; + if(coloring === 'fill') showLines = coerce('contours.showlines'); + + if(showLines !== false) { + if(coloring !== 'lines') coerce('line.color', '#000'); + coerce('line.width', 0.5); + coerce('line.dash'); + } + + coerce('line.smoothing'); + + if((traceOut.contours || {}).coloring !== 'none') { + Colorscale.handleDefaults( + traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} + ); + } +}; diff --git a/src/traces/heatmap/xyz_defaults.js b/src/traces/heatmap/xyz_defaults.js new file mode 100644 index 00000000000..3401b44694c --- /dev/null +++ b/src/traces/heatmap/xyz_defaults.js @@ -0,0 +1,87 @@ +/** +* Copyright 2012-2015, 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 isNumeric = require('fast-isnumeric'); + +var hasColumns = require('./has_columns'); + + +module.exports = function handleXYZDefaults(traceIn, traceOut, coerce) { + var z = coerce('z'); + var x, y; + + if(z===undefined || !z.length) return 0; + + if(hasColumns(traceIn)) { + x = coerce('x'); + y = coerce('y'); + + // column z must be accompanied by 'x' and 'y' arrays + if(!x || !y) return 0; + } + else { + x = coordDefaults('x', coerce); + y = coordDefaults('y', coerce); + + // TODO put z validation elsewhere + if(!isValidZ(z)) return 0; + + coerce('transpose'); + } + + return traceOut.z.length; +}; + +function coordDefaults(coordStr, coerce) { + var coord = coerce(coordStr), + coordType = coord ? + coerce(coordStr + 'type', 'array') : + 'scaled'; + + if(coordType === 'scaled') { + coerce(coordStr + '0'); + coerce('d' + coordStr); + } + + return coord; +} + +function isValidZ(z) { + var allRowsAreArrays = true, + oneRowIsFilled = false, + hasOneNumber = false, + zi; + + /* + * Without this step: + * + * hasOneNumber = false breaks contour but not heatmap + * allRowsAreArrays = false breaks contour but not heatmap + * oneRowIsFilled = false breaks both + */ + + for(var i = 0; i < z.length; i++) { + zi = z[i]; + if(!Array.isArray(zi)) { + allRowsAreArrays = false; + break; + } + if(zi.length > 0) oneRowIsFilled = true; + for(var j = 0; j < zi.length; j++) { + if(isNumeric(zi[j])) { + hasOneNumber = true; + break; + } + } + } + + return (allRowsAreArrays && oneRowIsFilled && hasOneNumber); +} From eb327f00060eef14cb3c7d212bf4a89fa7381a24 Mon Sep 17 00:00:00 2001 From: etpinard Date: Thu, 17 Dec 2015 12:16:16 -0500 Subject: [PATCH 25/28] make histogram2d and histogram2dcontour have distinct defaults step: - split up histogram2d sample attr logic to be reused in histogram2dcontour defaults - use contour style defaults in histogram2dcontour defaults --- src/traces/histogram2d/defaults.js | 31 +++++++++++++++++++++ src/traces/histogram2d/sample_defaults.js | 34 +++++++++++++++++++++++ src/traces/histogram2dcontour/defaults.js | 34 +++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 src/traces/histogram2d/defaults.js create mode 100644 src/traces/histogram2d/sample_defaults.js create mode 100644 src/traces/histogram2dcontour/defaults.js diff --git a/src/traces/histogram2d/defaults.js b/src/traces/histogram2d/defaults.js new file mode 100644 index 00000000000..53addb64cb1 --- /dev/null +++ b/src/traces/histogram2d/defaults.js @@ -0,0 +1,31 @@ +/** +* Copyright 2012-2015, 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 Lib = require('../../lib'); +var Colorscale = require('../../components/colorscale'); + +var handleSampleDefaults = require('./sample_defaults'); +var attributes = require('./attributes'); + + +module.exports = function supplyDefaults(traceIn, traceOut, layout) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + handleSampleDefaults(traceIn, traceOut, coerce); + + coerce('zsmooth'); + + Colorscale.handleDefaults( + traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} + ); +}; diff --git a/src/traces/histogram2d/sample_defaults.js b/src/traces/histogram2d/sample_defaults.js new file mode 100644 index 00000000000..3ab74913ed6 --- /dev/null +++ b/src/traces/histogram2d/sample_defaults.js @@ -0,0 +1,34 @@ +/** +* Copyright 2012-2015, 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 handleBinDefaults = require('../histogram/bin_defaults'); + + +module.exports = function handleSampleDefaults(traceIn, traceOut, coerce) { + var x = coerce('x'), + y = coerce('y'); + + // we could try to accept x0 and dx, etc... + // but that's a pretty weird use case. + // for now require both x and y explicitly specified. + if(!(x && x.length && y && y.length)) { + traceOut.visible = false; + return; + } + + // if marker.color is an array, we can use it in aggregation instead of z + var hasAggregationData = coerce('z') || coerce('marker.color'); + + if(hasAggregationData) coerce('histfunc'); + + var binDirections = ['x', 'y']; + handleBinDefaults(traceIn, traceOut, coerce, binDirections); +}; diff --git a/src/traces/histogram2dcontour/defaults.js b/src/traces/histogram2dcontour/defaults.js new file mode 100644 index 00000000000..443cbead2c4 --- /dev/null +++ b/src/traces/histogram2dcontour/defaults.js @@ -0,0 +1,34 @@ +/** +* Copyright 2012-2015, 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 Lib = require('../../lib'); + +var handleSampleDefaults = require('../histogram2d/sample_defaults'); +var handleStyleDefaults = require('../contour/style_defaults'); +var attributes = require('./attributes'); + + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + handleSampleDefaults(traceIn, traceOut, coerce); + + var contourStart = Lib.coerce2(traceIn, traceOut, attributes, 'contours.start'), + contourEnd = Lib.coerce2(traceIn, traceOut, attributes, 'contours.end'), + autocontour = coerce('autocontour', !(contourStart && contourEnd)); + + if(autocontour) coerce('ncontours'); + else coerce('contours.size'); + + handleStyleDefaults(traceIn, traceOut, coerce, layout); +}; From dccf3ede3ebf5400ddb69a021ba35eeed5fff3d2 Mon Sep 17 00:00:00 2001 From: etpinard Date: Thu, 17 Dec 2015 12:17:13 -0500 Subject: [PATCH 26/28] update histogram* index files, - so that they export their own attributes and defaults --- src/traces/histogram/index.js | 4 ++-- src/traces/histogram2d/index.js | 4 ++-- src/traces/histogram2dcontour/index.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/traces/histogram/index.js b/src/traces/histogram/index.js index 391e96c77f3..6e5725f7aa1 100644 --- a/src/traces/histogram/index.js +++ b/src/traces/histogram/index.js @@ -12,7 +12,7 @@ var Plotly = require('../../plotly'); /** - * Histogram has its own calc function, + * Histogram has its own attribute, defaults and calc steps, * but uses bar's plot to display * and bar's setPositions for stacking and grouping */ @@ -40,7 +40,7 @@ exports.attributes = require('./attributes'); exports.layoutAttributes = require('../bar/layout_attributes'); -exports.supplyDefaults = require('../bar/defaults'); +exports.supplyDefaults = require('./defaults'); exports.supplyLayoutDefaults = require('../bar/layout_defaults'); diff --git a/src/traces/histogram2d/index.js b/src/traces/histogram2d/index.js index c86ee285125..862b8c42df6 100644 --- a/src/traces/histogram2d/index.js +++ b/src/traces/histogram2d/index.js @@ -24,9 +24,9 @@ Plotly.Plots.register(exports, 'histogram2d', ].join(' ') }); -exports.attributes = require('../heatmap/attributes'); +exports.attributes = require('./attributes'); -exports.supplyDefaults = require('../heatmap/defaults'); +exports.supplyDefaults = require('./defaults'); exports.calc = require('../heatmap/calc'); diff --git a/src/traces/histogram2dcontour/index.js b/src/traces/histogram2dcontour/index.js index 6d97e38d9e6..a9ee307a1e3 100644 --- a/src/traces/histogram2dcontour/index.js +++ b/src/traces/histogram2dcontour/index.js @@ -24,9 +24,9 @@ Plotly.Plots.register(exports, 'histogram2dcontour', ].join(' ') }); -exports.attributes = require('../contour/attributes'); +exports.attributes = require('./attributes'); -exports.supplyDefaults = require('../contour/defaults'); +exports.supplyDefaults = require('./defaults'); exports.calc = require('../contour/calc'); From c1055b618f670145030d4a83446afadca1c42853 Mon Sep 17 00:00:00 2001 From: etpinard Date: Thu, 17 Dec 2015 12:17:47 -0500 Subject: [PATCH 27/28] lint (name module.exports functions to help out during debugging) --- src/traces/bar/plot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 8314e63cd84..363f3743372 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -18,7 +18,7 @@ var Color = require('../../components/color'); var arraysToCalcdata = require('./arrays_to_calcdata'); -module.exports = function(gd, plotinfo, cdbar) { +module.exports = function plot(gd, plotinfo, cdbar) { var xa = plotinfo.x(), ya = plotinfo.y(), fullLayout = gd._fullLayout; From 2247b7c3b9461e509bcc683d4a319d325629597d Mon Sep 17 00:00:00 2001 From: etpinard Date: Thu, 17 Dec 2015 12:18:52 -0500 Subject: [PATCH 28/28] remove warnings on PlotSchema.get() : - check for 'area' trace type in get-plot-schema routine before calling traceIs() --- src/plot_api/plot_schema.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plot_api/plot_schema.js b/src/plot_api/plot_schema.js index 9cb4bb28fa7..56acd0fff87 100644 --- a/src/plot_api/plot_schema.js +++ b/src/plot_api/plot_schema.js @@ -259,12 +259,13 @@ function assignPolarLayoutAttrs(layoutAttributes) { } function getSubplotRegistry(traceType) { + if(traceType === 'area') return {}; // FIXME + var subplotsRegistry = Plotly.Plots.subplotsRegistry, subplotType = Object.keys(subplotsRegistry).filter(function(subplotType) { return Plotly.Plots.traceIs({type: traceType}, subplotType); })[0]; - if(traceType === 'area') return {}; // FIXME if(subplotType === undefined) return {}; return subplotsRegistry[subplotType];