New Component - Dash Dynamic Grid Layout 🎛️

Dynamic Grid Example

This ddgl component is accessible through callbacks, the data appears in a format of:

[{"i": "draggable-map-1", "x": 5, "y": 0, "w": 7, "h": 4}, {"i": "draggable-map-2", "x": 7, "y": 4, "w": 2, "h": 2}, {"i": "draggable-map-3", "x": 5, "y": 4, "w": 2, "h": 2}, {"i": "3", "x": 2, "y": 6, "w": 2, "h": 2}, {"i": "4", "x": 0, "y": 0, "w": 5, "h": 6}]

With removing components this is done through setting up a callback of:

@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

It basically loops through the existing list of components and deletes the one that was removed.

You can also pre define a layout on load with a ddgl setup similar to:

import dash_dynamic_grid_layout as ddgl
from dash import Dash, html, dcc
import plotly.express as px

app = Dash(__name__)

df = px.data.iris()

app.layout = html.Div([
    ddgl.DashGridLayout(
        id='grid-layout',
        items=[
            ddgl.DraggableWrapper(html.Div('Item 1', ), id='item1'),
            ddgl.DraggableWrapper(html.Div('Item 2',), id='item2')
        ],
        itemLayout=[
            {'i': 'item1', 'x': 0, 'y': 0, 'w': 2, 'h': 4},
            {'i': 'item2', 'x': 4, 'y': 0, 'w': 4, 'h': 2}
        ],
        rowHeight=150,
        cols={'lg': 12, 'md': 10, 'sm': 6, 'xs': 4, 'xxs': 2},
        style={'height': '600px'},
    )
])


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

basically you assign the Wrappers with unique id’s and connect those id’s in the itemLayout with the associated location width and size you’d like on load.

When designing the component I used a dmc.Menu to show two options for the ddgl component, which could be triggered with callbacks the add_dynamic_component and the edit_mode .

The idea was to be able to enter edit mode, move components around and save them to the layout. Once placed and exited out of edit mode the components within the wrapper take up the full size allotted to them without the possibility of accidentally closing or moving them without re-entering edit mode.

@jinnyzor Has really polished out the package, I’ve initially come up with concept and built the documentation around the component but BSDEV has put in a ton of work getting the project to an official 0.1.0 State, everything is running a lot more polished and I’ll be working on better documentation when I find the time in the near future but their should be enough references to start using it. This component shouldn’t conflict with other callbacks if you structure it properly and we have tested it on most browsers and operating systems from computers to tablets to phones.

1 Like

This is great - thx for pulling this together. I was testing it however, and found that if you have callbacks in the DraggableWrapper they where being blocked by the looks of it and not updating any components. i can see the callbacks being triggered and the callback function running but the output is not being rendered in the DraggableWrapper. I was wonderinng if you have tried somethign like this or is it something im doing wrong…cheers

@jinnyzor

  • fixed issue with allowing callbacks to re-render the layout
  • this created another issue with the max call stack
    • fixed this by reducing the circular variables created in data-grid
  • removed persistence as it is no longer supported

Basically the grid looked really nice but the components within a DraggableWrapper their callbacks wouldn’t work. The project removed the persistence for the moment and made some other changes to allow callbacks within DraggableWrappers to work. But on refresh the DashGridLayout resets to its origin layout.

Fixed a major bug, lost the cool feature of persistence.

Newest Release: dash-dynamic-grid-layout==0.1.1

2 Likes

Hi,

First of all: Amazing component, I was looking for this.

Question: Is there an easy way to align this component with DMC Theming, including theme switch?

You can add custom theme via the props style in DashGridLayout or via handleBackground, handleColor , or handleText in the DraggableWrapper

check out the README.md for for information:

or the docs:

Basically you can hit those prop endpoint with a callback to change the style dynamically from a dmc switch component fairly easily.

Unfortunately there seems to be problems with the new Dash versions. I have essentially copy of usage.py here:

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(
            [
                html.Center(html.H4("json.dumps(current_layout)")),
                html.Hr(),
                html.Div(id="layout-output"),
                html.Hr(),
                dmc.Group([
                    html.H4("Add items or edit the layout ->"),
                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",
                            handleBackground="rgb(85,85,85)",
                        ),
                        dgl.DraggableWrapper(
                            html.Img(
                                src="https://picsum.photos/200/300",
                                style={
                                    "width": "100%",
                                    "height": "100%",
                                    "objectFit": "cover",
                                },
                            ),
                            id="draggable-image",
                        ),
                        dgl.DraggableWrapper(
                            dcc.Graph(
                                id="example-graph",
                                figure=px.scatter(
                                    df,
                                    x="sepal_width",
                                    y="sepal_length",
                                    color="species",
                                ),
                                style={"height": "100%"},
                            ),
                            id="draggable-graph",
                        ),
                        dgl.DraggableWrapper(
                            dmc.ColorPicker(
                                id="qr-color-picker",
                                format="rgba",
                                value="rgba(0, 0, 0, 1)",
                                fullWidth=True,
                            ),
                            id="draggable-color-picker",
                        ),
                        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
                            ),
                            id="draggable-calendar"
                        ),
                    ],
                    itemLayout=[
                        # wrapper id, x(0-12), y, w(0-12), h(0-12)
                        {'i': 'draggable-map', 'x': 0, 'y': 0, 'w': 6, 'h': 4},
                        {'i': 'draggable-image', 'x': 4, 'y': 0, 'w': 4, 'h': 2},
                        {'i': 'draggable-graph', 'x': 0, 'y': 4, 'w': 6, 'h': 4},
                        {'i': 'draggable-color-picker', 'x': 6, 'y': 2, 'w': 3, 'h': 2},
                        {'i': 'draggable-calendar', 'x': 6, 'y': 4, 'w': 6, 'h': 4}
                    ],
                    showRemoveButton=False,
                    showResizeHandles=False,
                    rowHeight=150,
                    cols={"lg": 12, "md": 10, "sm": 6, "xs": 4, "xxs": 2},
                    style={"height": "800px"},
                    compactType="horizontal",
                    # persistence=True,
                ),
                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", "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 html.Div(json.dumps(current_layout))
    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(debug=True)

