Some CSS tweaks after r166477 and r166479,
[WebKit.git] / Websites / perf.webkit.org / public / js / jquery.flot.time.js
1 /* Pretty handling of time axes.
2
3 Copyright (c) 2007-2012 IOLA and Ole Laursen.
4 Licensed under the MIT license.
5
6 Set axis.mode to "time" to enable. See the section "Time series data" in
7 API.txt for details.
8
9 */
10
11 (function($) {
12
13     var options = {};
14
15     // round to nearby lower multiple of base
16
17     function floorInBase(n, base) {
18         return base * Math.floor(n / base);
19     }
20
21     // Returns a string with the date d formatted according to fmt.
22     // A subset of the Open Group's strftime format is supported.
23
24     function formatDate(d, fmt, monthNames, dayNames) {
25
26         if (typeof d.strftime == "function") {
27             return d.strftime(fmt);
28         }
29
30         var leftPad = function(n, pad) {
31             n = "" + n;
32             pad = "" + (pad == null ? "0" : pad);
33             return n.length == 1 ? pad + n : n;
34         };
35
36         var r = [];
37         var escape = false;
38         var hours = d.getHours();
39         var isAM = hours < 12;
40
41         if (monthNames == null) {
42             monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
43         }
44
45         if (dayNames == null) {
46             dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
47         }
48
49         var hours12;
50
51         if (hours > 12) {
52             hours12 = hours - 12;
53         } else if (hours == 0) {
54             hours12 = 12;
55         } else {
56             hours12 = hours;
57         }
58
59         for (var i = 0; i < fmt.length; ++i) {
60
61             var c = fmt.charAt(i);
62
63             if (escape) {
64                 switch (c) {
65                     case 'a': c = "" + dayNames[d.getDay()]; break;
66                     case 'b': c = "" + monthNames[d.getMonth()]; break;
67                     case 'd': c = leftPad(d.getDate()); break;
68                     case 'e': c = leftPad(d.getDate(), " "); break;
69                     case 'H': c = leftPad(hours); break;
70                     case 'I': c = leftPad(hours12); break;
71                     case 'l': c = leftPad(hours12, " "); break;
72                     case 'm': c = leftPad(d.getMonth() + 1); break;
73                     case 'M': c = leftPad(d.getMinutes()); break;
74                     // quarters not in Open Group's strftime specification
75                     case 'q':
76                         c = "" + (Math.floor(d.getMonth() / 3) + 1); break;
77                     case 'S': c = leftPad(d.getSeconds()); break;
78                     case 'y': c = leftPad(d.getFullYear() % 100); break;
79                     case 'Y': c = "" + d.getFullYear(); break;
80                     case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
81                     case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
82                     case 'w': c = "" + d.getDay(); break;
83                 }
84                 r.push(c);
85                 escape = false;
86             } else {
87                 if (c == "%") {
88                     escape = true;
89                 } else {
90                     r.push(c);
91                 }
92             }
93         }
94
95         return r.join("");
96     }
97
98     // To have a consistent view of time-based data independent of which time
99     // zone the client happens to be in we need a date-like object independent
100     // of time zones.  This is done through a wrapper that only calls the UTC
101     // versions of the accessor methods.
102
103     function makeUtcWrapper(d) {
104
105         function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) {
106             sourceObj[sourceMethod] = function() {
107                 return targetObj[targetMethod].apply(targetObj, arguments);
108             };
109         };
110
111         var utc = {
112             date: d
113         };
114
115         // support strftime, if found
116
117         if (d.strftime != undefined) {
118             addProxyMethod(utc, "strftime", d, "strftime");
119         }
120
121         addProxyMethod(utc, "getTime", d, "getTime");
122         addProxyMethod(utc, "setTime", d, "setTime");
123
124         var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"];
125
126         for (var p = 0; p < props.length; p++) {
127             addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]);
128             addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]);
129         }
130
131         return utc;
132     };
133
134     // select time zone strategy.  This returns a date-like object tied to the
135     // desired timezone
136
137     function dateGenerator(ts, opts) {
138         if (opts.timezone == "browser") {
139             return new Date(ts);
140         } else if (!opts.timezone || opts.timezone == "utc") {
141             return makeUtcWrapper(new Date(ts));
142         } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") {
143             var d = new timezoneJS.Date();
144             // timezone-js is fickle, so be sure to set the time zone before
145             // setting the time.
146             d.setTimezone(opts.timezone);
147             d.setTime(ts);
148             return d;
149         } else {
150             return makeUtcWrapper(new Date(ts));
151         }
152     }
153     
154     // map of app. size of time units in milliseconds
155
156     var timeUnitSize = {
157         "second": 1000,
158         "minute": 60 * 1000,
159         "hour": 60 * 60 * 1000,
160         "day": 24 * 60 * 60 * 1000,
161         "month": 30 * 24 * 60 * 60 * 1000,
162         "quarter": 3 * 30 * 24 * 60 * 60 * 1000,
163         "year": 365.2425 * 24 * 60 * 60 * 1000
164     };
165
166     // the allowed tick sizes, after 1 year we use
167     // an integer algorithm
168
169     var baseSpec = [
170         [1, "second"], [2, "second"], [5, "second"], [10, "second"],
171         [30, "second"], 
172         [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
173         [30, "minute"], 
174         [1, "hour"], [2, "hour"], [4, "hour"],
175         [8, "hour"], [12, "hour"],
176         [1, "day"], [2, "day"], [3, "day"],
177         [0.25, "month"], [0.5, "month"], [1, "month"],
178         [2, "month"]
179     ];
180
181     // we don't know which variant(s) we'll need yet, but generating both is
182     // cheap
183
184     var specMonths = baseSpec.concat([[3, "month"], [6, "month"],
185         [1, "year"]]);
186     var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"],
187         [1, "year"]]);
188
189     function init(plot) {
190         plot.hooks.processDatapoints.push(function (plot, series, datapoints) {
191             $.each(plot.getAxes(), function(axisName, axis) {
192
193                 var opts = axis.options;
194
195                 if (opts.mode == "time") {
196                     axis.tickGenerator = function(axis) {
197
198                         var ticks = [];
199                         var d = dateGenerator(axis.min, opts);
200                         var minSize = 0;
201
202                         // make quarter use a possibility if quarters are
203                         // mentioned in either of these options
204
205                         var spec = (opts.tickSize && opts.tickSize[1] ===
206                             "quarter") ||
207                             (opts.minTickSize && opts.minTickSize[1] ===
208                             "quarter") ? specQuarters : specMonths;
209
210                         if (opts.minTickSize != null) {
211                             if (typeof opts.tickSize == "number") {
212                                 minSize = opts.tickSize;
213                             } else {
214                                 minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
215                             }
216                         }
217
218                         for (var i = 0; i < spec.length - 1; ++i) {
219                             if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]]
220                                               + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
221                                 && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) {
222                                 break;
223                             }
224                         }
225
226                         var size = spec[i][0];
227                         var unit = spec[i][1];
228
229                         // special-case the possibility of several years
230
231                         if (unit == "year") {
232
233                             // if given a minTickSize in years, just use it,
234                             // ensuring that it's an integer
235
236                             if (opts.minTickSize != null && opts.minTickSize[1] == "year") {
237                                 size = Math.floor(opts.minTickSize[0]);
238                             } else {
239
240                                 var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10));
241                                 var norm = (axis.delta / timeUnitSize.year) / magn;
242
243                                 if (norm < 1.5) {
244                                     size = 1;
245                                 } else if (norm < 3) {
246                                     size = 2;
247                                 } else if (norm < 7.5) {
248                                     size = 5;
249                                 } else {
250                                     size = 10;
251                                 }
252
253                                 size *= magn;
254                             }
255
256                             // minimum size for years is 1
257
258                             if (size < 1) {
259                                 size = 1;
260                             }
261                         }
262
263                         axis.tickSize = opts.tickSize || [size, unit];
264                         var tickSize = axis.tickSize[0];
265                         unit = axis.tickSize[1];
266
267                         var step = tickSize * timeUnitSize[unit];
268
269                         if (unit == "second") {
270                             d.setSeconds(floorInBase(d.getSeconds(), tickSize));
271                         } else if (unit == "minute") {
272                             d.setMinutes(floorInBase(d.getMinutes(), tickSize));
273                         } else if (unit == "hour") {
274                             d.setHours(floorInBase(d.getHours(), tickSize));
275                         } else if (unit == "month") {
276                             d.setMonth(floorInBase(d.getMonth(), tickSize));
277                         } else if (unit == "quarter") {
278                             d.setMonth(3 * floorInBase(d.getMonth() / 3,
279                                 tickSize));
280                         } else if (unit == "year") {
281                             d.setFullYear(floorInBase(d.getFullYear(), tickSize));
282                         }
283
284                         // reset smaller components
285
286                         d.setMilliseconds(0);
287
288                         if (step >= timeUnitSize.minute) {
289                             d.setSeconds(0);
290                         } else if (step >= timeUnitSize.hour) {
291                             d.setMinutes(0);
292                         } else if (step >= timeUnitSize.day) {
293                             d.setHours(0);
294                         } else if (step >= timeUnitSize.day * 4) {
295                             d.setDate(1);
296                         } else if (step >= timeUnitSize.month * 2) {
297                             d.setMonth(floorInBase(d.getMonth(), 3));
298                         } else if (step >= timeUnitSize.quarter * 2) {
299                             d.setMonth(floorInBase(d.getMonth(), 6));
300                         } else if (step >= timeUnitSize.year) {
301                             d.setMonth(0);
302                         }
303
304                         var carry = 0;
305                         var v = Number.NaN;
306                         var prev;
307
308                         do {
309
310                             prev = v;
311                             v = d.getTime();
312                             ticks.push(v);
313
314                             if (unit == "month" || unit == "quarter") {
315                                 if (tickSize < 1) {
316
317                                     // a bit complicated - we'll divide the
318                                     // month/quarter up but we need to take
319                                     // care of fractions so we don't end up in
320                                     // the middle of a day
321
322                                     d.setDate(1);
323                                     var start = d.getTime();
324                                     d.setMonth(d.getMonth() +
325                                         (unit == "quarter" ? 3 : 1));
326                                     var end = d.getTime();
327                                     d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
328                                     carry = d.getHours();
329                                     d.setHours(0);
330                                 } else {
331                                     d.setMonth(d.getMonth() +
332                                         tickSize * (unit == "quarter" ? 3 : 1));
333                                 }
334                             } else if (unit == "year") {
335                                 d.setFullYear(d.getFullYear() + tickSize);
336                             } else {
337                                 d.setTime(v + step);
338                             }
339                         } while (v < axis.max && v != prev);
340
341                         return ticks;
342                     };
343
344                     axis.tickFormatter = function (v, axis) {
345
346                         var d = dateGenerator(v, axis.options);
347
348                         // first check global format
349
350                         if (opts.timeformat != null) {
351                             return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames);
352                         }
353
354                         // possibly use quarters if quarters are mentioned in
355                         // any of these places
356
357                         var useQuarters = (axis.options.tickSize &&
358                                 axis.options.tickSize[1] == "quarter") ||
359                             (axis.options.minTickSize &&
360                                 axis.options.minTickSize[1] == "quarter");
361
362                         var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
363                         var span = axis.max - axis.min;
364                         var suffix = (opts.twelveHourClock) ? " %p" : "";
365                         var hourCode = (opts.twelveHourClock) ? "%I" : "%H";
366                         var fmt;
367
368                         if (t < timeUnitSize.minute) {
369                             fmt = hourCode + ":%M:%S" + suffix;
370                         } else if (t < timeUnitSize.day) {
371                             if (span < 2 * timeUnitSize.day) {
372                                 fmt = hourCode + ":%M" + suffix;
373                             } else {
374                                 fmt = "%b %d " + hourCode + ":%M" + suffix;
375                             }
376                         } else if (t < timeUnitSize.month) {
377                             fmt = "%b %d";
378                         } else if ((useQuarters && t < timeUnitSize.quarter) ||
379                             (!useQuarters && t < timeUnitSize.year)) {
380                             if (span < timeUnitSize.year) {
381                                 fmt = "%b";
382                             } else {
383                                 fmt = "%b %Y";
384                             }
385                         } else if (useQuarters && t < timeUnitSize.year) {
386                             if (span < timeUnitSize.year) {
387                                 fmt = "Q%q";
388                             } else {
389                                 fmt = "Q%q %Y";
390                             }
391                         } else {
392                             fmt = "%Y";
393                         }
394
395                         var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);
396
397                         return rt;
398                     };
399                 }
400             });
401         });
402     }
403
404     $.plot.plugins.push({
405         init: init,
406         options: options,
407         name: 'time',
408         version: '1.0'
409     });
410
411     // Time-axis support used to be in Flot core, which exposed the
412     // formatDate function on the plot object.  Various plugins depend
413     // on the function, so we need to re-expose it here.
414
415     $.plot.formatDate = formatDate;
416
417 })(jQuery);