From 1baa14a960645b84565863662cad4d56c4ff2c18 Mon Sep 17 00:00:00 2001 From: "Yauhen_Kavaliou@epam.com" Date: Thu, 17 Aug 2017 18:59:26 +0300 Subject: [PATCH 01/16] add 'tickformatstops' --- src/components/colorbar/attributes.js | 1 + src/plots/cartesian/axes.js | 57 ++++++++++++++++++++- src/plots/cartesian/layout_attributes.js | 12 +++++ src/plots/cartesian/tick_label_defaults.js | 1 + src/plots/gl3d/layout/axis_attributes.js | 1 + src/plots/ternary/layout/axis_attributes.js | 1 + src/traces/carpet/axis_attributes.js | 12 +++++ tasks/util/strict_d3.js | 2 +- 8 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/components/colorbar/attributes.js b/src/components/colorbar/attributes.js index 62f1e031ff8..5a2e14afb2c 100644 --- a/src/components/colorbar/attributes.js +++ b/src/components/colorbar/attributes.js @@ -163,6 +163,7 @@ module.exports = { tickfont: axesAttrs.tickfont, tickangle: axesAttrs.tickangle, tickformat: axesAttrs.tickformat, + tickformatstops: axesAttrs.tickformatstops, tickprefix: axesAttrs.tickprefix, showtickprefix: axesAttrs.showtickprefix, ticksuffix: axesAttrs.ticksuffix, diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 25d5f02f597..bd29ad53d80 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1241,7 +1241,7 @@ function tickTextObj(ax, x, text) { function formatDate(ax, out, hover, extraPrecision) { var tr = ax._tickround, - fmt = (hover && ax.hoverformat) || ax.tickformat; + fmt = (hover && ax.hoverformat) || axes.getTickFormat(ax); if(extraPrecision) { // second or sub-second precision: extra always shows max digits. @@ -1406,7 +1406,7 @@ function numFormat(v, ax, fmtoverride, hover) { tickRound = ax._tickround, exponentFormat = fmtoverride || ax.exponentformat || 'B', exponent = ax._tickexponent, - tickformat = ax.tickformat, + tickformat = axes.getTickFormat(ax), separatethousands = ax.separatethousands; // special case for hover: set exponent just for this value, and @@ -1507,6 +1507,59 @@ function numFormat(v, ax, fmtoverride, hover) { return v; } +axes.getTickFormat = function(ax) { + function convertToMs(dtick) { + return typeof dtick !== 'string' ? dtick : Number(dtick.replace('M', '') * ONEAVGMONTH); + } + function isProperStop(dtick, range, convert) { + var convertFn = convert || function(x) { return x;}; + var leftDtick = range[0]; + var rightDtick = range[1]; + return (!leftDtick || convertFn(leftDtick) <= convertFn(dtick)) && + (!rightDtick || convertFn(rightDtick) >= convertFn(dtick)); + } + function getRangeWidth(range, convert) { + var convertFn = convert || function(x) { return x;}; + var left = range[0] || 0; + var right = range[1] || 0; + return Math.abs(convertFn(right) - convertFn(left)); + } + + var tickstop; + if(ax.tickformatstops && ax.tickformatstops.length > 0) { + switch(ax.type) { + case 'date': { + tickstop = ax.tickformatstops.reduce(function(acc, stop) { + if(!isProperStop(ax.dtick, stop.dtickrange, convertToMs)) { + return acc; + } + if(!acc) { + return stop; + } else { + return getRangeWidth(stop.dtickrange, convertToMs) > getRangeWidth(acc.dtickrange, convertToMs) ? stop : acc; + } + }, null); + break; + } + case 'linear': { + tickstop = ax.tickformatstops.reduce(function(acc, stop) { + if(!isProperStop(ax.dtick, stop.dtickrange)) { + return acc; + } + if(!acc) { + return stop; + } else { + return getRangeWidth(stop.dtickrange) > getRangeWidth(acc.dtickrange) ? stop : acc; + } + }, null); + break; + } + default: + } + } + return tickstop ? tickstop.value : ax.tickformat; +}; + axes.subplotMatch = /^x([0-9]*)y([0-9]*)$/; // getSubplots - extract all combinations of axes we need to make plots for diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 90855d7452a..275f90128b6 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -448,6 +448,18 @@ module.exports = { '*%H~%M~%S.%2f* would display *09~15~23.46*' ].join(' ') }, + tickformatstops: { + valType: 'any', + arrayOk: true, + role: 'style', + description: [ + 'Set rules for customizing tickformat on different zoom levels for *date* and', + '*linear axis types. You can specify these rules in following way', + '[{dtickrange: [*min*, *max*], value: *format*}]. Where *min*, *max* - dtick values', + 'which describe some zoom level, it is possible to omit *min* or *max* value by passing', + '*null*. *format* - string, exactly as *tickformat*' + ].join(' ') + }, hoverformat: { valType: 'string', dflt: '', diff --git a/src/plots/cartesian/tick_label_defaults.js b/src/plots/cartesian/tick_label_defaults.js index 5f37680d331..bb8100cb98f 100644 --- a/src/plots/cartesian/tick_label_defaults.js +++ b/src/plots/cartesian/tick_label_defaults.js @@ -40,6 +40,7 @@ module.exports = function handleTickLabelDefaults(containerIn, containerOut, coe if(axType !== 'category') { var tickFormat = coerce('tickformat'); + coerce('tickformatstops'); if(!tickFormat && axType !== 'date') { coerce('showexponent', showAttrDflt); coerce('exponentformat'); diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index 16f901d56d0..ba956cb7523 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -100,6 +100,7 @@ module.exports = { exponentformat: axesAttrs.exponentformat, separatethousands: axesAttrs.separatethousands, tickformat: axesAttrs.tickformat, + tickformatstops: axesAttrs.tickformatstops, hoverformat: axesAttrs.hoverformat, // lines and grids showline: axesAttrs.showline, diff --git a/src/plots/ternary/layout/axis_attributes.js b/src/plots/ternary/layout/axis_attributes.js index fa35570ecb3..073cfa2fadf 100644 --- a/src/plots/ternary/layout/axis_attributes.js +++ b/src/plots/ternary/layout/axis_attributes.js @@ -39,6 +39,7 @@ module.exports = { tickfont: axesAttrs.tickfont, tickangle: axesAttrs.tickangle, tickformat: axesAttrs.tickformat, + tickformatstops: axesAttrs.tickformatstops, hoverformat: axesAttrs.hoverformat, // lines and grids showline: extendFlat({}, axesAttrs.showline, {dflt: true}), diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index cd689e2772f..fc38acdd5ee 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -265,6 +265,18 @@ module.exports = { '*%H~%M~%S.%2f* would display *09~15~23.46*' ].join(' ') }, + tickformatstops: { + valType: 'any', + arrayOk: true, + role: 'style', + description: [ + 'Set rules for customizing tickformat on different zoom levels for *date* and', + '*linear axis types. You can specify these rules in following way', + '[{dtickrange: [*min*, *max*], value: *format*}]. Where *min*, *max* - dtick values', + 'which describe some zoom level, it is possible to omit *min* or *max* value by passing', + '*null*. *format* - string, exactly as *tickformat*' + ].join(' ') + }, categoryorder: { valType: 'enumerated', values: [ diff --git a/tasks/util/strict_d3.js b/tasks/util/strict_d3.js index 1e51ab8912b..505fb33b64f 100644 --- a/tasks/util/strict_d3.js +++ b/tasks/util/strict_d3.js @@ -18,7 +18,7 @@ module.exports = transformTools.makeRequireTransform('requireTransform', var pathOut; if(pathIn === 'd3' && opts.file !== pathToStrictD3Module) { - pathOut = 'require(\'' + pathToStrictD3Module + '\')'; + pathOut = 'require(\'' + pathToStrictD3Module.replace(/\\/g, '/') + '\')'; } if(pathOut) return cb(null, pathOut); From f958c3b0463f37db80411fe674e7fb7e9a3b79d0 Mon Sep 17 00:00:00 2001 From: "Yauhen_Kavaliou@epam.com" Date: Wed, 23 Aug 2017 15:02:18 +0300 Subject: [PATCH 02/16] change tickformatstops definition and code review fixes --- src/plots/cartesian/axes.js | 38 +++++++++++++++++++--- src/plots/cartesian/layout_attributes.js | 28 ++++++++++------ src/plots/cartesian/tick_label_defaults.js | 27 +++++++++++++-- src/traces/carpet/axis_attributes.js | 28 ++++++++++------ tasks/util/strict_d3.js | 4 +-- 5 files changed, 97 insertions(+), 28 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index bd29ad53d80..f3f3767613a 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1297,7 +1297,8 @@ function formatDate(ax, out, hover, extraPrecision) { function formatLog(ax, out, hover, extraPrecision, hideexp) { var dtick = ax.dtick, - x = out.x; + x = out.x, + tickformat = ax.tickformat; if(hideexp === 'never') { // If this is a hover label, then we must *never* hide the exponent @@ -1311,7 +1312,7 @@ function formatLog(ax, out, hover, extraPrecision, hideexp) { if(extraPrecision && ((typeof dtick !== 'string') || dtick.charAt(0) !== 'L')) dtick = 'L3'; - if(ax.tickformat || (typeof dtick === 'string' && dtick.charAt(0) === 'L')) { + if(tickformat || (typeof dtick === 'string' && dtick.charAt(0) === 'L')) { out.text = numFormat(Math.pow(10, x), ax, hideexp, extraPrecision); } else if(isNumeric(dtick) || ((dtick.charAt(0) === 'D') && (Lib.mod(x + 0.01, 1) < 0.1))) { @@ -1515,8 +1516,8 @@ axes.getTickFormat = function(ax) { var convertFn = convert || function(x) { return x;}; var leftDtick = range[0]; var rightDtick = range[1]; - return (!leftDtick || convertFn(leftDtick) <= convertFn(dtick)) && - (!rightDtick || convertFn(rightDtick) >= convertFn(dtick)); + return (leftDtick === null || convertFn(leftDtick) <= convertFn(dtick)) && + (rightDtick === null || convertFn(rightDtick) >= convertFn(dtick)); } function getRangeWidth(range, convert) { var convertFn = convert || function(x) { return x;}; @@ -1524,6 +1525,24 @@ axes.getTickFormat = function(ax) { var right = range[1] || 0; return Math.abs(convertFn(right) - convertFn(left)); } + 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; + } + } + } else { + return typeof left === 'number' ? 1 : -1; + } + } var tickstop; if(ax.tickformatstops && ax.tickformatstops.length > 0) { @@ -1554,6 +1573,17 @@ axes.getTickFormat = function(ax) { }, null); break; } + case 'log': { + tickstop = ax.tickformatstops.filter(function(stop) { + var left = stop.dtickrange[0], right = stop.dtickrange[1]; + var isLeftDtickNull = left === null; + var isRightDtickNull = right === null; + var isDtickInRangeLeft = compareLogTicks(ax.dtick, left) >= 0; + var isDtickInRangeRight = compareLogTicks(ax.dtick, right) <= 0; + return (isLeftDtickNull || isDtickInRangeLeft) && (isRightDtickNull || isDtickInRangeRight); + })[0]; + break; + } default: } } diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 275f90128b6..18dac79b2fb 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -449,16 +449,24 @@ module.exports = { ].join(' ') }, tickformatstops: { - valType: 'any', - arrayOk: true, - role: 'style', - description: [ - 'Set rules for customizing tickformat on different zoom levels for *date* and', - '*linear axis types. You can specify these rules in following way', - '[{dtickrange: [*min*, *max*], value: *format*}]. Where *min*, *max* - dtick values', - 'which describe some zoom level, it is possible to omit *min* or *max* value by passing', - '*null*. *format* - string, exactly as *tickformat*' - ].join(' ') + _isLinkedToArray: 'tickformatstop', + + dtickrange: { + valType: 'data_array', + description: [ + 'range [*min*, *max*], where *min*, *max* - dtick values', + 'which describe some zoom level, it is possible to omit *min*', + 'or *max* value by passing *null*' + ].join(' ') + }, + value: { + valType: 'string', + dflt: '', + role: 'style', + description: [ + 'string - dtickformat for described zoom level, the same as *tickformat*' + ].join(' ') + } }, hoverformat: { valType: 'string', diff --git a/src/plots/cartesian/tick_label_defaults.js b/src/plots/cartesian/tick_label_defaults.js index bb8100cb98f..d35542cd8d7 100644 --- a/src/plots/cartesian/tick_label_defaults.js +++ b/src/plots/cartesian/tick_label_defaults.js @@ -10,7 +10,7 @@ 'use strict'; var Lib = require('../../lib'); - +var layoutAttributes = require('./layout_attributes'); /** * options: inherits font, outerTicks, noHover from axes.handleAxisDefaults @@ -40,7 +40,7 @@ module.exports = function handleTickLabelDefaults(containerIn, containerOut, coe if(axType !== 'category') { var tickFormat = coerce('tickformat'); - coerce('tickformatstops'); + tickformatstopsDefaults(containerIn, containerOut); if(!tickFormat && axType !== 'date') { coerce('showexponent', showAttrDflt); coerce('exponentformat'); @@ -81,3 +81,26 @@ function getShowAttrDflt(containerIn) { return containerIn[showAttrs[0]]; } } + +function tickformatstopsDefaults(tickformatIn, tickformatOut) { + var valuesIn = tickformatIn.tickformatstops || [], + valuesOut = tickformatOut.tickformatstops = []; + + var valueIn, valueOut; + + function coerce(attr, dflt) { + return Lib.coerce(valueIn, valueOut, layoutAttributes.tickformatstops, attr, dflt); + } + + for(var i = 0; i < valuesIn.length; i++) { + valueIn = valuesIn[i]; + valueOut = {}; + + coerce('dtickrange'); + coerce('value'); + + valuesOut.push(valueOut); + } + + return valuesOut; +} diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index fc38acdd5ee..43da5bb85c9 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -266,16 +266,24 @@ module.exports = { ].join(' ') }, tickformatstops: { - valType: 'any', - arrayOk: true, - role: 'style', - description: [ - 'Set rules for customizing tickformat on different zoom levels for *date* and', - '*linear axis types. You can specify these rules in following way', - '[{dtickrange: [*min*, *max*], value: *format*}]. Where *min*, *max* - dtick values', - 'which describe some zoom level, it is possible to omit *min* or *max* value by passing', - '*null*. *format* - string, exactly as *tickformat*' - ].join(' ') + _isLinkedToArray: 'tickformatstop', + + dtickrange: { + valType: 'data_array', + description: [ + 'range [*min*, *max*], where *min*, *max* - dtick values', + 'which describe some zoom level, it is possible to omit *min*', + 'or *max* value by passing *null*' + ].join(' ') + }, + value: { + valType: 'string', + dflt: '', + role: 'style', + description: [ + 'string - dtickformat for described zoom level, the same as *tickformat*' + ].join(' ') + } }, categoryorder: { valType: 'enumerated', diff --git a/tasks/util/strict_d3.js b/tasks/util/strict_d3.js index 505fb33b64f..3f0d177bcac 100644 --- a/tasks/util/strict_d3.js +++ b/tasks/util/strict_d3.js @@ -6,7 +6,7 @@ var pathToStrictD3Module = path.join( constants.pathToImageTest, 'strict-d3.js' ); - +var normalizedpathToStrictD3Module = pathToStrictD3Module.replace(/\\/g, '/'); // fix npm-sripts for windows users /** * Transform `require('d3')` expressions to `require(/path/to/strict-d3.js)` */ @@ -18,7 +18,7 @@ module.exports = transformTools.makeRequireTransform('requireTransform', var pathOut; if(pathIn === 'd3' && opts.file !== pathToStrictD3Module) { - pathOut = 'require(\'' + pathToStrictD3Module.replace(/\\/g, '/') + '\')'; + pathOut = 'require(\'' + normalizedpathToStrictD3Module + '\')'; } if(pathOut) return cb(null, pathOut); From a155bd96d423279146669986ef0a58aa6cfaef41 Mon Sep 17 00:00:00 2001 From: "Yauhen_Kavaliou@epam.com" Date: Thu, 24 Aug 2017 14:54:14 +0300 Subject: [PATCH 03/16] change valType --- src/plots/cartesian/layout_attributes.js | 7 ++++++- src/traces/carpet/axis_attributes.js | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 18dac79b2fb..de566864f23 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -452,7 +452,12 @@ module.exports = { _isLinkedToArray: 'tickformatstop', dtickrange: { - valType: 'data_array', + valType: 'info_array', + role: 'info', + items: [ + {valType: 'any'}, + {valType: 'any'} + ], description: [ 'range [*min*, *max*], where *min*, *max* - dtick values', 'which describe some zoom level, it is possible to omit *min*', diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index 43da5bb85c9..a2863ae7003 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -269,7 +269,12 @@ module.exports = { _isLinkedToArray: 'tickformatstop', dtickrange: { - valType: 'data_array', + valType: 'info_array', + role: 'info', + items: [ + {valType: 'any'}, + {valType: 'any'} + ], description: [ 'range [*min*, *max*], where *min*, *max* - dtick values', 'which describe some zoom level, it is possible to omit *min*', From be11c03708fdb07f550fb9c7cb186d24e06d0327 Mon Sep 17 00:00:00 2001 From: Andrei_Palchys Date: Thu, 24 Aug 2017 16:15:20 +0300 Subject: [PATCH 04/16] remove axis attribute duplication --- src/traces/carpet/axis_attributes.js | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index a2863ae7003..12e81894886 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -11,6 +11,7 @@ var extendFlat = require('../../lib/extend').extendFlat; var fontAttrs = require('../../plots/font_attributes'); var colorAttrs = require('../../components/color/attributes'); +var axesAttrs = require('../../plots/cartesian/layout_attributes') module.exports = { color: { @@ -265,31 +266,7 @@ module.exports = { '*%H~%M~%S.%2f* would display *09~15~23.46*' ].join(' ') }, - tickformatstops: { - _isLinkedToArray: 'tickformatstop', - - dtickrange: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'any'}, - {valType: 'any'} - ], - description: [ - 'range [*min*, *max*], where *min*, *max* - dtick values', - 'which describe some zoom level, it is possible to omit *min*', - 'or *max* value by passing *null*' - ].join(' ') - }, - value: { - valType: 'string', - dflt: '', - role: 'style', - description: [ - 'string - dtickformat for described zoom level, the same as *tickformat*' - ].join(' ') - } - }, + tickformatstops: axesAttrs.tickformatstops, categoryorder: { valType: 'enumerated', values: [ From a13c29eb851ff081388cecdfbbeb7710e31954ac Mon Sep 17 00:00:00 2001 From: Andrei_Palchys Date: Fri, 25 Aug 2017 09:36:16 +0300 Subject: [PATCH 05/16] fix: add missed semicolon --- src/traces/carpet/axis_attributes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index 12e81894886..dc9bae678dc 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -11,7 +11,7 @@ var extendFlat = require('../../lib/extend').extendFlat; var fontAttrs = require('../../plots/font_attributes'); var colorAttrs = require('../../components/color/attributes'); -var axesAttrs = require('../../plots/cartesian/layout_attributes') +var axesAttrs = require('../../plots/cartesian/layout_attributes'); module.exports = { color: { From 304d96b6de1d4cbc39c8d07b346551799b472775 Mon Sep 17 00:00:00 2001 From: Andrei_Palchys Date: Mon, 28 Aug 2017 16:28:57 +0300 Subject: [PATCH 06/16] add tests for tickformatstops --- src/plots/cartesian/axes.js | 4 +- test/image/mocks/tickformatstops.json | 46 ++++ test/jasmine/tests/tickformatstops_test.js | 294 +++++++++++++++++++++ 3 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 test/image/mocks/tickformatstops.json create mode 100644 test/jasmine/tests/tickformatstops_test.js diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index f3f3767613a..1b7734d6771 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1516,8 +1516,8 @@ axes.getTickFormat = function(ax) { var convertFn = convert || function(x) { return x;}; var leftDtick = range[0]; var rightDtick = range[1]; - return (leftDtick === null || convertFn(leftDtick) <= convertFn(dtick)) && - (rightDtick === null || convertFn(rightDtick) >= convertFn(dtick)); + return ((!leftDtick && typeof leftDtick !== 'number') || convertFn(leftDtick) <= convertFn(dtick)) && + ((!rightDtick && typeof rightDtick !== 'number') || convertFn(rightDtick) >= convertFn(dtick)); } function getRangeWidth(range, convert) { var convertFn = convert || function(x) { return x;}; diff --git a/test/image/mocks/tickformatstops.json b/test/image/mocks/tickformatstops.json new file mode 100644 index 00000000000..af59b7606ec --- /dev/null +++ b/test/image/mocks/tickformatstops.json @@ -0,0 +1,46 @@ +{ + "data": [ + { + "x": ["2005-01","2005-02","2005-03","2005-04","2005-05","2005-06","2005-07"], + "y": [-20,10,-5,0,5,-10,20] + } + ], + "layout": { + "xaxis": { + "tickformatstops": [ + { + "dtickrange": [null, 1000], + "value": "%H:%M:%S.%L ms" + }, + { + "dtickrange": [1000, 60000], + "value": "%H:%M:%S s" + }, + { + "dtickrange": [60000, 3600000], + "value": "%H:%M m" + }, + { + "dtickrange": [3600000, 86400000], + "value": "%H:%M h" + }, + { + "dtickrange": [86400000, 604800000], + "value": "%e. %b d" + }, + { + "dtickrange": [604800000, "M1"], + "value": "%e. %b w" + }, + { + "dtickrange": ["M1", "M12"], + "value": "%b '%y M" + }, + { + "dtickrange": ["M12", null], + "value": "%Y Y" + } + ] + } + } +} diff --git a/test/jasmine/tests/tickformatstops_test.js b/test/jasmine/tests/tickformatstops_test.js new file mode 100644 index 00000000000..c6699fbec9b --- /dev/null +++ b/test/jasmine/tests/tickformatstops_test.js @@ -0,0 +1,294 @@ +var Plotly = require('@lib/index'); +var Lib = require('@src/lib'); +var Axes = require('@src/plots/cartesian/axes'); +var Fx = require('@src/components/fx'); +var d3 = require('d3'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var selectButton = require('../assets/modebar_button'); + +var mock = require('@mocks/tickformatstops.json'); + +function getZoomInButton(gd) { + return selectButton(gd._fullLayout._modeBar, 'zoomIn2d'); +} + +function getZoomOutButton(gd) { + return selectButton(gd._fullLayout._modeBar, 'zoomOut2d'); +} + +function getFormatter(format) { + return d3.time.format.utc(format); +} + +describe('Test Axes.getTickformat', function() { + it('get proper tickformatstop for linear axis', function() { + var lineartickformatstops = [ + { + dtickrange: [null, 1], + value: '.f2', + }, + { + dtickrange: [1, 100], + value: '.f1', + }, + { + dtickrange: [100, null], + value: 'g', + } + ]; + expect(Axes.getTickFormat({ + type: 'linear', + tickformatstops: lineartickformatstops, + dtick: 0.1 + })).toEqual(lineartickformatstops[0].value); + + expect(Axes.getTickFormat({ + type: 'linear', + tickformatstops: lineartickformatstops, + dtick: 1 + })).toEqual(lineartickformatstops[1].value); + + expect(Axes.getTickFormat({ + type: 'linear', + tickformatstops: lineartickformatstops, + dtick: 99 + })).toEqual(lineartickformatstops[1].value); + expect(Axes.getTickFormat({ + type: 'linear', + tickformatstops: lineartickformatstops, + dtick: 99999 + })).toEqual(lineartickformatstops[2].value); + }); + + it('get proper tickformatstop for date axis', function() { + var MILLISECOND = 1; + var SECOND = MILLISECOND * 1000; + var MINUTE = SECOND * 60; + var HOUR = MINUTE * 60; + var DAY = HOUR * 24; + var WEEK = DAY * 7; + var MONTH = 'M1'; // or YEAR / 12; + var YEAR = 'M12'; // or 365.25 * DAY; + var datetickformatstops = [ + { + dtickrange: [null, SECOND], + value: '%H:%M:%S.%L ms' // millisecond + }, + { + dtickrange: [SECOND, MINUTE], + value: '%H:%M:%S s' // second + }, + { + dtickrange: [MINUTE, HOUR], + value: '%H:%M m' // minute + }, + { + dtickrange: [HOUR, DAY], + value: '%H:%M h' // hour + }, + { + dtickrange: [DAY, WEEK], + value: '%e. %b d' // day + }, + { + dtickrange: [WEEK, MONTH], + value: '%e. %b w' // week + }, + { + dtickrange: [MONTH, YEAR], + value: '%b \'%y M' // month + }, + { + dtickrange: [YEAR, null], + value: '%Y Y' // year + } + ]; + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 100 + })).toEqual(datetickformatstops[0].value); // millisecond + + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 1000 + })).toEqual(datetickformatstops[1].value); // second + + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 1000 * 60 * 60 * 3 // three hours + })).toEqual(datetickformatstops[3].value); // hour + + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 1000 * 60 * 60 * 24 * 7 * 2 // two weeks + })).toEqual(datetickformatstops[5].value); // week + + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 'M1' + })).toEqual(datetickformatstops[6].value); // month + + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 'M5' + })).toEqual(datetickformatstops[6].value); // month + + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 'M24' + })).toEqual(datetickformatstops[7].value); // year + }); + + it('get proper tickformatstop for log axis', function() { + var logtickformatstops = [ + { + dtickrange: [null, 'L0.01'], + value: '.f3', + }, + { + dtickrange: ['L0.01', 'L1'], + value: '.f2', + }, + { + dtickrange: ['D1', 'D2'], + value: '.f1', + }, + { + dtickrange: [1, null], + value: 'g' + } + ]; + expect(Axes.getTickFormat({ + type: 'log', + tickformatstops: logtickformatstops, + dtick: 'L0.0001' + })).toEqual(logtickformatstops[0].value); + + expect(Axes.getTickFormat({ + type: 'log', + tickformatstops: logtickformatstops, + dtick: 'L0.1' + })).toEqual(logtickformatstops[1].value); + + expect(Axes.getTickFormat({ + type: 'log', + tickformatstops: logtickformatstops, + dtick: 'L2' + })).toEqual(undefined); + expect(Axes.getTickFormat({ + type: 'log', + tickformatstops: logtickformatstops, + dtick: 'D2' + })).toEqual(logtickformatstops[2].value); + expect(Axes.getTickFormat({ + type: 'log', + tickformatstops: logtickformatstops, + dtick: 1 + })).toEqual(logtickformatstops[3].value); + }); +}); + +describe('Test tickformatstops:', function() { + + var mockCopy, gd; + + beforeEach(function() { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); + }); + + afterEach(destroyGraphDiv); + + describe('Zooming-in until milliseconds zoom level', function() { + it('Zoom in', function(done) { + var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); + var zoomIn = function() { + promise = promise.then(function() { + getZoomInButton(gd).click(); + var xLabels = Axes.calcTicks(gd._fullLayout.xaxis); + var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); + var expectedLabels = xLabels.map(function(d) {return formatter(new Date(d.x));}); + var actualLabels = xLabels.map(function(d) {return d.text;}); + expect(expectedLabels).toEqual(actualLabels); + if(gd._fullLayout.xaxis.dtick > 1) { + zoomIn(); + } else { + done(); + } + }); + }; + zoomIn(); + }); + }); + + describe('Zooming-out until years zoom level', function() { + it('Zoom out', function(done) { + var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); + + var zoomOut = function() { + promise = promise.then(function() { + getZoomOutButton(gd).click(); + var xLabels = Axes.calcTicks(gd._fullLayout.xaxis); + var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); + var expectedLabels = xLabels.map(function(d) {return formatter(new Date(d.x));}); + var actualLabels = xLabels.map(function(d) {return d.text;}); + expect(expectedLabels).toEqual(actualLabels); + if(typeof gd._fullLayout.xaxis.dtick === 'number' || + typeof gd._fullLayout.xaxis.dtick === 'string' && parseInt(gd._fullLayout.xaxis.dtick.replace(/\D/g, '')) < 48) { + zoomOut(); + } else { + done(); + } + }); + }; + zoomOut(); + }); + }); + + describe('Check tickformatstops for hover', function() { + 'use strict'; + + var evt = { xpx: 270, ypx: 10 }; + + afterEach(destroyGraphDiv); + + beforeEach(function() { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); + }); + + describe('hover info', function() { + + it('responds to hover', function(done) { + var mockCopy = Lib.extendDeep({}, mock); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + Fx.hover(gd, evt, 'xy'); + + var hoverTrace = gd._hoverdata[0]; + var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); + + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(3); + expect(hoverTrace.x).toEqual('2005-04-01'); + expect(hoverTrace.y).toEqual(0); + + expect(d3.selectAll('g.axistext').size()).toEqual(1); + expect(d3.selectAll('g.hovertext').size()).toEqual(1); + expect(d3.selectAll('g.axistext').select('text').html()).toEqual(formatter(new Date(hoverTrace.x))); + expect(d3.selectAll('g.hovertext').select('text').html()).toEqual('0'); + done(); + }); + }); + }); + }); + +}); From 0c9fb2bf57955462db7d90923faad524caaf6c51 Mon Sep 17 00:00:00 2001 From: Andrei_Palchys Date: Mon, 28 Aug 2017 21:52:00 +0300 Subject: [PATCH 07/16] add tickformatstop image for test --- test/image/baselines/tickformatstops.png | Bin 0 -> 29221 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/image/baselines/tickformatstops.png diff --git a/test/image/baselines/tickformatstops.png b/test/image/baselines/tickformatstops.png new file mode 100644 index 0000000000000000000000000000000000000000..e086e3e90543647bf69fac5b477d701f8f0999b3 GIT binary patch literal 29221 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 literal 0 HcmV?d00001 From 7f1fd411b6b759aef4f2a6074e23ce6e894a0531 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 9 Oct 2017 16:06:33 -0400 Subject: [PATCH 08/16] editTypes for tickformatstops --- src/plots/cartesian/layout_attributes.js | 9 ++++++--- src/traces/carpet/axis_attributes.js | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 9be8f605075..aae0ac26b43 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -499,9 +499,10 @@ module.exports = { valType: 'info_array', role: 'info', items: [ - {valType: 'any'}, - {valType: 'any'} + {valType: 'any', editType: 'ticks'}, + {valType: 'any', editType: 'ticks'} ], + editType: 'ticks', description: [ 'range [*min*, *max*], where *min*, *max* - dtick values', 'which describe some zoom level, it is possible to omit *min*', @@ -512,10 +513,12 @@ module.exports = { valType: 'string', dflt: '', role: 'style', + editType: 'ticks', description: [ 'string - dtickformat for described zoom level, the same as *tickformat*' ].join(' ') - } + }, + editType: 'ticks' }, hoverformat: { valType: 'string', diff --git a/src/traces/carpet/axis_attributes.js b/src/traces/carpet/axis_attributes.js index d50a3c8e21a..8478132e1bb 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -11,6 +11,7 @@ var fontAttrs = require('../../plots/font_attributes'); var colorAttrs = require('../../components/color/attributes'); var axesAttrs = require('../../plots/cartesian/layout_attributes'); +var overrideAll = require('../../plot_api/edit_types').overrideAll; module.exports = { color: { @@ -291,7 +292,7 @@ module.exports = { '*%H~%M~%S.%2f* would display *09~15~23.46*' ].join(' ') }, - tickformatstops: axesAttrs.tickformatstops, + tickformatstops: overrideAll(axesAttrs.tickformatstops, 'calc', 'from-root'), categoryorder: { valType: 'enumerated', values: [ From 86777f7cc73657f0bb67ef6c98002116d0c72a08 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 9 Oct 2017 17:05:46 -0400 Subject: [PATCH 09/16] make sure the tickformatstops tests actually run as expected --- test/jasmine/tests/tickformatstops_test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/jasmine/tests/tickformatstops_test.js b/test/jasmine/tests/tickformatstops_test.js index c6699fbec9b..91461579b7b 100644 --- a/test/jasmine/tests/tickformatstops_test.js +++ b/test/jasmine/tests/tickformatstops_test.js @@ -210,6 +210,9 @@ describe('Test tickformatstops:', function() { describe('Zooming-in until milliseconds zoom level', function() { it('Zoom in', function(done) { var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); + + var testCount = 0; + var zoomIn = function() { promise = promise.then(function() { getZoomInButton(gd).click(); @@ -218,9 +221,13 @@ describe('Test tickformatstops:', function() { var expectedLabels = xLabels.map(function(d) {return formatter(new Date(d.x));}); var actualLabels = xLabels.map(function(d) {return d.text;}); expect(expectedLabels).toEqual(actualLabels); + testCount++; + if(gd._fullLayout.xaxis.dtick > 1) { zoomIn(); } else { + // make sure we tested as many levels as we thought we would + expect(testCount).toBe(32); done(); } }); @@ -233,6 +240,8 @@ describe('Test tickformatstops:', function() { it('Zoom out', function(done) { var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); + var testCount = 0; + var zoomOut = function() { promise = promise.then(function() { getZoomOutButton(gd).click(); @@ -241,10 +250,14 @@ describe('Test tickformatstops:', function() { var expectedLabels = xLabels.map(function(d) {return formatter(new Date(d.x));}); var actualLabels = xLabels.map(function(d) {return d.text;}); expect(expectedLabels).toEqual(actualLabels); + testCount++; + if(typeof gd._fullLayout.xaxis.dtick === 'number' || typeof gd._fullLayout.xaxis.dtick === 'string' && parseInt(gd._fullLayout.xaxis.dtick.replace(/\D/g, '')) < 48) { zoomOut(); } else { + // make sure we tested as many levels as we thought we would + expect(testCount).toBe(5); done(); } }); From 531aea88fb83b468be6a3285ecad9a67e1fe08a4 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 9 Oct 2017 17:13:30 -0400 Subject: [PATCH 10/16] flatten tickformatstops tests --- test/jasmine/tests/tickformatstops_test.js | 157 ++++++++++----------- 1 file changed, 71 insertions(+), 86 deletions(-) diff --git a/test/jasmine/tests/tickformatstops_test.js b/test/jasmine/tests/tickformatstops_test.js index 91461579b7b..60c68b2613e 100644 --- a/test/jasmine/tests/tickformatstops_test.js +++ b/test/jasmine/tests/tickformatstops_test.js @@ -6,6 +6,7 @@ var d3 = require('d3'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var selectButton = require('../assets/modebar_button'); +var fail = require('../assets/fail_test'); var mock = require('@mocks/tickformatstops.json'); @@ -22,6 +23,8 @@ function getFormatter(format) { } describe('Test Axes.getTickformat', function() { + 'use strict'; + it('get proper tickformatstop for linear axis', function() { var lineartickformatstops = [ { @@ -197,6 +200,7 @@ describe('Test Axes.getTickformat', function() { }); describe('Test tickformatstops:', function() { + 'use strict'; var mockCopy, gd; @@ -207,101 +211,82 @@ describe('Test tickformatstops:', function() { afterEach(destroyGraphDiv); - describe('Zooming-in until milliseconds zoom level', function() { - it('Zoom in', function(done) { - var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); - - var testCount = 0; - - var zoomIn = function() { - promise = promise.then(function() { - getZoomInButton(gd).click(); - var xLabels = Axes.calcTicks(gd._fullLayout.xaxis); - var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); - var expectedLabels = xLabels.map(function(d) {return formatter(new Date(d.x));}); - var actualLabels = xLabels.map(function(d) {return d.text;}); - expect(expectedLabels).toEqual(actualLabels); - testCount++; - - if(gd._fullLayout.xaxis.dtick > 1) { - zoomIn(); - } else { - // make sure we tested as many levels as we thought we would - expect(testCount).toBe(32); - done(); - } - }); - }; - zoomIn(); - }); + it('handles zooming-in until milliseconds zoom level', function(done) { + var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); + + var testCount = 0; + + var zoomIn = function() { + promise = promise.then(function() { + getZoomInButton(gd).click(); + var xLabels = Axes.calcTicks(gd._fullLayout.xaxis); + var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); + var expectedLabels = xLabels.map(function(d) {return formatter(new Date(d.x));}); + var actualLabels = xLabels.map(function(d) {return d.text;}); + expect(expectedLabels).toEqual(actualLabels); + testCount++; + + if(gd._fullLayout.xaxis.dtick > 1) { + zoomIn(); + } else { + // make sure we tested as many levels as we thought we would + expect(testCount).toBe(32); + done(); + } + }); + }; + zoomIn(); }); - describe('Zooming-out until years zoom level', function() { - it('Zoom out', function(done) { - var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); - - var testCount = 0; - - var zoomOut = function() { - promise = promise.then(function() { - getZoomOutButton(gd).click(); - var xLabels = Axes.calcTicks(gd._fullLayout.xaxis); - var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); - var expectedLabels = xLabels.map(function(d) {return formatter(new Date(d.x));}); - var actualLabels = xLabels.map(function(d) {return d.text;}); - expect(expectedLabels).toEqual(actualLabels); - testCount++; - - if(typeof gd._fullLayout.xaxis.dtick === 'number' || - typeof gd._fullLayout.xaxis.dtick === 'string' && parseInt(gd._fullLayout.xaxis.dtick.replace(/\D/g, '')) < 48) { - zoomOut(); - } else { - // make sure we tested as many levels as we thought we would - expect(testCount).toBe(5); - done(); - } - }); - }; - zoomOut(); - }); + it('handles zooming-out until years zoom level', function(done) { + var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); + + var testCount = 0; + + var zoomOut = function() { + promise = promise.then(function() { + getZoomOutButton(gd).click(); + var xLabels = Axes.calcTicks(gd._fullLayout.xaxis); + var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); + var expectedLabels = xLabels.map(function(d) {return formatter(new Date(d.x));}); + var actualLabels = xLabels.map(function(d) {return d.text;}); + expect(expectedLabels).toEqual(actualLabels); + testCount++; + + if(typeof gd._fullLayout.xaxis.dtick === 'number' || + typeof gd._fullLayout.xaxis.dtick === 'string' && parseInt(gd._fullLayout.xaxis.dtick.replace(/\D/g, '')) < 48) { + zoomOut(); + } else { + // make sure we tested as many levels as we thought we would + expect(testCount).toBe(5); + done(); + } + }); + }; + zoomOut(); }); - describe('Check tickformatstops for hover', function() { - 'use strict'; - + it('responds to hover', function(done) { var evt = { xpx: 270, ypx: 10 }; - afterEach(destroyGraphDiv); + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + Fx.hover(gd, evt, 'xy'); - beforeEach(function() { - gd = createGraphDiv(); - mockCopy = Lib.extendDeep({}, mock); - }); + var hoverTrace = gd._hoverdata[0]; + var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); - describe('hover info', function() { + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(3); + expect(hoverTrace.x).toEqual('2005-04-01'); + expect(hoverTrace.y).toEqual(0); - it('responds to hover', function(done) { - var mockCopy = Lib.extendDeep({}, mock); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - Fx.hover(gd, evt, 'xy'); - - var hoverTrace = gd._hoverdata[0]; - var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); - - expect(hoverTrace.curveNumber).toEqual(0); - expect(hoverTrace.pointNumber).toEqual(3); - expect(hoverTrace.x).toEqual('2005-04-01'); - expect(hoverTrace.y).toEqual(0); - - expect(d3.selectAll('g.axistext').size()).toEqual(1); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); - expect(d3.selectAll('g.axistext').select('text').html()).toEqual(formatter(new Date(hoverTrace.x))); - expect(d3.selectAll('g.hovertext').select('text').html()).toEqual('0'); - done(); - }); - }); - }); + expect(d3.selectAll('g.axistext').size()).toEqual(1); + expect(d3.selectAll('g.hovertext').size()).toEqual(1); + expect(d3.selectAll('g.axistext').select('text').html()).toEqual(formatter(new Date(hoverTrace.x))); + expect(d3.selectAll('g.hovertext').select('text').html()).toEqual('0'); + }) + .catch(fail) + .then(done); }); }); From 0b48a3380376438a1b8821789e42f8e1106858c5 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 9 Oct 2017 17:23:26 -0400 Subject: [PATCH 11/16] robustify tickformatstops supplydefaults --- src/plots/cartesian/tick_label_defaults.js | 8 ++++---- test/jasmine/tests/tickformatstops_test.js | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/plots/cartesian/tick_label_defaults.js b/src/plots/cartesian/tick_label_defaults.js index d35542cd8d7..96af6b1be8b 100644 --- a/src/plots/cartesian/tick_label_defaults.js +++ b/src/plots/cartesian/tick_label_defaults.js @@ -83,8 +83,10 @@ function getShowAttrDflt(containerIn) { } function tickformatstopsDefaults(tickformatIn, tickformatOut) { - var valuesIn = tickformatIn.tickformatstops || [], - valuesOut = tickformatOut.tickformatstops = []; + var valuesIn = tickformatIn.tickformatstops; + var valuesOut = tickformatOut.tickformatstops = []; + + if(!Array.isArray(valuesIn)) return; var valueIn, valueOut; @@ -101,6 +103,4 @@ function tickformatstopsDefaults(tickformatIn, tickformatOut) { valuesOut.push(valueOut); } - - return valuesOut; } diff --git a/test/jasmine/tests/tickformatstops_test.js b/test/jasmine/tests/tickformatstops_test.js index 60c68b2613e..4d939004f7e 100644 --- a/test/jasmine/tests/tickformatstops_test.js +++ b/test/jasmine/tests/tickformatstops_test.js @@ -289,4 +289,19 @@ describe('Test tickformatstops:', function() { .then(done); }); + it('doesn\'t fail on bad input', function(done) { + var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); + + [1, {a: 1, b: 2}, 'boo'].forEach(function(v) { + promise = promise.then(function() { + return Plotly.relayout(gd, {'xaxis.tickformatstops': v}); + }).then(function() { + expect(gd._fullLayout.xaxis.tickformatstops).toEqual([]); + }); + }); + + promise + .catch(fail) + .then(done); + }); }); From 64200443c05d9bdcc28efebbffddf7655d2be83d Mon Sep 17 00:00:00 2001 From: Andrei_Palchys Date: Tue, 10 Oct 2017 17:15:25 +0300 Subject: [PATCH 12/16] take the first matching tickformatstop --- src/plots/cartesian/axes.js | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 5aab14a3c4d..6c4076eea40 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1539,40 +1539,26 @@ axes.getTickFormat = function(ax) { if(ax.tickformatstops && ax.tickformatstops.length > 0) { switch(ax.type) { case 'date': { - tickstop = ax.tickformatstops.reduce(function(acc, stop) { - if(!isProperStop(ax.dtick, stop.dtickrange, convertToMs)) { - return acc; - } - if(!acc) { - return stop; - } else { - return getRangeWidth(stop.dtickrange, convertToMs) > getRangeWidth(acc.dtickrange, convertToMs) ? stop : acc; - } - }, null); + tickstop = ax.tickformatstops.find(function(stop) { + return isProperStop(ax.dtick, stop.dtickrange, convertToMs) + }); break; } case 'linear': { - tickstop = ax.tickformatstops.reduce(function(acc, stop) { - if(!isProperStop(ax.dtick, stop.dtickrange)) { - return acc; - } - if(!acc) { - return stop; - } else { - return getRangeWidth(stop.dtickrange) > getRangeWidth(acc.dtickrange) ? stop : acc; - } - }, null); + tickstop = ax.tickformatstops.find(function(stop) { + return isProperStop(ax.dtick, stop.dtickrange, convertToMs) + }); break; } case 'log': { - tickstop = ax.tickformatstops.filter(function(stop) { + tickstop = ax.tickformatstops.find(function(stop) { var left = stop.dtickrange[0], right = stop.dtickrange[1]; var isLeftDtickNull = left === null; var isRightDtickNull = right === null; var isDtickInRangeLeft = compareLogTicks(ax.dtick, left) >= 0; var isDtickInRangeRight = compareLogTicks(ax.dtick, right) <= 0; return (isLeftDtickNull || isDtickInRangeLeft) && (isRightDtickNull || isDtickInRangeRight); - })[0]; + }); break; } default: From 92f7c13e86fad15ce39f76ef5685c4ae557b571b Mon Sep 17 00:00:00 2001 From: Andrei_Palchys Date: Tue, 10 Oct 2017 17:56:06 +0300 Subject: [PATCH 13/16] replace Array.prototype.find with for loop and update tests --- src/plots/cartesian/axes.js | 51 ++++++++++++---------- test/jasmine/tests/tickformatstops_test.js | 6 +-- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 6c4076eea40..e697f6f635c 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1503,13 +1503,6 @@ axes.getTickFormat = function(ax) { function convertToMs(dtick) { return typeof dtick !== 'string' ? dtick : Number(dtick.replace('M', '') * ONEAVGMONTH); } - 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 getRangeWidth(range, convert) { var convertFn = convert || function(x) { return x;}; var left = range[0] || 0; @@ -1534,31 +1527,41 @@ axes.getTickFormat = function(ax) { 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)); + } + 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; if(ax.tickformatstops && ax.tickformatstops.length > 0) { switch(ax.type) { - case 'date': { - tickstop = ax.tickformatstops.find(function(stop) { - return isProperStop(ax.dtick, stop.dtickrange, convertToMs) - }); - break; - } + case 'date': case 'linear': { - tickstop = ax.tickformatstops.find(function(stop) { - return isProperStop(ax.dtick, stop.dtickrange, convertToMs) - }); + for(var i = 0; i < ax.tickformatstops.length; i++) { + if (isProperStop(ax.dtick, ax.tickformatstops[i].dtickrange, convertToMs)){ + tickstop = ax.tickformatstops[i]; + break; + } + } break; } case 'log': { - tickstop = ax.tickformatstops.find(function(stop) { - var left = stop.dtickrange[0], right = stop.dtickrange[1]; - var isLeftDtickNull = left === null; - var isRightDtickNull = right === null; - var isDtickInRangeLeft = compareLogTicks(ax.dtick, left) >= 0; - var isDtickInRangeRight = compareLogTicks(ax.dtick, right) <= 0; - return (isLeftDtickNull || isDtickInRangeLeft) && (isRightDtickNull || isDtickInRangeRight); - }); + for(var i = 0; i < ax.tickformatstops.length; i++) { + if (isProperLogStop(ax.dtick, ax.tickformatstops[i].dtickrange)) { + tickstop = ax.tickformatstops[i]; + break; + } + } break; } default: diff --git a/test/jasmine/tests/tickformatstops_test.js b/test/jasmine/tests/tickformatstops_test.js index 4d939004f7e..a6538a5d0c3 100644 --- a/test/jasmine/tests/tickformatstops_test.js +++ b/test/jasmine/tests/tickformatstops_test.js @@ -50,7 +50,7 @@ describe('Test Axes.getTickformat', function() { type: 'linear', tickformatstops: lineartickformatstops, dtick: 1 - })).toEqual(lineartickformatstops[1].value); + })).toEqual(lineartickformatstops[0].value); expect(Axes.getTickFormat({ type: 'linear', @@ -117,7 +117,7 @@ describe('Test Axes.getTickformat', function() { type: 'date', tickformatstops: datetickformatstops, dtick: 1000 - })).toEqual(datetickformatstops[1].value); // second + })).toEqual(datetickformatstops[0].value); // millisecond expect(Axes.getTickFormat({ type: 'date', @@ -135,7 +135,7 @@ describe('Test Axes.getTickformat', function() { type: 'date', tickformatstops: datetickformatstops, dtick: 'M1' - })).toEqual(datetickformatstops[6].value); // month + })).toEqual(datetickformatstops[5].value); // week expect(Axes.getTickFormat({ type: 'date', From df72531425af254d6162aedd70e74442febbf6b6 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 10 Oct 2017 11:13:58 -0400 Subject: [PATCH 14/16] lint --- src/plots/cartesian/axes.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index e697f6f635c..64506c23eb9 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1500,15 +1500,12 @@ function numFormat(v, ax, fmtoverride, hover) { } axes.getTickFormat = function(ax) { + var i; + function convertToMs(dtick) { - return typeof dtick !== 'string' ? dtick : Number(dtick.replace('M', '') * ONEAVGMONTH); - } - function getRangeWidth(range, convert) { - var convertFn = convert || function(x) { return x;}; - var left = range[0] || 0; - var right = range[1] || 0; - return Math.abs(convertFn(right) - convertFn(left)); + return typeof dtick !== 'string' ? dtick : Number(dtick.replace('M', '')) * ONEAVGMONTH; } + function compareLogTicks(left, right) { var priority = ['L', 'D']; if(typeof left === typeof right) { @@ -1527,6 +1524,7 @@ axes.getTickFormat = function(ax) { return typeof left === 'number' ? 1 : -1; } } + function isProperStop(dtick, range, convert) { var convertFn = convert || function(x) { return x;}; var leftDtick = range[0]; @@ -1534,7 +1532,8 @@ axes.getTickFormat = function(ax) { return ((!leftDtick && typeof leftDtick !== 'number') || convertFn(leftDtick) <= convertFn(dtick)) && ((!rightDtick && typeof rightDtick !== 'number') || convertFn(rightDtick) >= convertFn(dtick)); } - function isProperLogStop(dtick, range){ + + function isProperLogStop(dtick, range) { var isLeftDtickNull = range[0] === null; var isRightDtickNull = range[1] === null; var isDtickInRangeLeft = compareLogTicks(dtick, range[0]) >= 0; @@ -1547,8 +1546,8 @@ axes.getTickFormat = function(ax) { switch(ax.type) { case 'date': case 'linear': { - for(var i = 0; i < ax.tickformatstops.length; i++) { - if (isProperStop(ax.dtick, ax.tickformatstops[i].dtickrange, convertToMs)){ + for(i = 0; i < ax.tickformatstops.length; i++) { + if(isProperStop(ax.dtick, ax.tickformatstops[i].dtickrange, convertToMs)) { tickstop = ax.tickformatstops[i]; break; } @@ -1556,8 +1555,8 @@ axes.getTickFormat = function(ax) { break; } case 'log': { - for(var i = 0; i < ax.tickformatstops.length; i++) { - if (isProperLogStop(ax.dtick, ax.tickformatstops[i].dtickrange)) { + for(i = 0; i < ax.tickformatstops.length; i++) { + if(isProperLogStop(ax.dtick, ax.tickformatstops[i].dtickrange)) { tickstop = ax.tickformatstops[i]; break; } From 7665b4c69827f5dd6eb9cea725ddb571aaa841d2 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Tue, 10 Oct 2017 11:18:03 -0400 Subject: [PATCH 15/16] updated tickformatstops baseline --- test/image/baselines/tickformatstops.png | Bin 29221 -> 28934 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/image/baselines/tickformatstops.png b/test/image/baselines/tickformatstops.png index e086e3e90543647bf69fac5b477d701f8f0999b3..3f0e66f942d32229047a1db3f007c0d4d3dfecb8 100644 GIT binary patch literal 28934 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 From 0e6747fef8825e716936fcf6c85e592b54aebcbb Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 12 Oct 2017 17:22:11 -0400 Subject: [PATCH 16/16] merge tickformatstops_test into axes_test --- test/jasmine/tests/axes_test.js | 301 ++++++++++++++++++++ test/jasmine/tests/tickformatstops_test.js | 307 --------------------- 2 files changed, 301 insertions(+), 307 deletions(-) delete mode 100644 test/jasmine/tests/tickformatstops_test.js diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 3bae4c904ac..fb269708016 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1,4 +1,5 @@ var Plotly = require('@lib/index'); +var d3 = require('d3'); var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); @@ -7,10 +8,12 @@ var tinycolor = require('tinycolor2'); var handleTickValueDefaults = require('@src/plots/cartesian/tick_value_defaults'); var Axes = require('@src/plots/cartesian/axes'); +var Fx = require('@src/components/fx'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var failTest = require('../assets/fail_test'); +var selectButton = require('../assets/modebar_button'); describe('Test axes', function() { @@ -2481,3 +2484,301 @@ describe('Test axes', function() { }); }); }); + +function getZoomInButton(gd) { + return selectButton(gd._fullLayout._modeBar, 'zoomIn2d'); +} + +function getZoomOutButton(gd) { + return selectButton(gd._fullLayout._modeBar, 'zoomOut2d'); +} + +function getFormatter(format) { + return d3.time.format.utc(format); +} + +describe('Test Axes.getTickformat', function() { + 'use strict'; + + it('get proper tickformatstop for linear axis', function() { + var lineartickformatstops = [ + { + dtickrange: [null, 1], + value: '.f2', + }, + { + dtickrange: [1, 100], + value: '.f1', + }, + { + dtickrange: [100, null], + value: 'g', + } + ]; + expect(Axes.getTickFormat({ + type: 'linear', + tickformatstops: lineartickformatstops, + dtick: 0.1 + })).toEqual(lineartickformatstops[0].value); + + expect(Axes.getTickFormat({ + type: 'linear', + tickformatstops: lineartickformatstops, + dtick: 1 + })).toEqual(lineartickformatstops[0].value); + + expect(Axes.getTickFormat({ + type: 'linear', + tickformatstops: lineartickformatstops, + dtick: 99 + })).toEqual(lineartickformatstops[1].value); + expect(Axes.getTickFormat({ + type: 'linear', + tickformatstops: lineartickformatstops, + dtick: 99999 + })).toEqual(lineartickformatstops[2].value); + }); + + it('get proper tickformatstop for date axis', function() { + var MILLISECOND = 1; + var SECOND = MILLISECOND * 1000; + var MINUTE = SECOND * 60; + var HOUR = MINUTE * 60; + var DAY = HOUR * 24; + var WEEK = DAY * 7; + var MONTH = 'M1'; // or YEAR / 12; + var YEAR = 'M12'; // or 365.25 * DAY; + var datetickformatstops = [ + { + dtickrange: [null, SECOND], + value: '%H:%M:%S.%L ms' // millisecond + }, + { + dtickrange: [SECOND, MINUTE], + value: '%H:%M:%S s' // second + }, + { + dtickrange: [MINUTE, HOUR], + value: '%H:%M m' // minute + }, + { + dtickrange: [HOUR, DAY], + value: '%H:%M h' // hour + }, + { + dtickrange: [DAY, WEEK], + value: '%e. %b d' // day + }, + { + dtickrange: [WEEK, MONTH], + value: '%e. %b w' // week + }, + { + dtickrange: [MONTH, YEAR], + value: '%b \'%y M' // month + }, + { + dtickrange: [YEAR, null], + value: '%Y Y' // year + } + ]; + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 100 + })).toEqual(datetickformatstops[0].value); // millisecond + + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 1000 + })).toEqual(datetickformatstops[0].value); // millisecond + + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 1000 * 60 * 60 * 3 // three hours + })).toEqual(datetickformatstops[3].value); // hour + + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 1000 * 60 * 60 * 24 * 7 * 2 // two weeks + })).toEqual(datetickformatstops[5].value); // week + + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 'M1' + })).toEqual(datetickformatstops[5].value); // week + + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 'M5' + })).toEqual(datetickformatstops[6].value); // month + + expect(Axes.getTickFormat({ + type: 'date', + tickformatstops: datetickformatstops, + dtick: 'M24' + })).toEqual(datetickformatstops[7].value); // year + }); + + it('get proper tickformatstop for log axis', function() { + var logtickformatstops = [ + { + dtickrange: [null, 'L0.01'], + value: '.f3', + }, + { + dtickrange: ['L0.01', 'L1'], + value: '.f2', + }, + { + dtickrange: ['D1', 'D2'], + value: '.f1', + }, + { + dtickrange: [1, null], + value: 'g' + } + ]; + expect(Axes.getTickFormat({ + type: 'log', + tickformatstops: logtickformatstops, + dtick: 'L0.0001' + })).toEqual(logtickformatstops[0].value); + + expect(Axes.getTickFormat({ + type: 'log', + tickformatstops: logtickformatstops, + dtick: 'L0.1' + })).toEqual(logtickformatstops[1].value); + + expect(Axes.getTickFormat({ + type: 'log', + tickformatstops: logtickformatstops, + dtick: 'L2' + })).toEqual(undefined); + expect(Axes.getTickFormat({ + type: 'log', + tickformatstops: logtickformatstops, + dtick: 'D2' + })).toEqual(logtickformatstops[2].value); + expect(Axes.getTickFormat({ + type: 'log', + tickformatstops: logtickformatstops, + dtick: 1 + })).toEqual(logtickformatstops[3].value); + }); +}); + +describe('Test tickformatstops:', function() { + 'use strict'; + + var mock = require('@mocks/tickformatstops.json'); + + var mockCopy, gd; + + beforeEach(function() { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); + }); + + afterEach(destroyGraphDiv); + + it('handles zooming-in until milliseconds zoom level', function(done) { + var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); + + var testCount = 0; + + var zoomIn = function() { + promise = promise.then(function() { + getZoomInButton(gd).click(); + var xLabels = Axes.calcTicks(gd._fullLayout.xaxis); + var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); + var expectedLabels = xLabels.map(function(d) {return formatter(new Date(d.x));}); + var actualLabels = xLabels.map(function(d) {return d.text;}); + expect(expectedLabels).toEqual(actualLabels); + testCount++; + + if(gd._fullLayout.xaxis.dtick > 1) { + zoomIn(); + } else { + // make sure we tested as many levels as we thought we would + expect(testCount).toBe(32); + done(); + } + }); + }; + zoomIn(); + }); + + it('handles zooming-out until years zoom level', function(done) { + var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); + + var testCount = 0; + + var zoomOut = function() { + promise = promise.then(function() { + getZoomOutButton(gd).click(); + var xLabels = Axes.calcTicks(gd._fullLayout.xaxis); + var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); + var expectedLabels = xLabels.map(function(d) {return formatter(new Date(d.x));}); + var actualLabels = xLabels.map(function(d) {return d.text;}); + expect(expectedLabels).toEqual(actualLabels); + testCount++; + + if(typeof gd._fullLayout.xaxis.dtick === 'number' || + typeof gd._fullLayout.xaxis.dtick === 'string' && parseInt(gd._fullLayout.xaxis.dtick.replace(/\D/g, '')) < 48) { + zoomOut(); + } else { + // make sure we tested as many levels as we thought we would + expect(testCount).toBe(5); + done(); + } + }); + }; + zoomOut(); + }); + + it('responds to hover', function(done) { + var evt = { xpx: 270, ypx: 10 }; + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + Fx.hover(gd, evt, 'xy'); + + var hoverTrace = gd._hoverdata[0]; + var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); + + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(3); + expect(hoverTrace.x).toEqual('2005-04-01'); + expect(hoverTrace.y).toEqual(0); + + expect(d3.selectAll('g.axistext').size()).toEqual(1); + expect(d3.selectAll('g.hovertext').size()).toEqual(1); + expect(d3.selectAll('g.axistext').select('text').html()).toEqual(formatter(new Date(hoverTrace.x))); + expect(d3.selectAll('g.hovertext').select('text').html()).toEqual('0'); + }) + .catch(failTest) + .then(done); + }); + + it('doesn\'t fail on bad input', function(done) { + var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); + + [1, {a: 1, b: 2}, 'boo'].forEach(function(v) { + promise = promise.then(function() { + return Plotly.relayout(gd, {'xaxis.tickformatstops': v}); + }).then(function() { + expect(gd._fullLayout.xaxis.tickformatstops).toEqual([]); + }); + }); + + promise + .catch(failTest) + .then(done); + }); +}); diff --git a/test/jasmine/tests/tickformatstops_test.js b/test/jasmine/tests/tickformatstops_test.js deleted file mode 100644 index a6538a5d0c3..00000000000 --- a/test/jasmine/tests/tickformatstops_test.js +++ /dev/null @@ -1,307 +0,0 @@ -var Plotly = require('@lib/index'); -var Lib = require('@src/lib'); -var Axes = require('@src/plots/cartesian/axes'); -var Fx = require('@src/components/fx'); -var d3 = require('d3'); -var createGraphDiv = require('../assets/create_graph_div'); -var destroyGraphDiv = require('../assets/destroy_graph_div'); -var selectButton = require('../assets/modebar_button'); -var fail = require('../assets/fail_test'); - -var mock = require('@mocks/tickformatstops.json'); - -function getZoomInButton(gd) { - return selectButton(gd._fullLayout._modeBar, 'zoomIn2d'); -} - -function getZoomOutButton(gd) { - return selectButton(gd._fullLayout._modeBar, 'zoomOut2d'); -} - -function getFormatter(format) { - return d3.time.format.utc(format); -} - -describe('Test Axes.getTickformat', function() { - 'use strict'; - - it('get proper tickformatstop for linear axis', function() { - var lineartickformatstops = [ - { - dtickrange: [null, 1], - value: '.f2', - }, - { - dtickrange: [1, 100], - value: '.f1', - }, - { - dtickrange: [100, null], - value: 'g', - } - ]; - expect(Axes.getTickFormat({ - type: 'linear', - tickformatstops: lineartickformatstops, - dtick: 0.1 - })).toEqual(lineartickformatstops[0].value); - - expect(Axes.getTickFormat({ - type: 'linear', - tickformatstops: lineartickformatstops, - dtick: 1 - })).toEqual(lineartickformatstops[0].value); - - expect(Axes.getTickFormat({ - type: 'linear', - tickformatstops: lineartickformatstops, - dtick: 99 - })).toEqual(lineartickformatstops[1].value); - expect(Axes.getTickFormat({ - type: 'linear', - tickformatstops: lineartickformatstops, - dtick: 99999 - })).toEqual(lineartickformatstops[2].value); - }); - - it('get proper tickformatstop for date axis', function() { - var MILLISECOND = 1; - var SECOND = MILLISECOND * 1000; - var MINUTE = SECOND * 60; - var HOUR = MINUTE * 60; - var DAY = HOUR * 24; - var WEEK = DAY * 7; - var MONTH = 'M1'; // or YEAR / 12; - var YEAR = 'M12'; // or 365.25 * DAY; - var datetickformatstops = [ - { - dtickrange: [null, SECOND], - value: '%H:%M:%S.%L ms' // millisecond - }, - { - dtickrange: [SECOND, MINUTE], - value: '%H:%M:%S s' // second - }, - { - dtickrange: [MINUTE, HOUR], - value: '%H:%M m' // minute - }, - { - dtickrange: [HOUR, DAY], - value: '%H:%M h' // hour - }, - { - dtickrange: [DAY, WEEK], - value: '%e. %b d' // day - }, - { - dtickrange: [WEEK, MONTH], - value: '%e. %b w' // week - }, - { - dtickrange: [MONTH, YEAR], - value: '%b \'%y M' // month - }, - { - dtickrange: [YEAR, null], - value: '%Y Y' // year - } - ]; - expect(Axes.getTickFormat({ - type: 'date', - tickformatstops: datetickformatstops, - dtick: 100 - })).toEqual(datetickformatstops[0].value); // millisecond - - expect(Axes.getTickFormat({ - type: 'date', - tickformatstops: datetickformatstops, - dtick: 1000 - })).toEqual(datetickformatstops[0].value); // millisecond - - expect(Axes.getTickFormat({ - type: 'date', - tickformatstops: datetickformatstops, - dtick: 1000 * 60 * 60 * 3 // three hours - })).toEqual(datetickformatstops[3].value); // hour - - expect(Axes.getTickFormat({ - type: 'date', - tickformatstops: datetickformatstops, - dtick: 1000 * 60 * 60 * 24 * 7 * 2 // two weeks - })).toEqual(datetickformatstops[5].value); // week - - expect(Axes.getTickFormat({ - type: 'date', - tickformatstops: datetickformatstops, - dtick: 'M1' - })).toEqual(datetickformatstops[5].value); // week - - expect(Axes.getTickFormat({ - type: 'date', - tickformatstops: datetickformatstops, - dtick: 'M5' - })).toEqual(datetickformatstops[6].value); // month - - expect(Axes.getTickFormat({ - type: 'date', - tickformatstops: datetickformatstops, - dtick: 'M24' - })).toEqual(datetickformatstops[7].value); // year - }); - - it('get proper tickformatstop for log axis', function() { - var logtickformatstops = [ - { - dtickrange: [null, 'L0.01'], - value: '.f3', - }, - { - dtickrange: ['L0.01', 'L1'], - value: '.f2', - }, - { - dtickrange: ['D1', 'D2'], - value: '.f1', - }, - { - dtickrange: [1, null], - value: 'g' - } - ]; - expect(Axes.getTickFormat({ - type: 'log', - tickformatstops: logtickformatstops, - dtick: 'L0.0001' - })).toEqual(logtickformatstops[0].value); - - expect(Axes.getTickFormat({ - type: 'log', - tickformatstops: logtickformatstops, - dtick: 'L0.1' - })).toEqual(logtickformatstops[1].value); - - expect(Axes.getTickFormat({ - type: 'log', - tickformatstops: logtickformatstops, - dtick: 'L2' - })).toEqual(undefined); - expect(Axes.getTickFormat({ - type: 'log', - tickformatstops: logtickformatstops, - dtick: 'D2' - })).toEqual(logtickformatstops[2].value); - expect(Axes.getTickFormat({ - type: 'log', - tickformatstops: logtickformatstops, - dtick: 1 - })).toEqual(logtickformatstops[3].value); - }); -}); - -describe('Test tickformatstops:', function() { - 'use strict'; - - var mockCopy, gd; - - beforeEach(function() { - gd = createGraphDiv(); - mockCopy = Lib.extendDeep({}, mock); - }); - - afterEach(destroyGraphDiv); - - it('handles zooming-in until milliseconds zoom level', function(done) { - var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); - - var testCount = 0; - - var zoomIn = function() { - promise = promise.then(function() { - getZoomInButton(gd).click(); - var xLabels = Axes.calcTicks(gd._fullLayout.xaxis); - var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); - var expectedLabels = xLabels.map(function(d) {return formatter(new Date(d.x));}); - var actualLabels = xLabels.map(function(d) {return d.text;}); - expect(expectedLabels).toEqual(actualLabels); - testCount++; - - if(gd._fullLayout.xaxis.dtick > 1) { - zoomIn(); - } else { - // make sure we tested as many levels as we thought we would - expect(testCount).toBe(32); - done(); - } - }); - }; - zoomIn(); - }); - - it('handles zooming-out until years zoom level', function(done) { - var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); - - var testCount = 0; - - var zoomOut = function() { - promise = promise.then(function() { - getZoomOutButton(gd).click(); - var xLabels = Axes.calcTicks(gd._fullLayout.xaxis); - var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); - var expectedLabels = xLabels.map(function(d) {return formatter(new Date(d.x));}); - var actualLabels = xLabels.map(function(d) {return d.text;}); - expect(expectedLabels).toEqual(actualLabels); - testCount++; - - if(typeof gd._fullLayout.xaxis.dtick === 'number' || - typeof gd._fullLayout.xaxis.dtick === 'string' && parseInt(gd._fullLayout.xaxis.dtick.replace(/\D/g, '')) < 48) { - zoomOut(); - } else { - // make sure we tested as many levels as we thought we would - expect(testCount).toBe(5); - done(); - } - }); - }; - zoomOut(); - }); - - it('responds to hover', function(done) { - var evt = { xpx: 270, ypx: 10 }; - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - Fx.hover(gd, evt, 'xy'); - - var hoverTrace = gd._hoverdata[0]; - var formatter = getFormatter(Axes.getTickFormat(gd._fullLayout.xaxis)); - - expect(hoverTrace.curveNumber).toEqual(0); - expect(hoverTrace.pointNumber).toEqual(3); - expect(hoverTrace.x).toEqual('2005-04-01'); - expect(hoverTrace.y).toEqual(0); - - expect(d3.selectAll('g.axistext').size()).toEqual(1); - expect(d3.selectAll('g.hovertext').size()).toEqual(1); - expect(d3.selectAll('g.axistext').select('text').html()).toEqual(formatter(new Date(hoverTrace.x))); - expect(d3.selectAll('g.hovertext').select('text').html()).toEqual('0'); - }) - .catch(fail) - .then(done); - }); - - it('doesn\'t fail on bad input', function(done) { - var promise = Plotly.plot(gd, mockCopy.data, mockCopy.layout); - - [1, {a: 1, b: 2}, 'boo'].forEach(function(v) { - promise = promise.then(function() { - return Plotly.relayout(gd, {'xaxis.tickformatstops': v}); - }).then(function() { - expect(gd._fullLayout.xaxis.tickformatstops).toEqual([]); - }); - }); - - promise - .catch(fail) - .then(done); - }); -});