With Dash version 3.0.1, if you remove any component via edit mode and clicking X, you will get multiple scary error messages. With versions 3.0.2 and 3.0.3, you can remove components with no problem, but after adding one new component you cannot add anymore of them.

Yeah their was some bugs in dash 3.0.1 and 3.0.2 that broke the component but most of those bugs where resolved in dash 3.0.3.

The component hasnt specifically been designed with dash 3 in mind so their might be some bugs.

Try working from this example:

If the adding new components still doesnt work. Please create a bug report on the repo with a limited working example so that we can track it.

Pretty busy with other work so im not entirely sure when ill be able to migrate everything over to dash 3 but ill try my best. :sweat_smile:
@PlotlyDude

2 Likes

Thank you for your fast response. Sorry I could not get into this earlier.

The issue is actually with the dmc,MenuItem, which kinda broke after 3.0.1: Problem with Dash 3.0.2 with dmc - Dash Python - Plotly Community Forum
It seems that the Dash Mantine Components still needs an update as the problem still persists with Dash 3.0.3. That is why the the dynamic add button doesnt work in the examples. I kinda should have known this, but kinda forgot about it, sorry. Here I added html.button to do the adding of draggable components and everything seems to work fine.

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(
            [
                html.Center(html.H4("json.dumps(current_layout)")),
                html.Hr(),
                html.Div(id="layout-output"),
                html.Hr(),
                dmc.Group([
                    html.H4("Add items or edit the layout ->"),
                    html.Button(
                                    "Add Dynamic Component",
                                    id="add-dynamic-component",
                                    n_clicks=0,
                                ),
                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-componentX",
                                    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",
                            handleBackground="rgb(85,85,85)",
                        ),
                        dgl.DraggableWrapper(
                            html.Img(
                                src="https://picsum.photos/200/300",
                                style={
                                    "width": "100%",
                                    "height": "100%",
                                    "objectFit": "cover",
                                },
                            ),
                            id="draggable-image",
                        ),
                        dgl.DraggableWrapper(
                            dcc.Graph(
                                id="example-graph",
                                figure=px.scatter(
                                    df,
                                    x="sepal_width",
                                    y="sepal_length",
                                    color="species",
                                ),
                                style={"height": "100%"},
                            ),
                            id="draggable-graph",
                        ),
                        dgl.DraggableWrapper(
                            dmc.ColorPicker(
                                id="qr-color-picker",
                                format="rgba",
                                value="rgba(0, 0, 0, 1)",
                                fullWidth=True,
                            ),
                            id="draggable-color-picker",
                        ),
                        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
                            ),
                            id="draggable-calendar"
                        ),
                    ],
                    itemLayout=[
                        # wrapper id, x(0-12), y, w(0-12), h(0-12)
                        {'i': 'draggable-map', 'x': 0, 'y': 0, 'w': 6, 'h': 4},
                        {'i': 'draggable-image', 'x': 4, 'y': 0, 'w': 4, 'h': 2},
                        {'i': 'draggable-graph', 'x': 0, 'y': 4, 'w': 6, 'h': 4},
                        {'i': 'draggable-color-picker', 'x': 6, 'y': 2, 'w': 3, 'h': 2},
                        {'i': 'draggable-calendar', 'x': 6, 'y': 4, 'w': 6, 'h': 4}
                    ],
                    showRemoveButton=False,
                    showResizeHandles=False,
                    rowHeight=150,
                    cols={"lg": 12, "md": 10, "sm": 6, "xs": 4, "xxs": 2},
                    style={"height": "1500px", 'backgroundColor': 'green'},
                    compactType=None,
                    autoSize = False,
                    maxRows = 40,
                    margin={"lg": [1, 1], "md": [1, 1], "sm": [6, 6], "xs": [4, 4], "xxs": [2, 2]}, #Margin between draggable components in pixels.
                    allowOverlap = True, #Components can now overlap each other.
                    draggableChildStyle = { ##Smaller padding, max height and backgroundcolor(for padding in practice)
                    'overflow': 'hidden',
                    'maxHeight': '100%',
                    'maxWidth': '100%',
                    "border": "5px solid yellow", ## This sort of creates some coloured extra padding for the handles.
                    "boxSizing": "border-box",     # Ensures border doesn't mess up sizing
                            },
                  
                ),
                dcc.Store(id="layout-store"),
            ], style={ 'backgroundColor': 'blue', 'overflowY':'hidden', 'height': '1200px'}
        )
    ],
    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", "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 html.Div(json.dumps(current_layout))
    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(debug=True)

So actually the Dash Dynamic Grid Layout is as marvelous as it has ever been.

1 Like

Hello @PlotlyDude,

You should be using 3.0.3+ and 1.1.2rc1 for dmc.

2 Likes

Thank you! I did not realize there was a new version as pipy or dmc website did not show this.
With Dash 3.0.3 and dmc 1.1.2.rc1 everything seems to work fabulously!

1 Like