From bf15bf9c958c638814351911316c38590aee0703 Mon Sep 17 00:00:00 2001 From: Jitendra Mishra Date: Fri, 5 May 2023 08:32:32 +0530 Subject: [PATCH 01/36] few changes --- highcharts_core/chart.py | 138 ++++++++---------- .../global_options/shared_options.py | 4 +- 2 files changed, 63 insertions(+), 79 deletions(-) diff --git a/highcharts_core/chart.py b/highcharts_core/chart.py index 668973d2..de1b2ddb 100644 --- a/highcharts_core/chart.py +++ b/highcharts_core/chart.py @@ -43,18 +43,16 @@ def _jupyter_include_scripts(self): :rtype: :class:`str ` """ - js_str = '' - for item in constants.INCLUDE_LIBS: - js_str += utility_functions.jupyter_add_script(item) - js_str += """.then(() => {""" - - for item in constants.INCLUDE_LIBS: - js_str += """});""" + js_str = ''.join( + utility_functions.jupyter_add_script(item) + """.then(() => {""" + for item in constants.INCLUDE_LIBS + ) + js_str += """});""" * len(constants.INCLUDE_LIBS) return js_str - def _jupyter_javascript(self, - global_options = None, + def _jupyter_javascript(self, + global_options = None, container = None, random_slug = None, retries = 3, @@ -65,21 +63,21 @@ def _jupyter_javascript(self, Defaults to :obj:`None ` :type global_options: :class:`SharedOptions ` or :obj:`None ` - + :param container: The ID to apply to the HTML container when rendered in Jupyter Labs. Defaults to - :obj:`None `, which applies the :meth:`.container ` + :obj:`None `, which applies the :meth:`.container ` property if set, and ``'highcharts_target_div'`` if not set. :type container: :class:`str ` or :obj:`None ` - + :param random_slug: The random sequence of characters to append to the container name to ensure uniqueness. Defaults to :obj:`None ` :type random_slug: :class:`str ` or :obj:`None ` - - :param retries: The number of times to retry rendering the chart. Used to avoid race conditions with the + + :param retries: The number of times to retry rendering the chart. Used to avoid race conditions with the Highcharts script. Defaults to 3. :type retries: :class:`int ` - - :param interval: The number of milliseconds to wait between retrying rendering the chart. Defaults to 1000 (1 + + :param interval: The number of milliseconds to wait between retrying rendering the chart. Defaults to 1000 (1 seocnd). :type interval: :class:`int ` @@ -91,14 +89,13 @@ def _jupyter_javascript(self, self.container = new_container else: self.container = f'{new_container}_{random_slug}' - + if global_options is not None: global_options = validate_types(global_options, types = SharedOptions) - js_str = '' - js_str += utility_functions.get_retryHighcharts() - + js_str = utility_functions.get_retryHighcharts() + if global_options: js_str += '\n' + utility_functions.prep_js_for_jupyter(global_options.to_js_literal()) + '\n' @@ -118,11 +115,11 @@ def _jupyter_container_html(self, """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 - :obj:`None `, which applies the :meth:`.container ` + :obj:`None `, which applies the :meth:`.container ` property if set, and ``'highcharts_target_div'`` if not set. :type container: :class:`str ` or :obj:`None ` - :param random_slug: The random sequence of characters to append to the container/function name to ensure + :param random_slug: The random sequence of characters to append to the container/function name to ensure uniqueness. Defaults to :obj:`None ` :type random_slug: :class:`str ` or :obj:`None ` @@ -296,8 +293,6 @@ def to_js_literal(self, :rtype: :class:`str ` or :obj:`None ` """ - if filename: - filename = validators.path(filename) untrimmed = self._to_untrimmed_dict() as_dict = {} @@ -307,27 +302,21 @@ def to_js_literal(self, if serialized is not None: as_dict[key] = serialized - signature_elements = 0 + signature_elements = 2 - container_as_str = '' if self.container: container_as_str = f"""'{self.container}'""" else: container_as_str = """null""" - signature_elements += 1 - options_as_str = '' if self.options: - options_as_str = self.options.to_js_literal(encoding = encoding) - options_as_str = f"""{options_as_str}""" + options_as_str = "{}".format(self.options.to_js_literal(encoding = encoding)) else: options_as_str = """null""" - signature_elements += 1 callback_as_str = '' if self.callback: - callback_as_str = self.callback.to_js_literal(encoding = encoding) - callback_as_str = f"""{callback_as_str}""" + callback_as_str = "{}".format(self.callback.to_js_literal(encoding = encoding)) signature_elements += 1 signature = """Highcharts.chart(""" @@ -351,7 +340,7 @@ def to_js_literal(self, suffix = """});""" as_str = prefix + as_str + '\n' + suffix - if filename: + if filename and validators.path(filename): with open(filename, 'w', encoding = encoding) as file_: file_.write(as_str) @@ -463,14 +452,14 @@ def _copy_dict_key(cls, if key == 'data' and preserve_data: return other_value - + if key == 'points' and preserve_data: return other_value - + if key == 'series' and preserve_data: if not other_value: return [x for x in original_value] - + if len(other_value) != len(original_value): matched_series = [] new_series = [] @@ -501,14 +490,12 @@ def _copy_dict_key(cls, return updated_series elif isinstance(original_value, (dict, UserDict)): - new_value = {} - for subkey in original_value: - new_key_value = cls._copy_dict_key(subkey, + new_value = {subkey: cls._copy_dict_key(subkey, original_value, other_value, overwrite = overwrite, **kwargs) - new_value[subkey] = new_key_value + for subkey in original_value} return new_value @@ -576,10 +563,8 @@ def add_series(self, *series): or coercable """ - new_series = [] - for item in series: - item_series = create_series_obj(item) - new_series.append(item_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] @@ -594,8 +579,8 @@ def add_series(self, *series): self.options.series = updated_series def update_series(self, *series, add_if_unmatched = False): - """Replace existing series with the new versions supplied in ``series``, - matching them based on their + """Replace existing series with the new versions supplied in ``series``, + matching them based on their :meth:`.id ` property. :param series: One or more :term:`series` instances (descended from @@ -605,17 +590,15 @@ def update_series(self, *series, add_if_unmatched = False): :type series: one or more :class:`SeriesBase ` or coercable - - :param add_if_unmatched: If ``True``, will add a series that does not have a - match. If ``False``, will raise a + + :param add_if_unmatched: If ``True``, will add a series that does not have a + match. If ``False``, will raise a :exc:`HighchartsMissingSeriesError ` if a series does not have a match on the chart. Defaults to ``False``. :type add_if_unmatched: :class:`bool ` """ - new_series = [] - for item in series: - item_series = create_series_obj(item) - new_series.append(item_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] @@ -624,23 +607,22 @@ def update_series(self, *series, add_if_unmatched = False): else: existing_series = [] self.options = HighchartsOptions() - + existing_ids = [x.id for x in existing_series] new_ids = [x.id for x in new_series] overlap_ids = [x for x in new_ids if x in existing_ids] - - updated_series = [] - for existing in existing_series: - if existing.id not in overlap_ids: - updated_series.append(existing) - + + 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') - + updated_series.append(new) self.options.series = updated_series @@ -680,11 +662,11 @@ def from_series(cls, *series, kwargs = None): instance.add_series(item) else: instance.add_series(series) - + return instance - def display(self, - global_options = None, + def display(self, + global_options = None, container = None, retries = 3, interval = 1000): @@ -695,29 +677,29 @@ def display(self, Defaults to :obj:`None ` :type global_options: :class:`SharedOptions ` or :obj:`None ` - + :param container: The ID to apply to the HTML container when rendered in Jupyter Labs. Defaults to - :obj:`None `, which applies the :meth:`.container ` + :obj:`None `, which applies the :meth:`.container ` property if set, and ``'highcharts_target_div'`` if not set. - + .. note:: - + Highcharts for Python will append a 6-character random string to the value of ``container`` to ensure uniqueness of the chart's container when rendering in a Jupyter Notebook/Labs context. The - :class:`Chart ` instance will retain the mapping between container and the + :class:`Chart ` instance will retain the mapping between container and the random string so long as the instance exists, thus allowing you to easily update the rendered chart by calling the :meth:`.display() ` method again. - + If you wish to create a new chart from the instance that does not update the existing chart, then you can do so by specifying a new ``container`` value. - + :type container: :class:`str ` or :obj:`None ` - :param retries: The number of times to retry rendering the chart. Used to avoid race conditions with the + :param retries: The number of times to retry rendering the chart. Used to avoid race conditions with the Highcharts script. Defaults to 3. :type retries: :class:`int ` - - :param interval: The number of milliseconds to wait between retrying rendering the chart. Defaults to 1000 (1 + + :param interval: The number of milliseconds to wait between retrying rendering the chart. Defaults to 1000 (1 seocnd). :type interval: :class:`int ` @@ -740,9 +722,9 @@ def display(self, container = container or self.container or 'highcharts_target_div' if not self._random_slug: self._random_slug = {} - + random_slug = self._random_slug.get(container, None) - + if not random_slug: random_slug = utility_functions.get_random_string() self._random_slug[container] = random_slug @@ -750,7 +732,7 @@ def display(self, 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, + chart_js_str = self._jupyter_javascript(global_options = global_options, container = container, random_slug = random_slug, retries = retries, diff --git a/highcharts_core/global_options/shared_options.py b/highcharts_core/global_options/shared_options.py index 80c7841f..320d4c5c 100644 --- a/highcharts_core/global_options/shared_options.py +++ b/highcharts_core/global_options/shared_options.py @@ -3,6 +3,8 @@ import esprima from esprima.error_handler import Error as ParseError +from validator_collection import validators, checkers + from highcharts_core import errors from highcharts_core.decorators import validate_types from highcharts_core.options import HighchartsOptions @@ -42,7 +44,7 @@ def to_js_literal(self, as_str = prefix + options_body + ');' - if filename: + if filename and validate.path(filename): with open(filename, 'w', encoding = encoding) as file_: file_.write(as_str) From cf0c56e6b6ea3189e26e92ee2682481c7950b62f Mon Sep 17 00:00:00 2001 From: Jitendra Mishra Date: Sun, 7 May 2023 08:02:46 +0530 Subject: [PATCH 02/36] avoid run if clause everytime in a loop --- highcharts_core/metaclasses.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/highcharts_core/metaclasses.py b/highcharts_core/metaclasses.py index a2610f7b..ad12b8cd 100644 --- a/highcharts_core/metaclasses.py +++ b/highcharts_core/metaclasses.py @@ -197,13 +197,12 @@ def from_dict(cls, """ as_dict = validators.dict(as_dict, allow_empty = True) or {} clean_as_dict = {} - for key in as_dict: - if allow_snake_case: - clean_key = utility_functions.to_camelCase(key) - else: - clean_key = key - - clean_as_dict[clean_key] = as_dict[key] + if allow_snake_case: + clean_as_dict = {utility_functions.to_camelCase(key): as_dict[key] + for key in as_dict} + else: + clean_as_dict = {key: as_dict[key] + for key in as_dict} kwargs = cls._get_kwargs_from_dict(clean_as_dict) From f5ce0455c476c6441c0c08df3b42d93a3c450d46 Mon Sep 17 00:00:00 2001 From: Jitendra Mishra Date: Thu, 11 May 2023 11:06:58 +0530 Subject: [PATCH 03/36] Apply suggestions from code review Co-authored-by: Chris Modzelewski <123704219+hcpchris@users.noreply.github.com> --- highcharts_core/global_options/shared_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highcharts_core/global_options/shared_options.py b/highcharts_core/global_options/shared_options.py index 320d4c5c..8e246c6e 100644 --- a/highcharts_core/global_options/shared_options.py +++ b/highcharts_core/global_options/shared_options.py @@ -44,7 +44,7 @@ def to_js_literal(self, as_str = prefix + options_body + ');' - if filename and validate.path(filename): + if validators.path(filename, allow_empty = True): with open(filename, 'w', encoding = encoding) as file_: file_.write(as_str) From 4ccb17c02c4762109c6d79abee8c68fdd4396c3e Mon Sep 17 00:00:00 2001 From: Jitendra Mishra Date: Thu, 11 May 2023 11:08:28 +0530 Subject: [PATCH 04/36] Update highcharts_core/chart.py Co-authored-by: Chris Modzelewski <123704219+hcpchris@users.noreply.github.com> --- highcharts_core/chart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highcharts_core/chart.py b/highcharts_core/chart.py index de1b2ddb..51fcef4a 100644 --- a/highcharts_core/chart.py +++ b/highcharts_core/chart.py @@ -340,7 +340,7 @@ def to_js_literal(self, suffix = """});""" as_str = prefix + as_str + '\n' + suffix - if filename and validators.path(filename): + if validators.path(filename, allow_empty = True): with open(filename, 'w', encoding = encoding) as file_: file_.write(as_str) From f8ef3d5ef4f1c8796bce287bd6461dac65bdea3d Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Sun, 4 Jun 2023 11:25:16 -0400 Subject: [PATCH 05/36] Added methods to compile include scripts based on class dot path notations. --- highcharts_core/chart.py | 89 +- highcharts_core/constants.py | 13 + highcharts_core/metaclasses.py | 42 +- highcharts_core/module_requirements.json | 1145 ++++++++++++++++++++++ 4 files changed, 1279 insertions(+), 10 deletions(-) create mode 100644 highcharts_core/module_requirements.json diff --git a/highcharts_core/chart.py b/highcharts_core/chart.py index 668973d2..deb975b3 100644 --- a/highcharts_core/chart.py +++ b/highcharts_core/chart.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, List from collections import UserDict from validator_collection import validators, checkers @@ -22,6 +22,7 @@ def __init__(self, **kwargs): self._container = None self._options = None self._variable_name = None + self._module_url = None self._random_slug = {} @@ -29,26 +30,23 @@ def __init__(self, **kwargs): 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('custom_module_url', 'https://code.highcharts.com/') super().__init__(**kwargs) def _jupyter_include_scripts(self): """Return the JavaScript code that is used to load the Highcharts JS libraries. - .. note:: - - Currently includes *all* `Highcharts JS `_ modules - in the HTML. This issue will be addressed when roadmap issue :issue:`2` is - released. - :rtype: :class:`str ` """ + required_modules = [f'{self.module_url}{x}' + for x in self.get_required_modules(include_extension = True)] js_str = '' - for item in constants.INCLUDE_LIBS: + for item in constants.required_modules: js_str += utility_functions.jupyter_add_script(item) js_str += """.then(() => {""" - for item in constants.INCLUDE_LIBS: + for item in required_modules: js_str += """});""" return js_str @@ -155,6 +153,38 @@ def _repr_html_(self): """ return self.display() + @property + 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. + + :param include_extension: if ``True``, will return script names with the + ``'.js'`` extension included. Defaults to ``False``. + :type include_extension: :class:`bool ` + + :rtype: :class:`list ` + """ + scripts = ['highcharts'] + properties = [x for x in self.__dict__ if x.__class__.__name__ == 'property'] + for property_name in properties: + property_value = getattr(self, property_name, None) + if not property_value: + continue + if isinstance(property_value, HighchartsMeta): + scripts.extend([x for x in property_value.get_required_modules() + if x not in scripts]) + continue + property_name_as_camelCase = utility_functions.to_camelCase(property_name) + dot_path = f'{self._dot_path}.' or '' + dot_path += {property_name_as_camelCase} + scripts.extend([x for x in constants.MODULE_REQUIREMENTS.get(dot_path, []) + if x not in scripts]) + + if include_extension: + scripts = [f'{x}.js' for x in scripts] + + return scripts + @property def callback(self) -> Optional[CallbackFunction]: """A (JavaScript) function that is run when the chart has loaded and all external @@ -174,6 +204,47 @@ def callback(self) -> Optional[CallbackFunction]: def callback(self, value): self._callback = value + @property + def module_url(self) -> str: + """The URL from which Highcharts modules should be downloaded when + generating the ``