lets-plot 4.7.0__cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.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.

Potentially problematic release.


This version of lets-plot might be problematic. Click here for more details.

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