Custom Component: Material UI React Table

I am very interested in getting a nice table component. The st.write(dataframe) component is pretty good, but a true react table would be outstanding. Most of what I do is display tabular data. Could someone provide high level guidance how one would create a static table like this

and powered by a pandas dataframe behind the scenes?

Thank you.

1 Like

Hi @jlarkin,

Your best bet for now is trying to play with the SelectableDataTable example which extracts your DataFrame from Python on the React side, and edit the return code in React to return a Material Table (check the source code for simple table) instead of the custom selectable datatable. I think this is the shortest path between what you want and what exists currently.

If you have no experience in React, it may be a good exercise to first follow the component-template tutorial. I also did a quick tutorial for integrating Plotly.js if you want to test. They teach mostly how to pass data back and forth between Python and React though, it won’t show you a lot about mapping the dataframe into a custom JS table, which the selectabledatatable example shows you.

if you’re only doing static rendering and don’t plan to interact with the table, maybe it’s possible to do it only with components.html actually hmmm…lemme see…

Fanilo

3 Likes

If you’re only doing static rendering (that is no interaction with the table and get back selected rows back to Streamlit) you can use components.html to run any HTML/JS code, so something like this is a good first start, maybe I’d use something like jinja2 or some string interpol to inject Python data into the HTML file :

This is a straight very rough copy of the CDN example project edited to include the Customized Table you mention in your OP. Not optimized for production !

app.py
import streamlit.components.v1 as components
import streamlit as st 

st.title("Hello Custom React Table")

with open('app.html') as f:
    data = f.read()
    components.html(data, height=800)
app.html
<!DOCTYPE html>
<html lang="en">

<head>
    <title>My page</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
    <script src="https://unpkg.com/react@latest/umd/react.development.js" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/react-dom@latest/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.development.js"
        crossorigin="anonymous"></script>
    <script src="https://unpkg.com/babel-standalone@latest/babel.min.js" crossorigin="anonymous"></script>
    <!-- Fonts to support Material Design -->
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
    <!-- Icons to support Material Design -->
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
</head>

<body>
    <div id="root"></div>
    <script type="text/babel">
        const {
            Button,
            CssBaseline,
            makeStyles,
            Paper,
            Table,
            TableBody,
            TableCell,
            TableContainer,
            TableHead,
            TableRow,
            withStyles
        } = MaterialUI;


        const StyledTableCell = withStyles((theme) => ({
            head: {
                backgroundColor: theme.palette.common.black,
                color: theme.palette.common.white,
            },
            body: {
                fontSize: 14,
            },
        }))(TableCell);

        const StyledTableRow = withStyles((theme) => ({
            root: {
                '&:nth-of-type(odd)': {
                    backgroundColor: theme.palette.action.hover,
                },
            },
        }))(TableRow);

        function createData(name, calories, fat, carbs, protein) {
            return { name, calories, fat, carbs, protein };
        }

        const rows = [
            createData('Frozen yoghurt', 159, 6.0, 24, 4.0),
            createData('Ice cream sandwich', 237, 9.0, 37, 4.3),
            createData('Eclair', 262, 16.0, 24, 6.0),
            createData('Cupcake', 305, 3.7, 67, 4.3),
            createData('Gingerbread', 356, 16.0, 49, 3.9),
        ];

        const useStyles = makeStyles({
            table: {
                minWidth: 700,
            },
        });

        const CustomTable = () => {
            const classes = useStyles();

            return (
                <TableContainer component={Paper}>
                    <Table className={classes.table} aria-label="customized table">
                        <TableHead>
                            <TableRow>
                                <StyledTableCell>Dessert (100g serving)</StyledTableCell>
                                <StyledTableCell align="right">Calories</StyledTableCell>
                                <StyledTableCell align="right">Fat&nbsp;(g)</StyledTableCell>
                                <StyledTableCell align="right">Carbs&nbsp;(g)</StyledTableCell>
                                <StyledTableCell align="right">Protein&nbsp;(g)</StyledTableCell>
                            </TableRow>
                        </TableHead>
                        <TableBody>
                            {rows.map((row) => (
                                <StyledTableRow key={row.name}>
                                    <StyledTableCell component="th" scope="row">
                                        {row.name}
                                    </StyledTableCell>
                                    <StyledTableCell align="right">{row.calories}</StyledTableCell>
                                    <StyledTableCell align="right">{row.fat}</StyledTableCell>
                                    <StyledTableCell align="right">{row.carbs}</StyledTableCell>
                                    <StyledTableCell align="right">{row.protein}</StyledTableCell>
                                </StyledTableRow>
                            ))}
                        </TableBody>
                    </Table>
                </TableContainer>
            );
        }

        const App = () => {
            return (
                <CustomTable />
            );
        }

        ReactDOM.render(
            <div>
                <CssBaseline />
                <App />
            </div>,
            document.querySelector('#root'),
        );
    </script>
</body>

</html>

3 Likes

Thank you @andfanilo! You are a national treasure! I love the plotly example as well.

I don’t have any experience in React (or Javascript, or HTML, or CSS …) but hacked up some ipyvuetify code at some point last year with success, so I sort of understand what’s going on here.

A full walkthru of Streamlit custom component creation (and production deployment) would be a great PyData Global conference presentation. Deadline August 2nd! :point_right: https://global.pydata.org/pages/cfp.html :pray: One of the great things about Streamlit today is that you need to know virtually nothing about front-end web dev but can still create beautiful pages for end users. I imagine I am in good company with other community members who would love to take advantage of custom components but lack some basic knowledge to do so.

Can you explain (or link to explainer) what’s going on with this?

_selectable_data_table = components.declare_component(
    "selectable_data_table", url="http://localhost:3001",
)

I’ve read thru the component docs you linked to. So this is a dev server? Where does it get hosted in production? How does one push to production (absent pushing to PyPI)?

3 Likes

:+1:

:thinking: hmmm interesting, we’ll look into it

Pointing the docs for this, the declare_component defines where Streamlit should fetch the React component, here on http://localhost:3001.
Notice the dev server that you start from my_component/frontend is configured to run on http://localhost:3001 :wink: .

The declare_component returns a function _selectable_data_table. Whenever you use that function, Streamlit calls the other dev frontend server on port 3001 for the component then renders it on your browser ! Any parameter you send like _selectable_data_table(name="Randy") will be available in the React component accessed from there, here in props.args.name.


For production, we don’t want that 2nd dev server for serving a frontend component. Instead we build and minify all frontend files with npm run build into a single bundle, and then instead of pointing to an URL we point to a path with the bundle on declare_component(path=build/). To publish on PyPi we deploy this small JS with the Python code, all together in the wheel file :slight_smile: that part is documented here


PS : shameless plug, if you’ve played with ipyvuetify maybe you’re familiar with Vue.js. I tried to build a Vue.js template for Component here. It’s not well documented and maybe bugs will pop up but maybe it’ll work for you


That’s too nice of you :upside_down_face:

1 Like

Thanks for sharing andfanilo!

These “React” tables look great!

I’ve yet to familiarise myself with React - do you think React could allow for some kind of on-the-fly table interactions (e.g. sorting or filtering?)

Thanks,
Charly

Yes! That’s key. It’s all done in the browser so no need to communicate back to Python. See the link in my initial post for examples or sorting, filtering, selecting, multi-page operations, etc.

2 Likes

Thanks for sharing Jonathan!

My good Lord!!!

Having some of the interactive tables listed here would be fantastic!!! :fire:

e.g.: the collapsible one! -> https://material-ui.com/components/tables/#collapsible-table

1 Like

I’m Olivier, a principal at Material-UI. We appreciate the interest in our table component. While the current version of the component focuses on the style, we are working on a new DataGrid API under https://github.com/mui-org/material-ui-x to solve all the advanced challenges around managing a lot of tabular data. This comes up in two flavors, an MIT license for the simplest features and an commercial one for the most advanced.

The demo with DevExpress (commercial license) gives an idea of what we are building.

  • What set of features do you need from the data table component?
3 Likes