Attachment 'burndown_D3.JS_from_local_file.py'
Download 1 # -*- coding: UTF-8 -*-
2
3 """
4 This file is Free Software under the GNU GPL, Version >=2;
5 and comes with NO WARRANTY!
6
7 Version 1.3
8
9 usage example:
10
11 {{{
12 #!burndown
13 points:180
14 start:2015-03-01
15 end:2015-03-07
16 2015-03-01:180
17 2015-03-02:100
18 2015-03-04:0
19 }}}
20
21 explanation:
22 'points' represents the size of the value of the y-Axis
23 'start' represents the start-point of a project.
24 'end' represents the end point.
25 the other values representing actual measured dates. The format is:
26 <year>-<date>-<month>:<remaining points>
27
28 the 'ideal line' will be plotted from the startpoint to the endpoint
29 the 'actual line' will be plotted over all remaining points
30
31 Initial Version 2015-03-05
32 @copyright: 2015 by Intevation GmbH Osnabrueck
33 @author: Sean Engelhardt <sean.engelhardt@intevation.de>
34 @license: GNU GPLv>=2.
35 """
36
37 import math
38
39 # a class named parser is needed my the moinmo.in plugin interface
40 class Parser:
41 def __init__(self, raw, request, **kw):
42 self.pagename = request.page.page_name
43 self.raw = raw
44 self.request = request
45 self.formatter = request.formatter
46 self.kw = kw
47 self.d3js_source = self.request.cfg.url_prefix_static + "/common/js/d3.v3.min.js"
48 self.chart = self.html_code()
49
50
51 #get month-numbers between 1-12 innstead 0-11
52 def normalize_month_number(self, month):
53 return str(int(month) - 1)
54
55
56 #cut of "start" or end, normalize the month, number and return as well-formed date array for d3.js
57 def refract_parameters(self, parameters):
58 #cut of the "start"
59 date_array = parameters.split(':')
60
61 #save the date as array
62 date_array = date_array[1].split('-')
63
64 date_array[1] = self.normalize_month_number(date_array[1])
65 return date_array
66
67
68 def is_valid_number(self, entry):
69 if math.isnan(int(entry)):
70 # raise ValueError("The entry: '" + entry + "' does not seem to be a proper number!")
71 raise ValueError(entry)
72
73
74 def validate_date_numbers(self, entry):
75 if math.isnan(int(entry[0])) or math.isnan(int(entry[1])) or math.isnan(int(entry[2])):
76 # raise ValueError('The entry: "' + entry + '" does not seem to be a proper date!')
77 raise ValueError(entry)
78
79
80 # the format methode is used by the plugin interface automaticly.
81 # format is also called for each !# command.
82 # the formatter (object) is not documented well. See moinmoin/formatter/base.py
83 # half of the methods raise a 'not implemented' error.
84 # however, the formater object is mostly used to print out lines on the webpage
85 # e.g. formatter.text(plain text) / formatter.rawHTML(htmlcode) / formatter.paragraph(int)
86 def format(self, formatter):
87
88 try:
89
90 parameters = self.raw.split()
91
92 ideal = []
93 actual = []
94 points = ""
95
96 for line in parameters:
97
98 if line.startswith("points"):
99 points = line.split(":")[1]
100 self.is_valid_number(points)
101
102 elif line.startswith("start"):
103
104 splitted_line = self.refract_parameters(line)
105 self.validate_date_numbers(splitted_line)
106
107 ideal.append("{date : new Date(%s), points: %s}" % (", ".join(splitted_line), points))
108
109 elif line.startswith("end"):
110 splitted_line = self.refract_parameters(line)
111 self.validate_date_numbers(splitted_line)
112
113 ideal.append("{date : new Date(%s), points: 0}," % (", ".join(splitted_line)))
114
115 elif line.startswith("20"):
116
117 # get the points
118 points_of_date = line.split(':')[1]
119 self.is_valid_number(points_of_date)
120
121 # Cut the points of the line
122 splitted_line = line.split(':')[0]
123
124 # get the date as array
125 splitted_line = splitted_line.split('-')
126 self.validate_date_numbers(splitted_line)
127
128 splitted_line[1] = self.normalize_month_number(splitted_line[1])
129
130 actual.append("{date : new Date(%s), points : %s}" % (", ".join(splitted_line), points_of_date))
131
132 self.chart = (self.chart.replace("var ideal=[];", "var ideal=[%s];" % (", ".join(ideal),))
133 .replace("var actual=[];", "var actual=[%s];" % (", ".join(actual))))
134
135 # print("REQUESR\n-----\n" + self.request)
136
137 self.request.write(formatter.rawHTML(self.chart))
138
139 except ValueError as err:
140 error_output = """
141 <div style="border:thin solid red;">
142 An Error Occured! Did you used the burn down chart parser in the wrong way? <br />
143 {0}
144 </div>
145 """.format(err)
146 self.request.write(formatter.rawHTML(error_output))
147
148 def html_code(self):
149 compressed_css_code = """
150 .svg div{ font: 10px sans-serif; text-align: right; float: left; display: block; padding: 10px; margin: 10px; color: white; } .axis path, .axis line { fill: none; stroke: black; stroke-width: 1px; } .line { fill: none; stroke-width: 3px; } .line.ideal { stroke: blue; } .line.actual { stroke: red; } .point.ideal { fill: blue; stroke: blue; } .point.actual { fill: red; stroke: red; } .grid .tick { stroke: lightgrey; opacity: 0.7; } .grid path { stroke-width: 0; }
151 """
152 compressed_js_code = """
153 var ideal=[];var actual=[];
154 var yAxisDomain=[];var daysInSprint=function(){return Math.max(dayDifference(getFirstDateInStructure(actual),getLastDateInStructure(actual)),dayDifference(getFirstDateInStructure(ideal),getLastDateInStructure(ideal)))};var pointsInSprint=function(){var a=Math.max(getMaxPointInStructure(actual),getMaxPointInStructure(ideal));var b=Math.min(getMinPointInStructure(actual),getMinPointInStructure(ideal));return(a-b)};function dayDifference(b,a){var c=(a-b)/(1000*60*60*24);if(c>60){c=60}return c}function getMaxPointInStructure(b){var a=0;for(var c=0;c<b.length;c++){if(b[c].points>a){a=b[c].points}}return a}function getMinPointInStructure(a){var c=0;for(var b=0;b<a.length;b++){if(a[b].points<c){c=a[b].points}}return c}function getLastDateInStructure(a){var c=a[0].date;for(var b=0;b<a.length;b++){if(a[b].date>c){c=a[b].date}}return c}function getFirstDateInStructure(a){var c=a[0].date;for(var b=0;b<a.length;b++){if(a[b].date<c){c=a[b].date}}return c}function contentOrPreviewDiv(){if(document.getElementById("preview")){return"#preview"}else{if(document.getElementById("content")){return"#content"}else{return"body"}}}function setPointTickLimit(a){if(pointsInSprint()<a){return pointsInSprint()}else{return a}}function setDateTickLimit(a){if(daysInSprint()<a){return daysInSprint()}else{return a}}function makeChart(){var e={top:10,right:30,bottom:100,left:65},c=800-e.left-e.right,k=600-e.top-e.bottom;var i=d3.time.scale().range([0,c]);var h=d3.scale.linear().range([k,0]);var j=d3.svg.line().x(function(m){return i(m.date)}).y(function(m){return h(m.points)});var l=d3.svg.line().x(function(m){return i(m.date)}).y(function(m){return h(m.points)});yAxisDomain.push(getMinPointInStructure(actual),Math.max(getMaxPointInStructure(actual),getMaxPointInStructure(ideal)));yAxisDomain[1]=function(){if(yAxisDomain[1]<=10){return Math.ceil(yAxisDomain[1]*1.1)}else{if(yAxisDomain[1]<100){return Math.ceil((yAxisDomain[1]+1)/5)*5}else{return Math.ceil((yAxisDomain[1]+1)/10)*10}}}();h.domain(d3.extent(yAxisDomain,function(m){return m}));i.domain(d3.extent(ideal,function(m){return m.date}));var d=d3.svg.axis().scale(i).orient("bottom").ticks(setDateTickLimit(10)).tickFormat(d3.time.format("%m-%d"));var b=d3.svg.axis().scale(h).orient("left").ticks(setPointTickLimit(10));var a=d3.svg.axis().scale(i).orient("bottom").ticks(setDateTickLimit(25));var g=d3.svg.axis().scale(h).orient("left").ticks(setPointTickLimit(25));var f=d3.select(contentOrPreviewDiv()).append("svg").attr("class","svg").attr("width",c+e.left+e.right).attr("height",k+e.top+e.bottom).append("g").attr("transform","translate("+e.left+","+e.top+")");f.append("g").attr("class","grid").attr("transform","translate(0,"+k+")").call(a.tickSize(-k,0,0).tickFormat(""));f.append("g").attr("class","grid").call(g.tickSize(-c,0,0).tickFormat(""));f.append("g").attr("class","x axis").attr("transform","translate(0,"+k+")").call(d).selectAll("text").style("text-anchor","end").attr("dx","-.7em").attr("dy",".2em").attr("transform",function(){return"rotate(-65)"});f.append("g").attr("class","y axis").call(b).append("text").attr("transform","rotate(-90)").attr("y",6).attr("dy",".5em").style("text-anchor","end");f.append("text").attr("x",c/2).attr("y",k+60).attr("dy",".5em").style("text-anchor","middle").text("Date");f.append("text").attr("transform","rotate(-90)").attr("y",10-e.left).attr("x",0-(k/2)).attr("dy","1em").style("text-anchor","middle").text("Points");f.append("path").datum(ideal).attr("class","line ideal").attr("d",j);f.append("path").datum(actual).attr("class","line actual").attr("d",l);f.selectAll("circle.point.ideal").data(ideal).enter().append("circle").attr("cx",function(m){return i(m.date)}).attr("cy",function(m){return h(m.points)}).attr("r",4).attr("class","point ideal");f.selectAll("circle.point.actual").data(actual).enter().append("circle").attr("cx",function(m){return i(m.date)}).attr("cy",function(m){return h(m.points)}).attr("r",4).attr("class","point actual")}makeChart();
155 """
156
157 return """
158 <!DOCTYPE html><html><head><style>{css}</style></head><body><script src="{d3js}"></script><script type="text/javascript">{js}</script></body></html>
159 """.format(css = compressed_css_code, d3js = self.d3js_source, js = compressed_js_code)
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.