-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathcomplex_app.py
More file actions
129 lines (107 loc) · 5.02 KB
/
Copy pathcomplex_app.py
File metadata and controls
129 lines (107 loc) · 5.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""A more complex nicegui-react demo.
The chart on the right is a React component (built on the third-party `recharts`
library, bundled automatically). The controls on the left are plain NiceGUI
widgets running in Python. Moving a control updates the React chart live, and
adding/removing metrics sends events from React back to Python.
Exercises: a third-party npm dependency, complex array/object props updating
live from Python, React-internal state (a controlled input) alongside
Python-driven props, and events with structured payloads.
Run with: python examples/complex_app.py
"""
import math
import os
import random
from nicegui import ui
from nicegui_react import React
N = 40 # number of samples visible in the chart window
@ui.page('/')
def index() -> None:
ui.label('nicegui-react — a live React chart, driven from Python').classes('text-2xl font-bold')
ui.label(
'The card on the right is a React component (recharts). The controls on the left are plain '
'Python (NiceGUI). Change a control and the React chart reacts instantly; add or remove a '
'metric and React sends an event back to Python.'
).classes('text-sm text-gray-500').style('max-width: 820px')
state = {
'waveform': 'Sine',
'amplitude': 70,
'noise': 5,
'phase': 0.0,
'series': [],
'items': [{'id': 1, 'label': 'CPU load'}, {'id': 2, 'label': 'Memory'}],
'next_id': 3,
}
def sample_at(phase: float) -> float:
if state['waveform'] == 'Sine':
base = math.sin(phase)
elif state['waveform'] == 'Square':
base = 1.0 if math.sin(phase) >= 0 else -1.0
else: # Triangle
base = (2.0 / math.pi) * math.asin(math.sin(phase))
value = 50 + (state['amplitude'] / 2) * base + random.uniform(-state['noise'], state['noise'])
return round(max(0.0, min(100.0, value)), 1)
# Seed the visible window so the chart is populated on first paint.
for _ in range(N):
state['series'].append({'t': round(state['phase'], 2), 'value': sample_at(state['phase'])})
state['phase'] += 0.3
dash: React # assigned below; the closures below read it lazily
def regenerate() -> None:
"""Recompute the whole visible window so a control change is reflected at once."""
end = state['phase']
state['series'] = [
{'t': round(end - 0.3 * (N - 1 - i), 2), 'value': sample_at(end - 0.3 * (N - 1 - i))}
for i in range(N)
]
dash.props(series=state['series'], waveform=state['waveform'])
with ui.row().classes('items-start gap-6 no-wrap'):
with ui.card().classes('w-72'):
ui.label('Controls · Python (NiceGUI)').classes(
'text-xs font-semibold text-indigo-600 tracking-wide'
).style('text-transform: uppercase')
ui.separator()
ui.label('Waveform').classes('text-sm text-gray-600')
ui.toggle(
['Sine', 'Square', 'Triangle'],
value=state['waveform'],
on_change=lambda e: (state.update(waveform=e.value), regenerate()),
).classes('wf-toggle').props('no-caps')
ui.label('Amplitude').classes('text-sm text-gray-600 q-mt-sm')
ui.slider(
min=0, max=100, value=state['amplitude'],
on_change=lambda e: (state.update(amplitude=e.value), regenerate()),
).classes('amp-slider').props('label')
ui.label('Noise').classes('text-sm text-gray-600 q-mt-sm')
ui.slider(
min=0, max=30, value=state['noise'],
on_change=lambda e: (state.update(noise=e.value), regenerate()),
).classes('noise-slider').props('label')
with ui.column():
dash = React('components/dashboard', main_component='Dashboard').props(
title='Live signal',
unit='%',
waveform=state['waveform'],
series=state['series'],
items=state['items'],
)
def on_add(data) -> None:
label = (data or {}).get('label', '').strip()
if not label:
return
state['items'] = state['items'] + [{'id': state['next_id'], 'label': label}]
state['next_id'] += 1
dash.props(items=state['items'])
ui.notify(f'Added: {label}')
def on_remove(data) -> None:
remove_id = (data or {}).get('id')
state['items'] = [item for item in state['items'] if item['id'] != remove_id]
dash.props(items=state['items'])
dash.on('onAddItem', on_add)
dash.on('onRemoveItem', on_remove)
# Live data: append a new sample and scroll the window (Python -> React).
def tick() -> None:
state['phase'] += 0.3
state['series'] = (state['series'] + [{'t': round(state['phase'], 2), 'value': sample_at(state['phase'])}])[-N:]
dash.props(series=state['series'])
ui.timer(0.4, tick)
if __name__ in {'__main__', '__mp_main__'}:
ui.run(port=int(os.environ.get('PORT', '8123')), reload=False)