lets-plot 4.3.3__cp312-cp312-win_amd64.whl → 4.4.0__cp312-cp312-win_amd64.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.

@@ -0,0 +1,123 @@
1
+ # Copyright (c) 2024. JetBrains s.r.o.
2
+ # Use of this source code is governed by the MIT license that can be found in the LICENSE file.
3
+ from datetime import datetime
4
+ from typing import Union, Dict, Iterable
5
+
6
+ from lets_plot._type_utils import is_polars_dataframe
7
+ from lets_plot.plot.util import is_pandas_data_frame
8
+
9
+ TYPE_INTEGER = 'int'
10
+ TYPE_FLOATING = 'float'
11
+ TYPE_STRING = 'str'
12
+ TYPE_BOOLEAN = 'bool'
13
+ TYPE_DATE_TIME = 'datetime'
14
+ TYPE_UNKNOWN = 'unknown'
15
+
16
+
17
+ def infer_type(data: Union[Dict, 'pandas.DataFrame', 'polars.DataFrame']) -> Dict[str, str]:
18
+ type_info = {}
19
+
20
+ if is_pandas_data_frame(data):
21
+ import pandas as pd
22
+ import numpy as np # np is a dependency of pandas, we can import it without checking
23
+
24
+ for var_name, var_content in data.items():
25
+ if data.empty:
26
+ type_info[var_name] = TYPE_UNKNOWN
27
+ continue
28
+
29
+ inferred_type = pd.api.types.infer_dtype(var_content.values, skipna=True)
30
+ if inferred_type == "categorical":
31
+ dtype = var_content.cat.categories.dtype
32
+
33
+ if np.issubdtype(dtype, np.integer):
34
+ type_info[var_name] = TYPE_INTEGER
35
+ elif np.issubdtype(dtype, np.floating):
36
+ type_info[var_name] = TYPE_FLOATING
37
+ elif np.issubdtype(dtype, np.object_):
38
+ # Check if all elements are strings
39
+ if all(isinstance(x, str) for x in var_content.cat.categories):
40
+ type_info[var_name] = TYPE_STRING
41
+ else:
42
+ type_info[var_name] = TYPE_UNKNOWN
43
+ else:
44
+ type_info[var_name] = TYPE_UNKNOWN
45
+ else:
46
+ # see https://pandas.pydata.org/docs/reference/api/pandas.api.types.infer_dtype.html
47
+ if inferred_type == 'string':
48
+ type_info[var_name] = TYPE_STRING
49
+ elif inferred_type == 'floating':
50
+ type_info[var_name] = TYPE_FLOATING
51
+ elif inferred_type == 'integer':
52
+ type_info[var_name] = TYPE_INTEGER
53
+ elif inferred_type == 'boolean':
54
+ type_info[var_name] = TYPE_BOOLEAN
55
+ elif inferred_type == 'datetime64' or inferred_type == 'datetime':
56
+ type_info[var_name] = TYPE_DATE_TIME
57
+ elif inferred_type == "date":
58
+ type_info[var_name] = TYPE_DATE_TIME
59
+ elif inferred_type == 'empty': # for columns with all None values
60
+ type_info[var_name] = TYPE_UNKNOWN
61
+ else:
62
+ type_info[var_name] = 'unknown(pandas:' + inferred_type + ')'
63
+ elif is_polars_dataframe(data):
64
+ import polars as pl
65
+ from polars.datatypes.group import INTEGER_DTYPES, FLOAT_DTYPES
66
+ for var_name, var_type in data.schema.items():
67
+
68
+ # https://docs.pola.rs/api/python/stable/reference/datatypes.html
69
+ if var_type in FLOAT_DTYPES:
70
+ type_info[var_name] = TYPE_FLOATING
71
+ elif var_type in INTEGER_DTYPES:
72
+ type_info[var_name] = TYPE_INTEGER
73
+ elif var_type == pl.datatypes.String:
74
+ type_info[var_name] = TYPE_STRING
75
+ elif var_type == pl.datatypes.Boolean:
76
+ type_info[var_name] = TYPE_BOOLEAN
77
+ elif var_type == pl.datatypes.Date or var_type == pl.datatypes.Datetime:
78
+ type_info[var_name] = TYPE_DATE_TIME
79
+ else:
80
+ type_info[var_name] = 'unknown(polars:' + str(var_type) + ')'
81
+ elif isinstance(data, dict):
82
+ for var_name, var_content in data.items():
83
+ if isinstance(var_content, Iterable):
84
+ if not any(True for _ in var_content): # empty
85
+ type_info[var_name] = TYPE_UNKNOWN
86
+ continue
87
+
88
+ type_set = set(type(val) for val in var_content)
89
+ if None in type_set:
90
+ type_set.remove(None)
91
+
92
+ if len(type_set) > 1:
93
+ type_info[var_name] = 'unknown(mixed types)'
94
+ continue
95
+
96
+ try:
97
+ import numpy
98
+ except ImportError:
99
+ numpy = None
100
+
101
+ type_obj = list(type_set)[0]
102
+ if type_obj == bool:
103
+ type_info[var_name] = TYPE_BOOLEAN
104
+ elif issubclass(type_obj, int):
105
+ type_info[var_name] = TYPE_INTEGER
106
+ elif issubclass(type_obj, float):
107
+ type_info[var_name] = TYPE_FLOATING
108
+ elif issubclass(type_obj, str):
109
+ type_info[var_name] = TYPE_STRING
110
+ elif issubclass(type_obj, datetime):
111
+ type_info[var_name] = TYPE_DATE_TIME
112
+ elif numpy and issubclass(type_obj, numpy.datetime64):
113
+ type_info[var_name] = TYPE_DATE_TIME
114
+ elif numpy and issubclass(type_obj, numpy.timedelta64):
115
+ type_info[var_name] = TYPE_DATE_TIME
116
+ elif numpy and issubclass(type_obj, numpy.integer):
117
+ type_info[var_name] = TYPE_INTEGER
118
+ elif numpy and issubclass(type_obj, numpy.floating):
119
+ type_info[var_name] = TYPE_FLOATING
120
+ else:
121
+ type_info[var_name] = 'unknown(python:' + str(type_obj) + ')'
122
+
123
+ return type_info
lets_plot/plot/stat.py CHANGED
@@ -498,11 +498,10 @@ def stat_sum(mapping=None, *, data=None, geom=None, position=None, show_legend=N
498
498
  --------
499
499
  .. jupyter-execute::
500
500
  :linenos:
501
- :emphasize-lines: 10
501
+ :emphasize-lines: 9
502
502
 
503
503
  import numpy as np
504
504
  from lets_plot import *
505
- from lets_plot.mapping import as_discrete
506
505
  LetsPlot.setup_html()
507
506
  n = 50
508
507
  np.random.seed(42)
@@ -515,11 +514,10 @@ def stat_sum(mapping=None, *, data=None, geom=None, position=None, show_legend=N
515
514
 
516
515
  .. jupyter-execute::
517
516
  :linenos:
518
- :emphasize-lines: 10
517
+ :emphasize-lines: 9
519
518
 
520
519
  import numpy as np
521
520
  from lets_plot import *
522
- from lets_plot.mapping import as_discrete
523
521
  LetsPlot.setup_html()
524
522
  n = 50
525
523
  np.random.seed(42)
lets_plot/plot/theme_.py CHANGED
@@ -69,6 +69,9 @@ def theme(*,
69
69
  plot_margin=None,
70
70
  plot_inset=None,
71
71
 
72
+ plot_title_position=None,
73
+ plot_caption_position=None,
74
+
72
75
  strip_background=None, # ToDo: x/y
73
76
  strip_text=None, # ToDo: x/y
74
77
  # ToDo: strip.placement
@@ -232,6 +235,14 @@ def theme(*,
232
235
  - a list of four numbers - the insets are applied to the top, right, bottom and left in that order.
233
236
 
234
237
  It is acceptable to use None for any side; in this case, the default value for the plot inset side will be used.
238
+ plot_title_position : {'panel', 'plot'}, default='panel'
239
+ Alignment of the plot title/subtitle.
240
+ A value of 'panel' means that title and subtitle are aligned to the plot panels.
241
+ A value of 'plot' means that title and subtitle are aligned to the entire plot (excluding margins).
242
+ plot_caption_position : {'panel', 'plot'}, default='panel'
243
+ Alignment of the plot caption.
244
+ A value of 'panel' means that caption is aligned to the plot panels.
245
+ A value of 'plot' means that caption is aligned to the entire plot (excluding margins).
235
246
  strip_background : str or dict
236
247
  Background of facet labels.
237
248
  Set 'blank' or result of `element_blank()` to draw nothing.
@@ -388,6 +399,7 @@ def element_rect(
388
399
  linetype : int or str
389
400
  Type of the line.
390
401
  Codes and names: 0 = 'blank', 1 = 'solid', 2 = 'dashed', 3 = 'dotted', 4 = 'dotdash', 5 = 'longdash', 6 = 'twodash'.
402
+ For more info see https://lets-plot.org/python/pages/aesthetics.html#line-types.
391
403
  blank : bool, default=False
392
404
  If True - draws nothing, and assigns no space.
393
405
 
lets_plot/plot/tooltip.py CHANGED
@@ -155,7 +155,7 @@ class layer_tooltips(FeatureSpec):
155
155
  - field='^X' - for all positional x,
156
156
  - field='^Y' - for all positional y.
157
157
 
158
- |
158
+ ----
159
159
 
160
160
  The string template in `format` will allow to change lines
161
161
  for the default tooltip without `line` specifying.
@@ -163,7 +163,7 @@ class layer_tooltips(FeatureSpec):
163
163
  Aes and var formats are not interchangeable, i.e. var format
164
164
  will not be applied to aes, mapped to this variable.
165
165
 
166
- |
166
+ ----
167
167
 
168
168
  For more info see https://lets-plot.org/python/pages/formats.html.
169
169
 
@@ -420,29 +420,29 @@ class layer_tooltips(FeatureSpec):
420
420
  The resulting string will be at the beginning of the general tooltip, centered and highlighted in bold.
421
421
  A long title can be split into multiple lines using `\\\\n` as a text separator.
422
422
 
423
- Examples
424
- --------
425
- .. jupyter-execute::
426
- :linenos:
427
- :emphasize-lines: 15
423
+ Examples
424
+ --------
425
+ .. jupyter-execute::
426
+ :linenos:
427
+ :emphasize-lines: 15
428
428
 
429
- import numpy as np
430
- from lets_plot import *
431
- LetsPlot.setup_html()
432
- n = 100
433
- np.random.seed(42)
434
- data = {
435
- 'id': np.arange(n),
436
- 'x': np.random.normal(size=n),
437
- 'y': np.random.normal(size=n),
438
- 'c': np.random.choice(['a', 'b'], size=n),
439
- 'w': np.random.randint(1, 11, size=n)
440
- }
441
- ggplot(data, aes('x', 'y')) + \\
442
- geom_point(aes(color='c', size='w'), show_legend=False, \\
443
- tooltips=layer_tooltips().title('@id')
444
- .line('color|@c')
445
- .line('size|@w'))
429
+ import numpy as np
430
+ from lets_plot import *
431
+ LetsPlot.setup_html()
432
+ n = 100
433
+ np.random.seed(42)
434
+ data = {
435
+ 'id': np.arange(n),
436
+ 'x': np.random.normal(size=n),
437
+ 'y': np.random.normal(size=n),
438
+ 'c': np.random.choice(['a', 'b'], size=n),
439
+ 'w': np.random.randint(1, 11, size=n)
440
+ }
441
+ ggplot(data, aes('x', 'y')) + \\
442
+ geom_point(aes(color='c', size='w'), show_legend=False, \\
443
+ tooltips=layer_tooltips().title('@id')
444
+ .line('color|@c')
445
+ .line('size|@w'))
446
446
 
447
447
  """
448
448
  self._tooltip_title = value
lets_plot/plot/util.py CHANGED
@@ -2,14 +2,13 @@
2
2
  # Copyright (c) 2019. JetBrains s.r.o.
3
3
  # Use of this source code is governed by the MIT license that can be found in the LICENSE file.
4
4
  #
5
- from collections.abc import Iterable
6
- from datetime import datetime
7
- from typing import Any, Tuple, Sequence, Optional, Dict
5
+ from typing import Any, Tuple, Sequence, Optional, Dict, List
8
6
 
9
- from lets_plot._type_utils import is_dict_or_dataframe, is_polars_dataframe
7
+ from lets_plot._type_utils import is_pandas_data_frame
10
8
  from lets_plot.geo_data_internals.utils import find_geo_names
11
9
  from lets_plot.mapping import MappingMeta
12
- from lets_plot.plot.core import aes
10
+ from lets_plot.plot.core import aes, FeatureSpec
11
+ from lets_plot.plot.series_meta import infer_type, TYPE_UNKNOWN
13
12
 
14
13
 
15
14
  def as_boolean(val, *, default):
@@ -19,104 +18,94 @@ def as_boolean(val, *, default):
19
18
  return bool(val) and val != 'False'
20
19
 
21
20
 
22
- def as_annotated_data(raw_data: Any, raw_mapping: Any) -> Tuple:
23
- data_meta = {}
21
+ def as_annotated_data(data: Any, mapping_spec: FeatureSpec) -> Tuple:
22
+ data_type_by_var: Dict[str, str] = {} # VarName to Type
23
+ mapping_meta_by_var: Dict[str, Dict[str, MappingMeta]] = {} # VarName to Dict[Aes, MappingMeta]
24
+ mappings = {} # Aes to VarName
24
25
 
25
- # data
26
- data = raw_data
27
-
28
- if is_data_pub_stream(data):
29
- data = {}
30
- for col_name in raw_data.col_names:
31
- data[col_name] = []
32
-
33
- data_meta.update({'pubsub': {'channel_id': raw_data.channel_id, 'col_names': raw_data.col_names}})
34
-
35
- # mapping
36
- mapping = {}
37
- mapping_meta = []
38
- # series annotations
39
- series_meta = []
40
-
41
- class VariableMeta:
42
- def __init__(self):
43
- self.levels = None
44
- self.aesthetics = []
45
- self.order = None
46
-
47
- variables_meta: Dict[str, VariableMeta] = {}
48
-
49
- if is_data_frame(data):
50
- dtypes = data.dtypes.to_dict().items()
51
- for column_name, dtype in dtypes:
52
- if dtype.name == 'category' and dtype.ordered:
53
- var_meta = VariableMeta()
54
- var_meta.levels = dtype.categories.to_list()
55
- variables_meta[column_name] = var_meta
56
-
57
- if raw_mapping is not None:
58
- for aesthetic, variable in raw_mapping.as_dict().items():
59
- if aesthetic == 'name': # ignore FeatureSpec.name property
26
+ # fill mapping_meta_by_var, mappings and data_type_by_var.
27
+ if mapping_spec is not None:
28
+ for key, spec in mapping_spec.props().items():
29
+ # key is either an aesthetic name or 'name' (FeatureSpec.name property)
30
+ if key == 'name': # ignore FeatureSpec.name property
60
31
  continue
61
-
62
- if isinstance(variable, MappingMeta):
63
- mapping[aesthetic] = variable.variable
64
- if variable.variable in variables_meta:
65
- var_meta = variables_meta[variable.variable]
66
- else:
67
- var_meta = VariableMeta()
68
- var_meta.aesthetics.append(aesthetic)
69
- if variable.levels is not None:
70
- var_meta.levels = variable.levels
71
- order = variable.parameters.get('order')
72
- if order is not None:
73
- var_meta.order = order
74
- variables_meta[variable.variable] = var_meta
32
+
33
+ if isinstance(spec, MappingMeta):
34
+ mappings[key] = spec.variable
35
+ mapping_meta_by_var.setdefault(spec.variable, {})[key] = spec
36
+ data_type_by_var[spec.variable] = TYPE_UNKNOWN
75
37
  else:
76
- mapping[aesthetic] = variable
77
-
78
- for variableName, settings in variables_meta.items():
79
- if settings.levels is not None:
80
- # series annotations
81
- series_meta.append({
82
- 'column': variableName,
83
- 'factor_levels': settings.levels,
84
- 'order': settings.order
85
- })
86
- else:
87
- # mapping annotations
88
- for aesthetic in settings.aesthetics:
89
- value = raw_mapping.as_dict().get(aesthetic)
90
- if value is not None and isinstance(value, MappingMeta):
91
- mapping_meta.append({
92
- 'aes': aesthetic,
93
- 'annotation': value.annotation,
94
- 'parameters': value.parameters
95
- })
96
-
97
- data_as_dict = None
98
- if is_dict_or_dataframe(data):
99
- data_as_dict = data
100
- elif is_polars_dataframe(data):
101
- data_as_dict = data.to_dict()
102
-
103
- if data_as_dict is not None:
104
- for column_name, values in data_as_dict.items():
105
- if isinstance(values, Iterable):
106
- not_empty_series = any(True for _ in values)
107
- if not_empty_series and all(isinstance(val, datetime) for val in values):
108
- series_meta.append({
109
- 'column': column_name,
110
- 'type': 'datetime'
111
- })
112
-
113
- if len(series_meta) > 0:
114
- data_meta.update({'series_annotations': series_meta})
115
-
116
- if len(mapping_meta) > 0:
117
- data_meta.update({'mapping_annotations': mapping_meta})
118
-
119
- return data, aes(**mapping), {'data_meta': data_meta}
38
+ mappings[key] = spec # spec is a variable name
39
+
40
+ data_type_by_var.update(infer_type(data))
41
+
42
+ # fill series annotations
43
+ series_annotations = {} # var to series_annotation
44
+ for var_name, data_type in data_type_by_var.items():
45
+ series_annotation = {}
46
+
47
+ if data_type != TYPE_UNKNOWN:
48
+ series_annotation['type'] = data_type
49
+
50
+ if is_pandas_data_frame(data) and data[var_name].dtype.name == 'category' and data[var_name].dtype.ordered:
51
+ series_annotation['factor_levels'] = data[var_name].cat.categories.to_list()
52
+ elif var_name in mapping_meta_by_var:
53
+ levels = last_not_none(list(map(lambda mm: mm.levels, mapping_meta_by_var[var_name].values())))
54
+ if levels is not None:
55
+ series_annotation['factor_levels'] = levels
56
+
57
+ if 'factor_levels' in series_annotation and var_name in mapping_meta_by_var:
58
+ order = last_not_none(list(map(lambda mm: mm.parameters['order'], mapping_meta_by_var[var_name].values())))
59
+ if order is not None:
60
+ series_annotation['order'] = order
61
+
62
+ if len(series_annotation) > 0:
63
+ series_annotation['column'] = var_name
64
+ series_annotations[var_name] = series_annotation
65
+
66
+ # fill mapping annotations
67
+ mapping_annotations = []
68
+ for var_name, meta_data in mapping_meta_by_var.items():
69
+ for aesthetic, mapping_meta in meta_data.items():
70
+ if mapping_meta.annotation == 'as_discrete':
71
+ if 'factor_levels' in series_annotations.get(var_name, {}):
72
+ # there is a bug - if label is set then levels are not applied
73
+ continue
74
+
75
+ mapping_annotation = {}
76
+
77
+ # Note that the label is always set; otherwise, the scale title will appear as 'color.cyl'
78
+ label = mapping_meta.parameters.get('label')
79
+ if label is not None:
80
+ mapping_annotation.setdefault('parameters', {})['label'] = label
81
+
82
+ if mapping_meta.levels is not None:
83
+ mapping_annotation['levels'] = mapping_meta.levels
84
+
85
+ order_by = mapping_meta.parameters.get('order_by')
86
+ if order_by is not None:
87
+ mapping_annotation.setdefault('parameters', {})['order_by'] = order_by
88
+
89
+ order = mapping_meta.parameters.get('order')
90
+ if order is not None:
91
+ mapping_annotation.setdefault('parameters', {})['order'] = order
92
+
93
+ # add mapping meta if custom label is set or if series annotation for var doesn't contain order options
94
+ # otherwise don't add mapping meta - it's redundant, nothing unique compared to series annotation
95
+ if len(mapping_annotation):
96
+ mapping_annotation['aes'] = aesthetic
97
+ mapping_annotation['annotation'] = 'as_discrete'
98
+ mapping_annotations.append(mapping_annotation)
99
+
100
+ data_meta = {}
101
+
102
+ if len(series_annotations) > 0:
103
+ data_meta.update({'series_annotations': list(series_annotations.values())})
104
+
105
+ if len(mapping_annotations) > 0:
106
+ data_meta.update({'mapping_annotations': mapping_annotations})
107
+
108
+ return data, aes(**mappings), {'data_meta': data_meta}
120
109
 
121
110
 
122
111
  def is_data_pub_stream(data: Any) -> bool:
@@ -147,7 +136,8 @@ def normalize_map_join(map_join):
147
136
  data_names = [map_join[0]]
148
137
  map_names = [map_join[1]]
149
138
  elif len(map_join) > 2: # ['foo', 'bar', 'baz'] -> error
150
- raise ValueError("map_join of type list[str] expected to have 1 or 2 items, but was {}".format(len(map_join)))
139
+ raise ValueError(
140
+ "map_join of type list[str] expected to have 1 or 2 items, but was {}".format(len(map_join)))
151
141
  else:
152
142
  raise invalid_map_join_format()
153
143
  elif all(isinstance(v, Sequence) and not isinstance(v, str) for v in map_join): # all items are lists
@@ -217,17 +207,20 @@ def geo_data_frame_to_crs(gdf: 'GeoDataFrame', use_crs: Optional[str]):
217
207
  return gdf.to_crs('EPSG:4326' if use_crs is None else use_crs)
218
208
 
219
209
 
220
- def is_ndarray(data) -> bool:
221
- try:
222
- from numpy import ndarray
223
- return isinstance(data, ndarray)
224
- except ImportError:
225
- return False
210
+ def key_int2str(data):
211
+ if is_pandas_data_frame(data):
212
+ if data.columns.inferred_type == 'integer' or data.columns.inferred_type == 'mixed-integer':
213
+ data.columns = data.columns.astype(str)
214
+ return data
226
215
 
216
+ if isinstance(data, dict):
217
+ return {(str(k) if isinstance(k, int) else k): v for k, v in data.items()}
227
218
 
228
- def is_data_frame(data: Any) -> bool:
229
- try:
230
- from pandas import DataFrame
231
- return isinstance(data, DataFrame)
232
- except ImportError:
233
- return False
219
+ return data
220
+
221
+
222
+ def last_not_none(lst: List) -> Optional[Any]:
223
+ for i in reversed(lst):
224
+ if i is not None:
225
+ return i
226
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lets-plot
3
- Version: 4.3.3
3
+ Version: 4.4.0
4
4
  Summary: An open source library for statistical plotting
5
5
  Home-page: https://lets-plot.org
6
6
  Author: JetBrains
@@ -36,7 +36,7 @@ Requires-Dist: palettable
36
36
 
37
37
  [![official JetBrains project](http://jb.gg/badges/official-flat-square.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub)
38
38
  [![License MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://raw.githubusercontent.com/JetBrains/lets-plot-kotlin/master/LICENSE)
39
- [![Latest Release](https://img.shields.io/github/v/release/JetBrains/lets-plot)](https://github.com/JetBrains/lets-plot-kotlin/releases/latest)
39
+ [![Latest Release](https://img.shields.io/github/v/release/JetBrains/lets-plot)](https://github.com/JetBrains/lets-plot/releases/latest)
40
40
 
41
41
 
42
42
  **Lets-Plot** is a multiplatform plotting library built on the principles of the Grammar of Graphics.
@@ -88,61 +88,37 @@ Also read:
88
88
  - [Scientific mode in PyCharm](https://www.jetbrains.com/help/pycharm/matplotlib-support.html)
89
89
  - [Scientific mode in IntelliJ IDEA](https://www.jetbrains.com/help/idea/matplotlib-support.html)
90
90
 
91
- ## What is new in 4.3.0
91
+ ## What is new in 4.4.0
92
92
 
93
- - #### `coord_polar()`
94
-
95
- The polar coordinate system is most commonly used for pie charts, but</br>
96
- it can also be used for constructing **Spider or Radar charts** using the `flat` option.
97
-
98
- <br>
99
- <img src="https://raw.githubusercontent.com/JetBrains/lets-plot/master/docs/f-24a/images/polar_coord_pie.png" alt="f-24a/images/polar_coord_pie.png" width="256" height="214">
100
- <img src="https://raw.githubusercontent.com/JetBrains/lets-plot/master/docs/f-24a/images/radar_chart.png" alt="f-24a/images/radar_chart.png" width="256" height="196">
101
-
102
- See: [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24a/coord_polar.ipynb).
103
-
104
- - #### In the `theme()`:
105
-
106
- - `panel_inset` parameter - primarily used for plots with polar coordinates.
107
-
108
- See: [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24a/theme_panel_inset.ipynb).
109
-
110
- - `panel_border_ontop` parameter - enables the drawing of panel border on top of the plot geoms.
111
- - `panel_grid_ontop, panel_grid_ontop_x, panel_grid_ontop_y` parameters - enable the drawing of grid lines on top of the plot geoms.
112
-
113
- - #### `geom_curve()`
114
-
115
- <br>
116
- <img src="https://raw.githubusercontent.com/JetBrains/lets-plot/master/docs/f-24a/images/curve_annotation.png" alt="f-24a/images/curve_annotation.png" width="338" height="296">
117
-
118
- See: [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24a/geom_curve.ipynb).
119
-
120
- - #### [**UNIQUE**] Visualizing Graph-like Data with `geom_segment()` and `geom_curve()`
93
+ - #### Waterfall Plot
94
+ <img src="https://raw.githubusercontent.com/JetBrains/lets-plot/master/docs/f-24e/images/waterfall.png" alt="f-24e/images/waterfall.png" width="460" height="220">
95
+
96
+ See [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24e/waterfall_plot.ipynb).
121
97
 
122
- - Aesthetics `size_start, size_end, stroke_start` and `stroke_end` enable better alignment of</br>
123
- segments/curves with nodes of the graph by considering the size of the nodes.
98
+ - #### **`geom_band()`**:
99
+ <img src="https://raw.githubusercontent.com/JetBrains/lets-plot/master/docs/f-24e/images/geom_band.png" alt="f-24e/images/geom_band.png.png" width="615" height="220">
124
100
 
125
- - The `spacer` parameter allows for additional manual fine-tuning.
101
+ See [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24e/geom_band.ipynb).
126
102
 
127
- <br>
128
- <img src="https://raw.githubusercontent.com/JetBrains/lets-plot/master/docs/f-24a/images/graph_simple.png" alt="f-24a/images/graph_simple.png" width="256" height="168">
129
- <img src="https://raw.githubusercontent.com/JetBrains/lets-plot/master/docs/f-24a/images/graph_on_map.png" alt="f-24a/images/graph_on_map.png" width="256" height="184">
103
+ - #### Custom Legends
104
+ - `manual_key` parameter in plot layer
105
+ - `layer_key()` function
106
+ <br>
107
+ <img src="https://raw.githubusercontent.com/JetBrains/lets-plot/master/docs/f-24e/images/custom_legend.png" alt="f-24e/images/custom_legend.png.png" width="294" height="147">
130
108
 
131
- See:
132
- - [A simple graph example](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24a/graph_edges.ipynb)
133
- - [An interactive map example](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24a/geom_curve_on_map.ipynb)
134
-
109
+ See [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24e/manual_legend.ipynb).
110
+
111
+ - #### Customizing Legends Appearence
112
+ The `override_aes` parameter in the `guide_legend()` function.
113
+
114
+ See [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24e/legend_override_aes.ipynb).
135
115
 
136
- - #### The `alpha_stroke` Parameter in `geom_label()`
137
116
 
138
- Use the `alpha_stroke` parameter to apply `alpha` to entire `label`. By default, `alpha` is only applied to the label background.
117
+ - #### And More
139
118
 
140
- See: [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24a/geom_label_alpha_stroke.ipynb).
119
+ See [CHANGELOG.md](https://github.com/JetBrains/lets-plot/blob/master/CHANGELOG.md) for a full list of changes.
141
120
 
142
- - #### Showing Plots in External Browser
143
121
 
144
- The [setup_show_ext()](https://lets-plot.org/python/pages/api/lets_plot.LetsPlot.html#lets_plot.LetsPlot.setup_show_ext) directive allows plots to be displayed in an external browser window.
145
-
146
122
  ## Recent Updates in the [Gallery](https://lets-plot.org/python/pages/gallery.html)
147
123
 
148
124
  <a href="https://nbviewer.org/github/JetBrains/lets-plot-docs/blob/master/source/examples/demo/venn_diagram.ipynb">
@@ -175,7 +151,7 @@ Also read:
175
151
 
176
152
  ## Change Log
177
153
 
178
- See [CHANGELOG.md](https://github.com/JetBrains/lets-plot/blob/master/CHANGELOG.md) for other changes and fixes.
154
+ [CHANGELOG.md](https://github.com/JetBrains/lets-plot/blob/master/CHANGELOG.md)
179
155
 
180
156
 
181
157
  ## Code of Conduct