Merge lp:~lucio.torre/graphite/add-flot into lp:~graphite-dev/graphite/main

Proposed by Lucio Torre on 2010-11-01
Status: Merged
Merge reported by: chrismd
Merged at revision: not available
Proposed branch: lp:~lucio.torre/graphite/add-flot
Merge into: lp:~graphite-dev/graphite/main
Diff against target: 4383 lines
To merge this branch: bzr merge lp:~lucio.torre/graphite/add-flot
Reviewer Review Type Date Requested Status
chrismd 2010-11-01 Approve on 2011-07-10
Review via email: mp+39768@code.launchpad.net

Description of the change

Adds a new interface for rendering graphs with the following features:
- rendering is done client side
- you can mouse-over data points to see their value and to which series they belong
- you can zoom into portions of the data
- you have an overview of the plot
- you can move the series between any of the two axis
- autocomplete for metric input
- faster manipulation of graph series

We are currently working with this live to see how well it copes with lots of data. So far it seems to work ok. I find it very useful to have two axis and an easy way to tell which series is the one that i am pointing with the mouse (with many series colors become very similar)

Please let me know what you think.

To post a comment you must log in.
chrismd (chrismd) wrote :

Sorry for the extremely long delay. I've finally reviewed your branch and I must say it is a pretty cool UI. I fixed a few merge issues (I think you forgot to bzr add a delete.png file you were replacing delete.gif with). And it is now in trunk, thanks and again sorry this took so long.

chrismd (chrismd) :
review: Approve
jeromy (fuji246) wrote :

Hi, is there any update on this? I've made an improved version, and added autoupdate and timezone suppport, and do some enhancement for the ui.

Amos Shapira (amos-shapira) wrote :

I'm not a member of the core team but from following them I understand that they moved the code repository to Github: https://github.com/graphite-project.

Maybe it could help if you submitted the new update there.

Thanks for all your work!

Lucio Torre (lucio.torre) wrote :

