mirror of https://github.com/Kozea/pygal.git
SANDIA\jaraco
19 years ago
commit
47e9b43895
1 changed files with 862 additions and 0 deletions
@ -0,0 +1,862 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
|
||||||
|
from xml.dom import minidom as dom |
||||||
|
|
||||||
|
try: |
||||||
|
import zlib |
||||||
|
__have_zlib = True |
||||||
|
except ImportError: |
||||||
|
__have_zlib = False |
||||||
|
|
||||||
|
def CreateElement( nodeName, attributes={} ): |
||||||
|
"Create an XML node and set the attributes from a dict" |
||||||
|
node = dom.Element( nodeName ) |
||||||
|
map( lambda a: node.setAttribute( *a ), attributes.items() ) |
||||||
|
return node |
||||||
|
|
||||||
|
class Graph( object ): |
||||||
|
"""=== Base object for generating SVG Graphs |
||||||
|
|
||||||
|
== Synopsis |
||||||
|
|
||||||
|
This class is only used as a superclass of specialized charts. Do not |
||||||
|
attempt to use this class directly, unless creating a new chart type. |
||||||
|
|
||||||
|
For examples of how to subclass this class, see the existing specific |
||||||
|
subclasses, such as SVG.Pie. |
||||||
|
|
||||||
|
== Examples |
||||||
|
|
||||||
|
For examples of how to use this package, see either the test files, or |
||||||
|
the documentation for the specific class you want to use. |
||||||
|
|
||||||
|
* file:test/plot.rb |
||||||
|
* file:test/single.rb |
||||||
|
* file:test/test.rb |
||||||
|
* file:test/timeseries.rb |
||||||
|
|
||||||
|
== Description |
||||||
|
|
||||||
|
This package should be used as a base for creating SVG graphs. |
||||||
|
|
||||||
|
== Acknowledgements |
||||||
|
|
||||||
|
Sean E. Russel for creating the SVG::Graph Ruby package from which this |
||||||
|
Python port is derived. |
||||||
|
|
||||||
|
Leo Lapworth for creating the SVG::TT::Graph package which the Ruby |
||||||
|
port is based on. |
||||||
|
|
||||||
|
Stephen Morgan for creating the TT template and SVG. |
||||||
|
|
||||||
|
== See |
||||||
|
|
||||||
|
* SVG.BarHorizontal |
||||||
|
* SVG.Bar |
||||||
|
* SVG.Line |
||||||
|
* SVG.Pie |
||||||
|
* SVG.Plot |
||||||
|
* SVG.TimeSeries |
||||||
|
|
||||||
|
== Author |
||||||
|
|
||||||
|
Jason R. Coombs <jaraco@jaraco.com> |
||||||
|
|
||||||
|
Copyright 2005 Sandia National Laboratories |
||||||
|
""" |
||||||
|
defaults = { |
||||||
|
'width': 500, |
||||||
|
'height': 300, |
||||||
|
'show_x_guidelines': False, |
||||||
|
'show_y_guidelines': True, |
||||||
|
'show_data_values': True, |
||||||
|
'min_scale_value': 0, |
||||||
|
'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, |
||||||
|
'scale_integers': False, |
||||||
|
'show_x_title': False, |
||||||
|
'x_title': 'X Field names', |
||||||
|
'show_y_title': False, |
||||||
|
'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, |
||||||
|
'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, |
||||||
|
|
||||||
|
'no_css': False, |
||||||
|
'add_popups': False, |
||||||
|
} |
||||||
|
#__slots__ = tuple( defaults ) + ( '__dict__', '__weakref__' ) |
||||||
|
|
||||||
|
def __init__( self, config ): |
||||||
|
"""Initialize the graph object with the graph settings. You won't |
||||||
|
instantiate this class directly; see the subclass for options. |
||||||
|
""" |
||||||
|
self.top_align = self.top_font = self.right_align = self.right_font = 0 |
||||||
|
self.load_config() |
||||||
|
self.load_config( config ) |
||||||
|
self.clear_data() |
||||||
|
|
||||||
|
def load_config( self, config = None ): |
||||||
|
if not config: config = self.defaults |
||||||
|
map( lambda pair: setattr( self, *pair ), config.items() ) |
||||||
|
|
||||||
|
def add_data( self, conf ): |
||||||
|
"""This method allows you do add data to the graph object. |
||||||
|
It can be called several times to add more data sets in. |
||||||
|
|
||||||
|
>>> data_sales_02 = [12, 45, 21] |
||||||
|
>>> graph.add_data({ |
||||||
|
... 'data': data_sales_02, |
||||||
|
... 'title': 'Sales 2002' |
||||||
|
... }) |
||||||
|
""" |
||||||
|
try: |
||||||
|
assert( isinstance( conf['data'], ( tuple, list ) ) ) |
||||||
|
except TypeError, e: |
||||||
|
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" |
||||||
|
|
||||||
|
self.data.append( conf ) |
||||||
|
|
||||||
|
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() |
||||||
|
""" |
||||||
|
self.data = [] |
||||||
|
|
||||||
|
def burn( self ): |
||||||
|
""" # This method processes the template with the data and |
||||||
|
config which has been set and returns the resulting SVG. |
||||||
|
|
||||||
|
This method will croak unless at least one data set has |
||||||
|
been added to the graph object. |
||||||
|
|
||||||
|
Ex: graph.burn() |
||||||
|
""" |
||||||
|
if not self.data: raise ValueError( "No data available" ) |
||||||
|
|
||||||
|
if hasattr( self, 'calculations' ): self.calculations() |
||||||
|
|
||||||
|
self.start_svg() |
||||||
|
self.calculate_graph_dimensions() |
||||||
|
self.foreground = dom.Element( "g" ) |
||||||
|
self.draw_graph() |
||||||
|
self.draw_titles() |
||||||
|
self.draw_legend() |
||||||
|
self.draw_data() |
||||||
|
self.graph.add_element( self.foreground ) |
||||||
|
self.style() |
||||||
|
|
||||||
|
data = self._doc.toprettyxml() |
||||||
|
|
||||||
|
if self.compress: |
||||||
|
if __have_zlib: |
||||||
|
data = zlib.compress( data ) |
||||||
|
else: |
||||||
|
data += '<!-- Python zlib not available for SVGZ -->' |
||||||
|
|
||||||
|
return data |
||||||
|
|
||||||
|
KEY_BOX_SIZE = 12 |
||||||
|
|
||||||
|
def calculate_left_margin( self ): |
||||||
|
"""Override this (and call super) to change the margin to the left |
||||||
|
of the plot area. Results in border_left being set.""" |
||||||
|
bl = 7 |
||||||
|
# Check for Y labels |
||||||
|
if self.rotate_y_labels: |
||||||
|
max_y_label_height_px = self.y_label_font_size |
||||||
|
else: |
||||||
|
label_lengths = map( len, self.get_y_labels() ) |
||||||
|
max_y_label_len = reduce( max, label_lengths ) |
||||||
|
max_y_label_height_px = 0.6 * max_y_label_len * self.y_label_font_size |
||||||
|
if show_y_labels: bl += max_y_label_height_px |
||||||
|
if stagger_y_labels: bl += max_y_label_height_px + 10 |
||||||
|
if show_y_title: bl += self.y_title_font_size + 5 |
||||||
|
self.border_left = bl |
||||||
|
|
||||||
|
def max_y_label_width_px( self ): |
||||||
|
"""Calculates the width of the widest Y label. This will be the |
||||||
|
character height if the Y labels are rotated.""" |
||||||
|
if self.rotate_y_labels: |
||||||
|
return self.font_size |
||||||
|
|
||||||
|
def calculate_right_margin( self ): |
||||||
|
"""Override this (and call super) to change the margin to the right |
||||||
|
of the plot area. Results in border_right being set.""" |
||||||
|
br = 7 |
||||||
|
if self.key and self.key_position == 'right': |
||||||
|
max_key_len = max( map( len, self.keys ) ) |
||||||
|
br += max_key_len * self.key_font_size * 0.6 |
||||||
|
br += self.KEY_BOX_SIZE |
||||||
|
br += 10 # Some padding around the box |
||||||
|
self.border_right = br |
||||||
|
|
||||||
|
def calculate_top_margin( self ): |
||||||
|
"""Override this (and call super) to change the margin to the top |
||||||
|
of the plot area. Results in border_top being set.""" |
||||||
|
self.border_top = 5 |
||||||
|
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 |
||||||
|
|
||||||
|
def add_popup( self, x, y, label ): |
||||||
|
"Adds pop-up point information to a graph." |
||||||
|
txt_width = len( label ) * self.font_size * 0.6 + 10 |
||||||
|
tx = x + [5,-5](x+txt_width > width) |
||||||
|
t = dom.Element( 'text' ) |
||||||
|
anchor = ['start', 'end'][x+txt_width > self.width] |
||||||
|
style = 'fill: #000; text-anchor: %s;' % anchor |
||||||
|
id = 'label-%s' % label |
||||||
|
attributes = { 'x': str( tx ), |
||||||
|
'y': str( y - self.font_size ), |
||||||
|
'visibility': 'hidden', |
||||||
|
'style': style, |
||||||
|
'text': label, |
||||||
|
'id': id |
||||||
|
} |
||||||
|
map( lambda a: t.setAttribute( *a ), attributes.items() ) |
||||||
|
self.foreground.appendChild( t ) |
||||||
|
|
||||||
|
visibility = "document.getElementById(%s).setAttribute('visibility', %%s )" % id |
||||||
|
t = dom.Element( 'circle' ) |
||||||
|
attributes = { 'cx': str( x ), |
||||||
|
'cy': str( y ), |
||||||
|
'r': 10, |
||||||
|
'style': 'opacity: 0;', |
||||||
|
'onmouseover': visibility % 'visible', |
||||||
|
'onmouseout': visibility % 'hidden', |
||||||
|
} |
||||||
|
map( lambda a: t.setAttribute( *a ), attributes.items() ) |
||||||
|
|
||||||
|
def calculate_bottom_margin( self ): |
||||||
|
"""Override this (and call super) to change the margin to the bottom |
||||||
|
of the plot area. Results in border_bottom being set.""" |
||||||
|
bb = 7 |
||||||
|
if self.key and self.key_position == 'bottom': |
||||||
|
bb += len( self.data ) * ( self.font_size + 5 ) |
||||||
|
bb += 10 |
||||||
|
if self.show_x_labels: |
||||||
|
max_x_label_height_px = self.x_label_font_size |
||||||
|
if self.rotate_x_labels: |
||||||
|
label_lengths = map( len, self.get_x_labels() ) |
||||||
|
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 |
||||||
|
self.border_bottom = bb |
||||||
|
|
||||||
|
def draw_graph: |
||||||
|
transform = 'translate ( %s %s )' % ( self.border_left, self.border_top ) |
||||||
|
self.graph = CreateElement( 'g', { 'transform': transform } ) |
||||||
|
self.root.appendChild( self.graph ) |
||||||
|
|
||||||
|
self.graph.appendChild( CreateElement( 'rect', { |
||||||
|
'x': '0', |
||||||
|
'y': '0', |
||||||
|
'width': str( self.graph_width ) |
||||||
|
'height': str( self.graph_height ) |
||||||
|
'class': 'graphBackground' |
||||||
|
} ) ) |
||||||
|
|
||||||
|
#Axis |
||||||
|
self.graph.appendChild( CreateElement( 'path', { |
||||||
|
'd': 'M 0 0 v%s' % self.graph_height, |
||||||
|
'class': 'axis', |
||||||
|
'id': 'xAxis' |
||||||
|
} ) ) |
||||||
|
self.graph.appendChild( CreateElement( 'path', { |
||||||
|
'd': 'M 0 %s h%s' % ( self.graph_height, self.graph_width ), |
||||||
|
'class': 'axis', |
||||||
|
'id': 'yAxis' |
||||||
|
} ) ) |
||||||
|
|
||||||
|
self.draw_x_labels() |
||||||
|
self.draw_y_labels() |
||||||
|
|
||||||
|
def x_label_offset( self, width ): |
||||||
|
"""Where in the X area the label is drawn |
||||||
|
Centered in the field, should be width/2. Start, 0.""" |
||||||
|
return 0 |
||||||
|
|
||||||
|
def make_datapoint_text( self, x, y, value, style='' ): |
||||||
|
if self.show_data_values: |
||||||
|
e = CreateElement( 'text', { |
||||||
|
'x': str( x ), |
||||||
|
'y': str( y ), |
||||||
|
'class': 'dataPointLabel', |
||||||
|
'style': '%(style)s stroke: #fff; stroke-width: 2;' % vars(), |
||||||
|
} ) |
||||||
|
e.nodeValue = value |
||||||
|
self.foreground.appendChild( e ) |
||||||
|
|
||||||
|
def draw_x_labels( self ): |
||||||
|
"Draw the X axis labels" |
||||||
|
stagger = self.x_label_font_size + 5 |
||||||
|
if self.show_x_labels: |
||||||
|
label_width = self.field_width |
||||||
|
|
||||||
|
labels = self.get_x_labels() |
||||||
|
count = len( labels ) |
||||||
|
|
||||||
|
labels = enumerate( iter( labels ) ) |
||||||
|
start = int( !self.step_include_first_x_label ) |
||||||
|
labels = itertools.islice( labels, start, None, self.step_x_labels ) |
||||||
|
map( self.draw_x_label, labels ) |
||||||
|
self.draw_x_guidelines( label_width, count ) |
||||||
|
|
||||||
|
def draw_x_label( self, label ): |
||||||
|
index, label = label |
||||||
|
text = CreateElement( 'text', { 'class': 'xAxisLabels' } ) |
||||||
|
self.graph.appendChild( text ) |
||||||
|
|
||||||
|
def start_svg( self ): |
||||||
|
"Base SVG Document Creation" |
||||||
|
impl = dom.getDOMImplementation() |
||||||
|
#dt = impl.createDocumentType( 'svg', 'PUBLIC' |
||||||
|
self._doc = impl.createDocument( None, 'svg', None ) |
||||||
|
self.root = self._doc.getDocumentElement() |
||||||
|
|
||||||
|
ruby = """ |
||||||
|
# Draws the background, axis, and labels. |
||||||
|
def draw_graph |
||||||
|
@graph = @root.add_element( "g", { |
||||||
|
"transform" => "translate( #@border_left #@border_top )" |
||||||
|
}) |
||||||
|
|
||||||
|
# Background |
||||||
|
@graph.add_element( "rect", { |
||||||
|
"x" => "0", |
||||||
|
"y" => "0", |
||||||
|
"width" => @graph_width.to_s, |
||||||
|
"height" => @graph_height.to_s, |
||||||
|
"class" => "graphBackground" |
||||||
|
}) |
||||||
|
|
||||||
|
# Axis |
||||||
|
@graph.add_element( "path", { |
||||||
|
"d" => "M 0 0 v#@graph_height", |
||||||
|
"class" => "axis", |
||||||
|
"id" => "xAxis" |
||||||
|
}) |
||||||
|
@graph.add_element( "path", { |
||||||
|
"d" => "M 0 #@graph_height h#@graph_width", |
||||||
|
"class" => "axis", |
||||||
|
"id" => "yAxis" |
||||||
|
}) |
||||||
|
|
||||||
|
draw_x_labels |
||||||
|
draw_y_labels |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
# Where in the X area the label is drawn |
||||||
|
# Centered in the field, should be width/2. Start, 0. |
||||||
|
def x_label_offset( width ) |
||||||
|
0 |
||||||
|
end |
||||||
|
|
||||||
|
def make_datapoint_text( x, y, value, style="" ) |
||||||
|
if show_data_values |
||||||
|
@foreground.add_element( "text", { |
||||||
|
"x" => x.to_s, |
||||||
|
"y" => y.to_s, |
||||||
|
"class" => "dataPointLabel", |
||||||
|
"style" => "#{style} stroke: #fff; stroke-width: 2;" |
||||||
|
}).text = value.to_s |
||||||
|
text = @foreground.add_element( "text", { |
||||||
|
"x" => x.to_s, |
||||||
|
"y" => y.to_s, |
||||||
|
"class" => "dataPointLabel" |
||||||
|
}) |
||||||
|
text.text = value.to_s |
||||||
|
text.attributes["style"] = style if style.length > 0 |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
# Draws the X axis labels |
||||||
|
def draw_x_labels |
||||||
|
stagger = x_label_font_size + 5 |
||||||
|
if show_x_labels |
||||||
|
label_width = field_width |
||||||
|
|
||||||
|
count = 0 |
||||||
|
for label in get_x_labels |
||||||
|
if step_include_first_x_label == true then |
||||||
|
step = count % step_x_labels |
||||||
|
else |
||||||
|
step = (count + 1) % step_x_labels |
||||||
|
end |
||||||
|
|
||||||
|
if step == 0 then |
||||||
|
text = @graph.add_element( "text" ) |
||||||
|
text.attributes["class"] = "xAxisLabels" |
||||||
|
text.text = label.to_s |
||||||
|
|
||||||
|
x = count * label_width + x_label_offset( label_width ) |
||||||
|
y = @graph_height + x_label_font_size + 3 |
||||||
|
t = 0 - (font_size / 2) |
||||||
|
|
||||||
|
if stagger_x_labels and count % 2 == 1 |
||||||
|
y += stagger |
||||||
|
@graph.add_element( "path", { |
||||||
|
"d" => "M#{x} #@graph_height v#{stagger}", |
||||||
|
"class" => "staggerGuideLine" |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
text.attributes["x"] = x.to_s |
||||||
|
text.attributes["y"] = y.to_s |
||||||
|
if rotate_x_labels |
||||||
|
text.attributes["transform"] = |
||||||
|
"rotate( 90 #{x} #{y-x_label_font_size} )"+ |
||||||
|
" translate( 0 -#{x_label_font_size/4} )" |
||||||
|
text.attributes["style"] = "text-anchor: start" |
||||||
|
else |
||||||
|
text.attributes["style"] = "text-anchor: middle" |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
draw_x_guidelines( label_width, count ) if show_x_guidelines |
||||||
|
count += 1 |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
# Where in the Y area the label is drawn |
||||||
|
# Centered in the field, should be width/2. Start, 0. |
||||||
|
def y_label_offset( height ) |
||||||
|
0 |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
def field_width |
||||||
|
(@graph_width.to_f - font_size*2*right_font) / |
||||||
|
(get_x_labels.length - right_align) |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
def field_height |
||||||
|
(@graph_height.to_f - font_size*2*top_font) / |
||||||
|
(get_y_labels.length - top_align) |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
# Draws the Y axis labels |
||||||
|
def draw_y_labels |
||||||
|
stagger = y_label_font_size + 5 |
||||||
|
if show_y_labels |
||||||
|
label_height = field_height |
||||||
|
|
||||||
|
count = 0 |
||||||
|
y_offset = @graph_height + y_label_offset( label_height ) |
||||||
|
y_offset += font_size/1.2 unless rotate_y_labels |
||||||
|
for label in get_y_labels |
||||||
|
y = y_offset - (label_height * count) |
||||||
|
x = rotate_y_labels ? 0 : -3 |
||||||
|
|
||||||
|
if stagger_y_labels and count % 2 == 1 |
||||||
|
x -= stagger |
||||||
|
@graph.add_element( "path", { |
||||||
|
"d" => "M#{x} #{y} h#{stagger}", |
||||||
|
"class" => "staggerGuideLine" |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
text = @graph.add_element( "text", { |
||||||
|
"x" => x.to_s, |
||||||
|
"y" => y.to_s, |
||||||
|
"class" => "yAxisLabels" |
||||||
|
}) |
||||||
|
text.text = label.to_s |
||||||
|
if rotate_y_labels |
||||||
|
text.attributes["transform"] = "translate( -#{font_size} 0 ) "+ |
||||||
|
"rotate( 90 #{x} #{y} ) " |
||||||
|
text.attributes["style"] = "text-anchor: middle" |
||||||
|
else |
||||||
|
text.attributes["y"] = (y - (y_label_font_size/2)).to_s |
||||||
|
text.attributes["style"] = "text-anchor: end" |
||||||
|
end |
||||||
|
draw_y_guidelines( label_height, count ) if show_y_guidelines |
||||||
|
count += 1 |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
# Draws the X axis guidelines |
||||||
|
def draw_x_guidelines( label_height, count ) |
||||||
|
if count != 0 |
||||||
|
@graph.add_element( "path", { |
||||||
|
"d" => "M#{label_height*count} 0 v#@graph_height", |
||||||
|
"class" => "guideLines" |
||||||
|
}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
# Draws the Y axis guidelines |
||||||
|
def draw_y_guidelines( label_height, count ) |
||||||
|
if count != 0 |
||||||
|
@graph.add_element( "path", { |
||||||
|
"d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width", |
||||||
|
"class" => "guideLines" |
||||||
|
}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
# Draws the graph title and subtitle |
||||||
|
def draw_titles |
||||||
|
if show_graph_title |
||||||
|
@root.add_element( "text", { |
||||||
|
"x" => (width / 2).to_s, |
||||||
|
"y" => (title_font_size).to_s, |
||||||
|
"class" => "mainTitle" |
||||||
|
}).text = graph_title.to_s |
||||||
|
end |
||||||
|
|
||||||
|
if show_graph_subtitle |
||||||
|
y_subtitle = show_graph_title ? |
||||||
|
title_font_size + 10 : |
||||||
|
subtitle_font_size |
||||||
|
@root.add_element("text", { |
||||||
|
"x" => (width / 2).to_s, |
||||||
|
"y" => (y_subtitle).to_s, |
||||||
|
"class" => "subTitle" |
||||||
|
}).text = graph_subtitle.to_s |
||||||
|
end |
||||||
|
|
||||||
|
if show_x_title |
||||||
|
y = @graph_height + @border_top + x_title_font_size |
||||||
|
if show_x_labels |
||||||
|
y += x_label_font_size + 5 if stagger_x_labels |
||||||
|
y += x_label_font_size + 5 |
||||||
|
end |
||||||
|
x = width / 2 |
||||||
|
|
||||||
|
@root.add_element("text", { |
||||||
|
"x" => x.to_s, |
||||||
|
"y" => y.to_s, |
||||||
|
"class" => "xAxisTitle", |
||||||
|
}).text = x_title.to_s |
||||||
|
end |
||||||
|
|
||||||
|
if show_y_title |
||||||
|
x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3) |
||||||
|
y = height / 2 |
||||||
|
|
||||||
|
text = @root.add_element("text", { |
||||||
|
"x" => x.to_s, |
||||||
|
"y" => y.to_s, |
||||||
|
"class" => "yAxisTitle", |
||||||
|
}) |
||||||
|
text.text = y_title.to_s |
||||||
|
if y_title_text_direction == :bt |
||||||
|
text.attributes["transform"] = "rotate( -90, #{x}, #{y} )" |
||||||
|
else |
||||||
|
text.attributes["transform"] = "rotate( 90, #{x}, #{y} )" |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def keys |
||||||
|
return @data.collect{ |d| d[:title] } |
||||||
|
end |
||||||
|
|
||||||
|
# Draws the legend on the graph |
||||||
|
def draw_legend |
||||||
|
if key |
||||||
|
group = @root.add_element( "g" ) |
||||||
|
|
||||||
|
key_count = 0 |
||||||
|
for key_name in keys |
||||||
|
y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5) |
||||||
|
group.add_element( "rect", { |
||||||
|
"x" => 0.to_s, |
||||||
|
"y" => y_offset.to_s, |
||||||
|
"width" => KEY_BOX_SIZE.to_s, |
||||||
|
"height" => KEY_BOX_SIZE.to_s, |
||||||
|
"class" => "key#{key_count+1}" |
||||||
|
}) |
||||||
|
group.add_element( "text", { |
||||||
|
"x" => (KEY_BOX_SIZE + 5).to_s, |
||||||
|
"y" => (y_offset + KEY_BOX_SIZE).to_s, |
||||||
|
"class" => "keyText" |
||||||
|
}).text = key_name.to_s |
||||||
|
key_count += 1 |
||||||
|
end |
||||||
|
|
||||||
|
case key_position |
||||||
|
when :right |
||||||
|
x_offset = @graph_width + @border_left + 10 |
||||||
|
y_offset = @border_top + 20 |
||||||
|
when :bottom |
||||||
|
x_offset = @border_left + 20 |
||||||
|
y_offset = @border_top + @graph_height + 5 |
||||||
|
if show_x_labels |
||||||
|
max_x_label_height_px = rotate_x_labels ? |
||||||
|
get_x_labels.max{|a,b| |
||||||
|
a.length<=>b.length |
||||||
|
}.length * x_label_font_size : |
||||||
|
x_label_font_size |
||||||
|
y_offset += max_x_label_height_px |
||||||
|
y_offset += max_x_label_height_px + 5 if stagger_x_labels |
||||||
|
end |
||||||
|
y_offset += x_title_font_size + 5 if show_x_title |
||||||
|
end |
||||||
|
group.attributes["transform"] = "translate(#{x_offset} #{y_offset})" |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 ) |
||||||
|
if lo < hi |
||||||
|
p = partition(arrys,lo,hi) |
||||||
|
sort_multiple(arrys, lo, p-1) |
||||||
|
sort_multiple(arrys, p+1, hi) |
||||||
|
end |
||||||
|
arrys |
||||||
|
end |
||||||
|
|
||||||
|
def partition( arrys, lo, hi ) |
||||||
|
p = arrys[0][lo] |
||||||
|
l = lo |
||||||
|
z = lo+1 |
||||||
|
while z <= hi |
||||||
|
if arrys[0][z] < p |
||||||
|
l += 1 |
||||||
|
arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] } |
||||||
|
end |
||||||
|
z += 1 |
||||||
|
end |
||||||
|
arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] } |
||||||
|
l |
||||||
|
end |
||||||
|
|
||||||
|
def style |
||||||
|
if no_css |
||||||
|
styles = parse_css |
||||||
|
@root.elements.each("//*[@class]") { |el| |
||||||
|
cl = el.attributes["class"] |
||||||
|
style = styles[cl] |
||||||
|
style += el.attributes["style"] if el.attributes["style"] |
||||||
|
el.attributes["style"] = style |
||||||
|
} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def parse_css |
||||||
|
css = get_style |
||||||
|
rv = {} |
||||||
|
while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m |
||||||
|
names_orig = names = $1 |
||||||
|
css = $' |
||||||
|
css =~ /([^}]+)\}/m |
||||||
|
content = $1 |
||||||
|
css = $' |
||||||
|
|
||||||
|
nms = [] |
||||||
|
while names =~ /^\s*,?\s*\.(\w+)/ |
||||||
|
nms << $1 |
||||||
|
names = $' |
||||||
|
end |
||||||
|
|
||||||
|
content = content.tr( "\n\t", " ") |
||||||
|
for name in nms |
||||||
|
current = rv[name] |
||||||
|
current = current ? current+"; "+content : content |
||||||
|
rv[name] = current.strip.squeeze(" ") |
||||||
|
end |
||||||
|
end |
||||||
|
return rv |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
# Override and place code to add defs here |
||||||
|
def add_defs defs |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
def start_svg |
||||||
|
# Base document |
||||||
|
@doc = Document.new |
||||||
|
@doc << XMLDecl.new |
||||||
|
@doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } + |
||||||
|
%q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} ) |
||||||
|
if style_sheet && style_sheet != '' |
||||||
|
@doc << ProcessingInstruction.new( "xml-stylesheet", |
||||||
|
%Q{href="#{style_sheet}" type="text/css"} ) |
||||||
|
end |
||||||
|
@root = @doc.add_element( "svg", { |
||||||
|
"width" => width.to_s, |
||||||
|
"height" => height.to_s, |
||||||
|
"viewBox" => "0 0 #{width} #{height}", |
||||||
|
"xmlns" => "http://www.w3.org/2000/svg", |
||||||
|
"xmlns:xlink" => "http://www.w3.org/1999/xlink", |
||||||
|
"xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/", |
||||||
|
"a3:scriptImplementation" => "Adobe" |
||||||
|
}) |
||||||
|
@root << Comment.new( " "+"\\"*66 ) |
||||||
|
@root << Comment.new( " Created with SVG::Graph " ) |
||||||
|
@root << Comment.new( " SVG::Graph by Sean E. Russell " ) |
||||||
|
@root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+ |
||||||
|
" Leo Lapworth & Stephan Morgan " ) |
||||||
|
@root << Comment.new( " "+"/"*66 ) |
||||||
|
|
||||||
|
defs = @root.add_element( "defs" ) |
||||||
|
add_defs defs |
||||||
|
if not(style_sheet && style_sheet != '') and !no_css |
||||||
|
@root << Comment.new(" include default stylesheet if none specified ") |
||||||
|
style = defs.add_element( "style", {"type"=>"text/css"} ) |
||||||
|
style << CData.new( get_style ) |
||||||
|
end |
||||||
|
|
||||||
|
@root << Comment.new( "SVG Background" ) |
||||||
|
@root.add_element( "rect", { |
||||||
|
"width" => width.to_s, |
||||||
|
"height" => height.to_s, |
||||||
|
"x" => "0", |
||||||
|
"y" => "0", |
||||||
|
"class" => "svgBackground" |
||||||
|
}) |
||||||
|
end |
||||||
|
|
||||||
|
|
||||||
|
def calculate_graph_dimensions |
||||||
|
calculate_left_margin |
||||||
|
calculate_right_margin |
||||||
|
calculate_bottom_margin |
||||||
|
calculate_top_margin |
||||||
|
@graph_width = width - @border_left - @border_right |
||||||
|
@graph_height = height - @border_top - @border_bottom |
||||||
|
end |
||||||
|
|
||||||
|
def get_style |
||||||
|
return <<EOL |
||||||
|
/* Copy from here for external style sheet */ |
||||||
|
.svgBackground{ |
||||||
|
fill:#ffffff; |
||||||
|
} |
||||||
|
.graphBackground{ |
||||||
|
fill:#f0f0f0; |
||||||
|
} |
||||||
|
|
||||||
|
/* graphs titles */ |
||||||
|
.mainTitle{ |
||||||
|
text-anchor: middle; |
||||||
|
fill: #000000; |
||||||
|
font-size: #{title_font_size}px; |
||||||
|
font-family: "Arial", sans-serif; |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
.subTitle{ |
||||||
|
text-anchor: middle; |
||||||
|
fill: #999999; |
||||||
|
font-size: #{subtitle_font_size}px; |
||||||
|
font-family: "Arial", sans-serif; |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
|
||||||
|
.axis{ |
||||||
|
stroke: #000000; |
||||||
|
stroke-width: 1px; |
||||||
|
} |
||||||
|
|
||||||
|
.guideLines{ |
||||||
|
stroke: #666666; |
||||||
|
stroke-width: 1px; |
||||||
|
stroke-dasharray: 5 5; |
||||||
|
} |
||||||
|
|
||||||
|
.xAxisLabels{ |
||||||
|
text-anchor: middle; |
||||||
|
fill: #000000; |
||||||
|
font-size: #{x_label_font_size}px; |
||||||
|
font-family: "Arial", sans-serif; |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
|
||||||
|
.yAxisLabels{ |
||||||
|
text-anchor: end; |
||||||
|
fill: #000000; |
||||||
|
font-size: #{y_label_font_size}px; |
||||||
|
font-family: "Arial", sans-serif; |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
|
||||||
|
.xAxisTitle{ |
||||||
|
text-anchor: middle; |
||||||
|
fill: #ff0000; |
||||||
|
font-size: #{x_title_font_size}px; |
||||||
|
font-family: "Arial", sans-serif; |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
|
||||||
|
.yAxisTitle{ |
||||||
|
fill: #ff0000; |
||||||
|
text-anchor: middle; |
||||||
|
font-size: #{y_title_font_size}px; |
||||||
|
font-family: "Arial", sans-serif; |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
|
||||||
|
.dataPointLabel{ |
||||||
|
fill: #000000; |
||||||
|
text-anchor:middle; |
||||||
|
font-size: 10px; |
||||||
|
font-family: "Arial", sans-serif; |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
|
||||||
|
.staggerGuideLine{ |
||||||
|
fill: none; |
||||||
|
stroke: #000000; |
||||||
|
stroke-width: 0.5px; |
||||||
|
} |
||||||
|
|
||||||
|
#{get_css} |
||||||
|
|
||||||
|
.keyText{ |
||||||
|
fill: #000000; |
||||||
|
text-anchor:start; |
||||||
|
font-size: #{key_font_size}px; |
||||||
|
font-family: "Arial", sans-serif; |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
/* End copy for external style sheet */ |
||||||
|
EOL |
||||||
|
end |
||||||
|
|
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
""" |
Loading…
Reference in new issue