/slippy-chart.js
JavaScript | 3341 lines | 2458 code | 532 blank | 351 comment | 309 complexity | 15df4ed905aea099527e72a5ccd3b5b0 MD5 | raw file
- /*!
- * Slippy Chart Library v0.1
- * http://www.scaledlabs.com
- *
- * Copyright (c) 2009 Kelvin Kakugawa
- *
- */
- /*jslint browser: true, debug: true, rhino: true*/
- /*extern jQuery*/
- (function($) {
- var
- window = this,
- document = window.document,
- undefined,
- _SlippyChart = window.SlippyChart,
- _$c = window.$c,
-
- $c = window.$c = SlippyChart = window.SlippyChart = {};
- // ========== Compatibility ==========
- // ---------- Event Handling ----------
- var addEvent = function(el, type, fn) {
- if (window.attachEvent) {
- addEvent = function(el, type, fn) {
- el.attachEvent("on"+type, fn);
- };
- } else {
- addEvent = function(el, type, fn) {
- if (type =="mousewheel") {
- type = "DOMMouseScroll";
- }
- el.addEventListener(type, fn, false);
- };
- }
- addEvent(el, type, fn);
- };
- ;;; $c.addEvent = addEvent;
- var removeEvent = function(el, type, fn) {
- if (window.detachEvent) {
- removeEvent = function(el, type, fn) {
- el.detachEvent("on"+type, fn);
- };
- } else {
- removeEvent = function(el, type, fn) {
- el.removeEventListener(type, fn, false);
- };
- }
- removeEvent(el, type, fn);
- };
- ;;; $c.removeEvent = removeEvent;
- var cancelEvent = function(e) {
- e = e ? e : window.event;
- if (e.stopPropagation) {
- e.stopPropagation();
- }
- if (e.preventDefault) {
- e.preventDefault();
- }
- e["cancelBubble"] = true;
- e["cancel"] = true;
- e["returnValue"] = false;
- return false; // note: exception for window.status
- };
- ;;; $c.cancelEvent = cancelEvent;
- // ========== Utilities ==========
- // ---------- Functional ----------
- var _map = function(func_, list_) {
- var len = list_.length || 0;
- var res = [];
- res.length = len;
- for (var i=len-1; i>=0; i--) {
- res[i] = func_(list_[i]);
- }
- return res;
- };
- ;;; $c._map = _map;
- var _reduce = function(func_, list_, initial_) {
- var len = list_.length || 0;
- if (len === 0) {
- return initial_;
- }
- var i = 0;
- if (typeof initial_ === "undefined") {
- initial_ = list_[i++];
- }
- for (; i<len; i++) {
- initial_ = func_(initial_, list_[i]);
- }
- return initial_;
- };
- ;;; $c._reduce = _reduce;
- var _filter = function(func_, list_) {
- var len = list_.length || 0;
- var res = [];
- for (var i=len-1; i>=0; i--) {
- if (func_(list_[i])) {
- res.push(list_[i]);
- }
- }
- return res.reverse();
- };
- ;;; $c._filter = _filter;
- var _zip = function() {
- var num_lists = arguments.length || 0;
- if (num_lists === 0) {
- return [];
- }
- var len = arguments[0].length || 0;
- var res = [];
- res.length = len;
- for (var i=len-1; i>=0; i--) {
- res[i] = _map(function (x) { return x[i];}, arguments);
- }
- return res;
- };
- ;;; $c._zip = _zip;
- // ---------- Binary Search ----------
- var _search = function(bucket_, key_) {
- var low = 0;
- var high = bucket_.length-1;
- while (low <= high) {
- var mid = Math.floor((low + high) / 2);
- var midVal = bucket_[mid];
- if (midVal < key_) {
- low = mid + 1;
- } else if (midVal > key_) {
- high = mid - 1;
- } else { // midVal == key_
- return mid;
- }
- }
- return -(low+1); // flag: key not found
- };
- ;;; $c._search = _search;
- var _range_r = function(bucket_, key_) {
- var low = 0;
- var high = bucket_.length-1;
- while (low <= high) {
- var mid = Math.floor((low + high) / 2);
- var midVal = bucket_[mid];
- if (midVal < key_) {
- low = mid + 1;
- } else if (midVal > key_) {
- high = mid - 1;
- } else { // midVal == key_
- return bucket_.slice(mid);
- }
- }
- return bucket_.slice(low);
- };
- ;;; $c._range_r = _range_r;
- var _range_l = function(bucket_, key_) {
- var low = 0;
- var high = bucket_.length-1;
- while (low <= high) {
- var mid = Math.floor((low + high) / 2);
- var midVal = bucket_[mid];
- if (midVal < key_) {
- low = mid + 1;
- } else if (midVal > key_) {
- high = mid - 1;
- } else { // midVal == key_
- return bucket_.slice(0, mid+1);
- }
- }
- return bucket_.slice(0, high+1);
- };
- ;;; $c._range_l = _range_l;
- var _range = function(bucket_, lkey_, rkey_) {
- var low, high, mid, midVal, lindex, rindex;
- low = 0;
- high = bucket_.length-1;
- while (low <= high) {
- mid = Math.floor((low + high) / 2);
- midVal = bucket_[mid];
- if (midVal < lkey_) {
- low = mid + 1;
- } else if (midVal > lkey_) {
- high = mid - 1;
- } else { // midval == lkey_
- low = mid;
- break;
- }
- }
- lindex = low;
- high = bucket_.length-1;
- while (low <= high) {
- mid = Math.floor((low + high) / 2);
- midVal = bucket_[mid];
- if (midVal < rkey_) {
- low = mid + 1;
- } else if (midVal > rkey_) {
- high = mid - 1;
- } else { // midval == rkey_
- high = mid;
- break;
- }
- }
- rindex = high + 1;
- return bucket_.slice(lindex, rindex);
- };
- ;;; $c._range = _range;
- // ---------- Linked List ----------
- var LinkedList = function() {
- this.first = null;
- this.last = null;
- this.length = 0;
- };
- ;;; $c.LinkedList = LinkedList;
- var LinkedListNode = function(data) {
- this.prev = null;
- this.next = null;
- this.data = data;
- };
- ;;; $c.LinkedListNode = LinkedListNode;
- LinkedList.prototype.prepend = function(node) {
- if (this.first === null) {
- this.first = node;
- this.last = node;
- } else {
- node.prev = null;
- node.next = this.first;
- this.first.prev = node;
- this.first = node;
- }
- this.length++;
- };
- LinkedList.prototype.append = function(node) {
- if (this.first === null) {
- this.first = node;
- this.last = node;
- } else {
- node.prev = this.last;
- node.next = null;
- this.last.next = node;
- this.last = node;
- }
- this.length++;
- };
- LinkedList.prototype.remove = function(node) {
- if (this.first == this.last) {
- this.first = null;
- this.last = null;
- } else {
- if (node.prev) {
- node.prev.next = node.next;
- }
- if (node.next) {
- node.next.prev = node.prev;
- }
- if (node == this.first) {
- this.first = node.next;
- }
- if (node == this.last) {
- this.last = node.prev;
- }
- }
- this.length--;
- node.prev = null;
- node.next = null;
- };
- // ---------- Mapped Linked List ----------
- var MappedLinkedList = function() {
- this.map = {};
- this.list = new LinkedList();
- };
- ;;; $c.MappedLinkedList = MappedLinkedList;
- MappedLinkedList.prototype.add = function(key, val) {
- if (typeof this.map[key] !== "undefined") {
- return; // don't re-add
- }
- var node = new LinkedListNode(val);
- this.map[key] = node;
- this.list.append(node);
- };
- MappedLinkedList.prototype.remove = function(key) {
- if (typeof this.map[key] === "undefined") {
- return;
- }
- this.list.remove(this.map[key]);
- delete(this.map[key]);
- };
- // ---------- Pub/Sub Broker ----------
- var PubSubBroker = function() {
- this.channels = {};
- };
- PubSubBroker.prototype = {
- uid_counter: 0
- };
- ;;; $c.PubSubBroker = PubSubBroker;
- PubSubBroker.prototype.publish = function(signal) {
- setTimeout((function(signal) { // make it asynch
- var passed = arguments;
- var channels = this.channels;
- return function() {
- var args = Array.prototype.slice.call(passed, 1);
- var node = channels[signal] && channels[signal].list.first;
- while (node) {
- var handler = node.data;
- setTimeout((function(handler, args) { // make it asynch
- return function() {
- handler.apply(this, args);
- };
- }).call(this, handler, args), 0);
-
- node = node.next;
- }
- };
- }).apply(this, arguments), 0);
- };
- PubSubBroker.prototype.subscribe = function(signal, scope, handlerName){
- if (typeof this.channels[signal] === "undefined") {
- this.channels[signal] = new MappedLinkedList();
- }
- var curryArray = Array.prototype.slice.call(arguments, 3);
- if (typeof scope._psb_uid == "undefined") {
- scope._psb_uid = ++this.uid_counter;
- }
- var key = scope._psb_uid + ":" + handlerName + ":" + curryArray;
- this.channels[signal].add(key, function(){
- var normalizedArgs = Array.prototype.slice.call(arguments, 0);
- scope[handlerName].apply((scope || window), curryArray.concat(normalizedArgs));
- });
- };
- PubSubBroker.prototype.unsubscribe = function(signal, scope, handlerName) {
- if (typeof this.channels[signal] === "undefined") {
- return; // throw warn?
- }
- var curryArray = Array.prototype.slice.call(arguments, 3);
- var key = scope._psb_uid + ":" + handlerName + ":" + curryArray;
- this.channels[signal].remove(key);
- };
- // ========== Canvas Utilities ==========
- // ---------- Context Helpers ----------
- function decorate_context(context_) {
- context_._calc = function(pos_) {
- return context_.lineWidth % 2 ? Math.floor(pos_) + 0.5 : Math.floor(pos_);
- };
- context_._moveTo = function(x_, y_) {
- context_.moveTo(context_._calc(x_), context_._calc(y_));
- };
- context_._lineTo = function(x_, y_) {
- context_.lineTo(context_._calc(x_), context_._calc(y_));
- };
- }
- // ========== Chart ==========
- // ---------- LineDataCache ----------
- //TODO: consider: auto-generating the zoom schema
- var _ZOOM_SCHEMA_HALF = [
- // [delta, low period, high period]
- [60, 1, 15],
- [60*5, 3, 12],
- [60*30, 2, 12],
- [60*60*3, 2, 16],
- [60*60*12, 4, 16]
- ];
- var _ZOOM_SCHEMA_RELAXED = [
- // [delta, low period, high period]
- [60, 1, 20],
- [60*5, 4, 18],
- [60*30, 3, 18],
- [60*60*3, 3, 20],
- [60*60*12, 5, 31]
- ];
- var _ZOOM_SCHEMA_STANDARD = [
- // [delta, low period, high period]
- [60, 1, 30],
- [60*5, 6, 30],
- [60*30, 5, 30],
- [60*60*3, 5, 32],
- [60*60*12, 8, 31]
- ];
- var _ZOOM_SCHEMA_AGGRESSIVE = [
- // [delta, low period, high period]
- [60, 1, 40],
- [60*5, 8, 42],
- [60*30, 7, 42],
- [60*60*3, 7, 40],
- [60*60*12, 10, 31]
- ];
- var _ZOOM_SCHEMA_INSANE = [
- // [delta, low period, high period]
- [60, 1, 60],
- [60*5, 12, 60],
- [60*30, 10, 60],
- [60*60*3, 10, 60],
- [60*60*12, 15, 31]
- ];
- var _ZOOM_SCHEMA_MAX = [
- // [delta, low period, high period]
- [60, 1, 120],
- [60*5, 24, 120],
- [60*30, 20, 120],
- [60*60*3, 20, 120],
- [60*60*12, 30, 31]
- ];
- /**
- * Data Cache for a Line
- * a cache for each zoom scale [different granularity of time data]
- * w/in each zoom scale cache, the point interval is the time delta at that zoom scale
- * @class
- */
- var LineDataCache = function(model_) {
- // Initialize
- var
- model = model_,
- min_time = 0,
- max_time = 0,
- bucket_size = 0,
- min_buckets = 0,
- max_buckets = 0,
- bucket_ttl = 0,
- cache = [], // at zoom scale
- proxy_bucket = [];
- this.model = function() { return model; };
- this.min_time = function() { return min_time; };
- this.max_time = function() { return max_time; };
- this.cache = function() { return cache; };
- /* Broker Channels:
- /cache/flush flush expired buckets
- */
- // Setup
- function init(min_time_, max_time_) {
- min_time = min_time_;
- max_time = max_time_;
- bucket_size = LineDataCache.prototype.bucket_size;
- min_buckets = LineDataCache.prototype.min_buckets;
- max_buckets = LineDataCache.prototype.max_buckets;
- bucket_ttl = LineDataCache.prototype.bucket_ttl;
- proxy_bucket.length = bucket_size;
- for (var i=0; i<model.zoom_schema().length; i++) {
- cache[i] = {};
- }
- //FIXME: pull these for real
- //XXX: bucket: 0; increment: 60
- for (var i=0; i<4; i++) {
- var bucket = [];
- bucket.length = bucket_size; //XXX: set length to get undefined from slice
- var range = 70-i*10;
- for (var j=0; j<bucket_size; j++) {
- if (i==2) {
- bucket[j] = 50;
- } else {
- bucket[j] = range+Math.random()*30;
- }
- }
- cache[0][i] = bucket;
- }
- var zoom_schema = model.zoom_schema();
- if (zoom_schema.length >= 2) {
- //XXX: bucket: 1; increment: 60*5
- __sum = function(x,y) { return (x || 0) + (y || 0); };
- _sum = function(list_) { return _reduce(__sum, list_, 0); };
- cache[1][0] = [];
- cache[1][0].length = bucket_size;
- var k=0;
- for (i=0; i<4; i++) {
- for (j=0; j<bucket_size; j+=5) {
- cache[1][0][k++] = _sum(cache[0][i].slice(j,j+5)) / 5;
- }
- }
- var len = k;
- }
- if (zoom_schema.length >= 3) {
- //XXX: bucket: 2; increment: 60*30
- cache[2][0] = [];
- cache[2][0].length = bucket_size;
- k=0;
- for (i=0; i<len; i+=6) {
- cache[2][0][k++] = _sum(cache[1][0].slice(i,i+6)) / 6;
- }
- len = k;
- }
- if (zoom_schema.length >= 4) {
- //XXX: bucket: 3; increment: 60*60*3
- cache[3][0] = [];
- cache[3][0].length = bucket_size;
- k=0;
- for (i=0; i<len; i+=6) {
- cache[3][0][k++] = _sum(cache[2][0].slice(i,i+6)) / 6;
- }
- len = k;
- }
- if (zoom_schema.length >= 5) {
- //XXX: bucket: 4; increment: 60*60*12
- cache[4][0] = [];
- cache[4][0].length = bucket_size;
- k=0;
- for (i=0; i<len; i+=4) {
- cache[4][0][k++] = _sum(cache[3][0].slice(i,i+4)) / 4;
- }
- }
- // Register for Upkeep
- var broker = model.broker();
- broker.subscribe("/cache/flush", this, "_flush");
- }
- this.init = init;
- // Data
- /**
- * @function
- * @param {int} zoom_scale_ cache data level to use
- * @param {int} start_offset_ first point in range
- * @param {int} end_offset_ last point in range
- * @returns cache[zoom_scale_][start_offset_:end_offset_+1]
- */
- function fetch(zoom_scale_, start_offset_, end_offset_) {
- var _data = cache[zoom_scale_];
- var start_idx = Math.floor(start_offset_/bucket_size);
- var end_idx = Math.floor(end_offset_/bucket_size);
- //XXX: avoid % (IE6 is slow)
- start_offset_ -= start_idx * bucket_size;
- end_offset_ -= end_idx * bucket_size;
- end_offset_ += 1; //XXX: compensate for exclusive
- if (start_idx === end_idx) {
- var _bucket = _data[start_idx];
- if (!_bucket) {
- _bucket = proxy_bucket;
- //TODO: fetch bucket from remote data source
- }
- return _bucket.slice(start_offset_, end_offset_);
- }
- var _start_bucket = _data[start_idx];
- if (!_start_bucket) {
- _start_bucket = proxy_bucket;
- //TODO: fetch bucket from remote data source
- }
- var _end_bucket = _data[end_idx];
- if (!_end_bucket) {
- _end_bucket = proxy_bucket;
- //TODO: fetch bucket from remote data source
- }
- var start_range = _start_bucket.slice(start_offset_, bucket_size);
- var end_range = _end_bucket.slice(0, end_offset_);
- return start_range.concat(end_range);
- }
- this.fetch = fetch;
-
- // Upkeep
- function _flush() {
- //TODO: flush expired buckets
- }
- this._flush = _flush;
- };
- //TODO: implement bucket caching
- //TODO: implement cache using a MappedLinkedList
- LineDataCache.prototype = {
- bucket_size: 60*24, // # of time points (depending on zoom scale)
- min_buckets: 10, // # of buckets to keep, regardless of TTL
- max_buckets: 100, // # of buckets to keep, at any time
- bucket_ttl: 60*1000 // 1m
- };
- // ---------- Model ----------
- var Model = function(parent_, zoom_schema_) {
- // Initialize
- var
- parent = parent_,
- zoom_schema = zoom_schema_,
- broker = parent.broker(),
- min_time = 0,
- max_time = 0,
- _time_range = 0,
- data = [],
- upkeep_interval = Model.prototype.upkeep_interval;
- this.parent = function() { return parent; };
- this.zoom_schema = function() { return zoom_schema; };
- this.zoom_level = function() { return zoom_schema[parent.zoom_scale()]; };
- this.zoom_delta = function() { return zoom_schema[parent.zoom_scale()][0]; };
- this.broker = function() { return broker; };
- this.min_time = function() { return min_time; };
- this.max_time = function() { return max_time; };
- this._time_range = function() { return _time_range; };
- this.data = function() { return data; };
- // Setup
- function init(min_time_, max_time_, chart_width_, pane_width_) {
- min_time = min_time_;
- max_time = max_time_;
-
- _time_range = max_time - min_time;
- // calculate: max zoom level and period
- var _num_panes = chart_width_ / pane_width_;
- for (var i=0; i<zoom_schema.length; i++) {
- var _level = zoom_schema[i];
- if (_time_range < (_level[0]*chart_width_)) {
- zoom_schema.length = i+1;
- _max_points = Math.floor(_time_range / _level[0]);
- zoom_schema[i][2] = Math.ceil(_max_points / _num_panes);
- break;
- }
- }
- //FIXME: create this for real
- var line_cache = new LineDataCache(this);
- line_cache.init(min_time, max_time);
- data[0] = line_cache;
- // Upkeep Thread
- setTimeout(_upkeep, upkeep_interval);
- }
- this.init = init;
- // Data
- function fetch(zoom_scale_, start_offset_, period_) {
- var _end_offset = start_offset_ + period_;
- var section = [];
- for (var i=data.length-1; i>=0; i--) {
- section.push(data[i].fetch(zoom_scale_, start_offset_, _end_offset));
- }
- section.reverse(); //XXX: think: is this necessary? consider how to identify lines
- return section;
- }
- this.fetch = fetch;
- // Upkeep
- function _upkeep() {
- broker.publish("/cache/flush");
- setTimeout(_upkeep, upkeep_interval);
- }
- };
- Model.prototype = {
- //TODO: make zoom_schema private (potentially, multiple charts)
- upkeep_interval: 60*1000 // background upkeep thread interval
- };
- // ---------- Pane ----------
- var PLOT_LINE_WIDTH = 1.25;
- if ($.browser.msie) { //XXX: hack: IE6
- PLOT_LINE_WIDTH = 1.6;
- }
- var BOTTOM_TAPE_HEIGHT = 16;
- var BOTTOM_TAPE_TOP_ADJUST = 0;
- //var BOTTOM_TAPE_COLOR = "rgb(147,190,255)";
- //var BOTTOM_TAPE_COLOR = "rgb(198,228,255)";
- var BOTTOM_TAPE_COLOR = "rgb(194,218,241)";
- if ($.browser.msie) { //XXX: hack: IE6
- BOTTOM_TAPE_TOP_ADJUST = -1;
- }
- var TAPE_MARKER_HEIGHT = 5;
- var Pane = function(parent_, width_) {
- // Initialize
- var
- parent = parent_,
- model = parent.model(),
- time_offset = 0,
- _data_offset = 0,
- width = width_,
- _half_width = Math.floor(width/2),
- draw_height = 0,
- chart_height = 0,
- container = null,
- _style = null,
- canvas = null,
- c = null,
- tape = null,
- _data = [],
- _min_y = 0,
- _max_y = 0;
-
- this.parent = function() { return parent; };
- this.time_offset = function() { return time_offset; };
- this._data_offset = function() { return _data_offset; };
- this.container = function() { return container; };
- this.canvas = function() { return canvas; };
- this.offset = function() { return parseInt(_style.left, 10); };
- this.c = function() { return c; };
- this.tape = function() { return tape; };
- this._data = function() { return _data; };
- this._min_y = function() { return _min_y; };
- this._max_y = function() { return _max_y; };
- /* Broker Channels:
- /pane/translate translate pane by delta
- /pane/redraw redraw pane
- */
- // Setup
- function init() {
- // Setup Variables
- draw_height = parent.draw_height();
- chart_height = parent.chart_height();
- // Container
- container = $([
- '<div',
- 'style="',
- 'position: absolute;',
- 'left: 0px;',
- 'top: 0px;',
- 'z-index: 0;',
- 'width: '+width+'px;',
- 'height: '+draw_height+'px;',
- '"></div>'].join(' ')).appendTo(parent.container());
- _style = container[0].style;
- // Canvas
- //XXX: build manually (excanvas r3 workaround)
- canvas = document.createElement("canvas");
- canvas.width = width;
- canvas.height = chart_height;
- var _canvas = $(canvas);
- _canvas.css("position", "absolute");
- _canvas.css("left", "0px");
- _canvas.css("top", "0px");
- _canvas.css("z-index", "0");
- _canvas.css("width", width+"px");
- _canvas.css("height", chart_height+"px");
- _canvas.appendTo(container);
- if ($.browser.msie) { //XXX: hack: excanvas
- canvas = window.G_vmlCanvasManager.initElement(canvas);
- }
- c = canvas.getContext("2d");
- decorate_context(c);
- if ($.browser.msie && c.setTransform) {
- //XXX: exception: IE6 renders more smoothly w/ setTransform
- c.setTransform(1, 0, 0, -1, 0, chart_height);
- }
- // Tape
- tape = $([
- '<div',
- 'style="',
- 'position: absolute;',
- 'left: 0px;',
- 'top: '+chart_height+'px;',
- 'z-index: 0;',
- 'width: '+width+'px;',
- 'height: '+(draw_height-chart_height)+'px;',
- 'text-align: left;',
- '"></div>'].join(' ')).appendTo(container);
- // Broker Subscriptions
- var broker = parent.broker();
- broker.subscribe("/pane/translate", this, "translate");
- broker.subscribe("/pane/redraw", this, "_draw");
-
- //XXX: needs reset()
- }
- this.init = init;
- // Translate Methods
- function translate(delta_) {
- _style.left = (parseInt(_style.left, 10)+delta_)+"px";
- }
- this.translate = translate;
- // Draw Methods
- function __grid_x(x_tick_) {
- if ($.browser.msie && c.setTransform) {
- //XXX: exception: IE6 renders more smoothly w/ setTransform
- __grid_x = function(x_tick_) {
- c.lineWidth = 1;
- // Vertical Line at 0
- c.beginPath();
- c.strokeStyle = "rgb(200,200,200)";
- c._moveTo(x_tick_, 0);
- c._lineTo(x_tick_, chart_height);
- c.stroke();
- c.beginPath();
- c.strokeStyle = "rgb(100,100,100)";
- c._moveTo(x_tick_, 0);
- c._lineTo(x_tick_, TAPE_MARKER_HEIGHT);
- c.stroke();
- };
- } else { //XXX: Safari,FF2 don't support setTransform; FF3 renders faster
- __grid_x = function(x_tick_) {
- c.lineWidth = 1;
- // Vertical Line at 0
- c.beginPath();
- c.strokeStyle = "rgb(200,200,200)";
- c._moveTo(x_tick_, 0);
- c._lineTo(x_tick_, chart_height);
- c.stroke();
- c.beginPath();
- c.strokeStyle = "rgb(100,100,100)";
- c._moveTo(x_tick_, chart_height);
- var _tape_y = chart_height-TAPE_MARKER_HEIGHT;
- c._lineTo(x_tick_, _tape_y);
- c.stroke();
- };
- }
- return __grid_x(x_tick_);
- }
- this.__grid_x = __grid_x;
- function __grid_y(y_intervals_) {
- if ($.browser.msie && c.setTransform) {
- //XXX: exception: IE6 renders more smoothly w/ setTransform
- __grid_y = function(y_intervals_) {
- c.lineWidth = 1;
- // Horizontal Lines
- c.strokeStyle = "rgb(200,200,200)";
- //XXX: consider Duff's device
- for (var i=y_intervals_.length-1; i>=0; i--) {
- c.beginPath();
- var y = y_intervals_[i];
- c._moveTo(0, y);
- c._lineTo(width, y);
- c.stroke();
- }
- };
- } else { //XXX: Safari,FF2 don't support setTransform; FF3 renders faster
- __grid_y = function(y_intervals_) {
- c.lineWidth = 1;
- // Horizontal Lines
- c.strokeStyle = "rgb(200,200,200)";
- //XXX: consider Duff's device
- for (var i=y_intervals_.length-1; i>=0; i--) {
- c.beginPath();
- var _y = chart_height - y_intervals_[i];
- c._moveTo(0, _y);
- c._lineTo(width, _y);
- c.stroke();
- }
- };
- }
- return __grid_y(y_intervals_);
- }
- this.__grid_y = __grid_y;
- function __plot(x_scale_, y_scale_, points_) {
- if ($.browser.msie && c.setTransform) {
- //XXX: exception: IE6 renders more smoothly w/ setTransform
- __plot = function(x_scale_, y_scale_, points_) {
- c.lineWidth = PLOT_LINE_WIDTH;
- c.strokeStyle = "rgb(25,106,227)";
- c.lineJoin = "miter"; //XXX: don't use round (prob w/ seams)
- c.beginPath();
- // first point @ 0
- //XXX: fix seams: account for idx: -1
- c._moveTo(-1*x_scale_, (points_[-1] || 0)*y_scale_);
- c._lineTo(0, (points_[0] || 0)*y_scale_);
- //XXX: faster cleaner modified duff's device
- //XXX: ref: http://home.earthlink.net/~kendrasg/info/js_opt/jsOptMain.html
- var i = 1;
- var iter = points_.length-2;
- var n = iter % 8;
- while (n--) {
- c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
- }
- n = parseInt(iter/8, 10);
- while (n--) {
- c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, (points_[i] || 0)*y_scale_); i++;
- }
- // last point @ width
- c._lineTo(width, (points_[points_.length-1] || 0)*y_scale_);
- c._lineTo(width+1, (points_[points_.length-1] || 0)*y_scale_);
- c.stroke();
- c._lineTo(width+1, 0);
- //XXX: fix seams: account for idx: -1
- c._lineTo(-1*x_scale_, 0);
- c.fillStyle = "rgba(183,220,255, 0.4)";
- c.fill();
- };
- } else { //XXX: Safari,FF2 don't support setTransform; FF3 renders faster
- __plot = function(x_scale_, y_scale_, points_) {
- c.lineWidth = PLOT_LINE_WIDTH;
- c.strokeStyle = "rgb(25,106,227)";
- c.lineJoin = "miter"; //XXX: don't use round (prob w/ seams)
- c.beginPath();
- // first point @ 0
- //XXX: fix seams: account for idx: -1
- c._moveTo(-1*x_scale_, chart_height - (points_[-1] || 0)*y_scale_);
- c._lineTo(0, chart_height - (points_[0] || 0)*y_scale_);
- //XXX: faster cleaner modified duff's device
- //XXX: ref: http://home.earthlink.net/~kendrasg/info/js_opt/jsOptMain.html
- var i = 1;
- var iter = points_.length-2;
- var n = iter % 8;
- while (n--) {
- c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
- }
- n = parseInt(iter/8, 10);
- while (n--) {
- c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
- c._lineTo(i*x_scale_, chart_height - (points_[i] || 0)*y_scale_); i++;
- }
- // last point @ width
- c._lineTo(width, chart_height - (points_[points_.length-1] || 0)*y_scale_);
- c._lineTo(width+1, chart_height - (points_[points_.length-1] || 0)*y_scale_);
- c.stroke();
- c.fillStyle = "rgba(183,220,255, 0.4)";
- c._lineTo(width+1, chart_height);
- //XXX: fix seams: account for idx: -1
- c._lineTo(-1*x_scale_, chart_height);
- c.fill();
- };
- }
- return __plot(x_scale_, y_scale_, points_);
- }
- this.__plot = __plot;
- function __tape_marker(x_tick_, x_text_) {
- //TODO: consider: color code tape?
- // Set Text
- tape.append([
- '<div',
- 'class="tape-tick"',
- 'style="',
- 'position: absolute;',
- 'left: '+x_tick_+'px;',
- 'top: 0px;',
- 'z-index: 0;',
- 'width: '+width+'px;',
- '">'+x_text_+'</div>'].join(' '));
- }
- this.__tape_marker = __tape_marker;
- function _fetch_data() {
- //XXX: fix seams: grab idx: -1
- _data = model.fetch(
- parent.zoom_scale(),
- _data_offset-1,
- parent._pane_period()+1);
-
- // save min/max-y
- if (_data.length === 0) {
- _min_y = 0;
- _max_y = 0;
- return;
- }
- var i,j, _source;
- //XXX: fix seams: put idx: -1 at [-1]
- for (i=_data.length-1; i>=0; i--) {
- _source = _data[i];
- _source[-1] = _source.shift();
- }
- _min_y = _max_y = _data[0][0] || 0;
- for (i=_data.length-1; i>=0; i--) {
- _source = _data[i];
- for (j=_source.length-1; j>=0; j--) {
- var _y = _source[j] || 0;
- _min_y = Math.min(_min_y, _y);
- _max_y = Math.max(_max_y, _y);
- }
- }
- }
- function _draw() {
- // reset canvas
- c.clearRect(0, 0, width, draw_height);
- // reset tape
- tape.empty();
- // determine: scale
- var x_scale = parent._x_scale();
- var y_scale = parent._y_scale();
- // draw grid
- var _x_data = parent._x_tick()(time_offset);
- var _x_tick = (_x_data[0] / model.zoom_delta()) * x_scale;
- if (_x_tick < width) {
- __grid_x(_x_tick);
- // draw tape
- __tape_marker(_x_tick, _x_data[1]);
- }
- __grid_y(parent._y_intervals());
- // draw chart
- for (var i=_data.length-1;i>=0;i--) {
- __plot(x_scale, y_scale, _data[i]);
- }
- }
- this._draw = _draw;
- function _reset(data_offset_, offset_) {
- _data_offset = data_offset_;
- _style.left = offset_ + "px";
-
- time_offset = model.min_time() + _data_offset * model.zoom_delta();
- _fetch_data();
- }
- this._reset = _reset;
- function reset(data_offset_, offset_) {
- _reset(data_offset_, offset_);
- _draw();
- }
- this.reset = reset;
- function range_min_max_y(start_, end_) {
- if (_data.length === 0) {
- return [0, 0];
- }
- var min, max;
- min = max = _data[0][start_] || 0;
- for (var i=_data.length-1; i>=0; i--) {
- var _source = _data[i].slice(start_, end_);
- for (var j=_source.length-1; j>=0; j--) {
- var _y = _source[j] || 0;
- min = Math.min(min, _y);
- max = Math.max(max, _y);
- }
- }
- return [min, max];
- }
- this.range_min_max_y = range_min_max_y;
- };
- // ---------- Navigation Pane ----------
- NAV_HEIGHT = 75;
- NAV_BUTTON_HEIGHT = 13;
- NAV_BUTTON_WIDTH = 16;
- NAV_SCROLL_RATE = 1000 * 0.1;
- NAV_SCROLL_SPEED = 50;
- NAV_SCROLL_MIN_WIDTH = 20;
- NAV_SCROLL_MID_THRESHOLD = 60;
- NAV_OVERVIEW_TOP_OFFSET = 5;
- NAV_OVERVIEW_SCALE_FACTOR = 1.1;
- NAV_RANGE_OFFSET = -9;
- NAV_RANGE_WIDTH = 13;
- NAV_RANGE_CENTER_THRESHOLD = 13;
- NAV_RANGE_CENTER_OFFSET = -8;
- var NavigationPane = function(parent_) {
- // Initialize
- var
- parent = parent_,
- model = parent.model(),
- broker = parent.broker(),
- width = 0,
- height = 0,
- container = null,
- nav_left = null,
- nav_right = null,
- nav_bar = null,
- zoom_level = null,
- overview_left = 0,
- overview = null,
- oc = null,
- preview = null,
- _preview = null,
- pc = null,
- _data = [],
- _x_scale = 0,
- _y_scale = 0,
- viewer = null,
- _viewer = null,
- viewer_width = 0,
- slider = null,
- _slider = null,
- slider_middle = null,
- drag_modifier = 1,
- range_min = null,
- _range_min = null,
- range_max = null,
- _range_max = null,
- __is_hover__ = false,
- __is_drag_min__ = false;
-
- this.parent = function() { return parent; };
- this.model = function() { return model; };
- this.width = function() { return width; };
- this.height = function() { return height; };
- this.container = function() { return container; };
- this.zoom_level = function() { return zoom_level; };
- this.overview_left = function() { return overview_left; };
- this.overview = function() { return overview; };
- this.oc = function() { return oc; };
- this.preview = function() { return preview; };
- this._preview = function() { return _preview; };
- this.pc = function() { return pc; };
- this._data = function() { return _data; };
- this._x_scale = function() { return _x_scale; };
- this._y_scale = function() { return _y_scale; };
- this.viewer = function() { return viewer; };
- this._viewer = function() { return _viewer; };
- this.viewer_width = function() { return viewer_width; };
- this.slider = function() { return slider; };
- this._slider = function() { return _slider; };
- this.slider_middle = function() { return slider_middle; };
- this.drag_modifier = function() { return drag_modifier; };
- this.range_min = function() { return range_min; };
- this._range_min = function() { return _range_min; };
- this.range_max = function() { return range_max; };
- this._range_max = function() { return _range_max; };
- /* Broker Channels:
- /nav/center (re-)center nav
- /nav/translate translate nav
- */
- // Helper Methods
- function _render_overview(context_, strokeStyle_, fillStyle_, boxStrokeStyle_) {
- // reset canvas
- context_.clearRect(0, 0, overview.width, overview.height);
- context_.lineWidth = 1;
- context_.strokeStyle = strokeStyle_;
- context_.lineJoin = "round";
- context_.beginPath();
- // last point @ width
- context_._moveTo(overview.width, overview.height - (_data[_data.length-2] || 0) * _y_scale);
-
- // draw overview
- for (var i=_data.length-3; i>0; i--) {
- context_._lineTo(i*_x_scale, overview.height - (_data[i] || 0) * _y_scale);
- }
- // first point @ 0
- context_._lineTo(
- 0,
- overview.height - (_data[0] || 0) * _y_scale);
-
- context_.stroke();
- // fill overview
- context_.fillStyle = fillStyle_;
- context_._lineTo(0, overview.height);
- context_._lineTo(overview.width, overview.height);
- context_.fill();
- // draw outline
- context_.lineWidth = 1;
- context_.strokeStyle = boxStrokeStyle_;
- context_.lineJoin = "round";
- context_.beginPath();
- context_.moveTo(0, overview.height);
- context_.lineTo(0, 0);
- context_.lineTo(overview.width, 0);
- context_.lineTo(overview.width, overview.height);
- context_.stroke();
- }
- // Setup
- function init(target_, width_, height_, top_, left_) {
- // Setup Variables
- width = width_;
- height = height_;
- // Container
- container = $([
- '<div',
- 'style="',
- 'position: absolute;',
- 'left: '+left_+'px;',
- 'top: '+top_+'px;',
- 'width: '+width+'px;',
- 'height: '+height+'px;',
- '"></div>'].join(' ')).appendTo(target_);
-
- // Navigation Scroll Bar
- nav_left = $([
- '<div',
- 'class="nav-left-unsel"',
- 'style="',
- 'position: absolute;',
- 'overflow: hidden;',
- 'left: '+(-BORDER_OFFSET_X)+'px;',
- 'top: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
- 'width: '+NAV_BUTTON_WIDTH+'px;',
- 'height: '+NAV_BUTTON_HEIGHT+'px;',
- '"></div>'].join(' ')).appendTo(container);
- nav_right = $([
- '<div',
- 'class="nav-unsel nav-right-unsel"',
- 'style="',
- 'position: absolute;',
- 'overflow: hidden;',
- 'left: '+(-BORDER_OFFSET_X+NAV_BUTTON_WIDTH)+'px;',
- 'top: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
- 'width: '+NAV_BUTTON_WIDTH+'px;',
- 'height: '+NAV_BUTTON_HEIGHT+'px;',
- '"></div>'].join(' ')).appendTo(container);
- nav_bar = $([
- '<div',
- 'style="',
- 'position: absolute;',
- 'left: '+(BORDER_OFFSET_X+2*NAV_BUTTON_WIDTH)+'px;',
- 'top: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
- 'width: '+(width-BORDER_OFFSET_X-2*NAV_BUTTON_WIDTH)+'px;',
- 'height: '+(NAV_BUTTON_HEIGHT)+'px;',
- 'font-size: 0px;', // hack: IE6 fix
- 'border-top: 1px solid #aaa;',
- 'border-right: 1px solid #aaa;',
- 'border-bottom: 1px solid #aaa;',
- '"></div>'].join(' ')).appendTo(container);
- // Overview
- //XXX: build manually (excanvas r3 workaround)
- overview = document.createElement("canvas");
- overview.width = width-2*NAV_BUTTON_WIDTH;
- overview.height = NAV_HEIGHT-NAV_BUTTON_HEIGHT-NAV_OVERVIEW_TOP_OFFSET;
-
- overview_left = BORDER_OFFSET_X+2*NAV_BUTTON_WIDTH;
- var _overview = $(overview);
- _overview.css("position", "absolute");
- _overview.css("left", overview_left+"px");
- _overview.css("top", NAV_OVERVIEW_TOP_OFFSET+"px");
- _overview.css("z-index", "0");
- _overview.css("width", overview.width+"px");
- _overview.css("height", overview.height+"px");
- _overview.appendTo(container);
- if ($.browser.msie) { //XXX: hack: excanvas
- overview = window.G_vmlCanvasManager.initElement(overview);
- }
- oc = overview.getContext("2d");
- decorate_context(oc);
- // Draw Overview
- var _zoom_idx = model.zoom_schema().length - 1;
- zoom_level = model.zoom_schema()[_zoom_idx];
- _data = model.fetch(
- _zoom_idx,
- 0,
- Math.ceil(model._time_range() / zoom_level[0]));
- _data = _data[0] || []; //XXX: use first data source for overview
- // Calculate X-coord Scale
- _x_scale = overview.width / (_data.length - 1);
- // Calculate Y-coord Scale
- //XXX: optional: make it logarithmic?
- //_data = _map(function (x) { return Math.log(x || 1); }, _data);
- var _min_y, _max_y, _point;
- _min_y = _max_y = _data[_data.length-1] || 0;
- for (var i=_data.length-2; i>=0; i--) {
- _point = _data[i] || 0;
- _min_y = _point < _min_y ? _point : _min_y;
- _max_y = _point > _max_y ? _point : _max_y;
- }
- _max_y = (_max_y - _min_y) * NAV_OVERVIEW_SCALE_FACTOR + _min_y;
-
- _y_scale = (overview.height - 1) / (_max_y - _min_y);
- _render_overview(oc, "rgb(225,225,225)", "rgba(200,200,200,0.2)", "rgb(200,200,200)");
- // Slider
- slider = $([
- '<div',
- 'class="nav-slider"',
- 'style="',
- 'display: none;',
- 'position: absolute;',
- 'overflow: hidden;',
- 'left: '+overview_left+'px;',
- 'top: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
- 'width: 10px;',
- 'height: '+NAV_BUTTON_HEIGHT+'px;',
- 'text-align: center;',
- '"></div>'].join(' ')).appendTo(container);
- _slider = slider[0];
- slider.append([
- '<img',
- 'src="/assets/slider.left.gif"',
- 'style="',
- 'position: absolute;',
- 'top: 0px;',
- 'left: 0px;',
- '"/>'].join(' '));
- slider.append([
- '<img',
- 'src="/assets/slider.right.gif"',
- 'style="',
- 'position: absolute;',
- 'top: 0px;',
- 'right: 0px;',
- '"/>'].join(' '));
- slider_middle = $([
- '<img',
- 'src="/assets/slider.middle.gif"',
- 'style="',
- 'display: none;',
- '"/>'].join(' ')).appendTo(slider);
- // Viewer
- viewer = $([
- '<div',
- 'class="nav-viewer"',
- 'style="',
- 'position: absolute;',
- 'left: '+overview_left+'px;',
- 'top: 0px;',
- 'width: 1px;',
- 'height: '+(NAV_HEIGHT-NAV_BUTTON_HEIGHT)+'px;',
- '"></div>'].join(' ')).appendTo(container);
- _viewer = viewer[0];
- //XXX: hide preview, but allow range controls to overflow
- var internal = $([
- '<div',
- 'style="',
- 'position: absolute;',
- 'overflow: hidden;',
- 'left: 0px;',
- 'top: 0px;',
- 'width: 100%;',
- 'height: 100%;',
- '"><div>'].join(' ')).appendTo(viewer);
- // Preview
- //XXX: build manually (excanvas r3 workaround)
- preview = document.createElement("canvas");
- preview.width = overview.width;
- preview.height = overview.height;
- _preview = $(preview);
- _preview.css("position", "absolute");
- _preview.css("left", (-BORDER_OFFSET_X)+"px");
- _preview.css("top", NAV_OVERVIEW_TOP_OFFSET+"px");
- _preview.css("width", preview.width+"px");
- _preview.css("height", preview.height+"px");
- _preview.css("z-index", "0");
- _preview.css("border-bottom", "1px solid #aaa");
- _preview.appendTo(internal);
- if ($.browser.msie) { //XXX: hack: excanvas
- preview = window.G_vmlCanvasManager.initElement(preview);
- }
- pc = preview.getContext("2d");
- decorate_context(pc);
- _render_overview(pc, "rgb(25,106,227)", "rgba(25,106,227,0.1)", "rgb(200,200,200)");
- //XXX: ie6 doesn't render in a hidden DOM element
- viewer.css("display", "none");
- // View Range Controls
- range_min = $([
- '<img',
- 'src="/assets/range.gif"',
- 'style="',
- 'position: absolute;',
- 'top: '+Math.floor(preview.height/2)+'px;',
- 'left: '+NAV_RANGE_OFFSET+'px;',
- 'cursor: col-resize;',
- '"/>'].join(' ')).appendTo(viewer);
- _range_min = range_min[0];
- range_max = $([
- '<img',
- 'src="/assets/range.gif"',
- 'style="',
- 'position: absolute;',
- 'top: '+Math.floor(preview.height/2)+'px;',
- 'right: '+NAV_RANGE_OFFSET+'px;',
- 'cursor: col-resize;',
- '"/>'].join(' ')).appendTo(viewer);
- _range_max = range_max[0];
- __is_hover__ = true; //XXX: initially display range controls
- // --- DOM Events
- // ----- Left/Right Navigation Buttons
- addEvent(nav_left[0], "mousedown", nav_left_mousedown);
- addEvent(nav_right[0], "mousedown", nav_right_mousedown);
- // ----- Preview Slider
- addEvent(_slider, "mousedown", drag_init);
- _slider.onmousedown = function(e) { return cancelEvent(e); };
- // ----- Preview Pane
- addEvent(_viewer, "mousedown", drag_init);
- _viewer.onmousedown = function(e) { return cancelEvent(e); };
-
- // ----- Preview Range Controls
- container.hover(container_mouseover, container_mouseout);
- addEvent(_range_min, "mousedown", range_min_drag_init);
- _range_min.onmousedown = function(e) { return cancelEvent(e); };
- addEvent(_range_max, "mousedown", range_max_drag_init);
- _range_max.onmousedown = function(e) { return cancelEvent(e); };
- // Events Setup
- broker.subscribe("/nav/center", this, "center");
- broker.subscribe("/nav/translate", this, "translate");
- broker.subscribe("/nav/range/update", this, "update_range");
- //XXX: post init
- broker.subscribe("/nav/center", this, "post_init");
- }
- this.init = init;
- function post_init() {
- broker.unsubscribe("/nav/center", this, "post_init");
- viewer.css("display", "inline");
- slider.css("display", "inline");
- }
- this.post_init = post_init;
- // View Translation Methods
- function center() {
- // static variables
- var _overview_width = overview.width - 2; //XXX: account for border-right/left
- center = function() {
- // calculate: width
- var _num_points = parent._chart_period() * model.zoom_delta() / zoom_level[0];
- viewer_width =
- Math.max(1,
- Math.min(_overview_width,
- Math.floor(_num_points * _x_scale)));
- _viewer.style.width = viewer_width+"px";
- _slider.style.width = Math.max(NAV_SCROLL_MIN_WIDTH, viewer_width)+"px";
-
- if (viewer_width > NAV_SCROLL_MID_THRESHOLD) {
- slider_middle.css("display", "inline");
- } else {
- slider_middle.css("display", "none");
- }
- // translate
- translate(); //XXX: don't publish, has to be in-sync (otherwise, flickers)
- // update: drag modifier
- drag_modifier = (zoom_level[0] / model.zoom_delta()) * parent._x_scale() / _x_scale;
- // range controls: re-position, if necessary
- var _viewer_width = viewer.width();
- if (_viewer_width > NAV_RANGE_CENTER_THRESHOLD) {
- _range_min.style.left = NAV_RANGE_OFFSET+"px";
- if (__is_hover__) {
- range_max.css("display", "inline");
- }
- } else { // viewer.width <= range control width
- _range_min.style.left =
- Math.floor(_viewer_width/2+NAV_RANGE_CENTER_OFFSET)+"px";
- range_max.css("display", "none");
- }
- };
- return center();
- }
- this.center = center;
- function translate() {
- // static variables
- var _half_width = parent.width() / 2;
- var _overview_width = overview.width - 2;
- var _overview_max = _overview_width + overview_left;
- translate = function() {
- // shared references
- var _min_pixel = parent.min_pixel();
- var _max_pixel = parent.max_pixel();
- var _center_pixel = parent.center_pixel();
- var _real_min_pixel = _min_pixel - _half_width;
- var _real_max_pixel = _max_pixel + _half_width;
- // calculate: offset
- var _offset;
- if (_center_pixel < _max_pixel) {
- _offset = overview_left;
- if (_center_pixel > _min_pixel) {
- var _pixel_range = _real_max_pixel - _real_min_pixel;
- _offset +=
- (_center_pixel - _real_min_pixel) / _pixel_range * _overview_width;
- _offset -= (viewer_width / 2);
- _offset = Math.round(_offset);
- } // else: center_pixel == min_pixel
- } else { // center_pixel == max_pixel
- _offset = _overview_max - viewer_width;
- }
- //XXX: correct out-of-bounds
- _offset = Math.min((_overview_max - viewer_width), _offset);
- _offset = Math.max(overview_left, _offset);
- // position view and set width
- preview.style.left = (overview_left-_offset)+"px";
- _viewer.style.left = _offset+"px";
- if (viewer_width < NAV_SCROLL_MIN_WIDTH) {
- _offset -= NAV_SCROLL_MIN_WIDTH - viewer_width;
- }
- _slider.style.left =
- Math.max(overview_left, _offset)+"px";
- };
- return translate();
- }
- this.translate = translate;
- // DOM Event Methods
- // --- Left/Right Navigation
- function nav_left_mousedown(e) {
- nav_right[0].className = "nav-right-unsel nav-sel";
- nav_left[0].className = "nav-left-sel";
- addEvent(document, "mouseup", nav_left_mouseup);
- broker.publish("/chart/draw/start"); // start draw loop
- broker.publish("/chart/scroll/start", 1); // start scroll loop
- }
- function nav_left_mouseup(e) {
- nav_right[0].className = "nav-right-unsel nav-unsel";
- nav_left[0].className = "nav-left-unsel";
- removeEvent(document, "mouseup", nav_left_mouseup);
- broker.publish("/chart/draw/stop"); // stop draw loop
- broker.publish("/chart/scroll/stop"); // stop scroll loop
- }
- function nav_right_mousedown(e) {
- nav_right[0].className = "nav-right-sel nav-sel";
- addEvent(document, "mouseup", nav_right_mouseup);
- broker.publish("/chart/draw/start"); // start draw loop
- broker.publish("/chart/scroll/start", -1); // start scroll loop
- }
- function nav_right_mouseup(e) {
- nav_right[0].className = "nav-right-unsel nav-unsel";
- removeEvent(document, "mouseup", nav_right_mouseup);
- broker.publish("/chart/draw/stop"); // stop draw loop
- broker.publish("/chart/scroll/stop"); // stop scroll loop
- }
- // --- Preview Pane
- function drag_init(e) {
- e = e ? e : window.event;
-
- addEvent(document, "mousemove", drag_move);
- viewer._drag_x = e.clientX;
- addEvent(document, "mouseup", drag_end);
- broker.publish("/chart/draw/start"); // start draw loop
- }
- function drag_move(e) {
- e = e ? e : window.event;
- var delta = e.clientX - viewer._drag_x;
- if (Math.abs(delta) <= 1) {
- return cancelEvent(e);
- }
- viewer._drag_x = e.clientX;
- delta = -1 * Math.floor(drag_modifier * delta);
- broker.publish("/chart/translate", delta);
- return cancelEvent(e);
- }
- function drag_end(e) {
- removeEvent(document, "mousemove", drag_move);
- removeEvent(document, "mouseup", drag_end);
- broker.publish("/chart/draw/stop"); // stop draw loop
- }
- // Preview Range Control
- function container_mouseover(e) {
- __is_hover__ = true;
- range_min.css("display", "inline");
- if (viewer.width() > NAV_RANGE_CENTER_THRESHOLD) {
- range_max.css("display", "inline");
- }
- }
- function container_mouseout(e) {
- __is_hover__ = false;
- range_min.css("display", "none");
- range_max.css("display", "none");
- }
- function range_min_drag_init(e) {
- __is_drag_min__ = true;
- _range_drag_init(e);
- }
- function range_max_drag_init(e) {
- __is_drag_min__ = false;
-
- _range_drag_init(e);
- }
- function _range_drag_init(e) {
- e = e ? e : window.event;
- addEvent(document, "mousemove", range_drag_move);
- viewer._range_x = e.clientX;
- addEvent(document, "mouseup", range_drag_end);
- broker.publish("/chart/draw/start"); // start draw loop
- }
- function update_range(modified, fixed) {
- // static variables
- var _overview_width = overview.width-2;
- var _zoom_schema = model.zoom_schema();
- var _visible_panes = parent.width() / parent._pane_width();
- var _half_width = Math.floor(parent.width() / 2);
- update_range = function(modified, fixed) {
- // sanitize parameters
- var _min = Math.min(modified, fixed);
- var _max = Math.max(modified, fixed);
- var _min_fixed = _min === fixed ? true : false;
- var _rel_min = Math.max(0, _min - overview_left);
- var _rel_max = Math.min(_overview_width, _max - overview_left);
- // calculate new time
- var _rel_range = (_rel_max - _rel_min) / _overview_width;
- var _time_range = Math.floor(model._time_range() * _rel_range);
-
- // determine new zoom/period
- var _zoom_level, _zoom_scale;
- for (var i=0; i<_zoom_schema.length; i++) {
- _zoom_scale = i;
- _zoom_level = _zoom_schema[_zoom_scale];
- if ((_zoom_level[0] * _zoom_level[2] * _visible_panes) >= _time_range) {
- break;
- }
- }
- var _pane_period, _zoom_time_range;
- for (i=_zoom_level[1]; i<=_zoom_level[2]; i++) {
- _pane_period = i;
- _zoom_time_range = _zoom_level[0] * i * _visible_panes;
- if (_zoom_time_range >= _time_range) {
- break;
- }
- }
- var _half_time = _zoom_time_range / 2;
- var _time_offset;
- if (_min_fixed) {
- _time_offset = model.min_time();
- _time_offset += model._time_range() * (_rel_min / _overview_width);
- _time_offset += _half_time;
- } else {
- _time_offset = model.min_time();
- _time_offset += model._time_range() * (_rel_max / _overview_width);
- _time_offset -= _half_time;
- }
- _time_offset = Math.floor(_time_offset);
- parent._update_zoom(_zoom_scale, _pane_period);
- parent.center(_time_offset, _half_width);
- };
- return update_range(modified, fixed);
- }
- this.update_range = update_range;
- function range_drag_move(e) {
- e = e ? e : window.event;
- if (Math.abs(e.clientX - viewer._range_x) <= 1) {
- return cancelEvent(e);
- }
- viewer._range_x = e.clientX;
- var _viewer_left = parseInt(_viewer.style.left, 10);
- var delta;
- //XXX: use a global to switch, because events can be queued up
- if (__is_drag_min__) { // dragging range min
- delta = viewer.offset().left - e.clientX;
- var _new_min = _viewer_left-delta;
- var _max = _viewer_left+viewer.width();
- if (_new_min > _max) {
- __is_drag_min__ = false;
- return cancelEvent(e);
- }
- broker.publish("/nav/range/update", _new_min, _max);
- } else { // draggging range max
- var _viewer_width = viewer.width();
- delta = (viewer.offset().left+_viewer_width) - e.clientX;
- var _new_max = _viewer_left+_viewer_width-delta;
- if (_new_max < _viewer_left) {
- __is_drag_min__ = true;
- return cancelEvent(e);
- }
- broker.publish("/nav/range/update", _new_max, _viewer_left);
- }
- return cancelEvent(e);
- }
- function range_drag_end(e) {
- removeEvent(document, "mousemove", range_drag_move);
- removeEvent(document, "mouseup", range_drag_end);
- broker.publish("/chart/draw/stop"); // stop draw loop
- }
- };
- // ---------- Chart ----------
- var LARGE_CHART_THRESHOLD_WIDTH = 720; // target: 720px x 6 panes @ 120px
- var LARGE_CHART_NUM_PANES = 6;
- var SMALL_CHART_NUM_PANES = 3; // target: 180px x 3 panes @ 60px
- var BORDER_OFFSET_X = 1;
- var BORDER_OFFSET_Y = 1;
- var TOP_PANE_HEIGHT = 25;
- var TOP_PANE_BORDER_COLOR = "rgb(175,175,175)"; //XXX: consider: 150
- var UPDATE_DELTA_FACTOR = 0.5; //XXX: speed at which updates will improve
- var UPDATE_BOUND = 1000 / 25; //XXX: lower bound on re-draw interval
- var UPDATE_AFTER_STOP = 1000 * 0.5; //XXX: allow a second between stop/start drag
- if ($.browser.msie) { // IE6 needs help
- UPDATE_BOUND = 4 * 1000;
- UPDATE_AFTER_STOP = 1000;
- }
- var Y_SCALE_FACTOR = 1.10;
- var Y_LABELS_WIDTH = 100;
- var Y_LABELS_RIGHT_OFFSET = 4;
- var Y_LABEL_OFFSET = -12;
- var Y_LABEL = "W"; //XXX: make this a parameter
- var MOUSE_SCROLL_ZOOM_FACTOR = 0.5;
- var MOUSE_SCROLL_ZOOM_DELTA_MAX = 2;
- if ($.browser.msie) { // IE6 needs help
- MOUSE_SCROLL_ZOOM_FACTOR = 0.75;
- }
- var TICK_MARK_INCREMENTS = [
- 60,
- 60*5,
- 60*10,
- 60*15,
- 60*30,
- 60*60,
- 60*60*3,
- 60*60*6,
- 60*60*12,
- 60*60*24,
- 60*60*24*7,
- 60*60*24*14,
- 60*60*24*31
- ];
- //FIXME: take into account: Date.getTimezoneOffset() [returns offset in minutes]
- var TICK_DATE_PROXY = new Date();
- var _format_date = function(date_) {
- if (date_.getSeconds() !== 0) {
- return formatDate(date_, "h:mm:ss a");
- }
- if (date_.getMinutes() !== 0) {
- return formatDate(date_, "h:mm a");
- }
-
- if (date_.getHours() !== 0) {
- return formatDate(date_, "h a");
- }
- if (date_.getDay() !== 1) {
- return formatDate(date_, "MMM d");
- }
- if (date_.getMonth() !== 0) {
- return formatDate(date_, "MMM");
- }
- return formatDate(date_, "yyyy");
- };
- ;;; $c._format_date = _format_date;
- var TICK_1_MINUTE = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- if (TICK_DATE_PROXY.getSeconds() !== 0) {
- TICK_DATE_PROXY.setSeconds(60);
- }
- return [
- (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_1_MINUTE = TICK_1_MINUTE;
- var TICK_10_MINUTES = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- var _min = TICK_DATE_PROXY.getMinutes();
- var _min_mod = _min % 10;
- if (TICK_DATE_PROXY.getSeconds() !== 0 || _min_mod !== 0) {
- TICK_DATE_PROXY.setSeconds(0);
- TICK_DATE_PROXY.setMinutes(_min + (10 - _min_mod));
- }
- return [
- (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_10_MINUTES = TICK_10_MINUTES;
- var TICK_5_MINUTES = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- var _min = TICK_DATE_PROXY.getMinutes();
- var _min_mod = _min % 5;
- if (TICK_DATE_PROXY.getSeconds() !== 0 || _min_mod !== 0) {
- TICK_DATE_PROXY.setSeconds(0);
- TICK_DATE_PROXY.setMinutes(_min + (5 - _min_mod));
- }
- return [
- (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_5_MINUTES = TICK_5_MINUTES;
- var TICK_15_MINUTES = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- var _min = TICK_DATE_PROXY.getMinutes();
- var _min_mod = _min % 15;
- if (TICK_DATE_PROXY.getSeconds() !== 0 || _min_mod !== 0) {
- TICK_DATE_PROXY.setSeconds(0);
- TICK_DATE_PROXY.setMinutes(_min + (15 - _min_mod));
- }
- return [
- (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_15_MINUTES = TICK_15_MINUTES;
- var TICK_30_MINUTES = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- var _min = TICK_DATE_PROXY.getMinutes();
- var _min_mod = _min % 30;
- if (TICK_DATE_PROXY.getSeconds() !== 0 || _min_mod !== 0) {
- TICK_DATE_PROXY.setSeconds(0);
- TICK_DATE_PROXY.setMinutes(_min + (30 - _min_mod));
- }
- return [
- (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_30_MINUTES = TICK_30_MINUTES;
- var TICK_1_HOUR = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- if (TICK_DATE_PROXY.getSeconds() !== 0 || TICK_DATE_PROXY.getMinutes() !== 0) {
- TICK_DATE_PROXY.setSeconds(0);
- TICK_DATE_PROXY.setMinutes(0);
- TICK_DATE_PROXY.setHours(TICK_DATE_PROXY.getHours() + 1);
- }
- return [
- (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_1_HOUR = TICK_1_HOUR;
- var TICK_3_HOURS = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- var _hour = TICK_DATE_PROXY.getHours();
- var _hour_mod = _hour % 3;
- if (TICK_DATE_PROXY.getSeconds() !== 0 || TICK_DATE_PROXY.getMinutes() !== 0 || _hour_mod !== 0) {
- TICK_DATE_PROXY.setSeconds(0);
- TICK_DATE_PROXY.setMinutes(0);
- TICK_DATE_PROXY.setHours(_hour + (3 - _hour_mod));
- }
- return [
- (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_3_HOURS = TICK_3_HOURS;
- var TICK_6_HOURS = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- var _hour = TICK_DATE_PROXY.getHours();
- var _hour_mod = _hour % 6;
- if (TICK_DATE_PROXY.getSeconds() !== 0 || TICK_DATE_PROXY.getMinutes() !== 0 || _hour_mod !== 0) {
- TICK_DATE_PROXY.setSeconds(0);
- TICK_DATE_PROXY.setMinutes(0);
- TICK_DATE_PROXY.setHours(_hour + (6 - _hour_mod));
- }
- return [
- (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_6_HOURS = TICK_6_HOURS;
- var TICK_12_HOURS = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- var _hour = TICK_DATE_PROXY.getHours();
- var _hour_mod = _hour % 12;
- if (TICK_DATE_PROXY.getSeconds() !== 0 || TICK_DATE_PROXY.getMinutes() !== 0 || _hour_mod !== 0) {
- TICK_DATE_PROXY.setSeconds(0);
- TICK_DATE_PROXY.setMinutes(0);
- TICK_DATE_PROXY.setHours(_hour + (12 - _hour_mod));
- }
- return [
- (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_12_HOURS = TICK_12_HOURS;
- var TICK_1_DAY = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- if (TICK_DATE_PROXY.getSeconds() !== 0 || TICK_DATE_PROXY.getMinutes() !== 0 || TICK_DATE_PROXY.getHours() !== 0) {
- TICK_DATE_PROXY.setSeconds(0);
- TICK_DATE_PROXY.setMinutes(0);
- TICK_DATE_PROXY.setHours(24);
- }
- return [
- (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_1_DAY = TICK_1_DAY;
- var WEEK_IN_SECS = 60*60*24*7;
- var TICK_1_WEEK = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- //XXX: calc from start of year
- TICK_DATE_PROXY.setSeconds(0);
- TICK_DATE_PROXY.setMinutes(0);
- TICK_DATE_PROXY.setHours(0);
- TICK_DATE_PROXY.setMonth(0, 1);
- var _year_start = TICK_DATE_PROXY.getTime() / 1000;
- var _delta = timestamp_ - _year_start;
- return [
- WEEK_IN_SECS - (_delta % WEEK_IN_SECS),
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_1_WEEK = TICK_1_WEEK;
- var TWO_WEEKS_IN_SECS = 2*WEEK_IN_SECS;
- var TICK_2_WEEKS = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- //XXX: calc from start of year
- TICK_DATE_PROXY.setSeconds(0);
- TICK_DATE_PROXY.setMinutes(0);
- TICK_DATE_PROXY.setHours(0);
- TICK_DATE_PROXY.setMonth(0, 1);
- var _year_start = TICK_DATE_PROXY.getTime() / 1000;
- var _delta = timestamp_ - _year_start;
- return [
- TWO_WEEKS_IN_SECS - (_delta % TWO_WEEKS_IN_SECS),
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_2_WEEKS = TICK_2_WEEKS;
- var TICK_1_MONTH = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- if (TICK_DATE_PROXY.getSeconds() !== 0 || TICK_DATE_PROXY.getMinutes() !== 0 || TICK_DATE_PROXY.getHours() !== 0 || TICK_DATE_PROXY.getDay() !== 1 || TICK_DATE_PROXY.getMonth() !== 0) {
- TICK_DATE_PROXY.setSeconds(0);
- TICK_DATE_PROXY.setMinutes(0);
- TICK_DATE_PROXY.setHours(0);
- TICK_DATE_PROXY.setMonth(TICK_DATE_PROXY.getMonth()+1, 1);
- }
- return [
- (TICK_DATE_PROXY.getTime() / 1000) - timestamp_,
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_1_MONTH = TICK_1_MONTH;
- var TICK_OVERFLOW = function(timestamp_) {
- TICK_DATE_PROXY.setTime(timestamp_*1000);
- return [
- 0,
- _format_date(TICK_DATE_PROXY)
- ];
- };
- ;;; $c.TICK_OVERFLOW = TICK_OVERFLOW;
- var TICK_MARK_SELECTORS = {};
- TICK_MARK_SELECTORS[60] = TICK_1_MINUTE;
- TICK_MARK_SELECTORS[60*5] = TICK_5_MINUTES;
- TICK_MARK_SELECTORS[60*10] = TICK_10_MINUTES;
- TICK_MARK_SELECTORS[60*15] = TICK_15_MINUTES;
- TICK_MARK_SELECTORS[60*30] = TICK_30_MINUTES;
- TICK_MARK_SELECTORS[60*60] = TICK_1_HOUR;
- TICK_MARK_SELECTORS[60*60*3] = TICK_3_HOURS;
- TICK_MARK_SELECTORS[60*60*6] = TICK_6_HOURS;
- TICK_MARK_SELECTORS[60*60*12] = TICK_12_HOURS;
- TICK_MARK_SELECTORS[60*60*24] = TICK_1_DAY;
- TICK_MARK_SELECTORS[60*60*24*7] = TICK_1_WEEK;
- TICK_MARK_SELECTORS[60*60*24*14] = TICK_2_WEEKS;
- TICK_MARK_SELECTORS[60*60*24*31] = TICK_1_MONTH;
- ;;; $c.TICK_MARK_SELECTORS = TICK_MARK_SELECTORS;
- var _bisect = function(list_, target_) {
- //XXX: warning: returns list_.length, if target_ higher than last element
- var lo = 0;
- var hi = list_.length;
- while (lo < hi) {
- var mid = Math.floor((lo + hi) / 2);
- if (list_[mid] < target_) {
- lo = mid+1;
- } else {
- hi = mid;
- }
- }
- return lo;
- };
- ;;; $c._bisect = _bisect;
- var ZOOM_POINTS = [
- [60*5, '5m'],
- [60*30, '30m'],
- [60*60, '1h'],
- [60*60*6, '6h'],
- [60*60*12, '12h'],
- [60*60*24, '1d'],
- [60*60*24*7, '1w'],
- [60*60*24*7*2, '2w'],
- [60*60*24*31, '1M'],
- [60*60*24*31*3, '3M']
- // max
- ];
- var Chart = function(target_) {
- // Initialize
- var
- target = $(target_),
- model = null,
- container = null,
- y_labels = null,
- width = 0,
- max_left = 0,
- max_right = 0,
- height = 0,
- top_pane = null,
- _top_range = null,
- _top_range_span = null,
- _top_point = null,
- _top_point_time = null,
- _top_point_val = null,
- _overlay_line = null,
- _is_mouseover = false,
- zoom_scale = 0, // zoom schema index [1m, 5m, 30m, 3h, 12h]
- _pane_period = 1,
- _pane_width = 0,
- max_y = 0,
- min_y = 0,
- _x_scale = 0,
- _x_tick = TICK_OVERFLOW,
- _y_scale = 0,
- _y_intervals = [],
- panes = [],
- bg = null,
- draw_width = 0,
- draw_height = 0,
- chart_height = 0,
- broker = new PubSubBroker(),
- updating = false,
- update_delta = 0,
- update_id = undefined,
- update_time = 0,
- center_pixel = 0,
- min_pixel = 0,
- max_pixel = 0,
- nav = null,
- scroll_rate = 0,
- scrolling = false,
- scroll_id = 0;
-
- this.target = function() { return target; };
- this.model = function() { return model; };
- this.container = function() { return container; };
- this.y_labels = function() { return y_labels; };
- this.width = function() { return width; };
- this.max_left = function() { return max_left; };
- this.max_right = function() { return max_right; };
- this.height = function() { return height; };
- this.top_pane = function() { return top_pane; };
- this.zoom_scale = function() { return zoom_scale; };
- this._pane_period = function() { return _pane_period; };
- this._pane_width = function() { return _pane_width; };
- this.max_y = function() { return max_y; };
- this.min_y = function() { return min_y; };
- this._x_scale = function() { return _x_scale; };
- this._x_tick = function() { return _x_tick; };
- this._y_scale = function() { return _y_scale; };
- this._y_intervals = function() { return _y_intervals; };
- this.panes = function() { return panes; };
- this.bg = function() { return bg; };
- this.draw_width = function() { return draw_width; };
- this.draw_height = function() { return draw_height; };
- this.chart_height = function() { return chart_height; };
- this.broker = function() { return broker; };
- this.updating = function() { return updating; };
- this.update_delta = function() { return update_delta; };
- this.update_id = function() { return update_id; };
- this.update_time = function() { return update_time; };
- this.center_pixel = function() { return center_pixel; };
- this.min_pixel = function() { return min_pixel; };
- this.max_pixel = function() { return max_pixel; };
- this.nav = function() { return nav; };
- this.scroll_id = function() { return scroll_id; };
- /* Broker Channels:
- /chart/center (re-)center chart
- /chart/translate translate chart by delta
- /chart/recycle recycle panes
- /chart/replace replace panes
- /chart/draw/start start draw loop
- /chart/draw/stop stop draw loop
- /chart/draw/rescale rescale y-axis
- /chart/scroll/start start scroll loop
- /chart/scroll/stop stop scroll loop
- /info/range/update update range (in top pane)
- /info/point/update update point (in top pane)
- */
- // Helper
- function _chart_period() {
- //XXX: consider: better memoization
- var _visible_panes = width / _pane_width;
- var map = {};
- for (var i=_pane_width; i>0; i--) {
- map[i] = _visible_panes * i;
- }
- _chart_period = function() {
- return map[_pane_period];
- };
- return _chart_period();
- }
- this._chart_period = _chart_period;
- function _calc_x_scale() {
- // x-coord scale
- _x_scale = _pane_width / _pane_period;
- var _idx = _bisect(TICK_MARK_INCREMENTS, model.zoom_delta() * _pane_period);
- var _tick_func = TICK_MARK_SELECTORS[TICK_MARK_INCREMENTS[_idx]];
- if (_tick_func) {
- _x_tick = _tick_func;
- } else {
- _x_tick = TICK_OVERFLOW;
- }
- }
- this._calc_x_scale = _calc_x_scale;
- function _min_max_y() {
- var min, max, _min_max, _pane, _pane_offset;
- //XXX: can be memoized
- var _pane_idx = panes.length-1;
- while (_pane_idx >= 0) {
- _pane = panes[_pane_idx--];
- _pane_offset = _pane.offset();
- if (_pane_offset <= width) {
- // last visible pane (partial)
- _min_max = _pane.range_min_max_y(0, Math.ceil(((width - _pane_offset) / _pane_width) * _pane_period));
- min = _min_max[0];
- max = _min_max[1];
- break;
- }
- }
- for (var i=_pane_idx; i>=0; i--) {
- _pane = panes[i];
- _pane_offset = _pane.offset();
- if (_pane_offset < width) {
- if (_pane_offset > 0) {
- min = Math.min(min, _pane._min_y());
- max = Math.max(max, _pane._max_y());
- } else if (_pane_offset > -_pane_width) { // first pane (partial)
- _min_max = _pane.range_min_max_y(Math.floor((-_pane_offset / _pane_width) * _pane_period), _pane_period);
- min = Math.min(min, _min_max[0]);
- max = Math.max(max, _min_max[1]);
- }
- }
- }
- return [min, max];
- }
- this._min_max_y = _min_max_y;
-
- var MATH_LOG_10 = Math.log(10);
- function _calc_y_scale() {
- //XXX: assumption: min-y == 0
- // Reset Y Labels
- y_labels.empty();
- // Calculate y-coord
- var _power = Math.pow(10, Math.floor(Math.log(max_y) / MATH_LOG_10));
- var _scale, _num_lines;
- if (max_y < (2 * _power)) {
- _scale = 0.5 * _power;
- _num_lines = 4;
- } else if (max_y < (3 * _power)) {
- _scale = _power;
- _num_lines = 3;
- } else if (max_y < (4 * _power)) {
- _scale = _power;
- _num_lines = 4;
- } else if (max_y < (5 * _power)) {
- _scale = _power;
- _num_lines = 5;
- } else if (max_y < (6 * _power)) {
- _scale = 2 * _power;
- _num_lines = 3;
- } else if (max_y < (8 * _power)) {
- _scale = 2 * _power;
- _num_lines = 4;
- } else { // < 10 * _power
- _scale = 2 * _power;
- _num_lines = 5;
- }
- var _scaled_range = _num_lines * _scale * Y_SCALE_FACTOR;
- // y-coord scale
- _y_scale = chart_height / _scaled_range;
- // y-coord grid line intervals @ y-translation
- _y_intervals = [];
- for (var i=_num_lines;i>0;i--) {
- var _y = i*_scale;
- var _y_scaled = _y * _y_scale;
- _y_intervals.push(_y_scaled);
- y_labels.append([
- '<div',
- 'class="y-tick"',
- 'style="',
- 'position: absolute;',
- 'left: 0px;',
- 'top: '+(chart_height-_y_scaled+Y_LABEL_OFFSET)+'px;',
- 'z-index: 0;',
- 'width: '+Y_LABELS_WIDTH+'px;',
- '">'+_y+' '+Y_LABEL+'</div>'].join(' '));
- }
- }
- this._calc_y_scale = _calc_y_scale;
- function _rescale_y_axis() {
- _calc_y_scale();
- broker.publish("/pane/redraw");
- }
- this._rescale_y_axis = _rescale_y_axis;
- // Setup
- function init() {
- // Preprocess: calculate pane width and zoom schema
- width = target.width();
- height = target.height() - NAV_HEIGHT;
- var _zoom_schema = _ZOOM_SCHEMA_STANDARD;
- if (width > LARGE_CHART_THRESHOLD_WIDTH) {
- _pane_width = Math.floor(width/LARGE_CHART_NUM_PANES);
- if ($.browser.msie) { // reduce granularity
- _zoom_schema = _ZOOM_SCHEMA_RELAXED;
- }
- } else { // small chart
- _pane_width = Math.floor(width/SMALL_CHART_NUM_PANES);
- _zoom_schema = _ZOOM_SCHEMA_HALF;
- }
- // Model
- model = new Model(this, _zoom_schema);
- //FIXME: pass these in
- model.init(1243411200, 1243411200+4*LineDataCache.prototype.bucket_size*60-60, width, _pane_width);
- //FIXME: pass these in; maybe, via: $.extend(true, options, o)
- var init_time_ = 1243411200+3*LineDataCache.prototype.bucket_size*60;
- zoom_scale = 0;
- _pane_period = 20;
- // Chart
- panes = [];
- target.html("");
- target.css("position", "relative");
- // Background
- bg = $([
- '<div',
- 'style="',
- 'overflow: hidden;',
- 'position: absolute;',
- 'left: 0px;',
- 'top: 0px;',
- 'z-index: -1;',
- 'width: '+width+'px;',
- 'height: '+height+'px;',
- 'border: 1px solid rgb(0,0,0);',
- '"></div>'].join(' ')).appendTo(target);
- // Top Pane
- top_pane = $([
- '<div',
- 'class="top-pane"',
- 'style="',
- 'position: absolute;',
- 'left: '+BORDER_OFFSET_X+'px;',
- 'top: 0px;',
- 'width: '+width+'px;',
- 'height: '+TOP_PANE_HEIGHT+'px;',
- 'border-bottom: 1px solid '+TOP_PANE_BORDER_COLOR+';',
- '"></div>'].join(' ')).appendTo(target);
- // Top Pane: Zoom
- var _top_pane_zoom = $([
- '<div',
- 'class="top-zoom"',
- '></div>'].join(' ')).appendTo(top_pane);
- _top_pane_zoom.append('<strong>Zoom:</strong>');
- var _max_idx = model.zoom_schema().length-1;
- var _max_zoom = model.zoom_schema()[_max_idx];
- var _max_delta = _max_zoom[0] * _max_zoom[2];
- var _max_pt = ZOOM_POINTS.length-1;
- for (; _max_pt>=0; _max_pt--) {
- if (ZOOM_POINTS[_max_pt][0] < _max_delta) {
- break;
- }
- }
- _max_pt += 1;
- var _visible_panes = width / _pane_width;
- var _link_correlations = [];
- var _link_idx = 0;
- for (i=0; i<=_max_idx; i++) {
- var _zoom_level = model.zoom_schema()[i];
- for (var j=_zoom_level[1]; j<=_zoom_level[2]; j++) {
- if ((_zoom_level[0] * j * _visible_panes) >= ZOOM_POINTS[_link_idx][0]) {
- _link_correlations[_link_idx] = [i,j];
- _link_idx += 1;
- if (_link_idx > _max_pt) {
- break;
- }
- }
- }
- }
- for (i=0; i<=_max_pt; i++) {
- var _zoom_point = ZOOM_POINTS[i];
- var _zoom_link = $([
- '<a',
- 'class="zoom-link"',
- 'href="#'+_zoom_point[1]+'"',
- '>'+_zoom_point[1]+'</a>'].join(' ')).appendTo(_top_pane_zoom)[0];
- (function(obj, zoom_idx, pane_period) {
- addEvent(obj, "click", function() {
- set_zoom(zoom_idx, pane_period);
- });
- })(_zoom_link, _link_correlations[i][0], _link_correlations[i][1]);
- }
- var _max_zoom_link = $([
- '<a',
- 'class="zoom-link"',
- 'href="#max"',
- '>max</a>'].join(' ')).appendTo(_top_pane_zoom)[0];
- addEvent(_max_zoom_link, "click", function() {
- set_zoom(_max_idx, _max_zoom[2]);
- });
- // Top Pane: Range
- _top_range = $([
- '<div',
- 'class="top-right"',
- '></div>'].join(' ')).appendTo(top_pane);
-
- _top_range_span = $('<span></span>').appendTo(_top_range);
- // Top Pane: Point
- _top_point = $([
- '<div',
- 'class="top-right"',
- 'style="',
- 'display: none;',
- '"></div>'].join(' ')).appendTo(top_pane);
- _top_point_time = $('<span class="top-point-time"></span>').appendTo(_top_point);
- _top_point_val = $('<span class="top-point-value"></span>').appendTo(_top_point);
- // Bottom Tape
- bg.append([
- '<div',
- 'style="',
- 'position: absolute;',
- 'left: 0px;',
- 'top: '+(height-BOTTOM_TAPE_HEIGHT+BOTTOM_TAPE_TOP_ADJUST)+'px;',
- 'width: '+width+'px;',
- 'height: '+BOTTOM_TAPE_HEIGHT+'px;',
- 'background-color: '+BOTTOM_TAPE_COLOR+';',
- 'border: 0px;',
- '"></div>'].join(' '));
- // Container
- draw_width = width;
- draw_height = height - TOP_PANE_HEIGHT - BORDER_OFFSET_Y;
- chart_height = draw_height - BOTTOM_TAPE_HEIGHT;
- container = $([
- '<div',
- 'style="',
- 'position: absolute;',
- 'overflow: hidden;',
- 'left: '+BORDER_OFFSET_X+'px;',
- 'top: '+(TOP_PANE_HEIGHT+2*BORDER_OFFSET_Y)+'px;',
- 'width: '+draw_width+'px;',
- 'height: '+draw_height+'px;',
- 'cursor: move;',
- '"></div>'].join(' ')).appendTo(target);
- // Panes
- var num_panes = Math.ceil(width/_pane_width)+2;
- var centered_offset = (-num_panes*_pane_width/2)-(-width/2);
- max_left = centered_offset - (_pane_width/2);
- max_right = centered_offset + ((num_panes - 0.5) * _pane_width);
- for (i=0;i<num_panes;i++) {
- var pane = new Pane(this, _pane_width);
- pane.init();
- panes.push(pane);
- }
- // Y-Labels
- y_labels = $([
- '<div',
- 'style="',
- 'overflow: hidden;',
- 'position: absolute;',
- 'right: '+Y_LABELS_RIGHT_OFFSET+'px;',
- 'top: 0px;',
- 'width: '+Y_LABELS_WIDTH+'px;',
- 'height: '+chart_height+'px;',
- '"></div>'].join(' ')).appendTo(container);
- // Navigation Pane
- nav = new NavigationPane(this);
- nav.init(target, width, NAV_HEIGHT, (height+BORDER_OFFSET_Y), BORDER_OFFSET_X);
- // Overlay: Line
- _overlay_line = $([
- '<div',
- 'style="',
- 'display: none;',
- 'position: absolute;',
- 'top: '+(TOP_PANE_HEIGHT+BORDER_OFFSET_Y)+'px;',
- 'width: 1px;',
- 'height: '+(chart_height+BORDER_OFFSET_Y)+'px;',
- 'background-color: #666;',
- '"></div>'].join(' ')).appendTo(target);
- // Events Setup
- // --- Pane Translation Events
- broker.subscribe("/chart/center", this, "center");
- broker.subscribe("/chart/translate", this, "translate");
- broker.subscribe("/chart/recycle", this, "recycle");
- broker.subscribe("/chart/replace", this, "replace");
-
- // --- Draw Loop Events
- broker.subscribe("/chart/draw/start", this, "draw_start");
- broker.subscribe("/chart/draw/stop", this, "draw_stop");
- broker.subscribe("/chart/draw/rescale", this, "_rescale_y_axis");
- // --- Navigation Translate Events
- broker.subscribe("/chart/scroll/start", this, "scroll_start");
- broker.subscribe("/chart/scroll/stop", this, "scroll_stop");
- // --- Info Display Events
- broker.subscribe("/info/range/update", this, "_update_top_range");
- broker.subscribe("/info/point/update", this, "_update_top_point");
- // --- DOM Events
- // ----- Translation
- addEvent(container[0], "mousedown", drag_init);
- container[0].onmousedown = function(e) { return cancelEvent(e); };
- // ----- Zooming
- addEvent(container[0], "dblclick", double_click);
- addEvent(container[0], "mousewheel", mouse_wheel);
-
- // ----- Cursor Tracking
- container.hover(chart_mouseover, chart_mouseout);
- // Draw Panes
- broker.publish("/chart/center", init_time_, width/2);
- }
- this.init = init;
- // DOM Event Methods
- // ----- Translation
- function drag_init(e) {
- e = e ? e : window.event;
-
- addEvent(document, "mousemove", drag_move);
- container._drag_x = e.clientX;
- addEvent(document, "mouseup", drag_end);
- broker.publish("/chart/draw/start"); // start draw loop
- removeEvent(container[0], "mousemove", cursor_move);
- }
- function drag_move(e) {
- e = e ? e : window.event;
- var delta = e.clientX - container._drag_x;
- if (Math.abs(delta) <= 1) {
- return cancelEvent(e);
- }
- container._drag_x = e.clientX;
- broker.publish("/chart/translate", delta);
-
- return cancelEvent(e);
- }
- function drag_end(e) {
- removeEvent(document, "mousemove", drag_move);
- removeEvent(document, "mouseup", drag_end);
- broker.publish("/chart/draw/stop"); // stop draw loop
- if (_is_mouseover) {
- addEvent(container[0], "mousemove", cursor_move);
- container._cursor_x = e.clientX;
- }
- }
- // ----- Zooming
- function _zoom_chart(e, delta) {
- var _offset = container.offset();
- var _x = e.clientX - _offset.left;
- var _y = e.clientY - _offset.top;
- var _pane = panes[0];
- var _time_offset = _pane.time_offset() + (_x - _pane.offset()) / _x_scale * model.zoom_delta();
- if (delta > 0) { // zoom in
- //XXX: on zoom in, maybe make delta larger at very low zoom levels
- _pane_period = Math.max(model.zoom_level()[1], _pane_period-delta);
- _pane_period -= delta;
- if (_pane_period < model.zoom_level()[1]) {
- if (zoom_scale > 0) {
- zoom_scale -= 1;
- _pane_period = model.zoom_level()[2];
- } else { // zoom_scale === 0
- _pane_period = model.zoom_level()[1];
- }
- }
- } else { // zoom out
- _pane_period -= delta;
- if (_pane_period > model.zoom_level()[2]) {
- if (zoom_scale < (model.zoom_schema().length-1)) {
- zoom_scale += 1;
- _pane_period = model.zoom_level()[1];
- } else { // _zoom_scale === (_zoom_schema.length-1)
- _pane_period = model.zoom_level()[2];
- }
- }
- }
- broker.publish("/chart/center", _time_offset, _x);
- }
- function double_click(e) {
- //FIXME: may not be cross-browser compatible; doesn't handle triple-clicks
- e = e ? e : window.event;
- _zoom_chart(e, Math.max(1, Math.floor(_pane_period * 0.25)));
- }
- function mouse_wheel(e) {
- //XXX: while zooming, maintain time position at mouse pointer
- e = e ? e : window.event;
- //FIXME? Opera uses -wheelDelta; what about Chrome?
- var delta = e.detail ? -e.detail : e.wheelDelta / 40;
- if (Math.abs(delta) > 0) {
- delta = Math.min(Math.round(delta * MOUSE_SCROLL_ZOOM_FACTOR), MOUSE_SCROLL_ZOOM_DELTA_MAX);
- _zoom_chart(e, delta);
- }
- return cancelEvent(e);
- }
- // ----- Cursor Tracking
- function chart_mouseover(e) {
- _is_mouseover = true;
- e = e ? e : window.event;
- addEvent(container[0], "mousemove", cursor_move);
- container._cursor_x = e.clientX;
- }
- function chart_mouseout(e) {
- _is_mouseover = false;
- removeEvent(container[0], "mousemove", cursor_move);
- }
- function cursor_move(e) {
- e = e ? e : window.event;
- if (Math.abs(e.clientX - container._cursor_x) <= 1) {
- return e;
- }
- container._cursor_x = e.clientX;
- _update_top_point(e.clientX - (container.offset().left + width/2));
- return e;
- }
- // Chart Translation Methods
- function center(time_offset_, pixel_offset_) {
- // determine: x-coord scale
- _calc_x_scale();
- // shared references
- var _zoom_delta = model.zoom_delta();
- var _half_width = width / 2;
- // calculate: plot offsets
- pixel_offset_ = Math.floor(pixel_offset_);
- var _point_offset =
- Math.floor((time_offset_ - model.min_time()) / _zoom_delta);
- //XXX: special case: plot span w/in chart width
- var _plot_points = Math.floor(model._time_range() / _zoom_delta);
- if (_plot_points <= _chart_period()) {
- _point_offset =
- Math.floor(_plot_points / 2); //XXX: last pixel is always length+1
- pixel_offset_ = Math.floor(_half_width);
- // chart center pixel tracking
- min_pixel = max_pixel = center_pixel =
- Math.round((_point_offset * _x_scale) + _half_width - pixel_offset_);
- } else {
- var _width_left =
- (((model.max_time() - time_offset_) / _zoom_delta) - 1) * _x_scale;
- if ((width - pixel_offset_) > _width_left) {
- //XXX: special case: plot end w/in chart width
- pixel_offset_ = Math.floor(width - _width_left);
- } else if (pixel_offset_ > (_point_offset * _x_scale)) {
- //XXX: special case: plot start w/in chart start
- pixel_offset_ = Math.floor(_point_offset * _x_scale);
- }
- // chart center pixel tracking
- min_pixel = Math.ceil(_half_width);
- var _num_points =
- (model._time_range() / _zoom_delta) - 1; //XXX: last pixel is always length+1
- max_pixel = Math.floor((_num_points * _x_scale) - _half_width);
-
- center_pixel = Math.round((_point_offset * _x_scale) + _half_width - pixel_offset_);
- }
- // calculate: pane reset
- var len = panes.length;
- var pane_pixel = width + (pixel_offset_ % _pane_width);
- var pane_point =
- _point_offset + (len - Math.floor(pixel_offset_ / _pane_width) - 2) * _pane_period;
-
- // reset panes
- var i = len;
- while (i--) {
- var pane = panes[i];
- pane._reset(pane_point, pane_pixel);
- pane_point -= _pane_period;
- pane_pixel -= _pane_width;
- }
- // determine: y-coord scale
- var min_max_y = _min_max_y();
- min_y = min_max_y[0];
- max_y = min_max_y[1];
- broker.publish("/chart/draw/rescale");
-
- // center nav
- broker.publish("/nav/center");
- // update range info
- broker.publish("/info/range/update");
- }
- this.center = center;
- function recycle(delta_) {
- // recycle available panes
- var first_pane, last_pane;
- if (delta_ > 0) {
- while (panes[panes.length-1].offset() > max_right) {
- last_pane = panes.pop();
- first_pane = panes[0];
- last_pane.reset(
- first_pane._data_offset()-_pane_period,
- first_pane.offset()-_pane_width);
- panes.unshift(last_pane);
- }
- } else if (delta_ < 0) {
- while (panes[0].offset() < max_left) {
- first_pane = panes.shift();
- last_pane = panes[panes.length-1];
- first_pane.reset(
- last_pane._data_offset()+_pane_period,
- last_pane.offset()+_pane_width);
- panes.push(first_pane);
- }
- }
- }
- this.recycle = recycle;
- function translate(delta_) {
- //XXX: hot code path: omg, make everything in the translate code tighter...
- // check for end of model range
- center_pixel -= delta_;
- if (center_pixel < max_pixel) { //XXX: take most common branches, first
- if (center_pixel < min_pixel) {
- delta_ -= min_pixel - center_pixel;
- center_pixel = min_pixel;
- if (!delta_) { //XXX: optimizing: delta_ === 0
- return;
- }
- }
- } else { // center_pixel >= max_pixel
- delta_ += center_pixel - max_pixel;
- center_pixel = max_pixel;
- if (!delta_) { //XXX: optimizing: delta_ === 0
- return;
- }
- }
- if (Math.abs(delta_) < width) {
- // translate panes
- broker.publish("/pane/translate", delta_);
- // recycle panes
- broker.publish("/chart/recycle", delta_);
- } else { // replace all panes, instead
- // replace panes
- broker.publish("/chart/replace", center_pixel);
- }
- // translate nav
- broker.publish("/nav/translate", delta_);
- // update range info
- broker.publish("/info/range/update");
- }
- this.translate = translate;
- function replace(center_pixel_) {
- //XXX: calculate from center_pixel
- var _num_panes = Math.floor(center_pixel_ / _pane_width);
- var _pixel_rem = center_pixel_ % _pane_width;
- var _idx_offset = Math.floor(panes.length/2);
- var pane_point = (_num_panes + _idx_offset) * _pane_period;
- var pane_pixel = Math.floor(width / 2) + _idx_offset * _pane_width - _pixel_rem;
- var i = panes.length;
- while (i--) {
- var pane = panes[i];
- pane.reset(pane_point, pane_pixel);
- pane_point -= _pane_period;
- pane_pixel -= _pane_width;
- }
- // determine: y-coord scale
- var min_max_y = _min_max_y();
- min_y = min_max_y[0];
- max_y = min_max_y[1];
- broker.publish("/chart/draw/rescale");
- // center nav
- broker.publish("/nav/center");
- }
- this.replace = replace;
- // Draw Loop Methods
- function _draw_loop() {
- var min_max_y = _min_max_y();
- if (min_max_y[0] != min_y || min_max_y[1] != max_y) {
- min_y = min_max_y[0];
- max_y = min_max_y[1];
- broker.publish("/chart/draw/rescale");
- }
- if (!updating) {
- return;
- }
- var now = (new Date()).getTime();
- //XXX: dynamically adjust re-draw rate
- update_delta = Math.max(UPDATE_DELTA_FACTOR * (now - update_time), UPDATE_BOUND);
- update_time = now;
- update_id = setTimeout(_draw_loop, update_delta);
- }
- function draw_start() {
- clearTimeout(update_id);
- updating = true;
- update_time = (new Date()).getTime();
- update_id = setTimeout(_draw_loop, update_delta);
- }
- this.draw_start = draw_start;
- function draw_stop() {
- //XXX: Make final draw update more deterministic, after stop
- clearTimeout(update_id);
- updating = false;
- update_id = setTimeout(_draw_loop, UPDATE_AFTER_STOP);
- }
- this.draw_stop = draw_stop;
- // Scroll Loop Methods
- function _scroll_loop() {
- if (!scrolling) {
- return;
- }
- broker.publish("/chart/translate", scroll_rate);
- scroll_id = setTimeout(_scroll_loop, NAV_SCROLL_RATE);
- }
- function scroll_start(sign) {
- clearTimeout(scroll_id);
- scroll_rate = sign * NAV_SCROLL_SPEED;
- scrolling = true;
- _scroll_loop();
- }
- this.scroll_start = scroll_start;
- function scroll_stop() {
- scrolling = false;
- }
- this.scroll_stop = scroll_stop;
- // Modify Zoom
- function _update_zoom(zoom_scale_, pane_period_) {
- zoom_scale = zoom_scale_;
- _pane_period = pane_period_;
- }
- this._update_zoom = _update_zoom;
- function set_zoom(zoom_scale_, pane_period_) {
- var _num_panes = center_pixel / _pane_width;
- var _time_offset = model.min_time();
- _time_offset += _num_panes * _pane_period * model.zoom_delta();
- _time_offset = Math.floor(_time_offset);
-
- _update_zoom(zoom_scale_, pane_period_);
- center(_time_offset, Math.floor(width/2));
- }
- this.set_zoom = set_zoom;
- // Update Top Range
- function _update_top_range() {
- // static variables
- var _half_width = width/2;
- var _day_in_secs = 60*60*24;
- _update_top_range = function() {
- // hide cursor info
- _overlay_line.css("display", "none");
- _top_point.css("display", "none");
- // calculate range
- var _min_pixel = center_pixel - _half_width;
- var _max_pixel = center_pixel + _half_width;
- var _min_time = model.min_time() + (_min_pixel / _x_scale * model.zoom_delta());
- var _max_time = model.min_time() + (_max_pixel / _x_scale * model.zoom_delta());
- var _min_date = new Date(_min_time * 1000);
- var _max_date = new Date(_max_time * 1000);
- if ((_max_time - _min_time) < _day_in_secs && _min_date.getDay() === _max_date.getDay()) {
- _top_range_span.text(formatDate(_min_date, "MMM d, yyyy"));
- } else {
- _top_range_span.text(
- formatDate(_min_date, "NNN d, yyyy") + " - " + formatDate(_max_date, "NNN d, yyyy"));
- }
- // display viewable range
- _top_range.css("display", "inline");
- };
- return _update_top_range();
- }
- this._update_top_range = _update_top_range;
- // Update Top Point
- function _update_top_point(pixel_offset_) {
- // static variables
- var _half_width = width/2;
- _update_top_point = function(pixel_offset_) {
- // hide range tracking
- _top_range.css("display", "none");
- // calculate time and value
- var _point = Math.round((center_pixel + pixel_offset_) / _pane_width * _pane_period);
- var _pixel = _point * _x_scale;
- var _date = new Date((model.min_time() + (_point * model.zoom_delta())) * 1000);
- var _val = model.fetch(zoom_scale, _point, 0)[0][0] || 0;
- _top_point_time.text(formatDate(_date, "MMM d, yyyy"));
- _top_point_val.text(Math.round(_val * 1000) / 1000);
- var _min = center_pixel - _half_width;
- _overlay_line.css("left", Math.floor(_pixel - _min)+1);
- // display cursor info
- _top_point.css("display", "inline");
- _overlay_line.css("display", "inline");
- };
- return _update_top_point(pixel_offset_);
- }
- this._update_top_point = _update_top_point;
- };
- Chart.prototype = {
- version: "0.1"
- };
- $c.chart = function(target) {
- var chart = new Chart(target);
- chart.init();
- return chart;
- };
- // ========== External ==========
- // reference: http://www.mattkruse.com/javascript/date/source.html
- // ===================================================================
- // Author: Matt Kruse <matt@mattkruse.com>
- // WWW: http://www.mattkruse.com/
- //
- // NOTICE: You may use this code for any purpose, commercial or
- // private, without any further permission from the author. You may
- // remove this notice from your final code if you wish, however it is
- // appreciated by the author if at least my web site address is kept.
- //
- // You may *NOT* re-distribute this code in any way except through its
- // use. That means, you can include it in your product, or your web
- // site, or any other form where the code is actually being used. You
- // may not put the plain javascript up on your site for download or
- // include it in your javascript libraries for download.
- // If you wish to share this code with others, please just point them
- // to the URL instead.
- // Please DO NOT link directly to my .js files from your site. Copy
- // the files to your server and use them there. Thank you.
- // ===================================================================
- // HISTORY
- // ------------------------------------------------------------------
- // May 17, 2003: Fixed bug in parseDate() for dates <1970
- // March 11, 2003: Added parseDate() function
- // March 11, 2003: Added "NNN" formatting option. Doesn't match up
- // perfectly with SimpleDateFormat formats, but
- // backwards-compatability was required.
- // ------------------------------------------------------------------
- // These functions use the same 'format' strings as the
- // java.text.SimpleDateFormat class, with minor exceptions.
- // The format string consists of the following abbreviations:
- //
- // Field | Full Form | Short Form
- // -------------+--------------------+-----------------------
- // Year | yyyy (4 digits) | yy (2 digits), y (2 or 4 digits)
- // Month | MMM (name or abbr.)| MM (2 digits), M (1 or 2 digits)
- // | NNN (abbr.) |
- // Day of Month | dd (2 digits) | d (1 or 2 digits)
- // Day of Week | EE (name) | E (abbr)
- // Hour (1-12) | hh (2 digits) | h (1 or 2 digits)
- // Hour (0-23) | HH (2 digits) | H (1 or 2 digits)
- // Hour (0-11) | KK (2 digits) | K (1 or 2 digits)
- // Hour (1-24) | kk (2 digits) | k (1 or 2 digits)
- // Minute | mm (2 digits) | m (1 or 2 digits)
- // Second | ss (2 digits) | s (1 or 2 digits)
- // AM/PM | a |
- //
- // NOTE THE DIFFERENCE BETWEEN MM and mm! Month=MM, not mm!
- // Examples:
- // "MMM d, y" matches: January 01, 2000
- // Dec 1, 1900
- // Nov 20, 00
- // "M/d/yy" matches: 01/20/00
- // 9/2/00
- // "MMM dd, yyyy hh:mm:ssa" matches: "January 01, 2000 12:30:45AM"
- // ------------------------------------------------------------------
- var MONTH_NAMES = [
- 'January',
- 'February',
- 'March',
- 'April',
- 'May',
- 'June',
- 'July',
- 'August',
- 'September',
- 'October',
- 'November',
- 'December',
- 'Jan',
- 'Feb',
- 'Mar',
- 'Apr',
- 'May',
- 'Jun',
- 'Jul',
- 'Aug',
- 'Sep',
- 'Oct',
- 'Nov',
- 'Dec'
- ];
- var DAY_NAMES = [
- 'Sunday',
- 'Monday',
- 'Tuesday',
- 'Wednesday',
- 'Thursday',
- 'Friday',
- 'Saturday',
- 'Sun',
- 'Mon',
- 'Tue',
- 'Wed',
- 'Thu',
- 'Fri',
- 'Sat'
- ];
- function LZ(x) {
- return (x < 0 || x > 9 ? "" : "0" ) + x;
- }
- // ------------------------------------------------------------------
- // formatDate (date_object, format)
- // Returns a date in the output format specified.
- // The format string uses the same abbreviations as in getDateFromFormat()
- // ------------------------------------------------------------------
- function formatDate(date,format) {
- format = format + "";
- var result="";
- var i_format=0;
- var c="";
- var token="";
- var y=date.getYear()+"";
- var M=date.getMonth()+1;
- var d=date.getDate();
- var E=date.getDay();
- var H=date.getHours();
- var m=date.getMinutes();
- var s=date.getSeconds();
- var yyyy,yy,MMM,MM,dd,hh,h,mm,ss,ampm,HH,KK,K,kk,k;
- // Convert real date parts into formatted versions
- var value = {};
- if (y.length < 4) {y=""+(y-0+1900);}
- value.y=""+y;
- value.yyyy=y;
- value.yy=y.substring(2,4);
- value.M=M;
- value.MM=LZ(M);
- value.MMM=MONTH_NAMES[M-1];
- value.NNN=MONTH_NAMES[M+11];
- value.d=d;
- value.dd=LZ(d);
- value.E=DAY_NAMES[E+7];
- value.EE=DAY_NAMES[E];
- value.H=H;
- value.HH=LZ(H);
- if (H==0){value.h=12;}
- else if (H>12){value.h=H-12;}
- else {value.h=H;}
- value.hh=LZ(value.h);
- if (H>11){value.K=H-12;} else {value.K=H;}
- value.k=H+1;
- value.KK=LZ(value.K);
- value.kk=LZ(value.k);
- if (H > 11) { value.a="PM"; }
- else { value.a="AM"; }
- value.m=m;
- value.mm=LZ(m);
- value.s=s;
- value.ss=LZ(s);
- while (i_format < format.length) {
- c = format.charAt(i_format);
- token="";
- while ((format.charAt(i_format)==c) && (i_format < format.length)) {
- token += format.charAt(i_format++);
- }
- if (value[token]) {
- result=result + value[token];
- } else {
- result=result + token;
- }
- }
- return result;
- }
- ;;; $c.formatDate = formatDate;
- })(jQuery);