widgetti / solara

A Pure Python, React-style Framework for Scaling Your Jupyter and Web Apps
https://solara.dev
MIT License
1.62k stars 105 forks source link

How to disable dynamic rendering/event driven rendering for some components #550

Closed BFAGIT closed 1 month ago

BFAGIT commented 2 months ago

Hello me again, I'm continuing to build my application with Solara šŸ˜šŸ˜šŸ˜, and some of the elements I want to chart takes quite some time to display due a lot of data points, around 30-60 seconds. (The charting library library also needs to be optimised more ). Having dynamic rendering is awesome feature 99% of the time but for some cases it would be nice to have the rendering and all the code present in the solara components to be triggered only on a event.

What I want to do is basically select all my options for the charting and then click on a button that triggers all compoenent rendering, code processing in the compoenent and chartsd display. The rendering should not occur dynamically, but only when the button is clicked.

Is there a way I could disable the automatic rendering and processing of the charts ?

From what I have looked in the docs and I haven't seen how i could achieve this. It might be dead simple but I haven't found anything yet.

Here is a small basic example of what

import solara
from highcharts_core.chart import Chart
from highcharts_core.options.series.area import LineSeries

exponent = solara.reactive(1.2)
series_type = solara.reactive('line')
button_state =
def on_click_button_chart():

@solara.component
def Page():
    with solara.Sidebar():
        with solara.Card("Select Charting options"):
             solara.Select('Type', value=series_type, values=['line', 'bar'])
             solara.SliderFloat(label='exponent', value=exponent, min=-1, max=2)
             solara.Button(label=f"Click to chart", on_click=on_click_button_chart)

    #Could something here dictates how the dynamic redendering occurs ? 
      with solara.Card(f'Demo: exponent={exponent}'):
          exp = exponent.value
          my_chart = Chart(data = [[1, 1*exp], [2, 2*exp], [3, 3**exp]],
                               series_type = series_type.value)
          my_chart.display()

Thanks again guys, cheers BFAGIT

iisakkirotko commented 2 months ago

Hi @BFAGIT!

I think what you're looking for is the functionality of the @Task-decorator, and use_task. See the docs here and here.

In use_task you can give it an argument dependencies to let it know when to rerun the function.

Let me know if you have any further questions!

BFAGIT commented 2 months ago

Hey @iisakkirotko,

@Task decotator is great, you even solved before me asking an other question i had about displaying a spinner while the chart was being processed šŸ˜.

Concerning the above ticket using the task decorator works well for the first click but if the button has already been clicked once then if any of the options are changed it will make the chart re-render as the task is finished and the task.finished is true even though the button has not been clicked. I tried seeting task.finished to False but no setters are available but i guess if I did that the displayed elements would disapear

I'm currently thinking if there is any other logic with other variables i could implement to make this behaves this way.

import solara
from solara.lab import task
import time
from highcharts_core.chart import Chart
from highcharts_core.options.series.area import LineSeries

exponent = solara.reactive(1.2)
series_type = solara.reactive('line')

@task
def button_clicked():
    time.sleep(2)
    pass

html = "<h1>Loading Chart...</h1"

@solara.component
def Page():
    with solara.Sidebar():
        with solara.Card("Select Charting options"):
            solara.Select('Type', value=series_type, values=['line', 'bar'])
            solara.SliderFloat(label='exponent', value=exponent, min=-1, max=2)
            solara.Button(label=f"Click to chart", on_click=button_clicked)
    with solara.Card(f'Demo: exponent={exponent}'):
        if button_clicked.pending:
            with solara.Column(align="center"):
                solara.SpinnerSolara(size="100px")
                solara.HTML(tag="div", unsafe_innerHTML=html)
        if button_clicked.finished:

            exp = exponent.value
            my_chart = Chart(data = [[1, 1*exp], [2, 2*exp], [3, 3**exp]],
                                     series_type = series_type.value)
            my_chart.display()
            #button_clicked.finished=False
        elif button_clicked.not_called:
            solara.Markdown('Please select options and click the "Click to chart" button')

