prawn-graph 1.0.0.pre1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,128 @@
1
+ module Prawn
2
+ module Graph
3
+ module ChartComponents
4
+ # A Prawn::Graph::Canvas represents the area on which a graph will be drawn. Think of it
5
+ # as the container in which your chart / graph will be sized to fit within.
6
+ #
7
+ class Canvas
8
+ attr_reader :layout, :series, :prawn, :theme, :options
9
+
10
+ # @param series [Array[Prawn::Graph::Series]]
11
+ # @param prawn [Prawn::Document]
12
+ # @param options [Hash]
13
+ # @return [Prawn::Graph::Canvas] a canvas ready to be drawn on the provided +prawn+ document.
14
+ #
15
+ def initialize(series, prawn, options = {}, &block)
16
+ @series = series
17
+ verify_series_are_ok!
18
+ @options = {xaxis_labels: []}.merge(options.merge({ series_count: series.size }))
19
+ @prawn = prawn
20
+ @theme = Prawn::Graph::Theme::Default
21
+ @layout = Prawn::Graph::Calculations::LayoutCalculator.new([prawn.bounds.width, prawn.bounds.height], @options, @theme).calculate
22
+
23
+ yield self if block_given?
24
+ end
25
+
26
+ # Fires off the actual drawing of the chart on the provided canvas.
27
+ # @return [nil]
28
+ #
29
+ def draw
30
+ prawn.bounding_box(position, :width => layout.canvas_width, :height => layout.canvas_height, padding: 0) do
31
+ prawn.save_graphics_state do
32
+ apply_theme!
33
+ render_title_area!
34
+ render_series_keys!
35
+ render_graph_area!
36
+ end
37
+ end
38
+ end
39
+
40
+ # The coordinates which the canvas will be drawn at
41
+ # @return [Array] [X-Coord, Y-Coord]
42
+ #
43
+ def position
44
+ @options[:at] || [0,0]
45
+ end
46
+
47
+ private
48
+
49
+ def apply_theme!
50
+ prawn.fill_color @theme.default
51
+ prawn.stroke_color @theme.default
52
+ prawn.font_size @theme.font_sizes.default
53
+ end
54
+
55
+ def plot_series!
56
+ bar_charts = series.collect{ |s| s if s.type == :bar }.compact
57
+ others = series - bar_charts
58
+
59
+ BarChartRenderer.new(bar_charts, self, theme.series[0..(bar_charts.size - 1)]).render unless bar_charts.empty?
60
+
61
+ i = bar_charts.size
62
+ others.each do |series|
63
+ LineChartRenderer.new(series, self, theme.series[i]).render
64
+ i+=1
65
+ end
66
+ end
67
+
68
+ def render_graph_area!
69
+ if layout.graph_area.renderable?
70
+ prawn.bounding_box layout.graph_area.point, width: layout.graph_area.width, height: layout.graph_area.height do
71
+ plot_series!
72
+ end
73
+ end
74
+ end
75
+
76
+ def render_title_area!
77
+ if layout.title_area.renderable?
78
+ prawn.text_box "<color rgb=\"#{@theme.title}\">#{@options[:title]}</color>", at: layout.title_area.point, inline_format: true,
79
+ valign: :center, align: :center, size: @theme.font_sizes.main_title, width: layout.title_area.width, height: layout.title_area.height
80
+ end
81
+ end
82
+
83
+ def render_series_keys!
84
+ if layout.series_key_area.renderable?
85
+ prawn.bounding_box layout.series_key_area.point, width: layout.series_key_area.width, height: layout.series_key_area.height do
86
+ series.each_with_index do |series, i|
87
+ series_offset = i + 1
88
+
89
+ prawn.save_graphics_state do
90
+ prawn.stroke_color = theme.axes
91
+ prawn.line_width = 0.5
92
+ prawn.fill_color = theme.series[i]
93
+
94
+
95
+ series_offset = series_offset * theme.font_sizes.series_key
96
+
97
+ title = series.title || "Series #{series_offset}"
98
+ top_position = (prawn.bounds.top - (series_offset * 3))
99
+
100
+
101
+ prawn.fill_and_stroke_rectangle([ theme.font_sizes.series_key, top_position ], theme.font_sizes.series_key, theme.font_sizes.series_key)
102
+
103
+ prawn.fill_color = theme.axes
104
+ prawn.text_box title, at: [ (theme.font_sizes.series_key * 3), top_position ], size: theme.font_sizes.series_key, height: (series_offset * 2)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ # Verifies that we provide an array-like object of Prawn::Graph::Series instances to
112
+ # the Canvas, for later rendering.
113
+ #
114
+ def verify_series_are_ok!
115
+ if @series.respond_to?(:each) && @series.respond_to?(:collect)
116
+ classes = @series.collect{ |c| c.is_a?(Prawn::Graph::Series) }.uniq
117
+ if classes.size > 1 || classes[0] != true
118
+ raise RuntimeError.new("All of the items provided must be instances of Prawn::Graph::Series")
119
+ end
120
+ else
121
+ raise RuntimeError.new("Series provided must be an Array (or Array-like) object.")
122
+ end
123
+ end
124
+
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,92 @@
1
+ module Prawn
2
+ module Graph
3
+ module ChartComponents
4
+
5
+ # The Prawn::Graph::ChartComponents::SeriesRenderer is used to plot indivdual Prawn::Graph::Series on
6
+ # a Prawn::Graph::ChartComponents::Canvas and its associated Prawn::Document.
7
+ #
8
+ class LineChartRenderer < SeriesRenderer
9
+
10
+ def render
11
+ render_line_chart
12
+ end
13
+
14
+ private
15
+
16
+ def mark_minimum_value(min_marked, value, x, y)
17
+ if @series.mark_minimum? && min_marked == false && value != 0 && value == @series.min
18
+ draw_marker_point(@canvas.theme.min, x, y)
19
+ min_marked = true
20
+ end
21
+ min_marked
22
+ end
23
+
24
+ def mark_maximum_value(max_marked, value, x, y)
25
+ if @series.mark_maximum? && max_marked == false && value != 0 && value == @series.max
26
+ draw_marker_point(@canvas.theme.max, x, y)
27
+ max_marked = true
28
+ end
29
+ max_marked
30
+ end
31
+
32
+ def mark_average_line
33
+ if @series.mark_average?
34
+ average_y_coordinate = (point_height_percentage(@series.avg) * @plot_area_height) - 5
35
+ prawn.line_width = 1
36
+ prawn.stroke_color = @color
37
+ prawn.dash(2)
38
+ prawn.stroke_line([0, average_y_coordinate], [ @plot_area_width, average_y_coordinate ])
39
+ prawn.undash
40
+ end
41
+ end
42
+
43
+ def render_line_chart
44
+ prawn.bounding_box [@graph_area.point[0] + 5, @graph_area.point[1] - 20], width: @plot_area_width, height: @plot_area_height do
45
+ j = 2
46
+ prawn.save_graphics_state do
47
+ max_marked = false
48
+ min_marked = false
49
+
50
+ @series.values.each_with_index do |v, i|
51
+ next if i == 0
52
+
53
+ width_per_point = (@plot_area_width / @series.size).round(2).to_f
54
+ spacing = width_per_point
55
+
56
+ prawn.line_width = 2
57
+ prawn.fill_color = @color
58
+ prawn.stroke_color = @color
59
+
60
+ previous_value = @series.values[i - 1]
61
+ this_value = v
62
+
63
+ previous_y = (point_height_percentage(previous_value) * @plot_area_height) - 5
64
+ this_y = (point_height_percentage(this_value) * @plot_area_height) - 5
65
+
66
+ previous_x_offset = ((spacing * (j - 1)) - spacing) + (spacing / 2.0)
67
+ this_x_offset = ((spacing * j) - spacing) + (spacing / 2.0)
68
+
69
+ unless previous_value.zero? || this_value.zero?
70
+ prawn.stroke_line([previous_x_offset, previous_y], [ this_x_offset, this_y ])
71
+ prawn.fill_color = @canvas.theme.markers
72
+ prawn.fill_ellipse([ ( previous_x_offset), previous_y ], 1)
73
+ prawn.fill_ellipse([ ( this_x_offset), this_y ], 1)
74
+ end
75
+
76
+ min_marked = mark_minimum_value(min_marked, previous_value, previous_x_offset, previous_y)
77
+ min_marked = mark_minimum_value(min_marked, this_value, this_x_offset, this_y)
78
+ max_marked = mark_maximum_value(max_marked, previous_value, previous_x_offset, previous_y)
79
+ max_marked = mark_maximum_value(max_marked, this_value, this_x_offset, this_y)
80
+
81
+ j += 1
82
+ end
83
+
84
+ mark_average_line
85
+ end
86
+ render_axes
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,120 @@
1
+ module Prawn
2
+ module Graph
3
+ module ChartComponents
4
+
5
+ # The Prawn::Graph::ChartComponents::SeriesRenderer is used to plot indivdual Prawn::Graph::Series on
6
+ # a Prawn::Graph::ChartComponents::Canvas and its associated Prawn::Document.
7
+ #
8
+ class SeriesRenderer
9
+ # @param series [Prawn::Graph::Series]
10
+ # @param canvas [Prawn::Graph::ChartComponents::Canvas]
11
+ #
12
+ def initialize(series, canvas, color = '000000')
13
+ if series.is_a?(Array)
14
+ raise ArgumentError.new("series must be a Prawn::Graph::Series") unless series.first.is_a?(Prawn::Graph::Series)
15
+ else
16
+ raise ArgumentError.new("series must be a Prawn::Graph::Series") unless series.is_a?(Prawn::Graph::Series)
17
+ end
18
+ raise ArgumentError.new("canvas must be a Prawn::Graph::ChartComponents::Canvas") unless canvas.is_a?(Prawn::Graph::ChartComponents::Canvas)
19
+
20
+ @series = series
21
+ @canvas = canvas
22
+ @prawn = canvas.prawn
23
+ @color = color
24
+
25
+ @graph_area = @canvas.layout.graph_area
26
+
27
+ @plot_area_width = @graph_area.width - 25
28
+ @plot_area_height = @graph_area.height - 20
29
+ end
30
+
31
+ def render
32
+ render_chart
33
+ end
34
+
35
+ private
36
+
37
+ def render_chart
38
+ raise "Subclass Me"
39
+ end
40
+
41
+ def render_axes
42
+ prawn.stroke_color = @canvas.theme.axes
43
+ prawn.fill_color = @canvas.theme.axes
44
+ prawn.stroke_horizontal_line(0, @plot_area_width, at: 0)
45
+ prawn.stroke_vertical_line(0, @plot_area_height, at: 0)
46
+ prawn.fill_and_stroke_ellipse [ 0,0], 1
47
+
48
+ add_y_axis_label(max)
49
+ add_y_axis_label(min)
50
+ add_y_axis_label(avg)
51
+ add_y_axis_label(mid)
52
+
53
+ add_x_axis_labels
54
+ end
55
+
56
+ def add_x_axis_labels
57
+ return if @canvas.options[:xaxis_labels].size.zero?
58
+ width_of_each_label = (@plot_area_width / @canvas.options[:xaxis_labels].size) - 1
59
+ @canvas.options[:xaxis_labels].each_with_index do |label, i|
60
+ offset = i + 1
61
+ position = ((offset * width_of_each_label) - width_of_each_label) + 1
62
+
63
+ prawn.text_box label, at: [ position, -2 ], width: width_of_each_label, height: 6, valign: :center, align: :center,
64
+ overflow: :shrink_to_fit
65
+ end
66
+ end
67
+
68
+ def add_y_axis_label(value)
69
+ unless value.zero?
70
+ y = (point_height_percentage(value) * @plot_area_height)
71
+ prawn.text_box "#{max}", at: [-14, y], height: 5, overflow: :shrink_to_fit, width: 12, valign: :bottom, align: :right
72
+ end
73
+ end
74
+
75
+ # Calculates the relative height of a given point based on the maximum value present in
76
+ # the series.
77
+ #
78
+ def point_height_percentage(value)
79
+ ((BigDecimal(value, 10)/BigDecimal(@canvas.series.collect(&:max).max, 10)) * BigDecimal(1)).round(2) rescue 0
80
+ end
81
+
82
+ def prawn
83
+ @prawn
84
+ end
85
+
86
+ def max
87
+ @series.max || 0
88
+ end
89
+
90
+ def min
91
+ @series.min || 0
92
+ end
93
+
94
+ def avg
95
+ @series.avg || 0
96
+ end
97
+
98
+ def mid
99
+ (min + max) / 2 rescue 0
100
+ end
101
+
102
+ def draw_marker_point(color, x_position, y_position)
103
+ prawn.save_graphics_state do
104
+ prawn.fill_color = color
105
+ prawn.stroke_color = color
106
+ prawn.line_width = 1
107
+
108
+ prawn.dash(2)
109
+ prawn.stroke_line([x_position, 0], [x_position, y_position])
110
+ prawn.undash
111
+
112
+ prawn.fill_ellipse([x_position, y_position ], 2)
113
+ return true
114
+ end
115
+ end
116
+
117
+ end
118
+ end
119
+ end
120
+ end
@@ -1,32 +1,25 @@
1
1
  module Prawn
