gruff 0.14.0 → 0.17.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +28 -12
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +20 -24
  5. data/CHANGELOG.md +52 -0
  6. data/README.md +10 -3
  7. data/gruff.gemspec +9 -10
  8. data/lib/gruff/accumulator_bar.rb +1 -1
  9. data/lib/gruff/area.rb +6 -4
  10. data/lib/gruff/bar.rb +53 -31
  11. data/lib/gruff/base.rb +292 -184
  12. data/lib/gruff/bezier.rb +4 -2
  13. data/lib/gruff/box_plot.rb +180 -0
  14. data/lib/gruff/bullet.rb +6 -6
  15. data/lib/gruff/candlestick.rb +120 -0
  16. data/lib/gruff/dot.rb +11 -12
  17. data/lib/gruff/font.rb +3 -0
  18. data/lib/gruff/helper/bar_conversion.rb +6 -10
  19. data/lib/gruff/helper/bar_mixin.rb +25 -0
  20. data/lib/gruff/helper/bar_value_label.rb +24 -40
  21. data/lib/gruff/helper/stacked_mixin.rb +19 -1
  22. data/lib/gruff/histogram.rb +9 -5
  23. data/lib/gruff/line.rb +49 -48
  24. data/lib/gruff/mini/legend.rb +11 -11
  25. data/lib/gruff/net.rb +23 -18
  26. data/lib/gruff/patch/rmagick.rb +0 -1
  27. data/lib/gruff/patch/string.rb +1 -0
  28. data/lib/gruff/pie.rb +26 -12
  29. data/lib/gruff/renderer/dash_line.rb +3 -2
  30. data/lib/gruff/renderer/dot.rb +28 -15
  31. data/lib/gruff/renderer/line.rb +1 -3
  32. data/lib/gruff/renderer/rectangle.rb +6 -2
  33. data/lib/gruff/renderer/renderer.rb +4 -8
  34. data/lib/gruff/renderer/text.rb +7 -1
  35. data/lib/gruff/scatter.rb +64 -56
  36. data/lib/gruff/side_bar.rb +64 -30
  37. data/lib/gruff/side_stacked_bar.rb +43 -54
  38. data/lib/gruff/spider.rb +52 -18
  39. data/lib/gruff/stacked_area.rb +18 -8
  40. data/lib/gruff/stacked_bar.rb +59 -29
  41. data/lib/gruff/store/xy_data.rb +2 -0
  42. data/lib/gruff/version.rb +1 -1
  43. data/lib/gruff.rb +67 -58
  44. metadata +22 -21
  45. data/.rubocop_todo.yml +0 -116
  46. data/lib/gruff/scene.rb +0 -200
  47. data/lib/gruff/store/custom_data.rb +0 -36
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'helper/stacked_mixin'
4
+
3
5
  #
4
6
  # Here's how to set up a Gruff::StackedBar.
5
7
  #
@@ -39,6 +41,16 @@ private
39
41
  @label_formatting = nil
40
42
  @show_labels_for_bar_values = false
41
43
  @hide_labels = false
44
+ @minimum_value = 0.0
45
+ end
46
+
47
+ def setup_drawing
48
+ # Labels will be centered over the left of the bar if
49
+ # there are more labels than columns. This is basically the same
50
+ # as where it would be for a line graph.
51
+ @center_labels_over_point = (@labels.keys.length > column_count)
52
+
53
+ super
42
54
  end
43
55
 
44
56
  def setup_data
@@ -46,47 +58,61 @@ private
46
58
  super
47
59
  end
48
60
 
61
+ def setup_graph_measurements
62
+ super
63
+ return if @hide_line_markers
64
+
65
+ if @show_labels_for_bar_values
66
+ if maximum_value >= 0
67
+ _, metrics = Gruff::BarValueLabel.metrics(maximum_value, @label_formatting, proc_text_metrics)
68
+ @graph_top += metrics.height
69
+ end
70
+
71
+ @graph_height = @graph_bottom - @graph_top
72
+ end
73
+ end
74
+
49
75
  # Draws a bar graph, but multiple sets are stacked on top of each other.
50
76
  def draw_graph
51
77
  # Setup spacing.
52
78
  #
53
79
  # Columns sit stacked.
