charty 0.2.1 → 0.2.7
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 +4 -4
- data/.github/workflows/ci.yml +56 -23
- data/.github/workflows/nmatrix.yml +67 -0
- data/.github/workflows/pycall.yml +86 -0
- data/Dockerfile.dev +9 -1
- data/Gemfile +18 -0
- data/README.md +177 -9
- data/Rakefile +4 -5
- data/charty.gemspec +10 -5
- data/examples/palette.rb +1 -1
- data/examples/sample_images/hist_gruff.png +0 -0
- data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
- data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
- data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
- data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
- data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
- data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
- data/lib/charty.rb +9 -2
- data/lib/charty/backends.rb +1 -0
- data/lib/charty/backends/bokeh.rb +2 -2
- data/lib/charty/backends/google_charts.rb +1 -1
- data/lib/charty/backends/gruff.rb +14 -3
- data/lib/charty/backends/plotly.rb +731 -32
- data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
- data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +86 -0
- data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
- data/lib/charty/backends/pyplot.rb +515 -67
- data/lib/charty/backends/rubyplot.rb +1 -1
- data/lib/charty/backends/unicode_plot.rb +79 -0
- data/lib/charty/cache_dir.rb +27 -0
- data/lib/charty/dash_pattern_generator.rb +57 -0
- data/lib/charty/index.rb +213 -0
- data/lib/charty/iruby_helper.rb +18 -0
- data/lib/charty/linspace.rb +1 -1
- data/lib/charty/plot_methods.rb +283 -8
- data/lib/charty/plotter.rb +2 -2
- data/lib/charty/plotters.rb +11 -0
- data/lib/charty/plotters/abstract_plotter.rb +188 -18
- data/lib/charty/plotters/bar_plotter.rb +189 -7
- data/lib/charty/plotters/box_plotter.rb +64 -11
- data/lib/charty/plotters/categorical_plotter.rb +272 -40
- data/lib/charty/plotters/count_plotter.rb +7 -0
- data/lib/charty/plotters/distribution_plotter.rb +143 -0
- data/lib/charty/plotters/estimation_support.rb +84 -0
- data/lib/charty/plotters/histogram_plotter.rb +182 -0
- data/lib/charty/plotters/line_plotter.rb +300 -0
- data/lib/charty/plotters/random_support.rb +25 -0
- data/lib/charty/plotters/relational_plotter.rb +635 -0
- data/lib/charty/plotters/scatter_plotter.rb +80 -0
- data/lib/charty/plotters/vector_plotter.rb +6 -0
- data/lib/charty/statistics.rb +96 -2
- data/lib/charty/table.rb +160 -15
- data/lib/charty/table_adapters.rb +2 -0
- data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
- data/lib/charty/table_adapters/base_adapter.rb +166 -0
- data/lib/charty/table_adapters/daru_adapter.rb +39 -3
- data/lib/charty/table_adapters/datasets_adapter.rb +13 -2
- data/lib/charty/table_adapters/hash_adapter.rb +141 -16
- data/lib/charty/table_adapters/narray_adapter.rb +25 -6
- data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
- data/lib/charty/table_adapters/pandas_adapter.rb +163 -0
- data/lib/charty/util.rb +28 -0
- data/lib/charty/vector.rb +69 -0
- data/lib/charty/vector_adapters.rb +187 -0
- data/lib/charty/vector_adapters/array_adapter.rb +101 -0
- data/lib/charty/vector_adapters/daru_adapter.rb +163 -0
- data/lib/charty/vector_adapters/narray_adapter.rb +182 -0
- data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
- data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
- data/lib/charty/vector_adapters/pandas_adapter.rb +199 -0
- data/lib/charty/version.rb +1 -1
- metadata +105 -24
- data/lib/charty/palette.rb +0 -235
@@ -0,0 +1,203 @@
|
|
1
|
+
require "datasets/downloader"
|
2
|
+
require "json"
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module Charty
|
6
|
+
module Backends
|
7
|
+
module PlotlyHelpers
|
8
|
+
class HtmlRenderer
|
9
|
+
def initialize(use_cdn: true,
|
10
|
+
full_html: false,
|
11
|
+
requirejs: true)
|
12
|
+
@use_cdn = use_cdn
|
13
|
+
@full_html = full_html
|
14
|
+
@requirejs = requirejs
|
15
|
+
end
|
16
|
+
|
17
|
+
PLOTLY_URL = "https://plot.ly".freeze
|
18
|
+
PLOTLY_LATEST_CDN_URL = "https://cdn.plot.ly/plotly-latest.min.js".freeze
|
19
|
+
MATHJAX_CDN_URL = ("https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js").freeze
|
20
|
+
|
21
|
+
DEFAULT_WIDTH = "100%".freeze
|
22
|
+
DEFAULT_HEIGHT = 525
|
23
|
+
|
24
|
+
def render(figure, element_id: nil, post_script: nil)
|
25
|
+
element_id = SecureRandom.uuid if element_id.nil?
|
26
|
+
plotly_html_div = build_plotly_html_div(figure, element_id, post_script)
|
27
|
+
|
28
|
+
if @full_html
|
29
|
+
<<~END_HTML % {div: plotly_html_div}
|
30
|
+
<!DOCTYPE html>
|
31
|
+
<html>
|
32
|
+
<head><meta charset="utf-8" /></head>
|
33
|
+
<body>
|
34
|
+
%{div}
|
35
|
+
</body>
|
36
|
+
</html>
|
37
|
+
END_HTML
|
38
|
+
else
|
39
|
+
plotly_html_div
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private def build_plotly_html_div(figure, element_id, post_script)
|
44
|
+
layout = figure.fetch(:layout, {})
|
45
|
+
|
46
|
+
json_data = JSON.dump(figure.fetch(:data, []))
|
47
|
+
json_layout = JSON.dump(layout)
|
48
|
+
json_frames = JSON.dump(figure[:frames]) if figure.key?(:frames)
|
49
|
+
|
50
|
+
# TODO: config and responsive support
|
51
|
+
|
52
|
+
template = layout.fetch(:template, {}).fetch(:layout, {})
|
53
|
+
div_width = layout.fetch(:width, template.fetch(:width, DEFAULT_WIDTH))
|
54
|
+
div_height = layout.fetch(:height, template.fetch(:height, DEFAULT_HEIGHT))
|
55
|
+
|
56
|
+
div_width = "#{div_width}px" if Float(div_width, exception: false)
|
57
|
+
div_height = "#{div_height}px" if Float(div_height, exception: false)
|
58
|
+
|
59
|
+
# TODO: showLink and showSendToCloud support
|
60
|
+
base_url_line = "window.PLOTLYENV.BASE_URL = '%{url}';" % {url: PLOTLY_URL}
|
61
|
+
|
62
|
+
## build script body
|
63
|
+
|
64
|
+
# TODO: post_script support
|
65
|
+
then_post_script = ""
|
66
|
+
if post_script
|
67
|
+
ary = Array.try_convert(post_script)
|
68
|
+
post_script = ary || [post_script]
|
69
|
+
post_script.each do |ps|
|
70
|
+
next if ps.nil?
|
71
|
+
then_post_script << '.then(function(){ %{post_script} })' % {
|
72
|
+
post_script: ps % {plot_id: element_id}
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
then_addframes = ""
|
78
|
+
then_animate = ""
|
79
|
+
if json_frames
|
80
|
+
then_addframes = <<~END_ADDFRAMES % {id: element_id, frames: json_frames}
|
81
|
+
.then(function(){
|
82
|
+
Plotly.addFrames('%{id}', {frames});
|
83
|
+
})
|
84
|
+
END_ADDFRAMES
|
85
|
+
|
86
|
+
# TODO: auto_play support
|
87
|
+
end
|
88
|
+
|
89
|
+
json_config = JSON.dump({}) # TODO: config support
|
90
|
+
|
91
|
+
script = <<~END_SCRIPT
|
92
|
+
if (document.getElementById("%{id}")) {
|
93
|
+
Plotly.newPlot("%{id}", %{data}, %{layout}, %{config})%{then_addframes}%{then_animate}%{then_post_script};
|
94
|
+
}
|
95
|
+
END_SCRIPT
|
96
|
+
script = script % {
|
97
|
+
id: element_id,
|
98
|
+
data: json_data,
|
99
|
+
layout: json_layout,
|
100
|
+
config: json_config,
|
101
|
+
then_addframes: then_addframes,
|
102
|
+
then_animate: then_animate,
|
103
|
+
then_post_script: then_post_script
|
104
|
+
}
|
105
|
+
|
106
|
+
## Handle loading/initializing plotlyjs
|
107
|
+
|
108
|
+
case
|
109
|
+
when @requirejs
|
110
|
+
include_plotlyjs = :require
|
111
|
+
include_mathjax = false
|
112
|
+
when @use_cdn
|
113
|
+
include_plotlyjs = :cdn
|
114
|
+
include_mathjax = :cdn
|
115
|
+
else
|
116
|
+
include_plotlyjs = true
|
117
|
+
include_mathjax = :cdn
|
118
|
+
end
|
119
|
+
|
120
|
+
case include_plotlyjs
|
121
|
+
when :require
|
122
|
+
require_start = 'require(["plotly"], function (Plotly) {'
|
123
|
+
require_end = '});'
|
124
|
+
when :cdn
|
125
|
+
load_plotlyjs = <<~END_LOAD_PLOTLYJS % {win_config: window_plotly_config, url: PLOTLY_LATEST_CDN_URL}
|
126
|
+
%{win_config}
|
127
|
+
<script src="%{url}"></script>
|
128
|
+
END_LOAD_PLOTLYJS
|
129
|
+
when true
|
130
|
+
load_plotlyjs = <<~END_LOAD_PLOTLYJS % {win_config: window_plotly_config, script: get_plotlyjs}
|
131
|
+
%{win_config}
|
132
|
+
<script type="text/javascript">%{script}</script>
|
133
|
+
END_LOAD_PLOTLYJS
|
134
|
+
end
|
135
|
+
|
136
|
+
## Handle loading/initializing MathJax
|
137
|
+
|
138
|
+
mathjax_tmplate = %Q[<script src="%{url}?config=TeX-AMS-MML_SVG"></script>]
|
139
|
+
case include_mathjax
|
140
|
+
when :cdn
|
141
|
+
mathjax_script = mathjax_tmplate % {url: MATHJAX_CDN_URL}
|
142
|
+
mathjax_script << <<~END_SCRIPT % {mathjax_config: mathjax_config}
|
143
|
+
<script type="text/javascript">%{mathjax_config}</script>
|
144
|
+
END_SCRIPT
|
145
|
+
else
|
146
|
+
mathjax_script = ""
|
147
|
+
end
|
148
|
+
|
149
|
+
div_template = <<~END_DIV
|
150
|
+
<div>
|
151
|
+
%{mathjax_script}
|
152
|
+
%{load_plotlyjs}
|
153
|
+
<div id="%{id}" class="plotly-graph-div" style="height: %{height}; width: %{width};"></div>
|
154
|
+
<script type="text/javascript">
|
155
|
+
%{require_start}
|
156
|
+
window.PLOTLYENV = window.PLOTLYENV || {};
|
157
|
+
%{base_url_line}
|
158
|
+
%{script}
|
159
|
+
%{require_end}
|
160
|
+
</script>
|
161
|
+
</div>
|
162
|
+
END_DIV
|
163
|
+
|
164
|
+
plotly_html_div = div_template % {
|
165
|
+
mathjax_script: mathjax_script,
|
166
|
+
load_plotlyjs: load_plotlyjs,
|
167
|
+
id: element_id,
|
168
|
+
height: div_height,
|
169
|
+
width: div_width,
|
170
|
+
require_start: require_start,
|
171
|
+
base_url_line: base_url_line,
|
172
|
+
script: script,
|
173
|
+
require_end: require_end
|
174
|
+
}
|
175
|
+
plotly_html_div.strip!
|
176
|
+
|
177
|
+
plotly_html_div
|
178
|
+
end
|
179
|
+
|
180
|
+
private def window_plotly_config
|
181
|
+
%Q(window.PlotlyConfig = {MathJaxConfig: 'local'};)
|
182
|
+
end
|
183
|
+
|
184
|
+
private def mathjax_config
|
185
|
+
%Q(if (window.MathJax) { MathJax.Hub.Config({SVG: {font: "STIX-Web"}}); })
|
186
|
+
end
|
187
|
+
|
188
|
+
private def get_plotlyjs
|
189
|
+
cache_path = CacheDir.path("plotly.min.js")
|
190
|
+
unless cache_path.exist?
|
191
|
+
download_plotlyjs(cache_path)
|
192
|
+
end
|
193
|
+
cache_path.read
|
194
|
+
end
|
195
|
+
|
196
|
+
private def download_plotlyjs(output_path)
|
197
|
+
downloader = Datasets::Downloader.new(PLOTLY_LATEST_CDN_URL)
|
198
|
+
downloader.download(output_path)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,86 @@
|
|
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
|
+
end
|
53
|
+
|
54
|
+
def render(figure, element_id: nil, post_script: nil)
|
55
|
+
ary = Array.try_convert(post_script)
|
56
|
+
post_script = ary || [post_script]
|
57
|
+
post_script.unshift(<<~END_POST_SCRIPT)
|
58
|
+
var gd = document.getElementById('%{plot_id}');
|
59
|
+
var x = new MutationObserver(function (mutations, observer) {
|
60
|
+
var display = window.getComputedStyle(gd).display;
|
61
|
+
if (!display || display === 'none') {
|
62
|
+
console.log([gd, 'removed']);
|
63
|
+
Plotly.purge(gd);
|
64
|
+
observer.disconnect();
|
65
|
+
}
|
66
|
+
});
|
67
|
+
|
68
|
+
// Listen for the removal of the full notebook cell
|
69
|
+
var notebookContainer = gd.closest('#notebook-container');
|
70
|
+
if (notebookContainer) {
|
71
|
+
x.observe(notebookContainer, {childList: true});
|
72
|
+
}
|
73
|
+
|
74
|
+
// Listen for the clearing of the current output cell
|
75
|
+
var outputEl = gd.closest('.output');
|
76
|
+
if (outputEl) {
|
77
|
+
x.observe(outputEl, {childList: true});
|
78
|
+
}
|
79
|
+
END_POST_SCRIPT
|
80
|
+
|
81
|
+
super(figure, element_id: element_id, post_script: post_script)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
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
|
@@ -8,11 +8,14 @@ module Charty
|
|
8
8
|
class << self
|
9
9
|
def prepare
|
10
10
|
require 'matplotlib/pyplot'
|
11
|
+
require 'numpy'
|
11
12
|
end
|
12
13
|
end
|
13
14
|
|
14
15
|
def initialize
|
15
16
|
@pyplot = ::Matplotlib::Pyplot
|
17
|
+
@default_line_width = ::Matplotlib.rcParams["lines.linewidth"]
|
18
|
+
@default_marker_size = ::Matplotlib.rcParams["lines.markersize"]
|
16
19
|
end
|
17
20
|
|
18
21
|
def self.activate_iruby_integration
|
@@ -38,7 +41,7 @@ module Charty
|
|
38
41
|
@pyplot.show
|
39
42
|
end
|
40
43
|
|
41
|
-
def
|
44
|
+
def old_style_render(context, filename)
|
42
45
|
plot(@pyplot, context)
|
43
46
|
if filename
|
44
47
|
FileUtils.mkdir_p(File.dirname(filename))
|
@@ -47,7 +50,7 @@ module Charty
|
|
47
50
|
@pyplot.show
|
48
51
|
end
|
49
52
|
|
50
|
-
def
|
53
|
+
def old_style_save(context, filename, finish: true)
|
51
54
|
plot(context)
|
52
55
|
if filename
|
53
56
|
FileUtils.mkdir_p(File.dirname(filename))
|
@@ -73,7 +76,7 @@ module Charty
|
|
73
76
|
ax.ylabel(context.ylabel) if context.ylabel
|
74
77
|
end
|
75
78
|
|
76
|
-
palette =
|
79
|
+
palette = Palette.default
|
77
80
|
colors = palette.colors.map {|c| c.to_rgb.to_hex_string }.cycle
|
78
81
|
case context.method
|
79
82
|
when :bar
|
@@ -90,7 +93,16 @@ module Charty
|
|
90
93
|
min_l = palette.colors.map {|c| c.to_rgb.to_hsl.l }.min
|
91
94
|
lum = min_l*0.6
|
92
95
|
gray = Colors::RGB.new(lum, lum, lum).to_hex_string
|
93
|
-
|
96
|
+
Array(context.data).each_with_index do |group_data, i|
|
97
|
+
next if group_data.empty?
|
98
|
+
|
99
|
+
box_data = group_data.compact
|
100
|
+
next if box_data.empty?
|
101
|
+
|
102
|
+
color = colors.next
|
103
|
+
draw_box_plot(box_data, vert: "v", position: i, color: color,
|
104
|
+
gray: gray, width: 0.8, whisker: 1.5, flier_size: 5)
|
105
|
+
end
|
94
106
|
when :bubble
|
95
107
|
context.series.each do |data|
|
96
108
|
ax.scatter(data.xs.to_a, data.ys.to_a, s: data.zs.to_a, alpha: 0.5,
|
@@ -125,93 +137,485 @@ module Charty
|
|
125
137
|
end
|
126
138
|
end
|
127
139
|
|
128
|
-
private def draw_box_plot(context, subplot, colors, gray)
|
129
|
-
Array(context.data).each_with_index do |group_data, i|
|
130
|
-
next if group_data.empty?
|
131
|
-
|
132
|
-
box_data = group_data.compact
|
133
|
-
next if box_data.empty?
|
134
|
-
|
135
|
-
artist_dict = @pyplot.boxplot(box_data, vert: "v", patch_artist: true,
|
136
|
-
positions: [i], widths: 0.8)
|
137
|
-
|
138
|
-
color = colors.next
|
139
|
-
artist_dict["boxes"].each do |box|
|
140
|
-
box.update({facecolor: color, zorder: 0.9, edgecolor: gray}, {})
|
141
|
-
end
|
142
|
-
artist_dict["whiskers"].each do |whisker|
|
143
|
-
whisker.update({color: gray, linestyle: "-"}, {})
|
144
|
-
end
|
145
|
-
artist_dict["caps"].each do |cap|
|
146
|
-
cap.update({color: gray}, {})
|
147
|
-
end
|
148
|
-
artist_dict["medians"].each do |median|
|
149
|
-
median.update({color: gray}, {})
|
150
|
-
end
|
151
|
-
artist_dict["fliers"].each do |flier|
|
152
|
-
flier.update({
|
153
|
-
markerfacecolor: gray,
|
154
|
-
marker: "d",
|
155
|
-
markeredgecolor: gray,
|
156
|
-
markersize: 5
|
157
|
-
}, {})
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
140
|
# ==== NEW PLOTTING API ====
|
163
141
|
|
164
142
|
def begin_figure
|
165
|
-
|
143
|
+
@legend_keys = []
|
144
|
+
@legend_labels = []
|
166
145
|
end
|
167
146
|
|
168
|
-
def bar(bar_pos, values,
|
147
|
+
def bar(bar_pos, _group_names, values, colors, orient, label: nil, width: 0.8r,
|
148
|
+
align: :center, conf_int: nil, error_colors: nil, error_width: nil, cap_size: nil)
|
169
149
|
bar_pos = Array(bar_pos)
|
170
150
|
values = Array(values)
|
171
|
-
|
151
|
+
colors = Array(colors).map(&:to_hex_string)
|
172
152
|
width = Float(width)
|
153
|
+
|
154
|
+
ax = @pyplot.gca
|
155
|
+
kw = {color: colors, align: align}
|
156
|
+
kw[:label] = label unless label.nil?
|
157
|
+
|
173
158
|
if orient == :v
|
174
|
-
|
159
|
+
ax.bar(bar_pos, values, width, **kw)
|
175
160
|
else
|
176
|
-
|
161
|
+
ax.barh(bar_pos, values, width, **kw)
|
162
|
+
end
|
163
|
+
|
164
|
+
if conf_int
|
165
|
+
error_colors = Array(error_colors).map(&:to_hex_string)
|
166
|
+
confidence_intervals(ax, bar_pos, conf_int, orient, error_colors, error_width, cap_size)
|
177
167
|
end
|
178
168
|
end
|
179
169
|
|
180
|
-
def
|
181
|
-
|
182
|
-
|
170
|
+
private def confidence_intervals(ax, at_group, conf_int, orient, colors, error_width=nil, cap_size=nil, **options)
|
171
|
+
options[:lw] = error_width || @default_line_width * 1.8
|
172
|
+
|
173
|
+
at_group.each_index do |i|
|
174
|
+
at = at_group[i]
|
175
|
+
ci_low, ci_high = conf_int[i]
|
176
|
+
color = colors[i]
|
177
|
+
|
178
|
+
if orient == :v
|
179
|
+
ax.plot([at, at], [ci_low, ci_high], color: color, **options)
|
180
|
+
unless cap_size.nil?
|
181
|
+
ax.plot([at - cap_size / 2.0, at + cap_size / 2.0], [ci_low, ci_low], color: color, **options)
|
182
|
+
ax.plot([at - cap_size / 2.0, at + cap_size / 2.0], [ci_high, ci_high], color: color, **options)
|
183
|
+
end
|
184
|
+
else
|
185
|
+
ax.plot([ci_low, ci_high], [at, at], color: color, **options)
|
186
|
+
unless cap_size.nil?
|
187
|
+
ax.plot([ci_low, ci_low], [at - cap_size / 2.0, at + cap_size / 2.0], color: color, **options)
|
188
|
+
ax.plot([ci_high, ci_high], [at - cap_size / 2.0, at + cap_size / 2.0], color: color, **options)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def box_plot(plot_data, group_names,
|
195
|
+
orient:, colors:, gray:, dodge:, width: 0.8r,
|
196
|
+
flier_size: 5, whisker: 1.5, notch: false)
|
197
|
+
colors = Array(colors).map(&:to_hex_string)
|
183
198
|
gray = gray.to_hex_string
|
184
199
|
width = Float(width)
|
185
200
|
flier_size = Float(flier_size)
|
186
201
|
whisker = Float(whisker)
|
202
|
+
|
187
203
|
plot_data.each_with_index do |group_data, i|
|
188
|
-
|
204
|
+
unless group_data.nil?
|
205
|
+
draw_box_plot(group_data,
|
206
|
+
vert: (orient == :v),
|
207
|
+
position: i,
|
208
|
+
color: colors[i],
|
209
|
+
gray: gray,
|
210
|
+
width: width,
|
211
|
+
whisker: whisker,
|
212
|
+
flier_size: flier_size)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
189
216
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
217
|
+
def grouped_box_plot(plot_data, group_names, color_names,
|
218
|
+
orient:, colors:, gray:, dodge:, width: 0.8r,
|
219
|
+
flier_size: 5, whisker: 1.5, notch: false)
|
220
|
+
colors = Array(colors).map(&:to_hex_string)
|
221
|
+
gray = gray.to_hex_string
|
222
|
+
width = Float(width)
|
223
|
+
flier_size = Float(flier_size)
|
224
|
+
whisker = Float(whisker)
|
225
|
+
|
226
|
+
offsets = color_offsets(color_names, dodge, width)
|
227
|
+
orig_width = width
|
228
|
+
width = Float(nested_width(color_names, dodge, width))
|
229
|
+
|
230
|
+
color_names.each_with_index do |color_name, i|
|
231
|
+
add_box_plot_legend(gray, colors[i], color_names[i])
|
232
|
+
|
233
|
+
plot_data[i].each_with_index do |group_data, j|
|
234
|
+
next if group_data.empty?
|
195
235
|
|
196
|
-
|
197
|
-
|
236
|
+
position = j + offsets[i]
|
237
|
+
draw_box_plot(group_data,
|
238
|
+
vert: (orient == :v),
|
239
|
+
position: position,
|
240
|
+
color: colors[i],
|
241
|
+
gray: gray,
|
242
|
+
width: width,
|
243
|
+
whisker: whisker,
|
244
|
+
flier_size: flier_size)
|
198
245
|
end
|
199
|
-
|
200
|
-
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
private def add_box_plot_legend(gray, color, name)
|
250
|
+
patch = @pyplot.Rectangle.new([0, 0], 0, 0, edgecolor: gray, facecolor: color, label: name)
|
251
|
+
@pyplot.gca.add_patch(patch)
|
252
|
+
end
|
253
|
+
|
254
|
+
private def draw_box_plot(group_data, vert:, position:, color:, gray:, width:, whisker:, flier_size:)
|
255
|
+
# TODO: Do not convert to Array when group_data is Pandas::Series or Numpy::NDArray,
|
256
|
+
# and use MemoryView if available when group_data is Numo::NArray
|
257
|
+
artist_dict = @pyplot.boxplot(Array(group_data),
|
258
|
+
vert: vert,
|
259
|
+
patch_artist: true,
|
260
|
+
positions: [position],
|
261
|
+
widths: width,
|
262
|
+
whis: whisker)
|
263
|
+
|
264
|
+
artist_dict["boxes"].each do |box|
|
265
|
+
box.update({facecolor: color, zorder: 0.9, edgecolor: gray}, {})
|
266
|
+
end
|
267
|
+
artist_dict["whiskers"].each do |whisker|
|
268
|
+
whisker.update({color: gray, linestyle: "-"}, {})
|
269
|
+
end
|
270
|
+
artist_dict["caps"].each do |cap|
|
271
|
+
cap.update({color: gray}, {})
|
272
|
+
end
|
273
|
+
artist_dict["medians"].each do |median|
|
274
|
+
median.update({color: gray}, {})
|
275
|
+
end
|
276
|
+
artist_dict["fliers"].each do |flier|
|
277
|
+
flier.update({
|
278
|
+
markerfacecolor: gray,
|
279
|
+
marker: "d",
|
280
|
+
markeredgecolor: gray,
|
281
|
+
markersize: flier_size
|
282
|
+
}, {})
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
private def color_offsets(color_names, dodge, width)
|
287
|
+
n_names = color_names.length
|
288
|
+
if dodge
|
289
|
+
each_width = width / n_names
|
290
|
+
offsets = Charty::Linspace.new(0 .. (width - each_width), n_names).to_a
|
291
|
+
mean = Statistics.mean(offsets)
|
292
|
+
offsets.map {|x| x - mean }
|
293
|
+
else
|
294
|
+
Array.new(n_names, 0)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
private def nested_width(color_names, dodge, width)
|
299
|
+
if dodge
|
300
|
+
width.to_r / color_names.length * 0.98r
|
301
|
+
else
|
302
|
+
width
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def scatter(x, y, variables, color:, color_mapper:,
|
307
|
+
style:, style_mapper:, size:, size_mapper:)
|
308
|
+
kwd = {}
|
309
|
+
kwd[:edgecolor] = "w"
|
310
|
+
|
311
|
+
ax = @pyplot.gca
|
312
|
+
points = ax.scatter(x.to_a, y.to_a, **kwd)
|
313
|
+
|
314
|
+
unless color.nil?
|
315
|
+
color = color_mapper[color].map(&:to_hex_string)
|
316
|
+
points.set_facecolors(color)
|
317
|
+
end
|
318
|
+
|
319
|
+
unless size.nil?
|
320
|
+
size = size_mapper[size].map {|x| scale_scatter_point_size(x).to_f }
|
321
|
+
points.set_sizes(size)
|
322
|
+
end
|
323
|
+
|
324
|
+
unless style.nil?
|
325
|
+
paths = style_mapper[style, :marker].map(&method(:marker_to_path))
|
326
|
+
points.set_paths(paths)
|
327
|
+
end
|
328
|
+
|
329
|
+
sizes = points.get_sizes
|
330
|
+
points.set_linewidths(0.08 * Numpy.sqrt(Numpy.percentile(sizes, 10)))
|
331
|
+
end
|
332
|
+
|
333
|
+
def add_scatter_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
|
334
|
+
ax = @pyplot.gca
|
335
|
+
add_relational_plot_legend(
|
336
|
+
ax, variables, color_mapper, size_mapper, style_mapper,
|
337
|
+
legend, [:color, :s, :marker]
|
338
|
+
) do |label, kwargs|
|
339
|
+
ax.scatter([], [], label: label, **kwargs)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
PYPLOT_MARKERS = {
|
344
|
+
circle: "o",
|
345
|
+
x: "X",
|
346
|
+
cross: "P",
|
347
|
+
triangle_up: "^",
|
348
|
+
triangle_down: "v",
|
349
|
+
square: [4, 0, 45].freeze,
|
350
|
+
diamond: [4, 0, 0].freeze,
|
351
|
+
star: [5, 1, 0].freeze,
|
352
|
+
star_diamond: [4, 1, 0].freeze,
|
353
|
+
star_square: [4, 1, 45].freeze,
|
354
|
+
pentagon: [5, 0, 0].freeze,
|
355
|
+
hexagon: [6, 0, 0].freeze,
|
356
|
+
}.freeze
|
357
|
+
|
358
|
+
private def marker_to_path(marker)
|
359
|
+
@path_cache ||= {}
|
360
|
+
if @path_cache.key?(marker)
|
361
|
+
@path_cache[marker]
|
362
|
+
elsif PYPLOT_MARKERS.key?(marker)
|
363
|
+
val = PYPLOT_MARKERS[marker]
|
364
|
+
ms = Matplotlib.markers.MarkerStyle.new(val)
|
365
|
+
@path_cache[marker] = ms.get_path().transformed(ms.get_transform())
|
366
|
+
else
|
367
|
+
raise ArgumentError, "Unknown marker name: %p" % marker
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
RELATIONAL_PLOT_LEGEND_BRIEF_TICKS = 6
|
372
|
+
|
373
|
+
private def add_relational_plot_legend(ax, variables, color_mapper, size_mapper, style_mapper,
|
374
|
+
verbosity, legend_attributes, &func)
|
375
|
+
brief_ticks = RELATIONAL_PLOT_LEGEND_BRIEF_TICKS
|
376
|
+
verbosity = :auto if verbosity == true
|
377
|
+
|
378
|
+
legend_titles = Util.filter_map([:color, :size, :style]) {|v| variables[v] }
|
379
|
+
legend_title = legend_titles.pop if legend_titles.length == 1
|
380
|
+
|
381
|
+
legend_kwargs = {}
|
382
|
+
update_legend = ->(var_name, val_name, **kw) do
|
383
|
+
key = [var_name, val_name]
|
384
|
+
if legend_kwargs.key?(key)
|
385
|
+
legend_kwargs[key].update(kw)
|
386
|
+
else
|
387
|
+
legend_kwargs[key] = kw
|
201
388
|
end
|
202
|
-
|
203
|
-
|
389
|
+
end
|
390
|
+
|
391
|
+
title_kwargs = {visible: false, color: "w", s: 0, linewidth: 0, marker: "", dashes: ""}
|
392
|
+
|
393
|
+
# color legend
|
394
|
+
|
395
|
+
brief_color = (color_mapper.map_type == :numeric) && (
|
396
|
+
(verbosity == :brief) || (
|
397
|
+
verbosity == :auto && color_mapper.levels.length > brief_ticks
|
398
|
+
)
|
399
|
+
)
|
400
|
+
case
|
401
|
+
when brief_color
|
402
|
+
# TODO: Also support LogLocator
|
403
|
+
# locator = Matplotlib.ticker.LogLocator.new(numticks: brief_ticks)
|
404
|
+
locator = Matplotlib.ticker.MaxNLocator.new(nbins: brief_ticks)
|
405
|
+
limits = color_mapper.levels.minmax
|
406
|
+
color_levels, color_formatted_levels = locator_to_legend_entries(locator, limits)
|
407
|
+
when color_mapper.levels.nil?
|
408
|
+
color_levels = color_formatted_levels = []
|
409
|
+
else
|
410
|
+
color_levels = color_formatted_levels = color_mapper.levels
|
411
|
+
end
|
412
|
+
|
413
|
+
if legend_title.nil? && variables.key?(:color)
|
414
|
+
update_legend.([variables[:color], :title], variables[:color], **title_kwargs)
|
415
|
+
end
|
416
|
+
|
417
|
+
color_levels.length.times do |i|
|
418
|
+
next if color_levels[i].nil?
|
419
|
+
color_value = color_mapper[color_levels[i]].to_rgb.to_hex_string
|
420
|
+
update_legend.(variables[:color], color_formatted_levels[i], color: color_value)
|
421
|
+
end
|
422
|
+
|
423
|
+
brief_size = (size_mapper.map_type == :numeric) && (
|
424
|
+
verbosity == :brief ||
|
425
|
+
(verbosity == :auto && size_mapper.levels.length > brief_ticks)
|
426
|
+
)
|
427
|
+
case
|
428
|
+
when brief_size
|
429
|
+
# TODO: Also support LogLocator
|
430
|
+
# locator = Matplotlib.ticker.LogLocator(numticks: brief_ticks)
|
431
|
+
locator = Matplotlib.ticker.MaxNLocator.new(nbins: brief_ticks)
|
432
|
+
limits = size_mapper.levels.minmax
|
433
|
+
size_levels, size_formatted_levels = locator_to_legend_entries(locator, limits)
|
434
|
+
when size_mapper.levels.nil?
|
435
|
+
size_levels = size_formatted_levels = []
|
436
|
+
else
|
437
|
+
size_levels = size_formatted_levels = size_mapper.levels
|
438
|
+
end
|
439
|
+
|
440
|
+
if legend_title.nil? && variables.key?(:size)
|
441
|
+
update_legend.([variables[:size], :title], variables[:size], **title_kwargs)
|
442
|
+
end
|
443
|
+
|
444
|
+
size_levels.length.times do |i|
|
445
|
+
next if size_levels[i].nil?
|
446
|
+
line_width = scale_line_width(size_mapper[size_levels[i]])
|
447
|
+
point_size = scale_scatter_point_size(size_mapper[size_levels[i]])
|
448
|
+
update_legend.(variables[:size], size_formatted_levels[i], linewidth: line_width, s: point_size)
|
449
|
+
end
|
450
|
+
|
451
|
+
if legend_title.nil? && variables.key?(:style)
|
452
|
+
update_legend.([variables[:style], :title], variables[:style], **title_kwargs)
|
453
|
+
end
|
454
|
+
|
455
|
+
unless style_mapper.levels.nil?
|
456
|
+
style_mapper.levels.each do |level|
|
457
|
+
next if level.nil?
|
458
|
+
attrs = style_mapper[level]
|
459
|
+
marker = if attrs.key?(:marker)
|
460
|
+
PYPLOT_MARKERS[attrs[:marker]]
|
461
|
+
else
|
462
|
+
""
|
463
|
+
end
|
464
|
+
dashes = if attrs.key?(:dashes)
|
465
|
+
attrs[:dashes]
|
466
|
+
else
|
467
|
+
""
|
468
|
+
end
|
469
|
+
update_legend.(variables[:style], level, marker: marker, dashes: dashes)
|
204
470
|
end
|
205
|
-
|
206
|
-
|
471
|
+
end
|
472
|
+
|
473
|
+
legend_kwargs.each do |key, kw|
|
474
|
+
_, label = key
|
475
|
+
kw[:color] ||= ".2"
|
476
|
+
use_kw = Util.filter_map(legend_attributes) {|attr|
|
477
|
+
[attr, kw[attr]] if kw.key?(attr)
|
478
|
+
}.to_h
|
479
|
+
use_kw[:visible] = kw[:visible] if kw.key?(:visible)
|
480
|
+
func.(label, use_kw)
|
481
|
+
end
|
482
|
+
|
483
|
+
handles = ax.get_legend_handles_labels()[0].to_a
|
484
|
+
unless handles.empty?
|
485
|
+
legend = ax.legend(title: legend_title || "")
|
486
|
+
adjust_legend_subtitles(legend)
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
private def scale_scatter_point_size(x)
|
491
|
+
min = 0.5 * @default_marker_size**2
|
492
|
+
max = 2.0 * @default_marker_size**2
|
493
|
+
|
494
|
+
min + x * (max - min)
|
495
|
+
end
|
496
|
+
|
497
|
+
def line(x, y, variables, color:, color_mapper:, size:, size_mapper:, style:, style_mapper:, ci_params:)
|
498
|
+
kws = {
|
499
|
+
markeredgewidth: 0.75,
|
500
|
+
markeredgecolor: "w",
|
501
|
+
}
|
502
|
+
ax = @pyplot.gca
|
503
|
+
|
504
|
+
x = x.to_a
|
505
|
+
y = y.to_a
|
506
|
+
lines = ax.plot(x, y, **kws)
|
507
|
+
|
508
|
+
lines.each do |line|
|
509
|
+
unless color.nil?
|
510
|
+
line.set_color(color_mapper[color].to_rgb.to_hex_string)
|
511
|
+
end
|
512
|
+
|
513
|
+
unless size.nil?
|
514
|
+
scaled_size = scale_line_width(size_mapper[size])
|
515
|
+
line.set_linewidth(scaled_size.to_f)
|
516
|
+
end
|
517
|
+
|
518
|
+
unless style.nil?
|
519
|
+
attributes = style_mapper[style]
|
520
|
+
if attributes.key?(:dashes)
|
521
|
+
line.set_dashes(attributes[:dashes])
|
522
|
+
end
|
523
|
+
if attributes.key?(:marker)
|
524
|
+
line.set_marker(PYPLOT_MARKERS[attributes[:marker]])
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
# TODO: support color, size, and style
|
530
|
+
|
531
|
+
line = lines[0]
|
532
|
+
line_color = line.get_color
|
533
|
+
line_alpha = line.get_alpha
|
534
|
+
line_capstyle = line.get_solid_capstyle
|
535
|
+
|
536
|
+
unless ci_params.nil?
|
537
|
+
y_min = ci_params[:y_min].to_a
|
538
|
+
y_max = ci_params[:y_max].to_a
|
539
|
+
case ci_params[:style]
|
540
|
+
when :band
|
541
|
+
# TODO: support to supply `alpha` via `err_kws`
|
542
|
+
ax.fill_between(x, y_min, y_max, color: line_color, alpha: 0.2)
|
543
|
+
when :bars
|
544
|
+
error_deltas = [
|
545
|
+
y.zip(y_min).map {|v, v_min| v - v_min },
|
546
|
+
y.zip(y_max).map {|v, v_max| v_max - v }
|
547
|
+
]
|
548
|
+
ebars = ax.errorbar(x, y, error_deltas,
|
549
|
+
linestyle: "", color: line_color, alpha: line_alpha)
|
550
|
+
ebars.get_children.each do |bar|
|
551
|
+
case bar
|
552
|
+
when Matplotlib.collections.LineCollection
|
553
|
+
bar.set_capstyle(line_capstyle)
|
554
|
+
end
|
555
|
+
end
|
207
556
|
end
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
def add_line_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
|
561
|
+
ax = @pyplot.gca
|
562
|
+
add_relational_plot_legend(
|
563
|
+
ax, variables, color_mapper, size_mapper, style_mapper,
|
564
|
+
legend, [:color, :linewidth, :marker, :dashes]
|
565
|
+
) do |label, kwargs|
|
566
|
+
ax.plot([], [], label: label, **kwargs)
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
|
571
|
+
private def scale_line_width(x)
|
572
|
+
min = 0.5 * @default_line_width
|
573
|
+
max = 2.0 * @default_line_width
|
574
|
+
|
575
|
+
min + x * (max - min)
|
576
|
+
end
|
577
|
+
|
578
|
+
private def locator_to_legend_entries(locator, limits)
|
579
|
+
vmin, vmax = limits
|
580
|
+
dtype = case vmin
|
581
|
+
when Numeric
|
582
|
+
:float64
|
583
|
+
else
|
584
|
+
:object
|
585
|
+
end
|
586
|
+
raw_levels = locator.tick_values(vmin, vmax).astype(dtype).to_a
|
587
|
+
raw_levels.reject! {|v| v < limits[0] || limits[1] < v }
|
588
|
+
|
589
|
+
formatter = case locator
|
590
|
+
when Matplotlib.ticker.LogLocator
|
591
|
+
Matplotlib.ticker.LogFormatter.new
|
592
|
+
else
|
593
|
+
Matplotlib.ticker.ScalarFormatter.new
|
594
|
+
end
|
595
|
+
|
596
|
+
dummy_axis = Object.new
|
597
|
+
dummy_axis.define_singleton_method(:get_view_interval) { limits }
|
598
|
+
formatter.axis = dummy_axis
|
599
|
+
|
600
|
+
formatter.set_locs(raw_levels)
|
601
|
+
formatted_levels = raw_levels.map {|x| formatter.(x) }
|
602
|
+
|
603
|
+
return raw_levels, formatted_levels
|
604
|
+
end
|
605
|
+
|
606
|
+
private def adjust_legend_subtitles(legend)
|
607
|
+
font_size = Matplotlib.rcParams.get("legend.title_fontsize", nil)
|
608
|
+
hpackers = legend.findobj(Matplotlib.offsetbox.VPacker)[0].get_children()
|
609
|
+
hpackers.each do |hpack|
|
610
|
+
draw_area, text_area = hpack.get_children()
|
611
|
+
handles = draw_area.get_children()
|
612
|
+
unless handles.all? {|a| a.get_visible() }
|
613
|
+
draw_area.set_width(0)
|
614
|
+
unless font_size.nil?
|
615
|
+
text_area.get_children().each do |text|
|
616
|
+
text.set_size(font_size)
|
617
|
+
end
|
618
|
+
end
|
215
619
|
end
|
216
620
|
end
|
217
621
|
end
|
@@ -228,18 +632,62 @@ module Charty
|
|
228
632
|
@pyplot.gca.set_xticks(Array(values))
|
229
633
|
end
|
230
634
|
|
635
|
+
def set_yticks(values)
|
636
|
+
@pyplot.gca.set_yticks(Array(values))
|
637
|
+
end
|
638
|
+
|
231
639
|
def set_xtick_labels(labels)
|
232
640
|
@pyplot.gca.set_xticklabels(Array(labels).map(&method(:String)))
|
233
641
|
end
|
234
642
|
|
643
|
+
def set_ytick_labels(labels)
|
644
|
+
@pyplot.gca.set_yticklabels(Array(labels).map(&method(:String)))
|
645
|
+
end
|
646
|
+
|
235
647
|
def set_xlim(min, max)
|
236
648
|
@pyplot.gca.set_xlim(Float(min), Float(max))
|
237
649
|
end
|
238
650
|
|
651
|
+
def set_ylim(min, max)
|
652
|
+
@pyplot.gca.set_ylim(Float(min), Float(max))
|
653
|
+
end
|
654
|
+
|
239
655
|
def disable_xaxis_grid
|
240
656
|
@pyplot.gca.xaxis.grid(false)
|
241
657
|
end
|
242
658
|
|
659
|
+
def disable_yaxis_grid
|
660
|
+
@pyplot.gca.xaxis.grid(false)
|
661
|
+
end
|
662
|
+
|
663
|
+
def invert_yaxis
|
664
|
+
@pyplot.gca.invert_yaxis
|
665
|
+
end
|
666
|
+
|
667
|
+
def legend(loc:, title:)
|
668
|
+
@pyplot.gca.legend(loc: loc, title: title)
|
669
|
+
end
|
670
|
+
|
671
|
+
def render(notebook: false)
|
672
|
+
show
|
673
|
+
nil
|
674
|
+
end
|
675
|
+
|
676
|
+
SAVEFIG_OPTIONAL_PARAMS = [
|
677
|
+
:dpi, :quality, :optimize, :progressive, :facecolor, :edgecolor,
|
678
|
+
:orientation, :papertype, :transparent, :bbox_inches, :pad_inches,
|
679
|
+
:bbox_extra_artists, :backend, :metadata, :pil_kwargs
|
680
|
+
].freeze
|
681
|
+
|
682
|
+
def save(filename, format: nil, title: nil, width: 700, height: 500, **kwargs)
|
683
|
+
params = {}
|
684
|
+
params[:format] = format unless format.nil?
|
685
|
+
SAVEFIG_OPTIONAL_PARAMS.each do |key|
|
686
|
+
params[key] = kwargs[key] if kwargs.key?(key)
|
687
|
+
end
|
688
|
+
@pyplot.savefig(filename, **params)
|
689
|
+
end
|
690
|
+
|
243
691
|
def show
|
244
692
|
@pyplot.show
|
245
693
|
end
|