Class: PDF::Charts::StdDev

Inherits:
Object
  • Object
show all
Defined in:
lib/pdf/charts/stddev.rb

Overview

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.

Defined Under Namespace

Classes: DataPoint, Label, Marker, Scale

Constant Summary collapse

VERSION =
'1.2.0'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize {|_self| ... } ⇒ StdDev

Returns a new instance of StdDev

Yields:

  • (_self)

Yield Parameters:


110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/pdf/charts/stddev.rb', line 110

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

Instance Attribute Details

#barObject

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.


198
199
200
# File 'lib/pdf/charts/stddev.rb', line 198

def bar
  @bar
end

#dataObject (readonly)

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.


167
168
169
# File 'lib/pdf/charts/stddev.rb', line 167

def data
  @data
end

#datapoint_widthObject

The width of a single datapoint.


217
218
219
# File 'lib/pdf/charts/stddev.rb', line 217

def datapoint_width
  @datapoint_width
end

#dotObject

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.


193
194
195
# File 'lib/pdf/charts/stddev.rb', line 193

def dot
  @dot
end

#heightObject

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


213
214
215
# File 'lib/pdf/charts/stddev.rb', line 213

def height
  @height
end

#inner_bordersObject

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


185
186
187
# File 'lib/pdf/charts/stddev.rb', line 185

def inner_borders
  @inner_borders
end

#labelObject

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


181
182
183
# File 'lib/pdf/charts/stddev.rb', line 181

def label
  @label
end

#leading_gapObject

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


175
176
177
# File 'lib/pdf/charts/stddev.rb', line 175

def leading_gap
  @leading_gap
end

#lower_crossbarObject

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.


210
211
212
# File 'lib/pdf/charts/stddev.rb', line 210

def lower_crossbar
  @lower_crossbar
end

#maximum_widthObject

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


215
216
217
# File 'lib/pdf/charts/stddev.rb', line 215

def maximum_width
  @maximum_width
end

#outer_bordersObject

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


188
189
190
# File 'lib/pdf/charts/stddev.rb', line 188

def outer_borders
  @outer_borders
end

#scaleObject

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.


171
172
173
# File 'lib/pdf/charts/stddev.rb', line 171

def scale
  @scale
end

#show_labelsObject

This will be true if labels are to be displayed.


178
179
180
# File 'lib/pdf/charts/stddev.rb', line 178

def show_labels
  @show_labels
end

#upper_crossbarObject

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.


204
205
206
# File 'lib/pdf/charts/stddev.rb', line 204

def upper_crossbar
  @upper_crossbar
end

Instance Method Details

#render_on(pdf) ⇒ Object

Draw the standard deviation chart on the supplied PDF document.

Raises:

  • (TypeError)

220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
# File 'lib/pdf/charts/stddev.rb', line 220

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