54
- bar_width = @graph_width / column_count.to_f
80
+ bar_width = @graph_width / column_count
55
81
  padding = (bar_width * (1 - @bar_spacing)) / 2
56
82
 
57
- height = Array.new(column_count, 0)
58
- stack_bar_value_label = Gruff::BarValueLabel::StackedBar.new
83
+ # Setup the BarConversion Object
84
+ conversion = Gruff::BarConversion.new(
85
+ top: @graph_top, bottom: @graph_bottom,
86
+ minimum_value: minimum_value, maximum_value: maximum_value, spread: @spread
87
+ )
59
88
 
60
- store.norm_data.each_with_index do |data_row, row_index|
61
- data_row.points.each_with_index do |data_point, point_index|
62
- next if data_point == 0
89
+ normalized_stacked_bars.each_with_index do |stacked_bars, stacked_index|
90
+ total = 0
91
+ left_x = @graph_left + (bar_width * stacked_index) + padding
92
+ right_x = left_x + (bar_width * @bar_spacing)
63
93
 
64
- # Use incremented x and scaled y
65
- 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]
94
+ top_y = 0
95
+ stacked_bars.each do |bar|
96
+ next if bar.point == 0
71
97
 
72
- # update the total height of the current stacked bar
73
- height[point_index] += (data_point * @graph_height)
98
+ bottom_y, = conversion.get_top_bottom_scaled(total)
99
+ bottom_y -= @segment_spacing
100
+ top_y, = conversion.get_top_bottom_scaled(total + bar.point)
74
101
 
75
- rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: data_row.color)
76
- rect_renderer.render(left_x, left_y, right_x, right_y)
102
+ rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: bar.color)
103
+ rect_renderer.render(left_x, bottom_y, right_x, top_y)
77
104
 
78
- # Calculate center based on bar_width and current row
79
- label_center = left_x + bar_width * @bar_spacing / 2.0
80
- draw_label(label_center, point_index)
81
-
82
- 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)
105
+ total += bar.point
84
106
  end
85
- end
86
107
 
87
- 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)
108
+ label_center = left_x + (bar_width * @bar_spacing / 2.0)
109
+ draw_label(label_center, stacked_index)
110
+
111
+ if @show_labels_for_bar_values
112
+ bar_value_label = Gruff::BarValueLabel::Bar.new([left_x, top_y, right_x, @graph_bottom], stacked_bars.sum(&:value))
113
+ bar_value_label.prepare_rendering(@label_formatting, proc_text_metrics) do |x, y, text, _text_width, text_height|
114
+ draw_value_label(bar_width * @bar_spacing, text_height, x, y, text)
115
+ end
90
116
  end
91
117
  end
92
118
  end
@@ -96,10 +122,14 @@ private
96
122
  end
97
123
 
98
124
  def hide_left_label_area?
99
- @hide_line_markers
125
+ @hide_line_markers && @y_axis_label.nil?
100
126
  end
101
127
 
102
128
  def hide_bottom_label_area?
103
- hide_labels?
129
+ hide_labels? && @x_axis_label.nil? && @legend_at_bottom == false
130
+ end
131
+
132
+ def proc_text_metrics
133
+ ->(text) { text_metrics(@marker_font, text) }
104
134
  end
105
135
  end
@@ -9,6 +9,8 @@ module Gruff
9
9
  super(label.to_s, Array(y_points), color, x_points)
10
10
  end
11
11
 
12
+ alias points y_points
13
+
12
14
  def x_points
13
15
  self[:x_points] || Array.new(y_points.length)
14
16
  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.14.0'
4
+ VERSION = '0.17.0'
5
5
  end