I also tried the following with use_task but i have the same beghaviour where changes in the options triggers a rerender of the chart without clicking the button.

import solara
from solara.lab import Task, use_task
import time
from highcharts_core.chart import Chart
from highcharts_core.options.series.area import LineSeries

def button_clicked():
    time.sleep(2)
    pass

html = "<h1>Loading Chart...</h1"

@solara.component
def Page():
    exponent = solara.use_reactive(1.2)
    series_type = solara.use_reactive('line')

    result: Task[int] = use_task(button_clicked, dependencies=[exponent.value,series_type.value])

    with solara.Sidebar():
        with solara.Card("Select Charting options"):
            solara.Select('Type', value=series_type, values=['line', 'bar'])
            solara.SliderFloat(label='exponent', value=exponent, min=-1, max=2)
            solara.Button(label=f"Click to chart", on_click=result)
    with solara.Card(f'Demo: exponent={exponent}'):
        if result.pending:
            with solara.Column(align="center"):
                solara.SpinnerSolara(size="100px")
                solara.HTML(tag="div", unsafe_innerHTML=html)
        if result.finished:

            exp = exponent.value
            my_chart = Chart(data = [[1, 1*exp], [2, 2*exp], [3, 3**exp]],
                                     series_type = series_type.value)
            my_chart.display()
            #button_clicked.finished=False
        elif result.not_called:

            solara.Markdown('Please select options and click the "Click to chart" button')

Thanks

iisakkirotko commented 2 months ago

Ah, maybe I should have been clearer; on change of the dependencies, the task will automatically rerun. You can leave the dependencies empty to have it only rerun when the button is clicked.

Edit: I do see some additional complications:

In the first case, the Task itself should render the figure, this way you could avoid the rerender when reactive variables are changed. In the second case, the dependencies should be changed, like I said above, so that the task only runs when the button is clicked. You could do this by using a counter variable that is incremented when the button is clicked, for instance.

BFAGIT commented 1 month ago

Hello @iisakkirotko, Thanks for your returns two weeks ago, had a few things that came up personnaly so i could not follow through at that time.

I've made the following changes as you suggested above. I have moved the chart instantiation to the task function and removed the depencies so the task is not triggered by changes of thoses.

1) Is there a way to actually put the entire render in the task ? Because the render is triggered by the .display() method so i do not see how to include it in the task and how to make it appear in the compoenent. Also the render through the .display() call is what takes the most tilme so the spinner will not be displayed for the entire time the .display() is trying to draw the chart. Having the render in the task eliminates this issue. Is there a way to do this as you suggested ?

The behaviour I have is almost perfect. Overall, the chart renders when I press the button, changes on the depencies do not trigger re-rendering of the chart when the chart instantiation is in the task.

2) The main issue/ question i have is why does the chart renders directly the first time without me needing to click the button ? Shoudln't it be displayed only when I click the button ? Shouldn't the displayed element be:

if result.not_called:

            solara.Markdown('Please select options and click the "Click to chart" button')

I have not be able to understand why this behaviour happens.

3) What would the counter be for I have failed to understand what its use case ? Is it to change the dependencies based on the intial state of the button ?

Here is my entire modified code:

import solara
from solara.lab import Task, use_task
import time
from highcharts_core.chart import Chart
from highcharts_core.options.series.area import LineSeries

html = "<h1>Loading Chart...</h1"

