Source code for highcharts_core.options.series.base

from typing import Optional, List
from decimal import Decimal
from collections import UserDict

try:
    import orjson as json
except ImportError:
    try:
        import rapidjson as json
    except ImportError:
        try:
            import simplejson as json
        except ImportError:
            import json
try:
    import numpy as np
    HAS_NUMPY = True
except ImportError:
    HAS_NUMPY = False

from validator_collection import validators, checkers

from highcharts_core import errors, utility_functions, constants
from highcharts_core.options.plot_options.series import SeriesOptions
from highcharts_core.options.series.data.base import DataBase
from highcharts_core.options.series.data.collections import DataPointCollection


class SeriesBase(SeriesOptions):
    """Generic base class for specific series configurations."""

    def __init__(self, **kwargs):
        self._data = None
        self._id = None
        self._index = None
        self._legend_index = None
        self._name = None
        self._stack = None
        self._x_axis = None
        self._y_axis = None
        self._z_index = None

        self.data = kwargs.get('data', None)
        self.id = kwargs.get('id', None)
        self.index = kwargs.get('index', None)
        self.legend_index = kwargs.get('legend_index', None)
        self.name = kwargs.get('name', None)
        self.stack = kwargs.get('stack', None)
        self.x_axis = kwargs.get('x_axis', None)
        self.y_axis = kwargs.get('y_axis', None)
        self.z_index = kwargs.get('z_index', None)

        super().__init__(**kwargs)

    def __str__(self):
        """Return a human-readable :class:`str <python:str>` representation of the series.

        .. warning::
        
          To ensure that the result is human-readable, the string representation
          will be generated *without* its 
          :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` 
          property.
        
          .. tip::
        
            If you would like a *complete* and *unambiguous* :class:`str <python:str>` 
            representation, then you can:
            
            * use the :meth:`__repr__() <highcharts_core.options.series.base.SeriesBase.__repr__>` method,
            * call ``repr(my_series)``, or
            * serialize the series to JSON using ``my_series.to_json()``.
            
        :returns: A :class:`str <python:str>` representation of the chart.
        :rtype: :class:`str <python:str>`
        """
        as_dict = self.to_dict()

        kwargs = {utility_functions.to_snake_case(key): as_dict[key]
                  for key in as_dict if key != 'data'}
        kwargs_as_str = ', '.join([f'{key} = {repr(kwargs[key])}'
                                   for key in kwargs])

        return f'{self.__class__.__name__}({kwargs_as_str})'

    def __getattr__(self, name):
        """Facilitates the retrieval of properties from the series and its underlying data.
        
        The logic is:
        
          1. If the attribute exists on the series object, then return it.
          2. If ``.data`` is empty, then return :obj:`None <python:None>`.
          3. If ``.data`` contains a 
             :class:`DataPointCollection <highcharts_core.options.series.data.collections.DataPointCollection>`,
             then return the attribute from the collection.
          4. Since ``.data`` contains a list of data points, return an iterable
             containing the attribute from each data point. If NumPy is available,
             return this iterable as a NumPy :class:`ndarray <numpy:numpy.ndarray>`.

        :param name: The name of the attribute to retrieve.
        :type name: :class:`str <python:str>`
        
        :returns: The value of the attribute.
        
        :raises AttributeError: If ``name`` is not a valid attribute of the data point
          class or the instance.
        """
        try:
            return super().__getattribute__(name)
        except AttributeError as error:
            if name in ['__iter__', '__next__', 'requires_js_object']:
                raise error
            pass

        if not self.data:
            raise AttributeError(name)

        if isinstance(self.data, DataPointCollection):
            return getattr(self.data, name)

        results = [getattr(x, name) for x in self.data]

        if HAS_NUMPY:
            results = np.asarray(results)

        return results
    
    def __setattr__(self, name, value):
        """Updates the series attribute, or descendent attributes on the ``.data`` 
        properties.
        """
        try:
            super().__setattr__(name, value)
            return
        except AttributeError:
            pass
        
        collection_cls = self._data_collection_class()
        data_point_cls = self._data_point_class()
        
        if not utility_functions.is_ndarray(self.data) and not self.data:
            if HAS_NUMPY:
                collection = collection_cls()
                setattr(collection, name, value)
                self.data = collection
            elif checkers.is_iterable(value, forbid_literals = (str, 
                                                                bytes, 
                                                                dict, 
                                                                UserDict)):
                collection = collection_cls()
                setattr(collection, name, value)
                self.data = collection
            else:
                data_point = data_point_cls(name = value)
                self._data = [data_point]
        elif not self.data:
            collection = collection_cls()
            setattr(collection, name, value)
            self.data = collection
        elif checkers.is_type(self.data, 'DataPointCollection'):
            setattr(self.data, name, value)
        else:
            if not checkers.is_iterable(value, forbid_literals = (str,
                                                                  bytes,
                                                                  dict,
                                                                  UserDict)):
                value = [value for x in self.data]
            
            if len(self.data) > len(value):
                value = value + [None for x in range(len(self.data) - len(value))]
            elif len(self.data) < len(value):
                self.data = self.data + [data_point_cls() 
                                         for x in range(len(value) - len(self.data))]
            
            for index in range(len(self.data)):
                setattr(self.data[index], name, value[index])

    @classmethod
    def _data_collection_class(cls):
        """Returns the class object used for the data collection.
        
        :rtype: :class:`DataPointCollection <highcharts_core.options.series.data.collections.DataPointCollection>`
          descendent
        """
        return DataPointCollection
    
    @classmethod
    def _data_point_class(cls):
        """Returns the class object used for individual data points.
        
        :rtype: :class:`DataBase <highcharts_core.options.series.data.base.DataBase>` 
          descendent
        """
        return DataBase

    @property
    def _dot_path(self) -> Optional[str]:
        """The dot-notation path to the options key for the current class.

        :rtype: :class:`str <python:str>` or :obj:`None <python:None>`
        """
        return f'series.{self.type}'

    @property
    def data(self) -> Optional[List[DataBase] | DataPointCollection]:
        """The collection of data points for the series. Defaults to
        :obj:`None <python:None>`.

        :rtype: :class:`DataBase` or 
          :class:`DataPointCollection <highcharts_core.options.series.data.collections.DataPointCollection>` 
          or :obj:`None <python:None>`
        """
        return self._data

    @data.setter
    def data(self, value):
        if not utility_functions.is_ndarray(value) and not value:
            self._data = None
        else:
            self._data = self._data_point_class().from_array(value)
              
    @property
    def id(self) -> Optional[str]:
        """An id for the series. Defaults to :obj:`None <python:None>`.

        .. hint::

          This can be used (in JavaScript) after render time to get a pointer to the
          series object through ``chart.get()``.

        :rtype: :class:`str <python:str>` or :obj:`None <python:None>`
        """
        return self._id

    @id.setter
    def id(self, value):
        self._id = validators.string(value, allow_empty = True)

    @property
    def index(self) -> Optional[int]:
        """The index for the series in the chart, affecting the internal index in the
        (JavaScript) ``chart.series`` array, the visible Z-index, and the order of the
        series in the legend. Defaults to :obj:`None <python:None>`.

        :rtype: :class:`int <python:int>` or :obj:`None <python:None>`
        """
        return self._index

    @index.setter
    def index(self, value):
        self._index = validators.integer(value,
                                         allow_empty = True,
                                         minimum = 0)

    @property
    def legend_index(self) -> Optional[int]:
        """The sequential index for the series in the legend. Defaults to
        :obj:`None <python:None>`.

        :rtype: :class:`int <python:int>` or :obj:`None <python:None>`
        """
        return self._legend_index

    @legend_index.setter
    def legend_index(self, value):
        self._legend_index = validators.integer(value,
                                                allow_empty = True,
                                                minimum = 0)

    @property
    def name(self) -> Optional[str]:
        """The name of the series as shown in the legend, tooltip, etc. Defaults to
        :obj:`None <python:None>`.

        :rtype: :class:`str <python:str>` or :obj:`None <python:None>`
        """
        return self._name

    @name.setter
    def name(self, value):
        self._name = validators.string(value, allow_empty = True)

    @property
    def stack(self) -> Optional[str]:
        """Indicates the "stack" into which the series should be grouped, if the chart
        groups series into stacks. Defaults to :obj:`None <python:None>`.

        .. note::

          The value can be a string or a numeric value, provided that series in the same
          stack all have the same value when converted to a string. For ease of ues,
          Highcharts for Python will attempt to force the conversion of the relevant value
          to a string.

        :rtype: :class:`str <python:str>` or :obj:`None <python:None>`
        """
        return self._stack

    @stack.setter
    def stack(self, value):
        if not value:
            self._stack = None
        else:
            self._stack = validators.string(value,
                                            coerce_value = True)

    @property
    def x_axis(self) -> Optional[str | int]:
        """When using multiple X-axes, this setting determines on which axis the series
        should be drawn. Its value should be either a numerical index position in the
        :meth:`Options.x_axis` array (starting at 0), or a :class:`str <python:str>`
        indicating the :meth:`id <XAxis.id>` of the axis to which the series should be
        connected. Defaults to :obj:`None <python:None>`, which behaves as if the value
        were set to ``0``.

        :rtype: :class:`str <python:str>`, :class:`int <python:int>`, or
          :obj:`None <python:None>`
        """
        return self._x_axis

    @x_axis.setter
    def x_axis(self, value):
        if value is None:
            self._x_axis = None
        else:
            try:
                value = validators.integer(value, minimum = 0)
            except (ValueError, TypeError):
                value = validators.string(value)

            self._x_axis = value

    @property
    def y_axis(self) -> Optional[str | int]:
        """When using multiple Y-axes, this setting determines on which axis the series
        should be drawn. Its value should be either a numerical index position in the
        :meth:`Options.y_axis` array (starting at 0), or a :class:`str <python:str>`
        indicating the :meth:`id <YAxis.id>` of the axis to which the series should be
        connected. Defaults to :obj:`None <python:None>`, which behaves as if the value
        were set to ``0``.

        :rtype: :class:`str <python:str>`, :class:`int <python:int>`, or
          :obj:`None <python:None>`
        """
        return self._y_axis

    @y_axis.setter
    def y_axis(self, value):
        if value is None:
            self._y_axis = None
        else:
            try:
                value = validators.integer(value, minimum = 0)
            except (ValueError, TypeError):
                value = validators.string(value)

            self._y_axis = value

    @property
    def z_index(self) -> Optional[int | float | Decimal]:
        """The visual z-index of the series. Defaults to :obj:`None <python:None>`.

        :rtype: numeric or :obj:`None <python:None>`
        """
        return self._z_index

    @z_index.setter
    def z_index(self, value):
        if value is None:
            self._z_index = None
        else:
            self._z_index = validators.numeric(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_axis': as_dict.get('colorAxis', None),
            'color_index': as_dict.get('colorIndex', None),
            'color_key': as_dict.get('colorKey', None),
            'connect_ends': as_dict.get('connectEnds', 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),
            'drag_drop': as_dict.get('dragDrop', 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),
            '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),
            'relative_x_value': as_dict.get('relativeXValue', None),
            'shadow': as_dict.get('shadow', None),
            'soft_threshold': as_dict.get('softThreshold', None),
            'stacking': as_dict.get('stacking', None),
            'step': as_dict.get('step', None),
            'zone_axis': as_dict.get('zoneAxis', None),
            'zones': as_dict.get('zones', None),

            'data': as_dict.get('data', None),
            'id': as_dict.get('id', None),
            'index': as_dict.get('index', None),
            'legend_index': as_dict.get('legendIndex', None),
            'name': as_dict.get('name', None),
            'stack': as_dict.get('stack', None),
            'x_axis': as_dict.get('xAxis', None),
            'y_axis': as_dict.get('yAxis', None),
            'z_index': as_dict.get('zIndex', None),
        }

        return kwargs

    def _to_untrimmed_dict(self, in_cls = None) -> dict:
        untrimmed = {
            'data': self.data,
            'id': self.id,
            'index': self.index,
            'legendIndex': self.legend_index,
            'name': self.name,
            'stack': self.stack,
            'xAxis': self.x_axis,
            'yAxis': self.y_axis,
            'zIndex': self.z_index,
        }
        parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls)

        for key in parent_as_dict:
            untrimmed[key] = parent_as_dict[key]

        return untrimmed