data/lib/gruff.rb CHANGED
@@ -1,62 +1,71 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rmagick'
4
- require 'gruff/version'
5
-
6
- # Extra full path added to fix loading errors on some installations.
7
-
8
- %w[
9
- patch/rmagick
10
- patch/string
11
-
12
- font
13
- base
14
-
15
- helper/bar_conversion.rb
16
- helper/stacked_mixin
17
- helper/bar_value_label
18
-
19
- themes
20
- area
21
- bar
22
- bezier
23
- bullet
24
- dot
25
- histogram
26
- line
27
- net
28
- pie
29
- scatter
30
- spider
31
- side_bar
32
- stacked_area
33
- stacked_bar
34
- side_stacked_bar
35
- accumulator_bar
36
-
37
- scene
38
-
39
- renderer/renderer
40
- renderer/rectangle
41
- renderer/circle
42
- renderer/dash_line
43
- renderer/line
44
- renderer/polyline
45
- renderer/polygon
46
- renderer/bezier
47
- renderer/ellipse
48
- renderer/dot
49
- renderer/text
50
-
51
- store/store
52
- store/basic_data
53
- store/custom_data
54
- store/xy_data
55
-
56
- mini/legend
57
- mini/bar
58
- mini/pie
59
- mini/side_bar
60
- ].each do |filename|
61
- require "gruff/#{filename}"
4
+
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
+
13
+ ##
14
+ # = Gruff. Graphs.
15
+ #
16
+ module Gruff
17
+ # @private
18
+ def self.libpath(path)
19
+ File.join(__dir__, 'gruff', path)
20
+ end
21
+
22
+ autoload :BarConversion, Gruff.libpath('helper/bar_conversion')
23
+ autoload :BarValueLabel, Gruff.libpath('helper/bar_value_label')
24
+
25
+ autoload :AccumulatorBar, Gruff.libpath('accumulator_bar')
26
+ autoload :Area, Gruff.libpath('area')
27
+ autoload :Bar, Gruff.libpath('bar')
28
+ autoload :Bezier, Gruff.libpath('bezier')
29
+ autoload :BoxPlot, Gruff.libpath('box_plot')
30
+ autoload :Bullet, Gruff.libpath('bullet')
31
+ autoload :Candlestick, Gruff.libpath('candlestick')
32
+ autoload :Dot, Gruff.libpath('dot')
33
+ autoload :Histogram, Gruff.libpath('histogram')
34
+ autoload :Line, Gruff.libpath('line')
35
+ autoload :Net, Gruff.libpath('net')
36
+ autoload :Pie, Gruff.libpath('pie')
37
+ autoload :Scatter, Gruff.libpath('scatter')
38
+ autoload :SideBar, Gruff.libpath('side_bar')
39
+ autoload :SideStackedBar, Gruff.libpath('side_stacked_bar')
40
+ autoload :Spider, Gruff.libpath('spider')
41
+ autoload :StackedArea, Gruff.libpath('stacked_area')
42
+ autoload :StackedBar, Gruff.libpath('stacked_bar')
43
+
44
+ autoload :Layer, Gruff.libpath('scene')
45
+ autoload :Themes, Gruff.libpath('themes')
46
+
47
+ class Renderer
48
+ autoload :Bezier, Gruff.libpath('renderer/bezier')
49
+ autoload :Circle, Gruff.libpath('renderer/circle')
50
+ autoload :DashLine, Gruff.libpath('renderer/dash_line')
51
+ autoload :Dot, Gruff.libpath('renderer/dot')
52
+ autoload :Ellipse, Gruff.libpath('renderer/ellipse')
53
+ autoload :Line, Gruff.libpath('renderer/line')
54
+ autoload :Polygon, Gruff.libpath('renderer/polygon')
55
+ autoload :Polyline, Gruff.libpath('renderer/polyline')
56
+ autoload :Rectangle, Gruff.libpath('renderer/rectangle')
57
+ autoload :Text, Gruff.libpath('renderer/text')
58
+ end
59
+
60
+ class Store
61
+ autoload :BasicData, Gruff.libpath('store/basic_data')
62
+ autoload :XYData, Gruff.libpath('store/xy_data')
63
+ end
64
+
65
+ module Mini
66
+ autoload :Bar, Gruff.libpath('mini/bar')
67
+ autoload :Legend, Gruff.libpath('mini/legend')
68
+ autoload :Pie, Gruff.libpath('mini/pie')
69
+ autoload :SideBar, Gruff.libpath('mini/side_bar')
70
+ end
62
71
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gruff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geoffrey Grosenbach
8
8
  - Uwe Kubosch
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-07-11 00:00:00.000000000 Z
12
+ date: 2022-06-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rmagick
@@ -17,56 +17,56 @@ dependencies:
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: '0'
20
+ version: '4.2'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
- version: '0'
27
+ version: '4.2'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: rubocop
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: 1.12.1
34
+ version: 1.28.2
35
35
  type: :development
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: 1.12.1
41
+ version: 1.28.2
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: rubocop-performance
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - "~>"
47
47
  - !ruby/object:Gem::Version