@solara.component
def Page():
    exponent = solara.use_reactive(1.2)
    series_type = solara.use_reactive('line')

    def button_clicked():
        exp = exponent.value
        my_chart = Chart(data = [[1, 1*exp], [2, 2*exp], [3, 3**exp]],series_type = series_type.value)
        time.sleep(2)
        return my_chart

    result: Task[int] = use_task(button_clicked, dependencies=[])
    #exponent.value, series_type.value

    with solara.Sidebar():
        with solara.Card("Select Charting options"):
            solara.Select('Type', value=series_type, values=['line', 'bar'])
            solara.SliderFloat(label='exponent', value=exponent, min=-1, max=2)
            solara.Button(label=f"Click to chart", on_click=result)
    with solara.Card(f'Demo: exponent'):
        if result.pending:
            with solara.Column(align="center"):
                solara.SpinnerSolara(size="100px")
                solara.HTML(tag="div", unsafe_innerHTML=html)
        if result.finished:

            result.value.display()

        if result.not_called:

            solara.Markdown('Please select options and click the "Click to chart" button')

Thanks again and have a great week-end :)

iisakkirotko commented 1 month ago

Hey @BFAGIT!

  1. Is there a way to actually put the entire render in the task?

I think @maartenbreddels has some ideas for how this might be possible.

  1. The main issue/ question i have is why does the chart renders directly the first time without me needing to click the button ? Shoudln't it be displayed only when I click the button?

I see that there is something missing from the docs here, I'll take a look at getting it fixed. Anyway, giving the use_task an empty list [], will run the task on first render, whereas setting dependencies=None will prevent this from happening, and make it run only on call.

  1. Is it to change the dependencies based on the intial state of the button ?

Right on the money. The point would be to make that the only dependency, and use the dependency to trigger a rerender. A trick like this can sometimes be useful if there aren't any other ways of making something re-render.

A great weekend to you too!

maartenbreddels commented 1 month ago

I've added a context manager in this example that might need to go into solara in some form or another. Let me know if you have questions:

import solara
import contextlib
from typing import Dict, Tuple
from IPython import get_ipython
import solara
from solara.lab import Task, use_task
import time
from highcharts_core.chart import Chart
from highcharts_core.options.series.area import LineSeries
import reacton.ipywidgets as w
from IPython.display import clear_output

html = "<h1>Loading Chart...</h1"
outputs = solara.reactive(())

@contextlib.contextmanager
def capture_to(outputs: solara.Reactive[Tuple[Dict]]):
    def hook(msg):
        if msg["msg_type"] == "display_data":
            outputs.value += ({"output_type": "display_data", "data": msg["content"]["data"], "metadata": msg["content"]["metadata"]},)
            return None
        if msg["msg_type"] == "clear_output":
            outputs.value = ()
            return None
        return msg

    ip = get_ipython()
    try:
        ip.display_pub.register_hook(hook)
        yield
    finally:
        ip.display_pub.unregister_hook(hook)

@solara.component
def Page():
    exponent = solara.use_reactive(1.2)
    series_type = solara.use_reactive("line")

    def button_clicked():
        exp = exponent.value
        my_chart = Chart(data=[[1, 1 * exp], [2, 2 * exp], [3, 3**exp]], series_type=series_type.value)
        time.sleep(2)
        with capture_to(outputs):
            clear_output()
            print("In solara we do not capture stdout, should we?")
            my_chart.display()

    result: Task[int] = use_task(button_clicked, dependencies=None)
    # exponent.value, series_type.value

    with solara.Sidebar():
        with solara.Card("Select Charting options"):
            solara.Select("Type", value=series_type, values=["line", "bar"])
            solara.SliderFloat(label="exponent", value=exponent, min=-1, max=2)
            solara.Button(label=f"Click to chart", on_click=result)
    with solara.Card(f"Demo: exponent"):
        if result.pending:
            with solara.Column(align="center"):
                solara.SpinnerSolara(size="100px")
                solara.HTML(tag="div", unsafe_innerHTML=html)
        if result.finished:
            w.Output(outputs=outputs.value)

        if result.not_called:
            solara.Markdown('Please select options and click the "Click to chart" button')
BFAGIT commented 1 month ago

