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.
- checksums.yaml +4 -4
- data/.travis.yml +10 -0
- data/README.md +72 -36
- data/Rakefile +6 -0
- data/lib/prawn-graph.rb +6 -1
- data/lib/prawn/graph/calculations.rb +1 -0
- data/lib/prawn/graph/calculations/layout_calculator.rb +109 -0
- data/lib/prawn/graph/chart_components.rb +4 -0
- data/lib/prawn/graph/chart_components/bar_chart_renderer.rb +94 -0
- data/lib/prawn/graph/chart_components/canvas.rb +128 -0
- data/lib/prawn/graph/chart_components/line_chart_renderer.rb +92 -0
- data/lib/prawn/graph/chart_components/series_renderer.rb +120 -0
- data/lib/prawn/graph/extension.rb +16 -23
- data/lib/prawn/graph/series.rb +57 -8
- data/lib/prawn/graph/theme.rb +23 -6
- data/lib/prawn/graph/version.rb +1 -1
- data/prawn-graph.gemspec +8 -2
- metadata +44 -15
- data/lib/prawn/graph/charts.rb +0 -4
- data/lib/prawn/graph/charts/bar.rb +0 -18
- data/lib/prawn/graph/charts/base.rb +0 -69
- data/lib/prawn/graph/charts/legacy.rb +0 -4
- data/lib/prawn/graph/charts/legacy/bar.rb +0 -28
- data/lib/prawn/graph/charts/legacy/base.rb +0 -195
- data/lib/prawn/graph/charts/legacy/grid.rb +0 -51
- data/lib/prawn/graph/charts/legacy/line.rb +0 -39
- data/lib/prawn/graph/charts/line.rb +0 -18
@@ -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
|
-
#
|
6
|
-
#
|
7
|
-
#
|
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
|
-
#
|
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
|
25
|
-
|
26
|
-
|
27
|
-
{warnings: [], width:
|
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
|
22
|
+
alias chart graph
|
30
23
|
|
31
24
|
end
|
32
25
|
end
|
data/lib/prawn/graph/series.rb
CHANGED
@@ -5,33 +5,82 @@ module Prawn
|
|
5
5
|
# on a chart.
|
6
6
|
#
|
7
7
|
class Series
|
8
|
-
attr_accessor :values, :
|
9
|
-
VALID_TYPES = [ :bar, :line ]
|
8
|
+
attr_accessor :values, :options
|
10
9
|
|
11
|
-
|
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
|
-
@
|
14
|
-
|
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
|
-
|
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
|
34
|
-
|
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
|