diff --git a/.circleci/config.yml b/.circleci/config.yml index 1f31df70a85..6d2821ddeb7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,8 @@ +# This config is equivalent to both the '.circleci/extended/orb-free.yml' and the base '.circleci/config.yml' version: 2.1 + +# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects. +# See: https://circleci.com/docs/2.0/orb-intro/ orbs: browser-tools: circleci/browser-tools@1.2.2 @@ -190,7 +194,7 @@ jobs: working_directory: ~/plotly.js steps: - browser-tools/install-browser-tools: &browser-versions - firefox-version: '81.0' + firefox-version: "81.0" install-chrome: false install-chromedriver: false - attach_workspace: @@ -209,7 +213,7 @@ jobs: working_directory: ~/plotly.js steps: - browser-tools/install-browser-tools: &browser-versions - firefox-version: '82.0' + firefox-version: "82.0" install-chrome: false install-chromedriver: false - attach_workspace: @@ -347,8 +351,8 @@ jobs: - run: name: Install poppler-utils to have pdftops for exporting eps command: | - sudo apt-get update --allow-releaseinfo-change - sudo apt-get install poppler-utils + sudo apt-get update --allow-releaseinfo-change + sudo apt-get install poppler-utils - run: name: Create svg, jpg, jpeg, webp, pdf and eps files command: python3 test/image/make_exports.py @@ -467,8 +471,8 @@ jobs: command: npm run no-new-func workflows: - version: 2 - build-and-test: + sample: # This is the name of the workflow, feel free to change it to better match your workflow. + # Inside the workflow, you define the jobs you want to run. jobs: - install-and-cibuild-node12 diff --git a/delete.txt b/delete.txt new file mode 100644 index 00000000000..f6c4fd068ea --- /dev/null +++ b/delete.txt @@ -0,0 +1 @@ +delete me \ No newline at end of file diff --git a/devtools/test_dashboard/index - boxplot.html b/devtools/test_dashboard/index - boxplot.html new file mode 100644 index 00000000000..dc9c10d469e --- /dev/null +++ b/devtools/test_dashboard/index - boxplot.html @@ -0,0 +1,82 @@ + + + + Plotly.js Devtools + + + + +
+ + + + + +
+ +
+
+
+
+
+
+
+ + + + + + + diff --git a/devtools/test_dashboard/index copy 2.html b/devtools/test_dashboard/index copy 2.html new file mode 100644 index 00000000000..e4596963f0a --- /dev/null +++ b/devtools/test_dashboard/index copy 2.html @@ -0,0 +1,171 @@ + + + + Plotly.js Devtools + + + + +
+ + + + + +
+ +
+
+
+
+
+
+
+ + + + + + + + + diff --git a/devtools/test_dashboard/index copy.html b/devtools/test_dashboard/index copy.html new file mode 100644 index 00000000000..bbe0c94afa3 --- /dev/null +++ b/devtools/test_dashboard/index copy.html @@ -0,0 +1,163 @@ + + + + Plotly.js Devtools + + + + +
+ + + + + +
+ +
+
+
+
+
+
+
+ + + + + + + + + diff --git a/devtools/test_dashboard/index.html b/devtools/test_dashboard/index.html index 0c0c9e89760..13f8a3c48d3 100644 --- a/devtools/test_dashboard/index.html +++ b/devtools/test_dashboard/index.html @@ -1,27 +1,526 @@ - - Plotly.js Devtools - - - - -
- - - - - -
- -
-
-
-
-
- - - - - + + Plotly.js Devtools + + + + +
+ + + + + +
+ +
+
+
+
+
+
+
+ + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index e6dd94e1092..7ee763f4bd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7886,9 +7886,9 @@ } }, "node_modules/jasmine-core": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", - "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "version": "3.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz", + "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==", "dev": true }, "node_modules/jest-worker": { @@ -14405,8 +14405,7 @@ "@mapbox/mapbox-gl-supported": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", - "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", - "requires": {} + "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==" }, "@mapbox/node-pre-gyp": { "version": "1.0.8", @@ -14992,8 +14991,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", - "dev": true, - "requires": {} + "dev": true }, "acorn-globals": { "version": "6.0.0", @@ -15017,8 +15015,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "acorn-walk": { "version": "7.1.1", @@ -19073,9 +19070,9 @@ } }, "jasmine-core": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", - "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", + "version": "3.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz", + "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==", "dev": true }, "jest-worker": { @@ -19333,8 +19330,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/karma-jasmine-spec-tags/-/karma-jasmine-spec-tags-1.3.0.tgz", "integrity": "sha512-J1iAZtcEcK/pCkaYsU0VbNJae+3Awz/3p1dhWnKgvcB4FkbBK1TIOi4qswJ6HAmmcdJZgudffXpslcNQJZH9sw==", - "dev": true, - "requires": {} + "dev": true }, "karma-spec-reporter": { "version": "0.0.36", @@ -21761,8 +21757,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-js": { "version": "1.0.2", @@ -21960,11 +21955,6 @@ "fs-extra": "^10.0.0" } }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, "string-split-by": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz", diff --git a/src/lib/sort_traces.js b/src/lib/sort_traces.js new file mode 100644 index 00000000000..9e4985e84ae --- /dev/null +++ b/src/lib/sort_traces.js @@ -0,0 +1,144 @@ +'use strict'; + +function zipArrays(arrays) { + var zipped = []; + arrays[0].forEach(function(e, i) { + var row = []; + arrays.forEach(function(arr) { + row.push(arr[i]); + }); + zipped.push(row); + }); + return zipped; +} + +function sortObjecstByKey(a, b, key) { + if(a[key] === b[key]) return 0; + if(a[key] < b[key]) return -1; + return 1; +} + +function matrixToObjectList(matrix, cols) { + var zipped = zipArrays(matrix); + + var objList = []; + + zipped.forEach(function(row) { + var objRow = {}; + cols.forEach(function(col, idx) { + objRow[col] = row[idx]; + }); + objRow.y = row[row.length - 1]; + objList.push(objRow); + }); + return objList; +} + +exports.matrixToObjectList = matrixToObjectList; + +function sortObjectList(cols, objList) { + var sortedObjectList = objList.map(function(e) { + return e; + }); + cols.slice().reverse().forEach(function(key) { + sortedObjectList = sortedObjectList.sort(function(a, b) { + return sortObjecstByKey(a, b, key); + }); + }); + return sortedObjectList; +} + +exports.sortObjectList = sortObjectList; + +function objectListToList(objectList) { + var list = []; + objectList.forEach(function(item) { + list.push(Object.values(item)); + }); + return list; +} + +exports.objectListToList = objectListToList; + +function sortedMatrix(list, removeNull) { + var xs = []; + var y = []; + + list.slice().forEach(function(item) { + var val = item.pop(); + + if(removeNull & item.includes(null)) { + return; + } + + y.push(val); + xs.push(item); + }); + + return [xs, y]; +} + +exports.sortedMatrix = sortedMatrix; + +function squareMatrix(matrix) { + var width = matrix[0].length; + var height = matrix.length; + + if(width === height) { + return matrix; + } + + var newMatrix = []; + + if(width > height) { + for(var rw = 0; rw < height; rw++) { + newMatrix.push(matrix[rw].slice()); + } + for(var i = height; i < width; i++) { + newMatrix.push(Array(width)); + } + } else { + for(var row = 0; row < height; row++) { + var rowExpansion = Array(height - width); + var rowSlice = matrix[row].slice(); + Array.prototype.push.apply(rowSlice, rowExpansion); + newMatrix.push(rowSlice); + } + } + return newMatrix; +} + +exports.squareMatrix = squareMatrix; + +function transpose(matrix) { + var height = matrix.length; + var width = matrix[0].length; + + var squaredMatrix = squareMatrix(matrix); + + var newMatrix = []; + + // prevent inplace change and mantain the main diagonal + for(var rw = 0; rw < squaredMatrix.length; rw++) { + newMatrix.push(squaredMatrix[rw].slice()); + } + + for(var i = 0; i < newMatrix.length; i++) { + for(var j = 0; j < i; j++) { + newMatrix = newMatrix.slice(); + var temp = newMatrix[i][j]; + newMatrix[i][j] = newMatrix[j][i]; + newMatrix[j][i] = temp; + } + } + if(width > height) { + for(var row = 0; row < newMatrix.length; row++) { + newMatrix[row] = newMatrix[row].slice(0, height); + } + } else { + newMatrix = newMatrix.slice(0, width); + } + return newMatrix; +} + +exports.transpose = transpose; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index dfdb0e5166d..1506cfa3cff 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1,21 +1,21 @@ -'use strict'; +"use strict"; -var d3 = require('@plotly/d3'); -var isNumeric = require('fast-isnumeric'); -var Plots = require('../../plots/plots'); +var d3 = require("@plotly/d3"); +var isNumeric = require("fast-isnumeric"); +var Plots = require("../../plots/plots"); -var Registry = require('../../registry'); -var Lib = require('../../lib'); +var Registry = require("../../registry"); +var Lib = require("../../lib"); var strTranslate = Lib.strTranslate; -var svgTextUtils = require('../../lib/svg_text_utils'); -var Titles = require('../../components/titles'); -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); +var svgTextUtils = require("../../lib/svg_text_utils"); +var Titles = require("../../components/titles"); +var Color = require("../../components/color"); +var Drawing = require("../../components/drawing"); -var axAttrs = require('./layout_attributes'); -var cleanTicks = require('./clean_ticks'); +var axAttrs = require("./layout_attributes"); +var cleanTicks = require("./clean_ticks"); -var constants = require('../../constants/numerical'); +var constants = require("../../constants/numerical"); var ONEMAXYEAR = constants.ONEMAXYEAR; var ONEAVGYEAR = constants.ONEAVGYEAR; var ONEMINYEAR = constants.ONEMINYEAR; @@ -34,21 +34,21 @@ var ONESEC = constants.ONESEC; var MINUS_SIGN = constants.MINUS_SIGN; var BADNUM = constants.BADNUM; -var ZERO_PATH = { K: 'zeroline' }; -var GRID_PATH = { K: 'gridline', L: 'path' }; -var MINORGRID_PATH = { K: 'minor-gridline', L: 'path' }; -var TICK_PATH = { K: 'tick', L: 'path' }; -var TICK_TEXT = { K: 'tick', L: 'text' }; +var ZERO_PATH = { K: "zeroline" }; +var GRID_PATH = { K: "gridline", L: "path" }; +var MINORGRID_PATH = { K: "minor-gridline", L: "path" }; +var TICK_PATH = { K: "tick", L: "path" }; +var TICK_TEXT = { K: "tick", L: "text" }; var MARGIN_MAPPING = { - width: ['x', 'r', 'l', 'xl', 'xr'], - height: ['y', 't', 'b', 'yt', 'yb'], - right: ['r', 'xr'], - left: ['l', 'xl'], - top: ['t', 'yt'], - bottom: ['b', 'yb'] + width: ["x", "r", "l", "xl", "xr"], + height: ["y", "t", "b", "yt", "yb"], + right: ["r", "xr"], + left: ["l", "xl"], + top: ["t", "yt"], + bottom: ["b", "yb"], }; -var alignmentConstants = require('../../constants/alignment'); +var alignmentConstants = require("../../constants/alignment"); var MID_SHIFT = alignmentConstants.MID_SHIFT; var CAP_SHIFT = alignmentConstants.CAP_SHIFT; var LINE_SPACING = alignmentConstants.LINE_SPACING; @@ -56,12 +56,12 @@ var OPPOSITE_SIDE = alignmentConstants.OPPOSITE_SIDE; var TEXTPAD = 3; -var axes = module.exports = {}; +var axes = (module.exports = {}); -axes.setConvert = require('./set_convert'); -var autoType = require('./axis_autotype'); +axes.setConvert = require("./set_convert"); +var autoType = require("./axis_autotype"); -var axisIds = require('./axis_ids'); +var axisIds = require("./axis_ids"); var idSort = axisIds.idSort; var isLinked = axisIds.isLinked; @@ -74,17 +74,14 @@ axes.listIds = axisIds.listIds; axes.getFromId = axisIds.getFromId; axes.getFromTrace = axisIds.getFromTrace; -var autorange = require('./autorange'); +var autorange = require("./autorange"); axes.getAutoRange = autorange.getAutoRange; axes.findExtremes = autorange.findExtremes; var epsilon = 0.0001; function expandRange(range) { - var delta = (range[1] - range[0]) * epsilon; - return [ - range[0] - delta, - range[1] + delta - ]; + var delta = (range[1] - range[0]) * epsilon; + return [range[0] - delta, range[1] + delta]; } /* @@ -98,28 +95,46 @@ function expandRange(range) { * extraOption: aside from existing axes with this letter, what non-axis value is allowed? * Only required if it's different from `dflt` */ -axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption) { - var axLetter = attr.charAt(attr.length - 1); - var axlist = gd._fullLayout._subplots[axLetter + 'axis']; - var refAttr = attr + 'ref'; - var attrDef = {}; - - if(!dflt) dflt = axlist[0] || (typeof extraOption === 'string' ? extraOption : extraOption[0]); - if(!extraOption) extraOption = dflt; - axlist = axlist.concat(axlist.map(function(x) { return x + ' domain'; })); - - // data-ref annotations are not supported in gl2d yet - - attrDef[refAttr] = { - valType: 'enumerated', - values: axlist.concat(extraOption ? - (typeof extraOption === 'string' ? [extraOption] : extraOption) : - []), - dflt: dflt - }; - - // xref, yref - return Lib.coerce(containerIn, containerOut, attrDef, refAttr); +axes.coerceRef = function ( + containerIn, + containerOut, + gd, + attr, + dflt, + extraOption +) { + var axLetter = attr.charAt(attr.length - 1); + var axlist = gd._fullLayout._subplots[axLetter + "axis"]; + var refAttr = attr + "ref"; + var attrDef = {}; + + if (!dflt) + dflt = + axlist[0] || + (typeof extraOption === "string" ? extraOption : extraOption[0]); + if (!extraOption) extraOption = dflt; + axlist = axlist.concat( + axlist.map(function (x) { + return x + " domain"; + }) + ); + + // data-ref annotations are not supported in gl2d yet + + attrDef[refAttr] = { + valType: "enumerated", + values: axlist.concat( + extraOption + ? typeof extraOption === "string" + ? [extraOption] + : extraOption + : [] + ), + dflt: dflt, + }; + + // xref, yref + return Lib.coerce(containerIn, containerOut, attrDef, refAttr); }; /* @@ -130,11 +145,21 @@ axes.coerceRef = function(containerIn, containerOut, gd, attr, dflt, extraOption * ar: the axis reference string * */ -axes.getRefType = function(ar) { - if(ar === undefined) { return ar; } - if(ar === 'paper') { return 'paper'; } - if(ar === 'pixel') { return 'pixel'; } - if(/( domain)$/.test(ar)) { return 'domain'; } else { return 'range'; } +axes.getRefType = function (ar) { + if (ar === undefined) { + return ar; + } + if (ar === "paper") { + return "paper"; + } + if (ar === "pixel") { + return "pixel"; + } + if (/( domain)$/.test(ar)) { + return "domain"; + } else { + return "range"; + } }; /* @@ -159,397 +184,425 @@ axes.getRefType = function(ar) { * - for date axes: JS Dates or milliseconds, and convert to date strings * - for other types: coerce them to numbers */ -axes.coercePosition = function(containerOut, gd, coerce, axRef, attr, dflt) { - var cleanPos, pos; - var axRefType = axes.getRefType(axRef); - if(axRefType !== 'range') { - cleanPos = Lib.ensureNumber; - pos = coerce(attr, dflt); - } else { - var ax = axes.getFromId(gd, axRef); - dflt = ax.fraction2r(dflt); - pos = coerce(attr, dflt); - cleanPos = ax.cleanPos; - } - containerOut[attr] = cleanPos(pos); +axes.coercePosition = function (containerOut, gd, coerce, axRef, attr, dflt) { + var cleanPos, pos; + var axRefType = axes.getRefType(axRef); + if (axRefType !== "range") { + cleanPos = Lib.ensureNumber; + pos = coerce(attr, dflt); + } else { + var ax = axes.getFromId(gd, axRef); + dflt = ax.fraction2r(dflt); + pos = coerce(attr, dflt); + cleanPos = ax.cleanPos; + } + containerOut[attr] = cleanPos(pos); }; -axes.cleanPosition = function(pos, gd, axRef) { - var cleanPos = (axRef === 'paper' || axRef === 'pixel') ? - Lib.ensureNumber : - axes.getFromId(gd, axRef).cleanPos; +axes.cleanPosition = function (pos, gd, axRef) { + var cleanPos = + axRef === "paper" || axRef === "pixel" + ? Lib.ensureNumber + : axes.getFromId(gd, axRef).cleanPos; - return cleanPos(pos); + return cleanPos(pos); }; -axes.redrawComponents = function(gd, axIds) { - axIds = axIds ? axIds : axes.listIds(gd); +axes.redrawComponents = function (gd, axIds) { + axIds = axIds ? axIds : axes.listIds(gd); - var fullLayout = gd._fullLayout; + var fullLayout = gd._fullLayout; - function _redrawOneComp(moduleName, methodName, stashName, shortCircuit) { - var method = Registry.getComponentMethod(moduleName, methodName); - var stash = {}; + function _redrawOneComp(moduleName, methodName, stashName, shortCircuit) { + var method = Registry.getComponentMethod(moduleName, methodName); + var stash = {}; - for(var i = 0; i < axIds.length; i++) { - var ax = fullLayout[axes.id2name(axIds[i])]; - var indices = ax[stashName]; + for (var i = 0; i < axIds.length; i++) { + var ax = fullLayout[axes.id2name(axIds[i])]; + var indices = ax[stashName]; - for(var j = 0; j < indices.length; j++) { - var ind = indices[j]; + for (var j = 0; j < indices.length; j++) { + var ind = indices[j]; - if(!stash[ind]) { - method(gd, ind); - stash[ind] = 1; - // once is enough for images (which doesn't use the `i` arg anyway) - if(shortCircuit) return; - } - } + if (!stash[ind]) { + method(gd, ind); + stash[ind] = 1; + // once is enough for images (which doesn't use the `i` arg anyway) + if (shortCircuit) return; } + } } + } - // annotations and shapes 'draw' method is slow, - // use the finer-grained 'drawOne' method instead - _redrawOneComp('annotations', 'drawOne', '_annIndices'); - _redrawOneComp('shapes', 'drawOne', '_shapeIndices'); - _redrawOneComp('images', 'draw', '_imgIndices', true); - _redrawOneComp('selections', 'drawOne', '_selectionIndices'); + // annotations and shapes 'draw' method is slow, + // use the finer-grained 'drawOne' method instead + _redrawOneComp("annotations", "drawOne", "_annIndices"); + _redrawOneComp("shapes", "drawOne", "_shapeIndices"); + _redrawOneComp("images", "draw", "_imgIndices", true); + _redrawOneComp("selections", "drawOne", "_selectionIndices"); }; -var getDataConversions = axes.getDataConversions = function(gd, trace, target, targetArray) { - var ax; - - // If target points to an axis, use the type we already have for that - // axis to find the data type. Otherwise use the values to autotype. - var d2cTarget = (target === 'x' || target === 'y' || target === 'z') ? - target : - targetArray; - - // In the case of an array target, make a mock data array - // and call supplyDefaults to the data type and - // setup the data-to-calc method. - if(Array.isArray(d2cTarget)) { - ax = { - type: autoType(targetArray, undefined, { - autotypenumbers: gd._fullLayout.autotypenumbers - }), - _categories: [] - }; - axes.setConvert(ax); +var getDataConversions = (axes.getDataConversions = function ( + gd, + trace, + target, + targetArray +) { + var ax; + + // If target points to an axis, use the type we already have for that + // axis to find the data type. Otherwise use the values to autotype. + var d2cTarget = + target === "x" || target === "y" || target === "z" ? target : targetArray; + + // In the case of an array target, make a mock data array + // and call supplyDefaults to the data type and + // setup the data-to-calc method. + if (Array.isArray(d2cTarget)) { + ax = { + type: autoType(targetArray, undefined, { + autotypenumbers: gd._fullLayout.autotypenumbers, + }), + _categories: [], + }; + axes.setConvert(ax); - // build up ax._categories (usually done during ax.makeCalcdata() - if(ax.type === 'category') { - for(var i = 0; i < targetArray.length; i++) { - ax.d2c(targetArray[i]); - } - } - // TODO what to do for transforms? - } else { - ax = axes.getFromTrace(gd, trace, d2cTarget); + // build up ax._categories (usually done during ax.makeCalcdata() + if (ax.type === "category") { + for (var i = 0; i < targetArray.length; i++) { + ax.d2c(targetArray[i]); + } } + // TODO what to do for transforms? + } else { + ax = axes.getFromTrace(gd, trace, d2cTarget); + } - // if 'target' has corresponding axis - // -> use setConvert method - if(ax) return {d2c: ax.d2c, c2d: ax.c2d}; + // if 'target' has corresponding axis + // -> use setConvert method + if (ax) return { d2c: ax.d2c, c2d: ax.c2d }; - // special case for 'ids' - // -> cast to String - if(d2cTarget === 'ids') return {d2c: toString, c2d: toString}; + // special case for 'ids' + // -> cast to String + if (d2cTarget === "ids") return { d2c: toString, c2d: toString }; - // otherwise (e.g. numeric-array of 'marker.color' or 'marker.size') - // -> cast to Number + // otherwise (e.g. numeric-array of 'marker.color' or 'marker.size') + // -> cast to Number - return {d2c: toNum, c2d: toNum}; -}; + return { d2c: toNum, c2d: toNum }; +}); -function toNum(v) { return +v; } -function toString(v) { return String(v); } +function toNum(v) { + return +v; +} +function toString(v) { + return String(v); +} -axes.getDataToCoordFunc = function(gd, trace, target, targetArray) { - return getDataConversions(gd, trace, target, targetArray).d2c; +axes.getDataToCoordFunc = function (gd, trace, target, targetArray) { + return getDataConversions(gd, trace, target, targetArray).d2c; }; // 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) { - var axLetter = id.charAt(0); - if(axLetter === 'x') return 'y'; - if(axLetter === 'y') return 'x'; +axes.counterLetter = function (id) { + var axLetter = id.charAt(0); + if (axLetter === "x") return "y"; + if (axLetter === "y") return "x"; }; // incorporate a new minimum difference and first tick into // forced // note that _forceTick0 is linearized, so needs to be turned into // a range value for setting tick0 -axes.minDtick = function(ax, newDiff, newFirst, allow) { - // doesn't make sense to do forced min dTick on log or category axes, - // and the plot itself may decide to cancel (ie non-grouped bars) - if(['log', 'category', 'multicategory'].indexOf(ax.type) !== -1 || !allow) { - ax._minDtick = 0; - } else if(ax._minDtick === undefined) { - // undefined means there's nothing there yet - - ax._minDtick = newDiff; - ax._forceTick0 = newFirst; - } else if(ax._minDtick) { - if((ax._minDtick / newDiff + 1e-6) % 1 < 2e-6 && - // existing minDtick is an integer multiple of newDiff - // (within rounding err) - // and forceTick0 can be shifted to newFirst - - (((newFirst - ax._forceTick0) / newDiff % 1) + - 1.000001) % 1 < 2e-6) { - ax._minDtick = newDiff; - ax._forceTick0 = newFirst; - } else if((newDiff / ax._minDtick + 1e-6) % 1 > 2e-6 || - // if the converse is true (newDiff is a multiple of minDtick and - // newFirst can be shifted to forceTick0) then do nothing - same - // forcing stands. Otherwise, cancel forced minimum - - (((newFirst - ax._forceTick0) / ax._minDtick % 1) + - 1.000001) % 1 > 2e-6) { - ax._minDtick = 0; - } +axes.minDtick = function (ax, newDiff, newFirst, allow) { + // doesn't make sense to do forced min dTick on log or category axes, + // and the plot itself may decide to cancel (ie non-grouped bars) + if (["log", "category", "multicategory"].indexOf(ax.type) !== -1 || !allow) { + ax._minDtick = 0; + } else if (ax._minDtick === undefined) { + // undefined means there's nothing there yet + + ax._minDtick = newDiff; + ax._forceTick0 = newFirst; + } else if (ax._minDtick) { + if ( + (ax._minDtick / newDiff + 1e-6) % 1 < 2e-6 && + // existing minDtick is an integer multiple of newDiff + // (within rounding err) + // and forceTick0 can be shifted to newFirst + + ((((newFirst - ax._forceTick0) / newDiff) % 1) + 1.000001) % 1 < 2e-6 + ) { + ax._minDtick = newDiff; + ax._forceTick0 = newFirst; + } else if ( + (newDiff / ax._minDtick + 1e-6) % 1 > 2e-6 || + // if the converse is true (newDiff is a multiple of minDtick and + // newFirst can be shifted to forceTick0) then do nothing - same + // forcing stands. Otherwise, cancel forced minimum + + ((((newFirst - ax._forceTick0) / ax._minDtick) % 1) + 1.000001) % 1 > 2e-6 + ) { + ax._minDtick = 0; } + } }; // save a copy of the initial axis ranges in fullLayout // use them in mode bar and dblclick events -axes.saveRangeInitial = function(gd, overwrite) { - var axList = axes.list(gd, '', true); - var hasOneAxisChanged = false; - - for(var i = 0; i < axList.length; i++) { - var ax = axList[i]; - var isNew = - ax._rangeInitial0 === undefined && - ax._rangeInitial1 === undefined; - - var hasChanged = isNew || ( - ax.range[0] !== ax._rangeInitial0 || - ax.range[1] !== ax._rangeInitial1 - ); - - var autorange = ax.autorange; - if((isNew && autorange !== true) || (overwrite && hasChanged)) { - ax._rangeInitial0 = (autorange === 'min' || autorange === 'max reversed') ? undefined : ax.range[0]; - ax._rangeInitial1 = (autorange === 'max' || autorange === 'min reversed') ? undefined : ax.range[1]; - ax._autorangeInitial = autorange; - hasOneAxisChanged = true; - } - } - - return hasOneAxisChanged; +axes.saveRangeInitial = function (gd, overwrite) { + var axList = axes.list(gd, "", true); + var hasOneAxisChanged = false; + + for (var i = 0; i < axList.length; i++) { + var ax = axList[i]; + var isNew = + ax._rangeInitial0 === undefined && ax._rangeInitial1 === undefined; + + var hasChanged = + isNew || + ax.range[0] !== ax._rangeInitial0 || + ax.range[1] !== ax._rangeInitial1; + + var autorange = ax.autorange; + if ((isNew && autorange !== true) || (overwrite && hasChanged)) { + ax._rangeInitial0 = + autorange === "min" || autorange === "max reversed" + ? undefined + : ax.range[0]; + ax._rangeInitial1 = + autorange === "max" || autorange === "min reversed" + ? undefined + : ax.range[1]; + ax._autorangeInitial = autorange; + hasOneAxisChanged = true; + } + } + + return hasOneAxisChanged; }; // save a copy of the initial spike visibility -axes.saveShowSpikeInitial = function(gd, overwrite) { - var axList = axes.list(gd, '', true); - var hasOneAxisChanged = false; - var allSpikesEnabled = 'on'; - - for(var i = 0; i < axList.length; i++) { - var ax = axList[i]; - var isNew = (ax._showSpikeInitial === undefined); - var hasChanged = isNew || !(ax.showspikes === ax._showspikes); - - if(isNew || (overwrite && hasChanged)) { - ax._showSpikeInitial = ax.showspikes; - hasOneAxisChanged = true; - } - - if(allSpikesEnabled === 'on' && !ax.showspikes) { - allSpikesEnabled = 'off'; - } - } - gd._fullLayout._cartesianSpikesEnabled = allSpikesEnabled; - return hasOneAxisChanged; -}; - -axes.autoBin = function(data, ax, nbins, is2d, calendar, size) { - var dataMin = Lib.aggNums(Math.min, null, data); - var dataMax = Lib.aggNums(Math.max, null, data); - - if(ax.type === 'category' || ax.type === 'multicategory') { - return { - start: dataMin - 0.5, - end: dataMax + 0.5, - size: Math.max(1, Math.round(size) || 1), - _dataSpan: dataMax - dataMin, - }; - } +axes.saveShowSpikeInitial = function (gd, overwrite) { + var axList = axes.list(gd, "", true); + var hasOneAxisChanged = false; + var allSpikesEnabled = "on"; - if(!calendar) calendar = ax.calendar; + for (var i = 0; i < axList.length; i++) { + var ax = axList[i]; + var isNew = ax._showSpikeInitial === undefined; + var hasChanged = isNew || !(ax.showspikes === ax._showspikes); - // piggyback off tick code to make "nice" bin sizes and edges - var dummyAx; - if(ax.type === 'log') { - dummyAx = { - type: 'linear', - range: [dataMin, dataMax] - }; - } else { - dummyAx = { - type: ax.type, - range: Lib.simpleMap([dataMin, dataMax], ax.c2r, 0, calendar), - calendar: calendar - }; + if (isNew || (overwrite && hasChanged)) { + ax._showSpikeInitial = ax.showspikes; + hasOneAxisChanged = true; } - axes.setConvert(dummyAx); - - size = size && cleanTicks.dtick(size, dummyAx.type); - - if(size) { - dummyAx.dtick = size; - dummyAx.tick0 = cleanTicks.tick0(undefined, dummyAx.type, calendar); - } else { - var size0; - if(nbins) size0 = ((dataMax - dataMin) / nbins); - else { - // totally auto: scale off std deviation so the highest bin is - // somewhat taller than the total number of bins, but don't let - // the size get smaller than the 'nice' rounded down minimum - // difference between values - var distinctData = Lib.distinctVals(data); - var msexp = Math.pow(10, Math.floor( - Math.log(distinctData.minDiff) / Math.LN10)); - var minSize = msexp * Lib.roundUp( - distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true); - size0 = Math.max(minSize, 2 * Lib.stdev(data) / - Math.pow(data.length, is2d ? 0.25 : 0.4)); - - // fallback if ax.d2c output BADNUMs - // e.g. when user try to plot categorical bins - // on a layout.xaxis.type: 'linear' - if(!isNumeric(size0)) size0 = 1; - } - axes.autoTicks(dummyAx, size0); + if (allSpikesEnabled === "on" && !ax.showspikes) { + allSpikesEnabled = "off"; } + } + gd._fullLayout._cartesianSpikesEnabled = allSpikesEnabled; + return hasOneAxisChanged; +}; - var finalSize = dummyAx.dtick; - var binStart = axes.tickIncrement( - axes.tickFirst(dummyAx), finalSize, 'reverse', calendar); - var binEnd, bincount; +axes.autoBin = function (data, ax, nbins, is2d, calendar, size) { + var dataMin = Lib.aggNums(Math.min, null, data); + var dataMax = Lib.aggNums(Math.max, null, data); - // check for too many data points right at the edges of bins - // (>50% within 1% of bin edges) or all data points integral - // and offset the bins accordingly - if(typeof finalSize === 'number') { - binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax); + if (ax.type === "category" || ax.type === "multicategory") { + return { + start: dataMin - 0.5, + end: dataMax + 0.5, + size: Math.max(1, Math.round(size) || 1), + _dataSpan: dataMax - dataMin, + }; + } - bincount = 1 + Math.floor((dataMax - binStart) / finalSize); - binEnd = binStart + bincount * finalSize; - } else { - // month ticks - should be the only nonlinear kind we have at this point. - // dtick (as supplied by axes.autoTick) only has nonlinear values on - // date and log axes, but even if you display a histogram on a log axis - // we bin it on a linear axis (which one could argue against, but that's - // a separate issue) - if(dummyAx.dtick.charAt(0) === 'M') { - binStart = autoShiftMonthBins(binStart, data, finalSize, dataMin, calendar); - } + if (!calendar) calendar = ax.calendar; - // calculate the endpoint for nonlinear ticks - you have to - // just increment until you're done - binEnd = binStart; - bincount = 0; - while(binEnd <= dataMax) { - binEnd = axes.tickIncrement(binEnd, finalSize, false, calendar); - bincount++; - } - } - - return { - start: ax.c2r(binStart, 0, calendar), - end: ax.c2r(binEnd, 0, calendar), - size: finalSize, - _dataSpan: dataMax - dataMin + // piggyback off tick code to make "nice" bin sizes and edges + var dummyAx; + if (ax.type === "log") { + dummyAx = { + type: "linear", + range: [dataMin, dataMax], }; + } else { + dummyAx = { + type: ax.type, + range: Lib.simpleMap([dataMin, dataMax], ax.c2r, 0, calendar), + calendar: calendar, + }; + } + axes.setConvert(dummyAx); + + size = size && cleanTicks.dtick(size, dummyAx.type); + + if (size) { + dummyAx.dtick = size; + dummyAx.tick0 = cleanTicks.tick0(undefined, dummyAx.type, calendar); + } else { + var size0; + if (nbins) size0 = (dataMax - dataMin) / nbins; + else { + // totally auto: scale off std deviation so the highest bin is + // somewhat taller than the total number of bins, but don't let + // the size get smaller than the 'nice' rounded down minimum + // difference between values + var distinctData = Lib.distinctVals(data); + var msexp = Math.pow( + 10, + Math.floor(Math.log(distinctData.minDiff) / Math.LN10) + ); + var minSize = + msexp * + Lib.roundUp(distinctData.minDiff / msexp, [0.9, 1.9, 4.9, 9.9], true); + size0 = Math.max( + minSize, + (2 * Lib.stdev(data)) / Math.pow(data.length, is2d ? 0.25 : 0.4) + ); + + // fallback if ax.d2c output BADNUMs + // e.g. when user try to plot categorical bins + // on a layout.xaxis.type: 'linear' + if (!isNumeric(size0)) size0 = 1; + } + + axes.autoTicks(dummyAx, size0); + } + + var finalSize = dummyAx.dtick; + var binStart = axes.tickIncrement( + axes.tickFirst(dummyAx), + finalSize, + "reverse", + calendar + ); + var binEnd, bincount; + + // check for too many data points right at the edges of bins + // (>50% within 1% of bin edges) or all data points integral + // and offset the bins accordingly + if (typeof finalSize === "number") { + binStart = autoShiftNumericBins(binStart, data, dummyAx, dataMin, dataMax); + + bincount = 1 + Math.floor((dataMax - binStart) / finalSize); + binEnd = binStart + bincount * finalSize; + } else { + // month ticks - should be the only nonlinear kind we have at this point. + // dtick (as supplied by axes.autoTick) only has nonlinear values on + // date and log axes, but even if you display a histogram on a log axis + // we bin it on a linear axis (which one could argue against, but that's + // a separate issue) + if (dummyAx.dtick.charAt(0) === "M") { + binStart = autoShiftMonthBins( + binStart, + data, + finalSize, + dataMin, + calendar + ); + } + + // calculate the endpoint for nonlinear ticks - you have to + // just increment until you're done + binEnd = binStart; + bincount = 0; + while (binEnd <= dataMax) { + binEnd = axes.tickIncrement(binEnd, finalSize, false, calendar); + bincount++; + } + } + + return { + start: ax.c2r(binStart, 0, calendar), + end: ax.c2r(binEnd, 0, calendar), + size: finalSize, + _dataSpan: dataMax - dataMin, + }; }; - function autoShiftNumericBins(binStart, data, ax, dataMin, dataMax) { - var edgecount = 0; - var midcount = 0; - var intcount = 0; - var blankCount = 0; - - function nearEdge(v) { - // is a value within 1% of a bin edge? - return (1 + (v - binStart) * 100 / ax.dtick) % 100 < 2; - } - - for(var i = 0; i < data.length; i++) { - if(data[i] % 1 === 0) intcount++; - else if(!isNumeric(data[i])) blankCount++; - - if(nearEdge(data[i])) edgecount++; - if(nearEdge(data[i] + ax.dtick / 2)) midcount++; - } - var dataCount = data.length - blankCount; - - if(intcount === dataCount && ax.type !== 'date') { - if(ax.dtick < 1) { - // all integers: if bin size is <1, it's because - // that was specifically requested (large nbins) - // so respect that... but center the bins containing - // integers on those integers - - binStart = dataMin - 0.5 * ax.dtick; - } else { - // otherwise start half an integer down regardless of - // the bin size, just enough to clear up endpoint - // ambiguity about which integers are in which bins. - - binStart -= 0.5; - if(binStart + ax.dtick < dataMin) binStart += ax.dtick; - } - } else if(midcount < dataCount * 0.1) { - if(edgecount > dataCount * 0.3 || - nearEdge(dataMin) || nearEdge(dataMax)) { - // lots of points at the edge, not many in the middle - // shift half a bin - var binshift = ax.dtick / 2; - binStart += (binStart + binshift < dataMin) ? binshift : -binshift; - } - } - return binStart; + var edgecount = 0; + var midcount = 0; + var intcount = 0; + var blankCount = 0; + + function nearEdge(v) { + // is a value within 1% of a bin edge? + return (1 + ((v - binStart) * 100) / ax.dtick) % 100 < 2; + } + + for (var i = 0; i < data.length; i++) { + if (data[i] % 1 === 0) intcount++; + else if (!isNumeric(data[i])) blankCount++; + + if (nearEdge(data[i])) edgecount++; + if (nearEdge(data[i] + ax.dtick / 2)) midcount++; + } + var dataCount = data.length - blankCount; + + if (intcount === dataCount && ax.type !== "date") { + if (ax.dtick < 1) { + // all integers: if bin size is <1, it's because + // that was specifically requested (large nbins) + // so respect that... but center the bins containing + // integers on those integers + + binStart = dataMin - 0.5 * ax.dtick; + } else { + // otherwise start half an integer down regardless of + // the bin size, just enough to clear up endpoint + // ambiguity about which integers are in which bins. + + binStart -= 0.5; + if (binStart + ax.dtick < dataMin) binStart += ax.dtick; + } + } else if (midcount < dataCount * 0.1) { + if (edgecount > dataCount * 0.3 || nearEdge(dataMin) || nearEdge(dataMax)) { + // lots of points at the edge, not many in the middle + // shift half a bin + var binshift = ax.dtick / 2; + binStart += binStart + binshift < dataMin ? binshift : -binshift; + } + } + return binStart; } - function autoShiftMonthBins(binStart, data, dtick, dataMin, calendar) { - var stats = Lib.findExactDates(data, calendar); - // number of data points that needs to be an exact value - // to shift that increment to (near) the bin center - var threshold = 0.8; - - if(stats.exactDays > threshold) { - var numMonths = Number(dtick.substr(1)); - - if((stats.exactYears > threshold) && (numMonths % 12 === 0)) { - // The exact middle of a non-leap-year is 1.5 days into July - // so if we start the bins here, all but leap years will - // get hover-labeled as exact years. - binStart = axes.tickIncrement(binStart, 'M6', 'reverse') + ONEDAY * 1.5; - } else if(stats.exactMonths > threshold) { - // Months are not as clean, but if we shift half the *longest* - // month (31/2 days) then 31-day months will get labeled exactly - // and shorter months will get labeled with the correct month - // but shifted 12-36 hours into it. - binStart = axes.tickIncrement(binStart, 'M1', 'reverse') + ONEDAY * 15.5; - } else { - // Shifting half a day is exact, but since these are month bins it - // will always give a somewhat odd-looking label, until we do something - // smarter like showing the bin boundaries (or the bounds of the actual - // data in each bin) - binStart -= HALFDAY; - } - var nextBinStart = axes.tickIncrement(binStart, dtick); - - if(nextBinStart <= dataMin) return nextBinStart; + var stats = Lib.findExactDates(data, calendar); + // number of data points that needs to be an exact value + // to shift that increment to (near) the bin center + var threshold = 0.8; + + if (stats.exactDays > threshold) { + var numMonths = Number(dtick.substr(1)); + + if (stats.exactYears > threshold && numMonths % 12 === 0) { + // The exact middle of a non-leap-year is 1.5 days into July + // so if we start the bins here, all but leap years will + // get hover-labeled as exact years. + binStart = axes.tickIncrement(binStart, "M6", "reverse") + ONEDAY * 1.5; + } else if (stats.exactMonths > threshold) { + // Months are not as clean, but if we shift half the *longest* + // month (31/2 days) then 31-day months will get labeled exactly + // and shorter months will get labeled with the correct month + // but shifted 12-36 hours into it. + binStart = axes.tickIncrement(binStart, "M1", "reverse") + ONEDAY * 15.5; + } else { + // Shifting half a day is exact, but since these are month bins it + // will always give a somewhat odd-looking label, until we do something + // smarter like showing the bin boundaries (or the bounds of the actual + // data in each bin) + binStart -= HALFDAY; } - return binStart; + var nextBinStart = axes.tickIncrement(binStart, dtick); + + if (nextBinStart <= dataMin) return nextBinStart; + } + return binStart; } // ---------------------------------------------------- @@ -557,346 +610,352 @@ function autoShiftMonthBins(binStart, data, dtick, dataMin, calendar) { // ---------------------------------------------------- // ensure we have minor tick0 and dtick calculated -axes.prepMinorTicks = function(mockAx, ax, opts) { - if(!ax.minor.dtick) { - delete mockAx.dtick; - var hasMajor = ax.dtick && isNumeric(ax._tmin); - var mockMinorRange; - if(hasMajor) { - var tick2 = axes.tickIncrement(ax._tmin, ax.dtick, true); - // mock range a tiny bit smaller than one major tick interval - mockMinorRange = [ax._tmin, tick2 * 0.99 + ax._tmin * 0.01]; +axes.prepMinorTicks = function (mockAx, ax, opts) { + if (!ax.minor.dtick) { + delete mockAx.dtick; + var hasMajor = ax.dtick && isNumeric(ax._tmin); + var mockMinorRange; + if (hasMajor) { + var tick2 = axes.tickIncrement(ax._tmin, ax.dtick, true); + // mock range a tiny bit smaller than one major tick interval + mockMinorRange = [ax._tmin, tick2 * 0.99 + ax._tmin * 0.01]; + } else { + var rl = Lib.simpleMap(ax.range, ax.r2l); + // If we don't have a major dtick, the concept of minor ticks is a little + // ambiguous - just take a stab and say minor.nticks should span 1/5 the axis + mockMinorRange = [rl[0], 0.8 * rl[0] + 0.2 * rl[1]]; + } + mockAx.range = Lib.simpleMap(mockMinorRange, ax.l2r); + mockAx._isMinor = true; + + axes.prepTicks(mockAx, opts); + + if (hasMajor) { + var numericMajor = isNumeric(ax.dtick); + var numericMinor = isNumeric(mockAx.dtick); + var majorNum = numericMajor ? ax.dtick : +ax.dtick.substring(1); + var minorNum = numericMinor ? mockAx.dtick : +mockAx.dtick.substring(1); + if (numericMajor && numericMinor) { + if (!isMultiple(majorNum, minorNum)) { + // give up on minor ticks - outside the below exceptions, + // this can only happen if minor.nticks is smaller than two jumps + // in the auto-tick scale and the first jump is not an even multiple + // (5 -> 2 or for dates 3 ->2, 15 -> 10 etc) or if you provided + // an explicit dtick, in which case it's fine to give up, + // you can provide an explicit minor.dtick. + if (majorNum === 2 * ONEWEEK && minorNum === 3 * ONEDAY) { + mockAx.dtick = ONEWEEK; + } else if (majorNum === ONEWEEK && !(ax._input.minor || {}).nticks) { + // minor.nticks defaults to 5, but in this one case we want 7, + // so the minor ticks show on all days of the week + mockAx.dtick = ONEDAY; + } else if (isClose(majorNum / minorNum, 2.5)) { + // 5*10^n -> 2*10^n and you've set nticks < 5 + // quarters are pretty common, we don't do this by default as it + // would add an extra digit to display, but minor has no labels + mockAx.dtick = majorNum / 2; + } else { + mockAx.dtick = majorNum; + } + } else if (majorNum === 2 * ONEWEEK && minorNum === 2 * ONEDAY) { + // this is a weird one: we don't want to automatically choose + // 2-day minor ticks for 2-week major, even though it IS an even multiple, + // because people would expect to see the weeks clearly + mockAx.dtick = ONEWEEK; + } + } else if (String(ax.dtick).charAt(0) === "M") { + if (numericMinor) { + mockAx.dtick = "M1"; } else { - var rl = Lib.simpleMap(ax.range, ax.r2l); - // If we don't have a major dtick, the concept of minor ticks is a little - // ambiguous - just take a stab and say minor.nticks should span 1/5 the axis - mockMinorRange = [rl[0], 0.8 * rl[0] + 0.2 * rl[1]]; + if (!isMultiple(majorNum, minorNum)) { + // unless you provided an explicit ax.dtick (in which case + // it's OK for us to give up, you can provide an explicit + // minor.dtick too), this can only happen with: + // minor.nticks < 3 and dtick === M3, or + // minor.nticks < 5 and dtick === 5 * 10^n years + // so in all cases we just give up. + mockAx.dtick = ax.dtick; + } else if (majorNum >= 12 && minorNum === 2) { + // another special carve-out: for year major ticks, don't show + // 2-month minor ticks, bump to quarters + mockAx.dtick = "M3"; + } } - mockAx.range = Lib.simpleMap(mockMinorRange, ax.l2r); - mockAx._isMinor = true; - - axes.prepTicks(mockAx, opts); - - if(hasMajor) { - var numericMajor = isNumeric(ax.dtick); - var numericMinor = isNumeric(mockAx.dtick); - var majorNum = numericMajor ? ax.dtick : +ax.dtick.substring(1); - var minorNum = numericMinor ? mockAx.dtick : +mockAx.dtick.substring(1); - if(numericMajor && numericMinor) { - if(!isMultiple(majorNum, minorNum)) { - // give up on minor ticks - outside the below exceptions, - // this can only happen if minor.nticks is smaller than two jumps - // in the auto-tick scale and the first jump is not an even multiple - // (5 -> 2 or for dates 3 ->2, 15 -> 10 etc) or if you provided - // an explicit dtick, in which case it's fine to give up, - // you can provide an explicit minor.dtick. - if((majorNum === 2 * ONEWEEK) && (minorNum === 3 * ONEDAY)) { - mockAx.dtick = ONEWEEK; - } else if(majorNum === ONEWEEK && !(ax._input.minor || {}).nticks) { - // minor.nticks defaults to 5, but in this one case we want 7, - // so the minor ticks show on all days of the week - mockAx.dtick = ONEDAY; - } else if(isClose(majorNum / minorNum, 2.5)) { - // 5*10^n -> 2*10^n and you've set nticks < 5 - // quarters are pretty common, we don't do this by default as it - // would add an extra digit to display, but minor has no labels - mockAx.dtick = majorNum / 2; - } else { - mockAx.dtick = majorNum; - } - } else if(majorNum === 2 * ONEWEEK && minorNum === 2 * ONEDAY) { - // this is a weird one: we don't want to automatically choose - // 2-day minor ticks for 2-week major, even though it IS an even multiple, - // because people would expect to see the weeks clearly - mockAx.dtick = ONEWEEK; - } - } else if(String(ax.dtick).charAt(0) === 'M') { - if(numericMinor) { - mockAx.dtick = 'M1'; - } else { - if(!isMultiple(majorNum, minorNum)) { - // unless you provided an explicit ax.dtick (in which case - // it's OK for us to give up, you can provide an explicit - // minor.dtick too), this can only happen with: - // minor.nticks < 3 and dtick === M3, or - // minor.nticks < 5 and dtick === 5 * 10^n years - // so in all cases we just give up. - mockAx.dtick = ax.dtick; - } else if((majorNum >= 12) && (minorNum === 2)) { - // another special carve-out: for year major ticks, don't show - // 2-month minor ticks, bump to quarters - mockAx.dtick = 'M3'; - } - } - } else if(String(mockAx.dtick).charAt(0) === 'L') { - if(String(ax.dtick).charAt(0) === 'L') { - if(!isMultiple(majorNum, minorNum)) { - mockAx.dtick = isClose(majorNum / minorNum, 2.5) ? (ax.dtick / 2) : ax.dtick; - } - } else { - mockAx.dtick = 'D1'; - } - } else if(mockAx.dtick === 'D2' && +ax.dtick > 1) { - // the D2 log axis tick spacing is confusing for unlabeled minor ticks if - // the major dtick is more than one order of magnitude. - mockAx.dtick = 1; - } + } else if (String(mockAx.dtick).charAt(0) === "L") { + if (String(ax.dtick).charAt(0) === "L") { + if (!isMultiple(majorNum, minorNum)) { + mockAx.dtick = isClose(majorNum / minorNum, 2.5) + ? ax.dtick / 2 + : ax.dtick; + } + } else { + mockAx.dtick = "D1"; } - // put back the original range, to use to find the full set of minor ticks - mockAx.range = ax.range; - } - if(ax.minor._tick0Init === undefined) { - // ensure identical tick0 - mockAx.tick0 = ax.tick0; - } + } else if (mockAx.dtick === "D2" && +ax.dtick > 1) { + // the D2 log axis tick spacing is confusing for unlabeled minor ticks if + // the major dtick is more than one order of magnitude. + mockAx.dtick = 1; + } + } + // put back the original range, to use to find the full set of minor ticks + mockAx.range = ax.range; + } + if (ax.minor._tick0Init === undefined) { + // ensure identical tick0 + mockAx.tick0 = ax.tick0; + } }; function isMultiple(bigger, smaller) { - return Math.abs((bigger / smaller + 0.5) % 1 - 0.5) < 0.001; + return Math.abs(((bigger / smaller + 0.5) % 1) - 0.5) < 0.001; } function isClose(a, b) { - return Math.abs((a / b) - 1) < 0.001; + return Math.abs(a / b - 1) < 0.001; } // ensure we have tick0, dtick, and tick rounding calculated -axes.prepTicks = function(ax, opts) { - var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts); - - // calculate max number of (auto) ticks to display based on plot size - if(ax.tickmode === 'auto' || !ax.dtick) { - var nt = ax.nticks; - var minPx; - - if(!nt) { - if(ax.type === 'category' || ax.type === 'multicategory') { - minPx = ax.tickfont ? Lib.bigFont(ax.tickfont.size || 12) : 15; - nt = ax._length / minPx; - } else { - minPx = ax._id.charAt(0) === 'y' ? 40 : 80; - nt = Lib.constrain(ax._length / minPx, 4, 9) + 1; - } - - // radial axes span half their domain, - // multiply nticks value by two to get correct number of auto ticks. - if(ax._name === 'radialaxis') nt *= 2; - } +axes.prepTicks = function (ax, opts) { + var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts); - if(!(ax.minor && ax.minor.tickmode !== 'array')) { - // add a couple of extra digits for filling in ticks when we - // have explicit tickvals without tick text - if(ax.tickmode === 'array') nt *= 100; - } + // calculate max number of (auto) ticks to display based on plot size + if (ax.tickmode === "auto" || !ax.dtick) { + var nt = ax.nticks; + var minPx; - ax._roughDTick = Math.abs(rng[1] - rng[0]) / nt; - axes.autoTicks(ax, ax._roughDTick); + if (!nt) { + if (ax.type === "category" || ax.type === "multicategory") { + minPx = ax.tickfont ? Lib.bigFont(ax.tickfont.size || 12) : 15; + nt = ax._length / minPx; + } else { + minPx = ax._id.charAt(0) === "y" ? 40 : 80; + nt = Lib.constrain(ax._length / minPx, 4, 9) + 1; + } - // check for a forced minimum dtick - if(ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) { - ax.dtick = ax._minDtick; - ax.tick0 = ax.l2r(ax._forceTick0); - } + // radial axes span half their domain, + // multiply nticks value by two to get correct number of auto ticks. + if (ax._name === "radialaxis") nt *= 2; } - if(ax.ticklabelmode === 'period') { - adjustPeriodDelta(ax); + if (!(ax.minor && ax.minor.tickmode !== "array")) { + // add a couple of extra digits for filling in ticks when we + // have explicit tickvals without tick text + if (ax.tickmode === "array") nt *= 100; } - // check for missing tick0 - if(!ax.tick0) { - ax.tick0 = (ax.type === 'date') ? '2000-01-01' : 0; + ax._roughDTick = Math.abs(rng[1] - rng[0]) / nt; + axes.autoTicks(ax, ax._roughDTick); + + // check for a forced minimum dtick + if (ax._minDtick > 0 && ax.dtick < ax._minDtick * 2) { + ax.dtick = ax._minDtick; + ax.tick0 = ax.l2r(ax._forceTick0); } + } + + if (ax.ticklabelmode === "period") { + adjustPeriodDelta(ax); + } + + // check for missing tick0 + if (!ax.tick0) { + ax.tick0 = ax.type === "date" ? "2000-01-01" : 0; + } - // ensure we don't try to make ticks below our minimum precision - // see https://github.com/plotly/plotly.js/issues/2892 - if(ax.type === 'date' && ax.dtick < 0.1) ax.dtick = 0.1; + // ensure we don't try to make ticks below our minimum precision + // see https://github.com/plotly/plotly.js/issues/2892 + if (ax.type === "date" && ax.dtick < 0.1) ax.dtick = 0.1; - // now figure out rounding of tick values - autoTickRound(ax); + // now figure out rounding of tick values + autoTickRound(ax); }; function nMonths(dtick) { - return +(dtick.substring(1)); + return +dtick.substring(1); } -function adjustPeriodDelta(ax) { // adjusts ax.dtick and sets ax._definedDelta - var definedDelta; - - function mDate() { - return !( - isNumeric(ax.dtick) || - ax.dtick.charAt(0) !== 'M' - ); - } - var isMDate = mDate(); - var tickformat = axes.getTickFormat(ax); - if(tickformat) { - var noDtick = ax._dtickInit !== ax.dtick; - if( - !(/%[fLQsSMX]/.test(tickformat)) - // %f: microseconds as a decimal number [000000, 999999] - // %L: milliseconds as a decimal number [000, 999] - // %Q: milliseconds since UNIX epoch - // %s: seconds since UNIX epoch - // %S: second as a decimal number [00,61] - // %M: minute as a decimal number [00,59] - // %X: the locale’s time, such as %-I:%M:%S %p - ) { - if( - /%[HI]/.test(tickformat) - // %H: hour (24-hour clock) as a decimal number [00,23] - // %I: hour (12-hour clock) as a decimal number [01,12] - ) { - definedDelta = ONEHOUR; - if(noDtick && !isMDate && ax.dtick < ONEHOUR) ax.dtick = ONEHOUR; - } else if( - /%p/.test(tickformat) // %p: either AM or PM - ) { - definedDelta = HALFDAY; - if(noDtick && !isMDate && ax.dtick < HALFDAY) ax.dtick = HALFDAY; - } else if( - /%[Aadejuwx]/.test(tickformat) - // %A: full weekday name - // %a: abbreviated weekday name - // %d: zero-padded day of the month as a decimal number [01,31] - // %e: space-padded day of the month as a decimal number [ 1,31] - // %j: day of the year as a decimal number [001,366] - // %u: Monday-based (ISO 8601) weekday as a decimal number [1,7] - // %w: Sunday-based weekday as a decimal number [0,6] - // %x: the locale’s date, such as %-m/%-d/%Y - ) { - definedDelta = ONEDAY; - if(noDtick && !isMDate && ax.dtick < ONEDAY) ax.dtick = ONEDAY; - } else if( - /%[UVW]/.test(tickformat) - // %U: Sunday-based week of the year as a decimal number [00,53] - // %V: ISO 8601 week of the year as a decimal number [01, 53] - // %W: Monday-based week of the year as a decimal number [00,53] - ) { - definedDelta = ONEWEEK; - if(noDtick && !isMDate && ax.dtick < ONEWEEK) ax.dtick = ONEWEEK; - } else if( - /%[Bbm]/.test(tickformat) - // %B: full month name - // %b: abbreviated month name - // %m: month as a decimal number [01,12] - ) { - definedDelta = ONEAVGMONTH; - if(noDtick && ( - isMDate ? nMonths(ax.dtick) < 1 : ax.dtick < ONEMINMONTH) - ) ax.dtick = 'M1'; - } else if( - /%[q]/.test(tickformat) - // %q: quarter of the year as a decimal number [1,4] - ) { - definedDelta = ONEAVGQUARTER; - if(noDtick && ( - isMDate ? nMonths(ax.dtick) < 3 : ax.dtick < ONEMINQUARTER) - ) ax.dtick = 'M3'; - } else if( - /%[Yy]/.test(tickformat) - // %Y: year with century as a decimal number, such as 1999 - // %y: year without century as a decimal number [00,99] - ) { - definedDelta = ONEAVGYEAR; - if(noDtick && ( - isMDate ? nMonths(ax.dtick) < 12 : ax.dtick < ONEMINYEAR) - ) ax.dtick = 'M12'; - } - } +function adjustPeriodDelta(ax) { + // adjusts ax.dtick and sets ax._definedDelta + var definedDelta; + + function mDate() { + return !(isNumeric(ax.dtick) || ax.dtick.charAt(0) !== "M"); + } + var isMDate = mDate(); + var tickformat = axes.getTickFormat(ax); + if (tickformat) { + var noDtick = ax._dtickInit !== ax.dtick; + if ( + !/%[fLQsSMX]/.test(tickformat) + // %f: microseconds as a decimal number [000000, 999999] + // %L: milliseconds as a decimal number [000, 999] + // %Q: milliseconds since UNIX epoch + // %s: seconds since UNIX epoch + // %S: second as a decimal number [00,61] + // %M: minute as a decimal number [00,59] + // %X: the locale’s time, such as %-I:%M:%S %p + ) { + if ( + /%[HI]/.test(tickformat) + // %H: hour (24-hour clock) as a decimal number [00,23] + // %I: hour (12-hour clock) as a decimal number [01,12] + ) { + definedDelta = ONEHOUR; + if (noDtick && !isMDate && ax.dtick < ONEHOUR) ax.dtick = ONEHOUR; + } else if ( + /%p/.test(tickformat) // %p: either AM or PM + ) { + definedDelta = HALFDAY; + if (noDtick && !isMDate && ax.dtick < HALFDAY) ax.dtick = HALFDAY; + } else if ( + /%[Aadejuwx]/.test(tickformat) + // %A: full weekday name + // %a: abbreviated weekday name + // %d: zero-padded day of the month as a decimal number [01,31] + // %e: space-padded day of the month as a decimal number [ 1,31] + // %j: day of the year as a decimal number [001,366] + // %u: Monday-based (ISO 8601) weekday as a decimal number [1,7] + // %w: Sunday-based weekday as a decimal number [0,6] + // %x: the locale’s date, such as %-m/%-d/%Y + ) { + definedDelta = ONEDAY; + if (noDtick && !isMDate && ax.dtick < ONEDAY) ax.dtick = ONEDAY; + } else if ( + /%[UVW]/.test(tickformat) + // %U: Sunday-based week of the year as a decimal number [00,53] + // %V: ISO 8601 week of the year as a decimal number [01, 53] + // %W: Monday-based week of the year as a decimal number [00,53] + ) { + definedDelta = ONEWEEK; + if (noDtick && !isMDate && ax.dtick < ONEWEEK) ax.dtick = ONEWEEK; + } else if ( + /%[Bbm]/.test(tickformat) + // %B: full month name + // %b: abbreviated month name + // %m: month as a decimal number [01,12] + ) { + definedDelta = ONEAVGMONTH; + if ( + noDtick && + (isMDate ? nMonths(ax.dtick) < 1 : ax.dtick < ONEMINMONTH) + ) + ax.dtick = "M1"; + } else if ( + /%[q]/.test(tickformat) + // %q: quarter of the year as a decimal number [1,4] + ) { + definedDelta = ONEAVGQUARTER; + if ( + noDtick && + (isMDate ? nMonths(ax.dtick) < 3 : ax.dtick < ONEMINQUARTER) + ) + ax.dtick = "M3"; + } else if ( + /%[Yy]/.test(tickformat) + // %Y: year with century as a decimal number, such as 1999 + // %y: year without century as a decimal number [00,99] + ) { + definedDelta = ONEAVGYEAR; + if ( + noDtick && + (isMDate ? nMonths(ax.dtick) < 12 : ax.dtick < ONEMINYEAR) + ) + ax.dtick = "M12"; + } } + } - isMDate = mDate(); - if(isMDate && ax.tick0 === ax._dowTick0) { - // discard Sunday/Monday tweaks - ax.tick0 = ax._rawTick0; - } + isMDate = mDate(); + if (isMDate && ax.tick0 === ax._dowTick0) { + // discard Sunday/Monday tweaks + ax.tick0 = ax._rawTick0; + } - ax._definedDelta = definedDelta; + ax._definedDelta = definedDelta; } function positionPeriodTicks(tickVals, ax, definedDelta) { - for(var i = 0; i < tickVals.length; i++) { - var v = tickVals[i].value; - - var a = i; - var b = i + 1; - if(i < tickVals.length - 1) { - a = i; - b = i + 1; - } else if(i > 0) { - a = i - 1; - b = i; - } else { - a = i; - b = i; - } - - var A = tickVals[a].value; - var B = tickVals[b].value; - var actualDelta = Math.abs(B - A); - var delta = definedDelta || actualDelta; - var periodLength = 0; - - if(delta >= ONEMINYEAR) { - if(actualDelta >= ONEMINYEAR && actualDelta <= ONEMAXYEAR) { - periodLength = actualDelta; - } else { - periodLength = ONEAVGYEAR; - } - } else if(definedDelta === ONEAVGQUARTER && delta >= ONEMINQUARTER) { - if(actualDelta >= ONEMINQUARTER && actualDelta <= ONEMAXQUARTER) { - periodLength = actualDelta; - } else { - periodLength = ONEAVGQUARTER; - } - } else if(delta >= ONEMINMONTH) { - if(actualDelta >= ONEMINMONTH && actualDelta <= ONEMAXMONTH) { - periodLength = actualDelta; - } else { - periodLength = ONEAVGMONTH; - } - } else if(definedDelta === ONEWEEK && delta >= ONEWEEK) { - periodLength = ONEWEEK; - } else if(delta >= ONEDAY) { - periodLength = ONEDAY; - } else if(definedDelta === HALFDAY && delta >= HALFDAY) { - periodLength = HALFDAY; - } else if(definedDelta === ONEHOUR && delta >= ONEHOUR) { - periodLength = ONEHOUR; - } - - var inBetween; - if(periodLength >= actualDelta) { - // ensure new label positions remain between ticks - periodLength = actualDelta; - inBetween = true; - } - - var endPeriod = v + periodLength; - if(ax.rangebreaks && periodLength > 0) { - var nAll = 84; // highly divisible 7 * 12 - var n = 0; - for(var c = 0; c < nAll; c++) { - var r = (c + 0.5) / nAll; - if(ax.maskBreaks(v * (1 - r) + r * endPeriod) !== BADNUM) n++; - } - periodLength *= n / nAll; - - if(!periodLength) { - tickVals[i].drop = true; - } - - if(inBetween && actualDelta > ONEWEEK) periodLength = actualDelta; // center monthly & longer periods - } - - if( - periodLength > 0 || // not instant - i === 0 // taking care first tick added - ) { - tickVals[i].periodX = v + periodLength / 2; - } + for (var i = 0; i < tickVals.length; i++) { + var v = tickVals[i].value; + + var a = i; + var b = i + 1; + if (i < tickVals.length - 1) { + a = i; + b = i + 1; + } else if (i > 0) { + a = i - 1; + b = i; + } else { + a = i; + b = i; + } + + var A = tickVals[a].value; + var B = tickVals[b].value; + var actualDelta = Math.abs(B - A); + var delta = definedDelta || actualDelta; + var periodLength = 0; + + if (delta >= ONEMINYEAR) { + if (actualDelta >= ONEMINYEAR && actualDelta <= ONEMAXYEAR) { + periodLength = actualDelta; + } else { + periodLength = ONEAVGYEAR; + } + } else if (definedDelta === ONEAVGQUARTER && delta >= ONEMINQUARTER) { + if (actualDelta >= ONEMINQUARTER && actualDelta <= ONEMAXQUARTER) { + periodLength = actualDelta; + } else { + periodLength = ONEAVGQUARTER; + } + } else if (delta >= ONEMINMONTH) { + if (actualDelta >= ONEMINMONTH && actualDelta <= ONEMAXMONTH) { + periodLength = actualDelta; + } else { + periodLength = ONEAVGMONTH; + } + } else if (definedDelta === ONEWEEK && delta >= ONEWEEK) { + periodLength = ONEWEEK; + } else if (delta >= ONEDAY) { + periodLength = ONEDAY; + } else if (definedDelta === HALFDAY && delta >= HALFDAY) { + periodLength = HALFDAY; + } else if (definedDelta === ONEHOUR && delta >= ONEHOUR) { + periodLength = ONEHOUR; + } + + var inBetween; + if (periodLength >= actualDelta) { + // ensure new label positions remain between ticks + periodLength = actualDelta; + inBetween = true; + } + + var endPeriod = v + periodLength; + if (ax.rangebreaks && periodLength > 0) { + var nAll = 84; // highly divisible 7 * 12 + var n = 0; + for (var c = 0; c < nAll; c++) { + var r = (c + 0.5) / nAll; + if (ax.maskBreaks(v * (1 - r) + r * endPeriod) !== BADNUM) n++; + } + periodLength *= n / nAll; + + if (!periodLength) { + tickVals[i].drop = true; + } + + if (inBetween && actualDelta > ONEWEEK) periodLength = actualDelta; // center monthly & longer periods + } + + if ( + periodLength > 0 || // not instant + i === 0 // taking care first tick added + ) { + tickVals[i].periodX = v + periodLength / 2; } + } } // calculate the ticks: text, values, positioning @@ -904,412 +963,411 @@ function positionPeriodTicks(tickVals, ax, definedDelta) { // in any case, set tickround to # of digits to round tick labels to, // or codes to this effect for log and date scales axes.calcTicks = function calcTicks(ax, opts) { - var type = ax.type; - var calendar = ax.calendar; - var ticklabelstep = ax.ticklabelstep; - var isPeriod = ax.ticklabelmode === 'period'; - - var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts); - var axrev = (rng[1] < rng[0]); - var minRange = Math.min(rng[0], rng[1]); - var maxRange = Math.max(rng[0], rng[1]); + var type = ax.type; + var calendar = ax.calendar; + var ticklabelstep = ax.ticklabelstep; + var isPeriod = ax.ticklabelmode === "period"; - var maxTicks = Math.max(1000, ax._length || 0); + var rng = Lib.simpleMap(ax.range, ax.r2l, undefined, undefined, opts); + var axrev = rng[1] < rng[0]; + var minRange = Math.min(rng[0], rng[1]); + var maxRange = Math.max(rng[0], rng[1]); - var ticksOut = []; - var minorTicks = []; + var maxTicks = Math.max(1000, ax._length || 0); - var tickVals = []; - var minorTickVals = []; + var ticksOut = []; + var minorTicks = []; - var hasMinor = ax.minor && (ax.minor.ticks || ax.minor.showgrid); + var tickVals = []; + var minorTickVals = []; - // calc major first - for(var major = 1; major >= (hasMinor ? 0 : 1); major--) { - var isMinor = !major; + var hasMinor = ax.minor && (ax.minor.ticks || ax.minor.showgrid); - if(major) { - ax._dtickInit = ax.dtick; - ax._tick0Init = ax.tick0; - } else { - ax.minor._dtickInit = ax.minor.dtick; - ax.minor._tick0Init = ax.minor.tick0; - } - - var mockAx = major ? ax : Lib.extendFlat({}, ax, ax.minor); - - if(isMinor) { - axes.prepMinorTicks(mockAx, ax, opts); - } else { - axes.prepTicks(mockAx, opts); - } + // calc major first + for (var major = 1; major >= (hasMinor ? 0 : 1); major--) { + var isMinor = !major; - // now that we've figured out the auto values for formatting - // in case we're missing some ticktext, we can break out for array ticks - if(mockAx.tickmode === 'array') { - if(major) { - tickVals = []; - ticksOut = arrayTicks(ax); - } else { - minorTickVals = []; - minorTicks = arrayTicks(ax); - } - continue; - } - - // fill tickVals based on overlaying axis - if(mockAx.tickmode === 'sync') { - tickVals = []; - ticksOut = syncTicks(ax); - continue; - } - - // add a tiny bit so we get ticks which may have rounded out - var exRng = expandRange(rng); - var startTick = exRng[0]; - var endTick = exRng[1]; - - var numDtick = isNumeric(mockAx.dtick); - var isDLog = (type === 'log') && !(numDtick || mockAx.dtick.charAt(0) === 'L'); + if (major) { + ax._dtickInit = ax.dtick; + ax._tick0Init = ax.tick0; + } else { + ax.minor._dtickInit = ax.minor.dtick; + ax.minor._tick0Init = ax.minor.tick0; + } - // find the first tick - var x0 = axes.tickFirst(mockAx, opts); + var mockAx = major ? ax : Lib.extendFlat({}, ax, ax.minor); - if(major) { - ax._tmin = x0; + if (isMinor) { + axes.prepMinorTicks(mockAx, ax, opts); + } else { + axes.prepTicks(mockAx, opts); + } - // No visible ticks? Quit. - // I've only seen this on category axes with all categories off the edge. - if((x0 < startTick) !== axrev) break; + // now that we've figured out the auto values for formatting + // in case we're missing some ticktext, we can break out for array ticks + if (mockAx.tickmode === "array") { + if (major) { + tickVals = []; + ticksOut = arrayTicks(ax); + } else { + minorTickVals = []; + minorTicks = arrayTicks(ax); + } + continue; + } - // return the full set of tick vals - if(type === 'category' || type === 'multicategory') { - endTick = (axrev) ? Math.max(-0.5, endTick) : - Math.min(ax._categories.length - 0.5, endTick); - } - } + // fill tickVals based on overlaying axis + if (mockAx.tickmode === "sync") { + tickVals = []; + ticksOut = syncTicks(ax); + continue; + } - var prevX = null; - var x = x0; - var majorId; - - if(major) { - // ids for ticklabelstep - var _dTick; - if(numDtick) { - _dTick = ax.dtick; - } else { - if(type === 'date') { - if(typeof ax.dtick === 'string' && ax.dtick.charAt(0) === 'M') { - _dTick = ONEAVGMONTH * ax.dtick.substring(1); - } - } else { - _dTick = ax._roughDTick; - } - } - - majorId = Math.round(( - ax.r2l(x) - - ax.r2l(ax.tick0) - ) / _dTick) - 1; + // add a tiny bit so we get ticks which may have rounded out + var exRng = expandRange(rng); + var startTick = exRng[0]; + var endTick = exRng[1]; + + var numDtick = isNumeric(mockAx.dtick); + var isDLog = + type === "log" && !(numDtick || mockAx.dtick.charAt(0) === "L"); + + // find the first tick + var x0 = axes.tickFirst(mockAx, opts); + + if (major) { + ax._tmin = x0; + + // No visible ticks? Quit. + // I've only seen this on category axes with all categories off the edge. + if (x0 < startTick !== axrev) break; + + // return the full set of tick vals + if (type === "category" || type === "multicategory") { + endTick = axrev + ? Math.max(-0.5, endTick) + : Math.min(ax._categories.length - 0.5, endTick); + } + } + + var prevX = null; + var x = x0; + var majorId; + + if (major) { + // ids for ticklabelstep + var _dTick; + if (numDtick) { + _dTick = ax.dtick; + } else { + if (type === "date") { + if (typeof ax.dtick === "string" && ax.dtick.charAt(0) === "M") { + _dTick = ONEAVGMONTH * ax.dtick.substring(1); + } + } else { + _dTick = ax._roughDTick; } + } - var dtick = mockAx.dtick; + majorId = Math.round((ax.r2l(x) - ax.r2l(ax.tick0)) / _dTick) - 1; + } - if(mockAx.rangebreaks && mockAx._tick0Init !== mockAx.tick0) { - // adjust tick0 - x = moveOutsideBreak(x, ax); - if(!axrev) { - x = axes.tickIncrement(x, dtick, !axrev, calendar); - } - } + var dtick = mockAx.dtick; - if(major && isPeriod) { - // add one item to label period before tick0 - x = axes.tickIncrement(x, dtick, !axrev, calendar); - majorId--; - } + if (mockAx.rangebreaks && mockAx._tick0Init !== mockAx.tick0) { + // adjust tick0 + x = moveOutsideBreak(x, ax); + if (!axrev) { + x = axes.tickIncrement(x, dtick, !axrev, calendar); + } + } - for(; - axrev ? - (x >= endTick) : - (x <= endTick); - x = axes.tickIncrement( - x, - dtick, - axrev, - calendar - ) - ) { - if(major) majorId++; - - if(mockAx.rangebreaks) { - if(!axrev) { - if(x < startTick) continue; - if(mockAx.maskBreaks(x) === BADNUM && moveOutsideBreak(x, mockAx) >= maxRange) break; - } - } - - // prevent infinite loops - no more than one tick per pixel, - // and make sure each value is different from the previous - if(tickVals.length > maxTicks || x === prevX) break; - prevX = x; - - var obj = { value: x }; - - if(major) { - if(isDLog && (x !== (x | 0))) { - obj.simpleLabel = true; - } - - if(ticklabelstep > 1 && majorId % ticklabelstep) { - obj.skipLabel = true; - } - - tickVals.push(obj); - } else { - obj.minor = true; - - minorTickVals.push(obj); - } - } + if (major && isPeriod) { + // add one item to label period before tick0 + x = axes.tickIncrement(x, dtick, !axrev, calendar); + majorId--; } - if(hasMinor) { - var canOverlap = - (ax.minor.ticks === 'inside' && ax.ticks === 'outside') || - (ax.minor.ticks === 'outside' && ax.ticks === 'inside'); - - if(!canOverlap) { - // remove duplicate minors - - var majorValues = tickVals.map(function(d) { return d.value; }); - - var list = []; - for(var k = 0; k < minorTickVals.length; k++) { - var T = minorTickVals[k]; - var v = T.value; - if(majorValues.indexOf(v) !== -1) { - continue; - } - var found = false; - for(var q = 0; !found && (q < tickVals.length); q++) { - if( - // add 10e6 to eliminate problematic digits - 10e6 + tickVals[q].value === - 10e6 + v - ) { - found = true; - } - } - if(!found) list.push(T); - } - minorTickVals = list; + for ( + ; + axrev ? x >= endTick : x <= endTick; + x = axes.tickIncrement(x, dtick, axrev, calendar) + ) { + if (major) majorId++; + + if (mockAx.rangebreaks) { + if (!axrev) { + if (x < startTick) continue; + if ( + mockAx.maskBreaks(x) === BADNUM && + moveOutsideBreak(x, mockAx) >= maxRange + ) + break; } - } + } - if(isPeriod) positionPeriodTicks(tickVals, ax, ax._definedDelta); + // prevent infinite loops - no more than one tick per pixel, + // and make sure each value is different from the previous + if (tickVals.length > maxTicks || x === prevX) break; + prevX = x; - var i; - if(ax.rangebreaks) { - var flip = ax._id.charAt(0) === 'y'; + var obj = { value: x }; - var fontSize = 1; // one pixel minimum - if(ax.tickmode === 'auto') { - fontSize = ax.tickfont ? ax.tickfont.size : 12; + if (major) { + if (isDLog && x !== (x | 0)) { + obj.simpleLabel = true; } - var prevL = NaN; - for(i = tickVals.length - 1; i > -1; i--) { - if(tickVals[i].drop) { - tickVals.splice(i, 1); - continue; - } - - tickVals[i].value = moveOutsideBreak(tickVals[i].value, ax); - - // avoid overlaps - var l = ax.c2p(tickVals[i].value); - if(flip ? - (prevL > l - fontSize) : - (prevL < l + fontSize) - ) { // ensure one pixel minimum - tickVals.splice(axrev ? i + 1 : i, 1); - } else { - prevL = l; - } + if (ticklabelstep > 1 && majorId % ticklabelstep) { + obj.skipLabel = true; } - } - // If same angle over a full circle, the last tick vals is a duplicate. - // TODO must do something similar for angular date axes. - if(isAngular(ax) && Math.abs(rng[1] - rng[0]) === 360) { - tickVals.pop(); - } + tickVals.push(obj); + } else { + obj.minor = true; - // save the last tick as well as first, so we can - // show the exponent only on the last one - ax._tmax = (tickVals[tickVals.length - 1] || {}).value; - - // for showing the rest of a date when the main tick label is only the - // latter part: ax._prevDateHead holds what we showed most recently. - // Start with it cleared and mark that we're in calcTicks (ie calculating a - // whole string of these so we should care what the previous date head was!) - ax._prevDateHead = ''; - ax._inCalcTicks = true; - - var lastVisibleHead; - var hideLabel = function(tick) { - tick.text = ''; - ax._prevDateHead = lastVisibleHead; - }; + minorTickVals.push(obj); + } + } + } - tickVals = tickVals.concat(minorTickVals); + if (hasMinor) { + var canOverlap = + (ax.minor.ticks === "inside" && ax.ticks === "outside") || + (ax.minor.ticks === "outside" && ax.ticks === "inside"); - var t, p; - for(i = 0; i < tickVals.length; i++) { - var _minor = tickVals[i].minor; - var _value = tickVals[i].value; + if (!canOverlap) { + // remove duplicate minors - if(_minor) { - minorTicks.push({ - x: _value, - minor: true - }); - } else { - lastVisibleHead = ax._prevDateHead; + var majorValues = tickVals.map(function (d) { + return d.value; + }); - t = axes.tickText( - ax, - _value, - false, // hover - tickVals[i].simpleLabel // noSuffixPrefix - ); - - p = tickVals[i].periodX; - if(p !== undefined) { - t.periodX = p; - if(p > maxRange || p < minRange) { // hide label if outside the range - if(p > maxRange) t.periodX = maxRange; - if(p < minRange) t.periodX = minRange; - - hideLabel(t); - } - } + var list = []; + for (var k = 0; k < minorTickVals.length; k++) { + var T = minorTickVals[k]; + var v = T.value; + if (majorValues.indexOf(v) !== -1) { + continue; + } + var found = false; + for (var q = 0; !found && q < tickVals.length; q++) { + if ( + // add 10e6 to eliminate problematic digits + 10e6 + tickVals[q].value === + 10e6 + v + ) { + found = true; + } + } + if (!found) list.push(T); + } + minorTickVals = list; + } + } + + if (isPeriod) positionPeriodTicks(tickVals, ax, ax._definedDelta); + + var i; + if (ax.rangebreaks) { + var flip = ax._id.charAt(0) === "y"; + + var fontSize = 1; // one pixel minimum + if (ax.tickmode === "auto") { + fontSize = ax.tickfont ? ax.tickfont.size : 12; + } + + var prevL = NaN; + for (i = tickVals.length - 1; i > -1; i--) { + if (tickVals[i].drop) { + tickVals.splice(i, 1); + continue; + } + + tickVals[i].value = moveOutsideBreak(tickVals[i].value, ax); + + // avoid overlaps + var l = ax.c2p(tickVals[i].value); + if (flip ? prevL > l - fontSize : prevL < l + fontSize) { + // ensure one pixel minimum + tickVals.splice(axrev ? i + 1 : i, 1); + } else { + prevL = l; + } + } + } + + // If same angle over a full circle, the last tick vals is a duplicate. + // TODO must do something similar for angular date axes. + if (isAngular(ax) && Math.abs(rng[1] - rng[0]) === 360) { + tickVals.pop(); + } + + // save the last tick as well as first, so we can + // show the exponent only on the last one + ax._tmax = (tickVals[tickVals.length - 1] || {}).value; + + // for showing the rest of a date when the main tick label is only the + // latter part: ax._prevDateHead holds what we showed most recently. + // Start with it cleared and mark that we're in calcTicks (ie calculating a + // whole string of these so we should care what the previous date head was!) + ax._prevDateHead = ""; + ax._inCalcTicks = true; + + var lastVisibleHead; + var hideLabel = function (tick) { + tick.text = ""; + ax._prevDateHead = lastVisibleHead; + }; + + tickVals = tickVals.concat(minorTickVals); + + var t, p; + for (i = 0; i < tickVals.length; i++) { + var _minor = tickVals[i].minor; + var _value = tickVals[i].value; + + if (_minor) { + minorTicks.push({ + x: _value, + minor: true, + }); + } else { + lastVisibleHead = ax._prevDateHead; + + t = axes.tickText( + ax, + _value, + false, // hover + tickVals[i].simpleLabel // noSuffixPrefix + ); + + p = tickVals[i].periodX; + if (p !== undefined) { + t.periodX = p; + if (p > maxRange || p < minRange) { + // hide label if outside the range + if (p > maxRange) t.periodX = maxRange; + if (p < minRange) t.periodX = minRange; + + hideLabel(t); + } + } - if(tickVals[i].skipLabel) { - hideLabel(t); - } + if (tickVals[i].skipLabel) { + hideLabel(t); + } - ticksOut.push(t); - } + ticksOut.push(t); } - ticksOut = ticksOut.concat(minorTicks); + } + ticksOut = ticksOut.concat(minorTicks); - ax._inCalcTicks = false; + ax._inCalcTicks = false; - if(isPeriod && ticksOut.length) { - // drop very first tick that we added to handle period - ticksOut[0].noTick = true; - } + if (isPeriod && ticksOut.length) { + // drop very first tick that we added to handle period + ticksOut[0].noTick = true; + } - return ticksOut; + return ticksOut; }; function filterRangeBreaks(ax, ticksOut) { - if(ax.rangebreaks) { - // remove ticks falling inside rangebreaks - ticksOut = ticksOut.filter(function(d) { - return ax.maskBreaks(d.x) !== BADNUM; - }); - } + if (ax.rangebreaks) { + // remove ticks falling inside rangebreaks + ticksOut = ticksOut.filter(function (d) { + return ax.maskBreaks(d.x) !== BADNUM; + }); + } - return ticksOut; + return ticksOut; } function syncTicks(ax) { - // get the overlaying axis - var baseAxis = ax._mainAxis; - - var ticksOut = []; - if(baseAxis._vals) { - for(var i = 0; i < baseAxis._vals.length; i++) { - // filter vals with noTick flag - if(baseAxis._vals[i].noTick) { - continue; - } - - // get the position of the every tick - var pos = baseAxis.l2p(baseAxis._vals[i].x); - - // get the tick for the current axis based on position - var vali = ax.p2l(pos); - var obj = axes.tickText(ax, vali); - - // assign minor ticks - if(baseAxis._vals[i].minor) { - obj.minor = true; - obj.text = ''; - } - - ticksOut.push(obj); - } - } + // get the overlaying axis + var baseAxis = ax._mainAxis; - ticksOut = filterRangeBreaks(ax, ticksOut); + var ticksOut = []; + if (baseAxis._vals) { + for (var i = 0; i < baseAxis._vals.length; i++) { + // filter vals with noTick flag + if (baseAxis._vals[i].noTick) { + continue; + } - return ticksOut; -} + // get the position of the every tick + var pos = baseAxis.l2p(baseAxis._vals[i].x); -function arrayTicks(ax) { - var rng = Lib.simpleMap(ax.range, ax.r2l); - var exRng = expandRange(rng); - var tickMin = Math.min(exRng[0], exRng[1]); - var tickMax = Math.max(exRng[0], exRng[1]); + // get the tick for the current axis based on position + var vali = ax.p2l(pos); + var obj = axes.tickText(ax, vali); - // make sure showing ticks doesn't accidentally add new categories - // TODO multicategory, if we allow ticktext / tickvals - var tickVal2l = ax.type === 'category' ? ax.d2l_noadd : ax.d2l; + // assign minor ticks + if (baseAxis._vals[i].minor) { + obj.minor = true; + obj.text = ""; + } - // array ticks on log axes always show the full number - // (if no explicit ticktext overrides it) - if(ax.type === 'log' && String(ax.dtick).charAt(0) !== 'L') { - ax.dtick = 'L' + Math.pow(10, Math.floor(Math.min(ax.range[0], ax.range[1])) - 1); + ticksOut.push(obj); } + } - var ticksOut = []; - for(var isMinor = 0; isMinor <= 1; isMinor++) { - if(isMinor && !ax.minor) continue; - var vals = !isMinor ? ax.tickvals : ax.minor.tickvals; - var text = !isMinor ? ax.ticktext : []; + ticksOut = filterRangeBreaks(ax, ticksOut); - if(!vals) continue; - - - // without a text array, just format the given values as any other ticks - // except with more precision to the numbers - if(!Array.isArray(text)) text = []; - - for(var i = 0; i < vals.length; i++) { - var vali = tickVal2l(vals[i]); - if(vali > tickMin && vali < tickMax) { - var obj = text[i] === undefined ? - axes.tickText(ax, vali) : - tickTextObj(ax, vali, String(text[i])); - - if(isMinor) { - obj.minor = true; - obj.text = ''; - } + return ticksOut; +} - ticksOut.push(obj); - } +function arrayTicks(ax) { + var rng = Lib.simpleMap(ax.range, ax.r2l); + var exRng = expandRange(rng); + var tickMin = Math.min(exRng[0], exRng[1]); + var tickMax = Math.max(exRng[0], exRng[1]); + + // make sure showing ticks doesn't accidentally add new categories + // TODO multicategory, if we allow ticktext / tickvals + var tickVal2l = ax.type === "category" ? ax.d2l_noadd : ax.d2l; + + // array ticks on log axes always show the full number + // (if no explicit ticktext overrides it) + if (ax.type === "log" && String(ax.dtick).charAt(0) !== "L") { + ax.dtick = + "L" + Math.pow(10, Math.floor(Math.min(ax.range[0], ax.range[1])) - 1); + } + + var ticksOut = []; + for (var isMinor = 0; isMinor <= 1; isMinor++) { + if (isMinor && !ax.minor) continue; + var vals = !isMinor ? ax.tickvals : ax.minor.tickvals; + var text = !isMinor ? ax.ticktext : []; + + if (!vals) continue; + + // without a text array, just format the given values as any other ticks + // except with more precision to the numbers + if (!Array.isArray(text)) text = []; + + for (var i = 0; i < vals.length; i++) { + var vali = tickVal2l(vals[i]); + if (vali > tickMin && vali < tickMax) { + var obj = + text[i] === undefined + ? axes.tickText(ax, vali) + : tickTextObj(ax, vali, String(text[i])); + + if (isMinor) { + obj.minor = true; + obj.text = ""; } + + ticksOut.push(obj); + } } + } - ticksOut = filterRangeBreaks(ax, ticksOut); + ticksOut = filterRangeBreaks(ax, ticksOut); - return ticksOut; + return ticksOut; } var roundBase10 = [2, 5, 10]; @@ -1319,13 +1377,15 @@ var roundBase60 = [1, 2, 5, 10, 15, 30]; var roundDays = [1, 2, 3, 7, 14]; // approx. tick positions for log axes, showing all (1) and just 1, 2, 5 (2) // these don't have to be exact, just close enough to round to the right value -var roundLog1 = [-0.046, 0, 0.301, 0.477, 0.602, 0.699, 0.778, 0.845, 0.903, 0.954, 1]; +var roundLog1 = [ + -0.046, 0, 0.301, 0.477, 0.602, 0.699, 0.778, 0.845, 0.903, 0.954, 1, +]; var roundLog2 = [-0.301, 0, 0.301, 0.699, 1]; // N.B. `thetaunit; 'radians' angular axes must be converted to degrees var roundAngles = [15, 30, 45, 90, 180]; function roundDTick(roughDTick, base, roundingSet) { - return base * Lib.roundUp(roughDTick / base, roundingSet); + return base * Lib.roundUp(roughDTick / base, roundingSet); } // autoTicks: calculate best guess at pleasant ticks for this axis @@ -1344,106 +1404,109 @@ function roundDTick(roughDTick, base, roundingSet) { // log with linear ticks: L# where # is the linear tick spacing // log showing powers plus some intermediates: // D1 shows all digits, D2 shows 2 and 5 -axes.autoTicks = function(ax, roughDTick, isMinor) { - var base; - - function getBase(v) { - return Math.pow(v, Math.floor(Math.log(roughDTick) / Math.LN10)); - } - - if(ax.type === 'date') { - ax.tick0 = Lib.dateTick0(ax.calendar, 0); - - // the criteria below are all based on the rough spacing we calculate - // being > half of the final unit - so precalculate twice the rough val - var roughX2 = 2 * roughDTick; - - if(roughX2 > ONEAVGYEAR) { - roughDTick /= ONEAVGYEAR; - base = getBase(10); - ax.dtick = 'M' + (12 * roundDTick(roughDTick, base, roundBase10)); - } else if(roughX2 > ONEAVGMONTH) { - roughDTick /= ONEAVGMONTH; - ax.dtick = 'M' + roundDTick(roughDTick, 1, roundBase24); - } else if(roughX2 > ONEDAY) { - ax.dtick = roundDTick(roughDTick, ONEDAY, ax._hasDayOfWeekBreaks ? [1, 2, 7, 14] : roundDays); - if(!isMinor) { - // get week ticks on sunday - // this will also move the base tick off 2000-01-01 if dtick is - // 2 or 3 days... but that's a weird enough case that we'll ignore it. - var tickformat = axes.getTickFormat(ax); - var isPeriod = ax.ticklabelmode === 'period'; - if(isPeriod) ax._rawTick0 = ax.tick0; - - if(/%[uVW]/.test(tickformat)) { - ax.tick0 = Lib.dateTick0(ax.calendar, 2); // Monday - } else { - ax.tick0 = Lib.dateTick0(ax.calendar, 1); // Sunday - } - - if(isPeriod) ax._dowTick0 = ax.tick0; - } - } else if(roughX2 > ONEHOUR) { - ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24); - } else if(roughX2 > ONEMIN) { - ax.dtick = roundDTick(roughDTick, ONEMIN, roundBase60); - } else if(roughX2 > ONESEC) { - ax.dtick = roundDTick(roughDTick, ONESEC, roundBase60); +axes.autoTicks = function (ax, roughDTick, isMinor) { + var base; + + function getBase(v) { + return Math.pow(v, Math.floor(Math.log(roughDTick) / Math.LN10)); + } + + if (ax.type === "date") { + ax.tick0 = Lib.dateTick0(ax.calendar, 0); + + // the criteria below are all based on the rough spacing we calculate + // being > half of the final unit - so precalculate twice the rough val + var roughX2 = 2 * roughDTick; + + if (roughX2 > ONEAVGYEAR) { + roughDTick /= ONEAVGYEAR; + base = getBase(10); + ax.dtick = "M" + 12 * roundDTick(roughDTick, base, roundBase10); + } else if (roughX2 > ONEAVGMONTH) { + roughDTick /= ONEAVGMONTH; + ax.dtick = "M" + roundDTick(roughDTick, 1, roundBase24); + } else if (roughX2 > ONEDAY) { + ax.dtick = roundDTick( + roughDTick, + ONEDAY, + ax._hasDayOfWeekBreaks ? [1, 2, 7, 14] : roundDays + ); + if (!isMinor) { + // get week ticks on sunday + // this will also move the base tick off 2000-01-01 if dtick is + // 2 or 3 days... but that's a weird enough case that we'll ignore it. + var tickformat = axes.getTickFormat(ax); + var isPeriod = ax.ticklabelmode === "period"; + if (isPeriod) ax._rawTick0 = ax.tick0; + + if (/%[uVW]/.test(tickformat)) { + ax.tick0 = Lib.dateTick0(ax.calendar, 2); // Monday } else { - // milliseconds - base = getBase(10); - ax.dtick = roundDTick(roughDTick, base, roundBase10); - } - } else if(ax.type === 'log') { - ax.tick0 = 0; - var rng = Lib.simpleMap(ax.range, ax.r2l); - if(ax._isMinor) { - // Log axes by default get MORE than nTicks based on the metrics below - // But for minor ticks we don't want this increase, we already have - // the major ticks. - roughDTick *= 1.5; + ax.tick0 = Lib.dateTick0(ax.calendar, 1); // Sunday } - if(roughDTick > 0.7) { - // only show powers of 10 - ax.dtick = Math.ceil(roughDTick); - } else if(Math.abs(rng[1] - rng[0]) < 1) { - // span is less than one power of 10 - var nt = 1.5 * Math.abs((rng[1] - rng[0]) / roughDTick); - - // ticks on a linear scale, labeled fully - roughDTick = Math.abs(Math.pow(10, rng[1]) - - Math.pow(10, rng[0])) / nt; - base = getBase(10); - ax.dtick = 'L' + roundDTick(roughDTick, base, roundBase10); - } else { - // include intermediates between powers of 10, - // labeled with small digits - // ax.dtick = "D2" (show 2 and 5) or "D1" (show all digits) - ax.dtick = (roughDTick > 0.3) ? 'D2' : 'D1'; - } - } else if(ax.type === 'category' || ax.type === 'multicategory') { - ax.tick0 = 0; - ax.dtick = Math.ceil(Math.max(roughDTick, 1)); - } else if(isAngular(ax)) { - ax.tick0 = 0; - base = 1; - ax.dtick = roundDTick(roughDTick, base, roundAngles); - } else { - // auto ticks always start at 0 - ax.tick0 = 0; - base = getBase(10); - ax.dtick = roundDTick(roughDTick, base, roundBase10); - } - - // prevent infinite loops - if(ax.dtick === 0) ax.dtick = 1; - // TODO: this is from log axis histograms with autorange off - if(!isNumeric(ax.dtick) && typeof ax.dtick !== 'string') { - var olddtick = ax.dtick; - ax.dtick = 1; - throw 'ax.dtick error: ' + String(olddtick); + if (isPeriod) ax._dowTick0 = ax.tick0; + } + } else if (roughX2 > ONEHOUR) { + ax.dtick = roundDTick(roughDTick, ONEHOUR, roundBase24); + } else if (roughX2 > ONEMIN) { + ax.dtick = roundDTick(roughDTick, ONEMIN, roundBase60); + } else if (roughX2 > ONESEC) { + ax.dtick = roundDTick(roughDTick, ONESEC, roundBase60); + } else { + // milliseconds + base = getBase(10); + ax.dtick = roundDTick(roughDTick, base, roundBase10); } + } else if (ax.type === "log") { + ax.tick0 = 0; + var rng = Lib.simpleMap(ax.range, ax.r2l); + if (ax._isMinor) { + // Log axes by default get MORE than nTicks based on the metrics below + // But for minor ticks we don't want this increase, we already have + // the major ticks. + roughDTick *= 1.5; + } + if (roughDTick > 0.7) { + // only show powers of 10 + ax.dtick = Math.ceil(roughDTick); + } else if (Math.abs(rng[1] - rng[0]) < 1) { + // span is less than one power of 10 + var nt = 1.5 * Math.abs((rng[1] - rng[0]) / roughDTick); + + // ticks on a linear scale, labeled fully + roughDTick = Math.abs(Math.pow(10, rng[1]) - Math.pow(10, rng[0])) / nt; + base = getBase(10); + ax.dtick = "L" + roundDTick(roughDTick, base, roundBase10); + } else { + // include intermediates between powers of 10, + // labeled with small digits + // ax.dtick = "D2" (show 2 and 5) or "D1" (show all digits) + ax.dtick = roughDTick > 0.3 ? "D2" : "D1"; + } + } else if (ax.type === "category" || ax.type === "multicategory") { + ax.tick0 = 0; + ax.dtick = Math.ceil(Math.max(roughDTick, 1)); + } else if (isAngular(ax)) { + ax.tick0 = 0; + base = 1; + ax.dtick = roundDTick(roughDTick, base, roundAngles); + } else { + // auto ticks always start at 0 + ax.tick0 = 0; + base = getBase(10); + ax.dtick = roundDTick(roughDTick, base, roundBase10); + } + + // prevent infinite loops + if (ax.dtick === 0) ax.dtick = 1; + + // TODO: this is from log axis histograms with autorange off + if (!isNumeric(ax.dtick) && typeof ax.dtick !== "string") { + var olddtick = ax.dtick; + ax.dtick = 1; + throw "ax.dtick error: " + String(olddtick); + } }; // after dtick is already known, find tickround = precision @@ -1452,64 +1515,67 @@ axes.autoTicks = function(ax, roughDTick, isMinor) { // for date ticks, the last date part to show (y,m,d,H,M,S) // or an integer # digits past seconds function autoTickRound(ax) { - var dtick = ax.dtick; - - ax._tickexponent = 0; - if(!isNumeric(dtick) && typeof dtick !== 'string') { - dtick = 1; - } - - if(ax.type === 'category' || ax.type === 'multicategory') { - ax._tickround = null; - } - if(ax.type === 'date') { - // If tick0 is unusual, give tickround a bit more information - // not necessarily *all* the information in tick0 though, if it's really odd - // minimal string length for tick0: 'd' is 10, 'M' is 16, 'S' is 19 - // take off a leading minus (year < 0) and i (intercalary month) so length is consistent - var tick0ms = ax.r2l(ax.tick0); - var tick0str = ax.l2r(tick0ms).replace(/(^-|i)/g, ''); - var tick0len = tick0str.length; - - if(String(dtick).charAt(0) === 'M') { - // any tick0 more specific than a year: alway show the full date - if(tick0len > 10 || tick0str.substr(5) !== '01-01') ax._tickround = 'd'; - // show the month unless ticks are full multiples of a year - else ax._tickround = (+(dtick.substr(1)) % 12 === 0) ? 'y' : 'm'; - } else if((dtick >= ONEDAY && tick0len <= 10) || (dtick >= ONEDAY * 15)) ax._tickround = 'd'; - else if((dtick >= ONEMIN && tick0len <= 16) || (dtick >= ONEHOUR)) ax._tickround = 'M'; - else if((dtick >= ONESEC && tick0len <= 19) || (dtick >= ONEMIN)) ax._tickround = 'S'; - else { - // tickround is a number of digits of fractional seconds - // of any two adjacent ticks, at least one will have the maximum fractional digits - // of all possible ticks - so take the max. length of tick0 and the next one - var tick1len = ax.l2r(tick0ms + dtick).replace(/^-/, '').length; - ax._tickround = Math.max(tick0len, tick1len) - 20; - - // We shouldn't get here... but in case there's a situation I'm - // not thinking of where tick0str and tick1str are identical or - // something, fall back on maximum precision - if(ax._tickround < 0) ax._tickround = 4; - } - } else if(isNumeric(dtick) || dtick.charAt(0) === 'L') { - // linear or log (except D1, D2) - var rng = ax.range.map(ax.r2d || Number); - if(!isNumeric(dtick)) dtick = Number(dtick.substr(1)); - // 2 digits past largest digit of dtick - ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01); - - var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1])); - var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01); - var minexponent = ax.minexponent === undefined ? 3 : ax.minexponent; - if(Math.abs(rangeexp) > minexponent) { - if(isSIFormat(ax.exponentformat) && !beyondSI(rangeexp)) { - ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3); - } else ax._tickexponent = rangeexp; - } - } else { - // D1 or D2 (log) - ax._tickround = null; - } + var dtick = ax.dtick; + + ax._tickexponent = 0; + if (!isNumeric(dtick) && typeof dtick !== "string") { + dtick = 1; + } + + if (ax.type === "category" || ax.type === "multicategory") { + ax._tickround = null; + } + if (ax.type === "date") { + // If tick0 is unusual, give tickround a bit more information + // not necessarily *all* the information in tick0 though, if it's really odd + // minimal string length for tick0: 'd' is 10, 'M' is 16, 'S' is 19 + // take off a leading minus (year < 0) and i (intercalary month) so length is consistent + var tick0ms = ax.r2l(ax.tick0); + var tick0str = ax.l2r(tick0ms).replace(/(^-|i)/g, ""); + var tick0len = tick0str.length; + + if (String(dtick).charAt(0) === "M") { + // any tick0 more specific than a year: alway show the full date + if (tick0len > 10 || tick0str.substr(5) !== "01-01") ax._tickround = "d"; + // show the month unless ticks are full multiples of a year + else ax._tickround = +dtick.substr(1) % 12 === 0 ? "y" : "m"; + } else if ((dtick >= ONEDAY && tick0len <= 10) || dtick >= ONEDAY * 15) + ax._tickround = "d"; + else if ((dtick >= ONEMIN && tick0len <= 16) || dtick >= ONEHOUR) + ax._tickround = "M"; + else if ((dtick >= ONESEC && tick0len <= 19) || dtick >= ONEMIN) + ax._tickround = "S"; + else { + // tickround is a number of digits of fractional seconds + // of any two adjacent ticks, at least one will have the maximum fractional digits + // of all possible ticks - so take the max. length of tick0 and the next one + var tick1len = ax.l2r(tick0ms + dtick).replace(/^-/, "").length; + ax._tickround = Math.max(tick0len, tick1len) - 20; + + // We shouldn't get here... but in case there's a situation I'm + // not thinking of where tick0str and tick1str are identical or + // something, fall back on maximum precision + if (ax._tickround < 0) ax._tickround = 4; + } + } else if (isNumeric(dtick) || dtick.charAt(0) === "L") { + // linear or log (except D1, D2) + var rng = ax.range.map(ax.r2d || Number); + if (!isNumeric(dtick)) dtick = Number(dtick.substr(1)); + // 2 digits past largest digit of dtick + ax._tickround = 2 - Math.floor(Math.log(dtick) / Math.LN10 + 0.01); + + var maxend = Math.max(Math.abs(rng[0]), Math.abs(rng[1])); + var rangeexp = Math.floor(Math.log(maxend) / Math.LN10 + 0.01); + var minexponent = ax.minexponent === undefined ? 3 : ax.minexponent; + if (Math.abs(rangeexp) > minexponent) { + if (isSIFormat(ax.exponentformat) && !beyondSI(rangeexp)) { + ax._tickexponent = 3 * Math.round((rangeexp - 1) / 3); + } else ax._tickexponent = rangeexp; + } + } else { + // D1 or D2 (log) + ax._tickround = null; + } } // months and years don't have constant millisecond values @@ -1518,95 +1584,104 @@ function autoTickRound(ax) { // for pure powers of 10 // numeric ticks always have constant differences, other datetime ticks // can all be calculated as constant number of milliseconds -axes.tickIncrement = function(x, dtick, axrev, calendar) { - var axSign = axrev ? -1 : 1; +axes.tickIncrement = function (x, dtick, axrev, calendar) { + var axSign = axrev ? -1 : 1; - // includes linear, all dates smaller than month, and pure 10^n in log - if(isNumeric(dtick)) return Lib.increment(x, axSign * dtick); + // includes linear, all dates smaller than month, and pure 10^n in log + if (isNumeric(dtick)) return Lib.increment(x, axSign * dtick); - // everything else is a string, one character plus a number - var tType = dtick.charAt(0); - var dtSigned = axSign * Number(dtick.substr(1)); + // everything else is a string, one character plus a number + var tType = dtick.charAt(0); + var dtSigned = axSign * Number(dtick.substr(1)); - // Dates: months (or years - see Lib.incrementMonth) - if(tType === 'M') return Lib.incrementMonth(x, dtSigned, calendar); + // Dates: months (or years - see Lib.incrementMonth) + if (tType === "M") return Lib.incrementMonth(x, dtSigned, calendar); - // Log scales: Linear, Digits - if(tType === 'L') return Math.log(Math.pow(10, x) + dtSigned) / Math.LN10; + // Log scales: Linear, Digits + if (tType === "L") return Math.log(Math.pow(10, x) + dtSigned) / Math.LN10; - // log10 of 2,5,10, or all digits (logs just have to be - // close enough to round) - if(tType === 'D') { - var tickset = (dtick === 'D2') ? roundLog2 : roundLog1; - var x2 = x + axSign * 0.01; - var frac = Lib.roundUp(Lib.mod(x2, 1), tickset, axrev); + // log10 of 2,5,10, or all digits (logs just have to be + // close enough to round) + if (tType === "D") { + var tickset = dtick === "D2" ? roundLog2 : roundLog1; + var x2 = x + axSign * 0.01; + var frac = Lib.roundUp(Lib.mod(x2, 1), tickset, axrev); - return Math.floor(x2) + - Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10; - } + return ( + Math.floor(x2) + Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10 + ); + } - throw 'unrecognized dtick ' + String(dtick); + throw "unrecognized dtick " + String(dtick); }; // calculate the first tick on an axis -axes.tickFirst = function(ax, opts) { - var r2l = ax.r2l || Number; - var rng = Lib.simpleMap(ax.range, r2l, undefined, undefined, opts); - var axrev = rng[1] < rng[0]; - var sRound = axrev ? Math.floor : Math.ceil; - // add a tiny extra bit to make sure we get ticks - // that may have been rounded out - var r0 = expandRange(rng)[0]; - var dtick = ax.dtick; - var tick0 = r2l(ax.tick0); - - if(isNumeric(dtick)) { - var tmin = sRound((r0 - tick0) / dtick) * dtick + tick0; - - // make sure no ticks outside the category list - if(ax.type === 'category' || ax.type === 'multicategory') { - tmin = Lib.constrain(tmin, 0, ax._categories.length - 1); - } - return tmin; - } +axes.tickFirst = function (ax, opts) { + var r2l = ax.r2l || Number; + var rng = Lib.simpleMap(ax.range, r2l, undefined, undefined, opts); + var axrev = rng[1] < rng[0]; + var sRound = axrev ? Math.floor : Math.ceil; + // add a tiny extra bit to make sure we get ticks + // that may have been rounded out + var r0 = expandRange(rng)[0]; + var dtick = ax.dtick; + var tick0 = r2l(ax.tick0); + + if (isNumeric(dtick)) { + var tmin = sRound((r0 - tick0) / dtick) * dtick + tick0; + + // make sure no ticks outside the category list + if (ax.type === "category" || ax.type === "multicategory") { + tmin = Lib.constrain(tmin, 0, ax._categories.length - 1); + } + return tmin; + } + + var tType = dtick.charAt(0); + var dtNum = Number(dtick.substr(1)); + + // Dates: months (or years) + if (tType === "M") { + var cnt = 0; + var t0 = tick0; + var t1, mult, newDTick; + + // This algorithm should work for *any* nonlinear (but close to linear!) + // tick spacing. Limit to 10 iterations, for gregorian months it's normally <=3. + while (cnt < 10) { + t1 = axes.tickIncrement(t0, dtick, axrev, ax.calendar); + if ((t1 - r0) * (t0 - r0) <= 0) { + // t1 and t0 are on opposite sides of r0! we've succeeded! + if (axrev) return Math.min(t0, t1); + return Math.max(t0, t1); + } + mult = (r0 - (t0 + t1) / 2) / (t1 - t0); + newDTick = tType + (Math.abs(Math.round(mult)) || 1) * dtNum; + t0 = axes.tickIncrement( + t0, + newDTick, + mult < 0 ? !axrev : axrev, + ax.calendar + ); + cnt++; + } + Lib.error("tickFirst did not converge", ax); + return t0; + } else if (tType === "L") { + // Log scales: Linear, Digits - var tType = dtick.charAt(0); - var dtNum = Number(dtick.substr(1)); - - // Dates: months (or years) - if(tType === 'M') { - var cnt = 0; - var t0 = tick0; - var t1, mult, newDTick; - - // This algorithm should work for *any* nonlinear (but close to linear!) - // tick spacing. Limit to 10 iterations, for gregorian months it's normally <=3. - while(cnt < 10) { - t1 = axes.tickIncrement(t0, dtick, axrev, ax.calendar); - if((t1 - r0) * (t0 - r0) <= 0) { - // t1 and t0 are on opposite sides of r0! we've succeeded! - if(axrev) return Math.min(t0, t1); - return Math.max(t0, t1); - } - mult = (r0 - ((t0 + t1) / 2)) / (t1 - t0); - newDTick = tType + ((Math.abs(Math.round(mult)) || 1) * dtNum); - t0 = axes.tickIncrement(t0, newDTick, mult < 0 ? !axrev : axrev, ax.calendar); - cnt++; - } - Lib.error('tickFirst did not converge', ax); - return t0; - } else if(tType === 'L') { - // Log scales: Linear, Digits - - return Math.log(sRound( - (Math.pow(10, r0) - tick0) / dtNum) * dtNum + tick0) / Math.LN10; - } else if(tType === 'D') { - var tickset = (dtick === 'D2') ? roundLog2 : roundLog1; - var frac = Lib.roundUp(Lib.mod(r0, 1), tickset, axrev); - - return Math.floor(r0) + - Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10; - } else throw 'unrecognized dtick ' + String(dtick); + return ( + Math.log(sRound((Math.pow(10, r0) - tick0) / dtNum) * dtNum + tick0) / + Math.LN10 + ); + } else if (tType === "D") { + var tickset = dtick === "D2" ? roundLog2 : roundLog1; + var frac = Lib.roundUp(Lib.mod(r0, 1), tickset, axrev); + + return ( + Math.floor(r0) + Math.log(d3.round(Math.pow(10, frac), 1)) / Math.LN10 + ); + } else throw "unrecognized dtick " + String(dtick); }; // draw the text for one tick. @@ -1615,77 +1690,78 @@ axes.tickFirst = function(ax, opts) { // ax is the axis layout, x is the tick value // hover is a (truthy) flag for whether to show numbers with a bit // more precision for hovertext -axes.tickText = function(ax, x, hover, noSuffixPrefix) { - var out = tickTextObj(ax, x); - var arrayMode = ax.tickmode === 'array'; - var extraPrecision = hover || arrayMode; - var axType = ax.type; - // TODO multicategory, if we allow ticktext / tickvals - var tickVal2l = axType === 'category' ? ax.d2l_noadd : ax.d2l; - var i; - - if(arrayMode && Array.isArray(ax.ticktext)) { - var rng = Lib.simpleMap(ax.range, ax.r2l); - var minDiff = (Math.abs(rng[1] - rng[0]) - (ax._lBreaks || 0)) / 10000; - - for(i = 0; i < ax.ticktext.length; i++) { - if(Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break; - } - if(i < ax.ticktext.length) { - out.text = String(ax.ticktext[i]); - return out; - } - } - - function isHidden(showAttr) { - if(showAttr === undefined) return true; - if(hover) return showAttr === 'none'; - - var firstOrLast = { - first: ax._tmin, - last: ax._tmax - }[showAttr]; - - return showAttr !== 'all' && x !== firstOrLast; - } - - var hideexp = hover ? - 'never' : - ax.exponentformat !== 'none' && isHidden(ax.showexponent) ? 'hide' : ''; - - if(axType === 'date') formatDate(ax, out, hover, extraPrecision); - else if(axType === 'log') formatLog(ax, out, hover, extraPrecision, hideexp); - else if(axType === 'category') formatCategory(ax, out); - else if(axType === 'multicategory') formatMultiCategory(ax, out, hover); - else if(isAngular(ax)) formatAngle(ax, out, hover, extraPrecision, hideexp); - else formatLinear(ax, out, hover, extraPrecision, hideexp); - - // add prefix and suffix - if(!noSuffixPrefix) { - if(ax.tickprefix && !isHidden(ax.showtickprefix)) out.text = ax.tickprefix + out.text; - if(ax.ticksuffix && !isHidden(ax.showticksuffix)) out.text += ax.ticksuffix; - } - - if(ax.labelalias && ax.labelalias.hasOwnProperty(out.text)) { - var t = ax.labelalias[out.text]; - if(typeof t === 'string') out.text = t; - } - - // Setup ticks and grid lines boundaries - // at 1/2 a 'category' to the left/bottom - if(ax.tickson === 'boundaries' || ax.showdividers) { - var inbounds = function(v) { - var p = ax.l2p(v); - return p >= 0 && p <= ax._length ? v : null; - }; +axes.tickText = function (ax, x, hover, noSuffixPrefix) { + var out = tickTextObj(ax, x); + var arrayMode = ax.tickmode === "array"; + var extraPrecision = hover || arrayMode; + var axType = ax.type; + // TODO multicategory, if we allow ticktext / tickvals + var tickVal2l = axType === "category" ? ax.d2l_noadd : ax.d2l; + var i; + + if (arrayMode && Array.isArray(ax.ticktext)) { + var rng = Lib.simpleMap(ax.range, ax.r2l); + var minDiff = (Math.abs(rng[1] - rng[0]) - (ax._lBreaks || 0)) / 10000; + + for (i = 0; i < ax.ticktext.length; i++) { + if (Math.abs(x - tickVal2l(ax.tickvals[i])) < minDiff) break; + } + if (i < ax.ticktext.length) { + out.text = String(ax.ticktext[i]); + return out; + } + } + + function isHidden(showAttr) { + if (showAttr === undefined) return true; + if (hover) return showAttr === "none"; + + var firstOrLast = { + first: ax._tmin, + last: ax._tmax, + }[showAttr]; + + return showAttr !== "all" && x !== firstOrLast; + } + + var hideexp = hover + ? "never" + : ax.exponentformat !== "none" && isHidden(ax.showexponent) + ? "hide" + : ""; + + if (axType === "date") formatDate(ax, out, hover, extraPrecision); + else if (axType === "log") formatLog(ax, out, hover, extraPrecision, hideexp); + else if (axType === "category") formatCategory(ax, out); + else if (axType === "multicategory") formatMultiCategory(ax, out, hover); + else if (isAngular(ax)) formatAngle(ax, out, hover, extraPrecision, hideexp); + else formatLinear(ax, out, hover, extraPrecision, hideexp); + + // add prefix and suffix + if (!noSuffixPrefix) { + if (ax.tickprefix && !isHidden(ax.showtickprefix)) + out.text = ax.tickprefix + out.text; + if (ax.ticksuffix && !isHidden(ax.showticksuffix)) + out.text += ax.ticksuffix; + } + + if (ax.labelalias && ax.labelalias.hasOwnProperty(out.text)) { + var t = ax.labelalias[out.text]; + if (typeof t === "string") out.text = t; + } + + // Setup ticks and grid lines boundaries + // at 1/2 a 'category' to the left/bottom + if (ax.tickson === "boundaries" || ax.showdividers) { + var inbounds = function (v) { + var p = ax.l2p(v); + return p >= 0 && p <= ax._length ? v : null; + }; - out.xbnd = [ - inbounds(out.x - 0.5), - inbounds(out.x + ax.dtick - 0.5) - ]; - } + out.xbnd = [inbounds(out.x - 0.5), inbounds(out.x + ax.dtick - 0.5)]; + } - return out; + return out; }; /** @@ -1700,291 +1776,325 @@ axes.tickText = function(ax, x, hover, noSuffixPrefix) { * first value and second value as a range (ie ' - ') if the second value is provided and * it's different from the first value. */ -axes.hoverLabelText = function(ax, values, hoverformat) { - if(hoverformat) ax = Lib.extendFlat({}, ax, {hoverformat: hoverformat}); - - var val = Array.isArray(values) ? values[0] : values; - var val2 = Array.isArray(values) ? values[1] : undefined; - if(val2 !== undefined && val2 !== val) { - return ( - axes.hoverLabelText(ax, val, hoverformat) + ' - ' + - axes.hoverLabelText(ax, val2, hoverformat) - ); - } +axes.hoverLabelText = function (ax, values, hoverformat) { + if (hoverformat) ax = Lib.extendFlat({}, ax, { hoverformat: hoverformat }); + + var val = Array.isArray(values) ? values[0] : values; + var val2 = Array.isArray(values) ? values[1] : undefined; + if (val2 !== undefined && val2 !== val) { + return ( + axes.hoverLabelText(ax, val, hoverformat) + + " - " + + axes.hoverLabelText(ax, val2, hoverformat) + ); + } - var logOffScale = (ax.type === 'log' && val <= 0); - var tx = axes.tickText(ax, ax.c2l(logOffScale ? -val : val), 'hover').text; + var logOffScale = ax.type === "log" && val <= 0; + var tx = axes.tickText(ax, ax.c2l(logOffScale ? -val : val), "hover").text; - if(logOffScale) { - return val === 0 ? '0' : MINUS_SIGN + tx; - } + if (logOffScale) { + return val === 0 ? "0" : MINUS_SIGN + tx; + } - // TODO: should we do something special if the axis calendar and - // the data calendar are different? Somehow display both dates with - // their system names? Right now it will just display in the axis calendar - // but users could add the other one as text. - return tx; + // TODO: should we do something special if the axis calendar and + // the data calendar are different? Somehow display both dates with + // their system names? Right now it will just display in the axis calendar + // but users could add the other one as text. + return tx; }; function tickTextObj(ax, x, text) { - var tf = ax.tickfont || {}; - - return { - x: x, - dx: 0, - dy: 0, - text: text || '', - fontSize: tf.size, - font: tf.family, - fontColor: tf.color - }; + var tf = ax.tickfont || {}; + + return { + x: x, + dx: 0, + dy: 0, + text: text || "", + fontSize: tf.size, + font: tf.family, + fontColor: tf.color, + }; } function formatDate(ax, out, hover, extraPrecision) { - var tr = ax._tickround; - var fmt = (hover && ax.hoverformat) || axes.getTickFormat(ax); - - // Only apply extra precision if no explicit format was provided. - extraPrecision = !fmt && extraPrecision; - - if(extraPrecision) { - // second or sub-second precision: extra always shows max digits. - // for other fields, extra precision just adds one field. - if(isNumeric(tr)) tr = 4; - else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 4}[tr]; - } - - var dateStr = Lib.formatDate(out.x, fmt, tr, ax._dateFormat, ax.calendar, ax._extraFormat); - var headStr; - - var splitIndex = dateStr.indexOf('\n'); - if(splitIndex !== -1) { - headStr = dateStr.substr(splitIndex + 1); - dateStr = dateStr.substr(0, splitIndex); - } - - if(extraPrecision) { - // if extraPrecision led to trailing zeros, strip them off - // actually, this can lead to removing even more zeros than - // in the original rounding, but that's fine because in these - // contexts uniformity is not so important (if there's even - // anything to be uniform with!) - - // can we remove the whole time part? - if(headStr !== undefined && (dateStr === '00:00:00' || dateStr === '00:00')) { - dateStr = headStr; - headStr = ''; - } else if(dateStr.length === 8) { - // strip off seconds if they're zero (zero fractional seconds - // are already omitted) - // but we never remove minutes and leave just hours - dateStr = dateStr.replace(/:00$/, ''); - } - } - - if(headStr) { - if(hover) { - // hover puts it all on one line, so headPart works best up front - // except for year headPart: turn this into "Jan 1, 2000" etc. - if(tr === 'd') dateStr += ', ' + headStr; - else dateStr = headStr + (dateStr ? ', ' + dateStr : ''); - } else { - if( - !ax._inCalcTicks || - ax._prevDateHead !== headStr - ) { - ax._prevDateHead = headStr; - dateStr += '
' + headStr; - } else { - var isInside = insideTicklabelposition(ax); - var side = ax._trueSide || ax.side; // polar mocks the side of the radial axis - if( - (!isInside && side === 'top') || - (isInside && side === 'bottom') - ) { - dateStr += '
'; - } - } + var tr = ax._tickround; + var fmt = (hover && ax.hoverformat) || axes.getTickFormat(ax); + + // Only apply extra precision if no explicit format was provided. + extraPrecision = !fmt && extraPrecision; + + if (extraPrecision) { + // second or sub-second precision: extra always shows max digits. + // for other fields, extra precision just adds one field. + if (isNumeric(tr)) tr = 4; + else tr = { y: "m", m: "d", d: "M", M: "S", S: 4 }[tr]; + } + + var dateStr = Lib.formatDate( + out.x, + fmt, + tr, + ax._dateFormat, + ax.calendar, + ax._extraFormat + ); + var headStr; + + var splitIndex = dateStr.indexOf("\n"); + if (splitIndex !== -1) { + headStr = dateStr.substr(splitIndex + 1); + dateStr = dateStr.substr(0, splitIndex); + } + + if (extraPrecision) { + // if extraPrecision led to trailing zeros, strip them off + // actually, this can lead to removing even more zeros than + // in the original rounding, but that's fine because in these + // contexts uniformity is not so important (if there's even + // anything to be uniform with!) + + // can we remove the whole time part? + if ( + headStr !== undefined && + (dateStr === "00:00:00" || dateStr === "00:00") + ) { + dateStr = headStr; + headStr = ""; + } else if (dateStr.length === 8) { + // strip off seconds if they're zero (zero fractional seconds + // are already omitted) + // but we never remove minutes and leave just hours + dateStr = dateStr.replace(/:00$/, ""); + } + } + + if (headStr) { + if (hover) { + // hover puts it all on one line, so headPart works best up front + // except for year headPart: turn this into "Jan 1, 2000" etc. + if (tr === "d") dateStr += ", " + headStr; + else dateStr = headStr + (dateStr ? ", " + dateStr : ""); + } else { + if (!ax._inCalcTicks || ax._prevDateHead !== headStr) { + ax._prevDateHead = headStr; + dateStr += "
" + headStr; + } else { + var isInside = insideTicklabelposition(ax); + var side = ax._trueSide || ax.side; // polar mocks the side of the radial axis + if ((!isInside && side === "top") || (isInside && side === "bottom")) { + dateStr += "
"; } + } } + } - out.text = dateStr; + out.text = dateStr; } function formatLog(ax, out, hover, extraPrecision, hideexp) { - var dtick = ax.dtick; - var x = out.x; - var tickformat = ax.tickformat; - var dtChar0 = typeof dtick === 'string' && dtick.charAt(0); - - if(hideexp === 'never') { - // If this is a hover label, then we must *never* hide the exponent - // for the sake of display, which could give the wrong value by - // potentially many orders of magnitude. If hideexp was 'never', then - // it's now succeeded by preventing the other condition from automating - // this choice. Thus we can unset it so that the axis formatting takes - // precedence. - hideexp = ''; - } - - if(extraPrecision && (dtChar0 !== 'L')) { - dtick = 'L3'; - dtChar0 = 'L'; - } + var dtick = ax.dtick; + var x = out.x; + var tickformat = ax.tickformat; + var dtChar0 = typeof dtick === "string" && dtick.charAt(0); + + if (hideexp === "never") { + // If this is a hover label, then we must *never* hide the exponent + // for the sake of display, which could give the wrong value by + // potentially many orders of magnitude. If hideexp was 'never', then + // it's now succeeded by preventing the other condition from automating + // this choice. Thus we can unset it so that the axis formatting takes + // precedence. + hideexp = ""; + } + + if (extraPrecision && dtChar0 !== "L") { + dtick = "L3"; + dtChar0 = "L"; + } + + if (tickformat || dtChar0 === "L") { + out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision); + } else if ( + isNumeric(dtick) || + (dtChar0 === "D" && Lib.mod(x + 0.01, 1) < 0.1) + ) { + var p = Math.round(x); + var absP = Math.abs(p); + var exponentFormat = ax.exponentformat; + if ( + exponentFormat === "power" || + (isSIFormat(exponentFormat) && beyondSI(p)) + ) { + if (p === 0) out.text = 1; + else if (p === 1) out.text = "10"; + else out.text = "10" + (p > 1 ? "" : MINUS_SIGN) + absP + ""; - if(tickformat || (dtChar0 === 'L')) { - out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision); - } else if(isNumeric(dtick) || ((dtChar0 === 'D') && (Lib.mod(x + 0.01, 1) < 0.1))) { - var p = Math.round(x); - var absP = Math.abs(p); - var exponentFormat = ax.exponentformat; - if(exponentFormat === 'power' || (isSIFormat(exponentFormat) && beyondSI(p))) { - if(p === 0) out.text = 1; - else if(p === 1) out.text = '10'; - else out.text = '10' + (p > 1 ? '' : MINUS_SIGN) + absP + ''; - - out.fontSize *= 1.25; - } else if((exponentFormat === 'e' || exponentFormat === 'E') && absP > 2) { - out.text = '1' + exponentFormat + (p > 0 ? '+' : MINUS_SIGN) + absP; - } else { - out.text = numFormat(Math.pow(10, x), ax, '', 'fakehover'); - if(dtick === 'D1' && ax._id.charAt(0) === 'y') { - out.dy -= out.fontSize / 6; - } - } - } else if(dtChar0 === 'D') { - out.text = String(Math.round(Math.pow(10, Lib.mod(x, 1)))); - out.fontSize *= 0.75; - } else throw 'unrecognized dtick ' + String(dtick); - - // if 9's are printed on log scale, move the 10's away a bit - if(ax.dtick === 'D1') { - var firstChar = String(out.text).charAt(0); - if(firstChar === '0' || firstChar === '1') { - if(ax._id.charAt(0) === 'y') { - out.dx -= out.fontSize / 4; - } else { - out.dy += out.fontSize / 2; - out.dx += (ax.range[1] > ax.range[0] ? 1 : -1) * - out.fontSize * (x < 0 ? 0.5 : 0.25); - } - } - } + out.fontSize *= 1.25; + } else if ((exponentFormat === "e" || exponentFormat === "E") && absP > 2) { + out.text = "1" + exponentFormat + (p > 0 ? "+" : MINUS_SIGN) + absP; + } else { + out.text = numFormat(Math.pow(10, x), ax, "", "fakehover"); + if (dtick === "D1" && ax._id.charAt(0) === "y") { + out.dy -= out.fontSize / 6; + } + } + } else if (dtChar0 === "D") { + out.text = String(Math.round(Math.pow(10, Lib.mod(x, 1)))); + out.fontSize *= 0.75; + } else throw "unrecognized dtick " + String(dtick); + + // if 9's are printed on log scale, move the 10's away a bit + if (ax.dtick === "D1") { + var firstChar = String(out.text).charAt(0); + if (firstChar === "0" || firstChar === "1") { + if (ax._id.charAt(0) === "y") { + out.dx -= out.fontSize / 4; + } else { + out.dy += out.fontSize / 2; + out.dx += + (ax.range[1] > ax.range[0] ? 1 : -1) * + out.fontSize * + (x < 0 ? 0.5 : 0.25); + } + } + } } function formatCategory(ax, out) { - var tt = ax._categories[Math.round(out.x)]; - if(tt === undefined) tt = ''; - out.text = String(tt); + var tt = ax._categories[Math.round(out.x)]; + if (tt === undefined) tt = ""; + out.text = String(tt); } function formatMultiCategory(ax, out, hover) { - var v = Math.round(out.x); - var cats = ax._categories[v] || []; - var tt = cats[1] === undefined ? '' : String(cats[1]); - var tt2 = cats[0] === undefined ? '' : String(cats[0]); - - if(hover) { - // TODO is this what we want? - out.text = tt2 + ' - ' + tt; - } else { - // setup for secondary labels - out.text = tt; - out.text2 = tt2; - } + var v = Math.round(out.x); + var cats = + ax._categories[v].map(function (cat) { + return cat; + }) || []; + var texts = cats + .slice() + .reverse() + .map(function (cat) { + return cat === undefined ? "" : String(cat); + }); + + if (hover) { + // TODO is this what we want? + var hoverText = ""; + cats.forEach(function (text, index) { + text = String(text); + if (index < texts.length - 1) { + hoverText = hoverText + " " + text + " - "; + } else { + hoverText = hoverText + " " + text; + } + }); + + out.text = hoverText; + } else { + // setup for secondary labels + out.text = texts[0]; + out.texts = texts; + } } function formatLinear(ax, out, hover, extraPrecision, hideexp) { - if(hideexp === 'never') { - // If this is a hover label, then we must *never* hide the exponent - // for the sake of display, which could give the wrong value by - // potentially many orders of magnitude. If hideexp was 'never', then - // it's now succeeded by preventing the other condition from automating - // this choice. Thus we can unset it so that the axis formatting takes - // precedence. - hideexp = ''; - } else if(ax.showexponent === 'all' && Math.abs(out.x / ax.dtick) < 1e-6) { - // don't add an exponent to zero if we're showing all exponents - // so the only reason you'd show an exponent on zero is if it's the - // ONLY tick to get an exponent (first or last) - hideexp = 'hide'; - } - out.text = numFormat(out.x, ax, hideexp, extraPrecision); + if (hideexp === "never") { + // If this is a hover label, then we must *never* hide the exponent + // for the sake of display, which could give the wrong value by + // potentially many orders of magnitude. If hideexp was 'never', then + // it's now succeeded by preventing the other condition from automating + // this choice. Thus we can unset it so that the axis formatting takes + // precedence. + hideexp = ""; + } else if (ax.showexponent === "all" && Math.abs(out.x / ax.dtick) < 1e-6) { + // don't add an exponent to zero if we're showing all exponents + // so the only reason you'd show an exponent on zero is if it's the + // ONLY tick to get an exponent (first or last) + hideexp = "hide"; + } + out.text = numFormat(out.x, ax, hideexp, extraPrecision); } function formatAngle(ax, out, hover, extraPrecision, hideexp) { - if(ax.thetaunit === 'radians' && !hover) { - var num = out.x / 180; + if (ax.thetaunit === "radians" && !hover) { + var num = out.x / 180; - if(num === 0) { - out.text = '0'; + if (num === 0) { + out.text = "0"; + } else { + var frac = num2frac(num); + + if (frac[1] >= 100) { + out.text = numFormat(Lib.deg2rad(out.x), ax, hideexp, extraPrecision); + } else { + var isNeg = out.x < 0; + + if (frac[1] === 1) { + if (frac[0] === 1) out.text = "π"; + else out.text = frac[0] + "π"; } else { - var frac = num2frac(num); - - if(frac[1] >= 100) { - out.text = numFormat(Lib.deg2rad(out.x), ax, hideexp, extraPrecision); - } else { - var isNeg = out.x < 0; - - if(frac[1] === 1) { - if(frac[0] === 1) out.text = 'π'; - else out.text = frac[0] + 'π'; - } else { - out.text = [ - '', frac[0], '', - '⁄', - '', frac[1], '', - 'π' - ].join(''); - } - - if(isNeg) out.text = MINUS_SIGN + out.text; - } + out.text = [ + "", + frac[0], + "", + "⁄", + "", + frac[1], + "", + "π", + ].join(""); } - } else { - out.text = numFormat(out.x, ax, hideexp, extraPrecision); + + if (isNeg) out.text = MINUS_SIGN + out.text; + } } + } else { + out.text = numFormat(out.x, ax, hideexp, extraPrecision); + } } // inspired by // https://github.com/yisibl/num2fraction/blob/master/index.js function num2frac(num) { - function almostEq(a, b) { - return Math.abs(a - b) <= 1e-6; - } - - function findGCD(a, b) { - return almostEq(b, 0) ? a : findGCD(b, a % b); - } - - function findPrecision(n) { - var e = 1; - while(!almostEq(Math.round(n * e) / e, n)) { - e *= 10; - } - return e; - } - - var precision = findPrecision(num); - var number = num * precision; - var gcd = Math.abs(findGCD(number, precision)); - - return [ - // numerator - Math.round(number / gcd), - // denominator - Math.round(precision / gcd) - ]; + function almostEq(a, b) { + return Math.abs(a - b) <= 1e-6; + } + + function findGCD(a, b) { + return almostEq(b, 0) ? a : findGCD(b, a % b); + } + + function findPrecision(n) { + var e = 1; + while (!almostEq(Math.round(n * e) / e, n)) { + e *= 10; + } + return e; + } + + var precision = findPrecision(num); + var number = num * precision; + var gcd = Math.abs(findGCD(number, precision)); + + return [ + // numerator + Math.round(number / gcd), + // denominator + Math.round(precision / gcd), + ]; } // format a number (tick value) according to the axis settings // new, more reliable procedure than d3.round or similar: // add half the rounding increment, then stringify and truncate // also automatically switch to sci. notation -var SIPREFIXES = ['f', 'p', 'n', 'μ', 'm', '', 'k', 'M', 'G', 'T']; +var SIPREFIXES = ["f", "p", "n", "μ", "m", "", "k", "M", "G", "T"]; function isSIFormat(exponentFormat) { - return exponentFormat === 'SI' || exponentFormat === 'B'; + return exponentFormat === "SI" || exponentFormat === "B"; } // are we beyond the range of common SI prefixes? @@ -1995,179 +2105,203 @@ function isSIFormat(exponentFormat) { // 10^15 -> 1x10^15 // 10^16 -> 1x10^16 function beyondSI(exponent) { - return exponent > 14 || exponent < -15; + return exponent > 14 || exponent < -15; } function numFormat(v, ax, fmtoverride, hover) { - var isNeg = v < 0; - // max number of digits past decimal point to show - var tickRound = ax._tickround; - var exponentFormat = fmtoverride || ax.exponentformat || 'B'; - var exponent = ax._tickexponent; - var tickformat = axes.getTickFormat(ax); - var separatethousands = ax.separatethousands; - - // special case for hover: set exponent just for this value, and - // add a couple more digits of precision over tick labels - if(hover) { - // make a dummy axis obj to get the auto rounding and exponent - var ah = { - exponentformat: exponentFormat, - minexponent: ax.minexponent, - dtick: ax.showexponent === 'none' ? ax.dtick : - (isNumeric(v) ? Math.abs(v) || 1 : 1), - // if not showing any exponents, don't change the exponent - // from what we calculate - range: ax.showexponent === 'none' ? ax.range.map(ax.r2d) : [0, v || 1] - }; - autoTickRound(ah); - tickRound = (Number(ah._tickround) || 0) + 4; - exponent = ah._tickexponent; - if(ax.hoverformat) tickformat = ax.hoverformat; - } - - if(tickformat) return ax._numFormat(tickformat)(v).replace(/-/g, MINUS_SIGN); - - // 'epsilon' - rounding increment - var e = Math.pow(10, -tickRound) / 2; - - // exponentFormat codes: - // 'e' (1.2e+6, default) - // 'E' (1.2E+6) - // 'SI' (1.2M) - // 'B' (same as SI except 10^9=B not G) - // 'none' (1200000) - // 'power' (1.2x10^6) - // 'hide' (1.2, use 3rd argument=='hide' to eg - // only show exponent on last tick) - if(exponentFormat === 'none') exponent = 0; - - // take the sign out, put it back manually at the end - // - makes cases easier - v = Math.abs(v); - if(v < e) { - // 0 is just 0, but may get exponent if it's the last tick - v = '0'; - isNeg = false; + var isNeg = v < 0; + // max number of digits past decimal point to show + var tickRound = ax._tickround; + var exponentFormat = fmtoverride || ax.exponentformat || "B"; + var exponent = ax._tickexponent; + var tickformat = axes.getTickFormat(ax); + var separatethousands = ax.separatethousands; + + // special case for hover: set exponent just for this value, and + // add a couple more digits of precision over tick labels + if (hover) { + // make a dummy axis obj to get the auto rounding and exponent + var ah = { + exponentformat: exponentFormat, + minexponent: ax.minexponent, + dtick: + ax.showexponent === "none" + ? ax.dtick + : isNumeric(v) + ? Math.abs(v) || 1 + : 1, + // if not showing any exponents, don't change the exponent + // from what we calculate + range: ax.showexponent === "none" ? ax.range.map(ax.r2d) : [0, v || 1], + }; + autoTickRound(ah); + tickRound = (Number(ah._tickround) || 0) + 4; + exponent = ah._tickexponent; + if (ax.hoverformat) tickformat = ax.hoverformat; + } + + if (tickformat) return ax._numFormat(tickformat)(v).replace(/-/g, MINUS_SIGN); + + // 'epsilon' - rounding increment + var e = Math.pow(10, -tickRound) / 2; + + // exponentFormat codes: + // 'e' (1.2e+6, default) + // 'E' (1.2E+6) + // 'SI' (1.2M) + // 'B' (same as SI except 10^9=B not G) + // 'none' (1200000) + // 'power' (1.2x10^6) + // 'hide' (1.2, use 3rd argument=='hide' to eg + // only show exponent on last tick) + if (exponentFormat === "none") exponent = 0; + + // take the sign out, put it back manually at the end + // - makes cases easier + v = Math.abs(v); + if (v < e) { + // 0 is just 0, but may get exponent if it's the last tick + v = "0"; + isNeg = false; + } else { + v += e; + // take out a common exponent, if any + if (exponent) { + v *= Math.pow(10, -exponent); + tickRound += exponent; + } + // round the mantissa + if (tickRound === 0) v = String(Math.floor(v)); + else if (tickRound < 0) { + v = String(Math.round(v)); + v = v.substr(0, v.length + tickRound); + for (var i = tickRound; i < 0; i++) v += "0"; } else { - v += e; - // take out a common exponent, if any - if(exponent) { - v *= Math.pow(10, -exponent); - tickRound += exponent; - } - // round the mantissa - if(tickRound === 0) v = String(Math.floor(v)); - else if(tickRound < 0) { - v = String(Math.round(v)); - v = v.substr(0, v.length + tickRound); - for(var i = tickRound; i < 0; i++) v += '0'; - } else { - v = String(v); - var dp = v.indexOf('.') + 1; - if(dp) v = v.substr(0, dp + tickRound).replace(/\.?0+$/, ''); - } - // insert appropriate decimal point and thousands separator - v = Lib.numSeparate(v, ax._separators, separatethousands); - } - - // add exponent - if(exponent && exponentFormat !== 'hide') { - if(isSIFormat(exponentFormat) && beyondSI(exponent)) exponentFormat = 'power'; - - var signedExponent; - if(exponent < 0) signedExponent = MINUS_SIGN + -exponent; - else if(exponentFormat !== 'power') signedExponent = '+' + exponent; - else signedExponent = String(exponent); - - if(exponentFormat === 'e' || exponentFormat === 'E') { - v += exponentFormat + signedExponent; - } else if(exponentFormat === 'power') { - v += '×10' + signedExponent + ''; - } else if(exponentFormat === 'B' && exponent === 9) { - v += 'B'; - } else if(isSIFormat(exponentFormat)) { - v += SIPREFIXES[exponent / 3 + 5]; - } - } - - // put sign back in and return - // replace standard minus character (which is technically a hyphen) - // with a true minus sign - if(isNeg) return MINUS_SIGN + v; - return v; + v = String(v); + var dp = v.indexOf(".") + 1; + if (dp) v = v.substr(0, dp + tickRound).replace(/\.?0+$/, ""); + } + // insert appropriate decimal point and thousands separator + v = Lib.numSeparate(v, ax._separators, separatethousands); + } + + // add exponent + if (exponent && exponentFormat !== "hide") { + if (isSIFormat(exponentFormat) && beyondSI(exponent)) + exponentFormat = "power"; + + var signedExponent; + if (exponent < 0) signedExponent = MINUS_SIGN + -exponent; + else if (exponentFormat !== "power") signedExponent = "+" + exponent; + else signedExponent = String(exponent); + + if (exponentFormat === "e" || exponentFormat === "E") { + v += exponentFormat + signedExponent; + } else if (exponentFormat === "power") { + v += "×10" + signedExponent + ""; + } else if (exponentFormat === "B" && exponent === 9) { + v += "B"; + } else if (isSIFormat(exponentFormat)) { + v += SIPREFIXES[exponent / 3 + 5]; + } + } + + // put sign back in and return + // replace standard minus character (which is technically a hyphen) + // with a true minus sign + if (isNeg) return MINUS_SIGN + v; + return v; } -axes.getTickFormat = function(ax) { - var i; - - function convertToMs(dtick) { - return typeof dtick !== 'string' ? dtick : Number(dtick.replace('M', '')) * ONEAVGMONTH; - } - - function compareLogTicks(left, right) { - var priority = ['L', 'D']; - if(typeof left === typeof right) { - if(typeof left === 'number') { - return left - right; - } else { - var leftPriority = priority.indexOf(left.charAt(0)); - var rightPriority = priority.indexOf(right.charAt(0)); - if(leftPriority === rightPriority) { - return Number(left.replace(/(L|D)/g, '')) - Number(right.replace(/(L|D)/g, '')); - } else { - return leftPriority - rightPriority; - } - } +axes.getTickFormat = function (ax) { + var i; + + function convertToMs(dtick) { + return typeof dtick !== "string" + ? dtick + : Number(dtick.replace("M", "")) * ONEAVGMONTH; + } + + function compareLogTicks(left, right) { + var priority = ["L", "D"]; + if (typeof left === typeof right) { + if (typeof left === "number") { + return left - right; + } else { + var leftPriority = priority.indexOf(left.charAt(0)); + var rightPriority = priority.indexOf(right.charAt(0)); + if (leftPriority === rightPriority) { + return ( + Number(left.replace(/(L|D)/g, "")) - + Number(right.replace(/(L|D)/g, "")) + ); } else { - return typeof left === 'number' ? 1 : -1; + return leftPriority - rightPriority; } - } - - function isProperStop(dtick, range, convert) { - var convertFn = convert || function(x) { return x;}; - var leftDtick = range[0]; - var rightDtick = range[1]; - return ((!leftDtick && typeof leftDtick !== 'number') || convertFn(leftDtick) <= convertFn(dtick)) && - ((!rightDtick && typeof rightDtick !== 'number') || convertFn(rightDtick) >= convertFn(dtick)); - } - - function isProperLogStop(dtick, range) { - var isLeftDtickNull = range[0] === null; - var isRightDtickNull = range[1] === null; - var isDtickInRangeLeft = compareLogTicks(dtick, range[0]) >= 0; - var isDtickInRangeRight = compareLogTicks(dtick, range[1]) <= 0; - return (isLeftDtickNull || isDtickInRangeLeft) && (isRightDtickNull || isDtickInRangeRight); - } + } + } else { + return typeof left === "number" ? 1 : -1; + } + } + + function isProperStop(dtick, range, convert) { + var convertFn = + convert || + function (x) { + return x; + }; + var leftDtick = range[0]; + var rightDtick = range[1]; + return ( + ((!leftDtick && typeof leftDtick !== "number") || + convertFn(leftDtick) <= convertFn(dtick)) && + ((!rightDtick && typeof rightDtick !== "number") || + convertFn(rightDtick) >= convertFn(dtick)) + ); + } - var tickstop, stopi; - if(ax.tickformatstops && ax.tickformatstops.length > 0) { - switch(ax.type) { - case 'date': - case 'linear': { - for(i = 0; i < ax.tickformatstops.length; i++) { - stopi = ax.tickformatstops[i]; - if(stopi.enabled && isProperStop(ax.dtick, stopi.dtickrange, convertToMs)) { - tickstop = stopi; - break; - } - } - break; - } - case 'log': { - for(i = 0; i < ax.tickformatstops.length; i++) { - stopi = ax.tickformatstops[i]; - if(stopi.enabled && isProperLogStop(ax.dtick, stopi.dtickrange)) { - tickstop = stopi; - break; - } - } - break; - } - default: + function isProperLogStop(dtick, range) { + var isLeftDtickNull = range[0] === null; + var isRightDtickNull = range[1] === null; + var isDtickInRangeLeft = compareLogTicks(dtick, range[0]) >= 0; + var isDtickInRangeRight = compareLogTicks(dtick, range[1]) <= 0; + return ( + (isLeftDtickNull || isDtickInRangeLeft) && + (isRightDtickNull || isDtickInRangeRight) + ); + } + + var tickstop, stopi; + if (ax.tickformatstops && ax.tickformatstops.length > 0) { + switch (ax.type) { + case "date": + case "linear": { + for (i = 0; i < ax.tickformatstops.length; i++) { + stopi = ax.tickformatstops[i]; + if ( + stopi.enabled && + isProperStop(ax.dtick, stopi.dtickrange, convertToMs) + ) { + tickstop = stopi; + break; + } } + break; + } + case "log": { + for (i = 0; i < ax.tickformatstops.length; i++) { + stopi = ax.tickformatstops[i]; + if (stopi.enabled && isProperLogStop(ax.dtick, stopi.dtickrange)) { + tickstop = stopi; + break; + } + } + break; + } + default: } - return tickstop ? tickstop.value : ax.tickformat; + } + return tickstop ? tickstop.value : ax.tickformat; }; // getSubplots - extract all subplot IDs we need @@ -2177,83 +2311,92 @@ axes.getTickFormat = function(ax) { // // NOTE: this is currently only used OUTSIDE plotly.js (toolpanel, webapp) // ideally we get rid of it there (or just copy this there) and remove it here -axes.getSubplots = function(gd, ax) { - var subplotObj = gd._fullLayout._subplots; - var allSubplots = subplotObj.cartesian.concat(subplotObj.gl2d || []); +axes.getSubplots = function (gd, ax) { + var subplotObj = gd._fullLayout._subplots; + var allSubplots = subplotObj.cartesian.concat(subplotObj.gl2d || []); - var out = ax ? axes.findSubplotsWithAxis(allSubplots, ax) : allSubplots; + var out = ax ? axes.findSubplotsWithAxis(allSubplots, ax) : allSubplots; - out.sort(function(a, b) { - var aParts = a.substr(1).split('y'); - var bParts = b.substr(1).split('y'); + out.sort(function (a, b) { + var aParts = a.substr(1).split("y"); + var bParts = b.substr(1).split("y"); - if(aParts[0] === bParts[0]) return +aParts[1] - +bParts[1]; - return +aParts[0] - +bParts[0]; - }); + if (aParts[0] === bParts[0]) return +aParts[1] - +bParts[1]; + return +aParts[0] - +bParts[0]; + }); - return out; + return out; }; // find all subplots with axis 'ax' // NOTE: this is only used in axes.getSubplots (only used outside plotly.js) and // gl2d/convert (where it restricts axis subplots to only those with gl2d) -axes.findSubplotsWithAxis = function(subplots, ax) { - var axMatch = new RegExp( - (ax._id.charAt(0) === 'x') ? ('^' + ax._id + 'y') : (ax._id + '$') - ); - var subplotsWithAx = []; - - for(var i = 0; i < subplots.length; i++) { - var sp = subplots[i]; - if(axMatch.test(sp)) subplotsWithAx.push(sp); - } - - return subplotsWithAx; +axes.findSubplotsWithAxis = function (subplots, ax) { + var axMatch = new RegExp( + ax._id.charAt(0) === "x" ? "^" + ax._id + "y" : ax._id + "$" + ); + var subplotsWithAx = []; + + for (var i = 0; i < subplots.length; i++) { + var sp = subplots[i]; + if (axMatch.test(sp)) subplotsWithAx.push(sp); + } + + return subplotsWithAx; }; // makeClipPaths: prepare clipPaths for all single axes and all possible xy pairings -axes.makeClipPaths = function(gd) { - var fullLayout = gd._fullLayout; - - // for more info: https://github.com/plotly/plotly.js/issues/2595 - if(fullLayout._hasOnlyLargeSploms) return; - - var fullWidth = {_offset: 0, _length: fullLayout.width, _id: ''}; - var fullHeight = {_offset: 0, _length: fullLayout.height, _id: ''}; - var xaList = axes.list(gd, 'x', true); - var yaList = axes.list(gd, 'y', true); - var clipList = []; - var i, j; - - for(i = 0; i < xaList.length; i++) { - clipList.push({x: xaList[i], y: fullHeight}); - for(j = 0; j < yaList.length; j++) { - if(i === 0) clipList.push({x: fullWidth, y: yaList[j]}); - clipList.push({x: xaList[i], y: yaList[j]}); - } - } - - // selectors don't work right with camelCase tags, - // have to use class instead - // https://groups.google.com/forum/#!topic/d3-js/6EpAzQ2gU9I - var axClips = fullLayout._clips.selectAll('.axesclip') - .data(clipList, function(d) { return d.x._id + d.y._id; }); - - axClips.enter().append('clipPath') - .classed('axesclip', true) - .attr('id', function(d) { return 'clip' + fullLayout._uid + d.x._id + d.y._id; }) - .append('rect'); - - axClips.exit().remove(); - - axClips.each(function(d) { - d3.select(this).select('rect').attr({ - x: d.x._offset || 0, - y: d.y._offset || 0, - width: d.x._length || 1, - height: d.y._length || 1 - }); +axes.makeClipPaths = function (gd) { + var fullLayout = gd._fullLayout; + + // for more info: https://github.com/plotly/plotly.js/issues/2595 + if (fullLayout._hasOnlyLargeSploms) return; + + var fullWidth = { _offset: 0, _length: fullLayout.width, _id: "" }; + var fullHeight = { _offset: 0, _length: fullLayout.height, _id: "" }; + var xaList = axes.list(gd, "x", true); + var yaList = axes.list(gd, "y", true); + var clipList = []; + var i, j; + + for (i = 0; i < xaList.length; i++) { + clipList.push({ x: xaList[i], y: fullHeight }); + for (j = 0; j < yaList.length; j++) { + if (i === 0) clipList.push({ x: fullWidth, y: yaList[j] }); + clipList.push({ x: xaList[i], y: yaList[j] }); + } + } + + // selectors don't work right with camelCase tags, + // have to use class instead + // https://groups.google.com/forum/#!topic/d3-js/6EpAzQ2gU9I + var axClips = fullLayout._clips + .selectAll(".axesclip") + .data(clipList, function (d) { + return d.x._id + d.y._id; }); + + axClips + .enter() + .append("clipPath") + .classed("axesclip", true) + .attr("id", function (d) { + return "clip" + fullLayout._uid + d.x._id + d.y._id; + }) + .append("rect"); + + axClips.exit().remove(); + + axClips.each(function (d) { + d3.select(this) + .select("rect") + .attr({ + x: d.x._offset || 0, + y: d.y._offset || 0, + width: d.x._length || 1, + height: d.y._length || 1, + }); + }); }; /** @@ -2279,80 +2422,88 @@ axes.makeClipPaths = function(gd) { * - ax._r (stored range for use by zoom/pan) * - ax._rl (stored linearized range for use by zoom/pan) */ -axes.draw = function(gd, arg, opts) { - var fullLayout = gd._fullLayout; - - if(arg === 'redraw') { - fullLayout._paper.selectAll('g.subplot').each(function(d) { - var id = d[0]; - var plotinfo = fullLayout._plots[id]; - if(plotinfo) { - var xa = plotinfo.xaxis; - var ya = plotinfo.yaxis; - - plotinfo.xaxislayer.selectAll('.' + xa._id + 'tick').remove(); - plotinfo.yaxislayer.selectAll('.' + ya._id + 'tick').remove(); - plotinfo.xaxislayer.selectAll('.' + xa._id + 'tick2').remove(); - plotinfo.yaxislayer.selectAll('.' + ya._id + 'tick2').remove(); - plotinfo.xaxislayer.selectAll('.' + xa._id + 'divider').remove(); - plotinfo.yaxislayer.selectAll('.' + ya._id + 'divider').remove(); - - if(plotinfo.minorGridlayer) plotinfo.minorGridlayer.selectAll('path').remove(); - if(plotinfo.gridlayer) plotinfo.gridlayer.selectAll('path').remove(); - if(plotinfo.zerolinelayer) plotinfo.zerolinelayer.selectAll('path').remove(); - - fullLayout._infolayer.select('.g-' + xa._id + 'title').remove(); - fullLayout._infolayer.select('.g-' + ya._id + 'title').remove(); - } - }); - } - - var axList = (!arg || arg === 'redraw') ? axes.listIds(gd) : arg; - - var fullAxList = axes.list(gd); - // Get the list of the overlaying axis for all 'shift' axes - var overlayingShiftedAx = fullAxList.filter(function(ax) { - return ax.autoshift; - }).map(function(ax) { - return ax.overlaying; +axes.draw = function (gd, arg, opts) { + var fullLayout = gd._fullLayout; + + if (arg === "redraw") { + fullLayout._paper.selectAll("g.subplot").each(function (d) { + var id = d[0]; + var plotinfo = fullLayout._plots[id]; + if (plotinfo) { + var xa = plotinfo.xaxis; + var ya = plotinfo.yaxis; + + plotinfo.xaxislayer.selectAll("." + xa._id + "tick").remove(); + plotinfo.yaxislayer.selectAll("." + ya._id + "tick").remove(); + plotinfo.xaxislayer.selectAll("." + xa._id + "tick2").remove(); + plotinfo.yaxislayer.selectAll("." + ya._id + "tick2").remove(); + plotinfo.xaxislayer.selectAll("." + xa._id + "divider").remove(); + plotinfo.yaxislayer.selectAll("." + ya._id + "divider").remove(); + + if (plotinfo.minorGridlayer) + plotinfo.minorGridlayer.selectAll("path").remove(); + if (plotinfo.gridlayer) plotinfo.gridlayer.selectAll("path").remove(); + if (plotinfo.zerolinelayer) + plotinfo.zerolinelayer.selectAll("path").remove(); + + fullLayout._infolayer.select(".g-" + xa._id + "title").remove(); + fullLayout._infolayer.select(".g-" + ya._id + "title").remove(); + } + }); + } + + var axList = !arg || arg === "redraw" ? axes.listIds(gd) : arg; + + var fullAxList = axes.list(gd); + // Get the list of the overlaying axis for all 'shift' axes + var overlayingShiftedAx = fullAxList + .filter(function (ax) { + return ax.autoshift; + }) + .map(function (ax) { + return ax.overlaying; }); - // order axes that have dependency to other axes - axList.map(function(axId) { - var ax = axes.getFromId(gd, axId); + // order axes that have dependency to other axes + axList.map(function (axId) { + var ax = axes.getFromId(gd, axId); - if(ax.tickmode === 'sync' && ax.overlaying) { - var overlayingIndex = axList.findIndex(function(axis) {return axis === ax.overlaying;}); + if (ax.tickmode === "sync" && ax.overlaying) { + var overlayingIndex = axList.findIndex(function (axis) { + return axis === ax.overlaying; + }); - if(overlayingIndex >= 0) { - axList.unshift(axList.splice(overlayingIndex, 1).shift()); - } - } - }); + if (overlayingIndex >= 0) { + axList.unshift(axList.splice(overlayingIndex, 1).shift()); + } + } + }); - var axShifts = {false: {left: 0, right: 0}}; + var axShifts = { false: { left: 0, right: 0 } }; - return Lib.syncOrAsync(axList.map(function(axId) { - return function() { - if(!axId) return; + return Lib.syncOrAsync( + axList.map(function (axId) { + return function () { + if (!axId) return; - var ax = axes.getFromId(gd, axId); + var ax = axes.getFromId(gd, axId); - if(!opts) opts = {}; - opts.axShifts = axShifts; - opts.overlayingShiftedAx = overlayingShiftedAx; + if (!opts) opts = {}; + opts.axShifts = axShifts; + opts.overlayingShiftedAx = overlayingShiftedAx; - var axDone = axes.drawOne(gd, ax, opts); + var axDone = axes.drawOne(gd, ax, opts); - if(ax._shiftPusher) { - incrementShift(ax, ax._fullDepth || 0, axShifts, true); - } - ax._r = ax.range.slice(); - ax._rl = Lib.simpleMap(ax._r, ax.r2l); + if (ax._shiftPusher) { + incrementShift(ax, ax._fullDepth || 0, axShifts, true); + } + ax._r = ax.range.slice(); + ax._rl = Lib.simpleMap(ax._r, ax.r2l); - return axDone; - }; - })); + return axDone; + }; + }) + ); }; /** @@ -2382,535 +2533,647 @@ axes.draw = function(gd, arg, opts) { * - ax._depth (when required only): * - and calls ax.setScale */ -axes.drawOne = function(gd, ax, opts) { - opts = opts || {}; - - var axShifts = opts.axShifts || {}; - var overlayingShiftedAx = opts.overlayingShiftedAx || []; - - var i, sp, plotinfo; - - ax.setScale(); - - var fullLayout = gd._fullLayout; - var axId = ax._id; - var axLetter = axId.charAt(0); - var counterLetter = axes.counterLetter(axId); - var mainPlotinfo = fullLayout._plots[ax._mainSubplot]; - - // this happens when updating matched group with 'missing' axes - if(!mainPlotinfo) return; - - ax._shiftPusher = ax.autoshift || - overlayingShiftedAx.indexOf(ax._id) !== -1 || - overlayingShiftedAx.indexOf(ax.overlaying) !== -1; - // An axis is also shifted by 1/2 of its own linewidth and inside tick length if applicable - // as well as its manually specified `shift` val if we're in the context of `autoshift` - if(ax._shiftPusher & ax.anchor === 'free') { - var selfPush = (ax.linewidth / 2 || 0); - if(ax.ticks === 'inside') { - selfPush += ax.ticklen; - } - incrementShift(ax, selfPush, axShifts, true); - incrementShift(ax, (ax.shift || 0), axShifts, false); - } - - // Somewhat inelegant way of making sure that the shift value is only updated when the - // Axes.DrawOne() function is called from the right context. An issue when redrawing the - // axis as result of using the dragbox, for example. - if(opts.skipTitle !== true || ax._shift === undefined) ax._shift = setShiftVal(ax, axShifts); - - var mainAxLayer = mainPlotinfo[axLetter + 'axislayer']; - var mainLinePosition = ax._mainLinePosition; - var mainLinePositionShift = mainLinePosition += ax._shift; - var mainMirrorPosition = ax._mainMirrorPosition; - - var vals = ax._vals = axes.calcTicks(ax); - - // Add a couple of axis properties that should cause us to recreate - // elements. Used in d3 data function. - var axInfo = [ax.mirror, mainLinePositionShift, mainMirrorPosition].join('_'); - for(i = 0; i < vals.length; i++) { - vals[i].axInfo = axInfo; - } - - // stash selections to avoid DOM queries e.g. - // - stash tickLabels selection, so that drawTitle can use it to scoot title - ax._selections = {}; - // stash tick angle (including the computed 'auto' values) per tick-label class - // linkup 'previous' tick angles on redraws - if(ax._tickAngles) ax._prevTickAngles = ax._tickAngles; - ax._tickAngles = {}; - // measure [in px] between axis position and outward-most part of bounding box - // (touching either the tick label or ticks) - // depth can be expansive to compute, so we only do so when required - ax._depth = null; - - // calcLabelLevelBbox can be expensive, - // so make sure to not call it twice during the same Axes.drawOne call - // by stashing label-level bounding boxes per tick-label class - var llbboxes = {}; - function getLabelLevelBbox(suffix) { - var cls = axId + (suffix || 'tick'); - if(!llbboxes[cls]) llbboxes[cls] = calcLabelLevelBbox(ax, cls); - return llbboxes[cls]; - } - - if(!ax.visible) return; - - var transTickFn = axes.makeTransTickFn(ax); - var transTickLabelFn = axes.makeTransTickLabelFn(ax); - - var tickVals; - // 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; - - var insideTicks = ax.ticks === 'inside'; - var outsideTicks = ax.ticks === 'outside'; +axes.drawOne = function (gd, ax, opts) { + opts = opts || {}; + + var axShifts = opts.axShifts || {}; + var overlayingShiftedAx = opts.overlayingShiftedAx || []; + + var i, sp, plotinfo; + + ax.setScale(); + + var fullLayout = gd._fullLayout; + var axId = ax._id; + var axLetter = axId.charAt(0); + var counterLetter = axes.counterLetter(axId); + var mainPlotinfo = fullLayout._plots[ax._mainSubplot]; + + // this happens when updating matched group with 'missing' axes + if (!mainPlotinfo) return; + + ax._shiftPusher = + ax.autoshift || + overlayingShiftedAx.indexOf(ax._id) !== -1 || + overlayingShiftedAx.indexOf(ax.overlaying) !== -1; + // An axis is also shifted by 1/2 of its own linewidth and inside tick length if applicable + // as well as its manually specified `shift` val if we're in the context of `autoshift` + if (ax._shiftPusher & (ax.anchor === "free")) { + var selfPush = ax.linewidth / 2 || 0; + if (ax.ticks === "inside") { + selfPush += ax.ticklen; + } + incrementShift(ax, selfPush, axShifts, true); + incrementShift(ax, ax.shift || 0, axShifts, false); + } + + // Somewhat inelegant way of making sure that the shift value is only updated when the + // Axes.DrawOne() function is called from the right context. An issue when redrawing the + // axis as result of using the dragbox, for example. + if (opts.skipTitle !== true || ax._shift === undefined) + ax._shift = setShiftVal(ax, axShifts); + + var mainAxLayer = mainPlotinfo[axLetter + "axislayer"]; + var mainLinePosition = ax._mainLinePosition; + var mainLinePositionShift = (mainLinePosition += ax._shift); + var mainMirrorPosition = ax._mainMirrorPosition; + + var vals = (ax._vals = axes.calcTicks(ax)); + + // Add a couple of axis properties that should cause us to recreate + // elements. Used in d3 data function. + var axInfo = [ax.mirror, mainLinePositionShift, mainMirrorPosition].join("_"); + for (i = 0; i < vals.length; i++) { + vals[i].axInfo = axInfo; + } + + // stash selections to avoid DOM queries e.g. + // - stash tickLabels selection, so that drawTitle can use it to scoot title + ax._selections = {}; + // stash tick angle (including the computed 'auto' values) per tick-label class + // linkup 'previous' tick angles on redraws + if (ax._tickAngles) ax._prevTickAngles = ax._tickAngles; + ax._tickAngles = {}; + // measure [in px] between axis position and outward-most part of bounding box + // (touching either the tick label or ticks) + // depth can be expansive to compute, so we only do so when required + ax._depth = null; + + // calcLabelLevelBbox can be expensive, + // so make sure to not call it twice during the same Axes.drawOne call + // by stashing label-level bounding boxes per tick-label class + var llbboxes = {}; + function getLabelLevelBbox(suffix) { + var cls = axId + (suffix || "tick"); + if (!llbboxes[cls]) llbboxes[cls] = calcLabelLevelBbox(ax, cls); + return llbboxes[cls]; + } + + if (!ax.visible) return; + + var transTickFn = axes.makeTransTickFn(ax); + var transTickLabelFn = axes.makeTransTickLabelFn(ax); + + var tickVals; + // 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; + + var insideTicks = ax.ticks === "inside"; + var outsideTicks = ax.ticks === "outside"; + + if (ax.tickson === "boundaries") { + var boundaryVals = getBoundaryVals(ax, vals); + valsClipped = axes.clipEnds(ax, boundaryVals); + tickVals = insideTicks ? valsClipped : boundaryVals; + } else { + valsClipped = axes.clipEnds(ax, vals); + tickVals = + insideTicks && ax.ticklabelmode !== "period" ? valsClipped : vals; + } + + var gridVals = (ax._gridVals = valsClipped); + var dividerVals = getDividerVals(ax, vals); + + if (!fullLayout._hasOnlyLargeSploms) { + var subplotsWithAx = ax._subplotsWith; + + // keep track of which subplots (by main counter axis) we've already + // drawn grids for, so we don't overdraw overlaying subplots + var finishedGrids = {}; + + for (i = 0; i < subplotsWithAx.length; i++) { + sp = subplotsWithAx[i]; + plotinfo = fullLayout._plots[sp]; + + var counterAxis = plotinfo[counterLetter + "axis"]; + var mainCounterID = counterAxis._mainAxis._id; + if (finishedGrids[mainCounterID]) continue; + finishedGrids[mainCounterID] = 1; + + var gridPath = + axLetter === "x" + ? "M0," + counterAxis._offset + "v" + counterAxis._length + : "M" + counterAxis._offset + ",0h" + counterAxis._length; + + axes.drawGrid(gd, ax, { + vals: gridVals, + counterAxis: counterAxis, + layer: plotinfo.gridlayer.select("." + axId), + minorLayer: plotinfo.minorGridlayer.select("." + axId), + path: gridPath, + transFn: transTickFn, + }); + axes.drawZeroLine(gd, ax, { + counterAxis: counterAxis, + layer: plotinfo.zerolinelayer, + path: gridPath, + transFn: transTickFn, + }); + } + } + + var tickPath; + + var majorTickSigns = axes.getTickSigns(ax); + var minorTickSigns = axes.getTickSigns(ax, "minor"); + + if (ax.ticks || (ax.minor && ax.minor.ticks)) { + var majorTickPath = axes.makeTickPath( + ax, + mainLinePositionShift, + majorTickSigns[2] + ); + var minorTickPath = axes.makeTickPath( + ax, + mainLinePositionShift, + minorTickSigns[2], + { minor: true } + ); - if(ax.tickson === 'boundaries') { - var boundaryVals = getBoundaryVals(ax, vals); - valsClipped = axes.clipEnds(ax, boundaryVals); - tickVals = insideTicks ? valsClipped : boundaryVals; + var mirrorMajorTickPath; + var mirrorMinorTickPath; + + var fullMajorTickPath; + var fullMinorTickPath; + + if (ax._anchorAxis && ax.mirror && ax.mirror !== true) { + mirrorMajorTickPath = axes.makeTickPath( + ax, + mainMirrorPosition, + majorTickSigns[3] + ); + mirrorMinorTickPath = axes.makeTickPath( + ax, + mainMirrorPosition, + minorTickSigns[3], + { minor: true } + ); + + fullMajorTickPath = majorTickPath + mirrorMajorTickPath; + fullMinorTickPath = minorTickPath + mirrorMinorTickPath; } else { - valsClipped = axes.clipEnds(ax, vals); - tickVals = (insideTicks && ax.ticklabelmode !== 'period') ? valsClipped : vals; - } - - var gridVals = ax._gridVals = valsClipped; - var dividerVals = getDividerVals(ax, vals); - - if(!fullLayout._hasOnlyLargeSploms) { - var subplotsWithAx = ax._subplotsWith; - - // keep track of which subplots (by main counter axis) we've already - // drawn grids for, so we don't overdraw overlaying subplots - var finishedGrids = {}; - - for(i = 0; i < subplotsWithAx.length; i++) { - sp = subplotsWithAx[i]; - plotinfo = fullLayout._plots[sp]; - - var counterAxis = plotinfo[counterLetter + 'axis']; - var mainCounterID = counterAxis._mainAxis._id; - if(finishedGrids[mainCounterID]) continue; - finishedGrids[mainCounterID] = 1; - - var gridPath = axLetter === 'x' ? - 'M0,' + counterAxis._offset + 'v' + counterAxis._length : - 'M' + counterAxis._offset + ',0h' + counterAxis._length; + mirrorMajorTickPath = ""; + mirrorMinorTickPath = ""; + fullMajorTickPath = majorTickPath; + fullMinorTickPath = minorTickPath; + } + + if (ax.showdividers && outsideTicks && ax.tickson === "boundaries") { + var dividerLookup = {}; + for (i = 0; i < dividerVals.length; i++) { + dividerLookup[dividerVals[i].x] = 1; + } + tickPath = function (d) { + return dividerLookup[d.x] ? mirrorMajorTickPath : fullMajorTickPath; + }; + } else { + tickPath = function (d) { + return d.minor ? fullMinorTickPath : fullMajorTickPath; + }; + } + } + + axes.drawTicks(gd, ax, { + vals: tickVals, + layer: mainAxLayer, + path: tickPath, + transFn: transTickFn, + }); + + if (ax.mirror === "allticks") { + var 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 p0 = linepositions[0]; + var p1 = linepositions[1]; + var isMinor = linepositions[2]; + + var spTickPath = + axes.makeTickPath( + ax, + p0, + isMinor ? majorTickSigns[0] : minorTickSigns[0], + { minor: isMinor } + ) + + axes.makeTickPath( + ax, + p1, + isMinor ? majorTickSigns[1] : minorTickSigns[1], + { minor: isMinor } + ); - axes.drawGrid(gd, ax, { - vals: gridVals, - counterAxis: counterAxis, - layer: plotinfo.gridlayer.select('.' + axId), - minorLayer: plotinfo.minorGridlayer.select('.' + axId), - path: gridPath, - transFn: transTickFn - }); - axes.drawZeroLine(gd, ax, { - counterAxis: counterAxis, - layer: plotinfo.zerolinelayer, - path: gridPath, - transFn: transTickFn - }); - } + axes.drawTicks(gd, ax, { + vals: tickVals, + layer: plotinfo[axLetter + "axislayer"], + path: spTickPath, + transFn: transTickFn, + }); } + } - var tickPath; + var seq = []; - var majorTickSigns = axes.getTickSigns(ax); - var minorTickSigns = axes.getTickSigns(ax, 'minor'); + // tick labels - for now just the main labels. + // TODO: mirror labels, esp for subplots - if(ax.ticks || (ax.minor && ax.minor.ticks)) { - var majorTickPath = axes.makeTickPath(ax, mainLinePositionShift, majorTickSigns[2]); - var minorTickPath = axes.makeTickPath(ax, mainLinePositionShift, minorTickSigns[2], { minor: true }); - - var mirrorMajorTickPath; - var mirrorMinorTickPath; - - var fullMajorTickPath; - var fullMinorTickPath; - - if(ax._anchorAxis && ax.mirror && ax.mirror !== true) { - mirrorMajorTickPath = axes.makeTickPath(ax, mainMirrorPosition, majorTickSigns[3]); - mirrorMinorTickPath = axes.makeTickPath(ax, mainMirrorPosition, minorTickSigns[3], { minor: true }); - - fullMajorTickPath = majorTickPath + mirrorMajorTickPath; - fullMinorTickPath = minorTickPath + mirrorMinorTickPath; - } else { - mirrorMajorTickPath = ''; - mirrorMinorTickPath = ''; - fullMajorTickPath = majorTickPath; - fullMinorTickPath = minorTickPath; - } - - if(ax.showdividers && outsideTicks && ax.tickson === 'boundaries') { - var dividerLookup = {}; - for(i = 0; i < dividerVals.length; i++) { - dividerLookup[dividerVals[i].x] = 1; - } - tickPath = function(d) { - return dividerLookup[d.x] ? mirrorMajorTickPath : fullMajorTickPath; - }; - } else { - tickPath = function(d) { - return d.minor ? fullMinorTickPath : fullMajorTickPath; - }; - } - } - - axes.drawTicks(gd, ax, { - vals: tickVals, - layer: mainAxLayer, - path: tickPath, - transFn: transTickFn + seq.push(function () { + return axes.drawLabels(gd, ax, { + vals: vals, + layer: mainAxLayer, + plotinfo: plotinfo, + transFn: transTickLabelFn, + labelFns: axes.makeLabelFns(ax, mainLinePositionShift), }); + }); + + var tickNames = ["tick"]; + + if (ax.type === "multicategory") { + ax.levels + .slice() + .reverse() + .slice(0, ax.levelNr - 1) + .forEach(function (_lvl) { + var pad = { x: 0 * _lvl, y: 10 }[axLetter]; + + var tickName = "tick" + String(_lvl); + tickNames.push(tickName); + + seq.push(function () { + var bboxKey = { x: "height", y: "width" }[axLetter]; + var standoff = + _lvl * getLabelLevelBbox()[bboxKey] + + pad + + (ax._tickAngles[axId + "tick"] + ? ax.tickfont.size * LINE_SPACING + : 0); + + return axes.drawLabels(gd, ax, { + vals: getSecondaryLabelVals(ax, vals, _lvl), + layer: mainAxLayer, + cls: axId + tickName, + repositionOnUpdate: true, + secondary: true, + transFn: transTickFn, + labelFns: axes.makeLabelFns( + ax, + mainLinePosition + standoff * majorTickSigns[4] + ), + }); + }); + }); - if(ax.mirror === 'allticks') { - var 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 p0 = linepositions[0]; - var p1 = linepositions[1]; - var isMinor = linepositions[2]; - - var spTickPath = - axes.makeTickPath(ax, p0, - isMinor ? majorTickSigns[0] : minorTickSigns[0], - { minor: isMinor } - ) + - axes.makeTickPath(ax, p1, - isMinor ? majorTickSigns[1] : minorTickSigns[1], - { minor: isMinor } - ); - - axes.drawTicks(gd, ax, { - vals: tickVals, - layer: plotinfo[axLetter + 'axislayer'], - path: spTickPath, - transFn: transTickFn - }); - } - } - - var seq = []; + tickNames = tickNames.sort(); - // tick labels - for now just the main labels. - // TODO: mirror labels, esp for subplots + ax.levels.forEach(function (_lvl, idx) { + seq.push(function () { + ax._depth = + majorTickSigns[4] * + (getLabelLevelBbox(tickNames.slice()[_lvl])[ax.side] - + mainLinePosition); - seq.push(function() { - return axes.drawLabels(gd, ax, { - vals: vals, - layer: mainAxLayer, - plotinfo: plotinfo, - transFn: transTickLabelFn, - labelFns: axes.makeLabelFns(ax, mainLinePositionShift) + var levelDividers = dividerVals.slice().filter(function (divider) { + return divider.level === idx; }); - }); - if(ax.type === 'multicategory') { - var pad = {x: 2, y: 10}[axLetter]; - - seq.push(function() { - var bboxKey = {x: 'height', y: 'width'}[axLetter]; - var standoff = getLabelLevelBbox()[bboxKey] + pad + - (ax._tickAngles[axId + 'tick'] ? ax.tickfont.size * LINE_SPACING : 0); - - return axes.drawLabels(gd, ax, { - vals: getSecondaryLabelVals(ax, vals), - layer: mainAxLayer, - cls: axId + 'tick2', - repositionOnUpdate: true, - secondary: true, - transFn: transTickFn, - labelFns: axes.makeLabelFns(ax, mainLinePositionShift + standoff * majorTickSigns[4]) - }); + return drawDividers(gd, ax, { + vals: levelDividers, + layer: mainAxLayer, + path: axes.makeTickPath(ax, mainLinePosition, majorTickSigns[4], { + len: ax._depth, + }), + transFn: transTickFn, + level: _lvl, }); + }); + }); + } else if (ax.title.hasOwnProperty("standoff")) { + seq.push(function () { + ax._depth = + majorTickSigns[4] * + (getLabelLevelBbox()[ax.side] - mainLinePositionShift); + }); + } - seq.push(function() { - ax._depth = majorTickSigns[4] * (getLabelLevelBbox('tick2')[ax.side] - mainLinePositionShift); + var hasRangeSlider = Registry.getComponentMethod( + "rangeslider", + "isVisible" + )(ax); - return drawDividers(gd, ax, { - vals: dividerVals, - layer: mainAxLayer, - path: axes.makeTickPath(ax, mainLinePositionShift, majorTickSigns[4], { len: ax._depth }), - transFn: transTickFn - }); - }); - } else if(ax.title.hasOwnProperty('standoff')) { - seq.push(function() { - ax._depth = majorTickSigns[4] * (getLabelLevelBbox()[ax.side] - mainLinePositionShift); - }); - } + if (!opts.skipTitle && !(hasRangeSlider && ax.side === "bottom")) { + seq.push(function () { + return drawTitle(gd, ax); + }); + } - var hasRangeSlider = Registry.getComponentMethod('rangeslider', 'isVisible')(ax); + seq.push(function () { + var s = ax.side.charAt(0); + var sMirror = OPPOSITE_SIDE[ax.side].charAt(0); + var pos = axes.getPxPosition(gd, ax); + var outsideTickLen = outsideTicks ? ax.ticklen : 0; + var llbbox; + + var push; + var mirrorPush; + var rangeSliderPush; + + if (ax.automargin || hasRangeSlider || ax._shiftPusher) { + if (ax.type === "multicategory") { + // hardcoded tick name, breakes only with plotly.py. Not sure if this is the right selection + // llbbox = getLabelLevelBbox('tick2'); + llbbox = getLabelLevelBbox("tick" + String(ax.levelNr - 1)); + } else { + llbbox = getLabelLevelBbox(); + if (axLetter === "x" && s === "b") { + ax._depth = Math.max( + llbbox.width > 0 ? llbbox.bottom - pos : 0, + outsideTickLen + ); + } + } + } + + var axDepth = 0; + var titleDepth = 0; + if (ax._shiftPusher) { + axDepth = Math.max( + outsideTickLen, + llbbox.height > 0 + ? s === "l" + ? pos - llbbox.left + : llbbox.right - pos + : 0 + ); + if (ax.title.text !== fullLayout._dfltTitle[axLetter]) { + titleDepth = (ax._titleStandoff || 0) + (ax._titleScoot || 0); + if (s === "l") { + titleDepth += approxTitleDepth(ax); + } + } - if(!opts.skipTitle && - !(hasRangeSlider && ax.side === 'bottom') - ) { - seq.push(function() { return drawTitle(gd, ax); }); + ax._fullDepth = Math.max(axDepth, titleDepth); } - seq.push(function() { - var s = ax.side.charAt(0); - var sMirror = OPPOSITE_SIDE[ax.side].charAt(0); - var pos = axes.getPxPosition(gd, ax); - var outsideTickLen = outsideTicks ? ax.ticklen : 0; - var llbbox; - - var push; - var mirrorPush; - var rangeSliderPush; - - if(ax.automargin || hasRangeSlider || ax._shiftPusher) { - if(ax.type === 'multicategory') { - llbbox = getLabelLevelBbox('tick2'); - } else { - llbbox = getLabelLevelBbox(); - if(axLetter === 'x' && s === 'b') { - ax._depth = Math.max(llbbox.width > 0 ? llbbox.bottom - pos : 0, outsideTickLen); - } - } + if (ax.automargin) { + push = { x: 0, y: 0, r: 0, l: 0, t: 0, b: 0 }; + var domainIndices = [0, 1]; + var shift = typeof ax._shift === "number" ? ax._shift : 0; + if (axLetter === "x") { + if (s === "b") { + push[s] = ax._depth; + } else { + push[s] = ax._depth = Math.max( + llbbox.width > 0 ? pos - llbbox.top : 0, + outsideTickLen + ); + domainIndices.reverse(); } - var axDepth = 0; - var titleDepth = 0; - if(ax._shiftPusher) { - axDepth = Math.max( - outsideTickLen, - llbbox.height > 0 ? (s === 'l' ? pos - llbbox.left : llbbox.right - pos) : 0 - ); - if(ax.title.text !== fullLayout._dfltTitle[axLetter]) { - titleDepth = (ax._titleStandoff || 0) + (ax._titleScoot || 0); - if(s === 'l') { - titleDepth += approxTitleDepth(ax); - } - } - - ax._fullDepth = Math.max(axDepth, titleDepth); + if (llbbox.width > 0) { + var rExtra = llbbox.right - (ax._offset + ax._length); + if (rExtra > 0) { + push.xr = 1; + push.r = rExtra; + } + var lExtra = ax._offset - llbbox.left; + if (lExtra > 0) { + push.xl = 0; + push.l = lExtra; + } + } + } else { + if (s === "l") { + ax._depth = Math.max( + llbbox.height > 0 ? pos - llbbox.left : 0, + outsideTickLen + ); + push[s] = ax._depth - shift; + } else { + ax._depth = Math.max( + llbbox.height > 0 ? llbbox.right - pos : 0, + outsideTickLen + ); + push[s] = ax._depth + shift; + domainIndices.reverse(); } - if(ax.automargin) { - push = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0}; - var domainIndices = [0, 1]; - var shift = typeof ax._shift === 'number' ? ax._shift : 0; - if(axLetter === 'x') { - if(s === 'b') { - push[s] = ax._depth; - } else { - push[s] = ax._depth = Math.max(llbbox.width > 0 ? pos - llbbox.top : 0, outsideTickLen); - domainIndices.reverse(); - } - - if(llbbox.width > 0) { - var rExtra = llbbox.right - (ax._offset + ax._length); - if(rExtra > 0) { - push.xr = 1; - push.r = rExtra; - } - var lExtra = ax._offset - llbbox.left; - if(lExtra > 0) { - push.xl = 0; - push.l = lExtra; - } - } - } else { - if(s === 'l') { - ax._depth = Math.max(llbbox.height > 0 ? pos - llbbox.left : 0, outsideTickLen); - push[s] = ax._depth - shift; - } else { - ax._depth = Math.max(llbbox.height > 0 ? llbbox.right - pos : 0, outsideTickLen); - push[s] = ax._depth + shift; - domainIndices.reverse(); - } - - if(llbbox.height > 0) { - var bExtra = llbbox.bottom - (ax._offset + ax._length); - if(bExtra > 0) { - push.yb = 0; - push.b = bExtra; - } - var tExtra = ax._offset - llbbox.top; - if(tExtra > 0) { - push.yt = 1; - push.t = tExtra; - } - } - } - - push[counterLetter] = ax.anchor === 'free' ? - ax.position : - ax._anchorAxis.domain[domainIndices[0]]; - - if(ax.title.text !== fullLayout._dfltTitle[axLetter]) { - push[s] += approxTitleDepth(ax) + (ax.title.standoff || 0); - } - - if(ax.mirror && ax.anchor !== 'free') { - mirrorPush = {x: 0, y: 0, r: 0, l: 0, t: 0, b: 0}; - - mirrorPush[sMirror] = ax.linewidth; - if(ax.mirror && ax.mirror !== true) mirrorPush[sMirror] += outsideTickLen; - - if(ax.mirror === true || ax.mirror === 'ticks') { - mirrorPush[counterLetter] = ax._anchorAxis.domain[domainIndices[1]]; - } else if(ax.mirror === 'all' || ax.mirror === 'allticks') { - mirrorPush[counterLetter] = [ax._counterDomainMin, ax._counterDomainMax][domainIndices[1]]; - } - } + if (llbbox.height > 0) { + var bExtra = llbbox.bottom - (ax._offset + ax._length); + if (bExtra > 0) { + push.yb = 0; + push.b = bExtra; + } + var tExtra = ax._offset - llbbox.top; + if (tExtra > 0) { + push.yt = 1; + push.t = tExtra; + } } - if(hasRangeSlider) { - rangeSliderPush = Registry.getComponentMethod('rangeslider', 'autoMarginOpts')(gd, ax); + } + + push[counterLetter] = + ax.anchor === "free" + ? ax.position + : ax._anchorAxis.domain[domainIndices[0]]; + + if (ax.title.text !== fullLayout._dfltTitle[axLetter]) { + push[s] += approxTitleDepth(ax) + (ax.title.standoff || 0); + } + + if (ax.mirror && ax.anchor !== "free") { + mirrorPush = { x: 0, y: 0, r: 0, l: 0, t: 0, b: 0 }; + + mirrorPush[sMirror] = ax.linewidth; + if (ax.mirror && ax.mirror !== true) + mirrorPush[sMirror] += outsideTickLen; + + if (ax.mirror === true || ax.mirror === "ticks") { + mirrorPush[counterLetter] = ax._anchorAxis.domain[domainIndices[1]]; + } else if (ax.mirror === "all" || ax.mirror === "allticks") { + mirrorPush[counterLetter] = [ + ax._counterDomainMin, + ax._counterDomainMax, + ][domainIndices[1]]; } + } + } + if (hasRangeSlider) { + rangeSliderPush = Registry.getComponentMethod( + "rangeslider", + "autoMarginOpts" + )(gd, ax); + } - if(typeof ax.automargin === 'string') { - filterPush(push, ax.automargin); - filterPush(mirrorPush, ax.automargin); - } + if (typeof ax.automargin === "string") { + filterPush(push, ax.automargin); + filterPush(mirrorPush, ax.automargin); + } - Plots.autoMargin(gd, axAutoMarginID(ax), push); - Plots.autoMargin(gd, axMirrorAutoMarginID(ax), mirrorPush); - Plots.autoMargin(gd, rangeSliderAutoMarginID(ax), rangeSliderPush); - }); + Plots.autoMargin(gd, axAutoMarginID(ax), push); + Plots.autoMargin(gd, axMirrorAutoMarginID(ax), mirrorPush); + Plots.autoMargin(gd, rangeSliderAutoMarginID(ax), rangeSliderPush); + }); - return Lib.syncOrAsync(seq); + return Lib.syncOrAsync(seq); }; function filterPush(push, automargin) { - if(!push) return; - - var keepMargin = Object.keys(MARGIN_MAPPING).reduce(function(data, nextKey) { - if(automargin.indexOf(nextKey) !== -1) { - MARGIN_MAPPING[nextKey].forEach(function(key) { data[key] = 1;}); - } - return data; - }, {}); - Object.keys(push).forEach(function(key) { - if(!keepMargin[key]) { - if(key.length === 1) push[key] = 0; - else delete push[key]; - } - }); + if (!push) return; + + var keepMargin = Object.keys(MARGIN_MAPPING).reduce(function (data, nextKey) { + if (automargin.indexOf(nextKey) !== -1) { + MARGIN_MAPPING[nextKey].forEach(function (key) { + data[key] = 1; + }); + } + return data; + }, {}); + Object.keys(push).forEach(function (key) { + if (!keepMargin[key]) { + if (key.length === 1) push[key] = 0; + else delete push[key]; + } + }); } function getBoundaryVals(ax, vals) { - var out = []; - var i; - - // boundaryVals are never used for labels; - // no need to worry about the other tickTextObj keys - var _push = function(d, bndIndex) { - var xb = d.xbnd[bndIndex]; - if(xb !== null) { - out.push(Lib.extendFlat({}, d, {x: xb})); - } - }; + var out = []; + var i; - if(vals.length) { - for(i = 0; i < vals.length; i++) { - _push(vals[i], 0); - } - _push(vals[i - 1], 1); + // boundaryVals are never used for labels; + // no need to worry about the other tickTextObj keys + var _push = function (d, bndIndex) { + var xb = d.xbnd[bndIndex]; + if (xb !== null) { + out.push(Lib.extendFlat({}, d, { x: xb })); } + }; - return out; -} + if (vals.length) { + for (i = 0; i < vals.length; i++) { + _push(vals[i], 0); + } + _push(vals[i - 1], 1); + } -function getSecondaryLabelVals(ax, vals) { - var out = []; - var lookup = {}; + return out; +} - for(var i = 0; i < vals.length; i++) { - var d = vals[i]; - if(lookup[d.text2]) { - lookup[d.text2].push(d.x); - } else { - lookup[d.text2] = [d.x]; - } +function getSecondaryLabelVals(ax, vals, level) { + var out = []; + var lookup = {}; + var appearences = {}; + var current; + var currentParent = null; + var parent = null; + + for (var i = 0; i < vals.length; i++) { + var d = vals[i]; + var text = d.texts[level]; + parent = d.texts[level + 1]; + if (lookup[text]) { + if ((d.texts[level] === current) & (parent === currentParent)) { + lookup[text][appearences[text]].push(d.x); + } else { + appearences[text] = appearences[text] + 1; + lookup[text].push([d.x]); + } + } else { + appearences[text] = 0; + lookup[text] = [[d.x]]; } + current = d.texts[level]; + currentParent = d.texts[level + 1]; + } - for(var k in lookup) { - out.push(tickTextObj(ax, Lib.interp(lookup[k], 0.5), k)); - } + Object.keys(lookup).forEach(function (key) { + lookup[key].forEach(function (pos) { + out.push(tickTextObj(ax, Lib.interp(pos, 0.5), key)); + }); + }); - return out; + return out; } function getDividerVals(ax, vals) { - var out = []; - var i, current; - - var reversed = (vals.length && vals[vals.length - 1].x < vals[0].x); - - // never used for labels; - // no need to worry about the other tickTextObj keys - var _push = function(d, bndIndex) { - var xb = d.xbnd[bndIndex]; - if(xb !== null) { - out.push(Lib.extendFlat({}, d, {x: xb})); - } - }; - - if(ax.showdividers && vals.length) { - for(i = 0; i < vals.length; i++) { - var d = vals[i]; - if(d.text2 !== current) { - _push(d, reversed ? 1 : 0); - } - current = d.text2; + var out = []; + var i, current; + + var reversed = vals.length && vals[vals.length - 1].x < vals[0].x; + + // never used for labels; + // no need to worry about the other tickTextObj keys + var _push = function (d, bndIndex, level) { + var xb = d.xbnd[bndIndex]; + if (xb !== null) { + var _out = Lib.extendFlat({}, d, { x: xb }); + _out.level = level; + out.push(_out); + } + }; + + if (ax.showdividers && vals.length) { + ax.levels.forEach(function (_lvl) { + current = undefined; + for (i = 0; i < vals.length; i++) { + var d = vals[i]; + if (d.texts[_lvl] !== current) { + _push(d, reversed ? 1 : 0, _lvl); } - _push(vals[i - 1], reversed ? 0 : 1); - } - - return out; + current = d.texts[_lvl]; + // text2 + } + _push(vals[i - 1], reversed ? 0 : 1); + }); + } + return out; } function calcLabelLevelBbox(ax, cls) { - var top, bottom; - var left, right; - - if(ax._selections[cls].size()) { - top = Infinity; - bottom = -Infinity; - left = Infinity; - right = -Infinity; - ax._selections[cls].each(function() { - var thisLabel = selectTickLabel(this); - // Use parent node , to make Drawing.bBox - // retrieve a bbox computed with transform info - // - // To improve perf, it would be nice to use `thisLabel.node()` - // (like in fixLabelOverlaps) instead and use Axes.getPxPosition - // together with the makeLabelFns outputs and `tickangle` - // to compute one bbox per (tick value x tick style) - var bb = Drawing.bBox(thisLabel.node().parentNode); - top = Math.min(top, bb.top); - bottom = Math.max(bottom, bb.bottom); - left = Math.min(left, bb.left); - right = Math.max(right, bb.right); - }); - } else { - top = 0; - bottom = 0; - left = 0; - right = 0; - } - - return { - top: top, - bottom: bottom, - left: left, - right: right, - height: bottom - top, - width: right - left - }; + var top, bottom; + var left, right; + + if (ax._selections[cls].size()) { + top = Infinity; + bottom = -Infinity; + left = Infinity; + right = -Infinity; + ax._selections[cls].each(function () { + var thisLabel = selectTickLabel(this); + // Use parent node , to make Drawing.bBox + // retrieve a bbox computed with transform info + // + // To improve perf, it would be nice to use `thisLabel.node()` + // (like in fixLabelOverlaps) instead and use Axes.getPxPosition + // together with the makeLabelFns outputs and `tickangle` + // to compute one bbox per (tick value x tick style) + var bb = Drawing.bBox(thisLabel.node().parentNode); + top = Math.min(top, bb.top); + bottom = Math.max(bottom, bb.bottom); + left = Math.min(left, bb.left); + right = Math.max(right, bb.right); + }); + } else { + top = 0; + bottom = 0; + left = 0; + right = 0; + } + + return { + top: top, + bottom: bottom, + left: left, + right: right, + height: bottom - top, + width: right - left, + }; } /** @@ -2927,22 +3190,24 @@ function calcLabelLevelBbox(ax, cls) { * - [3]: sign for ticks mirroring 'ax.side' * - [4]: sign of arrow starting at axis pointing towards margin */ -axes.getTickSigns = function(ax, minor) { - 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 - - var ticks = minor ? (ax.minor || {}).ticks : ax.ticks; - if((ticks !== 'inside') === (axLetter === 'x')) { - out = out.map(function(v) { return -v; }); - } - // independent of `ticks`; do not flip this one - if(ax.side) { - out.push({l: -1, t: -1, r: 1, b: 1}[ax.side.charAt(0)]); - } - return out; +axes.getTickSigns = function (ax, minor) { + 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 + + var ticks = minor ? (ax.minor || {}).ticks : ax.ticks; + if ((ticks !== "inside") === (axLetter === "x")) { + out = out.map(function (v) { + return -v; + }); + } + // independent of `ticks`; do not flip this one + if (ax.side) { + out.push({ l: -1, t: -1, r: 1, b: 1 }[ax.side.charAt(0)]); + } + return out; }; /** @@ -2954,80 +3219,75 @@ axes.getTickSigns = function(ax, minor) { * - {fn} l2p * @return {fn} function of calcTicks items */ -axes.makeTransTickFn = function(ax) { - return ax._id.charAt(0) === 'x' ? - function(d) { return strTranslate(ax._offset + ax.l2p(d.x), 0); } : - function(d) { return strTranslate(0, ax._offset + ax.l2p(d.x)); }; +axes.makeTransTickFn = function (ax) { + return ax._id.charAt(0) === "x" + ? function (d) { + return strTranslate(ax._offset + ax.l2p(d.x), 0); + } + : function (d) { + return strTranslate(0, ax._offset + ax.l2p(d.x)); + }; }; -axes.makeTransTickLabelFn = function(ax) { - var uv = getTickLabelUV(ax); - var u = uv[0]; - var v = uv[1]; - - return ax._id.charAt(0) === 'x' ? - function(d) { - return strTranslate( - u + ax._offset + ax.l2p(getPosX(d)), - v - ); - } : - function(d) { - return strTranslate( - v, - u + ax._offset + ax.l2p(getPosX(d)) - ); - }; +axes.makeTransTickLabelFn = function (ax) { + var uv = getTickLabelUV(ax); + var u = uv[0]; + var v = uv[1]; + + return ax._id.charAt(0) === "x" + ? function (d) { + return strTranslate(u + ax._offset + ax.l2p(getPosX(d)), v); + } + : function (d) { + return strTranslate(v, u + ax._offset + ax.l2p(getPosX(d))); + }; }; function getPosX(d) { - return d.periodX !== undefined ? d.periodX : d.x; + return d.periodX !== undefined ? d.periodX : d.x; } // u is a shift along the axis, // v is a shift perpendicular to the axis function getTickLabelUV(ax) { - var ticklabelposition = ax.ticklabelposition || ''; - var has = function(str) { - return ticklabelposition.indexOf(str) !== -1; - }; - - var isTop = has('top'); - var isLeft = has('left'); - var isRight = has('right'); - var isBottom = has('bottom'); - var isInside = has('inside'); - - var isAligned = isBottom || isLeft || isTop || isRight; - - // early return - if(!isAligned && !isInside) return [0, 0]; - - var side = ax.side; - - var u = isAligned ? (ax.tickwidth || 0) / 2 : 0; - var v = TEXTPAD; - - var fontSize = ax.tickfont ? ax.tickfont.size : 12; - if(isBottom || isTop) { - u += fontSize * CAP_SHIFT; - v += (ax.linewidth || 0) / 2; - } - if(isLeft || isRight) { - u += (ax.linewidth || 0) / 2; - v += TEXTPAD; - } - if(isInside && side === 'top') { - v -= fontSize * (1 - CAP_SHIFT); - } - - if(isLeft || isTop) u = -u; - if(side === 'bottom' || side === 'right') v = -v; - - return [ - isAligned ? u : 0, - isInside ? v : 0 - ]; + var ticklabelposition = ax.ticklabelposition || ""; + var has = function (str) { + return ticklabelposition.indexOf(str) !== -1; + }; + + var isTop = has("top"); + var isLeft = has("left"); + var isRight = has("right"); + var isBottom = has("bottom"); + var isInside = has("inside"); + + var isAligned = isBottom || isLeft || isTop || isRight; + + // early return + if (!isAligned && !isInside) return [0, 0]; + + var side = ax.side; + + var u = isAligned ? (ax.tickwidth || 0) / 2 : 0; + var v = TEXTPAD; + + var fontSize = ax.tickfont ? ax.tickfont.size : 12; + if (isBottom || isTop) { + u += fontSize * CAP_SHIFT; + v += (ax.linewidth || 0) / 2; + } + if (isLeft || isRight) { + u += (ax.linewidth || 0) / 2; + v += TEXTPAD; + } + if (isInside && side === "top") { + v -= fontSize * (1 - CAP_SHIFT); + } + + if (isLeft || isTop) u = -u; + if (side === "bottom" || side === "right") v = -v; + + return [isAligned ? u : 0, isInside ? v : 0]; } /** @@ -3043,20 +3303,20 @@ function getTickLabelUV(ax) { * - {number (optional)} len tick length * @return {string} */ -axes.makeTickPath = function(ax, shift, sgn, opts) { - if(!opts) opts = {}; - var minor = opts.minor; - if(minor && !ax.minor) return ''; +axes.makeTickPath = function (ax, shift, sgn, opts) { + if (!opts) opts = {}; + var minor = opts.minor; + if (minor && !ax.minor) return ""; - var len = opts.len !== undefined ? opts.len : - minor ? ax.minor.ticklen : ax.ticklen; + var len = + opts.len !== undefined ? opts.len : minor ? ax.minor.ticklen : ax.ticklen; - var axLetter = ax._id.charAt(0); - var pad = (ax.linewidth || 1) / 2; + var axLetter = ax._id.charAt(0); + var pad = (ax.linewidth || 1) / 2; - return axLetter === 'x' ? - 'M0,' + (shift + pad * sgn) + 'v' + (len * sgn) : - 'M' + (shift + pad * sgn) + ',0h' + (len * sgn); + return axLetter === "x" + ? "M0," + (shift + pad * sgn) + "v" + len * sgn + : "M" + (shift + pad * sgn) + ",0h" + len * sgn; }; /** @@ -3080,159 +3340,169 @@ axes.makeTickPath = function(ax, shift, sgn, opts) { * - {number} labelStandoff (gap parallel to ticks) * - {number} labelShift (gap perpendicular to ticks) */ -axes.makeLabelFns = function(ax, shift, angle) { - var ticklabelposition = ax.ticklabelposition || ''; - var has = function(str) { - return ticklabelposition.indexOf(str) !== -1; - }; - - var isTop = has('top'); - var isLeft = has('left'); - var isRight = has('right'); - var isBottom = has('bottom'); - var isAligned = isBottom || isLeft || isTop || isRight; - - var insideTickLabels = has('inside'); - var labelsOverTicks = - (ticklabelposition === 'inside' && ax.ticks === 'inside') || - (!insideTickLabels && ax.ticks === 'outside' && ax.tickson !== 'boundaries'); - - var labelStandoff = 0; - var labelShift = 0; - - var tickLen = labelsOverTicks ? ax.ticklen : 0; - if(insideTickLabels) { - tickLen *= -1; - } else if(isAligned) { - tickLen = 0; - } - - if(labelsOverTicks) { - labelStandoff += tickLen; - if(angle) { - var rad = Lib.deg2rad(angle); - labelStandoff = tickLen * Math.cos(rad) + 1; - labelShift = tickLen * Math.sin(rad); +axes.makeLabelFns = function (ax, shift, angle) { + var ticklabelposition = ax.ticklabelposition || ""; + var has = function (str) { + return ticklabelposition.indexOf(str) !== -1; + }; + + var isTop = has("top"); + var isLeft = has("left"); + var isRight = has("right"); + var isBottom = has("bottom"); + var isAligned = isBottom || isLeft || isTop || isRight; + + var insideTickLabels = has("inside"); + var labelsOverTicks = + (ticklabelposition === "inside" && ax.ticks === "inside") || + (!insideTickLabels && + ax.ticks === "outside" && + ax.tickson !== "boundaries"); + + var labelStandoff = 0; + var labelShift = 0; + + var tickLen = labelsOverTicks ? ax.ticklen : 0; + if (insideTickLabels) { + tickLen *= -1; + } else if (isAligned) { + tickLen = 0; + } + + if (labelsOverTicks) { + labelStandoff += tickLen; + if (angle) { + var rad = Lib.deg2rad(angle); + labelStandoff = tickLen * Math.cos(rad) + 1; + labelShift = tickLen * Math.sin(rad); + } + } + + if (ax.showticklabels && (labelsOverTicks || ax.showline)) { + labelStandoff += 0.2 * ax.tickfont.size; + } + labelStandoff += ((ax.linewidth || 1) / 2) * (insideTickLabels ? -1 : 1); + + var out = { + labelStandoff: labelStandoff, + labelShift: labelShift, + }; + + var x0, y0, ff, flipIt; + var xQ = 0; + + var side = ax.side; + var axLetter = ax._id.charAt(0); + var tickangle = ax.tickangle; + var endSide; + if (axLetter === "x") { + endSide = + (!insideTickLabels && side === "bottom") || + (insideTickLabels && side === "top"); + + flipIt = endSide ? 1 : -1; + if (insideTickLabels) flipIt *= -1; + + x0 = labelShift * flipIt; + y0 = shift + labelStandoff * flipIt; + ff = endSide ? 1 : -0.2; + if (Math.abs(tickangle) === 90) { + if (insideTickLabels) { + ff += MID_SHIFT; + } else { + if (tickangle === -90 && side === "bottom") { + ff = CAP_SHIFT; + } else if (tickangle === 90 && side === "top") { + ff = MID_SHIFT; + } else { + ff = 0.5; } - } + } - if(ax.showticklabels && (labelsOverTicks || ax.showline)) { - labelStandoff += 0.2 * ax.tickfont.size; + xQ = (MID_SHIFT / 2) * (tickangle / 90); } - labelStandoff += (ax.linewidth || 1) / 2 * (insideTickLabels ? -1 : 1); - var out = { - labelStandoff: labelStandoff, - labelShift: labelShift + out.xFn = function (d) { + return d.dx + x0 + xQ * d.fontSize; }; + out.yFn = function (d) { + return d.dy + y0 + d.fontSize * ff; + }; + out.anchorFn = function (d, a) { + if (isAligned) { + if (isLeft) return "end"; + if (isRight) return "start"; + } - var x0, y0, ff, flipIt; - var xQ = 0; - - var side = ax.side; - var axLetter = ax._id.charAt(0); - var tickangle = ax.tickangle; - var endSide; - if(axLetter === 'x') { - endSide = - (!insideTickLabels && side === 'bottom') || - (insideTickLabels && side === 'top'); - - flipIt = endSide ? 1 : -1; - if(insideTickLabels) flipIt *= -1; - - x0 = labelShift * flipIt; - y0 = shift + labelStandoff * flipIt; - ff = endSide ? 1 : -0.2; - if(Math.abs(tickangle) === 90) { - if(insideTickLabels) { - ff += MID_SHIFT; - } else { - if(tickangle === -90 && side === 'bottom') { - ff = CAP_SHIFT; - } else if(tickangle === 90 && side === 'top') { - ff = MID_SHIFT; - } else { - ff = 0.5; - } - } - - xQ = (MID_SHIFT / 2) * (tickangle / 90); - } - - out.xFn = function(d) { return d.dx + x0 + xQ * d.fontSize; }; - out.yFn = function(d) { return d.dy + y0 + d.fontSize * ff; }; - out.anchorFn = function(d, a) { - if(isAligned) { - if(isLeft) return 'end'; - if(isRight) return 'start'; - } - - if(!isNumeric(a) || a === 0 || a === 180) { - return 'middle'; - } - - return ((a * flipIt < 0) !== insideTickLabels) ? 'end' : 'start'; - }; - out.heightFn = function(d, a, h) { - return (a < -60 || a > 60) ? -0.5 * h : - ((ax.side === 'top') !== insideTickLabels) ? -h : - 0; - }; - } else if(axLetter === 'y') { - endSide = - (!insideTickLabels && side === 'left') || - (insideTickLabels && side === 'right'); - - flipIt = endSide ? 1 : -1; - if(insideTickLabels) flipIt *= -1; + if (!isNumeric(a) || a === 0 || a === 180) { + return "middle"; + } - x0 = labelStandoff; - y0 = labelShift * flipIt; + return a * flipIt < 0 !== insideTickLabels ? "end" : "start"; + }; + out.heightFn = function (d, a, h) { + return a < -60 || a > 60 + ? -0.5 * h + : (ax.side === "top") !== insideTickLabels + ? -h + : 0; + }; + } else if (axLetter === "y") { + endSide = + (!insideTickLabels && side === "left") || + (insideTickLabels && side === "right"); + + flipIt = endSide ? 1 : -1; + if (insideTickLabels) flipIt *= -1; + + x0 = labelStandoff; + y0 = labelShift * flipIt; + ff = 0; + if (!insideTickLabels && Math.abs(tickangle) === 90) { + if ( + (tickangle === -90 && side === "left") || + (tickangle === 90 && side === "right") + ) { + ff = CAP_SHIFT; + } else { + ff = 0.5; + } + } + + if (insideTickLabels) { + var ang = isNumeric(tickangle) ? +tickangle : 0; + if (ang !== 0) { + var rA = Lib.deg2rad(ang); + xQ = Math.abs(Math.sin(rA)) * CAP_SHIFT * flipIt; ff = 0; - if(!insideTickLabels && Math.abs(tickangle) === 90) { - if( - (tickangle === -90 && side === 'left') || - (tickangle === 90 && side === 'right') - ) { - ff = CAP_SHIFT; - } else { - ff = 0.5; - } - } - - if(insideTickLabels) { - var ang = isNumeric(tickangle) ? +tickangle : 0; - if(ang !== 0) { - var rA = Lib.deg2rad(ang); - xQ = Math.abs(Math.sin(rA)) * CAP_SHIFT * flipIt; - ff = 0; - } - } + } + } - out.xFn = function(d) { return d.dx + shift - (x0 + d.fontSize * ff) * flipIt + xQ * d.fontSize; }; - out.yFn = function(d) { return d.dy + y0 + d.fontSize * MID_SHIFT; }; - out.anchorFn = function(d, a) { - if(isNumeric(a) && Math.abs(a) === 90) { - return 'middle'; - } + out.xFn = function (d) { + return d.dx + shift - (x0 + d.fontSize * ff) * flipIt + xQ * d.fontSize; + }; + out.yFn = function (d) { + return d.dy + y0 + d.fontSize * MID_SHIFT; + }; + out.anchorFn = function (d, a) { + if (isNumeric(a) && Math.abs(a) === 90) { + return "middle"; + } - return endSide ? 'end' : 'start'; - }; - out.heightFn = function(d, a, h) { - if(ax.side === 'right') a *= -1; + return endSide ? "end" : "start"; + }; + out.heightFn = function (d, a, h) { + if (ax.side === "right") a *= -1; - return a < -30 ? -h : - a < 30 ? -0.5 * h : - 0; - }; - } + return a < -30 ? -h : a < 30 ? -0.5 * h : 0; + }; + } - return out; + return out; }; function tickDataFn(d) { - return [d.text, d.x, d.axInfo, d.font, d.fontSize, d.fontColor].join('_'); + return [d.text, d.x, d.axInfo, d.font, d.fontSize, d.fontColor].join("_"); } /** @@ -3251,48 +3521,57 @@ function tickDataFn(d) { * - {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 vals = [] - .concat(ax.minor && ax.minor.ticks ? - // minor vals - opts.vals.filter(function(d) { return d.minor && !d.noTick; }) : - [] - ) - .concat(ax.ticks ? - // major vals - opts.vals.filter(function(d) { return !d.minor && !d.noTick; }) : - [] - ); +axes.drawTicks = function (gd, ax, opts) { + opts = opts || {}; + + var cls = ax._id + "tick"; + + var vals = [] + .concat( + ax.minor && ax.minor.ticks + ? // minor vals + opts.vals.filter(function (d) { + return d.minor && !d.noTick; + }) + : [] + ) + .concat( + ax.ticks + ? // major vals + opts.vals.filter(function (d) { + return !d.minor && !d.noTick; + }) + : [] + ); - var ticks = opts.layer.selectAll('path.' + cls) - .data(vals, tickDataFn); - - ticks.exit().remove(); - - ticks.enter().append('path') - .classed(cls, 1) - .classed('ticks', 1) - .classed('crisp', opts.crisp !== false) - .each(function(d) { - return Color.stroke(d3.select(this), d.minor ? ax.minor.tickcolor : ax.tickcolor); - }) - .style('stroke-width', function(d) { - return Drawing.crispRound( - gd, - d.minor ? ax.minor.tickwidth : ax.tickwidth, - 1 - ) + 'px'; - }) - .attr('d', opts.path) - .style('display', null); // visible - - hideCounterAxisInsideTickLabels(ax, [TICK_PATH]); - - ticks.attr('transform', opts.transFn); + var ticks = opts.layer.selectAll("path." + cls).data(vals, tickDataFn); + + ticks.exit().remove(); + + ticks + .enter() + .append("path") + .classed(cls, 1) + .classed("ticks", 1) + .classed("crisp", opts.crisp !== false) + .each(function (d) { + return Color.stroke( + d3.select(this), + d.minor ? ax.minor.tickcolor : ax.tickcolor + ); + }) + .style("stroke-width", function (d) { + return ( + Drawing.crispRound(gd, d.minor ? ax.minor.tickwidth : ax.tickwidth, 1) + + "px" + ); + }) + .attr("d", opts.path) + .style("display", null); // visible + + hideCounterAxisInsideTickLabels(ax, [TICK_PATH]); + + ticks.attr("transform", opts.transFn); }; /** @@ -3317,80 +3596,90 @@ axes.drawTicks = function(gd, ax, opts) { * - {fn} transFn * - {boolean} crisp (set to false to unset crisp-edge SVG rendering) */ -axes.drawGrid = function(gd, ax, opts) { - opts = opts || {}; - - if(ax.tickmode === 'sync') { - // for tickmode sync we use the overlaying axis grid - return; - } - - var cls = ax._id + 'grid'; - - var hasMinor = ax.minor && ax.minor.showgrid; - var minorVals = hasMinor ? opts.vals.filter(function(d) { return d.minor; }) : []; - var majorVals = ax.showgrid ? opts.vals.filter(function(d) { return !d.minor; }) : []; - - var counterAx = opts.counterAxis; - if(counterAx && axes.shouldShowZeroLine(gd, ax, counterAx)) { - var isArrayMode = ax.tickmode === 'array'; - for(var i = 0; i < majorVals.length; i++) { - var xi = majorVals[i].x; - if(isArrayMode ? !xi : (Math.abs(xi) < ax.dtick / 100)) { - majorVals = majorVals.slice(0, i).concat(majorVals.slice(i + 1)); - // In array mode you can in principle have multiple - // ticks at 0, so test them all. Otherwise once we found - // one we can stop. - if(isArrayMode) i--; - else break; - } - } - } +axes.drawGrid = function (gd, ax, opts) { + opts = opts || {}; + + if (ax.tickmode === "sync") { + // for tickmode sync we use the overlaying axis grid + return; + } + + var cls = ax._id + "grid"; + + var hasMinor = ax.minor && ax.minor.showgrid; + var minorVals = hasMinor + ? opts.vals.filter(function (d) { + return d.minor; + }) + : []; + var majorVals = ax.showgrid + ? opts.vals.filter(function (d) { + return !d.minor; + }) + : []; + + var counterAx = opts.counterAxis; + if (counterAx && axes.shouldShowZeroLine(gd, ax, counterAx)) { + var isArrayMode = ax.tickmode === "array"; + for (var i = 0; i < majorVals.length; i++) { + var xi = majorVals[i].x; + if (isArrayMode ? !xi : Math.abs(xi) < ax.dtick / 100) { + majorVals = majorVals.slice(0, i).concat(majorVals.slice(i + 1)); + // In array mode you can in principle have multiple + // ticks at 0, so test them all. Otherwise once we found + // one we can stop. + if (isArrayMode) i--; + else break; + } + } + } + + ax._gw = Drawing.crispRound(gd, ax.gridwidth, 1); + + var wMinor = !hasMinor ? 0 : Drawing.crispRound(gd, ax.minor.gridwidth, 1); + + var majorLayer = opts.layer; + var minorLayer = opts.minorLayer; + for (var major = 1; major >= 0; major--) { + var layer = major ? majorLayer : minorLayer; + if (!layer) continue; + + var grid = layer + .selectAll("path." + cls) + .data(major ? majorVals : minorVals, tickDataFn); + + grid.exit().remove(); + + grid + .enter() + .append("path") + .classed(cls, 1) + .classed("crisp", opts.crisp !== false); + + grid + .attr("transform", opts.transFn) + .attr("d", opts.path) + .each(function (d) { + return Color.stroke( + d3.select(this), + d.minor ? ax.minor.gridcolor : ax.gridcolor || "#ddd" + ); + }) + .style("stroke-dasharray", function (d) { + return Drawing.dashStyle( + d.minor ? ax.minor.griddash : ax.griddash, + d.minor ? ax.minor.gridwidth : ax.gridwidth + ); + }) + .style("stroke-width", function (d) { + return (d.minor ? wMinor : ax._gw) + "px"; + }) + .style("display", null); // visible - ax._gw = - Drawing.crispRound(gd, ax.gridwidth, 1); - - var wMinor = !hasMinor ? 0 : - Drawing.crispRound(gd, ax.minor.gridwidth, 1); - - var majorLayer = opts.layer; - var minorLayer = opts.minorLayer; - for(var major = 1; major >= 0; major--) { - var layer = major ? majorLayer : minorLayer; - if(!layer) continue; - - var grid = layer.selectAll('path.' + cls) - .data(major ? majorVals : minorVals, tickDataFn); - - grid.exit().remove(); - - grid.enter().append('path') - .classed(cls, 1) - .classed('crisp', opts.crisp !== false); - - grid.attr('transform', opts.transFn) - .attr('d', opts.path) - .each(function(d) { - return Color.stroke(d3.select(this), d.minor ? - ax.minor.gridcolor : - (ax.gridcolor || '#ddd') - ); - }) - .style('stroke-dasharray', function(d) { - return Drawing.dashStyle( - d.minor ? ax.minor.griddash : ax.griddash, - d.minor ? ax.minor.gridwidth : ax.gridwidth - ); - }) - .style('stroke-width', function(d) { - return (d.minor ? wMinor : ax._gw) + 'px'; - }) - .style('display', null); // visible - - if(typeof opts.path === 'function') grid.attr('d', opts.path); - } + if (typeof opts.path === "function") grid.attr("d", opts.path); + } - hideCounterAxisInsideTickLabels(ax, [GRID_PATH, MINORGRID_PATH]); + hideCounterAxisInsideTickLabels(ax, [GRID_PATH, MINORGRID_PATH]); }; /** @@ -3410,37 +3699,42 @@ axes.drawGrid = function(gd, ax, opts) { * - {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) - .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 idSort(da.id, db.id); - }); - }); +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) + .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 idSort(da.id, db.id); + }); + }); - zl.attr('transform', opts.transFn) - .attr('d', opts.path) - .call(Color.stroke, ax.zerolinecolor || Color.defaultLine) - .style('stroke-width', Drawing.crispRound(gd, ax.zerolinewidth, ax._gw || 1) + 'px') - .style('display', null); // visible + zl.attr("transform", opts.transFn) + .attr("d", opts.path) + .call(Color.stroke, ax.zerolinecolor || Color.defaultLine) + .style( + "stroke-width", + Drawing.crispRound(gd, ax.zerolinewidth, ax._gw || 1) + "px" + ) + .style("display", null); // visible - hideCounterAxisInsideTickLabels(ax, [ZERO_PATH]); + hideCounterAxisInsideTickLabels(ax, [ZERO_PATH]); }; /** @@ -3467,467 +3761,499 @@ axes.drawZeroLine = function(gd, ax, opts) { * + {fn} anchorFn * + {fn} heightFn */ -axes.drawLabels = function(gd, ax, opts) { - opts = opts || {}; - - var fullLayout = gd._fullLayout; - var axId = ax._id; - var axLetter = axId.charAt(0); - var cls = opts.cls || axId + 'tick'; - - var vals = opts.vals.filter(function(d) { return d.text; }); - - var labelFns = opts.labelFns; - var tickAngle = opts.secondary ? 0 : ax.tickangle; - var prevAngle = (ax._prevTickAngles || {})[cls]; - - var tickLabels = opts.layer.selectAll('g.' + cls) - .data(ax.showticklabels ? vals : [], tickDataFn); - - 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) { - var thisLabel = d3.select(this); - var newPromise = gd._promises.length; - - thisLabel - .call(svgTextUtils.positionText, labelFns.xFn(d), labelFns.yFn(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, tickAngle); - })); - } else { - // sync label: just position it now. - positionLabels(thisLabel, tickAngle); - } - }); - - hideCounterAxisInsideTickLabels(ax, [TICK_TEXT]); +axes.drawLabels = function (gd, ax, opts) { + opts = opts || {}; + + var fullLayout = gd._fullLayout; + var axId = ax._id; + var axLetter = axId.charAt(0); + var cls = opts.cls || axId + "tick"; + + var vals = opts.vals.filter(function (d) { + return d.text; + }); + + var labelFns = opts.labelFns; + var tickAngle = opts.secondary ? 0 : ax.tickangle; + var prevAngle = (ax._prevTickAngles || {})[cls]; + + var tickLabels = opts.layer + .selectAll("g." + cls) + .data(ax.showticklabels ? vals : [], tickDataFn); + + 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) { + var thisLabel = d3.select(this); + var newPromise = gd._promises.length; + + thisLabel + .call(svgTextUtils.positionText, labelFns.xFn(d), labelFns.yFn(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, tickAngle); + }) + ); + } else { + // sync label: just position it now. + positionLabels(thisLabel, tickAngle); + } + }); - tickLabels.exit().remove(); + hideCounterAxisInsideTickLabels(ax, [TICK_TEXT]); - if(opts.repositionOnUpdate) { - tickLabels.each(function(d) { - d3.select(this).select('text') - .call(svgTextUtils.positionText, labelFns.xFn(d), labelFns.yFn(d)); - }); - } + tickLabels.exit().remove(); - function positionLabels(s, angle) { - s.each(function(d) { - var thisLabel = d3.select(this); - var mathjaxGroup = thisLabel.select('.text-math-group'); - var anchor = labelFns.anchorFn(d, angle); - - var transform = opts.transFn.call(thisLabel.node(), d) + - ((isNumeric(angle) && +angle !== 0) ? - (' rotate(' + angle + ',' + labelFns.xFn(d) + ',' + - (labelFns.yFn(d) - d.fontSize / 2) + ')') : - ''); - - // how much to shift a multi-line label to center it vertically. - var nLines = svgTextUtils.lineCount(thisLabel); - var lineHeight = LINE_SPACING * d.fontSize; - var anchorHeight = labelFns.heightFn(d, isNumeric(angle) ? +angle : 0, (nLines - 1) * lineHeight); - - if(anchorHeight) { - transform += strTranslate(0, anchorHeight); - } - - if(mathjaxGroup.empty()) { - var thisText = thisLabel.select('text'); - thisText.attr({ - transform: transform, - 'text-anchor': anchor - }); - - thisText.style('opacity', 1); // visible - - if(ax._adjustTickLabelsOverflow) { - ax._adjustTickLabelsOverflow(); - } - } else { - var mjWidth = Drawing.bBox(mathjaxGroup.node()).width; - var mjShift = mjWidth * {end: -0.5, start: 0.5}[anchor]; - mathjaxGroup.attr('transform', transform + strTranslate(mjShift, 0)); - } + if (opts.repositionOnUpdate) { + tickLabels.each(function (d) { + d3.select(this) + .select("text") + .call(svgTextUtils.positionText, labelFns.xFn(d), labelFns.yFn(d)); + }); + } + + function positionLabels(s, angle) { + s.each(function (d) { + var thisLabel = d3.select(this); + var mathjaxGroup = thisLabel.select(".text-math-group"); + var anchor = labelFns.anchorFn(d, angle); + + var transform = + opts.transFn.call(thisLabel.node(), d) + + (isNumeric(angle) && +angle !== 0 + ? " rotate(" + + angle + + "," + + labelFns.xFn(d) + + "," + + (labelFns.yFn(d) - d.fontSize / 2) + + ")" + : ""); + + // how much to shift a multi-line label to center it vertically. + var nLines = svgTextUtils.lineCount(thisLabel); + var lineHeight = LINE_SPACING * d.fontSize; + var anchorHeight = labelFns.heightFn( + d, + isNumeric(angle) ? +angle : 0, + (nLines - 1) * lineHeight + ); + + if (anchorHeight) { + transform += strTranslate(0, anchorHeight); + } + + if (mathjaxGroup.empty()) { + var thisText = thisLabel.select("text"); + thisText.attr({ + transform: transform, + "text-anchor": anchor, }); - } - - ax._adjustTickLabelsOverflow = function() { - var ticklabeloverflow = ax.ticklabeloverflow; - if(!ticklabeloverflow || ticklabeloverflow === 'allow') return; - var hideOverflow = ticklabeloverflow.indexOf('hide') !== -1; + thisText.style("opacity", 1); // visible - var isX = ax._id.charAt(0) === 'x'; - // div positions - var p0 = 0; - var p1 = isX ? - gd._fullLayout.width : - gd._fullLayout.height; - - if(ticklabeloverflow.indexOf('domain') !== -1) { - // domain positions - var rl = Lib.simpleMap(ax.range, ax.r2l); - p0 = ax.l2p(rl[0]) + ax._offset; - p1 = ax.l2p(rl[1]) + ax._offset; + if (ax._adjustTickLabelsOverflow) { + ax._adjustTickLabelsOverflow(); } + } else { + var mjWidth = Drawing.bBox(mathjaxGroup.node()).width; + var mjShift = mjWidth * { end: -0.5, start: 0.5 }[anchor]; + mathjaxGroup.attr("transform", transform + strTranslate(mjShift, 0)); + } + }); + } - var min = Math.min(p0, p1); - var max = Math.max(p0, p1); - - var side = ax.side; - - var visibleLabelMin = Infinity; - var visibleLabelMax = -Infinity; - - tickLabels.each(function(d) { - var thisLabel = d3.select(this); - var mathjaxGroup = thisLabel.select('.text-math-group'); - - if(mathjaxGroup.empty()) { - var bb = Drawing.bBox(thisLabel.node()); - var adjust = 0; - if(isX) { - if(bb.right > max) adjust = 1; - else if(bb.left < min) adjust = 1; - } else { - if(bb.bottom > max) adjust = 1; - else if(bb.top + (ax.tickangle ? 0 : d.fontSize / 4) < min) adjust = 1; - } - - var t = thisLabel.select('text'); - if(adjust) { - if(hideOverflow) t.style('opacity', 0); // hidden - } else { - t.style('opacity', 1); // visible - - if(side === 'bottom' || side === 'right') { - visibleLabelMin = Math.min(visibleLabelMin, isX ? bb.top : bb.left); - } else { - visibleLabelMin = -Infinity; - } - - if(side === 'top' || side === 'left') { - visibleLabelMax = Math.max(visibleLabelMax, isX ? bb.bottom : bb.right); - } else { - visibleLabelMax = Infinity; - } - } - } // TODO: hide mathjax? - }); + ax._adjustTickLabelsOverflow = function () { + var ticklabeloverflow = ax.ticklabeloverflow; + if (!ticklabeloverflow || ticklabeloverflow === "allow") return; - for(var subplot in fullLayout._plots) { - var plotinfo = fullLayout._plots[subplot]; - if(ax._id !== plotinfo.xaxis._id && ax._id !== plotinfo.yaxis._id) continue; - var anchorAx = isX ? plotinfo.yaxis : plotinfo.xaxis; - if(anchorAx) { - anchorAx['_visibleLabelMin_' + ax._id] = visibleLabelMin; - anchorAx['_visibleLabelMax_' + ax._id] = visibleLabelMax; - } - } - }; + var hideOverflow = ticklabeloverflow.indexOf("hide") !== -1; - ax._hideCounterAxisInsideTickLabels = function(partialOpts) { - var isX = ax._id.charAt(0) === 'x'; + var isX = ax._id.charAt(0) === "x"; + // div positions + var p0 = 0; + var p1 = isX ? gd._fullLayout.width : gd._fullLayout.height; - var anchoredAxes = []; - for(var subplot in fullLayout._plots) { - var plotinfo = fullLayout._plots[subplot]; - if(ax._id !== plotinfo.xaxis._id && ax._id !== plotinfo.yaxis._id) continue; - anchoredAxes.push(isX ? plotinfo.yaxis : plotinfo.xaxis); - } + if (ticklabeloverflow.indexOf("domain") !== -1) { + // domain positions + var rl = Lib.simpleMap(ax.range, ax.r2l); + p0 = ax.l2p(rl[0]) + ax._offset; + p1 = ax.l2p(rl[1]) + ax._offset; + } - anchoredAxes.forEach(function(anchorAx, idx) { - if(anchorAx && insideTicklabelposition(anchorAx)) { - (partialOpts || [ - ZERO_PATH, - MINORGRID_PATH, - GRID_PATH, - TICK_PATH, - TICK_TEXT - ]).forEach(function(e) { - var isPeriodLabel = - e.K === 'tick' && - e.L === 'text' && - ax.ticklabelmode === 'period'; - - var mainPlotinfo = fullLayout._plots[ax._mainSubplot]; - - var sel; - if(e.K === ZERO_PATH.K) sel = mainPlotinfo.zerolinelayer.selectAll('.' + ax._id + 'zl'); - else if(e.K === MINORGRID_PATH.K) sel = mainPlotinfo.minorGridlayer.selectAll('.' + ax._id); - else if(e.K === GRID_PATH.K) sel = mainPlotinfo.gridlayer.selectAll('.' + ax._id); - else sel = mainPlotinfo[ax._id.charAt(0) + 'axislayer']; - - sel.each(function() { - var w = d3.select(this); - if(e.L) w = w.selectAll(e.L); - - w.each(function(d) { - var q = ax.l2p( - isPeriodLabel ? getPosX(d) : d.x - ) + ax._offset; - - var t = d3.select(this); - if( - q < ax['_visibleLabelMax_' + anchorAx._id] && - q > ax['_visibleLabelMin_' + anchorAx._id] - ) { - t.style('display', 'none'); // hidden - } else if(e.K === 'tick' && !idx) { - t.style('display', null); // visible - } - }); - }); - }); - } - }); - }; + var min = Math.min(p0, p1); + var max = Math.max(p0, p1); - // 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, (prevAngle + 1) ? prevAngle : tickAngle); + var side = ax.side; - function allLabelsReady() { - return labelsReady.length && Promise.all(labelsReady); - } + var visibleLabelMin = Infinity; + var visibleLabelMax = -Infinity; - var autoangle = null; - - function fixLabelOverlaps() { - positionLabels(tickLabels, tickAngle); - - // check for auto-angling if x labels overlap - // don't auto-angle at all for log axes with - // base and digit format - if(vals.length && axLetter === 'x' && !isNumeric(tickAngle) && - (ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D') - ) { - autoangle = 0; - - var maxFontSize = 0; - var lbbArray = []; - var i; - - tickLabels.each(function(d) { - maxFontSize = Math.max(maxFontSize, d.fontSize); - - var x = ax.l2p(d.x); - var thisLabel = selectTickLabel(this); - 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 - }); - }); + tickLabels.each(function (d) { + var thisLabel = d3.select(this); + var mathjaxGroup = thisLabel.select(".text-math-group"); - if((ax.tickson === 'boundaries' || ax.showdividers) && !opts.secondary) { - var gap = 2; - if(ax.ticks) gap += ax.tickwidth / 2; - - // TODO should secondary labels also fall into this fix-overlap regime? - - for(i = 0; i < lbbArray.length; i++) { - var xbnd = vals[i].xbnd; - var lbb = lbbArray[i]; - if( - (xbnd[0] !== null && (lbb.left - ax.l2p(xbnd[0])) < gap) || - (xbnd[1] !== null && (ax.l2p(xbnd[1]) - lbb.right) < gap) - ) { - autoangle = 90; - break; - } - } - } else { - var vLen = vals.length; - var tickSpacing = Math.abs((vals[vLen - 1].x - vals[0].x) * ax._m) / (vLen - 1); - - var ticklabelposition = ax.ticklabelposition || ''; - var has = function(str) { - return ticklabelposition.indexOf(str) !== -1; - }; - var isTop = has('top'); - var isLeft = has('left'); - var isRight = has('right'); - var isBottom = has('bottom'); - var isAligned = isBottom || isLeft || isTop || isRight; - var pad = !isAligned ? 0 : - (ax.tickwidth || 0) + 2 * TEXTPAD; - - var rotate90 = (tickSpacing < maxFontSize * 2.5) || ax.type === 'multicategory' || ax._name === 'realaxis'; - - // any overlap at all - set 30 degrees or 90 degrees - for(i = 0; i < lbbArray.length - 1; i++) { - if(Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1], pad)) { - autoangle = rotate90 ? 90 : 30; - break; - } - } - } - - if(autoangle) { - positionLabels(tickLabels, autoangle); - } + if (mathjaxGroup.empty()) { + var bb = Drawing.bBox(thisLabel.node()); + var adjust = 0; + if (isX) { + if (bb.right > max) adjust = 1; + else if (bb.left < min) adjust = 1; + } else { + if (bb.bottom > max) adjust = 1; + else if (bb.top + (ax.tickangle ? 0 : d.fontSize / 4) < min) + adjust = 1; } - } - - if(ax._selections) { - ax._selections[cls] = tickLabels; - } - var seq = [allLabelsReady]; - - // N.B. during auto-margin redraws, if the axis fixed its label overlaps - // by rotating 90 degrees, do not attempt to re-fix its label overlaps - // as this can lead to infinite redraw loops! - if(ax.automargin && fullLayout._redrawFromAutoMarginCount && prevAngle === 90) { - autoangle = 90; - seq.push(function() { - positionLabels(tickLabels, prevAngle); - }); - } else { - seq.push(fixLabelOverlaps); - } + var t = thisLabel.select("text"); + if (adjust) { + if (hideOverflow) t.style("opacity", 0); // hidden + } else { + t.style("opacity", 1); // visible + + if (side === "bottom" || side === "right") { + visibleLabelMin = Math.min(visibleLabelMin, isX ? bb.top : bb.left); + } else { + visibleLabelMin = -Infinity; + } + + if (side === "top" || side === "left") { + visibleLabelMax = Math.max( + visibleLabelMax, + isX ? bb.bottom : bb.right + ); + } else { + visibleLabelMax = Infinity; + } + } + } // TODO: hide mathjax? + }); - // save current tick angle for future redraws - if(ax._tickAngles) { - seq.push(function() { - ax._tickAngles[cls] = autoangle === null ? - (isNumeric(tickAngle) ? tickAngle : 0) : - autoangle; - }); + for (var subplot in fullLayout._plots) { + var plotinfo = fullLayout._plots[subplot]; + if (ax._id !== plotinfo.xaxis._id && ax._id !== plotinfo.yaxis._id) + continue; + var anchorAx = isX ? plotinfo.yaxis : plotinfo.xaxis; + if (anchorAx) { + anchorAx["_visibleLabelMin_" + ax._id] = visibleLabelMin; + anchorAx["_visibleLabelMax_" + ax._id] = visibleLabelMax; + } } + }; - var computeTickLabelBoundingBoxes = function() { - var labelsMaxW = 0; - var labelsMaxH = 0; - tickLabels.each(function(d, i) { - var thisLabel = selectTickLabel(this); - var mathjaxGroup = thisLabel.select('.text-math-group'); - - if(mathjaxGroup.empty()) { - var bb; + ax._hideCounterAxisInsideTickLabels = function (partialOpts) { + var isX = ax._id.charAt(0) === "x"; - if(ax._vals[i]) { - bb = ax._vals[i].bb || Drawing.bBox(thisLabel.node()); - ax._vals[i].bb = bb; - } + var anchoredAxes = []; + for (var subplot in fullLayout._plots) { + var plotinfo = fullLayout._plots[subplot]; + if (ax._id !== plotinfo.xaxis._id && ax._id !== plotinfo.yaxis._id) + continue; + anchoredAxes.push(isX ? plotinfo.yaxis : plotinfo.xaxis); + } - labelsMaxW = Math.max(labelsMaxW, bb.width); - labelsMaxH = Math.max(labelsMaxH, bb.height); - } + anchoredAxes.forEach(function (anchorAx, idx) { + if (anchorAx && insideTicklabelposition(anchorAx)) { + ( + partialOpts || [ + ZERO_PATH, + MINORGRID_PATH, + GRID_PATH, + TICK_PATH, + TICK_TEXT, + ] + ).forEach(function (e) { + var isPeriodLabel = + e.K === "tick" && e.L === "text" && ax.ticklabelmode === "period"; + + var mainPlotinfo = fullLayout._plots[ax._mainSubplot]; + + var sel; + if (e.K === ZERO_PATH.K) + sel = mainPlotinfo.zerolinelayer.selectAll("." + ax._id + "zl"); + else if (e.K === MINORGRID_PATH.K) + sel = mainPlotinfo.minorGridlayer.selectAll("." + ax._id); + else if (e.K === GRID_PATH.K) + sel = mainPlotinfo.gridlayer.selectAll("." + ax._id); + else sel = mainPlotinfo[ax._id.charAt(0) + "axislayer"]; + + sel.each(function () { + var w = d3.select(this); + if (e.L) w = w.selectAll(e.L); + + w.each(function (d) { + var q = ax.l2p(isPeriodLabel ? getPosX(d) : d.x) + ax._offset; + + var t = d3.select(this); + if ( + q < ax["_visibleLabelMax_" + anchorAx._id] && + q > ax["_visibleLabelMin_" + anchorAx._id] + ) { + t.style("display", "none"); // hidden + } else if (e.K === "tick" && !idx) { + t.style("display", null); // visible + } + }); + }); }); - - return { - labelsMaxW: labelsMaxW, - labelsMaxH: labelsMaxH - }; - }; - - var anchorAx = ax._anchorAxis; - if( - anchorAx && (anchorAx.autorange || anchorAx.insiderange) && - insideTicklabelposition(ax) && - !isLinked(fullLayout, ax._id) + } + }); + }; + + // 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, prevAngle + 1 ? prevAngle : tickAngle); + + function allLabelsReady() { + return labelsReady.length && Promise.all(labelsReady); + } + + var autoangle = null; + + function fixLabelOverlaps() { + positionLabels(tickLabels, tickAngle); + + // check for auto-angling if x labels overlap + // don't auto-angle at all for log axes with + // base and digit format + if ( + vals.length && + axLetter === "x" && + !isNumeric(tickAngle) && + (ax.type !== "log" || String(ax.dtick).charAt(0) !== "D") ) { - if(!fullLayout._insideTickLabelsUpdaterange) { - fullLayout._insideTickLabelsUpdaterange = {}; + autoangle = 0; + + var maxFontSize = 0; + var lbbArray = []; + var i; + + tickLabels.each(function (d) { + maxFontSize = Math.max(maxFontSize, d.fontSize); + + var x = ax.l2p(d.x); + var thisLabel = selectTickLabel(this); + 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, + }); + }); + + if ((ax.tickson === "boundaries" || ax.showdividers) && !opts.secondary) { + var gap = 2; + if (ax.ticks) gap += ax.tickwidth / 2; + + // TODO should secondary labels also fall into this fix-overlap regime? + + for (i = 0; i < lbbArray.length; i++) { + var xbnd = vals[i].xbnd; + var lbb = lbbArray[i]; + if ( + (xbnd[0] !== null && lbb.left - ax.l2p(xbnd[0]) < gap) || + (xbnd[1] !== null && ax.l2p(xbnd[1]) - lbb.right < gap) + ) { + autoangle = 90; + break; + } } - - if(anchorAx.autorange) { - fullLayout._insideTickLabelsUpdaterange[anchorAx._name + '.autorange'] = anchorAx.autorange; - - seq.push(computeTickLabelBoundingBoxes); + } else { + var vLen = vals.length; + var tickSpacing = + Math.abs((vals[vLen - 1].x - vals[0].x) * ax._m) / (vLen - 1); + + var ticklabelposition = ax.ticklabelposition || ""; + var has = function (str) { + return ticklabelposition.indexOf(str) !== -1; + }; + var isTop = has("top"); + var isLeft = has("left"); + var isRight = has("right"); + var isBottom = has("bottom"); + var isAligned = isBottom || isLeft || isTop || isRight; + var pad = !isAligned ? 0 : (ax.tickwidth || 0) + 2 * TEXTPAD; + + var rotate90 = + tickSpacing < maxFontSize * 2.5 || + ax.type === "multicategory" || + ax._name === "realaxis"; + + // any overlap at all - set 30 degrees or 90 degrees + for (i = 0; i < lbbArray.length - 1; i++) { + if (Lib.bBoxIntersect(lbbArray[i], lbbArray[i + 1], pad)) { + autoangle = rotate90 ? 90 : 30; + break; + } } + } + + if (autoangle) { + positionLabels(tickLabels, autoangle); + } + } + } + + if (ax._selections) { + ax._selections[cls] = tickLabels; + } + + var seq = [allLabelsReady]; + + // N.B. during auto-margin redraws, if the axis fixed its label overlaps + // by rotating 90 degrees, do not attempt to re-fix its label overlaps + // as this can lead to infinite redraw loops! + if ( + ax.automargin && + fullLayout._redrawFromAutoMarginCount && + prevAngle === 90 + ) { + autoangle = 90; + seq.push(function () { + positionLabels(tickLabels, prevAngle); + }); + } else { + seq.push(fixLabelOverlaps); + } + + // save current tick angle for future redraws + if (ax._tickAngles) { + seq.push(function () { + ax._tickAngles[cls] = + autoangle === null ? (isNumeric(tickAngle) ? tickAngle : 0) : autoangle; + }); + } - if(anchorAx.insiderange) { - var BBs = computeTickLabelBoundingBoxes(); - var move = ax._id.charAt(0) === 'y' ? - BBs.labelsMaxW : - BBs.labelsMaxH; - - move += 2 * TEXTPAD; - - if(ax.ticklabelposition === 'inside') { - move += ax.ticklen || 0; - } - - var sgn = (ax.side === 'right' || ax.side === 'top') ? 1 : -1; - var index = sgn === 1 ? 1 : 0; - var otherIndex = sgn === 1 ? 0 : 1; - - var newRange = []; - newRange[otherIndex] = anchorAx.range[otherIndex]; - - var p0 = anchorAx.d2p(anchorAx.range[index]); - var p1 = anchorAx.d2p(anchorAx.range[otherIndex]); - var dist = Math.abs(p1 - p0); - if(dist - move > 0) { - dist -= move; - move *= 1 + move / dist; - } else { - move = 0; - } - - if(ax._id.charAt(0) !== 'y') move = -move; + var computeTickLabelBoundingBoxes = function () { + var labelsMaxW = 0; + var labelsMaxH = 0; + tickLabels.each(function (d, i) { + var thisLabel = selectTickLabel(this); + var mathjaxGroup = thisLabel.select(".text-math-group"); - newRange[index] = anchorAx.p2d( - anchorAx.d2p(anchorAx.range[index]) + - sgn * move - ); + if (mathjaxGroup.empty()) { + var bb; - // handle partial ranges in insiderange - if( - anchorAx.autorange === 'min' || - anchorAx.autorange === 'max reversed' - ) { - newRange[0] = null; - - anchorAx._rangeInitial0 = undefined; - anchorAx._rangeInitial1 = undefined; - } else if( - anchorAx.autorange === 'max' || - anchorAx.autorange === 'min reversed' - ) { - newRange[1] = null; - - anchorAx._rangeInitial0 = undefined; - anchorAx._rangeInitial1 = undefined; - } - - fullLayout._insideTickLabelsUpdaterange[anchorAx._name + '.range'] = newRange; + if (ax._vals[i]) { + bb = ax._vals[i].bb || Drawing.bBox(thisLabel.node()); + ax._vals[i].bb = bb; } - } - var done = Lib.syncOrAsync(seq); - if(done && done.then) gd._promises.push(done); - return done; + labelsMaxW = Math.max(labelsMaxW, bb.width); + labelsMaxH = Math.max(labelsMaxH, bb.height); + } + }); + + return { + labelsMaxW: labelsMaxW, + labelsMaxH: labelsMaxH, + }; + }; + + var anchorAx = ax._anchorAxis; + if ( + anchorAx && + (anchorAx.autorange || anchorAx.insiderange) && + insideTicklabelposition(ax) && + !isLinked(fullLayout, ax._id) + ) { + if (!fullLayout._insideTickLabelsUpdaterange) { + fullLayout._insideTickLabelsUpdaterange = {}; + } + + if (anchorAx.autorange) { + fullLayout._insideTickLabelsUpdaterange[anchorAx._name + ".autorange"] = + anchorAx.autorange; + + seq.push(computeTickLabelBoundingBoxes); + } + + if (anchorAx.insiderange) { + var BBs = computeTickLabelBoundingBoxes(); + var move = ax._id.charAt(0) === "y" ? BBs.labelsMaxW : BBs.labelsMaxH; + + move += 2 * TEXTPAD; + + if (ax.ticklabelposition === "inside") { + move += ax.ticklen || 0; + } + + var sgn = ax.side === "right" || ax.side === "top" ? 1 : -1; + var index = sgn === 1 ? 1 : 0; + var otherIndex = sgn === 1 ? 0 : 1; + + var newRange = []; + newRange[otherIndex] = anchorAx.range[otherIndex]; + + var p0 = anchorAx.d2p(anchorAx.range[index]); + var p1 = anchorAx.d2p(anchorAx.range[otherIndex]); + var dist = Math.abs(p1 - p0); + if (dist - move > 0) { + dist -= move; + move *= 1 + move / dist; + } else { + move = 0; + } + + if (ax._id.charAt(0) !== "y") move = -move; + + newRange[index] = anchorAx.p2d( + anchorAx.d2p(anchorAx.range[index]) + sgn * move + ); + + // handle partial ranges in insiderange + if ( + anchorAx.autorange === "min" || + anchorAx.autorange === "max reversed" + ) { + newRange[0] = null; + + anchorAx._rangeInitial0 = undefined; + anchorAx._rangeInitial1 = undefined; + } else if ( + anchorAx.autorange === "max" || + anchorAx.autorange === "min reversed" + ) { + newRange[1] = null; + + anchorAx._rangeInitial0 = undefined; + anchorAx._rangeInitial1 = undefined; + } + + fullLayout._insideTickLabelsUpdaterange[anchorAx._name + ".range"] = + newRange; + } + } + + var done = Lib.syncOrAsync(seq); + if (done && done.then) gd._promises.push(done); + return done; }; /** @@ -3946,23 +4272,28 @@ axes.drawLabels = function(gd, ax, opts) { * - {fn} transFn */ function drawDividers(gd, ax, opts) { - var cls = ax._id + 'divider'; - var vals = opts.vals; + var cls = ax._id + "divider"; + var vals = opts.vals; - var dividers = opts.layer.selectAll('path.' + cls) - .data(vals, tickDataFn); + var dividers = opts.layer.selectAll("path." + cls).data(vals, tickDataFn); + if (ax.type === "multicategory") { + if (opts.level === 0) { + dividers.exit().remove(); + } + } else { dividers.exit().remove(); + } - dividers.enter().insert('path', ':first-child') - .classed(cls, 1) - .classed('crisp', 1) - .call(Color.stroke, ax.dividercolor) - .style('stroke-width', Drawing.crispRound(gd, ax.dividerwidth, 1) + 'px'); + dividers + .enter() + .insert("path", ":first-child") + .classed(cls, 1) + .classed("crisp", 1) + .call(Color.stroke, ax.dividercolor) + .style("stroke-width", Drawing.crispRound(gd, ax.dividerwidth, 1) + "px"); - dividers - .attr('transform', opts.transFn) - .attr('d', opts.path); + dividers.attr("transform", opts.transFn).attr("d", opts.path); } /** @@ -3979,31 +4310,31 @@ function drawDividers(gd, ax, opts) { * - {number} position * @return {number} */ -axes.getPxPosition = function(gd, ax) { - var gs = gd._fullLayout._size; - var axLetter = ax._id.charAt(0); - var side = ax.side; - var anchorAxis; - - if(ax.anchor !== 'free') { - anchorAxis = ax._anchorAxis; - } else if(axLetter === 'x') { - anchorAxis = { - _offset: gs.t + (1 - (ax.position || 0)) * gs.h, - _length: 0 - }; - } else if(axLetter === 'y') { - anchorAxis = { - _offset: gs.l + (ax.position || 0) * gs.w + ax._shift, - _length: 0 - }; - } +axes.getPxPosition = function (gd, ax) { + var gs = gd._fullLayout._size; + var axLetter = ax._id.charAt(0); + var side = ax.side; + var anchorAxis; + + if (ax.anchor !== "free") { + anchorAxis = ax._anchorAxis; + } else if (axLetter === "x") { + anchorAxis = { + _offset: gs.t + (1 - (ax.position || 0)) * gs.h, + _length: 0, + }; + } else if (axLetter === "y") { + anchorAxis = { + _offset: gs.l + (ax.position || 0) * gs.w + ax._shift, + _length: 0, + }; + } - if(side === 'top' || side === 'left') { - return anchorAxis._offset; - } else if(side === 'bottom' || side === 'right') { - return anchorAxis._offset + anchorAxis._length; - } + if (side === "top" || side === "left") { + return anchorAxis._offset; + } else if (side === "bottom" || side === "right") { + return anchorAxis._offset + anchorAxis._length; + } }; /** @@ -4016,17 +4347,15 @@ axes.getPxPosition = function(gd, ax) { * @return {number} (in px) */ function approxTitleDepth(ax) { - var fontSize = ax.title.font.size; - var extraLines = (ax.title.text.match(svgTextUtils.BR_TAG_ALL) || []).length; - if(ax.title.hasOwnProperty('standoff')) { - return extraLines ? - fontSize * (CAP_SHIFT + (extraLines * LINE_SPACING)) : - fontSize * CAP_SHIFT; - } else { - return extraLines ? - fontSize * (extraLines + 1) * LINE_SPACING : - fontSize; - } + var fontSize = ax.title.font.size; + var extraLines = (ax.title.text.match(svgTextUtils.BR_TAG_ALL) || []).length; + if (ax.title.hasOwnProperty("standoff")) { + return extraLines + ? fontSize * (CAP_SHIFT + extraLines * LINE_SPACING) + : fontSize * CAP_SHIFT; + } else { + return extraLines ? fontSize * (extraLines + 1) * LINE_SPACING : fontSize; + } } /** @@ -4047,188 +4376,198 @@ function approxTitleDepth(ax) { * - {boolean} showticklabels */ function drawTitle(gd, ax) { - var fullLayout = gd._fullLayout; - var axId = ax._id; - var axLetter = axId.charAt(0); - var fontSize = ax.title.font.size; - var titleStandoff; - - if(ax.title.hasOwnProperty('standoff')) { - titleStandoff = ax._depth + ax.title.standoff + approxTitleDepth(ax); + var fullLayout = gd._fullLayout; + var axId = ax._id; + var axLetter = axId.charAt(0); + var fontSize = ax.title.font.size; + var titleStandoff; + + if (ax.title.hasOwnProperty("standoff")) { + titleStandoff = ax._depth + ax.title.standoff + approxTitleDepth(ax); + } else { + var isInside = insideTicklabelposition(ax); + + if (ax.type === "multicategory") { + titleStandoff = ax._depth; } else { - var isInside = insideTicklabelposition(ax); - - if(ax.type === 'multicategory') { - titleStandoff = ax._depth; - } else { - var offsetBase = 1.5 * fontSize; - if(isInside) { - offsetBase = 0.5 * fontSize; - if(ax.ticks === 'outside') { - offsetBase += ax.ticklen; - } - } - titleStandoff = 10 + offsetBase + (ax.linewidth ? ax.linewidth - 1 : 0); - } - - if(!isInside) { - if(axLetter === 'x') { - titleStandoff += ax.side === 'top' ? - fontSize * (ax.showticklabels ? 1 : 0) : - fontSize * (ax.showticklabels ? 1.5 : 0.5); - } else { - titleStandoff += ax.side === 'right' ? - fontSize * (ax.showticklabels ? 1 : 0.5) : - fontSize * (ax.showticklabels ? 0.5 : 0); - } + var offsetBase = 1.5 * fontSize; + if (isInside) { + offsetBase = 0.5 * fontSize; + if (ax.ticks === "outside") { + offsetBase += ax.ticklen; } - } - - var pos = axes.getPxPosition(gd, ax); - var transform, x, y; + } + titleStandoff = 10 + offsetBase + (ax.linewidth ? ax.linewidth - 1 : 0); + } + + if (!isInside) { + if (axLetter === "x") { + titleStandoff += + ax.side === "top" + ? fontSize * (ax.showticklabels ? 1 : 0) + : fontSize * (ax.showticklabels ? 1.5 : 0.5); + } else { + titleStandoff += + ax.side === "right" + ? fontSize * (ax.showticklabels ? 1 : 0.5) + : fontSize * (ax.showticklabels ? 0.5 : 0); + } + } + } + + var pos = axes.getPxPosition(gd, ax); + var transform, x, y; + + if (axLetter === "x") { + x = ax._offset + ax._length / 2; + y = ax.side === "top" ? pos - titleStandoff : pos + titleStandoff; + } else { + y = ax._offset + ax._length / 2; + x = ax.side === "right" ? pos + titleStandoff : pos - titleStandoff; + transform = { rotate: "-90", offset: 0 }; + } + + var avoid; + + if (ax.type !== "multicategory") { + var tickLabels = ax._selections[ax._id + "tick"]; + + avoid = { + selection: tickLabels, + side: ax.side, + }; - if(axLetter === 'x') { - x = ax._offset + ax._length / 2; - y = (ax.side === 'top') ? pos - titleStandoff : pos + titleStandoff; - } else { - y = ax._offset + ax._length / 2; - x = (ax.side === 'right') ? pos + titleStandoff : pos - titleStandoff; - transform = {rotate: '-90', offset: 0}; + if (tickLabels && tickLabels.node() && tickLabels.node().parentNode) { + var translation = Drawing.getTranslate(tickLabels.node().parentNode); + avoid.offsetLeft = translation.x; + avoid.offsetTop = translation.y; } - var avoid; - - if(ax.type !== 'multicategory') { - var tickLabels = ax._selections[ax._id + 'tick']; - - avoid = { - selection: tickLabels, - side: ax.side - }; - - if(tickLabels && tickLabels.node() && tickLabels.node().parentNode) { - var translation = Drawing.getTranslate(tickLabels.node().parentNode); - avoid.offsetLeft = translation.x; - avoid.offsetTop = translation.y; - } - - if(ax.title.hasOwnProperty('standoff')) { - avoid.pad = 0; - } + if (ax.title.hasOwnProperty("standoff")) { + avoid.pad = 0; } + } - ax._titleStandoff = titleStandoff; + ax._titleStandoff = titleStandoff; - return Titles.draw(gd, axId + 'title', { - propContainer: ax, - propName: ax._name + '.title.text', - placeholder: fullLayout._dfltTitle[axLetter], - avoid: avoid, - transform: transform, - attributes: {x: x, y: y, 'text-anchor': 'middle'} - }); + return Titles.draw(gd, axId + "title", { + propContainer: ax, + propName: ax._name + ".title.text", + placeholder: fullLayout._dfltTitle[axLetter], + avoid: avoid, + transform: transform, + attributes: { x: x, y: y, "text-anchor": "middle" }, + }); } -axes.shouldShowZeroLine = function(gd, ax, counterAxis) { - var rng = Lib.simpleMap(ax.range, ax.r2l); - return ( - (rng[0] * rng[1] <= 0) && - ax.zeroline && - (ax.type === 'linear' || ax.type === '-') && - !(ax.rangebreaks && ax.maskBreaks(0) === BADNUM) && - ( - clipEnds(ax, 0) || - !anyCounterAxLineAtZero(gd, ax, counterAxis, rng) || - hasBarsOrFill(gd, ax) - ) - ); +axes.shouldShowZeroLine = function (gd, ax, counterAxis) { + var rng = Lib.simpleMap(ax.range, ax.r2l); + return ( + rng[0] * rng[1] <= 0 && + ax.zeroline && + (ax.type === "linear" || ax.type === "-") && + !(ax.rangebreaks && ax.maskBreaks(0) === BADNUM) && + (clipEnds(ax, 0) || + !anyCounterAxLineAtZero(gd, ax, counterAxis, rng) || + hasBarsOrFill(gd, ax)) + ); }; -axes.clipEnds = function(ax, vals) { - return vals.filter(function(d) { return clipEnds(ax, d.x); }); +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); + var p = ax.l2p(l); + return p > 1 && p < ax._length - 1; } function anyCounterAxLineAtZero(gd, ax, counterAxis, rng) { - var mainCounterAxis = counterAxis._mainAxis; - if(!mainCounterAxis) return; - - var fullLayout = gd._fullLayout; - var axLetter = ax._id.charAt(0); - var counterLetter = axes.counterLetter(ax._id); - - var zeroPosition = ax._offset + ( - ((Math.abs(rng[0]) < Math.abs(rng[1])) === (axLetter === 'x')) ? - 0 : ax._length - ); + var mainCounterAxis = counterAxis._mainAxis; + if (!mainCounterAxis) return; - function lineNearZero(ax2) { - if(!ax2.showline || !ax2.linewidth) return false; - var tolerance = Math.max((ax2.linewidth + ax.zerolinewidth) / 2, 1); + var fullLayout = gd._fullLayout; + var axLetter = ax._id.charAt(0); + var counterLetter = axes.counterLetter(ax._id); - function closeEnough(pos2) { - return typeof pos2 === 'number' && Math.abs(pos2 - zeroPosition) < tolerance; - } + var zeroPosition = + ax._offset + + (Math.abs(rng[0]) < Math.abs(rng[1]) === (axLetter === "x") + ? 0 + : ax._length); - if(closeEnough(ax2._mainLinePosition) || closeEnough(ax2._mainMirrorPosition)) { - return true; - } - var linePositions = ax2._linepositions || {}; - for(var k in linePositions) { - if(closeEnough(linePositions[k][0]) || closeEnough(linePositions[k][1])) { - return true; - } - } - } + function lineNearZero(ax2) { + if (!ax2.showline || !ax2.linewidth) return false; + var tolerance = Math.max((ax2.linewidth + ax.zerolinewidth) / 2, 1); - var plotinfo = fullLayout._plots[counterAxis._mainSubplot]; - if(!(plotinfo.mainplotinfo || plotinfo).overlays.length) { - return lineNearZero(counterAxis, zeroPosition); + function closeEnough(pos2) { + return ( + typeof pos2 === "number" && Math.abs(pos2 - zeroPosition) < tolerance + ); } - var counterLetterAxes = axes.list(gd, counterLetter); - for(var i = 0; i < counterLetterAxes.length; i++) { - var counterAxis2 = counterLetterAxes[i]; - if( - counterAxis2._mainAxis === mainCounterAxis && - lineNearZero(counterAxis2, zeroPosition) - ) { - return true; - } + if ( + closeEnough(ax2._mainLinePosition) || + closeEnough(ax2._mainMirrorPosition) + ) { + return true; + } + var linePositions = ax2._linepositions || {}; + for (var k in linePositions) { + if ( + closeEnough(linePositions[k][0]) || + closeEnough(linePositions[k][1]) + ) { + return true; + } + } + } + + var plotinfo = fullLayout._plots[counterAxis._mainSubplot]; + if (!(plotinfo.mainplotinfo || plotinfo).overlays.length) { + return lineNearZero(counterAxis, zeroPosition); + } + + var counterLetterAxes = axes.list(gd, counterLetter); + for (var i = 0; i < counterLetterAxes.length; i++) { + var counterAxis2 = counterLetterAxes[i]; + if ( + counterAxis2._mainAxis === mainCounterAxis && + lineNearZero(counterAxis2, zeroPosition) + ) { + return true; } + } } function hasBarsOrFill(gd, ax) { - var fullData = gd._fullData; - var subplot = ax._mainSubplot; - var axLetter = ax._id.charAt(0); - - for(var i = 0; i < fullData.length; i++) { - var trace = fullData[i]; - - if(trace.visible === true && (trace.xaxis + trace.yaxis) === subplot) { - if( - Registry.traceIs(trace, 'bar-like') && - trace.orientation === {x: 'h', y: 'v'}[axLetter] - ) return true; - - if( - trace.fill && - trace.fill.charAt(trace.fill.length - 1) === axLetter - ) return true; - } + var fullData = gd._fullData; + var subplot = ax._mainSubplot; + var axLetter = ax._id.charAt(0); + + for (var i = 0; i < fullData.length; i++) { + var trace = fullData[i]; + + if (trace.visible === true && trace.xaxis + trace.yaxis === subplot) { + if ( + Registry.traceIs(trace, "bar-like") && + trace.orientation === { x: "h", y: "v" }[axLetter] + ) + return true; + + if (trace.fill && trace.fill.charAt(trace.fill.length - 1) === axLetter) + return true; } - return false; + } + return false; } function selectTickLabel(gTick) { - var s = d3.select(gTick); - var mj = s.select('.text-math-group'); - return mj.empty() ? s.select('text') : mj; + var s = d3.select(gTick); + var mj = s.select(".text-math-group"); + return mj.empty() ? s.select("text") : mj; } /** @@ -4240,215 +4579,245 @@ function selectTickLabel(gTick) { * We're probably also doing multiple redraws in this case, would be faster * if we can just do the whole calculation ahead of time and draw once. */ -axes.allowAutoMargin = function(gd) { - var axList = axes.list(gd, '', true); - for(var i = 0; i < axList.length; i++) { - var ax = axList[i]; - if(ax.automargin) { - Plots.allowAutoMargin(gd, axAutoMarginID(ax)); - if(ax.mirror) { - Plots.allowAutoMargin(gd, axMirrorAutoMarginID(ax)); - } - } - if(Registry.getComponentMethod('rangeslider', 'isVisible')(ax)) { - Plots.allowAutoMargin(gd, rangeSliderAutoMarginID(ax)); - } - } +axes.allowAutoMargin = function (gd) { + var axList = axes.list(gd, "", true); + for (var i = 0; i < axList.length; i++) { + var ax = axList[i]; + if (ax.automargin) { + Plots.allowAutoMargin(gd, axAutoMarginID(ax)); + if (ax.mirror) { + Plots.allowAutoMargin(gd, axMirrorAutoMarginID(ax)); + } + } + if (Registry.getComponentMethod("rangeslider", "isVisible")(ax)) { + Plots.allowAutoMargin(gd, rangeSliderAutoMarginID(ax)); + } + } }; -function axAutoMarginID(ax) { return ax._id + '.automargin'; } -function axMirrorAutoMarginID(ax) { return axAutoMarginID(ax) + '.mirror'; } -function rangeSliderAutoMarginID(ax) { return ax._id + '.rangeslider'; } +function axAutoMarginID(ax) { + return ax._id + ".automargin"; +} +function axMirrorAutoMarginID(ax) { + return axAutoMarginID(ax) + ".mirror"; +} +function rangeSliderAutoMarginID(ax) { + return ax._id + ".rangeslider"; +} // swap all the presentation attributes of the axes showing these traces -axes.swap = function(gd, traces) { - var axGroups = makeAxisGroups(gd, traces); +axes.swap = function (gd, traces) { + var axGroups = makeAxisGroups(gd, traces); - for(var i = 0; i < axGroups.length; i++) { - swapAxisGroup(gd, axGroups[i].x, axGroups[i].y); - } + for (var i = 0; i < axGroups.length; i++) { + swapAxisGroup(gd, axGroups[i].x, axGroups[i].y); + } }; function makeAxisGroups(gd, traces) { - var groups = []; - var i, j; - - for(i = 0; i < traces.length; i++) { - var groupsi = []; - var xi = gd._fullData[traces[i]].xaxis; - var yi = gd._fullData[traces[i]].yaxis; - if(!xi || !yi) continue; // not a 2D cartesian trace? - - for(j = 0; j < groups.length; j++) { - if(groups[j].x.indexOf(xi) !== -1 || groups[j].y.indexOf(yi) !== -1) { - groupsi.push(j); - } - } + var groups = []; + var i, j; - if(!groupsi.length) { - groups.push({x: [xi], y: [yi]}); - continue; - } + for (i = 0; i < traces.length; i++) { + var groupsi = []; + var xi = gd._fullData[traces[i]].xaxis; + var yi = gd._fullData[traces[i]].yaxis; + if (!xi || !yi) continue; // not a 2D cartesian trace? - var group0 = groups[groupsi[0]]; - var groupj; + for (j = 0; j < groups.length; j++) { + if (groups[j].x.indexOf(xi) !== -1 || groups[j].y.indexOf(yi) !== -1) { + groupsi.push(j); + } + } - if(groupsi.length > 1) { - for(j = 1; j < groupsi.length; j++) { - groupj = groups[groupsi[j]]; - mergeAxisGroups(group0.x, groupj.x); - mergeAxisGroups(group0.y, groupj.y); - } - } - mergeAxisGroups(group0.x, [xi]); - mergeAxisGroups(group0.y, [yi]); + if (!groupsi.length) { + groups.push({ x: [xi], y: [yi] }); + continue; + } + + var group0 = groups[groupsi[0]]; + var groupj; + + if (groupsi.length > 1) { + for (j = 1; j < groupsi.length; j++) { + groupj = groups[groupsi[j]]; + mergeAxisGroups(group0.x, groupj.x); + mergeAxisGroups(group0.y, groupj.y); + } } + mergeAxisGroups(group0.x, [xi]); + mergeAxisGroups(group0.y, [yi]); + } - return groups; + return groups; } function mergeAxisGroups(intoSet, fromSet) { - for(var i = 0; i < fromSet.length; i++) { - if(intoSet.indexOf(fromSet[i]) === -1) intoSet.push(fromSet[i]); - } + for (var i = 0; i < fromSet.length; i++) { + if (intoSet.indexOf(fromSet[i]) === -1) intoSet.push(fromSet[i]); + } } function swapAxisGroup(gd, xIds, yIds) { - var xFullAxes = []; - var yFullAxes = []; - var layout = gd.layout; - var i, j; - - for(i = 0; i < xIds.length; i++) xFullAxes.push(axes.getFromId(gd, xIds[i])); - for(i = 0; i < yIds.length; i++) yFullAxes.push(axes.getFromId(gd, yIds[i])); - - var allAxKeys = Object.keys(axAttrs); - - var noSwapAttrs = [ - 'anchor', 'domain', 'overlaying', 'position', 'side', 'tickangle', 'editType' - ]; - var numericTypes = ['linear', 'log']; - - for(i = 0; i < allAxKeys.length; i++) { - var keyi = allAxKeys[i]; - var xVal = xFullAxes[0][keyi]; - var yVal = yFullAxes[0][keyi]; - var allEqual = true; - var coerceLinearX = false; - var coerceLinearY = false; - if(keyi.charAt(0) === '_' || typeof xVal === 'function' || - noSwapAttrs.indexOf(keyi) !== -1) { - continue; - } - for(j = 1; j < xFullAxes.length && allEqual; j++) { - var xVali = xFullAxes[j][keyi]; - if(keyi === 'type' && numericTypes.indexOf(xVal) !== -1 && - numericTypes.indexOf(xVali) !== -1 && xVal !== xVali) { - // type is special - if we find a mixture of linear and log, - // coerce them all to linear on flipping - coerceLinearX = true; - } else if(xVali !== xVal) allEqual = false; - } - for(j = 1; j < yFullAxes.length && allEqual; j++) { - var yVali = yFullAxes[j][keyi]; - if(keyi === 'type' && numericTypes.indexOf(yVal) !== -1 && - numericTypes.indexOf(yVali) !== -1 && yVal !== yVali) { - // type is special - if we find a mixture of linear and log, - // coerce them all to linear on flipping - coerceLinearY = true; - } else if(yFullAxes[j][keyi] !== yVal) allEqual = false; - } - if(allEqual) { - if(coerceLinearX) layout[xFullAxes[0]._name].type = 'linear'; - if(coerceLinearY) layout[yFullAxes[0]._name].type = 'linear'; - swapAxisAttrs(layout, keyi, xFullAxes, yFullAxes, gd._fullLayout._dfltTitle); - } - } - - // now swap x&y for any annotations anchored to these x & y - for(i = 0; i < gd._fullLayout.annotations.length; i++) { - var ann = gd._fullLayout.annotations[i]; - if(xIds.indexOf(ann.xref) !== -1 && - yIds.indexOf(ann.yref) !== -1) { - Lib.swapAttrs(layout.annotations[i], ['?']); - } - } + var xFullAxes = []; + var yFullAxes = []; + var layout = gd.layout; + var i, j; + + for (i = 0; i < xIds.length; i++) xFullAxes.push(axes.getFromId(gd, xIds[i])); + for (i = 0; i < yIds.length; i++) yFullAxes.push(axes.getFromId(gd, yIds[i])); + + var allAxKeys = Object.keys(axAttrs); + + var noSwapAttrs = [ + "anchor", + "domain", + "overlaying", + "position", + "side", + "tickangle", + "editType", + ]; + var numericTypes = ["linear", "log"]; + + for (i = 0; i < allAxKeys.length; i++) { + var keyi = allAxKeys[i]; + var xVal = xFullAxes[0][keyi]; + var yVal = yFullAxes[0][keyi]; + var allEqual = true; + var coerceLinearX = false; + var coerceLinearY = false; + if ( + keyi.charAt(0) === "_" || + typeof xVal === "function" || + noSwapAttrs.indexOf(keyi) !== -1 + ) { + continue; + } + for (j = 1; j < xFullAxes.length && allEqual; j++) { + var xVali = xFullAxes[j][keyi]; + if ( + keyi === "type" && + numericTypes.indexOf(xVal) !== -1 && + numericTypes.indexOf(xVali) !== -1 && + xVal !== xVali + ) { + // type is special - if we find a mixture of linear and log, + // coerce them all to linear on flipping + coerceLinearX = true; + } else if (xVali !== xVal) allEqual = false; + } + for (j = 1; j < yFullAxes.length && allEqual; j++) { + var yVali = yFullAxes[j][keyi]; + if ( + keyi === "type" && + numericTypes.indexOf(yVal) !== -1 && + numericTypes.indexOf(yVali) !== -1 && + yVal !== yVali + ) { + // type is special - if we find a mixture of linear and log, + // coerce them all to linear on flipping + coerceLinearY = true; + } else if (yFullAxes[j][keyi] !== yVal) allEqual = false; + } + if (allEqual) { + if (coerceLinearX) layout[xFullAxes[0]._name].type = "linear"; + if (coerceLinearY) layout[yFullAxes[0]._name].type = "linear"; + swapAxisAttrs( + layout, + keyi, + xFullAxes, + yFullAxes, + gd._fullLayout._dfltTitle + ); + } + } + + // now swap x&y for any annotations anchored to these x & y + for (i = 0; i < gd._fullLayout.annotations.length; i++) { + var ann = gd._fullLayout.annotations[i]; + if (xIds.indexOf(ann.xref) !== -1 && yIds.indexOf(ann.yref) !== -1) { + Lib.swapAttrs(layout.annotations[i], ["?"]); + } + } } function swapAxisAttrs(layout, key, xFullAxes, yFullAxes, dfltTitle) { - // in case the value is the default for either axis, - // look at the first axis in each list and see if - // this key's value is undefined - var np = Lib.nestedProperty; - var xVal = np(layout[xFullAxes[0]._name], key).get(); - var yVal = np(layout[yFullAxes[0]._name], key).get(); - var i; - - if(key === 'title') { - // special handling of placeholder titles - if(xVal && xVal.text === dfltTitle.x) { - xVal.text = dfltTitle.y; - } - if(yVal && yVal.text === dfltTitle.y) { - yVal.text = dfltTitle.x; - } - } - - for(i = 0; i < xFullAxes.length; i++) { - np(layout, xFullAxes[i]._name + '.' + key).set(yVal); - } - for(i = 0; i < yFullAxes.length; i++) { - np(layout, yFullAxes[i]._name + '.' + key).set(xVal); - } + // in case the value is the default for either axis, + // look at the first axis in each list and see if + // this key's value is undefined + var np = Lib.nestedProperty; + var xVal = np(layout[xFullAxes[0]._name], key).get(); + var yVal = np(layout[yFullAxes[0]._name], key).get(); + var i; + + if (key === "title") { + // special handling of placeholder titles + if (xVal && xVal.text === dfltTitle.x) { + xVal.text = dfltTitle.y; + } + if (yVal && yVal.text === dfltTitle.y) { + yVal.text = dfltTitle.x; + } + } + + for (i = 0; i < xFullAxes.length; i++) { + np(layout, xFullAxes[i]._name + "." + key).set(yVal); + } + for (i = 0; i < yFullAxes.length; i++) { + np(layout, yFullAxes[i]._name + "." + key).set(xVal); + } } function isAngular(ax) { - return ax._id === 'angularaxis'; + return ax._id === "angularaxis"; } function moveOutsideBreak(v, ax) { - var len = ax._rangebreaks.length; - for(var k = 0; k < len; k++) { - var brk = ax._rangebreaks[k]; - if(v >= brk.min && v < brk.max) { - return brk.max; - } - } - return v; + var len = ax._rangebreaks.length; + for (var k = 0; k < len; k++) { + var brk = ax._rangebreaks[k]; + if (v >= brk.min && v < brk.max) { + return brk.max; + } + } + return v; } function insideTicklabelposition(ax) { - return ((ax.ticklabelposition || '').indexOf('inside') !== -1); + return (ax.ticklabelposition || "").indexOf("inside") !== -1; } function hideCounterAxisInsideTickLabels(ax, opts) { - if(insideTicklabelposition(ax._anchorAxis || {})) { - if(ax._hideCounterAxisInsideTickLabels) { - ax._hideCounterAxisInsideTickLabels(opts); - } + if (insideTicklabelposition(ax._anchorAxis || {})) { + if (ax._hideCounterAxisInsideTickLabels) { + ax._hideCounterAxisInsideTickLabels(opts); } + } } function incrementShift(ax, shiftVal, axShifts, normalize) { - // Need to set 'overlay' for anchored axis - var overlay = ((ax.anchor !== 'free') && ((ax.overlaying === undefined) || (ax.overlaying === false))) ? ax._id : ax.overlaying; - var shiftValAdj; - if(normalize) { - shiftValAdj = ax.side === 'right' ? shiftVal : -shiftVal; - } else { - shiftValAdj = shiftVal; - } - if(!(overlay in axShifts)) { - axShifts[overlay] = {}; - } - if(!(ax.side in axShifts[overlay])) { - axShifts[overlay][ax.side] = 0; - } - axShifts[overlay][ax.side] += shiftValAdj; + // Need to set 'overlay' for anchored axis + var overlay = + ax.anchor !== "free" && + (ax.overlaying === undefined || ax.overlaying === false) + ? ax._id + : ax.overlaying; + var shiftValAdj; + if (normalize) { + shiftValAdj = ax.side === "right" ? shiftVal : -shiftVal; + } else { + shiftValAdj = shiftVal; + } + if (!(overlay in axShifts)) { + axShifts[overlay] = {}; + } + if (!(ax.side in axShifts[overlay])) { + axShifts[overlay][ax.side] = 0; + } + axShifts[overlay][ax.side] += shiftValAdj; } function setShiftVal(ax, axShifts) { - return ax.autoshift ? - axShifts[ax.overlaying][ax.side] : - (ax.shift || 0); + return ax.autoshift ? axShifts[ax.overlaying][ax.side] : ax.shift || 0; } diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 14f17f9c2aa..8c167206b5d 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -156,9 +156,13 @@ module.exports = function setConvert(ax, fullLayout) { var arrayOut = new Array(len); for(var i = 0; i < len; i++) { - var v0 = (arrayIn[0] || [])[i]; - var v1 = (arrayIn[1] || [])[i]; - arrayOut[i] = getCategoryIndex([v0, v1]); + var vs = []; + for(var j = 0; j < ax.levelNr; j++) { + vs.push((arrayIn[j] || [])[i]); + } + // var v0 = (arrayIn[0] || [])[i]; + // var v1 = (arrayIn[1] || [])[i]; + arrayOut[i] = getCategoryIndex(vs); } return arrayOut; @@ -333,6 +337,7 @@ module.exports = function setConvert(ax, fullLayout) { // N.B. multicategory axes don't define d2c and d2l, // as 'data-to-calcdata' conversion needs to take into // account all data array items as in ax.makeCalcdata. + var sortLib = require('../../lib/sort_traces'); ax.r2d = ax.c2d = ax.l2d = getCategoryName; ax.d2r = ax.d2l_noadd = getCategoryPosition; @@ -357,9 +362,14 @@ module.exports = function setConvert(ax, fullLayout) { return ensureNumber(v); }; - ax.setupMultiCategory = function(fullData) { + ax.setupMultiCategory = function(gd) { + var fullData = gd._fullData; + // axes_test should set up category maps correctly for multicategory axes + if(!fullData) { + fullData = gd; + } var traceIndices = ax._traceIndices; - var i, j; + var i; var group = ax._matchGroup; if(group && ax._categories.length === 0) { @@ -371,49 +381,99 @@ module.exports = function setConvert(ax, fullLayout) { } } - // [ [cnt, {$cat: index}], for 1,2 ] - var seen = [[0, {}], [0, {}]]; - // [ [arrayIn[0][i], arrayIn[1][i]], for i .. N ] - var list = []; - + var axLabels = []; + var fullObjectList = []; + var cols = []; + // Don't think that the trace should be drawn at all if the lengths don't match. Removing the arrays length check. It is better to fail loudly than silently. for(i = 0; i < traceIndices.length; i++) { var trace = fullData[traceIndices[i]]; + cols = []; + + for(var k = 0; k < fullData[traceIndices[0]][axLetter].length; k++) { + cols.push('col' + k.toString()); + } + if(cols.length < 2) { + return; + } if(axLetter in trace) { var arrayIn = trace[axLetter]; - var len = trace._length || Lib.minRowLength(arrayIn); - - if(isArrayOrTypedArray(arrayIn[0]) && isArrayOrTypedArray(arrayIn[1])) { - for(j = 0; j < len; j++) { - var v0 = arrayIn[0][j]; - var v1 = arrayIn[1][j]; - - if(isValidCategory(v0) && isValidCategory(v1)) { - list.push([v0, v1]); - - if(!(v0 in seen[0][1])) { - seen[0][1][v0] = seen[0][0]++; - } - if(!(v1 in seen[1][1])) { - seen[1][1][v1] = seen[1][0]++; - } + if(isArrayOrTypedArray(arrayIn[0])) { + var arrays = arrayIn.map(function(x) { + return x; + }); + var valLetter; + if(trace.type === 'ohlc' | trace.type === 'candlestick') { + var t = trace; + var valsTransform = sortLib.transpose([t.open, t.high, t.low, t.close]); + arrays.push(valsTransform); + } else if(trace.z) { + if(axLetter === 'x') { + arrays.push(sortLib.transpose(trace.z)); + } else { + arrays.push(trace.z); } + valLetter = 'z'; + } else if(axLetter === 'y' && trace.x) { + arrays.push(trace.x); + valLetter = 'x'; + } else if(trace.y) { + arrays.push(trace.y); + valLetter = 'y'; + } else { + var nullArray = arrayIn[0].map(function() {return null;}); + arrays.push(nullArray); + } + var objList = sortLib.matrixToObjectList(arrays, cols); + + Array.prototype.push.apply(fullObjectList, objList); + + // convert the trace data from list to object and sort (backwards, stable sort) + var sortedObjectList = sortLib.sortObjectList(cols, objList); + var matrix = sortLib.objectListToList(sortedObjectList); + var sortedMatrix = sortLib.sortedMatrix(matrix); + + axLabels = sortedMatrix[0].slice(); + var axVals = sortedMatrix[1]; + + if(valLetter === 'z' & axLetter === 'x') { + axVals = sortLib.transpose(axVals); + } + + if(trace.type === 'ohlc' | trace.type === 'candlestick') { + var sortedValsTransform = sortLib.transpose(axVals); + gd._fullData[i].open = sortedValsTransform[0]; + gd._fullData[i].high = sortedValsTransform[1]; + gd._fullData[i].low = sortedValsTransform[2]; + gd._fullData[i].close = sortedValsTransform[3]; + } + // Could/should set sorted y axis values for each trace as the sorted values are already available. + // Need write access to gd._fullData, bad? Should probably be done right at newPlot, or on setting gd._fullData + + var transposedAxLabels = sortLib.transpose(axLabels); + if(gd._fullData) { + gd._fullData[i][axLetter] = transposedAxLabels; + } + if(valLetter) { + gd._fullData[i][valLetter] = axVals; } } } } - list.sort(function(a, b) { - var ind0 = seen[0][1]; - var d = ind0[a[0]] - ind0[b[0]]; - if(d) return d; + if(axLabels.length) { + ax.levelNr = axLabels[0].length; + ax.levels = axLabels[0].map(function(_, idx) {return idx;}); - var ind1 = seen[1][1]; - return ind1[a[1]] - ind1[b[1]]; - }); + var fullSortedObjectList = sortLib.sortObjectList(cols, fullObjectList.slice()); + var fullList = sortLib.objectListToList(fullSortedObjectList); + var fullSortedMatrix = sortLib.sortedMatrix(fullList, true); - for(i = 0; i < list.length; i++) { - setCategoryIndex(list[i]); + var fullXs = fullSortedMatrix[0].slice(); + + for(i = 0; i < fullXs.length; i++) { + setCategoryIndex(fullXs[i]); + } } }; } diff --git a/src/plots/plots.js b/src/plots/plots.js index 5a6b5e0466c..b32d05bd9e1 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -3136,7 +3136,7 @@ plots.doCalcdata = function(gd, traces) { calcdata[i] = cd; } - setupAxisCategories(axList, fullData, fullLayout); + setupAxisCategories(axList, gd, fullLayout); // 'transform' loop - must calc container traces first // so that if their dependent traces can get transform properly @@ -3144,7 +3144,7 @@ plots.doCalcdata = function(gd, traces) { for(i = 0; i < fullData.length; i++) transformCalci(i); // clear stuff that should recomputed in 'regular' loop - if(hasCalcTransform) setupAxisCategories(axList, fullData, fullLayout); + if(hasCalcTransform) setupAxisCategories(axList, gd, fullLayout); // 'regular' loop - make sure container traces (eg carpet) calc before // contained traces (eg contourcarpet) @@ -3352,13 +3352,13 @@ function sortAxisCategoriesByValue(axList, gd) { return affectedTraces; } -function setupAxisCategories(axList, fullData, fullLayout) { +function setupAxisCategories(axList, gd, fullLayout) { var axLookup = {}; function setupOne(ax) { ax.clearCalc(); if(ax.type === 'multicategory') { - ax.setupMultiCategory(fullData); + ax.setupMultiCategory(gd); } axLookup[ax._id] = 1; diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 165b90912f5..63ce83e9b53 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -6010,6 +6010,7 @@ describe('hovermode: (x|y)unified', function() { var mockOhlc = require('../../image/mocks/finance_multicategory.json'); var mockCopy = Lib.extendDeep({}, mockOhlc); mockCopy.layout.hovermode = 'x unified'; + Plotly.newPlot(gd, mockCopy) .then(function(gd) { _hover(gd, {curveNumber: 0, pointNumber: 0});