From 2a223247597521cbe9d7b68e33d6c610ef2958fc Mon Sep 17 00:00:00 2001 From: Daniel Herman Date: Tue, 10 Mar 2026 15:43:48 -0400 Subject: [PATCH 01/23] Optimize requires_js_object to avoid expensive trim_dict serialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `requires_js_object` property on data points was calling `self.to_dict()` + `self.trim_dict()` — a full serialization round-trip — just to determine whether a point could be represented as a JS array (e.g. `[1, 2]`) or needed a JS object (e.g. `{x: 1, y: 2, color: "red"}`). For large data series this was extremely slow. With ~7,500 points, `display()` took ~17 seconds, with the vast majority spent inside `trim_dict` performing type checks via the `validator_collection` library. Each validator/checker call in that library is wrapped by a decorator (`disable_on_env` / `disable_checker_on_env`) that calls `os.getenv()` on every invocation to check whether it should be disabled — resulting in ~6 million `os.getenv()` calls per render. The upstream `validator_collection` library (https://github.com/insightindustry/validator-collection) has not been updated in several years, so rather than wait for a fix there, this change avoids triggering the expensive code path entirely. The fix replaces the `to_dict()` + `trim_dict()` approach with a direct inspection of `_to_untrimmed_dict()`, checking whether any non-array properties hold non-None, non-empty values. This preserves the same semantics while bypassing all validator overhead. Result: `display()` drops from ~17.3s to ~0.96s (18x speedup) for a chart with 7,500 data points. Co-Authored-By: Claude Opus 4.6 --- highcharts_core/options/series/data/base.py | 32 +-- .../series/data/test_requires_js_object.py | 217 ++++++++++++++++++ 2 files changed, 236 insertions(+), 13 deletions(-) create mode 100644 tests/options/series/data/test_requires_js_object.py diff --git a/highcharts_core/options/series/data/base.py b/highcharts_core/options/series/data/base.py index d742ea6..7e55808 100644 --- a/highcharts_core/options/series/data/base.py +++ b/highcharts_core/options/series/data/base.py @@ -363,25 +363,31 @@ def _get_props_from_array_helper(prop_list, length = None) -> List[str]: @property def requires_js_object(self) -> bool: - """Indicates whether or not the data point *must* be serialized to a JS literal + """Indicates whether or not the data point *must* be serialized to a JS literal object or whether it can be serialized to a primitive array. - + :returns: ``True`` if the data point *must* be serialized to a JS literal object. ``False`` if it can be serialized to an array. :rtype: :class:`bool ` """ - from_array_props = [utility_functions.to_camelCase(x) - for x in self._get_props_from_array()] - - as_dict = self.to_dict() - trimmed_dict = self.trim_dict(as_dict) - for prop in from_array_props: - if prop in trimmed_dict: - del trimmed_dict[prop] - - if trimmed_dict: + from_array_props = {utility_functions.to_camelCase(x) + for x in self._get_props_from_array()} + + untrimmed = self._to_untrimmed_dict() + for key, value in untrimmed.items(): + if key in from_array_props: + continue + if value is None: + continue + # Filter out empty objects whose trimmed dict would be empty + if hasattr(value, '_to_untrimmed_dict'): + inner = value._to_untrimmed_dict() + if not any(v is not None for v in inner.values()): + continue + elif hasattr(value, '__len__') and not isinstance(value, str) and len(value) == 0: + continue return True - + return False def populate_from_array(self, value): diff --git a/tests/options/series/data/test_requires_js_object.py b/tests/options/series/data/test_requires_js_object.py new file mode 100644 index 0000000..bb206d0 --- /dev/null +++ b/tests/options/series/data/test_requires_js_object.py @@ -0,0 +1,217 @@ +"""Tests for the requires_js_object property optimization. + +Verifies that the optimized requires_js_object implementation produces the same +results as the original to_dict()/trim_dict() approach across various data point +configurations. +""" + +import pytest + +from highcharts_core.options.series.data.cartesian import CartesianData +from highcharts_core.options.series.data.cartesian import Cartesian3DData +from highcharts_core.options.series.data.cartesian import CartesianValueData +from highcharts_core.options.series.data.bar import BarData +from highcharts_core.options.series.data.pie import PieData +from highcharts_core.options.series.data.range import RangeData +from highcharts_core.options.series.data.single_point import SinglePointData +from highcharts_core.utility_classes.markers import Marker +from highcharts_core.utility_classes.data_labels import DataLabel + + +class TestCartesianDataRequiresJSObject: + """Tests for CartesianData.requires_js_object.""" + + def test_xy_only_does_not_require_object(self): + """A simple (x, y) point should serialize as an array.""" + point = CartesianData(x=1, y=2) + assert point.requires_js_object is False + + def test_y_only_does_not_require_object(self): + """A point with only y should serialize as an array.""" + point = CartesianData(y=42) + assert point.requires_js_object is False + + def test_name_and_y_does_not_require_object(self): + """A point with name and y should serialize as an array (name is an array prop).""" + point = CartesianData(name='Category A', y=10) + assert point.requires_js_object is False + + def test_empty_point_does_not_require_object(self): + """An empty point with no properties set should serialize as an array.""" + point = CartesianData() + assert point.requires_js_object is False + + def test_with_id_requires_object(self): + """A point with id set requires JS object (id is not an array prop).""" + point = CartesianData(x=1, y=2, id='point-1') + assert point.requires_js_object is True + + def test_with_color_requires_object(self): + """A point with color set requires JS object.""" + point = CartesianData(x=1, y=2, color='#ff0000') + assert point.requires_js_object is True + + def test_with_class_name_requires_object(self): + """A point with class_name set requires JS object.""" + point = CartesianData(x=1, y=2, class_name='highlighted') + assert point.requires_js_object is True + + def test_with_description_requires_object(self): + """A point with description set requires JS object.""" + point = CartesianData(x=1, y=2, description='An important point') + assert point.requires_js_object is True + + def test_with_selected_requires_object(self): + """A point with selected=True requires JS object.""" + point = CartesianData(x=1, y=2, selected=True) + assert point.requires_js_object is True + + def test_with_drilldown_requires_object(self): + """A point with drilldown set requires JS object.""" + point = CartesianData(x=1, y=2, drilldown='details') + assert point.requires_js_object is True + + def test_with_data_labels_requires_object(self): + """A point with non-empty data_labels requires JS object.""" + point = CartesianData(x=1, y=2, data_labels={'enabled': True}) + assert point.requires_js_object is True + + def test_with_marker_requires_object(self): + """A point with non-empty marker requires JS object.""" + point = CartesianData(x=1, y=2, marker={'enabled': True, 'radius': 5}) + assert point.requires_js_object is True + + def test_with_empty_marker_does_not_require_object(self): + """A point with an empty Marker() should not require JS object. + + This is the key edge case: Marker() has all None fields, so trim_dict + would filter it out. The optimized code must handle this too. + """ + point = CartesianData(x=1, y=2) + point._marker = Marker() + assert point.requires_js_object is False + + def test_with_empty_data_label_does_not_require_object(self): + """A point with an empty DataLabel() should not require JS object.""" + point = CartesianData(x=1, y=2) + point._data_labels = DataLabel() + assert point.requires_js_object is False + + def test_with_color_index_requires_object(self): + """A point with color_index set requires JS object.""" + point = CartesianData(x=1, y=2, color_index=3) + assert point.requires_js_object is True + + def test_with_custom_requires_object(self): + """A point with custom data requires JS object.""" + point = CartesianData(x=1, y=2, custom={'key': 'value'}) + assert point.requires_js_object is True + + def test_with_empty_custom_does_not_require_object(self): + """A point with an empty custom dict should not require JS object.""" + point = CartesianData(x=1, y=2) + point._custom = {} + assert point.requires_js_object is False + + +class TestToArrayConsistency: + """Verify that to_array() output is consistent with requires_js_object.""" + + def test_array_form_for_simple_point(self): + """A simple point should produce array output.""" + point = CartesianData(x=1, y=2) + result = point.to_array() + assert isinstance(result, list) + assert result == [1, 2] + + def test_dict_form_for_complex_point(self): + """A point with extra props should produce dict output.""" + point = CartesianData(x=1, y=2, color='#ff0000') + result = point.to_array() + assert isinstance(result, dict) + + def test_array_form_y_only(self): + """A y-only point should produce a single-element array.""" + point = CartesianData(y=5) + result = point.to_array() + assert isinstance(result, list) + assert result == [5] + + def test_array_form_name_and_y(self): + """A name+y point should produce [name, y] array.""" + point = CartesianData(name='A', y=10) + result = point.to_array() + assert isinstance(result, list) + assert result == ['A', 10] + + +class TestOtherDataTypes: + """Verify requires_js_object works correctly across data point types.""" + + def test_cartesian3d_xyz_only(self): + """3D point with only x,y,z should not require object.""" + point = Cartesian3DData(x=1, y=2, z=3) + assert point.requires_js_object is False + + def test_cartesian3d_with_color(self): + """3D point with color requires object.""" + point = Cartesian3DData(x=1, y=2, z=3, color='#ff0000') + assert point.requires_js_object is True + + def test_cartesian_value_xy_only(self): + """CartesianValueData with only x,y should not require object.""" + point = CartesianValueData(x=1, y=2) + assert point.requires_js_object is False + + def test_bar_data_xy_only(self): + """BarData with only x,y should not require object.""" + point = BarData(x=1, y=2) + assert point.requires_js_object is False + + def test_bar_data_with_color(self): + """BarData with color requires object.""" + point = BarData(x=1, y=2, color='#ff0000') + assert point.requires_js_object is True + + def test_range_data_simple(self): + """RangeData with only low,high should not require object.""" + point = RangeData(low=1, high=5) + assert point.requires_js_object is False + + def test_range_data_with_color(self): + """RangeData with color requires object.""" + point = RangeData(low=1, high=5, color='#ff0000') + assert point.requires_js_object is True + + def test_pie_data_name_y(self): + """PieData with name,y should not require object (both are array props).""" + point = PieData(name='Slice A', y=30) + assert point.requires_js_object is False + + def test_single_point_value_only(self): + """SinglePointData with only value should not require object.""" + point = SinglePointData(y=42) + assert point.requires_js_object is False + + +class TestBulkSerialization: + """Test that bulk serialization produces consistent results.""" + + def test_many_simple_points_all_array(self): + """All simple points should serialize as arrays.""" + points = [CartesianData(x=i, y=i * 2) for i in range(100)] + for point in points: + assert point.requires_js_object is False + result = point.to_array() + assert isinstance(result, list) + + def test_mixed_points(self): + """Mix of simple and complex points should be handled correctly.""" + simple = CartesianData(x=1, y=2) + complex_pt = CartesianData(x=1, y=2, color='red') + + assert simple.requires_js_object is False + assert complex_pt.requires_js_object is True + + assert isinstance(simple.to_array(), list) + assert isinstance(complex_pt.to_array(), dict) From 23725bdbdc0055a45cca733f3aa7df11c5a59465 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 20:05:44 -0400 Subject: [PATCH 02/23] Updated requirements and dependencies --- .gitignore | 5 ++++- pyproject.toml | 4 ++-- requirements.dev.numpy.txt | 4 ++-- requirements.dev.txt | 4 ++-- requirements.txt | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index a0cf18b..7de50eb 100644 --- a/.gitignore +++ b/.gitignore @@ -134,4 +134,7 @@ dmypy.json tests/input_files/headless_export/output/ # VSCode Settings -.vscode/ \ No newline at end of file +.vscode/ + +# test_stbox +.test_stbox/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2c1b230..f6d66a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ requires-python = ">= 3.10" dependencies = [ "esprima>=4.0.1", "validator-collection>=1.5.0", - "requests>=2.32.0" + "requests>=2.32.4" ] [tool.hatch.version] @@ -74,7 +74,7 @@ path = "highcharts_core/__version__.py" [project.optional-dependencies] dev = [ - "pytest>=7.0.2", + "pytest>=9.0.3", "pytest-cov>=3.0.0", "pytest-xdist>=2.5.0", "python-dotenv>=1.0.0", diff --git a/requirements.dev.numpy.txt b/requirements.dev.numpy.txt index f223333..7734dc9 100644 --- a/requirements.dev.numpy.txt +++ b/requirements.dev.numpy.txt @@ -1,5 +1,5 @@ esprima==4.0.1 -pytest==7.1.2 +pytest>=9.0.3 pytest-cov==3.0.0 pytest-xdist==2.5.0 python-dotenv>=0.20.0 @@ -9,7 +9,7 @@ sphinx-rtd-theme==1.2.0 sphinx-toolbox>=3.6.0 sphinx-tabs==3.4.1 tox==4.4.6 -requests==2.32.0 +requests>=2.32.4 validator-collection==1.5.0 anthropic==0.3.11 dill==0.3.7 diff --git a/requirements.dev.txt b/requirements.dev.txt index 0a46f6f..176d33a 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,5 +1,5 @@ esprima==4.0.1 -pytest==7.1.2 +pytest>=9.0.3 pytest-cov==3.0.0 pytest-xdist==2.5.0 python-dotenv>=0.20.0 @@ -9,7 +9,7 @@ sphinx-rtd-theme==1.2.0 sphinx-toolbox>=3.6.0 sphinx-tabs==3.4.1 tox==4.4.6 -requests==2.32.0 +requests>=2.32.4 validator-collection==1.5.0 anthropic==0.3.11 dill==0.3.7 diff --git a/requirements.txt b/requirements.txt index c3cef37..e6bceb8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ esprima==4.0.1 -requests==2.32.0 +requests>=2.32.4 validator-collection==1.5.0 From 50d1944c114b39170e550b23dcdd4479cc3e34ed Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 20:26:13 -0400 Subject: [PATCH 03/23] Updated docs and test for chart.module_url to support local path. --- highcharts_core/chart.py | 4 +- tests/test_chart.py | 781 ++++++++++++++++++++------------------- 2 files changed, 411 insertions(+), 374 deletions(-) diff --git a/highcharts_core/chart.py b/highcharts_core/chart.py index 507ae15..5fe8fb3 100644 --- a/highcharts_core/chart.py +++ b/highcharts_core/chart.py @@ -452,7 +452,7 @@ def callback(self, value): @property def module_url(self) -> str: - """The URL from which Highcharts modules should be downloaded when + """The URL or local path from which Highcharts modules should be downloaded when generating the ``', - '', - '' - ], None), - ("""{ + False, + [ + '', + '', + '', + ], + None, + ), + ( + """{ "chart": { "type": "column" }, @@ -275,16 +299,20 @@ def test_get_required_modules(json_str, expected_modules, error): } }] }""", - True, - """\n\n""", None), -]) + True, + """\n\n""", + None, + ), + ], +) def test_get_script_tags(options_str, as_str, expected, error): from highcharts_core.options import HighchartsOptions + options = HighchartsOptions.from_json(options_str) chart = cls.from_options(options) if not error: - result = chart.get_script_tags(as_str = as_str) + result = chart.get_script_tags(as_str=as_str) if isinstance(expected, list): assert isinstance(result, list) is True assert len(result) == len(expected) @@ -296,114 +324,112 @@ def test_get_script_tags(options_str, as_str, expected, error): assert result is None or len(result) == 0 else: with pytest.raises(error): - result = chart.get_script_tags(as_str = as_str) - + result = chart.get_script_tags(as_str=as_str) -@pytest.mark.parametrize('kwargs, error', [ - ({}, None), - ({ - 'container': 'my-container-name', - 'module_url': 'https://mycustomurl.com/', - 'options': { - 'title': { - 'text': 'My Chart' - } - } - }, None), -]) + +@pytest.mark.parametrize( + "kwargs, error", + [ + ({}, None), + ( + { + "container": "my-container-name", + "module_url": "https://mycustomurl.com/", + "options": {"title": {"text": "My Chart"}}, + }, + None, + ), + ], +) def test__repr__(kwargs, error): obj = cls(**kwargs) if not error: result = repr(obj) - if 'options' in kwargs: - assert 'options = ' in result + if "options" in kwargs: + assert "options = " in result else: with pytest.raises(error): result = repr(obj) -@pytest.mark.parametrize('kwargs, error', [ - ({}, None), - ({ - 'container': 'my-container-name', - 'module_url': 'https://mycustomurl.com/', - 'options': { - 'title': { - 'text': 'My Chart' - } - } - }, None), -]) +@pytest.mark.parametrize( + "kwargs, error", + [ + ({}, None), + ( + { + "container": "my-container-name", + "module_url": "https://mycustomurl.com/", + "options": {"title": {"text": "My Chart"}}, + }, + None, + ), + ], +) def test__str__(kwargs, error): obj = cls(**kwargs) if not error: result = str(obj) print(result) - if 'options' in kwargs: - assert 'options = ' in result + if "options" in kwargs: + assert "options = " in result else: with pytest.raises(error): result = str(obj) - -@pytest.mark.parametrize('kwargs, expected_series, expected_data_points, error', [ - ({}, 0, [], None), - ({ - 'series': [ +@pytest.mark.parametrize( + "kwargs, expected_series, expected_data_points, error", + [ + ({}, 0, [], None), + ({"series": [{"data": [[1, 2], [3, 4]], "type": "line"}]}, 1, [(0, 2)], None), + ({"series": {"data": [[1, 2], [3, 4]], "type": "line"}}, 1, [(0, 2)], None), + ({"data": [[1, 2], [3, 4]], "series_type": "line"}, 1, [(0, 2)], None), + ( { - 'data': [[1, 2], [3, 4]], - 'type': 'line' - } - ] - }, 1, [(0, 2)], None), - ({ - 'series': { - 'data': [[1, 2], [3, 4]], - 'type': 'line' - } - }, 1, [(0, 2)], None), - - ({ - 'data': [[1, 2], [3, 4]], - 'series_type': 'line' - }, 1, [(0, 2)], None), - - ({ - 'data': [[1, 2], [3, 4]], - }, 1, [(0, 2)], errors.HighchartsValueError), - -]) -def test_issue90_one_shot_creation(kwargs, expected_series, expected_data_points, error): + "data": [[1, 2], [3, 4]], + }, + 1, + [(0, 2)], + errors.HighchartsValueError, + ), + ], +) +def test_issue90_one_shot_creation( + kwargs, expected_series, expected_data_points, error +): if not error: result = cls(**kwargs) assert result is not None if kwargs: - assert getattr(result, 'options') is not None - assert getattr(result.options, 'series') is not None + assert getattr(result, "options") is not None + assert getattr(result.options, "series") is not None assert len(result.options.series) == expected_series for item in expected_data_points: assert len(result.options.series[item[0]].data) == item[1] else: with pytest.raises(error): result = cls(**kwargs) - -@pytest.mark.parametrize('filename, error', [ - ('test-data-files/nst-est2019-01.csv', None), -]) + +@pytest.mark.parametrize( + "filename, error", + [ + ("test-data-files/nst-est2019-01.csv", None), + ], +) def test_from_pandas_in_rows(run_pandas_tests, input_files, filename, error): if not run_pandas_tests: return import pandas - + input_file = check_input_file(input_files, filename) - df = pandas.read_csv(input_file, header = 0, thousands = ',') - df.index = df['Geographic Area'] - df = df.drop(columns = ['Geographic Area']) + df = pandas.read_csv(input_file, header=0, thousands=",") + df.index = df["Geographic Area"] + df = df.drop(columns=["Geographic Area"]) print(df) - + if not error: result = cls.from_pandas_in_rows(df) assert result is not None @@ -418,108 +444,92 @@ def test_from_pandas_in_rows(run_pandas_tests, input_files, filename, error): def prep_df(df): - df.index = df['Geographic Area'] - df = df.drop(columns = ['Geographic Area']) - + df.index = df["Geographic Area"] + df = df.drop(columns=["Geographic Area"]) + return df def reduce_to_two_columns(df): - df = df[['Geographic Area', '2010']] - + df = df[["Geographic Area", "2010"]] + return df -@pytest.mark.parametrize('filename, kwargs, pre_test_df_func, expected_series, expected_data_points, error', [ - # SCENARIO 0: Series in Rows - ('test-data-files/nst-est2019-01.csv', - { - 'series_in_rows': True - }, - prep_df, - 57, - 10, - None), - - # SCENARIO 1a: Has Property Map, Single Series - ('test-data-files/nst-est2019-01.csv', - { - 'property_map': { - 'name': 'Geographic Area', - }, - 'series_in_rows': False - }, - None, - 1, - 57, - None), - - # SCENARIO 1b: Has Property Map, Multiple Series - ('test-data-files/nst-est2019-01.csv', - { - 'property_map': { - 'x': ['Geographic Area', '2010'] - }, - 'series_in_rows': False - }, - None, - 2, - 57, - None), - - # SCENARIO 2a: Single Property in KWARGS - ('test-data-files/nst-est2019-01.csv', - { - 'x': 'Geographic Area', - 'y': '2010' - }, - None, - 1, - 57, - None), - - # SCENARIO 3a: Exact Match on Column Count - ('test-data-files/nst-est2019-01.csv', - {}, - reduce_to_two_columns, - 1, - 57, - None), - - # SCENARIO 3b: Multiple Series, Multipled Columns - ('test-data-files/nst-est2019-01.csv', - {}, - prep_df, - 10, - 57, - None), - - # SCENARIO 4: Mismatched Columns - # NOTE: On SeriesBase, this will actually return one series per column. - # This is because SeriesBase supports 1D arrays. - ('test-data-files/nst-est2019-01.csv', - {}, - None, - 11, - 57, - TypeError), - -]) -def test_from_pandas(run_pandas_tests, - input_files, - filename, - kwargs, - pre_test_df_func, - expected_series, - expected_data_points, - error): +@pytest.mark.parametrize( + "filename, kwargs, pre_test_df_func, expected_series, expected_data_points, error", + [ + # SCENARIO 0: Series in Rows + ( + "test-data-files/nst-est2019-01.csv", + {"series_in_rows": True}, + prep_df, + 57, + 10, + None, + ), + # SCENARIO 1a: Has Property Map, Single Series + ( + "test-data-files/nst-est2019-01.csv", + { + "property_map": { + "name": "Geographic Area", + }, + "series_in_rows": False, + }, + None, + 1, + 57, + None, + ), + # SCENARIO 1b: Has Property Map, Multiple Series + ( + "test-data-files/nst-est2019-01.csv", + { + "property_map": {"x": ["Geographic Area", "2010"]}, + "series_in_rows": False, + }, + None, + 2, + 57, + None, + ), + # SCENARIO 2a: Single Property in KWARGS + ( + "test-data-files/nst-est2019-01.csv", + {"x": "Geographic Area", "y": "2010"}, + None, + 1, + 57, + None, + ), + # SCENARIO 3a: Exact Match on Column Count + ("test-data-files/nst-est2019-01.csv", {}, reduce_to_two_columns, 1, 57, None), + # SCENARIO 3b: Multiple Series, Multipled Columns + ("test-data-files/nst-est2019-01.csv", {}, prep_df, 10, 57, None), + # SCENARIO 4: Mismatched Columns + # NOTE: On SeriesBase, this will actually return one series per column. + # This is because SeriesBase supports 1D arrays. + ("test-data-files/nst-est2019-01.csv", {}, None, 11, 57, TypeError), + ], +) +def test_from_pandas( + run_pandas_tests, + input_files, + filename, + kwargs, + pre_test_df_func, + expected_series, + expected_data_points, + error, +): if not run_pandas_tests: return import pandas input_file = check_input_file(input_files, filename) - df = pandas.read_csv(input_file, header = 0, thousands = ',') + df = pandas.read_csv(input_file, header=0, thousands=",") if pre_test_df_func: df = pre_test_df_func(df) print(df) @@ -537,14 +547,18 @@ def test_from_pandas(run_pandas_tests, result = cls.from_pandas(df, **kwargs) -@pytest.mark.parametrize('filename, expected_series, expected_data_points, error', [ - ('test-data-files/nst-est2019-01.csv', 57, 10, None), -]) -def test_from_csv_in_rows(input_files, filename, expected_series, expected_data_points, error): +@pytest.mark.parametrize( + "filename, expected_series, expected_data_points, error", + [ + ("test-data-files/nst-est2019-01.csv", 57, 10, None), + ], +) +def test_from_csv_in_rows( + input_files, filename, expected_series, expected_data_points, error +): input_file = check_input_file(input_files, filename) if not error: - result = cls.from_csv_in_rows(input_file, - wrapper_character = '"') + result = cls.from_csv_in_rows(input_file, wrapper_character='"') assert result is not None assert isinstance(result, cls) assert result.options is not None @@ -559,129 +573,113 @@ def test_from_csv_in_rows(input_files, filename, expected_series, expected_data_ result = cls.from_pandas_in_rows(input_file) -@pytest.mark.parametrize('filename, property_map, kwargs, expected_series, expected_data_points, error', [ - ('test-data-files/nst-est2019-01.csv', - {}, - { - 'wrapper_character': '"' - }, - 10, - 57, - None), - ('test-data-files/nst-est2019-01.csv', - { - 'name': 'Geographic Area', - 'x': 'Geographic Area', - 'y': '2010' - }, - { - 'wrapper_character': '"' - }, - 1, - 57, - None), - - # SCENARIO 0: Series in Rows - ('test-data-files/nst-est2019-01.csv', - {}, - { - 'wrapper_character': '"', - 'series_in_rows': True - }, - 57, - 10, - None), - - # SCENARIO 1a: Has Property Map, Single Series - ('test-data-files/nst-est2019-01.csv', - { - 'name': 'Geographic Area' - }, - { - 'wrapper_character': '"', - 'series_in_rows': False - }, - 1, - 57, - None), - - ('test-data-files/nst-est2019-01.csv', - { - 'x': 'Geographic Area', - 'y': '2010' - }, - { - 'wrapper_character': '"' - }, - 1, - 57, - None), - - # SCENARIO 1b: Has Property Map, Multiple Series - ('test-data-files/nst-est2019-01.csv', - { - 'x': ['Geographic Area', '2010'] - }, - { - 'series_in_rows': False, - 'wrapper_character': '"' - }, - 2, - 57, - None), - - # SCENARIO 2a: Single Property in KWARGS - ('test-data-files/nst-est2019-01.csv', - {}, - { - 'wrapper_character': '"', - 'x': 'Geographic Area', - 'y': '2010' - }, - 1, - 57, - None), - - # SCENARIO 3a: Exact Match on Column Count - ('test-data-files/nst-est2019-01-reduced-to-two.csv', - {}, - { - 'wrapper_character': '"' - }, - 1, - 57, - None), - - # SCENARIO 3b: Multiple Series, Multipled Columns - ('test-data-files/nst-est2019-01-removed-column.csv', - {}, - { - 'wrapper_character': '"' - }, - 9, - 57, - None), - - # SCENARIO 4: Mismatched Columns - # NOTE: On SeriesBase, this will actually return one series per column. - # This is because SeriesBase supports 1D arrays. - ('test-data-files/nst-est2019-01.csv', - {}, - { - 'wrapper_character': '"' - }, - 10, - 57, - None), - -]) -def test_from_csv(input_files, filename, property_map, kwargs, expected_series, expected_data_points, error): +@pytest.mark.parametrize( + "filename, property_map, kwargs, expected_series, expected_data_points, error", + [ + ( + "test-data-files/nst-est2019-01.csv", + {}, + {"wrapper_character": '"'}, + 10, + 57, + None, + ), + ( + "test-data-files/nst-est2019-01.csv", + {"name": "Geographic Area", "x": "Geographic Area", "y": "2010"}, + {"wrapper_character": '"'}, + 1, + 57, + None, + ), + # SCENARIO 0: Series in Rows + ( + "test-data-files/nst-est2019-01.csv", + {}, + {"wrapper_character": '"', "series_in_rows": True}, + 57, + 10, + None, + ), + # SCENARIO 1a: Has Property Map, Single Series + ( + "test-data-files/nst-est2019-01.csv", + {"name": "Geographic Area"}, + {"wrapper_character": '"', "series_in_rows": False}, + 1, + 57, + None, + ), + ( + "test-data-files/nst-est2019-01.csv", + {"x": "Geographic Area", "y": "2010"}, + {"wrapper_character": '"'}, + 1, + 57, + None, + ), + # SCENARIO 1b: Has Property Map, Multiple Series + ( + "test-data-files/nst-est2019-01.csv", + {"x": ["Geographic Area", "2010"]}, + {"series_in_rows": False, "wrapper_character": '"'}, + 2, + 57, + None, + ), + # SCENARIO 2a: Single Property in KWARGS + ( + "test-data-files/nst-est2019-01.csv", + {}, + {"wrapper_character": '"', "x": "Geographic Area", "y": "2010"}, + 1, + 57, + None, + ), + # SCENARIO 3a: Exact Match on Column Count + ( + "test-data-files/nst-est2019-01-reduced-to-two.csv", + {}, + {"wrapper_character": '"'}, + 1, + 57, + None, + ), + # SCENARIO 3b: Multiple Series, Multipled Columns + ( + "test-data-files/nst-est2019-01-removed-column.csv", + {}, + {"wrapper_character": '"'}, + 9, + 57, + None, + ), + # SCENARIO 4: Mismatched Columns + # NOTE: On SeriesBase, this will actually return one series per column. + # This is because SeriesBase supports 1D arrays. + ( + "test-data-files/nst-est2019-01.csv", + {}, + {"wrapper_character": '"'}, + 10, + 57, + None, + ), + ], +) +def test_from_csv( + input_files, + filename, + property_map, + kwargs, + expected_series, + expected_data_points, + error, +): input_file = check_input_file(input_files, filename) - + if not error: - result = cls.from_csv(input_file, - property_column_map = property_map, - **kwargs) + result = cls.from_csv(input_file, property_column_map=property_map, **kwargs) assert result is not None assert isinstance(result, cls) assert result.options is not None @@ -693,48 +691,64 @@ def test_from_csv(input_files, filename, property_map, kwargs, expected_series, assert len(series.data) == expected_data_points else: with pytest.raises(error): - result = cls.from_csv(input_file, - property_column_map = property_map, - **kwargs) - - -@pytest.mark.parametrize('value, expected_shape, has_ndarray, has_data_points, error', [ - (np.asarray([ - [0.0, 15.0], - [10.0, -50.0], - [20.0, -56.5], - [30.0, -46.5], - [40.0, -22.1], - [50.0, -2.5], - [60.0, -27.7], - [70.0, -55.7], - [80.0, -76.5] - ]) if HAS_NUMPY else [ - [0.0, 15.0], - [10.0, -50.0], - [20.0, -56.5], - [30.0, -46.5], - [40.0, -22.1], - [50.0, -2.5], - [60.0, -27.7], - [70.0, -55.7], - [80.0, -76.5] - ], (9, 2), True, False, None), - ([ - { - 'id': 'some-value' - }, - { - 'id': 'some other value' - }, - ], (2, 2), False, True, None), - - ('Not an Array', None, True, False, ValueError), -]) + result = cls.from_csv( + input_file, property_column_map=property_map, **kwargs + ) + + +@pytest.mark.parametrize( + "value, expected_shape, has_ndarray, has_data_points, error", + [ + ( + np.asarray( + [ + [0.0, 15.0], + [10.0, -50.0], + [20.0, -56.5], + [30.0, -46.5], + [40.0, -22.1], + [50.0, -2.5], + [60.0, -27.7], + [70.0, -55.7], + [80.0, -76.5], + ] + ) + if HAS_NUMPY + else [ + [0.0, 15.0], + [10.0, -50.0], + [20.0, -56.5], + [30.0, -46.5], + [40.0, -22.1], + [50.0, -2.5], + [60.0, -27.7], + [70.0, -55.7], + [80.0, -76.5], + ], + (9, 2), + True, + False, + None, + ), + ( + [ + {"id": "some-value"}, + {"id": "some other value"}, + ], + (2, 2), + False, + True, + None, + ), + ("Not an Array", None, True, False, ValueError), + ], +) def test_from_array(value, expected_shape, has_ndarray, has_data_points, error): if has_ndarray is False and has_data_points is False: - raise AssertionError('Test is invalid. has_ndarray or has_data_points must be ' - 'True. Both were supplied as False.') + raise AssertionError( + "Test is invalid. has_ndarray or has_data_points must be " + "True. Both were supplied as False." + ) if not error: result = cls.from_array(value) assert result is not None @@ -762,3 +776,26 @@ def test_from_array(value, expected_shape, has_ndarray, has_data_points, error): else: with pytest.raises(error): result = cls.from_array(value) + + +@pytest.mark.parametrize( + "module_url, error", + [ + (None, None), + ("https://mycustomurl.com/", None), + ("../some/relative/path", None), + ], +) +def test_chart_module_url(module_url, error): + if not error: + result = cls(module_url=module_url) + assert result is not None + if module_url is not None: + assert result.module_url == module_url + if module_url != "https://code.highcharts.com/": + assert result.module_url != "https://code.highcharts.com/" + else: + assert result.module_url == "https://code.highcharts.com/" + else: + with pytest.raises(error): + result = cls(module_url=module_url) From 6fc1508453d41ddff7c7311cb0c6f0fe7493ff20 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 20:32:12 -0400 Subject: [PATCH 04/23] Added Tooltip.fixed support --- highcharts_core/options/tooltips.py | 369 +++++++++++++++------------- 1 file changed, 195 insertions(+), 174 deletions(-) diff --git a/highcharts_core/options/tooltips.py b/highcharts_core/options/tooltips.py index 89ee410..1a02f2d 100644 --- a/highcharts_core/options/tooltips.py +++ b/highcharts_core/options/tooltips.py @@ -28,6 +28,7 @@ def __init__(self, **kwargs): self._date_time_label_formats = None self._distance = None self._enabled = None + self._fixed = None self._follow_pointer = None self._follow_touch_move = None self._footer_format = None @@ -56,51 +57,52 @@ def __init__(self, **kwargs): self._value_suffix = None self._x_date_format = None - self.animation = kwargs.get('animation', None) - self.background_color = kwargs.get('background_color', 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.class_name = kwargs.get('class_name', None) - self.cluster_format = kwargs.get('cluster_format', None) - self.date_time_label_formats = kwargs.get('date_time_label_formats', None) - self.distance = kwargs.get('distance', None) - self.enabled = kwargs.get('enabled', None) - self.follow_pointer = kwargs.get('follow_pointer', None) - self.follow_touch_move = kwargs.get('follow_touch_move', None) - self.footer_format = kwargs.get('footer_format', None) - self.format = kwargs.get('format', None) - self.formatter = kwargs.get('formatter', None) - self.header_format = kwargs.get('header_format', None) - self.header_shape = kwargs.get('header_shape', None) - self.hide_delay = kwargs.get('hide_delay', None) - self.null_format = kwargs.get('null_format', None) - self.null_formatter = kwargs.get('null_formatter', None) - self.outside = kwargs.get('outside', None) - self.padding = kwargs.get('padding', None) - self.point_format = kwargs.get('point_format', None) - self.point_formatter = kwargs.get('point_formatter', None) - self.positioner = kwargs.get('positioner', None) - self.shadow = kwargs.get('shadow', None) - self.shape = kwargs.get('shape', None) - self.shared = kwargs.get('shared', None) - self.snap = kwargs.get('snap', None) - self.split = kwargs.get('split', None) - self.stick_on_contact = kwargs.get('stick_on_contact', None) - self.style = kwargs.get('style', None) - self.use_html = kwargs.get('use_html', None) - self.value_decimals = kwargs.get('value_decimals', None) - self.value_prefix = kwargs.get('value_prefix', None) - self.value_suffix = kwargs.get('value_suffix', None) - self.x_date_format = kwargs.get('x_date_format', None) + self.animation = kwargs.get("animation", None) + self.background_color = kwargs.get("background_color", 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.class_name = kwargs.get("class_name", None) + self.cluster_format = kwargs.get("cluster_format", None) + self.date_time_label_formats = kwargs.get("date_time_label_formats", None) + self.distance = kwargs.get("distance", None) + self.enabled = kwargs.get("enabled", None) + self.fixed = kwargs.get("fixed", None) + self.follow_pointer = kwargs.get("follow_pointer", None) + self.follow_touch_move = kwargs.get("follow_touch_move", None) + self.footer_format = kwargs.get("footer_format", None) + self.format = kwargs.get("format", None) + self.formatter = kwargs.get("formatter", None) + self.header_format = kwargs.get("header_format", None) + self.header_shape = kwargs.get("header_shape", None) + self.hide_delay = kwargs.get("hide_delay", None) + self.null_format = kwargs.get("null_format", None) + self.null_formatter = kwargs.get("null_formatter", None) + self.outside = kwargs.get("outside", None) + self.padding = kwargs.get("padding", None) + self.point_format = kwargs.get("point_format", None) + self.point_formatter = kwargs.get("point_formatter", None) + self.positioner = kwargs.get("positioner", None) + self.shadow = kwargs.get("shadow", None) + self.shape = kwargs.get("shape", None) + self.shared = kwargs.get("shared", None) + self.snap = kwargs.get("snap", None) + self.split = kwargs.get("split", None) + self.stick_on_contact = kwargs.get("stick_on_contact", None) + self.style = kwargs.get("style", None) + self.use_html = kwargs.get("use_html", None) + self.value_decimals = kwargs.get("value_decimals", None) + self.value_prefix = kwargs.get("value_prefix", None) + self.value_suffix = kwargs.get("value_suffix", None) + self.x_date_format = kwargs.get("x_date_format", None) @property def _dot_path(self) -> Optional[str]: """The dot-notation path to the options key for the current class. - + :rtype: :class:`str ` or :obj:`None ` """ - return 'tooltip' + return "tooltip" @property def animation(self) -> Optional[bool]: @@ -133,6 +135,7 @@ def background_color(self) -> Optional[str | Gradient | Pattern]: @background_color.setter def background_color(self, value): from highcharts_core import utility_functions + self._background_color = utility_functions.validate_color(value) @property @@ -149,6 +152,7 @@ def border_color(self) -> Optional[str | Gradient | Pattern]: @border_color.setter def border_color(self, value): from highcharts_core import utility_functions + self._border_color = utility_functions.validate_color(value) @property @@ -163,13 +167,13 @@ def border_radius(self) -> Optional[int | float | Decimal | str]: @border_radius.setter def border_radius(self, value): - if value is None or value == '': + if value is None or value == "": self._border_radius = None else: try: - self._border_radius = validators.string(value, allow_empty = True) + self._border_radius = validators.string(value, allow_empty=True) except (TypeError, ValueError): - self._border_radius = validators.numeric(value, allow_empty = True) + self._border_radius = validators.numeric(value, allow_empty=True) @property def border_width(self) -> Optional[int | float | Decimal]: @@ -183,7 +187,7 @@ def border_width(self) -> Optional[int | float | Decimal]: @border_width.setter def border_width(self, value): - self._border_width = validators.numeric(value, allow_empty = True) + self._border_width = validators.numeric(value, allow_empty=True) @property def class_name(self) -> Optional[str]: @@ -196,7 +200,7 @@ def class_name(self) -> Optional[str]: @class_name.setter def class_name(self, value): - self._class_name = validators.string(value, allow_empty = True) + self._class_name = validators.string(value, allow_empty=True) @property def cluster_format(self) -> Optional[str]: @@ -218,7 +222,7 @@ def cluster_format(self) -> Optional[str]: @cluster_format.setter def cluster_format(self, value): - self._cluster_format = validators.string(value, allow_empty = True) + self._cluster_format = validators.string(value, allow_empty=True) @property def date_time_label_formats(self) -> Optional[DateTimeLabelFormats]: @@ -249,7 +253,7 @@ def distance(self) -> Optional[int | float | Decimal]: @distance.setter def distance(self, value): - self._distance = validators.numeric(value, allow_empty = True) + self._distance = validators.numeric(value, allow_empty=True) @property def enabled(self) -> Optional[bool]: @@ -266,6 +270,29 @@ def enabled(self, value): else: self._enabled = bool(value) + @property + def fixed(self) -> Optional[bool]: + """If ``True``, indicates that the tooltip should be fixed to one position in + the chart. If ``False``, the tooltip should be positioned next to the point or + mouse. + + .. note:: + + When ``True``, the specific position of the tooltip can be further specified + using the :meth:`Tooltip.position` property. + + :rtype: :class:`bool ` or :obj:`None ` + + """ + return self._fixed + + @fixed.setter + def fixed(self, value): + if value is None: + self._fixed = None + else: + self._fixed = bool(value) + @property def follow_pointer(self) -> Optional[bool]: """If ``True``, the tooltip will follow the mouse pointer as it moves across @@ -331,10 +358,10 @@ def footer_format(self) -> Optional[str]: @footer_format.setter def footer_format(self, value): - if value == '': + if value == "": self._footer_format = value else: - self._footer_format = validators.string(value, allow_empty = True) + self._footer_format = validators.string(value, allow_empty=True) @property def format(self) -> Optional[str]: @@ -346,21 +373,21 @@ def format(self) -> Optional[str]: :meth:`.header_format `, :meth:`.point_format `, and :meth:`.footer_format `. - + However, the ``.format`` option allows combining them into one setting. - + .. note:: - + The context of the format string is the same as that of the :meth:`.formatter ` callback. - - :rtype: :class:`str ` + + :rtype: :class:`str ` """ return self._format - + @format.setter def format(self, value): - self._format = validators.string(value, allow_empty = True) + self._format = validators.string(value, allow_empty=True) @property def formatter(self) -> Optional[CallbackFunction]: @@ -438,10 +465,10 @@ def header_format(self) -> Optional[str]: @header_format.setter def header_format(self, value): - if value == '': + if value == "": self._header_format = value else: - self._header_format = validators.string(value, allow_empty = True) + self._header_format = validators.string(value, allow_empty=True) @property def header_shape(self) -> Optional[str]: @@ -464,9 +491,10 @@ def header_shape(self, value): else: value = validators.string(value) value = value.lower() - if value not in ['callout', 'circle', 'square']: - raise errors.HighchartsValueError(f'shape expects a supported tooltip ' - f'header shape. Was: {value}') + if value not in ["callout", "circle", "square"]: + raise errors.HighchartsValueError( + f"shape expects a supported tooltip header shape. Was: {value}" + ) self._header_shape = value @property @@ -482,9 +510,7 @@ def hide_delay(self) -> Optional[int]: @hide_delay.setter def hide_delay(self, value): - self._hide_delay = validators.integer(value, - allow_empty = True, - minimum = 0) + self._hide_delay = validators.integer(value, allow_empty=True, minimum=0) @property def null_format(self) -> Optional[str]: @@ -501,7 +527,7 @@ def null_format(self) -> Optional[str]: @null_format.setter def null_format(self, value): - self._null_format = validators.string(value, allow_empty = True) + self._null_format = validators.string(value, allow_empty=True) @property def null_formatter(self) -> Optional[CallbackFunction]: @@ -565,7 +591,7 @@ def padding(self) -> Optional[int | float | Decimal]: @padding.setter def padding(self, value): - self._padding = validators.numeric(value, allow_empty = True) + self._padding = validators.numeric(value, allow_empty=True) @property def point_format(self) -> Optional[str]: @@ -588,7 +614,7 @@ def point_format(self) -> Optional[str]: @point_format.setter def point_format(self, value): - self._point_format = validators.string(value, allow_empty = True) + self._point_format = validators.string(value, allow_empty=True) @property def point_formatter(self) -> Optional[CallbackFunction]: @@ -649,8 +675,7 @@ def shadow(self, value): elif isinstance(value, bool) and value is False: self._shadow = False else: - value = validate_types(value, - types = ShadowOptions) + value = validate_types(value, types=ShadowOptions) self._shadow = value @property @@ -679,13 +704,12 @@ def shape(self, value): if not value: self._shape = None else: - value = validators.string(value, allow_empty = False) + value = validators.string(value, allow_empty=False) value = value.lower() - if value not in ['callout', - 'rect', - 'circle']: - raise errors.HighchartsValueError(f'shape expects a supported tooltip ' - f'shape. Was: {value}') + if value not in ["callout", "rect", "circle"]: + raise errors.HighchartsValueError( + f"shape expects a supported tooltip shape. Was: {value}" + ) self._shape = value @property @@ -734,9 +758,7 @@ def snap(self) -> Optional[int | float | Decimal]: @snap.setter def snap(self, value): - self._snap = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._snap = validators.numeric(value, allow_empty=True, minimum=0) @property def split(self) -> Optional[bool]: @@ -799,11 +821,9 @@ def style(self) -> Optional[str | dict]: @style.setter def style(self, value): try: - self._style = validators.dict(value, allow_empty = True) + self._style = validators.dict(value, allow_empty=True) except (ValueError, TypeError): - self._style = validators.string(value, - allow_empty = True, - coerce_value = True) + self._style = validators.string(value, allow_empty=True, coerce_value=True) @property def use_html(self) -> Optional[bool]: @@ -845,7 +865,7 @@ def value_decimals(self) -> Optional[int]: @value_decimals.setter def value_decimals(self, value): - self._value_decimals = validators.integer(value, allow_empty = True) + self._value_decimals = validators.integer(value, allow_empty=True) @property def value_prefix(self) -> Optional[str]: @@ -858,7 +878,7 @@ def value_prefix(self) -> Optional[str]: @value_prefix.setter def value_prefix(self, value): - self._value_prefix = validators.string(value, allow_empty = True) + self._value_prefix = validators.string(value, allow_empty=True) @property def value_suffix(self) -> Optional[str]: @@ -871,7 +891,7 @@ def value_suffix(self) -> Optional[str]: @value_suffix.setter def value_suffix(self, value): - self._value_suffix = validators.string(value, allow_empty = True) + self._value_suffix = validators.string(value, allow_empty=True) @property def x_date_format(self) -> Optional[str]: @@ -885,91 +905,93 @@ def x_date_format(self) -> Optional[str]: @x_date_format.setter def x_date_format(self, value): - self._x_date_format = validators.string(value, allow_empty = True) + self._x_date_format = validators.string(value, allow_empty=True) @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - '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), - 'cluster_format': as_dict.get('clusterFormat', None), - 'date_time_label_formats': as_dict.get('dateTimeLabelFormats', None), - 'distance': as_dict.get('distance', None), - 'enabled': as_dict.get('enabled', None), - 'follow_pointer': as_dict.get('followPointer', None), - 'follow_touch_move': as_dict.get('followTouchMove', None), - 'footer_format': as_dict.get('footerFormat', None), - 'format': as_dict.get('format', None), - 'formatter': as_dict.get('formatter', None), - 'header_format': as_dict.get('headerFormat', None), - 'header_shape': as_dict.get('headerShape', None), - 'hide_delay': as_dict.get('hideDelay', None), - 'null_format': as_dict.get('nullFormat', None), - 'null_formatter': as_dict.get('nullFormatter', None), - 'outside': as_dict.get('outside', None), - 'padding': as_dict.get('padding', None), - 'point_format': as_dict.get('pointFormat', None), - 'point_formatter': as_dict.get('pointFormatter', None), - 'positioner': as_dict.get('positioner', None), - 'shadow': as_dict.get('shadow', None), - 'shape': as_dict.get('shape', None), - 'shared': as_dict.get('shared', None), - 'snap': as_dict.get('snap', None), - 'split': as_dict.get('split', None), - 'stick_on_contact': as_dict.get('stickOnContact', None), - 'style': as_dict.get('style', None), - 'use_html': as_dict.get('useHTML', None), - 'value_decimals': as_dict.get('valueDecimals', None), - 'value_prefix': as_dict.get('valuePrefix', None), - 'value_suffix': as_dict.get('valueSuffix', None), - 'x_date_format': as_dict.get('xDateFormat', 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), + "cluster_format": as_dict.get("clusterFormat", None), + "date_time_label_formats": as_dict.get("dateTimeLabelFormats", None), + "distance": as_dict.get("distance", None), + "enabled": as_dict.get("enabled", None), + "fixed": as_dict.get("fixed", None), + "follow_pointer": as_dict.get("followPointer", None), + "follow_touch_move": as_dict.get("followTouchMove", None), + "footer_format": as_dict.get("footerFormat", None), + "format": as_dict.get("format", None), + "formatter": as_dict.get("formatter", None), + "header_format": as_dict.get("headerFormat", None), + "header_shape": as_dict.get("headerShape", None), + "hide_delay": as_dict.get("hideDelay", None), + "null_format": as_dict.get("nullFormat", None), + "null_formatter": as_dict.get("nullFormatter", None), + "outside": as_dict.get("outside", None), + "padding": as_dict.get("padding", None), + "point_format": as_dict.get("pointFormat", None), + "point_formatter": as_dict.get("pointFormatter", None), + "positioner": as_dict.get("positioner", None), + "shadow": as_dict.get("shadow", None), + "shape": as_dict.get("shape", None), + "shared": as_dict.get("shared", None), + "snap": as_dict.get("snap", None), + "split": as_dict.get("split", None), + "stick_on_contact": as_dict.get("stickOnContact", None), + "style": as_dict.get("style", None), + "use_html": as_dict.get("useHTML", None), + "value_decimals": as_dict.get("valueDecimals", None), + "value_prefix": as_dict.get("valuePrefix", None), + "value_suffix": as_dict.get("valueSuffix", None), + "x_date_format": as_dict.get("xDateFormat", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'animation': self.animation, - 'backgroundColor': self.background_color, - 'borderColor': self.border_color, - 'borderRadius': self.border_radius, - 'borderWidth': self.border_width, - 'className': self.class_name, - 'clusterFormat': self.cluster_format, - 'dateTimeLabelFormats': self.date_time_label_formats, - 'distance': self.distance, - 'enabled': self.enabled, - 'followPointer': self.follow_pointer, - 'followTouchMove': self.follow_touch_move, - 'footerFormat': self.footer_format, - 'format': self.format, - 'formatter': self.formatter, - 'headerFormat': self.header_format, - 'headerShape': self.header_shape, - 'hideDelay': self.hide_delay, - 'nullFormat': self.null_format, - 'nullFormatter': self.null_formatter, - 'outside': self.outside, - 'padding': self.padding, - 'pointFormat': self.point_format, - 'pointFormatter': self.point_formatter, - 'positioner': self.positioner, - 'shadow': self.shadow, - 'shape': self.shape, - 'shared': self.shared, - 'snap': self.snap, - 'split': self.split, - 'stickOnContact': self.stick_on_contact, - 'style': self.style, - 'useHTML': self.use_html, - 'valueDecimals': self.value_decimals, - 'valuePrefix': self.value_prefix, - 'valueSuffix': self.value_suffix, - 'xDateFormat': self.x_date_format + "animation": self.animation, + "backgroundColor": self.background_color, + "borderColor": self.border_color, + "borderRadius": self.border_radius, + "borderWidth": self.border_width, + "className": self.class_name, + "clusterFormat": self.cluster_format, + "dateTimeLabelFormats": self.date_time_label_formats, + "distance": self.distance, + "enabled": self.enabled, + "fixed": self.fixed, + "followPointer": self.follow_pointer, + "followTouchMove": self.follow_touch_move, + "footerFormat": self.footer_format, + "format": self.format, + "formatter": self.formatter, + "headerFormat": self.header_format, + "headerShape": self.header_shape, + "hideDelay": self.hide_delay, + "nullFormat": self.null_format, + "nullFormatter": self.null_formatter, + "outside": self.outside, + "padding": self.padding, + "pointFormat": self.point_format, + "pointFormatter": self.point_formatter, + "positioner": self.positioner, + "shadow": self.shadow, + "shape": self.shape, + "shared": self.shared, + "snap": self.snap, + "split": self.split, + "stickOnContact": self.stick_on_contact, + "style": self.style, + "useHTML": self.use_html, + "valueDecimals": self.value_decimals, + "valuePrefix": self.value_prefix, + "valueSuffix": self.value_suffix, + "xDateFormat": self.x_date_format, } return untrimmed @@ -977,27 +999,27 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: class DiagramTooltip(Tooltip): """Options for tooltips in diagram series, like :class:`DependencyWheelSeries ` or :class:`SankeySeries `.""" - + def __init__(self, **kwargs): self._node_format = None self._node_formatter = None - - self.node_format = kwargs.get('node_format', None) - self.node_formatter = kwargs.get('node_formatter', None) - + + self.node_format = kwargs.get("node_format", None) + self.node_formatter = kwargs.get("node_formatter", None) + super().__init__(**kwargs) - + @property def node_format(self) -> Optional[str]: """The format string specifying what to show for nodes in the tooltip of a diagram series, as opposed to links. - + :rtype: :class:`str ` or :obj:`None ` """ return self._node_format - + @node_format.setter def node_format(self, value): - self._node_format = validators.string(value, allow_empty = True) + self._node_format = validators.string(value, allow_empty=True) @property def node_formatter(self) -> Optional[CallbackFunction]: @@ -1052,17 +1074,16 @@ def _get_kwargs_from_dict(cls, as_dict): "value_prefix": as_dict.get("valuePrefix", None), "value_suffix": as_dict.get("valueSuffix", None), "x_date_format": as_dict.get("xDateFormat", None), - - "node_format": as_dict.get('nodeFormat', None), - "node_formatter": as_dict.get('nodeFormatter', None) + "node_format": as_dict.get("nodeFormat", None), + "node_formatter": as_dict.get("nodeFormatter", None), } return kwargs def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'nodeFormat': self.node_format, - 'nodeFormatter': self.node_formatter, + "nodeFormat": self.node_format, + "nodeFormatter": self.node_formatter, } parent_as_dict = super()._to_untrimmed_dict(in_cls=in_cls) or {} From b5fe391a0db89bdba53b76fe46a1733896211e7b Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 20:45:11 -0400 Subject: [PATCH 05/23] Added Tooltip.position property. --- highcharts_core/options/tooltips.py | 94 +++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/highcharts_core/options/tooltips.py b/highcharts_core/options/tooltips.py index 1a02f2d..aa38103 100644 --- a/highcharts_core/options/tooltips.py +++ b/highcharts_core/options/tooltips.py @@ -11,6 +11,76 @@ from highcharts_core.utility_classes.shadows import ShadowOptions from highcharts_core.utility_classes.date_time_label_formats import DateTimeLabelFormats from highcharts_core.utility_classes.javascript_functions import CallbackFunction +from highcharts_core.utility_classes.position import Position + + +class TooltipPosition(Position): + """Options for coniguring the fixed position of a Tooltip.""" + + def __init__(self, **kwargs): + self._relative_to = None + + self.relative_to = kwargs.get("relative_to", None) + + super().__init__(**kwargs) + + @property + def relative_to(self) -> Optional[str]: + """Indicates the object relative to which a fixed tooltip should be positioned. + + Accepts: + + * ``'pane'`` + * ``'chart'`` + * ``'plotBox'`` + * ``'spacingBox'`` + + Defaults to ``'pane'`` if not specified. + + :rtype: :class:`str ` or :obj:`None ` + + """ + return self.relative_to + + @relative_to.setter + def relative_to(self, value): + if not value: + value = None + else: + value = value.lower() + if value not in ["pane", "chart", "plotbox", "spacingbox"]: + raise errors.HighchartsValueError( + f'relative_to expects either "pane", "chart", "plotBox", or "spacingBox". Was: {value}' + ) + if value == "plotbox": + value = "plotBox" + elif value == "spacingbox": + value = "spacingBox" + + self._relative_to = value + + @classmethod + def _get_kwargs_from_dict(cls, as_dict): + kwargs = { + "align": as_dict.get("align", None), + "vertical_align": as_dict.get("verticalAlign", None), + "x": as_dict.get("x", None), + "y": as_dict.get("y", None), + "relative_to": as_dict.get("relativeTo", None), + } + + return kwargs + + def _to_untrimmed_dict(self, in_cls=None) -> dict: + untrimmed = { + "relativeTo": self.relative_to, + } + + 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 Tooltip(HighchartsMeta): @@ -43,6 +113,7 @@ def __init__(self, **kwargs): self._padding = None self._point_format = None self._point_formatter = None + self._position = None self._positioner = None self._shadow = None self._shape = None @@ -82,6 +153,7 @@ def __init__(self, **kwargs): self.padding = kwargs.get("padding", None) self.point_format = kwargs.get("point_format", None) self.point_formatter = kwargs.get("point_formatter", None) + self.position = kwargs.get("position", None) self.positioner = kwargs.get("positioner", None) self.shadow = kwargs.get("shadow", None) self.shape = kwargs.get("shape", None) @@ -629,6 +701,24 @@ def point_formatter(self) -> Optional[CallbackFunction]: def point_formatter(self, value): self._point_formatter = value + @property + def position(self) -> Optional[TooltipPosition]: + """Positioning options for the tooltip when it is fixed. + + .. note:: + + This option is only respected if :meth:`Tooltip.fixed` is ``True``. + + :rtype: :class:`Position ` or :obj:`None ` + + """ + return self._position + + @position.setter + @class_sensitive(TooltipPosition) + def position(self, value): + self._position = value + @property def positioner(self) -> Optional[CallbackFunction]: """A JavaScript callback function to place the tooltip in a custom position. @@ -935,6 +1025,7 @@ def _get_kwargs_from_dict(cls, as_dict): "padding": as_dict.get("padding", None), "point_format": as_dict.get("pointFormat", None), "point_formatter": as_dict.get("pointFormatter", None), + "position": as_dict.get("position", None), "positioner": as_dict.get("positioner", None), "shadow": as_dict.get("shadow", None), "shape": as_dict.get("shape", None), @@ -979,6 +1070,7 @@ def _to_untrimmed_dict(self, in_cls=None) -> dict: "padding": self.padding, "pointFormat": self.point_format, "pointFormatter": self.point_formatter, + "position": self.position, "positioner": self.positioner, "shadow": self.shadow, "shape": self.shape, @@ -1047,6 +1139,7 @@ def _get_kwargs_from_dict(cls, as_dict): "date_time_label_formats": as_dict.get("dateTimeLabelFormats", None), "distance": as_dict.get("distance", None), "enabled": as_dict.get("enabled", None), + "fixed": as_dict.get("fixed", None), "follow_pointer": as_dict.get("followPointer", None), "follow_touch_move": as_dict.get("followTouchMove", None), "footer_format": as_dict.get("footerFormat", None), @@ -1061,6 +1154,7 @@ def _get_kwargs_from_dict(cls, as_dict): "padding": as_dict.get("padding", None), "point_format": as_dict.get("pointFormat", None), "point_formatter": as_dict.get("pointFormatter", None), + "position": as_dict.get("position", None), "positioner": as_dict.get("positioner", None), "shadow": as_dict.get("shadow", None), "shape": as_dict.get("shape", None), From 8f4777a7cd27a0ae0c88cec22b69def28e2f5b94 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 20:51:12 -0400 Subject: [PATCH 06/23] Added headers property to treemap options and series. --- .../options/plot_options/treemap.py | 396 +++++++++--------- highcharts_core/options/series/treemap.py | 185 ++++---- 2 files changed, 301 insertions(+), 280 deletions(-) diff --git a/highcharts_core/options/plot_options/treemap.py b/highcharts_core/options/plot_options/treemap.py index a94d1d5..d56f2ec 100644 --- a/highcharts_core/options/plot_options/treemap.py +++ b/highcharts_core/options/plot_options/treemap.py @@ -59,48 +59,51 @@ def __init__(self, **kwargs): self._levels = None self._alternate_starting_direction = None + self._headers = None self._interact_by_leaf = None self._layout_algorithm = None self._layout_starting_direction = None self._sort_index = 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_axis = kwargs.get('color_axis', None) - self.color_key = kwargs.get('color_key', None) - self.colors = kwargs.get('colors', 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.ignore_hidden_point = kwargs.get('ignore_hidden_point', None) - self.linecap = kwargs.get('linecap', None) - self.line_width = kwargs.get('line_width', None) - self.negative_color = kwargs.get('negative_color', 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.relative_x_value = kwargs.get('relative_x_value', None) - self.soft_threshold = kwargs.get('soft_threshold', None) - self.stacking = kwargs.get('stacking', None) - self.step = kwargs.get('step', None) - self.zone_axis = kwargs.get('zone_axis', None) - self.zones = kwargs.get('zones', None) - - self.color_index = kwargs.get('color_index', None) - self.crisp = kwargs.get('crisp', None) - self.allow_traversing_tree = kwargs.get('allow_traversing_tree', None) - self.breadcrumbs = kwargs.get('breadcrumbs', None) - self.color_by_point = kwargs.get('color_by_point', None) - self.level_is_constant = kwargs.get('level_is_constant', None) - self.levels = kwargs.get('levels', None) - - self.alternate_starting_direction = kwargs.get('alternate_starting_direction', - None) - self.interact_by_leaf = kwargs.get('interact_by_leaf', None) - self.layout_algorithm = kwargs.get('layout_algorithm', None) - self.layout_starting_direction = kwargs.get('layout_starting_direction', None) - self.sort_index = kwargs.get('sort_index', 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_axis = kwargs.get("color_axis", None) + self.color_key = kwargs.get("color_key", None) + self.colors = kwargs.get("colors", 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.ignore_hidden_point = kwargs.get("ignore_hidden_point", None) + self.linecap = kwargs.get("linecap", None) + self.line_width = kwargs.get("line_width", None) + self.negative_color = kwargs.get("negative_color", 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.relative_x_value = kwargs.get("relative_x_value", None) + self.soft_threshold = kwargs.get("soft_threshold", None) + self.stacking = kwargs.get("stacking", None) + self.step = kwargs.get("step", None) + self.zone_axis = kwargs.get("zone_axis", None) + self.zones = kwargs.get("zones", None) + + self.color_index = kwargs.get("color_index", None) + self.crisp = kwargs.get("crisp", None) + self.allow_traversing_tree = kwargs.get("allow_traversing_tree", None) + self.breadcrumbs = kwargs.get("breadcrumbs", None) + self.color_by_point = kwargs.get("color_by_point", None) + self.level_is_constant = kwargs.get("level_is_constant", None) + self.levels = kwargs.get("levels", None) + + self.alternate_starting_direction = kwargs.get( + "alternate_starting_direction", None + ) + self.headers = kwargs.get("headers", None) + self.interact_by_leaf = kwargs.get("interact_by_leaf", None) + self.layout_algorithm = kwargs.get("layout_algorithm", None) + self.layout_starting_direction = kwargs.get("layout_starting_direction", None) + self.sort_index = kwargs.get("sort_index", None) super().__init__(**kwargs) @@ -153,12 +156,12 @@ def animation_limit(self) -> Optional[int | float | Decimal]: @animation_limit.setter def animation_limit(self, value): - if value == float('inf'): - self._animation_limit = float('inf') + if value == float("inf"): + self._animation_limit = float("inf") else: - self._animation_limit = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._animation_limit = validators.numeric( + value, allow_empty=True, minimum=0 + ) @property def boost_blending(self) -> Optional[str]: @@ -171,7 +174,7 @@ def boost_blending(self) -> Optional[str]: @boost_blending.setter def boost_blending(self, value): - self._boost_blending = validators.string(value, allow_empty = True) + self._boost_blending = validators.string(value, allow_empty=True) @property def boost_threshold(self) -> Optional[int]: @@ -198,9 +201,7 @@ def boost_threshold(self) -> Optional[int]: @boost_threshold.setter def boost_threshold(self, value): - self._boost_threshold = validators.integer(value, - allow_empty = True, - minimum = 0) + self._boost_threshold = validators.integer(value, allow_empty=True, minimum=0) @property def breadcrumbs(self) -> Optional[BreadcrumbOptions]: @@ -240,8 +241,7 @@ def color_axis(self, value): try: self._color_axis = validators.string(value) except TypeError: - self._color_axis = validators.integer(value, - minimum = 0) + self._color_axis = validators.integer(value, minimum=0) @property def color_by_point(self) -> Optional[bool]: @@ -276,9 +276,7 @@ def color_index(self) -> Optional[int]: @color_index.setter def color_index(self, value): - self._color_index = validators.integer(value, - allow_empty = True, - minimum = 0) + self._color_index = validators.integer(value, allow_empty=True, minimum=0) @property def color_key(self) -> Optional[str]: @@ -296,7 +294,7 @@ def color_key(self) -> Optional[str]: @color_key.setter def color_key(self, value): - self._color_key = validators.string(value, allow_empty = True) + self._color_key = validators.string(value, allow_empty=True) @property def colors(self) -> Optional[List[str | Gradient | Pattern]]: @@ -358,9 +356,7 @@ def crop_threshold(self) -> Optional[int]: @crop_threshold.setter def crop_threshold(self, value): - self._crop_threshold = validators.integer(value, - allow_empty = True, - minimum = 0) + self._crop_threshold = validators.integer(value, allow_empty=True, minimum=0) @property def find_nearest_point_by(self) -> Optional[str]: @@ -380,7 +376,7 @@ def find_nearest_point_by(self) -> Optional[str]: @find_nearest_point_by.setter def find_nearest_point_by(self, value): - self._find_nearest_point_by = validators.string(value, allow_empty = True) + self._find_nearest_point_by = validators.string(value, allow_empty=True) @property def get_extremes_from_all(self) -> Optional[bool]: @@ -403,6 +399,26 @@ def get_extremes_from_all(self, value): else: self._get_extremes_from_all = bool(value) + @property + def headers(self) -> Optional[bool]: + """If ``True``, indicates that the data label should act as a group-level header. + Defaults to ``False``. + + .. note:: + + For leaf nodes, headers are not supported and the data label will be rendered inside. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._headers + + @headers.setter + def headers(self, value): + if value is None: + self._headers = None + else: + self._headers = bool(value) + @property def ignore_hidden_point(self) -> Optional[bool]: """If ``True``, the series shall be redrawn as if the hidden points were ``null``. @@ -466,7 +482,7 @@ def layout_algorithm(self) -> Optional[str]: @layout_algorithm.setter def layout_algorithm(self, value): - self._layout_algorithm = validators.variable_name(value, allow_empty = True) + self._layout_algorithm = validators.variable_name(value, allow_empty=True) @property def layout_starting_direction(self) -> Optional[str]: @@ -489,10 +505,12 @@ def layout_starting_direction(self, value): else: value = validators.string(value) value = value.lower() - if value not in ['vertical', 'horizontal']: - raise errors.HighchartsError(f'layout_starting_direction expects either ' - f'"vertical" or "horizontal". Received: ' - f'{value}') + if value not in ["vertical", "horizontal"]: + raise errors.HighchartsError( + f"layout_starting_direction expects either " + f'"vertical" or "horizontal". Received: ' + f"{value}" + ) self._layout_starting_direction = value @@ -524,7 +542,7 @@ def levels(self) -> Optional[List[TreemapLevelOptions]]: return self._levels @levels.setter - @class_sensitive(TreemapLevelOptions, force_iterable = True) + @class_sensitive(TreemapLevelOptions, force_iterable=True) def levels(self, value): self._levels = value @@ -540,7 +558,7 @@ def linecap(self) -> Optional[str]: @linecap.setter def linecap(self, value): - self._linecap = validators.string(value, allow_empty = True) + self._linecap = validators.string(value, allow_empty=True) @property def line_width(self) -> Optional[int | float | Decimal]: @@ -552,9 +570,7 @@ def line_width(self) -> Optional[int | float | Decimal]: @line_width.setter def line_width(self, value): - self._line_width = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._line_width = validators.numeric(value, allow_empty=True, minimum=0) @property def negative_color(self) -> Optional[str | Gradient | Pattern]: @@ -574,6 +590,7 @@ def negative_color(self) -> Optional[str | Gradient | Pattern]: @negative_color.setter def negative_color(self, value): from highcharts_core import utility_functions + self._negative_color = utility_functions.validate_color(value) @property @@ -608,9 +625,7 @@ def point_interval(self) -> Optional[int | float | Decimal]: @point_interval.setter def point_interval(self, value): - self._point_interval = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._point_interval = validators.numeric(value, allow_empty=True, minimum=0) @property def point_interval_unit(self) -> Optional[str]: @@ -635,7 +650,7 @@ def point_interval_unit(self) -> Optional[str]: @point_interval_unit.setter def point_interval_unit(self, value): - self._point_interval_unit = validators.string(value, allow_empty = True) + self._point_interval_unit = validators.string(value, allow_empty=True) @property def point_start(self) -> Optional[int | float | Decimal]: @@ -656,7 +671,7 @@ def point_start(self) -> Optional[int | float | Decimal]: @point_start.setter def point_start(self, value): - self._point_start = validators.numeric(value, allow_empty = True) + self._point_start = validators.numeric(value, allow_empty=True) @property def relative_x_value(self) -> Optional[bool]: @@ -715,9 +730,7 @@ def sort_index(self) -> Optional[int]: @sort_index.setter def sort_index(self, value): - self._sort_index = validators.integer(value, - allow_empty = True, - minimum = 0) + self._sort_index = validators.integer(value, allow_empty=True, minimum=0) @property def stacking(self) -> Optional[str]: @@ -747,9 +760,11 @@ def stacking(self, value): 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}') + 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 @@ -769,7 +784,7 @@ def step(self) -> Optional[str]: @step.setter def step(self, value): - self._step = validators.string(value, allow_empty = True) + self._step = validators.string(value, allow_empty=True) @property def zone_axis(self) -> Optional[str]: @@ -781,7 +796,7 @@ def zone_axis(self) -> Optional[str]: @zone_axis.setter def zone_axis(self, value): - self._zone_axis = validators.string(value, allow_empty = True) + self._zone_axis = validators.string(value, allow_empty=True) @property def zones(self) -> Optional[List[Zone]]: @@ -800,128 +815,131 @@ def zones(self) -> Optional[List[Zone]]: return self._zones @zones.setter - @class_sensitive(Zone, force_iterable = True) + @class_sensitive(Zone, force_iterable=True) def zones(self, value): self._zones = value @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'accessibility': as_dict.get('accessibility', None), - 'allow_point_select': as_dict.get('allowPointSelect', None), - 'animation': as_dict.get('animation', None), - 'class_name': as_dict.get('className', None), - 'clip': as_dict.get('clip', None), - 'color': as_dict.get('color', None), - 'cursor': as_dict.get('cursor', None), - 'custom': as_dict.get('custom', None), - 'dash_style': as_dict.get('dashStyle', None), - 'data_labels': as_dict.get('dataLabels', None), - 'description': as_dict.get('description', None), - 'enable_mouse_tracking': as_dict.get('enableMouseTracking', None), - 'events': as_dict.get('events', None), - 'include_in_data_export': as_dict.get('includeInDataExport', None), - 'keys': as_dict.get('keys', None), - 'label': as_dict.get('label', None), - 'legend_symbol': as_dict.get('legendSymbol', None), - 'linked_to': as_dict.get('linkedTo', None), - 'marker': as_dict.get('marker', None), - 'on_point': as_dict.get('onPoint', None), - 'opacity': as_dict.get('opacity', None), - 'point': as_dict.get('point', None), - 'point_description_formatter': as_dict.get('pointDescriptionFormatter', None), - 'selected': as_dict.get('selected', None), - 'show_checkbox': as_dict.get('showCheckbox', None), - 'show_in_legend': as_dict.get('showInLegend', None), - 'skip_keyboard_navigation': as_dict.get('skipKeyboardNavigation', None), - 'sonification': as_dict.get('sonification', None), - 'states': as_dict.get('states', None), - 'sticky_tracking': as_dict.get('stickyTracking', None), - 'threshold': as_dict.get('threshold', None), - 'tooltip': as_dict.get('tooltip', None), - 'turbo_threshold': as_dict.get('turboThreshold', None), - 'visible': as_dict.get('visible', None), - - 'animation_limit': as_dict.get('animationLimit', None), - 'boost_blending': as_dict.get('boostBlending', None), - 'boost_threshold': as_dict.get('boostThreshold', None), - 'color_axis': as_dict.get('colorAxis', None), - 'color_key': as_dict.get('colorKey', None), - 'colors': as_dict.get('colors', 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), - 'inactive_other_points': as_dict.get('inactiveOtherPoints', None), - 'ignore_hidden_point': as_dict.get('ignoreHiddenPoint', None), - 'linecap': as_dict.get('linecap', None), - 'line_width': as_dict.get('lineWidth', None), - 'negative_color': as_dict.get('negativeColor', None), - 'point_description_format': as_dict.get('pointDescriptionFormat', None), - 'point_interval': as_dict.get('pointInterval', None), 'point_interval_unit': as_dict.get('pointIntervalUnit', None), - 'point_start': as_dict.get('pointStart', None), - 'relative_x_value': as_dict.get('relativeXValue', None), - 'soft_threshold': as_dict.get('softThreshold', None), - 'stacking': as_dict.get('stacking', None), - 'step': as_dict.get('step', None), - 'zone_axis': as_dict.get('zoneAxis', None), - 'zones': as_dict.get('zones', None), - - 'color_index': as_dict.get('colorIndex', None), - 'crisp': as_dict.get('crisp', None), - 'allow_traversing_tree': as_dict.get('allowTraversingTree', None), - 'breadcrumbs': as_dict.get('breadcrumbs', None), - 'color_by_point': as_dict.get('colorByPoint', None), - 'level_is_constant': as_dict.get('levelIsConstant', None), - 'levels': as_dict.get('levels', None), - - 'alternate_starting_direction': as_dict.get('alternateStartingDirection', - None), - 'interact_by_leaf': as_dict.get('interactByLeaf', None), - 'layout_algorithm': as_dict.get('layoutAlgorithm', None), - 'layout_starting_direction': as_dict.get('layoutStartingDirection', None), - 'sort_index': as_dict.get('sortIndex', None) + "accessibility": as_dict.get("accessibility", None), + "allow_point_select": as_dict.get("allowPointSelect", None), + "animation": as_dict.get("animation", None), + "class_name": as_dict.get("className", None), + "clip": as_dict.get("clip", None), + "color": as_dict.get("color", None), + "cursor": as_dict.get("cursor", None), + "custom": as_dict.get("custom", None), + "dash_style": as_dict.get("dashStyle", None), + "data_labels": as_dict.get("dataLabels", None), + "description": as_dict.get("description", None), + "enable_mouse_tracking": as_dict.get("enableMouseTracking", None), + "events": as_dict.get("events", None), + "include_in_data_export": as_dict.get("includeInDataExport", None), + "keys": as_dict.get("keys", None), + "label": as_dict.get("label", None), + "legend_symbol": as_dict.get("legendSymbol", None), + "linked_to": as_dict.get("linkedTo", None), + "marker": as_dict.get("marker", None), + "on_point": as_dict.get("onPoint", None), + "opacity": as_dict.get("opacity", None), + "point": as_dict.get("point", None), + "point_description_formatter": as_dict.get( + "pointDescriptionFormatter", None + ), + "selected": as_dict.get("selected", None), + "show_checkbox": as_dict.get("showCheckbox", None), + "show_in_legend": as_dict.get("showInLegend", None), + "skip_keyboard_navigation": as_dict.get("skipKeyboardNavigation", None), + "sonification": as_dict.get("sonification", None), + "states": as_dict.get("states", None), + "sticky_tracking": as_dict.get("stickyTracking", None), + "threshold": as_dict.get("threshold", None), + "tooltip": as_dict.get("tooltip", None), + "turbo_threshold": as_dict.get("turboThreshold", None), + "visible": as_dict.get("visible", None), + "animation_limit": as_dict.get("animationLimit", None), + "boost_blending": as_dict.get("boostBlending", None), + "boost_threshold": as_dict.get("boostThreshold", None), + "color_axis": as_dict.get("colorAxis", None), + "color_key": as_dict.get("colorKey", None), + "colors": as_dict.get("colors", 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), + "inactive_other_points": as_dict.get("inactiveOtherPoints", None), + "ignore_hidden_point": as_dict.get("ignoreHiddenPoint", None), + "linecap": as_dict.get("linecap", None), + "line_width": as_dict.get("lineWidth", None), + "negative_color": as_dict.get("negativeColor", None), + "point_description_format": as_dict.get("pointDescriptionFormat", None), + "point_interval": as_dict.get("pointInterval", None), + "point_interval_unit": as_dict.get("pointIntervalUnit", None), + "point_start": as_dict.get("pointStart", None), + "relative_x_value": as_dict.get("relativeXValue", None), + "soft_threshold": as_dict.get("softThreshold", None), + "stacking": as_dict.get("stacking", None), + "step": as_dict.get("step", None), + "zone_axis": as_dict.get("zoneAxis", None), + "zones": as_dict.get("zones", None), + "color_index": as_dict.get("colorIndex", None), + "crisp": as_dict.get("crisp", None), + "allow_traversing_tree": as_dict.get("allowTraversingTree", None), + "breadcrumbs": as_dict.get("breadcrumbs", None), + "color_by_point": as_dict.get("colorByPoint", None), + "level_is_constant": as_dict.get("levelIsConstant", None), + "levels": as_dict.get("levels", None), + "alternate_starting_direction": as_dict.get( + "alternateStartingDirection", None + ), + "headers": as_dict.get("headers", None), + "interact_by_leaf": as_dict.get("interactByLeaf", None), + "layout_algorithm": as_dict.get("layoutAlgorithm", None), + "layout_starting_direction": as_dict.get("layoutStartingDirection", None), + "sort_index": as_dict.get("sortIndex", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'allowTraversingTree': self.allow_traversing_tree, - 'alternateStartingDirection': self.alternate_starting_direction, - 'animationLimit': self.animation_limit, - 'boostBlending': self.boost_blending, - 'boostThreshold': self.boost_threshold, - 'breadcrumbs': self.breadcrumbs, - 'colorAxis': self.color_axis, - 'colorByPoint': self.color_by_point, - 'colorIndex': self.color_index, - 'colorKey': self.color_key, - 'colors': self.colors, - 'crisp': self.crisp, - 'cropThreshold': self.crop_threshold, - 'findNearestPointBy': self.find_nearest_point_by, - 'getExtremesFromAll': self.get_extremes_from_all, - 'ignoreHiddenPoint': self.ignore_hidden_point, - 'interactByLeaf': self.interact_by_leaf, - 'layoutAlgorithm': self.layout_algorithm, - 'layoutStartingDirection': self.layout_starting_direction, - 'levelIsConstant': self.level_is_constant, - 'levels': self.levels, - 'linecap': self.linecap, - 'lineWidth': self.line_width, - 'negativeColor': self.negative_color, - 'pointInterval': self.point_interval, - 'pointIntervalUnit': self.point_interval_unit, - 'pointStart': self.point_start, - 'relativeXValue': self.relative_x_value, - 'softThreshold': self.soft_threshold, - 'sortIndex': self.sort_index, - 'stacking': self.stacking, - 'step': self.step, - 'zoneAxis': self.zone_axis, - 'zones': self.zones + "allowTraversingTree": self.allow_traversing_tree, + "alternateStartingDirection": self.alternate_starting_direction, + "animationLimit": self.animation_limit, + "boostBlending": self.boost_blending, + "boostThreshold": self.boost_threshold, + "breadcrumbs": self.breadcrumbs, + "colorAxis": self.color_axis, + "colorByPoint": self.color_by_point, + "colorIndex": self.color_index, + "colorKey": self.color_key, + "colors": self.colors, + "crisp": self.crisp, + "cropThreshold": self.crop_threshold, + "findNearestPointBy": self.find_nearest_point_by, + "getExtremesFromAll": self.get_extremes_from_all, + "headers": self.headers, + "ignoreHiddenPoint": self.ignore_hidden_point, + "interactByLeaf": self.interact_by_leaf, + "layoutAlgorithm": self.layout_algorithm, + "layoutStartingDirection": self.layout_starting_direction, + "levelIsConstant": self.level_is_constant, + "levels": self.levels, + "linecap": self.linecap, + "lineWidth": self.line_width, + "negativeColor": self.negative_color, + "pointInterval": self.point_interval, + "pointIntervalUnit": self.point_interval_unit, + "pointStart": self.point_start, + "relativeXValue": self.relative_x_value, + "softThreshold": self.soft_threshold, + "sortIndex": self.sort_index, + "stacking": self.stacking, + "step": self.step, + "zoneAxis": self.zone_axis, + "zones": self.zones, } - parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) + parent_as_dict = super()._to_untrimmed_dict(in_cls=in_cls) for key in parent_as_dict: untrimmed[key] = parent_as_dict[key] diff --git a/highcharts_core/options/series/treemap.py b/highcharts_core/options/series/treemap.py index 8a8e6de..25a55b6 100644 --- a/highcharts_core/options/series/treemap.py +++ b/highcharts_core/options/series/treemap.py @@ -1,7 +1,10 @@ from typing import Optional, List from highcharts_core.options.series.base import SeriesBase -from highcharts_core.options.series.data.treemap import TreemapData, TreemapDataCollection +from highcharts_core.options.series.data.treemap import ( + TreemapData, + TreemapDataCollection, +) from highcharts_core.options.plot_options.treemap import TreemapOptions from highcharts_core.utility_functions import mro__to_untrimmed_dict, is_ndarray @@ -24,17 +27,17 @@ def __init__(self, **kwargs): @classmethod def _data_collection_class(cls): """Returns the class object used for the data collection. - + :rtype: :class:`DataPointCollection ` descendent """ return TreemapDataCollection - + @classmethod def _data_point_class(cls): """Returns the class object used for individual data points. - - :rtype: :class:`DataBase ` + + :rtype: :class:`DataBase ` descendent """ return TreemapData @@ -70,95 +73,95 @@ def data(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'accessibility': as_dict.get('accessibility', None), - 'allow_point_select': as_dict.get('allowPointSelect', None), - 'animation': as_dict.get('animation', None), - 'class_name': as_dict.get('className', None), - 'clip': as_dict.get('clip', None), - 'color': as_dict.get('color', None), - 'cursor': as_dict.get('cursor', None), - 'custom': as_dict.get('custom', None), - 'dash_style': as_dict.get('dashStyle', None), - 'data_labels': as_dict.get('dataLabels', None), - 'description': as_dict.get('description', None), - 'enable_mouse_tracking': as_dict.get('enableMouseTracking', None), - 'events': as_dict.get('events', None), - 'include_in_data_export': as_dict.get('includeInDataExport', None), - 'keys': as_dict.get('keys', None), - 'label': as_dict.get('label', None), - 'legend_symbol': as_dict.get('legendSymbol', None), - 'linked_to': as_dict.get('linkedTo', None), - 'marker': as_dict.get('marker', None), - 'on_point': as_dict.get('onPoint', None), - 'opacity': as_dict.get('opacity', None), - 'point': as_dict.get('point', None), - 'point_description_formatter': as_dict.get('pointDescriptionFormatter', None), - 'selected': as_dict.get('selected', None), - 'show_checkbox': as_dict.get('showCheckbox', None), - 'show_in_legend': as_dict.get('showInLegend', None), - 'skip_keyboard_navigation': as_dict.get('skipKeyboardNavigation', None), - 'sonification': as_dict.get('sonification', None), - 'states': as_dict.get('states', None), - 'sticky_tracking': as_dict.get('stickyTracking', None), - 'threshold': as_dict.get('threshold', None), - 'tooltip': as_dict.get('tooltip', None), - 'turbo_threshold': as_dict.get('turboThreshold', None), - 'visible': as_dict.get('visible', None), - - 'animation_limit': as_dict.get('animationLimit', None), - 'boost_blending': as_dict.get('boostBlending', None), - 'boost_threshold': as_dict.get('boostThreshold', None), - 'color_axis': as_dict.get('colorAxis', None), - 'color_key': as_dict.get('colorKey', None), - 'colors': as_dict.get('colors', 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), - 'inactive_other_points': as_dict.get('inactiveOtherPoints', None), - 'ignore_hidden_point': as_dict.get('ignoreHiddenPoint', None), - 'linecap': as_dict.get('linecap', None), - 'line_width': as_dict.get('lineWidth', None), - 'negative_color': as_dict.get('negativeColor', None), - 'point_description_format': as_dict.get('pointDescriptionFormat', None), - 'point_interval': as_dict.get('pointInterval', None), - 'point_interval_unit': as_dict.get('pointIntervalUnit', None), - 'point_start': as_dict.get('pointStart', None), - 'relative_x_value': as_dict.get('relativeXValue', None), - 'soft_threshold': as_dict.get('softThreshold', None), - 'stacking': as_dict.get('stacking', None), - 'step': as_dict.get('step', None), - 'zone_axis': as_dict.get('zoneAxis', None), - 'zones': as_dict.get('zones', None), - - 'color_index': as_dict.get('colorIndex', None), - 'crisp': as_dict.get('crisp', None), - 'allow_traversing_tree': as_dict.get('allowTraversingTree', None), - 'breadcrumbs': as_dict.get('breadcrumbs', None), - 'color_by_point': as_dict.get('colorByPoint', None), - 'level_is_constant': as_dict.get('levelIsConstant', None), - 'levels': as_dict.get('levels', 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), - - 'alternate_starting_direction': as_dict.get('alternateStartingDirection', - None), - 'interact_by_leaf': as_dict.get('interactByLeaf', None), - 'layout_algorithm': as_dict.get('layoutAlgorithm', None), - 'layout_starting_direction': as_dict.get('layoutStartingDirection', None), - 'sort_index': as_dict.get('sortIndex', None) + "accessibility": as_dict.get("accessibility", None), + "allow_point_select": as_dict.get("allowPointSelect", None), + "animation": as_dict.get("animation", None), + "class_name": as_dict.get("className", None), + "clip": as_dict.get("clip", None), + "color": as_dict.get("color", None), + "cursor": as_dict.get("cursor", None), + "custom": as_dict.get("custom", None), + "dash_style": as_dict.get("dashStyle", None), + "data_labels": as_dict.get("dataLabels", None), + "description": as_dict.get("description", None), + "enable_mouse_tracking": as_dict.get("enableMouseTracking", None), + "events": as_dict.get("events", None), + "include_in_data_export": as_dict.get("includeInDataExport", None), + "keys": as_dict.get("keys", None), + "label": as_dict.get("label", None), + "legend_symbol": as_dict.get("legendSymbol", None), + "linked_to": as_dict.get("linkedTo", None), + "marker": as_dict.get("marker", None), + "on_point": as_dict.get("onPoint", None), + "opacity": as_dict.get("opacity", None), + "point": as_dict.get("point", None), + "point_description_formatter": as_dict.get( + "pointDescriptionFormatter", None + ), + "selected": as_dict.get("selected", None), + "show_checkbox": as_dict.get("showCheckbox", None), + "show_in_legend": as_dict.get("showInLegend", None), + "skip_keyboard_navigation": as_dict.get("skipKeyboardNavigation", None), + "sonification": as_dict.get("sonification", None), + "states": as_dict.get("states", None), + "sticky_tracking": as_dict.get("stickyTracking", None), + "threshold": as_dict.get("threshold", None), + "tooltip": as_dict.get("tooltip", None), + "turbo_threshold": as_dict.get("turboThreshold", None), + "visible": as_dict.get("visible", None), + "animation_limit": as_dict.get("animationLimit", None), + "boost_blending": as_dict.get("boostBlending", None), + "boost_threshold": as_dict.get("boostThreshold", None), + "color_axis": as_dict.get("colorAxis", None), + "color_key": as_dict.get("colorKey", None), + "colors": as_dict.get("colors", 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), + "inactive_other_points": as_dict.get("inactiveOtherPoints", None), + "ignore_hidden_point": as_dict.get("ignoreHiddenPoint", None), + "linecap": as_dict.get("linecap", None), + "line_width": as_dict.get("lineWidth", None), + "negative_color": as_dict.get("negativeColor", None), + "point_description_format": as_dict.get("pointDescriptionFormat", None), + "point_interval": as_dict.get("pointInterval", None), + "point_interval_unit": as_dict.get("pointIntervalUnit", None), + "point_start": as_dict.get("pointStart", None), + "relative_x_value": as_dict.get("relativeXValue", None), + "soft_threshold": as_dict.get("softThreshold", None), + "stacking": as_dict.get("stacking", None), + "step": as_dict.get("step", None), + "zone_axis": as_dict.get("zoneAxis", None), + "zones": as_dict.get("zones", None), + "color_index": as_dict.get("colorIndex", None), + "crisp": as_dict.get("crisp", None), + "allow_traversing_tree": as_dict.get("allowTraversingTree", None), + "breadcrumbs": as_dict.get("breadcrumbs", None), + "color_by_point": as_dict.get("colorByPoint", None), + "level_is_constant": as_dict.get("levelIsConstant", None), + "levels": as_dict.get("levels", 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), + "alternate_starting_direction": as_dict.get( + "alternateStartingDirection", None + ), + "headers": as_dict.get("headers", None), + "interact_by_leaf": as_dict.get("interactByLeaf", None), + "layout_algorithm": as_dict.get("layoutAlgorithm", None), + "layout_starting_direction": as_dict.get("layoutStartingDirection", None), + "sort_index": as_dict.get("sortIndex", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: - untrimmed = mro__to_untrimmed_dict(self, in_cls = in_cls) or {} + def _to_untrimmed_dict(self, in_cls=None) -> dict: + untrimmed = mro__to_untrimmed_dict(self, in_cls=in_cls) or {} return untrimmed From 01a5938bb5c0ff40e4a91f91fb316d81de73e92c Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 20:56:08 -0400 Subject: [PATCH 07/23] Added group_padding support to Treemap options and series. --- .../options/plot_options/treemap.py | 21 +++++++++++++++++++ highcharts_core/options/series/treemap.py | 1 + 2 files changed, 22 insertions(+) diff --git a/highcharts_core/options/plot_options/treemap.py b/highcharts_core/options/plot_options/treemap.py index d56f2ec..6373ec1 100644 --- a/highcharts_core/options/plot_options/treemap.py +++ b/highcharts_core/options/plot_options/treemap.py @@ -59,6 +59,7 @@ def __init__(self, **kwargs): self._levels = None self._alternate_starting_direction = None + self._group_padding = None self._headers = None self._interact_by_leaf = None self._layout_algorithm = None @@ -99,6 +100,7 @@ def __init__(self, **kwargs): self.alternate_starting_direction = kwargs.get( "alternate_starting_direction", None ) + self.group_padding = kwargs.get("group_padding", None) self.headers = kwargs.get("headers", None) self.interact_by_leaf = kwargs.get("interact_by_leaf", None) self.layout_algorithm = kwargs.get("layout_algorithm", None) @@ -399,6 +401,23 @@ def get_extremes_from_all(self, value): else: self._get_extremes_from_all = bool(value) + @property + def group_padding(self) -> Optional[int | float | Decimal]: + """Group padding for parent elements, expressed in pixels. + + .. seealso:: + + :meth:`TreemapOptions.node_size_by` for how leaf nodes' size is affected + by group padding. + + :rtype: Number or :obj:`None ` + """ + return self._group_padding + + @group_padding.setter + def group_padding(self, value): + self._group_padding = validators.numeric(value, allow_empty=True) + @property def headers(self) -> Optional[bool]: """If ``True``, indicates that the data label should act as a group-level header. @@ -892,6 +911,7 @@ def _get_kwargs_from_dict(cls, as_dict): "alternate_starting_direction": as_dict.get( "alternateStartingDirection", None ), + "group_padding": as_dict.get("groupPadding", None), "headers": as_dict.get("headers", None), "interact_by_leaf": as_dict.get("interactByLeaf", None), "layout_algorithm": as_dict.get("layoutAlgorithm", None), @@ -918,6 +938,7 @@ def _to_untrimmed_dict(self, in_cls=None) -> dict: "cropThreshold": self.crop_threshold, "findNearestPointBy": self.find_nearest_point_by, "getExtremesFromAll": self.get_extremes_from_all, + "groupPadding": self.group_padding, "headers": self.headers, "ignoreHiddenPoint": self.ignore_hidden_point, "interactByLeaf": self.interact_by_leaf, diff --git a/highcharts_core/options/series/treemap.py b/highcharts_core/options/series/treemap.py index 25a55b6..c4627ce 100644 --- a/highcharts_core/options/series/treemap.py +++ b/highcharts_core/options/series/treemap.py @@ -152,6 +152,7 @@ def _get_kwargs_from_dict(cls, as_dict): "alternate_starting_direction": as_dict.get( "alternateStartingDirection", None ), + "group_padding": as_dict.get("groupPadding", None), "headers": as_dict.get("headers", None), "interact_by_leaf": as_dict.get("interactByLeaf", None), "layout_algorithm": as_dict.get("layoutAlgorithm", None), From a193aae797246ed9dd8ea451e4ba46520edafd16 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 21:00:56 -0400 Subject: [PATCH 08/23] Added node_size_by support to Treemap options and series. --- .../options/plot_options/treemap.py | 32 +++++++++++++++++++ highcharts_core/options/series/treemap.py | 1 + 2 files changed, 33 insertions(+) diff --git a/highcharts_core/options/plot_options/treemap.py b/highcharts_core/options/plot_options/treemap.py index 6373ec1..3aad36c 100644 --- a/highcharts_core/options/plot_options/treemap.py +++ b/highcharts_core/options/plot_options/treemap.py @@ -64,6 +64,7 @@ def __init__(self, **kwargs): self._interact_by_leaf = None self._layout_algorithm = None self._layout_starting_direction = None + self._node_size_by = None self._sort_index = None self.animation_limit = kwargs.get("animation_limit", None) @@ -105,6 +106,7 @@ def __init__(self, **kwargs): self.interact_by_leaf = kwargs.get("interact_by_leaf", None) self.layout_algorithm = kwargs.get("layout_algorithm", None) self.layout_starting_direction = kwargs.get("layout_starting_direction", None) + self.node_size_by = kwargs.get("node_size_by", None) self.sort_index = kwargs.get("sort_index", None) super().__init__(**kwargs) @@ -612,6 +614,34 @@ def negative_color(self, value): self._negative_color = utility_functions.validate_color(value) + @property + def node_size_by(self) -> Optional[str]: + """Determines how to calculate the size of a leaf node when a header or group padding is present. + + Accepts: + + * ``'leaf'``, which expands the group to make room for headers and padding to preserve + relative sizes between leaves + * ``'group'``, which fits leaves naively into the remaining area after the header and padding + are subtracted + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._node_size_by + + @node_size_by.setter + def node_size_by(self, value): + if not value: + value = None + else: + value = value.lower() + if value not in ["leaf", "group"]: + raise errors.HighchartsError( + f"node_size_by expects either 'leaf' or 'group'. Received: {value}" + ) + + self._node_size_by = value + @property def point_interval(self) -> Optional[int | float | Decimal]: """If no x values are given for the points in a series, ``point_interval`` defines @@ -916,6 +946,7 @@ def _get_kwargs_from_dict(cls, as_dict): "interact_by_leaf": as_dict.get("interactByLeaf", None), "layout_algorithm": as_dict.get("layoutAlgorithm", None), "layout_starting_direction": as_dict.get("layoutStartingDirection", None), + "node_size_by": as_dict.get("nodeSizeBy", None), "sort_index": as_dict.get("sortIndex", None), } @@ -949,6 +980,7 @@ def _to_untrimmed_dict(self, in_cls=None) -> dict: "linecap": self.linecap, "lineWidth": self.line_width, "negativeColor": self.negative_color, + "nodeSizeBy": self.node_size_by, "pointInterval": self.point_interval, "pointIntervalUnit": self.point_interval_unit, "pointStart": self.point_start, diff --git a/highcharts_core/options/series/treemap.py b/highcharts_core/options/series/treemap.py index c4627ce..c43d5b6 100644 --- a/highcharts_core/options/series/treemap.py +++ b/highcharts_core/options/series/treemap.py @@ -157,6 +157,7 @@ def _get_kwargs_from_dict(cls, as_dict): "interact_by_leaf": as_dict.get("interactByLeaf", None), "layout_algorithm": as_dict.get("layoutAlgorithm", None), "layout_starting_direction": as_dict.get("layoutStartingDirection", None), + "node_size_by": as_dict.get("nodeSizeBy", None), "sort_index": as_dict.get("sortIndex", None), } From 9b795a61625f1b361a18ba52d303491e69c24aaf Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 21:04:30 -0400 Subject: [PATCH 09/23] Added support for non-cartesian series zooming. --- highcharts_core/module_requirements.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/highcharts_core/module_requirements.json b/highcharts_core/module_requirements.json index 7c143eb..02e4cd3 100644 --- a/highcharts_core/module_requirements.json +++ b/highcharts_core/module_requirements.json @@ -81,6 +81,9 @@ "chart.zoomKey": [ "modules/draggable-points" ], + "chart.zooming": [ + "modules/non-cartesian-zoom" + ], "chart.zooming.key": [ "modules/draggable-points" ], From 385eb9842260e960abe11368ef148003da499aef Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 21:11:23 -0400 Subject: [PATCH 10/23] Added local support to Exporting --- highcharts_core/options/exporting/__init__.py | 303 ++++++++++-------- 1 file changed, 165 insertions(+), 138 deletions(-) diff --git a/highcharts_core/options/exporting/__init__.py b/highcharts_core/options/exporting/__init__.py index 60362ee..cc625e9 100644 --- a/highcharts_core/options/exporting/__init__.py +++ b/highcharts_core/options/exporting/__init__.py @@ -10,12 +10,14 @@ from highcharts_core.options.exporting.csv import ExportingCSV from highcharts_core.options.exporting.pdf_font import PDFFontOptions from highcharts_core.utility_classes.menus import MenuObject -from highcharts_core.utility_classes.buttons import ContextButtonConfiguration, \ - ExportingButtons +from highcharts_core.utility_classes.buttons import ( + ContextButtonConfiguration, + ExportingButtons, +) from highcharts_core.utility_classes.javascript_functions import CallbackFunction default_context_button = ExportingButtons() -default_context_button['contextButton'] = ContextButtonConfiguration() +default_context_button["contextButton"] = ContextButtonConfiguration() class ExportingAccessibilityOptions(HighchartsMeta): @@ -24,15 +26,15 @@ class ExportingAccessibilityOptions(HighchartsMeta): def __init_(self, **kwargs): self._enabled = None - self.enabled = kwargs.get('enabled', None) + self.enabled = kwargs.get("enabled", None) @property def _dot_path(self) -> Optional[str]: """The dot-notation path to the options key for the current class. - + :rtype: :class:`str ` or :obj:`None ` """ - return 'exporting.accessibility' + return "exporting.accessibility" @property def enabled(self) -> Optional[bool]: @@ -54,16 +56,12 @@ def enabled(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): - kwargs = { - 'enabled': as_dict.get('enabled', None) - } + kwargs = {"enabled": as_dict.get("enabled", None)} return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: - return { - 'enabled': self.enabled - } + def _to_untrimmed_dict(self, in_cls=None) -> dict: + return {"enabled": self.enabled} class Exporting(HighchartsMeta): @@ -82,6 +80,7 @@ def __init__(self, **kwargs): self._filename = None self._form_attributes = None self._lib_url = None + self._local = None self._menu_item_definitions = None self._pdf_font = None self._print_max_width = None @@ -97,40 +96,41 @@ def __init__(self, **kwargs): self._use_rowspan_headers = None self._width = None - self.accessibility = kwargs.get('accessibility', None) - self.allow_html = kwargs.get('allow_html', None) - self.buttons = kwargs.get('buttons', default_context_button) - self.chart_options = kwargs.get('chart_options', None) - self.csv = kwargs.get('csv', None) - self.enabled = kwargs.get('enabled', None) - self.error = kwargs.get('error', None) - self.fallback_to_export_server = kwargs.get('fallback_to_export_server', None) - self.fetch_options = kwargs.get('fetch_options', None) - self.filename = kwargs.get('filename', None) - self.form_attributes = kwargs.get('form_attributes', None) - self.lib_url = kwargs.get('lib_url', None) - self.menu_item_definitions = kwargs.get('menu_item_definitions', None) - self.pdf_font = kwargs.get('pdf_font', None) - self.print_max_width = kwargs.get('print_max_width', None) - self.scale = kwargs.get('scale', None) - self.show_export_in_progress = kwargs.get('show_export_in_progress', None) - self.show_table = kwargs.get('show_table', None) - self.source_height = kwargs.get('source_height', None) - self.source_width = kwargs.get('source_width', None) - self.table_caption = kwargs.get('table_caption', None) - self.type = kwargs.get('type', None) - self.url = kwargs.get('url', None) - self.use_multi_level_headers = kwargs.get('use_multi_level_headers', None) - self.use_rowspan_headers = kwargs.get('use_rowspan_headers', None) - self.width = kwargs.get('width', None) + self.accessibility = kwargs.get("accessibility", None) + self.allow_html = kwargs.get("allow_html", None) + self.buttons = kwargs.get("buttons", default_context_button) + self.chart_options = kwargs.get("chart_options", None) + self.csv = kwargs.get("csv", None) + self.enabled = kwargs.get("enabled", None) + self.error = kwargs.get("error", None) + self.fallback_to_export_server = kwargs.get("fallback_to_export_server", None) + self.fetch_options = kwargs.get("fetch_options", None) + self.filename = kwargs.get("filename", None) + self.form_attributes = kwargs.get("form_attributes", None) + self.lib_url = kwargs.get("lib_url", None) + self.local = kwargs.get("local", None) + self.menu_item_definitions = kwargs.get("menu_item_definitions", None) + self.pdf_font = kwargs.get("pdf_font", None) + self.print_max_width = kwargs.get("print_max_width", None) + self.scale = kwargs.get("scale", None) + self.show_export_in_progress = kwargs.get("show_export_in_progress", None) + self.show_table = kwargs.get("show_table", None) + self.source_height = kwargs.get("source_height", None) + self.source_width = kwargs.get("source_width", None) + self.table_caption = kwargs.get("table_caption", None) + self.type = kwargs.get("type", None) + self.url = kwargs.get("url", None) + self.use_multi_level_headers = kwargs.get("use_multi_level_headers", None) + self.use_rowspan_headers = kwargs.get("use_rowspan_headers", None) + self.width = kwargs.get("width", None) @property def _dot_path(self) -> Optional[str]: """The dot-notation path to the options key for the current class. - + :rtype: :class:`str ` or :obj:`None ` """ - return 'exporting' + return "exporting" @property def accessibility(self) -> Optional[ExportingAccessibilityOptions]: @@ -148,8 +148,8 @@ def accessibility(self, value): @property def allow_html(self) -> Optional[bool]: """If ``True``, allows HTML inside the chart (added using - ``.use_html`` properties present on various chart components) to be added - directly to the exported image. This allows you to preserve complicated HTML + ``.use_html`` properties present on various chart components) to be added + directly to the exported image. This allows you to preserve complicated HTML structures like tables or bi-directional text in exported charts. Defaults to ``False``. @@ -159,8 +159,8 @@ def allow_html(self) -> Optional[bool]: This setting is **EXPERIMENTAL**. The HTML is rendered in a ``foreignObject`` tag in the generated SVG. The - official export server is based on PhantomJS, which supports this, but other - SVG clients, like Batik, do not support it. This also applies to downloaded + official export server is based on PhantomJS, which supports this, but other + SVG clients, like Batik, do not support it. This also applies to downloaded SVG that you want to open in a desktop client. :returns: Flag indicating whether to allow HTML in the exported image. @@ -182,10 +182,10 @@ def buttons(self) -> Optional[ExportingButtons]: .. note:: In addition to the default buttons listed above, custom buttons can be added. - + .. warning:: - - The ``.buttons`` property accepts an + + The ``.buttons`` property accepts an :class:`ExportingButtons ` instance as its value. This object is a descendent of the special :class:`JavaScriptDict ` which by default initially contains a ``'context @@ -219,9 +219,9 @@ def chart_options(self): value of :obj:`None `. :rtype: :class:`Options` or :obj:`None ` - + :raises HighchartsInstanceNeededError: if attempting to set it to a value that is - not a :class:`Options ` (or descendent) + not a :class:`Options ` (or descendent) instance. """ return self._chart_options @@ -230,16 +230,16 @@ def chart_options(self): def chart_options(self, value): if not value: self._chart_options = None - elif not checkers.is_type(value, ['Options']): + elif not checkers.is_type(value, ["Options"]): raise errors.HighchartsInstanceNeededError( - f'The Exporting.chart_options property is ' - f'one of the few properties in Highcharts ' - f'for Python that REQUIRES a Highcharts for ' - f'Python instance as its value (or None). ' - f'Specifically, you should supply an Options' - f' instance to it, rather than a dict or a ' - f'string. The value you supplied was: ' - f'{value.__class__.__name__}' + f"The Exporting.chart_options property is " + f"one of the few properties in Highcharts " + f"for Python that REQUIRES a Highcharts for " + f"Python instance as its value (or None). " + f"Specifically, you should supply an Options" + f" instance to it, rather than a dict or a " + f"string. The value you supplied was: " + f"{value.__class__.__name__}" ) else: self._chart_options = value @@ -345,19 +345,19 @@ def fallback_to_export_server(self, value): def fetch_options(self) -> Optional[dict]: """Options for the fetch request used when sending the SVG to the export server. Defaults to :obj:`None `. - + .. seealso:: - + * `MDN: Fetch `__ for more information - + :returns: The options for the fetch request, expressed as a Python :class:`dict ` :rtype: :class:`dict ` or :obj:`None ` """ return self._fetch_options - + @fetch_options.setter def fetch_options(self, value): - self._fetch_options = validators.dict(value, allow_empty = True) + self._fetch_options = validators.dict(value, allow_empty=True) @property def filename(self) -> Optional[str]: @@ -370,7 +370,7 @@ def filename(self) -> Optional[str]: @filename.setter def filename(self, value): - self._filename = validators.string(value, allow_empty = True) + self._filename = validators.string(value, allow_empty=True) @property def form_attributes(self) -> Optional[dict]: @@ -387,7 +387,7 @@ def form_attributes(self) -> Optional[dict]: @form_attributes.setter def form_attributes(self, value): - self._form_attributes = validators.dict(value, allow_empty = True) + self._form_attributes = validators.dict(value, allow_empty=True) @property def lib_url(self) -> Optional[str]: @@ -412,6 +412,28 @@ def lib_url(self, value): allow_special_ips=os.getenv("HCP_ALLOW_SPECIAL_IPS", False), ) + @property + def local(self) -> Optional[bool]: + """Indicates whether the chart should be exported using the browser's built-in capabilities, allowing + offline exports without requiring access to the Highcharts export server. Defaults to ``True``. + + .. note:: + + This option is different from :meth:`Exporting.fallback_to_export_server`, which controls whether + the export server should be used if local export fails. This option explicitly controls which export + option to use. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._local + + @local.setter + def local(self, value): + if value is None: + self._local = None + else: + self._local = bool(value) + @property def menu_item_definitions(self) -> Optional[MenuObject]: """An object consisting of definitions for the menu items in the context menu. @@ -421,12 +443,12 @@ def menu_item_definitions(self) -> Optional[MenuObject]: * ``onclick``: The click handler for the menu item * ``text``: The text for the menu item - * ``textKey``: If internationalization is required, the key to a language + * ``textKey``: If internationalization is required, the key to a language string .. note:: - Custom text for ``"exitFullScreen"`` can be set only in ``language`` options + Custom text for ``"exitFullScreen"`` can be set only in ``language`` options (it is not a separate button). Defaults to: @@ -496,7 +518,7 @@ def print_max_width(self) -> Optional[int | float | Decimal]: @print_max_width.setter def print_max_width(self, value): - self._print_max_width = validators.numeric(value, allow_empty = True) + self._print_max_width = validators.numeric(value, allow_empty=True) @property def scale(self) -> Optional[int | float | Decimal]: @@ -513,22 +535,22 @@ def scale(self) -> Optional[int | float | Decimal]: @scale.setter def scale(self, value): - self._scale = validators.numeric(value, allow_empty = True) + self._scale = validators.numeric(value, allow_empty=True) @property def show_export_in_progress(self) -> Optional[bool]: """If ``True``, displays a message when export is in progress. Defaults to ``True``. - + .. note:: - - The message displayed can be adjusted in + + The message displayed can be adjusted in :class:`Language.export_in_progress `. - + :rtype: :class:`bool ` or :obj:`None ` """ return self._show_export_in_progress - + @show_export_in_progress.setter def show_export_in_progress(self, value): if value is None: @@ -566,7 +588,7 @@ def source_height(self) -> Optional[int | float | Decimal]: @source_height.setter def source_height(self, value): - self._source_height = validators.numeric(value, allow_empty = True) + self._source_height = validators.numeric(value, allow_empty=True) @property def source_width(self) -> Optional[int | float | Decimal]: @@ -581,7 +603,7 @@ def source_width(self) -> Optional[int | float | Decimal]: @source_width.setter def source_width(self, value): - self._source_width = validators.numeric(value, allow_empty = True) + self._source_width = validators.numeric(value, allow_empty=True) @property def table_caption(self) -> Optional[bool | str]: @@ -606,7 +628,7 @@ def table_caption(self, value): elif not value: self._table_caption = None else: - self._table_caption = validators.string(value, allow_empty = False) + self._table_caption = validators.string(value, allow_empty=False) @property def type(self) -> Optional[str]: @@ -633,12 +655,15 @@ def type(self, value): else: value = validators.string(value) value = value.lower() - if value not in ['image/png', - 'image/jpeg', - 'application/pdf', - 'image/svg+xml']: - raise errors.HighchartsValueError(f'type expects a supported export MIME ' - f'type. Received: "{value}"') + if value not in [ + "image/png", + "image/jpeg", + "application/pdf", + "image/svg+xml", + ]: + raise errors.HighchartsValueError( + f'type expects a supported export MIME type. Received: "{value}"' + ) self._type = value @@ -724,69 +749,71 @@ def width(self) -> Optional[int | float | Decimal]: @width.setter def width(self, value): - self._width = validators.numeric(value, allow_empty = True) + self._width = validators.numeric(value, allow_empty=True) @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'accessibility': as_dict.get('accessibility', None), - 'allow_html': as_dict.get('allowHTML', None), - 'buttons': as_dict.get('buttons', None), - 'chart_options': as_dict.get('chartOptions', None), - 'csv': as_dict.get('csv', None), - 'enabled': as_dict.get('enabled', None), - 'error': as_dict.get('error', None), - 'fallback_to_export_server': as_dict.get('fallbackToExportServer', None), - 'fetch_options': as_dict.get('fetchOptions', None), - 'filename': as_dict.get('filename', None), - 'form_attributes': as_dict.get('formAttributes', None), - 'lib_url': as_dict.get('libURL', None), - 'menu_item_definitions': as_dict.get('menuItemDefinitions', None), - 'pdf_font': as_dict.get('pdfFont', None), - 'print_max_width': as_dict.get('printMaxWidth', None), - 'scale': as_dict.get('scale', None), - 'show_export_in_progress': as_dict.get('showExportInProgress', None), - 'show_table': as_dict.get('showTable', None), - 'source_height': as_dict.get('sourceHeight', None), - 'source_width': as_dict.get('sourceWidth', None), - 'table_caption': as_dict.get('tableCaption', None), - 'type': as_dict.get('type', None), - 'url': as_dict.get('url', None), - 'use_multi_level_headers': as_dict.get('useMultiLevelHeaders', None), - 'use_rowspan_headers': as_dict.get('useRowspanHeaders', None), - 'width': as_dict.get('width', None) + "accessibility": as_dict.get("accessibility", None), + "allow_html": as_dict.get("allowHTML", None), + "buttons": as_dict.get("buttons", None), + "chart_options": as_dict.get("chartOptions", None), + "csv": as_dict.get("csv", None), + "enabled": as_dict.get("enabled", None), + "error": as_dict.get("error", None), + "fallback_to_export_server": as_dict.get("fallbackToExportServer", None), + "fetch_options": as_dict.get("fetchOptions", None), + "filename": as_dict.get("filename", None), + "form_attributes": as_dict.get("formAttributes", None), + "lib_url": as_dict.get("libURL", None), + "local": as_dict.get("local", None), + "menu_item_definitions": as_dict.get("menuItemDefinitions", None), + "pdf_font": as_dict.get("pdfFont", None), + "print_max_width": as_dict.get("printMaxWidth", None), + "scale": as_dict.get("scale", None), + "show_export_in_progress": as_dict.get("showExportInProgress", None), + "show_table": as_dict.get("showTable", None), + "source_height": as_dict.get("sourceHeight", None), + "source_width": as_dict.get("sourceWidth", None), + "table_caption": as_dict.get("tableCaption", None), + "type": as_dict.get("type", None), + "url": as_dict.get("url", None), + "use_multi_level_headers": as_dict.get("useMultiLevelHeaders", None), + "use_rowspan_headers": as_dict.get("useRowspanHeaders", None), + "width": as_dict.get("width", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'accessibility': self.accessibility, - 'allowHTML': self.allow_html, - 'buttons': self.buttons, - 'chartOptions': self.chart_options, - 'csv': self.csv, - 'enabled': self.enabled, - 'error': self.error, - 'fallbackToExportServer': self.fallback_to_export_server, - 'fetchOptions': self.fetch_options, - 'filename': self.filename, - 'formAttributes': self.form_attributes, - 'libURL': self.lib_url, - 'menuItemDefinitions': self.menu_item_definitions, - 'pdfFont': self.pdf_font, - 'printMaxWidth': self.print_max_width, - 'scale': self.scale, - 'showExportInProgress': self.show_export_in_progress, - 'showTable': self.show_table, - 'sourceHeight': self.source_height, - 'sourceWidth': self.source_width, - 'tableCaption': self.table_caption, - 'type': self.type, - 'url': self.url, - 'useMultiLevelHeaders': self.use_multi_level_headers, - 'useRowspanHeaders': self.use_rowspan_headers, - 'width': self.width + "accessibility": self.accessibility, + "allowHTML": self.allow_html, + "buttons": self.buttons, + "chartOptions": self.chart_options, + "csv": self.csv, + "enabled": self.enabled, + "error": self.error, + "fallbackToExportServer": self.fallback_to_export_server, + "fetchOptions": self.fetch_options, + "filename": self.filename, + "formAttributes": self.form_attributes, + "libURL": self.lib_url, + "local": self.local, + "menuItemDefinitions": self.menu_item_definitions, + "pdfFont": self.pdf_font, + "printMaxWidth": self.print_max_width, + "scale": self.scale, + "showExportInProgress": self.show_export_in_progress, + "showTable": self.show_table, + "sourceHeight": self.source_height, + "sourceWidth": self.source_width, + "tableCaption": self.table_caption, + "type": self.type, + "url": self.url, + "useMultiLevelHeaders": self.use_multi_level_headers, + "useRowspanHeaders": self.use_rowspan_headers, + "width": self.width, } return untrimmed From 302e433dffad46d2655adf32103de4d5569830a8 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 21:30:26 -0400 Subject: [PATCH 11/23] Updated treegraph and treemap confiuraiton options. --- .../options/plot_options/treegraph.py | 573 ++++++++++-------- .../options/plot_options/treemap.py | 43 ++ highcharts_core/options/series/treegraph.py | 172 +++--- highcharts_core/options/series/treemap.py | 2 + 4 files changed, 463 insertions(+), 327 deletions(-) diff --git a/highcharts_core/options/plot_options/treegraph.py b/highcharts_core/options/plot_options/treegraph.py index dbbed14..c51302f 100644 --- a/highcharts_core/options/plot_options/treegraph.py +++ b/highcharts_core/options/plot_options/treegraph.py @@ -15,69 +15,68 @@ class TreegraphEvents(SeriesEvents): - """General event handlers for the series items. - + """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) + + 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) - + + 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``, + """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), + "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: + 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 + "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 @@ -85,14 +84,14 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: 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): @@ -118,43 +117,53 @@ def __init__(self, **kwargs): self._color_by_point = None self._fill_space = None self._link = None - self._reversed = None + self._reversed = None self._traverse_up_button = None - + self._levels = None self._node_distance = None self._node_width = 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.fill_space = kwargs.get('fill_space', None) - self.link = kwargs.get('link', None) - self.reversed = kwargs.get('reversed', None) - - self.levels = kwargs.get('levels', None) - self.node_distance = kwargs.get('node_distance', None) - self.node_width = kwargs.get('node_width', None) - + + self._group_padding = None + self._node_size_by = None + self._traverse_to_leaf = None + self._zoom_enabled = 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.fill_space = kwargs.get("fill_space", None) + self.link = kwargs.get("link", None) + self.reversed = kwargs.get("reversed", None) + + self.levels = kwargs.get("levels", None) + self.node_distance = kwargs.get("node_distance", None) + self.node_width = kwargs.get("node_width", None) + + self.group_padding = kwargs.get("group_padding", None) + self.node_size_by = kwargs.get("node_size_by", None) + self.traverse_to_leaf = kwargs.get("traverse_to_leaf", None) + self.zoom_enabled = kwargs.get("zoom_enabled", 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 @@ -171,12 +180,12 @@ def animation_limit(self) -> Optional[int | float | Decimal]: @animation_limit.setter def animation_limit(self, value): - if value == float('inf'): - self._animation_limit = float('inf') + if value == float("inf"): + self._animation_limit = float("inf") else: - self._animation_limit = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._animation_limit = validators.numeric( + value, allow_empty=True, minimum=0 + ) @property def boost_blending(self) -> Optional[str]: @@ -189,7 +198,7 @@ def boost_blending(self) -> Optional[str]: @boost_blending.setter def boost_blending(self, value): - self._boost_blending = validators.string(value, allow_empty = True) + self._boost_blending = validators.string(value, allow_empty=True) @property def boost_threshold(self) -> Optional[int]: @@ -216,9 +225,7 @@ def boost_threshold(self) -> Optional[int]: @boost_threshold.setter def boost_threshold(self, value): - self._boost_threshold = validators.integer(value, - allow_empty = True, - minimum = 0) + self._boost_threshold = validators.integer(value, allow_empty=True, minimum=0) @property def color_index(self) -> Optional[int]: @@ -227,7 +234,7 @@ def color_index(self) -> Optional[int]: ``highcharts-color-{n}``. .. tip:: - + .. versionadded:: Highcharts (JS) v.11 With Highcharts (JS) v.11, using CSS variables of the form ``--highcharts-color-{n}`` make @@ -241,9 +248,7 @@ def color_index(self) -> Optional[int]: @color_index.setter def color_index(self, value): - self._color_index = validators.integer(value, - allow_empty = True, - minimum = 0) + self._color_index = validators.integer(value, allow_empty=True, minimum=0) @property def crisp(self) -> Optional[bool]: @@ -286,9 +291,7 @@ def crop_threshold(self) -> Optional[int]: @crop_threshold.setter def crop_threshold(self, value): - self._crop_threshold = validators.integer(value, - allow_empty = True, - minimum = 0) + self._crop_threshold = validators.integer(value, allow_empty=True, minimum=0) @property def events(self) -> Optional[TreegraphEvents]: @@ -326,7 +329,7 @@ def find_nearest_point_by(self) -> Optional[str]: @find_nearest_point_by.setter def find_nearest_point_by(self, value): - self._find_nearest_point_by = validators.string(value, allow_empty = True) + self._find_nearest_point_by = validators.string(value, allow_empty=True) @property def get_extremes_from_all(self) -> Optional[bool]: @@ -412,7 +415,7 @@ def step(self) -> Optional[str]: @step.setter def step(self, value): - self._step = validators.string(value, allow_empty = True) + self._step = validators.string(value, allow_empty=True) @property def point_interval(self) -> Optional[int | float | Decimal]: @@ -446,9 +449,7 @@ def point_interval(self) -> Optional[int | float | Decimal]: @point_interval.setter def point_interval(self, value): - self._point_interval = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._point_interval = validators.numeric(value, allow_empty=True, minimum=0) @property def point_interval_unit(self) -> Optional[str]: @@ -473,7 +474,7 @@ def point_interval_unit(self) -> Optional[str]: @point_interval_unit.setter def point_interval_unit(self, value): - self._point_interval_unit = validators.string(value, allow_empty = True) + self._point_interval_unit = validators.string(value, allow_empty=True) @property def point_start(self) -> Optional[int | float | Decimal]: @@ -495,18 +496,18 @@ def point_start(self) -> Optional[int | float | Decimal]: @point_start.setter def point_start(self, value): try: - value = validators.numeric(value, allow_empty = True) + 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()*1000 - elif hasattr(value, 'timestamp'): - value = value.replace(tzinfo = datetime.timezone.utc) - value = value.timestamp()*1000 + if hasattr(value, "timestamp") and value.tzinfo is not None: + self._point_start = value.timestamp() * 1000 + elif hasattr(value, "timestamp"): + value = value.replace(tzinfo=datetime.timezone.utc) + value = value.timestamp() * 1000 else: raise error - + self._point_start = value @property @@ -537,9 +538,11 @@ def stacking(self, value): 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}') + 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 @@ -561,17 +564,17 @@ def allow_traversing_tree(self, 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 ` + + :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 @@ -593,20 +596,20 @@ def color_by_point(self, value): @property def fill_space(self) -> Optional[bool]: - """If ``True``, the treegraph series should fill the entire plot area in the + """If ``True``, the treegraph series should fill the entire plot area in the X-axis direction, even when there are collapsed points. Defaults to ``False``. - + :rtype: :class:`bool ` """ return self._fill_space - + @fill_space.setter def fill_space(self, value): if value is None: self._fill_space = None else: self._fill_space = bool(value) - + @property def link(self) -> Optional[LinkOptions]: """Link style options. @@ -638,17 +641,17 @@ def reversed(self, value): @property def levels(self) -> Optional[List[TreegraphLevelOptions]]: - """Set options on specific levels. - + """Set options on specific levels. + .. note:: - + Takes precedence over series options, but not point options. - - :rtype: :class:`TreemapLevelOptions ` + + :rtype: :class:`TreemapLevelOptions ` or :obj:`None ` """ return self._levels - + @levels.setter @class_sensitive(TreegraphLevelOptions) def levels(self, value): @@ -656,7 +659,7 @@ def levels(self, value): @property def node_distance(self) -> Optional[str | int | float | Decimal]: - """The distance between nodes in a treegraph diagram in the longitudinal + """The distance between nodes in a treegraph diagram in the longitudinal direction. Defaults to ``30``. .. note:: @@ -700,8 +703,8 @@ def node_width(self) -> Optional[str | int | float | Decimal]: For tree graphs, the node width is only applied if the marker symbol is ``'rect'``, otherwise the marker sizing options apply. - Can be a number or a percentage string, or ``'auto'``. If ``'auto'``, the nodes are - sized to fill up the plot area in the longitudinal direction, regardless of the + Can be a number or a percentage string, or ``'auto'``. If ``'auto'``, the nodes are + sized to fill up the plot area in the longitudinal direction, regardless of the number of levels. :rtype: :class:`str ` or numeric or :obj:`None ` @@ -723,6 +726,86 @@ def node_width(self, value): self._node_width = value + @property + def group_padding(self) -> Optional[int | float | Decimal]: + """Group padding for parent elements, expressed in pixels. + + .. seealso:: + + :meth:`TreegraphOptions.node_size_by` for how leaf nodes' size is affected + by group padding. + + :rtype: Number or :obj:`None ` + """ + return self._group_padding + + @group_padding.setter + def group_padding(self, value): + self._group_padding = validators.numeric(value, allow_empty=True) + + @property + def node_size_by(self) -> Optional[str]: + """Determines how to calculate the size of a leaf node when a header or group padding is present. + + Accepts: + + * ``'leaf'``, which expands the group to make room for headers and padding to preserve + relative sizes between leaves + * ``'group'``, which fits leaves naively into the remaining area after the header and padding + are subtracted + + :rtype: :class:`str ` or :obj:`None ` + """ + return self._node_size_by + + @node_size_by.setter + def node_size_by(self, value): + if not value: + value = None + else: + value = value.lower() + if value not in ["leaf", "group"]: + raise errors.HighchartsError( + f"node_size_by expects either 'leaf' or 'group'. Received: {value}" + ) + + self._node_size_by = value + + @property + def traverse_to_leaf(self) -> Optional[bool]: + """If ``True``, enables automatic traversing to the last child upon node interaction. + Defaults to ``False``. + + .. tip:: + This feature simplifies navigation by immediately focusing on the deepest layer of the + data structure without intermediate steps. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._traverse_to_leaf + + @traverse_to_leaf.setter + def traverse_to_leaf(self, value): + if value is None: + self._traverse_to_leaf = None + else: + self._traverse_to_leaf = bool(value) + + @property + def zoom_enabled(self) -> Optional[bool]: + """If ``True``, enables zooming in on nodes when clicking on them. Defaults to ``True``. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._zoom_enabled + + @zoom_enabled.setter + def zoom_enabled(self, value): + if value is None: + self._zoom_enabled = None + else: + self._zoom_enabled = bool(value) + @classmethod def _get_kwargs_from_dict(cls, as_dict): """Convenience method which returns the keyword arguments used to initialize the @@ -737,133 +820,137 @@ class from a Highcharts Javascript-compatible :class:`dict ` object """ kwargs = { - 'accessibility': as_dict.get('accessibility', None), - 'allow_point_select': as_dict.get('allowPointSelect', None), - 'animation': as_dict.get('animation', None), - 'class_name': as_dict.get('className', None), - 'clip': as_dict.get('clip', None), - 'color': as_dict.get('color', None), - 'cursor': as_dict.get('cursor', None), - 'custom': as_dict.get('custom', None), - 'dash_style': as_dict.get('dashStyle', None), - 'data_labels': as_dict.get('dataLabels', None), - 'description': as_dict.get('description', None), - 'enable_mouse_tracking': as_dict.get('enableMouseTracking', None), - 'events': as_dict.get('events', None), - 'include_in_data_export': as_dict.get('includeInDataExport', None), - 'keys': as_dict.get('keys', None), - 'label': as_dict.get('label', None), - 'legend_symbol': as_dict.get('legendSymbol', None), - 'linked_to': as_dict.get('linkedTo', None), - 'marker': as_dict.get('marker', None), - 'on_point': as_dict.get('onPoint', None), - 'opacity': as_dict.get('opacity', None), - 'point': as_dict.get('point', None), - 'point_description_formatter': as_dict.get('pointDescriptionFormatter', None), - 'selected': as_dict.get('selected', None), - 'show_checkbox': as_dict.get('showCheckbox', None), - 'show_in_legend': as_dict.get('showInLegend', None), - 'skip_keyboard_navigation': as_dict.get('skipKeyboardNavigation', None), - 'sonification': as_dict.get('sonification', None), - 'states': as_dict.get('states', None), - 'sticky_tracking': as_dict.get('stickyTracking', None), - '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), - 'inactive_other_points': as_dict.get('inactiveOtherPoints', 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), - 'fill_space': as_dict.get('fillSpace', None), - 'link': as_dict.get('link', None), - 'reversed': as_dict.get('reversed', None), - 'levels': as_dict.get('levels', None), - 'node_distance': as_dict.get('nodeDistance', None), - 'node_width': as_dict.get('nodeWidth', None), + "accessibility": as_dict.get("accessibility", None), + "allow_point_select": as_dict.get("allowPointSelect", None), + "animation": as_dict.get("animation", None), + "class_name": as_dict.get("className", None), + "clip": as_dict.get("clip", None), + "color": as_dict.get("color", None), + "cursor": as_dict.get("cursor", None), + "custom": as_dict.get("custom", None), + "dash_style": as_dict.get("dashStyle", None), + "data_labels": as_dict.get("dataLabels", None), + "description": as_dict.get("description", None), + "enable_mouse_tracking": as_dict.get("enableMouseTracking", None), + "events": as_dict.get("events", None), + "include_in_data_export": as_dict.get("includeInDataExport", None), + "keys": as_dict.get("keys", None), + "label": as_dict.get("label", None), + "legend_symbol": as_dict.get("legendSymbol", None), + "linked_to": as_dict.get("linkedTo", None), + "marker": as_dict.get("marker", None), + "on_point": as_dict.get("onPoint", None), + "opacity": as_dict.get("opacity", None), + "point": as_dict.get("point", None), + "point_description_formatter": as_dict.get( + "pointDescriptionFormatter", None + ), + "selected": as_dict.get("selected", None), + "show_checkbox": as_dict.get("showCheckbox", None), + "show_in_legend": as_dict.get("showInLegend", None), + "skip_keyboard_navigation": as_dict.get("skipKeyboardNavigation", None), + "sonification": as_dict.get("sonification", None), + "states": as_dict.get("states", None), + "sticky_tracking": as_dict.get("stickyTracking", None), + "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), + "inactive_other_points": as_dict.get("inactiveOtherPoints", 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), + "fill_space": as_dict.get("fillSpace", None), + "link": as_dict.get("link", None), + "reversed": as_dict.get("reversed", None), + "levels": as_dict.get("levels", None), + "node_distance": as_dict.get("nodeDistance", None), + "node_width": as_dict.get("nodeWidth", None), + "group_padding": as_dict.get("groupPadding", None), + "node_size_by": as_dict.get("nodeSizeBy", None), + "traverse_to_leaf": as_dict.get("traverseToLeaf", None), + "zoom_enabled": as_dict.get("zoomEnabled", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + 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, - 'fillSpace': self.fill_space, - 'link': self.link, - 'reversed': self.reversed, - 'levels': self.levels, - 'nodeDistance': self.node_distance, - 'nodeWidth': self.node_width, + "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, + "fillSpace": self.fill_space, + "link": self.link, + "reversed": self.reversed, + "levels": self.levels, + "nodeDistance": self.node_distance, + "nodeWidth": self.node_width, + "groupPadding": self.group_padding, + "nodeSizeBy": self.node_size_by, + "traverseToLeaf": self.traverse_to_leaf, + "zoomEnabled": self.zoom_enabled, } return untrimmed diff --git a/highcharts_core/options/plot_options/treemap.py b/highcharts_core/options/plot_options/treemap.py index 3aad36c..2de7615 100644 --- a/highcharts_core/options/plot_options/treemap.py +++ b/highcharts_core/options/plot_options/treemap.py @@ -66,6 +66,8 @@ def __init__(self, **kwargs): self._layout_starting_direction = None self._node_size_by = None self._sort_index = None + self._traverse_to_leaf = None + self._zoom_enabled = None self.animation_limit = kwargs.get("animation_limit", None) self.boost_blending = kwargs.get("boost_blending", None) @@ -108,6 +110,8 @@ def __init__(self, **kwargs): self.layout_starting_direction = kwargs.get("layout_starting_direction", None) self.node_size_by = kwargs.get("node_size_by", None) self.sort_index = kwargs.get("sort_index", None) + self.traverse_to_leaf = kwargs.get("traverse_to_leaf", None) + self.zoom_enabled = kwargs.get("zoom_enabled", None) super().__init__(**kwargs) @@ -835,6 +839,41 @@ def step(self) -> Optional[str]: def step(self, value): self._step = validators.string(value, allow_empty=True) + @property + def traverse_to_leaf(self) -> Optional[bool]: + """If ``True``, enables automatic traversing to the last child upon node interaction. + Defaults to ``False``. + + .. tip:: + This feature simplifies navigation by immediately focusing on the deepest layer of the + data structure without intermediate steps. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._traverse_to_leaf + + @traverse_to_leaf.setter + def traverse_to_leaf(self, value): + if value is None: + self._traverse_to_leaf = None + else: + self._traverse_to_leaf = bool(value) + + @property + def zoom_enabled(self) -> Optional[bool]: + """If ``True``, enables zooming in on nodes when clicking on them. Defaults to ``True``. + + :rtype: :class:`bool ` or :obj:`None ` + """ + return self._zoom_enabled + + @zoom_enabled.setter + def zoom_enabled(self, value): + if value is None: + self._zoom_enabled = None + else: + self._zoom_enabled = bool(value) + @property def zone_axis(self) -> Optional[str]: """Defines the Axis on which the zones are applied. Defaults to ``'y'``. @@ -948,6 +987,8 @@ def _get_kwargs_from_dict(cls, as_dict): "layout_starting_direction": as_dict.get("layoutStartingDirection", None), "node_size_by": as_dict.get("nodeSizeBy", None), "sort_index": as_dict.get("sortIndex", None), + "traverse_to_leaf": as_dict.get("traverseToLeaf", None), + "zoom_enabled": as_dict.get("zoomEnabled", None), } return kwargs @@ -989,8 +1030,10 @@ def _to_untrimmed_dict(self, in_cls=None) -> dict: "sortIndex": self.sort_index, "stacking": self.stacking, "step": self.step, + "traverseToLeaf": self.traverse_to_leaf, "zoneAxis": self.zone_axis, "zones": self.zones, + "zoomEnabled": self.zoom_enabled, } parent_as_dict = super()._to_untrimmed_dict(in_cls=in_cls) diff --git a/highcharts_core/options/series/treegraph.py b/highcharts_core/options/series/treegraph.py index 81a2c1a..9fea2ca 100644 --- a/highcharts_core/options/series/treegraph.py +++ b/highcharts_core/options/series/treegraph.py @@ -1,21 +1,24 @@ from typing import Optional, List from highcharts_core.options.series.base import SeriesBase -from highcharts_core.options.series.data.treegraph import TreegraphData, TreegraphDataCollection +from highcharts_core.options.series.data.treegraph import ( + TreegraphData, + TreegraphDataCollection, +) from highcharts_core.options.plot_options.treegraph import TreegraphOptions from highcharts_core.utility_functions import mro__to_untrimmed_dict, is_ndarray 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): @@ -24,17 +27,17 @@ def __init__(self, **kwargs): @classmethod def _data_collection_class(cls): """Returns the class object used for the data collection. - + :rtype: :class:`DataPointCollection ` descendent """ return TreegraphDataCollection - + @classmethod def _data_point_class(cls): """Returns the class object used for individual data points. - - :rtype: :class:`DataBase ` + + :rtype: :class:`DataBase ` descendent """ return TreegraphData @@ -50,19 +53,19 @@ def data(self) -> Optional[List[TreegraphData] | TreegraphDataCollection]: .. 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 + + If using the Array of Arrays pattern you *must* set :meth:`.keys ` to indicate - which value in the inner array corresponds to - :meth:`.id `, + 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 @@ -80,81 +83,82 @@ def data(self, 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), - 'legend_symbol': as_dict.get('legendSymbol', None), - 'linked_to': as_dict.get('linkedTo', None), - 'marker': as_dict.get('marker', None), - 'on_point': as_dict.get('onPoint', None), - 'opacity': as_dict.get('opacity', None), - 'point': as_dict.get('point', None), - 'point_description_formatter': as_dict.get('pointDescriptionFormatter', None), - 'selected': as_dict.get('selected', None), - 'show_checkbox': as_dict.get('showCheckbox', None), - 'show_in_legend': as_dict.get('showInLegend', None), - 'skip_keyboard_navigation': as_dict.get('skipKeyboardNavigation', None), - 'sonification': as_dict.get('sonification', None), - 'states': as_dict.get('states', None), - 'sticky_tracking': as_dict.get('stickyTracking', None), - '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), - 'inactive_other_points': as_dict.get('inactiveOtherPoints', 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), - 'fill_space': as_dict.get('fillSpace', None), - 'link': as_dict.get('link', None), - 'reversed': as_dict.get('reversed', None), - 'levels': as_dict.get('levels', 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), - + "accessibility": as_dict.get("accessibility", None), + "allow_point_select": as_dict.get("allowPointSelect", None), + "animation": as_dict.get("animation", None), + "class_name": as_dict.get("className", None), + "clip": as_dict.get("clip", None), + "color": as_dict.get("color", None), + "cursor": as_dict.get("cursor", None), + "custom": as_dict.get("custom", None), + "dash_style": as_dict.get("dashStyle", None), + "data_labels": as_dict.get("dataLabels", None), + "description": as_dict.get("description", None), + "enable_mouse_tracking": as_dict.get("enableMouseTracking", None), + "events": as_dict.get("events", None), + "include_in_data_export": as_dict.get("includeInDataExport", None), + "keys": as_dict.get("keys", None), + "label": as_dict.get("label", None), + "legend_symbol": as_dict.get("legendSymbol", None), + "linked_to": as_dict.get("linkedTo", None), + "marker": as_dict.get("marker", None), + "on_point": as_dict.get("onPoint", None), + "opacity": as_dict.get("opacity", None), + "point": as_dict.get("point", None), + "point_description_formatter": as_dict.get( + "pointDescriptionFormatter", None + ), + "selected": as_dict.get("selected", None), + "show_checkbox": as_dict.get("showCheckbox", None), + "show_in_legend": as_dict.get("showInLegend", None), + "skip_keyboard_navigation": as_dict.get("skipKeyboardNavigation", None), + "sonification": as_dict.get("sonification", None), + "states": as_dict.get("states", None), + "sticky_tracking": as_dict.get("stickyTracking", None), + "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), + "inactive_other_points": as_dict.get("inactiveOtherPoints", 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), + "fill_space": as_dict.get("fillSpace", None), + "link": as_dict.get("link", None), + "reversed": as_dict.get("reversed", None), + "levels": as_dict.get("levels", None), + "traverse_to_leaf": as_dict.get("traverseToLeaf", None), + "group_padding": as_dict.get("groupPadding", None), + "node_size_by": as_dict.get("nodeSizeBy", None), + "zoom_enabled": as_dict.get("zoomEnabled", 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 {} + 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/options/series/treemap.py b/highcharts_core/options/series/treemap.py index c43d5b6..fac88c8 100644 --- a/highcharts_core/options/series/treemap.py +++ b/highcharts_core/options/series/treemap.py @@ -159,6 +159,8 @@ def _get_kwargs_from_dict(cls, as_dict): "layout_starting_direction": as_dict.get("layoutStartingDirection", None), "node_size_by": as_dict.get("nodeSizeBy", None), "sort_index": as_dict.get("sortIndex", None), + "traverse_to_leaf": as_dict.get("traverseToLeaf", None), + "zoom_enabled": as_dict.get("zoomEnabled", None), } return kwargs From 837a40c8c69a7d25496ff5aa2b913e767558f7d0 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 21:34:15 -0400 Subject: [PATCH 12/23] Added Legend.max_width support --- highcharts_core/options/legend/__init__.py | 404 +++++++++++---------- 1 file changed, 210 insertions(+), 194 deletions(-) diff --git a/highcharts_core/options/legend/__init__.py b/highcharts_core/options/legend/__init__.py index 6e4f942..fce8197 100644 --- a/highcharts_core/options/legend/__init__.py +++ b/highcharts_core/options/legend/__init__.py @@ -53,6 +53,7 @@ def __init__(self, **kwargs): self._layout = None self._margin = None self._max_height = None + self._max_width = None self._navigation = None self._padding = None self._reversed = None @@ -70,54 +71,55 @@ def __init__(self, **kwargs): self._x = None self._y = None - self.accessibility = kwargs.get('accessibility', None) - self.align = kwargs.get('align', None) - self.align_columns = kwargs.get('align_columns', None) - self.background_color = kwargs.get('background_color', None) - self.border_color = kwargs.get('border_color', None) - self.border_width = kwargs.get('border_width', None) - self.border_radius = kwargs.get('border_radius', None) - self.bubble_legend = kwargs.get('bubble_legend', None) - self.class_name = kwargs.get('class_name', None) - self.enabled = kwargs.get('enabled', None) - self.floating = kwargs.get('floating', None) - self.item_checkbox_style = kwargs.get('item_checkbox_style', None) - self.item_distance = kwargs.get('item_distance', None) - self.item_hidden_style = kwargs.get('item_hidden_style', None) - self.item_hover_style = kwargs.get('item_hover_style', None) - self.item_margin_bottom = kwargs.get('item_margin_bottom', None) - self.item_margin_top = kwargs.get('item_margin_top', None) - self.item_style = kwargs.get('item_style', None) - self.item_width = kwargs.get('item_width', None) - self.label_format = kwargs.get('label_format', None) - self.label_formatter = kwargs.get('label_formatter', None) - self.layout = kwargs.get('layout', None) - self.margin = kwargs.get('margin', None) - self.max_height = kwargs.get('max_height', None) - self.navigation = kwargs.get('navigation', None) - self.padding = kwargs.get('padding', None) - self.reversed = kwargs.get('reversed', None) - self.rtl = kwargs.get('rtl', None) - self.shadow = kwargs.get('shadow', None) - self.square_symbol = kwargs.get('square_symbol', None) - self.symbol_height = kwargs.get('symbol_height', None) - self.symbol_padding = kwargs.get('symbol_padding', None) - self.symbol_radius = kwargs.get('symbol_radius', None) - self.symbol_width = kwargs.get('symbol_width', None) - self.title = kwargs.get('title', None) - self.use_html = kwargs.get('use_html', None) - self.vertical_align = kwargs.get('vertical_align', None) - self.width = kwargs.get('width', None) - self.x = kwargs.get('x', None) - self.y = kwargs.get('y', None) + self.accessibility = kwargs.get("accessibility", None) + self.align = kwargs.get("align", None) + self.align_columns = kwargs.get("align_columns", None) + self.background_color = kwargs.get("background_color", None) + self.border_color = kwargs.get("border_color", None) + self.border_width = kwargs.get("border_width", None) + self.border_radius = kwargs.get("border_radius", None) + self.bubble_legend = kwargs.get("bubble_legend", None) + self.class_name = kwargs.get("class_name", None) + self.enabled = kwargs.get("enabled", None) + self.floating = kwargs.get("floating", None) + self.item_checkbox_style = kwargs.get("item_checkbox_style", None) + self.item_distance = kwargs.get("item_distance", None) + self.item_hidden_style = kwargs.get("item_hidden_style", None) + self.item_hover_style = kwargs.get("item_hover_style", None) + self.item_margin_bottom = kwargs.get("item_margin_bottom", None) + self.item_margin_top = kwargs.get("item_margin_top", None) + self.item_style = kwargs.get("item_style", None) + self.item_width = kwargs.get("item_width", None) + self.label_format = kwargs.get("label_format", None) + self.label_formatter = kwargs.get("label_formatter", None) + self.layout = kwargs.get("layout", None) + self.margin = kwargs.get("margin", None) + self.max_width = kwargs.get("max_width", None) + self.max_height = kwargs.get("max_height", None) + self.navigation = kwargs.get("navigation", None) + self.padding = kwargs.get("padding", None) + self.reversed = kwargs.get("reversed", None) + self.rtl = kwargs.get("rtl", None) + self.shadow = kwargs.get("shadow", None) + self.square_symbol = kwargs.get("square_symbol", None) + self.symbol_height = kwargs.get("symbol_height", None) + self.symbol_padding = kwargs.get("symbol_padding", None) + self.symbol_radius = kwargs.get("symbol_radius", None) + self.symbol_width = kwargs.get("symbol_width", None) + self.title = kwargs.get("title", None) + self.use_html = kwargs.get("use_html", None) + self.vertical_align = kwargs.get("vertical_align", None) + self.width = kwargs.get("width", None) + self.x = kwargs.get("x", None) + self.y = kwargs.get("y", None) @property def _dot_path(self) -> Optional[str]: """The dot-notation path to the options key for the current class. - + :rtype: :class:`str ` or :obj:`None ` """ - return 'legend' + return "legend" @property def accessibility(self) -> Optional[LegendAccessibilityOptions]: @@ -162,9 +164,10 @@ def align(self, value): else: value = validators.string(value) value = value.lower() - if value not in ['left', 'center', 'right']: - raise errors.HighchartsValueError(f'align must be either "left", "center"' - f', or "right". Was: "{value}"') + if value not in ["left", "center", "right"]: + raise errors.HighchartsValueError( + f'align must be either "left", "center", or "right". Was: "{value}"' + ) self._align = value @property @@ -201,6 +204,7 @@ def background_color(self) -> Optional[str | Gradient | Pattern]: @background_color.setter def background_color(self, value): from highcharts_core import utility_functions + self._background_color = utility_functions.validate_color(value) @property @@ -218,6 +222,7 @@ def border_color(self) -> Optional[str | Gradient | Pattern]: @border_color.setter def border_color(self, value): from highcharts_core import utility_functions + self._border_color = utility_functions.validate_color(value) @property @@ -232,7 +237,7 @@ def border_radius(self) -> Optional[int | float | Decimal]: @border_radius.setter def border_radius(self, value): - self._border_radius = validators.numeric(value, allow_empty = True) + self._border_radius = validators.numeric(value, allow_empty=True) @property def border_width(self) -> Optional[int | float | Decimal]: @@ -246,7 +251,7 @@ def border_width(self) -> Optional[int | float | Decimal]: @border_width.setter def border_width(self, value): - self._border_width = validators.numeric(value, allow_empty = True) + self._border_width = validators.numeric(value, allow_empty=True) @property def bubble_legend(self) -> Optional[BubbleLegend]: @@ -276,7 +281,7 @@ def class_name(self) -> Optional[str]: @class_name.setter def class_name(self, value): - self._class_name = validators.string(value, allow_empty = True) + self._class_name = validators.string(value, allow_empty=True) @property def enabled(self) -> Optional[bool]: @@ -324,7 +329,7 @@ def item_checkbox_style(self) -> Optional[str | dict]: :meth:`Legend.show_checkbox` is ``True``. Defaults to: ``'{"width": "13px", "height": "13px", "position":"absolute"}'``. - :rtype: :class:`str ` or :class:`dict ` or + :rtype: :class:`str ` or :class:`dict ` or :obj:`None ` """ return self._item_checkbox_style @@ -332,11 +337,11 @@ def item_checkbox_style(self) -> Optional[str | dict]: @item_checkbox_style.setter def item_checkbox_style(self, value): try: - self._item_checkbox_style = validators.dict(value, allow_empty = True) + self._item_checkbox_style = validators.dict(value, allow_empty=True) except (ValueError, TypeError): - self._item_checkbox_style = validators.string(value, - allow_empty = True, - coerce_value = True) + self._item_checkbox_style = validators.string( + value, allow_empty=True, coerce_value=True + ) @property def item_distance(self) -> Optional[int | float | Decimal]: @@ -349,9 +354,7 @@ def item_distance(self) -> Optional[int | float | Decimal]: @item_distance.setter def item_distance(self, value): - self._item_distance = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._item_distance = validators.numeric(value, allow_empty=True, minimum=0) @property def item_hidden_style(self) -> Optional[str | dict]: @@ -374,11 +377,11 @@ def item_hidden_style(self) -> Optional[str | dict]: @item_hidden_style.setter def item_hidden_style(self, value): try: - self._item_hidden_style = validators.dict(value, allow_empty = True) + self._item_hidden_style = validators.dict(value, allow_empty=True) except (ValueError, TypeError): - self._item_hidden_style = validators.string(value, - allow_empty = True, - coerce_value = True) + self._item_hidden_style = validators.string( + value, allow_empty=True, coerce_value=True + ) @property def item_hover_style(self) -> Optional[str | dict]: @@ -394,7 +397,7 @@ def item_hover_style(self) -> Optional[str | dict]: Properties are inherited from :meth:`Legend.style` unless overridden here. - :rtype: :class:`str ` or :class:`dict ` or + :rtype: :class:`str ` or :class:`dict ` or :obj:`None ` """ return self._item_hover_style @@ -402,11 +405,11 @@ def item_hover_style(self) -> Optional[str | dict]: @item_hover_style.setter def item_hover_style(self, value): try: - self._item_hover_style = validators.dict(value, allow_empty = True) + self._item_hover_style = validators.dict(value, allow_empty=True) except (ValueError, TypeError): - self._item_hover_style = validators.string(value, - allow_empty = True, - coerce_value = True) + self._item_hover_style = validators.string( + value, allow_empty=True, coerce_value=True + ) @property def item_margin_bottom(self) -> Optional[int | float | Decimal]: @@ -419,9 +422,9 @@ def item_margin_bottom(self) -> Optional[int | float | Decimal]: @item_margin_bottom.setter def item_margin_bottom(self, value): - self._item_margin_bottom = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._item_margin_bottom = validators.numeric( + value, allow_empty=True, minimum=0 + ) @property def item_margin_top(self) -> Optional[int | float | Decimal]: @@ -434,9 +437,7 @@ def item_margin_top(self) -> Optional[int | float | Decimal]: @item_margin_top.setter def item_margin_top(self, value): - self._item_margin_top = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._item_margin_top = validators.numeric(value, allow_empty=True, minimum=0) @property def item_style(self) -> Optional[str | dict]: @@ -460,11 +461,11 @@ def item_style(self) -> Optional[str | dict]: @item_style.setter def item_style(self, value): try: - self._item_style = validators.dict(value, allow_empty = True) + self._item_style = validators.dict(value, allow_empty=True) except (ValueError, TypeError): - self._item_style = validators.string(value, - allow_empty = True, - coerce_value = True) + self._item_style = validators.string( + value, allow_empty=True, coerce_value=True + ) @property def item_width(self) -> Optional[int | float | Decimal]: @@ -481,9 +482,7 @@ def item_width(self) -> Optional[int | float | Decimal]: @item_width.setter def item_width(self, value): - self._item_width = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._item_width = validators.numeric(value, allow_empty=True, minimum=0) @property def label_format(self) -> Optional[str]: @@ -501,7 +500,7 @@ def label_format(self) -> Optional[str]: @label_format.setter def label_format(self, value): - self._label_format = validators.string(value, allow_empty = True) + self._label_format = validators.string(value, allow_empty=True) @property def label_formatter(self) -> Optional[CallbackFunction]: @@ -547,10 +546,12 @@ def layout(self, value): else: value = validators.string(value) value = value.lower() - if value not in ['horizontal', 'vertical', 'proximate']: - raise errors.HighchartsValueError(f'layout must be either "horizontal", ' - f', "vertical", or "proximate". Was: ' - f'"{value}"') + if value not in ["horizontal", "vertical", "proximate"]: + raise errors.HighchartsValueError( + f'layout must be either "horizontal", ' + f', "vertical", or "proximate". Was: ' + f'"{value}"' + ) self._layout = value @property @@ -565,8 +566,7 @@ def margin(self) -> Optional[int | float | Decimal]: @margin.setter def margin(self, value): - self._margin = validators.numeric(value, - allow_empty = True) + self._margin = validators.numeric(value, allow_empty=True) @property def max_height(self) -> Optional[int | float | Decimal]: @@ -581,9 +581,25 @@ def max_height(self) -> Optional[int | float | Decimal]: @max_height.setter def max_height(self, value): - self._max_height = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._max_height = validators.numeric(value, allow_empty=True, minimum=0) + + @property + def max_width(self) -> Optional[int | float | Decimal | str]: + """The maximum width for the legend, expressed in pixels (as an integer) or a + percentage of the chart (as a string). Defaults to ``None``. + + When the maximum width is extended, navigation will show. + + :rtype: numeric or :class:`str ` or :obj:`None ` + """ + return self._max_width + + @max_width.setter + def max_width(self, value): + try: + self._max_width = validators.numeric(value, allow_empty=True, minimum=0) + except (ValueError, TypeError): + self._max_width = validators.string(value) @property def navigation(self) -> Optional[LegendNavigation]: @@ -615,7 +631,7 @@ def padding(self) -> Optional[int | float | Decimal]: @padding.setter def padding(self, value): - self._padding = validators.numeric(value, allow_empty = True) + self._padding = validators.numeric(value, allow_empty=True) @property def reversed(self) -> Optional[bool]: @@ -676,11 +692,9 @@ def shadow(self, value): self._shadow = False else: if value is True: - value = ShadowOptions(enabled = True) + value = ShadowOptions(enabled=True) else: - value = validate_types(value, - types = ShadowOptions, - allow_none = False) + value = validate_types(value, types=ShadowOptions, allow_none=False) self._shadow = value @property @@ -711,9 +725,7 @@ def symbol_height(self) -> Optional[int | float | Decimal]: @symbol_height.setter def symbol_height(self, value): - self._symbol_height = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._symbol_height = validators.numeric(value, allow_empty=True, minimum=0) @property def symbol_padding(self) -> Optional[int | float | Decimal]: @@ -726,7 +738,7 @@ def symbol_padding(self) -> Optional[int | float | Decimal]: @symbol_padding.setter def symbol_padding(self, value): - self._symbol_padding = validators.numeric(value, allow_empty = True) + self._symbol_padding = validators.numeric(value, allow_empty=True) @property def symbol_radius(self) -> Optional[int | float | Decimal]: @@ -739,7 +751,7 @@ def symbol_radius(self) -> Optional[int | float | Decimal]: @symbol_radius.setter def symbol_radius(self, value): - self._symbol_radius = validators.numeric(value, allow_empty = True) + self._symbol_radius = validators.numeric(value, allow_empty=True) @property def symbol_width(self) -> Optional[int | float | Decimal]: @@ -753,9 +765,7 @@ def symbol_width(self) -> Optional[int | float | Decimal]: @symbol_width.setter def symbol_width(self, value): - self._symbol_width = validators.numeric(value, - allow_empty = True, - minimum = 0) + self._symbol_width = validators.numeric(value, allow_empty=True, minimum=0) @property def title(self) -> Optional[LegendTitle]: @@ -821,11 +831,13 @@ def vertical_align(self, value): if not value: self._vertical_align = None else: - value = validators.string(value, allow_empty = True) + value = validators.string(value, allow_empty=True) value = value.lower() - if value not in ['bottom', 'middle', 'top']: - raise errors.HighchartsValueError(f'vertical_align expects either "top", ' - f'"middle", or "bottom". Was: {value}') + if value not in ["bottom", "middle", "top"]: + raise errors.HighchartsValueError( + f'vertical_align expects either "top", ' + f'"middle", or "bottom". Was: {value}' + ) self._vertical_align = value @property @@ -851,14 +863,16 @@ def width(self, value): else: try: value = validators.string(value) - if '%' not in value: - raise errors.HighchartsValueError(f'if width is a string, it is ' - f'expected to be a percentage of ' - f'the chart area. No % sign found ' - f'in value: {value}') + if "%" not in value: + raise errors.HighchartsValueError( + f"if width is a string, it is " + f"expected to be a percentage of " + f"the chart area. No % sign found " + f"in value: {value}" + ) self._width = value except (TypeError, ValueError): - self._width = validators.numeric(value, minimum = 0) + self._width = validators.numeric(value, minimum=0) @property def x(self) -> Optional[int]: @@ -876,7 +890,7 @@ def x(self) -> Optional[int]: @x.setter def x(self, value): - self._x = validators.numeric(value, allow_empty = True) + self._x = validators.numeric(value, allow_empty=True) @property def y(self) -> Optional[int]: @@ -894,97 +908,99 @@ def y(self) -> Optional[int]: @y.setter def y(self, value): - self._y = validators.numeric(value, allow_empty = True) + self._y = validators.numeric(value, allow_empty=True) @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'accessibility': as_dict.get('accessibility', None), - 'align': as_dict.get('align', None), - 'align_columns': as_dict.get('alignColumns', None), - 'background_color': as_dict.get('backgroundColor', None), - 'border_color': as_dict.get('borderColor', None), - 'border_width': as_dict.get('borderWidth', None), - 'border_radius': as_dict.get('borderRadius', None), - 'bubble_legend': as_dict.get('bubbleLegend', None), - 'class_name': as_dict.get('className', None), - 'enabled': as_dict.get('enabled', None), - 'floating': as_dict.get('floating', None), - 'item_checkbox_style': as_dict.get('itemCheckboxStyle', None), - 'item_distance': as_dict.get('itemDistance', None), - 'item_hidden_style': as_dict.get('itemHiddenStyle', None), - 'item_hover_style': as_dict.get('itemHoverStyle', None), - 'item_margin_bottom': as_dict.get('itemMarginBottom', None), - 'item_margin_top': as_dict.get('itemMarginTop', None), - 'item_style': as_dict.get('itemStyle', None), - 'item_width': as_dict.get('itemWidth', None), - 'label_format': as_dict.get('labelFormat', None), - 'label_formatter': as_dict.get('labelFormatter', None), - 'layout': as_dict.get('layout', None), - 'margin': as_dict.get('margin', None), - 'max_height': as_dict.get('maxHeight', None), - 'navigation': as_dict.get('navigation', None), - 'padding': as_dict.get('padding', None), - 'reversed': as_dict.get('reversed', None), - 'rtl': as_dict.get('rtl', None), - 'shadow': as_dict.get('shadow', None), - 'square_symbol': as_dict.get('squareSymbol', None), - 'symbol_height': as_dict.get('symbolHeight', None), - 'symbol_padding': as_dict.get('symbolPadding', None), - 'symbol_radius': as_dict.get('symbolRadius', None), - 'symbol_width': as_dict.get('symbolWidth', None), - 'title': as_dict.get('title', None), - 'use_html': as_dict.get('useHTML', None), - 'vertical_align': as_dict.get('verticalAlign', None), - 'width': as_dict.get('width', None), - 'x': as_dict.get('x', None), - 'y': as_dict.get('y', None), + "accessibility": as_dict.get("accessibility", None), + "align": as_dict.get("align", None), + "align_columns": as_dict.get("alignColumns", None), + "background_color": as_dict.get("backgroundColor", None), + "border_color": as_dict.get("borderColor", None), + "border_width": as_dict.get("borderWidth", None), + "border_radius": as_dict.get("borderRadius", None), + "bubble_legend": as_dict.get("bubbleLegend", None), + "class_name": as_dict.get("className", None), + "enabled": as_dict.get("enabled", None), + "floating": as_dict.get("floating", None), + "item_checkbox_style": as_dict.get("itemCheckboxStyle", None), + "item_distance": as_dict.get("itemDistance", None), + "item_hidden_style": as_dict.get("itemHiddenStyle", None), + "item_hover_style": as_dict.get("itemHoverStyle", None), + "item_margin_bottom": as_dict.get("itemMarginBottom", None), + "item_margin_top": as_dict.get("itemMarginTop", None), + "item_style": as_dict.get("itemStyle", None), + "item_width": as_dict.get("itemWidth", None), + "label_format": as_dict.get("labelFormat", None), + "label_formatter": as_dict.get("labelFormatter", None), + "layout": as_dict.get("layout", None), + "margin": as_dict.get("margin", None), + "max_height": as_dict.get("maxHeight", None), + "max_width": as_dict.get("maxWidth", None), + "navigation": as_dict.get("navigation", None), + "padding": as_dict.get("padding", None), + "reversed": as_dict.get("reversed", None), + "rtl": as_dict.get("rtl", None), + "shadow": as_dict.get("shadow", None), + "square_symbol": as_dict.get("squareSymbol", None), + "symbol_height": as_dict.get("symbolHeight", None), + "symbol_padding": as_dict.get("symbolPadding", None), + "symbol_radius": as_dict.get("symbolRadius", None), + "symbol_width": as_dict.get("symbolWidth", None), + "title": as_dict.get("title", None), + "use_html": as_dict.get("useHTML", None), + "vertical_align": as_dict.get("verticalAlign", 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: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'accessibility': self.accessibility, - 'align': self.align, - 'alignColumns': self.align_columns, - 'backgroundColor': self.background_color, - 'borderColor': self.border_color, - 'borderWidth': self.border_width, - 'borderRadius': self.border_radius, - 'bubbleLegend': self.bubble_legend, - 'className': self.class_name, - 'enabled': self.enabled, - 'floating': self.floating, - 'itemCheckboxStyle': self.item_checkbox_style, - 'itemDistance': self.item_distance, - 'itemHiddenStyle': self.item_hidden_style, - 'itemHoverStyle': self.item_hover_style, - 'itemMarginBottom': self.item_margin_bottom, - 'itemMarginTop': self.item_margin_top, - 'itemStyle': self.item_style, - 'itemWidth': self.item_width, - 'labelFormat': self.label_format, - 'labelFormatter': self.label_formatter, - 'layout': self.layout, - 'margin': self.margin, - 'maxHeight': self.max_height, - 'navigation': self.navigation, - 'padding': self.padding, - 'reversed': self.reversed, - 'rtl': self.rtl, - 'shadow': self.shadow, - 'squareSymbol': self.square_symbol, - 'symbolHeight': self.symbol_height, - 'symbolPadding': self.symbol_padding, - 'symbolRadius': self.symbol_radius, - 'symbolWidth': self.symbol_width, - 'title': self.title, - 'useHTML': self.use_html, - 'verticalAlign': self.vertical_align, - 'width': self.width, - 'x': self.x, - 'y': self.y + "accessibility": self.accessibility, + "align": self.align, + "alignColumns": self.align_columns, + "backgroundColor": self.background_color, + "borderColor": self.border_color, + "borderWidth": self.border_width, + "borderRadius": self.border_radius, + "bubbleLegend": self.bubble_legend, + "className": self.class_name, + "enabled": self.enabled, + "floating": self.floating, + "itemCheckboxStyle": self.item_checkbox_style, + "itemDistance": self.item_distance, + "itemHiddenStyle": self.item_hidden_style, + "itemHoverStyle": self.item_hover_style, + "itemMarginBottom": self.item_margin_bottom, + "itemMarginTop": self.item_margin_top, + "itemStyle": self.item_style, + "itemWidth": self.item_width, + "labelFormat": self.label_format, + "labelFormatter": self.label_formatter, + "layout": self.layout, + "margin": self.margin, + "maxHeight": self.max_height, + "maxWidth": self.max_width, + "navigation": self.navigation, + "padding": self.padding, + "reversed": self.reversed, + "rtl": self.rtl, + "shadow": self.shadow, + "squareSymbol": self.square_symbol, + "symbolHeight": self.symbol_height, + "symbolPadding": self.symbol_padding, + "symbolRadius": self.symbol_radius, + "symbolWidth": self.symbol_width, + "title": self.title, + "useHTML": self.use_html, + "verticalAlign": self.vertical_align, + "width": self.width, + "x": self.x, + "y": self.y, } return untrimmed From 6f5397d10d31f6e2ec6b5ced8170de4b6e6aa8a4 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 21:40:57 -0400 Subject: [PATCH 13/23] Added show_delay support to Tooltip an dCrosshair options. --- highcharts_core/options/axes/crosshair.py | 67 ++++++++++++++--------- highcharts_core/options/tooltips.py | 17 ++++++ 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/highcharts_core/options/axes/crosshair.py b/highcharts_core/options/axes/crosshair.py index 3bf82e7..ca706f6 100644 --- a/highcharts_core/options/axes/crosshair.py +++ b/highcharts_core/options/axes/crosshair.py @@ -17,16 +17,18 @@ def __init__(self, **kwargs): self._class_name = None self._color = None self._dash_style = None + self._show_delay = None self._snap = None self._width = None self._z_index = None - self.class_name = kwargs.get('class_name', None) - self.color = kwargs.get('color', None) - self.dash_style = kwargs.get('dash_style', None) - self.snap = kwargs.get('snap', None) - self.width = kwargs.get('width', None) - self.z_index = kwargs.get('z_index', None) + self.class_name = kwargs.get("class_name", None) + self.color = kwargs.get("color", None) + self.dash_style = kwargs.get("dash_style", None) + self.show_delay = kwargs.get("show_delay", None) + self.snap = kwargs.get("snap", None) + self.width = kwargs.get("width", None) + self.z_index = kwargs.get("z_index", None) @property def class_name(self) -> Optional[str]: @@ -39,7 +41,7 @@ def class_name(self) -> Optional[str]: @class_name.setter def class_name(self, value): - self._class_name = validators.string(value, allow_empty = True) + self._class_name = validators.string(value, allow_empty=True) @property def color(self) -> Optional[str | Gradient | Pattern]: @@ -57,6 +59,7 @@ def color(self) -> Optional[str | Gradient | Pattern]: @color.setter def color(self, value): from highcharts_core import utility_functions + self._color = utility_functions.validate_color(value) @property @@ -88,11 +91,23 @@ def dash_style(self, value): else: value = validators.string(value) if value not in constants.SUPPORTED_DASH_STYLE_VALUES: - raise errors.HighchartsValueError(f'dash_style expects a ' - f'recognized value, but received: ' - f'{value}') + raise errors.HighchartsValueError( + f"dash_style expects a recognized value, but received: {value}" + ) self._dash_style = value + @property + def show_delay(self) -> Optional[int | float | Decimal]: + """The number of milliseconds to wait until the crosshair is shown. Defaults to ``0``. + + :rtype: numeric or :obj:`None ` + """ + return self._show_delay + + @show_delay.setter + def show_delay(self, value): + self._show_delay = validators.numeric(value, allow_empty=True, minimum=0) + @property def snap(self) -> Optional[bool]: """If ``True``, the crosshair should snap to the point. If ``False``, the @@ -121,7 +136,7 @@ def width(self) -> Optional[int | float | Decimal]: @width.setter def width(self, value): - self._width = validators.numeric(value, allow_empty = True) + self._width = validators.numeric(value, allow_empty=True) @property def z_index(self) -> Optional[int | float | Decimal]: @@ -138,29 +153,31 @@ def z_index(self) -> Optional[int | float | Decimal]: @z_index.setter def z_index(self, value): - self._z_index = validators.numeric(value, allow_empty = True) + self._z_index = validators.numeric(value, allow_empty=True) @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'class_name': as_dict.get('className', None), - 'color': as_dict.get('color', None), - 'dash_style': as_dict.get('dashStyle', None), - 'snap': as_dict.get('snap', None), - 'width': as_dict.get('width', None), - 'z_index': as_dict.get('zIndex', None) + "class_name": as_dict.get("className", None), + "color": as_dict.get("color", None), + "dash_style": as_dict.get("dashStyle", None), + "show_delay": as_dict.get("showDelay", None), + "snap": as_dict.get("snap", None), + "width": as_dict.get("width", None), + "z_index": as_dict.get("zIndex", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'className': self.class_name, - 'color': self.color, - 'dashStyle': self.dash_style, - 'snap': self.snap, - 'width': self.width, - 'zIndex': self.z_index + "className": self.class_name, + "color": self.color, + "dashStyle": self.dash_style, + "showDelay": self.show_delay, + "snap": self.snap, + "width": self.width, + "zIndex": self.z_index, } return untrimmed diff --git a/highcharts_core/options/tooltips.py b/highcharts_core/options/tooltips.py index aa38103..54891d2 100644 --- a/highcharts_core/options/tooltips.py +++ b/highcharts_core/options/tooltips.py @@ -118,6 +118,7 @@ def __init__(self, **kwargs): self._shadow = None self._shape = None self._shared = None + self._show_delay = None self._snap = None self._split = None self._stick_on_contact = None @@ -158,6 +159,7 @@ def __init__(self, **kwargs): self.shadow = kwargs.get("shadow", None) self.shape = kwargs.get("shape", None) self.shared = kwargs.get("shared", None) + self.show_delay = kwargs.get("show_delay", None) self.snap = kwargs.get("snap", None) self.split = kwargs.get("split", None) self.stick_on_contact = kwargs.get("stick_on_contact", None) @@ -828,6 +830,18 @@ def shared(self, value): else: self._shared = bool(value) + @property + def show_delay(self) -> Optional[int | float | Decimal]: + """The number of milliseconds to wait until the tooltip is shown. Defaults to ``0``. + + :rtype: numeric or :obj:`None ` + """ + return self._show_delay + + @show_delay.setter + def show_delay(self, value): + self._show_delay = validators.numeric(value, allow_empty=True, minimum=0) + @property def snap(self) -> Optional[int | float | Decimal]: """Proximity snap for graphs or single points. If :obj:`None `, it @@ -1030,6 +1044,7 @@ def _get_kwargs_from_dict(cls, as_dict): "shadow": as_dict.get("shadow", None), "shape": as_dict.get("shape", None), "shared": as_dict.get("shared", None), + "show_delay": as_dict.get("showDelay", None), "snap": as_dict.get("snap", None), "split": as_dict.get("split", None), "stick_on_contact": as_dict.get("stickOnContact", None), @@ -1075,6 +1090,7 @@ def _to_untrimmed_dict(self, in_cls=None) -> dict: "shadow": self.shadow, "shape": self.shape, "shared": self.shared, + "showDelay": self.show_delay, "snap": self.snap, "split": self.split, "stickOnContact": self.stick_on_contact, @@ -1159,6 +1175,7 @@ def _get_kwargs_from_dict(cls, as_dict): "shadow": as_dict.get("shadow", None), "shape": as_dict.get("shape", None), "shared": as_dict.get("shared", None), + "show_delay": as_dict.get("showDelay", None), "snap": as_dict.get("snap", None), "split": as_dict.get("split", None), "stick_on_contact": as_dict.get("stickOnContact", None), From 72ef497654213ba2039b0f3984f9166bc2ef4a05 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 21:45:03 -0400 Subject: [PATCH 14/23] Added Boost.chunk_size support. --- highcharts_core/options/boost.py | 113 ++++++++++++++++++------------- 1 file changed, 66 insertions(+), 47 deletions(-) diff --git a/highcharts_core/options/boost.py b/highcharts_core/options/boost.py index 35fbb39..8af01c6 100644 --- a/highcharts_core/options/boost.py +++ b/highcharts_core/options/boost.py @@ -18,12 +18,12 @@ def __init__(self, **kwargs): self._time_series_processing = None self._time_setup = None - self.show_skip_summary = kwargs.get('show_skip_summary', None) - self.time_buffer_copy = kwargs.get('time_buffer_copy', None) - self.time_kd_tree = kwargs.get('time_kd_tree', None) - self.time_rendering = kwargs.get('time_rendering', None) - self.time_series_processing = kwargs.get('time_series_processing', None) - self.time_setup = kwargs.get('time_setup', None) + self.show_skip_summary = kwargs.get("show_skip_summary", None) + self.time_buffer_copy = kwargs.get("time_buffer_copy", None) + self.time_kd_tree = kwargs.get("time_kd_tree", None) + self.time_rendering = kwargs.get("time_rendering", None) + self.time_series_processing = kwargs.get("time_series_processing", None) + self.time_setup = kwargs.get("time_setup", None) @property def show_skip_summary(self) -> Optional[bool]: @@ -137,24 +137,24 @@ def time_setup(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'show_skip_summary': as_dict.get('showSkipSummary', None), - 'time_buffer_copy': as_dict.get('timeBufferCopy', None), - 'time_kd_tree': as_dict.get('timeKDTree', None), - 'time_rendering': as_dict.get('timeRendering', None), - 'time_series_processing': as_dict.get('timeSeriesProcessing', None), - 'time_setup': as_dict.get('timeSetup', None), + "show_skip_summary": as_dict.get("showSkipSummary", None), + "time_buffer_copy": as_dict.get("timeBufferCopy", None), + "time_kd_tree": as_dict.get("timeKDTree", None), + "time_rendering": as_dict.get("timeRendering", None), + "time_series_processing": as_dict.get("timeSeriesProcessing", None), + "time_setup": as_dict.get("timeSetup", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: return { - 'showSkipSummary': self.show_skip_summary, - 'timeBufferCopy': self.time_buffer_copy, - 'timeKDTree': self.time_kd_tree, - 'timeRendering': self.time_rendering, - 'timeSeriesProcessing': self.time_series_processing, - 'timeSetup': self.time_setup + "showSkipSummary": self.show_skip_summary, + "timeBufferCopy": self.time_buffer_copy, + "timeKDTree": self.time_kd_tree, + "timeRendering": self.time_rendering, + "timeSeriesProcessing": self.time_series_processing, + "timeSetup": self.time_setup, } @@ -178,6 +178,7 @@ class Boost(HighchartsMeta): def __init__(self, **kwargs): self._allow_force = None + self._chunk_size = None self._debug = None self._enabled = None self._pixel_ratio = None @@ -185,21 +186,22 @@ def __init__(self, **kwargs): self._use_gpu_translations = None self._use_preallocated = None - self.allow_force = kwargs.get('allow_force', None) - self.debug = kwargs.get('debug', None) - self.enabled = kwargs.get('enabled', None) - self.pixel_ratio = kwargs.get('pixel_ratio', None) - self.series_threshold = kwargs.get('series_threshold', None) - self.use_gpu_translations = kwargs.get('use_gpu_translations', None) - self.use_preallocated = kwargs.get('use_preallocated', None) + self.allow_force = kwargs.get("allow_force", None) + self.chunk_size = kwargs.get("chunk_size", None) + self.debug = kwargs.get("debug", None) + self.enabled = kwargs.get("enabled", None) + self.pixel_ratio = kwargs.get("pixel_ratio", None) + self.series_threshold = kwargs.get("series_threshold", None) + self.use_gpu_translations = kwargs.get("use_gpu_translations", None) + self.use_preallocated = kwargs.get("use_preallocated", None) @property def _dot_path(self) -> Optional[str]: """The dot-notation path to the options key for the current class. - + :rtype: :class:`str ` or :obj:`None ` """ - return 'boost' + return "boost" @property def allow_force(self) -> Optional[bool]: @@ -219,6 +221,23 @@ def allow_force(self, value): else: self._allow_force = bool(value) + @property + def chunk_size(self) -> Optional[int]: + """The number of points processed per frame when building the k-d tree for boosted series. + + .. tip:: + + Lower values improve responsiveness but increase the time it takes to build the tree. + + :rtype: :class:`int ` or :obj:`None ` + + """ + return self._chunk_size + + @chunk_size.setter + def chunk_size(self, value): + self._chunk_size = validators.integer(value, allow_empty=True) + @property def debug(self) -> Optional[BoostDebug]: """Debugging options for boost. Useful for benchmarking, and general timing. @@ -280,9 +299,7 @@ def pixel_ratio(self) -> Optional[int]: @pixel_ratio.setter def pixel_ratio(self, value): - self._pixel_ratio = validators.integer(value, - allow_empty = True, - minimum = 0) + self._pixel_ratio = validators.integer(value, allow_empty=True, minimum=0) @property def series_threshold(self) -> Optional[int]: @@ -300,7 +317,7 @@ def series_threshold(self) -> Optional[int]: @series_threshold.setter def series_threshold(self, value): - self._series_threshold = validators.integer(value, allow_empty = True) + self._series_threshold = validators.integer(value, allow_empty=True) @property def use_gpu_translations(self) -> Optional[bool]: @@ -358,26 +375,28 @@ def use_preallocated(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'allow_force': as_dict.get('allowForce', None), - 'debug': as_dict.get('debug', None), - 'enabled': as_dict.get('enabled', None), - 'pixel_ratio': as_dict.get('pixelRatio', None), - 'series_threshold': as_dict.get('seriesThreshold', None), - 'use_gpu_translations': as_dict.get('useGPUTranslations', None), - 'use_preallocated': as_dict.get('usePreallocated', None), + "allow_force": as_dict.get("allowForce", None), + "chunk_size": as_dict.get("chunkSize", None), + "debug": as_dict.get("debug", None), + "enabled": as_dict.get("enabled", None), + "pixel_ratio": as_dict.get("pixelRatio", None), + "series_threshold": as_dict.get("seriesThreshold", None), + "use_gpu_translations": as_dict.get("useGPUTranslations", None), + "use_preallocated": as_dict.get("usePreallocated", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'allowForce': self.allow_force, - 'debug': self.debug, - 'enabled': self.enabled, - 'pixelRatio': self.pixel_ratio, - 'seriesThreshold': self.series_threshold, - 'useGPUTranslations': self.use_gpu_translations, - 'usePreallocated': self.use_preallocated + "allowForce": self.allow_force, + "chunkSize": self.chunk_size, + "debug": self.debug, + "enabled": self.enabled, + "pixelRatio": self.pixel_ratio, + "seriesThreshold": self.series_threshold, + "useGPUTranslations": self.use_gpu_translations, + "usePreallocated": self.use_preallocated, } return untrimmed From 27bfe30bddd9044d4f9033387c13fac7547e89d5 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 21:48:34 -0400 Subject: [PATCH 15/23] Added events support to Credits object. --- highcharts_core/options/credits.py | 79 ++-- highcharts_core/utility_classes/events.py | 467 +++++++++++----------- 2 files changed, 279 insertions(+), 267 deletions(-) diff --git a/highcharts_core/options/credits.py b/highcharts_core/options/credits.py index 719de05..d8a61e4 100644 --- a/highcharts_core/options/credits.py +++ b/highcharts_core/options/credits.py @@ -7,6 +7,7 @@ from highcharts_core.decorators import class_sensitive from highcharts_core.metaclasses import HighchartsMeta from highcharts_core.utility_classes.position import Position +from highcharts_core.utility_classes.events import CreditsEvents class CreditStyleOptions(HighchartsMeta): @@ -17,9 +18,9 @@ def __init__(self, **kwargs): self._cursor = None self._font_size = None - self.color = kwargs.get('color', None) - self.cursor = kwargs.get('cursor', None) - self.font_size = kwargs.get('font_size', None) + self.color = kwargs.get("color", None) + self.cursor = kwargs.get("cursor", None) + self.font_size = kwargs.get("font_size", None) @property def color(self) -> Optional[str]: @@ -31,7 +32,7 @@ def color(self) -> Optional[str]: @color.setter def color(self, value): - self._color = validators.string(value, allow_empty = True) + self._color = validators.string(value, allow_empty=True) @property def cursor(self) -> Optional[str]: @@ -43,7 +44,7 @@ def cursor(self) -> Optional[str]: @cursor.setter def cursor(self, value): - self._cursor = validators.string(value, allow_empty = True) + self._cursor = validators.string(value, allow_empty=True) @property def font_size(self) -> Optional[str]: @@ -55,23 +56,23 @@ def font_size(self) -> Optional[str]: @font_size.setter def font_size(self, value): - self._font_size = validators.string(value, allow_empty = True) + self._font_size = validators.string(value, allow_empty=True) @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'color': as_dict.get('color', None), - 'cursor': as_dict.get('cursor', None), - 'font_size': as_dict.get('fontSize', None) + "color": as_dict.get("color", None), + "cursor": as_dict.get("cursor", None), + "font_size": as_dict.get("fontSize", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'color': self.color, - 'cursor': self.cursor, - 'fontSize': self.font_size + "color": self.color, + "cursor": self.cursor, + "fontSize": self.font_size, } return untrimmed @@ -83,24 +84,26 @@ class Credits(HighchartsMeta): def __init__(self, **kwargs): self._enabled = None + self._events = None self._href = None self._position = None self._style = None self._text = None - self.enabled = kwargs.get('enabled', None) - self.href = kwargs.get('href', None) - self.position = kwargs.get('position', None) - self.style = kwargs.get('style', None) - self.text = kwargs.get('text', None) + self.enabled = kwargs.get("enabled", None) + self.events = kwargs.get("events", None) + self.href = kwargs.get("href", None) + self.position = kwargs.get("position", None) + self.style = kwargs.get("style", None) + self.text = kwargs.get("text", None) @property def _dot_path(self) -> Optional[str]: """The dot-notation path to the options key for the current class. - + :rtype: :class:`str ` or :obj:`None ` """ - return 'credits' + return "credits" @property def enabled(self) -> Optional[bool]: @@ -118,6 +121,16 @@ def enabled(self, value): else: self._enabled = bool(value) + @property + def events(self) -> Optional[CreditsEvents]: + """Events for the credits label.""" + return self._events + + @events.setter + @class_sensitive(CreditsEvents) + def events(self, value): + self._events = value + @property def href(self) -> Optional[str]: """The URL for the credits label. Defaults to @@ -184,27 +197,29 @@ def text(self) -> Optional[str]: @text.setter def text(self, value): - self._text = validators.string(value, allow_empty = True) + self._text = validators.string(value, allow_empty=True) @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'enabled': as_dict.get('enabled', None), - 'href': as_dict.get('href', None), - 'position': as_dict.get('position', None), - 'style': as_dict.get('style', None), - 'text': as_dict.get('text', None) + "enabled": as_dict.get("enabled", None), + "events": as_dict.get("events", None), + "href": as_dict.get("href", None), + "position": as_dict.get("position", None), + "style": as_dict.get("style", None), + "text": as_dict.get("text", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'enabled': self.enabled, - 'href': self.href, - 'position': self.position, - 'style': self.style, - 'text': self.text, + "enabled": self.enabled, + "events": self.events, + "href": self.href, + "position": self.position, + "style": self.style, + "text": self.text, } return untrimmed diff --git a/highcharts_core/utility_classes/events.py b/highcharts_core/utility_classes/events.py index 1146d06..bf301c3 100644 --- a/highcharts_core/utility_classes/events.py +++ b/highcharts_core/utility_classes/events.py @@ -27,17 +27,17 @@ def __init__(self, **kwargs): self._selection = None for attribute in dir(self): - if attribute.startswith('_') and not attribute.startswith('__'): + if attribute.startswith("_") and not attribute.startswith("__"): non_private_name = attribute[1:] setattr(self, non_private_name, kwargs.get(non_private_name, None)) @property def _dot_path(self) -> Optional[str]: """The dot-notation path to the options key for the current class. - + :rtype: :class:`str ` or :obj:`None ` """ - return 'chart.events' + return "chart.events" @property def add_series(self) -> Optional[CallbackFunction]: @@ -235,7 +235,7 @@ def load(self, value): @property def render(self) -> Optional[CallbackFunction]: - """JavaScript callback function that fires when the chart is initially loaded + """JavaScript callback function that fires when the chart is initially loaded (directly after the ``load`` event), and after each redraw (directly after the ``redraw`` event). :rtype: :class:`CallbackFunction` or :obj:`None ` @@ -302,40 +302,40 @@ def selection(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'add_series': as_dict.get('addSeries', None), - 'after_print': as_dict.get('afterPrint', None), - 'before_print': as_dict.get('beforePrint', None), - 'click': as_dict.get('click', None), - 'drilldown': as_dict.get('drilldown', None), - 'drillup': as_dict.get('drillup', None), - 'drillupall': as_dict.get('drillupall', None), - 'export_data': as_dict.get('exportData', None), - 'fullscreen_close': as_dict.get('fullscreenClose', None), - 'fullscreen_open': as_dict.get('fullscreenOpen', None), - 'load': as_dict.get('load', None), - 'redraw': as_dict.get('redraw', None), - 'render': as_dict.get('render', None), - 'selection': as_dict.get('selection', None) + "add_series": as_dict.get("addSeries", None), + "after_print": as_dict.get("afterPrint", None), + "before_print": as_dict.get("beforePrint", None), + "click": as_dict.get("click", None), + "drilldown": as_dict.get("drilldown", None), + "drillup": as_dict.get("drillup", None), + "drillupall": as_dict.get("drillupall", None), + "export_data": as_dict.get("exportData", None), + "fullscreen_close": as_dict.get("fullscreenClose", None), + "fullscreen_open": as_dict.get("fullscreenOpen", None), + "load": as_dict.get("load", None), + "redraw": as_dict.get("redraw", None), + "render": as_dict.get("render", None), + "selection": as_dict.get("selection", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'addSeries': self.add_series, - 'afterPrint': self.after_print, - 'beforePrint': self.before_print, - 'click': self.click, - 'drilldown': self.drilldown, - 'drillup': self.drillup, - 'drillupall': self.drillupall, - 'exportData': self.export_data, - 'fullscreenClose': self.fullscreen_close, - 'fullscreenOpen': self.fullscreen_open, - 'load': self.load, - 'redraw': self.redraw, - 'render': self.render, - 'selection': self.selection + "addSeries": self.add_series, + "afterPrint": self.after_print, + "beforePrint": self.before_print, + "click": self.click, + "drilldown": self.drilldown, + "drillup": self.drillup, + "drillupall": self.drillupall, + "exportData": self.export_data, + "fullscreenClose": self.fullscreen_close, + "fullscreenOpen": self.fullscreen_open, + "load": self.load, + "redraw": self.redraw, + "render": self.render, + "selection": self.selection, } return untrimmed @@ -348,17 +348,17 @@ def __init__(self, **kwargs): self._click = None for attribute in dir(self): - if attribute.startswith('_') and not attribute.startswith('__'): + if attribute.startswith("_") and not attribute.startswith("__"): non_private_name = attribute[1:] setattr(self, non_private_name, kwargs.get(non_private_name, None)) @property def _dot_path(self) -> Optional[str]: """The dot-notation path to the options key for the current class. - + :rtype: :class:`str ` or :obj:`None ` """ - return 'breadcrumb.events' + return "breadcrumb.events" @property def click(self) -> Optional[CallbackFunction]: @@ -387,20 +387,22 @@ def click(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): - kwargs = { - 'click': as_dict.get('click', None) - } + kwargs = {"click": as_dict.get("click", None)} return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: - untrimmed = { - 'click': self.click - } + def _to_untrimmed_dict(self, in_cls=None) -> dict: + untrimmed = {"click": self.click} return untrimmed +class CreditsEvents(BreadcrumbEvents): + """Event listeners for Credits.""" + + pass + + class NavigationEvents(HighchartsMeta): """Event listeners for the chart.""" @@ -411,17 +413,17 @@ def __init__(self, **kwargs): self._show_popup = None for attribute in dir(self): - if attribute.startswith('_') and not attribute.startswith('__'): + if attribute.startswith("_") and not attribute.startswith("__"): non_private_name = attribute[1:] setattr(self, non_private_name, kwargs.get(non_private_name, None)) @property def _dot_path(self) -> Optional[str]: """The dot-notation path to the options key for the current class. - + :rtype: :class:`str ` or :obj:`None ` """ - return 'navigation.events' + return "navigation.events" @property def close_popup(self) -> Optional[CallbackFunction]: @@ -480,20 +482,20 @@ def show_popup(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'close_popup': as_dict.get('closePopup', None), - 'deselect_button': as_dict.get('deselectButton', None), - 'select_button': as_dict.get('selectButton', None), - 'show_popup': as_dict.get('showPopup', None) + "close_popup": as_dict.get("closePopup", None), + "deselect_button": as_dict.get("deselectButton", None), + "select_button": as_dict.get("selectButton", None), + "show_popup": as_dict.get("showPopup", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'closePopup': self.close_popup, - 'deselectButton': self.deselect_button, - 'selectButton': self.select_button, - 'showPopup': self.show_popup + "closePopup": self.close_popup, + "deselectButton": self.deselect_button, + "selectButton": self.select_button, + "showPopup": self.show_popup, } return untrimmed @@ -514,24 +516,24 @@ def __init__(self, **kwargs): self._unselect = None self._update = None - self.click = kwargs.get('click', None) - self.drag = kwargs.get('drag', None) - self.drag_start = kwargs.get('drag_start', None) - self.drop = kwargs.get('drop', None) - self.mouse_out = kwargs.get('mouse_out', None) - self.mouse_over = kwargs.get('mouse_over', None) - self.remove = kwargs.get('remove', None) - self.select = kwargs.get('select', None) - self.unselect = kwargs.get('unselect', None) - self.update = kwargs.get('update', None) + self.click = kwargs.get("click", None) + self.drag = kwargs.get("drag", None) + self.drag_start = kwargs.get("drag_start", None) + self.drop = kwargs.get("drop", None) + self.mouse_out = kwargs.get("mouse_out", None) + self.mouse_over = kwargs.get("mouse_over", None) + self.remove = kwargs.get("remove", None) + self.select = kwargs.get("select", None) + self.unselect = kwargs.get("unselect", None) + self.update = kwargs.get("update", None) @property def _dot_path(self) -> Optional[str]: """The dot-notation path to the options key for the current class. - + :rtype: :class:`str ` or :obj:`None ` """ - return 'plotOptions.series.point.events' + return "plotOptions.series.point.events" @property def click(self) -> Optional[CallbackFunction]: @@ -710,32 +712,32 @@ def update(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'click': as_dict.get('click', None), - 'drag': as_dict.get('drag', None), - 'drag_start': as_dict.get('dragStart', None), - 'drop': as_dict.get('drop', None), - 'mouse_out': as_dict.get('mouseOut', None), - 'mouse_over': as_dict.get('mouseOver', None), - 'remove': as_dict.get('remove', None), - 'select': as_dict.get('select', None), - 'unselect': as_dict.get('unselect', None), - 'update': as_dict.get('update', None) + "click": as_dict.get("click", None), + "drag": as_dict.get("drag", None), + "drag_start": as_dict.get("dragStart", None), + "drop": as_dict.get("drop", None), + "mouse_out": as_dict.get("mouseOut", None), + "mouse_over": as_dict.get("mouseOver", None), + "remove": as_dict.get("remove", None), + "select": as_dict.get("select", None), + "unselect": as_dict.get("unselect", None), + "update": as_dict.get("update", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'click': self.click, - 'drag': self.drag, - 'dragStart': self.drag_start, - 'drop': self.drop, - 'mouseOut': self.mouse_out, - 'mouseOver': self.mouse_over, - 'remove': self.remove, - 'select': self.select, - 'unselect': self.unselect, - 'update': self.update + "click": self.click, + "drag": self.drag, + "dragStart": self.drag_start, + "drop": self.drop, + "mouseOut": self.mouse_out, + "mouseOver": self.mouse_over, + "remove": self.remove, + "select": self.select, + "unselect": self.unselect, + "update": self.update, } return untrimmed @@ -754,14 +756,14 @@ def __init__(self, **kwargs): self._mouse_over = None self._show = None - self.after_animate = kwargs.get('after_animate', None) - self.checkbox_click = kwargs.get('checkbox_click', None) - self.click = kwargs.get('click', None) - self.hide = kwargs.get('hide', None) - self.legend_item_click = kwargs.get('legend_item_click', None) - self.mouse_out = kwargs.get('mouse_out', None) - self.mouse_over = kwargs.get('mouse_over', None) - self.show = kwargs.get('show', None) + self.after_animate = kwargs.get("after_animate", None) + self.checkbox_click = kwargs.get("checkbox_click", None) + self.click = kwargs.get("click", None) + self.hide = kwargs.get("hide", None) + self.legend_item_click = kwargs.get("legend_item_click", None) + self.mouse_out = kwargs.get("mouse_out", None) + self.mouse_over = kwargs.get("mouse_over", None) + self.show = kwargs.get("show", None) @property def after_animate(self) -> Optional[CallbackFunction]: @@ -898,28 +900,28 @@ def show(self, 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) + "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), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + 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, - 'show': self.show + "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, + "show": self.show, } return untrimmed @@ -927,22 +929,22 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: 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) - + + 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 ` """ @@ -956,30 +958,29 @@ def after_simulation(self, 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), - - 'after_simulation': as_dict.get('afterSimulation', None), + "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: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'afterSimulation': self.after_simulation, + "afterSimulation": self.after_simulation, } - parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls) or {} + 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.""" @@ -987,7 +988,7 @@ class ClusterEvents(HighchartsMeta): def __init__(self, **kwargs): self._drill_to_cluster = None - self.drill_to_cluster = kwargs.get('drill_to_cluster', None) + self.drill_to_cluster = kwargs.get("drill_to_cluster", None) @property def drill_to_cluster(self) -> Optional[CallbackFunction]: @@ -1009,14 +1010,10 @@ def drill_to_cluster(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): - return { - 'drill_to_cluster': as_dict.get('drillToCluster', None) - } + return {"drill_to_cluster": as_dict.get("drillToCluster", None)} - def _to_untrimmed_dict(self, in_cls = None) -> dict: - untrimmed = { - 'drillToCluster': self.drill_to_cluster - } + def _to_untrimmed_dict(self, in_cls=None) -> dict: + untrimmed = {"drillToCluster": self.drill_to_cluster} return untrimmed @@ -1032,12 +1029,12 @@ def __init__(self, **kwargs): self._point_in_break = None self._set_extremes = None - self.after_breaks = kwargs.get('after_breaks', None) - self.after_set_extremes = kwargs.get('after_set_extremes', None) - self.point_break = kwargs.get('point_break', None) - self.point_break_out = kwargs.get('point_break_out', None) - self.point_in_break = kwargs.get('point_in_break', None) - self.set_extremes = kwargs.get('set_extremes', None) + self.after_breaks = kwargs.get("after_breaks", None) + self.after_set_extremes = kwargs.get("after_set_extremes", None) + self.point_break = kwargs.get("point_break", None) + self.point_break_out = kwargs.get("point_break_out", None) + self.point_in_break = kwargs.get("point_in_break", None) + self.set_extremes = kwargs.get("set_extremes", None) @property def after_breaks(self) -> Optional[CallbackFunction]: @@ -1095,12 +1092,12 @@ def point_break(self, value): @property def point_break_out(self) -> Optional[CallbackFunction]: """An event fired when a point is outside a break after zoom. - + :rtype: :class:`CallbackFunction ` or :obj:`None ` """ return self._point_break_out - + @point_break_out.setter def point_break_out(self, value): self._point_break_out = value @@ -1146,24 +1143,24 @@ def set_extremes(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'after_breaks': as_dict.get('afterBreaks', None), - 'after_set_extremes': as_dict.get('afterSetExtremes', None), - 'point_break': as_dict.get('pointBreak', None), - 'point_break_out': as_dict.get('pointBreakOut', None), - 'point_in_break': as_dict.get('pointInBreak', None), - 'set_extremes': as_dict.get('setExtremes', None) + "after_breaks": as_dict.get("afterBreaks", None), + "after_set_extremes": as_dict.get("afterSetExtremes", None), + "point_break": as_dict.get("pointBreak", None), + "point_break_out": as_dict.get("pointBreakOut", None), + "point_in_break": as_dict.get("pointInBreak", None), + "set_extremes": as_dict.get("setExtremes", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'afterBreaks': self.after_breaks, - 'afterSetExtremes': self.after_set_extremes, - 'pointBreak': self.point_break, - 'pointBreakOut': self.point_break_out, - 'pointInBreak': self.point_in_break, - 'setExtremes': self.set_extremes + "afterBreaks": self.after_breaks, + "afterSetExtremes": self.after_set_extremes, + "pointBreak": self.point_break, + "pointBreakOut": self.point_break_out, + "pointInBreak": self.point_in_break, + "setExtremes": self.set_extremes, } return untrimmed @@ -1178,10 +1175,10 @@ def __init__(self, **kwargs): self._mouseout = None self._mouseover = None - self.click = kwargs.get('click', None) - self.mousemove = kwargs.get('mousemove', None) - self.mouseout = kwargs.get('mouseout', None) - self.mouseover = kwargs.get('mouseover', None) + self.click = kwargs.get("click", None) + self.mousemove = kwargs.get("mousemove", None) + self.mouseout = kwargs.get("mouseout", None) + self.mouseover = kwargs.get("mouseover", None) @property def click(self) -> Optional[CallbackFunction]: @@ -1244,20 +1241,20 @@ def mouseover(self, value): @classmethod def _get_kwargs_from_dict(cls, as_dict): kwargs = { - 'click': as_dict.get('click', None), - 'mousemove': as_dict.get('mousemove', None), - 'mouseout': as_dict.get('mouseout', None), - 'mouseover': as_dict.get('mouseover', None) + "click": as_dict.get("click", None), + "mousemove": as_dict.get("mousemove", None), + "mouseout": as_dict.get("mouseout", None), + "mouseover": as_dict.get("mouseover", None), } return kwargs - def _to_untrimmed_dict(self, in_cls = None) -> dict: + def _to_untrimmed_dict(self, in_cls=None) -> dict: untrimmed = { - 'click': self.click, - 'mousemove': self.mousemove, - 'mouseout': self.mouseout, - 'mouseover': self.mouseover + "click": self.click, + "mousemove": self.mousemove, + "mouseout": self.mouseout, + "mouseover": self.mouseover, } return untrimmed @@ -1265,7 +1262,7 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict: class SonificationEvents(HighchartsMeta): """Event handlers for sonification.""" - + def __init__(self, **kwargs): self._after_update = None self._before_play = None @@ -1276,29 +1273,29 @@ def __init__(self, **kwargs): 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) + + 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): @@ -1307,14 +1304,14 @@ def after_update(self, 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): @@ -1324,14 +1321,14 @@ def before_play(self, value): 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): @@ -1341,16 +1338,16 @@ def before_update(self, value): 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 + ``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): @@ -1359,15 +1356,15 @@ def on_boundary_hit(self, 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): @@ -1376,14 +1373,14 @@ def on_end(self, 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): @@ -1392,14 +1389,14 @@ def on_play(self, 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): @@ -1408,14 +1405,14 @@ def on_series_end(self, 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): @@ -1425,15 +1422,15 @@ def on_series_start(self, value): 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): @@ -1442,30 +1439,30 @@ def on_stop(self, 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), + "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: + 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, + "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 From 8afba90589281eef770a3af8bd8dbd6284a23d4a Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 22:28:05 -0400 Subject: [PATCH 16/23] Updated changelog. --- CHANGES.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3e5c9e2..69c6399 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,22 @@ +Release 1.11.0 +========================================= + +* **ENHANCEMENT:** Align the API to **Highcharts (JS) v.12.6**. In particular, this includes: + + * Added ``Credits.events`` property. + * Added ``Boost.chunk_size`` property. + * Added ``Exporting.local`` property. + * Added non-Cartesian series zoom module. + * Added ``Tooltip.show_delay`` and ``CrosshairOptions.show_delay`` properties. + * Added ``Legend.max_width`` support. + * Added multiple new properties to Treegraph and Treemap series types, including: + ``headers``, ``group_padding``, ``node_size_by``, ``traverse_to_leaf``, and ``zoom_enabled``. + * Added ``Tooltip.fixed`` and ``Tooltip.position`` support. + +* **TESTS:** Added unit tests to confirm ``Chart.module_url`` support for local path. +* **ENHANCEMENT:** Updated dependencies and requirements to more-recent versions to address security patches. + +---- Release 1.10.3 ========================================= From 7437e6b112f696e728b0f420aff49dedda3dd23b Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 22:34:45 -0400 Subject: [PATCH 17/23] Updated test matrix. --- .travis.yml | 6 ++++++ tox.ini | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7e2a5ce..1360656 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,12 @@ job: - python: "3.11" dist: bionic env: TOXENV=py311 + - python: "3.12" + dist: bionic + env: TOXENV=py312 + - python: "3.13" + dist: bionic + env: TOXENV=py313 - python: "3.10" dist: focal env: TOXENV=coverage diff --git a/tox.ini b/tox.ini index f91ce5a..f21edff 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{310,311},no_numpy{310,311},docs,coverage +envlist = py{310,311,312,313},no_numpy{310,311,312,313},docs,coverage minversion = 4.4 [testenv] @@ -13,7 +13,7 @@ commands = [testenv:py] description = - py{310,311}: Run unit tests against {envname} + py{310,311,312,313}: Run unit tests against {envname} commands = {[testenv]commands} From 3bd502cc25fa4d072cb5ae380e07bc25f1672d45 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 22:34:54 -0400 Subject: [PATCH 18/23] Bumped 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 bc8f116..f84c53b 100644 --- a/highcharts_core/__version__.py +++ b/highcharts_core/__version__.py @@ -1 +1 @@ -__version__ = "1.10.3" +__version__ = "1.11.0" From 8dd1f25cbe11541dfe89d247caac7be05c8bf886 Mon Sep 17 00:00:00 2001 From: Chris Modzelewski Date: Mon, 18 May 2026 23:12:12 -0400 Subject: [PATCH 19/23] Updated ALLOWED_NONE_CONTEXTS and added missing non-cartesian zoom module. --- highcharts_core/constants.py | 1299 ++++++++++++++++++---------------- 1 file changed, 670 insertions(+), 629 deletions(-) diff --git a/highcharts_core/constants.py b/highcharts_core/constants.py index 86251e4..8931b46 100644 --- a/highcharts_core/constants.py +++ b/highcharts_core/constants.py @@ -1,7 +1,10 @@ """Defines a set of constants that are used throughout the library.""" + import os + try: from dotenv import load_dotenv + load_dotenv() except ImportError: pass @@ -16,9 +19,9 @@ except ImportError: import json -with open(os.path.join(os.path.dirname(__file__), - './module_requirements.json'), - 'r') as module_requirements: +with open( + os.path.join(os.path.dirname(__file__), "./module_requirements.json"), "r" +) as module_requirements: try: MODULE_REQUIREMENTS = json.load(module_requirements) except AttributeError: @@ -28,7 +31,7 @@ class EnforcedNullType: def __eq__(self, other): return isinstance(other, self.__class__) - + def __repr__(self): return "EnforcedNullType()" @@ -36,70 +39,81 @@ def __repr__(self): EnforcedNull = EnforcedNullType() -JAVASCRIPT_INDENT_SPACES = os.getenv('JAVASCRIPT_INDENT_SPACES') or 2 -JAVASCRIPT_INDENT = '' +JAVASCRIPT_INDENT_SPACES = os.getenv("JAVASCRIPT_INDENT_SPACES") or 2 +JAVASCRIPT_INDENT = "" indent_count = 1 while indent_count < int(JAVASCRIPT_INDENT_SPACES): - JAVASCRIPT_INDENT += ' ' + JAVASCRIPT_INDENT += " " indent_count += 1 -DEFAULT_COLORS = ["#7cb5ec", "#434348", "#90ed7d", "#f7a35c", "#8085e9", "#f15c80", - "#e4d354", "#2b908f", "#f45b5b", "#91e8e1"] +DEFAULT_COLORS = [ + "#7cb5ec", + "#434348", + "#90ed7d", + "#f7a35c", + "#8085e9", + "#f15c80", + "#e4d354", + "#2b908f", + "#f45b5b", + "#91e8e1", +] INCLUDE_LIBS = [ - 'https://code.highcharts.com/highcharts.js', - 'https://code.highcharts.com/highcharts-more.js', - 'https://code.highcharts.com/highcharts-3d.js', - 'https://code.highcharts.com/modules/sonification.js', - 'https://code.highcharts.com/modules/accessibility.js', - 'https://code.highcharts.com/modules/annotations.js', - 'https://code.highcharts.com/modules/annotations-advanced.js', - 'https://code.highcharts.com/modules/sankey.js', - 'https://code.highcharts.com/modules/arc-diagram.js', - 'https://code.highcharts.com/modules/boost.js', - 'https://code.highcharts.com/modules/broken-axis.js', - 'https://code.highcharts.com/modules/bullet.js', - 'https://code.highcharts.com/modules/cylinder.js', - 'https://code.highcharts.com/modules/data.js', - 'https://code.highcharts.com/modules/datagrouping.js', - 'https://code.highcharts.com/modules/debugger.js', - 'https://code.highcharts.com/modules/dependency-wheel.js', - 'https://code.highcharts.com/modules/drag-panes.js', - 'https://code.highcharts.com/modules/draggable-points.js', - 'https://code.highcharts.com/modules/drilldown.js', - 'https://code.highcharts.com/modules/dumbbell.js', - 'https://code.highcharts.com/modules/export-data.js', - 'https://code.highcharts.com/modules/exporting.js', - 'https://code.highcharts.com/modules/funnel.js', - 'https://code.highcharts.com/modules/funnel3d.js', - 'https://code.highcharts.com/modules/heatmap.js', - 'https://code.highcharts.com/modules/item-series.js', - 'https://code.highcharts.com/modules/lollipop.js', - 'https://code.highcharts.com/modules/networkgraph.js', - 'https://code.highcharts.com/modules/no-data-to-display.js', - 'https://code.highcharts.com/modules/offline-exporting.js', - 'https://code.highcharts.com/modules/oldie.js', - 'https://code.highcharts.com/modules/organization.js', - 'https://code.highcharts.com/modules/parallel-coordinates.js', - 'https://code.highcharts.com/modules/pareto.js', - 'https://code.highcharts.com/modules/pictorial.js', - 'https://code.highcharts.com/modules/pyramid3d.js', - 'https://code.highcharts.com/modules/series-label.js', - 'https://code.highcharts.com/modules/series-on-point.js', - 'https://code.highcharts.com/modules/solid-gauge.js', - 'https://code.highcharts.com/modules/streamgraph.js', - 'https://code.highcharts.com/modules/sunburst.js', - 'https://code.highcharts.com/modules/tilemap.js', - 'https://code.highcharts.com/modules/timeline.js', - 'https://code.highcharts.com/modules/treegraph.js', - 'https://code.highcharts.com/modules/treemap.js', - 'https://code.highcharts.com/modules/variable-pie.js', - 'https://code.highcharts.com/modules/variwide.js', - 'https://code.highcharts.com/modules/vector.js', - 'https://code.highcharts.com/modules/venn.js', - 'https://code.highcharts.com/modules/windbarb.js', - 'https://code.highcharts.com/modules/wordcloud.js', - 'https://code.highcharts.com/modules/xrange.js', + "https://code.highcharts.com/highcharts.js", + "https://code.highcharts.com/highcharts-more.js", + "https://code.highcharts.com/highcharts-3d.js", + "https://code.highcharts.com/modules/sonification.js", + "https://code.highcharts.com/modules/accessibility.js", + "https://code.highcharts.com/modules/annotations.js", + "https://code.highcharts.com/modules/annotations-advanced.js", + "https://code.highcharts.com/modules/sankey.js", + "https://code.highcharts.com/modules/arc-diagram.js", + "https://code.highcharts.com/modules/boost.js", + "https://code.highcharts.com/modules/broken-axis.js", + "https://code.highcharts.com/modules/bullet.js", + "https://code.highcharts.com/modules/cylinder.js", + "https://code.highcharts.com/modules/data.js", + "https://code.highcharts.com/modules/datagrouping.js", + "https://code.highcharts.com/modules/debugger.js", + "https://code.highcharts.com/modules/dependency-wheel.js", + "https://code.highcharts.com/modules/drag-panes.js", + "https://code.highcharts.com/modules/draggable-points.js", + "https://code.highcharts.com/modules/drilldown.js", + "https://code.highcharts.com/modules/dumbbell.js", + "https://code.highcharts.com/modules/export-data.js", + "https://code.highcharts.com/modules/exporting.js", + "https://code.highcharts.com/modules/funnel.js", + "https://code.highcharts.com/modules/funnel3d.js", + "https://code.highcharts.com/modules/heatmap.js", + "https://code.highcharts.com/modules/item-series.js", + "https://code.highcharts.com/modules/lollipop.js", + "https://code.highcharts.com/modules/networkgraph.js", + "https://code.highcharts.com/modules/non-cartesian-zoom.js", + "https://code.highcharts.com/modules/no-data-to-display.js", + "https://code.highcharts.com/modules/offline-exporting.js", + "https://code.highcharts.com/modules/oldie.js", + "https://code.highcharts.com/modules/organization.js", + "https://code.highcharts.com/modules/parallel-coordinates.js", + "https://code.highcharts.com/modules/pareto.js", + "https://code.highcharts.com/modules/pictorial.js", + "https://code.highcharts.com/modules/pyramid3d.js", + "https://code.highcharts.com/modules/series-label.js", + "https://code.highcharts.com/modules/series-on-point.js", + "https://code.highcharts.com/modules/solid-gauge.js", + "https://code.highcharts.com/modules/streamgraph.js", + "https://code.highcharts.com/modules/sunburst.js", + "https://code.highcharts.com/modules/tilemap.js", + "https://code.highcharts.com/modules/timeline.js", + "https://code.highcharts.com/modules/treegraph.js", + "https://code.highcharts.com/modules/treemap.js", + "https://code.highcharts.com/modules/variable-pie.js", + "https://code.highcharts.com/modules/variwide.js", + "https://code.highcharts.com/modules/vector.js", + "https://code.highcharts.com/modules/venn.js", + "https://code.highcharts.com/modules/windbarb.js", + "https://code.highcharts.com/modules/wordcloud.js", + "https://code.highcharts.com/modules/xrange.js", ] INCLUDE_STR = """ @@ -134,6 +148,7 @@ def __repr__(self):