plotly / dash

Data Apps & Dashboards for Python. No JavaScript Required.
https://plotly.com/dash
MIT License
21.17k stars 2.04k forks source link

add new API for defining the layout of the app #1282

Open gioxc88 opened 4 years ago

gioxc88 commented 4 years ago

*** UPDATE *** I created this repo also available on PyPi here, if you want to test the API I propose in this post. It is very simple to use. First install it with pip:

pip install dash-wrapper

Then, instead of:

import dash_core_components as dcc
import dash_html_components as html

element1 = html.Div()
element2 = dcc.Input()

you would simply do:

import dash_wrapper as dw

element1 = dw.Div()
element2 = dw.Input()

This solves the problem about changing the code of the Component class in dash.development.base_component. Instead of a core feature of dash, it simply becomes an extension.

You can then refer to the README.md file in this repo for the logic of how to create the layout, with examples. A quick summary of the logic:

This API implements the nesting of elements as a single chain of arithmetic operations.

There are 5 simple rules to define the layout :

  1. *add an element on the lower level with ``**
  2. add an element on the same level with @
  3. add an element on the previous level with /
  4. add an element on the nth previous level with /n/
  5. add an element on specific level with %n%

*** END UPDATE ***

rbpatt2019 commented 4 years ago

Love the idea! Much easier to read and follow IMHO. One potential catch is that, for this to work as I understand, every element in dash_core_components and dash_html_components would have to inherit these properties. If all you want to do is put divs inside divs, this works as is. But, if I put something else in my divs, then this fails as the methods aren't recognised by the new items (say dcc.Graph). This is probably easily solved, though, as you also mention - create a bas class that overloads the operators. Would just need to make sure that everything in dcc and html inherits from it.

gioxc88 commented 4 years ago

Love the idea! Much easier to read and follow IMHO. One potential catch is that, for this to work as I understand, every element in dash_core_components and dash_html_components would have to inherit these properties. If all you want to do is put divs inside divs, this works as is. But, if I put something else in my divs, then this fails as the methods aren't recognised by the new items (say dcc.Graph). This is probably easily solved, though, as you also mention - create a bas class that overloads the operators. Would just need to make sure that everything in dcc and html inherits from it.

Yes exactly! My idea is exactly that all html and dcc components inherits from the base class. I used the Div example just for illustration purposes, but of course it would be unusable if it worked only for Div. This methods must be shared across all html and dcc components

alexcjohnson commented 4 years ago

@gioxc88 thanks for opening this issue! We've been pondering exactly this issue for a while, so it's great to see other folks thinking creatively about how to make the API more readable and useful. You sparked a good deal of discussion among Plotly staff 🍻 @Marc-Andre-Rivet @chriddyp @nicolaskruchten @xhlulu @dmt0 I hope you don't mind me paraphrasing your comments 😈

First off, there is a natural place already to put functionality like this - dash.development.base_component.Component. All components inherit from this class, so if we put it here we don't need to do anything in the component packages; any component with children will be able to use this functionality.

Doing this with operator overloading is certainly compact, but has a couple of undesirable features. It's a lot of new syntax that doesn't necessarily map intuitively to the new meanings; moving components around sometimes requires altering neighboring operators to keep everything in sync; and for those of us that use linters (Plotly's Python code is gradually adopting black) the nice indentation will likely not survive.

But one idea that came up while looking at your approach is to make components callable, ie:

Div(**props)(children) == Div(children, **props)

There are various ways this might work - maybe the second call has the same signature as the first, so you could also use this to alter any props, which would also allow you to reuse partially-defined components, like:

StyledDiv = Div(style={...})
app.layout = Div([StyledDiv(message1, id="msg1"), StyledDiv(message2, id="msg2")])

Or maybe the second call is reserved only for children, in which case it could be variadic and we can drop the [] around multiple children. The point is this lets you cleanly put children at the end, with nice indentation that matches the code structure so black and other linters won't complain:

Div(id='Parent')(
    Div(id='Child1'),
    Div(id='Child2')(
        Div(id='SubChild1'),
        Div(id='SubChild2')(
            Div(id='SubSubChild1'),
            Div(id='SubSubChild2')(
                Div(id='SubSubSubChild1')
            ),
            Div(id='SubSubChild3')(
                Div(id='SubSubSubChild2'),
                Div(id='SubSubSubChild3')
            )
        )
    ),
    Div(id='Child3')
)

The order of information is exactly the same as in the operator version. The trailing parens are new, though if your linter allows, you can put them on the previous line - ))) is equivalent to *3* - giving the same number of lines as with operators.

