Skip to content

Commit 9743b6c

Browse files
authored
Merge pull request #29 from highcharts-for-python/develop
PR for v.1.0.0-rc8
2 parents b42d438 + f238b0c commit 9743b6c

File tree

15 files changed

+191
-30
lines changed

15 files changed

+191
-30
lines changed

CHANGES.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
Release 1.0.0-rc8
2+
=========================================
3+
4+
* **BUG:** #25. Fixed the edge case where if multiple notebooks are open in Jupyter Labs and
5+
different notebooks use the same container, the charts get rendered in *one* container.
6+
* **BUG:** Fixed bug when serializing a string value equal to ``'Date'``.
7+
* **BUG:** Fixed boolean handling in ``options.legend.LegendOptions.shadow``.
8+
* **Enhancement:** Added ``.from_array()`` support to the ``decorators.validate_types()`` function.
9+
* **BUG:** Fixed data valization in ``options.plot_options.pie.PieOptions.end_angle`` and ``.start_angle``.
10+
* Added ``date`` and ``datetime`` support to axis min and max.
11+
* Added iterable support to ``.from_dict()`` method.
12+
13+
---------------
14+
115
Release 1.0.0-rc7
216
=========================================
317

docs/api/options/series/heatmap.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
##########################################################################################
2-
:mod:`.Heatmap <highcharts_core.options.series.heatmap>`
2+
:mod:`.heatmap <highcharts_core.options.series.heatmap>`
33
##########################################################################################
44

55
.. contents:: Module Contents

highcharts_core/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.0.0-rc7'
1+
__version__ = '1.0.0-rc8'

highcharts_core/chart.py

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def __init__(self, **kwargs):
2323
self._options = None
2424
self._variable_name = None
2525

26+
self._random_slug = {}
27+
2628
self.callback = kwargs.get('callback', None)
2729
self.container = kwargs.get('container', None)
2830
self.options = kwargs.get('options', None)
@@ -54,6 +56,7 @@ def _jupyter_include_scripts(self):
5456
def _jupyter_javascript(self,
5557
global_options = None,
5658
container = None,
59+
random_slug = None,
5760
retries = 3,
5861
interval = 1000):
5962
"""Return the JavaScript code which Jupyter Labs will need to render the chart.
@@ -68,6 +71,10 @@ def _jupyter_javascript(self,
6871
property if set, and ``'highcharts_target_div'`` if not set.
6972
:type container: :class:`str <python:str>` or :obj:`None <python:None>`
7073
74+
:param random_slug: The random sequence of characters to append to the container name to ensure uniqueness.
75+
Defaults to :obj:`None <python:None>`
76+
:type random_slug: :class:`str <python:str>` or :obj:`None <python:None>`
77+
7178
:param retries: The number of times to retry rendering the chart. Used to avoid race conditions with the
7279
Highcharts script. Defaults to 3.
7380
:type retries: :class:`int <python:int>`
@@ -79,35 +86,46 @@ def _jupyter_javascript(self,
7986
:rtype: :class:`str <python:str>`
8087
"""
8188
original_container = self.container
82-
self.container = container or self.container or 'highcharts_target_div'
89+
new_container = container or self.container or 'highcharts_target_div'
90+
if not random_slug:
91+
self.container = new_container
92+
else:
93+
self.container = f'{new_container}_{random_slug}'
8394

8495
if global_options is not None:
8596
global_options = validate_types(global_options,
8697
types = SharedOptions)
8798

8899
js_str = ''
89100
js_str += utility_functions.get_retryHighcharts()
90-
101+
91102
if global_options:
92103
js_str += '\n' + utility_functions.prep_js_for_jupyter(global_options.to_js_literal()) + '\n'
93104

94105
js_str += utility_functions.prep_js_for_jupyter(self.to_js_literal(),
95106
container = self.container,
107+
random_slug = random_slug,
96108
retries = retries,
97109
interval = interval)
98110

99111
self.container = original_container
100112

101113
return js_str
102114

103-
def _jupyter_container_html(self, container = None):
115+
def _jupyter_container_html(self,
116+
container = None,
117+
random_slug = None):
104118
"""Returns the Jupyter Labs HTML container for rendering the chart in Jupyter Labs context.
105119
106120
:param container: The ID to apply to the HTML container when rendered in Jupyter Labs. Defaults to
107121
:obj:`None <python:None>`, which applies the :meth:`.container <highcharts_core.chart.Chart.container>`
108122
property if set, and ``'highcharts_target_div'`` if not set.
109123
:type container: :class:`str <python:str>` or :obj:`None <python:None>`
110124
125+
:param random_slug: The random sequence of characters to append to the container/function name to ensure
126+
uniqueness. Defaults to :obj:`None <python:None>`
127+
:type random_slug: :class:`str <python:str>` or :obj:`None <python:None>`
128+
111129
:rtype: :class:`str <python:str>`
112130
"""
113131
if self.options.chart:
@@ -116,6 +134,8 @@ def _jupyter_container_html(self, container = None):
116134
height = 400
117135

