charty 0.2.6 → 0.2.10

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/charty.gemspec +2 -1
  3. data/examples/bar_plot.rb +19 -0
  4. data/examples/box_plot.rb +17 -0
  5. data/examples/scatter_plot.rb +17 -0
  6. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  7. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  8. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  9. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  10. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  11. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  12. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  13. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  14. data/lib/charty.rb +2 -0
  15. data/lib/charty/backends/plotly.rb +127 -24
  16. data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
  17. data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +89 -0
  18. data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
  19. data/lib/charty/backends/pyplot.rb +74 -0
  20. data/lib/charty/backends/unicode_plot.rb +9 -9
  21. data/lib/charty/cache_dir.rb +27 -0
  22. data/lib/charty/iruby_helper.rb +18 -0
  23. data/lib/charty/plot_methods.rb +82 -6
  24. data/lib/charty/plotters.rb +3 -0
  25. data/lib/charty/plotters/abstract_plotter.rb +56 -16
  26. data/lib/charty/plotters/bar_plotter.rb +39 -0
  27. data/lib/charty/plotters/categorical_plotter.rb +9 -1
  28. data/lib/charty/plotters/distribution_plotter.rb +180 -0
  29. data/lib/charty/plotters/histogram_plotter.rb +244 -0
  30. data/lib/charty/plotters/line_plotter.rb +38 -5
  31. data/lib/charty/plotters/scatter_plotter.rb +4 -2
  32. data/lib/charty/statistics.rb +9 -0
  33. data/lib/charty/table.rb +30 -23
  34. data/lib/charty/table_adapters/base_adapter.rb +88 -0
  35. data/lib/charty/table_adapters/daru_adapter.rb +41 -1
  36. data/lib/charty/table_adapters/hash_adapter.rb +59 -1
  37. data/lib/charty/table_adapters/pandas_adapter.rb +49 -1
  38. data/lib/charty/vector.rb +29 -1
  39. data/lib/charty/vector_adapters.rb +16 -0
  40. data/lib/charty/vector_adapters/pandas_adapter.rb +10 -1
  41. data/lib/charty/version.rb +1 -1
  42. metadata +39 -15