Hello guys, Thank you both for your response, you guys are so dedicated it is so nice to discover all that can be done with Solara espacially with your support.

@maartenbreddels thanks for your code you provided this work spot on and is exaclty what I wanted Thanks. It might be nice in the future as you stated to have it in some sort or form in solara :). I don't have any particular questions on it as it is straighforward and by reading the contextmanager documentation I should be able to answer any qestions i have so thanks a lot.

I've provided an example on how to display more charts as It could help others. One nice enhancement that i will probably do in the future is to have a bunch of individual async workers triggered by the task that generates all the charts in parallel. This could improve the user experience for the dashboard that I'm building.

import solara
import contextlib
from typing import Dict, Tuple
from IPython import get_ipython
from solara.lab import Task, use_task
import time
from highcharts_core.chart import Chart
from highcharts_core.options.series.area import LineSeries
import reacton.ipywidgets as w
from IPython.display import clear_output

html = "<h1>Loading Chart...</h1>"
output1 = solara.reactive(())
output2 = solara.reactive(())
show_chart1 = solara.reactive(True)
show_chart2 = solara.reactive(True)

@contextlib.contextmanager
def capture_to(outputs: solara.Reactive[Tuple[Dict]]):
    def hook(msg):
        if msg["msg_type"] == "display_data":
            outputs.value += ({"output_type": "display_data", "data": msg["content"]["data"], "metadata": msg["content"]["metadata"]},)
            return None
        if msg["msg_type"] == "clear_output":
            outputs.value = ()
            return None
        return msg

    ip = get_ipython()
    try:
        ip.display_pub.register_hook(hook)
        yield
    finally:
        ip.display_pub.unregister_hook(hook)

@solara.component
def Page():
    exponent = solara.use_reactive(1.2)
    series_type = solara.use_reactive("line")

    def button_clicked():
        exp = exponent.value
        if show_chart1.value:
            with capture_to(output1):
                clear_output()
                my_chart1 = Chart(data=[[1, 1 * exp], [2, 2 * exp], [3, 3**exp]], series_type=series_type.value)
                my_chart1.display()
        if show_chart2.value:
            with capture_to(output2):           
                clear_output()
                my_chart2 = Chart(data=[[1, 1 * exp], [2, 2 * exp], [3, 3**exp]], series_type=series_type.value)
                my_chart2.display()
        time.sleep(2) # This is justso the spinenr is displayed the spinner

    result: Task[int] = use_task(button_clicked, dependencies=None)

    with solara.Sidebar():
        with solara.Card("Select Charting options"):
            solara.Select("Type", value=series_type, values=["line", "bar"])
            solara.SliderFloat(label="exponent", value=exponent, min=-1, max=2)
            solara.Switch(label="Show chart 1", value=show_chart1)
            solara.Switch(label="Show chart 2", value=show_chart2)              
            solara.Button(label=f"Click to chart", on_click=result)
    if show_chart1.value:    
        with solara.Card(title = "Chart 1"):
            if result.pending:
                with solara.Column(align="center"):
                    solara.SpinnerSolara(size="100px")
                    solara.HTML(tag="div", unsafe_innerHTML=html)
            if result.finished:
                w.Output(outputs=output1.value)
            if result.not_called:
                solara.Markdown('Please select options and click the "Click to chart" button')
    if show_chart2.value:             
        with solara.Card(title = "Chart 2"):
            if result.pending:
                with solara.Column(align="center"):
                    solara.SpinnerSolara(size="100px")
                    solara.HTML(tag="div", unsafe_innerHTML=html)
            if result.finished:
                w.Output(outputs=output2.value)
            if result.not_called:
                solara.Markdown('Please select options and click the "Click to chart" button')

@iisakkirotko Thanks for your answer to :) 2) was very helpfull. As for 3) On my side I prefer not to have anything re-render based on depencies but I see the use case for it so thanks for the sugesstion it will probably come handy in the future.

Thanks guys and enjoy the week-end