gruff 0.6.0-java → 0.11.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 +112 -0
- data/.travis.yml +24 -15
- data/.yardopts +1 -0
- data/{History.txt → CHANGELOG.md} +72 -25
- data/Gemfile +3 -7
- data/README.md +57 -25
- data/Rakefile +21 -192
- 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 +21 -13
- 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 +435 -704
- data/lib/gruff/bezier.rb +32 -17
- data/lib/gruff/bullet.rb +62 -68
- data/lib/gruff/dot.rb +38 -82
- 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 +60 -0
- data/lib/gruff/line.rb +134 -170
- 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 +68 -81
- data/lib/gruff/patch/rmagick.rb +33 -0
- data/lib/gruff/patch/string.rb +10 -0
- data/lib/gruff/photo_bar.rb +39 -42
- data/lib/gruff/pie.rb +180 -89
- 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 +42 -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 +132 -0
- data/lib/gruff/renderer/text.rb +53 -0
- data/lib/gruff/scatter.rb +163 -182
- data/lib/gruff/scene.rb +31 -41
- data/lib/gruff/side_bar.rb +81 -65
- data/lib/gruff/side_stacked_bar.rb +78 -62
- data/lib/gruff/spider.rb +49 -57
- data/lib/gruff/stacked_area.rb +40 -32
- data/lib/gruff/stacked_bar.rb +86 -53
- data/lib/gruff/store/base_data.rb +38 -0
- data/lib/gruff/store/custom_data.rb +38 -0
- data/lib/gruff/store/store.rb +80 -0
- data/lib/gruff/store/xy_data.rb +59 -0
- data/lib/gruff/themes.rb +32 -33
- data/lib/gruff/version.rb +3 -1
- metadata +80 -102
- 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_labels_for_null_data.rb +0 -27
- 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 -161
- 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 -68
|
@@ -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,42 @@
|
|
|
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
|
+
@shadow_color = args[:shadow_color]
|
|
10
|
+
@width = args[:width]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render(start_x, start_y, end_x, end_y)
|
|
14
|
+
render_line(start_x, start_y, end_x, end_y, @color)
|
|
15
|
+
render_line(start_x, start_y + 1, end_x, end_y + 1, @shadow_color) if @shadow_color
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def render_line(start_x, start_y, end_x, end_y, color)
|
|
21
|
+
# FIXME(uwe): Workaround for Issue #66
|
|
22
|
+
# https://github.com/topfunky/gruff/issues/66
|
|
23
|
+
# https://github.com/rmagick/rmagick/issues/82
|
|
24
|
+
# Remove if the issue gets fixed.
|
|
25
|
+
unless defined?(JRUBY_VERSION)
|
|
26
|
+
start_x += EPSILON
|
|
27
|
+
end_x += EPSILON
|
|
28
|
+
start_y += EPSILON
|
|
29
|
+
end_y += EPSILON
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
draw = Renderer.instance.draw
|
|
33
|
+
|
|
34
|
+
draw.push
|
|
35
|
+
draw.stroke(color)
|
|
36
|
+
draw.fill(color)
|
|
37
|
+
draw.stroke_width(@width) if @width
|
|
38
|
+
draw.line(start_x, start_y, end_x, end_y)
|
|
39
|
+
draw.pop
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
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,132 @@
|
|
|
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, :text_renderers
|
|
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
|
+
Renderer.instance.text_renderers = []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def setup_transparent_background(columns, rows)
|
|
28
|
+
image = Renderer.instance.render_transparent_background(columns, rows)
|
|
29
|
+
Renderer.instance.image = image
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def background_image=(image)
|
|
33
|
+
Renderer.instance.image = image
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def font=(font)
|
|
37
|
+
draw = Renderer.instance.draw
|
|
38
|
+
draw.font = font if font
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def finish
|
|
42
|
+
draw = Renderer.instance.draw
|
|
43
|
+
image = Renderer.instance.image
|
|
44
|
+
|
|
45
|
+
draw.draw(image)
|
|
46
|
+
|
|
47
|
+
Renderer.instance.text_renderers.each do |renderer|
|
|
48
|
+
renderer.render(renderer.width, renderer.height, renderer.x, renderer.y, renderer.gravity)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def write(file_name)
|
|
53
|
+
Renderer.instance.image.write(file_name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_blob(file_format)
|
|
57
|
+
Renderer.instance.image.to_blob do
|
|
58
|
+
self.format = file_format
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def background(columns, rows, scale, theme_options)
|
|
64
|
+
case theme_options[:background_colors]
|
|
65
|
+
when Array
|
|
66
|
+
gradated_background(columns, rows, theme_options[:background_colors][0], theme_options[:background_colors][1], theme_options[:background_direction])
|
|
67
|
+
when String
|
|
68
|
+
solid_background(columns, rows, theme_options[:background_colors])
|
|
69
|
+
else
|
|
70
|
+
image_background(scale, *theme_options[:background_image])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Make a new image at the current size with a solid +color+.
|
|
75
|
+
def solid_background(columns, rows, color)
|
|
76
|
+
Magick::Image.new(columns, rows) do
|
|
77
|
+
self.background_color = color
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Use with a theme definition method to draw a gradated background.
|
|
82
|
+
def gradated_background(columns, rows, top_color, bottom_color, direct = :top_bottom)
|
|
83
|
+
gradient_fill = begin
|
|
84
|
+
case direct
|
|
85
|
+
when :bottom_top
|
|
86
|
+
Magick::GradientFill.new(0, 0, 100, 0, bottom_color, top_color)
|
|
87
|
+
when :left_right
|
|
88
|
+
Magick::GradientFill.new(0, 0, 0, 100, top_color, bottom_color)
|
|
89
|
+
when :right_left
|
|
90
|
+
Magick::GradientFill.new(0, 0, 0, 100, bottom_color, top_color)
|
|
91
|
+
when :topleft_bottomright
|
|
92
|
+
Magick::GradientFill.new(0, 100, 100, 0, top_color, bottom_color)
|
|
93
|
+
when :topright_bottomleft
|
|
94
|
+
Magick::GradientFill.new(0, 0, 100, 100, bottom_color, top_color)
|
|
95
|
+
else
|
|
96
|
+
Magick::GradientFill.new(0, 0, 100, 0, top_color, bottom_color)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
image = Magick::Image.new(columns, rows, gradient_fill)
|
|
101
|
+
@gradated_background_retry_count = 0
|
|
102
|
+
|
|
103
|
+
image
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
@gradated_background_retry_count ||= 0
|
|
106
|
+
GC.start
|
|
107
|
+
|
|
108
|
+
if @gradated_background_retry_count < 3
|
|
109
|
+
@gradated_background_retry_count += 1
|
|
110
|
+
gradated_background(columns, rows, top_color, bottom_color, direct)
|
|
111
|
+
else
|
|
112
|
+
raise e
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Use with a theme to use an image (800x600 original) background.
|
|
117
|
+
def image_background(scale, image_path)
|
|
118
|
+
image = Magick::Image.read(image_path)
|
|
119
|
+
if scale != 1.0
|
|
120
|
+
image[0].resize!(scale) # TODO: Resize with new scale (crop if necessary for wide graph)
|
|
121
|
+
end
|
|
122
|
+
image[0]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Use with a theme to make a transparent background
|
|
126
|
+
def render_transparent_background(columns, rows)
|
|
127
|
+
Magick::Image.new(columns, rows) do
|
|
128
|
+
self.background_color = 'transparent'
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
attr_reader :width, :height, :x, :y, :gravity
|
|
15
|
+
def add_to_render_queue(width, height, x, y, gravity = Magick::NorthGravity)
|
|
16
|
+
@width = width
|
|
17
|
+
@height = height
|
|
18
|
+
@x = x
|
|
19
|
+
@y = y
|
|
20
|
+
@gravity = gravity
|
|
21
|
+
|
|
22
|
+
Renderer.instance.text_renderers << self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render(width, height, x, y, gravity = Magick::NorthGravity)
|
|
26
|
+
draw = Renderer.instance.draw
|
|
27
|
+
image = Renderer.instance.image
|
|
28
|
+
scale = Renderer.instance.scale
|
|
29
|
+
|
|
30
|
+
draw.rotation = @rotation if @rotation
|
|
31
|
+
draw.fill = @font_color
|
|
32
|
+
draw.stroke = 'transparent'
|
|
33
|
+
draw.font = @font if @font
|
|
34
|
+
draw.font_weight = @font_weight
|
|
35
|
+
draw.pointsize = @font_size * scale
|
|
36
|
+
draw.gravity = gravity
|
|
37
|
+
draw.annotate_scaled(image,
|
|
38
|
+
width, height,
|
|
39
|
+
x, y,
|
|
40
|
+
@text, scale)
|
|
41
|
+
draw.rotation = -@rotation if @rotation
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.metrics(text, size, font_weight = Magick::NormalWeight)
|
|
45
|
+
draw = Renderer.instance.draw
|
|
46
|
+
image = Renderer.instance.image
|
|
47
|
+
|
|
48
|
+
draw.font_weight = font_weight
|
|
49
|
+
draw.pointsize = size
|
|
50
|
+
draw.get_type_metrics(image, text.to_s)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/gruff/scatter.rb
CHANGED
|
@@ -1,109 +1,93 @@
|
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
# Minimum X Value. The value will get overwritten by the min in the
|
|
18
|
-
# datasets.
|
|
19
|
-
attr_accessor :minimum_x_value
|
|
20
|
-
|
|
21
|
-
# The number of vertical lines shown for reference
|
|
22
|
-
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
|
|
15
|
+
# datasets.
|
|
16
|
+
attr_writer :maximum_x_value
|
|
51
17
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
18
|
+
# Minimum X Value. The value will get overwritten by the min in the
|
|
19
|
+
# datasets.
|
|
20
|
+
attr_writer :minimum_x_value
|
|
21
|
+
|
|
22
|
+
# The number of vertical lines shown for reference.
|
|
23
|
+
attr_writer :marker_x_count
|
|
24
|
+
|
|
25
|
+
# Attributes to allow customising the size of the points.
|
|
26
|
+
attr_writer :circle_radius
|
|
27
|
+
attr_writer :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_writer :disable_significant_rounding_x_axis
|
|
32
|
+
|
|
33
|
+
# Allow enabling vertical lines. When you have a lot of data, they can work great.
|
|
34
|
+
attr_writer :enable_vertical_line_markers
|
|
55
35
|
|
|
36
|
+
# Allow using vertical labels in the X axis (and setting the label margin).
|
|
37
|
+
attr_writer :x_label_margin
|
|
38
|
+
attr_writer :use_vertical_x_labels
|
|
39
|
+
|
|
40
|
+
# Allow passing lambdas to format labels.
|
|
41
|
+
attr_writer :y_axis_label_format
|
|
42
|
+
attr_writer :x_axis_label_format
|
|
43
|
+
|
|
44
|
+
def initialize_store
|
|
45
|
+
@store = Gruff::Store.new(Gruff::Store::XYData)
|
|
46
|
+
end
|
|
47
|
+
private :initialize_store
|
|
48
|
+
|
|
49
|
+
def initialize_ivars
|
|
56
50
|
super
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@
|
|
52
|
+
@baseline_x_color = @baseline_y_color = 'red'
|
|
53
|
+
@baseline_x_value = @baseline_y_value = nil
|
|
54
|
+
@circle_radius = nil
|
|
55
|
+
@disable_significant_rounding_x_axis = false
|
|
56
|
+
@enable_vertical_line_markers = false
|
|
57
|
+
@marker_x_count = nil
|
|
58
|
+
@maximum_x_value = @minimum_x_value = nil
|
|
59
|
+
@stroke_width = nil
|
|
60
|
+
@use_vertical_x_labels = false
|
|
61
|
+
@x_axis_label_format = nil
|
|
62
|
+
@x_label_margin = nil
|
|
63
|
+
@y_axis_label_format = nil
|
|
61
64
|
end
|
|
65
|
+
private :initialize_ivars
|
|
62
66
|
|
|
63
67
|
def draw
|
|
64
68
|
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?
|
|
69
|
+
return unless data_given?
|
|
70
|
+
|
|
71
|
+
# Check to see if more than one datapoint was given. NaN can result otherwise.
|
|
72
|
+
@x_increment = (@x_spread > 1) ? (@graph_width / (@x_spread - 1).to_f) : @graph_width
|
|
73
|
+
|
|
74
|
+
store.norm_data.each do |data_row|
|
|
75
|
+
data_row.coordinates.each do |x_value, y_value|
|
|
76
|
+
next if y_value.nil? || x_value.nil?
|
|
89
77
|
|
|
90
78
|
new_x = get_x_coord(x_value, @graph_width, @graph_left)
|
|
91
|
-
new_y = @graph_top + (@graph_height -
|
|
79
|
+
new_y = @graph_top + (@graph_height - y_value * @graph_height)
|
|
92
80
|
|
|
93
81
|
# 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)
|
|
82
|
+
stroke_width = @stroke_width || clip_value_if_greater_than(@columns / (store.norm_data.first[1].size * 4), 5.0)
|
|
83
|
+
circle_radius = @circle_radius || clip_value_if_greater_than(@columns / (store.norm_data.first[1].size * 2.5), 5.0)
|
|
84
|
+
Gruff::Renderer::Circle.new(color: data_row.color, width: stroke_width).render(new_x, new_y, new_x - circle_radius, new_y)
|
|
101
85
|
end
|
|
102
86
|
end
|
|
103
87
|
|
|
104
|
-
|
|
88
|
+
Gruff::Renderer.finish
|
|
105
89
|
end
|
|
106
|
-
|
|
90
|
+
|
|
107
91
|
# The first parameter is the name of the dataset. The next two are the
|
|
108
92
|
# x and y axis data points contain in their own array in that respective
|
|
109
93
|
# order. The final parameter is the color.
|
|
@@ -114,95 +98,83 @@ class Gruff::Scatter < Gruff::Base
|
|
|
114
98
|
# If the color argument is nil, the next color from the default theme will
|
|
115
99
|
# be used.
|
|
116
100
|
#
|
|
117
|
-
#
|
|
118
|
-
# data().
|
|
101
|
+
# @note If you want to use a preset theme, you must set it before calling {#data}.
|
|
119
102
|
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
# color:: The hex string for the color of the dataset. Defaults to nil.
|
|
103
|
+
# @param name [String, Symbol] containing the name of the dataset.
|
|
104
|
+
# @param x_data_points [Array] An Array of of x-axis data points.
|
|
105
|
+
# @param y_data_points [Array] An Array of of y-axis data points.
|
|
106
|
+
# @param color [String] The hex string for the color of the dataset. Defaults to nil.
|
|
125
107
|
#
|
|
126
|
-
#
|
|
127
|
-
# Data points contain nil values
|
|
108
|
+
#
|
|
109
|
+
# @raise [ArgumentError] Data points contain nil values.
|
|
128
110
|
# 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
|
|
111
|
+
# contains a +nil+ value. The graph will not make an assumption
|
|
112
|
+
# as how to graph +nil+.
|
|
113
|
+
# @raise [ArgumentError] +x_data_points+ is empty.
|
|
132
114
|
# 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
|
|
115
|
+
# @raise [ArgumentError] +y_data_points+ is empty.
|
|
116
|
+
# This error is raised when the array for the y-axis points are empty.
|
|
117
|
+
# @raise [ArgumentError] +x_data_points.length != y_data_points.length+.
|
|
118
|
+
# Error means that the x and y axis point arrays do not match in length.
|
|
137
119
|
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
120
|
+
# @example
|
|
121
|
+
# g = Gruff::Scatter.new
|
|
122
|
+
# g.data(:apples, [1,2,3], [3,2,1])
|
|
123
|
+
# g.data('oranges', [1,1,1], [2,3,4])
|
|
124
|
+
# g.data('bitter_melon', [3,5,6], [6,7,8], '#000000')
|
|
143
125
|
#
|
|
144
|
-
def data(name, x_data_points=[], y_data_points=[], color=nil)
|
|
145
|
-
|
|
126
|
+
def data(name, x_data_points = [], y_data_points = [], color = nil)
|
|
127
|
+
# make sure it's an array
|
|
128
|
+
x_data_points = Array(x_data_points)
|
|
129
|
+
y_data_points = Array(y_data_points)
|
|
130
|
+
|
|
146
131
|
raise ArgumentError, 'Data Points contain nil Value!' if x_data_points.include?(nil) || y_data_points.include?(nil)
|
|
147
132
|
raise ArgumentError, 'x_data_points is empty!' if x_data_points.empty?
|
|
148
133
|
raise ArgumentError, 'y_data_points is empty!' if y_data_points.empty?
|
|
149
134
|
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
|
-
#append the x data to the last entry that was just added in the @data member
|
|
155
|
-
last_elem = @data.length()-1
|
|
156
|
-
@data[last_elem] << x_data_points
|
|
157
|
-
|
|
158
|
-
if @maximum_x_value.nil? && @minimum_x_value.nil?
|
|
159
|
-
@maximum_x_value = @minimum_x_value = x_data_points.first
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
@maximum_x_value = x_data_points.max > @maximum_x_value ?
|
|
163
|
-
x_data_points.max : @maximum_x_value
|
|
164
|
-
@minimum_x_value = x_data_points.min < @minimum_x_value ?
|
|
165
|
-
x_data_points.min : @minimum_x_value
|
|
135
|
+
|
|
136
|
+
# Call the existing data routine for the x/y axis data
|
|
137
|
+
store.add(name, y_data_points, color, x_data_points)
|
|
166
138
|
end
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
139
|
+
|
|
140
|
+
alias dataxy data
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def setup_data
|
|
145
|
+
# Update the global min/max values for the x data
|
|
146
|
+
@maximum_x_value ||= store.max_x
|
|
147
|
+
@minimum_x_value ||= store.min_x
|
|
148
|
+
|
|
149
|
+
super
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def setup_drawing
|
|
153
|
+
# TODO: Need to get x-axis labels working. Current behavior will be to not allow.
|
|
154
|
+
@labels = {}
|
|
155
|
+
|
|
156
|
+
super
|
|
157
|
+
end
|
|
158
|
+
|
|
170
159
|
def calculate_spread #:nodoc:
|
|
171
160
|
super
|
|
172
161
|
@x_spread = @maximum_x_value.to_f - @minimum_x_value.to_f
|
|
173
162
|
@x_spread = @x_spread > 0 ? @x_spread : 1
|
|
174
163
|
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
|
|
164
|
+
|
|
165
|
+
def normalize
|
|
166
|
+
return unless data_given?
|
|
167
|
+
|
|
168
|
+
store.normalize(minimum_x: @minimum_x_value, spread_x: @x_spread, minimum_y: minimum_value, spread_y: @spread)
|
|
195
169
|
end
|
|
196
|
-
|
|
170
|
+
|
|
197
171
|
def draw_line_markers
|
|
198
172
|
# do all of the stuff for the horizontal lines on the y-axis
|
|
199
173
|
super
|
|
200
174
|
return if @hide_line_markers
|
|
201
|
-
|
|
202
|
-
@d = @d.stroke_antialias false
|
|
203
175
|
|
|
204
176
|
if @x_axis_increment.nil?
|
|
205
|
-
# TODO Do the same for larger numbers...100, 75, 50, 25
|
|
177
|
+
# TODO: Do the same for larger numbers...100, 75, 50, 25
|
|
206
178
|
if @marker_x_count.nil?
|
|
207
179
|
(3..7).each do |lines|
|
|
208
180
|
if @x_spread % lines == 0.0
|
|
@@ -212,53 +184,62 @@ protected
|
|
|
212
184
|
end
|
|
213
185
|
@marker_x_count ||= 4
|
|
214
186
|
end
|
|
215
|
-
@x_increment = (@x_spread > 0) ?
|
|
187
|
+
@x_increment = (@x_spread > 0) ? (@x_spread / @marker_x_count) : 1
|
|
188
|
+
unless @disable_significant_rounding_x_axis
|
|
189
|
+
@x_increment = significant(@x_increment)
|
|
190
|
+
end
|
|
216
191
|
else
|
|
217
|
-
# TODO Make this work for negative values
|
|
218
|
-
@maximum_x_value = [
|
|
192
|
+
# TODO: Make this work for negative values
|
|
193
|
+
@maximum_x_value = [maximum_value.ceil, @x_axis_increment].max
|
|
219
194
|
@minimum_x_value = @minimum_x_value.floor
|
|
220
195
|
calculate_spread
|
|
221
|
-
normalize
|
|
222
|
-
|
|
196
|
+
normalize
|
|
197
|
+
|
|
223
198
|
@marker_count = (@x_spread / @x_axis_increment).to_i
|
|
224
199
|
@x_increment = @x_axis_increment
|
|
225
200
|
end
|
|
226
|
-
|
|
201
|
+
increment_x_scaled = @graph_width.to_f / (@x_spread / @x_increment)
|
|
227
202
|
|
|
228
203
|
# Draw vertical line markers and annotate with numbers
|
|
229
204
|
(0..@marker_x_count).each do |index|
|
|
205
|
+
# TODO: Fix the vertical lines, and enable them by default. Not pretty when they don't match up with top y-axis line
|
|
206
|
+
if @enable_vertical_line_markers
|
|
207
|
+
x = @graph_left + @graph_width - index.to_f * increment_x_scaled
|
|
230
208
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
# @d = @d.stroke_width 1
|
|
235
|
-
# @d = @d.line(x, @graph_top, x, @graph_bottom)
|
|
209
|
+
line_renderer = Gruff::Renderer::Line.new(color: @marker_color, shadow_color: @marker_shadow_color)
|
|
210
|
+
line_renderer.render(x, @graph_top, x, @graph_bottom)
|
|
211
|
+
end
|
|
236
212
|
|
|
237
213
|
unless @hide_line_numbers
|
|
238
214
|
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)
|
|
215
|
+
y_offset = @graph_bottom + (@x_label_margin || LABEL_MARGIN)
|
|
216
|
+
x_offset = get_x_coord(index.to_f, increment_x_scaled, @graph_left)
|
|
217
|
+
|
|
218
|
+
label = vertical_label(marker_label, @x_increment)
|
|
219
|
+
rotation = -90.0 if @use_vertical_x_labels
|
|
220
|
+
text_renderer = Gruff::Renderer::Text.new(label, font: @font, size: @marker_font_size, color: @font_color, rotation: rotation)
|
|
221
|
+
text_renderer.add_to_render_queue(1.0, 1.0, x_offset, y_offset)
|
|
252
222
|
end
|
|
253
223
|
end
|
|
254
|
-
|
|
255
|
-
@d = @d.stroke_antialias true
|
|
256
224
|
end
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
225
|
+
|
|
226
|
+
def label(value, increment)
|
|
227
|
+
if @y_axis_label_format
|
|
228
|
+
@y_axis_label_format.call(value)
|
|
229
|
+
else
|
|
230
|
+
super
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def vertical_label(value, increment)
|
|
235
|
+
if @x_axis_label_format
|
|
236
|
+
@x_axis_label_format.call(value)
|
|
237
|
+
else
|
|
238
|
+
label(value, increment)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
260
242
|
def get_x_coord(x_data_point, width, offset) #:nodoc:
|
|
261
243
|
x_data_point * width + offset
|
|
262
244
|
end
|
|
263
|
-
|
|
264
|
-
end # end Gruff::Scatter
|
|
245
|
+
end
|