This is all still up for discussion - we also need to weigh it against concerns for example that this obscures how children is a prop like any other so can be set by callbacks. But I'd be curious to hear what folks think of the callable version, and again thanks @gioxc88 for kicking this off 🎉

gioxc88 commented 4 years ago

Hello and thank you for the answer. That is exactly what I did yesterday after I made the post. I changed the Component class and tested extensively. I also changed the logic but not the API itself.

I appreciate your solution involving the callability. The only issue I see with that is still managing parenthesis, but it's certainly easier compared to the current way of defining children and it looks nice.

The main reason I wanted to go with arithmetic operations is because it feels more pythonic in the sense that it is a bit like the indentation system in python: when you write conditional statements or loops you don't have to worry about parenthesis and this is one of the main things that IMHO makes python great.

In my implementation the operators * / @ behave a bit like the : in python when you write a statement: it is a way to give a meaning to the indentation within the layout.

The indentation is indeed a problem with mt method. In PyCharm if I press CTRL + ALT+ L it disappears.

One thing that I end up doing if I want to allow auto-formatting is to define my layout in a separate module (which I wont format) and then import the layout in the module where my app lives.

I updated my post with my new implementation. please have a look.

Many thanks Gio

gioxc88 commented 4 years ago

I also provide the code for a real application that I tested recently using this new API and everything works flawlessly. Please try it yourself to see how smooth it is to nest and change the layout of the page with this method. Not having to handle parenthesis and commas is life changing for me. This app doesn't really do anything. I only added a stupid callback to generate random numbers in the table. The main purpose is showing the layout syntax with many html and dcc components. It seems to work.

Below the code to reproduce (except for the data in the table which are random in the code below). I apologize for my poor CSS skills but I never learned properly how to use it.

Remember that if you want to use it you still need to modify the Component class in dash.development.base_component as explained in my first post.

image

import dash
import dash_table
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
import numpy as np
import pandas as pd

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

columns = ['ID', 'Date', 'Name', 'Category', 'Color']
table = dash_table.DataTable(id='table',
                             columns=[dict(name=col, id=col) for col in columns],
                             editable=True,
                             filter_action='native',
                             sort_action='native',
                             sort_mode='multi',
                             row_selectable='multi',
                             selected_rows=[],
                             page_action='native',
                             page_current=0,
                             page_size=9)

app.layout = (
        html.Div(className='', style={'margin': '30px'}) *
            html.Img(src='https://upload.wikimedia.org/wikipedia/commons/3/37/Plotly-logo-01-square.png',
                     style={'width': '10%', 'height': 'auto'}) @
            html.Div() *
                html.Div(style={'display': 'inline-block', 'margin-right': '10px', 'vertical-align': 'top'}) *

                    html.Label('start date') @
                    dcc.DatePickerSingle(id='date_start',
                                         placeholder='start date',
                                         persistence=True,
                                         display_format='DD MM YYYY') @
                    html.Label('end date') @
                    dcc.DatePickerSingle(id='date_end',
                                         placeholder='end date',
                                         persistence=True,
                                         display_format='DD MM YYYY') @
                    html.Label('Input1') @
                    dcc.Input(id='input1',
                              type='number',
                              placeholder='input2',
                              persistence=True,
                              style={'width': 130}, min=1) @
                    html.Label('Input2') @
                    dcc.Input(id='input2',
                              type='number',
                              placeholder='input1',
                              persistence=True,
                              style={'width': 130}, min=1) @
                    html.Div(style={'margin-top': '30px'}) *
                        html.Button('generate', id='gen') /2/

                html.Div(id='out', style={'display': 'inline-block', 'vertical-align': 'top'}) *
                    table /2/
            html.Div(id='hidden-div', style={'display': 'none'})
)

@app.callback(Output('table', 'data'),
              [Input('gen', 'n_clicks')])
def gen(n_clicks):
    df = pd.DataFrame(data=np.random.normal(size=(100, 5)),
                      columns=columns)
    return df.to_dict('records')

if __name__ == "__main__":
    app.run_server(debug=True)
