schapht-gruff 0.3.5

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.
Files changed (80) hide show
  1. data/History.txt +111 -0
  2. data/MIT-LICENSE +21 -0
  3. data/Manifest.txt +79 -0
  4. data/README.txt +40 -0
  5. data/Rakefile +55 -0
  6. data/assets/bubble.png +0 -0
  7. data/assets/city_scene/background/0000.png +0 -0
  8. data/assets/city_scene/background/0600.png +0 -0
  9. data/assets/city_scene/background/2000.png +0 -0
  10. data/assets/city_scene/clouds/cloudy.png +0 -0
  11. data/assets/city_scene/clouds/partly_cloudy.png +0 -0
  12. data/assets/city_scene/clouds/stormy.png +0 -0
  13. data/assets/city_scene/grass/default.png +0 -0
  14. data/assets/city_scene/haze/true.png +0 -0
  15. data/assets/city_scene/number_sample/1.png +0 -0
  16. data/assets/city_scene/number_sample/2.png +0 -0
  17. data/assets/city_scene/number_sample/default.png +0 -0
  18. data/assets/city_scene/sky/0000.png +0 -0
  19. data/assets/city_scene/sky/0200.png +0 -0
  20. data/assets/city_scene/sky/0400.png +0 -0
  21. data/assets/city_scene/sky/0600.png +0 -0
  22. data/assets/city_scene/sky/0800.png +0 -0
  23. data/assets/city_scene/sky/1000.png +0 -0
  24. data/assets/city_scene/sky/1200.png +0 -0
  25. data/assets/city_scene/sky/1400.png +0 -0
  26. data/assets/city_scene/sky/1500.png +0 -0
  27. data/assets/city_scene/sky/1700.png +0 -0
  28. data/assets/city_scene/sky/2000.png +0 -0
  29. data/assets/pc306715.jpg +0 -0
  30. data/assets/plastik/blue.png +0 -0
  31. data/assets/plastik/green.png +0 -0
  32. data/assets/plastik/red.png +0 -0
  33. data/init.rb +2 -0
  34. data/lib/gruff.rb +27 -0
  35. data/lib/gruff/accumulator_bar.rb +27 -0
  36. data/lib/gruff/area.rb +58 -0
  37. data/lib/gruff/bar.rb +84 -0
  38. data/lib/gruff/bar_conversion.rb +46 -0
  39. data/lib/gruff/base.rb +1112 -0
  40. data/lib/gruff/bullet.rb +109 -0
  41. data/lib/gruff/deprecated.rb +39 -0
  42. data/lib/gruff/line.rb +105 -0
  43. data/lib/gruff/mini/bar.rb +32 -0
  44. data/lib/gruff/mini/legend.rb +77 -0
  45. data/lib/gruff/mini/pie.rb +36 -0
  46. data/lib/gruff/mini/side_bar.rb +35 -0
  47. data/lib/gruff/net.rb +142 -0
  48. data/lib/gruff/photo_bar.rb +100 -0
  49. data/lib/gruff/pie.rb +124 -0
  50. data/lib/gruff/scene.rb +209 -0
  51. data/lib/gruff/side_bar.rb +115 -0
  52. data/lib/gruff/side_stacked_bar.rb +74 -0
  53. data/lib/gruff/spider.rb +130 -0
  54. data/lib/gruff/stacked_area.rb +67 -0
  55. data/lib/gruff/stacked_bar.rb +54 -0
  56. data/lib/gruff/stacked_mixin.rb +23 -0
  57. data/rails_generators/gruff/gruff_generator.rb +63 -0
  58. data/rails_generators/gruff/templates/controller.rb +32 -0
  59. data/rails_generators/gruff/templates/functional_test.rb +24 -0
  60. data/test/gruff_test_case.rb +123 -0
  61. data/test/test_accumulator_bar.rb +50 -0
  62. data/test/test_area.rb +134 -0
  63. data/test/test_bar.rb +283 -0
  64. data/test/test_base.rb +8 -0
  65. data/test/test_bullet.rb +26 -0
  66. data/test/test_legend.rb +68 -0
  67. data/test/test_line.rb +513 -0
  68. data/test/test_mini_bar.rb +32 -0
  69. data/test/test_mini_pie.rb +20 -0
  70. data/test/test_mini_side_bar.rb +37 -0
  71. data/test/test_net.rb +230 -0
  72. data/test/test_photo.rb +41 -0
  73. data/test/test_pie.rb +154 -0
  74. data/test/test_scene.rb +100 -0
  75. data/test/test_side_bar.rb +12 -0
  76. data/test/test_sidestacked_bar.rb +89 -0
  77. data/test/test_spider.rb +216 -0
  78. data/test/test_stacked_area.rb +52 -0
  79. data/test/test_stacked_bar.rb +52 -0
  80. metadata +160 -0
