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;