【问题标题】:Dygraph showing dates in an arbitrary TimezoneDygraph 显示任意时区的日期
【发布时间】:2014-08-03 11:22:21
【问题描述】:

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

问题:

【问题讨论】:

    标签: javascript timezone momentjs dygraphs


    【解决方案1】:

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

    现在有一种更简单的方法可以让 Dygraph 在任意时区工作。如果函数兼容,旧方法(实现自定义 valueFormatteraxisLabelFormatterticker 函数)理论上应该仍然有效(旧答案中的代码与 Dygraph v 兼容。 1.1.1.)

    不过,这次我决定走另一条路。这种新方法不使用记录的选项,所以它有点hacky。但是,它确实具有代码少得多的好处(无需重新实现ticker 函数等)。因此,虽然它有点 hacky,但它是一个优雅的 hack IMO。请注意,新版本仍然需要 moment 和 moment timezone 3rd 方库。

    请注意,此版本要求您使用新的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 对象,并且似乎没有使用我传递给它的 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');
        }
    }
    

    在测试这些格式化程序时,我注意到刻度线有点奇怪。例如,我的图表涵盖了两天的数据,但我没有在刻度标签中看到任何日期。相反,我只看到了时间值。当图表涵盖两天的数据时,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}}});
    

    这些代码 sn-ps 使用 Dygraphs.VERSION 1.0.1、moment.version 2.7.0 和 moment.tz.version 0.0.6 进行了测试。

    【讨论】:

    • 干得好!一些清理提示:您可以使用moment(date_millis),而不是moment(date_millis/1000, 'X')。同样,您可以使用moment(t),而不是moment(new Date(t))
    • 这是一个错误吗? m.startOf('周'); .. 我们在哪里?
    • 是的,这是一个错误。我在我的代码库中修复了它,但忘记在这里更新。 'm' 应该是 'd',IIRC。
    【解决方案2】:

    有点晚了,但 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

    【讨论】:

    • 我已经更新了我的答案,现在它可以与 Dygraph 1.1.1 版一起使用
    猜你喜欢
    • 2014-03-02
    • 1970-01-01
    • 1970-01-01
    • 2016-07-21
    • 2021-07-09
    • 2013-09-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多