48
- version: 1.10.2
48
+ version: 1.13.3
49
49
  type: :development
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - "~>"
54
54
  - !ruby/object:Gem::Version
55
- version: 1.10.2
55
+ version: 1.13.3
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: rubocop-rake
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
60
  - - "~>"
61
61
  - !ruby/object:Gem::Version
62
- version: 0.5.1
62
+ version: 0.6.0
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
67
  - - "~>"
68
68
  - !ruby/object:Gem::Version
69
- version: 0.5.1
69
+ version: 0.6.0
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: histogram
72
72
  requirement: !ruby/object:Gem::Requirement
@@ -129,14 +129,14 @@ dependencies:
129
129
  requirements:
130
130
  - - "~>"
131
131
  - !ruby/object:Gem::Version
132
- version: 0.9.25
132
+ version: 0.9.28
133
133
  type: :development
134
134
  prerelease: false
135
135
  version_requirements: !ruby/object:Gem::Requirement
136
136
  requirements:
137
137
  - - "~>"
138
138
  - !ruby/object:Gem::Version
139
- version: 0.9.25
139
+ version: 0.9.28
140
140
  description: Beautiful graphs for one or multiple datasets. Can be used on websites
141
141
  or in documents.
142
142
  email: boss@topfunky.com
@@ -149,7 +149,6 @@ files:
149
149
  - ".github/workflows/ci.yml"
150
150
  - ".gitignore"
151
151
  - ".rubocop.yml"
152
- - ".rubocop_todo.yml"
153
152
  - ".yardopts"
154
153
  - CHANGELOG.md
155
154
  - Gemfile
@@ -166,10 +165,13 @@ files:
166
165
  - lib/gruff/bar.rb
167
166
  - lib/gruff/base.rb
168
167
  - lib/gruff/bezier.rb
168
+ - lib/gruff/box_plot.rb
169
169
  - lib/gruff/bullet.rb
170
+ - lib/gruff/candlestick.rb
170
171
  - lib/gruff/dot.rb
171
172
  - lib/gruff/font.rb
172
173
  - lib/gruff/helper/bar_conversion.rb
174
+ - lib/gruff/helper/bar_mixin.rb
173
175
  - lib/gruff/helper/bar_value_label.rb
174
176
  - lib/gruff/helper/stacked_mixin.rb
175
177
  - lib/gruff/histogram.rb
@@ -194,14 +196,12 @@ files:
194
196
  - lib/gruff/renderer/renderer.rb
195
197
  - lib/gruff/renderer/text.rb
196
198
  - lib/gruff/scatter.rb
197
- - lib/gruff/scene.rb
198
199
  - lib/gruff/side_bar.rb
199
200
  - lib/gruff/side_stacked_bar.rb
200
201
  - lib/gruff/spider.rb
201
202
  - lib/gruff/stacked_area.rb
202
203
  - lib/gruff/stacked_bar.rb
203
204
  - lib/gruff/store/basic_data.rb
204
- - lib/gruff/store/custom_data.rb
205
205
  - lib/gruff/store/store.rb
206
206
  - lib/gruff/store/xy_data.rb
207
207
  - lib/gruff/themes.rb
@@ -212,8 +212,9 @@ files:
212
212
  homepage: https://github.com/topfunky/gruff
213
213
  licenses:
214
214
  - MIT
215
- metadata: {}
216
- post_install_message:
215
+ metadata:
216
+ rubygems_mfa_required: 'true'
217
+ post_install_message:
217
218
  rdoc_options: []
218
219
  require_paths:
219
220
  - lib
@@ -221,15 +222,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
221
222
  requirements:
222
223
  - - ">="
223
224
  - !ruby/object:Gem::Version
224
- version: 2.4.0
225
+ version: 2.5.0
225
226
  required_rubygems_version: !ruby/object:Gem::Requirement
226
227
  requirements:
227
228
  - - ">="
228
229
  - !ruby/object:Gem::Version
229
230
  version: '0'
230
231
  requirements: []
