New Component - Dash Dynamic Grid Layout 🎛️


Downloads
One component I thought dash was missing is one that allows developers to create dynamic layouts for dashboards. With this I found and adapted from this great project :GitHub - react-grid-layout/react-grid-layout: A draggable and resizable grid layout with responsive breakpoints, for React.

And set off to build my own port of a dynamic grid into Plotly’s Dash Framework.

Features Include:

  • Compatible with server-rendered apps
  • Draggable widgets
  • Resizable widgets
  • Static widgets
  • Configurable packing: horizontal, vertical, or None
  • Bounds checking for dragging and resizing
  • Responsive breakpoints
  • Separate layouts per responsive breakpoint

You can add this new component into any Dash application with a simple:
pip install dash-dynamic-grid-layout

I’ve setup an initial example and home for the documentation of this component for developers here:

This shows a basic example of setting up pre-rendered wrapped other components like a map, img and graph. Along with how to dynamically add new components through the newItemTemplate prop.

The github repo of this project:

Check it out, give the github repo a :star: let me know your thoughts, curious to see what you all think.

Happy coding,
Pip

9 Likes

Birds of a feather … :slight_smile:

1 Like

Oh wow, :sweat_smile: first time seeing this package. Didn’t know their was already a dragable wrapper for components within dash. Probably wouldnt have made this package or offer a commit update to that rather than building one from scratch if i knew this existed lol. Just checked out the docs and it looks like a wonderful project. Good to get some attention to this type of design, i really like the feeform app with apple, would be cool to make a web version woth something like this

It’s pretty old. Doesn’t look maintained. Could be wrong though.

But yeah … I’d be keen to see if/where someone might actually use this in practice (i.e. who doesn’t love “drag & drop” right?!)

1 Like

I use it, or something similar.

Since this is now wrapped, and up to date, I can have a demo app setup for it. :nerd_face:

2 Likes

Quick update, I made an upgrade to a 0.0.3 distribution, New features include the ability to set handleText, handleBackground, handleColor in the DraggableWrapper so you can change the color and what it says default is “Drag Me”

Made changes to the DashGridLayout, the add button was removed from automatically being created, now you can create any button from any component and hit the create item prop from the prop: addItem and the prop: newItemTemplat, also added props showRemoveButton: bool and showResizeHandles: bool so you can dynamically turn those features on and off from a button callback example in usage.py.

Documentation of the two components created in this package.

DashGridLayout

Property Type Default Description
id string - The ID used to identify this component in Dash callbacks
children list - A list of dash components to be rendered in the grid
currentLayout list The current layout of the grid items (Not working)
rowHeight number 100 The height of a single row in pixels
cols dict { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 } An object containing breakpoints and column numbers
compactType string ‘vertical’ Compaction type. Can be ‘vertical’, ‘horizontal’, or null
showRemoveButton bool True Whether to show remove buttons for grid items
showResizeHandles bool True Whether to show resize handles for grid items

DraggableWrapper

Property Type Default Description
children node - The content to be wrapped and made draggable
handleBackground string “rgb(85,85,85)” Background color of the drag handle
handleColor string “white” Text color of the drag handle
handleText string “Drag here” Text to display in the drag handle
3 Likes

Big updates & improvements to the project thanks to the work of @jinnyzor

Projects now at pip install dash-dynamic-grid-layout==0.0.4

  • Edit on/off mode have much more of a distinction in style, the drag bar hides after the user sets a Dashboard.
  • Much more snappy in design, resizes much better, retains & tracks wrapper components positions
  • Cleaner Code, updated usage.py to work with react 18.2 and dmc 0.14.3
  • Updated Documentation to reflect more of the project.

Another tip / trick; You can make this project overlay a map via changing the style of the ddgl component with a callback. Specifically the ‘pointerEvents’: ‘none’. For example:

