gruff 0.6.0 → 0.7.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1c618f771c03abae8519d369261cbc112e0cbc41
4
- data.tar.gz: 406a006220455d8360e8b19cd4b25f88663befca
3
+ metadata.gz: 1f51ad044a490cf08a5bc23565b9a840e8f25ec7
4
+ data.tar.gz: e491d7b01929ec882b9e9760838fe021440eab14
5
5
  SHA512:
6
- metadata.gz: cd1ceca4d3c901e2067df8f44d264e7ef66829367232fb237528453598109f4a432ed4d5f23008890fbafa960b395f65945f15d8e25efa3357acee8594d39e5d
7
- data.tar.gz: 5fe22dbd8fa28469dbe85aa2511cd0815bcb702ac958b10b32305be9d6d0e74bea47cdfbdc673396d81df64281a53e007c30ab045002cd9de7c9c692b0e65303
6
+ metadata.gz: 0e70c18694a2d3dd2339e9374e8bd26628ce738fa854d38bde0f7b7fe8b7f07298e2617b50a8e2816bcb23cd1c777617fa5bff326ff821dc7baf9e0b902a61be
7
+ data.tar.gz: d458e987e717773484d57f85f9e9754c54aea06ceccf7bb6839d19413e79cd0605faa858912d3bee9aafef90b7c6c238a33174ea879e86de17a0d8cb69abc3a1
@@ -1,17 +1,19 @@
1
1
  language: ruby
2
2
  sudo: false
3
3
  rvm:
4
- - "1.8.7"
5
4
  - "1.9.3"
6
5
  - "2.0.0"
7
6
  - "2.1"
8
7
  - "2.2"
9
- - jruby-18mode
10
- - jruby-19mode
11
- - jruby-20mode
8
+ - "2.3.1"
9
+ - jruby-1.7.25
10
+ - jruby-9.0.5.0
11
+ - jruby-9.1.2.0
12
12
  - jruby-head
13
13
  - rbx
14
14
  - rbx-2
15
+ before_install:
16
+ - gem query -i -n ^bundler$ > /dev/null || gem install --no-ri --no-rdoc bundler
15
17
  notifications:
16
18
  email:
17
19
  - uwe@kubosch.no
data/Gemfile CHANGED
@@ -2,9 +2,3 @@ source 'http://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in gruff.gemspec
4
4
  gemspec
5
-
6
- group :test do
7
- if RUBY_VERSION =~ /^1\.9\./ || RUBY_VERSION =~ /^2\./
8
- gem 'minitest-reporters', '<1.0.0'
9
- end
10
- end
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Gruff Graphs
2
2
 
