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
[docs]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 <highcharts_core.options.sonification.track_configurations.ActiveWhen.prop>` is
at or *below* this value. Defaults to :obj:`None <python:None>`.
.. warning::
If both ``.crossing_down`` and
:meth:`.crossing_up <highcharts_core.options.sonification.track_configurations.ActiveWhen.crossing_up>` are
defined, the track will be active if either condition is met.
:rtype: numeric or :obj:`None <python: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 <highcharts_core.options.sonification.track_configurations.ActiveWhen.prop>` is
at or *above* this value. Defaults to :obj:`None <python:None>`.
.. warning::
If both ``.crossing_up`` and
:meth:`.crossing_down <highcharts_core.options.sonification.track_configurations.ActiveWhen.crossing_down>`
are defined, the track will be active if either condition is met.
:rtype: numeric or :obj:`None <python: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 <highcharts_core.options.sonification.track_configurations.ActiveWhen.prop>` is
at or *below* this value. Defaults to :obj:`None <python:None>`.
:rtype: numeric or :obj:`None <python: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 <highcharts_core.options.sonification.track_configurations.ActiveWhen.prop>` is
at or *above* this value. Defaults to :obj:`None <python:None>`.
:rtype: numeric or :obj:`None <python: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 <python:None>`.
:rtype: :class:`str <python:str>` or :obj:`None <python: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
[docs]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 <highcharts_core.utility_classes.javascript_functions.CallbackFunction>` or an
:class:`ActiveWhen <highcharts_core.options.sonification.track_configurations.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 <highcharts_core.options.sonification.track_configurations.ContextTrackConfiguration.value_interval>`
is used.
:rtype: :class:`ActiveWhen <highcharts_core.options.sonification.track_configurations.ActiveWhen>` or
:class:`CallbackFunction <highcharts_core.utility_classes.javascript_functions.CallbackFunction>` or
:obj:`None <python: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 <highcharts_core.options.sonification.mapping.SonificationMapping>` or
:obj:`None <python: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 <highcharts_core.options.sonification.grouping.SonificationGrouping>` or
:obj:`None <python: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 <python:bool>` or :obj:`None <python: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 <python:str>` or :obj:`None <python: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
[docs]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 <python: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 <python:None>`, will
use the series name if the track is related to a series. Defaults to :obj:`None <python:None>`.
:rtype: :class:`str <python:str>` or :obj:`None <python: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 <python:bool>` or :obj:`None <python: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 <python:None>`.
:rtype: :class:`str <python:str>` or :obj:`None <python: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
[docs]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 <https://www.rfc-editor.org/info/bcp47>`__
language tag. Defaults to ``'en-US'``.
:rtype: :class:`str <python:str>` or :obj:`None <python: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 <python:None>` or
unavabilable, will fall back to the default voice for the selected language. Defaults to
:obj:`None <python:None>`.
.. warning::
Different platforms (operating systems in which your users will view your visualizations)
provide different voices for web speech synthesis.
:rtype: :class:`str <python:str>` or :obj:`None <python: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
[docs]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 <python:None>`.
:rtype: numeric or :obj:`None <python: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 <highcharts_core.options.sonification.track_configurations.ContextTrackConfiguration.value_prop>`.
For example, setting
:meth:`.value_prop <highcharts_core.options.sonification.track_configurations.ContextTrackConfiguration.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 <python: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 <highcharts_core.options.sonification.track_configurations.ContextTrackConfiguration.value_interval>`
property. Accepts either ``'linear'`` or ``'logarithmic'``. Defaults to ``'linear'``.
:rtype: :class:`str <python:str>` or :obj:`None <python: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 <highcharts_core.options.sonification.track_configurations.ContextTrackConfiguration.value_interval>`
Defaults to :obj:`None <python:None>`.
:rtype: :class:`str <python:str>` or :obj:`None <python: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 <python:str>` or :obj:`None <python: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