Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support easy iterable layout #7570

Open
MarcSkovMadsen opened this issue Dec 24, 2024 · 6 comments
Open

Support easy iterable layout #7570

MarcSkovMadsen opened this issue Dec 24, 2024 · 6 comments
Labels
type: discussion Requiring community discussion

Comments

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Dec 24, 2024

React and Reflex makes it easy to render a dynamic iterable:

Image

See https://reflex.dev/docs/library/dynamic-rendering/foreach/.

With Panel you have to break out from the layout flow to create reactive functions to do this:

# https://reflex.dev/docs/library/dynamic-rendering/cond/

import param
import panel as pn

pn.extension()

class ForEachState(param.Parameterized):
    colors: list[str] = param.List([
        "red",
        "green",
        "blue",
        "yellow",
        "orange",
        "purple",
    ])

def colored_box(color: str):
    return pn.pane.Str(color, styles={"background": color}, margin=0, width=50, height=20)

state=ForEachState()

def for_each_example():
    grid = pn.GridBox(ncols=2)
    
    @pn.depends(state.param.colors)
    def foreach_example(colors: list[str]):
        grid[:]=list(map(colored_box, colors))
        return grid
    
    return foreach_example

pn.Column(
    for_each_example(),
    state.param.colors,
).servable()

Please make this just as easy and readable as Reflex. Thanks.

@MarcSkovMadsen
Copy link
Collaborator Author

MarcSkovMadsen commented Dec 24, 2024

Alternative Implementations

Custom foreach:

You might implement the foreach function below.

import param
import panel as pn

pn.extension()

def foreach(iterable, func):
    if isinstance(iterable, param.List):
        return pn.bind(foreach, func, iterable)

    return list(map(func, iterable))

class ForEachState(param.Parameterized):
    colors: list[str] = param.List(
        [
            "red",
            "green",
            "blue",
            "yellow",
            "orange",
            "purple",
        ]
    )


def colored_box(color: str):
    return pn.pane.Str(color, styles={"background": color}, margin=0, width=50, height=20)

state = ForEachState()

def for_each_example():
    return pn.GridBox(
        objects=foreach(state.param.colors, colored_box),
        ncols=2
    )


pn.Column(
    for_each_example(),
    state.param.colors,
).servable()

This makes the layout easier to implement. The main problem is that you set objects which you normally don't do. And that you cannot simply (by position) add more elements to the GridBox that are not coming form the foreach function.

rx.map (Recommended?)

import param
import panel as pn

pn.extension()

class ForEachState(param.Parameterized):
    colors: list[str] = param.List(
        [
            "red",
            "green",
            "blue",
            "yellow",
            "orange",
            "purple",
        ]
    )


def colored_box(color: str):
    return pn.pane.Str(color, styles={"background": color}, margin=0, width=50, height=20)

state = ForEachState()

def for_each_example():
    return pn.GridBox(
        objects=state.param.colors.rx.map(colored_box),
        ncols=2
    )


pn.Column(
    for_each_example(),
    state.param.colors,
).servable()

The main downside is you use objects which is normal and that you cannot easily add more positional elements to the GridBox. I also think the foreach version is more readable.

@MarcSkovMadsen MarcSkovMadsen changed the title Make it easy to render an iterable dynamically Support easy layout of dynamic iterable Dec 24, 2024
@MarcSkovMadsen MarcSkovMadsen changed the title Support easy layout of dynamic iterable Support easy iterable layout Dec 24, 2024
@ahuang11
Copy link
Contributor

ahuang11 commented Dec 26, 2024

I might be missing something, but to me, param.depends and list comprehension is recommended and readable(?). The foreach sounds like map() & filter()

import param
import panel as pn

pn.extension()


class ForEachState(pn.viewable.Viewer):
    colors: list[str] = param.List(
        [
            "red",
            "green",
            "blue",
            "yellow",
            "orange",
            "purple",
        ]
    )

    def colored_box(self, color: str):
        return pn.pane.Str(
            color, styles={"background": color}, margin=0, width=50, height=20
        )

    @param.depends("colors")
    def gridbox(self):
        return pn.GridBox(*[self.colored_box(color) for color in ForEachState.colors], ncols=2)

    def __panel__(self):
        return self.gridbox()

ForEachState()

Or equivalent pn.bind.

import param
import panel as pn

pn.extension()


class ForEachState(pn.viewable.Viewer):
    colors: list[str] = param.List(
        [
            "red",
            "green",
            "blue",
            "yellow",
            "orange",
            "purple",
        ]
    )

def colored_box(color: str):
    return pn.pane.Str(
        color, styles={"background": color}, margin=0, width=50, height=20
    )

def gridbox(colors):
    return pn.GridBox(
        *[colored_box(color) for color in colors],
        ncols=2
    )

state = ForEachState()
pn.bind(gridbox, colors=state.param.colors)

@MarcSkovMadsen
Copy link
Collaborator Author

MarcSkovMadsen commented Dec 27, 2024

The points to me are

  • Planning: In your example you have to do more thinking. You can't just insert a Gridbox and then figure out how to insert the content. You have to reorganize your code to make it work. You have to define the gridbox function before you insert it into the Column.
  • Readability: Try comparing your code to the top reflex example. Which one would be easier to read for new users?
  • Not documented. This is is a very common layout operation and in Panel its "left to the reader" to figure out how to do this.
  • Recreation of Gridbox: The code does not exactly do the same thing. In my examples Gridbox is created once. In your examples it is recreated every time the colors list is updated. Try reorganising your pn.bind code to not recreate Gridbox. That is cumbersome.

I think the main and overall point is that our code needs to be organized in a different order to most other frameworks which makes it harder to read and learn. I think the main reason is a missing feature.

@philippjfr
Copy link
Member

To me the rx version is the correct way to do it:

import panel as pn

pn.extension()

class ForEachState(param.Parameterized):
    colors: list[str] = param.List([
        "red",
        "green",
        "blue",
        "yellow",
        "orange",
        "purple",
    ])

def colored_box(color: str):
    return pn.pane.Str(color, styles={"background": color})

state = ForEachState()

pn.Column(
    pn.GridBox(objects=state.param.colors.rx.map(colored_box), ncols=2),
    state.param.colors,
).servable()

This seems pretty close to equivalent to the Reflex version of the code.

The main downside is you use objects which is normal

I'm not sure what this means.

and that you cannot easily add more positional elements to the GridBox

Sure but neither can you do that in the reflex version correct? You can also still easily add more items:

pn.GridBox(objects=state.param.colors.rx.map(colored_box)+extra_items, ncols=2)

@MarcSkovMadsen
Copy link
Collaborator Author

'Objects' refers to the parameter name. I've never seen anyone use it. And the Docs warns against its usage.

@philippjfr
Copy link
Member

And the Docs warns against its usage.

Can you point me to where? Because they definitely shouldn't.

@philippjfr philippjfr added the type: discussion Requiring community discussion label Jan 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: discussion Requiring community discussion
Projects
None yet
Development

No branches or pull requests

3 participants