Skip to content

Commit 2dac732

Browse files
authored
Merge pull request #155 from highcharts-for-python/bugfix/148-nodeformat-not-working-for-sankey-diagram
Bugfix/148 nodeformat not working for sankey diagram. Closes #148
2 parents 0955834 + 602a238 commit 2dac732

File tree

8 files changed

+306
-2
lines changed

8 files changed

+306
-2
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Release 1.6.0
33
=========================================
44

5+
* **BUGFIX:** Added support for ``nodeFormat`` and ``nodeFormatter`` to tooltip properties for
6+
diagram series (Organization, Dependency Wheel, and Sankey). (#148)
57
* **ENHANCEMENT:** Added ability to remove or override the JavaScript event listener when
68
serializing a chart to a JavaScript literal (#131 - courtesy of
79
`@ByronCook <https://github.com/ByronCook>`__ ).

highcharts_core/options/plot_options/arcdiagram.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from highcharts_core.utility_classes.gradients import Gradient
1010
from highcharts_core.utility_classes.patterns import Pattern
1111
from highcharts_core.options.plot_options.levels import LevelOptions
12+
from highcharts_core.options.tooltips import DiagramTooltip
1213

1314

1415
class ArcDiagramOptions(GenericTypeOptions):
@@ -250,6 +251,21 @@ def reversed(self, value):
250251
else:
251252
self._reversed = bool(value)
252253

254+
@property
255+
def tooltip(self) -> Optional[DiagramTooltip]:
256+
"""A configuration object for the tooltip rendering of each single series.
257+
Properties are inherited from tooltip, but only the following properties can be
258+
defined on a series level.
259+
260+
:rtype: :class:`DiagramTooltip <highcharts_core.options.tooltips.DiagramTooltip` or :obj:`None <python:None>`
261+
"""
262+
return self._tooltip
263+
264+
@tooltip.setter
265+
@class_sensitive(DiagramTooltip)
266+
def tooltip(self, value):
267+
self._tooltip = value
268+
253269
@classmethod
254270
def _get_kwargs_from_dict(cls, as_dict):
255271
kwargs = {

highcharts_core/options/plot_options/dependencywheel.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from highcharts_core.utility_classes.gradients import Gradient
1010
from highcharts_core.utility_classes.patterns import Pattern
1111
from highcharts_core.options.plot_options.levels import LevelOptions
12+
from highcharts_core.options.tooltips import DiagramTooltip
1213

1314

1415
class DependencyWheelOptions(GenericTypeOptions):
@@ -306,6 +307,21 @@ def start_angle(self, value):
306307
minimum = 0,
307308
maximum = 360)
308309

310+
@property
311+
def tooltip(self) -> Optional[DiagramTooltip]:
312+
"""A configuration object for the tooltip rendering of each single series.
313+
Properties are inherited from tooltip, but only the following properties can be
314+
defined on a series level.
315+
316+
:rtype: :class:`DiagramTooltip <highcharts_core.options.tooltips.DiagramTooltip` or :obj:`None <python:None>`
317+
"""
318+
return self._tooltip
319+
320+
@tooltip.setter
321+
@class_sensitive(DiagramTooltip)
322+
def tooltip(self, value):
323+
self._tooltip = value
324+
309325
@classmethod
310326
def _get_kwargs_from_dict(cls, as_dict):
311327
kwargs = {

highcharts_core/options/plot_options/organization.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from highcharts_core.options.plot_options.bar import BarOptions
99
from highcharts_core.options.plot_options.levels import LevelOptions
1010
from highcharts_core.utility_classes.data_labels import OrganizationDataLabel
11+
from highcharts_core.options.tooltips import DiagramTooltip
1112

1213

1314
class OrganizationOptions(BarOptions):
@@ -270,6 +271,21 @@ def node_width(self) -> Optional[int | float | Decimal]:
270271
def node_width(self, value):
271272
self._node_width = validators.numeric(value, allow_empty = True)
272273

274+
@property
275+
def tooltip(self) -> Optional[DiagramTooltip]:
276+
"""A configuration object for the tooltip rendering of each single series.
277+
Properties are inherited from tooltip, but only the following properties can be
278+
defined on a series level.
279+
280+
:rtype: :class:`DiagramTooltip <highcharts_core.options.tooltips.DiagramTooltip` or :obj:`None <python:None>`
281+
"""
282+
return self._tooltip
283+
284+
@tooltip.setter
285+
@class_sensitive(DiagramTooltip)
286+
def tooltip(self, value):
287+
self._tooltip = value
288+
273289
@classmethod
274290
def _get_kwargs_from_dict(cls, as_dict):
275291
kwargs = {

highcharts_core/options/tooltips.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,3 +970,100 @@ def _to_untrimmed_dict(self, in_cls = None) -> dict:
970970
}
971971

972972
return untrimmed
973+
974+
975+
class DiagramTooltip(Tooltip):
976+
"""Options for tooltips in diagram series, like :class:`DependencyWheelSeries <highcharts_core.options.series.dependencywheel.DependencyWheelSeries>` or :class:`SankeySeries <highcharts_core.options.series.sankey.SankeySeries>`."""
977+
978+
def __init__(self, **kwargs):
979+
self._node_format = None
980+
self._node_formatter = None
981+
982+
self.node_format = kwargs.get('node_format', None)
983+
self.node_formatter = kwargs.get('node_formatter', None)
984+
985+
super().__init__(**kwargs)
986+
987+
@property
988+
def node_format(self) -> Optional[str]:
989+
"""The format string specifying what to show for nodes in the tooltip of a diagram series, as opposed to links.
990+
991+
:rtype: :class:`str <python:str>` or :obj:`None <python:None>`
992+
"""
993+
return self._node_format
994+
995+
@node_format.setter
996+
def node_format(self, value):
997+
self._node_format = validators.string(value, allow_empty = True)
998+
999+
@property
1000+
def node_formatter(self) -> Optional[CallbackFunction]:
1001+
"""JavaScript callback function to format the text of the tooltip's point line.
1002+
1003+
:rtype: :class:`CallbackFunction` or :obj:`None <python:None>`
1004+
"""
1005+
return self._node_formatter
1006+
1007+
@node_formatter.setter
1008+
@class_sensitive(CallbackFunction)
1009+
def node_formatter(self, value):
1010+
self._node_formatter = value
1011+
1012+
@classmethod
1013+
def _get_kwargs_from_dict(cls, as_dict):
1014+
kwargs = {
1015+
"animation": as_dict.get("animation", None),
1016+
"background_color": as_dict.get("backgroundColor", None),
1017+
"border_color": as_dict.get("borderColor", None),
1018+
"border_radius": as_dict.get("borderRadius", None),
1019+
"border_width": as_dict.get("borderWidth", None),
1020+
"class_name": as_dict.get("className", None),
1021+
"cluster_format": as_dict.get("clusterFormat", None),
1022+
"date_time_label_formats": as_dict.get("dateTimeLabelFormats", None),
1023+
"distance": as_dict.get("distance", None),
1024+
"enabled": as_dict.get("enabled", None),
1025+
"follow_pointer": as_dict.get("followPointer", None),
1026+
"follow_touch_move": as_dict.get("followTouchMove", None),
1027+
"footer_format": as_dict.get("footerFormat", None),
1028+
"format": as_dict.get("format", None),
1029+
"formatter": as_dict.get("formatter", None),
1030+
"header_format": as_dict.get("headerFormat", None),
1031+
"header_shape": as_dict.get("headerShape", None),
1032+
"hide_delay": as_dict.get("hideDelay", None),
1033+
"null_format": as_dict.get("nullFormat", None),
1034+
"null_formatter": as_dict.get("nullFormatter", None),
1035+
"outside": as_dict.get("outside", None),
1036+
"padding": as_dict.get("padding", None),
1037+
"point_format": as_dict.get("pointFormat", None),
1038+
"point_formatter": as_dict.get("pointFormatter", None),
1039+
"positioner": as_dict.get("positioner", None),
1040+
"shadow": as_dict.get("shadow", None),
1041+
"shape": as_dict.get("shape", None),
1042+
"shared": as_dict.get("shared", None),
1043+
"snap": as_dict.get("snap", None),
1044+
"split": as_dict.get("split", None),
1045+
"stick_on_contact": as_dict.get("stickOnContact", None),
1046+
"style": as_dict.get("style", None),
1047+
"use_html": as_dict.get("useHTML", None),
1048+
"value_decimals": as_dict.get("valueDecimals", None),
1049+
"value_prefix": as_dict.get("valuePrefix", None),
1050+
"value_suffix": as_dict.get("valueSuffix", None),
1051+
"x_date_format": as_dict.get("xDateFormat", None),
1052+
1053+
"node_format": as_dict.get('nodeFormat', None),
1054+
'node_formatter': as_dict.get('nodeFormatter', None)
1055+
}
1056+
1057+
return kwargs
1058+
1059+
def _to_untrimmed_dict(self, in_cls=None) -> dict:
1060+
untrimmed = {
1061+
'nodeFormat': self.node_format,
1062+
'nodeFormatter': self.node_formatter,
1063+
}
1064+
1065+
parent_as_dict = super()._to_untrimmed_dict(in_cls=in_cls) or {}
1066+
for key in parent_as_dict:
1067+
untrimmed[key] = parent_as_dict[key]
1068+
1069+
return untrimmed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
nodeFormat: 'Node: {point.name}',
3+
borderRadius: 3,
4+
borderWidth: 44,
5+
valueDecimals: 2,
6+
valuePrefix: '$',
7+
valueSuffix: ' USD'
8+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
nodeFormatter: function () {
3+
// The first returned item is the header, subsequent items are the
4+
// points
5+
return ['<b>' + this.x + '</b>'].concat(
6+
this.points ?
7+
this.points.map(function (point) {
8+
return point.series.name + ': ' + point.y + 'm';
9+
}) : []
10+
);
11+
}
12+
}

tests/options/test_tooltips.py

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
from validator_collection import checkers
66

7-
from highcharts_core.options.tooltips import Tooltip as cls
7+
from highcharts_core.options.tooltips import (Tooltip as cls,
8+
DiagramTooltip as cls2)
89
from highcharts_core.utility_classes.javascript_functions import CallbackFunction
910
from highcharts_core import errors
1011
from tests.fixtures import input_files, check_input_file, to_camelCase, to_js_dict, \
@@ -144,4 +145,140 @@ def test_bug130_tooltip_serialization():
144145
assert obj.format == '{point.name}: {point.y}'
145146

146147
result = obj.to_js_literal()
147-
assert "'{point.name}: {point.y}'" in result or '"{point.name}: {point.y}"' in result
148+
assert "'{point.name}: {point.y}'" in result or '"{point.name}: {point.y}"' in result
149+
150+
#### DIAGRAM TOOLTIP TESTS
151+
152+
STANDARD_PARAMS_2 = [
153+
({}, None),
154+
({
155+
'node_format': 'format string',
156+
'node_formatter': """function() { return true; }""",
157+
}, None),
158+
(
159+
{
160+
"animation": True,
161+
"background_color": "#ccc",
162+
"border_color": "#999",
163+
"border_radius": 4,
164+
"border_width": 1,
165+
"class_name": "some-class-name",
166+
"cluster_format": "format string",
167+
"date_time_label_formats": {
168+
"day": "test",
169+
"hour": "test",
170+
"millisecond": "test",
171+
"minute": "test",
172+
"month": "test",
173+
"second": "test",
174+
"week": "test",
175+
"year": "test",
176+
},
177+
"distance": 12,
178+
"enabled": True,
179+
"follow_pointer": True,
180+
"follow_touch_move": True,
181+
"footer_format": "format string",
182+
"formatter": """function() { return true; }""",
183+
"header_format": "format string",
184+
"header_shape": "circle",
185+
"hide_delay": 3,
186+
"null_format": "format string",
187+
"null_formatter": """function() { return true; }""",
188+
"outside": False,
189+
"padding": 6,
190+
"point_format": "format string",
191+
"point_formatter": """function() { return true; }""",
192+
"positioner": """function() { return true; }""",
193+
"shadow": False,
194+
"shape": "rect",
195+
"shared": False,
196+
"snap": 4,
197+
"split": False,
198+
"stick_on_contact": True,
199+
"style": "style string goes here",
200+
"use_html": False,
201+
"value_decimals": 2,
202+
"value_prefix": "$",
203+
"value_suffix": " USD",
204+
"x_date_format": "format string",
205+
},
206+
None,
207+
),
208+
(
209+
{
210+
"formatter": CallbackFunction.from_js_literal("""function () {
211+
// The first returned item is the header, subsequent items are the
212+
// points
213+
return ['<b>' + this.x + '</b>'].concat(
214+
this.points ?
215+
this.points.map(function (point) {
216+
return point.series.name + ': ' + point.y + 'm';
217+
}) : []
218+
);
219+
}"""),
220+
"split": True,
221+
},
222+
None,
223+
),
224+
(
225+
{
226+
"formatter": """function () {
227+
// The first returned item is the header, subsequent items are the
228+
// points
229+
return ['<b>' + this.x + '</b>'].concat(
230+
this.points ?
231+
this.points.map(function (point) {
232+
return point.series.name + ': ' + point.y + 'm';
233+
}) : []
234+
);
235+
}""",
236+
"split": True,
237+
},
238+
None,
239+
),
240+
({"format": "{point.name} {point.y}"}, None),
241+
({"border_width": "not-a-number"}, (ValueError, TypeError)),
242+
]
243+
244+
245+
@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS_2)
246+
def test_DiagramTooltip__init__(kwargs, error):
247+
Class__init__(cls2, kwargs, error)
248+
249+
250+
@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS_2)
251+
def test_DiagramTooltip__to_untrimmed_dict(kwargs, error):
252+
Class__to_untrimmed_dict(cls2, kwargs, error)
253+
254+
255+
@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS_2)
256+
def test_DiagramTooltip_from_dict(kwargs, error):
257+
Class_from_dict(cls2, kwargs, error)
258+
259+
260+
@pytest.mark.parametrize('kwargs, error', STANDARD_PARAMS_2)
261+
def test_DiagramTooltip_to_dict(kwargs, error):
262+
Class_to_dict(cls2, kwargs, error)
263+
264+
265+
@pytest.mark.parametrize('filename, as_file, error', [
266+
('tooltips/01.js', False, None),
267+
('tooltips/02.js', False, None),
268+
('tooltips/03-diagram.js', False, None),
269+
('tooltips/04-diagram.js', False, None),
270+
271+
('tooltips/error-01.js', False, (TypeError, ValueError)),
272+
('tooltips/error-02.js', False, errors.HighchartsParseError),
273+
274+
('tooltips/01.js', True, None),
275+
('tooltips/02.js', True, None),
276+
('tooltips/03-diagram.js', True, None),
277+
('tooltips/04-diagram.js', False, None),
278+
279+
('tooltips/error-01.js', True, (TypeError, ValueError)),
280+
('tooltips/error-02.js', True, errors.HighchartsParseError),
281+
])
282+
def test_DiagramTooltip_from_js_literal(input_files, filename, as_file, error):
283+
Class_from_js_literal(cls2, input_files, filename, as_file, error)
284+

0 commit comments

Comments
 (0)