• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
Keine Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

allura


Commit MetaInfo

Revisionbd5aaa6f622f2040c7896f53868455b3ace1ac9c (tree)
Zeit2012-02-22 03:26:00
AutorTim Van Steenburgh <tvansteenburgh@geek...>
CommiterTim Van Steenburgh

Log Message

Merge branch 'dev' of git://sfi-engr-scm-1/forge into dev

Ändern Zusammenfassung

Diff

--- /dev/null
+++ b/Allura/allura/lib/zarkov_helpers.py
@@ -0,0 +1,77 @@
1+import calendar
2+from datetime import datetime, timedelta
3+
4+def zero_fill_zarkov_result(zarkov_data, period, start_date, end_date):
5+ """Return a new copy of zarkov_data (a dict returned from a zarkov
6+ query) with the timeseries data zero-filled for missing dates.
7+
8+ Args:
9+ zarkov_data (dict): A Zarkov query result.
10+ period (str): 'month' or 'date' for monthly or daily timestamps
11+ start_date (datetime or str): Start of the date range. If a str is
12+ passed, it must be in %Y-%m-%d format.
13+ end_date (datetime or str): End of the date range. If a str is
14+ passed, it must be in %Y-%m-%d format.
15+
16+ Returns:
17+ dict. A new copy of zarkov_data, zero-filled.
18+
19+ """
20+ d = zarkov_data.copy()
21+ if isinstance(start_date, basestring):
22+ start_date = datetime.strptime(start_date, '%Y-%m-%d')
23+ if isinstance(end_date, basestring):
24+ end_date = datetime.strptime(end_date, '%Y-%m-%d')
25+ for query in zarkov_data.iterkeys():
26+ for series in zarkov_data[query].iterkeys():
27+ d[query][series] = zero_fill_time_series(d[query][series],
28+ period, start_date, end_date)
29+ return d
30+
31+def zero_fill_time_series(time_series, period, start_date, end_date):
32+ """Return a copy of time_series after adding [timestamp, 0] pairs for
33+ each missing timestamp in the given date range.
34+
35+ Args:
36+ time_series (list): A list of [timestamp, value] pairs, e.g.:
37+ [[1306886400000.0, 1], [1309478400000.0, 0]]
38+ period (str): 'month' or 'date' for monthly or daily timestamps
39+ start_date (datetime or str): Start of the date range. If a str is
40+ passed, it must be in %Y-%m-%d format.
41+ end_date (datetime or str): End of the date range. If a str is
42+ passed, it must be in %Y-%m-%d format.
43+
44+ Returns:
45+ list. A new copy of time_series, zero-filled.
46+
47+ If you want to zero-fill an entire zarkov result, you should use
48+ :func:`zero_fill_zarkov_result` instead, which will call this function
49+ for each timeseries list in the zarkov result.
50+
51+ """
52+ new_series = dict(time_series)
53+ if period == 'month':
54+ date = start_date
55+ while date <= end_date:
56+ ts = to_utc_timestamp(date)
57+ if ts not in new_series:
58+ new_series[ts] = 0
59+ # next month
60+ if date.month == 12:
61+ date = date.replace(year=date.year+1, month=1)
62+ else:
63+ date = date.replace(month=date.month+1)
64+ else: # daily
65+ days = (end_date - start_date).days + 1
66+ periods = range(0, days)
67+ for dayoffset in periods:
68+ date = start_date + timedelta(days=dayoffset)
69+ ts = to_utc_timestamp(date)
70+ if ts not in new_series:
71+ new_series[ts] = 0
72+ return sorted([[k, v] for k, v in new_series.items()])
73+
74+def to_utc_timestamp(d):
75+ """Return UTC unix timestamp representation of d (datetime)."""
76+ # http://stackoverflow.com/questions/1077285/how-to-specify-time-zone-utc-when-converting-to-unix-time-python
77+ return calendar.timegm(d.utctimetuple()) * 1000.0
--- a/Allura/allura/model/project.py
+++ b/Allura/allura/model/project.py
@@ -643,9 +643,18 @@ class AppConfig(MappedClass):
643643 options=FieldProperty(None)
644644 project = RelationProperty(Project, via='project_id')
645645 discussion = RelationProperty('Discussion', via='discussion_id')
646+ tool_data = FieldProperty({str:{str:None}}) # entry point: prefs dict
646647
647648 acl = FieldProperty(ACL())
648649
650+ def get_tool_data(self, tool, key, default=None):
651+ return self.tool_data.get(tool, {}).get(key, default)
652+
653+ def set_tool_data(self, tool, **kw):
654+ d = self.tool_data.setdefault(tool, {})
655+ d.update(kw)
656+ state(self).soil()
657+
649658 def parent_security_context(self):
650659 '''ACL processing should terminate at the AppConfig'''
651660 return None
--- a/Allura/allura/public/nf/js/allura-base.js
+++ b/Allura/allura/public/nf/js/allura-base.js
@@ -145,6 +145,11 @@ function attach_form_retry( form ){
145145 });
146146 }
147147
148+function addCommas(num) {
149+ // http://stackoverflow.com/questions/1990512/add-comma-to-numbers-every-three-digits-using-jquery/1990590#1990590
150+ return String(num).replace(new RegExp('(\\d)(?=(\\d\\d\\d)+(?!\\d))', 'g'), "$1,");
151+}
152+
148153 $(function(){
149154 // Add notifications for form submission.
150155 attach_form_retry('form.can-retry');
--- /dev/null
+++ b/Allura/allura/public/nf/js/jquery.daterangepicker.js
@@ -0,0 +1,715 @@
1+/**
2+ * --------------------------------------------------------------------
3+ * jQuery-Plugin "daterangepicker.jQuery.js"
4+ * by Scott Jehl, scott@filamentgroup.com
5+ * http://www.filamentgroup.com
6+ * reference article: http://www.filamentgroup.com/lab/update_date_range_picker_with_jquery_ui/
7+ * demo page: http://www.filamentgroup.com/examples/daterangepicker/
8+ *
9+ * Copyright (c) 2008 Filament Group, Inc
10+ * Dual licensed under the MIT (filamentgroup.com/examples/mit-license.txt) and GPL (filamentgroup.com/examples/gpl-license.txt) licenses.
11+ *
12+ * Dependencies: jquery, jquery UI datepicker, date.js library (included at bottom), jQuery UI CSS Framework
13+ * Changelog:
14+ * 10.23.2008 initial Version
15+ * 11.12.2008 changed dateFormat option to allow custom date formatting (credit: http://alexgoldstone.com/)
16+ * 01.04.09 updated markup to new jQuery UI CSS Framework
17+ * 01.19.2008 changed presets hash to support different text
18+ * --------------------------------------------------------------------
19+ */
20+jQuery.fn.daterangepicker = function(settings){
21+ var rangeInput = jQuery(this);
22+
23+ //defaults
24+ var options = jQuery.extend({
25+ presetRanges: [
26+ {text: 'Today', dateStart: 'today', dateEnd: 'today' },
27+ {text: 'Last 7 days', dateStart: 'today-6days', dateEnd: 'today' },
28+ {text: 'Month to date', dateStart: function(){ return Date.parse('today').moveToFirstDayOfMonth(); }, dateEnd: 'today' },
29+ {text: 'Year to date', dateStart: function(){ var x= Date.parse('today'); x.setMonth(0); x.setDate(1); return x; }, dateEnd: 'today' },
30+ //extras:
31+ {text: 'The previous Month', dateStart: function(){ return Date.parse('1 month ago').moveToFirstDayOfMonth(); }, dateEnd: function(){ return Date.parse('1 month ago').moveToLastDayOfMonth(); } }
32+ //{text: 'Tomorrow', dateStart: 'Tomorrow', dateEnd: 'Tomorrow' },
33+ //{text: 'Ad Campaign', dateStart: '03/07/08', dateEnd: 'Today' },
34+ //{text: 'Last 30 Days', dateStart: 'Today-30', dateEnd: 'Today' },
35+ //{text: 'Next 30 Days', dateStart: 'Today', dateEnd: 'Today+30' },
36+ //{text: 'Our Ad Campaign', dateStart: '03/07/08', dateEnd: '07/08/08' }
37+ ],
38+ //presetRanges: array of objects for each menu preset.
39+ //Each obj must have text, dateStart, dateEnd. dateStart, dateEnd accept date.js string or a function which returns a date object
40+ presets: {
41+ specificDate: 'Specific Date',
42+ allDatesBefore: 'All Dates Before',
43+ allDatesAfter: 'All Dates After',
44+ dateRange: 'Date Range'
45+ },
46+ rangeStartTitle: 'Start date',
47+ rangeEndTitle: 'End date',
48+ nextLinkText: 'Next',
49+ prevLinkText: 'Prev',
50+ doneButtonText: 'Done',
51+ earliestDate: Date.parse('-15years'), //earliest date allowed
52+ latestDate: Date.parse('+15years'), //latest date allowed
53+ rangeSplitter: '-', //string to use between dates in single input
54+ dateFormat: 'm/d/yy', // date formatting. Available formats: http://docs.jquery.com/UI/Datepicker/%24.datepicker.formatDate
55+ closeOnSelect: true, //if a complete selection is made, close the menu
56+ arrows: false,
57+ posX: rangeInput.offset().left, // x position
58+ posY: rangeInput.offset().top + rangeInput.outerHeight(), // y position
59+ appendTo: 'body',
60+ onClose: function(){},
61+ onOpen: function(){},
62+ onChange: function(){},
63+ datepickerOptions: null //object containing native UI datepicker API options
64+ }, settings);
65+
66+
67+ //custom datepicker options, extended by options
68+ var datepickerOptions = {
69+ onSelect: function() {
70+ if(rp.find('.ui-daterangepicker-specificDate').is('.ui-state-active')){
71+ rp.find('.range-end').datepicker('setDate', rp.find('.range-start').datepicker('getDate') );
72+ }
73+ var rangeA = fDate( rp.find('.range-start').datepicker('getDate') );
74+ var rangeB = fDate( rp.find('.range-end').datepicker('getDate') );
75+
76+ //send back to input or inputs
77+ if(rangeInput.length == 2){
78+ rangeInput.eq(0).val(rangeA);
79+ rangeInput.eq(1).val(rangeB);
80+ }
81+ else{
82+ rangeInput.val((rangeA != rangeB) ? rangeA+' '+ options.rangeSplitter +' '+rangeB : rangeA);
83+ }
84+ //if closeOnSelect is true
85+ if(options.closeOnSelect){
86+ if(!rp.find('li.ui-state-active').is('.ui-daterangepicker-dateRange') && !rp.is(':animated') ){
87+ hideRP();
88+ }
89+ }
90+ options.onChange();
91+ },
92+ defaultDate: +0
93+ };
94+
95+ //change event fires both when a calendar is updated or a change event on the input is triggered
96+ rangeInput.change(options.onChange);
97+
98+
99+ //datepicker options from options
100+ options.datepickerOptions = (settings) ? jQuery.extend(datepickerOptions, settings.datepickerOptions) : datepickerOptions;
101+
102+ //Capture Dates from input(s)
103+ var inputDateA, inputDateB = Date.parse('today');
104+ var inputDateAtemp, inputDateBtemp;
105+ if(rangeInput.size() == 2){
106+ inputDateAtemp = Date.parse( rangeInput.eq(0).val() );
107+ inputDateBtemp = Date.parse( rangeInput.eq(1).val() );
108+ if(inputDateAtemp == null){inputDateAtemp = inputDateBtemp;}
109+ if(inputDateBtemp == null){inputDateBtemp = inputDateAtemp;}
110+ }
111+ else {
112+ inputDateAtemp = Date.parse( rangeInput.val().split(options.rangeSplitter)[0] );
113+ inputDateBtemp = Date.parse( rangeInput.val().split(options.rangeSplitter)[1] );
114+ if(inputDateBtemp == null){inputDateBtemp = inputDateAtemp;} //if one date, set both
115+ }
116+ if(inputDateAtemp != null){inputDateA = inputDateAtemp;}
117+ if(inputDateBtemp != null){inputDateB = inputDateBtemp;}
118+
119+
120+ //build picker and
121+ var rp = jQuery('<div class="ui-daterangepicker ui-widget ui-helper-clearfix ui-widget-content ui-corner-all"></div>');
122+ var rpPresets = (function(){
123+ var ul = jQuery('<ul class="ui-widget-content"></ul>').appendTo(rp);
124+ jQuery.each(options.presetRanges,function(){
125+ jQuery('<li class="ui-daterangepicker-'+ this.text.replace(/ /g, '') +' ui-corner-all"><a href="#">'+ this.text +'</a></li>')
126+ .data('dateStart', this.dateStart)
127+ .data('dateEnd', this.dateEnd)
128+ .appendTo(ul);
129+ });
130+ var x=0;
131+ jQuery.each(options.presets, function(key, value) {
132+ jQuery('<li class="ui-daterangepicker-'+ key +' preset_'+ x +' ui-helper-clearfix ui-corner-all"><span class="ui-icon ui-icon-triangle-1-e"></span><a href="#">'+ value +'</a></li>')
133+ .appendTo(ul);
134+ x++;
135+ });
136+
137+ ul.find('li').hover(
138+ function(){
139+ jQuery(this).addClass('ui-state-hover');
140+ },
141+ function(){
142+ jQuery(this).removeClass('ui-state-hover');
143+ })
144+ .click(function(){
145+ rp.find('.ui-state-active').removeClass('ui-state-active');
146+ jQuery(this).addClass('ui-state-active').clickActions(rp, rpPickers, doneBtn);
147+ return false;
148+ });
149+ return ul;
150+ })();
151+
152+ //function to format a date string
153+ function fDate(date){
154+ if(!date.getDate()){return '';}
155+ var day = date.getDate();
156+ var month = date.getMonth();
157+ var year = date.getFullYear();
158+ month++; // adjust javascript month
159+ var dateFormat = options.dateFormat;
160+ return jQuery.datepicker.formatDate( dateFormat, date );
161+ }
162+
163+
164+ jQuery.fn.restoreDateFromData = function(){
165+ if(jQuery(this).data('saveDate')){
166+ jQuery(this).datepicker('setDate', jQuery(this).data('saveDate')).removeData('saveDate');
167+ }
168+ return this;
169+ }
170+ jQuery.fn.saveDateToData = function(){
171+ if(!jQuery(this).data('saveDate')){
172+ jQuery(this).data('saveDate', jQuery(this).datepicker('getDate') );
173+ }
174+ return this;
175+ }
176+
177+ //show, hide, or toggle rangepicker
178+ function showRP(){
179+ if(rp.data('state') == 'closed'){
180+ rp.data('state', 'open');
181+ rp.fadeIn(300);
182+ options.onOpen();
183+ }
184+ }
185+ function hideRP(){
186+ if(rp.data('state') == 'open'){
187+ rp.data('state', 'closed');
188+ rp.fadeOut(300);
189+ options.onClose();
190+ }
191+ }
192+ function toggleRP(){
193+ if( rp.data('state') == 'open' ){ hideRP(); }
194+ else { showRP(); }
195+ }
196+ rp.data('state', 'closed');
197+
198+ //preset menu click events
199+ jQuery.fn.clickActions = function(rp, rpPickers, doneBtn){
200+
201+ if(jQuery(this).is('.ui-daterangepicker-specificDate')){
202+ doneBtn.hide();
203+ rpPickers.show();
204+ rp.find('.title-start').text( options.presets.specificDate );
205+ rp.find('.range-start').restoreDateFromData().show(400);
206+ rp.find('.range-end').restoreDateFromData().hide(400);
207+ setTimeout(function(){doneBtn.fadeIn();}, 400);
208+ }
209+ else if(jQuery(this).is('.ui-daterangepicker-allDatesBefore')){
210+ doneBtn.hide();
211+ rpPickers.show();
212+ rp.find('.title-end').text( options.presets.allDatesBefore );
213+ rp.find('.range-start').saveDateToData().datepicker('setDate', options.earliestDate).hide(400);
214+ rp.find('.range-end').restoreDateFromData().show(400);
215+ setTimeout(function(){doneBtn.fadeIn();}, 400);
216+ }
217+ else if(jQuery(this).is('.ui-daterangepicker-allDatesAfter')){
218+ doneBtn.hide();
219+ rpPickers.show();
220+ rp.find('.title-start').text( options.presets.allDatesAfter );
221+ rp.find('.range-start').restoreDateFromData().show(400);
222+ rp.find('.range-end').saveDateToData().datepicker('setDate', options.latestDate).hide(400);
223+ setTimeout(function(){doneBtn.fadeIn();}, 400);
224+ }
225+ else if(jQuery(this).is('.ui-daterangepicker-dateRange')){
226+ doneBtn.hide();
227+ rpPickers.show();
228+ rp.find('.title-start').text(options.rangeStartTitle);
229+ rp.find('.title-end').text(options.rangeEndTitle);
230+ rp.find('.range-start').restoreDateFromData().show(400);
231+ rp.find('.range-end').restoreDateFromData().show(400);
232+ setTimeout(function(){doneBtn.fadeIn();}, 400);
233+ }
234+ else {
235+ //custom date range
236+ doneBtn.hide();
237+ rp.find('.range-start, .range-end').hide(400, function(){
238+ rpPickers.hide();
239+ });
240+ var dateStart = (typeof jQuery(this).data('dateStart') == 'string') ? Date.parse(jQuery(this).data('dateStart')) : jQuery(this).data('dateStart')();
241+ var dateEnd = (typeof jQuery(this).data('dateEnd') == 'string') ? Date.parse(jQuery(this).data('dateEnd')) : jQuery(this).data('dateEnd')();
242+ rp.find('.range-start').datepicker('setDate', dateStart).find('.ui-datepicker-current-day').trigger('click');
243+ rp.find('.range-end').datepicker('setDate', dateEnd).find('.ui-datepicker-current-day').trigger('click');
244+ }
245+
246+ return false;
247+ }
248+
249+
250+ //picker divs
251+ var rpPickers = jQuery('<div class="ranges ui-widget-header ui-corner-all ui-helper-clearfix"><div class="range-start"><span class="title-start">Start Date</span></div><div class="range-end"><span class="title-end">End Date</span></div></div>').appendTo(rp);
252+ rpPickers.find('.range-start, .range-end').datepicker(options.datepickerOptions);
253+ rpPickers.find('.range-start').datepicker('setDate', inputDateA);
254+ rpPickers.find('.range-end').datepicker('setDate', inputDateB);
255+ var doneBtn = jQuery('<button class="btnDone ui-state-default ui-corner-all">'+ options.doneButtonText +'</button>')
256+ .click(function(){
257+ rp.find('.ui-datepicker-current-day').trigger('click');
258+ hideRP();
259+ })
260+ .hover(
261+ function(){
262+ jQuery(this).addClass('ui-state-hover');
263+ },
264+ function(){
265+ jQuery(this).removeClass('ui-state-hover');
266+ }
267+ )
268+ .appendTo(rpPickers);
269+
270+
271+
272+
273+ //inputs toggle rangepicker visibility
274+ jQuery(this).click(function(){
275+ toggleRP();
276+ return false;
277+ });
278+ //hide em all
279+ rpPickers.css('display', 'none').find('.range-start, .range-end, .btnDone').css('display', 'none');
280+
281+ //inject rp
282+ jQuery(options.appendTo).append(rp);
283+
284+ //wrap and position
285+ rp.wrap('<div class="ui-daterangepickercontain"></div>');
286+ if(options.posX){
287+ rp.parent().css('left', options.posX);
288+ }
289+ if(options.posY){
290+ rp.parent().css('top', options.posY);
291+ }
292+
293+ //add arrows (only available on one input)
294+ if(options.arrows && rangeInput.size()==1){
295+ var prevLink = jQuery('<a href="#" class="ui-daterangepicker-prev ui-corner-all" title="'+ options.prevLinkText +'"><span class="ui-icon ui-icon-circle-triangle-w">'+ options.prevLinkText +'</span></a>');
296+ var nextLink = jQuery('<a href="#" class="ui-daterangepicker-next ui-corner-all" title="'+ options.nextLinkText +'"><span class="ui-icon ui-icon-circle-triangle-e">'+ options.nextLinkText +'</span></a>');
297+ jQuery(this)
298+ .addClass('ui-rangepicker-input ui-widget-content')
299+ .wrap('<div class="ui-daterangepicker-arrows ui-widget ui-widget-header ui-helper-clearfix ui-corner-all"></div>')
300+ .before( prevLink )
301+ .before( nextLink )
302+ .parent().find('a').click(function(){
303+ var dateA = rpPickers.find('.range-start').datepicker('getDate');
304+ var dateB = rpPickers.find('.range-end').datepicker('getDate');
305+ var diff = Math.abs( new TimeSpan(dateA - dateB).getTotalMilliseconds() ) + 86400000; //difference plus one day
306+ if(jQuery(this).is('.ui-daterangepicker-prev')){ diff = -diff; }
307+
308+ rpPickers.find('.range-start, .range-end ').each(function(){
309+ var thisDate = jQuery(this).datepicker( "getDate");
310+ if(thisDate == null){return false;}
311+ jQuery(this).datepicker( "setDate", thisDate.add({milliseconds: diff}) ).find('.ui-datepicker-current-day').trigger('click');
312+ });
313+
314+ return false;
315+ })
316+ .hover(
317+ function(){
318+ jQuery(this).addClass('ui-state-hover');
319+ },
320+ function(){
321+ jQuery(this).removeClass('ui-state-hover');
322+ })
323+ ;
324+ }
325+
326+
327+ jQuery(document).click(function(){
328+ if (rp.is(':visible')) {
329+ hideRP();
330+ }
331+ });
332+
333+ rp.click(function(){return false;}).hide();
334+ return this;
335+}
336+
337+
338+
339+
340+
341+/**
342+ * Version: 1.0 Alpha-1
343+ * Build Date: 13-Nov-2007
344+ * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved.
345+ * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/.
346+ * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/
347+ */
348+Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|after|from)/i,subtract:/^(\-|before|ago)/i,yesterday:/^yesterday/i,today:/^t(oday)?/i,tomorrow:/^tomorrow/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^min(ute)?s?/i,hour:/^h(ou)?rs?/i,week:/^w(ee)?k/i,month:/^m(o(nth)?s?)?/i,day:/^d(ays?)?/i,year:/^y((ea)?rs?)?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a|p)/i},abbreviatedTimeZoneStandard:{GMT:"-000",EST:"-0400",CST:"-0500",MST:"-0600",PST:"-0700"},abbreviatedTimeZoneDST:{GMT:"-000",EDT:"-0500",CDT:"-0600",MDT:"-0700",PDT:"-0800"}};
349+Date.getMonthNumberFromName=function(name){var n=Date.CultureInfo.monthNames,m=Date.CultureInfo.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
350+return-1;};Date.getDayNumberFromName=function(name){var n=Date.CultureInfo.dayNames,m=Date.CultureInfo.abbreviatedDayNames,o=Date.CultureInfo.shortestDayNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
351+return-1;};Date.isLeapYear=function(year){return(((year%4===0)&&(year%100!==0))||(year%400===0));};Date.getDaysInMonth=function(year,month){return[31,(Date.isLeapYear(year)?29:28),31,30,31,30,31,31,30,31,30,31][month];};Date.getTimezoneOffset=function(s,dst){return(dst||false)?Date.CultureInfo.abbreviatedTimeZoneDST[s.toUpperCase()]:Date.CultureInfo.abbreviatedTimeZoneStandard[s.toUpperCase()];};Date.getTimezoneAbbreviation=function(offset,dst){var n=(dst||false)?Date.CultureInfo.abbreviatedTimeZoneDST:Date.CultureInfo.abbreviatedTimeZoneStandard,p;for(p in n){if(n[p]===offset){return p;}}
352+return null;};Date.prototype.clone=function(){return new Date(this.getTime());};Date.prototype.compareTo=function(date){if(isNaN(this)){throw new Error(this);}
353+if(date instanceof Date&&!isNaN(date)){return(this>date)?1:(this<date)?-1:0;}else{throw new TypeError(date);}};Date.prototype.equals=function(date){return(this.compareTo(date)===0);};Date.prototype.between=function(start,end){var t=this.getTime();return t>=start.getTime()&&t<=end.getTime();};Date.prototype.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};Date.prototype.addSeconds=function(value){return this.addMilliseconds(value*1000);};Date.prototype.addMinutes=function(value){return this.addMilliseconds(value*60000);};Date.prototype.addHours=function(value){return this.addMilliseconds(value*3600000);};Date.prototype.addDays=function(value){return this.addMilliseconds(value*86400000);};Date.prototype.addWeeks=function(value){return this.addMilliseconds(value*604800000);};Date.prototype.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,this.getDaysInMonth()));return this;};Date.prototype.addYears=function(value){return this.addMonths(value*12);};Date.prototype.add=function(config){if(typeof config=="number"){this._orient=config;return this;}
354+var x=config;if(x.millisecond||x.milliseconds){this.addMilliseconds(x.millisecond||x.milliseconds);}
355+if(x.second||x.seconds){this.addSeconds(x.second||x.seconds);}
356+if(x.minute||x.minutes){this.addMinutes(x.minute||x.minutes);}
357+if(x.hour||x.hours){this.addHours(x.hour||x.hours);}
358+if(x.month||x.months){this.addMonths(x.month||x.months);}
359+if(x.year||x.years){this.addYears(x.year||x.years);}
360+if(x.day||x.days){this.addDays(x.day||x.days);}
361+return this;};Date._validate=function(value,min,max,name){if(typeof value!="number"){throw new TypeError(value+" is not a Number.");}else if(value<min||value>max){throw new RangeError(value+" is not a valid value for "+name+".");}
362+return true;};Date.validateMillisecond=function(n){return Date._validate(n,0,999,"milliseconds");};Date.validateSecond=function(n){return Date._validate(n,0,59,"seconds");};Date.validateMinute=function(n){return Date._validate(n,0,59,"minutes");};Date.validateHour=function(n){return Date._validate(n,0,23,"hours");};Date.validateDay=function(n,year,month){return Date._validate(n,1,Date.getDaysInMonth(year,month),"days");};Date.validateMonth=function(n){return Date._validate(n,0,11,"months");};Date.validateYear=function(n){return Date._validate(n,1,9999,"seconds");};Date.prototype.set=function(config){var x=config;if(!x.millisecond&&x.millisecond!==0){x.millisecond=-1;}
363+if(!x.second&&x.second!==0){x.second=-1;}
364+if(!x.minute&&x.minute!==0){x.minute=-1;}
365+if(!x.hour&&x.hour!==0){x.hour=-1;}
366+if(!x.day&&x.day!==0){x.day=-1;}
367+if(!x.month&&x.month!==0){x.month=-1;}
368+if(!x.year&&x.year!==0){x.year=-1;}
369+if(x.millisecond!=-1&&Date.validateMillisecond(x.millisecond)){this.addMilliseconds(x.millisecond-this.getMilliseconds());}
370+if(x.second!=-1&&Date.validateSecond(x.second)){this.addSeconds(x.second-this.getSeconds());}
371+if(x.minute!=-1&&Date.validateMinute(x.minute)){this.addMinutes(x.minute-this.getMinutes());}
372+if(x.hour!=-1&&Date.validateHour(x.hour)){this.addHours(x.hour-this.getHours());}
373+if(x.month!==-1&&Date.validateMonth(x.month)){this.addMonths(x.month-this.getMonth());}
374+if(x.year!=-1&&Date.validateYear(x.year)){this.addYears(x.year-this.getFullYear());}
375+if(x.day!=-1&&Date.validateDay(x.day,this.getFullYear(),this.getMonth())){this.addDays(x.day-this.getDate());}
376+if(x.timezone){this.setTimezone(x.timezone);}
377+if(x.timezoneOffset){this.setTimezoneOffset(x.timezoneOffset);}
378+return this;};Date.prototype.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};Date.prototype.isLeapYear=function(){var y=this.getFullYear();return(((y%4===0)&&(y%100!==0))||(y%400===0));};Date.prototype.isWeekday=function(){return!(this.is().sat()||this.is().sun());};Date.prototype.getDaysInMonth=function(){return Date.getDaysInMonth(this.getFullYear(),this.getMonth());};Date.prototype.moveToFirstDayOfMonth=function(){return this.set({day:1});};Date.prototype.moveToLastDayOfMonth=function(){return this.set({day:this.getDaysInMonth()});};Date.prototype.moveToDayOfWeek=function(day,orient){var diff=(day-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};Date.prototype.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};Date.prototype.getDayOfYear=function(){return Math.floor((this-new Date(this.getFullYear(),0,1))/86400000);};Date.prototype.getWeekOfYear=function(firstDayOfWeek){var y=this.getFullYear(),m=this.getMonth(),d=this.getDate();var dow=firstDayOfWeek||Date.CultureInfo.firstDayOfWeek;var offset=7+1-new Date(y,0,1).getDay();if(offset==8){offset=1;}
379+var daynum=((Date.UTC(y,m,d,0,0,0)-Date.UTC(y,0,1,0,0,0))/86400000)+1;var w=Math.floor((daynum-offset+7)/7);if(w===dow){y--;var prevOffset=7+1-new Date(y,0,1).getDay();if(prevOffset==2||prevOffset==8){w=53;}else{w=52;}}
380+return w;};Date.prototype.isDST=function(){return this.toString().match(/(E|C|M|P)(S|D)T/)[2]=="D";};Date.prototype.getTimezone=function(){return Date.getTimezoneAbbreviation(this.getUTCOffset,this.isDST());};Date.prototype.setTimezoneOffset=function(s){var here=this.getTimezoneOffset(),there=Number(s)*-6/10;this.addMinutes(there-here);return this;};Date.prototype.setTimezone=function(s){return this.setTimezoneOffset(Date.getTimezoneOffset(s));};Date.prototype.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r[0]+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};Date.prototype.getDayName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedDayNames[this.getDay()]:Date.CultureInfo.dayNames[this.getDay()];};Date.prototype.getMonthName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedMonthNames[this.getMonth()]:Date.CultureInfo.monthNames[this.getMonth()];};Date.prototype._toString=Date.prototype.toString;Date.prototype.toString=function(format){var self=this;var p=function p(s){return(s.toString().length==1)?"0"+s:s;};return format?format.replace(/dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?/g,function(format){switch(format){case"hh":return p(self.getHours()<13?self.getHours():(self.getHours()-12));case"h":return self.getHours()<13?self.getHours():(self.getHours()-12);case"HH":return p(self.getHours());case"H":return self.getHours();case"mm":return p(self.getMinutes());case"m":return self.getMinutes();case"ss":return p(self.getSeconds());case"s":return self.getSeconds();case"yyyy":return self.getFullYear();case"yy":return self.getFullYear().toString().substring(2,4);case"dddd":return self.getDayName();case"ddd":return self.getDayName(true);case"dd":return p(self.getDate());case"d":return self.getDate().toString();case"MMMM":return self.getMonthName();case"MMM":return self.getMonthName(true);case"MM":return p((self.getMonth()+1));case"M":return self.getMonth()+1;case"t":return self.getHours()<12?Date.CultureInfo.amDesignator.substring(0,1):Date.CultureInfo.pmDesignator.substring(0,1);case"tt":return self.getHours()<12?Date.CultureInfo.amDesignator:Date.CultureInfo.pmDesignator;case"zzz":case"zz":case"z":return"";}}):this._toString();};
381+Date.now=function(){return new Date();};Date.today=function(){return Date.now().clearTime();};Date.prototype._orient=+1;Date.prototype.next=function(){this._orient=+1;return this;};Date.prototype.last=Date.prototype.prev=Date.prototype.previous=function(){this._orient=-1;return this;};Date.prototype._is=false;Date.prototype.is=function(){this._is=true;return this;};Number.prototype._dateElement="day";Number.prototype.fromNow=function(){var c={};c[this._dateElement]=this;return Date.now().add(c);};Number.prototype.ago=function(){var c={};c[this._dateElement]=this*-1;return Date.now().add(c);};(function(){var $D=Date.prototype,$N=Number.prototype;var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),de;var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;}
382+return this.moveToDayOfWeek(n,this._orient);};};for(var i=0;i<dx.length;i++){$D[dx[i]]=$D[dx[i].substring(0,3)]=df(i);}
383+var mf=function(n){return function(){if(this._is){this._is=false;return this.getMonth()===n;}
384+return this.moveToMonth(n,this._orient);};};for(var j=0;j<mx.length;j++){$D[mx[j]]=$D[mx[j].substring(0,3)]=mf(j);}
385+var ef=function(j){return function(){if(j.substring(j.length-1)!="s"){j+="s";}
386+return this["add"+j](this._orient);};};var nf=function(n){return function(){this._dateElement=n;return this;};};for(var k=0;k<px.length;k++){de=px[k].toLowerCase();$D[de]=$D[de+"s"]=ef(px[k]);$N[de]=$N[de+"s"]=nf(de);}}());Date.prototype.toJSONString=function(){return this.toString("yyyy-MM-ddThh:mm:ssZ");};Date.prototype.toShortDateString=function(){return this.toString(Date.CultureInfo.formatPatterns.shortDatePattern);};Date.prototype.toLongDateString=function(){return this.toString(Date.CultureInfo.formatPatterns.longDatePattern);};Date.prototype.toShortTimeString=function(){return this.toString(Date.CultureInfo.formatPatterns.shortTimePattern);};Date.prototype.toLongTimeString=function(){return this.toString(Date.CultureInfo.formatPatterns.longTimePattern);};Date.prototype.getOrdinal=function(){switch(this.getDate()){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};
387+(function(){Date.Parsing={Exception:function(s){this.message="Parse error at '"+s.substring(0,10)+" ...'";}};var $P=Date.Parsing;var _=$P.Operators={rtoken:function(r){return function(s){var mx=s.match(r);if(mx){return([mx[0],s.substring(mx[0].length)]);}else{throw new $P.Exception(s);}};},token:function(s){return function(s){return _.rtoken(new RegExp("^\s*"+s+"\s*"))(s);};},stoken:function(s){return _.rtoken(new RegExp("^"+s));},until:function(p){return function(s){var qx=[],rx=null;while(s.length){try{rx=p.call(this,s);}catch(e){qx.push(rx[0]);s=rx[1];continue;}
388+break;}
389+return[qx,s];};},many:function(p){return function(s){var rx=[],r=null;while(s.length){try{r=p.call(this,s);}catch(e){return[rx,s];}
390+rx.push(r[0]);s=r[1];}
391+return[rx,s];};},optional:function(p){return function(s){var r=null;try{r=p.call(this,s);}catch(e){return[null,s];}
392+return[r[0],r[1]];};},not:function(p){return function(s){try{p.call(this,s);}catch(e){return[null,s];}
393+throw new $P.Exception(s);};},ignore:function(p){return p?function(s){var r=null;r=p.call(this,s);return[null,r[1]];}:null;},product:function(){var px=arguments[0],qx=Array.prototype.slice.call(arguments,1),rx=[];for(var i=0;i<px.length;i++){rx.push(_.each(px[i],qx));}
394+return rx;},cache:function(rule){var cache={},r=null;return function(s){try{r=cache[s]=(cache[s]||rule.call(this,s));}catch(e){r=cache[s]=e;}
395+if(r instanceof $P.Exception){throw r;}else{return r;}};},any:function(){var px=arguments;return function(s){var r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
396+try{r=(px[i].call(this,s));}catch(e){r=null;}
397+if(r){return r;}}
398+throw new $P.Exception(s);};},each:function(){var px=arguments;return function(s){var rx=[],r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
399+try{r=(px[i].call(this,s));}catch(e){throw new $P.Exception(s);}
400+rx.push(r[0]);s=r[1];}
401+return[rx,s];};},all:function(){var px=arguments,_=_;return _.each(_.optional(px));},sequence:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;if(px.length==1){return px[0];}
402+return function(s){var r=null,q=null;var rx=[];for(var i=0;i<px.length;i++){try{r=px[i].call(this,s);}catch(e){break;}
403+rx.push(r[0]);try{q=d.call(this,r[1]);}catch(ex){q=null;break;}
404+s=q[1];}
405+if(!r){throw new $P.Exception(s);}
406+if(q){throw new $P.Exception(q[1]);}
407+if(c){try{r=c.call(this,r[1]);}catch(ey){throw new $P.Exception(r[1]);}}
408+return[rx,(r?r[1]:s)];};},between:function(d1,p,d2){d2=d2||d1;var _fn=_.each(_.ignore(d1),p,_.ignore(d2));return function(s){var rx=_fn.call(this,s);return[[rx[0][0],r[0][2]],rx[1]];};},list:function(p,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return(p instanceof Array?_.each(_.product(p.slice(0,-1),_.ignore(d)),p.slice(-1),_.ignore(c)):_.each(_.many(_.each(p,_.ignore(d))),px,_.ignore(c)));},set:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return function(s){var r=null,p=null,q=null,rx=null,best=[[],s],last=false;for(var i=0;i<px.length;i++){q=null;p=null;r=null;last=(px.length==1);try{r=px[i].call(this,s);}catch(e){continue;}
409+rx=[[r[0]],r[1]];if(r[1].length>0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;}
410+if(!last&&q[1].length===0){last=true;}
411+if(!last){var qx=[];for(var j=0;j<px.length;j++){if(i!=j){qx.push(px[j]);}}
412+p=_.set(qx,d).call(this,q[1]);if(p[0].length>0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}}
413+if(rx[1].length<best[1].length){best=rx;}
414+if(best[1].length===0){break;}}
415+if(best[0].length===0){return best;}
416+if(c){try{q=c.call(this,best[1]);}catch(ey){throw new $P.Exception(best[1]);}
417+best[1]=q[1];}
418+return best;};},forward:function(gr,fname){return function(s){return gr[fname].call(this,s);};},replace:function(rule,repl){return function(s){var r=rule.call(this,s);return[repl,r[1]];};},process:function(rule,fn){return function(s){var r=rule.call(this,s);return[fn.call(this,r[0]),r[1]];};},min:function(min,rule){return function(s){var rx=rule.call(this,s);if(rx[0].length<min){throw new $P.Exception(s);}
419+return rx;};}};var _generator=function(op){return function(){var args=null,rx=[];if(arguments.length>1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];}
420+if(args){for(var i=0,px=args.shift();i<px.length;i++){args.unshift(px[i]);rx.push(op.apply(null,args));args.shift();return rx;}}else{return op.apply(null,arguments);}};};var gx="optional not ignore cache".split(/\s/);for(var i=0;i<gx.length;i++){_[gx[i]]=_generator(_[gx[i]]);}
421+var _vector=function(op){return function(){if(arguments[0]instanceof Array){return op.apply(null,arguments[0]);}else{return op.apply(null,arguments);}};};var vx="each any all".split(/\s/);for(var j=0;j<vx.length;j++){_[vx[j]]=_vector(_[vx[j]]);}}());(function(){var flattenAndCompact=function(ax){var rx=[];for(var i=0;i<ax.length;i++){if(ax[i]instanceof Array){rx=rx.concat(flattenAndCompact(ax[i]));}else{if(ax[i]){rx.push(ax[i]);}}}
422+return rx;};Date.Grammar={};Date.Translator={hour:function(s){return function(){this.hour=Number(s);};},minute:function(s){return function(){this.minute=Number(s);};},second:function(s){return function(){this.second=Number(s);};},meridian:function(s){return function(){this.meridian=s.slice(0,1).toLowerCase();};},timezone:function(s){return function(){var n=s.replace(/[^\d\+\-]/g,"");if(n.length){this.timezoneOffset=Number(n);}else{this.timezone=s.toLowerCase();}};},day:function(x){var s=x[0];return function(){this.day=Number(s.match(/\d+/)[0]);};},month:function(s){return function(){this.month=((s.length==3)?Date.getMonthNumberFromName(s):(Number(s)-1));};},year:function(s){return function(){var n=Number(s);this.year=((s.length>2)?n:(n+(((n+2000)<Date.CultureInfo.twoDigitYearMax)?2000:1900)));};},rday:function(s){return function(){switch(s){case"yesterday":this.days=-1;break;case"tomorrow":this.days=1;break;case"today":this.days=0;break;case"now":this.days=0;this.now=true;break;}};},finishExact:function(x){x=(x instanceof Array)?x:[x];var now=new Date();this.year=now.getFullYear();this.month=now.getMonth();this.day=1;this.hour=0;this.minute=0;this.second=0;for(var i=0;i<x.length;i++){if(x[i]){x[i].call(this);}}
423+this.hour=(this.meridian=="p"&&this.hour<13)?this.hour+12:this.hour;if(this.day>Date.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");}
424+var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});}
425+return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;}
426+for(var i=0;i<x.length;i++){if(typeof x[i]=="function"){x[i].call(this);}}
427+if(this.now){return new Date();}
428+var today=Date.today();var method=null;var expression=!!(this.days!=null||this.orient||this.operator);if(expression){var gap,mod,orient;orient=((this.orient=="past"||this.operator=="subtract")?-1:1);if(this.weekday){this.unit="day";gap=(Date.getDayNumberFromName(this.weekday)-today.getDay());mod=7;this.days=gap?((gap+(orient*mod))%mod):(orient*mod);}
429+if(this.month){this.unit="month";gap=(this.month-today.getMonth());mod=12;this.months=gap?((gap+(orient*mod))%mod):(orient*mod);this.month=null;}
430+if(!this.unit){this.unit="day";}
431+if(this[this.unit+"s"]==null||this.operator!=null){if(!this.value){this.value=1;}
432+if(this.unit=="week"){this.unit="day";this.value=this.value*7;}
433+this[this.unit+"s"]=this.value*orient;}
434+return today.add(this);}else{if(this.meridian&&this.hour){this.hour=(this.hour<13&&this.meridian=="p")?this.hour+12:this.hour;}
435+if(this.weekday&&!this.day){this.day=(today.addDays((Date.getDayNumberFromName(this.weekday)-today.getDay()))).getDate();}
436+if(this.month&&!this.day){this.day=1;}
437+return today.set(this);}}};var _=Date.Parsing.Operators,g=Date.Grammar,t=Date.Translator,_fn;g.datePartDelimiter=_.rtoken(/^([\s\-\.\,\/\x27]+)/);g.timePartDelimiter=_.stoken(":");g.whiteSpace=_.rtoken(/^\s*/);g.generalDelimiter=_.rtoken(/^(([\s\,]|at|on)+)/);var _C={};g.ctoken=function(keys){var fn=_C[keys];if(!fn){var c=Date.CultureInfo.regexPatterns;var kx=keys.split(/\s+/),px=[];for(var i=0;i<kx.length;i++){px.push(_.replace(_.rtoken(c[kx[i]]),kx[i]));}
438+fn=_C[keys]=_.any.apply(null,px);}
439+return fn;};g.ctoken2=function(key){return _.rtoken(Date.CultureInfo.regexPatterns[key]);};g.h=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2]|[1-9])/),t.hour));g.hh=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2])/),t.hour));g.H=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3]|[0-9])/),t.hour));g.HH=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3])/),t.hour));g.m=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.minute));g.mm=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.minute));g.s=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.second));g.ss=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.second));g.hms=_.cache(_.sequence([g.H,g.mm,g.ss],g.timePartDelimiter));g.t=_.cache(_.process(g.ctoken2("shortMeridian"),t.meridian));g.tt=_.cache(_.process(g.ctoken2("longMeridian"),t.meridian));g.z=_.cache(_.process(_.rtoken(/^(\+|\-)?\s*\d\d\d\d?/),t.timezone));g.zz=_.cache(_.process(_.rtoken(/^(\+|\-)\s*\d\d\d\d/),t.timezone));g.zzz=_.cache(_.process(g.ctoken2("timezone"),t.timezone));g.timeSuffix=_.each(_.ignore(g.whiteSpace),_.set([g.tt,g.zzz]));g.time=_.each(_.optional(_.ignore(_.stoken("T"))),g.hms,g.timeSuffix);g.d=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1]|\d)/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.dd=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1])/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.ddd=g.dddd=_.cache(_.process(g.ctoken("sun mon tue wed thu fri sat"),function(s){return function(){this.weekday=s;};}));g.M=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d|\d)/),t.month));g.MM=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d)/),t.month));g.MMM=g.MMMM=_.cache(_.process(g.ctoken("jan feb mar apr may jun jul aug sep oct nov dec"),t.month));g.y=_.cache(_.process(_.rtoken(/^(\d\d?)/),t.year));g.yy=_.cache(_.process(_.rtoken(/^(\d\d)/),t.year));g.yyy=_.cache(_.process(_.rtoken(/^(\d\d?\d?\d?)/),t.year));g.yyyy=_.cache(_.process(_.rtoken(/^(\d\d\d\d)/),t.year));_fn=function(){return _.each(_.any.apply(null,arguments),_.not(g.ctoken2("timeContext")));};g.day=_fn(g.d,g.dd);g.month=_fn(g.M,g.MMM);g.year=_fn(g.yyyy,g.yy);g.orientation=_.process(g.ctoken("past future"),function(s){return function(){this.orient=s;};});g.operator=_.process(g.ctoken("add subtract"),function(s){return function(){this.operator=s;};});g.rday=_.process(g.ctoken("yesterday tomorrow today now"),t.rday);g.unit=_.process(g.ctoken("minute hour day week month year"),function(s){return function(){this.unit=s;};});g.value=_.process(_.rtoken(/^\d\d?(st|nd|rd|th)?/),function(s){return function(){this.value=s.replace(/\D/g,"");};});g.expression=_.set([g.rday,g.operator,g.value,g.unit,g.orientation,g.ddd,g.MMM]);_fn=function(){return _.set(arguments,g.datePartDelimiter);};g.mdy=_fn(g.ddd,g.month,g.day,g.year);g.ymd=_fn(g.ddd,g.year,g.month,g.day);g.dmy=_fn(g.ddd,g.day,g.month,g.year);g.date=function(s){return((g[Date.CultureInfo.dateElementOrder]||g.mdy).call(this,s));};g.format=_.process(_.many(_.any(_.process(_.rtoken(/^(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?)/),function(fmt){if(g[fmt]){return g[fmt];}else{throw Date.Parsing.Exception(fmt);}}),_.process(_.rtoken(/^[^dMyhHmstz]+/),function(s){return _.ignore(_.stoken(s));}))),function(rules){return _.process(_.each.apply(null,rules),t.finishExact);});var _F={};var _get=function(f){return _F[f]=(_F[f]||g.format(f)[0]);};g.formats=function(fx){if(fx instanceof Array){var rx=[];for(var i=0;i<fx.length;i++){rx.push(_get(fx[i]));}
440+return _.any.apply(null,rx);}else{return _get(fx);}};g._formats=g.formats(["yyyy-MM-ddTHH:mm:ss","ddd, MMM dd, yyyy H:mm:ss tt","ddd MMM d yyyy HH:mm:ss zzz","d"]);g._start=_.process(_.set([g.date,g.time,g.expression],g.generalDelimiter,g.whiteSpace),t.finish);g.start=function(s){try{var r=g._formats.call({},s);if(r[1].length===0){return r;}}catch(e){}
441+return g._start.call({},s);};}());Date._parse=Date.parse;Date.parse=function(s){var r=null;if(!s){return null;}
442+try{r=Date.Grammar.start.call({},s);}catch(e){return null;}
443+return((r[1].length===0)?r[0]:null);};Date.getParseFunction=function(fx){var fn=Date.Grammar.formats(fx);return function(s){var r=null;try{r=fn.call({},s);}catch(e){return null;}
444+return((r[1].length===0)?r[0]:null);};};Date.parseExact=function(s,fx){return Date.getParseFunction(fx)(s);};
445+
446+
447+/**
448+ * @version: 1.0 Alpha-1
449+ * @author: Coolite Inc. http://www.coolite.com/
450+ * @date: 2008-04-13
451+ * @copyright: Copyright (c) 2006-2008, Coolite Inc. (http://www.coolite.com/). All rights reserved.
452+ * @license: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/.
453+ * @website: http://www.datejs.com/
454+ */
455+
456+/*
457+ * TimeSpan(milliseconds);
458+ * TimeSpan(days, hours, minutes, seconds);
459+ * TimeSpan(days, hours, minutes, seconds, milliseconds);
460+ */
461+var TimeSpan = function (days, hours, minutes, seconds, milliseconds) {
462+ var attrs = "days hours minutes seconds milliseconds".split(/\s+/);
463+
464+ var gFn = function (attr) {
465+ return function () {
466+ return this[attr];
467+ };
468+ };
469+
470+ var sFn = function (attr) {
471+ return function (val) {
472+ this[attr] = val;
473+ return this;
474+ };
475+ };
476+
477+ for (var i = 0; i < attrs.length ; i++) {
478+ var $a = attrs[i], $b = $a.slice(0, 1).toUpperCase() + $a.slice(1);
479+ TimeSpan.prototype[$a] = 0;
480+ TimeSpan.prototype["get" + $b] = gFn($a);
481+ TimeSpan.prototype["set" + $b] = sFn($a);
482+ }
483+
484+ if (arguments.length == 4) {
485+ this.setDays(days);
486+ this.setHours(hours);
487+ this.setMinutes(minutes);
488+ this.setSeconds(seconds);
489+ } else if (arguments.length == 5) {
490+ this.setDays(days);
491+ this.setHours(hours);
492+ this.setMinutes(minutes);
493+ this.setSeconds(seconds);
494+ this.setMilliseconds(milliseconds);
495+ } else if (arguments.length == 1 && typeof days == "number") {
496+ var orient = (days < 0) ? -1 : +1;
497+ this.setMilliseconds(Math.abs(days));
498+
499+ this.setDays(Math.floor(this.getMilliseconds() / 86400000) * orient);
500+ this.setMilliseconds(this.getMilliseconds() % 86400000);
501+
502+ this.setHours(Math.floor(this.getMilliseconds() / 3600000) * orient);
503+ this.setMilliseconds(this.getMilliseconds() % 3600000);
504+
505+ this.setMinutes(Math.floor(this.getMilliseconds() / 60000) * orient);
506+ this.setMilliseconds(this.getMilliseconds() % 60000);
507+
508+ this.setSeconds(Math.floor(this.getMilliseconds() / 1000) * orient);
509+ this.setMilliseconds(this.getMilliseconds() % 1000);
510+
511+ this.setMilliseconds(this.getMilliseconds() * orient);
512+ }
513+
514+ this.getTotalMilliseconds = function () {
515+ return (this.getDays() * 86400000) + (this.getHours() * 3600000) + (this.getMinutes() * 60000) + (this.getSeconds() * 1000);
516+ };
517+
518+ this.compareTo = function (time) {
519+ var t1 = new Date(1970, 1, 1, this.getHours(), this.getMinutes(), this.getSeconds()), t2;
520+ if (time === null) {
521+ t2 = new Date(1970, 1, 1, 0, 0, 0);
522+ }
523+ else {
524+ t2 = new Date(1970, 1, 1, time.getHours(), time.getMinutes(), time.getSeconds());
525+ }
526+ return (t1 < t2) ? -1 : (t1 > t2) ? 1 : 0;
527+ };
528+
529+ this.equals = function (time) {
530+ return (this.compareTo(time) === 0);
531+ };
532+
533+ this.add = function (time) {
534+ return (time === null) ? this : this.addSeconds(time.getTotalMilliseconds() / 1000);
535+ };
536+
537+ this.subtract = function (time) {
538+ return (time === null) ? this : this.addSeconds(-time.getTotalMilliseconds() / 1000);
539+ };
540+
541+ this.addDays = function (n) {
542+ return new TimeSpan(this.getTotalMilliseconds() + (n * 86400000));
543+ };
544+
545+ this.addHours = function (n) {
546+ return new TimeSpan(this.getTotalMilliseconds() + (n * 3600000));
547+ };
548+
549+ this.addMinutes = function (n) {
550+ return new TimeSpan(this.getTotalMilliseconds() + (n * 60000));
551+ };
552+
553+ this.addSeconds = function (n) {
554+ return new TimeSpan(this.getTotalMilliseconds() + (n * 1000));
555+ };
556+
557+ this.addMilliseconds = function (n) {
558+ return new TimeSpan(this.getTotalMilliseconds() + n);
559+ };
560+
561+ this.get12HourHour = function () {
562+ return (this.getHours() > 12) ? this.getHours() - 12 : (this.getHours() === 0) ? 12 : this.getHours();
563+ };
564+
565+ this.getDesignator = function () {
566+ return (this.getHours() < 12) ? Date.CultureInfo.amDesignator : Date.CultureInfo.pmDesignator;
567+ };
568+
569+ this.toString = function (format) {
570+ this._toString = function () {
571+ if (this.getDays() !== null && this.getDays() > 0) {
572+ return this.getDays() + "." + this.getHours() + ":" + this.p(this.getMinutes()) + ":" + this.p(this.getSeconds());
573+ }
574+ else {
575+ return this.getHours() + ":" + this.p(this.getMinutes()) + ":" + this.p(this.getSeconds());
576+ }
577+ };
578+
579+ this.p = function (s) {
580+ return (s.toString().length < 2) ? "0" + s : s;
581+ };
582+
583+ var me = this;
584+
585+ return format ? format.replace(/dd?|HH?|hh?|mm?|ss?|tt?/g,
586+ function (format) {
587+ switch (format) {
588+ case "d":
589+ return me.getDays();
590+ case "dd":
591+ return me.p(me.getDays());
592+ case "H":
593+ return me.getHours();
594+ case "HH":
595+ return me.p(me.getHours());
596+ case "h":
597+ return me.get12HourHour();
598+ case "hh":
599+ return me.p(me.get12HourHour());
600+ case "m":
601+ return me.getMinutes();
602+ case "mm":
603+ return me.p(me.getMinutes());
604+ case "s":
605+ return me.getSeconds();
606+ case "ss":
607+ return me.p(me.getSeconds());
608+ case "t":
609+ return ((me.getHours() < 12) ? Date.CultureInfo.amDesignator : Date.CultureInfo.pmDesignator).substring(0, 1);
610+ case "tt":
611+ return (me.getHours() < 12) ? Date.CultureInfo.amDesignator : Date.CultureInfo.pmDesignator;
612+ }
613+ }
614+ ) : this._toString();
615+ };
616+ return this;
617+};
618+
619+/**
620+ * Gets the time of day for this date instances.
621+ * @return {TimeSpan} TimeSpan
622+ */
623+Date.prototype.getTimeOfDay = function () {
624+ return new TimeSpan(0, this.getHours(), this.getMinutes(), this.getSeconds(), this.getMilliseconds());
625+};
626+
627+/*
628+ * TimePeriod(startDate, endDate);
629+ * TimePeriod(years, months, days, hours, minutes, seconds, milliseconds);
630+ */
631+var TimePeriod = function (years, months, days, hours, minutes, seconds, milliseconds) {
632+ var attrs = "years months days hours minutes seconds milliseconds".split(/\s+/);
633+
634+ var gFn = function (attr) {
635+ return function () {
636+ return this[attr];
637+ };
638+ };
639+
640+ var sFn = function (attr) {
641+ return function (val) {
642+ this[attr] = val;
643+ return this;
644+ };
645+ };
646+
647+ for (var i = 0; i < attrs.length ; i++) {
648+ var $a = attrs[i], $b = $a.slice(0, 1).toUpperCase() + $a.slice(1);
649+ TimePeriod.prototype[$a] = 0;
650+ TimePeriod.prototype["get" + $b] = gFn($a);
651+ TimePeriod.prototype["set" + $b] = sFn($a);
652+ }
653+
654+ if (arguments.length == 7) {
655+ this.years = years;
656+ this.months = months;
657+ this.setDays(days);
658+ this.setHours(hours);
659+ this.setMinutes(minutes);
660+ this.setSeconds(seconds);
661+ this.setMilliseconds(milliseconds);
662+ } else if (arguments.length == 2 && arguments[0] instanceof Date && arguments[1] instanceof Date) {
663+ // startDate and endDate as arguments
664+
665+ var d1 = years.clone();
666+ var d2 = months.clone();
667+
668+ var temp = d1.clone();
669+ var orient = (d1 > d2) ? -1 : +1;
670+
671+ this.years = d2.getFullYear() - d1.getFullYear();
672+ temp.addYears(this.years);
673+
674+ if (orient == +1) {
675+ if (temp > d2) {
676+ if (this.years !== 0) {
677+ this.years--;
678+ }
679+ }
680+ } else {
681+ if (temp < d2) {
682+ if (this.years !== 0) {
683+ this.years++;
684+ }
685+ }
686+ }
687+
688+ d1.addYears(this.years);
689+
690+ if (orient == +1) {
691+ while (d1 < d2 && d1.clone().addDays(Date.getDaysInMonth(d1.getYear(), d1.getMonth()) ) < d2) {
692+ d1.addMonths(1);
693+ this.months++;
694+ }
695+ }
696+ else {
697+ while (d1 > d2 && d1.clone().addDays(-d1.getDaysInMonth()) > d2) {
698+ d1.addMonths(-1);
699+ this.months--;
700+ }
701+ }
702+
703+ var diff = d2 - d1;
704+
705+ if (diff !== 0) {
706+ var ts = new TimeSpan(diff);
707+ this.setDays(ts.getDays());
708+ this.setHours(ts.getHours());
709+ this.setMinutes(ts.getMinutes());
710+ this.setSeconds(ts.getSeconds());
711+ this.setMilliseconds(ts.getMilliseconds());
712+ }
713+ }
714+ return this;
715+};
\ No newline at end of file
--- /dev/null
+++ b/Allura/allura/public/nf/js/jquery.flot.js
@@ -0,0 +1,2599 @@
1+/*! Javascript plotting library for jQuery, v. 0.7.
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.1.
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() return the same modified object
26+ * instead of making a new one.
27+ *
28+ * V. 1.1: Fix error handling so e.g. parsing an empty string does
29+ * produce a color rather than just crashing.
30+ */
31+(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]+=I}return G.normalize()};G.scale=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]*=I}return G.normalize()};G.toString=function(){if(G.a>=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return K<J?J:(K>I?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/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(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/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(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[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]}})(jQuery);
32+
33+// the actual Flot code
34+(function($) {
35+ function Plot(placeholder, data_, options_, plugins) {
36+ // data is on the form:
37+ // [ series1, series2 ... ]
38+ // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
39+ // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
40+
41+ var series = [],
42+ options = {
43+ // the color theme used for graphs
44+ colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
45+ legend: {
46+ show: true,
47+ noColumns: 1, // number of colums in legend table
48+ labelFormatter: null, // fn: string -> string
49+ labelBoxBorderColor: "#ccc", // border color for the little label boxes
50+ container: null, // container (as jQuery object) to put legend in, null means default on top of graph
51+ position: "ne", // position of default legend container within plot
52+ margin: 5, // distance from grid edge to default legend container within plot
53+ backgroundColor: null, // null means auto-detect
54+ backgroundOpacity: 0.85 // set to 0 to avoid background
55+ },
56+ xaxis: {
57+ show: null, // null = auto-detect, true = always, false = never
58+ position: "bottom", // or "top"
59+ mode: null, // null or "time"
60+ color: null, // base color, labels, ticks
61+ tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
62+ transform: null, // null or f: number -> number to transform axis
63+ inverseTransform: null, // if transform is set, this should be the inverse function
64+ min: null, // min. value to show, null means set automatically
65+ max: null, // max. value to show, null means set automatically
66+ autoscaleMargin: null, // margin in % to add if auto-setting min/max
67+ ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
68+ tickFormatter: null, // fn: number -> string
69+ labelWidth: null, // size of tick labels in pixels
70+ labelHeight: null,
71+ reserveSpace: null, // whether to reserve space even if axis isn't shown
72+ tickLength: null, // size in pixels of ticks, or "full" for whole line
73+ alignTicksWithAxis: null, // axis number or null for no sync
74+
75+ // mode specific options
76+ tickDecimals: null, // no. of decimals, null means auto
77+ tickSize: null, // number or [number, "unit"]
78+ minTickSize: null, // number or [number, "unit"]
79+ monthNames: null, // list of names of months
80+ timeformat: null, // format string to use
81+ twelveHourClock: false // 12 or 24 time in time mode
82+ },
83+ yaxis: {
84+ autoscaleMargin: 0.02,
85+ position: "left" // or "right"
86+ },
87+ xaxes: [],
88+ yaxes: [],
89+ series: {
90+ points: {
91+ show: false,
92+ radius: 3,
93+ lineWidth: 2, // in pixels
94+ fill: true,
95+ fillColor: "#ffffff",
96+ symbol: "circle" // or callback
97+ },
98+ lines: {
99+ // we don't put in show: false so we can see
100+ // whether lines were actively disabled
101+ lineWidth: 2, // in pixels
102+ fill: false,
103+ fillColor: null,
104+ steps: false
105+ },
106+ bars: {
107+ show: false,
108+ lineWidth: 2, // in pixels
109+ barWidth: 1, // in units of the x axis
110+ fill: true,
111+ fillColor: null,
112+ align: "left", // or "center"
113+ horizontal: false
114+ },
115+ shadowSize: 3
116+ },
117+ grid: {
118+ show: true,
119+ aboveData: false,
120+ color: "#545454", // primary color used for outline and labels
121+ backgroundColor: null, // null for transparent, else color
122+ borderColor: null, // set if different from the grid color
123+ tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
124+ labelMargin: 5, // in pixels
125+ axisMargin: 8, // in pixels
126+ borderWidth: 2, // in pixels
127+ minBorderMargin: null, // in pixels, null means taken from points radius
128+ markings: null, // array of ranges or fn: axes -> array of ranges
129+ markingsColor: "#f4f4f4",
130+ markingsLineWidth: 2,
131+ // interactive stuff
132+ clickable: false,
133+ hoverable: false,
134+ autoHighlight: true, // highlight in case mouse is near
135+ mouseActiveRadius: 10 // how far the mouse can be away to activate an item
136+ },
137+ hooks: {}
138+ },
139+ canvas = null, // the canvas for the plot itself
140+ overlay = null, // canvas for interactive stuff on top of plot
141+ eventHolder = null, // jQuery object that events should be bound to
142+ ctx = null, octx = null,
143+ xaxes = [], yaxes = [],
144+ plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
145+ canvasWidth = 0, canvasHeight = 0,
146+ plotWidth = 0, plotHeight = 0,
147+ hooks = {
148+ processOptions: [],
149+ processRawData: [],
150+ processDatapoints: [],
151+ drawSeries: [],
152+ draw: [],
153+ bindEvents: [],
154+ drawOverlay: [],
155+ shutdown: []
156+ },
157+ plot = this;
158+
159+ // public functions
160+ plot.setData = setData;
161+ plot.setupGrid = setupGrid;
162+ plot.draw = draw;
163+ plot.getPlaceholder = function() { return placeholder; };
164+ plot.getCanvas = function() { return canvas; };
165+ plot.getPlotOffset = function() { return plotOffset; };
166+ plot.width = function () { return plotWidth; };
167+ plot.height = function () { return plotHeight; };
168+ plot.offset = function () {
169+ var o = eventHolder.offset();
170+ o.left += plotOffset.left;
171+ o.top += plotOffset.top;
172+ return o;
173+ };
174+ plot.getData = function () { return series; };
175+ plot.getAxes = function () {
176+ var res = {}, i;
177+ $.each(xaxes.concat(yaxes), function (_, axis) {
178+ if (axis)
179+ res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
180+ });
181+ return res;
182+ };
183+ plot.getXAxes = function () { return xaxes; };
184+ plot.getYAxes = function () { return yaxes; };
185+ plot.c2p = canvasToAxisCoords;
186+ plot.p2c = axisToCanvasCoords;
187+ plot.getOptions = function () { return options; };
188+ plot.highlight = highlight;
189+ plot.unhighlight = unhighlight;
190+ plot.triggerRedrawOverlay = triggerRedrawOverlay;
191+ plot.pointOffset = function(point) {
192+ return {
193+ left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left),
194+ top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top)
195+ };
196+ };
197+ plot.shutdown = shutdown;
198+ plot.resize = function () {
199+ getCanvasDimensions();
200+ resizeCanvas(canvas);
201+ resizeCanvas(overlay);
202+ };
203+
204+ // public attributes
205+ plot.hooks = hooks;
206+
207+ // initialize
208+ initPlugins(plot);
209+ parseOptions(options_);
210+ setupCanvases();
211+ setData(data_);
212+ setupGrid();
213+ draw();
214+ bindEvents();
215+
216+
217+ function executeHooks(hook, args) {
218+ args = [plot].concat(args);
219+ for (var i = 0; i < hook.length; ++i)
220+ hook[i].apply(this, args);
221+ }
222+
223+ function initPlugins() {
224+ for (var i = 0; i < plugins.length; ++i) {
225+ var p = plugins[i];
226+ p.init(plot);
227+ if (p.options)
228+ $.extend(true, options, p.options);
229+ }
230+ }
231+
232+ function parseOptions(opts) {
233+ var i;
234+
235+ $.extend(true, options, opts);
236+
237+ if (options.xaxis.color == null)
238+ options.xaxis.color = options.grid.color;
239+ if (options.yaxis.color == null)
240+ options.yaxis.color = options.grid.color;
241+
242+ if (options.xaxis.tickColor == null) // backwards-compatibility
243+ options.xaxis.tickColor = options.grid.tickColor;
244+ if (options.yaxis.tickColor == null) // backwards-compatibility
245+ options.yaxis.tickColor = options.grid.tickColor;
246+
247+ if (options.grid.borderColor == null)
248+ options.grid.borderColor = options.grid.color;
249+ if (options.grid.tickColor == null)
250+ options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
251+
252+ // fill in defaults in axes, copy at least always the
253+ // first as the rest of the code assumes it'll be there
254+ for (i = 0; i < Math.max(1, options.xaxes.length); ++i)
255+ options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
256+ for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
257+ options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);
258+
259+ // backwards compatibility, to be removed in future
260+ if (options.xaxis.noTicks && options.xaxis.ticks == null)
261+ options.xaxis.ticks = options.xaxis.noTicks;
262+ if (options.yaxis.noTicks && options.yaxis.ticks == null)
263+ options.yaxis.ticks = options.yaxis.noTicks;
264+ if (options.x2axis) {
265+ options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
266+ options.xaxes[1].position = "top";
267+ }
268+ if (options.y2axis) {
269+ options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
270+ options.yaxes[1].position = "right";
271+ }
272+ if (options.grid.coloredAreas)
273+ options.grid.markings = options.grid.coloredAreas;
274+ if (options.grid.coloredAreasColor)
275+ options.grid.markingsColor = options.grid.coloredAreasColor;
276+ if (options.lines)
277+ $.extend(true, options.series.lines, options.lines);
278+ if (options.points)
279+ $.extend(true, options.series.points, options.points);
280+ if (options.bars)
281+ $.extend(true, options.series.bars, options.bars);
282+ if (options.shadowSize != null)
283+ options.series.shadowSize = options.shadowSize;
284+
285+ // save options on axes for future reference
286+ for (i = 0; i < options.xaxes.length; ++i)
287+ getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
288+ for (i = 0; i < options.yaxes.length; ++i)
289+ getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];
290+
291+ // add hooks from options
292+ for (var n in hooks)
293+ if (options.hooks[n] && options.hooks[n].length)
294+ hooks[n] = hooks[n].concat(options.hooks[n]);
295+
296+ executeHooks(hooks.processOptions, [options]);
297+ }
298+
299+ function setData(d) {
300+ series = parseData(d);
301+ fillInSeriesOptions();
302+ processData();
303+ }
304+
305+ function parseData(d) {
306+ var res = [];
307+ for (var i = 0; i < d.length; ++i) {
308+ var s = $.extend(true, {}, options.series);
309+
310+ if (d[i].data != null) {
311+ s.data = d[i].data; // move the data instead of deep-copy
312+ delete d[i].data;
313+
314+ $.extend(true, s, d[i]);
315+
316+ d[i].data = s.data;
317+ }
318+ else
319+ s.data = d[i];
320+ res.push(s);
321+ }
322+
323+ return res;
324+ }
325+
326+ function axisNumber(obj, coord) {
327+ var a = obj[coord + "axis"];
328+ if (typeof a == "object") // if we got a real axis, extract number
329+ a = a.n;
330+ if (typeof a != "number")
331+ a = 1; // default to first axis
332+ return a;
333+ }
334+
335+ function allAxes() {
336+ // return flat array without annoying null entries
337+ return $.grep(xaxes.concat(yaxes), function (a) { return a; });
338+ }
339+
340+ function canvasToAxisCoords(pos) {
341+ // return an object with x/y corresponding to all used axes
342+ var res = {}, i, axis;
343+ for (i = 0; i < xaxes.length; ++i) {
344+ axis = xaxes[i];
345+ if (axis && axis.used)
346+ res["x" + axis.n] = axis.c2p(pos.left);
347+ }
348+
349+ for (i = 0; i < yaxes.length; ++i) {
350+ axis = yaxes[i];
351+ if (axis && axis.used)
352+ res["y" + axis.n] = axis.c2p(pos.top);
353+ }
354+
355+ if (res.x1 !== undefined)
356+ res.x = res.x1;
357+ if (res.y1 !== undefined)
358+ res.y = res.y1;
359+
360+ return res;
361+ }
362+
363+ function axisToCanvasCoords(pos) {
364+ // get canvas coords from the first pair of x/y found in pos
365+ var res = {}, i, axis, key;
366+
367+ for (i = 0; i < xaxes.length; ++i) {
368+ axis = xaxes[i];
369+ if (axis && axis.used) {
370+ key = "x" + axis.n;
371+ if (pos[key] == null && axis.n == 1)
372+ key = "x";
373+
374+ if (pos[key] != null) {
375+ res.left = axis.p2c(pos[key]);
376+ break;
377+ }
378+ }
379+ }
380+
381+ for (i = 0; i < yaxes.length; ++i) {
382+ axis = yaxes[i];
383+ if (axis && axis.used) {
384+ key = "y" + axis.n;
385+ if (pos[key] == null && axis.n == 1)
386+ key = "y";
387+
388+ if (pos[key] != null) {
389+ res.top = axis.p2c(pos[key]);
390+ break;
391+ }
392+ }
393+ }
394+
395+ return res;
396+ }
397+
398+ function getOrCreateAxis(axes, number) {
399+ if (!axes[number - 1])
400+ axes[number - 1] = {
401+ n: number, // save the number for future reference
402+ direction: axes == xaxes ? "x" : "y",
403+ options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
404+ };
405+
406+ return axes[number - 1];
407+ }
408+
409+ function fillInSeriesOptions() {
410+ var i;
411+
412+ // collect what we already got of colors
413+ var neededColors = series.length,
414+ usedColors = [],
415+ assignedColors = [];
416+ for (i = 0; i < series.length; ++i) {
417+ var sc = series[i].color;
418+ if (sc != null) {
419+ --neededColors;
420+ if (typeof sc == "number")
421+ assignedColors.push(sc);
422+ else
423+ usedColors.push($.color.parse(series[i].color));
424+ }
425+ }
426+
427+ // we might need to generate more colors if higher indices
428+ // are assigned
429+ for (i = 0; i < assignedColors.length; ++i) {
430+ neededColors = Math.max(neededColors, assignedColors[i] + 1);
431+ }
432+
433+ // produce colors as needed
434+ var colors = [], variation = 0;
435+ i = 0;
436+ while (colors.length < neededColors) {
437+ var c;
438+ if (options.colors.length == i) // check degenerate case
439+ c = $.color.make(100, 100, 100);
440+ else
441+ c = $.color.parse(options.colors[i]);
442+
443+ // vary color if needed
444+ var sign = variation % 2 == 1 ? -1 : 1;
445+ c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)
446+
447+ // FIXME: if we're getting to close to something else,
448+ // we should probably skip this one
449+ colors.push(c);
450+
451+ ++i;
452+ if (i >= options.colors.length) {
453+ i = 0;
454+ ++variation;
455+ }
456+ }
457+
458+ // fill in the options
459+ var colori = 0, s;
460+ for (i = 0; i < series.length; ++i) {
461+ s = series[i];
462+
463+ // assign colors
464+ if (s.color == null) {
465+ s.color = colors[colori].toString();
466+ ++colori;
467+ }
468+ else if (typeof s.color == "number")
469+ s.color = colors[s.color].toString();
470+
471+ // turn on lines automatically in case nothing is set
472+ if (s.lines.show == null) {
473+ var v, show = true;
474+ for (v in s)
475+ if (s[v] && s[v].show) {
476+ show = false;
477+ break;
478+ }
479+ if (show)
480+ s.lines.show = true;
481+ }
482+
483+ // setup axes
484+ s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
485+ s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
486+ }
487+ }
488+
489+ function processData() {
490+ var topSentry = Number.POSITIVE_INFINITY,
491+ bottomSentry = Number.NEGATIVE_INFINITY,
492+ fakeInfinity = Number.MAX_VALUE,
493+ i, j, k, m, length,
494+ s, points, ps, x, y, axis, val, f, p;
495+
496+ function updateAxis(axis, min, max) {
497+ if (min < axis.datamin && min != -fakeInfinity)
498+ axis.datamin = min;
499+ if (max > axis.datamax && max != fakeInfinity)
500+ axis.datamax = max;
501+ }
502+
503+ $.each(allAxes(), function (_, axis) {
504+ // init axis
505+ axis.datamin = topSentry;
506+ axis.datamax = bottomSentry;
507+ axis.used = false;
508+ });
509+
510+ for (i = 0; i < series.length; ++i) {
511+ s = series[i];
512+ s.datapoints = { points: [] };
513+
514+ executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
515+ }
516+
517+ // first pass: clean and copy data
518+ for (i = 0; i < series.length; ++i) {
519+ s = series[i];
520+
521+ var data = s.data, format = s.datapoints.format;
522+
523+ if (!format) {
524+ format = [];
525+ // find out how to copy
526+ format.push({ x: true, number: true, required: true });
527+ format.push({ y: true, number: true, required: true });
528+
529+ if (s.bars.show || (s.lines.show && s.lines.fill)) {
530+ format.push({ y: true, number: true, required: false, defaultValue: 0 });
531+ if (s.bars.horizontal) {
532+ delete format[format.length - 1].y;
533+ format[format.length - 1].x = true;
534+ }
535+ }
536+
537+ s.datapoints.format = format;
538+ }
539+
540+ if (s.datapoints.pointsize != null)
541+ continue; // already filled in
542+
543+ s.datapoints.pointsize = format.length;
544+
545+ ps = s.datapoints.pointsize;
546+ points = s.datapoints.points;
547+
548+ insertSteps = s.lines.show && s.lines.steps;
549+ s.xaxis.used = s.yaxis.used = true;
550+
551+ for (j = k = 0; j < data.length; ++j, k += ps) {
552+ p = data[j];
553+
554+ var nullify = p == null;
555+ if (!nullify) {
556+ for (m = 0; m < ps; ++m) {
557+ val = p[m];
558+ f = format[m];
559+
560+ if (f) {
561+ if (f.number && val != null) {
562+ val = +val; // convert to number
563+ if (isNaN(val))
564+ val = null;
565+ else if (val == Infinity)
566+ val = fakeInfinity;
567+ else if (val == -Infinity)
568+ val = -fakeInfinity;
569+ }
570+
571+ if (val == null) {
572+ if (f.required)
573+ nullify = true;
574+
575+ if (f.defaultValue != null)
576+ val = f.defaultValue;
577+ }
578+ }
579+
580+ points[k + m] = val;
581+ }
582+ }
583+
584+ if (nullify) {
585+ for (m = 0; m < ps; ++m) {
586+ val = points[k + m];
587+ if (val != null) {
588+ f = format[m];
589+ // extract min/max info
590+ if (f.x)
591+ updateAxis(s.xaxis, val, val);
592+ if (f.y)
593+ updateAxis(s.yaxis, val, val);
594+ }
595+ points[k + m] = null;
596+ }
597+ }
598+ else {
599+ // a little bit of line specific stuff that
600+ // perhaps shouldn't be here, but lacking
601+ // better means...
602+ if (insertSteps && k > 0
603+ && points[k - ps] != null
604+ && points[k - ps] != points[k]
605+ && points[k - ps + 1] != points[k + 1]) {
606+ // copy the point to make room for a middle point
607+ for (m = 0; m < ps; ++m)
608+ points[k + ps + m] = points[k + m];
609+
610+ // middle point has same y
611+ points[k + 1] = points[k - ps + 1];
612+
613+ // we've added a point, better reflect that
614+ k += ps;
615+ }
616+ }
617+ }
618+ }
619+
620+ // give the hooks a chance to run
621+ for (i = 0; i < series.length; ++i) {
622+ s = series[i];
623+
624+ executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
625+ }
626+
627+ // second pass: find datamax/datamin for auto-scaling
628+ for (i = 0; i < series.length; ++i) {
629+ s = series[i];
630+ points = s.datapoints.points,
631+ ps = s.datapoints.pointsize;
632+
633+ var xmin = topSentry, ymin = topSentry,
634+ xmax = bottomSentry, ymax = bottomSentry;
635+
636+ for (j = 0; j < points.length; j += ps) {
637+ if (points[j] == null)
638+ continue;
639+
640+ for (m = 0; m < ps; ++m) {
641+ val = points[j + m];
642+ f = format[m];
643+ if (!f || val == fakeInfinity || val == -fakeInfinity)
644+ continue;
645+
646+ if (f.x) {
647+ if (val < xmin)
648+ xmin = val;
649+ if (val > xmax)
650+ xmax = val;
651+ }
652+ if (f.y) {
653+ if (val < ymin)
654+ ymin = val;
655+ if (val > ymax)
656+ ymax = val;
657+ }
658+ }
659+ }
660+
661+ if (s.bars.show) {
662+ // make sure we got room for the bar on the dancing floor
663+ var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
664+ if (s.bars.horizontal) {
665+ ymin += delta;
666+ ymax += delta + s.bars.barWidth;
667+ }
668+ else {
669+ xmin += delta;
670+ xmax += delta + s.bars.barWidth;
671+ }
672+ }
673+
674+ updateAxis(s.xaxis, xmin, xmax);
675+ updateAxis(s.yaxis, ymin, ymax);
676+ }
677+
678+ $.each(allAxes(), function (_, axis) {
679+ if (axis.datamin == topSentry)
680+ axis.datamin = null;
681+ if (axis.datamax == bottomSentry)
682+ axis.datamax = null;
683+ });
684+ }
685+
686+ function makeCanvas(skipPositioning, cls) {
687+ var c = document.createElement('canvas');
688+ c.className = cls;
689+ c.width = canvasWidth;
690+ c.height = canvasHeight;
691+
692+ if (!skipPositioning)
693+ $(c).css({ position: 'absolute', left: 0, top: 0 });
694+
695+ $(c).appendTo(placeholder);
696+
697+ if (!c.getContext) // excanvas hack
698+ c = window.G_vmlCanvasManager.initElement(c);
699+
700+ // used for resetting in case we get replotted
701+ c.getContext("2d").save();
702+
703+ return c;
704+ }
705+
706+ function getCanvasDimensions() {
707+ canvasWidth = placeholder.width();
708+ canvasHeight = placeholder.height();
709+
710+ if (canvasWidth <= 0 || canvasHeight <= 0)
711+ throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
712+ }
713+
714+ function resizeCanvas(c) {
715+ // resizing should reset the state (excanvas seems to be
716+ // buggy though)
717+ if (c.width != canvasWidth)
718+ c.width = canvasWidth;
719+
720+ if (c.height != canvasHeight)
721+ c.height = canvasHeight;
722+
723+ // so try to get back to the initial state (even if it's
724+ // gone now, this should be safe according to the spec)
725+ var cctx = c.getContext("2d");
726+ cctx.restore();
727+
728+ // and save again
729+ cctx.save();
730+ }
731+
732+ function setupCanvases() {
733+ var reused,
734+ existingCanvas = placeholder.children("canvas.base"),
735+ existingOverlay = placeholder.children("canvas.overlay");
736+
737+ if (existingCanvas.length == 0 || existingOverlay == 0) {
738+ // init everything
739+
740+ placeholder.html(""); // make sure placeholder is clear
741+
742+ placeholder.css({ padding: 0 }); // padding messes up the positioning
743+
744+ if (placeholder.css("position") == 'static')
745+ placeholder.css("position", "relative"); // for positioning labels and overlay
746+
747+ getCanvasDimensions();
748+
749+ canvas = makeCanvas(true, "base");
750+ overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features
751+
752+ reused = false;
753+ }
754+ else {
755+ // reuse existing elements
756+
757+ canvas = existingCanvas.get(0);
758+ overlay = existingOverlay.get(0);
759+
760+ reused = true;
761+ }
762+
763+ ctx = canvas.getContext("2d");
764+ octx = overlay.getContext("2d");
765+
766+ // we include the canvas in the event holder too, because IE 7
767+ // sometimes has trouble with the stacking order
768+ eventHolder = $([overlay, canvas]);
769+
770+ if (reused) {
771+ // run shutdown in the old plot object
772+ placeholder.data("plot").shutdown();
773+
774+ // reset reused canvases
775+ plot.resize();
776+
777+ // make sure overlay pixels are cleared (canvas is cleared when we redraw)
778+ octx.clearRect(0, 0, canvasWidth, canvasHeight);
779+
780+ // then whack any remaining obvious garbage left
781+ eventHolder.unbind();
782+ placeholder.children().not([canvas, overlay]).remove();
783+ }
784+
785+ // save in case we get replotted
786+ placeholder.data("plot", plot);
787+ }
788+
789+ function bindEvents() {
790+ // bind events
791+ if (options.grid.hoverable) {
792+ eventHolder.mousemove(onMouseMove);
793+ eventHolder.mouseleave(onMouseLeave);
794+ }
795+
796+ if (options.grid.clickable)
797+ eventHolder.click(onClick);
798+
799+ executeHooks(hooks.bindEvents, [eventHolder]);
800+ }
801+
802+ function shutdown() {
803+ if (redrawTimeout)
804+ clearTimeout(redrawTimeout);
805+
806+ eventHolder.unbind("mousemove", onMouseMove);
807+ eventHolder.unbind("mouseleave", onMouseLeave);
808+ eventHolder.unbind("click", onClick);
809+
810+ executeHooks(hooks.shutdown, [eventHolder]);
811+ }
812+
813+ function setTransformationHelpers(axis) {
814+ // set helper functions on the axis, assumes plot area
815+ // has been computed already
816+
817+ function identity(x) { return x; }
818+
819+ var s, m, t = axis.options.transform || identity,
820+ it = axis.options.inverseTransform;
821+
822+ // precompute how much the axis is scaling a point
823+ // in canvas space
824+ if (axis.direction == "x") {
825+ s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
826+ m = Math.min(t(axis.max), t(axis.min));
827+ }
828+ else {
829+ s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
830+ s = -s;
831+ m = Math.max(t(axis.max), t(axis.min));
832+ }
833+
834+ // data point to canvas coordinate
835+ if (t == identity) // slight optimization
836+ axis.p2c = function (p) { return (p - m) * s; };
837+ else
838+ axis.p2c = function (p) { return (t(p) - m) * s; };
839+ // canvas coordinate to data point
840+ if (!it)
841+ axis.c2p = function (c) { return m + c / s; };
842+ else
843+ axis.c2p = function (c) { return it(m + c / s); };
844+ }
845+
846+ function measureTickLabels(axis) {
847+ var opts = axis.options, i, ticks = axis.ticks || [], labels = [],
848+ l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv;
849+
850+ function makeDummyDiv(labels, width) {
851+ return $('<div style="position:absolute;top:-10000px;' + width + 'font-size:smaller">' +
852+ '<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis">'
853+ + labels.join("") + '</div></div>')
854+ .appendTo(placeholder);
855+ }
856+
857+ if (axis.direction == "x") {
858+ // to avoid measuring the widths of the labels (it's slow), we
859+ // construct fixed-size boxes and put the labels inside
860+ // them, we don't need the exact figures and the
861+ // fixed-size box content is easy to center
862+ if (w == null)
863+ w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1));
864+
865+ // measure x label heights
866+ if (h == null) {
867+ labels = [];
868+ for (i = 0; i < ticks.length; ++i) {
869+ l = ticks[i].label;
870+ if (l)
871+ labels.push('<div class="tickLabel" style="float:left;width:' + w + 'px">' + l + '</div>');
872+ }
873+
874+ if (labels.length > 0) {
875+ // stick them all in the same div and measure
876+ // collective height
877+ labels.push('<div style="clear:left"></div>');
878+ dummyDiv = makeDummyDiv(labels, "width:10000px;");
879+ h = dummyDiv.height();
880+ dummyDiv.remove();
881+ }
882+ }
883+ }
884+ else if (w == null || h == null) {
885+ // calculate y label dimensions
886+ for (i = 0; i < ticks.length; ++i) {
887+ l = ticks[i].label;
888+ if (l)
889+ labels.push('<div class="tickLabel">' + l + '</div>');
890+ }
891+
892+ if (labels.length > 0) {
893+ dummyDiv = makeDummyDiv(labels, "");
894+ if (w == null)
895+ w = dummyDiv.children().width();
896+ if (h == null)
897+ h = dummyDiv.find("div.tickLabel").height();
898+ dummyDiv.remove();
899+ }
900+ }
901+
902+ if (w == null)
903+ w = 0;
904+ if (h == null)
905+ h = 0;
906+
907+ axis.labelWidth = w;
908+ axis.labelHeight = h;
909+ }
910+
911+ function allocateAxisBoxFirstPhase(axis) {
912+ // find the bounding box of the axis by looking at label
913+ // widths/heights and ticks, make room by diminishing the
914+ // plotOffset
915+
916+ var lw = axis.labelWidth,
917+ lh = axis.labelHeight,
918+ pos = axis.options.position,
919+ tickLength = axis.options.tickLength,
920+ axismargin = options.grid.axisMargin,
921+ padding = options.grid.labelMargin,
922+ all = axis.direction == "x" ? xaxes : yaxes,
923+ index;
924+
925+ // determine axis margin
926+ var samePosition = $.grep(all, function (a) {
927+ return a && a.options.position == pos && a.reserveSpace;
928+ });
929+ if ($.inArray(axis, samePosition) == samePosition.length - 1)
930+ axismargin = 0; // outermost
931+
932+ // determine tick length - if we're innermost, we can use "full"
933+ if (tickLength == null)
934+ tickLength = "full";
935+
936+ var sameDirection = $.grep(all, function (a) {
937+ return a && a.reserveSpace;
938+ });
939+
940+ var innermost = $.inArray(axis, sameDirection) == 0;
941+ if (!innermost && tickLength == "full")
942+ tickLength = 5;
943+
944+ if (!isNaN(+tickLength))
945+ padding += +tickLength;
946+
947+ // compute box
948+ if (axis.direction == "x") {
949+ lh += padding;
950+
951+ if (pos == "bottom") {
952+ plotOffset.bottom += lh + axismargin;
953+ axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };
954+ }
955+ else {
956+ axis.box = { top: plotOffset.top + axismargin, height: lh };
957+ plotOffset.top += lh + axismargin;
958+ }
959+ }
960+ else {
961+ lw += padding;
962+
963+ if (pos == "left") {
964+ axis.box = { left: plotOffset.left + axismargin, width: lw };
965+ plotOffset.left += lw + axismargin;
966+ }
967+ else {
968+ plotOffset.right += lw + axismargin;
969+ axis.box = { left: canvasWidth - plotOffset.right, width: lw };
970+ }
971+ }
972+
973+ // save for future reference
974+ axis.position = pos;
975+ axis.tickLength = tickLength;
976+ axis.box.padding = padding;
977+ axis.innermost = innermost;
978+ }
979+
980+ function allocateAxisBoxSecondPhase(axis) {
981+ // set remaining bounding box coordinates
982+ if (axis.direction == "x") {
983+ axis.box.left = plotOffset.left;
984+ axis.box.width = plotWidth;
985+ }
986+ else {
987+ axis.box.top = plotOffset.top;
988+ axis.box.height = plotHeight;
989+ }
990+ }
991+
992+ function setupGrid() {
993+ var i, axes = allAxes();
994+
995+ // first calculate the plot and axis box dimensions
996+
997+ $.each(axes, function (_, axis) {
998+ axis.show = axis.options.show;
999+ if (axis.show == null)
1000+ axis.show = axis.used; // by default an axis is visible if it's got data
1001+
1002+ axis.reserveSpace = axis.show || axis.options.reserveSpace;
1003+
1004+ setRange(axis);
1005+ });
1006+
1007+ allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });
1008+
1009+ plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
1010+ if (options.grid.show) {
1011+ $.each(allocatedAxes, function (_, axis) {
1012+ // make the ticks
1013+ setupTickGeneration(axis);
1014+ setTicks(axis);
1015+ snapRangeToTicks(axis, axis.ticks);
1016+
1017+ // find labelWidth/Height for axis
1018+ measureTickLabels(axis);
1019+ });
1020+
1021+ // with all dimensions in house, we can compute the
1022+ // axis boxes, start from the outside (reverse order)
1023+ for (i = allocatedAxes.length - 1; i >= 0; --i)
1024+ allocateAxisBoxFirstPhase(allocatedAxes[i]);
1025+
1026+ // make sure we've got enough space for things that
1027+ // might stick out
1028+ var minMargin = options.grid.minBorderMargin;
1029+ if (minMargin == null) {
1030+ minMargin = 0;
1031+ for (i = 0; i < series.length; ++i)
1032+ minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2);
1033+ }
1034+
1035+ for (var a in plotOffset) {
1036+ plotOffset[a] += options.grid.borderWidth;
1037+ plotOffset[a] = Math.max(minMargin, plotOffset[a]);
1038+ }
1039+ }
1040+
1041+ plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
1042+ plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
1043+
1044+ // now we got the proper plotWidth/Height, we can compute the scaling
1045+ $.each(axes, function (_, axis) {
1046+ setTransformationHelpers(axis);
1047+ });
1048+
1049+ if (options.grid.show) {
1050+ $.each(allocatedAxes, function (_, axis) {
1051+ allocateAxisBoxSecondPhase(axis);
1052+ });
1053+
1054+ insertAxisLabels();
1055+ }
1056+
1057+ insertLegend();
1058+ }
1059+
1060+ function setRange(axis) {
1061+ var opts = axis.options,
1062+ min = +(opts.min != null ? opts.min : axis.datamin),
1063+ max = +(opts.max != null ? opts.max : axis.datamax),
1064+ delta = max - min;
1065+
1066+ if (delta == 0.0) {
1067+ // degenerate case
1068+ var widen = max == 0 ? 1 : 0.01;
1069+
1070+ if (opts.min == null)
1071+ min -= widen;
1072+ // always widen max if we couldn't widen min to ensure we
1073+ // don't fall into min == max which doesn't work
1074+ if (opts.max == null || opts.min != null)
1075+ max += widen;
1076+ }
1077+ else {
1078+ // consider autoscaling
1079+ var margin = opts.autoscaleMargin;
1080+ if (margin != null) {
1081+ if (opts.min == null) {
1082+ min -= delta * margin;
1083+ // make sure we don't go below zero if all values
1084+ // are positive
1085+ if (min < 0 && axis.datamin != null && axis.datamin >= 0)
1086+ min = 0;
1087+ }
1088+ if (opts.max == null) {
1089+ max += delta * margin;
1090+ if (max > 0 && axis.datamax != null && axis.datamax <= 0)
1091+ max = 0;
1092+ }
1093+ }
1094+ }
1095+ axis.min = min;
1096+ axis.max = max;
1097+ }
1098+
1099+ function setupTickGeneration(axis) {
1100+ var opts = axis.options;
1101+
1102+ // estimate number of ticks
1103+ var noTicks;
1104+ if (typeof opts.ticks == "number" && opts.ticks > 0)
1105+ noTicks = opts.ticks;
1106+ else
1107+ // heuristic based on the model a*sqrt(x) fitted to
1108+ // some data points that seemed reasonable
1109+ noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);
1110+
1111+ var delta = (axis.max - axis.min) / noTicks,
1112+ size, generator, unit, formatter, i, magn, norm;
1113+
1114+ if (opts.mode == "time") {
1115+ // pretty handling of time
1116+
1117+ // map of app. size of time units in milliseconds
1118+ var timeUnitSize = {
1119+ "second": 1000,
1120+ "minute": 60 * 1000,
1121+ "hour": 60 * 60 * 1000,
1122+ "day": 24 * 60 * 60 * 1000,
1123+ "month": 30 * 24 * 60 * 60 * 1000,
1124+ "year": 365.2425 * 24 * 60 * 60 * 1000
1125+ };
1126+
1127+
1128+ // the allowed tick sizes, after 1 year we use
1129+ // an integer algorithm
1130+ var spec = [
1131+ [1, "second"], [2, "second"], [5, "second"], [10, "second"],
1132+ [30, "second"],
1133+ [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
1134+ [30, "minute"],
1135+ [1, "hour"], [2, "hour"], [4, "hour"],
1136+ [8, "hour"], [12, "hour"],
1137+ [1, "day"], [2, "day"], [3, "day"],
1138+ [0.25, "month"], [0.5, "month"], [1, "month"],
1139+ [2, "month"], [3, "month"], [6, "month"],
1140+ [1, "year"]
1141+ ];
1142+
1143+ var minSize = 0;
1144+ if (opts.minTickSize != null) {
1145+ if (typeof opts.tickSize == "number")
1146+ minSize = opts.tickSize;
1147+ else
1148+ minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
1149+ }
1150+
1151+ for (var i = 0; i < spec.length - 1; ++i)
1152+ if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
1153+ + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
1154+ && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
1155+ break;
1156+ size = spec[i][0];
1157+ unit = spec[i][1];
1158+
1159+ // special-case the possibility of several years
1160+ if (unit == "year") {
1161+ magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
1162+ norm = (delta / timeUnitSize.year) / magn;
1163+ if (norm < 1.5)
1164+ size = 1;
1165+ else if (norm < 3)
1166+ size = 2;
1167+ else if (norm < 7.5)
1168+ size = 5;
1169+ else
1170+ size = 10;
1171+
1172+ size *= magn;
1173+ }
1174+
1175+ axis.tickSize = opts.tickSize || [size, unit];
1176+
1177+ generator = function(axis) {
1178+ var ticks = [],
1179+ tickSize = axis.tickSize[0], unit = axis.tickSize[1],
1180+ d = new Date(axis.min);
1181+
1182+ var step = tickSize * timeUnitSize[unit];
1183+
1184+ if (unit == "second")
1185+ d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
1186+ if (unit == "minute")
1187+ d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
1188+ if (unit == "hour")
1189+ d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
1190+ if (unit == "month")
1191+ d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
1192+ if (unit == "year")
1193+ d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
1194+
1195+ // reset smaller components
1196+ d.setUTCMilliseconds(0);
1197+ if (step >= timeUnitSize.minute)
1198+ d.setUTCSeconds(0);
1199+ if (step >= timeUnitSize.hour)
1200+ d.setUTCMinutes(0);
1201+ if (step >= timeUnitSize.day)
1202+ d.setUTCHours(0);
1203+ if (step >= timeUnitSize.day * 4)
1204+ d.setUTCDate(1);
1205+ if (step >= timeUnitSize.year)
1206+ d.setUTCMonth(0);
1207+
1208+
1209+ var carry = 0, v = Number.NaN, prev;
1210+ do {
1211+ prev = v;
1212+ v = d.getTime();
1213+ ticks.push(v);
1214+ if (unit == "month") {
1215+ if (tickSize < 1) {
1216+ // a bit complicated - we'll divide the month
1217+ // up but we need to take care of fractions
1218+ // so we don't end up in the middle of a day
1219+ d.setUTCDate(1);
1220+ var start = d.getTime();
1221+ d.setUTCMonth(d.getUTCMonth() + 1);
1222+ var end = d.getTime();
1223+ d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
1224+ carry = d.getUTCHours();
1225+ d.setUTCHours(0);
1226+ }
1227+ else
1228+ d.setUTCMonth(d.getUTCMonth() + tickSize);
1229+ }
1230+ else if (unit == "year") {
1231+ d.setUTCFullYear(d.getUTCFullYear() + tickSize);
1232+ }
1233+ else
1234+ d.setTime(v + step);
1235+ } while (v < axis.max && v != prev);
1236+
1237+ return ticks;
1238+ };
1239+
1240+ formatter = function (v, axis) {
1241+ var d = new Date(v);
1242+
1243+ // first check global format
1244+ if (opts.timeformat != null)
1245+ return $.plot.formatDate(d, opts.timeformat, opts.monthNames);
1246+
1247+ var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
1248+ var span = axis.max - axis.min;
1249+ var suffix = (opts.twelveHourClock) ? " %p" : "";
1250+
1251+ if (t < timeUnitSize.minute)
1252+ fmt = "%h:%M:%S" + suffix;
1253+ else if (t < timeUnitSize.day) {
1254+ if (span < 2 * timeUnitSize.day)
1255+ fmt = "%h:%M" + suffix;
1256+ else
1257+ fmt = "%b %d %h:%M" + suffix;
1258+ }
1259+ else if (t < timeUnitSize.month)
1260+ fmt = "%b %d";
1261+ else if (t < timeUnitSize.year) {
1262+ if (span < timeUnitSize.year)
1263+ fmt = "%b";
1264+ else
1265+ fmt = "%b %y";
1266+ }
1267+ else
1268+ fmt = "%y";
1269+
1270+ return $.plot.formatDate(d, fmt, opts.monthNames);
1271+ };
1272+ }
1273+ else {
1274+ // pretty rounding of base-10 numbers
1275+ var maxDec = opts.tickDecimals;
1276+ var dec = -Math.floor(Math.log(delta) / Math.LN10);
1277+ if (maxDec != null && dec > maxDec)
1278+ dec = maxDec;
1279+
1280+ magn = Math.pow(10, -dec);
1281+ norm = delta / magn; // norm is between 1.0 and 10.0
1282+
1283+ if (norm < 1.5)
1284+ size = 1;
1285+ else if (norm < 3) {
1286+ size = 2;
1287+ // special case for 2.5, requires an extra decimal
1288+ if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
1289+ size = 2.5;
1290+ ++dec;
1291+ }
1292+ }
1293+ else if (norm < 7.5)
1294+ size = 5;
1295+ else
1296+ size = 10;
1297+
1298+ size *= magn;
1299+
1300+ if (opts.minTickSize != null && size < opts.minTickSize)
1301+ size = opts.minTickSize;
1302+
1303+ axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
1304+ axis.tickSize = opts.tickSize || size;
1305+
1306+ generator = function (axis) {
1307+ var ticks = [];
1308+
1309+ // spew out all possible ticks
1310+ var start = floorInBase(axis.min, axis.tickSize),
1311+ i = 0, v = Number.NaN, prev;
1312+ do {
1313+ prev = v;
1314+ v = start + i * axis.tickSize;
1315+ ticks.push(v);
1316+ ++i;
1317+ } while (v < axis.max && v != prev);
1318+ return ticks;
1319+ };
1320+
1321+ formatter = function (v, axis) {
1322+ return v.toFixed(axis.tickDecimals);
1323+ };
1324+ }
1325+
1326+ if (opts.alignTicksWithAxis != null) {
1327+ var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
1328+ if (otherAxis && otherAxis.used && otherAxis != axis) {
1329+ // consider snapping min/max to outermost nice ticks
1330+ var niceTicks = generator(axis);
1331+ if (niceTicks.length > 0) {
1332+ if (opts.min == null)
1333+ axis.min = Math.min(axis.min, niceTicks[0]);
1334+ if (opts.max == null && niceTicks.length > 1)
1335+ axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
1336+ }
1337+
1338+ generator = function (axis) {
1339+ // copy ticks, scaled to this axis
1340+ var ticks = [], v, i;
1341+ for (i = 0; i < otherAxis.ticks.length; ++i) {
1342+ v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
1343+ v = axis.min + v * (axis.max - axis.min);
1344+ ticks.push(v);
1345+ }
1346+ return ticks;
1347+ };
1348+
1349+ // we might need an extra decimal since forced
1350+ // ticks don't necessarily fit naturally
1351+ if (axis.mode != "time" && opts.tickDecimals == null) {
1352+ var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1),
1353+ ts = generator(axis);
1354+
1355+ // only proceed if the tick interval rounded
1356+ // with an extra decimal doesn't give us a
1357+ // zero at end
1358+ if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
1359+ axis.tickDecimals = extraDec;
1360+ }
1361+ }
1362+ }
1363+
1364+ axis.tickGenerator = generator;
1365+ if ($.isFunction(opts.tickFormatter))
1366+ axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
1367+ else
1368+ axis.tickFormatter = formatter;
1369+ }
1370+
1371+ function setTicks(axis) {
1372+ var oticks = axis.options.ticks, ticks = [];
1373+ if (oticks == null || (typeof oticks == "number" && oticks > 0))
1374+ ticks = axis.tickGenerator(axis);
1375+ else if (oticks) {
1376+ if ($.isFunction(oticks))
1377+ // generate the ticks
1378+ ticks = oticks({ min: axis.min, max: axis.max });
1379+ else
1380+ ticks = oticks;
1381+ }
1382+
1383+ // clean up/labelify the supplied ticks, copy them over
1384+ var i, v;
1385+ axis.ticks = [];
1386+ for (i = 0; i < ticks.length; ++i) {
1387+ var label = null;
1388+ var t = ticks[i];
1389+ if (typeof t == "object") {
1390+ v = +t[0];
1391+ if (t.length > 1)
1392+ label = t[1];
1393+ }
1394+ else
1395+ v = +t;
1396+ if (label == null)
1397+ label = axis.tickFormatter(v, axis);
1398+ if (!isNaN(v))
1399+ axis.ticks.push({ v: v, label: label });
1400+ }
1401+ }
1402+
1403+ function snapRangeToTicks(axis, ticks) {
1404+ if (axis.options.autoscaleMargin && ticks.length > 0) {
1405+ // snap to ticks
1406+ if (axis.options.min == null)
1407+ axis.min = Math.min(axis.min, ticks[0].v);
1408+ if (axis.options.max == null && ticks.length > 1)
1409+ axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
1410+ }
1411+ }
1412+
1413+ function draw() {
1414+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
1415+
1416+ var grid = options.grid;
1417+
1418+ // draw background, if any
1419+ if (grid.show && grid.backgroundColor)
1420+ drawBackground();
1421+
1422+ if (grid.show && !grid.aboveData)
1423+ drawGrid();
1424+
1425+ for (var i = 0; i < series.length; ++i) {
1426+ executeHooks(hooks.drawSeries, [ctx, series[i]]);
1427+ drawSeries(series[i]);
1428+ }
1429+
1430+ executeHooks(hooks.draw, [ctx]);
1431+
1432+ if (grid.show && grid.aboveData)
1433+ drawGrid();
1434+ }
1435+
1436+ function extractRange(ranges, coord) {
1437+ var axis, from, to, key, axes = allAxes();
1438+
1439+ for (i = 0; i < axes.length; ++i) {
1440+ axis = axes[i];
1441+ if (axis.direction == coord) {
1442+ key = coord + axis.n + "axis";
1443+ if (!ranges[key] && axis.n == 1)
1444+ key = coord + "axis"; // support x1axis as xaxis
1445+ if (ranges[key]) {
1446+ from = ranges[key].from;
1447+ to = ranges[key].to;
1448+ break;
1449+ }
1450+ }
1451+ }
1452+
1453+ // backwards-compat stuff - to be removed in future
1454+ if (!ranges[key]) {
1455+ axis = coord == "x" ? xaxes[0] : yaxes[0];
1456+ from = ranges[coord + "1"];
1457+ to = ranges[coord + "2"];
1458+ }
1459+
1460+ // auto-reverse as an added bonus
1461+ if (from != null && to != null && from > to) {
1462+ var tmp = from;
1463+ from = to;
1464+ to = tmp;
1465+ }
1466+
1467+ return { from: from, to: to, axis: axis };
1468+ }
1469+
1470+ function drawBackground() {
1471+ ctx.save();
1472+ ctx.translate(plotOffset.left, plotOffset.top);
1473+
1474+ ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
1475+ ctx.fillRect(0, 0, plotWidth, plotHeight);
1476+ ctx.restore();
1477+ }
1478+
1479+ function drawGrid() {
1480+ var i;
1481+
1482+ ctx.save();
1483+ ctx.translate(plotOffset.left, plotOffset.top);
1484+
1485+ // draw markings
1486+ var markings = options.grid.markings;
1487+ if (markings) {
1488+ if ($.isFunction(markings)) {
1489+ var axes = plot.getAxes();
1490+ // xmin etc. is backwards compatibility, to be
1491+ // removed in the future
1492+ axes.xmin = axes.xaxis.min;
1493+ axes.xmax = axes.xaxis.max;
1494+ axes.ymin = axes.yaxis.min;
1495+ axes.ymax = axes.yaxis.max;
1496+
1497+ markings = markings(axes);
1498+ }
1499+
1500+ for (i = 0; i < markings.length; ++i) {
1501+ var m = markings[i],
1502+ xrange = extractRange(m, "x"),
1503+ yrange = extractRange(m, "y");
1504+
1505+ // fill in missing
1506+ if (xrange.from == null)
1507+ xrange.from = xrange.axis.min;
1508+ if (xrange.to == null)
1509+ xrange.to = xrange.axis.max;
1510+ if (yrange.from == null)
1511+ yrange.from = yrange.axis.min;
1512+ if (yrange.to == null)
1513+ yrange.to = yrange.axis.max;
1514+
1515+ // clip
1516+ if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
1517+ yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
1518+ continue;
1519+
1520+ xrange.from = Math.max(xrange.from, xrange.axis.min);
1521+ xrange.to = Math.min(xrange.to, xrange.axis.max);
1522+ yrange.from = Math.max(yrange.from, yrange.axis.min);
1523+ yrange.to = Math.min(yrange.to, yrange.axis.max);
1524+
1525+ if (xrange.from == xrange.to && yrange.from == yrange.to)
1526+ continue;
1527+
1528+ // then draw
1529+ xrange.from = xrange.axis.p2c(xrange.from);
1530+ xrange.to = xrange.axis.p2c(xrange.to);
1531+ yrange.from = yrange.axis.p2c(yrange.from);
1532+ yrange.to = yrange.axis.p2c(yrange.to);
1533+
1534+ if (xrange.from == xrange.to || yrange.from == yrange.to) {
1535+ // draw line
1536+ ctx.beginPath();
1537+ ctx.strokeStyle = m.color || options.grid.markingsColor;
1538+ ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
1539+ ctx.moveTo(xrange.from, yrange.from);
1540+ ctx.lineTo(xrange.to, yrange.to);
1541+ ctx.stroke();
1542+ }
1543+ else {
1544+ // fill area
1545+ ctx.fillStyle = m.color || options.grid.markingsColor;
1546+ ctx.fillRect(xrange.from, yrange.to,
1547+ xrange.to - xrange.from,
1548+ yrange.from - yrange.to);
1549+ }
1550+ }
1551+ }
1552+
1553+ // draw the ticks
1554+ var axes = allAxes(), bw = options.grid.borderWidth;
1555+
1556+ for (var j = 0; j < axes.length; ++j) {
1557+ var axis = axes[j], box = axis.box,
1558+ t = axis.tickLength, x, y, xoff, yoff;
1559+ if (!axis.show || axis.ticks.length == 0)
1560+ continue
1561+
1562+ ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();
1563+ ctx.lineWidth = 1;
1564+
1565+ // find the edges
1566+ if (axis.direction == "x") {
1567+ x = 0;
1568+ if (t == "full")
1569+ y = (axis.position == "top" ? 0 : plotHeight);
1570+ else
1571+ y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
1572+ }
1573+ else {
1574+ y = 0;
1575+ if (t == "full")
1576+ x = (axis.position == "left" ? 0 : plotWidth);
1577+ else
1578+ x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
1579+ }
1580+
1581+ // draw tick bar
1582+ if (!axis.innermost) {
1583+ ctx.beginPath();
1584+ xoff = yoff = 0;
1585+ if (axis.direction == "x")
1586+ xoff = plotWidth;
1587+ else
1588+ yoff = plotHeight;
1589+
1590+ if (ctx.lineWidth == 1) {
1591+ x = Math.floor(x) + 0.5;
1592+ y = Math.floor(y) + 0.5;
1593+ }
1594+
1595+ ctx.moveTo(x, y);
1596+ ctx.lineTo(x + xoff, y + yoff);
1597+ ctx.stroke();
1598+ }
1599+
1600+ // draw ticks
1601+ ctx.beginPath();
1602+ for (i = 0; i < axis.ticks.length; ++i) {
1603+ var v = axis.ticks[i].v;
1604+
1605+ xoff = yoff = 0;
1606+
1607+ if (v < axis.min || v > axis.max
1608+ // skip those lying on the axes if we got a border
1609+ || (t == "full" && bw > 0
1610+ && (v == axis.min || v == axis.max)))
1611+ continue;
1612+
1613+ if (axis.direction == "x") {
1614+ x = axis.p2c(v);
1615+ yoff = t == "full" ? -plotHeight : t;
1616+
1617+ if (axis.position == "top")
1618+ yoff = -yoff;
1619+ }
1620+ else {
1621+ y = axis.p2c(v);
1622+ xoff = t == "full" ? -plotWidth : t;
1623+
1624+ if (axis.position == "left")
1625+ xoff = -xoff;
1626+ }
1627+
1628+ if (ctx.lineWidth == 1) {
1629+ if (axis.direction == "x")
1630+ x = Math.floor(x) + 0.5;
1631+ else
1632+ y = Math.floor(y) + 0.5;
1633+ }
1634+
1635+ ctx.moveTo(x, y);
1636+ ctx.lineTo(x + xoff, y + yoff);
1637+ }
1638+
1639+ ctx.stroke();
1640+ }
1641+
1642+
1643+ // draw border
1644+ if (bw) {
1645+ ctx.lineWidth = bw;
1646+ ctx.strokeStyle = options.grid.borderColor;
1647+ ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
1648+ }
1649+
1650+ ctx.restore();
1651+ }
1652+
1653+ function insertAxisLabels() {
1654+ placeholder.find(".tickLabels").remove();
1655+
1656+ var html = ['<div class="tickLabels" style="font-size:smaller">'];
1657+
1658+ var axes = allAxes();
1659+ for (var j = 0; j < axes.length; ++j) {
1660+ var axis = axes[j], box = axis.box;
1661+ if (!axis.show)
1662+ continue;
1663+ //debug: html.push('<div style="position:absolute;opacity:0.10;background-color:red;left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px"></div>')
1664+ html.push('<div class="' + axis.direction + 'Axis ' + axis.direction + axis.n + 'Axis" style="color:' + axis.options.color + '">');
1665+ for (var i = 0; i < axis.ticks.length; ++i) {
1666+ var tick = axis.ticks[i];
1667+ if (!tick.label || tick.v < axis.min || tick.v > axis.max)
1668+ continue;
1669+
1670+ var pos = {}, align;
1671+
1672+ if (axis.direction == "x") {
1673+ align = "center";
1674+ pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2);
1675+ if (axis.position == "bottom")
1676+ pos.top = box.top + box.padding;
1677+ else
1678+ pos.bottom = canvasHeight - (box.top + box.height - box.padding);
1679+ }
1680+ else {
1681+ pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2);
1682+ if (axis.position == "left") {
1683+ pos.right = canvasWidth - (box.left + box.width - box.padding)
1684+ align = "right";
1685+ }
1686+ else {
1687+ pos.left = box.left + box.padding;
1688+ align = "left";
1689+ }
1690+ }
1691+
1692+ pos.width = axis.labelWidth;
1693+
1694+ var style = ["position:absolute", "text-align:" + align ];
1695+ for (var a in pos)
1696+ style.push(a + ":" + pos[a] + "px")
1697+
1698+ html.push('<div class="tickLabel" style="' + style.join(';') + '">' + tick.label + '</div>');
1699+ }
1700+ html.push('</div>');
1701+ }
1702+
1703+ html.push('</div>');
1704+
1705+ placeholder.append(html.join(""));
1706+ }
1707+
1708+ function drawSeries(series) {
1709+ if (series.lines.show)
1710+ drawSeriesLines(series);
1711+ if (series.bars.show)
1712+ drawSeriesBars(series);
1713+ if (series.points.show)
1714+ drawSeriesPoints(series);
1715+ }
1716+
1717+ function drawSeriesLines(series) {
1718+ function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
1719+ var points = datapoints.points,
1720+ ps = datapoints.pointsize,
1721+ prevx = null, prevy = null;
1722+
1723+ ctx.beginPath();
1724+ for (var i = ps; i < points.length; i += ps) {
1725+ var x1 = points[i - ps], y1 = points[i - ps + 1],
1726+ x2 = points[i], y2 = points[i + 1];
1727+
1728+ if (x1 == null || x2 == null)
1729+ continue;
1730+
1731+ // clip with ymin
1732+ if (y1 <= y2 && y1 < axisy.min) {
1733+ if (y2 < axisy.min)
1734+ continue; // line segment is outside
1735+ // compute new intersection point
1736+ x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1737+ y1 = axisy.min;
1738+ }
1739+ else if (y2 <= y1 && y2 < axisy.min) {
1740+ if (y1 < axisy.min)
1741+ continue;
1742+ x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1743+ y2 = axisy.min;
1744+ }
1745+
1746+ // clip with ymax
1747+ if (y1 >= y2 && y1 > axisy.max) {
1748+ if (y2 > axisy.max)
1749+ continue;
1750+ x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1751+ y1 = axisy.max;
1752+ }
1753+ else if (y2 >= y1 && y2 > axisy.max) {
1754+ if (y1 > axisy.max)
1755+ continue;
1756+ x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1757+ y2 = axisy.max;
1758+ }
1759+
1760+ // clip with xmin
1761+ if (x1 <= x2 && x1 < axisx.min) {
1762+ if (x2 < axisx.min)
1763+ continue;
1764+ y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1765+ x1 = axisx.min;
1766+ }
1767+ else if (x2 <= x1 && x2 < axisx.min) {
1768+ if (x1 < axisx.min)
1769+ continue;
1770+ y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1771+ x2 = axisx.min;
1772+ }
1773+
1774+ // clip with xmax
1775+ if (x1 >= x2 && x1 > axisx.max) {
1776+ if (x2 > axisx.max)
1777+ continue;
1778+ y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1779+ x1 = axisx.max;
1780+ }
1781+ else if (x2 >= x1 && x2 > axisx.max) {
1782+ if (x1 > axisx.max)
1783+ continue;
1784+ y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1785+ x2 = axisx.max;
1786+ }
1787+
1788+ if (x1 != prevx || y1 != prevy)
1789+ ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
1790+
1791+ prevx = x2;
1792+ prevy = y2;
1793+ ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
1794+ }
1795+ ctx.stroke();
1796+ }
1797+
1798+ function plotLineArea(datapoints, axisx, axisy) {
1799+ var points = datapoints.points,
1800+ ps = datapoints.pointsize,
1801+ bottom = Math.min(Math.max(0, axisy.min), axisy.max),
1802+ i = 0, top, areaOpen = false,
1803+ ypos = 1, segmentStart = 0, segmentEnd = 0;
1804+
1805+ // we process each segment in two turns, first forward
1806+ // direction to sketch out top, then once we hit the
1807+ // end we go backwards to sketch the bottom
1808+ while (true) {
1809+ if (ps > 0 && i > points.length + ps)
1810+ break;
1811+
1812+ i += ps; // ps is negative if going backwards
1813+
1814+ var x1 = points[i - ps],
1815+ y1 = points[i - ps + ypos],
1816+ x2 = points[i], y2 = points[i + ypos];
1817+
1818+ if (areaOpen) {
1819+ if (ps > 0 && x1 != null && x2 == null) {
1820+ // at turning point
1821+ segmentEnd = i;
1822+ ps = -ps;
1823+ ypos = 2;
1824+ continue;
1825+ }
1826+
1827+ if (ps < 0 && i == segmentStart + ps) {
1828+ // done with the reverse sweep
1829+ ctx.fill();
1830+ areaOpen = false;
1831+ ps = -ps;
1832+ ypos = 1;
1833+ i = segmentStart = segmentEnd + ps;
1834+ continue;
1835+ }
1836+ }
1837+
1838+ if (x1 == null || x2 == null)
1839+ continue;
1840+
1841+ // clip x values
1842+
1843+ // clip with xmin
1844+ if (x1 <= x2 && x1 < axisx.min) {
1845+ if (x2 < axisx.min)
1846+ continue;
1847+ y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1848+ x1 = axisx.min;
1849+ }
1850+ else if (x2 <= x1 && x2 < axisx.min) {
1851+ if (x1 < axisx.min)
1852+ continue;
1853+ y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
1854+ x2 = axisx.min;
1855+ }
1856+
1857+ // clip with xmax
1858+ if (x1 >= x2 && x1 > axisx.max) {
1859+ if (x2 > axisx.max)
1860+ continue;
1861+ y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1862+ x1 = axisx.max;
1863+ }
1864+ else if (x2 >= x1 && x2 > axisx.max) {
1865+ if (x1 > axisx.max)
1866+ continue;
1867+ y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
1868+ x2 = axisx.max;
1869+ }
1870+
1871+ if (!areaOpen) {
1872+ // open area
1873+ ctx.beginPath();
1874+ ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
1875+ areaOpen = true;
1876+ }
1877+
1878+ // now first check the case where both is outside
1879+ if (y1 >= axisy.max && y2 >= axisy.max) {
1880+ ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
1881+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
1882+ continue;
1883+ }
1884+ else if (y1 <= axisy.min && y2 <= axisy.min) {
1885+ ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
1886+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
1887+ continue;
1888+ }
1889+
1890+ // else it's a bit more complicated, there might
1891+ // be a flat maxed out rectangle first, then a
1892+ // triangular cutout or reverse; to find these
1893+ // keep track of the current x values
1894+ var x1old = x1, x2old = x2;
1895+
1896+ // clip the y values, without shortcutting, we
1897+ // go through all cases in turn
1898+
1899+ // clip with ymin
1900+ if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
1901+ x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1902+ y1 = axisy.min;
1903+ }
1904+ else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
1905+ x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
1906+ y2 = axisy.min;
1907+ }
1908+
1909+ // clip with ymax
1910+ if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
1911+ x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1912+ y1 = axisy.max;
1913+ }
1914+ else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
1915+ x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
1916+ y2 = axisy.max;
1917+ }
1918+
1919+ // if the x value was changed we got a rectangle
1920+ // to fill
1921+ if (x1 != x1old) {
1922+ ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
1923+ // it goes to (x1, y1), but we fill that below
1924+ }
1925+
1926+ // fill triangular section, this sometimes result
1927+ // in redundant points if (x1, y1) hasn't changed
1928+ // from previous line to, but we just ignore that
1929+ ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
1930+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
1931+
1932+ // fill the other rectangle if it's there
1933+ if (x2 != x2old) {
1934+ ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
1935+ ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
1936+ }
1937+ }
1938+ }
1939+
1940+ ctx.save();
1941+ ctx.translate(plotOffset.left, plotOffset.top);
1942+ ctx.lineJoin = "round";
1943+
1944+ var lw = series.lines.lineWidth,
1945+ sw = series.shadowSize;
1946+ // FIXME: consider another form of shadow when filling is turned on
1947+ if (lw > 0 && sw > 0) {
1948+ // draw shadow as a thick and thin line with transparency
1949+ ctx.lineWidth = sw;
1950+ ctx.strokeStyle = "rgba(0,0,0,0.1)";
1951+ // position shadow at angle from the mid of line
1952+ var angle = Math.PI/18;
1953+ plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
1954+ ctx.lineWidth = sw/2;
1955+ plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
1956+ }
1957+
1958+ ctx.lineWidth = lw;
1959+ ctx.strokeStyle = series.color;
1960+ var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
1961+ if (fillStyle) {
1962+ ctx.fillStyle = fillStyle;
1963+ plotLineArea(series.datapoints, series.xaxis, series.yaxis);
1964+ }
1965+
1966+ if (lw > 0)
1967+ plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
1968+ ctx.restore();
1969+ }
1970+
1971+ function drawSeriesPoints(series) {
1972+ function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
1973+ var points = datapoints.points, ps = datapoints.pointsize;
1974+
1975+ for (var i = 0; i < points.length; i += ps) {
1976+ var x = points[i], y = points[i + 1];
1977+ if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
1978+ continue;
1979+
1980+ ctx.beginPath();
1981+ x = axisx.p2c(x);
1982+ y = axisy.p2c(y) + offset;
1983+ if (symbol == "circle")
1984+ ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
1985+ else
1986+ symbol(ctx, x, y, radius, shadow);
1987+ ctx.closePath();
1988+
1989+ if (fillStyle) {
1990+ ctx.fillStyle = fillStyle;
1991+ ctx.fill();
1992+ }
1993+ ctx.stroke();
1994+ }
1995+ }
1996+
1997+ ctx.save();
1998+ ctx.translate(plotOffset.left, plotOffset.top);
1999+
2000+ var lw = series.points.lineWidth,
2001+ sw = series.shadowSize,
2002+ radius = series.points.radius,
2003+ symbol = series.points.symbol;
2004+ if (lw > 0 && sw > 0) {
2005+ // draw shadow in two steps
2006+ var w = sw / 2;
2007+ ctx.lineWidth = w;
2008+ ctx.strokeStyle = "rgba(0,0,0,0.1)";
2009+ plotPoints(series.datapoints, radius, null, w + w/2, true,
2010+ series.xaxis, series.yaxis, symbol);
2011+
2012+ ctx.strokeStyle = "rgba(0,0,0,0.2)";
2013+ plotPoints(series.datapoints, radius, null, w/2, true,
2014+ series.xaxis, series.yaxis, symbol);
2015+ }
2016+
2017+ ctx.lineWidth = lw;
2018+ ctx.strokeStyle = series.color;
2019+ plotPoints(series.datapoints, radius,
2020+ getFillStyle(series.points, series.color), 0, false,
2021+ series.xaxis, series.yaxis, symbol);
2022+ ctx.restore();
2023+ }
2024+
2025+ function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
2026+ var left, right, bottom, top,
2027+ drawLeft, drawRight, drawTop, drawBottom,
2028+ tmp;
2029+
2030+ // in horizontal mode, we start the bar from the left
2031+ // instead of from the bottom so it appears to be
2032+ // horizontal rather than vertical
2033+ if (horizontal) {
2034+ drawBottom = drawRight = drawTop = true;
2035+ drawLeft = false;
2036+ left = b;
2037+ right = x;
2038+ top = y + barLeft;
2039+ bottom = y + barRight;
2040+
2041+ // account for negative bars
2042+ if (right < left) {
2043+ tmp = right;
2044+ right = left;
2045+ left = tmp;
2046+ drawLeft = true;
2047+ drawRight = false;
2048+ }
2049+ }
2050+ else {
2051+ drawLeft = drawRight = drawTop = true;
2052+ drawBottom = false;
2053+ left = x + barLeft;
2054+ right = x + barRight;
2055+ bottom = b;
2056+ top = y;
2057+
2058+ // account for negative bars
2059+ if (top < bottom) {
2060+ tmp = top;
2061+ top = bottom;
2062+ bottom = tmp;
2063+ drawBottom = true;
2064+ drawTop = false;
2065+ }
2066+ }
2067+
2068+ // clip
2069+ if (right < axisx.min || left > axisx.max ||
2070+ top < axisy.min || bottom > axisy.max)
2071+ return;
2072+
2073+ if (left < axisx.min) {
2074+ left = axisx.min;
2075+ drawLeft = false;
2076+ }
2077+
2078+ if (right > axisx.max) {
2079+ right = axisx.max;
2080+ drawRight = false;
2081+ }
2082+
2083+ if (bottom < axisy.min) {
2084+ bottom = axisy.min;
2085+ drawBottom = false;
2086+ }
2087+
2088+ if (top > axisy.max) {
2089+ top = axisy.max;
2090+ drawTop = false;
2091+ }
2092+
2093+ left = axisx.p2c(left);
2094+ bottom = axisy.p2c(bottom);
2095+ right = axisx.p2c(right);
2096+ top = axisy.p2c(top);
2097+
2098+ // fill the bar
2099+ if (fillStyleCallback) {
2100+ c.beginPath();
2101+ c.moveTo(left, bottom);
2102+ c.lineTo(left, top);
2103+ c.lineTo(right, top);
2104+ c.lineTo(right, bottom);
2105+ c.fillStyle = fillStyleCallback(bottom, top);
2106+ c.fill();
2107+ }
2108+
2109+ // draw outline
2110+ if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
2111+ c.beginPath();
2112+
2113+ // FIXME: inline moveTo is buggy with excanvas
2114+ c.moveTo(left, bottom + offset);
2115+ if (drawLeft)
2116+ c.lineTo(left, top + offset);
2117+ else
2118+ c.moveTo(left, top + offset);
2119+ if (drawTop)
2120+ c.lineTo(right, top + offset);
2121+ else
2122+ c.moveTo(right, top + offset);
2123+ if (drawRight)
2124+ c.lineTo(right, bottom + offset);
2125+ else
2126+ c.moveTo(right, bottom + offset);
2127+ if (drawBottom)
2128+ c.lineTo(left, bottom + offset);
2129+ else
2130+ c.moveTo(left, bottom + offset);
2131+ c.stroke();
2132+ }
2133+ }
2134+
2135+ function drawSeriesBars(series) {
2136+ function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
2137+ var points = datapoints.points, ps = datapoints.pointsize;
2138+
2139+ for (var i = 0; i < points.length; i += ps) {
2140+ if (points[i] == null)
2141+ continue;
2142+ drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
2143+ }
2144+ }
2145+
2146+ ctx.save();
2147+ ctx.translate(plotOffset.left, plotOffset.top);
2148+
2149+ // FIXME: figure out a way to add shadows (for instance along the right edge)
2150+ ctx.lineWidth = series.bars.lineWidth;
2151+ ctx.strokeStyle = series.color;
2152+ var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
2153+ var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
2154+ plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
2155+ ctx.restore();
2156+ }
2157+
2158+ function getFillStyle(filloptions, seriesColor, bottom, top) {
2159+ var fill = filloptions.fill;
2160+ if (!fill)
2161+ return null;
2162+
2163+ if (filloptions.fillColor)
2164+ return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
2165+
2166+ var c = $.color.parse(seriesColor);
2167+ c.a = typeof fill == "number" ? fill : 0.4;
2168+ c.normalize();
2169+ return c.toString();
2170+ }
2171+
2172+ function insertLegend() {
2173+ placeholder.find(".legend").remove();
2174+
2175+ if (!options.legend.show)
2176+ return;
2177+
2178+ var fragments = [], rowStarted = false,
2179+ lf = options.legend.labelFormatter, s, label;
2180+ for (var i = 0; i < series.length; ++i) {
2181+ s = series[i];
2182+ label = s.label;
2183+ if (!label)
2184+ continue;
2185+
2186+ if (i % options.legend.noColumns == 0) {
2187+ if (rowStarted)
2188+ fragments.push('</tr>');
2189+ fragments.push('<tr>');
2190+ rowStarted = true;
2191+ }
2192+
2193+ if (lf)
2194+ label = lf(label, s);
2195+
2196+ fragments.push(
2197+ '<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>' +
2198+ '<td class="legendLabel">' + label + '</td>');
2199+ }
2200+ if (rowStarted)
2201+ fragments.push('</tr>');
2202+
2203+ if (fragments.length == 0)
2204+ return;
2205+
2206+ var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
2207+ if (options.legend.container != null)
2208+ $(options.legend.container).html(table);
2209+ else {
2210+ var pos = "",
2211+ p = options.legend.position,
2212+ m = options.legend.margin;
2213+ if (m[0] == null)
2214+ m = [m, m];
2215+ if (p.charAt(0) == "n")
2216+ pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
2217+ else if (p.charAt(0) == "s")
2218+ pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
2219+ if (p.charAt(1) == "e")
2220+ pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
2221+ else if (p.charAt(1) == "w")
2222+ pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
2223+ var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
2224+ if (options.legend.backgroundOpacity != 0.0) {
2225+ // put in the transparent background
2226+ // separately to avoid blended labels and
2227+ // label boxes
2228+ var c = options.legend.backgroundColor;
2229+ if (c == null) {
2230+ c = options.grid.backgroundColor;
2231+ if (c && typeof c == "string")
2232+ c = $.color.parse(c);
2233+ else
2234+ c = $.color.extract(legend, 'background-color');
2235+ c.a = 1;
2236+ c = c.toString();
2237+ }
2238+ var div = legend.children();
2239+ $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
2240+ }
2241+ }
2242+ }
2243+
2244+
2245+ // interactive features
2246+
2247+ var highlights = [],
2248+ redrawTimeout = null;
2249+
2250+ // returns the data item the mouse is over, or null if none is found
2251+ function findNearbyItem(mouseX, mouseY, seriesFilter) {
2252+ var maxDistance = options.grid.mouseActiveRadius,
2253+ smallestDistance = maxDistance * maxDistance + 1,
2254+ item = null, foundPoint = false, i, j;
2255+
2256+ for (i = series.length - 1; i >= 0; --i) {
2257+ if (!seriesFilter(series[i]))
2258+ continue;
2259+
2260+ var s = series[i],
2261+ axisx = s.xaxis,
2262+ axisy = s.yaxis,
2263+ points = s.datapoints.points,
2264+ ps = s.datapoints.pointsize,
2265+ mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
2266+ my = axisy.c2p(mouseY),
2267+ maxx = maxDistance / axisx.scale,
2268+ maxy = maxDistance / axisy.scale;
2269+
2270+ // with inverse transforms, we can't use the maxx/maxy
2271+ // optimization, sadly
2272+ if (axisx.options.inverseTransform)
2273+ maxx = Number.MAX_VALUE;
2274+ if (axisy.options.inverseTransform)
2275+ maxy = Number.MAX_VALUE;
2276+
2277+ if (s.lines.show || s.points.show) {
2278+ for (j = 0; j < points.length; j += ps) {
2279+ var x = points[j], y = points[j + 1];
2280+ if (x == null)
2281+ continue;
2282+
2283+ // For points and lines, the cursor must be within a
2284+ // certain distance to the data point
2285+ if (x - mx > maxx || x - mx < -maxx ||
2286+ y - my > maxy || y - my < -maxy)
2287+ continue;
2288+
2289+ // We have to calculate distances in pixels, not in
2290+ // data units, because the scales of the axes may be different
2291+ var dx = Math.abs(axisx.p2c(x) - mouseX),
2292+ dy = Math.abs(axisy.p2c(y) - mouseY),
2293+ dist = dx * dx + dy * dy; // we save the sqrt
2294+
2295+ // use <= to ensure last point takes precedence
2296+ // (last generally means on top of)
2297+ if (dist < smallestDistance) {
2298+ smallestDistance = dist;
2299+ item = [i, j / ps];
2300+ }
2301+ }
2302+ }
2303+
2304+ if (s.bars.show && !item) { // no other point can be nearby
2305+ var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
2306+ barRight = barLeft + s.bars.barWidth;
2307+
2308+ for (j = 0; j < points.length; j += ps) {
2309+ var x = points[j], y = points[j + 1], b = points[j + 2];
2310+ if (x == null)
2311+ continue;
2312+
2313+ // for a bar graph, the cursor must be inside the bar
2314+ if (series[i].bars.horizontal ?
2315+ (mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
2316+ my >= y + barLeft && my <= y + barRight) :
2317+ (mx >= x + barLeft && mx <= x + barRight &&
2318+ my >= Math.min(b, y) && my <= Math.max(b, y)))
2319+ item = [i, j / ps];
2320+ }
2321+ }
2322+ }
2323+
2324+ if (item) {
2325+ i = item[0];
2326+ j = item[1];
2327+ ps = series[i].datapoints.pointsize;
2328+
2329+ return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
2330+ dataIndex: j,
2331+ series: series[i],
2332+ seriesIndex: i };
2333+ }
2334+
2335+ return null;
2336+ }
2337+
2338+ function onMouseMove(e) {
2339+ if (options.grid.hoverable)
2340+ triggerClickHoverEvent("plothover", e,
2341+ function (s) { return s["hoverable"] != false; });
2342+ }
2343+
2344+ function onMouseLeave(e) {
2345+ if (options.grid.hoverable)
2346+ triggerClickHoverEvent("plothover", e,
2347+ function (s) { return false; });
2348+ }
2349+
2350+ function onClick(e) {
2351+ triggerClickHoverEvent("plotclick", e,
2352+ function (s) { return s["clickable"] != false; });
2353+ }
2354+
2355+ // trigger click or hover event (they send the same parameters
2356+ // so we share their code)
2357+ function triggerClickHoverEvent(eventname, event, seriesFilter) {
2358+ var offset = eventHolder.offset(),
2359+ canvasX = event.pageX - offset.left - plotOffset.left,
2360+ canvasY = event.pageY - offset.top - plotOffset.top,
2361+ pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
2362+
2363+ pos.pageX = event.pageX;
2364+ pos.pageY = event.pageY;
2365+
2366+ var item = findNearbyItem(canvasX, canvasY, seriesFilter);
2367+
2368+ if (item) {
2369+ // fill in mouse pos for any listeners out there
2370+ item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
2371+ item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
2372+ }
2373+
2374+ if (options.grid.autoHighlight) {
2375+ // clear auto-highlights
2376+ for (var i = 0; i < highlights.length; ++i) {
2377+ var h = highlights[i];
2378+ if (h.auto == eventname &&
2379+ !(item && h.series == item.series &&
2380+ h.point[0] == item.datapoint[0] &&
2381+ h.point[1] == item.datapoint[1]))
2382+ unhighlight(h.series, h.point);
2383+ }
2384+
2385+ if (item)
2386+ highlight(item.series, item.datapoint, eventname);
2387+ }
2388+
2389+ placeholder.trigger(eventname, [ pos, item ]);
2390+ }
2391+
2392+ function triggerRedrawOverlay() {
2393+ if (!redrawTimeout)
2394+ redrawTimeout = setTimeout(drawOverlay, 30);
2395+ }
2396+
2397+ function drawOverlay() {
2398+ redrawTimeout = null;
2399+
2400+ // draw highlights
2401+ octx.save();
2402+ octx.clearRect(0, 0, canvasWidth, canvasHeight);
2403+ octx.translate(plotOffset.left, plotOffset.top);
2404+
2405+ var i, hi;
2406+ for (i = 0; i < highlights.length; ++i) {
2407+ hi = highlights[i];
2408+
2409+ if (hi.series.bars.show)
2410+ drawBarHighlight(hi.series, hi.point);
2411+ else
2412+ drawPointHighlight(hi.series, hi.point);
2413+ }
2414+ octx.restore();
2415+
2416+ executeHooks(hooks.drawOverlay, [octx]);
2417+ }
2418+
2419+ function highlight(s, point, auto) {
2420+ if (typeof s == "number")
2421+ s = series[s];
2422+
2423+ if (typeof point == "number") {
2424+ var ps = s.datapoints.pointsize;
2425+ point = s.datapoints.points.slice(ps * point, ps * (point + 1));
2426+ }
2427+
2428+ var i = indexOfHighlight(s, point);
2429+ if (i == -1) {
2430+ highlights.push({ series: s, point: point, auto: auto });
2431+
2432+ triggerRedrawOverlay();
2433+ }
2434+ else if (!auto)
2435+ highlights[i].auto = false;
2436+ }
2437+
2438+ function unhighlight(s, point) {
2439+ if (s == null && point == null) {
2440+ highlights = [];
2441+ triggerRedrawOverlay();
2442+ }
2443+
2444+ if (typeof s == "number")
2445+ s = series[s];
2446+
2447+ if (typeof point == "number")
2448+ point = s.data[point];
2449+
2450+ var i = indexOfHighlight(s, point);
2451+ if (i != -1) {
2452+ highlights.splice(i, 1);
2453+
2454+ triggerRedrawOverlay();
2455+ }
2456+ }
2457+
2458+ function indexOfHighlight(s, p) {
2459+ for (var i = 0; i < highlights.length; ++i) {
2460+ var h = highlights[i];
2461+ if (h.series == s && h.point[0] == p[0]
2462+ && h.point[1] == p[1])
2463+ return i;
2464+ }
2465+ return -1;
2466+ }
2467+
2468+ function drawPointHighlight(series, point) {
2469+ var x = point[0], y = point[1],
2470+ axisx = series.xaxis, axisy = series.yaxis;
2471+
2472+ if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
2473+ return;
2474+
2475+ var pointRadius = series.points.radius + series.points.lineWidth / 2;
2476+ octx.lineWidth = pointRadius;
2477+ octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
2478+ var radius = 1.5 * pointRadius,
2479+ x = axisx.p2c(x),
2480+ y = axisy.p2c(y);
2481+
2482+ octx.beginPath();
2483+ if (series.points.symbol == "circle")
2484+ octx.arc(x, y, radius, 0, 2 * Math.PI, false);
2485+ else
2486+ series.points.symbol(octx, x, y, radius, false);
2487+ octx.closePath();
2488+ octx.stroke();
2489+ }
2490+
2491+ function drawBarHighlight(series, point) {
2492+ octx.lineWidth = series.bars.lineWidth;
2493+ octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
2494+ var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
2495+ var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
2496+ drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
2497+ 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
2498+ }
2499+
2500+ function getColorOrGradient(spec, bottom, top, defaultColor) {
2501+ if (typeof spec == "string")
2502+ return spec;
2503+ else {
2504+ // assume this is a gradient spec; IE currently only
2505+ // supports a simple vertical gradient properly, so that's
2506+ // what we support too
2507+ var gradient = ctx.createLinearGradient(0, top, 0, bottom);
2508+
2509+ for (var i = 0, l = spec.colors.length; i < l; ++i) {
2510+ var c = spec.colors[i];
2511+ if (typeof c != "string") {
2512+ var co = $.color.parse(defaultColor);
2513+ if (c.brightness != null)
2514+ co = co.scale('rgb', c.brightness)
2515+ if (c.opacity != null)
2516+ co.a *= c.opacity;
2517+ c = co.toString();
2518+ }
2519+ gradient.addColorStop(i / (l - 1), c);
2520+ }
2521+
2522+ return gradient;
2523+ }
2524+ }
2525+ }
2526+
2527+ $.plot = function(placeholder, data, options) {
2528+ //var t0 = new Date();
2529+ var plot = new Plot($(placeholder), data, options, $.plot.plugins);
2530+ //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
2531+ return plot;
2532+ };
2533+
2534+ $.plot.version = "0.7";
2535+
2536+ $.plot.plugins = [];
2537+
2538+ // returns a string with the date d formatted according to fmt
2539+ $.plot.formatDate = function(d, fmt, monthNames) {
2540+ var leftPad = function(n) {
2541+ n = "" + n;
2542+ return n.length == 1 ? "0" + n : n;
2543+ };
2544+
2545+ var r = [];
2546+ var escape = false, padNext = false;
2547+ var hours = d.getUTCHours();
2548+ var isAM = hours < 12;
2549+ if (monthNames == null)
2550+ monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
2551+
2552+ if (fmt.search(/%p|%P/) != -1) {
2553+ if (hours > 12) {
2554+ hours = hours - 12;
2555+ } else if (hours == 0) {
2556+ hours = 12;
2557+ }
2558+ }
2559+ for (var i = 0; i < fmt.length; ++i) {
2560+ var c = fmt.charAt(i);
2561+
2562+ if (escape) {
2563+ switch (c) {
2564+ case 'h': c = "" + hours; break;
2565+ case 'H': c = leftPad(hours); break;
2566+ case 'M': c = leftPad(d.getUTCMinutes()); break;
2567+ case 'S': c = leftPad(d.getUTCSeconds()); break;
2568+ case 'd': c = "" + d.getUTCDate(); break;
2569+ case 'm': c = "" + (d.getUTCMonth() + 1); break;
2570+ case 'y': c = "" + d.getUTCFullYear(); break;
2571+ case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
2572+ case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
2573+ case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
2574+ case '0': c = ""; padNext = true; break;
2575+ }
2576+ if (c && padNext) {
2577+ c = leftPad(c);
2578+ padNext = false;
2579+ }
2580+ r.push(c);
2581+ if (!padNext)
2582+ escape = false;
2583+ }
2584+ else {
2585+ if (c == "%")
2586+ escape = true;
2587+ else
2588+ r.push(c);
2589+ }
2590+ }
2591+ return r.join("");
2592+ };
2593+
2594+ // round to nearby lower multiple of base
2595+ function floorInBase(n, base) {
2596+ return base * Math.floor(n / base);
2597+ }
2598+
2599+})(jQuery);
--- /dev/null
+++ b/Allura/allura/public/nf/js/stats.js
@@ -0,0 +1,97 @@
1+/*global jQuery, $, addCommas */
2+jQuery(function($) {
3+ // date range picker
4+ if ($('.picker input').length) {
5+ $('.picker input').daterangepicker({
6+ onOpen: function() {
7+ $('.picker input')[0].prev_value = $('.picker input').val();
8+ },
9+ onClose: function() {
10+ if ($('.picker input')[0].prev_value !== $('.picker input').val()) {
11+ $('.picker input').parents('form').submit();
12+ //console.log('close',$('.picker input').val());
13+ }
14+ },
15+ rangeSplitter: 'to',
16+ dateFormat: 'yy-mm-dd', // yy is 4 digit
17+ earliestDate: new Date($('.picker input').attr('data-start-date')),
18+ latestDate: new Date()
19+ });
20+ }
21+});
22+
23+function chartProjectStats(url, params, series, checkEmpty, tooltipFormat){
24+ var holder = $('#stats-viz');
25+ var dates = $('#dates').val().split(' to ');
26+ var begin = Date.parse(dates[0]).setTimezoneOffset(0);
27+ /* Use slice(-1) to get end date, so that if there is no explicit end
28+ * date, end date will be the same as begin date (instead of null).
29+ */
30+ var end = Date.parse(dates.slice(-1)[0]).setTimezoneOffset(0);
31+ params.begin = dates[0];
32+ params.end = dates.slice(-1)[0];
33+ if (end >= begin){
34+ $.get(url, params, function(resp) {
35+ if (checkEmpty(resp.data)) {
36+ holder.html('<p>No results found for the parameters you have selected.</p>');
37+ } else {
38+ var number_formatter = function(val, axis) {
39+ if (val > 1000) {
40+ return addCommas(val / 1000) + 'k';
41+ }
42+ return val;
43+ },
44+ chart = $.plot($('#project_stats_holder'), series(resp.data), {
45+ colors: ['#0685c6','#87c706','#c7c706','#c76606'],
46+ xaxis:{
47+ mode: "time",
48+ timeformat: "%y-%0m-%0d",
49+ minTickSize: [1, "day"],
50+ min: begin,
51+ max: end,
52+ color: '#484848'},
53+ yaxis:{
54+ tickDecimals: 0,
55+ min: 0,
56+ tickFormatter: number_formatter
57+ },
58+ grid: { hoverable: true, color: '#aaa' },
59+ legend: {
60+ show: true,
61+ margin: 10,
62+ backgroundOpacity: 0.5
63+ }
64+ });
65+ chart.draw();
66+ }
67+ });
68+ }
69+ else{
70+ holder.html('<p>The date range you have chosen is not valid.</p>');
71+ }
72+ $(".busy").hide();
73+
74+ var previousPoint = null;
75+ holder.bind("plothover", function (event, pos, item) {
76+ if (item) {
77+ if (previousPoint !== item.dataIndex) {
78+ previousPoint = item.dataIndex;
79+
80+ $("#tooltip").remove();
81+ var x = item.datapoint[0].toFixed(0),
82+ y = item.datapoint[1].toFixed(0);
83+
84+ $('<div id="tooltip" class="tooltip">' + tooltipFormat(x,y,item) + '</div>').css( {
85+ position: 'absolute',
86+ display: 'none',
87+ top: item.pageY + 5,
88+ left: item.pageX + 5
89+ }).appendTo("body").fadeIn(200);
90+ }
91+ }
92+ else {
93+ $("#tooltip").remove();
94+ previousPoint = null;
95+ }
96+ });
97+}
--- a/ForgeTracker/forgetracker/templates/tracker/stats.html
+++ b/ForgeTracker/forgetracker/templates/tracker/stats.html
@@ -24,17 +24,60 @@
2424 <li>14 days: {{fortnight_comments}}</li>
2525 <li>30 days: {{month_comments}}</li>
2626 </ul>
27-<!--
28-<p># of ticket changes in the last...</p>
29-<ul>
30-<li>7 days:</li>
31-<li>14 days:</li>
32-<li>30 days:</li>
33-</ul>
34--->
35-{% endblock %}
36-
37-
38-
27+{% if show_stats %}
28+<h2>Open and closed tickets over time</h2>
29+<form class="bp" action="{{request.path_url}}">
30+ <div id="stats_date_picker">
31+ <label for="dates">Date Range: </label>
32+ <input value="{{dates}}" type="text" class="text ui-corner-all" name="dates" id="dates">
33+ </div>
34+</form>
3935
36+<div id="stats-viz-container" class="project_stats">
37+ <div id="stats-viz" class="ui-corner-left ui-corner-br">
38+ <table>
39+ <tr>
40+ <td class="yaxis">Tickets</td>
41+ <td>
42+ <div id="project_stats_holder">
43+ <div id="grid">
44+ <div class="busy"></div>
45+ </div>
46+ </div>
47+ </td>
48+ </tr>
49+ <tr>
50+ <td colspan="2" class="xaxis">Date</td>
51+ </tr>
52+ </table>
53+ </div>
54+</div>
55+{% endif %}
56+{% endblock %}
57+{% block extra_js %}
58+{% if show_stats %}
59+<script type="text/javascript" src="{{g.forge_static('js/jquery.flot.js')}}"></script>
60+<script type="text/javascript" src="{{g.forge_static('js/jquery.daterangepicker.js')}}"></script>
61+<script type="text/javascript" src="{{g.forge_static('js/stats.js')}}"></script>
62+<script type="text/javascript">
63+ /*global chartProjectStats */
64+ $(document).ready(function () {
65+ var series = function(data){
66+ return [{label: "Opened", lines: {show: true, lineWidth: 3}, points: {show:true, radius:2, fill: true, fillColor: '#0685c6'}, data: data.opened, shadowSize: 0},
67+ {label: "Closed", lines: {show: true, lineWidth: 3}, points: {show:true, radius:2, fill: true, fillColor: '#87c706'}, data: data.closed, shadowSize: 0}];
68+ };
69+ var checkEmpty = function(data){
70+ return !data.opened && !data.closed;
71+ };
72+ var tooltipFormat = function(x,y,item){
73+ return y + " tickets";
74+ };
75+ chartProjectStats('{{c.app.url}}stats_data',{},series,checkEmpty,tooltipFormat);
4076
77+ $('#dates').change(function(){
78+ $("form.bp").submit();
79+ });
80+ });
81+</script>
82+{% endif %}
83+{% endblock %}
\ No newline at end of file
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -3,11 +3,13 @@ import logging
33 import re
44 from datetime import datetime, timedelta
55 from urllib import urlencode, unquote
6+from urllib2 import urlopen
67 from webob import exc
8+import json
79
810 # Non-stdlib imports
911 import pkg_resources
10-from tg import expose, validate, redirect, flash, url
12+from tg import expose, validate, redirect, flash, url, config
1113 from tg.decorators import with_trailing_slash, without_trailing_slash
1214 from pylons import g, c, request, response
1315 from formencode import validators
@@ -29,6 +31,7 @@ from allura.lib import widgets as w
2931 from allura.lib import validators as V
3032 from allura.lib.widgets import form_fields as ffw
3133 from allura.lib.widgets.subscriptions import SubscribeForm
34+from allura.lib.zarkov_helpers import zero_fill_zarkov_result
3235 from allura.controllers import AppDiscussionController, AppDiscussionRestController
3336 from allura.controllers import attachments as ac
3437 from allura.controllers import BaseController
@@ -656,7 +659,7 @@ class RootController(BaseController):
656659
657660 @with_trailing_slash
658661 @expose('jinja:forgetracker:templates/tracker/stats.html')
659- def stats(self):
662+ def stats(self, dates=None, **kw):
660663 globals = c.app.globals
661664 total = TM.Ticket.query.find(dict(app_config_id=c.app.config._id)).count()
662665 open = TM.Ticket.query.find(dict(app_config_id=c.app.config._id,status={'$in': list(globals.set_of_open_status_names)})).count()
@@ -676,6 +679,13 @@ class RootController(BaseController):
676679 fortnight_comments=self.ticket_comments_since(fortnight_ago)
677680 month_comments=self.ticket_comments_since(month_ago)
678681 c.user_select = ffw.ProjectUserSelect()
682+ if dates is None:
683+ today = datetime.utcnow()
684+ dates = "%s to %s" % ((today - timedelta(days=61)).strftime('%Y-%m-%d'), today.strftime('%Y-%m-%d'))
685+ if c.app.config.get_tool_data('sfx', 'group_artifact_id') and config.get('zarkov.webservice_host'):
686+ show_stats = True
687+ else:
688+ show_stats = False
679689 return dict(
680690 now=str(now),
681691 week_ago=str(week_ago),
@@ -691,7 +701,30 @@ class RootController(BaseController):
691701 total=total,
692702 open=open,
693703 closed=closed,
694- globals=globals)
704+ globals=globals,
705+ dates=dates,
706+ show_stats=show_stats)
707+
708+ @expose('json:')
709+ def stats_data(self, begin=None, end=None, **kw):
710+ if c.app.config.get_tool_data('sfx', 'group_artifact_id') and config.get('zarkov.webservice_host'):
711+ if begin is None and end is None:
712+ end_time = datetime.utcnow()
713+ begin_time = (end_time - timedelta(days=61))
714+ end = end_time.strftime('%Y-%m-%d')
715+ begin = begin_time.strftime('%Y-%m-%d')
716+ else:
717+ end_time = datetime.strptime(end,'%Y-%m-%d')
718+ begin_time = datetime.strptime(begin,'%Y-%m-%d')
719+ time_interval = 'date'
720+ if end_time - begin_time > timedelta(days=183):
721+ time_interval = 'month'
722+ q_filter = 'group-tracker-%s/%s/%s/' % (time_interval,c.project.get_tool_data('sfx', 'group_id'),c.app.config.get_tool_data('sfx', 'group_artifact_id'))
723+ params = urlencode({'data': '{"c":"tracker","b":"'+q_filter+begin+'","e":"'+q_filter+end+'"}'})
724+ read_zarkov = json.load(urlopen(config.get('zarkov.webservice_host')+'/q', params))
725+ return zero_fill_zarkov_result(read_zarkov, time_interval, begin, end)
726+ else:
727+ return dict()
695728
696729 @expose()
697730 @validate(W.subscribe_form)