Skip to content

Commit

Permalink
adjust to_html to bootstrap 5 (#217)
Browse files Browse the repository at this point in the history
* port to_html functions to bootstrap-5

* to_html: reimplement forced reflow on tab activation

* fixes static html card row spacing

* fixup random doublequote

Co-authored-by: Oege Dijk <oege.dijk@fourkind.com>
  • Loading branch information
Achim Gädke and Oege Dijk committed Jun 14, 2022
1 parent 3464a6d commit f0a3427
Showing 1 changed file with 141 additions and 113 deletions.
254 changes: 141 additions & 113 deletions explainerdashboard/to_html.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
"""Helper module to define static html outputs"""

__all__ = [
'add_header',
'row',
'rows',
'fig',
'card',
'dashboard_card',
'card_deck',
'card_rows',
'title',
'div',
'table_from_df',
'hide',
'tabs',
'input',
'jumbotron'
"add_header",
"row",
"rows",
"fig",
"card",
"dashboard_card",
"card_row",
"card_rows",
"title",
"div",
"table_from_df",
"hide",
"tabs",
"input",
"jumbotron",
]

def add_header(html:str, title="explainerdashboard", resize=True)->str:

def add_header(html: str, title="explainerdashboard", resize=True) -> str:
"""Turns a html snippet into a full html layout by adding <html>, <head> and <body> tags.
Loads bootstrap css and javascript and triggers a resize event in order to prevent
plotly figs from overflowing their div containers.
Expand All @@ -34,10 +35,9 @@ def add_header(html:str, title="explainerdashboard", resize=True)->str:
<title>{title}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js" integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js" integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13" crossorigin="anonymous"></script>
</head>
<body>
Expand All @@ -56,10 +56,10 @@ def add_header(html:str, title="explainerdashboard", resize=True)->str:
full_html += """
</html>
"""
return full_html
return full_html


def row(*cols)->str:
def row(*cols) -> str:
"""Turns a series of html snippets into a bootstrap row with equally sized
columns for each snippet.
Expand All @@ -71,7 +71,7 @@ def row(*cols)->str:
"""
for col in cols:
row += f"""
<div class="col-sm">
<div class="col">
{col}
</div>
"""
Expand All @@ -80,7 +80,8 @@ def row(*cols)->str:
"""
return row

def rows(*col_lists)->str:

def rows(*col_lists) -> str:
"""Turns a list of lists of html snippets into a series of bootstrap rows
with equally sized columns for each snippet.
Expand All @@ -90,10 +91,19 @@ def rows(*col_lists)->str:
["<div>second row snippet snippet</div>", "<div>second row snippet two</div>"]
)
"""
rows = [row(*cols) for cols in col_lists]
return "".join(rows)
rows = """
<div>
<div>
"""
rows += " </div><div>".join([row(*cols) for cols in col_lists])
rows += """
</div>
</div>
"""
return rows


def fig(fig, include_plotlyjs='cdn', full_html:bool=False)->str:
def fig(fig, include_plotlyjs="cdn", full_html: bool = False) -> str:
"""Returns html for a plotly figure. By default the plotly javascript is not
included but imported from the plotly cdn, and the full html wrapper is not included.
Expand All @@ -103,23 +113,28 @@ def fig(fig, include_plotlyjs='cdn', full_html:bool=False)->str:
hosted by plotly. If set to True then a 3MB javascript snippet is included.
For other options check https://plotly.com/python-api-reference/generated/plotly.io.to_html.html
full_html (bool): include <html>, <head> and <body> tags. Defaults to False.
"""
"""
return fig.to_html(include_plotlyjs=include_plotlyjs, full_html=full_html)

def card(html:str, title:str=None, subtitle:str=None, border=True)->str:

def card(
html: str, title: str = None, subtitle: str = None, border: bool = True
) -> str:
"""Wrap to html snippet in a bootstrap card. You can optionally add a title
and subtitle to the card.
"""
if title:
card_header = f"""<div class="card-header"><h3 class="card-title">{title}</h3>"""
card_header = (
f"""<div class="card-header"><h3 class="card-title">{title}</h3>"""
)
if subtitle:
card_header += f"""<h6 class="card-subtitle">{subtitle}</h6></div>"""
else:
card_header += "</div>"
else:
card_header = ""
return f"""
<div class="card" {'' if border else 'border-0'}>
<div class="card h-100" {'' if border else 'border-0'}>
{card_header}
<div class="card-body">
<div class="w-100">
Expand All @@ -129,8 +144,44 @@ def card(html:str, title:str=None, subtitle:str=None, border=True)->str:
</div>
"""

def dashboard_card(title:str=None, description:str=None, url:str=None)->str:
"""Generate a dashboard description car for ExplainerHub.
def card_row(*cards) -> str:
"""Turns a series of bootstrap into a row with equally sized
columns for each card.
Example:
to_html.card_row('<div class="card">first card</div>", '<div class="card">second snippet</div>')
"""
row = f"""
<div class="row row-cols-{len(cards)} g-4" style="margin-top: 20px;">
"""
for card in cards:
row += f"""
<div class="col">
{card}
</div>
"""
row += """
</div>
"""
return row


def card_rows(*cardrows_list) -> str:
"""Turn a list of lists of bootstrap cards into a series of bootstrap rows
with cards.
Example:
to_html.card_rows(
[to_html.card("card1"), to_html.card("card2")],
[to_html.card("card3"), to_html.card("card4")],
)
"""
card_decks = [[card_row(*cards)] for cards in cardrows_list]
return rows(*card_decks)


def dashboard_card(title: str = None, description: str = None, url: str = None) -> str:
"""Generate a dashboard description car for ExplainerHub.
Consists of title, description and url.
"""
return f"""
Expand All @@ -152,54 +203,28 @@ def dashboard_card(title:str=None, description:str=None, url:str=None)->str:
"""


def card_deck(*cards)->str:
"""turn a list of bootstrap cards into an equally spaced card deck.
Example:
to_html.card_deck(to_html.card("card1"), to_html.card("card2"))
"""
cards = list(cards)
cards = "".join(cards)
return f"""
<div class="card-deck">
{cards}
</div>
"""


def card_rows(*card_lists)->str:
"""Turn a list of lists of bootstrap cards into a series of bootstrap rows
with card decks.
Example:
to_html.card_rows(
[to_html.card("card1"), to_html.card("card2")],
[to_html.card("card3"), to_html.card("card4")],
)
"""
card_decks = [[card_deck(*cards)] for cards in card_lists]
return rows(*card_decks)


def title(title:str)->str:
def title(title: str) -> str:
"""wrap a title string in div and <H1></H1>"""
return f"<div><H1>{title}</H1></div>"

def div(html:str)->str:

def div(html: str) -> str:
"""wrap an html snippet in a <div></div>"""
return f'<div>{html}</div>'
return f"<div>{html}</div>"


def table_from_df(df)->str:
def table_from_df(df) -> str:
"""Generate a html table from a pandas DataFrame"""

header_row = '\n'.join([f' <th scope="col">{col}</th>' for col in df.columns])
header_row = "\n".join([f' <th scope="col">{col}</th>' for col in df.columns])
body_rows = ""
for i, row in df.iterrows():
body_rows += (' <tr>\n'+'\n'.join([" <td>"+str(val)+"</td>" for val in row.values])+'\n </tr>\n')

body_rows += (
" <tr>\n"
+ "\n".join([" <td>" + str(val) + "</td>" for val in row.values])
+ "\n </tr>\n"
)

table = f"""
<table class="table">
<thead>
Expand All @@ -214,59 +239,60 @@ def table_from_df(df)->str:
"""
return table

def hide(html:str, hide:bool=False)->str:

def hide(html: str, hide: bool = False) -> str:
"""optionally hide an html snippet (return empty div) if parameter hide=True"""
if hide:
return "<div></div>"
return html

def tabs(tabs_dict:dict)->str:

def tabs(tabs_dict: dict) -> str:
"""Generate a series of bootstrap tabs for a dictionary tabs_dict with the
name of each tab as the dict key and the html contents of the tab as the dict value.
"""
html = '<ul class="nav nav-tabs" id="myTab" role="tablist">'
tabs_id = "myTab"
html = f'<ul class="nav nav-tabs" id="{tabs_id}" role="tablist">'
for i, tab_name in enumerate(tabs_dict.keys()):
if i == 0:
html += f"""
<li class="nav-item">
<a class="nav-link active" id="{tab_name}-tab" data-toggle="tab" href="#{tab_name}" role="tab" aria-controls="{tab_name}" aria-selected="true">{tab_name}</a>
</li>\n"""
else:
html += f"""
<li class="nav-item">
<a class="nav-link" id="{tab_name}-tab" data-toggle="tab" href="#{tab_name}" role="tab" aria-controls="{tab_name}" aria-selected="false">{tab_name}</a>
</li>\n"""
tab_id_name = f"{tabs_id}{i}"
html += f"""
<li class="nav-item" role="presentation">
<button class="nav-link{'' if i else ' active'}" id="{tab_id_name}-tab"
data-bs-toggle="tab" type="button" data-bs-target="#{tab_id_name}" role="tab"
aria-selected="{'false' if i else 'true'}" aria-controls="{tab_id_name}">{tab_name}</button>
</li>
"""

html += """\n</ul>\n\n"""
html += """<div class="tab-content">\n\n"""

for i, (tab_name, tab_contents) in enumerate(tabs_dict.items()):
if i == 0:
html += f"""<div class="tab-pane active" id="{tab_name}" role="tabpanel" aria-labelledby="{tab_name}-tab">\n {tab_contents} \n</div>\n"""
else:
html += f"""<div class="tab-pane" id="{tab_name}" role="tabpanel" aria-labelledby="{tab_name}-tab">\n {tab_contents} \n</div>\n"""
html += f"""<div class="tab-content" id="{tabs_id}Content">\n\n"""

for i, (tab_name, tab_contents) in enumerate(tabs_dict.items()):
tab_id_name = f"{tabs_id}{i}"
html += f"""
<div class="tab-pane {'' if i else 'active'}" id="{tab_id_name}"
role="tabpanel" aria-labelledby="{tab_id_name}-tab"> {tab_contents}
</div>
"""
html += "\n</div>"

html += """
# https://getbootstrap.com/docs/5.1/components/navs-tabs/#via-javascript
html += f"""
<script type="text/javascript">
var triggerTabList = [].slice.call(document.querySelectorAll('#{tabs_id} button'))
triggerTabList.forEach(function (triggerEl) {{
var tabTrigger = new bootstrap.Tab(triggerEl)
$('#myTab a').on('click', function (e) {
e.preventDefault()
$(this).tab('show')
})
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
triggerEl.addEventListener('click', function (event) {{
event.preventDefault()
tabTrigger.show()
window.dispatchEvent(new Event('resize'));
})
}})
}})
</script>
"""

return html


def input(feature:str, value, disabled:bool=False)->str:
def input(feature: str, value, disabled: bool = False) -> str:
"""
Return a html feature input with a feature name and default value.
Expand All @@ -282,13 +308,15 @@ def input(feature:str, value, disabled:bool=False)->str:
</div>
"""

def jumbotron(title:str, description:str)->str:

def jumbotron(title: str, description: str) -> str:
"""display a bootstrap jumbotron with title and description"""
# see https://getbootstrap.com/docs/5.0/migration/#jumbotron
return f"""
<div class="jumbotron">
<h1 class="display-4">{title}</h1>
<hr class="my-2">
<p class="lead">{description}</p>
</div>
"""
<div class="p-5 mb-4 bg-light rounded-3">
<div class="container-fluid py-5">
<h1 class="display-5 fw-bold">{title}</h1>
<p class="col-md-8 fs-4">{description}</p>
</div>
</div>
"""

0 comments on commit f0a3427

Please sign in to comment.