Streamlit: Grid layout

Created on 8 Oct 2019  路  20Comments  路  Source: streamlit/streamlit

Users sometimes want to lay items in a grid. For example:

image 1 | label 1 | score 1
image 2 | label 2 | score 2
image 3 | label 3 | score 3

The difference between a grid an a horizontal layout (Issue #241) is in a grid both the rows and the columns must line up.

And here's a straw-man API for this feature:

grid = st.grid()
for image_info in image_info_list:
  row = grid.row()
  # Now you can use row.foo() to do the same as st.foo(), but 
  # the elements you insert will all go inside a horizontal
  # container.
  row.image(image_info.image)
  row.text(image_info.label)
  row.text(image_info.score)

...but this is just one of multiple possible APIs we could come up with here.

Everyone: what are other possible APIs?

enhancement layout spec_needed

Most helpful comment

This looks pretty good, any updates on the plan for Streamlit to add in this feature?

All 20 comments

A grid row could contain another grid, I am thinking something something that could build the following:

+-----------------+----------|----------------+
|                 | label 1  |   Score 1      |
|   Image 1       +----------|----------------+
|                 | label 2  |  Score 2       |
|                 +----------|----------------+
|                 |  label 3 |  score3        |
+-----------------+----------|----------------+
|                 | label1   |  score 1       |
|   Image 2       +----------|----------------+
|                 | label2   |  score2        |
|                 +----------|----------------+
|                 | label3   |  score3        |
+-----------------+----------|----------------+

The straw-man API would look something on the lines:

grid = st.grid()
for image, scores in images:
    row = grid.row()

    row.write(image) // aspect ratio or width as parameter
    subgrid = row.grid()
    for label, score in scores:
          row2 = subgrid.row()
          row2.write(label)
          row2.write(score)

On #241 , I suggested having a column()/col() method would help with defining aspect ratios of columns, similar to how https://github.com/pyviz/panel does. With col, the above code would look like:

grid = st.grid()
for image, scores in images:
    row = grid.row()

    row.col(width_ratio=1).write(image) // aspect ratio or width as parameter
    right_col = row.col(width_ratio=2) // meaning write col should be twice as large as left col
    for label, score in scores:
          small_row = right_col.row()
          small_row.col().write(label)
          small_row.col().write(score)

The last example doesn't break the general idea that write creates a new horizontal element, and makes the design explicit. This pattern is common across many tools (Android uses something very similar as well).

Very interesting solution, great i 'll be tracking this issue :+1:

See also this comment on app layout and grids

https://github.com/streamlit/streamlit/issues/486#issuecomment-548666166

And here is my brainstorm on how a gridlayout could work. It's included in the gallery at awesome-streamlit.org.

image

"""This application experiments with the (grid) layout and some styling

Can we make a compact dashboard across several columns and with a dark theme?"""
from typing import List, Optional

import markdown
import pandas as pd
import streamlit as st
from plotly import express as px


def main():
    """Main function. Run this to run the app"""
    st.sidebar.title("Layout and Style Experiments")
    st.sidebar.header("Settings")
    st.markdown(
        """
# Layout and Style Experiments

The basic question is: Can we create a multi-column dashboard with plots, numbers and text using
the [CSS Grid](https://gridbyexample.com/examples)?

Can we do it with a nice api?
Can have a dark theme?
"""
    )

    select_block_container_style()
    add_resources_section()

    # My preliminary idea of an API for generating a grid
    with Grid("1 1 1") as grid:
        grid.cell(
            class_="a",
            grid_column_start=2,
            grid_column_end=3,
            grid_row_start=1,
            grid_row_end=2,
        ).markdown("# This is A Markdown Cell")
        grid.cell("b", 2, 3, 2, 3).text("The cell to the left is a dataframe")
        grid.cell("c", 3, 4, 2, 3).plotly_chart(get_plotly_fig())
        grid.cell("d", 1, 2, 1, 3).dataframe(get_dataframe())
        grid.cell("e", 3, 4, 1, 2).markdown("Try changing the **block container style** in the sidebar!")

def add_resources_section():
    """Adds a resources section to the sidebar"""
    st.sidebar.header("Add_resources_section")
    st.sidebar.markdown(
        """
- [gridbyexample.com] (https://gridbyexample.com/examples/)
"""
    )


class Cell:
    """A Cell can hold text, markdown, plots etc."""
    def __init__(
        self,
        class_: str = None,
        grid_column_start: Optional[int] = None,
        grid_column_end: Optional[int] = None,
        grid_row_start: Optional[int] = None,
        grid_row_end: Optional[int] = None,
    ):
        self.class_ = class_
        self.grid_column_start = grid_column_start
        self.grid_column_end = grid_column_end
        self.grid_row_start = grid_row_start
        self.grid_row_end = grid_row_end
        self.inner_html = ""

    def _to_style(self) -> str:
        return f"""
.{self.class_} {{
    grid-column-start: {self.grid_column_start};
    grid-column-end: {self.grid_column_end};
    grid-row-start: {self.grid_row_start};
    grid-row-end: {self.grid_row_end};
}}
"""

    def text(self, text: str = ""):
        self.inner_html = text

    def markdown(self, text):
        self.inner_html = markdown.markdown(text)

    def dataframe(self, dataframe: pd.DataFrame):
        self.inner_html = dataframe.to_html()

    def plotly_chart(self, fig):
        self.inner_html = f"""
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<body>
    <p>This should have been a plotly plot.
    But since *script* tags are removed when inserting MarkDown/ HTML i cannot get it to work</p>
    <div id='divPlotly'></div>
    <script>
        var plotly_data = {fig.to_json()}
        Plotly.react('divPlotly', plotly_data.data, plotly_data.layout);
    </script>
</body>
"""

    def to_html(self):
        return f"""<div class="box {self.class_}">{self.inner_html}</div>"""


class Grid:
    """A (CSS) Grid"""
    def __init__(
        self, template_columns="1 1 1", gap="10px", background_color="#fff", color="#444"
    ):
        self.template_columns = template_columns
        self.gap = gap
        self.background_color = background_color
        self.color = color
        self.cells: List[Cell] = []

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        st.markdown(self._get_grid_style(), unsafe_allow_html=True)
        st.markdown(self._get_cells_style(), unsafe_allow_html=True)
        st.markdown(self._get_cells_html(), unsafe_allow_html=True)

    def _get_grid_style(self):
        return f"""
<style>
    .wrapper {{
    display: grid;
    grid-template-columns: {self.template_columns};
    grid-gap: {self.gap};
    background-color: {self.background_color};
    color: {self.color};
    }}
    .box {{
    background-color: {self.color};
    color: {self.background_color};
    border-radius: 5px;
    padding: 20px;
    font-size: 150%;
    }}
    table {{
        color: {self.color}
    }}
</style>
"""

    def _get_cells_style(self):
        return (
            "<style>" + "\n".join([cell._to_style() for cell in self.cells]) + "</style>"
        )

    def _get_cells_html(self):
        return (
            '<div class="wrapper">'
            + "\n".join([cell.to_html() for cell in self.cells])
            + "</div>"
        )

    def cell(
        self,
        class_: str = None,
        grid_column_start: Optional[int] = None,
        grid_column_end: Optional[int] = None,
        grid_row_start: Optional[int] = None,
        grid_row_end: Optional[int] = None,
    ):
        cell = Cell(
            class_=class_,
            grid_column_start=grid_column_start,
            grid_column_end=grid_column_end,
            grid_row_start=grid_row_start,
            grid_row_end=grid_row_end,
        )
        self.cells.append(cell)
        return cell

def select_block_container_style():
    """Add selection section for setting setting the max-width and padding
    of the main block container"""
    st.sidebar.header("Block Container Style")
    max_width_100_percent = st.sidebar.checkbox("Max-width: 100%?", False)
    if not max_width_100_percent:
        max_width = st.sidebar.slider("Select max-width in px", 100, 2000, 1200, 100)
    else:
        max_width = 1200
    padding_top = st.sidebar.number_input("Select padding top in rem", 0, 200, 5, 1)
    padding_right = st.sidebar.number_input("Select padding right in rem", 0, 200, 1, 1)
    padding_left = st.sidebar.number_input("Select padding left in rem", 0, 200, 1, 1)
    padding_bottom = st.sidebar.number_input(
        "Select padding bottom in rem", 0, 200, 10, 1
    )
    _set_block_container_style(
        max_width,
        max_width_100_percent,
        padding_top,
        padding_right,
        padding_left,
        padding_bottom,
    )


def _set_block_container_style(
    max_width: int = 1200,
    max_width_100_percent: bool = False,
    padding_top: int = 5,
    padding_right: int = 1,
    padding_left: int = 1,
    padding_bottom: int = 10,
):
    if max_width_100_percent:
        max_width_str = f"max-width: 100%;"
    else:
        max_width_str = f"max-width: {max_width}px;"
    st.markdown(
        f"""
<style>
    .reportview-container .main .block-container{{
        {max_width_str}
        padding-top: {padding_top}rem;
        padding-right: {padding_right}rem;
        padding-left: {padding_left}rem;
        padding-bottom: {padding_bottom}rem;
    }}
</style>
""",
        unsafe_allow_html=True,
    )

@st.cache
def get_dataframe() -> pd.DataFrame():
    """Dummy DataFrame"""
    data = [
        {"quantity": 1, "price": 2},
        {"quantity": 3, "price": 5},
        {"quantity": 4, "price": 8},
    ]
    return pd.DataFrame(data)

def get_plotly_fig():
    """Dummy Plotly Plot"""
    return px.line(
        data_frame=get_dataframe(),
        x="quantity",
        y="price"
    )

main()

I might later improve it. The improved file can be found here

https://github.com/MarcSkovMadsen/awesome-streamlit/blob/master/gallery/layout_experiments/app.py

@tvst . See above post.

And here is an image using the full screen width.

image

This looks pretty good, any updates on the plan for Streamlit to add in this feature?

+1 Any updates?

mark

@MarcSkovMadsen, Great code, any thoughts on adding an image to a cell with the code you've written. Tried <img src="path"> in the markdown grid cell but that did not work.

The following function did the trick for my needs:

def image(self, image_path, width = "150px",height = "200px",caption = ""):
        data_uri = base64.b64encode(open(image_path, 'rb').read()).decode('utf-8')
        img_tag = '<br><figure ><center><img src="data:image/png;base64,{0}" width={2} height={3} ><figcaption>{1}</figcaption></center></figure>'.format((data_uri,caption,width,height))
        self.inner_html =  """<body>{0}</body>""".format(img_tag)  

This feature would be very useful on streamlit, but the code kindly shared by @MarcSkovMadsen is doing the trick so far.

Can't wait to hear more on this. I love streamlit and it would be even better with the power to play with the UI more.

+1 for the easiest way to displaython the web

I just started using Streamlit (and I really love it) and it would be great to have a simple Layout API for it!
So far I needed the only layout for the plotly plots, so for this particular case my solution was using plotly layout API, one example is here: https://stackoverflow.com/a/55501565/1376910

And here is my brainstorm on how a gridlayout could work. It's included in the gallery at awesome-streamlit.org.

image

Fantastic suggestions Marc, as always! :)

