class PDF::Charts::StdDev

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.

Constants

DataPoint

A data element.

Attributes

bar[RW]

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.

data[R]

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.

datapoint_width[RW]

The width of a single datapoint.

dot[RW]

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.

height[RW]

The height of the chart in PDF user units. Default 200 units.

inner_borders[RW]

The inner border style. If nil, no inner borders are drawn. This is a PDF::Charts::StdDev::Marker object.

label[RW]

The label style of the labels if they are displayed. This must be a PDF::Charts::StdDev::Label object.

leading_gap[RW]

The minimum gap between the chart and the bottom of the page, in PDF user units.

lower_crossbar[RW]

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.

maximum_width[RW]

The maximum width of the chart in PDF user units. Default 500 units.

outer_borders[RW]

The outer border style. If nil, no inner borders are drawn. This is a PDF::Charts::StdDev::Marker object.

scale[RW]

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.

show_labels[RW]

This will be true if labels are to be displayed.

upper_crossbar[RW]

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.

Public Instance Methods

render_on(pdf) click to toggle source

Draw the standard deviation chart on the supplied PDF document.

# File lib/pdf/charts/stddev.rb, line 219
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

Public Class Methods

new() { |self| ... } click to toggle source
# File lib/pdf/charts/stddev.rb, line 109
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