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