diff --git a/src/components/colorbar/defaults.js b/src/components/colorbar/defaults.js index af485987074..feba990b80f 100644 --- a/src/components/colorbar/defaults.js +++ b/src/components/colorbar/defaults.js @@ -9,8 +9,9 @@ 'use strict'; -var Axes = require('../../plots/cartesian/axes'); var Lib = require('../../lib'); +var handleTickValueDefaults = require('../../plots/cartesian/tick_value_defaults'); +var handleTickDefaults = require('../../plots/cartesian/tick_defaults'); var attributes = require('./attributes'); @@ -49,9 +50,9 @@ module.exports = function colorbarDefaults(containerIn, containerOut, layout) { coerce('borderwidth'); coerce('bgcolor'); - Axes.handleTickValueDefaults(colorbarIn, colorbarOut, coerce, 'linear'); + handleTickValueDefaults(colorbarIn, colorbarOut, coerce, 'linear'); - Axes.handleTickDefaults(colorbarIn, colorbarOut, coerce, 'linear', + handleTickDefaults(colorbarIn, colorbarOut, coerce, 'linear', {outerTicks: false, font: layout.font, noHover: true}); coerce('title'); diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index c354608f8cf..4b5b40f1d0c 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -12,6 +12,16 @@ var d3 = require('d3'); var Plotly = require('../../plotly'); +var Plots = require('../../plots/plots'); +var Axes = require('../../plots/cartesian/axes'); +var Fx = require('../../plots/cartesian/graph_interact'); +var Lib = require('../../lib'); +var Drawing = require('../drawing'); +var Color = require('../color'); + +var handleAxisDefaults = require('../../plots/cartesian/axis_defaults'); +var handleAxisPositionDefaults = require('../../plots/cartesian/position_defaults'); +var axisLayoutAttrs = require('../../plots/cartesian/layout_attributes'); var attributes = require('./attributes'); @@ -163,23 +173,19 @@ module.exports = function draw(gd, id) { // Coerce w.r.t. Axes layoutAttributes: // re-use axes.js logic without updating _fullData function coerce(attr, dflt) { - return Plotly.Lib.coerce(cbAxisIn, cbAxisOut, - Plotly.Axes.layoutAttributes, - attr, dflt); + return Lib.coerce(cbAxisIn, cbAxisOut, axisLayoutAttrs, attr, dflt); } // Prepare the Plotly axis object - Plotly.Axes.handleAxisDefaults(cbAxisIn, cbAxisOut, - coerce, axisOptions); - Plotly.Axes.handleAxisPositioningDefaults(cbAxisIn, cbAxisOut, - coerce, axisOptions); + handleAxisDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions); + handleAxisPositionDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions); cbAxisOut._id = 'y' + id; cbAxisOut._td = gd; // position can't go in through supplyDefaults // because that restricts it to [0,1] - cbAxisOut.position = opts.x+xpadFrac+thickFrac; + cbAxisOut.position = opts.x + xpadFrac + thickFrac; // save for other callers to access this axis component.axis = cbAxisOut; @@ -196,14 +202,14 @@ module.exports = function draw(gd, id) { cbAxisOut.tick0 = opts.levels.start; var dtick = opts.levels.size; // expand if too many contours, so we don't get too many ticks - var autoNtick = Plotly.Lib.constrain( + var autoNtick = Lib.constrain( (yBottomPx-yTopPx)/50, 4, 15) + 1, dtFactor = (zrange[1]-zrange[0]) / ((opts.nticks||autoNtick)*dtick); if(dtFactor>1) { var dtexp = Math.pow(10,Math.floor( Math.log(dtFactor)/Math.LN10)); - dtick *= dtexp*Plotly.Lib.roundUp(dtFactor/dtexp,[2,5,10]); + dtick *= dtexp * Lib.roundUp(dtFactor/dtexp,[2,5,10]); // if the contours are at round multiples, reset tick0 // so they're still at round multiples. Otherwise, // keep the first label on the first contour level @@ -269,7 +275,7 @@ module.exports = function draw(gd, id) { parseInt(titleText.style('font-size'), 10) * 1.3; } if(mathJaxNode) { - titleHeight = Plotly.Drawing.bBox(mathJaxNode).height; + titleHeight = Drawing.bBox(mathJaxNode).height; if(titleHeight>lineSize) { // not entirely sure how mathjax is doing // vertical alignment, but this seems to work. @@ -278,7 +284,7 @@ module.exports = function draw(gd, id) { } else if(titleText.node() && !titleText.classed('js-placeholder')) { - titleHeight = Plotly.Drawing.bBox( + titleHeight = Drawing.bBox( titleGroup.node()).height; } if(titleHeight) { @@ -351,7 +357,7 @@ module.exports = function draw(gd, id) { .attr('d','M'+xLeft+',' + (Math.round(cbAxisOut.c2p(d))+(opts.line.width/2)%1) + 'h'+thickPx) - .call(Plotly.Drawing.lineGroupStyle, + .call(Drawing.lineGroupStyle, opts.line.width, linecolormap(d), opts.line.dash); }); @@ -363,7 +369,7 @@ module.exports = function draw(gd, id) { (opts.outlinewidth||0)/2 - (opts.ticks==='outside' ? 1 : 0); cbAxisOut.side = 'right'; - return Plotly.Axes.doTicks(gd, cbAxisOut); + return Axes.doTicks(gd, cbAxisOut); } function positionCB(){ @@ -372,7 +378,7 @@ module.exports = function draw(gd, id) { // TODO: why are we redrawing multiple times now with this? // I guess autoMargin doesn't like being post-promise? var innerWidth = thickPx + opts.outlinewidth/2 + - Plotly.Drawing.bBox(cbAxisOut._axislayer.node()).width; + Drawing.bBox(cbAxisOut._axislayer.node()).width; titleEl = titleCont.select('text'); if(titleEl.node() && !titleEl.classed('js-placeholder')) { var mathJaxNode = titleCont @@ -381,7 +387,7 @@ module.exports = function draw(gd, id) { titleWidth; if(mathJaxNode && ['top','bottom'].indexOf(opts.titleside)!==-1) { - titleWidth = Plotly.Drawing.bBox(mathJaxNode).width; + titleWidth = Drawing.bBox(mathJaxNode).width; } else { // note: the formula below works for all titlesides, @@ -389,7 +395,7 @@ module.exports = function draw(gd, id) { // but the weird fullLayout._size.l is because the titleunshift // transform gets removed by Drawing.bBox titleWidth = - Plotly.Drawing.bBox(titleCont.node()).right - + Drawing.bBox(titleCont.node()).right - xLeft - fullLayout._size.l; } innerWidth = Math.max(innerWidth,titleWidth); @@ -406,8 +412,8 @@ module.exports = function draw(gd, id) { width: Math.max(outerwidth,2), height: Math.max(outerheight + 2*yExtraPx,2) }) - .call(Plotly.Color.fill, opts.bgcolor) - .call(Plotly.Color.stroke, opts.bordercolor) + .call(Color.fill, opts.bgcolor) + .call(Color.stroke, opts.bordercolor) .style({'stroke-width': opts.borderwidth}); container.selectAll('.cboutline').attr({ @@ -417,7 +423,7 @@ module.exports = function draw(gd, id) { width: Math.max(thickPx,2), height: Math.max(outerheight - 2*opts.ypad - titleHeight, 2) }) - .call(Plotly.Color.stroke, opts.outlinecolor) + .call(Color.stroke, opts.outlinecolor) .style({ fill: 'None', 'stroke-width': opts.outlinewidth @@ -430,7 +436,7 @@ module.exports = function draw(gd, id) { 'translate('+(fullLayout._size.l-xoffset)+','+fullLayout._size.t+')'); //auto margin adjustment - Plotly.Plots.autoMargin(gd, id,{ + Plots.autoMargin(gd, id,{ x: opts.x, y: opts.y, l: outerwidth*({right:1, center:0.5}[opts.xanchor]||0), @@ -440,10 +446,10 @@ module.exports = function draw(gd, id) { }); } - var cbDone = Plotly.Lib.syncOrAsync([ - Plotly.Plots.previousPromises, + var cbDone = Lib.syncOrAsync([ + Plots.previousPromises, drawAxis, - Plotly.Plots.previousPromises, + Plots.previousPromises, positionCB ], gd); @@ -455,11 +461,11 @@ module.exports = function draw(gd, id) { xf, yf; - Plotly.Fx.dragElement({ + Fx.dragElement({ element: container.node(), prepFn: function() { t0 = container.attr('transform'); - Plotly.Fx.setCursor(container); + Fx.setCursor(container); }, moveFn: function(dx, dy) { var gs = gd._fullLayout._size; @@ -467,17 +473,17 @@ module.exports = function draw(gd, id) { container.attr('transform', t0+' ' + 'translate('+dx+','+dy+')'); - xf = Plotly.Fx.dragAlign(xLeftFrac + (dx/gs.w), thickFrac, + xf = Fx.dragAlign(xLeftFrac + (dx/gs.w), thickFrac, 0, 1, opts.xanchor); - yf = Plotly.Fx.dragAlign(yBottomFrac - (dy/gs.h), lenFrac, + yf = Fx.dragAlign(yBottomFrac - (dy/gs.h), lenFrac, 0, 1, opts.yanchor); - var csr = Plotly.Fx.dragCursors(xf, yf, + var csr = Fx.dragCursors(xf, yf, opts.xanchor, opts.yanchor); - Plotly.Fx.setCursor(container, csr); + Fx.setCursor(container, csr); }, doneFn: function(dragged) { - Plotly.Fx.setCursor(container); + Fx.setCursor(container); if(dragged && xf!==undefined && yf!==undefined) { var idNum = id.substr(2), @@ -507,8 +513,8 @@ module.exports = function draw(gd, id) { // setter - for multi-part properties, // set only the parts that are provided - opts[name] = Plotly.Lib.isPlainObject(opts[name]) ? - Plotly.Lib.extendFlat(opts[name], v) : + opts[name] = Lib.isPlainObject(opts[name]) ? + Lib.extendFlat(opts[name], v) : v; return component; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index fefaaba3154..7d8001eb07d 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -16,325 +16,18 @@ var isNumeric = require('fast-isnumeric'); var axes = module.exports = {}; axes.layoutAttributes = require('./layout_attributes'); +axes.supplyLayoutDefaults = require('./layout_defaults'); -var xAxisMatch = /^xaxis[0-9]*$/, - yAxisMatch = /^yaxis[0-9]*$/; +axes.setConvert = require('./set_convert'); -axes.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { - // get the full list of axes already defined - var layoutKeys = Object.keys(layoutIn), - xaList = [], - yaList = [], - outerTicks = {}, - noGrids = {}, - i; - - for(i = 0; i < layoutKeys.length; i++) { - var key = layoutKeys[i]; - if(xAxisMatch.test(key)) xaList.push(key); - else if(yAxisMatch.test(key)) yaList.push(key); - } - - for(i = 0; i < fullData.length; i++) { - var trace = fullData[i], - xaName = axes.id2name(trace.xaxis), - yaName = axes.id2name(trace.yaxis); - - // add axes implied by traces - if(xaName && xaList.indexOf(xaName)===-1) xaList.push(xaName); - if(yaName && yaList.indexOf(yaName)===-1) yaList.push(yaName); - - // check for default formatting tweaks - if(Plotly.Plots.traceIs(trace, '2dMap')) { - outerTicks[xaName] = true; - outerTicks[yaName] = true; - } - - if(Plotly.Plots.traceIs(trace, 'oriented')) { - var positionAxis = trace.orientation==='h' ? yaName : xaName; - noGrids[positionAxis] = true; - } - } - - function axSort(a,b) { - var aNum = Number(a.substr(5)||1), - bNum = Number(b.substr(5)||1); - return aNum - bNum; - } - - if(layoutOut._hasCartesian || layoutOut._hasGL2D || !fullData.length) { - // make sure there's at least one of each and lists are sorted - if(!xaList.length) xaList = ['xaxis']; - else xaList.sort(axSort); - - if(!yaList.length) yaList = ['yaxis']; - else yaList.sort(axSort); - } - - xaList.concat(yaList).forEach(function(axName){ - var axLetter = axName.charAt(0), - axLayoutIn = layoutIn[axName] || {}, - axLayoutOut = {}, - defaultOptions = { - letter: axLetter, - font: layoutOut.font, - outerTicks: outerTicks[axName], - showGrid: !noGrids[axName], - name: axName, - data: fullData - }, - positioningOptions = { - letter: axLetter, - counterAxes: {x: yaList, y: xaList}[axLetter].map(axes.name2id), - overlayableAxes: {x: xaList, y: yaList}[axLetter].filter(function(axName2){ - return axName2!==axName && !(layoutIn[axName2]||{}).overlaying; - }).map(axes.name2id) - }; - - function coerce(attr, dflt) { - return Plotly.Lib.coerce(axLayoutIn, axLayoutOut, - axes.layoutAttributes, - attr, dflt); - } - - axes.handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions); - axes.handleAxisPositioningDefaults(axLayoutIn, axLayoutOut, coerce, positioningOptions); - layoutOut[axName] = axLayoutOut; - - // so we don't have to repeat autotype unnecessarily, - // copy an autotype back to layoutIn - if(!layoutIn[axName] && axLayoutIn.type!=='-') { - layoutIn[axName] = {type: axLayoutIn.type}; - } - - }); - - // plot_bgcolor only makes sense if there's a (2D) plot! - // TODO: bgcolor for each subplot, to inherit from the main one - if(xaList.length && yaList.length) { - Plotly.Lib.coerce(layoutIn, layoutOut, - Plotly.Plots.layoutAttributes, 'plot_bgcolor'); - } -}; - -/** - * options: object containing: - * letter: 'x' or 'y' - * title: name of the axis (ie 'Colorbar') to go in default title - * name: axis object name (ie 'xaxis') if one should be stored - * font: the default font to inherit - * outerTicks: boolean, should ticks default to outside? - * showGrid: boolean, should gridlines be shown by default? - * noHover: boolean, this axis doesn't support hover effects? - * data: the plot data to use in choosing auto type - */ -axes.handleAxisDefaults = function(containerIn, containerOut, coerce, options) { - var letter = options.letter, - font = options.font || {}, - defaultTitle = 'Click to enter ' + - (options.title || (letter.toUpperCase() + ' axis')) + - ' title'; - - // set up some private properties - if(options.name) { - containerOut._name = options.name; - containerOut._id = axes.name2id(options.name); - } - - // now figure out type and do some more initialization - var axType = coerce('type'); - if(axType==='-') { - setAutoType(containerOut, options.data); - - if(containerOut.type==='-') { - containerOut.type = 'linear'; - } - else { - // copy autoType back to input axis - // note that if this object didn't exist - // in the input layout, we have to put it in - // this happens in the main supplyDefaults function - axType = containerIn.type = containerOut.type; - } - } - axes.setConvert(containerOut); - - coerce('title', defaultTitle); - Plotly.Lib.coerceFont(coerce, 'titlefont', { - family: font.family, - size: Math.round(font.size * 1.2), - color: font.color - }); - - var validRange = (containerIn.range||[]).length===2 && - isNumeric(containerIn.range[0]) && - isNumeric(containerIn.range[1]), - autoRange = coerce('autorange', !validRange); - - if(autoRange) coerce('rangemode'); - var range = coerce('range', [-1, letter==='x' ? 6 : 4]); - if(range[0] === range[1]) { - containerOut.range = [range[0] - 1, range[0] + 1]; - } - Plotly.Lib.noneOrAll(containerIn.range, containerOut.range, [0, 1]); - - coerce('fixedrange'); - - axes.handleTickValueDefaults(containerIn, containerOut, coerce, axType); - - axes.handleTickDefaults(containerIn, containerOut, coerce, axType, options); - - - - var lineColor = Plotly.Lib.coerce2(containerIn, containerOut, axes.layoutAttributes, 'linecolor'), - lineWidth = Plotly.Lib.coerce2(containerIn, containerOut, axes.layoutAttributes, 'linewidth'), - showLine = coerce('showline', !!lineColor || !!lineWidth); - - if(!showLine) { - delete containerOut.linecolor; - delete containerOut.linewidth; - } - - if(showLine || containerOut.ticks) coerce('mirror'); - - var gridColor = Plotly.Lib.coerce2(containerIn, containerOut, axes.layoutAttributes, 'gridcolor'), - gridWidth = Plotly.Lib.coerce2(containerIn, containerOut, axes.layoutAttributes, 'gridwidth'), - showGridLines = coerce('showgrid', options.showGrid || !!gridColor || !!gridWidth); - - if(!showGridLines) { - delete containerOut.gridcolor; - delete containerOut.gridwidth; - } - - var zeroLineColor = Plotly.Lib.coerce2(containerIn, containerOut, axes.layoutAttributes, 'zerolinecolor'), - zeroLineWidth = Plotly.Lib.coerce2(containerIn, containerOut, axes.layoutAttributes, 'zerolinewidth'), - showZeroLine = coerce('zeroline', options.showGrid || !!zeroLineColor || !!zeroLineWidth); - - if(!showZeroLine) { - delete containerOut.zerolinecolor; - delete containerOut.zerolinewidth; - } - - return containerOut; -}; - -/** - * options: inherits font, outerTicks, noHover from axes.handleAxisDefaults - */ -axes.handleTickDefaults = function(containerIn, containerOut, coerce, axType, options) { - var tickLen = Plotly.Lib.coerce2(containerIn, containerOut, axes.layoutAttributes, 'ticklen'), - tickWidth = Plotly.Lib.coerce2(containerIn, containerOut, axes.layoutAttributes, 'tickwidth'), - tickColor = Plotly.Lib.coerce2(containerIn, containerOut, axes.layoutAttributes, 'tickcolor'), - showTicks = coerce('ticks', (options.outerTicks || tickLen || tickWidth || tickColor) ? 'outside' : ''); - if(!showTicks) { - delete containerOut.ticklen; - delete containerOut.tickwidth; - delete containerOut.tickcolor; - } - - var showTickLabels = coerce('showticklabels'); - if(showTickLabels) { - Plotly.Lib.coerceFont(coerce, 'tickfont', options.font || {}); - coerce('tickangle'); - - var showAttrDflt = axes.getShowAttrDflt(containerIn); - - if(axType !== 'category') { - var tickFormat = coerce('tickformat'); - if(!options.noHover) coerce('hoverformat'); - - if(!tickFormat && axType !== 'date') { - coerce('showexponent', showAttrDflt); - coerce('exponentformat'); - } - } - - var tickPrefix = coerce('tickprefix'); - if(tickPrefix) coerce('showtickprefix', showAttrDflt); - - var tickSuffix = coerce('ticksuffix'); - if(tickSuffix) coerce('showticksuffix', showAttrDflt); - } -}; - -axes.handleTickValueDefaults = function(containerIn, containerOut, coerce, axType) { - var tickmodeDefault = 'auto'; - - if(containerIn.tickmode === 'array' && - (axType === 'log' || axType === 'date')) { - containerIn.tickmode = 'auto'; - } - - if(Array.isArray(containerIn.tickvals)) tickmodeDefault = 'array'; - else if(containerIn.dtick && isNumeric(containerIn.dtick)) { - tickmodeDefault = 'linear'; - } - var tickmode = coerce('tickmode', tickmodeDefault); - - if(tickmode === 'auto') coerce('nticks'); - else if(tickmode === 'linear') { - coerce('tick0'); - coerce('dtick'); - } - else { - var tickvals = coerce('tickvals'); - if(tickvals === undefined) containerOut.tickmode = 'auto'; - else coerce('ticktext'); - } -}; - -axes.handleAxisPositioningDefaults = function(containerIn, containerOut, coerce, options) { - var counterAxes = options.counterAxes || [], - overlayableAxes = options.overlayableAxes || [], - letter = options.letter; - - var anchor = Plotly.Lib.coerce(containerIn, containerOut, - { - anchor: { - valType:'enumerated', - values: ['free'].concat(counterAxes), - dflt: isNumeric(containerIn.position) ? 'free' : - (counterAxes[0] || 'free') - } - }, - 'anchor'); - - if(anchor==='free') coerce('position'); - - Plotly.Lib.coerce(containerIn, containerOut, - { - side: { - valType: 'enumerated', - values: letter==='x' ? ['bottom', 'top'] : ['left', 'right'], - dflt: letter==='x' ? 'bottom' : 'left' - } - }, - 'side'); - - var overlaying = false; - if(overlayableAxes.length) { - overlaying = Plotly.Lib.coerce(containerIn, containerOut, { - overlaying: { - valType: 'enumerated', - values: [false].concat(overlayableAxes), - dflt: false - } - }, - 'overlaying'); - } +var axisIds = require('./axis_ids'); +axes.id2name = axisIds.id2name; +axes.cleanId = axisIds.cleanId; +axes.list = axisIds.list; +axes.listIds = axisIds.listIds; +axes.getFromId = axisIds.getFromId; +axes.getFromTrace = axisIds.getFromTrace; - if(!overlaying) { - // TODO: right now I'm copying this domain over to overlaying axes - // in ax.setscale()... but this means we still need (imperfect) logic - // in the axes popover to hide domain for the overlaying axis. - // perhaps I should make a private version _domain that all axes get??? - var domain = coerce('domain'); - if(domain[0] > domain[1] - 0.01) containerOut.domain = [0,1]; - Plotly.Lib.noneOrAll(containerIn.domain, containerOut.domain, [0, 1]); - } - - return containerOut; -}; // find the list of possible axes to reference with an xref or yref attribute // and coerce it to that list @@ -359,450 +52,21 @@ axes.coerceRef = function(containerIn, containerOut, td, axLetter) { // so we auto-set them again axes.clearTypes = function(gd, traces) { if(!Array.isArray(traces) || !traces.length) { - traces = (gd._fullData).map(function(d,i) { return i; }); + traces = (gd._fullData).map(function(d, i) { return i; }); } traces.forEach(function(tracenum) { var trace = gd.data[tracenum]; - delete (axes.getFromId(gd, trace.xaxis)||{}).type; - delete (axes.getFromId(gd, trace.yaxis)||{}).type; + delete (axes.getFromId(gd, trace.xaxis) || {}).type; + delete (axes.getFromId(gd, trace.yaxis) || {}).type; }); }; -// convert between axis names (xaxis, xaxis2, etc, elements of td.layout) -// and axis id's (x, x2, etc). Would probably have ditched 'xaxis' -// completely in favor of just 'x' if it weren't ingrained in the API etc. -var AX_ID_PATTERN = /^[xyz][0-9]*$/, - AX_NAME_PATTERN = /^[xyz]axis[0-9]*$/; -axes.id2name = function(id) { - if(typeof id !== 'string' || !id.match(AX_ID_PATTERN)) return; - var axNum = id.substr(1); - if(axNum==='1') axNum = ''; - return id.charAt(0) + 'axis' + axNum; -}; - -axes.name2id = function(name) { - if(!name.match(AX_NAME_PATTERN)) return; - var axNum = name.substr(5); - if(axNum==='1') axNum = ''; - return name.charAt(0)+axNum; -}; - -axes.cleanId = function(id, axLetter) { - if(!id.match(AX_ID_PATTERN)) return; - if(axLetter && id.charAt(0)!==axLetter) return; - - var axNum = id.substr(1).replace(/^0+/,''); - if(axNum==='1') axNum = ''; - return id.charAt(0) + axNum; -}; - -axes.cleanName = function(name, axLetter) { - if(!name.match(AX_ID_PATTERN)) return; - if(axLetter && name.charAt(0)!==axLetter) return; - - var axNum = name.substr(5).replace(/^0+/,''); - if(axNum==='1') axNum = ''; - return name.charAt(0) + 'axis' + axNum; -}; - // get counteraxis letter for this axis (name or id) // this can also be used as the id for default counter axis axes.counterLetter = function(id) { - return {x:'y',y:'x'}[id.charAt(0)]; -}; - -function setAutoType(ax, data){ - // new logic: let people specify any type they want, - // only autotype if type is '-' - if(ax.type!=='-') return; - - var id = ax._id, - axLetter = id.charAt(0); - - // support 3d - if(id.indexOf('scene') !== -1) id = axLetter; - - var d0 = getFirstNonEmptyTrace(data, id, axLetter); - if(!d0) return; - - // first check for histograms, as the count direction - // should always default to a linear axis - if(d0.type==='histogram' && - axLetter==={v:'y', h:'x'}[d0.orientation || 'v']) { - ax.type='linear'; - return; - } - - // check all boxes on this x axis to see - // if they're dates, numbers, or categories - if(isBoxWithoutPositionCoords(d0, axLetter)) { - var posLetter = getBoxPosLetter(d0), - boxPositions = [], - trace; - - for(var i = 0; i < data.length; i++) { - trace = data[i]; - if(!Plotly.Plots.traceIs(trace, 'box') || - (trace[axLetter + 'axis'] || axLetter) !== id) continue; - - if(trace[posLetter] !== undefined) boxPositions.push(trace[posLetter][0]); - else if(trace.name !== undefined) boxPositions.push(trace.name); - else boxPositions.push('text'); - } - - ax.type = axes.autoType(boxPositions); - } - else { - ax.type = axes.autoType(d0[axLetter] || [d0[axLetter+'0']]); - } -} - -function getBoxPosLetter(trace) { - return {v:'x', h:'y'}[trace.orientation || 'v']; -} - -function isBoxWithoutPositionCoords(trace, axLetter) { - var posLetter = getBoxPosLetter(trace); - return Plotly.Plots.traceIs(trace, 'box') && axLetter===posLetter && - trace[posLetter]===undefined && trace[posLetter + '0']===undefined; -} - -function getFirstNonEmptyTrace(data, id, axLetter) { - var trace; - - for(var i = 0; i < data.length; i++) { - trace = data[i]; - - if((trace[axLetter + 'axis'] || axLetter) === id) { - if(isBoxWithoutPositionCoords(trace, axLetter)) { - return trace; - } - else if((trace[axLetter] || []).length || trace[axLetter + '0']) { - return trace; - } - } - } -} - -axes.autoType = function(array) { - if(axes.moreDates(array)) return 'date'; - if(axes.category(array)) return 'category'; - if(linearOK(array)) return 'linear'; - else return '-'; -}; - -/* - * Attributes 'showexponent', 'showtickprefix' and 'showticksuffix' - * share values. - * - * If only 1 attribute is set, - * the remaining attributes inherit that value. - * - * If 2 attributes are set to the same value, - * the remaining attribute inherits that value. - * - * If 2 attributes are set to different values, - * the remaining is set to its dflt value. - * - */ -axes.getShowAttrDflt = function getShowAttrDflt(containerIn) { - var showAttrsAll = ['showexponent', - 'showtickprefix', - 'showticksuffix'], - showAttrs = showAttrsAll.filter(function(a){ - return containerIn[a]!==undefined; - }), - sameVal = function(a){ - return containerIn[a]===containerIn[showAttrs[0]]; - }; - if (showAttrs.every(sameVal) || showAttrs.length===1) { - return containerIn[showAttrs[0]]; - } -}; - -// is there at least one number in array? If not, we should leave -// ax.type empty so it can be autoset later -function linearOK(array) { - if(!array) return false; - for(var i = 0; i < array.length; i++) { - if(isNumeric(array[i])) return true; - } - return false; -} - -// does the array a have mostly dates rather than numbers? -// note: some values can be neither (such as blanks, text) -// 2- or 4-digit integers can be both, so require twice as many -// dates as non-dates, to exclude cases with mostly 2 & 4 digit -// numbers and a few dates -axes.moreDates = function(a) { - var dcnt=0, ncnt=0, - // test at most 1000 points, evenly spaced - inc = Math.max(1,(a.length-1)/1000), - ai; - for(var i=0; incnt*2); -}; - -// are the (x,y)-values in td.data mostly text? -// require twice as many categories as numbers -axes.category = function(a) { - // test at most 1000 points - var inc = Math.max(1, (a.length - 1) / 1000), - curvenums = 0, - curvecats = 0, - ai; - - for(var i = 0; i < a.length; i += inc) { - ai = axes.cleanDatum(a[Math.round(i)]); - if(isNumeric(ai)) curvenums++; - else if(typeof ai === 'string' && ai !== '' && ai !== 'None') curvecats++; - } - return curvecats > curvenums * 2; -}; - -// cleanDatum: removes characters -// same replace criteria used in the grid.js:scrapeCol -// but also handling dates, numbers, and NaN, null, Infinity etc -axes.cleanDatum = function(c){ - try{ - if(typeof c==='object' && c!==null && c.getTime) { - return Plotly.Lib.ms2DateTime(c); - } - if(typeof c!=='string' && !isNumeric(c)) { - return ''; - } - c = c.toString().replace(/['"%,$# ]/g,''); - }catch(e){ - console.log(e,c); - } - return c; -}; - -/** - * standardize all missing data in calcdata to use undefined - * never null or NaN. - * that way we can use !==undefined, or !==axes.BADNUM, - * to test for real data - */ -axes.BADNUM = undefined; - -// setConvert: define the conversion functions for an axis -// data is used in 4 ways: -// d: data, in whatever form it's provided -// c: calcdata: turned into numbers, but not linearized -// l: linearized - same as c except for log axes (and other -// mappings later?) this is used by ranges, and when we -// need to know if it's *possible* to show some data on -// this axis, without caring about the current range -// p: pixel value - mapped to the screen with current size and zoom -// setAxConvert creates/updates these conversion functions -// also clears the autorange bounds ._min and ._max -// and the autotick constraints ._minDtick, ._forceTick0, -// and looks for date ranges that aren't yet in numeric format -axes.setConvert = function(ax) { - // clipMult: how many axis lengths past the edge do we render? - // for panning, 1-2 would suffice, but for zooming more is nice. - // also, clipping can affect the direction of lines off the edge... - var clipMult = 10; - - function toLog(v, clip){ - if(v>0) return Math.log(v)/Math.LN10; - - else if(v<=0 && clip && ax.range && ax.range.length===2) { - // clip NaN (ie past negative infinity) to clipMult axis - // length past the negative edge - var r0 = ax.range[0], - r1 = ax.range[1]; - return 0.5*(r0 + r1 - 3 * clipMult * Math.abs(r0 - r1)); - } - - else return axes.BADNUM; - } - function fromLog(v){ return Math.pow(10,v); } - function num(v){ return isNumeric(v) ? Number(v) : axes.BADNUM; } - - ax.c2l = (ax.type==='log') ? toLog : num; - ax.l2c = (ax.type==='log') ? fromLog : num; - ax.l2d = function(v) { return ax.c2d(ax.l2c(v)); }; - ax.p2d = function(v) { return ax.l2d(ax.p2l(v)); }; - - // set scaling to pixels - ax.setScale = function(){ - var gs = ax._td._fullLayout._size, - i; - - // TODO cleaner way to handle this case - if (!ax._categories) ax._categories = []; - - // make sure we have a domain (pull it in from the axis - // this one is overlaying if necessary) - if(ax.overlaying) { - var ax2 = axes.getFromId(ax._td, ax.overlaying); - ax.domain = ax2.domain; - } - - // make sure we have a range (linearized data values) - // and that it stays away from the limits of javascript numbers - if(!ax.range || ax.range.length!==2 || ax.range[0]===ax.range[1]) { - ax.range = [-1,1]; - } - for(i=0; i<2; i++) { - if(!isNumeric(ax.range[i])) { - ax.range[i] = isNumeric(ax.range[1-i]) ? - (ax.range[1-i] * (i ? 10 : 0.1)) : - (i ? 1 : -1); - } - - if(ax.range[i]<-(Number.MAX_VALUE/2)) { - ax.range[i] = -(Number.MAX_VALUE/2); - } - else if(ax.range[i]>Number.MAX_VALUE/2) { - ax.range[i] = Number.MAX_VALUE/2; - } - - } - - if(ax._id.charAt(0)==='y') { - ax._offset = gs.t+(1-ax.domain[1])*gs.h; - ax._length = gs.h*(ax.domain[1]-ax.domain[0]); - ax._m = ax._length/(ax.range[0]-ax.range[1]); - ax._b = -ax._m*ax.range[1]; - } - else { - ax._offset = gs.l+ax.domain[0]*gs.w; - ax._length = gs.w*(ax.domain[1]-ax.domain[0]); - ax._m = ax._length/(ax.range[1]-ax.range[0]); - ax._b = -ax._m*ax.range[0]; - } - - if (!isFinite(ax._m) || !isFinite(ax._b)) { - Plotly.Lib.notifier( - 'Something went wrong with axis scaling', - 'long'); - ax._td._replotting = false; - throw new Error('axis scaling'); - } - }; - - ax.l2p = function(v) { - if(!isNumeric(v)) return axes.BADNUM; - // include 2 fractional digits on pixel, for PDF zooming etc - return d3.round(Plotly.Lib.constrain(ax._b + ax._m*v, - -clipMult*ax._length, (1+clipMult)*ax._length), 2); - }; - - ax.p2l = function(px) { return (px-ax._b)/ax._m; }; - - ax.c2p = function(v, clip) { return ax.l2p(ax.c2l(v, clip)); }; - ax.p2c = function(px){ return ax.l2c(ax.p2l(px)); }; - - if(['linear','log','-'].indexOf(ax.type)!==-1) { - ax.c2d = num; - ax.d2c = function(v){ - v = axes.cleanDatum(v); - return isNumeric(v) ? Number(v) : axes.BADNUM; - }; - ax.d2l = function(v, clip) { - if (ax.type === 'log') return ax.c2l(ax.d2c(v), clip); - else return ax.d2c(v); - }; - } - else if(ax.type==='date') { - ax.c2d = function(v) { - return isNumeric(v) ? Plotly.Lib.ms2DateTime(v) : axes.BADNUM; - }; - - ax.d2c = function(v){ - return (isNumeric(v)) ? Number(v) : Plotly.Lib.dateTime2ms(v); - }; - - ax.d2l = ax.d2c; - - // check if date strings or js date objects are provided for range - // and convert to ms - if(ax.range && ax.range.length>1) { - try { - var ar1 = ax.range.map(Plotly.Lib.dateTime2ms); - if(!isNumeric(ax.range[0]) && isNumeric(ar1[0])) { - ax.range[0] = ar1[0]; - } - if(!isNumeric(ax.range[1]) && isNumeric(ar1[1])) { - ax.range[1] = ar1[1]; - } - } - catch(e) { console.log(e, ax.range); } - } - } - else if(ax.type==='category') { - - ax.c2d = function(v) { - return ax._categories[Math.round(v)]; - }; - - ax.d2c = function(v) { - // create the category list - // this will enter the categories in the order it - // encounters them, ie all the categories from the - // first data set, then all the ones from the second - // that aren't in the first etc. - // TODO: sorting options - do the sorting - // progressively here as we insert? - if(ax._categories.indexOf(v)===-1) ax._categories.push(v); - - var c = ax._categories.indexOf(v); - return c===-1 ? axes.BADNUM : c; - }; - - ax.d2l = ax.d2c; - } - - // makeCalcdata: takes an x or y array and converts it - // to a position on the axis object "ax" - // inputs: - // tdc - a data object from td.data - // axletter - a string, either 'x' or 'y', for which item - // to convert (TODO: is this now always the same as - // the first letter of ax._id?) - // in case the expected data isn't there, make a list of - // integers based on the opposite data - ax.makeCalcdata = function(tdc, axletter) { - var arrayIn, arrayOut, i; - - if(axletter in tdc) { - arrayIn = tdc[axletter]; - arrayOut = new Array(arrayIn.length); - - for(i = 0; i < arrayIn.length; i++) arrayOut[i] = ax.d2c(arrayIn[i]); - } - else { - var v0 = ((axletter+'0') in tdc) ? - ax.d2c(tdc[axletter+'0']) : 0, - dv = (tdc['d'+axletter]) ? - Number(tdc['d'+axletter]) : 1; - - // the opposing data, for size if we have x and dx etc - arrayIn = tdc[{x: 'y',y: 'x'}[axletter]]; - arrayOut = new Array(arrayIn.length); - - for(i = 0; i < arrayIn.length; i++) arrayOut[i] = v0+i*dv; - } - return arrayOut; - }; - - // for autoranging: arrays of objects: - // {val: axis value, pad: pixel padding} - // on the low and high sides - ax._min = []; - ax._max = []; - - // and for bar charts and box plots: reset forced minimum tick spacing - ax._minDtick = null; - ax._forceTick0 = null; + var axLetter = id.charAt(0); + if(axLetter === 'x') return 'y'; + if(axLetter === 'y') return 'x'; }; // incorporate a new minimum difference and first tick into @@ -1081,8 +345,8 @@ axes.autoBin = function(data,ax,nbins,is2d) { datamax = Plotly.Lib.aggNums(Math.max, null, data); if(ax.type==='category') { return { - start: datamin-0.5, - end: datamax+0.5, + start: datamin - 0.5, + end: datamax + 0.5, size: 1 }; } @@ -1195,12 +459,12 @@ axes.calcTicks = function calcTicks(ax) { var nt = ax.nticks, minPx; if(!nt) { - if(ax.type==='category') { + if(ax.type === 'category') { minPx = ax.tickfont ? (ax.tickfont.size || 12) * 1.2 : 15; nt = ax._length / minPx; } else { - minPx = ax._id.charAt(0)==='y' ? 40 : 80; + minPx = ax._id.charAt(0) === 'y' ? 40 : 80; nt = Plotly.Lib.constrain(ax._length / minPx, 4, 9) + 1; } } @@ -1874,81 +1138,6 @@ function numSeparate(nStr, separators) { return x1 + x2; } -// get all axis object names -// optionally restricted to only x or y or z by string axLetter -// and optionally 2D axes only, not those inside 3D scenes -function listNames(td, axLetter, only2d) { - var fullLayout = td._fullLayout; - if(!fullLayout) return []; - - function filterAxis(obj, extra) { - var keys = Object.keys(obj), - axMatch = /^[xyz]axis[0-9]*/, - out = []; - - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; - if(axLetter && k.charAt(0) !== axLetter) continue; - if(axMatch.test(k)) out.push(extra + k); - } - - return out.sort(); - } - - var names = filterAxis(fullLayout, ''); - if(only2d) return names; - - var sceneIds3D = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d') || []; - for(var i = 0; i < sceneIds3D.length; i++) { - var sceneId = sceneIds3D[i]; - names = names.concat( - filterAxis(fullLayout[sceneId], sceneId + '.') - ); - } - - return names; -} - -// get all axis objects, as restricted in listNames -axes.list = function(td, axletter, only2d) { - return listNames(td, axletter, only2d) - .map(function(axName) { - return Plotly.Lib.nestedProperty(td._fullLayout, axName).get(); - }); -}; - -// get all axis ids, optionally restricted by letter -// this only makes sense for 2d axes -axes.listIds = function(td, axletter) { - return listNames(td, axletter, true).map(axes.name2id); -}; - -// get an axis object from its id 'x','x2' etc -// optionally, id can be a subplot (ie 'x2y3') and type gets x or y from it -axes.getFromId = function(td, id, type) { - var fullLayout = td._fullLayout; - - if(type==='x') id = id.replace(/y[0-9]*/,''); - else if(type==='y') id = id.replace(/x[0-9]*/,''); - - return fullLayout[axes.id2name(id)]; -}; - -// get an axis object of specified type from the containing trace -axes.getFromTrace = function(td, fullTrace, type) { - var fullLayout = td._fullLayout; - var ax = null; - if (Plotly.Plots.traceIs(fullTrace, 'gl3d')) { - var scene = fullTrace.scene; - if (scene.substr(0,5)==='scene') { - ax = fullLayout[scene][type + 'axis']; - } - } else { - ax = axes.getFromId(td, fullTrace[type + 'axis'] || type); - } - - return ax; -}; axes.subplotMatch = /^x([0-9]*)y([0-9]*)$/; @@ -2230,7 +1419,7 @@ axes.doTicks = function(td, axid, skipTitle) { var valsClipped = vals.filter(clipEnds); function drawTicks(container,tickpath) { - var ticks=container.selectAll('path.'+tcls) + var ticks = container.selectAll('path.'+tcls) .data(ax.ticks==='inside' ? valsClipped : vals, datafn); if(tickpath && ax.ticks) { ticks.enter().append('path').classed(tcls, 1).classed('ticks', 1) @@ -2244,7 +1433,7 @@ axes.doTicks = function(td, axid, skipTitle) { else ticks.remove(); } - function drawLabels(container,position) { + function drawLabels(container, position) { // tick labels - for now just the main labels. // TODO: mirror labels, esp for subplots var tickLabels=container.selectAll('g.'+tcls).data(vals, datafn); diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js new file mode 100644 index 00000000000..a1e112adb42 --- /dev/null +++ b/src/plots/cartesian/axis_defaults.js @@ -0,0 +1,259 @@ +/** +* Copyright 2012-2016, 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 Lib = require('../../lib'); +var Plots = require('../plots'); + +var layoutAttributes = require('./layout_attributes'); +var handleTickValueDefaults = require('./tick_value_defaults'); +var handleTickDefaults = require('./tick_defaults'); +var setConvert = require('./set_convert'); +var cleanDatum = require('./clean_datum'); +var axisIds = require('./axis_ids'); + + +/** + * options: object containing: + * + * letter: 'x' or 'y' + * title: name of the axis (ie 'Colorbar') to go in default title + * name: axis object name (ie 'xaxis') if one should be stored + * font: the default font to inherit + * outerTicks: boolean, should ticks default to outside? + * showGrid: boolean, should gridlines be shown by default? + * noHover: boolean, this axis doesn't support hover effects? + * data: the plot data to use in choosing auto type + */ +module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, options) { + var letter = options.letter, + font = options.font || {}, + defaultTitle = 'Click to enter ' + + (options.title || (letter.toUpperCase() + ' axis')) + + ' title'; + + // set up some private properties + if(options.name) { + containerOut._name = options.name; + containerOut._id = axisIds.name2id(options.name); + } + + // now figure out type and do some more initialization + var axType = coerce('type'); + if(axType === '-') { + setAutoType(containerOut, options.data); + + if(containerOut.type === '-') { + containerOut.type = 'linear'; + } + else { + // copy autoType back to input axis + // note that if this object didn't exist + // in the input layout, we have to put it in + // this happens in the main supplyDefaults function + axType = containerIn.type = containerOut.type; + } + } + + setConvert(containerOut); + + coerce('title', defaultTitle); + Lib.coerceFont(coerce, 'titlefont', { + family: font.family, + size: Math.round(font.size * 1.2), + color: font.color + }); + + var validRange = ( + (containerIn.range || []).length === 2 && + isNumeric(containerIn.range[0]) && + isNumeric(containerIn.range[1]) + ); + var autoRange = coerce('autorange', !validRange); + + if(autoRange) coerce('rangemode'); + var range = coerce('range', [-1, letter === 'x' ? 6 : 4]); + if(range[0] === range[1]) { + containerOut.range = [range[0] - 1, range[0] + 1]; + } + Lib.noneOrAll(containerIn.range, containerOut.range, [0, 1]); + + coerce('fixedrange'); + + handleTickValueDefaults(containerIn, containerOut, coerce, axType); + handleTickDefaults(containerIn, containerOut, coerce, axType, options); + + var lineColor = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'linecolor'), + lineWidth = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'linewidth'), + showLine = coerce('showline', !!lineColor || !!lineWidth); + + if(!showLine) { + delete containerOut.linecolor; + delete containerOut.linewidth; + } + + if(showLine || containerOut.ticks) coerce('mirror'); + + var gridColor = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'gridcolor'), + gridWidth = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'gridwidth'), + showGridLines = coerce('showgrid', options.showGrid || !!gridColor || !!gridWidth); + + if(!showGridLines) { + delete containerOut.gridcolor; + delete containerOut.gridwidth; + } + + var zeroLineColor = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'zerolinecolor'), + zeroLineWidth = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'zerolinewidth'), + showZeroLine = coerce('zeroline', options.showGrid || !!zeroLineColor || !!zeroLineWidth); + + if(!showZeroLine) { + delete containerOut.zerolinecolor; + delete containerOut.zerolinewidth; + } + + return containerOut; +}; + +function setAutoType(ax, data){ + // new logic: let people specify any type they want, + // only autotype if type is '-' + if(ax.type!=='-') return; + + var id = ax._id, + axLetter = id.charAt(0); + + // support 3d + if(id.indexOf('scene') !== -1) id = axLetter; + + var d0 = getFirstNonEmptyTrace(data, id, axLetter); + if(!d0) return; + + // first check for histograms, as the count direction + // should always default to a linear axis + if(d0.type==='histogram' && + axLetter === {v:'y', h:'x'}[d0.orientation || 'v']) { + ax.type='linear'; + return; + } + + // check all boxes on this x axis to see + // if they're dates, numbers, or categories + if(isBoxWithoutPositionCoords(d0, axLetter)) { + var posLetter = getBoxPosLetter(d0), + boxPositions = [], + trace; + + for(var i = 0; i < data.length; i++) { + trace = data[i]; + if(!Plots.traceIs(trace, 'box') || + (trace[axLetter + 'axis'] || axLetter) !== id) continue; + + if(trace[posLetter] !== undefined) boxPositions.push(trace[posLetter][0]); + else if(trace.name !== undefined) boxPositions.push(trace.name); + else boxPositions.push('text'); + } + + ax.type = autoType(boxPositions); + } + else { + ax.type = autoType(d0[axLetter] || [d0[axLetter+'0']]); + } +} + +function getBoxPosLetter(trace) { + return {v:'x', h:'y'}[trace.orientation || 'v']; +} + +function isBoxWithoutPositionCoords(trace, axLetter) { + var posLetter = getBoxPosLetter(trace); + + return ( + Plots.traceIs(trace, 'box') && + axLetter === posLetter && + trace[posLetter] === undefined && + trace[posLetter + '0'] === undefined + ); +} + +function autoType(array) { + if(moreDates(array)) return 'date'; + if(category(array)) return 'category'; + if(linearOK(array)) return 'linear'; + else return '-'; +} + +function getFirstNonEmptyTrace(data, id, axLetter) { + for(var i = 0; i < data.length; i++) { + var trace = data[i]; + + if((trace[axLetter + 'axis'] || axLetter) === id) { + if(isBoxWithoutPositionCoords(trace, axLetter)) { + return trace; + } + else if((trace[axLetter] || []).length || trace[axLetter + '0']) { + return trace; + } + } + } +} + +// is there at least one number in array? If not, we should leave +// ax.type empty so it can be autoset later +function linearOK(array) { + if(!array) return false; + + for(var i = 0; i < array.length; i++) { + if(isNumeric(array[i])) return true; + } + + return false; +} + +// does the array a have mostly dates rather than numbers? +// note: some values can be neither (such as blanks, text) +// 2- or 4-digit integers can be both, so require twice as many +// dates as non-dates, to exclude cases with mostly 2 & 4 digit +// numbers and a few dates +function moreDates(a) { + var dcnt = 0, + ncnt = 0, + // test at most 1000 points, evenly spaced + inc = Math.max(1, (a.length - 1) / 1000), + ai; + + for(var i = 0; i < a.length; i += inc) { + ai = a[Math.round(i)]; + if(Lib.isDateTime(ai)) dcnt += 1; + if(isNumeric(ai)) ncnt += 1; + } + + return (dcnt > ncnt * 2); +} + +// are the (x,y)-values in td.data mostly text? +// require twice as many categories as numbers +function category(a) { + // test at most 1000 points + var inc = Math.max(1, (a.length - 1) / 1000), + curvenums = 0, + curvecats = 0, + ai; + + for(var i = 0; i < a.length; i += inc) { + ai = cleanDatum(a[Math.round(i)]); + if(isNumeric(ai)) curvenums++; + else if(typeof ai === 'string' && ai !== '' && ai !== 'None') curvecats++; + } + + return curvecats > curvenums * 2; +} diff --git a/src/plots/cartesian/axis_ids.js b/src/plots/cartesian/axis_ids.js new file mode 100644 index 00000000000..0ff9626289c --- /dev/null +++ b/src/plots/cartesian/axis_ids.js @@ -0,0 +1,119 @@ +/** +* Copyright 2012-2016, 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 Plots = require('../plots'); +var Lib = require('../../lib'); + +var constants = require('./constants'); + + +// convert between axis names (xaxis, xaxis2, etc, elements of td.layout) +// and axis id's (x, x2, etc). Would probably have ditched 'xaxis' +// completely in favor of just 'x' if it weren't ingrained in the API etc. +exports.id2name = function id2name(id) { + if(typeof id !== 'string' || !id.match(constants.AX_ID_PATTERN)) return; + var axNum = id.substr(1); + if(axNum === '1') axNum = ''; + return id.charAt(0) + 'axis' + axNum; +}; + +exports.name2id = function name2id(name) { + if(!name.match(constants.AX_NAME_PATTERN)) return; + var axNum = name.substr(5); + if(axNum === '1') axNum = ''; + return name.charAt(0) + axNum; +}; + +exports.cleanId = function cleanId(id, axLetter) { + if(!id.match(constants.AX_ID_PATTERN)) return; + if(axLetter && id.charAt(0) !== axLetter) return; + + var axNum = id.substr(1).replace(/^0+/,''); + if(axNum === '1') axNum = ''; + return id.charAt(0) + axNum; +}; + +// get all axis object names +// optionally restricted to only x or y or z by string axLetter +// and optionally 2D axes only, not those inside 3D scenes +function listNames(td, axLetter, only2d) { + var fullLayout = td._fullLayout; + if(!fullLayout) return []; + + function filterAxis(obj, extra) { + var keys = Object.keys(obj), + axMatch = /^[xyz]axis[0-9]*/, + out = []; + + for(var i = 0; i < keys.length; i++) { + var k = keys[i]; + if(axLetter && k.charAt(0) !== axLetter) continue; + if(axMatch.test(k)) out.push(extra + k); + } + + return out.sort(); + } + + var names = filterAxis(fullLayout, ''); + if(only2d) return names; + + var sceneIds3D = Plots.getSubplotIds(fullLayout, 'gl3d') || []; + for(var i = 0; i < sceneIds3D.length; i++) { + var sceneId = sceneIds3D[i]; + names = names.concat( + filterAxis(fullLayout[sceneId], sceneId + '.') + ); + } + + return names; +} + +// get all axis objects, as restricted in listNames +exports.list = function(td, axletter, only2d) { + return listNames(td, axletter, only2d) + .map(function(axName) { + return Lib.nestedProperty(td._fullLayout, axName).get(); + }); +}; + +// get all axis ids, optionally restricted by letter +// this only makes sense for 2d axes +exports.listIds = function(td, axletter) { + return listNames(td, axletter, true).map(exports.name2id); +}; + +// get an axis object from its id 'x','x2' etc +// optionally, id can be a subplot (ie 'x2y3') and type gets x or y from it +exports.getFromId = function(td, id, type) { + var fullLayout = td._fullLayout; + + if(type === 'x') id = id.replace(/y[0-9]*/,''); + else if(type === 'y') id = id.replace(/x[0-9]*/,''); + + return fullLayout[exports.id2name(id)]; +}; + +// get an axis object of specified type from the containing trace +exports.getFromTrace = function(td, fullTrace, type) { + var fullLayout = td._fullLayout; + var ax = null; + + if(Plots.traceIs(fullTrace, 'gl3d')) { + var scene = fullTrace.scene; + if(scene.substr(0, 5) === 'scene') { + ax = fullLayout[scene][type + 'axis']; + } + } + else { + ax = exports.getFromId(td, fullTrace[type + 'axis'] || type); + } + + return ax; +}; diff --git a/src/plots/cartesian/clean_datum.js b/src/plots/cartesian/clean_datum.js new file mode 100644 index 00000000000..e4e32b92cea --- /dev/null +++ b/src/plots/cartesian/clean_datum.js @@ -0,0 +1,37 @@ +/** +* Copyright 2012-2016, 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 Lib = require('../../lib'); + + +/** + * cleanDatum: removes characters + * same replace criteria used in the grid.js:scrapeCol + * but also handling dates, numbers, and NaN, null, Infinity etc + */ +module.exports = function cleanDatum(c) { + try{ + if(typeof c === 'object' && c !== null && c.getTime) { + return Lib.ms2DateTime(c); + } + if(typeof c !== 'string' && !isNumeric(c)) { + return ''; + } + c = c.toString().replace(/['"%,$# ]/g, ''); + } + catch(e) { + console.log(e, c); + } + + return c; +}; diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index fb30322e895..77fa41ebdb3 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -10,6 +10,22 @@ module.exports = { + /** + * standardize all missing data in calcdata to use undefined + * never null or NaN. + * that way we can use !==undefined, or !== BADNUM, + * to test for real data + */ + BADNUM: undefined, + + // axis match regular expression + xAxisMatch: /^xaxis[0-9]*$/, + yAxisMatch: /^yaxis[0-9]*$/, + + // pattern matching axis ids and names + AX_ID_PATTERN: /^[xyz][0-9]*$/, + AX_NAME_PATTERN: /^[xyz]axis[0-9]*$/, + // ms between first mousedown and 2nd mouseup to constitute dblclick... // we don't seem to have access to the system setting DBLCLICKDELAY: 600, diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js new file mode 100644 index 00000000000..7d656c0463b --- /dev/null +++ b/src/plots/cartesian/layout_defaults.js @@ -0,0 +1,114 @@ +/** +* Copyright 2012-2016, 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 Plots = require('../plots'); + +var constants = require('./constants'); +var layoutAttributes = require('./layout_attributes'); +var handleAxisDefaults = require('./axis_defaults'); +var handlePositionDefaults = require('./position_defaults'); +var axisIds = require('./axis_ids'); + + +module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { + // get the full list of axes already defined + var layoutKeys = Object.keys(layoutIn), + xaList = [], + yaList = [], + outerTicks = {}, + noGrids = {}, + i; + + for(i = 0; i < layoutKeys.length; i++) { + var key = layoutKeys[i]; + if(constants.xAxisMatch.test(key)) xaList.push(key); + else if(constants.yAxisMatch.test(key)) yaList.push(key); + } + + for(i = 0; i < fullData.length; i++) { + var trace = fullData[i], + xaName = axisIds.id2name(trace.xaxis), + yaName = axisIds.id2name(trace.yaxis); + + // add axes implied by traces + if(xaName && xaList.indexOf(xaName) === -1) xaList.push(xaName); + if(yaName && yaList.indexOf(yaName) === -1) yaList.push(yaName); + + // check for default formatting tweaks + if(Plots.traceIs(trace, '2dMap')) { + outerTicks[xaName] = true; + outerTicks[yaName] = true; + } + + if(Plots.traceIs(trace, 'oriented')) { + var positionAxis = trace.orientation === 'h' ? yaName : xaName; + noGrids[positionAxis] = true; + } + } + + function axSort(a,b) { + var aNum = Number(a.substr(5)||1), + bNum = Number(b.substr(5)||1); + return aNum - bNum; + } + + if(layoutOut._hasCartesian || layoutOut._hasGL2D || !fullData.length) { + // make sure there's at least one of each and lists are sorted + if(!xaList.length) xaList = ['xaxis']; + else xaList.sort(axSort); + + if(!yaList.length) yaList = ['yaxis']; + else yaList.sort(axSort); + } + + xaList.concat(yaList).forEach(function(axName){ + var axLetter = axName.charAt(0), + axLayoutIn = layoutIn[axName] || {}, + axLayoutOut = {}, + defaultOptions = { + letter: axLetter, + font: layoutOut.font, + outerTicks: outerTicks[axName], + showGrid: !noGrids[axName], + name: axName, + data: fullData + }, + positioningOptions = { + letter: axLetter, + counterAxes: {x: yaList, y: xaList}[axLetter].map(axisIds.name2id), + overlayableAxes: {x: xaList, y: yaList}[axLetter].filter(function(axName2){ + return axName2!==axName && !(layoutIn[axName2]||{}).overlaying; + }).map(axisIds.name2id) + }; + + function coerce(attr, dflt) { + return Lib.coerce(axLayoutIn, axLayoutOut, layoutAttributes, attr, dflt); + } + + handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions); + handlePositionDefaults(axLayoutIn, axLayoutOut, coerce, positioningOptions); + layoutOut[axName] = axLayoutOut; + + // so we don't have to repeat autotype unnecessarily, + // copy an autotype back to layoutIn + if(!layoutIn[axName] && axLayoutIn.type!=='-') { + layoutIn[axName] = {type: axLayoutIn.type}; + } + + }); + + // plot_bgcolor only makes sense if there's a (2D) plot! + // TODO: bgcolor for each subplot, to inherit from the main one + if(xaList.length && yaList.length) { + Lib.coerce(layoutIn, layoutOut, Plots.layoutAttributes, 'plot_bgcolor'); + } +}; diff --git a/src/plots/cartesian/position_defaults.js b/src/plots/cartesian/position_defaults.js new file mode 100644 index 00000000000..97898d86f55 --- /dev/null +++ b/src/plots/cartesian/position_defaults.js @@ -0,0 +1,63 @@ +/** +* Copyright 2012-2016, 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 Lib = require('../../lib'); + + +module.exports = function handlePositionDefaults(containerIn, containerOut, coerce, options) { + var counterAxes = options.counterAxes || [], + overlayableAxes = options.overlayableAxes || [], + letter = options.letter; + + var anchor = Lib.coerce(containerIn, containerOut, { + anchor: { + valType:'enumerated', + values: ['free'].concat(counterAxes), + dflt: isNumeric(containerIn.position) ? 'free' : + (counterAxes[0] || 'free') + } + }, 'anchor'); + + if(anchor === 'free') coerce('position'); + + Lib.coerce(containerIn, containerOut, { + side: { + valType: 'enumerated', + values: letter === 'x' ? ['bottom', 'top'] : ['left', 'right'], + dflt: letter === 'x' ? 'bottom' : 'left' + } + }, 'side'); + + var overlaying = false; + if(overlayableAxes.length) { + overlaying = Lib.coerce(containerIn, containerOut, { + overlaying: { + valType: 'enumerated', + values: [false].concat(overlayableAxes), + dflt: false + } + }, 'overlaying'); + } + + if(!overlaying) { + // TODO: right now I'm copying this domain over to overlaying axes + // in ax.setscale()... but this means we still need (imperfect) logic + // in the axes popover to hide domain for the overlaying axis. + // perhaps I should make a private version _domain that all axes get??? + var domain = coerce('domain'); + if(domain[0] > domain[1] - 0.01) containerOut.domain = [0, 1]; + Lib.noneOrAll(containerIn.domain, containerOut.domain, [0, 1]); + } + + return containerOut; +}; diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js new file mode 100644 index 00000000000..a7d28d24655 --- /dev/null +++ b/src/plots/cartesian/set_convert.js @@ -0,0 +1,238 @@ +/** +* Copyright 2012-2016, 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 constants = require('./constants'); +var cleanDatum = require('./clean_datum'); +var axisIds = require('./axis_ids'); + + +/** + * Define the conversion functions for an axis data is used in 4 ways: + * + * d: data, in whatever form it's provided + * c: calcdata: turned into numbers, but not linearized + * l: linearized - same as c except for log axes (and other + * mappings later?) this is used by ranges, and when we + * need to know if it's *possible* to show some data on + * this axis, without caring about the current range + * p: pixel value - mapped to the screen with current size and zoom + * + * Creates/updates these conversion functions + * also clears the autorange bounds ._min and ._max + * and the autotick constraints ._minDtick, ._forceTick0, + * and looks for date ranges that aren't yet in numeric format + */ +module.exports = function setConvert(ax) { + + // clipMult: how many axis lengths past the edge do we render? + // for panning, 1-2 would suffice, but for zooming more is nice. + // also, clipping can affect the direction of lines off the edge... + var clipMult = 10; + + function toLog(v, clip){ + if(v>0) return Math.log(v)/Math.LN10; + + else if(v<=0 && clip && ax.range && ax.range.length===2) { + // clip NaN (ie past negative infinity) to clipMult axis + // length past the negative edge + var r0 = ax.range[0], + r1 = ax.range[1]; + return 0.5*(r0 + r1 - 3 * clipMult * Math.abs(r0 - r1)); + } + + else return constants.BADNUM; + } + function fromLog(v){ return Math.pow(10,v); } + function num(v){ return isNumeric(v) ? Number(v) : constants.BADNUM; } + + ax.c2l = (ax.type==='log') ? toLog : num; + ax.l2c = (ax.type==='log') ? fromLog : num; + ax.l2d = function(v) { return ax.c2d(ax.l2c(v)); }; + ax.p2d = function(v) { return ax.l2d(ax.p2l(v)); }; + + // set scaling to pixels + ax.setScale = function(){ + var gs = ax._td._fullLayout._size, + i; + + // TODO cleaner way to handle this case + if (!ax._categories) ax._categories = []; + + // make sure we have a domain (pull it in from the axis + // this one is overlaying if necessary) + if(ax.overlaying) { + var ax2 = axisIds.getFromId(ax._td, ax.overlaying); + ax.domain = ax2.domain; + } + + // make sure we have a range (linearized data values) + // and that it stays away from the limits of javascript numbers + if(!ax.range || ax.range.length!==2 || ax.range[0]===ax.range[1]) { + ax.range = [-1,1]; + } + for(i=0; i<2; i++) { + if(!isNumeric(ax.range[i])) { + ax.range[i] = isNumeric(ax.range[1-i]) ? + (ax.range[1-i] * (i ? 10 : 0.1)) : + (i ? 1 : -1); + } + + if(ax.range[i]<-(Number.MAX_VALUE/2)) { + ax.range[i] = -(Number.MAX_VALUE/2); + } + else if(ax.range[i]>Number.MAX_VALUE/2) { + ax.range[i] = Number.MAX_VALUE/2; + } + + } + + if(ax._id.charAt(0)==='y') { + ax._offset = gs.t+(1-ax.domain[1])*gs.h; + ax._length = gs.h*(ax.domain[1]-ax.domain[0]); + ax._m = ax._length/(ax.range[0]-ax.range[1]); + ax._b = -ax._m*ax.range[1]; + } + else { + ax._offset = gs.l+ax.domain[0]*gs.w; + ax._length = gs.w*(ax.domain[1]-ax.domain[0]); + ax._m = ax._length/(ax.range[1]-ax.range[0]); + ax._b = -ax._m*ax.range[0]; + } + + if (!isFinite(ax._m) || !isFinite(ax._b)) { + Lib.notifier( + 'Something went wrong with axis scaling', + 'long'); + ax._td._replotting = false; + throw new Error('axis scaling'); + } + }; + + ax.l2p = function(v) { + if(!isNumeric(v)) return constants.BADNUM; + // include 2 fractional digits on pixel, for PDF zooming etc + return d3.round(Lib.constrain(ax._b + ax._m*v, + -clipMult*ax._length, (1+clipMult)*ax._length), 2); + }; + + ax.p2l = function(px) { return (px-ax._b)/ax._m; }; + + ax.c2p = function(v, clip) { return ax.l2p(ax.c2l(v, clip)); }; + ax.p2c = function(px){ return ax.l2c(ax.p2l(px)); }; + + if(['linear','log','-'].indexOf(ax.type)!==-1) { + ax.c2d = num; + ax.d2c = function(v){ + v = cleanDatum(v); + return isNumeric(v) ? Number(v) : constants.BADNUM; + }; + ax.d2l = function(v, clip) { + if (ax.type === 'log') return ax.c2l(ax.d2c(v), clip); + else return ax.d2c(v); + }; + } + else if(ax.type==='date') { + ax.c2d = function(v) { + return isNumeric(v) ? Lib.ms2DateTime(v) : constants.BADNUM; + }; + + ax.d2c = function(v){ + return (isNumeric(v)) ? Number(v) : Lib.dateTime2ms(v); + }; + + ax.d2l = ax.d2c; + + // check if date strings or js date objects are provided for range + // and convert to ms + if(ax.range && ax.range.length>1) { + try { + var ar1 = ax.range.map(Lib.dateTime2ms); + if(!isNumeric(ax.range[0]) && isNumeric(ar1[0])) { + ax.range[0] = ar1[0]; + } + if(!isNumeric(ax.range[1]) && isNumeric(ar1[1])) { + ax.range[1] = ar1[1]; + } + } + catch(e) { console.log(e, ax.range); } + } + } + else if(ax.type==='category') { + + ax.c2d = function(v) { + return ax._categories[Math.round(v)]; + }; + + ax.d2c = function(v) { + // create the category list + // this will enter the categories in the order it + // encounters them, ie all the categories from the + // first data set, then all the ones from the second + // that aren't in the first etc. + // TODO: sorting options - do the sorting + // progressively here as we insert? + if(ax._categories.indexOf(v)===-1) ax._categories.push(v); + + var c = ax._categories.indexOf(v); + return c===-1 ? constants.BADNUM : c; + }; + + ax.d2l = ax.d2c; + } + + // makeCalcdata: takes an x or y array and converts it + // to a position on the axis object "ax" + // inputs: + // tdc - a data object from td.data + // axletter - a string, either 'x' or 'y', for which item + // to convert (TODO: is this now always the same as + // the first letter of ax._id?) + // in case the expected data isn't there, make a list of + // integers based on the opposite data + ax.makeCalcdata = function(tdc, axletter) { + var arrayIn, arrayOut, i; + + if(axletter in tdc) { + arrayIn = tdc[axletter]; + arrayOut = new Array(arrayIn.length); + + for(i = 0; i < arrayIn.length; i++) arrayOut[i] = ax.d2c(arrayIn[i]); + } + else { + var v0 = ((axletter+'0') in tdc) ? + ax.d2c(tdc[axletter+'0']) : 0, + dv = (tdc['d'+axletter]) ? + Number(tdc['d'+axletter]) : 1; + + // the opposing data, for size if we have x and dx etc + arrayIn = tdc[{x: 'y',y: 'x'}[axletter]]; + arrayOut = new Array(arrayIn.length); + + for(i = 0; i < arrayIn.length; i++) arrayOut[i] = v0+i*dv; + } + return arrayOut; + }; + + // for autoranging: arrays of objects: + // {val: axis value, pad: pixel padding} + // on the low and high sides + ax._min = []; + ax._max = []; + + // and for bar charts and box plots: reset forced minimum tick spacing + ax._minDtick = null; + ax._forceTick0 = null; +}; diff --git a/src/plots/cartesian/tick_defaults.js b/src/plots/cartesian/tick_defaults.js new file mode 100644 index 00000000000..a6a2585b2c1 --- /dev/null +++ b/src/plots/cartesian/tick_defaults.js @@ -0,0 +1,84 @@ +/** +* Copyright 2012-2016, 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 layoutAttributes = require('./layout_attributes'); + + +/** + * options: inherits font, outerTicks, noHover from axes.handleAxisDefaults + */ +module.exports = function handleTickDefaults(containerIn, containerOut, coerce, axType, options) { + var tickLen = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'ticklen'), + tickWidth = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'tickwidth'), + tickColor = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'tickcolor'), + showTicks = coerce('ticks', (options.outerTicks || tickLen || tickWidth || tickColor) ? 'outside' : ''); + + if(!showTicks) { + delete containerOut.ticklen; + delete containerOut.tickwidth; + delete containerOut.tickcolor; + } + + var showTickLabels = coerce('showticklabels'); + if(showTickLabels) { + Lib.coerceFont(coerce, 'tickfont', options.font || {}); + coerce('tickangle'); + + var showAttrDflt = getShowAttrDflt(containerIn); + + if(axType !== 'category') { + var tickFormat = coerce('tickformat'); + if(!options.noHover) coerce('hoverformat'); + + if(!tickFormat && axType !== 'date') { + coerce('showexponent', showAttrDflt); + coerce('exponentformat'); + } + } + + var tickPrefix = coerce('tickprefix'); + if(tickPrefix) coerce('showtickprefix', showAttrDflt); + + var tickSuffix = coerce('ticksuffix'); + if(tickSuffix) coerce('showticksuffix', showAttrDflt); + } +}; + +/* + * Attributes 'showexponent', 'showtickprefix' and 'showticksuffix' + * share values. + * + * If only 1 attribute is set, + * the remaining attributes inherit that value. + * + * If 2 attributes are set to the same value, + * the remaining attribute inherits that value. + * + * If 2 attributes are set to different values, + * the remaining is set to its dflt value. + * + */ +function getShowAttrDflt(containerIn) { + var showAttrsAll = ['showexponent', + 'showtickprefix', + 'showticksuffix'], + showAttrs = showAttrsAll.filter(function(a){ + return containerIn[a]!==undefined; + }), + sameVal = function(a){ + return containerIn[a]===containerIn[showAttrs[0]]; + }; + if (showAttrs.every(sameVal) || showAttrs.length===1) { + return containerIn[showAttrs[0]]; + } +} diff --git a/src/plots/cartesian/tick_value_defaults.js b/src/plots/cartesian/tick_value_defaults.js new file mode 100644 index 00000000000..b51994b5ce7 --- /dev/null +++ b/src/plots/cartesian/tick_value_defaults.js @@ -0,0 +1,39 @@ +/** +* Copyright 2012-2016, 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'); + + +module.exports = function handleTickValueDefaults(containerIn, containerOut, coerce, axType) { + var tickmodeDefault = 'auto'; + + if(containerIn.tickmode === 'array' && + (axType === 'log' || axType === 'date')) { + containerIn.tickmode = 'auto'; + } + + if(Array.isArray(containerIn.tickvals)) tickmodeDefault = 'array'; + else if(containerIn.dtick && isNumeric(containerIn.dtick)) { + tickmodeDefault = 'linear'; + } + var tickmode = coerce('tickmode', tickmodeDefault); + + if(tickmode === 'auto') coerce('nticks'); + else if(tickmode === 'linear') { + coerce('tick0'); + coerce('dtick'); + } + else { + var tickvals = coerce('tickvals'); + if(tickvals === undefined) containerOut.tickmode = 'auto'; + else coerce('ticktext'); + } +}; diff --git a/src/plots/gl3d/layout/axis_defaults.js b/src/plots/gl3d/layout/axis_defaults.js index f44ca510854..7abad5d2edd 100644 --- a/src/plots/gl3d/layout/axis_defaults.js +++ b/src/plots/gl3d/layout/axis_defaults.js @@ -9,19 +9,19 @@ 'use strict'; -var Plotly = require('../../../plotly'); +var Lib = require('../../../lib'); var layoutAttributes = require('./axis_attributes'); +var handleAxisDefaults = require('../../cartesian/axis_defaults'); var axesNames = ['xaxis', 'yaxis', 'zaxis']; var noop = function() {}; module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) { - var Axes = Plotly.Axes; var containerIn, containerOut; function coerce(attr, dflt) { - return Plotly.Lib.coerce(containerIn, containerOut, layoutAttributes, attr, dflt); + return Lib.coerce(containerIn, containerOut, layoutAttributes, attr, dflt); } for (var j = 0; j < axesNames.length; j++) { @@ -33,11 +33,10 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) { _name: axName }; - layoutOut[axName] = containerOut = Axes.handleAxisDefaults( + layoutOut[axName] = containerOut = handleAxisDefaults( containerIn, containerOut, - coerce, - { + coerce, { font: options.font, letter: axName[0], data: options.data, diff --git a/src/plots/gl3d/set_convert.js b/src/plots/gl3d/set_convert.js index 3e73e511be7..c5dcddfe3ec 100644 --- a/src/plots/gl3d/set_convert.js +++ b/src/plots/gl3d/set_convert.js @@ -9,12 +9,12 @@ 'use strict'; -var Plotly = require('../../plotly'); +var Axes = require('../cartesian/axes'); var noop = function() {}; module.exports = function setConvert(containerOut) { - Plotly.Axes.setConvert(containerOut); + Axes.setConvert(containerOut); containerOut.setScale = noop; }; diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 0d223be78d8..b9866b580b3 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1,4 +1,11 @@ -var Plotly = require('@src/plotly'); +var PlotlyInternal = require('@src/plotly'); + +var Plots = require('@src/plots/plots'); +var Lib = require('@src/lib'); +var Color = require('@src/components/color'); + +var handleTickValueDefaults = require('@src/plots/cartesian/tick_value_defaults'); +var Axes = PlotlyInternal.Axes; describe('Test axes', function() { @@ -28,15 +35,15 @@ describe('Test axes', function() { } } }; - var expectedYaxis = Plotly.Lib.extendDeep({}, gd.layout.xaxis), + var expectedYaxis = Lib.extendDeep({}, gd.layout.xaxis), expectedXaxis = { title: 'Click to enter X axis title', type: 'date' }; - Plotly.Plots.supplyDefaults(gd); + Plots.supplyDefaults(gd); - Plotly.Axes.swap(gd, [0]); + Axes.swap(gd, [0]); expect(gd.layout.xaxis).toEqual(expectedXaxis); expect(gd.layout.yaxis).toEqual(expectedYaxis); @@ -61,13 +68,13 @@ describe('Test axes', function() { } } }; - var expectedLayoutAfter = Plotly.Lib.extendDeep({}, gd.layout); + var expectedLayoutAfter = Lib.extendDeep({}, gd.layout); expectedLayoutAfter.xaxis.type = 'linear'; expectedLayoutAfter.yaxis.type = 'linear'; - Plotly.Plots.supplyDefaults(gd); + Plots.supplyDefaults(gd); - Plotly.Axes.swap(gd, [0]); + Axes.swap(gd, [0]); expect(gd.layout.xaxis).toEqual(expectedLayoutAfter.xaxis); expect(gd.layout.yaxis).toEqual(expectedLayoutAfter.yaxis); @@ -145,9 +152,9 @@ describe('Test axes', function() { {x: 5, y: 0.5, xref: 'x', yref: 'paper'} ]; - Plotly.Plots.supplyDefaults(gd); + Plots.supplyDefaults(gd); - Plotly.Axes.swap(gd, [0, 1]); + Axes.swap(gd, [0, 1]); expect(gd.layout.xaxis).toEqual(expectedXaxis); expect(gd.layout.xaxis2).toEqual(expectedXaxis2); @@ -161,7 +168,7 @@ describe('Test axes', function() { layoutOut = {}, fullData = []; - var supplyLayoutDefaults = Plotly.Axes.supplyLayoutDefaults; + var supplyLayoutDefaults = Axes.supplyLayoutDefaults; it('should set undefined linewidth/linecolor if linewidth, linecolor or showline is not supplied', function() { layoutIn = { @@ -181,7 +188,7 @@ describe('Test axes', function() { }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.xaxis.linewidth).toBe(1); - expect(layoutOut.xaxis.linecolor).toBe(Plotly.Color.defaultLine); + expect(layoutOut.xaxis.linecolor).toBe(Color.defaultLine); }); it('should set linewidth to default if linecolor is supplied and valid', function() { @@ -199,7 +206,7 @@ describe('Test axes', function() { }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.yaxis.linewidth).toBe(2); - expect(layoutOut.yaxis.linecolor).toBe(Plotly.Color.defaultLine); + expect(layoutOut.yaxis.linecolor).toBe(Color.defaultLine); }); it('should set default gridwidth and gridcolor', function() { @@ -209,9 +216,9 @@ describe('Test axes', function() { }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.xaxis.gridwidth).toBe(1); - expect(layoutOut.xaxis.gridcolor).toBe(Plotly.Color.lightLine); + expect(layoutOut.xaxis.gridcolor).toBe(Color.lightLine); expect(layoutOut.yaxis.gridwidth).toBe(1); - expect(layoutOut.yaxis.gridcolor).toBe(Plotly.Color.lightLine); + expect(layoutOut.yaxis.gridcolor).toBe(Color.lightLine); }); it('should set gridcolor/gridwidth to undefined if showgrid is false', function() { @@ -230,9 +237,9 @@ describe('Test axes', function() { }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut.xaxis.zerolinewidth).toBe(1); - expect(layoutOut.xaxis.zerolinecolor).toBe(Plotly.Color.defaultLine); + expect(layoutOut.xaxis.zerolinecolor).toBe(Color.defaultLine); expect(layoutOut.yaxis.zerolinewidth).toBe(1); - expect(layoutOut.yaxis.zerolinecolor).toBe(Plotly.Color.defaultLine); + expect(layoutOut.yaxis.zerolinecolor).toBe(Color.defaultLine); }); it('should set zerolinecolor/zerolinewidth to undefined if zeroline is false', function() { @@ -246,82 +253,80 @@ describe('Test axes', function() { }); describe('handleTickValueDefaults', function() { - function handleTickValueDefaults(axIn, axOut, axType) { + function mockSupplyDefaults(axIn, axOut, axType) { function coerce(attr, dflt) { - return Plotly.Lib.coerce(axIn, axOut, - Plotly.Axes.layoutAttributes, - attr, dflt); + return Lib.coerce(axIn, axOut, Axes.layoutAttributes, attr, dflt); } - Plotly.Axes.handleTickValueDefaults(axIn, axOut, coerce, axType); + handleTickValueDefaults(axIn, axOut, coerce, axType); } it('should set default tickmode correctly', function() { var axIn = {}, axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tickmode).toBe('auto'); axIn = {tickmode: 'array', tickvals: 'stuff'}; axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tickmode).toBe('auto'); axIn = {tickmode: 'array', tickvals: [1, 2, 3]}; axOut = {}; - handleTickValueDefaults(axIn, axOut, 'date'); + mockSupplyDefaults(axIn, axOut, 'date'); expect(axOut.tickmode).toBe('auto'); axIn = {tickvals: [1, 2, 3]}; axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tickmode).toBe('array'); axIn = {dtick: 1}; axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tickmode).toBe('linear'); }); it('should set nticks iff tickmode=auto', function() { var axIn = {}, axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.nticks).toBe(0); axIn = {tickmode: 'auto', nticks: 5}; axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.nticks).toBe(5); axIn = {tickmode: 'linear', nticks: 15}; axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.nticks).toBe(undefined); }); it('should set tick0 and dtick iff tickmode=linear', function() { var axIn = {tickmode: 'auto', tick0: 1, dtick: 1}, axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tick0).toBe(undefined); expect(axOut.dtick).toBe(undefined); axIn = {tickvals: [1,2,3], tick0: 1, dtick: 1}; axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tick0).toBe(undefined); expect(axOut.dtick).toBe(undefined); axIn = {tick0: 2.71, dtick: 0.00828}; axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tick0).toBe(2.71); expect(axOut.dtick).toBe(0.00828); axIn = {tickmode: 'linear', tick0: 3.14, dtick: 0.00159}; axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tick0).toBe(3.14); expect(axOut.dtick).toBe(0.00159); }); @@ -329,20 +334,20 @@ describe('Test axes', function() { it('should set tickvals and ticktext iff tickmode=array', function() { var axIn = {tickmode: 'auto', tickvals: [1,2,3], ticktext: ['4','5','6']}, axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tickvals).toBe(undefined); expect(axOut.ticktext).toBe(undefined); axIn = {tickvals: [2,4,6,8], ticktext: ['who','do','we','appreciate']}; axOut = {}; - handleTickValueDefaults(axIn, axOut, 'linear'); + mockSupplyDefaults(axIn, axOut, 'linear'); expect(axOut.tickvals).toEqual([2,4,6,8]); expect(axOut.ticktext).toEqual(['who','do','we','appreciate']); }); }); describe('saveRangeInitial', function() { - var saveRangeInitial = Plotly.Axes.saveRangeInitial; + var saveRangeInitial = Axes.saveRangeInitial; var gd, hasOneAxisChanged; beforeEach(function() { @@ -401,7 +406,7 @@ describe('Test axes', function() { }); describe('list', function() { - var listFunc = Plotly.Axes.list; + var listFunc = Axes.list; var gd; it('returns empty array when no fullLayout is present', function() { @@ -484,7 +489,7 @@ describe('Test axes', function() { }); describe('getSubplots', function() { - var getSubplots = Plotly.Axes.getSubplots; + var getSubplots = Axes.getSubplots; var gd; it('returns list of subplots ids (from data only)', function() {