我需要让Dygraph尊重任意时区。图表中/上/周围显示的所有日期都应该能够以任意时区显示。
问题:
答案 0 :(得分:5)
更新的答案,现在适用于Dygraph版本1.1.1。对于旧答案,请跳过水平规则。
现在有一种更简单的方法可以让Dygraph使用任意时区。如果功能兼容,旧方法(实现自定义valueFormatter
,axisLabelFormatter
和ticker
函数)理论上仍然可以工作(旧答案中的代码不与Dygraph v.1.1.1兼容。)
但是,这次我决定采用不同的路线。这种新方法不使用记录的选项,所以它更加hacky。但是,它确实具有代码少得多的好处(无需重新实现ticker
函数等)。所以虽然它有点hacky,但它是一个优雅的黑客IMO。请注意,新版本仍然需要时刻和时刻第三方库。
请注意,此版本要求您使用新的labelsUTC
选项,设置为true
。您可以将此黑客视为将Dygraph的UTC选项转换为您选择的时区。
var g_timezoneName = 'America/Los_Angeles'; // US Pacific Time
function getMomentTZ(d, interpret) {
// Always setting a timezone seems to prevent issues with daylight savings time boundaries, even when the timezone we are setting is the same as the browser: https://github.com/moment/moment/issues/1709
// The moment tz docs state this:
// moment.tz(..., String) is used to create a moment with a timezone, and moment().tz(String) is used to change the timezone on an existing moment.
// Here is some code demonstrating the difference.
// d = new Date()
// d.getTime() / 1000 // 1448297005.27
// moment(d).tz(tzStringName).toDate().getTime() / 1000 // 1448297005.27
// moment.tz(d, tzStringName).toDate().getTime() / 1000 // 1448300605.27
if (interpret) {
return moment.tz(d, g_timezoneName); // if d is a javascript Date object, the resulting moment may have a *different* epoch than the input Date d.
} else {
return moment(d).tz(g_timezoneName); // does not change epoch value, just outputs same epoch value as different timezone
}
}
/** Elegant hack: overwrite Dygraph's DateAccessorsUTC to return values
* according to the currently selected timezone (which is stored in
* g_timezoneName) instead of UTC.
* This hack has no effect unless the 'labelsUTC' setting is true. See Dygraph
* documentation regarding labelsUTC flag.
*/
Dygraph.DateAccessorsUTC = {
getFullYear: function(d) {return getMomentTZ(d, false).year();},
getMonth: function(d) {return getMomentTZ(d, false).month();},
getDate: function(d) {return getMomentTZ(d, false).date();},
getHours: function(d) {return getMomentTZ(d, false).hour();},
getMinutes: function(d) {return getMomentTZ(d, false).minute();},
getSeconds: function(d) {return getMomentTZ(d, false).second();},
getMilliseconds: function(d) {return getMomentTZ(d, false).millisecond();},
getDay: function(d) {return getMomentTZ(d, false).day();},
makeDate: function(y, m, d, hh, mm, ss, ms) {
return getMomentTZ({
year: y,
month: m,
day: d,
hour: hh,
minute: mm,
second: ss,
millisecond: ms,
}, true).toDate();
},
};
// ok, now be sure to set labelsUTC option to true
var graphoptions = {
labels: ['Time', 'Impressions', 'Clicks'],
labelsUTC: true
};
var g = new Dygraph(chart, data, graphoptions);
(注意:我仍然想指定valueFormatter
,因为我希望标签为“YYYY-MM-DD”格式而不是“YYYY / MM / DD”,但没有必要指定valueFormatter
只是为了得到任意的时区支持。)
使用Dygraph 1.1.1,Moment 2.10.6和Moment Timezone v 0.4.1进行测试
旧答案,适用于Dygraph版本1.0.1。
我的第一次尝试是简单地将Dygraph传递给一些timezone-js对象而不是Date对象,因为它们声称能够用作javascript Date对象的drop-in替换(你可以像Date对象一样使用它们)。不幸的是,这不起作用,因为Dygraph从头开始创建Date对象,似乎没有使用我传给它的timezone-js。
深入研究Dygraph文档显示我们可以使用一些钩子来覆盖日期的显示方式:
<强> valueFormatter 强>
为鼠标悬停时显示的值提供自定义显示格式的功能。这不会影响轴旁边的刻度线上显示的值。要格式化这些,请参阅axisLabelFormatter。
<强> axisLabelFormatter 强>
调用以格式化沿轴出现的刻度值的函数。这通常是按轴设置的。
<强>股票</强>
这允许您指定任意函数以在轴上生成刻度线。刻度线是(值,标签)对的数组。内置函数非常适合选择好的刻度标记,因此,如果设置此选项,您很可能想要调用其中一个并修改结果。
最后一个实际上也取决于时区,所以我们需要覆盖它以及格式化程序。
所以我首先使用timezone-js为valueFormatter和axisLabelFormatter编写了替换,但事实证明timezone-js在非DST日期(浏览器当前在DST中)实际上无法正常工作。所以首先我设置moment-js,moment-timezone-js,以及我需要的时区数据。 (对于这个例子,我们只需要'Etc / UTC')。请注意,我使用全局变量来存储我传递给moment-timezone-js的时区。如果你想出一个更好的方法,请评论。以下是我使用moment-js编写的valueFormatter和axisLabelFormatters:
var g_timezoneName = 'Etc/UTC'; // UTC
/*
Copied the Dygraph.dateAxisFormatter function and modified it to not create a new Date object, and to use moment-js
*/
function dateAxisFormatter(date, granularity) {
var mmnt = moment(date).tz(g_timezoneName);
if (granularity >= Dygraph.DECADAL) {
return mmnt.format('YYYY');
} else if (granularity >= Dygraph.MONTHLY) {
return mmnt.format('MMM YYYY');
} else {
var frac = mmnt.hour() * 3600 + mmnt.minute() * 60 + mmnt.second() + mmnt.millisecond();
if (frac === 0 || granularity >= Dygraph.DAILY) {
return mmnt.format('DD MMM');
} else {
return hmsString_(mmnt);
}
}
}
/*
Copied the Dygraph.dateString_ function and modified it to use moment-js
*/
function valueFormatter(date_millis) {
var mmnt = moment(date_millis).tz(g_timezoneName);
var frac = mmnt.hour() * 3600 + mmnt.minute() * 60 + mmnt.second();
if (frac) {
return mmnt.format('YYYY-MM-DD') + ' ' + hmsString_(mmnt);
}
return mmnt.format('YYYY-MM-DD');
}
/*
Format hours, mins, seconds, but leave off seconds if they are zero
@param mmnt - moment object
*/
function hmsString_(mmnt) {
if (mmnt.second()) {
return mmnt.format('HH:mm:ss');
} else {
return mmnt.format('HH:mm');
}
}
在测试这些格式化程序时,我注意到刻度线有点奇怪。例如,我的图表涵盖了两天的数据,但我没有在tick标签中看到任何日期。相反,我只看到了时间价值。默认情况下,当图表覆盖两天的数据时,Dygraph会在中间显示日期。
所以,为了解决这个问题,我们需要提供自己的Ticker。使用什么比Dygraph的修改版更好?
/** @type {Dygraph.Ticker}
Copied from Dygraph.dateTicker. Using our own function to getAxis, which respects TZ
*/
function customDateTickerTZ(a, b, pixels, opts, dygraph, vals) {
var chosen = Dygraph.pickDateTickGranularity(a, b, pixels, opts);
if (chosen >= 0) {
return getDateAxis(a, b, chosen, opts, dygraph); // use own own function here
} else {
// this can happen if self.width_ is zero.
return [];
}
};
/**
* Copied from Dygraph.getDateAxis - modified to respect TZ
* @param {number} start_time
* @param {number} end_time
* @param {number} granularity (one of the granularities enumerated in Dygraph code)
* @param {function(string):*} opts Function mapping from option name -> value.
* @param {Dygraph=} dg
* @return {!Dygraph.TickList}
*/
function getDateAxis(start_time, end_time, granularity, opts, dg) {
var formatter = /** @type{AxisLabelFormatter} */(
opts("axisLabelFormatter"));
var ticks = [];
var t;
if (granularity < Dygraph.MONTHLY) {
// Generate one tick mark for every fixed interval of time.
var spacing = Dygraph.SHORT_SPACINGS[granularity];
// Find a time less than start_time which occurs on a "nice" time boundary
// for this granularity.
var g = spacing / 1000;
var d = moment(start_time);
d.tz(g_timezoneName); // setting a timezone seems to prevent issues with daylight savings time boundaries, even when the timezone we are setting is the same as the browser: https://github.com/moment/moment/issues/1709
d.millisecond(0);
var x;
if (g <= 60) { // seconds
x = d.second();
d.second(x - x % g);
} else {
d.second(0);
g /= 60;
if (g <= 60) { // minutes
x = d.minute();
d.minute(x - x % g);
} else {
d.minute(0);
g /= 60;
if (g <= 24) { // days
x = d.hour();
d.hour(x - x % g);
} else {
d.hour(0);
g /= 24;
if (g == 7) { // one week
d.startOf('week');
}
}
}
}
start_time = d.valueOf();
// For spacings coarser than two-hourly, we want to ignore daylight
// savings transitions to get consistent ticks. For finer-grained ticks,
// it's essential to show the DST transition in all its messiness.
var start_offset_min = moment(start_time).tz(g_timezoneName).zone();
var check_dst = (spacing >= Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]);
for (t = start_time; t <= end_time; t += spacing) {
d = moment(t).tz(g_timezoneName);
// This ensures that we stay on the same hourly "rhythm" across
// daylight savings transitions. Without this, the ticks could get off
// by an hour. See tests/daylight-savings.html or issue 147.
if (check_dst && d.zone() != start_offset_min) {
var delta_min = d.zone() - start_offset_min;
t += delta_min * 60 * 1000;
d = moment(t).tz(g_timezoneName);
start_offset_min = d.zone();
// Check whether we've backed into the previous timezone again.
// This can happen during a "spring forward" transition. In this case,
// it's best to skip this tick altogether (we may be shooting for a
// non-existent time like the 2AM that's skipped) and go to the next
// one.
if (moment(t + spacing).tz(g_timezoneName).zone() != start_offset_min) {
t += spacing;
d = moment(t).tz(g_timezoneName);
start_offset_min = d.zone();
}
}
ticks.push({ v:t,
label: formatter(d, granularity, opts, dg)
});
}
} else {
// Display a tick mark on the first of a set of months of each year.
// Years get a tick mark iff y % year_mod == 0. This is useful for
// displaying a tick mark once every 10 years, say, on long time scales.
var months;
var year_mod = 1; // e.g. to only print one point every 10 years.
if (granularity < Dygraph.NUM_GRANULARITIES) {
months = Dygraph.LONG_TICK_PLACEMENTS[granularity].months;
year_mod = Dygraph.LONG_TICK_PLACEMENTS[granularity].year_mod;
} else {
Dygraph.warn("Span of dates is too long");
}
var start_year = moment(start_time).tz(g_timezoneName).year();
var end_year = moment(end_time).tz(g_timezoneName).year();
for (var i = start_year; i <= end_year; i++) {
if (i % year_mod !== 0) continue;
for (var j = 0; j < months.length; j++) {
var dt = moment.tz(new Date(i, months[j], 1), g_timezoneName); // moment.tz(Date, tz_String) is NOT the same as moment(Date).tz(String) !!
dt.year(i);
t = dt.valueOf();
if (t < start_time || t > end_time) continue;
ticks.push({ v:t,
label: formatter(moment(t).tz(g_timezoneName), granularity, opts, dg)
});
}
}
}
return ticks;
};
最后,我们必须告诉Dygraph使用这个自动收报机和格式化程序,方法是将它们添加到选项对象中:
var graphoptions = {
labels: ['Time', 'Impressions', 'Clicks'],
axes: {
x: {
valueFormatter: valueFormatter,
axisLabelFormatter: dateAxisFormatter,
ticker: customDateTickerTZ
}
}
};
g = new Dygraph(chart, data, graphoptions);
如果您想更改时区然后刷新图表,请执行以下操作:
g_timezoneName = "<a new timezone name that you've configured moment-timezone to use>";
g.updateOptions({axes: {x: {valueFormatter: valueFormatter, axisLabelFormatter: dateAxisFormatter, ticker: customDateTickerTZ}}});
这些代码片段使用Dygraphs.VERSION 1.0.1,moment.version 2.7.0和moment.tz.version 0.0.6进行测试。
答案 1 :(得分:0)
它有点晚了,但Eddified的回答不再适用于当前版本的dygraph.js,现在有一个选项可以使用UTC时间:labelsUTC: true
示例是这里提供:
var data = (function() {
var rand10 = function () { return Math.round(10 * Math.random()); };
var a = []
for (var y = 2009, m = 6, d = 23, hh = 18, n=0; n < 72; n++) {
a.push([new Date(Date.UTC(y, m, d, hh + n, 0, 0)), rand10()]);
}
return a;
})();
gloc = new Dygraph(
document.getElementById("div_loc"),
data,
{
labels: ['local time', 'random']
}
);
gutc = new Dygraph(
document.getElementById("div_utc"),
data,
{
labelsUTC: true,
labels: ['UTC', 'random']
}
);