From f2edfd57d983299e79d5691eb6475bb3fa925ee4 Mon Sep 17 00:00:00 2001 From: chrono Date: Fri, 29 Nov 2013 19:39:11 +0100 Subject: [PATCH] Messify everything and updated to flot --- public/assets/css/picoreflow.css | 24 +- public/assets/css/select2-bootstrap.css | 151 +- public/assets/fonts/LCDN.eot | Bin 0 -> 28178 bytes public/assets/fonts/LCDN.ttf | Bin 0 -> 27968 bytes public/assets/fonts/LCDN.woff | Bin 0 -> 8984 bytes public/assets/js/jquery.event.drag-2.2.js | 402 +++ public/assets/js/jquery.flot.draggable.js | 158 ++ public/assets/js/jquery.flot.js | 3143 +++++++++++++++++++++ public/assets/js/jquery.flot.resize.js | 60 + public/assets/js/picoreflow.js | 424 +-- public/index.html | 408 ++- 11 files changed, 4281 insertions(+), 489 deletions(-) create mode 100644 public/assets/fonts/LCDN.eot create mode 100644 public/assets/fonts/LCDN.ttf create mode 100644 public/assets/fonts/LCDN.woff create mode 100644 public/assets/js/jquery.event.drag-2.2.js create mode 100644 public/assets/js/jquery.flot.draggable.js create mode 100644 public/assets/js/jquery.flot.js create mode 100644 public/assets/js/jquery.flot.resize.js diff --git a/public/assets/css/picoreflow.css b/public/assets/css/picoreflow.css index f8d0bb6..1f2555a 100644 --- a/public/assets/css/picoreflow.css +++ b/public/assets/css/picoreflow.css @@ -1,7 +1,14 @@ - body { - background: #b9b6af; - } +@font-face{ + font-family: "LCDN"; + src: url('/picoreflow/assets/fonts/LCDN.eot'); + src: local("LCD Normal"), + url('/picoreflow/assets/fonts/LCDN.woff') format("woff"), + url('/picoreflow/assets/fonts/LCDN.ttf') format("truetype"); +} +body { + background: #b9b6af; +} .display { display: inline-block; @@ -24,6 +31,9 @@ box-shadow: 0 0 1.1em 0 rgba(0,0,0,0.75),inset 0 0 9px 2px #000; border-color: #000000; } +.ds-num { + font-family: "LCDN"; +} #main_status { margin-top: 15px; @@ -81,6 +91,7 @@ background: #3F3E3A; .progress-bar { background-color: #75890c; + color: #000; } .btn-success { @@ -120,3 +131,10 @@ border-color: #b92c28; } +.graph { + width: 100%; + height: 300px; + font-size: 14px; + line-height: 1.2em; +} + diff --git a/public/assets/css/select2-bootstrap.css b/public/assets/css/select2-bootstrap.css index 9099710..c38afd4 100644 --- a/public/assets/css/select2-bootstrap.css +++ b/public/assets/css/select2-bootstrap.css @@ -1,87 +1,114 @@ -.form-control .select2-choice { - border: 0; - border-radius: 2px; +/** + * Select2 Bootstrap CSS 1.0 + * Compatible with select2 3.3.2 and bootstrap 2.3.1 + * MIT License + */ +.select2-container { + vertical-align: middle; +} +.select2-container.input-mini { + width: 60px; +} +.select2-container.input-small { + width: 90px; +} +.select2-container.input-medium { + width: 150px; +} +.select2-container.input-large { + width: 210px; +} +.select2-container.input-xlarge { + width: 270px; +} +.select2-container.input-xxlarge { + width: 530px; +} +.select2-container.input-default { + width: 220px; +} +.select2-container[class*="span"] { + float: none; + margin-left: 0; } -.form-control .select2-choice .select2-arrow { - border-radius: 0 2px 2px 0; +.select2-container .select2-choice, +.select2-container-multi .select2-choices { + height: 28px; + line-height: 29px; + border: 1px solid #cccccc; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + background: none; + background-color: white; + filter: none; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); } -.form-control.select2-container { - height: auto !important; - padding: 0px; +.select2-container .select2-choice div, .select2-container .select2-choice .select2-arrow, +.select2-container.select2-container-disabled .select2-choice div, +.select2-container.select2-container-disabled .select2-choice .select2-arrow { + border-left: none; + background: none; + filter: none; } -.form-control.select2-container.select2-dropdown-open { - border-color: #5897FB; - border-radius: 3px 3px 0 0; +.control-group.error [class^="select2-choice"] { + border-color: #b94a48; } -.form-control .select2-container.select2-dropdown-open .select2-choices { - border-radius: 3px 3px 0 0; +.select2-container-multi .select2-choices .select2-search-field { + height: 28px; + line-height: 27px; } -.form-control.select2-container .select2-choices { - border: 0 !important; - border-radius: 3px; +.select2-drop.select2-drop-active, +.select2-container-active .select2-choice, +.select2-container-multi.select2-container-active .select2-choices { + border-color: red; + outline: none; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); } -.control-group.warning .select2-container .select2-choice, -.control-group.warning .select2-container .select2-choices, -.control-group.warning .select2-container-active .select2-choice, -.control-group.warning .select2-container-active .select2-choices, -.control-group.warning .select2-dropdown-open.select2-drop-above .select2-choice, -.control-group.warning .select2-dropdown-open.select2-drop-above .select2-choices, -.control-group.warning .select2-container-multi.select2-container-active .select2-choices { - border: 1px solid #C09853 !important; +[class^="input-"] .select2-container { + font-size: 14px; } -.control-group.warning .select2-container .select2-choice div { - border-left: 1px solid #C09853 !important; - background: #FCF8E3 !important; +.input-prepend [class^="select2-choice"] { + border-top-left-radius: 0; + border-bottom-left-radius: 0; } -.control-group.error .select2-container .select2-choice, -.control-group.error .select2-container .select2-choices, -.control-group.error .select2-container-active .select2-choice, -.control-group.error .select2-container-active .select2-choices, -.control-group.error .select2-dropdown-open.select2-drop-above .select2-choice, -.control-group.error .select2-dropdown-open.select2-drop-above .select2-choices, -.control-group.error .select2-container-multi.select2-container-active .select2-choices { - border: 1px solid #B94A48 !important; +.input-append [class^="select2-choice"] { + border-top-right-radius: 0; + border-bottom-right-radius: 0; } -.control-group.error .select2-container .select2-choice div { - border-left: 1px solid #B94A48 !important; - background: #F2DEDE !important; +.select2-dropdown-open [class^="select2-choice"] { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } -.control-group.info .select2-container .select2-choice, -.control-group.info .select2-container .select2-choices, -.control-group.info .select2-container-active .select2-choice, -.control-group.info .select2-container-active .select2-choices, -.control-group.info .select2-dropdown-open.select2-drop-above .select2-choice, -.control-group.info .select2-dropdown-open.select2-drop-above .select2-choices, -.control-group.info .select2-container-multi.select2-container-active .select2-choices { - border: 1px solid #3A87AD !important; +.select2-dropdown-open.select2-drop-above [class^="select2-choice"] { + border-top-left-radius: 0; + border-top-right-radius: 0; } -.control-group.info .select2-container .select2-choice div { - border-left: 1px solid #3A87AD !important; - background: #D9EDF7 !important; +[class^="input-"] .select2-offscreen { + position: absolute; } -.control-group.success .select2-container .select2-choice, -.control-group.success .select2-container .select2-choices, -.control-group.success .select2-container-active .select2-choice, -.control-group.success .select2-container-active .select2-choices, -.control-group.success .select2-dropdown-open.select2-drop-above .select2-choice, -.control-group.success .select2-dropdown-open.select2-drop-above .select2-choices, -.control-group.success .select2-container-multi.select2-container-active .select2-choices { - border: 1px solid #468847 !important; -} - -.control-group.success .select2-container .select2-choice div { - border-left: 1px solid #468847 !important; - background: #DFF0D8 !important; +/** + * This stops the quick flash when a native selectbox is shown and + * then replaced by a select2 input when javascript kicks in. This can be + * removed if javascript is not present + */ +select.select2 { + height: 28px; + visibility: hidden; } diff --git a/public/assets/fonts/LCDN.eot b/public/assets/fonts/LCDN.eot new file mode 100644 index 0000000000000000000000000000000000000000..17a69a32538ca968d55f5369cb95a298ac1c3c33 GIT binary patch literal 28178 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 G@qYoehuiZ2 literal 0 HcmV?d00001 diff --git a/public/assets/fonts/LCDN.woff b/public/assets/fonts/LCDN.woff new file mode 100644 index 0000000000000000000000000000000000000000..2c3a989c135fe617c6c55177150fafbfc03e54da GIT binary patch literal 8984 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 LJQWJ~p@aG#AMDNy literal 0 HcmV?d00001 diff --git a/public/assets/js/jquery.event.drag-2.2.js b/public/assets/js/jquery.event.drag-2.2.js new file mode 100644 index 0000000..1cda0e2 --- /dev/null +++ b/public/assets/js/jquery.event.drag-2.2.js @@ -0,0 +1,402 @@ +/*! + * jquery.event.drag - v 2.2 + * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com + * Open Source MIT License - http://threedubmedia.com/code/license + */ +// Created: 2008-06-04 +// Updated: 2012-05-21 +// REQUIRES: jquery 1.7.x + +;(function( $ ){ + +// add the jquery instance method +$.fn.drag = function( str, arg, opts ){ + // figure out the event type + var type = typeof str == "string" ? str : "", + // figure out the event handler... + fn = $.isFunction( str ) ? str : $.isFunction( arg ) ? arg : null; + // fix the event type + if ( type.indexOf("drag") !== 0 ) + type = "drag"+ type; + // were options passed + opts = ( str == fn ? arg : opts ) || {}; + // trigger or bind event handler + return fn ? this.bind( type, opts, fn ) : this.trigger( type ); +}; + +// local refs (increase compression) +var $event = $.event, +$special = $event.special, +// configure the drag special event +drag = $special.drag = { + + // these are the default settings + defaults: { + which: 1, // mouse button pressed to start drag sequence + distance: 0, // distance dragged before dragstart + not: ':input', // selector to suppress dragging on target elements + handle: null, // selector to match handle target elements + relative: false, // true to use "position", false to use "offset" + drop: true, // false to suppress drop events, true or selector to allow + click: false // false to suppress click events after dragend (no proxy) + }, + + // the key name for stored drag data + datakey: "dragdata", + + // prevent bubbling for better performance + noBubble: true, + + // count bound related events + add: function( obj ){ + // read the interaction data + var data = $.data( this, drag.datakey ), + // read any passed options + opts = obj.data || {}; + // count another realted event + data.related += 1; + // extend data options bound with this event + // don't iterate "opts" in case it is a node + $.each( drag.defaults, function( key, def ){ + if ( opts[ key ] !== undefined ) + data[ key ] = opts[ key ]; + }); + }, + + // forget unbound related events + remove: function(){ + $.data( this, drag.datakey ).related -= 1; + }, + + // configure interaction, capture settings + setup: function(){ + // check for related events + if ( $.data( this, drag.datakey ) ) + return; + // initialize the drag data with copied defaults + var data = $.extend({ related:0 }, drag.defaults ); + // store the interaction data + $.data( this, drag.datakey, data ); + // bind the mousedown event, which starts drag interactions + $event.add( this, "touchstart mousedown", drag.init, data ); + // prevent image dragging in IE... + if ( this.attachEvent ) + this.attachEvent("ondragstart", drag.dontstart ); + }, + + // destroy configured interaction + teardown: function(){ + var data = $.data( this, drag.datakey ) || {}; + // check for related events + if ( data.related ) + return; + // remove the stored data + $.removeData( this, drag.datakey ); + // remove the mousedown event + $event.remove( this, "touchstart mousedown", drag.init ); + // enable text selection + drag.textselect( true ); + // un-prevent image dragging in IE... + if ( this.detachEvent ) + this.detachEvent("ondragstart", drag.dontstart ); + }, + + // initialize the interaction + init: function( event ){ + // sorry, only one touch at a time + if ( drag.touched ) + return; + // the drag/drop interaction data + var dd = event.data, results; + // check the which directive + if ( event.which != 0 && dd.which > 0 && event.which != dd.which ) + return; + // check for suppressed selector + if ( $( event.target ).is( dd.not ) ) + return; + // check for handle selector + if ( dd.handle && !$( event.target ).closest( dd.handle, event.currentTarget ).length ) + return; + + drag.touched = event.type == 'touchstart' ? this : null; + dd.propagates = 1; + dd.mousedown = this; + dd.interactions = [ drag.interaction( this, dd ) ]; + dd.target = event.target; + dd.pageX = event.pageX; + dd.pageY = event.pageY; + dd.dragging = null; + // handle draginit event... + results = drag.hijack( event, "draginit", dd ); + // early cancel + if ( !dd.propagates ) + return; + // flatten the result set + results = drag.flatten( results ); + // insert new interaction elements + if ( results && results.length ){ + dd.interactions = []; + $.each( results, function(){ + dd.interactions.push( drag.interaction( this, dd ) ); + }); + } + // remember how many interactions are propagating + dd.propagates = dd.interactions.length; + // locate and init the drop targets + if ( dd.drop !== false && $special.drop ) + $special.drop.handler( event, dd ); + // disable text selection + drag.textselect( false ); + // bind additional events... + if ( drag.touched ) + $event.add( drag.touched, "touchmove touchend", drag.handler, dd ); + else + $event.add( document, "mousemove mouseup", drag.handler, dd ); + // helps prevent text selection or scrolling + if ( !drag.touched || dd.live ) + return false; + }, + + // returns an interaction object + interaction: function( elem, dd ){ + var offset = $( elem )[ dd.relative ? "position" : "offset" ]() || { top:0, left:0 }; + return { + drag: elem, + callback: new drag.callback(), + droppable: [], + offset: offset + }; + }, + + // handle drag-releatd DOM events + handler: function( event ){ + // read the data before hijacking anything + var dd = event.data; + // handle various events + switch ( event.type ){ + // mousemove, check distance, start dragging + case !dd.dragging && 'touchmove': + event.preventDefault(); + case !dd.dragging && 'mousemove': + // drag tolerance, x² + y² = distance² + if ( Math.pow( event.pageX-dd.pageX, 2 ) + Math.pow( event.pageY-dd.pageY, 2 ) < Math.pow( dd.distance, 2 ) ) + break; // distance tolerance not reached + event.target = dd.target; // force target from "mousedown" event (fix distance issue) + drag.hijack( event, "dragstart", dd ); // trigger "dragstart" + if ( dd.propagates ) // "dragstart" not rejected + dd.dragging = true; // activate interaction + // mousemove, dragging + case 'touchmove': + event.preventDefault(); + case 'mousemove': + if ( dd.dragging ){ + // trigger "drag" + drag.hijack( event, "drag", dd ); + if ( dd.propagates ){ + // manage drop events + if ( dd.drop !== false && $special.drop ) + $special.drop.handler( event, dd ); // "dropstart", "dropend" + break; // "drag" not rejected, stop + } + event.type = "mouseup"; // helps "drop" handler behave + } + // mouseup, stop dragging + case 'touchend': + case 'mouseup': + default: + if ( drag.touched ) + $event.remove( drag.touched, "touchmove touchend", drag.handler ); // remove touch events + else + $event.remove( document, "mousemove mouseup", drag.handler ); // remove page events + if ( dd.dragging ){ + if ( dd.drop !== false && $special.drop ) + $special.drop.handler( event, dd ); // "drop" + drag.hijack( event, "dragend", dd ); // trigger "dragend" + } + drag.textselect( true ); // enable text selection + // if suppressing click events... + if ( dd.click === false && dd.dragging ) + $.data( dd.mousedown, "suppress.click", new Date().getTime() + 5 ); + dd.dragging = drag.touched = false; // deactivate element + break; + } + }, + + // re-use event object for custom events + hijack: function( event, type, dd, x, elem ){ + // not configured + if ( !dd ) + return; + // remember the original event and type + var orig = { event:event.originalEvent, type:event.type }, + // is the event drag related or drog related? + mode = type.indexOf("drop") ? "drag" : "drop", + // iteration vars + result, i = x || 0, ia, $elems, callback, + len = !isNaN( x ) ? x : dd.interactions.length; + // modify the event type + event.type = type; + // remove the original event + event.originalEvent = null; + // initialize the results + dd.results = []; + // handle each interacted element + do if ( ia = dd.interactions[ i ] ){ + // validate the interaction + if ( type !== "dragend" && ia.cancelled ) + continue; + // set the dragdrop properties on the event object + callback = drag.properties( event, dd, ia ); + // prepare for more results + ia.results = []; + // handle each element + $( elem || ia[ mode ] || dd.droppable ).each(function( p, subject ){ + // identify drag or drop targets individually + callback.target = subject; + // force propagtion of the custom event + event.isPropagationStopped = function(){ return false; }; + // handle the event + result = subject ? $event.dispatch.call( subject, event, callback ) : null; + // stop the drag interaction for this element + if ( result === false ){ + if ( mode == "drag" ){ + ia.cancelled = true; + dd.propagates -= 1; + } + if ( type == "drop" ){ + ia[ mode ][p] = null; + } + } + // assign any dropinit elements + else if ( type == "dropinit" ) + ia.droppable.push( drag.element( result ) || subject ); + // accept a returned proxy element + if ( type == "dragstart" ) + ia.proxy = $( drag.element( result ) || ia.drag )[0]; + // remember this result + ia.results.push( result ); + // forget the event result, for recycling + delete event.result; + // break on cancelled handler + if ( type !== "dropinit" ) + return result; + }); + // flatten the results + dd.results[ i ] = drag.flatten( ia.results ); + // accept a set of valid drop targets + if ( type == "dropinit" ) + ia.droppable = drag.flatten( ia.droppable ); + // locate drop targets + if ( type == "dragstart" && !ia.cancelled ) + callback.update(); + } + while ( ++i < len ) + // restore the original event & type + event.type = orig.type; + event.originalEvent = orig.event; + // return all handler results + return drag.flatten( dd.results ); + }, + + // extend the callback object with drag/drop properties... + properties: function( event, dd, ia ){ + var obj = ia.callback; + // elements + obj.drag = ia.drag; + obj.proxy = ia.proxy || ia.drag; + // starting mouse position + obj.startX = dd.pageX; + obj.startY = dd.pageY; + // current distance dragged + obj.deltaX = event.pageX - dd.pageX; + obj.deltaY = event.pageY - dd.pageY; + // original element position + obj.originalX = ia.offset.left; + obj.originalY = ia.offset.top; + // adjusted element position + obj.offsetX = obj.originalX + obj.deltaX; + obj.offsetY = obj.originalY + obj.deltaY; + // assign the drop targets information + obj.drop = drag.flatten( ( ia.drop || [] ).slice() ); + obj.available = drag.flatten( ( ia.droppable || [] ).slice() ); + return obj; + }, + + // determine is the argument is an element or jquery instance + element: function( arg ){ + if ( arg && ( arg.jquery || arg.nodeType == 1 ) ) + return arg; + }, + + // flatten nested jquery objects and arrays into a single dimension array + flatten: function( arr ){ + return $.map( arr, function( member ){ + return member && member.jquery ? $.makeArray( member ) : + member && member.length ? drag.flatten( member ) : member; + }); + }, + + // toggles text selection attributes ON (true) or OFF (false) + textselect: function( bool ){ + $( document )[ bool ? "unbind" : "bind" ]("selectstart", drag.dontstart ) + .css("MozUserSelect", bool ? "" : "none" ); + // .attr("unselectable", bool ? "off" : "on" ) + document.unselectable = bool ? "off" : "on"; + }, + + // suppress "selectstart" and "ondragstart" events + dontstart: function(){ + return false; + }, + + // a callback instance contructor + callback: function(){} + +}; + +// callback methods +drag.callback.prototype = { + update: function(){ + if ( $special.drop && this.available.length ) + $.each( this.available, function( i ){ + $special.drop.locate( this, i ); + }); + } +}; + +// patch $.event.$dispatch to allow suppressing clicks +var $dispatch = $event.dispatch; +$event.dispatch = function( event ){ + if ( $.data( this, "suppress."+ event.type ) - new Date().getTime() > 0 ){ + $.removeData( this, "suppress."+ event.type ); + return; + } + return $dispatch.apply( this, arguments ); +}; + +// event fix hooks for touch events... +var touchHooks = +$event.fixHooks.touchstart = +$event.fixHooks.touchmove = +$event.fixHooks.touchend = +$event.fixHooks.touchcancel = { + props: "clientX clientY pageX pageY screenX screenY".split( " " ), + filter: function( event, orig ) { + if ( orig ){ + var touched = ( orig.touches && orig.touches[0] ) + || ( orig.changedTouches && orig.changedTouches[0] ) + || null; + // iOS webkit: touchstart, touchmove, touchend + if ( touched ) + $.each( touchHooks.props, function( i, prop ){ + event[ prop ] = touched[ prop ]; + }); + } + return event; + } +}; + +// share the same special event configuration with related events... +$special.draginit = $special.dragstart = $special.dragend = drag; + +})( jQuery ); \ No newline at end of file diff --git a/public/assets/js/jquery.flot.draggable.js b/public/assets/js/jquery.flot.draggable.js new file mode 100644 index 0000000..56a1a40 --- /dev/null +++ b/public/assets/js/jquery.flot.draggable.js @@ -0,0 +1,158 @@ +/* +Author: Zach Dwiel + +Flot plugin for adding point dragging capabilities to a plot. + +Heavy inspiration from Chris Leonello. Thank you! + +Example usage: + + plot = $.plot(...); + +Options: + + // to set the draggable properties of all series: + grid, xaxis, yaxis : { + draggable: boolean + } + + // to set the draggable properties of a single series: + // can also be set in the data series rather than the options, see example + series : { + draggable : boolean, + draggablex : boolean, + draggabley : boolean + } + + // series specifc options over-ride 'global' options +*/ + + +// dependencies: jquery.event.drag.js, we put them inline here to save people +// the effort of downloading them. + +/* +jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com) +Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt +*/ + +(function ($) { + var options = { + xaxis: { + draggable: false, + }, yaxis: { + draggable: false, + }, grid: { + draggable: false, + } + }, + drag = { pos: { x:null, y:null}, active: false }; + + function init(plot) { + function bindEvents(plot, eventHolder) { + var o = plot.getOptions(); + var i; + var series_draggable = false; + var series = plot.getData(); + for (i = 0; i < series.length; ++i) { + if(series[i].draggable || series[i].draggablex || series[i].draggabley) { + series_draggable = true; + } + } + if (o.grid.draggable || o.xaxis.draggable || o.yaxis.draggable || series_draggable) { + eventHolder.bind("dragstart", { distance: 10 }, function (e) { + if (e.which != 1) // only accept left-click + return false; + var plotOffset = plot.getPlotOffset(); + var offset = eventHolder.offset(), + pos = { pageX: e.pageX, pageY: e.pageY }, + canvasX = e.pageX - offset.left - plotOffset.left, + canvasY = e.pageY - offset.top - plotOffset.top; + drag.gridOffset = {top: offset.top + plotOffset.top, left: offset.left + plotOffset.left}; + + drag.item = plot.findNearbyItem(canvasX, canvasY, function (s) { return s["draggable"] != false; }); + + if (drag.item) { + drag.item.pageX = parseInt(drag.item.series.xaxis.p2c(drag.item.datapoint[0]) + offset.left + plotOffset.left); + drag.item.pageY = parseInt(drag.item.series.yaxis.p2c(drag.item.datapoint[1]) + offset.top + plotOffset.top); + drag.active = true; + } + }); + eventHolder.bind("drag", function (pos) { + var axes = plot.getAxes(); + var ax = axes.xaxis; + var ay = axes.yaxis; + var ax2 = axes.x2axis; + var ay2 = axes.y2axis; + var sidx = drag.item.seriesIndex; + var didx = drag.item.dataIndex; + var s = plot.getData()[sidx]; + + if (drag.item.series.yaxis == ay2) + ay = ay2; + if (drag.item.series.xaxis == ax2) + ax = ax2; + + var newx = ax.min + (pos.pageX-drag.gridOffset.left)/ax.scale; + var newy = ay.max - (pos.pageY-drag.gridOffset.top)/ay.scale; + +// // this version will change the data itself rather than +// // the points and then reprocess all the data and redraw. +// // NOTE: reuqires exposing plot.processData as a public +// // function in jquery.flot.js +// series[sidx].data[didx] = [newx, newy]; +// plot.processData(); + + // change the raw data instead of processing every point all over again, not as clean, but faster + var points = s.datapoints.points; + var ps = s.datapoints.pointsize; + if((o.grid.draggable || o.xaxis.draggable || s.draggablex || s.draggable) && (s.draggablex != false)) { + points[didx*ps] = newx; + } + if((o.grid.draggable || o.yaxis.draggable || s.draggabley || s.draggable) && (s.draggabley != false)) { + points[didx*ps+1] = newy; + } + + plot.draw(); + + var retx = points[didx*ps]; + var rety = points[didx*ps+1]; + + // uncomment if you are using Jonathan Leto's log plugin +// var yaxisBase = o.yaxis.base; +// var xaxisBase = o.xaxis.base; +// if (s.yaxis == axes.y2axis) +// yaxisBase = o.y2axis.base; +// if (s.xaxis == axes.x2axis) +// xaxisBase = o.x2axis.base; +// +// if ( yaxisBase > 1 ) { +// rety = Math.exp(newy*Math.LN10); +// } +// +// if ( xaxisBase > 1 ) { +// retx = Math.exp(newx*Math.LN10); +// } + + plot.getPlaceholder().trigger('plotSeriesChange', [sidx, didx, retx, rety]) + }); + eventHolder.bind("dragend", function (e) { + var sidx = drag.item.seriesIndex; + var didx = drag.item.dataIndex; + var s = plot.getData()[sidx]; + var ps = s.datapoints.pointsize; + plot.getPlaceholder().trigger('plotFinalSeriesChange', [sidx, didx, s.datapoints.points[didx*ps], s.datapoints.points[didx*ps+1]]) + }); + } + } + + plot.hooks.bindEvents.push(bindEvents); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'draggable', + version: '1.0' + }); +})(jQuery); diff --git a/public/assets/js/jquery.flot.js b/public/assets/js/jquery.flot.js new file mode 100644 index 0000000..d3f269d --- /dev/null +++ b/public/assets/js/jquery.flot.js @@ -0,0 +1,3143 @@ +/* Javascript plotting library for jQuery, version 0.8.2. + +Copyright (c) 2007-2013 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ + +// first an inline dependency, jquery.colorhelpers.js, we inline it here +// for convenience + +/* Plugin for jQuery for working with colors. + * + * Version 1.1. + * + * Inspiration from jQuery color animation plugin by John Resig. + * + * Released under the MIT license by Ole Laursen, October 2009. + * + * Examples: + * + * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() + * var c = $.color.extract($("#mydiv"), 'background-color'); + * console.log(c.r, c.g, c.b, c.a); + * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" + * + * Note that .scale() and .add() return the same modified object + * instead of making a new one. + * + * V. 1.1: Fix error handling so e.g. parsing an empty string does + * produce a color rather than just crashing. + */ +(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); + +// the actual Flot code +(function($) { + + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + + /////////////////////////////////////////////////////////////////////////// + // The Canvas object is a wrapper around an HTML5 tag. + // + // @constructor + // @param {string} cls List of classes to apply to the canvas. + // @param {element} container Element onto which to append the canvas. + // + // Requiring a container is a little iffy, but unfortunately canvas + // operations don't work unless the canvas is attached to the DOM. + + function Canvas(cls, container) { + + var element = container.children("." + cls)[0]; + + if (element == null) { + + element = document.createElement("canvas"); + element.className = cls; + + $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) + .appendTo(container); + + // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas + + if (!element.getContext) { + if (window.G_vmlCanvasManager) { + element = window.G_vmlCanvasManager.initElement(element); + } else { + throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); + } + } + } + + this.element = element; + + var context = this.context = element.getContext("2d"); + + // Determine the screen's ratio of physical to device-independent + // pixels. This is the ratio between the canvas width that the browser + // advertises and the number of pixels actually present in that space. + + // The iPhone 4, for example, has a device-independent width of 320px, + // but its screen is actually 640px wide. It therefore has a pixel + // ratio of 2, while most normal devices have a ratio of 1. + + var devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = + context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || 1; + + this.pixelRatio = devicePixelRatio / backingStoreRatio; + + // Size the canvas to match the internal dimensions of its container + + this.resize(container.width(), container.height()); + + // Collection of HTML div layers for text overlaid onto the canvas + + this.textContainer = null; + this.text = {}; + + // Cache of text fragments and metrics, so we can avoid expensively + // re-calculating them when the plot is re-rendered in a loop. + + this._textCache = {}; + } + + // Resizes the canvas to the given dimensions. + // + // @param {number} width New width of the canvas, in pixels. + // @param {number} width New height of the canvas, in pixels. + + Canvas.prototype.resize = function(width, height) { + + if (width <= 0 || height <= 0) { + throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); + } + + var element = this.element, + context = this.context, + pixelRatio = this.pixelRatio; + + // Resize the canvas, increasing its density based on the display's + // pixel ratio; basically giving it more pixels without increasing the + // size of its element, to take advantage of the fact that retina + // displays have that many more pixels in the same advertised space. + + // Resizing should reset the state (excanvas seems to be buggy though) + + if (this.width != width) { + element.width = width * pixelRatio; + element.style.width = width + "px"; + this.width = width; + } + + if (this.height != height) { + element.height = height * pixelRatio; + element.style.height = height + "px"; + this.height = height; + } + + // Save the context, so we can reset in case we get replotted. The + // restore ensure that we're really back at the initial state, and + // should be safe even if we haven't saved the initial state yet. + + context.restore(); + context.save(); + + // Scale the coordinate space to match the display density; so even though we + // may have twice as many pixels, we still want lines and other drawing to + // appear at the same size; the extra pixels will just make them crisper. + + context.scale(pixelRatio, pixelRatio); + }; + + // Clears the entire canvas area, not including any overlaid HTML text + + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + }; + + // Finishes rendering the canvas, including managing the text overlay. + + Canvas.prototype.render = function() { + + var cache = this._textCache; + + // For each text layer, add elements marked as active that haven't + // already been rendered, and remove those that are no longer active. + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + + var layer = this.getTextLayer(layerKey), + layerCache = cache[layerKey]; + + layer.hide(); + + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + + var positions = styleCache[key].positions; + + for (var i = 0, position; position = positions[i]; i++) { + if (position.active) { + if (!position.rendered) { + layer.append(position.element); + position.rendered = true; + } + } else { + positions.splice(i--, 1); + if (position.rendered) { + position.element.detach(); + } + } + } + + if (positions.length == 0) { + delete styleCache[key]; + } + } + } + } + } + + layer.show(); + } + } + }; + + // Creates (if necessary) and returns the text overlay container. + // + // @param {string} classes String of space-separated CSS classes used to + // uniquely identify the text layer. + // @return {object} The jQuery-wrapped text-layer div. + + Canvas.prototype.getTextLayer = function(classes) { + + var layer = this.text[classes]; + + // Create the text layer if it doesn't exist + + if (layer == null) { + + // Create the text layer container, if it doesn't exist + + if (this.textContainer == null) { + this.textContainer = $("
") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + 'font-size': "smaller", + color: "#545454" + }) + .insertAfter(this.element); + } + + layer = this.text[classes] = $("
") + .addClass(classes) + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .appendTo(this.textContainer); + } + + return layer; + }; + + // Creates (if necessary) and returns a text info object. + // + // The object looks like this: + // + // { + // width: Width of the text's wrapper div. + // height: Height of the text's wrapper div. + // element: The jQuery-wrapped HTML div containing the text. + // positions: Array of positions at which this text is drawn. + // } + // + // The positions array contains objects that look like this: + // + // { + // active: Flag indicating whether the text should be visible. + // rendered: Flag indicating whether the text is currently visible. + // element: The jQuery-wrapped HTML div containing the text. + // x: X coordinate at which to draw the text. + // y: Y coordinate at which to draw the text. + // } + // + // Each position after the first receives a clone of the original element. + // + // The idea is that that the width, height, and general 'identity' of the + // text is constant no matter where it is placed; the placements are a + // secondary property. + // + // Canvas maintains a cache of recently-used text info objects; getTextInfo + // either returns the cached element or creates a new entry. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {string} text Text string to retrieve info for. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @return {object} a text info object. + + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number or such + + text = "" + text; + + // If the font is a font-spec object, generate a CSS font definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + // If we can't find a matching element in our cache, create a new one + + if (info == null) { + + var element = $("
").html(text) + .css({ + position: "absolute", + 'max-width': width, + top: -9999 + }) + .appendTo(this.getTextLayer(layer)); + + if (typeof font === "object") { + element.css({ + font: textStyle, + color: font.color + }); + } else if (typeof font === "string") { + element.addClass(font); + } + + info = styleCache[text] = { + width: element.outerWidth(true), + height: element.outerHeight(true), + element: element, + positions: [] + }; + + element.detach(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + // + // The text isn't drawn immediately; it is marked as rendering, which will + // result in its addition to the canvas on the next render pass. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number} x X coordinate at which to draw the text. + // @param {number} y Y coordinate at which to draw the text. + // @param {string} text Text string to draw. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @param {string=} halign Horizontal alignment of the text; either "left", + // "center" or "right". + // @param {string=} valign Vertical alignment of the text; either "top", + // "middle" or "bottom". + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { + + var info = this.getTextInfo(layer, text, font, angle, width), + positions = info.positions; + + // Tweak the div's position to match the text's alignment + + if (halign == "center") { + x -= info.width / 2; + } else if (halign == "right") { + x -= info.width; + } + + if (valign == "middle") { + y -= info.height / 2; + } else if (valign == "bottom") { + y -= info.height; + } + + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. + + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = true; + return; + } + } + + // If the text doesn't exist at this position, create a new entry + + // For the very first position we'll re-use the original element, + // while for subsequent ones we'll clone it. + + position = { + active: true, + rendered: false, + element: positions.length ? info.element.clone() : info.element, + x: x, + y: y + }; + + positions.push(position); + + // Move the element to its final position within the container + + position.element.css({ + top: Math.round(y), + left: Math.round(x), + 'text-align': halign // In case the text wraps + }); + }; + + // Removes one or more text strings from the canvas text overlay. + // + // If no parameters are given, all text within the layer is removed. + // + // Note that the text is not immediately removed; it is simply marked as + // inactive, which will result in its removal on the next render pass. + // This avoids the performance penalty for 'clear and redraw' behavior, + // where we potentially get rid of all text on a layer, but will likely + // add back most or all of it later, as when redrawing axes, for example. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number=} x X coordinate of the text. + // @param {number=} y Y coordinate of the text. + // @param {string=} text Text string to remove. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which the text is rotated, in degrees. + // Angle is currently unused, it will be implemented in the future. + + Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { + if (text == null) { + var layerCache = this._textCache[layer]; + if (layerCache != null) { + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + var positions = styleCache[key].positions; + for (var i = 0, position; position = positions[i]; i++) { + position.active = false; + } + } + } + } + } + } + } else { + var positions = this.getTextInfo(layer, text, font, angle).positions; + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = false; + } + } + } + }; + + /////////////////////////////////////////////////////////////////////////// + // The top-level container for the entire plot. + + function Plot(placeholder, data_, options_, plugins) { + // data is on the form: + // [ series1, series2 ... ] + // where series is either just the data as [ [x1, y1], [x2, y2], ... ] + // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } + + var series = [], + options = { + // the color theme used for graphs + colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], + legend: { + show: true, + noColumns: 1, // number of colums in legend table + labelFormatter: null, // fn: string -> string + labelBoxBorderColor: "#ccc", // border color for the little label boxes + container: null, // container (as jQuery object) to put legend in, null means default on top of graph + position: "ne", // position of default legend container within plot + margin: 5, // distance from grid edge to default legend container within plot + backgroundColor: null, // null means auto-detect + backgroundOpacity: 0.85, // set to 0 to avoid background + sorted: null // default to no legend sorting + }, + xaxis: { + show: null, // null = auto-detect, true = always, false = never + position: "bottom", // or "top" + mode: null, // null or "time" + font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } + color: null, // base color, labels, ticks + tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" + transform: null, // null or f: number -> number to transform axis + inverseTransform: null, // if transform is set, this should be the inverse function + min: null, // min. value to show, null means set automatically + max: null, // max. value to show, null means set automatically + autoscaleMargin: null, // margin in % to add if auto-setting min/max + ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks + tickFormatter: null, // fn: number -> string + labelWidth: null, // size of tick labels in pixels + labelHeight: null, + reserveSpace: null, // whether to reserve space even if axis isn't shown + tickLength: null, // size in pixels of ticks, or "full" for whole line + alignTicksWithAxis: null, // axis number or null for no sync + tickDecimals: null, // no. of decimals, null means auto + tickSize: null, // number or [number, "unit"] + minTickSize: null // number or [number, "unit"] + }, + yaxis: { + autoscaleMargin: 0.02, + position: "left" // or "right" + }, + xaxes: [], + yaxes: [], + series: { + points: { + show: false, + radius: 3, + lineWidth: 2, // in pixels + fill: true, + fillColor: "#ffffff", + symbol: "circle" // or callback + }, + lines: { + // we don't put in show: false so we can see + // whether lines were actively disabled + lineWidth: 2, // in pixels + fill: false, + fillColor: null, + steps: false + // Omit 'zero', so we can later default its value to + // match that of the 'fill' option. + }, + bars: { + show: false, + lineWidth: 2, // in pixels + barWidth: 1, // in units of the x axis + fill: true, + fillColor: null, + align: "left", // "left", "right", or "center" + horizontal: false, + zero: true + }, + shadowSize: 3, + highlightColor: null + }, + grid: { + show: true, + aboveData: false, + color: "#545454", // primary color used for outline and labels + backgroundColor: null, // null for transparent, else color + borderColor: null, // set if different from the grid color + tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" + margin: 0, // distance from the canvas edge to the grid + labelMargin: 5, // in pixels + axisMargin: 8, // in pixels + borderWidth: 2, // in pixels + minBorderMargin: null, // in pixels, null means taken from points radius + markings: null, // array of ranges or fn: axes -> array of ranges + markingsColor: "#f4f4f4", + markingsLineWidth: 2, + // interactive stuff + clickable: false, + hoverable: false, + autoHighlight: true, // highlight in case mouse is near + mouseActiveRadius: 10 // how far the mouse can be away to activate an item + }, + interaction: { + redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow + }, + hooks: {} + }, + surface = null, // the canvas for the plot itself + overlay = null, // canvas for interactive stuff on top of plot + eventHolder = null, // jQuery object that events should be bound to + ctx = null, octx = null, + xaxes = [], yaxes = [], + plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, + plotWidth = 0, plotHeight = 0, + hooks = { + processOptions: [], + processRawData: [], + processDatapoints: [], + processOffset: [], + drawBackground: [], + drawSeries: [], + draw: [], + bindEvents: [], + drawOverlay: [], + shutdown: [] + }, + plot = this; + + // public functions + plot.setData = setData; + plot.setupGrid = setupGrid; + plot.draw = draw; + plot.findNearbyItem = findNearbyItem; + plot.getPlaceholder = function() { return placeholder; }; + plot.getCanvas = function() { return surface.element; }; + plot.getPlotOffset = function() { return plotOffset; }; + plot.width = function () { return plotWidth; }; + plot.height = function () { return plotHeight; }; + plot.offset = function () { + var o = eventHolder.offset(); + o.left += plotOffset.left; + o.top += plotOffset.top; + return o; + }; + plot.getData = function () { return series; }; + plot.getAxes = function () { + var res = {}, i; + $.each(xaxes.concat(yaxes), function (_, axis) { + if (axis) + res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; + }); + return res; + }; + plot.getXAxes = function () { return xaxes; }; + plot.getYAxes = function () { return yaxes; }; + plot.c2p = canvasToAxisCoords; + plot.p2c = axisToCanvasCoords; + plot.getOptions = function () { return options; }; + plot.highlight = highlight; + plot.unhighlight = unhighlight; + plot.triggerRedrawOverlay = triggerRedrawOverlay; + plot.pointOffset = function(point) { + return { + left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), + top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) + }; + }; + plot.shutdown = shutdown; + plot.destroy = function () { + shutdown(); + placeholder.removeData("plot").empty(); + + series = []; + options = null; + surface = null; + overlay = null; + eventHolder = null; + ctx = null; + octx = null; + xaxes = []; + yaxes = []; + hooks = null; + highlights = []; + plot = null; + }; + plot.resize = function () { + var width = placeholder.width(), + height = placeholder.height(); + surface.resize(width, height); + overlay.resize(width, height); + }; + + // public attributes + plot.hooks = hooks; + + // initialize + initPlugins(plot); + parseOptions(options_); + setupCanvases(); + setData(data_); + setupGrid(); + draw(); + bindEvents(); + + + function executeHooks(hook, args) { + args = [plot].concat(args); + for (var i = 0; i < hook.length; ++i) + hook[i].apply(this, args); + } + + function initPlugins() { + + // References to key classes, allowing plugins to modify them + + var classes = { + Canvas: Canvas + }; + + for (var i = 0; i < plugins.length; ++i) { + var p = plugins[i]; + p.init(plot, classes); + if (p.options) + $.extend(true, options, p.options); + } + } + + function parseOptions(opts) { + + $.extend(true, options, opts); + + // $.extend merges arrays, rather than replacing them. When less + // colors are provided than the size of the default palette, we + // end up with those colors plus the remaining defaults, which is + // not expected behavior; avoid it by replacing them here. + + if (opts && opts.colors) { + options.colors = opts.colors; + } + + if (options.xaxis.color == null) + options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + if (options.yaxis.color == null) + options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility + options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; + if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility + options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; + + if (options.grid.borderColor == null) + options.grid.borderColor = options.grid.color; + if (options.grid.tickColor == null) + options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + // Fill in defaults for axis options, including any unspecified + // font-spec fields, if a font-spec was provided. + + // If no x/y axis options were provided, create one of each anyway, + // since the rest of the code assumes that they exist. + + var i, axisOptions, axisCount, + fontSize = placeholder.css("font-size"), + fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, + fontDefaults = { + style: placeholder.css("font-style"), + size: Math.round(0.8 * fontSizeDefault), + variant: placeholder.css("font-variant"), + weight: placeholder.css("font-weight"), + family: placeholder.css("font-family") + }; + + axisCount = options.xaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.xaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.xaxis, axisOptions); + options.xaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + axisCount = options.yaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.yaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.yaxis, axisOptions); + options.yaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + // backwards compatibility, to be removed in future + if (options.xaxis.noTicks && options.xaxis.ticks == null) + options.xaxis.ticks = options.xaxis.noTicks; + if (options.yaxis.noTicks && options.yaxis.ticks == null) + options.yaxis.ticks = options.yaxis.noTicks; + if (options.x2axis) { + options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); + options.xaxes[1].position = "top"; + } + if (options.y2axis) { + options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); + options.yaxes[1].position = "right"; + } + if (options.grid.coloredAreas) + options.grid.markings = options.grid.coloredAreas; + if (options.grid.coloredAreasColor) + options.grid.markingsColor = options.grid.coloredAreasColor; + if (options.lines) + $.extend(true, options.series.lines, options.lines); + if (options.points) + $.extend(true, options.series.points, options.points); + if (options.bars) + $.extend(true, options.series.bars, options.bars); + if (options.shadowSize != null) + options.series.shadowSize = options.shadowSize; + if (options.highlightColor != null) + options.series.highlightColor = options.highlightColor; + + // save options on axes for future reference + for (i = 0; i < options.xaxes.length; ++i) + getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; + for (i = 0; i < options.yaxes.length; ++i) + getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; + + // add hooks from options + for (var n in hooks) + if (options.hooks[n] && options.hooks[n].length) + hooks[n] = hooks[n].concat(options.hooks[n]); + + executeHooks(hooks.processOptions, [options]); + } + + function setData(d) { + series = parseData(d); + fillInSeriesOptions(); + processData(); + } + + function parseData(d) { + var res = []; + for (var i = 0; i < d.length; ++i) { + var s = $.extend(true, {}, options.series); + + if (d[i].data != null) { + s.data = d[i].data; // move the data instead of deep-copy + delete d[i].data; + + $.extend(true, s, d[i]); + + d[i].data = s.data; + } + else + s.data = d[i]; + res.push(s); + } + + return res; + } + + function axisNumber(obj, coord) { + var a = obj[coord + "axis"]; + if (typeof a == "object") // if we got a real axis, extract number + a = a.n; + if (typeof a != "number") + a = 1; // default to first axis + return a; + } + + function allAxes() { + // return flat array without annoying null entries + return $.grep(xaxes.concat(yaxes), function (a) { return a; }); + } + + function canvasToAxisCoords(pos) { + // return an object with x/y corresponding to all used axes + var res = {}, i, axis; + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) + res["x" + axis.n] = axis.c2p(pos.left); + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) + res["y" + axis.n] = axis.c2p(pos.top); + } + + if (res.x1 !== undefined) + res.x = res.x1; + if (res.y1 !== undefined) + res.y = res.y1; + + return res; + } + + function axisToCanvasCoords(pos) { + // get canvas coords from the first pair of x/y found in pos + var res = {}, i, axis, key; + + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) { + key = "x" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "x"; + + if (pos[key] != null) { + res.left = axis.p2c(pos[key]); + break; + } + } + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) { + key = "y" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "y"; + + if (pos[key] != null) { + res.top = axis.p2c(pos[key]); + break; + } + } + } + + return res; + } + + function getOrCreateAxis(axes, number) { + if (!axes[number - 1]) + axes[number - 1] = { + n: number, // save the number for future reference + direction: axes == xaxes ? "x" : "y", + options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) + }; + + return axes[number - 1]; + } + + function fillInSeriesOptions() { + + var neededColors = series.length, maxIndex = -1, i; + + // Subtract the number of series that already have fixed colors or + // color indexes from the number that we still need to generate. + + for (i = 0; i < series.length; ++i) { + var sc = series[i].color; + if (sc != null) { + neededColors--; + if (typeof sc == "number" && sc > maxIndex) { + maxIndex = sc; + } + } + } + + // If any of the series have fixed color indexes, then we need to + // generate at least as many colors as the highest index. + + if (neededColors <= maxIndex) { + neededColors = maxIndex + 1; + } + + // Generate all the colors, using first the option colors and then + // variations on those colors once they're exhausted. + + var c, colors = [], colorPool = options.colors, + colorPoolSize = colorPool.length, variation = 0; + + for (i = 0; i < neededColors; i++) { + + c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); + + // Each time we exhaust the colors in the pool we adjust + // a scaling factor used to produce more variations on + // those colors. The factor alternates negative/positive + // to produce lighter/darker colors. + + // Reset the variation after every few cycles, or else + // it will end up producing only white or black colors. + + if (i % colorPoolSize == 0 && i) { + if (variation >= 0) { + if (variation < 0.5) { + variation = -variation - 0.2; + } else variation = 0; + } else variation = -variation; + } + + colors[i] = c.scale('rgb', 1 + variation); + } + + // Finalize the series options, filling in their colors + + var colori = 0, s; + for (i = 0; i < series.length; ++i) { + s = series[i]; + + // assign colors + if (s.color == null) { + s.color = colors[colori].toString(); + ++colori; + } + else if (typeof s.color == "number") + s.color = colors[s.color].toString(); + + // turn on lines automatically in case nothing is set + if (s.lines.show == null) { + var v, show = true; + for (v in s) + if (s[v] && s[v].show) { + show = false; + break; + } + if (show) + s.lines.show = true; + } + + // If nothing was provided for lines.zero, default it to match + // lines.fill, since areas by default should extend to zero. + + if (s.lines.zero == null) { + s.lines.zero = !!s.lines.fill; + } + + // setup axes + s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); + s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); + } + } + + function processData() { + var topSentry = Number.POSITIVE_INFINITY, + bottomSentry = Number.NEGATIVE_INFINITY, + fakeInfinity = Number.MAX_VALUE, + i, j, k, m, length, + s, points, ps, x, y, axis, val, f, p, + data, format; + + function updateAxis(axis, min, max) { + if (min < axis.datamin && min != -fakeInfinity) + axis.datamin = min; + if (max > axis.datamax && max != fakeInfinity) + axis.datamax = max; + } + + $.each(allAxes(), function (_, axis) { + // init axis + axis.datamin = topSentry; + axis.datamax = bottomSentry; + axis.used = false; + }); + + for (i = 0; i < series.length; ++i) { + s = series[i]; + s.datapoints = { points: [] }; + + executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); + } + + // first pass: clean and copy data + for (i = 0; i < series.length; ++i) { + s = series[i]; + + data = s.data; + format = s.datapoints.format; + + if (!format) { + format = []; + // find out how to copy + format.push({ x: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + + if (s.bars.show || (s.lines.show && s.lines.fill)) { + var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); + format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } + + s.datapoints.format = format; + } + + if (s.datapoints.pointsize != null) + continue; // already filled in + + s.datapoints.pointsize = format.length; + + ps = s.datapoints.pointsize; + points = s.datapoints.points; + + var insertSteps = s.lines.show && s.lines.steps; + s.xaxis.used = s.yaxis.used = true; + + for (j = k = 0; j < data.length; ++j, k += ps) { + p = data[j]; + + var nullify = p == null; + if (!nullify) { + for (m = 0; m < ps; ++m) { + val = p[m]; + f = format[m]; + + if (f) { + if (f.number && val != null) { + val = +val; // convert to number + if (isNaN(val)) + val = null; + else if (val == Infinity) + val = fakeInfinity; + else if (val == -Infinity) + val = -fakeInfinity; + } + + if (val == null) { + if (f.required) + nullify = true; + + if (f.defaultValue != null) + val = f.defaultValue; + } + } + + points[k + m] = val; + } + } + + if (nullify) { + for (m = 0; m < ps; ++m) { + val = points[k + m]; + if (val != null) { + f = format[m]; + // extract min/max info + if (f.autoscale !== false) { + if (f.x) { + updateAxis(s.xaxis, val, val); + } + if (f.y) { + updateAxis(s.yaxis, val, val); + } + } + } + points[k + m] = null; + } + } + else { + // a little bit of line specific stuff that + // perhaps shouldn't be here, but lacking + // better means... + if (insertSteps && k > 0 + && points[k - ps] != null + && points[k - ps] != points[k] + && points[k - ps + 1] != points[k + 1]) { + // copy the point to make room for a middle point + for (m = 0; m < ps; ++m) + points[k + ps + m] = points[k + m]; + + // middle point has same y + points[k + 1] = points[k - ps + 1]; + + // we've added a point, better reflect that + k += ps; + } + } + } + } + + // give the hooks a chance to run + for (i = 0; i < series.length; ++i) { + s = series[i]; + + executeHooks(hooks.processDatapoints, [ s, s.datapoints]); + } + + // second pass: find datamax/datamin for auto-scaling + for (i = 0; i < series.length; ++i) { + s = series[i]; + points = s.datapoints.points; + ps = s.datapoints.pointsize; + format = s.datapoints.format; + + var xmin = topSentry, ymin = topSentry, + xmax = bottomSentry, ymax = bottomSentry; + + for (j = 0; j < points.length; j += ps) { + if (points[j] == null) + continue; + + for (m = 0; m < ps; ++m) { + val = points[j + m]; + f = format[m]; + if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) + continue; + + if (f.x) { + if (val < xmin) + xmin = val; + if (val > xmax) + xmax = val; + } + if (f.y) { + if (val < ymin) + ymin = val; + if (val > ymax) + ymax = val; + } + } + } + + if (s.bars.show) { + // make sure we got room for the bar on the dancing floor + var delta; + + switch (s.bars.align) { + case "left": + delta = 0; + break; + case "right": + delta = -s.bars.barWidth; + break; + default: + delta = -s.bars.barWidth / 2; + } + + if (s.bars.horizontal) { + ymin += delta; + ymax += delta + s.bars.barWidth; + } + else { + xmin += delta; + xmax += delta + s.bars.barWidth; + } + } + + updateAxis(s.xaxis, xmin, xmax); + updateAxis(s.yaxis, ymin, ymax); + } + + $.each(allAxes(), function (_, axis) { + if (axis.datamin == topSentry) + axis.datamin = null; + if (axis.datamax == bottomSentry) + axis.datamax = null; + }); + } + + function setupCanvases() { + + // Make sure the placeholder is clear of everything except canvases + // from a previous plot in this container that we'll try to re-use. + + placeholder.css("padding", 0) // padding messes up the positioning + .children().filter(function(){ + return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); + }).remove(); + + if (placeholder.css("position") == 'static') + placeholder.css("position", "relative"); // for positioning labels and overlay + + surface = new Canvas("flot-base", placeholder); + overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features + + ctx = surface.context; + octx = overlay.context; + + // define which element we're listening for events on + eventHolder = $(overlay.element).unbind(); + + // If we're re-using a plot object, shut down the old one + + var existing = placeholder.data("plot"); + + if (existing) { + existing.shutdown(); + overlay.clear(); + } + + // save in case we get replotted + placeholder.data("plot", plot); + } + + function bindEvents() { + // bind events + if (options.grid.hoverable) { + eventHolder.mousemove(onMouseMove); + + // Use bind, rather than .mouseleave, because we officially + // still support jQuery 1.2.6, which doesn't define a shortcut + // for mouseenter or mouseleave. This was a bug/oversight that + // was fixed somewhere around 1.3.x. We can return to using + // .mouseleave when we drop support for 1.2.6. + + eventHolder.bind("mouseleave", onMouseLeave); + } + + if (options.grid.clickable) + eventHolder.click(onClick); + + executeHooks(hooks.bindEvents, [eventHolder]); + } + + function shutdown() { + if (redrawTimeout) + clearTimeout(redrawTimeout); + + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mouseleave", onMouseLeave); + eventHolder.unbind("click", onClick); + + executeHooks(hooks.shutdown, [eventHolder]); + } + + function setTransformationHelpers(axis) { + // set helper functions on the axis, assumes plot area + // has been computed already + + function identity(x) { return x; } + + var s, m, t = axis.options.transform || identity, + it = axis.options.inverseTransform; + + // precompute how much the axis is scaling a point + // in canvas space + if (axis.direction == "x") { + s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); + m = Math.min(t(axis.max), t(axis.min)); + } + else { + s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); + s = -s; + m = Math.max(t(axis.max), t(axis.min)); + } + + // data point to canvas coordinate + if (t == identity) // slight optimization + axis.p2c = function (p) { return (p - m) * s; }; + else + axis.p2c = function (p) { return (t(p) - m) * s; }; + // canvas coordinate to data point + if (!it) + axis.c2p = function (c) { return m + c / s; }; + else + axis.c2p = function (c) { return it(m + c / s); }; + } + + function measureTickLabels(axis) { + + var opts = axis.options, + ticks = axis.ticks || [], + labelWidth = opts.labelWidth || 0, + labelHeight = opts.labelHeight || 0, + maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = opts.font || "flot-tick-label tickLabel"; + + for (var i = 0; i < ticks.length; ++i) { + + var t = ticks[i]; + + if (!t.label) + continue; + + var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); + + labelWidth = Math.max(labelWidth, info.width); + labelHeight = Math.max(labelHeight, info.height); + } + + axis.labelWidth = opts.labelWidth || labelWidth; + axis.labelHeight = opts.labelHeight || labelHeight; + } + + function allocateAxisBoxFirstPhase(axis) { + // find the bounding box of the axis by looking at label + // widths/heights and ticks, make room by diminishing the + // plotOffset; this first phase only looks at one + // dimension per axis, the other dimension depends on the + // other axes so will have to wait + + var lw = axis.labelWidth, + lh = axis.labelHeight, + pos = axis.options.position, + isXAxis = axis.direction === "x", + tickLength = axis.options.tickLength, + axisMargin = options.grid.axisMargin, + padding = options.grid.labelMargin, + innermost = true, + outermost = true, + first = true, + found = false; + + // Determine the axis's position in its direction and on its side + + $.each(isXAxis ? xaxes : yaxes, function(i, a) { + if (a && a.reserveSpace) { + if (a === axis) { + found = true; + } else if (a.options.position === pos) { + if (found) { + outermost = false; + } else { + innermost = false; + } + } + if (!found) { + first = false; + } + } + }); + + // The outermost axis on each side has no margin + + if (outermost) { + axisMargin = 0; + } + + // The ticks for the first axis in each direction stretch across + + if (tickLength == null) { + tickLength = first ? "full" : 5; + } + + if (!isNaN(+tickLength)) + padding += +tickLength; + + if (isXAxis) { + lh += padding; + + if (pos == "bottom") { + plotOffset.bottom += lh + axisMargin; + axis.box = { top: surface.height - plotOffset.bottom, height: lh }; + } + else { + axis.box = { top: plotOffset.top + axisMargin, height: lh }; + plotOffset.top += lh + axisMargin; + } + } + else { + lw += padding; + + if (pos == "left") { + axis.box = { left: plotOffset.left + axisMargin, width: lw }; + plotOffset.left += lw + axisMargin; + } + else { + plotOffset.right += lw + axisMargin; + axis.box = { left: surface.width - plotOffset.right, width: lw }; + } + } + + // save for future reference + axis.position = pos; + axis.tickLength = tickLength; + axis.box.padding = padding; + axis.innermost = innermost; + } + + function allocateAxisBoxSecondPhase(axis) { + // now that all axis boxes have been placed in one + // dimension, we can set the remaining dimension coordinates + if (axis.direction == "x") { + axis.box.left = plotOffset.left - axis.labelWidth / 2; + axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; + } + else { + axis.box.top = plotOffset.top - axis.labelHeight / 2; + axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; + } + } + + function adjustLayoutForThingsStickingOut() { + // possibly adjust plot offset to ensure everything stays + // inside the canvas and isn't clipped off + + var minMargin = options.grid.minBorderMargin, + axis, i; + + // check stuff from the plot (FIXME: this should just read + // a value from the series, otherwise it's impossible to + // customize) + if (minMargin == null) { + minMargin = 0; + for (i = 0; i < series.length; ++i) + minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); + } + + var margins = { + left: minMargin, + right: minMargin, + top: minMargin, + bottom: minMargin + }; + + // check axis labels, note we don't check the actual + // labels but instead use the overall width/height to not + // jump as much around with replots + $.each(allAxes(), function (_, axis) { + if (axis.reserveSpace && axis.ticks && axis.ticks.length) { + var lastTick = axis.ticks[axis.ticks.length - 1]; + if (axis.direction === "x") { + margins.left = Math.max(margins.left, axis.labelWidth / 2); + if (lastTick.v <= axis.max) { + margins.right = Math.max(margins.right, axis.labelWidth / 2); + } + } else { + margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); + if (lastTick.v <= axis.max) { + margins.top = Math.max(margins.top, axis.labelHeight / 2); + } + } + } + }); + + plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); + plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); + plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); + plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); + } + + function setupGrid() { + var i, axes = allAxes(), showGrid = options.grid.show; + + // Initialize the plot's offset from the edge of the canvas + + for (var a in plotOffset) { + var margin = options.grid.margin || 0; + plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; + } + + executeHooks(hooks.processOffset, [plotOffset]); + + // If the grid is visible, add its border width to the offset + + for (var a in plotOffset) { + if(typeof(options.grid.borderWidth) == "object") { + plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; + } + else { + plotOffset[a] += showGrid ? options.grid.borderWidth : 0; + } + } + + // init axes + $.each(axes, function (_, axis) { + axis.show = axis.options.show; + if (axis.show == null) + axis.show = axis.used; // by default an axis is visible if it's got data + + axis.reserveSpace = axis.show || axis.options.reserveSpace; + + setRange(axis); + }); + + if (showGrid) { + + var allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; }); + + $.each(allocatedAxes, function (_, axis) { + // make the ticks + setupTickGeneration(axis); + setTicks(axis); + snapRangeToTicks(axis, axis.ticks); + // find labelWidth/Height for axis + measureTickLabels(axis); + }); + + // with all dimensions calculated, we can compute the + // axis bounding boxes, start from the outside + // (reverse order) + for (i = allocatedAxes.length - 1; i >= 0; --i) + allocateAxisBoxFirstPhase(allocatedAxes[i]); + + // make sure we've got enough space for things that + // might stick out + adjustLayoutForThingsStickingOut(); + + $.each(allocatedAxes, function (_, axis) { + allocateAxisBoxSecondPhase(axis); + }); + } + + plotWidth = surface.width - plotOffset.left - plotOffset.right; + plotHeight = surface.height - plotOffset.bottom - plotOffset.top; + + // now we got the proper plot dimensions, we can compute the scaling + $.each(axes, function (_, axis) { + setTransformationHelpers(axis); + }); + + if (showGrid) { + drawAxisLabels(); + } + + insertLegend(); + } + + function setRange(axis) { + var opts = axis.options, + min = +(opts.min != null ? opts.min : axis.datamin), + max = +(opts.max != null ? opts.max : axis.datamax), + delta = max - min; + + if (delta == 0.0) { + // degenerate case + var widen = max == 0 ? 1 : 0.01; + + if (opts.min == null) + min -= widen; + // always widen max if we couldn't widen min to ensure we + // don't fall into min == max which doesn't work + if (opts.max == null || opts.min != null) + max += widen; + } + else { + // consider autoscaling + var margin = opts.autoscaleMargin; + if (margin != null) { + if (opts.min == null) { + min -= delta * margin; + // make sure we don't go below zero if all values + // are positive + if (min < 0 && axis.datamin != null && axis.datamin >= 0) + min = 0; + } + if (opts.max == null) { + max += delta * margin; + if (max > 0 && axis.datamax != null && axis.datamax <= 0) + max = 0; + } + } + } + axis.min = min; + axis.max = max; + } + + function setupTickGeneration(axis) { + var opts = axis.options; + + // estimate number of ticks + var noTicks; + if (typeof opts.ticks == "number" && opts.ticks > 0) + noTicks = opts.ticks; + else + // heuristic based on the model a*sqrt(x) fitted to + // some data points that seemed reasonable + noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); + + var delta = (axis.max - axis.min) / noTicks, + dec = -Math.floor(Math.log(delta) / Math.LN10), + maxDec = opts.tickDecimals; + + if (maxDec != null && dec > maxDec) { + dec = maxDec; + } + + var magn = Math.pow(10, -dec), + norm = delta / magn, // norm is between 1.0 and 10.0 + size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + if (opts.minTickSize != null && size < opts.minTickSize) { + size = opts.minTickSize; + } + + axis.delta = delta; + axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + axis.tickSize = opts.tickSize || size; + + // Time mode was moved to a plug-in in 0.8, but since so many people use this + // we'll add an especially friendly make sure they remembered to include it. + + if (opts.mode == "time" && !axis.tickGenerator) { + throw new Error("Time mode requires the flot.time plugin."); + } + + // Flot supports base-10 axes; any other mode else is handled by a plug-in, + // like flot.time.js. + + if (!axis.tickGenerator) { + + axis.tickGenerator = function (axis) { + + var ticks = [], + start = floorInBase(axis.min, axis.tickSize), + i = 0, + v = Number.NaN, + prev; + + do { + prev = v; + v = start + i * axis.tickSize; + ticks.push(v); + ++i; + } while (v < axis.max && v != prev); + return ticks; + }; + + axis.tickFormatter = function (value, axis) { + + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; + var formatted = "" + Math.round(value * factor) / factor; + + // If tickDecimals was specified, ensure that we have exactly that + // much precision; otherwise default to the value's own precision. + + if (axis.tickDecimals != null) { + var decimal = formatted.indexOf("."); + var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; + if (precision < axis.tickDecimals) { + return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); + } + } + + return formatted; + }; + } + + if ($.isFunction(opts.tickFormatter)) + axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; + + if (opts.alignTicksWithAxis != null) { + var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; + if (otherAxis && otherAxis.used && otherAxis != axis) { + // consider snapping min/max to outermost nice ticks + var niceTicks = axis.tickGenerator(axis); + if (niceTicks.length > 0) { + if (opts.min == null) + axis.min = Math.min(axis.min, niceTicks[0]); + if (opts.max == null && niceTicks.length > 1) + axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); + } + + axis.tickGenerator = function (axis) { + // copy ticks, scaled to this axis + var ticks = [], v, i; + for (i = 0; i < otherAxis.ticks.length; ++i) { + v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); + v = axis.min + v * (axis.max - axis.min); + ticks.push(v); + } + return ticks; + }; + + // we might need an extra decimal since forced + // ticks don't necessarily fit naturally + if (!axis.mode && opts.tickDecimals == null) { + var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), + ts = axis.tickGenerator(axis); + + // only proceed if the tick interval rounded + // with an extra decimal doesn't give us a + // zero at end + if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) + axis.tickDecimals = extraDec; + } + } + } + } + + function setTicks(axis) { + var oticks = axis.options.ticks, ticks = []; + if (oticks == null || (typeof oticks == "number" && oticks > 0)) + ticks = axis.tickGenerator(axis); + else if (oticks) { + if ($.isFunction(oticks)) + // generate the ticks + ticks = oticks(axis); + else + ticks = oticks; + } + + // clean up/labelify the supplied ticks, copy them over + var i, v; + axis.ticks = []; + for (i = 0; i < ticks.length; ++i) { + var label = null; + var t = ticks[i]; + if (typeof t == "object") { + v = +t[0]; + if (t.length > 1) + label = t[1]; + } + else + v = +t; + if (label == null) + label = axis.tickFormatter(v, axis); + if (!isNaN(v)) + axis.ticks.push({ v: v, label: label }); + } + } + + function snapRangeToTicks(axis, ticks) { + if (axis.options.autoscaleMargin && ticks.length > 0) { + // snap to ticks + if (axis.options.min == null) + axis.min = Math.min(axis.min, ticks[0].v); + if (axis.options.max == null && ticks.length > 1) + axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); + } + } + + function draw() { + + surface.clear(); + + executeHooks(hooks.drawBackground, [ctx]); + + var grid = options.grid; + + // draw background, if any + if (grid.show && grid.backgroundColor) + drawBackground(); + + if (grid.show && !grid.aboveData) { + drawGrid(); + } + + for (var i = 0; i < series.length; ++i) { + executeHooks(hooks.drawSeries, [ctx, series[i]]); + drawSeries(series[i]); + } + + executeHooks(hooks.draw, [ctx]); + + if (grid.show && grid.aboveData) { + drawGrid(); + } + + surface.render(); + + // A draw implies that either the axes or data have changed, so we + // should probably update the overlay highlights as well. + + triggerRedrawOverlay(); + } + + function extractRange(ranges, coord) { + var axis, from, to, key, axes = allAxes(); + + for (var i = 0; i < axes.length; ++i) { + axis = axes[i]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? xaxes[0] : yaxes[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function drawBackground() { + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); + ctx.fillRect(0, 0, plotWidth, plotHeight); + ctx.restore(); + } + + function drawGrid() { + var i, axes, bw, bc; + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // draw markings + var markings = options.grid.markings; + if (markings) { + if ($.isFunction(markings)) { + axes = plot.getAxes(); + // xmin etc. is backwards compatibility, to be + // removed in the future + axes.xmin = axes.xaxis.min; + axes.xmax = axes.xaxis.max; + axes.ymin = axes.yaxis.min; + axes.ymax = axes.yaxis.max; + + markings = markings(axes); + } + + for (i = 0; i < markings.length; ++i) { + var m = markings[i], + xrange = extractRange(m, "x"), + yrange = extractRange(m, "y"); + + // fill in missing + if (xrange.from == null) + xrange.from = xrange.axis.min; + if (xrange.to == null) + xrange.to = xrange.axis.max; + if (yrange.from == null) + yrange.from = yrange.axis.min; + if (yrange.to == null) + yrange.to = yrange.axis.max; + + // clip + if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || + yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) + continue; + + xrange.from = Math.max(xrange.from, xrange.axis.min); + xrange.to = Math.min(xrange.to, xrange.axis.max); + yrange.from = Math.max(yrange.from, yrange.axis.min); + yrange.to = Math.min(yrange.to, yrange.axis.max); + + if (xrange.from == xrange.to && yrange.from == yrange.to) + continue; + + // then draw + xrange.from = xrange.axis.p2c(xrange.from); + xrange.to = xrange.axis.p2c(xrange.to); + yrange.from = yrange.axis.p2c(yrange.from); + yrange.to = yrange.axis.p2c(yrange.to); + + if (xrange.from == xrange.to || yrange.from == yrange.to) { + // draw line + ctx.beginPath(); + ctx.strokeStyle = m.color || options.grid.markingsColor; + ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth; + ctx.moveTo(xrange.from, yrange.from); + ctx.lineTo(xrange.to, yrange.to); + ctx.stroke(); + } + else { + // fill area + ctx.fillStyle = m.color || options.grid.markingsColor; + ctx.fillRect(xrange.from, yrange.to, + xrange.to - xrange.from, + yrange.from - yrange.to); + } + } + } + + // draw the ticks + axes = allAxes(); + bw = options.grid.borderWidth; + + for (var j = 0; j < axes.length; ++j) { + var axis = axes[j], box = axis.box, + t = axis.tickLength, x, y, xoff, yoff; + if (!axis.show || axis.ticks.length == 0) + continue; + + ctx.lineWidth = 1; + + // find the edges + if (axis.direction == "x") { + x = 0; + if (t == "full") + y = (axis.position == "top" ? 0 : plotHeight); + else + y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); + } + else { + y = 0; + if (t == "full") + x = (axis.position == "left" ? 0 : plotWidth); + else + x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); + } + + // draw tick bar + if (!axis.innermost) { + ctx.strokeStyle = axis.options.color; + ctx.beginPath(); + xoff = yoff = 0; + if (axis.direction == "x") + xoff = plotWidth + 1; + else + yoff = plotHeight + 1; + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") { + y = Math.floor(y) + 0.5; + } else { + x = Math.floor(x) + 0.5; + } + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + ctx.stroke(); + } + + // draw ticks + + ctx.strokeStyle = axis.options.tickColor; + + ctx.beginPath(); + for (i = 0; i < axis.ticks.length; ++i) { + var v = axis.ticks[i].v; + + xoff = yoff = 0; + + if (isNaN(v) || v < axis.min || v > axis.max + // skip those lying on the axes if we got a border + || (t == "full" + && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) + && (v == axis.min || v == axis.max))) + continue; + + if (axis.direction == "x") { + x = axis.p2c(v); + yoff = t == "full" ? -plotHeight : t; + + if (axis.position == "top") + yoff = -yoff; + } + else { + y = axis.p2c(v); + xoff = t == "full" ? -plotWidth : t; + + if (axis.position == "left") + xoff = -xoff; + } + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") + x = Math.floor(x) + 0.5; + else + y = Math.floor(y) + 0.5; + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + } + + ctx.stroke(); + } + + + // draw border + if (bw) { + // If either borderWidth or borderColor is an object, then draw the border + // line by line instead of as one rectangle + bc = options.grid.borderColor; + if(typeof bw == "object" || typeof bc == "object") { + if (typeof bw !== "object") { + bw = {top: bw, right: bw, bottom: bw, left: bw}; + } + if (typeof bc !== "object") { + bc = {top: bc, right: bc, bottom: bc, left: bc}; + } + + if (bw.top > 0) { + ctx.strokeStyle = bc.top; + ctx.lineWidth = bw.top; + ctx.beginPath(); + ctx.moveTo(0 - bw.left, 0 - bw.top/2); + ctx.lineTo(plotWidth, 0 - bw.top/2); + ctx.stroke(); + } + + if (bw.right > 0) { + ctx.strokeStyle = bc.right; + ctx.lineWidth = bw.right; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); + ctx.lineTo(plotWidth + bw.right / 2, plotHeight); + ctx.stroke(); + } + + if (bw.bottom > 0) { + ctx.strokeStyle = bc.bottom; + ctx.lineWidth = bw.bottom; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); + ctx.lineTo(0, plotHeight + bw.bottom / 2); + ctx.stroke(); + } + + if (bw.left > 0) { + ctx.strokeStyle = bc.left; + ctx.lineWidth = bw.left; + ctx.beginPath(); + ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); + ctx.lineTo(0- bw.left/2, 0); + ctx.stroke(); + } + } + else { + ctx.lineWidth = bw; + ctx.strokeStyle = options.grid.borderColor; + ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); + } + } + + ctx.restore(); + } + + function drawAxisLabels() { + + $.each(allAxes(), function (_, axis) { + var box = axis.box, + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = axis.options.font || "flot-tick-label tickLabel", + tick, x, y, halign, valign; + + // Remove text before checking for axis.show and ticks.length; + // otherwise plugins, like flot-tickrotor, that draw their own + // tick labels will end up with both theirs and the defaults. + + surface.removeText(layer); + + if (!axis.show || axis.ticks.length == 0) + return; + + for (var i = 0; i < axis.ticks.length; ++i) { + + tick = axis.ticks[i]; + if (!tick.label || tick.v < axis.min || tick.v > axis.max) + continue; + + if (axis.direction == "x") { + halign = "center"; + x = plotOffset.left + axis.p2c(tick.v); + if (axis.position == "bottom") { + y = box.top + box.padding; + } else { + y = box.top + box.height - box.padding; + valign = "bottom"; + } + } else { + valign = "middle"; + y = plotOffset.top + axis.p2c(tick.v); + if (axis.position == "left") { + x = box.left + box.width - box.padding; + halign = "right"; + } else { + x = box.left + box.padding; + } + } + + surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); + } + }); + } + + function drawSeries(series) { + if (series.lines.show) + drawSeriesLines(series); + if (series.bars.show) + drawSeriesBars(series); + if (series.points.show) + drawSeriesPoints(series); + } + + function drawSeriesLines(series) { + function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + prevx = null, prevy = null; + + ctx.beginPath(); + for (var i = ps; i < points.length; i += ps) { + var x1 = points[i - ps], y1 = points[i - ps + 1], + x2 = points[i], y2 = points[i + 1]; + + if (x1 == null || x2 == null) + continue; + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min) { + if (y2 < axisy.min) + continue; // line segment is outside + // compute new intersection point + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min) { + if (y1 < axisy.min) + continue; + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max) { + if (y2 > axisy.max) + continue; + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max) { + if (y1 > axisy.max) + continue; + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (x1 != prevx || y1 != prevy) + ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); + + prevx = x2; + prevy = y2; + ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); + } + ctx.stroke(); + } + + function plotLineArea(datapoints, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + bottom = Math.min(Math.max(0, axisy.min), axisy.max), + i = 0, top, areaOpen = false, + ypos = 1, segmentStart = 0, segmentEnd = 0; + + // we process each segment in two turns, first forward + // direction to sketch out top, then once we hit the + // end we go backwards to sketch the bottom + while (true) { + if (ps > 0 && i > points.length + ps) + break; + + i += ps; // ps is negative if going backwards + + var x1 = points[i - ps], + y1 = points[i - ps + ypos], + x2 = points[i], y2 = points[i + ypos]; + + if (areaOpen) { + if (ps > 0 && x1 != null && x2 == null) { + // at turning point + segmentEnd = i; + ps = -ps; + ypos = 2; + continue; + } + + if (ps < 0 && i == segmentStart + ps) { + // done with the reverse sweep + ctx.fill(); + areaOpen = false; + ps = -ps; + ypos = 1; + i = segmentStart = segmentEnd + ps; + continue; + } + } + + if (x1 == null || x2 == null) + continue; + + // clip x values + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (!areaOpen) { + // open area + ctx.beginPath(); + ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); + areaOpen = true; + } + + // now first check the case where both is outside + if (y1 >= axisy.max && y2 >= axisy.max) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); + continue; + } + else if (y1 <= axisy.min && y2 <= axisy.min) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); + continue; + } + + // else it's a bit more complicated, there might + // be a flat maxed out rectangle first, then a + // triangular cutout or reverse; to find these + // keep track of the current x values + var x1old = x1, x2old = x2; + + // clip the y values, without shortcutting, we + // go through all cases in turn + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // if the x value was changed we got a rectangle + // to fill + if (x1 != x1old) { + ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); + // it goes to (x1, y1), but we fill that below + } + + // fill triangular section, this sometimes result + // in redundant points if (x1, y1) hasn't changed + // from previous line to, but we just ignore that + ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + + // fill the other rectangle if it's there + if (x2 != x2old) { + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); + } + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + ctx.lineJoin = "round"; + + var lw = series.lines.lineWidth, + sw = series.shadowSize; + // FIXME: consider another form of shadow when filling is turned on + if (lw > 0 && sw > 0) { + // draw shadow as a thick and thin line with transparency + ctx.lineWidth = sw; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + // position shadow at angle from the mid of line + var angle = Math.PI/18; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); + ctx.lineWidth = sw/2; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); + if (fillStyle) { + ctx.fillStyle = fillStyle; + plotLineArea(series.datapoints, series.xaxis, series.yaxis); + } + + if (lw > 0) + plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); + ctx.restore(); + } + + function drawSeriesPoints(series) { + function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + var x = points[i], y = points[i + 1]; + if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + continue; + + ctx.beginPath(); + x = axisx.p2c(x); + y = axisy.p2c(y) + offset; + if (symbol == "circle") + ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); + else + symbol(ctx, x, y, radius, shadow); + ctx.closePath(); + + if (fillStyle) { + ctx.fillStyle = fillStyle; + ctx.fill(); + } + ctx.stroke(); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var lw = series.points.lineWidth, + sw = series.shadowSize, + radius = series.points.radius, + symbol = series.points.symbol; + + // If the user sets the line width to 0, we change it to a very + // small value. A line width of 0 seems to force the default of 1. + // Doing the conditional here allows the shadow setting to still be + // optional even with a lineWidth of 0. + + if( lw == 0 ) + lw = 0.0001; + + if (lw > 0 && sw > 0) { + // draw shadow in two steps + var w = sw / 2; + ctx.lineWidth = w; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + plotPoints(series.datapoints, radius, null, w + w/2, true, + series.xaxis, series.yaxis, symbol); + + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + plotPoints(series.datapoints, radius, null, w/2, true, + series.xaxis, series.yaxis, symbol); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + plotPoints(series.datapoints, radius, + getFillStyle(series.points, series.color), 0, false, + series.xaxis, series.yaxis, symbol); + ctx.restore(); + } + + function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { + var left, right, bottom, top, + drawLeft, drawRight, drawTop, drawBottom, + tmp; + + // in horizontal mode, we start the bar from the left + // instead of from the bottom so it appears to be + // horizontal rather than vertical + if (horizontal) { + drawBottom = drawRight = drawTop = true; + drawLeft = false; + left = b; + right = x; + top = y + barLeft; + bottom = y + barRight; + + // account for negative bars + if (right < left) { + tmp = right; + right = left; + left = tmp; + drawLeft = true; + drawRight = false; + } + } + else { + drawLeft = drawRight = drawTop = true; + drawBottom = false; + left = x + barLeft; + right = x + barRight; + bottom = b; + top = y; + + // account for negative bars + if (top < bottom) { + tmp = top; + top = bottom; + bottom = tmp; + drawBottom = true; + drawTop = false; + } + } + + // clip + if (right < axisx.min || left > axisx.max || + top < axisy.min || bottom > axisy.max) + return; + + if (left < axisx.min) { + left = axisx.min; + drawLeft = false; + } + + if (right > axisx.max) { + right = axisx.max; + drawRight = false; + } + + if (bottom < axisy.min) { + bottom = axisy.min; + drawBottom = false; + } + + if (top > axisy.max) { + top = axisy.max; + drawTop = false; + } + + left = axisx.p2c(left); + bottom = axisy.p2c(bottom); + right = axisx.p2c(right); + top = axisy.p2c(top); + + // fill the bar + if (fillStyleCallback) { + c.fillStyle = fillStyleCallback(bottom, top); + c.fillRect(left, top, right - left, bottom - top) + } + + // draw outline + if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { + c.beginPath(); + + // FIXME: inline moveTo is buggy with excanvas + c.moveTo(left, bottom); + if (drawLeft) + c.lineTo(left, top); + else + c.moveTo(left, top); + if (drawTop) + c.lineTo(right, top); + else + c.moveTo(right, top); + if (drawRight) + c.lineTo(right, bottom); + else + c.moveTo(right, bottom); + if (drawBottom) + c.lineTo(left, bottom); + else + c.moveTo(left, bottom); + c.stroke(); + } + } + + function drawSeriesBars(series) { + function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) + continue; + drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // FIXME: figure out a way to add shadows (for instance along the right edge) + ctx.lineWidth = series.bars.lineWidth; + ctx.strokeStyle = series.color; + + var barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; + plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); + ctx.restore(); + } + + function getFillStyle(filloptions, seriesColor, bottom, top) { + var fill = filloptions.fill; + if (!fill) + return null; + + if (filloptions.fillColor) + return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); + + var c = $.color.parse(seriesColor); + c.a = typeof fill == "number" ? fill : 0.4; + c.normalize(); + return c.toString(); + } + + function insertLegend() { + + if (options.legend.container != null) { + $(options.legend.container).html(""); + } else { + placeholder.find(".legend").remove(); + } + + if (!options.legend.show) { + return; + } + + var fragments = [], entries = [], rowStarted = false, + lf = options.legend.labelFormatter, s, label; + + // Build a list of legend entries, with each having a label and a color + + for (var i = 0; i < series.length; ++i) { + s = series[i]; + if (s.label) { + label = lf ? lf(s.label, s) : s.label; + if (label) { + entries.push({ + label: label, + color: s.color + }); + } + } + } + + // Sort the legend using either the default or a custom comparator + + if (options.legend.sorted) { + if ($.isFunction(options.legend.sorted)) { + entries.sort(options.legend.sorted); + } else if (options.legend.sorted == "reverse") { + entries.reverse(); + } else { + var ascending = options.legend.sorted != "descending"; + entries.sort(function(a, b) { + return a.label == b.label ? 0 : ( + (a.label < b.label) != ascending ? 1 : -1 // Logical XOR + ); + }); + } + } + + // Generate markup for the list of entries, in their final order + + for (var i = 0; i < entries.length; ++i) { + + var entry = entries[i]; + + if (i % options.legend.noColumns == 0) { + if (rowStarted) + fragments.push(''); + fragments.push(''); + rowStarted = true; + } + + fragments.push( + '
' + + '' + entry.label + '' + ); + } + + if (rowStarted) + fragments.push(''); + + if (fragments.length == 0) + return; + + var table = '' + fragments.join("") + '
'; + if (options.legend.container != null) + $(options.legend.container).html(table); + else { + var pos = "", + p = options.legend.position, + m = options.legend.margin; + if (m[0] == null) + m = [m, m]; + if (p.charAt(0) == "n") + pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; + else if (p.charAt(0) == "s") + pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; + if (p.charAt(1) == "e") + pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; + else if (p.charAt(1) == "w") + pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; + var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); + if (options.legend.backgroundOpacity != 0.0) { + // put in the transparent background + // separately to avoid blended labels and + // label boxes + var c = options.legend.backgroundColor; + if (c == null) { + c = options.grid.backgroundColor; + if (c && typeof c == "string") + c = $.color.parse(c); + else + c = $.color.extract(legend, 'background-color'); + c.a = 1; + c = c.toString(); + } + var div = legend.children(); + $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); + } + } + } + + + // interactive features + + var highlights = [], + redrawTimeout = null; + + // returns the data item the mouse is over, or null if none is found + function findNearbyItem(mouseX, mouseY, seriesFilter) { + var maxDistance = options.grid.mouseActiveRadius, + smallestDistance = maxDistance * maxDistance + 1, + item = null, foundPoint = false, i, j, ps; + + console.log('max dist:' + maxDistance); + + + + for (i = series.length - 1; i >= 0; --i) { + if (!seriesFilter(series[i])) + continue; + + var s = series[i], + axisx = s.xaxis, + axisy = s.yaxis, + points = s.datapoints.points, + mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster + my = axisy.c2p(mouseY), + maxx = maxDistance / axisx.scale, + maxy = maxDistance / axisy.scale; + + ps = s.datapoints.pointsize; + // with inverse transforms, we can't use the maxx/maxy + // optimization, sadly + if (axisx.options.inverseTransform) + maxx = Number.MAX_VALUE; + if (axisy.options.inverseTransform) + maxy = Number.MAX_VALUE; + + if (s.lines.show || s.points.show) { + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1]; + if (x == null) + continue; + + // For points and lines, the cursor must be within a + // certain distance to the data point + if (x - mx > maxx || x - mx < -maxx || + y - my > maxy || y - my < -maxy) + continue; + + // We have to calculate distances in pixels, not in + // data units, because the scales of the axes may be different + var dx = Math.abs(axisx.p2c(x) - mouseX), + dy = Math.abs(axisy.p2c(y) - mouseY), + dist = dx * dx + dy * dy; // we save the sqrt + + // use <= to ensure last point takes precedence + // (last generally means on top of) + if (dist < smallestDistance) { + smallestDistance = dist; + item = [i, j / ps]; + } + } + } + + if (s.bars.show && !item) { // no other point can be nearby + + var barLeft, barRight; + + switch (s.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -s.bars.barWidth; + break; + default: + barLeft = -s.bars.barWidth / 2; + } + + barRight = barLeft + s.bars.barWidth; + + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1], b = points[j + 2]; + if (x == null) + continue; + + // for a bar graph, the cursor must be inside the bar + if (series[i].bars.horizontal ? + (mx <= Math.max(b, x) && mx >= Math.min(b, x) && + my >= y + barLeft && my <= y + barRight) : + (mx >= x + barLeft && mx <= x + barRight && + my >= Math.min(b, y) && my <= Math.max(b, y))) + item = [i, j / ps]; + } + } + } + + if (item) { + i = item[0]; + j = item[1]; + ps = series[i].datapoints.pointsize; + + return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), + dataIndex: j, + series: series[i], + seriesIndex: i }; + + } + + return null; + } + + function onMouseMove(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return s["hoverable"] != false; }); + } + + function onMouseLeave(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return false; }); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e, + function (s) { return s["clickable"] != false; }); + } + + // trigger click or hover event (they send the same parameters + // so we share their code) + function triggerClickHoverEvent(eventname, event, seriesFilter) { + var offset = eventHolder.offset(), + canvasX = event.pageX - offset.left - plotOffset.left, + canvasY = event.pageY - offset.top - plotOffset.top, + pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); + + pos.pageX = event.pageX; + pos.pageY = event.pageY; + + var item = findNearbyItem(canvasX, canvasY, seriesFilter); + + if (item) { + // fill in mouse pos for any listeners out there + item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); + item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); + } + + if (options.grid.autoHighlight) { + // clear auto-highlights + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto == eventname && + !(item && h.series == item.series && + h.point[0] == item.datapoint[0] && + h.point[1] == item.datapoint[1])) + unhighlight(h.series, h.point); + } + + if (item) + highlight(item.series, item.datapoint, eventname); + } + + placeholder.trigger(eventname, [ pos, item ]); + } + + function triggerRedrawOverlay() { + var t = options.interaction.redrawOverlayInterval; + if (t == -1) { // skip event queue + drawOverlay(); + return; + } + + if (!redrawTimeout) + redrawTimeout = setTimeout(drawOverlay, t); + } + + function drawOverlay() { + redrawTimeout = null; + + // draw highlights + octx.save(); + overlay.clear(); + octx.translate(plotOffset.left, plotOffset.top); + + var i, hi; + for (i = 0; i < highlights.length; ++i) { + hi = highlights[i]; + + if (hi.series.bars.show) + drawBarHighlight(hi.series, hi.point); + else + drawPointHighlight(hi.series, hi.point); + } + octx.restore(); + + executeHooks(hooks.drawOverlay, [octx]); + } + + function highlight(s, point, auto) { + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i == -1) { + highlights.push({ series: s, point: point, auto: auto }); + + triggerRedrawOverlay(); + } + else if (!auto) + highlights[i].auto = false; + } + + function unhighlight(s, point) { + if (s == null && point == null) { + highlights = []; + triggerRedrawOverlay(); + return; + } + + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i != -1) { + highlights.splice(i, 1); + + triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s, p) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s && h.point[0] == p[0] + && h.point[1] == p[1]) + return i; + } + return -1; + } + + function drawPointHighlight(series, point) { + var x = point[0], y = point[1], + axisx = series.xaxis, axisy = series.yaxis, + highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); + + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + return; + + var pointRadius = series.points.radius + series.points.lineWidth / 2; + octx.lineWidth = pointRadius; + octx.strokeStyle = highlightColor; + var radius = 1.5 * pointRadius; + x = axisx.p2c(x); + y = axisy.p2c(y); + + octx.beginPath(); + if (series.points.symbol == "circle") + octx.arc(x, y, radius, 0, 2 * Math.PI, false); + else + series.points.symbol(octx, x, y, radius, false); + octx.closePath(); + octx.stroke(); + } + + function drawBarHighlight(series, point) { + var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), + fillStyle = highlightColor, + barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + octx.lineWidth = series.bars.lineWidth; + octx.strokeStyle = highlightColor; + + drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, + function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); + } + + function getColorOrGradient(spec, bottom, top, defaultColor) { + if (typeof spec == "string") + return spec; + else { + // assume this is a gradient spec; IE currently only + // supports a simple vertical gradient properly, so that's + // what we support too + var gradient = ctx.createLinearGradient(0, top, 0, bottom); + + for (var i = 0, l = spec.colors.length; i < l; ++i) { + var c = spec.colors[i]; + if (typeof c != "string") { + var co = $.color.parse(defaultColor); + if (c.brightness != null) + co = co.scale('rgb', c.brightness); + if (c.opacity != null) + co.a *= c.opacity; + c = co.toString(); + } + gradient.addColorStop(i / (l - 1), c); + } + + return gradient; + } + } + } + + // Add the plot function to the top level of the jQuery object + + $.plot = function(placeholder, data, options) { + //var t0 = new Date(); + var plot = new Plot($(placeholder), data, options, $.plot.plugins); + //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); + return plot; + }; + + $.plot.version = "0.8.2"; + + $.plot.plugins = []; + + // Also add the plot function as a chainable property + + $.fn.plot = function(data, options) { + return this.each(function() { + $.plot(this, data, options); + }); + }; + + // round to nearby lower multiple of base + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + +})(jQuery); diff --git a/public/assets/js/jquery.flot.resize.js b/public/assets/js/jquery.flot.resize.js new file mode 100644 index 0000000..44e04f8 --- /dev/null +++ b/public/assets/js/jquery.flot.resize.js @@ -0,0 +1,60 @@ +/* Flot plugin for automatically redrawing plots as the placeholder resizes. + +Copyright (c) 2007-2013 IOLA and Ole Laursen. +Licensed under the MIT license. + +It works by listening for changes on the placeholder div (through the jQuery +resize event plugin) - if the size changes, it will redraw the plot. + +There are no options. If you need to disable the plugin for some plots, you +can just fix the size of their placeholders. + +*/ + +/* Inline dependency: + * jQuery resize event - v1.1 - 3/14/2010 + * http://benalman.com/projects/jquery-resize-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ + +(function($,t,n){function p(){for(var n=r.length-1;n>=0;n--){var o=$(r[n]);if(o[0]==t||o.is(":visible")){var h=o.width(),d=o.height(),v=o.data(a);!v||h===v.w&&d===v.h?i[f]=i[l]:(i[f]=i[c],o.trigger(u,[v.w=h,v.h=d]))}else v=o.data(a),v.w=0,v.h=0}s!==null&&(s=t.requestAnimationFrame(p))}var r=[],i=$.resize=$.extend($.resize,{}),s,o="setTimeout",u="resize",a=u+"-special-event",f="delay",l="pendingDelay",c="activeDelay",h="throttleWindow";i[l]=250,i[c]=20,i[f]=i[l],i[h]=!0,$.event.special[u]={setup:function(){if(!i[h]&&this[o])return!1;var t=$(this);r.push(this),t.data(a,{w:t.width(),h:t.height()}),r.length===1&&(s=n,p())},teardown:function(){if(!i[h]&&this[o])return!1;var t=$(this);for(var n=r.length-1;n>=0;n--)if(r[n]==this){r.splice(n,1);break}t.removeData(a),r.length||(cancelAnimationFrame(s),s=null)},add:function(t){function s(t,i,s){var o=$(this),u=o.data(a);u.w=i!==n?i:o.width(),u.h=s!==n?s:o.height(),r.apply(this,arguments)}if(!i[h]&&this[o])return!1;var r;if($.isFunction(t))return r=t,s;r=t.handler,t.handler=s}},t.requestAnimationFrame||(t.requestAnimationFrame=function(){return t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame||t.msRequestAnimationFrame||function(e,n){return t.setTimeout(e,i[f])}}()),t.cancelAnimationFrame||(t.cancelAnimationFrame=function(){return t.webkitCancelRequestAnimationFrame||t.mozCancelRequestAnimationFrame||t.oCancelRequestAnimationFrame||t.msCancelRequestAnimationFrame||clearTimeout}())})(jQuery,this); + +(function ($) { + var options = { }; // no options + + function init(plot) { + function onResize() { + var placeholder = plot.getPlaceholder(); + + // somebody might have hidden us and we can't plot + // when we don't have the dimensions + if (placeholder.width() == 0 || placeholder.height() == 0) + return; + + plot.resize(); + plot.setupGrid(); + plot.draw(); + } + + function bindEvents(plot, eventHolder) { + plot.getPlaceholder().resize(onResize); + } + + function shutdown(plot, eventHolder) { + plot.getPlaceholder().unbind("resize", onResize); + } + + plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'resize', + version: '1.0' + }); +})(jQuery); diff --git a/public/assets/js/picoreflow.js b/public/assets/js/picoreflow.js index 4b8ee1e..9e6bd49 100644 --- a/public/assets/js/picoreflow.js +++ b/public/assets/js/picoreflow.js @@ -127,405 +127,11 @@ function update_profile(id) { } - Highcharts.theme = { - colors: ["#D8D3C5", "#75890c", "#c70000", "#55BF3B", "#DF5353", "#aaeeee", "#ff0066", "#eeaaee", - "#55BF3B", "#DF5353", "#7798BF", "#aaeeee"], - chart: { - /* - backgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0, '#686764'], - [1, '#383734'] - ] - }, - * */ - backgroundColor:'rgba(255, 255, 255, 0)', - borderWidth: 0, - borderRadius: 0, - plotBackgroundColor: null, - plotShadow: true, - plotBorderWidth: 0, - marginBottom: 50 - }, - title: { - style: { - color: '#FFF', - font: '16px Lucida Grande, Lucida Sans Unicode, Verdana, Arial, Helvetica, sans-serif' - } - }, - subtitle: { - style: { - color: '#DDD', - font: '12px Lucida Grande, Lucida Sans Unicode, Verdana, Arial, Helvetica, sans-serif' - } - }, - xAxis: { - gridLineWidth: 1, - gridLineColor: 'rgba(255, 255, 255, .1)', - lineColor: 'rgba(255, 255, 255, .1)', - tickColor: 'rgba(255, 255, 255, .1)', - labels: { - style: { - paddingTop: '4px', - color: '#D8D3C5', - font: '15px Arial, Helvetica, sans-serif' - } - }, - title: { - style: { - color: '#FFF', - font: '12px Arial, Helvetica, sans-serif' - } - } - }, - yAxis: { - alternateGridColor: null, - minorTickInterval: null, - gridLineColor: 'rgba(255, 255, 255, .1)', - minorGridLineColor: 'rgba(255,255,255,0.05)', - lineWidth: 0, - tickWidth: 0, - labels: { - style: { - color: '#D8D3C5', - font: '15px Arial, Helvetica, sans-serif' - } - }, - title: { - style: { - color: '#FFF', - font: '12px Arial, Helvetica, sans-serif' - } - } - }, - legend: { - enabled: false, - itemStyle: { - color: '#CCC' - }, - itemHoverStyle: { - color: '#FFF' - }, - itemHiddenStyle: { - color: '#333' - }, - borderRadius: 0, - borderWidth: 0 - }, - labels: { - style: { - color: '#CCC' - } - }, - tooltip: { - backgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0, 'rgba(96, 96, 96, .8)'], - [1, 'rgba(16, 16, 16, .8)'] - ] - }, - borderWidth: 0, - style: { - color: '#FFF' - } - }, - - - plotOptions: { - series: { - shadow: true - }, - line: { - dataLabels: { - color: '#CCC' - }, - marker: { - lineColor: '#333' - } - }, - spline: { - marker: { - lineColor: '#333' - } - } - }, - - toolbar: { - itemStyle: { - color: '#CCC' - } - }, - - navigation: { - buttonOptions: { - symbolStroke: '#DDDDDD', - hoverSymbolStroke: '#FFFFFF', - theme: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#606060'], - [0.6, '#333333'] - ] - }, - stroke: '#000000' - } - } - }, - - // scroll charts - rangeSelector: { - buttonTheme: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#888'], - [0.6, '#555'] - ] - }, - stroke: '#000000', - style: { - color: '#CCC', - fontWeight: 'bold' - }, - states: { - hover: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#BBB'], - [0.6, '#888'] - ] - }, - stroke: '#000000', - style: { - color: 'white' - } - }, - select: { - fill: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.1, '#000'], - [0.3, '#333'] - ] - }, - stroke: '#000000', - style: { - color: 'yellow' - } - } - } - }, - inputStyle: { - backgroundColor: '#333', - color: 'silver' - }, - labelStyle: { - color: 'silver' - } - }, - - navigator: { - handles: { - backgroundColor: '#666', - borderColor: '#AAA' - }, - outlineColor: '#CCC', - maskFill: 'rgba(16, 16, 16, 0.5)', - series: { - color: '#7798BF', - lineColor: '#A6C7ED' - } - }, - - scrollbar: { - barBackgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#888'], - [0.6, '#555'] - ] - }, - barBorderColor: '#CCC', - buttonArrowColor: '#CCC', - buttonBackgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0.4, '#888'], - [0.6, '#555'] - ] - }, - buttonBorderColor: '#CCC', - rifleColor: '#FFF', - trackBackgroundColor: { - linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, - stops: [ - [0, '#000'], - [1, '#333'] - ] - }, - trackBorderColor: '#666' - }, - - // special colors for some of the demo examples - legendBackgroundColor: 'rgba(48, 48, 48, 0.8)', - legendBackgroundColorSolid: 'rgb(70, 70, 70)', - dataLabelsColor: '#444', - textColor: '#E0E0E0', - maskColor: 'rgba(255,255,255,0.3)' -}; -function getHCOptions() { - var options = - { - title: { text: '' }, - xAxis: { - title: { text: 'Time (s)' }, - type: 'integer', - tickPixelInterval: 60 - }, - yAxis: { - title: { text: 'Temperature (\xB0C)' }, - tickInterval: 25, - min: 0, - max: 300 - }, - tooltip: { - formatter: function() { - return Highcharts.numberFormat(this.y, 0); - } - }, - chart: { - type: 'line', - renderTo: 'graph_container', - animation: true, - //zoomType: 'x', - marginTop: 30, - marginRight: 30, - events: { - load: function() { - var series = this.series[1]; - eta=0; - - - ws_status.onmessage = function(e) - { - x = JSON.parse(e.data); - - if(state!="EDIT") - { - state = x.state; - - if(state=="RUNNING") - { - $("#nav_start").hide(); - $("#nav_stop").show(); - series.addPoint([x.runtime, x.temperature], true, false); - - left = parseInt(x.totaltime-x.runtime); - var minutes = Math.floor(left / 60); - var seconds = left - minutes * 60; - eta = minutes+':'+ (seconds < 10 ? "0" : "") + seconds; - - } - else - { - $("#nav_start").show(); - $("#nav_stop").hide(); - } - - } - - $('#state').html(state); - - updateProgress(parseFloat(x.runtime)/parseFloat(x.totaltime)*100,eta); - - $('#act_temp').html(Highcharts.numberFormat(x.temperature, 0) + ' \xB0C'); - $('#power').css("background-color", (x.power > 0.5 ? "#75890c" : "#1F1E1A") ); - - - if (x.target == 0) - { - $('#target_temp').html('OFF'); - } - else - { - $('#target_temp').html(Highcharts.numberFormat(x.target, 0) + ' \xB0C'); - } - } - } - }, - resetZoomButton: { - position: { - align: 'right', - verticalAlign: 'top' - } - } - }, - - plotOptions: { - series: { - cursor: 'move', - point: { - events: { - /* - drag: function (e) { - $('#drag').html('Dragging ' + this.series.name + ', ' + this.category + ' to ' + Highcharts.numberFormat(e.newY, 0) + ''); - }, - drop: function () { - $('#drop').html('In ' + this.series.name + ', ' + this.category + ' was set to ' + Highcharts.numberFormat(this.y, 0) + ''); - }*/ - } - }, - stickyTracking: false - }, - - }, - - credits: { - enabled: false - }, - - series: [{ - name: 'Ref', - data: [ - [1, 25 ], - [70, 150 ], - [180, 183 ], - [210, 230 ], - [240, 183 ], - [300, 25 ] - ], - draggableX: false, - draggableY: false, - dragMinY: 0, - dragMaxY: 250, - marker: { - enabled: false - } - }, - { - name: 'Act', - data: [ - [0,0] - ], - marker: { - enabled: false - } - }] - - }; - - return (options); - -} @@ -542,13 +148,23 @@ $(document).ready(function() { ws_status.onopen = function() { console.log("Status Socket has been opened"); + $.bootstrapGrowl(" Yay
I'm alive", { + ele: 'body', // which element to append to + type: 'success', // (null, 'info', 'error', 'success') + offset: {from: 'top', amount: 250}, // 'top', or 'bottom' + align: 'center', // ('left', 'right', or 'center') + width: 385, // (integer, or 'auto') + delay: 2500, + allow_dismiss: true, + stackup_spacing: 10 // spacing between consecutively stacked growls. + }); }; ws_status.onclose = function() { $.bootstrapGrowl(" ERROR 1:
Status Websocket not available", { ele: 'body', // which element to append to - type: 'alert', // (null, 'info', 'error', 'success') + type: 'error', // (null, 'info', 'error', 'success') offset: {from: 'top', amount: 250}, // 'top', or 'bottom' align: 'center', // ('left', 'right', or 'center') width: 385, // (integer, or 'auto') @@ -652,24 +268,6 @@ $("#e2").on("change", function(e) { -// Apply the theme -var highchartsOptions = Highcharts.setOptions(Highcharts.theme); - - -$(function() { - Highcharts.setOptions({ - global: { - useUTC: false - } - }); - -graph = new Highcharts.Chart(getHCOptions()); - - -}); - - - } }); diff --git a/public/index.html b/public/index.html index 7c7aff0..a2a3cb5 100644 --- a/public/index.html +++ b/public/index.html @@ -6,18 +6,397 @@ - - + + + + - + + + + @@ -25,8 +404,8 @@
- 25 °C - OFF + 25 °C + OFF Idle     @@ -46,7 +425,14 @@
- + +
+ Profile Name + + +
+ + -
- -
@@ -67,8 +450,9 @@
-
-
+ +
+
@@ -98,5 +482,7 @@
+ +