@callback(
    Output('grid-layout', 'showRemoveButton'),
    Output('grid-layout', 'showResizeHandles'),
    Output('grid-layout-container', 'style'),
    Input('edit-mode', 'n_clicks'),

)
def enter_editable_mode(n_clicks):
    if n_clicks % 2 != 0:
        return True, True, {
        'position': 'absolute',
        'right': '-50px',
        'width': '100%',
        'height': '100%',
        'zIndex': '1',
    }
    return False, False, {
        'position': 'absolute',
        'right': '-50px',
        'width': '100%',
        'height': '100%',
        'zIndex': '1',
        'pointerEvents': 'none'
    }
4 Likes

This so cool! Thank you @PipInstallPython and @jinnyzor

Is it possible to limit where you can drag the components? At the moment you can drag the components vertically and horizontally as far as you will. It only “sticks” to its place horizontally though.

I dont want it to be possible to drag components outside of my app.

Here is an example from Dash Pip Components | Dynamic Grid Layout (pip-install-python.com):

Hello @PlotlyDude,

My guess is because there isnt an overflow set, it is just expanding the div to hold onto the data.

If you add a style of overflow: 'auto' or something similar, I believe it will be contained within the div.

1 Like

Thanks! Setting ‘overflow-y’: ‘hidden’ in the usage.py helps a lot, but then it makes it possible to make the component disappear from the screen altogether. This problem is only vertically, horizontal is fine.

1 Like

Auto will apply a scroll when the component goes out of the original viewport. Hidden means that you cant even scroll to it.

Yes sorry about that. I didnt make myself clear.

If you try overflow: ‘auto’ you can still scroll away forever and generaly it just works kinda worse than ‘overflow-y’: ‘hidden’ for some reason:


See draggagle-map-1 y:111.

Here is the same thing with ‘overflow-y’: ‘hidden’ where I just hid one component under the screen and cant get it back:


draggagle-map-1 has y:7 and the user cannot see or access it.

This is what I’m working with in the usage.py and it seems to be working well for me:

import dash_dynamic_grid_layout as dgl
from dash import *
from dash.exceptions import PreventUpdate
import plotly.express as px
import dash_leaflet as dl
import dash_mantine_components as dmc
from dash_iconify import DashIconify
from datetime import datetime, date
import json
import random
import string
import full_calendar_component as fcc
from datetime import datetime

# Set the React version
dash._dash_renderer._set_react_version("18.2.0")

app = Dash(__name__)

# Get today's date
today = datetime.now()

# Format the date
formatted_date = today.strftime("%Y-%m-%d")

# Sample data for the graph
df = px.data.iris()


# Create a Random String ID for the new component
def generate_random_string(length):
    # Define the characters to choose from
    characters = string.ascii_letters + string.digits
    # Generate a random string
    random_string = "".join(random.choice(characters) for _ in range(length))
    return random_string


