Initial OpenECOMP policy/engine commit
[policy/engine.git] / ecomp-sdk-app / src / main / webapp / static / fusion / raptor / dy3 / js / dygraph-tickers.js
1 /**
2  * @license
3  * Copyright 2011 Dan Vanderkam (danvdk@gmail.com)
4  * MIT-licensed (http://opensource.org/licenses/MIT)
5  */
6
7 /**
8  * @fileoverview Description of this file.
9  * @author danvk@google.com (Dan Vanderkam)
10  *
11  * A ticker is a function with the following interface:
12  *
13  * function(a, b, pixels, options_view, dygraph, forced_values);
14  * -> [ { v: tick1_v, label: tick1_label[, label_v: label_v1] },
15  *      { v: tick2_v, label: tick2_label[, label_v: label_v2] },
16  *      ...
17  *    ]
18  *
19  * The returned value is called a "tick list".
20  *
21  * Arguments
22  * ---------
23  *
24  * [a, b] is the range of the axis for which ticks are being generated. For a
25  * numeric axis, these will simply be numbers. For a date axis, these will be
26  * millis since epoch (convertable to Date objects using "new Date(a)" and "new
27  * Date(b)").
28  *
29  * opts provides access to chart- and axis-specific options. It can be used to
30  * access number/date formatting code/options, check for a log scale, etc.
31  *
32  * pixels is the length of the axis in pixels. opts('pixelsPerLabel') is the
33  * minimum amount of space to be allotted to each label. For instance, if
34  * pixels=400 and opts('pixelsPerLabel')=40 then the ticker should return
35  * between zero and ten (400/40) ticks.
36  *
37  * dygraph is the Dygraph object for which an axis is being constructed.
38  *
39  * forced_values is used for secondary y-axes. The tick positions are typically
40  * set by the primary y-axis, so the secondary y-axis has no choice in where to
41  * put these. It simply has to generate labels for these data values.
42  *
43  * Tick lists
44  * ----------
45  * Typically a tick will have both a grid/tick line and a label at one end of
46  * that line (at the bottom for an x-axis, at left or right for the y-axis).
47  *
48  * A tick may be missing one of these two components:
49  * - If "label_v" is specified instead of "v", then there will be no tick or
50  *   gridline, just a label.
51  * - Similarly, if "label" is not specified, then there will be a gridline
52  *   without a label.
53  *
54  * This flexibility is useful in a few situations:
55  * - For log scales, some of the tick lines may be too close to all have labels.
56  * - For date scales where years are being displayed, it is desirable to display
57  *   tick marks at the beginnings of years but labels (e.g. "2006") in the
58  *   middle of the years.
59  */
60
61 /*jshint globalstrict:true, sub:true */
62 /*global Dygraph:false */
63 "use strict";
64
65 /** @typedef {Array.<{v:number, label:string, label_v:(string|undefined)}>} */
66 Dygraph.TickList = undefined;  // the ' = undefined' keeps jshint happy.
67
68 /** @typedef {function(
69  *    number,
70  *    number,
71  *    number,
72  *    function(string):*,
73  *    Dygraph=,
74  *    Array.<number>=
75  *  ): Dygraph.TickList}
76  */
77 Dygraph.Ticker = undefined;  // the ' = undefined' keeps jshint happy.
78
79 /** @type {Dygraph.Ticker} */
80 Dygraph.numericLinearTicks = function(a, b, pixels, opts, dygraph, vals) {
81   var nonLogscaleOpts = function(opt) {
82     if (opt === 'logscale') return false;
83     return opts(opt);
84   };
85   return Dygraph.numericTicks(a, b, pixels, nonLogscaleOpts, dygraph, vals);
86 };
87
88 /** @type {Dygraph.Ticker} */
89 Dygraph.numericTicks = function(a, b, pixels, opts, dygraph, vals) {
90   var pixels_per_tick = /** @type{number} */(opts('pixelsPerLabel'));
91   var ticks = [];
92   var i, j, tickV, nTicks;
93   if (vals) {
94     for (i = 0; i < vals.length; i++) {
95       ticks.push({v: vals[i]});
96     }
97   } else {
98     // TODO(danvk): factor this log-scale block out into a separate function.
99     if (opts("logscale")) {
100       nTicks  = Math.floor(pixels / pixels_per_tick);
101       var minIdx = Dygraph.binarySearch(a, Dygraph.PREFERRED_LOG_TICK_VALUES, 1);
102       var maxIdx = Dygraph.binarySearch(b, Dygraph.PREFERRED_LOG_TICK_VALUES, -1);
103       if (minIdx == -1) {
104         minIdx = 0;
105       }
106       if (maxIdx == -1) {
107         maxIdx = Dygraph.PREFERRED_LOG_TICK_VALUES.length - 1;
108       }
109       // Count the number of tick values would appear, if we can get at least
110       // nTicks / 4 accept them.
111       var lastDisplayed = null;
112       if (maxIdx - minIdx >= nTicks / 4) {
113         for (var idx = maxIdx; idx >= minIdx; idx--) {
114           var tickValue = Dygraph.PREFERRED_LOG_TICK_VALUES[idx];
115           var pixel_coord = Math.log(tickValue / a) / Math.log(b / a) * pixels;
116           var tick = { v: tickValue };
117           if (lastDisplayed === null) {
118             lastDisplayed = {
119               tickValue : tickValue,
120               pixel_coord : pixel_coord
121             };
122           } else {
123             if (Math.abs(pixel_coord - lastDisplayed.pixel_coord) >= pixels_per_tick) {
124               lastDisplayed = {
125                 tickValue : tickValue,
126                 pixel_coord : pixel_coord
127               };
128             } else {
129               tick.label = "";
130             }
131           }
132           ticks.push(tick);
133         }
134         // Since we went in backwards order.
135         ticks.reverse();
136       }
137     }
138
139     // ticks.length won't be 0 if the log scale function finds values to insert.
140     if (ticks.length === 0) {
141       // Basic idea:
142       // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
143       // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
144       // The first spacing greater than pixelsPerYLabel is what we use.
145       // TODO(danvk): version that works on a log scale.
146       var kmg2 = opts("labelsKMG2");
147       var mults, base;
148       if (kmg2) {
149         mults = [1, 2, 4, 8, 16, 32, 64, 128, 256];
150         base = 16;
151       } else {
152         mults = [1, 2, 5, 10, 20, 50, 100];
153         base = 10;
154       }
155
156       // Get the maximum number of permitted ticks based on the
157       // graph's pixel size and pixels_per_tick setting.
158       var max_ticks = Math.ceil(pixels / pixels_per_tick);
159
160       // Now calculate the data unit equivalent of this tick spacing.
161       // Use abs() since graphs may have a reversed Y axis.
162       var units_per_tick = Math.abs(b - a) / max_ticks;
163
164       // Based on this, get a starting scale which is the largest
165       // integer power of the chosen base (10 or 16) that still remains
166       // below the requested pixels_per_tick spacing.
167       var base_power = Math.floor(Math.log(units_per_tick) / Math.log(base));
168       var base_scale = Math.pow(base, base_power);
169
170       // Now try multiples of the starting scale until we find one
171       // that results in tick marks spaced sufficiently far apart.
172       // The "mults" array should cover the range 1 .. base^2 to
173       // adjust for rounding and edge effects.
174       var scale, low_val, high_val, spacing;
175       for (j = 0; j < mults.length; j++) {
176         scale = base_scale * mults[j];
177         low_val = Math.floor(a / scale) * scale;
178         high_val = Math.ceil(b / scale) * scale;
179         nTicks = Math.abs(high_val - low_val) / scale;
180         spacing = pixels / nTicks;
181         if (spacing > pixels_per_tick) break;
182       }
183
184       // Construct the set of ticks.
185       // Allow reverse y-axis if it's explicitly requested.
186       if (low_val > high_val) scale *= -1;
187       for (i = 0; i < nTicks; i++) {
188         tickV = low_val + i * scale;
189         ticks.push( {v: tickV} );
190       }
191     }
192   }
193
194   var formatter = /**@type{AxisLabelFormatter}*/(opts('axisLabelFormatter'));
195
196   // Add labels to the ticks.
197   for (i = 0; i < ticks.length; i++) {
198     if (ticks[i].label !== undefined) continue;  // Use current label.
199     // TODO(danvk): set granularity to something appropriate here.
200     ticks[i].label = formatter(ticks[i].v, 0, opts, dygraph);
201   }
202
203   return ticks;
204 };
205
206
207 /** @type {Dygraph.Ticker} */
208 Dygraph.dateTicker = function(a, b, pixels, opts, dygraph, vals) {
209   var chosen = Dygraph.pickDateTickGranularity(a, b, pixels, opts);
210
211   if (chosen >= 0) {
212     return Dygraph.getDateAxis(a, b, chosen, opts, dygraph);
213   } else {
214     // this can happen if self.width_ is zero.
215     return [];
216   }
217 };
218
219 // Time granularity enumeration
220 // TODO(danvk): make this an @enum
221 Dygraph.SECONDLY = 0;
222 Dygraph.TWO_SECONDLY = 1;
223 Dygraph.FIVE_SECONDLY = 2;
224 Dygraph.TEN_SECONDLY = 3;
225 Dygraph.THIRTY_SECONDLY  = 4;
226 Dygraph.MINUTELY = 5;
227 Dygraph.TWO_MINUTELY = 6;
228 Dygraph.FIVE_MINUTELY = 7;
229 Dygraph.TEN_MINUTELY = 8;
230 Dygraph.THIRTY_MINUTELY = 9;
231 Dygraph.HOURLY = 10;
232 Dygraph.TWO_HOURLY = 11;
233 Dygraph.SIX_HOURLY = 12;
234 Dygraph.DAILY = 13;
235 Dygraph.WEEKLY = 14;
236 Dygraph.MONTHLY = 15;
237 Dygraph.QUARTERLY = 16;
238 Dygraph.BIANNUAL = 17;
239 Dygraph.ANNUAL = 18;
240 Dygraph.DECADAL = 19;
241 Dygraph.CENTENNIAL = 20;
242 Dygraph.NUM_GRANULARITIES = 21;
243
244 /** @type {Array.<number>} */
245 Dygraph.SHORT_SPACINGS = [];
246 Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]        = 1000 * 1;
247 Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY]    = 1000 * 2;
248 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY]   = 1000 * 5;
249 Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY]    = 1000 * 10;
250 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
251 Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY]        = 1000 * 60;
252 Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY]    = 1000 * 60 * 2;
253 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY]   = 1000 * 60 * 5;
254 Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY]    = 1000 * 60 * 10;
255 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
256 Dygraph.SHORT_SPACINGS[Dygraph.HOURLY]          = 1000 * 3600;
257 Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]      = 1000 * 3600 * 2;
258 Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY]      = 1000 * 3600 * 6;
259 Dygraph.SHORT_SPACINGS[Dygraph.DAILY]           = 1000 * 86400;
260 Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]          = 1000 * 604800;
261
262 /** 
263  * A collection of objects specifying where it is acceptable to place tick
264  * marks for granularities larger than WEEKLY.  
265  * 'months' is an array of month indexes on which to place tick marks.
266  * 'year_mod' ticks are placed when year % year_mod = 0.
267  * @type {Array.<Object>} 
268  */
269 Dygraph.LONG_TICK_PLACEMENTS = [];
270 Dygraph.LONG_TICK_PLACEMENTS[Dygraph.MONTHLY] = {
271   months : [0,1,2,3,4,5,6,7,8,9,10,11], 
272   year_mod : 1
273 };
274 Dygraph.LONG_TICK_PLACEMENTS[Dygraph.QUARTERLY] = {
275   months: [0,3,6,9], 
276   year_mod: 1
277 };
278 Dygraph.LONG_TICK_PLACEMENTS[Dygraph.BIANNUAL] = {
279   months: [0,6], 
280   year_mod: 1
281 };
282 Dygraph.LONG_TICK_PLACEMENTS[Dygraph.ANNUAL] = {
283   months: [0], 
284   year_mod: 1
285 };
286 Dygraph.LONG_TICK_PLACEMENTS[Dygraph.DECADAL] = {
287   months: [0], 
288   year_mod: 10
289 };
290 Dygraph.LONG_TICK_PLACEMENTS[Dygraph.CENTENNIAL] = {
291   months: [0], 
292   year_mod: 100
293 };
294
295 /**
296  * This is a list of human-friendly values at which to show tick marks on a log
297  * scale. It is k * 10^n, where k=1..9 and n=-39..+39, so:
298  * ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ...
299  * NOTE: this assumes that Dygraph.LOG_SCALE = 10.
300  * @type {Array.<number>}
301  */
302 Dygraph.PREFERRED_LOG_TICK_VALUES = function() {
303   var vals = [];
304   for (var power = -39; power <= 39; power++) {
305     var range = Math.pow(10, power);
306     for (var mult = 1; mult <= 9; mult++) {
307       var val = range * mult;
308       vals.push(val);
309     }
310   }
311   return vals;
312 }();
313
314 /**
315  * Determine the correct granularity of ticks on a date axis.
316  *
317  * @param {number} a Left edge of the chart (ms)
318  * @param {number} b Right edge of the chart (ms)
319  * @param {number} pixels Size of the chart in the relevant dimension (width).
320  * @param {function(string):*} opts Function mapping from option name ->
321  *     value.
322  * @return {number} The appropriate axis granularity for this chart. See the
323  *     enumeration of possible values in dygraph-tickers.js.
324  */
325 Dygraph.pickDateTickGranularity = function(a, b, pixels, opts) {
326   var pixels_per_tick = /** @type{number} */(opts('pixelsPerLabel'));
327   for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
328     var num_ticks = Dygraph.numDateTicks(a, b, i);
329     if (pixels / num_ticks >= pixels_per_tick) {
330       return i;
331     }
332   }
333   return -1;
334 };
335
336 /**
337  * @param {number} start_time
338  * @param {number} end_time
339  * @param {number} granularity (one of the granularities enumerated above)
340  * @return {number} Number of ticks that would result.
341  */
342 Dygraph.numDateTicks = function(start_time, end_time, granularity) {
343   if (granularity < Dygraph.MONTHLY) {
344     // Generate one tick mark for every fixed interval of time.
345     var spacing = Dygraph.SHORT_SPACINGS[granularity];
346     return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
347   } else {
348     var tickPlacement = Dygraph.LONG_TICK_PLACEMENTS[granularity];
349
350     var msInYear = 365.2524 * 24 * 3600 * 1000;
351     var num_years = 1.0 * (end_time - start_time) / msInYear;
352     return Math.floor(0.5 + 1.0 * num_years * tickPlacement.months.length / tickPlacement.year_mod);
353   }
354 };
355
356 /**
357  * @param {number} start_time
358  * @param {number} end_time
359  * @param {number} granularity (one of the granularities enumerated above)
360  * @param {function(string):*} opts Function mapping from option name -&gt; value.
361  * @param {Dygraph=} dg
362  * @return {!Dygraph.TickList}
363  */
364 Dygraph.getDateAxis = function(start_time, end_time, granularity, opts, dg) {
365   var formatter = /** @type{AxisLabelFormatter} */(
366       opts("axisLabelFormatter"));
367   var ticks = [];
368   var t;
369
370   if (granularity < Dygraph.MONTHLY) {
371     // Generate one tick mark for every fixed interval of time.
372     var spacing = Dygraph.SHORT_SPACINGS[granularity];
373
374     // Find a time less than start_time which occurs on a "nice" time boundary
375     // for this granularity.
376     var g = spacing / 1000;
377     var d = new Date(start_time);
378     Dygraph.setDateSameTZ(d, {ms: 0});
379
380     var x;
381     if (g <= 60) {  // seconds
382       x = d.getSeconds();
383       Dygraph.setDateSameTZ(d, {s: x - x % g});
384     } else {
385       Dygraph.setDateSameTZ(d, {s: 0});
386       g /= 60;
387       if (g <= 60) {  // minutes
388         x = d.getMinutes();
389         Dygraph.setDateSameTZ(d, {m: x - x % g});
390       } else {
391         Dygraph.setDateSameTZ(d, {m: 0});
392         g /= 60;
393
394         if (g <= 24) {  // days
395           x = d.getHours();
396           d.setHours(x - x % g);
397         } else {
398           d.setHours(0);
399           g /= 24;
400
401           if (g == 7) {  // one week
402             d.setDate(d.getDate() - d.getDay());
403           }
404         }
405       }
406     }
407     start_time = d.getTime();
408
409     // For spacings coarser than two-hourly, we want to ignore daylight
410     // savings transitions to get consistent ticks. For finer-grained ticks,
411     // it's essential to show the DST transition in all its messiness.
412     var start_offset_min = new Date(start_time).getTimezoneOffset();
413     var check_dst = (spacing >= Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]);
414
415     for (t = start_time; t <= end_time; t += spacing) {
416       d = new Date(t);
417
418       // This ensures that we stay on the same hourly "rhythm" across
419       // daylight savings transitions. Without this, the ticks could get off
420       // by an hour. See tests/daylight-savings.html or issue 147.
421       if (check_dst && d.getTimezoneOffset() != start_offset_min) {
422         var delta_min = d.getTimezoneOffset() - start_offset_min;
423         t += delta_min * 60 * 1000;
424         d = new Date(t);
425         start_offset_min = d.getTimezoneOffset();
426
427         // Check whether we've backed into the previous timezone again.
428         // This can happen during a "spring forward" transition. In this case,
429         // it's best to skip this tick altogether (we may be shooting for a
430         // non-existent time like the 2AM that's skipped) and go to the next
431         // one.
432         if (new Date(t + spacing).getTimezoneOffset() != start_offset_min) {
433           t += spacing;
434           d = new Date(t);
435           start_offset_min = d.getTimezoneOffset();
436         }
437       }
438
439       ticks.push({ v:t,
440                    label: formatter(d, granularity, opts, dg)
441                  });
442     }
443   } else {
444     // Display a tick mark on the first of a set of months of each year.
445     // Years get a tick mark iff y % year_mod == 0. This is useful for
446     // displaying a tick mark once every 10 years, say, on long time scales.
447     var months;
448     var year_mod = 1;  // e.g. to only print one point every 10 years.
449
450     if (granularity < Dygraph.NUM_GRANULARITIES) {
451       months = Dygraph.LONG_TICK_PLACEMENTS[granularity].months;
452       year_mod = Dygraph.LONG_TICK_PLACEMENTS[granularity].year_mod;
453     } else {
454       Dygraph.warn("Span of dates is too long");
455     }
456
457     var start_year = new Date(start_time).getFullYear();
458     var end_year   = new Date(end_time).getFullYear();
459     var zeropad = Dygraph.zeropad;
460     for (var i = start_year; i <= end_year; i++) {
461       if (i % year_mod !== 0) continue;
462       for (var j = 0; j < months.length; j++) {
463         var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
464         t = Dygraph.dateStrToMillis(date_str);
465         if (t < start_time || t > end_time) continue;
466         ticks.push({ v:t,
467                      label: formatter(new Date(t), granularity, opts, dg)
468                    });
469       }
470     }
471   }
472
473   return ticks;
474 };
475
476 // These are set here so that this file can be included after dygraph.js
477 // or independently.
478 if (Dygraph &&
479     Dygraph.DEFAULT_ATTRS &&
480     Dygraph.DEFAULT_ATTRS['axes'] &&
481     Dygraph.DEFAULT_ATTRS['axes']['x'] &&
482     Dygraph.DEFAULT_ATTRS['axes']['y'] &&
483     Dygraph.DEFAULT_ATTRS['axes']['y2']) {
484   Dygraph.DEFAULT_ATTRS['axes']['x']['ticker'] = Dygraph.dateTicker;
485   Dygraph.DEFAULT_ATTRS['axes']['y']['ticker'] = Dygraph.numericTicks;
486   Dygraph.DEFAULT_ATTRS['axes']['y2']['ticker'] = Dygraph.numericTicks;
487 }