Is it possible to drag and drop elements into a Dash DataTable?

I currently have a Dash DataTable and I would like to be able to drag and drop elements into certain rows within a single column. Is this at all possible? I have some callbacks related to active cells/filtering but I am thinking that perhaps using Dash AG Grid or HTML might be a better solution.

Thanks in advance! :slight_smile:

Hello @Svendiagram,

Welcome to the community!

I would recommend going the route of trying to implement this in dag, there are quite a few documents to get you started down that route.

I’ve done it recently in a project of mine as well.

Thanks I’ve made decent progress so far! I have a table on one side and on the other side I have some cards that I want to drag into the table. Is it possible to drag a card (i.e some text) into a specific cell? It currently defaults to creating a new row entry.

For example, I want to add a description next to these IDs. However, they default to a new row:

image

Really, it’s up to you and how you want to create it. There are a couple of examples on AG grids site to create cards.

Cheers! :slight_smile: Do you have any links to examples specifically for dragging cards to a specific AG Grid cell? I’ve been searching but can’t seem to find much. All I’m able to do is create new rows rather than edit current ones. Perhaps I’ve missed it as I have no prior knowledge of JavaScript.

To edit an existing row, you need to first know what row it is hovering over, typically referred to as the overNode, you can then perform an applyTransaction update with the new data against the api.

1 Like

Thanks that was useful! Doing some logging I can get the specific cell to updated. However, it doesn’t seem to refresh the grid. Is there something I’m missing?

app.clientside_callback(
    """
    function (p) {
        console.log("JavaScript code executed!");
        var gridOrigin;
        var gridDragOver = (event) => {
            console.log("dragOver event triggered!");
            const dragSupported = event.dataTransfer.types.length;

            if (dragSupported) {
                event.dataTransfer.dropEffect = 'copy';
                event.preventDefault();
            }
        };

        var gridDragStart = (origin, event) => {
            console.log("dragStart event triggered!");
            gridOrigin = origin;
        
            // Get the dragged data (JSON string)
            const jsonData = event.dataTransfer.getData('application/json');
            console.log("Dragged data:", jsonData);
        
            // Parse the JSON data to obtain the card value
            const data = JSON.parse(jsonData);
            console.log("Card value being dragged:", data.card);
        }

        var gridDrop = (target, event) => {
            console.log("drop event triggered!");
            event.preventDefault();
        
            const jsonData = event.dataTransfer.getData('application/json');
            const data = JSON.parse(jsonData);
        
            // If data is missing or the drop target is the same as the origin, do nothing
            if (!data || target == gridOrigin) {
                console.log("Data missing or same target as origin. Exiting.");
                return;
            }
        
            // Get the grid API for the target
            const gridApi = dash_ag_grid.getApi(target);
        
            // Get the overNode (row) based on drop coordinates
            const overNode = gridApi.getRenderedNodes().find(node => {
                const rowTop = node.rowTop;
                const rowBottom = node.rowTop + node.rowHeight;
        
                return rowTop <= event.clientY && event.clientY <= rowBottom;
            });
        
            console.log("overNode:", overNode);
        
            // Log the existing data for the overNode
            console.log("Existing data for overNode:", overNode.data);
        
            // Update the specific row with the dragged card value
            const updatedData = { ...overNode.data, card: data.card };
            console.log("Updated data:", updatedData);
        
            gridApi.applyTransactionAsync({
                'update': [{ data: updatedData }]
            });
        };


        setTimeout(() => {
            document.querySelector('#token_table').addEventListener('dragstart', (event) => gridDragStart('token_table', event))
            document.querySelector('#cards_table').addEventListener('dragstart', (event) => gridDragStart('cards_table', event))

            document.querySelector('#token_table').addEventListener('dragover', gridDragOver)
            document.querySelector('#cards_table').addEventListener('dragover', gridDragOver)
            document.querySelector('#cards_table').addEventListener('drop', (event) => gridDrop('cards_table', event))
            document.querySelector('#token_table').addEventListener('drop', (event) => gridDrop('token_table', event))
        }, 500)

        return window.dash_clientside.no_update
    }
    """,
    Output('cards_table', 'id'),
    Input('cards_table', 'id')
)

I think that this:

gridApi.applyTransactionAsync({
                'update': [{ data: updatedData }]
            });

should be this:

gridApi.applyTransactionAsync({
                'update': [updatedData ]
            });

It didn’t work :frowning: I apologise for all the questions - as it’s my first time interacting with JS I’m sort of struggling. With my logs I can see that the const updatedData is correct. I don’t think it’s executing though as when I click on the cell afterwards, it is back to it’s original state. My logs are showing as such that the card value is being replaced:

Card value being dragged:
second, 4.163, 4.483 

Existing data for overNode: 
Object { card: "TESTESTTEST" }

