ex4nicegui 0.2.18__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ex4nicegui/__init__.py +1 -1
- ex4nicegui/bi/__init__.py +13 -0
- ex4nicegui/bi/dataSource.py +203 -0
- ex4nicegui/bi/dataSourceFacade.py +148 -0
- ex4nicegui/bi/elements/__init__.py +0 -0
- ex4nicegui/bi/elements/containers.py +13 -0
- ex4nicegui/bi/elements/layouts.py +28 -0
- ex4nicegui/bi/elements/models.py +55 -0
- ex4nicegui/bi/elements/text.py +33 -0
- ex4nicegui/bi/elements/ui_aggrid.py +46 -0
- ex4nicegui/bi/elements/ui_date_picker.js +35 -0
- ex4nicegui/bi/elements/ui_date_picker.py +77 -0
- ex4nicegui/bi/elements/ui_echarts.py +72 -0
- ex4nicegui/bi/elements/ui_radio.py +60 -0
- ex4nicegui/bi/elements/ui_range.py +119 -0
- ex4nicegui/bi/elements/ui_select.py +105 -0
- ex4nicegui/bi/elements/ui_slider.py +59 -0
- ex4nicegui/bi/index.py +76 -0
- ex4nicegui/bi/protocols.py +158 -0
- ex4nicegui/bi/types.py +13 -0
- ex4nicegui/reactive/EChartsComponent/ECharts.js +1 -2
- ex4nicegui/reactive/EChartsComponent/ECharts.py +1 -0
- ex4nicegui/reactive/officials/base.py +12 -1
- ex4nicegui/reactive/officials/button.py +1 -1
- ex4nicegui/reactive/officials/drawer.py +1 -2
- ex4nicegui/reactive/officials/echarts.py +1 -1
- ex4nicegui/utils/clientScope.py +42 -0
- ex4nicegui/utils/signals.py +54 -5
- {ex4nicegui-0.2.18.dist-info → ex4nicegui-0.3.1.dist-info}/METADATA +2 -2
- {ex4nicegui-0.2.18.dist-info → ex4nicegui-0.3.1.dist-info}/RECORD +33 -13
- {ex4nicegui-0.2.18.dist-info → ex4nicegui-0.3.1.dist-info}/LICENSE +0 -0
- {ex4nicegui-0.2.18.dist-info → ex4nicegui-0.3.1.dist-info}/WHEEL +0 -0
- {ex4nicegui-0.2.18.dist-info → ex4nicegui-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, cast
|
|
3
|
+
from nicegui.events import UiEventArguments
|
|
4
|
+
from ex4nicegui.reactive import rxui
|
|
5
|
+
from ex4nicegui.reactive.EChartsComponent.ECharts import (
|
|
6
|
+
EChartsClickEventArguments,
|
|
7
|
+
echarts,
|
|
8
|
+
)
|
|
9
|
+
from nicegui import ui
|
|
10
|
+
from ex4nicegui.bi.dataSource import DataSource
|
|
11
|
+
from .models import UiResult
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ex4nicegui.bi.dataSourceFacade import DataSourceFacade
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EChartsResult(UiResult[echarts]):
|
|
18
|
+
def __init__(
|
|
19
|
+
self, element: echarts, dataSource: DataSource, chart_update: Callable
|
|
20
|
+
) -> None:
|
|
21
|
+
super().__init__(element, dataSource)
|
|
22
|
+
self.chart_update = chart_update
|
|
23
|
+
|
|
24
|
+
def on_chart_click(
|
|
25
|
+
self, handler: Optional[Callable[[EChartsClickEventArguments], Any]]
|
|
26
|
+
):
|
|
27
|
+
return self.element.on_chart_click(handler)
|
|
28
|
+
|
|
29
|
+
def on_chart_click_blank(
|
|
30
|
+
self, handler: Optional[Callable[[UiEventArguments], Any]]
|
|
31
|
+
):
|
|
32
|
+
return self.element.on_chart_click_blank(handler)
|
|
33
|
+
|
|
34
|
+
def cancel_linkage(self, *source: Union[ui.element, "UiResult"]):
|
|
35
|
+
super().cancel_linkage(*source)
|
|
36
|
+
self.element.update_options(
|
|
37
|
+
self.chart_update(self._dataSource.get_filtered_data(self.element))
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def ui_echarts(
|
|
42
|
+
self: DataSourceFacade,
|
|
43
|
+
fn: Callable[[Any], Union[Dict, "pyecharts.Base"]], # pyright: ignore
|
|
44
|
+
**kwargs,
|
|
45
|
+
):
|
|
46
|
+
def create_options(data):
|
|
47
|
+
options = fn(data)
|
|
48
|
+
if isinstance(options, Dict):
|
|
49
|
+
return options
|
|
50
|
+
|
|
51
|
+
import simplejson as json
|
|
52
|
+
from pyecharts.charts.chart import Base
|
|
53
|
+
|
|
54
|
+
if isinstance(options, Base):
|
|
55
|
+
return cast(Dict, json.loads(options.dump_options()))
|
|
56
|
+
|
|
57
|
+
raise TypeError(f"not support options type[{type(options)}]")
|
|
58
|
+
|
|
59
|
+
cp = rxui.echarts({})
|
|
60
|
+
ele_id = cp.element.id
|
|
61
|
+
|
|
62
|
+
def on_source_update():
|
|
63
|
+
data = self._dataSource.get_filtered_data(cp.element)
|
|
64
|
+
options = create_options(data)
|
|
65
|
+
cp.element.update_options(options)
|
|
66
|
+
|
|
67
|
+
info = self._dataSource._register_component(ele_id, on_source_update)
|
|
68
|
+
|
|
69
|
+
cp.element.update_options(
|
|
70
|
+
create_options(self._dataSource.get_filtered_data(cp.element))
|
|
71
|
+
)
|
|
72
|
+
return EChartsResult(cp.element, self._dataSource, create_options)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from nicegui import ui
|
|
4
|
+
from nicegui.elements.radio import Radio
|
|
5
|
+
from ex4nicegui import to_ref
|
|
6
|
+
from ex4nicegui.utils.signals import Ref
|
|
7
|
+
from ex4nicegui.bi.dataSource import DataSource, Filter
|
|
8
|
+
from .models import UiResult
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ex4nicegui.bi.dataSourceFacade import DataSourceFacade
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RadioResult(UiResult[ui.radio]):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
element: Radio,
|
|
18
|
+
dataSource: DataSource,
|
|
19
|
+
ref_value: Ref,
|
|
20
|
+
) -> None:
|
|
21
|
+
super().__init__(element, dataSource)
|
|
22
|
+
self._ref_value = ref_value
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def value(self):
|
|
26
|
+
return self._ref_value.value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def ui_radio(self: DataSourceFacade, column: str, **kwargs) -> RadioResult:
|
|
30
|
+
options = self._dataSource._idataSource.duplicates_column_values(self.data, column)
|
|
31
|
+
kwargs.update({"options": options})
|
|
32
|
+
|
|
33
|
+
cp = ui.radio(**kwargs)
|
|
34
|
+
ref_value = to_ref(cp.value)
|
|
35
|
+
|
|
36
|
+
def onchange(e):
|
|
37
|
+
cp.value = cp.options[e.args]
|
|
38
|
+
|
|
39
|
+
def data_filter(data):
|
|
40
|
+
if cp.value not in cp.options:
|
|
41
|
+
return data
|
|
42
|
+
cond = data[column] == cp.value
|
|
43
|
+
return data[cond]
|
|
44
|
+
|
|
45
|
+
self._dataSource.send_filter(cp.id, Filter(data_filter))
|
|
46
|
+
|
|
47
|
+
cp.on("update:modelValue", onchange)
|
|
48
|
+
|
|
49
|
+
def on_source_update():
|
|
50
|
+
data = self._dataSource.get_filtered_data(cp)
|
|
51
|
+
options = self._dataSource._idataSource.duplicates_column_values(data, column)
|
|
52
|
+
value = cp.value
|
|
53
|
+
if value not in options:
|
|
54
|
+
value = ""
|
|
55
|
+
|
|
56
|
+
cp.set_options(options, value=value)
|
|
57
|
+
|
|
58
|
+
self._dataSource._register_component(cp.id, on_source_update)
|
|
59
|
+
|
|
60
|
+
return RadioResult(cp, self._dataSource, ref_value)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from nicegui import ui
|
|
3
|
+
from ex4nicegui import to_ref
|
|
4
|
+
from ex4nicegui.utils.signals import Ref
|
|
5
|
+
from ex4nicegui.bi.dataSource import Filter
|
|
6
|
+
from .models import UiResult
|
|
7
|
+
from typing import Any, Callable, Optional, TYPE_CHECKING, cast
|
|
8
|
+
from typing_extensions import TypedDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from nicegui.elements.mixins.disableable_element import DisableableElement
|
|
12
|
+
from nicegui.elements.mixins.value_element import ValueElement
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ex4nicegui.bi.dataSourceFacade import DataSourceFacade, DataSource
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class QRangeValue(TypedDict):
|
|
20
|
+
min: float
|
|
21
|
+
max: float
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
TQRangeValue = Optional[QRangeValue]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class QRange(ValueElement, DisableableElement):
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
min: float, # pylint: disable=redefined-builtin
|
|
32
|
+
max: float, # pylint: disable=redefined-builtin
|
|
33
|
+
step: float = 1.0,
|
|
34
|
+
value: TQRangeValue = None,
|
|
35
|
+
on_change: Optional[Callable[..., Any]] = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Slider
|
|
38
|
+
|
|
39
|
+
:param min: lower bound of the slider
|
|
40
|
+
:param max: upper bound of the slider
|
|
41
|
+
:param step: step size
|
|
42
|
+
:param value: initial value to set position of the slider
|
|
43
|
+
:param on_change: callback which is invoked when the user releases the slider
|
|
44
|
+
"""
|
|
45
|
+
super().__init__(
|
|
46
|
+
tag="q-range", value=value, on_value_change=on_change, throttle=0.05
|
|
47
|
+
)
|
|
48
|
+
self._props["min"] = min
|
|
49
|
+
self._props["max"] = max
|
|
50
|
+
self._props["step"] = step
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RangeResult(UiResult[QRange]):
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
element: QRange,
|
|
57
|
+
dataSource: DataSource,
|
|
58
|
+
ref_value: Ref,
|
|
59
|
+
) -> None:
|
|
60
|
+
super().__init__(element, dataSource)
|
|
61
|
+
self._ref_value = ref_value
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def value(self):
|
|
65
|
+
return cast(TQRangeValue, self._ref_value.value)
|
|
66
|
+
|
|
67
|
+
def drag_range(self):
|
|
68
|
+
self.element.props("drag-range")
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def ui_range(self: DataSourceFacade, column: str, **kwargs):
|
|
73
|
+
self._dataSource._idataSource.slider_check(self.data, column)
|
|
74
|
+
|
|
75
|
+
min, max = self._dataSource._idataSource.range_min_max(self.data, column)
|
|
76
|
+
kwargs.update({"min": min, "max": max, "value": {"min": min, "max": max}})
|
|
77
|
+
|
|
78
|
+
with ui.element("q-item").classes("w-full"):
|
|
79
|
+
with ui.element("q-item-section").props("avatar"):
|
|
80
|
+
ui.label(column)
|
|
81
|
+
with ui.element("q-item-section"):
|
|
82
|
+
cp = QRange(**kwargs).props("label")
|
|
83
|
+
|
|
84
|
+
ref_value = to_ref(cast(TQRangeValue, cp.value))
|
|
85
|
+
|
|
86
|
+
def onchange():
|
|
87
|
+
def data_filter(data):
|
|
88
|
+
cp_value = cast(TQRangeValue, cp.value)
|
|
89
|
+
|
|
90
|
+
if cp_value is None:
|
|
91
|
+
return data
|
|
92
|
+
cond = (data[column] >= cp_value["min"]) & (data[column] <= cp_value["max"])
|
|
93
|
+
return data[cond]
|
|
94
|
+
|
|
95
|
+
self._dataSource.send_filter(cp.id, Filter(data_filter))
|
|
96
|
+
|
|
97
|
+
cp.on("change", onchange)
|
|
98
|
+
|
|
99
|
+
def on_source_update():
|
|
100
|
+
data = self._dataSource.get_filtered_data(cp)
|
|
101
|
+
min, max = self._dataSource._idataSource.range_min_max(data, column)
|
|
102
|
+
if min is None or max is None:
|
|
103
|
+
cp.value = None
|
|
104
|
+
else:
|
|
105
|
+
new_value = cast(TQRangeValue, cp.value)
|
|
106
|
+
if new_value is not None:
|
|
107
|
+
new_value = new_value.copy()
|
|
108
|
+
|
|
109
|
+
if new_value["min"] < min:
|
|
110
|
+
new_value["min"] = min
|
|
111
|
+
if new_value["max"] > max:
|
|
112
|
+
new_value["max"] = max
|
|
113
|
+
|
|
114
|
+
cp.value = new_value
|
|
115
|
+
cp.update()
|
|
116
|
+
|
|
117
|
+
self._dataSource._register_component(cp.id, on_source_update)
|
|
118
|
+
|
|
119
|
+
return RangeResult(cp, self._dataSource, ref_value)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from nicegui import ui
|
|
4
|
+
from ex4nicegui import to_ref, ref_computed
|
|
5
|
+
from ex4nicegui.utils.signals import Ref
|
|
6
|
+
from ex4nicegui.bi.dataSource import Filter
|
|
7
|
+
from .models import UiResult
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ex4nicegui.bi.dataSourceFacade import DataSourceFacade, DataSource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SelectResult(UiResult[ui.select]):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
element: ui.select,
|
|
17
|
+
dataSource: DataSource,
|
|
18
|
+
ref_value: Ref,
|
|
19
|
+
) -> None:
|
|
20
|
+
super().__init__(element, dataSource)
|
|
21
|
+
self._ref_value = ref_value
|
|
22
|
+
|
|
23
|
+
@ref_computed
|
|
24
|
+
def value_or_options():
|
|
25
|
+
if not self._ref_value.value:
|
|
26
|
+
return self.element.options
|
|
27
|
+
return self._ref_value.value
|
|
28
|
+
|
|
29
|
+
self._value_or_options = value_or_options
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def value(self):
|
|
33
|
+
return self.get_value()
|
|
34
|
+
|
|
35
|
+
def get_value(self, no_select_eq_all=True):
|
|
36
|
+
"""Get the selected items of the dropdown box
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
no_select_eq_all (bool, optional): When there is no selection, it is eq to selecting all. Defaults to True.
|
|
40
|
+
"""
|
|
41
|
+
if no_select_eq_all:
|
|
42
|
+
return self._value_or_options.value
|
|
43
|
+
|
|
44
|
+
return self._ref_value.value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def ui_select(
|
|
48
|
+
self: DataSourceFacade, column: str, *, clearable=True, multiple=True, **kwargs
|
|
49
|
+
) -> SelectResult:
|
|
50
|
+
options = self._dataSource._idataSource.duplicates_column_values(self.data, column)
|
|
51
|
+
kwargs.update(
|
|
52
|
+
{
|
|
53
|
+
"options": options,
|
|
54
|
+
"multiple": multiple,
|
|
55
|
+
"clearable": clearable,
|
|
56
|
+
"label": column,
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
cp = ui.select(**kwargs).props("use-chips outlined").classes("min-w-[8rem]")
|
|
61
|
+
ref_value = to_ref(cp.value)
|
|
62
|
+
|
|
63
|
+
def onchange(e):
|
|
64
|
+
value = None
|
|
65
|
+
if e.args:
|
|
66
|
+
if isinstance(e.args, list):
|
|
67
|
+
value = [arg["label"] for arg in e.args]
|
|
68
|
+
else:
|
|
69
|
+
value = e.args["label"]
|
|
70
|
+
|
|
71
|
+
cp.value = value
|
|
72
|
+
ref_value.value = value # type: ignore
|
|
73
|
+
|
|
74
|
+
def data_filter(data):
|
|
75
|
+
if cp.value is None or not cp.value:
|
|
76
|
+
return data
|
|
77
|
+
|
|
78
|
+
cond = None
|
|
79
|
+
if isinstance(cp.value, list):
|
|
80
|
+
cond = data[column].isin(cp.value)
|
|
81
|
+
else:
|
|
82
|
+
cond = data[column] == cp.value
|
|
83
|
+
return data[cond]
|
|
84
|
+
|
|
85
|
+
self._dataSource.send_filter(cp.id, Filter(data_filter))
|
|
86
|
+
|
|
87
|
+
cp.on("update:modelValue", onchange)
|
|
88
|
+
|
|
89
|
+
def on_source_update():
|
|
90
|
+
data = self._dataSource.get_filtered_data(cp)
|
|
91
|
+
options = self._dataSource._idataSource.duplicates_column_values(data, column)
|
|
92
|
+
value = cp.value
|
|
93
|
+
|
|
94
|
+
# Make the value within the options
|
|
95
|
+
if isinstance(value, list):
|
|
96
|
+
value = list(set(value) & set(options))
|
|
97
|
+
else:
|
|
98
|
+
if value not in options:
|
|
99
|
+
value = ""
|
|
100
|
+
|
|
101
|
+
cp.set_options(options, value=value)
|
|
102
|
+
|
|
103
|
+
self._dataSource._register_component(cp.id, on_source_update)
|
|
104
|
+
|
|
105
|
+
return SelectResult(cp, self._dataSource, ref_value)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
from nicegui import ui
|
|
4
|
+
from ex4nicegui import to_ref
|
|
5
|
+
from ex4nicegui.utils.signals import Ref
|
|
6
|
+
from ex4nicegui.bi.dataSource import Filter
|
|
7
|
+
from .models import UiResult
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ex4nicegui.bi.dataSourceFacade import DataSourceFacade, DataSource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SliderResult(UiResult[ui.slider]):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
element: ui.slider,
|
|
17
|
+
dataSource: DataSource,
|
|
18
|
+
ref_value: Ref,
|
|
19
|
+
) -> None:
|
|
20
|
+
super().__init__(element, dataSource)
|
|
21
|
+
self._ref_value = ref_value
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def value(self):
|
|
25
|
+
return self._ref_value.value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def ui_slider(self: DataSourceFacade, column: str, **kwargs):
|
|
29
|
+
self._dataSource._idataSource.slider_check(self.data, column)
|
|
30
|
+
|
|
31
|
+
min, max = self._dataSource._idataSource.slider_min_max(self.data, column)
|
|
32
|
+
kwargs.update({"min": min, "max": max})
|
|
33
|
+
|
|
34
|
+
cp = ui.slider(**kwargs).props("label label-always switch-label-side")
|
|
35
|
+
ref_value = to_ref(cp.value)
|
|
36
|
+
|
|
37
|
+
def onchange():
|
|
38
|
+
def data_filter(data):
|
|
39
|
+
if cp.value is None or cp.value < min:
|
|
40
|
+
return data
|
|
41
|
+
cond = data[column] == cp.value
|
|
42
|
+
return data[cond]
|
|
43
|
+
|
|
44
|
+
self._dataSource.send_filter(cp.id, Filter(data_filter))
|
|
45
|
+
|
|
46
|
+
cp.on("change", onchange)
|
|
47
|
+
|
|
48
|
+
def on_source_update():
|
|
49
|
+
data = self._dataSource.get_filtered_data(cp)
|
|
50
|
+
min, max = self._dataSource._idataSource.slider_min_max(data, column)
|
|
51
|
+
if min is None or max is None:
|
|
52
|
+
cp.value = None
|
|
53
|
+
else:
|
|
54
|
+
cp._props["min"] = min
|
|
55
|
+
cp._props["max"] = max
|
|
56
|
+
|
|
57
|
+
self._dataSource._register_component(cp.id, on_source_update)
|
|
58
|
+
|
|
59
|
+
return SliderResult(cp, self._dataSource, ref_value)
|
ex4nicegui/bi/index.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from .dataSourceFacade import DataSourceFacade
|
|
2
|
+
from .dataSource import DataSource
|
|
3
|
+
from .protocols import CallableDataSourceAble, DataFrameDataSourceAble
|
|
4
|
+
from typing import Callable, cast, TypeVar, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_TData = TypeVar("_TData")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def data_source(data: Union[Callable[..., _TData], _TData]) -> DataSourceFacade[_TData]:
|
|
11
|
+
"""Create a data source
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
data (_TData): Any supported data
|
|
15
|
+
|
|
16
|
+
Raises:
|
|
17
|
+
TypeError: Throw an error when the data type is not supported
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
DataSourceFacade[_TData]: _description_
|
|
21
|
+
|
|
22
|
+
## Examples
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
pandas dataframe source:
|
|
27
|
+
```python
|
|
28
|
+
df = pd.DataFrame({"name": list("abcdc"), "value": range(5)})
|
|
29
|
+
ds = bi.data_source(df)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
Link multiple data sources
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
```python
|
|
38
|
+
df = pd.DataFrame({"name": list("abcdc"), "value": range(5)})
|
|
39
|
+
ds = bi.data_source(df)
|
|
40
|
+
|
|
41
|
+
@bi.data_source
|
|
42
|
+
def ds_other():
|
|
43
|
+
# ds.filtered_data is DataFrame after filtering
|
|
44
|
+
where = ds.filtered_data[''].isin(['b','c','d'])
|
|
45
|
+
return ds.filtered_data[where]
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
Now, when `ds` changes, it will trigger changes to `ds_other` and thus drive the related interface components to change.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# select box of data source 'ds'
|
|
55
|
+
# it change will trigger changes to table
|
|
56
|
+
ds.ui_select('name')
|
|
57
|
+
|
|
58
|
+
# table of data 'ds_other'
|
|
59
|
+
ds_other.ui_aggrid()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
ds_protocol = None
|
|
65
|
+
|
|
66
|
+
if type(data).__name__ == "DataFrame":
|
|
67
|
+
ds_protocol = DataFrameDataSourceAble(data)
|
|
68
|
+
|
|
69
|
+
if isinstance(data, Callable):
|
|
70
|
+
ds_protocol = CallableDataSourceAble(data)
|
|
71
|
+
|
|
72
|
+
if ds_protocol is None:
|
|
73
|
+
raise TypeError(f"not support type[{type(data)}]")
|
|
74
|
+
|
|
75
|
+
ds = DataSource(ds_protocol)
|
|
76
|
+
return cast(DataSourceFacade[_TData], DataSourceFacade(ds))
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from typing_extensions import Protocol
|
|
2
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from ex4nicegui.bi.types import _TFilterCallback
|
|
5
|
+
from .types import _TFilterCallback
|
|
6
|
+
from ex4nicegui.utils import common as utils_common
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IDataSourceAble(Protocol):
|
|
10
|
+
def get_data(self) -> Any:
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
def apply_filters(self, data, filters: List[_TFilterCallback]) -> Any:
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
def duplicates_column_values(self, data, column_name: str) -> List:
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
def get_aggrid_options(self, data) -> Dict:
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
def slider_check(self, data, column_name: str) -> None:
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
def slider_min_max(
|
|
26
|
+
self, data, column_name: str
|
|
27
|
+
) -> Tuple[Optional[float], Optional[float]]:
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
def range_check(self, data, column_name: str) -> None:
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
def range_min_max(
|
|
34
|
+
self, data, column_name: str
|
|
35
|
+
) -> Tuple[Optional[float], Optional[float]]:
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DataFrameDataSourceAble(IDataSourceAble):
|
|
40
|
+
def __init__(self, df) -> None:
|
|
41
|
+
self.data = df
|
|
42
|
+
|
|
43
|
+
def get_data(self):
|
|
44
|
+
return self.data
|
|
45
|
+
|
|
46
|
+
def apply_filters(self, data, filters: List[_TFilterCallback]):
|
|
47
|
+
new_data = data
|
|
48
|
+
for f in filters:
|
|
49
|
+
new_data = f(new_data)
|
|
50
|
+
|
|
51
|
+
return new_data
|
|
52
|
+
|
|
53
|
+
def duplicates_column_values(self, data, column_name: str) -> List:
|
|
54
|
+
return data[column_name].drop_duplicates().tolist()
|
|
55
|
+
|
|
56
|
+
def get_aggrid_options(self, data) -> Dict:
|
|
57
|
+
df = utils_common.convert_dataframe(data)
|
|
58
|
+
return {
|
|
59
|
+
"columnDefs": [{"field": col} for col in df.columns],
|
|
60
|
+
"rowData": df.to_dict("records"),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def slider_check(self, data, column_name: str) -> None:
|
|
64
|
+
from pandas.api.types import is_numeric_dtype
|
|
65
|
+
|
|
66
|
+
if not is_numeric_dtype(data[column_name]):
|
|
67
|
+
raise ValueError(f"column[{column_name}] must be numeric type")
|
|
68
|
+
|
|
69
|
+
def slider_min_max(
|
|
70
|
+
self, data, column_name: str
|
|
71
|
+
) -> Tuple[Optional[float], Optional[float]]:
|
|
72
|
+
import numpy as np
|
|
73
|
+
|
|
74
|
+
min, max = data[column_name].min(), data[column_name].max()
|
|
75
|
+
|
|
76
|
+
if np.isnan(min) or np.isnan(max):
|
|
77
|
+
return None, None
|
|
78
|
+
|
|
79
|
+
return min, max
|
|
80
|
+
|
|
81
|
+
def range_check(self, data, column_name: str) -> None:
|
|
82
|
+
from pandas.api.types import is_numeric_dtype
|
|
83
|
+
|
|
84
|
+
if not is_numeric_dtype(data[column_name]):
|
|
85
|
+
raise ValueError(f"column[{column_name}] must be numeric type")
|
|
86
|
+
|
|
87
|
+
def range_min_max(
|
|
88
|
+
self, data, column_name: str
|
|
89
|
+
) -> Tuple[Optional[float], Optional[float]]:
|
|
90
|
+
import numpy as np
|
|
91
|
+
|
|
92
|
+
min, max = data[column_name].min(), data[column_name].max()
|
|
93
|
+
|
|
94
|
+
if np.isnan(min) or np.isnan(max):
|
|
95
|
+
return None, None
|
|
96
|
+
|
|
97
|
+
return min, max
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CallableDataSourceAble(IDataSourceAble):
|
|
101
|
+
def __init__(self, fn: Callable) -> None:
|
|
102
|
+
self.data_fn = fn
|
|
103
|
+
|
|
104
|
+
def get_data(self):
|
|
105
|
+
return self.data_fn()
|
|
106
|
+
|
|
107
|
+
def apply_filters(self, data, filters: List[_TFilterCallback]):
|
|
108
|
+
new_data = data
|
|
109
|
+
for f in filters:
|
|
110
|
+
new_data = f(new_data)
|
|
111
|
+
|
|
112
|
+
return new_data
|
|
113
|
+
|
|
114
|
+
def duplicates_column_values(self, data, column_name: str) -> List:
|
|
115
|
+
return data[column_name].drop_duplicates().tolist()
|
|
116
|
+
|
|
117
|
+
def get_aggrid_options(self, data) -> Dict:
|
|
118
|
+
df = utils_common.convert_dataframe(data)
|
|
119
|
+
return {
|
|
120
|
+
"columnDefs": [{"field": col} for col in df.columns],
|
|
121
|
+
"rowData": df.to_dict("records"),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
def slider_check(self, data, column_name: str) -> None:
|
|
125
|
+
from pandas.api.types import is_numeric_dtype
|
|
126
|
+
|
|
127
|
+
if not is_numeric_dtype(data[column_name]):
|
|
128
|
+
raise ValueError(f"column[{column_name}] must be numeric type")
|
|
129
|
+
|
|
130
|
+
def slider_min_max(
|
|
131
|
+
self, data, column_name: str
|
|
132
|
+
) -> Tuple[Optional[float], Optional[float]]:
|
|
133
|
+
import numpy as np
|
|
134
|
+
|
|
135
|
+
min, max = data[column_name].min(), data[column_name].max()
|
|
136
|
+
|
|
137
|
+
if np.isnan(min) or np.isnan(max):
|
|
138
|
+
return None, None
|
|
139
|
+
|
|
140
|
+
return min, max
|
|
141
|
+
|
|
142
|
+
def range_check(self, data, column_name: str) -> None:
|
|
143
|
+
from pandas.api.types import is_numeric_dtype
|
|
144
|
+
|
|
145
|
+
if not is_numeric_dtype(data[column_name]):
|
|
146
|
+
raise ValueError(f"column[{column_name}] must be numeric type")
|
|
147
|
+
|
|
148
|
+
def range_min_max(
|
|
149
|
+
self, data, column_name: str
|
|
150
|
+
) -> Tuple[Optional[float], Optional[float]]:
|
|
151
|
+
import numpy as np
|
|
152
|
+
|
|
153
|
+
min, max = data[column_name].min(), data[column_name].max()
|
|
154
|
+
|
|
155
|
+
if np.isnan(min) or np.isnan(max):
|
|
156
|
+
return None, None
|
|
157
|
+
|
|
158
|
+
return min, max
|
ex4nicegui/bi/types.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Callable, TypeVar
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
_TData = TypeVar("_TData")
|
|
7
|
+
|
|
8
|
+
_TElementID = int
|
|
9
|
+
_TNgClientID = str
|
|
10
|
+
_TFilterCallback = Callable[[_TData], _TData]
|
|
11
|
+
|
|
12
|
+
_TDataSourceId = int
|
|
13
|
+
_TSourceBuildFn = Callable[..., _TData]
|
|
@@ -43967,8 +43967,7 @@ const TX = { class: "echart-container relative" }, CX = /* @__PURE__ */ GL({
|
|
|
43967
43967
|
l.target || t("chartClickBlank");
|
|
43968
43968
|
});
|
|
43969
43969
|
function s() {
|
|
43970
|
-
|
|
43971
|
-
i == null || i.resize({ width: l.width - 5, height: l.height - 5 });
|
|
43970
|
+
i == null || i.resize();
|
|
43972
43971
|
}
|
|
43973
43972
|
ZL("resize", () => {
|
|
43974
43973
|
s();
|
|
@@ -55,6 +55,7 @@ class echarts(Element, component="ECharts.js"):
|
|
|
55
55
|
[open echarts setOption docs](https://echarts.apache.org/zh/api.html#echartsInstance.setOption)
|
|
56
56
|
|
|
57
57
|
"""
|
|
58
|
+
self._props["options"] = options
|
|
58
59
|
self.run_method("updateOptions", options, opts)
|
|
59
60
|
self.update()
|
|
60
61
|
return self
|