prawn-graph 1.0.0.pre1 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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