|
|
|
@ -1,5 +1,5 @@
|
|
|
|
|
#!python |
|
|
|
|
# -*- coding: UTF-8 -*- |
|
|
|
|
# -*- coding: utf-8 -*- |
|
|
|
|
|
|
|
|
|
""" |
|
|
|
|
svg.charts.graph |
|
|
|
@ -14,24 +14,28 @@ import functools
|
|
|
|
|
|
|
|
|
|
import cssutils |
|
|
|
|
from lxml import etree |
|
|
|
|
from xml import xpath |
|
|
|
|
|
|
|
|
|
from svg.charts import css # causes the SVG profile to be loaded |
|
|
|
|
from svg.charts import css # causes the SVG profile to be loaded |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
import zlib |
|
|
|
|
except ImportError: |
|
|
|
|
zlib = None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sort_multiple(arrays): |
|
|
|
|
"sort multiple lists (of equal size) using the first list for the sort keys" |
|
|
|
|
"sort multiple lists (of equal size) " |
|
|
|
|
"using the first list for the sort keys" |
|
|
|
|
tuples = zip(*arrays) |
|
|
|
|
tuples.sort() |
|
|
|
|
return zip(*tuples) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Graph(object): |
|
|
|
|
""" |
|
|
|
|
Base object for generating SVG Graphs |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Synopsis |
|
|
|
|
|
|
|
|
|
This class is only used as a superclass of specialized charts. Do not |
|
|
|
@ -47,54 +51,56 @@ class Graph(object):
|
|
|
|
|
* svg.charts.time_series |
|
|
|
|
|
|
|
|
|
""" |
|
|
|
|
width= 500 |
|
|
|
|
height= 300 |
|
|
|
|
show_x_guidelines= False |
|
|
|
|
show_y_guidelines= True |
|
|
|
|
show_data_values= True |
|
|
|
|
min_scale_value= None |
|
|
|
|
show_x_labels= True |
|
|
|
|
stagger_x_labels= False |
|
|
|
|
rotate_x_labels= False |
|
|
|
|
step_x_labels= 1 |
|
|
|
|
step_include_first_x_label= True |
|
|
|
|
show_y_labels= True |
|
|
|
|
rotate_y_labels= False |
|
|
|
|
stagger_y_labels= False |
|
|
|
|
step_include_first_y_label= True |
|
|
|
|
step_y_labels= 1 |
|
|
|
|
scale_integers= False |
|
|
|
|
show_x_title= False |
|
|
|
|
x_title= 'X Field names' |
|
|
|
|
show_y_title= False |
|
|
|
|
y_title_text_direction= 'bt' # 'bt' for bottom to top; 'tb' for top to bottom |
|
|
|
|
y_title= 'Y Scale' |
|
|
|
|
show_graph_title= False |
|
|
|
|
graph_title= 'Graph Title' |
|
|
|
|
show_graph_subtitle= False |
|
|
|
|
graph_subtitle= 'Graph Subtitle' |
|
|
|
|
key= True |
|
|
|
|
key_position= 'right' # 'bottom' or 'right', |
|
|
|
|
|
|
|
|
|
font_size= 12 |
|
|
|
|
title_font_size= 16 |
|
|
|
|
subtitle_font_size= 14 |
|
|
|
|
x_label_font_size= 12 |
|
|
|
|
x_title_font_size= 14 |
|
|
|
|
y_label_font_size= 12 |
|
|
|
|
y_title_font_size= 14 |
|
|
|
|
key_font_size= 10 |
|
|
|
|
|
|
|
|
|
css_inline= False |
|
|
|
|
add_popups= False |
|
|
|
|
width = 500 |
|
|
|
|
height = 300 |
|
|
|
|
show_x_guidelines = False |
|
|
|
|
show_y_guidelines = True |
|
|
|
|
show_data_values = True |
|
|
|
|
min_scale_value = None |
|
|
|
|
show_x_labels = True |
|
|
|
|
stagger_x_labels = False |
|
|
|
|
rotate_x_labels = False |
|
|
|
|
step_x_labels = 1 |
|
|
|
|
step_include_first_x_label = True |
|
|
|
|
show_y_labels = True |
|
|
|
|
rotate_y_labels = False |
|
|
|
|
stagger_y_labels = False |
|
|
|
|
step_include_first_y_label = True |
|
|
|
|
step_y_labels = 1 |
|
|
|
|
scale_integers = False |
|
|
|
|
show_x_title = False |
|
|
|
|
x_title = 'X Field names' |
|
|
|
|
show_y_title = False |
|
|
|
|
# 'bt' for bottom to top; 'tb' for top to bottom |
|
|
|
|
y_title_text_direction = 'bt' |
|
|
|
|
y_title = 'Y Scale' |
|
|
|
|
show_graph_title = False |
|
|
|
|
graph_title = 'Graph Title' |
|
|
|
|
show_graph_subtitle = False |
|
|
|
|
graph_subtitle = 'Graph Subtitle' |
|
|
|
|
key = True |
|
|
|
|
# 'bottom' or 'right', |
|
|
|
|
key_position = 'right' |
|
|
|
|
|
|
|
|
|
font_size = 12 |
|
|
|
|
title_font_size = 16 |
|
|
|
|
subtitle_font_size = 14 |
|
|
|
|
x_label_font_size = 12 |
|
|
|
|
x_title_font_size = 14 |
|
|
|
|
y_label_font_size = 12 |
|
|
|
|
y_title_font_size = 14 |
|
|
|
|
key_font_size = 10 |
|
|
|
|
|
|
|
|
|
css_inline = False |
|
|
|
|
add_popups = False |
|
|
|
|
|
|
|
|
|
top_align = top_font = right_align = right_font = 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
compress = False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
stylesheet_names = ['graph.css'] |
|
|
|
|
|
|
|
|
|
def __init__(self, config = {}): |
|
|
|
|
def __init__(self, config={}): |
|
|
|
|
"""Initialize the graph object with the graph settings.""" |
|
|
|
|
if self.__class__ is Graph: |
|
|
|
|
raise NotImplementedError("Graph is an abstract base class") |
|
|
|
@ -104,12 +110,12 @@ class Graph(object):
|
|
|
|
|
|
|
|
|
|
def load_config(self, config): |
|
|
|
|
self.__dict__.update(config) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_data(self, conf): |
|
|
|
|
""" |
|
|
|
|
Add data to the graph object. May be called several times to add |
|
|
|
|
additional data sets. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>>> data_sales_02 = [12, 45, 21] # doctest: +SKIP |
|
|
|
|
>>> graph.add_data({ # doctest: +SKIP |
|
|
|
|
... 'data': data_sales_02, |
|
|
|
@ -124,34 +130,39 @@ class Graph(object):
|
|
|
|
|
try: |
|
|
|
|
assert(isinstance(conf['data'], (tuple, list))) |
|
|
|
|
except TypeError, e: |
|
|
|
|
raise TypeError, "conf should be dictionary with 'data' and other items" |
|
|
|
|
raise TypeError( |
|
|
|
|
"conf should be dictionary with 'data' and other items") |
|
|
|
|
except AssertionError: |
|
|
|
|
if not hasattr(conf['data'], '__iter__'): |
|
|
|
|
raise TypeError, "conf['data'] should be tuple or list or iterable" |
|
|
|
|
raise TypeError( |
|
|
|
|
"conf['data'] should be tuple or list or iterable") |
|
|
|
|
|
|
|
|
|
def process_data(self, data): |
|
|
|
|
pass |
|
|
|
|
|
|
|
|
|
def process_data(self, data): pass |
|
|
|
|
|
|
|
|
|
def clear_data(self): |
|
|
|
|
""" |
|
|
|
|
This method removes all data from the object so that you can |
|
|
|
|
reuse it to create a new graph but with the same config options. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>>> graph.clear_data() # doctest: +SKIP |
|
|
|
|
""" |
|
|
|
|
self.data = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def burn(self): |
|
|
|
|
""" |
|
|
|
|
Process the template with the data and |
|
|
|
|
config which has been set and return the resulting SVG. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Raises ValueError when no data set has |
|
|
|
|
been added to the graph object. |
|
|
|
|
""" |
|
|
|
|
if not self.data: raise ValueError("No data available") |
|
|
|
|
|
|
|
|
|
if hasattr(self, 'calculations'): self.calculations() |
|
|
|
|
|
|
|
|
|
if not self.data: |
|
|
|
|
raise ValueError("No data available") |
|
|
|
|
|
|
|
|
|
if hasattr(self, 'calculations'): |
|
|
|
|
self.calculations() |
|
|
|
|
|
|
|
|
|
self.start_svg() |
|
|
|
|
self.calculate_graph_dimensions() |
|
|
|
|
self.foreground = etree.Element("g") |
|
|
|
@ -161,22 +172,25 @@ class Graph(object):
|
|
|
|
|
self.draw_data() |
|
|
|
|
self.graph.append(self.foreground) |
|
|
|
|
self.render_inline_styles() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return self._burn_compressed() |
|
|
|
|
|
|
|
|
|
def _burn_compressed(self): |
|
|
|
|
if self.compress and not zlib: |
|
|
|
|
self.root.addprevious(etree.Comment('Python zlib not available for SVGZ')) |
|
|
|
|
|
|
|
|
|
data = etree.tostring(self.root, pretty_print=True, xml_declaration=True, encoding='utf-8') |
|
|
|
|
|
|
|
|
|
self.root.addprevious( |
|
|
|
|
etree.Comment('Python zlib not available for SVGZ')) |
|
|
|
|
|
|
|
|
|
data = etree.tostring( |
|
|
|
|
self.root, pretty_print=True, |
|
|
|
|
xml_declaration=True, encoding='utf-8') |
|
|
|
|
|
|
|
|
|
if self.compress and zlib: |
|
|
|
|
data = zlib.compress(data) |
|
|
|
|
|
|
|
|
|
return data |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
KEY_BOX_SIZE = 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_left_margin(self): |
|
|
|
|
""" |
|
|
|
|
Calculates the margin to the left of the plot area, setting |
|
|
|
@ -189,12 +203,16 @@ class Graph(object):
|
|
|
|
|
else: |
|
|
|
|
label_lengths = map(len, self.get_y_labels()) |
|
|
|
|
max_y_label_len = max(label_lengths) |
|
|
|
|
max_y_label_height_px = 0.6 * max_y_label_len * self.y_label_font_size |
|
|
|
|
if self.show_y_labels: bl += max_y_label_height_px |
|
|
|
|
if self.stagger_y_labels: bl += max_y_label_height_px + 10 |
|
|
|
|
if self.show_y_title: bl += self.y_title_font_size + 5 |
|
|
|
|
max_y_label_height_px = (0.6 * max_y_label_len * |
|
|
|
|
self.y_label_font_size) |
|
|
|
|
if self.show_y_labels: |
|
|
|
|
bl += max_y_label_height_px |
|
|
|
|
if self.stagger_y_labels: |
|
|
|
|
bl += max_y_label_height_px + 10 |
|
|
|
|
if self.show_y_title: |
|
|
|
|
bl += self.y_title_font_size + 5 |
|
|
|
|
self.border_left = bl |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def max_y_label_width_px(self): |
|
|
|
|
""" |
|
|
|
|
Calculate the width of the widest Y label. This will be the |
|
|
|
@ -202,7 +220,7 @@ class Graph(object):
|
|
|
|
|
""" |
|
|
|
|
if self.rotate_y_labels: |
|
|
|
|
return self.font_size |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_right_margin(self): |
|
|
|
|
""" |
|
|
|
|
Calculate the margin in pixels to the right of the plot area, |
|
|
|
@ -215,24 +233,26 @@ class Graph(object):
|
|
|
|
|
br += self.KEY_BOX_SIZE |
|
|
|
|
br += 10 # Some padding around the box |
|
|
|
|
self.border_right = br |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_top_margin(self): |
|
|
|
|
""" |
|
|
|
|
Calculate the margin in pixels above the plot area, setting |
|
|
|
|
border_top. |
|
|
|
|
""" |
|
|
|
|
self.border_top = 5 |
|
|
|
|
if self.show_graph_title: self.border_top += self.title_font_size |
|
|
|
|
if self.show_graph_title: |
|
|
|
|
self.border_top += self.title_font_size |
|
|
|
|
self.border_top += 5 |
|
|
|
|
if self.show_graph_subtitle: self.border_top += self.subtitle_font_size |
|
|
|
|
|
|
|
|
|
if self.show_graph_subtitle: |
|
|
|
|
self.border_top += self.subtitle_font_size |
|
|
|
|
|
|
|
|
|
def add_popup(self, x, y, label): |
|
|
|
|
""" |
|
|
|
|
Add pop-up information to a point on the graph. |
|
|
|
|
""" |
|
|
|
|
txt_width = len(label) * self.font_size * 0.6 + 10 |
|
|
|
|
tx = x + [5,-5][int(x+txt_width > self.width)] |
|
|
|
|
anchor = ['start', 'end'][x+txt_width > self.width] |
|
|
|
|
tx = x + [5, -5][int(x + txt_width > self.width)] |
|
|
|
|
anchor = ['start', 'end'][x + txt_width > self.width] |
|
|
|
|
style = 'fill: #000; text-anchor: %s;' % anchor |
|
|
|
|
id = 'label-%s' % label |
|
|
|
|
t = etree.SubElement(self.foreground, 'text', { |
|
|
|
@ -245,7 +265,8 @@ class Graph(object):
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
# add the circle element to the foreground |
|
|
|
|
visibility = "document.getElementById('%s').setAttribute('visibility', %%s)" % id |
|
|
|
|
visibility = ("document.getElementById('%s')." |
|
|
|
|
"setAttribute('visibility', %%s)" % id) |
|
|
|
|
t = etree.SubElement(self.foreground, 'circle', { |
|
|
|
|
'cx': str(x), |
|
|
|
|
'cy': str(y), |
|
|
|
@ -271,10 +292,12 @@ class Graph(object):
|
|
|
|
|
max_x_label_len = reduce(max, label_lengths) |
|
|
|
|
max_x_label_height_px *= 0.6 * max_x_label_len |
|
|
|
|
bb += max_x_label_height_px |
|
|
|
|
if self.stagger_x_labels: bb += max_x_label_height_px + 10 |
|
|
|
|
if self.show_x_title: bb += self.x_title_font_size + 5 |
|
|
|
|
if self.stagger_x_labels: |
|
|
|
|
bb += max_x_label_height_px + 10 |
|
|
|
|
if self.show_x_title: |
|
|
|
|
bb += self.x_title_font_size + 5 |
|
|
|
|
self.border_bottom = bb |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def draw_graph(self): |
|
|
|
|
""" |
|
|
|
|
The central logic for drawing the graph. |
|
|
|
@ -283,7 +306,7 @@ class Graph(object):
|
|
|
|
|
""" |
|
|
|
|
transform = 'translate (%s %s)' % (self.border_left, self.border_top) |
|
|
|
|
self.graph = etree.SubElement(self.root, 'g', transform=transform) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
etree.SubElement(self.graph, 'rect', { |
|
|
|
|
'x': '0', |
|
|
|
|
'y': '0', |
|
|
|
@ -291,7 +314,7 @@ class Graph(object):
|
|
|
|
|
'height': str(self.graph_height), |
|
|
|
|
'class': 'graphBackground' |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#Axis |
|
|
|
|
etree.SubElement(self.graph, 'path', { |
|
|
|
|
'd': 'M 0 0 v%s' % self.graph_height, |
|
|
|
@ -303,10 +326,10 @@ class Graph(object):
|
|
|
|
|
'class': 'axis', |
|
|
|
|
'id': 'yAxis' |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.draw_x_labels() |
|
|
|
|
self.draw_y_labels() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def x_label_offset(self, width): |
|
|
|
|
""" |
|
|
|
|
Return an offset for drawing the x label. Currently returns 0. |
|
|
|
@ -336,30 +359,31 @@ class Graph(object):
|
|
|
|
|
'y': str(y), |
|
|
|
|
'class': 'dataPointLabel'}) |
|
|
|
|
e.text = str(value) |
|
|
|
|
if style: e.set('style', style) |
|
|
|
|
if style: |
|
|
|
|
e.set('style', style) |
|
|
|
|
|
|
|
|
|
def draw_x_labels(self): |
|
|
|
|
"Draw the X axis labels" |
|
|
|
|
if self.show_x_labels: |
|
|
|
|
labels = self.get_x_labels() |
|
|
|
|
count = len(labels) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
labels = enumerate(iter(labels)) |
|
|
|
|
start = int(not self.step_include_first_x_label) |
|
|
|
|
labels = islice(labels, start, None, self.step_x_labels) |
|
|
|
|
map(self.draw_x_label, labels) |
|
|
|
|
self.draw_x_guidelines(self.field_width(), count) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def draw_x_label(self, label): |
|
|
|
|
label_width = self.field_width() |
|
|
|
|
index, label = label |
|
|
|
|
text = etree.SubElement(self.graph, 'text', {'class': 'xAxisLabels'}) |
|
|
|
|
text.text = label |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
x = index * label_width + self.x_label_offset(label_width) |
|
|
|
|
y = self.graph_height + self.x_label_font_size + 3 |
|
|
|
|
t = 0 - (self.font_size / 2) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.stagger_x_labels and (index % 2): |
|
|
|
|
stagger = self.x_label_font_size + 5 |
|
|
|
|
y += stagger |
|
|
|
@ -368,33 +392,34 @@ class Graph(object):
|
|
|
|
|
'd': 'M%(x)f %(graph_height)f v%(stagger)d' % vars(), |
|
|
|
|
'class': 'staggerGuideLine' |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
text.set('x', str(x)) |
|
|
|
|
text.set('y', str(y)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.rotate_x_labels: |
|
|
|
|
transform = 'rotate(90 %d %d) translate(0 -%d)' % \ |
|
|
|
|
(x, y-self.x_label_font_size, self.x_label_font_size/4) |
|
|
|
|
(x, y - self.x_label_font_size, self.x_label_font_size / 4) |
|
|
|
|
text.set('transform', transform) |
|
|
|
|
text.set('style', 'text-anchor: start') |
|
|
|
|
else: |
|
|
|
|
text.set('style', 'text-anchor: middle') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def y_label_offset(self, height): |
|
|
|
|
""" |
|
|
|
|
Return an offset for drawing the y label. Currently returns 0. |
|
|
|
|
""" |
|
|
|
|
# Consider height/2 to center within the field. |
|
|
|
|
return 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_field_width(self): |
|
|
|
|
return float(self.graph_width - self.font_size*2*self.right_font) / \ |
|
|
|
|
(len(self.get_x_labels()) - self.right_align) |
|
|
|
|
return (float( |
|
|
|
|
self.graph_width - self.font_size * 2 * self.right_font) / |
|
|
|
|
(len(self.get_x_labels()) - self.right_align)) |
|
|
|
|
field_width = get_field_width |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_field_height(self): |
|
|
|
|
return float(self.graph_height - self.font_size*2*self.top_font) / \ |
|
|
|
|
(len(self.get_y_labels()) - self.top_align) |
|
|
|
|
return (float(self.graph_height - self.font_size * 2 * self.top_font) / |
|
|
|
|
(len(self.get_y_labels()) - self.top_align)) |
|
|
|
|
field_height = get_field_height |
|
|
|
|
|
|
|
|
|
def draw_y_labels(self): |
|
|
|
@ -405,7 +430,7 @@ class Graph(object):
|
|
|
|
|
|
|
|
|
|
labels = self.get_y_labels() |
|
|
|
|
count = len(labels) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
labels = enumerate(iter(labels)) |
|
|
|
|
start = int(not self.step_include_first_y_label) |
|
|
|
|
labels = islice(labels, start, None, self.step_y_labels) |
|
|
|
@ -414,19 +439,20 @@ class Graph(object):
|
|
|
|
|
|
|
|
|
|
def get_y_offset(self): |
|
|
|
|
result = self.graph_height + self.y_label_offset(self.field_height()) |
|
|
|
|
if not self.rotate_y_labels: result += self.font_size/1.2 |
|
|
|
|
if not self.rotate_y_labels: |
|
|
|
|
result += self.font_size / 1.2 |
|
|
|
|
return result |
|
|
|
|
y_offset = property(get_y_offset) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def draw_y_label(self, label): |
|
|
|
|
label_height = self.field_height() |
|
|
|
|
index, label = label |
|
|
|
|
text = etree.SubElement(self.graph, 'text', {'class': 'yAxisLabels'}) |
|
|
|
|
text.text = label |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
y = self.y_offset - (label_height * index) |
|
|
|
|
x = {True: 0, False:-3}[self.rotate_y_labels] |
|
|
|
|
|
|
|
|
|
x = {True: 0, False: -3}[self.rotate_y_labels] |
|
|
|
|
|
|
|
|
|
if self.stagger_y_labels and (index % 2): |
|
|
|
|
stagger = self.y_label_font_size + 5 |
|
|
|
|
x -= stagger |
|
|
|
@ -434,25 +460,26 @@ class Graph(object):
|
|
|
|
|
'd': 'M%(x)f %(y)f h%(stagger)d' % vars(), |
|
|
|
|
'class': 'staggerGuideLine' |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
text.set('x', str(x)) |
|
|
|
|
text.set('y', str(y)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.rotate_y_labels: |
|
|
|
|
transform = 'translate(-%d 0) rotate (90 %d %d)' % \ |
|
|
|
|
(self.font_size, x, y) |
|
|
|
|
text.set('transform', transform) |
|
|
|
|
text.set('style', 'text-anchor: middle') |
|
|
|
|
else: |
|
|
|
|
text.set('y', str(y - self.y_label_font_size/2)) |
|
|
|
|
text.set('y', str(y - self.y_label_font_size / 2)) |
|
|
|
|
text.set('style', 'text-anchor: end') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def draw_x_guidelines(self, label_height, count): |
|
|
|
|
"Draw the X-axis guidelines" |
|
|
|
|
if not self.show_x_guidelines: return |
|
|
|
|
if not self.show_x_guidelines: |
|
|
|
|
return |
|
|
|
|
# skip the first one |
|
|
|
|
for count in range(1,count): |
|
|
|
|
start = label_height*count |
|
|
|
|
for count in range(1, count): |
|
|
|
|
start = label_height * count |
|
|
|
|
stop = self.graph_height |
|
|
|
|
path = etree.SubElement(self.graph, 'path', { |
|
|
|
|
'd': 'M %(start)s 0 v%(stop)s' % vars(), |
|
|
|
@ -460,9 +487,10 @@ class Graph(object):
|
|
|
|
|
|
|
|
|
|
def draw_y_guidelines(self, label_height, count): |
|
|
|
|
"Draw the Y-axis guidelines" |
|
|
|
|
if not self.show_y_guidelines: return |
|
|
|
|
if not self.show_y_guidelines: |
|
|
|
|
return |
|
|
|
|
for count in range(1, count): |
|
|
|
|
start = self.graph_height - label_height*count |
|
|
|
|
start = self.graph_height - label_height * count |
|
|
|
|
stop = self.graph_width |
|
|
|
|
path = etree.SubElement(self.graph, 'path', { |
|
|
|
|
'd': 'M 0 %(start)s h%(stop)s' % vars(), |
|
|
|
@ -470,10 +498,14 @@ class Graph(object):
|
|
|
|
|
|
|
|
|
|
def draw_titles(self): |
|
|
|
|
"Draws the graph title and subtitle" |
|
|
|
|
if self.show_graph_title: self.draw_graph_title() |
|
|
|
|
if self.show_graph_subtitle: self.draw_graph_subtitle() |
|
|
|
|
if self.show_x_title: self.draw_x_title() |
|
|
|
|
if self.show_y_title: self.draw_y_title() |
|
|
|
|
if self.show_graph_title: |
|
|
|
|
self.draw_graph_title() |
|
|
|
|
if self.show_graph_subtitle: |
|
|
|
|
self.draw_graph_subtitle() |
|
|
|
|
if self.show_x_title: |
|
|
|
|
self.draw_x_title() |
|
|
|
|
if self.show_y_title: |
|
|
|
|
self.draw_y_title() |
|
|
|
|
|
|
|
|
|
def draw_graph_title(self): |
|
|
|
|
text = etree.SubElement(self.root, 'text', { |
|
|
|
@ -483,10 +515,11 @@ class Graph(object):
|
|
|
|
|
text.text = self.graph_title |
|
|
|
|
|
|
|
|
|
def draw_graph_subtitle(self): |
|
|
|
|
y_subtitle_options = [subtitle_font_size, title_font_size+10] |
|
|
|
|
y_subtitle_options = [self.subtitle_font_size, |
|
|
|
|
self.title_font_size + 10] |
|
|
|
|
y_subtitle = y_subtitle_options[self.show_graph_title] |
|
|
|
|
text = etree.SubElement(self.root, 'text', { |
|
|
|
|
'x': str(self.width/2), |
|
|
|
|
'x': str(self.width / 2), |
|
|
|
|
'y': str(y_subtitle), |
|
|
|
|
'class': 'subTitle', |
|
|
|
|
}) |
|
|
|
@ -495,11 +528,12 @@ class Graph(object):
|
|
|
|
|
def draw_x_title(self): |
|
|
|
|
y = self.graph_height + self.border_top + self.x_title_font_size |
|
|
|
|
if self.show_x_labels: |
|
|
|
|
y_size = self.x_label_font_size+5 |
|
|
|
|
if self.stagger_x_labels: y_size*=2 |
|
|
|
|
y_size = self.x_label_font_size + 5 |
|
|
|
|
if self.stagger_x_labels: |
|
|
|
|
y_size *= 2 |
|
|
|
|
y += y_size |
|
|
|
|
x = self.width / 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
text = etree.SubElement(self.root, 'text', { |
|
|
|
|
'x': str(x), |
|
|
|
|
'y': str(y), |
|
|
|
@ -509,7 +543,7 @@ class Graph(object):
|
|
|
|
|
|
|
|
|
|
def draw_y_title(self): |
|
|
|
|
x = self.y_title_font_size |
|
|
|
|
if self.y_title_text_direction=='bt': |
|
|
|
|
if self.y_title_text_direction == 'bt': |
|
|
|
|
x += 3 |
|
|
|
|
rotate = -90 |
|
|
|
|
else: |
|
|
|
@ -526,14 +560,14 @@ class Graph(object):
|
|
|
|
|
|
|
|
|
|
def keys(self): |
|
|
|
|
return map(itemgetter('title'), self.data) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def draw_legend(self): |
|
|
|
|
if not self.key: |
|
|
|
|
# do nothing |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
group = etree.SubElement(self.root, 'g') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for key_count, key_name in enumerate(self.keys()): |
|
|
|
|
y_offset = (self.KEY_BOX_SIZE * key_count) + (key_count * 5) |
|
|
|
|
etree.SubElement(group, 'rect', { |
|
|
|
@ -548,7 +582,7 @@ class Graph(object):
|
|
|
|
|
'y': str(y_offset + self.KEY_BOX_SIZE), |
|
|
|
|
'class': 'keyText'}) |
|
|
|
|
text.text = key_name |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if self.key_position == 'right': |
|
|
|
|
x_offset = self.graph_width + self.border_left + 10 |
|
|
|
|
y_offset = self.border_top + 20 |
|
|
|
@ -560,18 +594,18 @@ class Graph(object):
|
|
|
|
|
x_offset = self.border_left + 20 |
|
|
|
|
y_offset = self.border_top + self.graph_height + 5 |
|
|
|
|
if self.show_x_labels: |
|
|
|
|
max_x_label_height_px = x_label_font_size |
|
|
|
|
max_x_label_height_px = self.x_label_font_size |
|
|
|
|
if self.rotate_x_labels: |
|
|
|
|
longest_label_length = max(map(len, self.get_x_labels())) |
|
|
|
|
# note: I think 0.6 is the ratio of width to height of characters |
|
|
|
|
# I think 0.6 is the ratio of width to height of characters |
|
|
|
|
max_x_label_height_px *= longest_label_length * 0.6 |
|
|
|
|
y_offset += max_x_label_height_px |
|
|
|
|
if self.stagger_x_labels: |
|
|
|
|
y_offset += max_x_label_height_px + 5 |
|
|
|
|
if self.show_x_title: |
|
|
|
|
y_offset += x_title_font_size + 5 |
|
|
|
|
y_offset += self.x_title_font_size + 5 |
|
|
|
|
return x_offset, y_offset |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render_inline_styles(self): |
|
|
|
|
"Hard-code the styles into the SVG XML if style sheets are not used." |
|
|
|
|
if not self.css_inline: |
|
|
|
@ -602,7 +636,7 @@ class Graph(object):
|
|
|
|
|
""" |
|
|
|
|
Override and place code to add defs here. TODO: what are defs? |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def start_svg(self): |
|
|
|
|
"Base SVG Document Creation" |
|
|
|
|
SVG_NAMESPACE = 'http://www.w3.org/2000/svg' |
|
|
|
@ -612,11 +646,12 @@ class Graph(object):
|
|
|
|
|
'xlink': 'http://www.w3.org/1999/xlink', |
|
|
|
|
'a3': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', |
|
|
|
|
} |
|
|
|
|
self.root = etree.Element(SVG+"svg", attrib={ |
|
|
|
|
self.root = etree.Element(SVG + "svg", attrib={ |
|
|
|
|
'width': str(self.width), |
|
|
|
|
'height': str(self.height), |
|
|
|
|
'viewBox': '0 0 %s %s' % (self.width, self.height), |
|
|
|
|
'{http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/}scriptImplementation': 'Adobe', |
|
|
|
|
'{http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/}' |
|
|
|
|
'scriptImplementation': 'Adobe', |
|
|
|
|
}, nsmap=NSMAP) |
|
|
|
|
if hasattr(self, 'style_sheet_href'): |
|
|
|
|
pi = etree.ProcessingInstruction( |
|
|
|
@ -624,26 +659,27 @@ class Graph(object):
|
|
|
|
|
'href="%s" type="text/css"' % self.style_sheet_href |
|
|
|
|
) |
|
|
|
|
self.root.addprevious(pi) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
comment_strings = ( |
|
|
|
|
' Created with SVG.Graph ', |
|
|
|
|
' SVG.Graph by Jason R. Coombs ', |
|
|
|
|
' Based on SVG::Graph by Sean E. Russel ', |
|
|
|
|
' Based on Perl SVG:TT:Graph by Leo Lapworth & Stephan Morgan ', |
|
|
|
|
' '+'/'*66, |
|
|
|
|
' ' + '/' * 66, |
|
|
|
|
) |
|
|
|
|
map(self.root.append, map(etree.Comment, comment_strings)) |
|
|
|
|
|
|
|
|
|
defs = etree.SubElement(self.root, 'defs') |
|
|
|
|
self.add_defs(defs) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not hasattr(self, 'style_sheet_href') and not self.css_inline: |
|
|
|
|
self.root.append(etree.Comment(' include default stylesheet if none specified ')) |
|
|
|
|
self.root.append(etree.Comment( |
|
|
|
|
' include default stylesheet if none specified ')) |
|
|
|
|
style = etree.SubElement(defs, 'style', type='text/css') |
|
|
|
|
# TODO: the text was previously escaped in a CDATA declaration... how |
|
|
|
|
# to do that with etree? |
|
|
|
|
# TODO: the text was previously escaped in a CDATA declaration... |
|
|
|
|
# how to do that with etree? |
|
|
|
|
style.text = self.get_stylesheet().cssText |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.root.append(etree.Comment('SVG Background')) |
|
|
|
|
rect = etree.SubElement(self.root, 'rect', { |
|
|
|
|
'width': str(self.width), |
|
|
|
@ -651,7 +687,7 @@ class Graph(object):
|
|
|
|
|
'x': '0', |
|
|
|
|
'y': '0', |
|
|
|
|
'class': 'svgBackground'}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_graph_dimensions(self): |
|
|
|
|
self.calculate_left_margin() |
|
|
|
|
self.calculate_right_margin() |
|
|
|
@ -678,12 +714,14 @@ class Graph(object):
|
|
|
|
|
return sheets |
|
|
|
|
|
|
|
|
|
def get_stylesheet(self): |
|
|
|
|
cssutils.log.setLevel(30) # disable INFO log messages |
|
|
|
|
cssutils.log.setLevel(30) # disable INFO log messages |
|
|
|
|
|
|
|
|
|
def merge_sheets(s1, s2): |
|
|
|
|
map(s1.add, s2) |
|
|
|
|
return s1 |
|
|
|
|
return reduce(merge_sheets, self.get_stylesheet_resources()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class class_dict(object): |
|
|
|
|
"Emulates a dictionary, but retrieves class attributes" |
|
|
|
|
def __init__(self, obj): |
|
|
|
|