app.layout = dmc.MantineProvider(
    [
        html.Div(
            [
                dmc.Menu(
                    [
                        dmc.MenuTarget(
                            dmc.ActionIcon(
                                DashIconify(icon="icon-park:add-web", width=20),
                                size="lg",
                                color="#fff",
                                variant="filled",
                                id="action-icon",
                                n_clicks=0,
                                mb=8,
                                style={"backgroundColor": "#fff"},
                            )
                        ),
                        dmc.MenuDropdown(
                            [
                                dmc.MenuItem(
                                    "Add Dynamic Component",
                                    id="add-dynamic-component",
                                    n_clicks=0,
                                ),
                                dmc.MenuItem(
                                    "Edit Dynamic Layout", id="edit-mode", n_clicks=0
                                ),
                            ]
                        ),
                    ],
                    transitionProps={
                        "transition": "rotate-right",
                        "duration": 150,
                    },
                    position="right",
                ),
                dgl.DashGridLayout(
                    id="grid-layout",
                    items=[
                        dgl.DraggableWrapper(
                            children=[
                                dl.Map(
                                    dl.TileLayer(),
                                    center=[56, 10],
                                    zoom=6,
                                    style={
                                        "height": "100vh",
                                        "width": "100vw",
                                    },
                                ),
                            ],
                            id="draggable-map-1",
                            handleBackground="rgb(85,85,85)",
                        ),
                        dgl.DraggableWrapper(
                            html.Img(
                                src="https://picsum.photos/200/300",
                                style={
                                    "width": "100%",
                                    "height": "100%",
                                    "objectFit": "cover",
                                },
                            ),
                            id="draggable-map-2",
                        ),
                        dgl.DraggableWrapper(
                            dcc.Graph(
                                id="example-graph",
                                figure=px.scatter(
                                    df,
                                    x="sepal_width",
                                    y="sepal_length",
                                    color="species",
                                ),
                                style={"height": "100%"},
                            ),
                            id="draggable-map-3",
                        ),
                        dgl.DraggableWrapper(
                            dmc.ColorPicker(
                                id="qr-color-picker",
                                format="rgba",
                                value="rgba(0, 0, 0, 1)",
                                fullWidth=True,
                            ),
                        ),
                        dgl.DraggableWrapper(
                            fcc.FullCalendarComponent(
                                id="api_calendar",  # Unique ID for the component
                                initialView='dayGridMonth',  # dayGridMonth, timeGridWeek, timeGridDay, listWeek,
                                # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek
                                headerToolbar={
                                    "left": "prev,next today",
                                    "center": "",
                                    "right": "",
                                },  # Calendar header
                                initialDate=f"{formatted_date}",  # Start date for calendar
                                editable=True,  # Allow events to be edited
                                selectable=True,  # Allow dates to be selected
                                events=[],
                                nowIndicator=True,  # Show current time indicator
                                navLinks=True,  # Allow navigation to other dates
                            )
                        ),
                    ],
                    showRemoveButton=False,
                    showResizeHandles=False,
                    rowHeight=150,
                    cols={"lg": 12, "md": 10, "sm": 6, "xs": 4, "xxs": 2},
                    style={"height": "800px"},
                    compactType=None,
                    persistence=True,
                ),
                html.Div(id="layout-output"),
                dcc.Store(id="layout-store"),
            ]
        )
    ],
    id="mantine-provider",
    forceColorScheme="light",
)


@callback(Output("layout-store", "data"), Input("grid-layout", "currentLayout"))
def store_layout(current_layout):
    return current_layout


@callback(
    Output("grid-layout", "showRemoveButton"),
    Output("grid-layout", "showResizeHandles"),
    # show how to dynamically change the handle background color of a wrapped component
    Output("draggable-map-1", "handleBackground"),
    Input("edit-mode", "n_clicks"),
    State("grid-layout", "showRemoveButton"),
    State("grid-layout", "showResizeHandles"),
    prevent_initial_call=True,
)
def enter_editable_mode(n_clicks, current_remove, current_resize):
    print("Edit mode clicked:", n_clicks)  # Debug print
    if n_clicks is None:
        raise PreventUpdate
    return not current_remove, not current_resize, "red"


@callback(Output("layout-output", "children"), Input("grid-layout", "itemLayout"))
def display_layout(current_layout):
    if current_layout and isinstance(current_layout, list):
        return ""
    return "No layout data available"


@callback(
    Output("grid-layout", "items"),
    Output("grid-layout", "itemLayout"),
    Input("add-dynamic-component", "n_clicks"),
    prevent_initial_call=True,
)
def add_dynamic_component(n):
    if n:
        items = Patch()
        new_id = generate_random_string(10)
        items.append(
            dgl.DraggableWrapper(
                dcc.Graph(
                    figure=px.scatter(
                        df, x="petal_width", y="petal_length", color="species"
                    ),
                    style={"height": "100%"},
                ),
                id=new_id
            )
        )
        itemLayout = Patch()
        itemLayout.append({"i": f"{new_id}", "w": 6})
        return items, itemLayout
    return no_update, no_update

