lib/SVG/Graph/Graph.rb in svg-graph-2.0.1 vs lib/SVG/Graph/Graph.rb in svg-graph-2.0.2.beta

- old
+ new

@@ -100,11 +100,12 @@ # [add_popups] false # [number_format] '%.2f' def initialize( config ) @config = config @data = [] - self.top_align = self.top_font = self.right_align = self.right_font = 0 + #self.top_align = self.top_font = 0 + #self.right_align = self.right_font = 0 init_with({ :width => 500, :height => 300, :show_x_guidelines => false, @@ -190,24 +191,27 @@ # 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. - # + # # print graph.burn + # def burn raise "No data available" unless @data.size > 0 - calculations if methods.include? 'calculations' + # undocumented and not used in any sublass + # to be removed + #calculations if methods.include? 'calculations' start_svg calculate_graph_dimensions @foreground = Element.new( "g" ) draw_graph draw_titles draw_legend - draw_data + draw_data # this method needs to be implemented by child classes @graph.add_element( @foreground ) style data = "" @doc.write( data, 0 ) @@ -276,11 +280,11 @@ attr_accessor :stagger_y_labels # This turns the X axis labels by 90 degrees. # Default it false, to turn on set to true. attr_accessor :rotate_x_labels # This turns the Y axis labels by 90 degrees. - # Default it false, to turn on set to true. + # Default it true, to turn on set to false. 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 @@ -380,11 +384,13 @@ # defaults to '%.2f' attr_accessor :number_format protected - + + # implementation of quicksort + # used for Schedule and Plot def sort( *arrys ) sort_multiple( arrys ) end # Overwrite configuration options with supplied options. Used @@ -394,39 +400,49 @@ self.send( key.to_s+"=", value ) if self.respond_to? key } @popup_radius ||= 10 end - attr_accessor :top_align, :top_font, :right_align, :right_font + # unknown why needed + # attr_accessor :top_align, :top_font, :right_align, :right_font + # size of the square box in the legend which indicates the colors KEY_BOX_SIZE = 12 # Override this (and call super) to change the margin to the left # of the plot area. Results in @border_left being set. + # + # By default it is 7 + max label height(font size or string length, depending on rotate) + title height def calculate_left_margin @border_left = 7 # Check size of Y labels - max_y_label_height_px = y_label_font_size - if !rotate_y_labels - max_y_label_height_px = get_longest_label(get_y_labels).to_s.length * y_label_font_size * 0.6 + @border_left += max_y_label_width_px + if (show_y_title && (y_title_location ==:middle)) + @border_left += y_title_font_size + 5 end - - @border_left += max_y_label_height_px if show_y_labels - @border_left += max_y_label_height_px + 10 if stagger_y_labels - @border_left += y_title_font_size + 5 if (show_y_title && (y_title_location ==:middle)) end - # Calculates the width of the widest Y label. This will be the - # character height if the Y labels are rotated + # character height if the Y labels are rotated. Returns 0 if labels + # are not shown def max_y_label_width_px - return font_size if rotate_y_labels + 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 + end + max_width += 10 if stagger_y_labels + return max_width end # Override this (and call super) to change the margin to the right # of the plot area. Results in @border_right being set. + # + # By default it is 7 + width of the key if it is placed on the right + # or the maximum of this value or the tilte length (if title is placed at :end) def calculate_right_margin @border_right = 7 if key and key_position == :right val = keys.max { |a,b| a.length <=> b.length } @border_right += val.length * key_font_size * 0.6 @@ -439,43 +455,55 @@ end # Override this (and call super) to change the margin to the top # of the plot area. Results in @border_top being set. + # + # This is 5 + the Title size + 5 + subTitle size def calculate_top_margin @border_top = 5 @border_top += [title_font_size, y_title_font_size].max if (show_graph_title || (y_title_location ==:end)) @border_top += 5 @border_top += subtitle_font_size if show_graph_subtitle end + def add_datapoint_text_and_popup( x, y, label ) + add_popup( x, y, label ) + make_datapoint_text( x, y, label ) + end - # Adds pop-up point information to a graph. - def add_popup( x, y, label ) - txt_width = label.length * font_size * 0.6 + 10 - tx = (x+txt_width > width ? x-5 : x+5) - t = @foreground.add_element( "text", { - "x" => tx.to_s, - "y" => (y - font_size).to_s, - "visibility" => "hidden", - }) - t.attributes["style"] = "fill: #000; "+ - (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;") - t.text = label.to_s - t.attributes["id"] = t.object_id.to_s - - @foreground.add_element( "circle", { - "cx" => x.to_s, - "cy" => y.to_s, - "r" => "#{@popup_radius}", - "style" => "opacity: 0", - "onmouseover" => - "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )", - "onmouseout" => - "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )", - }) - + # Adds pop-up point information to a graph only if the config option is set. + def add_popup( x, y, label, style="" ) + if add_popups + if( numeric?(label) ) + label = @number_format % label + end + txt_width = label.length * font_size * 0.6 + 10 + tx = (x+txt_width > width ? x-5 : x+5) + t = @foreground.add_element( "text", { + "x" => tx.to_s, + "y" => (y - font_size).to_s, + "class" => "dataPointLabel", + "visibility" => "hidden", + }) + t.attributes["style"] = "stroke-width: 2; fill: #000; #{style}"+ + (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;") + t.text = label.to_s + t.attributes["id"] = t.object_id.to_s + + # add a circle to catch the mouseover + @foreground.add_element( "circle", { + "cx" => x.to_s, + "cy" => y.to_s, + "r" => "#{popup_radius}", + "style" => "opacity: 0", + "onmouseover" => + "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )", + "onmouseout" => + "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )", + }) + end # add_popups end # 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) @@ -489,27 +517,37 @@ return longest_label end # Override this (and call super) to change the margin to the bottom # of the plot area. Results in @border_bottom being set. + # + # 7 + max label height(font size or string length, depending on rotate) + title height def calculate_bottom_margin @border_bottom = 7 if key and key_position == :bottom @border_bottom += @data.size * (font_size + 5) @border_bottom += 10 end - if show_x_labels - max_x_label_height_px = x_label_font_size - if rotate_x_labels - max_x_label_height_px = get_longest_label(get_x_labels).to_s.length * x_label_font_size * 0.6 - end - - @border_bottom += max_x_label_height_px - @border_bottom += max_x_label_height_px + 10 if stagger_x_labels + @border_bottom += max_x_label_height_px + if (show_x_title && (x_title_location ==:middle)) + @border_bottom += x_title_font_size + 5 end - @border_bottom += x_title_font_size + 5 if (show_x_title && (x_title_location ==:middle)) end + + # returns the maximum height of the labels respect the rotation or 0 if + # the labels are not shown + def max_x_label_height_px + return 0 if !show_x_labels + + if rotate_x_labels + max_height = get_longest_label(get_x_labels).to_s.length * x_label_font_size * 0.6 + else + max_height = x_label_font_size + 3 + end + max_height += 10 if stagger_x_labels + return max_height + end # Draws the background, axis, and labels. def draw_graph @graph = @root.add_element( "g", { @@ -551,33 +589,40 @@ # check if an object can be converted to float def numeric?(object) true if Float(object) rescue false end + # adds the datapoint text to the graph only if the config option is set def make_datapoint_text( x, y, value, style="" ) if show_data_values textStr = value if( numeric?(value) ) textStr = @number_format % value end + # change anchor is label overlaps axis + if x < textStr.length/2 * font_size * 0.6 + style << "text-anchor: start;" + end + # white background for better readability @foreground.add_element( "text", { "x" => x.to_s, "y" => y.to_s, "class" => "dataPointLabel", "style" => "#{style} stroke: #fff; stroke-width: 2;" }).text = textStr + # actual label text = @foreground.add_element( "text", { "x" => x.to_s, "y" => y.to_s, "class" => "dataPointLabel" }) text.text = textStr 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 @@ -589,10 +634,14 @@ else step = (count + 1) % step_x_labels end if step == 0 then + label = label.to_s + if( numeric?(label) ) + label = @number_format % label + end text = @graph.add_element( "text" ) text.attributes["class"] = "xAxisLabels" text.text = label.to_s x = count * label_width + x_label_offset( label_width ) @@ -630,24 +679,43 @@ # Centered in the field, should be width/2. Start, 0. def y_label_offset( height ) 0 end - + # override this method in child class + # must return the array of labels for the x-axis + def get_x_labels + end + + # override this method in child class + # must return the array of labels for the y-axis + # this method defines @y_scale_division + def get_y_labels + end + + # space in px between x-labels def field_width - (@graph_width.to_f - font_size*2*right_font) / - (get_x_labels.length - right_align) + #(@graph_width.to_f - font_size*2*right_font) / + # (get_x_labels.length - right_align) + @graph_width.to_f / get_x_labels.length end - + # space in px between the y-labels def field_height - (@graph_height.to_f - font_size*2*top_font) / - (get_y_labels.length - top_align) + #(@graph_height.to_f - font_size*2*top_font) / + # (get_y_labels.length - top_align) + @graph_height.to_f / get_y_labels.length end - # Draws the Y axis labels + # Draws the Y axis labels, the Y-Axis (@graph_height) is divided equally into #get_y_labels.lenght sections + # So the y coordinate for an arbitrary value is calculated as follows: + # y = @graph_height equals the min_value + # #normalize value of a single scale_division: + # count = value /(@y_scale_division) + # y = @graph_height - count * field_height + # def draw_y_labels stagger = y_label_font_size + 5 if show_y_labels label_height = field_height @@ -733,36 +801,32 @@ "class" => "subTitle" }).text = graph_subtitle.to_s end if show_x_title - y = @graph_height + @border_top + x_title_font_size if (x_title_location == :end) - y = y - x_title_font_size/2.0 - x = width - x_title.length * x_title_font_size * 0.6/2.0 + y = @graph_height + @border_top + x_title_font_size/2.0 + x = @border_left + @graph_width + x_title.length * x_title_font_size * 0.6/2.0 else - x = width / 2 - if show_x_labels - y += x_label_font_size + 5 if stagger_x_labels - y += x_label_font_size + 5 - end + y = @graph_height + @border_top + x_title_font_size + max_x_label_height_px + x = @border_left + @graph_width / 2 end @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) if (y_title_location == :end) x = y_title.length * y_title_font_size * 0.6/2.0 # positioning is not optimal but ok for now y = @border_top - y_title_font_size/2.0 else - y = height / 2 + x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3) + y = @border_top + @graph_height / 2 end text = @root.add_element("text", { "x" => x.to_s, "y" => y.to_s, "class" => "yAxisTitle", @@ -814,16 +878,16 @@ 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 = (not rotate_x_labels) ? - x_label_font_size : - get_x_labels.max{|a,b| - a.to_s.length<=>b.to_s.length - }.to_s.length * x_label_font_size * 0.6 - x_label_font_size + # max_x_label_height_px = (not rotate_x_labels) ? + # x_label_font_size : + # get_x_labels.max{|a,b| + # a.to_s.length<=>b.to_s.length + # }.to_s.length * x_label_font_size * 0.6 + # 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 @@ -896,14 +960,21 @@ return rv end # Override and place code to add defs here + # @param defs [REXML::Element] def add_defs defs end - + # Creates the XML document and adds the root svg element with + # the width, height and viewBox attributes already set. + # The element is stored as @root. + # + # In addition a rectangle background of the same size as the + # svg is added. + # def start_svg # Base document @doc = Document.new @doc << XMLDecl.new @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } + @@ -944,10 +1015,10 @@ "y" => "0", "class" => "svgBackground" }) end - + # def calculate_graph_dimensions calculate_left_margin calculate_right_margin calculate_bottom_margin calculate_top_margin