Extension library based on nicegui, providing data responsive,BI functionality modules
Project description
ex4nicegui
简体中文| English
对 nicegui 做的扩展库。内置响应式组件,完全实现数据响应式界面编程。
教程
头条文章-秒杀官方实现,python界面库,去掉90%事件代码的nicegui
微信公众号-秒杀官方实现,python界面库,去掉90%事件代码的nicegui
📦 安装
pip install ex4nicegui -U
入门
我们从一个简单的计数器应用开始,用户可以通过点击按钮让计数增加或减少。
下面是完整代码:
from nicegui import ui
from ex4nicegui import rxui
# 数据状态代码
class Counter(rxui.ViewModel):
count: int = 0
def increment(self):
self.count += 1
def decrement(self):
self.count -= 1
# 界面代码
counter = Counter()
with ui.row(align_items="center"):
ui.button(icon="remove", on_click=counter.decrement)
rxui.label(counter.count)
ui.button(icon="add", on_click=counter.increment)
ui.run()
现在看更多细节。ex4nicegui
遵从数据驱动方式定义界面。状态数据定义应用程序中所有可以变化的数据。
下面是 Counter
状态数据定义:
class Counter(rxui.ViewModel):
count: int = 0
- 自定义类需要继承
rxui.ViewModel
- 这里定义了一个变量
count
,表示计数器的当前值,初始值为 0
接着,在类中定义一系列操作数据的方法:
def increment(self):
self.count += 1
def decrement(self):
self.count -= 1
- 这些都是实例方法,可以修改
count
变量的值
然后,在界面代码中,实例化 Counter
的对象。
counter = Counter()
我们通过 rxui.label
组件绑定 count
变量。把操作数据的方法绑定到按钮点击事件上。
ui.button(icon="remove", on_click=counter.decrement)
rxui.label(counter.count)
ui.button(icon="add", on_click=counter.increment)
- 我们需要使用
rxui
命名空间下的label
组件,而不是nicegui
命名空间下的label
组件。 rxui.label
组件绑定counter.count
变量,当counter.count
变化时,rxui.label
组件自动更新。ui.button
组件绑定counter.decrement
和counter.increment
方法,点击按钮时调用相应方法。
在复杂项目中,
Counter
定义的代码可以放到单独的模块中,然后在界面代码中导入。
注意,当类变量名前面带有下划线时,数据状态不会自动更新。
class Counter(rxui.ViewModel):
count: int = 0 # 响应式数据,能自动同步界面
_count: int = 0 # 这里的下划线表示私有变量,不会自动同步界面
二次计算
接着前面的例子,我们再添加一个功能。当计数器的值小于 0 时,字体显示为红色,大于 0 时显示为绿色,否则显示为黑色。
# 数据状态代码
class Counter(rxui.ViewModel):
count: int = 0
def text_color(self):
if self.count > 0:
return "green"
elif self.count < 0:
return "red"
else:
return "black"
def increment(self):
self.count += 1
def decrement(self):
self.count -= 1
# 界面代码
counter = Counter()
with ui.row(align_items="center"):
ui.button(icon="remove", on_click=counter.decrement)
rxui.label(counter.count).bind_color(counter.text_color)
ui.button(icon="add", on_click=counter.increment)
颜色值是依据计数器当前值计算得到的。属于二次计算。通过定义普通的实例函数即可。
def text_color(self):
if self.count > 0:
return "green"
elif self.count < 0:
return "red"
else:
return "black"
然后,通过 rxui.label
组件的 bind_color
方法绑定 text_color
方法,使得颜色值自动更新。
rxui.label(counter.count).bind_color(counter.text_color)
二次计算缓存
现在,我们在计数器下方使用文字,显示当前计数器的颜色文本值。
...
# 数据状态代码
class Counter(rxui.ViewModel):
...
# 界面代码
counter = Counter()
with ui.row(align_items="center"):
ui.button(icon="remove", on_click=counter.decrement)
rxui.label(counter.count).bind_color(counter.text_color)
ui.button(icon="add", on_click=counter.increment)
rxui.label(lambda: f"当前计数器值为 {counter.count}, 颜色值为 {counter.text_color()}")
- 当二次计算非常简单时,可以直接使用 lambda 表达式
上面的代码中,有两个地方使用了 counter.text_color
方法。当 counter.count
变化时,counter.text_color
会执行两次计算。第二次计算是多余的。
为了避免多余的计算,我们可以把 counter.text_color
缓存起来。
# 数据状态代码
class Counter(rxui.ViewModel):
count: int = 0
@rxui.cached_var
def text_color(self):
if self.count > 0:
return "green"
elif self.count < 0:
return "red"
else:
return "black"
rxui.cached_var
装饰器可以把函数结果缓存起来,避免多余的计算。
列表
下面的示例,展示了如何使用列表。
class AppState(rxui.ViewModel):
nums = []
# nums = [1,2,3] ❌ 如果需要初始化,必须在 __init__ 中设置
def __init__(self):
super().__init__()
self.nums = [1, 2, 3]
def append(self):
new_num = max(self.nums) + 1
self.nums.append(new_num)
def pop(self):
self.nums.pop()
def reverse(self):
self.nums.reverse()
def display_nums(self):
return ", ".join(map(str, self.nums))
# 界面代码
state = AppState()
with ui.row(align_items="center"):
ui.button("append", on_click=state.append)
ui.button("pop", on_click=state.pop)
ui.button("reverse", on_click=state.reverse)
rxui.label(state.display_nums)
如果你需要在定义列表时,初始化列表,建议在 __init__
中设置。
class AppState(rxui.ViewModel):
nums = []
# nums = [1,2,3] ❌ 如果需要初始化,必须在 __init__ 中设置
def __init__(self):
super().__init__()
self.nums = [1, 2, 3]
...
另一种方式是使用 rxui.list_var
class AppState(rxui.ViewModel):
# nums = []
# nums = [1,2,3] ❌ 如果需要初始化,必须在 __init__ 中设置
nums = rxui.list_var(lambda: [1, 2, 3])
...
rxui.list_var
参数是一个返回列表的函数
列表循环
定义列表后,我们可以用 effect_refreshable.on
装饰器,在界面中展示列表数据。
下面的例子中,界面会动态展示下拉框选中的图标
from ex4nicegui import rxui, effect_refreshable
class AppState(rxui.ViewModel):
icons = []
_option_icons = ["font_download", "warning", "format_size", "print"]
state = AppState()
# 界面代码
with ui.row(align_items="center"):
@effect_refreshable.on(state.icons)
def _():
for icon in state.icons:
ui.icon(icon, size="2rem")
rxui.select(state._option_icons, value=state.icons, multiple=True)
其中,@effect_refreshable.on(state.icons)
明确指定了依赖关系。当 state.icons
变化时,_
函数会重新执行。
@effect_refreshable.on(state.icons)
def _():
# 这里的代码会在 state.icons 变化时重新执行
...
注意,每次执行,里面的内容都会被清除。这是数据驱动版本的
ui.refreshable
原则上,可以不通过 .on
指定监控的数据,只要函数中使用到的"响应式数据",都会自动监控
@effect_refreshable # 没有使用 .on(state.icons)
def _():
# 这里读取了 state.icons,因此会自动监控
for icon in state.icons:
ui.icon(icon, size="2rem")
建议总是通过
.on
指定依赖关系,避免预料之外的刷新
数据持久化
ViewModel
使用代理对象创建响应式数据,当需要保存数据时,可以使用 rxui.ViewModel.to_value
转换成普通数据.
下面的例子,点击按钮将显示 my_app 的状态数据字典。
from nicegui import ui
from ex4nicegui import rxui
class MyApp(rxui.ViewModel):
a = 0
sign = "+"
b = 0
def show_data(self):
# >> {"a": 0, "sign": '+, "b": 0}
return rxui.ViewModel.to_value(self)
def show_a(self):
# >> 0
return rxui.ViewModel.to_value(self.a)
my_app = MyApp()
rxui.number(value=my_app.a, min=0, max=10)
rxui.radio(["+", "-", "*", "/"], value=my_app.sign)
rxui.number(value=my_app.b, min=0, max=10)
ui.button("show data", on_click=lambda: ui.notify(my_app.show_data()))
结合 rxui.ViewModel.on_refs_changed
,可以在数据变化时,自动保存数据到本地。
from nicegui import ui
from ex4nicegui import rxui
from pathlib import Path
import json
class MyApp(rxui.ViewModel):
a = 0
sign = "+"
b = 0
_json_path = Path(__file__).parent / "data.json"
def __init__(self):
super().__init__()
@rxui.ViewModel.on_refs_changed(self)
def _():
# a, sign, b 任意一个值变化时,自动保存到本地
self._json_path.write_text(json.dumps(self.show_data()))
def show_data(self):
return rxui.ViewModel.to_value(self)
...
apis
ViewModel
在 v0.7.0
版本中,引入 ViewModel
类,用于管理一组响应式数据。
下面是一个简单的计算器示例:
- 当用户修改数值输入框或符号选择框,右侧会自动显示计算结果
- 当结果小于 0 时,结果显示为红色,否则为黑色
from ex4nicegui import rxui
class Calculator(rxui.ViewModel):
num1 = 0
sign = "+"
num2 = 0
@rxui.cached_var
def result(self):
# 当 num1,sign,num2 任意一个值发生变化时,result 也会重新计算
return eval(f"{self.num1}{self.sign}{self.num2}")
# 每个对象拥有独立的数据
calc = Calculator()
with ui.row(align_items="center"):
rxui.number(value=calc.num1, label="Number 1")
rxui.select(value=calc.sign, options=["+", "-", "*", "/"], label="Sign")
rxui.number(value=calc.num2, label="Number 2")
ui.label("=")
rxui.label(calc.result).bind_color(
lambda: "red" if calc.result() < 0 else "black"
)
使用列表
下面的示例,每个 person 使用卡片展示。最上方显示所有人的平均年龄。当个人年龄大于平均年龄,卡片外边框将变为红色。
通过 number
组件修改年龄,一切都会自动更新。
from typing import List
from ex4nicegui import rxui
from itertools import count
from nicegui import ui
id_generator = count()
class Person(rxui.ViewModel):
name = ""
age = 0
def __init__(self, name: str, age: int):
super().__init__()
self.name = name
self.age = age
self.id = next(id_generator)
class Home(rxui.ViewModel):
persons: List[Person] = []
deleted_person_index = 0
@rxui.cached_var
def avg_age(self) -> float:
if len(self.persons) == 0:
return 0
return round(sum(p.age for p in self.persons) / len(self.persons), 2)
def avg_name_length(self):
if len(self.persons) == 0:
return 0
return round(sum(len(p.name) for p in self.persons) / len(self.persons), 2)
def delete_person(self):
if self.deleted_person_index < len(self.persons):
del self.persons[int(self.deleted_person_index)]
def sample_data(self):
self.persons = [
Person("alice", 25),
Person("bob", 30),
Person("charlie", 31),
Person("dave", 22),
Person("eve", 26),
Person("frank", 29),
]
home = Home()
home.sample_data()
rxui.label(lambda: f"平均年龄: {home.avg_age()}")
rxui.label(lambda: f"平均名字长度: {home.avg_name_length()}")
rxui.number(
value=home.deleted_person_index, min=0, max=lambda: len(home.persons) - 1, step=1
)
ui.button("删除", on_click=home.delete_person)
with ui.row():
@rxui.vfor(home.persons, key="id")
def _(store: rxui.VforStore[Person]):
person = store.get_item()
with rxui.card().classes("outline").bind_classes(
{
"outline-red-500": lambda: person.age > home.avg_age(),
}
):
rxui.input(value=person.name, placeholder="名字")
rxui.number(value=person.age, min=1, max=100, step=1, placeholder="年龄")
ui.run()
如果你觉得 rxui.vfor
代码过于复杂,可以使用 effect_refreshable
装饰器代替。
from ex4nicegui import rxui, Ref,effect_refreshable
...
# 明确指定监控 home.persons 变化,可以避免意外刷新
@effect_refreshable.on(home.persons)
def _():
for person in home.persons.value:
...
rxui.number(value=person.age, min=1, max=100, step=1, placeholder="年龄")
...
需要注意到,每当 home.persons
列表变化时(比如新增或删除元素),effect_refreshable
装饰的函数都会重新执行。意味着所有元素都会重新创建。
更多复杂的应用,可以查看 examples
响应式
from ex4nicegui import (
to_ref,
ref_computed,
on,
effect,
effect_refreshable,
batch,
event_batch,
deep_ref,
async_computed
)
常用 to_ref
,deep_ref
,effect
,ref_computed
,on
,async_computed
to_ref
定义响应式对象,通过 .value
读写
a = to_ref(1)
b = to_ref("text")
a.value =2
b.value = 'new text'
print(a.value)
当值为复杂对象时,默认不会保持嵌套对象的响应性。
a = to_ref([1,2])
@effect
def _():
print('len:',len(a.value))
# 不会触发 effect
a.value.append(10)
# 整个替换则会触发
a.value = [1,2,10]
参数 is_deep
设置为 True
时,能得到深度响应能力
a = to_ref([1,2],is_deep=True)
@effect
def _():
print('len:',len(a.value))
# print 3
a.value.append(10)
deep_ref
等价于is_deep
设置为True
时的to_ref
deep_ref
等价于 is_deep
设置为 True
时的 to_ref
。
当数据源为列表、字典或自定义类时,特别有用。通过 .value
获取的对象为代理对象
data = [1,2,3]
data_ref = deep_ref(data)
assert data_ref.value is not data
通过 to_raw
可以获取原始对象
from ex4nicegui import to_raw, deep_ref
data = [1, 2, 3]
data_ref = deep_ref(data)
assert data_ref.value is not data
assert to_raw(data_ref.value) is data
effect
接受一个函数,自动监控函数中使用到的响应式对象变化,从而自动执行函数
a = to_ref(1)
b = to_ref("text")
@effect
def auto_run_when_ref_value():
print(f"a:{a.value}")
def change_value():
a.value = 2
b.value = "new text"
ui.button("change", on_click=change_value)
首次执行 effect ,函数auto_run_when_ref_value
将被执行一次.之后点击按钮,改变 a
的值(通过 a.value
),函数auto_run_when_ref_value
再次执行
切忌把大量数据处理逻辑分散在多个
on
或effect
中,on
或effect
中应该大部分为界面操作逻辑,而非响应式数据处理逻辑
ref_computed
与 effect
具备一样的功能,ref_computed
还能从函数中返回结果。一般用于从 to_ref
中进行二次计算
a = to_ref(1)
a_square = ref_computed(lambda: a.value * 2)
@effect
def effect1():
print(f"a_square:{a_square.value}")
def change_value():
a.value = 2
ui.button("change", on_click=change_value)
点击按钮后,a.value
值被修改,从而触发 a_square
重新计算.由于 effect1
中读取了 a_square
的值,从而触发 effect1
执行
ref_computed
是只读的to_ref
从 v0.7.0
版本开始,不建议使用 ref_computed
应用实例方法。你可以使用 rxui.ViewModel
,并使用 rxui.cached_var
装饰器
class MyState(rxui.ViewModel):
def __init__(self) -> None:
self.r_text = to_ref("")
@rxui.cached_var
def post_text(self):
return self.r_text.value + "post"
state = MyState()
rxui.input(value=state.r_text)
rxui.label(state.post_text)
async_computed
二次计算中需要使用异步函数时,使用 async_computed
# 模拟长时间执行的异步函数
async def long_time_query(input: str):
await asyncio.sleep(2)
num = random.randint(20, 100)
return f"query result[{input=}]:{num=}"
search = to_ref("")
evaluating = to_ref(False)
@async_computed(search, evaluating=evaluating, init="")
async def search_result():
return await long_time_query(search.value)
rxui.lazy_input(value=search)
rxui.label(
lambda: "查询中" if evaluating.value else "上方输入框输入内容并回车搜索"
)
rxui.label(search_result)
async_computed
第一个参数必须明确指定需要监控的响应式数据. 使用列表可以同时指定多个响应式数据- 参数
evaluating
为 bool 类型的响应式数据,当异步函数执行中,此变量值为True
,计算结束后为False
- 参数
init
指定初始结果
on
类似 effect
的功能,但是 on
需要明确指定监控的响应式对象
a1 = to_ref(1)
a2 = to_ref(10)
b = to_ref("text")
@on(a1)
def watch_a1_only():
print(f"watch_a1_only ... a1:{a1.value},a2:{a2.value}")
@on([a1, b], onchanges=True)
def watch_a1_and_b():
print(f"watch_a1_and_b ... a1:{a1.value},a2:{a2.value},b:{b.value}")
def change_a1():
a1.value += 1
ui.notify("change_a1")
ui.button("change a1", on_click=change_a1)
def change_a2():
a2.value += 1
ui.notify("change_a2")
ui.button("change a2", on_click=change_a2)
def change_b():
b.value += "x"
ui.notify("change_b")
ui.button("change b", on_click=change_b)
- 参数
onchanges
为 True 时(默认值为 False),指定的函数不会在绑定时执行
切忌把大量数据处理逻辑分散在多个
on
或effect
中,on
或effect
中应该大部分为界面操作逻辑,而非响应式数据处理逻辑
new_scope
默认情况下,所有检测函数在客户端连接断开时自动销毁。如果需要更细粒度的控制,可以使用 new_scope
from nicegui import ui
from ex4nicegui import rxui, to_ref, effect, new_scope
a = to_ref(0.0)
scope1 = new_scope()
@scope1.run
def _():
@effect
def _():
print(f"scope 1:{a.value}")
rxui.number(value=a)
rxui.button("dispose scope 1", on_click=scope1.dispose)
组件功能
vmodel
在表单输入元素或组件上创建双向绑定。
简单值类型的 ref
默认支持双向绑定
from ex4nicegui import rxui, to_ref, deep_ref
data = to_ref("init")
rxui.label(lambda: f"{data.value=}")
# 默认就是双向绑定
rxui.input(value=data)
- 简单值类型一般是
str
,int
等不可变值类型
当使用复杂数据结构时,会使用 deep_ref
保持嵌套值的响应性
data = deep_ref({"a": 1, "b": [1, 2, 3, 4]})
rxui.label(lambda: f"{data.value=!s}")
# 当前版本没有任何绑定效果.或许未来的版本可以解决
rxui.input(value=data.value["a"])
# 只读绑定.其他途径修改了 `data.value["a"]` ,此输入框会同步,但反过来不行
rxui.input(value=lambda: data.value["a"])
# 要使用 vmodel 才能双向绑定
rxui.input(value=rxui.vmodel(data, "a"))
# 也可以直接使用,但不推荐
rxui.input(value=rxui.vmodel(data.value['a']))
- 第一个输入框将完全失去响应性,因为代码等价于
rxui.input(value=1)
- 第二个输入框由于使用函数,将得到读取响应性(第三个输入框输入值,将得到同步)
- 第三个输入框,使用
rxui.vmodel
包裹,即可实现双向绑定
如果使用
rxui.ViewModel
,你可能不需要使用vmodel
可参考 todo list 案例
vfor
基于列表响应式数据,渲染列表组件。每项组件按需更新。数据项支持字典或任意类型对象。
从 v0.7.0
版本开始,建议配合 rxui.ViewModel
使用。与使用 effect_refreshable
装饰器不同,vfor
不会重新创建所有的元素,而是更新已存在的元素。
下面是卡片排序例子,卡片总是按年龄排序。当你修改某个卡片中的年龄数据时,卡片会实时调整顺序。但是,光标焦点不会离开输入框。
from typing import List
from nicegui import ui
from ex4nicegui import rxui, deep_ref as ref, Ref
class Person(rxui.ViewModel):
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = ref(age)
class MyApp(rxui.ViewModel):
persons: Ref[List[Person]] = rxui.var(lambda: [])
order = rxui.var("asc")
def sort_by_age(self):
return sorted(
self.persons.value,
key=lambda p: p.age.value,
reverse=self.order.value == "desc",
)
@staticmethod
def create():
persons = [
Person(name="Alice", age=25),
Person(name="Bob", age=30),
Person(name="Charlie", age=20),
Person(name="Dave", age=35),
Person(name="Eve", age=28),
]
app = MyApp()
app.persons.value = persons
return app
# ui
app = MyApp.create()
with rxui.tabs(app.order):
rxui.tab("asc", "Ascending")
rxui.tab("desc", "Descending")
@rxui.vfor(app.sort_by_age, key="name")
def each_person(s: rxui.VforStore[Person]):
person = s.get_item()
with ui.card(), ui.row(align_items="center"):
rxui.label(person.name)
rxui.number(value=person.age, step=1, min=0, max=100)
rxui.vfor
装饰器到自定义函数- 第一个参数传入响应式列表。注意,无须调用
app.sort_by_age
- 第二个参数
key
: 为了可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你可以为每个元素对应的块提供一个唯一的 key 。默认情况使用列表元素索引。例子中假定每个人的名字唯一。
- 第一个参数传入响应式列表。注意,无须调用
- 自定义函数带有一个参数。通过
store.get_item
可以获取当前行的对象。由于 Person 本身继承自rxui.ViewModel
,所以它的各项属性可以直接绑定到组件。
bind_classes
所有的组件类提供 bind_classes
用于绑定 class
,支持三种不同的数据结构。
绑定字典
bg_color = to_ref(False)
has_error = to_ref(False)
rxui.label("test").bind_classes({"bg-blue": bg_color, "text-red": has_error})
rxui.switch("bg_color", value=bg_color)
rxui.switch("has_error", value=has_error)
字典键值为类名,对应值为 bool 的响应式变量。当响应式值为 True
,类名应用到组件 class
绑定返回值为字典的响应式变量
bg_color = to_ref(False)
has_error = to_ref(False)
class_obj = ref_computed(
lambda: {"bg-blue": bg_color.value, "text-red": has_error.value}
)
rxui.switch("bg_color", value=bg_color)
rxui.switch("has_error", value=has_error)
rxui.label("bind to ref_computed").bind_classes(class_obj)
# or direct function passing
rxui.label("bind to ref_computed").bind_classes(
lambda: {"bg-blue": bg_color.value, "text-red": has_error.value}
)
绑定为列表或单个字符串的响应式变量
bg_color = to_ref("red")
bg_color_class = ref_computed(lambda: f"bg-{bg_color.value}")
text_color = to_ref("green")
text_color_class = ref_computed(lambda: f"text-{text_color.value}")
rxui.select(["red", "green", "yellow"], label="bg color", value=bg_color)
rxui.select(["red", "green", "yellow"], label="text color", value=text_color)
rxui.label("binding to arrays").bind_classes([bg_color_class, text_color_class])
rxui.label("binding to single string").bind_classes(bg_color_class)
- 列表中每个元素为返回类名的响应式变量
bind_style
from nicegui import ui
from ex4nicegui.reactive import rxui
from ex4nicegui.utils.signals import to_ref
bg_color = to_ref("blue")
text_color = to_ref("red")
rxui.label("test").bind_style(
{
"background-color": bg_color,
"color": text_color,
}
)
rxui.select(["blue", "green", "yellow"], label="bg color", value=bg_color)
rxui.select(["red", "green", "yellow"], label="text color", value=text_color)
bind_style
传入字典,key
为样式名字,value
为样式值,响应式字符串
bind_prop
绑定单个属性
label = to_ref("hello")
rxui.button("").bind_prop("label", label)
# 允许使用函数
rxui.button("").bind_prop(
"label", lambda: f"{label.value} world"
)
rxui.input(value=label)
rxui.echarts
使用 echarts 制作图表
from nicegui import ui
from ex4nicegui import ref_computed, effect, to_ref
from ex4nicegui.reactive import rxui
r_input = to_ref("")
# ref_computed 创建只读响应式变量
# 函数中使用任意其他响应式变量,会自动关联
@ref_computed
def cp_echarts_opts():
return {
"title": {"text": r_input.value}, #字典中使用任意响应式变量,通过 .value 获取值
"xAxis": {
"type": "category",
"data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
},
"yAxis": {"type": "value"},
"series": [
{
"data": [120, 200, 150, 80, 70, 110, 130],
"type": "bar",
"showBackground": True,
"backgroundStyle": {"color": "rgba(180, 180, 180, 0.2)"},
}
],
}
input = rxui.input("输入内容,图表标题会同步", value=r_input)
# 通过响应式组件对象的 element 属性,获取原生 nicegui 组件对象
input.element.classes("w-full")
rxui.echarts(cp_echarts_opts)
ui.run()
echarts 图表鼠标事件
on
函数参数 event_name
以及 query
使用,查看echarts 事件中文文档
以下例子绑定鼠标单击事件
from nicegui import ui
from ex4nicegui.reactive import rxui
opts = {
"xAxis": {"type": "value", "boundaryGap": [0, 0.01]},
"yAxis": {
"type": "category",
"data": ["Brazil", "Indonesia", "USA", "India", "China", "World"],
},
"series": [
{
"name": "first",
"type": "bar",
"data": [18203, 23489, 29034, 104970, 131744, 630230],
},
{
"name": "second",
"type": "bar",
"data": [19325, 23438, 31000, 121594, 134141, 681807],
},
],
}
bar = rxui.echarts(opts)
def on_click(e: rxui.echarts.EChartsMouseEventArguments):
ui.notify(f"on_click:{e.seriesName}:{e.name}:{e.value}")
bar.on("click", on_click)
以下例子只针对指定系列触发鼠标划过事件
from nicegui import ui
from ex4nicegui.reactive import rxui
opts = {
"xAxis": {"type": "value", "boundaryGap": [0, 0.01]},
"yAxis": {
"type": "category",
"data": ["Brazil", "Indonesia", "USA", "India", "China", "World"],
},
"series": [
{
"name": "first",
"type": "bar",
"data": [18203, 23489, 29034, 104970, 131744, 630230],
},
{
"name": "second",
"type": "bar",
"data": [19325, 23438, 31000, 121594, 134141, 681807],
},
],
}
bar = rxui.echarts(opts)
def on_first_series_mouseover(e: rxui.echarts.EChartsMouseEventArguments):
ui.notify(f"on_first_series_mouseover:{e.seriesName}:{e.name}:{e.value}")
bar.on("mouseover", on_first_series_mouseover, query={"seriesName": "first"})
ui.run()
rxui.echarts.from_javascript
从 javascript 代码创建 echart
from pathlib import Path
rxui.echarts.from_javascript(Path("code.js"))
# or
rxui.echarts.from_javascript(
"""
(myChart) => {
option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar'
}
]
};
myChart.setOption(option);
}
"""
)
- 函数第一个参数为 echart 实例对象.你需要在函数中通过
setOption
完成图表配置
函数也有第二个参数,为 echarts
全局对象,你可以通过 echarts.registerMap
注册地图。
rxui.echarts.from_javascript(
"""
(chart,echarts) =>{
fetch('https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json')
.then(response => response.json())
.then(data => {
echarts.registerMap('test_map', data);
chart.setOption({
geo: {
map: 'test_map',
roam: true,
},
tooltip: {},
legend: {},
series: [],
});
});
}
"""
)
rxui.echarts.register_map
注册地图.
rxui.echarts.register_map(
"china", "https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json"
)
rxui.echarts(
{
"geo": {
"map": "china",
"roam": True,
},
"tooltip": {},
"legend": {},
"series": [],
}
)
- 参数
map_name
为自定义的地图名字。注意在图表配置中map
必需对应注册的名字 - 参数
src
为有效的地图数据网络链接。
如果是 svg 数据,需要设置参数 type="svg"
rxui.echarts.register_map("svg-rect", "/test/svg", type="svg")
你也可以直接提供本地地图数据的json文件路径对象(Path)
from pathlib import Path
rxui.echarts.register_map(
"china", Path("map-data.json")
)
tab_panels
相比较于 nicegui.ui.tab_panels
, rxui.tab_panels
没有参数 tabs
。在数据响应式机制下,tabs
与 tab_panels
联动只需要通过参数 value
即可。
from nicegui import ui
from ex4nicegui import rxui, to_ref
names = ["Tab 1", "Tab 2", "Tab 3"]
current_tab = to_ref(names[0])
with rxui.tabs(current_tab):
for name in names:
rxui.tab(name)
with rxui.tab_panels(current_tab):
for name in names:
with rxui.tab_panel(name):
ui.label(f"Content of {name}")
这是因为,数据响应机制下,组件联动是通过中间数据层(to_ref
)实现的。因此,tab_panels
可以与其他组件联动(只需要保证使用同样的 ref
对象即可)
names = ["Tab 1", "Tab 2", "Tab 3"]
current_tab = to_ref(names[0])
with rxui.tab_panels(current_tab):
for name in names:
with rxui.tab_panel(name):
ui.label(f"Content of {name}")
# tabs 不必在 panels 前面
with rxui.tabs(current_tab):
for name in names:
rxui.tab(name)
rxui.select(names, value=current_tab)
rxui.radio(names, value=current_tab).props("inline")
rxui.label(lambda: f"当前 tab 为:{current_tab.value}")
lazy_tab_panels
懒加载模式下,只有当前激活的 tab 才会渲染。
from ex4nicegui import to_ref, rxui, on, deep_ref
current_tab = to_ref("t1")
with rxui.tabs(current_tab):
ui.tab("t1")
ui.tab("t2")
with rxui.lazy_tab_panels(current_tab) as panels:
@panels.add_tab_panel("t1")
def _():
# 通过 `panels.get_panel` 获取当前激活的 panel 组件
panels.get_panel("t1").classes("bg-green")
ui.notify("Hello from t1")
ui.label("This is t1")
@panels.add_tab_panel("t2")
def _():
panels.get_panel("t2").style("background-color : red")
ui.notify("Hello from t2")
ui.label("This is t2")
页面加载后,立刻显示 "Hello from t1"。当切换到 "t2" 页签,才会显示 "Hello from t2"。
scoped_style
scoped_style
方法允许你创建限定在组件内部的样式。
# 所有子元素都会有红色轮廓,但排除自身
with rxui.row().scoped_style("*", "outline: 1px solid red;") as row:
ui.label("Hello")
ui.label("World")
# 所有子元素都会有红色轮廓,包括自身
with rxui.row().scoped_style(":self *", "outline: 1px solid red;") as row:
ui.label("Hello")
ui.label("World")
# 当鼠标悬停在 row 组件时,所有子元素都会有红色轮廓,但排除自身
with rxui.row().scoped_style(":hover *", "outline: 1px solid red;") as row:
ui.label("Hello")
ui.label("World")
# 当鼠标悬停在 row 组件时,所有子元素都会有红色轮廓,包括自身
with rxui.row().scoped_style(":self:hover *", "outline: 1px solid red;") as row:
ui.label("Hello")
ui.label("World")
BI 模块
以最精简的 apis 创建可交互的数据可视化报表
from nicegui import ui
import pandas as pd
import numpy as np
from ex4nicegui import bi
from ex4nicegui.reactive import rxui
from ex4nicegui import effect, effect_refreshable
from pyecharts.charts import Bar
# data ready
def gen_data():
np.random.seed(265)
field1 = ["a1", "a2", "a3", "a4"]
field2 = [f"name{i}" for i in range(1, 11)]
df = (
pd.MultiIndex.from_product([field1, field2], names=["cat", "name"])
.to_frame()
.reset_index(drop=True)
)
df[["idc1", "idc2"]] = np.random.randint(50, 1000, size=(len(df), 2))
return df
df = gen_data()
# 创建数据源
ds = bi.data_source(df)
# ui
ui.query(".nicegui-content").classes("items-stretch no-wrap")
with ui.row().classes("justify-evenly"):
# 基于数据源 `ds` 创建界面组件
ds.ui_select("cat").classes("min-w-[10rem]")
ds.ui_select("name").classes("min-w-[10rem]")
with ui.grid(columns=2):
# 使用字典配置图表
@ds.ui_echarts
def bar1(data: pd.DataFrame):
data = data.groupby("name").agg({"idc1": "sum", "idc2": "sum"}).reset_index()
return {
"xAxis": {"type": "value"},
"yAxis": {
"type": "category",
"data": data["name"].tolist(),
"inverse": True,
},
"legend": {"textStyle": {"color": "gray"}},
"series": [
{"type": "bar", "name": "idc1", "data": data["idc1"].tolist()},
{"type": "bar", "name": "idc2", "data": data["idc2"].tolist()},
],
}
bar1.classes("h-[20rem]")
# 使用pyecharts配置图表
@ds.ui_echarts
def bar2(data: pd.DataFrame):
data = data.groupby("name").agg({"idc1": "sum", "idc2": "sum"}).reset_index()
return (
Bar()
.add_xaxis(data["name"].tolist())
.add_yaxis("idc1", data["idc1"].tolist())
.add_yaxis("idc2", data["idc2"].tolist())
)
bar2.classes("h-[20rem]")
# 绑定点击事件,即可实现跳转
@bar2.on_chart_click
def _(e: rxui.echarts.EChartsMouseEventArguments):
ui.open(f"/details/{e.name}", new_tab=True)
# 利用响应式机制,你可以随意组合原生 nicegui 组件
label_a1_total = ui.label("")
# 当 ds 有变化,都会触发此函数
@effect
def _():
# filtered_data 为过滤后的 DataFrame
df = ds.filtered_data
total = df[df["cat"] == "a1"]["idc1"].sum()
label_a1_total.text = f"idc1 total(cat==a1):{total}"
# 你也可以使用 `effect_refreshable`,但需要注意函数中的组件每次都被重建
@effect_refreshable
def _():
df = ds.filtered_data
total = df[df["cat"] == "a2"]["idc1"].sum()
ui.label(f"idc1 total(cat==a2):{total}")
# 当点击图表系列时,跳转的页面
@ui.page("/details/{name}")
def details_page(name: str):
ui.label("This table data will not change")
ui.aggrid.from_pandas(ds.data.query(f'name=="{name}"'))
ui.label("This table will change when the homepage data changes. ")
@bi.data_source
def new_ds():
return ds.filtered_data[["name", "idc1", "idc2"]]
new_ds.ui_aggrid()
ui.run()
bi.data_source
数据源是 BI 模块的核心概念,所有数据的联动基于此展开。当前版本(0.4.3)中,有两种创建数据源的方式
接收 pandas
的 DataFrame
:
from nicegui import ui
from ex4nicegui import bi
import pandas as pd
df = pd.DataFrame(
{
"name": list("aabcdf"),
"cls": ["c1", "c2", "c1", "c1", "c3", None],
"value": range(6),
}
)
ds = bi.data_source(df)
有时候,我们希望基于另一个数据源创建新的数据源,此时可以使用装饰器创建联动数据源:
df = pd.DataFrame(
{
"name": list("aabcdf"),
"cls": ["c1", "c2", "c1", "c1", "c3", None],
"value": range(6),
}
)
ds = bi.data_source(df)
@bi.data_source
def new_ds():
# df is pd.DataFrame
df = ds.filtered_data
df=df.copy()
df['value'] = df['value'] * 100
return df
ds.ui_select('name')
new_ds.ui_aggrid()
注意,由于 new_ds
中使用了 ds.filtered_data
,因此 ds
的变动会触发 new_ds
的联动变化,从而导致 new_ds
创建的表格组件产生变化
通过 ds.remove_filters
方法,移除所有筛选状态:
ds = bi.data_source(df)
def on_remove_filters():
ds.remove_filters()
ui.button("remove all filters", on_click=on_remove_filters)
ds.ui_select("name")
ds.ui_aggrid()
通过 ds.reload
方法,重设数据源:
df = pd.DataFrame(
{
"name": list("aabcdf"),
"cls": ["c1", "c2", "c1", "c1", "c3", None],
"value": range(6),
}
)
new_df = pd.DataFrame(
{
"name": list("xxyyds"),
"cls": ["cla1", "cla2", "cla3", "cla3", "cla3", None],
"value": range(100, 106),
}
)
ds = bi.data_source(df)
def on_remove_filters():
ds.reload(new_df)
ui.button("reload data", on_click=on_remove_filters)
ds.ui_select("name")
ds.ui_aggrid()
ui_select
from nicegui import ui
from ex4nicegui import bi
import pandas as pd
df = pd.DataFrame(
{
"name": list("aabcdf"),
"cls": ["c1", "c2", "c1", "c1", "c3", None],
"value": range(6),
}
)
ds = bi.data_source(df)
ds.ui_select("name")
第一个参数 column 指定数据源的列名
通过参数 sort_options
设置选项顺序:
ds.ui_select("name", sort_options={"value": "desc", "name": "asc"})
参数 exclude_null_value
设置是否排除空值:
df = pd.DataFrame(
{
"cls": ["c1", "c2", "c1", "c1", "c3", None],
}
)
ds = bi.data_source(df)
ds.ui_select("cls", exclude_null_value=True)
你可以通过关键字参数,设置原生 nicegui select 组件的参数.
通过 value 属性,设置默认值:
ds.ui_select("cls",value=['c1','c2'])
ds.ui_select("cls",multiple=False,value='c1')
多选时(参数 multiple
默认为 True),value
需要指定为 list
单选时,value
设置为非 list
ui_table
表格
from nicegui import ui
from ex4nicegui import bi
import pandas as pd
data = pd.DataFrame({"name": ["f", "a", "c", "b"], "age": [1, 2, 3, 1]})
ds = bi.data_source(data)
ds.ui_table(
columns=[
{"label": "new colA", "field": "colA", "sortable": True},
]
)
- columns 与 nicegui
ui.table
一致。其中 键值field
对应数据源的列名,如果不存在,则该配置不会生效 - rows 参数不会生效。因为表格的数据源始终由 data source 控制
ui_aggrid
from nicegui import ui
from ex4nicegui import bi
import pandas as pd
data = pd.DataFrame(
{
"colA": list("abcde"),
"colB": [f"n{idx}" for idx in range(5)],
"colC": list(range(5)),
}
)
df = pd.DataFrame(data)
source = bi.data_source(df)
source.ui_aggrid(
options={
"columnDefs": [
{"headerName": "xx", "field": "no exists"},
{"headerName": "new colA", "field": "colA"},
{
"field": "colC",
"cellClassRules": {
"bg-red-300": "x < 3",
"bg-green-300": "x >= 3",
},
},
],
"rowData": [{"colX": [1, 2, 3, 4, 5]}],
}
)
- 参数 options 与 nicegui
ui.aggrid
一致。其中columnDefs
中的键值field
对应数据源的列名,如果不存在,则该配置不会生效 rowData
键值不会生效。因为表格的数据源始终由 data source 控制
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file ex4nicegui-0.8.3.tar.gz
.
File metadata
- Download URL: ex4nicegui-0.8.3.tar.gz
- Upload date:
- Size: 318.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.8.3 CPython/3.11.2 Windows/10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 3025ff7c763a817b8bcae3ba95058a3288071d424fb1629f604a6a00eab9463d |
|
MD5 | fbd997bbcbe3c94d2df4a3d0b5f0d93b |
|
BLAKE2b-256 | 4911149e1d4b17c0ef08edabcf6eef73a3185da269d04065d0e085df6857f27a |
File details
Details for the file ex4nicegui-0.8.3-py3-none-any.whl
.
File metadata
- Download URL: ex4nicegui-0.8.3-py3-none-any.whl
- Upload date:
- Size: 380.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.8.3 CPython/3.11.2 Windows/10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 81da5125b4a5137e450dae34a4e84a7c621bcf06d55934eda8e596bec6217372 |
|
MD5 | 5d9874391d5d39c17583fff502d49825 |
|
BLAKE2b-256 | 0f73880fe0208a50ee9546dd6743acfac5489c7fcdbf42be2d292036941d82ff |