Fix #5396 #5393 (time zone)
diff --git a/src/model/globalDefault.js b/src/model/globalDefault.js
index 8b9b6e4..8333f9a 100644
--- a/src/model/globalDefault.js
+++ b/src/model/globalDefault.js
@@ -51,6 +51,9 @@
// `progressiveThreshold`, otherwise hover will cause restart of progressive,
// which is unexpected.
// see example <echarts/test/heatmap-large.html>.
- hoverLayerThreshold: 3000
+ hoverLayerThreshold: 3000,
+
+ // See: module:echarts/scale/Time
+ useUTC: false
};
});
\ No newline at end of file
diff --git a/src/scale/Scale.js b/src/scale/Scale.js
index 1f4a755..1d58e2e 100644
--- a/src/scale/Scale.js
+++ b/src/scale/Scale.js
@@ -6,7 +6,12 @@
var clazzUtil = require('../util/clazz');
- function Scale() {
+ /**
+ * @param {Object} [setting]
+ */
+ function Scale(setting) {
+ this._setting = setting || {};
+
/**
* Extent
* @type {Array.<number>}
@@ -39,6 +44,10 @@
return val;
};
+ scaleProto.getSetting = function (name) {
+ return this._setting[name];
+ };
+
scaleProto.contain = function (val) {
var extent = this._extent;
return val >= extent[0] && val <= extent[1];
diff --git a/src/scale/Time.js b/src/scale/Time.js
index e83c094..7bd88b2 100644
--- a/src/scale/Time.js
+++ b/src/scale/Time.js
@@ -5,9 +5,20 @@
define(function (require) {
+ // [About UTC and local time zone]:
+ // In most cases, `number.parseDate` will treat input data string as local time
+ // (except time zone is specified in time string). And `format.formateTime` returns
+ // local time by default. option.useUTC is false by default. This design have
+ // concidered these common case:
+ // (1) Time that is persistent in server is in UTC, but it is needed to be diplayed
+ // in local time by default.
+ // (2) By default, the input data string (e.g., '2011-01-02') should be displayed
+ // as its original time, without any time difference.
+
var zrUtil = require('zrender/core/util');
var numberUtil = require('../util/number');
var formatUtil = require('../util/format');
+ var scaleHelper = require('./helper');
var IntervalScale = require('./Interval');
@@ -47,7 +58,7 @@
var date = new Date(val);
- return formatUtil.formatTime(stepLvl[0], date);
+ return formatUtil.formatTime(stepLvl[0], date, this.getSetting('useUTC'));
},
// Overwrite
@@ -81,6 +92,8 @@
// Overwrite
niceTicks: function (approxTickNum) {
+ var timezoneOffset = this.getSetting('useUTC')
+ ? 0 : numberUtil.getTimezoneOffset() * 60 * 1000;
approxTickNum = approxTickNum || 10;
var extent = this._extent;
@@ -103,10 +116,12 @@
}
var niceExtent = [
- mathCeil(extent[0] / interval) * interval,
- mathFloor(extent[1] / interval) * interval
+ Math.round(mathCeil((extent[0] - timezoneOffset) / interval) * interval + timezoneOffset),
+ Math.round(mathFloor((extent[1] - timezoneOffset)/ interval) * interval + timezoneOffset)
];
+ scaleHelper.fixExtent(niceExtent, extent);
+
this._stepLvl = level;
// Interval will be used in getTicks
this._interval = interval;
@@ -151,10 +166,11 @@
];
/**
+ * @param {module:echarts/model/Model}
* @return {module:echarts/scale/Time}
*/
- TimeScale.create = function () {
- return new TimeScale();
+ TimeScale.create = function (model) {
+ return new TimeScale({useUTC: model.ecModel.get('useUTC')});
};
return TimeScale;
diff --git a/src/scale/helper.js b/src/scale/helper.js
index 4a08390..237e0ef 100644
--- a/src/scale/helper.js
+++ b/src/scale/helper.js
@@ -28,14 +28,7 @@
roundNumber(Math.floor(extent[1] / interval) * interval, precision)
];
- // In some cases (e.g., splitNumber is 1), niceTickExtent may be out of extent.
- !isFinite(niceTickExtent[0]) && (niceTickExtent[0] = extent[0]);
- !isFinite(niceTickExtent[1]) && (niceTickExtent[1] = extent[1]);
- clamp(niceTickExtent, 0, extent);
- clamp(niceTickExtent, 1, extent);
- if (niceTickExtent[0] > niceTickExtent[1]) {
- niceTickExtent[0] = niceTickExtent[1];
- }
+ helper.fixExtent(niceTickExtent, extent);
return result;
};
@@ -44,6 +37,17 @@
niceTickExtent[idx] = Math.max(Math.min(niceTickExtent[idx], extent[1]), extent[0]);
}
+ // In some cases (e.g., splitNumber is 1), niceTickExtent may be out of extent.
+ helper.fixExtent = function (niceTickExtent, extent) {
+ !isFinite(niceTickExtent[0]) && (niceTickExtent[0] = extent[0]);
+ !isFinite(niceTickExtent[1]) && (niceTickExtent[1] = extent[1]);
+ clamp(niceTickExtent, 0, extent);
+ clamp(niceTickExtent, 1, extent);
+ if (niceTickExtent[0] > niceTickExtent[1]) {
+ niceTickExtent[0] = niceTickExtent[1];
+ }
+ };
+
helper.intervalScaleGetTicks = function (interval, extent, niceTickExtent, intervalPrecision) {
var ticks = [];
diff --git a/src/util/format.js b/src/util/format.js
index b7ee9d5..3cb507b 100644
--- a/src/util/format.js
+++ b/src/util/format.js
@@ -143,15 +143,12 @@
* ISO Date format
* @param {string} tpl
* @param {number} value
- * @param {boolean} [isLocal=false] Default use UTC
- * Why default UTC? In most case, time provided by user is
- * understood in UTC. For example, new Date('2012-01-01')
- * or a string '2012-01-01' or a timestamp. So it is
- * recommended to format time in UTC.
- * (see `echarts/util/number.js#parseDate`);
+ * @param {boolean} [isUTC=false] Default in local time.
+ * see `module:echarts/scale/Time`
+ * and `module:echarts/util/number#parseDate`.
* @inner
*/
- formatUtil.formatTime = function (tpl, value, isLocal) {
+ formatUtil.formatTime = function (tpl, value, isUTC) {
if (tpl === 'week'
|| tpl === 'month'
|| tpl === 'quarter'
@@ -162,7 +159,7 @@
}
var date = numberUtil.parseDate(value);
- var utc = isLocal ? '' : 'UTC';
+ var utc = isUTC ? 'UTC' : '';
var y = date['get' + utc + 'FullYear']();
var M = date['get' + utc + 'Month']() + 1;
var d = date['get' + utc + 'Date']();
diff --git a/src/util/number.js b/src/util/number.js
index fcb8d4d..23ed770 100644
--- a/src/util/number.js
+++ b/src/util/number.js
@@ -206,8 +206,14 @@
return val > -RADIAN_EPSILON && val < RADIAN_EPSILON;
};
- var TIME_REG = /^(?:(\d{4})(?:[-\/](\d{1,2})(?:[-\/](\d{1,2})(?:[T ](\d{1,2})(?::(\d\d)(?::(\d\d)(?:[.,](\d+))?)?)?(?:Z|([\+\-]\d\d):?\d\d)?)?)?)?)?$/; // jshint ignore:line
- var TIMEZONE_OFFSET = (new Date()).getTimezoneOffset();
+ var TIME_REG = /^(?:(\d{4})(?:[-\/](\d{1,2})(?:[-\/](\d{1,2})(?:[T ](\d{1,2})(?::(\d\d)(?::(\d\d)(?:[.,](\d+))?)?)?(Z|[\+\-]\d\d:?\d\d)?)?)?)?)?$/; // jshint ignore:line
+
+ /**
+ * @return {number} in minutes
+ */
+ number.getTimezoneOffset = function () {
+ return (new Date()).getTimezoneOffset();
+ };
/**
* @param {string|Date|number} value These values can be accepted:
@@ -216,9 +222,9 @@
* + only year, month, date: '2012-03', '2012-03-01', '2012-03-01 05', '2012-03-01 05:06',
* + separated with T or space: '2012-03-01T12:22:33.123', '2012-03-01 12:22:33.123',
* + time zone: '2012-03-01T12:22:33Z', '2012-03-01T12:22:33+8000', '2012-03-01T12:22:33-05:00',
- * all of which will be treated as they reperent a time in UTC
- * if time zone is not specified.
- * + Or other string format, including:
+ * all of which will be treated as local time if time zone is not specified
+ * (see <https://momentjs.com/>).
+ * + Or other string format, including (all of which will be treated as loacal time):
* '2012', '2012-3-1', '2012/3/1', '2012/03/01',
* '2009/6/12 2:00', '2009/6/12 2:05:08', '2009/6/12 2:05:08.123'
* + a timestamp, which represent a time in UTC.
@@ -241,6 +247,13 @@
return new Date(NaN);
}
+ var timezoneOffset = number.getTimezoneOffset();
+ var timeOffset = !match[8]
+ ? 0
+ : match[8].toUpperCase() === 'Z'
+ ? timezoneOffset
+ : +match[8].slice(0, 3) * 60 + timezoneOffset;
+
// match[n] can only be string or undefined.
// But take care of '12' + 1 => '121'.
return new Date(
@@ -248,7 +261,7 @@
+(match[2] || 1) - 1,
+match[3] || 1,
+match[4] || 0,
- +(match[5] || 0) - (match[8] || 0) * 60 - TIMEZONE_OFFSET,
+ +(match[5] || 0) - timeOffset,
+match[6] || 0,
+match[7] || 0
);
diff --git a/test/timeZone.html b/test/timeZone.html
new file mode 100644
index 0000000..a7df234
--- /dev/null
+++ b/test/timeZone.html
@@ -0,0 +1,293 @@
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script src="esl.js"></script>
+ <script src="config.js"></script>
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="stylesheet" href="reset.css" />
+ </head>
+ <body>
+ <style>
+ h1 {
+ line-height: 60px;
+ height: 60px;
+ background: #ddd;
+ text-align: center;
+ font-weight: bold;
+ font-size: 14px;
+ }
+ .chart {
+ height: 350px;
+ }
+ </style>
+
+
+ <h1>time scale label shoule in local time when timestamp (in UTC) input, tick should align with day.</h1>
+ <div class="chart" id="chart0"></div>
+
+ <h1>time scale label shoule in UTC time when option.useUTC is `true`, tick should not align with day.</h1>
+ <div class="chart" id="chart1"></div>
+
+ <h1>useUTC: null, should display '00:00 01-03' in tooltip on the 1st point</h1>
+ <div class="chart" id="chart2"></div>
+
+ <h1>useUTC: true, should display '16:00 01-02' in tooltip on the 1st point</h1>
+ <div class="chart" id="chart3"></div>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <script>
+ var dataTimestamp = [
+ [1486656000000, 20000],
+ [1486742400000, 30000],
+ [1486828800000, 10000],
+ [1486915200000, 290000],
+ [1487001600000, 123355],
+ [1487088000000, 198128],
+ [1487174400000, 123124]
+ ];
+
+ var dataTimeString = [
+ ['2012-01-03', 20000],
+ ['2012-01-04', 30000],
+ ['2012-01-05', 10000],
+ ['2012-01-06', 290000]
+ ];
+
+ function makeTimeScaleOption(data, useUTC) {
+ return {
+ useUTC: useUTC,
+ tooltip: {
+ trigger: 'axis'
+ },
+ xAxis: [{
+ type: 'time',
+ splitNumber: 7,
+ axisLabel: {
+ formatter: function (value) {
+ return echarts.format.formatTime('yyyy-MM-dd hh:mm:ss', value, useUTC);
+ },
+ rotate: 10
+ },
+ splitLine: {
+ show: false
+ }
+ }],
+ yAxis: [{
+ type: 'value',
+ splitLine: {
+ show: false
+ }
+ }],
+ series: [{
+ type: 'line',
+ smooth: true,
+ data: data,
+ label: {
+ normal: {
+ show: true,
+ formatter: function (params) {
+ return echarts.format.formatTime('yyyy-MM-dd hh:mm:ss', params.value[0], useUTC);
+ }
+ }
+ }
+ }]
+ };
+ }
+
+
+ </script>
+
+
+
+
+
+ <script>
+
+ var echarts;
+ var colorTool;
+ var chart;
+ var myChart;
+ var groupCategories = [];
+ var groupColors = [];
+
+ require([
+ 'echarts',
+ 'zrender/tool/color',
+ 'echarts/chart/line',
+ 'echarts/component/grid',
+ 'echarts/component/legend',
+ 'echarts/component/tooltip',
+ 'echarts/component/toolbox',
+ 'echarts/component/visualMap',
+ 'echarts/component/dataZoom'
+ ], function (ec, ct) {
+ echarts = ec;
+ colorTool = ct;
+ var dom = document.getElementById('chart0');
+ if (!dom) {
+ return;
+ }
+ chart = myChart = echarts.init(dom);
+
+ var option = makeTimeScaleOption(dataTimestamp);
+
+ chart.setOption(option);
+ });
+
+ </script>
+
+
+
+
+
+
+
+
+ <script>
+
+ var echarts;
+ var colorTool;
+ var chart;
+ var myChart;
+ var groupCategories = [];
+ var groupColors = [];
+
+ require([
+ 'echarts',
+ 'zrender/tool/color',
+ 'echarts/chart/line',
+ 'echarts/component/grid',
+ 'echarts/component/legend',
+ 'echarts/component/tooltip',
+ 'echarts/component/toolbox',
+ 'echarts/component/visualMap',
+ 'echarts/component/dataZoom'
+ ], function (ec, ct) {
+ echarts = ec;
+ colorTool = ct;
+ var dom = document.getElementById('chart1');
+ if (!dom) {
+ return;
+ }
+ chart = myChart = echarts.init(dom);
+
+ var option = makeTimeScaleOption(dataTimestamp, true);
+
+ chart.setOption(option);
+ });
+
+ </script>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <script>
+
+ var echarts;
+ var colorTool;
+ var chart;
+ var myChart;
+ var groupCategories = [];
+ var groupColors = [];
+
+ require([
+ 'echarts',
+ 'zrender/tool/color',
+ 'echarts/chart/line',
+ 'echarts/component/grid',
+ 'echarts/component/legend',
+ 'echarts/component/tooltip',
+ 'echarts/component/toolbox',
+ 'echarts/component/visualMap',
+ 'echarts/component/dataZoom'
+ ], function (ec, ct) {
+ echarts = ec;
+ colorTool = ct;
+ var dom = document.getElementById('chart2');
+ if (!dom) {
+ return;
+ }
+ chart = myChart = echarts.init(dom);
+
+ var option = makeTimeScaleOption(dataTimeString);
+
+ chart.setOption(option);
+ });
+
+ </script>
+
+
+
+
+
+
+
+
+
+
+
+ <script>
+
+ var echarts;
+ var colorTool;
+ var chart;
+ var myChart;
+ var groupCategories = [];
+ var groupColors = [];
+
+ require([
+ 'echarts',
+ 'zrender/tool/color',
+ 'echarts/chart/line',
+ 'echarts/component/grid',
+ 'echarts/component/legend',
+ 'echarts/component/tooltip',
+ 'echarts/component/toolbox',
+ 'echarts/component/visualMap',
+ 'echarts/component/dataZoom'
+ ], function (ec, ct) {
+ echarts = ec;
+ colorTool = ct;
+ var dom = document.getElementById('chart3');
+ if (!dom) {
+ return;
+ }
+ chart = myChart = echarts.init(dom);
+
+ var option = makeTimeScaleOption(dataTimeString, true);
+
+ chart.setOption(option);
+ });
+
+ </script>
+
+
+
+
+ </body>
+</html>
\ No newline at end of file
diff --git a/test/ut/spec/util/number.js b/test/ut/spec/util/number.js
index 0ef5779..903be81 100755
--- a/test/ut/spec/util/number.js
+++ b/test/ut/spec/util/number.js
@@ -214,18 +214,18 @@
expect(+numberUtil.parseDate(1330819200000.01)).toEqual(1330819200000);
// ISO string
- expect(+numberUtil.parseDate('2012-03')).toEqual(1330560000000);
- expect(+numberUtil.parseDate('2012-03-04')).toEqual(1330819200000);
- expect(+numberUtil.parseDate('2012-03-04 05')).toEqual(1330837200000);
- expect(+numberUtil.parseDate('2012-03-04T05')).toEqual(1330837200000);
- expect(+numberUtil.parseDate('2012-03-04 05:06')).toEqual(1330837560000);
- expect(+numberUtil.parseDate('2012-03-04T05:06')).toEqual(1330837560000);
- expect(+numberUtil.parseDate('2012-03-04 05:06:07')).toEqual(1330837567000);
- expect(+numberUtil.parseDate('2012-03-04T05:06:07')).toEqual(1330837567000);
- expect(+numberUtil.parseDate('2012-03-04T05:06:07.123')).toEqual(1330837567123);
- expect(+numberUtil.parseDate('2012-03-04T05:06:07,123')).toEqual(1330837567123);
- expect(+numberUtil.parseDate('2012-03-04T05:06:07.12')).toEqual(1330837567012);
- expect(+numberUtil.parseDate('2012-03-04T05:06:07.1')).toEqual(1330837567001);
+ expect(+numberUtil.parseDate('2012-03')).toEqual(1330531200000);
+ expect(+numberUtil.parseDate('2012-03-04')).toEqual(1330790400000);
+ expect(+numberUtil.parseDate('2012-03-04 05')).toEqual(1330808400000);
+ expect(+numberUtil.parseDate('2012-03-04T05')).toEqual(1330808400000);
+ expect(+numberUtil.parseDate('2012-03-04 05:06')).toEqual(1330808760000);
+ expect(+numberUtil.parseDate('2012-03-04T05:06')).toEqual(1330808760000);
+ expect(+numberUtil.parseDate('2012-03-04 05:06:07')).toEqual(1330808767000);
+ expect(+numberUtil.parseDate('2012-03-04T05:06:07')).toEqual(1330808767000);
+ expect(+numberUtil.parseDate('2012-03-04T05:06:07.123')).toEqual(1330808767123);
+ expect(+numberUtil.parseDate('2012-03-04T05:06:07,123')).toEqual(1330808767123);
+ expect(+numberUtil.parseDate('2012-03-04T05:06:07.12')).toEqual(1330808767012);
+ expect(+numberUtil.parseDate('2012-03-04T05:06:07.1')).toEqual(1330808767001);
expect(+numberUtil.parseDate('2012-03-04T05:06:07,123Z')).toEqual(1330837567123);
expect(+numberUtil.parseDate('2012-03-04T05:06:07.123+0800')).toEqual(1330808767123);
expect(+numberUtil.parseDate('2012-03-04T05:06:07.123+08:00')).toEqual(1330808767123);
@@ -233,18 +233,18 @@
expect(+numberUtil.parseDate('2012-03-04T05:06:07.123-07:00')).toEqual(1330862767123);
// Other string
- expect(+numberUtil.parseDate('2012')).toEqual(1325376000000);
- expect(+numberUtil.parseDate('2012/03')).toEqual(1330560000000);
- expect(+numberUtil.parseDate('2012/03/04')).toEqual(1330819200000);
- expect(+numberUtil.parseDate('2012-3-4')).toEqual(1330819200000);
- expect(+numberUtil.parseDate('2012/3')).toEqual(1330560000000);
- expect(+numberUtil.parseDate('2012/3/4')).toEqual(1330819200000);
- expect(+numberUtil.parseDate('2012/3/4 2:05')).toEqual(1330826700000);
- expect(+numberUtil.parseDate('2012/03/04 2:05')).toEqual(1330826700000);
- expect(+numberUtil.parseDate('2012/3/4 2:05:08')).toEqual(1330826708000);
- expect(+numberUtil.parseDate('2012/03/04 2:05:08')).toEqual(1330826708000);
- expect(+numberUtil.parseDate('2012/3/4 2:05:08.123')).toEqual(1330826708123);
- expect(+numberUtil.parseDate('2012/03/04 2:05:08.123')).toEqual(1330826708123);
+ expect(+numberUtil.parseDate('2012')).toEqual(1325347200000);
+ expect(+numberUtil.parseDate('2012/03')).toEqual(1330531200000);
+ expect(+numberUtil.parseDate('2012/03/04')).toEqual(1330790400000);
+ expect(+numberUtil.parseDate('2012-3-4')).toEqual(1330790400000);
+ expect(+numberUtil.parseDate('2012/3')).toEqual(1330531200000);
+ expect(+numberUtil.parseDate('2012/3/4')).toEqual(1330790400000);
+ expect(+numberUtil.parseDate('2012/3/4 2:05')).toEqual(1330797900000);
+ expect(+numberUtil.parseDate('2012/03/04 2:05')).toEqual(1330797900000);
+ expect(+numberUtil.parseDate('2012/3/4 2:05:08')).toEqual(1330797908000);
+ expect(+numberUtil.parseDate('2012/03/04 2:05:08')).toEqual(1330797908000);
+ expect(+numberUtil.parseDate('2012/3/4 2:05:08.123')).toEqual(1330797908123);
+ expect(+numberUtil.parseDate('2012/03/04 2:05:08.123')).toEqual(1330797908123);
});
});