@@ -0,0 +1,89 @@
1
+ module Charty
2
+ module Backends
3
+ module PlotlyHelpers
4
+ class NotebookRenderer < HtmlRenderer
5
+ def initialize(use_cdn: false)
6
+ super(use_cdn: use_cdn, full_html: false, requirejs: true)
7
+ @initialized = false
8
+ end
9
+
10
+ def activate
11
+ return if @initialized
12
+
13
+ unless IRubyHelper.iruby_notebook?
14
+ raise "IRuby is unavailable"
15
+ end
16
+
17
+ if @use_cdn
18
+ script = <<~END_SCRIPT % {win_config: window_plotly_config, mathjax_config: mathjax_config}
19
+ <script type="text/javascript">
20
+ %{win_config}
21
+ %{mathjax_config}
22
+ if (typeof require !== 'undefined') {
23
+ require.undef("plotly");
24
+ requirejs.config({
25
+ paths: {
26
+ 'plotly': ['https://cdn.plot.ly/plotly-latest.min']
27
+ }
28
+ });
29
+ require(['plotly'], function (Plotly) {
30
+ window._Plotly = Plotly;
31
+ });
32
+ }
33
+ </script>
34
+ END_SCRIPT
35
+ else
36
+ script = <<~END_SCRIPT % {script: get_plotlyjs, win_config: window_plotly_config, mathjax_config: mathjax_config}
37
+ <script type="text/javascript">
38
+ %{win_config}
39
+ %{mathjax_config}
40
+ if (typeof require !== 'undefined') {
41
+ require.undef("plotly");
42
+ define('plotly', function (require, exports, module) {
43
+ %{script}
44
+ });
45
+ require(['plotly'], function (Plotly) {
46
+ window._Plotly = Plotly;
47
+ });
48
+ }
49
+ </script>
50
+ END_SCRIPT
51
+ end
52
+ IRuby.display(script, mime: "text/html")
53
+ @initialized = true
54
+ nil
55
+ end
56
+
57
+ def render(figure, element_id: nil, post_script: nil)
58
+ ary = Array.try_convert(post_script)
59
+ post_script = ary || [post_script]
60
+ post_script.unshift(<<~END_POST_SCRIPT)
61
+ var gd = document.getElementById('%{plot_id}');
62
+ var x = new MutationObserver(function (mutations, observer) {
63
+ var display = window.getComputedStyle(gd).display;
64
+ if (!display || display === 'none') {
65
+ console.log([gd, 'removed']);
66
+ Plotly.purge(gd);
67
+ observer.disconnect();
68
+ }
69
+ });
70
+
71
+ // Listen for the removal of the full notebook cell
72
+ var notebookContainer = gd.closest('#notebook-container');
73
+ if (notebookContainer) {
74
+ x.observe(notebookContainer, {childList: true});
75
+ }
76
+
77
+ // Listen for the clearing of the current output cell
78
+ var outputEl = gd.closest('.output');
79
+ if (outputEl) {
80
+ x.observe(outputEl, {childList: true});
81
+ }
82
+ END_POST_SCRIPT
83
+
84
+ super(figure, element_id: element_id, post_script: post_script)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,121 @@
1
+ require "date"
2
+ require "json"
3
+ require "time"
4
+
5
+ module Charty
6
+ module Backends
7
+ module PlotlyHelpers
8
+ class PlotlyRenderer
9
+ def render(figure)
10
+ json = JSON.generate(figure, allow_nan: true)
11
+ case json
12
+ when /\b(?:Infinity|NaN)\b/
13
+ visit(figure)
14
+ else
15
+ JSON.load(json)
16
+ end
17
+ end
18
+
19
+ private def visit(obj)
20
+ case obj
21
+ when Integer, String, Symbol, true, false, nil
22
+ obj
23
+
24
+ when Numeric
25
+ visit_float(obj)
26
+
27
+ when Time
28
+ visit_time(obj)
29
+
30
+ when Date
31
+ visit_date(obj)
32
+
33
+ when DateTime
34
+ visit_datetime(obj)
35
+
36
+ when Array
37
+ visit_array(obj)
38
+
39
+ when Hash
40
+ visit_hash(obj)
41
+
42
+ when ->(x) { defined?(Numo::NArray) && obj.is_a?(Numo::NArray) }
43
+ visit_array(obj.to_a)
44
+
45
+ when ->(x) { defined?(NMatrix) && obj.is_a?(NMatrix) }
46
+ visit_array(obj.to_a)
47
+
48
+ when ->(x) { defined?(Numpy::NDArray) && obj.is_a?(Numpy::NDArray) }
49
+ visit_array(obj.to_a)
50
+
51
+ when ->(x) { defined?(PyCall::List) && obj.is_a?(PyCall::List) }
52
+ visit_array(obj.to_a)
53
+
54
+ when ->(x) { defined?(PyCall::Tuple) && obj.is_a?(PyCall::Tuple) }
55
+ visit_array(obj.to_a)
56
+
57
+ when ->(x) { defined?(PyCall::Dict) && obj.is_a?(PyCall::Dict) }
58
+ visit_hash(obj.to_h)
59
+
60
+ when ->(x) { defined?(Pandas::Series) && obj.is_a?(Pandas::Series) }
61
+ visit_array(obj.to_a)
62
+
63
+ else
64
+ str = String.try_convert(obj)
65
+ return str unless str.nil?
66
+
67
+ ary = Array.try_convert(obj)
68
+ return visit_array(ary) unless ary.nil?
69
+
70
+ hsh = Hash.try_convert(obj)
71
+ return visit_hash(hsh) unless hsh.nil?
72
+
73
+ type_error(obj)
74
+ end
75
+ end
76
+
77
+ private def visit_float(obj)
78
+ obj = obj.to_f
79
+ rescue RangeError
80
+ type_error(obj)
81
+ else
82
+ case
83
+ when obj.finite?
84
+ obj
85
+ else
86
+ nil
87
+ end
88
+ end
89
+
90
+ private def visit_time(obj)
91
+ obj.iso8601(6)
92
+ end
93
+
94
+ private def visit_date(obj)
95
+ obj.iso8601(6)
96
+ end
97
+
98
+ private def visit_datetime(obj)
99
+ obj.iso8601(6)
100
+ end
101
+
102
+ private def visit_array(obj)
103
+ obj.map {|x| visit(x) }
104
+ end
105
+
106
+ private def visit_hash(obj)
107
+ obj.map { |key, value|
108
+ [
109
+ key,
110
+ visit(value)
111
+ ]
112
+ }.to_h
113
+ end
114
+
115
+ private def type_error(obj)
116
+ raise TypeError, "Unable to convert to JSON: %p" % obj
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -14,6 +14,7 @@ module Charty
14
14
 
