gruff 0.8.0 → 0.9.0
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/.github/ISSUE_TEMPLATE.md +18 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +93 -0
- data/.rubocop_todo.yml +23 -810
- data/.travis.yml +4 -4
- data/.yardopts +1 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile +3 -1
- data/README.md +44 -21
- data/Rakefile +2 -206
- data/docker/Dockerfile +14 -0
- data/docker/build.sh +4 -0
- data/docker/launch.sh +4 -0
- data/gruff.gemspec +11 -8
- data/init.rb +2 -0
- data/lib/gruff.rb +23 -0
- data/lib/gruff/accumulator_bar.rb +6 -6
- data/lib/gruff/area.rb +13 -17
- data/lib/gruff/bar.rb +58 -41
- data/lib/gruff/base.rb +243 -566
- data/lib/gruff/bezier.rb +12 -14
- data/lib/gruff/bullet.rb +39 -57
- data/lib/gruff/dot.rb +25 -59
- data/lib/gruff/{bar_conversion.rb → helper/bar_conversion.rb} +13 -12
- data/lib/gruff/helper/bar_value_label_mixin.rb +30 -0
- data/lib/gruff/{stacked_mixin.rb → helper/stacked_mixin.rb} +7 -6
- data/lib/gruff/line.rb +95 -177
- data/lib/gruff/mini/bar.rb +6 -7
- data/lib/gruff/mini/legend.rb +16 -32
- data/lib/gruff/mini/pie.rb +6 -7
- data/lib/gruff/mini/side_bar.rb +4 -5
- data/lib/gruff/net.rb +37 -65
- data/lib/gruff/patch/rmagick.rb +33 -0
- data/lib/gruff/patch/string.rb +8 -0
- data/lib/gruff/photo_bar.rb +19 -19
- data/lib/gruff/pie.rb +22 -73
- data/lib/gruff/renderer/bezier.rb +21 -0
- data/lib/gruff/renderer/circle.rb +21 -0
- data/lib/gruff/renderer/dash_line.rb +22 -0
- data/lib/gruff/renderer/dot.rb +39 -0
- data/lib/gruff/renderer/ellipse.rb +21 -0
- data/lib/gruff/renderer/line.rb +34 -0
- data/lib/gruff/renderer/polygon.rb +23 -0
- data/lib/gruff/renderer/polyline.rb +21 -0
- data/lib/gruff/renderer/rectangle.rb +19 -0
- data/lib/gruff/renderer/renderer.rb +127 -0
- data/lib/gruff/renderer/text.rb +42 -0
- data/lib/gruff/scatter.rb +85 -156
- data/lib/gruff/scene.rb +22 -30
- data/lib/gruff/side_bar.rb +62 -58
- data/lib/gruff/side_stacked_bar.rb +47 -43
- data/lib/gruff/spider.rb +19 -36
- data/lib/gruff/stacked_area.rb +17 -21
- data/lib/gruff/stacked_bar.rb +50 -24
- data/lib/gruff/store/base_data.rb +34 -0
- data/lib/gruff/store/custom_data.rb +34 -0
- data/lib/gruff/store/store.rb +80 -0
- data/lib/gruff/store/xy_data.rb +55 -0
- data/lib/gruff/themes.rb +3 -3
- data/lib/gruff/version.rb +3 -1
- metadata +41 -30
- data/Manifest.txt +0 -81
- data/assets/bubble.png +0 -0
- data/assets/city_scene/background/0000.png +0 -0
- data/assets/city_scene/background/0600.png +0 -0
- data/assets/city_scene/background/2000.png +0 -0
- data/assets/city_scene/clouds/cloudy.png +0 -0
- data/assets/city_scene/clouds/partly_cloudy.png +0 -0
- data/assets/city_scene/clouds/stormy.png +0 -0
- data/assets/city_scene/grass/default.png +0 -0
- data/assets/city_scene/haze/true.png +0 -0
- data/assets/city_scene/number_sample/1.png +0 -0
- data/assets/city_scene/number_sample/2.png +0 -0
- data/assets/city_scene/number_sample/default.png +0 -0
- data/assets/city_scene/sky/0000.png +0 -0
- data/assets/city_scene/sky/0200.png +0 -0
- data/assets/city_scene/sky/0400.png +0 -0
- data/assets/city_scene/sky/0600.png +0 -0
- data/assets/city_scene/sky/0800.png +0 -0
- data/assets/city_scene/sky/1000.png +0 -0
- data/assets/city_scene/sky/1200.png +0 -0
- data/assets/city_scene/sky/1400.png +0 -0
- data/assets/city_scene/sky/1500.png +0 -0
- data/assets/city_scene/sky/1700.png +0 -0
- data/assets/city_scene/sky/2000.png +0 -0
- data/assets/pc306715.jpg +0 -0
- data/lib/gruff/deprecated.rb +0 -38
data/lib/gruff/bezier.rb
CHANGED
@@ -1,20 +1,21 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gruff/base'
|
2
4
|
|
3
5
|
class Gruff::Bezier < Gruff::Base
|
4
6
|
def draw
|
5
7
|
super
|
6
8
|
|
7
|
-
return unless
|
9
|
+
return unless data_given?
|
8
10
|
|
9
|
-
|
11
|
+
x_increment = @graph_width / (column_count - 1).to_f
|
10
12
|
|
11
|
-
|
12
|
-
poly_points =
|
13
|
-
@d = @d.fill data_row[DATA_COLOR_INDEX]
|
13
|
+
store.norm_data.each do |data_row|
|
14
|
+
poly_points = []
|
14
15
|
|
15
16
|
data_row[1].each_with_index do |data_point, index|
|
16
17
|
# Use incremented x and scaled y
|
17
|
-
new_x = @graph_left + (
|
18
|
+
new_x = @graph_left + (x_increment * index)
|
18
19
|
new_y = @graph_top + (@graph_height - data_point * @graph_height)
|
19
20
|
|
20
21
|
if index == 0 && RUBY_PLATFORM != 'java'
|
@@ -28,18 +29,15 @@ class Gruff::Bezier < Gruff::Base
|
|
28
29
|
draw_label(new_x, index)
|
29
30
|
end
|
30
31
|
|
31
|
-
|
32
|
-
@d = @d.stroke data_row[DATA_COLOR_INDEX]
|
33
|
-
@d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
|
32
|
+
stroke_width = clip_value_if_greater_than(@columns / (store.norm_data.first[1].size * 4), 5.0)
|
34
33
|
|
35
34
|
if RUBY_PLATFORM == 'java'
|
36
|
-
|
35
|
+
Gruff::Renderer::Polyline.new(color: data_row.color, width: stroke_width).render(poly_points)
|
37
36
|
else
|
38
|
-
|
37
|
+
Gruff::Renderer::Bezier.new(color: data_row.color, width: stroke_width).render(poly_points)
|
39
38
|
end
|
40
39
|
end
|
41
40
|
|
42
|
-
|
41
|
+
Gruff::Renderer.finish
|
43
42
|
end
|
44
|
-
|
45
43
|
end
|
data/lib/gruff/bullet.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gruff/base'
|
2
4
|
require 'gruff/themes'
|
3
5
|
|
4
6
|
# http://en.wikipedia.org/wiki/Bullet_graph
|
5
7
|
class Gruff::Bullet < Gruff::Base
|
6
|
-
|
7
|
-
|
8
|
-
if not Numeric === target_width
|
8
|
+
def initialize(target_width = '400x40')
|
9
|
+
if target_width.is_a?(String)
|
9
10
|
geometric_width, geometric_height = target_width.split('x')
|
10
11
|
@columns = geometric_width.to_f
|
11
12
|
@rows = geometric_height.to_f
|
@@ -13,6 +14,8 @@ class Gruff::Bullet < Gruff::Base
|
|
13
14
|
@columns = target_width.to_f
|
14
15
|
@rows = target_width.to_f / 5.0
|
15
16
|
end
|
17
|
+
@columns.freeze
|
18
|
+
@rows.freeze
|
16
19
|
|
17
20
|
initialize_ivars
|
18
21
|
|
@@ -23,30 +26,13 @@ class Gruff::Bullet < Gruff::Base
|
|
23
26
|
|
24
27
|
def data(value, maximum_value, options = {})
|
25
28
|
@value = value.to_f
|
26
|
-
|
29
|
+
self.maximum_value = maximum_value.to_f
|
27
30
|
@options = options
|
28
|
-
@options.map { |k, v| @options[k] = v.to_f if v
|
31
|
+
@options.map { |k, v| @options[k] = v.to_f if v.is_a?(Numeric) }
|
29
32
|
end
|
30
33
|
|
31
|
-
# def setup_drawing
|
32
|
-
# # Maybe should be done in one of the following functions for more granularity.
|
33
|
-
# unless @has_data
|
34
|
-
# draw_no_data()
|
35
|
-
# return
|
36
|
-
# end
|
37
|
-
#
|
38
|
-
# normalize()
|
39
|
-
# setup_graph_measurements()
|
40
|
-
# sort_norm_data() if @sort # Sort norm_data with avg largest values set first (for display)
|
41
|
-
#
|
42
|
-
# draw_legend()
|
43
|
-
# draw_line_markers()
|
44
|
-
# draw_axis_labels()
|
45
|
-
# draw_title
|
46
|
-
# end
|
47
|
-
|
48
34
|
def draw
|
49
|
-
# TODO Left label
|
35
|
+
# TODO: Left label
|
50
36
|
# TODO Bottom labels and markers
|
51
37
|
# @graph_bottom
|
52
38
|
# Calculations are off 800x???
|
@@ -55,54 +41,50 @@ class Gruff::Bullet < Gruff::Base
|
|
55
41
|
|
56
42
|
draw_title
|
57
43
|
|
58
|
-
|
59
|
-
|
60
|
-
@
|
61
|
-
|
62
|
-
|
63
|
-
|
44
|
+
title_width = calculate_width(@title_font_size, @title)
|
45
|
+
margin = 30.0
|
46
|
+
thickness = @raw_rows / 6.0
|
47
|
+
right_margin = margin
|
48
|
+
graph_left = (@title && (title_width * 1.3)) || margin
|
49
|
+
graph_width = @raw_columns - graph_left - right_margin
|
50
|
+
graph_height = thickness * 3.0
|
64
51
|
|
65
52
|
# Background
|
66
|
-
|
67
|
-
|
53
|
+
rect_renderer = Gruff::Renderer::Rectangle.new(color: @colors[0])
|
54
|
+
rect_renderer.render(graph_left, 0, graph_left + graph_width, graph_height)
|
68
55
|
|
69
56
|
[:high, :low].each_with_index do |indicator, index|
|
70
|
-
next unless @options.
|
57
|
+
next unless @options.key?(indicator)
|
58
|
+
|
59
|
+
indicator_width_x = graph_left + graph_width * (@options[indicator] / maximum_value)
|
71
60
|
|
72
|
-
|
73
|
-
|
74
|
-
@d = @d.rectangle(@graph_left, 0, indicator_width_x, @graph_height)
|
61
|
+
rect_renderer = Gruff::Renderer::Rectangle.new(color: @colors[index + 1])
|
62
|
+
rect_renderer.render(graph_left, 0, indicator_width_x, graph_height)
|
75
63
|
end
|
76
64
|
|
77
|
-
if @options.
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
65
|
+
if @options.key?(:target)
|
66
|
+
target_x = graph_left + graph_width * (@options[:target] / maximum_value)
|
67
|
+
half_thickness = thickness / 2.0
|
68
|
+
|
69
|
+
rect_renderer = Gruff::Renderer::Rectangle.new(color: @font_color)
|
70
|
+
rect_renderer.render(target_x, half_thickness, target_x + half_thickness, thickness * 2 + half_thickness)
|
82
71
|
end
|
83
72
|
|
84
73
|
# Value
|
85
|
-
|
86
|
-
|
74
|
+
rect_renderer = Gruff::Renderer::Rectangle.new(color: @font_color)
|
75
|
+
rect_renderer.render(graph_left, thickness, graph_left + graph_width * (@value / maximum_value), thickness * 2)
|
87
76
|
|
88
|
-
|
77
|
+
Gruff::Renderer.finish
|
89
78
|
end
|
90
79
|
|
80
|
+
private
|
81
|
+
|
91
82
|
def draw_title
|
92
83
|
return unless @title
|
93
84
|
|
94
|
-
|
95
|
-
@title_width = calculate_width(@title_font_size, @title)
|
96
|
-
|
97
|
-
@d.fill = @font_color
|
98
|
-
@d.font = @font if @font
|
99
|
-
@d.stroke('transparent')
|
100
|
-
@d.font_weight = NormalWeight
|
101
|
-
@d.pointsize = scale_fontsize(@title_font_size)
|
102
|
-
@d.gravity = NorthWestGravity
|
103
|
-
@d = @d.annotate_scaled(
|
104
|
-
@base_image, 1.0, 1.0, @font_height / 2, @font_height / 2, @title, @scale
|
105
|
-
)
|
106
|
-
end
|
85
|
+
font_height = calculate_caps_height(scale_fontsize(@title_font_size))
|
107
86
|
|
87
|
+
text_renderer = Gruff::Renderer::Text.new(@title, font: @font, size: @title_font_size, color: @font_color)
|
88
|
+
text_renderer.render(1.0, 1.0, font_height / 2, font_height / 2, Magick::NorthWestGravity)
|
89
|
+
end
|
108
90
|
end
|
data/lib/gruff/dot.rb
CHANGED
@@ -1,50 +1,40 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gruff/base'
|
2
4
|
|
3
|
-
##
|
4
5
|
# Graph with dots and labels along a vertical access
|
5
6
|
# see: 'Creating More Effective Graphs' by Robbins
|
6
|
-
|
7
7
|
class Gruff::Dot < Gruff::Base
|
8
|
-
|
9
8
|
def draw
|
10
9
|
@has_left_labels = true
|
11
10
|
super
|
12
11
|
|
13
|
-
return unless
|
12
|
+
return unless data_given?
|
14
13
|
|
15
14
|
# Setup spacing.
|
16
15
|
#
|
17
16
|
spacing_factor = 1.0
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
padding = (@items_width * (1 - spacing_factor)) / 2
|
18
|
+
items_width = @graph_height / column_count.to_f
|
19
|
+
item_width = items_width * spacing_factor / store.length
|
20
|
+
padding = (items_width * (1 - spacing_factor)) / 2
|
23
21
|
|
24
|
-
|
25
|
-
data_row
|
22
|
+
store.norm_data.each_with_index do |data_row, row_index|
|
23
|
+
data_row.points.each_with_index do |data_point, point_index|
|
26
24
|
x_pos = @graph_left + (data_point * @graph_width)
|
27
|
-
y_pos = @graph_top + (
|
25
|
+
y_pos = @graph_top + (items_width * point_index) + padding + (items_width.to_f / 2.0).round
|
28
26
|
|
29
27
|
if row_index == 0
|
30
|
-
|
31
|
-
@d = @d.fill(@marker_color)
|
32
|
-
@d = @d.stroke_width 1.0
|
33
|
-
@d = @d.stroke_opacity 0.1
|
34
|
-
@d = @d.fill_opacity 0.1
|
35
|
-
@d = @d.line(@graph_left, y_pos, @graph_left + @graph_width, y_pos)
|
36
|
-
@d = @d.fill_opacity 1
|
28
|
+
Gruff::Renderer::Line.new(color: @marker_color).render(@graph_left, y_pos, @graph_left + @graph_width, y_pos)
|
37
29
|
end
|
38
30
|
|
39
|
-
|
40
|
-
@d = @d.stroke('transparent')
|
41
|
-
@d = @d.circle(x_pos, y_pos, x_pos + (@item_width.to_f / 3.0).round, y_pos)
|
31
|
+
Gruff::Renderer::Circle.new(color: data_row.color).render(x_pos, y_pos, x_pos + (item_width.to_f / 3.0).round, y_pos)
|
42
32
|
|
43
33
|
draw_label(y_pos, point_index)
|
44
34
|
end
|
45
35
|
end
|
46
36
|
|
47
|
-
|
37
|
+
Gruff::Renderer.finish
|
48
38
|
end
|
49
39
|
|
50
40
|
protected
|
@@ -53,11 +43,7 @@ protected
|
|
53
43
|
def draw_line_markers
|
54
44
|
return if @hide_line_markers
|
55
45
|
|
56
|
-
@d = @d.stroke_antialias false
|
57
|
-
|
58
46
|
# Draw horizontal line markers and annotate with numbers
|
59
|
-
@d = @d.stroke(@marker_color)
|
60
|
-
@d = @d.stroke_width 1
|
61
47
|
if @y_axis_increment
|
62
48
|
increment = @y_axis_increment
|
63
49
|
number_of_lines = (@spread / @y_axis_increment).to_i
|
@@ -74,31 +60,21 @@ protected
|
|
74
60
|
end
|
75
61
|
@marker_count ||= 5
|
76
62
|
end
|
77
|
-
# TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
|
78
|
-
|
79
|
-
|
63
|
+
# TODO: Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
|
64
|
+
increment = (@spread > 0 && @marker_count > 0) ? significant(@spread / @marker_count) : 1
|
80
65
|
number_of_lines = @marker_count
|
81
|
-
increment = @increment
|
82
66
|
end
|
83
67
|
|
84
68
|
(0..number_of_lines).each do |index|
|
85
|
-
marker_label =
|
86
|
-
x = @graph_left + (marker_label -
|
87
|
-
|
69
|
+
marker_label = minimum_value + index * increment
|
70
|
+
x = @graph_left + (marker_label - minimum_value) * @graph_width / @spread
|
71
|
+
Gruff::Renderer::Line.new(color: @marker_color).render(x, @graph_bottom, x, @graph_bottom + 0.5 * LABEL_MARGIN)
|
88
72
|
|
89
73
|
unless @hide_line_numbers
|
90
|
-
|
91
|
-
|
92
|
-
@
|
93
|
-
|
94
|
-
@d.gravity = CenterGravity
|
95
|
-
# TODO Center text over line
|
96
|
-
@d = @d.annotate_scaled(@base_image,
|
97
|
-
0, 0, # Width of box to draw text in
|
98
|
-
x, @graph_bottom + (LABEL_MARGIN * 2.0), # Coordinates of text
|
99
|
-
label(marker_label, increment), @scale)
|
100
|
-
end # unless
|
101
|
-
@d = @d.stroke_antialias true
|
74
|
+
label = label(marker_label, increment)
|
75
|
+
text_renderer = Gruff::Renderer::Text.new(label, font: @font, size: @marker_font_size, color: @font_color)
|
76
|
+
text_renderer.render(0, 0, x, @graph_bottom + (LABEL_MARGIN * 2.0), Magick::CenterGravity)
|
77
|
+
end
|
102
78
|
end
|
103
79
|
end
|
104
80
|
|
@@ -106,19 +82,9 @@ protected
|
|
106
82
|
# Draw on the Y axis instead of the X
|
107
83
|
|
108
84
|
def draw_label(y_offset, index)
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
@d.stroke = 'transparent'
|
113
|
-
@d.font_weight = NormalWeight
|
114
|
-
@d.pointsize = scale_fontsize(@marker_font_size)
|
115
|
-
@d.gravity = EastGravity
|
116
|
-
@d = @d.annotate_scaled(@base_image,
|
117
|
-
1, 1,
|
118
|
-
-@graph_left + LABEL_MARGIN * 2.0, y_offset,
|
119
|
-
@labels[index], @scale)
|
120
|
-
@labels_seen[index] = 1
|
85
|
+
draw_unique_label(index) do
|
86
|
+
text_renderer = Gruff::Renderer::Text.new(@labels[index], font: @font, size: @marker_font_size, color: @font_color)
|
87
|
+
text_renderer.render(1, 1, -@graph_left + LABEL_MARGIN * 2.0, y_offset, Magick::EastGravity)
|
121
88
|
end
|
122
89
|
end
|
123
|
-
|
124
90
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
##
|
2
4
|
# Original Author: David Stokar
|
3
5
|
#
|
@@ -9,6 +11,7 @@
|
|
9
11
|
# 2. Bars all go from zero to negative direction
|
10
12
|
# 3. Bars either go from zero to positive or from zero to negative
|
11
13
|
#
|
14
|
+
# @private
|
12
15
|
class Gruff::BarConversion
|
13
16
|
attr_writer :mode
|
14
17
|
attr_writer :zero
|
@@ -17,30 +20,28 @@ class Gruff::BarConversion
|
|
17
20
|
attr_writer :minimum_value
|
18
21
|
attr_writer :spread
|
19
22
|
|
20
|
-
def get_left_y_right_y_scaled(data_point
|
23
|
+
def get_left_y_right_y_scaled(data_point)
|
24
|
+
result = []
|
25
|
+
|
21
26
|
case @mode
|
22
27
|
when 1 then # Case one
|
23
|
-
# minimum value >= 0 ( only
|
28
|
+
# minimum value >= 0 ( only positive values )
|
24
29
|
result[0] = @graph_top + @graph_height * (1 - data_point) + 1
|
25
30
|
result[1] = @graph_top + @graph_height - 1
|
26
31
|
when 2 then # Case two
|
27
|
-
# only
|
32
|
+
# only negative values
|
28
33
|
result[0] = @graph_top + 1
|
29
34
|
result[1] = @graph_top + @graph_height * (1 - data_point) - 1
|
30
35
|
when 3 then # Case three
|
31
|
-
#
|
36
|
+
# positive and negative values
|
32
37
|
val = data_point - @minimum_value / @spread
|
33
|
-
|
34
|
-
|
35
|
-
result[1] = @graph_top + @graph_height * (1 - @zero) - 1
|
36
|
-
else
|
37
|
-
result[0] = @graph_top + @graph_height * (1 - (val - @zero)) + 1
|
38
|
-
result[1] = @graph_top + @graph_height * (1 - @zero) - 1
|
39
|
-
end
|
38
|
+
result[0] = @graph_top + @graph_height * (1 - (val - @zero)) + 1
|
39
|
+
result[1] = @graph_top + @graph_height * (1 - @zero) - 1
|
40
40
|
else
|
41
41
|
result[0] = 0.0
|
42
42
|
result[1] = 0.0
|
43
43
|
end
|
44
|
-
end
|
45
44
|
|
45
|
+
result
|
46
|
+
end
|
46
47
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @private
|
4
|
+
module Gruff::Base::BarValueLabelMixin
|
5
|
+
class BarValueLabel
|
6
|
+
attr_accessor :coordinates, :values
|
7
|
+
|
8
|
+
def initialize(size, bar_width)
|
9
|
+
@coordinates = Array.new(size)
|
10
|
+
@values = Hash.new(0)
|
11
|
+
@bar_width = bar_width
|
12
|
+
end
|
13
|
+
|
14
|
+
def prepare_rendering(format)
|
15
|
+
@coordinates.each_with_index do |(left_x, left_y, right_x, _right_y), index|
|
16
|
+
value = @values[index]
|
17
|
+
val = (format || '%.2f') % value
|
18
|
+
y = value >= 0 ? left_y - 30 : left_y + 12
|
19
|
+
yield left_x + (right_x - left_x) / 2, y, val.commify
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def prepare_sidebar_rendering(format)
|
24
|
+
@coordinates.each_with_index do |(_left_x, _left_y, right_x, right_y), index|
|
25
|
+
val = (format || '%.2f') % @values[index]
|
26
|
+
yield right_x + 40, right_y - @bar_width / 2, val.commify
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -1,22 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @private
|
1
4
|
module Gruff::Base::StackedMixin
|
2
5
|
# Used by StackedBar and child classes.
|
3
6
|
#
|
4
7
|
# tsal: moved from Base 03 FEB 2007
|
5
|
-
DATA_VALUES_INDEX = Gruff::Base::DATA_VALUES_INDEX
|
6
8
|
def get_maximum_by_stack
|
7
9
|
# Get sum of each stack
|
8
10
|
max_hash = {}
|
9
|
-
|
10
|
-
data_set
|
11
|
+
store.data.each do |data_set|
|
12
|
+
data_set.points.each_with_index do |data_point, i|
|
11
13
|
max_hash[i] = 0.0 unless max_hash[i]
|
12
14
|
max_hash[i] += data_point.to_f
|
13
15
|
end
|
14
16
|
end
|
15
17
|
|
16
|
-
# @maximum_value = 0
|
17
18
|
max_hash.each_key do |key|
|
18
|
-
|
19
|
+
self.maximum_value = max_hash[key] if max_hash[key] > maximum_value
|
19
20
|
end
|
20
|
-
|
21
|
+
self.minimum_value = 0
|
21
22
|
end
|
22
23
|
end
|
data/lib/gruff/line.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gruff/base'
|
2
4
|
|
3
|
-
##
|
4
5
|
# Here's how to make a Line graph:
|
5
6
|
#
|
6
7
|
# g = Gruff::Line.new
|
@@ -9,79 +10,74 @@ require File.dirname(__FILE__) + '/base'
|
|
9
10
|
# g.data 'Hamburgers', [50, 19, 99, 29]
|
10
11
|
# g.write("test/output/line.png")
|
11
12
|
#
|
12
|
-
# There are also other options described below, such as #baseline_value, #baseline_color,
|
13
|
-
|
13
|
+
# There are also other options described below, such as {#baseline_value}, {#baseline_color},
|
14
|
+
# {#hide_dots}, and {#hide_lines}.
|
14
15
|
class Gruff::Line < Gruff::Base
|
15
|
-
|
16
|
-
# Allow for reference lines ( which are like baseline ... just allowing for more & on both axes )
|
16
|
+
# Allow for reference lines ( which are like baseline ... just allowing for more & on both axes ).
|
17
17
|
attr_accessor :reference_lines
|
18
18
|
attr_accessor :reference_line_default_color
|
19
19
|
attr_accessor :reference_line_default_width
|
20
20
|
|
21
|
-
# Allow for vertical marker lines
|
21
|
+
# Allow for vertical marker lines.
|
22
22
|
attr_accessor :show_vertical_markers
|
23
23
|
|
24
|
-
# Dimensions of lines and dots; calculated based on dataset size if left unspecified
|
24
|
+
# Dimensions of lines and dots; calculated based on dataset size if left unspecified.
|
25
25
|
attr_accessor :line_width
|
26
26
|
attr_accessor :dot_radius
|
27
27
|
|
28
|
-
# default is
|
28
|
+
# default is +'circle'+, other options include square.
|
29
29
|
attr_accessor :dot_style
|
30
30
|
|
31
31
|
# Hide parts of the graph to fit more datapoints, or for a different appearance.
|
32
32
|
attr_accessor :hide_dots, :hide_lines
|
33
33
|
|
34
|
-
#accessors for support of xy data
|
34
|
+
# accessors for support of xy data.
|
35
35
|
attr_accessor :minimum_x_value
|
36
|
+
|
37
|
+
# accessors for support of xy data.
|
36
38
|
attr_accessor :maximum_x_value
|
37
39
|
|
38
40
|
# Get the value if somebody has defined it.
|
39
41
|
def baseline_value
|
40
42
|
if @reference_lines.key?(:baseline)
|
41
43
|
@reference_lines[:baseline][:value]
|
42
|
-
else
|
43
|
-
nil
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
47
|
# Set a value for a baseline reference line..
|
48
48
|
def baseline_value=(new_value)
|
49
|
-
@reference_lines[:baseline] ||=
|
49
|
+
@reference_lines[:baseline] ||= {}
|
50
50
|
@reference_lines[:baseline][:value] = new_value
|
51
51
|
end
|
52
52
|
|
53
53
|
def baseline_color
|
54
54
|
if @reference_lines.key?(:baseline)
|
55
55
|
@reference_lines[:baseline][:color]
|
56
|
-
else
|
57
|
-
nil
|
58
56
|
end
|
59
57
|
end
|
60
58
|
|
61
59
|
def baseline_color=(new_value)
|
62
|
-
@reference_lines[:baseline] ||=
|
60
|
+
@reference_lines[:baseline] ||= {}
|
63
61
|
@reference_lines[:baseline][:color] = new_value
|
64
62
|
end
|
65
63
|
|
66
|
-
# Call with target pixel width of graph (800
|
64
|
+
# Call with target pixel width of graph (+800+, +400+, +300+), and/or +false+ to omit lines (points only).
|
67
65
|
#
|
68
|
-
#
|
66
|
+
# g = Gruff::Line.new(400) # 400px wide with lines
|
67
|
+
# g = Gruff::Line.new(400, false) # 400px wide, no lines (for backwards compatibility)
|
68
|
+
# g = Gruff::Line.new(false) # Defaults to 800px wide, no lines (for backwards compatibility)
|
69
69
|
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
# g = Gruff::Line.new(false) # Defaults to 800px wide, no lines (for backwards compatibility)
|
73
|
-
#
|
74
|
-
# The preferred way is to call hide_dots or hide_lines instead.
|
70
|
+
# The preferred way is to call {#hide_dots} or {#hide_lines} instead.
|
75
71
|
def initialize(*args)
|
76
72
|
raise ArgumentError, 'Wrong number of arguments' if args.length > 2
|
77
73
|
|
78
|
-
if args.empty? || (
|
74
|
+
if args.empty? || (!args.first.is_a?(Numeric) && !args.first.is_a?(String))
|
79
75
|
super()
|
80
76
|
else
|
81
77
|
super args.shift
|
82
78
|
end
|
83
79
|
|
84
|
-
@reference_lines =
|
80
|
+
@reference_lines = {}
|
85
81
|
@reference_line_default_color = 'red'
|
86
82
|
@reference_line_default_width = 5
|
87
83
|
|
@@ -92,83 +88,67 @@ class Gruff::Line < Gruff::Base
|
|
92
88
|
@dot_style = 'circle'
|
93
89
|
|
94
90
|
@show_vertical_markers = false
|
91
|
+
|
92
|
+
@store = Gruff::Store.new(Gruff::Store::XYData)
|
95
93
|
end
|
96
94
|
|
97
95
|
# This method allows one to plot a dataset with both X and Y data.
|
98
96
|
#
|
99
|
-
#
|
100
|
-
# name
|
101
|
-
# x_data_points
|
102
|
-
# y_data_points
|
103
|
-
# color
|
97
|
+
# @overload dataxy(name, x_data_points = [], y_data_points = [], color = nil)
|
98
|
+
# @param name [String] the title of the dataset.
|
99
|
+
# @param x_data_points [Array] an array containing the x data points for the graph.
|
100
|
+
# @param y_data_points [Array] an array containing the y data points for the graph.
|
101
|
+
# @param color [String] hex number indicating the line color as an RGB triplet.
|
104
102
|
#
|
105
|
-
#
|
103
|
+
# @overload dataxy(name, xy_data_points = [], color = nil)
|
104
|
+
# @param name [String] the title of the dataset.
|
105
|
+
# @param xy_data_points [Array] an array containing both x and y data points for the graph.
|
106
|
+
# @param color [String] hex number indicating the line color as an RGB triplet.
|
106
107
|
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
# color: hex number indicating the line color as an RGB triplet
|
110
|
-
#
|
111
|
-
# Notes:
|
112
|
-
# -if (x_data_points.length != y_data_points.length) an error is
|
108
|
+
# @note
|
109
|
+
# - if (x_data_points.length != y_data_points.length) an error is
|
113
110
|
# returned.
|
114
|
-
# -if the color argument is nil, the next color from the default theme will
|
111
|
+
# - if the color argument is nil, the next color from the default theme will
|
115
112
|
# be used.
|
116
|
-
# -if you want to use a preset theme, you must set it before calling
|
117
|
-
# dataxy().
|
113
|
+
# - if you want to use a preset theme, you must set it before calling {#dataxy}.
|
118
114
|
#
|
119
|
-
#
|
115
|
+
# @example
|
120
116
|
# g = Gruff::Line.new
|
121
117
|
# g.title = "X/Y Dataset"
|
122
118
|
# g.dataxy("Apples", [1,3,4,5,6,10], [1, 2, 3, 4, 4, 3])
|
123
119
|
# g.dataxy("Bapples", [1,3,4,5,7,9], [1, 1, 2, 2, 3, 3])
|
124
120
|
# g.dataxy("Capples", [[1,1],[2,3],[3,4],[4,5],[5,7],[6,9]])
|
125
|
-
#
|
121
|
+
#
|
122
|
+
# # you can still use the old data method too if you want:
|
126
123
|
# g.data("Capples", [1, 1, 2, 2, 3, 3])
|
127
|
-
#
|
124
|
+
#
|
125
|
+
# # labels will be drawn at the x locations of the keys passed in.
|
128
126
|
# In this example the lables are drawn at x positions 2, 4, and 6:
|
129
127
|
# g.labels = {0 => '2003', 2 => '2004', 4 => '2005', 6 => '2006'}
|
130
|
-
# The 0 => '2003' label will be ignored since it is outside the chart range.
|
128
|
+
# # The 0 => '2003' label will be ignored since it is outside the chart range.
|
131
129
|
def dataxy(name, x_data_points = [], y_data_points = [], color = nil)
|
132
|
-
|
130
|
+
# make sure it's an array
|
131
|
+
x_data_points = Array(x_data_points)
|
132
|
+
y_data_points = Array(y_data_points)
|
133
|
+
|
134
|
+
raise ArgumentError, 'x_data_points is nil!' if x_data_points.empty?
|
133
135
|
|
134
136
|
if x_data_points.all? { |p| p.is_a?(Array) && p.size == 2 }
|
135
|
-
y_data_points = x_data_points.
|
136
|
-
x_data_points = x_data_points.map { |p| p[0] }
|
137
|
+
x_data_points, y_data_points = x_data_points.transpose
|
137
138
|
end
|
138
139
|
|
139
140
|
raise ArgumentError, 'x_data_points.length != y_data_points.length!' if x_data_points.length != y_data_points.length
|
140
141
|
|
141
|
-
# call the existing data routine for the y data.
|
142
|
-
|
143
|
-
|
144
|
-
x_data_points = Array(x_data_points) # make sure it's an array
|
145
|
-
# append the x data to the last entry that was just added in the @data member
|
146
|
-
@data.last[DATA_VALUES_X_INDEX] = x_data_points
|
147
|
-
|
148
|
-
# Update the global min/max values for the x data
|
149
|
-
x_data_points.each do |x_data_point|
|
150
|
-
next if x_data_point.nil?
|
151
|
-
|
152
|
-
# Setup max/min so spread starts at the low end of the data points
|
153
|
-
if @maximum_x_value.nil? && @minimum_x_value.nil?
|
154
|
-
@maximum_x_value = @minimum_x_value = x_data_point
|
155
|
-
end
|
156
|
-
|
157
|
-
@maximum_x_value = (x_data_point > @maximum_x_value) ?
|
158
|
-
x_data_point : @maximum_x_value
|
159
|
-
@minimum_x_value = (x_data_point < @minimum_x_value) ?
|
160
|
-
x_data_point : @minimum_x_value
|
161
|
-
end
|
142
|
+
# call the existing data routine for the x/y data.
|
143
|
+
store.add(name, y_data_points, color, x_data_points)
|
162
144
|
end
|
163
145
|
|
164
146
|
def draw_reference_line(reference_line, left, right, top, bottom)
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
@d.line(left, top, right, bottom)
|
171
|
-
@d = @d.pop
|
147
|
+
config = {
|
148
|
+
color: reference_line[:color] || @reference_line_default_color,
|
149
|
+
width: reference_line[:width] || @reference_line_default_width
|
150
|
+
}
|
151
|
+
Gruff::Renderer::DashLine.new(config).render(left, top, right, bottom)
|
172
152
|
end
|
173
153
|
|
174
154
|
def draw_horizontal_reference_line(reference_line)
|
@@ -184,10 +164,10 @@ class Gruff::Line < Gruff::Base
|
|
184
164
|
def draw
|
185
165
|
super
|
186
166
|
|
187
|
-
return unless
|
167
|
+
return unless data_given?
|
188
168
|
|
189
169
|
# Check to see if more than one datapoint was given. NaN can result otherwise.
|
190
|
-
@x_increment = (
|
170
|
+
@x_increment = (column_count > 1) ? (@graph_width / (column_count - 1).to_f) : @graph_width
|
191
171
|
|
192
172
|
@reference_lines.each_value do |curr_reference_line|
|
193
173
|
draw_horizontal_reference_line(curr_reference_line) if curr_reference_line.key?(:norm_value)
|
@@ -195,70 +175,51 @@ class Gruff::Line < Gruff::Base
|
|
195
175
|
end
|
196
176
|
|
197
177
|
if @show_vertical_markers
|
198
|
-
(0
|
178
|
+
(0..column_count).each do |column|
|
199
179
|
x = @graph_left + @graph_width - column.to_f * @x_increment
|
200
180
|
|
201
|
-
|
202
|
-
|
203
|
-
# FIXME(uwe): Workaround for Issue #66
|
204
|
-
# https://github.com/topfunky/gruff/issues/66
|
205
|
-
# https://github.com/rmagick/rmagick/issues/82
|
206
|
-
# Remove if the issue gets fixed.
|
207
|
-
x += 0.001 unless defined?(JRUBY_VERSION)
|
208
|
-
# EMXIF
|
209
|
-
|
210
|
-
@d = @d.line(x, @graph_bottom, x, @graph_top)
|
181
|
+
Gruff::Renderer::Line.new(color: @marker_color).render(x, @graph_bottom, x, @graph_top)
|
211
182
|
#If the user specified a marker shadow color, draw a shadow just below it
|
212
|
-
|
213
|
-
|
214
|
-
@d = @d.line(x + 1, @graph_bottom, x + 1, @graph_top)
|
183
|
+
if @marker_shadow_color
|
184
|
+
Gruff::Renderer::Line.new(color: @marker_shadow_color).render(x + 1, @graph_bottom, x + 1, @graph_top)
|
215
185
|
end
|
216
186
|
end
|
217
187
|
end
|
218
188
|
|
219
|
-
|
189
|
+
store.norm_data.each do |data_row|
|
220
190
|
prev_x = prev_y = nil
|
221
191
|
|
222
|
-
|
192
|
+
one_point = contains_one_point_only?(data_row)
|
223
193
|
|
224
|
-
data_row
|
225
|
-
x_data
|
226
|
-
if x_data == nil
|
194
|
+
data_row.coordinates.each_with_index do |(x_data, y_data), index|
|
195
|
+
if x_data.nil?
|
227
196
|
#use the old method: equally spaced points along the x-axis
|
228
197
|
new_x = @graph_left + (@x_increment * index)
|
229
198
|
draw_label(new_x, index)
|
230
199
|
else
|
231
|
-
new_x = get_x_coord(x_data
|
200
|
+
new_x = get_x_coord(x_data, @graph_width, @graph_left)
|
232
201
|
@labels.each do |label_pos, _|
|
233
202
|
draw_label(@graph_left + ((label_pos - @minimum_x_value) * @graph_width) / (@maximum_x_value - @minimum_x_value), label_pos)
|
234
203
|
end
|
235
204
|
end
|
236
|
-
unless
|
205
|
+
unless y_data # we can't draw a line for a null data point, we can still label the axis though
|
237
206
|
prev_x = prev_y = nil
|
238
207
|
next
|
239
208
|
end
|
240
209
|
|
241
|
-
new_y = @graph_top + (@graph_height -
|
210
|
+
new_y = @graph_top + (@graph_height - y_data * @graph_height)
|
242
211
|
|
243
212
|
# Reset each time to avoid thin-line errors
|
244
|
-
@
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
circle_radius = dot_radius ||
|
251
|
-
clip_value_if_greater_than(@columns / (@norm_data.first[DATA_VALUES_INDEX].size * 2.5), 5.0)
|
252
|
-
|
253
|
-
if !@hide_lines && !prev_x.nil? && !prev_y.nil?
|
254
|
-
@d = @d.line(prev_x, prev_y, new_x, new_y)
|
255
|
-
elsif @one_point
|
256
|
-
# Show a circle if there's just one_point
|
257
|
-
@d = DotRenderers.renderer(@dot_style).render(@d, new_x, new_y, circle_radius)
|
213
|
+
stroke_width = line_width || clip_value_if_greater_than(@columns / (store.norm_data.first.y_points.size * 4), 5.0)
|
214
|
+
circle_radius = dot_radius || clip_value_if_greater_than(@columns / (store.norm_data.first.y_points.size * 2.5), 5.0)
|
215
|
+
|
216
|
+
if !@hide_lines && prev_x && prev_y
|
217
|
+
Gruff::Renderer::Line.new(color: data_row.color, width: stroke_width)
|
218
|
+
.render(prev_x, prev_y, new_x, new_y)
|
258
219
|
end
|
259
220
|
|
260
|
-
|
261
|
-
@
|
221
|
+
if one_point || !@hide_dots
|
222
|
+
Gruff::Renderer::Dot.new(@dot_style, color: data_row.color, width: stroke_width).render(new_x, new_y, circle_radius)
|
262
223
|
end
|
263
224
|
|
264
225
|
prev_x = new_x
|
@@ -266,13 +227,19 @@ class Gruff::Line < Gruff::Base
|
|
266
227
|
end
|
267
228
|
end
|
268
229
|
|
269
|
-
|
230
|
+
Gruff::Renderer.finish
|
270
231
|
end
|
271
232
|
|
233
|
+
private
|
234
|
+
|
272
235
|
def setup_data
|
236
|
+
# Update the global min/max values for the x data
|
237
|
+
@maximum_x_value ||= store.max_x
|
238
|
+
@minimum_x_value ||= store.min_x
|
239
|
+
|
273
240
|
# Deal with horizontal reference line values that exceed the existing minimum & maximum values.
|
274
|
-
possible_maximums = [
|
275
|
-
possible_minimums = [
|
241
|
+
possible_maximums = [maximum_value.to_f]
|
242
|
+
possible_minimums = [minimum_value.to_f]
|
276
243
|
|
277
244
|
@reference_lines.each_value do |curr_reference_line|
|
278
245
|
if curr_reference_line.key?(:value)
|
@@ -281,37 +248,28 @@ class Gruff::Line < Gruff::Base
|
|
281
248
|
end
|
282
249
|
end
|
283
250
|
|
284
|
-
|
285
|
-
|
251
|
+
self.maximum_value = possible_maximums.max
|
252
|
+
self.minimum_value = possible_minimums.min
|
286
253
|
|
287
254
|
super
|
288
255
|
end
|
289
256
|
|
290
|
-
def normalize
|
291
|
-
|
257
|
+
def normalize
|
258
|
+
return unless data_given?
|
259
|
+
|
260
|
+
spread_x = @maximum_x_value.to_f - @minimum_x_value.to_f
|
261
|
+
store.normalize(minimum_x: @minimum_x_value, spread_x: spread_x, minimum_y: minimum_value, spread_y: @spread)
|
292
262
|
|
293
263
|
@reference_lines.each_value do |curr_reference_line|
|
294
264
|
# We only care about horizontal markers ... for normalization.
|
295
265
|
# Vertical markers won't have a :value, they will have an :index
|
296
266
|
|
297
|
-
curr_reference_line[:norm_value] = ((curr_reference_line[:value].to_f -
|
298
|
-
end
|
299
|
-
|
300
|
-
#normalize the x data if it is specified
|
301
|
-
@data.each_with_index do |data_row, index|
|
302
|
-
norm_x_data_points = []
|
303
|
-
if data_row[DATA_VALUES_X_INDEX] != nil
|
304
|
-
data_row[DATA_VALUES_X_INDEX].each do |x_data_point|
|
305
|
-
norm_x_data_points << ((x_data_point.to_f - @minimum_x_value.to_f) /
|
306
|
-
(@maximum_x_value.to_f - @minimum_x_value.to_f))
|
307
|
-
end
|
308
|
-
@norm_data[index] << norm_x_data_points
|
309
|
-
end
|
267
|
+
curr_reference_line[:norm_value] = ((curr_reference_line[:value].to_f - minimum_value) / @spread.to_f) if curr_reference_line.key?(:value)
|
310
268
|
end
|
311
269
|
end
|
312
270
|
|
313
271
|
def sort_norm_data
|
314
|
-
super unless
|
272
|
+
super unless store.data.any?(&:x_points)
|
315
273
|
end
|
316
274
|
|
317
275
|
def get_x_coord(x_data_point, width, offset)
|
@@ -319,46 +277,6 @@ class Gruff::Line < Gruff::Base
|
|
319
277
|
end
|
320
278
|
|
321
279
|
def contains_one_point_only?(data_row)
|
322
|
-
|
323
|
-
one_point = false
|
324
|
-
data_row[DATA_VALUES_INDEX].each do |data_point|
|
325
|
-
unless data_point.nil?
|
326
|
-
if one_point
|
327
|
-
# more than one point, bail
|
328
|
-
return false
|
329
|
-
end
|
330
|
-
|
331
|
-
# there is at least one data point
|
332
|
-
one_point = true
|
333
|
-
end
|
334
|
-
end
|
335
|
-
one_point
|
336
|
-
end
|
337
|
-
|
338
|
-
module DotRenderers
|
339
|
-
class Circle
|
340
|
-
def render(d, new_x, new_y, circle_radius)
|
341
|
-
d.circle(new_x, new_y, new_x - circle_radius, new_y)
|
342
|
-
end
|
343
|
-
end
|
344
|
-
|
345
|
-
class Square
|
346
|
-
def render(d, new_x, new_y, circle_radius)
|
347
|
-
offset = (circle_radius * 0.8).to_i
|
348
|
-
corner_1 = new_x - offset
|
349
|
-
corner_2 = new_y - offset
|
350
|
-
corner_3 = new_x + offset
|
351
|
-
corner_4 = new_y + offset
|
352
|
-
d.rectangle(corner_1, corner_2, corner_3, corner_4)
|
353
|
-
end
|
354
|
-
end
|
355
|
-
|
356
|
-
def self.renderer(style)
|
357
|
-
if style.to_s == 'square'
|
358
|
-
Square.new
|
359
|
-
else
|
360
|
-
Circle.new
|
361
|
-
end
|
362
|
-
end
|
280
|
+
data_row.y_points.compact.count == 1
|
363
281
|
end
|
364
282
|
end
|