gruff 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -39,6 +39,7 @@ private
39
39
  @label_formatting = nil
40
40
  @show_labels_for_bar_values = false
41
41
  @hide_labels = false
42
+ @minimum_value = 0.0
42
43
  end
43
44
 
44
45
  def setup_data
@@ -46,47 +47,68 @@ private
46
47
  super
47
48
  end
48
49
 
50
+ def setup_graph_measurements
51
+ super
52
+ return if @hide_line_markers
53
+
54
+ if @show_labels_for_bar_values
55
+ proc_text_metrics = ->(text) { text_metrics(@marker_font, text) }
56
+
57
+ if maximum_value >= 0
58
+ _, metrics = Gruff::BarValueLabel.metrics(maximum_value, @label_formatting, proc_text_metrics)
59
+ @graph_top += metrics.height
60
+ end
61
+
62
+ @graph_height = @graph_bottom - @graph_top
63
+ end
64
+ end
65
+
49
66
  # Draws a bar graph, but multiple sets are stacked on top of each other.
50
67
  def draw_graph
51
68
  # Setup spacing.
52
69
  #
53
70
  # Columns sit stacked.
54
- bar_width = @graph_width / column_count.to_f
71
+ bar_width = @graph_width / column_count
55
72
  padding = (bar_width * (1 - @bar_spacing)) / 2
56
73
 
57
74
  height = Array.new(column_count, 0)
58
- stack_bar_value_label = Gruff::BarValueLabel::StackedBar.new
75
+ length = Array.new(column_count, @graph_bottom)
76
+ stack_bar_value_labels = Gruff::BarValueLabel::StackedBar.new
59
77
 
60
78
  store.norm_data.each_with_index do |data_row, row_index|
61
79
  data_row.points.each_with_index do |data_point, point_index|
62
- next if data_point == 0
80
+ temp1 = @graph_top + (@graph_height - (data_point * @graph_height) - height[point_index])
81
+ temp2 = @graph_top + @graph_height - height[point_index]
82
+ difference = temp2 - temp1
83
+ difference = 0 if difference < 0
63
84
 
64
85
  # Use incremented x and scaled y
65
86
  left_x = @graph_left + (bar_width * point_index) + padding
66
- left_y = @graph_top + (@graph_height -
67
- data_point * @graph_height -
68
- height[point_index]) + @segment_spacing
69
- right_x = left_x + bar_width * @bar_spacing
70
- right_y = @graph_top + @graph_height - height[point_index]
87
+ left_y = length[point_index] - difference
88
+ right_x = left_x + (bar_width * @bar_spacing)
89
+ right_y = length[point_index]
90
+ right_y -= @segment_spacing if row_index != store.columns - 1
71
91
 
72
92
  # update the total height of the current stacked bar
93
+ length[point_index] -= difference
73
94
  height[point_index] += (data_point * @graph_height)
74
95
 
75
96
  rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: data_row.color)
76
97
  rect_renderer.render(left_x, left_y, right_x, right_y)
77
98
 
78
99
  # Calculate center based on bar_width and current row
79
- label_center = left_x + bar_width * @bar_spacing / 2.0
100
+ label_center = left_x + (bar_width * @bar_spacing / 2.0)
80
101
  draw_label(label_center, point_index)
81
102
 
82
103
  bar_value_label = Gruff::BarValueLabel::Bar.new([left_x, left_y, right_x, right_y], store.data[row_index].points[point_index])
83
- stack_bar_value_label.add(bar_value_label, point_index)
104
+ stack_bar_value_labels.add(bar_value_label, point_index)
84
105
  end
85
106
  end
86
107
 
87
108
  if @show_labels_for_bar_values
88
- stack_bar_value_label.prepare_rendering(@label_formatting, bar_width) do |x, y, text|
89
- draw_value_label(x, y, text)
109
+ proc_text_metrics = ->(text) { text_metrics(@marker_font, text) }
110
+ stack_bar_value_labels.prepare_rendering(@label_formatting, proc_text_metrics) do |x, y, text, _text_width, text_height|
111
+ draw_value_label(bar_width * @bar_spacing, text_height, x, y, text)
90
112
  end
91
113
  end
92
114
  end
@@ -96,10 +118,10 @@ private
96
118
  end
97
119
 
98
120
  def hide_left_label_area?
99
- @hide_line_markers
121
+ @hide_line_markers && @y_axis_label.nil?
100
122
  end
101
123
 
102
124
  def hide_bottom_label_area?
103
- hide_labels?
125
+ hide_labels? && @x_axis_label.nil? && @legend_at_bottom == false
104
126
  end
105
127
  end
data/lib/gruff/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gruff
4
- VERSION = '0.15.0'
4
+ VERSION = '0.16.0'
5
5
  end
data/lib/gruff.rb CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  require 'rmagick'
4
4
 
