ex4nicegui

repository·main·Indexed 42 days ago

https://github.com/crystalwindsnake/ex4nicegui

An extension library for NiceGUI that implements a reactive, data-driven programming model. It provides ViewModels for state synchronization, reactive primitives like ref and deep_ref, and specialized modules for BI functionality and interactive ECharts visualizations. Version 0.9.3.

Tokens
16.7K
Snippets
56
Records
66
Agent score
44%

What's inside ex4nicegui

  1. Manage shared state with PageState

    main

    For complex applications where state needs to be shared across different modules or views, use the PageState class.

    1. Define State: Create a class inheriting from PageState. Use to_ref() for reactive properties and the @on(self.property) decorator to handle changes.
    2. Access State: In any view or module, use YourStateClass.get() to retrieve the shared instance of the state.
    3. Integration: This allows multiple views (e.g., view1(), view2()) to interact with the same reactive data source.
    # state.py
    from ex4nicegui import PageState, to_ref, on
    
    class CounterState(PageState):
        def __init__(self):
            self.count = to_ref(0)
    
        @on(self.count)
        def on_count_change(self, val):
            print(f"count changed to {val}")
    
    # view.py
    from ex4nicegui import rxui
    from state import CounterState
    
    def view1():
        state = CounterState.get()
        ui.label(f"count: {state.count.value} in view1")
  2. Handle list data in ViewModels

    main

    When working with lists in an rxui.ViewModel, you have two primary ways to initialize them:

    1. Standard Initialization: Define an empty list at the class level and initialize the actual data inside the __init__ method using super().__init__().
    2. Using rxui.list_var: Use rxui.list_var(factory_function) where the factory function returns the initial list. This is often cleaner for simple initializations.

    Note: Do not attempt to initialize lists with values directly at the class level (e.g., nums = [1, 2, 3]) as this may not behave as expected with the reactive proxy system.

    # Option 1: __init__
    class AppState(rxui.ViewModel):
        nums = []
        def __init__(self):
            super().__init__()
            self.nums = [1, 2, 3]
    
    # Option 2: rxui.list_var
    class AppState(rxui.ViewModel):
        nums = rxui.list_var(lambda: [1, 2, 3])
  3. Handle type support for .vue imports in TypeScript

    main

    Because standard tsc cannot process type information for .vue files, this project uses vue-tsc instead of tsc for type checking.

    To get type awareness in your editor for .vue imports, you must use the TypeScript Vue Plugin (Volar). If you are not using the plugin, the TypeScript language service will not recognize the types exported from Vue Single File Components.

  4. Use derived computations and caching

    main

    You can create derived values by defining ordinary instance methods in your rxui.ViewModel. These methods can be bound to UI properties (like color or text) using specific binding methods.

    To prevent redundant computations when a derived value is used in multiple places, use the @rxui.cached_var decorator.

    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"
            return "black"
    
    counter = Counter()
    # Bind the computed method to a property
    rxui.label(counter.count).bind_color(counter.text_color)
  5. Build BI dashboards with bi.data_source

    main

    The bi module is designed for creating interactive Business Intelligence dashboards. The core concept is the bi.data_source.

    Workflow:

    1. Create a Data Source: Initialize with a pandas.DataFrame using bi.data_source(df) or use the @bi.data_source decorator on a function that returns a DataFrame.
    2. Add Interactive Filters: Use ds.ui_select(column_name) to automatically create dropdown filters that update the underlying data.
    3. Render Visualizations: Use @ds.ui_echarts on a function. This function receives the filtered DataFrame as its argument. The chart will automatically re-render whenever the user changes a filter.
    4. Chained Data Sources: You can create a new data source that depends on a filtered version of an existing one, creating a cascading filter effect.

    Methods:

    • ds.remove_filters(): Clears all active filters.
    • ds.reload(new_df): Replaces the entire dataset.
    import pandas as pd
    from ex4nicegui import bi
    
    df = pd.DataFrame({"cat": ["A", "B"], "val": [10, 20]})
    ds = bi.data_source(df)
    
    # Create a filter
    ds.ui_select("cat")
    
    # Create a reactive chart based on filtered data
    @ds.ui_echarts
    def my_chart(filtered_df):
        return {"series": [{"data": filtered_df["val"].tolist()}]}
    
    my_chart.classes("h-40")
  6. Render reactive lists with vfor

    main

    The @rxui.vfor decorator is used to render components based on a reactive list. Unlike manual loops or effect_refreshable, vfor is optimized to update existing elements rather than recreating the entire list when data changes.

    To use it:

    1. Pass a reactive list or a function that returns a sorted list to the decorator.
    2. Provide a key argument (e.g., key="id") to help the engine track and reuse elements.
    3. The decorated function receives a store object; use store.get_item() to access the current item in the iteration.
    @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="年龄")
  7. How reactive data-driven programming works in ex4nicegui

    main

    ex4nicegui follows a data-driven approach where the UI is a reflection of an application's state.

    1. Define State: Create a class that inherits from rxui.ViewModel. Define reactive variables as class attributes.
    2. Define Logic: Add instance methods to the ViewModel to modify the state.
    3. Build UI: Use rxui components (e.g., rxui.label) to bind to the state variables. When the state changes, the UI updates automatically.

    Note on Privacy: Variables prefixed with an underscore (e.g., _count) are treated as private and will not automatically synchronize with the UI.

    from nicegui import ui
    from ex4nicegui import rxui
    
    # 1. Define State
    class Counter(rxui.ViewModel):
        count: int = 0
    
        # 2. Define Logic
        def increment(self):
            self.count += 1
    
        def decrement(self):
            self.count -= 1
    
    # 3. Build UI
    counter = Counter()
    
    with ui.row(align_items="center"):
        ui.button(icon="remove", on_click=counter.decrement)
        rxui.label(counter.count)  # Automatically updates when counter.count changes
        ui.button(icon="add", on_click=counter.increment)
    
    ui.run()
  8. Use the BI Module for interactive data visualization

    main

    The BI (Business Intelligence) module provides a high-level API for creating interactive reports linked to a bi.data_source.

    Core Concepts

    • bi.data_source(df): The central object that holds a pandas.DataFrame. It manages filtering and provides the data to all connected UI components.
    • Linked Data Sources: You can create a new data source using the @bi.data_source decorator that depends on an existing one (e.g., using ds.filtered_data). When the parent source is filtered, the child source updates automatically.
    • Reactivity: Use @effect or @effect_refreshable to trigger logic when the data source changes.

    Key Methods

    • ds.remove_filters(): Clears all active filters in the data source.
    • ds.reload(new_df): Replaces the current data with a new DataFrame.
    from ex4nicegui import bi
    import pandas as pd
    
    df = pd.DataFrame({"name": ["a", "b"], "val": [1, 2]})
    ds = bi.data_source(df)
    
    @bi.data_source
    def filtered_ds():
        return ds.filtered_data.copy()
    
    filtered_ds.ui_aggrid()
  9. Use computed properties (Secondary Calculations)

    main

    You can derive UI values from the current state by defining regular instance methods in your rxui.ViewModel. These are called 'secondary calculations'. Use binding methods like .bind_color() to link these calculations to component properties.

    To prevent redundant calculations when a method is called multiple times in the UI, use the @rxui.cached_var decorator to cache the result.

    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"
    
    counter = Counter()
    
    with ui.row(align_items="center"):
        ui.button(icon="remove", on_click=counter.decrement)
        # Bind the color property to the computed method
        rxui.label(counter.count).bind_color(counter.text_color)
        ui.button(icon="add", on_click=counter.increment)
  10. Synchronize tabs and tab panels reactively

    main

    In ex4nicegui, rxui.tabs and rxui.tab_panels are synchronized by sharing the same reactive value (a Ref or a variable created via to_ref).

    Unlike standard NiceGUI, you do not need to pass a tabs parameter to tab_panels. Instead, both components bind to the same underlying data source, ensuring that selecting a tab updates the panel and vice versa.

    from ex4nicegui import rxui, to_ref
    from nicegui import ui
    
    names = ["Tab 1", "Tab 2", "Tab 3"]
    current_tab = to_ref(names[0])
    
    # The tabs definition
    with rxui.tabs(current_tab):
        for name in names:
            rxui.tab(name)
    
    # The panels definition, synchronized via current_tab
    with rxui.tab_panels(current_tab):
        for name in names:
            with rxui.tab_panel(name):
                ui.label(f"Content of {name}")
  11. How data-responsive programming works in ex4nicegui

    main

    ex4nicegui uses a data-driven approach where the UI is bound to a state object.

    1. Define State: Create a class inheriting from rxui.ViewModel. Variables defined in this class are reactive and automatically synchronized with the UI.
    2. Private Variables: Prefix a variable name with an underscore (e.g., _count) to make it private; these will not be automatically synchronized with the UI.
    3. Define Logic: Add instance methods to the ViewModel to manipulate the state.
    4. Build UI: Use components from the rxui namespace (e.g., rxui.label) instead of the standard nicegui namespace to enable automatic data binding. When the ViewModel state changes, the bound UI components update automatically.
    from nicegui import ui
    from ex4nicegui import rxui
    
    # 1. Define State
    class Counter(rxui.ViewModel):
        count: int = 0
    
        def increment(self):
            self.count += 1
    
    # 2. Instantiate
    counter = Counter()
    
    # 3. Build UI with rxui components for binding
    with ui.row():
        ui.button(on_click=counter.increment)
        rxui.label(counter.count)  # Automatically updates when counter.count changes
    
    ui.run()
  12. Manage reactive data with ViewModel

    main

    The rxui.ViewModel class is used to manage sets of reactive data. It allows you to define stateful objects where properties are automatically tracked for changes. You can use the @rxui.cached_var decorator on methods within a ViewModel to create computed properties that are automatically recalculated whenever their dependencies (other properties in the same ViewModel) change.

    When using ViewModel, you often don't need manual two-way binding tools like vmodel because the class handles the reactivity lifecycle.

    from ex4nicegui import rxui
    
    class Calculator(rxui.ViewModel):
        num1 = 0
        sign = "+"
        num2 = 0
    
    @rxui.cached_var
        def result(self):
            # Recalculates automatically when num1, sign, or num2 changes
            return eval(f"{self.num1}{self.sign}{self.num2}")
    
    calc = Calculator()
    
    # Usage in UI
    with ui.row():
        rxui.number(value=calc.num1, label="Number 1")
        rxui.select(value=calc.sign, options=["+", "-", "*", "/"], label="Sign")
        rxui.number(value=calc.num2, label="Number 2")
        rxui.label(calc.result)