Dygraph在任意时区显示日期 [英] Dygraph showing dates in an arbitrary Timezone

查看:133
本文介绍了Dygraph在任意时区显示日期的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I needed to get Dygraph to respect arbitrary timezones. All dates shown in/on/around the graph should be able to be shown in an arbitrary timezone.

问题:

  • Javascript does not support timezones other than the browser local timezone.
  • There are libraries out there (timezone-js, moment-js with moment-timezone-js) to address this, but Dygraph doesn't know about them.

推荐答案

更新的答案,现在适用于Dygraph版本1.1.1。对于旧答案跳过水平规则。

UPDATED answer, now works with Dygraph version 1.1.1. For old answer skip past horizontal rule.

现在有一种更简单的方法可以使Dygraph与任意时区一起工作。旧方法(实现自定义 valueFormatter axisLabelFormatter 自动收报机不与Dygraph v.1.1.1兼容。)

There is now an easier way to make Dygraph work with arbitrary timezones. The old way (implementing custom valueFormatter, axisLabelFormatter, and ticker functions) should theoretically still work provided the functions are compatible (the code in the old answer is not compatible with Dygraph v. 1.1.1.)

但是,这次我决定采用不同的路线。这种新方法不使用记录的选项,所以它更加hacky。但是,它确实具有代码少得多的好处(无需重新实现 ticker 函数等)。所以虽然它有点hacky,但它是一个优雅的黑客IMO。请注意,新版本仍需要时刻和时刻第三方库。

However, I decided to go a different route this time around. This new way does not use documented options, so it is a bit more hacky. However, it does have the benefit of being much less code (no need to re-implement the ticker function, etc). So while it is a little hacky, it's an elegant hack IMO. Note that the new version still requires moment and moment timezone 3rd party libraries.

请注意,此版本要求您使用新的 labelsUTC 选项,设置为 true 。您可以将此黑客视为将Dygraph的UTC选项转换为您选择的时区。

Note that this version requires that you use the new labelsUTC option, set to true. You can think of this hack as turning Dygraph's UTC option into a timezone of your choosing.

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 来获取任意时区支持。)

(Note: I still like to specify valueFormatter because I want labels in "YYYY-MM-DD" format instead of "YYYY/MM/DD", but it's not necessary to specify valueFormatter anymore just to get arbitrary timezone support.)

使用Dygraph 1.1.1,时刻2.10.6和时刻时区v 0.4.1进行测试

Tested using Dygraph 1.1.1, Moment 2.10.6, and Moment Timezone v 0.4.1

旧答案,适用于Dygraph版本1.0.1。

Old answer, works with Dygraph version 1.0.1.

我的第一次尝试是简单地通过Dygraph一些时区 - js对象而不是Date对象,因为它们声称能够用作javascript Date对象的drop-in替换(你可以像Date对象一样使用它们)。不幸的是,这不起作用,因为Dygraph从头开始创建Date对象,似乎没有使用我传给它的timezone-js。

My first attempt was to simply pass Dygraph some timezone-js objects instead of Date objects, due to their purported ability to be used as drop-in replacements for javascript Date objects (you can use them like Date objects). Unfortunately this didn't work because Dygraph creates Date objects from scratch and doesn't seem to use the timezone-js ones I'm passing to it.

挖掘到Dygraph文档显示我们可以使用一些钩子来覆盖日期的显示方式:

Digging into the Dygraph documentation shows that there are some hooks that we can use to override how dates are displayed:


valueFormatter

为鼠标悬停时显示的值提供自定义显示格式的功能。这不会影响轴旁边的刻度线上显示的值。要格式化这些,请参阅axisLabelFormatter。

Function to provide a custom display format for the values displayed on mouseover. This does not affect the values that appear on tick marks next to the axes. To format those, see axisLabelFormatter.

axisLabelFormatter

调用格式的函数沿轴出现的刻度值。这通常是按轴设置的。

Function to call to format the tick values that appear along an axis. This is usually set on a per-axis basis.

股票代码

这允许您指定任意函数以在轴上生成刻度线。刻度线是(值,标签)对的数组。内置函数可以选择好的刻度标记,因此,如果设置此选项,您很可能需要调用其中一个并修改结果。

This lets you specify an arbitrary function to generate tick marks on an axis. The tick marks are an array of (value, label) pairs. The built-in functions go to great lengths to choose good tick marks so, if you set this option, you'll most likely want to call one of them and modify the result.

最后一个实际上也取决于时区,因此我们需要覆盖它以及格式化程序。

That last one actually depends on the timezone too, so we'll need to override it as well as the formatters.

所以我首先使用timezone-js为valueFormatter和axisLabelFormatter编写了替换,但事实证明timezone-js在非DST日期(浏览器当前在DST中)实际上无法正常工作。所以首先我设置moment-js,moment-timezone-js,以及我需要的时区数据。 (对于这个例子,我们只需要'Etc / UTC')。请注意,我使用全局变量来存储我传递给moment-timezone-js的时区。如果你想出一个更好的方法,请评论。以下是我使用moment-js编写的valueFormatter和axisLabelFormatters:

So I first wrote replacements for valueFormatter and axisLabelFormatter using timezone-js, but it turns out timezone-js didn't actually work correctly for non-DST dates (browser currently in DST). So first I setup moment-js, moment-timezone-js, and the timezone data I need. (For this example we only need 'Etc/UTC'). Note that I am using a global variable to store the timezone that I pass to moment-timezone-js. If you come up with a better way, please comment. Here are the valueFormatter and axisLabelFormatters I wrote using moment-js:

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会在图表显示两天的数据时显示中间的日期。

While testing these formatters, I noticed that the tick marks were a little strange. For instance, my graph covered two days worth of data, yet I didn't see any dates in the tick labels. Instead, I only saw time values. Dygraph by default would show a date in the middle when the graph covers two days of data.

因此,为了解决这个问题,我们需要提供自己的Ticker。使用什么比Dygraph的修改版本更好?

So, to fix this we need to supply our own Ticker. What better ticker to use than a modified version of Dygraph's?

/** @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使用此代码和格式化程序,通过将它们添加到选项对象,如下所示:

Finally, we have to tell Dygraph to use this ticker and formatters, by adding them to the options object like this:

var graphoptions = {
  labels: ['Time', 'Impressions', 'Clicks'],
  axes: {
    x: {
      valueFormatter: valueFormatter,
      axisLabelFormatter: dateAxisFormatter,
      ticker: customDateTickerTZ
    }
  }
};
g = new Dygraph(chart, data, graphoptions);

如果您想更改时区然后刷新图表,请执行以下操作:

If you want to change the timezone and then refresh the graph, do this:

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。

These code snippets were tested with Dygraphs.VERSION 1.0.1, moment.version 2.7.0, and moment.tz.version 0.0.6.

这篇关于Dygraph在任意时区显示日期的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