231
- rubygems_version: 3.1.4
232
- signing_key:
232
+ rubygems_version: 3.3.7
233
+ signing_key:
233
234
  specification_version: 4
234
235
  summary: Beautiful graphs for one or multiple datasets.
235
236
  test_files: []
data/.rubocop_todo.yml DELETED
@@ -1,116 +0,0 @@
1
- # This configuration was generated by
2
- # `rubocop --auto-gen-config`
3
- # on 2021-07-11 06:52:14 UTC using RuboCop version 1.12.1.
4
- # The point is for the user to remove these configuration records
5
- # one by one as the offenses are removed from the code base.
6
- # Note that changes in the inspected code, or installation of new
7
- # versions of RuboCop, may require this file to be generated again.
8
-
9
- # Offense count: 13
10
- Lint/FloatComparison:
11
- Exclude:
12
- - 'Rakefile'
13
- - 'lib/gruff/base.rb'
14
- - 'lib/gruff/renderer/renderer.rb'
15
- - 'lib/gruff/scatter.rb'
16
-
17
- # Offense count: 1
18
- # Configuration parameters: CheckForMethodsWithNoSideEffects.
19
- Lint/Void:
20
- Exclude:
21
- - 'lib/gruff/patch/rmagick.rb'
22
-
23
- # Offense count: 57
24
- # Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
25
- Metrics/AbcSize:
26
- Max: 66
27
-
28
- # Offense count: 3
29
- # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
30
- # IgnoredMethods: refine
31
- Metrics/BlockLength:
32
- Max: 32
33
-
34
- # Offense count: 14
35
- # Configuration parameters: CountComments, CountAsOne.
36
- Metrics/ClassLength:
37
- Max: 683
38
-
39
- # Offense count: 8
40
- # Configuration parameters: IgnoredMethods.
41
- Metrics/CyclomaticComplexity:
42
- Max: 17
43
-
44
- # Offense count: 134
45
- # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
46
- Metrics/MethodLength:
47
- Max: 81
48
-
49
- # Offense count: 3
50
- # Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
51
- Metrics/ParameterLists:
52
- Max: 7
53
-
54
- # Offense count: 6
55
- # Configuration parameters: IgnoredMethods.
56
- Metrics/PerceivedComplexity:
57
- Max: 19
58
-
59
- # Offense count: 1
60
- # Configuration parameters: EnforcedStyleForLeadingUnderscores.
61
- # SupportedStylesForLeadingUnderscores: disallowed, required, optional
62
- Naming/MemoizedInstanceVariableName:
63
- Exclude:
64
- - 'lib/gruff/scene.rb'
65
-
66
- # Offense count: 6
67
- # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers.
68
- # SupportedStyles: snake_case, normalcase, non_integer
69
- # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339
70
- Naming/VariableNumber:
71
- Exclude:
72
- - 'test/test_bar.rb'
73
- - 'test/test_bezier.rb'
74
- - 'test/test_line.rb'
75
-
76
- # Offense count: 2
77
- # Cop supports --auto-correct.
78
- Performance/StringReplacement:
79
- Exclude:
80
- - 'lib/gruff/scene.rb'
81
-
82
- # Offense count: 1
83
- Rake/Desc:
84
- Exclude:
85
- - 'Rakefile'
86
-
87
- # Offense count: 1
88
- Security/Eval:
89
- Exclude:
90
- - 'test/gruff_test_case.rb'
91
-
92
- # Offense count: 1
93
- Style/CombinableLoops:
94
- Exclude:
95
- - 'test/test_line.rb'
96
-
97
- # Offense count: 2
98
- Style/DocumentDynamicEvalDefinition:
99
- Exclude:
100
- - 'test/gruff_test_case.rb'
101
-
102
- # Offense count: 97
103
- # Configuration parameters: RequireForNonPublicMethods.
104
- Style/DocumentationMethod:
105
- Enabled: false
106
-
107
- # Offense count: 1
108
- Style/MissingRespondToMissing:
109
- Exclude:
110
- - 'lib/gruff/scene.rb'
111
-
112
- # Offense count: 2
113
- # Cop supports --auto-correct.
114
- Style/StringConcatenation:
115
- Exclude:
116
- - 'test/gruff_test_case.rb'