Skip to content

Commit 248e46f

Browse files
Introduce on_component_click for ui.echart (zauberzeug#5578)
### Motivation Fix zauberzeug#5576, where in the case of clicking indicator label, all we have in `e.args` is key `componentType` and `name`, causing library code to throw an error. But zauberzeug#5577 no good because it shoehorns the component click into point click handler, which has a risk of breaking downstream user code expecteding to take a point from the handler args. ### Implementation We have to segregate: - All mandatory keys available for point click? Go to `on_point_click` handler with `EChartPointClickEventArguments` - Any else case, go to `on_component_click` handler with `EChartComponentClickEventArguments` ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] Pytests have been added (or are not necessary). - [x] Documentation has been added (or is not necessary). --------- Co-authored-by: Falko Schindler <[email protected]>
1 parent bad0c1e commit 248e46f

File tree

5 files changed

+96
-6
lines changed

5 files changed

+96
-6
lines changed

nicegui/elements/echart/echart.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default {
2222
}
2323

2424
this.chart = echarts.init(this.$el, theme_name, { renderer: this.renderer });
25-
this.chart.on("click", (e) => this.$emit("pointClick", e));
25+
this.chart.on("click", (e) => this.$emit("componentClick", e));
2626
for (const event of [
2727
"click",
2828
"dblclick",

nicegui/elements/echart/echart.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
from ... import optional_features
66
from ...awaitable_response import AwaitableResponse
77
from ...element import Element
8-
from ...events import EChartPointClickEventArguments, GenericEventArguments, Handler, handle_event
8+
from ...events import (
9+
EChartComponentClickEventArguments,
10+
EChartPointClickEventArguments,
11+
GenericEventArguments,
12+
Handler,
13+
handle_event,
14+
)
915

1016
try:
1117
from pyecharts.charts.base import default, json
@@ -22,6 +28,7 @@ class EChart(Element, component='echart.js', esm={'nicegui-echart': 'dist'}, def
2228
def __init__(self,
2329
options: dict,
2430
on_point_click: Optional[Handler[EChartPointClickEventArguments]] = None, *,
31+
on_click: Optional[Handler[EChartComponentClickEventArguments]] = None,
2532
enable_3d: bool = False,
2633
renderer: Literal['canvas', 'svg'] = 'canvas',
2734
theme: Optional[Union[str, dict]] = None,
@@ -32,7 +39,8 @@ def __init__(self,
3239
Updates can be pushed to the chart by changing the `options` property.
3340
3441
:param options: dictionary of EChart options
35-
:param on_click_point: callback that is invoked when a point is clicked
42+
:param on_point_click: callback that is invoked when a point is clicked
43+
:param on_click: callback that is invoked when any component is clicked (*added in version 3.5.0*)
3644
:param enable_3d: enforce importing the echarts-gl library
3745
:param renderer: renderer to use ("canvas" or "svg", *added in version 2.7.0*)
3846
:param theme: an EChart theme configuration (dictionary or a URL returning a JSON object, *added in version 2.15.0*)
@@ -46,6 +54,8 @@ def __init__(self,
4654

4755
if on_point_click:
4856
self.on_point_click(on_point_click)
57+
if on_click:
58+
self.on_click(on_click)
4959

5060
def on_point_click(self, callback: Handler[EChartPointClickEventArguments]) -> Self:
5161
"""Add a callback to be invoked when a point is clicked."""
@@ -65,7 +75,7 @@ def handle_point_click(e: GenericEventArguments) -> None:
6575
data_type=e.args.get('dataType'),
6676
value=e.args['value'],
6777
))
68-
self.on('pointClick', handle_point_click, [
78+
self.on('componentClick', handle_point_click, [
6979
'componentType',
7080
'seriesType',
7181
'seriesIndex',
@@ -78,6 +88,21 @@ def handle_point_click(e: GenericEventArguments) -> None:
7888
])
7989
return self
8090

91+
def on_click(self, callback: Handler[EChartComponentClickEventArguments]) -> Self:
92+
"""Add a callback to be invoked when any component is clicked."""
93+
def handle_click(e: GenericEventArguments) -> None:
94+
handle_event(callback, EChartComponentClickEventArguments(
95+
sender=self,
96+
client=self.client,
97+
component_type=e.args['componentType'],
98+
name=e.args.get('name'),
99+
))
100+
self.on('componentClick', handle_click, [
101+
'componentType',
102+
'name',
103+
])
104+
return self
105+
81106
@classmethod
82107
def from_pyecharts(cls, chart: 'Chart', on_point_click: Optional[Callable] = None) -> Self:
83108
"""Create an echart element from a pyecharts object.

nicegui/events.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,22 @@ class SlideEventArguments(UiEventArguments):
5050
side: SlideSide
5151

5252

53+
@dataclass(**KWONLY_SLOTS)
54+
class EChartComponentClickEventArguments(UiEventArguments):
55+
component_type: str
56+
name: str | None
57+
58+
5359
@dataclass(**KWONLY_SLOTS)
5460
class EChartPointClickEventArguments(UiEventArguments):
5561
component_type: str
62+
name: str
5663
series_type: str
5764
series_index: int
5865
series_name: str
59-
name: str
6066
data_index: int
6167
data: float | int | str
62-
data_type: str
68+
data_type: str | None
6369
value: float | int | list
6470

6571

tests/test_echart.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,37 @@ def page():
170170

171171
screen.open('/')
172172
assert screen.find_by_tag('rect').value_of_css_property('fill') == 'rgb(254, 248, 239)'
173+
174+
175+
def test_click(screen: Screen):
176+
events = []
177+
178+
@ui.page('/')
179+
def page():
180+
ui.echart({
181+
'legend': {
182+
'triggerEvent': True,
183+
},
184+
'radar': {
185+
'triggerEvent': True,
186+
'indicator': [{'name': name, 'max': 100} for name in ['A', 'B', 'C']],
187+
},
188+
'series': [{
189+
'type': 'radar',
190+
'data': [{'name': 'Test', 'value': [77.0, 50.0, 90.0]}],
191+
'animation': False,
192+
}],
193+
}, on_point_click=lambda e: events.append(('point', e)), on_click=lambda e: events.append(('component', e))) \
194+
.style('width: 200px; height: 200px')
195+
196+
screen.open('/')
197+
echart = screen.find_by_tag('canvas')
198+
for x, y in [(20, 20), (0, 70), (60, 30)]:
199+
screen.click_at_position(echart, x, y)
200+
screen.wait(0.5)
201+
assert [(source, event.component_type, event.name) for source, event in events] == [
202+
('point', 'series', 'Test'),
203+
('component', 'series', 'Test'),
204+
('component', 'legend', None),
205+
('component', 'radar', 'C'),
206+
]

website/documentation/content/echart_documentation.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,31 @@ def clickable_points() -> None:
3434
}, on_point_click=ui.notify)
3535

3636

37+
@doc.demo('EChart with clickable components', '''
38+
Besides series points, you can register a callback for an event
39+
when any component registered with `triggerEvent=True` is clicked.
40+
41+
Hint: Check if that component is a point by checking `e.component_type == 'series'`
42+
to avoid double-processing with `on_point_click`.
43+
44+
*Added in version 3.5.0*
45+
''')
46+
def clickable_components() -> None:
47+
ui.echart({
48+
'legend': {
49+
'triggerEvent': True,
50+
},
51+
'radar': {
52+
'triggerEvent': True,
53+
'indicator': [{'name': name, 'max': 100} for name in ['A', 'B', 'C']],
54+
},
55+
'series': [{
56+
'type': 'radar',
57+
'data': [{'name': 'Test', 'value': [77.0, 50.0, 90.0]}],
58+
}],
59+
}, on_click=ui.notify)
60+
61+
3762
@doc.demo('EChart with dynamic properties', '''
3863
Dynamic properties can be passed to chart elements to customize them such as apply an axis label format.
3964
To make a property dynamic, prefix a colon ":" to the property name.

0 commit comments

Comments
 (0)