Advanced Layouts#

Responsive Layouts with FlexBox#

So far when we have talked about responsive layouts we have primarily focused on simple width/height responsiveness of individual components, i.e. whether they will grow and shrink to fill the available space. For a truly responsive experience however we will need responsive layouts that will reflow the content depending on the size of the screen, browser window or the container they are placed inside of, much like how text wraps when there is insufficient width to accommodate it:

Panel offers one such component out of the box, the FlexBox layout.

import panel as pn
pn.extension('tabulator')
import panel as pn
import random

pn.extension()

def create_random_spacer():
    return pn.Spacer(
        height=100,
        width=random.randint(1, 4) * 100,
        styles={"background": "teal"},
        margin=5,
    )
spacers = [create_random_spacer() for _ in range(10)]

pn.FlexBox(*spacers).servable()

FlexBox is based on CSS Flexbox and supports many of the same options, such as setting flex_direction, flex-wrap, align_items and align_content.

import panel as pn
import random

pn.extension()

def create_random_spacer():
    return pn.Spacer(
        height=random.randint(1, 2) * 100,
        width=random.randint(1, 4) * 100,
        styles={"background": "teal"},
        margin=5,
    )
spacers = [create_random_spacer() for _ in range(10)]

pn.FlexBox(*spacers, align_items="center").servable()

Distributing proportions#

To achieve more complex layouts, i.e. specific proportions between different components we can use the flex property on the children of our FlexBox, e.g. here we declare that the green Spacer should be three times as wide as the red and blue components.

import panel as pn

pn.extension()

red = pn.Spacer(height=200, styles={'background': 'red', 'flex': '1 1 auto'})
green = pn.Spacer(height=200, styles={'background': 'green', 'flex': '3 1 auto'})
blue = pn.Spacer(height=200, styles={'background': 'blue', 'flex': '1 1 auto'})

pn.FlexBox(red, green, blue).servable()

To learn more about this read this guide on controlling ratios of flex items.

Exercise#

Using only the styles of the components, distribute the two dataframes and the markdown such that the dataframes are both 3 times wider than the markdown, then center the markdown vertically.

You might find inspiration in this guide on aligning items in a flex container.

import pandas as pd
import panel as pn

pn.extension()

text = """A *wind turbine* is a renewable energy device that converts the kinetic energy from wind into electricity."""
wind_speed = pd.DataFrame(
    [
        ("Monday", 7),
        ("Tuesday", 4),
        ("Wednesday", 9),
        ("Thursday", 4),
        ("Friday", 4),
        ("Saturday", 5),
        ("Sunday", 4),
    ],
    columns=["Day", "Wind Speed (m/s)"],
)

dataframe_1 = pn.pane.DataFrame(wind_speed, styles={'flex': '3 1 auto'})
markdown = pn.pane.Markdown(text, styles={'flex': '3 1 auto'})
dataframe_2 = pn.pane.DataFrame(wind_speed, styles={'flex': '3 1 auto'})

pn.FlexBox(dataframe_1, markdown, dataframe_2).servable()
Solution
import pandas as pd
import panel as pn

pn.extension()

text = """A *wind turbine* is a renewable energy device that converts the kinetic energy from wind into electricity."""
wind_speed = pd.DataFrame(
    [
        ("Monday", 7),
        ("Tuesday", 4),
        ("Wednesday", 9),
        ("Thursday", 4),
        ("Friday", 4),
        ("Saturday", 5),
        ("Sunday", 4),
    ],
    columns=["Day", "Wind Speed (m/s)"],
)

dataframe_1 = pn.pane.DataFrame(wind_speed, styles={'flex': '3 1 auto'})
markdown = pn.pane.Markdown(text, styles={'flex': '1 1 0', "align-self": "center"})
dataframe_2 = pn.pane.DataFrame(wind_speed, styles={'flex': '3 1 auto'})

pn.FlexBox(dataframe_1, markdown, dataframe_2).servable()

Note

Getting the FlexBox styles right can be tricky. If you need help try posting a minimum, reproducible example on Discourse.

Media queries#

To achieve layouts depending on the overall screen/browser width, e.g. to have a different layout depending on whether we are working on a desktop or a mobile we can use media queries. Media queries allow us to apply different rules depending on a min-width or max-width, e.g. the example below will force the flexbow into a column layout when the viewport is below a size of 1200px:

import panel as pn

pn.extension()


red = pn.Spacer(height=200, width=400, styles={'background': 'red'})
green = pn.Spacer(height=200, width=400, styles={'background': 'green'})
blue = pn.Spacer(height=200, width=400, styles={'background': 'blue'})

media_query = """
@media screen and (max-width: 1200px) {
  div[id^="flexbox"] {
    flex-flow: column !important;
  }
}
"""

pn.FlexBox(red, green, blue, stylesheets=[media_query]).servable()

Try changing your browser width to see how the layout changes from row based to column based.

Exercise: Challenge#

This exercise is a bit more free-form and can be solved in many ways.

Please generate a layout that is both responsive and visually pleasing starting from the below code.

import panel as pn
import holoviews as hv
import hvplot.pandas
import pandas as pd

data_url = 'https://assets.holoviz.org/panel/tutorials/turbines.csv.gz'

@pn.cache
def get_data():
    return pd.read_csv(data_url)

df = pn.rx(get_data())

CARD_STYLE = """
:host {
  box-shadow: rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px;
  padding: 5px 10px;
}"""

manufacturers = pn.widgets.MultiChoice(options=df.t_manu.unique().rx.pipe(list), name='Manufacturer')
year = pn.widgets.IntRangeSlider(start=df.p_year.min().rx.pipe(int), end=df.p_year.max().rx.pipe(int), name='Year')
columns = ['p_name', 't_state', 't_county', 'p_year', 't_manu', 'p_cap']

filtered = df[columns][df.t_manu.isin(manufacturers.rx.where(manufacturers, df.t_manu.unique())) & df.p_year.between(*year.rx())]

count = pn.indicators.Number(name='Turbine Count', value=filtered.rx.len(), format='{value:,d} TWh', stylesheets=[CARD_STYLE])
total_cap = pn.indicators.Number(name='Total Capacity', value=filtered.p_cap.mean(), format='{value:.2f} TWh', stylesheets=[CARD_STYLE])
modal_year = pn.indicators.Number(name='Modal Year', value=filtered.p_year.mode().iloc[0], stylesheets=[CARD_STYLE])

widgets = pn.Column(manufacturers, year, stylesheets=[CARD_STYLE], margin=10)
table = pn.widgets.Tabulator(filtered, stylesheets=[CARD_STYLE], max_width=500)

year_hist = filtered.hvplot.hist(y='p_year', responsive=True, max_width=300, height=312)
cap_hist = filtered.hvplot.hist(y='p_cap', responsive=True, max_width=300, height=312)

plots = pn.Column(hv.DynamicMap(cap_hist), hv.DynamicMap(year_hist), stylesheets=[CARD_STYLE], max_width=400, margin=5)

pn.Column(count, total_cap, modal_year, widgets, table, plots).servable()
Solution

This is just an example solution. The exercise can be solved in many ways.

COMING UP