Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -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
=========================================

Expand Down
2 changes: 1 addition & 1 deletion highcharts_core/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.3.4'
__version__ = '1.4.0'
67 changes: 66 additions & 1 deletion highcharts_core/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,51 @@ class Chart(HighchartsMeta):
"""Python representation of a Highcharts ``Chart`` object."""

def __init__(self, **kwargs):
"""Creates a :class:`Chart <highcharts_core.chart.Chart>` instance.

When creating a :class:`Chart <highcharts_core.chart.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 <highcharts_core.chart.Chart>` instances:

* ``series`` which accepts an iterable of
:class:`SeriesBase <highcharts_core.options.series.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 <highcharts_core.errors.HighchartsValueError>`.

If you are also supplying an
:meth:`options <highcharts_core.chart.Chart.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 <highcharts_core.errors.HighchartsValueError>`
if supplying ``data`` with no ``series_type``.

:returns: A :class:`Chart <highcharts_core.chart.Chart>` instance.
:rtype: :class:`Chart <highcharts_core.chart.Chart>`
"""
self._callback = None
self._container = None
self._options = None
Expand All @@ -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 <python:str>` representation of the chart.
Expand Down
103 changes: 103 additions & 0 deletions highcharts_core/options/series/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <highcharts_core.chart.Chart>` instance containing the
series instance.

:param chart_kwargs: Optional keyword arguments to use when constructing the
:class:`Chart <highcharts_core.chart.Chart>` instance. Defaults to
:obj:`None <python:None>`.
:type chart_kwargs: :class:`dict <python:dict>`

:param options_kwargs: Optional keyword arguments to use when constructing the
chart's :class:`HighchartsOptions <highcharts_core.options.HighchartsOptions>`
object. Defaults to :obj:`None <python: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 <python:dict>`

:returns: A :class:`Chart <highcharts_core.chart.Chart>` instance containing the
series instance.
:rtype: :class:`Chart <highcharts_core.chart.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 <https://jupyter.org/>`_ or
`Jupyter Notebooks <https://jupyter.org/>`_.

:param global_options: The :term:`shared options` to use when rendering the chart.
Defaults to :obj:`None <python:None>`
:type global_options: :class:`SharedOptions <highcharts_stock.global_options.shared_options.SharedOptions>`
or :obj:`None <python:None>`

:param container: The ID to apply to the HTML container when rendered in Jupyter Labs. Defaults to
:obj:`None <python:None>`, which applies the :meth:`.container <highcharts_core.chart.Chart.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 <highcharts_core.chart.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() <highcharts_core.chart.Chart.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 <python:str>` or :obj:`None <python: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 <python:int>`

:param interval: The number of milliseconds to wait between retrying rendering the chart. Defaults to 1000 (1
seocnd).
:type interval: :class:`int <python:int>`

:param chart_kwargs: Optional keyword arguments to use when constructing the
:class:`Chart <highcharts_core.chart.Chart>` instance. Defaults to
:obj:`None <python:None>`.
:type chart_kwargs: :class:`dict <python:dict>`

:param options_kwargs: Optional keyword arguments to use when constructing the
chart's :class:`HighchartsOptions <highcharts_core.options.HighchartsOptions>`
object. Defaults to :obj:`None <python: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 <python:dict>`

:raises HighchartsDependencyError: if
`ipython <https://ipython.readthedocs.io/en/stable/>`_ 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)
16 changes: 15 additions & 1 deletion tests/options/series/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
45 changes: 44 additions & 1 deletion tests/test_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,4 +337,47 @@ def test__str__(kwargs, error):
assert 'options = ' in result
else:
with pytest.raises(error):
result = str(obj)
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)