2
2
  module Graph
3
3
  module Extension
4
-
5
- # Draws a bar graph into the PDF
6
- #
7
- # Example:
8
- #
9
- # bar_graph [ ["A", 1], ["B", 2], ["C", 3] ], at: [10,10]
10
- #
11
- def bar_graph(data, options = {}, &block)
12
- graph = Prawn::Graph::Charts::Bar.new(data, self, options, &block)
13
- graph.draw
14
- {warnings: [], width: graph.prawn.bounds.width, height: graph.prawn.bounds.height}
15
- end
16
- alias bar_chart bar_graph
17
-
18
- # Draws a line graph into the PDF
19
- #
20
- # Example:
4
+
5
+ # Plots one or more Prawn::Graph::Series on a chart. Expects an array-like object of
6
+ # Prawn::Graph::Series objects and some options for positioning the sizing the
7
+ # rendered graph
21
8
  #
22
- # line_graph [ ["A", 1], ["B", 2], ["C", 3] ], at: [10,10]
9
+ # @param series [Array] of Prawn::Graph::Series objects
10
+ # @param options [Hash] of options, which can be:
11
+ # `:width` - The overall width of the graph to be drawn. `<Integer>`
12
+ # `:height` - The overall height of the graph to be drawn. `<Integer>`
13
+ # `:at` - The point from where the graph will be drawn. `[<Integer>x, <Integer>y]`
14
+ # `:title` - The title for this chart. Must be a string. `<String>`
15
+ # `:series_key` - Should we render the key to series in this chart? `<Boolean>`
23
16
  #
