gruff 0.5.1-java → 0.10.0-java
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 +5 -5
- data/.editorconfig +14 -0
- data/.github/ISSUE_TEMPLATE.md +18 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +109 -0
- data/.rubocop_todo.yml +130 -0
- data/.travis.yml +24 -12
- data/.yardopts +1 -0
- data/{History.txt → CHANGELOG.md} +61 -25
- data/Gemfile +3 -7
- data/README.md +57 -25
- data/Rakefile +6 -201
- data/assets/plastik/blue.png +0 -0
- data/assets/plastik/green.png +0 -0
- data/assets/plastik/red.png +0 -0
- data/docker/Dockerfile +14 -0
- data/docker/build.sh +4 -0
- data/docker/launch.sh +4 -0
- data/gruff.gemspec +19 -14
- data/init.rb +2 -0
- data/lib/gruff.rb +26 -2
- data/lib/gruff/accumulator_bar.rb +18 -8
- data/lib/gruff/area.rb +33 -19
- data/lib/gruff/bar.rb +76 -45
- data/lib/gruff/base.rb +337 -613
- data/lib/gruff/bezier.rb +34 -19
- data/lib/gruff/bullet.rb +51 -62
- data/lib/gruff/dot.rb +38 -62
- data/lib/gruff/helper/bar_conversion.rb +47 -0
- data/lib/gruff/helper/bar_value_label_mixin.rb +30 -0
- data/lib/gruff/helper/stacked_mixin.rb +23 -0
- data/lib/gruff/histogram.rb +59 -0
- data/lib/gruff/line.rb +130 -150
- data/lib/gruff/mini/bar.rb +17 -10
- data/lib/gruff/mini/legend.rb +24 -36
- data/lib/gruff/mini/pie.rb +18 -12
- data/lib/gruff/mini/side_bar.rb +26 -12
- data/lib/gruff/net.rb +60 -84
- data/lib/gruff/patch/rmagick.rb +33 -0
- data/lib/gruff/patch/string.rb +10 -0
- data/lib/gruff/photo_bar.rb +27 -30
- data/lib/gruff/pie.rb +190 -93
- 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 +156 -180
- data/lib/gruff/scene.rb +31 -41
- data/lib/gruff/side_bar.rb +77 -63
- data/lib/gruff/side_stacked_bar.rb +77 -60
- data/lib/gruff/spider.rb +37 -50
- data/lib/gruff/stacked_area.rb +32 -30
- data/lib/gruff/stacked_bar.rb +87 -49
- 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 +32 -33
- data/lib/gruff/version.rb +3 -1
- metadata +88 -94
- data/Manifest.txt +0 -81
- data/RELEASE.md +0 -30
- 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/bar_conversion.rb +0 -46
- data/lib/gruff/deprecated.rb +0 -39
- data/lib/gruff/stacked_mixin.rb +0 -23
- data/test/gruff_test_case.rb +0 -154
- data/test/image_compare.rb +0 -58
- data/test/test_accumulator_bar.rb +0 -51
- data/test/test_area.rb +0 -134
- data/test/test_bar.rb +0 -505
- data/test/test_base.rb +0 -8
- data/test/test_bezier.rb +0 -33
- data/test/test_bullet.rb +0 -26
- data/test/test_dot.rb +0 -263
- data/test/test_legend.rb +0 -68
- data/test/test_line.rb +0 -657
- data/test/test_mini_bar.rb +0 -33
- data/test/test_mini_pie.rb +0 -25
- data/test/test_mini_side_bar.rb +0 -36
- data/test/test_net.rb +0 -231
- data/test/test_photo.rb +0 -41
- data/test/test_pie.rb +0 -154
- data/test/test_scatter.rb +0 -233
- data/test/test_scene.rb +0 -100
- data/test/test_side_bar.rb +0 -56
- data/test/test_sidestacked_bar.rb +0 -105
- data/test/test_spider.rb +0 -226
- data/test/test_stacked_area.rb +0 -52
- data/test/test_stacked_bar.rb +0 -52
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gruff
|
|
4
|
+
class Renderer::Bezier
|
|
5
|
+
def initialize(args = {})
|
|
6
|
+
@color = args[:color]
|
|
7
|
+
@width = args[:width] || 1.0
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def render(points)
|
|
11
|
+
draw = Renderer.instance.draw
|
|
12
|
+
|
|
13
|
+
draw.push
|
|
14
|
+
draw.stroke(@color)
|
|
15
|
+
draw.stroke_width(@width)
|
|
16
|
+
draw.fill_opacity(0.0)
|
|
17
|
+
draw.bezier(*points)
|
|
18
|
+
draw.pop
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gruff
|
|
4
|
+
class Renderer::Circle
|
|
5
|
+
def initialize(args = {})
|
|
6
|
+
@color = args[:color]
|
|
7
|
+
@width = args[:width] || 1.0
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def render(origin_x, origin_y, perim_x, perim_y)
|
|
11
|
+
draw = Renderer.instance.draw
|
|
12
|
+
|
|
13
|
+
draw.push
|
|
14
|
+
draw.fill(@color)
|
|
15
|
+
draw.stroke(@color)
|
|
16
|
+
draw.stroke_width(@width)
|
|
17
|
+
draw.circle(origin_x, origin_y, perim_x, perim_y)
|
|
18
|
+
draw.pop
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gruff
|
|
4
|
+
class Renderer::DashLine
|
|
5
|
+
def initialize(args = {})
|
|
6
|
+
@color = args[:color]
|
|
7
|
+
@width = args[:width]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def render(start_x, start_y, end_x, end_y)
|
|
11
|
+
draw = Renderer.instance.draw
|
|
12
|
+
|
|
13
|
+
draw.push
|
|
14
|
+
draw.stroke_color(@color)
|
|
15
|
+
draw.fill_opacity(0.0)
|
|
16
|
+
draw.stroke_dasharray(10, 20)
|
|
17
|
+
draw.stroke_width(@width)
|
|
18
|
+
draw.line(start_x, start_y, end_x, end_y)
|
|
19
|
+
draw.pop
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gruff
|
|
4
|
+
class Renderer::Dot
|
|
5
|
+
def initialize(style, config)
|
|
6
|
+
@style = style
|
|
7
|
+
@color = config[:color]
|
|
8
|
+
@width = config[:width] || 1.0
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def render(new_x, new_y, circle_radius)
|
|
12
|
+
draw = Renderer.instance.draw
|
|
13
|
+
|
|
14
|
+
# draw.push # TODO
|
|
15
|
+
draw.stroke_width(@width)
|
|
16
|
+
draw.stroke(@color)
|
|
17
|
+
draw.fill(@color)
|
|
18
|
+
if @style.to_s == 'square'
|
|
19
|
+
square(draw, new_x, new_y, circle_radius)
|
|
20
|
+
else
|
|
21
|
+
circle(draw, new_x, new_y, circle_radius)
|
|
22
|
+
end
|
|
23
|
+
# draw.pop # TODO
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def circle(draw, new_x, new_y, circle_radius)
|
|
27
|
+
draw.circle(new_x, new_y, new_x - circle_radius, new_y)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def square(draw, new_x, new_y, circle_radius)
|
|
31
|
+
offset = (circle_radius * 0.8).to_i
|
|
32
|
+
corner1 = new_x - offset
|
|
33
|
+
corner2 = new_y - offset
|
|
34
|
+
corner3 = new_x + offset
|
|
35
|
+
corner4 = new_y + offset
|
|
36
|
+
draw.rectangle(corner1, corner2, corner3, corner4)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gruff
|
|
4
|
+
class Renderer::Ellipse
|
|
5
|
+
def initialize(args = {})
|
|
6
|
+
@color = args[:color]
|
|
7
|
+
@width = args[:width] || 1.0
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def render(origin_x, origin_y, width, height, arc_start, arc_end)
|
|
11
|
+
draw = Renderer.instance.draw
|
|
12
|
+
|
|
13
|
+
draw.push
|
|
14
|
+
draw.stroke_width(@width)
|
|
15
|
+
draw.stroke(@color)
|
|
16
|
+
draw.fill('transparent')
|
|
17
|
+
draw.ellipse(origin_x, origin_y, width, height, arc_start, arc_end)
|
|
18
|
+
draw.pop
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gruff
|
|
4
|
+
class Renderer::Line
|
|
5
|
+
EPSILON = 0.001
|
|
6
|
+
|
|
7
|
+
def initialize(args = {})
|
|
8
|
+
@color = args[:color]
|
|
9
|
+
@width = args[:width]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render(start_x, start_y, end_x, end_y)
|
|
13
|
+
# FIXME(uwe): Workaround for Issue #66
|
|
14
|
+
# https://github.com/topfunky/gruff/issues/66
|
|
15
|
+
# https://github.com/rmagick/rmagick/issues/82
|
|
16
|
+
# Remove if the issue gets fixed.
|
|
17
|
+
unless defined?(JRUBY_VERSION)
|
|
18
|
+
start_x += EPSILON
|
|
19
|
+
end_x += EPSILON
|
|
20
|
+
start_y += EPSILON
|
|
21
|
+
end_y += EPSILON
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
draw = Renderer.instance.draw
|
|
25
|
+
|
|
26
|
+
draw.push
|
|
27
|
+
draw.stroke(@color)
|
|
28
|
+
draw.fill(@color)
|
|
29
|
+
draw.stroke_width(@width) if @width
|
|
30
|
+
draw.line(start_x, start_y, end_x, end_y)
|
|
31
|
+
draw.pop
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gruff
|
|
4
|
+
class Renderer::Polygon
|
|
5
|
+
def initialize(args = {})
|
|
6
|
+
@color = args[:color]
|
|
7
|
+
@width = args[:width] || 1.0
|
|
8
|
+
@opacity = args[:opacity] || 1.0
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def render(points)
|
|
12
|
+
draw = Renderer.instance.draw
|
|
13
|
+
|
|
14
|
+
draw.push
|
|
15
|
+
draw.stroke_width(@width)
|
|
16
|
+
draw.stroke(@color)
|
|
17
|
+
draw.fill(@color)
|
|
18
|
+
draw.fill_opacity(@opacity)
|
|
19
|
+
draw.polygon(*points)
|
|
20
|
+
draw.pop
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gruff
|
|
4
|
+
class Renderer::Polyline
|
|
5
|
+
def initialize(args = {})
|
|
6
|
+
@color = args[:color]
|
|
7
|
+
@width = args[:width]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def render(points)
|
|
11
|
+
draw = Renderer.instance.draw
|
|
12
|
+
|
|
13
|
+
draw.push
|
|
14
|
+
draw.stroke(@color)
|
|
15
|
+
draw.fill('transparent')
|
|
16
|
+
draw.stroke_width(@width)
|
|
17
|
+
draw.polyline(*points)
|
|
18
|
+
draw.pop
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gruff
|
|
4
|
+
class Renderer::Rectangle
|
|
5
|
+
def initialize(args = {})
|
|
6
|
+
@color = args[:color]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def render(upper_left_x, upper_left_y, lower_right_x, lower_right_y)
|
|
10
|
+
draw = Renderer.instance.draw
|
|
11
|
+
|
|
12
|
+
draw.push
|
|
13
|
+
draw.stroke('transparent')
|
|
14
|
+
draw.fill(@color) if @color
|
|
15
|
+
draw.rectangle(upper_left_x, upper_left_y, lower_right_x, lower_right_y)
|
|
16
|
+
draw.pop
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
|
|
5
|
+
module Gruff
|
|
6
|
+
# @private
|
|
7
|
+
class Renderer
|
|
8
|
+
include Singleton
|
|
9
|
+
|
|
10
|
+
attr_accessor :draw, :image, :scale
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def setup(columns, rows, font, scale, theme_options)
|
|
14
|
+
draw = Magick::Draw.new
|
|
15
|
+
draw.font = font if font
|
|
16
|
+
# Scale down from 800x600 used to calculate drawing.
|
|
17
|
+
draw.scale(scale, scale)
|
|
18
|
+
|
|
19
|
+
image = Renderer.instance.background(columns, rows, scale, theme_options)
|
|
20
|
+
|
|
21
|
+
Renderer.instance.draw = draw
|
|
22
|
+
Renderer.instance.scale = scale
|
|
23
|
+
Renderer.instance.image = image
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def setup_transparent_background(columns, rows)
|
|
27
|
+
image = Renderer.instance.render_transparent_background(columns, rows)
|
|
28
|
+
Renderer.instance.image = image
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def background_image=(image)
|
|
32
|
+
Renderer.instance.image = image
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def font=(font)
|
|
36
|
+
draw = Renderer.instance.draw
|
|
37
|
+
draw.font = font if font
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def finish
|
|
41
|
+
draw = Renderer.instance.draw
|
|
42
|
+
image = Renderer.instance.image
|
|
43
|
+
|
|
44
|
+
draw.draw(image)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def write(file_name)
|
|
48
|
+
Renderer.instance.image.write(file_name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_blob(file_format)
|
|
52
|
+
Renderer.instance.image.to_blob do
|
|
53
|
+
self.format = file_format
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def background(columns, rows, scale, theme_options)
|
|
59
|
+
case theme_options[:background_colors]
|
|
60
|
+
when Array
|
|
61
|
+
gradated_background(columns, rows, theme_options[:background_colors][0], theme_options[:background_colors][1], theme_options[:background_direction])
|
|
62
|
+
when String
|
|
63
|
+
solid_background(columns, rows, theme_options[:background_colors])
|
|
64
|
+
else
|
|
65
|
+
image_background(scale, *theme_options[:background_image])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Make a new image at the current size with a solid +color+.
|
|
70
|
+
def solid_background(columns, rows, color)
|
|
71
|
+
Magick::Image.new(columns, rows) do
|
|
72
|
+
self.background_color = color
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Use with a theme definition method to draw a gradated background.
|
|
77
|
+
def gradated_background(columns, rows, top_color, bottom_color, direct = :top_bottom)
|
|
78
|
+
gradient_fill = begin
|
|
79
|
+
case direct
|
|
80
|
+
when :bottom_top
|
|
81
|
+
Magick::GradientFill.new(0, 0, 100, 0, bottom_color, top_color)
|
|
82
|
+
when :left_right
|
|
83
|
+
Magick::GradientFill.new(0, 0, 0, 100, top_color, bottom_color)
|
|
84
|
+
when :right_left
|
|
85
|
+
Magick::GradientFill.new(0, 0, 0, 100, bottom_color, top_color)
|
|
86
|
+
when :topleft_bottomright
|
|
87
|
+
Magick::GradientFill.new(0, 100, 100, 0, top_color, bottom_color)
|
|
88
|
+
when :topright_bottomleft
|
|
89
|
+
Magick::GradientFill.new(0, 0, 100, 100, bottom_color, top_color)
|
|
90
|
+
else
|
|
91
|
+
Magick::GradientFill.new(0, 0, 100, 0, top_color, bottom_color)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
image = Magick::Image.new(columns, rows, gradient_fill)
|
|
96
|
+
@gradated_background_retry_count = 0
|
|
97
|
+
|
|
98
|
+
image
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
@gradated_background_retry_count ||= 0
|
|
101
|
+
GC.start
|
|
102
|
+
|
|
103
|
+
if @gradated_background_retry_count < 3
|
|
104
|
+
@gradated_background_retry_count += 1
|
|
105
|
+
gradated_background(top_color, bottom_color, direct)
|
|
106
|
+
else
|
|
107
|
+
raise e
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Use with a theme to use an image (800x600 original) background.
|
|
112
|
+
def image_background(scale, image_path)
|
|
113
|
+
image = Magick::Image.read(image_path)
|
|
114
|
+
if scale != 1.0
|
|
115
|
+
image[0].resize!(scale) # TODO: Resize with new scale (crop if necessary for wide graph)
|
|
116
|
+
end
|
|
117
|
+
image[0]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Use with a theme to make a transparent background
|
|
121
|
+
def render_transparent_background(columns, rows)
|
|
122
|
+
Magick::Image.new(columns, rows) do
|
|
123
|
+
self.background_color = 'transparent'
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gruff
|
|
4
|
+
class Renderer::Text
|
|
5
|
+
def initialize(text, args = {})
|
|
6
|
+
@text = text.to_s
|
|
7
|
+
@font = args[:font]
|
|
8
|
+
@font_size = args[:size]
|
|
9
|
+
@font_color = args[:color]
|
|
10
|
+
@font_weight = args[:weight] || Magick::NormalWeight
|
|
11
|
+
@rotation = args[:rotation]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render(width, height, x, y, gravity = Magick::NorthGravity)
|
|
15
|
+
draw = Renderer.instance.draw
|
|
16
|
+
image = Renderer.instance.image
|
|
17
|
+
scale = Renderer.instance.scale
|
|
18
|
+
|
|
19
|
+
draw.rotation = @rotation if @rotation
|
|
20
|
+
draw.fill = @font_color
|
|
21
|
+
draw.stroke = 'transparent'
|
|
22
|
+
draw.font = @font if @font
|
|
23
|
+
draw.font_weight = @font_weight
|
|
24
|
+
draw.pointsize = @font_size * scale
|
|
25
|
+
draw.gravity = gravity
|
|
26
|
+
draw.annotate_scaled(image,
|
|
27
|
+
width, height,
|
|
28
|
+
x, y,
|
|
29
|
+
@text, scale)
|
|
30
|
+
draw.rotation = -@rotation if @rotation
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.metrics(text, size, font_weight = Magick::NormalWeight)
|
|
34
|
+
draw = Renderer.instance.draw
|
|
35
|
+
image = Renderer.instance.image
|
|
36
|
+
|
|
37
|
+
draw.font_weight = font_weight
|
|
38
|
+
draw.pointsize = size
|
|
39
|
+
draw.get_type_metrics(image, text.to_s)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/gruff/scatter.rb
CHANGED
|
@@ -1,109 +1,90 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require 'gruff/base'
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# Here's how to set up a Gruff::Scatter.
|
|
4
7
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# g = Gruff::Scatter.new(800)
|
|
9
|
+
# g.data :apples, [1,2,3,4], [4,3,2,1]
|
|
10
|
+
# g.data 'oranges', [5,7,8], [4,1,7]
|
|
11
|
+
# g.write('scatter.png')
|
|
10
12
|
#
|
|
11
13
|
class Gruff::Scatter < Gruff::Base
|
|
12
|
-
|
|
13
14
|
# Maximum X Value. The value will get overwritten by the max in the
|
|
14
|
-
# datasets.
|
|
15
|
+
# datasets.
|
|
15
16
|
attr_accessor :maximum_x_value
|
|
16
|
-
|
|
17
|
-
# Minimum X Value. The value will get overwritten by the min in the
|
|
18
|
-
# datasets.
|
|
17
|
+
|
|
18
|
+
# Minimum X Value. The value will get overwritten by the min in the
|
|
19
|
+
# datasets.
|
|
19
20
|
attr_accessor :minimum_x_value
|
|
20
|
-
|
|
21
|
-
# The number of vertical lines shown for reference
|
|
21
|
+
|
|
22
|
+
# The number of vertical lines shown for reference.
|
|
22
23
|
attr_accessor :marker_x_count
|
|
23
|
-
|
|
24
|
-
#~ # Draw a dashed horizontal line at the given y value
|
|
25
|
-
#~ attr_accessor :baseline_y_value
|
|
26
|
-
|
|
27
|
-
#~ # Color of the horizontal baseline
|
|
28
|
-
#~ attr_accessor :baseline_y_color
|
|
29
|
-
|
|
30
|
-
#~ # Draw a dashed horizontal line at the given y value
|
|
31
|
-
#~ attr_accessor :baseline_x_value
|
|
32
|
-
|
|
33
|
-
#~ # Color of the horizontal baseline
|
|
34
|
-
#~ attr_accessor :baseline_x_color
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# Gruff::Scatter takes the same parameters as the Gruff::Line graph
|
|
38
|
-
#
|
|
39
|
-
# ==== Example
|
|
40
|
-
#
|
|
41
|
-
# g = Gruff::Scatter.new
|
|
42
|
-
#
|
|
43
|
-
def initialize(*args)
|
|
44
|
-
super(*args)
|
|
45
|
-
|
|
46
|
-
@maximum_x_value = @minimum_x_value = nil
|
|
47
|
-
@baseline_x_color = @baseline_y_color = 'red'
|
|
48
|
-
@baseline_x_value = @baseline_y_value = nil
|
|
49
|
-
@marker_x_count = nil
|
|
50
|
-
end
|
|
51
24
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
25
|
+
# Attributes to allow customising the size of the points.
|
|
26
|
+
attr_accessor :circle_radius
|
|
27
|
+
attr_accessor :stroke_width
|
|
28
|
+
|
|
29
|
+
# Allow disabling the significant rounding when labeling the X axis.
|
|
30
|
+
# This is useful when working with a small range of high values (for example, a date range of months, while seconds as units).
|
|
31
|
+
attr_accessor :disable_significant_rounding_x_axis
|
|
32
|
+
|
|
33
|
+
# Allow enabling vertical lines. When you have a lot of data, they can work great.
|
|
34
|
+
attr_accessor :enable_vertical_line_markers
|
|
55
35
|
|
|
36
|
+
# Allow using vertical labels in the X axis (and setting the label margin).
|
|
37
|
+
attr_accessor :x_label_margin
|
|
38
|
+
attr_accessor :use_vertical_x_labels
|
|
39
|
+
|
|
40
|
+
# Allow passing lambdas to format labels.
|
|
41
|
+
attr_accessor :y_axis_label_format
|
|
42
|
+
attr_accessor :x_axis_label_format
|
|
43
|
+
|
|
44
|
+
def initialize_ivars
|
|
56
45
|
super
|
|
57
46
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@
|
|
47
|
+
@baseline_x_color = @baseline_y_color = 'red'
|
|
48
|
+
@baseline_x_value = @baseline_y_value = nil
|
|
49
|
+
@circle_radius = nil
|
|
50
|
+
@disable_significant_rounding_x_axis = false
|
|
51
|
+
@enable_vertical_line_markers = false
|
|
52
|
+
@marker_x_count = nil
|
|
53
|
+
@maximum_x_value = @minimum_x_value = nil
|
|
54
|
+
@stroke_width = nil
|
|
55
|
+
@use_vertical_x_labels = false
|
|
56
|
+
@x_axis_label_format = nil
|
|
57
|
+
@x_label_margin = nil
|
|
58
|
+
@y_axis_label_format = nil
|
|
59
|
+
|
|
60
|
+
@store = Gruff::Store.new(Gruff::Store::XYData)
|
|
61
61
|
end
|
|
62
|
+
private :initialize_ivars
|
|
62
63
|
|
|
63
64
|
def draw
|
|
64
65
|
super
|
|
65
|
-
return unless
|
|
66
|
-
|
|
67
|
-
# Check to see if more than one datapoint was given. NaN can result otherwise.
|
|
68
|
-
@x_increment = (@
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
#~ @d.stroke_color @baseline_color
|
|
74
|
-
#~ @d.fill_opacity 0.0
|
|
75
|
-
#~ @d.stroke_dasharray(10, 20)
|
|
76
|
-
#~ @d.stroke_width 5
|
|
77
|
-
#~ @d.line(@graph_left, level, @graph_left + @graph_width, level)
|
|
78
|
-
#~ @d = @d.pop
|
|
79
|
-
#~ end
|
|
80
|
-
|
|
81
|
-
#~ if (defined?(@norm_x_baseline)) then
|
|
82
|
-
|
|
83
|
-
#~ end
|
|
84
|
-
|
|
85
|
-
@norm_data.each do |data_row|
|
|
86
|
-
data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index|
|
|
87
|
-
x_value = data_row[DATA_VALUES_X_INDEX][index]
|
|
88
|
-
next if data_point.nil? || x_value.nil?
|
|
66
|
+
return unless data_given?
|
|
67
|
+
|
|
68
|
+
# Check to see if more than one datapoint was given. NaN can result otherwise.
|
|
69
|
+
@x_increment = (@x_spread > 1) ? (@graph_width / (@x_spread - 1).to_f) : @graph_width
|
|
70
|
+
|
|
71
|
+
store.norm_data.each do |data_row|
|
|
72
|
+
data_row.coordinates.each do |x_value, y_value|
|
|
73
|
+
next if y_value.nil? || x_value.nil?
|
|
89
74
|
|
|
90
75
|
new_x = get_x_coord(x_value, @graph_width, @graph_left)
|
|
91
|
-
new_y = @graph_top + (@graph_height -
|
|
76
|
+
new_y = @graph_top + (@graph_height - y_value * @graph_height)
|
|
92
77
|
|
|
93
78
|
# Reset each time to avoid thin-line errors
|
|
94
|
-
@
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
|
|
98
|
-
|
|
99
|
-
circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
|
|
100
|
-
@d = @d.circle(new_x, new_y, new_x - circle_radius, new_y)
|
|
79
|
+
stroke_width = @stroke_width || clip_value_if_greater_than(@columns / (store.norm_data.first[1].size * 4), 5.0)
|
|
80
|
+
circle_radius = @circle_radius || clip_value_if_greater_than(@columns / (store.norm_data.first[1].size * 2.5), 5.0)
|
|
81
|
+
Gruff::Renderer::Circle.new(color: data_row.color, width: stroke_width).render(new_x, new_y, new_x - circle_radius, new_y)
|
|
101
82
|
end
|
|
102
83
|
end
|
|
103
84
|
|
|
104
|
-
|
|
85
|
+
Gruff::Renderer.finish
|
|
105
86
|
end
|
|
106
|
-
|
|
87
|
+
|
|
107
88
|
# The first parameter is the name of the dataset. The next two are the
|
|
108
89
|
# x and y axis data points contain in their own array in that respective
|
|
109
90
|
# order. The final parameter is the color.
|
|
@@ -114,95 +95,83 @@ class Gruff::Scatter < Gruff::Base
|
|
|
114
95
|
# If the color argument is nil, the next color from the default theme will
|
|
115
96
|
# be used.
|
|
116
97
|
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
98
|
+
# @note If you want to use a preset theme, you must set it before calling {#data}.
|
|
99
|
+
#
|
|
100
|
+
# @param name [String, Symbol] containing the name of the dataset.
|
|
101
|
+
# @param x_data_points [Array] An Array of of x-axis data points.
|
|
102
|
+
# @param y_data_points [Array] An Array of of y-axis data points.
|
|
103
|
+
# @param color [String] The hex string for the color of the dataset. Defaults to nil.
|
|
119
104
|
#
|
|
120
|
-
# ==== Parameters
|
|
121
|
-
# name:: String or Symbol containing the name of the dataset.
|
|
122
|
-
# x_data_points:: An Array of of x-axis data points.
|
|
123
|
-
# y_data_points:: An Array of of y-axis data points.
|
|
124
|
-
# color:: The hex string for the color of the dataset. Defaults to nil.
|
|
125
105
|
#
|
|
126
|
-
#
|
|
127
|
-
# Data points contain nil values::
|
|
106
|
+
# @raise [ArgumentError] Data points contain nil values.
|
|
128
107
|
# This error will get raised if either the x or y axis data points array
|
|
129
|
-
# contains a
|
|
130
|
-
# as how to graph
|
|
131
|
-
# x_data_points is empty
|
|
108
|
+
# contains a +nil+ value. The graph will not make an assumption
|
|
109
|
+
# as how to graph +nil+.
|
|
110
|
+
# @raise [ArgumentError] +x_data_points+ is empty.
|
|
132
111
|
# This error is raised when the array for the x-axis points are empty
|
|
133
|
-
# y_data_points is empty
|
|
134
|
-
# This error is raised when the array for the y-axis points are empty
|
|
135
|
-
# x_data_points.length != y_data_points.length
|
|
136
|
-
# Error means that the x and y axis point arrays do not match in length
|
|
112
|
+
# @raise [ArgumentError] +y_data_points+ is empty.
|
|
113
|
+
# This error is raised when the array for the y-axis points are empty.
|
|
114
|
+
# @raise [ArgumentError] +x_data_points.length != y_data_points.length+.
|
|
115
|
+
# Error means that the x and y axis point arrays do not match in length.
|
|
137
116
|
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
117
|
+
# @example
|
|
118
|
+
# g = Gruff::Scatter.new
|
|
119
|
+
# g.data(:apples, [1,2,3], [3,2,1])
|
|
120
|
+
# g.data('oranges', [1,1,1], [2,3,4])
|
|
121
|
+
# g.data('bitter_melon', [3,5,6], [6,7,8], '#000000')
|
|
143
122
|
#
|
|
144
|
-
def data(name, x_data_points=[], y_data_points=[], color=nil)
|
|
145
|
-
|
|
123
|
+
def data(name, x_data_points = [], y_data_points = [], color = nil)
|
|
124
|
+
# make sure it's an array
|
|
125
|
+
x_data_points = Array(x_data_points)
|
|
126
|
+
y_data_points = Array(y_data_points)
|
|
127
|
+
|
|
146
128
|
raise ArgumentError, 'Data Points contain nil Value!' if x_data_points.include?(nil) || y_data_points.include?(nil)
|
|
147
129
|
raise ArgumentError, 'x_data_points is empty!' if x_data_points.empty?
|
|
148
130
|
raise ArgumentError, 'y_data_points is empty!' if y_data_points.empty?
|
|
149
131
|
raise ArgumentError, 'x_data_points.length != y_data_points.length!' if x_data_points.length != y_data_points.length
|
|
150
|
-
|
|
151
|
-
# Call the existing data routine for the y axis data
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
@
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
x_data_points.min : @minimum_x_value
|
|
132
|
+
|
|
133
|
+
# Call the existing data routine for the x/y axis data
|
|
134
|
+
store.add(name, y_data_points, color, x_data_points)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
alias dataxy data
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def setup_data
|
|
142
|
+
# Update the global min/max values for the x data
|
|
143
|
+
@maximum_x_value ||= store.max_x
|
|
144
|
+
@minimum_x_value ||= store.min_x
|
|
145
|
+
|
|
146
|
+
super
|
|
166
147
|
end
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
148
|
+
|
|
149
|
+
def setup_drawing
|
|
150
|
+
# TODO: Need to get x-axis labels working. Current behavior will be to not allow.
|
|
151
|
+
@labels = {}
|
|
152
|
+
|
|
153
|
+
super
|
|
154
|
+
end
|
|
155
|
+
|
|
170
156
|
def calculate_spread #:nodoc:
|
|
171
157
|
super
|
|
172
158
|
@x_spread = @maximum_x_value.to_f - @minimum_x_value.to_f
|
|
173
159
|
@x_spread = @x_spread > 0 ? @x_spread : 1
|
|
174
160
|
end
|
|
175
|
-
|
|
176
|
-
def normalize
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
@data.each do |data_row|
|
|
182
|
-
norm_data_points = [data_row[DATA_LABEL_INDEX]]
|
|
183
|
-
norm_data_points << data_row[DATA_VALUES_INDEX].map do |r|
|
|
184
|
-
(r.to_f - @minimum_value.to_f) / @spread
|
|
185
|
-
end
|
|
186
|
-
norm_data_points << data_row[DATA_COLOR_INDEX]
|
|
187
|
-
norm_data_points << data_row[DATA_VALUES_X_INDEX].map do |r|
|
|
188
|
-
(r.to_f - @minimum_x_value.to_f) / @x_spread
|
|
189
|
-
end
|
|
190
|
-
@norm_data << norm_data_points
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
#~ @norm_y_baseline = (@baseline_y_value.to_f / @maximum_value.to_f) if @baseline_y_value
|
|
194
|
-
#~ @norm_x_baseline = (@baseline_x_value.to_f / @maximum_x_value.to_f) if @baseline_x_value
|
|
161
|
+
|
|
162
|
+
def normalize
|
|
163
|
+
return unless data_given?
|
|
164
|
+
|
|
165
|
+
store.normalize(minimum_x: @minimum_x_value, spread_x: @x_spread, minimum_y: minimum_value, spread_y: @spread)
|
|
195
166
|
end
|
|
196
|
-
|
|
167
|
+
|
|
197
168
|
def draw_line_markers
|
|
198
169
|
# do all of the stuff for the horizontal lines on the y-axis
|
|
199
170
|
super
|
|
200
171
|
return if @hide_line_markers
|
|
201
|
-
|
|
202
|
-
@d = @d.stroke_antialias false
|
|
203
172
|
|
|
204
173
|
if @x_axis_increment.nil?
|
|
205
|
-
# TODO Do the same for larger numbers...100, 75, 50, 25
|
|
174
|
+
# TODO: Do the same for larger numbers...100, 75, 50, 25
|
|
206
175
|
if @marker_x_count.nil?
|
|
207
176
|
(3..7).each do |lines|
|
|
208
177
|
if @x_spread % lines == 0.0
|
|
@@ -212,53 +181,60 @@ protected
|
|
|
212
181
|
end
|
|
213
182
|
@marker_x_count ||= 4
|
|
214
183
|
end
|
|
215
|
-
@x_increment = (@x_spread > 0) ?
|
|
184
|
+
@x_increment = (@x_spread > 0) ? (@x_spread / @marker_x_count) : 1
|
|
185
|
+
unless @disable_significant_rounding_x_axis
|
|
186
|
+
@x_increment = significant(@x_increment)
|
|
187
|
+
end
|
|
216
188
|
else
|
|
217
|
-
# TODO Make this work for negative values
|
|
218
|
-
@maximum_x_value = [
|
|
189
|
+
# TODO: Make this work for negative values
|
|
190
|
+
@maximum_x_value = [maximum_value.ceil, @x_axis_increment].max
|
|
219
191
|
@minimum_x_value = @minimum_x_value.floor
|
|
220
192
|
calculate_spread
|
|
221
|
-
normalize
|
|
222
|
-
|
|
193
|
+
normalize
|
|
194
|
+
|
|
223
195
|
@marker_count = (@x_spread / @x_axis_increment).to_i
|
|
224
196
|
@x_increment = @x_axis_increment
|
|
225
197
|
end
|
|
226
|
-
|
|
198
|
+
increment_x_scaled = @graph_width.to_f / (@x_spread / @x_increment)
|
|
227
199
|
|
|
228
200
|
# Draw vertical line markers and annotate with numbers
|
|
229
201
|
(0..@marker_x_count).each do |index|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
# @d = @d.line(x, @graph_top, x, @graph_bottom)
|
|
202
|
+
# TODO: Fix the vertical lines, and enable them by default. Not pretty when they don't match up with top y-axis line
|
|
203
|
+
if @enable_vertical_line_markers
|
|
204
|
+
x = @graph_left + @graph_width - index.to_f * increment_x_scaled
|
|
205
|
+
Gruff::Renderer::Line.new(color: @marker_color).render(x, @graph_top, x, @graph_bottom)
|
|
206
|
+
end
|
|
236
207
|
|
|
237
208
|
unless @hide_line_numbers
|
|
238
209
|
marker_label = index * @x_increment + @minimum_x_value.to_f
|
|
239
|
-
y_offset = @graph_bottom + LABEL_MARGIN
|
|
240
|
-
x_offset = get_x_coord(index.to_f,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
@d.gravity = NorthGravity
|
|
247
|
-
|
|
248
|
-
@d = @d.annotate_scaled(@base_image,
|
|
249
|
-
1.0, 1.0,
|
|
250
|
-
x_offset, y_offset,
|
|
251
|
-
label(marker_label, @x_increment), @scale)
|
|
210
|
+
y_offset = @graph_bottom + (@x_label_margin || LABEL_MARGIN)
|
|
211
|
+
x_offset = get_x_coord(index.to_f, increment_x_scaled, @graph_left)
|
|
212
|
+
|
|
213
|
+
label = vertical_label(marker_label, @x_increment)
|
|
214
|
+
rotation = -90.0 if @use_vertical_x_labels
|
|
215
|
+
text_renderer = Gruff::Renderer::Text.new(label, font: @font, size: @marker_font_size, color: @font_color, rotation: rotation)
|
|
216
|
+
text_renderer.render(1.0, 1.0, x_offset, y_offset)
|
|
252
217
|
end
|
|
253
218
|
end
|
|
254
|
-
|
|
255
|
-
@d = @d.stroke_antialias true
|
|
256
219
|
end
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
220
|
+
|
|
221
|
+
def label(value, increment)
|
|
222
|
+
if @y_axis_label_format
|
|
223
|
+
@y_axis_label_format.call(value)
|
|
224
|
+
else
|
|
225
|
+
super
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def vertical_label(value, increment)
|
|
230
|
+
if @x_axis_label_format
|
|
231
|
+
@x_axis_label_format.call(value)
|
|
232
|
+
else
|
|
233
|
+
label(value, increment)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
260
237
|
def get_x_coord(x_data_point, width, offset) #:nodoc:
|
|
261
238
|
x_data_point * width + offset
|
|
262
239
|
end
|
|
263
|
-
|
|
264
|
-
end # end Gruff::Scatter
|
|
240
|
+
end
|