allura
Revision | bd5aaa6f622f2040c7896f53868455b3ace1ac9c (tree) |
---|---|
Zeit | 2012-02-22 03:26:00 |
Autor | Tim Van Steenburgh <tvansteenburgh@geek...> |
Commiter | Tim Van Steenburgh |
Merge branch 'dev' of git://sfi-engr-scm-1/forge into dev
@@ -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 |
@@ -643,9 +643,18 @@ class AppConfig(MappedClass): | ||
643 | 643 | options=FieldProperty(None) |
644 | 644 | project = RelationProperty(Project, via='project_id') |
645 | 645 | discussion = RelationProperty('Discussion', via='discussion_id') |
646 | + tool_data = FieldProperty({str:{str:None}}) # entry point: prefs dict | |
646 | 647 | |
647 | 648 | acl = FieldProperty(ACL()) |
648 | 649 | |
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 | + | |
649 | 658 | def parent_security_context(self): |
650 | 659 | '''ACL processing should terminate at the AppConfig''' |
651 | 660 | return None |
@@ -145,6 +145,11 @@ function attach_form_retry( form ){ | ||
145 | 145 | }); |
146 | 146 | } |
147 | 147 | |
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 | + | |
148 | 153 | $(function(){ |
149 | 154 | // Add notifications for form submission. |
150 | 155 | attach_form_retry('form.can-retry'); |
@@ -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 |
@@ -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); |
@@ -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 | +} |
@@ -24,17 +24,60 @@ | ||
24 | 24 | <li>14 days: {{fortnight_comments}}</li> |
25 | 25 | <li>30 days: {{month_comments}}</li> |
26 | 26 | </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> | |
39 | 35 | |
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); | |
40 | 76 | |
77 | + $('#dates').change(function(){ | |
78 | + $("form.bp").submit(); | |
79 | + }); | |
80 | + }); | |
81 | +</script> | |
82 | +{% endif %} | |
83 | +{% endblock %} | |
\ No newline at end of file |
@@ -3,11 +3,13 @@ import logging | ||
3 | 3 | import re |
4 | 4 | from datetime import datetime, timedelta |
5 | 5 | from urllib import urlencode, unquote |
6 | +from urllib2 import urlopen | |
6 | 7 | from webob import exc |
8 | +import json | |
7 | 9 | |
8 | 10 | # Non-stdlib imports |
9 | 11 | import pkg_resources |
10 | -from tg import expose, validate, redirect, flash, url | |
12 | +from tg import expose, validate, redirect, flash, url, config | |
11 | 13 | from tg.decorators import with_trailing_slash, without_trailing_slash |
12 | 14 | from pylons import g, c, request, response |
13 | 15 | from formencode import validators |
@@ -29,6 +31,7 @@ from allura.lib import widgets as w | ||
29 | 31 | from allura.lib import validators as V |
30 | 32 | from allura.lib.widgets import form_fields as ffw |
31 | 33 | from allura.lib.widgets.subscriptions import SubscribeForm |
34 | +from allura.lib.zarkov_helpers import zero_fill_zarkov_result | |
32 | 35 | from allura.controllers import AppDiscussionController, AppDiscussionRestController |
33 | 36 | from allura.controllers import attachments as ac |
34 | 37 | from allura.controllers import BaseController |
@@ -656,7 +659,7 @@ class RootController(BaseController): | ||
656 | 659 | |
657 | 660 | @with_trailing_slash |
658 | 661 | @expose('jinja:forgetracker:templates/tracker/stats.html') |
659 | - def stats(self): | |
662 | + def stats(self, dates=None, **kw): | |
660 | 663 | globals = c.app.globals |
661 | 664 | total = TM.Ticket.query.find(dict(app_config_id=c.app.config._id)).count() |
662 | 665 | 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): | ||
676 | 679 | fortnight_comments=self.ticket_comments_since(fortnight_ago) |
677 | 680 | month_comments=self.ticket_comments_since(month_ago) |
678 | 681 | 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 | |
679 | 689 | return dict( |
680 | 690 | now=str(now), |
681 | 691 | week_ago=str(week_ago), |
@@ -691,7 +701,30 @@ class RootController(BaseController): | ||
691 | 701 | total=total, |
692 | 702 | open=open, |
693 | 703 | 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() | |
695 | 728 | |
696 | 729 | @expose() |
697 | 730 | @validate(W.subscribe_form) |