From f0bfb3053f07f96ffac504fd9a6e128f1f49c851 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Fri, 14 Apr 2023 14:02:34 -0400 Subject: [PATCH 01/24] Updated docs to align to new v11 design changes. --- highcharts_core/options/chart/__init__.py | 1 + highcharts_core/options/plot_options/series.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/highcharts_core/options/chart/__init__.py b/highcharts_core/options/chart/__init__.py index f1a04321..22ac3df2 100644 --- a/highcharts_core/options/chart/__init__.py +++ b/highcharts_core/options/chart/__init__.py @@ -1210,6 +1210,7 @@ def styled_mode(self) -> Optional[bool]: .. seealso:: + * `Available CSS Styles and Variables `__. * The Default Style Sheet: `https://code.highcharts.com/css/highcharts.css `_. diff --git a/highcharts_core/options/plot_options/series.py b/highcharts_core/options/plot_options/series.py index 5336cbe1..298ad6e0 100644 --- a/highcharts_core/options/plot_options/series.py +++ b/highcharts_core/options/plot_options/series.py @@ -132,6 +132,13 @@ def color_index(self) -> Optional[int]: series, so that its graphic representations are given the class name ``highcharts-color-{n}``. + .. tip:: + + .. versionadded:: Highcharts (JS) v.11 + + With Highcharts (JS) v.11, using CSS variables of the form ``--highcharts-color-{n}`` make + changing the color scheme very simple. + Defaults to :obj:`None `. :rtype: :class:`int ` or :obj:`None ` From 8414d893599ea37146d52dd4467c5067c2d205f8 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Fri, 14 Apr 2023 14:18:33 -0400 Subject: [PATCH 02/24] Added .rotation_mode support for SunburstOptions.data_labels. --- .../options/plot_options/sunburst.py | 34 ++++- .../utility_classes/data_labels.py | 106 +++++++++++++ .../utility_classes/data_labels/03.js | 48 ++++++ tests/options/plot_options/test_sunburst.py | 1 + tests/utility_classes/test_data_labels.py | 144 ++++++++++++++++++ 5 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 tests/input_files/utility_classes/data_labels/03.js diff --git a/highcharts_core/options/plot_options/sunburst.py b/highcharts_core/options/plot_options/sunburst.py index 539c074f..8845dd43 100644 --- a/highcharts_core/options/plot_options/sunburst.py +++ b/highcharts_core/options/plot_options/sunburst.py @@ -1,7 +1,7 @@ from typing import Optional, List from decimal import Decimal -from validator_collection import validators +from validator_collection import validators, checkers from highcharts_core import constants, errors from highcharts_core.decorators import class_sensitive, validate_types @@ -11,6 +11,7 @@ from highcharts_core.utility_classes.patterns import Pattern from highcharts_core.utility_classes.breadcrumbs import BreadcrumbOptions from highcharts_core.utility_classes.shadows import ShadowOptions +from highcharts_core.utility_classes.data_labels import SunburstDataLabel class SunburstOptions(GenericTypeOptions): @@ -232,6 +233,37 @@ def crisp(self, value): else: self._crisp = bool(value) + @property + def data_labels(self) -> Optional[SunburstDataLabel | List[SunburstDataLabel]]: + """Options for the series data labels, appearing next to each data point. + + .. note:: + + To have multiple data labels per data point, you can also supply a collection of + :class:`DataLabel` configuration settings. + + :rtype: :class:`SunburstDataLabel `, + :class:`list ` of + :class:`SunburstDataLabel ` or + :obj:`None ` + """ + return self._data_labels + + @data_labels.setter + def data_labels(self, value): + if not value: + self._data_labels = None + else: + if checkers.is_iterable(value): + self._data_labels = validate_types(value, + types = SunburstDataLabel, + allow_none = False, + force_iterable = True) + else: + self._data_labels = validate_types(value, + types = SunburstDataLabel, + allow_none = False) + @property def fill_color(self) -> Optional[str | Gradient | Pattern]: """If the total sum of the pie's values is ``0``, the series is represented as an diff --git a/highcharts_core/utility_classes/data_labels.py b/highcharts_core/utility_classes/data_labels.py index 84199c5e..c9544d4b 100644 --- a/highcharts_core/utility_classes/data_labels.py +++ b/highcharts_core/utility_classes/data_labels.py @@ -905,6 +905,112 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: return untrimmed +class SunburstDataLabel(DataLabel): + """Variant of :class:`DataLabel` used for :term:`sunburst` series.""" + + def __init__(self, **kwargs): + self._rotation_mode = None + + self.rotation_mode = kwargs.get('rotation_mode', None) + + super().__init__(**kwargs) + + @property + def rotation_mode(self) -> Optional[str]: + """Determines how the data label will be rotated relative to the perimeter of the sunburst. + + Valid values are: + + * ``'circular'`` + * ``'auto'`` + * ``'parallel'`` + * ``'perpendicular'``. + + Defaults to ``'circular'``. + + .. note:: + + When ``'circular'``, the best fit will be computed for the point, so that the label is curved around the + center when there is room for it, otherwise perpendicular. + + The legacy ``'auto'`` option works similiarly to ``'circular'``, but instead of curving the labels, they are + tangented to the perimiter. + + .. warning:: + + The :meth:`.rotation ` property + takes precedence over ``.rotation_mode``. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._rotation_mode + + @rotation_mode.setter + def rotation_mode(self, value): + if not value: + self._rotation_mode = None + else: + value = validators.string(value, allow_empty = False) + value = value.lower() + if value not in ['circular', 'auto', 'parallel', 'perpendicular']: + raise errors.HighchartsValueError(f'if not empty, rotation_mode expects a value of either ' + f'"circular", "auto", "parallel", or "perpendicular", ' + f' but received "{str}".') + + self._rotation_mode = value + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'align': as_dict.get('align', None), + 'allow_overlap': as_dict.get('allowOverlap', None), + 'animation': as_dict.get('animation', None), + 'background_color': as_dict.get('backgroundColor', None), + 'border_color': as_dict.get('borderColor', None), + 'border_radius': as_dict.get('borderRadius', None), + 'border_width': as_dict.get('borderWidth', None), + 'class_name': as_dict.get('className', None), + 'color': as_dict.get('color', None), + 'crop': as_dict.get('crop', None), + 'defer': as_dict.get('defer', None), + 'enabled': as_dict.get('enabled', None), + 'filter': as_dict.get('filter', None), + 'format': as_dict.get('format', None), + 'formatter': as_dict.get('formatter', None), + 'inside': as_dict.get('inside', None), + 'null_format': as_dict.get('nullFormat', None), + 'null_formatter': as_dict.get('nullFormatter', None), + 'overflow': as_dict.get('overflow', None), + 'padding': as_dict.get('padding', None), + 'position': as_dict.get('position', None), + 'rotation': as_dict.get('rotation', None), + 'shadow': as_dict.get('shadow', None), + 'shape': as_dict.get('shape', None), + 'style': as_dict.get('style', None), + 'text_path': as_dict.get('textPath', None), + 'use_html': as_dict.get('useHTML', None), + 'vertical_align': as_dict.get('verticalAlign', None), + 'x': as_dict.get('x', None), + 'y': as_dict.get('y', None), + 'z': as_dict.get('z', None), + + 'rotation_mode': as_dict.get('rotationMode', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'rotationMode': self.rotation_mode, + } + + parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) or {} + for key in parent_as_dict: + untrimmed[key] = parent_as_dict[key] + + return untrimmed + + class NodeDataLabel(DataLabel): """Variant of :class:`DataLabel` used for node-based charts/diagrams.""" diff --git a/tests/input_files/utility_classes/data_labels/03.js b/tests/input_files/utility_classes/data_labels/03.js new file mode 100644 index 00000000..870e4a79 --- /dev/null +++ b/tests/input_files/utility_classes/data_labels/03.js @@ -0,0 +1,48 @@ +{ + rotationMode: 'circular', + align: 'center', + allowOverlap: true, + animation: { + defer: 5 + }, + backgroundColor: { + linearGradient: { + x1: 0.123, + x2: 0.234, + y1: 0.345, + y2: 0.456 + }, + stops: [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + borderColor: '#999999', + borderRadius: 24, + borderWidth: 1, + className: 'some-class-name', + color: '#000000', + crop: true, + defer: false, + enabled: true, + filter: { + operator: '>=', + property: 'some_property', + value: 123 + }, + format: 'some format', + formatter: function() { return true; }, + inside: true, + overflow: 'none', + padding: 12, + position: 'center', + rotation: 0, + shadow: false, + shape: 'rect', + style: 'style goes here', + useHTML: false, + verticalAlign: 'top', + x: 10, + y: 20, + z: 0 + } diff --git a/tests/options/plot_options/test_sunburst.py b/tests/options/plot_options/test_sunburst.py index 393a364f..dbd92063 100644 --- a/tests/options/plot_options/test_sunburst.py +++ b/tests/options/plot_options/test_sunburst.py @@ -204,6 +204,7 @@ 'padding': 12, 'position': 'center', 'rotation': 0, + 'rotationMode': 'circular', 'shadow': False, 'shape': 'rect', 'style': 'style goes here', diff --git a/tests/utility_classes/test_data_labels.py b/tests/utility_classes/test_data_labels.py index 7e0fd183..6d11604f 100644 --- a/tests/utility_classes/test_data_labels.py +++ b/tests/utility_classes/test_data_labels.py @@ -6,6 +6,7 @@ from highcharts_core.utility_classes.data_labels import DataLabel as cls from highcharts_core.utility_classes.data_labels import NodeDataLabel as cls2 +from highcharts_core.utility_classes.data_labels import SunburstDataLabel as cls3 from highcharts_core import errors from tests.fixtures import input_files, check_input_file, to_camelCase, to_js_dict, \ Class__init__, Class__to_untrimmed_dict, Class_from_dict, Class_to_dict, \ @@ -199,3 +200,146 @@ def test_NodeDataLabel_to_dict(kwargs, error): ]) def test_NodeDataLabel_from_js_literal(input_files, filename, as_file, error): Class_from_js_literal(cls2, input_files, filename, as_file, error) + + +STANDARD_PARAMS_3 = [ + ({}, None), + ({ + 'align': 'center', + 'allow_overlap': True, + 'animation': { + 'defer': 5 + }, + 'background_color': { + 'linearGradient': { + 'x1': 0.123, + 'x2': 0.234, + 'y1': 0.345, + 'y2': 0.456 + }, + 'stops': [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + 'border_color': '#999999', + 'border_radius': 24, + 'border_width': 1, + 'class_name': 'some-class-name', + 'color': '#000000', + 'crop': True, + 'defer': False, + 'enabled': True, + 'filter': { + 'operator': '>=', + 'property': 'some_property', + 'value': 123 + }, + 'format': 'some format', + 'formatter': """function() { return true; }""", + 'inside': True, + 'overflow': 'none', + 'padding': 12, + 'position': 'center', + 'rotation': 0, + 'rotation_mode': 'circular', + 'shadow': False, + 'shape': 'rect', + 'style': 'style goes here', + 'use_html': False, + 'vertical_align': 'top', + 'x': 10, + 'y': 20, + 'z': 0 + }, None), + + ({ + 'align': 'center', + 'allow_overlap': True, + 'animation': { + 'defer': 5 + }, + 'background_color': { + 'linearGradient': { + 'x1': 0.123, + 'x2': 0.234, + 'y1': 0.345, + 'y2': 0.456 + }, + 'stops': [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + 'border_color': '#999999', + 'border_radius': 24, + 'border_width': 1, + 'class_name': 'some-class-name', + 'color': '#000000', + 'crop': True, + 'defer': False, + 'enabled': True, + 'filter': { + 'operator': '>=', + 'property': 'some_property', + 'value': 123 + }, + 'format': 'some format', + 'formatter': """function() { return true; }""", + 'inside': True, + 'overflow': 'none', + 'padding': 12, + 'position': 'center', + 'rotation': 0, + 'rotation_mode': 'invalid-value', + 'shadow': False, + 'shape': 'rect', + 'style': 'style goes here', + 'use_html': False, + 'vertical_align': 'top', + 'x': 10, + 'y': 20, + 'z': 0 + }, errors.HighchartsValueError), +] + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS_3) +def test_SunburstDataLabel__init__(kwargs, error): + Class__init__(cls3, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS_3) +def test_SunburstDataLabel__to_untrimmed_dict(kwargs, error): + Class__to_untrimmed_dict(cls3, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS_3) +def test_SunburstDataLabel_from_dict(kwargs, error): + Class_from_dict(cls3, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS_3) +def test_SunburstDataLabel_to_dict(kwargs, error): + Class_to_dict(cls3, kwargs, error) + + +@pytest.mark.parametrize('filename, as_file, error', [ + ('utility_classes/data_labels/03.js', False, None), + + ('utility_classes/data_labels/error-03.js', False, (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + + ('utility_classes/data_labels/03.js', True, None), + + ('utility_classes/data_labels/error-03.js', True, (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), +]) +def test_SunburstDataLabel_from_js_literal(input_files, filename, as_file, error): + Class_from_js_literal(cls3, input_files, filename, as_file, error) From f33fbc147eb5252bdec50c94f5fac87306aa84fb Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Fri, 14 Apr 2023 14:29:26 -0400 Subject: [PATCH 03/24] Updated docs on options.series.data.base.DataBase.color_index to align to v11. --- highcharts_core/options/series/data/base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/highcharts_core/options/series/data/base.py b/highcharts_core/options/series/data/base.py index 1c240d7e..5e8af127 100644 --- a/highcharts_core/options/series/data/base.py +++ b/highcharts_core/options/series/data/base.py @@ -190,6 +190,13 @@ def color_index(self) -> Optional[int]: point, so its graphic representations are given the class name ``highcharts-color-{n}``. Defaults to :obj:`None `. + .. tip:: + + .. versionadded:: Highcharts (JS) v.11 + + With Highcharts (JS) v.11, using CSS variables of the form ``--highcharts-color-{n}`` make + changing the color scheme very simple. + :rtype: :class:`int ` or :obj:`None ` """ return self._color_index From 8925598480ecb383d7a54130e0715541c2e47406 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Fri, 14 Apr 2023 14:53:45 -0400 Subject: [PATCH 04/24] Fixed broken links to HeatmapOptions and TilemapOptions in documentation. --- docs/api.rst | 6 +++--- docs/api/options/index.rst | 6 +++--- docs/api/options/plot_options/index.rst | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 65a72515..18afb93a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -337,9 +337,9 @@ Core Components :class:`SolidGaugeOptions ` * - :mod:`.options.plot_options.generic ` - :class:`GenericTypeOptions ` - * - :mod:`.options.plot_options.Heatmap ` - - :class:`HeatmapOptions ` - :class:`TilemapOptions ` + * - :mod:`.options.plot_options.heatmap ` + - :class:`HeatmapOptions ` + :class:`TilemapOptions ` * - :mod:`.options.plot_options.histogram ` - :class:`HistogramOptions ` * - :mod:`.options.plot_options.item ` diff --git a/docs/api/options/index.rst b/docs/api/options/index.rst index b9a6d73a..a4b67e6c 100644 --- a/docs/api/options/index.rst +++ b/docs/api/options/index.rst @@ -265,9 +265,9 @@ Sub-components :class:`SolidGaugeOptions ` * - :mod:`.options.plot_options.generic ` - :class:`GenericTypeOptions ` - * - :mod:`.options.plot_options.Heatmap ` - - :class:`HeatmapOptions ` - :class:`TilemapOptions ` + * - :mod:`.options.plot_options.heatmap ` + - :class:`HeatmapOptions ` + :class:`TilemapOptions ` * - :mod:`.options.plot_options.histogram ` - :class:`HistogramOptions ` * - :mod:`.options.plot_options.item ` diff --git a/docs/api/options/plot_options/index.rst b/docs/api/options/plot_options/index.rst index cd543720..0fb32b9f 100644 --- a/docs/api/options/plot_options/index.rst +++ b/docs/api/options/plot_options/index.rst @@ -139,9 +139,9 @@ Sub-components :class:`SolidGaugeOptions ` * - :mod:`.options.plot_options.generic ` - :class:`GenericTypeOptions ` - * - :mod:`.options.plot_options.Heatmap ` - - :class:`HeatmapOptions ` - :class:`TilemapOptions ` + * - :mod:`.options.plot_options.heatmap ` + - :class:`HeatmapOptions ` + :class:`TilemapOptions ` * - :mod:`.options.plot_options.histogram ` - :class:`HistogramOptions ` * - :mod:`.options.plot_options.item ` From 4262064f795876dd3a18eb1f4d86762dd4566159 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Fri, 14 Apr 2023 14:54:34 -0400 Subject: [PATCH 05/24] Modified documentation and functionality of options.axes.labels.AxisLabelOptions.distance and .x. --- highcharts_core/options/axes/labels.py | 42 +++++++++++++++++++------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/highcharts_core/options/axes/labels.py b/highcharts_core/options/axes/labels.py index 63d0a361..561db212 100644 --- a/highcharts_core/options/axes/labels.py +++ b/highcharts_core/options/axes/labels.py @@ -1,7 +1,7 @@ -from typing import Optional, List +from typing import Optional, List, Type from decimal import Decimal -from validator_collection import validators +from validator_collection import validators, checkers from highcharts_core import errors from highcharts_core.decorators import class_sensitive @@ -105,7 +105,7 @@ def allow_overlap(self, value): self._allow_overlap = bool(value) @property - def auto_rotation(self) -> Optional[List[int | float | Decimal | type(None)]]: + def auto_rotation(self) -> Optional[List[int | float | Decimal | Type[None]]]: """For horizontal axes, provides the allowed degrees of label rotation to prevent overlapping labels. @@ -165,22 +165,36 @@ def auto_rotation_limit(self, value): self._auto_rotation_limit = validators.integer(value, allow_empty = True) @property - def distance(self) -> Optional[int | float | Decimal]: + def distance(self) -> Optional[int | float | Decimal | str]: """The label's pixel distance from the perimeter of the plot area. - .. warning:: + .. versionchanged:: Highcharts for Python v.2.0.0 + Highcharts Core (JS) v.11 - Applies to polar charts only. + If not specified, defaults to ``8``. - :rtype: numeric or :obj:`None ` + .. warning:: + + On cartesian charts, this is overridden if the :meth:`.x ` property + is set. + + On polar charts, if it's a percentage string, it is interpreted the same as + :meth:`SolidGaugeOptions.radius `, so that the label can be + aligned under the gauge's shape. + + :rtype: numeric, :class:`str `, or :obj:`None ` """ return self._distance @distance.setter def distance(self, value): - self._distance = validators.numeric(value, - allow_empty = True, - minimum = 0) + if value is None: + self._distance = None + elif checkers.is_string(value, allow_empty = False): + self._distance = validators.string(value) + else: + self._distance = validators.numeric(value, + allow_empty = True, + minimum = 0) @property def enabled(self) -> Optional[bool]: @@ -467,7 +481,13 @@ def use_html(self, value): @property def x(self) -> Optional[int | float | Decimal]: """The x position offset of all labels relative to the tick positions on the axis. - Defaults to ``0``. + + .. versionchanged:: Highcharts Core for Python v.2.0.0 / Highcharts Core (JS) v.11. + + .. note:: + + If set, overrides the :meth:`.distance ` + property. :rtype: numeric or :obj:`None ` """ From 3eb4858c1e0f1bc07ee414028c39fe80c7991248 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Fri, 14 Apr 2023 18:13:44 -0400 Subject: [PATCH 06/24] Bumped version and listed changes in changelog. --- CHANGES.rst | 20 ++++++++++++++++++++ highcharts_core/__version__.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4cf409b6..91b864f9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,23 @@ +Release 2.0.0 +========================================= + +* Align the API to **Highcharts (JS) v.11**. In particular, this includes: + + * Updating documentation for ``options.chart.ChartOptions.styled_mode`` to align + to new v11 design changes. + * Updated documentation for ``options.series.data.base.DataBase.color_index`` to align to + new v11 design changes. + * Added new ``utility_classes.data_labels.SunburstDataLabel`` class to patch missing + data label ``.rotation_mode`` property. + * Updated ``options.plot_options.SunburstOptions.data_labels`` to accept ``SunburstDataLabel`` + values. + * Updated documentation of ``options.axes.labels.AxisLabelOptions.distance`` to reflect new (or + newly-documented) behavior. + +* **FIXED:** Broken heatmap and tilemap documentation links. + +------------------------------- + Release 1.0.0 ========================================= diff --git a/highcharts_core/__version__.py b/highcharts_core/__version__.py index 1f356cc5..afced147 100644 --- a/highcharts_core/__version__.py +++ b/highcharts_core/__version__.py @@ -1 +1 @@ -__version__ = '1.0.0' +__version__ = '2.0.0' From 60c02481f2fd039a8b5ecdc2eac4fda1987025f8 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Sat, 22 Apr 2023 15:30:21 -0400 Subject: [PATCH 07/24] Implemented link_text_path support to options.plot_options.OrganizationOptions.data_labels. --- CHANGES.rst | 4 + docs/api.rst | 2 + docs/api/utility_classes/data_labels.rst | 36 ++++++++ docs/api/utility_classes/index.rst | 2 + .../options/plot_options/organization.py | 36 +++++++- .../utility_classes/data_labels.py | 82 ++++++++++++++++++- 6 files changed, 159 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 91b864f9..107545ab 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,10 @@ Release 2.0.0 values. * Updated documentation of ``options.axes.labels.AxisLabelOptions.distance`` to reflect new (or newly-documented) behavior. + * Added new ``utility_classes.data_labels.OrganizationDataLabel`` class to patch misisng data label ``. + link_text_path`` property. + * Updated ``options.plot_options.organization.OrganizationOptions.data_labels`` to accept ``OrganizationDataLabel`` + values. * **FIXED:** Broken heatmap and tilemap documentation links. diff --git a/docs/api.rst b/docs/api.rst index 18afb93a..e29066b2 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -573,6 +573,8 @@ Core Components - :class:`DataGroupingOptions ` * - :mod:`.utility_classes.data_labels ` - :class:`DataLabel ` + :class:`SunburstDataLabel ` + :class:`OrganizationDataLabel ` :class:`NodeDataLabel ` :class:`Filter ` * - :mod:`.utility_classes.date_time_label_formats ` diff --git a/docs/api/utility_classes/data_labels.rst b/docs/api/utility_classes/data_labels.rst index abb44dd1..7f110cf8 100644 --- a/docs/api/utility_classes/data_labels.rst +++ b/docs/api/utility_classes/data_labels.rst @@ -29,6 +29,42 @@ class: :class:`DataLabel ----------------- +******************************************************************************************************************** +class: :class:`OrganizationDataLabel ` +******************************************************************************************************************** + +.. autoclass:: OrganizationDataLabel + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: OrganizationDataLabel + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +----------------- + +******************************************************************************************************************** +class: :class:`SunburstDataLabel ` +******************************************************************************************************************** + +.. autoclass:: SunburstDataLabel + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: SunburstDataLabel + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +----------------- + ******************************************************************************************************************** class: :class:`NodeDataLabel ` ******************************************************************************************************************** diff --git a/docs/api/utility_classes/index.rst b/docs/api/utility_classes/index.rst index d81a2a15..6a6e86e8 100644 --- a/docs/api/utility_classes/index.rst +++ b/docs/api/utility_classes/index.rst @@ -68,6 +68,8 @@ Sub-components - :class:`DataGroupingOptions ` * - :mod:`.utility_classes.data_labels ` - :class:`DataLabel ` + :class:`SunburstDataLabel ` + :class:`OrganizationDataLabel ` :class:`NodeDataLabel ` :class:`Filter ` * - :mod:`.utility_classes.date_time_label_formats ` diff --git a/highcharts_core/options/plot_options/organization.py b/highcharts_core/options/plot_options/organization.py index 29783c73..85810c8a 100644 --- a/highcharts_core/options/plot_options/organization.py +++ b/highcharts_core/options/plot_options/organization.py @@ -1,12 +1,13 @@ from typing import Optional, List from decimal import Decimal -from validator_collection import validators +from validator_collection import validators, checkers from highcharts_core import errors -from highcharts_core.decorators import class_sensitive +from highcharts_core.decorators import class_sensitive, validate_types from highcharts_core.options.plot_options.bar import BarOptions from highcharts_core.options.plot_options.levels import LevelOptions +from highcharts_core.utility_classes.data_labels import OrganizationDataLabel class OrganizationOptions(BarOptions): @@ -58,6 +59,37 @@ def __init__(self, **kwargs): super().__init__(**kwargs) + @property + def data_labels(self) -> Optional[OrganizationDataLabel | List[OrganizationDataLabel]]: + """Options for the series data labels, appearing next to each data point. + + .. note:: + + To have multiple data labels per data point, you can also supply a collection of + :class:`DataLabel` configuration settings. + + :rtype: :class:`OrganizationDataLabel `, + :class:`list ` of + :class:`OrganizationDataLabel ` or + :obj:`None ` + """ + return self._data_labels + + @data_labels.setter + def data_labels(self, value): + if not value: + self._data_labels = None + else: + if checkers.is_iterable(value): + self._data_labels = validate_types(value, + types = OrganizationDataLabel, + allow_none = False, + force_iterable = True) + else: + self._data_labels = validate_types(value, + types = OrganizationDataLabel, + allow_none = False) + @property def hanging_indent(self) -> Optional[int | float | Decimal]: """The indentation in pixels of hanging nodes (nodes whose parent has diff --git a/highcharts_core/utility_classes/data_labels.py b/highcharts_core/utility_classes/data_labels.py index c9544d4b..87d4a4e0 100644 --- a/highcharts_core/utility_classes/data_labels.py +++ b/highcharts_core/utility_classes/data_labels.py @@ -3,7 +3,7 @@ from validator_collection import validators -from highcharts_core import constants, errors +from highcharts_core import errors from highcharts_core.decorators import class_sensitive, validate_types from highcharts_core.metaclasses import HighchartsMeta from highcharts_core.utility_classes.animation import AnimationOptions @@ -1011,6 +1011,86 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: return untrimmed +class OrganizationDataLabel(DataLabel): + """Variant of :class:`DataLabel` used for :term:`organization` series.""" + + def __init__(self, **kwargs): + self._link_text_path = None + + self.link_text_path = kwargs.get('link_text_path', None) + + super().__init__(**kwargs) + + @property + def link_text_path(self) -> Optional[TextPath]: + """Options for a label text which should follow the link's shape. + + .. note:: + + Border and background are disabled for a label that follows a path. + + :rtype: :class:`TextPath ` or :obj:`None ` + """ + return self._link_text_path + + @link_text_path.setter + @class_sensitive(TextPath) + def link_text_path(self, value): + self._link_text_path = value + + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'align': as_dict.get('align', None), + 'allow_overlap': as_dict.get('allowOverlap', None), + 'animation': as_dict.get('animation', None), + 'background_color': as_dict.get('backgroundColor', None), + 'border_color': as_dict.get('borderColor', None), + 'border_radius': as_dict.get('borderRadius', None), + 'border_width': as_dict.get('borderWidth', None), + 'class_name': as_dict.get('className', None), + 'color': as_dict.get('color', None), + 'crop': as_dict.get('crop', None), + 'defer': as_dict.get('defer', None), + 'enabled': as_dict.get('enabled', None), + 'filter': as_dict.get('filter', None), + 'format': as_dict.get('format', None), + 'formatter': as_dict.get('formatter', None), + 'inside': as_dict.get('inside', None), + 'null_format': as_dict.get('nullFormat', None), + 'null_formatter': as_dict.get('nullFormatter', None), + 'overflow': as_dict.get('overflow', None), + 'padding': as_dict.get('padding', None), + 'position': as_dict.get('position', None), + 'rotation': as_dict.get('rotation', None), + 'shadow': as_dict.get('shadow', None), + 'shape': as_dict.get('shape', None), + 'style': as_dict.get('style', None), + 'text_path': as_dict.get('textPath', None), + 'use_html': as_dict.get('useHTML', None), + 'vertical_align': as_dict.get('verticalAlign', None), + 'x': as_dict.get('x', None), + 'y': as_dict.get('y', None), + 'z': as_dict.get('z', None), + + 'link_text_path': as_dict.get('linkTextPath', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'linkTextPath': self.link_text_path, + } + + parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) or {} + for key in parent_as_dict: + untrimmed[key] = parent_as_dict[key] + + return untrimmed + + class NodeDataLabel(DataLabel): """Variant of :class:`DataLabel` used for node-based charts/diagrams.""" From 14cd7f2525bcb797197e9ade532d006db5ac1cec Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Sat, 22 Apr 2023 15:38:40 -0400 Subject: [PATCH 08/24] Implemented options.plot_options.accessibility.TypeOptionsAccessibility.description_format --- CHANGES.rst | 1 + .../options/accessibility/__init__.py | 1 - .../options/plot_options/accessibility.py | 38 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 107545ab..c9b3f6e4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,7 @@ Release 2.0.0 link_text_path`` property. * Updated ``options.plot_options.organization.OrganizationOptions.data_labels`` to accept ``OrganizationDataLabel`` values. + * Added ``.description_format`` property to ``options.plot_options.accessibility.TypeOptionsAccessibility``. * **FIXED:** Broken heatmap and tilemap documentation links. diff --git a/highcharts_core/options/accessibility/__init__.py b/highcharts_core/options/accessibility/__init__.py index d84662ab..3226ab70 100644 --- a/highcharts_core/options/accessibility/__init__.py +++ b/highcharts_core/options/accessibility/__init__.py @@ -32,7 +32,6 @@ class CustomAccessibilityComponents(JavaScriptDict): _allow_empty_value = True - class Accessibility(HighchartsMeta): """Options for configuring accessibility for the chart.""" diff --git a/highcharts_core/options/plot_options/accessibility.py b/highcharts_core/options/plot_options/accessibility.py index 5aaf6200..23247791 100644 --- a/highcharts_core/options/plot_options/accessibility.py +++ b/highcharts_core/options/plot_options/accessibility.py @@ -51,12 +51,14 @@ class TypeOptionsAccessibility(HighchartsMeta): def __init__(self, **kwargs): self._description = None + self._description_format = None self._enabled = None self._expose_as_group_only = None self._keyboard_navigation = None self._point = None self.description = kwargs.get('description', None) + self.description_format = kwargs.get('description_format', None) self.enabled = kwargs.get('enabled', None) self.expose_as_group_only = kwargs.get('expose_as_group_only', None) self.keyboard_navigation = kwargs.get('keyboard_navigation', None) @@ -74,6 +76,40 @@ def description(self) -> Optional[str]: def description(self, value): self._description = validators.string(value, allow_empty = True) + @property + def description_format(self) -> Optional[str]: + """Format to use for describing the data series group to assistive technology - + including screen readers. + + Defaults to ``'{seriesDescription}{authorDescription}{axisDescription}'``. + + The series context and its subproperties are available under the variable + ``{{series}}``, for example ``{{series.name}}`` for the series name, and + ``{{series.points.length}}`` for the number of data points. + + The chart context and its subproperties are available under the variable + ``{{chart}}``, for example ``{{chart.series.length}}`` for the number of series in + the chart. + + ``{{seriesDescription}}`` refers to the automatic description of the series type + and number of points added by Highcharts by default. + + ``{{authorDescription}}`` refers to the description added in + ``series.description`` if one is present. + + ``{{axisDescription}}`` refers to the description added if the chart has multiple + X or Y axes. + + :returns: Format string that applies to the description produced for the data + series. + :rtype: :class:`str ` or :obj:`None ` + """ + return self._description_format + + @description_format.setter + def description_format(self, value): + self._description_format = validators.string(value, allow_empty = True) + @property def enabled(self) -> Optional[bool]: """If ``True``, enable accessibility functionality for the series. @@ -134,6 +170,7 @@ def point(self, value): def _get_kwargs_from_dict(cls, as_dict): kwargs = { 'description': as_dict.get('description', None), + 'description_format': as_dict.get('description_format', None), 'enabled': as_dict.get('enabled', None), 'expose_as_group_only': as_dict.get('exposeAsGroupOnly', None), 'keyboard_navigation': as_dict.get('keyboardNavigation', None), @@ -145,6 +182,7 @@ def _get_kwargs_from_dict(cls, as_dict): def _to_untrimmed_dict(self, in_cls = None) -> dict: untrimmed = { 'description': self.description, + 'descriptionFormat': self.description_format, 'enabled': self.enabled, 'exposeAsGroupOnly': self.expose_as_group_only, 'keyboardNavigation': self.keyboard_navigation, From b3eb584d912dd1ff65ef69532898148843679a47 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Sat, 22 Apr 2023 23:39:45 -0400 Subject: [PATCH 09/24] Added support for TreegraphOptions and TreegraphSeries with related classes. --- CHANGES.rst | 1 + docs/api.rst | 8 + docs/api/options/index.rst | 6 + docs/api/options/plot_options/index.rst | 3 + docs/api/options/plot_options/treegraph.rst | 47 ++ docs/api/options/series/data/index.rst | 3 + docs/api/options/series/data/treegraph.rst | 28 + docs/api/options/series/index.rst | 5 + docs/api/options/series/treegraph.rst | 28 + docs/api/utility_classes/buttons.rst | 18 + highcharts_core/options/axes/labels.py | 2 +- .../options/plot_options/__init__.py | 25 + .../options/plot_options/generic.py | 2 +- .../options/plot_options/series.py | 1 - .../options/plot_options/treegraph.py | 744 ++++++++++++++++++ .../options/series/data/treegraph.py | 223 ++++++ .../options/series/series_generator.py | 4 +- highcharts_core/options/series/treegraph.py | 136 ++++ highcharts_core/utility_classes/buttons.py | 152 ++++ .../input_files/plot_options/treegraph/01.js | 22 + .../input_files/plot_options/treegraph/02.js | 262 ++++++ .../plot_options/treegraph/error-00.js | 1 + .../plot_options/treegraph/error-01.js | 98 +++ .../plot_options/treegraph/error-02.js | 336 ++++++++ tests/input_files/series/treegraph/01.js | 129 +++ tests/input_files/series/treegraph/02.js | 7 + .../input_files/series/treegraph/error-00.js | 1 + .../input_files/series/treegraph/error-01.js | 211 +++++ .../input_files/series/treegraph/error-02.js | 449 +++++++++++ tests/options/plot_options/test_treegraph.py | 354 +++++++++ tests/options/series/test_treegraph.py | 168 ++++ 31 files changed, 3469 insertions(+), 5 deletions(-) create mode 100644 docs/api/options/plot_options/treegraph.rst create mode 100644 docs/api/options/series/data/treegraph.rst create mode 100644 docs/api/options/series/treegraph.rst create mode 100644 highcharts_core/options/plot_options/treegraph.py create mode 100644 highcharts_core/options/series/data/treegraph.py create mode 100644 highcharts_core/options/series/treegraph.py create mode 100644 tests/input_files/plot_options/treegraph/01.js create mode 100644 tests/input_files/plot_options/treegraph/02.js create mode 100644 tests/input_files/plot_options/treegraph/error-00.js create mode 100644 tests/input_files/plot_options/treegraph/error-01.js create mode 100644 tests/input_files/plot_options/treegraph/error-02.js create mode 100644 tests/input_files/series/treegraph/01.js create mode 100644 tests/input_files/series/treegraph/02.js create mode 100644 tests/input_files/series/treegraph/error-00.js create mode 100644 tests/input_files/series/treegraph/error-01.js create mode 100644 tests/input_files/series/treegraph/error-02.js create mode 100644 tests/options/plot_options/test_treegraph.py create mode 100644 tests/options/series/test_treegraph.py diff --git a/CHANGES.rst b/CHANGES.rst index c9b3f6e4..dcc83e9d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,7 @@ Release 2.0.0 * Added ``.description_format`` property to ``options.plot_options.accessibility.TypeOptionsAccessibility``. * **FIXED:** Broken heatmap and tilemap documentation links. +* **FIXED:** Fixed missing ``TreegraphOptions`` / ``TreegraphSeries`` series type. ------------------------------- diff --git a/docs/api.rst b/docs/api.rst index e29066b2..694de9c0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -388,6 +388,9 @@ Core Components - :class:`SunburstOptions ` * - :mod:`.options.plot_options.timeline ` - :class:`TimelineOptions ` + * - :mod:`.options.plot_options.treegraph ` + - :class:`TreegraphOptions ` + :class:`TreegraphEvents ` * - :mod:`.options.plot_options.treemap ` - :class:`TreemapOptions ` * - :mod:`.options.plot_options.vector ` @@ -475,6 +478,8 @@ Core Components :class:`SinglePointBase ` * - :mod:`.options.series.data.sunburst ` - :class:`SunburstData ` + * - :mod:`.options.series.data.treegraph ` + - :class:`TreegraphData ` * - :mod:`.options.series.data.treemap ` - :class:`TreemapData ` * - :mod:`.options.series.data.vector ` @@ -533,6 +538,8 @@ Core Components - :class:`SunburstSeries ` * - :mod:`.options.series.timeline ` - :class:`TimelineSeries ` + * - :mod:`.options.series.treegraph ` + - :class:`TreegraphSeries ` * - :mod:`.options.series.treemap ` - :class:`TreemapSeries ` * - :mod:`.options.series.vector ` @@ -563,6 +570,7 @@ Core Components :class:`Separator ` * - :mod:`.utility_classes.buttons ` - :class:`ExportingButtons ` + :class:`CollapseButtonConfiguration ` :class:`ContextButtonConfiguration ` :class:`ButtonConfiguration ` :class:`ButtonTheme ` diff --git a/docs/api/options/index.rst b/docs/api/options/index.rst index a4b67e6c..63d557f0 100644 --- a/docs/api/options/index.rst +++ b/docs/api/options/index.rst @@ -316,6 +316,8 @@ Sub-components - :class:`SunburstOptions ` * - :mod:`.options.plot_options.timeline ` - :class:`TimelineOptions ` + * - :mod:`.options.plot_options.treegraph ` + - :class:`TreegraphSeries ` * - :mod:`.options.plot_options.treemap ` - :class:`TreemapOptions ` * - :mod:`.options.plot_options.vector ` @@ -403,6 +405,8 @@ Sub-components :class:`SinglePointBase ` * - :mod:`.options.series.data.sunburst ` - :class:`SunburstData ` + * - :mod:`.options.series.data.treegraph ` + - :class:`TreegraphData ` * - :mod:`.options.series.data.treemap ` - :class:`TreemapData ` * - :mod:`.options.series.data.vector ` @@ -461,6 +465,8 @@ Sub-components - :class:`SunburstSeries ` * - :mod:`.options.series.timeline ` - :class:`TimelineSeries ` + * - :mod:`.options.series.treegraph ` + - :class:`TreegraphSeries ` * - :mod:`.options.series.treemap ` - :class:`TreemapSeries ` * - :mod:`.options.series.vector ` diff --git a/docs/api/options/plot_options/index.rst b/docs/api/options/plot_options/index.rst index 0fb32b9f..3493fa46 100644 --- a/docs/api/options/plot_options/index.rst +++ b/docs/api/options/plot_options/index.rst @@ -44,6 +44,7 @@ spline sunburst timeline + treegraph treemap vector venn @@ -190,6 +191,8 @@ Sub-components - :class:`SunburstOptions ` * - :mod:`.options.plot_options.timeline ` - :class:`TimelineOptions ` + * - :mod:`.options.series.treegraph ` + - :class:`TreegraphSeries ` * - :mod:`.options.plot_options.treemap ` - :class:`TreemapOptions ` * - :mod:`.options.plot_options.vector ` diff --git a/docs/api/options/plot_options/treegraph.rst b/docs/api/options/plot_options/treegraph.rst new file mode 100644 index 00000000..dd2ad1d0 --- /dev/null +++ b/docs/api/options/plot_options/treegraph.rst @@ -0,0 +1,47 @@ +########################################################################################## +:mod:`.treegraph ` +########################################################################################## + +.. contents:: Module Contents + :local: + :depth: 3 + :backlinks: entry + +-------------- + +.. module:: highcharts_core.options.plot_options.treegraph + +******************************************************************************************************************** +class: :class:`TreegraphOptions ` +******************************************************************************************************************** + +.. autoclass:: TreegraphOptions + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: TreegraphOptions + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + + +-------------- + +******************************************************************************************************************** +class: :class:`TreegraphEvents ` +******************************************************************************************************************** + +.. autoclass:: TreegraphEvents + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: TreegraphEvents + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | diff --git a/docs/api/options/series/data/index.rst b/docs/api/options/series/data/index.rst index 8c04cefb..6850b91b 100644 --- a/docs/api/options/series/data/index.rst +++ b/docs/api/options/series/data/index.rst @@ -22,6 +22,7 @@ range single_point sunburst + treegraph treemap vector venn @@ -78,6 +79,8 @@ Sub-components :class:`LabeledSingleXData ` :class:`ConnectedSingleXData ` :class:`SinglePointBase ` + * - :mod:`.options.series.data.treegraph ` + - :class:`TreegraphData ` * - :mod:`.options.series.data.sunburst ` - :class:`SunburstData ` * - :mod:`.options.series.data.treemap ` diff --git a/docs/api/options/series/data/treegraph.rst b/docs/api/options/series/data/treegraph.rst new file mode 100644 index 00000000..f78541ea --- /dev/null +++ b/docs/api/options/series/data/treegraph.rst @@ -0,0 +1,28 @@ +########################################################################################## +:mod:`.treegraph ` +########################################################################################## + +.. contents:: Module Contents + :local: + :depth: 3 + :backlinks: entry + +-------------- + +.. module:: highcharts_core.options.series.data.treegraph + +******************************************************************************************************************** +class: :class:`TreegraphData ` +******************************************************************************************************************** + +.. autoclass:: TreegraphData + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: TreegraphData + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | diff --git a/docs/api/options/series/index.rst b/docs/api/options/series/index.rst index 45b93da0..a0684dd8 100644 --- a/docs/api/options/series/index.rst +++ b/docs/api/options/series/index.rst @@ -41,6 +41,7 @@ spline sunburst timeline + treegraph treemap vector venn @@ -132,6 +133,8 @@ Sub-components :class:`SinglePointBase ` * - :mod:`.options.series.data.sunburst ` - :class:`SunburstData ` + * - :mod:`.options.series.data.treegraph ` + - :class:`TreegraphData ` * - :mod:`.options.series.data.treemap ` - :class:`TreemapData ` * - :mod:`.options.series.data.vector ` @@ -190,6 +193,8 @@ Sub-components - :class:`SunburstSeries ` * - :mod:`.options.series.timeline ` - :class:`TimelineSeries ` + * - :mod:`.options.series.treegraph ` + - :class:`TreegraphSeries ` * - :mod:`.options.series.treemap ` - :class:`TreemapSeries ` * - :mod:`.options.series.vector ` diff --git a/docs/api/options/series/treegraph.rst b/docs/api/options/series/treegraph.rst new file mode 100644 index 00000000..ee30a17c --- /dev/null +++ b/docs/api/options/series/treegraph.rst @@ -0,0 +1,28 @@ +########################################################################################## +:mod:`.treegraph ` +########################################################################################## + +.. contents:: Module Contents + :local: + :depth: 3 + :backlinks: entry + +-------------- + +.. module:: highcharts_core.options.series.treegraph + +******************************************************************************************************************** +class: :class:`TreegraphSeries ` +******************************************************************************************************************** + +.. autoclass:: TreegraphSeries + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: TreegraphSeries + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | diff --git a/docs/api/utility_classes/buttons.rst b/docs/api/utility_classes/buttons.rst index b1874433..c80cce83 100644 --- a/docs/api/utility_classes/buttons.rst +++ b/docs/api/utility_classes/buttons.rst @@ -29,6 +29,24 @@ class: :class:`ExportingButtons ` +******************************************************************************************************************** + +.. autoclass:: CollapseButtonConfiguration + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: CollapseButtonConfiguration + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +-------------- + ******************************************************************************************************************** class: :class:`ContextButtonConfiguration ` ******************************************************************************************************************** diff --git a/highcharts_core/options/axes/labels.py b/highcharts_core/options/axes/labels.py index 561db212..6a12e983 100644 --- a/highcharts_core/options/axes/labels.py +++ b/highcharts_core/options/axes/labels.py @@ -189,7 +189,7 @@ def distance(self) -> Optional[int | float | Decimal | str]: def distance(self, value): if value is None: self._distance = None - elif checkers.is_string(value, allow_empty = False): + elif checkers.is_string(value) or not value: self._distance = validators.string(value) else: self._distance = validators.numeric(value, diff --git a/highcharts_core/options/plot_options/__init__.py b/highcharts_core/options/plot_options/__init__.py index 8a3c5bbe..cbd65b9b 100644 --- a/highcharts_core/options/plot_options/__init__.py +++ b/highcharts_core/options/plot_options/__init__.py @@ -45,6 +45,7 @@ from highcharts_core.options.plot_options.sunburst import SunburstOptions from highcharts_core.options.plot_options.heatmap import TilemapOptions from highcharts_core.options.plot_options.timeline import TimelineOptions +from highcharts_core.options.plot_options.treegraph import TreegraphOptions from highcharts_core.options.plot_options.treemap import TreemapOptions from highcharts_core.options.plot_options.pie import VariablePieOptions from highcharts_core.options.plot_options.bar import VariwideOptions @@ -118,6 +119,7 @@ def __init__(self, **kwargs): self._sunburst = None self._tilemap = None self._timeline = None + self._treegraph = None self._treemap = None self._variablepie = None self._variwide = None @@ -1145,6 +1147,27 @@ def timeline(self) -> Optional[TimelineOptions]: def timeline(self, value): self._timeline = value + @property + def treegraph(self) -> Optional[TreegraphOptions]: + """General options to apply to all :term:`Treegraph` series types. + + A treegraph visualizes a relationship between ancestors and descendants with a clear parent-child relationship, + e.g. a family tree or a directory structure. + + .. figure:: ../../../_static/treegraph-example.png + :alt: Treegraph Example Chart + :align: center + + :rtype: :class:`TreegraphOptions ` or + :obj:`None ` + """ + return self._treegraph + + @treegraph.setter + @class_sensitive(TreegraphOptions) + def treegraph(self, value): + self._treegraph = value + @property def treemap(self) -> Optional[TreemapOptions]: """General options to apply to all Treemap series types. @@ -1428,6 +1451,7 @@ def _get_kwargs_from_dict(cls, as_dict): 'sunburst': as_dict.get('sunburst', None), 'tilemap': as_dict.get('tilemap', None), 'timeline': as_dict.get('timeline', None), + 'treegraph': as_dict.get('treegraph', None), 'treemap': as_dict.get('treemap', None), 'variablepie': as_dict.get('variablepie', None), 'variwide': as_dict.get('variwide', None), @@ -1486,6 +1510,7 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: 'sunburst': self.sunburst, 'tilemap': self.tilemap, 'timeline': self.timeline, + 'treegraph': self.treegraph, 'treemap': self.treemap, 'variablepie': self.variablepie, 'variwide': self.variwide, diff --git a/highcharts_core/options/plot_options/generic.py b/highcharts_core/options/plot_options/generic.py index 6d87c34f..ba483375 100644 --- a/highcharts_core/options/plot_options/generic.py +++ b/highcharts_core/options/plot_options/generic.py @@ -810,7 +810,7 @@ class from a Highcharts Javascript-compatible :class:`dict ` object 'threshold': as_dict.get('threshold', None), 'tooltip': as_dict.get('tooltip', None), 'turbo_threshold': as_dict.get('turboThreshold', None), - 'visible': as_dict.get('visible', True) + 'visible': as_dict.get('visible', None) } return kwargs diff --git a/highcharts_core/options/plot_options/series.py b/highcharts_core/options/plot_options/series.py index 298ad6e0..a9252884 100644 --- a/highcharts_core/options/plot_options/series.py +++ b/highcharts_core/options/plot_options/series.py @@ -786,7 +786,6 @@ def point_start(self, value): self._point_start = value - @property def stacking(self) -> Optional[str]: """Whether to stack the values of each series on top of each other. Defaults to diff --git a/highcharts_core/options/plot_options/treegraph.py b/highcharts_core/options/plot_options/treegraph.py new file mode 100644 index 00000000..2a6c80df --- /dev/null +++ b/highcharts_core/options/plot_options/treegraph.py @@ -0,0 +1,744 @@ +import datetime +from typing import Optional +from decimal import Decimal + +from validator_collection import validators + +from highcharts_core import errors +from highcharts_core.decorators import class_sensitive +from highcharts_core.options.plot_options.generic import GenericTypeOptions +from highcharts_core.utility_classes.buttons import CollapseButtonConfiguration +from highcharts_core.utility_classes.events import SeriesEvents +from highcharts_core.utility_classes.javascript_functions import CallbackFunction +from highcharts_core.options.plot_options.link import LinkOptions + + +class TreegraphEvents(SeriesEvents): + """General event handlers for the series items. + + .. tip:: + + These event hooks can also be attached to the series at run time using the ``Highcharts.addEvent()`` (JavaScript) + function. + + """ + + def __init__(self, **kwargs): + self._set_root_node = None + + self.set_root_node = kwargs.get('set_root_node', None) + + super().__init__(**kwargs) + + @property + def set_root_node(self) -> Optional[CallbackFunction]: + """Event handler that fires on a request to change the tree's root node, *before* the update is made. + + An event object is passed to the function, containing additional properties ``newRootId``, ``previousRootId``, + ``redraw``, and ``trigger``. + + Defaults to :obj:`None ` + + :rtype: :class:`CallbackFunction ` or + :obj:`None ` + """ + return self._set_root_node + + @set_root_node.setter + @class_sensitive(CallbackFunction) + def set_root_node(self, value): + self._set_root_node = value + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'after_animate': as_dict.get('afterAnimate', None), + 'checkbox_click': as_dict.get('checkboxClick', None), + 'click': as_dict.get('click', None), + 'hide': as_dict.get('hide', None), + 'legend_item_click': as_dict.get('legendItemClick', None), + 'mouse_out': as_dict.get('mouseOut', None), + 'mouse_over': as_dict.get('mouseOver', None), + 'show': as_dict.get('show', None), + + 'set_root_node': as_dict.get('setRootNode', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'afterAnimate': self.after_animate, + 'checkboxClick': self.checkbox_click, + 'click': self.click, + 'hide': self.hide, + 'legendItemClick': self.legend_item_click, + 'mouseOut': self.mouse_out, + 'mouseOver': self.mouse_over, + 'setRootNode': self.set_root_node, + 'show': self.show + } + + return untrimmed + + +class TreegraphOptions(GenericTypeOptions): + """General options to apply to all :term:`Treegraph` series types. + + A treegraph visualizes a relationship between ancestors and descendants with a clear parent-child relationship, + e.g. a family tree or a directory structure. + + .. figure:: ../../../_static/treegraph-example.png + :alt: Treegraph Example Chart + :align: center + + """ + + def __init__(self, **kwargs): + self._animation_limit = None + self._boost_blending = None + self._boost_threshold = None + self._color_index = None + self._crisp = None + self._crop_threshold = None + self._find_nearest_point_by = None + self._get_extremes_from_all = None + self._relative_x_value = None + self._soft_threshold = None + self._step = None + + self._point_interval = None + self._point_interval_unit = None + self._point_start = None + self._stacking = None + + self._allow_traversing_tree = None + self._collapse_button = None + self._color_by_point = None + self._link = None + self._reversed = None + self._traverse_up_button = None + + self.animation_limit = kwargs.get('animation_limit', None) + self.boost_blending = kwargs.get('boost_blending', None) + self.boost_threshold = kwargs.get('boost_threshold', None) + self.color_index = kwargs.get('color_index', None) + self.crisp = kwargs.get('crisp', None) + self.crop_threshold = kwargs.get('crop_threshold', None) + self.find_nearest_point_by = kwargs.get('find_nearest_point_by', None) + self.get_extremes_from_all = kwargs.get('get_extremes_from_all', None) + self.relative_x_value = kwargs.get('relative_x_value', None) + self.soft_threshold = kwargs.get('soft_threshold', None) + self.step = kwargs.get('step', None) + + self.point_interval = kwargs.get('point_interval', None) + self.point_interval_unit = kwargs.get('point_interval_unit', None) + self.point_start = kwargs.get('point_start', None) + self.stacking = kwargs.get('stacking', None) + + self.allow_traversing_tree = kwargs.get('allow_traversing_tree', None) + self.collapse_button = kwargs.get('collapse_button', None) + self.color_by_point = kwargs.get('color_by_point', None) + self.link = kwargs.get('link', None) + self.reversed = kwargs.get('reversed', None) + + super().__init__(**kwargs) + + @property + def animation_limit(self) -> Optional[int | float | Decimal]: + """For some series, there is a limit that shuts down initial animation by default + when the total number of points in the chart is too high. Defaults to + :obj:`None `. + + For example, for a column chart and its derivatives, animation does not run if + there is more than 250 points totally. To disable this cap, set + ``animation_limit`` to ``float("inf")`` (which represents infinity). + + :rtype: numeric or :obj:`None ` + """ + return self._animation_limit + + @animation_limit.setter + def animation_limit(self, value): + if value == float('inf'): + self._animation_limit = float('inf') + else: + self._animation_limit = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def boost_blending(self) -> Optional[str]: + """Sets the color blending in the boost module. Defaults to + :obj:`None `. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._boost_blending + + @boost_blending.setter + def boost_blending(self, value): + self._boost_blending = validators.string(value, allow_empty = True) + + @property + def boost_threshold(self) -> Optional[int]: + """Set the point threshold for when a series should enter boost mode. Defaults to + ``5000``. + + Setting it to e.g. 2000 will cause the series to enter boost mode when there are + 2,000 or more points in the series. + + To disable boosting on the series, set the ``boost_threshold`` to ``0``. Setting + it to ``1`` will force boosting. + + .. note:: + + The :meth:`AreaOptions.crop_threshold` also affects this setting. + + When zooming in on a series that has fewer points than the ``crop_threshold``, + all points are rendered although outside the visible plot area, and the + ``boost_threshold`` won't take effect. + + :rtype: :class:`int ` or :obj:`None ` + """ + return self._boost_threshold + + @boost_threshold.setter + def boost_threshold(self, value): + self._boost_threshold = validators.integer(value, + allow_empty = True, + minimum = 0) + + @property + def color_index(self) -> Optional[int]: + """When operating in :term:`styled mode`, a specific color index to use for the + series, so that its graphic representations are given the class name + ``highcharts-color-{n}``. + + .. tip:: + + .. versionadded:: Highcharts (JS) v.11 + + With Highcharts (JS) v.11, using CSS variables of the form ``--highcharts-color-{n}`` make + changing the color scheme very simple. + + Defaults to :obj:`None `. + + :rtype: :class:`int ` or :obj:`None ` + """ + return self._color_index + + @color_index.setter + def color_index(self, value): + self._color_index = validators.integer(value, + allow_empty = True, + minimum = 0) + + @property + def crisp(self) -> Optional[bool]: + """If ``True``, each point or column edge is rounded to its nearest pixel in order + to render sharp on screen. Defaults to ``True``. + + .. hint:: + + In some cases, when there are a lot of densely packed columns, this leads to + visible difference in column widths or distance between columns. In these cases, + setting ``crisp`` to ``False`` may look better, even though each column is + rendered blurry. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._crisp + + @crisp.setter + def crisp(self, value): + if value is None: + self._crisp = None + else: + self._crisp = bool(value) + + @property + def crop_threshold(self) -> Optional[int]: + """When the series contains less points than the crop threshold, all points are + drawn, even if the points fall outside the visible plot area at the current zoom. + Defaults to ``300``. + + The advantage of drawing all points (including markers and columns), is that + animation is performed on updates. On the other hand, when the series contains + more points than the crop threshold, the series data is cropped to only contain + points that fall within the plot area. The advantage of cropping away invisible + points is to increase performance on large series. + + :rtype: :class:`int ` or :obj:`None ` + """ + return self._crop_threshold + + @crop_threshold.setter + def crop_threshold(self, value): + self._crop_threshold = validators.integer(value, + allow_empty = True, + minimum = 0) + + @property + def events(self) -> Optional[TreegraphEvents]: + """General event handlers for the series items. + + .. note:: + + These event hooks can also be attached to the series at run time using the + (JavaScript) ``Highcharts.addEvent()`` function. + + :rtype: :class:`TreegraphEvents` or :obj:`None ` + """ + return self._events + + @events.setter + @class_sensitive(TreegraphEvents) + def events(self, value): + self._events = value + + @property + def find_nearest_point_by(self) -> Optional[str]: + """Determines whether the series should look for the nearest point in both + dimensions or just the x-dimension when hovering the series. + + If :obj:`None `, defaults to ``'xy'`` for scatter series and ``'x'`` + for most other series. If the data has duplicate x-values, it is recommended to + set this to ``'xy'`` to allow hovering over all points. + + Applies only to series types using nearest neighbor search (not direct hover) for + tooltip. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._find_nearest_point_by + + @find_nearest_point_by.setter + def find_nearest_point_by(self, value): + self._find_nearest_point_by = validators.string(value, allow_empty = True) + + @property + def get_extremes_from_all(self) -> Optional[bool]: + """If ``True``, uses the Y extremes of the total chart width or only the zoomed + area when zooming in on parts of the X axis. By default, the Y axis adjusts to the + min and max of the visible data. + + .. warning:: + + Applies to :term:`Cartesian series` only. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._get_extremes_from_all + + @get_extremes_from_all.setter + def get_extremes_from_all(self, value): + if value is None: + self._get_extremes_from_all = None + else: + self._get_extremes_from_all = bool(value) + + @property + def relative_x_value(self) -> Optional[bool]: + """When ``True``, X values in the data set are relative to the current + :meth:`point_start `, + :meth:`point_interval `, and + :meth:`point_interval_unit ` settings. This + allows compression of the data for datasets with irregular X values. Defaults to + ``False``. + + The real X values are computed on the formula ``f(x) = ax + b``, where ``a`` is + the :meth:`point_interval ` (optionally with a time + unit given by :meth:`point_interval_unit `), and + ``b`` is the :meth:`point_start `. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._relative_x_value + + @relative_x_value.setter + def relative_x_value(self, value): + if value is None: + self._relative_x_value = None + else: + self._relative_x_value = bool(value) + + @property + def soft_threshold(self) -> Optional[bool]: + """When ``True``, the series will not cause the Y axis to cross the zero plane (or + threshold option) unless the data actually crosses the plane. Defaults to + ``True``. + + For example, if ``False``, a series of ``0, 1, 2, 3`` will make the Y axis show + negative values according to the ``min_padidng`` option. If ``True``, the Y axis + starts at 0. + + :rtype: :class:`bool ` + """ + return self._soft_threshold + + @soft_threshold.setter + def soft_threshold(self, value): + if value is None: + self._soft_threshold = None + else: + self._soft_threshold = bool(value) + + @property + def step(self) -> Optional[str]: + """Whether to apply steps to the line. Defaults to :obj:`None `. + + Possible values are: + + * :obj:`None ` + * ``'left'`` + * ``'center'`` + * ``'right'`` + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._step + + @step.setter + def step(self, value): + self._step = validators.string(value, allow_empty = True) + + @property + def point_interval(self) -> Optional[int | float | Decimal]: + """If no x values are given for the points in a series, ``point_interval`` defines + the interval of the x values. Defaults to ``1``. + + For example, if a series contains one value every decade starting from year 0, set + ``point_interval`` to ``10``. In true datetime axes, the ``point_interval`` is set + in milliseconds. + + .. hint:: + + ``point_interval`` can be also be combined with + :meth:`point_interval_unit ` to draw irregular + time intervals. + + .. note:: + + If combined with :meth:`relative_x_value `, an x + value can be set on each point, and the ``point_interval`` is added x times to + the :meth:`point_start ` setting. + + .. warning:: + + This options applies to the series data, not the interval of the axis ticks, + which is independent. + + :rtype: numeric or :obj:`None ` + """ + return self._point_interval + + @point_interval.setter + def point_interval(self, value): + self._point_interval = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def point_interval_unit(self) -> Optional[str]: + """On datetime series, this allows for setting the + :meth:`point_interval ` to irregular time units, day, + month, and year. + + A day is usually the same as 24 hours, but ``point_interval_unit`` also takes the + DST crossover into consideration when dealing with local time. + + Combine this option with :meth:`point_interval ` to + draw weeks, quarters, 6 month periods, 10 year periods, etc. + + .. warning:: + + This options applies to the series data, not the interval of the axis ticks, + which is independent. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._point_interval_unit + + @point_interval_unit.setter + def point_interval_unit(self, value): + self._point_interval_unit = validators.string(value, allow_empty = True) + + @property + def point_start(self) -> Optional[int | float | Decimal]: + """If no x values are given for the points in a series, ``point_start`` defines + on what value to start. For example, if a series contains one yearly value + starting from 1945, set ``point_start`` to ``1945``. Defaults to ``0``. + + .. note:: + + If combined with :meth:`relative_x_value `, an x + value can be set on each point. The x value from the point options is multiplied + by :meth:`point_interval ` and added to + ``point_start`` to produce a modified x value. + + :rtype: numeric or :obj:`None ` + """ + return self._point_start + + @point_start.setter + def point_start(self, value): + try: + value = validators.numeric(value, allow_empty = True) + except (TypeError, ValueError) as error: + value = validators.datetime(value) + + if hasattr(value, 'timestamp') and value.tzinfo is not None: + self._point_start = value.timestamp() + elif hasattr(value, 'timestamp'): + value = value.replace(tzinfo = datetime.timezone.utc) + value = value.timestamp() + else: + raise error + + self._point_start = value + + @property + def stacking(self) -> Optional[str]: + """Whether to stack the values of each series on top of each other. Defaults to + :obj:`None `. + + Acceptable values are: + + * :obj:`None ` to disable stacking, + * ``"normal"`` to stack by value or + * ``"percent"`` + * ``'stream'`` (for streamgraph series type only) + * ``'overlap'`` (for waterfall series type only) + + .. note:: + + When stacking is enabled, data must be sorted in ascending X order. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._stacking + + @stacking.setter + def stacking(self, value): + if not value: + self._stacking = None + else: + value = validators.string(value) + value = value.lower() + if value not in ['normal', 'percent', 'stream', 'overlap']: + raise errors.HighchartsValueError(f'stacking expects a valid stacking ' + f'value. However, received: {value}') + self._stacking = value + + @property + def allow_traversing_tree(self) -> Optional[bool]: + """If ``True``, the user can click on a point which is a parent and zoom in on its + children. Defaults to ``False``. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._allow_traversing_tree + + @allow_traversing_tree.setter + def allow_traversing_tree(self, value): + if value is None: + self._allow_traversing_tree = None + else: + self._allow_traversing_tree = bool(value) + + @property + def collapse_button(self) -> Optional[CollapseButtonConfiguration]: + """Options applied to the Collapse Button, which is the small button that indicates the node is collapsible. + + :rtype: :class:`CollapseButtonConfiguration ` + or :obj:`None ` + """ + return self._collapse_button + + @collapse_button.setter + @class_sensitive(CollapseButtonConfiguration) + def collapse_button(self, value): + self._collapse_button = value + + @property + def color_by_point(self) -> Optional[bool]: + """When using automatic point colors pulled from the global colors or + series-specific collections, this option determines whether the chart should + receive one color per series (``False``) or one color per point (``True``). + + Defaults to ``True``. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._color_by_point + + @color_by_point.setter + def color_by_point(self, value): + if value is None: + self._color_by_point = None + else: + self._color_by_point = bool(value) + + @property + def link(self) -> Optional[LinkOptions]: + """Link style options. + + :rtype: :class:`LinkOptions` or :obj:`None ` + """ + return self._link + + @link.setter + @class_sensitive(LinkOptions) + def link(self, value): + self._link = value + + @property + def reversed(self) -> Optional[bool]: + """If ``True``, places the series on the other side of the plot area. Defaults to + ``False``. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._reversed + + @reversed.setter + def reversed(self, value): + if value is None: + self._reversed = None + else: + self._reversed = bool(value) + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + """Convenience method which returns the keyword arguments used to initialize the + class from a Highcharts Javascript-compatible :class:`dict ` object. + + :param as_dict: The HighCharts JS compatible :class:`dict ` + representation of the object. + :type as_dict: :class:`dict ` + + :returns: The keyword arguments that would be used to initialize an instance. + :rtype: :class:`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), + '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), + 'states': as_dict.get('states', None), + 'sticky_tracking': as_dict.get('stickyTracking', None), + 'tooltip': as_dict.get('tooltip', None), + 'turbo_threshold': as_dict.get('turboThreshold', None), + 'visible': as_dict.get('visible', None), + + 'animation_limit': as_dict.get('animationLimit', None), + 'boost_blending': as_dict.get('boostBlending', None), + 'boost_threshold': as_dict.get('boostThreshold', None), + 'color_index': as_dict.get('colorIndex', None), + 'crisp': as_dict.get('crisp', None), + 'crop_threshold': as_dict.get('cropThreshold', None), + 'find_nearest_point_by': as_dict.get('findNearestPointBy', None), + 'get_extremes_from_all': as_dict.get('getExtremesFromAll', None), + 'relative_x_value': as_dict.get('relativeXValue', None), + 'soft_threshold': as_dict.get('softThreshold', None), + 'step': as_dict.get('step', None), + + 'point_interval': as_dict.get('pointInterval', None), + 'point_interval_unit': as_dict.get('pointIntervalUnit', None), + 'point_start': as_dict.get('pointStart', None), + 'stacking': as_dict.get('stacking', None), + + 'allow_traversing_tree': as_dict.get('allowTraversingTree', None), + 'collapse_button': as_dict.get('collapseButton', None), + 'color_by_point': as_dict.get('colorByPoint', None), + 'link': as_dict.get('link', None), + 'reversed': as_dict.get('reversed', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'accessibility': self.accessibility, + 'allowPointSelect': self.allow_point_select, + 'animation': self.animation, + 'className': self.class_name, + 'clip': self.clip, + 'color': self.color, + 'cursor': self.cursor, + 'custom': self.custom, + 'dashStyle': self.dash_style, + 'dataLabels': self.data_labels, + 'description': self.description, + 'enableMouseTracking': self.enable_mouse_tracking, + 'events': self.events, + 'includeInDataExport': self.include_in_data_export, + 'keys': self.keys, + 'label': self.label, + 'linkedTo': self.linked_to, + 'marker': self.marker, + 'onPoint': self.on_point, + 'opacity': self.opacity, + 'point': self.point, + 'pointDescriptionFormatter': self.point_description_formatter, + 'selected': self.selected, + 'showCheckbox': self.show_checkbox, + 'showInLegend': self.show_in_legend, + 'skipKeyboardNavigation': self.skip_keyboard_navigation, + 'states': self.states, + 'stickyTracking': self.sticky_tracking, + 'threshold': self.threshold, + 'tooltip': self.tooltip, + 'turboThreshold': self.turbo_threshold, + 'visible': self.visible, + 'type': self.type, + + 'animationLimit': self.animation_limit, + 'boostBlending': self.boost_blending, + 'boostThreshold': self.boost_threshold, + 'colorIndex': self.color_index, + 'crisp': self.crisp, + 'cropThreshold': self.crop_threshold, + 'findNearestPointBy': self.find_nearest_point_by, + 'getExtremesFromAll': self.get_extremes_from_all, + 'relativeXValue': self.relative_x_value, + 'softThreshold': self.soft_threshold, + 'step': self.step, + + 'pointInterval': self.point_interval, + 'pointIntervalUnit': self.point_interval_unit, + 'pointStart': self.point_start, + 'stacking': self.stacking, + + 'allowTraversingTree': self.allow_traversing_tree, + 'collapseButton': self.collapse_button, + 'colorByPoint': self.color_by_point, + 'link': self.link, + 'reversed': self.reversed, + } + + return untrimmed diff --git a/highcharts_core/options/series/data/treegraph.py b/highcharts_core/options/series/data/treegraph.py new file mode 100644 index 00000000..fe4d8d43 --- /dev/null +++ b/highcharts_core/options/series/data/treegraph.py @@ -0,0 +1,223 @@ +from typing import Optional +from decimal import Decimal + +from validator_collection import validators, checkers + +from highcharts_core import constants, errors +from highcharts_core.decorators import class_sensitive +from highcharts_core.options.series.data.base import DataBase +from highcharts_core.options.plot_options.drag_drop import DragDropOptions +from highcharts_core.utility_classes.data_labels import DataLabel +from highcharts_core.utility_classes.buttons import CollapseButtonConfiguration + + +class TreegraphData(DataBase): + """Data point that can feature a ``parent``.""" + + def __init__(self, **kwargs): + self._collapse_button = None + self._collapsed = None + self._color_value = None + self._data_labels = None + self._drag_drop = None + self._drilldown = None + self._parent = None + + self.color_value = kwargs.get('color_value', None) + self.data_labels = kwargs.get('data_labels', None) + self.drag_drop = kwargs.get('drag_drop', None) + self.drilldown = kwargs.get('drilldown', None) + self.parent = kwargs.get('parent', None) + + super().__init__(**kwargs) + + @property + def collapse_button(self) -> Optional[CollapseButtonConfiguration]: + """Options applied to the Collapse Button, which is the small button that indicates the node is collapsible. + + :rtype: :class:`CollapseButtonConfiguration ` + or :obj:`None ` + """ + return self._collapse_button + + @collapse_button.setter + @class_sensitive(CollapseButtonConfiguration) + def collapse_button(self, value): + self._collapse_button = value + + @property + def collapsed(self) -> Optional[bool]: + """If ``True``, the point's children should be hidden. Defaults to :obj:`None `. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._collapsed + + @property + def color_value(self) -> Optional[int]: + """If :meth:`SunburstOptions.color_axis` is set, this property determines which + color should be applied to the data point from the scale of the color axis. + Defaults to :obj:`None `. + + :rtype: :class:`int ` or :obj:`None ` + """ + return self._color_value + + @color_value.setter + def color_value(self, value): + self._color_value = validators.integer(value, allow_empty = True) + + @property + def data_labels(self) -> Optional[DataLabel]: + """Individual data label for the data point. + + :rtype: :class:`DataLabel` or :obj:`None ` + """ + return self._data_labels + + @data_labels.setter + @class_sensitive(DataLabel) + def data_labels(self, value): + self._data_labels = value + + @property + def drag_drop(self) -> Optional[DragDropOptions]: + """The draggable-points module allows points to be moved around or modified in the + chart. + + In addition to the options mentioned under the dragDrop API structure, the module + fires three (JavaScript) events: + + * ``point.dragStart`` + * ``point.drag`` + * ``point.drop`` + + :rtype: :class:`DragDropOptions` or :obj:`None ` + """ + return self._drag_drop + + @drag_drop.setter + @class_sensitive(DragDropOptions) + def drag_drop(self, value): + self._drag_drop = value + + @property + def drilldown(self) -> Optional[str]: + """The :meth:`id ` of a series in the ``drilldown.series`` array + to use as a drilldown destination for this point. Defaults to + :obj:`None `. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._drilldown + + @drilldown.setter + def drilldown(self, value): + self._drilldown = validators.string(value, allow_empty = True) + + @property + def parent(self) -> Optional[str]: + """The :meth:`id ` of the parent data point. If no points match + the value provided, or if set to :obj:`None `, the parent will be set + to the root. Defaults to :obj:`None `. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._parent + + @parent.setter + def parent(self, value): + self._parent = validators.string(value, allow_empty = True) + + @classmethod + def from_array(cls, value): + if not value: + return [] + elif checkers.is_string(value): + try: + value = validators.json(value) + except (ValueError, TypeError): + pass + elif not checkers.is_iterable(value): + value = [value] + + collection = [] + for item in value: + if checkers.is_type(item, + 'TreegraphData') or checkers.is_iterable(item, + forbid_literals = (str, bytes, dict)): + as_obj = item + elif checkers.is_dict(item): + as_obj = cls.from_dict(item) + elif item is None or isinstance(item, constants.EnforcedNullType): + as_obj = cls() + else: + raise errors.HighchartsValueError(f'each data point supplied must either ' + f'be a Treemap Data Point or be ' + f'coercable to one. Could not coerce: ' + f'{item}') + + collection.append(as_obj) + + return collection + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + """Convenience method which returns the keyword arguments used to initialize the + class from a Highcharts Javascript-compatible :class:`dict ` object. + + :param as_dict: The HighCharts JS compatible :class:`dict ` + representation of the object. + :type as_dict: :class:`dict ` + + :returns: The keyword arguments that would be used to initialize an instance. + :rtype: :class:`dict ` + + """ + kwargs = { + 'accessibility': as_dict.get('accessibility', None), + 'class_name': as_dict.get('className', None), + 'color': as_dict.get('color', None), + 'color_index': as_dict.get('colorIndex', None), + 'custom': as_dict.get('custom', None), + 'description': as_dict.get('description', None), + 'events': as_dict.get('events', None), + 'id': as_dict.get('id', None), + 'label_rank': as_dict.get('labelrank', None), + 'name': as_dict.get('name', None), + 'selected': as_dict.get('selected', None), + + 'collapse_button': as_dict.get('collapseButton', None), + 'color_value': as_dict.get('colorValue', None), + 'data_labels': as_dict.get('dataLabels', None), + 'drag_drop': as_dict.get('dragDrop', None), + 'drilldown': as_dict.get('drilldown', None), + 'parent': as_dict.get('parent', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'collapseButton': self.collapse_button, + 'collapsed': self.collapsed, + 'colorValue': self.color_value, + 'dataLabels': self.data_labels, + 'dragDrop': self.drag_drop, + 'drilldown': self.drilldown, + 'parent': self.parent, + + 'accessibility': self.accessibility, + 'className': self.class_name, + 'color': self.color, + 'colorIndex': self.color_index, + 'custom': self.custom, + 'description': self.description, + 'events': self.events, + 'id': self.id, + 'labelrank': self.label_rank, + 'name': self.name, + 'selected': self.selected + } + + return untrimmed diff --git a/highcharts_core/options/series/series_generator.py b/highcharts_core/options/series/series_generator.py index 06caef53..f6b6dd4d 100644 --- a/highcharts_core/options/series/series_generator.py +++ b/highcharts_core/options/series/series_generator.py @@ -2,8 +2,6 @@ import json -from validator_collection import validators, checkers - from highcharts_core import errors from highcharts_core.options.series.base import SeriesBase @@ -54,6 +52,7 @@ from highcharts_core.options.series.spline import SplineSeries from highcharts_core.options.series.sunburst import SunburstSeries from highcharts_core.options.series.timeline import TimelineSeries +from highcharts_core.options.series.treegraph import TreegraphSeries from highcharts_core.options.series.treemap import TreemapSeries from highcharts_core.options.series.vector import VectorSeries from highcharts_core.options.series.venn import VennSeries @@ -108,6 +107,7 @@ 'spline': SplineSeries, 'sunburst': SunburstSeries, 'timeline': TimelineSeries, + 'treegraph': TreegraphSeries, 'treemap': TreemapSeries, 'vector': VectorSeries, 'venn': VennSeries, diff --git a/highcharts_core/options/series/treegraph.py b/highcharts_core/options/series/treegraph.py new file mode 100644 index 00000000..9d76dba2 --- /dev/null +++ b/highcharts_core/options/series/treegraph.py @@ -0,0 +1,136 @@ +from typing import Optional, List + +from highcharts_core.options.series.base import SeriesBase +from highcharts_core.options.series.data.treegraph import TreegraphData +from highcharts_core.options.plot_options.treegraph import TreegraphOptions +from highcharts_core.utility_functions import mro__to_untrimmed_dict + + +class TreegraphSeries(SeriesBase, TreegraphOptions): + """General options to apply to all :term:`Treegraph` series types. + + A treegraph visualizes a relationship between ancestors and descendants with a clear parent-child relationship, + e.g. a family tree or a directory structure. + + .. figure:: ../../../_static/treegraph-example.png + :alt: Treegraph Example Chart + :align: center + + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + @property + def data(self) -> Optional[List[TreegraphData]]: + """Collection of data that represents the series. Defaults to + :obj:`None `. + + While the series type returns a collection of :class:`TreegraphData` instances, + it accepts as input: + + .. tabs:: + + .. tab:: 1D Array of Arrays + + A one-dimensional collection where each member of the collection is itself + a collection of data points. + + .. note:: + + If using the Array of Arrays pattern you *must* set + :meth:`.keys ` to indicate + which value in the inner array corresponds to + :meth:`.id `, + :meth:`.parent `, or + :meth:`.name `. + + .. tab:: Object Collection + + A one-dimensional collection of :class:`TreegraphData` objects or + :class:`dict ` instances coercable to :class:`TreegraphData` + + :rtype: :class:`list ` of :class:`TreegraphData` or + :obj:`None ` + """ + return self._data + + @data.setter + def data(self, value): + if not value: + self._data = None + else: + self._data = TreegraphData.from_array(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), + '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), + 'states': as_dict.get('states', None), + 'sticky_tracking': as_dict.get('stickyTracking', None), + 'tooltip': as_dict.get('tooltip', None), + 'turbo_threshold': as_dict.get('turboThreshold', None), + 'visible': as_dict.get('visible', None), + + 'animation_limit': as_dict.get('animationLimit', None), + 'boost_blending': as_dict.get('boostBlending', None), + 'boost_threshold': as_dict.get('boostThreshold', None), + 'color_index': as_dict.get('colorIndex', None), + 'crisp': as_dict.get('crisp', None), + 'crop_threshold': as_dict.get('cropThreshold', None), + 'find_nearest_point_by': as_dict.get('findNearestPointBy', None), + 'get_extremes_from_all': as_dict.get('getExtremesFromAll', None), + 'relative_x_value': as_dict.get('relativeXValue', None), + 'soft_threshold': as_dict.get('softThreshold', None), + 'step': as_dict.get('step', None), + + 'point_interval': as_dict.get('pointInterval', None), + 'point_interval_unit': as_dict.get('pointIntervalUnit', None), + 'point_start': as_dict.get('pointStart', None), + 'stacking': as_dict.get('stacking', None), + + 'allow_traversing_tree': as_dict.get('allowTraversingTree', None), + 'collapse_button': as_dict.get('collapseButton', None), + 'color_by_point': as_dict.get('colorByPoint', None), + 'link': as_dict.get('link', None), + 'reversed': as_dict.get('reversed', 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), + + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = mro__to_untrimmed_dict(self, in_cls = in_cls) or {} + + return untrimmed diff --git a/highcharts_core/utility_classes/buttons.py b/highcharts_core/utility_classes/buttons.py index 7aaf3c3b..2455ff7f 100644 --- a/highcharts_core/utility_classes/buttons.py +++ b/highcharts_core/utility_classes/buttons.py @@ -169,6 +169,158 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: return untrimmed +class CollapseButtonConfiguration(HighchartsMeta): + """Configuration options that apply to the Collapse button used in certain series types.""" + + def __init__(self, **kwargs): + self._enabled = None + self._height = None + self._only_on_hover = None + self._shape = None + self._width = None + self._x = None + self._y = None + + self.enabled = kwargs.get('enabled', None) + self.height = kwargs.get('height', None) + self.only_on_hover = kwargs.get('only_on_hover', None) + self.shape = kwargs.get('shape', None) + self.width = kwargs.get('width', None) + self.x = kwargs.get('x', None) + self.y = kwargs.get('y', None) + + @property + def enabled(self) -> Optional[bool]: + """If ``True``, displays the button. If ``False``, the button will be hidden. + + Defaults to ``True``. + + :returns: Flag indicating whether the button is displayed on the chart. + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._enabled + + @enabled.setter + def enabled(self, value): + if value is None: + self._enabled = None + else: + self._enabled = bool(value) + + @property + def height(self) -> Optional[int | float | Decimal]: + """The height of the button, expressed in pixels. Defaults to ``10``. + + :rtype: numeric or :obj:`None ` + """ + return self._height + + @height.setter + def height(self, value): + if value is None: + self._height = None + else: + self._height = validators.numeric(value, + allow_empty = False, + minimum = 0) + + @property + def only_on_hover(self) -> Optional[bool]: + """Whether the button should be visible only when the node is hovered. Defaults to ``True``. + + .. note:: + + When set to ``True``, the button is hidden for uncollapsed nodes and shown for collapsed nodes. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._only_on_hover + + @only_on_hover.setter + def only_on_hover(self, value): + if value is None: + self._only_on_hover = None + else: + self._only_on_hover = bool(value) + + @property + def shape(self) -> Optional[str]: + """The symbol to use on the collapse button. Defaults to ``'circle'``. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._shape + + @shape.setter + def shape(self, value): + self._shape = validators.string(value, allow_empty = True) + + @property + def width(self) -> Optional[int | float | Decimal]: + """The width of the button, expressed in pixels. Defaults to ``10``. + + :rtype: numeric or :obj:`None ` + """ + return self._width + + @width.setter + def width(self, value): + self._width = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def x(self) -> Optional[int | float | Decimal]: + """The horizontal offset of the button's position. Defaults to ``0``. + + :rtype: numeric or :obj:`None ` + """ + return self._x + + @x.setter + def x(self, value): + self._x = validators.numeric(value, allow_empty = True) + + @property + def y(self) -> Optional[int | float | Decimal]: + """The vertical offset of the button's position. Defaults to ``0``. + + :rtype: numeric or :obj:`None ` + """ + return self._y + + @y.setter + def y(self, value): + self._y = validators.numeric(value, allow_empty = True) + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'enabled': as_dict.get('enabled', None), + 'height': as_dict.get('height', None), + 'only_on_hover': as_dict.get('onlyOnHover', None), + 'shape': as_dict.get('shape', None), + 'width': as_dict.get('width', None), + 'x': as_dict.get('x', None), + 'y': as_dict.get('y', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'enabled': self.enabled, + 'height': self.height, + 'onlyOnHover': self.only_on_hover, + 'shape': self.shape, + 'width': self.width, + 'x': self.x, + 'y': self.y + } + + return untrimmed + + class ContextButtonConfiguration(ButtonConfiguration): """Configuration options that apply to the Context Menu button.""" diff --git a/tests/input_files/plot_options/treegraph/01.js b/tests/input_files/plot_options/treegraph/01.js new file mode 100644 index 00000000..b8e4bd0c --- /dev/null +++ b/tests/input_files/plot_options/treegraph/01.js @@ -0,0 +1,22 @@ +{ + type: 'treegraph', + animationLimit: 10, + boostBlending: 'some-value-goes-here', + boostThreshold: 5000, + colorIndex: 2, + crisp: true, + cropThreshold: 500, + findNearestPointBy: 'xy', + getExtremesFromAll: true, + relativeXValue: true, + softThreshold: true, + step: 'left', + + pointInterval: 5, + pointIntervalUnit: 'day', + pointStart: 1, + stacking: 'normal', + + allowTraversingTree: true, + colorByPoint: true +} \ No newline at end of file diff --git a/tests/input_files/plot_options/treegraph/02.js b/tests/input_files/plot_options/treegraph/02.js new file mode 100644 index 00000000..b298bd6d --- /dev/null +++ b/tests/input_files/plot_options/treegraph/02.js @@ -0,0 +1,262 @@ +{ + accessibility: { + description: 'Description goes here', + enabled: true, + exposeAsGroupOnly: true, + keyboardNavigation: { + enabled: true + }, + point: { + dateFormat: 'format string', + dateFormatter: function() { return true; }, + describeNull: false, + descriptionFormatter: function() { return true; }, + valueDecimals: 2, + valueDescriptionFormat: 'format string', + valuePrefix: '$', + valueSuffix: 'USD' + }, + }, + allowPointSelect: true, + animation: { + defer: 5 + }, + className: 'some-class-name', + clip: false, + color: '#fff', + cursor: 'alias', + custom: { + 'item1': 'some value', + 'item2': 'some value' + }, + dashStyle: 'Dash', + dataLabels: { + align: 'center', + allowOverlap: true, + animation: { + defer: 5 + }, + backgroundColor: { + linearGradient: { + x1: 0.123, + x2: 0.234, + y1: 0.345, + y2: 0.456 + }, + stops: [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + borderColor: '#999999', + borderRadius: 24, + borderWidth: 1, + className: 'some-class-name', + color: '#000000', + crop: true, + defer: false, + enabled: true, + filter: { + operator: '>=', + property: 'some_property', + value: 123 + }, + format: 'some format', + formatter: function() { return true; }, + inside: true, + nullFormat: 'some format', + nullFormatter: function() { return true; }, + overflow: 'none', + padding: 12, + position: 'center', + rotation: 0, + shadow: false, + shape: 'rect', + style: 'style goes here', + useHTML: false, + verticalAlign: 'top', + x: 10, + y: 20, + z: 0 + }, + description: 'Description goes here', + enableMouseTracking: true, + events: { + afterAnimate: function(event) { return true; }, + click: function(event) { return true; }, + hide: function(event) { return true; }, + mouseOut: function(event) { return true; }, + show: function(event) { return true; } + }, + includeInDataExport: true, + keys: [ + 'somevalue', + 'somevalue', + 'somevalue' + ], + label: { + boxesToAvoid: [ + { + bottom: 12, + left: -46, + right: 84, + top: 24 + }, + { + bottom: 48, + left: -46, + right: 84, + top: 86 + } + ], + connectorAllowed: true, + connectorNeighbourDistance: 12, + enabled: true, + format: 'format string', + formatter: function() { return true; }, + maxFontSize: 18, + minFontSize: 6, + onArea: false, + style: 'some style string' + }, + linkedTo: 'some_id', + marker: { + enabled: true, + fillColor: '#cccccc', + height: 24, + lineWidth: 2, + radius: 2, + states: { + hover: { + enabled: true + } + }, + symbol: 'circle', + width: 48 + }, + onPoint: { + connectorOptions: { + dashstyle: 'Dash', + stroke: '#ccc', + width: 2 + }, + id: 'some-id', + position: { + align: 'left', + verticalAlign: 'top', + x: 15, + y: -46 + } + }, + opacity: 0.2, + point: { + events: { + click: function(event) { return true; }, + drag: function(event) { return true; }, + drop: function(event) { return true; }, + mouseOut: function(event) { return true; } + } + }, + pointDescriptionFormatter: function (point) { return true; }, + selected: false, + showCheckbox: true, + showInLegend: true, + skipKeyboardNavigation: false, + states: { + hover: { + animation: { + duration: 123 + }, + borderColor: '#cccccc', + brightness: 0.3, + enabled: true + }, + inactive: { + enabled: true, + opacity: 0.5 + }, + normal: { + animation: { + defer: 24 + } + }, + select: { + color: '#ff0000', + enabled: true + } + }, + stickyTracking: true, + tooltip: { + animation: true, + backgroundColor: '#ccc', + borderColor: '#999', + borderRadius: 4, + borderWidth: 1, + className: 'some-class-name', + clusterFormat: 'format string', + dateTimeLabelFormats: { + day: 'test', + hour: 'test', + millisecond: 'test', + minute: 'test', + month: 'test', + second: 'test', + week: 'test', + year: 'test' + }, + distance: 12, + enabled: true, + followPointer: true, + followTouchMove: true, + footerFormat: 'format string', + formatter: function() { return true; }, + headerFormat: 'format string', + headerShape: 'circle', + hideDelay: 3, + nullFormat: 'format string', + nullFormatter: function() { return true; }, + outside: false, + padding: 6, + pointFormat: 'format string', + pointFormatter: function() { return true; }, + positioner: function() { return true; }, + shadow: false, + shape: 'rect', + shared: false, + snap: 4, + split: false, + stickOnContact: true, + style: 'style string goes here', + useHTML: false, + valueDecimals: 2, + valuePrefix: '$', + valueSuffix: ' USD', + xDateFormat: 'format string' + }, + turboThreshold: 456, + visible: true, + type: 'treegraph', + + animationLimit: 10, + boostBlending: 'some-value-goes-here', + boostThreshold: 5000, + colorIndex: 2, + crisp: true, + cropThreshold: 500, + findNearestPointBy: 'xy', + getExtremesFromAll: true, + relativeXValue: true, + softThreshold: true, + step: 'left', + + pointInterval: 5, + pointIntervalUnit: 'day', + pointStart: 1, + stacking: 'normal', + + allowTraversingTree: true, + collapseButton: { + enabled: true + }, + colorByPoint: true +} \ No newline at end of file diff --git a/tests/input_files/plot_options/treegraph/error-00.js b/tests/input_files/plot_options/treegraph/error-00.js new file mode 100644 index 00000000..ae670c0c --- /dev/null +++ b/tests/input_files/plot_options/treegraph/error-00.js @@ -0,0 +1 @@ +not a valid JavaScript file diff --git a/tests/input_files/plot_options/treegraph/error-01.js b/tests/input_files/plot_options/treegraph/error-01.js new file mode 100644 index 00000000..8f969091 --- /dev/null +++ b/tests/input_files/plot_options/treegraph/error-01.js @@ -0,0 +1,98 @@ +{ + allowTraversingTree: true, + alternateStartingDirection: false, + animationLimit: 10, + boostBlending: 'some-value-goes-here', + boostThreshold: 'invalid value', + breadcrumbs: { + buttonSpacing: 6, + buttonTheme: { + 'fill': '#fff' + }, + events: { + click: function(event) { return true; } + }, + floating: true, + format: 'some format string', + formatter: function () { return true; }, + position: None, + relativeTo: 'plot', + rtl: false, + separator: { + style: { + 'some-key': 'some-value' + }, + text: '>' + }, + useHTML: false, + zIndex: 3 + }, + colorAxis: 'some-id-goes-here', + colorByPoint: true, + colorIndex: 2, + colorKey: 'some-key-goes-here', + colors: [ + '#ccc', + '#fff', + '000000' + ], + crisp: true, + cropThreshold: 500, + findNearestPointBy: 'xy', + getExtremesFromAll: true, + ignoreHiddenPoint: true, + interactByLeaf: true, + layoutAlgorithm: 'sliceAndDice', + layoutStartingDirection: 'vertical', + levelIsConstant: true, + levels: [ + { + borderDashStyle: 'Solid', + color: '#ccc', + colorVariation: { + key: 'brightness', + to: 50 + }, + layoutAlgorithm: 'sliceAndDice', + layoutStartingDirection: 'vertical', + borderColor: '#ccc', + borderWidth: 1, + level: 1, + } + ], + linecap: 'round', + lineWidth: 1, + negativeColor: '#ccc', + pointInterval: 5, + pointIntervalUnit: 'day', + pointStart: 1, + relativeXVvalue: true, + softThreshold: true, + sortIndex: 2, + stacking: 'normal', + step: 'left', + zoneAxis: 'y', + zones: [ + { + className: 'some-class-name1', + color: '#999999', + dashStyle: 'Solid', + fillColor: '#cccccc', + value: 123 + }, + { + className: 'some-class-name1', + color: '#999999', + dashStyle: 'Solid', + fillColor: '#cccccc', + value: 123 + }, + { + className: 'some-class-name1', + color: '#999999', + dashStyle: 'Solid', + fillColor: '#cccccc', + value: 123 + } + ] +} diff --git a/tests/input_files/plot_options/treegraph/error-02.js b/tests/input_files/plot_options/treegraph/error-02.js new file mode 100644 index 00000000..7f305c24 --- /dev/null +++ b/tests/input_files/plot_options/treegraph/error-02.js @@ -0,0 +1,336 @@ +{ + allowTraversingTree: true, + alternateStartingDirection: false, + animationLimit: 10, + boostBlending: 'some-value-goes-here', + boostThreshold: 'invalid value', + breadcrumbs: { + buttonSpacing: 6, + buttonTheme: { + 'fill': '#fff' + }, + events: { + click: function(event) { return true; } + }, + floating: true, + format: 'some format string', + formatter: function () { return true; }, + position: None, + relativeTo: 'plot', + rtl: false, + separator: { + style: { + 'some-key': 'some-value' + }, + text: '>' + }, + useHTML: false, + zIndex: 3 + }, + colorAxis: 'some-id-goes-here', + colorByPoint: true, + colorIndex: 2, + colorKey: 'some-key-goes-here', + colors: [ + '#ccc', + '#fff', + '000000' + ], + crisp: true, + cropThreshold: 500, + findNearestPointBy: 'xy', + getExtremesFromAll: true, + ignoreHiddenPoint: true, + interactByLeaf: true, + layoutAlgorithm: 'sliceAndDice', + layoutStartingDirection: 'vertical', + levelIsConstant: true, + levels: [ + { + borderDashStyle: 'Solid', + color: '#ccc', + colorVariation: { + key: 'brightness', + to: 50 + }, + layoutAlgorithm: 'sliceAndDice', + layoutStartingDirection: 'vertical', + borderColor: '#ccc', + borderWidth: 1, + level: 1, + } + ], + linecap: 'round', + lineWidth: 1, + negativeColor: '#ccc', + pointInterval: 5, + pointIntervalUnit: 'day', + pointStart: 1, + relativeXVvalue: true, + softThreshold: true, + sortIndex: 2, + stacking: 'normal', + step: 'left', + zoneAxis: 'y', + zones: [ + { + className: 'some-class-name1', + color: '#999999', + dashStyle: 'Solid', + fillColor: '#cccccc', + value: 123 + }, + { + className: 'some-class-name1', + color: '#999999', + dashStyle: 'Solid', + fillColor: '#cccccc', + value: 123 + }, + { + className: 'some-class-name1', + color: '#999999', + dashStyle: 'Solid', + fillColor: '#cccccc', + value: 123 + } + ], + + accessibility: { + description: 'Description goes here', + enabled: true, + exposeAsGroupOnly: true, + keyboardNavigation: { + enabled: true + }, + point: { + dateFormat: 'format string', + dateFormatter: function() { return true; }, + describeNull: false, + descriptionFormatter: function() { return true; }, + valueDecimals: 2, + valueDescriptionFormat: 'format string', + valuePrefix: '$', + valueSuffix: 'USD' + }, + }, + allowPointSelect: true, + animation: { + defer: 5 + }, + className: 'some-class-name', + clip: false, + color: '#fff', + cursor: 'alias', + custom: { + 'item1': 'some value', + 'item2': 'some value' + }, + dashStyle: 'Dash', + dataLabels: { + align: 'center', + allowOverlap: true, + animation: { + defer: 5 + }, + backgroundColor: { + linearGradient: { + x1: 0.123, + x2: 0.234, + y1: 0.345, + y2: 0.456 + }, + stops: [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + borderColor: '#999999', + borderRadius: 24, + borderWidth: 1, + className: 'some-class-name', + color: '#000000', + crop: true, + defer: false, + enabled: true, + filter: { + operator: '>=', + property: 'some_property', + value: 123 + }, + format: 'some format', + formatter: function() { return true; }, + inside: true, + nullFormat: 'some format', + nullFormatter: function() { return true; }, + overflow: 'none', + padding: 12, + position: 'center', + rotation: 0, + shadow: false, + shape: 'rect', + style: 'style goes here', + useHTML: false, + verticalAlign: 'top', + x: 10, + y: 20, + z: 0 + }, + description: 'Description goes here', + enableMouseTracking: true, + events: { + afterAnimate: function(event) { return true; }, + click: function(event) { return true; }, + hide: function(event) { return true; }, + mouseOut: function(event) { return true; }, + show: function(event) { return true; } + }, + includeInDataExport: true, + keys: [ + 'somevalue', + 'somevalue', + 'somevalue' + ], + label: { + boxesToAvoid: [ + { + bottom: 12, + left: -46, + right: 84, + top: 24 + }, + { + bottom: 48, + left: -46, + right: 84, + top: 86 + } + ], + connectorAllowed: true, + connectorNeighbourDistance: 12, + enabled: true, + format: 'format string', + formatter: function() { return true; }, + maxFontSize: 18, + minFontSize: 6, + onArea: false, + style: 'some style string' + }, + linkedTo: 'some_id', + marker: { + enabled: true, + fillColor: '#cccccc', + height: 24, + lineWidth: 2, + radius: 2, + states: { + hover: { + enabled: true + } + }, + symbol: 'circle', + width: 48 + }, + onPoint: { + connectorOptions: { + dashstyle: 'Dash', + stroke: '#ccc', + width: 2 + }, + id: 'some-id', + position: { + align: 'left', + verticalAlign: 'top', + x: 15, + y: -46 + } + }, + opacity: 0.2, + point: { + events: { + click: function(event) { return true; }, + drag: function(event) { return true; }, + drop: function(event) { return true; }, + mouseOut: function(event) { return true; } + } + }, + pointDescriptionFormatter: function (point) { return true; }, + selected: false, + showCheckbox: true, + showInLegend: true, + skipKeyboardNavigation: false, + states: { + hover: { + animation: { + duration: 123 + }, + borderColor: '#cccccc', + brightness: 0.3, + enabled: true + }, + inactive: { + enabled: true, + opacity: 0.5 + }, + normal: { + animation: { + defer: 24 + } + }, + select: { + color: '#ff0000', + enabled: true + } + }, + stickyTracking: true, + threshold: 123, + tooltip: { + animation: true, + backgroundColor: '#ccc', + borderColor: '#999', + borderRadius: 4, + borderWidth: 1, + className: 'some-class-name', + clusterFormat: 'format string', + dateTimeLabelFormats: { + day: 'test', + hour: 'test', + millisecond: 'test', + minute: 'test', + month: 'test', + second: 'test', + week: 'test', + year: 'test' + }, + distance: 12, + enabled: true, + followPointer: true, + followTouchMove: true, + footerFormat: 'format string', + formatter: function() { return true; }, + headerFormat: 'format string', + headerShape: 'circle', + hideDelay: 3, + nullFormat: 'format string', + nullFormatter: function() { return true; }, + outside: false, + padding: 6, + pointFormat: 'format string', + pointFormatter: function() { return true; }, + positioner: function() { return true; }, + shadow: false, + shape: 'rect', + shared: false, + snap: 4, + split: false, + stickOnContact: true, + style: 'style string goes here', + useHTML: false, + valueDecimals: 2, + valuePrefix: '$', + valueSuffix: ' USD', + xDateFormat: 'format string' + }, + turboThreshold: 456, + visible: true +} diff --git a/tests/input_files/series/treegraph/01.js b/tests/input_files/series/treegraph/01.js new file mode 100644 index 00000000..3324324c --- /dev/null +++ b/tests/input_files/series/treegraph/01.js @@ -0,0 +1,129 @@ +{ + data: [ + { + colorValue: 2, + dataLabels: { + align: 'center', + allowOverlap: true, + animation: { + defer: 5 + }, + backgroundColor: { + linearGradient: { + x1: 0.123, + x2: 0.234, + y1: 0.345, + y2: 0.456 + }, + stops: [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + borderColor: '#999999', + borderRadius: 24, + borderWidth: 1, + className: 'some-class-name', + color: '#000000', + crop: true, + defer: false, + enabled: true, + filter: { + operator: '>=', + property: 'some_property', + value: 123 + }, + format: 'some format', + formatter: function() { return true; }, + inside: true, + nullFormat: 'some format', + nullFormatter: function() { return true; }, + overflow: 'none', + padding: 12, + position: 'center', + rotation: 0, + shadow: false, + shape: 'rect', + style: 'style goes here', + useHTML: false, + verticalAlign: 'top', + x: 10, + y: 20, + z: 0 + }, + dragDrop: { + draggableX: true, + draggableY: true, + dragHandle: { + className: 'draghandle-classname-goes-here', + color: '#ccc', + cursor: 'alias', + lineColor: '#ddd', + lineWidth: 2, + pathFormatter: function() { return true; }, + zIndex: 10 + }, + dragMaxX: 3456, + dragMaxY: 6532, + dragMinX: 123, + dragMinY: 321, + dragPrecisionX: 5, + dragPrecisionY: 5, + dragSensitivity: 2, + groupBy: 'some-property-name', + guideBox: { + default: { + className: 'some-classname-goes-here', + color: '#999', + cursor: 'pointer', + lineColor: '#ccc', + lineWidth: 2, + zIndex: 100 + } + }, + liveRedraw: true + }, + drilldown: 'some-id-goes-here', + parent: 'some-id-goes-here', + + accessibility: { + description: 'Some description goes here', + enabled: true + }, + className: 'some-class-name', + color: '#ccc', + colorIndex: 2, + custom: { + 'some_key': 123, + 'other_key': 456 + }, + description: 'Some description goes here', + events: { + click: function(event) { return true; }, + drag: function(event) { return true; }, + drop: function(event) { return true; }, + mouseOut: function(event) { return true; } + }, + id: 'some-id-goes-here', + labelrank: 3, + name: 'Some Name Goes here', + selected: false + } + ], + pointInterval: 5, + pointIntervalUnit: 'day', + pointStart: 1, + stacking: 'normal', + animationLimit: 10, + boostBlending: 'some-value-goes-here', + boostThreshold: 5000, + colorIndex: 2, + crisp: true, + cropThreshold: 500, + findNearestPointBy: 'xy', + getExtremesFromAll: true, + relativeXValue: true, + softThreshold: true, + step: 'left', + type: 'treegraph' +} \ No newline at end of file diff --git a/tests/input_files/series/treegraph/02.js b/tests/input_files/series/treegraph/02.js new file mode 100644 index 00000000..2ed77e43 --- /dev/null +++ b/tests/input_files/series/treegraph/02.js @@ -0,0 +1,7 @@ +{ + data: [ + ['id1'], + ['id2', 'id1'] + ], + keys: ['id', 'parent'] +} diff --git a/tests/input_files/series/treegraph/error-00.js b/tests/input_files/series/treegraph/error-00.js new file mode 100644 index 00000000..ae670c0c --- /dev/null +++ b/tests/input_files/series/treegraph/error-00.js @@ -0,0 +1 @@ +not a valid JavaScript file diff --git a/tests/input_files/series/treegraph/error-01.js b/tests/input_files/series/treegraph/error-01.js new file mode 100644 index 00000000..3ae45b11 --- /dev/null +++ b/tests/input_files/series/treegraph/error-01.js @@ -0,0 +1,211 @@ +{ + data: [ + 'invalid value', + { + colorValue: 2, + dataLabels: { + align: 'center', + allowOverlap: true, + animation: { + defer: 5 + }, + backgroundColor: { + linearGradient: { + x1: 0.123, + x2: 0.234, + y1: 0.345, + y2: 0.456 + }, + stops: [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + borderColor: '#999999', + borderRadius: 24, + borderWidth: 1, + className: 'some-class-name', + color: '#000000', + crop: true, + defer: false, + enabled: true, + filter: { + operator: '>=', + property: 'some_property', + value: 123 + }, + format: 'some format', + formatter: function() { return true; }, + inside: true, + nullFormat: 'some format', + nullFormatter: function() { return true; }, + overflow: 'none', + padding: 12, + position: 'center', + rotation: 0, + shadow: false, + shape: 'rect', + style: 'style goes here', + useHTML: false, + verticalAlign: 'top', + x: 10, + y: 20, + z: 0 + }, + dragDrop: { + draggableX: true, + draggableY: true, + dragHandle: { + className: 'draghandle-classname-goes-here', + color: '#ccc', + cursor: 'alias', + lineColor: '#ddd', + lineWidth: 2, + pathFormatter: function() { return true; }, + zIndex: 10 + }, + dragMaxX: 3456, + dragMaxY: 6532, + dragMinX: 123, + dragMinY: 321, + dragPrecisionX: 5, + dragPrecisionY: 5, + dragSensitivity: 2, + groupBy: 'some-property-name', + guideBox: { + default: { + className: 'some-classname-goes-here', + color: '#999', + cursor: 'pointer', + lineColor: '#ccc', + lineWidth: 2, + zIndex: 100 + } + }, + liveRedraw: true + }, + drilldown: 'some-id-goes-here', + parent: 'some-id-goes-here', + value: 123.45, + + accessibility: { + description: 'Some description goes here', + enabled: true + }, + className: 'some-class-name', + color: '#ccc', + colorIndex: 2, + custom: { + 'some_key': 123, + 'other_key': 456 + }, + description: 'Some description goes here', + events: { + click: function(event) { return true; }, + drag: function(event) { return true; }, + drop: function(event) { return true; }, + mouseOut: function(event) { return true; } + }, + id: 'some-id-goes-here', + labelrank: 3, + name: 'Some Name Goes here', + selected: false + } + ], + allowTraversingTree: true, + alternateStartingDirection: false, + animationLimit: 10, + boostBlending: 'some-value-goes-here', + boostThreshold: 'invalid value', + breadcrumbs: { + buttonSpacing: 6, + buttonTheme: { + 'fill': '#fff' + }, + events: { + click: function(event) { return true; } + }, + floating: true, + format: 'some format string', + formatter: function () { return true; }, + position: None, + relativeTo: 'plot', + rtl: false, + separator: { + style: { + 'some-key': 'some-value' + }, + text: '>' + }, + useHTML: false, + zIndex: 3 + }, + colorAxis: 'some-id-goes-here', + colorByPoint: true, + colorIndex: 2, + colorKey: 'some-key-goes-here', + colors: [ + '#ccc', + '#fff', + '000000' + ], + crisp: true, + cropThreshold: 500, + findNearestPointBy: 'xy', + getExtremesFromAll: true, + ignoreHiddenPoint: true, + interactByLeaf: true, + layoutAlgorithm: 'sliceAndDice', + layoutStartingDirection: 'vertical', + levelIsConstant: true, + levels: [ + { + borderDashStyle: 'Solid', + color: '#ccc', + colorVariation: { + key: 'brightness', + to: 50 + }, + layoutAlgorithm: 'sliceAndDice', + layoutStartingDirection: 'vertical', + borderColor: '#ccc', + borderWidth: 1, + level: 1, + } + ], + linecap: 'round', + lineWidth: 1, + negativeColor: '#ccc', + pointInterval: 5, + pointIntervalUnit: 'day', + pointStart: 1, + relativeXVvalue: true, + softThreshold: true, + sortIndex: 2, + stacking: 'normal', + step: 'left', + zoneAxis: 'y', + zones: [ + { + className: 'some-class-name1', + color: '#999999', + dashStyle: 'Solid', + fillColor: '#cccccc', + value: 123 + }, + { + className: 'some-class-name1', + color: '#999999', + dashStyle: 'Solid', + fillColor: '#cccccc', + value: 123 + }, + { + className: 'some-class-name1', + color: '#999999', + dashStyle: 'Solid', + fillColor: '#cccccc', + value: 123 + } + ] +} diff --git a/tests/input_files/series/treegraph/error-02.js b/tests/input_files/series/treegraph/error-02.js new file mode 100644 index 00000000..55685bef --- /dev/null +++ b/tests/input_files/series/treegraph/error-02.js @@ -0,0 +1,449 @@ +{ + data: [ + 'invalid value', + { + colorValue: 2, + dataLabels: { + align: 'center', + allowOverlap: true, + animation: { + defer: 5 + }, + backgroundColor: { + linearGradient: { + x1: 0.123, + x2: 0.234, + y1: 0.345, + y2: 0.456 + }, + stops: [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + borderColor: '#999999', + borderRadius: 24, + borderWidth: 1, + className: 'some-class-name', + color: '#000000', + crop: true, + defer: false, + enabled: true, + filter: { + operator: '>=', + property: 'some_property', + value: 123 + }, + format: 'some format', + formatter: function() { return true; }, + inside: true, + nullFormat: 'some format', + nullFormatter: function() { return true; }, + overflow: 'none', + padding: 12, + position: 'center', + rotation: 0, + shadow: false, + shape: 'rect', + style: 'style goes here', + useHTML: false, + verticalAlign: 'top', + x: 10, + y: 20, + z: 0 + }, + dragDrop: { + draggableX: true, + draggableY: true, + dragHandle: { + className: 'draghandle-classname-goes-here', + color: '#ccc', + cursor: 'alias', + lineColor: '#ddd', + lineWidth: 2, + pathFormatter: function() { return true; }, + zIndex: 10 + }, + dragMaxX: 3456, + dragMaxY: 6532, + dragMinX: 123, + dragMinY: 321, + dragPrecisionX: 5, + dragPrecisionY: 5, + dragSensitivity: 2, + groupBy: 'some-property-name', + guideBox: { + default: { + className: 'some-classname-goes-here', + color: '#999', + cursor: 'pointer', + lineColor: '#ccc', + lineWidth: 2, + zIndex: 100 + } + }, + liveRedraw: true + }, + drilldown: 'some-id-goes-here', + parent: 'some-id-goes-here', + value: 123.45, + + accessibility: { + description: 'Some description goes here', + enabled: true + }, + className: 'some-class-name', + color: '#ccc', + colorIndex: 2, + custom: { + 'some_key': 123, + 'other_key': 456 + }, + description: 'Some description goes here', + events: { + click: function(event) { return true; }, + drag: function(event) { return true; }, + drop: function(event) { return true; }, + mouseOut: function(event) { return true; } + }, + id: 'some-id-goes-here', + labelrank: 3, + name: 'Some Name Goes here', + selected: false + } + ], + allowTraversingTree: true, + alternateStartingDirection: false, + animationLimit: 10, + boostBlending: 'some-value-goes-here', + boostThreshold: 'invalid value', + breadcrumbs: { + buttonSpacing: 6, + buttonTheme: { + 'fill': '#fff' + }, + events: { + click: function(event) { return true; } + }, + floating: true, + format: 'some format string', + formatter: function () { return true; }, + position: None, + relativeTo: 'plot', + rtl: false, + separator: { + style: { + 'some-key': 'some-value' + }, + text: '>' + }, + useHTML: false, + zIndex: 3 + }, + colorAxis: 'some-id-goes-here', + colorByPoint: true, + colorIndex: 2, + colorKey: 'some-key-goes-here', + colors: [ + '#ccc', + '#fff', + '000000' + ], + crisp: true, + cropThreshold: 500, + findNearestPointBy: 'xy', + getExtremesFromAll: true, + ignoreHiddenPoint: true, + interactByLeaf: true, + layoutAlgorithm: 'sliceAndDice', + layoutStartingDirection: 'vertical', + levelIsConstant: true, + levels: [ + { + borderDashStyle: 'Solid', + color: '#ccc', + colorVariation: { + key: 'brightness', + to: 50 + }, + layoutAlgorithm: 'sliceAndDice', + layoutStartingDirection: 'vertical', + borderColor: '#ccc', + borderWidth: 1, + level: 1, + } + ], + linecap: 'round', + lineWidth: 1, + negativeColor: '#ccc', + pointInterval: 5, + pointIntervalUnit: 'day', + pointStart: 1, + relativeXVvalue: true, + softThreshold: true, + sortIndex: 2, + stacking: 'normal', + step: 'left', + zoneAxis: 'y', + zones: [ + { + className: 'some-class-name1', + color: '#999999', + dashStyle: 'Solid', + fillColor: '#cccccc', + value: 123 + }, + { + className: 'some-class-name1', + color: '#999999', + dashStyle: 'Solid', + fillColor: '#cccccc', + value: 123 + }, + { + className: 'some-class-name1', + color: '#999999', + dashStyle: 'Solid', + fillColor: '#cccccc', + value: 123 + } + ], + + accessibility: { + description: 'Description goes here', + enabled: true, + exposeAsGroupOnly: true, + keyboardNavigation: { + enabled: true + }, + point: { + dateFormat: 'format string', + dateFormatter: function() { return true; }, + describeNull: false, + descriptionFormatter: function() { return true; }, + valueDecimals: 2, + valueDescriptionFormat: 'format string', + valuePrefix: '$', + valueSuffix: 'USD' + }, + }, + allowPointSelect: true, + animation: { + defer: 5 + }, + className: 'some-class-name', + clip: false, + color: '#fff', + cursor: 'alias', + custom: { + 'item1': 'some value', + 'item2': 'some value' + }, + dashStyle: 'Dash', + dataLabels: { + align: 'center', + allowOverlap: true, + animation: { + defer: 5 + }, + backgroundColor: { + linearGradient: { + x1: 0.123, + x2: 0.234, + y1: 0.345, + y2: 0.456 + }, + stops: [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + borderColor: '#999999', + borderRadius: 24, + borderWidth: 1, + className: 'some-class-name', + color: '#000000', + crop: true, + defer: false, + enabled: true, + filter: { + operator: '>=', + property: 'some_property', + value: 123 + }, + format: 'some format', + formatter: function() { return true; }, + inside: true, + nullFormat: 'some format', + nullFormatter: function() { return true; }, + overflow: 'none', + padding: 12, + position: 'center', + rotation: 0, + shadow: false, + shape: 'rect', + style: 'style goes here', + useHTML: false, + verticalAlign: 'top', + x: 10, + y: 20, + z: 0 + }, + description: 'Description goes here', + enableMouseTracking: true, + events: { + afterAnimate: function(event) { return true; }, + click: function(event) { return true; }, + hide: function(event) { return true; }, + mouseOut: function(event) { return true; }, + show: function(event) { return true; } + }, + includeInDataExport: true, + keys: [ + 'somevalue', + 'somevalue', + 'somevalue' + ], + label: { + boxesToAvoid: [ + { + bottom: 12, + left: -46, + right: 84, + top: 24 + }, + { + bottom: 48, + left: -46, + right: 84, + top: 86 + } + ], + connectorAllowed: true, + connectorNeighbourDistance: 12, + enabled: true, + format: 'format string', + formatter: function() { return true; }, + maxFontSize: 18, + minFontSize: 6, + onArea: false, + style: 'some style string' + }, + linkedTo: 'some_id', + marker: { + enabled: true, + fillColor: '#cccccc', + height: 24, + lineWidth: 2, + radius: 2, + states: { + hover: { + enabled: true + } + }, + symbol: 'circle', + width: 48 + }, + onPoint: { + connectorOptions: { + dashstyle: 'Dash', + stroke: '#ccc', + width: 2 + }, + id: 'some-id', + position: { + align: 'left', + verticalAlign: 'top', + x: 15, + y: -46 + } + }, + opacity: 0.2, + point: { + events: { + click: function(event) { return true; }, + drag: function(event) { return true; }, + drop: function(event) { return true; }, + mouseOut: function(event) { return true; } + } + }, + pointDescriptionFormatter: function (point) { return true; }, + selected: false, + showCheckbox: true, + showInLegend: true, + skipKeyboardNavigation: false, + states: { + hover: { + animation: { + duration: 123 + }, + borderColor: '#cccccc', + brightness: 0.3, + enabled: true + }, + inactive: { + enabled: true, + opacity: 0.5 + }, + normal: { + animation: { + defer: 24 + } + }, + select: { + color: '#ff0000', + enabled: true + } + }, + stickyTracking: true, + threshold: 123, + tooltip: { + animation: true, + backgroundColor: '#ccc', + borderColor: '#999', + borderRadius: 4, + borderWidth: 1, + className: 'some-class-name', + clusterFormat: 'format string', + dateTimeLabelFormats: { + day: 'test', + hour: 'test', + millisecond: 'test', + minute: 'test', + month: 'test', + second: 'test', + week: 'test', + year: 'test' + }, + distance: 12, + enabled: true, + followPointer: true, + followTouchMove: true, + footerFormat: 'format string', + formatter: function() { return true; }, + headerFormat: 'format string', + headerShape: 'circle', + hideDelay: 3, + nullFormat: 'format string', + nullFormatter: function() { return true; }, + outside: false, + padding: 6, + pointFormat: 'format string', + pointFormatter: function() { return true; }, + positioner: function() { return true; }, + shadow: false, + shape: 'rect', + shared: false, + snap: 4, + split: false, + stickOnContact: true, + style: 'style string goes here', + useHTML: false, + valueDecimals: 2, + valuePrefix: '$', + valueSuffix: ' USD', + xDateFormat: 'format string' + }, + turboThreshold: 456, + visible: true +} diff --git a/tests/options/plot_options/test_treegraph.py b/tests/options/plot_options/test_treegraph.py new file mode 100644 index 00000000..9e14fcf0 --- /dev/null +++ b/tests/options/plot_options/test_treegraph.py @@ -0,0 +1,354 @@ +"""Tests for ``highcharts.no_data``.""" + +import pytest + +from json.decoder import JSONDecodeError + +from highcharts_core.options.plot_options.treegraph import TreegraphOptions as cls +from highcharts_core import errors +from tests.fixtures import input_files, check_input_file, to_camelCase, to_js_dict, \ + Class__init__, Class__to_untrimmed_dict, Class_from_dict, Class_to_dict, \ + Class_from_js_literal + +STANDARD_PARAMS = [ + ({}, None), + ({ + 'allow_traversing_tree': True, + 'animation_limit': 10, + 'boost_blending': 'some-value-goes-here', + 'boost_threshold': 5000, + 'color_by_point': True, + 'color_index': 2, + 'crisp': True, + 'crop_threshold': 500, + 'find_nearest_point_by': 'xy', + 'get_extremes_from_all': True, + 'point_interval': 5, + 'point_interval_unit': 'day', + 'point_start': 1, + 'relative_x_value': True, + 'soft_threshold': True, + 'stacking': 'normal', + 'step': 'left', + }, None), + # + Generic Options + ({ + 'allow_traversing_tree': True, + 'animation_limit': 10, + 'boost_blending': 'some-value-goes-here', + 'boost_threshold': 5000, + 'color_by_point': True, + 'color_index': 2, + 'crisp': True, + 'crop_threshold': 500, + 'find_nearest_point_by': 'xy', + 'get_extremes_from_all': True, + 'point_interval': 5, + 'point_interval_unit': 'day', + 'point_start': 1, + 'relative_x_value': True, + 'soft_threshold': True, + 'stacking': 'normal', + 'step': 'left', + + 'accessibility': { + 'description': 'Description goes here', + 'enabled': True, + 'exposeAsGroupOnly': True, + 'keyboardNavigation': { + 'enabled': True + }, + 'point': { + 'dateFormat': 'format string', + 'dateFormatter': """function() { return true; }""", + 'describeNull': False, + 'descriptionFormatter': """function() { return true; }""", + 'valueDecimals': 2, + 'valueDescriptionFormat': 'format string', + 'valuePrefix': '$', + 'valueSuffix': 'USD' + }, + }, + 'allow_point_select': True, + 'animation': { + 'defer': 5 + }, + 'class_name': 'some-class-name', + 'clip': False, + 'color': '#fff', + 'cursor': 'alias', + 'custom': { + 'item1': 'some value', + 'item2': 'some value' + }, + 'dash_style': 'Dash', + 'data_labels': { + 'align': 'center', + 'allowOverlap': True, + 'animation': { + 'defer': 5 + }, + 'backgroundColor': { + 'linearGradient': { + 'x1': 0.123, + 'x2': 0.234, + 'y1': 0.345, + 'y2': 0.456 + }, + 'stops': [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + 'borderColor': '#999999', + 'borderRadius': 24, + 'borderWidth': 1, + 'className': 'some-class-name', + 'color': '#000000', + 'crop': True, + 'defer': False, + 'enabled': True, + 'filter': { + 'operator': '>=', + 'property': 'some_property', + 'value': 123 + }, + 'format': 'some format', + 'formatter': """function() { return true; }""", + 'inside': True, + 'nullFormat': 'some format', + 'nullFormatter': """function() { return true; }""", + 'overflow': 'none', + 'padding': 12, + 'position': 'center', + 'rotation': 0, + 'shadow': False, + 'shape': 'rect', + 'style': 'style goes here', + 'useHTML': False, + 'verticalAlign': 'top', + 'x': 10, + 'y': 20, + 'z': 0 + }, + 'description': 'Description goes here', + 'enable_mouse_tracking': True, + 'events': { + 'afterAnimate': """function(event) { return true; }""", + 'click': """function(event) { return true; }""", + 'hide': """function(event) { return true; }""", + 'mouseOut': """function(event) { return true; }""", + 'show': """function(event) { return true; }""" + }, + 'include_in_data_export': True, + 'keys': [ + 'somevalue', + 'somevalue', + 'somevalue' + ], + 'label': { + 'boxesToAvoid': [ + { + 'bottom': 12, + 'left': -46, + 'right': 84, + 'top': 24 + }, + { + 'bottom': 48, + 'left': -46, + 'right': 84, + 'top': 86 + } + ], + 'connectorAllowed': True, + 'connectorNeighbourDistance': 12, + 'enabled': True, + 'format': 'format string', + 'formatter': """function() { return true; }""", + 'maxFontSize': 18, + 'minFontSize': 6, + 'onArea': False, + 'style': 'some style string' + }, + 'linked_to': 'some_id', + 'marker': { + 'enabled': True, + 'fillColor': '#cccccc', + 'height': 24, + 'lineWidth': 2, + 'radius': 2, + 'states': { + 'hover': { + 'enabled': True + } + }, + 'symbol': 'circle', + 'width': 48 + }, + 'on_point': { + 'connectorOptions': { + 'dashstyle': 'Dash', + 'stroke': '#ccc', + 'width': 2 + }, + 'id': 'some-id', + 'position': { + 'align': 'left', + 'verticalAlign': 'top', + 'x': 15, + 'y': -46 + } + }, + 'opacity': 0.2, + 'point': { + 'events': { + 'click': """function(event) { return true; }""", + 'drag': """function(event) { return true; }""", + 'drop': """function(event) { return true; }""", + 'mouseOut': """function(event) { return true; }""" + } + }, + 'point_description_formatter': """function (point) { return true; }""", + 'selected': False, + 'show_checkbox': True, + 'show_in_legend': True, + 'skip_keyboard_navigation': False, + 'states': { + 'hover': { + 'animation': { + 'duration': 123 + }, + 'borderColor': '#cccccc', + 'brightness': 0.3, + 'enabled': True + }, + 'inactive': { + 'enabled': True, + 'opacity': 0.5 + }, + 'normal': { + 'animation': { + 'defer': 24 + } + }, + 'select': { + 'color': '#ff0000', + 'enabled': True, + } + }, + 'sticky_tracking': True, + 'tooltip': { + 'animation': True, + 'backgroundColor': '#ccc', + 'borderColor': '#999', + 'borderRadius': 4, + 'borderWidth': 1, + 'className': 'some-class-name', + 'clusterFormat': 'format string', + 'dateTimeLabelFormats': { + 'day': 'test', + 'hour': 'test', + 'millisecond': 'test', + 'minute': 'test', + 'month': 'test', + 'second': 'test', + 'week': 'test', + 'year': 'test' + }, + 'distance': 12, + 'enabled': True, + 'followPointer': True, + 'followTouch_move': True, + 'footerFormat': 'format string', + 'formatter': """function() { return true; }""", + 'headerFormat': 'format string', + 'headerShape': 'circle', + 'hideDelay': 3, + 'nullFormat': 'format string', + 'nullFormatter': """function() { return true; }""", + 'outside': False, + 'padding': 6, + 'pointFormat': 'format string', + 'pointFormatter': """function() { return true; }""", + 'positioner': """function() { return true; }""", + 'shadow': False, + 'shape': 'rect', + 'shared': False, + 'snap': 4, + 'split': False, + 'stickOnContact': True, + 'style': 'style string goes here', + 'useHTML': False, + 'valueDecimals': 2, + 'valuePrefix': '$', + 'valueSuffix': ' USD', + 'xDateFormat': 'format string' + }, + 'turbo_threshold': 456, + 'visible': True + }, None), + +] + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test__init__(kwargs, error): + Class__init__(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test__to_untrimmed_dict(kwargs, error): + Class__to_untrimmed_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_from_dict(kwargs, error): + Class_from_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_to_dict(kwargs, error): + Class_to_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('filename, as_file, error', [ + ('plot_options/treegraph/01.js', False, None), + ('plot_options/treegraph/02.js', False, None), + + ('plot_options/treegraph/error-01.js', + False, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + ('plot_options/treegraph/error-02.js', + False, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + + ('plot_options/treegraph/01.js', True, None), + ('plot_options/treegraph/02.js', True, None), + + ('plot_options/treegraph/error-01.js', + True, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + ('plot_options/treegraph/error-02.js', + True, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + +]) +def test_from_js_literal(input_files, filename, as_file, error): + Class_from_js_literal(cls, input_files, filename, as_file, error) diff --git a/tests/options/series/test_treegraph.py b/tests/options/series/test_treegraph.py new file mode 100644 index 00000000..74a34f6a --- /dev/null +++ b/tests/options/series/test_treegraph.py @@ -0,0 +1,168 @@ +"""Tests for ``highcharts.no_data``.""" + +import pytest + +from json.decoder import JSONDecodeError + +from highcharts_core.options.series.treegraph import TreegraphSeries as cls +from highcharts_core import errors +from tests.fixtures import input_files, check_input_file, to_camelCase, to_js_dict, \ + Class__init__, Class__to_untrimmed_dict, Class_from_dict, Class_to_dict, \ + Class_from_js_literal + +STANDARD_PARAMS = [ + ({}, None), + ({ + 'data': [ + { + 'colorValue': 2, + 'dataLabels': { + 'align': 'center', + 'allowOverlap': True, + 'animation': { + 'defer': 5 + }, + 'backgroundColor': { + 'linearGradient': { + 'x1': 0.123, + 'x2': 0.234, + 'y1': 0.345, + 'y2': 0.456 + }, + 'stops': [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + 'borderColor': '#999999', + 'borderRadius': 24, + 'borderWidth': 1, + 'className': 'some-class-name', + 'color': '#000000', + 'crop': True, + 'defer': False, + 'enabled': True, + 'filter': { + 'operator': '>=', + 'property': 'some_property', + 'value': 123 + }, + 'format': 'some format', + 'formatter': """function() { return true; }""", + 'inside': True, + 'nullFormat': 'some format', + 'nullFormatter': """function() { return true; }""", + 'overflow': 'none', + 'padding': 12, + 'position': 'center', + 'rotation': 0, + 'shadow': False, + 'shape': 'rect', + 'style': 'style goes here', + 'useHTML': False, + 'verticalAlign': 'top', + 'x': 10, + 'y': 20, + 'z': 0 + }, + 'dragDrop': { + 'draggableX': True, + 'draggableY': True, + 'dragHandle': { + 'className': 'draghandle-classname-goes-here', + 'color': '#ccc', + 'cursor': 'alias', + 'lineColor': '#ddd', + 'lineWidth': 2, + 'pathFormatter': """function() { return true; }""", + 'zIndex': 10 + }, + 'dragMaxX': 3456, + 'dragMaxY': 6532, + 'dragMinX': 123, + 'dragMinY': 321, + 'dragPrecisionX': 5, + 'dragPrecisionY': 5, + 'dragSensitivity': 2, + 'groupBy': 'some-property-name', + 'guideBox': { + 'default': { + 'className': 'some-classname-goes-here', + 'color': '#999', + 'cursor': 'pointer', + 'lineColor': '#ccc', + 'lineWidth': 2, + 'zIndex': 100 + } + }, + 'liveRedraw': True + }, + 'drilldown': 'some-id-goes-here', + 'parent': 'some-id-goes-here', + } + ] + }, None), + +] + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test__init__(kwargs, error): + Class__init__(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test__to_untrimmed_dict(kwargs, error): + Class__to_untrimmed_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_from_dict(kwargs, error): + Class_from_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_to_dict(kwargs, error): + Class_to_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('filename, as_file, error', [ + ('series/treegraph/01.js', False, None), + ('series/treegraph/02.js', False, None), + + ('series/treegraph/error-01.js', + False, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + ('series/treegraph/error-02.js', + False, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + + ('series/treegraph/01.js', True, None), + ('series/treegraph/02.js', True, None), + + ('series/treegraph/error-01.js', + True, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + ('series/treegraph/error-02.js', + True, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + +]) +def test_from_js_literal(input_files, filename, as_file, error): + Class_from_js_literal(cls, input_files, filename, as_file, error) From b88d58f8891b5125d92a1287a1de043e62653010 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Sun, 23 Apr 2023 09:25:24 -0400 Subject: [PATCH 10/24] Added additional properties to OrganizationDataLabel --- .../utility_classes/data_labels.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/highcharts_core/utility_classes/data_labels.py b/highcharts_core/utility_classes/data_labels.py index 87d4a4e0..71264a60 100644 --- a/highcharts_core/utility_classes/data_labels.py +++ b/highcharts_core/utility_classes/data_labels.py @@ -1015,12 +1015,51 @@ class OrganizationDataLabel(DataLabel): """Variant of :class:`DataLabel` used for :term:`organization` series.""" def __init__(self, **kwargs): + self._link_format = None self._link_text_path = None + self.link_format = kwargs.get('link_format', None) self.link_text_path = kwargs.get('link_text_path', None) super().__init__(**kwargs) + @property + def link_format(self) -> Optional[str]: + """The format string specifying what to show for links in the\rorganization chart. + + .. tip:: + + Best to use with + :meth:`.link_text_path ` + enabled. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._link_format + + @link_format.setter + def link_format(self, value): + self._link_format = validators.string(value, allow_empty = True) + + @property + def link_formatter(self) -> Optional[CallbackFunction]: + """JavaScript callback function to format data labels for links in the organization chart. + + .. note:: + + The :meth:`.link_format ` + property takes precedence over the ``link_formatter``. + + :rtype: :class:`CallbackFunction ` or + :obj:`None ` + """ + return self._link_formatter + + @link_formatter.setter + @class_sensitive(CallbackFunction) + def link_formatter(self, value): + self._link_formatter = value + @property def link_text_path(self) -> Optional[TextPath]: """Options for a label text which should follow the link's shape. @@ -1074,6 +1113,8 @@ def _get_kwargs_from_dict(cls, as_dict): 'y': as_dict.get('y', None), 'z': as_dict.get('z', None), + 'link_format': as_dict.get('linkFormat', None), + 'link_formatter': as_dict.get('linkFormatter', None), 'link_text_path': as_dict.get('linkTextPath', None), } @@ -1081,6 +1122,8 @@ def _get_kwargs_from_dict(cls, as_dict): def _to_untrimmed_dict(self, in_cls = None) -> dict: untrimmed = { + 'linkFormat': self.link_format, + 'linkFormatter': self.link_formatter, 'linkTextPath': self.link_text_path, } From 4c660247972715e6d787d0fc3832c0bc54afd78a Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Sun, 23 Apr 2023 14:21:37 -0400 Subject: [PATCH 11/24] Added support for Pictorial series type. --- CHANGES.rst | 1 + docs/api.rst | 5 + docs/api/options/plot_options/index.rst | 3 + docs/api/options/plot_options/pictorial.rst | 28 + docs/api/options/series/index.rst | 4 + docs/api/options/series/pictorial.rst | 48 ++ .../options/plot_options/__init__.py | 25 + .../options/plot_options/pictorial.py | 331 +++++++++ highcharts_core/options/series/pictorial.py | 281 ++++++++ .../options/series/series_generator.py | 2 + .../input_files/plot_options/pictorial/03.js | 6 + .../input_files/plot_options/pictorial/04.js | 6 + .../plot_options/pictorial/error-01.js | 46 ++ .../plot_options/pictorial/error-02.js | 284 ++++++++ tests/input_files/series/pictorial/01.js | 14 + .../input_files/series/pictorial/error-01.js | 168 +++++ tests/options/plot_options/test_pictorial.py | 97 +++ tests/options/series/test_pictorial.py | 682 ++++++++++++++++++ 18 files changed, 2031 insertions(+) create mode 100644 docs/api/options/plot_options/pictorial.rst create mode 100644 docs/api/options/series/pictorial.rst create mode 100644 highcharts_core/options/plot_options/pictorial.py create mode 100644 highcharts_core/options/series/pictorial.py create mode 100644 tests/input_files/plot_options/pictorial/03.js create mode 100644 tests/input_files/plot_options/pictorial/04.js create mode 100644 tests/input_files/plot_options/pictorial/error-01.js create mode 100644 tests/input_files/plot_options/pictorial/error-02.js create mode 100644 tests/input_files/series/pictorial/01.js create mode 100644 tests/input_files/series/pictorial/error-01.js create mode 100644 tests/options/plot_options/test_pictorial.py create mode 100644 tests/options/series/test_pictorial.py diff --git a/CHANGES.rst b/CHANGES.rst index dcc83e9d..bda282ff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,6 +18,7 @@ Release 2.0.0 * Updated ``options.plot_options.organization.OrganizationOptions.data_labels`` to accept ``OrganizationDataLabel`` values. * Added ``.description_format`` property to ``options.plot_options.accessibility.TypeOptionsAccessibility``. + * Added ``PictorialOptions`` / ``PictorialSeries`` series type with related classes. * **FIXED:** Broken heatmap and tilemap documentation links. * **FIXED:** Fixed missing ``TreegraphOptions`` / ``TreegraphSeries`` series type. diff --git a/docs/api.rst b/docs/api.rst index 694de9c0..c6bad721 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -363,6 +363,8 @@ Core Components :class:`ParentNodeOptions ` * - :mod:`.options.plot_options.pareto ` - :class:`ParetoOptions ` + * - :mod:`.options.plot_options.pictorial ` + - :class:`PictorialOptions ` * - :mod:`.options.plot_options.pie ` - :class:`PieOptions ` :class:`VariablePieOptions ` @@ -517,6 +519,9 @@ Core Components - :class:`PackedBubbleSeries ` * - :mod:`.options.series.pareto ` - :class:`ParetoSeries ` + * - :mod:`.options.series.pictorial ` + - :class:`PictorialSeries ` + :class:`PictorialPaths ` * - :mod:`.options.series.pie ` - :class:`PieSeries ` :class:`VariablePieSeries ` diff --git a/docs/api/options/plot_options/index.rst b/docs/api/options/plot_options/index.rst index 3493fa46..247bc127 100644 --- a/docs/api/options/plot_options/index.rst +++ b/docs/api/options/plot_options/index.rst @@ -34,6 +34,7 @@ organization packedbubble pareto + pictorial pie points polygon @@ -166,6 +167,8 @@ Sub-components :class:`ParentNodeOptions ` * - :mod:`.options.plot_options.pareto ` - :class:`ParetoOptions ` + * - :mod:`.options.plot_options.pictorial ` + - :class:`PictorialOptions ` * - :mod:`.options.plot_options.pie ` - :class:`PieOptions ` :class:`VariablePieOptions ` diff --git a/docs/api/options/plot_options/pictorial.rst b/docs/api/options/plot_options/pictorial.rst new file mode 100644 index 00000000..509c50bc --- /dev/null +++ b/docs/api/options/plot_options/pictorial.rst @@ -0,0 +1,28 @@ +########################################################################################## +:mod:`.pictorial ` +########################################################################################## + +.. contents:: Module Contents + :local: + :depth: 3 + :backlinks: entry + +-------------- + +.. module:: highcharts_core.options.plot_options.pictorial + +******************************************************************************************************************** +class: :class:`PictorialOptions ` +******************************************************************************************************************** + +.. autoclass:: PictorialOptions + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: PictorialOptions + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | diff --git a/docs/api/options/series/index.rst b/docs/api/options/series/index.rst index a0684dd8..70d19f8d 100644 --- a/docs/api/options/series/index.rst +++ b/docs/api/options/series/index.rst @@ -31,6 +31,7 @@ organization packedbubble pareto + pictorial pie points polygon @@ -172,6 +173,9 @@ Sub-components - :class:`PackedBubbleSeries ` * - :mod:`.options.series.pareto ` - :class:`ParetoSeries ` + * - :mod:`.options.series.pictorial ` + - :class:`PictorialSeries ` + :class:`PictorialPaths ` * - :mod:`.options.series.pie ` - :class:`PieSeries ` :class:`VariablePieSeries ` diff --git a/docs/api/options/series/pictorial.rst b/docs/api/options/series/pictorial.rst new file mode 100644 index 00000000..dbd7ba4a --- /dev/null +++ b/docs/api/options/series/pictorial.rst @@ -0,0 +1,48 @@ +########################################################################################## +:mod:`.pictorial ` +########################################################################################## + +.. contents:: Module Contents + :local: + :depth: 3 + :backlinks: entry + +-------------- + +.. module:: highcharts_core.options.series.pictorial + +******************************************************************************************************************** +class: :class:`PictorialSeries ` +******************************************************************************************************************** + +.. autoclass:: PictorialSeries + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: PictorialSeries + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +-------------- + +.. module:: highcharts_core.options.series.pictorial + +******************************************************************************************************************** +class: :class:`PictorialPaths ` +******************************************************************************************************************** + +.. autoclass:: PictorialPaths + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: PictorialPaths + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | diff --git a/highcharts_core/options/plot_options/__init__.py b/highcharts_core/options/plot_options/__init__.py index cbd65b9b..3093451b 100644 --- a/highcharts_core/options/plot_options/__init__.py +++ b/highcharts_core/options/plot_options/__init__.py @@ -31,6 +31,7 @@ from highcharts_core.options.plot_options.organization import OrganizationOptions from highcharts_core.options.plot_options.packedbubble import PackedBubbleOptions from highcharts_core.options.plot_options.pareto import ParetoOptions +from highcharts_core.options.plot_options.pictorial import PictorialOptions from highcharts_core.options.plot_options.pie import PieOptions from highcharts_core.options.plot_options.polygon import PolygonOptions from highcharts_core.options.plot_options.pyramid import PyramidOptions @@ -105,6 +106,7 @@ def __init__(self, **kwargs): self._organization = None self._packedbubble = None self._pareto = None + self._pictorial = None self._pie = None self._polygon = None self._pyramid = None @@ -815,6 +817,27 @@ def pareto(self) -> Optional[ParetoOptions]: def pareto(self, value): self._pareto = value + @property + def pictorial(self) -> Optional[PictorialOptions]: + """General options to apply to all Pictorial series types. + + A pictorial series uses vector images to represent the data, with the data's shape + determined by the ``path`` parameter. + + .. figure:: ../../../_static/pictorial-example.png + :alt: Pictorial Example Chart + :align: center + + + :rtype: :class:`ParetoOptions` or :obj:`None ` + """ + return self._pictorial + + @pictorial.setter + @class_sensitive(PictorialOptions) + def pictorial(self, value): + self._pictorial = value + @property def pie(self) -> Optional[PieOptions]: """General options to apply to all Pie series types. @@ -1437,6 +1460,7 @@ def _get_kwargs_from_dict(cls, as_dict): 'organization': as_dict.get('organization', None), 'packedbubble': as_dict.get('packedbubble', None), 'pareto': as_dict.get('pareto', None), + 'pictorial': as_dict.get('pictorial', None), 'pie': as_dict.get('pie', None), 'polygon': as_dict.get('polygon', None), 'pyramid': as_dict.get('pyramid', None), @@ -1496,6 +1520,7 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: 'organization': self.organization, 'packedbubble': self.packedbubble, 'pareto': self.pareto, + 'pictorial': self.pictorial, 'pie': self.pie, 'polygon': self.polygon, 'pyramid': self.pyramid, diff --git a/highcharts_core/options/plot_options/pictorial.py b/highcharts_core/options/plot_options/pictorial.py new file mode 100644 index 00000000..76b3a408 --- /dev/null +++ b/highcharts_core/options/plot_options/pictorial.py @@ -0,0 +1,331 @@ +from decimal import Decimal +from typing import Optional + +from validator_collection import validators + +from highcharts_core import constants +from highcharts_core.options.plot_options.series import SeriesOptions + + +class PictorialOptions(SeriesOptions): + """General options to apply to all Pictorial series types. + + A pictorial series uses vector images to represent the data, with the data's shape + determined by the ``path`` parameter. + + .. figure:: ../../../_static/pictorial-example.png + :alt: Pictorial Example Chart + :align: center + + """ + + def __init__(self, **kwargs): + self._depth = None + self._edge_color = None + self._edge_width = None + self._grouping = None + self._group_padding = None + self._group_z_padding = None + self._max_point_width = None + self._min_point_length = None + self._point_range = None + + self.depth = kwargs.get('depth', None) + self.edge_color = kwargs.get('edge_color', None) + self.edge_width = kwargs.get('edge_width', None) + self.grouping = kwargs.get('grouping', None) + self.group_padding = kwargs.get('group_padding', None) + self.group_z_padding = kwargs.get('group_z_padding', None) + self.max_point_width = kwargs.get('max_point_width', None) + self.min_point_length = kwargs.get('min_point_length', None) + self.point_padding = kwargs.get('point_padding', None) + self.point_range = kwargs.get('point_range', None) + self.point_width = kwargs.get('point_width', None) + + super().__init__(**kwargs) + + @property + def depth(self) -> Optional[int | float | Decimal]: + """Depth of the columns in a 3D column chart. Defaults to ``25``. + + :rtype: numeric or :obj:`None ` + """ + return self._depth + + @depth.setter + def depth(self, value): + self._depth = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def edge_color(self) -> Optional[str]: + """The color of the edges when rendering 3D columns. Defaults to + :obj:`None `. + + Similar to :meth:`border_color `, except it defaults to + the same color as the column if set to :obj:`None `. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._edge_color + + @edge_color.setter + def edge_color(self, value): + self._edge_color = validators.string(value, allow_empty = True) + + @property + def edge_width(self) -> Optional[int | float | Decimal]: + """The width of the colored edges applied to a 3D column. Defaults to ``1``. + + :rtype: numeric or :obj:`None ` + """ + return self._edge_width + + @edge_width.setter + def edge_width(self, value): + self._edge_width = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def grouping(self) -> Optional[bool]: + """If ``True``, groups non-stacked columns. If ``False``, renders them + independent of each other. Non-grouped columns will be laid out individually and + overlap each other. + + Defaults to ``True``. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._grouping + + @grouping.setter + def grouping(self, value): + if value is None: + self._grouping = None + else: + self._grouping = bool(value) + + @property + def group_padding(self) -> Optional[int | float | Decimal]: + """Padding between each value group, in x axis units. Defaults to ``0.2``. + + :rtype: numeric or :obj:`None ` + """ + return self._group_padding + + @group_padding.setter + def group_padding(self, value): + self._group_padding = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def group_z_padding(self) -> Optional[int | float | Decimal]: + """Spacing between columns along the Z axis in a 3D chart. Defaults to ``1``. + + :rtype: numeric or :obj:`None ` + """ + return self._group_z_padding + + @group_z_padding.setter + def group_z_padding(self, value): + self._group_z_padding = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def max_point_width(self) -> Optional[int | float | Decimal]: + """The maximum allowed pixel width for a column. This prevents the image from + becoming too wide when there is a small number of points in the chart. Defaults + to :obj:`None `. + + :rtype: numeric or :obj:`None ` + """ + return self._max_point_width + + @max_point_width.setter + def max_point_width(self, value): + self._max_point_width = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def min_point_length(self) -> Optional[int | float | Decimal]: + """The minimal height for a column or width for a data point. Defaults to + :obj:`None `. + + By default, ``0`` values are not shown. To visualize a ``0`` (or close to zero) + point, set the minimal point length to a pixel value like ``3``. + + :rtype: numeric or :obj:`None ` + """ + return self._min_point_length + + @min_point_length.setter + def min_point_length(self, value): + self._min_point_length = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def point_padding(self) -> Optional[int | float | Decimal]: + """Padding between each column or bar, in x axis units. Defaults to ``0.1``. + + :rtype: numeric or :obj:`None ` + """ + return self._point_padding + + @point_padding.setter + def point_padding(self, value): + self._point_padding = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def point_range(self) -> Optional[constants.EnforcedNullType | int | float | Decimal]: + """The X axis range that each point is valid for, which determines the width of + the column. Defaults to ``EnforcedNull``, which computes the range automatically. + + On a categorized axis, the range will be ``1`` by default (one category unit). On + linear and datetime axes, the range will be computed as the distance between the + two closest data points. + + The default ``EnforcedNull`` means it is computed automatically, but the setting + can be used to override the default value. + + .. note:: + + If :meth:`data_sorting ` is enabled, the default value + is implicitly adjusted to ``1``. + + :rtype: numeric or :class:`EnforcedNullType` or :obj:`None ` + """ + return self._point_range + + @point_range.setter + def point_range(self, value): + if isinstance(value, constants.EnforcedNullType): + self._point_range = constants.EnforcedNull + else: + self._point_range = validators.numeric(value, allow_empty = True) + + @property + def point_width(self) -> Optional[int | float | Decimal]: + """A pixel value specifying a fixed width for each point. When set + to :obj:`None `, the width is calculated from the + :meth:`point_padding ` and + :meth:`group_padding `. Defaults to + :obj:`None `. + + :rtype: numeric or :obj:`None ` + """ + return self._point_width + + @point_width.setter + def point_width(self, value): + self._point_width = validators.numeric(value, + allow_empty = True, + minimum = 0) + @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), + '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), + 'states': as_dict.get('states', None), + 'sticky_tracking': as_dict.get('stickyTracking', None), + 'threshold': as_dict.get('threshold', None), + 'tooltip': as_dict.get('tooltip', None), + 'turbo_threshold': as_dict.get('turboThreshold', None), + 'visible': as_dict.get('visible', None), + + 'animation_limit': as_dict.get('animationLimit', None), + 'boost_blending': as_dict.get('boostBlending', None), + 'boost_threshold': as_dict.get('boostThreshold', None), + 'color_index': as_dict.get('colorIndex', None), + 'color_key': as_dict.get('colorKey', None), + 'connect_nulls': as_dict.get('connectNulls', None), + 'crisp': as_dict.get('crisp', None), + 'crop_threshold': as_dict.get('cropThreshold', None), + 'data_sorting': as_dict.get('dataSorting', None), + 'find_nearest_point_by': as_dict.get('findNearestPointBy', None), + 'get_extremes_from_all': as_dict.get('getExtremesFromAll', None), + 'linecap': as_dict.get('linecap', None), + 'line_width': as_dict.get('lineWidth', None), + 'relative_x_value': as_dict.get('relativeXValue', None), + 'shadow': as_dict.get('shadow', None), + 'soft_threshold': as_dict.get('softThreshold', None), + 'step': as_dict.get('step', None), + 'zone_axis': as_dict.get('zoneAxis', None), + 'zones': as_dict.get('zones', None), + + 'color_axis': as_dict.get('colorAxis', None), + 'connect_ends': as_dict.get('connectEnds', None), + 'drag_drop': as_dict.get('dragDrop', None), + 'negative_color': as_dict.get('negativeColor', None), + 'point_interval': as_dict.get('pointInterval', None), + 'point_interval_unit': as_dict.get('pointIntervalUnit', None), + 'point_placement': as_dict.get('pointPlacement', None), + 'point_start': as_dict.get('pointStart', None), + 'stacking': as_dict.get('stacking', None), + + 'depth': as_dict.get('depth', None), + 'edge_color': as_dict.get('edgeColor', None), + 'edge_width': as_dict.get('edgeWidth', None), + 'grouping': as_dict.get('grouping', None), + 'group_padding': as_dict.get('groupPadding', None), + 'group_z_padding': as_dict.get('groupZPadding', None), + 'max_point_width': as_dict.get('maxPointWidth', None), + 'min_point_length': as_dict.get('minPointLength', None), + 'point_padding': as_dict.get('pointPadding', None), + 'point_range': as_dict.get('pointRange', None), + 'point_width': as_dict.get('pointWidth', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'depth': self.depth, + 'edgeColor': self.edge_color, + 'edgeWidth': self.edge_width, + 'grouping': self.grouping, + 'groupPadding': self.group_padding, + 'groupZPadding': self.group_z_padding, + 'maxPointWidth': self.max_point_width, + 'minPointLength': self.min_point_length, + 'pointPadding': self.point_padding, + 'pointRange': self.point_range, + 'pointWidth': self.point_width, + } + 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 diff --git a/highcharts_core/options/series/pictorial.py b/highcharts_core/options/series/pictorial.py new file mode 100644 index 00000000..07610190 --- /dev/null +++ b/highcharts_core/options/series/pictorial.py @@ -0,0 +1,281 @@ +from decimal import Decimal +from typing import Optional, List + +from validator_collection import validators + +from highcharts_core.options.series.base import SeriesBase +from highcharts_core.options.series.data.cartesian import CartesianData +from highcharts_core.options.plot_options.pictorial import PictorialOptions +from highcharts_core.utility_functions import mro__to_untrimmed_dict +from highcharts_core.metaclasses import HighchartsMeta +from highcharts_core.decorators import class_sensitive +from highcharts_core.utility_classes.ast import AttributeObject + + +class PictorialPaths(HighchartsMeta): + """Configuration of pictorial point images.""" + + def __init__(self, **kwargs): + self._definition = None + self._max = None + + self.definition = kwargs.get('definition', None) + self.max = kwargs.get('max', None) + + @property + def definition(self) -> Optional[AttributeObject]: + """Defines the path to be drawn, corresponding to the SVG ``d`` attribute. + + :rtype: :class:`AttributeObject ` or + :obj:`None ` + """ + return self._definition + + @definition.setter + @class_sensitive(AttributeObject) + def definition(self, value): + self._definition = value + + @property + def max(self) -> Optional[int | float | Decimal]: + """Determines height of the image. It is the ratio of ``yAxis.max`` to the ``paths.max``. + + Defaults to the maximum value of the y-axis. + + :rtype: numeric or :obj:`None ` + """ + return self._max + + @max.setter + def max(self, value): + self._max = validators.numeric(value, allow_empty = True) + + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'definition': as_dict.get('definition', None), + 'max': as_dict.get('max', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'definition': self.definition, + 'max': self.max + } + + return untrimmed + + +class PictorialSeries(SeriesBase, PictorialOptions): + """Options to configure a Pictorial series. + + A pictorial series uses vector images to represent the data, with the data's shape + determined by the ``path`` parameter. + + .. figure:: ../../../_static/pictorial-example.png + :alt: Pictorial Example Chart + :align: center + + """ + + def __init__(self, **kwargs): + self._paths = None + + self.paths = kwargs.get('paths', None) + + super().__init__(**kwargs) + + @property + def data(self) -> Optional[List[CartesianData]]: + """Collection of data that represents the series. Defaults to + :obj:`None `. + + While the series type returns a collection of :class:`CartesianData` instances, + it accepts as input three different types of data: + + .. tabs:: + + .. tab:: 1D Collection + + .. code-block:: + + series = PictorialSeries() + series.data = [0, 5, 3, 5] + + A one-dimensional collection of numerical values. Each member of the + collection will be interpreted as a ``y``-value, with its corresponding ``x`` + value automatically determined. + + If :meth:`PictorialSeries.point_start` is :obj:`None `, ``x`` + values will begin at ``0``. Otherwise, they will start at ``point_start``. + + If :meth:`PictorialSeries.point_interval` is :obj:`None `, ``x`` + values will be incremented by ``1``. Otherwise, they will be incremented + by the value of ``point_interval``. + + .. tab:: 2D Collection + + .. code-block:: + + series = PictorialSeries() + # Category X-axis + series.data = [ + ['Category A', 0], + ['Category B', 5], + ['Category C', 3], + ['Category D', 5] + ] + + # Numerical X-axis + series.data = [ + [9, 0], + [1, 5], + [2, 3], + [7, 5] + ] + + A two-dimensional collection of values. Each member of the collection will be + interpreted as an ``x`` and ``y`` pair. The ``x`` value can be a + :class:`str `, :class:`date `, + :class:`datetime `, or numeric value. + + .. note:: + + If the ``x`` value is a :class:`str `, it will be interpreted + as the name of the data point. + + .. tab:: Object Collection + + A one-dimensional collection of :class:`CartesianData` objects. + + :rtype: :class:`list ` of :class:`CartesianData` or + :obj:`None ` + """ + return self._data + + @data.setter + def data(self, value): + if not value: + self._data = None + else: + self._data = CartesianData.from_array(value) + + @property + def paths(self) -> Optional[PictorialPaths]: + """Configuration of the point image. + + :rtype: :class:`PictorialPaths ` or + :obj:`None ` + """ + return self._paths + + @paths.setter + @class_sensitive(PictorialPaths) + def paths(self, value): + self._paths = 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), + '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), + 'states': as_dict.get('states', None), + 'sticky_tracking': as_dict.get('stickyTracking', None), + 'threshold': as_dict.get('threshold', None), + 'tooltip': as_dict.get('tooltip', None), + 'turbo_threshold': as_dict.get('turboThreshold', None), + 'visible': as_dict.get('visible', None), + + 'animation_limit': as_dict.get('animationLimit', None), + 'boost_blending': as_dict.get('boostBlending', None), + 'boost_threshold': as_dict.get('boostThreshold', None), + 'color_index': as_dict.get('colorIndex', None), + 'color_key': as_dict.get('colorKey', None), + 'connect_nulls': as_dict.get('connectNulls', None), + 'crisp': as_dict.get('crisp', None), + 'crop_threshold': as_dict.get('cropThreshold', None), + 'data_sorting': as_dict.get('dataSorting', None), + 'find_nearest_point_by': as_dict.get('findNearestPointBy', None), + 'get_extremes_from_all': as_dict.get('getExtremesFromAll', None), + 'linecap': as_dict.get('linecap', None), + 'line_width': as_dict.get('lineWidth', None), + 'relative_x_value': as_dict.get('relativeXValue', None), + 'shadow': as_dict.get('shadow', None), + 'soft_threshold': as_dict.get('softThreshold', None), + 'step': as_dict.get('step', None), + 'zone_axis': as_dict.get('zoneAxis', None), + 'zones': as_dict.get('zones', None), + + 'color_axis': as_dict.get('colorAxis', None), + 'connect_ends': as_dict.get('connectEnds', None), + 'drag_drop': as_dict.get('dragDrop', None), + 'negative_color': as_dict.get('negativeColor', None), + 'point_interval': as_dict.get('pointInterval', None), + 'point_interval_unit': as_dict.get('pointIntervalUnit', None), + 'point_placement': as_dict.get('pointPlacement', None), + 'point_start': as_dict.get('pointStart', None), + 'stacking': as_dict.get('stacking', None), + + 'depth': as_dict.get('depth', None), + 'edge_color': as_dict.get('edgeColor', None), + 'edge_width': as_dict.get('edgeWidth', None), + 'grouping': as_dict.get('grouping', None), + 'group_padding': as_dict.get('groupPadding', None), + 'group_z_padding': as_dict.get('groupZPadding', None), + 'max_point_width': as_dict.get('maxPointWidth', None), + 'min_point_length': as_dict.get('minPointLength', None), + 'point_padding': as_dict.get('pointPadding', None), + 'point_range': as_dict.get('pointRange', None), + 'point_width': as_dict.get('pointWidth', 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), + + 'paths': as_dict.get('paths', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'paths': self.paths + } + parent_as_dict = mro__to_untrimmed_dict(self, in_cls = in_cls) + for key in parent_as_dict: + untrimmed[key] = parent_as_dict[key] + + return untrimmed diff --git a/highcharts_core/options/series/series_generator.py b/highcharts_core/options/series/series_generator.py index f6b6dd4d..64146bb3 100644 --- a/highcharts_core/options/series/series_generator.py +++ b/highcharts_core/options/series/series_generator.py @@ -41,6 +41,7 @@ from highcharts_core.options.series.organization import OrganizationSeries from highcharts_core.options.series.packedbubble import PackedBubbleSeries from highcharts_core.options.series.pareto import ParetoSeries +from highcharts_core.options.series.pictorial import PictorialSeries from highcharts_core.options.series.pie import PieSeries from highcharts_core.options.series.pie import VariablePieSeries from highcharts_core.options.series.polygon import PolygonSeries @@ -96,6 +97,7 @@ 'organization': OrganizationSeries, 'packedbubble': PackedBubbleSeries, 'pareto': ParetoSeries, + 'pictorial': PictorialSeries, 'pie': PieSeries, 'variablepie': VariablePieSeries, 'polygon': PolygonSeries, diff --git a/tests/input_files/plot_options/pictorial/03.js b/tests/input_files/plot_options/pictorial/03.js new file mode 100644 index 00000000..bf975059 --- /dev/null +++ b/tests/input_files/plot_options/pictorial/03.js @@ -0,0 +1,6 @@ +{ + depth: 10, + edgeColor: '#999', + edgeWidth: 1, + groupZPadding: 4 +} diff --git a/tests/input_files/plot_options/pictorial/04.js b/tests/input_files/plot_options/pictorial/04.js new file mode 100644 index 00000000..bf975059 --- /dev/null +++ b/tests/input_files/plot_options/pictorial/04.js @@ -0,0 +1,6 @@ +{ + depth: 10, + edgeColor: '#999', + edgeWidth: 1, + groupZPadding: 4 +} diff --git a/tests/input_files/plot_options/pictorial/error-01.js b/tests/input_files/plot_options/pictorial/error-01.js new file mode 100644 index 00000000..2e4ec63e --- /dev/null +++ b/tests/input_files/plot_options/pictorial/error-01.js @@ -0,0 +1,46 @@ +{ + borderColor: '#ccc', + borderRadius: 'invalid value', + borderWidth: 2, + centerInCategory: true, + colorByPoint: true, + colors: [ + '#fff', + '#ccc', + { + linearGradient: { + x1: 0.123, + x2: 0.567, + y1: 0.891, + y2: 0.987 + }, + stops: [ + [0.123, '#cccccc'], + [0.456, '#ff0000'], + [1, '#00ff00'] + ] + }, + { + animation: { + defer: 5 + }, + patternOptions: { + aspectRatio: 0.5, + backgroundColor: '#999999', + id: 'some_id_goes_here', + opacity: 0.5, + width: 120, + x: 5, + y: 10 + }, + patternIndex: 2 + } + ], + grouping: false, + groupPadding: 6, + maxPointWidth: 12, + minPointLength: 12, + pointPadding: 6, + pointRange: 'invalid value', + pointWidth: 'invalid-value' +} diff --git a/tests/input_files/plot_options/pictorial/error-02.js b/tests/input_files/plot_options/pictorial/error-02.js new file mode 100644 index 00000000..659707f3 --- /dev/null +++ b/tests/input_files/plot_options/pictorial/error-02.js @@ -0,0 +1,284 @@ +{ + borderColor: '#ccc', + borderRadius: 4, + borderWidth: 'invalid value', + centerInCategory: true, + colorByPoint: true, + colors: [ + '#fff', + '#ccc', + { + linearGradient: { + x1: 0.123, + x2: 0.567, + y1: 0.891, + y2: 0.987 + }, + stops: [ + [0.123, '#cccccc'], + [0.456, '#ff0000'], + [1, '#00ff00'] + ] + }, + { + animation: { + defer: 5 + }, + patternOptions: { + aspectRatio: 0.5, + backgroundColor: '#999999', + id: 'some_id_goes_here', + opacity: 0.5, + width: 120, + x: 5, + y: 10 + }, + patternIndex: 2 + } + ], + grouping: false, + groupPadding: 6, + maxPointWidth: 12, + minPointLength: 12, + pointPadding: 6, + pointRange: 24, + pointWidth: 12, + + accessibility: { + description: 'Description goes here', + enabled: true, + exposeAsGroupOnly: true, + keyboardNavigation: { + enabled: true + }, + point: { + dateFormat: 'format string', + dateFormatter: function() { return true; }, + describeNull: false, + descriptionFormatter: function() { return true; }, + valueDecimals: 2, + valueDescriptionFormat: 'format string', + valuePrefix: '$', + valueSuffix: 'USD' + }, + }, + allowPointSelect: true, + animation: { + defer: 5 + }, + className: 'some-class-name', + clip: false, + color: '#fff', + cursor: 'alias', + custom: { + 'item1': 'some value', + 'item2': 'some value' + }, + dashStyle: 'Dash', + dataLabels: { + align: 'center', + allowOverlap: true, + animation: { + defer: 5 + }, + backgroundColor: { + linearGradient: { + x1: 0.123, + x2: 0.234, + y1: 0.345, + y2: 0.456 + }, + stops: [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + borderColor: '#999999', + borderRadius: 24, + borderWidth: 1, + className: 'some-class-name', + color: '#000000', + crop: true, + defer: false, + enabled: true, + filter: { + operator: '>=', + property: 'some_property', + value: 123 + }, + format: 'some format', + formatter: function() { return true; }, + inside: true, + nullFormat: 'some format', + nullFormatter: function() { return true; }, + overflow: 'none', + padding: 12, + position: 'center', + rotation: 0, + shadow: false, + shape: 'rect', + style: 'style goes here', + useHTML: false, + verticalAlign: 'top', + x: 10, + y: 20, + z: 0 + }, + description: 'Description goes here', + enableMouseTracking: true, + events: { + afterAnimate: function(event) { return true; }, + click: function(event) { return true; }, + hide: function(event) { return true; }, + mouseOut: function(event) { return true; }, + show: function(event) { return true; } + }, + includeInDataExport: true, + keys: [ + 'somevalue', + 'somevalue', + 'somevalue' + ], + label: { + boxesToAvoid: [ + { + bottom: 12, + left: -46, + right: 84, + top: 24 + }, + { + bottom: 48, + left: -46, + right: 84, + top: 86 + } + ], + connectorAllowed: true, + connectorNeighbourDistance: 12, + enabled: true, + format: 'format string', + formatter: function() { return true; }, + maxFontSize: 18, + minFontSize: 6, + onArea: false, + style: 'some style string' + }, + linkedTo: 'some_id', + marker: { + enabled: true, + fillColor: '#cccccc', + height: 24, + lineWidth: 2, + radius: 2, + states: { + hover: { + enabled: true + } + }, + symbol: 'circle', + width: 48 + }, + onPoint: { + connectorOptions: { + dashstyle: 'Dash', + stroke: '#ccc', + width: 2 + }, + id: 'some-id', + position: { + align: 'left', + verticalAlign: 'top', + x: 15, + y: -46 + } + }, + opacity: 0.2, + point: { + events: { + click: function(event) { return true; }, + drag: function(event) { return true; }, + drop: function(event) { return true; }, + mouseOut: function(event) { return true; } + } + }, + pointDescriptionFormatter: function (point) { return true; }, + selected: false, + showCheckbox: true, + showInLegend: true, + skipKeyboardNavigation: false, + states: { + hover: { + animation: { + duration: 123 + }, + borderColor: '#cccccc', + brightness: 0.3, + enabled: true + }, + inactive: { + enabled: true, + opacity: 0.5 + }, + normal: { + animation: { + defer: 24 + } + }, + select: { + color: '#ff0000', + enabled: true, + } + }, + stickyTracking: true, + threshold: 123, + tooltip: { + animation: true, + backgroundColor: '#ccc', + borderColor: '#999', + borderRadius: 4, + borderWidth: 1, + className: 'some-class-name', + clusterFormat: 'format string', + dateTimeLabelFormats: { + day: 'test', + hour: 'test', + millisecond: 'test', + minute: 'test', + month: 'test', + second: 'test', + week: 'test', + year: 'test' + }, + distance: 12, + enabled: true, + followPointer: true, + followTouchMove: true, + footerFormat: 'format string', + formatter: function() { return true; }, + headerFormat: 'format string', + headerShape: 'circle', + hideDelay: 3, + nullFormat: 'format string', + nullFormatter: function() { return true; }, + outside: false, + padding: 6, + pointFormat: 'format string', + pointFormatter: function() { return true; }, + positioner: function() { return true; }, + shadow: false, + shape: 'rect', + shared: false, + snap: 4, + split: false, + stickOnContact: true, + style: 'style string goes here', + useHTML: false, + valueDecimals: 2, + valuePrefix: '$', + valueSuffix: ' USD', + xDateFormat: 'format string' + }, + turboThreshold: 'invalid value', + visible: true +} diff --git a/tests/input_files/series/pictorial/01.js b/tests/input_files/series/pictorial/01.js new file mode 100644 index 00000000..c300767d --- /dev/null +++ b/tests/input_files/series/pictorial/01.js @@ -0,0 +1,14 @@ +{ + id: 'some-id-goes-here', + index: 3, + legendIndex: 3, + name: 'Series Name Goes Here', + grouping: false, + groupPadding: 6, + maxPointWidth: 12, + minPointLength: 12, + pointPadding: 6, + pointRange: 24, + pointWidth: 12, + type: 'pictorial' +} \ No newline at end of file diff --git a/tests/input_files/series/pictorial/error-01.js b/tests/input_files/series/pictorial/error-01.js new file mode 100644 index 00000000..377682d0 --- /dev/null +++ b/tests/input_files/series/pictorial/error-01.js @@ -0,0 +1,168 @@ +{ + data: [ + { + borderColor: '#ccc', + borderWidth: 2, + dashStyle: 'Solid', + pointWidth: 12 + }, + { + borderColor: '#ccc', + borderWidth: 2, + dashStyle: 'Solid', + pointWidth: 12, + + dataLabels: { + align: 'center', + allowOverlap: True, + animation: { + defer: 5 + }, + backgroundColor: { + linearGradient: { + x1: 0.123, + x2: 0.234, + y1: 0.345, + y2: 0.456 + }, + stops: [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + borderColor: '#999999', + borderRadius: 24, + borderWidth: 1, + className: 'some-class-name', + color: '#000000', + crop: True, + defer: False, + enabled: True, + filter: { + operator: '>=', + property: 'some_property', + value: 123 + }, + format: 'some format', + formatter: """function() { return true; }""", + inside: True, + nullFormat: 'some format', + nullFormatter: """function() { return true; }""", + overflow: 'none', + padding: 12, + position: 'center', + rotation: 0, + shadow: False, + shape: 'rect', + style: 'style goes here', + useHTML: False, + verticalAlign: 'top', + x: 10, + y: 20, + z: 0 + }, + dragDrop: { + draggableX: True, + draggableY: True, + dragHandle: { + className: 'draghandle-classname-goes-here', + color: '#ccc', + cursor: 'alias', + lineColor: '#ddd', + lineWidth: 2, + pathFormatter: """function() { return true; }""", + zIndex: 10 + }, + dragMaxX: 3456, + dragMaxY: 6532, + dragMinX: 123, + dragMinY: 321, + dragPrecisionX: 5, + dragPrecisionY: 5, + dragSensitivity: 2, + groupBy: 'some-property-name', + guideBox: { + default: { + className: 'some-classname-goes-here', + color: '#999', + cursor: 'pointer', + lineColor: '#ccc', + lineWidth: 2, + zIndex: 100 + } + }, + liveRedraw: True + }, + drilldown: 'some-id-goes-here', + marker: { + enabled: True, + fillColor: '#cccccc', + height: 24, + lineWidth: 2, + radius: 2, + states: { + hover: { + enabled: True + } + }, + symbol: 'circle', + width: 48 + }, + x: 'some category', + y: 123 + } + ], + id: 'some-id-goes-here', + index: 'invalid value', + legendIndex: 3, + name: 'Series Name Goes Here', + stack: 'stack-id', + xAxis: 'some-id', + yAxis: 0, + zIndex: 3, + + borderColor: '#ccc', + borderRadius: 4, + borderWidth: 2, + centerInCategory: true, + colorByPoint: true, + colors: [ + '#fff', + '#ccc', + { + linearGradient: { + x1: 0.123, + x2: 0.567, + y1: 0.891, + y2: 0.987 + }, + stops: [ + [0.123, '#cccccc'], + [0.456, '#ff0000'], + [1, '#00ff00'] + ] + }, + { + animation: { + defer: 5 + }, + patternOptions: { + aspectRatio: 0.5, + backgroundColor: '#999999', + id: 'some_id_goes_here', + opacity: 0.5, + width: 120, + x: 5, + y: 10 + }, + patternIndex: 2 + } + ], + grouping: false, + groupPadding: 6, + maxPointWidth: 12, + minPointLength: 12, + pointPadding: 6, + pointRange: 24, + pointWidth: 12 +} diff --git a/tests/options/plot_options/test_pictorial.py b/tests/options/plot_options/test_pictorial.py new file mode 100644 index 00000000..4784848f --- /dev/null +++ b/tests/options/plot_options/test_pictorial.py @@ -0,0 +1,97 @@ +"""Tests for ``highcharts.no_data``.""" + +import pytest + +from json.decoder import JSONDecodeError + +from highcharts_core.options.plot_options.pictorial import PictorialOptions as cls +from highcharts_core import errors +from tests.fixtures import input_files, check_input_file, to_camelCase, to_js_dict, \ + Class__init__, Class__to_untrimmed_dict, Class_from_dict, Class_to_dict, \ + Class_from_js_literal + +STANDARD_PARAMS = [ + ({}, None), + ({ + 'depth': 10, + 'edge_color': '#999', + 'edge_width': 1, + 'group_z_padding': 4 + }, None), + # + Base Bar Options + ({ + 'depth': 10, + 'edge_color': '#999', + 'edge_width': 1, + 'group_z_padding': 4, + 'grouping': False, + 'group_padding': 6, + 'max_point_width': 12, + 'min_point_length': 12, + 'point_padding': 6, + 'point_range': 24, + 'point_width': 12 + }, None), +] + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_PictorialOptions__init__(kwargs, error): + Class__init__(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_PictorialOptions__to_untrimmed_dict(kwargs, error): + Class__to_untrimmed_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_PictorialOptions_from_dict(kwargs, error): + Class_from_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_PictorialOptions_to_dict(kwargs, error): + Class_to_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('filename, as_file, error', [ + ('plot_options/pictorial/03.js', False, None), + ('plot_options/pictorial/04.js', False, None), + + ('plot_options/pictorial/error-01.js', + False, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + ('plot_options/pictorial/error-02.js', + False, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + + ('plot_options/pictorial/03.js', True, None), + ('plot_options/pictorial/04.js', True, None), + + ('plot_options/pictorial/error-01.js', + True, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + ('plot_options/pictorial/error-02.js', + True, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + +]) +def test_PictorialOptions_from_js_literal(input_files, filename, as_file, error): + Class_from_js_literal(cls, input_files, filename, as_file, error) diff --git a/tests/options/series/test_pictorial.py b/tests/options/series/test_pictorial.py new file mode 100644 index 00000000..6bedca56 --- /dev/null +++ b/tests/options/series/test_pictorial.py @@ -0,0 +1,682 @@ +"""Tests for ``highcharts.no_data``.""" + +import pytest +import datetime + +from json.decoder import JSONDecodeError + +from highcharts_core.options.series.pictorial import PictorialSeries as cls +from highcharts_core import errors +from tests.fixtures import input_files, check_input_file, to_camelCase, to_js_dict, \ + Class__init__, Class__to_untrimmed_dict, Class_from_dict, Class_to_dict, \ + Class_from_js_literal + +STANDARD_PARAMS = [ + ({}, None), + ({ + 'data': [ + { + 'dataLabels': { + 'align': 'center', + 'allowOverlap': True, + 'animation': { + 'defer': 5 + }, + 'backgroundColor': { + 'linearGradient': { + 'x1': 0.123, + 'x2': 0.234, + 'y1': 0.345, + 'y2': 0.456 + }, + 'stops': [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + 'borderColor': '#999999', + 'borderRadius': 24, + 'borderWidth': 1, + 'className': 'some-class-name', + 'color': '#000000', + 'crop': True, + 'defer': False, + 'enabled': True, + 'filter': { + 'operator': '>=', + 'property': 'some_property', + 'value': 123 + }, + 'format': 'some format', + 'formatter': """function() { return true; }""", + 'inside': True, + 'nullFormat': 'some format', + 'nullFormatter': """function() { return true; }""", + 'overflow': 'none', + 'padding': 12, + 'position': 'center', + 'rotation': 0, + 'shadow': False, + 'shape': 'rect', + 'style': 'style goes here', + 'useHTML': False, + 'verticalAlign': 'top', + 'x': 10, + 'y': 20, + 'z': 0 + }, + 'dragDrop': { + 'draggableX': True, + 'draggableY': True, + 'dragHandle': { + 'className': 'draghandle-classname-goes-here', + 'color': '#ccc', + 'cursor': 'alias', + 'lineColor': '#ddd', + 'lineWidth': 2, + 'pathFormatter': """function() { return true; }""", + 'zIndex': 10 + }, + 'dragMaxX': 3456, + 'dragMaxY': 6532, + 'dragMinX': 123, + 'dragMinY': 321, + 'dragPrecisionX': 5, + 'dragPrecisionY': 5, + 'dragSensitivity': 2, + 'groupBy': 'some-property-name', + 'guideBox': { + 'default': { + 'className': 'some-classname-goes-here', + 'color': '#999', + 'cursor': 'pointer', + 'lineColor': '#ccc', + 'lineWidth': 2, + 'zIndex': 100 + } + }, + 'liveRedraw': True + }, + 'drilldown': 'some-id-goes-here', + 'marker': { + 'enabled': True, + 'fillColor': '#cccccc', + 'height': 24, + 'lineWidth': 2, + 'radius': 2, + 'states': { + 'hover': { + 'enabled': True + } + }, + 'symbol': 'circle', + 'width': 48 + }, + 'x': 'some category', + 'y': 123 + }, + { + 'dataLabels': { + 'align': 'center', + 'allowOverlap': True, + 'animation': { + 'defer': 5 + }, + 'backgroundColor': { + 'linearGradient': { + 'x1': 0.123, + 'x2': 0.234, + 'y1': 0.345, + 'y2': 0.456 + }, + 'stops': [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + 'borderColor': '#999999', + 'borderRadius': 24, + 'borderWidth': 1, + 'className': 'some-class-name', + 'color': '#000000', + 'crop': True, + 'defer': False, + 'enabled': True, + 'filter': { + 'operator': '>=', + 'property': 'some_property', + 'value': 123 + }, + 'format': 'some format', + 'formatter': """function() { return true; }""", + 'inside': True, + 'nullFormat': 'some format', + 'nullFormatter': """function() { return true; }""", + 'overflow': 'none', + 'padding': 12, + 'position': 'center', + 'rotation': 0, + 'shadow': False, + 'shape': 'rect', + 'style': 'style goes here', + 'useHTML': False, + 'verticalAlign': 'top', + 'x': 10, + 'y': 20, + 'z': 0 + }, + 'dragDrop': { + 'draggableX': True, + 'draggableY': True, + 'dragHandle': { + 'className': 'draghandle-classname-goes-here', + 'color': '#ccc', + 'cursor': 'alias', + 'lineColor': '#ddd', + 'lineWidth': 2, + 'pathFormatter': """function() { return true; }""", + 'zIndex': 10 + }, + 'dragMaxX': 3456, + 'dragMaxY': 6532, + 'dragMinX': 123, + 'dragMinY': 321, + 'dragPrecisionX': 5, + 'dragPrecisionY': 5, + 'dragSensitivity': 2, + 'groupBy': 'some-property-name', + 'guideBox': { + 'default': { + 'className': 'some-classname-goes-here', + 'color': '#999', + 'cursor': 'pointer', + 'lineColor': '#ccc', + 'lineWidth': 2, + 'zIndex': 100 + } + }, + 'liveRedraw': True + }, + 'drilldown': 'some-id-goes-here', + 'marker': { + 'enabled': True, + 'fillColor': '#cccccc', + 'height': 24, + 'lineWidth': 2, + 'radius': 2, + 'states': { + 'hover': { + 'enabled': True + } + }, + 'symbol': 'circle', + 'width': 48 + }, + 'x': datetime.datetime(2022, 7, 26, 0, 4, 0), + 'y': 123 + }, + { + 'dataLabels': { + 'align': 'center', + 'allowOverlap': True, + 'animation': { + 'defer': 5 + }, + 'backgroundColor': { + 'linearGradient': { + 'x1': 0.123, + 'x2': 0.234, + 'y1': 0.345, + 'y2': 0.456 + }, + 'stops': [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + 'borderColor': '#999999', + 'borderRadius': 24, + 'borderWidth': 1, + 'className': 'some-class-name', + 'color': '#000000', + 'crop': True, + 'defer': False, + 'enabled': True, + 'filter': { + 'operator': '>=', + 'property': 'some_property', + 'value': 123 + }, + 'format': 'some format', + 'formatter': """function() { return true; }""", + 'inside': True, + 'nullFormat': 'some format', + 'nullFormatter': """function() { return true; }""", + 'overflow': 'none', + 'padding': 12, + 'position': 'center', + 'rotation': 0, + 'shadow': False, + 'shape': 'rect', + 'style': 'style goes here', + 'useHTML': False, + 'verticalAlign': 'top', + 'x': 10, + 'y': 20, + 'z': 0 + }, + 'dragDrop': { + 'draggableX': True, + 'draggableY': True, + 'dragHandle': { + 'className': 'draghandle-classname-goes-here', + 'color': '#ccc', + 'cursor': 'alias', + 'lineColor': '#ddd', + 'lineWidth': 2, + 'pathFormatter': """function() { return true; }""", + 'zIndex': 10 + }, + 'dragMaxX': 3456, + 'dragMaxY': 6532, + 'dragMinX': 123, + 'dragMinY': 321, + 'dragPrecisionX': 5, + 'dragPrecisionY': 5, + 'dragSensitivity': 2, + 'groupBy': 'some-property-name', + 'guideBox': { + 'default': { + 'className': 'some-classname-goes-here', + 'color': '#999', + 'cursor': 'pointer', + 'lineColor': '#ccc', + 'lineWidth': 2, + 'zIndex': 100 + } + }, + 'liveRedraw': True + }, + 'drilldown': 'some-id-goes-here', + 'marker': { + 'enabled': True, + 'fillColor': '#cccccc', + 'height': 24, + 'lineWidth': 2, + 'radius': 2, + 'states': { + 'hover': { + 'enabled': True + } + }, + 'symbol': 'circle', + 'width': 48 + }, + 'x': datetime.date(2022, 7, 26), + 'y': 123 + } + ], + 'id': 'some-id-goes-here', + 'index': 3, + 'legend_index': 3, + 'name': 'Series Name Goes Here', + }, None), + ({ + 'data': [ + { + 'dataLabels': { + 'align': 'center', + 'allowOverlap': True, + 'animation': { + 'defer': 5 + }, + 'backgroundColor': { + 'linearGradient': { + 'x1': 0.123, + 'x2': 0.234, + 'y1': 0.345, + 'y2': 0.456 + }, + 'stops': [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + 'borderColor': '#999999', + 'borderRadius': 24, + 'borderWidth': 1, + 'className': 'some-class-name', + 'color': '#000000', + 'crop': True, + 'defer': False, + 'enabled': True, + 'filter': { + 'operator': '>=', + 'property': 'some_property', + 'value': 123 + }, + 'format': 'some format', + 'formatter': """function() { return true; }""", + 'inside': True, + 'nullFormat': 'some format', + 'nullFormatter': """function() { return true; }""", + 'overflow': 'none', + 'padding': 12, + 'position': 'center', + 'rotation': 0, + 'shadow': False, + 'shape': 'rect', + 'style': 'style goes here', + 'useHTML': False, + 'verticalAlign': 'top', + 'x': 10, + 'y': 20, + 'z': 0 + }, + 'dragDrop': { + 'draggableX': True, + 'draggableY': True, + 'dragHandle': { + 'className': 'draghandle-classname-goes-here', + 'color': '#ccc', + 'cursor': 'alias', + 'lineColor': '#ddd', + 'lineWidth': 2, + 'pathFormatter': """function() { return true; }""", + 'zIndex': 10 + }, + 'dragMaxX': 3456, + 'dragMaxY': 6532, + 'dragMinX': 123, + 'dragMinY': 321, + 'dragPrecisionX': 5, + 'dragPrecisionY': 5, + 'dragSensitivity': 2, + 'groupBy': 'some-property-name', + 'guideBox': { + 'default': { + 'className': 'some-classname-goes-here', + 'color': '#999', + 'cursor': 'pointer', + 'lineColor': '#ccc', + 'lineWidth': 2, + 'zIndex': 100 + } + }, + 'liveRedraw': True + }, + 'drilldown': 'some-id-goes-here', + 'marker': { + 'enabled': True, + 'fillColor': '#cccccc', + 'height': 24, + 'lineWidth': 2, + 'radius': 2, + 'states': { + 'hover': { + 'enabled': True + } + }, + 'symbol': 'circle', + 'width': 48 + }, + 'x': 'some category', + 'y': 123 + }, + { + 'dataLabels': { + 'align': 'center', + 'allowOverlap': True, + 'animation': { + 'defer': 5 + }, + 'backgroundColor': { + 'linearGradient': { + 'x1': 0.123, + 'x2': 0.234, + 'y1': 0.345, + 'y2': 0.456 + }, + 'stops': [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + 'borderColor': '#999999', + 'borderRadius': 24, + 'borderWidth': 1, + 'className': 'some-class-name', + 'color': '#000000', + 'crop': True, + 'defer': False, + 'enabled': True, + 'filter': { + 'operator': '>=', + 'property': 'some_property', + 'value': 123 + }, + 'format': 'some format', + 'formatter': """function() { return true; }""", + 'inside': True, + 'nullFormat': 'some format', + 'nullFormatter': """function() { return true; }""", + 'overflow': 'none', + 'padding': 12, + 'position': 'center', + 'rotation': 0, + 'shadow': False, + 'shape': 'rect', + 'style': 'style goes here', + 'useHTML': False, + 'verticalAlign': 'top', + 'x': 10, + 'y': 20, + 'z': 0 + }, + 'dragDrop': { + 'draggableX': True, + 'draggableY': True, + 'dragHandle': { + 'className': 'draghandle-classname-goes-here', + 'color': '#ccc', + 'cursor': 'alias', + 'lineColor': '#ddd', + 'lineWidth': 2, + 'pathFormatter': """function() { return true; }""", + 'zIndex': 10 + }, + 'dragMaxX': 3456, + 'dragMaxY': 6532, + 'dragMinX': 123, + 'dragMinY': 321, + 'dragPrecisionX': 5, + 'dragPrecisionY': 5, + 'dragSensitivity': 2, + 'groupBy': 'some-property-name', + 'guideBox': { + 'default': { + 'className': 'some-classname-goes-here', + 'color': '#999', + 'cursor': 'pointer', + 'lineColor': '#ccc', + 'lineWidth': 2, + 'zIndex': 100 + } + }, + 'liveRedraw': True + }, + 'drilldown': 'some-id-goes-here', + 'marker': { + 'enabled': True, + 'fillColor': '#cccccc', + 'height': 24, + 'lineWidth': 2, + 'radius': 2, + 'states': { + 'hover': { + 'enabled': True + } + }, + 'symbol': 'circle', + 'width': 48 + }, + 'x': datetime.datetime(2022, 7, 26, 0, 4, 0), + 'y': 123 + }, + { + 'dataLabels': { + 'align': 'center', + 'allowOverlap': True, + 'animation': { + 'defer': 5 + }, + 'backgroundColor': { + 'linearGradient': { + 'x1': 0.123, + 'x2': 0.234, + 'y1': 0.345, + 'y2': 0.456 + }, + 'stops': [ + [0.12, '#999'], + [0.34, '#fff'] + ] + }, + 'borderColor': '#999999', + 'borderRadius': 24, + 'borderWidth': 1, + 'className': 'some-class-name', + 'color': '#000000', + 'crop': True, + 'defer': False, + 'enabled': True, + 'filter': { + 'operator': '>=', + 'property': 'some_property', + 'value': 123 + }, + 'format': 'some format', + 'formatter': """function() { return true; }""", + 'inside': True, + 'nullFormat': 'some format', + 'nullFormatter': """function() { return true; }""", + 'overflow': 'none', + 'padding': 12, + 'position': 'center', + 'rotation': 0, + 'shadow': False, + 'shape': 'rect', + 'style': 'style goes here', + 'useHTML': False, + 'verticalAlign': 'top', + 'x': 10, + 'y': 20, + 'z': 0 + }, + 'dragDrop': { + 'draggableX': True, + 'draggableY': True, + 'dragHandle': { + 'className': 'draghandle-classname-goes-here', + 'color': '#ccc', + 'cursor': 'alias', + 'lineColor': '#ddd', + 'lineWidth': 2, + 'pathFormatter': """function() { return true; }""", + 'zIndex': 10 + }, + 'dragMaxX': 3456, + 'dragMaxY': 6532, + 'dragMinX': 123, + 'dragMinY': 321, + 'dragPrecisionX': 5, + 'dragPrecisionY': 5, + 'dragSensitivity': 2, + 'groupBy': 'some-property-name', + 'guideBox': { + 'default': { + 'className': 'some-classname-goes-here', + 'color': '#999', + 'cursor': 'pointer', + 'lineColor': '#ccc', + 'lineWidth': 2, + 'zIndex': 100 + } + }, + 'liveRedraw': True + }, + 'drilldown': 'some-id-goes-here', + 'marker': { + 'enabled': True, + 'fillColor': '#cccccc', + 'height': 24, + 'lineWidth': 2, + 'radius': 2, + 'states': { + 'hover': { + 'enabled': True + } + }, + 'symbol': 'circle', + 'width': 48 + }, + 'x': datetime.date(2022, 7, 26), + 'y': 123 + } + ], + 'id': 'some-id-goes-here', + 'index': 3, + 'legend_index': 3, + 'name': 'Series Name Goes Here', + 'paths': { + 'definition': None, + 'max': 123 + }, + }, None), +] + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_PictorialSeries__init__(kwargs, error): + Class__init__(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_PictorialSeries__to_untrimmed_dict(kwargs, error): + Class__to_untrimmed_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_PictorialSeries_from_dict(kwargs, error): + Class_from_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_PictorialSeries_to_dict(kwargs, error): + Class_to_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('filename, as_file, error', [ + ('series/pictorial/01.js', False, None), + + ('series/pictorial/error-01.js', + False, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + + ('series/pictorial/01.js', True, None), + + ('series/pictorial/error-01.js', + True, + (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError, + ValueError)), + +]) +def test_PictorialSeries_from_js_literal(input_files, filename, as_file, error): + Class_from_js_literal(cls, input_files, filename, as_file, error) From 6648dc17a18bed5ffdf5e44b6603d43f2bc2c5e2 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Sun, 23 Apr 2023 14:21:49 -0400 Subject: [PATCH 12/24] Fixed missing property in OrganizationDataLabel. --- highcharts_core/utility_classes/data_labels.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/highcharts_core/utility_classes/data_labels.py b/highcharts_core/utility_classes/data_labels.py index 71264a60..c36ca598 100644 --- a/highcharts_core/utility_classes/data_labels.py +++ b/highcharts_core/utility_classes/data_labels.py @@ -1016,9 +1016,11 @@ class OrganizationDataLabel(DataLabel): def __init__(self, **kwargs): self._link_format = None + self._link_formatter = None self._link_text_path = None self.link_format = kwargs.get('link_format', None) + self.link_formatter = kwargs.get('link_formatter', None) self.link_text_path = kwargs.get('link_text_path', None) super().__init__(**kwargs) @@ -1077,7 +1079,6 @@ def link_text_path(self) -> Optional[TextPath]: def link_text_path(self, value): self._link_text_path = value - @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { From b6a12b6949bd2775e15d1d169be41488e3a95156 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 24 Apr 2023 21:52:35 -0400 Subject: [PATCH 13/24] Added XAxisOptions.minor_ticks_per_major. --- CHANGES.rst | 1 + highcharts_core/options/axes/x_axis.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bda282ff..978e912e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -19,6 +19,7 @@ Release 2.0.0 values. * Added ``.description_format`` property to ``options.plot_options.accessibility.TypeOptionsAccessibility``. * Added ``PictorialOptions`` / ``PictorialSeries`` series type with related classes. + * Added ``.minor_ticks_per_major`` to ``options.axes.x_axis.XAxisOptions``. * **FIXED:** Broken heatmap and tilemap documentation links. * **FIXED:** Fixed missing ``TreegraphOptions`` / ``TreegraphSeries`` series type. diff --git a/highcharts_core/options/axes/x_axis.py b/highcharts_core/options/axes/x_axis.py index f77a05a3..2935f876 100644 --- a/highcharts_core/options/axes/x_axis.py +++ b/highcharts_core/options/axes/x_axis.py @@ -24,6 +24,7 @@ def __init__(self, **kwargs): self._left = None self._line_color = None self._line_width = None + self._minor_ticks_per_major = None self._show_empty = None self._top = None self._width = None @@ -33,6 +34,7 @@ def __init__(self, **kwargs): self.left = kwargs.get('left', None) self.line_color = kwargs.get('line_color', None) self.line_width = kwargs.get('line_width', None) + self.minor_ticks_per_major = kwargs.get('minor_ticks_per_major', None) self.show_empty = kwargs.get('show_empty', None) self.top = kwargs.get('top', None) self.width = kwargs.get('width', None) @@ -135,6 +137,23 @@ def line_width(self, value): minimum = 0) @property + def minor_ticks_per_major(self) -> Optional[int | float | Decimal]: + """The number of minor ticks per major tick. Defaults to ``5``. + + .. note:: + + Works for ``linear``, ``logarithmic`` and ``datetime`` axes. + + :rtype: numeric or :obj:`None ` + """ + return self._minor_ticks_per_major + + @minor_ticks_per_major.setter + def minor_ticks_per_major(self, value): + self._minor_ticks_per_major = validators.numeric(value, + allow_empty = True, + minimum = 0) + @property def show_empty(self) -> Optional[bool]: """If ``True``, render the axis title and axis line even if the axis has no data. If ``False``, does not render the axis when the axis has no data. Defaults to @@ -277,6 +296,7 @@ def _get_kwargs_from_dict(cls, as_dict): 'left': as_dict.get('left', None), 'line_color': as_dict.get('lineColor', None), 'line_width': as_dict.get('lineWidth', None), + 'minor_ticks_per_major': as_dict.get('minorTicksPerMajor', None), 'show_empty': as_dict.get('showEmpty', None), 'top': as_dict.get('top', None), 'width': as_dict.get('width', None) @@ -291,6 +311,7 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: 'left': self.left, 'lineColor': self.line_color, 'lineWidth': self.line_width, + 'minorTicksPerMajor': self.minor_ticks_per_major, 'showEmpty': self.show_empty, 'top': self.top, 'width': self.width From ccb5529dd7ac290b3518c63fecc5409b577ba973 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 24 Apr 2023 22:35:53 -0400 Subject: [PATCH 14/24] Added options.axes.y_axis.YAxisOptions.stack_shadow. --- CHANGES.rst | 1 + docs/api.rst | 1 + docs/api/options/axes/index.rst | 1 + docs/api/options/axes/y_axis.rst | 20 +++ docs/api/options/index.rst | 6 + highcharts_core/options/axes/y_axis.py | 130 ++++++++++++++++++ .../options/plot_options/__init__.py | 3 +- tests/options/axes/test_y_axis.py | 8 ++ 8 files changed, 169 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 978e912e..8bd718d3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,7 @@ Release 2.0.0 * Added ``.description_format`` property to ``options.plot_options.accessibility.TypeOptionsAccessibility``. * Added ``PictorialOptions`` / ``PictorialSeries`` series type with related classes. * Added ``.minor_ticks_per_major`` to ``options.axes.x_axis.XAxisOptions``. + * Added ``.stack_shadow`` to ``options.axes.y_axis.YAxisOptions``. * **FIXED:** Broken heatmap and tilemap documentation links. * **FIXED:** Fixed missing ``TreegraphOptions`` / ``TreegraphSeries`` series type. diff --git a/docs/api.rst b/docs/api.rst index c6bad721..40d6a46a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -211,6 +211,7 @@ Core Components - :class:`XAxis ` * - :mod:`.options.axes.y_axis ` - :class:`YAxis ` + :class:`StackShadow ` * - :mod:`.options.axes.z_axis ` - :class:`ZAxis ` * - :mod:`.options.boost ` diff --git a/docs/api/options/axes/index.rst b/docs/api/options/axes/index.rst index 0160a3bf..0a4d5996 100644 --- a/docs/api/options/axes/index.rst +++ b/docs/api/options/axes/index.rst @@ -71,5 +71,6 @@ Sub-components - :class:`XAxis ` * - :mod:`.options.axes.y_axis ` - :class:`YAxis ` + :class:`StackShadow ` * - :mod:`.options.axes.z_axis ` - :class:`ZAxis ` diff --git a/docs/api/options/axes/y_axis.rst b/docs/api/options/axes/y_axis.rst index 49bc41ea..efc35ef3 100644 --- a/docs/api/options/axes/y_axis.rst +++ b/docs/api/options/axes/y_axis.rst @@ -26,3 +26,23 @@ class: :class:`YAxis ` :parts: -1 | + +-------------- + +.. module:: highcharts_core.options.axes.y_axis + +******************************************************************************************************************** +class: :class:`StackShadow ` +******************************************************************************************************************** + +.. autoclass:: StackShadow + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: StackShadow + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | diff --git a/docs/api/options/index.rst b/docs/api/options/index.rst index 63d557f0..56ab9a8d 100644 --- a/docs/api/options/index.rst +++ b/docs/api/options/index.rst @@ -139,6 +139,7 @@ Sub-components - :class:`XAxis ` * - :mod:`.options.axes.y_axis ` - :class:`YAxis ` + :class:`StackShadow ` * - :mod:`.options.axes.z_axis ` - :class:`ZAxis ` * - :mod:`.options.boost ` @@ -291,6 +292,8 @@ Sub-components :class:`ParentNodeOptions ` * - :mod:`.options.plot_options.pareto ` - :class:`ParetoOptions ` + * - :mod:`.options.plot_options.pictorial ` + - :class:`PictorialOptions ` * - :mod:`.options.plot_options.pie ` - :class:`PieOptions ` :class:`VariablePieOptions ` @@ -444,6 +447,9 @@ Sub-components - :class:`PackedBubbleSeries ` * - :mod:`.options.series.pareto ` - :class:`ParetoSeries ` + * - :mod:`.options.series.pictorial ` + - :class:`PictorialSeries ` + :class:`PictorialPaths ` * - :mod:`.options.series.pie ` - :class:`PieSeries ` :class:`VariablePieSeries ` diff --git a/highcharts_core/options/axes/y_axis.py b/highcharts_core/options/axes/y_axis.py index 2f9373f0..d920d8ae 100644 --- a/highcharts_core/options/axes/y_axis.py +++ b/highcharts_core/options/axes/y_axis.py @@ -4,14 +4,117 @@ from validator_collection import validators from highcharts_core import errors +from highcharts_core.metaclasses import HighchartsMeta from highcharts_core.decorators import class_sensitive from highcharts_core.utility_classes.gradients import Gradient from highcharts_core.utility_classes.patterns import Pattern from highcharts_core.utility_classes.data_labels import DataLabel +from highcharts_core.utility_functions import validate_color from highcharts_core.options.axes.x_axis import XAxis +class StackShadow(HighchartsMeta): + """Configures the background of stacked points. + + .. versionadded:: Highcharts Core (JS) v.11.0.0 + + .. note:: + + Only applies to the :class:`PictorialSeries `. + + """ + + def __init__(self, **kwargs): + self._border_color = None + self._border_width = None + self._color = None + self._enabled = None + + self.border_color = kwargs.get('border_color', None) + self.border_width = kwargs.get('border_width', None) + self.color = kwargs.get('color', None) + self.enabled = kwargs.get('enabled', None) + + @property + def border_color(self) -> Optional[str | Gradient | Pattern]: + """The color of the stack shadow border. Defaults to ``'transparent'``. + + :rtype: :class:`str ` or + :class:`Gradient ` or + :class:`Pattern ` or + :obj:`None ` + """ + return self._border_color + + @border_color.setter + def border_color(self, value): + self._border_color = validate_color(value) + + @property + def border_width(self) -> Optional[int | float | Decimal]: + """The width of the border surrounding the stack shadow. Defaults to ``0``. + + :rtype: numeric or :obj:`None ` + """ + return self._border_width + + @border_width.setter + def border_width(self, value): + self._border_width = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def color(self) -> Optional[str | Gradient | Pattern]: + """The color of the stack shadow. Defaults to ``#dedede``. + + :rtype: :class:`str `, :class:`Gradient`, :class:`Pattern``, or + :obj:`None ` + + """ + return self._color + + @color.setter + def color(self, value): + self._color = validate_color(value) + + @property + def enabled(self) -> Optional[bool]: + """If ``True``, enable the stack shadow. Defaults to :obj:`None `. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._enabled + + @enabled.setter + def enabled(self, value): + if value is None: + self._enabled = None + else: + self._enabled = bool(value) + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'border_color': as_dict.get('borderColor', None), + 'border_width': as_dict.get('borderWidth', None), + 'color': as_dict.get('color', None), + 'enabled': as_dict.get('enabled', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'borderColor': self.border_color, + 'borderWidth': self.border_width, + 'color': self.color, + 'enabled': self.enabled, + } + + return untrimmed + class YAxis(XAxis): """Configuration settings for the Y axis or value axis. @@ -22,12 +125,14 @@ def __init__(self, **kwargs): self._max_color = None self._min_color = None self._stack_labels = None + self._stack_shadow = None self._stops = None self._tooltip_value_format = None self.max_color = kwargs.get('max_color', None) self.min_color = kwargs.get('min_color', None) self.stack_labels = kwargs.get('stack_labels', None) + self.stack_shadow = kwargs.get('stack_shadow', None) self.stops = kwargs.get('stops', None) self.tooltip_value_format = kwargs.get('tooltip_value_format', None) @@ -97,6 +202,29 @@ def stack_labels(self) -> Optional[DataLabel]: def stack_labels(self, value): self._stack_labels = value + @property + def stack_shadow(self) -> Optional[StackShadow]: + """Configures the background of stacked points. + + .. versionadded:: Highcharts Core (JS) v.11.0.0 + + .. note:: + + Only applies to the :class:`PictorialSeries ` + + .. warning:: + + Requires ``series.stacking`` to be defined. + + :rtype: :class:`StackShadow ` or :obj:`None ` + """ + return self._stack_shadow + + @stack_shadow.setter + @class_sensitive(StackShadow) + def stack_shadow(self, value): + self._stack_shadow = value + @property def stops(self) -> Optional[List[List[int | float | Decimal | str]]]: """Color stops for use in the gradient of a solid gauge. Defaults to @@ -251,6 +379,7 @@ def _get_kwargs_from_dict(cls, as_dict): 'max_color': as_dict.get('maxColor', None), 'min_color': as_dict.get('minColor', None), 'stack_labels': as_dict.get('stackLabels', None), + 'stack_shadow': as_dict.get('stackShadow', None), 'stops': as_dict.get('stops', None), 'tooltip_value_format': as_dict.get('tooltipValueFormat', None) } @@ -262,6 +391,7 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: 'maxColor': self.max_color, 'minColor': self.min_color, 'stackLabels': self.stack_labels, + 'stackShadow': self.stack_shadow, 'stops': self.stops, 'tooltipValueFormat': self.tooltip_value_format } diff --git a/highcharts_core/options/plot_options/__init__.py b/highcharts_core/options/plot_options/__init__.py index 3093451b..2dcffb96 100644 --- a/highcharts_core/options/plot_options/__init__.py +++ b/highcharts_core/options/plot_options/__init__.py @@ -829,7 +829,8 @@ def pictorial(self) -> Optional[PictorialOptions]: :align: center - :rtype: :class:`ParetoOptions` or :obj:`None ` + :rtype: :class:`PictorialOptions ` or + :obj:`None ` """ return self._pictorial diff --git a/tests/options/axes/test_y_axis.py b/tests/options/axes/test_y_axis.py index 9d2830fb..18714c19 100644 --- a/tests/options/axes/test_y_axis.py +++ b/tests/options/axes/test_y_axis.py @@ -12,6 +12,14 @@ STANDARD_PARAMS = [ ({}, None), + ({ + 'stack_shadow': { + 'enabled': True, + 'borderColor': '#ccc', + 'borderWidth': 1, + 'color': '#000' + } + }, None), # Y-Axis propeties only ({ 'max_color': '#ccc', From 3c5bfa1861a9b31baaa6500f2370b15929729b11 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Apr 2023 11:22:30 -0400 Subject: [PATCH 15/24] Added SimulationEvents and related. --- docs/api.rst | 1 + docs/api/utility_classes/events.rst | 18 +++++++ docs/api/utility_classes/index.rst | 1 + .../options/plot_options/networkgraph.py | 20 ++++++++ highcharts_core/utility_classes/events.py | 51 +++++++++++++++++++ 5 files changed, 91 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 40d6a46a..96a9c57a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -599,6 +599,7 @@ Core Components :class:`NavigationEvents ` :class:`PointEvents ` :class:`SeriesEvents ` + :class:`SimulationEvents ` :class:`ClusterEvents ` :class:`AxisEvents ` :class:`MouseEvents ` diff --git a/docs/api/utility_classes/events.rst b/docs/api/utility_classes/events.rst index 8841c6d9..bb2a4de1 100644 --- a/docs/api/utility_classes/events.rst +++ b/docs/api/utility_classes/events.rst @@ -101,6 +101,24 @@ class: :class:`SeriesEvents ` +******************************************************************************************************************** + +.. autoclass:: SimulationEvents + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: SimulationEvents + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +------------------ + ******************************************************************************************************************** class: :class:`ClusterEvents ` ******************************************************************************************************************** diff --git a/docs/api/utility_classes/index.rst b/docs/api/utility_classes/index.rst index 6a6e86e8..ca472ef4 100644 --- a/docs/api/utility_classes/index.rst +++ b/docs/api/utility_classes/index.rst @@ -80,6 +80,7 @@ Sub-components :class:`NavigationEvents ` :class:`PointEvents ` :class:`SeriesEvents ` + :class:`SimulationEvents ` :class:`ClusterEvents ` :class:`AxisEvents ` :class:`MouseEvents ` diff --git a/highcharts_core/options/plot_options/networkgraph.py b/highcharts_core/options/plot_options/networkgraph.py index 0805bce9..78bc7ca5 100644 --- a/highcharts_core/options/plot_options/networkgraph.py +++ b/highcharts_core/options/plot_options/networkgraph.py @@ -11,6 +11,7 @@ from highcharts_core.utility_classes.zones import Zone from highcharts_core.utility_classes.shadows import ShadowOptions from highcharts_core.utility_classes.javascript_functions import CallbackFunction +from highcharts_core.utility_classes.events import SimulationEvents class LayoutAlgorithm(HighchartsMeta): @@ -481,6 +482,25 @@ def draggable(self, value): else: self._draggable = bool(value) + @property + def events(self) -> Optional[SimulationEvents]: + """Event handlers for a network graph series. + + .. note:: + + These event hooks can also be attached to the series at run time using the + (JavaScript) ``Highcharts.addEvent()`` function. + + :rtype: :class:`SimulationEvents ` or + :obj:`None ` + """ + return self._events + + @events.setter + @class_sensitive(SimulationEvents) + def events(self, value): + self._events = value + @property def find_nearest_point_by(self) -> Optional[str]: """Determines whether the series should look for the nearest point in both diff --git a/highcharts_core/utility_classes/events.py b/highcharts_core/utility_classes/events.py index 2622196f..60755d90 100644 --- a/highcharts_core/utility_classes/events.py +++ b/highcharts_core/utility_classes/events.py @@ -877,6 +877,57 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: return untrimmed +class SimulationEvents(SeriesEvents): + """Event listeners for series that involve simulation / layout. + + .. versionadded:: Highcharts Core for Python v.1.1.0 / Highcharts Core (JS) v.11.0.0 + + """ + + def __init__(self, **kwargs): + self._after_simulation = None + + self.after_simulation = kwargs.get('after_simulation', None) + + super().__init__(**kwargs) + + @property + def after_simulation(self) -> Optional[CallbackFunction]: + """Event which fires after the simulation is ended and the layout is stable. + + :rtype: :class:`CallbackFunction ` or + :obj:`None ` + """ + return self._after_simulation + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'after_animate': as_dict.get('afterAnimate', None), + 'checkbox_click': as_dict.get('checkboxClick', None), + 'click': as_dict.get('click', None), + 'hide': as_dict.get('hide', None), + 'legend_item_click': as_dict.get('legendItemClick', None), + 'mouse_out': as_dict.get('mouseOut', None), + 'mouse_over': as_dict.get('mouseOver', None), + 'show': as_dict.get('show', None), + + 'after_simulation': as_dict.get('afterSimulation', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'afterSimulation': self.after_simulation, + } + parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) or {} + for key in parent_as_dict: + untrimmed[key] = parent_as_dict[key] + + return untrimmed + + class ClusterEvents(HighchartsMeta): """General event handlers for marker clusters.""" From 8000b7e6ee4037c0fb12982365c0ebdbb7a59df5 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Apr 2023 11:22:53 -0400 Subject: [PATCH 16/24] Added sonification-related properties to global_options.language.Language. --- .../global_options/language/__init__.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/highcharts_core/global_options/language/__init__.py b/highcharts_core/global_options/language/__init__.py index 474cbf95..f598670b 100644 --- a/highcharts_core/global_options/language/__init__.py +++ b/highcharts_core/global_options/language/__init__.py @@ -28,6 +28,7 @@ def __init__(self, **kwargs): self._decimal_point = None self._download_csv = None self._download_jpeg = None + self._download_midi = None self._download_pdf = None self._download_png = None self._download_svg = None @@ -44,6 +45,7 @@ def __init__(self, **kwargs): self._no_data = None self._numeric_symbol_magnitude = None self._numeric_symbols = None + self._play_as_sound = None self._print_chart = None self._reset_zoom = None self._reset_zoom_title = None @@ -59,6 +61,7 @@ def __init__(self, **kwargs): self.decimal_point = kwargs.get('decimal_point', None) self.download_csv = kwargs.get('download_csv', None) self.download_jpeg = kwargs.get('download_jpeg', None) + self.download_midi = kwargs.get('download_midi', None) self.download_pdf = kwargs.get('download_pdf', None) self.download_png = kwargs.get('download_png', None) self.download_svg = kwargs.get('download_svg', None) @@ -75,6 +78,7 @@ def __init__(self, **kwargs): self.no_data = kwargs.get('no_data', None) self.numeric_symbol_magnitude = kwargs.get('numeric_symbol_magnitude', None) self.numeric_symbols = kwargs.get('numeric_symbols', None) + self.play_as_sound = kwargs.get('play_as_sound', None) self.print_chart = kwargs.get('print_chart', None) self.reset_zoom = kwargs.get('reset_zoom', None) self.reset_zoom_title = kwargs.get('reset_zoom_title', None) @@ -168,6 +172,22 @@ def download_jpeg(self) -> Optional[str]: def download_jpeg(self, value): self._download_jpeg = validators.string(value, allow_empty = True) + @property + def download_midi(self) -> Optional[str]: + """ + .. versionadded:: Highcharts Core for Python v.1.1.0 / Highcharts Core (JS) v.11.0.0 + + Text for the context menu item that allows the user to download a MIDI of the + chart/data. Defaults to ``'Download MIDI'``. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._download_midi + + @download_midi.setter + def download_midi(self, value): + self._download_midi = validators.string(value, allow_empty = True) + @property def download_pdf(self) -> Optional[str]: """Text for the context menu item that allows the user to download a PDF of the @@ -455,6 +475,22 @@ def numeric_symbols(self, value): self._numeric_symbols = validated + @property + def play_as_sound(self) -> Optional[str]: + """ + .. versionadded:: Highcharts Core for Python v.1.1.0 / Highcharts Core (JS) v.11.0.0 + + Text for the context menu item that allows the user to play the chart/data as a sound. + Defaults to ``'Play as sound'``. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._play_as_sound + + @play_as_sound.setter + def play_as_sound(self, value): + self._play_as_sound = validators.string(value, allow_empty = True) + @property def print_chart(self) -> Optional[str]: """The text for the menu item to print the chart. Defaults to @@ -649,6 +685,7 @@ def _get_kwargs_from_dict(cls, as_dict): 'decimal_point': as_dict.get('decimalPoint', None), 'download_csv': as_dict.get('downloadCSV', None), 'download_jpeg': as_dict.get('downloadJPEG', None), + 'download_midi': as_dict.get('downloadMIDI', None), 'download_pdf': as_dict.get('downloadPDF', None), 'download_png': as_dict.get('downloadPNG', None), 'download_svg': as_dict.get('downloadSVG', None), @@ -665,6 +702,7 @@ def _get_kwargs_from_dict(cls, as_dict): 'no_data': as_dict.get('noData', None), 'numeric_symbol_magnitude': as_dict.get('numericSymbolMagnitude', None), 'numeric_symbols': as_dict.get('numericSymbols', None), + 'play_as_sound': as_dict.get('playAsSound', None), 'print_chart': as_dict.get('printChart', None), 'reset_zoom': as_dict.get('resetZoom', None), 'reset_zoom_title': as_dict.get('resetZoomTitle', None), @@ -685,6 +723,7 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: 'decimalPoint': self.decimal_point, 'downloadCSV': self.download_csv, 'downloadJPEG': self.download_jpeg, + 'downloadMIDI': self.download_midi, 'downloadPDF': self.download_pdf, 'downloadPNG': self.download_png, 'downloadSVG': self.download_svg, @@ -701,6 +740,7 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: 'noData': self.no_data, 'numericSymbolMagnitude': self.numeric_symbol_magnitude, 'numericSymbols': self.numeric_symbols, + 'playAsSound': self.play_as_sound, 'printChart': self.print_chart, 'resetZoom': self.reset_zoom, 'resetZoomTitle': self.reset_zoom_title, From b52148e6d1a31fff4c8397be032a20ea310cca85 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Apr 2023 11:23:15 -0400 Subject: [PATCH 17/24] Added border radius to Pie* and related. --- highcharts_core/options/plot_options/pie.py | 35 +++++++++++++++++++++ highcharts_core/options/series/pie.py | 2 ++ 2 files changed, 37 insertions(+) diff --git a/highcharts_core/options/plot_options/pie.py b/highcharts_core/options/plot_options/pie.py index d1ba4c61..20f89b25 100644 --- a/highcharts_core/options/plot_options/pie.py +++ b/highcharts_core/options/plot_options/pie.py @@ -33,6 +33,7 @@ class PieOptions(GenericTypeOptions): def __init__(self, **kwargs): self._border_color = None + self._border_radius = None self._border_width = None self._center = None self._color_axis = None @@ -52,6 +53,7 @@ def __init__(self, **kwargs): self._thickness = None self.border_color = kwargs.get('border_color', None) + self.border_radius = kwargs.get('border_radius', None) self.border_width = kwargs.get('border_width', None) self.center = kwargs.get('center', None) self.color_axis = kwargs.get('color_axis', None) @@ -88,6 +90,36 @@ def border_color(self, value): from highcharts_core import utility_functions self._border_color = utility_functions.validate_color(value) + @property + def border_radius(self) -> Optional[str | int | float | Decimal]: + """ + .. versionadded:: Highcharts Core for Python v.1.1.0 / Highcharts Core (JS) v.11.0.0 + + The corner radius of the border surrounding each slice. Defaults to ``3``. + + .. note:: + + A numerical value signifies the value is expressed in pixels. A percentage string like `50%` + signifies a size relative to the radius and the inner radius. + + :rtype: numeric, :class:`str ` or :obj:`None ` + """ + return self._border_radius + + @border_radius.setter + def border_radius(self, value): + if value is None: + self._border_radius = None + else: + try: + value = validators.string(value) + if '%' not in value: + raise ValueError + except (TypeError, ValueError): + value = validators.numeric(value, minimum = 0) + + self._border_radius = value + @property def border_width(self) -> Optional[int | float | Decimal]: """The width of the border surrounding each slice. Defaults to ``1``. @@ -497,6 +529,7 @@ def _get_kwargs_from_dict(cls, as_dict): 'visible': as_dict.get('visible', None), 'border_color': as_dict.get('borderColor', None), + 'border_radius': as_dict.get('borderRadius', None), 'border_width': as_dict.get('borderWidth', None), 'center': as_dict.get('center', None), 'color_axis': as_dict.get('colorAxis', None), @@ -521,6 +554,7 @@ def _get_kwargs_from_dict(cls, as_dict): def _to_untrimmed_dict(self, in_cls = None) -> dict: untrimmed = { 'borderColor': self.border_color, + 'borderRadius': self.border_radius, 'borderWidth': self.border_width, 'center': self.center, 'colorAxis': self.color_axis, @@ -723,6 +757,7 @@ def _get_kwargs_from_dict(cls, as_dict): 'visible': as_dict.get('visible', None), 'border_color': as_dict.get('borderColor', None), + 'border_radius': as_dict.get('borderRadius', None), 'border_width': as_dict.get('borderWidth', None), 'center': as_dict.get('center', None), 'color_axis': as_dict.get('colorAxis', None), diff --git a/highcharts_core/options/series/pie.py b/highcharts_core/options/series/pie.py index 18203974..98f41ccf 100644 --- a/highcharts_core/options/series/pie.py +++ b/highcharts_core/options/series/pie.py @@ -143,6 +143,7 @@ def _get_kwargs_from_dict(cls, as_dict): 'z_index': as_dict.get('zIndex', None), 'border_color': as_dict.get('borderColor', None), + 'border_radius': as_dict.get('borderRadius', None), 'border_width': as_dict.get('borderWidth', None), 'center': as_dict.get('center', None), 'colors': as_dict.get('colors', None), @@ -282,6 +283,7 @@ def _get_kwargs_from_dict(cls, as_dict): 'z_index': as_dict.get('zIndex', None), 'border_color': as_dict.get('borderColor', None), + 'border_radius': as_dict.get('borderRadius', None), 'border_width': as_dict.get('borderWidth', None), 'center': as_dict.get('center', None), 'color_axis': as_dict.get('colorAxis', None), From f0db7c847bd23dd111277e38962574a0111a27bc Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Apr 2023 11:23:33 -0400 Subject: [PATCH 18/24] Added additional properties to utility_classes.buttons.CollapseButtonConfiguration. --- highcharts_core/utility_classes/buttons.py | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/highcharts_core/utility_classes/buttons.py b/highcharts_core/utility_classes/buttons.py index 2455ff7f..3a1f6080 100644 --- a/highcharts_core/utility_classes/buttons.py +++ b/highcharts_core/utility_classes/buttons.py @@ -175,16 +175,20 @@ class CollapseButtonConfiguration(HighchartsMeta): def __init__(self, **kwargs): self._enabled = None self._height = None + self._line_width = None self._only_on_hover = None self._shape = None + self._style = None self._width = None self._x = None self._y = None self.enabled = kwargs.get('enabled', None) self.height = kwargs.get('height', None) + self.line_width = kwargs.get('line_width', None) self.only_on_hover = kwargs.get('only_on_hover', None) self.shape = kwargs.get('shape', None) + self.style = kwargs.get('style', None) self.width = kwargs.get('width', None) self.x = kwargs.get('x', None) self.y = kwargs.get('y', None) @@ -224,6 +228,23 @@ def height(self, value): allow_empty = False, minimum = 0) + @property + def line_width(self) -> Optional[int | float | Decimal]: + """The line_width of the button, expressed in pixels. Defaults to ``1``. + + :rtype: numeric or :obj:`None ` + """ + return self._line_width + + @line_width.setter + def line_width(self, value): + if value is None: + self._line_width = None + else: + self._line_width = validators.numeric(value, + allow_empty = False, + minimum = 0) + @property def only_on_hover(self) -> Optional[bool]: """Whether the button should be visible only when the node is hovered. Defaults to ``True``. @@ -254,6 +275,22 @@ def shape(self) -> Optional[str]: @shape.setter def shape(self, value): self._shape = validators.string(value, allow_empty = True) + + @property + def style(self) -> Optional[dict]: + """CSS styles for the collapse button. + + .. note:: + + In styled mode, the collapse button style is given in the ``.highcharts-collapse-button`` CSS class. + + :rtype: :class:`dict ` or :obj:`None ` + """ + return self._style + + @style.setter + def style(self, value): + self._value = validators.dict(value, allow_empty = True) @property def width(self) -> Optional[int | float | Decimal]: @@ -298,8 +335,10 @@ def _get_kwargs_from_dict(cls, as_dict): kwargs = { 'enabled': as_dict.get('enabled', None), 'height': as_dict.get('height', None), + 'line_width': as_dict.get('lineWidth', None), 'only_on_hover': as_dict.get('onlyOnHover', None), 'shape': as_dict.get('shape', None), + 'style': as_dict.get('style', None), 'width': as_dict.get('width', None), 'x': as_dict.get('x', None), 'y': as_dict.get('y', None), @@ -311,8 +350,10 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: untrimmed = { 'enabled': self.enabled, 'height': self.height, + 'lineWidth': self.line_width, 'onlyOnHover': self.only_on_hover, 'shape': self.shape, + 'style': self.style, 'width': self.width, 'x': self.x, 'y': self.y From b09a251022f814367dfefc61d3a9b3cbd57de998 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Apr 2023 11:23:43 -0400 Subject: [PATCH 19/24] Fixed typos in documentation. --- highcharts_core/options/plot_options/sunburst.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/highcharts_core/options/plot_options/sunburst.py b/highcharts_core/options/plot_options/sunburst.py index 8845dd43..024358bd 100644 --- a/highcharts_core/options/plot_options/sunburst.py +++ b/highcharts_core/options/plot_options/sunburst.py @@ -86,7 +86,7 @@ def allow_traversing_tree(self, value): def border_color(self) -> Optional[str | Gradient | Pattern]: """The color of the border surrounding each slice. When :obj:`None `, the border takes the same color as the slice fill. This can be used together with - a :meth:`border_width ` to fill drawing gaps created by + a :meth:`border_width ` to fill drawing gaps created by antialiazing artefacts in borderless pies. Defaults to ``'#ffffff'``. :rtype: :class:`str ` or :obj:`None ` @@ -104,7 +104,7 @@ def border_width(self) -> Optional[int | float | Decimal]: When setting the border width to ``0``, there may be small gaps between the slices due to SVG antialiasing artefacts. To work around this, keep the border width at - ``0.5`` or ``1``, but set the :meth:`border_color ` to + ``0.5`` or ``1``, but set the :meth:`border_color ` to :obj:`None ` instead. :rtype: numeric or :obj:`None ` @@ -268,7 +268,7 @@ def data_labels(self, value): def fill_color(self) -> Optional[str | Gradient | Pattern]: """If the total sum of the pie's values is ``0``, the series is represented as an empty circle . The ``fill_color`` setting defines the color of that circle. - Use :meth:`PieOptions.border_width` to set the border thickness. + Use :meth:`SunburstOptions.border_width` to set the border thickness. Defaults to :obj:`None `. @@ -373,7 +373,7 @@ def size(self) -> Optional[str | int]: .. note:: - :meth:`PieOptions.sliced_offset` is also included in the default size + :meth:`SunburstOptions.sliced_offset` is also included in the default size calculation. As a consequence, the size of the pie may vary when points are updated and data labels more around. In that case it is best to set a fixed value, for example ``"75%"``. From a0de4792819d812ced9c027eac4cb925024089b4 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Apr 2023 11:23:53 -0400 Subject: [PATCH 20/24] Added numerous changelog entries. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8bd718d3..4075a8ba 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,11 @@ Release 2.0.0 * Added ``PictorialOptions`` / ``PictorialSeries`` series type with related classes. * Added ``.minor_ticks_per_major`` to ``options.axes.x_axis.XAxisOptions``. * Added ``.stack_shadow`` to ``options.axes.y_axis.YAxisOptions``. + * Added ``.border_radius`` to ``ColumnRangeOptions`` / ``ColumnRangeSeries``. + * Added ``.play_as_sand`` and ``.download_midi`` to ``global_options.language.Language``. + * Added ``.border_radius`` to ``PieOptions`` / ``PieSeries``. + * Added ``.style`` to ``utility_classes.buttons.CollapseButtonConfiguration``. + * Added ``utility_classes.events.SimulationEvents`` and modified ``NetworkGraphOptions`` to support. * **FIXED:** Broken heatmap and tilemap documentation links. * **FIXED:** Fixed missing ``TreegraphOptions`` / ``TreegraphSeries`` series type. From 913eb79afde72e71c4d07f69842c85c7d9bd7839 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Apr 2023 19:42:30 -0400 Subject: [PATCH 21/24] Added global Sonification options. --- CHANGES.rst | 3 +- docs/api.rst | 16 + docs/api/options/index.rst | 16 + docs/api/options/sonification/grouping.rst | 28 + docs/api/options/sonification/index.rst | 54 + docs/api/options/sonification/mapping.rst | 101 ++ .../sonification/track_configurations.rst | 100 ++ docs/glossary.rst | 9 + highcharts_core/constants.py | 20 + highcharts_core/options/__init__.py | 20 + .../options/sonification/__init__.py | 332 ++++++ .../options/sonification/grouping.py | 125 ++ .../options/sonification/mapping.py | 1031 +++++++++++++++++ .../sonification/track_configurations.py | 640 ++++++++++ highcharts_core/utility_classes/events.py | 208 ++++ .../sonification/sonification/01.js | 458 ++++++++ .../sonification/sonification/error-01.js | 447 +++++++ tests/options/test_sonification.py | 54 + 18 files changed, 3661 insertions(+), 1 deletion(-) create mode 100644 docs/api/options/sonification/grouping.rst create mode 100644 docs/api/options/sonification/index.rst create mode 100644 docs/api/options/sonification/mapping.rst create mode 100644 docs/api/options/sonification/track_configurations.rst create mode 100644 highcharts_core/options/sonification/__init__.py create mode 100644 highcharts_core/options/sonification/grouping.py create mode 100644 highcharts_core/options/sonification/mapping.py create mode 100644 highcharts_core/options/sonification/track_configurations.py create mode 100644 tests/input_files/sonification/sonification/01.js create mode 100644 tests/input_files/sonification/sonification/error-01.js create mode 100644 tests/options/test_sonification.py diff --git a/CHANGES.rst b/CHANGES.rst index 4075a8ba..b0747ade 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -Release 2.0.0 +Release 1.1.0 ========================================= * Align the API to **Highcharts (JS) v.11**. In particular, this includes: @@ -26,6 +26,7 @@ Release 2.0.0 * Added ``.border_radius`` to ``PieOptions`` / ``PieSeries``. * Added ``.style`` to ``utility_classes.buttons.CollapseButtonConfiguration``. * Added ``utility_classes.events.SimulationEvents`` and modified ``NetworkGraphOptions`` to support. + * Added ``options.sonification`` and all related classes. * **FIXED:** Broken heatmap and tilemap documentation links. * **FIXED:** Fixed missing ``TreegraphOptions`` / ``TreegraphSeries`` series type. diff --git a/docs/api.rst b/docs/api.rst index 96a9c57a..428555fb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -554,6 +554,22 @@ Core Components - :class:`VennSeries ` * - :mod:`.options.series.wordcloud ` - :class:`WordcloudSeries ` + * - :mod:`.options.sonification ` + - :class:`SonificationOptions ` + * - :mod:`.options.sonification.grouping ` + - :class:`PointGrouping ` + * - :mod:`.options.sonification.mapping ` + - :class:`SonificationMapping ` + :class:`AudioParameter ` + :class:`AudioFilter ` + :class:`PitchParameter ` + :class:`TremoloEffect ` + * - :mod:`.options.sonification.track_configurations ` + - :class:`InstrumentTrackConfiguration ` + :class:`SpeechTrackConfiguration ` + :class:`ContextTrackConfiguration ` + :class:`TrackConfigurationBase ` + :class:`ActiveWhen ` * - :mod:`.options.subtitle ` - :class:`Subtitle ` * - :mod:`.options.time ` diff --git a/docs/api/options/index.rst b/docs/api/options/index.rst index 56ab9a8d..04fdb063 100644 --- a/docs/api/options/index.rst +++ b/docs/api/options/index.rst @@ -481,6 +481,22 @@ Sub-components - :class:`VennSeries ` * - :mod:`.options.series.wordcloud ` - :class:`WordcloudSeries ` + * - :mod:`.options.sonification ` + - :class:`SonificationOptions ` + * - :mod:`.options.sonification.grouping ` + - :class:`PointGrouping ` + * - :mod:`.options.sonification.mapping ` + - :class:`SonificationMapping ` + :class:`AudioParameter ` + :class:`AudioFilter ` + :class:`PitchParameter ` + :class:`TremoloEffect ` + * - :mod:`.options.sonification.track_configurations ` + - :class:`InstrumentTrackConfiguration ` + :class:`SpeechTrackConfiguration ` + :class:`ContextTrackConfiguration ` + :class:`TrackConfigurationBase ` + :class:`ActiveWhen ` * - :mod:`.options.subtitle ` - :class:`Subtitle ` * - :mod:`.options.time ` diff --git a/docs/api/options/sonification/grouping.rst b/docs/api/options/sonification/grouping.rst new file mode 100644 index 00000000..f7f546ab --- /dev/null +++ b/docs/api/options/sonification/grouping.rst @@ -0,0 +1,28 @@ +########################################################################################## +:mod:`.grouping ` +########################################################################################## + +.. contents:: Module Contents + :local: + :depth: 3 + :backlinks: entry + +-------------- + +.. module:: highcharts_core.options.sonification.grouping + +******************************************************************************************************************** +class: :class:`PointGrouping ` +******************************************************************************************************************** + +.. autoclass:: PointGrouping + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: PointGrouping + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | diff --git a/docs/api/options/sonification/index.rst b/docs/api/options/sonification/index.rst new file mode 100644 index 00000000..dc3d92d3 --- /dev/null +++ b/docs/api/options/sonification/index.rst @@ -0,0 +1,54 @@ +############################################################## +:mod:`.options.sonification ` +############################################################## + +.. contents:: Module Contents + :local: + :depth: 3 + :backlinks: entry + +.. toctree:: + :titlesonly: + + grouping + mapping + track_configurations + +-------------- + +.. module:: highcharts_core.options.sonification + +**************************************************************************************** +class: :class:`SonificationOptions ` +**************************************************************************************** + +.. autoclass:: SonificationOptions + :members: + :inherited-members: + +----------------------- + +*************************** +Sub-components +*************************** + +.. list-table:: + :widths: 60 40 + :header-rows: 1 + + * - Module + - Classes / Functions + * - :mod:`.options.sonification.grouping ` + - :class:`PointGrouping ` + * - :mod:`.options.sonification.mapping ` + - :class:`SonificationMapping ` + :class:`AudioParameter ` + :class:`AudioFilter ` + :class:`PitchParameter ` + :class:`TremoloEffect ` + * - :mod:`.options.sonification.track_configurations ` + - :class:`InstrumentTrackConfiguration ` + :class:`SpeechTrackConfiguration ` + :class:`ContextTrackConfiguration ` + :class:`TrackConfigurationBase ` + :class:`ActiveWhen ` diff --git a/docs/api/options/sonification/mapping.rst b/docs/api/options/sonification/mapping.rst new file mode 100644 index 00000000..1d345c17 --- /dev/null +++ b/docs/api/options/sonification/mapping.rst @@ -0,0 +1,101 @@ +########################################################################################## +:mod:`.mapping ` +########################################################################################## + +.. contents:: Module Contents + :local: + :depth: 3 + :backlinks: entry + +-------------- + +.. module:: highcharts_core.options.sonification.mapping + +******************************************************************************************************************** +class: :class:`AudioParameter ` +******************************************************************************************************************** + +.. autoclass:: AudioParameter + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: AudioParameter + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +----------------------- + +******************************************************************************************************************** +class: :class:`AudioParameter ` +******************************************************************************************************************** + +.. autoclass:: AudioParameter + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: AudioParameter + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +----------------------- + +******************************************************************************************************************** +class: :class:`AudioFilter ` +******************************************************************************************************************** + +.. autoclass:: AudioFilter + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: AudioFilter + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +----------------------- + +******************************************************************************************************************** +class: :class:`PitchParameter ` +******************************************************************************************************************** + +.. autoclass:: PitchParameter + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: PitchParameter + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +----------------------- + +******************************************************************************************************************** +class: :class:`TremoloEffect ` +******************************************************************************************************************** + +.. autoclass:: TremoloEffect + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: TremoloEffect + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + diff --git a/docs/api/options/sonification/track_configurations.rst b/docs/api/options/sonification/track_configurations.rst new file mode 100644 index 00000000..177311ac --- /dev/null +++ b/docs/api/options/sonification/track_configurations.rst @@ -0,0 +1,100 @@ +########################################################################################## +:mod:`.track_configurations ` +########################################################################################## + +.. contents:: Module Contents + :local: + :depth: 3 + :backlinks: entry + +-------------- + +.. module:: highcharts_core.options.sonification.track_configurations + +*************************************************************************************************************************************************************** +class: :class:`InstrumentTrackConfiguration ` +*************************************************************************************************************************************************************** + +.. autoclass:: InstrumentTrackConfiguration + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: InstrumentTrackConfiguration + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +--------------------- + +*************************************************************************************************************************************************************** +class: :class:`SpeechTrackConfiguration ` +*************************************************************************************************************************************************************** + +.. autoclass:: SpeechTrackConfiguration + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: SpeechTrackConfiguration + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +--------------------- + +*************************************************************************************************************************************************************** +class: :class:`ContextTrackConfiguration ` +*************************************************************************************************************************************************************** + +.. autoclass:: ContextTrackConfiguration + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: ContextTrackConfiguration + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +--------------------- + +*************************************************************************************************************************************************************** +class: :class:`ActiveWhen ` +*************************************************************************************************************************************************************** + +.. autoclass:: ActiveWhen + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: ActiveWhen + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | + +--------------------- + +*************************************************************************************************************************************************************** +class: :class:`TrackConfigurationBase ` +*************************************************************************************************************************************************************** + +.. autoclass:: TrackConfigurationBase + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: TrackConfigurationBase + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | diff --git a/docs/glossary.rst b/docs/glossary.rst index 3f196df2..247ef0b0 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -210,6 +210,9 @@ Glossary * :class:`GaugeSeries ` * :class:`SolidGaugeSeries ` + Highpass + A highpass audio filter lets high frequencies through, but stops low frequencies, making the sound thinner. + JavaScript Object Literal Notation A way of representing data in JavaScript as native JavaScript objects which is necessary to maximize value from `Highcharts JS `__. @@ -267,6 +270,9 @@ Glossary Typically, JSON can be converted to JavaScript object literal notation easily...but the opposite does not hold true. + Lowpass + A lowpass audio filter lets low frequencies through, but stops high frequencies, making the sound more dull. + Metaclass A Python class that is used to define properties and methods - including abstract properties or methods which are not implemented in the metaclass itself - which are @@ -499,6 +505,9 @@ Glossary * :doc:`Supported Visualizations ` > :ref:`Technical Indicators ` + Tremolo + An audio effect with repeated changes in volume over time. + Untrimmed .. note:: diff --git a/highcharts_core/constants.py b/highcharts_core/constants.py index 86f95ace..eaa92379 100644 --- a/highcharts_core/constants.py +++ b/highcharts_core/constants.py @@ -897,3 +897,23 @@ def __eq__(self, other): 'friday': 5, 'saturday': 6 } + +## SONIFICATION INSTRUMENTS +INSTRUMENT_PRESETS = [ + 'piano', + 'flute', + 'saxophone', + 'trumpet', + 'sawsynth', + 'wobble', + 'basic1', + 'basic2', + 'sine', + 'sineGlide', + 'triangle', + 'square', + 'sawtooth', + 'noise', + 'filteredNoise', + 'wind', +] \ No newline at end of file diff --git a/highcharts_core/options/__init__.py b/highcharts_core/options/__init__.py index 208a1307..e3fae433 100644 --- a/highcharts_core/options/__init__.py +++ b/highcharts_core/options/__init__.py @@ -30,6 +30,7 @@ from highcharts_core.options.plot_options import PlotOptions from highcharts_core.options.plot_options.generic import GenericTypeOptions from highcharts_core.options.responsive import Responsive +from highcharts_core.options.sonification import SonificationOptions from highcharts_core.options.subtitle import Subtitle from highcharts_core.options.time import Time from highcharts_core.options.title import Title @@ -61,6 +62,7 @@ def __init__(self, **kwargs): self._plot_options = None self._responsive = None self._series = None + self._sonification = None self._subtitle = None self._time = None self._title = None @@ -84,6 +86,7 @@ def __init__(self, **kwargs): self.navigation = kwargs.get('navigation', None) self.plot_options = kwargs.get('plot_options', None) self.responsive = kwargs.get('responsive', None) + self.sonification = kwargs.get('sonification', None) self.subtitle = kwargs.get('subtitle', None) self.time = kwargs.get('time', None) self.title = kwargs.get('title', None) @@ -92,6 +95,7 @@ def __init__(self, **kwargs): self.y_axis = kwargs.get('y_axis', None) self.series = kwargs.get('series', None) + @property def accessibility(self) -> Optional[Accessibility]: """Options for configuring accessibility for the chart. @@ -482,6 +486,20 @@ def series(self, value): default_type = default_series_type) for x in value] + @property + def sonification(self) -> Optional[SonificationOptions]: + """Configuration of global sonification settings for the entire chart. + + :rtype: :class:`SonificationOptions ` or + :obj:`None ` + """ + return self._sonification + + @sonification.setter + @class_sensitive(SonificationOptions) + def sonification(self, value): + self._sonification = value + @property def subtitle(self) -> Optional[Subtitle]: """The chart's subtitle. @@ -798,6 +816,7 @@ def _get_kwargs_from_dict(cls, as_dict): 'plot_options': as_dict.get('plotOptions', None), 'responsive': as_dict.get('responsive', None), 'series': as_dict.get('series', None), + 'sonification': as_dict.get('sonification', None), 'subtitle': as_dict.get('subtitle', None), 'time': as_dict.get('time', None), 'title': as_dict.get('title', None), @@ -832,6 +851,7 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: 'plotOptions': self.plot_options, 'responsive': self.responsive, 'series': self.series, + 'sonification': self.sonification, 'subtitle': self.subtitle, 'time': self.time, 'title': self.title, diff --git a/highcharts_core/options/sonification/__init__.py b/highcharts_core/options/sonification/__init__.py new file mode 100644 index 00000000..64b29f2e --- /dev/null +++ b/highcharts_core/options/sonification/__init__.py @@ -0,0 +1,332 @@ +from typing import Optional +from decimal import Decimal + +from validator_collection import validators + +from highcharts_core import errors +from highcharts_core.metaclasses import HighchartsMeta +from highcharts_core.decorators import class_sensitive +from highcharts_core.options.sonification.track_configurations import (InstrumentTrackConfiguration, + SpeechTrackConfiguration, + ContextTrackConfiguration) +from highcharts_core.options.sonification.grouping import SonificationGrouping +from highcharts_core.utility_classes.events import SonificationEvents + + +class SonificationOptions(HighchartsMeta): + """Options for configuring sonification and audio charts.""" + + def __init__(self, **kwargs): + self._after_series_wait = None + self._default_instrument_options = None + self._default_speech_options = None + self._duration = None + self._enabled = None + self._events = None + self._global_context_tracks = None + self._global_tracks = None + self._master_volume = None + self._order = None + self._point_grouping = None + self._show_crosshair = None + self._show_tooltip = None + self._update_interval = None + + self.after_series_wait = kwargs.get('after_series_wait', None) + self.default_instrument_options = kwargs.get('default_instrument_options', None) + self.default_speech_options = kwargs.get('default_speech_options', None) + self.duration = kwargs.get('duration', None) + self.enabled = kwargs.get('enabled', None) + self.events = kwargs.get('events', None) + self.global_context_tracks = kwargs.get('global_context_tracks', None) + self.global_tracks = kwargs.get('global_tracks', None) + self.master_volume = kwargs.get('master_volume', None) + self.order = kwargs.get('order', None) + self.point_grouping = kwargs.get('point_grouping', None) + self.show_crosshair = kwargs.get('show_crosshair', None) + self.show_tooltip = kwargs.get('show_tooltip', None) + self.update_interval = kwargs.get('update_interval', None) + + @property + def after_series_wait(self) -> Optional[int | float | Decimal]: + """The time to wait in milliseconds after each data series when playing the visualization's data series + in sequence. Defaults to ``700``. + + :rtype: numeric or :obj:`None ` + """ + return self._after_series_wait + + @after_series_wait.setter + def after_series_wait(self, value): + self._after_series_wait = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def default_instrument_options(self) -> Optional[InstrumentTrackConfiguration]: + """Default sonification options for all instrument tracks. + + .. warning:: + + If specific options are also set on individual tracks or per-series, this configuration will be *overridden*. + + :rtype: :class:`InstrumentTrackConfiguration ` + or :obj:`None ` + """ + return self._default_instrument_options + + @default_instrument_options.setter + @class_sensitive(InstrumentTrackConfiguration) + def default_instrument_options(self, value): + self._default_instrument_options = value + + @property + def default_speech_options(self) -> Optional[SpeechTrackConfiguration]: + """Default sonification options for all speech tracks. + .. warning:: + + If specific options are also set on individual tracks or per-series, this configuration will be *overridden*. + + :rtype: :class:`SpeechTrackConfiguration ` + or :obj:`None ` + """ + return self._default_speech_options + + @default_speech_options.setter + @class_sensitive(SpeechTrackConfiguration) + def default_speech_options(self, value): + self._default_speech_options = value + + @property + def duration(self) -> Optional[int | float | Decimal]: + """The total duration of the sonification, expressed in milliseconds. Defaults to ``6000``. + + :rtype: numeric or :obj:`None ` + """ + return self._duration + + @duration.setter + def duration(self, value): + self._duration = validators.numeric(value, allow_empty = True, minimum = 0) + + @property + def enabled(self) -> Optional[bool]: + """If ``True``, enables sonification functionality on the chart. Defaults to ``True``. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._enabled + + @enabled.setter + def enabled(self, value): + if value is None: + self._enabled = None + else: + self._enabled = bool(value) + + @property + def events(self) -> Optional[SonificationEvents]: + """Event handlers for sonification. + + :rtype: :class:`SonificationEvents ` or + :obj:`None ` + """ + return self._events + + @events.setter + @class_sensitive(SonificationEvents) + def events(self, value): + self._events = value + + @property + def global_context_tracks(self) -> Optional[ContextTrackConfiguration]: + """Context tracks to add globally, an array of either instrument tracks, speech tracks, or a mix. + + .. note:: + + Context tracks are not tied to data points, but play at a set interval - either based on ``time`` or on + ``prop`` values. + + :rtype: :class:`ContextTrackConfiguration ` + or :obj:`None ` + """ + return self._global_context_tracks + + @global_context_tracks.setter + @class_sensitive(ContextTrackConfiguration) + def global_context_tracks(self, value): + self._global_context_tracks = value + + @property + def global_tracks(self) -> Optional[InstrumentTrackConfiguration]: + """Global tracks to add to every series. + + :rtype: :class:`InstrumentTrackConfiguration ` + or :obj:`None ` + """ + return self._global_tracks + + @global_tracks.setter + @class_sensitive(InstrumentTrackConfiguration) + def global_tracks(self, value): + self._global_tracks = value + + @property + def master_volume(self) -> Optional[int | float | Decimal]: + """The overall/master volume for the sonification, from ``0`` to ``1``. Defaults to ``0.7``. + + :rtype: numeric or :obj:`None ` + """ + return self._master_volume + + @master_volume.setter + def master_volume(self, value): + self._master_volume = validators.numeric(value, + allow_empty = True, + minimum = 0, + maximum = 1) + + @property + def order(self) -> Optional[str]: + """The order in which to play the sonification for data series. Accepts either: + + * ``'sequential'`` where the series play individually one after the other or + * ``'simultaneous'`` where the series play at the same time + + Defaults to ``'sequential'``. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._order + + @order.setter + def order(self, value): + if not value: + self._order = None + else: + value = validators.string(value) + value = value.lower() + if value not in ['sequential', 'simultaneous']: + raise errors.HighchartsValueError(f'.order expects either "sequential" or "simultaenous". ' + f'Received: "{value}"') + + self._order = value + + @property + def point_grouping(self) -> Optional[SonificationGrouping]: + """Options for grouping data points together when sonifying. + + This allows for the visual presentation to contain more points than what is being played. + + If not enabled, all visible / uncropped points are played. + + :rtype: :class:`SonificationGrouping ` or + :obj:`None ` + """ + return self._point_grouping + + @point_grouping.setter + @class_sensitive(SonificationGrouping) + def point_grouping(self, value): + self._point_grouping = value + + @property + def show_crosshair(self) -> Optional[bool]: + """If ``True``, show X and Y crosshairs (if defined on the chart) as the sonification plays. Defaults to + ``True``. + + .. warning:: + + If multiple tracks that play at different times try to show crosshairs, it can be glitchy. Therefore, + it is recommended in those cases to turn this on/off for individual tracks using the ``.show_play_marker`` + property. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._show_crosshair + + @show_crosshair.setter + def show_crosshair(self, value): + if value is None: + self._show_crosshair = None + else: + self._show_crosshair = bool(value) + + @property + def show_tooltip(self) -> Optional[bool]: + """If ``True``, show tooltips as the sonification plays. Defaults to ``True``. + + .. warning:: + + If multiple tracks that play at different times try to show tooltips, it can be glitchy. Therefore, + it is recommended in those cases to turn this on/off for individual tracks using the ``.show_play_marker`` + property. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._show_tooltip + + @show_tooltip.setter + def show_tooltip(self, value): + if value is None: + self._show_tooltip = None + else: + self._show_tooltip = bool(value) + + @property + def update_interval(self) -> Optional[int | float | Decimal]: + """The number of milliseconds to wait between each recomputation of the sonification, if the chart updates + rapidly. + + .. tip:: + + This avoids slowing down processes like panning. + + :rtype: numeric or :obj:`None ` + """ + return self._update_interval + + @update_interval.setter + def update_interval(self, value): + self._update_interval = validators.numeric(value, allow_empty = True, minimum = 0) + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'after_series_wait': as_dict.get('afterSeriesWait', None), + 'default_instrument_options': as_dict.get('defaultInstrumentOptions', None), + 'default_speech_options': as_dict.get('defaultSpeechOptions', None), + 'duration': as_dict.get('duration', None), + 'enabled': as_dict.get('enabled', None), + 'events': as_dict.get('events', None), + 'global_context_tracks': as_dict.get('globalContextTracks', None), + 'global_tracks': as_dict.get('globalTracks', None), + 'master_volume': as_dict.get('masterVolume', None), + 'order': as_dict.get('order', None), + 'point_grouping': as_dict.get('pointGrouping', None), + 'show_crosshair': as_dict.get('showCrosshair', None), + 'show_tooltip': as_dict.get('showTooltip', None), + 'update_interval': as_dict.get('updateInterval', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'afterSeriesWait': self.after_series_wait, + 'defaultInstrumentOptions': self.default_instrument_options, + 'defaultSpeechOptions': self.default_speech_options, + 'duration': self.duration, + 'enabled': self.enabled, + 'events': self.events, + 'globalContextTracks': self.global_context_tracks, + 'globalTracks': self.global_tracks, + 'masterVolume': self.master_volume, + 'order': self.order, + 'pointGrouping': self.point_grouping, + 'showCrosshair': self.show_crosshair, + 'showTooltip': self.show_tooltip, + 'updateInterval': self.update_interval, + } + + return untrimmed diff --git a/highcharts_core/options/sonification/grouping.py b/highcharts_core/options/sonification/grouping.py new file mode 100644 index 00000000..b0851ec6 --- /dev/null +++ b/highcharts_core/options/sonification/grouping.py @@ -0,0 +1,125 @@ +from typing import Optional +from decimal import Decimal + +from validator_collection import validators + +from highcharts_core import errors +from highcharts_core.metaclasses import HighchartsMeta + + +class SonificationGrouping(HighchartsMeta): + """Options for grouping data points together when sonifying. + + This allows for the visual presentation to contain more points than what is being played. + + .. tip:: + + If not enabled, all visible / uncropped points are played. + + """ + def __init__(self, **kwargs): + self._algorithm = None + self._enabled = None + self._group_timespan = None + self._prop = None + + self.algorithm = kwargs.get('algorithm', None) + self.enabled = kwargs.get('enabled', None) + self.group_timespan = kwargs.get('group_timespan', None) + self.prop = kwargs.get('prop', None) + + @property + def algorithm(self) -> Optional[str]: + """The grouping algorithm, which determines which points to keep when grouping a set of points together. Accepts + ``'minmax'``, ``'first'``, ``'last'``, ``'middle'``, and ``'firstLast'``. + + By default ``'minmax'`` is used, which keeps both the minimum and maximum points. + + The other algorithms will either keep the first point in the group (time wise), last point, middle point, or both the first and the last point. + + The timing of the resulting point(s) is then adjusted to play evenly, regardless of its original position within the group. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._algorithm + + @algorithm.setter + def algorithm(self, value): + if not value: + self._algorithm = None + else: + value = validators.string(value) + value = value.lower() + if value not in ['minmax', 'first', 'last', 'middle', 'firstlast']: + raise errors.HighchartsValueError(f'.algorithm expects either "minmax", "first", ' + f'"last", "middle", or "firstLast". Received: ' + f'"{value}"') + if value == 'firstlast': + value = 'firstLast' + + self._algorithm = value + + @property + def enabled(self) -> Optional[bool]: + """If ``True``, points should be grouped. Defaults to ``True``. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._enabled + + @enabled.setter + def enabled(self, value): + if value is None: + self._enabled = None + else: + self._enabled = bool(value) + + @property + def group_timespan(self) -> Optional[int | float | Decimal]: + """The size of each group, expressed in milliseconds. Audio events closer than this value are grouped together. + Defaults to ``15``. + + :rtype: numeric or :obj:`None ` + """ + return self._group_timespan + + @group_timespan.setter + def group_timespan(self, value): + self._group_timespan = validators.numeric(value, + allow_empty = True, + minimum = 0) + + @property + def prop(self) -> Optional[str]: + """The data point property to use when evaluating which points to keep in the group. Defaults to ``'y'``, + which means that when the ``'minmax'`` algorithm is applied, the two points with the lowest and highest ``'y'`` + values will be kept and the others not played. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._prop + + @prop.setter + def prop(self, value): + self._prop = validators.string(value, allow_empty = True) + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'algorithm': as_dict.get('algorithm', None), + 'enabled': as_dict.get('enabled', None), + 'group_timespan': as_dict.get('groupTimespan', None), + 'prop': as_dict.get('prop', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'algorithm': self.algorithm, + 'enabled': self.enabled, + 'groupTimespan': self.group_timespan, + 'prop': self.prop, + } + + return untrimmed diff --git a/highcharts_core/options/sonification/mapping.py b/highcharts_core/options/sonification/mapping.py new file mode 100644 index 00000000..9e9bf00d --- /dev/null +++ b/highcharts_core/options/sonification/mapping.py @@ -0,0 +1,1031 @@ +from typing import Optional, List +from decimal import Decimal + +from validator_collection import validators, checkers + +from highcharts_core import errors +from highcharts_core.metaclasses import HighchartsMeta +from highcharts_core.decorators import class_sensitive, validate_types +from highcharts_core.utility_classes.javascript_functions import CallbackFunction + + +class AudioParameter(HighchartsMeta): + """Configuration of an audio parameter's settings.""" + + def __init__(self, **kwargs): + self._map_function = None + self._map_to = None + self._max = None + self._min = None + self._value = None + self._within = None + + self.map_function = kwargs.get('map_function', None) + self.map_to = kwargs.get('map_to', None) + self.max = kwargs.get('max', None) + self.min = kwargs.get('min', None) + self.value = kwargs.get('value', None) + self.within = kwargs.get('within', None) + + @property + def map_function(self) -> Optional[str]: + """The name of the mapping function to apply. Accepts either ``'linear'`` or ``'logarithmic'``. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._map_function + + @map_function.setter + def map_function(self, value): + if not value: + self._map_function = None + else: + value = validators.string(value) + value = value.lower() + if value not in ['linear', 'logarithmic']: + raise errors.HighchartsValueError(f'.map_function expects either "linear" or "logarithmic". ' + f'Received: "{value}"') + self._map_function = value + + @property + def map_to(self) -> Optional[str]: + """The name of the point property to map to determine the audio setting. + + .. tip:: + + A negative sign (``'-'``) can be placed *before* the name of the property to invert + the mapping. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._map_to + + @map_to.setter + def map_to(self, value): + self._map_to = validators.string(value, allow_empty = True) + + @property + def max(self) -> Optional[int | float | Decimal]: + """The maximum value for the audio parameter. This is the highest value the audio parameter will be + mapped to. + + :rtype: numeric or :obj:`None ` + """ + return self._max + + @max.setter + def max(self, value): + self._max = validators.numeric(value, allow_empty = True) + + @property + def min(self) -> Optional[int | float | Decimal]: + """The minimum value for the audio parameter. This is the lowest value the audio parameter will be + mapped to. + + :rtype: numeric or :obj:`None ` + """ + return self._min + + @min.setter + def min(self, value): + self._min = validators.numeric(value, allow_empty = True) + + @property + def value(self) -> Optional[int | float | Decimal]: + """A fixed value to use in place of the mapped ``prop`` when mapping. + + .. note:: + + For example, if mapping to ``'y'``, setting ``.value = 4`` will map as if all + points had a ``y`` value equal of ``4``. + + :rtype: numeric or :obj:`None ` + """ + return self._value + + @value.setter + def value(self, value): + self._value = validators.numeric(value, allow_empty = True) + + @property + def within(self) -> Optional[str]: + """The data values within which to map the audio parameter. Accepts ``'series'``, ``'chart'``, ``'yAxis'``, + ``'xAxis'``. Defaults to :obj:`None `. + + .. note:: + + Mapping within ``'series'`` will make the lowest value point in the *series* map to the + :meth:`.min ` audio parameter value, and the + highest value will map to the :meth:`.max ` + audio parameter. + + Mapping within ``'chart'`` will make the lowest value point in the *chart* (across all series) map to the + :meth:`.min ` audio parameter value, and the + highest value will map to the :meth:`.max ` + audio parameter. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._within + + @within.setter + def within(self, value): + if not value: + self._within = None + else: + value = validators.string(value) + value = value.lower() + if value not in ['series', 'chart', 'xaxis', 'yaxis']: + raise errors.HighchartsValueError(f'.within expects a value of either "series", "chart", ' + f'"xAxis", or "yAxis". Received: "{value}"') + if value == 'xaxis': + value = 'xAxis' + elif value == 'yaxis': + value = 'yAxis' + + self._within = value + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'map_function': as_dict.get('mapFunction', None), + 'map_to': as_dict.get('mapTo', None), + 'max': as_dict.get('max', None), + 'min': as_dict.get('min', None), + 'value': as_dict.get('value', None), + 'within': as_dict.get('within', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'mapFunction': self.map_function, + 'mapTo': self.map_to, + 'max': self.max, + 'min': self.min, + 'value': self.value, + 'within': self.within, + } + + return untrimmed + + +class PitchParameter(AudioParameter): + """The configuration settings to configure :term:`pitch`. + + By default, this object maps to ``'y'``, so low ``'y'`` values are played with a lower pitch + and high ``'y'`` values are played with a higher pitch. + + .. note:: + + The properties for configuring pitch accept :class:`str ` values as well as + numerical values, specifically allowing you to indicate the musical note that you wish the + audio track to play. These notes are expressed in the form ````, such as ``'c6'`` or + ``'F#2'``. + + .. tip:: + + You configure the pitch to map to an array of notes, with the delay between notes determined by the + :meth:`SonificationMapping.gap_between_notes ` + property. + + .. tip:: + + You can also define a musical :meth:`.scale ` + to follow when mapping. + + """ + + def __init__(self, **kwargs): + self._scale = None + + self.scale = kwargs.get('scale', None) + + super().__init__(**kwargs) + + @property + def map_to(self) -> Optional[str]: + """The name of the point property to map to determine the audio setting. Defaults to ``'y'``. + + .. tip:: + + A negative sign (``'-'``) can be placed *before* the name of the property to invert + the mapping. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._map_to + + @map_to.setter + def map_to(self, value): + self._map_to = validators.string(value, allow_empty = True) + + @property + def max(self) -> Optional[str | int | float | Decimal]: + """The maximum value for the audio parameter. This is the highest value the audio parameter will be + mapped to. Defaults to ``'c6'``. + + :rtype: numeric, :class:`str `, or :obj:`None ` + """ + return self._max + + @max.setter + def max(self, value): + if value is None: + self._max = None + else: + try: + self._max = validators.numeric(value) + except (ValueError, TypeError): + self._max = validators.string(value) + + @property + def min(self) -> Optional[str | int | float | Decimal]: + """The minimum value for the audio parameter. This is the lowest value the audio parameter will be + mapped to. Defaults to ``'c2'``. + + :rtype: numeric, :class:`str `, or :obj:`None ` + """ + return self._min + + @min.setter + def min(self, value): + if value is None: + self._min = None + else: + try: + self._min = validators.numeric(value) + except (ValueError, TypeError): + self._min = validators.string(value) + + @property + def scale(self) -> Optional[List[str | int | float | Decimal]]: + """The scale to map pitches against, defined as an array of semitone offsets from the root note. + + :rtype: :class:`list ` of :class:`str ` in the form ``''`` + (e.g. ``'c2'``, ``'F#2'``, etc.) or numeric values, or :obj:`None ` + """ + return self._scale + + @scale.setter + def scale(self, value): + if not value: + self._scale = None + else: + value = validators.iterable(value, forbid_literals = (str, bytes, dict)) + validated = [] + for item in value: + try: + item = validators.numeric(item) + except (ValueError, TypeError): + item = validators.string(item) + + validated.append(item) + + self._scale = [x for x in validated] + + @property + def value(self) -> Optional[int | float | Decimal | str]: + """A fixed value to use in place of the mapped ``prop`` when mapping. Defaults to + :obj:`None `. + + .. note:: + + For example, if mapping to ``'y'``, setting ``.value = 4`` will map as if all + points had a ``y`` value equal of ``4``. + + :rtype: :class:`str ` or numeric or :obj:`None ` + """ + return self._value + + @value.setter + def value(self, value): + if value is None: + self._value = None + else: + try: + self._value = validators.numeric(value) + except (ValueError, TypeError): + self._value = validators.string(value) + + @property + def within(self) -> Optional[str]: + """The data values within which to map the audio parameter. Accepts ``'series'``, ``'chart'``, ``'yAxis'``, + ``'xAxis'``. Defaults to ``'yAxis'``. + + .. note:: + + Mapping within ``'series'`` will make the lowest value point in the *series* map to the + :meth:`.min ` audio parameter value, and the + highest value will map to the :meth:`.max ` + audio parameter. + + Mapping within ``'chart'`` will make the lowest value point in the *chart* (across all series) map to the + :meth:`.min ` audio parameter value, and the + highest value will map to the :meth:`.max ` + audio parameter. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._within + + @within.setter + def within(self, value): + if not value: + self._within = None + else: + value = validators.string(value) + value = value.lower() + if value not in ['series', 'chart', 'xaxis', 'yaxis']: + raise errors.HighchartsValueError(f'.within expects a value of either "series", "chart", ' + f'"xAxis", or "yAxis". Received: "{value}"') + if value == 'xaxis': + value = 'xAxis' + elif value == 'yaxis': + value = 'yAxis' + + self._within = value + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'map_function': as_dict.get('mapFunction', None), + 'map_to': as_dict.get('mapTo', None), + 'max': as_dict.get('max', None), + 'min': as_dict.get('min', None), + 'scale': as_dict.get('scale', None), + 'value': as_dict.get('value', None), + 'within': as_dict.get('within', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'mapFunction': self.map_function, + 'mapTo': self.map_to, + 'max': self.max, + 'min': self.min, + 'scale': self.scale, + 'value': self.value, + 'within': self.within, + } + + return untrimmed + + +class AudioFilter(HighchartsMeta): + """Configuration of an audio filter, such as a :term:`highpass` or :term:`lowpass` filter. + """ + + def __init__(self, **kwargs): + self._frequency = None + self._resonance = None + + self.frequency = kwargs.get('frequency', None) + self.resonance = kwargs.get('resonance', None) + + @property + def frequency(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """The frequency, expressed in Hertz, between 1 to 20,000 Hz. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._frequency + + @frequency.setter + def frequency(self, value): + if value is None: + self._frequency = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value, + minimum = 1, + maximum = 20000) + except (ValueError, TypeError): + value = validators.string(value) + + self._frequency = value + + @property + def resonance(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """The resonance, expressed in Db. Can be negative to cause a dip, or positive to cause a bump. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._resonance + + @resonance.setter + def resonance(self, value): + if value is None: + self._resonance = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value) + except (ValueError, TypeError): + value = validators.string(value) + + self._resonance = value + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'frequency': as_dict.get('frequency', None), + 'resonance': as_dict.get('resonance', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'frequency': self.frequency, + 'resonance': self.resonance, + } + + return untrimmed + + +class TremoloEffect(HighchartsMeta): + """Configuration of a :term:`tremolo` effect.""" + + def __init__(self, **kwargs): + self._depth = None + self._speed = None + + self.depth = kwargs.get('depth', None) + self.speed = kwargs.get('speed', None) + + @property + def depth(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """The intensity of the :term:`tremolo` effect over time, mapped from ``0`` to ``1``. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._depth + + @depth.setter + def depth(self, value): + if value is None: + self._depth = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value, + minimum = 0, + maximum = 1) + except (ValueError, TypeError): + value = validators.string(value) + + self._depth = value + + @property + def speed(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """The speed of the :term:`tremolo` effect, mapped from ``0`` to ``1``, which determines how rapidly + the volume changes. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._speed + + @speed.setter + def speed(self, value): + if value is None: + self._speed = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value, + minimum = 0, + maximum = 1) + except (ValueError, TypeError): + value = validators.string(value) + + self._speed = value + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'depth': as_dict.get('depth', None), + 'speed': as_dict.get('speed', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'depth': self.depth, + 'speed': self.speed, + } + + return untrimmed + + +class SonificationMapping(HighchartsMeta): + """Mapping options for audio parameters. + + All properties in this object accept either: + + * an object instance (or coercable to an object instance) as outlined in the documentation + * a :class:`CallbackFunction ` + * a :class:`str ` + * a numeric value + + If supplying a :class:`CallbackFunction `, + the function is expected to: + + * be called for each audio event to be played + * receive a context object parameter containing ``time``, and potentially ``point`` and ``value`` depending on the + track being played (``point`` provided if the audio event is related to a data point, and ``value`` if the + track is used as a context track with + :meth:`.value_interval ` set). + + """ + + def __init__(self, **kwargs): + self._frequency = None + self._gap_between_notes = None + self._highpass = None + self._lowpass = None + self._note_duration = None + self._pan = None + self._pitch = None + self._play_delay = None + self._rate = None + self._text = None + self._time = None + self._tremolo = None + self._volume = None + + self.frequency = kwargs.get('frequency', None) + self.gap_between_notes = kwargs.get('gap_between_notes', None) + self.highpass = kwargs.get('highpass', None) + self.lowpass = kwargs.get('lowpass', None) + self.note_duration = kwargs.get('note_duration', None) + self.pan = kwargs.get('pan', None) + self.pitch = kwargs.get('pitch', None) + self.play_delay = kwargs.get('play_delay', None) + self.rate = kwargs.get('rate', None) + self.text = kwargs.get('text', None) + self.time = kwargs.get('time', None) + self.tremolo = kwargs.get('tremolo', None) + self.volume = kwargs.get('volume', None) + + @property + def frequency(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """The frequency, expressed in Hertz, of notes. + + Overrides :meth:`.pitch ` if set. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._frequency + + @frequency.setter + def frequency(self, value): + if value is None: + self._frequency = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value) + except (ValueError, TypeError): + value = validators.string(value) + + self._frequency = value + + @property + def gap_between_notes(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """The gap in milliseconds between notes if + :meth:`.pitch ` is mapped to an + array of notes. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._gap_between_notes + + @gap_between_notes.setter + def gap_between_notes(self, value): + if value is None: + self._gap_between_notes = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value) + except (ValueError, TypeError): + value = validators.string(value) + + self._gap_between_notes = value + + @property + def highpass(self) -> Optional[AudioFilter]: + """Configuration for the :term:`highpass` filter. + + :rtype: :class:`AudioFilter ` or + :obj:`None ` + """ + return self._highpass + + @highpass.setter + @class_sensitive(AudioFilter) + def highpass(self, value): + self._highpass = value + + @property + def lowpass(self) -> Optional[AudioFilter]: + """Configuration for the :term:`lowpass` filter. + + :rtype: :class:`AudioFilter ` or + :obj:`None ` + """ + return self._lowpass + + @lowpass.setter + @class_sensitive(AudioFilter) + def lowpass(self, value): + self._lowpass = value + + @property + def note_duration(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """How long a note plays in a sustained fashion, expressed in milliseconds. + + .. note:: + + It only affects instruments that are able to play continuous sustained notes. Examples of these instruments + from the presets include: ``'flute'``, ``'saxophone'``, ``'trumpet'``, ``'sawsynth'``, ``'wobble'``, + ``'basic1'``, ``'basic2'``, ``'sine'``, ``'sineGlide'``, ``'triangle'``, ``'square'``, ``'sawtooth'``, + ``'noise'``, ``'filteredNoise'``, and ``'wind'``. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._note_duration + + @note_duration.setter + def note_duration(self, value): + if value is None: + self._note_duration = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value) + except (ValueError, TypeError): + value = validators.string(value) + + self._note_duration = value + + @property + def pan(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """The stereo panning position of the sound, defined between left (``-1``) and right (``1``). + Defaults to a mapping object which maps to the ``'x'`` property, which causes the sound to move + from left to right as the chart plays. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._pan + + @pan.setter + def pan(self, value): + if value is None: + self._pan = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value, + minimum = -1, + maximum = 1) + except (ValueError, TypeError): + value = validators.string(value) + + self._pan = value + + @property + def pitch(self) -> Optional[List[PitchParameter | CallbackFunction | str | int | float | Decimal] | PitchParameter | CallbackFunction | str | int | float | Decimal]: + """Configuration of :term:`pitch` for the audio track. + + By default, this object maps to ``'y'``, so low ``'y'`` values are played with a lower pitch + and high ``'y'`` values are played with a higher pitch. + + .. note:: + + The properties for configuring pitch accept :class:`str ` values as well as + numerical values, specifically allowing you to indicate the musical note that you wish the + audio track to play. These notes are expressed in the form ````, such as ``'c6'`` or + ``'F#2'``. + + .. tip:: + + You configure the pitch to map to an array of notes, with the delay between notes determined by the + :meth:`SonificationMapping.gap_between_notes ` + property. + + .. tip:: + + You can also define a musical + :meth:`.scale ` + to follow when mapping. + + :rtype: iterable of :class:`PitchParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric, or :class:`PitchParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric, or:obj:`None ` + """ + return self._pitch + + @pitch.setter + def pitch(self, value): + def check_pitch_values(item): + try: + item = validate_types(item, types = (PitchParameter)) + except (ValueError, TypeError): + try: + item = validate_types(item, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + item = validators.numeric(item) + except (ValueError, TypeError): + item = validators.string(item) + + return item + + if value is None: + self._pitch = None + elif checkers.is_iterable(value, forbid_literals = (str, bytes, dict)): + self._pitch = [check_pitch_values(x) for x in value] + else: + self._pitch = check_pitch_values(value) + + @property + def play_delay(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """The number of milliseconds to wait before playing, which comes in addition to the time determined by + the :meth:`.time ` property. + + .. tip:: + + Can also be negative, which can cause the audio to play *before* the mapped time. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._play_delay + + @play_delay.setter + def play_delay(self, value): + if value is None: + self._play_delay = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value) + except (ValueError, TypeError): + value = validators.string(value) + + self._play_delay = value + + @property + def rate(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """Mapping configuration for the speech rate multiplier. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._rate + + @rate.setter + def rate(self, value): + if value is None: + self._rate = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value) + except (ValueError, TypeError): + value = validators.string(value) + + self._rate = value + + @property + def text(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """Mapping configuration for the text parameter for speech tracks. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._text + + @text.setter + def text(self, value): + if value is None: + self._text = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value) + except (ValueError, TypeError): + value = validators.string(value) + + self._text = value + + @property + def time(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """The offset, expressed in milliseconds, which determines when audio for a point is played (e.g. a value of + ``'0'`` means it plays immediately when the chart is sonified). + + By default, it is mapped to the ``'x'`` property, which means that points with the lowest ``'x'`` values will + play first, while points with the highest value will play last. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._time + + @time.setter + def time(self, value): + if value is None: + self._time = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value) + except (ValueError, TypeError): + value = validators.string(value) + + self._time = value + + @property + def tremolo(self) -> Optional[TremoloEffect]: + """Mapping options for a :term:`tremolo` effect. + + :rtype: :class:`TremoloEffect ` or + :obj:`None ` + """ + return self._tremolo + + @tremolo.setter + def tremolo(self, value): + if value is None: + self._tremolo = None + else: + try: + value = validate_types(value, types = (TremoloEffect)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value) + except (ValueError, TypeError): + value = validators.string(value) + + self._tremolo = value + + @property + def volume(self) -> Optional[AudioParameter | CallbackFunction | str | int | float | Decimal]: + """The volume at which to play notes, expressed from ``0`` (muted) to ``1``. + + :rtype: :class:`AudioParameter ` or + :class:`CallbackFunction ` or + :class:`str ` or numeric or :obj:`None ` + """ + return self._volume + + @volume.setter + def volume(self, value): + if value is None: + self._volume = None + else: + try: + value = validate_types(value, types = (AudioParameter)) + except (ValueError, TypeError): + try: + value = validate_types(value, types = (CallbackFunction)) + except (ValueError, TypeError): + try: + value = validators.numeric(value, + minimum = 0, + maximum = 1) + except (ValueError, TypeError): + value = validators.string(value) + + self._volume = value + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'frequency': as_dict.get('frequency', None), + 'gap_between_notes': as_dict.get('gapBetweenNotes', None), + 'highpass': as_dict.get('highpass', None), + 'lowpass': as_dict.get('lowpass', None), + 'note_duration': as_dict.get('noteDuration', None), + 'pan': as_dict.get('pan', None), + 'pitch': as_dict.get('pitch', None), + 'play_delay': as_dict.get('playDelay', None), + 'rate': as_dict.get('rate', None), + 'text': as_dict.get('text', None), + 'time': as_dict.get('time', None), + 'tremolo': as_dict.get('tremolo', None), + 'volume': as_dict.get('volume', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'frequency': self.frequency, + 'gapBetweenNotes': self.gap_between_notes, + 'highpass': self.highpass, + 'lowpass': self.lowpass, + 'noteDuration': self.note_duration, + 'pan': self.pan, + 'pitch': self.pitch, + 'playDelay': self.play_delay, + 'rate': self.rate, + 'text': self.text, + 'time': self.time, + 'tremolo': self.tremolo, + 'volume': self.volume, + } + + return untrimmed diff --git a/highcharts_core/options/sonification/track_configurations.py b/highcharts_core/options/sonification/track_configurations.py new file mode 100644 index 00000000..c30fe442 --- /dev/null +++ b/highcharts_core/options/sonification/track_configurations.py @@ -0,0 +1,640 @@ +from typing import Optional +from decimal import Decimal + +from validator_collection import validators + +from highcharts_core import errors, constants +from highcharts_core.metaclasses import HighchartsMeta +from highcharts_core.decorators import class_sensitive, validate_types +from highcharts_core.utility_functions import mro__to_untrimmed_dict +from highcharts_core.utility_classes.javascript_functions import CallbackFunction +from highcharts_core.options.sonification.mapping import SonificationMapping +from highcharts_core.options.sonification.grouping import SonificationGrouping + + +class ActiveWhen(HighchartsMeta): + """Definition of the condition for when a track should be active or not.""" + + def __init__(self, **kwargs): + self._crossing_down = None + self._crossing_up = None + self._max = None + self._min = None + self._prop = None + + self.crossing_down = kwargs.get('crossing_down', None) + self.crossing_up = kwargs.get('crossing_up', None) + self.max = kwargs.get('max', None) + self.min = kwargs.get('min', None) + self.prop = kwargs.get('prop', None) + + @property + def crossing_down(self) -> Optional[int | float | Decimal]: + """Track will be active when the property indicated by + :meth:`.prop ` is + at or *below* this value. Defaults to :obj:`None `. + + .. warning:: + + If both ``.crossing_down`` and + :meth:`.crossing_up ` are + defined, the track will be active if either condition is met. + + :rtype: numeric or :obj:`None ` + """ + return self._crossing_down + + @crossing_down.setter + def crossing_down(self, value): + self._crossing_down = validators.numeric(value, allow_empty = True) + + @property + def crossing_up(self) -> Optional[int | float | Decimal]: + """Track will be active when the property indicated by + :meth:`.prop ` is + at or *above* this value. Defaults to :obj:`None `. + + .. warning:: + + If both ``.crossing_up`` and + :meth:`.crossing_down ` + are defined, the track will be active if either condition is met. + + :rtype: numeric or :obj:`None ` + """ + return self._crossing_up + + @crossing_up.setter + def crossing_up(self, value): + self._crossing_up = validators.numeric(value, allow_empty = True) + + @property + def max(self) -> Optional[int | float | Decimal]: + """Track will be active when the property indicated by + :meth:`.prop ` is + at or *below* this value. Defaults to :obj:`None `. + + :rtype: numeric or :obj:`None ` + """ + return self._max + + @max.setter + def max(self, value): + self._max = validators.numeric(value, allow_empty = True) + + @property + def min(self) -> Optional[int | float | Decimal]: + """Track will be active when the property indicated by + :meth:`.prop ` is + at or *above* this value. Defaults to :obj:`None `. + + :rtype: numeric or :obj:`None ` + """ + return self._min + + @min.setter + def min(self, value): + self._min = validators.numeric(value, allow_empty = True) + + @property + def prop(self) -> Optional[str]: + """The data point property to use when evaluating the condition, for example ``'y'`` or ``'x'``. + Defaults to :obj:`None `. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._prop + + @prop.setter + def prop(self, value): + self._prop = validators.string(value, allow_empty = True) + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'crossing_down': as_dict.get('crossingDown', None), + 'crossing_up': as_dict.get('crossingUp', None), + 'max': as_dict.get('max', None), + 'min': as_dict.get('min', None), + 'prop': as_dict.get('prop', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'crossingDown': self.crossing_down, + 'crossingUp': self.crossing_up, + 'max': self.max, + 'min': self.min, + 'prop': self.prop, + } + + return untrimmed + + +class TrackConfigurationBase(HighchartsMeta): + """Base class for use in configuring Sonification tracks.""" + + def __init__(self, **kwargs): + self._active_when = None + self._mapping = None + self._midi_name = None + self._point_grouping = None + self._show_play_marker = None + self._type = None + + self.active_when = kwargs.get('active_when', None) + self.mapping = kwargs.get('mapping', None) + self.midi_name = kwargs.get('midi_name', None) + self.point_grouping = kwargs.get('point_grouping', None) + self.show_play_marker = kwargs.get('show_play_marker', None) + self.type = kwargs.get('type', None) + + @property + def active_when(self) -> Optional[ActiveWhen | CallbackFunction]: + """The condition for when a track should be active or not. + + Accepts either a (Javascript) + :class:`CallbackFunction ` or an + :class:`ActiveWhen ` configuration object. + + .. note:: + + If a callback function is used, it should return a boolean for whether or not the track should be active. + + The function is called for each audio event, and receives a parameter object with ``time``, and potentially + ``point`` and ``value`` properties depending on the track. + + ``point`` is available if the audio event is related to a data point. + + ``value`` is available if the track is used as a context track, and + :meth:`.value_interval ` + is used. + + :rtype: :class:`ActiveWhen ` or + :class:`CallbackFunction ` or + :obj:`None `. + """ + return self._active_when + + @active_when.setter + def active_when(self, value): + if not value: + self._active_when = None + else: + try: + value = validate_types(value, types = (ActiveWhen)) + except (ValueError, TypeError): + value = validate_types(value, types = (CallbackFunction)) + + self._active_when = value + + @property + def mapping(self) -> Optional[SonificationMapping]: + """Mapping options for the audio parameter. + + :rtype: :class:`SonificationMapping ` or + :obj:`None ` + """ + return self._mapping + + @mapping.setter + @class_sensitive(SonificationMapping) + def mapping(self, value): + self._mapping = value + + @property + def point_grouping(self) -> Optional[SonificationGrouping]: + """Options for configurign the grouping of points. + + :rtype: :class:`SonificationGrouping ` or + :obj:`None ` + """ + return self._point_grouping + + @point_grouping.setter + @class_sensitive(SonificationGrouping) + def point_grouping(self, value): + self._point_grouping = value + + @property + def show_play_marker(self) -> Optional[bool]: + """If ``True``, displays the play marker (tooltip and/or crosshair) for a track. Defaults to ``True``. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._show_play_marker + + @show_play_marker.setter + def show_play_marker(self, value): + if value is None: + self._show_play_marker = None + else: + self._show_play_marker = bool(value) + + @property + def type(self) -> Optional[str]: + """The type of track. Accepts either ``'instrument'`` or ``'speech'``. Defaults to ``'instrument'``. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._type + + @type.setter + def type(self, value): + if not value: + self._type = None + else: + value = validators.string(value) + value = value.lower() + if value not in ['instrument', 'speech']: + raise errors.HighchartsValueError(f'type expects either "instrument" or "speech". ' + f'Received: "{value}"') + self._type = value + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'active_when': as_dict.get('activeWhen', None), + 'mapping': as_dict.get('mapping', None), + 'midi_name': as_dict.get('midiName', None) or as_dict.get('MIDIName', None), + 'point_grouping': as_dict.get('pointGrouping', None), + 'show_play_marker': as_dict.get('showPlayMarker', None), + 'type': as_dict.get('type', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'activeWhen': self.active_when, + 'mapping': self.mapping, + 'midiName': self.midi_name, + 'pointGrouping': self.point_grouping, + 'showPlayMarker': self.show_play_marker, + 'type': self.type, + } + + return untrimmed + + +class InstrumentTrackConfiguration(TrackConfigurationBase): + """Configuration of an Instrument Track for use in sonification.""" + + def __init__(self, **kwargs): + self._instrument = None + self._round_to_musical_notes = None + + self.instrument = kwargs.get('instrument', None) + self.round_to_musical_notes = kwargs.get('round_to_musical_notes', None) + + super().__init__(**kwargs) + + @property + def instrument(self) -> Optional[str]: + """The instrument to use for playing. Defaults to ``'piano'``. + + Accepts: + + * ``'flute'`` + * ``'saxophone'`` + * ``'trumpet'`` + * ``'sawsynth'`` + * ``'wobble'`` + * ``'basic1'`` + * ``'basic2'`` + * ``'sine'`` + * ``'sineGlide'`` + * ``'triangle'`` + * ``'square'`` + * ``'sawtooth'`` + * ``'noise'`` + * ``'filteredNoise'`` + * ``'wind'`` + + :rtype: :class:`str ` + """ + return self._instrument + + @instrument.setter + def instrument(self, value): + if not value: + self._instrument = None + else: + value = validators.string(value) + value = value.lower() + if value not in constants.INSTRUMENT_PRESETS: + raise errors.HighchartsValueError(f'.instrument expects a predefined instrument name. Did not ' + f'recognize: "{value}".') + self._instrument = value + + @property + def midi_name(self) -> Optional[str]: + """The name to use for a track when exporting it to MIDI. If :obj:`None `, will + use the series name if the track is related to a series. Defaults to :obj:`None `. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._midi_name + + @midi_name.setter + def midi_name(self, value): + self._midi_name = validators.string(value, allow_empty = True) + + @property + def round_to_musical_notes(self) -> Optional[bool]: + """If ``True``, will round pitch matching to musical notes in 440Hz standard tuning. If ``False``, + will play the exact mapped/configured note even if it is out of tune as per standard tuning. Defaults to ``True``. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._round_to_musical_notes + + @round_to_musical_notes.setter + def round_to_musical_notes(self, value): + if value is None: + self._round_to_musical_notes = None + else: + self._round_to_musical_notes = bool(value) + + @property + def type(self) -> Optional[str]: + """The type of track. + + .. note:: + + In the context of an :class:`InstrumentTrackConfiguration`, this will *always* return ``'instrument'`` if + not :obj:`None `. + + :rtype: :class:`str ` or :obj:`None ` + """ + if self._instrument: + return 'instrument' + + return None + + @type.setter + def type(self, value): + if not value: + self._type = None + else: + value = validators.string(value) + value = value.lower() + if value not in ['instrument', 'speech']: + raise errors.HighchartsValueError(f'type expects either "instrument" or "speech". ' + f'Received: "{value}"') + self._type = value + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'active_when': as_dict.get('activeWhen', None), + 'mapping': as_dict.get('mapping', None), + 'point_grouping': as_dict.get('pointGrouping', None), + 'show_play_marker': as_dict.get('showPlayMarker', None), + 'type': as_dict.get('type', None), + + 'instrument': as_dict.get('instrument', None), + 'midi_name': as_dict.get('midiName', None) or as_dict.get('MIDIName', None), + 'round_to_musical_notes': as_dict.get('roundToMusicalNotes', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'instrument': self.instrument, + 'midiName': self.midi_name, + 'roundToMusicalNotes': self.round_to_musical_notes, + } + + 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 + + +class SpeechTrackConfiguration(TrackConfigurationBase): + """Configuration of a Speech Track for use in sonification.""" + + def __init__(self, **kwargs): + self._language = None + self._preferred_voice = None + + self.language = kwargs.get('language', None) + self.preferred_voice = kwargs.get('preferred_voice', None) + + super().__init__(**kwargs) + + @property + def language(self) -> Optional[str]: + """The language to speak in for speech tracks, as an `IETF BCP 47 `__ + language tag. Defaults to ``'en-US'``. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._language + + @language.setter + def language(self, value): + self._language = validators.string(value, allow_empty = True) + + @property + def preferred_voice(self) -> Optional[str]: + """The name of the voice synthesis to prefer for speech tracks. If :obj:`None ` or + unavabilable, will fall back to the default voice for the selected language. Defaults to + :obj:`None `. + + .. warning:: + + Different platforms (operating systems in which your users will view your visualizations) + provide different voices for web speech synthesis. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._preferred_voice + + @preferred_voice.setter + def preferred_voice(self, value): + self._preferred_voice = validators.string(value, allow_empty = True) + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'active_when': as_dict.get('activeWhen', None), + 'mapping': as_dict.get('mapping', None), + 'point_grouping': as_dict.get('pointGrouping', None), + 'show_play_marker': as_dict.get('showPlayMarker', None), + 'type': as_dict.get('type', None), + + 'language': as_dict.get('language', None), + 'preferred_voice': as_dict.get('preferredVoice', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'language': self.language, + 'preferredVoice': self.preferred_voice, + } + + 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 + + +class ContextTrackConfiguration(InstrumentTrackConfiguration, SpeechTrackConfiguration): + """Configuration of a Context Track for use in sonification.""" + + def __init__(self, **kwargs): + self._time_interval = None + self._value_interval = None + self._value_map_function = None + self._value_prop = None + + self.time_interval = kwargs.get('time_interval', None) + self.value_interval = kwargs.get('value_interval', None) + self.value_map_function = kwargs.get('value_map_function', None) + self.value_prop = kwargs.get('value_prop', None) + + super().__init__(**kwargs) + + @property + def time_interval(self) -> Optional[int | float | Decimal]: + """Determines the number of milliseconds between playback of a context track. Defaults to + :obj:`None `. + + :rtype: numeric or :obj:`None ` + """ + return self._time_interval + + @time_interval.setter + def time_interval(self, value): + self._time_interval = validators.numeric(value, allow_empty = True) + + @property + def value_interval(self) -> Optional[int | float | Decimal]: + """Determines the number of units between playback of a context track, where + units are determined by :meth:`.value_prop `. + + For example, setting + :meth:`.value_prop ` + to ``'x'`` and ``.value_interval`` to ``5`` means the context track + should be played for every 5th value of ``'x'``. + + .. note:: + + The context audio events will be mapped to time according to the prop value relative to the min/max values + for that prop. + + :rtype: numeric or :obj:`None ` + """ + return self._value_interval + + @value_interval.setter + def value_interval(self, value): + self._value_interval = validators.numeric(value, allow_empty = True) + + @property + def value_map_function(self) -> Optional[str]: + """Determines how to map context events to time when using the + :meth:`.value_interval ` + property. Accepts either ``'linear'`` or ``'logarithmic'``. Defaults to ``'linear'``. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._value_map_function + + @value_map_function.setter + def value_map_function(self, value): + if not value: + self._value_map_function = None + else: + value = validators.string(value) + value = value.lower() + if value not in ['linear', 'logarithmic']: + raise errors.HighchartsValueError(f'value_map_function expects either "linear" or ' + f'"logarithmic. Received: "{value}"') + + self._value_map_function = value + + @property + def value_prop(self) -> Optional[str]: + """The data point property to use when evaluating whether to play the context track in conjunction with + :meth:`.value_interval ` + Defaults to :obj:`None `. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._value_prop + + @value_prop.setter + def value_prop(self, value): + self._value_prop = validators.string(value, allow_empty = True) + + @property + def type(self) -> Optional[str]: + """The type of track. Accepts either ``'instrument'`` or ``'speech'``. Defaults to ``'instrument'``. + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._type + + @type.setter + def type(self, value): + if not value: + self._type = None + else: + value = validators.string(value) + value = value.lower() + if value not in ['instrument', 'speech']: + raise errors.HighchartsValueError(f'type expects either "instrument" or "speech". ' + f'Received: "{value}"') + self._type = value + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'active_when': as_dict.get('activeWhen', None), + 'mapping': as_dict.get('mapping', None), + 'point_grouping': as_dict.get('pointGrouping', None), + 'show_play_marker': as_dict.get('showPlayMarker', None), + 'type': as_dict.get('type', None), + + 'instrument': as_dict.get('instrument', None), + 'midi_name': as_dict.get('midiName', None) or as_dict.get('MIDIName', None), + 'round_to_musical_notes': as_dict.get('roundToMusicalNotes', None), + + 'language': as_dict.get('language', None), + 'preferred_voice': as_dict.get('preferredVoice', None), + + 'time_interval': as_dict.get('timeInterval', None), + 'value_interval': as_dict.get('valueInterval', None), + 'value_map_function': as_dict.get('valueMapFunction', None), + 'value_prop': as_dict.get('valueProp', None), + + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'timeInterval': self.time_interval, + 'valueInterval': self.value_interval, + 'valueMapFunction': self.value_map_function, + 'valueProp': self.value_prop, + } + + parent_as_dict = mro__to_untrimmed_dict(self, in_cls = in_cls) + + for key in parent_as_dict: + untrimmed[key] = parent_as_dict[key] + + return untrimmed \ No newline at end of file diff --git a/highcharts_core/utility_classes/events.py b/highcharts_core/utility_classes/events.py index 60755d90..b45a3432 100644 --- a/highcharts_core/utility_classes/events.py +++ b/highcharts_core/utility_classes/events.py @@ -1191,3 +1191,211 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: } return untrimmed + + +class SonificationEvents(HighchartsMeta): + """Event handlers for sonification.""" + + def __init__(self, **kwargs): + self._after_update = None + self._before_play = None + self._before_update = None + self._on_boundary_hit = None + self._on_end = None + self._on_play = None + self._on_series_end = None + self._on_series_start = None + self._on_stop = None + + self.after_update = kwargs.get('after_update', None) + self.before_play = kwargs.get('before_play', None) + self.before_update = kwargs.get('before_update', None) + self.on_boundary_hit = kwargs.get('on_boundary_hit', None) + self.on_end = kwargs.get('on_end', None) + self.on_play = kwargs.get('on_play', None) + self.on_series_end = kwargs.get('on_series_end', None) + self.on_series_start = kwargs.get('on_series_start', None) + self.on_stop = kwargs.get('on_stop', None) + + @property + def after_update(self) -> Optional[CallbackFunction]: + """Event (Javascript) :term:`callback function` that is called *after* updating the + sonification. + + A context object is passed to the function, with properties ``chart`` and ``timeline``. + + :rtype: :class:`CallbackFunction ` or + :obj:`None ` + """ + return self._after_update + + @after_update.setter + @class_sensitive(CallbackFunction) + def after_update(self, value): + self._after_update = value + + @property + def before_play(self) -> Optional[CallbackFunction]: + """Event (Javascript) :term:`callback function` that is called immediately when playback is requested. + + A context object is passed to the function, with properties ``chart`` and ``timeline``. + + :rtype: :class:`CallbackFunction ` or + :obj:`None ` + """ + return self._before_play + + @before_play.setter + @class_sensitive(CallbackFunction) + def before_play(self, value): + self._before_play = value + + @property + def before_update(self) -> Optional[CallbackFunction]: + """Event (Javascript) :term:`callback function` that is called *before* updating the + sonification. + + A context object is passed to the function, with properties ``chart`` and ``timeline``. + + :rtype: :class:`CallbackFunction ` or + :obj:`None ` + """ + return self._before_update + + @before_update.setter + @class_sensitive(CallbackFunction) + def before_update(self, value): + self._before_update = value + + @property + def on_boundary_hit(self) -> Optional[CallbackFunction]: + """Event (Javascript) :term:`callback function` that is called when attempting to play an adjacent point + or series, and there is none found. By defualt, a percussive sound is played. + + A context object is passed to the function, with properties ``chart``, ``timeline``, and ``attemptedNext``. The + ``attemptedNext`` property is a boolean value that is ``true`` if the boundary hit was from trying to play the + next series/point, and ``false`` if it was from trying to play the previous. + + :rtype: :class:`CallbackFunction ` or + :obj:`None ` + """ + return self._on_boundary_hit + + @on_boundary_hit.setter + @class_sensitive(CallbackFunction) + def on_boundary_hit(self, value): + self._on_boundary_hit = value + + @property + def on_end(self) -> Optional[CallbackFunction]: + """Event (Javascript) :term:`callback function` that is called when playback is completed. + + A context object is passed to the function, with properties ``chart``, ``timeline``, and ``pointsPlayed`` where + ``pointsPlayed`` is an array of ``Point`` objects referencing data points related to the audio events played. + + :rtype: :class:`CallbackFunction ` or + :obj:`None ` + """ + return self._on_end + + @on_end.setter + @class_sensitive(CallbackFunction) + def on_end(self, value): + self._on_end = value + + @property + def on_play(self) -> Optional[CallbackFunction]: + """Event (Javascript) :term:`callback function` that is called on play. + + A context object is passed to the function, with properties ``chart`` and ``timeline``. + + :rtype: :class:`CallbackFunction ` or + :obj:`None ` + """ + return self._on_play + + @on_play.setter + @class_sensitive(CallbackFunction) + def on_play(self, value): + self._on_play = value + + @property + def on_series_end(self) -> Optional[CallbackFunction]: + """Event (Javascript) :term:`callback function` that is called when finished playing a series. + + A context object is passed to the function, with properties ``series`` and ``timeline``. + + :rtype: :class:`CallbackFunction ` or + :obj:`None ` + """ + return self._on_series_end + + @on_series_end.setter + @class_sensitive(CallbackFunction) + def on_series_end(self, value): + self._on_series_end = value + + @property + def on_series_start(self) -> Optional[CallbackFunction]: + """Event (Javascript) :term:`callback function` that is called when starting to play a series. + + A context object is passed to the function, with properties ``series`` and ``timeline``. + + :rtype: :class:`CallbackFunction ` or + :obj:`None ` + """ + return self._on_series_start + + @on_series_start.setter + @class_sensitive(CallbackFunction) + def on_series_start(self, value): + self._on_series_start = value + + @property + def on_stop(self) -> Optional[CallbackFunction]: + """Event (Javascript) :term:`callback function` that is called on pause, cancel, or if playback is + completed. + + A context object is passed to the function, with properties ``chart``, ``timeline``, and ``pointsPlayed`` where + ``pointsPlayed`` is an array of ``Point`` objects referencing data points related to the audio events played. + + :rtype: :class:`CallbackFunction ` or + :obj:`None ` + """ + return self._on_stop + + @on_stop.setter + @class_sensitive(CallbackFunction) + def on_stop(self, value): + self._on_stop = value + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'after_update': as_dict.get('afterUpdate', None), + 'before_play': as_dict.get('beforePlay', None), + 'before_update': as_dict.get('beforeUpdate', None), + 'on_boundary_hit': as_dict.get('onBoundaryHit', None), + 'on_end': as_dict.get('onEnd', None), + 'on_play': as_dict.get('onPlay', None), + 'on_series_end': as_dict.get('onSeriesEnd', None), + 'on_series_start': as_dict.get('onSeriesStart', None), + 'on_stop': as_dict.get('onStop', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'afterUpdate': self.after_update, + 'beforePlay': self.before_play, + 'beforeUpdate': self.before_update, + 'onBoundaryHit': self.on_boundary_hit, + 'onEnd': self.on_end, + 'onPlay': self.on_play, + 'onSeriesEnd': self.on_series_end, + 'onSeriesStart': self.on_series_start, + 'onStop': self.on_stop, + } + + return untrimmed diff --git a/tests/input_files/sonification/sonification/01.js b/tests/input_files/sonification/sonification/01.js new file mode 100644 index 00000000..dd6ecd2f --- /dev/null +++ b/tests/input_files/sonification/sonification/01.js @@ -0,0 +1,458 @@ +{ + afterSeriesWait: 300, + defaultInstrumentOptions: { + instrument: 'piano', + midiName: 'some-value', + roundToMusicalNotes: true, + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument' + }, + defaultSpeechOptions: { + language: 'en-US', + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'speech' + }, + duration: 6000, + enabled: true, + events: { + afterUpdate: function() { return true; }, + beforePlay: function() { return true; }, + beforeUpdate: function() { return true; }, + onBoundaryHit: function() { return true; }, + onEnd: function() { return true; }, + onPlay: function() { return true; }, + onSeriesEnd: function() { return true; }, + onSeriesStart: function() { return true; }, + onStop: function() { return true; } + }, + globalContextTracks: { + valueInterval: 5, + valueMapFunction: 'linear', + valueProp: 'y', + midiName: 'some-value', + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument' + }, + globalTracks: { + instrument: 'piano', + midiName: 'some-value', + roundToMusicalNotes: true, + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument' + }, + masterVolume: 0.5, + order: 'sequential', + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showCrosshair: true, + showTooltip: true, + updateInterval: 200 +} \ No newline at end of file diff --git a/tests/input_files/sonification/sonification/error-01.js b/tests/input_files/sonification/sonification/error-01.js new file mode 100644 index 00000000..e6466e21 --- /dev/null +++ b/tests/input_files/sonification/sonification/error-01.js @@ -0,0 +1,447 @@ +{ + afterSeriesWait: 'invalid-value', + defaultInstrumentOptions: { + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + instrument: 'piano', + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + midiName: 'some-value', + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + roundToMusicalNotes: true, + showPlayMarker: true, + type: 'instrument' + }, + defaultSpeechOptions: { + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + language: 'en-US', + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + midiName: 'some-value', + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'speech' + }, + duration: 6000, + enabled: true, + events: { + afterUpdate: function() { return true; }, + beforePlay: function() { return true; }, + beforeUpdate: function() { return true; }, + onBoundaryHit: function() { return true; }, + onEnd: function() { return true; }, + onPlay: function() { return true; }, + onSeriesEnd: function() { return true; }, + onSeriesStart: function() { return true; }, + onStop: function() { return true; } + }, + globalContextTracks: { + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + midiName: 'some-value', + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument', + value_interval: 5, + valueMapFunction: 'linear', + valueProp: 'y' + }, + globalTracks: { + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + instrument: 'piano', + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + midiName: 'some-value', + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + roundToMusicalNotes: true, + showPlayMarker: true, + type: 'instrument' + }, + masterVolume: 0.5, + order: 'sequential', + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showCrosshair: true, + showTooltip: true, + updateInterval: 200 +} \ No newline at end of file diff --git a/tests/options/test_sonification.py b/tests/options/test_sonification.py new file mode 100644 index 00000000..3b2e2eb7 --- /dev/null +++ b/tests/options/test_sonification.py @@ -0,0 +1,54 @@ +"""Tests for ``highcharts.responsive``.""" + +import pytest + +from json.decoder import JSONDecodeError + +from highcharts_core.options.sonification import SonificationOptions as cls +from highcharts_core import errors +from tests.fixtures import input_files, check_input_file, to_camelCase, to_js_dict, \ + Class__init__, Class__to_untrimmed_dict, Class_from_dict, Class_to_dict, \ + Class_from_js_literal + +STANDARD_PARAMS = [ + ({}, None), +] + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test__init__(kwargs, error): + Class__init__(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test__to_untrimmed_dict(kwargs, error): + Class__to_untrimmed_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_from_dict(kwargs, error): + Class_from_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_to_dict(kwargs, error): + Class_to_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('filename, as_file, error', [ + ('sonification/sonification/01.js', False, None), + + ('sonification/sonification/error-01.js', False, (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError)), + + ('sonification/sonification/01.js', True, None), + + ('sonification/sonification/error-01.js', True, (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError)), +]) +def test_from_js_literal(input_files, filename, as_file, error): + Class_from_js_literal(cls, input_files, filename, as_file, error) From d38c7db0f812ae99fd4c987df66a94843965321f Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Apr 2023 20:45:51 -0400 Subject: [PATCH 22/24] Added series-level sonification support. --- CHANGES.rst | 1 + docs/api.rst | 2 + docs/api/options/index.rst | 2 + docs/api/options/plot_options/index.rst | 3 + .../api/options/plot_options/sonification.rst | 28 + .../options/plot_options/arcdiagram.py | 1 + highcharts_core/options/plot_options/area.py | 1 + highcharts_core/options/plot_options/bar.py | 5 + .../options/plot_options/bellcurve.py | 1 + .../options/plot_options/boxplot.py | 1 + .../options/plot_options/bubble.py | 1 + .../options/plot_options/bullet.py | 1 + .../options/plot_options/dependencywheel.py | 1 + .../options/plot_options/dumbbell.py | 1 + .../options/plot_options/funnel.py | 1 + highcharts_core/options/plot_options/gauge.py | 2 + .../options/plot_options/generic.py | 19 + .../options/plot_options/heatmap.py | 2 + .../options/plot_options/histogram.py | 1 + highcharts_core/options/plot_options/item.py | 1 + .../options/plot_options/networkgraph.py | 1 + .../options/plot_options/organization.py | 1 + .../options/plot_options/packedbubble.py | 1 + .../options/plot_options/pictorial.py | 1 + highcharts_core/options/plot_options/pie.py | 2 + .../options/plot_options/polygon.py | 1 + .../options/plot_options/scatter.py | 1 + .../options/plot_options/series.py | 2 + .../options/plot_options/sonification.py | 161 +++++ .../options/plot_options/sunburst.py | 1 + .../options/plot_options/timeline.py | 1 + .../options/plot_options/treegraph.py | 1 + .../options/plot_options/treemap.py | 1 + .../options/plot_options/vector.py | 1 + highcharts_core/options/plot_options/venn.py | 1 + .../options/plot_options/wordcloud.py | 1 + highcharts_core/options/series/arcdiagram.py | 1 + highcharts_core/options/series/area.py | 1 + highcharts_core/options/series/bar.py | 5 + highcharts_core/options/series/base.py | 1 + highcharts_core/options/series/bellcurve.py | 1 + highcharts_core/options/series/boxplot.py | 1 + highcharts_core/options/series/bubble.py | 1 + highcharts_core/options/series/bullet.py | 1 + .../options/series/dependencywheel.py | 1 + highcharts_core/options/series/dumbbell.py | 1 + highcharts_core/options/series/funnel.py | 2 + highcharts_core/options/series/gauge.py | 2 + highcharts_core/options/series/heatmap.py | 2 + highcharts_core/options/series/histogram.py | 1 + highcharts_core/options/series/item.py | 1 + .../options/series/networkgraph.py | 1 + .../options/series/organization.py | 1 + .../options/series/packedbubble.py | 1 + highcharts_core/options/series/pareto.py | 1 + highcharts_core/options/series/pictorial.py | 1 + highcharts_core/options/series/pie.py | 2 + highcharts_core/options/series/polygon.py | 1 + highcharts_core/options/series/pyramid.py | 1 + highcharts_core/options/series/sankey.py | 1 + highcharts_core/options/series/scatter.py | 1 + highcharts_core/options/series/spline.py | 1 + highcharts_core/options/series/sunburst.py | 1 + highcharts_core/options/series/timeline.py | 1 + highcharts_core/options/series/treegraph.py | 1 + highcharts_core/options/series/treemap.py | 1 + highcharts_core/options/series/vector.py | 1 + highcharts_core/options/series/venn.py | 1 + highcharts_core/options/series/wordcloud.py | 1 + highcharts_core/utility_classes/events.py | 5 + .../plot_options/sonification/01.js | 440 ++++++++++++++ .../plot_options/sonification/02.js | 551 ++++++++++++++++++ .../plot_options/sonification/error-01.js | 333 +++++++++++ .../options/plot_options/test_sonification.py | 56 ++ 74 files changed, 1679 insertions(+) create mode 100644 docs/api/options/plot_options/sonification.rst create mode 100644 highcharts_core/options/plot_options/sonification.py create mode 100644 tests/input_files/plot_options/sonification/01.js create mode 100644 tests/input_files/plot_options/sonification/02.js create mode 100644 tests/input_files/plot_options/sonification/error-01.js create mode 100644 tests/options/plot_options/test_sonification.py diff --git a/CHANGES.rst b/CHANGES.rst index b0747ade..a1dea01e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -27,6 +27,7 @@ Release 1.1.0 * Added ``.style`` to ``utility_classes.buttons.CollapseButtonConfiguration``. * Added ``utility_classes.events.SimulationEvents`` and modified ``NetworkGraphOptions`` to support. * Added ``options.sonification`` and all related classes. + * Added series-level ``SeriesSonification`` to all series. * **FIXED:** Broken heatmap and tilemap documentation links. * **FIXED:** Fixed missing ``TreegraphOptions`` / ``TreegraphSeries`` series type. diff --git a/docs/api.rst b/docs/api.rst index 428555fb..a0022c2b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -385,6 +385,8 @@ Core Components :class:`Scatter3DOptions ` * - :mod:`.options.plot_options.series ` - :class:`SeriesOptions ` + * - :mod:`.options.plot_options.sonification ` + - :class:`SeriesSonification ` * - :mod:`.options.plot_options.spline ` - :class:`SplineOptions ` * - :mod:`.options.plot_options.sunburst ` diff --git a/docs/api/options/index.rst b/docs/api/options/index.rst index 04fdb063..801aceb5 100644 --- a/docs/api/options/index.rst +++ b/docs/api/options/index.rst @@ -313,6 +313,8 @@ Sub-components :class:`Scatter3DOptions ` * - :mod:`.options.plot_options.series ` - :class:`SeriesOptions ` + * - :mod:`.options.plot_options.sonification ` + - :class:`SeriesSonification ` * - :mod:`.options.plot_options.spline ` - :class:`SplineOptions ` * - :mod:`.options.plot_options.sunburst ` diff --git a/docs/api/options/plot_options/index.rst b/docs/api/options/plot_options/index.rst index 247bc127..5fc6b343 100644 --- a/docs/api/options/plot_options/index.rst +++ b/docs/api/options/plot_options/index.rst @@ -42,6 +42,7 @@ sankey scatter series + sonification spline sunburst timeline @@ -188,6 +189,8 @@ Sub-components :class:`Scatter3DOptions ` * - :mod:`.options.plot_options.series ` - :class:`SeriesOptions ` + * - :mod:`.options.plot_options.sonification ` + - :class:`SeriesSonification ` * - :mod:`.options.plot_options.spline ` - :class:`SplineOptions ` * - :mod:`.options.plot_options.sunburst ` diff --git a/docs/api/options/plot_options/sonification.rst b/docs/api/options/plot_options/sonification.rst new file mode 100644 index 00000000..dd6d2ec3 --- /dev/null +++ b/docs/api/options/plot_options/sonification.rst @@ -0,0 +1,28 @@ +########################################################################################## +:mod:`.sonification ` +########################################################################################## + +.. contents:: Module Contents + :local: + :depth: 3 + :backlinks: entry + +-------------- + +.. module:: highcharts_core.options.plot_options.sonification + +******************************************************************************************************************** +class: :class:`SeriesSonification ` +******************************************************************************************************************** + +.. autoclass:: SeriesSonification + :members: + :inherited-members: + + .. collapse:: Class Inheritance + + .. inheritance-diagram:: SeriesSonification + :top-classes: highcharts_core.metaclasses.HighchartsMeta + :parts: -1 + + | diff --git a/highcharts_core/options/plot_options/arcdiagram.py b/highcharts_core/options/plot_options/arcdiagram.py index d17e7cdc..4b4fa678 100644 --- a/highcharts_core/options/plot_options/arcdiagram.py +++ b/highcharts_core/options/plot_options/arcdiagram.py @@ -279,6 +279,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/area.py b/highcharts_core/options/plot_options/area.py index 36df0307..e7431840 100644 --- a/highcharts_core/options/plot_options/area.py +++ b/highcharts_core/options/plot_options/area.py @@ -147,6 +147,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/bar.py b/highcharts_core/options/plot_options/bar.py index 84954511..f8e7d83d 100644 --- a/highcharts_core/options/plot_options/bar.py +++ b/highcharts_core/options/plot_options/bar.py @@ -309,6 +309,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -496,6 +497,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -784,6 +786,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -999,6 +1002,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -1171,6 +1175,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/bellcurve.py b/highcharts_core/options/plot_options/bellcurve.py index 04f505a5..5f649257 100644 --- a/highcharts_core/options/plot_options/bellcurve.py +++ b/highcharts_core/options/plot_options/bellcurve.py @@ -73,6 +73,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/boxplot.py b/highcharts_core/options/plot_options/boxplot.py index 28f198d4..2154c3bf 100644 --- a/highcharts_core/options/plot_options/boxplot.py +++ b/highcharts_core/options/plot_options/boxplot.py @@ -345,6 +345,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/bubble.py b/highcharts_core/options/plot_options/bubble.py index 70506d6d..922022be 100644 --- a/highcharts_core/options/plot_options/bubble.py +++ b/highcharts_core/options/plot_options/bubble.py @@ -263,6 +263,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/bullet.py b/highcharts_core/options/plot_options/bullet.py index fa01d98b..2b390d07 100644 --- a/highcharts_core/options/plot_options/bullet.py +++ b/highcharts_core/options/plot_options/bullet.py @@ -240,6 +240,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/dependencywheel.py b/highcharts_core/options/plot_options/dependencywheel.py index 480eff29..b46a1a88 100644 --- a/highcharts_core/options/plot_options/dependencywheel.py +++ b/highcharts_core/options/plot_options/dependencywheel.py @@ -335,6 +335,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/dumbbell.py b/highcharts_core/options/plot_options/dumbbell.py index 906ac882..8fc68e4d 100644 --- a/highcharts_core/options/plot_options/dumbbell.py +++ b/highcharts_core/options/plot_options/dumbbell.py @@ -787,6 +787,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/funnel.py b/highcharts_core/options/plot_options/funnel.py index 07e6bfdd..2da9f86e 100644 --- a/highcharts_core/options/plot_options/funnel.py +++ b/highcharts_core/options/plot_options/funnel.py @@ -194,6 +194,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/gauge.py b/highcharts_core/options/plot_options/gauge.py index 6df6450f..49d2b71a 100644 --- a/highcharts_core/options/plot_options/gauge.py +++ b/highcharts_core/options/plot_options/gauge.py @@ -669,6 +669,7 @@ class from a Highcharts Javascript-compatible :class:`dict ` object '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), @@ -858,6 +859,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/generic.py b/highcharts_core/options/plot_options/generic.py index ba483375..387d455e 100644 --- a/highcharts_core/options/plot_options/generic.py +++ b/highcharts_core/options/plot_options/generic.py @@ -10,6 +10,7 @@ from highcharts_core.options.series.labels import SeriesLabel from highcharts_core.options.plot_options.points import OnPointOptions from highcharts_core.options.plot_options.points import Point +from highcharts_core.options.plot_options.sonification import SeriesSonification from highcharts_core.options.tooltips import Tooltip from highcharts_core.utility_classes.animation import AnimationOptions from highcharts_core.utility_classes.gradients import Gradient @@ -52,6 +53,7 @@ def __init__(self, **kwargs): self._show_checkbox = None self._show_in_legend = None self._skip_keyboard_navigation = None + self._sonification = None self._states = None self._sticky_tracking = None self._threshold = None @@ -85,6 +87,7 @@ def __init__(self, **kwargs): self.show_checkbox = kwargs.get('show_checkbox', None) self.show_in_legend = kwargs.get('show_in_legend', None) self.skip_keyboard_navigation = kwargs.get('skip_keyboard_navigation', None) + self.sonification = kwargs.get('sonification', None) self.states = kwargs.get('states', None) self.sticky_tracking = kwargs.get('sticky_tracking', None) self.threshold = kwargs.get('threshold', None) @@ -646,6 +649,20 @@ def skip_keyboard_navigation(self, value): else: self._skip_keyboard_navigation = bool(value) + @property + def sonification(self) -> Optional[SeriesSonification]: + """Sonification configuration for the series type/series. + + :rtype: :class:`SeriesSonification ` or + :obj:`None ` + """ + return self._sonification + + @sonification.setter + @class_sensitive(SeriesSonification) + def sonification(self, value): + self._sonification = value + @property def states(self) -> Optional[States]: """Configuration for state-specific configuration to apply to the data series. @@ -805,6 +822,7 @@ class from a Highcharts Javascript-compatible :class:`dict ` object '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), @@ -843,6 +861,7 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: 'showCheckbox': self.show_checkbox, 'showInLegend': self.show_in_legend, 'skipKeyboardNavigation': self.skip_keyboard_navigation, + 'sonification': self.sonification, 'states': self.states, 'stickyTracking': self.sticky_tracking, 'threshold': self.threshold, diff --git a/highcharts_core/options/plot_options/heatmap.py b/highcharts_core/options/plot_options/heatmap.py index 95c89b59..611a4a46 100644 --- a/highcharts_core/options/plot_options/heatmap.py +++ b/highcharts_core/options/plot_options/heatmap.py @@ -139,6 +139,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -291,6 +292,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/histogram.py b/highcharts_core/options/plot_options/histogram.py index b4ee2dfe..a55ba04d 100644 --- a/highcharts_core/options/plot_options/histogram.py +++ b/highcharts_core/options/plot_options/histogram.py @@ -129,6 +129,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/item.py b/highcharts_core/options/plot_options/item.py index 6f2ac598..baf527ae 100644 --- a/highcharts_core/options/plot_options/item.py +++ b/highcharts_core/options/plot_options/item.py @@ -173,6 +173,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/networkgraph.py b/highcharts_core/options/plot_options/networkgraph.py index 78bc7ca5..c8468611 100644 --- a/highcharts_core/options/plot_options/networkgraph.py +++ b/highcharts_core/options/plot_options/networkgraph.py @@ -661,6 +661,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/organization.py b/highcharts_core/options/plot_options/organization.py index 85810c8a..3eca1cbf 100644 --- a/highcharts_core/options/plot_options/organization.py +++ b/highcharts_core/options/plot_options/organization.py @@ -299,6 +299,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/packedbubble.py b/highcharts_core/options/plot_options/packedbubble.py index 4cb2bfd9..614799ca 100644 --- a/highcharts_core/options/plot_options/packedbubble.py +++ b/highcharts_core/options/plot_options/packedbubble.py @@ -264,6 +264,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/pictorial.py b/highcharts_core/options/plot_options/pictorial.py index 76b3a408..2048c3bd 100644 --- a/highcharts_core/options/plot_options/pictorial.py +++ b/highcharts_core/options/plot_options/pictorial.py @@ -257,6 +257,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/pie.py b/highcharts_core/options/plot_options/pie.py index 20f89b25..141724a0 100644 --- a/highcharts_core/options/plot_options/pie.py +++ b/highcharts_core/options/plot_options/pie.py @@ -521,6 +521,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -749,6 +750,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/polygon.py b/highcharts_core/options/plot_options/polygon.py index cf133d77..a89783a7 100644 --- a/highcharts_core/options/plot_options/polygon.py +++ b/highcharts_core/options/plot_options/polygon.py @@ -70,6 +70,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/scatter.py b/highcharts_core/options/plot_options/scatter.py index 0ac2962e..a30f57f6 100644 --- a/highcharts_core/options/plot_options/scatter.py +++ b/highcharts_core/options/plot_options/scatter.py @@ -79,6 +79,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/series.py b/highcharts_core/options/plot_options/series.py index a9252884..e1ad8c21 100644 --- a/highcharts_core/options/plot_options/series.py +++ b/highcharts_core/options/plot_options/series.py @@ -464,6 +464,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -848,6 +849,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/sonification.py b/highcharts_core/options/plot_options/sonification.py new file mode 100644 index 00000000..69506ba3 --- /dev/null +++ b/highcharts_core/options/plot_options/sonification.py @@ -0,0 +1,161 @@ +from typing import Optional, List + +from validator_collection import validators, checkers + +from highcharts_core.metaclasses import HighchartsMeta +from highcharts_core.decorators import class_sensitive, validate_types +from highcharts_core.options.sonification.track_configurations import (InstrumentTrackConfiguration, + SpeechTrackConfiguration, + ContextTrackConfiguration) +from highcharts_core.options.sonification.grouping import SonificationGrouping + + +class SeriesSonification(HighchartsMeta): + """Sonification/audio chart options for a series.""" + + def __init__(self, **kwargs): + self._context_tracks = None + self._default_instrument_options = None + self._default_speech_options = None + self._enabled = None + self._point_grouping = None + self._tracks = None + + self.context_tracks = kwargs.get('context_tracks', None) + self.default_instrument_options = kwargs.get('default_instrument_options', None) + self.default_speech_options = kwargs.get('default_speech_options', None) + self.enabled = kwargs.get('enabled', None) + self.point_grouping = kwargs.get('point_grouping', None) + self.tracks = kwargs.get('tracks', None) + + @property + def context_tracks(self) -> Optional[ContextTrackConfiguration | List[ContextTrackConfiguration]]: + """Context tracks for the series. Context tracks are not tied to data points. + + :rtype: :class:`ContextTrackConfiguration ` + or :class:`list ` of track configuration types + """ + return self._context_tracks + + @context_tracks.setter + def context_tracks(self, value): + if not value: + self._context_tracks = None + elif checkers.is_iterable(value, forbid_literals = (str, bytes, dict)): + self._context_tracks = [validate_types(x, types = (ContextTrackConfiguration)) for x in value] + else: + value = validate_types(value, types = ContextTrackConfiguration) + self._context_tracks = value + + @property + def default_instrument_options(self) -> Optional[InstrumentTrackConfiguration]: + """Default sonification options for all instrument tracks. + + .. warning:: + + If specific options are also set on individual tracks or per-series, this configuration will be *overridden*. + + :rtype: :class:`InstrumentTrackConfiguration ` + or :obj:`None ` + """ + return self._default_instrument_options + + @default_instrument_options.setter + @class_sensitive(InstrumentTrackConfiguration) + def default_instrument_options(self, value): + self._default_instrument_options = value + + @property + def default_speech_options(self) -> Optional[SpeechTrackConfiguration]: + """Default sonification options for all speech tracks. + .. warning:: + + If specific options are also set on individual tracks or per-series, this configuration will be *overridden*. + + :rtype: :class:`SpeechTrackConfiguration ` + or :obj:`None ` + """ + return self._default_speech_options + + @default_speech_options.setter + @class_sensitive(SpeechTrackConfiguration) + def default_speech_options(self, value): + self._default_speech_options = value + + @property + def enabled(self) -> Optional[bool]: + """If ``True``, sonification will be enabled for the series. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._enabled + + @enabled.setter + def enabled(self, value): + if value is None: + self._enabled = None + else: + self._enabled = bool(value) + + @property + def point_grouping(self) -> Optional[SonificationGrouping]: + """Options for grouping data points together when sonifying. + + This allows for the visual presentation to contain more points than what is being played. + + If not enabled, all visible / uncropped points are played. + + :rtype: :class:`SonificationGrouping ` or + :obj:`None ` + """ + return self._point_grouping + + @point_grouping.setter + @class_sensitive(SonificationGrouping) + def point_grouping(self, value): + self._point_grouping = value + + @property + def tracks(self) -> Optional[ContextTrackConfiguration | List[ContextTrackConfiguration]]: + """Tracks for the series. + + :rtype: :class:`ContextTrackConfiguration ` + or :class:`list ` of + :class:`ContextTrackConfiguration ` + or :obj:`None ` + """ + return self._tracks + + @tracks.setter + def tracks(self, value): + if not value: + self._tracks = None + elif checkers.is_iterable(value, forbid_literals = (str, bytes, dict)): + self._tracks = [validate_types(x, types = (ContextTrackConfiguration)) for x in value] + else: + self._tracks = validate_types(value, types = (ContextTrackConfiguration)) + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + 'context_tracks': as_dict.get('contextTracks', None), + 'default_instrument_options': as_dict.get('defaultInstrumentOptions', None), + 'default_speech_options': as_dict.get('defaultSpeechOptions', None), + 'enabled': as_dict.get('enabled', None), + 'point_grouping': as_dict.get('pointGrouping', None), + 'tracks': as_dict.get('tracks', None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls = None) -> dict: + untrimmed = { + 'contextTracks': self.context_tracks, + 'defaultInstrumentOptions': self.default_instrument_options, + 'defaultSpeechOptions': self.default_speech_options, + 'enabled': self.enabled, + 'pointGrouping': self.point_grouping, + 'tracks': self.tracks, + } + + return untrimmed diff --git a/highcharts_core/options/plot_options/sunburst.py b/highcharts_core/options/plot_options/sunburst.py index 024358bd..5bee4a53 100644 --- a/highcharts_core/options/plot_options/sunburst.py +++ b/highcharts_core/options/plot_options/sunburst.py @@ -456,6 +456,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/timeline.py b/highcharts_core/options/plot_options/timeline.py index 55e80d6d..2939a39e 100644 --- a/highcharts_core/options/plot_options/timeline.py +++ b/highcharts_core/options/plot_options/timeline.py @@ -309,6 +309,7 @@ class from a Highcharts Javascript-compatible :class:`dict ` object '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), diff --git a/highcharts_core/options/plot_options/treegraph.py b/highcharts_core/options/plot_options/treegraph.py index 2a6c80df..a3c775c9 100644 --- a/highcharts_core/options/plot_options/treegraph.py +++ b/highcharts_core/options/plot_options/treegraph.py @@ -649,6 +649,7 @@ class from a Highcharts Javascript-compatible :class:`dict ` object '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), 'tooltip': as_dict.get('tooltip', None), diff --git a/highcharts_core/options/plot_options/treemap.py b/highcharts_core/options/plot_options/treemap.py index 665020af..e7d91e95 100644 --- a/highcharts_core/options/plot_options/treemap.py +++ b/highcharts_core/options/plot_options/treemap.py @@ -833,6 +833,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/vector.py b/highcharts_core/options/plot_options/vector.py index 78c624b6..22d3bf40 100644 --- a/highcharts_core/options/plot_options/vector.py +++ b/highcharts_core/options/plot_options/vector.py @@ -136,6 +136,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/plot_options/venn.py b/highcharts_core/options/plot_options/venn.py index bea38045..b86f3a3f 100644 --- a/highcharts_core/options/plot_options/venn.py +++ b/highcharts_core/options/plot_options/venn.py @@ -309,6 +309,7 @@ class from a Highcharts Javascript-compatible :class:`dict ` object '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), diff --git a/highcharts_core/options/plot_options/wordcloud.py b/highcharts_core/options/plot_options/wordcloud.py index 04be5d98..d4aa987e 100644 --- a/highcharts_core/options/plot_options/wordcloud.py +++ b/highcharts_core/options/plot_options/wordcloud.py @@ -499,6 +499,7 @@ class from a Highcharts Javascript-compatible :class:`dict ` object '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), diff --git a/highcharts_core/options/series/arcdiagram.py b/highcharts_core/options/series/arcdiagram.py index f8751d66..77a350c3 100644 --- a/highcharts_core/options/series/arcdiagram.py +++ b/highcharts_core/options/series/arcdiagram.py @@ -141,6 +141,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/area.py b/highcharts_core/options/series/area.py index 5d0190e5..8f92adeb 100644 --- a/highcharts_core/options/series/area.py +++ b/highcharts_core/options/series/area.py @@ -123,6 +123,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/bar.py b/highcharts_core/options/series/bar.py index b47f108f..19eaaaf3 100644 --- a/highcharts_core/options/series/bar.py +++ b/highcharts_core/options/series/bar.py @@ -120,6 +120,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -239,6 +240,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -795,6 +797,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -998,6 +1001,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -1153,6 +1157,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/base.py b/highcharts_core/options/series/base.py index 05e9eb2b..a47ea555 100644 --- a/highcharts_core/options/series/base.py +++ b/highcharts_core/options/series/base.py @@ -241,6 +241,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/bellcurve.py b/highcharts_core/options/series/bellcurve.py index 1759486a..95ad05c3 100644 --- a/highcharts_core/options/series/bellcurve.py +++ b/highcharts_core/options/series/bellcurve.py @@ -98,6 +98,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/boxplot.py b/highcharts_core/options/series/boxplot.py index b7486bf1..b8d51791 100644 --- a/highcharts_core/options/series/boxplot.py +++ b/highcharts_core/options/series/boxplot.py @@ -144,6 +144,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/bubble.py b/highcharts_core/options/series/bubble.py index ce843962..742613a8 100644 --- a/highcharts_core/options/series/bubble.py +++ b/highcharts_core/options/series/bubble.py @@ -132,6 +132,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/bullet.py b/highcharts_core/options/series/bullet.py index fcbc086e..16e62156 100644 --- a/highcharts_core/options/series/bullet.py +++ b/highcharts_core/options/series/bullet.py @@ -132,6 +132,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/dependencywheel.py b/highcharts_core/options/series/dependencywheel.py index bc20c0c5..207f7285 100644 --- a/highcharts_core/options/series/dependencywheel.py +++ b/highcharts_core/options/series/dependencywheel.py @@ -100,6 +100,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/dumbbell.py b/highcharts_core/options/series/dumbbell.py index 12bdb069..5bd0c462 100644 --- a/highcharts_core/options/series/dumbbell.py +++ b/highcharts_core/options/series/dumbbell.py @@ -155,6 +155,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/funnel.py b/highcharts_core/options/series/funnel.py index fea58090..71e2fa2c 100644 --- a/highcharts_core/options/series/funnel.py +++ b/highcharts_core/options/series/funnel.py @@ -90,6 +90,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -268,6 +269,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/gauge.py b/highcharts_core/options/series/gauge.py index 7cad0899..e224ad98 100644 --- a/highcharts_core/options/series/gauge.py +++ b/highcharts_core/options/series/gauge.py @@ -86,6 +86,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -226,6 +227,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/heatmap.py b/highcharts_core/options/series/heatmap.py index a959225b..5b53ed65 100644 --- a/highcharts_core/options/series/heatmap.py +++ b/highcharts_core/options/series/heatmap.py @@ -134,6 +134,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -254,6 +255,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/histogram.py b/highcharts_core/options/series/histogram.py index 57ca1fab..a2ba08f2 100644 --- a/highcharts_core/options/series/histogram.py +++ b/highcharts_core/options/series/histogram.py @@ -98,6 +98,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/item.py b/highcharts_core/options/series/item.py index 2eee2026..cb9e2eac 100644 --- a/highcharts_core/options/series/item.py +++ b/highcharts_core/options/series/item.py @@ -107,6 +107,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/networkgraph.py b/highcharts_core/options/series/networkgraph.py index ebe2eaab..4ac92448 100644 --- a/highcharts_core/options/series/networkgraph.py +++ b/highcharts_core/options/series/networkgraph.py @@ -75,6 +75,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/organization.py b/highcharts_core/options/series/organization.py index 5e9e9ad0..c66d87c2 100644 --- a/highcharts_core/options/series/organization.py +++ b/highcharts_core/options/series/organization.py @@ -157,6 +157,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/packedbubble.py b/highcharts_core/options/series/packedbubble.py index 6230a25c..f87090bd 100644 --- a/highcharts_core/options/series/packedbubble.py +++ b/highcharts_core/options/series/packedbubble.py @@ -98,6 +98,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/pareto.py b/highcharts_core/options/series/pareto.py index d0235c8b..896633e6 100644 --- a/highcharts_core/options/series/pareto.py +++ b/highcharts_core/options/series/pareto.py @@ -98,6 +98,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/pictorial.py b/highcharts_core/options/series/pictorial.py index 07610190..d6592d8f 100644 --- a/highcharts_core/options/series/pictorial.py +++ b/highcharts_core/options/series/pictorial.py @@ -206,6 +206,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/pie.py b/highcharts_core/options/series/pie.py index 98f41ccf..46aa111c 100644 --- a/highcharts_core/options/series/pie.py +++ b/highcharts_core/options/series/pie.py @@ -96,6 +96,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), @@ -265,6 +266,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/polygon.py b/highcharts_core/options/series/polygon.py index d24dd75f..345d0e55 100644 --- a/highcharts_core/options/series/polygon.py +++ b/highcharts_core/options/series/polygon.py @@ -127,6 +127,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/pyramid.py b/highcharts_core/options/series/pyramid.py index 4f5c8cb5..a61ba3f1 100644 --- a/highcharts_core/options/series/pyramid.py +++ b/highcharts_core/options/series/pyramid.py @@ -121,6 +121,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/sankey.py b/highcharts_core/options/series/sankey.py index a7153e62..79454275 100644 --- a/highcharts_core/options/series/sankey.py +++ b/highcharts_core/options/series/sankey.py @@ -94,6 +94,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/scatter.py b/highcharts_core/options/series/scatter.py index 3976e1c3..5506a2bd 100644 --- a/highcharts_core/options/series/scatter.py +++ b/highcharts_core/options/series/scatter.py @@ -125,6 +125,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/spline.py b/highcharts_core/options/series/spline.py index 6fe34d3c..050b237c 100644 --- a/highcharts_core/options/series/spline.py +++ b/highcharts_core/options/series/spline.py @@ -125,6 +125,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/sunburst.py b/highcharts_core/options/series/sunburst.py index e113fb59..49e2641e 100644 --- a/highcharts_core/options/series/sunburst.py +++ b/highcharts_core/options/series/sunburst.py @@ -78,6 +78,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/timeline.py b/highcharts_core/options/series/timeline.py index d60ada90..65a2c116 100644 --- a/highcharts_core/options/series/timeline.py +++ b/highcharts_core/options/series/timeline.py @@ -91,6 +91,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/treegraph.py b/highcharts_core/options/series/treegraph.py index 9d76dba2..c7baad6b 100644 --- a/highcharts_core/options/series/treegraph.py +++ b/highcharts_core/options/series/treegraph.py @@ -91,6 +91,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), 'tooltip': as_dict.get('tooltip', None), diff --git a/highcharts_core/options/series/treemap.py b/highcharts_core/options/series/treemap.py index 8e4a19c2..a00f6f4b 100644 --- a/highcharts_core/options/series/treemap.py +++ b/highcharts_core/options/series/treemap.py @@ -77,6 +77,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/vector.py b/highcharts_core/options/series/vector.py index 37f219fc..ea2969d2 100644 --- a/highcharts_core/options/series/vector.py +++ b/highcharts_core/options/series/vector.py @@ -93,6 +93,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/venn.py b/highcharts_core/options/series/venn.py index c294c522..e0f25292 100644 --- a/highcharts_core/options/series/venn.py +++ b/highcharts_core/options/series/venn.py @@ -90,6 +90,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/options/series/wordcloud.py b/highcharts_core/options/series/wordcloud.py index e0a4f545..ce61ffc7 100644 --- a/highcharts_core/options/series/wordcloud.py +++ b/highcharts_core/options/series/wordcloud.py @@ -77,6 +77,7 @@ def _get_kwargs_from_dict(cls, as_dict): '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), diff --git a/highcharts_core/utility_classes/events.py b/highcharts_core/utility_classes/events.py index b45a3432..71b1e401 100644 --- a/highcharts_core/utility_classes/events.py +++ b/highcharts_core/utility_classes/events.py @@ -900,6 +900,11 @@ def after_simulation(self) -> Optional[CallbackFunction]: """ return self._after_simulation + @after_simulation.setter + @class_sensitive(CallbackFunction) + def after_simulation(self, value): + self._after_simulation = value + @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { diff --git a/tests/input_files/plot_options/sonification/01.js b/tests/input_files/plot_options/sonification/01.js new file mode 100644 index 00000000..87a687a7 --- /dev/null +++ b/tests/input_files/plot_options/sonification/01.js @@ -0,0 +1,440 @@ +{ + contextTracks: { + valueInterval: 5, + valueMapFunction: 'linear', + valueProp: 'y', + midiName: 'some-value', + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument' + }, + defaultInstrumentOptions: { + instrument: 'piano', + midiName: 'some-value', + roundToMusicalNotes: true, + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument' + }, + defaultSpeechOptions: { + language: 'en-US', + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'speech' + }, + enabled: true, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + tracks: { + instrument: 'piano', + midiName: 'some-value', + roundToMusicalNotes: true, + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument' + } +} \ No newline at end of file diff --git a/tests/input_files/plot_options/sonification/02.js b/tests/input_files/plot_options/sonification/02.js new file mode 100644 index 00000000..f4de6ae8 --- /dev/null +++ b/tests/input_files/plot_options/sonification/02.js @@ -0,0 +1,551 @@ +{ + contextTracks: [ + { + valueInterval: 5, + valueMapFunction: 'linear', + valueProp: 'y', + midiName: 'some-value', + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument' + }, + { + valueInterval: 5, + valueMapFunction: 'linear', + valueProp: 'y', + midiName: 'some-value', + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument' + } + ], + defaultInstrumentOptions: { + instrument: 'piano', + midiName: 'some-value', + roundToMusicalNotes: true, + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument' + }, + defaultSpeechOptions: { + language: 'en-US', + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'speech' + }, + enabled: true, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + tracks: { + instrument: 'piano', + midiName: 'some-value', + roundToMusicalNotes: true, + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument' + } +} \ No newline at end of file diff --git a/tests/input_files/plot_options/sonification/error-01.js b/tests/input_files/plot_options/sonification/error-01.js new file mode 100644 index 00000000..a028a437 --- /dev/null +++ b/tests/input_files/plot_options/sonification/error-01.js @@ -0,0 +1,333 @@ +{ + defaultInstrumentOptions: 'invalid value', + defaultSpeechOptions: { + language: 'en-US', + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'speech' + }, + enabled: true, + contextTracks: { + valueInterval: 5, + valueMapFunction: 'linear', + valueProp: 'y', + midiName: 'some-value', + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument' + }, + tracks: { + instrument: 'piano', + midiName: 'some-value', + roundToMusicalNotes: true, + activeWhen: { + crossingDown: 2, + crossingUp: 3, + max: 5, + min: 1, + prop: 'y' + }, + mapping: { + frequency: 3, + gapBetweenNotes: 5, + highpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + lowpass: { + frequency: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + resonance: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + noteDuration: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pan: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + pitch: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + playDelay: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + time: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + tremolo: { + depth: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + }, + speed: 0.5 + }, + volume: { + mapFunction: 'linear', + mapTo: 'y', + max: 12345, + min: 0, + within: 'series' + } + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + }, + showPlayMarker: true, + type: 'instrument' + }, + pointGrouping: { + algorithm: 'minmax', + enabled: true, + groupTimespan: 15, + prop: 'y' + } +} \ No newline at end of file diff --git a/tests/options/plot_options/test_sonification.py b/tests/options/plot_options/test_sonification.py new file mode 100644 index 00000000..b9081541 --- /dev/null +++ b/tests/options/plot_options/test_sonification.py @@ -0,0 +1,56 @@ +"""Tests for ``highcharts.responsive``.""" + +import pytest + +from json.decoder import JSONDecodeError + +from highcharts_core.options.plot_options.sonification import SeriesSonification as cls +from highcharts_core import errors +from tests.fixtures import input_files, check_input_file, to_camelCase, to_js_dict, \ + Class__init__, Class__to_untrimmed_dict, Class_from_dict, Class_to_dict, \ + Class_from_js_literal + +STANDARD_PARAMS = [ + ({}, None), +] + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test__init__(kwargs, error): + Class__init__(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test__to_untrimmed_dict(kwargs, error): + Class__to_untrimmed_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_from_dict(kwargs, error): + Class_from_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS) +def test_to_dict(kwargs, error): + Class_to_dict(cls, kwargs, error) + + +@pytest.mark.parametrize('filename, as_file, error', [ + ('plot_options/sonification/01.js', False, None), + ('plot_options/sonification/02.js', False, None), + + ('plot_options/sonification/error-01.js', False, (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError)), + + ('plot_options/sonification/01.js', True, None), + ('plot_options/sonification/02.js', True, None), + + ('plot_options/sonification/error-01.js', True, (errors.HighchartsValueError, + errors.HighchartsParseError, + JSONDecodeError, + TypeError)), +]) +def test_from_js_literal(input_files, filename, as_file, error): + Class_from_js_literal(cls, input_files, filename, as_file, error) From 915bf73eb99ad2399bf8181dcad914bb0dadfc10 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Apr 2023 21:42:19 -0400 Subject: [PATCH 23/24] Fixed Sonification documentation typos. --- docs/api/options/index.rst | 3 ++- docs/api/options/sonification/grouping.rst | 4 ++-- docs/api/options/sonification/index.rst | 10 +++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/api/options/index.rst b/docs/api/options/index.rst index 801aceb5..9581d6c8 100644 --- a/docs/api/options/index.rst +++ b/docs/api/options/index.rst @@ -28,7 +28,8 @@ pane plot_options/index responsive - series/index + series/color_index + sonification/index subtitle time title diff --git a/docs/api/options/sonification/grouping.rst b/docs/api/options/sonification/grouping.rst index f7f546ab..6c9429c0 100644 --- a/docs/api/options/sonification/grouping.rst +++ b/docs/api/options/sonification/grouping.rst @@ -12,10 +12,10 @@ .. module:: highcharts_core.options.sonification.grouping ******************************************************************************************************************** -class: :class:`PointGrouping ` +class: :class:`SonificationGrouping ` ******************************************************************************************************************** -.. autoclass:: PointGrouping +.. autoclass:: SonificationGrouping :members: :inherited-members: diff --git a/docs/api/options/sonification/index.rst b/docs/api/options/sonification/index.rst index dc3d92d3..f819f926 100644 --- a/docs/api/options/sonification/index.rst +++ b/docs/api/options/sonification/index.rst @@ -1,6 +1,6 @@ -############################################################## +####################################################################### :mod:`.options.sonification ` -############################################################## +####################################################################### .. contents:: Module Contents :local: @@ -18,9 +18,9 @@ .. module:: highcharts_core.options.sonification -**************************************************************************************** +******************************************************************************************************* class: :class:`SonificationOptions ` -**************************************************************************************** +******************************************************************************************************* .. autoclass:: SonificationOptions :members: @@ -39,7 +39,7 @@ Sub-components * - Module - Classes / Functions * - :mod:`.options.sonification.grouping ` - - :class:`PointGrouping ` + - :class:`SonificationGrouping ` * - :mod:`.options.sonification.mapping ` - :class:`SonificationMapping ` :class:`AudioParameter ` From 69dce9a3d779b1d53121ffa0021cf194a12bbf55 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Tue, 25 Apr 2023 22:09:13 -0400 Subject: [PATCH 24/24] Adjusted new version number. --- highcharts_core/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highcharts_core/__version__.py b/highcharts_core/__version__.py index afced147..1a72d32e 100644 --- a/highcharts_core/__version__.py +++ b/highcharts_core/__version__.py @@ -1 +1 @@ -__version__ = '2.0.0' +__version__ = '1.1.0'