prawn-graph 0.0.4 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +16 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1168 -0
- data/.travis.yml +17 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/CONTRIBUTORS.md +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +142 -0
- data/Rakefile +7 -43
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/prawn-graph.rb +16 -0
- data/lib/prawn/graph/calculations.rb +1 -0
- data/lib/prawn/graph/calculations/layout_calculator.rb +108 -0
- data/lib/prawn/graph/chart_components.rb +2 -0
- data/lib/prawn/graph/chart_components/canvas.rb +138 -0
- data/lib/prawn/graph/chart_components/series_renderer.rb +173 -0
- data/lib/prawn/graph/charts.rb +4 -0
- data/lib/prawn/graph/charts/bar.rb +18 -0
- data/lib/prawn/graph/charts/base.rb +69 -0
- data/lib/prawn/graph/charts/legacy.rb +4 -0
- data/lib/prawn/graph/charts/legacy/bar.rb +28 -0
- data/lib/prawn/graph/charts/legacy/base.rb +193 -0
- data/lib/prawn/graph/charts/legacy/grid.rb +51 -0
- data/lib/prawn/graph/charts/legacy/line.rb +39 -0
- data/lib/prawn/graph/charts/line.rb +18 -0
- data/lib/prawn/graph/extension.rb +59 -0
- data/lib/prawn/graph/series.rb +79 -0
- data/lib/prawn/graph/theme.rb +41 -0
- data/lib/prawn/graph/version.rb +5 -0
- data/prawn-graph.gemspec +42 -0
- metadata +156 -80
- data/README.markdown +0 -64
- data/examples/example_helper.rb +0 -10
- data/examples/graph/advanced_bar_chart.rb +0 -22
- data/examples/graph/bar_chart.pdf +0 -185
- data/examples/graph/bar_chart.rb +0 -18
- data/examples/graph/line_chart.pdf +0 -219
- data/examples/graph/line_chart.rb +0 -18
- data/examples/graph/themed_bar_chart.rb +0 -18
- data/examples/graph/themed_line_chart.rb +0 -18
- data/lib/prawn/graph.rb +0 -94
- data/lib/prawn/graph/bar.rb +0 -64
- data/lib/prawn/graph/base.rb +0 -231
- data/lib/prawn/graph/chart.rb +0 -4
- data/lib/prawn/graph/errors.rb +0 -7
- data/lib/prawn/graph/grid.rb +0 -50
- data/lib/prawn/graph/line.rb +0 -74
- data/lib/prawn/graph/themes.rb +0 -116
- data/lib/prawn/graph/themes/37signals.yml +0 -14
- data/lib/prawn/graph/themes/keynote.yml +0 -14
- data/lib/prawn/graph/themes/monochome.yml +0 -8
- data/lib/prawn/graph/themes/odeo.yml +0 -14
@@ -0,0 +1,138 @@
|
|
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
|
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 = 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.render(bar_charts, self, theme.series[0..(bar_charts.size - 1)]) unless bar_charts.empty?
|
60
|
+
|
61
|
+
i = bar_charts.size
|
62
|
+
others.each do |series|
|
63
|
+
SeriesRenderer.render(series, self, theme.series[i])
|
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
|
+
series_offset = series_offset * theme.font_sizes.series_key
|
95
|
+
|
96
|
+
title = series.title || "Series #{series_offset}"
|
97
|
+
top_position = (prawn.bounds.top - (series_offset * 3))
|
98
|
+
|
99
|
+
|
100
|
+
prawn.fill_and_stroke_rectangle([ theme.font_sizes.series_key, top_position ], theme.font_sizes.series_key, theme.font_sizes.series_key)
|
101
|
+
|
102
|
+
prawn.fill_color = theme.axes
|
103
|
+
prawn.text_box title, at: [ (theme.font_sizes.series_key * 3), top_position ], size: theme.font_sizes.series_key, height: (series_offset * 2)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Verifies that we provide an array-like object of Prawn::Graph::Series instances to
|
111
|
+
# the Canvas, for later rendering.
|
112
|
+
#
|
113
|
+
def verify_series_are_ok!
|
114
|
+
if @series.respond_to?(:each) && @series.respond_to?(:collect)
|
115
|
+
classes = @series.collect{ |c| c.is_a?(Prawn::Graph::Series) }.uniq
|
116
|
+
if classes.size > 1 || classes[0] != true
|
117
|
+
raise RuntimeError.new("All of the items provided must be instances of Prawn::Graph::Series")
|
118
|
+
end
|
119
|
+
else
|
120
|
+
raise RuntimeError.new("Series provided must be an Array (or Array-like) object.")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# From prawn-svg - creates a cipped retangle, which we will then use to draw the graphs
|
125
|
+
# upon.
|
126
|
+
#
|
127
|
+
def clip_graph_to_bounds(x, y, width, height)
|
128
|
+
prawn.move_to x, y
|
129
|
+
prawn.line_to x + width, y
|
130
|
+
prawn.line_to x + width, y + height
|
131
|
+
prawn.line_to x, y + height
|
132
|
+
prawn.close_path
|
133
|
+
prawn.add_content "W n"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,173 @@
|
|
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
|
+
module SeriesRenderer
|
9
|
+
class << self
|
10
|
+
# @param series [Prawn::Graph::Series]
|
11
|
+
# @param canvas [Prawn::Graph::ChartComponents::Canvas]
|
12
|
+
#
|
13
|
+
def render(series, canvas, color = '000000')
|
14
|
+
raise ArgumentError.new("series must be a Prawn::Graph::Series") unless series.is_a?(Prawn::Graph::Series)
|
15
|
+
raise ArgumentError.new("canvas must be a Prawn::Graph::ChartComponents::Canvas") unless canvas.is_a?(Prawn::Graph::ChartComponents::Canvas)
|
16
|
+
|
17
|
+
@series = series
|
18
|
+
@canvas = canvas
|
19
|
+
@prawn = canvas.prawn
|
20
|
+
@color = color
|
21
|
+
|
22
|
+
@graph_area = @canvas.layout.graph_area
|
23
|
+
|
24
|
+
@plot_area_width = @graph_area.width - 25
|
25
|
+
@plot_area_height = @graph_area.height - 20
|
26
|
+
|
27
|
+
render_line_chart
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def render_line_chart
|
33
|
+
prawn.bounding_box [@graph_area.point[0] + 5, @graph_area.point[1] - 20], width: @plot_area_width, height: @plot_area_height do
|
34
|
+
j = 2
|
35
|
+
prawn.save_graphics_state do
|
36
|
+
@series.values.each_with_index do |v, i|
|
37
|
+
next if i == 0
|
38
|
+
|
39
|
+
width_per_point = (@plot_area_width / @series.size).round(2).to_f
|
40
|
+
spacing = width_per_point
|
41
|
+
|
42
|
+
prawn.line_width = 2
|
43
|
+
prawn.fill_color = @color
|
44
|
+
prawn.stroke_color = @color
|
45
|
+
|
46
|
+
|
47
|
+
previous_value = @series.values[i - 1]
|
48
|
+
this_value = v
|
49
|
+
|
50
|
+
previous_y = (point_height_percentage(previous_value) * @plot_area_height) - 5
|
51
|
+
this_y = (point_height_percentage(this_value) * @plot_area_height) - 5
|
52
|
+
|
53
|
+
previous_x_offset = ((spacing * (j - 1)) - spacing) + (spacing / 2.0)
|
54
|
+
this_x_offset = ((spacing * j) - spacing) + (spacing / 2.0)
|
55
|
+
|
56
|
+
|
57
|
+
prawn.stroke_line([previous_x_offset, previous_y], [ this_x_offset, this_y ])
|
58
|
+
|
59
|
+
prawn.fill_color = @canvas.theme.markers
|
60
|
+
prawn.fill_ellipse([ ( previous_x_offset), previous_y ], 1)
|
61
|
+
prawn.fill_ellipse([ ( this_x_offset), this_y ], 1)
|
62
|
+
j += 1
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
render_axes
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def render_axes
|
71
|
+
prawn.stroke_color = @canvas.theme.axes
|
72
|
+
prawn.fill_color = @canvas.theme.axes
|
73
|
+
prawn.stroke_horizontal_line(0, @plot_area_width, at: 0)
|
74
|
+
prawn.stroke_vertical_line(0, @plot_area_height, at: 0)
|
75
|
+
prawn.fill_and_stroke_ellipse [ 0,0], 1
|
76
|
+
end
|
77
|
+
|
78
|
+
# Calculates the relative height of a given point based on the maximum value present in
|
79
|
+
# the series.
|
80
|
+
#
|
81
|
+
def point_height_percentage(value)
|
82
|
+
((BigDecimal(value, 10)/BigDecimal(@series.max, 10)) * BigDecimal(1)).round(2)
|
83
|
+
end
|
84
|
+
|
85
|
+
def prawn
|
86
|
+
@prawn
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
|
93
|
+
# The Prawn::Graph::ChartComponents::BarChartRenderer is used to plot one or more bar charts
|
94
|
+
# in a sensible way on a a Prawn::Graph::ChartComponents::Canvas and its associated
|
95
|
+
# Prawn::Document.
|
96
|
+
#
|
97
|
+
module BarChartRenderer
|
98
|
+
class << self
|
99
|
+
# @param series [Prawn::Graph::Series]
|
100
|
+
# @param canvas [Prawn::Graph::ChartComponents::Canvas]
|
101
|
+
#
|
102
|
+
def render(series, canvas, colors)
|
103
|
+
@series = series
|
104
|
+
@canvas = canvas
|
105
|
+
@prawn = canvas.prawn
|
106
|
+
@colors = colors
|
107
|
+
|
108
|
+
|
109
|
+
@graph_area = @canvas.layout.graph_area
|
110
|
+
|
111
|
+
@plot_area_width = @graph_area.width - 25
|
112
|
+
@plot_area_height = @graph_area.height - 20
|
113
|
+
|
114
|
+
render_bar_charts
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def render_bar_charts
|
120
|
+
prawn.bounding_box [@graph_area.point[0] + 5, @graph_area.point[1] - 20], width: @plot_area_width, height: @plot_area_height do
|
121
|
+
|
122
|
+
prawn.save_graphics_state do
|
123
|
+
num_points = @series[0].size
|
124
|
+
width_per_point = (@plot_area_width / num_points).round(2).to_f
|
125
|
+
width = ((width_per_point * BigDecimal('0.9')) / @series.size).round(2).to_f
|
126
|
+
spacing = (width_per_point - (width * @series.size).to_f / 2.0)
|
127
|
+
|
128
|
+
num_points.times do |point|
|
129
|
+
|
130
|
+
@series.size.times do |series_index|
|
131
|
+
series_offset = series_index + 1
|
132
|
+
prawn.fill_color = @colors[series_index]
|
133
|
+
prawn.stroke_color = @colors[series_index]
|
134
|
+
prawn.line_width = width
|
135
|
+
|
136
|
+
starting = (prawn.bounds.left + (point * width_per_point))
|
137
|
+
|
138
|
+
x_position = ( (starting + (series_offset * width) ).to_f - (width / 2.0))
|
139
|
+
y_position = ((point_height_percentage(@series[series_index].values[point]) * @plot_area_height) - 5).to_f
|
140
|
+
|
141
|
+
prawn.fill_and_stroke_line([ x_position ,0], [x_position ,y_position])
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
render_axes
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def render_axes
|
152
|
+
prawn.stroke_color = @canvas.theme.axes
|
153
|
+
prawn.fill_color = @canvas.theme.axes
|
154
|
+
prawn.stroke_horizontal_line(0, @plot_area_width, at: 0)
|
155
|
+
prawn.stroke_vertical_line(0, @plot_area_height, at: 0)
|
156
|
+
prawn.fill_and_stroke_ellipse [ 0,0], 1
|
157
|
+
end
|
158
|
+
|
159
|
+
# Calculates the relative height of a given point based on the maximum value present in
|
160
|
+
# the series.
|
161
|
+
#
|
162
|
+
def point_height_percentage(value)
|
163
|
+
((BigDecimal(value, 10)/BigDecimal(@series[0].max, 10)) * BigDecimal(1)).round(2)
|
164
|
+
end
|
165
|
+
|
166
|
+
def prawn
|
167
|
+
@prawn
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Prawn
|
2
|
+
module Graph
|
3
|
+
module Charts
|
4
|
+
class Bar < Base
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def chart_object
|
9
|
+
Prawn::Graph::Charts::Legacy::Bar.new(@series.collect(&:to_a), @prawn, @options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def series_type
|
13
|
+
:bar
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Prawn
|
2
|
+
module Graph
|
3
|
+
module Charts
|
4
|
+
|
5
|
+
class Base
|
6
|
+
VALID_OPTIONS = [:at, :width, :height]
|
7
|
+
attr_reader :series, :prawn, :options, :lowest_value, :highest_value
|
8
|
+
|
9
|
+
def initialize(data, prawn, options = {}, &block)
|
10
|
+
Prawn.verify_options VALID_OPTIONS, options
|
11
|
+
|
12
|
+
if data.is_a?(Array) || (!data.is_a?(Array) && data.is_a?(Prawn::Graph::Series))
|
13
|
+
data = [ data ] unless data.is_a?(Array)
|
14
|
+
else
|
15
|
+
raise ArgumentError.new("data must be a multidimensional Array, array of Prawn::Graph::Series or a single Prawn::Graph::Series")
|
16
|
+
end
|
17
|
+
|
18
|
+
@series = []
|
19
|
+
data.each do |s|
|
20
|
+
if s.is_a?(Prawn::Graph::Series)
|
21
|
+
@series << s
|
22
|
+
else
|
23
|
+
title = s.shift
|
24
|
+
@series << Prawn::Graph::Series.new(s, title: title, type: series_type)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
@prawn = prawn
|
29
|
+
@options = options
|
30
|
+
calculate_extreme_values!
|
31
|
+
end
|
32
|
+
|
33
|
+
def titles
|
34
|
+
@series.collect(&:title)
|
35
|
+
end
|
36
|
+
|
37
|
+
def position
|
38
|
+
end
|
39
|
+
|
40
|
+
def draw
|
41
|
+
chart_object.draw
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def chart_object
|
47
|
+
raise RuntimeError.new("I need to be subclassed.")
|
48
|
+
end
|
49
|
+
|
50
|
+
def series_type
|
51
|
+
:bar
|
52
|
+
end
|
53
|
+
|
54
|
+
def calculate_extreme_values!
|
55
|
+
@lowest_value = @series.collect(&:min).min
|
56
|
+
@highest_value = @series.collect(&:max).max
|
57
|
+
end
|
58
|
+
|
59
|
+
def process_the(data_array)
|
60
|
+
col = []
|
61
|
+
val = []
|
62
|
+
data_array.each { |i| val << i[1]; col << i[0] }
|
63
|
+
[ col, val, val.min, val.max ]
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Prawn
|
2
|
+
module Graph
|
3
|
+
module Charts
|
4
|
+
module Legacy
|
5
|
+
class Bar < Prawn::Graph::Charts::Legacy::Base
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def plot_values
|
10
|
+
base_x = @grid.start_x + 1
|
11
|
+
base_y = @grid.start_y + 1
|
12
|
+
bar_width = calculate_bar_width
|
13
|
+
@document.line_width bar_width
|
14
|
+
last_position = base_x
|
15
|
+
point_spacing = calculate_plot_spacing
|
16
|
+
@values.each do |value|
|
17
|
+
@document.move_to [base_x + last_position, base_y]
|
18
|
+
bar_height = calculate_point_height_from value
|
19
|
+
@document.stroke_color @theme.series.sample
|
20
|
+
@document.stroke_line_to [base_x + last_position, base_y + bar_height]
|
21
|
+
last_position += point_spacing
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|