lets-plot 4.8.1rc1__cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
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.
- lets_plot/__init__.py +382 -0
- lets_plot/_global_settings.py +192 -0
- lets_plot/_kbridge.py +197 -0
- lets_plot/_type_utils.py +133 -0
- lets_plot/_version.py +6 -0
- lets_plot/bistro/__init__.py +16 -0
- lets_plot/bistro/_plot2d_common.py +106 -0
- lets_plot/bistro/corr.py +448 -0
- lets_plot/bistro/im.py +196 -0
- lets_plot/bistro/joint.py +192 -0
- lets_plot/bistro/qq.py +207 -0
- lets_plot/bistro/residual.py +341 -0
- lets_plot/bistro/waterfall.py +332 -0
- lets_plot/export/__init__.py +6 -0
- lets_plot/export/ggsave_.py +172 -0
- lets_plot/frontend_context/__init__.py +8 -0
- lets_plot/frontend_context/_configuration.py +140 -0
- lets_plot/frontend_context/_dynamic_configure_html.py +115 -0
- lets_plot/frontend_context/_frontend_ctx.py +16 -0
- lets_plot/frontend_context/_html_contexts.py +223 -0
- lets_plot/frontend_context/_intellij_python_json_ctx.py +38 -0
- lets_plot/frontend_context/_isolated_webview_panel_ctx.py +81 -0
- lets_plot/frontend_context/_json_contexts.py +39 -0
- lets_plot/frontend_context/_jupyter_notebook_ctx.py +82 -0
- lets_plot/frontend_context/_mime_types.py +7 -0
- lets_plot/frontend_context/_static_html_page_ctx.py +76 -0
- lets_plot/frontend_context/_static_svg_ctx.py +26 -0
- lets_plot/frontend_context/_webbr_html_page_ctx.py +29 -0
- lets_plot/frontend_context/sandbox.py +5 -0
- lets_plot/geo_data/__init__.py +19 -0
- lets_plot/geo_data/core.py +335 -0
- lets_plot/geo_data/geocoder.py +988 -0
- lets_plot/geo_data/geocodes.py +512 -0
- lets_plot/geo_data/gis/__init__.py +0 -0
- lets_plot/geo_data/gis/fluent_dict.py +201 -0
- lets_plot/geo_data/gis/geocoding_service.py +42 -0
- lets_plot/geo_data/gis/geometry.py +91 -0
- lets_plot/geo_data/gis/json_request.py +232 -0
- lets_plot/geo_data/gis/json_response.py +308 -0
- lets_plot/geo_data/gis/request.py +492 -0
- lets_plot/geo_data/gis/response.py +247 -0
- lets_plot/geo_data/livemap_helper.py +65 -0
- lets_plot/geo_data/to_geo_data_frame.py +141 -0
- lets_plot/geo_data/type_assertion.py +34 -0
- lets_plot/geo_data_internals/__init__.py +4 -0
- lets_plot/geo_data_internals/constants.py +13 -0
- lets_plot/geo_data_internals/utils.py +33 -0
- lets_plot/mapping.py +115 -0
- lets_plot/package_data/lets-plot.min.js +3 -0
- lets_plot/plot/__init__.py +64 -0
- lets_plot/plot/_global_theme.py +14 -0
- lets_plot/plot/annotation.py +290 -0
- lets_plot/plot/coord.py +242 -0
- lets_plot/plot/core.py +1071 -0
- lets_plot/plot/expand_limits_.py +78 -0
- lets_plot/plot/facet.py +210 -0
- lets_plot/plot/font_features.py +71 -0
- lets_plot/plot/geom.py +9146 -0
- lets_plot/plot/geom_extras.py +53 -0
- lets_plot/plot/geom_function_.py +219 -0
- lets_plot/plot/geom_imshow_.py +393 -0
- lets_plot/plot/geom_livemap_.py +343 -0
- lets_plot/plot/ggbunch_.py +96 -0
- lets_plot/plot/gggrid_.py +139 -0
- lets_plot/plot/ggtb_.py +81 -0
- lets_plot/plot/guide.py +231 -0
- lets_plot/plot/label.py +187 -0
- lets_plot/plot/marginal_layer.py +181 -0
- lets_plot/plot/plot.py +245 -0
- lets_plot/plot/pos.py +344 -0
- lets_plot/plot/sampling.py +338 -0
- lets_plot/plot/sandbox_.py +26 -0
- lets_plot/plot/scale.py +3580 -0
- lets_plot/plot/scale_colormap_mpl.py +300 -0
- lets_plot/plot/scale_convenience.py +155 -0
- lets_plot/plot/scale_identity_.py +653 -0
- lets_plot/plot/scale_position.py +1342 -0
- lets_plot/plot/series_meta.py +209 -0
- lets_plot/plot/stat.py +585 -0
- lets_plot/plot/subplots.py +331 -0
- lets_plot/plot/subplots_util.py +24 -0
- lets_plot/plot/theme_.py +790 -0
- lets_plot/plot/theme_set.py +418 -0
- lets_plot/plot/tooltip.py +486 -0
- lets_plot/plot/util.py +267 -0
- lets_plot/settings_utils.py +244 -0
- lets_plot/tilesets.py +429 -0
- lets_plot-4.8.1rc1.dist-info/METADATA +221 -0
- lets_plot-4.8.1rc1.dist-info/RECORD +97 -0
- lets_plot-4.8.1rc1.dist-info/WHEEL +6 -0
- lets_plot-4.8.1rc1.dist-info/licenses/LICENSE +21 -0
- lets_plot-4.8.1rc1.dist-info/licenses/licenses/LICENSE.FreeType +166 -0
- lets_plot-4.8.1rc1.dist-info/licenses/licenses/LICENSE.ImageMagick +106 -0
- lets_plot-4.8.1rc1.dist-info/licenses/licenses/LICENSE.expat +21 -0
- lets_plot-4.8.1rc1.dist-info/licenses/licenses/LICENSE.fontconfig +200 -0
- lets_plot-4.8.1rc1.dist-info/top_level.txt +2 -0
- lets_plot_kotlin_bridge.cpython-311-x86_64-linux-gnu.so +0 -0
lets_plot/plot/core.py
ADDED
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2019. JetBrains s.r.o.
|
|
3
|
+
# Use of this source code is governed by the MIT license that can be found in the LICENSE file.
|
|
4
|
+
#
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
__all__ = ['aes', 'layer']
|
|
11
|
+
|
|
12
|
+
from lets_plot._global_settings import get_global_bool, has_global_value, FRAGMENTS_ENABLED
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def aes(x=None, y=None, **kwargs):
|
|
16
|
+
"""
|
|
17
|
+
Define aesthetic mappings.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
x, y, ... :
|
|
22
|
+
Aesthetic mappings. Name-value pairs specifying which data variables to use for each aesthetic.
|
|
23
|
+
The names for x and y aesthetics are typically omitted because they are so common; all other aesthetics must be named.
|
|
24
|
+
The specific list of supported aesthetics differs by geometry type.
|
|
25
|
+
group : str or list, optional
|
|
26
|
+
Data grouping control (not a true aesthetic). Use a variable name to group by that variable,
|
|
27
|
+
a list of variables to group by their interaction, or an empty list to disable all grouping.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
``FeatureSpec``
|
|
32
|
+
Aesthetic mapping specification.
|
|
33
|
+
|
|
34
|
+
Notes
|
|
35
|
+
-----
|
|
36
|
+
Generate aesthetic mappings that describe how variables in the data are projected to visual properties
|
|
37
|
+
(aesthetics) of geometries. This function also standardizes aesthetic names by, for example, converting
|
|
38
|
+
colour to color.
|
|
39
|
+
|
|
40
|
+
Aesthetic mappings are not to be confused with aesthetic settings; the latter is used to set aesthetics to
|
|
41
|
+
some constant values, e.g., make all points red in the plot. If one wants to make the color of a point
|
|
42
|
+
depend on the value of a variable, he/she should project this variable to the color aesthetic via
|
|
43
|
+
aesthetic mapping.
|
|
44
|
+
|
|
45
|
+
**Data Grouping**
|
|
46
|
+
|
|
47
|
+
The ``group`` parameter is not a true aesthetic but controls how data is grouped for visualization:
|
|
48
|
+
|
|
49
|
+
Default Grouping Behavior:
|
|
50
|
+
Lets-Plot automatically groups data by discrete variables mapped to aesthetics like ``color``, ``shape``,
|
|
51
|
+
``linetype``, etc. This creates separate visual elements (lines, paths, polygons) for each unique
|
|
52
|
+
combination of these variables.
|
|
53
|
+
|
|
54
|
+
Explicit Group Control:
|
|
55
|
+
- Use ``group='variable_name'`` to group only by that specific variable, overriding default grouping
|
|
56
|
+
- Use ``group=['var1', 'var2', ...]`` to group by the interaction of multiple variables
|
|
57
|
+
- Use ``group=[]`` to disable all the grouping completely
|
|
58
|
+
|
|
59
|
+
Examples
|
|
60
|
+
--------
|
|
61
|
+
.. jupyter-execute::
|
|
62
|
+
:linenos:
|
|
63
|
+
:emphasize-lines: 10-11
|
|
64
|
+
|
|
65
|
+
import numpy as np
|
|
66
|
+
from lets_plot import *
|
|
67
|
+
LetsPlot.setup_html()
|
|
68
|
+
n = 100
|
|
69
|
+
np.random.seed(42)
|
|
70
|
+
x = np.random.uniform(-1, 1, size=n)
|
|
71
|
+
y = 25 * x ** 2 + np.random.normal(size=n)
|
|
72
|
+
c = np.where(x < 0, '0', '1')
|
|
73
|
+
ggplot({'x': x, 'y': y, 'c': c}) + \\
|
|
74
|
+
geom_point(aes('x', 'y', color='y', shape='c', size='x')) + \\
|
|
75
|
+
geom_smooth(aes(x='x', y='y'), deg=2, size=1)
|
|
76
|
+
|
|
77
|
+
|
|
|
78
|
+
|
|
79
|
+
.. jupyter-execute::
|
|
80
|
+
:linenos:
|
|
81
|
+
:emphasize-lines: 3-4
|
|
82
|
+
|
|
83
|
+
from lets_plot import *
|
|
84
|
+
LetsPlot.setup_html()
|
|
85
|
+
ggplot() + geom_polygon(aes(x=[0, 1, 2], y=[2, 1, 4]), \\
|
|
86
|
+
color='black', alpha=.5, size=1)
|
|
87
|
+
|
|
88
|
+
|
|
|
89
|
+
|
|
90
|
+
.. jupyter-execute::
|
|
91
|
+
:linenos:
|
|
92
|
+
:emphasize-lines: 12,16,18,20
|
|
93
|
+
|
|
94
|
+
import numpy as np
|
|
95
|
+
from lets_plot import *
|
|
96
|
+
LetsPlot.setup_html()
|
|
97
|
+
n = 50
|
|
98
|
+
np.random.seed(42)
|
|
99
|
+
data = {
|
|
100
|
+
'val': np.concatenate([np.random.normal(loc=-2, size=n),
|
|
101
|
+
np.random.normal(loc=2, size=n)]),
|
|
102
|
+
'group': ['g1'] * n + ['g2'] * n,
|
|
103
|
+
'cat': np.random.choice(['A', 'B', 'C'], size=2*n),
|
|
104
|
+
}
|
|
105
|
+
g = ggplot(data, aes(x='val', fill='cat'))
|
|
106
|
+
gggrid([
|
|
107
|
+
g + geom_density(alpha=.25) + \\
|
|
108
|
+
ggtitle("Default grouping"),
|
|
109
|
+
g + geom_density(aes(group='group'), alpha=.25) + \\
|
|
110
|
+
ggtitle("group='group'"),
|
|
111
|
+
g + geom_density(aes(group=['cat', 'group']), alpha=.25) + \\
|
|
112
|
+
ggtitle("group=['cat', 'group']"),
|
|
113
|
+
g + geom_density(aes(group=[]), alpha=.25) + \\
|
|
114
|
+
ggtitle("group=[]"),
|
|
115
|
+
], ncol=2)
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
return FeatureSpec('mapping', name=None, x=x, y=y, **kwargs)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def layer(geom=None, stat=None, data=None, mapping=None, position=None, **kwargs):
|
|
123
|
+
"""
|
|
124
|
+
Create a new layer.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
geom : str
|
|
129
|
+
The geometric object to use to display the data.
|
|
130
|
+
stat : str, default='identity'
|
|
131
|
+
The statistical transformation to use on the data for this layer, as a string.
|
|
132
|
+
Supported transformations: 'identity' (leaves the data unchanged),
|
|
133
|
+
'count' (count number of points with same x-axis coordinate),
|
|
134
|
+
'bin' (count number of points with x-axis coordinate in the same bin),
|
|
135
|
+
'smooth' (perform smoothing - linear default),
|
|
136
|
+
'density' (compute and draw kernel density estimate).
|
|
137
|
+
data : dict or Pandas or Polars ``DataFrame``
|
|
138
|
+
The data to be displayed in this layer. If None, the default, the data
|
|
139
|
+
is inherited from the plot data as specified in the call to ggplot.
|
|
140
|
+
mapping : ``FeatureSpec``
|
|
141
|
+
Set of aesthetic mappings created by `aes() <https://lets-plot.org/python/pages/api/lets_plot.aes.html>`__ function.
|
|
142
|
+
Aesthetic mappings describe the way that variables in the data are
|
|
143
|
+
mapped to plot "aesthetics".
|
|
144
|
+
position : str or ``FeatureSpec``
|
|
145
|
+
Position adjustment.
|
|
146
|
+
Either a position adjustment name: 'dodge', 'jitter', 'nudge', 'jitterdodge', 'fill',
|
|
147
|
+
'stack' or 'identity', or the result of calling a position adjustment function
|
|
148
|
+
(e.g., `position_dodge() <https://lets-plot.org/python/pages/api/lets_plot.position_dodge.html>`__ etc.).
|
|
149
|
+
kwargs:
|
|
150
|
+
Other arguments passed on to layer. These are often aesthetics settings, used to set an aesthetic to a fixed
|
|
151
|
+
value, like color = "red", fill = "blue", size = 3 or shape = 21. They may also be parameters to the
|
|
152
|
+
paired geom/stat.
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
``LayerSpec``
|
|
157
|
+
Geom object specification.
|
|
158
|
+
|
|
159
|
+
Notes
|
|
160
|
+
-----
|
|
161
|
+
A layer is a combination of data, stat and geom with a potential position adjustment. Usually layers are created
|
|
162
|
+
using geom_* or stat_* calls but they can be created directly using this function.
|
|
163
|
+
|
|
164
|
+
Examples
|
|
165
|
+
--------
|
|
166
|
+
.. jupyter-execute::
|
|
167
|
+
:linenos:
|
|
168
|
+
:emphasize-lines: 8
|
|
169
|
+
|
|
170
|
+
import numpy as np
|
|
171
|
+
from lets_plot import *
|
|
172
|
+
LetsPlot.setup_html()
|
|
173
|
+
n = 50
|
|
174
|
+
np.random.seed(42)
|
|
175
|
+
x = np.random.uniform(-1, 1, size=n)
|
|
176
|
+
y = 25 * x ** 2 + np.random.normal(size=n)
|
|
177
|
+
ggplot({'x': x, 'y': y}, aes(x='x', y='y')) + layer(geom='point')
|
|
178
|
+
|
|
179
|
+
"""
|
|
180
|
+
# todo: other parameters: inherit.aes = TRUE, subset = NULL, show.legend = NA
|
|
181
|
+
|
|
182
|
+
return LayerSpec(**locals())
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _filter_none(original: dict) -> dict:
|
|
186
|
+
return {k: v for k, v in original.items() if v is not None}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
#
|
|
190
|
+
# -----------------------------------
|
|
191
|
+
# Specs
|
|
192
|
+
#
|
|
193
|
+
|
|
194
|
+
def _specs_to_dict(opts_raw):
|
|
195
|
+
opts = {}
|
|
196
|
+
for k, v in opts_raw.items():
|
|
197
|
+
if isinstance(v, FeatureSpec):
|
|
198
|
+
opts[k] = v.as_dict()
|
|
199
|
+
elif isinstance(v, dict):
|
|
200
|
+
opts[k] = _specs_to_dict(v)
|
|
201
|
+
else:
|
|
202
|
+
opts[k] = v
|
|
203
|
+
|
|
204
|
+
return _filter_none(opts)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class FeatureSpec():
|
|
208
|
+
"""
|
|
209
|
+
A base class of the plot objects.
|
|
210
|
+
|
|
211
|
+
Do not use this class explicitly.
|
|
212
|
+
|
|
213
|
+
Instead, you should construct its objects with functions `ggplot() <https://lets-plot.org/python/pages/api/lets_plot.ggplot.html>`__,
|
|
214
|
+
`geom_point() <https://lets-plot.org/python/pages/api/lets_plot.geom_point.html>`__,
|
|
215
|
+
`position_dodge() <https://lets-plot.org/python/pages/api/lets_plot.position_dodge.html>`__,
|
|
216
|
+
`scale_x_continuous() <https://lets-plot.org/python/pages/api/lets_plot.scale_x_continuous.html>`__ etc.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def __init__(self, kind, name, **kwargs):
|
|
220
|
+
"""Initialize self."""
|
|
221
|
+
self.kind = kind
|
|
222
|
+
self.__props = {}
|
|
223
|
+
if name is not None:
|
|
224
|
+
self.__props['name'] = name
|
|
225
|
+
self.__props.update(**kwargs)
|
|
226
|
+
|
|
227
|
+
def props(self):
|
|
228
|
+
return self.__props
|
|
229
|
+
|
|
230
|
+
def as_dict(self):
|
|
231
|
+
"""
|
|
232
|
+
Return the dictionary of all properties of the object with ``as_dict()``
|
|
233
|
+
applied recursively to all subproperties of ``FeatureSpec`` type.
|
|
234
|
+
|
|
235
|
+
Returns
|
|
236
|
+
-------
|
|
237
|
+
dict
|
|
238
|
+
Dictionary of properties.
|
|
239
|
+
|
|
240
|
+
Examples
|
|
241
|
+
--------
|
|
242
|
+
.. jupyter-execute::
|
|
243
|
+
:linenos:
|
|
244
|
+
:emphasize-lines: 4
|
|
245
|
+
|
|
246
|
+
from lets_plot import *
|
|
247
|
+
LetsPlot.setup_html()
|
|
248
|
+
p = ggplot({'x': [0], 'y': [0]}) + geom_point(aes('x', 'y'))
|
|
249
|
+
p.as_dict()
|
|
250
|
+
"""
|
|
251
|
+
return _specs_to_dict(self.props())
|
|
252
|
+
|
|
253
|
+
def __str__(self):
|
|
254
|
+
return json.dumps(self.as_dict(), indent=2)
|
|
255
|
+
|
|
256
|
+
def __add__(self, other):
|
|
257
|
+
if isinstance(other, DummySpec):
|
|
258
|
+
# nothing
|
|
259
|
+
return self
|
|
260
|
+
|
|
261
|
+
if self.kind in ["plot", "subplots"]:
|
|
262
|
+
# pass and fail: don't allow to add plot to a feature list.
|
|
263
|
+
pass
|
|
264
|
+
elif isinstance(other, FeatureSpec):
|
|
265
|
+
if other.kind in ["plot", "subplots"]:
|
|
266
|
+
# pass and fail: don't allow to add plot to a feature list.
|
|
267
|
+
pass
|
|
268
|
+
else:
|
|
269
|
+
arr = FeatureSpecArray(self, other)
|
|
270
|
+
return arr
|
|
271
|
+
|
|
272
|
+
raise TypeError('unsupported operand type(s) for +: {} and {}'
|
|
273
|
+
.format(self.__class__, other.__class__))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class PlotSpec(FeatureSpec):
|
|
277
|
+
"""
|
|
278
|
+
A class of the initial plot object.
|
|
279
|
+
|
|
280
|
+
Do not use this class explicitly.
|
|
281
|
+
|
|
282
|
+
Instead, you should construct its objects with functions `ggplot() <https://lets-plot.org/python/pages/api/lets_plot.ggplot.html>`__,
|
|
283
|
+
``corr_plot(...).points().build()`` etc.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def duplicate(cls, other):
|
|
288
|
+
dup = PlotSpec(data=None, mapping=None,
|
|
289
|
+
scales=other.__scales,
|
|
290
|
+
layers=other.__layers,
|
|
291
|
+
metainfo_list=other.__metainfo_list,
|
|
292
|
+
is_livemap=other.__is_livemap,
|
|
293
|
+
crs_initialized=other.__crs_initialized,
|
|
294
|
+
crs=other.__crs,
|
|
295
|
+
)
|
|
296
|
+
dup.props().update(other.props())
|
|
297
|
+
return dup
|
|
298
|
+
|
|
299
|
+
def __init__(self, data, mapping, scales, layers, metainfo_list=[], is_livemap=False, crs_initialized=False,
|
|
300
|
+
crs=None, **kwargs):
|
|
301
|
+
"""Initialize self."""
|
|
302
|
+
super().__init__('plot', name=None, data=data, mapping=mapping, **kwargs)
|
|
303
|
+
self.__scales = list(scales)
|
|
304
|
+
self.__layers = list(layers)
|
|
305
|
+
self.__metainfo_list = list(metainfo_list)
|
|
306
|
+
self.__is_livemap = is_livemap
|
|
307
|
+
self.__crs_initialized = crs_initialized
|
|
308
|
+
self.__crs = crs
|
|
309
|
+
|
|
310
|
+
def get_plot_shared_data(self):
|
|
311
|
+
"""
|
|
312
|
+
Extract the data shared by all layers.
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
dict or ``DataFrame``
|
|
317
|
+
Object data.
|
|
318
|
+
|
|
319
|
+
Examples
|
|
320
|
+
--------
|
|
321
|
+
.. jupyter-execute::
|
|
322
|
+
:linenos:
|
|
323
|
+
:emphasize-lines: 5
|
|
324
|
+
|
|
325
|
+
from lets_plot import *
|
|
326
|
+
LetsPlot.setup_html()
|
|
327
|
+
p = ggplot({'x': [0], 'y': [0]}, aes('x', 'y'))
|
|
328
|
+
p += geom_point(data={'x': [1], 'y': [1]})
|
|
329
|
+
p.get_plot_shared_data()
|
|
330
|
+
|
|
331
|
+
"""
|
|
332
|
+
# used to evaluate 'completion'
|
|
333
|
+
return self.props()['data']
|
|
334
|
+
|
|
335
|
+
def has_layers(self) -> bool:
|
|
336
|
+
"""
|
|
337
|
+
Check if the ``PlotSpec`` object has at least one layer.
|
|
338
|
+
|
|
339
|
+
Returns
|
|
340
|
+
-------
|
|
341
|
+
bool
|
|
342
|
+
True if object has layers.
|
|
343
|
+
|
|
344
|
+
Examples
|
|
345
|
+
--------
|
|
346
|
+
.. jupyter-execute::
|
|
347
|
+
:linenos:
|
|
348
|
+
:emphasize-lines: 4, 6
|
|
349
|
+
|
|
350
|
+
from lets_plot import *
|
|
351
|
+
LetsPlot.setup_html()
|
|
352
|
+
p = ggplot()
|
|
353
|
+
print(p.has_layers())
|
|
354
|
+
p += geom_point(x=0, y=0)
|
|
355
|
+
print(p.has_layers())
|
|
356
|
+
|
|
357
|
+
"""
|
|
358
|
+
return True if self.__layers else False
|
|
359
|
+
|
|
360
|
+
def __add__(self, other):
|
|
361
|
+
"""
|
|
362
|
+
Allow to add different specs to the ``PlotSpec`` object.
|
|
363
|
+
|
|
364
|
+
Examples
|
|
365
|
+
--------
|
|
366
|
+
.. jupyter-execute::
|
|
367
|
+
:linenos:
|
|
368
|
+
:emphasize-lines: 7
|
|
369
|
+
|
|
370
|
+
from lets_plot import *
|
|
371
|
+
LetsPlot.setup_html()
|
|
372
|
+
p = ggplot({'x': [0, 1, 2], 'y': [0, 1, 2]}, aes('x', 'y'))
|
|
373
|
+
l = layer('point', mapping=aes(color='x'))
|
|
374
|
+
s = scale_color_discrete()
|
|
375
|
+
t = theme(axis_title='blank')
|
|
376
|
+
p + l + s + t
|
|
377
|
+
|
|
378
|
+
"""
|
|
379
|
+
"""
|
|
380
|
+
plot + other_plot -> fail
|
|
381
|
+
plot + layer -> plot[layers] += layer
|
|
382
|
+
plot + geom -> plot[layers] += new layer(geom)
|
|
383
|
+
plot + scale -> plot[scales] += scale
|
|
384
|
+
plot + metainfo -> plot[metainfo_list] += metainfo
|
|
385
|
+
plot + [feature] -> plot + each feature in []
|
|
386
|
+
plot + theme + theme -> merged theme
|
|
387
|
+
"""
|
|
388
|
+
if isinstance(other, PlotSpec):
|
|
389
|
+
# pass and fail
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
if isinstance(other, DummySpec):
|
|
393
|
+
# nothing
|
|
394
|
+
return self
|
|
395
|
+
|
|
396
|
+
elif isinstance(other, FeatureSpec):
|
|
397
|
+
plot = PlotSpec.duplicate(self)
|
|
398
|
+
if other.kind == 'layer':
|
|
399
|
+
if other.props()['geom'] == 'livemap':
|
|
400
|
+
plot.__is_livemap = True
|
|
401
|
+
|
|
402
|
+
from lets_plot.plot.util import is_geo_data_frame # local import to break circular reference
|
|
403
|
+
if is_geo_data_frame(other.props().get('data')) \
|
|
404
|
+
or is_geo_data_frame(other.props().get('map')):
|
|
405
|
+
if plot.__crs_initialized:
|
|
406
|
+
if plot.__crs != other.props().get('use_crs'):
|
|
407
|
+
raise ValueError(
|
|
408
|
+
'All geoms with map parameter should either use same `use_crs` or not use it at all')
|
|
409
|
+
else:
|
|
410
|
+
plot.__crs_initialized = True
|
|
411
|
+
plot.__crs = other.props().get('use_crs')
|
|
412
|
+
|
|
413
|
+
if plot.__is_livemap and plot.__crs is not None:
|
|
414
|
+
raise ValueError("livemap doesn't support `use_crs`")
|
|
415
|
+
|
|
416
|
+
other.before_append(plot.__is_livemap)
|
|
417
|
+
plot.__layers.append(other)
|
|
418
|
+
return plot
|
|
419
|
+
|
|
420
|
+
if other.kind == 'scale':
|
|
421
|
+
plot.__scales.append(other)
|
|
422
|
+
return plot
|
|
423
|
+
|
|
424
|
+
if other.kind == 'theme':
|
|
425
|
+
new_theme_options = {k: v for k, v in other.props().items() if v is not None}
|
|
426
|
+
if 'name' in new_theme_options:
|
|
427
|
+
# keep the previously specified flavor
|
|
428
|
+
if plot.props().get('theme', {}).get('flavor', None) is not None:
|
|
429
|
+
new_theme_options.update({'flavor': plot.props()['theme']['flavor']})
|
|
430
|
+
|
|
431
|
+
# pre-configured theme overrides existing theme altogether.
|
|
432
|
+
plot.props()['theme'] = new_theme_options
|
|
433
|
+
else:
|
|
434
|
+
# merge themes
|
|
435
|
+
old_theme_options = plot.props().get('theme', {})
|
|
436
|
+
plot.props()['theme'] = _theme_dicts_merge(old_theme_options, new_theme_options)
|
|
437
|
+
|
|
438
|
+
return plot
|
|
439
|
+
|
|
440
|
+
if other.kind == 'metainfo':
|
|
441
|
+
plot.__metainfo_list.append(other)
|
|
442
|
+
return plot
|
|
443
|
+
|
|
444
|
+
if isinstance(other, FeatureSpecArray):
|
|
445
|
+
for spec in other.elements():
|
|
446
|
+
plot = plot.__add__(spec)
|
|
447
|
+
return plot
|
|
448
|
+
|
|
449
|
+
if other.kind == 'guides':
|
|
450
|
+
existing_options = plot.props().get('guides', {})
|
|
451
|
+
plot.props()['guides'] = _merge_dicts_recursively(existing_options, other.as_dict())
|
|
452
|
+
return plot
|
|
453
|
+
|
|
454
|
+
if other.kind == 'mapping': # +aes(..)
|
|
455
|
+
# existing_spec = plot.props().get('mapping', aes())
|
|
456
|
+
# merged_mapping = {**existing_spec.as_dict(), **other.as_dict()}
|
|
457
|
+
# plot.props()['mapping'] = aes(**merged_mapping)
|
|
458
|
+
from lets_plot.plot.util import update_plot_aes_mapping # local import to break circular reference
|
|
459
|
+
update_plot_aes_mapping(plot, other)
|
|
460
|
+
return plot
|
|
461
|
+
|
|
462
|
+
# add feature to properties
|
|
463
|
+
plot.props()[other.kind] = other
|
|
464
|
+
return plot
|
|
465
|
+
|
|
466
|
+
return super().__add__(other)
|
|
467
|
+
|
|
468
|
+
def as_dict(self):
|
|
469
|
+
d = super().as_dict()
|
|
470
|
+
d['kind'] = self.kind
|
|
471
|
+
d['scales'] = [scale.as_dict() for scale in self.__scales]
|
|
472
|
+
d['layers'] = [layer.as_dict() for layer in self.__layers]
|
|
473
|
+
d['metainfo_list'] = [metainfo.as_dict() for metainfo in self.__metainfo_list]
|
|
474
|
+
return d
|
|
475
|
+
|
|
476
|
+
def __str__(self):
|
|
477
|
+
result = ['plot']
|
|
478
|
+
result.extend('{}: {}'.format(k, v)
|
|
479
|
+
for k, v in self.props().items())
|
|
480
|
+
|
|
481
|
+
result.append('scales [{}]'.format(len(self.__scales)))
|
|
482
|
+
for scale in self.__scales:
|
|
483
|
+
result.append(str(scale))
|
|
484
|
+
result.append('-' * 34)
|
|
485
|
+
|
|
486
|
+
result.append('layers [{}]'.format(len(self.__layers)))
|
|
487
|
+
for layer in self.__layers:
|
|
488
|
+
result.append(str(layer))
|
|
489
|
+
result.append('-' * 34)
|
|
490
|
+
|
|
491
|
+
result.append('metainfo_list [{}]'.format(len(self.__metainfo_list)))
|
|
492
|
+
for metainfo in self.__metainfo_list:
|
|
493
|
+
result.append(str(metainfo))
|
|
494
|
+
result.append('-' * 34)
|
|
495
|
+
|
|
496
|
+
result.append('') # for trailing \n
|
|
497
|
+
return '\n'.join(result)
|
|
498
|
+
|
|
499
|
+
def _repr_html_(self):
|
|
500
|
+
# Special method discovered and invoked by IPython.display.display.
|
|
501
|
+
from ..frontend_context._configuration import _as_html
|
|
502
|
+
return _as_html(self.as_dict())
|
|
503
|
+
|
|
504
|
+
def show(self):
|
|
505
|
+
"""
|
|
506
|
+
Draw a plot.
|
|
507
|
+
|
|
508
|
+
Examples
|
|
509
|
+
--------
|
|
510
|
+
.. jupyter-execute::
|
|
511
|
+
:linenos:
|
|
512
|
+
:emphasize-lines: 4
|
|
513
|
+
|
|
514
|
+
from lets_plot import *
|
|
515
|
+
LetsPlot.setup_html()
|
|
516
|
+
p = ggplot() + geom_point(x=0, y=0)
|
|
517
|
+
p.show()
|
|
518
|
+
|
|
519
|
+
"""
|
|
520
|
+
from ..frontend_context._configuration import _display_plot
|
|
521
|
+
_display_plot(self)
|
|
522
|
+
|
|
523
|
+
def to_svg(self, path=None, w=None, h=None, unit=None) -> str:
|
|
524
|
+
"""
|
|
525
|
+
Export the plot in SVG format.
|
|
526
|
+
|
|
527
|
+
Plots containing ``geom_livemap()`` are not supported.
|
|
528
|
+
|
|
529
|
+
Parameters
|
|
530
|
+
----------
|
|
531
|
+
self : ``PlotSpec``
|
|
532
|
+
Plot specification to export.
|
|
533
|
+
path : str, file-like object, default=None
|
|
534
|
+
Сan be either a string specifying a file path or a file-like object.
|
|
535
|
+
If a string is provided, the result will be exported to the file at that path.
|
|
536
|
+
If a file-like object is provided, the result will be exported to that object.
|
|
537
|
+
If None is provided, the result will be returned as a string.
|
|
538
|
+
w : float, default=None
|
|
539
|
+
Width of the output image in units.
|
|
540
|
+
h : float, default=None
|
|
541
|
+
Height of the output image in units.
|
|
542
|
+
unit : {'in', 'cm', 'mm', 'px'}, default='in'
|
|
543
|
+
Unit of the output image. One of: 'in', 'cm', 'mm' or 'px'.
|
|
544
|
+
|
|
545
|
+
Returns
|
|
546
|
+
-------
|
|
547
|
+
str
|
|
548
|
+
Absolute pathname of created file,
|
|
549
|
+
SVG content as a string or None if a file-like object is provided.
|
|
550
|
+
|
|
551
|
+
Examples
|
|
552
|
+
--------
|
|
553
|
+
.. jupyter-execute::
|
|
554
|
+
:linenos:
|
|
555
|
+
:emphasize-lines: 10
|
|
556
|
+
|
|
557
|
+
import numpy as np
|
|
558
|
+
import io
|
|
559
|
+
from lets_plot import *
|
|
560
|
+
from IPython import display
|
|
561
|
+
LetsPlot.setup_html()
|
|
562
|
+
np.random.seed(42)
|
|
563
|
+
x = np.random.randint(10, size=100)
|
|
564
|
+
p = ggplot({'x': x}, aes(x='x')) + geom_bar()
|
|
565
|
+
file_like = io.BytesIO()
|
|
566
|
+
p.to_svg(file_like)
|
|
567
|
+
display.SVG(file_like.getvalue())
|
|
568
|
+
|
|
569
|
+
"""
|
|
570
|
+
return _to_svg(self, path, w=w, h=h, unit=unit)
|
|
571
|
+
|
|
572
|
+
def to_html(self, path=None, iframe: bool = None) -> str:
|
|
573
|
+
"""
|
|
574
|
+
Export the plot in HTML format.
|
|
575
|
+
|
|
576
|
+
Parameters
|
|
577
|
+
----------
|
|
578
|
+
self : ``PlotSpec``
|
|
579
|
+
Plot specification to export.
|
|
580
|
+
path : str, file-like object, default=None
|
|
581
|
+
Сan be either a string specifying a file path or a file-like object.
|
|
582
|
+
If a string is provided, the result will be exported to the file at that path.
|
|
583
|
+
If a file-like object is provided, the result will be exported to that object.
|
|
584
|
+
If None is provided, the result will be returned as a string.
|
|
585
|
+
iframe : bool, default=False
|
|
586
|
+
Whether to wrap HTML page into a iFrame.
|
|
587
|
+
|
|
588
|
+
Returns
|
|
589
|
+
-------
|
|
590
|
+
str
|
|
591
|
+
Absolute pathname of created file,
|
|
592
|
+
HTML content as a string or None if a file-like object is provided.
|
|
593
|
+
|
|
594
|
+
Examples
|
|
595
|
+
--------
|
|
596
|
+
.. jupyter-execute::
|
|
597
|
+
:linenos:
|
|
598
|
+
:emphasize-lines: 10
|
|
599
|
+
|
|
600
|
+
import io
|
|
601
|
+
from IPython.display import HTML
|
|
602
|
+
import numpy as np
|
|
603
|
+
from lets_plot import *
|
|
604
|
+
LetsPlot.setup_html()
|
|
605
|
+
np.random.seed(42)
|
|
606
|
+
x = np.random.randint(10, size=100)
|
|
607
|
+
p = ggplot({'x': x}, aes(x='x')) + geom_bar()
|
|
608
|
+
file_like = io.BytesIO()
|
|
609
|
+
p.to_html(file_like)
|
|
610
|
+
HTML(file_like.getvalue().decode('utf-8'))
|
|
611
|
+
|
|
612
|
+
"""
|
|
613
|
+
return _to_html(self, path, iframe)
|
|
614
|
+
|
|
615
|
+
def to_png(self, path, scale: float = None, w=None, h=None, unit=None, dpi=None) -> str:
|
|
616
|
+
"""
|
|
617
|
+
Export a plot to a file or to a file-like object in PNG format.
|
|
618
|
+
|
|
619
|
+
Plots containing ``geom_livemap()`` are not supported.
|
|
620
|
+
|
|
621
|
+
Parameters
|
|
622
|
+
----------
|
|
623
|
+
self : ``PlotSpec``
|
|
624
|
+
Plot specification to export.
|
|
625
|
+
path : str, file-like object
|
|
626
|
+
Сan be either a string specifying a file path or a file-like object.
|
|
627
|
+
If a string is provided, the result will be exported to the file at that path.
|
|
628
|
+
If a file-like object is provided, the result will be exported to that object.
|
|
629
|
+
scale : float
|
|
630
|
+
Scaling factor for raster output. Default value is 2.0.
|
|
631
|
+
w : float, default=None
|
|
632
|
+
Width of the output image in units.
|
|
633
|
+
Only applicable when exporting to PNG or PDF.
|
|
634
|
+
h : float, default=None
|
|
635
|
+
Height of the output image in units.
|
|
636
|
+
Only applicable when exporting to PNG or PDF.
|
|
637
|
+
unit : {'in', 'cm', 'mm', 'px'}, default='in'
|
|
638
|
+
Unit of the output image. One of: 'in', 'cm', 'mm' or 'px'.
|
|
639
|
+
Only applicable when exporting to PNG or PDF.
|
|
640
|
+
dpi : int, default=300
|
|
641
|
+
Resolution in dots per inch.
|
|
642
|
+
Only applicable when exporting to PNG or PDF.
|
|
643
|
+
The default value depends on the unit:
|
|
644
|
+
|
|
645
|
+
- for 'px' it is 96 (output image will have the same pixel size as ``w`` and ``h`` values)
|
|
646
|
+
- for physical units ('in', 'cm', 'mm') it is 300
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
Returns
|
|
650
|
+
-------
|
|
651
|
+
str
|
|
652
|
+
Absolute pathname of created file or None if a file-like object is provided.
|
|
653
|
+
|
|
654
|
+
Notes
|
|
655
|
+
-----
|
|
656
|
+
- If ``w``, ``h``, ``unit``, and ``dpi`` are all specified:
|
|
657
|
+
|
|
658
|
+
- The plot's pixel size (default or set by `ggsize() <https://lets-plot.org/python/pages/api/lets_plot.ggsize.html>`__) is ignored.
|
|
659
|
+
- The output size is calculated using the specified ``w``, ``h``, ``unit``, and ``dpi``.
|
|
660
|
+
|
|
661
|
+
- The plot is resized to fit the specified ``w`` x ``h`` area, which may affect the layout, tick labels, and other elements.
|
|
662
|
+
|
|
663
|
+
- If only ``dpi`` is specified:
|
|
664
|
+
|
|
665
|
+
- The plot's pixel size (default or set by `ggsize() <https://lets-plot.org/python/pages/api/lets_plot.ggsize.html>`__) is converted to inches using the standard display PPI of 96.
|
|
666
|
+
- The output size is then calculated based on the specified DPI.
|
|
667
|
+
|
|
668
|
+
- The plot maintains its aspect ratio, preserving layout, tick labels, and other visual elements.
|
|
669
|
+
- Useful for printing - the plot will appear nearly the same size as on screen.
|
|
670
|
+
|
|
671
|
+
- If ``w``, ``h`` are not specified:
|
|
672
|
+
|
|
673
|
+
- The ``scale`` parameter is used to determine the output size.
|
|
674
|
+
|
|
675
|
+
- The plot maintains its aspect ratio, preserving layout, tick labels, and other visual elements.
|
|
676
|
+
- Useful for generating high-resolution images suitable for publication.
|
|
677
|
+
|
|
678
|
+
Examples
|
|
679
|
+
--------
|
|
680
|
+
.. jupyter-execute::
|
|
681
|
+
:linenos:
|
|
682
|
+
:emphasize-lines: 10
|
|
683
|
+
|
|
684
|
+
import numpy as np
|
|
685
|
+
import io
|
|
686
|
+
from lets_plot import *
|
|
687
|
+
from IPython import display
|
|
688
|
+
LetsPlot.setup_html()
|
|
689
|
+
np.random.seed(42)
|
|
690
|
+
x = np.random.randint(10, size=100)
|
|
691
|
+
p = ggplot({'x': x}, aes(x='x')) + geom_bar()
|
|
692
|
+
file_like = io.BytesIO()
|
|
693
|
+
p.to_png(file_like)
|
|
694
|
+
display.Image(file_like.getvalue())
|
|
695
|
+
|
|
696
|
+
"""
|
|
697
|
+
return _export_as_raster(self, path, scale, 'png', w=w, h=h, unit=unit, dpi=dpi)
|
|
698
|
+
|
|
699
|
+
def to_pdf(self, path, scale: float = None, w=None, h=None, unit=None, dpi=None) -> str:
|
|
700
|
+
"""
|
|
701
|
+
Export a plot to a file or to a file-like object in PDF format.
|
|
702
|
+
|
|
703
|
+
Plots containing ``geom_livemap()`` are not supported.
|
|
704
|
+
|
|
705
|
+
Parameters
|
|
706
|
+
----------
|
|
707
|
+
self : ``PlotSpec``
|
|
708
|
+
Plot specification to export.
|
|
709
|
+
path : str, file-like object
|
|
710
|
+
Сan be either a string specifying a file path or a file-like object.
|
|
711
|
+
If a string is provided, the result will be exported to the file at that path.
|
|
712
|
+
If a file-like object is provided, the result will be exported to that object.
|
|
713
|
+
scale : float
|
|
714
|
+
Scaling factor for raster output. Default value is 2.0.
|
|
715
|
+
w : float, default=None
|
|
716
|
+
Width of the output image in units.
|
|
717
|
+
Only applicable when exporting to PNG or PDF.
|
|
718
|
+
h : float, default=None
|
|
719
|
+
Height of the output image in units.
|
|
720
|
+
Only applicable when exporting to PNG or PDF.
|
|
721
|
+
unit : {'in', 'cm', 'mm', 'px'}, default='in'
|
|
722
|
+
Unit of the output image. One of: 'in', 'cm', 'mm' or 'px'.
|
|
723
|
+
Only applicable when exporting to PNG or PDF.
|
|
724
|
+
dpi : int, default=300
|
|
725
|
+
Resolution in dots per inch.
|
|
726
|
+
Only applicable when exporting to PNG or PDF.
|
|
727
|
+
The default value depends on the unit:
|
|
728
|
+
|
|
729
|
+
- for 'px' it is 96 (output image will have the same pixel size as ``w`` and ``h`` values)
|
|
730
|
+
- for physical units ('in', 'cm', 'mm') it is 300
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
Returns
|
|
734
|
+
-------
|
|
735
|
+
str
|
|
736
|
+
Absolute pathname of created file or None if a file-like object is provided.
|
|
737
|
+
|
|
738
|
+
Notes
|
|
739
|
+
-----
|
|
740
|
+
- If ``w``, ``h``, ``unit``, and ``dpi`` are all specified:
|
|
741
|
+
|
|
742
|
+
- The plot's pixel size (default or set by `ggsize() <https://lets-plot.org/python/pages/api/lets_plot.ggsize.html>`__) is ignored.
|
|
743
|
+
- The output size is calculated using the specified ``w``, ``h``, ``unit``, and ``dpi``.
|
|
744
|
+
|
|
745
|
+
- The plot is resized to fit the specified ``w`` x ``h`` area, which may affect the layout, tick labels, and other elements.
|
|
746
|
+
|
|
747
|
+
- If only ``dpi`` is specified:
|
|
748
|
+
|
|
749
|
+
- The plot's pixel size (default or set by `ggsize() <https://lets-plot.org/python/pages/api/lets_plot.ggsize.html>`__) is converted to inches using the standard display PPI of 96.
|
|
750
|
+
- The output size is then calculated based on the specified DPI.
|
|
751
|
+
|
|
752
|
+
- The plot maintains its aspect ratio, preserving layout, tick labels, and other visual elements.
|
|
753
|
+
- Useful for printing - the plot will appear nearly the same size as on screen.
|
|
754
|
+
|
|
755
|
+
- If ``w``, ``h`` are not specified:
|
|
756
|
+
|
|
757
|
+
- The ``scale`` parameter is used to determine the output size.
|
|
758
|
+
|
|
759
|
+
- The plot maintains its aspect ratio, preserving layout, tick labels, and other visual elements.
|
|
760
|
+
- Useful for generating high-resolution images suitable for publication.
|
|
761
|
+
|
|
762
|
+
Examples
|
|
763
|
+
--------
|
|
764
|
+
.. jupyter-execute::
|
|
765
|
+
:linenos:
|
|
766
|
+
:emphasize-lines: 13
|
|
767
|
+
|
|
768
|
+
import numpy as np
|
|
769
|
+
import io
|
|
770
|
+
import os
|
|
771
|
+
from lets_plot import *
|
|
772
|
+
from IPython import display
|
|
773
|
+
LetsPlot.setup_html()
|
|
774
|
+
n = 60
|
|
775
|
+
np.random.seed(42)
|
|
776
|
+
x = np.random.choice(list('abcde'), size=n)
|
|
777
|
+
y = np.random.normal(size=n)
|
|
778
|
+
p = ggplot({'x': x, 'y': y}, aes(x='x', y='y')) + geom_jitter()
|
|
779
|
+
file_like = io.BytesIO()
|
|
780
|
+
p.to_pdf(file_like)
|
|
781
|
+
|
|
782
|
+
"""
|
|
783
|
+
return _export_as_raster(self, path, scale, 'pdf', w=w, h=h, unit=unit, dpi=dpi)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
class LayerSpec(FeatureSpec):
|
|
787
|
+
"""
|
|
788
|
+
A class of the plot layer object.
|
|
789
|
+
|
|
790
|
+
Do not use this class explicitly.
|
|
791
|
+
|
|
792
|
+
Instead, you should construct its objects with functions `geom_point() <https://lets-plot.org/python/pages/api/lets_plot.geom_point.html>`__,
|
|
793
|
+
`geom_contour() <https://lets-plot.org/python/pages/api/lets_plot.geom_contour.html>`__,
|
|
794
|
+
`geom_boxplot() <https://lets-plot.org/python/pages/api/lets_plot.geom_boxplot.html>`__,
|
|
795
|
+
`geom_text() <https://lets-plot.org/python/pages/api/lets_plot.geom_text.html>`__ etc.
|
|
796
|
+
"""
|
|
797
|
+
|
|
798
|
+
__own_features = ['geom', 'stat', 'mapping', 'position']
|
|
799
|
+
|
|
800
|
+
@classmethod
|
|
801
|
+
def duplicate(cls, other):
|
|
802
|
+
# A shallow copy!
|
|
803
|
+
return LayerSpec(**other.props())
|
|
804
|
+
|
|
805
|
+
def __init__(self, **kwargs):
|
|
806
|
+
super().__init__('layer', name=None, **kwargs)
|
|
807
|
+
|
|
808
|
+
def before_append(self, is_livemap):
|
|
809
|
+
from .util import normalize_map_join, is_geo_data_frame, auto_join_geo_names, geo_data_frame_to_crs, \
|
|
810
|
+
get_geo_data_frame_meta
|
|
811
|
+
from lets_plot.geo_data_internals.utils import is_geocoder
|
|
812
|
+
|
|
813
|
+
name = self.props()['geom']
|
|
814
|
+
map_join = self.props().get('map_join')
|
|
815
|
+
map = self.props().get('map')
|
|
816
|
+
map_data_meta = None
|
|
817
|
+
|
|
818
|
+
if map_join is None and map is None:
|
|
819
|
+
return
|
|
820
|
+
|
|
821
|
+
map_join = normalize_map_join(map_join)
|
|
822
|
+
|
|
823
|
+
if is_geocoder(map):
|
|
824
|
+
if is_livemap and get_global_bool(FRAGMENTS_ENABLED) if has_global_value(FRAGMENTS_ENABLED) else False:
|
|
825
|
+
map = map.get_geocodes()
|
|
826
|
+
map_join = auto_join_geo_names(map_join, map)
|
|
827
|
+
map_data_meta = {'georeference': {}}
|
|
828
|
+
else:
|
|
829
|
+
# Fetch proper GeoDataFrame. Further processing is the same as if map was a GDF.
|
|
830
|
+
if name in ['point', 'pie', 'text', 'label', 'livemap']:
|
|
831
|
+
map = map.get_centroids()
|
|
832
|
+
elif name in ['map', 'polygon']:
|
|
833
|
+
map = map.get_boundaries()
|
|
834
|
+
elif name in ['rect']:
|
|
835
|
+
map = map.get_limits()
|
|
836
|
+
else:
|
|
837
|
+
raise ValueError("Geocoding doesn't provide geometries for geom_{}".format(name))
|
|
838
|
+
|
|
839
|
+
if is_geo_data_frame(map):
|
|
840
|
+
# map = geo_data_frame_to_crs(map, self.props().get('use_crs'))
|
|
841
|
+
use_crs = self.props().get('use_crs')
|
|
842
|
+
if use_crs != "provided":
|
|
843
|
+
map = geo_data_frame_to_crs(map, use_crs)
|
|
844
|
+
map_join = auto_join_geo_names(map_join, map)
|
|
845
|
+
map_data_meta = get_geo_data_frame_meta(map)
|
|
846
|
+
|
|
847
|
+
if map_join is not None:
|
|
848
|
+
self.props()['map_join'] = map_join
|
|
849
|
+
|
|
850
|
+
if map is not None:
|
|
851
|
+
self.props()['map'] = map
|
|
852
|
+
|
|
853
|
+
if map_data_meta is not None:
|
|
854
|
+
self.props()['map_data_meta'] = map_data_meta
|
|
855
|
+
|
|
856
|
+
def get_plot_layer_data(self):
|
|
857
|
+
# used to evaluate 'completion'
|
|
858
|
+
return self.props()['data']
|
|
859
|
+
|
|
860
|
+
def __add__(self, other):
|
|
861
|
+
if isinstance(other, DummySpec):
|
|
862
|
+
# nothing
|
|
863
|
+
return self
|
|
864
|
+
|
|
865
|
+
"""
|
|
866
|
+
layer + own_feature -> layer[feature] = feature
|
|
867
|
+
layer + other -> default
|
|
868
|
+
"""
|
|
869
|
+
if isinstance(other, FeatureSpec):
|
|
870
|
+
if other.kind in LayerSpec.__own_features:
|
|
871
|
+
l = LayerSpec.duplicate(self)
|
|
872
|
+
l.props()[other.kind] = other
|
|
873
|
+
return l
|
|
874
|
+
|
|
875
|
+
return super().__add__(other)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
class FeatureSpecArray(FeatureSpec):
|
|
879
|
+
def __init__(self, *features):
|
|
880
|
+
super().__init__('feature-list', name=None)
|
|
881
|
+
self.__elements = []
|
|
882
|
+
self._flatten(list(features), self.__elements)
|
|
883
|
+
|
|
884
|
+
def __len__(self):
|
|
885
|
+
return len(self.__elements)
|
|
886
|
+
|
|
887
|
+
def __iter__(self):
|
|
888
|
+
return self.__elements.__iter__()
|
|
889
|
+
|
|
890
|
+
def __getitem__(self, item):
|
|
891
|
+
return self.__elements[item]
|
|
892
|
+
|
|
893
|
+
def elements(self):
|
|
894
|
+
return self.__elements
|
|
895
|
+
|
|
896
|
+
def as_dict(self):
|
|
897
|
+
elements = [{e.kind: e.as_dict()} for e in self.__elements]
|
|
898
|
+
return {'feature-list': elements}
|
|
899
|
+
|
|
900
|
+
def __add__(self, other):
|
|
901
|
+
if isinstance(other, DummySpec):
|
|
902
|
+
# nothing
|
|
903
|
+
return self
|
|
904
|
+
|
|
905
|
+
"""
|
|
906
|
+
array + other_feature -> [my_elements, other]
|
|
907
|
+
array + other_array -> [my_elements, other_elements]
|
|
908
|
+
layer + ? -> fail
|
|
909
|
+
"""
|
|
910
|
+
if isinstance(other, FeatureSpec):
|
|
911
|
+
if isinstance(other, FeatureSpecArray):
|
|
912
|
+
return FeatureSpecArray(*self.__elements, *other.__elements)
|
|
913
|
+
|
|
914
|
+
elif other.kind != 'plot':
|
|
915
|
+
return FeatureSpecArray(*self.__elements, other)
|
|
916
|
+
|
|
917
|
+
return super().__add__(other)
|
|
918
|
+
|
|
919
|
+
def _flatten(self, features, out):
|
|
920
|
+
for feature in features:
|
|
921
|
+
if isinstance(feature, FeatureSpecArray):
|
|
922
|
+
self._flatten(feature.elements(), out)
|
|
923
|
+
else:
|
|
924
|
+
out.append(feature)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
class DummySpec(FeatureSpec):
|
|
928
|
+
def __init__(self):
|
|
929
|
+
super().__init__('dummy', name=None)
|
|
930
|
+
|
|
931
|
+
def as_dict(self):
|
|
932
|
+
return {'dummy-feature': True}
|
|
933
|
+
|
|
934
|
+
def __add__(self, other):
|
|
935
|
+
return other
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _generate_data(size):
|
|
939
|
+
""" For testing reasons only """
|
|
940
|
+
# return FeatureSpec('dummy', name=None, data='x' * size)
|
|
941
|
+
return PlotSpec(data='x' * size, mapping=None, scales=[], layers=[])
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _merge_dicts_recursively(d1, d2):
|
|
945
|
+
merged = d1.copy()
|
|
946
|
+
for key, value in d2.items():
|
|
947
|
+
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
948
|
+
merged[key] = _merge_dicts_recursively(merged[key], value)
|
|
949
|
+
else:
|
|
950
|
+
merged[key] = value
|
|
951
|
+
return merged
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _theme_dicts_merge(x, y):
|
|
955
|
+
"""
|
|
956
|
+
Simple values in ``y`` override values in ``x``.
|
|
957
|
+
If values in ``y`` and ``x`` both are dictionaries, then they are merged.
|
|
958
|
+
"""
|
|
959
|
+
overlapping_keys = x.keys() & y.keys()
|
|
960
|
+
z = {k: {**x[k], **y[k]} for k in overlapping_keys if type(x[k]) is dict and type(y[k]) is dict}
|
|
961
|
+
return {**x, **y, **z}
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def _to_svg(spec, path, w=None, h=None, unit=None) -> Union[str, None]:
|
|
965
|
+
from .. import _kbridge as kbr
|
|
966
|
+
|
|
967
|
+
svg = kbr._generate_svg(spec.as_dict(), w, h, unit, use_css_pixelated_image_rendering=True)
|
|
968
|
+
|
|
969
|
+
if path is None:
|
|
970
|
+
return svg
|
|
971
|
+
elif isinstance(path, str):
|
|
972
|
+
abspath = _makedirs(path)
|
|
973
|
+
with io.open(abspath, mode="w", encoding="utf-8") as f:
|
|
974
|
+
f.write(svg)
|
|
975
|
+
return abspath
|
|
976
|
+
else:
|
|
977
|
+
path.write(svg.encode())
|
|
978
|
+
return None
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _to_html(spec, path, iframe: bool) -> Union[str, None]:
|
|
982
|
+
if iframe is None:
|
|
983
|
+
iframe = False
|
|
984
|
+
|
|
985
|
+
from .. import _kbridge as kbr
|
|
986
|
+
html_page = kbr._generate_static_html_page(spec.as_dict(), iframe)
|
|
987
|
+
|
|
988
|
+
if path is None:
|
|
989
|
+
return html_page
|
|
990
|
+
elif isinstance(path, str):
|
|
991
|
+
abspath = _makedirs(path)
|
|
992
|
+
with io.open(abspath, mode="w", encoding="utf-8") as f:
|
|
993
|
+
f.write(html_page)
|
|
994
|
+
return abspath
|
|
995
|
+
else:
|
|
996
|
+
path.write(html_page.encode())
|
|
997
|
+
return None
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def _export_as_raster(spec, path, scale: float, export_format: str, w=None, h=None, unit=None, dpi=None) -> Union[str, None]:
|
|
1001
|
+
import base64
|
|
1002
|
+
from .. import _kbridge
|
|
1003
|
+
|
|
1004
|
+
if isinstance(path, str):
|
|
1005
|
+
file_path = _makedirs(path)
|
|
1006
|
+
file_like_object = None
|
|
1007
|
+
else:
|
|
1008
|
+
file_like_object = path
|
|
1009
|
+
file_path = None
|
|
1010
|
+
|
|
1011
|
+
png_base64 = _kbridge._generate_png(spec.as_dict(), w, h, unit, dpi, scale)
|
|
1012
|
+
png = base64.b64decode(png_base64)
|
|
1013
|
+
|
|
1014
|
+
if export_format.lower() == 'png':
|
|
1015
|
+
if file_path is not None:
|
|
1016
|
+
with open(file_path, 'wb') as f:
|
|
1017
|
+
f.write(png)
|
|
1018
|
+
return file_path
|
|
1019
|
+
else:
|
|
1020
|
+
file_like_object.write(png)
|
|
1021
|
+
return None
|
|
1022
|
+
elif export_format.lower() == 'pdf':
|
|
1023
|
+
try:
|
|
1024
|
+
from PIL import Image
|
|
1025
|
+
except ImportError:
|
|
1026
|
+
import sys
|
|
1027
|
+
print("\n"
|
|
1028
|
+
"To export Lets-Plot figure to a PDF file please install pillow library"
|
|
1029
|
+
"to your Python environment.\n"
|
|
1030
|
+
"Pillow is free and distributed under the MIT-CMU license.\n"
|
|
1031
|
+
"For more details visit: https://python-pillow.github.io/\n", file=sys.stderr)
|
|
1032
|
+
return None
|
|
1033
|
+
|
|
1034
|
+
with Image.open(io.BytesIO(png)) as img:
|
|
1035
|
+
if img.mode == 'RGBA':
|
|
1036
|
+
img = img.convert('RGB')
|
|
1037
|
+
|
|
1038
|
+
if file_path is not None:
|
|
1039
|
+
img.save(file_path, "PDF", dpi=img.info.get("dpi"))
|
|
1040
|
+
return file_path
|
|
1041
|
+
else:
|
|
1042
|
+
img.save(file_like_object, "PDF", dpi=img.info.get("dpi"))
|
|
1043
|
+
return None
|
|
1044
|
+
else:
|
|
1045
|
+
raise ValueError("Unknown export format: {}".format(export_format))
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def _to_mvg(spec, path, scale: float, w=None, h=None, unit=None, dpi=None) -> Union[str, None]:
|
|
1049
|
+
from .. import _kbridge
|
|
1050
|
+
|
|
1051
|
+
mvg = _kbridge._generate_mvg(spec.as_dict(), w, h, unit, dpi, scale)
|
|
1052
|
+
|
|
1053
|
+
if path is None:
|
|
1054
|
+
return mvg
|
|
1055
|
+
elif isinstance(path, str):
|
|
1056
|
+
abspath = _makedirs(path)
|
|
1057
|
+
with io.open(abspath, mode="w", encoding="utf-8") as f:
|
|
1058
|
+
f.write(mvg)
|
|
1059
|
+
return abspath
|
|
1060
|
+
else:
|
|
1061
|
+
path.write(mvg.encode())
|
|
1062
|
+
return None
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def _makedirs(path: str) -> str:
|
|
1066
|
+
"""Return absolute path to a file after creating all directories in the path."""
|
|
1067
|
+
abspath = os.path.abspath(path)
|
|
1068
|
+
dirname = os.path.dirname(abspath)
|
|
1069
|
+
if dirname and not os.path.exists(dirname):
|
|
1070
|
+
os.makedirs(dirname)
|
|
1071
|
+
return abspath
|