24
- def line_graph(data, options = {}, &block)
25
- graph = Prawn::Graph::Charts::Line.new(data, self, options, &block)
26
- graph.draw
27
- {warnings: [], width: graph.prawn.bounds.width, height: graph.prawn.bounds.height}
17
+ def graph(series, options = {}, &block)
18
+ canvas = Prawn::Graph::ChartComponents::Canvas.new(series, self, options, &block)
19
+ canvas.draw
20
+ {warnings: [], width: self.bounds.width, height: self.bounds.height}
28
21
  end
29
- alias line_chart line_graph
22
+ alias chart graph
30
23
 
31
24
  end
32
25
  end
@@ -5,33 +5,82 @@ module Prawn
5
5
  # on a chart.
6
6
  #
7
7
  class Series
8
- attr_accessor :values, :title, :type
9
- VALID_TYPES = [ :bar, :line ]
8
+ attr_accessor :values, :options
10
9
 
11
- def initialize(values = [], title = nil, type = :bar)
10
+ DEFAULT_OPTIONS = {
11
+ title: nil,
12
+ type: :bar,
13
+ mark_average: false,
14
+ mark_minimum: false,
15
+ mark_maximum: false,
16
+ }
17
+
18
+ def initialize(values = [], options = {})
12
19
  @values = values
