1 /**
  2  * @license
  3  * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
  4  * MIT-licenced: https://opensource.org/licenses/MIT
  5  */
  6 
  7 /**
  8  * @fileoverview Description of this file.
  9  * @author danvk@google.com (Dan Vanderkam)
 10  */
 11 
 12 /*
 13  * A ticker is a function with the following interface:
 14  *
 15  * function(a, b, pixels, options_view, dygraph, forced_values);
 16  * -> [ { v: tick1_v, label: tick1_label[, label_v: label_v1] },
 17  *      { v: tick2_v, label: tick2_label[, label_v: label_v2] },
 18  *      ...
 19  *    ]
 20  *
 21  * The returned value is called a "tick list".
 22  *
 23  * Arguments
 24  * ---------
 25  *
 26  * [a, b] is the range of the axis for which ticks are being generated. For a
 27  * numeric axis, these will simply be numbers. For a date axis, these will be
 28  * millis since epoch (convertable to Date objects using "new Date(a)" and "new
 29  * Date(b)").
 30  *
 31  * opts provides access to chart- and axis-specific options. It can be used to
 32  * access number/date formatting code/options, check for a log scale, etc.
 33  *
 34  * pixels is the length of the axis in pixels. opts('pixelsPerLabel') is the
 35  * minimum amount of space to be allotted to each label. For instance, if
 36  * pixels=400 and opts('pixelsPerLabel')=40 then the ticker should return
 37  * between zero and ten (400/40) ticks.
 38  *
 39  * dygraph is the Dygraph object for which an axis is being constructed.
 40  *
 41  * forced_values is used for secondary y-axes. The tick positions are typically
 42  * set by the primary y-axis, so the secondary y-axis has no choice in where to
 43  * put these. It simply has to generate labels for these data values.
 44  *
 45  * Tick lists
 46  * ----------
 47  * Typically a tick will have both a grid/tick line and a label at one end of
 48  * that line (at the bottom for an x-axis, at left or right for the y-axis).
 49  *
 50  * A tick may be missing one of these two components:
 51  * - If "label_v" is specified instead of "v", then there will be no tick or
 52  *   gridline, just a label.
 53  * - Similarly, if "label" is not specified, then there will be a gridline
 54  *   without a label.
 55  *
 56  * This flexibility is useful in a few situations:
 57  * - For log scales, some of the tick lines may be too close to all have labels.
 58  * - For date scales where years are being displayed, it is desirable to display
 59  *   tick marks at the beginnings of years but labels (e.g. "2006") in the
 60  *   middle of the years.
 61  */
 62 
 63 /*jshint sub:true */
 64 /*global Dygraph:false */
 65 "use strict";
 66 
 67 import * as utils from './dygraph-utils';
 68 
 69 /** @typedef {Array.<{v:number, label:string, label_v:(string|undefined)}>} */
 70 var TickList = undefined;  // the ' = undefined' keeps jshint happy.
 71 
 72 /** @typedef {function(
 73  *    number,
 74  *    number,
 75  *    number,
 76  *    function(string):*,
 77  *    Dygraph=,
 78  *    Array.<number>=
 79  *  ): TickList}
 80  */
 81 var Ticker = undefined;  // the ' = undefined' keeps jshint happy.
 82 
 83 /** @type {Ticker} */
 84 export var numericLinearTicks = function(a, b, pixels, opts, dygraph, vals) {
 85   var nonLogscaleOpts = function(opt) {
 86     if (opt === 'logscale') return false;
 87     return opts(opt);
 88   };
 89   return numericTicks(a, b, pixels, nonLogscaleOpts, dygraph, vals);
 90 };
 91 
 92 /** @type {Ticker} */
 93 export var numericTicks = function(a, b, pixels, opts, dygraph, vals) {
 94   var pixels_per_tick = /** @type{number} */(opts('pixelsPerLabel'));
 95   var ticks = [];
 96   var i, j, tickV, nTicks;
 97   if (vals) {
 98     for (i = 0; i < vals.length; i++) {
 99       ticks.push({v: vals[i]});
100     }
101   } else {
102     // TODO(danvk): factor this log-scale block out into a separate function.
103     if (opts("logscale")) {
104       nTicks  = Math.floor(pixels / pixels_per_tick);
105       var minIdx = utils.binarySearch(a, PREFERRED_LOG_TICK_VALUES, 1);
106       var maxIdx = utils.binarySearch(b, PREFERRED_LOG_TICK_VALUES, -1);
107       if (minIdx == -1) {
108         minIdx = 0;
109       }
110       if (maxIdx == -1) {
111         maxIdx = PREFERRED_LOG_TICK_VALUES.length - 1;
112       }
113       // Count the number of tick values would appear, if we can get at least
114       // nTicks / 4 accept them.
115       var lastDisplayed = null;
116       if (maxIdx - minIdx >= nTicks / 4) {
117         for (var idx = maxIdx; idx >= minIdx; idx--) {
118           var tickValue = PREFERRED_LOG_TICK_VALUES[idx];
119           var pixel_coord = Math.log(tickValue / a) / Math.log(b / a) * pixels;
120           var tick = { v: tickValue };
121           if (lastDisplayed === null) {
122             lastDisplayed = {
123               tickValue : tickValue,
124               pixel_coord : pixel_coord
125             };
126           } else {
127             if (Math.abs(pixel_coord - lastDisplayed.pixel_coord) >= pixels_per_tick) {
128               lastDisplayed = {
129                 tickValue : tickValue,
130                 pixel_coord : pixel_coord
131               };
132             } else {
133               tick.label = "";
134             }
135           }
136           ticks.push(tick);
137         }
138         // Since we went in backwards order.
139         ticks.reverse();
140       }
141     }
142 
143     // ticks.length won't be 0 if the log scale function finds values to insert.
144     if (ticks.length === 0) {
145       // Basic idea:
146       // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
147       // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
148       // The first spacing greater than pixelsPerYLabel is what we use.
149       // TODO(danvk): version that works on a log scale.
150       var kmg2 = opts("labelsKMG2");
151       var mults, base;
152       if (kmg2) {
153         mults = [1, 2, 4, 8, 16, 32, 64, 128, 256];
154         base = 16;
155       } else {
156         mults = [1, 2, 5, 10, 20, 50, 100];
157         base = 10;
158       }
159 
160       // Get the maximum number of permitted ticks based on the
161       // graph's pixel size and pixels_per_tick setting.
162       var max_ticks = Math.ceil(pixels / pixels_per_tick);
163 
164       // Now calculate the data unit equivalent of this tick spacing.
165       // Use abs() since graphs may have a reversed Y axis.
166       var units_per_tick = Math.abs(b - a) / max_ticks;
167 
168       // Based on this, get a starting scale which is the largest
169       // integer power of the chosen base (10 or 16) that still remains
170       // below the requested pixels_per_tick spacing.
171       var base_power = Math.floor(Math.log(units_per_tick) / Math.log(base));
172       var base_scale = Math.pow(base, base_power);
173 
174       // Now try multiples of the starting scale until we find one
175       // that results in tick marks spaced sufficiently far apart.
176       // The "mults" array should cover the range 1 .. base^2 to
177       // adjust for rounding and edge effects.
178       var scale, low_val, high_val, spacing;
179       for (j = 0; j < mults.length; j++) {
180         scale = base_scale * mults[j];
181         low_val = Math.floor(a / scale) * scale;
182         high_val = Math.ceil(b / scale) * scale;
183         nTicks = Math.abs(high_val - low_val) / scale;
184         spacing = pixels / nTicks;
185         if (spacing > pixels_per_tick) break;
186       }
187 
188       // Construct the set of ticks.
189       // Allow reverse y-axis if it's explicitly requested.
190       if (low_val > high_val) scale *= -1;
191       for (i = 0; i <= nTicks; i++) {
192         tickV = low_val + i * scale;
193         ticks.push( {v: tickV} );
194       }
195     }
196   }
197 
198   var formatter = /**@type{AxisLabelFormatter}*/(opts('axisLabelFormatter'));
199 
200   // Add labels to the ticks.
201   for (i = 0; i < ticks.length; i++) {
202     if (ticks[i].label !== undefined) continue;  // Use current label.
203     // TODO(danvk): set granularity to something appropriate here.
204     ticks[i].label = formatter.call(dygraph, ticks[i].v, 0, opts, dygraph);
205   }
206 
207   return ticks;
208 };
209 
210 /** @type {Ticker} */
211 export var dateTicker = function(a, b, pixels, opts, dygraph, vals) {
212   var chosen = pickDateTickGranularity(a, b, pixels, opts);
213 
214   if (chosen >= 0) {
215     return getDateAxis(a, b, chosen, opts, dygraph);
216   } else {
217     // this can happen if self.width_ is zero.
218     return [];
219   }
220 };
221 
222 // Time granularity enumeration
223 export var Granularity = {
224   MILLISECONDLY: 0,
225   TWO_MILLISECONDLY: 1,
226   FIVE_MILLISECONDLY: 2,
227   TEN_MILLISECONDLY: 3,
228   FIFTY_MILLISECONDLY: 4,
229   HUNDRED_MILLISECONDLY: 5,
230   FIVE_HUNDRED_MILLISECONDLY: 6,
231   SECONDLY: 7,
232   TWO_SECONDLY: 8,
233   FIVE_SECONDLY: 9,
234   TEN_SECONDLY: 10,
235   THIRTY_SECONDLY: 11,
236   MINUTELY: 12,
237   TWO_MINUTELY: 13,
238   FIVE_MINUTELY: 14,
239   TEN_MINUTELY: 15,
240   THIRTY_MINUTELY: 16,
241   HOURLY: 17,
242   TWO_HOURLY: 18,
243   SIX_HOURLY: 19,
244   DAILY: 20,
245   TWO_DAILY: 21,
246   WEEKLY: 22,
247   MONTHLY: 23,
248   QUARTERLY: 24,
249   BIANNUAL: 25,
250   ANNUAL: 26,
251   DECADAL: 27,
252   CENTENNIAL: 28,
253   NUM_GRANULARITIES: 29
254 }
255 
256 // Date components enumeration (in the order of the arguments in Date)
257 // TODO: make this an @enum
258 var DateField = {
259   DATEFIELD_Y: 0,
260   DATEFIELD_M: 1,
261   DATEFIELD_D: 2,
262   DATEFIELD_HH: 3,
263   DATEFIELD_MM: 4,
264   DATEFIELD_SS: 5,
265   DATEFIELD_MS: 6,
266   NUM_DATEFIELDS: 7
267 };
268 
269 /**
270  * The value of datefield will start at an even multiple of "step", i.e.
271  *   if datefield=SS and step=5 then the first tick will be on a multiple of 5s.
272  *
273  * For granularities <= HOURLY, ticks are generated every `spacing` ms.
274  *
275  * At coarser granularities, ticks are generated by incrementing `datefield` by
276  *   `step`. In this case, the `spacing` value is only used to estimate the
277  *   number of ticks. It should roughly correspond to the spacing between
278  *   adjacent ticks.
279  *
280  * @type {Array.<{datefield:number, step:number, spacing:number}>}
281  */
282 var TICK_PLACEMENT = [];
283 TICK_PLACEMENT[Granularity.MILLISECONDLY]               = {datefield: DateField.DATEFIELD_MS, step:   1, spacing: 1};
284 TICK_PLACEMENT[Granularity.TWO_MILLISECONDLY]           = {datefield: DateField.DATEFIELD_MS, step:   2, spacing: 2};
285 TICK_PLACEMENT[Granularity.FIVE_MILLISECONDLY]          = {datefield: DateField.DATEFIELD_MS, step:   5, spacing: 5};
286 TICK_PLACEMENT[Granularity.TEN_MILLISECONDLY]           = {datefield: DateField.DATEFIELD_MS, step:  10, spacing: 10};
287 TICK_PLACEMENT[Granularity.FIFTY_MILLISECONDLY]         = {datefield: DateField.DATEFIELD_MS, step:  50, spacing: 50};
288 TICK_PLACEMENT[Granularity.HUNDRED_MILLISECONDLY]       = {datefield: DateField.DATEFIELD_MS, step: 100, spacing: 100};
289 TICK_PLACEMENT[Granularity.FIVE_HUNDRED_MILLISECONDLY]  = {datefield: DateField.DATEFIELD_MS, step: 500, spacing: 500};
290 TICK_PLACEMENT[Granularity.SECONDLY]        = {datefield: DateField.DATEFIELD_SS, step:   1, spacing: 1000 * 1};
291 TICK_PLACEMENT[Granularity.TWO_SECONDLY]    = {datefield: DateField.DATEFIELD_SS, step:   2, spacing: 1000 * 2};
292 TICK_PLACEMENT[Granularity.FIVE_SECONDLY]   = {datefield: DateField.DATEFIELD_SS, step:   5, spacing: 1000 * 5};
293 TICK_PLACEMENT[Granularity.TEN_SECONDLY]    = {datefield: DateField.DATEFIELD_SS, step:  10, spacing: 1000 * 10};
294 TICK_PLACEMENT[Granularity.THIRTY_SECONDLY] = {datefield: DateField.DATEFIELD_SS, step:  30, spacing: 1000 * 30};
295 TICK_PLACEMENT[Granularity.MINUTELY]        = {datefield: DateField.DATEFIELD_MM, step:   1, spacing: 1000 * 60};
296 TICK_PLACEMENT[Granularity.TWO_MINUTELY]    = {datefield: DateField.DATEFIELD_MM, step:   2, spacing: 1000 * 60 * 2};
297 TICK_PLACEMENT[Granularity.FIVE_MINUTELY]   = {datefield: DateField.DATEFIELD_MM, step:   5, spacing: 1000 * 60 * 5};
298 TICK_PLACEMENT[Granularity.TEN_MINUTELY]    = {datefield: DateField.DATEFIELD_MM, step:  10, spacing: 1000 * 60 * 10};
299 TICK_PLACEMENT[Granularity.THIRTY_MINUTELY] = {datefield: DateField.DATEFIELD_MM, step:  30, spacing: 1000 * 60 * 30};
300 TICK_PLACEMENT[Granularity.HOURLY]          = {datefield: DateField.DATEFIELD_HH, step:   1, spacing: 1000 * 3600};
301 TICK_PLACEMENT[Granularity.TWO_HOURLY]      = {datefield: DateField.DATEFIELD_HH, step:   2, spacing: 1000 * 3600 * 2};
302 TICK_PLACEMENT[Granularity.SIX_HOURLY]      = {datefield: DateField.DATEFIELD_HH, step:   6, spacing: 1000 * 3600 * 6};
303 TICK_PLACEMENT[Granularity.DAILY]           = {datefield: DateField.DATEFIELD_D,  step:   1, spacing: 1000 * 86400};
304 TICK_PLACEMENT[Granularity.TWO_DAILY]       = {datefield: DateField.DATEFIELD_D,  step:   2, spacing: 1000 * 86400 * 2};
305 TICK_PLACEMENT[Granularity.WEEKLY]          = {datefield: DateField.DATEFIELD_D,  step:   7, spacing: 1000 * 604800};
306 TICK_PLACEMENT[Granularity.MONTHLY]         = {datefield: DateField.DATEFIELD_M,  step:   1, spacing: 1000 * 7200  * 365.2425}; // 1e3 * 60 * 60 * 24 * 365.2425 / 12
307 TICK_PLACEMENT[Granularity.QUARTERLY]       = {datefield: DateField.DATEFIELD_M,  step:   3, spacing: 1000 * 21600 * 365.2425}; // 1e3 * 60 * 60 * 24 * 365.2425 / 4
308 TICK_PLACEMENT[Granularity.BIANNUAL]        = {datefield: DateField.DATEFIELD_M,  step:   6, spacing: 1000 * 43200 * 365.2425}; // 1e3 * 60 * 60 * 24 * 365.2425 / 2
309 TICK_PLACEMENT[Granularity.ANNUAL]          = {datefield: DateField.DATEFIELD_Y,  step:   1, spacing: 1000 * 86400   * 365.2425}; // 1e3 * 60 * 60 * 24 * 365.2425 * 1
310 TICK_PLACEMENT[Granularity.DECADAL]         = {datefield: DateField.DATEFIELD_Y,  step:  10, spacing: 1000 * 864000  * 365.2425}; // 1e3 * 60 * 60 * 24 * 365.2425 * 10
311 TICK_PLACEMENT[Granularity.CENTENNIAL]      = {datefield: DateField.DATEFIELD_Y,  step: 100, spacing: 1000 * 8640000 * 365.2425}; // 1e3 * 60 * 60 * 24 * 365.2425 * 100
312 
313 /**
314  * This is a list of human-friendly values at which to show tick marks on a log
315  * scale. It is k * 10^n, where k=1..9 and n=-39..+39, so:
316  * ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ...
317  * NOTE: this assumes that utils.LOG_SCALE = 10.
318  * @type {Array.<number>}
319  */
320 var PREFERRED_LOG_TICK_VALUES = (function() {
321   var vals = [];
322   for (var power = -39; power <= 39; power++) {
323     var range = Math.pow(10, power);
324     for (var mult = 1; mult <= 9; mult++) {
325       var val = range * mult;
326       vals.push(val);
327     }
328   }
329   return vals;
330 })();
331 
332 /**
333  * Determine the correct granularity of ticks on a date axis.
334  *
335  * @param {number} a Left edge of the chart (ms)
336  * @param {number} b Right edge of the chart (ms)
337  * @param {number} pixels Size of the chart in the relevant dimension (width).
338  * @param {function(string):*} opts Function mapping from option name -> value.
339  * @return {number} The appropriate axis granularity for this chart. See the
340  *     enumeration of possible values in dygraph-tickers.js.
341  */
342 var pickDateTickGranularity = function(a, b, pixels, opts) {
343   var pixels_per_tick = /** @type{number} */(opts('pixelsPerLabel'));
344   for (var i = 0; i < Granularity.NUM_GRANULARITIES; i++) {
345     var num_ticks = numDateTicks(a, b, i);
346     if (pixels / num_ticks >= pixels_per_tick) {
347       return i;
348     }
349   }
350   return -1;
351 };
352 
353 /**
354  * Compute the number of ticks on a date axis for a given granularity.
355  * @param {number} start_time
356  * @param {number} end_time
357  * @param {number} granularity (one of the granularities enumerated above)
358  * @return {number} (Approximate) number of ticks that would result.
359  */
360 var numDateTicks = function(start_time, end_time, granularity) {
361   var spacing = TICK_PLACEMENT[granularity].spacing;
362   return Math.round(1.0 * (end_time - start_time) / spacing);
363 };
364 
365 /**
366  * Compute the positions and labels of ticks on a date axis for a given granularity.
367  * @param {number} start_time
368  * @param {number} end_time
369  * @param {number} granularity (one of the granularities enumerated above)
370  * @param {function(string):*} opts Function mapping from option name -> value.
371  * @param {Dygraph=} dg
372  * @return {!TickList}
373  */
374 export var getDateAxis = function(start_time, end_time, granularity, opts, dg) {
375   var formatter = /** @type{AxisLabelFormatter} */(
376       opts("axisLabelFormatter"));
377   var utc = opts("labelsUTC");
378   var accessors = utc ? utils.DateAccessorsUTC : utils.DateAccessorsLocal;
379 
380   var datefield = TICK_PLACEMENT[granularity].datefield;
381   var step = TICK_PLACEMENT[granularity].step;
382   var spacing = TICK_PLACEMENT[granularity].spacing;
383 
384   // Choose a nice tick position before the initial instant.
385   // Currently, this code deals properly with the existent daily granularities:
386   // DAILY (with step of 1) and WEEKLY (with step of 7 but specially handled).
387   // Other daily granularities (say TWO_DAILY) should also be handled specially
388   // by setting the start_date_offset to 0.
389   var start_date = new Date(start_time);
390   var date_array = [];
391   date_array[DateField.DATEFIELD_Y]  = accessors.getFullYear(start_date);
392   date_array[DateField.DATEFIELD_M]  = accessors.getMonth(start_date);
393   date_array[DateField.DATEFIELD_D]  = accessors.getDate(start_date);
394   date_array[DateField.DATEFIELD_HH] = accessors.getHours(start_date);
395   date_array[DateField.DATEFIELD_MM] = accessors.getMinutes(start_date);
396   date_array[DateField.DATEFIELD_SS] = accessors.getSeconds(start_date);
397   date_array[DateField.DATEFIELD_MS] = accessors.getMilliseconds(start_date);
398 
399   var start_date_offset = date_array[datefield] % step;
400   if (granularity == Granularity.WEEKLY) {
401     // This will put the ticks on Sundays.
402     start_date_offset = accessors.getDay(start_date);
403   }
404 
405   date_array[datefield] -= start_date_offset;
406   for (var df = datefield + 1; df < DateField.NUM_DATEFIELDS; df++) {
407     // The minimum value is 1 for the day of month, and 0 for all other fields.
408     date_array[df] = (df === DateField.DATEFIELD_D) ? 1 : 0;
409   }
410 
411   // Generate the ticks.
412   // For granularities not coarser than HOURLY we use the fact that:
413   //   the number of milliseconds between ticks is constant
414   //   and equal to the defined spacing.
415   // Otherwise we rely on the 'roll over' property of the Date functions:
416   //   when some date field is set to a value outside of its logical range,
417   //   the excess 'rolls over' the next (more significant) field.
418   // However, when using local time with DST transitions,
419   // there are dates that do not represent any time value at all
420   // (those in the hour skipped at the 'spring forward'),
421   // and the JavaScript engines usually return an equivalent value.
422   // Hence we have to check that the date is properly increased at each step,
423   // returning a date at a nice tick position.
424   var ticks = [];
425   var tick_date = accessors.makeDate.apply(null, date_array);
426   var tick_time = tick_date.getTime();
427   if (granularity <= Granularity.HOURLY) {
428     if (tick_time < start_time) {
429       tick_time += spacing;
430       tick_date = new Date(tick_time);
431     }
432     while (tick_time <= end_time) {
433       ticks.push({ v: tick_time,
434                    label: formatter.call(dg, tick_date, granularity, opts, dg)
435                  });
436       tick_time += spacing;
437       tick_date = new Date(tick_time);
438     }
439   } else {
440     if (tick_time < start_time) {
441       date_array[datefield] += step;
442       tick_date = accessors.makeDate.apply(null, date_array);
443       tick_time = tick_date.getTime();
444     }
445     while (tick_time <= end_time) {
446       if (granularity >= Granularity.DAILY ||
447           accessors.getHours(tick_date) % step === 0) {
448         ticks.push({ v: tick_time,
449                      label: formatter.call(dg, tick_date, granularity, opts, dg)
450                    });
451       }
452       date_array[datefield] += step;
453       tick_date = accessors.makeDate.apply(null, date_array);
454       tick_time = tick_date.getTime();
455     }
456   }
457   return ticks;
458 };
459