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