# Copyright 2021 Google LLC.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""Library code for creating visual reports or dashboards from Altair charts.
This is used by different dashboards that have multiple Altair charts to put
together into a single report or dashboard.
"""
from typing import Any, Dict, List, Text
import altair as alt
VEGA_URL = 'https://storage.googleapis.com/deepvariant/lib/vega'
CSS_STYLES = """
<style>
body {
font-family: sans-serif;
}
.chart-container {
padding: 30px;
}
.dataframe {
border-collapse: collapse;
white-space: nowrap;
}
.dataframe tr:nth-child(even){background-color: #ddd;}
.dataframe td,th {
border: 1px solid #ddd;
padding: 8px;
}
.dataframe tr:hover {background-color: #a8a8a8;}
.dataframe th {
text-align: left;
background-color: #4c78a8;
padding-top: 12px;
padding-bottom: 12px;
color: white;
}
.chart-container {
padding: 30px;
}
</style>
"""
def create_html_report(
specs: List[Dict[Text, alt.Chart]],
html_output: Any,
title: str = '',
subtitle: str = '',
charts_on_separate_lines: bool = False,
include_outline: bool = False,
) -> None:
"""Makes the html report with all the charts inserted.
Args:
specs: A list of dictionaries with keys "id" (unique name) and either
"chart" (should be an Altair chart object) or "html" (a string to be
inserted as html into the report).
html_output: A writable file object.
title: The title to show at the top of the report.
subtitle: The subtitle to show just below the title on the report.
charts_on_separate_lines: Put charts on separate lines. If false, charts
will set next to each other as space allows and flow to the next line,
similar to text wrapping.
include_outline: If true, an outline with chart IDs will be added on top.
Returns:
None. Writes into the html_output file object.
"""
# First sanity check input specs list.
for i, spec in enumerate(specs):
if not isinstance(spec, dict):
raise ValueError(f'item #{i+1} in specs list is not a dictionary.')
elif 'id' not in spec:
raise ValueError(f'item #{i+1} in specs list does not have an "id" key.')
chart_div_style = 'style="display:block"' if charts_on_separate_lines else ''
# Start the HTML document.
html_string = (
f'<!DOCTYPE html>\n<html>\n<head>\n'
# Add dependencies vega and vega-lite, which render the altair charts.
f'<script type="text/javascript" src="{VEGA_URL}/vega@5"></script>\n'
f'<script type="text/javascript" src="{VEGA_URL}/vega-lite@4.8.1">'
'</script>\n'
f'<script type="text/javascript" src="{VEGA_URL}/vega-embed@6">'
'</script>\n'
# Add styles (CSS).
f'{CSS_STYLES}'
'</head>\n<body>'
# Titles
f'<h1>{title}</h1>\n'
f'<h2>{subtitle}</h2>\n'
# Make a div containing all the charts.
'<div>'
)
if include_outline:
html_string += '<h3>Outline</h3>\n'
html_string += '<ul>\n'
for spec in specs:
chart_id = spec['id']
html_string += f' <li><a href="#a_{chart_id}">{chart_id}</a></li>\n'
html_string += '</ul>\n'
for spec in specs:
chart_id = spec['id']
if 'chart' in spec:
html_string += f'<a name="a_{chart_id}"></a>\n'
html_string += (
f'<div class="chart-container" {chart_div_style} '
f'id="vis_{chart_id}"></div>\n'
)
elif 'html' in spec:
html_string += spec['html']
# End the chart container and start the JavaScript section.
html_string += '</div><script>\n'
# Add JSON vega specs and hook them up to the divs with VegaEmbed.
for spec in specs:
chart_id = spec['id']
if 'chart' in spec:
try:
chart_json = spec['chart'].to_json()
# pylint: disable=bare-except
except:
print(f'Error making chart for {chart_id}')
html_string += (
f'document.getElementById("vis_{chart_id}").innerHTML = "<p>Error'
f' making chart for {chart_id}</p>"\n'
)
continue
download_filename = '{}_{}'.format(title.replace(' ', '_'), spec['id'])
embed_options = {
'mode': 'vega-lite',
'downloadFileName': download_filename,
'loader': {'target': '_blank'},
}
html_string += (
f'var spec_{chart_id} = {chart_json};\n'
f'vegaEmbed("#vis_{chart_id}", spec_{chart_id}, {embed_options})\n'
)
html_string += (
'</script>\n'
# Close HTML document.
'</body></html>'
)
html_output.write(html_string)