charty 0.2.4 → 0.2.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +64 -15
- data/charty.gemspec +10 -3
- data/lib/charty.rb +5 -2
- data/lib/charty/backends/bokeh.rb +2 -2
- data/lib/charty/backends/google_charts.rb +1 -1
- data/lib/charty/backends/gruff.rb +1 -1
- data/lib/charty/backends/plotly.rb +434 -32
- data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
- data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +87 -0
- data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
- data/lib/charty/backends/pyplot.rb +187 -48
- data/lib/charty/backends/rubyplot.rb +1 -1
- data/lib/charty/cache_dir.rb +27 -0
- data/lib/charty/dash_pattern_generator.rb +57 -0
- data/lib/charty/index.rb +1 -1
- data/lib/charty/iruby_helper.rb +18 -0
- data/lib/charty/plot_methods.rb +115 -3
- data/lib/charty/plotter.rb +2 -2
- data/lib/charty/plotters.rb +4 -0
- data/lib/charty/plotters/abstract_plotter.rb +106 -11
- data/lib/charty/plotters/bar_plotter.rb +1 -16
- data/lib/charty/plotters/box_plotter.rb +1 -16
- data/lib/charty/plotters/distribution_plotter.rb +150 -0
- data/lib/charty/plotters/histogram_plotter.rb +242 -0
- data/lib/charty/plotters/line_plotter.rb +300 -0
- data/lib/charty/plotters/relational_plotter.rb +213 -96
- data/lib/charty/plotters/scatter_plotter.rb +8 -43
- data/lib/charty/statistics.rb +11 -2
- data/lib/charty/table.rb +124 -14
- data/lib/charty/table_adapters/base_adapter.rb +97 -0
- data/lib/charty/table_adapters/daru_adapter.rb +2 -0
- data/lib/charty/table_adapters/datasets_adapter.rb +7 -0
- data/lib/charty/table_adapters/hash_adapter.rb +19 -3
- data/lib/charty/table_adapters/pandas_adapter.rb +82 -0
- data/lib/charty/util.rb +28 -0
- data/lib/charty/vector_adapters.rb +5 -1
- data/lib/charty/vector_adapters/array_adapter.rb +2 -10
- data/lib/charty/vector_adapters/daru_adapter.rb +3 -11
- data/lib/charty/vector_adapters/narray_adapter.rb +1 -6
- data/lib/charty/vector_adapters/numpy_adapter.rb +1 -1
- data/lib/charty/vector_adapters/pandas_adapter.rb +0 -1
- data/lib/charty/version.rb +1 -1
- metadata +104 -11
- data/lib/charty/missing_value_support.rb +0 -14
@@ -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,87 @@
|
|
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
|
+
end
|
54
|
+
|
55
|
+
def render(figure, element_id: nil, post_script: nil)
|
56
|
+
ary = Array.try_convert(post_script)
|
57
|
+
post_script = ary || [post_script]
|
58
|
+
post_script.unshift(<<~END_POST_SCRIPT)
|
59
|
+
var gd = document.getElementById('%{plot_id}');
|
60
|
+
var x = new MutationObserver(function (mutations, observer) {
|
61
|
+
var display = window.getComputedStyle(gd).display;
|
62
|
+
if (!display || display === 'none') {
|
63
|
+
console.log([gd, 'removed']);
|
64
|
+
Plotly.purge(gd);
|
65
|
+
observer.disconnect();
|
66
|
+
}
|
67
|
+
});
|
68
|
+
|
69
|
+
// Listen for the removal of the full notebook cell
|
70
|
+
var notebookContainer = gd.closest('#notebook-container');
|
71
|
+
if (notebookContainer) {
|
72
|
+
x.observe(notebookContainer, {childList: true});
|
73
|
+
}
|
74
|
+
|
75
|
+
// Listen for the clearing of the current output cell
|
76
|
+
var outputEl = gd.closest('.output');
|
77
|
+
if (outputEl) {
|
78
|
+
x.observe(outputEl, {childList: true});
|
79
|
+
}
|
80
|
+
END_POST_SCRIPT
|
81
|
+
|
82
|
+
super(figure, element_id: element_id, post_script: post_script)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
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
|
@@ -41,7 +42,7 @@ module Charty
|
|
41
42
|
@pyplot.show
|
42
43
|
end
|
43
44
|
|
44
|
-
def
|
45
|
+
def old_style_render(context, filename)
|
45
46
|
plot(@pyplot, context)
|
46
47
|
if filename
|
47
48
|
FileUtils.mkdir_p(File.dirname(filename))
|
@@ -50,7 +51,7 @@ module Charty
|
|
50
51
|
@pyplot.show
|
51
52
|
end
|
52
53
|
|
53
|
-
def
|
54
|
+
def old_style_save(context, filename, finish: true)
|
54
55
|
plot(context)
|
55
56
|
if filename
|
56
57
|
FileUtils.mkdir_p(File.dirname(filename))
|
@@ -303,7 +304,7 @@ module Charty
|
|
303
304
|
end
|
304
305
|
end
|
305
306
|
|
306
|
-
def scatter(x, y, variables,
|
307
|
+
def scatter(x, y, variables, color:, color_mapper:,
|
307
308
|
style:, style_mapper:, size:, size_mapper:)
|
308
309
|
kwd = {}
|
309
310
|
kwd[:edgecolor] = "w"
|
@@ -317,7 +318,7 @@ module Charty
|
|
317
318
|
end
|
318
319
|
|
319
320
|
unless size.nil?
|
320
|
-
size = size_mapper[size].map(
|
321
|
+
size = size_mapper[size].map {|x| scale_scatter_point_size(x).to_f }
|
321
322
|
points.set_sizes(size)
|
322
323
|
end
|
323
324
|
|
@@ -328,14 +329,15 @@ module Charty
|
|
328
329
|
|
329
330
|
sizes = points.get_sizes
|
330
331
|
points.set_linewidths(0.08 * Numpy.sqrt(Numpy.percentile(sizes, 10)))
|
332
|
+
end
|
331
333
|
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
334
|
+
def add_scatter_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
|
335
|
+
ax = @pyplot.gca
|
336
|
+
add_relational_plot_legend(
|
337
|
+
ax, variables, color_mapper, size_mapper, style_mapper,
|
338
|
+
legend, [:color, :s, :marker]
|
339
|
+
) do |label, kwargs|
|
340
|
+
ax.scatter([], [], label: label, **kwargs)
|
339
341
|
end
|
340
342
|
end
|
341
343
|
|
@@ -369,12 +371,12 @@ module Charty
|
|
369
371
|
|
370
372
|
RELATIONAL_PLOT_LEGEND_BRIEF_TICKS = 6
|
371
373
|
|
372
|
-
private def add_relational_plot_legend(ax,
|
373
|
-
legend_attributes, &func)
|
374
|
+
private def add_relational_plot_legend(ax, variables, color_mapper, size_mapper, style_mapper,
|
375
|
+
verbosity, legend_attributes, &func)
|
374
376
|
brief_ticks = RELATIONAL_PLOT_LEGEND_BRIEF_TICKS
|
375
377
|
verbosity = :auto if verbosity == true
|
376
378
|
|
377
|
-
legend_titles = [:color, :size, :style]
|
379
|
+
legend_titles = Util.filter_map([:color, :size, :style]) {|v| variables[v] }
|
378
380
|
legend_title = legend_titles.pop if legend_titles.length == 1
|
379
381
|
|
380
382
|
legend_kwargs = {}
|
@@ -391,24 +393,17 @@ module Charty
|
|
391
393
|
|
392
394
|
# color legend
|
393
395
|
|
394
|
-
brief_color =
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
false
|
400
|
-
else
|
401
|
-
color_mapper.levels.length > brief_ticks
|
402
|
-
end
|
403
|
-
else
|
404
|
-
false
|
405
|
-
end
|
396
|
+
brief_color = (color_mapper.map_type == :numeric) && (
|
397
|
+
(verbosity == :brief) || (
|
398
|
+
verbosity == :auto && color_mapper.levels.length > brief_ticks
|
399
|
+
)
|
400
|
+
)
|
406
401
|
case
|
407
402
|
when brief_color
|
408
403
|
# TODO: Also support LogLocator
|
409
404
|
# locator = Matplotlib.ticker.LogLocator.new(numticks: brief_ticks)
|
410
405
|
locator = Matplotlib.ticker.MaxNLocator.new(nbins: brief_ticks)
|
411
|
-
limits =
|
406
|
+
limits = color_mapper.levels.minmax
|
412
407
|
color_levels, color_formatted_levels = locator_to_legend_entries(locator, limits)
|
413
408
|
when color_mapper.levels.nil?
|
414
409
|
color_levels = color_formatted_levels = []
|
@@ -422,22 +417,14 @@ module Charty
|
|
422
417
|
|
423
418
|
color_levels.length.times do |i|
|
424
419
|
next if color_levels[i].nil?
|
425
|
-
color_value = color_mapper[color_levels[i]].to_hex_string
|
420
|
+
color_value = color_mapper[color_levels[i]].to_rgb.to_hex_string
|
426
421
|
update_legend.(variables[:color], color_formatted_levels[i], color: color_value)
|
427
422
|
end
|
428
423
|
|
429
|
-
brief_size =
|
430
|
-
|
431
|
-
size_mapper.
|
432
|
-
|
433
|
-
if size_mapper.levels.nil?
|
434
|
-
false
|
435
|
-
else
|
436
|
-
size_mapper.levels.length > brief_ticks
|
437
|
-
end
|
438
|
-
else
|
439
|
-
false
|
440
|
-
end
|
424
|
+
brief_size = (size_mapper.map_type == :numeric) && (
|
425
|
+
verbosity == :brief ||
|
426
|
+
(verbosity == :auto && size_mapper.levels.length > brief_ticks)
|
427
|
+
)
|
441
428
|
case
|
442
429
|
when brief_size
|
443
430
|
# TODO: Also support LogLocator
|
@@ -457,8 +444,9 @@ module Charty
|
|
457
444
|
|
458
445
|
size_levels.length.times do |i|
|
459
446
|
next if size_levels[i].nil?
|
460
|
-
|
461
|
-
|
447
|
+
line_width = scale_line_width(size_mapper[size_levels[i]])
|
448
|
+
point_size = scale_scatter_point_size(size_mapper[size_levels[i]])
|
449
|
+
update_legend.(variables[:size], size_formatted_levels[i], linewidth: line_width, s: point_size)
|
462
450
|
end
|
463
451
|
|
464
452
|
if legend_title.nil? && variables.key?(:style)
|
@@ -474,17 +462,19 @@ module Charty
|
|
474
462
|
else
|
475
463
|
""
|
476
464
|
end
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
465
|
+
dashes = if attrs.key?(:dashes)
|
466
|
+
attrs[:dashes]
|
467
|
+
else
|
468
|
+
""
|
469
|
+
end
|
470
|
+
update_legend.(variables[:style], level, marker: marker, dashes: dashes)
|
481
471
|
end
|
482
472
|
end
|
483
473
|
|
484
474
|
legend_kwargs.each do |key, kw|
|
485
475
|
_, label = key
|
486
476
|
kw[:color] ||= ".2"
|
487
|
-
use_kw =
|
477
|
+
use_kw = Util.filter_map(legend_attributes) {|attr|
|
488
478
|
[attr, kw[attr]] if kw.key?(attr)
|
489
479
|
}.to_h
|
490
480
|
use_kw[:visible] = kw[:visible] if kw.key?(:visible)
|
@@ -505,9 +495,137 @@ module Charty
|
|
505
495
|
min + x * (max - min)
|
506
496
|
end
|
507
497
|
|
498
|
+
def line(x, y, variables, color:, color_mapper:, size:, size_mapper:, style:, style_mapper:, ci_params:)
|
499
|
+
kws = {
|
500
|
+
markeredgewidth: 0.75,
|
501
|
+
markeredgecolor: "w",
|
502
|
+
}
|
503
|
+
ax = @pyplot.gca
|
504
|
+
|
505
|
+
x = x.to_a
|
506
|
+
y = y.to_a
|
507
|
+
lines = ax.plot(x, y, **kws)
|
508
|
+
|
509
|
+
lines.each do |line|
|
510
|
+
unless color.nil?
|
511
|
+
line.set_color(color_mapper[color].to_rgb.to_hex_string)
|
512
|
+
end
|
513
|
+
|
514
|
+
unless size.nil?
|
515
|
+
scaled_size = scale_line_width(size_mapper[size])
|
516
|
+
line.set_linewidth(scaled_size.to_f)
|
517
|
+
end
|
518
|
+
|
519
|
+
unless style.nil?
|
520
|
+
attributes = style_mapper[style]
|
521
|
+
if attributes.key?(:dashes)
|
522
|
+
line.set_dashes(attributes[:dashes])
|
523
|
+
end
|
524
|
+
if attributes.key?(:marker)
|
525
|
+
line.set_marker(PYPLOT_MARKERS[attributes[:marker]])
|
526
|
+
end
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
# TODO: support color, size, and style
|
531
|
+
|
532
|
+
line = lines[0]
|
533
|
+
line_color = line.get_color
|
534
|
+
line_alpha = line.get_alpha
|
535
|
+
line_capstyle = line.get_solid_capstyle
|
536
|
+
|
537
|
+
unless ci_params.nil?
|
538
|
+
y_min = ci_params[:y_min].to_a
|
539
|
+
y_max = ci_params[:y_max].to_a
|
540
|
+
case ci_params[:style]
|
541
|
+
when :band
|
542
|
+
# TODO: support to supply `alpha` via `err_kws`
|
543
|
+
ax.fill_between(x, y_min, y_max, color: line_color, alpha: 0.2)
|
544
|
+
when :bars
|
545
|
+
error_deltas = [
|
546
|
+
y.zip(y_min).map {|v, v_min| v - v_min },
|
547
|
+
y.zip(y_max).map {|v, v_max| v_max - v }
|
548
|
+
]
|
549
|
+
ebars = ax.errorbar(x, y, error_deltas,
|
550
|
+
linestyle: "", color: line_color, alpha: line_alpha)
|
551
|
+
ebars.get_children.each do |bar|
|
552
|
+
case bar
|
553
|
+
when Matplotlib.collections.LineCollection
|
554
|
+
bar.set_capstyle(line_capstyle)
|
555
|
+
end
|
556
|
+
end
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
def add_line_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
|
562
|
+
ax = @pyplot.gca
|
563
|
+
add_relational_plot_legend(
|
564
|
+
ax, variables, color_mapper, size_mapper, style_mapper,
|
565
|
+
legend, [:color, :linewidth, :marker, :dashes]
|
566
|
+
) do |label, kwargs|
|
567
|
+
ax.plot([], [], label: label, **kwargs)
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
|
572
|
+
private def scale_line_width(x)
|
573
|
+
min = 0.5 * @default_line_width
|
574
|
+
max = 2.0 * @default_line_width
|
575
|
+
|
576
|
+
min + x * (max - min)
|
577
|
+
end
|
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
|
+
|
508
620
|
private def locator_to_legend_entries(locator, limits)
|
509
621
|
vmin, vmax = limits
|
510
|
-
|
622
|
+
dtype = case vmin
|
623
|
+
when Numeric
|
624
|
+
:float64
|
625
|
+
else
|
626
|
+
:object
|
627
|
+
end
|
628
|
+
raw_levels = locator.tick_values(vmin, vmax).astype(dtype).to_a
|
511
629
|
raw_levels.reject! {|v| v < limits[0] || limits[1] < v }
|
512
630
|
|
513
631
|
formatter = case locator
|
@@ -592,6 +710,27 @@ module Charty
|
|
592
710
|
@pyplot.gca.legend(loc: loc, title: title)
|
593
711
|
end
|
594
712
|
|
713
|
+
def render(notebook: false)
|
714
|
+
show
|
715
|
+
nil
|
716
|
+
end
|
717
|
+
|
718
|
+
SAVEFIG_OPTIONAL_PARAMS = [
|
719
|
+
:dpi, :quality, :optimize, :progressive, :facecolor, :edgecolor,
|
720
|
+
:orientation, :papertype, :transparent, :bbox_inches, :pad_inches,
|
721
|
+
:bbox_extra_artists, :backend, :metadata, :pil_kwargs
|
722
|
+
].freeze
|
723
|
+
|
724
|
+
def save(filename, format: nil, title: nil, width: 700, height: 500, **kwargs)
|
725
|
+
params = {}
|
726
|
+
params[:format] = format unless format.nil?
|
727
|
+
SAVEFIG_OPTIONAL_PARAMS.each do |key|
|
728
|
+
params[key] = kwargs[key] if kwargs.key?(key)
|
729
|
+
end
|
730
|
+
@pyplot.savefig(filename, **params)
|
731
|
+
@pyplot.close
|
732
|
+
end
|
733
|
+
|
595
734
|
def show
|
596
735
|
@pyplot.show
|
597
736
|
end
|