"""Collection of utility functions used across the library."""
import csv
import datetime
import os
import string
import random
import typing
from collections import UserDict
from validator_collection import validators, checkers
try:
import numpy as np
HAS_NUMPY = True
except ImportError:
HAS_NUMPY = False
from highcharts_core import errors, constants
def get_random_string(length = 6):
"""Generate a short random alphanumeric string.
:param length: The length of the string to generate. Defaults to ``8``.
:type length: :class:`int <python:int>`
:returns: A random alphanumeric string of length ``length``.
:rtype: :class:`str <python:str>`
"""
length = validators.integer(length, minimum = 1)
result = ''.join(random.choices(string.ascii_uppercase + string.digits,
k = length))
return str(result)
[docs]def mro_to_dict(obj):
"""Work through ``obj``'s multiple parent classes, executing the appropriate
``to_dict()`` method for each parent and consolidaitng the results to a single
:class:`dict <python:dict>`.
:param obj: An object that has a ``to_dict()`` method.
:rtype: :class:`dict <python:dict>`
"""
if not hasattr(obj, 'to_dict'):
raise TypeError('obj does not have a to_dict() method.')
classes = [x for x in obj.__class__.mro()
if x.__name__ != 'object']
as_dict = {}
for item in classes:
has_to_dict = hasattr(super(item, obj), 'to_dict')
if not has_to_dict:
break
try:
item_dict = super(item, obj).to_dict()
except (NotImplementedError, AttributeError):
continue
for key in item_dict:
as_dict[key] = item_dict[key]
return as_dict
[docs]def get_remaining_mro(cls,
in_cls = None,
method = '_to_untrimmed_dict'):
"""Retrieve the remaining classes that should be processed for ``method`` when
traversing ``cls``.
:param cls: The class whose ancestors are being traversed.
:type cls: :class:`HighchartsMeta`
:param in_cls: The class that the traversal currently finds itself in. Defaults to
:obj:`None <python:None>`
:type in_cls: ``type`` or :obj:`None <python:None>`
:param method: The method to search for in the MRO. Defaults to
``'_to_untrimmed_dict'``.
:type method: :class:`str <python:str>`
:returns: List of classes that have ``method`` that occur *after* ``in_cls`` in
the MRO for ``cls``.
:rtype: :class:`list <python:list>` of ``type`` objects
"""
mro = [x for x in cls.mro()
if hasattr(x, method) and x.__name__ != 'HighchartsMeta']
if in_cls is None:
return mro[1:]
else:
index = mro.index(in_cls)
return mro[(index + 1):]
[docs]def mro__to_untrimmed_dict(obj, in_cls = None):
"""Traverse the ancestor classes of ``obj`` and execute their ``_to_untrimmed_dict()``
methods.
:param obj: The object to be traversed.
:type obj: :class:`HighchartsMeta`
:param in_cls: The class from which ``mro__to_untrimmed_dict()`` was called.
:type in_cls: ``type`` or :obj:`None <python:None>`
:returns: Collection of untrimmed :class:`dict <python:dict>` representations in the
same order as the MRO.
:rtype: :class:`list <python:list>` of :class:`dict <python:dict>`
for each class in the MRO, execute _to_untrimmed_dict()
do not repeat for each class
"""
cls = obj.__class__
remaining_mro = get_remaining_mro(cls,
in_cls = in_cls,
method = '_to_untrimmed_dict')
ancestor_dicts = []
for x in remaining_mro:
if hasattr(x, '_to_untrimmed_dict') and x != cls:
ancestor_dicts.append(x._to_untrimmed_dict(obj,
in_cls = x))
consolidated = {}
for item in ancestor_dicts:
for key in item:
consolidated[key] = item[key]
return consolidated
[docs]def validate_color(value):
"""Validate that ``value`` is either a :class:`Gradient`, :class:`Pattern`, or a
:class:`str <python:str>`.
:param value: The value to validate.
:returns: The validated value.
:rtype: :class:`str <python:str>`, :class:`Gradient`, :class:`Pattern``, or
:obj:`None <python:None>`
"""
from highcharts_core.utility_classes.gradients import Gradient
from highcharts_core.utility_classes.patterns import Pattern
if not value:
return None
elif value.__class__.__name__ == 'EnforcedNullType':
return value
elif isinstance(value, (Gradient, Pattern)):
return value
elif isinstance(value, (dict, str)) and ('linearGradient' in value or
'radialGradient' in value):
try:
value = Gradient.from_json(value)
except (TypeError, ValueError):
if isinstance(value, dict):
value = Gradient.from_dict(value)
else:
value = validators.string(value)
elif isinstance(value, dict) and ('linear_gradient' in value or
'radial_gradient' in value):
value = Gradient(**value)
elif isinstance(value, (dict, str)) and ('patternOptions' in value or
'pattern' in value):
try:
value = Pattern.from_json(value)
except (TypeError, ValueError):
if isinstance(value, dict):
value = Pattern.from_dict(value)
else:
value = validators.string(value)
elif isinstance(value, dict) and 'pattern_options' in value:
value = Pattern(**value)
elif isinstance(value, str):
value = validators.string(value)
else:
raise errors.HighchartsValueError(f'Unable to resolve value to a string, '
f'Gradient, or Pattern. Value received '
f'was: {value}')
return value
[docs]def to_camelCase(snake_case):
"""Convert ``snake_case`` to ``camelCase``.
:param snake_case: A :class:`str <python:str>` which is likely to contain
``snake_case``.
:type snake_case: :class:`str <python:str>`
:returns: A ``camelCase`` representation of ``snake_case``.
:rtype: :class:`str <python:str>`
"""
if not snake_case:
raise errors.HighchartsValueError(f'snake_case cannot be empty')
snake_case = str(snake_case)
if '_' not in snake_case:
return snake_case
if 'url' in snake_case:
snake_case = snake_case.replace('url', 'URL')
elif 'utc' in snake_case:
snake_case = snake_case.replace('utc', 'UTC')
elif '_csv' in snake_case:
snake_case = snake_case.replace('csv', 'CSV')
elif '_jpeg' in snake_case:
snake_case = snake_case.replace('jpeg', 'JPEG')
elif '_pdf' in snake_case:
snake_case = snake_case.replace('pdf', 'PDF')
elif '_png' in snake_case:
snake_case = snake_case.replace('png', 'PNG')
elif '_svg' in snake_case:
snake_case = snake_case.replace('svg', 'SVG')
elif '_xls' in snake_case:
snake_case = snake_case.replace('xls', 'XLS')
elif '_atr' in snake_case:
snake_case = snake_case.replace('atr', 'ATR')
elif '_hlc' in snake_case:
snake_case = snake_case.replace('hlc', 'HLC')
elif '_ohlc' in snake_case:
snake_case = snake_case.replace('ohlc', 'OHLC')
elif '_xy' in snake_case:
snake_case = snake_case.replace('xy', 'XY')
elif snake_case.endswith('_x'):
snake_case = snake_case.replace('_x', '_X')
elif snake_case.endswith('_y'):
snake_case = snake_case.replace('_y', '_Y')
elif snake_case.endswith('_id'):
snake_case = snake_case.replace('_id', '_ID')
elif snake_case == 'drillup_text':
snake_case = 'drillUpText'
elif snake_case == 'drillup_button':
snake_case = 'drillUpButton'
elif snake_case == 'thousands_separator':
snake_case = 'thousandsSep'
elif snake_case == 'measure_xy':
snake_case = 'measureXY'
elif snake_case == 'use_gpu_translations':
snake_case = 'useGPUTranslations'
elif snake_case == 'label_rank':
snake_case = 'labelrank'
elif '_di_line' in snake_case:
snake_case = snake_case.replace('_di_line', '_DILine')
camel_case = ''
previous_character = ''
for character in snake_case:
if character != '_' and previous_character != '_':
camel_case += character
previous_character = character
elif character == '_':
previous_character = character
elif character != '_' and previous_character == '_':
camel_case += character.upper()
previous_character = character
return camel_case
[docs]def to_snake_case(camel_case) -> str:
"""Convert ``camelCase`` to ``snake_case``.
:param camel_case: A :class:`str <python:str>` which is likely to contain
``camelCase``.
:type camel_case: :class:`str <python:str>`
:returns: A ``snake_case`` representation of ``camel_case``.
:rtype: :class:`str <python:str>`
"""
camel_case = validators.string(camel_case)
snake_case = ''
previous_character = ''
for character in camel_case:
if character.isupper() and not previous_character.isupper():
snake_case += f'_{character.lower()}'
elif character.isupper() and previous_character.isupper():
snake_case += character.lower()
elif character.isupper() and not previous_character:
snake_case += character.lower()
else:
snake_case += character
previous_character = character
return snake_case
[docs]def parse_csv(csv_data,
has_header_row = True,
delimiter = ',',
null_text = 'None',
wrapper_character = "'",
wrap_all_strings = False,
double_wrapper_character_when_nested = False,
escape_character = "\\",
line_terminator = '\r\n'):
"""Parse ``csv_data`` to return a list of :class:`dict <python:dict>` objects, one
for each record.
:param csv_data: The CSV record expressed as a :class:`str <python:str>`
:type csv_data: :class:`str <python:str>`
:param delimiter: The delimiter used between columns. Defaults to ``,``.
:type delimiter: :class:`str <python:str>`
:param wrapper_character: The string used to wrap string values when
wrapping is applied. Defaults to ``'``.
:type wrapper_character: :class:`str <python:str>`
:param null_text: The string used to indicate an empty value if empty
values are wrapped. Defaults to `None`.
:type null_text: :class:`str <python:str>`
:returns: Collection of column names (or numerical keys) and CSV records as
:class:`dict <python:dict>` values
:rtype: :class:`tuple <python:tuple>` of a :class:`list <python:list>` of column names
and :class:`list <python:list>` of :class:`dict <python:dict>`
"""
if not csv_data:
return [], []
if isinstance(csv_data, str):
csv_data = csv_data.split(line_terminator)
if not wrapper_character:
wrapper_character = "'"
if wrap_all_strings:
quoting = csv.QUOTE_NONNUMERIC
else:
quoting = csv.QUOTE_MINIMAL
if 'highcharts' in csv.list_dialects():
csv.unregister_dialect('highcharts')
csv.register_dialect('highcharts',
delimiter = delimiter,
doublequote = double_wrapper_character_when_nested,
escapechar = escape_character,
quotechar = wrapper_character,
quoting = quoting,
lineterminator = line_terminator)
if has_header_row:
csv_reader = csv.DictReader(csv_data,
dialect = 'highcharts',
restkey = None,
restval = None)
records_as_dicts = [x for x in csv_reader]
columns = csv_reader.fieldnames
else:
csv_reader = csv.reader(csv_data,
dialect = 'highcharts')
records_as_dicts = []
columns = []
for row in csv_reader:
record_as_dict = {}
column_counter = 0
for column in row:
record_as_dict[column_counter] = column
columns.append(column_counter)
column_counter += 1
records_as_dicts.append(record_as_dict)
return columns, records_as_dicts
def jupyter_add_script(url, is_last = False, use_require = False):
"""Generates the JavaScript code Promise which adds a <script/> tag to the Jupyter
Lab environment.
:param url: The URL to use for the script's source.
:type url: :class:`str <python:str>`
:param is_last: Whether the URL is the last of the promises.
:type is_last: :class:`bool <python:bool>`
:param use_require: Whether to return the script needed for RequireJS.
Defaults to ``False``.
:type use_require: :class:`bool <python:bool>`
:returns: The JavaScript code for adding the script.
:rtype: :class:`str <python:str>`
"""
url = validators.url(
url, allow_special_ips=os.getenv("HCP_ALLOW_SPECIAL_IPS", False)
)
if url.endswith('.css'):
return jupyter_add_link(url, is_last = is_last)
js_str = """"""
if use_require:
js_str += f"""require(['{url}'], function() """
js_str += """{\n"""
if is_last:
js_str += """});"""
else:
js_str += """new Promise(function(resolve, reject) {\n"""
js_str += f""" var existing_tags = document.querySelectorAll("script[src='{url}']");"""
js_str += """ if (existing_tags.length == 0) {
var script = document.createElement("script");
script.onload = resolve;
script.onerror = reject;"""
js_str += f""" script.src = '{url}';"""
js_str += """ document.head.appendChild(script);
} else { resolve() };
})"""
return js_str
def jupyter_add_link(url, is_last = False):
"""Generates the JavaScript code Promise which adds a <link/> tag to the Jupyter
Lab environment.
:param url: The URL to use for the link's source.
:type url: :class:`str <python:str>`
:param is_last: Whether the URL is the last of the promises.
:type is_last: :class:`bool <python:bool>`
:returns: The JavaScript code for adding the link.
:rtype: :class:`str <python:str>`
"""
url = validators.url(
url, allow_special_ips=os.getenv("HCP_ALLOW_SPECIAL_IPS", False)
)
js_str = ''
js_str += """new Promise(function(resolve, reject) {\n"""
js_str += f""" var existing_tags = document.querySelectorAll("link[href='{url}']");"""
js_str += """ if (existing_tags.length == 0) {
var link = document.createElement("link");
link.onload = resolve;
link.onerror = reject;"""
js_str += f""" link.href = '{url}';"""
js_str += f""" link.rel = 'stylesheet';"""
js_str += f""" link.type = 'text/css';"""
js_str += """ document.head.appendChild(link);
} else { resolve() };
})"""
return js_str
def get_retryHighcharts():
"""Retrieve the ``retryHighcharts()`` JavaScript function.
:returns: The JavaScript code of the ``retryHighcharts()`` JavaScript function.
:rtype: :class:`str <python:str>`
"""
js_str = """function retryHighcharts(fn, container = 'highcharts_target_div', retries = 5, retriesLeft = 5,
interval = 1000) {
return new Promise((resolve, reject) => {
try {
fn()
return resolve();
} catch (err) {
if ((err instanceof ReferenceError) || (err instanceof TypeError) || (err.message.includes('#17'))) {
if (retriesLeft === 0) {
var target_div = document.getElementById(container);
if (target_div) {
var timeElapsed = (retries * interval) / 1000;
var errorMessage = "Something went wrong with the Highcharts.js script. It should have been automatically loaded, but it did not load for over " + timeElapsed + " seconds. Check your internet connection, and then if the problem persists please reach out for support. (You can also check your browser's console log for more details.)<br/><br/>Detailed Error Message:<br/>" + err.message;
var errorHTML = errorMessage;
target_div.innerHTML = errorHTML;
console.log(errorMessage);
console.error(err);
}
return reject();
}
setTimeout(() => {
retryHighcharts(fn, container, retries, retriesLeft - 1, interval).then(resolve).catch(reject);
}, interval);
} else if ((err instanceof Error) && (err.message.includes('#13'))) {
var errorMessage = "It looks like the container specified \'" + container + "\' was not created successfully. Please check your browser\'s console log for more details.";
console.error(errorMessage);
console.error(err);
return reject();
} else {
throw err;
}
}
});
};"""
return js_str
def prep_js_for_jupyter(js_str,
container = 'highcharts_target_div',
random_slug = None,
retries = 5,
interval = 1000):
"""Remove the JavaScript event listeners from the code in ``js_str`` and prepare the
JavaScript code for rending in an IPython context.
:param js_str: The JavaScript code from which the event listeners should be stripped.
:type js_str: :class:`str <python:str>`
:param container: The DIV where the Highcharts visualization is to be rendered. Defaults to
``'highcharts_target_div'``.
:type container: :class:`str <python:str>`
:param random_slug: The random sequence of characters to append to the container/function name to ensure uniqueness.
Defaults to :obj:`None <python:None>`
:type random_slug: :class:`str <python:str>` or :obj:`None <python:None>`
:param retries: The number of times to retry the rendering. Defaults to 3.
:type retries: :class:`int <python:int>`
:param interval: The number of milliseconds to wait between retries. Defaults to 1000 (1 second).
:type interval: :class:`int <python:int>`
:returns: The JavaScript code having removed the non-Jupyter compliant JS code.
:rtype: :class:`str <python:str>`
"""
js_str = js_str.replace(
"""document.addEventListener('DOMContentLoaded', function() {""", '')
js_str = js_str.replace('renderTo = ', '')
js_str = js_str.replace(',\noptions = ', ',\n')
if '.setOptions(' not in js_str:
js_str = js_str[:-3]
if random_slug:
function_str = f"""function insertChart_{random_slug}() """
else:
function_str = """function insertChart() """
function_str += """{\n"""
function_str += js_str
function_str += """\n};\n"""
if random_slug:
function_str += f"""retryHighcharts(insertChart_{random_slug}, '{container}', {retries}, {retries}, {interval});"""
else:
function_str += f"""retryHighcharts(insertChart, '{container}', {retries}, {retries}, {interval});"""
return function_str
def wrap_for_requirejs(if_require_js, if_no_requirejs = None):
"""Wrap ``if_require_js`` in a conditional JavaScript ``if ... { }`` statement
that evalutes whether RequireJS is present in the browser.
:param if_require_js: The (JavaScript) code that should be executed if RequireJS
*is* present.
:type if_require_js: :class:`str <python:str>`
:param if_no_require_js: The (JavaScript) code that should be executed if RequireJS
is *not* present. Defaults to :obj:`None <python:None>` (nothing gets executed).
:type if_no_require_js: :class:`str <python:str>`
"""
js_str = """var has_requirejs = typeof requirejs !== 'undefined';\n"""
js_str += """if (has_requirejs) {\n"""
js_str += if_require_js + '\n}'
if if_no_requirejs:
js_str += """ else {\n"""
js_str += if_no_requirejs + '\n}'
js_str += ';'
return js_str
def to_ndarray(value):
"""Convert ``value`` to a :class:`numpy.ndarray <numpy:numpy.ndarray>`.
:param value: The value to be converted. Expects the value to be an iterable.
:type value: iterable
:raises HighchartsDependencyError: if NumPy is not installed
:returns: A :class:`numpy.ndarray <numpy:numpy.ndarray>` representation of
``value``.
:rtype: :class:`numpy.ndarray <numpy:numpy.ndarray>`
"""
if not HAS_NUMPY:
raise errors.HighchartsDependencyError('NumPy is required for this feature. '
'It was not found in the runtime environment. '
'Please install it using "pip install numpy" '
'or equivalent.')
for i, item in enumerate(value):
is_iterable = not isinstance(item,
(str, bytes, dict, UserDict)) and hasattr(item,
'__iter__')
if item is None or isinstance(item, constants.EnforcedNullType):
value[i] = np.nan
elif is_iterable:
for index, subitem in enumerate(item):
if subitem is None or isinstance(subitem, constants.EnforcedNullType):
item[i] = np.nan
value[i] = item
if hasattr(value, '__array__'):
as_array = np.array(value)
else:
as_array = np.asarray(value)
return as_array
def to_ndarray_dict(keys, as_iterable):
"""Convert ``as_iterable`` into a :class:`dict <python:dict>`
whose keys align to the values in ``keys``, and whose values
are :class:`numpy.ndarray <numpy:numpy.ndarray>` instances
corresponding to the index in ``as_iterable``.
:param keys: The collection of keys to use for the resulting
:class:`dict <python:dict>`.
:type keys: iterable of :class:`str <python:str>`
:param as_iterable: The collection of values to be converted
to :class:`numpy.ndarray <numpy:numpy.ndarray>` instances
:type as_iterable: iterable
:returns: A :class:`dict <python:dict>` whose keys are values
from ``keys``, and whose values are items from ``as_iterable``
with each item converted to a
:class:`numpy.ndarray <numpy:numpy.ndarray>`
:rtype: :class:`dict <python:dict>`
:raises HighchartsValueError: if ``keys`` and ``as_iterable``
have different lengths
"""
keys = validators.iterable(keys,
allow_empty = False,
forbid_literals = (str, bytes, dict, UserDict))
as_iterable = validators.iterable(as_iterable,
allow_empty = False,
forbid_literals = (str, bytes, dict, UserDict))
if len(keys) != len(as_iterable):
raise errors.HighchartsValueError(f'keys and as_iterable must have the same '
f'length. Received: {len(keys)} for keys,'
f'{len(as_iterable)} for as_iterable ')
as_dict = {}
for index, key in enumerate(keys):
as_dict[key] = to_ndarray(as_iterable[index])
return as_dict
def from_ndarray(as_ndarray, force_enforced_null = False):
"""Convert ``as_ndarray`` to a Python :class:`list <python:list>`.
:param as_ndarray: The :class:`numpy.ndarray <numpy:numpy.ndarray>`
to be converted.
:type as_ndarray: :class:`numpy.ndarray <numpy:numpy.ndarray>`
:param force_enforced_null: if ``True``, converts any
:class:`numpy.nan <numpy:numpy.nan>` values to :obj:`EnforcedNull`.
Otherwise, converts them to :obj:`None <python:None>`. Defaults to
``False``.
:type force_enforced_null: :class:`bool <python:bool>`
:raises HighchartsDependencyError: if NumPy is not installed
:raises HighchartsValueError: if ``as_ndarray`` is not a
:class:`numpy.ndarray <numpy:numpy.ndarray>`
:returns: The Python :class:`list <python:list>` representation of
``as_ndarray``.
:rtype: :class:`list <python:list>`
"""
if not HAS_NUMPY:
raise errors.HighchartsDependencyError('NumPy is required for this feature. '
'It was not found in the runtime environment. '
'Please install it using "pip install numpy" '
'or equivalent.')
if not isinstance(as_ndarray, np.ndarray):
raise errors.HighchartsValueError(f'as_ndarray is expected to be a NumPy ndarray. '
f'Received: {as_ndarray.__class__.__name__}')
if force_enforced_null:
nan_replacement = constants.EnforcedNull
else:
nan_replacement = None
if as_ndarray.dtype.char not in ['O', 'U', 'M']:
stripped = np.where(np.isnan(as_ndarray), nan_replacement, as_ndarray)
elif as_ndarray.dtype.char == 'M':
stripped = (as_ndarray.astype(np.int64) / 10**6).astype(np.int64)
else:
prelim_stripped = as_ndarray.tolist()
stripped = []
for item in prelim_stripped:
if item == np.nan:
stripped.append(nan_replacement)
else:
stripped.append(item)
return stripped
return stripped.tolist()
def get_ndarray_slice(array, index):
"""Return the slice of ``array`` at ``index``.
:param array: A `NumPy <https://numpy.org>`__ :class:`ndarray <numpy:numpy.ndarray>`
instance or a Python iterable.
:type array: :class:`numpy.ndarray <numpy:numpy.ndarray>` or iterable
:param index: The 0-based index of the column to return from ``array``.
.. note::
If ``index`` exceeds the number of dimensions in ``array``, then
an empty collection of values should be returned, with the number
of empty values matching the length of the ``array``.
:type index: :class:`int <python:int>`
:returns: A collection of values.
:rtype: :class:`numpy.ndarray <numpy:numpy.ndarray>`
or :class:`list <python:list>`
"""
index = validators.integer(index, minimum = 0, allow_none = False)
if HAS_NUMPY and isinstance(array, np.ndarray):
if index < array.shape[1]:
return array[:, index]
else:
len_array = array.shape[0]
return np.full((len_array, 1), np.nan)
else:
array = validators.iterable(array,
allow_empty = True,
forbid_literals = (str, bytes, dict, UserDict)) or []
return [x[index] for x in array]
def lengthen_array(value, members):
"""Create a NumPy :class:`ndarray <numpy:numpy.ndarray>` from
``value`` where the result has ``members``.
:param value: The array-like value to be inserted into the resulting array.
.. note::
If an :class:`int <python:int>` is supplied, the value will be repeated all
``members``.
:type value: Array-like or :class:`int <python:int>`
:param members: The number of members the resulting ``value`` expects.
:type members: :class:`int <python:int>`
:returns: A NumPy :class:`ndarray <numpy:numpy.ndarray>` of length ``members``.
:rtype: :class:`numpy.ndarray <numpy:numpy.ndarray>`
:raises HighchartsDependencyError: if NumPy is not available in the runtime
environment
:raises HighchartsValueError: if ``value`` has more members than ``members``
"""
if not HAS_NUMPY:
raise errors.HighchartsDependencyError('NumPy is required for this feature. '
'It was not found in your runtime '
'environment. Please make sure it is '
'installed in your runtime '
'environment.')
is_ndarray = isinstance(value, np.ndarray)
is_list = False
if not is_ndarray:
is_list = checkers.is_iterable(value,
forbid_literals = (str, bytes, dict, UserDict),
allow_empty = False)
is_int = False
if not is_ndarray and not is_list:
value = validators.integer(value, allow_empty = None)
is_int = True
if is_list:
value = np.asarray(value)
elif is_int:
value = np.full((members, 1), value)
if len(value) > members:
raise errors.HighchartsValueError(f'Value has more members than specified. '
f'Received: {len(value)}. Expected up to: '
f'{members}.')
elif len(value) < members:
members_to_add = members - len(value)
else:
members_to_add = 0
if members_to_add:
try:
value = np.vstack((value, np.full((members_to_add, value.shape[1]), np.nan)))
except IndexError:
value = np.vstack((value, np.full((members_to_add, value.ndim), np.nan)))
return value
def is_iterable(value) -> bool:
"""Evaluate whether ``value`` is iterable, with support for NumPy arrays.
:param value: The value to evaluate.
:type value: Any
:returns: ``True`` if iterable, ``False`` if not
:rtype: :class:`bool <python:bool>`
"""
return checkers.is_type(value, 'ndarray') or \
(not isinstance(value,
(str, bytes, dict, UserDict)) and hasattr(value, '__iter__'))
def is_arraylike(value) -> bool:
"""Evaluate whether ``value`` is a NumPy array or a Python iterable.
:param value: The value to evaluate.
:type value: Any
:raises HighchartsDependencyError: if NumPy is not available in the runtime
environment
:returns: ``True`` if an array or array-like. ``False`` if not.
:rtype: :class:`bool <python:bool>`
"""
if not HAS_NUMPY:
return is_iterable(value)
return isinstance(value, np.ndarray) or is_iterable(value)
def is_ndarray(value) -> bool:
"""Evaluate whether ``value`` is a NumPy :class:`ndarray <numpy:numpy.ndarray>`.
:param value: The value to evaluate.
:type value: Any
:returns: ``True`` if an array. ``False`` if not.
:rtype: :class:`bool <python:bool>`
"""
if value.__class__.__name__ == 'ndarray':
return True
classes = [x.__name__ for x in value.__class__.__mro__]
return 'ndarray' in classes
def extend_columns(array, needed_members):
"""Extends ``array`` with additional positions for the number
of members to equal ``needed_members``. Additional positions recieve
a value of :obj:`None <python:None>`.
:param array: The array to extend
:type array: iterable
:param needed_members: the number of members the array should contain
:type needed_members: :class:`int <python:int>`
:returns: ``array`` with ``needed_members``
:rtype: iterable
"""
if not is_arraylike(array):
raise errors.HighchartsValueError(f'array is expected to be an iterable. '
f'Received a: {array.__class__.__name__}')
needed_members = validators.integer(needed_members)
original_length = len(array)
if needed_members <= original_length:
return array
new_members = original_length - needed_members
array.extend([None for x in range(new_members)])
return array
def dict_to_ndarray(as_dict):
"""Convert ``as_dict`` to a :class:`numpy.ndarray <numpy:numpy.ndarray>`,
with each key becoming a column.
:param as_dict: :class:`dict <python:dict>` to be converted
:type as_dict: :class:`dict <python:dict>`
:returns: :class:`numpy.ndarray <numpy:numpy.ndarray>` with 1 column
per key in ``as_dict``
:rtype: :class:`numpy.ndarray <nunpy:numpy.ndarray>`
"""
if not HAS_NUMPY:
raise errors.HighchartsDependencyError('NumPy is required for this feature. '
'It was not found in your runtime '
'environment. Please make sure it is '
'installed in your runtime '
'environment.')
as_dict = validators.dict(as_dict, allow_empty = True) or {}
columns = [as_dict[key] for key in as_dict]
as_ndarray = np.column_stack(columns)
return as_ndarray
def datetime64_to_datetime(dt64):
"""Convert a NumPy :class:`datetime64 <numpy:numpy.datetime64>` to a Python
:class:`datetime <python:datetime.datetime>`.
:param dt64: The NumPy :class:`datetime64 <numpy:numpy.datetime64>` to convert.
:type dt64: :class:`numpy.datetime64 <numpy:numpy.datetime64>`
:returns: A Python :class:`datetime <python:datetime.datetime>` instance.
:rtype: :class:`datetime <python:datetime.datetime>`
:raises HighchartsDependencyError: if NumPy is not available in the runtime
environment
"""
if not HAS_NUMPY:
raise errors.HighchartsDependencyError('NumPy is required for this feature. '
'It was not found in your runtime '
'environment. Please make sure it is '
'installed in your runtime '
'environment.')
timestamp = (dt64 - np.datetime64("1970-01-01T00:00:00")) / np.timedelta64(1, "s")
return datetime.datetime.fromtimestamp(timestamp, datetime.timezone.utc)