#-- # PDF::Writer for Ruby. # http://rubyforge.org/projects/ruby-pdf/ # Copyright 2003 - 2005 Austin Ziegler. # # Licensed under a MIT-style licence. See LICENCE in the main distribution # for full licensing information. # # $Id: stddev.rb 186 2007-12-10 22:58:48Z sandal $ #++ require 'pdf/writer' require 'pdf/charts' require 'ostruct' # Creates a standard deviation chart. This is a type of chart that is # effective for the display of survey results or other data that can # easily be measured in terms of the average and the standard deviation # from that average. # # The scale of responses is the vertical scale; the average data points # and standard deviation values are the horizontal scale. class PDF::Charts::StdDev # A data element. DataPoint = Struct.new(:label, :average, :stddev) # A label for displaying the scale (vertical) of data in the dataset or # the data set identifiers. class Label def initialize yield self if block_given? end # The height of the label, in PDF user units. Ignored for scale # labels. attr_accessor :height # The background color of the label. Ignored for scale labels. attr_accessor :background_color # The text color of the label. attr_accessor :text_color # The text size, in points, of the label. attr_accessor :text_size # The padding of the label. Only used for scale labels. attr_accessor :pad # The decimal precision of the label. Only used for scale labels. attr_accessor :decimal_precision end # The scale of the dataset. class Scale def initialize(args = { }) @range = args[:range] @step = args[:step] @style = args[:style] @show_labels = false yield self if block_given? raise TypeError, PDF::Lange[:charts_stddev_scale_norange] if @range.nil? raise TypeError, PDF::Lange[:charts_stddev_scale_nostep] if @step.nil? end # Range of the scale. This should be a Range object. attr_accessor :range # The lower end of the range of the scale. The scale range may be # modified by changing this value. attr_accessor :first def first #:nodoc: @range.first end def first=(ff) #:nodoc: @range = (ff..@range.last) end # The upper end of the range of the scale. The scale range may be # modified by changing this value. attr_accessor :last def last #:nodoc: @range.last end def last=(ll) #:nodoc: @range = (@range.first..ll) end # Defines the step of the scale. Each step represents a vertical # position on the chart. attr_accessor :step # Defines the line style for the scale on the chart. If this is unset # (+nil+), there will be no horizontal marks across the chart for the # steps of the scale. attr_accessor :style # Shows the scale labels if +true+. attr_accessor :show_labels # Defines the label options. attr_accessor :label end # This is any line that will be drawn; this is a combination of the line # style (which must be a PDF::Writer::StrokeStyle object) and a color. class Marker def initialize yield self if block_given? end # The stroke style of the marker. attr_accessor :style # The stroke color of the marker. attr_accessor :color end def initialize @data = [] @scale = Scale.new do |scale| scale.range = 0..6 scale.step = 1 scale.style = PDF::Writer::StrokeStyle.new(0.25) scale.show_labels = false scale.label = Label.new do |label| label.text_size = 8 label.text_color = Color::RGB::Black label.pad = 2 label.decimal_precision = 1 end end @leading_gap = 10 @show_labels = true @label = Label.new do |label| label.height = 25 label.background_color = Color::RGB::Black label.text_color = Color::RGB::White label.text_size = 12 end @outer_borders = Marker.new do |marker| marker.style = PDF::Writer::StrokeStyle.new(1.5) marker.color = Color::RGB::Black end @inner_borders = nil @dot = Marker.new do |marker| marker.style = PDF::Writer::StrokeStyle.new(5) marker.color = Color::RGB::Black end @bar = Marker.new do |marker| marker.style = PDF::Writer::StrokeStyle.new(0.5) marker.color = Color::RGB::Black end @upper_crossbar = Marker.new do |marker| marker.style = PDF::Writer::StrokeStyle.new(1) marker.color = Color::RGB::Black end @lower_crossbar = Marker.new do |marker| marker.style = PDF::Writer::StrokeStyle.new(1) marker.color = Color::RGB::Black end @height = 200 @maximum_width = 500 @datapoint_width = 35 yield self if block_given? end # The data used to generate the standard deviation chart. This is an # array of DataPoint objects, each containing a +label+, an +average+, # and the +stddev+ (standard deviation) from that average. attr_reader :data # The scale of the chart. All values must be within this range. This # will be a Scale object. It defaults to a scale of 0..6 with a step of # 1. attr_accessor :scale # The minimum gap between the chart and the bottom of the page, in # PDF user units. attr_accessor :leading_gap # This will be +true+ if labels are to be displayed. attr_accessor :show_labels # The label style of the labels if they are displayed. This must be a # PDF::Charts::StdDev::Label object. attr_accessor :label # The inner border style. If +nil+, no inner borders are drawn. This is # a PDF::Charts::StdDev::Marker object. attr_accessor :inner_borders # The outer border style. If +nil+, no inner borders are drawn. This is # a PDF::Charts::StdDev::Marker object. attr_accessor :outer_borders # The dot marker. A filled circle will be drawn with this information. # If +nil+, the dot will not be drawn. This is a # PDF::Charts::StdDev::Marker object. attr_accessor :dot # The standard deviation bar. A line will be drawn through the dot # marker (if drawn) from the upper to lower standard deviation. # If +nil+, the line will not be drawn. This is a # PDF::Charts::StdDev::Marker object. attr_accessor :bar # The upper crossbar. A line will be drawn across the top of the # standard deviation bar to the width of the dot marker. If #dot is # +nil+, then the line will be twice as wide as it is thick. If +nil+, # the upper crossbar will not be drawn. This is a # PDF::Charts::StdDev::Marker object. attr_accessor :upper_crossbar # The lower crossbar. A line will be drawn across the bottom of the # standard deviation bar to the width of the dot marker. If #dot is # +nil+, then the line will be twice as wide as it is thick. If +nil+, # the lower crossbar will not be drawn. This is a # PDF::Charts::StdDev::Marker object. attr_accessor :lower_crossbar # The height of the chart in PDF user units. Default 200 units. attr_accessor :height # The maximum width of the chart in PDF user units. Default 500 units. attr_accessor :maximum_width # The width of a single datapoint. attr_accessor :datapoint_width # Draw the standard deviation chart on the supplied PDF document. def render_on(pdf) raise TypeError, PDF::Writer::Lang[:charts_stddev_data_empty] if @data.empty? data = @data.dup leftover_data = nil loop do # Set up the scale information. scale = [] (@scale.first + @scale.step).step(@scale.last, @scale.step) do |ii| scale << "%01.#{@scale.label.decimal_precision}f" % ii end scales = PDF::Writer::OHash.new scale.each_with_index do |gg, ii| scales[ii] = OpenStruct.new scales[ii].value = gg end # Add information about the scales' locations to the scales # hash. Note that the count is one smaller than it should be, so we're # increasing it. The first scale is the bottom of the chart. scale_count = scale.size + 1 label_height_adjuster = 0 label_height_adjuster = @label.height if @show_labels chart_area_height = @height - label_height_adjuster scale_height = chart_area_height / scale_count.to_f scales.each_key do |index| this_height = scale_height * (index + 1) + @label.height scales[index].line_height = this_height if @scale.show_labels scales[index].label_height = this_height - (@scale.label.text_size / 3.0) end end # How many sections do we need in this chart, and how wide will it # need to be? chunk_width = @datapoint_width num_chunks = data.size widest_scale_label = 0 if @scale.show_labels scales.each_value do |scale| this_width = pdf.text_width(scale.value, @scale.label.text_size) widest_scale_label = this_width if this_width > widest_scale_label end end chart_width = chunk_width * num_chunks total_width = chart_width + widest_scale_label + @scale.label.pad # What happens if the projected width of the chart is too big? # Figure out how to break the chart in pieces. if total_width > @maximum_width max_column_count = 0 base_width = widest_scale_label + @scale.label.pad (1..(num_chunks + 1)).each do |ii| if (base_width + (ii * chunk_width)) > @maximum_width break else max_column_count += 1 end end leftover_data = data.slice!(max_column_count, -1) num_chunks = data.size chart_width = chunk_width * num_chunks total_width = chart_width + widest_scale_label + @scale.label.pad end chart_y = pdf.y - @height + @leading_gap chart_y += (@outer_borders.style.width * 2.0) if @outer_borders if chart_y < pdf.bottom_margin pdf.start_new_page chart_y = pdf.y - @height chart_y += (@outer_borders.style.width * 2.0) if @outer_borders end chart_x = pdf.absolute_x_middle - (total_width / 2.0) + widest_scale_label # Add labels, if needed. if @show_labels pdf.save_state pdf.fill_color! @label.background_color # Draw a rectangle for each label num_chunks.times do |ii| this_x = chart_x + ii * chunk_width pdf.rectangle(this_x, chart_y, chunk_width, @label.height).fill end # Add a border above the label rectangle. if @outer_borders pdf.stroke_style! @outer_borders.style pdf.line(chart_x, chart_y + @label.height, chart_x + chart_width, chart_y + @label.height).stroke end pdf.fill_color! @label.text_color data.each_with_index do |datum, ii| label = datum.label.to_s label_width = pdf.text_width(label, @label.text_size) this_x = chart_x + (ii * chunk_width) + (chunk_width / 2.0) - (label_width / 2.0) this_y = chart_y + (@label.height / 2.0) - (@label.text_size / 3.0) pdf.add_text(this_x, this_y, label, @label.text_size) end pdf.restore_state end if @inner_borders pdf.save_state pdf.stroke_color! @inner_borders.color pdf.stroke_style! @inner_borders.style (num_chunks - 1).times do |ii| this_x = chart_x + (ii * chunk_width) + chunk_width pdf.line(this_x, chart_y, this_x, chart_y + @height).stroke end pdf.restore_state end pdf.save_state if @outer_borders pdf.stroke_color! @outer_borders.color pdf.stroke_style! @outer_borders.style pdf.rectangle(chart_x, chart_y, chart_width, @height).stroke end if @scale.style pdf.save_state pdf.stroke_style! @scale.style scales.each_value do |scale| this_y = chart_y + scale.line_height pdf.line(chart_x, this_y, chart_x + chart_width, this_y).stroke end pdf.restore_state end if @scale.show_labels pdf.save_state scales.each_value do |scale| this_y = chart_y + scale.label_height label_width = pdf.text_width(scale.value, @scale.label.text_size) this_x = chart_x - label_width - @scale.label.pad pdf.fill_color! @scale.label.text_color pdf.add_text(this_x, this_y, scale.value, @scale.label.text_size) end pdf.restore_state end data.each_with_index do |datum, ii| avg_height = datum.average * scale_height stddev_height = datum.stddev * scale_height this_y = chart_y + label_height_adjuster + avg_height this_x = chart_x + (ii * chunk_width) + (chunk_width / 2.0) line_top_y = this_y + (stddev_height / 2.0) line_bot_y = this_y - (stddev_height / 2.0) # Plot the dot if @dot pdf.stroke_color! @dot.color pdf.stroke_style! @dot.style pdf.circle_at(this_x, this_y, (@dot.style.width / 2.0)).fill end # Plot the bar if @bar pdf.stroke_color! @bar.color pdf.stroke_style! @bar.style pdf.line(this_x, line_top_y, this_x, line_bot_y).stroke end # Plot the crossbars if @upper_crossbar if @dot cb_width = @dot.style.width else cb_width = @upper_crossbar.style.width end pdf.stroke_color! @upper_crossbar.color pdf.stroke_style! @upper_crossbar.style pdf.line(this_x - cb_width, line_top_y, this_x + cb_width, line_top_y).stroke end if @lower_crossbar if @dot cb_width = @dot.style.width else cb_width = @lower_crossbar.style.width end pdf.stroke_color! @lower_crossbar.color pdf.stroke_style! @lower_crossbar.style pdf.line(this_x - cb_width, line_bot_y, this_x + cb_width, line_bot_y).stroke end end pdf.restore_state pdf.y = chart_y break if leftover_data.nil? data = leftover_data leftover_data = nil end pdf.y end end