charty 0.2.4 → 0.2.9
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/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
|