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 zcmeGFc{tST{|An@6WJ1yog{l?38Ab-C_)S)d)CQ5_Lh+%`<84WWZz8~)D&4l+1GI< z`#O`gvHtF_V+r<7zDc$+~=O|q|nojh(Utlq$GzI-I59n-#H&^}JGj~8fC=%*N1!1 z>Xcts`1VcL(>-X&EZ*5^*H@&%Qh#m(estAtA1N&VO-ubn;=bR$Vn>aA-}38O@(;74 z$|;2zNWLEq{2e&R{y%>Ww)?+>{2xXBk4OGbYW`1;{C`WcGTBg~J~uFl-ul2kJyPfY zT%TG6*_#jHQG9SD!f$Im;A83q@6YSAg)M))zTbqTA62X+>^G|Ot684zNQI)R73+6ww32L>BA5%wB6_!pK@&_ z>B^=DqBq6ZTm$@*3q9K|gm_8{BfW;7+K-;yn}3<^z}p5IY`K$j4Cjauf*R_tQ1O(s zFhr+Ro3v2An%b5V#i8^%or<)OL=4f(bOJpaQlPF(T#5^|J$D@28pwi9pn8{McoCk< zIr7Pq21CEMR)=M8v6B+qA<-zKQIdf}1-^B--&Pcy=q*??>#7|mDpjT7qcB1;cr@>t1x{r@N%6Q=I1IB;emljZlw+m3_ zMZ`t8`lBQJ%-%f451gx1i+~9!fPFY1fPM{!u`83uY&8&)U5)Ih45dSRi%nI2ToddQ zo_`7)09?a`to#(3$<~4zqs@8$PTv_WaM}Xxv4mi1^sH8+9xXTkxQ0}KAuX0dT$B{X z8*%Z1{S)G8^?4Utl}WKHJQlf+zyZKD5*6|v;UAn}L?ELZ+1VxD|34-dPHEyU32Qja zcNUVyc1dR8WqP9LM7;B>y(B07eJA3rEneR{xHywr*xXlaKEAos9+%`kklpd5q|;FD z3k@}s>Tg~Gr=()JR`iB4D#-(5kPQEHxN^BeeY^((ixGrWG-hWd^Oah+$3CLvX$C*w z4U=1cVdUJz9n)|O+e#w8&usMJKWUzAklkJ9WeoKhcZ)~iPVcC^Fksw^K$0xKWA)DS zSxOj*7PsqZT}T^HQs1OE`xjA2~B%Pp^OB@ilE1N5UhYjber< zf7aqR?u+AL{ue^~{GXeccjX}Vpa&0ZEVauEGZ?!@47qk{IPYw&@o-g5CVM+tHd4+_K~O6VUl#bqNMTlFwyDF3wQSJ-(<2WeY&(gf4Oq>V}2*&iLi{6jfZn`KDTYe*4J<* z1C=vyPQ+T$i=4R`wIgo=sp7Q!*E^Gnq2;zQMxNs_&cklK+&h^gp;;OUr-B)TV^b9a z^PA0s3{%oWIcCR8w?!m-pM&jbB}ppVIzpKC0S; zj*@#)BVv19We?VuWJ|o)DC+zrEt&Z;q1RVwlBHZS6P+3ulXArbOlpmU>NXdf?`86+ zz?WW!-RV0=R6IY4@(gr_O1N6zZ)IJj-(&2m)WWM_iyqa6b#bNqW>R((XOr5Il=ZHx z3c@4R9gWGw0%(olM0<*`U2j3mu*ZmOJm!qEMLEox6Ga$0`#0VFLy2WIXj2vXDQg-o z7tKlg6m3q%vmKr2u(Cd`K!K1%oQ5y&#jf1xc~17P3^?iNZM1NsSVeOSzhSXp>dlVo zr#W{)ZK-k`gbdk#RuZ zLiC0R1c9F)Gaaai`q!VYwt-DNrk-u53FlHUo!^5k&(OnXP-Fg3>_XlD%~#nQ1JxlJ z@f|l?im$43{11rXJ=x_uZ;a@T)fb6YfSW(mj*+O%S zO>9iP7-hJFC!HcMLh`|B4pe~h;l0JF;E%q;r$0}TsH64YPT|lt{UVnwI!3r4>R{)) z%ixho3k3_!P6yx{sJRiyqSF9$mHptLKS39Ayle2k{?hfcG5>f&U~<%VshFiXIg1}~ z7Ug@gkl_(01zSrvY1Bdk{i{ zL7{=fUi!!LZOQVq03Nyy@ZtE47l7ria=rOiY_^@OQ?5>F~d; z2#x5Z-?_11Oene0^B*|cs+t_u^IwFee*x`1S~5BI30H7z_P=nby$7}xP~m?cdC&l^ zU;9$?Iy{haM)<#-r42n>`X863dO5}VF|H@}y_}M?3PtH&yVLcWcJUDhb@yT^_?TDQ ziaMVr>B6{wfv78T@tJnG1!T*aO7Aufi5tU!@X)?{*$Dh0+8}$Di}2FVGudMQG}o=} zbkLD<52kFOtC;^`G=7rBR`+9=hH{UTm$!3{Fr9yOD3}H+teL8(@Ef12A{}1=cwrZN zJzb8V`86+=vIIy1HU5!@r?G2Gb>M?7w$_jbp-Qe=f`%mYf#<KLMUKO$TP#DY-m*mpB+~h0dS1&mIy-f!>gB7 z^`wQt;qy3vsA+%FezO87<-D4fU1m|`Uq~rU4g*^LGp$MsUFW<0I~J$wNnwX>?4?(R z?>`chq5c?ec)GHMo!wEzaNkd_$wq;hq@&N57PH9RylR1-Ul@+?jCn#mURUbT-u=ae?&IhnLL zl^)j29cQF%<3n7z&f-+Oro6DnF?G^DxSkXFP7c6i{=tdb23XVI zJw$x=G)lPuFyus(2S?dGyn8TOLvr{e&a1atI$&*k1E;0p3Vhq4wn*TsS0uXi7?iZj zZCs~UL?FeNqiTKXP-TvaaViDUHYjEpzuuk`1vbPDUj4a&8v`baURHqUfCb$Eg4)W1 zQ@C{y>qc4_`bBPIuCyuo!SWLskAg&>#U@d|%`anqDb>o@c^P zo9*s^QS!Qe_CS-Z`v)4So(?oj}c$X(T0#1i-=@>m^4CCW}BxcFKSxye2GRqEzbfU z16!%d>Y>4B4G$=oEUM=#xJC>gmxpRoGM}RkWn~`mS!z}4%88^16CyECmYSJ-;P*M_ zjnn*^UW+(YUaa{$9H+_Fm+b&(Bw3-KO}F>Up!A$2J*Rx-L|Q;4T#jz;STlcN^lU-3 z5O4O#LgqL3U2|ex&+SY`s`%nNlR6{4O`M(aiOLCDwrKN)5Ziq0(;h1|MY+3{YWL+; zGsV7?+T|G_rBKR+HG_?z0A4n|CdM3D(7D3dt%*`PZRbWGx9OgU6*duyGx77(6;W>s z##cPpNr?sV8^1|j#;&@QBaD3}P{O=QyqG&;NYqZ-j-t0r0-d0B6L1j-LdsWHkrlQL zb`z?1Gm|tze+T8YVjC~-Uz=xKt>-@CyJq3_rZIwxKZFf1>UMg8-&NPtMfFe&B$Tx- z!N=mYf72|J1kn$Rbjd6-RP?AKh1n5edq5ae3b54N3?ETxS;`_A0Vk2<+8)H_2l)g~ zD#N$-7I-?;Ots%!cYdSjzk;>!tYq85!6GB}$$keBOY#>fK=hPwfDQ=VL;QMaZaTgI zvJ-nHdFRfQe_X4~brmYMM5<>{KpJvYqp8Ex7=_RDz2`u1bM;fTUULYQEYCZwqXWs(8 zm(&eieo~n3805S#dY0oRUhS%m}_snmk31k2-d=l~_ z?pzRxhV%e*JqN(6b5V{SH72JK35Ozjscx>Zcvg`=Y@6*S)Obyi+plvn1(Jeyp=bXt z;cm1Tq0Wv87Q=x;r=Jf&Qnc~n!r8r*PW>*eXiJFMVDe2Xvwc-zRhre~38|Q}qCEMP($-_e>msB3sT9 z*~1St9ll^^^etw#xhw&av5+@twI%AjV}FuAfe6%MxA;MnQHxTqn+y%1Bha@8g z?5{uL*?Ub&&KbKFmC-D8u1eAB3TxCBLOka#@W`+Hc`8jbWc3$Rk(7C5mAnawWa-~K``^Jgu<$fA_GEaW15>5<)g zG^8G2beNncg92FoTb{u#Fta1a4mT4~n-|+tL3&Br`pRtCELf{hcEpR{rU~iOcD)W9 z-N)=iDLxZ|7Jee9nW=F+i7Na^{+U3@{ds$WIZ=BL)pu|sdKGN!;$7p5-^jSfGHJ&5 zWu)m|=wQIOvJ&q$kA8q3SfT(Nth+{B?qvfXN;e$= zXS?EV+8J?>#q?Izejt?t28)6zXM3{ffEl;}772l%<0=8{Ptd{yd>}(JDI7b$eg{AB ztTMK!iIcsGeDG1QU$}s7&y{2c*`U&6;CDkMK(AZpX%R_W{8$&BYxtMppx_{5^}PUh z_+X^oXF(V4J{&*+z+Xb{^&TIW@?K4URX|Ah$LPUMJ{baaF%r!TB3#t!AQ7Wd{{dt( zD4I$TAz^o(R~KoVkWp~WN^w62?wU3i1tTO%ZGR=>@pOFEA)!T%QAc2=&XQFgKr=bJ zdQCH-XDJgc4>Pgty%r(&C0BjWmO%p1n*@tJDo3!gL9#Rh-d2*5z&aRNTKA(A`G@zA z{-mOY_Xw)@eIdIvDrApLl;G`ulGa87ZtLp1B$!hAZkP=G`=%K!#`TZ1{%?oBhaWHq zxN#IqA;R}SjssPr*un7ro4=*yH~o;69LdQ(4S6$PM9ze^d&x_oY}h2JxKs`Va)K7q zhv*f(9YMwD!B1M4aIKM>8WW!0sXjuD!R(J~o_c)$UE{%buE#i0A=O2dZL~H|-Q1bL zaqs;p7v`-`5J;!_Vaz`$K@G)CO|{wbJugW5Xh;|CdeT59N4RJ@*xv0tk&*+pwd0+^ z=e{yDbQ zRVS#51$;--%0IF=tzksOSa131u@si`#vlZ}OGb}l9Vi87{boH6KTKY03>eXhI>)>8cppw$Fz?s0_VbE13p9R+=7 z<^;u-su3^DKIef?Y%MkXCYn%aZYNQ~-)RgySHlouwF*B0%+$)GDMAXX3h*W+dK`o? zV^VOxZwW?R#Rs1WpK(EU@SI6!Erq45n=z~(DGG@^b!D*)5R^`pNKmIBprigNO~`=Q z&;qOYC0)dwDRP(fPl>FlDC4;ZGvgd~RQ}N+!@7sy4%S%YVgt?(fU@P7c@iqJy3)OC zXI!!6jwh}kG@HQdU6ieJ0mmv9C0+S*111k=8vTeFD;VXz zJqZH1)`n`qq%i&WwJxk&VO!^BUB>gip${ymkSzV;9-M{-LR-a7D?mXvem(DQlpOil_-*pHfsbjcu ziw~ZIyr&Ip(;JW;Q_sqvX4bSbD=Cd+IEacVcS>K zZD_DtvJcPja;vF90Pz;R1<^fDx9SeLxlkU zrRc~@4N>eUm8+p8ie3+9bv4-JV{sswIz( z<7S^8D!bufstA{bbvbr7Hv^`H`0(=~y{JaMd~j)s)0cI%?RXCqSJ~n8>MMKIda+v@ zmhxIEcg$Xi%3}4YuSTX?iSk4!^3eCLpe5n%zlhk`A zL#8hPGocp`o;57kM%iTQdXqz9iuZ>;8c|>Ll#*)#O9+$J^G<{rDlUk_7ACHQrSO~| zRw)5Cw_O2bAs!s3Q%s?TMx1Ajv?O~NdXvR-<^CetZnoaKwwObZjGw4OD&B*4eMhKB z>!h!(ovRdDatTX0BTZx6yNpo5I-ugNhcjHUHip_gS2#vI_J_FJO7MIGdMhb}5Z0L1oJ%@p!rd4*k12T9%5a*l*~1>q#E$f zHKwl#DOsMmZ0KWrgRch%D4!@C4v^e>X1Bs&5OasQ693wQq2h*LD8NRZSFgtahQ#y) zZ3m^US+5Z)&r)HyIy27|*SccD?r<`gm+mS4K`qhOt{*d{%jZPtUTw!F z&I_S<1ijA1ll0AK#p@n^xXtYSDUGcW_zB#0OIH0XLV?RoLkG|aj0T-#x1 zBw|o6uiwhApx|ZryGA=LbZ(7W-Gv5Jj~k;%hYR8)N`x4OiH(V`Pa(kyCOdIh-?GMb zr>ME{n;K{7c<9+#s$GcLca*4yVRA^P7(#42ovVX#!1$_a@52e_(e=u+JhLTc)t*}7 zwmrQVU@h}!`=F$PoI*9_jFI?;A^>?H?hoiah?#GuuZi(AygY!(^hGIv>)-0CTmAx{s3!cx$q@}zKt zK8cW6oY|Kcb%JPM?^8?DSVj1o$HArg^8%=8MQxKWwkug3IR}+g8!5MEIxK7z8>@>r zP;HQo>(6F<&bpPvDJZwLmoK+)qXtyg6;3B;FJBP@$#-*u%j%dhgG1QXC@X>k2WtFm z`}pH|;A%0|R53+qTHjhBy?CL+uEVZL@e{?nVKT(`#ktkg9mO!YiT6N=hnG~GFK>?JsSNkY4d=()X z3*xjkoG3XD$8&DfnA>I%**x>7QPVB9WHXP*vnCerG6mfc^SgvBiBQQr*?bjbNA~2# zrKOHRVEXB6@tPDbt;btBUiuZZwdXs#kmTZL%Eb%rbLo`h{th0d@+B6ZhF&*^@ayUIY>^!X{A^0*>Rcmu-5NJ&0AWs`W@>6|6O zq?Eyz^M#J>HE!t-KN5^AF6MtB6Hd@}rMT1$?jr1VFk?ufl@nzdAvAHG6eeOs4l4(K zmeWQPh-S8~Soh={e4eFblj#3KJw|t{Qxu)hE@-SsSBJBdaxsQWdu^jCA2dX}2_3;ty8|2X^+$?=O%&Txp{gy~u zd-;b}MR=!Ao%&4I?+q4X_0`gnqRTfPz$U~=3%i00%37?FCJ*DNV8lIfg zOesou|Kxn;VjS~QYIk`I3ZW0tP-R;2*C(`Grb#q%&;zNhLa;bL?sUI7R`+}ad%+1K7XE`l4ymJYepPsELudTRPv!dJ# ziUEA?lRj#ieLg^6xXxHIKpGNRLr?*}f(qeJy)I(h z=sfQk%L{=A`+YvAB~eg84P{6z+6l#<__Xx?Y()>g4{IIc4)>(3<8Fh7Z6lOzM@M zXkFA$J-(t`*0AE@=C|#}kTw+kQNh~q`M-S9?=oFauWG8zs^ zQA0@vvr_yF3+lGL3|`(g)pl-)6?8@)=4ot|91#GOp}az(p|K!ck^MgAbCho$dhq2P zKSrXG%^#l8s#~t|8DGe<>@Tg0U7Ihfd4JyG@sYV+Bv9nWPO0WyMLn7~-V7*{!$ueV z*Eyfn3P9eaU%?8F){lysBk-}8Wp-RRP?VW#BmVxPY2FK!lkTzSK|Fgrue2@FceS5q z=izY(NFBu1K+W^exi%f=*6^_gQDnmwd$8h|C$h(6mV}qyjvS5nRmT^5@^6@GUKR+3(Hh zcR7YacDYi2>~qw}k!Tr{Ar7f}M)Msl70LlGOcgV%W<_ri1*+RjQsbVtXOj4p`z{K| zJ9aSzB#RlCJhJws&R8mE7FD5~@A@&%rwW6$ zjw_*y9S{5#U$aOyzawA2nQd1~cj7pW=laTgr0{KFhatU=Q*$pb%1PvW5q48HF*lfx z<3@SOden(2`Qc~SKMF_k)hPQ$2m~9CdNv=$l%$MX7Ae77o*8{(Gu>RhrWk!FZ?Cb3 zvG(FTawOKKGm|?_#&xP9U7|i9$F9XKu_GJ=#$cHs%WlamqGfZ5Ahvx3(p92%#iq#G zywtLOeU+wv$N|5Vsk3uct_tTpPl5Do63rA7<1CoXJZ5>t=0zunl}Npge(;hFRLyev ztw#>Lk3T#9_-xyhQHCjDaom|O9;bhTAWa6Vf|@f5n`XSuy$`UKdRts1` z>678Wr*$_P=y+^_Ss#29D<})8wB#7N0%I9>S&VDLnlE@2-)T z?5^19=0b!TZ%8Ct-%5`|8X)=OpL9uKYbIJVMZC%%iJ9zNlXaq{8GjkrwTBDEq0HrN zLXDYwfA7ULNo>+)cSe&$j&ke8dEQ%YwBfF-Wl&ghbr%N;a`pw%hP=u>cfqfB@EX#G zf64Sl3|?Z}D{YqRT`h5G2$C4D5nK`yUIo5)u@AXjjV8#O7_+#o+!uAd>hHOzu6nE? z&)|*QvG8D=VZ8mdT(&m88c$~6wHRayvZFjLhAJGc1JyMUopuSEPb?^ciLAN&@NV3W zVwMjEGLnQv0NdMmH?-yvcwZ1A^b~PW#jYe(fKVx}PJlc272rO@?q zylr47A$z@UDN!_uudrG!D=| zs~Oh_YWZ}V$gi(s=boAjKw>L^krj8?i*4QU%o2RS4)m7mv9n zet;KC9!+}7AtX(bGs^WCu-(@#ZDrqnz3h}GycO-+WdM&Ee-z9b%U0m0?7FhYm*d0Q z3y^CS0n4tmmSAQBI8g&FPb4!=tHEchRvpdtI8pUOgO=q%0I5n(E_GdmGnw2zy)&>zOM}q6@VT=4cb<2uxT@X9NvFBdV7@z)A`VAQ zt7y*B+XniyJbY_({v@_r^Ia9MG7TEC(k13{wEPA&)`8|`eK!pzX#XGH>(Zo!3wfDb z`*mwKKML*Zm=UrUAupWh1Gyw&WZ@U(lF$Oyssoti!_`fXo6OY)yS3T}g$NTHMzi_Q zTj*S0hoKHg?__tz9!2>ekn_pPjHw5~uwYk_$&2t{tRXzNm>ed}Su`T_W^6%XbX-9NG4T)U^{CyC0pVgLZaM!G8X!&S=S?2JiQLQ2a z)o$~n7^_Zt*Or-h5P^7J%(Zxl2`4eYJA&d^xhnFO24m6Xw*_fxNhJcyv3iJaW`NI_ zAd2IP27j<58bUv9)^;CH|E3(TXHJiCRl2|->xAu6QyJ*kQ=afO{AL;4^&vp=xea;E z@aB9h%u|k3kq%nZ(4fH!(m`DDe6%wWNTUB@wRMz*F9ov>3iOVbkE}6*P=}ugC_Ip} zC$0WmmVW2CI-jLy_4(^q3MU{2koSt>7T9k7P;lMz0Z5h}jKg~?1oDY@b)Fva&XYdL zPr$CjaNzFg9_=*lb9I0kr}w%cPN6uoAQ#}q{bOru9|H6X`L+%!>njXLYrr zm%c@DRLi$S9S03bVbGJgHYC{m2i`5#A-ZryCYoES*zPeqpX#B-*Ub0u4WfX{9SEO$ z%I+RV=*uKKuBI*3y9Ez_2t^@~#>CPG$+9D>I4WZ9t>bAi8SECBi1%7q= z6h{d0b6c}aohwGwMKRncw)5xYNOv z&9+DQiFYMkv1Z3GARas$tZli+!2_$?FfB%Fe9k0}-q!t?Aj<72aV?k0hEu3vT)sfd z9(u*wcDbIUFqyCEr-=R=j)SDIN|cKX#2%gC==Ds-SXK=8Vdz}h05!VW4ZF!z=yw$g zcc@t|BZpi=L*ChtlfrU6ESJ_r$Y2i>w8uD59QNcUS7JrLnR-jWnZD|ex2I6kr_Df^ z*F~kLObX$_9}Aq5<<`XonaFXVu2a4r>NHA?vrL26kPh=fZ-DpI;8SFC^n+bt-s4gD zoIgMg`GRQVOM6VJsMnW2w2vET-otMvYgchRY*5XTm_lKe8x;X@iF&wlsU+@^E__8z z=W^GM@k*#gJ}~Eks9ELSIU35kLue+y$6m7>XeOIYv!fVS!qfW=q2ZAIkhr7Rd9VG% zyj8*(L;5RhaUerKSauMwZlLXZbgq&Da3JXbk7p#Am4G5m(aKmiAp}yOLn?*m6o_6D zP9TtWW)j<*17Dm?duTB3$>4eun?>GPm3_e1;zEEM(qO3di8rMF+t(m_;tRg^7<_AQ zq)0CL zlv~r8T}DX0eQ|vK7%*?OJ{0>p&i9{COp(EE@y;Oe(J&+o^v zUbpfgU@A+iPXlC&X}ek;J%HpybB-m*PgIc>9)01gLuLXlgGbAk&VlBpzw&}vAT8r- z;817$plR6UDeTtGmt6|M?Ci3m8W;f+l*03{#-fW`}$d4yl5HLa23Gq$78pywhx(ZHQeJJ=rPq zQPNB8ueLV=+?dE)k*5wx?}ID9@hV`AGrUEIKw6{Z2?V>v&$F>GojN;OS!9!-|F+8voRG7f=7L z;zO(=!?&_KQPw~5@2Fhx%JTTqTPvK~Q;W&?5W^E17v_JvSFZ@^U<_!n>_!LW)}F{b zW)OfJ{#uH-lS~1+$CwP29&a5sfSV84vFx3e#U^_>B@xS@nvCX>9@z7oHkVQtKxGCv2_mlCqk4j0=foy%&1r-D2Q@W<6MiC9<<*~E#O&Vp{A__G$E!zR^{c`QZ453r}aV`>+uCw-*U;D_Kt z6}PT|Qus%KKZ+G)cWPJ=`h{}Fbw>IXA6Goxf)tZg>9OAuo$qUM1{L7Bp?4+;igZBp zmfu)4RJ~w-cy|@+(bGU4azUKhDcoOz&~1U}4TGF62W8uw5yfmAWQDbT&l5DJ3LpS^ z9U<9g7JPJ98KBy;Mh)IPG*f5w@oj?`K>36a{aYE|nmV)|q7y>Z62B)Q8~C#;j)Nl7 zupb8}g{fVLb2|?zJH}Ti62`vgRk!gcYz+~T!{;vLmJ?eMKG0x{w2Mkqs4>kSknW#8 zAdsIe%R%@s#EL}dz}wisT^0BeM^6K`4aPUD3O(@Gvjm-gR-<%=Ho$;BJibBu6rZ_N zLkV*Iz&mJy({(d|E(;M*Kk#RN08-|zYpXI@seNYZ z=l=H92VQek=+-k7e!$DdO~WV-U$W}hE(J=HM(Q}aK>sGOm;^x9Cv4%Q4+RO-e>FJH zKe_--(0)lwOiN~Zh-{YffeuU1Nc2IC3ze$;wl5_dAvsasL_-ZhbNtnDHOQ2DULs>k z18(4rT-#KFZ|&w(QrH~TB|ur9U6idNEeFB@Bf3{igVo1{tUR|^u&u!s;`8jN?%Oaz zUMgv!qgX1F=G*l$G$+ zlL7MacGu;~sciu`Oa7X5|8h8eJZO4UVE9(GC@E}+Llw>yVnT}c{EY@iNv_I+r@<`mt+ZI>=n%2J5q%Pnrf=7ttqL!kbjW-u!&v3| z3~J)>Gi72)$qfV5HKU5~q-i?Wy$H!E{PIv`jw-zKV?ZDALH@R@B!6mNg0>hf=1E-a z`{vx3MN%05a!w)epxE{ovR)TNO%I|Nn1?yeo(lV1^?;fWsFTy|f4xjX%+=A%W1XiS zYeLx<=BI>EhW?A2BuYD=q2e*#s#|Z$cz}n z%}oqD0^?vTDsbPXfNNA(3NFfc?`s+Oo@oatcv`PrPrH7!1gPe3pTxy!$s&3K4S(cP ze(YlNm%agoAsi8|LIt&`OyOd22w%xH`0TQsG5=5=2m$bCpK0MhnKsn-=jI@gj<6+8 z_O|QlWV>!St92>T^X2-{K`sAEO`;K__jN`~w2A}l)2#SjUA+R$Z)Z%5t`~u;n%2J= zO2|P-vRpi8-3)bZi>_(iq9|tvN(gkKeDBOA_P12@*bhag^}AGo)QrijW4!p~Qz0xL zXR_@~*sviopz{6s=UfH2+wvcGf)_47>>ejl!F~VVPBpbA-f8k_*`%t)o)0DOiKXbl zw332K7*c_vPecq|?4Csig115EQ0CT^*yu++1&545pHl$I-wKwE?N-ZpmHAL6U)nmI z-vxdEQxerG=ihIXA72nx zAY(eCZtwL1zrEVp>fDDO&#OtG0kxJiH*VI`U>@8-;nC!a^}>GFLgm(PNiDn_mEKwW zmjJL%Mydb=-s#3lY|B{vx+8n0U5_-z}MtoK@ag3#&bo zF>P3*yqrquz=?8FrNPeA*CjYsUB;{wc({?_w$?^c9#18M@NN<}D0{Y-n$0Vwh6=)qZ~0)NW!z__FOhM50RPS1YPj1d7NR&)t5uJU@th->KqJAi5+deU4H3 zO0DIc?)(>Wb{osnQRmmrmCzpdpVlS}C%`bn(kKizb@ZCR@Hi9A?d zxOcKwwz{<@0lzW!DXd2RF{R1}@=P%$>gUj^hj>uMT=(%8b80YjIy2jo#Z` z<;=70?Ccc}|JE4yV(qD&$;j!Hx~;6jMC7VJ8yEK{dCd7vQ6spw_74OAH5UT1fW08G00b9XG6U!QS+z zUP-H}@}kE24y4^(YMJ1@0Os=ze&o?fgnAR_jC)(Ecwyfdzw{t6opKu)32YO9XGuvz`hu>Cv`<;o*; zE`)Se54~RUbRkc|9)IUjwgpaNpP4x*e%{>%_S3+U=+me_HHwrU`c@?X;vs~v+58)%s5K}0vGr;m#ZXmk1z2|x6E!bk3kM^HYBe&7$dpGoHTR6L25b@&`M zj)@Ugudd09{q!?P{O3d$oqPC9;oRx$WKggVsmTKc{OUg6#}R;fyjq zR7&-{Q>A(RiOO$1&W}~8X09FFTU`H(c|N;%&S^Q94%;p60SYx_W52bGF!KiVR0ADz z{BN57A9HFBlKoTW4=hdt-rF^Mfrzqy9+q?LYh_cgoYX~tn?<$v5nPFE_t%7=?=9|s z8=XUT6!1Yo6lkw0ng>evGbeim6PN|C3xj>t)w?dndxy}e5`~@n z_riW>m$3UAfUu+eqcGrock9>?J2YlNkBwd4BkCApyYuDVP%sVvDr0QH3IKy|LUv6HSCsL3&?dRXe4UGw)ssf4%+Rl8>*C}~* z6FdMBMSik-9Pd~}+Qp_8Zs>3;icP1oRBV6Mq|)H)qh z&klWTZ!zdQ*bVLfp#e2P8Dd^$D@eCoXOw_nc&=}Bg{WQhe^CCHH{!w8d>`iV$+Dak z{=fOD>GI#?TIsny#E7^L1H-zECWbA3a5;+7`A-7-`8PkDwLbln}z%tgNuNgN)e4t;!jZVrx96N*zUM)P#Jb%x$gU1lh9?d zmahuxZywG+6cn87N1ON8WQDax(DF?4EYYfl{8lc(hu8GwPxG4?@qBMXR`qflCtB6w z-%kcRkI+}v4?SsaTCeYo7*{FOug$8My!dXM~QCN?k4A8L-DA4ybmlD}1Rqa3KJ z(x>3%zDJLq>+3lZ`3m-1zB-jow662t=MY3dbqT~bc>mish<}XEWzv{>Knto&1)u#- zuoZO?-ixY$y7HU)8q9@>bT%q)f=2zo+e4sB?BA>(PA0~E2= z^=M@b#D$%ILrCw3?~C8D@{epzNLsc&75}SgobMG+;_Z7wpfy?4qA^_UQ%*_sj*#Mb zHfR?oy5I8avv2sPRarEH?)0VYj_Uq|PLP)^lKr>71cJ&X5Ntx|e*A`>K>gvk;x~7%Z@oZwZ!ihl>&A6D2^3z#&_mimA z`E5DFs*_Hf9NA2-aT--2Hs{+ThuybBuIfam%i#_kxvAQgH;rE%gtZW>?m#B8%}_jU z@ok>?V+Sr2{S|3rY)Oky6rh8S+~-GHCw2ce67Fxo6J--t{#m$vtF#nhBE{aN>i*i$ zv}ZEs4zFp1YETC(L8^Rw4VFz;@^ zZ@b2Faj7Urcw1(!2+Rl3Nsrot-k>DYurPjP`UQ_&oYwNMqW!SDmz*H!d*e!iga)Xw z6HpAK6qJoQW#mr5FjZmpC>0$RjK6DUv`+k3srF`Z zyk&f`MY!;!;|`%qeK9~LWiA>M7Q)6&uC;!X+c*2oWe}J@_6rFY=MMmAl05Yknm;n{ zEk0Yz*ET^)V;v?P=ke=OwL|v(ox;s8XcR5yyqno9DLI^$X7=wh z<-fBX0lj#fsBfHpZ*Tu!IixaG<^N`S{=a+#>IdO(^Vf;v&+i(6L5Gz$pVRF6qpM&P zdaB9;zg@%9#}xKpTMu%DbcMVk)_3zHxkf%_k}PnHYgqX<%DwG?-+E_Cv;6j`d^4TC zQG`p2SR4aLT?t1vxL4TsbM9=m?+ADdIRz5OlVlpEe0j?=21b>}uAwGnGZ)>2mw%p@ z;sRX&;~lwn2q`#B5YNhY0ipdM9(yJG$_?uc6z{t&ZKfSUV|E*qsw~_+$ zyDPK3E&6rqx*xsQ(t?FVO+LM$k{8f7c8l?ug^Wak0_CT(-Ko#oT^4|bh$7DVAkHok zB3h1L zFI-t?42SOM*Dlf#-pFq%O3f9$@lw8re~7~N)3vhp} zXQq}FP+;EY81a4(vO?(EK?bAnv4KMWt=TK+T<5%8NFw~xqb3roYUbK@ zDLM=oPTT~F;%PC{pV@Xkvn?j!d{o`REY1o#Xv%reZCNwwShwX)cunQU&Lpvbs)Oy> zL!C|he+1Zv*RAK)DZmHEQ};8c+PZ=%w5QlI*XOlWBQaYG9xcQ%tkQhWo1fpcbY<&QCn~DqB^-xNpS5~_ zX?ttPr9I|jx@t?JluP7}e-n3HW6EYzN+U-gv)K%!KVW%aSLUTc0JXCyyu-PM@h-H- zprmNbLe%+C>#5ZMzbF}kTJ)4NC(hlzImtasQZ3pOp#>(srub;ftyQ)+2_lC$K|_Mh zQIeQ?CdZel*RRw*E^A*`o|`q<@v`=JbG`N9*~O-~Qda~L$-KGXQOA$-#d${!C5zj| zGJB3iT!y5UI3HBOrGo*oLX4hWCKh>IsH)8;e$R+jKkdp1)l(}iT^C7MPOb}l7}AcA znM`zvWbPR1_uH7atPi4#^mFQWKn_PbZm;?8ctolb$K7u4NY)WrPeMWT%6xH0m&|{A zDW%20I%&Kr552Rnl&FKQkwfqB?MG`~`}*m1gMr5udRL)kxF;mzSh!Za2o3M^{_d z9BGdv&7N6m(`&tE|FsAmn2_65yEp(mcQ8Os;o)dUTW0XC`(fL-e!JHGN3Gf1-oTl? z`UKN#(S)Jxm8e!Gaiw%A%OdPj4%@26MqJmb6+hTI~$^J0?*EA zkB~!?jkJe+*{((idW)~H*b`?MFUv?<2Wmset>W#P&G7n-TjJm1aaPKWC)1w_5OJ!e z)3{y{!n4Iv@AK2TEBr)}r;Sl+gI84Kv9{&49onUFG4G+gJ$Ol)EeAj|>FcJTkQC|N zx)%wzZY)ugnOXiQ>EQ$t{W0 zO(|o)UGHL=p~_0_lk4QM?n|_x$=lm_M+6uN+!@R{&;q=dyQtN=Rafp(_9?`gsZXV4 z^LK0$XP;;sT6{?rhcH)iD%nYZ;IZ?aPEf;L;-j&!KuG`?MD@}*sg~%g4fdS!NuxGp zf6Bu{XJm1;+a-g+Lp1VCjOt)jUxX^_keZ4jM|yeUks!Lw$q# zScffj+h#yfXF!34%zVK)lo5Ho$ab=Nc0YB%mj#tisW@?&){Xv2RF|Ly-m^0O{L+=N zLj>rtbTzPR_` zKS>L=sOoCP_Gh0rjKPctX_iT#4ahI$Q7ewzqzxep4<{FbnM(0))b!vh+URoV{L=Z6 zj86<#Ct#G%c%bJhdSk z-Hz?%W{;4t?$yATrv)2ZydPd)&c;c-%cYQ9W1QottPVeYF|DeqSK3HJ{ask>jk|}( z)pZBg15=>irUtZ-XB-rh#tad((gTq)@i-^tw-qMSS83LHz$T+ezgFli2o{mJsG^Ecz&i-*h0 zHgIhVS`Diymw^KrFWk@u8Hzj-@xlRMtsHry_HcfV%l$F$Hph>tAeggmvKQ~A1x>57 zdZ{4GE5Z(~+((zitK`y%HMnD6aNDn1&nI&dP3oPJY;a7Jn~PNhbZiP3(N=~hE` zAkWs{g{An&rMWc=Y6S!v)LhbO=Q9H)Zf)hI?deB!BT}dE!yPBG61xjNW5ulo-al;w zeB6F_H34j9KQwK^=DHA59)zf(f`~3$-D~%*0Pcl*5;;l;Yg&fZhDCJKX6&Su$Btu{ z{#L}0TV5k1DLtM7mv)_J?g*4>wHR6|9%~)^Luk`#4Jl4F#YV(>bI{Fjrw4S{+RpzA zMTQ^8WnWycWf$FrZ_7W4PS5gLBA>&q z+Ox>j6cY7T#{3YbB$r!nB1-6Lca;oAJA&%tMpU4*jNJ%(;K68jkHuRcO0Vz1Cn<|+ z;$ACzj;GAX^y)@3$vx5oH}dYt!9yVnj#Is?L^oqMb1u|2$b&jYRp8`Mziy5`wtS#} z=@(#&J-za*{^vRWqz5IAVPCtSy{Z6%4rZ0q_U+93ziB@jbJC?QVTrpM9hsZ39r9y zlUi73?BOAdnk?_IKTY?KNo`x^e9$g$_zruUL;srcATg3oWFb?=7@s#U;^nLeEl& zKQxr&l#bNjVw*ln838)0W3;DH>M4vrcEgAYFlsztCWw`P=Vgs}z~oE{rWMo<9u@fX zWWlmIN-}tiy7ws_;ZQ&t7edx{vVv@f)P#7--Q&=1DLpcRbD=-je+N+ZG8(l?6KiR| z*hr?Q7uXkq>cG?oF+S)hAT}O{5i#9pU*06)ILghDs&X#0Pu5_`I#)$xWE>1C94tEw z4iVkrRIRDLe5|l0=4t|NFUM+%U$(%F&m@v{eVT;nJz&_gF%?A>`W|$MSM$^^yLW-S2 zB01e1DK*_}XvPNno?xOHcj|QB+zJMvG#acmz788t*>0s1iX=0#;*GYQ#S=Vmx{r7h zcPQUjrz?7DT9c@2fdz{{=it9S7qvra++2M=vR0;9xqtCRB4wu;fBZeh4s#tkUG=Ww z=hKdTk9?D;r!!)O7$w$u*rXXd?QTSYuDt;l9bFCRvt|Hi`k=r)byo#WAht!&;MZV` z&Gb8%bYjv)&NL2D9pujb05`|IXm8mX6H}_6+s5s4RjsDr|4s}9(a6CM_rU8fnd$@facc&4V0kf7=Br;52c`z2gZkmlnu|X6U@Z-N+H%quNx@EX~yAo)W-xE}Oz zV!~wz{~~1=lq{lo-Qq|T^n`_u=DiH>8)@ctROvf zfPK$i68Sy*sBY~!;Gp)99&6gw=xR*-+m~OE?u92Q{ry*}r{a6zs2=?IyYF74vT*(U zg-<_w*mHmiXA+VWcK6#ysN`Wy-Dk zRyD$%>9zmhVFL7tvZG6XiL!+AzzOLEj9b{zr>nMH7}^LnEECTSlF$e)tmEjIfO1VG ziN~%Nyf@R_S`+-23p?8`5}ZN;J*-QZ+K-*xTNozr!^0ey9k`P?wFOgdksA2(d#k7S z2n|$!M9H#aSl>`tK(CV^A?PEt)E0q};e?G+YdGYvH2C*biOG3b3+Kp~H#LrDZ;b)V z-r}1|Fy}P0>_!MZhYD<)wBO+r*wIT8q+IT0M@~$!N<)Vz9HQ|T6%~^zytkpr;J%+5 zwZf+dIWZDie?NB+FMQ$@NhrnXZwLCeM_zCuy#jG++-0u21iGUP2VMp<;LC5iCUB0J z!Ko|)6)I9lkP$TC%=|sz%mp#$VdVD?@3X24Ck&jeREvR%D1dc1OaWyYNbxI^#~d}` zke)_%OpwySy~S5mz8wIp6J5vyHUKAVxZN-3!7@48P~j9f@80S=$pv;>bZIO#j0!ue z-DpSyHUN$xS#Lsvj}Sja0_BJ~FYojKP?Xn*f3a1W1i!*-gSZDa0FIGnRS3w-$N)!0 zH?p%!yav0CLmXS5-~{W?lynm*2#%pu+@l1ujyQ}py1nI$i=`o0&d{P-?xJkiu81~_d^O}v>xZj)^Z_h@*VS-gfg zqRuUbnz?r5J63lT*|`XXc3PL_cVupLf5uw7&kd`AV;<4K$oFn_3G=?s+-UHJGbS1nD5G^WM-k}9>G32w?5H+bGE1OqV4w3D1rCkT1f!(&yKbmZ4kDk z1V>BUBP>KKATyjz<}FSqj@t|8J;`HweQt&Xs<`EW#&Rc_ar!P)qx{~{WM0@Q@mqKG z{)i75PjhRt7xkXW%WcMZFWCU5q2JyoINJC8=Hums5xE%gBZ2#*2E=asB`>?hLXyyR zYw!L>$gaM+Lo9S>4+M%!3rz`_zxk0yCsLqldt+q@`lQ}Lxtc9U)cQG^y)XO|g|*ONM+s0>?u-|sqLpIeUXeC9f? zR=M1%u1=nkar`$dTKSgE%r$Cjrn|Ld_bG?#>leO_X_S#Gj-e*ROID6eI4--Ar#(jq zRo&H`Tr4uLGNcnRcqWre~cO(o=JSSIGN< zC!55Dou%b9ayR?Z)8UT}Vwn_=o!huY@R6U*BHL#*8v1YDmRJ0C(q;SJP>n-++SYBa zrmea)1z0IpcoukB`FM%fK74Ci0fO-thM??ia0Ik*ENCrkXRRu8^C8Ax2WW04>~%FR zOb49PRFw+ngh2~N{9H4rsSj5;q`$6R;dTA9NO8QaOO8EN!Wk^j5uS4f%cRLy;&A}q z+CYwXj-hF{nhOl$X&YK_o>|OIE_=7&eu{Qa_s- z_Y`||;V{D?&Y*NwFuz$7`R(Jx-WU3Ut zeI6F^kh%UBR?I|A)ypG(Z9+26rn8Tl1j%9^&rOZvt9v(p3_E+xhX3}h%Y0&3$H!wL zz)&-I7~PQsCYKSIM~TP?#|7EvSJW=u1Ba@*tj_VH#r)eeeYUMcl{`IAWv4l;+LnSA z#X8o}dGNy_GX(b$$zNJo7cH|Cp3Qs|E_NR=Ps)t^k z=df076n5h&H@c#BH`V+#Hx=%|<1WP!D%{E2it^vx!dFv=mJ~0lmbS)ThxbMl90iZ7 z!R=pZ0dC(H@-v>9OC~N7ecNBz(vV5&DI3;Ny>JQ;pd;kz?2+HSHCgV(q<%X4MWS_a zq5?3O;g2mzpb!4!_?hzeop|d#XAbX8G&0}|A{cV0nJ-iS>eSG+G_;8?J-&S0fEGAK z2BAl>siyJZ%1!=diHF^09DevL3DiYbLEh>5H`e@06wlU+y*QbvJ5_OV5=GTvgn`9; zvTQ1xfD$RbRag!9JHr3;cW_0L9;j6jtYZs8AYK86pyGx@P<-HYW&bj5RTJ)S6DS*J zCLxVk$)ith{e8Eh(SMFmt zv-H(6GYK~{6uWUq?{}0JQm6ZeANs{@!?8$aJ1&T%`aUR|lQ@Pi&%QJch?+p&zj}o0 zmn_jZJJ!4`dNZ|+pV={c9 zO&`$Zv=RqisvP~Nrd?Q&H15GUKWPRJzp@ezB06U(zj7Ma*3vr5y`ru&@%N^m=^&-T zdvM-I@4+&8z|afKZm_xoM}FqfZ2D>?mU{s|^(!}BqW4$&)x%ZkdZPnXQGwHSJg{&= z%1LEY5`2Wj85ys`zhc$}PpzIGK7W+!m$#T6=keb-D^Z5|79x`Xu`Gs_FxI2C?MeOQ^FC3KBEH11x`!_CSQRc zjv~`lfPt`TpOqpI&hMjzRzo_A%>N=M*96HI&eIPGZ*&fnREAs}bWwEvlTod>%jBAk zu7N!4FGx&Vh}P>T%C=Ad12p=f_fq=vDP9jK40A7$An7Zv3jjs{HX9Sf`0{z5tT-^UI}cccG`7c){vZSW_R8EvY|AkZh>7@}C&!iWGuG z)v5^B-xqH63x(>$jjwj@mP!ts)qaxv-)^218v36eM35|?jU<}KkRsX-&y@Zb?e0X2 zrzroA4|aE$vy3}?{ud$dC&Mzi{>KM5D@_AAQAiVBjvgGa4c-Ppmq?(rS#ZJx9XE)< z>HeWr3)zXBEJ&vj(@}S@|Ig!U#UJ2%FTGTz!iC=L((CPmasCtr=rM;WBCA^f8zb{| zfZA4wKmH5TI4eS2AnaOX90QHY;KSqUz^6JuYcB?ebpH7qT~C4xi=JBm-Yv~POzNx< z#=GFZtsq2P;g!~L;iOh(yZy0`0FyCXQNDZ(A90c|Q3R~_XiVhpt>+B(r2p^O9 zmwl7Iv#Mgh@E&>$U|r>2UckL)H9J{{{R z2iB4HyDfU!GTdQw!m&w~0mcoHEP~*XqQh*mn72BouuKQ#{v8UmqRF5Z-wD;o{m#cx zvI0hy{^jq>do4P9dU`}KrYM($IBi+uiMDh>8eXk<>G|i1tlnePl~Y+^Y7wH~%eyI) z0m8l!L&_T~b4)oeQY!8cf=Pt{$Z~VEA)*suB+791#=}6axAC8s%$d>ci-Hxlzu8Jg->1(uQE85f|4>j=)ipi-Nn8 z+?7+(i1FOoA@=>7D>0I6%z}a5FKb%GO-2WRclQ@3W(WBFfu0fKvwRqDqA>|>syYtP zC~0MUFq2C(s(Q|O4(mJOzsAjlw#)SQ^ldubeon9L?C9ZsXzjLI7zc z%TwesN;_#GD-p0Z!B}MP3|!|cM5qg(L{(_=1yXg^SYj9knC9CuELa3rjW&Q!PGxh* zOtfc;@@BxY!rA0xz0c`4d}v^TUdWX7f8w=(vrVh%oZNa*ifqhHlttHx`;EzPjdxlP z2>3gDdt~^LD;i3!&J9Am&$P!x{Fw@^N!EYUE(7B0jduwXmgZ+=*HSfIUnY!b-`e_X zDWoa-_PVBa7k^|*2r;gvl&2B*0wF7pgB~s3gJfZGyk#68!j|aPCYf73mY16$ur$#c zx4kxEDFMZqsnJb)ujDAgN=E~oh7Trt57$ZSp9^?5?~3m@WCMNnhlA5V*~N{u1??Jf z21k}`=95?UFw2~++1Z%A_cVc@~mmki}>J) zMmFC$HRF|9r2F-;d~J+&BrD3)jiR?qLXQ!9Ad#P9hdH_PulAziYr_+SiRx7sF6l&W z^0>RU<#z~Da3jl0sWQG5X`Tc2bBHqqW|fyjs+T)UtP<1c@yI)QKHF~R-DjBOW;8)sF z4FHDyd~B8=)Kd_p_5brTlvgHMyrsxP+h)7ht9_=(oi;d`LP!9VO%CwS&*Dc44Y9W; z&pEbw<_MnR^Tj7B*EB70VgmL^N0GU6v-culDk58LGh>SG^^JSt5h!zeB=7TNo#kU?5PUvlu zUVMIFEIwN<*4ME1K|u?mb1?r9SbVX_kK8TMhp4;nIVPa!M67}BhiKsH-vB@0GoWzDkluawCFUeD-OVltM3wj{DQFKzX&|=url#klK^`m znw$wZx)7thg7papWWr7)xD-#?AXJrsCzh%hg=MyoMjTh?#E7ZXzX3T9il!2HMAWS( zRnIgTr4`(BGY!uI-qT%eLY=fL3IMx%aNJNAX1<^PYU6X$^RVG?E0NoT2Ih3ujbH+L zznw9*K~-M=3FY?8V&Xs$1>R#aH5;y7V?5v-1kx1`_>44ROh*02#aGnwnD+crP$}Mk zwUM{Z%ws*5_~T+D_-FixIy`|1JUvcB~zStV^Au=K>1gCcDJW2LDea3Q@6KsTFs0kDP z=^<8`pX;mGu}G=3Nuz~zqEazP4ys|+jRL;{V2yf^Ju95a28p^pR{sPQ`g z1krUYx*c}XW(cs8i;7s1Jj^;0fE^Ycl2@A)vzW9lubNVFvrmgSq2p3r)O1n%!NMIn zU=xsT2jV7>`6N)Ge{%5^ILh(5@fz_>9~>8bpr~>FYt8{y4$*QTh6e+m`jK~~Isoe! ze9y=LPE_HTY3Eapiw{oHkJt!?6C_BVjREk#X)i%W20JKTS>RTqKz2bY!D{MZK+_N& zcDy8sm_scr;jw%}!j^$DIV}$X9-#7>R1~#qcY_6&h zl-~Er$i$&GN6Aai^Y! zNb_QuB5fZDb6{9*=87VwYh^um&UAQ}C31=GwI2%7M6p(^zUrJj)eLz4>0|=*st>)L zplO9m!XGF(5fBh1x!;(0+2J}Je)Fbh#lGtM0N$tx(LLb`M_m}ew+9h|?MyC&kw8^n z*SOUz9;HejxUm%*GDh@mJ^FwoQn;CZ{pjS-&hw$Ix=gRBp-i@tT(iXwYYqMQMqk=0 zf(e8|VE<{X>A{s7lK?m>7AoV0qqydmZN<&^8}(w#S-r_FjEXoJZ`#fU>W%}6e6mEm zz;I;3cI`*DlIaMDw7b*fZU6Dw+GYGlDbw4m^+Figg{#NzG2?|}JwLi}VDQOwot)TB z-`CS={Qz)_Jw&h3^imGG_o8Zshg#kJ_Q0nldQ4O}4f}3qJZ4E)Lv-}IaxVY0NHBWH za&k0X@nCaZpa+bZrSP(-6#z@tPTqeRMWDMVy;VttBWRG@Q{%T~CfskkW*Si6rvhXk z+$|U7=4v+>{!9e=7(iJQ3&D;3t3*p(#qH_Ej9wcY&(xB9g3&Vk__~-33Sco~Tpi!3 zV8NpY4^qt^Vn~SBCEP-GH0VU^hZ7`7>iEZ(sewS{)>mY$6&G3h zuUE^_R9h||2QJ|v*N0NQwS>TckCTgqY{KvLs$sWzAC*NrQ@LJw)}pWmbP1|F zZn%J-W%J`%|BxS|rJ3SCbv#+@s(T2({InD%5cO`diR6aCLiSePs$cMBqN4?wSYrXs zRVk`+i3`*EL9kE)TgBpEN*+(o&py3XRxupRGrMFsEf1?xLNo#qAN4copqDamWtyC1 zJ>?01opw@+dblXEA%eHlUcms9w8-HA6<5JWcmqVJgaj$mZY2aV&c|sLGpQ_MsMuna z$wDKquy~K}UmOx-s<*2t<`8xj1WMz)sZCAL^vym)_W^9L(Ydax0rSN)D!f^e8W`y9kl^dfw&mAkPlVA$H<{Fgh&C`>lpK%G8e@*J!SkPo`v;6FQ4y!xPAjkv} z$SiDcy-hGYlSqQRu)$Gj_2Sr4(zVGONgV9xx3O*K--`OA59kB|y5ml=9kV6sT|21q zHQAOl zR5ns3plNNqThlbQ+`IJs5qft2Q;^$)YbrgqK_!4~7JOaCR}E&zdJJYgA7ml~Mc~K1 z=d@tuM$?py0!R;YrSN5DF;XZk(a8#He!bOxmoU+OHtBR(31Q~(rD3(pz0+^gyHk>n z>g3$gx2KnYLtZnsbf~(?lr46omxsn^L1~sc_L}!l2}M;SBHS(-rxI9O&|li-uJC zqkgV~doLm{BDvu&AuUf38z8*N7aL1$Png;oLdt#AC|ULJnW9P50pIoqLQF=3w_{kx8gXBt{PjJk)n%vd_g zT{fA5wJzhkd-~$7RC8>}p{&|7_Z#J84q;7?P5ZS0wyhIiGc*PA$Q`0kT|hTV0v*Ub zPTv7st)b3h5~!4%Q4*bFTE$5>>~((YfTAFm9%|f!1>TmG34XfeMi)I44}hAqKNcJ=Xzzt;LgnI&>-ag zah3^AH-~y7Sbuz$wR|%j?p(&(@pxD1GhqZeWvR?1cFg*X*6MDYKxYk!YN;z%%Dh)O&xqEcQ2{6}S5xosL$7-W) zBm>FGrP{b?uDkxPFP}qY3aQHrk;c~aQP&Zfo!e^u6wr}v?bTbjOpnWkP5?MRQL$d> z;>y_yShGFn*dTurKgPh!7bOOvwrELbgfhS(xW47SQn1m-MSkqY*Z895c!Ay(n(lc{ zZaS66Y`pQvmKs;^5-nn>RAX?HNXb$xV)BaKeb-MmXE{zveFx5Wmk|8$vIP?f)cbv^ zlpBHsiW}6X<6v(+fc~KCluY!{Ta`Ync`Ut#R2waGmkG4Bm%pLlz&NStW)KWPCP@V* z>}4koe53Brt|Qd|r*7S(zMQe|s6aVz>Z;b`FP@heY8-&TA%ey4RA39n+gX18^`}`T zCTExk=ywVR6O$%W!BiG9+6btcZclu0J`v3`nQwE5KMzAt=O!MrV2Mb^5O!?(pI}&T z15a{gBg%3ydTTj}DVF;&hy@A>1t9KZr%ZsR&$KaWgOT6}3&T!Xew2I)^h{l27v|T?jfF zZ6?+G<~nexykFaTuCU=fs7to1`~CE8zS+!L+Ndn09Ih$odvkuhKJ{?H(c6+W5LUhyXwl2cNe_>ZD0oE7^sZ^V&uTojl_Y>y)$t z5a1zS*qksmT5?+nd8lO?>c2g7axBF%`w=4GNh;>$v!iN!T1B4CAWi%U7#Dn;zvKl+ zNfkTfQR9}W`#{co{KX%7u5r?qd-(;O-5u&E0a}SJi;rBU^8i}+)M31O+s1G&DV=t3OEzy;B%7$Y+3T#QE+*9Ie0W;tYJb@aqCK$!K8`IcxtmVy5NBtk`>LB;Br)~(AmaCTM}z#yi+*FjTu;Km{w38 z)?25U-8igF2Bj8drEjAC>gFEF`5oGgRpn9)^Pt1&PknY47M>l}FQG5YBFq@jRo{}u zrplnu`5gu|(jfc1=XPI$_*W^=|PZ$N>0{(BLf%{ z{Top$V8~IGrrS;Q7756ql_v$Tjo!r~ymZt1x`xJEvei{-L{|Z%)XOSqdY*aWKwfVv zvxelUXiLurzDuw$*oWth$2l;{oks#i;Jq)FYc}WARovTUH{Y+u+GTj>q}^;_C>aUZ z42lXZt_R3{$E>)a0Zd$zagr5x&hAoC>S1oFHFVi3peVr! zhed2(57Okne-xKxajs%QJJgi~N^*a^{5C$RBO_pA+D1*$=XIu>rPjF*N(^!V zm?8Hr&1|KGm0rsvPz|fLfU8!Q47A{_-Zi>wv%YcuR6tUz>-+ii0~{f7E;x+w#>%Xb zaU?&|e50B_ccJ?H!ByQbvOxWGJs9UIUFN#8T;)zbhqV6Gh;OQ&&0FaBEe_-)9mH33 zR5Y*TQAoi2xcAma$o{d&NiQv}n*)8jak~3anJAH0Oi)aWA8uv3i?>(3vpr`suv~A^ zIN-Gla@?mVi?R0;!oImCIup;*=GJ+UH^CK$PCJJ8P;y;g9A6~VQMO1pVLb$@Y&ON_Mc!I7T#s^@V2zpoulW* z_{exwpS)bNyC~nm7AsKgy&Wxd-+a`YNPW4Muq*cnbmt{&-PkclDt-Xn$3Rkn|Ma!j z7nHmjqghm)7s(A?CC;AB=I}Sx9MR}4_X<0ke=eJ2yzAp}38ZbpgU&1M4MmBxv%~5( zj=3+^n6!5GufN$@D1!{W;|_b!`6l#vQR<~*V%d+h0CP_q&|}Qs=71BV-sOtswPwhe zYDv$Ptr{>I7MbxUS@_uJ#ULLOa%RQpJ{&b<#7L1KE!1&DlQKVOJ}ERqm;cGBNuupa zdebmh9A;L&HGM?1aJzqV%^jk4&vTzLIkJK91}Tzc1aXexl#5$D;!TY;ZLssk4VC%B zQ6$al>HG2G&&+=Y(z_;+=cf{8lEMH0`Re{3dSs?%wIW!=P-nOBVjmAJes1UPyNqtp z=1*}Bq-;Ks7NJ+eAS-p3j^6}UzT`0!&k4mB4if$PgVho0&WG{Bw6(iUvpkq-l5uKh zu5ol6&&J)R%cWZTI?r;_`doy-QPx%-D5jL6R0NZ9IIG0d7L5YE8e2?viikxp!85?; zQivx1A{|Ivn{Rh`wWM9td?k}}@Hjq#pRs@Q4&j;|opJ=4bb*)^uF!3DJmTPW9+7(M z$=G4;l;v6%DNIb>cjNCqA0VKPoG8?g2W~?#>=~A zQUSI!o@zSlan!Ha!w3!ZGZp_~H~2T6mJ$S7Mw8B+ASMN-X7+9-}>w?=C^2<}aP)76<#YGa{JgkB#*s>U8bU3037`xKW_ZbeO-n z^UScnAHCAlO^u7)|CjFu5YPF79IpMgHCr75`#Rr?I2q7cv=##(!_dWa zCCrA!T*jRglT5Q3Z5;RNSOp?817)z-y5?4x>r+gMTrIz;4mHm-O4=8`-KYp_$w)Cg z<_1Ucb*`r!!ncMv&KDY*!@8(%@AqU8@Ma#EPi%=djgJU@Nfjbi> zKJR6nbV_Ijhyi8w%Op|@MZ94X#8TFP#$pb-VO=7SdI?U4JD!s>9s@Nm2deKo$2}he zmE0Y#F`ZP=rWfTI*QqY5%)yu&pZr8$)bV5knY^$Ci+)cazQ1fIvM zOd}U8j%-hhLQaU{X6#H}t6s-PTy-$f#ej(F!lWB39Ku#qi6Z7HR1X~M1Zg5c+b6;l zItbEFs9VK_2y>WqO2wP5))9wwy2ZOE4e{NMPDAnU0RGyfpL{v$4G}O&T~T!}@bj4g zeT(%^w`L%kP*%4bgOp*L4|zy!n(3Q;E<%Kb9u1@ zf!miM*(4m8yf?E4MQokPGqn>}h6K1UwmOtvT5t$+i-KuB2%AL_#+LB{`>rF%k*BnB zlc%uj=rSE6RUFnd_tR#)T)N??rRoG*hClsCT%h7`LPh5%2q-zCz9ND^*CO55pASq$ z&LMPH_g(Uw1EX)86iF6VgO;K&i+jE26Cyf?L1;k=WyuA!X~iX&_Yx2OoFBA`L|irf z=vNX>jXU+6&O__VMT-N*qb@DjW;|}3)XISwEC$J#9B85!E=K4Hp~oNSq15(tQ)Rrt z(PC${jsikMMW>c*e~$-3!=CSNc-JZcZuE!UB6c$_<|Qq8PUF(wtED=vtct{@cAo0qDd-C-nwZi&t0 zg?wUTZoFm^QW78umE0vB{M5Keh&-BnFr4(UZO7}I=i#U?Oz_mu7?yBBDD~1AE{s@F zS)6u-7}oSeZ&q6nQHlxPAaNolYI&F7sqwnu6*5AU{u&G3d7Mb|zbLCvDF`%S#`fhi ze&y3xrcpKNbsMH1-v&KK#JA@kJcwxFpB^+&%r8Bei97@N}od+w12(!ptj)F znaDX-18~K^f&#DggW7OUL$O5A4-|g?k+uO94xp@`_2{gR;{Lkq36leRr&*zk*Va1? zIIER{kGvOLU=+Kowee?_O%z_bnj1YkAUM0xoSBYBYB)0WU-bwvyK4mcCLp^HyD!2L z6h?zZm0i^Ef|z1Ti;s4R)u8Lij@J4j9MbUg_=f%;co~qgYT78;sHm-3VH>@DYneG= z&C(TF5a{8C3W`iWX>v{Lcy%4RTC71TST+W_qx&?KAmO4WHutA)){<4&SwE1AG8CS8 z5ysgEq7j|81!fXxfRmfEB+yQ(L+FHOvhRA*yBT}kTWK{K8mOb^Z*Q-;G1+VwP`=lB zXqky~-3VDtWT08SGO%wwG3Hj_2Q(=%md{#`j14dcP>ZMO@GG@})?eG!UV_pvo`X{R zV8(SmMXX6FjU2yS(qv5>HUtEXg1Ohkh4oxy9p(V6x)>SJav4}H_knAak zB*j20^WN727*Hk;54j~FjY;qQ|J2k_pFK{IufWmKj4FJc-q+YV2#Ti->i^6t&B|wiF)QMVbmG>IH zu+w6eD{Ku0T{N240Q4BGC!>%2qZTE z=h9@DCybDzo?DN;867CG4%*a4<9&O+)&gulufIf&kKjJaut|*|7&_O`@3T@r_pK@p zwB`UO%x7hKV;o%)QPhsmV^&}_h9?ni2Su{4Eh>nR$g;P630S`A76*fVN6<>h&Hi2C zo*H|*CVv4aPmj2}RoaB);lczAS>=IB?W6a?w?lPdEA>3XP6}SbpfnqCgPpwsRIz_+ zS3?|F9E!&ZU<`cd>*Nr`2D{P^_X|NA3bB*po-V9Q_g-nvJQr|7dSTq-bfBAI>$}2z zP1327NL7)Y3=G;nEDchvc}E_kGA`UmH$d#bOS=8v$fy95rn%r%_%^A}&~HakbHV)@H7>t844-Mx@4$)aht(T0 z0#nS{u=5g_o_Ay3!r9g@V=Iq?YUbd-tC^uj2NZy;AH})bmkIpz(iH&qGIX0)g_J>=k2*f42{yr1w3N$iDHZ zWF`SuPiW6~0_~el7%RaDLIWMOb)Xe^OH?cJR^QPhe^-&>GDw2@;6~b>KZF+!@$Iq$Sp$#9q{BC| z^4X$ELWOC+yJZh*2T#5G^F)p`A5*5XfINPn4sy<_xgj<600ciY22i%R^ltb5w0suu zs?~SR7bYa)rTofXME}nBOS@2_OPLvH?;PMzC1#k8!`HCVMlCCpvRno`#7#r?d`DM9 zg__h$?!1_S>RFyB0-B-T4D{njO2q&WjxD-nf|bX)ilXoYs1>J^_>K=ssT_vl734xx zDVfF!a0!H&vNP!!4Fgp&ql&QfXcOH^T99YOh@u7qjNcf>7#m$;~PBoGX} z?VAcPDg03c2}R@;l1K7bwERi^4=d_2g>pRNNO(byQ!^e zhwt8qqsNJESQ2Z*A&xr+dFQaE+1JIF%Zah%AciD#?x6hN4AC2+lc>orMGoywN?gW| zn}$$ZSi0VPs|XtnRh+Ph=f`;Xq|)y#&Sw3NPBriXKqop}zNqkYOG_>*c2-e1Ly81? zuDJ1S+^Hax7IX=)UD3`o5fk-YN0_;c=oPD2F7-&@c(veN2 zBU$}=y*08}(>W?)E4L*B7V-GdA@dtv3I!sc&h-7gfeyD?WU}nNuE1%b(mcY>$I9Nz zz7Q@aIE3q2<9#kn%pWfY(oex5{`~JkD`H%SFVYm6N1&PaS&>uzuo0=A_bz((^h@sYM97?QzlCy&BD{D(`gWJ7HbnEwgSd zlE~J$+G?yj2`BWePhMX#Iyrpx*}S=%P{FxNXI2&x?Y zL3wB5*z}O@p~FdWNwNZza+_*}89hN0JqErcQ0jOh%)3c92nvo34^pxUwImA1aVQ3t zkuQMSYW`je-$Ee^%vt_Q=Rpa{`RAi!piWq$ImzP~;CGzQ(Y?jCCx5sjG%~p+Q0$>4 z)ce(6drRgaOv<}!l(i?zTgrWwrW9EQlwb%JWjIQ2%jG%#p!eV)4is|#vRS!oivH^| z**`AhQ{?@!cSpUaAh1yJktnmrt=H>+^@U;)G*tBzm++}5#lG__! z9AX>VX-@}rpMIG?0eCT>U^v_6&m7IvCziDV#0dvO?Sb!B5E zdf$m)f;5B9VPcWAaz=&gq`;{WfkV8CAx)^m&!lL(+Xuc3K9axi*qOH_ z^PQ%2Uq<=uS~M8DEhQGZ5=Q)>A#K&xjeFMTjoCs=yQo*ya?d!h&UmRXs3v#uwLHhY z0o6Lv)N?}rD23+P3}-lPGu@fpWHwR~nU!UZ>mZS}=I~Rv+j6?<}a?qYTHQK#x+t zor+guRoaExWUMkSkji%-KXm4L)o7D0p)*bmD8pJFvd*WGAQfRN?T)=g2|?ua%>b#l zZI@Zo$bp12iVK(>J7*%G0P{E_ulmR5JPLF(YurPcipo^!2SK($RL&di@@|77f3NwH z(!zb&1Ee68sC5vI%K9Lv0P8aMX-mcNl=4L7@#SSyfp%l;5BudXZFP6N>TjIH#RNjT#9QqR671#X2%(z zzfIrQ9^>x@Ox#=+gyKJS-Bf$Gok%|8sie$b3HJJv#t>r5E~o(hG7Td~3GaV>PTdt7 z8)CC|_3ju*;|ZuW_BeyaFw)#Y^?ahRgOC<0I_`kT2L zIVbSl;$9$aD`P11fb($(MgD(%tvaCov@71ru;mN(Z5SU39N*9g+8=ZlaO{8y1r^F) zCUoe<5?DRP_m^y}Q#hVOM%4f8!=a0hchjATp@9X^uOQXGr~-Z!zb@$#1LjITpxC;&t=c= z_jcu5XV};mUtK_r8`PmvF`fRuUevTSH2qxWjLy<8@nYIrEcTO4XArLj8X?#vuKk`Ax3Rte;UC{Plo?~q8=ly5EfT>;T}yn?<^OsSA&mf0 z*jL~4zrA&yNO{jMm9Yl#!GT0-NdKN1?06a5>8LS~g;7IP7bLz@{)cP)ML@r2r&~m5 zZaR-uI?zPDx&RcVs{?3(2{{kMeuo+2GQ?Nyvl4rI|Dg?tg64VHcH9+UgG=nYc>293 z)3=!(U9$)CJv!e&yxsw8EWG~L!XOIirTA6ch1W|Mv8lq&#L)(zvJArr>Zt2ZDu1ag z{x%c`44E;%uc7J<%{YCLCxQjv5jJCV^<^ua;z8&sgLs9dVHJ6&!~f95KMtr#W)520plxpq8Zj1DEsKZ; zc+4C~m;Mzp{CgIU>ro;O_N50DG1VP1E3wbYns|m^Px%>R$AF!E8f$^-wloBjj7;Bg zfymjzh!J#)G)jG$#IVC`ocKD^{Oh$H$~0hfF)i1c7}=ot1aaE7oro&!L4wH}Vf^2; z3uvSLG78A*Zv_}vM}lk{?v})SX;d^?Zm#nLQvcyQOyDPThW3#oT{sF5rw@N|g9UxM z@kHzQfM`H|F@^^W9=aX$E!6zdwrJFWI5I*sD4^;7H#3q*g)r#Sl$wGHr}?X$6@zc? zTX=jV>WZA->k4^1&}Y(DdLOy=V(J8Z>et=D(Qf{0a=;HGl=hK*ZK-?N;$VLA{}y_h z3$U)F?y^eaBoDFz%v=2TkQE@r|C;}GE@+LIOYd%nMk}9w)dbQTg8%VFxRJ6^kXPfMHH#mUWX(DR z1uKXLg#4B?qZ}A|T%}1-Ab}y-W9qo%!c@h53*%zq0WrTnAmt}N-?rS=jWQ0Dj~gyG z-=728aWvw$+R^6N-lc z`cp>cV2E0D^|uELKMxD6zL~o9!FEJljo5o)VYn2^z0d0IccAS*2iC0>HVy^uZZEb% z1N4^3?UT$sW5E0{^CMDI566(d#8#{~K;Ly!YZQi$Uexyz%(zwV7~nyc-|!%c^KpqU zm7{fJQ{E9U$Otj{*OXWCSe}!{un%`p6DX)36EnICL+tL;AlkhAkSENAoI?&z*>pBdVbZl1Iu?UYER(bnk7aUggOt7Ur#$;9lK$-KNWOe(6~O z+YWJ%<*Jt6<8)(%9oyh#yv99wJ=O>!)%()StvMYwl!EIr_vkTh z^-ojwXr@Y#zMF1;_BsVXh093vO#ay2<#;`(ks#b-7>|9LvO+ z>Dp;CT)AU!Gd76a7Ig8KfB_CXT}&zw0$vQyXtiZUt0InbT94>%@wpwo$COVA@d=ywO)}Ert6QK|=jm>W1Zdv(Xsk&R>*cpyHO&HJCV*B;Z4sWg`IZ$9H!Y zc5U$3phaR$^&oClVtr^*miG`V?22m!G+-%pF#_tRxUoDPx3z2iDv~&BWZ_K+82P9+ zk6F@M&kfw*^?oId>%cE$Zoju1ubj+?3tUeMB<=LdR9R-U17kTsA*-IR$N0KY;EK_( zgdOcCp(PH?wA|h3jjNci*>oXRZjTd7nVaHxdBc6z=7qIEnKQNm0N-pBhrehS51s-{Jdk00$1$h z7!8}PmFe3$W@G>=FC~=)4j*Ur-m{0gk@)`zC?nv&P|u!JX2OX+>k^&U7YEl+N-S zQE8Sx^z40>S|PS?9-dze67@R|hM{&ovXlGxf--jF6{~Lnv)gO2EUpNF=RSVZ>Xxx` z>v3`y2Xte)Rt(39%S8{hrFwyO&=Cl5le^>bl{Ns*DVvt>b`X8WQ_ zBr`v8WX}K7-nITEm2TmgW}3>z?4mh&LMG8nvYZ+nlTtD*QWI||=8Z~A%`r#88wyzF zVYJdTZy;iC}3SkXrear%1TdrkAN6>6o$yXI3Xcgeu)olpcXF6>c?aOmoiINEM z2MOZ&T2TI9TGVYE(p0&QCeO?*uLb(0e&&yMA)e-}eBw&t zhxiomqoAYnV<@sf!6Q%dt_l8fE8ng3dV+1|?fv4m0XDQ#>J58w z04j6y*q2a$2hw}&-9iQ{e(b#MqyMiAe9T6JyNhiS`#It6degXSTQ_ner#SeJ$EyWJ zY1Hv)#Jni4n$UnmsE_gpPf*c2>`Az2>FWxC`{yAVI3y@)5H?abP8SKF8Z+@PSFqCC zqGRU%aGqWdL}qN2e29t2LtaA7=`$7Q;l8`Dc2oJzQq2TfgIKz!n6jeTEvfv|ij`H+kK(ryy`5H(L>)E)mzhjc$AwGt-#G&+eR-YT0NI zsA|)x|KLofX`rXZcLvR4=Jy($1Wnx8TdK`n<-uSqi+KzMbJ0F9R-e6-cjsQ?hrkny z7YaM1dbnI8Q!mg;5Rz@j4QJ$u}Y1GW{$_XNbDt|+GS z%?$Q}xBFB72{E?WAWm@7RhWHF-{PVsU=(a2diM z+&^HY4|Wb{Q$F5%zBze-z-CB*X~Tl7QtCF|&$902&l5T32&{Ss_sGfniiFZG9^sPu z%*CuK4Qj~A>}Up7f{?Ar7hV?Gy64RIpwYQ+o%HOR+)8QlwzL~pv$@4AdZD&Q`=!Tu zvu~R3I}(?Ezxrx?Y0N|}HJPep!rh+)X1HLgNd4I5N^Q4Kl4?y)nRcnnoY~j)Z(TQs zVjF&bX0|55#;}>V!8qZ~X@QbnnqBG-tl?+>!cp=U-bD$D?h5$}%-!QEfQWr?KBaL? zt64ldB`grMMYnHdyyW@VDd{yj@_HI5n{?=PB0AB20@~S&Iiukye4w3@B)tLa4rXX7 z>7z{b1pets`DwR~rzE+0;b<8Nh^W#;c+Rd=1*KOXarAj7lusIjP=2E~uB~?f&X*jk z>f~*2RgZHeb<6hKy_3ZEm618wT-hmCveX9&%kO(!#ih-jjBD5nZ4{T7yxrA?TJ(b3 z1Ba}^BmEdt)Kf>e4@Y54`|@dqxee)|cSv#gfmJwQc!>&V=FwVWN{?zi?kHv$NUcc& zA_65vl0GXgbniQ`ZQp>^Pn0m`+|c0Q)(q%B^lC@Ao@@=7`H0uV3xg8k6~Wq|iT7@d z+dQ;oOUkz{+BzlzKtzCwzD)tJZdt0ds6ljd_KS%##IWV$h}o;^@`Uf3rdMgS>WLj5 z%c**I=f%Z6I!Vi?jCnQ|)%kfw%$vvW`$vvnhovU0M| z$#UZCHOqYM!6h#iI>6UhNj$;!2Pk(I-Typvb*40aimqulANO5SuRvqLLYFZwQb8@f zlsC}PyL|Y&$+!Bh->xrffs*@8c)E*Jb$S)2+u55XB;{!%!g?Tsa`asSq{73CH2D`13)r>1gpy8h6hS#PLC>}!x}>jMyoq!^ zK+W{#JE@Fugr+(})W2~98|5vvQe(fuc{~mHY+*W4Dphf^q z7gw*}YRbIs(8(L|-67r-4BO-FDDHKlSaK_w;vFPsB%`k!^jyKmZ&WYN-AEb>%ea;L zX}XAZr<0dMQLXp7$$bydi(G4{nJi+G@0@D2Ljji3R=-)CydlOY0p{MfDex?pXDvmb zVFm-6%o#;I;_U)np}02*Tm|*8(pwBdD4C9!SMlEm@SmvupsZVBonjthNqT8wFF5#f zjQ*&l-J4(<`AjuIM_n9JAIK(3n{MQXhNhfUe|-6rJ<8X}_&cr2tB48BP(LLJA@~(^ z%oPD0>S^8$!vWlOlG+JHA40aF;GS%V9l*LKjOQ#%B}c79lz`>A&h=KN9xUa^cMOxN z`4cKmo({)Y-B8mokX$2dWiwYIi!J0aIRgB#FL4biEC@|gt}Cfm3@#bZVv}qZmBwQE zw-mUOL?X+HZKv;nrlZ4^W7hzrE?FM+J6H2CT<{!K;Fv_rItTSnby)~DvH~T+VCbX{ zcD-U6fn)HvR-K#|!23#NZ4AYdr>(QvBfYW@758@O-j$HJ3Bv;FJz2z+`BY1%`c%&D;9^xcIaoIViRIlMbqRu+g z^hpjzm?Tw9i%B}xpw}zSBm-}D1ipoLqIw^eZ5<|Xg!`$_{Mm7_9+9ZN-!94=+|>Ro zKDl{B)H&=^Mr^TZ{oPN#g|ZYTZ(p2MBr*`3&Wgnntc!Z7hgLdCXh9usaZUG4YL4B* z>*FB=w-A6^_F(E2V6?l>5FD?S?~=M&tG_p)tovnk9pWbZ^P@F+ZLu)l_VD1LYFdnb zNVg3bG?+XooJsp`!|{;R1%r@i{=o!aKD~8I2$%@QR+ouUo>jcNU!)kcE6X6@GF~Q2 zy!OnoY!ndMqS_tt-*LD6gK(8$RaSA7{reKEN8`q{!-rQPnd&aZjj}IckNhIJxoLG9 zndmo8R%3V@p}H$z%*AutgaT2HW|7>Zhm5AFwLUy9H0*hlE4Dfcv*^z~3X2pcM{b8K zHLt5tOzY^hc(27ItwP}g%0$v#0X$>%NQL+=Dbj^9_c)i4<}f05VzU5oKgqD`;~p<& z_KGd9fuj8+YR+gxo$mToWw$;+k~k%6p1=Al)*TAO4Xf`zmC%>Z`Qd lSFyXV@_+wN5K)6({qcD4-D!hiNdEB~m!nwcx+AA9{RhXkIu`%{ 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); - }); -}); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy