Links in datatable - Multipage App

Hi There,

I’m new here and I’m building a dashboard to visualise the signal of an large amount of sensors.
In the main page I have a table and I would like to pass a < href > to each value in the selected column. How can I do it? Any ideias? I looked around and could not find an answer.

Another Small question, do you know how to change the “Dash” in the tab for the real name of the tab?

Thank you…!

3 Likes

you can actually i made a function that does this from a dataframe
what you do is you insert a html.a inside a html.td and thats pretty much it.
Below i pasted a code that returns a hyperlinked table from a dataframe

1 Like
def table_link(dataframe,link_column,link_format,**kwargs):
"""note that this function should only be used if the table needs hyperlinking otherwise
use the table_div function already existing.

Args:
    dataframe (pandas.DataFrame): The representative dateframe we want to make a table that somehow needs hyperlinking
    in a cell.
    link_column (str): name of the column that needs hyperlinks.
    link_format (lambda): lambda function that parses **kwargs and returns a str
                        note that we need to sent make sure all values that we need are sent in kwargs
                        if you need to use the cell content use the paramater value. (e.g. lambda value,x:f'{x}/{value}')


Returns:
    HTML table this the dataframe represented and the linkin in the column needed
"""
rows = []
for i in range(len(dataframe)):
    row = []
    for col in dataframe.columns:
        value = dataframe.iloc[i][col]
        kwargs['value'] = value
        if col == link_column:
            cell = html.Td(html.A(value, target='_blank',href=link_format(**kwargs)))
        else:
            cell = html.Td(children=value)
        row.append(cell)
    rows.append(html.Tr(row))
return html.Table(
                # Header
                [html.Tr([html.Th(col.title()) for col in dataframe.columns])] +
                rows,className='table',
                )
2 Likes

Thank you so much @lahashh Luis,

I’ll try it out!!
I’m using dt.DataTable so that my table is filterable :slight_smile: Let’s see If I manage to do it.
I’ll let you know.

I was not able to reproduce this behaviour using dash table that’s why i had to look for an iterative solution if you managed to do it please share it please :pleading_face:

2 Likes

Nice site!

As far as I can tell, supporting non-text / dropdown data types is still on the list for dash-table, but unfortunately it’s not currently on the roadmap. Seems like once it’s on the roadmap the plotly devs still need to figure out expected behavior for other table actions (see issue #166) – e.g. how to filter, sort, etc.

In the meantime, I came up with a little bit of a hack that adds links into the a column after the table has been loaded. I am doing this using a clientside callback. In the following code snippets, I assume that all links share a common baseHref and the differentiation for the URI is contained as the text content of column 0. From your screenshot, the first row link would point to a relative url: "/route/to/SISM-CHR-001"

Step-by-step:

  1. Create a javascript file in the app’s assets folder (e.g. assets/app-ui.js):
if (!window.dash_clientside) {
    window.dash_clientside = {};
}

const baseHref = "/route/to/";

// create the "ui" namespace within dash_clientside
window.dash_clientside.ui = { 
    // this function should be used in the callback whenever the table viewport has changed
    replaceWithLinks: function(trigger) {
        // find all dash-table cells that are in column 0
        let cells = document.getElementsByClassName("dash-cell column-0");

        cells.forEach((elem, index, array) => {
            // each cell element should be modified with a new link
            // elem.children[0].innerText contains the link information, which we append to a specified route on our server
            elem.children[0].innerHTML =
                '<a href="' +
                baseHref +
                elem.children[0].innerText +
                '">' +
                elem.children[0].innerText +
                "</a>";
        });

        // arbitrary return.. callback needs a target
        return true;
    }
}

Just change baseHref to your desired routing.

  1. In your Dash layout, add a hidden div that will be the Output target of the callback.
layout = ...
    ...,
    html.Div(id='table-hidden-target', style={'display': 'none'}),
    ...
  1. Add a dash clientside callback to execute our function “replaceWithLinks” every time the table viewport changes.
...
app.clientside_callback(
    ClientsideFunction('ui', 'replaceWithLinks'),
    Output('table-hidden-target', 'children'),
    [Input('[ENTER-YOUR-TABLE-ID]', 'derived_viewport_data')]
)
...

That should be it!

Looks something like this (in the image, I actually generated links using text from column-0 to modify column-1, but the idea should be the same):
before: image
after: image

This hack works as expected when you only have the one table on a page. If that changes, you’ll need to modify the javascript to find the correct table and modify the children cells appropriately. Hope this helps; the clientside callbacks really gave a lot of capability to end users (for example, how to get around file size limits in downloading data in browser – I posted an example for that in the csv download thread as well)

4 Likes

Hi there :smiley:

Thank you both!
@brad monday I’ll try it out. Thank you so much for your help.

Hi @brad,

I couldn’t implement it, because I had an old version of dash.
I’ve actualised but it and I had to change dash_table_experiment for dash_table module.
My code returns this error:

it Invalid argument columns[0] passed into DataTable with ID “datatable”.
Expected object.
Was supplied type string.

Any idea what can I do to fix it?:woozy_face:

I was looking, I’ve commented all the changes you suggested and the error keeps appearing so the error is in my table… when converting from dash_table_experiments to dash_table it stopped working.

Hi @cecilia

Yes, there were considerable changes between dash-table-experiments and dash-table. I remember there was a workaround that chriddyp published in the dash-table-experiments repo which could work if you prefer to keep using dash-table-experiments. See these github threads: https://github.com/plotly/dash-table-experiments/issues/6, https://github.com/plotly/dash-table-experiments/pull/11#issuecomment-411587918

Unfortunately, it’s been quite awhile since I’ve used the older repo, so I’m not the best person to give you advice there. Good luck!

edit: added a link

1 Like

Nice trick @brad, that worked a treat!

The main catch is that for tables with more than a handful of items, updating every element in the DOM is slow, and it appears that the app is blocked from continuing to run while the DOM is being updated.

Hopefully we see this functionality coming to the DataTable component soon!

I guess the more general concern (which could be related to the slowness), is that this approach involves directly accessing the DOM, which React is already managing for us. In my excitement I had overlooked this general issue. While the hack might appear to work, there is the potential for things to break, since React is blind to external modifications to the DOM.

Agree with the sentiment. It would be much faster (and safer) if one were to fork the dash-table component and implement link-from-html-inner-text feature directly.

In this specific case, a couple reasons why implementing the hack may be appropriate (at least until component-as-cell is supported natively):

  1. The DOM changes, by design, will only render AFTER virtualDOM component-element rendering. The javascript is triggered by a callback based on updates in the derived_viewport_data property of the table component.
  2. Adding links does not change the functionality/relevant properties of a cell within the dash-table component. Put another way, the “data” of the cell (element.innerText) remains unchanged and can be filtered/sorted the same way as the unmodified dash-table cell. Adding a hyperlink is more of a “styling” change.
1 Like

Yep that makes sense, and I had suspected that those characteristics you mentioned might be the case. I guess I mainly wanted to qualify my enthusiasm with an acknowledgement that when doing this kind kind of accessing DOM directly while React is managing it, there’s always the possibility of weird and breaky happening.

1 Like

@nedned I’m using a dash html table and i’m creating links like this.
def Table(dataframe):
rows = []
for i in range(len(dataframe)):
row = []
for col in dataframe.columns:
value = dataframe.iloc[i][col]
# update this depending on which
# columns you want to show links for
# and what you want those links to be
if col == ‘id’:
cell = html.Td(html.A(href=value, children=value, className=value))
# print(html.Td(html.A(href=value)))
print(html.Td(html.A(className=value)))

        else:
            cell = html.Td(children=value,)
        row.append(cell)
    rows.append(html.Tr(row))
return html.Table(
    # Header
    [html.Tr([html.Th(col) for col in dataframe.columns])] +

    rows
)

I want to display the href value in a text field.
My call back is as follows,
@app.callback(
Output(component_id=‘my-div’,component_property=‘children’),
[Input(component_id=‘mytable’,component_property=‘n_clicks’)]
)
def update_textBox(n_clicks):
return ‘You’ve entered “{}”’.format(n_clicks)

But it triggers only. Is there any way to get the link value. cause i need to query some data using the link value.

Thanks.

@brad thanks for your sharing.
it didn’t work for me, can you help with these naive questions:

  1. how to specify baseHref? is it point to external url which i want to redirect to or the dash page url itself?
  2. how to specify getElementsByClassName? should i change the value?
  3. how to ‘import’ this js asset? and how to make sure it execute actually?
    i’m a newbie to the js world, forgive me for these naive questions.

In the first step, change line 4 of the javascript file that you’ve created in the assets folder. It should be the path that you would like your links to point to (external site, most likely). The script is set up so that the links will all be of the form $baseHref$/$text-of-specified-cell$

baseHref can be an absolute or relative URL. For example, you could write “https://my.first.domain.com/route2/”, “https://my.other.domain.com/route2/”, or “/route2/”

For the example provided, I am grabbing the text from column-0 and creating a link for that column at the same time (elem.children[0].innerHTML). Change column-[NUMBER] to use a different column cell. If the first cell had the text “Location1” and baseHref remains the same (/route/to/), the cell would now have a link to “/route/to/Location1” The link text doesn’t change, it’ll still read “Location1”

Before the javascript is executed, your table will look like:

<table>
  <tbody>
  <tr> [ header row .. ] </tr>
  <tr> [ filters row .. ] </tr>
  <tr>
    <!-- this is the first column 'getElementsByClassName("dash-cell column-0")' of the first row -->
    <td tabindex="-1" class="dash-cell column-0" data-dash-column=".." data-dash-row="0" ..>
      <div class="unfocused dash-cell-value">
        <!-- this is child 0 of the cell; "elem.children[0]" -->
        TEXT-FOR-COLUMN-0-ROW-0
      </div>
    </td>
    <!-- this is the second column 'getElementsByClassName("dash-cell column-1")' of the first row -->
    <td tabindex="-1" class="dash-cell column-1" ...>
      <div class="unfocused dash-cell-value">
        ..
    </td>
    .. additional columns in the row
  </tr>
  <tr>
    ...additional rows in the table
  </tr>
  </tbody>
</table>

The javascript will replace TEXT-FOR-COLUMN-0-ROW-0 with…

        <a href="/route/to/TEXT-FOR-COLUMN-0-ROW-0">
          TEXT-FOR-COLUMN-0-ROW-0
        </a>

… and will do that for every row.

No need to specify. That’s a javascript built-in that will search the DOM for a specific element (e.g. a div with the class name specified) to modify. This is the part that @nedned mentioned is un-reactlike – we should ideally do these types of operations before any elements are rendered; things are much slower using this hack (depends on how many rows need to be changed).

What happens here is that in your dash app, the “replaceWithLinks()” function is executed by a clientside callback (step 3). The callback is triggered any time the table’s “derived_viewport_data” is changed (at load, when filtered, when paged). So, once the viewport changes, the script executes to add links to every visible row in column 0.

From Dash Documentation & User Guide | Plotly

If you can’t/don’t-want-to create an assets folder, there are alternatives for importing JS and CSS files from external URLs listed in the link (i.e. if the js file is saved on a CDN):

app = dash.Dash(__name__,
                external_scripts=external_scripts,
                external_stylesheets=external_stylesheets)```

edit:fix

@brad thanks, i can implement the linkable click now, but i have several tables in the page, and this add links to column-0 of each table.
how can i only add link to a specific table?
is there a way to specify tableid when call “let cells = document.getElementsByClassName(‘dash-cell column-0’);”?
and how to know the dash-generated page html?

The way to select a specific table would be using getElementById(). For example:

let cells = document.getElementById('YOUR-TABLE-ID').getElementsByClassName("dash-cell column-0");

One way to avoid hard-coding it could be to pass the table id into the function… something like

...
    replaceWithLinks: function(trigger, table_id) {
        // find all dash-table cells that are in column 0
        let cells = document.getElementById(table_id).getElementsByClassName("dash-cell column-0");

        cells.forEach((elem, index, array) => {
          ..

Then in your clientside callback, you would add in a state parameter:

app.clientside_callback(
    ClientsideFunction('ui', 'replaceWithLinks'),
    Output('table-hidden-target', 'children'),
    [Input('[ENTER-YOUR-TABLE-ID]', 'derived_viewport_data')],
    [State('[ENTER-YOUR-TABLE-ID]', 'id')]
)

To your last question, on dash-generated page html, I usually just inspect the page in the browser (Ctrl-Shift-I to bring up DevTools and click the Elements tab on Chrome… more easily you can right click on the table cell and select Inspect. I think this is the same behavior for Firefox and Microsoft products as well).

Hope that helps–

I tried the way/hack, unfortunately I am not able to replicate the change. Here is the my workaround:-

app.py

import dash
import dash_table
import pandas as pd
import dash_html_components as html 
from dash.dependencies import Input, Output, State, ClientsideFunction
import dash_bootstrap_components as dbc
from dash.exceptions import PreventUpdate
import dash_core_components as dcc
df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/solar.csv')
app = dash.Dash(__name__)
app.config['suppress_callback_exceptions'] = True
app.layout = html.Div([
    html.Div(id='table-hidden-target', style={'display': 'none'}),
    dbc.Button(id='submit-button', n_clicks_timestamp=0, n_clicks=0,children='submit'),
    html.Div(id='tabldata'),
    dash_table.DataTable(
        id='table-1')
])
@app.callback(
    [Output("table-1", "data"), Output("table-1", "columns")],
    [Input("submit-button", "n_clicks")]
)
def toggle_collapse(n):
    if n:
        return df.to_dict('records'),[{"name": i, "id": i} for i in df.columns]
    else:
        raise PreventUpdate

app.clientside_callback(
    ClientsideFunction('ui', 'replaceWithLinks'),
    Output('table-hidden-target', 'children'),
    [Input('[table-1]', 'derived_viewport_data')]
)
if __name__ == '__main__':
    app.run_server(debug=True)

I have created the app-ui.js in assets folder.

Could you please suggests me what I missed?

@yoges
Sorry for the delay. I haven’t been checking these forums for awhile as I’m not doing a ton of python dev right now.

I should probably change the instructions a bit. the only thing that jumps out to me is the Input portion of the callback…
[Input('[table-1]', 'derived_viewport_data')]
should read
[Input('table-1', 'derived_viewport_data')]

No [ or ] brackets… Try it out and let me know if it works. Cheers-