diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index 18d776a2873..a87982e3628 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -187,6 +187,7 @@ module.exports = function draw(gd, id) { titlefont: opts.titlefont, showline: true, anchor: 'free', + side: 'right', position: 1 }, cbAxisOut = { @@ -281,7 +282,8 @@ module.exports = function draw(gd, id) { Math.round(gs.l) + ',-' + Math.round(gs.t) + ')'); - cbAxisOut._axislayer = container.select('.cbaxis'); + var axisLayer = container.select('.cbaxis'); + var titleHeight = 0; if(['top', 'bottom'].indexOf(opts.titleside) !== -1) { // draw the title so we know how much room it needs @@ -357,8 +359,7 @@ module.exports = function draw(gd, id) { .attr('transform', 'translate(0,' + Math.round(gs.h * (1 - cbAxisOut.domain[1])) + ')'); - cbAxisOut._axislayer.attr('transform', 'translate(0,' + - Math.round(-gs.t) + ')'); + axisLayer.attr('transform', 'translate(0,' + Math.round(-gs.t) + ')'); var fills = container.select('.cbfills') .selectAll('rect.cbfill') @@ -425,12 +426,7 @@ module.exports = function draw(gd, id) { }); // force full redraw of labels and ticks - cbAxisOut._axislayer.selectAll('g.' + cbAxisOut._id + 'tick,path') - .remove(); - - cbAxisOut._pos = xLeft + thickPx + - (opts.outlinewidth||0) / 2 - (opts.ticks === 'outside' ? 1 : 0); - cbAxisOut.side = 'right'; + axisLayer.selectAll('g.' + cbAxisOut._id + 'tick,path').remove(); // separate out axis and title drawing, // so we don't need such complicated logic in Titles.draw @@ -438,7 +434,29 @@ module.exports = function draw(gd, id) { // this title call only handles side=right return Lib.syncOrAsync([ function() { - return Axes.doTicksSingle(gd, cbAxisOut, true); + var shift = xLeft + thickPx + + (opts.outlinewidth || 0) / 2 - (opts.ticks === 'outside' ? 1 : 0); + + var vals = Axes.calcTicks(cbAxisOut); + var transFn = Axes.makeTransFn(cbAxisOut); + var labelFns = Axes.makeLabelFns(cbAxisOut, shift); + var tickSign = Axes.getTickSigns(cbAxisOut)[2]; + + Axes.drawTicks(gd, cbAxisOut, { + vals: cbAxisOut.ticks === 'inside' ? Axes.clipEnds(cbAxisOut, vals) : vals, + layer: axisLayer, + path: Axes.makeTickPath(cbAxisOut, shift, tickSign), + transFn: transFn + }); + + return Axes.drawLabels(gd, cbAxisOut, { + vals: vals, + layer: axisLayer, + transFn: transFn, + labelXFn: labelFns.labelXFn, + labelYFn: labelFns.labelYFn, + labelAnchorFn: labelFns.labelAnchorFn + }); }, function() { if(['top', 'bottom'].indexOf(opts.titleside) === -1) { @@ -499,7 +517,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 + - Drawing.bBox(cbAxisOut._axislayer.node()).width; + Drawing.bBox(axisLayer.node()).width; titleEl = titleCont.select('text'); if(titleEl.node() && !titleEl.classed(cn.jsPlaceholder)) { var mathJaxNode = titleCont diff --git a/src/core.js b/src/core.js index 1025c37d4a7..4a796c94190 100644 --- a/src/core.js +++ b/src/core.js @@ -18,7 +18,7 @@ require('es6-promise').polyfill(); require('../build/plotcss'); // inject default MathJax config -require('./fonts/mathjax_config'); +require('./fonts/mathjax_config')(); // include registry module and expose register method var Registry = require('./registry'); diff --git a/src/fonts/mathjax_config.js b/src/fonts/mathjax_config.js index a810af204a0..cb15dd2b2d8 100644 --- a/src/fonts/mathjax_config.js +++ b/src/fonts/mathjax_config.js @@ -10,26 +10,20 @@ /* global MathJax:false */ -/** - * Check and configure MathJax - */ -if(typeof MathJax !== 'undefined') { - exports.MathJax = true; - - var globalConfig = (window.PlotlyConfig || {}).MathJaxConfig !== 'local'; +module.exports = function() { + if(typeof MathJax !== 'undefined') { + var globalConfig = (window.PlotlyConfig || {}).MathJaxConfig !== 'local'; - if(globalConfig) { - MathJax.Hub.Config({ - messageStyle: 'none', - skipStartupTypeset: true, - displayAlign: 'left', - tex2jax: { - inlineMath: [['$', '$'], ['\\(', '\\)']] - } - }); - MathJax.Hub.Configured(); + if(globalConfig) { + MathJax.Hub.Config({ + messageStyle: 'none', + skipStartupTypeset: true, + displayAlign: 'left', + tex2jax: { + inlineMath: [['$', '$'], ['\\(', '\\)']] + } + }); + MathJax.Hub.Configured(); + } } - -} else { - exports.MathJax = false; -} +}; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index dfc66dd9868..93dd63c3cd4 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -350,7 +350,7 @@ exports.plot = function(gd, data, layout, config) { // draw ticks, titles, and calculate axis scaling (._b, ._m) function drawAxes() { - return Axes.doTicks(gd, graphWasEmpty ? '' : 'redraw'); + return Axes.draw(gd, graphWasEmpty ? '' : 'redraw'); } var seq = [ @@ -1797,13 +1797,13 @@ function addAxRangeSequence(seq, rangesAltered) { // N.B. leave as sequence of subroutines (for now) instead of // subroutine of its own so that finalDraw always gets // executed after drawData - var doTicks = rangesAltered ? - function(gd) { return Axes.doTicks(gd, Object.keys(rangesAltered), true); } : - function(gd) { return Axes.doTicks(gd, 'redraw'); }; + var drawAxes = rangesAltered ? + function(gd) { return Axes.draw(gd, Object.keys(rangesAltered), {skipTitle: true}); } : + function(gd) { return Axes.draw(gd, 'redraw'); }; seq.push( subroutines.doAutoRangeAndConstraints, - doTicks, + drawAxes, subroutines.drawData, subroutines.finalDraw ); diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 07445cd3816..9a6bcfe5814 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -539,7 +539,7 @@ exports.doLegend = function(gd) { }; exports.doTicksRelayout = function(gd) { - Axes.doTicks(gd, 'redraw'); + Axes.draw(gd, 'redraw'); if(gd._fullLayout._hasOnlyLargeSploms) { Registry.subplotsRegistry.splom.updateGrid(gd); diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index c2352fcdc33..3f1464aa9fb 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1472,14 +1472,14 @@ axes.findSubplotsWithAxis = function(subplots, ax) { var axMatch = new RegExp( (ax._id.charAt(0) === 'x') ? ('^' + ax._id + 'y') : (ax._id + '$') ); - var subplotsWithAxis = []; + var subplotsWithAx = []; for(var i = 0; i < subplots.length; i++) { var sp = subplots[i]; - if(axMatch.test(sp)) subplotsWithAxis.push(sp); + if(axMatch.test(sp)) subplotsWithAx.push(sp); } - return subplotsWithAxis; + return subplotsWithAx; }; // makeClipPaths: prepare clipPaths for all single axes and all possible xy pairings @@ -1532,24 +1532,25 @@ axes.makeClipPaths = function(gd) { * * @param {DOM element} gd : graph div * @param {string or array of strings} arg : polymorphic argument - * @param {boolean} skipTitle : optional flag to skip axis title draw/update + * @param {object} opts: + * - @param {boolean} skipTitle : optional flag to skip axis title draw/update * - * Signature 1: Axes.doTicks(gd, 'redraw') + * Signature 1: Axes.draw(gd, 'redraw') * use this to clear and redraw all axes on graph * - * Signature 2: Axes.doTicks(gd, '') + * Signature 2: Axes.draw(gd, '') * use this to draw all axes on graph w/o the selectAll().remove() * of the 'redraw' signature * - * Signature 3: Axes.doTicks(gd, [axId, axId2, ...]) + * Signature 3: Axes.draw(gd, [axId, axId2, ...]) * where the items are axis id string, * use this to update multiple axes in one call * - * N.B doTicks updates: + * N.B draw updates: * - ax._r (stored range for use by zoom/pan) * - ax._rl (stored linearized range for use by zoom/pan) */ -axes.doTicks = function(gd, arg, skipTitle) { +axes.draw = function(gd, arg, opts) { var fullLayout = gd._fullLayout; if(arg === 'redraw') { @@ -1570,13 +1571,13 @@ axes.doTicks = function(gd, arg, skipTitle) { var axList = (!arg || arg === 'redraw') ? axes.listIds(gd) : arg; - Lib.syncOrAsync(axList.map(function(axid) { + Lib.syncOrAsync(axList.map(function(axId) { return function() { - if(!axid) return; + if(!axId) return; - var axDone = axes.doTicksSingle(gd, axid, skipTitle); + var ax = axes.getFromId(gd, axId); + var axDone = axes.drawOne(gd, ax, opts); - var ax = axes.getFromId(gd, axid); ax._r = ax.range.slice(); ax._rl = Lib.simpleMap(ax._r, ax.r2l); @@ -1586,683 +1587,812 @@ axes.doTicks = function(gd, arg, skipTitle) { }; /** - * Per-axis drawing routine! + * Draw one cartesian axis * - * This routine draws axis ticks and much more (... grids, labels, title etc.) - * Supports multiple argument signatures. - * N.B. this thing is async in general (because of MathJax rendering) - * - * @param {DOM element} gd : graph div - * @param {string or object} arg : polymorphic argument - * @param {boolean} skipTitle : optional flag to skip axis title draw/update - * @return {promise} - * - * Signature 1: Axes.doTicks(gd, ax) - * where ax is an axis object as in fullLayout - * - * Signature 2: Axes.doTicks(gd, axId) - * where axId is a axis id string + * @param {DOM element} gd + * @param {object} ax (full) axis object + * @param {object} opts + * - @param {boolean} skipTitle (set to true to skip axis title draw call) */ -axes.doTicksSingle = function(gd, arg, skipTitle) { - var fullLayout = gd._fullLayout; - var independent = false; - var ax; +axes.drawOne = function(gd, ax, opts) { + opts = opts || {}; - if(Lib.isPlainObject(arg)) { - ax = arg; - independent = true; - } else { - ax = axes.getFromId(gd, arg); - } + var i, sp, plotinfo; - // set scaling to pixels ax.setScale(); - var axid = ax._id; - var axLetter = axid.charAt(0); - var counterLetter = axes.counterLetter(axid); - var vals = ax._vals = axes.calcTicks(ax); - var datafn = function(d) { return [d.text, d.x, ax.mirror, d.font, d.fontSize, d.fontColor].join('_'); }; - var tcls = axid + 'tick'; - var gcls = axid + 'grid'; - var zcls = axid + 'zl'; - var pad = (ax.linewidth || 1) / 2; - var labelStandoff = (ax.ticks === 'outside' ? ax.ticklen : 0); - var labelShift = 0; - var gridWidth = Drawing.crispRound(gd, ax.gridwidth, 1); - var zeroLineWidth = Drawing.crispRound(gd, ax.zerolinewidth, gridWidth); - var tickWidth = Drawing.crispRound(gd, ax.tickwidth, 1); - var sides, transfn, tickpathfn, subplots; - var tickLabels; - var i; + var fullLayout = gd._fullLayout; + var axId = ax._id; + var axLetter = axId.charAt(0); + var counterLetter = axes.counterLetter(axId); + var mainSubplot = ax._mainSubplot; + var mainPlotinfo = fullLayout._plots[mainSubplot]; + var subplotsWithAx = axes.getSubplots(gd, ax); - if(ax._counterangle && ax.ticks === 'outside') { - var caRad = ax._counterangle * Math.PI / 180; - labelStandoff = ax.ticklen * Math.cos(caRad) + 1; - labelShift = ax.ticklen * Math.sin(caRad); - } + var vals = ax._vals = axes.calcTicks(ax); + // We remove zero lines, grid lines, and inside ticks if they're within 1px of the end + // The key case here is removing zero lines when the axis bound is zero + var valsClipped = ax._valsClipped = axes.clipEnds(ax, vals); - if(ax.showticklabels && (ax.ticks === 'outside' || ax.showline)) { - labelStandoff += 0.2 * ax.tickfont.size; - } + if(!ax.visible) return; - // positioning arguments for x vs y axes - if(axLetter === 'x') { - sides = ['bottom', 'top']; - transfn = ax._transfn || function(d) { - return 'translate(' + (ax._offset + ax.l2p(d.x)) + ',0)'; - }; - tickpathfn = function(shift, len) { - if(ax._counterangle) { - var caRad = ax._counterangle * Math.PI / 180; - return 'M0,' + shift + 'l' + (Math.sin(caRad) * len) + ',' + (Math.cos(caRad) * len); - } - else return 'M0,' + shift + 'v' + len; - }; - } - else if(axLetter === 'y') { - sides = ['left', 'right']; - transfn = ax._transfn || function(d) { - return 'translate(0,' + (ax._offset + ax.l2p(d.x)) + ')'; - }; - tickpathfn = function(shift, len) { - if(ax._counterangle) { - var caRad = ax._counterangle * Math.PI / 180; - return 'M' + shift + ',0l' + (Math.cos(caRad) * len) + ',' + (-Math.sin(caRad) * len); - } - else return 'M' + shift + ',0h' + len; - }; - } - else if(isAngular(ax)) { - sides = ['left', 'right']; - transfn = ax._transfn; - tickpathfn = function(shift, len) { - return 'M' + shift + ',0h' + len; - }; - } - else { - Lib.warn('Unrecognized doTicks axis:', axid); - return; - } + var transFn = axes.makeTransFn(ax); - var axside = ax.side || sides[0]; - // which direction do the side[0], side[1], and free ticks go? - // then we flip if outside XOR y axis - var ticksign = [-1, 1, axside === sides[1] ? 1 : -1]; - if((ax.ticks !== 'inside') === (axLetter === 'x')) { - ticksign = ticksign.map(function(v) { return -v; }); - } - - if(!ax.visible) return; + if(!fullLayout._hasOnlyLargeSploms) { + // keep track of which subplots (by main conteraxis) we've already + // drawn grids for, so we don't overdraw overlaying subplots + var finishedGrids = {}; - if(ax._tickFilter) { - vals = vals.filter(ax._tickFilter); - } + for(i = 0; i < subplotsWithAx.length; i++) { + sp = subplotsWithAx[i]; + plotinfo = fullLayout._plots[sp]; - // Remove zero lines, grid lines, and inside ticks if they're within - // 1 pixel of the end. - // The key case here is removing zero lines when the axis bound is zero. - // Don't clip angular values. - var valsClipped = ax._valsClipped = isAngular(ax) ? - vals : - vals.filter(function(d) { return clipEnds(ax, d.x); }); + var counterAxis = plotinfo[counterLetter + 'axis']; + var mainCounterID = counterAxis._mainAxis._id; + if(finishedGrids[mainCounterID]) continue; + finishedGrids[mainCounterID] = 1; - function drawTicks(container, tickpath) { - var ticks = container.selectAll('path.' + tcls) - .data(ax.ticks === 'inside' ? valsClipped : vals, datafn); + var gridPath = axLetter === 'x' ? + 'M0,' + counterAxis._offset + 'v' + counterAxis._length : + 'M' + counterAxis._offset + ',0h' + counterAxis._length; - if(tickpath && ax.ticks) { - ticks.enter().append('path').classed(tcls, 1).classed('ticks', 1) - .classed('crisp', 1) - .call(Color.stroke, ax.tickcolor) - .style('stroke-width', tickWidth + 'px') - .attr('d', tickpath); - ticks.attr('transform', transfn); - ticks.exit().remove(); + axes.drawGrid(gd, ax, { + vals: valsClipped, + layer: plotinfo.gridlayer.select('.' + axId), + path: gridPath, + transFn: transFn + }); + axes.drawZeroLine(gd, ax, { + counterAxis: counterAxis, + layer: plotinfo.zerolinelayer, + path: gridPath, + transFn: transFn + }); } - else ticks.remove(); } - function drawLabels(container, position) { - // tick labels - for now just the main labels. - // TODO: mirror labels, esp for subplots - tickLabels = container.selectAll('g.' + tcls).data(vals, datafn); + var tickSigns = axes.getTickSigns(ax); + var tickVals = ax.ticks === 'inside' ? valsClipped : vals; + var tickSubplots = []; - if(!isNumeric(position)) { - tickLabels.remove(); - drawAxTitle(); - return; - } - if(!ax.showticklabels) { - tickLabels.remove(); - drawAxTitle(); - calcBoundingBox(); - return; + if(ax.ticks) { + var mainTickPath = axes.makeTickPath(ax, ax._mainLinePosition, tickSigns[2]); + if(ax._anchorAxis && ax.mirror && ax.mirror !== true) { + mainTickPath += axes.makeTickPath(ax, ax._mainMirrorPosition, tickSigns[3]); } - var labelx, labely, labelanchor, labelpos0, flipit; - if(axLetter === 'x') { - flipit = (axside === 'bottom') ? 1 : -1; - labelx = function(d) { return d.dx + labelShift * flipit; }; - labelpos0 = position + (labelStandoff + pad) * flipit; - labely = function(d) { - return d.dy + labelpos0 + d.fontSize * - ((axside === 'bottom') ? 1 : -0.2); - }; - labelanchor = function(angle) { - if(!isNumeric(angle) || angle === 0 || angle === 180) { - return 'middle'; - } - return (angle * flipit < 0) ? 'end' : 'start'; - }; - } - else if(axLetter === 'y') { - flipit = (axside === 'right') ? 1 : -1; - labely = function(d) { - return d.dy + d.fontSize * MID_SHIFT - labelShift * flipit; - }; - labelx = function(d) { - return d.dx + position + (labelStandoff + pad + - ((Math.abs(ax.tickangle) === 90) ? d.fontSize / 2 : 0)) * flipit; - }; - labelanchor = function(angle) { - if(isNumeric(angle) && Math.abs(angle) === 90) { - return 'middle'; - } - return axside === 'right' ? 'start' : 'end'; - }; - } - else if(isAngular(ax)) { - ax._labelShift = labelShift; - ax._labelStandoff = labelStandoff; - ax._pad = pad; - - labelx = ax._labelx; - labely = ax._labely; - labelanchor = ax._labelanchor; - } - - var maxFontSize = 0, - autoangle = 0, - labelsReady = []; - tickLabels.enter().append('g').classed(tcls, 1) - .append('text') - // only so tex has predictable alignment that we can - // alter later - .attr('text-anchor', 'middle') - .each(function(d) { - var thisLabel = d3.select(this), - newPromise = gd._promises.length; - thisLabel - .call(svgTextUtils.positionText, labelx(d), labely(d)) - .call(Drawing.font, d.font, d.fontSize, d.fontColor) - .text(d.text) - .call(svgTextUtils.convertToTspans, gd); - newPromise = gd._promises[newPromise]; - if(newPromise) { - // if we have an async label, we'll deal with that - // all here so take it out of gd._promises and - // instead position the label and promise this in - // labelsReady - labelsReady.push(gd._promises.pop().then(function() { - positionLabels(thisLabel, ax.tickangle); - })); - } - else { - // sync label: just position it now. - positionLabels(thisLabel, ax.tickangle); - } - }); - tickLabels.exit().remove(); + axes.drawTicks(gd, ax, { + vals: tickVals, + layer: mainPlotinfo[axLetter + 'axislayer'], + path: mainTickPath, + transFn: transFn + }); - tickLabels.each(function(d) { - maxFontSize = Math.max(maxFontSize, d.fontSize); + tickSubplots = Object.keys(ax._linepositions || {}); + } + + for(i = 0; i < tickSubplots.length; i++) { + sp = tickSubplots[i]; + plotinfo = fullLayout._plots[sp]; + // [bottom or left, top or right], free and main are handled above + var linepositions = ax._linepositions[sp] || []; + var spTickPath = axes.makeTickPath(ax, linepositions[0], tickSigns[0]) + + axes.makeTickPath(ax, linepositions[1], tickSigns[1]); + + axes.drawTicks(gd, ax, { + vals: tickVals, + layer: plotinfo[axLetter + 'axislayer'], + path: spTickPath, + transFn: transFn }); + } - if(isAngular(ax)) { - tickLabels.each(function(d) { - d3.select(this).select('text') - .call(svgTextUtils.positionText, labelx(d), labely(d)); + var labelFns = axes.makeLabelFns(ax, ax._mainLinePosition); + // stash tickLabels selection, so that drawTitle can use it + // to scoot title w/o having to query the axis layer again + ax._tickLabels = null; + + var seq = []; + + // tick labels - for now just the main labels. + // TODO: mirror labels, esp for subplots + if(ax._mainLinePosition) { + seq.push(function() { + return axes.drawLabels(gd, ax, { + vals: vals, + layer: mainPlotinfo[axLetter + 'axislayer'], + transFn: transFn, + labelXFn: labelFns.labelXFn, + labelYFn: labelFns.labelYFn, + labelAnchorFn: labelFns.labelAnchorFn, }); - } + }); + } - // How much to shift a multi-line label to center it vertically. - function getAnchorHeight(lineCount, lineHeight, angle) { - var h = (lineCount - 1) * lineHeight; - if(axLetter === 'x') { - if(angle < -60 || 60 < angle) { - return -0.5 * h; - } else if(axside === 'top') { - return -h; - } - } else { - angle *= axside === 'left' ? 1 : -1; - if(angle < -30) { - return -h; - } else if(angle < 30) { - return -0.5 * h; - } - } - return 0; - } - - function positionLabels(s, angle) { - s.each(function(d) { - var anchor = labelanchor(angle, d); - var thisLabel = d3.select(this), - mathjaxGroup = thisLabel.select('.text-math-group'), - transform = transfn.call(thisLabel.node(), d) + - ((isNumeric(angle) && +angle !== 0) ? - (' rotate(' + angle + ',' + labelx(d) + ',' + - (labely(d) - d.fontSize / 2) + ')') : - ''); - var anchorHeight = getAnchorHeight( - svgTextUtils.lineCount(thisLabel), - LINE_SPACING * d.fontSize, - isNumeric(angle) ? +angle : 0); - if(anchorHeight) { - transform += ' translate(0, ' + anchorHeight + ')'; - } - if(mathjaxGroup.empty()) { - thisLabel.select('text').attr({ - transform: transform, - 'text-anchor': anchor - }); - } - else { - var mjShift = - Drawing.bBox(mathjaxGroup.node()).width * - {end: -0.5, start: 0.5}[anchor]; - mathjaxGroup.attr('transform', transform + - (mjShift ? 'translate(' + mjShift + ',0)' : '')); - } - }); - } + if(!opts.skipTitle && + !((ax.rangeslider || {}).visible && ax._boundingBox && ax.side === 'bottom') + ) { + seq.push(function() { + return axes.drawTitle(gd, ax); + }); + } - // make sure all labels are correctly positioned at their base angle - // the positionLabels call above is only for newly drawn labels. - // do this without waiting, using the last calculated angle to - // minimize flicker, then do it again when we know all labels are - // there, putting back the prescribed angle to check for overlaps. - positionLabels(tickLabels, ax._lastangle || ax.tickangle); - - function allLabelsReady() { - return labelsReady.length && Promise.all(labelsReady); - } - - function fixLabelOverlaps() { - positionLabels(tickLabels, ax.tickangle); - - // check for auto-angling if x labels overlap - // don't auto-angle at all for log axes with - // base and digit format - if(axLetter === 'x' && !isNumeric(ax.tickangle) && - (ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D')) { - var lbbArray = []; - tickLabels.each(function(d) { - var s = d3.select(this), - thisLabel = s.select('.text-math-group'), - x = ax.l2p(d.x); - if(thisLabel.empty()) thisLabel = s.select('text'); - - var bb = Drawing.bBox(thisLabel.node()); - - lbbArray.push({ - // ignore about y, just deal with x overlaps - top: 0, - bottom: 10, - height: 10, - left: x - bb.width / 2, - // impose a 2px gap - right: x + bb.width / 2 + 2, - width: bb.width + 2 - }); - }); - for(i = 0; i < lbbArray.length - 1; i++) { - if(Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1])) { - // any overlap at all - set 30 degrees - autoangle = 30; - break; - } - } - if(autoangle) { - var tickspacing = Math.abs( - (vals[vals.length - 1].x - vals[0].x) * ax._m - ) / (vals.length - 1); - if(tickspacing < maxFontSize * 2.5) { - autoangle = 90; - } - positionLabels(tickLabels, autoangle); - } - ax._lastangle = autoangle; - } + function extendRange(range, newRange) { + range[0] = Math.min(range[0], newRange[0]); + range[1] = Math.max(range[1], newRange[1]); + } - // update the axis title - // (so it can move out of the way if needed) - // TODO: separate out scoot so we don't need to do - // a full redraw of the title (mostly relevant for MathJax) - drawAxTitle(); - return axid + ' done'; - } + seq.push(function calcBoundingBox() { + if(ax.showticklabels) { + var gdBB = gd.getBoundingClientRect(); + var bBox = mainPlotinfo[axLetter + 'axislayer'].node().getBoundingClientRect(); - function calcBoundingBox() { - if(ax.showticklabels) { - var gdBB = gd.getBoundingClientRect(); - var bBox = container.node().getBoundingClientRect(); + /* + * the way we're going to use this, the positioning that matters + * is relative to the origin of gd. This is important particularly + * if gd is scrollable, and may have been scrolled between the time + * we calculate this and the time we use it + */ + + ax._boundingBox = { + width: bBox.width, + height: bBox.height, + left: bBox.left - gdBB.left, + right: bBox.right - gdBB.left, + top: bBox.top - gdBB.top, + bottom: bBox.bottom - gdBB.top + }; + } else { + var gs = fullLayout._size; + var pos; - /* - * the way we're going to use this, the positioning that matters - * is relative to the origin of gd. This is important particularly - * if gd is scrollable, and may have been scrolled between the time - * we calculate this and the time we use it - */ + // set dummy bbox for ticklabel-less axes + + if(axLetter === 'x') { + pos = ax.anchor === 'free' ? + gs.t + gs.h * (1 - ax.position) : + gs.t + gs.h * (1 - ax._anchorAxis.domain[{bottom: 0, top: 1}[ax.side]]); ax._boundingBox = { - width: bBox.width, - height: bBox.height, - left: bBox.left - gdBB.left, - right: bBox.right - gdBB.left, - top: bBox.top - gdBB.top, - bottom: bBox.bottom - gdBB.top + top: pos, + bottom: pos, + left: ax._offset, + right: ax._offset + ax._length, + width: ax._length, + height: 0 }; } else { - var gs = fullLayout._size; - var pos; - - // set dummy bbox for ticklabel-less axes - - if(axLetter === 'x') { - pos = ax.anchor === 'free' ? - gs.t + gs.h * (1 - ax.position) : - gs.t + gs.h * (1 - ax._anchorAxis.domain[{bottom: 0, top: 1}[ax.side]]); - - ax._boundingBox = { - top: pos, - bottom: pos, - left: ax._offset, - right: ax._offset + ax._length, - width: ax._length, - height: 0 - }; - } else { - pos = ax.anchor === 'free' ? - gs.l + gs.w * ax.position : - gs.l + gs.w * ax._anchorAxis.domain[{left: 0, right: 1}[ax.side]]; - - ax._boundingBox = { - left: pos, - right: pos, - bottom: ax._offset + ax._length, - top: ax._offset, - height: ax._length, - width: 0 - }; - } - } + pos = ax.anchor === 'free' ? + gs.l + gs.w * ax.position : + gs.l + gs.w * ax._anchorAxis.domain[{left: 0, right: 1}[ax.side]]; - /* - * for spikelines: what's the full domain of positions in the - * opposite direction that are associated with this axis? - * This means any axes that we make a subplot with, plus the - * position of the axis itself if it's free. - */ - if(subplots) { - var fullRange = ax._counterSpan = [Infinity, -Infinity]; + ax._boundingBox = { + left: pos, + right: pos, + bottom: ax._offset + ax._length, + top: ax._offset, + height: ax._length, + width: 0 + }; + } + } - for(i = 0; i < subplots.length; i++) { - var subplot = fullLayout._plots[subplots[i]]; - var counterAxis = subplot[(axLetter === 'x') ? 'yaxis' : 'xaxis']; + /* + * for spikelines: what's the full domain of positions in the + * opposite direction that are associated with this axis? + * This means any axes that we make a subplot with, plus the + * position of the axis itself if it's free. + */ + if(subplotsWithAx) { + var fullRange = ax._counterSpan = [Infinity, -Infinity]; - extendRange(fullRange, [ - counterAxis._offset, - counterAxis._offset + counterAxis._length - ]); - } + for(var i = 0; i < subplotsWithAx.length; i++) { + var plotinfo = fullLayout._plots[subplotsWithAx[i]]; + var counterAxis = plotinfo[(axLetter === 'x') ? 'yaxis' : 'xaxis']; - if(ax.anchor === 'free') { - extendRange(fullRange, (axLetter === 'x') ? - [ax._boundingBox.bottom, ax._boundingBox.top] : - [ax._boundingBox.right, ax._boundingBox.left]); - } + extendRange(fullRange, [ + counterAxis._offset, + counterAxis._offset + counterAxis._length + ]); } - function extendRange(range, newRange) { - range[0] = Math.min(range[0], newRange[0]); - range[1] = Math.max(range[1], newRange[1]); + if(ax.anchor === 'free') { + extendRange(fullRange, (axLetter === 'x') ? + [ax._boundingBox.bottom, ax._boundingBox.top] : + [ax._boundingBox.right, ax._boundingBox.left]); } } + }); - function doAutoMargins() { - var pushKey = ax._name + '.automargin'; - if(axLetter !== 'x' && axLetter !== 'y') { return; } - if(!ax.automargin) { - Plots.autoMargin(gd, pushKey); - return; - } + seq.push(function doAutoMargins() { + var pushKey = ax._name + '.automargin'; - var s = ax.side[0]; - var push = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0}; + if(!ax.automargin) { + Plots.autoMargin(gd, pushKey); + return; + } - if(axLetter === 'x') { - push.y = (ax.anchor === 'free' ? ax.position : - ax._anchorAxis.domain[s === 't' ? 1 : 0]); - push[s] += ax._boundingBox.height; - } - else { - push.x = (ax.anchor === 'free' ? ax.position : - ax._anchorAxis.domain[s === 'r' ? 1 : 0]); - push[s] += ax._boundingBox.width; - } + var s = ax.side.charAt(0); + var push = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0}; - if(ax.title !== fullLayout._dfltTitle[axLetter]) { - push[s] += ax.titlefont.size; - } + if(axLetter === 'x') { + push.y = (ax.anchor === 'free' ? ax.position : + ax._anchorAxis.domain[s === 't' ? 1 : 0]); + push[s] += ax._boundingBox.height; + } else { + push.x = (ax.anchor === 'free' ? ax.position : + ax._anchorAxis.domain[s === 'r' ? 1 : 0]); + push[s] += ax._boundingBox.width; + } - Plots.autoMargin(gd, pushKey, push); + if(ax.title !== fullLayout._dfltTitle[axLetter]) { + push[s] += ax.titlefont.size; } - var done = Lib.syncOrAsync([ - allLabelsReady, - fixLabelOverlaps, - calcBoundingBox, - doAutoMargins - ]); - if(done && done.then) gd._promises.push(done); - return done; - } + Plots.autoMargin(gd, pushKey, push); + }); - function drawAxTitle() { - if(skipTitle) return; + return Lib.syncOrAsync(seq); +}; - // now this only applies to regular cartesian axes; colorbars and - // others ALWAYS call doTicks with skipTitle=true so they can - // configure their own titles. +/** + * Which direction do the 'ax.side' values, and free ticks go? + * + * @param {object} ax (full) axis object + * - {string} _id (starting with 'x' or 'y') + * - {string} side + * - {string} ticks + * @return {array} all entries are either -1 or 1 + * - [0]: sign for top/right ticks (i.e. negative SVG direction) + * - [1]: sign for bottom/left ticks (i.e. positive SVG direction) + * - [2]: sign for ticks corresponding to 'ax.side' + * - [3]: sign for ticks mirroring 'ax.side' + */ +axes.getTickSigns = function(ax) { + var axLetter = ax._id.charAt(0); + var sideOpposite = {x: 'top', y: 'right'}[axLetter]; + var main = ax.side === sideOpposite ? 1 : -1; + var out = [-1, 1, main, -main]; + // then we flip if outside XOR y axis + if((ax.ticks !== 'inside') === (axLetter === 'x')) { + out = out.map(function(v) { return -v; }); + } + return out; +}; - // rangeslider takes over a bottom title so drop it here - if(ax.rangeslider && ax.rangeslider.visible && ax._boundingBox && ax.side === 'bottom') return; +/** + * Make axis translate transform function + * + * @param {object} ax (full) axis object + * - {string} _id + * - {number} _offset + * - {fn} l2p + * @return {fn} function of calcTicks items + */ +axes.makeTransFn = function(ax) { + var axLetter = ax._id.charAt(0); + var offset = ax._offset; + return axLetter === 'x' ? + function(d) { return 'translate(' + (offset + ax.l2p(d.x)) + ',0)'; } : + function(d) { return 'translate(0,' + (offset + ax.l2p(d.x)) + ')'; }; +}; - var avoid = { - selection: tickLabels, - side: ax.side - }; - var axLetter = axid.charAt(0); - var gs = gd._fullLayout._size; - var offsetBase = 1.5; - var fontSize = ax.titlefont.size; +/** + * Make axis tick path string + * + * @param {object} ax (full) axis object + * - {string} _id + * - {number} ticklen + * - {number} linewidth + * @param {number} shift along direction of ticklen + * @param {1 or -1} sng tick sign + * @return {string} + */ +axes.makeTickPath = function(ax, shift, sgn) { + var axLetter = ax._id.charAt(0); + var pad = (ax.linewidth || 1) / 2; + var len = ax.ticklen; + return axLetter === 'x' ? + 'M0,' + (shift + pad * sgn) + 'v' + (len * sgn) : + 'M' + (shift + pad * sgn) + ',0h' + (len * sgn); +}; - var transform, counterAxis, x, y; +/** + * Make axis tick label x, y and anchor functions + * + * @param {object} ax (full) axis object + * - {string} _id + * - {string} ticks + * - {number} ticklen + * - {string} side + * - {number} linewidth + * - {number} tickfont.size + * - {boolean} showline + * @param {number} shift + * @param {number} angle [in degrees] ... + * @return {object} + * - {fn} labelXFn + * - {fn} labelYFn + * - {fn} labelAnchorFn + * - {number} labelStandoff + * - {number} labelShift + */ +axes.makeLabelFns = function(ax, shift, angle) { + var axLetter = ax._id.charAt(0); + var pad = (ax.linewidth || 1) / 2; - if(tickLabels.size()) { - var translation = Drawing.getTranslate(tickLabels.node().parentNode); - avoid.offsetLeft = translation.x; - avoid.offsetTop = translation.y; - } + var labelStandoff = ax.ticks === 'outside' ? ax.ticklen : 0; + var labelShift = 0; - var titleStandoff = 10 + fontSize * offsetBase + - (ax.linewidth ? ax.linewidth - 1 : 0); + if(angle && ax.ticks === 'outside') { + var rad = Lib.deg2rad(angle); + labelStandoff = ax.ticklen * Math.cos(rad) + 1; + labelShift = ax.ticklen * Math.sin(rad); + } - if(axLetter === 'x') { - counterAxis = (ax.anchor === 'free') ? - {_offset: gs.t + (1 - (ax.position || 0)) * gs.h, _length: 0} : - axisIds.getFromId(gd, ax.anchor); + if(ax.showticklabels && (ax.ticks === 'outside' || ax.showline)) { + labelStandoff += 0.2 * ax.tickfont.size; + } - x = ax._offset + ax._length / 2; + // Used in polar angular label x/y functions + // TODO generalize makeLabelFns so that it just work for angular axes + var out = { + labelStandoff: labelStandoff, + labelShift: labelShift + }; - if(ax.side === 'top') { - y = -titleStandoff - fontSize * (ax.showticklabels ? 1 : 0); + var x0, y0, ff, flipIt; + if(axLetter === 'x') { + flipIt = ax.side === 'bottom' ? 1 : -1; + x0 = labelShift * flipIt; + y0 = shift + (labelStandoff + pad) * flipIt; + ff = ax.side === 'bottom' ? 1 : -0.2; + + out.labelXFn = function(d) { return d.dx + x0; }; + out.labelYFn = function(d) { return d.dy + y0 + d.fontSize * ff; }; + out.labelAnchorFn = function(a) { + if(!isNumeric(a) || a === 0 || a === 180) { + return 'middle'; } - else { - y = counterAxis._length + titleStandoff + - fontSize * (ax.showticklabels ? 1.5 : 0.5); + return (a * flipIt < 0) ? 'end' : 'start'; + }; + } else if(axLetter === 'y') { + flipIt = ax.side === 'right' ? 1 : -1; + x0 = labelStandoff + pad; + y0 = -labelShift * flipIt; + ff = Math.abs(ax.tickangle) === 90 ? 0.5 : 0; + + out.labelXFn = function(d) { return d.dx + shift + (x0 + d.fontSize * ff) * flipIt; }; + out.labelYFn = function(d) { return d.dy + y0 + d.fontSize * MID_SHIFT; }; + out.labelAnchorFn = function(a) { + if(isNumeric(a) && Math.abs(a) === 90) { + return 'middle'; } - y += counterAxis._offset; + return ax.side === 'right' ? 'start' : 'end'; + }; + } - if(!avoid.side) avoid.side = 'bottom'; - } - else { - counterAxis = (ax.anchor === 'free') ? - {_offset: gs.l + (ax.position || 0) * gs.w, _length: 0} : - axisIds.getFromId(gd, ax.anchor); - - y = ax._offset + ax._length / 2; - if(ax.side === 'right') { - x = counterAxis._length + titleStandoff + - fontSize * (ax.showticklabels ? 1 : 0.5); - } - else { - x = -titleStandoff - fontSize * (ax.showticklabels ? 0.5 : 0); + return out; +}; + +function makeDataFn(ax) { + return function(d) { + return [d.text, d.x, ax.mirror, d.font, d.fontSize, d.fontColor].join('_'); + }; +} + +/** + * Draw axis ticks + * + * @param {DOM element} gd + * @param {object} ax (full) axis object + * - {string} _id + * - {string} ticks + * - {number} linewidth + * - {string} tickcolor + * @param {object} opts + * - {array of object} vals (calcTicks output-like) + * - {d3 selection} layer + * - {string or fn} path + * - {fn} transFn + * - {boolean} crisp (set to false to unset crisp-edge SVG rendering) + */ +axes.drawTicks = function(gd, ax, opts) { + opts = opts || {}; + + var cls = ax._id + 'tick'; + + var ticks = opts.layer.selectAll('path.' + cls) + .data(ax.ticks ? opts.vals : [], makeDataFn(ax)); + + ticks.exit().remove(); + + ticks.enter().append('path') + .classed(cls, 1) + .classed('ticks', 1) + .classed('crisp', opts.crisp !== false) + .call(Color.stroke, ax.tickcolor) + .style('stroke-width', Drawing.crispRound(gd, ax.tickwidth, 1) + 'px') + .attr('d', opts.path); + + ticks.attr('transform', opts.transFn); +}; + + +/** + * Draw axis grid + * + * @param {DOM element} gd + * @param {object} ax (full) axis object + * - {string} _id + * - {boolean} showgrid + * - {string} gridcolor + * - {string} gridwidth + * - {boolean} zeroline + * - {string} type + * - {string} dtick + * @param {object} opts + * - {array of object} vals (calcTicks output-like) + * - {d3 selection} layer + * - {string or fn} path + * - {fn} transFn + * - {boolean} crisp (set to false to unset crisp-edge SVG rendering) + */ +axes.drawGrid = function(gd, ax, opts) { + opts = opts || {}; + + var cls = ax._id + 'grid'; + + var grid = opts.layer.selectAll('path.' + cls) + .data((ax.showgrid === false) ? [] : opts.vals, makeDataFn(ax)); + + grid.exit().remove(); + + grid.enter().append('path') + .classed(cls, 1) + .classed('crisp', opts.crisp !== false) + .attr('d', opts.path) + .each(function(d) { + if(ax.zeroline && (ax.type === 'linear' || ax.type === '-') && + Math.abs(d.x) < ax.dtick / 100) { + d3.select(this).remove(); } - x += counterAxis._offset; + }); - transform = {rotate: '-90', offset: 0}; - if(!avoid.side) avoid.side = 'left'; - } + ax._gridWidthCrispRound = Drawing.crispRound(gd, ax.gridwidth, 1); - Titles.draw(gd, axid + 'title', { - propContainer: ax, - propName: ax._name + '.title', - placeholder: fullLayout._dfltTitle[axLetter], - avoid: avoid, - transform: transform, - attributes: {x: x, y: y, 'text-anchor': 'middle'} + grid.attr('transform', opts.transFn) + .call(Color.stroke, ax.gridcolor || '#ddd') + .style('stroke-width', ax._gridWidthCrispRound + 'px'); + + if(typeof opts.path === 'function') grid.attr('d', opts.path); +}; + +/** + * Draw axis zero-line + * + * @param {DOM element} gd + * @param {object} ax (full) axis object + * - {string} _id + * - {boolean} zeroline + * - {number} zerolinewidth + * - {string} zerolinecolor + * - {number (optional)} _gridWidthCrispRound + * @param {object} opts + * - {array of object} vals (calcTicks output-like) + * - {d3 selection} layer + * - {object} counterAxis (full axis object corresponding to counter axis) + * - {string or fn} path + * - {fn} transFn + * - {boolean} crisp (set to false to unset crisp-edge SVG rendering) + */ +axes.drawZeroLine = function(gd, ax, opts) { + opts = opts || opts; + + var cls = ax._id + 'zl'; + var show = axes.shouldShowZeroLine(gd, ax, opts.counterAxis); + + var zl = opts.layer.selectAll('path.' + cls) + .data(show ? [{x: 0, id: ax._id}] : []); + + zl.exit().remove(); + + zl.enter().append('path') + .classed(cls, 1) + .classed('zl', 1) + .classed('crisp', opts.crisp !== false) + .attr('d', opts.path) + .each(function() { + // use the fact that only one element can enter to trigger a sort. + // If several zerolines enter at the same time we will sort once per, + // but generally this should be a minimal overhead. + opts.layer.selectAll('path').sort(function(da, db) { + return axisIds.idSort(da.id, db.id); + }); }); - } - function drawGrid(plotinfo, counteraxis) { - if(fullLayout._hasOnlyLargeSploms) return; - - var gridcontainer = plotinfo.gridlayer.selectAll('.' + axid); - var zlcontainer = plotinfo.zerolinelayer; - var gridpath = ax._gridpath || ((axLetter === 'x' ? - ('M0,' + counteraxis._offset + 'v') : - ('M' + counteraxis._offset + ',0h') - ) + counteraxis._length); - var grid = gridcontainer.selectAll('path.' + gcls) - .data((ax.showgrid === false) ? [] : valsClipped, datafn); - grid.enter().append('path').classed(gcls, 1) - .classed('crisp', 1) - .attr('d', gridpath) + var strokeWidth = Drawing.crispRound(gd, + ax.zerolinewidth, + ax._gridWidthCrispRound || 1 + ); + + zl.attr('transform', opts.transFn) + .call(Color.stroke, ax.zerolinecolor || Color.defaultLine) + .style('stroke-width', strokeWidth + 'px'); +}; + +/** + * Draw axis tick labels + * + * @param {DOM element} gd + * @param {object} ax (full) axis object + * - {string} _id + * - {boolean} showticklabels + * - {number} tickangle + * @param {object} opts + * - {array of object} vals (calcTicks output-like) + * - {d3 selection} layer + * - {fn} transFn + * - {fn} labelXFn + * - {fn} labelYFn + * - {fn} labelAnchorFn + */ +axes.drawLabels = function(gd, ax, opts) { + opts = opts || {}; + + var axId = ax._id; + var axLetter = axId.charAt(0); + var cls = axId + 'tick'; + var vals = opts.vals; + var labelXFn = opts.labelXFn; + var labelYFn = opts.labelYFn; + var labelAnchorFn = opts.labelAnchorFn; + + var tickLabels = opts.layer.selectAll('g.' + cls) + .data(ax.showticklabels ? vals : [], makeDataFn(ax)); + + var maxFontSize = 0; + var autoangle = 0; + var labelsReady = []; + + tickLabels.enter().append('g') + .classed(cls, 1) + .append('text') + // only so tex has predictable alignment that we can + // alter later + .attr('text-anchor', 'middle') .each(function(d) { - if(ax.zeroline && (ax.type === 'linear' || ax.type === '-') && - Math.abs(d.x) < ax.dtick / 100) { - d3.select(this).remove(); + var thisLabel = d3.select(this); + var newPromise = gd._promises.length; + + thisLabel + .call(svgTextUtils.positionText, labelXFn(d), labelYFn(d)) + .call(Drawing.font, d.font, d.fontSize, d.fontColor) + .text(d.text) + .call(svgTextUtils.convertToTspans, gd); + + if(gd._promises[newPromise]) { + // if we have an async label, we'll deal with that + // all here so take it out of gd._promises and + // instead position the label and promise this in + // labelsReady + labelsReady.push(gd._promises.pop().then(function() { + positionLabels(thisLabel, ax.tickangle); + })); + } else { + // sync label: just position it now. + positionLabels(thisLabel, ax.tickangle); } }); - grid.attr('transform', transfn) - .call(Color.stroke, ax.gridcolor || '#ddd') - .style('stroke-width', gridWidth + 'px'); - if(typeof gridpath === 'function') grid.attr('d', gridpath); - grid.exit().remove(); - - // zero line - if(zlcontainer) { - var zlData = {x: 0, id: axid}; - var showZl = axes.shouldShowZeroLine(gd, ax, counteraxis); - var zl = zlcontainer.selectAll('path.' + zcls) - .data(showZl ? [zlData] : []); - zl.enter().append('path').classed(zcls, 1).classed('zl', 1) - .classed('crisp', 1) - .attr('d', gridpath) - .each(function() { - // use the fact that only one element can enter to trigger a sort. - // If several zerolines enter at the same time we will sort once per, - // but generally this should be a minimal overhead. - zlcontainer.selectAll('path').sort(function(da, db) { - return axisIds.idSort(da.id, db.id); - }); - }); - zl.attr('transform', transfn) - .call(Color.stroke, ax.zerolinecolor || Color.defaultLine) - .style('stroke-width', zeroLineWidth + 'px'); - zl.exit().remove(); - } + + tickLabels.exit().remove(); + + tickLabels.each(function(d) { + maxFontSize = Math.max(maxFontSize, d.fontSize); + }); + + ax._tickLabels = tickLabels; + + // TODO ?? + if(isAngular(ax)) { + tickLabels.each(function(d) { + d3.select(this).select('text') + .call(svgTextUtils.positionText, labelXFn(d), labelYFn(d)); + }); } - if(independent) { - drawTicks(ax._axislayer, tickpathfn(ax._pos + pad * ticksign[2], ticksign[2] * ax.ticklen)); - if(ax._counteraxis) { - var fictionalPlotinfo = { - gridlayer: ax._gridlayer, - zerolinelayer: ax._zerolinelayer - }; - drawGrid(fictionalPlotinfo, ax._counteraxis); + // How much to shift a multi-line label to center it vertically. + function getAnchorHeight(lineCount, lineHeight, angle) { + var h = (lineCount - 1) * lineHeight; + if(axLetter === 'x') { + if(angle < -60 || 60 < angle) { + return -0.5 * h; + } else if(ax.side === 'top') { + return -h; + } + } else { + angle *= ax.side === 'left' ? 1 : -1; + if(angle < -30) { + return -h; + } else if(angle < 30) { + return -0.5 * h; + } } - return drawLabels(ax._axislayer, ax._pos); + return 0; } - else if(fullLayout._has('cartesian')) { - subplots = axes.getSubplots(gd, ax); - // keep track of which subplots (by main conteraxis) we've already - // drawn grids for, so we don't overdraw overlaying subplots - var finishedGrids = {}; + function positionLabels(s, angle) { + s.each(function(d) { + var thisLabel = d3.select(this); + var mathjaxGroup = thisLabel.select('.text-math-group'); + var anchor = labelAnchorFn(angle, d); - subplots.map(function(subplot) { - var plotinfo = fullLayout._plots[subplot]; - var counterAxis = plotinfo[counterLetter + 'axis']; + var transform = opts.transFn.call(thisLabel.node(), d) + + ((isNumeric(angle) && +angle !== 0) ? + (' rotate(' + angle + ',' + labelXFn(d) + ',' + + (labelYFn(d) - d.fontSize / 2) + ')') : + ''); - var mainCounterID = counterAxis._mainAxis._id; - if(finishedGrids[mainCounterID]) return; - finishedGrids[mainCounterID] = 1; + var anchorHeight = getAnchorHeight( + svgTextUtils.lineCount(thisLabel), + LINE_SPACING * d.fontSize, + isNumeric(angle) ? +angle : 0 + ); + + if(anchorHeight) { + transform += ' translate(0, ' + anchorHeight + ')'; + } - drawGrid(plotinfo, counterAxis, subplot); + if(mathjaxGroup.empty()) { + thisLabel.select('text').attr({ + transform: transform, + 'text-anchor': anchor + }); + } else { + var mjWidth = Drawing.bBox(mathjaxGroup.node()).width; + var mjShift = mjWidth * {end: -0.5, start: 0.5}[anchor]; + mathjaxGroup.attr('transform', transform + (mjShift ? 'translate(' + mjShift + ',0)' : '')); + } }); + } + + // make sure all labels are correctly positioned at their base angle + // the positionLabels call above is only for newly drawn labels. + // do this without waiting, using the last calculated angle to + // minimize flicker, then do it again when we know all labels are + // there, putting back the prescribed angle to check for overlaps. + positionLabels(tickLabels, ax._lastangle || ax.tickangle); - var mainSubplot = ax._mainSubplot; - var mainPlotinfo = fullLayout._plots[mainSubplot]; - var tickSubplots = []; + function allLabelsReady() { + return labelsReady.length && Promise.all(labelsReady); + } + + function fixLabelOverlaps() { + positionLabels(tickLabels, ax.tickangle); - if(ax.ticks) { - var mainSign = ticksign[2]; - var tickpath = tickpathfn(ax._mainLinePosition + pad * mainSign, mainSign * ax.ticklen); - if(ax._anchorAxis && ax.mirror && ax.mirror !== true) { - tickpath += tickpathfn(ax._mainMirrorPosition - pad * mainSign, -mainSign * ax.ticklen); + // check for auto-angling if x labels overlap + // don't auto-angle at all for log axes with + // base and digit format + if(axLetter === 'x' && !isNumeric(ax.tickangle) && + (ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D') + ) { + var lbbArray = []; + + tickLabels.each(function(d) { + var s = d3.select(this); + var thisLabel = s.select('.text-math-group'); + if(thisLabel.empty()) thisLabel = s.select('text'); + + var x = ax.l2p(d.x); + var bb = Drawing.bBox(thisLabel.node()); + + lbbArray.push({ + // ignore about y, just deal with x overlaps + top: 0, + bottom: 10, + height: 10, + left: x - bb.width / 2, + // impose a 2px gap + right: x + bb.width / 2 + 2, + width: bb.width + 2 + }); + }); + + for(var i = 0; i < lbbArray.length - 1; i++) { + if(Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1])) { + // any overlap at all - set 30 degrees + autoangle = 30; + break; + } } - drawTicks(mainPlotinfo[axLetter + 'axislayer'], tickpath); - tickSubplots = Object.keys(ax._linepositions || {}); + if(autoangle) { + var tickspacing = Math.abs( + (vals[vals.length - 1].x - vals[0].x) * ax._m + ) / (vals.length - 1); + if(tickspacing < maxFontSize * 2.5) { + autoangle = 90; + } + positionLabels(tickLabels, autoangle); + } + ax._lastangle = autoangle; } + } - tickSubplots.map(function(subplot) { - var plotinfo = fullLayout._plots[subplot]; + var done = Lib.syncOrAsync([allLabelsReady, fixLabelOverlaps]); + if(done && done.then) gd._promises.push(done); + return done; +}; - var container = plotinfo[axLetter + 'axislayer']; +axes.drawTitle = function(gd, ax) { + var fullLayout = gd._fullLayout; + var tickLabels = ax._tickLabels; - // [bottom or left, top or right] - // free and main are handled above - var linepositions = ax._linepositions[subplot] || []; + var avoid = { + selection: tickLabels, + side: ax.side + }; - function tickPathSide(sidei) { - var tsign = ticksign[sidei]; - return tickpathfn(linepositions[sidei] + pad * tsign, tsign * ax.ticklen); - } + var axId = ax._id; + var axLetter = axId.charAt(0); + var offsetBase = 1.5; + var gs = fullLayout._size; + var fontSize = ax.titlefont.size; - drawTicks(container, tickPathSide(0) + tickPathSide(1)); - }); + var transform, counterAxis, x, y; - var mainContainer = mainPlotinfo[axLetter + 'axislayer']; + if(tickLabels && tickLabels.node() && tickLabels.node().parentNode) { + var translation = Drawing.getTranslate(tickLabels.node().parentNode); + avoid.offsetLeft = translation.x; + avoid.offsetTop = translation.y; + } + + var titleStandoff = 10 + fontSize * offsetBase + + (ax.linewidth ? ax.linewidth - 1 : 0); + + if(axLetter === 'x') { + counterAxis = (ax.anchor === 'free') ? + {_offset: gs.t + (1 - (ax.position || 0)) * gs.h, _length: 0} : + axisIds.getFromId(gd, ax.anchor); + + x = ax._offset + ax._length / 2; - return drawLabels(mainContainer, ax._mainLinePosition); + if(ax.side === 'top') { + y = -titleStandoff - fontSize * (ax.showticklabels ? 1 : 0); + } else { + y = counterAxis._length + titleStandoff + + fontSize * (ax.showticklabels ? 1.5 : 0.5); + } + y += counterAxis._offset; + + if(!avoid.side) avoid.side = 'bottom'; } + else { + counterAxis = (ax.anchor === 'free') ? + {_offset: gs.l + (ax.position || 0) * gs.w, _length: 0} : + axisIds.getFromId(gd, ax.anchor); + + y = ax._offset + ax._length / 2; + if(ax.side === 'right') { + x = counterAxis._length + titleStandoff + + fontSize * (ax.showticklabels ? 1 : 0.5); + } else { + x = -titleStandoff - fontSize * (ax.showticklabels ? 0.5 : 0); + } + x += counterAxis._offset; + + transform = {rotate: '-90', offset: 0}; + if(!avoid.side) avoid.side = 'left'; + } + + Titles.draw(gd, axId + 'title', { + propContainer: ax, + propName: ax._name + '.title', + placeholder: fullLayout._dfltTitle[axLetter], + avoid: avoid, + transform: transform, + attributes: {x: x, y: y, 'text-anchor': 'middle'} + }); }; axes.shouldShowZeroLine = function(gd, ax, counterAxis) { @@ -2280,6 +2410,10 @@ axes.shouldShowZeroLine = function(gd, ax, counterAxis) { ); }; +axes.clipEnds = function(ax, vals) { + return vals.filter(function(d) { return clipEnds(ax, d.x); }); +}; + function clipEnds(ax, l) { var p = ax.l2p(l); return (p > 1 && p < ax._length - 1); diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 6f8d03c8cc2..abeebf0fe80 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -19,6 +19,7 @@ var svgTextUtils = require('../../lib/svg_text_utils'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); var Fx = require('../../components/fx'); +var Axes = require('./axes'); var setCursor = require('../../lib/setcursor'); var dragElement = require('../../components/dragelement'); var FROM_TL = require('../../constants/alignment').FROM_TL; @@ -27,7 +28,6 @@ var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; var Plots = require('../plots'); -var doTicksSingle = require('./axes').doTicksSingle; var getFromId = require('./axis_ids').getFromId; var prepSelect = require('./select').prepSelect; var clearSelect = require('./select').clearSelect; @@ -619,8 +619,8 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { updates = {}; for(i = 0; i < activeAxIds.length; i++) { var axId = activeAxIds[i]; - doTicksSingle(gd, axId, true); var ax = getFromId(gd, axId); + Axes.drawOne(gd, ax, {skipTitle: true}); updates[ax._name + '.range[0]'] = ax.range[0]; updates[ax._name + '.range[1]'] = ax.range[1]; } diff --git a/src/plots/cartesian/transition_axes.js b/src/plots/cartesian/transition_axes.js index 4a21331ba97..33234acf0f8 100644 --- a/src/plots/cartesian/transition_axes.js +++ b/src/plots/cartesian/transition_axes.js @@ -119,14 +119,11 @@ module.exports = function transitionAxes(gd, newLayout, transitionOpts, makeOnCo } function ticksAndAnnotations(xa, ya) { - var activeAxIds = [], - i; + var activeAxIds = [xa._id, ya._id]; + var i; - activeAxIds = [xa._id, ya._id]; - - for(i = 0; i < activeAxIds.length; i++) { - Axes.doTicksSingle(gd, activeAxIds[i], true); - } + Axes.drawOne(gd, xa, {skipTitle: true}); + Axes.drawOne(gd, ya, {skipTitle: true}); function redrawObjs(objArray, method, shortCircuit) { for(i = 0; i < objArray.length; i++) { diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js index 97142b3a31d..d36c10e7b65 100644 --- a/src/plots/polar/polar.js +++ b/src/plots/polar/polar.js @@ -16,10 +16,10 @@ var Lib = require('../../lib'); var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); var Plots = require('../plots'); +var Axes = require('../../plots/cartesian/axes'); var setConvertCartesian = require('../cartesian/set_convert'); var setConvertPolar = require('./set_convert'); var doAutoRange = require('../cartesian/autorange').doAutoRange; -var doTicksSingle = require('../cartesian/axes').doTicksSingle; var dragBox = require('../cartesian/dragbox'); var dragElement = require('../../components/dragelement'); var Fx = require('../../components/fx'); @@ -65,7 +65,7 @@ function Polar(gd, id) { .attr('class', id); // unfortunately, we have to keep track of some axis tick settings - // so that we don't have to call doTicksSingle with its special redraw flag + // as polar subplots do not implement the 'ticks' editType this.radialTickLayout = null; this.angularTickLayout = null; } @@ -141,11 +141,9 @@ proto.updateLayers = function(fullLayout, polarLayout) { break; case 'radial-grid': sel.style('fill', 'none'); - sel.append('g').classed('x', 1); break; case 'angular-grid': sel.style('fill', 'none'); - sel.append('g').classed('angularaxis', 1); break; case 'radial-line': sel.append('line').style('fill', 'none'); @@ -167,14 +165,15 @@ proto.updateLayers = function(fullLayout, polarLayout) { * * - this.radialAxis * extends polarLayout.radialaxis, adds mocked 'domain' and - * few other keys in order to reuse Cartesian doAutoRange and doTicksSingle, + * few other keys in order to reuse Cartesian doAutoRange and the Axes + * drawing routines. * used for calcdata -> geometric conversions (aka c2g) during the plot step * + setGeometry setups ax.c2g for given ax.range * + setScale setups ax._m,ax._b for given ax.range * * - this.angularAxis * extends polarLayout.angularaxis, adds mocked 'range' and 'domain' and - * a few other keys in order to reuse Cartesian doTicksSingle, + * a few other keys in order to reuse the Axes drawing routines. * used for calcdata -> geometric conversions (aka c2g) during the plot step * + setGeometry setups ax.c2g given ax.rotation, ax.direction & ax._categories, * and mocks ax.range @@ -247,8 +246,6 @@ proto.updateLayout = function(fullLayout, polarLayout) { var cyy = _this.cyy = cy - yOffset2; _this.radialAxis = _this.mockAxis(fullLayout, polarLayout, radialLayout, { - _axislayer: layers['radial-axis'], - _gridlayer: layers['radial-grid'], // make this an 'x' axis to make positioning (especially rotation) easier _id: 'x', // convert to 'x' axis equivalent @@ -261,8 +258,6 @@ proto.updateLayout = function(fullLayout, polarLayout) { }); _this.angularAxis = _this.mockAxis(fullLayout, polarLayout, angularLayout, { - _axislayer: layers['angular-axis'], - _gridlayer: layers['angular-grid'], side: 'right', // to get auto nticks right domain: [0, Math.PI], @@ -301,22 +296,13 @@ proto.updateLayout = function(fullLayout, polarLayout) { .attr('d', dPath) .attr('transform', strTranslate(cx, cy)) .call(Color.fill, polarLayout.bgcolor); - - // remove crispEdges - all the off-square angles in polar plots - // make these counterproductive. - _this.framework.selectAll('.crisp').classed('crisp', 0); }; proto.mockAxis = function(fullLayout, polarLayout, axLayout, opts) { var commonOpts = { // to get _boundingBox computation right when showticklabels is false anchor: 'free', - position: 0, - _pos: 0, - // dummy truthy value to make doTicksSingle draw the grid - _counteraxis: true, - // don't use automargins routine for labels - automargin: false + position: 0 }; var ax = Lib.extendFlat(commonOpts, axLayout, opts); @@ -392,18 +378,18 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) { // rotate auto tick labels by 180 if in quadrant II and III to make them // readable from left-to-right // - // TODO try moving deeper in doTicksSingle for better results? + // TODO try moving deeper in Axes.drawLabels for better results? if(ax.tickangle === 'auto' && (a0 > 90 && a0 <= 270)) { ax.tickangle = 180; } // easier to set rotate angle with custom translate function - ax._transfn = function(d) { + var transFn = function(d) { return 'translate(' + (ax.l2p(d.x) + innerRadius) + ',0)'; }; // set special grid path function - ax._gridpath = function(d) { + var gridPathFn = function(d) { return _this.pathArc(ax.r2p(d.x) + innerRadius); }; @@ -415,7 +401,36 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) { if(hasRoomForIt) { ax.setScale(); - doTicksSingle(gd, ax, true); + + var vals = Axes.calcTicks(ax); + var valsClipped = Axes.clipEnds(ax, vals); + var labelFns = Axes.makeLabelFns(ax, 0); + var tickSign = Axes.getTickSigns(ax)[2]; + + Axes.drawTicks(gd, ax, { + vals: vals, + layer: layers['radial-axis'], + path: Axes.makeTickPath(ax, 0, tickSign), + transFn: transFn, + crisp: false + }); + + Axes.drawGrid(gd, ax, { + vals: valsClipped, + layer: layers['radial-grid'], + path: gridPathFn, + transFn: Lib.noop, + crisp: false + }); + + Axes.drawLabels(gd, ax, { + vals: vals, + layer: layers['radial-axis'], + transFn: transFn, + labelXFn: labelFns.labelXFn, + labelYFn: labelFns.labelYFn, + labelAnchorFn: labelFns.labelAnchorFn + }); } // stash 'actual' radial axis angle for drag handlers (in degrees) @@ -423,21 +438,20 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) { rad2deg(snapToVertexAngle(deg2rad(radialLayout.angle), _this.vangles)) : radialLayout.angle; - var trans = strTranslate(cx, cy) + strRotate(-angle); + var tLayer = strTranslate(cx, cy); + var tLayer2 = tLayer + strRotate(-angle); updateElement( layers['radial-axis'], hasRoomForIt && (radialLayout.showticklabels || radialLayout.ticks), - {transform: trans} + {transform: tLayer2} ); - // move all grid paths to about circle center, - // undo individual grid lines translations updateElement( layers['radial-grid'], hasRoomForIt && radialLayout.showgrid, - {transform: strTranslate(cx, cy)} - ).selectAll('path').attr('transform', null); + {transform: tLayer} + ); updateElement( layers['radial-line'].select('line'), @@ -447,7 +461,7 @@ proto.updateRadialAxis = function(fullLayout, polarLayout) { y1: 0, x2: radius, y2: 0, - transform: trans + transform: tLayer2 } ) .attr('stroke-width', radialLayout.linewidth) @@ -504,6 +518,7 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) { _this.fillViewInitialKey('angularaxis.rotation', angularLayout.rotation); ax.setGeometry(); + ax.setScale(); // 't'ick to 'g'eometric radians is used all over the place here var t2g = function(d) { return ax.t2g(d.x); }; @@ -514,34 +529,20 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) { ax.dtick = rad2deg(ax.dtick); } - // Use tickval filter for category axes instead of tweaking - // the range w.r.t sector, so that sectors that cross 360 can - // show all their ticks. - if(ax.type === 'category') { - ax._tickFilter = function(d) { - return Lib.isAngleInsideSector(t2g(d), _this.sectorInRad); - }; - } - - ax._transfn = function(d) { - var sel = d3.select(this); - var hasElement = sel && sel.node(); + var _transFn = function(rad) { + return strTranslate(cx + radius * Math.cos(rad), cy - radius * Math.sin(rad)); + }; - // don't translate grid lines - if(hasElement && sel.classed('angularaxisgrid')) return ''; + var transFn = function(d) { + return _transFn(t2g(d)); + }; + var transFn2 = function(d) { var rad = t2g(d); - var out = strTranslate(cx + radius * Math.cos(rad), cy - radius * Math.sin(rad)); - - // must also rotate ticks, but don't rotate labels - if(hasElement && sel.classed('ticks')) { - out += strRotate(-rad2deg(rad)); - } - - return out; + return _transFn(rad) + strRotate(-rad2deg(rad)); }; - ax._gridpath = function(d) { + var gridPathFn = function(d) { var rad = t2g(d); var cosRad = Math.cos(rad); var sinRad = Math.sin(rad); @@ -549,12 +550,14 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) { 'L' + [cx + radius * cosRad, cy - radius * sinRad]; }; + var out = Axes.makeLabelFns(ax, 0); + var labelStandoff = out.labelStandoff; + var labelShift = out.labelShift; var offset4fontsize = (angularLayout.ticks !== 'outside' ? 0.7 : 0.5); + var pad = (ax.linewidth || 1) / 2; - ax._labelx = function(d) { + var labelXFn = function(d) { var rad = t2g(d); - var labelStandoff = ax._labelStandoff; - var pad = ax._pad; var offset4tx = signSin(rad) === 0 ? 0 : @@ -564,11 +567,8 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) { return offset4tx + offset4tick; }; - ax._labely = function(d) { + var labelYFn = function(d) { var rad = t2g(d); - var labelStandoff = ax._labelStandoff; - var labelShift = ax._labelShift; - var pad = ax._pad; var offset4tx = d.dy + d.fontSize * MID_SHIFT - labelShift; var offset4tick = -Math.sin(rad) * (labelStandoff + pad + offset4fontsize * d.fontSize); @@ -576,7 +576,8 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) { return offset4tx + offset4tick; }; - ax._labelanchor = function(angle, d) { + // TODO maybe switch angle, d ordering ?? + var labelAnchorFn = function(angle, d) { var rad = t2g(d); return signSin(rad) === 0 ? (signCos(rad) > 0 ? 'start' : 'end') : @@ -589,14 +590,13 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) { _this.angularTickLayout = newTickLayout; } - ax.setScale(); - doTicksSingle(gd, ax, true); + var vals = Axes.calcTicks(ax); // angle of polygon vertices in geometric radians (null means circles) // TODO what to do when ax.period > ax._categories ?? var vangles; if(polarLayout.gridshape === 'linear') { - vangles = ax._vals.map(t2g); + vangles = vals.map(t2g); // ax._vals should be always ordered, make them // always turn counterclockwise for convenience here @@ -608,6 +608,44 @@ proto.updateAngularAxis = function(fullLayout, polarLayout) { } _this.vangles = vangles; + // Use tickval filter for category axes instead of tweaking + // the range w.r.t sector, so that sectors that cross 360 can + // show all their ticks. + if(ax.type === 'category') { + vals = vals.filter(function(d) { + return Lib.isAngleInsideSector(t2g(d), _this.sectorInRad); + }); + } + + if(ax.visible) { + var tickSign = ax.ticks === 'inside' ? -1 : 1; + + Axes.drawTicks(gd, ax, { + vals: vals, + layer: layers['angular-axis'], + path: 'M' + (tickSign * pad) + ',0h' + (tickSign * ax.ticklen), + transFn: transFn2, + crisp: false + }); + + Axes.drawGrid(gd, ax, { + vals: vals, + layer: layers['angular-grid'], + path: gridPathFn, + transFn: Lib.noop, + crisp: false + }); + + Axes.drawLabels(gd, ax, { + vals: vals, + layer: layers['angular-axis'], + transFn: transFn, + labelXFn: labelXFn, + labelYFn: labelYFn, + labelAnchorFn: labelAnchorFn + }); + } + // TODO maybe two arcs is better here? // maybe split style attributes between inner and outer angular axes? @@ -1044,29 +1082,25 @@ proto.updateRadialDrag = function(fullLayout, polarLayout, rngIndex) { return; } + var fullLayoutNow = gd._fullLayout; + var polarLayoutNow = fullLayoutNow[_this.id]; + // update radial range -> update c2g -> update _m,_b radialAxis.range[rngIndex] = rprime; radialAxis._rl[rngIndex] = rprime; - radialAxis.setGeometry(); - radialAxis.setScale(); + _this.updateRadialAxis(fullLayoutNow, polarLayoutNow); _this.xaxis.setRange(); _this.xaxis.setScale(); _this.yaxis.setRange(); _this.yaxis.setScale(); - doTicksSingle(gd, radialAxis, true); - layers['radial-grid'] - .attr('transform', strTranslate(cx, cy)) - .selectAll('path').attr('transform', null); - var hasRegl = false; for(var traceType in _this.traceHash) { var moduleCalcData = _this.traceHash[traceType]; var moduleCalcDataVisible = Lib.filterVisible(moduleCalcData); var _module = moduleCalcData[0][0].trace._module; - var polarLayoutNow = gd._fullLayout[_this.id]; _module.plot(gd, _this, moduleCalcDataVisible, polarLayoutNow); if(Registry.traceIs(traceType, 'gl') && moduleCalcDataVisible.length) hasRegl = true; } @@ -1140,6 +1174,7 @@ proto.updateAngularDrag = function(fullLayout) { function moveFn(dx, dy) { var fullLayoutNow = _this.gd._fullLayout; var polarLayoutNow = fullLayoutNow[_this.id]; + var x1 = x0 + dx; var y1 = y0 + dy; var a1 = xy2a(x1, y1); @@ -1158,7 +1193,6 @@ proto.updateAngularDrag = function(fullLayout) { layers.bg.attr('transform', trans); layers['radial-grid'].attr('transform', trans); - layers['angular-line'].select('path').attr('transform', trans); layers['radial-axis'].attr('transform', trans2); layers['radial-line'].select('line').attr('transform', trans2); _this.updateRadialAxisTitle(fullLayoutNow, polarLayoutNow, rrot1); @@ -1184,10 +1218,7 @@ proto.updateAngularDrag = function(fullLayout) { // update rotation -> range -> _m,_b angularAxis.rotation = Lib.modHalf(rot1, 360); - angularAxis.setGeometry(); - angularAxis.setScale(); - - doTicksSingle(gd, angularAxis, true); + _this.updateAngularAxis(fullLayoutNow, polarLayoutNow); if(_this._hasClipOnAxisFalse && !Lib.isFullCircle(_this.sectorInRad)) { scatterTraces.call(Drawing.hideOutsideRangePoints, _this); diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index a4ec4a7427c..75f3a17c996 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -158,9 +158,6 @@ proto.updateLayers = function(ternaryLayout) { } else if(d === 'grids') { grids.forEach(function(d) { layers[d] = s.append('g').classed('grid ' + d, true); - - var fictID = (d === 'bgrid') ? 'x' : 'y'; - layers[d].append('g').classed(fictID, true); }); } }); @@ -250,23 +247,16 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { // fictitious angles and domain, but then rotate and translate // it into place at the end var aaxis = _this.aaxis = extendFlat({}, ternaryLayout.aaxis, { - visible: true, range: [amin, sum - bmin - cmin], side: 'left', - _counterangle: 30, // tickangle = 'auto' means 0 anyway for a y axis, need to coerce to 0 here // so we can shift by 30. tickangle: (+ternaryLayout.aaxis.tickangle || 0) - 30, domain: [yDomain0, yDomain0 + yDomainFinal * w_over_h], - _axislayer: _this.layers.aaxis, - _gridlayer: _this.layers.agrid, anchor: 'free', position: 0, - _pos: 0, // _this.xaxis.domain[0] * graphSize.w, _id: 'y', - _length: w, - _gridpath: 'M0,0l' + h + ',-' + (w / 2), - automargin: false // don't use automargins routine for labels + _length: w }); setConvert(aaxis, _this.graphDiv._fullLayout); aaxis.setScale(); @@ -274,45 +264,28 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { // baxis goes across the bottom (backward). We can set it up as an x axis // without any enclosing transformation. var baxis = _this.baxis = extendFlat({}, ternaryLayout.baxis, { - visible: true, range: [sum - amin - cmin, bmin], side: 'bottom', - _counterangle: 30, domain: _this.xaxis.domain, - _axislayer: _this.layers.baxis, - _gridlayer: _this.layers.bgrid, - _counteraxis: _this.aaxis, anchor: 'free', position: 0, - _pos: 0, // (1 - yDomain0) * graphSize.h, _id: 'x', - _length: w, - _gridpath: 'M0,0l-' + (w / 2) + ',-' + h, - automargin: false // don't use automargins routine for labels + _length: w }); setConvert(baxis, _this.graphDiv._fullLayout); baxis.setScale(); - aaxis._counteraxis = baxis; // caxis goes down the right side. Set it up as a y axis, with // post-transformation similar to aaxis var caxis = _this.caxis = extendFlat({}, ternaryLayout.caxis, { - visible: true, range: [sum - amin - bmin, cmin], side: 'right', - _counterangle: 30, tickangle: (+ternaryLayout.caxis.tickangle || 0) + 30, domain: [yDomain0, yDomain0 + yDomainFinal * w_over_h], - _axislayer: _this.layers.caxis, - _gridlayer: _this.layers.cgrid, - _counteraxis: _this.baxis, anchor: 'free', position: 0, - _pos: 0, // _this.xaxis.domain[1] * graphSize.w, _id: 'y', - _length: w, - _gridpath: 'M0,0l-' + h + ',' + (w / 2), - automargin: false // don't use automargins routine for labels + _length: w }); setConvert(caxis, _this.graphDiv._fullLayout); caxis.setScale(); @@ -350,10 +323,6 @@ proto.adjustLayout = function(ternaryLayout, graphSize) { _this.drawAxes(true); - // remove crispEdges - all the off-square angles in ternary plots - // make these counterproductive. - _this.plotContainer.selectAll('.crisp').classed('crisp', false); - _this.layers.aline.select('path') .attr('d', aaxis.showline ? 'M' + x0 + ',' + (y0 + h) + 'l' + (w / 2) + ',-' + h : 'M0,0') @@ -388,37 +357,19 @@ proto.drawAxes = function(doTitles) { var aaxis = _this.aaxis; var baxis = _this.baxis; var caxis = _this.caxis; - var newTickLayout; - - newTickLayout = strTickLayout(aaxis); - if(_this.aTickLayout !== newTickLayout) { - layers.aaxis.selectAll('.ytick').remove(); - _this.aTickLayout = newTickLayout; - } - - newTickLayout = strTickLayout(baxis); - if(_this.bTickLayout !== newTickLayout) { - layers.baxis.selectAll('.xtick').remove(); - _this.bTickLayout = newTickLayout; - } - - newTickLayout = strTickLayout(caxis); - if(_this.cTickLayout !== newTickLayout) { - layers.caxis.selectAll('.ytick').remove(); - _this.cTickLayout = newTickLayout; - } - // 3rd arg true below skips titles, so we can configure them - // correctly later on. - Axes.doTicksSingle(gd, aaxis, true); - Axes.doTicksSingle(gd, baxis, true); - Axes.doTicksSingle(gd, caxis, true); + _this.drawAx(aaxis); + _this.drawAx(baxis); + _this.drawAx(caxis); if(doTitles) { var apad = Math.max(aaxis.showticklabels ? aaxis.tickfont.size / 2 : 0, (caxis.showticklabels ? caxis.tickfont.size * 0.75 : 0) + (caxis.ticks === 'outside' ? caxis.ticklen * 0.87 : 0)); - _this.layers['a-title'] = Titles.draw(gd, 'a' + titlesuffix, { + var bpad = (baxis.showticklabels ? baxis.tickfont.size : 0) + + (baxis.ticks === 'outside' ? baxis.ticklen : 0) + 3; + + layers['a-title'] = Titles.draw(gd, 'a' + titlesuffix, { propContainer: aaxis, propName: _this.id + '.aaxis.title', placeholder: _(gd, 'Click to enter Component A title'), @@ -428,12 +379,7 @@ proto.drawAxes = function(doTitles) { 'text-anchor': 'middle' } }); - - - var bpad = (baxis.showticklabels ? baxis.tickfont.size : 0) + - (baxis.ticks === 'outside' ? baxis.ticklen : 0) + 3; - - _this.layers['b-title'] = Titles.draw(gd, 'b' + titlesuffix, { + layers['b-title'] = Titles.draw(gd, 'b' + titlesuffix, { propContainer: baxis, propName: _this.id + '.baxis.title', placeholder: _(gd, 'Click to enter Component B title'), @@ -443,8 +389,7 @@ proto.drawAxes = function(doTitles) { 'text-anchor': 'middle' } }); - - _this.layers['c-title'] = Titles.draw(gd, 'c' + titlesuffix, { + layers['c-title'] = Titles.draw(gd, 'c' + titlesuffix, { propContainer: caxis, propName: _this.id + '.caxis.title', placeholder: _(gd, 'Click to enter Component C title'), @@ -457,11 +402,77 @@ proto.drawAxes = function(doTitles) { } }; +proto.drawAx = function(ax) { + var _this = this; + var gd = _this.graphDiv; + var axName = ax._name; + var axLetter = axName.charAt(0); + var axId = ax._id; + var axLayer = _this.layers[axName]; + var counterAngle = 30; + + var stashKey = axLetter + 'tickLayout'; + var newTickLayout = strTickLayout(ax); + if(_this[stashKey] !== newTickLayout) { + axLayer.selectAll('.' + axId + 'tick').remove(); + _this[stashKey] = newTickLayout; + } + + ax.setScale(); + + var vals = Axes.calcTicks(ax); + var valsClipped = Axes.clipEnds(ax, vals); + var transFn = Axes.makeTransFn(ax); + var tickSign = Axes.getTickSigns(ax)[2]; + + var caRad = Lib.deg2rad(counterAngle); + var pad = tickSign * (ax.linewidth || 1) / 2; + var len = tickSign * ax.ticklen; + var w = _this.w; + var h = _this.h; + + var tickPath = axLetter === 'b' ? + 'M0,' + pad + 'l' + (Math.sin(caRad) * len) + ',' + (Math.cos(caRad) * len) : + 'M' + pad + ',0l' + (Math.cos(caRad) * len) + ',' + (-Math.sin(caRad) * len); + + var gridPath = { + a: 'M0,0l' + h + ',-' + (w / 2), + b: 'M0,0l-' + (w / 2) + ',-' + h, + c: 'M0,0l-' + h + ',' + (w / 2) + }[axLetter]; + + Axes.drawTicks(gd, ax, { + vals: ax.ticks === 'inside' ? valsClipped : vals, + layer: axLayer, + path: tickPath, + transFn: transFn, + crisp: false + }); + + Axes.drawGrid(gd, ax, { + vals: valsClipped, + layer: _this.layers[axLetter + 'grid'], + path: gridPath, + transFn: transFn, + crisp: false + }); + + var labelFns = Axes.makeLabelFns(ax, 0, counterAngle); + + Axes.drawLabels(gd, ax, { + vals: vals, + layer: axLayer, + transFn: transFn, + labelXFn: labelFns.labelXFn, + labelYFn: labelFns.labelYFn, + labelAnchorFn: labelFns.labelAnchorFn + }); +}; + function strTickLayout(axLayout) { return axLayout.ticks + String(axLayout.ticklen) + String(axLayout.showticklabels); } - // hard coded paths for zoom corners // uses the same sizing as cartesian, length is MINZOOM/2, width is 3px var CLEN = constants.MINZOOM / 2 + 0.87; @@ -711,7 +722,6 @@ proto.initInteractions = function() { _this.caxis.range = [_this.sum - mins.a - mins.b, mins.c]; _this.drawAxes(false); - _this.plotContainer.selectAll('.crisp').classed('crisp', false); if(_this._hasClipOnAxisFalse) { _this.plotContainer diff --git a/test/jasmine/bundle_tests/mathjax_test.js b/test/jasmine/bundle_tests/mathjax_test.js new file mode 100644 index 00000000000..26207cec527 --- /dev/null +++ b/test/jasmine/bundle_tests/mathjax_test.js @@ -0,0 +1,164 @@ +var Plotly = require('@lib/index'); +var d3 = require('d3'); + +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); + +describe('Test MathJax:', function() { + var mathJaxScriptTag; + + // N.B. we have to load MathJax "dynamically" as Karam + // does not undefined the MathJax's `?config=` parameter. + // + // Eventually, it might be nice to move these tests in the "regular" test + // suites, but to do that we'll need to find a way to remove MathJax from + // page without breaking things downstream. + beforeAll(function(done) { + mathJaxScriptTag = document.createElement('script'); + mathJaxScriptTag.type = 'text/javascript'; + mathJaxScriptTag.onload = function() { + require('@src/fonts/mathjax_config')(); + done(); + }; + mathJaxScriptTag.onerror = function() { + fail('MathJax failed to load'); + }; + mathJaxScriptTag.src = '/base/dist/extras/mathjax/MathJax.js?config=TeX-AMS-MML_SVG'; + document.body.appendChild(mathJaxScriptTag); + }); + + describe('Test axis title scoot:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function assertNoIntersect(msg) { + var gd3 = d3.select(gd); + var xTitle = gd3.select('.g-xtitle'); + var xTicks = gd3.selectAll('.xtick > text'); + + expect(xTitle.size()).toBe(1, '1 x-axis title'); + expect(xTicks.size()).toBeGreaterThan(1, 'x-axis ticks'); + + var titleTop = xTitle.node().getBoundingClientRect().top; + + xTicks.each(function(_, i) { + var tickBottom = this.getBoundingClientRect().bottom; + expect(tickBottom).toBeLessThan(titleTop, 'xtick #' + i + ' - ' + msg); + }); + } + + function testTitleScoot(fig, opts) { + var xCategories = opts.xCategories; + + return Plotly.plot(gd, fig) + .then(function() { assertNoIntersect('base'); }) + .then(function() { return Plotly.relayout(gd, 'xaxis.titlefont.size', 40); }) + .then(function() { assertNoIntersect('large title font size'); }) + .then(function() { return Plotly.relayout(gd, 'xaxis.titlefont.size', null); }) + .then(function() { assertNoIntersect('back to base'); }) + .then(function() { return Plotly.relayout(gd, 'xaxis.tickfont.size', 40); }) + .then(function() { assertNoIntersect('large title font size'); }) + .then(function() { return Plotly.relayout(gd, 'xaxis.tickfont.size', null); }) + .then(function() { assertNoIntersect('back to base 2'); }) + .then(function() { return Plotly.update(gd, {x: [xCategories]}, {'xaxis.tickangle': 90}); }) + .then(function() { assertNoIntersect('long tick labels'); }) + .then(function() { return Plotly.update(gd, {x: [null]}, {'xaxis.tickangle': null}); }) + .then(function() { assertNoIntersect('back to base 3'); }); + } + + var longCats = ['aaaaaaaaa', 'bbbbbbbbb', 'cccccccc']; + var texTitle = '$f(x) = \\int_0^\\infty \\psi(t) dt$'; + var texCats = ['$\\phi$', '$\\nabla \\cdot \\vec{F}$', '$\\frac{\\partial x}{\\partial y}$']; + var longTexCats = [ + '$\\int_0^\\infty \\psi(t) dt$', + '$\\alpha \\int_0^\\infty \\eta(t) dt$', + '$\\int_0^\\infty \\zeta(t) dt$' + ]; + + it('should scoot x-axis title below x-axis ticks', function(done) { + testTitleScoot({ + data: [{ + y: [1, 2, 1] + }], + layout: { + xaxis: {title: 'TITLE'}, + width: 500, + height: 500, + margin: {t: 100, b: 100, l: 100, r: 100} + } + }, { + xCategories: longCats + }) + .catch(failTest) + .then(done); + }); + + it('should scoot x-axis title (with MathJax) below x-axis ticks', function(done) { + expect(window.MathJax).toBeDefined(); + + testTitleScoot({ + data: [{ + y: [1, 2, 1] + }], + layout: { + xaxis: {title: texTitle}, + width: 500, + height: 500, + margin: {t: 100, b: 100, l: 100, r: 100} + } + }, { + xCategories: longCats + }) + .catch(failTest) + .then(done); + }); + + it('should scoot x-axis title below x-axis ticks (with MathJax)', function(done) { + expect(window.MathJax).toBeDefined(); + + testTitleScoot({ + data: [{ + x: texCats, + y: [1, 2, 1] + }], + layout: { + xaxis: {title: 'TITLE'}, + width: 500, + height: 500, + margin: {t: 100, b: 100, l: 100, r: 100} + } + }, { + xCategories: longTexCats + }) + .catch(failTest) + .then(done); + }); + + it('should scoot x-axis title (with MathJax) below x-axis ticks (with MathJax)', function(done) { + expect(window.MathJax).toBeDefined(); + + testTitleScoot({ + data: [{ + x: texCats, + y: [1, 2, 1] + }], + layout: { + xaxis: {title: texTitle}, + width: 500, + height: 500, + margin: {t: 100, b: 100, l: 100, r: 100} + } + }, { + xCategories: longTexCats + }) + .catch(failTest) + .then(done); + }); + }); +}); diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js index 0d8f789b90d..904a3334bbf 100644 --- a/test/jasmine/karma.conf.js +++ b/test/jasmine/karma.conf.js @@ -88,7 +88,7 @@ var isFullSuite = !isBundleTest && argv._.length === 0; var testFileGlob; if(isFullSuite) { - testFileGlob = path.join('tests', '*' + SUFFIX); + testFileGlob = path.join(__dirname, 'tests', '*' + SUFFIX); } else if(isBundleTest) { var _ = merge(argv.bundleTest); @@ -96,9 +96,9 @@ if(isFullSuite) { console.warn('Can only run one bundle test suite at a time, ignoring ', _.slice(1)); } - testFileGlob = path.join('bundle_tests', glob([basename(_[0])])); + testFileGlob = path.join(__dirname, 'bundle_tests', glob([basename(_[0])])); } else { - testFileGlob = path.join('tests', glob(merge(argv._).map(basename))); + testFileGlob = path.join(__dirname, 'tests', glob(merge(argv._).map(basename))); } var pathToShortcutPath = path.join(__dirname, '..', '..', 'tasks', 'util', 'shortcut_paths.js'); @@ -107,6 +107,7 @@ var pathToJQuery = path.join(__dirname, 'assets', 'jquery-1.8.3.min.js'); var pathToIE9mock = path.join(__dirname, 'assets', 'ie9_mock.js'); var pathToCustomMatchers = path.join(__dirname, 'assets', 'custom_matchers.js'); var pathToUnpolyfill = path.join(__dirname, 'assets', 'unpolyfill.js'); +var pathToMathJax = path.join(constants.pathToDist, 'extras', 'mathjax'); var reporters = (isFullSuite && !argv.tags) ? ['dots', 'spec'] : ['progress']; if(argv.failFast) reporters.push('fail-fast'); @@ -134,7 +135,7 @@ function func(config) { func.defaultConfig = { // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '.', + basePath: constants.pathToRoot, // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter @@ -143,7 +144,13 @@ func.defaultConfig = { // list of files / patterns to load in the browser // // N.B. the rest of this field is filled below - files: [pathToCustomMatchers, pathToUnpolyfill], + files: [ + pathToCustomMatchers, + pathToUnpolyfill, + // available to fetch from /base/path/to/mathjax + // more info: http://karma-runner.github.io/3.0/config/files.html + {pattern: pathToMathJax + '/**', included: false, watched: false, served: true} + ], // list of files / pattern to exclude exclude: [], diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js index 3ff769c6ece..4898c620c41 100644 --- a/test/jasmine/tests/plot_api_test.js +++ b/test/jasmine/tests/plot_api_test.js @@ -569,7 +569,7 @@ describe('Test plot api', function() { mockedMethods.forEach(function(m) { spyOn(subroutines, m); }); - spyOn(Axes, 'doTicks'); + spyOn(Axes, 'draw'); spyOn(Plots, 'supplyDefaults').and.callThrough(); }); @@ -577,7 +577,7 @@ describe('Test plot api', function() { mockedMethods.forEach(function(m) { subroutines[m].calls.reset(); }); - Axes.doTicks.calls.reset(); + Axes.draw.calls.reset(); supplyAllDefaults(gd); Plots.supplyDefaults.calls.reset(); @@ -686,9 +686,9 @@ describe('Test plot api', function() { '# of ' + m + ' calls - ' + msg ); }); - expect(Axes.doTicks).toHaveBeenCalledTimes(1); - expect(Axes.doTicks.calls.allArgs()[0][1]).toEqual(['x']); - expect(Axes.doTicks.calls.allArgs()[0][2]).toBe(true, 'skip-axis-title argument'); + expect(Axes.draw).toHaveBeenCalledTimes(1); + expect(Axes.draw.calls.allArgs()[0][1]).toEqual(['x']); + expect(Axes.draw.calls.allArgs()[0][2]).toEqual({skipTitle: true}, 'skip-axis-title argument'); expect(Plots.supplyDefaults).not.toHaveBeenCalled(); } @@ -2664,7 +2664,7 @@ describe('Test plot api', function() { spyOn(annotations, 'drawOne').and.callThrough(); spyOn(annotations, 'draw').and.callThrough(); spyOn(images, 'draw').and.callThrough(); - spyOn(Axes, 'doTicks').and.callThrough(); + spyOn(Axes, 'draw').and.callThrough(); }); afterEach(destroyGraphDiv); @@ -2900,11 +2900,11 @@ describe('Test plot api', function() { Plotly.newPlot(gd, data, layout) .then(countPlots) .then(function() { - expect(Axes.doTicks).toHaveBeenCalledWith(gd, ''); + expect(Axes.draw).toHaveBeenCalledWith(gd, ''); return Plotly.react(gd, data, layout2); }) .then(function() { - expect(Axes.doTicks).toHaveBeenCalledWith(gd, 'redraw'); + expect(Axes.draw).toHaveBeenCalledWith(gd, 'redraw'); expect(subroutines.layoutStyles).not.toHaveBeenCalled(); }) .catch(failTest) diff --git a/test/jasmine/tests/polar_test.js b/test/jasmine/tests/polar_test.js index 27d6d5dfbb9..be0325be62b 100644 --- a/test/jasmine/tests/polar_test.js +++ b/test/jasmine/tests/polar_test.js @@ -428,7 +428,7 @@ describe('Test relayout on polar subplots:', function() { .then(toggle( 'polar.angularaxis.showgrid', [true, false], [8, 0], - '.angular-grid > .angularaxis > path', assertCnt + '.angular-grid > path', assertCnt )) .then(toggle( 'polar.angularaxis.showticklabels', @@ -608,7 +608,7 @@ describe('Test relayout on polar subplots:', function() { } assertLetterCount('.plotbg > path'); - assertLetterCount('.radial-grid > .x > path'); + assertLetterCount('.radial-grid > path'); assertLetterCount('.angular-line > path'); } @@ -1303,7 +1303,7 @@ describe('Test polar *gridshape linear* interactions', function() { var dragCoverNode; var p1; - var layersRotateFromZero = ['.plotbg > path', '.radial-grid', '.angular-line > path']; + var layersRotateFromZero = ['.plotbg > path', '.radial-grid']; var layersRotateFromRadialAxis = ['.radial-axis', '.radial-line > line']; function _assertTransformRotate(msg, query, rot) { diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 98435848e53..f1dfb13523b 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -1079,7 +1079,7 @@ describe('Test splom update switchboard:', function() { methods = [ [Plots, 'supplyDefaults'], - [Axes, 'doTicks'], + [Axes, 'draw'], [regl, 'clear'], [splomGrid, 'update'] ]; @@ -1094,7 +1094,7 @@ describe('Test splom update switchboard:', function() { assertSpies(msg, [ ['supplyDefaults', 0], - ['doTicks', 1], + ['Axes.draw', 1], ['regl clear', 1], ['splom grid update', 1], ['splom grid draw', 1], @@ -1123,7 +1123,7 @@ describe('Test splom update switchboard:', function() { methods = [ [Plots, 'supplyDefaults'], [Plots, 'doCalcdata'], - [Axes, 'doTicks'], + [Axes, 'draw'], [regl, 'clear'], [matrix, 'update'], [matrix, 'draw'] @@ -1143,7 +1143,7 @@ describe('Test splom update switchboard:', function() { assertSpies(msg, [ ['supplyDefaults', 1], ['doCalcdata', 0], - ['doTicks', 0], + ['Axes.draw', 0], ['regl clear', 1], ['update', 1], ['draw', 1] @@ -1160,7 +1160,7 @@ describe('Test splom update switchboard:', function() { assertSpies(msg, [ ['supplyDefaults', 1], ['doCalcdata', 0], - ['doTicks', 0], + ['Axes.draw', 0], ['clear', 1], ['update', 1], ['draw', 1] @@ -1185,7 +1185,7 @@ describe('Test splom update switchboard:', function() { assertSpies(msg, [ ['supplyDefaults', 1], ['doCalcdata', 0], - ['doTicks', 0], + ['Axes.draw', 0], ['clear', 1], ['update', 1], ['draw', 1] @@ -1206,7 +1206,7 @@ describe('Test splom update switchboard:', function() { assertSpies(msg, [ ['supplyDefaults', 1], ['doCalcdata', 1], - ['doTicks', 1], + ['Axes.draw', 1], ['regl clear', 1], ['update', 1], ['draw', 1] @@ -1224,7 +1224,7 @@ describe('Test splom update switchboard:', function() { assertSpies(msg, [ ['supplyDefaults', 1], ['doCalcdata', 1], - ['doTicks', 1], + ['Axes.draw', 1], ['regl clear', 1], ['update', 1], ['draw', 1] @@ -1242,7 +1242,7 @@ describe('Test splom update switchboard:', function() { assertSpies(msg, [ ['supplyDefaults', 1], ['doCalcdata', 0], - ['doTicks', 0], + ['Axes.draw', 0], ['clear', 1], ['update', 1], ['draw', 1]