15
15
  def initialize
16
16
  @pyplot = ::Matplotlib::Pyplot
17
+ @default_edgecolor = Colors["white"].to_rgb
17
18
  @default_line_width = ::Matplotlib.rcParams["lines.linewidth"]
18
19
  @default_marker_size = ::Matplotlib.rcParams["lines.markersize"]
19
20
  end
@@ -575,6 +576,47 @@ module Charty
575
576
  min + x * (max - min)
576
577
  end
577
578
 
579
+ def univariate_histogram(hist, name, variable_name, stat,
580
+ alpha, color, key_color, color_mapper,
581
+ multiple, element, fill, shrink)
582
+ mid_points = hist.edges.each_cons(2).map {|a, b| a + (b - a) / 2 }
583
+ orient = variable_name == :x ? :v : :h
584
+ width = shrink * (hist.edges[1] - hist.edges[0])
585
+
586
+ kw = {align: :edge}
587
+
588
+ color = if color.nil?
589
+ key_color.to_rgb
590
+ else
591
+ color_mapper[color].to_rgb
592
+ end
593
+
594
+ alpha = 1r unless fill
595
+
596
+ if fill
597
+ kw[:facecolor] = color.to_rgba(alpha: alpha).to_hex_string
598
+ if multiple == :stack || multiple == :fill || element == :bars
599
+ kw[:edgecolor] = @default_edgecolor.to_hex_string
600
+ else
601
+ kw[:edgecolor] = color.to_hex_string
602
+ end
603
+ elsif element == :bars
604
+ kw.delete(:facecolor)
605
+ kw[:edgecolor] = color.to_rgba(alpha: alpha).to_hex_string
606
+ else
607
+ kw[:color] = color.to_rgba(alpha: alpha).to_hex_string
608
+ end
609
+
610
+ kw[:label] = name unless name.nil?
611
+
612
+ ax = @pyplot.gca
613
+ if orient == :v
614
+ ax.bar(mid_points, hist.weights, width, **kw)
615
+ else
616
+ ax.barh(mid_points, hist.weights, width, **kw)
617
+ end
618
+ end
619
+
578
620
  private def locator_to_legend_entries(locator, limits)
579
621
  vmin, vmax = limits
580
622
  dtype = case vmin
@@ -620,6 +662,10 @@ module Charty
620
662
  end
621
663
  end
622
664
 
665
+ def set_title(title)
666
+ @pyplot.gca.set_title(String(title))
667
+ end
668
+
623
669
  def set_xlabel(label)
624
670
  @pyplot.gca.set_xlabel(String(label))
625
671
  end
@@ -652,6 +698,27 @@ module Charty
652
698
  @pyplot.gca.set_ylim(Float(min), Float(max))
653
699
  end
654
700
 
701
+ def set_xscale(scale)
702
+ scale = check_scale_type(scale, :xscale)
703
+ @pyplot.gca.set_xscale(scale)
704
+ end
705
+
706
+ def set_yscale(scale)
707
+ scale = check_scale_type(scale, :yscale)
708
+ @pyplot.gca.set_yscale(scale)
709
+ end
710
+
711
+ private def check_scale_type(val, name)
712
+ case
713
+ when :linear, :log
714
+ val
715
+ else
716
+ raise ArgumentError,
717
+ "Invalid #{name} type: %p" % val,
718
+ caller
719
+ end
720
+ end
721
+
655
722
  def disable_xaxis_grid