Updated data:
Object { card: "second, 4.163, 4.483" }

Can you please provide a full working example for me to troubleshoot?

Here is how it should be done I think:

import dash
from dash import dcc, html
from dash.dependencies import State, Input, Output
from dash import dash_table
from dataclasses import dataclass
import pandas as pd
import dash_ag_grid as dag
import pandas as pd
from dash import Dash, html, Output, Input, dcc


@dataclass()
class Token:
    id: int
    text: str
    upos: str
    xpos: str
    start_char: int
    end_char: int
    feats: dict[str, str]


@dataclass()
class Audio:
    word: str
    start: float
    end: float


app = dash.Dash(__name__)


def get_text_data():

    TOKENS = [
        Token(id=1, text='Pride', upos='PROPN', xpos='NNP', start_char=0, end_char=5, feats={'Number': 'Sing'}),
        Token(id=2, text='and', upos='CCONJ', xpos='CC', start_char=6, end_char=9, feats=None),
        Token(id=3, text='Prejudice', upos='PROPN', xpos='NNP', start_char=10, end_char=19,feats={'Number': 'Sing'}),
        Token(id=4, text='is', upos='AUX', xpos='VBZ', start_char=20, end_char=22,feats={'Mood': 'Ind', 'Number': 'Sing', 'Person': '3', 'Tense': 'Pres', 'VerbForm': 'Fin'}),
        Token(id=5, text='the', upos='DET', xpos='DT', start_char=23, end_char=26,feats={'Definite': 'Def', 'PronType': 'Art'}),
    ]


    df = pd.DataFrame(TOKENS)
    df["mergeUp"] = "↑"
    df["mergeDown"] = "↓"
    df["split"] = ""
    df["card"] = "This a test card"

    for col in ["id", "xpos", "upos", "start_char", "end_char"]:
        df[col] = df[col].apply(lambda x: x if isinstance(x, list) else [x])

    df = df[["id", "text", "upos", "xpos", "start_char", "end_char", "mergeUp", "mergeDown", "split", "card"]]
    return df.to_dict("records")


def get_audio_data():
    AUDIO_EN = [
        Audio(word='Pride', start=2.062, end=2.382),
        Audio(word='and', start=2.422, end=2.522),
        Audio(word='Prejudice', start=2.582, end=3.103),
        Audio(word='is', start=3.863, end=3.943),
        Audio(word='the', start=3.983, end=4.103),
    ]
    df = pd.DataFrame(AUDIO_EN)
    # df['card'] = df.apply(lambda row: [row['word'], row['start'], row['end']], axis=1)
    df['card'] = df.apply(lambda row: f"{row['word']}, {row['start']}, {row['end']}", axis=1)

    # Drop the original columns if needed
    df = df.drop(['word', 'start', 'end'], axis=1)
    return df.to_dict("records")


app = Dash(__name__)

columnDefs = [
    {"field": "id"},
    {"field": "text"},
    {"field": "upos"},
    {"field": "xpos"},
    {"field": "start_char"},
    {"field": "end_char"},
    {"field": "mergeUp"},
    {"field": "mergeDown"},
    {"field": "split"},
    {"field": "card", "dndSource": True}
]

# {'name': 'DnD', 'id': 'dragDrop'},

app.layout = html.Div(
    [
        dag.AgGrid(
            columnDefs=columnDefs,
            rowData=get_text_data(),
            columnSize="sizeToFit",
            defaultColDef={"resizable": True, "sortable": True, "filter": True},
            dashGridOptions={"rowDragManaged": True, "animateRows": True},
            style={'width': '60%', "height": "90vh"},
            id='token_table'
        ),
        dag.AgGrid(
            columnDefs=[{"field": "card", "dndSource": True}],
            rowData=get_audio_data(),
            columnSize="sizeToFit",
            defaultColDef={"resizable": True, "sortable": True, "filter": True},
            dashGridOptions={"rowDragManaged": True},
            style={'width': '40%', "height": "90vh"},
            id='cards_table'
        ),
        # html.Button(id='drop1', style={'display': 'none'}, n_clicks=0),
        # html.Button(id='drop2', style={'display': 'none'}, n_clicks=0),
        dcc.Store(id='dropStore', storage_type='session')
    ],
    style={"display": "flex", "height": "90vh"},
)


