PageRenderTime 60ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 1ms

/slippy-chart.js

https://bitbucket.org/kelvin/slippy-chart
JavaScript | 3341 lines | 2458 code | 532 blank | 351 comment | 309 complexity | 15df4ed905aea099527e72a5ccd3b5b0 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. /*!
  2. * Slippy Chart Library v0.1
  3. * http://www.scaledlabs.com
  4. *
  5. * Copyright (c) 2009 Kelvin Kakugawa
  6. *
  7. */
  8. /*jslint browser: true, debug: true, rhino: true*/
  9. /*extern jQuery*/
  10. (function($) {
  11. var
  12. window = this,
  13. document = window.document,
  14. undefined,
  15. _SlippyChart = window.SlippyChart,
  16. _$c = window.$c,
  17. $c = window.$c = SlippyChart = window.SlippyChart = {};
  18. // ========== Compatibility ==========
  19. // ---------- Event Handling ----------
  20. var addEvent = function(el, type, fn) {
  21. if (window.attachEvent) {
  22. addEvent = function(el, type, fn) {
  23. el.attachEvent("on"+type, fn);
  24. };
  25. } else {
  26. addEvent = function(el, type, fn) {
  27. if (type =="mousewheel") {
  28. type = "DOMMouseScroll";
  29. }
  30. el.addEventListener(type, fn, false);
  31. };
  32. }
  33. addEvent(el, type, fn);
  34. };
  35. ;;; $c.addEvent = addEvent;
  36. var removeEvent = function(el, type, fn) {
  37. if (window.detachEvent) {
  38. removeEvent = function(el, type, fn) {
  39. el.detachEvent("on"+type, fn);
  40. };
  41. } else {
  42. removeEvent = function(el, type, fn) {
  43. el.removeEventListener(type, fn, false);
  44. };
  45. }
  46. removeEvent(el, type, fn);
  47. };
  48. ;;; $c.removeEvent = removeEvent;
  49. var cancelEvent = function(e) {
  50. e = e ? e : window.event;
  51. if (e.stopPropagation) {
  52. e.stopPropagation();
  53. }
  54. if (e.preventDefault) {
  55. e.preventDefault();
  56. }
  57. e["cancelBubble"] = true;
  58. e["cancel"] = true;
  59. e["returnValue"] = false;
  60. return false; // note: exception for window.status
  61. };
  62. ;;; $c.cancelEvent = cancelEvent;
  63. // ========== Utilities ==========
  64. // ---------- Functional ----------
  65. var _map = function(func_, list_) {
  66. var len = list_.length || 0;
  67. var res = [];
  68. res.length = len;
  69. for (var i=len-1; i>=0; i--) {
  70. res[i] = func_(list_[i]);
  71. }
  72. return res;
  73. };
  74. ;;; $c._map = _map;
  75. var _reduce = function(func_, list_, initial_) {
  76. var len = list_.length || 0;
  77. if (len === 0) {
  78. return initial_;
  79. }
  80. var i = 0;
  81. if (typeof initial_ === "undefined") {
  82. initial_ = list_[i++];
  83. }
  84. for (; i<len; i++) {
  85. initial_ = func_(initial_, list_[i]);
  86. }
  87. return initial_;
  88. };
  89. ;;; $c._reduce = _reduce;
  90. var _filter = function(func_, list_) {
  91. var len = list_.length || 0;
  92. var res = [];
  93. for (var i=len-1; i>=0; i--) {
  94. if (func_(list_[i])) {
  95. res.push(list_[i]);
  96. }
  97. }
  98. return res.reverse();
  99. };
  100. ;;; $c._filter = _filter;
  101. var _zip = function() {
  102. var num_lists = arguments.length || 0;
  103. if (num_lists === 0) {
  104. return [];
  105. }
  106. var len = arguments[0].length || 0;
  107. var res = [];
  108. res.length = len;
  109. for (var i=len-1; i>=0; i--) {
  110. res[i] = _map(function (x) { return x[i];}, arguments);
  111. }
  112. return res;
  113. };
  114. ;;; $c._zip = _zip;
  115. // ---------- Binary Search ----------
  116. var _search = function(bucket_, key_) {
  117. var low = 0;
  118. var high = bucket_.length-1;
  119. while (low <= high) {
  120. var mid = Math.floor((low + high) / 2);
  121. var midVal = bucket_[mid];
  122. if (midVal < key_) {
  123. low = mid + 1;
  124. } else if (midVal > key_) {
  125. high = mid - 1;
  126. } else { // midVal == key_
  127. return mid;
  128. }
  129. }
  130. return -(low+1); // flag: key not found
  131. };
  132. ;;; $c._search = _search;
  133. var _range_r = function(bucket_, key_) {
  134. var low = 0;
  135. var high = bucket_.length-1;
  136. while (low <= high) {
  137. var mid = Math.floor((low + high) / 2);
  138. var midVal = bucket_[mid];
  139. if (midVal < key_) {
  140. low = mid + 1;
  141. } else if (midVal > key_) {
  142. high = mid - 1;
  143. } else { // midVal == key_
  144. return bucket_.slice(mid);
  145. }
  146. }
  147. return bucket_.slice(low);
  148. };
  149. ;;; $c._range_r = _range_r;
  150. var _range_l = function(bucket_, key_) {
  151. var low = 0;
  152. var high = bucket_.length-1;
  153. while (low <= high) {
  154. var mid = Math.floor((low + high) / 2);
  155. var midVal = bucket_[mid];
  156. if (midVal < key_) {
  157. low = mid + 1;
  158. } else if (midVal > key_) {
  159. high = mid - 1;
  160. } else { // midVal == key_
  161. return bucket_.slice(0, mid+1);
  162. }
  163. }
  164. return bucket_.slice(0, high+1);
  165. };
  166. ;;; $c._range_l = _range_l;
  167. var _range = function(bucket_, lkey_, rkey_) {
  168. var low, high, mid, midVal, lindex, rindex;
  169. low = 0;
  170. high = bucket_.length-1;
  171. while (low <= high) {
  172. mid = Math.floor((low + high) / 2);
  173. midVal = bucket_[mid];
  174. if (midVal < lkey_) {
  175. low = mid + 1;
  176. } else if (midVal > lkey_) {
  177. high = mid - 1;
  178. } else { // midval == lkey_
  179. low = mid;
  180. break;
  181. }
  182. }
  183. lindex = low;
  184. high = bucket_.length-1;
  185. while (low <= high) {
  186. mid = Math.floor((low + high) / 2);
  187. midVal = bucket_[mid];
  188. if (midVal < rkey_) {
  189. low = mid + 1;
  190. } else if (midVal > rkey_) {
  191. high = mid - 1;
  192. } else { // midval == rkey_
  193. high = mid;
  194. break;
  195. }
  196. }
  197. rindex = high + 1;
  198. return bucket_.slice(lindex, rindex);
  199. };
  200. ;;; $c._range = _range;
  201. // ---------- Linked List ----------
  202. var LinkedList = function() {
  203. this.first = null;
  204. this.last = null;
  205. this.length = 0;
  206. };
  207. ;;; $c.LinkedList = LinkedList;
  208. var LinkedListNode = function(data) {
  209. this.prev = null;
  210. this.next = null;
  211. this.data = data;
  212. };
  213. ;;; $c.LinkedListNode = LinkedListNode;
  214. LinkedList.prototype.prepend = function(node) {
  215. if (this.first === null) {
  216. this.first = node;
  217. this.last = node;
  218. } else {
  219. node.prev = null;
  220. node.next = this.first;
  221. this.first.prev = node;
  222. this.first = node;
  223. }
  224. this.length++;
  225. };
  226. LinkedList.prototype.append = function(node) {
  227. if (this.first === null) {
  228. this.first = node;
  229. this.last = node;
  230. } else {
  231. node.prev = this.last;
  232. node.next = null;
  233. this.last.next = node;
  234. this.last = node;
  235. }
  236. this.length++;
  237. };
  238. LinkedList.prototype.remove = function(node) {
  239. if (this.first == this.last) {
  240. this.first = null;
  241. this.last = null;
  242. } else {
  243. if (node.prev) {
  244. node.prev.next = node.next;
  245. }
  246. if (node.next) {
  247. node.next.prev = node.prev;
  248. }
  249. if (node == this.first) {
  250. this.first = node.next;
  251. }
  252. if (node == this.last) {
  253. this.last = node.prev;
  254. }
  255. }
  256. this.length--;
  257. node.prev = null;
  258. node.next = null;
  259. };
  260. // ---------- Mapped Linked List ----------
  261. var MappedLinkedList = function() {
  262. this.map = {};
  263. this.list = new LinkedList();
  264. };
  265. ;;; $c.MappedLinkedList = MappedLinkedList;
  266. MappedLinkedList.prototype.add = function(key, val) {
  267. if (typeof this.map[key] !== "undefined") {
  268. return; // don't re-add
  269. }
  270. var node = new LinkedListNode(val);
  271. this.map[key] = node;
  272. this.list.append(node);
  273. };
  274. MappedLinkedList.prototype.remove = function(key) {
  275. if (typeof this.map[key] === "undefined") {
  276. return;
  277. }
  278. this.list.remove(this.map[key]);
  279. delete(this.map[key]);
  280. };
  281. // ---------- Pub/Sub Broker ----------
  282. var PubSubBroker = function() {
  283. this.channels = {};
  284. };
  285. PubSubBroker.prototype = {
  286. uid_counter: 0
  287. };
  288. ;;; $c.PubSubBroker = PubSubBroker;
  289. PubSubBroker.prototype.publish = function(signal) {
  290. setTimeout((function(signal) { // make it asynch
  291. var passed = arguments;
  292. var channels = this.channels;
  293. return function() {
  294. var args = Array.prototype.slice.call(passed, 1);
  295. var node = channels[signal] && channels[signal].list.first;
  296. while (node) {
  297. var handler = node.data;
  298. setTimeout((function(handler, args) { // make it asynch
  299. return function() {
  300. handler.apply(this, args);
  301. };
  302. }).call(this, handler, args), 0);
  303. node = node.next;
  304. }
  305. };
  306. }).apply(this, arguments), 0);
  307. };
  308. PubSubBroker.prototype.subscribe = function(signal, scope, handlerName){
  309. if (typeof this.channels[signal] === "undefined") {
  310. this.channels[signal] = new MappedLinkedList();
  311. }
  312. var curryArray = Array.prototype.slice.call(arguments, 3);
  313. if (typeof scope._psb_uid == "undefined") {
  314. scope._psb_uid = ++this.uid_counter;
  315. }
  316. var key = scope._psb_uid + ":" + handlerName + ":" + curryArray;
  317. this.channels[signal].add(key, function(){
  318. var normalizedArgs = Array.prototype.slice.call(arguments, 0);
  319. scope[handlerName].apply((scope || window), curryArray.concat(normalizedArgs));
  320. });
  321. };
  322. PubSubBroker.prototype.unsubscribe = function(signal, scope, handlerName) {
  323. if (typeof this.channels[signal] === "undefined") {
  324. return; // throw warn?
  325. }
  326. var curryArray = Array.prototype.slice.call(arguments, 3);
  327. var key = scope._psb_uid + ":" + handlerName + ":" + curryArray;
  328. this.channels[signal].remove(key);
  329. };
  330. // ========== Canvas Utilities ==========
  331. // ---------- Context Helpers ----------
  332. function decorate_context(context_) {
  333. context_._calc = function(pos_) {
  334. return context_.lineWidth % 2 ? Math.floor(pos_) + 0.5 : Math.floor(pos_);
  335. };
  336. context_._moveTo = function(x_, y_) {
  337. context_.moveTo(context_._calc(x_), context_._calc(y_));
  338. };
  339. context_._lineTo = function(x_, y_) {
  340. context_.lineTo(context_._calc(x_), context_._calc(y_));
  341. };
  342. }
  343. // ========== Chart ==========
  344. // ---------- LineDataCache ----------
  345. //TODO: consider: auto-generating the zoom schema
  346. var _ZOOM_SCHEMA_HALF = [
  347. // [delta, low period, high period]
  348. [60, 1, 15],
  349. [60*5, 3, 12],
  350. [60*30, 2, 12],
  351. [60*60*3, 2, 16],
  352. [60*60*12, 4, 16]
  353. ];
  354. var _ZOOM_SCHEMA_RELAXED = [
  355. // [delta, low period, high period]
  356. [60, 1, 20],
  357. [60*5, 4, 18],
  358. [60*30, 3, 18],
  359. [60*60*3, 3, 20],
  360. [60*60*12, 5, 31]
  361. ];
  362. var _ZOOM_SCHEMA_STANDARD = [
  363. // [delta, low period, high period]
  364. [60, 1, 30],
  365. [60*5, 6, 30],
  366. [60*30, 5, 30],
  367. [60*60*3, 5, 32],
  368. [60*60*12, 8, 31]
  369. ];
  370. var _ZOOM_SCHEMA_AGGRESSIVE = [
  371. // [delta, low period, high period]
  372. [60, 1, 40],
  373. [60*5, 8, 42],
  374. [60*30, 7, 42],
  375. [60*60*3, 7, 40],
  376. [60*60*12, 10, 31]
  377. ];
  378. var _ZOOM_SCHEMA_INSANE = [
  379. // [delta, low period, high period]
  380. [60, 1, 60],
  381. [60*5, 12, 60],
  382. [60*30, 10, 60],
  383. [60*60*3, 10, 60],
  384. [60*60*12, 15, 31]
  385. ];
  386. var _ZOOM_SCHEMA_MAX = [
  387. // [delta, low period, high period]
  388. [60, 1, 120],
  389. [60*5, 24, 120],
  390. [60*30, 20, 120],
  391. [60*60*3, 20, 120],
  392. [60*60*12, 30, 31]
  393. ];
  394. /**
  395. * Data Cache for a Line
  396. * a cache for each zoom scale [different granularity of time data]
  397. * w/in each zoom scale cache, the point interval is the time delta at that zoom scale
  398. * @class
  399. */
  400. var LineDataCache = function(model_) {
  401. // Initialize
  402. var
  403. model = model_,
  404. min_time = 0,
  405. max_time = 0,
  406. bucket_size = 0,
  407. min_buckets = 0,
  408. max_buckets = 0,
  409. bucket_ttl = 0,
  410. cache = [], // at zoom scale
  411. proxy_bucket = [];
  412. this.model = function() { return model; };
  413. this.min_time = function() { return min_time; };
  414. this.max_time = function() { return max_time; };
  415. this.cache = function() { return cache; };
  416. /* Broker Channels:
  417. /cache/flush flush expired buckets
  418. */
  419. // Setup
  420. function init(min_time_, max_time_) {
  421. min_time = min_time_;
  422. max_time = max_time_;
  423. bucket_size = LineDataCache.prototype.bucket_size;
  424. min_buckets = LineDataCache.prototype.min_buckets;
  425. max_buckets = LineDataCache.prototype.max_buckets;
  426. bucket_ttl = LineDataCache.prototype.bucket_ttl;
  427. proxy_bucket.length = bucket_size;
  428. for (var i=0; i<model.zoom_schema().length; i++) {
  429. cache[i] = {};
  430. }
  431. //FIXME: pull these for real
  432. //XXX: bucket: 0; increment: 60
  433. for (var i=0; i<4; i++) {
  434. var bucket = [];
  435. bucket.length = bucket_size; //XXX: set length to get undefined from slice
  436. var range = 70-i*10;
  437. for (var j=0; j<bucket_size; j++) {
  438. if (i==2) {
  439. bucket[j] = 50;
  440. } else {
  441. bucket[j] = range+Math.random()*30;
  442. }
  443. }
  444. cache[0][i] = bucket;
  445. }
  446. var zoom_schema = model.zoom_schema();
  447. if (zoom_schema.length >= 2) {
  448. //XXX: bucket: 1; increment: 60*5
  449. __sum = function(x,y) { return (x || 0) + (y || 0); };
  450. _sum = function(list_) { return _reduce(__sum, list_, 0); };
  451. cache[1][0] = [];
  452. cache[1][0].length = bucket_size;
  453. var k=0;
  454. for (i=0; i<4; i++) {
  455. for (j=0; j<bucket_size; j+=5) {
  456. cache[1][0][k++] = _sum(cache[0][i].slice(j,j+5)) / 5;
  457. }
  458. }
  459. var len = k;
  460. }
  461. if (zoom_schema.length >= 3) {
  462. //XXX: bucket: 2; increment: 60*30
  463. cache[2][0] = [];
  464. cache[2][0].length = bucket_size;
  465. k=0;
  466. for (i=0; i<len; i+=6) {
  467. cache[2][0][k++] = _sum(cache[1][0].slice(i,i+6)) / 6;
  468. }
  469. len = k;
  470. }
  471. if (zoom_schema.length >= 4) {
  472. //XXX: bucket: 3; increment: 60*60*3
  473. cache[3][0] = [];
  474. cache[3][0].length = bucket_size;
  475. k=0;
  476. for (i=0; i<len; i+=6) {
  477. cache[3][0][k++] = _sum(cache[2][0].slice(i,i+6)) / 6;
  478. }
  479. len = k;
  480. }
  481. if (zoom_schema.length >= 5) {
  482. //XXX: bucket: 4; increment: 60*60*12
  483. cache[4][0] = [];
  484. cache[4][0].length = bucket_size;
  485. k=0;
  486. for (i=0; i<len; i+=4) {
  487. cache[4][0][k++] = _sum(cache[3][0].slice(i,i+4)) / 4;
  488. }
  489. }
  490. // Register for Upkeep
  491. var broker = model.broker();
  492. broker.subscribe("/cache/flush", this, "_flush");
  493. }
  494. this.init = init;
  495. // Data
  496. /**
  497. * @function
  498. * @param {int} zoom_scale_ cache data level to use
  499. * @param {int} start_offset_ first point in range
  500. * @param {int} end_offset_ last point in range
  501. * @returns cache[zoom_scale_][start_offset_:end_offset_+1]
  502. */
  503. function fetch(zoom_scale_, start_offset_, end_offset_) {
  504. var _data = cache[zoom_scale_];
  505. var start_idx = Math.floor(start_offset_/bucket_size);
  506. var end_idx = Math.floor(end_offset_/bucket_size);
  507. //XXX: avoid % (IE6 is slow)
  508. start_offset_ -= start_idx * bucket_size;
  509. end_offset_ -= end_idx * bucket_size;
  510. end_offset_ += 1; //XXX: compensate for exclusive
  511. if (start_idx === end_idx) {
  512. var _bucket = _data[start_idx];
  513. if (!_bucket) {
  514. _bucket = proxy_bucket;
  515. //TODO: fetch bucket from remote data source
  516. }
  517. return _bucket.slice(start_offset_, end_offset_);
  518. }
  519. var _start_bucket = _data[start_idx];
  520. if (!_start_bucket) {
  521. _start_bucket = proxy_bucket;
  522. //TODO: fetch bucket from remote data source
  523. }
  524. var _end_bucket = _data[end_idx];
  525. if (!_end_bucket) {
  526. _end_bucket = proxy_bucket;
  527. //TODO: fetch bucket from remote data source
  528. }
  529. var start_range = _start_bucket.slice(start_offset_, bucket_size);
  530. var end_range = _end_bucket.slice(0, end_offset_);
  531. return start_range.concat(end_range);
  532. }
  533. this.fetch = fetch;
  534. // Upkeep
  535. function _flush() {
  536. //TODO: flush expired buckets
  537. }
  538. this._flush = _flush;
  539. };
  540. //TODO: implement bucket caching
  541. //TODO: implement cache using a MappedLinkedList
  542. LineDataCache.prototype = {
  543. bucket_size: 60*24, // # of time points (depending on zoom scale)
  544. min_buckets: 10, // # of buckets to keep, regardless of TTL
  545. max_buckets: 100, // # of buckets to keep, at any time
  546. bucket_ttl: 60*1000 // 1m
  547. };
  548. // ---------- Model ----------
  549. var Model = function(parent_, zoom_schema_) {
  550. // Initialize
  551. var
  552. parent = parent_,
  553. zoom_schema = zoom_schema_,
  554. broker = parent.broker(),
  555. min_time = 0,
  556. max_time = 0,
  557. _time_range = 0,
  558. data = [],
  559. upkeep_interval = Model.prototype.upkeep_interval;
  560. this.parent = function() { return parent; };
  561. this.zoom_schema = function() { return zoom_schema; };
  562. this.zoom_level = function() { return zoom_schema[parent.zoom_scale()]; };
  563. this.zoom_delta = function() { return zoom_schema[parent.zoom_scale()][0]; };
  564. this.broker = function() { return broker; };
  565. this.min_time = function() { return min_time; };
  566. this.max_time = function() { return max_time; };
  567. this._time_range = function() { return _time_range; };
  568. this.data = function() { return data; };
  569. // Setup
  570. function init(min_time_, max_time_, chart_width_, pane_width_) {
  571. min_time = min_time_;
  572. max_time = max_time_;
  573. _time_range = max_time - min_time;
  574. // calculate: max zoom level and period
  575. var _num_panes = chart_width_ / pane_width_;
  576. for (var i=0; i<zoom_schema.length; i++) {
  577. var _level = zoom_schema[i];
  578. if (_time_range < (_level[0]*chart_width_)) {
  579. zoom_schema.length = i+1;
  580. _max_points = Math.floor(_time_range / _level[0]);
  581. zoom_schema[i][2] = Math.ceil(_max_points / _num_panes);
  582. break;
  583. }
  584. }
  585. //FIXME: create this for real
  586. var line_cache = new LineDataCache(this);
  587. line_cache.init(min_time, max_time);
  588. data[0] = line_cache;
  589. // Upkeep Thread
  590. setTimeout(_upkeep, upkeep_interval);
  591. }
  592. this.init = init;
  593. // Data
  594. function fetch(zoom_scale_, start_offset_, period_) {
  595. var _end_offset = start_offset_ + period_;
  596. var section = [];
  597. for (var i=data.length-1; i>=0; i--) {
  598. section.push(data[i].fetch(zoom_scale_, start_offset_, _end_offset));
  599. }
  600. section.reverse(); //XXX: think: is this necessary? consider how to identify lines
  601. return section;
  602. }
  603. this.fetch = fetch;
  604. // Upkeep
  605. function _upkeep() {
  606. broker.publish("/cache/flush");
  607. setTimeout(_upkeep, upkeep_interval);
  608. }
  609. };
  610. Model.prototype = {
  611. //TODO: make zoom_schema private (potentially, multiple charts)
  612. upkeep_interval: 60*1000 // background upkeep thread interval
  613. };
  614. // ---------- Pane ----------
  615. var PLOT_LINE_WIDTH = 1.25;
  616. if ($.browser.msie) { //XXX: hack: IE6
  617. PLOT_LINE_WIDTH = 1.6;
  618. }
  619. var BOTTOM_TAPE_HEIGHT = 16;
  620. var BOTTOM_TAPE_TOP_ADJUST = 0;
  621. //var BOTTOM_TAPE_COLOR = "rgb(147,190,255)";
  622. //var BOTTOM_TAPE_COLOR = "rgb(198,228,255)";
  623. var BOTTOM_TAPE_COLOR = "rgb(194,218,241)";
  624. if ($.browser.msie) { //XXX: hack: IE6
  625. BOTTOM_TAPE_TOP_ADJUST = -1;
  626. }
  627. var TAPE_MARKER_HEIGHT = 5;
  628. var Pane = function(parent_, width_) {
  629. // Initialize
  630. var
  631. parent = parent_,
  632. model = parent.model(),
  633. time_offset = 0,
  634. _data_offset = 0,
  635. width = width_,
  636. _half_width = Math.floor(width/2),
  637. draw_height = 0,
  638. chart_height = 0,
  639. container = null,
  640. _style = null,
  641. canvas = null,
  642. c = null,
  643. tape = null,
  644. _data = [],
  645. _min_y = 0,
  646. _max_y = 0;
  647. this.parent = function() { return parent; };
  648. this.time_offset = function() { return time_offset; };
  649. this._data_offset = function() { return _data_offset; };
  650. this.container = function() { return container; };
  651. this.canvas = function() { return canvas; };
  652. this.offset = function() { return parseInt(_style.left, 10); };
  653. this.c = function() { return c; };
  654. this.tape = function() { return tape; };
  655. this._data = function() { return _data; };
  656. this._min_y = function() { return _min_y; };
  657. this._max_y = function() { return _max_y; };
  658. /* Broker Channels:
  659. /pane/translate translate pane by delta
  660. /pane/redraw redraw pane
  661. */
  662. // Setup
  663. function init() {
  664. // Setup Variables
  665. draw_height = parent.draw_height();
  666. chart_height = parent.chart_height();
  667. // Container
  668. container = $([
  669. '<div',
  670. 'style="',
  671. 'position: absolute;',
  672. 'left: 0px;',
  673. 'top: 0px;',
  674. 'z-index: 0;',
  675. 'width: '+width+'px;',
  676. 'height: '+draw_height+'px;',
  677. '"></div>'].join(' ')).appendTo(parent.container());
  678. _style = container[0].style;
  679. // Canvas
  680. //XXX: build manually (excanvas r3 workaround)
  681. canvas = document.createElement("canvas");
  682. canvas.width = width;
  683. canvas.height = chart_height;
  684. var _canvas = $(canvas);
  685. _canvas.css("position", "absolute");
  686. _canvas.css("left", "0px");
  687. _canvas.css("top", "0px");
  688. _canvas.css("z-index", "0");
  689. _canvas.css("width", width+"px");
  690. _canvas.css("height", chart_height+"px");
  691. _canvas.appendTo(container);
  692. if ($.browser.msie) { //XXX: hack: excanvas
  693. canvas = window.G_vmlCanvasManager.initElement(canvas);
  694. }
  695. c = canvas.getContext("2d");
  696. decorate_context(c);
  697. if ($.browser.msie && c.setTransform) {
  698. //XXX: exception: IE6 renders more smoothly w/ setTransform
  699. c.setTransform(1, 0, 0, -1, 0, chart_height);
  700. }
  701. // Tape
  702. tape = $([
  703. '<div',
  704. 'style="',
  705. 'position: absolute;',
  706. 'left: 0px;',
  707. 'top: '+chart_height+'px;',
  708. 'z-index: 0;',
  709. 'width: '+width+'px;',
  710. 'height: '+(draw_height-chart_height)+'px;',
  711. 'text-align: left;',
  712. '"></div>'].join(' ')).appendTo(container);
  713. // Broker Subscriptions
  714. var broker = parent.broker();
  715. broker.subscribe("/pane/translate", this, "translate");
  716. broker.subscribe("/pane/redraw", this, "_draw");
  717. //XXX: needs reset()
  718. }
  719. this.init = init;
  720. // Translate Methods
  721. function translate(delta_) {
  722. _style.left = (parseInt(_style.left, 10)+delta_)+"px";
  723. }
  724. this.translate = translate;
  725. // Draw Methods
  726. function __grid_x(x_tick_) {
  727. if ($.browser.msie && c.setTransform) {
  728. //XXX: exception: IE6 renders more smoothly w/ setTransform
  729. __grid_x = function(x_tick_) {
  730. c.lineWidth = 1;
  731. // Vertical Line at 0
  732. c.beginPath();
  733. c.strokeStyle = "rgb(200,200,200)";
  734. c._moveTo(x_tick_, 0);
  735. c._lineTo(x_tick_, chart_height);
  736. c.stroke();
  737. c.beginPath();
  738. c.strokeStyle = "rgb(100,100,100)";
  739. c._moveTo(x_tick_, 0);
  740. c._lineTo(x_tick_, TAPE_MARKER_HEIGHT);
  741. c.stroke();
  742. };
  743. } else { //XXX: Safari,FF2 don't support setTransform; FF3 renders faster
  744. __grid_x = function(x_tick_) {
  745. c.lineWidth = 1;
  746. // Vertical Line at 0
  747. c.beginPath();
  748. c.strokeStyle = "rgb(200,200,200)";
  749. c._moveTo(x_tick_, 0);
  750. c._lineTo(x_tick_, chart_height);
  751. c.stroke();
  752. c.beginPath();
  753. c.strokeStyle = "rgb(100,100,100)";
  754. c._moveTo(x_tick_, chart_height);
  755. var _tape_y = chart_height-TAPE_MARKER_HEIGHT;
  756. c._lineTo(x_tick_, _tape_y);
  757. c.stroke();
  758. };
  759. }
  760. return __grid_x(x_tick_);
  761. }
  762. this.__grid_x = __grid_x;
  763. function __grid_y(y_intervals_) {
  764. if ($.browser.msie && c.setTransform) {
  765. //XXX: exception: IE6 renders more smoothly w/ setTransform
  766. __grid_y = function(y_intervals_) {
  767. c.lineWidth = 1;
  768. // Horizontal Lines
  769. c.strokeStyle = "rgb(200,200,200)";
  770. //XXX: consider Duff's device
  771. for (var i=y_intervals_.length-1; i>=0; i--) {
  772. c.beginPath();
  773. var y = y_intervals_[i];
  774. c._moveTo(0, y);
  775. c._lineTo(width, y);
  776. c.stroke();
  777. }
  778. };
  779. } else { //XXX: Safari,FF2 don't support setTransform; FF3 renders faster
  780. __grid_y = function(y_intervals_) {
  781. c.lineWidth = 1;
  782. // Horizontal Lines
  783. c.strokeStyle = "rgb(200,200,200)";
  784. //XXX: consider Duff's device
  785. for (var i=y_intervals_.length-1; i>=0; i--) {
  786. c.beginPath();
  787. var _y = chart_height - y_intervals_[i];
  788. c._moveTo(0, _y);
  789. c._lineTo(width, _y);
  790. c.stroke();
  791. }
  792. };
  793. }
  794. return __grid_y(y_intervals_);
  795. }
  796. this.__grid_y = __grid_y;
  797. function __plot(x_scale_, y_scale_, points_) {
  798. if ($.browser.msie && c.setTransform) {
  799. //XXX: exception: IE6 renders more smoothly w/ setTransform
  800. __plot = function(x_scale_, y_scale_, points_) {
  801. c.lineWidth = PLOT_LINE_WIDTH;
  802. c.strokeStyle = "rgb(25,106,227)";
  803. c.lineJoin = "miter"; //XXX: don't use round (prob w/ seams)
  804. c.beginPath();
  805. // first point @ 0
  806. //XXX: fix seams: account for idx: -1
  807. c._moveTo(-1*x_scale_, (points_[-1] || 0)*y_scale_);
  808. c._lineTo(0, (points_[0] || 0)*y_scale_);
  809. //XXX: faster cleaner modified duff's device
  810. //XXX: ref: http://home.earthlink.net/~kendrasg/info/js_opt/jsOptMain.html
  811. var i = 1;
  812. var iter = points_.length-2;
  813. var n = iter % 8;
  814. while (n--) {
  815. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  816. }
  817. n = parseInt(iter/8, 10);
  818. while (n--) {
  819. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  820. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  821. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  822. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  823. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  824. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  825. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  826. c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
  827. }
  828. // last point @ width
  829. c._lineTo(width, (points_[points_.length-1] || 0)*y_scale_);
  830. c._lineTo(width+1, (points_[points_.length-1] || 0)*y_scale_);
  831. c.stroke();
  832. c._lineTo(width+1, 0);
  833. //XXX: fix seams: account for idx: -1
  834. c._lineTo(-1*x_scale_, 0);
  835. c.fillStyle = "rgba(183,220,255, 0.4)";
  836. c.fill();
  837. };
  838. } else { //XXX: Safari,FF2 don't support setTransform; FF3 renders faster
  839. __plot = function(x_scale_, y_scale_, points_) {
  840. c.lineWidth = PLOT_LINE_WIDTH;
  841. c.strokeStyle = "rgb(25,106,227)";
  842. c.lineJoin = "miter"; //XXX: don't use round (prob w/ seams)
  843. c.beginPath();
  844. // first point @ 0
  845. //XXX: fix seams: account for idx: -1
  846. c._moveTo(-1*x_scale_, chart_height - (points_[-1] || 0)*y_scale_);
  847. c._lineTo(0, chart_height - (points_[0] || 0)*y_scale_);
  848. //XXX: faster cleaner modified duff's device
  849. //XXX: ref: http://home.earthlink.net/~kendrasg/info/js_opt/jsOptMain.html
  850. var i = 1;
  851. var iter = points_.length-2;
  852. var n = iter % 8;
  853. while (n--) {
  854. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  855. }
  856. n = parseInt(iter/8, 10);
  857. while (n--) {
  858. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  859. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  860. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  861. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  862. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  863. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  864. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  865. c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
  866. }
  867. // last point @ width
  868. c._lineTo(width, chart_height - (points_[points_.length-1] || 0)*y_scale_);
  869. c._lineTo(width+1, chart_height - (points_[points_.length-1] || 0)*y_scale_);
  870. c.stroke();
  871. c.fillStyle = "rgba(183,220,255, 0.4)";
  872. c._lineTo(width+1, chart_height);
  873. //XXX: fix seams: account for idx: -1
  874. c._lineTo(-1*x_scale_, chart_height);
  875. c.fill();
  876. };
  877. }
  878. return __plot(x_scale_, y_scale_, points_);
  879. }
  880. this.__plot = __plot;
  881. function __tape_marker(x_tick_, x_text_) {
  882. //TODO: consider: color code tape?
  883. // Set Text
  884. tape.append([
  885. '<div',
  886. 'class="tape-tick"',
  887. 'style="',
  888. 'position: absolute;',
  889. 'left: '+x_tick_+'px;',
  890. 'top: 0px;',
  891. 'z-index: 0;',
  892. 'width: '+width+'px;',
  893. '">'+x_text_+'</div>'].join(' '));
  894. }
  895. this.__tape_marker = __tape_marker;
  896. function _fetch_data() {
  897. //XXX: fix seams: grab idx: -1
  898. _data = model.fetch(
  899. parent.zoom_scale(),
  900. _data_offset-1,
  901. parent._pane_period()+1);
  902. // save min/max-y
  903. if (_data.length === 0) {
  904. _min_y = 0;
  905. _max_y = 0;
  906. return;
  907. }
  908. var i,j, _source;
  909. //XXX: fix seams: put idx: -1 at [-1]
  910. for (i=_data.length-1; i>=0; i--) {
  911. _source = _data[i];
  912. _source[-1] = _source.shift();
  913. }
  914. _min_y = _max_y = _data[0][0] || 0;
  915. for (i=_data.length-1; i>=0; i--) {
  916. _source = _data[i];
  917. for (j=_source.length-1; j>=0; j--) {
  918. var _y = _source[j] || 0;
  919. _min_y = Math.min(_min_y, _y);
  920. _max_y = Math.max(_max_y, _y);
  921. }
  922. }
  923. }
  924. function _draw() {
  925. // reset canvas
  926. c.clearRect(0, 0, width, draw_height);
  927. // reset tape
  928. tape.empty();
  929. // determine: scale
  930. var x_scale = parent._x_scale();
  931. var y_scale = parent._y_scale();
  932. // draw grid
  933. var _x_data = parent._x_tick()(time_offset);
  934. var _x_tick = (_x_data[0] / model.zoom_delta()) * x_scale;
  935. if (_x_tick < width) {
  936. __grid_x(_x_tick);
  937. // draw tape
  938. __tape_marker(_x_tick, _x_data[1]);
  939. }
  940. __grid_y(parent._y_intervals());
  941. // draw chart
  942. for (var i=_data.length-1;i>=0;i--) {
  943. __plot(x_scale, y_scale, _data[i]);
  944. }
  945. }
  946. this._draw = _draw;
  947. function _reset(data_offset_, offset_) {
  948. _data_offset = data_offset_;
  949. _style.left = offset_ + "px";
  950. time_offset = model.min_time() + _data_offset * model.zoom_delta();
  951. _fetch_data();
  952. }
  953. this._reset = _reset;
  954. function reset(data_offset_, offset_) {
  955. _reset(data_offset_, offset_);
  956. _draw();
  957. }
  958. this.reset = reset;
  959. function range_min_max_y(start_, end_) {
  960. if (_data.length === 0) {
  961. return [0, 0];
  962. }
  963. var min, max;
  964. min = max = _data[0][start_] || 0;
  965. for (var i=_data.length-1; i>=0; i--) {
  966. var _source = _data[i].slice(start_, end_);
  967. for (var j=_source.length-1; j>=0; j--) {
  968. var _y = _source[j] || 0;
  969. min = Math.min(min, _y);
  970. max = Math.max(max, _y);
  971. }
  972. }
  973. return [min, max];
  974. }
  975. this.range_min_max_y = range_min_max_y;
  976. };
  977. // ---------- Navigation Pane ----------
  978. NAV_HEIGHT = 75;
  979. NAV_BUTTON_HEIGHT = 13;
  980. NAV_BUTTON_WIDTH = 16;
  981. NAV_SCROLL_RATE = 1000 * 0.1;
  982. NAV_SCROLL_SPEED = 50;
  983. NAV_SCROLL_MIN_WIDTH = 20;
  984. NAV_SCROLL_MID_THRESHOLD = 60;
  985. NAV_OVERVIEW_TOP_OFFSET = 5;
  986. NAV_OVERVIEW_SCALE_FACTOR = 1.1;
  987. NAV_RANGE_OFFSET = -9;
  988. NAV_RANGE_WIDTH = 13;
  989. NAV_RANGE_CENTER_THRESHOLD = 13;
  990. NAV_RANGE_CENTER_OFFSET = -8;
  991. var NavigationPane = function(parent_) {
  992. // Initialize
  993. var
  994. parent = parent_,
  995. model = parent.model(),
  996. broker = parent.broker(),
  997. width = 0,
  998. height = 0,
  999. container = null,
  1000. nav_left = null,
  1001. nav_right = null,
  1002. nav_bar = null,
  1003. zoom_level = null,
  1004. overview_left = 0,
  1005. overview = null,
  1006. oc = null,
  1007. preview = null,
  1008. _preview = null,
  1009. pc = null,
  1010. _data = [],
  1011. _x_scale = 0,
  1012. _y_scale = 0,
  1013. viewer = null,
  1014. _viewer = null,
  1015. viewer_width = 0,
  1016. slider = null,
  1017. _slider = null,
  1018. slider_middle = null,
  1019. drag_modifier = 1,
  1020. range_min = null,
  1021. _range_min = null,
  1022. range_max = null,
  1023. _range_max = null,
  1024. __is_hover__ = false,
  1025. __is_drag_min__ = false;
  1026. this.parent = function() { return parent; };
  1027. this.model = function() { return model; };
  1028. this.width = function() { return width; };
  1029. this.height = function() { return height; };
  1030. this.container = function() { return container; };
  1031. this.zoom_level = function() { return zoom_level; };
  1032. this.overview_left = function() { return overview_left; };
  1033. this.overview = function() { return overview; };
  1034. this.oc = function() { return oc; };
  1035. this.preview = function() { return preview; };
  1036. this._preview = function() { return _preview; };
  1037. this.pc = function() { return pc; };
  1038. this._data = function() { return _data; };
  1039. this._x_scale = function() { return _x_scale; };
  1040. this._y_scale = function() { return _y_scale; };
  1041. this.viewer = function() { return viewer; };
  1042. this._viewer = function() { return _viewer; };
  1043. this.viewer_width = function() { return viewer_width; };
  1044. this.slider = function() { return slider; };
  1045. this._slider = function() { return _slider; };
  1046. this.slider_middle = function() { return slider_middle; };
  1047. this.drag_modifier = function() { return drag_modifier; };
  1048. this.range_min = function() { return range_min; };
  1049. this._range_min = function() { return _range_min; };
  1050. this.range_max = function() { return range_max; };
  1051. this._range_max = function() { return _range_max; };
  1052. /* Broker Channels:
  1053. /nav/center (re-)center nav
  1054. /nav/translate translate nav
  1055. */
  1056. // Helper Methods
  1057. function _render_overview(context_, strokeStyle_, fillStyle_, boxStrokeStyle_) {
  1058. // reset canvas
  1059. context_.clearRect(0, 0, overview.width, overview.height);
  1060. context_.lineWidth = 1;
  1061. context_.strokeStyle = strokeStyle_;
  1062. context_.lineJoin = "round";
  1063. context_.beginPath();
  1064. // last point @ width
  1065. context_._moveTo(overview.width, overview.height - (_data[_data.length-2] || 0) * _y_scale);
  1066. // draw overview
  1067. for (var i=_data.length-3; i>0; i--) {
  1068. context_._lineTo(i*_x_scale, overview.height - (_data[i] || 0) * _y_scale);
  1069. }
  1070. // first point @ 0
  1071. context_._lineTo(
  1072. 0,
  1073. overview.height - (_data[0] || 0) * _y_scale);
  1074. context_.stroke();
  1075. // fill overview
  1076. context_.fillStyle = fillStyle_;
  1077. context_._lineTo(0, overview.height);
  1078. context_._lineTo(overview.width, overview.height);
  1079. context_.fill();
  1080. // draw outline
  1081. context_.lineWidth = 1;
  1082. context_.strokeStyle = boxStrokeStyle_;
  1083. context_.lineJoin = "round";
  1084. context_.beginPath();
  1085. context_.moveTo(0, overview.height);
  1086. context_.lineTo(0, 0);
  1087. context_.lineTo(overview.width, 0);
  1088. context_.lineTo(overview.width, overview.height);
  1089. context_.stroke();
  1090. }
  1091. // Setup
  1092. function init(target_, width_, height_, top_, left_) {
  1093. // Setup Variables
  1094. width = width_;
  1095. height = height_;
  1096. // Container
  1097. container = $([
  1098. '<div',
  1099. 'style="',
  1100. 'position: absolute;',
  1101. 'left: '+left_+'px;',
  1102. 'top: '+top_+'px;',
  1103. 'width: '+width+'px;',
  1104. 'height: '+height+'px;',
  1105. '"></div>'].join(' ')).appendTo(target_);
  1106. // Navigation Scroll Bar
  1107. nav_left = $([
  1108. '<div',
  1109. 'class="nav-left-unsel"',
  1110. 'style="',
  1111. 'position: absolute;',
  1112. 'overflow: hidden;',
  1113. 'left: '+(-BORDER_OFFSET_X)+'px;',
  1114. 'top: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
  1115. 'width: '+NAV_BUTTON_WIDTH+'px;',
  1116. 'height: '+NAV_BUTTON_HEIGHT+'px;',
  1117. '"></div>'].join(' ')).appendTo(container);
  1118. nav_right = $([
  1119. '<div',
  1120. 'class="nav-unsel nav-right-unsel"',
  1121. 'style="',
  1122. 'position: absolute;',
  1123. 'overflow: hidden;',
  1124. 'left: '+(-BORDER_OFFSET_X+NAV_BUTTON_WIDTH)+'px;',
  1125. 'top: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
  1126. 'width: '+NAV_BUTTON_WIDTH+'px;',
  1127. 'height: '+NAV_BUTTON_HEIGHT+'px;',
  1128. '"></div>'].join(' ')).appendTo(container);
  1129. nav_bar = $([
  1130. '<div',
  1131. 'style="',
  1132. 'position: absolute;',
  1133. 'left: '+(BORDER_OFFSET_X+2*NAV_BUTTON_WIDTH)+'px;',
  1134. 'top: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
  1135. 'width: '+(width-BORDER_OFFSET_X-2*NAV_BUTTON_WIDTH)+'px;',
  1136. 'height: '+(NAV_BUTTON_HEIGHT)+'px;',
  1137. 'font-size: 0px;', // hack: IE6 fix
  1138. 'border-top: 1px solid #aaa;',
  1139. 'border-right: 1px solid #aaa;',
  1140. 'border-bottom: 1px solid #aaa;',
  1141. '"></div>'].join(' ')).appendTo(container);
  1142. // Overview
  1143. //XXX: build manually (excanvas r3 workaround)
  1144. overview = document.createElement("canvas");
  1145. overview.width = width-2*NAV_BUTTON_WIDTH;
  1146. overview.height = NAV_HEIGHT-NAV_BUTTON_HEIGHT-NAV_OVERVIEW_TOP_OFFSET;
  1147. overview_left = BORDER_OFFSET_X+2*NAV_BUTTON_WIDTH;
  1148. var _overview = $(overview);
  1149. _overview.css("position", "absolute");
  1150. _overview.css("left", overview_left+"px");
  1151. _overview.css("top", NAV_OVERVIEW_TOP_OFFSET+"px");
  1152. _overview.css("z-index", "0");
  1153. _overview.css("width", overview.width+"px");
  1154. _overview.css("height", overview.height+"px");
  1155. _overview.appendTo(container);
  1156. if ($.browser.msie) { //XXX: hack: excanvas
  1157. overview = window.G_vmlCanvasManager.initElement(overview);
  1158. }
  1159. oc = overview.getContext("2d");
  1160. decorate_context(oc);
  1161. // Draw Overview
  1162. var _zoom_idx = model.zoom_schema().length - 1;
  1163. zoom_level = model.zoom_schema()[_zoom_idx];
  1164. _data = model.fetch(
  1165. _zoom_idx,
  1166. 0,
  1167. Math.ceil(model._time_range() / zoom_level[0]));
  1168. _data = _data[0] || []; //XXX: use first data source for overview
  1169. // Calculate X-coord Scale
  1170. _x_scale = overview.width / (_data.length - 1);
  1171. // Calculate Y-coord Scale
  1172. //XXX: optional: make it logarithmic?
  1173. //_data = _map(function (x) { return Math.log(x || 1); }, _data);
  1174. var _min_y, _max_y, _point;
  1175. _min_y = _max_y = _data[_data.length-1] || 0;
  1176. for (var i=_data.length-2; i>=0; i--) {
  1177. _point = _data[i] || 0;
  1178. _min_y = _point < _min_y ? _point : _min_y;
  1179. _max_y = _point > _max_y ? _point : _max_y;
  1180. }
  1181. _max_y = (_max_y - _min_y) * NAV_OVERVIEW_SCALE_FACTOR + _min_y;
  1182. _y_scale = (overview.height - 1) / (_max_y - _min_y);
  1183. _render_overview(oc, "rgb(225,225,225)", "rgba(200,200,200,0.2)", "rgb(200,200,200)");
  1184. // Slider
  1185. slider = $([
  1186. '<div',
  1187. 'class="nav-slider"',
  1188. 'style="',
  1189. 'display: none;',
  1190. 'position: absolute;',
  1191. 'overflow: hidden;',
  1192. 'left: '+overview_left+'px;',
  1193. 'top: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
  1194. 'width: 10px;',
  1195. 'height: '+NAV_BUTTON_HEIGHT+'px;',
  1196. 'text-align: center;',
  1197. '"></div>'].join(' ')).appendTo(container);
  1198. _slider = slider[0];
  1199. slider.append([
  1200. '<img',
  1201. 'src="/assets/slider.left.gif"',
  1202. 'style="',
  1203. 'position: absolute;',
  1204. 'top: 0px;',
  1205. 'left: 0px;',
  1206. '"/>'].join(' '));
  1207. slider.append([
  1208. '<img',
  1209. 'src="/assets/slider.right.gif"',
  1210. 'style="',
  1211. 'position: absolute;',
  1212. 'top: 0px;',
  1213. 'right: 0px;',
  1214. '"/>'].join(' '));
  1215. slider_middle = $([
  1216. '<img',
  1217. 'src="/assets/slider.middle.gif"',
  1218. 'style="',
  1219. 'display: none;',
  1220. '"/>'].join(' ')).appendTo(slider);
  1221. // Viewer
  1222. viewer = $([
  1223. '<div',
  1224. 'class="nav-viewer"',
  1225. 'style="',
  1226. 'position: absolute;',
  1227. 'left: '+overview_left+'px;',
  1228. 'top: 0px;',
  1229. 'width: 1px;',
  1230. 'height: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
  1231. '"></div>'].join(' ')).appendTo(container);
  1232. _viewer = viewer[0];
  1233. //XXX: hide preview, but allow range controls to overflow
  1234. var internal = $([
  1235. '<div',
  1236. 'style="',
  1237. 'position: absolute;',
  1238. 'overflow: hidden;',
  1239. 'left: 0px;',
  1240. 'top: 0px;',
  1241. 'width: 100%;',
  1242. 'height: 100%;',
  1243. '"><div>'].join(' ')).appendTo(viewer);
  1244. // Preview
  1245. //XXX: build manually (excanvas r3 workaround)
  1246. preview = document.createElement("canvas");
  1247. preview.width = overview.width;
  1248. preview.height = overview.height;
  1249. _preview = $(preview);
  1250. _preview.css("position", "absolute");
  1251. _preview.css("left", (-BORDER_OFFSET_X)+"px");
  1252. _preview.css("top", NAV_OVERVIEW_TOP_OFFSET+"px");
  1253. _preview.css("width", preview.width+"px");
  1254. _preview.css("height", preview.height+"px");
  1255. _preview.css("z-index", "0");
  1256. _preview.css("border-bottom", "1px solid #aaa");
  1257. _preview.appendTo(internal);
  1258. if ($.browser.msie) { //XXX: hack: excanvas
  1259. preview = window.G_vmlCanvasManager.initElement(preview);
  1260. }
  1261. pc = preview.getContext("2d");
  1262. decorate_context(pc);
  1263. _render_overview(pc, "rgb(25,106,227)", "rgba(25,106,227,0.1)", "rgb(200,200,200)");
  1264. //XXX: ie6 doesn't render in a hidden DOM element
  1265. viewer.css("display", "none");
  1266. // View Range Controls
  1267. range_min = $([
  1268. '<img',
  1269. 'src="/assets/range.gif"',
  1270. 'style="',
  1271. 'position: absolute;',
  1272. 'top: '+Math.floor(preview.height/2)+'px;',
  1273. 'left: '+NAV_RANGE_OFFSET+'px;',
  1274. 'cursor: col-resize;',
  1275. '"/>'].join(' ')).appendTo(viewer);
  1276. _range_min = range_min[0];
  1277. range_max = $([
  1278. '<img',
  1279. 'src="/assets/range.gif"',
  1280. 'style="',
  1281. 'position: absolute;',
  1282. 'top: '+Math.floor(preview.height/2)+'px;',
  1283. 'right: '+NAV_RANGE_OFFSET+'px;',
  1284. 'cursor: col-resize;',
  1285. '"/>'].join(' ')).appendTo(viewer);
  1286. _range_max = range_max[0];
  1287. __is_hover__ = true; //XXX: initially display range controls
  1288. // --- DOM Events
  1289. // ----- Left/Right Navigation Buttons
  1290. addEvent(nav_left[0], "mousedown", nav_left_mousedown);
  1291. addEvent(nav_right[0], "mousedown", nav_right_mousedown);
  1292. // ----- Preview Slider
  1293. addEvent(_slider, "mousedown", drag_init);
  1294. _slider.onmousedown = function(e) { return cancelEvent(e); };
  1295. // ----- Preview Pane
  1296. addEvent(_viewer, "mousedown", drag_init);
  1297. _viewer.onmousedown = function(e) { return cancelEvent(e); };
  1298. // ----- Preview Range Controls
  1299. container.hover(container_mouseover, container_mouseout);
  1300. addEvent(_range_min, "mousedown", range_min_drag_init);
  1301. _range_min.onmousedown = function(e) { return cancelEvent(e); };
  1302. addEvent(_range_max, "mousedown", range_max_drag_init);
  1303. _range_max.onmousedown = function(e) { return cancelEvent(e); };
  1304. // Events Setup
  1305. broker.subscribe("/nav/center", this, "center");
  1306. broker.subscribe("/nav/translate", this, "translate");
  1307. broker.subscribe("/nav/range/update", this, "update_range");
  1308. //XXX: post init
  1309. broker.subscribe("/nav/center", this, "post_init");
  1310. }
  1311. this.init = init;
  1312. function post_init() {
  1313. broker.unsubscribe("/nav/center", this, "post_init");
  1314. viewer.css("display", "inline");
  1315. slider.css("display", "inline");
  1316. }
  1317. this.post_init = post_init;
  1318. // View Translation Methods
  1319. function center() {
  1320. // static variables
  1321. var _overview_width = overview.width - 2; //XXX: account for border-right/left
  1322. center = function() {
  1323. // calculate: width
  1324. var _num_points = parent._chart_period() * model.zoom_delta() / zoom_level[0];
  1325. viewer_width =
  1326. Math.max(1,
  1327. Math.min(_overview_width,
  1328. Math.floor(_num_points * _x_scale)));
  1329. _viewer.style.width = viewer_width+"px";
  1330. _slider.style.width = Math.max(NAV_SCROLL_MIN_WIDTH, viewer_width)+"px";
  1331. if (viewer_width > NAV_SCROLL_MID_THRESHOLD) {
  1332. slider_middle.css("display", "inline");
  1333. } else {
  1334. slider_middle.css("display", "none");
  1335. }
  1336. // translate
  1337. translate(); //XXX: don't publish, has to be in-sync (otherwise, flickers)
  1338. // update: drag modifier
  1339. drag_modifier = (zoom_level[0] / model.zoom_delta()) * parent._x_scale() / _x_scale;
  1340. // range controls: re-position, if necessary
  1341. var _viewer_width = viewer.width();
  1342. if (_viewer_width > NAV_RANGE_CENTER_THRESHOLD) {
  1343. _range_min.style.left = NAV_RANGE_OFFSET+"px";
  1344. if (__is_hover__) {
  1345. range_max.css("display", "inline");
  1346. }
  1347. } else { // viewer.width <= range control width
  1348. _range_min.style.left =
  1349. Math.floor(_viewer_width/2+NAV_RANGE_CENTER_OFFSET)+"px";
  1350. range_max.css("display", "none");
  1351. }
  1352. };
  1353. return center();
  1354. }
  1355. this.center = center;
  1356. function translate() {
  1357. // static variables
  1358. var _half_width = parent.width() / 2;
  1359. var _overview_width = overview.width - 2;
  1360. var _overview_max = _overview_width + overview_left;
  1361. translate = function() {
  1362. // shared references
  1363. var _min_pixel = parent.min_pixel();
  1364. var _max_pixel = parent.max_pixel();
  1365. var _center_pixel = parent.center_pixel();
  1366. var _real_min_pixel = _min_pixel - _half_width;
  1367. var _real_max_pixel = _max_pixel + _half_width;
  1368. // calculate: offset
  1369. var _offset;
  1370. if (_center_pixel < _max_pixel) {
  1371. _offset = overview_left;
  1372. if (_center_pixel > _min_pixel) {
  1373. var _pixel_range = _real_max_pixel - _real_min_pixel;
  1374. _offset +=
  1375. (_center_pixel - _real_min_pixel) / _pixel_range * _overview_width;
  1376. _offset -= (viewer_width / 2);
  1377. _offset = Math.round(_offset);
  1378. } // else: center_pixel == min_pixel
  1379. } else { // center_pixel == max_pixel
  1380. _offset = _overview_max - viewer_width;
  1381. }
  1382. //XXX: correct out-of-bounds
  1383. _offset = Math.min((_overview_max - viewer_width), _offset);
  1384. _offset = Math.max(overview_left, _offset);
  1385. // position view and set width
  1386. preview.style.left = (overview_left-_offset)+"px";
  1387. _viewer.style.left = _offset+"px";
  1388. if (viewer_width < NAV_SCROLL_MIN_WIDTH) {
  1389. _offset -= NAV_SCROLL_MIN_WIDTH - viewer_width;
  1390. }
  1391. _slider.style.left =
  1392. Math.max(overview_left, _offset)+"px";
  1393. };
  1394. return translate();
  1395. }
  1396. this.translate = translate;
  1397. // DOM Event Methods
  1398. // --- Left/Right Navigation
  1399. function nav_left_mousedown(e) {
  1400. nav_right[0].className = "nav-right-unsel nav-sel";
  1401. nav_left[0].className = "nav-left-sel";
  1402. addEvent(document, "mouseup", nav_left_mouseup);
  1403. broker.publish("/chart/draw/start"); // start draw loop
  1404. broker.publish("/chart/scroll/start", 1); // start scroll loop
  1405. }
  1406. function nav_left_mouseup(e) {
  1407. nav_right[0].className = "nav-right-unsel nav-unsel";
  1408. nav_left[0].className = "nav-left-unsel";
  1409. removeEvent(document, "mouseup", nav_left_mouseup);
  1410. broker.publish("/chart/draw/stop"); // stop draw loop
  1411. broker.publish("/chart/scroll/stop"); // stop scroll loop
  1412. }
  1413. function nav_right_mousedown(e) {
  1414. nav_right[0].className = "nav-right-sel nav-sel";
  1415. addEvent(document, "mouseup", nav_right_mouseup);
  1416. broker.publish("/chart/draw/start"); // start draw loop
  1417. broker.publish("/chart/scroll/start", -1); // start scroll loop
  1418. }
  1419. function nav_right_mouseup(e) {
  1420. nav_right[0].className = "nav-right-unsel nav-unsel";
  1421. removeEvent(document, "mouseup", nav_right_mouseup);
  1422. broker.publish("/chart/draw/stop"); // stop draw loop
  1423. broker.publish("/chart/scroll/stop"); // stop scroll loop
  1424. }
  1425. // --- Preview Pane
  1426. function drag_init(e) {
  1427. e = e ? e : window.event;
  1428. addEvent(document, "mousemove", drag_move);
  1429. viewer._drag_x = e.clientX;
  1430. addEvent(document, "mouseup", drag_end);
  1431. broker.publish("/chart/draw/start"); // start draw loop
  1432. }
  1433. function drag_move(e) {
  1434. e = e ? e : window.event;
  1435. var delta = e.clientX - viewer._drag_x;
  1436. if (Math.abs(delta) <= 1) {
  1437. return cancelEvent(e);
  1438. }
  1439. viewer._drag_x = e.clientX;
  1440. delta = -1 * Math.floor(drag_modifier * delta);
  1441. broker.publish("/chart/translate", delta);
  1442. return cancelEvent(e);
  1443. }
  1444. function drag_end(e) {
  1445. removeEvent(document, "mousemove", drag_move);
  1446. removeEvent(document, "mouseup", drag_end);
  1447. broker.publish("/chart/draw/stop"); // stop draw loop
  1448. }
  1449. // Preview Range Control
  1450. function container_mouseover(e) {
  1451. __is_hover__ = true;
  1452. range_min.css("display", "inline");
  1453. if (viewer.width() > NAV_RANGE_CENTER_THRESHOLD) {
  1454. range_max.css("display", "inline");
  1455. }
  1456. }
  1457. function container_mouseout(e) {
  1458. __is_hover__ = false;
  1459. range_min.css("display", "none");
  1460. range_max.css("display", "none");
  1461. }
  1462. function range_min_drag_init(e) {
  1463. __is_drag_min__ = true;
  1464. _range_drag_init(e);
  1465. }
  1466. function range_max_drag_init(e) {
  1467. __is_drag_min__ = false;
  1468. _range_drag_init(e);
  1469. }
  1470. function _range_drag_init(e) {
  1471. e = e ? e : window.event;
  1472. addEvent(document, "mousemove", range_drag_move);
  1473. viewer._range_x = e.clientX;
  1474. addEvent(document, "mouseup", range_drag_end);
  1475. broker.publish("/chart/draw/start"); // start draw loop
  1476. }
  1477. function update_range(modified, fixed) {
  1478. // static variables
  1479. var _overview_width = overview.width-2;
  1480. var _zoom_schema = model.zoom_schema();
  1481. var _visible_panes = parent.width() / parent._pane_width();
  1482. var _half_width = Math.floor(parent.width() / 2);
  1483. update_range = function(modified, fixed) {
  1484. // sanitize parameters
  1485. var _min = Math.min(modified, fixed);
  1486. var _max = Math.max(modified, fixed);
  1487. var _min_fixed = _min === fixed ? true : false;
  1488. var _rel_min = Math.max(0, _min - overview_left);
  1489. var _rel_max = Math.min(_overview_width, _max - overview_left);
  1490. // calculate new time
  1491. var _rel_range = (_rel_max - _rel_min) / _overview_width;
  1492. var _time_range = Math.floor(model._time_range() * _rel_range);
  1493. // determine new zoom/period
  1494. var _zoom_level, _zoom_scale;
  1495. for (var i=0; i<_zoom_schema.length; i++) {
  1496. _zoom_scale = i;
  1497. _zoom_level = _zoom_schema[_zoom_scale];
  1498. if ((_zoom_level[0] * _zoom_level[2] * _visible_panes) >= _time_range) {
  1499. break;
  1500. }
  1501. }
  1502. var _pane_period, _zoom_time_range;
  1503. for (i=_zoom_level[1]; i<=_zoom_level[2]; i++) {
  1504. _pane_period = i;
  1505. _zoom_time_range = _zoom_level[0] * i * _visible_panes;
  1506. if (_zoom_time_range >= _time_range) {
  1507. break;
  1508. }
  1509. }
  1510. var _half_time = _zoom_time_range / 2;
  1511. var _time_offset;
  1512. if (_min_fixed) {
  1513. _time_offset = model.min_time();
  1514. _time_offset += model._time_range() * (_rel_min / _overview_width);
  1515. _time_offset += _half_time;
  1516. } else {
  1517. _time_offset = model.min_time();
  1518. _time_offset += model._time_range() * (_rel_max / _overview_width);
  1519. _time_offset -= _half_time;
  1520. }
  1521. _time_offset = Math.floor(_time_offset);
  1522. parent._update_zoom(_zoom_scale, _pane_period);
  1523. parent.center(_time_offset, _half_width);
  1524. };
  1525. return update_range(modified, fixed);
  1526. }
  1527. this.update_range = update_range;
  1528. function range_drag_move(e) {
  1529. e = e ? e : window.event;
  1530. if (Math.abs(e.clientX - viewer._range_x) <= 1) {
  1531. return cancelEvent(e);
  1532. }
  1533. viewer._range_x = e.clientX;
  1534. var _viewer_left = parseInt(_viewer.style.left, 10);
  1535. var delta;
  1536. //XXX: use a global to switch, because events can be queued up
  1537. if (__is_drag_min__) { // dragging range min
  1538. delta = viewer.offset().left - e.clientX;
  1539. var _new_min = _viewer_left-delta;
  1540. var _max = _viewer_left+viewer.width();
  1541. if (_new_min > _max) {
  1542. __is_drag_min__ = false;
  1543. return cancelEvent(e);
  1544. }
  1545. broker.publish("/nav/range/update", _new_min, _max);
  1546. } else { // draggging range max
  1547. var _viewer_width = viewer.width();
  1548. delta = (viewer.offset().left+_viewer_width) - e.clientX;
  1549. var _new_max = _viewer_left+_viewer_width-delta;
  1550. if (_new_max < _viewer_left) {
  1551. __is_drag_min__ = true;
  1552. return cancelEvent(e);
  1553. }
  1554. broker.publish("/nav/range/update", _new_max, _viewer_left);
  1555. }
  1556. return cancelEvent(e);
  1557. }
  1558. function range_drag_end(e) {
  1559. removeEvent(document, "mousemove", range_drag_move);
  1560. removeEvent(document, "mouseup", range_drag_end);
  1561. broker.publish("/chart/draw/stop"); // stop draw loop
  1562. }
  1563. };
  1564. // ---------- Chart ----------
  1565. var LARGE_CHART_THRESHOLD_WIDTH = 720; // target: 720px x 6 panes @ 120px
  1566. var LARGE_CHART_NUM_PANES = 6;
  1567. var SMALL_CHART_NUM_PANES = 3; // target: 180px x 3 panes @ 60px
  1568. var BORDER_OFFSET_X = 1;
  1569. var BORDER_OFFSET_Y = 1;
  1570. var TOP_PANE_HEIGHT = 25;
  1571. var TOP_PANE_BORDER_COLOR = "rgb(175,175,175)"; //XXX: consider: 150
  1572. var UPDATE_DELTA_FACTOR = 0.5; //XXX: speed at which updates will improve
  1573. var UPDATE_BOUND = 1000 / 25; //XXX: lower bound on re-draw interval
  1574. var UPDATE_AFTER_STOP = 1000 * 0.5; //XXX: allow a second between stop/start drag
  1575. if ($.browser.msie) { // IE6 needs help
  1576. UPDATE_BOUND = 4 * 1000;
  1577. UPDATE_AFTER_STOP = 1000;
  1578. }
  1579. var Y_SCALE_FACTOR = 1.10;
  1580. var Y_LABELS_WIDTH = 100;
  1581. var Y_LABELS_RIGHT_OFFSET = 4;
  1582. var Y_LABEL_OFFSET = -12;
  1583. var Y_LABEL = "W"; //XXX: make this a parameter
  1584. var MOUSE_SCROLL_ZOOM_FACTOR = 0.5;
  1585. var MOUSE_SCROLL_ZOOM_DELTA_MAX = 2;
  1586. if ($.browser.msie) { // IE6 needs help
  1587. MOUSE_SCROLL_ZOOM_FACTOR = 0.75;
  1588. }
  1589. var TICK_MARK_INCREMENTS = [
  1590. 60,
  1591. 60*5,
  1592. 60*10,
  1593. 60*15,
  1594. 60*30,
  1595. 60*60,
  1596. 60*60*3,
  1597. 60*60*6,
  1598. 60*60*12,
  1599. 60*60*24,
  1600. 60*60*24*7,
  1601. 60*60*24*14,
  1602. 60*60*24*31
  1603. ];
  1604. //FIXME: take into account: Date.getTimezoneOffset() [returns offset in minutes]
  1605. var TICK_DATE_PROXY = new Date();
  1606. var _format_date = function(date_) {
  1607. if (date_.getSeconds() !== 0) {
  1608. return formatDate(date_, "h:mm:ss a");
  1609. }
  1610. if (date_.getMinutes() !== 0) {
  1611. return formatDate(date_, "h:mm a");
  1612. }
  1613. if (date_.getHours() !== 0) {
  1614. return formatDate(date_, "h a");
  1615. }
  1616. if (date_.getDay() !== 1) {
  1617. return formatDate(date_, "MMM d");
  1618. }
  1619. if (date_.getMonth() !== 0) {
  1620. return formatDate(date_, "MMM");
  1621. }
  1622. return formatDate(date_, "yyyy");
  1623. };
  1624. ;;; $c._format_date = _format_date;
  1625. var TICK_1_MINUTE = function(timestamp_) {
  1626. TICK_DATE_PROXY.setTime(timestamp_*1000);
  1627. if (TICK_DATE_PROXY.getSeconds() !== 0) {
  1628. TICK_DATE_PROXY.setSeconds(60);
  1629. }
  1630. return [
  1631. (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
  1632. _format_date(TICK_DATE_PROXY)
  1633. ];
  1634. };
  1635. ;;; $c

Large files files are truncated, but you can click here to view the full file