Attachment 'burndown_D3.JS_from_online_source.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 = "http://d3js.org/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.
  • [get | view] (2015-04-02 13:39:23, 9.7 KB) [[attachment:burndown_D3.JS_from_local_file.py]]
  • [get | view] (2015-04-02 13:39:33, 9.7 KB) [[attachment:burndown_D3.JS_from_online_source.py]]
  • [get | view] (2015-04-02 13:38:52, 59.8 KB) [[attachment:burndown_moinmoin_parser_source.tar.gz]]
  • [get | view] (2015-04-02 14:04:12, 28.9 KB) [[attachment:example_output.png]]
 All files | Selected Files: delete move to page copy to page

You are not allowed to attach a file to this page.