From 50fe02a36e310a8b5521b3003cea728345ea01e4 Mon Sep 17 00:00:00 2001 From: Andrei_Palchys Date: Thu, 3 Aug 2017 17:40:06 +0300 Subject: [PATCH] Extend tickformat behaviour for dates. Add possibilty to configure date format for each zoom level independently --- src/lib/dates.js | 32 ++++- src/plots/cartesian/axes.js | 2 +- src/plots/cartesian/layout_attributes.js | 7 +- tasks/util/strict_d3.js | 3 +- test/image/baselines/custom_tickformat.png | Bin 0 -> 29940 bytes test/image/mocks/custom_tickformat.json | 23 ++++ test/jasmine/tests/custom_tickformat_test.js | 138 +++++++++++++++++++ 7 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 test/image/baselines/custom_tickformat.png create mode 100644 test/image/mocks/custom_tickformat.json create mode 100644 test/jasmine/tests/custom_tickformat_test.js diff --git a/src/lib/dates.js b/src/lib/dates.js index c5c05fcfda9..28c3e63d9cb 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -451,6 +451,7 @@ function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, yyyy'); * tr: tickround ('y', 'm', 'd', 'M', 'S', or # digits) * used if no explicit fmt is provided * calendar: optional string, the world calendar system to use + * dtick: optional string or number, side of tick for custom formatting * * returns the date/time as a string, potentially with the leading portion * on a separate line (after '\n') @@ -458,13 +459,40 @@ function yearMonthDayFormatWorld(cDate) { return cDate.formatDate('M d, yyyy'); * the axis may choose to strip things after it when they don't change from * one tick to the next (as it does with automatic formatting) */ -exports.formatDate = function(x, fmt, tr, calendar) { +exports.formatDate = function(x, fmt, tr, calendar, dtick) { var headStr, dateStr; calendar = isWorldCalendar(calendar) && calendar; - if(fmt) return modDateFormat(fmt, x, calendar); + if(fmt) { + if(typeof fmt === 'string') return modDateFormat(fmt, x, calendar); + if(typeof fmt === 'object') { + var unit = ''; + if(typeof dtick === 'string') { + if(Number(dtick.replace('M', '')) > 6) { + unit = 'year'; + } else if(Number(dtick.replace('M', '')) >= 1) { + unit = 'month'; + } + } else if(dtick >= ONEDAY * 7) { + unit = 'week'; + } else if(dtick >= ONEDAY) { + unit = 'day'; + } else if(dtick >= ONEHOUR) { + unit = 'hour'; + } else if(dtick >= ONEMIN) { + unit = 'minute'; + } else if(dtick >= ONESEC) { + unit = 'second'; + } else if(dtick >= 0) { + unit = 'millisecond'; + } + if(fmt[unit]) { + return modDateFormat(fmt[unit], x, calendar); + } + } + } if(calendar) { try { diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 5a2767c0d55..cc269119c81 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -1250,7 +1250,7 @@ function formatDate(ax, out, hover, extraPrecision) { else tr = {y: 'm', m: 'd', d: 'M', M: 'S', S: 4}[tr]; } - var dateStr = Lib.formatDate(out.x, fmt, tr, ax.calendar), + var dateStr = Lib.formatDate(out.x, fmt, tr, ax.calendar, ax.dtick), headStr; var splitIndex = dateStr.indexOf('\n'); diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 90855d7452a..a3639d9fbfc 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -434,7 +434,7 @@ module.exports = { ].join(' ') }, tickformat: { - valType: 'string', + valType: 'any', dflt: '', role: 'style', description: [ @@ -445,7 +445,10 @@ module.exports = { 'https://github.com/d3/d3-time-format/blob/master/README.md#locale_format', 'We add one item to d3\'s date formatter: *%{n}f* for fractional seconds', 'with n digits. For example, *2016-10-13 09:15:23.456* with tickformat', - '*%H~%M~%S.%2f* would display *09~15~23.46*' + '*%H~%M~%S.%2f* would display *09~15~23.46* Also now you can specify date', + 'format for each zooming level independently using following configuration', + 'object {hour: *%H:%M*, day: *%e %b*}. Available following zooming levels:', + 'year, month, week, day, hour, minute, second, millisecond' ].join(' ') }, hoverformat: { diff --git a/tasks/util/strict_d3.js b/tasks/util/strict_d3.js index 1e51ab8912b..db853e179ee 100644 --- a/tasks/util/strict_d3.js +++ b/tasks/util/strict_d3.js @@ -6,6 +6,7 @@ var pathToStrictD3Module = path.join( constants.pathToImageTest, 'strict-d3.js' ); +var normalizedPathToStrictD3Module = pathToStrictD3Module.replace(/\\/g, '/'); // replacing of "\" for windows users /** * Transform `require('d3')` expressions to `require(/path/to/strict-d3.js)` @@ -18,7 +19,7 @@ module.exports = transformTools.makeRequireTransform('requireTransform', var pathOut; if(pathIn === 'd3' && opts.file !== pathToStrictD3Module) { - pathOut = 'require(\'' + pathToStrictD3Module + '\')'; + pathOut = 'require(\'' + normalizedPathToStrictD3Module + '\')'; } if(pathOut) return cb(null, pathOut); diff --git a/test/image/baselines/custom_tickformat.png b/test/image/baselines/custom_tickformat.png new file mode 100644 index 0000000000000000000000000000000000000000..0a7b09e0f46cc6bfd421ad021d6b81d70da78cd2 GIT binary patch literal 29940 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�Rwf{IV`;$&VHhFwfVsVQ`)2w*H&S7i0*>^b0uJq< zY6TlAM7}&3ZmUTk1)9Hxv5n23(oLn=TMi2r#H*>xv#(fz(s1bF`E|WE1-ZM}XY)BC z`YIKpL#MjM?9h?Nzp@<+r;pQiUcPb%2^2EhCp$X!xdAwU*-4&LAIw^@52`INF{~}A z-ln&jGQFW}5c^{wqA1%|myZ;hW}G4^+ves`hfFwJrZc7{WwkG26sFS^_HcIn)&$pg z1BaY3r&o%0_(=2xPds=%-seltkJO|Ht?nf~LXJ7e;fp@&__LnjbVYHS1ti?<@^!+|+tZr17992wjl)q;_Tj`5vW@wJw@d#G!S zT+ZA-ZxVp2AE)|jiJyNQa2z%J316a84&y<~7~9mgL1*ytMO<)Rz%;~Y!*!jDj z)UJ6DDEQZZLZOY%dFwQW=_+-!Z|=3v@cY2JyxrJ$S)FAm)_K*Ry;UzFldq4_J1U=$ zdi7C*_hsR4q$>r1x-K2tR7f>3P;ytStdkqow8(!g7Y6QFvg08{7v26U_o*w!w6j&o zuU!$(heC+bH>|+(CBjG0y)9bL*y-y?m}2~eXDmhai3ZRIsC4g4cSQ9+!p=uGHfBT- z+MF@I)|--ez%3rWS{C|2=)YZuzx{VVKH=R^ZXQn&;s4bVMtB(<-T634WoMNO8#!%&dS$aJV;JhJW`+u2mLl_SF{|*-azk~JTNc`{2 iT3Gx43(IiUpBmdpC^&fL=Klae+91aO literal 0 HcmV?d00001 diff --git a/test/image/mocks/custom_tickformat.json b/test/image/mocks/custom_tickformat.json new file mode 100644 index 00000000000..8ca46f96fd2 --- /dev/null +++ b/test/image/mocks/custom_tickformat.json @@ -0,0 +1,23 @@ +{ + "data": [ + { + "x": ["2010-01-01","2010-01-02","2010-01-03","2010-01-04","2010-01-05","2010-01-06","2010-01-07"], + "y": [-5,3,-1,7,-1,3,-5] + } + ], + "layout": { + "title": "tickformat", + "xaxis": { + "tickformat": { + "millisecond": "%H:%M:%S.%L ms", + "second": "%H:%M:%S s", + "minute": "%H:%M m", + "hour": "%H:%M h", + "day": "%e %b d", + "week": "%b %d w", + "month": "%b %y M", + "year": "%Y Y" + } + } + } +} diff --git a/test/jasmine/tests/custom_tickformat_test.js b/test/jasmine/tests/custom_tickformat_test.js new file mode 100644 index 00000000000..826e4b0ce7e --- /dev/null +++ b/test/jasmine/tests/custom_tickformat_test.js @@ -0,0 +1,138 @@ +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 constants = require('@src/constants/numerical'); + +var mock = require('@mocks/custom_tickformat.json'); +var tickFormat = mock.layout.xaxis.tickformat; + +function getZoomInButton(gd) { + return selectButton(gd._fullLayout._modeBar, 'zoomIn2d'); +} + +function getZoomOutButton(gd) { + return selectButton(gd._fullLayout._modeBar, 'zoomOut2d'); +} + +function getFormatter(dtick) { + var unit = ''; + if(typeof dtick === 'string') { + if(Number(dtick.replace('M', '')) > 6) { + unit = 'year'; + } else if(Number(dtick.replace('M', '')) >= 1) { + unit = 'month'; + } + } else if(dtick >= constants.ONEDAY * 7) { + unit = 'week'; + } else if(dtick >= constants.ONEDAY) { + unit = 'day'; + } else if(dtick >= constants.ONEHOUR) { + unit = 'hour'; + } else if(dtick >= constants.ONEMIN) { + unit = 'minute'; + } else if(dtick >= constants.ONESEC) { + unit = 'second'; + } else if(dtick >= 0) { + unit = 'millisecond'; + } + if(tickFormat[unit]) { + return d3.time.format.utc(tickFormat[unit]); + } + return function(mock) {return mock;}; +} + +describe('Test extended tickformat:', function() { + + var mockCopy, gd; + + beforeEach(function() { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); + }); + + afterEach(destroyGraphDiv); + + it('Zooming-in until milliseconds zoom level', 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(gd._fullLayout.xaxis.dtick); + 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(); + }); + + it('Zooming-out until years zoom level', 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(gd._fullLayout.xaxis.dtick); + 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 tickformat for hover', function() { + 'use strict'; + + var evt = { xpx: 270, ypx: 10 }; + + afterEach(destroyGraphDiv); + + beforeEach(function() { + gd = createGraphDiv(); + mockCopy = Lib.extendDeep({}, mock); + }); + + it('tickformat for hover and xaxes should coincide', 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(gd._fullLayout.xaxis.dtick); + + expect(hoverTrace.curveNumber).toEqual(0); + expect(hoverTrace.pointNumber).toEqual(3); + expect(hoverTrace.x).toEqual('2010-01-04'); + expect(hoverTrace.y).toEqual(7); + + 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('7'); + done(); + }); + }); + }); + +});