656
723
  @pyplot.gca.xaxis.grid(false)
657
724
  end
@@ -670,6 +737,12 @@ module Charty
670
737
 
671
738
  def render(notebook: false)
672
739
  show
740
+ nil
741
+ end
742
+
743
+ def render_mimebundle(include: [], exclude: [])
744
+ show
745
+ {}
673
746
  end
674
747
 
675
748
  SAVEFIG_OPTIONAL_PARAMS = [
@@ -685,6 +758,7 @@ module Charty
685
758
  params[key] = kwargs[key] if kwargs.key?(key)
686
759
  end
687
760
  @pyplot.savefig(filename, **params)
761
+ @pyplot.close
688
762
  end
689
763
 
690
764
  def show
@@ -16,7 +16,7 @@ module Charty
16
16
  @layout = {}
17
17
  end
18
18
 
19
- def bar(bar_pos, values, color: nil)
19
+ def bar(bar_pos, _group_names, values, colors, _orient, **kwargs)
20
20
  @figure = {
21
21
  type: :bar,
22
22
  bar_pos: bar_pos,
@@ -24,7 +24,7 @@ module Charty
24
24
  }
25
25
  end
26
26
 
27
- def box_plot(plot_data, positions, color:, gray:)
27
+ def box_plot(plot_data, positions, **kwargs)
28
28
  @figure = { type: :box, data: plot_data }
29
29
  end
30
30
 
@@ -52,13 +52,13 @@ module Charty
52
52
  # do nothing
53
53
  end
54
54
 
55
- def show
56
- case @figure[:type]
57
- when :bar
58
- plot = ::UnicodePlot.barplot(@layout[:xtick_labels], @figure[:values], xlabel: @layout[:xlabel])
59
- when :box
60
- plot = ::UnicodePlot.boxplot(@layout[:xtick_labels], @figure[:data], xlabel: @layout[:xlabel])
61
- end
55
+ def render(**kwargs)
56
+ plot = case @figure[:type]
57
+ when :bar
58
+ ::UnicodePlot.barplot(@layout[:xtick_labels], @figure[:values], xlabel: @layout[:xlabel])
59
+ when :box
60
+ ::UnicodePlot.boxplot(@layout[:xtick_labels], @figure[:data], xlabel: @layout[:xlabel])
61
+ end
62
62
  sio = StringIO.new
63
63
  class << sio
64
64
  def tty?; true; end
@@ -0,0 +1,27 @@
1
+ require "pathname"
2
+
3
+ module Charty
4
+ module CacheDir
5
+ module_function
6
+
7
+ def cache_dir_path
8
+ platform_cache_dir_path + "charty"
9
+ end
10
+
11
+ def platform_cache_dir_path
12
+ base_dir = case RUBY_PLATFORM
13
+ when /mswin/, /mingw/
14
+ ENV.fetch("LOCALAPPDATA", "~/AppData/Local")
15
+ when /darwin/
16
+ "~/Library/Caches"
17
+ else
18
+ ENV.fetch("XDG_CACHE_HOME", "~/.cache")
19
+ end
20
+ Pathname(base_dir).expand_path
21
+ end
22
+
23
+ def path(*path_components)
24
+ cache_dir_path.join(*path_components)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module Charty
2
+ module IRubyHelper
3
+ module_function
4
+
5
+ def iruby_notebook?
6
+ # TODO: This cannot distinguish notebook and console.
7
+ defined?(IRuby)
8
+ end
9
+
10
+ def vscode?
11
+ ENV.key?("VSCODE_PID")
12
+ end
13
+
14
+ def nteract?
15
+ ENV.key?("NTERACT_EXE")
16
+ end
17
+ end
18
+ end
@@ -23,19 +23,24 @@ module Charty
23
23
  # @param cap_size Width of the caps on error bars.
24
24
  # @param dodge [true,false] If true, bar position is shifted along the
25
25
  # categorical axis for avoid overlapping when the color-dimension is used.
26
+ # @param log [true,false] Set the value-axis (e.g. Y-axis if orient is :v) to be log scale.
27
+ # @param x_label [String,Symbol,#to_str,nil] X-axis label.
28
+ # @param y_label [String,Symbol,#to_str,nil] Y-axis label.
29
+ # @param title [String,Symbol,#to_str,nil] Title text.
26
30
  def bar_plot(x: nil, y: nil, color: nil, data: nil,
27
31
  order: nil, color_order: nil,
28
32
  estimator: :mean, ci: 95, n_boot: 1000, units: nil, random: nil,
29
33
  orient: nil, key_color: nil, palette: nil, saturation: 1r,
30
34
  error_color: [0.26, 0.26, 0.26], error_width: nil, cap_size: nil,
31
- dodge: true, **options, &block)
35
+ dodge: true, log: false, x_label: nil, y_label: nil, title: nil,
36
+ **options, &block)
32
37
  Plotters::BarPlotter.new(
33
38
  data: data, variables: { x: x, y: y, color: color },
34
39
  order: order, orient: orient,
35
40
  estimator: estimator, ci: ci, n_boot: n_boot, units: units, random: random,
36
41
  color_order: color_order, key_color: key_color, palette: palette, saturation: saturation,
37
42
  error_color: error_color, error_width: error_width, cap_size: cap_size,
38
- dodge: dodge,
43
+ dodge: dodge, log: log, x_label: x_label, y_label: y_label, title: title,
39
44
  **options, &block
40
45
  )
