"""This application experiments with the (grid) layout and some styling
Can we make a compact dashboard across several columns and with a dark theme?"""
import io
from typing import List, Optional
import markdown
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd
import plotly.graph_objects as go
import streamlit as st
from plotly import express as px
from plotly.subplots import make_subplots
# matplotlib.use("TkAgg")
matplotlib.use("Agg")
COLOR = "black"
BACKGROUND_COLOR = "#fff"
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", color=COLOR, background_color=BACKGROUND_COLOR) 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!"
)
grid.cell("f", 1, 3, 3, 4).text(
"The cell to the right is a matplotlib svg image"
)
grid.cell("g", 3, 4, 3, 4).pyplot(get_matplotlib_plt())
st.plotly_chart(get_plotly_subplots())
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"""
This should have been a plotly plot.
But since *script* tags are removed when inserting MarkDown/ HTML i cannot get it to workto work.
But I could potentially save to svg and insert that.
"""
def pyplot(self, fig=None, **kwargs):
string_io = io.StringIO()
plt.savefig(string_io, format="svg", fig=(2, 2))
svg = string_io.getvalue()[215:]
plt.close(fig)
self.inner_html = '' + svg + "
"
def _to_html(self):
return f"""{self.inner_html}
"""
class Grid:
"""A (CSS) Grid"""
def __init__(
self,
template_columns="1 1 1",
gap="10px",
background_color=COLOR,
color=BACKGROUND_COLOR,
):
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"""
"""
def _get_cells_style(self):
return (
""
)
def _get_cells_html(self):
return (
''
+ "\n".join([cell._to_html() for cell in self.cells])
+ "
"
)
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
dark_theme = st.sidebar.checkbox("Dark Theme?", False)
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
)
if dark_theme:
global COLOR
global BACKGROUND_COLOR
BACKGROUND_COLOR = "rgb(17,17,17)"
COLOR = "#fff"
_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"""
""",
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")
def get_matplotlib_plt():
get_dataframe().plot(kind="line", x="quantity", y="price", figsize=(5, 3))
def get_plotly_subplots():
fig = make_subplots(
rows=2,
cols=2,
subplot_titles=("Plot 1", "Plot 2", "Plot 3", "Table 4"),
specs=[
[{"type": "scatter"}, {"type": "scatter"}],
[{"type": "scatter"}, {"type": "table"}],
],
)
fig.add_trace(go.Scatter(x=[1, 2, 3], y=[4, 5, 6]), row=1, col=1)
fig.add_trace(go.Scatter(x=[20, 30, 40], y=[50, 60, 70]), row=1, col=2)
fig.add_trace(go.Scatter(x=[300, 400, 500], y=[600, 700, 800]), row=2, col=1)
fig.add_table(
header=dict(values=["A Scores", "B Scores"]),
cells=dict(values=[[100, 90, 80, 90], [95, 85, 75, 95]]),
row=2,
col=2,
)
if COLOR == "black":
template="plotly"
else:
template ="plotly_dark"
fig.update_layout(
height=500,
width=700,
title_text="Plotly Multiple Subplots with Titles",
template=template,
)
return fig
main()