## enable drag-n-drop
app.clientside_callback(
    """
    function (p) {
        console.log("JavaScript code executed!");
        var gridOrigin;
        var gridDragOver = (event) => {
            console.log("dragOver event triggered!");
            const dragSupported = event.dataTransfer.types.length;

            if (dragSupported) {
                event.dataTransfer.dropEffect = 'copy';
                event.preventDefault();
            }
        };

        var gridDragStart = (origin, event) => {
            console.log("dragStart event triggered!");
            gridOrigin = origin;

            // Get the dragged data (JSON string)
            const jsonData = event.dataTransfer.getData('application/json');
            console.log("Dragged data:", jsonData);

            // Parse the JSON data to obtain the card value
            const data = JSON.parse(jsonData);
            console.log("Card value being dragged:", data.card);
        }

        var gridDrop = (target, event) => {
            console.log("drop event triggered!");
            event.preventDefault();

            const jsonData = event.dataTransfer.getData('application/json');
            const data = JSON.parse(jsonData);

            // If data is missing or the drop target is the same as the origin, do nothing
            if (!data || target == gridOrigin) {
                console.log("Data missing or same target as origin. Exiting.");
                return;
            }

            // Get the grid API for the target
            const gridApi = dash_ag_grid.getApi(target);

            // Get the overNode (row) based on drop coordinates
            const overNode = gridApi.getRenderedNodes().find(node => {
                const rowTop = node.rowTop;
                const rowBottom = node.rowTop + node.rowHeight;

                return rowTop <= event.clientY && event.clientY <= rowBottom;
            });

            console.log("overNode:", overNode);

            // Log the existing data for the overNode
            console.log("Existing data for overNode:", overNode.data);

            // Update the specific row with the dragged card value
            const updatedData = { ...overNode.data, card: data.card };
            console.log("Updated data:", updatedData);

            gridApi.applyTransactionAsync({'update': [updatedData]});
        };


        setTimeout(() => {
            document.querySelector('#token_table').addEventListener('dragstart', (event) => gridDragStart('token_table', event))
            document.querySelector('#cards_table').addEventListener('dragstart', (event) => gridDragStart('cards_table', event))

            document.querySelector('#token_table').addEventListener('dragover', gridDragOver)
            document.querySelector('#cards_table').addEventListener('dragover', gridDragOver)
            document.querySelector('#cards_table').addEventListener('drop', (event) => gridDrop('cards_table', event))
            document.querySelector('#token_table').addEventListener('drop', (event) => gridDrop('token_table', event))
        }, 500)

        return window.dash_clientside.no_update
    }
    """,
    Output('cards_table', 'id'),
    Input('cards_table', 'id')
)

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

Also, to be able to update your data in the grid, you need to set row ids that are unique for the data.

Once you do that, I believe you should be good with the drag and drop.

Here is a working example, also note, I adjusted your overnode code to work (it was shifted one) :slight_smile:

import dash
from dash import dcc, html
from dash.dependencies import State, Input, Output
from dash import dash_table
from dataclasses import dataclass
import pandas as pd
import dash_ag_grid as dag
import pandas as pd
from dash import Dash, html, Output, Input, dcc


@dataclass()
class Token:
    id: int
    text: str
    upos: str
    xpos: str
    start_char: int
    end_char: int
    feats: dict[str, str]


@dataclass()
class Audio:
    word: str
    start: float
    end: float


app = dash.Dash(__name__)


def get_text_data():

    TOKENS = [
        Token(id=1, text='Pride', upos='PROPN', xpos='NNP', start_char=0, end_char=5, feats={'Number': 'Sing'}),
        Token(id=2, text='and', upos='CCONJ', xpos='CC', start_char=6, end_char=9, feats=None),
        Token(id=3, text='Prejudice', upos='PROPN', xpos='NNP', start_char=10, end_char=19,feats={'Number': 'Sing'}),
        Token(id=4, text='is', upos='AUX', xpos='VBZ', start_char=20, end_char=22,feats={'Mood': 'Ind', 'Number': 'Sing', 'Person': '3', 'Tense': 'Pres', 'VerbForm': 'Fin'}),
        Token(id=5, text='the', upos='DET', xpos='DT', start_char=23, end_char=26,feats={'Definite': 'Def', 'PronType': 'Art'}),
    ]


    df = pd.DataFrame(TOKENS)
    df["mergeUp"] = "↑"
    df["mergeDown"] = "↓"
    df["split"] = ""
    df["card"] = "This a test card"

    for col in ["id", "xpos", "upos", "start_char", "end_char"]:
        df[col] = df[col].apply(lambda x: x if isinstance(x, list) else [x])

    df = df[["id", "text", "upos", "xpos", "start_char", "end_char", "mergeUp", "mergeDown", "split", "card"]]
    return df.to_dict("records")


def get_audio_data():
    AUDIO_EN = [
        Audio(word='Pride', start=2.062, end=2.382),
        Audio(word='and', start=2.422, end=2.522),
        Audio(word='Prejudice', start=2.582, end=3.103),
        Audio(word='is', start=3.863, end=3.943),
        Audio(word='the', start=3.983, end=4.103),
    ]
    df = pd.DataFrame(AUDIO_EN)
    # df['card'] = df.apply(lambda row: [row['word'], row['start'], row['end']], axis=1)
    df['card'] = df.apply(lambda row: f"{row['word']}, {row['start']}, {row['end']}", axis=1)

    # Drop the original columns if needed
    df = df.drop(['word', 'start', 'end'], axis=1)
    df['id'] = df.index
    return df.to_dict("records")