Great work! I hope to see this advance further.
The layout limitation and lack of some sort of session state is holding streamlit up from being adopted as a serious dashboarding contender.

I tried to build a calculator with buttons and a output field, but it also requires a grid layout. Might be a good test-case as well for this feature request.

Great code! I only miss the option to include widgets to a cell. I didn't find a way to get the widget Html code. Any thoughts on a way to add a widget to a cell?

@WilberDelbrison I think a grid API like the ones discussed at the top would have to be implemented directly in streamlit to be able to include streamlit widgets in grid cells. I think the
working grid code above only works within an st.markdown() call

@MarcSkovMadsen, Great code, any thoughts on adding an image to a cell with the code you've written. Tried <img src="path"> in the markdown grid cell but that did not work.

The following function did the trick for my needs:

def image(self, image_path, width = "150px",height = "200px",caption = ""):
        data_uri = base64.b64encode(open(image_path, 'rb').read()).decode('utf-8')
        img_tag = '<br><figure ><center><img src="data:image/png;base64,{0}" width={2} height={3} ><figcaption>{1}</figcaption></center></figure>'.format((data_uri,caption,width,height))
        self.inner_html =  """<body>{0}</body>""".format(img_tag)  

This feature would be very useful on streamlit, but the code kindly shared by @MarcSkovMadsen is doing the trick so far.

@rodrigoxrma what to pass in self (of this image() function)?

@MarcSkovMadsen, Great code, any thoughts on adding an image to a cell with the code you've written. Tried <img src="path"> in the markdown grid cell but that did not work.

The following function did the trick for my needs:

def image(self, image_path, width = "150px",height = "200px",caption = ""):
        data_uri = base64.b64encode(open(image_path, 'rb').read()).decode('utf-8')
        img_tag = '<br><figure ><center><img src="data:image/png;base64,{0}" width={2} height={3} ><figcaption>{1}</figcaption></center></figure>'.format((data_uri,caption,width,height))
        self.inner_html =  """<body>{0}</body>""".format(img_tag)  

This feature would be very useful on streamlit, but the code kindly shared by @MarcSkovMadsen is doing the trick so far.

@rodrigoxrma what to pass in self (of this image() function)?

The method I suggested goes inside the Cell class, so self refers to its instance in the code. It enables this method to update the value of the attribute inner_html of the class.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Code4SAFrankie picture Code4SAFrankie  路  31Comments

DylanModesitt picture DylanModesitt  路  16Comments

blester125 picture blester125  路  34Comments

tvst picture tvst  路  24Comments

tvst picture tvst  路  21Comments