[docs] def load_from_array(self, value): """Update the :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` property with data loaded from an iterable in ``value``. :param value: The value that should contain the data which will be converted into data point instances. .. note:: If ``value`` is not an iterable, it will be converted into an iterable to be further de-serialized correctly. :type value: iterable """ data_point_cls = self._data_point_class() self.data = data_point_cls.from_array(value)
[docs] @classmethod def from_array(cls, value, series_kwargs = None): """Create one instance of the series with ``data`` populated from ``value``. :param value: The value that should contain the data which will be converted into data point instances. .. note:: If ``value`` is not an iterable, it will be converted into an iterable to be further de-serialized correctly. :type value: iterable :param series_kwargs: Optional keyword arguments to apply when instanting the series. Defaults to :obj:`None <python:None>`. :type series_kwargs: :class:`dict <python:dict>` or :obj:`None <python:None>` :returns: An instance of the series type with ``data`` populated from the value. :rtype: :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>` descendent """ series_kwargs = validators.dict(series_kwargs, allow_empty = True) or {} data_point_cls = cls._data_point_class() data_points = data_point_cls.from_array(value) series_kwargs['data'] = data_points series = cls(**series_kwargs) return series
[docs] def load_from_csv(self, as_string_or_file, property_column_map = None, has_header_row = True, 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 = True, **kwargs): """Replace the existing :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` property with a new value populated from data in a CSV string or file. .. note:: For an example :class:`LineSeries <highcharts_core.options.series.area.LineSeries>`, the minimum code required would be: .. code-block:: python my_series = LineSeries() # EXAMPLE 1. Minimal code - will attempt to update the line series # taking x-values from the first column, and y-values from # the second column. If there are too many columns in the CSV, # will throw an error. my_series = my_series.from_csv('some-csv-file.csv') # EXAMPLE 2. More precise code - will attempt to update the line series # mapping columns in the CSV file to properties on the series # instance. my_series = my_series.from_csv('some-csv-file.csv', property_column_map = { 'x': 0, 'y': 3, 'id': 'id' }) # EXAMPLE 3. More precise code - will update the line series # using a specific series generated from the CSV file. my_series = my_series.from_csv('some-csv-file.csv', series_index = 2) As the example above shows, data is loaded into the ``my_series`` instance from the CSV file with a filename ``some-csv-file.csv``. As shown in EXAMPLE 1, unless otherwise specified, the :meth:`.x <CartesianData.x>` values for each data point will be taken from the first (index 0) column in the CSV file, while the :meth:`.y <CartesianData.y>` values will be taken from the second column. If the CSV has more than 2 columns, then this will throw an :exc:`HighchartsCSVDeserializationError` because the function is not certain which columns to use to update the series. If this happens, you can either: #. As shown in EXAMPLE 2, precisely specify which columns to use by providing a ``property_column_map`` argument. In EXAMPLE 2, the :meth:`.x <CartesianData.x>` values for each data point will be taken from the first (index 0) column in the CSV file. The :meth:`.y <CartesianData.y>` values will be taken from the fourth (index 3) column in the CSV file. And the :meth:`.id <CartesianData.id>` values will be taken from a column whose header row is labeled ``'id'`` (regardless of its index). #. Supply a ``series_index`` argument, which indicates which of the series generated from the CSV file should be used to update the instance. :param as_string_or_file: The CSV data to use to pouplate data. Accepts either the raw CSV data as a :class:`str <python:str>` or a path to a file in the runtime environment that contains the CSV data. .. tip:: Unwrapped empty column values are automatically interpreted as null (:obj:`None <python:None>`). :type as_string_or_file: :class:`str <python:str>` or Path-like :param property_column_map: An optional :class:`dict <python:dict>` used to indicate which data point property should be set to which CSV column. The keys in the :class:`dict <python:dict>` should correspond to properties in the data point class, while the value can either be a numerical index (starting with 0) or a :class:`str <python:str>` indicating the label for the CSV column. Defaults to :obj:`None <python:None>`. .. warning:: If the ``property_column_map`` uses :class:`str <python:str>` values, the CSV file *must* have a header row (this is expected, by default). If there is no header row and a :class:`str <python:str>` value is found, a :exc:`HighchartsCSVDeserializationError` will be raised. :type property_column_map: :class:`dict <python:dict>` or :obj:`None <python:None>` :param has_header_row: If ``True``, indicates that the first row of ``as_string_or_file`` contains column labels, rather than actual data. Defaults to ``True``. :type has_header_row: :class:`bool <python:bool>` :param delimiter: The delimiter used between columns. Defaults to ``,``. :type delimiter: :class:`str <python:str>` :param wrapper_character: The string used to wrap string values when wrapping is applied. Defaults to ``'``. :type wrapper_character: :class:`str <python:str>` :param null_text: The string used to indicate an empty value if empty values are wrapped. Defaults to `None`. :type null_text: :class:`str <python:str>` :param line_terminator: The string used to indicate the end of a line/record in the CSV data. Defaults to ``'\\r\\n'``. :type line_terminator: :class:`str <python:str>` :param line_terminator: The string used to indicate the end of a line/record in the CSV data. Defaults to ``'\\r\\n'``. .. note:: The Python :mod:`csv <python:csv>` currently ignores the ``line_terminator`` parameter and always applies ``'\\r\\n'``, by design. The Python docs say this may change in the future, so for future backwards compatibility we are including it here. :type line_terminator: :class:`str <python:str>` :param wrap_all_strings: If ``True``, indicates that the CSV file has all string data values wrapped in quotation marks. Defaults to ``False``. .. warning:: If set to ``True``, the :mod:`csv <python:csv>` module will try to coerce any value that is *not* wrapped in quotation marks to a :class:`float <python:float>`. This can cause unexpected behavior, and typically we recommend leaving this as ``False`` and then re-casting values after they have been parsed. :type wrap_all_strings: :class:`bool <python:bool>` :param double_wrapper_character_when_nested: If ``True``, quote character is doubled when appearing within a string value. If ``False``, the ``escape_character`` is used to prefix quotation marks. Defaults to ``False``. :type double_wrapper_character_when_nested: :class:`bool <python:bool>` :param escape_character: A one-character string that indicates the character used to escape quotation marks if they appear within a string value that is already wrapped in quotation marks. Defaults to ``\\`` (which is Python for ``'\'``, which is Python's native escape character). :type escape_character: :class:`str <python:str>` :param series_in_rows: if ``True``, will attempt a streamlined cartesian series with x-values taken from column names, y-values taken from row values, and the series name taken from the row index. Defaults to :obj:`False <python:False>`. :type series_in_rows: :class:`bool <python:bool>` :param series_index: if :obj:`None <python:None>`, will raise a :exc:`HighchartsCSVDeserializationError <highcharts_core.errors.HighchartsCSVDeserializationError>` if the CSV data contains more than one series and no ``property_column_map`` is provided. Otherwise, will update the instance with the series found in the CSV at the ``series_index`` value. Defaults to :obj:`None <python:None>`. :type series_index: :class:`int <python:int>` or :obj:`None <python:None>` :param **kwargs: Remaining keyword arguments will be attempted on the resulting :term:`series` instance and the data points it contains. :raises HighchartsCSVDeserializationError: if ``property_column_map`` references CSV columns by their label, but the CSV data does not contain a header row """ cls = self.__class__ new_instance = cls.from_csv( as_string_or_file, property_column_map = property_column_map, has_header_row = has_header_row, 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 = series_in_rows, series_index = series_index, **kwargs ) if series_index is None and isinstance(new_instance, list): raise errors.HighchartsCSVDeserializationError( f'Expected data for a single series, but got {len(new_instance)} when ' f'loading from CSV. Please either modify the structure of your CSV ' f'or provide more targeted instructions using the property_column_map ' f'argument.' ) elif isinstance(new_instance, list): new_instance = new_instance[series_index] self.data = new_instance.data
@classmethod def _from_csv_multi_map(cls, as_string_or_file, property_column_map = None, has_header_row = True, series_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, **kwargs): """Replace the existing :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` property with a new value populated from data in a CSV string or file. .. note:: For an example :class:`LineSeries <highcharts_core.options.series.area.LineSeries>`, the minimum code required would be: .. code-block:: python my_series = LineSeries() my_series = my_series.from_csv('some-csv-file.csv', property_column_map = { 'x': 0, 'y': 3, 'id': 'id' }) As the example above shows, data is loaded into the ``my_series`` instance from the CSV file with a filename ``some-csv-file.csv``. The :meth:`x <CartesianData.x>` values for each data point will be taken from the first (index 0) column in the CSV file. The :meth:`y <CartesianData.y>` values will be taken from the fourth (index 3) column in the CSV file. And the :meth:`id <CartesianData.id>` values will be taken from a column whose header row is labeled ``'id'`` (regardless of its index). :param as_string_or_file: The CSV data to use to pouplate data. Accepts either the raw CSV data as a :class:`str <python:str>` or a path to a file in the runtime environment that contains the CSV data. .. tip:: Unwrapped empty column values are automatically interpreted as null (:obj:`None <python:None>`). :type as_string_or_file: :class:`str <python:str>` or Path-like :param property_column_map: A :class:`dict <python:dict>` used to indicate which data point property should be set to which CSV column. The keys in the :class:`dict <python:dict>` should correspond to properties in the data point class, while the value can either be a numerical index (starting with 0) or a :class:`str <python:str>` indicating the label for the CSV column. Defaults to :obj:`None <python:None>`. .. warning:: If the ``property_column_map`` uses :class:`str <python:str>` values, the CSV file *must* have a header row (this is expected, by default). If there is no header row and a :class:`str <python:str>` value is found, a :exc:`HighchartsCSVDeserializationError` will be raised. :type property_column_map: :class:`dict <python:dict>` or :obj:`None <python:None>` :param has_header_row: If ``True``, indicates that the first row of ``as_string_or_file`` contains column labels, rather than actual data. Defaults to ``True``. :type has_header_row: :class:`bool <python:bool>` :param series_kwargs: An optional :class:`dict <python:dict>` containing keyword arguments that should be used when instantiating the series instance. Defaults to :obj:`None <python:None>`. .. warning:: If ``series_kwargs`` contains a ``data`` key, its value will be *overwritten*. The ``data`` value will be created from the CSV file instead. :type series_kwargs: :class:`dict <python:dict>` :param delimiter: The delimiter used between columns. Defaults to ``,``. :type delimiter: :class:`str <python:str>` :param wrapper_character: The string used to wrap string values when wrapping is applied. Defaults to ``'``. :type wrapper_character: :class:`str <python:str>` :param null_text: The string used to indicate an empty value if empty values are wrapped. Defaults to `None`. :type null_text: :class:`str <python:str>` :param line_terminator: The string used to indicate the end of a line/record in the CSV data. Defaults to ``'\\r\\n'``. :type line_terminator: :class:`str <python:str>` :param line_terminator: The string used to indicate the end of a line/record in the CSV data. Defaults to ``'\\r\\n'``. .. note:: The Python :mod:`csv <python:csv>` currently ignores the ``line_terminator`` parameter and always applies ``'\\r\\n'``, by design. The Python docs say this may change in the future, so for future backwards compatibility we are including it here. :type line_terminator: :class:`str <python:str>` :param wrap_all_strings: If ``True``, indicates that the CSV file has all string data values wrapped in quotation marks. Defaults to ``False``. .. warning:: If set to ``True``, the :mod:`csv <python:csv>` module will try to coerce any value that is *not* wrapped in quotation marks to a :class:`float <python:float>`. This can cause unexpected behavior, and typically we recommend leaving this as ``False`` and then re-casting values after they have been parsed. :type wrap_all_strings: :class:`bool <python:bool>` :param double_wrapper_character_when_nested: If ``True``, quote character is doubled when appearing within a string value. If ``False``, the ``escape_character`` is used to prefix quotation marks. Defaults to ``False``. :type double_wrapper_character_when_nested: :class:`bool <python:bool>` :param escape_character: A one-character string that indicates the character used to escape quotation marks if they appear within a string value that is already wrapped in quotation marks. Defaults to ``\\`` (which is Python for ``'\'``, which is Python's native escape character). :type escape_character: :class:`str <python:str>` :param series_in_rows: if ``True``, will attempt a streamlined cartesian series with x-values taken from column names, y-values taken from row values, and the series name taken from the row index. Defaults to :obj:`False <python:False>`. :type series_in_rows: :class:`bool <python:bool>` :param **kwargs: Remaining keyword arguments will be attempted on the resulting :term:`series` instance and the data points it contains. :raises HighchartsCSVDeserializationError: if ``property_column_map`` references CSV columns by their label, but the CSV data does not contain a header row """ try: as_string_or_file = as_string_or_file.strip() except AttributeError: pass property_column_map = validators.dict(property_column_map, allow_empty = True) or {} cleaned_column_map = {} for key in property_column_map: map_value = property_column_map.get(key, None) if map_value is None: continue if not isinstance(map_value, int) and not has_header_row: raise errors.HighchartsCSVDeserializationError(f'The supplied CSV ' f'data does not have a' f'header row, but the ' f'property_column_map ' f'did not supply an ' f'index. Received: ' f'column name ' f'"{map_value}" ' f'instead.') cleaned_column_map[key] = map_value if not checkers.is_on_filesystem(as_string_or_file): as_str = as_string_or_file columns, csv_records = utility_functions.parse_csv( as_str, has_header_row = has_header_row, delimiter = delimiter, null_text = null_text, wrapper_character = wrapper_character, line_terminator = line_terminator, wrap_all_strings = False, double_wrapper_character_when_nested = False, escape_character = "\\" ) else: with open(as_string_or_file, 'r', newline = '') as file_: columns, csv_records = utility_functions.parse_csv( file_, has_header_row = has_header_row, delimiter = delimiter, null_text = null_text, wrapper_character = wrapper_character, line_terminator = line_terminator, wrap_all_strings = False, double_wrapper_character_when_nested = False, escape_character = "\\" ) fixed_values = {} iterable_values = {} number_of_series = 1 mismatched_series = {} names = [] for key in cleaned_column_map: map_value = cleaned_column_map[key] is_iterable = not isinstance(map_value, (str, bytes, dict, UserDict)) and \ hasattr(map_value, '__iter__') if is_iterable: for item in map_value: if item not in columns: raise errors.HighchartsCSVDeserializationError( f'property_column_map is looking for a column labeled ' f'"{item}", but no corresponding column was found.' ) implied_series = len(map_value) if number_of_series == 1 and implied_series > number_of_series: number_of_series = implied_series elif implied_series != number_of_series: mismatched_series[key] = implied_series iterable_values[key] = map_value if key == 'y': name_list = [x if isinstance(x, str) else columns[x] for x in map_value] names.extend(name_list) else: if isinstance(map_value, str) and map_value not in columns: raise errors.HighchartsCSVDeserializationError( f'property_column_map is looking for a column labeled ' f'"{map_value}", but no corresponding column was found.' ) elif map_value not in columns and checkers.is_integer( map_value, coerce_value = True ) and int(map_value) > len(columns): raise errors.HighchartsCSVDeserializationError( f'property_column_map is looking for a column at index ' f'{map_value}, but no corresponding column was found.' ) fixed_values[key] = map_value if key == 'y': if isinstance(map_value, str): names.append(map_value) else: names.append(columns[map_value]) if mismatched_series: raise errors.HighchartsCSVDeserializationError( f'Unable to create series from CSV. The property map implied ' f'multiple series were needed, but properties had mismatched ' f'number of values:\n{mismatched_series}' ) collections = [] for index in range(number_of_series): collection_cls = cls._data_collection_class() collection_instance = collection_cls() for key in iterable_values: iterable_value = iterable_values[key][index] prop_array = [x.get(iterable_value, None) for x in csv_records] for i, value in enumerate(prop_array): if value and isinstance(value, str) and ',' in value: test_value = value.replace(',', '') if checkers.is_numeric(test_value): value = test_value prop_array[i] = value setattr(collection_instance, key, prop_array) for key in fixed_values: fixed_value = fixed_values[key] prop_array = [x.get(fixed_value, None) for x in csv_records] for i, value in enumerate(prop_array): if value and isinstance(value, str) and ',' in value: test_value = value.replace(',', '') if checkers.is_numeric(test_value): value = test_value prop_array[i] = value setattr(collection_instance, key, prop_array) getattr(collection_instance, key, None) collections.append(collection_instance) series_list = [] for index in range(number_of_series): series_kwargs['data'] = collections[index] series_instance = cls(**series_kwargs) try: series_instance.name = names[index] except IndexError: pass for key in kwargs: if key not in series_kwargs and key not in cleaned_column_map: setattr(series_instance, key, kwargs[key]) series_list.append(series_instance) return series_list
[docs] @classmethod def from_csv(cls, as_string_or_file, property_column_map = None, has_header_row = True, series_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 one or more new :term:`series` instances with :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` populated from data in a CSV string or file. .. note:: For an example :class:`LineSeries <highcharts_core.options.series.area.LineSeries>`, the minimum code required would be: .. code-block:: python # Create one or more LineSeries instances from the CSV file "some-csv-file.csv". # EXAMPLE 1. The minimum code to produce one series for each # column in the CSV file (excluding the first column): my_series = LineSeries.from_csv('some-csv-file.csv') # EXAMPLE 2. Produces ONE series with more precise configuration: my_series = LineSeries.from_csv('some-csv-file.csv', property_column_map = { 'x': 0, 'y': 3, 'id': 'id' }) # EXAMPLE 3. Produces THREE series instances with # more precise configuration: my_series = LineSeries.from_csv('some-csv-file.csv', property_column_map = { 'x': 0, 'y': [3, 5, 8], 'id': 'id' }) As the example above shows, data is loaded into the ``my_series`` instance from the CSV file with a filename ``some-csv-file.csv``. The :meth:`x <CartesianData.x>` values for each data point will be taken from the first (index 0) column in the CSV file. The :meth:`y <CartesianData.y>` values will be taken from the fourth (index 3) column in the CSV file. And the :meth:`id <CartesianData.id>` values will be taken from a column whose header row is labeled ``'id'`` (regardless of its index). :param as_string_or_file: The CSV data to use to pouplate data. Accepts either the raw CSV data as a :class:`str <python:str>` or a path to a file in the runtime environment that contains the CSV data. .. tip:: Unwrapped empty column values are automatically interpreted as null (:obj:`None <python:None>`). :type as_string_or_file: :class:`str <python:str>` or Path-like :param property_column_map: A :class:`dict <python:dict>` used to indicate which data point property should be set to which CSV column. The keys in the :class:`dict <python:dict>` should correspond to properties in the data point class, while the value can either be a numerical index (starting with 0) or a :class:`str <python:str>` indicating the label for the CSV column. Defaults to :obj:`None <python:None>`. .. note:: If any of the values in ``property_column_map`` contain an iterable, then one series will be produced for each item in the iterable. For example, the following: .. code-block:: python { 'x': 0, 'y': [3, 5, 8] } will return *three* series, each of which will have its :meth:`.x <CartesianData.x>` value populated from the first column (index 0), and whose :meth:`.y <CartesianData.y>` values will be populated from the fourth, sixth, and ninth columns (indices 3, 5, and 8), respectively. .. warning:: If the ``property_column_map`` uses :class:`str <python:str>` values, the CSV file *must* have a header row (this is expected, by default). If there is no header row and a :class:`str <python:str>` value is found, a :exc:`HighchartsCSVDeserializationError` will be raised. :type property_column_map: :class:`dict <python:dict>` or :obj:`None <python:None>` :param has_header_row: If ``True``, indicates that the first row of ``as_string_or_file`` contains column labels, rather than actual data. Defaults to ``True``. :type has_header_row: :class:`bool <python:bool>` :param series_kwargs: An optional :class:`dict <python:dict>` containing keyword arguments that should be used when instantiating the series instance. Defaults to :obj:`None <python:None>`. .. warning:: If ``series_kwargs`` contains a ``data`` key, its value will be *overwritten*. The ``data`` value will be created from the CSV file instead. :type series_kwargs: :class:`dict <python:dict>` :param delimiter: The delimiter used between columns. Defaults to ``,``. :type delimiter: :class:`str <python:str>` :param wrapper_character: The string used to wrap string values when wrapping is applied. Defaults to ``'``. :type wrapper_character: :class:`str <python:str>` :param null_text: The string used to indicate an empty value if empty values are wrapped. Defaults to `None`. :type null_text: :class:`str <python:str>` :param line_terminator: The string used to indicate the end of a line/record in the CSV data. Defaults to ``'\\r\\n'``. :type line_terminator: :class:`str <python:str>` :param line_terminator: The string used to indicate the end of a line/record in the CSV data. Defaults to ``'\\r\\n'``. .. note:: The Python :mod:`csv <python:csv>` currently ignores the ``line_terminator`` parameter and always applies ``'\\r\\n'``, by design. The Python docs say this may change in the future, so for future backwards compatibility we are including it here. :type line_terminator: :class:`str <python:str>` :param wrap_all_strings: If ``True``, indicates that the CSV file has all string data values wrapped in quotation marks. Defaults to ``False``. .. warning:: If set to ``True``, the :mod:`csv <python:csv>` module will try to coerce any value that is *not* wrapped in quotation marks to a :class:`float <python:float>`. This can cause unexpected behavior, and typically we recommend leaving this as ``False`` and then re-casting values after they have been parsed. :type wrap_all_strings: :class:`bool <python:bool>` :param double_wrapper_character_when_nested: If ``True``, quote character is doubled when appearing within a string value. If ``False``, the ``escape_character`` is used to prefix quotation marks. Defaults to ``False``. :type double_wrapper_character_when_nested: :class:`bool <python:bool>` :param escape_character: A one-character string that indicates the character used to escape quotation marks if they appear within a string value that is already wrapped in quotation marks. Defaults to ``\\\\`` (which is Python for ``'\\'``, which is Python's native escape character). :type escape_character: :class:`str <python:str>` :param series_in_rows: if ``True``, will attempt a streamlined cartesian series with x-values taken from column names, y-values taken from row values, and the series name taken from the row index. Defaults to ``False``. :obj:`False <python:False>`. :type series_in_rows: :class:`bool <python:bool>` :param series_index: If supplied, return the series that Highcharts for Python generated from the CSV at the ``series_index`` position. Defaults to :obj:`None <python:None>`, which returns all series generated from the CSV. :type series_index: :class:`int <python:int>`, slice, or :obj:`None <python:None>` :param **kwargs: Remaining keyword arguments will be attempted on the resulting :term:`series` instance and the data points it contains. :returns: A :term:`series` instance (descended from :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`) OR :class:`list <python:list>` of series instances with its :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` property populated from the data in ``df``. :rtype: :class:`list <python:list>` of series instances (descended from :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`) or :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`-descendent :raises HighchartsCSVDeserializationError: if ``property_column_map`` references CSV columns by their label, but the CSV data does not contain a header row """ series_kwargs = validators.dict(series_kwargs, allow_empty = True) or {} if series_in_rows: return cls.from_csv_in_rows( as_string_or_file, has_header_row = has_header_row, 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_index = series_index, **kwargs ) # SCENARIO 1: Has Property Map if property_column_map: series_list = cls._from_csv_multi_map( 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 len(series_list) == 1: return series_list[0] return series_list # SCENARIO 2: Properties in KWARGS collection_cls = cls._data_collection_class() data_point_cls = cls._data_point_class() props_from_array = data_point_cls._get_props_from_array() if not props_from_array: props_from_array = ['x', 'y'] property_map = {} for prop in props_from_array: if prop in kwargs: property_map[prop] = kwargs[prop] if property_map: series_list = cls._from_csv_multi_map( as_string_or_file, property_column_map = property_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 ) for index in range(len(series_list)): for key in kwargs: if key not in props_from_array and key not in series_kwargs: setattr(series_list[index], key, kwargs[key]) if len(series_list) == 1: return series_list[0] if series_index is not None: return series_list[index] return series_list # SCENARIO 3: No Explicit Properties if not checkers.is_on_filesystem(as_string_or_file): as_str = as_string_or_file columns, csv_records = utility_functions.parse_csv( as_str, has_header_row = has_header_row, delimiter = delimiter, null_text = null_text, wrapper_character = wrapper_character, line_terminator = line_terminator, wrap_all_strings = False, double_wrapper_character_when_nested = False, escape_character = "\\" ) else: with open(as_string_or_file, 'r', newline = '') as file_: columns, csv_records = utility_functions.parse_csv( file_, has_header_row = has_header_row, delimiter = delimiter, null_text = null_text, wrapper_character = wrapper_character, line_terminator = line_terminator, wrap_all_strings = False, double_wrapper_character_when_nested = False, escape_character = "\\" ) try: series_idx = kwargs.get('index', columns[0]) except IndexError: series_idx = kwargs.get('index', 0) column_count = len(columns) if not columns: column_count = len(csv_records[0]) supported_dimensions = collection_cls._get_supported_dimensions() # SCENARIO 3a: Single Series, Data Frame Columns align exactly to Data Point Properties if column_count in supported_dimensions: property_map = {} props_from_array = data_point_cls._get_props_from_array(length = column_count) if not props_from_array: props_from_array = ['x', 'y'] property_map[props_from_array[0]] = [x.get(series_idx, None) for x in csv_records] for index, prop in enumerate(props_from_array[1:]): if series_idx is not None: prop_array = [x.get(columns[index + 1], index + 1) for x in csv_records] else: prop_array = [x.get(columns[index], index) for x in csv_records] for i, value in enumerate(prop_array): if value and isinstance(value, str) and ',' in value: test_value = value.replace(',', '') if checkers.is_numeric(test_value): value = test_value prop_array[i] = value property_map[prop] = prop_array collection = collection_cls() for key in property_map: setattr(collection, key, property_map[key]) series_kwargs['data'] = collection series_instance = cls(**series_kwargs) for key in kwargs: if key not in series_kwargs and key not in property_map: setattr(series_instance, key, kwargs[key]) return series_instance # SCENARIO 3b: Multiple Series, Data Frame Columns correspond to multiples of Data Point Properties reversed_dimensions = sorted(supported_dimensions, reverse = True) columns_per_series = None if reversed_dimensions: for dimension in reversed_dimensions: if series_idx is not None and dimension > 1 and column_count % (dimension - 1) == 0: if dimension > 2 and props_from_array[-1] == 'name': columns_per_series = dimension - 2 else: columns_per_series = dimension - 1 break if dimension > 1 and column_count % dimension == 0: columns_per_series = dimension break elif dimension == 1: columns_per_series = 1 if not columns_per_series: raise errors.HighchartsCSVDeserializationError( f'Could not determine how to deserialize CSV with {column_count}' f' columns into a {collection_cls.__name__} instance. Please supply ' f'more precise instructions using property_column_map or ' f'by explicitly specificying data property kwargs.' ) series_count = column_count // columns_per_series if columns_per_series == 1 and series_idx: series_count -= 1 series_list = [] for index in range(series_count): start = 1 + (len(series_list) * columns_per_series) property_map = {} if series_idx is not None: expected_length = columns_per_series + 1 else: expected_length = columns_per_series props_from_array = data_point_cls._get_props_from_array(length = expected_length) if not props_from_array: props_from_array = ['x', 'y'] property_map[props_from_array[0]] = [x.get(series_idx, None) for x in csv_records] has_implicit_series_name = 'name' not in kwargs and 'name' not in series_kwargs if has_implicit_series_name: try: series_name = columns[start] except (IndexError, TypeError): series_name = None else: series_name = series_kwargs.get('name', None) or kwargs.get('name', None) props_from_array = props_from_array[1:] for idx, prop in enumerate(props_from_array): index = start + idx prop_array = [x.get(columns[index], idx) for x in csv_records] property_map[prop] = prop_array collection = collection_cls() for key in property_map: try: setattr(collection, key, property_map[key]) except ValueError as error: if key not in ['x', 'name'] and 'name' not in property_map: setattr(collection, 'name', property_map[key]) else: raise error series_kwargs['data'] = collection series_instance = cls(**series_kwargs) for key in kwargs: if key not in series_kwargs and key not in property_map: setattr(series_instance, key, kwargs[key]) if 'name' not in series_kwargs and 'name' not in kwargs: series_instance.name = series_name series_list.append(series_instance) if series_index is not None: return series_list[series_index] return series_list
[docs] @classmethod def from_csv_in_rows(cls, as_string_or_file, has_header_row = True, series_kwargs = None, delimiter = ',', null_text = 'None', wrapper_character = "'", line_terminator = '\r\n', wrap_all_strings = False, double_wrapper_character_when_nested = False, escape_character = "\\", **kwargs): """Create a new :term:`series` instance with a :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` property populated from data in a CSV string or file. .. note:: For an example :class:`LineSeries <highcharts_core.options.series.area.LineSeries>`, the minimum code required would be: .. code-block:: python my_series = LineSeries.from_csv_in_rows('some-csv-file.csv') :param as_string_or_file: The CSV data to use to pouplate data. Accepts either the raw CSV data as a :class:`str <python:str>` or a path to a file in the runtime environment that contains the CSV data. .. tip:: Unwrapped empty column values are automatically interpreted as null (:obj:`None <python:None>`). :type as_string_or_file: :class:`str <python:str>` or Path-like :param has_header_row: If ``True``, indicates that the first row of ``as_string_or_file`` contains column labels, rather than actual data. Defaults to ``True``. :type has_header_row: :class:`bool <python:bool>` :param series_kwargs: An optional :class:`dict <python:dict>` containing keyword arguments that should be used when instantiating the series instance. Defaults to :obj:`None <python:None>`. .. warning:: If ``series_kwargs`` contains a ``data`` key, its value will be *overwritten*. The ``data`` value will be created from the CSV file instead. :type series_kwargs: :class:`dict <python:dict>` :param delimiter: The delimiter used between columns. Defaults to ``,``. :type delimiter: :class:`str <python:str>` :param wrapper_character: The string used to wrap string values when wrapping is applied. Defaults to ``'``. :type wrapper_character: :class:`str <python:str>` :param null_text: The string used to indicate an empty value if empty values are wrapped. Defaults to `None`. :type null_text: :class:`str <python:str>` :param line_terminator: The string used to indicate the end of a line/record in the CSV data. Defaults to ``'\\r\\n'``. :type line_terminator: :class:`str <python:str>` :param line_terminator: The string used to indicate the end of a line/record in the CSV data. Defaults to ``'\\r\\n'``. .. note:: The Python :mod:`csv <python:csv>` currently ignores the ``line_terminator`` parameter and always applies ``'\\r\\n'``, by design. The Python docs say this may change in the future, so for future backwards compatibility we are including it here. :type line_terminator: :class:`str <python:str>` :param wrap_all_strings: If ``True``, indicates that the CSV file has all string data values wrapped in quotation marks. Defaults to ``False``. .. warning:: If set to ``True``, the :mod:`csv <python:csv>` module will try to coerce any value that is *not* wrapped in quotation marks to a :class:`float <python:float>`. This can cause unexpected behavior, and typically we recommend leaving this as ``False`` and then re-casting values after they have been parsed. :type wrap_all_strings: :class:`bool <python:bool>` :param double_wrapper_character_when_nested: If ``True``, quote character is doubled when appearing within a string value. If ``False``, the ``escape_character`` is used to prefix quotation marks. Defaults to ``False``. :type double_wrapper_character_when_nested: :class:`bool <python:bool>` :param escape_character: A one-character string that indicates the character used to escape quotation marks if they appear within a string value that is already wrapped in quotation marks. Defaults to ``\\\\`` (which is Python for ``'\\'``, which is Python's native escape character). :type escape_character: :class:`str <python:str>` :param **kwargs: Remaining keyword arguments will be attempted on the resulting :term:`series` instance and the data points it contains. :returns: A :term:`series` instance (descended from :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`) OR :class:`list <python:list>` of series instances with its :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` property populated from the data in ``df``. :rtype: :class:`list <python:list>` of series instances (descended from :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`) or :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`-descendent :raises HighchartsCSVDeserializationError: if ``property_column_map`` references CSV columns by their label, but the CSV data does not contain a header row """ series_kwargs = validators.dict(series_kwargs, allow_empty = True) or {} if not checkers.is_on_filesystem(as_string_or_file): as_str = as_string_or_file columns, csv_records = utility_functions.parse_csv( as_str, has_header_row = has_header_row, delimiter = delimiter, null_text = null_text, wrapper_character = wrapper_character, line_terminator = line_terminator, wrap_all_strings = False, double_wrapper_character_when_nested = False, escape_character = "\\" ) else: with open(as_string_or_file, 'r', newline = '') as file_: columns, csv_records = utility_functions.parse_csv( file_, has_header_row = has_header_row, delimiter = delimiter, null_text = null_text, wrapper_character = wrapper_character, line_terminator = line_terminator, wrap_all_strings = False, double_wrapper_character_when_nested = False, escape_character = "\\" ) collection_cls = cls._data_collection_class() supported_dimensions = collection_cls._get_supported_dimensions() if 2 not in supported_dimensions: raise errors.HighchartsPandasDeserializationError( f'Unable to create a collection of {cls.__name__} instances ' f'from CSV using a 2-dimensional array because {cls.__name__} does ' f'not support 2-dimensional arrays as inputs. Please use a ' f'different series type, or transpose the CSV to a columnar structure ' f'and supply a column_property_map for greater precision.' ) data_properties = collection_cls._get_props_from_array() if columns: x_values = columns[1:] else: x_values = [x for x in range(len(csv_records[0].keys()) - 1)] name_key = list(csv_records[0].keys())[0] name_values = [row[name_key] for row in csv_records] series_count = len(csv_records) series_list = [] for row in range(series_count): series_name = name_values[row] y_values = [x for x in list(csv_records[row].values())[1:]] for i, value in enumerate(y_values): if value and isinstance(value, str) and ',' in value: test_value = value.replace(',', '') if checkers.is_numeric(test_value): value = test_value y_values[i] = value as_array = zip(x_values, y_values) collection = collection_cls.from_array(as_array) series_instance_kwargs = series_kwargs.copy() series_instance_kwargs['data'] = collection series_instance_kwargs['name'] = series_name series_instance = cls(**series_instance_kwargs) for key in kwargs: if key not in series_instance_kwargs and key not in data_properties: setattr(series_instance, key, kwargs[key]) series_list.append(series_instance) return series_list
[docs] def load_from_pandas(self, df, property_map = None, series_in_rows = False, series_index = None): """Replace the contents of the :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` property with data points populated from a `pandas <https://pandas.pydata.org/>`_ :class:`DataFrame <pandas:pandas.DataFrame>`. :param df: The :class:`DataFrame <pandas:pandas.DataFrame>` from which data should be loaded. :type df: :class:`DataFrame <pandas:pandas.DataFrame>` :param property_map: A :class:`dict <python:dict>` used to indicate which data point property should be set to which column in ``df``. The keys in the :class:`dict <python:dict>` should correspond to properties in the data point class, while the value should indicate the label for the :class:`DataFrame <pandas:pandas.DataFrame>` column. Defaults to :obj:`None <python:None>`. :type property_map: :class:`dict <python:dict>` or :obj:`None <python:None>` :param series_in_rows: if ``True``, will attempt a streamlined cartesian series with x-values taken from column names, y-values taken from row values, and the series name taken from the row index. Defaults to :obj:`False <python:False>`. :type series_in_rows: :class:`bool <python:bool>` :param series_index: If supplied, return the series that Highcharts for Python generated from ``df`` at the ``series_index`` value. Defaults to :obj:`None <python:None>`, which returns all series generated from ``df``. .. warning:: If :obj:`None <python:None>` and Highcharts for Python generates multiple series, then a :exc:`HighchartsPandasDeserializationError` will be raised. :type series_index: :class:`int <python:int>`, or :obj:`None <python:None>` :raises HighchartsPandasDeserializationError: if ``property_map`` references a column that does not exist in the data frame :raises HighchartsPandasDeserializationError: if ``series_index`` is :obj:`None <python:None>`, and it is ambiguous which series generated from the dataframe should be used :raises HighchartsDependencyError: if `pandas <https://pandas.pydata.org/>`_ is not available in the runtime environment """ cls = self.__class__ new_instance = cls.from_pandas(df, property_map = property_map, series_in_rows = series_in_rows) if series_index is None and isinstance(new_instance, list): raise errors.HighchartsPandasDeserializationError( f'Expected data for a single series, but got {len(new_instance)} when ' f'loading from df. Please either modify the structure of df ' f'or provide more targeted instructions using the property_map ' f'argument.' ) elif isinstance(new_instance, list): new_instance = new_instance[series_index] self.data = new_instance.data
@classmethod def _from_pandas_multi_map(cls, df, property_map, series_kwargs = None, **kwargs): """Create one or more :term:`series` instances whose :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` properties are populated from a `pandas <https://pandas.pydata.org/>`_ :class:`DataFrame <pandas:pandas.DataFrame>`, when ``property_map`` suggests there are multiple series. :param df: The :class:`DataFrame <pandas:pandas.DataFrame>` from which data should be loaded. :type df: :class:`DataFrame <pandas:pandas.DataFrame>` :param property_map: A :class:`dict <python:dict>` used to indicate which data point property should be set to which column in ``df``. The keys in the :class:`dict <python:dict>` should correspond to properties in the data point class, while the value should indicate the label for the :class:`DataFrame <pandas:pandas.DataFrame>` column. Defaults to :obj:`None <python:None>` :type property_map: :class:`dict <python:dict>` or :obj:`None <python:None>` :param series_kwargs: An optional :class:`dict <python:dict>` containing keyword arguments that should be used when instantiating the series instance. Defaults to :obj:`None <python:None>`. .. warning:: If ``series_kwargs`` contains a ``data`` key, its value will be *overwritten*. The ``data`` value will be created from ``df`` instead. :type series_kwargs: :class:`dict <python:dict>` :param **kwargs: Remaining keyword arguments will be attempted on the resulting :term:`series` instance and the data points it contains. :returns: A :term:`series` instance (descended from :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`) OR :class:`list <python:list>` of series instances with its :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` property populated from the data in ``df``. :rtype: :class:`list <python:list>` of series instances (descended from :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`) or :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`-descendent :raises HighchartsPandasDeserializationError: if ``property_map`` references a column that does not exist in the data frame :raises HighchartsDependencyError: if `pandas <https://pandas.pydata.org/>`_ is not available in the runtime environment """ series_kwargs = validators.dict(series_kwargs, allow_empty = True) or {} fixed_values = {} iterable_values = {} number_of_series = 1 mismatched_series = {} names = [] for key in property_map: map_value = property_map[key] is_iterable = not isinstance(map_value, (str, bytes, dict, UserDict)) and \ hasattr(map_value, '__iter__') if is_iterable: for item in map_value: if item not in df.columns.values: raise errors.HighchartsPandasDeserializationError( f'Unable to find a column labeled "{item}" in df.' ) implied_series = len(map_value) if number_of_series == 1 and implied_series > number_of_series: number_of_series = implied_series elif implied_series != number_of_series: mismatched_series[key] = implied_series iterable_values[key] = map_value if key == 'y': names.extend(map_value) else: if map_value not in df.columns.values: if map_value != df.index.name: raise errors.HighchartsPandasDeserializationError( f'Unable to find a column labeled "{map_value}" in df.' ) fixed_values[key] = map_value if key == 'y': names.append(map_value) if mismatched_series: raise errors.HighchartsPandasDeserializationError( f'Unable to create series from df. The property map implied ' f'multiple series were needed, but properties had mismatched ' f'number of values:\n{mismatched_series}' ) collections = [] for index in range(number_of_series): collection_cls = cls._data_collection_class() collection_instance = collection_cls() for key in iterable_values: iterable_value = iterable_values[key][index] prop_array = df[iterable_value].values setattr(collection_instance, key, prop_array) for key in fixed_values: fixed_value = fixed_values[key] try: prop_array = df[fixed_value].values except KeyError: prop_array = df.index.values setattr(collection_instance, key, prop_array) collections.append(collection_instance) series_list = [] for index in range(number_of_series): series_kwargs['data'] = collections[index] series_instance = cls(**series_kwargs) try: series_instance.name = names[index] except IndexError: pass for key in kwargs: if key not in series_kwargs and property_map: setattr(series_instance, key, kwargs[key]) series_list.append(series_instance) return series_list
[docs] @classmethod def from_pandas_in_rows(cls, df, series_kwargs = None, series_index = None, **kwargs): """Create a collection of :term:`series` instances, one for each row in ``df``. :param df: The :class:`DataFrame <pyspark:pyspark.sql.DataFrame>` from which data should be loaded. :type df: :class:`DataFrame <pyspark:pyspark.sql.DataFrame>` :param series_kwargs: An optional :class:`dict <python:dict>` containing keyword arguments that should be used when instantiating the series instance. Defaults to :obj:`None <python:None>`. .. warning:: If ``series_kwargs`` contains a ``data`` key, its value will be *overwritten*. The ``data`` value will be created from ``df`` instead. :type series_kwargs: :class:`dict <python:dict>` :param series_index: If supplied, return the series that Highcharts for Python generated from ``df`` at the ``series_index`` value. Defaults to :obj:`None <python:None>`, which returns all series generated from ``df``. :type series_index: :class:`int <python:int>`, slice, or :obj:`None <python:None>` :param **kwargs: Remaining keyword arguments will be attempted on the resulting :term:`series` instance and the data points it contains. :returns: Collection of :term:`series` instances corresponding, with one series per row in ``df``, and where: * the series x-values are populated from the column labels in ``df`` * the series name is set to the row label from ``df`` * the series y-values are populated from the values within that row in ``df`` :rtype: :class:`list <python:list>` of :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`-descendent instances """ try: from pandas import DataFrame except ImportError: raise errors.HighchartsDependencyError('pandas is not available in the ' 'runtime environment. Please install ' 'using "pip install pandas"') if not checkers.is_type(df, ('DataFrame')): raise errors.HighchartsValueError(f'df is expected to be a Pandas DataFrame.' f'Was: {df.__class__.__name__}') series_kwargs = validators.dict(series_kwargs, allow_empty = True) or {} collection_cls = cls._data_collection_class() supported_dimensions = collection_cls._get_supported_dimensions() if 2 not in supported_dimensions: raise errors.HighchartsPandasDeserializationError( f'Unable to create a collection of {cls.__name__} instances ' f'from df using a 2-dimensional array because {cls.__name__} does ' f'not support 2-dimensional arrays as inputs. Please use a ' f'different series type, or transpose df to a columnar structure ' f'and supply a property_map for greater precision.' ) data_properties = collection_cls._get_props_from_array() x_values = df.columns.values name_values = df.index.values series_count = len(df) series_list = [] for row in range(series_count): series_name = name_values[row] y_values = df.iloc[[row]].values y_values = y_values.reshape(x_values.shape) as_array = np.column_stack((x_values, y_values)) collection = collection_cls.from_array(as_array) series_instance_kwargs = series_kwargs.copy() series_instance_kwargs['data'] = collection series_instance_kwargs['name'] = series_name series_instance = cls(**series_instance_kwargs) for key in kwargs: if key not in series_instance_kwargs and key not in data_properties: setattr(series_instance, key, kwargs[key]) series_list.append(series_instance) if series_index is not None: return series_list[series_index] return series_list
[docs] @classmethod def from_pandas(cls, df, property_map = None, series_kwargs = None, series_in_rows = False, series_index = None, **kwargs): """Create one or more :term:`series` instances whose :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` properties are populated from a `pandas <https://pandas.pydata.org/>`_ :class:`DataFrame <pandas:pandas.DataFrame>`. .. code-block:: python # Given a Pandas DataFrame instance named "df" from highcharts_core.chart import Chart from highcharts_core.options.series.area import LineSeries # Creating a Series from the DataFrame ## EXAMPLE 1. Minimum code required. Creates one or more series. my_series = LineSeries.from_pandas(df) ## EXAMPLE 2. More precise configuration. Creates ONE series. my_series = LineSeries.from_pandas(df, series_index = 2) ## EXAMPLE 3. More precise configuration. Creates ONE series. my_series = LineSeries.from_pandas(df, property_map = { 'x': 'date', 'y': 'value', 'id': 'id' }) ## EXAMPLE 4. More precise configuration. Creates THREE series. my_series = LineSeries.from_pandas(df, property_map = { 'x': 'date', 'y': ['value1', 'value2', 'value3'], 'id': 'id' }) :param df: The :class:`DataFrame <pandas:pandas.DataFrame>` from which data should be loaded. :type df: :class:`DataFrame <pandas:pandas.DataFrame>` :param property_map: A :class:`dict <python:dict>` used to indicate which data point property should be set to which column in ``df``. The keys in the :class:`dict <python:dict>` should correspond to properties in the data point class, while the value should indicate the label for the :class:`DataFrame <pandas:pandas.DataFrame>` column. Defaults to :obj:`None <python:None>`. .. note:: If any of the values in ``property_map`` contain an iterable, then one series will be produced for each item in the iterable. For example, the following: .. code-block:: python { 'x': 'timestamp', 'y': ['value1', 'value2', 'value3'] } will return *three* series, each of which will have its :meth:`.x <CartesianData.x>` value populated from the column labeled ``'timestamp'``, and whose :meth:`.y <CartesianData.y>` values will be populated from the columns labeled ``'value1'``, ``'value2'``, and ``'value3'``, respectively. :type property_map: :class:`dict <python:dict>` or :obj:`None <python:None>` :param series_kwargs: An optional :class:`dict <python:dict>` containing keyword arguments that should be used when instantiating the series instance. Defaults to :obj:`None <python:None>`. .. warning:: If ``series_kwargs`` contains a ``data`` key, its value will be *overwritten*. The ``data`` value will be created from ``df`` instead. :type series_kwargs: :class:`dict <python:dict>` :param series_in_rows: if ``True``, will attempt a streamlined cartesian series with x-values taken from column names, y-values taken from row values, and the series name taken from the row index. Defaults to ``False``. :obj:`False <python:False>`. :type series_in_rows: :class:`bool <python:bool>` :param series_index: If supplied, return the series that Highcharts for Python generated from ``df`` at the ``series_index`` value. Defaults to :obj:`None <python:None>`, which returns all series generated from ``df``. :type series_index: :class:`int <python:int>`, slice, or :obj:`None <python:None>` :param **kwargs: Remaining keyword arguments will be attempted on the resulting :term:`series` instance and the data points it contains. :returns: A :term:`series` instance (descended from :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`) OR :class:`list <python:list>` of series instances with its :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` property populated from the data in ``df``. :rtype: :class:`list <python:list>` of series instances (descended from :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`) or :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`-descendent :raises HighchartsPandasDeserializationError: if ``property_map`` references a column that does not exist in the data frame :raises HighchartsDependencyError: if `pandas <https://pandas.pydata.org/>`_ is not available in the runtime environment """ series_kwargs = validators.dict(series_kwargs, allow_empty = True) or {} # SCENARIO 0: Series in Rows if series_in_rows: return cls.from_pandas_in_rows(df, series_kwargs, series_index = series_index, **kwargs) # SCENARIO 1: Has Property Map if property_map: series_list = cls._from_pandas_multi_map(df, property_map, series_kwargs, **kwargs) if len(series_list) == 1: return series_list[0] if series_index is not None: return series_list[series_index] return series_list # SCENARIO 2: Properties in KWARGS collection_cls = cls._data_collection_class() data_point_cls = cls._data_point_class() props_from_array = data_point_cls._get_props_from_array() if not props_from_array: props_from_array = ['x', 'y'] property_map = {} for prop in props_from_array: if prop in kwargs: property_map[prop] = kwargs[prop] if property_map: series_list = cls._from_pandas_multi_map(df, property_map, series_kwargs) for index in range(len(series_list)): for key in kwargs: if key not in props_from_array and key not in series_kwargs: setattr(series_list[index], key, kwargs[key]) if len(series_list) == 1: return series_list[0] if series_index is not None: return series_list[series_index] return series_list # SCENARIO 3: No Explicit Properties series_idx = kwargs.get('index', df.index) column_count = len(df.columns) supported_dimensions = collection_cls._get_supported_dimensions() # SCENARIO 3a: Single Series, Data Frame Columns align exactly to Data Point Properties if column_count in supported_dimensions: property_map = {} props_from_array = data_point_cls._get_props_from_array(length = column_count) if not props_from_array: props_from_array = ['x', 'y'] property_map[props_from_array[0]] = series_idx for index, prop in enumerate(props_from_array[1:]): prop_value = df.iloc[:, index + 1].values property_map[prop] = prop_value collection = collection_cls() for key in property_map: setattr(collection, key, property_map[key]) series_kwargs['data'] = collection series_instance = cls(**series_kwargs) for key in kwargs: if key not in series_kwargs and key not in property_map: setattr(series_instance, key, kwargs[key]) return series_instance # SCENARIO 3b: Multiple Series, Data Frame Columns correspond to multiples of Data Point Properties reversed_dimensions = sorted(supported_dimensions, reverse = True) columns_per_series = None if reversed_dimensions: for dimension in reversed_dimensions: if series_idx is not None and dimension > 1 and column_count % (dimension - 1) == 0: if dimension > 2 and props_from_array[-1] == 'name': columns_per_series = dimension - 2 else: columns_per_series = dimension - 1 break elif dimension > 1 and column_count % dimension == 0: columns_per_series = dimension break elif dimension == 1: columns_per_series = 1 if not columns_per_series: raise errors.HighchartsPandasDeserializationError( f'Could not determine how to deserialize data frame with {column_count}' f' columns into a {collection_cls.__name__} instance. Please supply ' f'more precise instructions using property_map or ' f'by explicitly specificying data property kwargs.' ) series_count = column_count // columns_per_series series_list = [] for index in range(series_count): start = len(series_list) * columns_per_series property_map = {} if series_idx is not None: expected_length = columns_per_series + 1 else: expected_length = columns_per_series props_from_array = data_point_cls._get_props_from_array(length = expected_length) if not props_from_array: props_from_array = ['x', 'y'] property_map[props_from_array[0]] = series_idx has_implicit_series_name = 'name' not in kwargs and 'name' not in series_kwargs if has_implicit_series_name: series_name = df.columns[start] else: series_name = series_kwargs.get('name', None) or kwargs.get('name', None) for index, prop in enumerate(props_from_array[1:]): index = start + index prop_value = df.iloc[:, index].values property_map[prop] = prop_value collection = collection_cls() for key in property_map: setattr(collection, key, property_map[key]) series_kwargs['data'] = collection series_kwargs['name'] = series_name series_instance = cls(**series_kwargs) for key in kwargs: if key not in series_kwargs and key not in property_map: setattr(series_instance, key, kwargs[key]) series_list.append(series_instance) if series_index is not None: return series_list[index] return series_list
[docs] def load_from_pyspark(self, df, property_map): """Replaces the contents of the :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` property with values from a `PySpark <https://spark.apache.org/docs/latest/api/python/>`_ :class:`DataFrame <pyspark:pyspark.sql.DataFrame>`. :param df: The :class:`DataFrame <pyspark:pyspark.sql.DataFrame>` from which data should be loaded. :type df: :class:`DataFrame <pyspark:pyspark.sql.DataFrame>` :param property_map: A :class:`dict <python:dict>` used to indicate which data point property should be set to which column in ``df``. The keys in the :class:`dict <python:dict>` should correspond to properties in the data point class, while the value should indicate the label for the :class:`DataFrame <pyspark:pyspark.sql.DataFrame>` column. :type property_map: :class:`dict <python:dict>` :raises HighchartsPySparkDeserializationError: if ``property_map`` references a column that does not exist in the data frame :raises HighchartsDependencyError: if `PySpark <https://spark.apache.org/docs/latest/api/python/>`_ is not available in the runtime environment """ try: from pyspark.sql import DataFrame except ImportError: raise errors.HighchartsDependencyError('pyspark is not available in the ' 'runtime environment. Please install ' 'using "pip install pyspark"') if not checkers.is_type(df, ('DataFrame')): raise errors.HighchartsValueError(f'df is expected to be a PySpark DataFrame.' f'Was: {df.__class__.__name__}') property_map = validators.dict(property_map) column_instances = [] for key in property_map: map_value = property_map[key] if map_value not in df.columns: raise errors.HighchartsPySparkDeserializationError( f'Unable to find a column labeled "{map_value}" in df.' ) column_instance = getattr(df, map_value) column_instances.append(column_instance) narrower_df = df.select(*column_instances) rdd_as_jsons = narrower_df.toJSON() df_as_dicts = [json.loads(x) for x in rdd_as_jsons.toLocalIterator()] records_as_dicts = [] for record in df_as_dicts: record_as_dict = {} for key in property_map: map_value = property_map[key] record_as_dict[key] = record.get(map_value, None) records_as_dicts.append(record_as_dict) self.data = records_as_dicts
[docs] @classmethod def from_pyspark(cls, df, property_map, series_kwargs = None): """Create a :term:`series` instance whose :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` property is populated from a `PySpark <https://spark.apache.org/docs/latest/api/python/>`_ :class:`DataFrame <pyspark:pyspark.sql.DataFrame>`. :param df: The :class:`DataFrame <pyspark:pyspark.sql.DataFrame>` from which data should be loaded. :type df: :class:`DataFrame <pyspark:pyspark.sql.DataFrame>` :param property_map: A :class:`dict <python:dict>` used to indicate which data point property should be set to which column in ``df``. The keys in the :class:`dict <python:dict>` should correspond to properties in the data point class, while the value should indicate the label for the :class:`DataFrame <pyspark:pyspark.sql.DataFrame>` column. :type property_map: :class:`dict <python:dict>` :param series_kwargs: An optional :class:`dict <python:dict>` containing keyword arguments that should be used when instantiating the series instance. Defaults to :obj:`None <python:None>`. .. warning:: If ``series_kwargs`` contains a ``data`` key, its value will be *overwritten*. The ``data`` value will be created from ``df`` instead. :type series_kwargs: :class:`dict <python:dict>` :returns: A :term:`series` instance (descended from :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`) with its :meth:`.data <highcharts_core.options.series.base.SeriesBase.data>` property populated from the data in ``df``. :rtype: :class:`list <python:list>` of series instances (descended from :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`) :raises HighchartsPySparkDeserializationError: if ``property_map`` references a column that does not exist in the data frame :raises HighchartsDependencyError: if `PySpark <https://spark.apache.org/docs/latest/api/python/>`_ is not available in the runtime environment """ series_kwargs = validators.dict(series_kwargs, allow_empty = True) or {} instance = cls(**series_kwargs) instance.load_from_pyspark(df, property_map) return instance
[docs] 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
[docs] 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)
def convert_to(self, series_type): """Creates a new series of ``series_type`` from the current series. :param series_type: The series type that should be returned. :type series_type: :class:`str <python:str>` or :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>`-descended .. warning:: This operation is *not* guaranteed to work converting between all series types. This is because some series types have different properties, different logic / functionality for their properties, and may have entirely different data requirements. In general, this method is expected to be *lossy* in nature, meaning that when the series can be converted "close enough" the series will be converted. However, if the target ``series_type`` does not support certain properties set on the original instance, then those settings will *not* be propagated to the new series. In certain cases, this method may raise an :exc:`HighchartsSeriesConversionError <highcharts_core.errors.HighchartsSeriesConversionError>` if the method is unable to convert (even losing some data) the original into ``series_type``. :returns: A new series of ``series_type``, maintaining relevant properties and data from the original instance. :rtype: ``series_type`` :class:`SeriesBase <highcharts_core.options.series.base.SeriesBase>` descendant :raises HighchartsSeriesConversionError: if unable to convert (even after losing some data) the original instance into an instance of ``series_type``. :raises HighchartsValueError: if ``series_type`` is not a recognized series type """ from highcharts_core.options.series.series_generator import SERIES_CLASSES if isinstance(series_type, str): 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}') series_type_name = series_type series_type = SERIES_CLASSES.get(series_type) elif not issubclass(series_type, SeriesBase): raise errors.HighchartsValueError(f'series_type expects a valid Highcharts ' f'series type. Received: {series_type}') else: series_type_name = series_type.__name__ as_js_literal = self.to_js_literal() try: target = series_type.from_js_literal(as_js_literal) except (ValueError, TypeError): raise errors.HighchartsSeriesConversionError(f'Unable to convert ' f'{self.__class__.__name__} instance ' f'to {series_type_name}') return target