41
46
  end
@@ -43,7 +48,8 @@ module Charty
43
48
  def count_plot(x: nil, y: nil, color: nil, data: nil,
44
49
  order: nil, color_order: nil,
45
50
  orient: nil, key_color: nil, palette: nil, saturation: 1r,
46
- dodge: true, **options, &block)
51
+ dodge: true, log: false, x_label: nil, y_label: nil, title: nil,
52
+ **options, &block)
47
53
  case
48
54
  when x.nil? && !y.nil?
49
55
  x = y
@@ -70,6 +76,9 @@ module Charty
70
76
  palette: palette,
71
77
  saturation: saturation,
72
78
  dodge: dodge,
79
+ x_label: x_label,
80
+ y_label: y_label,
81
+ title: title,
73
82
  **options
74
83
  ) do |plotter|
75
84
  plotter.value_label = "count"
@@ -106,11 +115,15 @@ module Charty
106
115
  # @param whisker Propotion of the IQR past the low and high quartiles to
107
116
  # extend the plot whiskers. Points outside of this range will be
108
117
  # treated as outliers.
118
+ # @param x_label [String,Symbol,#to_str,nil] X-axis label.
119
+ # @param y_label [String,Symbol,#to_str,nil] Y-axis label.
120
+ # @param title [String,Symbol,#to_str,nil] Title text.
109
121
  def box_plot(x: nil, y: nil, color: nil, data: nil,
110
122
  order: nil, color_order: nil,
111
123
  orient: nil, key_color: nil, palette: nil, saturation: 1r,
112
124
  width: 0.8r, dodge: true, flier_size: 5, line_width: nil,
113
- whisker: 1.5, **options, &block)
125
+ whisker: 1.5, x_label: nil, y_label: nil, title: nil,
126
+ **options, &block)
114
127
  Plotters::BoxPlotter.new(
115
128
  data: data,
116
129
  variables: { x: x, y: y, color: color },
@@ -125,6 +138,9 @@ module Charty
125
138
  flier_size: flier_size,
126
139
  line_width: line_width,
127
140
  whisker: whisker,
141
+ x_label: x_label,
142
+ y_label: y_label,
143
+ title: title,
128
144
  **options,
129
145
  &block
130
146
  )
@@ -164,13 +180,18 @@ module Charty
164
180
  # :full, every group will get an entry in the legend. If :auto,
165
181
  # choose between brief or full representation based on number of
166
182
  # levels. If false, no legend data is added and no legend is drawn.
