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.
- 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
|