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