5
- require 'gruff/patch/rmagick'
6
- require 'gruff/patch/string'
7
- require 'gruff/renderer/renderer'
8
- require 'gruff/store/store'
9
- require 'gruff/font'
10
- require 'gruff/base'
11
- require 'gruff/version'
5
+ require_relative 'gruff/patch/rmagick'
6
+ require_relative 'gruff/patch/string'
7
+ require_relative 'gruff/renderer/renderer'
8
+ require_relative 'gruff/store/store'
9
+ require_relative 'gruff/font'
10
+ require_relative 'gruff/base'
11
+ require_relative 'gruff/version'
12
12
 
13
13
  ##
14
14
  # = Gruff. Graphs.
@@ -29,14 +29,15 @@ module Gruff
29
29
  autoload :Area, Gruff.libpath('area')
30
30
  autoload :Bar, Gruff.libpath('bar')
31
31
  autoload :Bezier, Gruff.libpath('bezier')
32
+ autoload :BoxPlot, Gruff.libpath('box_plot')
32
33
  autoload :Bullet, Gruff.libpath('bullet')
34
+ autoload :Candlestick, Gruff.libpath('candlestick')
33
35
  autoload :Dot, Gruff.libpath('dot')
34
36
  autoload :Histogram, Gruff.libpath('histogram')
35
37
  autoload :Line, Gruff.libpath('line')
36
38
  autoload :Net, Gruff.libpath('net')
37
39
  autoload :Pie, Gruff.libpath('pie')
38
40
  autoload :Scatter, Gruff.libpath('scatter')
39
- autoload :Scene, Gruff.libpath('scene')
40
41
  autoload :SideBar, Gruff.libpath('side_bar')
41
42
  autoload :SideStackedBar, Gruff.libpath('side_stacked_bar')
42
43
  autoload :Spider, Gruff.libpath('spider')
@@ -61,7 +62,6 @@ module Gruff
61
62
 
62
63
  class Store
63
64
  autoload :BasicData, Gruff.libpath('store/basic_data')
64
- autoload :CustomData, Gruff.libpath('store/custom_data')
65
65
  autoload :XYData, Gruff.libpath('store/xy_data')
66
66
  end
67
67
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gruff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geoffrey Grosenbach
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-04-17 00:00:00.000000000 Z
12
+ date: 2022-05-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rmagick
@@ -165,7 +165,9 @@ files:
165
165
  - lib/gruff/bar.rb
166
166
  - lib/gruff/base.rb
167
167
  - lib/gruff/bezier.rb
168
+ - lib/gruff/box_plot.rb
168
169
  - lib/gruff/bullet.rb
170
+ - lib/gruff/candlestick.rb
169
171
  - lib/gruff/dot.rb
170
172
  - lib/gruff/font.rb
171
173
  - lib/gruff/helper/bar_conversion.rb
@@ -193,14 +195,12 @@ files:
193
195
  - lib/gruff/renderer/renderer.rb
194
196
  - lib/gruff/renderer/text.rb
195
197
  - lib/gruff/scatter.rb
196
- - lib/gruff/scene.rb
197
198
  - lib/gruff/side_bar.rb
198
199
  - lib/gruff/side_stacked_bar.rb
199
200
  - lib/gruff/spider.rb
200
201
  - lib/gruff/stacked_area.rb
201
202
  - lib/gruff/stacked_bar.rb
202
203
  - lib/gruff/store/basic_data.rb
203
- - lib/gruff/store/custom_data.rb
204
204
  - lib/gruff/store/store.rb
205
205
  - lib/gruff/store/xy_data.rb
206
206
  - lib/gruff/themes.rb