psippola commented 4 years ago

I like more the 'callable' approach as * and @ are rather special and hard to understand at first sight. But how about implementing __getitem__ for Components so that @alexcjohnson s example becomes

Div(id='Parent')[
    Div(id='Child1'),
    Div(id='Child2')[
        Div(id='SubChild1'),
        Div(id='SubChild2')[
            Div(id='SubSubChild1'),
            Div(id='SubSubChild2')[
                Div(id='SubSubSubChild1')
            ],
            Div(id='SubSubChild3')[
                Div(id='SubSubSubChild2'),
                Div(id='SubSubSubChild3')
            ]
        ]
    ],
    Div(id='Child3')
]

I think that square brackets make the code much more readable. In general, its much like html - you first define each components attributes, then its children.

Could work something like this:

import copy

class Component:

    def __init__(self, id, children=None):

        self.id=id
        self.children=children or []

    def __getitem__(self, *children):
        component = copy.deepcopy(self)
        component.children = list(children)
        return component

    def __repr__(self):
        return str(self.__dict__)

component = Component(id='item')[
    Component('subitem'),
    Component('subitem2')[
        Component('subsubitem')
    ]
]

print(component)
chriddyp commented 4 years ago

In general, its much like html - you first define each components attributes, then its children.

FWIW, this general principle of defining component attributes first and then it's children is possible right now with setting children= as a keyword argument. Any time that children is long and there are component attributes, I usually organize my code this way so that the component attributes aren't trailing at the end.

So, instead of this:

html.Div([
    html.Div([
        # ...
    ]),

    # ...

    html.Div([
        html.Div([
            # ...
            # ...
            # This could be like 100 lines
            # ...
        ], id='my-child-div', style={'color': 'red'})

        # ...
        # There could be another set of components in here
        # that could be another 100 lines...
        # ...

    ], id='my-parent-div', style={'color': 'blue'}),
])

You can write this (today!):

html.Div([
    html.Div([
        # ...
    ]),

    # ...

    html.Div(
        id='my-parent-div',
        style={'color': 'blue'},
        children=[
            html.Div(
                id='my-child-div',
                style={'color': 'red'},
                children=[
                    # ...
                    # ...
                    # This could be like 100 lines
                    # ...
                ]
            )
        ]
    )
])

With callables, some indentation would be saved at the expense of another set of parantheses:

html.Div([
    html.Div([
        # ...
    ]),

    # ...

    html.Div(
        id='my-parent-div',
        style={'color': 'blue'}
    )(
        html.Div(
            id='my-child-div',
            style={'color': 'red'},
            children=[
                # ...
                # ...
                # This could be like 100 lines
                # ...
            ]
        )
    )
])
gioxc88 commented 4 years ago

@psippola I appreciate the suggestion of getitem but honestly I don't see how much this is different from the current way of defining children

nicolaskruchten commented 4 years ago

I would favour a more-explicit variadic .children(...) method to override children than a callable, to avoid lines containing only )( ... they would become ).children(, making it really clear that these are the children.

My biggest concern with the operator-based approach is that the number matters too much i.e. with *3* if you want to change the nesting level you need to increment/decrement these numbers everywhere. My secondary concern is that these expressions will not survive running through black, as mentioned.

psippola commented 4 years ago

@chriddyp, what I mean is that callable (implementing __get__) or implementing __getitem__ result in code which resembles html: Attributes are defined inside first parantheses, resembling defining attributes inside a html start tag. Then inside the next pair of parantheses or brackets are listed the children of the component:

<div id='item'>Child</div>

vs.

Div(id='item')[Child]

I just think that square brackets are more readable than another parantheses: one could avoid )( problem also mentioned by @nicolaskruchten.

@gioxc88 it's just level of nesting and readability

gioxc88 commented 4 years ago

***UPDATE*** I created this repo also available on PyPi here, if you want to test the API I propose in this post. It is very simple to use. First install it with pip:

pip install dash-wrapper

Then, instead of:

import dash_core_components as dcc
import dash_html_components as html

element1 = html.Div()
element2 = dcc.Input()

you would simply do:

import dash_wrapper as dw

element1 = dw.Div()
element2 = dw.Input()

This solves the problem about changing the code of the Component class in dash.development.base_component. Instead of a core feature of dash, it simply becomes an extension.