AgGrid: getting the data type of a column or cell

I am having trouble extracting the data-type from an AgGrid column dynamically. in my case, the AgGrid starts with no data (i.e: rowData = []), meaning I can’t just iterate over rowData and extract the type of the value stored at any field, and I populate the data with a callback to rowTransaction from a button click (essentially, add rows when a user specifies). Additionally, my AgGrid is a subcomponent of a custom AIO (All-in-One) component because I want to re-use the grid in different places in my app and re-use the callbacks as I don’t want to re-write them for every similar grid I create; i.e: the layout of my grid doesn’t change (i.e: it’s placement in a div with a few other buttons and multi-selects remains the same) and the interactivity (callbacks) the grid has with other subcomponents (like buttons and multi-selects) remains the same, but the data/rows I am trying to display will change (hence, why I thought an AIO would be appropriate).

In javascript (see here ), one can define cell-datatypes (and I can see how this would be easy to map to a python data type (i.e: “text → str()”, and "number → int/float etc…), but there doesn’t seem to be an analog in the Dash AgGrid component properties. I found that in columnDefs there is a property called type, but that doesn’t appear to be useful information (or straight forward to derive the data type of a cell or column dynamically).

Additionally, I tried passing additional context in the AIO components class constructor:

def __init__(self, row_format, AgGrid_props=None, button_props=None, <remaining_subcomponents_props>=None)

where, row_format = {"field1":str, "field2":int} is the information I want to utilize, but I seem to be not able to pass class properties to callbacks, as the @callback decorator dictates the outputs and inputs of the callback, and an error is thrown if I pass self to the callback.

What is the best way to go about fixing this issue?

Hello @dave0,

Welcome to the community!

Dash AG Grid is currently using v29.3.5 of the underlying AG Grid, as such, there is no inferring of data types by the grid, this was introduced in v30. As such, there are some breaking changes in how applications work, the team is working hard to update the document examples to make sure that users are aware of some of the things they will encounter by upgrading to v31.

So, currently, there is no way to see what data type there is unless you define it.

Also, I’m not entirely sure you would be able to see this in a callback, because columnDefs goes one direction and gets adjusted by the grid after being passed it. You could however use the api to be able to pull this info from the grid, once v31 is released from dag.

Hi @jinnyzor , thank you for the reply. Is there a way in my AIO to pass additional properties that can be used in callbacks? for example, AIO components are structured such that I need to pass sucomponent props to the class constructor. Can I define custom props for the AIO component itself, or is this not possible since the component inherits from a Dash component? I tried adding additional props to a subcomponent prop and threw a TypeError as it was not defined property of the component’s class. For example, is there a way to access row_format in a callback (maybe by specifying State("ids.myComponent(MATCH)", "row_format")?

Another thing I tried was adding a dcc.Store to the AIO component, but the app did not render after doing this and instead I got the Error Loading message in the top left hand corner of the web page when it got rendered

Correct, AIO components can only have props from the underlying components.

If done correctly, you shouldnt have an error with it.

In my personal use, I have functions that return the configured grid that falls into pattern-matching realms, thus I only have a pattern-matching callback to deal with.

This is incorrect:
State("ids.myComponent(MATCH)", "row_format")State(ids.myComponent(MATCH), "row_format") would be proper, if the row_format existed as a prop.

To gain access to additional properties from the grid, you have access in the clientside by using the dash_ag_grid.getApi method and passing a stringified id to find the grid’s api. Then you can pull whatever from the api.

Thank you for the reply! It’s helpful to know. I cannot find getApi in the dash AgGrid reference. Can you please point me to it?

Thanks!

Sure, take a look at this example:

Thanks! Also, I am guessing global variables inside the callback are a no-no? for example within the callback declare global row_format and then outside of the AIO define row_format = {"field1":str, "field2":int} ?

Global variables are useful if you are the only user using the application and you will only have one session in use.

If you plan to not do the above, then you should avoid them and only declare things within the scope of the callback. Return any variables to be picked up by the app with functions.

Ok! That’s what I thought. Do you know why I get Error loading layout when I try to use a dcc.Store in my AIO component? I don’t think I have the technical background to understand why.

I am getting the following error:

File python3.11/json/encoder.py", line 180, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type type is not JSON serializable

With stack trace:

Traceback (most recent call last):
  File "/Users/<user>/opt/miniconda3/lib/python3.11/site-packages/flask/app.py", line 1455, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/<user>/opt/miniconda3/lib/python3.11/site-packages/flask/app.py", line 869, in full_dispatch_request
    rv = self.handle_user_exception(e)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/<user>/opt/miniconda3/lib/python3.11/site-packages/flask/app.py", line 867, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/<user>/opt/miniconda3/lib/python3.11/site-packages/flask/app.py", line 852, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/<user>/opt/miniconda3/lib/python3.11/site-packages/dash/dash.py", line 733, in serve_layout
    to_json(layout),
    ^^^^^^^^^^^^^^^
  File "/Users/<user>opt/miniconda3/lib/python3.11/site-packages/dash/_utils.py", line 26, in to_json
    return to_json_plotly(value)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/<user>/opt/miniconda3/lib/python3.11/site-packages/plotly/io/_json.py", line 143, in to_json_plotly
    json.dumps(plotly_object, cls=PlotlyJSONEncoder, **opts), _swap_json
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/<user>/opt/miniconda3/lib/python3.11/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
          ^^^^^^^^^^^
  File "/Users/<user>opt/miniconda3/lib/python3.11/site-packages/_plotly_utils/utils.py", line 59, in encode
    encoded_o = super(PlotlyJSONEncoder, self).encode(o)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/<user>/opt/miniconda3/lib/python3.11/json/encoder.py", line 200, in encode
    chunks = self.iterencode(o, _one_shot=True)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/<user>i/opt/miniconda3/lib/python3.11/json/encoder.py", line 258, in iterencode
    return _iterencode(o, 0)
           ^^^^^^^^^^^^^^^^^
  File "/Users/<user>/opt/miniconda3/lib/python3.11/site-packages/_plotly_utils/utils.py", line 136, in default
    return _json.JSONEncoder.default(self, obj)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/<user>/opt/miniconda3/lib/python3.11/json/encoder.py", line 180, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type type is not JSON serializable

Without seeing how you are spinning it up, I’m not sure. Something seems to be crashing it.

python src/app.py

Can I see your AIO definition?

Sure!

import uuid
from dash import Output, Input, State, html, dcc, MATCH, callback, no_update
import dash_ag_grid as dag
import dash_mantine_components as dmc

class ConstraintTableAIO(html.Div):

    # Set of functions to create pattern-matching callbacks of the subcomponents:
    class Ids:
        """
            IDS: Makes the subcomponent id elements able to be pattern matched with Dash's `MATCH`.
        """
        # Store to include additional information about the row format of the grid:
        store = lambda aio_id: {
            "component": "ConstraintTableAIO",
            "subcomponent":"store",
            "aio_id":aio_id
        }

        # Button to add constraints to the AgGrid
        addconstr = lambda aio_id: {
            "component": "ConstraintTableAIO",
            "subcomponent":"addconstr",
            "aio_id":aio_id
        }

        # Multi-select to define the constriant
        selector = lambda aio_id: {
            "component": "ConstraintTableAIO",
            "subcomponent": "selector",
            "aio_id": aio_id
        }

        # AgGrid object to visualize the user's choices
        grid = lambda aio_id: {
            "component":"ConstraintTableAIO",
            "subcomponent": "grid",
            "aio_id": aio_id
        }

        # A button to clear selections from the table
        remconstr = lambda aio_id: {
            "component":"ConstraintTableAIO",
            "subcomponent": "remconstr",
            "aio_id": aio_id
        }
    
    # Make the ids public:
    ids = Ids
    
    # Define the arguments of the All-in-One component:
    def __init__(
            self,
            store_props,
            addbtn_props=None,
            selector_props=None,
            grid_props=None,
            rembtn_props=None,
            aio_id=None
    ):

        # Define the aio_id:
        if aio_id is None:
            aio_id = str(uuid.uuid4())
            
        # Get all of the properties for each subcomponent:
        addbtn_props = addbtn_props.copy() if addbtn_props else {}
        selector_props = selector_props.copy() if selector_props else {}
        grid_props = grid_props.copy() if grid_props else {}
        rembtn_props = rembtn_props.copy() if rembtn_props else {}
        store_props = store_props.copy() # required!
        # Define the component's layout:
        super().__init__(
            [
                dcc.Store(id=self.ids.store(aio_id),**store_props),
                dmc.Group(
                    children=
                    [
                        dmc.Button(id=self.ids.addconstr(aio_id), **addbtn_props),
                        dmc.Space(w="sm"),
                        dmc.MultiSelect(id=self.ids.selector(aio_id), **selector_props)
                    ],
                    align="center"
                ),
                dag.AgGrid(id=self.ids.grid(aio_id), **grid_props),
                dmc.Space(mb=5),
                dmc.Button(id=self.ids.remconstr(aio_id), **rembtn_props)
            ]
        )

Here is the callback I am attempting to use to fill a row properly and allow a row_format to be specified at run-time:

@callback(
        Output(ids.grid(MATCH), "rowTransaction"),
        Output(ids.selector(MATCH), "value"),
        Input(ids.addconstr(MATCH), "n_clicks"),
        State(ids.selector(MATCH), "value"),
        State(ids.selector(MATCH), "rowData"),
        State(ids.store(MATCH), "data")
        # prevent_initial_call=True
    )
    def add_row_to_grid(n,value,data,store):
        """
            ADD_ROW_TO_GRID: Adds a new row to the grid. Conforms to the row_format.
        """
        if n:
            field = store["selector_field"]
            field_dtype = store["row_format"][field]
            default_row = store["row_format"].copy()
            if data is not None and data!=[]:
                
                # All the values already added to a row in the grid:
                exists = [d[field] for d in data]
                
                # This prevents us from adding the same selection twice.
                # A user will have to clear the table via a selection and clear action first.
                vals = [v for v in value if v not in exists]
            else:
                vals = value
                

            # make sure to cast v to the proper type
            rows = [default_row.update({field:field_dtype(v)}) for v in vals]
            txn = {"add":rows}
            value = []
            return txn, value
        return no_update,no_update

here, the values stored indmc.MultiSelect only change the value of a specified AgGrid field and the rest of the row fields have default values. In the dcc.Store, "data" contains a dictionary: {"row_format":{"field1":str,"field2":<default_float_value>}, "selector_field":"field1"}

additionally, I tried changing modified_timestamp in the store_props I pass in to something other than -1.

Follow up: I think it is a problem with to_json_plotly. Before creatiing my AIO component, I had already created existing UI elements that I was playing around with. I used the to_json_plotly()["props"] function to retrieve their properties before passing them into my AIO.

For example, I had a button called add_constr and wanted to copy it’s properties rather than re-write them:

addbtn_props = add_constr.to_json_plotly()["props"]
del addbtn["id"]

aio = ConstraintTableAIO(addbtn_props=addbtn_props, etc...)

Ah, yeah, your error message looked similar to that.

I think the plotly json string is a little more complex then just regular props, that might be why you are running into this issue.

I think it is at least one layer in:

aio = ConstraintTableAIO(addbtn_props=addbtn_props['props'], etc...)

ok! Actually, to clarifiy I am passing a dict of props in for the dcc.Store properties and it is complaining. The other properties use the to_json_plotly() method and the app renders if I remove the dcc.Store subcomponent from the AIO

@jinnyzor , I solved my issue by replacing the dcc.Store with a hidden html.Div and passing a string representation of the row_format to the children property of the Div. In the callback, I call eval() on the string stored in children to extract my static info as a dictionary

I would use something other than eval…

You should probably do json.loads since you are expecting a list. :wink:

Eval is very dangerous, could remove everything on your computer. :stuck_out_tongue:

@dave0 here is more info

ah! wow I see what you mean! The only annoyance is json cannot handle str or object when calling json.loads on the string representation. I am trying to specify a type