Changeset 3276
- Timestamp:
- 02/26/08 15:19:05 (11 months ago)
- Files:
-
- timevisualizerplugin/tags/TimeVisualizer_0.6 (copied) (copied from timevisualizerplugin/tags/TimeVisualizer_0.5)
- timevisualizerplugin/tags/TimeVisualizer_0.6/release_notes.txt (modified) (2 diffs)
- timevisualizerplugin/tags/TimeVisualizer_0.6/tractimevisualizerplugin/impl.py (modified) (15 diffs)
- timevisualizerplugin/tags/TimeVisualizer_0.6/tractimevisualizerplugin/__init__.py (modified) (1 diff)
- timevisualizerplugin/tags/TimeVisualizer_0.6/tractimevisualizerplugin/pluginwrapper.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
timevisualizerplugin/tags/TimeVisualizer_0.6/release_notes.txt
r2871 r3276 1 1 2 TracTimeVisualizerPlugin 2 TracTimeVisualizerPlugin v. 0.6 3 3 -------------------------------------------------------------------------------- 4 4 5 This is a plugin to render burn down graphs from ticket history information. 5 This is a Trac plugin to render burn down graphs from ticket history 6 information. 6 7 8 9 Usage 10 -------------------------------------------------------------------------------- 11 12 1. uninstall previous installation if such exist, e.g. remove 13 `./TracTimeVisualizerPlugin-0.5-py2.4.egg` from 14 `/usr/lib/python2.4/site-packages/easy-install.pth` 15 16 2. stop web server, e.g. `apache -k stop` 17 18 3. Download & install latest plugin:: 19 20 svn co http://trac-hacks.org/svn/timevisualizerplugin/tags/TimeVisualizer_0.6 21 cd TimeVisualizer_0.6 22 python setup.py install 23 24 4. congigure custom fields to trac.ini, e.g.:: 25 26 [ticket-custom] 27 workleft = text 28 workleft.label = WL 29 workleft.order = 0 30 workleft.value = 0 31 32 5. define to trac.ini, where from graph is rendered:: 33 34 [timevisualizer] 35 calc_fields = workleft 36 37 or make timing and estimation plugin compatible:: 38 39 [timevisualizer] 40 calc_fields = estimatedhours-totalhours 41 42 6. enable plugin component in trac.ini:: 43 44 [components] 45 tractimevisualizerplugin.* = enabled 46 47 7. Start your web server, e.g. `apache2 -k start` 48 49 Using ISO 8601 format 50 ~~~~~~~~~~~~~~~~~~~~~ 51 52 To enable ISO 8601 dates, times & timedeltas, configure trac.ini:: 53 54 [timevisualizer] 55 time_format = iso8601 56 57 Format can be also set in the actual query. Then just use new formats, e.g. 58 burndown from 2007 2nd half using one week time delta 59 60 time_format=iso8601&datestart=2007-07&dateend=2008-01&timeinterval=7D 61 62 Dates shall be passed in form YYYY-MM-DDThh:mm:ss. Components can be left 63 out from right side (but not from left). For example 2008 is same as 64 `2008-01-01T00:00:00Z` 65 66 Timedelta shall be passed in form xxYxxMxDTxxHxxMxxS. Any component is allowed, 67 e.g. 4 days and 5 seconds period is `4DT5S`. 68 69 Version 0.6 70 -------------------------------------------------------------------------------- 71 72 - Implementation is now Trac 0.11 (dev-r6368) compatible (closes #1956) 73 - Support added for ISO8601 datatime and period formats (closes #2149). All 74 users having users from more than one time zone should migrate burndown macros 75 asap. 76 - Sql routines rewritten => ticket footprint at certain 'revision' may contain 77 all ticket fields, including custom ones! Requires small source tuning 78 before installation though. This is preparation for custom graph data 79 suppliers planned for v.0.7. 80 - Debug logging was simplified 81 - Usage documentation moved to pluginwrapper so that WikiMacros page 82 or `[[MacroList(BurnDown)]]` renders documentation (requires that 83 PythonOptimize disabled is enabled for mod_python). 7 84 8 85 Version 0.5 … … 10 87 11 88 - Timing And Estimation plugin no more required, still compatible by default 12 13 89 - Burndown graph can be now rendered from custom fields through configuration. 14 90 Two options available: timevisualizerplugin/tags/TimeVisualizer_0.6/tractimevisualizerplugin/impl.py
r2871 r3276 1 """This is the actual implementation of SVGRenderer component 2 3 The actual implementation was moved to this implementation file - this way the wrapper can use reload implementation on each request. This is needed when using mod python - another option would be cgi but it is slow. With reloading & mod python the development becomes extremely fast - modify source, save & reload web page. 1 """This file provides actual Burndown renderer implementation 2 3 The actual implementation was moved to this implementation file - this way the 4 wrapper can use reload implementation on each request. This is needed when using 5 mod python - another option would be cgi but it is slow. With reloading & mod 6 python the development becomes extremely fast - modify source, save & reload web 7 page. 4 8 """ 5 9 10 # ============================================================================== 6 11 class NullOut: 7 12 def write(self, data): 8 13 pass 9 14 10 def build_svg(db, options, debug=None): 11 """= Function build_svg = 12 13 This function does all the work. Options for method are: 14 15 * db::trac database instance, usually taken from environment 16 * targetmilestone - only tickets data bount to given milestone name is included 17 * targetcomponent - only ticket data bound to given component name is included 18 * targetticket - only data in given ticket # is included 19 * timeinterval - time interval lines as seconds in graph, 3600 = 1h, 86400 = 1d 20 * timestart - filters out ticket data before this timestamp 21 * timeend - filters out ticket data after this timestamp 22 * datestart - overrides timestart if passed, e.g. '8/14/07' 23 * dateend - overrides timeend when passed - e.g. '8/20/07' 24 * hidedates - any non empty string causes start and end times not to be rendered to the graph 25 * hidehours - any non empty string causes hours not to be rendered to the graph 26 27 Note that filtering tickets that are not belonging to any milestone or component is not currently possible, because there is no way to indicate 'NULL' milestone or component! 28 """ 29 30 if not debug: 31 debug = NullOut() 32 15 # ============================================================================== 16 import trac 17 from calendar import timegm 18 import time 19 import sys 20 import StringIO 21 import datetime 22 23 #http://en.wikipedia.org/wiki/ISO_8601 24 #http://wiki.python.org/moin/WorkingWithTime 25 #http://seehuhn.de/pages/pdate 26 #http://cheeseshop.python.org/pypi/iso8601/0.1.2 27 #http://www.w3.org/TR/NOTE-datetime 28 #http://hydracen.com/dx/iso8601.htm 29 #http://www.cl.cam.ac.uk/%7emgk25/iso-time.html 30 # 12:00Z = 13:00+01:00 = 0700-0500 31 32 # parse period, e.g. P18Y9M4DT11H9M8S 33 # return seconds. 34 # note: this gives very approximate result, e.g. 1 month is very obfuscating == 365/12 D 35 def parse_iso8601_period(text): 36 result = 0 37 text = text.replace('P','') 38 text = text.replace('T','') 39 text = text.upper() 40 # => 18Y9M4D11H9M8S 41 a = ( 42 'Y', 365 * 24 * 60 * 60, 43 'M', 365 * 24 * 60 * 60 / 12, 44 'W', 7 * 24 * 60 * 60, 45 'D', 24 * 60 * 60, 46 'H', 60 * 60, 47 'M', 60, 48 'S', 1 ) 49 for i in range(0,len(a),2): 50 tmp = text.split(a[i]) 51 if len(tmp) > 1: 52 text = tmp[1] 53 result += int(tmp[0]) * a[i+1] 54 return result 55 56 def parse_iso8601_datetime(text): 57 # todo: expecting zero zone => let user pass in his/her zone? 58 seconds = None 59 text = text.strip() 60 format = '' 61 for append in ('%Y', '-%m', '-%d', ' %H', ':%M', ':%S'): 62 format += append 63 try: 64 date = time.strptime(text, format) 65 seconds = timegm(date) 66 break 67 except ValueError: 68 continue 69 if seconds == None: 70 raise ValueError, "'%s' is not ISO 8601 date format (YYYY-MM-DD hh:mm:ss)." % text 71 return seconds 72 73 def format_iso8601_datetime(secs): 74 return time.strftime('%Y-%m-%d %H:%M:%SZ',time.gmtime(secs)) 75 76 # this wrapper is needed for trac.util.parse_date, cos trac 0.10 does return seconds but 0.11 returns datetime object 77 def trac_parse_date(obj): 78 result = trac.util.parse_date(obj) 79 if isinstance(result,datetime.datetime): 80 result = timegm(result.timetuple()) 81 return result 82 83 # ============================================================================== 84 def build_svg(db, options): 85 """trac db instance (usually taken from environment) is mandatory.""" 33 86 def strtotype(val, type): 34 87 if isinstance(val, str) or isinstance(val, unicode): … … 36 89 return val 37 90 91 # todo: take filters as functions => let caller to build them 38 92 targetmilestone=options.get('targetmilestone', None) 39 93 targetticket=strtotype(options.get('targetticket', None), int) 40 94 targetcomponent=options.get('targetcomponent', None) 41 time_interval=strtotype(options.get('timeinterval', 3600*24), int)42 95 timestart=strtotype(options.get('timestart', 0), int) 43 96 timeend=strtotype(options.get('timeend',0), int) … … 49 102 calc_fields = calc_fields_str.split('-') 50 103 51 print>>debug, "********************** serving ***********************" 52 53 import trac 104 print "********************** serving ***********************" 105 format_datetime = trac.util.format_datetime 106 parse_date = trac_parse_date 107 parse_interval = int 108 109 if options.get('time_format') == 'iso8601': 110 format_datetime = format_iso8601_datetime 111 parse_date = parse_iso8601_datetime 112 parse_interval = parse_iso8601_period 113 114 time_interval=strtotype(options.get('timeinterval', 86400), parse_interval) 115 assert time_interval > 0, "'timeinterval' must be at least one second, now %d" % time_interval 116 54 117 # override timestart if datestart given 55 118 try: 56 if datestart: timestart = trac.util.parse_date(datestart)119 if datestart: timestart = parse_date(datestart) 57 120 except Exception: 58 raise Exception, "invalid startdate: '%s', expected format: '%s'" % (str(datestart), trac.util.get_date_format_hint())121 raise Exception, "invalid startdate: " + str(sys.exc_info()[1]) 59 122 60 123 # override timeend if dateend given 61 124 try: 62 if dateend: timeend = trac.util.parse_date(dateend)125 if dateend: timeend = parse_date(dateend) 63 126 except Exception: 64 raise Exception, "invalid dateend: '%s', expected format: '%s'" % (str(dateend), trac.util.get_date_format_hint())65 66 # this dict stores tickets. Tickets are updated on each 'revision' visited - so this is somekinf of snapshot of ticket data at given time127 raise Exception, "invalid dateend: " + str(sys.exc_info()[1]) 128 129 # this dict stores tickets. Tickets are updated on each 'revision' visited - so this contains snapshot of tickets at certain time 67 130 tickets = {} 68 69 def calc_hours(tickets,max_time):70 """Calculates totalhours of tickets on current tickets state taking filters in the account."""71 result = 0.072 for ticket in tickets.values():73 if targetmilestone and ticket['milestone'] != targetmilestone: continue74 if targetticket and ticket['ticket'] != targetticket: continue75 if targetcomponent and ticket['component'] != targetcomponent: continue76 if len(calc_fields) == 2:77 result += ticket[calc_fields[0]] - ticket[calc_fields[1]]78 else:79 result += ticket[calc_fields[0]]80 #print>>debug, ticket81 return result82 131 83 132 def tofloat(obj): … … 91 140 return float(obj) #fallback 92 141 142 def calc_hours(tickets): 143 """Calculates totalhours of tickets on current tickets state taking filters in the account.""" 144 result = 0.0 145 for ticket in tickets.values(): 146 if targetmilestone and ticket['milestone'] != targetmilestone: continue 147 if targetticket and ticket['id'] != targetticket: continue 148 if targetcomponent and ticket['component'] != targetcomponent: continue 149 if len(calc_fields) == 2: 150 result += tofloat(ticket[calc_fields[0]]) - tofloat(ticket[calc_fields[1]]) 151 else: 152 result += tofloat(ticket[calc_fields[0]]) 153 return result 154 93 155 cursor = db.cursor() 94 156 … … 97 159 # ---------------------------------------------------- 98 160 99 if len(calc_fields) == 2: 100 sql = """ 101 SELECT t.id, t.time, t.status, t.milestone, est.value, th.value, t.component 102 FROM ticket t 103 LEFT OUTER JOIN ticket_custom est ON (t.id = est.ticket AND est.name = '%s') 104 LEFT OUTER JOIN ticket_custom th ON (t.id = th.ticket AND th.name = '%s') 105 ORDER BY t.id 106 """ % (calc_fields[0], calc_fields[1]) 107 108 cursor.execute(sql) 109 for (ticket,time, status,milestone,estimatedhours,totalhours,component) in cursor.fetchall(): 110 tickets[ticket] = {'ticket':ticket, 111 'time':time, 112 'status':status, 113 'milestone':milestone, 114 calc_fields[0]:tofloat(estimatedhours), 115 calc_fields[1]:tofloat(totalhours), 116 'component':component} 117 else: 118 sql = """ 119 SELECT t.id, t.time, t.status, t.milestone, cust.value, t.component 120 FROM ticket t 121 LEFT OUTER JOIN ticket_custom cust ON (t.id = cust.ticket AND cust.name = '%s') 122 ORDER BY t.id 123 """ % (calc_fields[0]) 124 125 cursor.execute(sql) 126 for (ticket,time, status,milestone,workleft,component) in cursor.fetchall(): 127 tickets[ticket] = {'ticket':ticket, 128 'time':time, 129 'status':status, 130 'milestone':milestone, 131 calc_fields[0]:tofloat(workleft), 132 'component':component} 161 ticket_fields = ( 162 'id', 163 # 'type', 164 # 'time', # created 165 # 'changetime', # modified 166 'component', 167 # 'severity', 168 # 'priority', 169 # 'owner', 170 # 'reporter', 171 # 'cc', 172 # 'version', 173 'milestone', 174 # 'status', 175 # 'resolution', 176 # 'summary', 177 # 'description', 178 # 'keywords' 179 ) 180 181 # todo: fetch from db/env 182 custom_ticket_fields = calc_fields 183 184 sql = StringIO.StringIO() 185 sql.write('SELECT \n') 186 for i in range(len(ticket_fields)): 187 if i>0 : sql.write(',\n') 188 sql.write(' t.%s AS %s' % (ticket_fields[i], ticket_fields[i])) 189 190 for i in range(len(custom_ticket_fields)): 191 sql.write(',\n') 192 sql.write(' j%d.value AS %s' % (i, custom_ticket_fields[i])) 193 194 sql.write('\nFROM ticket t\n') 195 for i in range(len(custom_ticket_fields)): 196 sql.write(" LEFT OUTER JOIN ticket_custom j%d ON(t.id=j%d.ticket AND j%d.name='%s')\n" % (i,i,i,custom_ticket_fields[i])) 197 198 sql.write('ORDER BY t.id') 199 #print sql.getvalue() 200 201 cursor.execute(sql.getvalue()) 202 tickets = {} 203 all_fields = [] 204 all_fields += ticket_fields 205 all_fields += custom_ticket_fields 206 it = cursor.fetchall() 207 for row in it: 208 ticket = {} 209 tickets[row[0]] = ticket 210 for i in range(len(all_fields)): 211 ticket[all_fields[i]] = row[i] 133 212 134 213 # ---------------------------------------------------- … … 139 218 result = [] 140 219 def process_time(time): 141 hours = calc_hours(tickets ,time) # todo: remove time from call220 hours = calc_hours(tickets) 142 221 if len(result) >= 2 and result[-1] == hours: 143 222 # update timestamp … … 151 230 del tickets[id] 152 231 232 process_ticket_change_sql = """ 233 SELECT field, oldvalue 234 FROM ticket_change 235 WHERE time=%d AND ticket=%d AND field IN (""" + "'" + "','".join(all_fields) + "'" + """) 236 ORDER BY time desc""" 237 153 238 def process_ticket_change(t, id): 154 sql = """ 155 SELECT ticket, time, field, oldvalue, newvalue 156 FROM ticket_change 157 WHERE time=%d AND ticket=%d 158 ORDER BY time desc""" % (t,id) 239 sql = process_ticket_change_sql % (t,id) 159 240 160 241 cursor.execute(sql) 161 242 data = cursor.fetchall() 162 # print>>debug, "ticket_change data:" 163 #for line in data: 164 # print>>debug, line 165 166 for (ticket,time,field,oldvalue,newvalue) in data: 243 for (field,oldvalue) in data: 167 244 # we iterate backwards, thus we save old values 168 if field == calc_fields[0]: 169 tickets[ticket][calc_fields[0]] = tofloat(oldvalue) 170 elif len(calc_fields) == 0 and field == calc_fields[1]: 171 tickets[ticket][calc_fields[1]] = tofloat(oldvalue) 172 elif field == 'milestone': 173 tickets[ticket]['milestone'] = oldvalue 174 elif field == 'component': 175 tickets[ticket]['component'] = oldvalue 245 tickets[id][field] = oldvalue 176 246 177 247 # -- find out timestamps (revisions) and bind processors for them 178 248 179 249 timestamps = {} 250 251 # creation times 180 252 cursor.execute("SELECT DISTINCT time, id from ticket ORDER BY time") 181 253 for line in cursor.fetchall(): 182 254 timestamps[int(line[0])] = [(process_ticket_create, int(line[1]))] 183 255 256 # modified times 184 257 cursor.execute("SELECT DISTINCT time, ticket from ticket_change ORDER BY time") 185 258 for line in cursor.fetchall(): … … 188 261 timestamps[int(line[0])] = item 189 262 190 # -- process each timestamp from oldest to newest (direction is importat cos we know only the latest time)263 # -- process each timestamp from oldest to newest (direction is importat cos we know only the latest state) 191 264 192 265 tmp = [] + timestamps.keys() … … 194 267 tmp.reverse() # from oldest to youngest 195 268 for t in tmp: 196 # print status after changes first (remember backward iterating)197 269 process_time(t) 198 270 for processor in timestamps[t]: … … 213 285 if len(result) < 4: 214 286 return NO_DATA 215 287 216 288 # last item is zero and timestamp is updated to first ticket creation, so it needs to be 'scaled' to first item where hours change 217 289 result[-2] = result[-4] 218 290 219 #print >>debug, "result=",str(result)291 #print, "result=",str(result) 220 292 221 293 # if start or end times are defined, drop results outside of them … … 252 324 if timeend: 253 325 largesttime = timeend - smallesttime 254 255 256 257 326 258 327 maxhours = 0.1; # there was division by zero if all hours are zero (should not happen anymore though) … … 302 371 if not hidedates: 303 372 # draw graph start & end dates 304 import trac305 373 # http://www.w3.org/TR/SVG11/text.html#AlignmentProperties 306 svg += '<text x="0" y="99%%" text-anchor="start" style="font-family:verdana;">%s</text>' % ( trac.util.format_datetime(smallesttime))307 svg += '<text x="100%%" y="99%%" text-anchor="end" style="font-family:verdana;">%s</text>' % ( trac.util.format_datetime(smallesttime+largesttime))374 svg += '<text x="0" y="99%%" text-anchor="start" style="font-family:verdana;">%s</text>' % (format_datetime(smallesttime)) 375 svg += '<text x="100%%" y="99%%" text-anchor="end" style="font-family:verdana;">%s</text>' % (format_datetime(smallesttime+largesttime)) 308 376 309 377 svg += "</svg>\n" … … 316 384 return build_svg(db, args) 317 385 318 from trac.env import open_environment 319 env = open_environment("c:\\scm\\trac\\visualizerdemo") 386 env = trac.env.open_environment("/var/scm/trac/tvdemo1") 320 387 db = env.get_db_cnx() 321 import sys 322 svg = build_svg_paramlist(db, milestone='mile1', time_interval=3600*24, debug=sys.stdout, datestart="8/1/07", dateend="10/1/07") 323 324 FILE = open("c:\\test.svg", "w") 388 svg = build_svg_paramlist(db, milestone='milestone1', time_interval=3600*24, datestart="8/1/07", dateend="10/1/07") 389 390 FILE = open("test.svg", "w") 325 391 FILE.write(svg) 326 392 FILE.close() … … 328 394 def process_request(plugin, req): 329 395 """Renders a svg graph based on request attributes and returns a http response (or traceback in case of error)""" 330 class MyDebug: 331 out = "" 332 def write(self, data): 333 self.out += data 396 397 old_sys_stdout = sys.stdout 334 398 335 399 import tractimevisualizerplugin 336 debug = None;337 400 if tractimevisualizerplugin.DEVELOPER_MODE: 338 debug = MyDebug() 401 sys.stdout = StringIO.StringIO() 402 else: 403 sys.stdout = NullOut() 339 404 try: 340 #print>>debug, dir(req)341 from trac.web import RequestDone342 from trac.util.datefmt import http_date343 from time import time344 345 405 req.send_response(200) 346 406 req.send_header('Content-Type', "image/svg+xml") 347 req.send_header('Last-Modified', http_date(time()))407 req.send_header('Last-Modified', trac.util.datefmt.http_date(time.time())) 348 408 req.end_headers() 349 409 … … 351 411 db = plugin.env.get_db_cnx() 352 412 args = req.args.copy() 353 args['calc_fields'] = plugin.env.config.get('timevisualizer','calc_fields','estimatedhours-totalhours') 354 req.write(build_svg(db, args, debug)) 355 raise RequestDone 413 if not args.get('calc_fields'): 414 args['calc_fields'] = plugin.env.config.get('timevisualizer','calc_fields','estimatedhours-totalhours') 415 if not args.get('time_format'): 416 args['time_format'] = plugin.env.config.get('timevisualizer','time_format', None) 417 req.write(build_svg(db, args)) 418 raise trac.web.RequestDone 356 419 finally: 357 if debug: 358 plugin.log.debug(debug.out) 420 log = sys.stdout 421 sys.stdout = old_sys_stdout 422 if isinstance(log, StringIO.StringIO): 423 plugin.log.debug(log.getvalue()) timevisualizerplugin/tags/TimeVisualizer_0.6/tractimevisualizerplugin/__init__.py
r2871 r3276 5 5 DEVELOPER_MODE=False 6 6 7 __version__ = '0. 5'7 __version__ = '0.6' 8 8 __url__ = 'http://trac-hacks.org/wiki/TimeVisualizerPlugin' 9 9 __author__ = 'Markus Pelkonen' 10 __copyright__ = 'Copyright (C) 200 7Markus Pelkonen'10 __copyright__ = 'Copyright (C) 2008 Markus Pelkonen' 11 11 __license__ = 'BSD' 12 12 __license_long__ = __copyright__ + """ timevisualizerplugin/tags/TimeVisualizer_0.6/tractimevisualizerplugin/pluginwrapper.py
r2607 r3276 32 32 33 33 class BurnDownMacro(WikiMacroBase): 34 """ Renders iframe sets the content to be svg created by burndownimage.34 """This macro renders iframe and sets to be svg image to be created on the fly based on given filters. 35 35 36 Takes three options: width, height and query. Query is the the format of http query and is passed to SVGRenderer.36 Macro takes three options: width, height and query. 37 37 38 Example macro usage: 38 Query is the the format of http query and is passed to SVGRenderer as is. Query parameters are used as follows: 39 40 To include ticket history from certain milestone/component/or certain ticket, use filters: 41 42 * targetmilestone - only tickets data bound to given milestone name are included 43 * targetcomponent - only ticket data bound to given component name are included 44 * targetticket - only data in given ticket # is included 45 46 To limit burndown to certain time period, use following filters: 47 48 * timestart - filters out ticket data before this timestamp 49 * timeend - filters out ticket data after this timestamp 50 * datestart - overrides timestart if passed, e.g. '8/14/07' 51 * dateend - overrides timeend when passed - e.g. '8/20/07' 52 53 To override L&F of the generated svg: 54 55 * timeinterval - time interval lines (horisontal) as seconds in graph, 3600 = 1h, 86400 = 1 day 56 * hidedates - any non empty string causes start and end times not to be rendered to the graph (X-axis) 57 * hidehours - any non empty string causes hours not to be rendered to the graph (Y-axis) 58 59 To use ISO8601 time in date & time parameters, override with `time_format=iso8601` 60 61 To override which fields is used to calucalte Y-value at certain change, define `calc_fields`, e.g. 62 `calc_fields=workleft` or `calc_fields=estimatedhours-totalhours` 63 64 Few use examples: 65 1. Simplest ever case: all tickets from the whole project history 39 66 {{{ 40 [[BurnDown(width=600,height=200,query=targetmilestone=milestone1&dateend=8/31/07)]] 67 [[BurnDown]] 68 }}} 69 70 1. Example macro usage using old pre 0.6 time format: 71 {{{ 72 [[BurnDown(width=600,height=200,query=targetmilestone=mymilestone&dateend=8/31/07)]] 73 }}} 74 75 2. Example macro usage using ISO 8601 format: 76 {{{ 77 [[BurnDown(width=600,height=200,query=targetmilestone=2007-12&time_format=iso8601&datestart=2007-12&dateend=2008-01&timeinterval=1D)]] 41 78 }}} 42 79 """
