Fix tick location according to local time near DST transitions.
authorJoan Pau Beltran <joanpau.beltran@socib.cat>
Thu, 30 Oct 2014 13:35:42 +0000 (09:35 -0400)
committerJoan Pau Beltran <joanpau.beltran@socib.cat>
Mon, 3 Nov 2014 15:32:32 +0000 (16:32 +0100)
The curent algorithm for tick generation uses the 'roll over'
property of the `Date` methods. Hence, it produces two artifacts:

  1.  A one-hour gap of ticks within an hour of the 'fall back' transition.

  2.  Moves ticks within the hour skipped at the 'spring forward' transition.

To solve the first defect, use the constant spacing in milliseconds
for granularities not coarser than hourly.

To solve the second defect, check that computed dates using the 'roll over'
method return the expected tick position.

dygraph-tickers.js

index 1e0c61d..7b3dae1 100644 (file)
@@ -344,8 +344,9 @@ Dygraph.getDateAxis = function(start_time, end_time, granularity, opts, dg) {
       opts("axisLabelFormatter"));
   var utc = opts("labelsDateUTC");
   
-  var step = Dygraph.TICK_PLACEMENT[granularity].step;
   var datefield = Dygraph.TICK_PLACEMENT[granularity].datefield;
+  var step = Dygraph.TICK_PLACEMENT[granularity].step;
+  var spacing = Dygraph.TICK_PLACEMENT[granularity].spacing;
   
   // Choose appropiate date methods according to UTC or local time option.
   // weekday:        return the day of week from a Date object.
@@ -423,30 +424,67 @@ Dygraph.getDateAxis = function(start_time, end_time, granularity, opts, dg) {
   }
 
   // Generate the ticks.
-  // This relies on the roll over property of the Date functions:
-  // when some date field is set to a value outside of its logical range,
-  // the excess 'rolls over' the next (more significant) field.
-  // When using local time with DST transitions, different dates may represent 
-  // the same time instant, so do not repeat the tick. At each step, 
-  // we have to check that the date is effectively increased because native 
-  // JS date functions do not assert that on DST transitions.
+  // For granularities not coarser than HOURLY we use the fact that:
+  //   the number of milliseconds between ticks is constant
+  //   and equal to the defined spacing.
+  // Otherwise we rely on the 'roll over' property of the Date functions:
+  //   when some date field is set to a value outside of its logical range,
+  //   the excess 'rolls over' the next (more significant) field.
+  // However, when using local time with DST transitions,
+  // there are dates that do not represent any time value at all
+  // (those in the hour skipped at the 'spring forward'),
+  // and the JavaScript engines usually return an equivalent value.
+  // Hence we have to check that the date is effectively increased at each step,
+  // and that a tick should be place at the returned date.
   // Since start_date is no later than start_time (but possibly equal), 
-  // assuming a previous tick just before start_time also removes an spurious
+  // assuming a previous tick just before start_time removes an spurious
   // tick outside the given time range.
   var ticks = [];
   var next_tick_date = compose_date(date_array);
   var next_tick_time = next_tick_date.getTime();
   var prev_tick_time = start_time - 1;
-  while (next_tick_time <= end_time) {
-    if (next_tick_time > prev_tick_time) {
-      ticks.push({ v: next_tick_time,
-                   label: formatter(next_tick_date, granularity, opts, dg)
-                 });
-      prev_tick_time = next_tick_time;
+  if (granularity <= Dygraph.HOURLY) {
+    while (next_tick_time <= end_time) {
+      if (next_tick_time > prev_tick_time) {
+        ticks.push({ v: next_tick_time,
+                     label: formatter(next_tick_date, granularity, opts, dg)
+                   });
+        prev_tick_time = next_tick_time;
+      }
+      next_tick_time += spacing;
+      next_tick_date = new Date(next_tick_time);
+    }
+  } else if (granularity < Dygraph.DAILY) {
+    var get_hours;
+    if (utc) {
+        get_hours = function (d) { return d.getUTCHours(); };
+    } else {
+        get_hours = function (d) { return d.getHours(); };
+    }
+    while (next_tick_time <= end_time) {
+      if (next_tick_time > prev_tick_time &&
+          get_hours(next_tick_date) % step == 0) {
+        ticks.push({ v: next_tick_time,
+                     label: formatter(next_tick_date, granularity, opts, dg)
+                   });
+        prev_tick_time = next_tick_time;
+      }
+      date_array[datefield] += step;
+      next_tick_date = compose_date(date_array);
+      next_tick_time = next_tick_date.getTime();
+    }
+  } else {
+    while (next_tick_time <= end_time) {
+      if (next_tick_time > prev_tick_time) {
+        ticks.push({ v: next_tick_time,
+                     label: formatter(next_tick_date, granularity, opts, dg)
+                   });
+        prev_tick_time = next_tick_time;
+      }
+      date_array[datefield] += step;
+      next_tick_date = compose_date(date_array);
+      next_tick_time = next_tick_date.getTime();
     }
-    date_array[datefield] += step;
-    next_tick_date = compose_date(date_array);
-    next_tick_time = next_tick_date.getTime();
   }
   return ticks;
 };