3
- [![Build Status](https://travis-ci.org/topfunky/gruff.png?branch=master)](https://travis-ci.org/topfunky/gruff)
4
- [![Gem Version](https://badge.fury.io/rb/gruff.png)](http://badge.fury.io/rb/gruff)
3
+ [![Build Status](https://travis-ci.org/topfunky/gruff.svg?branch=master)](https://travis-ci.org/topfunky/gruff)
4
+ [![Gem Version](https://badge.fury.io/rb/gruff.svg)](https://badge.fury.io/rb/gruff)
5
5
 
6
6
  A library for making beautiful graphs.
7
7
 
@@ -117,6 +117,10 @@ In progress!
117
117
 
118
118
  http://www.rubydoc.info/github/topfunky/gruff/frames
119
119
 
120
+ ## Supported Ruby Versions
121
+
122
+ We aim to support all Ruby implementations supporting Ruby language level 1.9.3
123
+ or later. Currently we are running CI for MRI, JRuby, and Rubinius.
120
124
 
121
125
  ## Contributing
122
126
 
data/Rakefile CHANGED
@@ -3,7 +3,7 @@ require 'bundler/gem_tasks'
3
3
  require 'rake/testtask'
4
4
  require 'rake/clean'
5
5
 
6
- CLEAN << %w(pkg test/output/*)
6
+ CLEAN.concat %w(pkg test/output/*)
7
7
 
8
8
  desc 'Run tests'
9
9
  task :default => :test
@@ -15,16 +15,18 @@ Gem::Specification.new do |s|
15
15
  s.homepage = %q{https://github.com/topfunky/gruff}
16
16
  s.require_paths = %w(lib)
17
17
  s.summary = %q{Beautiful graphs for one or multiple datasets.}
18
+ s.license = 'MIT'
18
19
  s.test_files = s.files.grep(%r{^(test|spec|features)/})
19
20
  s.executables = s.files.grep(%r{^bin/}).map { |f| File.basename(f) }
20
21
  s.specification_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
22
+ s.required_ruby_version = ['>= 1.9.3', '< 3']
23
+
21
24
  if defined? JRUBY_VERSION
22
25
  s.platform = 'java'
23
- s.add_dependency 'rmagick4j', '>= 0.3.9'
26
+ s.add_dependency 'rmagick4j', '~> 0.3', '>= 0.3.9'
24
27
  else
25
- s.add_dependency 'rmagick', '>= 2.13.4'
28
+ s.add_dependency 'rmagick', '~> 2.13', '>= 2.13.4'
26
29
  end
27
30
  s.add_development_dependency('rake')
28
- s.add_development_dependency('test-unit')
29
- s.license = 'MIT'
31
+ s.add_development_dependency('minitest-reporters')
30
32
  end
@@ -82,7 +82,8 @@ module Gruff
82
82
  # A label for the left side of the graph
83
83
  attr_accessor :y_axis_label
84
84
 
85
- # attr_accessor :x_axis_increment
85
+ # Manually set increment of the vertical marking lines
86
+ attr_accessor :x_axis_increment
86
87
 
87
88
  # Manually set increment of the horizontal marking lines
88
89
  attr_accessor :y_axis_increment
@@ -118,7 +119,7 @@ module Gruff
118
119
  attr_reader :font
119
120
 
120
121
  # Same as font but for the title.
121
- attr_reader :title_font
122
+ attr_accessor :title_font
122
123
 
123
124
  # Specifies whether to draw the title bolded or not.
124
125
  attr_accessor :bold_title
@@ -242,19 +243,23 @@ module Gruff
242
243
  @raw_columns = 800.0
243
244
  @raw_rows = 800.0 * (@rows/@columns)
244
245
  @column_count = 0
246
+ @data = Array.new
245
247
  @marker_count = nil
246
248
  @maximum_value = @minimum_value = nil
247
249
  @has_data = false
248
- @data = Array.new
250
+ @increment = nil
249
251
  @labels = Hash.new
252
+ @label_formatting = nil
250
253
  @labels_seen = Hash.new
251
254
  @sort = false
255
+ @sorted_drawing = false
252
256
  @title = nil
257
+ @title_font = nil
253
258
 
254
259
  @scale = @columns / @raw_columns
255
260
 
256
261
  vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
257
- @font = File.exists?(vera_font_path) ? vera_font_path : nil
262
+ @font = File.exist?(vera_font_path) ? vera_font_path : nil
258
263
  @bold_title = true
259
264
 
260
265
  @marker_font_size = 21.0
@@ -280,6 +285,8 @@ module Gruff
280
285
  @additional_line_colors = []
281
286
  @theme_options = {}
282
287
 
288
+ @use_data_label = false
289
+ @x_axis_increment = nil
283
290
  @x_axis_label = @y_axis_label = nil
284
291
  @y_axis_increment = nil
285
292
  @stacked = nil
@@ -297,11 +304,6 @@ module Gruff
297
304
  @d.font = @font
298
305
  end
299
306
 
300
- # Sets the title font to the font at +font_path+
301
- def title_font=(font_path)
302
- @title_font = font_path
303
- end
304
-
305
307
  # Add a color to the list of available colors for lines.
306
308
  #
307
309
  # Example:
@@ -836,7 +838,7 @@ module Gruff
836
838
  # TODO: See if index.odd? is the best stragegy
837
839
  y_offset += @label_stagger_height if index.odd?
838
840
 
839
- label_text = @labels[index]
841
+ label_text = labels[index].to_s
840
842
 
841
843
  # TESTME
842
844
  # FIXME: Consider chart types other than bar
@@ -58,7 +58,7 @@ class Gruff::Bullet < Gruff::Base
58
58
  @margin = 30.0
59
59
  @thickness = @raw_rows / 6.0
60
60
  @right_margin = @margin
61
- @graph_left = @title_width * 1.3 rescue @margin # HACK Need to calculate real width
61
+ @graph_left = (@title && (@title_width * 1.3)) || @margin
62
62
  @graph_width = @raw_columns - @graph_left - @right_margin
63
63
  @graph_height = @thickness * 3.0
64
64
 
@@ -25,6 +25,9 @@ class Gruff::Line < Gruff::Base
25
25
  attr_accessor :line_width
26
26
  attr_accessor :dot_radius
27
27
 
28
+ # default is a circle, other options include square
29
+ attr_accessor :dot_style
30
+
28
31
  # Hide parts of the graph to fit more datapoints, or for a different appearance.
29
32
  attr_accessor :hide_dots, :hide_lines
30
33
 
@@ -67,7 +70,7 @@ class Gruff::Line < Gruff::Base
67
70
  # g = Gruff::Line.new(400, false) # 400px wide, no lines (for backwards compatibility)
68
71
  #
69
72
  # g = Gruff::Line.new(false) # Defaults to 800px wide, no lines (for backwards compatibility)
70
- #
73
+ #
71
74
  # The preferred way is to call hide_dots or hide_lines instead.
72
75
  def initialize(*args)
73
76
  raise ArgumentError, 'Wrong number of arguments' if args.length > 2
@@ -84,6 +87,10 @@ class Gruff::Line < Gruff::Base
84
87
  @hide_dots = @hide_lines = false
85
88
  @maximum_x_value = nil
86
89
  @minimum_x_value = nil
90
+
91
+ @dot_style = 'circle'
92
+
93
+ @show_vertical_markers = false
87
94
  end
88
95
 
89
96
  # This method allows one to plot a dataset with both X and Y data.
@@ -101,7 +108,7 @@ class Gruff::Line < Gruff::Base
101
108
  # color: hex number indicating the line color as an RGB triplet
102
109
  #
103
110
  # Notes:
104
- # -if (x_data_points.length != y_data_points.length) an error is
111
+ # -if (x_data_points.length != y_data_points.length) an error is
105
112
  # returned.
106
113
  # -if the color argument is nil, the next color from the default theme will
107
114
  # be used.
@@ -115,7 +122,7 @@ class Gruff::Line < Gruff::Base
115
122
  # g.dataxy("Bapples", [1,3,4,5,7,9], [1, 1, 2, 2, 3, 3])
116
123
  # g.dataxy("Capples", [[1,1],[2,3],[3,4],[4,5],[5,7],[6,9]])
117
124
  # #you can still use the old data method too if you want:
118
- # g.data("Capples", [1, 1, 2, 2, 3, 3])
125
+ # g.data("Capples", [1, 1, 2, 2, 3, 3])
119
126
  # #labels will be drawn at the x locations of the keys passed in.
120
127
  # In this example the lables are drawn at x positions 2, 4, and 6:
121
128
  # g.labels = {0 => '2003', 2 => '2004', 4 => '2005', 6 => '2006'}
@@ -178,7 +185,7 @@ class Gruff::Line < Gruff::Base
178
185
 
179
186
  return unless @has_data
180
187
 
181
- # Check to see if more than one datapoint was given. NaN can result otherwise.
188
+ # Check to see if more than one datapoint was given. NaN can result otherwise.
182
189
  @x_increment = (@column_count > 1) ? (@graph_width / (@column_count - 1).to_f) : @graph_width
183
190
 
184
191
  @reference_lines.each_value do |curr_reference_line|
@@ -246,9 +253,12 @@ class Gruff::Line < Gruff::Base
246
253
  @d = @d.line(prev_x, prev_y, new_x, new_y)
247
254
  elsif @one_point
248
255
  # Show a circle if there's just one_point
249
- @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y)
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)
250
261
  end
251
- @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y) unless @hide_dots
252
262
 
253
263
  prev_x, prev_y = new_x, new_y
254
264
  end
@@ -281,7 +291,7 @@ class Gruff::Line < Gruff::Base
281
291
 
282
292
  @reference_lines.each_value do |curr_reference_line|
283
293
 
284
- # We only care about horizontal markers ... for normalization.
294
+ # We only care about horizontal markers ... for normalization.
285
295
  # Vertical markers won't have a :value, they will have an :index
286
296
 
287
297
  curr_reference_line[:norm_value] = ((curr_reference_line[:value].to_f - @minimum_value) / @spread.to_f) if (curr_reference_line.key?(:value))
@@ -326,4 +336,30 @@ class Gruff::Line < Gruff::Base
326
336
  one_point
327
337
  end
328
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
329
365
  end
@@ -4,6 +4,12 @@ module Gruff
4
4
 
5
5
  attr_accessor :hide_mini_legend, :legend_position
6
6
 
7
+ def initialize(*)
8
+ @hide_mini_legend = false
9
+ @legend_position = nil
10
+ super
11
+ end
12
+
7
13
  ##
8
14
  # The canvas needs to be bigger so we can put the legend beneath it.
9
15
 
@@ -45,7 +51,6 @@ module Gruff
45
51
  return if @hide_mini_legend
46
52
 
47
53
  legend_square_width = 40.0 # small square with color of this item
48
- legend_square_margin = 10.0
49
54
  @legend_left_margin = 100.0
50
55
  legend_top_margin = 40.0
51
56
 
@@ -17,107 +17,255 @@ class Gruff::Pie < Gruff::Base
17
17
 
18
18
  # Can be used to make the pie start cutting slices at the top (-90.0)
19
19
  # or at another angle. Default is 0.0, which starts at 3 o'clock.
20
- attr_accessor :zero_degree
20
+ attr_writer :zero_degree
21
+
21
22
  # Do not show labels for slices that are less than this percent. Use 0 to always show all labels.
22
23
  # Defaults to 0
23
- attr_accessor :hide_labels_less_than
24
+ attr_writer :hide_labels_less_than
25
+
24
26
  # Affect the distance between the percentages and the pie chart
25
27
  # Defaults to 0.15
26
- attr_accessor :text_offset_percentage
28
+ attr_writer :text_offset_percentage
29
+
27
30
  ## Use values instead of percentages
28
31
  attr_accessor :show_values_as_labels
29
32
 
30
33
  def initialize_ivars
31
34
  super
32
- @zero_degree = 0.0
33
- @hide_labels_less_than = 0.0
34
- @text_offset_percentage = DEFAULT_TEXT_OFFSET_PERCENTAGE
35
+
35
36
  @show_values_as_labels = false
36
37
  end
37
38
 
39
+ def zero_degree
40
+ @zero_degree ||= 0.0
41
+ end
42
+
43
+ def hide_labels_less_than
44
+ @hide_labels_less_than ||= 0.0
45
+ end
46
+
47
+ def text_offset_percentage
48
+ @text_offset_percentage ||= DEFAULT_TEXT_OFFSET_PERCENTAGE
49
+ end
50
+
51
+ def options
52
+ {
53
+ :zero_degree => zero_degree,
54
+ :hide_labels_less_than => hide_labels_less_than,
55
+ :text_offset_percentage => text_offset_percentage,
56
+ :show_values_as_labels => show_values_as_labels
57
+ }
58
+ end
59
+
38
60
  def draw
39
- @hide_line_markers = true
40
-
61
+ hide_line_markers
62
+
41
63
  super
42
64
 
43
- return unless @has_data
44
-
45
- diameter = @graph_height
46
- radius = ([@graph_width, @graph_height].min / 2.0) * 0.8
47
- center_x = @graph_left + (@graph_width / 2.0)
48
- center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
49
- total_sum = sums_for_pie()
50
- prev_degrees = @zero_degree
51
-
52
- # Use full data since we can easily calculate percentages
53
- data = (@sort ? @data.sort{ |a, b| a[DATA_VALUES_INDEX].first <=> b[DATA_VALUES_INDEX].first } : @data)
54
- data.each do |data_row|
55
- if data_row[DATA_VALUES_INDEX].first > 0
56
- @d = @d.stroke data_row[DATA_COLOR_INDEX]
57
- @d = @d.fill 'transparent'
58
- @d.stroke_width(radius) # stroke width should be equal to radius. we'll draw centered on (radius / 2)
59
-
60
- current_degrees = (data_row[DATA_VALUES_INDEX].first / total_sum) * 360.0
61
-
62
- # ellipse will draw the the stroke centered on the first two parameters offset by the second two.
63
- # therefore, in order to draw a circle of the proper diameter we must center the stroke at
64
- # half the radius for both x and y
65
- @d = @d.ellipse(center_x, center_y,
66
- radius / 2.0, radius / 2.0,
67
- prev_degrees, prev_degrees + current_degrees + 0.5) # <= +0.5 'fudge factor' gets rid of the ugly gaps
68
-
69
- half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2
70
-
71
- label_val = ((data_row[DATA_VALUES_INDEX].first / total_sum) * 100.0).round
72
- unless label_val < @hide_labels_less_than
73
- # RMagick must use sprintf with the string and % has special significance.
74
- label_string = @show_values_as_labels ? data_row[DATA_VALUES_INDEX].first.to_s : label_val.to_s + '%'
75
- @d = draw_label(center_x,center_y, half_angle,
76
- radius + (radius * @text_offset_percentage),
77
- label_string)
78
- end
79
-
80
- prev_degrees += current_degrees
65
+ return unless data_given?
66
+
67
+ slices.each do |slice|
68
+ if slice.value > 0
69
+ set_stroke_color slice
70
+ set_fill_color
71
+ set_stroke_width
72
+ set_drawing_points_for slice
73
+ process_label_for slice
74
+ update_chart_degrees_with slice.degrees
81
75
  end
82
76
  end
83
77
 
84
- # TODO debug a circle where the text is drawn...
85
-
78
+ trigger_final_draw
79
+ end
80
+
81
+ private
82
+
83
+ def slices
84
+ @slices ||= begin
85
+ slices = @data.map { |data| slice_class.new(data, options) }
86
+
87
+ slices.sort_by(&:value) if @sort
88
+
89
+ total = slices.map(&:value).inject(:+).to_f
90
+ slices.each { |slice| slice.total = total }
91
+ end
92
+ end
93
+
94
+ # General Helper Methods
95
+
96
+ def hide_line_markers
97
+ @hide_line_markers = true
98
+ end
99
+
100
+ def data_given?
101
+ @has_data
102
+ end
103
+
104
+ def update_chart_degrees_with(degrees)
105
+ @chart_degrees = chart_degrees + degrees
106
+ end
107
+
108
+ def slice_class
109
+ PieSlice
110
+ end
111
+
112
+ # Spatial Value-Related Methods
113
+
114
+ def chart_degrees
115
+ @chart_degrees ||= zero_degree
116
+ end
117
+
118
+ def graph_height
119
+ @graph_height
120
+ end
121
+
122
+ def graph_width
123
+ @graph_width
124
+ end
125
+
126
+ def diameter
127
+ graph_height
128
+ end
129
+
130
+ def half_width
131
+ graph_width / 2.0
132
+ end
133
+
134
+ def half_height
135
+ graph_height / 2.0
136
+ end
137
+
138
+ def radius
139
+ @radius ||= ([graph_width, graph_height].min / 2.0) * 0.8
140
+ end
141
+
142
+ def center_x
143
+ @center_x ||= @graph_left + half_width
144
+ end
145
+
146
+ def center_y
147
+ @center_y ||= @graph_top + half_height - 10
148
+ end
149
+
150
+ def distance_from_center
151
+ 20.0
152
+ end
153
+
154
+ def radius_offset
155
+ radius + (radius * text_offset_percentage) + distance_from_center
156
+ end
157
+
158
+ def ellipse_factor
159
+ radius_offset * text_offset_percentage
160
+ end
161
+
162
+ # Label-Related Methods
163
+
164
+ def process_label_for(slice)
165
+ if slice.percentage >= hide_labels_less_than
166
+ x, y = label_coordinates_for slice
167
+
168
+ @d = draw_label(x, y, slice.label)
169
+ end
170
+ end
171
+
172
+ def label_coordinates_for(slice)
173
+ angle = chart_degrees + slice.degrees / 2
174
+
175
+ [x_label_coordinate(angle), y_label_coordinate(angle)]
176
+ end
177
+
178
+ def x_label_coordinate(angle)
179
+ center_x + ((radius_offset + ellipse_factor) * Math.cos(deg2rad(angle)))
180
+ end
181
+
182
+ def y_label_coordinate(angle)
183
+ center_y + (radius_offset * Math.sin(deg2rad(angle)))
184
+ end
185
+
186
+ # Drawing-Related Methods
187
+
188
+ def set_stroke_width
189
+ @d.stroke_width(radius)
190
+ end
191
+
192
+ def set_stroke_color(slice)
193
+ @d = @d.stroke slice.color
194
+ end
195
+
196
+ def set_fill_color
197
+ @d = @d.fill 'transparent'
198
+ end
199
+
200
+ def set_drawing_points_for(slice)
201
+ @d = @d.ellipse(
202
+ center_x,
203
+ center_y,
204
+ radius / 2.0,
205
+ radius / 2.0,
206
+ chart_degrees,
207
+ chart_degrees + slice.degrees + 0.5
208
+ )
209
+ end
210
+
211
+ def trigger_final_draw
86
212
  @d.draw(@base_image)
87
213
  end
88
214
 
89
- private
90
-
91
- ##
92
- # Labels are drawn around a slightly wider ellipse to give room for
93
- # labels on the left and right.
94
- def draw_label(center_x, center_y, angle, radius, amount)
95
- # TODO Don't use so many hard-coded numbers
96
- r_offset = 20.0 # The distance out from the center of the pie to get point
97
- x_offset = center_x # + 15.0 # The label points need to be tweaked slightly
98
- y_offset = center_y # This one doesn't though
99
- radius_offset = (radius + r_offset)
100
- ellipse_factor = radius_offset * @text_offset_percentage
101
- x = x_offset + ((radius_offset + ellipse_factor) * Math.cos(deg2rad(angle)))
102
- y = y_offset + (radius_offset * Math.sin(deg2rad(angle)))
103
-
104
- # Draw label
105
- @d.fill = @font_color
106
- @d.font = @font if @font
107
- @d.pointsize = scale_fontsize(@marker_font_size)
108
- @d.stroke = 'transparent'
215
+ def configure_label_styling
216
+ @d.fill = @font_color
217
+ @d.font = @font if @font
218
+ @d.pointsize = scale_fontsize(@marker_font_size)
219
+ @d.stroke = 'transparent'
109
220
  @d.font_weight = BoldWeight
110
- @d.gravity = CenterGravity
111
- @d.annotate_scaled( @base_image,
112
- 0, 0,
113
- x, y,
114
- amount, @scale)
221
+ @d.gravity = CenterGravity
115
222
  end
116
223
 
117
- def sums_for_pie
118
- total_sum = 0.0
119
- @data.collect {|data_row| total_sum += data_row[DATA_VALUES_INDEX].first }
120
- total_sum
224
+ def draw_label(x, y, value)
225
+ configure_label_styling
226
+
227
+ @d.annotate_scaled(
228
+ @base_image,
229
+ 0,
230
+ 0,
231
+ x,
232
+ y,
233
+ value,
234
+ @scale
235
+ )
121
236
  end
122
237
 
238
+ # Helper Classes
239
+
240
+ class PieSlice < Struct.new(:data_array, :options)
241
+ attr_accessor :total
242
+
243
+ def name
244
+ data_array[0]
245
+ end
246
+
247
+ def value
248
+ data_array[1].first
249
+ end
250
+
251
+ def color
252
+ data_array[2]
253
+ end
254
+
255
+ def size
256
+ @size ||= value / total
257
+ end
258
+
259
+ def percentage
260
+ @percentage ||= (size * 100.0).round
261
+ end
262
+
263
+ def degrees
264
+ @degrees ||= size * 360.0
265
+ end
266
+
267
+ def label
268
+ options[:show_values_as_labels] ? value.to_s : "#{percentage}%"
269
+ end
270
+ end
123
271
  end
@@ -32,6 +32,25 @@ class Gruff::Scatter < Gruff::Base
32
32
 
33
33
  #~ # Color of the horizontal baseline
34
34
  #~ attr_accessor :baseline_x_color
35
+
36
+ # Attributes to allow customising the size of the points
37
+ attr_accessor :circle_radius
38
+ attr_accessor :stroke_width
39
+
40
+ # Allow disabling the significant rounding when labeling the X axis
41
+ # This is useful when working with a small range of high values (for example, a date range of months, while seconds as units)
42
+ attr_accessor :disable_significant_rounding_x_axis
43
+
44
+ # Allow enabling vertical lines. When you have a lot of data, they can work great
45
+ attr_accessor :enable_vertical_line_markers
46
+
47
+ # Allow using vertical labels in the X axis (and setting the label margin)
48
+ attr_accessor :x_label_margin
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
35
54
 
36
55
 
37
56
  # Gruff::Scatter takes the same parameters as the Gruff::Line graph
@@ -40,13 +59,21 @@ class Gruff::Scatter < Gruff::Base
40
59
  #
41
60
  # g = Gruff::Scatter.new
42
61
  #
43
- def initialize(*args)
44
- super(*args)
45
-
46
- @maximum_x_value = @minimum_x_value = nil
62
+ def initialize(*)
63
+ super
64
+
47
65
  @baseline_x_color = @baseline_y_color = 'red'
48
66
  @baseline_x_value = @baseline_y_value = nil
67
+ @circle_radius = nil
68
+ @disable_significant_rounding_x_axis = false
69
+ @enable_vertical_line_markers = false
49
70
  @marker_x_count = nil
71
+ @maximum_x_value = @minimum_x_value = nil
72
+ @stroke_width = nil
73
+ @use_vertical_x_labels = false
74
+ @x_axis_label_format = nil
75
+ @x_label_margin = nil
76
+ @y_axis_label_format = nil
50
77
  end
51
78
 
52
79
  def setup_drawing
@@ -94,9 +121,9 @@ class Gruff::Scatter < Gruff::Base
94
121
  @d = @d.stroke data_row[DATA_COLOR_INDEX]
95
122
  @d = @d.fill data_row[DATA_COLOR_INDEX]
96
123
  @d = @d.stroke_opacity 1.0
97
- @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
124
+ @d = @d.stroke_width @stroke_width || clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
98
125
 
99
- circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
126
+ circle_radius = @circle_radius || clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
100
127
  @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y)
101
128
  end
102
129
  end
@@ -173,7 +200,7 @@ protected
173
200
  @x_spread = @x_spread > 0 ? @x_spread : 1
174
201
  end
175
202
 
176
- def normalize(force=@xy_normalize)
203
+ def normalize(force=nil)
177
204
  if @norm_data.nil? || force
178
205
  @norm_data = []
179
206
  return unless @has_data
@@ -212,7 +239,10 @@ protected
212
239
  end
213
240
  @marker_x_count ||= 4
214
241
  end
215
- @x_increment = (@x_spread > 0) ? significant(@x_spread / @marker_x_count) : 1
242
+ @x_increment = (@x_spread > 0) ? (@x_spread / @marker_x_count) : 1
243
+ unless @disable_significant_rounding_x_axis
244
+ @x_increment = significant(@x_increment)
245
+ end
216
246
  else
217
247
  # TODO Make this work for negative values
218
248
  @maximum_x_value = [@maximum_value.ceil, @x_axis_increment].max
@@ -228,15 +258,17 @@ protected
228
258
  # Draw vertical line markers and annotate with numbers
229
259
  (0..@marker_x_count).each do |index|
230
260
 
231
- # TODO Fix the vertical lines. Not pretty when they don't match up with top y-axis line
232
- # x = @graph_left + @graph_width - index.to_f * @increment_x_scaled
233
- # @d = @d.stroke(@marker_color)
234
- # @d = @d.stroke_width 1
235
- # @d = @d.line(x, @graph_top, x, @graph_bottom)
261
+ # TODO Fix the vertical lines, and enable them by default. Not pretty when they don't match up with top y-axis line
262
+ if @enable_vertical_line_markers
263
+ x = @graph_left + @graph_width - index.to_f * @increment_x_scaled
264
+ @d = @d.stroke(@marker_color)
265
+ @d = @d.stroke_width 1
266
+ @d = @d.line(x, @graph_top, x, @graph_bottom)
267
+ end
236
268
 
237
269
  unless @hide_line_numbers
238
270
  marker_label = index * @x_increment + @minimum_x_value.to_f
239
- y_offset = @graph_bottom + LABEL_MARGIN
271
+ y_offset = @graph_bottom + (@x_label_margin || LABEL_MARGIN)
240
272
  x_offset = get_x_coord(index.to_f, @increment_x_scaled, @graph_left)
241
273
 
242
274
  @d.fill = @font_color
@@ -244,16 +276,34 @@ protected
244
276
  @d.stroke('transparent')
245
277
  @d.pointsize = scale_fontsize(@marker_font_size)
246
278
  @d.gravity = NorthGravity
247
-
279
+ @d.rotation = -90.0 if @use_vertical_x_labels
248
280
  @d = @d.annotate_scaled(@base_image,
249
281
  1.0, 1.0,
250
282
  x_offset, y_offset,
251
- label(marker_label, @x_increment), @scale)
283
+ vertical_label(marker_label, @x_increment), @scale)
284
+ @d.rotation = 90.0 if @use_vertical_x_labels
252
285
  end
253
286
  end
254
287
 
255
288
  @d = @d.stroke_antialias true
256
289
  end
290
+
291
+
292
+ def label(value, increment)
293
+ if @y_axis_label_format
294
+ @y_axis_label_format.call(value)
295
+ else
296
+ super
297
+ end
298
+ end
299
+
300
+ def vertical_label(value, increment)
301
+ if @x_axis_label_format
302
+ @x_axis_label_format.call(value)
303
+ else
304
+ label(value, increment)
305
+ end
306
+ end
257
307
 
258
308
  private
259
309
 
@@ -24,7 +24,7 @@ class Gruff::Spider < Gruff::Base
24
24
  def initialize(max_value, target_width = 800)
25
25
  super(target_width)
26
26
  @max_value = max_value
27
- @hide_legend = true;
27
+ @hide_legend = true
28
28
  @rotation = 0
29
29
  end
30
30
 
@@ -36,20 +36,14 @@ class Gruff::Spider < Gruff::Base
36
36
  return unless @has_data
37
37
 
38
38
  # Setup basic positioning
39
- diameter = @graph_height
40
39
  radius = @graph_height / 2.0
41
- top_x = @graph_left + (@graph_width - diameter) / 2.0
42
40
  center_x = @graph_left + (@graph_width / 2.0)
43
41
  center_y = @graph_top + (@graph_height / 2.0) - 25 # Move graph up a bit
44
42
 
45
43
  @unit_length = radius / @max_value
46
44
 
47
- total_sum = sums_for_spider
48
- prev_degrees = 0.0
49
45
  additive_angle = (2 * Math::PI)/ @data.size
50
46
 
51
- current_angle = rotation * Math::PI / 180.0
52
-
53
47
  # Draw axes
54
48
  draw_axes(center_x, center_y, radius, additive_angle) unless hide_axes
55
49
 
@@ -125,7 +119,7 @@ private
125
119
  end
126
120
 
127
121
  def sums_for_spider
128
- @data.inject(0.0) {|sum, data_row| sum += data_row[DATA_VALUES_INDEX].first}
122
+ @data.inject(0.0) {|sum, data_row| sum + data_row[DATA_VALUES_INDEX].first}
129
123
  end
130
124
 
131
125
  end
@@ -1,3 +1,3 @@
1
1
  module Gruff
2
- VERSION = '0.6.0'
2
+ VERSION = '0.7.0'
3
3
  end
@@ -2,7 +2,6 @@ $:.unshift(File.dirname(__FILE__) + '/../lib/')
2
2
 
3
3
  RMAGICK_BYPASS_VERSION_TEST = true
4
4
 
5
- require 'test/unit'
6
5
  require 'gruff'
7
6
  require 'fileutils'
8
7
 
@@ -10,10 +9,9 @@ TEST_OUTPUT_DIR = File.dirname(__FILE__) + "/output#{'_java' if RUBY_PLATFORM ==
10
9
  FileUtils.mkdir_p(TEST_OUTPUT_DIR)
11
10
  FileUtils.rm_f Dir[TEST_OUTPUT_DIR + '/*']
12
11
 
13
- if ENV['RM_INFO'] && RUBY_VERSION =~ /^(1\.9|2\.0)\./
14
- require 'minitest/reporters'
15
- MiniTest::Reporters.use!
16
- end
12
+ require 'minitest/autorun'
13
+ require 'minitest/reporters'
14
+ Minitest::Reporters.use!
17
15
 
18
16
  class Gruff::Base
19
17
  alias :write_org :write
@@ -23,7 +21,7 @@ class Gruff::Base
23
21
  extension = filename.slice(/\.[^\.]*$/)
24
22
  testfilename = File.join(TEST_OUTPUT_DIR, basefilename) + extension
25
23
  counter = 0
26
- while File.exists?(testfilename)
24
+ while File.exist?(testfilename)
27
25
  counter += 1
28
26
  testfilename = File.join(TEST_OUTPUT_DIR, basefilename) + "-#{counter}#{extension}"
29
27
  end
@@ -31,7 +29,7 @@ class Gruff::Base
31
29
  end
32
30
  end
33
31
 
34
- class GruffTestCase < Test::Unit::TestCase
32
+ class GruffTestCase < Minitest::Test
35
33
  def setup
36
34
  srand 42
37
35
  @datasets = [
@@ -106,7 +104,7 @@ class GruffTestCase < Test::Unit::TestCase
106
104
  basefilename = filename.split('.')[0..-2].join('.')
107
105
  extension = filename.slice(/\..*$/)
108
106
  counter = 0
109
- while File.exists? testfilename
107
+ while File.exist? testfilename
110
108
  counter += 1
111
109
  testfilename = [TEST_OUTPUT_DIR, basefilename].join('/') + "-#{counter}#{extension}"
112
110
  end
@@ -40,7 +40,7 @@ class TestGruffAccumulatorBar < GruffTestCase
40
40
  end
41
41
 
42
42
  def test_too_many_args
43
- assert_raise(Gruff::IncorrectNumberOfDatasetsException) {
43
+ assert_raises(Gruff::IncorrectNumberOfDatasetsException) {
44
44
  g = Gruff::AccumulatorBar.new
45
45
  g.data 'First', [1,1,1]
46
46
  g.data 'Too Many', [1,1,1]
@@ -259,11 +259,11 @@ class TestGruffBar < GruffTestCase
259
259
  def test_spacing_factor_does_not_accept_values_lt_0_and_gt_1
260
260
  g = Gruff::Bar.new
261
261
 
262
- assert_raise ArgumentError do
262
+ assert_raises ArgumentError do
263
263
  g.spacing_factor = 1.01
264
264
  end
265
265
 
266
- assert_raise ArgumentError do
266
+ assert_raises ArgumentError do
267
267
  g.spacing_factor = -0.01
268
268
  end
269
269
  end
@@ -4,5 +4,30 @@ require File.dirname(__FILE__) + "/gruff_test_case"
4
4
 
5
5
  class TestGruffBase < GruffTestCase
6
6
 
7
+ def setup
8
+ @sample_numeric_labels = {
9
+ 0 => 6,
10
+ 1 => 15,
11
+ 2 => 24,
12
+ 3 => 30,
13
+ 4 => 4,
14
+ 5 => 12,
15
+ 6 => 21,
16
+ 7 => 28,
17
+ }
18
+ end
7
19
 
8
- end
20
+ def test_labels_can_be_any_object
21
+ g = Gruff::Bar.new
22
+ g.title = 'Bar Graph With Manual Colors'
23
+ g.legend_margin = 50
24
+ g.labels = @sample_numeric_labels
25
+ g.data(:Art, [0, 5, 8, 15], '#990000')
26
+ g.data(:Philosophy, [10, 3, 2, 8], '#009900')
27
+ g.data(:Science, [2, 15, 8, 11], '#990099')
28
+
29
+ g.minimum_value = 0
30
+
31
+ g.write('test/output/bar_object_labels.png')
32
+ end
33
+ end
@@ -13,13 +13,13 @@ class TestGruffBullet < GruffTestCase
13
13
  def test_bullet_graph
14
14
  g = Gruff::Bullet.new
15
15
  g.title = "Monthly Revenue"
16
- g.data *@data_args
16
+ g.data(*@data_args)
17
17
  g.write("test/output/bullet_greyscale.png")
18
18
  end
19
19
 
20
20
  def test_no_options
21
21
  g = Gruff::Bullet.new
22
- g.data *@data_args
22
+ g.data(*@data_args)
23
23
  g.write("test/output/bullet_no_options.png")
24
24
  end
25
25
 
@@ -234,11 +234,11 @@ class TestGruffDot < GruffTestCase
234
234
  g.hide_legend = true
235
235
  g.title = 'Full speed ahead'
236
236
  g.labels = (0..10).inject({}) { |memo, i| memo.merge({ i => (i*10).to_s}) }
237
- g.data(:apples, (0..9).map { rand(20)/10.0 })
237
+ g.data(:apples, [1.7, 0.8, 0.1, 1.9, 1.4, 0.6, 1.1, 0.7, 1.4, 0.2])
238
238
  g.y_axis_increment = 1.0
239
239
  g.x_axis_label = 'Score (%)'
240
240
  g.y_axis_label = 'Students'
241
- write_test_file g, 'enhancements.png'
241
+ write_test_file g, 'enhancements_dot.png'
242
242
  end
243
243
 
244
244
 
@@ -192,6 +192,23 @@ class TestGruffLine < GruffTestCase
192
192
  end
193
193
 
194
194
 
195
+ def test_dot_style_square
196
+ g = Gruff::Line.new
197
+ g.title = 'Square points'
198
+ g.labels = {
199
+ 0 => 'June',
200
+ 10 => 'July',
201
+ 30 => 'August',
202
+ 50 => 'September',
203
+ }
204
+ g.dot_style = :square
205
+ g.data('many points', (0..50).collect { |i| rand(100) })
206
+ g.x_axis_label = 'Months'
207
+
208
+ # Default theme
209
+ g.write('test/output/line_dot_style_square.png')
210
+ end
211
+
195
212
  def test_similar_high_end_values
196
213
  @dataset = %w(29.43 29.459 29.498 29.53 29.548 29.589 29.619 29.66 29.689 29.849 29.878 29.74 29.769 29.79 29.808 29.828).collect { |i| i.to_f }
197
214
 
@@ -577,7 +594,7 @@ class TestGruffLine < GruffTestCase
577
594
  g.title = 'Line Chart WEBP'
578
595
  g.write('line_webp.webp')
579
596
  rescue Magick::ImageMagickError
580
- assert_match /no encode delegate for this image format .*\.webp/, $!.message
597
+ assert_match(/no encode delegate for this image format .*\.webp/, $!.message)
581
598
  end
582
599
 
583
600
  private
@@ -127,7 +127,8 @@ class TestGruffPie < GruffTestCase
127
127
 
128
128
 
129
129
  def test_tiny_simple_pie
130
- @datasets = (1..5).map {|n| ['Auto', [rand(100)]]}
130
+ r = Random.new(297427)
131
+ @datasets = (1..5).map {|n| ['Auto', [r.rand(100)]]}
131
132
 
132
133
  g = setup_basic_graph 200
133
134
  g.hide_legend = true
@@ -140,13 +141,25 @@ class TestGruffPie < GruffTestCase
140
141
  write_test_file g, "pie_simple.png"
141
142
  end
142
143
 
143
- def test_pie_with_adjusted_text_offset_percentage
144
+ def test_pie_with_adjusted_text_offset_percentage
144
145
  g = setup_basic_graph
145
146
  g.title = "Adjusted Text Offset Percentage"
146
147
  g.text_offset_percentage = 0.03
147
148
  g.write "test/output/pie_adjusted_text_offset_percentage.png"
148
149
  end
149
150
 
151
+ def test_subclassed_pie_with_custom_labels
152
+ CustomLabeledPie.new(800).tap do |graph|
153
+ graph.title = "Subclassed Pie with Custom Lables"
154
+
155
+ @datasets.map { |set| set << set.join(': ') }.each do |data|
156
+ graph.data(data[0], data[1], :label => data[2])
157
+ end
158
+
159
+ graph.write 'test/output/pie_subclass_custom_labels.png'
160
+ end
161
+ end
162
+
150
163
  protected
151
164
 
152
165
  def setup_basic_graph(size=800)
@@ -157,5 +170,25 @@ protected
157
170
  end
158
171
  return g
159
172
  end
160
-
173
+
174
+ # Example Gruff::Pie Subclass demonstrating custom labels
175
+ class CustomLabeledPie < Gruff::Pie
176
+ def data(name, data_points = [], options = {})
177
+ super(name, data_points, options[:color])
178
+
179
+ @data.each { |data_array| data_array << options[:label] }
180
+ end
181
+
182
+ private
183
+
184
+ def slice_class
185
+ CustomLabeledSlice
186
+ end
187
+
188
+ class CustomLabeledSlice < ::Gruff::Pie::PieSlice
189
+ def label
190
+ data_array[3] || super
191
+ end
192
+ end
193
+ end
161
194
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require File.dirname(__FILE__) + '/gruff_test_case'
4
4
 
5
- class TestGruffScatter < Test::Unit::TestCase
5
+ class TestGruffScatter < Minitest::Test
6
6
 
7
7
  def setup
8
8
  @datasets = [
@@ -31,6 +31,43 @@ class TestGruffScatter < Test::Unit::TestCase
31
31
  g.write('test/output/scatter_many.png')
32
32
  end
33
33
 
34
+ def test_custom_label_format
35
+ g = Gruff::Scatter.new('1000x500')
36
+ g.top_margin = 0
37
+ g.hide_legend = true
38
+ g.hide_title = true
39
+ g.marker_font_size = 10
40
+ g.theme = {
41
+ :colors => ['#12a702', '#aedaa9'],
42
+ :marker_color => '#dddddd',
43
+ :font_color => 'black',
44
+ :background_colors => 'white'
45
+ }
46
+
47
+ # Points style
48
+ g.circle_radius = 1
49
+ g.stroke_width = 0.01
50
+
51
+ # Axis labels
52
+ g.x_label_margin = 40
53
+ g.bottom_margin = 60
54
+ g.disable_significant_rounding_x_axis = true
55
+ g.use_vertical_x_labels = true
56
+ g.enable_vertical_line_markers = true
57
+ g.marker_x_count = 50 # One label every 2 days
58
+ g.x_axis_label_format = lambda do |value|
59
+ DateTime.strptime(value.to_i.to_s,'%s').strftime('%d.%m.%Y')
60
+ end
61
+ g.y_axis_increment = 1
62
+
63
+ # Fake data (100 days, random times of day between 5 and 16)
64
+ r = Random.new(269155)
65
+ y_values = (0..100).map { 5 + r.rand(12) }
66
+ x_values = (0..100).map { |i| Date.today.strftime('%s').to_i + i*3600*24 }
67
+ g.data('many points', x_values, y_values)
68
+ g.write('test/output/scatter_custom_label_format.png')
69
+ end
70
+
34
71
  # Done
35
72
  def test_no_data
36
73
  g = Gruff::Scatter.new(400)
@@ -65,7 +102,7 @@ class TestGruffScatter < Test::Unit::TestCase
65
102
  ]
66
103
 
67
104
  @datasets.each do |data|
68
- assert_raise ArgumentError do
105
+ assert_raises ArgumentError do
69
106
  g.data(*data)
70
107
  end
71
108
  end
@@ -82,7 +119,7 @@ class TestGruffScatter < Test::Unit::TestCase
82
119
  ]
83
120
 
84
121
  @datasets.each do |data|
85
- assert_raise ArgumentError do
122
+ assert_raises ArgumentError do
86
123
  g.data(*data)
87
124
  end
88
125
  end
@@ -98,7 +135,7 @@ class TestGruffScatter < Test::Unit::TestCase
98
135
  ]
99
136
 
100
137
  @datasets.each do |data|
101
- assert_raise ArgumentError do
138
+ assert_raises ArgumentError do
102
139
  g.data(*data)
103
140
  end
104
141
  end
@@ -87,7 +87,7 @@ class TestGruffSideStackedBar < GruffTestCase
87
87
  end
88
88
  g.show_labels_for_bar_values = true
89
89
  g.write "test/output/side_stacked_bar_labels.png"
90
- end
90
+ end
91
91
 
92
92
  protected
93
93
 
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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geoffrey Grosenbach
@@ -9,12 +9,15 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-05-31 00:00:00.000000000 Z
12
+ date: 2016-06-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rmagick
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '2.13'
18
21
  - - ">="
19
22
  - !ruby/object:Gem::Version
20
23
  version: 2.13.4
@@ -22,6 +25,9 @@ dependencies:
22
25
  prerelease: false
23
26
  version_requirements: !ruby/object:Gem::Requirement
24
27
  requirements:
28
+ - - "~>"
29
+ - !ruby/object:Gem::Version
30
+ version: '2.13'
25
31
  - - ">="
26
32
  - !ruby/object:Gem::Version
27
33
  version: 2.13.4
@@ -40,7 +46,7 @@ dependencies:
40
46
  - !ruby/object:Gem::Version
41
47
  version: '0'
42
48
  - !ruby/object:Gem::Dependency
43
- name: test-unit
49
+ name: minitest-reporters
44
50
  requirement: !ruby/object:Gem::Requirement
45
51
  requirements:
46
52
  - - ">="
@@ -166,7 +172,10 @@ required_ruby_version: !ruby/object:Gem::Requirement
166
172
  requirements:
167
173
  - - ">="
168
174
  - !ruby/object:Gem::Version
169
- version: '0'
175
+ version: 1.9.3
176
+ - - "<"
177
+ - !ruby/object:Gem::Version
178
+ version: '3'
170
179
  required_rubygems_version: !ruby/object:Gem::Requirement
171
180
  requirements:
172
181
  - - ">="
@@ -174,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
183
  version: '0'
175
184
  requirements: []
176
185
  rubyforge_project:
177
- rubygems_version: 2.4.6
186
+ rubygems_version: 2.5.1
178
187
  signing_key:
179
188
  specification_version: 4
180
189
  summary: Beautiful graphs for one or multiple datasets.