@@ -0,0 +1,100 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ # EXPERIMENTAL!
4
+ #
5
+ # Doesn't work yet.
6
+ #
7
+ class Gruff::PhotoBar < Gruff::Base
8
+
9
+ # TODO
10
+ #
11
+ # define base and cap in yml
12
+ # allow for image directory to be located elsewhere
13
+ # more exact measurements for bar heights (go all the way to the bottom of the graph)
14
+ # option to tile images instead of use a single image
15
+ # drop base label a few px lower so photo bar graphs can have a base dropping over the lower marker line
16
+ #
17
+
18
+ # The name of a pre-packaged photo-based theme.
19
+ attr_reader :theme
20
+
21
+ # def initialize(target_width=800)
22
+ # super
23
+ # init_photo_bar_graphics()
24
+ # end
25
+
26
+ def draw
27
+ super
28
+ return unless @has_data
29
+
30
+ return # TODO Remove for further development
31
+
32
+ init_photo_bar_graphics()
33
+
34
+ #Draw#define_clip_path()
35
+ #Draw#clip_path(pathname)
36
+ #Draw#composite....with bar graph image OverCompositeOp
37
+ #
38
+ # See also
39
+ #
40
+ # Draw.pattern # define an image to tile as the filling of a draw object
41
+ #
42
+
43
+ # Setup spacing.
44
+ #
45
+ # Columns sit side-by-side.
46
+ spacing_factor = 0.9
47
+ @bar_width = @norm_data[0][DATA_COLOR_INDEX].columns
48
+
49
+ @norm_data.each_with_index do |data_row, row_index|
50
+
51
+ data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index|
52
+ data_point = 0 if data_point.nil?
53
+ # Use incremented x and scaled y
54
+ left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index)))
55
+ left_y = @graph_top + (@graph_height - data_point * @graph_height) + 1
56
+ right_x = left_x + @bar_width * spacing_factor
57
+ right_y = @graph_top + @graph_height - 1
58
+
59
+ bar_image_width = data_row[DATA_COLOR_INDEX].columns
60
+ bar_image_height = right_y.to_f - left_y.to_f
61
+
62
+ # Crop to scale for data
63
+ bar_image = data_row[DATA_COLOR_INDEX].crop(0, 0, bar_image_width, bar_image_height)
64
+
65
+ @d.gravity = NorthWestGravity
66
+ @d = @d.composite(left_x, left_y, bar_image_width, bar_image_height, bar_image)
67
+
68
+ # Calculate center based on bar_width and current row
69
+ label_center = @graph_left + (@data.length * @bar_width * point_index) + (@data.length * @bar_width / 2.0)
70
+ draw_label(label_center, point_index)
71
+ end
72
+
73
+ end
74
+
75
+ @d.draw(@base_image)
76
+ end
77
+
78
+
79
+ # Return the chosen theme or the default
80
+ def theme
81
+ @theme || 'plastik'
82
+ end
83
+
84
+ protected
85
+
86
+ # Sets up colors with a list of images that will be used.
87
+ # Images should be 340px tall
88
+ def init_photo_bar_graphics
89
+ color_list = Array.new
90
+ theme_dir = File.dirname(__FILE__) + '/../../assets/' + theme
91
+
92
+ Dir.open(theme_dir).each do |file|
93
+ next unless /\.png$/.match(file)
94
+ color_list << Image.read("#{theme_dir}/#{file}").first
95
+ end
96
+ @colors = color_list
97
+ end
98
+
99
+ end
100
+
data/lib/gruff/pie.rb ADDED
@@ -0,0 +1,124 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ ##
4
+ # Here's how to make a Pie graph:
5
+ #
6
+ # g = Gruff::Pie.new
7
+ # g.title = "Visual Pie Graph Test"
8
+ # g.data 'Fries', 20
9
+ # g.data 'Hamburgers', 50
10
+ # g.write("test/output/pie_keynote.png")
11
+ #
12
+ # To control where the pie chart starts creating slices, use #zero_degree.
13
+
14
+ class Gruff::Pie < Gruff::Base
15
+
16
+ TEXT_OFFSET_PERCENTAGE = 0.15
17
+
18
+ # Can be used to make the pie start cutting slices at the top (-90.0)
19
+ # or at another angle. Default is 0.0, which starts at 3 o'clock.
20
+ attr_accessor :zero_degree
21
+
22
+ def initialize_ivars
23
+ super
24
+ @zero_degree = 0.0
25
+ end
26
+
27
+ def draw
28
+ @hide_line_markers = true
29
+
30
+ super
31
+
32
+ return unless @has_data
33
+
34
+ diameter = @graph_height
35
+ radius = ([@graph_width, @graph_height].min / 2.0) * 0.8
36
+ top_x = @graph_left + (@graph_width - diameter) / 2.0
37
+ center_x = @graph_left + (@graph_width / 2.0)
38
+ center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
39
+ total_sum = sums_for_pie()
40
+ prev_degrees = @zero_degree
41
+
42
+ # Use full data since we can easily calculate percentages
43
+ data = (@sort ? @data.sort{ |a, b| a[DATA_VALUES_INDEX].first <=> b[DATA_VALUES_INDEX].first } : @data)
44
+ data.each do |data_row|
45
+ if data_row[DATA_VALUES_INDEX].first > 0
46
+ @d = @d.stroke data_row[DATA_COLOR_INDEX]
47
+ @d = @d.fill 'transparent'
48
+ @d.stroke_width(radius) # stroke width should be equal to radius. we'll draw centered on (radius / 2)
49
+
50
+ current_degrees = (data_row[DATA_VALUES_INDEX].first / total_sum) * 360.0
51
+
52
+ # ellipse will draw the the stroke centered on the first two parameters offset by the second two.
53
+ # therefore, in order to draw a circle of the proper diameter we must center the stroke at
54
+ # half the radius for both x and y
55
+ @d = @d.ellipse(center_x, center_y,
56
+ radius / 2.0, radius / 2.0,
57
+ prev_degrees, prev_degrees + current_degrees + 0.5) # <= +0.5 'fudge factor' gets rid of the ugly gaps
58
+
59
+ half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2
60
+
61
+ # Following line is commented to allow display of the percentiles
62
+ # bug appeared between r90 and r92
63
+ # unless @hide_line_markers then
64
+ # End the string with %% to escape the single %.
65
+ # RMagick must use sprintf with the string and % has special significance.
66
+ label_string = ((data_row[DATA_VALUES_INDEX].first / total_sum) *
67
+ 100.0).round.to_s + '%%'
68
+ @d = draw_label(center_x,center_y, half_angle,
69
+ radius + (radius * TEXT_OFFSET_PERCENTAGE),
70
+ label_string)
71
+ # end
72
+
73
+ prev_degrees += current_degrees
74
+ end
75
+ end
76
+
77
+ # TODO debug a circle where the text is drawn...
78
+
79
+ @d.draw(@base_image)
80
+ end
81
+
82
+ private
83
+
84
+ ##
85
+ # Labels are drawn around a slightly wider ellipse to give room for
86
+ # labels on the left and right.
87
+ def draw_label(center_x, center_y, angle, radius, amount)
88
+ # TODO Don't use so many hard-coded numbers
89
+ r_offset = 20.0 # The distance out from the center of the pie to get point
90
+ x_offset = center_x # + 15.0 # The label points need to be tweaked slightly
91
+ y_offset = center_y # This one doesn't though
92
+ radius_offset = (radius + r_offset)
93
+ ellipse_factor = radius_offset * 0.15
94
+ x = x_offset + ((radius_offset + ellipse_factor) * Math.cos(angle.deg2rad))
95
+ y = y_offset + (radius_offset * Math.sin(angle.deg2rad))
96
+
97
+ # Draw label
98
+ @d.fill = @font_color
99
+ @d.font = @font if @font
100
+ @d.pointsize = scale_fontsize(@marker_font_size)
101
+ @d.stroke = 'transparent'
102
+ @d.font_weight = BoldWeight
103
+ @d.gravity = CenterGravity
104
+ @d.annotate_scaled( @base_image,
105
+ 0, 0,
106
+ x, y,
107
+ amount, @scale)
108
+ end
109
+
110
+ def sums_for_pie
111
+ total_sum = 0.0
112
+ @data.collect {|data_row| total_sum += data_row[DATA_VALUES_INDEX].first }
113
+ total_sum
114
+ end
115
+
116
+ end
117
+
118
+ class Float
119
+ # Used for degree => radian conversions
120
+ def deg2rad
121
+ self * (Math::PI/180.0)
122
+ end
123
+ end
124
+
@@ -0,0 +1,209 @@
1
+
2
+ require "observer"
3
+ require File.dirname(__FILE__) + '/base'
4
+
5
+ ##
6
+ # A scene is a non-linear graph that assembles layers together to tell a story.
7
+ # Layers are folders with appropriately named files (see below). You can group
8
+ # layers and control them together or just set their values individually.
9
+ #
10
+ # Examples:
11
+ #
12
+ # * A city scene that changes with the time of day and the weather conditions.
13
+ # * A traffic map that shows red lines on streets that are crowded and green on free-flowing ones.
14
+ #
15
+ # Usage:
16
+ #
17
+ # g = Gruff::Scene.new("500x100", "path/to/city_scene_directory")
18
+ #
19
+ # # Define order of layers, back to front
20
+ # g.layers = %w(background haze sky clouds)
21
+ #
22
+ # # Define groups that will be controlled by the same input
23
+ # g.weather_group = %w(clouds)
24
+ # g.time_group = %w(background sky)
25
+ #
26
+ # # Set values for the layers or groups
27
+ # g.weather = "cloudy"
28
+ # g.time = Time.now
29
+ # g.haze = true
30
+ #
31
+ # # Write the final graph to disk
32
+ # g.write "hazy_daytime_city_scene.png"
33
+ #
34
+ #
35
+ # There are several rules that will magically select a layer when possible.
36
+ #
37
+ # * Numbered files will be selected according to the closest value that is less than the input value.
38
+ # * 'true.png' and 'false.png' will be used as booleans.
39
+ # * Other named files will be used if the input matches the filename (without the filetype extension).
40
+ # * If there is a file named 'default.png', it will be used unless other input values are set for the corresponding layer.
41
+
42
+ class Gruff::Scene < Gruff::Base
43
+
44
+ # An array listing the foldernames that will be rendered, from back to front.
45
+ #
46
+ # g.layers = %w(sky clouds buildings street people)
47
+ #
48
+ attr_reader :layers
49
+
50
+ def initialize(target_width, base_dir)
51
+ @base_dir = base_dir
52
+ @groups = {}
53
+ @layers = []
54
+ super target_width
55
+ end
56
+
57
+ def draw
58
+ # Join all the custom paths and filter out the empty ones
59
+ image_paths = @layers.map { |layer| layer.path }.select { |path| !path.empty? }
60
+ images = Magick::ImageList.new(*image_paths)
61
+ @base_image = images.flatten_images
62
+ end
63
+
64
+ def layers=(ordered_list)
65
+ ordered_list.each do |layer_name|
66
+ @layers << Gruff::Layer.new(@base_dir, layer_name)
67
+ end
68
+ end
69
+
70
+ # Group layers to input values
71
+ #
72
+ # g.weather_group = ["sky", "sea", "clouds"]
73
+ #
74
+ # Set input values
75
+ #
76
+ # g.weather = "cloudy"
77
+ #
78
+ def method_missing(method_name, *args)
79
+ case method_name.to_s
80
+ when /^(\w+)_group=$/
81
+ add_group $1, *args
82
+ return
83
+ when /^(\w+)=$/
84
+ set_input $1, args.first
85
+ return
86
+ end
87
+ super
88
+ end
89
+
90
+ private
91
+
92
+ def add_group(input_name, layer_names)
93
+ @groups[input_name] = Gruff::Group.new(input_name, @layers.select { |layer| layer_names.include?(layer.name) })
94
+ end
95
+
96
+ def set_input(input_name, input_value)
97
+ if not @groups[input_name].nil?
98
+ @groups[input_name].send_updates(input_value)
99
+ else
100
+ if chosen_layer = @layers.detect { |layer| layer.name == input_name }
101
+ chosen_layer.update input_value
102
+ end
103
+ end
104
+ end
105
+
106
+ end
107
+
108
+
109
+ class Gruff::Group
110
+
111
+ include Observable
112
+ attr_reader :name
113
+
114
+ def initialize(folder_name, layers)
115
+ @name = folder_name
116
+ layers.each do |layer|
117
+ layer.observe self
118
+ end
119
+ end
120
+
121
+ def send_updates(value)
122
+ changed
123
+ notify_observers value
124
+ end
125
+
126
+ end
127
+
128
+
129
+ class Gruff::Layer
130
+
131
+ attr_reader :name
132
+
133
+ def initialize(base_dir, folder_name)
134
+ @base_dir = base_dir.to_s
135
+ @name = folder_name.to_s
136
+ @filenames = Dir.open(File.join(base_dir, folder_name)).entries.select { |file| file =~ /^[^.]+\.png$/ }
137
+ @selected_filename = select_default
138
+ end
139
+
140
+ # Register this layer so it receives updates from the group
141
+ def observe(obj)
142
+ obj.add_observer self
143
+ end
144
+
145
+ # Choose the appropriate filename for this layer, based on the input
146
+ def update(value)
147
+ @selected_filename = case value.to_s
148
+ when /^(true|false)$/
149
+ select_boolean value
150
+ when /^(\w|\s)+$/
151
+ select_string value
152
+ when /^-?(\d+\.)?\d+$/
153
+ select_numeric value
154
+ when /(\d\d):(\d\d):\d\d/
155
+ select_time "#{$1}#{$2}"
156
+ else
157
+ select_default
158
+ end
159
+ # Finally, try to use 'default' if we're still blank
160
+ @selected_filename ||= select_default
161
+ end
162
+
163
+ # Returns the full path to the selected image, or a blank string
164
+ def path
165
+ unless @selected_filename.nil? || @selected_filename.empty?
166
+ return File.join(@base_dir, @name, @selected_filename)
167
+ end
168
+ ''
169
+ end
170
+
171
+ private
172
+
173
+ # Match "true.png" or "false.png"
174
+ def select_boolean(value)
175
+ file_exists_or_blank value.to_s
176
+ end
177
+
178
+ # Match -5 to _5.png
179
+ def select_numeric(value)
180
+ file_exists_or_blank value.to_s.gsub('-', '_')
181
+ end
182
+
183
+ def select_time(value)
184
+ times = @filenames.map { |filename| filename.gsub('.png', '') }
185
+ times.each_with_index do |time, index|
186
+ if (time > value) && (index > 0)
187
+ return "#{times[index - 1]}.png"
188
+ end
189
+ end
190
+ return "#{times.last}.png"
191
+ end
192
+
193
+ # Match "partly cloudy" to "partly_cloudy.png"
194
+ def select_string(value)
195
+ file_exists_or_blank value.to_s.gsub(' ', '_')
196
+ end
197
+
198
+ def select_default
199
+ @filenames.include?("default.png") ? "default.png" : ''
200
+ end
201
+
202
+ # Returns the string "#{filename}.png", if it exists.
203
+ #
204
+ # Failing that, it returns default.png, or '' if that doesn't exist.
205
+ def file_exists_or_blank(filename)
206
+ @filenames.include?("#{filename}.png") ? "#{filename}.png" : select_default
207
+ end
208
+
209
+ end
@@ -0,0 +1,115 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ ##
4
+ # Graph with individual horizontal bars instead of vertical bars.
5
+
6
+ class Gruff::SideBar < Gruff::Base
7
+
8
+ def draw
9
+ @has_left_labels = true
10
+ super
11
+
12
+ return unless @has_data
13
+
14
+ # Setup spacing.
15
+ #
16
+ spacing_factor = 0.9
17
+
18
+ @bars_width = @graph_height / @column_count.to_f
19
+ @bar_width = @bars_width * spacing_factor / @norm_data.size
20
+ @d = @d.stroke_opacity 0.0
21
+ height = Array.new(@column_count, 0)
22
+ length = Array.new(@column_count, @graph_left)
23
+ padding = (@bars_width * (1 - spacing_factor)) / 2
24
+
25
+ @norm_data.each_with_index do |data_row, row_index|
26
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
27
+
28
+ data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index|
29
+
30
+ # Using the original calcs from the stacked bar chart
31
+ # to get the difference between
32
+ # part of the bart chart we wish to stack.
33
+ temp1 = @graph_left + (@graph_width - data_point * @graph_width - height[point_index])
34
+ temp2 = @graph_left + @graph_width - height[point_index]
35
+ difference = temp2 - temp1
36
+
37
+ left_x = length[point_index] - 1
38
+ left_y = @graph_top + (@bars_width * point_index) + (@bar_width * row_index) + padding
39
+ right_x = left_x + difference
40
+ right_y = left_y + @bar_width
41
+
42
+ height[point_index] += (data_point * @graph_width)
43
+
44
+ @d = @d.rectangle(left_x, left_y, right_x, right_y)
45
+
46
+ # Calculate center based on bar_width and current row
47
+ label_center = @graph_top + (@bars_width * point_index + @bars_width / 2)
48
+ draw_label(label_center, point_index)
49
+ end
50
+
51
+ end
52
+
53
+ @d.draw(@base_image)
54
+ end
55
+
56
+ protected
57
+
58
+ # Instead of base class version, draws vertical background lines and label
59
+ def draw_line_markers
60
+
61
+ return if @hide_line_markers
62
+
63
+ @d = @d.stroke_antialias false
64
+
65
+ # Draw horizontal line markers and annotate with numbers
66
+ @d = @d.stroke(@marker_color)
67
+ @d = @d.stroke_width 1
68
+ number_of_lines = 5
69
+
70
+ # TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
71
+ increment = significant(@maximum_value.to_f / number_of_lines)
72
+ (0..number_of_lines).each do |index|
73
+
74
+ line_diff = (@graph_right - @graph_left) / number_of_lines
75
+ x = @graph_right - (line_diff * index) - 1
76
+ @d = @d.line(x, @graph_bottom, x, @graph_top)
77
+ diff = index - number_of_lines
78
+ marker_label = diff.abs * increment
79
+
80
+ unless @hide_line_numbers
81
+ @d.fill = @font_color
82
+ @d.font = @font if @font
83
+ @d.stroke = 'transparent'
84
+ @d.pointsize = scale_fontsize(@marker_font_size)
85
+ @d.gravity = CenterGravity
86
+ # TODO Center text over line
87
+ @d = @d.annotate_scaled( @base_image,
88
+ 0, 0, # Width of box to draw text in
89
+ x, @graph_bottom + (LABEL_MARGIN * 2.0), # Coordinates of text
90
+ marker_label.to_s, @scale)
91
+ end # unless
92
+ @d = @d.stroke_antialias true
93
+ end
94
+ end
95
+
96
+ ##
97
+ # Draw on the Y axis instead of the X
98
+
99
+ def draw_label(y_offset, index)
100
+ if !@labels[index].nil? && @labels_seen[index].nil?
101
+ @d.fill = @font_color
102
+ @d.font = @font if @font
103
+ @d.stroke = 'transparent'
104
+ @d.font_weight = NormalWeight
105
+ @d.pointsize = scale_fontsize(@marker_font_size)
106
+ @d.gravity = EastGravity
107
+ @d = @d.annotate_scaled(@base_image,
108
+ 1, 1,
109
+ -@graph_left + LABEL_MARGIN * 2.0, y_offset,
110
+ @labels[index], @scale)
111
+ @labels_seen[index] = 1
112
+ end
113
+ end
114
+
115
+ end