183
+ # @param x_label [String,Symbol,#to_str,nil] X-axis label.
184
+ # @param y_label [String,Symbol,#to_str,nil] Y-axis label.
185
+ # @param title [String,Symbol,#to_str,nil] Title text.
167
186
  def line_plot(x: nil, y: nil, color: nil, style: nil, size: nil,
168
187
  data: nil, key_color: nil, palette: nil, color_order: nil,
169
188
  color_norm: nil, sizes: nil, size_order: nil, size_norm: nil,
170
189
  markers: nil, dashes: true, style_order: nil,
171
190
  units: nil, estimator: :mean, n_boot: 1000, random: nil,
172
191
  sort: true, err_style: :band, err_params: nil, error_bar: [:ci, 95],
173
- x_scale: :linear, y_scale: :linear, legend: :auto, **options, &block)
192
+ x_scale: :linear, y_scale: :linear, legend: :auto,
193
+ x_label: nil, y_label: nil, title: nil,
194
+ **options, &block)
174
195
  Plotters::LinePlotter.new(
175
196
  data: data,
176
197
  variables: { x: x, y: y, color: color, style: style, size: size },
@@ -195,6 +216,9 @@ module Charty
195
216
  x_scale: x_scale,
196
217
  y_scale: y_scale,
197
218
  legend: legend,
219
+ x_label: x_label,
220
+ y_label: y_label,
221
+ title: title,
198
222
  **options,
199
223
  &block
200
224
  )
@@ -225,11 +249,14 @@ module Charty
225
249
  # :full, every group will get an entry in the legend. If :auto,
226
250
  # choose between brief or full representation based on number of
227
251
  # levels. If false, no legend data is added and no legend is drawn.
252
+ # @param x_label [String,Symbol,#to_str,nil] X-axis label.
253
+ # @param y_label [String,Symbol,#to_str,nil] Y-axis label.
254
+ # @param title [String,Symbol,#to_str,nil] Title text.
228
255
  def scatter_plot(x: nil, y: nil, color: nil, style: nil, size: nil,
229
256
  data: nil, key_color: nil, palette: nil, color_order: nil,
230
257
  color_norm: nil, sizes: nil, size_order: nil, size_norm: nil,
231
258
  markers: true, style_order: nil, alpha: nil, legend: :auto,
232
- **options, &block)
259
+ x_label: nil, y_label: nil, title: nil, **options, &block)
233
260
  Plotters::ScatterPlotter.new(
234
261
  data: data,
235
262
  variables: { x: x, y: y, color: color, style: style, size: size },
@@ -244,10 +271,59 @@ module Charty
244
271
  style_order: style_order,
245
272
  alpha: alpha,
246
273
  legend: legend,
274
+ x_label: x_label,
275
+ y_label: y_label,
276
+ title: title,
247
277
  **options,
248
278
  &block
249
279
  )
250
280
  end
281
+
282
+ def hist_plot(data: nil, x: nil, y: nil, color: nil, weights: nil,
283
+ stat: :count, bins: :auto,
284
+ bin_range: nil, common_bins: true,
285
+ key_color: nil, palette: nil, color_order: nil, color_norm: nil,
286
+ legend: true, x_label: nil, y_label: nil, title: nil,
287
+ **options, &block)
288
+ # TODO: support following arguments
289
+ # - wiehgts
290
+ # - binwidth
291
+ # - discrete
292
+ # - cumulative
293
+ # - common_norm
294
+ # - multiple
295
+ # - element
296
+ # - fill
297
+ # - shrink
298
+ # - kde
299
+ # - kde_params
300
+ # - line_params
301
+ # - thresh
302
+ # - pthresh
303
+ # - pmax
304
+ # - cbar
305
+ # - cbar_params
306
+ # - x_log_scale
307
+ # - y_log_scale
308
+ Plotters::HistogramPlotter.new(
309
+ data: data,
310
+ variables: { x: x, y: y, color: color },
311
+ weights: weights,
312
+ stat: stat,
313
+ bins: bins,
314
+ bin_range: bin_range,
315
+ common_bins: common_bins,
316
+ key_color: key_color,
317
+ palette: palette,
318
+ color_order: color_order,
319
+ color_norm: color_norm,
320
+ legend: legend,
321
+ x_label: x_label,
322
+ y_label: y_label,
323
+ title: title,
324
+ **options,
325
+ &block)
326
+ end
251
327
  end
252
328
 
253
329
  extend PlotMethods