This has been merged and can be found on the "flot (experimental)" link in the main page.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'webapp/content/css/jquery.autocomplete.css'
--- webapp/content/css/jquery.autocomplete.css 1970-01-01 00:00:00 +0000
+++ webapp/content/css/jquery.autocomplete.css 2010-11-01 18:48:50 +0000
@@ -0,0 +1,48 @@
1.ac_results {
2 padding: 0px;
3 border: 1px solid black;
4 background-color: white;
5 overflow: hidden;
6 z-index: 99999;
7}
8
9.ac_results ul {
10 width: 100%;
11 list-style-position: outside;
12 list-style: none;
13 padding: 0;
14 margin: 0;
15}
16
17.ac_results li {
18 margin: 0px;
19 padding: 2px 5px;
20 cursor: default;
21 display: block;
22 /*
23 if width will be 100% horizontal scrollbar will apear
24 when scroll mode will be used
25 */
26 /*width: 100%;*/
27 font: menu;
28 font-size: 12px;
29 /*
30 it is very important, if line-height not setted or setted
31 in relative units scroll will be broken in firefox
32 */
33 line-height: 16px;
34 overflow: hidden;
35}
36
37.ac_loading {
38 background: white url('../img/indicator.png') right center no-repeat;
39}
40
41.ac_odd {
42 background-color: #eee;
43}
44
45.ac_over {
46 background-color: #0A246A;
47 color: white;
48}
049
=== added file 'webapp/content/css/table.css'
--- webapp/content/css/table.css 1970-01-01 00:00:00 +0000
+++ webapp/content/css/table.css 2010-11-01 18:48:50 +0000
@@ -0,0 +1,1 @@
1body {
0 color: #4f6b72;2 color: #4f6b72;
1 background: #E6EAE9;3 background: #E6EAE9;
2}4}
35
4a {6a {
5 color: #c75f3e;7 color: #c75f3e;
6}8}
79
#canvas {
8 border-right: 1px solid #C1DAD7;10 border-right: 1px solid #C1DAD7;
9 border-bottom: 1px solid #C1DAD7;11 border-bottom: 1px solid #C1DAD7;
10 background: #fff;12 background: #fff;
11 13
12 color: #4f6b72;14 color: #4f6b72;
13}15}
1416
#title {
15 font: bold 11px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;17 font: bold 11px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
16 color: #4f6b72;18 color: #4f6b72;
17 border-right: 1px solid #C1DAD7;19 border-right: 1px solid #C1DAD7;
18 border-bottom: 1px solid #C1DAD7;20 border-bottom: 1px solid #C1DAD7;
19 border-top: 1px solid #C1DAD7;21 border-top: 1px solid #C1DAD7;
20 letter-spacing: 2px;22 letter-spacing: 2px;
21 text-transform: uppercase;23 text-transform: uppercase;
22 text-align: left;24 text-align: left;
23 padding: 6px 6px 6px 12px;25 padding: 6px 6px 6px 12px;
24 background: #CAE8EA;26 background: #CAE8EA;
25}27}
2628
27.styledtable th {29.styledtable th {
28 font: bold 11px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;30 font: bold 11px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
29 color: #4f6b72;31 color: #4f6b72;
30 border-right: 1px solid #C1DAD7;32 border-right: 1px solid #C1DAD7;
31 border-bottom: 1px solid #C1DAD7;33 border-bottom: 1px solid #C1DAD7;
32 border-top: 1px solid #C1DAD7;34 border-top: 1px solid #C1DAD7;
33 letter-spacing: 2px;35 letter-spacing: 2px;
34 text-transform: uppercase;36 text-transform: uppercase;
35 text-align: left;37 text-align: left;
36 padding: 6px 6px 6px 12px;38 padding: 6px 6px 6px 12px;
37 background: #CAE8EA;39 background: #CAE8EA;
38}40}
3941
4042
4143
42.styledtable td {44.styledtable td {
43 border-right: 1px solid #C1DAD7;45 border-right: 1px solid #C1DAD7;
44 border-bottom: 1px solid #C1DAD7;46 border-bottom: 1px solid #C1DAD7;
45 background: #fff;47 background: #fff;
46 padding: 6px 6px 6px 12px;48 padding: 6px 6px 6px 12px;
47 color: #4f6b72;49 color: #4f6b72;
48}50}
4951
50\ No newline at end of file52\ No newline at end of file
5153
=== removed file 'webapp/content/img/delete.gif'
52Binary files webapp/content/img/delete.gif 2010-02-26 16:15:44 +0000 and webapp/content/img/delete.gif 1970-01-01 00:00:00 +0000 differ54Binary files webapp/content/img/delete.gif 2010-02-26 16:15:44 +0000 and webapp/content/img/delete.gif 1970-01-01 00:00:00 +0000 differ
=== added file 'webapp/content/img/delete.png'
53Binary files webapp/content/img/delete.png 1970-01-01 00:00:00 +0000 and webapp/content/img/delete.png 2010-11-01 18:48:50 +0000 differ55Binary files webapp/content/img/delete.png 1970-01-01 00:00:00 +0000 and webapp/content/img/delete.png 2010-11-01 18:48:50 +0000 differ
=== added file 'webapp/content/img/indicator.png'
54Binary files webapp/content/img/indicator.png 1970-01-01 00:00:00 +0000 and webapp/content/img/indicator.png 2010-11-01 18:48:50 +0000 differ56Binary files webapp/content/img/indicator.png 1970-01-01 00:00:00 +0000 and webapp/content/img/indicator.png 2010-11-01 18:48:50 +0000 differ
=== added file 'webapp/content/js/jquery.autocomplete.js'
--- webapp/content/js/jquery.autocomplete.js 1970-01-01 00:00:00 +0000
+++ webapp/content/js/jquery.autocomplete.js 2010-11-01 18:48:50 +0000
@@ -0,0 +1,762 @@
1/*
2 * Autocomplete - jQuery plugin 1.1pre
3 *
4 * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
5 *
6 * Dual licensed under the MIT and GPL licenses:
7 * http://www.opensource.org/licenses/mit-license.php
8 * http://www.gnu.org/licenses/gpl.html
9 *
10 * Revision: $Id: jquery.autocomplete.js 5785 2008-07-12 10:37:33Z joern.zaefferer $
11 *
12 */
13
14;(function($) {
15
16$.fn.extend({
17 autocomplete: function(urlOrData, options) {
18 var isUrl = typeof urlOrData == "string";
19 options = $.extend({}, $.Autocompleter.defaults, {
20 url: isUrl ? urlOrData : null,
21 data: isUrl ? null : urlOrData,
22 delay: isUrl ? $.Autocompleter.defaults.delay : 10,
23 max: options && !options.scroll ? 10 : 150
24 }, options);
25
26 // if highlight is set to false, replace it with a do-nothing function
27 options.highlight = options.highlight || function(value) { return value; };
28
29 // if the formatMatch option is not specified, then use formatItem for backwards compatibility
30 options.formatMatch = options.formatMatch || options.formatItem;
31
32 return this.each(function() {
33 new $.Autocompleter(this, options);
34 });
35 },
36 result: function(handler) {
37 return this.bind("result", handler);
38 },
39 search: function(handler) {
40 return this.trigger("search", [handler]);
41 },
42 flushCache: function() {
43 return this.trigger("flushCache");
44 },
45 setOptions: function(options){
46 return this.trigger("setOptions", [options]);
47 },
48 unautocomplete: function() {
49 return this.trigger("unautocomplete");
50 }
51});
52
53$.Autocompleter = function(input, options) {
54
55 var KEY = {
56 UP: 38,
57 DOWN: 40,
58 DEL: 46,
59 TAB: 9,
60 RETURN: 13,
61 ESC: 27,
62 COMMA: 188,
63 PAGEUP: 33,
64 PAGEDOWN: 34,
65 BACKSPACE: 8
66 };
67
68 // Create $ object for input element
69 var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
70
71 var timeout;
72 var previousValue = "";
73 var cache = $.Autocompleter.Cache(options);
74 var hasFocus = 0;
75 var lastKeyPressCode;
76 var config = {
77 mouseDownOnSelect: false
78 };
79 var select = $.Autocompleter.Select(options, input, selectCurrent, config);
80
81 var blockSubmit;
82
83 // prevent form submit in opera when selecting with return key
84 $.browser.opera && $(input.form).bind("submit.autocomplete", function() {
85 if (blockSubmit) {
86 blockSubmit = false;
87 return false;
88 }
89 });
90
91 // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
92 $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
93 // track last key pressed
94 lastKeyPressCode = event.keyCode;
95 switch(event.keyCode) {
96
97 case KEY.UP:
98 event.preventDefault();
99 if ( select.visible() ) {
100 select.prev();
101 } else {
102 onChange(0, true);
103 }
104 break;
105
106 case KEY.DOWN:
107 event.preventDefault();
108 if ( select.visible() ) {
109 select.next();
110 } else {
111 onChange(0, true);
112 }
113 break;
114
115 case KEY.PAGEUP:
116 event.preventDefault();
117 if ( select.visible() ) {
118 select.pageUp();
119 } else {
120 onChange(0, true);
121 }
122 break;
123
124 case KEY.PAGEDOWN:
125 event.preventDefault();
126 if ( select.visible() ) {
127 select.pageDown();
128 } else {
129 onChange(0, true);
130 }
131 break;
132
133 // matches also semicolon
134 case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
135 case KEY.TAB:
136 case KEY.RETURN:
137 if( selectCurrent() ) {
138 // stop default to prevent a form submit, Opera needs special handling
139 event.preventDefault();
140 blockSubmit = true;
141 return false;
142 }
143 break;
144
145 case KEY.ESC:
146 select.hide();
147 break;
148
149 default:
150 clearTimeout(timeout);
151 timeout = setTimeout(onChange, options.delay);
152 break;
153 }
154 }).focus(function(){
155 // track whether the field has focus, we shouldn't process any
156 // results if the field no longer has focus
157 hasFocus++;
158 }).blur(function() {
159 hasFocus = 0;
160 if (!config.mouseDownOnSelect) {
161 hideResults();
162 }
163 }).click(function() {
164 // show select when clicking in a focused field
165 if ( hasFocus++ > 1 && !select.visible() ) {
166 onChange(0, true);
167 }
168 }).bind("search", function() {
169 // TODO why not just specifying both arguments?
170 var fn = (arguments.length > 1) ? arguments[1] : null;
171 function findValueCallback(q, data) {
172 var result;
173 if( data && data.length ) {
174 for (var i=0; i < data.length; i++) {
175 if( data[i].result.toLowerCase() == q.toLowerCase() ) {
176 result = data[i];
177 break;
178 }
179 }
180 }
181 if( typeof fn == "function" ) fn(result);
182 else $input.trigger("result", result && [result.data, result.value]);
183 }
184 $.each(trimWords($input.val()), function(i, value) {
185 request(value, findValueCallback, findValueCallback);
186 });
187 }).bind("flushCache", function() {
188 cache.flush();
189 }).bind("setOptions", function() {
190 $.extend(options, arguments[1]);
191 // if we've updated the data, repopulate
192 if ( "data" in arguments[1] )
193 cache.populate();
194 }).bind("unautocomplete", function() {
195 select.unbind();
196 $input.unbind();
197 $(input.form).unbind(".autocomplete");
198 });
199
200
201 function selectCurrent() {
202 var selected = select.selected();
203 if( !selected )
204 return false;
205
206 var v = selected.result;
207 previousValue = v;
208
209 if ( options.multiple ) {
210 var words = trimWords($input.val());
211 if ( words.length > 1 ) {
212 v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
213 }
214 v += options.multipleSeparator;
215 }
216
217 $input.val(v);
218 hideResultsNow();
219 $input.trigger("result", [selected.data, selected.value]);
220 return true;
221 }
222
223 function onChange(crap, skipPrevCheck) {
224 if( lastKeyPressCode == KEY.DEL ) {
225 select.hide();
226 return;
227 }
228
229 var currentValue = $input.val();
230
231 if ( !skipPrevCheck && currentValue == previousValue )
232 return;
233
234 previousValue = currentValue;
235
236 currentValue = lastWord(currentValue);
237 if ( currentValue.length >= options.minChars) {
238 $input.addClass(options.loadingClass);
239 if (!options.matchCase)
240 currentValue = currentValue.toLowerCase();
241 request(currentValue, receiveData, hideResultsNow);
242 } else {
243 stopLoading();
244 select.hide();
245 }
246 };
247
248 function trimWords(value) {
249 if ( !value ) {
250 return [""];
251 }
252 var words = value.split( options.multipleSeparator );
253 var result = [];
254 $.each(words, function(i, value) {
255 if ( $.trim(value) )
256 result[i] = $.trim(value);
257 });
258 return result;
259 }
260
261 function lastWord(value) {
262 if ( !options.multiple )
263 return value;
264 var words = trimWords(value);
265 return words[words.length - 1];
266 }
267
268 // fills in the input box w/the first match (assumed to be the best match)
269 // q: the term entered
270 // sValue: the first matching result
271 function autoFill(q, sValue){
272 // autofill in the complete box w/the first match as long as the user hasn't entered in more data
273 // if the last user key pressed was backspace, don't autofill
274 if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
275 // fill in the value (keep the case the user has typed)
276 $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
277 // select the portion of the value not typed by the user (so the next character will erase)
278 $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
279 }
280 };
281
282 function hideResults() {
283 clearTimeout(timeout);
284 timeout = setTimeout(hideResultsNow, 200);
285 };
286
287 function hideResultsNow() {
288 var wasVisible = select.visible();
289 select.hide();
290 clearTimeout(timeout);
291 stopLoading();
292 if (options.mustMatch) {
293 // call search and run callback
294 $input.search(
295 function (result){
296 // if no value found, clear the input box
297 if( !result ) {
298 if (options.multiple) {
299 var words = trimWords($input.val()).slice(0, -1);
300 $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
301 }
302 else
303 $input.val( "" );
304 }
305 }
306 );
307 }
308 if (wasVisible)
309 // position cursor at end of input field
310 $.Autocompleter.Selection(input, input.value.length, input.value.length);
311 };
312
313 function receiveData(q, data) {
314 if ( data && data.length && hasFocus ) {
315 stopLoading();
316 select.display(data, q);
317 autoFill(q, data[0].value);
318 select.show();
319 } else {
320 hideResultsNow();
321 }
322 };
323
324 function request(term, success, failure) {
325 if (!options.matchCase)
326 term = term.toLowerCase();
327 var data = cache.load(term);
328 // recieve the cached data
329 if (data && data.length) {
330 success(term, data);
331 // if an AJAX url has been supplied, try loading the data now
332 } else if( (typeof options.url == "string") && (options.url.length > 0) ){
333
334 var extraParams = {
335 timestamp: +new Date()
336 };
337 $.each(options.extraParams, function(key, param) {
338 extraParams[key] = typeof param == "function" ? param() : param;
339 });
340
341 $.ajax({
342 // try to leverage ajaxQueue plugin to abort previous requests
343 mode: "abort",
344 // limit abortion to this input
345 port: "autocomplete" + input.name,
346 dataType: options.dataType,
347 url: options.url,
348 data: $.extend({
349 q: lastWord(term),
350 limit: options.max
351 }, extraParams),
352 success: function(data) {
353 var parsed = options.parse && options.parse(data) || parse(data);
354 cache.add(term, parsed);
355 success(term, parsed);
356 }
357 });
358 } else {
359 // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
360 select.emptyList();
361 failure(term);
362 }
363 };
364
365 function parse(data) {
366 var parsed = [];
367 var rows = data.split("\n");
368 for (var i=0; i < rows.length; i++) {
369 var row = $.trim(rows[i]);
370 if (row) {
371 row = row.split("|");
372 parsed[parsed.length] = {
373 data: row,
374 value: row[0],
375 result: options.formatResult && options.formatResult(row, row[0]) || row[0]
376 };
377 }
378 }
379 return parsed;
380 };
381
382 function stopLoading() {
383 $input.removeClass(options.loadingClass);
384 };
385
386};
387
388$.Autocompleter.defaults = {
389 inputClass: "ac_input",
390 resultsClass: "ac_results",
391 loadingClass: "ac_loading",
392 minChars: 1,
393 delay: 400,
394 matchCase: false,
395 matchSubset: true,
396 matchContains: false,
397 cacheLength: 10,
398 max: 100,
399 mustMatch: false,
400 extraParams: {},
401 selectFirst: true,
402 formatItem: function(row) { return row[0]; },
403 formatMatch: null,
404 autoFill: false,
405 width: 0,
406 multiple: false,
407 multipleSeparator: ", ",
408 highlight: function(value, term) {
409 return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
410 },
411 scroll: true,
412 scrollHeight: 180
413};
414
415$.Autocompleter.Cache = function(options) {
416
417 var data = {};
418 var length = 0;
419
420 function matchSubset(s, sub) {
421 if (!options.matchCase)
422 s = s.toLowerCase();
423 var i = s.indexOf(sub);
424 if (options.matchContains == "word"){
425 i = s.toLowerCase().search("\\b" + sub.toLowerCase());
426 }
427 if (i == -1) return false;
428 return i == 0 || options.matchContains;
429 };
430
431 function add(q, value) {
432 if (length > options.cacheLength){
433 flush();
434 }
435 if (!data[q]){
436 length++;
437 }
438 data[q] = value;
439 }
440
441 function populate(){
442 if( !options.data ) return false;
443 // track the matches
444 var stMatchSets = {},
445 nullData = 0;
446
447 // no url was specified, we need to adjust the cache length to make sure it fits the local data store
448 if( !options.url ) options.cacheLength = 1;
449
450 // track all options for minChars = 0
451 stMatchSets[""] = [];
452
453 // loop through the array and create a lookup structure
454 for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
455 var rawValue = options.data[i];
456 // if rawValue is a string, make an array otherwise just reference the array
457 rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
458
459 var value = options.formatMatch(rawValue, i+1, options.data.length);
460 if ( value === false )
461 continue;
462
463 var firstChar = value.charAt(0).toLowerCase();
464 // if no lookup array for this character exists, look it up now
465 if( !stMatchSets[firstChar] )
466 stMatchSets[firstChar] = [];
467
468 // if the match is a string
469 var row = {
470 value: value,
471 data: rawValue,
472 result: options.formatResult && options.formatResult(rawValue) || value
473 };
474
475 // push the current match into the set list
476 stMatchSets[firstChar].push(row);
477
478 // keep track of minChars zero items
479 if ( nullData++ < options.max ) {
480 stMatchSets[""].push(row);
481 }
482 };
483
484 // add the data items to the cache
485 $.each(stMatchSets, function(i, value) {
486 // increase the cache size
487 options.cacheLength++;
488 // add to the cache
489 add(i, value);
490 });
491 }
492
493 // populate any existing data
494 setTimeout(populate, 25);
495
496 function flush(){
497 data = {};
498 length = 0;
499 }
500
501 return {
502 flush: flush,
503 add: add,
504 populate: populate,
505 load: function(q) {
506 if (!options.cacheLength || !length)
507 return null;
508 /*
509 * if dealing w/local data and matchContains than we must make sure
510 * to loop through all the data collections looking for matches
511 */
512 if( !options.url && options.matchContains ){
513 // track all matches
514 var csub = [];
515 // loop through all the data grids for matches
516 for( var k in data ){
517 // don't search through the stMatchSets[""] (minChars: 0) cache
518 // this prevents duplicates
519 if( k.length > 0 ){
520 var c = data[k];
521 $.each(c, function(i, x) {
522 // if we've got a match, add it to the array
523 if (matchSubset(x.value, q)) {
524 csub.push(x);
525 }
526 });
527 }
528 }
529 return csub;
530 } else
531 // if the exact item exists, use it
532 if (data[q]){
533 return data[q];
534 } else
535 if (options.matchSubset) {
536 for (var i = q.length - 1; i >= options.minChars; i--) {
537 var c = data[q.substr(0, i)];
538 if (c) {
539 var csub = [];
540 $.each(c, function(i, x) {
541 if (matchSubset(x.value, q)) {
542 csub[csub.length] = x;
543 }
544 });
545 return csub;
546 }
547 }
548 }
549 return null;
550 }
551 };
552};
553
554$.Autocompleter.Select = function (options, input, select, config) {
555 var CLASSES = {
556 ACTIVE: "ac_over"
557 };
558
559 var listItems,
560 active = -1,
561 data,
562 term = "",
563 needsInit = true,
564 element,
565 list;
566
567 // Create results
568 function init() {
569 if (!needsInit)
570 return;
571 element = $("<div/>")
572 .hide()
573 .addClass(options.resultsClass)
574 .css("position", "absolute")
575 .appendTo(document.body);
576
577 list = $("<ul/>").appendTo(element).mouseover( function(event) {
578 if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
579 active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
580 $(target(event)).addClass(CLASSES.ACTIVE);
581 }
582 }).click(function(event) {
583 $(target(event)).addClass(CLASSES.ACTIVE);
584 select();
585 // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
586 input.focus();
587 return false;
588 }).mousedown(function() {
589 config.mouseDownOnSelect = true;
590 }).mouseup(function() {
591 config.mouseDownOnSelect = false;
592 });
593
594 if( options.width > 0 )
595 element.css("width", options.width);
596
597 needsInit = false;
598 }
599
600 function target(event) {
601 var element = event.target;
602 while(element && element.tagName != "LI")
603 element = element.parentNode;
604 // more fun with IE, sometimes event.target is empty, just ignore it then
605 if(!element)
606 return [];
607 return element;
608 }
609
610 function moveSelect(step) {
611 listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
612 movePosition(step);
613 var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
614 if(options.scroll) {
615 var offset = 0;
616 listItems.slice(0, active).each(function() {
617 offset += this.offsetHeight;
618 });
619 if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
620 list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
621 } else if(offset < list.scrollTop()) {
622 list.scrollTop(offset);
623 }
624 }
625 };
626
627 function movePosition(step) {
628 active += step;
629 if (active < 0) {
630 active = listItems.size() - 1;
631 } else if (active >= listItems.size()) {
632 active = 0;
633 }
634 }
635
636 function limitNumberOfItems(available) {
637 return options.max && options.max < available
638 ? options.max
639 : available;
640 }
641
642 function fillList() {
643 list.empty();
644 var max = limitNumberOfItems(data.length);
645 for (var i=0; i < max; i++) {
646 if (!data[i])
647 continue;
648 var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
649 if ( formatted === false )
650 continue;
651 var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
652 $.data(li, "ac_data", data[i]);
653 }
654 listItems = list.find("li");
655 if ( options.selectFirst ) {
656 listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
657 active = 0;
658 }
659 // apply bgiframe if available
660 if ( $.fn.bgiframe )
661 list.bgiframe();
662 }
663
664 return {
665 display: function(d, q) {
666 init();
667 data = d;
668 term = q;
669 fillList();
670 },
671 next: function() {
672 moveSelect(1);
673 },
674 prev: function() {
675 moveSelect(-1);
676 },
677 pageUp: function() {
678 if (active != 0 && active - 8 < 0) {
679 moveSelect( -active );
680 } else {
681 moveSelect(-8);
682 }
683 },
684 pageDown: function() {
685 if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
686 moveSelect( listItems.size() - 1 - active );
687 } else {
688 moveSelect(8);
689 }
690 },
691 hide: function() {
692 element && element.hide();
693 listItems && listItems.removeClass(CLASSES.ACTIVE);
694 active = -1;
695 },
696 visible : function() {
697 return element && element.is(":visible");
698 },
699 current: function() {
700 return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
701 },
702 show: function() {
703 var offset = $(input).offset();
704 element.css({
705 width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
706 top: offset.top + input.offsetHeight,
707 left: offset.left
708 }).show();
709 if(options.scroll) {
710 list.scrollTop(0);
711 list.css({
712 maxHeight: options.scrollHeight,
713 overflow: 'auto'
714 });
715
716 if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
717 var listHeight = 0;
718 listItems.each(function() {
719 listHeight += this.offsetHeight;
720 });
721 var scrollbarsVisible = listHeight > options.scrollHeight;
722 list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
723 if (!scrollbarsVisible) {
724 // IE doesn't recalculate width when scrollbar disappears
725 listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
726 }
727 }
728
729 }
730 },
731 selected: function() {
732 var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
733 return selected && selected.length && $.data(selected[0], "ac_data");
734 },
735 emptyList: function (){
736 list && list.empty();
737 },
738 unbind: function() {
739 element && element.remove();
740 }
741 };
742};
743
744$.Autocompleter.Selection = function(field, start, end) {
745 if( field.createTextRange ){
746 var selRange = field.createTextRange();
747 selRange.collapse(true);
748 selRange.moveStart("character", start);
749 selRange.moveEnd("character", end);
750 selRange.select();
751 } else if( field.setSelectionRange ){
752 field.setSelectionRange(start, end);
753 } else {
754 if( field.selectionStart ){
755 field.selectionStart = start;
756 field.selectionEnd = end;
757 }
758 }
759 field.focus();
760};
761
762})(jQuery);
0\ No newline at end of file763\ No newline at end of file
1764
=== added file 'webapp/content/js/jquery.flot.crosshair.js'
--- webapp/content/js/jquery.flot.crosshair.js 1970-01-01 00:00:00 +0000
+++ webapp/content/js/jquery.flot.crosshair.js 2010-11-01 18:48:50 +0000
@@ -0,0 +1,156 @@
1/*
2Flot plugin for showing a crosshair, thin lines, when the mouse hovers
3over the plot.
4
5 crosshair: {
6 mode: null or "x" or "y" or "xy"
7 color: color
8 lineWidth: number
9 }
10
11Set the mode to one of "x", "y" or "xy". The "x" mode enables a
12vertical crosshair that lets you trace the values on the x axis, "y"
13enables a horizontal crosshair and "xy" enables them both. "color" is
14the color of the crosshair (default is "rgba(170, 0, 0, 0.80)"),
15"lineWidth" is the width of the drawn lines (default is 1).
16
17The plugin also adds four public methods:
18
19 - setCrosshair(pos)
20
21 Set the position of the crosshair. Note that this is cleared if
22 the user moves the mouse. "pos" should be on the form { x: xpos,
23 y: ypos } (or x2 and y2 if you're using the secondary axes), which
24 is coincidentally the same format as what you get from a "plothover"
25 event. If "pos" is null, the crosshair is cleared.
26
27 - clearCrosshair()
28
29 Clear the crosshair.
30
31 - lockCrosshair(pos)
32
33 Cause the crosshair to lock to the current location, no longer
34 updating if the user moves the mouse. Optionally supply a position
35 (passed on to setCrosshair()) to move it to.
36
37 Example usage:
38 var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
39 $("#graph").bind("plothover", function (evt, position, item) {
40 if (item) {
41 // Lock the crosshair to the data point being hovered
42 myFlot.lockCrosshair({ x: item.datapoint[0], y: item.datapoint[1] });
43 }
44 else {
45 // Return normal crosshair operation
46 myFlot.unlockCrosshair();
47 }
48 });
49
50 - unlockCrosshair()
51
52 Free the crosshair to move again after locking it.
53*/
54
55(function ($) {
56 var options = {
57 crosshair: {
58 mode: null, // one of null, "x", "y" or "xy",
59 color: "rgba(170, 0, 0, 0.80)",
60 lineWidth: 1
61 }
62 };
63
64 function init(plot) {
65 // position of crosshair in pixels
66 var crosshair = { x: -1, y: -1, locked: false };
67
68 plot.setCrosshair = function setCrosshair(pos) {
69 if (!pos)
70 crosshair.x = -1;
71 else {
72 var axes = plot.getAxes();
73
74 crosshair.x = Math.max(0, Math.min(pos.x != null ? axes.xaxis.p2c(pos.x) : axes.x2axis.p2c(pos.x2), plot.width()));
75 crosshair.y = Math.max(0, Math.min(pos.y != null ? axes.yaxis.p2c(pos.y) : axes.y2axis.p2c(pos.y2), plot.height()));
76 }
77
78 plot.triggerRedrawOverlay();
79 };
80
81 plot.clearCrosshair = plot.setCrosshair; // passes null for pos
82
83 plot.lockCrosshair = function lockCrosshair(pos) {
84 if (pos)
85 plot.setCrosshair(pos);
86 crosshair.locked = true;
87 }
88
89 plot.unlockCrosshair = function unlockCrosshair() {
90 crosshair.locked = false;
91 }
92
93 plot.hooks.bindEvents.push(function (plot, eventHolder) {
94 if (!plot.getOptions().crosshair.mode)
95 return;
96
97 eventHolder.mouseout(function () {
98 if (crosshair.x != -1) {
99 crosshair.x = -1;
100 plot.triggerRedrawOverlay();
101 }
102 });
103
104 eventHolder.mousemove(function (e) {
105 if (plot.getSelection && plot.getSelection()) {
106 crosshair.x = -1; // hide the crosshair while selecting
107 return;
108 }
109
110 if (crosshair.locked)
111 return;
112
113 var offset = plot.offset();
114 crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
115 crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
116 plot.triggerRedrawOverlay();
117 });
118 });
119
120 plot.hooks.drawOverlay.push(function (plot, ctx) {
121 var c = plot.getOptions().crosshair;
122 if (!c.mode)
123 return;
124
125 var plotOffset = plot.getPlotOffset();
126
127 ctx.save();
128 ctx.translate(plotOffset.left, plotOffset.top);
129
130 if (crosshair.x != -1) {
131 ctx.strokeStyle = c.color;
132 ctx.lineWidth = c.lineWidth;
133 ctx.lineJoin = "round";
134
135 ctx.beginPath();
136 if (c.mode.indexOf("x") != -1) {
137 ctx.moveTo(crosshair.x, 0);
138 ctx.lineTo(crosshair.x, plot.height());
139 }
140 if (c.mode.indexOf("y") != -1) {
141 ctx.moveTo(0, crosshair.y);
142 ctx.lineTo(plot.width(), crosshair.y);
143 }
144 ctx.stroke();
145 }
146 ctx.restore();
147 });
148 }
149
150 $.plot.plugins.push({
151 init: init,
152 options: options,
153 name: 'crosshair',
154 version: '1.0'
155 });
156})(jQuery);
0157
=== added file 'webapp/content/js/jquery.flot.js'
--- webapp/content/js/jquery.flot.js 1970-01-01 00:00:00 +0000
+++ webapp/content/js/jquery.flot.js 2010-11-01 18:48:50 +0000
@@ -0,0 +1,2119 @@
1/* Javascript plotting library for jQuery, v. 0.6.
2 *
3 * Released under the MIT license by IOLA, December 2007.
4 *
5 */
6
7// first an inline dependency, jquery.colorhelpers.js, we inline it here
8// for convenience
9
10/* Plugin for jQuery for working with colors.
11 *
12 * Version 1.0.
13 *
14 * Inspiration from jQuery color animation plugin by John Resig.
15 *
16 * Released under the MIT license by Ole Laursen, October 2009.
17 *
18 * Examples:
19 *
20 * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
21 * var c = $.color.extract($("#mydiv"), 'background-color');
22 * console.log(c.r, c.g, c.b, c.a);
23 * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
24 *
25 * Note that .scale() and .add() work in-place instead of returning
26 * new objects.
27 */
28(function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]+=H}return F.normalize()};F.scale=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]*=H}return F.normalize()};F.toString=function(){if(F.a>=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return J<I?I:(J>H?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/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(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/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(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[2])}};var A={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]}})();
29
30// the actual Flot code
31(function($) {
32 function Plot(placeholder, data_, options_, plugins) {
33 // data is on the form:
34 // [ series1, series2 ... ]
35 // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
36 // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
37
38 var series = [],
39 options = {
40 // the color theme used for graphs
41 colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
42 legend: {
43 show: true,
44 noColumns: 1, // number of colums in legend table
45 labelFormatter: null, // fn: string -> string
46 labelBoxBorderColor: "#ccc", // border color for the little label boxes
47 container: null, // container (as jQuery object) to put legend in, null means default on top of graph
48 position: "ne", // position of default legend container within plot
49 margin: 5, // distance from grid edge to default legend container within plot
50 backgroundColor: null, // null means auto-detect
51 backgroundOpacity: 0.85 // set to 0 to avoid background
52 },
53 xaxis: {
54 mode: null, // null or "time"
55 transform: null, // null or f: number -> number to transform axis
56 inverseTransform: null, // if transform is set, this should be the inverse function
57 min: null, // min. value to show, null means set automatically
58 max: null, // max. value to show, null means set automatically
59 autoscaleMargin: null, // margin in % to add if auto-setting min/max
60 ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
61 tickFormatter: null, // fn: number -> string
62 labelWidth: null, // size of tick labels in pixels
63 labelHeight: null,
64
65 // mode specific options
66 tickDecimals: null, // no. of decimals, null means auto
67 tickSize: null, // number or [number, "unit"]
68 minTickSize: null, // number or [number, "unit"]
69 monthNames: null, // list of names of months
70 timeformat: null, // format string to use
71 twelveHourClock: false // 12 or 24 time in time mode
72 },
73 yaxis: {
74 autoscaleMargin: 0.02
75 },
76 x2axis: {
77 autoscaleMargin: null
78 },
79 y2axis: {
80 autoscaleMargin: 0.02
81 },
82 series: {
83 points: {
84 show: false,
85 radius: 3,
86 lineWidth: 2, // in pixels
87 fill: true,
88 fillColor: "#ffffff"
89 },
90 lines: {
91 // we don't put in show: false so we can see
92 // whether lines were actively disabled
93 lineWidth: 2, // in pixels
94 fill: false,
95 fillColor: null,
96 steps: false
97 },
98 bars: {
99 show: false,
100 lineWidth: 2, // in pixels
101 barWidth: 1, // in units of the x axis
102 fill: true,
103 fillColor: null,
104 align: "left", // or "center"
105 horizontal: false // when horizontal, left is now top
106 },
107 shadowSize: 3
108 },
109 grid: {
110 show: true,
111 aboveData: false,
112 color: "#545454", // primary color used for outline and labels
113 backgroundColor: null, // null for transparent, else color
114 tickColor: "rgba(0,0,0,0.15)", // color used for the ticks
115 labelMargin: 5, // in pixels
116 borderWidth: 2, // in pixels
117 borderColor: null, // set if different from the grid color
118 markings: null, // array of ranges or fn: axes -> array of ranges
119 markingsColor: "#f4f4f4",
120 markingsLineWidth: 2,
121 // interactive stuff
122 clickable: false,
123 hoverable: false,
124 autoHighlight: true, // highlight in case mouse is near
125 mouseActiveRadius: 10 // how far the mouse can be away to activate an item
126 },
127 hooks: {}
128 },
129 canvas = null, // the canvas for the plot itself
130 overlay = null, // canvas for interactive stuff on top of plot
131 eventHolder = null, // jQuery object that events should be bound to
132 ctx = null, octx = null,
133 axes = { xaxis: {}, yaxis: {}, x2axis: {}, y2axis: {} },
134 plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
135 canvasWidth = 0, canvasHeight = 0,
136 plotWidth = 0, plotHeight = 0,
137 hooks = {
138 processOptions: [],
139 processRawData: [],
140 processDatapoints: [],
141 draw: [],
142 bindEvents: [],
143 drawOverlay: []
144 },
145 plot = this;
146
147 // public functions
148 plot.setData = setData;
149 plot.setupGrid = setupGrid;
150 plot.draw = draw;
151 plot.getPlaceholder = function() { return placeholder; };
152 plot.getCanvas = function() { return canvas; };
153 plot.getPlotOffset = function() { return plotOffset; };
154 plot.width = function () { return plotWidth; };
155 plot.height = function () { return plotHeight; };
156 plot.offset = function () {
157 var o = eventHolder.offset();
158 o.left += plotOffset.left;
159 o.top += plotOffset.top;
160 return o;
161 };
162 plot.getData = function() { return series; };
163 plot.getAxes = function() { return axes; };
164 plot.getOptions = function() { return options; };
165 plot.highlight = highlight;
166 plot.unhighlight = unhighlight;
167 plot.triggerRedrawOverlay = triggerRedrawOverlay;
168 plot.pointOffset = function(point) {
169 return { left: parseInt(axisSpecToRealAxis(point, "xaxis").p2c(+point.x) + plotOffset.left),
170 top: parseInt(axisSpecToRealAxis(point, "yaxis").p2c(+point.y) + plotOffset.top) };
171 };
172
173
174 // public attributes
175 plot.hooks = hooks;
176
177 // initialize
178 initPlugins(plot);
179 parseOptions(options_);
180 constructCanvas();
181 setData(data_);
182 setupGrid();
183 draw();
184 bindEvents();
185
186
187 function executeHooks(hook, args) {
188 args = [plot].concat(args);
189 for (var i = 0; i < hook.length; ++i)
190 hook[i].apply(this, args);
191 }
192
193 function initPlugins() {
194 for (var i = 0; i < plugins.length; ++i) {
195 var p = plugins[i];
196 p.init(plot);
197 if (p.options)
198 $.extend(true, options, p.options);
199 }
200 }
201
202 function parseOptions(opts) {
203 $.extend(true, options, opts);
204 if (options.grid.borderColor == null)
205 options.grid.borderColor = options.grid.color;
206 // backwards compatibility, to be removed in future
207 if (options.xaxis.noTicks && options.xaxis.ticks == null)
208 options.xaxis.ticks = options.xaxis.noTicks;
209 if (options.yaxis.noTicks && options.yaxis.ticks == null)
210 options.yaxis.ticks = options.yaxis.noTicks;
211 if (options.grid.coloredAreas)
212 options.grid.markings = options.grid.coloredAreas;
213 if (options.grid.coloredAreasColor)
214 options.grid.markingsColor = options.grid.coloredAreasColor;
215 if (options.lines)
216 $.extend(true, options.series.lines, options.lines);
217 if (options.points)
218 $.extend(true, options.series.points, options.points);
219 if (options.bars)
220 $.extend(true, options.series.bars, options.bars);
221 if (options.shadowSize)
222 options.series.shadowSize = options.shadowSize;
223
224 for (var n in hooks)
225 if (options.hooks[n] && options.hooks[n].length)
226 hooks[n] = hooks[n].concat(options.hooks[n]);
227
228 executeHooks(hooks.processOptions, [options]);
229 }
230
231 function setData(d) {
232 series = parseData(d);
233 fillInSeriesOptions();
234 processData();
235 }
236
237 function parseData(d) {
238 var res = [];
239 for (var i = 0; i < d.length; ++i) {
240 var s = $.extend(true, {}, options.series);
241
242 if (d[i].data) {
243 s.data = d[i].data; // move the data instead of deep-copy
244 delete d[i].data;
245
246 $.extend(true, s, d[i]);
247
248 d[i].data = s.data;
249 }
250 else
251 s.data = d[i];
252 res.push(s);
253 }
254
255 return res;
256 }
257
258 function axisSpecToRealAxis(obj, attr) {
259 var a = obj[attr];
260 if (!a || a == 1)
261 return axes[attr];
262 if (typeof a == "number")
263 return axes[attr.charAt(0) + a + attr.slice(1)];
264 return a; // assume it's OK
265 }
266
267 function fillInSeriesOptions() {
268 var i;
269
270 // collect what we already got of colors
271 var neededColors = series.length,
272 usedColors = [],
273 assignedColors = [];
274 for (i = 0; i < series.length; ++i) {
275 var sc = series[i].color;
276 if (sc != null) {
277 --neededColors;
278 if (typeof sc == "number")
279 assignedColors.push(sc);
280 else
281 usedColors.push($.color.parse(series[i].color));
282 }
283 }
284
285 // we might need to generate more colors if higher indices
286 // are assigned
287 for (i = 0; i < assignedColors.length; ++i) {
288 neededColors = Math.max(neededColors, assignedColors[i] + 1);
289 }
290
291 // produce colors as needed
292 var colors = [], variation = 0;
293 i = 0;
294 while (colors.length < neededColors) {
295 var c;
296 if (options.colors.length == i) // check degenerate case
297 c = $.color.make(100, 100, 100);
298 else
299 c = $.color.parse(options.colors[i]);
300
301 // vary color if needed
302 var sign = variation % 2 == 1 ? -1 : 1;
303 c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)
304
305 // FIXME: if we're getting to close to something else,
306 // we should probably skip this one
307 colors.push(c);
308
309 ++i;
310 if (i >= options.colors.length) {
311 i = 0;
312 ++variation;
313 }
314 }
315
316 // fill in the options
317 var colori = 0, s;
318 for (i = 0; i < series.length; ++i) {
319 s = series[i];
320
321 // assign colors
322 if (s.color == null) {
323 s.color = colors[colori].toString();
324 ++colori;
325 }
326 else if (typeof s.color == "number")
327 s.color = colors[s.color].toString();
328
329 // turn on lines automatically in case nothing is set
330 if (s.lines.show == null) {
331 var v, show = true;
332 for (v in s)
333 if (s[v].show) {
334 show = false;
335 break;
336 }
337 if (show)
338 s.lines.show = true;
339 }
340
341 // setup axes
342 s.xaxis = axisSpecToRealAxis(s, "xaxis");
343 s.yaxis = axisSpecToRealAxis(s, "yaxis");
344 }
345 }
346
347 function processData() {
348 var topSentry = Number.POSITIVE_INFINITY,
349 bottomSentry = Number.NEGATIVE_INFINITY,
350 i, j, k, m, length,
351 s, points, ps, x, y, axis, val, f, p;
352
353 for (axis in axes) {
354 axes[axis].datamin = topSentry;
355 axes[axis].datamax = bottomSentry;
356 axes[axis].used = false;
357 }
358
359 function updateAxis(axis, min, max) {
360 if (min < axis.datamin)
361 axis.datamin = min;
362 if (max > axis.datamax)
363 axis.datamax = max;
364 }
365
366 for (i = 0; i < series.length; ++i) {
367 s = series[i];
368 s.datapoints = { points: [] };
369
370 executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
371 }
372
373 // first pass: clean and copy data
374 for (i = 0; i < series.length; ++i) {
375 s = series[i];
376
377 var data = s.data, format = s.datapoints.format;
378
379 if (!format) {
380 format = [];
381 // find out how to copy
382 format.push({ x: true, number: true, required: true });
383 format.push({ y: true, number: true, required: true });
384
385 if (s.bars.show)
386 format.push({ y: true, number: true, required: false, defaultValue: 0 });
387
388 s.datapoints.format = format;
389 }
390
391 if (s.datapoints.pointsize != null)
392 continue; // already filled in
393
394 if (s.datapoints.pointsize == null)
395 s.datapoints.pointsize = format.length;
396
397 ps = s.datapoints.pointsize;
398 points = s.datapoints.points;
399
400 insertSteps = s.lines.show && s.lines.steps;
401 s.xaxis.used = s.yaxis.used = true;
402
403 for (j = k = 0; j < data.length; ++j, k += ps) {
404 p = data[j];
405
406 var nullify = p == null;
407 if (!nullify) {
408 for (m = 0; m < ps; ++m) {
409 val = p[m];
410 f = format[m];
411
412 if (f) {
413 if (f.number && val != null) {
414 val = +val; // convert to number
415 if (isNaN(val))
416 val = null;
417 }
418
419 if (val == null) {
420 if (f.required)
421 nullify = true;
422
423 if (f.defaultValue != null)
424 val = f.defaultValue;
425 }
426 }
427
428 points[k + m] = val;
429 }
430 }
431
432 if (nullify) {
433 for (m = 0; m < ps; ++m) {
434 val = points[k + m];
435 if (val != null) {
436 f = format[m];
437 // extract min/max info
438 if (f.x)
439 updateAxis(s.xaxis, val, val);
440 if (f.y)
441 updateAxis(s.yaxis, val, val);
442 }
443 points[k + m] = null;
444 }
445 }
446 else {
447 // a little bit of line specific stuff that
448 // perhaps shouldn't be here, but lacking
449 // better means...
450 if (insertSteps && k > 0
451 && points[k - ps] != null
452 && points[k - ps] != points[k]
453 && points[k - ps + 1] != points[k + 1]) {
454 // copy the point to make room for a middle point
455 for (m = 0; m < ps; ++m)
456 points[k + ps + m] = points[k + m];
457
458 // middle point has same y
459 points[k + 1] = points[k - ps + 1];
460
461 // we've added a point, better reflect that
462 k += ps;
463 }
464 }
465 }
466 }
467
468 // give the hooks a chance to run
469 for (i = 0; i < series.length; ++i) {
470 s = series[i];
471
472 executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
473 }
474
475 // second pass: find datamax/datamin for auto-scaling
476 for (i = 0; i < series.length; ++i) {
477 s = series[i];
478 points = s.datapoints.points,
479 ps = s.datapoints.pointsize;
480
481 var xmin = topSentry, ymin = topSentry,
482 xmax = bottomSentry, ymax = bottomSentry;
483
484 for (j = 0; j < points.length; j += ps) {
485 if (points[j] == null)
486 continue;
487
488 for (m = 0; m < ps; ++m) {
489 val = points[j + m];
490 f = format[m];
491 if (!f)
492 continue;
493
494 if (f.x) {
495 if (val < xmin)
496 xmin = val;
497 if (val > xmax)
498 xmax = val;
499 }
500 if (f.y) {
501 if (val < ymin)
502 ymin = val;
503 if (val > ymax)
504 ymax = val;
505 }
506 }
507 }
508
509 if (s.bars.show) {
510 // make sure we got room for the bar on the dancing floor
511 var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
512 if (s.bars.horizontal) {
513 ymin += delta;
514 ymax += delta + s.bars.barWidth;
515 }
516 else {
517 xmin += delta;
518 xmax += delta + s.bars.barWidth;
519 }
520 }
521
522 updateAxis(s.xaxis, xmin, xmax);
523 updateAxis(s.yaxis, ymin, ymax);
524 }
525
526 for (axis in axes) {
527 if (axes[axis].datamin == topSentry)
528 axes[axis].datamin = null;
529 if (axes[axis].datamax == bottomSentry)
530 axes[axis].datamax = null;
531 }
532 }
533
534 function constructCanvas() {
535 function makeCanvas(width, height) {
536 var c = document.createElement('canvas');
537 c.width = width;
538 c.height = height;
539 if ($.browser.msie) // excanvas hack
540 c = window.G_vmlCanvasManager.initElement(c);
541 return c;
542 }
543
544 canvasWidth = placeholder.width();
545 canvasHeight = placeholder.height();
546 placeholder.html(""); // clear placeholder
547 if (placeholder.css("position") == 'static')
548 placeholder.css("position", "relative"); // for positioning labels and overlay
549
550 if (canvasWidth <= 0 || canvasHeight <= 0)
551 throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
552
553 if ($.browser.msie) // excanvas hack
554 window.G_vmlCanvasManager.init_(document); // make sure everything is setup
555
556 // the canvas
557 canvas = $(makeCanvas(canvasWidth, canvasHeight)).appendTo(placeholder).get(0);
558 ctx = canvas.getContext("2d");
559
560 // overlay canvas for interactive features
561 overlay = $(makeCanvas(canvasWidth, canvasHeight)).css({ position: 'absolute', left: 0, top: 0 }).appendTo(placeholder).get(0);
562 octx = overlay.getContext("2d");
563 octx.stroke();
564 }
565
566 function bindEvents() {
567 // we include the canvas in the event holder too, because IE 7
568 // sometimes has trouble with the stacking order
569 eventHolder = $([overlay, canvas]);
570
571 // bind events
572 if (options.grid.hoverable)
573 eventHolder.mousemove(onMouseMove);
574
575 if (options.grid.clickable)
576 eventHolder.click(onClick);
577
578 executeHooks(hooks.bindEvents, [eventHolder]);
579 }
580
581 function setupGrid() {
582 function setTransformationHelpers(axis, o) {
583 function identity(x) { return x; }
584
585 var s, m, t = o.transform || identity,
586 it = o.inverseTransform;
587
588 // add transformation helpers
589 if (axis == axes.xaxis || axis == axes.x2axis) {
590 // precompute how much the axis is scaling a point
591 // in canvas space
592 s = axis.scale = plotWidth / (t(axis.max) - t(axis.min));
593 m = t(axis.min);
594
595 // data point to canvas coordinate
596 if (t == identity) // slight optimization
597 axis.p2c = function (p) { return (p - m) * s; };
598 else
599 axis.p2c = function (p) { return (t(p) - m) * s; };
600 // canvas coordinate to data point
601 if (!it)
602 axis.c2p = function (c) { return m + c / s; };
603 else
604 axis.c2p = function (c) { return it(m + c / s); };
605 }
606 else {
607 s = axis.scale = plotHeight / (t(axis.max) - t(axis.min));
608 m = t(axis.max);
609
610 if (t == identity)
611 axis.p2c = function (p) { return (m - p) * s; };
612 else
613 axis.p2c = function (p) { return (m - t(p)) * s; };
614 if (!it)
615 axis.c2p = function (c) { return m - c / s; };
616 else
617 axis.c2p = function (c) { return it(m - c / s); };
618 }
619 }
620
621 function measureLabels(axis, axisOptions) {
622 var i, labels = [], l;
623
624 axis.labelWidth = axisOptions.labelWidth;
625 axis.labelHeight = axisOptions.labelHeight;
626
627 if (axis == axes.xaxis || axis == axes.x2axis) {
628 // to avoid measuring the widths of the labels, we
629 // construct fixed-size boxes and put the labels inside
630 // them, we don't need the exact figures and the
631 // fixed-size box content is easy to center
632 if (axis.labelWidth == null)
633 axis.labelWidth = canvasWidth / (axis.ticks.length > 0 ? axis.ticks.length : 1);
634
635 // measure x label heights
636 if (axis.labelHeight == null) {
637 labels = [];
638 for (i = 0; i < axis.ticks.length; ++i) {
639 l = axis.ticks[i].label;
640 if (l)
641 labels.push('<div class="tickLabel" style="float:left;width:' + axis.labelWidth + 'px">' + l + '</div>');
642 }
643
644 if (labels.length > 0) {
645 var dummyDiv = $('<div style="position:absolute;top:-10000px;width:10000px;font-size:smaller">'
646 + labels.join("") + '<div style="clear:left"></div></div>').appendTo(placeholder);
647 axis.labelHeight = dummyDiv.height();
648 dummyDiv.remove();
649 }
650 }
651 }
652 else if (axis.labelWidth == null || axis.labelHeight == null) {
653 // calculate y label dimensions
654 for (i = 0; i < axis.ticks.length; ++i) {
655 l = axis.ticks[i].label;
656 if (l)
657 labels.push('<div class="tickLabel">' + l + '</div>');
658 }
659
660 if (labels.length > 0) {
661 var dummyDiv = $('<div style="position:absolute;top:-10000px;font-size:smaller">'
662 + labels.join("") + '</div>').appendTo(placeholder);
663 if (axis.labelWidth == null)
664 axis.labelWidth = dummyDiv.width();
665 if (axis.labelHeight == null)
666 axis.labelHeight = dummyDiv.find("div").height();
667 dummyDiv.remove();
668 }
669
670 }
671
672 if (axis.labelWidth == null)
673 axis.labelWidth = 0;
674 if (axis.labelHeight == null)
675 axis.labelHeight = 0;
676 }
677
678 function setGridSpacing() {
679 // get the most space needed around the grid for things
680 // that may stick out
681 var maxOutset = options.grid.borderWidth;
682 for (i = 0; i < series.length; ++i)
683 maxOutset = Math.max(maxOutset, 2 * (series[i].points.radius + series[i].points.lineWidth/2));
684
685 plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = maxOutset;
686
687 var margin = options.grid.labelMargin + options.grid.borderWidth;
688
689 if (axes.xaxis.labelHeight > 0)
690 plotOffset.bottom = Math.max(maxOutset, axes.xaxis.labelHeight + margin);
691 if (axes.yaxis.labelWidth > 0)
692 plotOffset.left = Math.max(maxOutset, axes.yaxis.labelWidth + margin);
693 if (axes.x2axis.labelHeight > 0)
694 plotOffset.top = Math.max(maxOutset, axes.x2axis.labelHeight + margin);
695 if (axes.y2axis.labelWidth > 0)
696 plotOffset.right = Math.max(maxOutset, axes.y2axis.labelWidth + margin);
697
698 plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
699 plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
700 }
701
702 var axis;
703 for (axis in axes)
704 setRange(axes[axis], options[axis]);
705
706 if (options.grid.show) {
707 for (axis in axes) {
708 prepareTickGeneration(axes[axis], options[axis]);
709 setTicks(axes[axis], options[axis]);
710 measureLabels(axes[axis], options[axis]);
711 }
712
713 setGridSpacing();
714 }
715 else {
716 plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
717 plotWidth = canvasWidth;
718 plotHeight = canvasHeight;
719 }
720
721 for (axis in axes)
722 setTransformationHelpers(axes[axis], options[axis]);
723
724 if (options.grid.show)
725 insertLabels();
726
727 insertLegend();
728 }
729
730 function setRange(axis, axisOptions) {
731 var min = +(axisOptions.min != null ? axisOptions.min : axis.datamin),
732 max = +(axisOptions.max != null ? axisOptions.max : axis.datamax),
733 delta = max - min;
734
735 if (delta == 0.0) {
736 // degenerate case
737 var widen = max == 0 ? 1 : 0.01;
738
739 if (axisOptions.min == null)
740 min -= widen;
741 // alway widen max if we couldn't widen min to ensure we
742 // don't fall into min == max which doesn't work
743 if (axisOptions.max == null || axisOptions.min != null)
744 max += widen;
745 }
746 else {
747 // consider autoscaling
748 var margin = axisOptions.autoscaleMargin;
749 if (margin != null) {
750 if (axisOptions.min == null) {
751 min -= delta * margin;
752 // make sure we don't go below zero if all values
753 // are positive
754 if (min < 0 && axis.datamin != null && axis.datamin >= 0)
755 min = 0;
756 }
757 if (axisOptions.max == null) {
758 max += delta * margin;
759 if (max > 0 && axis.datamax != null && axis.datamax <= 0)
760 max = 0;
761 }
762 }
763 }
764 axis.min = min;
765 axis.max = max;
766 }
767
768 function prepareTickGeneration(axis, axisOptions) {
769 // estimate number of ticks
770 var noTicks;
771 if (typeof axisOptions.ticks == "number" && axisOptions.ticks > 0)
772 noTicks = axisOptions.ticks;
773 else if (axis == axes.xaxis || axis == axes.x2axis)
774 // heuristic based on the model a*sqrt(x) fitted to
775 // some reasonable data points
776 noTicks = 0.3 * Math.sqrt(canvasWidth);
777 else
778 noTicks = 0.3 * Math.sqrt(canvasHeight);
779
780 var delta = (axis.max - axis.min) / noTicks,
781 size, generator, unit, formatter, i, magn, norm;
782
783 if (axisOptions.mode == "time") {
784 // pretty handling of time
785
786 // map of app. size of time units in milliseconds
787 var timeUnitSize = {
788 "second": 1000,
789 "minute": 60 * 1000,
790 "hour": 60 * 60 * 1000,
791 "day": 24 * 60 * 60 * 1000,
792 "month": 30 * 24 * 60 * 60 * 1000,
793 "year": 365.2425 * 24 * 60 * 60 * 1000
794 };
795
796
797 // the allowed tick sizes, after 1 year we use
798 // an integer algorithm
799 var spec = [
800 [1, "second"], [2, "second"], [5, "second"], [10, "second"],
801 [30, "second"],
802 [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
803 [30, "minute"],
804 [1, "hour"], [2, "hour"], [4, "hour"],
805 [8, "hour"], [12, "hour"],
806 [1, "day"], [2, "day"], [3, "day"],
807 [0.25, "month"], [0.5, "month"], [1, "month"],
808 [2, "month"], [3, "month"], [6, "month"],
809 [1, "year"]
810 ];
811
812 var minSize = 0;
813 if (axisOptions.minTickSize != null) {
814 if (typeof axisOptions.tickSize == "number")
815 minSize = axisOptions.tickSize;
816 else
817 minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]];
818 }
819
820 for (i = 0; i < spec.length - 1; ++i)
821 if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
822 + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
823 && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
824 break;
825 size = spec[i][0];
826 unit = spec[i][1];
827
828 // special-case the possibility of several years
829 if (unit == "year") {
830 magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
831 norm = (delta / timeUnitSize.year) / magn;
832 if (norm < 1.5)
833 size = 1;
834 else if (norm < 3)
835 size = 2;
836 else if (norm < 7.5)
837 size = 5;
838 else
839 size = 10;
840
841 size *= magn;
842 }
843
844 if (axisOptions.tickSize) {
845 size = axisOptions.tickSize[0];
846 unit = axisOptions.tickSize[1];
847 }
848
849 generator = function(axis) {
850 var ticks = [],
851 tickSize = axis.tickSize[0], unit = axis.tickSize[1],
852 d = new Date(axis.min);
853
854 var step = tickSize * timeUnitSize[unit];
855
856 if (unit == "second")
857 d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
858 if (unit == "minute")
859 d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
860 if (unit == "hour")
861 d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
862 if (unit == "month")
863 d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
864 if (unit == "year")
865 d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
866
867 // reset smaller components
868 d.setUTCMilliseconds(0);
869 if (step >= timeUnitSize.minute)
870 d.setUTCSeconds(0);
871 if (step >= timeUnitSize.hour)
872 d.setUTCMinutes(0);
873 if (step >= timeUnitSize.day)
874 d.setUTCHours(0);
875 if (step >= timeUnitSize.day * 4)
876 d.setUTCDate(1);
877 if (step >= timeUnitSize.year)
878 d.setUTCMonth(0);
879
880
881 var carry = 0, v = Number.NaN, prev;
882 do {
883 prev = v;
884 v = d.getTime();
885 ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
886 if (unit == "month") {
887 if (tickSize < 1) {
888 // a bit complicated - we'll divide the month
889 // up but we need to take care of fractions
890 // so we don't end up in the middle of a day
891 d.setUTCDate(1);
892 var start = d.getTime();
893 d.setUTCMonth(d.getUTCMonth() + 1);
894 var end = d.getTime();
895 d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
896 carry = d.getUTCHours();
897 d.setUTCHours(0);
898 }
899 else
900 d.setUTCMonth(d.getUTCMonth() + tickSize);
901 }
902 else if (unit == "year") {
903 d.setUTCFullYear(d.getUTCFullYear() + tickSize);
904 }
905 else
906 d.setTime(v + step);
907 } while (v < axis.max && v != prev);
908
909 return ticks;
910 };
911
912 formatter = function (v, axis) {
913 var d = new Date(v);
914
915 // first check global format
916 if (axisOptions.timeformat != null)
917 return $.plot.formatDate(d, axisOptions.timeformat, axisOptions.monthNames);
918
919 var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
920 var span = axis.max - axis.min;
921 var suffix = (axisOptions.twelveHourClock) ? " %p" : "";
922
923 if (t < timeUnitSize.minute)
924 fmt = "%h:%M:%S" + suffix;
925 else if (t < timeUnitSize.day) {
926 if (span < 2 * timeUnitSize.day)
927 fmt = "%h:%M" + suffix;
928 else
929 fmt = "%b %d %h:%M" + suffix;
930 }
931 else if (t < timeUnitSize.month)
932 fmt = "%b %d";
933 else if (t < timeUnitSize.year) {
934 if (span < timeUnitSize.year)
935 fmt = "%b";
936 else
937 fmt = "%b %y";
938 }
939 else
940 fmt = "%y";
941
942 return $.plot.formatDate(d, fmt, axisOptions.monthNames);
943 };
944 }
945 else {
946 // pretty rounding of base-10 numbers
947 var maxDec = axisOptions.tickDecimals;
948 var dec = -Math.floor(Math.log(delta) / Math.LN10);
949 if (maxDec != null && dec > maxDec)
950 dec = maxDec;
951
952 magn = Math.pow(10, -dec);
953 norm = delta / magn; // norm is between 1.0 and 10.0
954
955 if (norm < 1.5)
956 size = 1;
957 else if (norm < 3) {
958 size = 2;
959 // special case for 2.5, requires an extra decimal
960 if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
961 size = 2.5;
962 ++dec;
963 }
964 }
965 else if (norm < 7.5)
966 size = 5;
967 else
968 size = 10;
969
970 size *= magn;
971
972 if (axisOptions.minTickSize != null && size < axisOptions.minTickSize)
973 size = axisOptions.minTickSize;
974
975 if (axisOptions.tickSize != null)
976 size = axisOptions.tickSize;
977
978 axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec);
979
980 generator = function (axis) {
981 var ticks = [];
982
983 // spew out all possible ticks
984 var start = floorInBase(axis.min, axis.tickSize),
985 i = 0, v = Number.NaN, prev;
986 do {
987 prev = v;
988 v = start + i * axis.tickSize;
989 ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
990 ++i;
991 } while (v < axis.max && v != prev);
992 return ticks;
993 };
994
995 formatter = function (v, axis) {
996 return v.toFixed(axis.tickDecimals);
997 };
998 }
999
1000 axis.tickSize = unit ? [size, unit] : size;
1001 axis.tickGenerator = generator;
1002 if ($.isFunction(axisOptions.tickFormatter))
1003 axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); };
1004 else
1005 axis.tickFormatter = formatter;
1006 }
1007
1008 function setTicks(axis, axisOptions) {
1009 axis.ticks = [];
1010
1011 if (!axis.used)
1012 return;
1013
1014 if (axisOptions.ticks == null)
1015 axis.ticks = axis.tickGenerator(axis);
1016 else if (typeof axisOptions.ticks == "number") {
1017 if (axisOptions.ticks > 0)
1018 axis.ticks = axis.tickGenerator(axis);
1019 }
1020 else if (axisOptions.ticks) {
1021 var ticks = axisOptions.ticks;
1022
1023 if ($.isFunction(ticks))
1024 // generate the ticks
1025 ticks = ticks({ min: axis.min, max: axis.max });
1026
1027 // clean up the user-supplied ticks, copy them over
1028 var i, v;
1029 for (i = 0; i < ticks.length; ++i) {
1030 var label = null;
1031 var t = ticks[i];
1032 if (typeof t == "object") {
1033 v = t[0];
1034 if (t.length > 1)
1035 label = t[1];
1036 }
1037 else
1038 v = t;
1039 if (label == null)
1040 label = axis.tickFormatter(v, axis);
1041 axis.ticks[i] = { v: v, label: label };
1042 }
1043 }
1044
1045 if (axisOptions.autoscaleMargin != null && axis.ticks.length > 0) {
1046 // snap to ticks
1047 if (axisOptions.min == null)
1048 axis.min = Math.min(axis.min, axis.ticks[0].v);
1049 if (axisOptions.max == null && axis.ticks.length > 1)
1050 axis.max = Math.max(axis.max, axis.ticks[axis.ticks.length - 1].v);
1051 }
1052 }
1053
1054 function draw() {
1055 ctx.clearRect(0, 0, canvasWidth, canvasHeight);
1056
1057 var grid = options.grid;
1058
1059 if (grid.show && !grid.aboveData)
1060 drawGrid();
1061
1062 for (var i = 0; i < series.length; ++i)
1063 drawSeries(series[i]);
1064
1065 executeHooks(hooks.draw, [ctx]);
1066
1067 if (grid.show && grid.aboveData)
1068 drawGrid();
1069 }
1070
1071 function extractRange(ranges, coord) {
1072 var firstAxis = coord + "axis",
1073 secondaryAxis = coord + "2axis",
1074 axis, from, to, reverse;
1075
1076 if (ranges[firstAxis]) {
1077 axis = axes[firstAxis];
1078 from = ranges[firstAxis].from;
1079 to = ranges[firstAxis].to;
1080 }
1081 else if (ranges[secondaryAxis]) {
1082 axis = axes[secondaryAxis];
1083 from = ranges[secondaryAxis].from;
1084 to = ranges[secondaryAxis].to;
1085 }
1086 else {
1087 // backwards-compat stuff - to be removed in future
1088 axis = axes[firstAxis];
1089 from = ranges[coord + "1"];
1090 to = ranges[coord + "2"];
1091 }
1092
1093 // auto-reverse as an added bonus
1094 if (from != null && to != null && from > to)
1095 return { from: to, to: from, axis: axis };
1096
1097 return { from: from, to: to, axis: axis };
1098 }
1099
1100 function drawGrid() {
1101 var i;
1102
1103 ctx.save();
1104 ctx.translate(plotOffset.left, plotOffset.top);
1105
1106 // draw background, if any
1107 if (options.grid.backgroundColor) {
1108 ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
1109 ctx.fillRect(0, 0, plotWidth, plotHeight);
1110 }
1111
1112 // draw markings
1113 var markings = options.grid.markings;
1114 if (markings) {
1115 if ($.isFunction(markings))
1116 // xmin etc. are backwards-compatible, to be removed in future
1117 markings = markings({ xmin: axes.xaxis.min, xmax: axes.xaxis.max, ymin: axes.yaxis.min, ymax: axes.yaxis.max, xaxis: axes.xaxis, yaxis: axes.yaxis, x2axis: axes.x2axis, y2axis: axes.y2axis });
1118
1119 for (i = 0; i < markings.length; ++i) {
1120 var m = markings[i],
1121 xrange = extractRange(m, "x"),
1122 yrange = extractRange(m, "y");
1123
1124 // fill in missing
1125 if (xrange.from == null)
1126 xrange.from = xrange.axis.min;
1127 if (xrange.to == null)
1128 xrange.to = xrange.axis.max;
1129 if (yrange.from == null)
1130 yrange.from = yrange.axis.min;
1131 if (yrange.to == null)
1132 yrange.to = yrange.axis.max;
1133
1134 // clip
1135 if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
1136 yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
1137 continue;
1138
1139 xrange.from = Math.max(xrange.from, xrange.axis.min);
1140 xrange.to = Math.min(xrange.to, xrange.axis.max);
1141 yrange.from = Math.max(yrange.from, yrange.axis.min);
1142 yrange.to = Math.min(yrange.to, yrange.axis.max);
1143
1144 if (xrange.from == xrange.to && yrange.from == yrange.to)
1145 continue;
1146
1147 // then draw
1148 xrange.from = xrange.axis.p2c(xrange.from);
1149 xrange.to = xrange.axis.p2c(xrange.to);
1150 yrange.from = yrange.axis.p2c(yrange.from);
1151 yrange.to = yrange.axis.p2c(yrange.to);
1152
1153 if (xrange.from == xrange.to || yrange.from == yrange.to) {
1154 // draw line
1155 ctx.beginPath();
1156 ctx.strokeStyle = m.color || options.grid.markingsColor;
1157 ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
1158 //ctx.moveTo(Math.floor(xrange.from), yrange.from);
1159 //ctx.lineTo(Math.floor(xrange.to), yrange.to);
1160 ctx.moveTo(xrange.from, yrange.from);
1161 ctx.lineTo(xrange.to, yrange.to);
1162 ctx.stroke();
1163 }
1164 else {
1165 // fill area
1166 ctx.fillStyle = m.color || options.grid.markingsColor;
1167 ctx.fillRect(xrange.from, yrange.to,
1168 xrange.to - xrange.from,
1169 yrange.from - yrange.to);
1170 }
1171 }
1172 }
1173
1174 // draw the inner grid
1175 ctx.lineWidth = 1;
1176 ctx.strokeStyle = options.grid.tickColor;
1177 ctx.beginPath();
1178 var v, axis = axes.xaxis;
1179 for (i = 0; i < axis.ticks.length; ++i) {
1180 v = axis.ticks[i].v;
1181 if (v <= axis.min || v >= axes.xaxis.max)
1182 continue; // skip those lying on the axes
1183
1184 ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 0);
1185 ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, plotHeight);
1186 }
1187
1188 axis = axes.yaxis;
1189 for (i = 0; i < axis.ticks.length; ++i) {
1190 v = axis.ticks[i].v;
1191 if (v <= axis.min || v >= axis.max)
1192 continue;
1193
1194 ctx.moveTo(0, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
1195 ctx.lineTo(plotWidth, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
1196 }
1197
1198 axis = axes.x2axis;
1199 for (i = 0; i < axis.ticks.length; ++i) {
1200 v = axis.ticks[i].v;
1201 if (v <= axis.min || v >= axis.max)
1202 continue;
1203
1204 ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, -5);
1205 ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 5);
1206 }
1207
1208 axis = axes.y2axis;
1209 for (i = 0; i < axis.ticks.length; ++i) {
1210 v = axis.ticks[i].v;
1211 if (v <= axis.min || v >= axis.max)
1212 continue;
1213
1214 ctx.moveTo(plotWidth-5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
1215 ctx.lineTo(plotWidth+5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
1216 }
1217
1218 ctx.stroke();
1219
1220 if (options.grid.borderWidth) {
1221 // draw border
1222 var bw = options.grid.borderWidth;
1223 ctx.lineWidth = bw;
1224 ctx.strokeStyle = options.grid.borderColor;
1225 ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
1226 }
1227
1228 ctx.restore();
1229 }
1230
1231 function insertLabels() {
1232 placeholder.find(".tickLabels").remove();
1233
1234 var html = ['<div class="tickLabels" style="font-size:smaller;color:' + options.grid.color + '">'];
1235
1236 function addLabels(axis, labelGenerator) {
1237 for (var i = 0; i < axis.ticks.length; ++i) {
1238 var tick = axis.ticks[i];
1239 if (!tick.label || tick.v < axis.min || tick.v > axis.max)
1240 continue;
1241 html.push(labelGenerator(tick, axis));
1242 }
1243 }
1244
1245 var margin = options.grid.labelMargin + options.grid.borderWidth;
1246
1247 addLabels(axes.xaxis, function (tick, axis) {
1248 return '<div style="position:absolute;top:' + (plotOffset.top + plotHeight + margin) + 'px;left:' + Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2) + 'px;width:' + axis.labelWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>";
1249 });
1250
1251
1252 addLabels(axes.yaxis, function (tick, axis) {
1253 return '<div style="position:absolute;top:' + Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2) + 'px;right:' + (plotOffset.right + plotWidth + margin) + 'px;width:' + axis.labelWidth + 'px;text-align:right" class="tickLabel">' + tick.label + "</div>";
1254 });
1255
1256 addLabels(axes.x2axis, function (tick, axis) {
1257 return '<div style="position:absolute;bottom:' + (plotOffset.bottom + plotHeight + margin) + 'px;left:' + Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2) + 'px;width:' + axis.labelWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>";
1258 });
1259
1260 addLabels(axes.y2axis, function (tick, axis) {
1261 return '<div style="position:absolute;top:' + Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2) + 'px;left:' + (plotOffset.left + plotWidth + margin) +'px;width:' + axis.labelWidth + 'px;text-align:left" class="tickLabel">' + tick.label + "</div>";
1262 });
1263
1264 html.push('</div>');
1265
1266 placeholder.append(html.join(""));
1267 }
1268
1269 function drawSeries(series) {
1270 if (series.lines.show)
1271 drawSeriesLines(series);
1272 if (series.bars.show)
1273 drawSeriesBars(series);
1274 if (series.points.show)
1275 drawSeriesPoints(series);
1276 }
1277
1278 function drawSeriesLines(series) {
1279 function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
1280 var points = datapoints.points,
1281 ps = datapoints.pointsize,
1282 prevx = null, prevy = null;
1283
1284 ctx.beginPath();
1285 for (var i = ps; i < points.length; i += ps) {
1286 var x1 = points[i - ps], y1 = points[i - ps + 1],
1287 x2 = points[i], y2 = points[i + 1];
1288
1289 if (x1 == null || x2 == null)
1290 continue;
1291
1292 // clip with ymin
1293 if (y1 <= y2 && y1 < axisy.min) {
1294 if (y2 < axisy.min)
1295 continue; // line segment is outside
1296 // compute new intersection point
1297 x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1298 y1 = axisy.min;
1299 }
1300 else if (y2 <= y1 && y2 < axisy.min) {
1301 if (y1 < axisy.min)
1302 continue;
1303 x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1304 y2 = axisy.min;
1305 }
1306
1307 // clip with ymax
1308 if (y1 >= y2 && y1 > axisy.max) {
1309 if (y2 > axisy.max)
1310 continue;
1311 x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1312 y1 = axisy.max;
1313 }
1314 else if (y2 >= y1 && y2 > axisy.max) {
1315 if (y1 > axisy.max)
1316 continue;
1317 x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1318 y2 = axisy.max;
1319 }
1320
1321 // clip with xmin
1322 if (x1 <= x2 && x1 < axisx.min) {
1323 if (x2 < axisx.min)
1324 continue;
1325 y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1326 x1 = axisx.min;
1327 }
1328 else if (x2 <= x1 && x2 < axisx.min) {
1329 if (x1 < axisx.min)
1330 continue;
1331 y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1332 x2 = axisx.min;
1333 }
1334
1335 // clip with xmax
1336 if (x1 >= x2 && x1 > axisx.max) {
1337 if (x2 > axisx.max)
1338 continue;
1339 y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1340 x1 = axisx.max;
1341 }
1342 else if (x2 >= x1 && x2 > axisx.max) {
1343 if (x1 > axisx.max)
1344 continue;
1345 y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1346 x2 = axisx.max;
1347 }
1348
1349 if (x1 != prevx || y1 != prevy)
1350 ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
1351
1352 prevx = x2;
1353 prevy = y2;
1354 ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
1355 }
1356 ctx.stroke();
1357 }
1358
1359 function plotLineArea(datapoints, axisx, axisy) {
1360 var points = datapoints.points,
1361 ps = datapoints.pointsize,
1362 bottom = Math.min(Math.max(0, axisy.min), axisy.max),
1363 top, lastX = 0, areaOpen = false;
1364
1365 for (var i = ps; i < points.length; i += ps) {
1366 var x1 = points[i - ps], y1 = points[i - ps + 1],
1367 x2 = points[i], y2 = points[i + 1];
1368
1369 if (areaOpen && x1 != null && x2 == null) {
1370 // close area
1371 ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom));
1372 ctx.fill();
1373 areaOpen = false;
1374 continue;
1375 }
1376
1377 if (x1 == null || x2 == null)
1378 continue;
1379
1380 // clip x values
1381
1382 // clip with xmin
1383 if (x1 <= x2 && x1 < axisx.min) {
1384 if (x2 < axisx.min)
1385 continue;
1386 y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1387 x1 = axisx.min;
1388 }
1389 else if (x2 <= x1 && x2 < axisx.min) {
1390 if (x1 < axisx.min)
1391 continue;
1392 y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1393 x2 = axisx.min;
1394 }
1395
1396 // clip with xmax
1397 if (x1 >= x2 && x1 > axisx.max) {
1398 if (x2 > axisx.max)
1399 continue;
1400 y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1401 x1 = axisx.max;
1402 }
1403 else if (x2 >= x1 && x2 > axisx.max) {
1404 if (x1 > axisx.max)
1405 continue;
1406 y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1407 x2 = axisx.max;
1408 }
1409
1410 if (!areaOpen) {
1411 // open area
1412 ctx.beginPath();
1413 ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
1414 areaOpen = true;
1415 }
1416
1417 // now first check the case where both is outside
1418 if (y1 >= axisy.max && y2 >= axisy.max) {
1419 ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
1420 ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
1421 lastX = x2;
1422 continue;
1423 }
1424 else if (y1 <= axisy.min && y2 <= axisy.min) {
1425 ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
1426 ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
1427 lastX = x2;
1428 continue;
1429 }
1430
1431 // else it's a bit more complicated, there might
1432 // be two rectangles and two triangles we need to fill
1433 // in; to find these keep track of the current x values
1434 var x1old = x1, x2old = x2;
1435
1436 // and clip the y values, without shortcutting
1437
1438 // clip with ymin
1439 if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
1440 x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1441 y1 = axisy.min;
1442 }
1443 else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
1444 x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1445 y2 = axisy.min;
1446 }
1447
1448 // clip with ymax
1449 if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
1450 x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1451 y1 = axisy.max;
1452 }
1453 else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
1454 x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1455 y2 = axisy.max;
1456 }
1457
1458
1459 // if the x value was changed we got a rectangle
1460 // to fill
1461 if (x1 != x1old) {
1462 if (y1 <= axisy.min)
1463 top = axisy.min;
1464 else
1465 top = axisy.max;
1466
1467 ctx.lineTo(axisx.p2c(x1old), axisy.p2c(top));
1468 ctx.lineTo(axisx.p2c(x1), axisy.p2c(top));
1469 }
1470
1471 // fill the triangles
1472 ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
1473 ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
1474
1475 // fill the other rectangle if it's there
1476 if (x2 != x2old) {
1477 if (y2 <= axisy.min)
1478 top = axisy.min;
1479 else
1480 top = axisy.max;
1481
1482 ctx.lineTo(axisx.p2c(x2), axisy.p2c(top));
1483 ctx.lineTo(axisx.p2c(x2old), axisy.p2c(top));
1484 }
1485
1486 lastX = Math.max(x2, x2old);
1487 }
1488
1489 if (areaOpen) {
1490 ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom));
1491 ctx.fill();
1492 }
1493 }
1494
1495 ctx.save();
1496 ctx.translate(plotOffset.left, plotOffset.top);
1497 ctx.lineJoin = "round";
1498
1499 var lw = series.lines.lineWidth,
1500 sw = series.shadowSize;
1501 // FIXME: consider another form of shadow when filling is turned on
1502 if (lw > 0 && sw > 0) {
1503 // draw shadow as a thick and thin line with transparency
1504 ctx.lineWidth = sw;
1505 ctx.strokeStyle = "rgba(0,0,0,0.1)";
1506 // position shadow at angle from the mid of line
1507 var angle = Math.PI/18;
1508 plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
1509 ctx.lineWidth = sw/2;
1510 plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
1511 }
1512
1513 ctx.lineWidth = lw;
1514 ctx.strokeStyle = series.color;
1515 var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
1516 if (fillStyle) {
1517 ctx.fillStyle = fillStyle;
1518 plotLineArea(series.datapoints, series.xaxis, series.yaxis);
1519 }
1520
1521 if (lw > 0)
1522 plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
1523 ctx.restore();
1524 }
1525
1526 function drawSeriesPoints(series) {
1527 function plotPoints(datapoints, radius, fillStyle, offset, circumference, axisx, axisy) {
1528 var points = datapoints.points, ps = datapoints.pointsize;
1529
1530 for (var i = 0; i < points.length; i += ps) {
1531 var x = points[i], y = points[i + 1];
1532 if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
1533 continue;
1534
1535 ctx.beginPath();
1536 ctx.arc(axisx.p2c(x), axisy.p2c(y) + offset, radius, 0, circumference, false);
1537 if (fillStyle) {
1538 ctx.fillStyle = fillStyle;
1539 ctx.fill();
1540 }
1541 ctx.stroke();
1542 }
1543 }
1544
1545 ctx.save();
1546 ctx.translate(plotOffset.left, plotOffset.top);
1547
1548 var lw = series.lines.lineWidth,
1549 sw = series.shadowSize,
1550 radius = series.points.radius;
1551 if (lw > 0 && sw > 0) {
1552 // draw shadow in two steps
1553 var w = sw / 2;
1554 ctx.lineWidth = w;
1555 ctx.strokeStyle = "rgba(0,0,0,0.1)";
1556 plotPoints(series.datapoints, radius, null, w + w/2, Math.PI,
1557 series.xaxis, series.yaxis);
1558
1559 ctx.strokeStyle = "rgba(0,0,0,0.2)";
1560 plotPoints(series.datapoints, radius, null, w/2, Math.PI,
1561 series.xaxis, series.yaxis);
1562 }
1563
1564 ctx.lineWidth = lw;
1565 ctx.strokeStyle = series.color;
1566 plotPoints(series.datapoints, radius,
1567 getFillStyle(series.points, series.color), 0, 2 * Math.PI,
1568 series.xaxis, series.yaxis);
1569 ctx.restore();
1570 }
1571
1572 function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal) {
1573 var left, right, bottom, top,
1574 drawLeft, drawRight, drawTop, drawBottom,
1575 tmp;
1576
1577 if (horizontal) {
1578 drawBottom = drawRight = drawTop = true;
1579 drawLeft = false;
1580 left = b;
1581 right = x;
1582 top = y + barLeft;
1583 bottom = y + barRight;
1584
1585 // account for negative bars
1586 if (right < left) {
1587 tmp = right;
1588 right = left;
1589 left = tmp;
1590 drawLeft = true;
1591 drawRight = false;
1592 }
1593 }
1594 else {
1595 drawLeft = drawRight = drawTop = true;
1596 drawBottom = false;
1597 left = x + barLeft;
1598 right = x + barRight;
1599 bottom = b;
1600 top = y;
1601
1602 // account for negative bars
1603 if (top < bottom) {
1604 tmp = top;
1605 top = bottom;
1606 bottom = tmp;
1607 drawBottom = true;
1608 drawTop = false;
1609 }
1610 }
1611
1612 // clip
1613 if (right < axisx.min || left > axisx.max ||
1614 top < axisy.min || bottom > axisy.max)
1615 return;
1616
1617 if (left < axisx.min) {
1618 left = axisx.min;
1619 drawLeft = false;
1620 }
1621
1622 if (right > axisx.max) {
1623 right = axisx.max;
1624 drawRight = false;
1625 }
1626
1627 if (bottom < axisy.min) {
1628 bottom = axisy.min;
1629 drawBottom = false;
1630 }
1631
1632 if (top > axisy.max) {
1633 top = axisy.max;
1634 drawTop = false;
1635 }
1636
1637 left = axisx.p2c(left);
1638 bottom = axisy.p2c(bottom);
1639 right = axisx.p2c(right);
1640 top = axisy.p2c(top);
1641
1642 // fill the bar
1643 if (fillStyleCallback) {
1644 c.beginPath();
1645 c.moveTo(left, bottom);
1646 c.lineTo(left, top);
1647 c.lineTo(right, top);
1648 c.lineTo(right, bottom);
1649 c.fillStyle = fillStyleCallback(bottom, top);
1650 c.fill();
1651 }
1652
1653 // draw outline
1654 if (drawLeft || drawRight || drawTop || drawBottom) {
1655 c.beginPath();
1656
1657 // FIXME: inline moveTo is buggy with excanvas
1658 c.moveTo(left, bottom + offset);
1659 if (drawLeft)
1660 c.lineTo(left, top + offset);
1661 else
1662 c.moveTo(left, top + offset);
1663 if (drawTop)
1664 c.lineTo(right, top + offset);
1665 else
1666 c.moveTo(right, top + offset);
1667 if (drawRight)
1668 c.lineTo(right, bottom + offset);
1669 else
1670 c.moveTo(right, bottom + offset);
1671 if (drawBottom)
1672 c.lineTo(left, bottom + offset);
1673 else
1674 c.moveTo(left, bottom + offset);
1675 c.stroke();
1676 }
1677 }
1678
1679 function drawSeriesBars(series) {
1680 function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
1681 var points = datapoints.points, ps = datapoints.pointsize;
1682
1683 for (var i = 0; i < points.length; i += ps) {
1684 if (points[i] == null)
1685 continue;
1686 drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal);
1687 }
1688 }
1689
1690 ctx.save();
1691 ctx.translate(plotOffset.left, plotOffset.top);
1692
1693 // FIXME: figure out a way to add shadows (for instance along the right edge)
1694 ctx.lineWidth = series.bars.lineWidth;
1695 ctx.strokeStyle = series.color;
1696 var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
1697 var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
1698 plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
1699 ctx.restore();
1700 }
1701
1702 function getFillStyle(filloptions, seriesColor, bottom, top) {
1703 var fill = filloptions.fill;
1704 if (!fill)
1705 return null;
1706
1707 if (filloptions.fillColor)
1708 return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
1709
1710 var c = $.color.parse(seriesColor);
1711 c.a = typeof fill == "number" ? fill : 0.4;
1712 c.normalize();
1713 return c.toString();
1714 }
1715
1716 function insertLegend() {
1717 placeholder.find(".legend").remove();
1718
1719 if (!options.legend.show)
1720 return;
1721
1722 var fragments = [], rowStarted = false,
1723 lf = options.legend.labelFormatter, s, label;
1724 for (i = 0; i < series.length; ++i) {
1725 s = series[i];
1726 label = s.label;
1727 if (!label)
1728 continue;
1729
1730 if (i % options.legend.noColumns == 0) {
1731 if (rowStarted)
1732 fragments.push('</tr>');
1733 fragments.push('<tr>');
1734 rowStarted = true;
1735 }
1736
1737 if (lf)
1738 label = lf(label, s);
1739
1740 fragments.push(
1741 '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + s.color + ';overflow:hidden"></div></div></td>' +
1742 '<td class="legendLabel">' + label + '</td>');
1743 }
1744 if (rowStarted)
1745 fragments.push('</tr>');
1746
1747 if (fragments.length == 0)
1748 return;
1749
1750 var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
1751 if (options.legend.container != null)
1752 $(options.legend.container).html(table);
1753 else {
1754 var pos = "",
1755 p = options.legend.position,
1756 m = options.legend.margin;
1757 if (m[0] == null)
1758 m = [m, m];
1759 if (p.charAt(0) == "n")
1760 pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
1761 else if (p.charAt(0) == "s")
1762 pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
1763 if (p.charAt(1) == "e")
1764 pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
1765 else if (p.charAt(1) == "w")
1766 pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
1767 var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
1768 if (options.legend.backgroundOpacity != 0.0) {
1769 // put in the transparent background
1770 // separately to avoid blended labels and
1771 // label boxes
1772 var c = options.legend.backgroundColor;
1773 if (c == null) {
1774 c = options.grid.backgroundColor;
1775 if (c && typeof c == "string")
1776 c = $.color.parse(c);
1777 else
1778 c = $.color.extract(legend, 'background-color');
1779 c.a = 1;
1780 c = c.toString();
1781 }
1782 var div = legend.children();
1783 $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
1784 }
1785 }
1786 }
1787
1788
1789 // interactive features
1790
1791 var highlights = [],
1792 redrawTimeout = null;
1793
1794 // returns the data item the mouse is over, or null if none is found
1795 function findNearbyItem(mouseX, mouseY, seriesFilter) {
1796 var maxDistance = options.grid.mouseActiveRadius,
1797 smallestDistance = maxDistance * maxDistance + 1,
1798 item = null, foundPoint = false, i, j;
1799
1800 for (i = 0; i < series.length; ++i) {
1801 if (!seriesFilter(series[i]))
1802 continue;
1803
1804 var s = series[i],
1805 axisx = s.xaxis,
1806 axisy = s.yaxis,
1807 points = s.datapoints.points,
1808 ps = s.datapoints.pointsize,
1809 mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
1810 my = axisy.c2p(mouseY),
1811 maxx = maxDistance / axisx.scale,
1812 maxy = maxDistance / axisy.scale;
1813
1814 if (s.lines.show || s.points.show) {
1815 for (j = 0; j < points.length; j += ps) {
1816 var x = points[j], y = points[j + 1];
1817 if (x == null)
1818 continue;
1819
1820 // For points and lines, the cursor must be within a
1821 // certain distance to the data point
1822 if (x - mx > maxx || x - mx < -maxx ||
1823 y - my > maxy || y - my < -maxy)
1824 continue;
1825
1826 // We have to calculate distances in pixels, not in
1827 // data units, because the scales of the axes may be different
1828 var dx = Math.abs(axisx.p2c(x) - mouseX),
1829 dy = Math.abs(axisy.p2c(y) - mouseY),
1830 dist = dx * dx + dy * dy; // we save the sqrt
1831
1832 // use <= to ensure last point takes precedence
1833 // (last generally means on top of)
1834 if (dist <= smallestDistance) {
1835 smallestDistance = dist;
1836 item = [i, j / ps];
1837 }
1838 }
1839 }
1840
1841 if (s.bars.show && !item) { // no other point can be nearby
1842 var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
1843 barRight = barLeft + s.bars.barWidth;
1844
1845 for (j = 0; j < points.length; j += ps) {
1846 var x = points[j], y = points[j + 1], b = points[j + 2];
1847 if (x == null)
1848 continue;
1849
1850 // for a bar graph, the cursor must be inside the bar
1851 if (series[i].bars.horizontal ?
1852 (mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
1853 my >= y + barLeft && my <= y + barRight) :
1854 (mx >= x + barLeft && mx <= x + barRight &&
1855 my >= Math.min(b, y) && my <= Math.max(b, y)))
1856 item = [i, j / ps];
1857 }
1858 }
1859 }
1860
1861 if (item) {
1862 i = item[0];
1863 j = item[1];
1864 ps = series[i].datapoints.pointsize;
1865
1866 return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
1867 dataIndex: j,
1868 series: series[i],
1869 seriesIndex: i };
1870 }
1871
1872 return null;
1873 }
1874
1875 function onMouseMove(e) {
1876 if (options.grid.hoverable)
1877 triggerClickHoverEvent("plothover", e,
1878 function (s) { return s["hoverable"] != false; });
1879 }
1880
1881 function onClick(e) {
1882 triggerClickHoverEvent("plotclick", e,
1883 function (s) { return s["clickable"] != false; });
1884 }
1885
1886 // trigger click or hover event (they send the same parameters
1887 // so we share their code)
1888 function triggerClickHoverEvent(eventname, event, seriesFilter) {
1889 var offset = eventHolder.offset(),
1890 pos = { pageX: event.pageX, pageY: event.pageY },
1891 canvasX = event.pageX - offset.left - plotOffset.left,
1892 canvasY = event.pageY - offset.top - plotOffset.top;
1893
1894 if (axes.xaxis.used)
1895 pos.x = axes.xaxis.c2p(canvasX);
1896 if (axes.yaxis.used)
1897 pos.y = axes.yaxis.c2p(canvasY);
1898 if (axes.x2axis.used)
1899 pos.x2 = axes.x2axis.c2p(canvasX);
1900 if (axes.y2axis.used)
1901 pos.y2 = axes.y2axis.c2p(canvasY);
1902
1903 var item = findNearbyItem(canvasX, canvasY, seriesFilter);
1904
1905 if (item) {
1906 // fill in mouse pos for any listeners out there
1907 item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
1908 item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
1909 }
1910
1911 if (options.grid.autoHighlight) {
1912 // clear auto-highlights
1913 for (var i = 0; i < highlights.length; ++i) {
1914 var h = highlights[i];
1915 if (h.auto == eventname &&
1916 !(item && h.series == item.series && h.point == item.datapoint))
1917 unhighlight(h.series, h.point);
1918 }
1919
1920 if (item)
1921 highlight(item.series, item.datapoint, eventname);
1922 }
1923
1924 placeholder.trigger(eventname, [ pos, item ]);
1925 }
1926
1927 function triggerRedrawOverlay() {
1928 if (!redrawTimeout)
1929 redrawTimeout = setTimeout(drawOverlay, 30);
1930 }
1931
1932 function drawOverlay() {
1933 redrawTimeout = null;
1934
1935 // draw highlights
1936 octx.save();
1937 octx.clearRect(0, 0, canvasWidth, canvasHeight);
1938 octx.translate(plotOffset.left, plotOffset.top);
1939
1940 var i, hi;
1941 for (i = 0; i < highlights.length; ++i) {
1942 hi = highlights[i];
1943
1944 if (hi.series.bars.show)
1945 drawBarHighlight(hi.series, hi.point);
1946 else
1947 drawPointHighlight(hi.series, hi.point);
1948 }
1949 octx.restore();
1950
1951 executeHooks(hooks.drawOverlay, [octx]);
1952 }
1953
1954 function highlight(s, point, auto) {
1955 if (typeof s == "number")
1956 s = series[s];
1957
1958 if (typeof point == "number")
1959 point = s.data[point];
1960
1961 var i = indexOfHighlight(s, point);
1962 if (i == -1) {
1963 highlights.push({ series: s, point: point, auto: auto });
1964
1965 triggerRedrawOverlay();
1966 }
1967 else if (!auto)
1968 highlights[i].auto = false;
1969 }
1970
1971 function unhighlight(s, point) {
1972 if (s == null && point == null) {
1973 highlights = [];
1974 triggerRedrawOverlay();
1975 }
1976
1977 if (typeof s == "number")
1978 s = series[s];
1979
1980 if (typeof point == "number")
1981 point = s.data[point];
1982
1983 var i = indexOfHighlight(s, point);
1984 if (i != -1) {
1985 highlights.splice(i, 1);
1986
1987 triggerRedrawOverlay();
1988 }
1989 }
1990
1991 function indexOfHighlight(s, p) {
1992 for (var i = 0; i < highlights.length; ++i) {
1993 var h = highlights[i];
1994 if (h.series == s && h.point[0] == p[0]
1995 && h.point[1] == p[1])
1996 return i;
1997 }
1998 return -1;
1999 }
2000
2001 function drawPointHighlight(series, point) {
2002 var x = point[0], y = point[1],
2003 axisx = series.xaxis, axisy = series.yaxis;
2004
2005 if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
2006 return;
2007
2008 var pointRadius = series.points.radius + series.points.lineWidth / 2;
2009 octx.lineWidth = pointRadius;
2010 octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
2011 var radius = 1.5 * pointRadius;
2012 octx.beginPath();
2013 octx.arc(axisx.p2c(x), axisy.p2c(y), radius, 0, 2 * Math.PI, false);
2014 octx.stroke();
2015 }
2016
2017 function drawBarHighlight(series, point) {
2018 octx.lineWidth = series.bars.lineWidth;
2019 octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
2020 var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
2021 var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
2022 drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
2023 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal);
2024 }
2025
2026 function getColorOrGradient(spec, bottom, top, defaultColor) {
2027 if (typeof spec == "string")
2028 return spec;
2029 else {
2030 // assume this is a gradient spec; IE currently only
2031 // supports a simple vertical gradient properly, so that's
2032 // what we support too
2033 var gradient = ctx.createLinearGradient(0, top, 0, bottom);
2034
2035 for (var i = 0, l = spec.colors.length; i < l; ++i) {
2036 var c = spec.colors[i];
2037 if (typeof c != "string") {
2038 c = $.color.parse(defaultColor).scale('rgb', c.brightness);
2039 c.a *= c.opacity;
2040 c = c.toString();
2041 }
2042 gradient.addColorStop(i / (l - 1), c);
2043 }
2044
2045 return gradient;
2046 }
2047 }
2048 }
2049
2050 $.plot = function(placeholder, data, options) {
2051 var plot = new Plot($(placeholder), data, options, $.plot.plugins);
2052 /*var t0 = new Date();
2053 var t1 = new Date();
2054 var tstr = "time used (msecs): " + (t1.getTime() - t0.getTime())
2055 if (window.console)
2056 console.log(tstr);
2057 else
2058 alert(tstr);*/
2059 return plot;
2060 };
2061
2062 $.plot.plugins = [];
2063
2064 // returns a string with the date d formatted according to fmt
2065 $.plot.formatDate = function(d, fmt, monthNames) {
2066 var leftPad = function(n) {
2067 n = "" + n;
2068 return n.length == 1 ? "0" + n : n;
2069 };
2070
2071 var r = [];
2072 var escape = false;
2073 var hours = d.getUTCHours();
2074 var isAM = hours < 12;
2075 if (monthNames == null)
2076 monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
2077
2078 if (fmt.search(/%p|%P/) != -1) {
2079 if (hours > 12) {
2080 hours = hours - 12;
2081 } else if (hours == 0) {
2082 hours = 12;
2083 }
2084 }
2085 for (var i = 0; i < fmt.length; ++i) {
2086 var c = fmt.charAt(i);
2087
2088 if (escape) {
2089 switch (c) {
2090 case 'h': c = "" + hours; break;
2091 case 'H': c = leftPad(hours); break;
2092 case 'M': c = leftPad(d.getUTCMinutes()); break;
2093 case 'S': c = leftPad(d.getUTCSeconds()); break;
2094 case 'd': c = "" + d.getUTCDate(); break;
2095 case 'm': c = "" + (d.getUTCMonth() + 1); break;
2096 case 'y': c = "" + d.getUTCFullYear(); break;
2097 case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
2098 case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
2099 case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
2100 }
2101 r.push(c);
2102 escape = false;
2103 }
2104 else {
2105 if (c == "%")
2106 escape = true;
2107 else
2108 r.push(c);
2109 }
2110 }
2111 return r.join("");
2112 };
2113
2114 // round to nearby lower multiple of base
2115 function floorInBase(n, base) {
2116 return base * Math.floor(n / base);
2117 }
2118
2119})(jQuery);
02120
=== added file 'webapp/content/js/jquery.flot.selection.js'
--- webapp/content/js/jquery.flot.selection.js 1970-01-01 00:00:00 +0000
+++ webapp/content/js/jquery.flot.selection.js 2010-11-01 18:48:50 +0000
@@ -0,0 +1,299 @@
1/*
2Flot plugin for selecting regions.
3
4The plugin defines the following options:
5
6 selection: {
7 mode: null or "x" or "y" or "xy",
8 color: color
9 }
10
11You enable selection support by setting the mode to one of "x", "y" or
12"xy". In "x" mode, the user will only be able to specify the x range,
13similarly for "y" mode. For "xy", the selection becomes a rectangle
14where both ranges can be specified. "color" is color of the selection.
15
16When selection support is enabled, a "plotselected" event will be emitted
17on the DOM element you passed into the plot function. The event
18handler gets one extra parameter with the ranges selected on the axes,
19like this:
20
21 placeholder.bind("plotselected", function(event, ranges) {
22 alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to)
23 // similar for yaxis, secondary axes are in x2axis
24 // and y2axis if present
25 });
26
27The "plotselected" event is only fired when the user has finished
28making the selection. A "plotselecting" event is fired during the
29process with the same parameters as the "plotselected" event, in case
30you want to know what's happening while it's happening,
31
32A "plotunselected" event with no arguments is emitted when the user
33clicks the mouse to remove the selection.
34
35The plugin allso adds the following methods to the plot object:
36
37- setSelection(ranges, preventEvent)
38
39 Set the selection rectangle. The passed in ranges is on the same
40 form as returned in the "plotselected" event. If the selection
41 mode is "x", you should put in either an xaxis (or x2axis) object,
42 if the mode is "y" you need to put in an yaxis (or y2axis) object
43 and both xaxis/x2axis and yaxis/y2axis if the selection mode is
44 "xy", like this:
45
46 setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } });
47
48 setSelection will trigger the "plotselected" event when called. If
49 you don't want that to happen, e.g. if you're inside a
50 "plotselected" handler, pass true as the second parameter.
51
52- clearSelection(preventEvent)
53
54 Clear the selection rectangle. Pass in true to avoid getting a
55 "plotunselected" event.
56
57- getSelection()
58
59 Returns the current selection in the same format as the
60 "plotselected" event. If there's currently no selection, the
61 function returns null.
62
63*/
64
65(function ($) {
66 function init(plot) {
67 var selection = {
68 first: { x: -1, y: -1}, second: { x: -1, y: -1},
69 show: false,
70 active: false
71 };
72
73 // FIXME: The drag handling implemented here should be
74 // abstracted out, there's some similar code from a library in
75 // the navigation plugin, this should be massaged a bit to fit
76 // the Flot cases here better and reused. Doing this would
77 // make this plugin much slimmer.
78 var savedhandlers = {};
79
80 function onMouseMove(e) {
81 if (selection.active) {
82 plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]);
83
84 updateSelection(e);
85 }
86 }
87
88 function onMouseDown(e) {
89 if (e.which != 1) // only accept left-click
90 return;
91
92 // cancel out any text selections
93 document.body.focus();
94
95 // prevent text selection and drag in old-school browsers
96 if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) {
97 savedhandlers.onselectstart = document.onselectstart;
98 document.onselectstart = function () { return false; };
99 }
100 if (document.ondrag !== undefined && savedhandlers.ondrag == null) {
101 savedhandlers.ondrag = document.ondrag;
102 document.ondrag = function () { return false; };
103 }
104
105 setSelectionPos(selection.first, e);
106
107 selection.active = true;
108
109 $(document).one("mouseup", onMouseUp);
110 }
111
112 function onMouseUp(e) {
113 // revert drag stuff for old-school browsers
114 if (document.onselectstart !== undefined)
115 document.onselectstart = savedhandlers.onselectstart;
116 if (document.ondrag !== undefined)
117 document.ondrag = savedhandlers.ondrag;
118
119 // no more draggy-dee-drag
120 selection.active = false;
121 updateSelection(e);
122
123 if (selectionIsSane())
124 triggerSelectedEvent();
125 else {
126 // this counts as a clear
127 plot.getPlaceholder().trigger("plotunselected", [ ]);
128 plot.getPlaceholder().trigger("plotselecting", [ null ]);
129 }
130
131 return false;
132 }
133
134 function getSelection() {
135 if (!selectionIsSane())
136 return null;
137
138 var x1 = Math.min(selection.first.x, selection.second.x),
139 x2 = Math.max(selection.first.x, selection.second.x),
140 y1 = Math.max(selection.first.y, selection.second.y),
141 y2 = Math.min(selection.first.y, selection.second.y);
142
143 var r = {};
144 var axes = plot.getAxes();
145 if (axes.xaxis.used)
146 r.xaxis = { from: axes.xaxis.c2p(x1), to: axes.xaxis.c2p(x2) };
147 if (axes.x2axis.used)
148 r.x2axis = { from: axes.x2axis.c2p(x1), to: axes.x2axis.c2p(x2) };
149 if (axes.yaxis.used)
150 r.yaxis = { from: axes.yaxis.c2p(y1), to: axes.yaxis.c2p(y2) };
151 if (axes.y2axis.used)
152 r.y2axis = { from: axes.y2axis.c2p(y1), to: axes.y2axis.c2p(y2) };
153 return r;
154 }
155
156 function triggerSelectedEvent() {
157 var r = getSelection();
158
159 plot.getPlaceholder().trigger("plotselected", [ r ]);
160
161 // backwards-compat stuff, to be removed in future
162 var axes = plot.getAxes();
163 if (axes.xaxis.used && axes.yaxis.used)
164 plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]);
165 }
166
167 function clamp(min, value, max) {
168 return value < min? min: (value > max? max: value);
169 }
170
171 function setSelectionPos(pos, e) {
172 var o = plot.getOptions();
173 var offset = plot.getPlaceholder().offset();
174 var plotOffset = plot.getPlotOffset();
175 pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
176 pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height());
177
178 if (o.selection.mode == "y")
179 pos.x = pos == selection.first? 0: plot.width();
180
181 if (o.selection.mode == "x")
182 pos.y = pos == selection.first? 0: plot.height();
183 }
184
185 function updateSelection(pos) {
186 if (pos.pageX == null)
187 return;
188
189 setSelectionPos(selection.second, pos);
190 if (selectionIsSane()) {
191 selection.show = true;
192 plot.triggerRedrawOverlay();
193 }
194 else
195 clearSelection(true);
196 }
197
198 function clearSelection(preventEvent) {
199 if (selection.show) {
200 selection.show = false;
201 plot.triggerRedrawOverlay();
202 if (!preventEvent)
203 plot.getPlaceholder().trigger("plotunselected", [ ]);
204 }
205 }
206
207 function setSelection(ranges, preventEvent) {
208 var axis, range, axes = plot.getAxes();
209 var o = plot.getOptions();
210
211 if (o.selection.mode == "y") {
212 selection.first.x = 0;
213 selection.second.x = plot.width();
214 }
215 else {
216 axis = ranges["xaxis"]? axes["xaxis"]: (ranges["x2axis"]? axes["x2axis"]: axes["xaxis"]);
217 range = ranges["xaxis"] || ranges["x2axis"] || { from:ranges["x1"], to:ranges["x2"] }
218 selection.first.x = axis.p2c(Math.min(range.from, range.to));
219 selection.second.x = axis.p2c(Math.max(range.from, range.to));
220 }
221
222 if (o.selection.mode == "x") {
223 selection.first.y = 0;
224 selection.second.y = plot.height();
225 }
226 else {
227 axis = ranges["yaxis"]? axes["yaxis"]: (ranges["y2axis"]? axes["y2axis"]: axes["yaxis"]);
228 range = ranges["yaxis"] || ranges["y2axis"] || { from:ranges["y1"], to:ranges["y2"] }
229 selection.first.y = axis.p2c(Math.min(range.from, range.to));
230 selection.second.y = axis.p2c(Math.max(range.from, range.to));
231 }
232
233 selection.show = true;
234 plot.triggerRedrawOverlay();
235 if (!preventEvent)
236 triggerSelectedEvent();
237 }
238
239 function selectionIsSane() {
240 var minSize = 5;
241 return Math.abs(selection.second.x - selection.first.x) >= minSize &&
242 Math.abs(selection.second.y - selection.first.y) >= minSize;
243 }
244
245 plot.clearSelection = clearSelection;
246 plot.setSelection = setSelection;
247 plot.getSelection = getSelection;
248
249 plot.hooks.bindEvents.push(function(plot, eventHolder) {
250 var o = plot.getOptions();
251 if (o.selection.mode != null)
252 eventHolder.mousemove(onMouseMove);
253
254 if (o.selection.mode != null)
255 eventHolder.mousedown(onMouseDown);
256 });
257
258
259 plot.hooks.drawOverlay.push(function (plot, ctx) {
260 // draw selection
261 if (selection.show && selectionIsSane()) {
262 var plotOffset = plot.getPlotOffset();
263 var o = plot.getOptions();
264
265 ctx.save();
266 ctx.translate(plotOffset.left, plotOffset.top);
267
268 var c = $.color.parse(o.selection.color);
269
270 ctx.strokeStyle = c.scale('a', 0.8).toString();
271 ctx.lineWidth = 1;
272 ctx.lineJoin = "round";
273 ctx.fillStyle = c.scale('a', 0.4).toString();
274
275 var x = Math.min(selection.first.x, selection.second.x),
276 y = Math.min(selection.first.y, selection.second.y),
277 w = Math.abs(selection.second.x - selection.first.x),
278 h = Math.abs(selection.second.y - selection.first.y);
279
280 ctx.fillRect(x, y, w, h);
281 ctx.strokeRect(x, y, w, h);
282
283 ctx.restore();
284 }
285 });
286 }
287
288 $.plot.plugins.push({
289 init: init,
290 options: {
291 selection: {
292 mode: null, // one of null, "x", "y" or "xy"
293 color: "#e8cfac"
294 }
295 },
296 name: 'selection',
297 version: '1.0'
298 });
299})(jQuery);
0300
=== added file 'webapp/content/js/jquery.graphite.js'
--- webapp/content/js/jquery.graphite.js 1970-01-01 00:00:00 +0000
+++ webapp/content/js/jquery.graphite.js 2010-11-01 18:48:50 +0000
@@ -0,0 +1,383 @@
1(function( $ ) {
2 $.fn.editable_in_place = function(callback) {
3 var editable = $(this);
4 if (editable.length > 1) {
5 console.error("Call $().editable_in_place only on a singular jquery object.");
6 }
7
8 var editing = false;
9
10 editable.bind('click', function () {
11 var $element = this;
12
13 if (editing == true) return;
14
15 editing = true;
16
17 var $edit = $('<input type="text" class="edit_in_place" value="' + editable.text() + '"/>');
18
19 $edit.css({'height' : editable.height(), 'width' : editable.width()});
20 editable.hide();
21 editable.after($edit);
22 $edit.focus();
23
24 $edit.bind('blur', function() { // on blur, forget edits and reset.
25 $edit.remove();
26 editable.show();
27 editing = false;
28 });
29
30 $edit.keydown(function(e) {
31 if(e.which===27)$edit.blur(); // blur on Esc: see above
32 if(e.which===13 || e.which===9) { // Enter or Tab: run the callback with the value
33 e.preventDefault();
34 $edit.hide();
35 editable.show();
36 if($edit.val()!=='') {
37 editing = false;
38 callback($element, $edit.val());
39 }
40 $edit.remove();
41 }
42 });
43 });
44 };
45
46 $.fn.graphiteGraph = function() {
47 return this.each(function() {
48
49 var graph = $(this);
50 var plot = null;
51 var graph_lines = {};
52 var metric_yaxis = {};
53 var xaxisranges = {};
54 var yaxisranges = {};
55 var legends = null;
56 var updateLegendTimeout = null;
57 var latestPosition = null;
58 var autocompleteoptions = {
59 minChars: 0,
60 selectFirst: false,
61 };
62
63 var parse_incoming = function(incoming_data) {
64 var result = [];
65 var start = incoming_data.start;
66 var end = incoming_data.end;
67 var step = incoming_data.step;
68
69 for (i in incoming_data.data) {
70 result.push([(start+step*i)*1000, incoming_data.data[i]]);
71
72 }
73 return {
74 label: incoming_data.name,
75 data: result,
76 lines: {show: true, fill: false}
77 };
78 };
79
80
81 var render = function () {
82 var lines = []
83 for (i in graph_lines) {
84 for (j in graph_lines[i]) {
85 var newline = $.extend({}, graph_lines[i][j]);
86 if (metric_yaxis[i] == "two") {
87 newline['yaxis'] = 2;
88 }
89 lines.push(newline);
90 }
91 }
92 var xaxismode = { mode: "time" };
93 var yaxismode = { };
94
95 $.extend(xaxismode, xaxisranges);
96 $.extend(yaxismode, yaxisranges);
97
98 plot = $.plot($("#graph"),
99 lines,
100 {
101 xaxis: xaxismode,
102 yaxis: yaxismode,
103 grid: { hoverable: true, },
104 selection: { mode: "xy" },
105 legend: { show: true, container: graph.find("#legend") },
106 crosshair: { mode: "x" },
107 }
108 );
109
110
111 for (i in lines) {
112 lines[i] = $.extend({}, lines[i]);
113 lines[i].label = null;
114 }
115 var overview = $.plot($("#overview"),
116 lines,
117 {
118 xaxis: { mode: "time" },
119 selection: { mode: "x" },
120 }
121 );
122
123 // legends magic
124 legends = graph.find(".legendLabel");
125 // update link
126 graph.find("#graphurl").attr("href", build_full_url());
127
128 }
129
130 function updateLegend() {
131 updateLegendTimeout = null;
132
133 var pos = latestPosition;
134
135 var axes = plot.getAxes();
136 if (pos.x < axes.xaxis.min || pos.x > axes.xaxis.max ||
137 pos.y < axes.yaxis.min || pos.y > axes.yaxis.max) {
138 var i, j, dataset = plot.getData();
139 for (i = 0; i < dataset.length; ++i) {
140 var series = dataset[i];
141 legends.eq(i).text(series.label);
142 }
143 }
144
145 var i, j, dataset = plot.getData();
146 for (i = 0; i < dataset.length; ++i) {
147 var series = dataset[i];
148
149 // find the nearest points, x-wise
150 for (j = 0; j < series.data.length; ++j)
151 if (series.data[j][0] > pos.x)
152 break;
153
154 // now interpolate
155 var y, p1 = series.data[j - 1], p2 = series.data[j];
156 if (p1 == null)
157 y = p2[1];
158 else if (p2 == null)
159 y = p1[1];
160 else
161 y = p1[1] + (p2[1] - p1[1]) * (pos.x - p1[0]) / (p2[0] - p1[0]);