Details | Last modification | View Log | RSS feed
| Rev | Author | Line No. | Line |
|---|---|---|---|
| 1 | espaco | 1 | /* Pretty handling of time axes. |
| 2 | |||
| 3 | Copyright (c) 2007-2014 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 | |||
| 119 | var utc = { |
||
| 120 | date: d |
||
| 121 | }; |
||
| 122 | |||
| 123 | // support strftime, if found |
||
| 124 | |||
| 125 | if (d.strftime != undefined) { |
||
| 126 | addProxyMethod(utc, "strftime", d, "strftime"); |
||
| 127 | } |
||
| 128 | |||
| 129 | addProxyMethod(utc, "getTime", d, "getTime"); |
||
| 130 | addProxyMethod(utc, "setTime", d, "setTime"); |
||
| 131 | |||
| 132 | var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; |
||
| 133 | |||
| 134 | for (var p = 0; p < props.length; p++) { |
||
| 135 | addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); |
||
| 136 | addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); |
||
| 137 | } |
||
| 138 | |||
| 139 | return utc; |
||
| 140 | }; |
||
| 141 | |||
| 142 | // select time zone strategy. This returns a date-like object tied to the |
||
| 143 | // desired timezone |
||
| 144 | |||
| 145 | function dateGenerator(ts, opts) { |
||
| 146 | if (opts.timezone == "browser") { |
||
| 147 | return new Date(ts); |
||
| 148 | } else if (!opts.timezone || opts.timezone == "utc") { |
||
| 149 | return makeUtcWrapper(new Date(ts)); |
||
| 150 | } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { |
||
| 151 | var d = new timezoneJS.Date(); |
||
| 152 | // timezone-js is fickle, so be sure to set the time zone before |
||
| 153 | // setting the time. |
||
| 154 | d.setTimezone(opts.timezone); |
||
| 155 | d.setTime(ts); |
||
| 156 | return d; |
||
| 157 | } else { |
||
| 158 | return makeUtcWrapper(new Date(ts)); |
||
| 159 | } |
||
| 160 | } |
||
| 161 | |||
| 162 | // map of app. size of time units in milliseconds |
||
| 163 | |||
| 164 | var timeUnitSize = { |
||
| 165 | "second": 1000, |
||
| 166 | "minute": 60 * 1000, |
||
| 167 | "hour": 60 * 60 * 1000, |
||
| 168 | "day": 24 * 60 * 60 * 1000, |
||
| 169 | "month": 30 * 24 * 60 * 60 * 1000, |
||
| 170 | "quarter": 3 * 30 * 24 * 60 * 60 * 1000, |
||
| 171 | "year": 365.2425 * 24 * 60 * 60 * 1000 |
||
| 172 | }; |
||
| 173 | |||
| 174 | // the allowed tick sizes, after 1 year we use |
||
| 175 | // an integer algorithm |
||
| 176 | |||
| 177 | var baseSpec = [ |
||
| 178 | [1, "second"], [2, "second"], [5, "second"], [10, "second"], |
||
| 179 | [30, "second"], |
||
| 180 | [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], |
||
| 181 | [30, "minute"], |
||
| 182 | [1, "hour"], [2, "hour"], [4, "hour"], |
||
| 183 | [8, "hour"], [12, "hour"], |
||
| 184 | [1, "day"], [2, "day"], [3, "day"], |
||
| 185 | [0.25, "month"], [0.5, "month"], [1, "month"], |
||
| 186 | [2, "month"] |
||
| 187 | ]; |
||
| 188 | |||
| 189 | // we don't know which variant(s) we'll need yet, but generating both is |
||
| 190 | // cheap |
||
| 191 | |||
| 192 | var specMonths = baseSpec.concat([[3, "month"], [6, "month"], |
||
| 193 | [1, "year"]]); |
||
| 194 | var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], |
||
| 195 | [1, "year"]]); |
||
| 196 | |||
| 197 | function init(plot) { |
||
| 198 | plot.hooks.processOptions.push(function (plot, options) { |
||
| 199 | $.each(plot.getAxes(), function(axisName, axis) { |
||
| 200 | |||
| 201 | var opts = axis.options; |
||
| 202 | |||
| 203 | if (opts.mode == "time") { |
||
| 204 | axis.tickGenerator = function(axis) { |
||
| 205 | |||
| 206 | var ticks = []; |
||
| 207 | var d = dateGenerator(axis.min, opts); |
||
| 208 | var minSize = 0; |
||
| 209 | |||
| 210 | // make quarter use a possibility if quarters are |
||
| 211 | // mentioned in either of these options |
||
| 212 | |||
| 213 | var spec = (opts.tickSize && opts.tickSize[1] === |
||
| 214 | "quarter") || |
||
| 215 | (opts.minTickSize && opts.minTickSize[1] === |
||
| 216 | "quarter") ? specQuarters : specMonths; |
||
| 217 | |||
| 218 | if (opts.minTickSize != null) { |
||
| 219 | if (typeof opts.tickSize == "number") { |
||
| 220 | minSize = opts.tickSize; |
||
| 221 | } else { |
||
| 222 | minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; |
||
| 223 | } |
||
| 224 | } |
||
| 225 | |||
| 226 | for (var i = 0; i < spec.length - 1; ++i) { |
||
| 227 | if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] |
||
| 228 | + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 |
||
| 229 | && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { |
||
| 230 | break; |
||
| 231 | } |
||
| 232 | } |
||
| 233 | |||
| 234 | var size = spec[i][0]; |
||
| 235 | var unit = spec[i][1]; |
||
| 236 | |||
| 237 | // special-case the possibility of several years |
||
| 238 | |||
| 239 | if (unit == "year") { |
||
| 240 | |||
| 241 | // if given a minTickSize in years, just use it, |
||
| 242 | // ensuring that it's an integer |
||
| 243 | |||
| 244 | if (opts.minTickSize != null && opts.minTickSize[1] == "year") { |
||
| 245 | size = Math.floor(opts.minTickSize[0]); |
||
| 246 | } else { |
||
| 247 | |||
| 248 | var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); |
||
| 249 | var norm = (axis.delta / timeUnitSize.year) / magn; |
||
| 250 | |||
| 251 | if (norm < 1.5) { |
||
| 252 | size = 1; |
||
| 253 | } else if (norm < 3) { |
||
| 254 | size = 2; |
||
| 255 | } else if (norm < 7.5) { |
||
| 256 | size = 5; |
||
| 257 | } else { |
||
| 258 | size = 10; |
||
| 259 | } |
||
| 260 | |||
| 261 | size *= magn; |
||
| 262 | } |
||
| 263 | |||
| 264 | // minimum size for years is 1 |
||
| 265 | |||
| 266 | if (size < 1) { |
||
| 267 | size = 1; |
||
| 268 | } |
||
| 269 | } |
||
| 270 | |||
| 271 | axis.tickSize = opts.tickSize || [size, unit]; |
||
| 272 | var tickSize = axis.tickSize[0]; |
||
| 273 | unit = axis.tickSize[1]; |
||
| 274 | |||
| 275 | var step = tickSize * timeUnitSize[unit]; |
||
| 276 | |||
| 277 | if (unit == "second") { |
||
| 278 | d.setSeconds(floorInBase(d.getSeconds(), tickSize)); |
||
| 279 | } else if (unit == "minute") { |
||
| 280 | d.setMinutes(floorInBase(d.getMinutes(), tickSize)); |
||
| 281 | } else if (unit == "hour") { |
||
| 282 | d.setHours(floorInBase(d.getHours(), tickSize)); |
||
| 283 | } else if (unit == "month") { |
||
| 284 | d.setMonth(floorInBase(d.getMonth(), tickSize)); |
||
| 285 | } else if (unit == "quarter") { |
||
| 286 | d.setMonth(3 * floorInBase(d.getMonth() / 3, |
||
| 287 | tickSize)); |
||
| 288 | } else if (unit == "year") { |
||
| 289 | d.setFullYear(floorInBase(d.getFullYear(), tickSize)); |
||
| 290 | } |
||
| 291 | |||
| 292 | // reset smaller components |
||
| 293 | |||
| 294 | d.setMilliseconds(0); |
||
| 295 | |||
| 296 | if (step >= timeUnitSize.minute) { |
||
| 297 | d.setSeconds(0); |
||
| 298 | } |
||
| 299 | if (step >= timeUnitSize.hour) { |
||
| 300 | d.setMinutes(0); |
||
| 301 | } |
||
| 302 | if (step >= timeUnitSize.day) { |
||
| 303 | d.setHours(0); |
||
| 304 | } |
||
| 305 | if (step >= timeUnitSize.day * 4) { |
||
| 306 | d.setDate(1); |
||
| 307 | } |
||
| 308 | if (step >= timeUnitSize.month * 2) { |
||
| 309 | d.setMonth(floorInBase(d.getMonth(), 3)); |
||
| 310 | } |
||
| 311 | if (step >= timeUnitSize.quarter * 2) { |
||
| 312 | d.setMonth(floorInBase(d.getMonth(), 6)); |
||
| 313 | } |
||
| 314 | if (step >= timeUnitSize.year) { |
||
| 315 | d.setMonth(0); |
||
| 316 | } |
||
| 317 | |||
| 318 | var carry = 0; |
||
| 319 | var v = Number.NaN; |
||
| 320 | var prev; |
||
| 321 | |||
| 322 | do { |
||
| 323 | |||
| 324 | prev = v; |
||
| 325 | v = d.getTime(); |
||
| 326 | ticks.push(v); |
||
| 327 | |||
| 328 | if (unit == "month" || unit == "quarter") { |
||
| 329 | if (tickSize < 1) { |
||
| 330 | |||
| 331 | // a bit complicated - we'll divide the |
||
| 332 | // month/quarter up but we need to take |
||
| 333 | // care of fractions so we don't end up in |
||
| 334 | // the middle of a day |
||
| 335 | |||
| 336 | d.setDate(1); |
||
| 337 | var start = d.getTime(); |
||
| 338 | d.setMonth(d.getMonth() + |
||
| 339 | (unit == "quarter" ? 3 : 1)); |
||
| 340 | var end = d.getTime(); |
||
| 341 | d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); |
||
| 342 | carry = d.getHours(); |
||
| 343 | d.setHours(0); |
||
| 344 | } else { |
||
| 345 | d.setMonth(d.getMonth() + |
||
| 346 | tickSize * (unit == "quarter" ? 3 : 1)); |
||
| 347 | } |
||
| 348 | } else if (unit == "year") { |
||
| 349 | d.setFullYear(d.getFullYear() + tickSize); |
||
| 350 | } else { |
||
| 351 | d.setTime(v + step); |
||
| 352 | } |
||
| 353 | } while (v < axis.max && v != prev); |
||
| 354 | |||
| 355 | return ticks; |
||
| 356 | }; |
||
| 357 | |||
| 358 | axis.tickFormatter = function (v, axis) { |
||
| 359 | |||
| 360 | var d = dateGenerator(v, axis.options); |
||
| 361 | |||
| 362 | // first check global format |
||
| 363 | |||
| 364 | if (opts.timeformat != null) { |
||
| 365 | return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); |
||
| 366 | } |
||
| 367 | |||
| 368 | // possibly use quarters if quarters are mentioned in |
||
| 369 | // any of these places |
||
| 370 | |||
| 371 | var useQuarters = (axis.options.tickSize && |
||
| 372 | axis.options.tickSize[1] == "quarter") || |
||
| 373 | (axis.options.minTickSize && |
||
| 374 | axis.options.minTickSize[1] == "quarter"); |
||
| 375 | |||
| 376 | var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; |
||
| 377 | var span = axis.max - axis.min; |
||
| 378 | var suffix = (opts.twelveHourClock) ? " %p" : ""; |
||
| 379 | var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; |
||
| 380 | var fmt; |
||
| 381 | |||
| 382 | if (t < timeUnitSize.minute) { |
||
| 383 | fmt = hourCode + ":%M:%S" + suffix; |
||
| 384 | } else if (t < timeUnitSize.day) { |
||
| 385 | if (span < 2 * timeUnitSize.day) { |
||
| 386 | fmt = hourCode + ":%M" + suffix; |
||
| 387 | } else { |
||
| 388 | fmt = "%b %d " + hourCode + ":%M" + suffix; |
||
| 389 | } |
||
| 390 | } else if (t < timeUnitSize.month) { |
||
| 391 | fmt = "%b %d"; |
||
| 392 | } else if ((useQuarters && t < timeUnitSize.quarter) || |
||
| 393 | (!useQuarters && t < timeUnitSize.year)) { |
||
| 394 | if (span < timeUnitSize.year) { |
||
| 395 | fmt = "%b"; |
||
| 396 | } else { |
||
| 397 | fmt = "%b %Y"; |
||
| 398 | } |
||
| 399 | } else if (useQuarters && t < timeUnitSize.year) { |
||
| 400 | if (span < timeUnitSize.year) { |
||
| 401 | fmt = "Q%q"; |
||
| 402 | } else { |
||
| 403 | fmt = "Q%q %Y"; |
||
| 404 | } |
||
| 405 | } else { |
||
| 406 | fmt = "%Y"; |
||
| 407 | } |
||
| 408 | |||
| 409 | var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); |
||
| 410 | |||
| 411 | return rt; |
||
| 412 | }; |
||
| 413 | } |
||
| 414 | }); |
||
| 415 | }); |
||
| 416 | } |
||
| 417 | |||
| 418 | $.plot.plugins.push({ |
||
| 419 | init: init, |
||
| 420 | options: options, |
||
| 421 | name: 'time', |
||
| 422 | version: '1.0' |
||
| 423 | }); |
||
| 424 | |||
| 425 | // Time-axis support used to be in Flot core, which exposed the |
||
| 426 | // formatDate function on the plot object. Various plugins depend |
||
| 427 | // on the function, so we need to re-expose it here. |
||
| 428 | |||
| 429 | $.plot.formatDate = formatDate; |
||
| 430 | $.plot.dateGenerator = dateGenerator; |
||
| 431 | |||
| 432 | })(jQuery); |