data/lib/gruff/scene.rb DELETED
@@ -1,208 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'observer'
4
-
5
- # A scene is a non-linear graph that assembles layers together to tell a story.
6
- # Layers are folders with appropriately named files (see below). You can group
7
- # layers and control them together or just set their values individually.
8
- #
9
- # Examples:
10
- #
11
- # * A city scene that changes with the time of day and the weather conditions.
12
- # * A traffic map that shows red lines on streets that are crowded and green on free-flowing ones.
13
- #
14
- # g = Gruff::Scene.new("500x100", "path/to/city_scene_directory")
15
- #
16
- # # Define order of layers, back to front
17
- # g.layers = %w(background haze sky clouds)
18
- #
19
- # # Define groups that will be controlled by the same input
20
- # g.weather_group = %w(clouds)
21
- # g.time_group = %w(background sky)
22
- #
23
- # # Set values for the layers or groups
24
- # g.weather = "cloudy"
25
- # g.time = Time.now
26
- # g.haze = true
27
- #
28
- # # Write the final graph to disk
29
- # g.write "hazy_daytime_city_scene.png"
30
- #
31
- # There are several rules that will magically select a layer when possible.
32
- #
33
- # * Numbered files will be selected according to the closest value that is less than the input value.
34
- # * +'true.png'+ and +'false.png'+ will be used as booleans.
35
- # * Other named files will be used if the input matches the filename (without the filetype extension).
36
- # * If there is a file named +'default.png'+, it will be used unless other input values are set for the corresponding layer.
37
- class Gruff::Scene < Gruff::Base
38
- # An array listing the folder names that will be rendered, from back to front.
39
- #
40
- # @example
41
- # g.layers = %w(sky clouds buildings street people)
42
- attr_reader :layers
43
-
44
- def initialize(target_width, base_dir)
45
- @base_dir = base_dir
46
- @groups = {}
47
- @layers = []
48
- super target_width
49
- end
50
-
51
- def draw
52
- # Join all the custom paths and filter out the empty ones
53
- image_paths = @layers.map(&:path).reject(&:empty?)
54
- images = Magick::ImageList.new(*image_paths)
55
- renderer.background_image = images.flatten_images
56
- end
57
-
58
- def layers=(ordered_list)
59
- ordered_list.each do |layer_name|
60
- @layers << Gruff::Layer.new(@base_dir, layer_name)
61
- end
62
- end
63
-
64
- # Group layers to input values
65
- #
66
- # g.weather_group = ["sky", "sea", "clouds"]
67
- #
68
- # Set input values
69
- #
70
- # g.weather = "cloudy"
71
- #
72
- def method_missing(method_name, *args)
73
- case method_name.to_s
74
- when /^(\w+)_group=$/
75
- add_group Regexp.last_match(1), *args
76
- when /^(\w+)=$/
77
- set_input Regexp.last_match(1), args.first
78
- else
79
- super
80
- end
81
- end
82
-
83
- def respond_to_missing?(method_sym, include_private)
84
- case method_sym.to_s
85
- when /^(\w+)_group=$/, /^(\w+)=$/
86
- true
87
- else
88
- super
89
- end
90
- end
91
-
92
- private
93
-
94
- def add_group(input_name, layer_names)
95
- @groups[input_name] = Gruff::Group.new(input_name, @layers.select { |layer| layer_names.include?(layer.name) })
96
- end
97
-
98
- def set_input(input_name, input_value)
99
- if !@groups[input_name].nil?
100
- @groups[input_name].send_updates(input_value)
101
- elsif (chosen_layer = @layers.find { |layer| layer.name == input_name })
102
- chosen_layer.update input_value
103
- end
104
- end
105
- end
106
-
107
- # @private
108
- class Gruff::Group
109
- include Observable
110
- attr_reader :name
111
-
112
- def initialize(folder_name, layers)
113
- @name = folder_name
114
- layers.each do |layer|
115
- layer.observe self
116
- end
117
- end
118
-
119
- def send_updates(value)
120
- changed
121
- notify_observers value
122
- end
123
- end
124
-
125
- # @private
126
- class Gruff::Layer
127
- attr_reader :name
128
-
129
- def initialize(base_dir, folder_name)
130
- @base_dir = base_dir.to_s
131
- @name = folder_name.to_s
132
- @filenames = Dir.open(File.join(base_dir, folder_name)).entries.grep(/^[^.]+\.png$/).sort
133
- @selected_filename = select_default
134
- end
135
-
136
- # Register this layer so it receives updates from the group
137
- def observe(obj)
138
- obj.add_observer self
139
- end
140
-
141
- # Choose the appropriate filename for this layer, based on the input
142
- def update(value)
143
- @selected_filename = begin
144
- case value.to_s
145
- when /^(true|false)$/
146
- select_boolean value
147
- when /^(\w|\s)+$/
148
- select_string value
149
- when /^-?(\d+\.)?\d+$/
150
- select_numeric value
151
- when /(\d\d):(\d\d):\d\d/
152
- select_time "#{Regexp.last_match(1)}#{Regexp.last_match(2)}"
153
- else
154
- select_default
155
- end
156
- end
157
- # Finally, try to use 'default' if we're still blank
158
- @selected_filename ||= select_default
159
- end
160
-
161
- # Returns the full path to the selected image, or a blank string
162
- def path
163
- unless @selected_filename.nil? || @selected_filename.empty?
164
- return File.join(@base_dir, @name, @selected_filename)
165
- end
166
-
167
- ''
168
- end
169
-
170
- private
171
-
172
- # Match "true.png" or "false.png"
173
- def select_boolean(value)
174
- file_exists_or_blank value.to_s
175
- end
176
-
177
- # Match -5 to _5.png
178
- def select_numeric(value)
179
- file_exists_or_blank value.to_s.gsub('-', '_')
180
- end
181
-
182
- def select_time(value)
183
- times = @filenames.map { |filename| filename.gsub('.png', '') }
184
- times.each_with_index do |time, index|
185
- if (time > value) && (index > 0)
186
- return "#{times[index - 1]}.png"
187
- end
188
- end
189
-
190
- "#{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
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Gruff
4
- class Store
5
- # @private
6
- class CustomData < Struct.new(:label, :points, :color, :custom)
7
- def initialize(label, points, color, custom = nil)
8
- super(label.to_s, Array(points), color, custom)
9
- end
10
-
11
- def empty?
12
- points.empty?
13
- end
14
-
15
- def columns
16
- points.length
17
- end
18
-
19
- def min
20
- points.compact.min
21
- end
22
-
23
- def max
24
- points.compact.max
25
- end
26
-
27
- def normalize(minimum:, spread:)
28
- norm_points = points.map do |point|
29
- point.nil? ? nil : (point.to_f - minimum.to_f) / spread
30
- end
31
-
32
- self.class.new(label, norm_points, color, custom)
33
- end
34
- end
35
- end
36
- end