diff --git a/src/components/colorbar/attributes.js b/src/components/colorbar/attributes.js index 67ce2557895..aa2c456c9de 100644 --- a/src/components/colorbar/attributes.js +++ b/src/components/colorbar/attributes.js @@ -166,6 +166,7 @@ module.exports = overrideAll({ }), 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 59e94a3ca60..64506c23eb9 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1232,7 +1232,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. @@ -1288,7 +1288,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 @@ -1302,7 +1303,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))) { @@ -1397,7 +1398,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 @@ -1498,6 +1499,76 @@ function numFormat(v, ax, fmtoverride, hover) { return v; } +axes.getTickFormat = function(ax) { + var i; + + function convertToMs(dtick) { + return typeof dtick !== 'string' ? dtick : Number(dtick.replace('M', '')) * ONEAVGMONTH; + } + + function compareLogTicks(left, right) { + var priority = ['L', 'D']; + if(typeof left === typeof right) { + if(typeof left === 'number') { + return left - right; + } else { + var leftPriority = priority.indexOf(left.charAt(0)); + var rightPriority = priority.indexOf(right.charAt(0)); + if(leftPriority === rightPriority) { + return Number(left.replace(/(L|D)/g, '')) - Number(right.replace(/(L|D)/g, '')); + } else { + return leftPriority - rightPriority; + } + } + } else { + return typeof left === 'number' ? 1 : -1; + } + } + + function isProperStop(dtick, range, convert) { + var convertFn = convert || function(x) { return x;}; + var leftDtick = range[0]; + var rightDtick = range[1]; + return ((!leftDtick && typeof leftDtick !== 'number') || convertFn(leftDtick) <= convertFn(dtick)) && + ((!rightDtick && typeof rightDtick !== 'number') || convertFn(rightDtick) >= convertFn(dtick)); + } + + 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': + case 'linear': { + for(i = 0; i < ax.tickformatstops.length; i++) { + if(isProperStop(ax.dtick, ax.tickformatstops[i].dtickrange, convertToMs)) { + tickstop = ax.tickformatstops[i]; + break; + } + } + break; + } + case 'log': { + for(i = 0; i < ax.tickformatstops.length; i++) { + if(isProperLogStop(ax.dtick, ax.tickformatstops[i].dtickrange)) { + tickstop = ax.tickformatstops[i]; + break; + } + } + 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 52571086cde..aae0ac26b43 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -492,6 +492,34 @@ 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', 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*', + 'or *max* value by passing *null*' + ].join(' ') + }, + value: { + valType: 'string', + dflt: '', + role: 'style', + editType: 'ticks', + description: [ + 'string - dtickformat for described zoom level, the same as *tickformat*' + ].join(' ') + }, + editType: 'ticks' + }, hoverformat: { valType: 'string', dflt: '', diff --git a/src/plots/cartesian/tick_label_defaults.js b/src/plots/cartesian/tick_label_defaults.js index 5f37680d331..96af6b1be8b 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,6 +40,7 @@ module.exports = function handleTickLabelDefaults(containerIn, containerOut, coe if(axType !== 'category') { var tickFormat = coerce('tickformat'); + tickformatstopsDefaults(containerIn, containerOut); if(!tickFormat && axType !== 'date') { coerce('showexponent', showAttrDflt); coerce('exponentformat'); @@ -80,3 +81,26 @@ function getShowAttrDflt(containerIn) { return containerIn[showAttrs[0]]; } } + +function tickformatstopsDefaults(tickformatIn, tickformatOut) { + var valuesIn = tickformatIn.tickformatstops; + var valuesOut = tickformatOut.tickformatstops = []; + + if(!Array.isArray(valuesIn)) return; + + 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); + } +} diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index f6e91b3daa3..7375c0c553a 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -101,6 +101,7 @@ module.exports = overrideAll({ 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 75f4f37132b..8478132e1bb 100644 --- a/src/traces/carpet/axis_attributes.js +++ b/src/traces/carpet/axis_attributes.js @@ -10,6 +10,8 @@ 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: { @@ -290,6 +292,7 @@ module.exports = { '*%H~%M~%S.%2f* would display *09~15~23.46*' ].join(' ') }, + tickformatstops: overrideAll(axesAttrs.tickformatstops, 'calc', 'from-root'), categoryorder: { valType: 'enumerated', values: [ diff --git a/tasks/util/strict_d3.js b/tasks/util/strict_d3.js index 18b8ff25915..d667f6d9d74 100644 --- a/tasks/util/strict_d3.js +++ b/tasks/util/strict_d3.js @@ -6,7 +6,6 @@ var pathToStrictD3Module = path.join( constants.pathToImageTest, 'strict-d3.js' ); - /** * Transform `require('d3')` expressions to `require(/path/to/strict-d3.js)` */ @@ -18,6 +17,8 @@ module.exports = transformTools.makeRequireTransform('requireTransform', var pathOut; if(pathIn === 'd3' && opts.file !== pathToStrictD3Module) { + // JSON.stringify: fix npm-scripts for windows users, for whom + // path has \ in it, without stringify that turns into control chars. pathOut = 'require(' + JSON.stringify(pathToStrictD3Module) + ')'; } diff --git a/test/image/baselines/tickformatstops.png b/test/image/baselines/tickformatstops.png new file mode 100644 index 00000000000..3f0e66f942d Binary files /dev/null and b/test/image/baselines/tickformatstops.png differ 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/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); + }); +}); 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