app = Dash(__name__)

columnDefs = [
    {"field": "id"},
    {"field": "text"},
    {"field": "upos"},
    {"field": "xpos"},
    {"field": "start_char"},
    {"field": "end_char"},
    {"field": "mergeUp"},
    {"field": "mergeDown"},
    {"field": "split"},
    {"field": "card", "dndSource": True}
]

# {'name': 'DnD', 'id': 'dragDrop'},

app.layout = html.Div(
    [
        dag.AgGrid(
            columnDefs=columnDefs,
            rowData=get_text_data(),
            columnSize="sizeToFit",
            defaultColDef={"resizable": True, "sortable": True, "filter": True},
            dashGridOptions={"rowDragManaged": True, "animateRows": True},
            style={'width': '60%', "height": "90vh"},
            id='token_table',
            getRowId='params.data.id'
        ),
        dag.AgGrid(
            columnDefs=[{"field": "card", "dndSource": True}, {'field': 'id', 'hide': True}],
            rowData=get_audio_data(),
            columnSize="sizeToFit",
            defaultColDef={"resizable": True, "sortable": True, "filter": True},
            dashGridOptions={"rowDragManaged": True},
            style={'width': '40%', "height": "90vh"},
            id='cards_table',
            getRowId='params.data.id'
        ),
        # html.Button(id='drop1', style={'display': 'none'}, n_clicks=0),
        # html.Button(id='drop2', style={'display': 'none'}, n_clicks=0),
        dcc.Store(id='dropStore', storage_type='session')
    ],
    style={"display": "flex", "height": "90vh"},
)


## enable drag-n-drop
app.clientside_callback(
    """
    function (p) {
        console.log("JavaScript code executed!");
        var gridOrigin;
        var gridDragOver = (event) => {
            console.log("dragOver event triggered!");
            const dragSupported = event.dataTransfer.types.length;

            if (dragSupported) {
                event.dataTransfer.dropEffect = 'copy';
                event.preventDefault();
            }
        };

        var gridDragStart = (origin, event) => {
            console.log("dragStart event triggered!");
            gridOrigin = origin;

            // Get the dragged data (JSON string)
            const jsonData = event.dataTransfer.getData('application/json');
            console.log("Dragged data:", jsonData);

            // Parse the JSON data to obtain the card value
            const data = JSON.parse(jsonData);
            console.log("Card value being dragged:", data.card);
        }

        var gridDrop = (target, event) => {
            console.log("drop event triggered!");
            row = event.target.closest('.ag-row')
            event.preventDefault();

            const jsonData = event.dataTransfer.getData('application/json');
            const data = JSON.parse(jsonData);

            // If data is missing or the drop target is the same as the origin, do nothing
            if (!data || target == gridOrigin) {
                console.log("Data missing or same target as origin. Exiting.");
                return;
            }

            // Get the grid API for the target
            const gridApi = dash_ag_grid.getApi(target);

            // Get the overNode (row) based on drop coordinates
            const overNode = gridApi.getRowNode(row.getAttribute('row-id'));

            console.log("overNode:", overNode);

            // Log the existing data for the overNode
            console.log("Existing data for overNode:", overNode.data);

            // Update the specific row with the dragged card value
            const updatedData = { ...overNode.data, card: data.card };
            console.log("Updated data:", updatedData);

            gridApi.applyTransactionAsync({'update': [updatedData]});
        };


        setTimeout(() => {
            document.querySelector('#token_table').addEventListener('dragstart', (event) => gridDragStart('token_table', event))
            document.querySelector('#cards_table').addEventListener('dragstart', (event) => gridDragStart('cards_table', event))

            document.querySelector('#token_table').addEventListener('dragover', gridDragOver)
            document.querySelector('#cards_table').addEventListener('dragover', gridDragOver)
            document.querySelector('#cards_table').addEventListener('drop', (event) => gridDrop('cards_table', event))
            document.querySelector('#token_table').addEventListener('drop', (event) => gridDrop('token_table', event))
        }, 500)

        return window.dash_clientside.no_update
    }
    """,
    Output('cards_table', 'id'),
    Input('cards_table', 'id')
)

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

Hey! I’ve been making good progress and I have one more question. Is it possible to drag multiple cards into one particular cell or does that add extra complexity? Right now I’m just using one card and a long string that I can unpack. Wondering whether it would be feasible to have multiple draggable elements in each row or if it will cause problems. Thanks :slight_smile:

I think it would make it harder to determine. You would have to decide which column the card originated from.