显示任意时区日期的Dygraph

Dygraph showing dates in an arbitrary Timezone

本文关键字:Dygraph 日期 任意时 显示      更新时间:2023-09-26

我需要让Dygraph尊重任意时区。图表中/上/周围显示的所有日期应该能够在任意时区显示。

问题:

  • Javascript不支持浏览器本地时区以外的时区。
  • 有库(timezone-js, moment-js with moment-timezone-js)来解决这个问题,但是Dygraph不知道它们。

已更新答案,现在适用于Dygraph 1.1.1版。对于旧答案,跳过横线。

现在有一种更简单的方法可以使Dygraph与任意时区一起工作。旧的方法(实现自定义的valueFormatter, axisLabelFormatterticker函数)理论上仍然可以工作,只要函数是兼容的(旧答案中的代码是而不是与Dygraph v. 1.1.1兼容)

然而,这次我决定走一条不同的路线。这种新方法不使用文档化的选项,因此有点粗糙。但是,它确实具有代码少得多的好处(不需要重新实现ticker函数等)。所以,虽然这是一个小黑客,这是一个优雅的黑客在我看来。请注意,新版本仍然需要时刻和时刻时区第三方库。

注意,这个版本要求您使用新的labelsUTC选项,设置为true。您可以将此hack视为将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对象的插入替代品(您可以像使用Date对象一样使用它们)。不幸的是,这不起作用,因为Dygraph从头开始创建Date对象,并且似乎不使用我传递给它的时区-js对象。

深入Dygraph文档可以发现,我们可以使用一些钩子来覆盖日期的显示方式:

valueFormatter

为鼠标悬停时显示的值提供自定义显示格式的函数。这不会影响显示在轴旁边的勾号上的值。要格式化它们,请参见axisLabelFormatter。

axisLabelFormatter

函数调用以格式化沿轴显示的刻度值。这通常是在每个轴的基础上设置的。

股票

这允许您指定一个任意函数来在轴上生成标记。勾号是一个(值,标签)对数组。内置函数会不遗余力地选择合适的勾号,因此,如果您设置了这个选项,您很可能会想调用其中一个并修改结果。

最后一个实际上也依赖于时区,所以我们需要重写它以及格式化器。

因此,我首先使用timezone-js编写了valueFormatter和axisLabelFormatter的替代品,但事实证明,timezone-js实际上不能正确工作于非夏令时日期(浏览器当前处于夏令时)。因此,首先我设置moment-js、moment-timezone-js和我需要的时区数据。(对于这个例子,我们只需要'Etc/UTC')。注意,我使用一个全局变量来存储传递给moment-timezone-js的时区。如果你想到一个更好的方法,请评论。以下是我使用moment-js编写的valueFormatter和axislabelformatter:

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');
    }
}
在测试这些格式化程序时,我注意到这些标记有点奇怪。例如,我的图表涵盖了两天的数据,但我没有在刻度标签中看到任何日期。相反,我只看到了时间值。默认情况下,当图表包含两天的数据时,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使用这个计时器和格式化器,通过将它们添加到options对象中,像这样:

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进行了测试。版本1.0.1,瞬间。版本2.7.0和moment.tz.version 0.0.6

这有点晚了,但是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']
                   }
                 );
http://dygraphs.com/tests/labelsDateUTC.html