From c5e0e272faa666a89f8f3fbe53b761b8a6aa96b4 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 14 Aug 2023 11:07:53 -0400 Subject: [PATCH 1/3] Implemented one-shot series-to-chart creation and rendering. Closes #89. --- highcharts_core/options/series/base.py | 103 +++++++++++++++++++++++++ tests/options/series/test_base.py | 16 +++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/highcharts_core/options/series/base.py b/highcharts_core/options/series/base.py index 38740c65..7a910dce 100644 --- a/highcharts_core/options/series/base.py +++ b/highcharts_core/options/series/base.py @@ -943,3 +943,106 @@ def from_pyspark(cls, instance.load_from_pyspark(df, property_map) return instance + + def to_chart(self, chart_kwargs = None, options_kwargs = None): + """Create a :class:`Chart ` instance containing the + series instance. + + :param chart_kwargs: Optional keyword arguments to use when constructing the + :class:`Chart ` instance. Defaults to + :obj:`None `. + :type chart_kwargs: :class:`dict ` + + :param options_kwargs: Optional keyword arguments to use when constructing the + chart's :class:`HighchartsOptions ` + object. Defaults to :obj:`None `. + + .. warning:: + + If your ``chart_kwargs`` contains an ``options`` key, its value + will be overwritten if you supply ``options_kwargs``. + + :type options_kwargs: :class:`dict ` + + :returns: A :class:`Chart ` instance containing the + series instance. + :rtype: :class:`Chart ` + """ + from highcharts_core.chart import Chart + + chart_kwargs = validators.dict(chart_kwargs, allow_empty = True) or {} + + as_chart = Chart(**chart_kwargs) + if options_kwargs: + as_chart.options = options_kwargs + + as_chart.add_series(self) + + return as_chart + + def display(self, + global_options = None, + container = None, + retries = 5, + interval = 1000, + chart_kwargs = None, + options_kwargs = None): + """Display the series in `Jupyter Labs `_ or + `Jupyter Notebooks `_. + + :param global_options: The :term:`shared options` to use when rendering the chart. + 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 ` + 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 + 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 + Highcharts script. Defaults to 5. + :type retries: :class:`int ` + + :param interval: The number of milliseconds to wait between retrying rendering the chart. Defaults to 1000 (1 + seocnd). + :type interval: :class:`int ` + + :param chart_kwargs: Optional keyword arguments to use when constructing the + :class:`Chart ` instance. Defaults to + :obj:`None `. + :type chart_kwargs: :class:`dict ` + + :param options_kwargs: Optional keyword arguments to use when constructing the + chart's :class:`HighchartsOptions ` + object. Defaults to :obj:`None `. + + .. warning:: + + If your ``chart_kwargs`` contains an ``options`` key, its value + will be overwritten if you supply ``options_kwargs``. + + :type options_kwargs: :class:`dict ` + + :raises HighchartsDependencyError: if + `ipython `_ is not available in the + runtime environment + """ + as_chart = self.to_chart(chart_kwargs = chart_kwargs, + options_kwargs = options_kwargs) + as_chart.display(global_options = global_options, + container = container, + retries = retries, + interval = interval) \ No newline at end of file diff --git a/tests/options/series/test_base.py b/tests/options/series/test_base.py index 9dbef12c..389cf82f 100644 --- a/tests/options/series/test_base.py +++ b/tests/options/series/test_base.py @@ -3,6 +3,7 @@ import pytest from json.decoder import JSONDecodeError +from validator_collection import checkers from highcharts_core.options.series.base import SeriesBase as cls from highcharts_core import errors @@ -684,7 +685,20 @@ def test_load_from_pandas(input_files, filename, property_map, error): else: with pytest.raises(error): instance.load_from_pandas(df, property_map = property_map) - + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_to_chart(kwargs, error): + if not error: + instance = cls(**kwargs) + chart = instance.to_chart() + assert chart is not None + assert checkers.is_type(chart, 'Chart') + else: + with pytest.raises(error): + instance = cls(**kwargs) + chart = instance.to_chart() + @pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) def test__repr__(kwargs, error): From 86c4cea9ecc9a794b29e529f2765bade60cd37f9 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 14 Aug 2023 11:57:41 -0400 Subject: [PATCH 2/3] Implemented one-shot Chart creation. Closes #90. --- highcharts_core/chart.py | 67 +++++++++++++++++++++++++++++++++++++++- tests/test_chart.py | 45 ++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/highcharts_core/chart.py b/highcharts_core/chart.py index cd8e29e1..213f39d9 100644 --- a/highcharts_core/chart.py +++ b/highcharts_core/chart.py @@ -19,6 +19,51 @@ class Chart(HighchartsMeta): """Python representation of a Highcharts ``Chart`` object.""" def __init__(self, **kwargs): + """Creates a :class:`Chart ` instance. + + When creating a :class:`Chart ` instance, you can + provide any of the object's properties as keyword arguments. + **Positional arguments are not supported**. + + In addition to the standard properties, there are three special keyword + arguments which streamline the creation of + :class:`Chart ` instances: + + * ``series`` which accepts an iterable of + :class:`SeriesBase ` descendents. + These are automatically then populated as series within the chart. + + .. note:: + + Each member of ``series`` must be coercable into a Highcharts Core for Python + series. And it must contain a ``type`` property. + + * ``data`` which accepts an iterable of objects that are coercable to Highcharts + data point objects, which are then automatically used to create/populate a + series on your chart instance + * ``series_type`` which accepts a string indicating the type of series to render + for your data. + + .. warning:: + + If you supply ``series``, the ``data`` and ``series_type`` keywords will be + *ignored*. + + If you supply ``data``, then ``series_type`` must *also* be supplied. Failure + to do so will raise a + :exc:`HighchartsValueError `. + + If you are also supplying an + :meth:`options ` keyword argument, then + any series derived from ``series``, ``data``, and ``series_type`` will be + *added* to any series defined in that ``options`` value. + + :raises: :exc:`HighchartsValueError ` + if supplying ``data`` with no ``series_type``. + + :returns: A :class:`Chart ` instance. + :rtype: :class:`Chart ` + """ self._callback = None self._container = None self._options = None @@ -35,7 +80,27 @@ def __init__(self, **kwargs): None) or os.environ.get('HIGHCHARTS_MODULE_URL', 'https://code.highcharts.com/') - super().__init__(**kwargs) + 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)): + series = [series] + self.add_series(*series) + elif data is not None and 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.') def __str__(self): """Return a human-readable :class:`str ` representation of the chart. diff --git a/tests/test_chart.py b/tests/test_chart.py index 30eb7564..3409334d 100644 --- a/tests/test_chart.py +++ b/tests/test_chart.py @@ -337,4 +337,47 @@ def test__str__(kwargs, error): assert 'options = ' in result else: with pytest.raises(error): - result = str(obj) \ No newline at end of file + result = str(obj) + + +@pytest.mark.parametrize('kwargs, expected_series, expected_data_points, error', [ + ({}, 0, [], None), + + ({ + 'series': [ + { + 'data': [[1, 2], [3, 4]], + 'type': 'line' + } + ] + }, 1, [(0, 2)], None), + ({ + 'series': { + 'data': [[1, 2], [3, 4]], + 'type': 'line' + } + }, 1, [(0, 2)], None), + + ({ + 'data': [[1, 2], [3, 4]], + 'series_type': 'line' + }, 1, [(0, 2)], None), + + ({ + 'data': [[1, 2], [3, 4]], + }, 1, [(0, 2)], errors.HighchartsValueError), + +]) +def test_issue90_one_shot_creation(kwargs, expected_series, expected_data_points, error): + if not error: + result = cls(**kwargs) + assert result is not None + if kwargs: + assert getattr(result, 'options') is not None + assert getattr(result.options, 'series') is not None + assert len(result.options.series) == expected_series + for item in expected_data_points: + assert len(result.options.series[item[0]].data) == item[1] + else: + with pytest.raises(error): + result = cls(**kwargs) \ No newline at end of file From 3aad1f90d8d4778fab96a42b135727e945225c77 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 14 Aug 2023 11:58:28 -0400 Subject: [PATCH 3/3] Bumped version number and updated changelog. --- CHANGES.rst | 9 +++++++++ highcharts_core/__version__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 75bb63b6..27ef0bda 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,13 @@ +Release 1.4.0 +========================================= + +* **ENHANCEMENT:** Added one-shot chart creation and rendering from Series objects (#89). +* **ENHANCEMENT:** Added one-shot chart creation using ``series`` and ``data``/``series_type`` keywords. (#90). + +--------------------- + + Release 1.3.4 ========================================= diff --git a/highcharts_core/__version__.py b/highcharts_core/__version__.py index e372abf0..bdbb22b7 100644 --- a/highcharts_core/__version__.py +++ b/highcharts_core/__version__.py @@ -1 +1 @@ -__version__ = '1.3.4' \ No newline at end of file +__version__ = '1.4.0' \ No newline at end of file