lib/SVG/Graph/Graph.rb in svg-graph-2.2.0 vs lib/SVG/Graph/Graph.rb in svg-graph-2.2.1
- old
+ new
@@ -101,10 +101,11 @@
# [no_css] false
# [add_popups] false
# [number_format] '%.2f'
def initialize( config )
@config = config
+ # array of Hash
@data = []
#self.top_align = self.top_font = 0
#self.right_align = self.right_font = 0
init_with({
@@ -178,21 +179,43 @@
#
# graph.add_data({
# :data => data_sales_02,
# :title => 'Sales 2002'
# })
- def add_data conf
- @data = [] unless (defined? @data and !@data.nil?)
+ # @param conf [Hash] with the following keys:
+ # :data [Array] mandatory
+ # :title [String] mandatory name of data series for legend of graph
+ # :description [Array<String>] (optional) if given, description for each datapoint (shown in popups)
+ # :shape [Array<String>] (optional) if given, DataPoint shape is chosen based on this string instead of description
+ # :url [Array<String>] (optional) if given, link will be added to each datapoint
+ def add_data(conf)
+ @data ||= []
+ raise "No data provided by #{conf.inspect}" unless conf[:data].is_a?(Array)
- if conf[:data] and conf[:data].kind_of? Array
- @data << conf
- else
- raise "No data provided by #{conf.inspect}"
- end
+ add_data_init_or_check_optional_keys(conf, conf[:data].size)
+ @data << conf
end
+ # Checks all optional keys of the add_data method
+ def add_data_init_or_check_optional_keys(conf, datasize)
+ conf[:description] ||= Array.new(datasize)
+ conf[:shape] ||= Array.new(datasize)
+ conf[:url] ||= Array.new(datasize)
+ if conf[:description].size != datasize
+ raise "Description for popups does not have same size as provided data: #{conf[:description].size} vs #{conf[:data].size/2}"
+ end
+
+ if conf[:shape].size != datasize
+ raise "Shapes for points do not have same size as provided data: #{conf[:shape].size} vs #{conf[:data].size/2}"
+ end
+
+ if conf[:url].size != datasize
+ raise "URLs for points do not have same size as provided data: #{conf[:url].size} vs #{conf[:data].size/2}"
+ end
+ end
+
# 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
def clear_data
@@ -321,11 +344,11 @@
# amount when a numeric value is given.
# Default is false, to turn on set to true.
attr_accessor :rotate_x_labels
# This turns the Y axis labels by 90 degrees when true or by a custom
# amount when a numeric value is given.
- # Default is true, to turn on set to false.
+ # Default is false, to turn on set to true or numeric value.
attr_accessor :rotate_y_labels
# How many "steps" to use between displayed X axis labels,
# a step of one means display every label, a step of two results
# in every other label being displayed (label <gap> label <gap> label),
# a step of three results in every third label being displayed
@@ -434,21 +457,21 @@
attr_accessor :number_format
protected
- # implementation of quicksort
- # used for Schedule and Plot
+ # implementation of a multiple array sort used for Schedule and Plot
def sort( *arrys )
- sort_multiple( arrys )
+ new_arrys = arrys.transpose.sort_by(&:first).transpose
+ new_arrys.each_index { |k| arrys[k].replace(new_arrys[k]) }
end
# Overwrite configuration options with supplied options. Used
# by subclasses.
def init_with config
config.each { |key, value|
- self.send( key.to_s+"=", value ) if self.respond_to? key
+ self.send( key.to_s+"=", value ) if self.respond_to? key
}
end
# Override this (and call super) to change the margin to the left
# of the plot area. Results in @border_left being set.
@@ -466,16 +489,24 @@
# Calculates the width of the widest Y label. This will be the
# character height if the Y labels are rotated. Returns 0 if labels
# are not shown
def max_y_label_width_px
return 0 if !show_y_labels
- if !rotate_y_labels
- max_width = get_longest_label(get_y_labels).to_s.length * y_label_font_size * 0.6
- else
- max_width = y_label_font_size + 3
+ base_width = y_label_font_size + 3
+ if rotate_y_labels == true
+ self.rotate_y_labels = 90
end
- max_width += 5 + y_label_font_size if stagger_y_labels
+ if rotate_y_labels == false
+ self.rotate_y_labels = 0
+ end
+ # don't change rotate_y_label, if neither true nor false
+ label_width = get_longest_label(get_y_labels).to_s.length * y_label_font_size * 0.5
+ rotated_width = label_width * Math.cos( rotate_y_labels * Math::PI / 180).abs()
+ max_width = base_width + rotated_width
+ if stagger_y_labels
+ max_width += 5 + y_label_font_size
+ end
return max_width
end
# Override this (and call super) to change the margin to the right
@@ -518,41 +549,78 @@
add_popup( x, y, label )
make_datapoint_text( x, y, label )
end
# Adds pop-up point information to a graph only if the config option is set.
- def add_popup( x, y, label, style="" )
+ def add_popup( x, y, label, style="", url="" )
if add_popups
if( numeric?(label) )
label = @number_format % label
end
txt_width = label.length * font_size * 0.6 + 10
tx = (x+txt_width > @graph_width ? x-5 : x+5)
- t = @foreground.add_element( "text", {
+ g = Element.new( "g" )
+ g.attributes["id"] = g.object_id.to_s
+ g.attributes["visibility"] = "hidden"
+
+ # First add the mask
+ t = g.add_element( "text", {
"x" => tx.to_s,
"y" => (y - font_size).to_s,
- "class" => "dataPointLabel",
- "visibility" => "hidden",
+ "class" => "dataPointPopupMask"
})
- t.attributes["style"] = "stroke-width: 2; fill: #000; #{style}"+
+ t.attributes["style"] = style +
(x+txt_width > @graph_width ? "text-anchor: end;" : "text-anchor: start;")
t.text = label.to_s
- t.attributes["id"] = t.object_id.to_s
+ # Then add the text
+ t = g.add_element( "text", {
+ "x" => tx.to_s,
+ "y" => (y - font_size).to_s,
+ "class" => "dataPointPopup"
+ })
+ t.attributes["style"] = style +
+ (x+txt_width > @graph_width ? "text-anchor: end;" : "text-anchor: start;")
+ t.text = label.to_s
+
+ @foreground.add_element( g )
+
# add a circle to catch the mouseover
- @foreground.add_element( "circle", {
+ mouseover = Element.new( "circle" )
+ mouseover.add_attributes({
"cx" => x.to_s,
"cy" => y.to_s,
"r" => "#{popup_radius}",
"style" => "opacity: 0",
"onmouseover" =>
- "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )",
+ "document.getElementById(#{g.object_id.to_s}).style.visibility ='visible'",
"onmouseout" =>
- "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )",
+ "document.getElementById(#{g.object_id.to_s}).style.visibility = 'hidden'",
})
+ if !url.nil?
+ href = Element.new("a")
+ href.add_attribute("xlink:href", url)
+ href.add_element(mouseover)
+ @foreground.add_element(href)
+ else
+ @foreground.add_element(mouseover)
+ end
+ elsif !url.nil?
+ # add a circle to catch the mouseover
+ mouseover = Element.new( "circle" )
+ mouseover.add_attributes({
+ "cx" => x.to_s,
+ "cy" => y.to_s,
+ "r" => "#{popup_radius}",
+ "style" => "opacity: 0",
+ })
+ href = Element.new("a")
+ href.add_attribute("xlink:href", url)
+ href.add_element(mouseover)
+ @foreground.add_element(href)
end # if add_popups
- end # add_popup
+ end # def add_popup
# returns the longest label from an array of labels as string
# each object in the array must support .to_s
def get_longest_label(arry)
longest_label = arry.max{|a,b|
@@ -710,17 +778,18 @@
if x < textStr.length/2 * font_size
style << "text-anchor: start;"
elsif x > @graph_width - textStr.length/2 * font_size
style << "text-anchor: end;"
end
- # white background for better readability
- @foreground.add_element( "text", {
+ # background for better readability
+ text = @foreground.add_element( "text", {
"x" => x.to_s,
"y" => y.to_s,
- "class" => "dataPointLabel",
- "style" => "#{style} stroke: #fff; stroke-width: 2;"
- }).text = textStr
+ "class" => "dataPointLabelBackground",
+ })
+ text.text = textStr
+ text.attributes["style"] = style if style.length > 0
# actual label
text = @foreground.add_element( "text", {
"x" => x.to_s,
"y" => y.to_s,
"class" => "dataPointLabel"
@@ -829,22 +898,24 @@
# y = @graph_height - count * field_height
#
def draw_y_labels
stagger = y_label_font_size + 5
label_height = field_height
+ label_width = max_y_label_width_px
count = 0
y_offset = @graph_height + y_label_offset( label_height )
- y_offset += font_size/1.2 unless rotate_y_labels
+ y_offset += font_size/3.0
for label in get_y_labels
if show_y_labels
+ # x = 0, y = 0 is top left right next to graph area
y = y_offset - (label_height * count)
- x = rotate_y_labels ? 0 : -3
+ x = -label_width/2.0 + y_label_font_size/2.0
if stagger_y_labels and count % 2 == 1
x -= stagger
@graph.add_element( "path", {
- "d" => "M#{x} #{y} h#{stagger}",
+ "d" => "M0 #{y} h#{-stagger}",
"class" => "staggerGuideLine"
})
end
text = @graph.add_element( "text", {
@@ -855,22 +926,17 @@
textStr = label.to_s
if( numeric?(label) )
textStr = @number_format % label
end
text.text = textStr
- if rotate_y_labels
- degrees = 90
- if numeric? rotate_y_labels
- degrees = rotate_y_labels
- end
- text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
+ # note text-anchor is at bottom of textfield
+ text.attributes["style"] = "text-anchor: middle"
+ degrees = rotate_y_labels
+ text.attributes["transform"] = "translate( -#{font_size} 0 ) " +
"rotate( #{degrees} #{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
+ # text.attributes["y"] = (y - (y_label_font_size/2)).to_s
+
end # if show_y_labels
draw_y_guidelines( label_height, count ) if show_y_guidelines
count += 1
end # for label in get_y_labels
end # draw_y_labels
@@ -1006,34 +1072,10 @@
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"]
@@ -1210,15 +1252,30 @@
font-size: #{y_title_font_size}px;
font-family: "Arial", sans-serif;
font-weight: normal;
}
-.dataPointLabel{
+.dataPointLabel, .dataPointLabelBackground, .dataPointPopup, .dataPointPopupMask{
fill: #000000;
text-anchor:middle;
font-size: 10px;
font-family: "Arial", sans-serif;
font-weight: normal;
+}
+
+.dataPointLabelBackground{
+ stroke: #ffffff;
+ stroke-width: 2;
+}
+
+.dataPointPopupMask{
+ stroke: white;
+ stroke-width: 7;
+}
+
+.dataPointPopup{
+ fill: black;
+ stroke-width: 2;
}
.staggerGuideLine{
fill: none;
stroke: #000000;