diff --git a/CHANGES.rst b/CHANGES.rst index 263fb81..c5204e5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,16 @@ +Release 1.10.2 +========================================= + +* **BUGFIX:** Fixed type coercion for `plot_options.series.data_labels` to allow for series type-specific + label label classes. Closes #209. +* **BUGFIX:** Modified the default `Referer` and `User-Agent` strings for use when exporting charts. +* **DOCS:** Added some additional documentation around the `User-Agent` and `Referer` support for use when + exporting charts. +* **ENHANCEMENT:** Some minor refactoring. + +---- + Release 1.10.1 ========================================= diff --git a/highcharts_core/__version__.py b/highcharts_core/__version__.py index ff987d2..83b8f26 100644 --- a/highcharts_core/__version__.py +++ b/highcharts_core/__version__.py @@ -1 +1 @@ -__version__ = '1.10.1' +__version__ = "1.10.2" diff --git a/highcharts_core/chart.py b/highcharts_core/chart.py index bdcce4d..507ae15 100644 --- a/highcharts_core/chart.py +++ b/highcharts_core/chart.py @@ -11,7 +11,10 @@ from highcharts_core.utility_classes.javascript_functions import CallbackFunction from highcharts_core.js_literal_functions import serialize_to_js_literal from highcharts_core.headless_export import ExportServer -from highcharts_core.options.series.series_generator import create_series_obj, SERIES_CLASSES +from highcharts_core.options.series.series_generator import ( + create_series_obj, + SERIES_CLASSES, +) from highcharts_core.global_options.shared_options import SharedOptions @@ -72,35 +75,36 @@ def __init__(self, **kwargs): self._random_slug = {} - self.callback = kwargs.get('callback', None) - self.container = kwargs.get('container', None) - self.options = kwargs.get('options', None) - self.variable_name = kwargs.get('variable_name', None) - self.module_url = kwargs.get('module_url', - None) or os.environ.get('HIGHCHARTS_MODULE_URL', - 'https://code.highcharts.com/') + self.callback = kwargs.get("callback", None) + self.container = kwargs.get("container", None) + self.options = kwargs.get("options", None) + self.variable_name = kwargs.get("variable_name", None) + self.module_url = kwargs.get("module_url", None) or os.environ.get( + "HIGHCHARTS_MODULE_URL", "https://code.highcharts.com/" + ) - series = kwargs.get('series', None) - series_type = kwargs.get('series_type', None) - data = kwargs.get('data', None) + series = kwargs.get("series", None) + series_type = kwargs.get("series_type", None) + data = kwargs.get("data", None) if series_type and not data: data = [] if series is not None: - if not checkers.is_iterable(series, forbid_literals = (str, bytes, dict, UserDict)): + if not checkers.is_iterable( + series, forbid_literals=(str, bytes, dict, UserDict) + ): series = [series] self.add_series(*series) elif data is not None and series_type: - series_as_dict = { - 'data': data, - 'type': series_type - } + series_as_dict = {"data": data, "type": series_type} self.add_series(series_as_dict) elif data is not None: - raise errors.HighchartsValueError('If ``data`` is provided, then ' - '``series_type`` must also be provided. ' - '``series_type`` was empty.') + raise errors.HighchartsValueError( + "If ``data`` is provided, then " + "``series_type`` must also be provided. " + "``series_type`` was empty." + ) def __str__(self): """Return a human-readable :class:`str ` representation of the chart. @@ -124,33 +128,38 @@ def __str__(self): """ as_dict = self.to_dict() - kwargs = {utility_functions.to_snake_case(key): as_dict[key] - for key in as_dict if key not in ['options', 'userOptions']} + kwargs = { + utility_functions.to_snake_case(key): as_dict[key] + for key in as_dict + if key not in ["options", "userOptions"] + } - if 'options' in as_dict: - kwargs['options'] = str(as_dict['options']) - elif 'userOptions' in as_dict: - kwargs['options'] = str(as_dict['userOptions']) + if "options" in as_dict: + kwargs["options"] = str(as_dict["options"]) + elif "userOptions" in as_dict: + kwargs["options"] = str(as_dict["userOptions"]) - kwargs_as_str = '' + kwargs_as_str = "" for index, key in enumerate(kwargs): if index > 0: - kwargs_as_str += ', ' - if key == 'options': - kwargs_as_str += f'options = {kwargs[key]}' + kwargs_as_str += ", " + if key == "options": + kwargs_as_str += f"options = {kwargs[key]}" else: - kwargs_as_str += f'{key} = {repr(kwargs[key])}' + kwargs_as_str += f"{key} = {repr(kwargs[key])}" - return f'{self.__class__.__name__}({kwargs_as_str})' + return f"{self.__class__.__name__}({kwargs_as_str})" def _jupyter_include_scripts(self, **kwargs): """Return the JavaScript code that is used to load the Highcharts JS libraries. :rtype: :class:`str ` """ - required_modules = [f'{self.module_url}{x}' - for x in self.get_required_modules(include_extension = True)] - js_str = '' + required_modules = [ + f"{self.module_url}{x}" + for x in self.get_required_modules(include_extension=True) + ] + js_str = "" for item in required_modules: js_str += utility_functions.jupyter_add_script(item) js_str += """.then(() => {""" @@ -160,12 +169,14 @@ def _jupyter_include_scripts(self, **kwargs): return js_str - def _jupyter_javascript(self, - global_options = None, - container = None, - random_slug = None, - retries = 5, - interval = 1000): + def _jupyter_javascript( + self, + global_options=None, + container=None, + random_slug=None, + retries=5, + interval=1000, + ): """Return the JavaScript code which Jupyter Labs will need to render the chart. :param global_options: The :term:`shared options` to use when rendering the chart. @@ -193,34 +204,37 @@ def _jupyter_javascript(self, :rtype: :class:`str ` """ original_container = self.container - new_container = container or self.container or 'highcharts_target_div' + new_container = container or self.container or "highcharts_target_div" if not random_slug: self.container = new_container else: - self.container = f'{new_container}_{random_slug}' + self.container = f"{new_container}_{random_slug}" if global_options is not None: - global_options = validate_types(global_options, - types = SharedOptions) + global_options = validate_types(global_options, types=SharedOptions) js_str = utility_functions.get_retryHighcharts() if global_options: - js_str += '\n' + utility_functions.prep_js_for_jupyter(global_options.to_js_literal()) + '\n' + js_str += ( + "\n" + + utility_functions.prep_js_for_jupyter(global_options.to_js_literal()) + + "\n" + ) - js_str += utility_functions.prep_js_for_jupyter(self.to_js_literal(), - container = self.container, - random_slug = random_slug, - retries = retries, - interval = interval) + js_str += utility_functions.prep_js_for_jupyter( + self.to_js_literal(), + container=self.container, + random_slug=random_slug, + retries=retries, + interval=interval, + ) self.container = original_container return js_str - def _jupyter_container_html(self, - container = None, - random_slug = None): + def _jupyter_container_html(self, container=None, random_slug=None): """Returns the Jupyter Labs HTML container for rendering the chart in Jupyter Labs context. :param container: The ID to apply to the HTML container when rendered in Jupyter Labs. Defaults to @@ -239,9 +253,9 @@ def _jupyter_container_html(self, else: height = 400 - container = container or self.container or 'highcharts_target_div' + container = container or self.container or "highcharts_target_div" if random_slug: - container = f'{container}_{random_slug}' + container = f"{container}_{random_slug}" container_str = f"""
\n""" @@ -253,7 +267,7 @@ def _repr_html_(self) -> str: :returns: The HTML representation of the chart. :rtype: :class:`str ` """ - container = self.container or 'highcharts_target_div' + container = self.container or "highcharts_target_div" if not self._random_slug: self._random_slug = {} @@ -265,9 +279,10 @@ def _repr_html_(self) -> str: html_str = self._jupyter_container_html(container, random_slug) - chart_js_str = self._jupyter_javascript(container = container, - random_slug = random_slug) - wrapped_chart_js_str = utility_functions.wrap_for_requirejs('', chart_js_str) + chart_js_str = self._jupyter_javascript( + container=container, random_slug=random_slug + ) + wrapped_chart_js_str = utility_functions.wrap_for_requirejs("", chart_js_str) include_js_str = self._get_jupyter_script_loader(chart_js_str) @@ -280,53 +295,53 @@ def _repr_html_(self) -> str: def _repr_png_(self) -> bytes: """Return a PNG representation of the chart, expressed as bytes. - + .. note:: - - This relies on the + + This relies on the :meth:`.download_chart() ` method, which in turn relies on the Highcharts Export Server. If you need to override - the default Export Server configuration, you can do so using environment - variables as documented for the + the default Export Server configuration, you can do so using environment + variables as documented for the :class:`ExportServer ` class. - + :rtype: :class:`bytes ` """ - return self.download_chart(format = 'png') + return self.download_chart(format="png") def _repr_svg_(self): """Return an SVG representation of the chart. - + .. note:: - - This relies on the + + This relies on the :meth:`.download_chart() ` method, which in turn relies on the Highcharts Export Server. If you need to override - the default Export Server configuration, you can do so using environment - variables as documented for the + the default Export Server configuration, you can do so using environment + variables as documented for the :class:`ExportServer ` class. - + :rtype: :class:`str ` """ - return self.download_chart(format = 'svg') + return self.download_chart(format="svg") def _repr_jpeg_(self) -> bytes: """Return a JPEG representation of the chart, expressed as bytes. - + .. note:: - - This relies on the + + This relies on the :meth:`.download_chart() ` method, which in turn relies on the Highcharts Export Server. If you need to override - the default Export Server configuration, you can do so using environment - variables as documented for the + the default Export Server configuration, you can do so using environment + variables as documented for the :class:`ExportServer ` class. - + :rtype: :class:`bytes ` """ - return self.download_chart(format = 'jpeg') + return self.download_chart(format="jpeg") - def get_script_tags(self, as_str = False) -> List[str] | str: + def get_script_tags(self, as_str=False) -> List[str] | str: """Return the collection of ``' - for x in self.get_required_modules(include_extension = True)] + scripts = [ + f'' + for x in self.get_required_modules(include_extension=True) + ] if as_str: - return '\n'.join(scripts) + return "\n".join(scripts) return scripts - def get_required_modules(self, - include_extension = False) -> List[str]: + def get_required_modules(self, include_extension=False) -> List[str]: """Return the list of URLs from which the Highcharts JavaScript modules needed to render the chart can be retrieved. @@ -357,7 +373,7 @@ def get_required_modules(self, :rtype: :class:`list ` of :class:`str ` """ - initial_scripts = ['highcharts'] + initial_scripts = ["highcharts"] scripts = self._process_required_modules(initial_scripts, include_extension) return scripts @@ -372,7 +388,7 @@ def _get_jupyter_script_loader(self, chart_js_str) -> str: :returns: The JavaScript code that loads the required modules. :rtype: :class:`str ` """ - if_no_requirejs = '' + if_no_requirejs = "" if_requirejs = """require.config({\n""" if_requirejs += """ packages: [{\n""" @@ -384,10 +400,10 @@ def _get_jupyter_script_loader(self, chart_js_str) -> str: if_requirejs += """ require([""" requirejs_modules = [] for item in self.get_required_modules(): - if item == 'highcharts' and item not in requirejs_modules: + if item == "highcharts" and item not in requirejs_modules: requirejs_modules.append(item) else: - revised_item = f'highcharts/{item}' + revised_item = f"highcharts/{item}" if revised_item not in requirejs_modules: requirejs_modules.append(revised_item) @@ -395,13 +411,15 @@ def _get_jupyter_script_loader(self, chart_js_str) -> str: is_last = index == len(requirejs_modules) - 1 if_requirejs += f"""'{item}'""" if not is_last: - if_requirejs += ', ' + if_requirejs += ", " if_requirejs += """], function (Highcharts) {\n""" if_requirejs += chart_js_str if_requirejs += """\n});""" - required_modules = [f'{self.module_url}{x}' - for x in self.get_required_modules(include_extension = True)] + required_modules = [ + f"{self.module_url}{x}" + for x in self.get_required_modules(include_extension=True) + ] for item in required_modules: if_no_requirejs += utility_functions.jupyter_add_script(item) if_no_requirejs += """.then(() => {""" @@ -470,11 +488,13 @@ def module_url(self) -> str: @module_url.setter def module_url(self, value): try: - value = validators.url(value, - allow_empty = True, - allow_special_ips = os.getenv('HCP_ALLOW_SPECIAL_IPS', False)) + value = validators.url( + value, + allow_empty=True, + allow_special_ips=os.getenv("HCP_ALLOW_SPECIAL_IPS", False), + ) except (ValueError, TypeError): - value = validators.path(value, allow_empty = True) + value = validators.path(value, allow_empty=True) self._module_url = value @@ -493,13 +513,14 @@ def options(self, value): if not value: self._options = None elif isinstance(value, SharedOptions): - raise errors.HighchartsValueError('Chart.options expects a HighchartsOptions instance ' - 'or a valid descendent. However, the value you supplied ' - 'is a SharedOptions instance, which wil a descendent is not ' - 'valid for this parameter.') + raise errors.HighchartsValueError( + "Chart.options expects a HighchartsOptions instance " + "or a valid descendent. However, the value you supplied " + "is a SharedOptions instance, which wil a descendent is not " + "valid for this parameter." + ) else: - value = validate_types(value, - types = HighchartsOptions) + value = validate_types(value, types=HighchartsOptions) self._options = value @@ -520,7 +541,7 @@ def container(self) -> Optional[str]: @container.setter def container(self, value): - self._container = validators.string(value, allow_empty = True) + self._container = validators.string(value, allow_empty=True) @property def variable_name(self) -> Optional[str]: @@ -549,35 +570,38 @@ def variable_name(self) -> Optional[str]: @variable_name.setter def variable_name(self, value): - self._variable_name = validators.variable_name(value, allow_empty = True) + self._variable_name = validators.variable_name(value, allow_empty=True) @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'callback': as_dict.get('callback', None), - 'container': as_dict.get('container', None) or as_dict.get('renderTo', None), - 'options': as_dict.get('options', None) or as_dict.get('userOptions', None), - 'variable_name': as_dict.get('variable_name', - None) or as_dict.get('variableName', None) + "callback": as_dict.get("callback", None), + "container": as_dict.get("container", None) + or as_dict.get("renderTo", None), + "options": as_dict.get("options", None) or as_dict.get("userOptions", None), + "variable_name": as_dict.get("variable_name", None) + or as_dict.get("variableName", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'callback': self.callback, - 'container': self.container, - 'userOptions': self.options + "callback": self.callback, + "container": self.container, + "userOptions": self.options, } return untrimmed - def to_js_literal(self, - filename = None, - encoding = 'utf-8', - careful_validation = False, - event_listener: str = 'DOMContentLoaded', - event_listener_enabled: bool = True) -> Optional[str]: + def to_js_literal( + self, + filename=None, + encoding="utf-8", + careful_validation=False, + event_listener: str = "DOMContentLoaded", + event_listener_enabled: bool = True, + ) -> Optional[str]: """Return the object represented as a :class:`str ` containing the JavaScript object literal. @@ -631,9 +655,9 @@ def to_js_literal(self, as_dict = {} for key in untrimmed: item = untrimmed[key] - serialized = serialize_to_js_literal(item, - encoding = encoding, - careful_validation = careful_validation) + serialized = serialize_to_js_literal( + item, encoding=encoding, careful_validation=careful_validation + ) if serialized is not None: as_dict[key] = serialized @@ -646,61 +670,71 @@ def to_js_literal(self, if self.options: options_as_str = "{}".format( - self.options.to_js_literal(encoding = encoding, - careful_validation = careful_validation) + self.options.to_js_literal( + encoding=encoding, careful_validation=careful_validation + ) ) else: options_as_str = """null""" - callback_as_str = '' + callback_as_str = "" if self.callback: callback_as_str = "{}".format( - self.callback.to_js_literal(encoding = encoding, - careful_validation = careful_validation) + self.callback.to_js_literal( + encoding=encoding, careful_validation=careful_validation + ) ) signature_elements += 1 signature = """Highcharts.chart(""" signature += container_as_str if signature_elements > 1: - signature += ',\n' + signature += ",\n" signature += options_as_str if signature_elements > 1: - signature += ',\n' + signature += ",\n" if callback_as_str: signature += callback_as_str - signature += ');' + signature += ");" - constructor_prefix = '' + constructor_prefix = "" if self.variable_name: - constructor_prefix = f'var {self.variable_name} = ' + constructor_prefix = f"var {self.variable_name} = " as_str = constructor_prefix + signature if event_listener_enabled: if event_listener: - prefix = """document.addEventListener('""" + event_listener + """', function() {\n""" + prefix = ( + """document.addEventListener('""" + + event_listener + + """', function() {\n""" + ) else: prefix = """document.addEventListener(function() {\n""" suffix = """});""" - as_str = prefix + as_str + '\n' + suffix + as_str = prefix + as_str + "\n" + suffix - if validators.path(filename, allow_empty = True): - with open(filename, 'w', encoding = encoding) as file_: + if validators.path(filename, allow_empty=True): + with open(filename, "w", encoding=encoding) as file_: file_.write(as_str) return as_str - def download_chart(self, - format = 'png', - scale = 1, - width = None, - filename = None, - auth_user = None, - auth_password = None, - timeout = 3, - server_instance = None, - **kwargs): + def download_chart( + self, + format="png", + scale=1, + width=None, + filename=None, + auth_user=None, + auth_password=None, + timeout=3, + referer=None, + user_agent=None, + server_instance=None, + **kwargs, + ): """Export a downloaded form of the chart using a Highcharts :term:`Export Server`. :param filename: The name of the file where the exported chart should (optionally) @@ -722,6 +756,18 @@ def download_chart(self, than the ``timeout`` value. Defaults to ``3``. :type timeout: numeric or :obj:`None ` + :param referer: Provide the referer URL to use when making the request to the Export + Server. If not specified, will try to read from the + ``HIGHCHARTS_EXPORT_SERVER_REFERER`` environment variable. If that is not found, then + will apply a default of ``https://www.highchartspython.com``. + :type referer: :class:`str ` or :obj:`None ` + + :param user_agent: Provide the user agent to use when making the request to the Export + Server. If not specified, will try to read from the ``HIGHCHARTS_EXPORT_SERVER_USER_AGENT`` + environment variable. If that is not found, then will apply a default submitting Highcharts + for Python as the user agent. + :type user_agent: :class:`str ` or :obj:`None ` + :param server_instance: Provide an already-configured :class:`ExportServer` instance to use to programmatically produce the exported chart. Defaults to :obj:`None `, which causes Highcharts for Python to instantiate @@ -737,44 +783,49 @@ def download_chart(self, keyword argument). :rtype: :class:`bytes ` or :class:`str ` """ - if checkers.is_type(self.options, 'HighchartsStockOptions'): - constructor = 'Stock' + if checkers.is_type(self.options, "HighchartsStockOptions"): + constructor = "Stock" else: - constructor = 'Chart' + constructor = "Chart" if not server_instance: - return ExportServer.get_chart(filename = filename, - auth_user = auth_user, - auth_password = auth_password, - timeout = timeout, - options = self.options, - constructor = constructor, - scale = scale, - width = width, - format_ = format, - **kwargs) + return ExportServer.get_chart( + filename=filename, + auth_user=auth_user, + auth_password=auth_password, + timeout=timeout, + options=self.options, + constructor=constructor, + scale=scale, + width=width, + format_=format, + referer=referer, + user_agent=user_agent, + **kwargs, + ) if not isinstance(server_instance, ExportServer): - raise errors.HighchartsValueError(f'server_instance is expected to be an ' - f'ExportServer instance. Was: ' - f'{server_instance.__class__.__name__}') - - return server_instance.request_chart(filename = filename, - auth_user = auth_user, - auth_password = auth_password, - timeout = timeout, - options = self.options, - constructor = constructor, - format_ = format, - **kwargs) + raise errors.HighchartsValueError( + f"server_instance is expected to be an " + f"ExportServer instance. Was: " + f"{server_instance.__class__.__name__}" + ) + + return server_instance.request_chart( + filename=filename, + auth_user=auth_user, + auth_password=auth_password, + timeout=timeout, + options=self.options, + constructor=constructor, + format_=format, + referer=referer, + user_agent=user_agent, + **kwargs, + ) @classmethod - def _copy_dict_key(cls, - key, - original, - other, - overwrite = True, - **kwargs): + def _copy_dict_key(cls, key, original, other, overwrite=True, **kwargs): """Copies the value of ``key`` from ``original`` to ``other``. :param key: The key that is to be copied. @@ -789,7 +840,7 @@ def _copy_dict_key(cls, :returns: The value that should be placed in ``other`` for ``key``. """ - preserve_data = kwargs.get('preserve_data', True) + preserve_data = kwargs.get("preserve_data", True) original_value = original[key] if other is None: @@ -797,13 +848,13 @@ def _copy_dict_key(cls, other_value = other.get(key, None) - if key == 'data' and preserve_data: + if key == "data" and preserve_data: return other_value - if key == 'points' and preserve_data: + if key == "points" and preserve_data: return other_value - if key == 'series' and preserve_data: + if key == "series" and preserve_data: if not other_value: return [x for x in original_value] @@ -825,11 +876,13 @@ def _copy_dict_key(cls, other_item = items[1] new_item = {} for subkey in original_item: - new_item_value = cls._copy_dict_key(subkey, - original_item, - new_item, - overwrite = overwrite, - **kwargs) + new_item_value = cls._copy_dict_key( + subkey, + original_item, + new_item, + overwrite=overwrite, + **kwargs, + ) new_item[subkey] = new_item_value updated_series.append(new_item) updated_series.extend(new_series) @@ -837,20 +890,18 @@ def _copy_dict_key(cls, return updated_series elif isinstance(original_value, (dict, UserDict)): - new_value = {subkey: cls._copy_dict_key(subkey, - original_value, - other_value, - overwrite = overwrite, - **kwargs) - for subkey in original_value} + new_value = { + subkey: cls._copy_dict_key( + subkey, original_value, other_value, overwrite=overwrite, **kwargs + ) + for subkey in original_value + } return new_value - elif checkers.is_iterable(original_value, - forbid_literals = (str, - bytes, - dict, - UserDict)): + elif checkers.is_iterable( + original_value, forbid_literals=(str, bytes, dict, UserDict) + ): if overwrite: new_value = [x for x in original_value] @@ -863,10 +914,7 @@ def _copy_dict_key(cls, return original_value - def copy(self, - other = None, - overwrite = True, - **kwargs): + def copy(self, other=None, overwrite=True, **kwargs): """Copy the configuration settings from this chart to the ``other`` chart. :param other: The target chart to which the properties of this chart should @@ -892,9 +940,7 @@ def copy(self, :returns: A mutated version of ``other`` with new property values """ - return super().copy(other = other, - overwrite = overwrite, - **kwargs) + return super().copy(other=other, overwrite=overwrite, **kwargs) def add_series(self, *series): """Adds ``series`` to the @@ -910,8 +956,7 @@ def add_series(self, *series): or coercable """ - new_series = [create_series_obj(item) - for item in series] + new_series = [create_series_obj(item) for item in series] if self.options and self.options.series: existing_series = [x for x in self.options.series] @@ -925,7 +970,7 @@ def add_series(self, *series): self.options.series = updated_series - def update_series(self, *series, add_if_unmatched = False): + def update_series(self, *series, add_if_unmatched=False): """Replace existing series with the new versions supplied in ``series``, matching them based on their :meth:`.id ` property. @@ -944,8 +989,7 @@ def update_series(self, *series, add_if_unmatched = False): if a series does not have a match on the chart. Defaults to ``False``. :type add_if_unmatched: :class:`bool ` """ - new_series = [create_series_obj(item) - for item in series] + new_series = [create_series_obj(item) for item in series] if self.options and self.options.series: existing_series = [x for x in self.options.series] @@ -959,23 +1003,25 @@ def update_series(self, *series, add_if_unmatched = False): new_ids = [x.id for x in new_series] overlap_ids = [x for x in new_ids if x in existing_ids] - updated_series = [existing - for existing in existing_series - if existing.id not in overlap_ids] + updated_series = [ + existing for existing in existing_series if existing.id not in overlap_ids + ] for new in new_series: if new.id not in overlap_ids and not add_if_unmatched: - raise errors.HighchartsMissingSeriesError(f'attempted to update series ' - f'id "{new.id}", but that ' - f'series is not present in ' - f'the chart') + raise errors.HighchartsMissingSeriesError( + f"attempted to update series " + f'id "{new.id}", but that ' + f"series is not present in " + f"the chart" + ) updated_series.append(new) self.options.series = updated_series @classmethod - def from_series(cls, *series, kwargs = None): + def from_series(cls, *series, kwargs=None): """Creates a new :class:`Chart ` instance populated with ``series``. @@ -1001,7 +1047,7 @@ def from_series(cls, *series, kwargs = None): :returns: A new :class:`Chart ` instance :rtype: :class:`Chart ` """ - kwargs = validators.dict(kwargs, allow_empty = True) or {} + kwargs = validators.dict(kwargs, allow_empty=True) or {} instance = cls(**kwargs) if checkers.is_iterable(series) is True: @@ -1012,11 +1058,7 @@ def from_series(cls, *series, kwargs = None): return instance - def display(self, - global_options = None, - container = None, - retries = 5, - interval = 1000): + def display(self, global_options=None, container=None, retries=5, interval=1000): """Display the chart in `Jupyter Labs `_ or `Jupyter Notebooks `_. @@ -1058,12 +1100,14 @@ def display(self, from IPython import display as display_mod from IPython.core.display_functions import display except ImportError: - raise errors.HighchartsDependencyError('Unable to import IPython modules. ' - 'Make sure that it is available in ' - 'your runtime environment. To install,' - 'use: pip install ipython') + raise errors.HighchartsDependencyError( + "Unable to import IPython modules. " + "Make sure that it is available in " + "your runtime environment. To install," + "use: pip install ipython" + ) - container = container or self.container or 'highcharts_target_div' + container = container or self.container or "highcharts_target_div" if not self._random_slug: self._random_slug = {} @@ -1074,30 +1118,34 @@ def display(self, self._random_slug[container] = random_slug html_str = self._jupyter_container_html(container, random_slug) - html_display = display_mod.HTML(data = html_str) - - chart_js_str = self._jupyter_javascript(global_options = global_options, - container = container, - random_slug = random_slug, - retries = retries, - interval = interval) - wrapped_chart_js_str = utility_functions.wrap_for_requirejs('', chart_js_str) - javascript_display = display_mod.Javascript(data = wrapped_chart_js_str) + html_display = display_mod.HTML(data=html_str) + + chart_js_str = self._jupyter_javascript( + global_options=global_options, + container=container, + random_slug=random_slug, + retries=retries, + interval=interval, + ) + wrapped_chart_js_str = utility_functions.wrap_for_requirejs("", chart_js_str) + javascript_display = display_mod.Javascript(data=wrapped_chart_js_str) include_js_str = self._get_jupyter_script_loader(chart_js_str) - include_display = display_mod.Javascript(data = include_js_str) + include_display = display_mod.Javascript(data=include_js_str) display(html_display) display(include_display) display(javascript_display) @classmethod - def from_array(cls, - value, - series_type = 'line', - series_kwargs = None, - options_kwargs = None, - chart_kwargs = None): + def from_array( + cls, + value, + series_type="line", + series_kwargs=None, + options_kwargs=None, + chart_kwargs=None, + ): """Create a :class:`Chart ` instance with one series populated from the array contained in ``value``. @@ -1155,21 +1203,23 @@ def from_array(cls, :rtype: :class:`Chart ` """ - series_type = validators.string(series_type, allow_empty = False) + series_type = validators.string(series_type, allow_empty=False) series_type = series_type.lower() if series_type not in SERIES_CLASSES: - raise errors.HighchartsValueError(f'series_type expects a valid Highcharts ' - f'series type. Received: {series_type}') + raise errors.HighchartsValueError( + f"series_type expects a valid Highcharts " + f"series type. Received: {series_type}" + ) - series_kwargs = validators.dict(series_kwargs, allow_empty = True) or {} - options_kwargs = validators.dict(options_kwargs, allow_empty = True) or {} - chart_kwargs = validators.dict(chart_kwargs, allow_empty = True) or {} + series_kwargs = validators.dict(series_kwargs, allow_empty=True) or {} + options_kwargs = validators.dict(options_kwargs, allow_empty=True) or {} + chart_kwargs = validators.dict(chart_kwargs, allow_empty=True) or {} series_cls = SERIES_CLASSES.get(series_type, None) - series = series_cls.from_array(value, series_kwargs = series_kwargs) + series = series_cls.from_array(value, series_kwargs=series_kwargs) - options_kwargs['series'] = [series] + options_kwargs["series"] = [series] options = HighchartsOptions(**options_kwargs) instance = cls(**chart_kwargs) @@ -1178,22 +1228,24 @@ def from_array(cls, return instance @classmethod - def from_csv_in_rows(cls, - as_string_or_file, - series_type = 'line', - has_header_row = True, - series_kwargs = None, - options_kwargs = None, - chart_kwargs = None, - delimiter = ',', - null_text = 'None', - wrapper_character = "'", - line_terminator = '\r\n', - wrap_all_strings = False, - double_wrapper_character_when_nested = False, - escape_character = "\\", - series_index = None, - **kwargs): + def from_csv_in_rows( + cls, + as_string_or_file, + series_type="line", + has_header_row=True, + series_kwargs=None, + options_kwargs=None, + chart_kwargs=None, + delimiter=",", + null_text="None", + wrapper_character="'", + line_terminator="\r\n", + wrap_all_strings=False, + double_wrapper_character_when_nested=False, + escape_character="\\", + series_index=None, + **kwargs, + ): """Create a new :class:`Chart ` instance with data populated from a CSV string or file. @@ -1342,43 +1394,47 @@ def from_csv_in_rows(cls, CSV columns by their label, but the CSV data does not contain a header row """ - return cls.from_csv(as_string_or_file, - property_column_map = None, - series_type = series_type, - has_header_row = has_header_row, - series_kwargs = series_kwargs, - options_kwargs = options_kwargs, - chart_kwargs = chart_kwargs, - delimiter = delimiter, - null_text = null_text, - wrapper_character = wrapper_character, - line_terminator = line_terminator, - wrap_all_strings = wrap_all_strings, - double_wrapper_character_when_nested = double_wrapper_character_when_nested, - escape_character = escape_character, - series_in_rows = True, - series_index = series_index, - **kwargs) + return cls.from_csv( + as_string_or_file, + property_column_map=None, + series_type=series_type, + has_header_row=has_header_row, + series_kwargs=series_kwargs, + options_kwargs=options_kwargs, + chart_kwargs=chart_kwargs, + delimiter=delimiter, + null_text=null_text, + wrapper_character=wrapper_character, + line_terminator=line_terminator, + wrap_all_strings=wrap_all_strings, + double_wrapper_character_when_nested=double_wrapper_character_when_nested, + escape_character=escape_character, + series_in_rows=True, + series_index=series_index, + **kwargs, + ) @classmethod - def from_csv(cls, - as_string_or_file, - property_column_map = None, - series_type = 'line', - has_header_row = True, - series_kwargs = None, - options_kwargs = None, - chart_kwargs = None, - delimiter = ',', - null_text = 'None', - wrapper_character = "'", - line_terminator = '\r\n', - wrap_all_strings = False, - double_wrapper_character_when_nested = False, - escape_character = "\\", - series_in_rows = False, - series_index = None, - **kwargs): + def from_csv( + cls, + as_string_or_file, + property_column_map=None, + series_type="line", + has_header_row=True, + series_kwargs=None, + options_kwargs=None, + chart_kwargs=None, + delimiter=",", + null_text="None", + wrapper_character="'", + line_terminator="\r\n", + wrap_all_strings=False, + double_wrapper_character_when_nested=False, + escape_character="\\", + series_in_rows=False, + series_index=None, + **kwargs, + ): """Create a new :class:`Chart ` instance with data populated from a CSV string or file. @@ -1598,44 +1654,48 @@ def from_csv(cls, CSV columns by their label, but the CSV data does not contain a header row """ - series_type = validators.string(series_type, allow_empty = False) + series_type = validators.string(series_type, allow_empty=False) series_type = series_type.lower() if series_type not in SERIES_CLASSES: - raise errors.HighchartsValueError(f'series_type expects a valid Highcharts ' - f'series type. Received: {series_type}') + raise errors.HighchartsValueError( + f"series_type expects a valid Highcharts " + f"series type. Received: {series_type}" + ) - options_kwargs = validators.dict(options_kwargs, allow_empty = True) or {} - chart_kwargs = validators.dict(chart_kwargs, allow_empty = True) or {} + options_kwargs = validators.dict(options_kwargs, allow_empty=True) or {} + chart_kwargs = validators.dict(chart_kwargs, allow_empty=True) or {} series_cls = SERIES_CLASSES.get(series_type, None) if series_in_rows: series = series_cls.from_csv_in_rows( as_string_or_file, - has_header_row = has_header_row, - series_kwargs = series_kwargs, - delimiter = delimiter, - null_text = null_text, - wrapper_character = wrapper_character, - line_terminator = line_terminator, - wrap_all_strings = wrap_all_strings, - double_wrapper_character_when_nested = double_wrapper_character_when_nested, - escape_character = escape_character, - **kwargs + has_header_row=has_header_row, + series_kwargs=series_kwargs, + delimiter=delimiter, + null_text=null_text, + wrapper_character=wrapper_character, + line_terminator=line_terminator, + wrap_all_strings=wrap_all_strings, + double_wrapper_character_when_nested=double_wrapper_character_when_nested, + escape_character=escape_character, + **kwargs, ) else: - series = series_cls.from_csv(as_string_or_file, - property_column_map = property_column_map, - has_header_row = has_header_row, - series_kwargs = series_kwargs, - delimiter = delimiter, - null_text = null_text, - wrapper_character = wrapper_character, - line_terminator = line_terminator, - wrap_all_strings = wrap_all_strings, - double_wrapper_character_when_nested = double_wrapper_character_when_nested, - escape_character = escape_character, - **kwargs) + series = series_cls.from_csv( + as_string_or_file, + property_column_map=property_column_map, + has_header_row=has_header_row, + series_kwargs=series_kwargs, + delimiter=delimiter, + null_text=null_text, + wrapper_character=wrapper_character, + line_terminator=line_terminator, + wrap_all_strings=wrap_all_strings, + double_wrapper_character_when_nested=double_wrapper_character_when_nested, + escape_character=escape_character, + **kwargs, + ) if isinstance(series, list) and series_index is not None: series = series[series_index] @@ -1643,7 +1703,7 @@ def from_csv(cls, if not isinstance(series, list): series = [series] - options_kwargs['series'] = series + options_kwargs["series"] = series options = HighchartsOptions(**options_kwargs) @@ -1653,14 +1713,16 @@ def from_csv(cls, return instance @classmethod - def from_pandas_in_rows(cls, - df, - series_type = 'line', - series_kwargs = None, - options_kwargs = None, - chart_kwargs = None, - series_index = None, - **kwargs): + def from_pandas_in_rows( + cls, + df, + series_type="line", + series_kwargs=None, + options_kwargs=None, + chart_kwargs=None, + series_index=None, + **kwargs, + ): """Create a chart from a Pandas :class:`DataFrame `, treating each row in the dataframe as a :term:`series` instances. @@ -1725,27 +1787,31 @@ def from_pandas_in_rows(cls, not available in the runtime environment """ - return cls.from_pandas(df, - property_map = None, - series_type = series_type, - series_kwargs = series_kwargs, - options_kwargs = options_kwargs, - chart_kwargs = chart_kwargs, - series_in_rows = True, - series_index = series_index, - **kwargs) + return cls.from_pandas( + df, + property_map=None, + series_type=series_type, + series_kwargs=series_kwargs, + options_kwargs=options_kwargs, + chart_kwargs=chart_kwargs, + series_in_rows=True, + series_index=series_index, + **kwargs, + ) @classmethod - def from_pandas(cls, - df, - property_map = None, - series_type = 'line', - series_kwargs = None, - options_kwargs = None, - chart_kwargs = None, - series_in_rows = False, - series_index = None, - **kwargs): + def from_pandas( + cls, + df, + property_map=None, + series_type="line", + series_kwargs=None, + options_kwargs=None, + chart_kwargs=None, + series_in_rows=False, + series_index=None, + **kwargs, + ): """Create a :class:`Chart ` instance whose series are populated from a `pandas `_ :class:`DataFrame `. @@ -1879,48 +1945,56 @@ def from_pandas(cls, not available in the runtime environment """ if not series_type: - raise errors.HighchartsValueError('series_type cannot be empty') + raise errors.HighchartsValueError("series_type cannot be empty") series_type = str(series_type).lower() if series_type not in SERIES_CLASSES: - raise errors.HighchartsValueError(f'series_type expects a valid Highcharts ' - f'series type. Received: {series_type}') + raise errors.HighchartsValueError( + f"series_type expects a valid Highcharts " + f"series type. Received: {series_type}" + ) if not isinstance(options_kwargs, (dict, UserDict, type(None))): - raise errors.HighchartsValueError(f'options_kwarts expects a dict. ' - f'Received: {options_kwargs.__class__.__name__}') + raise errors.HighchartsValueError( + f"options_kwarts expects a dict. " + f"Received: {options_kwargs.__class__.__name__}" + ) if not options_kwargs: options_kwargs = {} if not isinstance(chart_kwargs, (dict, UserDict, type(None))): - raise errors.HighchartsValueError(f'chart_kwargs expects a dict. ' - f'Received: {chart_kwargs.__class__.__name__}') + raise errors.HighchartsValueError( + f"chart_kwargs expects a dict. " + f"Received: {chart_kwargs.__class__.__name__}" + ) if not chart_kwargs: chart_kwargs = {} if not isinstance(kwargs, (dict, UserDict, type(None))): - raise errors.HighchartsValueError(f'kwargs expects a dict. ' - f'Received: {kwargs.__class__.__name__}') + raise errors.HighchartsValueError( + f"kwargs expects a dict. " f"Received: {kwargs.__class__.__name__}" + ) if not kwargs: kwargs = {} series_cls = SERIES_CLASSES.get(series_type, None) if series_in_rows: - series = series_cls.from_pandas_in_rows(df, - series_kwargs = series_kwargs, - series_index = series_index, - **kwargs) + series = series_cls.from_pandas_in_rows( + df, series_kwargs=series_kwargs, series_index=series_index, **kwargs + ) else: - series = series_cls.from_pandas(df, - property_map = property_map, - series_kwargs = series_kwargs, - series_index = series_index, - **kwargs) + series = series_cls.from_pandas( + df, + property_map=property_map, + series_kwargs=series_kwargs, + series_index=series_index, + **kwargs, + ) if isinstance(series, series_cls): series = [series] - options_kwargs['series'] = series + options_kwargs["series"] = series options = HighchartsOptions(**options_kwargs) instance = cls(**chart_kwargs) @@ -1929,13 +2003,15 @@ def from_pandas(cls, return instance @classmethod - def from_pyspark(cls, - df, - property_map, - series_type, - series_kwargs = None, - options_kwargs = None, - chart_kwargs = None): + def from_pyspark( + cls, + df, + property_map, + series_type, + series_kwargs=None, + options_kwargs=None, + chart_kwargs=None, + ): """Create a :class:`Chart ` instance whose data is populated from a `PySpark `_ @@ -2000,20 +2076,20 @@ def from_pyspark(cls, `PySpark `_ is not available in the runtime environment """ - series_type = validators.string(series_type, allow_empty = False) + series_type = validators.string(series_type, allow_empty=False) series_type = series_type.lower() if series_type not in SERIES_CLASSES: - raise errors.HighchartsValueError(f'series_type expects a valid Highcharts ' - f'series type. Received: {series_type}') + raise errors.HighchartsValueError( + f"series_type expects a valid Highcharts " + f"series type. Received: {series_type}" + ) - options_kwargs = validators.dict(options_kwargs, allow_empty = True) or {} - chart_kwargs = validators.dict(chart_kwargs, allow_empty = True) or {} + options_kwargs = validators.dict(options_kwargs, allow_empty=True) or {} + chart_kwargs = validators.dict(chart_kwargs, allow_empty=True) or {} series_cls = SERIES_CLASSES.get(series_type, None) - series = series_cls.from_pyspark(df, - property_map, - series_kwargs) + series = series_cls.from_pyspark(df, property_map, series_kwargs) options = HighchartsOptions(**options_kwargs) options.series = [series] @@ -2024,9 +2100,7 @@ def from_pyspark(cls, return instance @classmethod - def from_options(cls, - options, - chart_kwargs = None): + def from_options(cls, options, chart_kwargs=None): """Create a :class:`Chart ` instance from a :class:`HighchartsOptions ` object. @@ -2049,12 +2123,10 @@ def from_options(cls, :returns: The :class:`Chart ` instance :rtype: :class:`Chart ` """ - chart_kwargs = validators.dict(chart_kwargs, allow_empty = True) or {} - options = validate_types(options, - types = (HighchartsOptions)) + chart_kwargs = validators.dict(chart_kwargs, allow_empty=True) or {} + options = validate_types(options, types=(HighchartsOptions)) instance = cls(**chart_kwargs) instance.options = options return instance - diff --git a/highcharts_core/headless_export.py b/highcharts_core/headless_export.py index 44ba137..cc3ec2f 100644 --- a/highcharts_core/headless_export.py +++ b/highcharts_core/headless_export.py @@ -1,5 +1,6 @@ try: from dotenv import load_dotenv + load_dotenv() except ImportError: pass @@ -31,7 +32,7 @@ class ExportServer(HighchartsMeta): configure the class using either the :meth:`url `, :meth:`port `, and :meth:`path ` properties explicitly or by setting - the ``HIGHCHARTS_EXPORT_SERVER_DOMAIN`, ``HIGHCHARTS_EXPORT_SERVER_PORT``, or + the ``HIGHCHARTS_EXPORT_SERVER_DOMAIN``, ``HIGHCHARTS_EXPORT_SERVER_PORT``, or ``HIGHCHARTS_EXPORT_SERVER_PATH`` environment variables. """ @@ -58,89 +59,102 @@ def __init__(self, **kwargs): self._files = None self._css = None self._js = None - + self._referer = None self._user_agent = None - self.protocol = kwargs.get('protocol', - os.getenv('HIGHCHARTS_EXPORT_SERVER_PROTOCOL', - 'https')) - self.domain = kwargs.get('domain', os.getenv('HIGHCHARTS_EXPORT_SERVER_DOMAIN', - 'export.highcharts.com')) - self.port = kwargs.get('port', os.getenv('HIGHCHARTS_EXPORT_SERVER_PORT', - '')) - self.path = kwargs.get('path', os.getenv('HIGHCHARTS_EXPORT_SERVER_PATH', - '')) - - self.options = kwargs.get('options', None) - self.format_ = kwargs.get('format_', kwargs.get('type', 'png')) - self.scale = kwargs.get('scale', 1) - self.width = kwargs.get('width', None) - self.height = kwargs.get('height', None) - self.callback = kwargs.get('callback', None) - self.constructor = kwargs.get('constructor', 'chart') - self.use_base64 = kwargs.get('use_base64', False) - self.no_download = kwargs.get('no_download', False) - self.async_rendering = kwargs.get('async_rendering', False) - self.global_options = kwargs.get('global_options', None) - self.data_options = kwargs.get('data_options', None) - self.custom_code = kwargs.get('custom_code', None) - - files = kwargs.get('files', None) - css = kwargs.get('css', None) - js = kwargs.get('js', None) - resources = kwargs.get('resources', None) - - self.referer = kwargs.get('referer', - os.getenv('HIGHCHARTS_EXPORT_SERVER_REFERER', 'https://www.highcharts.com')) - self.user_agent = kwargs.get('user_agent', - os.getenv('HIGHCHARTS_EXPORT_SERVER_USER_AGENT', None)) - + self.protocol = kwargs.get( + "protocol", os.getenv("HIGHCHARTS_EXPORT_SERVER_PROTOCOL", "https") + ) + self.domain = kwargs.get( + "domain", + os.getenv("HIGHCHARTS_EXPORT_SERVER_DOMAIN", "export.highcharts.com"), + ) + self.port = kwargs.get("port", os.getenv("HIGHCHARTS_EXPORT_SERVER_PORT", "")) + self.path = kwargs.get("path", os.getenv("HIGHCHARTS_EXPORT_SERVER_PATH", "")) + + self.options = kwargs.get("options", None) + self.format_ = kwargs.get("format_", kwargs.get("type", "png")) + self.scale = kwargs.get("scale", 1) + self.width = kwargs.get("width", None) + self.height = kwargs.get("height", None) + self.callback = kwargs.get("callback", None) + self.constructor = kwargs.get("constructor", "chart") + self.use_base64 = kwargs.get("use_base64", False) + self.no_download = kwargs.get("no_download", False) + self.async_rendering = kwargs.get("async_rendering", False) + self.global_options = kwargs.get("global_options", None) + self.data_options = kwargs.get("data_options", None) + self.custom_code = kwargs.get("custom_code", None) + + files = kwargs.get("files", None) + css = kwargs.get("css", None) + js = kwargs.get("js", None) + resources = kwargs.get("resources", None) + + self.referer = kwargs.get( + "referer", + os.getenv( + "HIGHCHARTS_EXPORT_SERVER_REFERER", "https://www.highchartspython.com" + ), + ) + self.user_agent = kwargs.get( + "user_agent", os.getenv("HIGHCHARTS_EXPORT_SERVER_USER_AGENT", None) + ) + if resources: - self.resources = kwargs.get('resources', None) + self.resources = kwargs.get("resources", None) else: self.files = files self.css = css self.js = js - + super().__init__(**kwargs) @property def referer(self) -> Optional[str]: - """The referer to use when making requests to the export server. Defaults to the + """The referer to use when making requests to the export server. Defaults to the ``HIGHCHARTS_EXPORT_SERVER_REFERER`` environment variable if present, otherwise defaults to - ``'https://www.highcharts.com'``. + ``'https://www.highchartspython.com'``. :rtype: :class:`str ` or :obj:`None ` """ + if not self._referer: + return os.getenv( + "HIGHCHARTS_EXPORT_SERVER_REFERER", "https://www.highchartspython.com" + ) + return self._referer - + @referer.setter def referer(self, value): - value = validators.url(value, allow_empty = True) - if not value: - value = 'https://www.highcharts.com' - + value = validators.url(value, allow_empty=True) + self._referer = value - + @property def user_agent(self) -> Optional[str]: """The user agent to use when making requests to the export server. Defaults to the ``HIGHCHARTS_EXPORT_SERVER_USER_AGENT`` environment variable if present, otherwise defaults to - ``Highcharts Core for Python / v.. + ``Highcharts Core for Python / v.``. :rtype: :class:`str ` or :obj:`None ` """ if self._user_agent: return self._user_agent - - return f'Highcharts Core for Python / v.{highcharts_version.__version__}' - + + user_agent = os.getenv( + "HIGHCHARTS_EXPORT_SERVER_USER_AGENT", + f"HighchartsCoreforPy/{highcharts_version.__version__}", + ) + + return user_agent + @user_agent.setter def user_agent(self, value): - value = validators.string(value, allow_empty = True) + value = validators.string(value, allow_empty=True) if not value: value = None - + self._user_agent = value @property @@ -167,15 +181,17 @@ def protocol(self) -> Optional[str]: @protocol.setter def protocol(self, value): - value = validators.string(value, allow_empty = True) + value = validators.string(value, allow_empty=True) if not value: - value = os.getenv('HIGHCHARTS_EXPORT_SERVER_PROTOCOL', 'https') + value = os.getenv("HIGHCHARTS_EXPORT_SERVER_PROTOCOL", "https") value = value.lower() - if value not in ['https', 'http']: - raise errors.HighchartsUnsupportedProtocolError(f'protocol expects either ' - f'"https" or "http". ' - f'Received: "{value}"') + if value not in ["https", "http"]: + raise errors.HighchartsUnsupportedProtocolError( + f"protocol expects either " + f'"https" or "http". ' + f'Received: "{value}"' + ) self._protocol = value self._url = None @@ -203,10 +219,11 @@ def domain(self) -> Optional[str]: @domain.setter def domain(self, value): - value = validators.domain(value, allow_empty = True) + value = validators.domain(value, allow_empty=True) if not value: - value = os.getenv('HIGHCHARTS_EXPORT_SERVER_DOMAIN', - 'export.highcharts.com') + value = os.getenv( + "HIGHCHARTS_EXPORT_SERVER_DOMAIN", "export.highcharts.com" + ) self._domain = value self._url = None @@ -234,12 +251,11 @@ def port(self) -> Optional[int]: @port.setter def port(self, value): if value or value == 0: - value = validators.integer(value, - allow_empty = True, - minimum = 0, - maximum = 65536) + value = validators.integer( + value, allow_empty=True, minimum=0, maximum=65536 + ) else: - value = os.getenv('HIGHCHARTS_EXPORT_SERVER_PORT', None) + value = os.getenv("HIGHCHARTS_EXPORT_SERVER_PORT", None) self._port = value self._url = None @@ -268,9 +284,9 @@ def path(self) -> Optional[str]: @path.setter def path(self, value): - value = validators.path(value, allow_empty = True) + value = validators.path(value, allow_empty=True) if value is None: - value = os.getenv('HIGHCHARTS_EXPORT_SERVER_PATH', None) + value = os.getenv("HIGHCHARTS_EXPORT_SERVER_PATH", None) self._path = value self._url = None @@ -296,9 +312,9 @@ def url(self) -> Optional[str]: if self._url: return self._url else: - return_value = f'{self.protocol}://{self.domain}' + return_value = f"{self.protocol}://{self.domain}" if self.port is not None: - return_value += f':{self.port}/' + return_value += f":{self.port}/" if self.path is not None: return_value += self.path @@ -318,41 +334,41 @@ def url(self, value): self.path = None else: original_value = value - self.protocol = value[:value.index(':')] + self.protocol = value[: value.index(":")] - protocol = self.protocol + '://' - value = value.replace(protocol, '') + protocol = self.protocol + "://" + value = value.replace(protocol, "") no_port = False try: - end_of_domain = value.index(':') + end_of_domain = value.index(":") self.domain = value[:end_of_domain] except ValueError: no_port = True try: - end_of_domain = value.index('/') + end_of_domain = value.index("/") self.domain = value[:end_of_domain] except ValueError: self.domain = value - domain = self.domain + '/' + domain = self.domain + "/" if domain in value: - value = value.replace(domain, '') + value = value.replace(domain, "") elif self.domain in value: - value = value.replace(self.domain, '') + value = value.replace(self.domain, "") if value and no_port: - if value.startswith('/'): + if value.startswith("/"): self.path = value[1:] else: self.path = value else: - if value.startswith(':'): + if value.startswith(":"): start_of_port = 1 else: start_of_port = 0 try: - end_of_port = value.index('/') + end_of_port = value.index("/") except ValueError: end_of_port = None @@ -361,9 +377,9 @@ def url(self, value): else: self.port = value[start_of_port:] - port = f':{self.port}' - value = value.replace(port, '') - if value.startswith('/'): + port = f":{self.port}" + value = value.replace(port, "") + if value.startswith("/"): self.path = value[1:] elif value: self.path = value @@ -405,19 +421,19 @@ def format_(self) -> Optional[str]: @format_.setter def format_(self, value): - value = validators.string(value, allow_empty = True) + value = validators.string(value, allow_empty=True) if not value: self._format_ = None else: value = value.lower() - if value not in ['png', 'jpeg', 'pdf', 'svg', 'image/svg+xml']: + if value not in ["png", "jpeg", "pdf", "svg", "image/svg+xml"]: raise errors.HighchartsUnsupportedExportTypeError( - f'format_ expects either ' + f"format_ expects either " f'"png", "jpeg", "pdf", "svg", or ' f'"image/svg+xml". Received: {value}' ) - if value == 'svg': - value = 'image/svg+xml' + if value == "svg": + value = "image/svg+xml" self._format_ = value @@ -443,9 +459,7 @@ def scale(self) -> Optional[int | float]: @scale.setter def scale(self, value): - value = validators.numeric(value, - allow_empty = True, - minimum = 0) + value = validators.numeric(value, allow_empty=True, minimum=0) if not value: value = 1 @@ -467,9 +481,7 @@ def width(self) -> Optional[int | float]: @width.setter def width(self, value): - value = validators.numeric(value, - allow_empty = True, - minimum = 0) + value = validators.numeric(value, allow_empty=True, minimum=0) if not value: value = None @@ -536,23 +548,34 @@ def constructor(self) -> Optional[str]: @constructor.setter def constructor(self, value): - value = validators.string(value, allow_empty = True) + value = validators.string(value, allow_empty=True) if not value: self._constructor = None else: - if value not in ['Chart', 'Stock', 'Map', 'Gantt', 'chart', 'stockChart', 'mapChart', 'ganttChart',]: - raise errors.HighchartsUnsupportedConstructorError(f'constructor expects ' - f'"Chart", "Stock", "Map", "Gantt", "chart", ' - f'"stockChart", "mapChart", or "ganttChart", but ' - f'received: "{value}"') - if value == 'Chart': - value = 'chart' - elif value == 'Stock': - value = 'stockChart' - elif value == 'Map': - value = 'mapChart' - elif value == 'Gantt': - value = 'ganttChart' + if value not in [ + "Chart", + "Stock", + "Map", + "Gantt", + "chart", + "stockChart", + "mapChart", + "ganttChart", + ]: + raise errors.HighchartsUnsupportedConstructorError( + f"constructor expects " + f'"Chart", "Stock", "Map", "Gantt", "chart", ' + f'"stockChart", "mapChart", or "ganttChart", but ' + f'received: "{value}"' + ) + if value == "Chart": + value = "chart" + elif value == "Stock": + value = "stockChart" + elif value == "Map": + value = "mapChart" + elif value == "Gantt": + value = "ganttChart" self._constructor = value @@ -645,31 +668,31 @@ def custom_code(self, value): @property def resources(self) -> Optional[Dict]: """A dictionary of resources to be used in the export server. - - Expects to contain up to three keys: - + + Expects to contain up to three keys: + * ``files`` which contains an array of JS filenames * ``js`` which contains a string representation of JS code * ``css`` which contains a string representation of CSS code that will applied to the chart on export - + Defaults to :obj:`None `. :rtype: :class:`dict ` or :obj:`None ` """ resources = {} if self.files: - resources['files'] = self.files + resources["files"] = self.files if self.js: - resources['js'] = self.js + resources["js"] = self.js if self.css: - resources['css'] = self.css - + resources["css"] = self.css + if not resources: return None - + return resources - + @resources.setter def resources(self, value): if not value: @@ -677,22 +700,24 @@ def resources(self, value): self.js = None self.css = None elif not isinstance(value, dict): - raise errors.HighchartsValueError('resources expects a dictionary with keys "files", "js", and "css"') + raise errors.HighchartsValueError( + 'resources expects a dictionary with keys "files", "js", and "css"' + ) else: - self.files = value.get('files', None) - self.js = value.get('js', None) - self.css = value.get('css', None) + self.files = value.get("files", None) + self.js = value.get("js", None) + self.css = value.get("css", None) @property def files(self) -> Optional[List[str]]: """Collection of files that will be loaded into context for the export. Defaults to :obj:`None `. - + :rtype: :class:`list ` of :class:`str ` or :obj:`None ` """ return self._files - + @files.setter def files(self, value): if not value: @@ -701,51 +726,51 @@ def files(self, value): if isinstance(value, str): value = [value] elif not checkers.is_iterable(value): - raise errors.HighchartsValueError('files expects a list of strings') - + raise errors.HighchartsValueError("files expects a list of strings") + self._files = value - + @property def js(self) -> Optional[str]: """JavaScript code that will be loaded into context for the exported chart. Defaults to :obj:`None `. - + :rtype: :class:`str ` or :obj:`None ` """ return self._js - + @js.setter def js(self, value): if not value: self._js = None else: if not isinstance(value, str): - raise errors.HighchartsValueError('js expects a string') + raise errors.HighchartsValueError("js expects a string") self._js = value - + @property def css(self) -> Optional[str]: """CSS code that will be loaded into context for the exported chart. Defaults to :obj:`None `. - + :rtype: :class:`str ` or :obj:`None ` """ return self._css - + @css.setter def css(self, value): if not value: self._css = None else: if not isinstance(value, str): - raise errors.HighchartsValueError('css expects a string') - + raise errors.HighchartsValueError("css expects a string") + self._css = value @classmethod def is_export_supported(cls, options) -> bool: """Evaluates whether the Highcharts Export Server supports exporting the series types in ``options``. - + :rtype: :class:`bool ` """ if not isinstance(options, HighchartsOptions): @@ -758,83 +783,78 @@ def is_export_supported(cls, options) -> bool: for item in series_types: if item in constants.EXPORT_SERVER_UNSUPPORTED_SERIES_TYPES: return False - + return True @classmethod def _get_kwargs_from_dict(cls, as_dict): - url = as_dict.get('url', None) + url = as_dict.get("url", None) protocol = None domain = None port = None path = None if not url: - protocol = as_dict.get('protocol', None) - domain = as_dict.get('domain', None) - port = as_dict.get('port', None) - path = as_dict.get('path', None) + protocol = as_dict.get("protocol", None) + domain = as_dict.get("domain", None) + port = as_dict.get("port", None) + path = as_dict.get("path", None) kwargs = { - 'options': as_dict.get('options', None), - 'format_': as_dict.get('type', as_dict.get('format_', 'png')), - 'scale': as_dict.get('scale', 1), - 'width': as_dict.get('width', None), - 'callback': as_dict.get('callback', None), - 'constructor': as_dict.get('constructor', - None) or as_dict.get('constr', None), - 'use_base64': as_dict.get('use_base64', None) or as_dict.get('b64', False), - 'no_download': as_dict.get('noDownload', - None) or as_dict.get('no_download', None), - 'async_rendering': as_dict.get('asyncRendering', - False) or as_dict.get('async_rendering', - False), - 'global_options': as_dict.get('global_options', - None) or as_dict.get('globalOptions', - None), - 'data_options': as_dict.get('data_options', - None) or as_dict.get('dataOptions', None), - 'custom_code': as_dict.get('custom_code', - None) or as_dict.get('customCode', None) + "options": as_dict.get("options", None), + "format_": as_dict.get("type", as_dict.get("format_", "png")), + "scale": as_dict.get("scale", 1), + "width": as_dict.get("width", None), + "callback": as_dict.get("callback", None), + "constructor": as_dict.get("constructor", None) + or as_dict.get("constr", None), + "use_base64": as_dict.get("use_base64", None) or as_dict.get("b64", False), + "no_download": as_dict.get("noDownload", None) + or as_dict.get("no_download", None), + "async_rendering": as_dict.get("asyncRendering", False) + or as_dict.get("async_rendering", False), + "global_options": as_dict.get("global_options", None) + or as_dict.get("globalOptions", None), + "data_options": as_dict.get("data_options", None) + or as_dict.get("dataOptions", None), + "custom_code": as_dict.get("custom_code", None) + or as_dict.get("customCode", None), } if url: - kwargs['url'] = url + kwargs["url"] = url if protocol: - kwargs['protocol'] = protocol + kwargs["protocol"] = protocol if domain: - kwargs['domain'] = domain + kwargs["domain"] = domain if port: - kwargs['port'] = port + kwargs["port"] = port if path: - kwargs['path'] = path + kwargs["path"] = path return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'url': self.url, - 'options': self.options, - 'type': self.format_, - 'scale': self.scale, - 'width': self.width, - 'callback': self.callback, - 'constr': self.constructor, - 'b64': self.use_base64, - 'noDownload': self.no_download, - 'asyncRendering': self.async_rendering, - 'globalOptions': self.global_options, - 'dataOptions': self.data_options, - 'customCode': self.custom_code, - 'resources': self.resources, + "url": self.url, + "options": self.options, + "type": self.format_, + "scale": self.scale, + "width": self.width, + "callback": self.callback, + "constr": self.constructor, + "b64": self.use_base64, + "noDownload": self.no_download, + "asyncRendering": self.async_rendering, + "globalOptions": self.global_options, + "dataOptions": self.data_options, + "customCode": self.custom_code, + "resources": self.resources, } return untrimmed - def request_chart(self, - filename = None, - auth_user = None, - auth_password = None, - timeout = 3, - **kwargs): + def request_chart( + self, filename=None, auth_user=None, auth_password=None, timeout=3, **kwargs + ): """Execute a request against the export server based on the configuration in the instance. @@ -867,34 +887,34 @@ def request_chart(self, :meth:`use_base64 ` property). :rtype: :class:`bytes ` or :class:`str ` """ - self.options = kwargs.get('options', self.options) - self.format_ = kwargs.get('format_', kwargs.get('type', self.format_)) - self.scale = kwargs.get('scale', self.scale) - self.width = kwargs.get('width', self.width) - self.callback = kwargs.get('callback', self.callback) - self.constructor = kwargs.get('constructor', self.constructor) - self.use_base64 = kwargs.get('use_base64', self.use_base64) - self.no_download = kwargs.get('no_download', self.no_download) - self.async_rendering = kwargs.get('async_rendering', self.async_rendering) - self.global_options = kwargs.get('global_options', self.global_options) - self.custom_code = kwargs.get('custom_code', self.custom_code) + self.options = kwargs.get("options", self.options) + self.format_ = kwargs.get("format_", kwargs.get("type", self.format_)) + self.scale = kwargs.get("scale", self.scale) + self.width = kwargs.get("width", self.width) + self.callback = kwargs.get("callback", self.callback) + self.constructor = kwargs.get("constructor", self.constructor) + self.use_base64 = kwargs.get("use_base64", self.use_base64) + self.no_download = kwargs.get("no_download", self.no_download) + self.async_rendering = kwargs.get("async_rendering", self.async_rendering) + self.global_options = kwargs.get("global_options", self.global_options) + self.custom_code = kwargs.get("custom_code", self.custom_code) missing_details = [] if not self.options: - missing_details.append('options') + missing_details.append("options") if not self.format_: - missing_details.append('format_') + missing_details.append("format_") if not self.constructor: - missing_details.append('constructor') + missing_details.append("constructor") if not self.url: - missing_details.append('url') + missing_details.append("url") if missing_details: raise errors.HighchartsMissingExportSettingsError( - f'Unable to export a chart.' - f'ExportServer was missing ' - f' following settings: ' - f'{missing_details}' + f"Unable to export a chart." + f"ExportServer was missing " + f" following settings: " + f"{missing_details}" ) basic_auth = None @@ -902,104 +922,113 @@ def request_chart(self, basic_auth = requests.HTTPBasicAuth(auth_user, auth_password) payload = { - 'infile': 'HIGHCHARTS FOR PYTHON: REPLACE WITH OPTIONS', - 'type': self.format_, - 'scale': self.scale, - 'constr': self.constructor, - 'b64': self.use_base64, - 'noDownload': self.no_download, + "infile": "HIGHCHARTS FOR PYTHON: REPLACE WITH OPTIONS", + "type": self.format_, + "scale": self.scale, + "constr": self.constructor, + "b64": self.use_base64, + "noDownload": self.no_download, } if self.width: - payload['width'] = self.width + payload["width"] = self.width if self.height: - payload['height'] = self.height + payload["height"] = self.height if self.callback: - payload['callback'] = 'HIGHCHARTS FOR PYTHON: REPLACE WITH CALLBACK' + payload["callback"] = "HIGHCHARTS FOR PYTHON: REPLACE WITH CALLBACK" if self.global_options: - payload['globalOptions'] = 'HIGHCHARTS FOR PYTHON: REPLACE WITH GLOBAL' + payload["globalOptions"] = "HIGHCHARTS FOR PYTHON: REPLACE WITH GLOBAL" if self.custom_code: - payload['customCode'] = 'HIGHCHARTS FOR PYTHON: REPLACE WITH CUSTOM' + payload["customCode"] = "HIGHCHARTS FOR PYTHON: REPLACE WITH CUSTOM" if self.resources: - payload['resources'] = self.resources + payload["resources"] = self.resources as_json = json.dumps(payload) - + if not self.is_export_supported(self.options): - raise errors.HighchartsUnsupportedExportError('The Highcharts Export Server currently only supports ' - 'exports from Highcharts (Javascript) v.10. You are ' - 'using a series type introduced in v.11. Sorry, but ' - 'that functionality is still forthcoming.') - - options_as_json = self.options.to_json(for_export = True) + raise errors.HighchartsUnsupportedExportError( + "The Highcharts Export Server currently only supports " + "exports from Highcharts (Javascript) v.10. You are " + "using a series type introduced in v.11. Sorry, but " + "that functionality is still forthcoming." + ) + + options_as_json = self.options.to_json(for_export=True) if isinstance(options_as_json, bytes): - options_as_str = str(options_as_json, encoding = 'utf-8') + options_as_str = str(options_as_json, encoding="utf-8") else: options_as_str = options_as_json - as_json = as_json.replace('"HIGHCHARTS FOR PYTHON: REPLACE WITH OPTIONS"', - options_as_str) + as_json = as_json.replace( + '"HIGHCHARTS FOR PYTHON: REPLACE WITH OPTIONS"', options_as_str + ) if self.callback: - callback_as_json = self.callback.to_json(for_export = True) + callback_as_json = self.callback.to_json(for_export=True) if isinstance(callback_as_json, bytes): - callback_as_str = str(callback_as_json, encoding = 'utf-8') + callback_as_str = str(callback_as_json, encoding="utf-8") else: callback_as_str = callback_as_json - as_json = as_json.replace('"HIGHCHARTS FOR PYTHON: REPLACE WITH CALLBACK"', - callback_as_str) + as_json = as_json.replace( + '"HIGHCHARTS FOR PYTHON: REPLACE WITH CALLBACK"', callback_as_str + ) if self.global_options: - global_as_json = self.global_options.to_json(for_export = True) + global_as_json = self.global_options.to_json(for_export=True) if isinstance(global_as_json, bytes): - global_as_str = str(global_as_json, encoding = 'utf-8') + global_as_str = str(global_as_json, encoding="utf-8") else: global_as_str = global_as_json - as_json = as_json.replace('"HIGHCHARTS FOR PYTHON: REPLACE WITH GLOBAL"', - global_as_str) + as_json = as_json.replace( + '"HIGHCHARTS FOR PYTHON: REPLACE WITH GLOBAL"', global_as_str + ) if self.data_options: - data_as_json = self.data_options.to_json(for_export = True) + data_as_json = self.data_options.to_json(for_export=True) if isinstance(data_as_json, bytes): - data_as_str = str(data_as_json, encoding = 'utf-8') + data_as_str = str(data_as_json, encoding="utf-8") else: data_as_str = data_as_json - as_json = as_json.replace('"HIGHCHARTS FOR PYTHON: REPLACE WITH DATA"', - data_as_str) + as_json = as_json.replace( + '"HIGHCHARTS FOR PYTHON: REPLACE WITH DATA"', data_as_str + ) if self.custom_code: - code_as_json = self.custom_code.to_json(for_export = True) + code_as_json = self.custom_code.to_json(for_export=True) if isinstance(code_as_json, bytes): - code_as_str = str(code_as_json, encoding = 'utf-8') + code_as_str = str(code_as_json, encoding="utf-8") else: code_as_str = code_as_json - as_json = as_json.replace('"HIGHCHARTS FOR PYTHON: REPLACE WITH CUSTOM"', - code_as_str) - - result = requests.post(self.url, - data = as_json.encode('utf-8'), - headers = { - 'Content-Type': 'application/json', - 'Referer': self.referer, - 'User-Agent': self.user_agent, - }, - auth = basic_auth, - timeout = timeout) + as_json = as_json.replace( + '"HIGHCHARTS FOR PYTHON: REPLACE WITH CUSTOM"', code_as_str + ) + + headers = { + "Content-Type": "application/json", + "Origin": self.referer, + "Referer": self.referer, + "User-Agent": self.user_agent, + } + + result = requests.post( + self.url, + data=as_json.encode("utf-8"), + headers=headers, + auth=basic_auth, + timeout=timeout, + ) result.raise_for_status() - if filename and self.format_ != 'svg': - with open(filename, 'wb') as file_: + if filename and self.format_ != "svg": + with open(filename, "wb") as file_: file_.write(result.content) - elif filename and self.format_ == 'svg': + elif filename and self.format_ == "svg": content = str(result.content, encoding="utf-8").replace("\u200b", " ") - with open(filename, 'wt') as file_: + with open(filename, "wt") as file_: file_.write(content) return result.content @classmethod - def get_chart(cls, - filename = None, - auth_user = None, - auth_password = None, - timeout = 3, - **kwargs): + def get_chart( + cls, filename=None, auth_user=None, auth_password=None, timeout=3, **kwargs + ): """Produce an exported chart image. :param filename: The name of the file where the exported chart should (optionally) @@ -1033,9 +1062,11 @@ def get_chart(cls, """ instance = cls(**kwargs) - exported_chart = instance.request_chart(filename = filename, - auth_user = auth_user, - auth_password = auth_password, - timeout = timeout) + exported_chart = instance.request_chart( + filename=filename, + auth_user=auth_user, + auth_password=auth_password, + timeout=timeout, + ) return exported_chart diff --git a/highcharts_core/options/plot_options/series.py b/highcharts_core/options/plot_options/series.py index 36f3081..1b05459 100644 --- a/highcharts_core/options/plot_options/series.py +++ b/highcharts_core/options/plot_options/series.py @@ -6,7 +6,12 @@ from highcharts_core import errors from highcharts_core.decorators import class_sensitive, validate_types +from highcharts_core.metaclasses import HighchartsMeta from highcharts_core.options.plot_options.generic import GenericTypeOptions +from highcharts_core.utility_classes.data_labels import ( + DataLabel, + data_label_property_map, +) from highcharts_core.utility_classes.gradients import Gradient from highcharts_core.utility_classes.patterns import Pattern from highcharts_core.utility_classes.shadows import ShadowOptions @@ -40,26 +45,26 @@ def __init__(self, **kwargs): self._zone_axis = None self._zones = None - self.animation_limit = kwargs.get('animation_limit', None) - self.boost_blending = kwargs.get('boost_blending', None) - self.boost_threshold = kwargs.get('boost_threshold', None) - self.color_index = kwargs.get('color_index', None) - self.color_key = kwargs.get('color_key', None) - self.connect_nulls = kwargs.get('connect_nulls', None) - self.crisp = kwargs.get('crisp', None) - self.crop_threshold = kwargs.get('crop_threshold', None) - self.data_sorting = kwargs.get('data_sorting', None) - self.find_nearest_point_by = kwargs.get('find_nearest_point_by', None) - self.get_extremes_from_all = kwargs.get('get_extremes_from_all', None) - self.inactive_other_points = kwargs.get('inactive_other_points', None) - self.linecap = kwargs.get('linecap', None) - self.line_width = kwargs.get('line_width', None) - self.relative_x_value = kwargs.get('relative_x_value', None) - self.shadow = kwargs.get('shadow', None) - self.soft_threshold = kwargs.get('soft_threshold', None) - self.step = kwargs.get('step', None) - self.zone_axis = kwargs.get('zone_axis', None) - self.zones = kwargs.get('zones', None) + self.animation_limit = kwargs.get("animation_limit", None) + self.boost_blending = kwargs.get("boost_blending", None) + self.boost_threshold = kwargs.get("boost_threshold", None) + self.color_index = kwargs.get("color_index", None) + self.color_key = kwargs.get("color_key", None) + self.connect_nulls = kwargs.get("connect_nulls", None) + self.crisp = kwargs.get("crisp", None) + self.crop_threshold = kwargs.get("crop_threshold", None) + self.data_sorting = kwargs.get("data_sorting", None) + self.find_nearest_point_by = kwargs.get("find_nearest_point_by", None) + self.get_extremes_from_all = kwargs.get("get_extremes_from_all", None) + self.inactive_other_points = kwargs.get("inactive_other_points", None) + self.linecap = kwargs.get("linecap", None) + self.line_width = kwargs.get("line_width", None) + self.relative_x_value = kwargs.get("relative_x_value", None) + self.shadow = kwargs.get("shadow", None) + self.soft_threshold = kwargs.get("soft_threshold", None) + self.step = kwargs.get("step", None) + self.zone_axis = kwargs.get("zone_axis", None) + self.zones = kwargs.get("zones", None) super().__init__(**kwargs) @@ -79,12 +84,12 @@ def animation_limit(self) -> Optional[int | float | Decimal]: @animation_limit.setter def animation_limit(self, value): - if value == float('inf'): - self._animation_limit = float('inf') + if value == float("inf"): + self._animation_limit = float("inf") else: - self._animation_limit = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._animation_limit = validators.numeric( + value, allow_empty=True, minimum=0 + ) @property def boost_blending(self) -> Optional[str]: @@ -97,7 +102,7 @@ def boost_blending(self) -> Optional[str]: @boost_blending.setter def boost_blending(self, value): - self._boost_blending = validators.string(value, allow_empty = True) + self._boost_blending = validators.string(value, allow_empty=True) @property def boost_threshold(self) -> Optional[int]: @@ -124,9 +129,7 @@ def boost_threshold(self) -> Optional[int]: @boost_threshold.setter def boost_threshold(self, value): - self._boost_threshold = validators.integer(value, - allow_empty = True, - minimum = 0) + self._boost_threshold = validators.integer(value, allow_empty=True, minimum=0) @property def color_index(self) -> Optional[int]: @@ -135,7 +138,7 @@ def color_index(self) -> Optional[int]: ``highcharts-color-{n}``. .. tip:: - + .. versionadded:: Highcharts (JS) v.11 With Highcharts (JS) v.11, using CSS variables of the form ``--highcharts-color-{n}`` make @@ -149,9 +152,7 @@ def color_index(self) -> Optional[int]: @color_index.setter def color_index(self, value): - self._color_index = validators.integer(value, - allow_empty = True, - minimum = 0) + self._color_index = validators.integer(value, allow_empty=True, minimum=0) @property def color_key(self) -> Optional[str]: @@ -169,7 +170,7 @@ def color_key(self) -> Optional[str]: @color_key.setter def color_key(self, value): - self._color_key = validators.string(value, allow_empty = True) + self._color_key = validators.string(value, allow_empty=True) @property def connect_nulls(self) -> Optional[bool]: @@ -228,9 +229,60 @@ def crop_threshold(self) -> Optional[int]: @crop_threshold.setter def crop_threshold(self, value): - self._crop_threshold = validators.integer(value, - allow_empty = True, - minimum = 0) + self._crop_threshold = validators.integer(value, allow_empty=True, minimum=0) + + @property + def data_labels(self) -> Optional[DataLabel | List[DataLabel]]: + """Options for the series data labels, appearing next to each data point. + + .. note:: + + To have multiple data labels per data point, you can also supply a collection of + :class:`DataLabel` configuration settings. + + .. note:: + + Highcharts for Python has a generic :class:`DataLabel` class for data labels, but + certain specific series types may take a series-specific class (e.g. :class:`PieDataLabel` + or :class:`SunburstDataLabel`) with series type-specific properties. This + property will examine the type of value supplied and coerce it to the appropriate + sub-type of :class:`DataLabel`. + + :rtype: :class:`DataLabel`, :class:`list ` of :class:`DataLabel`, or + :obj:`None ` + """ + return self._data_labels + + @data_labels.setter + def data_labels(self, value): + if not value: + self._data_labels = None + else: + allowed_types = DataLabel + for key, types_ in data_label_property_map.items(): + possible_properties = key.split("|") + found_prop = False + for prop in possible_properties: + if isinstance(value, HighchartsMeta) and hasattr(value, prop): + allowed_types = types_ + found_prop = True + break + if isinstance(value, (str, dict)) and prop in value: + allowed_types = types_ + found_prop = True + break + + if found_prop: + break + + if checkers.is_iterable(value): + self._data_labels = validate_types( + value, types=allowed_types, allow_none=False, force_iterable=True + ) + else: + self._data_labels = validate_types( + value, types=allowed_types, allow_none=False + ) @property def data_sorting(self) -> Optional[DataSorting]: @@ -263,7 +315,7 @@ def find_nearest_point_by(self) -> Optional[str]: @find_nearest_point_by.setter def find_nearest_point_by(self, value): - self._find_nearest_point_by = validators.string(value, allow_empty = True) + self._find_nearest_point_by = validators.string(value, allow_empty=True) @property def get_extremes_from_all(self) -> Optional[bool]: @@ -290,16 +342,16 @@ def get_extremes_from_all(self, value): def inactive_other_points(self) -> Optional[bool]: """If ``True``, highlights only the hovered point and fades the other points. Defaults to ``False``. - + .. warning:: - + Scatter-type series require enabling the 'inactive' marker state and adjusting opacity. This approach could affect performance\nwith large datasets. - + :rtype: :class:`bool ` or :obj:`None ` """ return self._inactive_other_points - + @inactive_other_points.setter def inactive_other_points(self, value): if value is None: @@ -319,7 +371,7 @@ def linecap(self) -> Optional[str]: @linecap.setter def linecap(self, value): - self._linecap = validators.string(value, allow_empty = True) + self._linecap = validators.string(value, allow_empty=True) @property def line_width(self) -> Optional[int | float | Decimal]: @@ -331,9 +383,7 @@ def line_width(self) -> Optional[int | float | Decimal]: @line_width.setter def line_width(self, value): - self._line_width = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._line_width = validators.numeric(value, allow_empty=True, minimum=0) @property def relative_x_value(self) -> Optional[bool]: @@ -380,8 +430,7 @@ def shadow(self, value): elif not value: self._shadow = None else: - value = validate_types(value, - types = ShadowOptions) + value = validate_types(value, types=ShadowOptions) self._shadow = value @property @@ -422,7 +471,7 @@ def step(self) -> Optional[str]: @step.setter def step(self, value): - self._step = validators.string(value, allow_empty = True) + self._step = validators.string(value, allow_empty=True) @property def zone_axis(self) -> Optional[str]: @@ -434,7 +483,7 @@ def zone_axis(self) -> Optional[str]: @zone_axis.setter def zone_axis(self, value): - self._zone_axis = validators.string(value, allow_empty = True) + self._zone_axis = validators.string(value, allow_empty=True) @property def zones(self) -> Optional[List[Zone]]: @@ -453,97 +502,97 @@ def zones(self) -> Optional[List[Zone]]: return self._zones @zones.setter - @class_sensitive(Zone, - force_iterable = True) + @class_sensitive(Zone, force_iterable=True) def zones(self, value): self._zones = value @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'accessibility': as_dict.get('accessibility', None), - 'allow_point_select': as_dict.get('allowPointSelect', None), - 'animation': as_dict.get('animation', None), - 'class_name': as_dict.get('className', None), - 'clip': as_dict.get('clip', None), - 'color': as_dict.get('color', None), - 'cursor': as_dict.get('cursor', None), - 'custom': as_dict.get('custom', None), - 'dash_style': as_dict.get('dashStyle', None), - 'data_labels': as_dict.get('dataLabels', None), - 'description': as_dict.get('description', None), - 'enable_mouse_tracking': as_dict.get('enableMouseTracking', None), - 'events': as_dict.get('events', None), - 'include_in_data_export': as_dict.get('includeInDataExport', None), - 'keys': as_dict.get('keys', None), - 'label': as_dict.get('label', None), - 'legend_symbol': as_dict.get('legendSymbol', None), - 'linked_to': as_dict.get('linkedTo', None), - 'marker': as_dict.get('marker', None), - 'on_point': as_dict.get('onPoint', None), - 'opacity': as_dict.get('opacity', None), - 'point': as_dict.get('point', None), - 'point_description_formatter': as_dict.get('pointDescriptionFormatter', None), - 'selected': as_dict.get('selected', None), - 'show_checkbox': as_dict.get('showCheckbox', None), - 'show_in_legend': as_dict.get('showInLegend', None), - 'skip_keyboard_navigation': as_dict.get('skipKeyboardNavigation', None), - 'sonification': as_dict.get('sonification', None), - 'states': as_dict.get('states', None), - 'sticky_tracking': as_dict.get('stickyTracking', None), - 'threshold': as_dict.get('threshold', None), - 'tooltip': as_dict.get('tooltip', None), - 'turbo_threshold': as_dict.get('turboThreshold', None), - 'visible': as_dict.get('visible', None), - - 'animation_limit': as_dict.get('animationLimit', None), - 'boost_blending': as_dict.get('boostBlending', None), - 'boost_threshold': as_dict.get('boostThreshold', None), - 'color_index': as_dict.get('colorIndex', None), - 'color_key': as_dict.get('colorKey', None), - 'connect_nulls': as_dict.get('connectNulls', None), - 'crisp': as_dict.get('crisp', None), - 'crop_threshold': as_dict.get('cropThreshold', None), - 'data_sorting': as_dict.get('dataSorting', None), - 'find_nearest_point_by': as_dict.get('findNearestPointBy', None), - 'get_extremes_from_all': as_dict.get('getExtremesFromAll', None), - 'inactive_other_points': as_dict.get('inactiveOtherPoints', None), - 'linecap': as_dict.get('linecap', None), - 'line_width': as_dict.get('lineWidth', None), - 'relative_x_value': as_dict.get('relativeXValue', None), - 'shadow': as_dict.get('shadow', None), - 'soft_threshold': as_dict.get('softThreshold', None), - 'step': as_dict.get('step', None), - 'zone_axis': as_dict.get('zoneAxis', None), - 'zones': as_dict.get('zones', None), + "accessibility": as_dict.get("accessibility", None), + "allow_point_select": as_dict.get("allowPointSelect", None), + "animation": as_dict.get("animation", None), + "class_name": as_dict.get("className", None), + "clip": as_dict.get("clip", None), + "color": as_dict.get("color", None), + "cursor": as_dict.get("cursor", None), + "custom": as_dict.get("custom", None), + "dash_style": as_dict.get("dashStyle", None), + "data_labels": as_dict.get("dataLabels", None), + "description": as_dict.get("description", None), + "enable_mouse_tracking": as_dict.get("enableMouseTracking", None), + "events": as_dict.get("events", None), + "include_in_data_export": as_dict.get("includeInDataExport", None), + "keys": as_dict.get("keys", None), + "label": as_dict.get("label", None), + "legend_symbol": as_dict.get("legendSymbol", None), + "linked_to": as_dict.get("linkedTo", None), + "marker": as_dict.get("marker", None), + "on_point": as_dict.get("onPoint", None), + "opacity": as_dict.get("opacity", None), + "point": as_dict.get("point", None), + "point_description_formatter": as_dict.get( + "pointDescriptionFormatter", None + ), + "selected": as_dict.get("selected", None), + "show_checkbox": as_dict.get("showCheckbox", None), + "show_in_legend": as_dict.get("showInLegend", None), + "skip_keyboard_navigation": as_dict.get("skipKeyboardNavigation", None), + "sonification": as_dict.get("sonification", None), + "states": as_dict.get("states", None), + "sticky_tracking": as_dict.get("stickyTracking", None), + "threshold": as_dict.get("threshold", None), + "tooltip": as_dict.get("tooltip", None), + "turbo_threshold": as_dict.get("turboThreshold", None), + "visible": as_dict.get("visible", None), + "animation_limit": as_dict.get("animationLimit", None), + "boost_blending": as_dict.get("boostBlending", None), + "boost_threshold": as_dict.get("boostThreshold", None), + "color_index": as_dict.get("colorIndex", None), + "color_key": as_dict.get("colorKey", None), + "connect_nulls": as_dict.get("connectNulls", None), + "crisp": as_dict.get("crisp", None), + "crop_threshold": as_dict.get("cropThreshold", None), + "data_sorting": as_dict.get("dataSorting", None), + "find_nearest_point_by": as_dict.get("findNearestPointBy", None), + "get_extremes_from_all": as_dict.get("getExtremesFromAll", None), + "inactive_other_points": as_dict.get("inactiveOtherPoints", None), + "linecap": as_dict.get("linecap", None), + "line_width": as_dict.get("lineWidth", None), + "relative_x_value": as_dict.get("relativeXValue", None), + "shadow": as_dict.get("shadow", None), + "soft_threshold": as_dict.get("softThreshold", None), + "step": as_dict.get("step", None), + "zone_axis": as_dict.get("zoneAxis", None), + "zones": as_dict.get("zones", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'animationLimit': self.animation_limit, - 'boostBlending': self.boost_blending, - 'boostThreshold': self.boost_threshold, - 'colorIndex': self.color_index, - 'colorKey': self.color_key, - 'connectNulls': self.connect_nulls, - 'crisp': self.crisp, - 'cropThreshold': self.crop_threshold, - 'dataSorting': self.data_sorting, - 'findNearestPointBy': self.find_nearest_point_by, - 'getExtremesFromAll': self.get_extremes_from_all, - 'inactiveOtherPoints': self.inactive_other_points, - 'linecap': self.linecap, - 'lineWidth': self.line_width, - 'relativeXValue': self.relative_x_value, - 'shadow': self.shadow, - 'softThreshold': self.soft_threshold, - 'step': self.step, - 'zoneAxis': self.zone_axis, - 'zones': self.zones + "animationLimit": self.animation_limit, + "boostBlending": self.boost_blending, + "boostThreshold": self.boost_threshold, + "colorIndex": self.color_index, + "colorKey": self.color_key, + "connectNulls": self.connect_nulls, + "crisp": self.crisp, + "cropThreshold": self.crop_threshold, + "dataSorting": self.data_sorting, + "findNearestPointBy": self.find_nearest_point_by, + "getExtremesFromAll": self.get_extremes_from_all, + "inactiveOtherPoints": self.inactive_other_points, + "linecap": self.linecap, + "lineWidth": self.line_width, + "relativeXValue": self.relative_x_value, + "shadow": self.shadow, + "softThreshold": self.soft_threshold, + "step": self.step, + "zoneAxis": self.zone_axis, + "zones": self.zones, } - parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) + parent_as_dict = super()._to_untrimmed_dict(in_cls=in_cls) for key in parent_as_dict: untrimmed[key] = parent_as_dict[key] @@ -565,16 +614,16 @@ def __init__(self, **kwargs): self._point_start = None self._stacking = None - self.color_axis = kwargs.get('color_axis', None) - self.connect_ends = kwargs.get('connect_ends', None) - self.drag_drop = kwargs.get('drag_drop', None) - self.negative_color = kwargs.get('negative_color', None) - self.point_description_format = kwargs.get('point_description_format', None) - self.point_interval = kwargs.get('point_interval', None) - self.point_interval_unit = kwargs.get('point_interval_unit', None) - self.point_placement = kwargs.get('point_placement', None) - self.point_start = kwargs.get('point_start', None) - self.stacking = kwargs.get('stacking', None) + self.color_axis = kwargs.get("color_axis", None) + self.connect_ends = kwargs.get("connect_ends", None) + self.drag_drop = kwargs.get("drag_drop", None) + self.negative_color = kwargs.get("negative_color", None) + self.point_description_format = kwargs.get("point_description_format", None) + self.point_interval = kwargs.get("point_interval", None) + self.point_interval_unit = kwargs.get("point_interval_unit", None) + self.point_placement = kwargs.get("point_placement", None) + self.point_start = kwargs.get("point_start", None) + self.stacking = kwargs.get("stacking", None) super().__init__(**kwargs) @@ -603,8 +652,7 @@ def color_axis(self, value): try: self._color_axis = validators.string(value) except TypeError: - self._color_axis = validators.integer(value, - minimum = 0) + self._color_axis = validators.integer(value, minimum=0) @property def connect_ends(self) -> Optional[bool]: @@ -665,24 +713,25 @@ def negative_color(self) -> Optional[str | Gradient | Pattern]: @negative_color.setter def negative_color(self, value): from highcharts_core import utility_functions + self._negative_color = utility_functions.validate_color(value) @property def point_description_format(self) -> Optional[str]: - """A :term:`format string` to use instead of the default for + """A :term:`format string` to use instead of the default for point descriptions on the series. Defaults to :obj:`None `. - + .. note:: - + Overrides the chart-wide configuration, as applicable. - + :rtype: :class:`str ` or :obj:`None ` """ return self._point_description_format - + @point_description_format.setter def point_description_format(self, value): - self._point_description_format = validators.string(value, allow_empty = True) + self._point_description_format = validators.string(value, allow_empty=True) @property def point_interval(self) -> Optional[int | float | Decimal]: @@ -716,9 +765,7 @@ def point_interval(self) -> Optional[int | float | Decimal]: @point_interval.setter def point_interval(self, value): - self._point_interval = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._point_interval = validators.numeric(value, allow_empty=True, minimum=0) @property def point_interval_unit(self) -> Optional[str]: @@ -743,7 +790,7 @@ def point_interval_unit(self) -> Optional[str]: @point_interval_unit.setter def point_interval_unit(self, value): - self._point_interval_unit = validators.string(value, allow_empty = True) + self._point_interval_unit = validators.string(value, allow_empty=True) @property def point_placement(self) -> Optional[str | int | float | Decimal]: @@ -785,16 +832,18 @@ def point_placement(self, value): self._point_placement = None else: try: - self._point_placement = validators.numeric(value, - minimum = -0.5, - maximum = 0.5) + self._point_placement = validators.numeric( + value, minimum=-0.5, maximum=0.5 + ) except (TypeError, ValueError): value = validators.string(value) value = value.lower() - if value not in ['on', 'between']: - raise errors.HighchartsValueError(f'point_placement must be a number,' - f' "on", or "between". Was: ' - f'{value}') + if value not in ["on", "between"]: + raise errors.HighchartsValueError( + f"point_placement must be a number," + f' "on", or "between". Was: ' + f"{value}" + ) self._point_placement = value @property @@ -817,20 +866,20 @@ def point_start(self) -> Optional[int | float | Decimal]: @point_start.setter def point_start(self, value): try: - value = validators.numeric(value, allow_empty = True) + value = validators.numeric(value, allow_empty=True) except (TypeError, ValueError) as error: value = validators.datetime(value) - if hasattr(value, 'timestamp') and value.tzinfo is not None: - self._point_start = value.timestamp()*1000 - elif hasattr(value, 'timestamp'): - value = value.replace(tzinfo = datetime.timezone.utc) - value = value.timestamp()*1000 + if hasattr(value, "timestamp") and value.tzinfo is not None: + self._point_start = value.timestamp() * 1000 + elif hasattr(value, "timestamp"): + value = value.replace(tzinfo=datetime.timezone.utc) + value = value.timestamp() * 1000 else: raise error - + self._point_start = value - + @property def stacking(self) -> Optional[str]: """Whether to stack the values of each series on top of each other. Defaults to @@ -859,98 +908,100 @@ def stacking(self, value): else: value = validators.string(value) value = value.lower() - if value not in ['normal', 'percent', 'stream', 'overlap']: - raise errors.HighchartsValueError(f'stacking expects a valid stacking ' - f'value. However, received: {value}') + if value not in ["normal", "percent", "stream", "overlap"]: + raise errors.HighchartsValueError( + f"stacking expects a valid stacking " + f"value. However, received: {value}" + ) self._stacking = value @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'accessibility': as_dict.get('accessibility', None), - 'allow_point_select': as_dict.get('allowPointSelect', None), - 'animation': as_dict.get('animation', None), - 'class_name': as_dict.get('className', None), - 'clip': as_dict.get('clip', None), - 'color': as_dict.get('color', None), - 'cursor': as_dict.get('cursor', None), - 'custom': as_dict.get('custom', None), - 'dash_style': as_dict.get('dashStyle', None), - 'data_labels': as_dict.get('dataLabels', None), - 'description': as_dict.get('description', None), - 'enable_mouse_tracking': as_dict.get('enableMouseTracking', None), - 'events': as_dict.get('events', None), - 'include_in_data_export': as_dict.get('includeInDataExport', None), - 'keys': as_dict.get('keys', None), - 'label': as_dict.get('label', None), - 'legend_symbol': as_dict.get('legendSymbol', None), - 'linked_to': as_dict.get('linkedTo', None), - 'marker': as_dict.get('marker', None), - 'on_point': as_dict.get('onPoint', None), - 'opacity': as_dict.get('opacity', None), - 'point': as_dict.get('point', None), - 'point_description_formatter': as_dict.get('pointDescriptionFormatter', None), - 'selected': as_dict.get('selected', None), - 'show_checkbox': as_dict.get('showCheckbox', None), - 'show_in_legend': as_dict.get('showInLegend', None), - 'skip_keyboard_navigation': as_dict.get('skipKeyboardNavigation', None), - 'sonification': as_dict.get('sonification', None), - 'states': as_dict.get('states', None), - 'sticky_tracking': as_dict.get('stickyTracking', None), - 'threshold': as_dict.get('threshold', None), - 'tooltip': as_dict.get('tooltip', None), - 'turbo_threshold': as_dict.get('turboThreshold', None), - 'visible': as_dict.get('visible', None), - - 'animation_limit': as_dict.get('animationLimit', None), - 'boost_blending': as_dict.get('boostBlending', None), - 'boost_threshold': as_dict.get('boostThreshold', None), - 'color_index': as_dict.get('colorIndex', None), - 'color_key': as_dict.get('colorKey', None), - 'connect_nulls': as_dict.get('connectNulls', None), - 'crisp': as_dict.get('crisp', None), - 'crop_threshold': as_dict.get('cropThreshold', None), - 'data_sorting': as_dict.get('dataSorting', None), - 'find_nearest_point_by': as_dict.get('findNearestPointBy', None), - 'get_extremes_from_all': as_dict.get('getExtremesFromAll', None), - 'inactive_other_points': as_dict.get('inactiveOtherPoints', None), - 'linecap': as_dict.get('linecap', None), - 'line_width': as_dict.get('lineWidth', None), - 'relative_x_value': as_dict.get('relativeXValue', None), - 'shadow': as_dict.get('shadow', None), - 'soft_threshold': as_dict.get('softThreshold', None), - 'step': as_dict.get('step', None), - 'zone_axis': as_dict.get('zoneAxis', None), - 'zones': as_dict.get('zones', None), - - 'color_axis': as_dict.get('colorAxis', None), - 'connect_ends': as_dict.get('connectEnds', None), - 'drag_drop': as_dict.get('dragDrop', None), - 'negative_color': as_dict.get('negativeColor', None), - 'point_description_format': as_dict.get('pointDescriptionFormat', None), - 'point_interval': as_dict.get('pointInterval', None), - 'point_interval_unit': as_dict.get('pointIntervalUnit', None), - 'point_placement': as_dict.get('pointPlacement', None), - 'point_start': as_dict.get('pointStart', None), - 'stacking': as_dict.get('stacking', None), + "accessibility": as_dict.get("accessibility", None), + "allow_point_select": as_dict.get("allowPointSelect", None), + "animation": as_dict.get("animation", None), + "class_name": as_dict.get("className", None), + "clip": as_dict.get("clip", None), + "color": as_dict.get("color", None), + "cursor": as_dict.get("cursor", None), + "custom": as_dict.get("custom", None), + "dash_style": as_dict.get("dashStyle", None), + "data_labels": as_dict.get("dataLabels", None), + "description": as_dict.get("description", None), + "enable_mouse_tracking": as_dict.get("enableMouseTracking", None), + "events": as_dict.get("events", None), + "include_in_data_export": as_dict.get("includeInDataExport", None), + "keys": as_dict.get("keys", None), + "label": as_dict.get("label", None), + "legend_symbol": as_dict.get("legendSymbol", None), + "linked_to": as_dict.get("linkedTo", None), + "marker": as_dict.get("marker", None), + "on_point": as_dict.get("onPoint", None), + "opacity": as_dict.get("opacity", None), + "point": as_dict.get("point", None), + "point_description_formatter": as_dict.get( + "pointDescriptionFormatter", None + ), + "selected": as_dict.get("selected", None), + "show_checkbox": as_dict.get("showCheckbox", None), + "show_in_legend": as_dict.get("showInLegend", None), + "skip_keyboard_navigation": as_dict.get("skipKeyboardNavigation", None), + "sonification": as_dict.get("sonification", None), + "states": as_dict.get("states", None), + "sticky_tracking": as_dict.get("stickyTracking", None), + "threshold": as_dict.get("threshold", None), + "tooltip": as_dict.get("tooltip", None), + "turbo_threshold": as_dict.get("turboThreshold", None), + "visible": as_dict.get("visible", None), + "animation_limit": as_dict.get("animationLimit", None), + "boost_blending": as_dict.get("boostBlending", None), + "boost_threshold": as_dict.get("boostThreshold", None), + "color_index": as_dict.get("colorIndex", None), + "color_key": as_dict.get("colorKey", None), + "connect_nulls": as_dict.get("connectNulls", None), + "crisp": as_dict.get("crisp", None), + "crop_threshold": as_dict.get("cropThreshold", None), + "data_sorting": as_dict.get("dataSorting", None), + "find_nearest_point_by": as_dict.get("findNearestPointBy", None), + "get_extremes_from_all": as_dict.get("getExtremesFromAll", None), + "inactive_other_points": as_dict.get("inactiveOtherPoints", None), + "linecap": as_dict.get("linecap", None), + "line_width": as_dict.get("lineWidth", None), + "relative_x_value": as_dict.get("relativeXValue", None), + "shadow": as_dict.get("shadow", None), + "soft_threshold": as_dict.get("softThreshold", None), + "step": as_dict.get("step", None), + "zone_axis": as_dict.get("zoneAxis", None), + "zones": as_dict.get("zones", None), + "color_axis": as_dict.get("colorAxis", None), + "connect_ends": as_dict.get("connectEnds", None), + "drag_drop": as_dict.get("dragDrop", None), + "negative_color": as_dict.get("negativeColor", None), + "point_description_format": as_dict.get("pointDescriptionFormat", None), + "point_interval": as_dict.get("pointInterval", None), + "point_interval_unit": as_dict.get("pointIntervalUnit", None), + "point_placement": as_dict.get("pointPlacement", None), + "point_start": as_dict.get("pointStart", None), + "stacking": as_dict.get("stacking", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'colorAxis': self.color_axis, - 'connectEnds': self.connect_ends, - 'dragDrop': self.drag_drop, - 'negativeColor': self.negative_color, - 'pointDescriptionFormat': self.point_description_format, - 'pointInterval': self.point_interval, - 'pointIntervalUnit': self.point_interval_unit, - 'pointPlacement': self.point_placement, - 'pointStart': self.point_start, - 'stacking': self.stacking, + "colorAxis": self.color_axis, + "connectEnds": self.connect_ends, + "dragDrop": self.drag_drop, + "negativeColor": self.negative_color, + "pointDescriptionFormat": self.point_description_format, + "pointInterval": self.point_interval, + "pointIntervalUnit": self.point_interval_unit, + "pointPlacement": self.point_placement, + "pointStart": self.point_start, + "stacking": self.stacking, } - parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) + parent_as_dict = super()._to_untrimmed_dict(in_cls=in_cls) for key in parent_as_dict: untrimmed[key] = parent_as_dict[key] diff --git a/highcharts_core/utility_classes/data_labels.py b/highcharts_core/utility_classes/data_labels.py index b56bb78..4cb0cec 100644 --- a/highcharts_core/utility_classes/data_labels.py +++ b/highcharts_core/utility_classes/data_labels.py @@ -28,9 +28,9 @@ def __init__(self, **kwargs): self._property = None self._value = None - self.operator = kwargs.get('operator', None) - self.property_ = kwargs.get('property_', None) - self.value = kwargs.get('value', None) + self.operator = kwargs.get("operator", None) + self.property_ = kwargs.get("property_", None) + self.value = kwargs.get("value", None) @property def operator(self) -> Optional[str]: @@ -51,7 +51,7 @@ def operator(self) -> Optional[str]: @operator.setter def operator(self, value): - self._operator = validators.string(value, allow_empty = True) + self._operator = validators.string(value, allow_empty=True) @property def property_(self) -> Optional[str]: @@ -66,7 +66,7 @@ def property_(self) -> Optional[str]: @property_.setter def property_(self, value): - self._property = validators.string(value, allow_empty = True) + self._property = validators.string(value, allow_empty=True) @property def value(self) -> Optional[int | float | Decimal]: @@ -78,23 +78,23 @@ def value(self) -> Optional[int | float | Decimal]: @value.setter def value(self, value_): - self._value = validators.numeric(value_, allow_empty = True) + self._value = validators.numeric(value_, allow_empty=True) @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'operator': as_dict.get('operator', None), - 'property_': as_dict.get('property', None), - 'value': as_dict.get('value', None) + "operator": as_dict.get("operator", None), + "property_": as_dict.get("property", None), + "value": as_dict.get("value", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: return { - 'operator': self.operator, - 'property': self.property_, - 'value': self.value + "operator": self.operator, + "property": self.property_, + "value": self.value, } @@ -134,37 +134,37 @@ def __init__(self, **kwargs): self._y = None self._z = None - self.align = kwargs.get('align', None) - self.allow_overlap = kwargs.get('allow_overlap', None) - self.animation = kwargs.get('animation', None) - self.background_color = kwargs.get('background_color', None) - self.border_color = kwargs.get('border_color', None) - self.border_radius = kwargs.get('border_radius', None) - self.border_width = kwargs.get('border_width', None) - self.class_name = kwargs.get('class_name', None) - self.color = kwargs.get('color', None) - self.crop = kwargs.get('crop', None) - self.defer = kwargs.get('defer', None) - self.enabled = kwargs.get('enabled', None) - self.filter = kwargs.get('filter', None) - self.format = kwargs.get('format', None) - self.formatter = kwargs.get('formatter', None) - self.inside = kwargs.get('inside', None) - self.null_format = kwargs.get('null_format', None) - self.null_formatter = kwargs.get('null_formatter', None) - self.overflow = kwargs.get('overflow', None) - self.padding = kwargs.get('padding', None) - self.position = kwargs.get('position', None) - self.rotation = kwargs.get('rotation', None) - self.shadow = kwargs.get('shadow', None) - self.shape = kwargs.get('shape', None) - self.style = kwargs.get('style', None) - self.text_path = kwargs.get('text_path', None) - self.use_html = kwargs.get('use_html', None) - self.vertical_align = kwargs.get('vertical_align', None) - self.x = kwargs.get('x', None) - self.y = kwargs.get('y', None) - self.z = kwargs.get('z', None) + self.align = kwargs.get("align", None) + self.allow_overlap = kwargs.get("allow_overlap", None) + self.animation = kwargs.get("animation", None) + self.background_color = kwargs.get("background_color", None) + self.border_color = kwargs.get("border_color", None) + self.border_radius = kwargs.get("border_radius", None) + self.border_width = kwargs.get("border_width", None) + self.class_name = kwargs.get("class_name", None) + self.color = kwargs.get("color", None) + self.crop = kwargs.get("crop", None) + self.defer = kwargs.get("defer", None) + self.enabled = kwargs.get("enabled", None) + self.filter = kwargs.get("filter", None) + self.format = kwargs.get("format", None) + self.formatter = kwargs.get("formatter", None) + self.inside = kwargs.get("inside", None) + self.null_format = kwargs.get("null_format", None) + self.null_formatter = kwargs.get("null_formatter", None) + self.overflow = kwargs.get("overflow", None) + self.padding = kwargs.get("padding", None) + self.position = kwargs.get("position", None) + self.rotation = kwargs.get("rotation", None) + self.shadow = kwargs.get("shadow", None) + self.shape = kwargs.get("shape", None) + self.style = kwargs.get("style", None) + self.text_path = kwargs.get("text_path", None) + self.use_html = kwargs.get("use_html", None) + self.vertical_align = kwargs.get("vertical_align", None) + self.x = kwargs.get("x", None) + self.y = kwargs.get("y", None) + self.z = kwargs.get("z", None) @property def align(self) -> Optional[str]: @@ -191,11 +191,13 @@ def align(self, value): if not value: self._align = None else: - value = validators.string(value, allow_empty = False) + value = validators.string(value, allow_empty=False) value = value.lower() - if value not in ['left', 'center', 'right']: - raise errors.HighchartsValueError(f'align must be either "left", ' - f'"center", or "right". Was: {value}') + if value not in ["left", "center", "right"]: + raise errors.HighchartsValueError( + f'align must be either "left", ' + f'"center", or "right". Was: {value}' + ) self._align = value @@ -262,6 +264,7 @@ def background_color(self) -> Optional[str | Gradient | Pattern]: @background_color.setter def background_color(self, value): from highcharts_core import utility_functions + self._background_color = utility_functions.validate_color(value) @property @@ -276,7 +279,7 @@ def border_color(self) -> Optional[str]: @border_color.setter def border_color(self, value): - self._border_color = validators.string(value, allow_empty = True) + self._border_color = validators.string(value, allow_empty=True) @property def border_radius(self) -> Optional[int | float | Decimal]: @@ -290,7 +293,7 @@ def border_radius(self) -> Optional[int | float | Decimal]: @border_radius.setter def border_radius(self, value): - self._border_radius = validators.numeric(value, allow_empty = True) + self._border_radius = validators.numeric(value, allow_empty=True) @property def border_width(self) -> Optional[int | float | Decimal]: @@ -304,7 +307,7 @@ def border_width(self) -> Optional[int | float | Decimal]: @border_width.setter def border_width(self, value): - self._border_width = validators.numeric(value, allow_empty = True) + self._border_width = validators.numeric(value, allow_empty=True) @property def class_name(self) -> Optional[str]: @@ -318,7 +321,7 @@ def class_name(self) -> Optional[str]: @class_name.setter def class_name(self, value): - self._class_name = validators.string(value, allow_empty = True) + self._class_name = validators.string(value, allow_empty=True) @property def color(self) -> Optional[str]: @@ -339,7 +342,7 @@ def color(self) -> Optional[str]: @color.setter def color(self, value): - self._color = validators.string(value, allow_empty = True) + self._color = validators.string(value, allow_empty=True) @property def crop(self) -> Optional[bool]: @@ -445,7 +448,7 @@ def format(self) -> Optional[str]: @format.setter def format(self, value): - self._format = validators.string(value, allow_empty = True) + self._format = validators.string(value, allow_empty=True) @property def formatter(self) -> Optional[CallbackFunction]: @@ -503,7 +506,7 @@ def null_format(self) -> Optional[str]: @null_format.setter def null_format(self, value): - self._null_format = validators.string(value, allow_empty = True) + self._null_format = validators.string(value, allow_empty=True) @property def null_formatter(self) -> Optional[CallbackFunction]: @@ -556,14 +559,15 @@ def overflow(self) -> Optional[str]: @overflow.setter def overflow(self, value): - value = validators.string(value, allow_empty = True) + value = validators.string(value, allow_empty=True) if not value: self._overflow = None else: value = value.lower() - if value not in ['justify', 'allow', 'none']: - raise errors.HighchartsValueError(f'overflow accepts "justify", "allow", or "none".' - f' Was: {value}') + if value not in ["justify", "allow", "none"]: + raise errors.HighchartsValueError( + f'overflow accepts "justify", "allow", or "none".' f" Was: {value}" + ) self._overflow = value @property @@ -580,7 +584,7 @@ def padding(self) -> Optional[int]: @padding.setter def padding(self, value): - self._padding = validators.numeric(value, allow_empty = True) + self._padding = validators.numeric(value, allow_empty=True) @property def position(self) -> Optional[str]: @@ -608,9 +612,11 @@ def position(self, value): else: value = validators.string(value) value = value.lower() - if value not in ['center', 'left', 'right']: - raise errors.HighchartsValueError(f'position expects a value of "center",' - f' "left", or "right". Was: {value}') + if value not in ["center", "left", "right"]: + raise errors.HighchartsValueError( + f'position expects a value of "center",' + f' "left", or "right". Was: {value}' + ) self._position = value @property @@ -629,7 +635,7 @@ def rotation(self) -> Optional[int | float | Decimal]: @rotation.setter def rotation(self, value): - self._rotation = validators.numeric(value, allow_empty = True) + self._rotation = validators.numeric(value, allow_empty=True) @property def shadow(self) -> Optional[bool | ShadowOptions]: @@ -651,8 +657,7 @@ def shadow(self, value): elif value is False: self._shadow = False else: - value = validate_types(value, - types = ShadowOptions) + value = validate_types(value, types=ShadowOptions) self._shadow = value @property @@ -679,16 +684,20 @@ def shape(self, value): if not value: self._shape = None else: - value = validators.string(value, allow_empty = False) + value = validators.string(value, allow_empty=False) value = value.lower() - if value not in ['callout', - 'connector', - 'rect', - 'circle', - 'diamond', - 'triangle']: - raise errors.HighchartsValueError(f'shape expects a supported annotation ' - f'label shape. Was: {value}') + if value not in [ + "callout", + "connector", + "rect", + "circle", + "diamond", + "triangle", + ]: + raise errors.HighchartsValueError( + f"shape expects a supported annotation " + f"label shape. Was: {value}" + ) self._shape = value @property @@ -721,9 +730,9 @@ def style(self) -> Optional[dict | str]: @style.setter def style(self, value): try: - self._style = validators.dict(value, allow_empty = True) + self._style = validators.dict(value, allow_empty=True) except (ValueError, TypeError): - self._style = validators.string(value, allow_empty = True) + self._style = validators.string(value, allow_empty=True) @property def text_path(self) -> Optional[TextPath]: @@ -783,14 +792,16 @@ def vertical_align(self) -> Optional[str]: @vertical_align.setter def vertical_align(self, value): - value = validators.string(value, allow_empty = True) + value = validators.string(value, allow_empty=True) if not value: self._vertical_align = None else: value = value.lower() - if value not in ['bottom', 'middle', 'top']: - raise errors.HighchartsValueError(f'vertical_align expects either "top", ' - f'"middle", or "bottom". Was: {value}') + if value not in ["bottom", "middle", "top"]: + raise errors.HighchartsValueError( + f'vertical_align expects either "top", ' + f'"middle", or "bottom". Was: {value}' + ) self._vertical_align = value @property @@ -804,7 +815,7 @@ def x(self) -> Optional[int | float | Decimal]: @x.setter def x(self, value): - self._x = validators.numeric(value, allow_empty = True) + self._x = validators.numeric(value, allow_empty=True) @property def y(self) -> Optional[int | float | Decimal]: @@ -817,7 +828,7 @@ def y(self) -> Optional[int | float | Decimal]: @y.setter def y(self, value): - self._y = validators.numeric(value, allow_empty = True) + self._y = validators.numeric(value, allow_empty=True) @property def z(self) -> Optional[int]: @@ -836,79 +847,79 @@ def z(self) -> Optional[int]: @z.setter def z(self, value): - self._z = validators.numeric(value, allow_empty = True) + self._z = validators.numeric(value, allow_empty=True) @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'align': as_dict.get('align', None), - 'allow_overlap': as_dict.get('allowOverlap', None), - 'animation': as_dict.get('animation', None), - 'background_color': as_dict.get('backgroundColor', None), - 'border_color': as_dict.get('borderColor', None), - 'border_radius': as_dict.get('borderRadius', None), - 'border_width': as_dict.get('borderWidth', None), - 'class_name': as_dict.get('className', None), - 'color': as_dict.get('color', None), - 'crop': as_dict.get('crop', None), - 'defer': as_dict.get('defer', None), - 'enabled': as_dict.get('enabled', None), - 'filter': as_dict.get('filter', None), - 'format': as_dict.get('format', None), - 'formatter': as_dict.get('formatter', None), - 'inside': as_dict.get('inside', None), - 'null_format': as_dict.get('nullFormat', None), - 'null_formatter': as_dict.get('nullFormatter', None), - 'overflow': as_dict.get('overflow', None), - 'padding': as_dict.get('padding', None), - 'position': as_dict.get('position', None), - 'rotation': as_dict.get('rotation', None), - 'shadow': as_dict.get('shadow', None), - 'shape': as_dict.get('shape', None), - 'style': as_dict.get('style', None), - 'text_path': as_dict.get('textPath', None), - 'use_html': as_dict.get('useHTML', None), - 'vertical_align': as_dict.get('verticalAlign', None), - 'x': as_dict.get('x', None), - 'y': as_dict.get('y', None), - 'z': as_dict.get('z', None), + "align": as_dict.get("align", None), + "allow_overlap": as_dict.get("allowOverlap", None), + "animation": as_dict.get("animation", None), + "background_color": as_dict.get("backgroundColor", None), + "border_color": as_dict.get("borderColor", None), + "border_radius": as_dict.get("borderRadius", None), + "border_width": as_dict.get("borderWidth", None), + "class_name": as_dict.get("className", None), + "color": as_dict.get("color", None), + "crop": as_dict.get("crop", None), + "defer": as_dict.get("defer", None), + "enabled": as_dict.get("enabled", None), + "filter": as_dict.get("filter", None), + "format": as_dict.get("format", None), + "formatter": as_dict.get("formatter", None), + "inside": as_dict.get("inside", None), + "null_format": as_dict.get("nullFormat", None), + "null_formatter": as_dict.get("nullFormatter", None), + "overflow": as_dict.get("overflow", None), + "padding": as_dict.get("padding", None), + "position": as_dict.get("position", None), + "rotation": as_dict.get("rotation", None), + "shadow": as_dict.get("shadow", None), + "shape": as_dict.get("shape", None), + "style": as_dict.get("style", None), + "text_path": as_dict.get("textPath", None), + "use_html": as_dict.get("useHTML", None), + "vertical_align": as_dict.get("verticalAlign", None), + "x": as_dict.get("x", None), + "y": as_dict.get("y", None), + "z": as_dict.get("z", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'align': self.align, - 'allowOverlap': self.allow_overlap, - 'animation': self.animation, - 'backgroundColor': self.background_color, - 'borderColor': self.border_color, - 'borderRadius': self.border_radius, - 'borderWidth': self.border_width, - 'className': self.class_name, - 'color': self.color, - 'crop': self.crop, - 'defer': self.defer, - 'enabled': self.enabled, - 'filter': self.filter, - 'format': self.format, - 'formatter': self.formatter, - 'inside': self.inside, - 'nullFormat': self.null_format, - 'nullFormatter': self.null_formatter, - 'overflow': self.overflow, - 'padding': self.padding, - 'position': self.position, - 'rotation': self.rotation, - 'shadow': self.shadow, - 'shape': self.shape, - 'style': self.style, - 'textPath': self.text_path, - 'useHTML': self.use_html, - 'verticalAlign': self.vertical_align, - 'x': self.x, - 'y': self.y, - 'z': self.z + "align": self.align, + "allowOverlap": self.allow_overlap, + "animation": self.animation, + "backgroundColor": self.background_color, + "borderColor": self.border_color, + "borderRadius": self.border_radius, + "borderWidth": self.border_width, + "className": self.class_name, + "color": self.color, + "crop": self.crop, + "defer": self.defer, + "enabled": self.enabled, + "filter": self.filter, + "format": self.format, + "formatter": self.formatter, + "inside": self.inside, + "nullFormat": self.null_format, + "nullFormatter": self.null_formatter, + "overflow": self.overflow, + "padding": self.padding, + "position": self.position, + "rotation": self.rotation, + "shadow": self.shadow, + "shape": self.shape, + "style": self.style, + "textPath": self.text_path, + "useHTML": self.use_html, + "verticalAlign": self.vertical_align, + "x": self.x, + "y": self.y, + "z": self.z, } return untrimmed @@ -916,46 +927,48 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: class PieDataLabel(DataLabel): """Variant of :class:`DataLabel` used for pie (and related) series.""" - + def __init__(self, **kwargs): self._distance = None - - self.distance = kwargs.get('distance', None) - + + self.distance = kwargs.get("distance", None) + super().__init__(**kwargs) - + @property def distance(self) -> Optional[int | float | Decimal | str]: - """The distance of the data label from the pie's edge. - + """The distance of the data label from the pie's edge. + .. note:: - - Negative numbers put the data label on top of the pie slices. - + + Negative numbers put the data label on top of the pie slices. + .. tip:: - - Can also be defined as a percentage of pie's radius. - + + Can also be defined as a percentage of pie's radius. + .. warning:: - + Connectors are only shown for data labels outside the pie. - + :rtype: numeric or :class:`str ` or :obj:`None ` """ return self._distance - + @distance.setter def distance(self, value): if value is None: self._distance = None else: try: - value = validators.numeric(value, allow_empty = False) + value = validators.numeric(value, allow_empty=False) except (ValueError, TypeError): if not isinstance(value, str): - raise errors.HighchartsValueError(f'distance must be a number or a string, but received ' - f'{type(value).__name__}.') - if value == '': + raise errors.HighchartsValueError( + f"distance must be a number or a string, but received " + f"{type(value).__name__}." + ) + if value == "": value = None self._distance = value @@ -963,49 +976,48 @@ def distance(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'align': as_dict.get('align', None), - 'allow_overlap': as_dict.get('allowOverlap', None), - 'animation': as_dict.get('animation', None), - 'background_color': as_dict.get('backgroundColor', None), - 'border_color': as_dict.get('borderColor', None), - 'border_radius': as_dict.get('borderRadius', None), - 'border_width': as_dict.get('borderWidth', None), - 'class_name': as_dict.get('className', None), - 'color': as_dict.get('color', None), - 'crop': as_dict.get('crop', None), - 'defer': as_dict.get('defer', None), - 'enabled': as_dict.get('enabled', None), - 'filter': as_dict.get('filter', None), - 'format': as_dict.get('format', None), - 'formatter': as_dict.get('formatter', None), - 'inside': as_dict.get('inside', None), - 'null_format': as_dict.get('nullFormat', None), - 'null_formatter': as_dict.get('nullFormatter', None), - 'overflow': as_dict.get('overflow', None), - 'padding': as_dict.get('padding', None), - 'position': as_dict.get('position', None), - 'rotation': as_dict.get('rotation', None), - 'shadow': as_dict.get('shadow', None), - 'shape': as_dict.get('shape', None), - 'style': as_dict.get('style', None), - 'text_path': as_dict.get('textPath', None), - 'use_html': as_dict.get('useHTML', None), - 'vertical_align': as_dict.get('verticalAlign', None), - 'x': as_dict.get('x', None), - 'y': as_dict.get('y', None), - 'z': as_dict.get('z', None), - - 'distance': as_dict.get('distance', None), + "align": as_dict.get("align", None), + "allow_overlap": as_dict.get("allowOverlap", None), + "animation": as_dict.get("animation", None), + "background_color": as_dict.get("backgroundColor", None), + "border_color": as_dict.get("borderColor", None), + "border_radius": as_dict.get("borderRadius", None), + "border_width": as_dict.get("borderWidth", None), + "class_name": as_dict.get("className", None), + "color": as_dict.get("color", None), + "crop": as_dict.get("crop", None), + "defer": as_dict.get("defer", None), + "enabled": as_dict.get("enabled", None), + "filter": as_dict.get("filter", None), + "format": as_dict.get("format", None), + "formatter": as_dict.get("formatter", None), + "inside": as_dict.get("inside", None), + "null_format": as_dict.get("nullFormat", None), + "null_formatter": as_dict.get("nullFormatter", None), + "overflow": as_dict.get("overflow", None), + "padding": as_dict.get("padding", None), + "position": as_dict.get("position", None), + "rotation": as_dict.get("rotation", None), + "shadow": as_dict.get("shadow", None), + "shape": as_dict.get("shape", None), + "style": as_dict.get("style", None), + "text_path": as_dict.get("textPath", None), + "use_html": as_dict.get("useHTML", None), + "vertical_align": as_dict.get("verticalAlign", None), + "x": as_dict.get("x", None), + "y": as_dict.get("y", None), + "z": as_dict.get("z", None), + "distance": as_dict.get("distance", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'distance': self.distance, + "distance": self.distance, } - parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) or {} + parent_as_dict = super()._to_untrimmed_dict(in_cls=in_cls) or {} for key in parent_as_dict: untrimmed[key] = parent_as_dict[key] @@ -1014,104 +1026,105 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: class SunburstDataLabel(DataLabel): """Variant of :class:`DataLabel` used for :term:`sunburst` series.""" - + def __init__(self, **kwargs): self._rotation_mode = None - - self.rotation_mode = kwargs.get('rotation_mode', None) - + + self.rotation_mode = kwargs.get("rotation_mode", None) + super().__init__(**kwargs) - + @property def rotation_mode(self) -> Optional[str]: - """Determines how the data label will be rotated relative to the perimeter of the sunburst. - + """Determines how the data label will be rotated relative to the perimeter of the sunburst. + Valid values are: - + * ``'circular'`` * ``'auto'`` - * ``'parallel'`` - * ``'perpendicular'``. - + * ``'parallel'`` + * ``'perpendicular'``. + Defaults to ``'circular'``. - + .. note:: - When ``'circular'``, the best fit will be computed for the point, so that the label is curved around the - center when there is room for it, otherwise perpendicular. - - The legacy ``'auto'`` option works similiarly to ``'circular'``, but instead of curving the labels, they are + When ``'circular'``, the best fit will be computed for the point, so that the label is curved around the + center when there is room for it, otherwise perpendicular. + + The legacy ``'auto'`` option works similiarly to ``'circular'``, but instead of curving the labels, they are tangented to the perimiter. - + .. warning:: - - The :meth:`.rotation ` property + + The :meth:`.rotation ` property takes precedence over ``.rotation_mode``. - + :rtype: :class:`str ` or :obj:`None ` """ return self._rotation_mode - + @rotation_mode.setter def rotation_mode(self, value): if not value: self._rotation_mode = None else: - value = validators.string(value, allow_empty = False) + value = validators.string(value, allow_empty=False) value = value.lower() - if value not in ['circular', 'auto', 'parallel', 'perpendicular']: - raise errors.HighchartsValueError(f'if not empty, rotation_mode expects a value of either ' - f'"circular", "auto", "parallel", or "perpendicular", ' - f' but received "{str}".') + if value not in ["circular", "auto", "parallel", "perpendicular"]: + raise errors.HighchartsValueError( + f"if not empty, rotation_mode expects a value of either " + f'"circular", "auto", "parallel", or "perpendicular", ' + f' but received "{str}".' + ) self._rotation_mode = value @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'align': as_dict.get('align', None), - 'allow_overlap': as_dict.get('allowOverlap', None), - 'animation': as_dict.get('animation', None), - 'background_color': as_dict.get('backgroundColor', None), - 'border_color': as_dict.get('borderColor', None), - 'border_radius': as_dict.get('borderRadius', None), - 'border_width': as_dict.get('borderWidth', None), - 'class_name': as_dict.get('className', None), - 'color': as_dict.get('color', None), - 'crop': as_dict.get('crop', None), - 'defer': as_dict.get('defer', None), - 'enabled': as_dict.get('enabled', None), - 'filter': as_dict.get('filter', None), - 'format': as_dict.get('format', None), - 'formatter': as_dict.get('formatter', None), - 'inside': as_dict.get('inside', None), - 'null_format': as_dict.get('nullFormat', None), - 'null_formatter': as_dict.get('nullFormatter', None), - 'overflow': as_dict.get('overflow', None), - 'padding': as_dict.get('padding', None), - 'position': as_dict.get('position', None), - 'rotation': as_dict.get('rotation', None), - 'shadow': as_dict.get('shadow', None), - 'shape': as_dict.get('shape', None), - 'style': as_dict.get('style', None), - 'text_path': as_dict.get('textPath', None), - 'use_html': as_dict.get('useHTML', None), - 'vertical_align': as_dict.get('verticalAlign', None), - 'x': as_dict.get('x', None), - 'y': as_dict.get('y', None), - 'z': as_dict.get('z', None), - - 'rotation_mode': as_dict.get('rotationMode', None), + "align": as_dict.get("align", None), + "allow_overlap": as_dict.get("allowOverlap", None), + "animation": as_dict.get("animation", None), + "background_color": as_dict.get("backgroundColor", None), + "border_color": as_dict.get("borderColor", None), + "border_radius": as_dict.get("borderRadius", None), + "border_width": as_dict.get("borderWidth", None), + "class_name": as_dict.get("className", None), + "color": as_dict.get("color", None), + "crop": as_dict.get("crop", None), + "defer": as_dict.get("defer", None), + "enabled": as_dict.get("enabled", None), + "filter": as_dict.get("filter", None), + "format": as_dict.get("format", None), + "formatter": as_dict.get("formatter", None), + "inside": as_dict.get("inside", None), + "null_format": as_dict.get("nullFormat", None), + "null_formatter": as_dict.get("nullFormatter", None), + "overflow": as_dict.get("overflow", None), + "padding": as_dict.get("padding", None), + "position": as_dict.get("position", None), + "rotation": as_dict.get("rotation", None), + "shadow": as_dict.get("shadow", None), + "shape": as_dict.get("shape", None), + "style": as_dict.get("style", None), + "text_path": as_dict.get("textPath", None), + "use_html": as_dict.get("useHTML", None), + "vertical_align": as_dict.get("verticalAlign", None), + "x": as_dict.get("x", None), + "y": as_dict.get("y", None), + "z": as_dict.get("z", None), + "rotation_mode": as_dict.get("rotationMode", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'rotationMode': self.rotation_mode, + "rotationMode": self.rotation_mode, } - parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) or {} + parent_as_dict = super()._to_untrimmed_dict(in_cls=in_cls) or {} for key in parent_as_dict: untrimmed[key] = parent_as_dict[key] @@ -1120,39 +1133,39 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: class OrganizationDataLabel(DataLabel): """Variant of :class:`DataLabel` used for :term:`organization` series.""" - + def __init__(self, **kwargs): self._link_format = None self._link_formatter = None self._link_text_path = None - - self.link_format = kwargs.get('link_format', None) - self.link_formatter = kwargs.get('link_formatter', None) - self.link_text_path = kwargs.get('link_text_path', None) - + + self.link_format = kwargs.get("link_format", None) + self.link_formatter = kwargs.get("link_formatter", None) + self.link_text_path = kwargs.get("link_text_path", None) + super().__init__(**kwargs) @property def link_format(self) -> Optional[str]: """The format string specifying what to show for links in the\rorganization chart. - + .. tip:: - - Best to use with + + Best to use with :meth:`.link_text_path ` enabled. - + :rtype: :class:`str ` or :obj:`None ` """ return self._link_format - + @link_format.setter def link_format(self, value): - self._link_format = validators.string(value, allow_empty = True) + self._link_format = validators.string(value, allow_empty=True) @property def link_formatter(self) -> Optional[CallbackFunction]: - """JavaScript callback function to format data labels for links in the organization chart. + """JavaScript callback function to format data labels for links in the organization chart. .. note:: @@ -1189,53 +1202,52 @@ def link_text_path(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'align': as_dict.get('align', None), - 'allow_overlap': as_dict.get('allowOverlap', None), - 'animation': as_dict.get('animation', None), - 'background_color': as_dict.get('backgroundColor', None), - 'border_color': as_dict.get('borderColor', None), - 'border_radius': as_dict.get('borderRadius', None), - 'border_width': as_dict.get('borderWidth', None), - 'class_name': as_dict.get('className', None), - 'color': as_dict.get('color', None), - 'crop': as_dict.get('crop', None), - 'defer': as_dict.get('defer', None), - 'enabled': as_dict.get('enabled', None), - 'filter': as_dict.get('filter', None), - 'format': as_dict.get('format', None), - 'formatter': as_dict.get('formatter', None), - 'inside': as_dict.get('inside', None), - 'null_format': as_dict.get('nullFormat', None), - 'null_formatter': as_dict.get('nullFormatter', None), - 'overflow': as_dict.get('overflow', None), - 'padding': as_dict.get('padding', None), - 'position': as_dict.get('position', None), - 'rotation': as_dict.get('rotation', None), - 'shadow': as_dict.get('shadow', None), - 'shape': as_dict.get('shape', None), - 'style': as_dict.get('style', None), - 'text_path': as_dict.get('textPath', None), - 'use_html': as_dict.get('useHTML', None), - 'vertical_align': as_dict.get('verticalAlign', None), - 'x': as_dict.get('x', None), - 'y': as_dict.get('y', None), - 'z': as_dict.get('z', None), - - 'link_format': as_dict.get('linkFormat', None), - 'link_formatter': as_dict.get('linkFormatter', None), - 'link_text_path': as_dict.get('linkTextPath', None), + "align": as_dict.get("align", None), + "allow_overlap": as_dict.get("allowOverlap", None), + "animation": as_dict.get("animation", None), + "background_color": as_dict.get("backgroundColor", None), + "border_color": as_dict.get("borderColor", None), + "border_radius": as_dict.get("borderRadius", None), + "border_width": as_dict.get("borderWidth", None), + "class_name": as_dict.get("className", None), + "color": as_dict.get("color", None), + "crop": as_dict.get("crop", None), + "defer": as_dict.get("defer", None), + "enabled": as_dict.get("enabled", None), + "filter": as_dict.get("filter", None), + "format": as_dict.get("format", None), + "formatter": as_dict.get("formatter", None), + "inside": as_dict.get("inside", None), + "null_format": as_dict.get("nullFormat", None), + "null_formatter": as_dict.get("nullFormatter", None), + "overflow": as_dict.get("overflow", None), + "padding": as_dict.get("padding", None), + "position": as_dict.get("position", None), + "rotation": as_dict.get("rotation", None), + "shadow": as_dict.get("shadow", None), + "shape": as_dict.get("shape", None), + "style": as_dict.get("style", None), + "text_path": as_dict.get("textPath", None), + "use_html": as_dict.get("useHTML", None), + "vertical_align": as_dict.get("verticalAlign", None), + "x": as_dict.get("x", None), + "y": as_dict.get("y", None), + "z": as_dict.get("z", None), + "link_format": as_dict.get("linkFormat", None), + "link_formatter": as_dict.get("linkFormatter", None), + "link_text_path": as_dict.get("linkTextPath", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'linkFormat': self.link_format, - 'linkFormatter': self.link_formatter, - 'linkTextPath': self.link_text_path, + "linkFormat": self.link_format, + "linkFormatter": self.link_formatter, + "linkTextPath": self.link_text_path, } - parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) or {} + parent_as_dict = super()._to_untrimmed_dict(in_cls=in_cls) or {} for key in parent_as_dict: untrimmed[key] = parent_as_dict[key] @@ -1249,8 +1261,8 @@ def __init__(self, **kwargs): self._node_format = None self._node_formatter = None - self.node_format = kwargs.get('node_format', None) - self.node_formatter = kwargs.get('node_formatter', None) + self.node_format = kwargs.get("node_format", None) + self.node_formatter = kwargs.get("node_formatter", None) super().__init__(**kwargs) @@ -1265,7 +1277,7 @@ def node_format(self) -> Optional[str]: @node_format.setter def node_format(self, value): - self._node_format = validators.string(value, allow_empty = True) + self._node_format = validators.string(value, allow_empty=True) @property def node_formatter(self) -> Optional[CallbackFunction]: @@ -1289,52 +1301,59 @@ def node_formatter(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'align': as_dict.get('align', None), - 'allow_overlap': as_dict.get('allowOverlap', None), - 'animation': as_dict.get('animation', None), - 'background_color': as_dict.get('backgroundColor', None), - 'border_color': as_dict.get('borderColor', None), - 'border_radius': as_dict.get('borderRadius', None), - 'border_width': as_dict.get('borderWidth', None), - 'class_name': as_dict.get('className', None), - 'color': as_dict.get('color', None), - 'crop': as_dict.get('crop', None), - 'defer': as_dict.get('defer', None), - 'enabled': as_dict.get('enabled', None), - 'filter': as_dict.get('filter', None), - 'format': as_dict.get('format', None), - 'formatter': as_dict.get('formatter', None), - 'inside': as_dict.get('inside', None), - 'null_format': as_dict.get('nullFormat', None), - 'null_formatter': as_dict.get('nullFormatter', None), - 'overflow': as_dict.get('overflow', None), - 'padding': as_dict.get('padding', None), - 'position': as_dict.get('position', None), - 'rotation': as_dict.get('rotation', None), - 'shadow': as_dict.get('shadow', None), - 'shape': as_dict.get('shape', None), - 'style': as_dict.get('style', None), - 'text_path': as_dict.get('textPath', None), - 'use_html': as_dict.get('useHTML', None), - 'vertical_align': as_dict.get('verticalAlign', None), - 'x': as_dict.get('x', None), - 'y': as_dict.get('y', None), - 'z': as_dict.get('z', None), - - 'node_format': as_dict.get('nodeFormat', None), - 'node_formatter': as_dict.get('nodeFormatter', None), + "align": as_dict.get("align", None), + "allow_overlap": as_dict.get("allowOverlap", None), + "animation": as_dict.get("animation", None), + "background_color": as_dict.get("backgroundColor", None), + "border_color": as_dict.get("borderColor", None), + "border_radius": as_dict.get("borderRadius", None), + "border_width": as_dict.get("borderWidth", None), + "class_name": as_dict.get("className", None), + "color": as_dict.get("color", None), + "crop": as_dict.get("crop", None), + "defer": as_dict.get("defer", None), + "enabled": as_dict.get("enabled", None), + "filter": as_dict.get("filter", None), + "format": as_dict.get("format", None), + "formatter": as_dict.get("formatter", None), + "inside": as_dict.get("inside", None), + "null_format": as_dict.get("nullFormat", None), + "null_formatter": as_dict.get("nullFormatter", None), + "overflow": as_dict.get("overflow", None), + "padding": as_dict.get("padding", None), + "position": as_dict.get("position", None), + "rotation": as_dict.get("rotation", None), + "shadow": as_dict.get("shadow", None), + "shape": as_dict.get("shape", None), + "style": as_dict.get("style", None), + "text_path": as_dict.get("textPath", None), + "use_html": as_dict.get("useHTML", None), + "vertical_align": as_dict.get("verticalAlign", None), + "x": as_dict.get("x", None), + "y": as_dict.get("y", None), + "z": as_dict.get("z", None), + "node_format": as_dict.get("nodeFormat", None), + "node_formatter": as_dict.get("nodeFormatter", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'nodeFormat': self.node_format, - 'nodeFormatter': self.node_formatter, + "nodeFormat": self.node_format, + "nodeFormatter": self.node_formatter, } - parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) or {} + parent_as_dict = super()._to_untrimmed_dict(in_cls=in_cls) or {} for key in parent_as_dict: untrimmed[key] = parent_as_dict[key] return untrimmed + + +data_label_property_map = { + "distance": PieDataLabel, + "rotation_mode": SunburstDataLabel, + "link_format|link_formatter|link_text_path": OrganizationDataLabel, + "node_format|node_formatter": NodeDataLabel, +}