118136
container = container or self.container or 'highcharts_target_div'
137+
if random_slug:
138+
container = f'{container}_{random_slug}'
119139

120140
container_str = f"""<div id=\"{container}\" style=\"width:100%; height:{height};\"></div>\n"""
121141

@@ -426,6 +446,9 @@ def _copy_dict_key(cls,
426446
preserve_data = kwargs.get('preserve_data', True)
427447

428448
original_value = original[key]
449+
if other is None:
450+
other = {}
451+
429452
other_value = other.get(key, None)
430453

431454
if key == 'data' and preserve_data:
@@ -525,9 +548,9 @@ def copy(self,
525548
:returns: A mutated version of ``other`` with new property values
526549
527550
"""
528-
super().copy(other = other,
529-
overwrite = overwrite,
530-
**kwargs)
551+
return super().copy(other = other,
552+
overwrite = overwrite,
553+
**kwargs)
531554

532555
def add_series(self, *series):
533556
"""Adds ``series`` to the
@@ -666,6 +689,18 @@ def display(self,
666689
:param container: The ID to apply to the HTML container when rendered in Jupyter Labs. Defaults to
667690
:obj:`None <python:None>`, which applies the :meth:`.container <highcharts_core.chart.Chart.container>`
668691
property if set, and ``'highcharts_target_div'`` if not set.
692+
693+
.. note::
694+
695+
Highcharts for Python will append a 6-character random string to the value of ``container``
696+
to ensure uniqueness of the chart's container when rendering in a Jupyter Notebook/Labs context. The
697+
:class:`Chart <highcharts_core.chart.Chart>` instance will retain the mapping between container and the
698+
random string so long as the instance exists, thus allowing you to easily update the rendered chart by
699+
calling the :meth:`.display() <highcharts_core.chart.Chart.display>` method again.
700+
701+
If you wish to create a new chart from the instance that does not update the existing chart, then you can do
702+
so by specifying a new ``container`` value.
703+
669704
:type container: :class:`str <python:str>` or :obj:`None <python:None>`
670705
671706
:param retries: The number of times to retry rendering the chart. Used to avoid race conditions with the
@@ -693,11 +728,21 @@ def display(self,
693728
include_display = display_mod.Javascript(data = include_js_str)
694729

695730
container = container or self.container or 'highcharts_target_div'
696-
html_str = self._jupyter_container_html(container)
731+
if not self._random_slug:
732+
self._random_slug = {}
733+
734+
random_slug = self._random_slug.get(container, None)
735+
736+
if not random_slug:
737+
random_slug = utility_functions.get_random_string()
738+
self._random_slug[container] = random_slug
739+
740+
html_str = self._jupyter_container_html(container, random_slug)
697741
html_display = display_mod.HTML(data = html_str)
698742

699743
chart_js_str = self._jupyter_javascript(global_options = global_options,
700744
container = container,
745+
random_slug = random_slug,
701746
retries = retries,
702747
interval = interval)
703748
javascript_display = display_mod.Javascript(data = chart_js_str)

highcharts_core/decorators.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Implements decorators used throughout the library."""
22
import json
33
from functools import wraps
4+
from collections import UserDict
45

56
from validator_collection import checkers
67

@@ -113,7 +114,13 @@ def validate_types(value,
113114
except ValueError:
114115
pass
115116

116-
if allow_json and isinstance(value, (str, bytes)):
117+
if (
118+
force_iterable and
119+
checkers.is_iterable(value, forbid_literals = (str, dict, bytes, UserDict)) and
120+
hasattr(primary_type, 'from_array')
121+
):
122+
value = primary_type.from_array(value)
123+
elif allow_json and isinstance(value, (str, bytes)):
117124
try:
118125
value = primary_type.from_json(value)
119126
except AttributeError:

highcharts_core/js_literal_functions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,14 +205,16 @@ def get_js_literal(item) -> str:
205205
as_str += ',\n'
206206
as_str += ']'
207207
elif checkers.is_string(item):
208-
if item.startswith('[') or item.startswith('Date'):
208+
if (item.startswith('[') or item.startswith('Date')) and item != 'Date':
209209
as_str += f"""{item}"""
210210
elif item.startswith('{') and item.endswith('}'):
211211
if is_js_object(item):
212212
as_str += f"""{item}"""
213213
elif "'" in item:
214214
item = item.replace("'", "\\'")
215215
as_str += f'"{item}"'
216+
else:
217+
as_str += f"'{item}'"
216218
elif item in string.whitespace:
217219
as_str += f"""`{item}`"""
218220
elif item.startswith == 'HCP: REPLACE-WITH-':

highcharts_core/metaclasses.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ def from_json(cls,
235235

236236
as_dict = json.loads(as_str)
237237

238+
if checkers.is_iterable(as_dict, forbid_literals = (str, bytes, dict, UserDict)):
239+
return [cls.from_dict(x, allow_snake_case = allow_snake_case)
240+
for x in as_dict]
241+
238242
return cls.from_dict(as_dict,
239243
allow_snake_case = allow_snake_case)
240244

highcharts_core/options/axes/generic.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from typing import Optional, List
22
from decimal import Decimal
3+
import datetime
34

4-
from validator_collection import validators
5+
from validator_collection import validators, checkers
56

67
from highcharts_core import constants, errors
78
from highcharts_core.decorators import class_sensitive
@@ -395,7 +396,7 @@ def margin(self, value):
395396
self._margin = validators.numeric(value, allow_empty = True)
396397

397398
@property
398-
def max(self) -> Optional[int | float | Decimal]:
399+
def max(self) -> Optional[int | float | Decimal | datetime.date | datetime.datetime]:
399400
"""The maximum value of the axis. If :obj:`None <python:None>`, the ``max`` value
400401
is automatically calculated. Defaults to :obj:`None <python:None>`.
401402
@@ -417,7 +418,14 @@ def max(self) -> Optional[int | float | Decimal]:
417418

418419
@max.setter
419420
def max(self, value):
420-
self._max = validators.numeric(value, allow_empty = True)
421+
if value is None:
422+
self._max = None
423+
elif checkers.is_date(value):
424+
self._max = validators.date(value)
425+
elif checkers.is_datetime(value):
426+
self._max = validators.datetime(value)
427+
else:
428+
self._max = validators.numeric(value, allow_empty = True)
421429

422430
@property
423431
def max_padding(self) -> Optional[int | float | Decimal]:
@@ -447,7 +455,7 @@ def max_padding(self, value):
447455
minimum = 0)
448456

449457
@property
450-
def min(self) -> Optional[int | float | Decimal]:
458+
def min(self) -> Optional[int | float | Decimal | datetime.date | datetime.datetime]:
451459
"""The minimum value of the axis. If :obj:`None <python:None>`, the ``min`` value
452460
is automatically calculated. Defaults to :obj:`None <python:None>`.
453461
@@ -473,7 +481,14 @@ def min(self) -> Optional[int | float | Decimal]:
473481

474482
@min.setter
475483
def min(self, value):
476-
self._min = validators.numeric(value, allow_empty = True)
484+
if value is None:
485+
self._min = None
486+
elif checkers.is_date(value):
487+
self._min = validators.date(value)
488+
elif checkers.is_datetime(value):
489+
self._min = validators.datetime(value)
490+
else:
491+
self._min = validators.numeric(value, allow_empty = True)
477492

478493
@property
479494
def minor_grid_line_color(self) -> Optional[str | Gradient | Pattern]:

highcharts_core/options/drilldown.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from highcharts_core.utility_classes.animation import AnimationOptions
99
from highcharts_core.utility_classes.breadcrumbs import BreadcrumbOptions
1010
from highcharts_core.options.series.base import SeriesBase
11-
11+
from highcharts_core.options.series.series_generator import create_series_obj
1212

1313
class Drilldown(HighchartsMeta):
1414
"""Options to configure :term:`drilldown` functionality in the chart, which
@@ -169,9 +169,14 @@ def series(self) -> Optional[List[SeriesBase]]:
169169
return self._series
170170

171171
@series.setter
172-
@class_sensitive(SeriesBase, force_iterable = True)
173172
def series(self, value):
174-
self._series = value
173+
value = validators.iterable(value, allow_empty = True)
174+
if not value:
175+
self._series = None
176+
else:
177+
self._series = [create_series_obj(x,
178+
default_type = None)
179+
for x in value]
175180

176181
@classmethod
177182
def _get_kwargs_from_dict(cls, as_dict):

highcharts_core/options/legend/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -645,9 +645,12 @@ def shadow(self, value):
645645
elif value is False:
646646
self._shadow = False
647647
else:
648-
value = validate_types(value,
649-
types = ShadowOptions,
650-
allow_none = False)
648+
if value is True:
649+
value = ShadowOptions(enabled = True)
650+
else:
651+
value = validate_types(value,
652+
types = ShadowOptions,
653+
allow_none = False)
651654
self._shadow = value
652655

653656
@property

0 commit comments

Comments
 (0)