13
- @title = title
14
- @type = type
20
+ @options = OpenStruct.new(DEFAULT_OPTIONS.merge(options))
21
+ end
22
+
23
+ # @return [String] The value of +options.title+.
24
+ #
25
+ def title
26
+ options.title
15
27
  end
16
28
 
29
+ # @return [Symbol] The value of +options.type+.
30
+ #
31
+ def type
32
+ options.type
33
+ end
34
+
35
+ # @param value [Object] a value to be added to the series. Must be of the same kind as other +values+.
36
+ # @return [Array] The modified +values+ object.
37
+ #
17
38
  def <<(value)
18
39
  @values << value
19
40
  end
20
41
 
42
+ # @return [Numeric] The smallest value stored in the +values+ of this Series.
43
+ #
21
44
  def min
22
- @values.min || 0
45
+ if values.empty?
46
+ 0
47
+ else
48
+ values.sort.collect{ |x| x unless x.zero? }.compact.first
49
+ end
23
50
  end
24
51
 
52
+ # @return [Numeric] The largest value stored in the +values+ of this Series.
53
+ #
25
54
  def max
26
55
  @values.max || 0
27
56
  end
28
57
 
58
+ # @return [Numeric] The average value stored in the +values+ of this Series.
59
+ #
60
+ def avg
61
+ if size > 0
62
+ @values.inject(:+) / size
63
+ else
64
+ 0
65
+ end
66
+ end
67
+
68
+ # @return [Numeric] The size of the +values+ stored in this Series.
69
+ #
29
70
  def size
30
71
  @values.size
31
72
  end
32
73
 
33
- def to_a
34
- [title, @values].compact.flatten
74
+ def mark_average?
75
+ options.mark_average == true
76
+ end
77
+
78
+ def mark_minimum?
79
+ options.mark_minimum == true
80
+ end
81
+
82
+ def mark_maximum?
83
+ options.mark_maximum == true
35
84
  end
36
85
  end
37
86
  end