gruffy 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/gruffy.rb +32 -0
- data/lib/gruffy/accumulator_bar.rb +18 -0
- data/lib/gruffy/area.rb +51 -0
- data/lib/gruffy/bar.rb +108 -0
- data/lib/gruffy/bar_conversion.rb +46 -0
- data/lib/gruffy/base.rb +1201 -0
- data/lib/gruffy/bezier.rb +46 -0
- data/lib/gruffy/bullet.rb +111 -0
- data/lib/gruffy/deprecated.rb +39 -0
- data/lib/gruffy/dot.rb +125 -0
- data/lib/gruffy/line.rb +365 -0
- data/lib/gruffy/mini/bar.rb +37 -0
- data/lib/gruffy/mini/legend.rb +114 -0
- data/lib/gruffy/mini/pie.rb +36 -0
- data/lib/gruffy/mini/side_bar.rb +35 -0
- data/lib/gruffy/net.rb +127 -0
- data/lib/gruffy/photo_bar.rb +100 -0
- data/lib/gruffy/pie.rb +271 -0
- data/lib/gruffy/scatter.rb +314 -0
- data/lib/gruffy/scene.rb +209 -0
- data/lib/gruffy/side_bar.rb +138 -0
- data/lib/gruffy/side_stacked_bar.rb +97 -0
- data/lib/gruffy/spider.rb +125 -0
- data/lib/gruffy/stacked_area.rb +67 -0
- data/lib/gruffy/stacked_bar.rb +61 -0
- data/lib/gruffy/stacked_mixin.rb +23 -0
- data/lib/gruffy/themes.rb +102 -0
- data/lib/gruffy/version.rb +3 -0
- data/rails_generators/gruffy/gruffy_generator.rb +63 -0
- data/rails_generators/gruffy/templates/controller.rb +32 -0
- data/rails_generators/gruffy/templates/functional_test.rb +24 -0
- metadata +33 -2
@@ -0,0 +1,46 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
class Gruffy::Bezier < Gruffy::Base
|
4
|
+
def draw
|
5
|
+
super
|
6
|
+
|
7
|
+
return unless @has_data
|
8
|
+
|
9
|
+
@x_increment = @graph_width / (@column_count - 1).to_f
|
10
|
+
|
11
|
+
@norm_data.each do |data_row|
|
12
|
+
poly_points = Array.new
|
13
|
+
@d = @d.fill data_row[DATA_COLOR_INDEX]
|
14
|
+
|
15
|
+
data_row[1].each_with_index do |data_point, index|
|
16
|
+
# Use incremented x and scaled y
|
17
|
+
new_x = @graph_left + (@x_increment * index)
|
18
|
+
new_y = @graph_top + (@graph_height - data_point * @graph_height)
|
19
|
+
|
20
|
+
if index == 0 && RUBY_PLATFORM != 'java'
|
21
|
+
poly_points << new_x
|
22
|
+
poly_points << new_y
|
23
|
+
end
|
24
|
+
|
25
|
+
poly_points << new_x
|
26
|
+
poly_points << new_y
|
27
|
+
|
28
|
+
draw_label(new_x, index)
|
29
|
+
end
|
30
|
+
|
31
|
+
@d = @d.fill_opacity 0.0
|
32
|
+
@d = @d.stroke data_row[DATA_COLOR_INDEX]
|
33
|
+
@d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
|
34
|
+
|
35
|
+
if RUBY_PLATFORM == 'java'
|
36
|
+
@d = @d.polyline(*poly_points)
|
37
|
+
else
|
38
|
+
@d = @d.bezier(*poly_points)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
@d.draw(@base_image)
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
require 'gruffy/themes'
|
3
|
+
|
4
|
+
# http://en.wikipedia.org/wiki/Bullet_graph
|
5
|
+
class Gruffy::Bullet < Gruffy::Base
|
6
|
+
|
7
|
+
def initialize(target_width="400x40")
|
8
|
+
if not Numeric === target_width
|
9
|
+
geometric_width, geometric_height = target_width.split('x')
|
10
|
+
@columns = geometric_width.to_f
|
11
|
+
@rows = geometric_height.to_f
|
12
|
+
else
|
13
|
+
@columns = target_width.to_f
|
14
|
+
@rows = target_width.to_f / 5.0
|
15
|
+
end
|
16
|
+
|
17
|
+
initialize_ivars
|
18
|
+
|
19
|
+
reset_themes
|
20
|
+
self.theme = Gruffy::Themes::GREYSCALE
|
21
|
+
@title_font_size = 20
|
22
|
+
end
|
23
|
+
|
24
|
+
def data(value, maximum_value, options={})
|
25
|
+
@value = value.to_f
|
26
|
+
@maximum_value = maximum_value.to_f
|
27
|
+
@options = options
|
28
|
+
@options.map { |k, v| @options[k] = v.to_f if v === Numeric }
|
29
|
+
end
|
30
|
+
|
31
|
+
# def setup_drawing
|
32
|
+
# # Maybe should be done in one of the following functions for more granularity.
|
33
|
+
# unless @has_data
|
34
|
+
# draw_no_data()
|
35
|
+
# return
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# normalize()
|
39
|
+
# setup_graph_measurements()
|
40
|
+
# sort_norm_data() if @sort # Sort norm_data with avg largest values set first (for display)
|
41
|
+
#
|
42
|
+
# draw_legend()
|
43
|
+
# draw_line_markers()
|
44
|
+
# draw_axis_labels()
|
45
|
+
# draw_title
|
46
|
+
# end
|
47
|
+
|
48
|
+
def draw
|
49
|
+
# TODO Left label
|
50
|
+
# TODO Bottom labels and markers
|
51
|
+
# @graph_bottom
|
52
|
+
# Calculations are off 800x???
|
53
|
+
|
54
|
+
@colors.reverse!
|
55
|
+
|
56
|
+
draw_title
|
57
|
+
|
58
|
+
@margin = 30.0
|
59
|
+
@thickness = @raw_rows / 6.0
|
60
|
+
@right_margin = @margin
|
61
|
+
@graph_left = (@title && (@title_width * 1.3)) || @margin
|
62
|
+
@graph_width = @raw_columns - @graph_left - @right_margin
|
63
|
+
@graph_height = @thickness * 3.0
|
64
|
+
|
65
|
+
# Background
|
66
|
+
@d = @d.fill @colors[0]
|
67
|
+
@d = @d.rectangle(@graph_left, 0, @graph_left + @graph_width, @graph_height)
|
68
|
+
|
69
|
+
[:high, :low].each_with_index do |indicator, index|
|
70
|
+
next unless @options.has_key?(indicator)
|
71
|
+
@d = @d.fill @colors[index + 1]
|
72
|
+
indicator_width_x = @graph_left + @graph_width * (@options[indicator] / @maximum_value)
|
73
|
+
@d = @d.rectangle(@graph_left, 0, indicator_width_x, @graph_height)
|
74
|
+
end
|
75
|
+
|
76
|
+
if @options.has_key?(:target)
|
77
|
+
@d = @d.fill @font_color
|
78
|
+
target_x = @graph_left + @graph_width * (@options[:target] / @maximum_value)
|
79
|
+
half_thickness = @thickness / 2.0
|
80
|
+
@d = @d.rectangle(target_x, half_thickness, target_x + half_thickness, @thickness * 2 + half_thickness)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Value
|
84
|
+
@d = @d.fill @font_color
|
85
|
+
@d = @d.rectangle(@graph_left, @thickness, @graph_left + @graph_width * (@value / @maximum_value), @thickness * 2)
|
86
|
+
|
87
|
+
@d.draw(@base_image)
|
88
|
+
end
|
89
|
+
|
90
|
+
def draw_title
|
91
|
+
return unless @title
|
92
|
+
|
93
|
+
@font_height = calculate_caps_height(scale_fontsize(@title_font_size))
|
94
|
+
@title_width = calculate_width(@title_font_size, @title)
|
95
|
+
|
96
|
+
@d.fill = @font_color
|
97
|
+
@d.font = @font if @font
|
98
|
+
@d.stroke('transparent')
|
99
|
+
@d.font_weight = NormalWeight
|
100
|
+
@d.pointsize = scale_fontsize(@title_font_size)
|
101
|
+
@d.gravity = NorthWestGravity
|
102
|
+
@d = @d.annotate_scaled(*[
|
103
|
+
@base_image,
|
104
|
+
1.0, 1.0,
|
105
|
+
@font_height/2, @font_height/2,
|
106
|
+
@title,
|
107
|
+
@scale
|
108
|
+
])
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
##
|
3
|
+
# A mixin for methods that need to be deleted or have been
|
4
|
+
# replaced by cleaner code.
|
5
|
+
|
6
|
+
module Gruffy
|
7
|
+
module Deprecated
|
8
|
+
|
9
|
+
def scale_measurements
|
10
|
+
setup_graph_measurements
|
11
|
+
end
|
12
|
+
|
13
|
+
def total_height
|
14
|
+
@rows + 10
|
15
|
+
end
|
16
|
+
|
17
|
+
def graph_top
|
18
|
+
@graph_top * @scale
|
19
|
+
end
|
20
|
+
|
21
|
+
def graph_height
|
22
|
+
@graph_height * @scale
|
23
|
+
end
|
24
|
+
|
25
|
+
def graph_left
|
26
|
+
@graph_left * @scale
|
27
|
+
end
|
28
|
+
|
29
|
+
def graph_width
|
30
|
+
@graph_width * @scale
|
31
|
+
end
|
32
|
+
|
33
|
+
# TODO Should be calculate_graph_height
|
34
|
+
# def setup_graph_height
|
35
|
+
# @graph_height = @graph_bottom - @graph_top
|
36
|
+
# end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
data/lib/gruffy/dot.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
##
|
4
|
+
# Graph with dots and labels along a vertical access
|
5
|
+
# see: 'Creating More Effective Graphs' by Robbins
|
6
|
+
|
7
|
+
class Gruffy::Dot < Gruffy::Base
|
8
|
+
|
9
|
+
def draw
|
10
|
+
@has_left_labels = true
|
11
|
+
super
|
12
|
+
|
13
|
+
return unless @has_data
|
14
|
+
|
15
|
+
# Setup spacing.
|
16
|
+
#
|
17
|
+
spacing_factor = 1.0
|
18
|
+
|
19
|
+
@items_width = @graph_height / @column_count.to_f
|
20
|
+
@item_width = @items_width * spacing_factor / @norm_data.size
|
21
|
+
@d = @d.stroke_opacity 0.0
|
22
|
+
padding = (@items_width * (1 - spacing_factor)) / 2
|
23
|
+
|
24
|
+
@norm_data.each_with_index do |data_row, row_index|
|
25
|
+
data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index|
|
26
|
+
x_pos = @graph_left + (data_point * @graph_width)
|
27
|
+
y_pos = @graph_top + (@items_width * point_index) + padding + (@items_width.to_f/2.0).round
|
28
|
+
|
29
|
+
if row_index == 0
|
30
|
+
@d = @d.stroke(@marker_color)
|
31
|
+
@d = @d.fill(@marker_color)
|
32
|
+
@d = @d.stroke_width 1.0
|
33
|
+
@d = @d.stroke_opacity 0.1
|
34
|
+
@d = @d.fill_opacity 0.1
|
35
|
+
@d = @d.line(@graph_left, y_pos, @graph_left + @graph_width, y_pos)
|
36
|
+
@d = @d.fill_opacity 1
|
37
|
+
end
|
38
|
+
|
39
|
+
@d = @d.fill data_row[DATA_COLOR_INDEX]
|
40
|
+
@d = @d.stroke('transparent')
|
41
|
+
@d = @d.circle(x_pos, y_pos, x_pos + (@item_width.to_f/3.0).round, y_pos)
|
42
|
+
|
43
|
+
draw_label(y_pos, point_index)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
@d.draw(@base_image)
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
# Instead of base class version, draws vertical background lines and label
|
54
|
+
def draw_line_markers
|
55
|
+
return if @hide_line_markers
|
56
|
+
|
57
|
+
@d = @d.stroke_antialias false
|
58
|
+
|
59
|
+
# Draw horizontal line markers and annotate with numbers
|
60
|
+
@d = @d.stroke(@marker_color)
|
61
|
+
@d = @d.stroke_width 1
|
62
|
+
if @y_axis_increment
|
63
|
+
increment = @y_axis_increment
|
64
|
+
number_of_lines = (@spread / @y_axis_increment).to_i
|
65
|
+
else
|
66
|
+
# Try to use a number of horizontal lines that will come out even.
|
67
|
+
#
|
68
|
+
# TODO Do the same for larger numbers...100, 75, 50, 25
|
69
|
+
if @marker_count.nil?
|
70
|
+
(3..7).each do |lines|
|
71
|
+
if @spread % lines == 0.0
|
72
|
+
@marker_count = lines
|
73
|
+
break
|
74
|
+
end
|
75
|
+
end
|
76
|
+
@marker_count ||= 5
|
77
|
+
end
|
78
|
+
# TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
|
79
|
+
@increment = (@spread > 0 && @marker_count > 0) ? significant(@spread / @marker_count) : 1
|
80
|
+
|
81
|
+
number_of_lines = @marker_count
|
82
|
+
increment = @increment
|
83
|
+
end
|
84
|
+
|
85
|
+
(0..number_of_lines).each do |index|
|
86
|
+
marker_label = @minimum_value + index * increment
|
87
|
+
x = @graph_left + (marker_label - @minimum_value) * @graph_width / @spread
|
88
|
+
@d = @d.line(x, @graph_bottom, x, @graph_bottom + 0.5 * LABEL_MARGIN)
|
89
|
+
|
90
|
+
unless @hide_line_numbers
|
91
|
+
@d.fill = @font_color
|
92
|
+
@d.font = @font if @font
|
93
|
+
@d.stroke = 'transparent'
|
94
|
+
@d.pointsize = scale_fontsize(@marker_font_size)
|
95
|
+
@d.gravity = CenterGravity
|
96
|
+
# TODO Center text over line
|
97
|
+
@d = @d.annotate_scaled(@base_image,
|
98
|
+
0, 0, # Width of box to draw text in
|
99
|
+
x, @graph_bottom + (LABEL_MARGIN * 2.0), # Coordinates of text
|
100
|
+
label(marker_label, increment), @scale)
|
101
|
+
end # unless
|
102
|
+
@d = @d.stroke_antialias true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Draw on the Y axis instead of the X
|
108
|
+
|
109
|
+
def draw_label(y_offset, index)
|
110
|
+
if !@labels[index].nil? && @labels_seen[index].nil?
|
111
|
+
@d.fill = @font_color
|
112
|
+
@d.font = @font if @font
|
113
|
+
@d.stroke = 'transparent'
|
114
|
+
@d.font_weight = NormalWeight
|
115
|
+
@d.pointsize = scale_fontsize(@marker_font_size)
|
116
|
+
@d.gravity = EastGravity
|
117
|
+
@d = @d.annotate_scaled(@base_image,
|
118
|
+
1, 1,
|
119
|
+
-@graph_left + LABEL_MARGIN * 2.0, y_offset,
|
120
|
+
@labels[index], @scale)
|
121
|
+
@labels_seen[index] = 1
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
data/lib/gruffy/line.rb
ADDED
@@ -0,0 +1,365 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
##
|
4
|
+
# Here's how to make a Line graph:
|
5
|
+
#
|
6
|
+
# g = Gruffy::Line.new
|
7
|
+
# g.title = "A Line Graph"
|
8
|
+
# g.data 'Fries', [20, 23, 19, 8]
|
9
|
+
# g.data 'Hamburgers', [50, 19, 99, 29]
|
10
|
+
# g.write("test/output/line.png")
|
11
|
+
#
|
12
|
+
# There are also other options described below, such as #baseline_value, #baseline_color, #hide_dots, and #hide_lines.
|
13
|
+
|
14
|
+
class Gruffy::Line < Gruffy::Base
|
15
|
+
|
16
|
+
# Allow for reference lines ( which are like baseline ... just allowing for more & on both axes )
|
17
|
+
attr_accessor :reference_lines
|
18
|
+
attr_accessor :reference_line_default_color
|
19
|
+
attr_accessor :reference_line_default_width
|
20
|
+
|
21
|
+
# Allow for vertical marker lines
|
22
|
+
attr_accessor :show_vertical_markers
|
23
|
+
|
24
|
+
# Dimensions of lines and dots; calculated based on dataset size if left unspecified
|
25
|
+
attr_accessor :line_width
|
26
|
+
attr_accessor :dot_radius
|
27
|
+
|
28
|
+
# default is a circle, other options include square
|
29
|
+
attr_accessor :dot_style
|
30
|
+
|
31
|
+
# Hide parts of the graph to fit more datapoints, or for a different appearance.
|
32
|
+
attr_accessor :hide_dots, :hide_lines
|
33
|
+
|
34
|
+
#accessors for support of xy data
|
35
|
+
attr_accessor :minimum_x_value
|
36
|
+
attr_accessor :maximum_x_value
|
37
|
+
|
38
|
+
# Get the value if somebody has defined it.
|
39
|
+
def baseline_value
|
40
|
+
if (@reference_lines.key?(:baseline))
|
41
|
+
@reference_lines[:baseline][:value]
|
42
|
+
else
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set a value for a baseline reference line..
|
48
|
+
def baseline_value=(new_value)
|
49
|
+
@reference_lines[:baseline] ||= Hash.new
|
50
|
+
@reference_lines[:baseline][:value] = new_value
|
51
|
+
end
|
52
|
+
|
53
|
+
def baseline_color
|
54
|
+
if (@reference_lines.key?(:baseline))
|
55
|
+
@reference_lines[:baseline][:color]
|
56
|
+
else
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def baseline_color=(new_value)
|
62
|
+
@reference_lines[:baseline] ||= Hash.new
|
63
|
+
@reference_lines[:baseline][:color] = new_value
|
64
|
+
end
|
65
|
+
|
66
|
+
# Call with target pixel width of graph (800, 400, 300), and/or 'false' to omit lines (points only).
|
67
|
+
#
|
68
|
+
# g = Gruffy::Line.new(400) # 400px wide with lines
|
69
|
+
#
|
70
|
+
# g = Gruffy::Line.new(400, false) # 400px wide, no lines (for backwards compatibility)
|
71
|
+
#
|
72
|
+
# g = Gruffy::Line.new(false) # Defaults to 800px wide, no lines (for backwards compatibility)
|
73
|
+
#
|
74
|
+
# The preferred way is to call hide_dots or hide_lines instead.
|
75
|
+
def initialize(*args)
|
76
|
+
raise ArgumentError, 'Wrong number of arguments' if args.length > 2
|
77
|
+
if args.empty? || ((not Numeric === args.first) && (not String === args.first))
|
78
|
+
super()
|
79
|
+
else
|
80
|
+
super args.shift
|
81
|
+
end
|
82
|
+
|
83
|
+
@reference_lines = Hash.new
|
84
|
+
@reference_line_default_color = 'red'
|
85
|
+
@reference_line_default_width = 5
|
86
|
+
|
87
|
+
@hide_dots = @hide_lines = false
|
88
|
+
@maximum_x_value = nil
|
89
|
+
@minimum_x_value = nil
|
90
|
+
|
91
|
+
@dot_style = 'circle'
|
92
|
+
|
93
|
+
@show_vertical_markers = false
|
94
|
+
end
|
95
|
+
|
96
|
+
# This method allows one to plot a dataset with both X and Y data.
|
97
|
+
#
|
98
|
+
# Parameters are as follows:
|
99
|
+
# name: string, the title of the dataset
|
100
|
+
# x_data_points: an array containing the x data points for the graph
|
101
|
+
# y_data_points: an array containing the y data points for the graph
|
102
|
+
# color: hex number indicating the line color as an RGB triplet
|
103
|
+
#
|
104
|
+
# or
|
105
|
+
#
|
106
|
+
# name: string, the title of the dataset
|
107
|
+
# xy_data_points: an array containing both x and y data points for the graph
|
108
|
+
# color: hex number indicating the line color as an RGB triplet
|
109
|
+
#
|
110
|
+
# Notes:
|
111
|
+
# -if (x_data_points.length != y_data_points.length) an error is
|
112
|
+
# returned.
|
113
|
+
# -if the color argument is nil, the next color from the default theme will
|
114
|
+
# be used.
|
115
|
+
# -if you want to use a preset theme, you must set it before calling
|
116
|
+
# dataxy().
|
117
|
+
#
|
118
|
+
# Example:
|
119
|
+
# g = Gruffy::Line.new
|
120
|
+
# g.title = "X/Y Dataset"
|
121
|
+
# g.dataxy("Apples", [1,3,4,5,6,10], [1, 2, 3, 4, 4, 3])
|
122
|
+
# g.dataxy("Bapples", [1,3,4,5,7,9], [1, 1, 2, 2, 3, 3])
|
123
|
+
# g.dataxy("Capples", [[1,1],[2,3],[3,4],[4,5],[5,7],[6,9]])
|
124
|
+
# #you can still use the old data method too if you want:
|
125
|
+
# g.data("Capples", [1, 1, 2, 2, 3, 3])
|
126
|
+
# #labels will be drawn at the x locations of the keys passed in.
|
127
|
+
# In this example the lables are drawn at x positions 2, 4, and 6:
|
128
|
+
# g.labels = {0 => '2003', 2 => '2004', 4 => '2005', 6 => '2006'}
|
129
|
+
# The 0 => '2003' label will be ignored since it is outside the chart range.
|
130
|
+
def dataxy(name, x_data_points=[], y_data_points=[], color=nil)
|
131
|
+
raise ArgumentError, 'x_data_points is nil!' if x_data_points.length == 0
|
132
|
+
|
133
|
+
if x_data_points.all? { |p| p.is_a?(Array) && p.size == 2 }
|
134
|
+
x_data_points, y_data_points = x_data_points.map { |p| p[0] }, x_data_points.map { |p| p[1] }
|
135
|
+
end
|
136
|
+
|
137
|
+
raise ArgumentError, 'x_data_points.length != y_data_points.length!' if x_data_points.length != y_data_points.length
|
138
|
+
|
139
|
+
# call the existing data routine for the y data.
|
140
|
+
self.data(name, y_data_points, color)
|
141
|
+
|
142
|
+
x_data_points = Array(x_data_points) # make sure it's an array
|
143
|
+
# append the x data to the last entry that was just added in the @data member
|
144
|
+
@data.last[DATA_VALUES_X_INDEX] = x_data_points
|
145
|
+
|
146
|
+
# Update the global min/max values for the x data
|
147
|
+
x_data_points.each do |x_data_point|
|
148
|
+
next if x_data_point.nil?
|
149
|
+
|
150
|
+
# Setup max/min so spread starts at the low end of the data points
|
151
|
+
if @maximum_x_value.nil? && @minimum_x_value.nil?
|
152
|
+
@maximum_x_value = @minimum_x_value = x_data_point
|
153
|
+
end
|
154
|
+
|
155
|
+
@maximum_x_value = (x_data_point > @maximum_x_value) ?
|
156
|
+
x_data_point : @maximum_x_value
|
157
|
+
@minimum_x_value = (x_data_point < @minimum_x_value) ?
|
158
|
+
x_data_point : @minimum_x_value
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
def draw_reference_line(reference_line, left, right, top, bottom)
|
164
|
+
@d = @d.push
|
165
|
+
@d.stroke_color(reference_line[:color] || @reference_line_default_color)
|
166
|
+
@d.fill_opacity 0.0
|
167
|
+
@d.stroke_dasharray(10, 20)
|
168
|
+
@d.stroke_width(reference_line[:width] || @reference_line_default_width)
|
169
|
+
@d.line(left, top, right, bottom)
|
170
|
+
@d = @d.pop
|
171
|
+
end
|
172
|
+
|
173
|
+
def draw_horizontal_reference_line(reference_line)
|
174
|
+
level = @graph_top + (@graph_height - reference_line[:norm_value] * @graph_height)
|
175
|
+
draw_reference_line(reference_line, @graph_left, @graph_left + @graph_width, level, level)
|
176
|
+
end
|
177
|
+
|
178
|
+
def draw_vertical_reference_line(reference_line)
|
179
|
+
index = @graph_left + (@x_increment * reference_line[:index])
|
180
|
+
draw_reference_line(reference_line, index, index, @graph_top, @graph_top + @graph_height)
|
181
|
+
end
|
182
|
+
|
183
|
+
def draw
|
184
|
+
super
|
185
|
+
|
186
|
+
return unless @has_data
|
187
|
+
|
188
|
+
# Check to see if more than one datapoint was given. NaN can result otherwise.
|
189
|
+
@x_increment = (@column_count > 1) ? (@graph_width / (@column_count - 1).to_f) : @graph_width
|
190
|
+
|
191
|
+
@reference_lines.each_value do |curr_reference_line|
|
192
|
+
draw_horizontal_reference_line(curr_reference_line) if curr_reference_line.key?(:norm_value)
|
193
|
+
draw_vertical_reference_line(curr_reference_line) if curr_reference_line.key?(:index)
|
194
|
+
end
|
195
|
+
|
196
|
+
if (@show_vertical_markers)
|
197
|
+
(0..@column_count).each do |column|
|
198
|
+
x = @graph_left + @graph_width - column.to_f * @x_increment
|
199
|
+
|
200
|
+
@d = @d.fill(@marker_color)
|
201
|
+
|
202
|
+
# FIXME(uwe): Workaround for Issue #66
|
203
|
+
# https://github.com/topfunky/gruffy/issues/66
|
204
|
+
# https://github.com/rmagick/rmagick/issues/82
|
205
|
+
# Remove if the issue gets fixed.
|
206
|
+
x += 0.001 unless defined?(JRUBY_VERSION)
|
207
|
+
# EMXIF
|
208
|
+
|
209
|
+
@d = @d.line(x, @graph_bottom, x, @graph_top)
|
210
|
+
#If the user specified a marker shadow color, draw a shadow just below it
|
211
|
+
unless @marker_shadow_color.nil?
|
212
|
+
@d = @d.fill(@marker_shadow_color)
|
213
|
+
@d = @d.line(x + 1, @graph_bottom, x + 1, @graph_top)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
@norm_data.each do |data_row|
|
219
|
+
prev_x = prev_y = nil
|
220
|
+
|
221
|
+
@one_point = contains_one_point_only?(data_row)
|
222
|
+
|
223
|
+
data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index|
|
224
|
+
x_data = data_row[DATA_VALUES_X_INDEX]
|
225
|
+
if x_data == nil
|
226
|
+
#use the old method: equally spaced points along the x-axis
|
227
|
+
new_x = @graph_left + (@x_increment * index)
|
228
|
+
draw_label(new_x, index)
|
229
|
+
else
|
230
|
+
new_x = get_x_coord(x_data[index], @graph_width, @graph_left)
|
231
|
+
@labels.each do |label_pos, _|
|
232
|
+
draw_label(@graph_left + ((label_pos - @minimum_x_value) * @graph_width) / (@maximum_x_value - @minimum_x_value), label_pos)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
unless data_point # we can't draw a line for a null data point, we can still label the axis though
|
236
|
+
prev_x = prev_y = nil
|
237
|
+
next
|
238
|
+
end
|
239
|
+
|
240
|
+
new_y = @graph_top + (@graph_height - data_point * @graph_height)
|
241
|
+
|
242
|
+
# Reset each time to avoid thin-line errors
|
243
|
+
@d = @d.stroke data_row[DATA_COLOR_INDEX]
|
244
|
+
@d = @d.fill data_row[DATA_COLOR_INDEX]
|
245
|
+
@d = @d.stroke_opacity 1.0
|
246
|
+
@d = @d.stroke_width line_width ||
|
247
|
+
clip_value_if_greater_than(@columns / (@norm_data.first[DATA_VALUES_INDEX].size * 4), 5.0)
|
248
|
+
|
249
|
+
circle_radius = dot_radius ||
|
250
|
+
clip_value_if_greater_than(@columns / (@norm_data.first[DATA_VALUES_INDEX].size * 2.5), 5.0)
|
251
|
+
|
252
|
+
if !@hide_lines && !prev_x.nil? && !prev_y.nil?
|
253
|
+
@d = @d.line(prev_x, prev_y, new_x, new_y)
|
254
|
+
elsif @one_point
|
255
|
+
# Show a circle if there's just one_point
|
256
|
+
@d = DotRenderers.renderer(@dot_style).render(@d, new_x, new_y, circle_radius)
|
257
|
+
end
|
258
|
+
|
259
|
+
unless @hide_dots
|
260
|
+
@d = DotRenderers.renderer(@dot_style).render(@d, new_x, new_y, circle_radius)
|
261
|
+
end
|
262
|
+
|
263
|
+
prev_x, prev_y = new_x, new_y
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
@d.draw(@base_image)
|
268
|
+
end
|
269
|
+
|
270
|
+
def setup_data
|
271
|
+
|
272
|
+
# Deal with horizontal reference line values that exceed the existing minimum & maximum values.
|
273
|
+
possible_maximums = [@maximum_value.to_f]
|
274
|
+
possible_minimums = [@minimum_value.to_f]
|
275
|
+
|
276
|
+
@reference_lines.each_value do |curr_reference_line|
|
277
|
+
if (curr_reference_line.key?(:value))
|
278
|
+
possible_maximums << curr_reference_line[:value].to_f
|
279
|
+
possible_minimums << curr_reference_line[:value].to_f
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
@maximum_value = possible_maximums.max
|
284
|
+
@minimum_value = possible_minimums.min
|
285
|
+
|
286
|
+
super
|
287
|
+
end
|
288
|
+
|
289
|
+
def normalize(force=false)
|
290
|
+
super(force)
|
291
|
+
|
292
|
+
@reference_lines.each_value do |curr_reference_line|
|
293
|
+
|
294
|
+
# We only care about horizontal markers ... for normalization.
|
295
|
+
# Vertical markers won't have a :value, they will have an :index
|
296
|
+
|
297
|
+
curr_reference_line[:norm_value] = ((curr_reference_line[:value].to_f - @minimum_value) / @spread.to_f) if (curr_reference_line.key?(:value))
|
298
|
+
|
299
|
+
end
|
300
|
+
|
301
|
+
#normalize the x data if it is specified
|
302
|
+
@data.each_with_index do |data_row, index|
|
303
|
+
norm_x_data_points = []
|
304
|
+
if data_row[DATA_VALUES_X_INDEX] != nil
|
305
|
+
data_row[DATA_VALUES_X_INDEX].each do |x_data_point|
|
306
|
+
norm_x_data_points << ((x_data_point.to_f - @minimum_x_value.to_f) /
|
307
|
+
(@maximum_x_value.to_f - @minimum_x_value.to_f))
|
308
|
+
end
|
309
|
+
@norm_data[index] << norm_x_data_points
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
end
|
314
|
+
|
315
|
+
def sort_norm_data
|
316
|
+
super unless @data.any? { |d| d[DATA_VALUES_X_INDEX] }
|
317
|
+
end
|
318
|
+
|
319
|
+
def get_x_coord(x_data_point, width, offset)
|
320
|
+
x_data_point * width + offset
|
321
|
+
end
|
322
|
+
|
323
|
+
def contains_one_point_only?(data_row)
|
324
|
+
# Spin through data to determine if there is just one_value present.
|
325
|
+
one_point = false
|
326
|
+
data_row[DATA_VALUES_INDEX].each do |data_point|
|
327
|
+
unless data_point.nil?
|
328
|
+
if one_point
|
329
|
+
# more than one point, bail
|
330
|
+
return false
|
331
|
+
end
|
332
|
+
# there is at least one data point
|
333
|
+
one_point = true
|
334
|
+
end
|
335
|
+
end
|
336
|
+
one_point
|
337
|
+
end
|
338
|
+
|
339
|
+
module DotRenderers
|
340
|
+
class Circle
|
341
|
+
def render(d, new_x, new_y, circle_radius)
|
342
|
+
d.circle(new_x, new_y, new_x - circle_radius, new_y)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
class Square
|
347
|
+
def render(d, new_x, new_y, circle_radius)
|
348
|
+
offset = (circle_radius * 0.8).to_i
|
349
|
+
corner_1 = new_x - offset
|
350
|
+
corner_2 = new_y - offset
|
351
|
+
corner_3 = new_x + offset
|
352
|
+
corner_4 = new_y + offset
|
353
|
+
d.rectangle(corner_1, corner_2, corner_3, corner_4)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
def self.renderer(style)
|
358
|
+
if style.to_s == 'square'
|
359
|
+
Square.new
|
360
|
+
else
|
361
|
+
Circle.new
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|