@callback(
    Output("grid-layout", "items", allow_duplicate=True),
    Input("grid-layout", "itemToRemove"),
    State("grid-layout", "itemLayout"),
    prevent_initial_call=True,
)
def remove_component(key, layout):
    if key:
        items = Patch()
        print(key)
        for i in range(len(layout)):
            if layout[i]['i'] == key:
                del items[i]
                break
        return items
    return no_update


if __name__ == "__main__":
    app.run_server(debug=True, port=8321, host='0.0.0.0')

Their are 3 options for compactType, None, vertically and horizontal. None allows you free movement .

make sure you are on the most recent release 0.1.0 . Need to update the docs but if you setup in the same format of the usage.py I’ve included you wont be able to drag outside the app.

1 Like

Thanks!
I am using 0.1.0, have tried all the compactType options.

I tried the new code you just pasted and the problem still persists more or less with all overflow options unfortunately.

Wrap the dynamic grid in a div wrapper and set the overflow options within that:

 html.Div(
            [
                html.Div(
                dgl.DashGridLayout(
                    id="grid-layout",
                    items=[
                        dgl.DraggableWrapper(
                            children=[
                                
                            ],
                            id="draggable-map-1",
                            handleBackground="rgb(85,85,85)",
                        ),
                    ],
                    showRemoveButton=False,
                    showResizeHandles=False,
                    rowHeight=150,
                    cols={"lg": 12, "md": 10, "sm": 6, "xs": 4, "xxs": 2},
                    style={"height": "800px"},
                    compactType=None,
                    persistence=True,
                ),
                    style={"height": "30vh", 'overflow': 'auto'}, # set height wrap DashGridLayout with overflow
                ),
                html.Div(id="layout-output"),
                dcc.Store(id="layout-store"),
                html.Div(style={"height": "100vh", "backgroundColor": "red"}), # Test setup to see if DraggableWrapper components can be dragged in layout that isn't DashGridLayout

This is what I setup, and I wasn’t able to drag the DraggableWrapper components in the html.Div(style={"height": "100vh", "backgroundColor": "red"}), I think you are trying to setup the overflow auto directly in the DashGridLayout which would not work. You need to place DashGridLayout in a div and use that div’s style to setup the limit on the height, width and overflow settings.

I tried all kind of stuff with your hint, but still the dragging doesnt quite work as intended vertically. Here is my full code:

import dash_dynamic_grid_layout as dgl
from dash import *
from dash.exceptions import PreventUpdate
import plotly.express as px
import dash_leaflet as dl
import dash_mantine_components as dmc
from dash_iconify import DashIconify
from datetime import datetime, date
import json
import random
import string
import full_calendar_component as fcc
from datetime import datetime

# Set the React version
dash._dash_renderer._set_react_version("18.2.0")

app = Dash(__name__)

# Get today's date
today = datetime.now()

# Format the date
formatted_date = today.strftime("%Y-%m-%d")

# Sample data for the graph
df = px.data.iris()


# Create a Random String ID for the new component
def generate_random_string(length):
    # Define the characters to choose from
    characters = string.ascii_letters + string.digits
    # Generate a random string
    random_string = "".join(random.choice(characters) for _ in range(length))
    return random_string


app.layout = dmc.MantineProvider(
    [
        html.Div(
            [
                dmc.Menu(
                    [
                        dmc.MenuTarget(
                            dmc.ActionIcon(
                                DashIconify(icon="icon-park:add-web", width=20),
                                size="lg",
                                color="#fff",
                                variant="filled",
                                id="action-icon",
                                n_clicks=0,
                                mb=8,
                                style={"backgroundColor": "#fff"},
                            )
                        ),
                        dmc.MenuDropdown(
                            [
                                dmc.MenuItem(
                                    "Add Dynamic Component",
                                    id="add-dynamic-component",
                                    n_clicks=0,
                                ),
                                dmc.MenuItem(
                                    "Edit Dynamic Layout", id="edit-mode", n_clicks=0
                                ),
                            ]
                        ),
                    ],
                    transitionProps={
                        "transition": "rotate-right",
                        "duration": 150,
                    },
                    position="right",
                ),
                html.Div(
                dgl.DashGridLayout(
                    id="grid-layout",
                    items=[
                        dgl.DraggableWrapper(
                            children=[
                                
                            ],
                            id="draggable-map-1",
                            handleBackground="rgb(85,85,85)",
                        ),
                    ],
                    showRemoveButton=False,
                    showResizeHandles=False,
                    rowHeight=150,
                    cols={"lg": 12, "md": 10, "sm": 6, "xs": 4, "xxs": 2},
                    style={"height": "800px"},
                    compactType=None,
                    #persistence=True,
                ),
                    style={"height": "70vh", 'overflow': 'auto'}, # set height wrap DashGridLayout with overflow
                ),
                html.Div(id="layout-output"),
                dcc.Store(id="layout-store"),
                html.Div(style={"height": "100vh", "backgroundColor": "red"})
            ]
        )
    ],
    id="mantine-provider",
    forceColorScheme="light",
)


@callback(Output("layout-store", "data"), Input("grid-layout", "currentLayout"))
def store_layout(current_layout):
    return current_layout


@callback(
    Output("grid-layout", "showRemoveButton"),
    Output("grid-layout", "showResizeHandles"),
    # show how to dynamically change the handle background color of a wrapped component
    Output("draggable-map-1", "handleBackground"),
    Input("edit-mode", "n_clicks"),
    State("grid-layout", "showRemoveButton"),
    State("grid-layout", "showResizeHandles"),
    prevent_initial_call=True,
)
def enter_editable_mode(n_clicks, current_remove, current_resize):
    print("Edit mode clicked:", n_clicks)  # Debug print
    if n_clicks is None:
        raise PreventUpdate
    return not current_remove, not current_resize, "red"


@callback(Output("layout-output", "children"), Input("grid-layout", "itemLayout"))
def display_layout(current_layout):
    if current_layout and isinstance(current_layout, list):
        return ""
    return "No layout data available"


@callback(
    Output("grid-layout", "items"),
    Output("grid-layout", "itemLayout"),
    Input("add-dynamic-component", "n_clicks"),
    prevent_initial_call=True,
)
def add_dynamic_component(n):
    if n:
        items = Patch()
        new_id = generate_random_string(10)
        items.append(
            dgl.DraggableWrapper(
                dcc.Graph(
                    figure=px.scatter(
                        df, x="petal_width", y="petal_length", color="species"
                    ),
                    style={"height": "100%"},
                ),
                id=new_id
            )
        )
        itemLayout = Patch()
        itemLayout.append({"i": f"{new_id}", "w": 6})
        return items, itemLayout
    return no_update, no_update

@callback(
    Output("grid-layout", "items", allow_duplicate=True),
    Input("grid-layout", "itemToRemove"),
    State("grid-layout", "itemLayout"),
    prevent_initial_call=True,
)
def remove_component(key, layout):
    if key:
        items = Patch()
        print(key)
        for i in range(len(layout)):
            if layout[i]['i'] == key:
                del items[i]
                break
        return items
    return no_update


if __name__ == "__main__":
    app.run_server(debug=True)

with compactType=‘vertical’ you can only move components left to right, their is no overflow available for this on the y axis from what I can see.

with compactType=None you can move components anywhere on the layout but their is only horizontal overflow and no vertical overflow

with compactType='horizontal` you can only move components up and down and they cant wonder past the most previous wrapper above them.

This is my test showing it can scroll and the dynamic wrappers dont bleed in the red box using the code you sent me.

Hello,

it looks great. I worked on something similar before, but still did not finish it :smiley:

I have a question regarding the feature which looks something similar like a toolbox.

How does it behave? Is the content really removed from the page or just hidden? I am asking because linked to it are also some callbacks, which might be triggered, without knowing.

Thank you :slight_smile:

1 Like

wow you are so Genius !!!1

2 Likes