How to add interactive Altair charts to your GitHub pages
Yesterday I tried to upload an interactive Altair chart that shows tooltip on hover to my GitHub blog but when I download it from the Jupyter notebook as markdown file, there's only .png
version of the chart. So, I found a workaround to have an interactive Altair on my website.
I'll use the chart from my previous blog post as an example.
import numpy as np
import pandas as pd
import altair as alt
alt.renderers.enable('notebook')
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
Create a dataset for us to plot
np.random.seed(42)
# Create a simple regression problem
X, y = make_regression(
n_samples=500,
n_features=5,
n_informative=10,
n_targets=1,
bias=10.0,
effective_rank=None,
tail_strength=0.5,
noise=155.0
)
# Train test split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
# Create percentile of y_train dataframe for chart to display
y_sort = np.sort(y_train)
perc = []
y_val = []
for i in range(101):
y_val.append(np.percentile(y_sort, i))
perc.append(i)
plot_y = pd.DataFrame({
'y': y_val,
'percentile': perc
})
Usually running only graph
will plot the graph out in the Jupyter Notebook and it will save as .png
when we download it. We don't want that. Instead, we want to save it as .html
by running .save(chart.html)
and the file contains everything we need for the tooltip to display.
# Create a selection that chooses the nearest point & selects based on x-axis value
base = alt.Chart(plot_y)
nearest = alt.selection(type='single', nearest=True, on='mouseover',
fields=['y'], empty='none')
bar = base.mark_bar().encode(
x='percentile',
y='y'
)
selectors = base.mark_point().encode(
x='percentile:Q',
opacity=alt.value(0),
).add_selection(
nearest
)
# Draw points on the line, and highlight based on selection
points = bar.mark_point().encode(
opacity=alt.condition(nearest, alt.value(1), alt.value(0))
)
# Draw text labels near the points, and highlight based on selection
text = bar.mark_text(align='left', dx=5, dy=0).encode(
text=alt.condition(nearest, 'percentile:Q', alt.value(' '))
)
text2 = bar.mark_text(align='left', dx=5, dy=-10).encode(
text=alt.condition(nearest, 'y:Q', alt.value(' '))
)
# Draw a rule at the location of the selection
rules = base.mark_rule(color='red').encode(
x='percentile:Q',
).transform_filter(
nearest
)
# Put the five layers into a chart and bind the data
graph = alt.layer(
bar, selectors, points, rules, text, text2
).properties(
width=600,
height=600,
title="Percentile of y_train values"
)
# Important step for us to get an interactive chart!!!
graph.save('chart.html')
After that, we can find the chart.html
file inside the folder that contains your current Jupyter Notebook file. Then, use a text editor to open the chart.html
file and we'll find something like this
<!DOCTYPE html>
<html>
<head>
<style>
.vega-actions a {
margin-right: 12px;
color: #757575;
font-weight: normal;
font-size: 13px;
}
.error {
color: red;
}
</style>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm//vega@5"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm//vega-lite@3.4.0"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm//vega-embed@4"></script>
</head>
<body>
<div id="vis"></div>
<script>
(function(vegaEmbed) {
var spec = {"config": {"view": {"width": 400, "height": 300}, "mark": {"tooltip": null}}, "layer": [{"mark": "bar", "encoding": {"x": {"type": "quantitative", "field": "percentile"}, "y": {"type": "quantitative", "field": "y"}}}, {"mark": "point", "encoding": {"opacity": {"value": 0}, "x": {"type": "quantitative", "field": "percentile"}}, "selection": {"selector003": {"type": "single", "nearest": true, "on": "mouseover", "fields": ["y"], "empty": "none"}}}, {"mark": "point", "encoding": {"opacity": {"condition": {"value": 1, "selection": "selector003"}, "value": 0}, "x": {"type": "quantitative", "field": "percentile"}, "y": {"type": "quantitative", "field": "y"}}}, {"mark": {"type": "rule", "color": "red"}, "encoding": {"x": {"type": "quantitative", "field": "percentile"}}, "transform": [{"filter": {"selection": "selector003"}}]}, {"mark": {"type": "text", "align": "left", "dx": 5, "dy": 0}, "encoding": {"text": {"condition": {"type": "quantitative", "field": "percentile", "selection": "selector003"}, "value": " "}, "x": {"type": "quantitative", "field": "percentile"}, "y": {"type": "quantitative", "field": "y"}}}, {"mark": {"type": "text", "align": "left", "dx": 5, "dy": -10}, "encoding": {"text": {"condition": {"type": "quantitative", "field": "y", "selection": "selector003"}, "value": " "}, "x": {"type": "quantitative", "field": "percentile"}, "y": {"type": "quantitative", "field": "y"}}}], "data": {"name": "data-589c8009bd1032acf6c3fa2e860ade11"}, "height": 600, "title": "Percentile of y_train values", "width": 600, "$schema": "https://vega.github.io/schema/vega-lite/v3.4.0.json", "datasets": {"data-589c8009bd1032acf6c3fa2e860ade11": [{"y": -835.018071493274, "percentile": 0}, {"y": -444.83823646552, "percentile": 1}, {"y": -363.3886505275625, "percentile": 2}, {"y": -340.13261772541864, "percentile": 3}, {"y": -308.54151792979076, "percentile": 4}, {"y": -282.8571006987326, "percentile": 5}, {"y": -258.2682562559114, "percentile": 6}, {"y": -252.57614139095102, "percentile": 7}, {"y": -247.6628018177709, "percentile": 8}, {"y": -241.67992060310775, "percentile": 9}, {"y": -227.7402897303325, "percentile": 10}, {"y": -216.87865611723595, "percentile": 11}, {"y": -214.36028119122957, "percentile": 12}, {"y": -204.1143076422381, "percentile": 13}, {"y": -187.18952807759686, "percentile": 14}, {"y": -170.54037671160953, "percentile": 15}, {"y": -163.05447886448047, "percentile": 16}, {"y": -155.4614553449076, "percentile": 17}, {"y": -151.55855865927654, "percentile": 18}, {"y": -144.89623543628028, "percentile": 19}, {"y": -138.75429456707442, "percentile": 20}, {"y": -134.1087335158439, "percentile": 21}, {"y": -127.43482918223924, "percentile": 22}, {"y": -120.23061569971266, "percentile": 23}, {"y": -111.79755697988983, "percentile": 24}, {"y": -101.50524308625384, "percentile": 25}, {"y": -92.70649677811616, "percentile": 26}, {"y": -88.38990107709756, "percentile": 27}, {"y": -79.03705317533408, "percentile": 28}, {"y": -74.73672458832394, "percentile": 29}, {"y": -67.99230490004527, "percentile": 30}, {"y": -63.814393030110736, "percentile": 31}, {"y": -60.740325154198885, "percentile": 32}, {"y": -56.07157187097297, "percentile": 33}, {"y": -48.53265858329275, "percentile": 34}, {"y": -45.981398293498664, "percentile": 35}, {"y": -42.39723763952238, "percentile": 36}, {"y": -36.97761712925085, "percentile": 37}, {"y": -33.26452232646075, "percentile": 38}, {"y": -31.911420870058702, "percentile": 39}, {"y": -28.20075968640002, "percentile": 40}, {"y": -21.71192215969929, "percentile": 41}, {"y": -20.613811369098432, "percentile": 42}, {"y": -15.769485333543091, "percentile": 43}, {"y": -13.435908346501137, "percentile": 44}, {"y": -7.845757682422691, "percentile": 45}, {"y": -4.274909675082899, "percentile": 46}, {"y": -0.3074004978537239, "percentile": 47}, {"y": 2.788385337913249, "percentile": 48}, {"y": 6.728667500764336, "percentile": 49}, {"y": 14.575618468552392, "percentile": 50}, {"y": 16.520205067812604, "percentile": 51}, {"y": 19.436020392895152, "percentile": 52}, {"y": 25.47180493152756, "percentile": 53}, {"y": 34.42985406425026, "percentile": 54}, {"y": 38.48102281322768, "percentile": 55}, {"y": 41.67449078991093, "percentile": 56}, {"y": 46.65879274249497, "percentile": 57}, {"y": 48.49049304230679, "percentile": 58}, {"y": 55.11450020183062, "percentile": 59}, {"y": 57.55704217447, "percentile": 60}, {"y": 61.9167307326847, "percentile": 61}, {"y": 67.7972860501853, "percentile": 62}, {"y": 74.86056713548781, "percentile": 63}, {"y": 78.28336134349712, "percentile": 64}, {"y": 90.05277287891795, "percentile": 65}, {"y": 92.65603782901759, "percentile": 66}, {"y": 99.31266688862766, "percentile": 67}, {"y": 101.10647390741705, "percentile": 68}, {"y": 102.19948053284466, "percentile": 69}, {"y": 111.85747243526058, "percentile": 70}, {"y": 120.88047946652841, "percentile": 71}, {"y": 122.42949795432479, "percentile": 72}, {"y": 127.64771726254492, "percentile": 73}, {"y": 131.74726411945286, "percentile": 74}, {"y": 135.43852133253466, "percentile": 75}, {"y": 139.15043901201068, "percentile": 76}, {"y": 140.56649199146744, "percentile": 77}, {"y": 144.97815036149007, "percentile": 78}, {"y": 156.09722114688284, "percentile": 79}, {"y": 161.9680367390797, "percentile": 80}, {"y": 168.05877291160894, "percentile": 81}, {"y": 174.0234819237287, "percentile": 82}, {"y": 176.17602735053197, "percentile": 83}, {"y": 183.60001089849922, "percentile": 84}, {"y": 189.54519567362985, "percentile": 85}, {"y": 197.1004610848934, "percentile": 86}, {"y": 209.30885091639664, "percentile": 87}, {"y": 214.57999310933724, "percentile": 88}, {"y": 219.21003558091812, "percentile": 89}, {"y": 226.4055149463644, "percentile": 90}, {"y": 251.63664457942005, "percentile": 91}, {"y": 261.79776887850085, "percentile": 92}, {"y": 292.9073422122388, "percentile": 93}, {"y": 310.27943509990195, "percentile": 94}, {"y": 342.82672465408746, "percentile": 95}, {"y": 384.57953214295935, "percentile": 96}, {"y": 398.5356115522994, "percentile": 97}, {"y": 431.1703567571817, "percentile": 98}, {"y": 459.4642261422396, "percentile": 99}, {"y": 635.4261652802663, "percentile": 100}]}};
var embedOpt = {"mode": "vega-lite"};
function showError(el, error){
el.innerHTML = ('<div class="error" style="color:red;">'
+ '<p>JavaScript Error: ' + error.message + '</p>'
+ "<p>This usually means there's a typo in your chart specification. "
+ "See the javascript console for the full traceback.</p>"
+ '</div>');
throw error;
}
const el = document.getElementById('vis');
vegaEmbed("#vis", spec, embedOpt)
.catch(error => showError(el, error));
})(vegaEmbed);
</script>
</body>
</html>
Just copy everything inside the <head></head>
tag of the chart.html
and paste it in the <head></head>
tag of the html
file you want to push to GitHub pages. In my case, the code looks like this.
<style>
.vega-actions a {
margin-right: 12px;
color: #757575;
font-weight: normal;
font-size: 13px;
}
.error {
color: red;
}
</style>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm//vega@5"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm//vega-lite@3.4.0"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm//vega-embed@4"></script>
Then copy everything from the <div>
tag till the </script>
tag in your chart.html
file and paste it into where you want the image to appear inside the html
file you want to push to GitHub pages. In my case, the code looks like this.
<div id="vis"></div>
<script>
(function(vegaEmbed) {
var spec = {"config": {"view": {"width": 400, "height": 300}, "mark": {"tooltip": null}}, "layer": [{"mark": "bar", "encoding": {"x": {"type": "quantitative", "field": "percentile"}, "y": {"type": "quantitative", "field": "y"}}}, {"mark": "point", "encoding": {"opacity": {"value": 0}, "x": {"type": "quantitative", "field": "percentile"}}, "selection": {"selector003": {"type": "single", "nearest": true, "on": "mouseover", "fields": ["y"], "empty": "none"}}}, {"mark": "point", "encoding": {"opacity": {"condition": {"value": 1, "selection": "selector003"}, "value": 0}, "x": {"type": "quantitative", "field": "percentile"}, "y": {"type": "quantitative", "field": "y"}}}, {"mark": {"type": "rule", "color": "red"}, "encoding": {"x": {"type": "quantitative", "field": "percentile"}}, "transform": [{"filter": {"selection": "selector003"}}]}, {"mark": {"type": "text", "align": "left", "dx": 5, "dy": 0}, "encoding": {"text": {"condition": {"type": "quantitative", "field": "percentile", "selection": "selector003"}, "value": " "}, "x": {"type": "quantitative", "field": "percentile"}, "y": {"type": "quantitative", "field": "y"}}}, {"mark": {"type": "text", "align": "left", "dx": 5, "dy": -10}, "encoding": {"text": {"condition": {"type": "quantitative", "field": "y", "selection": "selector003"}, "value": " "}, "x": {"type": "quantitative", "field": "percentile"}, "y": {"type": "quantitative", "field": "y"}}}], "data": {"name": "data-589c8009bd1032acf6c3fa2e860ade11"}, "height": 600, "title": "Percentile of y_train values", "width": 600, "$schema": "https://vega.github.io/schema/vega-lite/v3.4.0.json", "datasets": {"data-589c8009bd1032acf6c3fa2e860ade11": [{"y": -835.018071493274, "percentile": 0}, {"y": -444.83823646552, "percentile": 1}, {"y": -363.3886505275625, "percentile": 2}, {"y": -340.13261772541864, "percentile": 3}, {"y": -308.54151792979076, "percentile": 4}, {"y": -282.8571006987326, "percentile": 5}, {"y": -258.2682562559114, "percentile": 6}, {"y": -252.57614139095102, "percentile": 7}, {"y": -247.6628018177709, "percentile": 8}, {"y": -241.67992060310775, "percentile": 9}, {"y": -227.7402897303325, "percentile": 10}, {"y": -216.87865611723595, "percentile": 11}, {"y": -214.36028119122957, "percentile": 12}, {"y": -204.1143076422381, "percentile": 13}, {"y": -187.18952807759686, "percentile": 14}, {"y": -170.54037671160953, "percentile": 15}, {"y": -163.05447886448047, "percentile": 16}, {"y": -155.4614553449076, "percentile": 17}, {"y": -151.55855865927654, "percentile": 18}, {"y": -144.89623543628028, "percentile": 19}, {"y": -138.75429456707442, "percentile": 20}, {"y": -134.1087335158439, "percentile": 21}, {"y": -127.43482918223924, "percentile": 22}, {"y": -120.23061569971266, "percentile": 23}, {"y": -111.79755697988983, "percentile": 24}, {"y": -101.50524308625384, "percentile": 25}, {"y": -92.70649677811616, "percentile": 26}, {"y": -88.38990107709756, "percentile": 27}, {"y": -79.03705317533408, "percentile": 28}, {"y": -74.73672458832394, "percentile": 29}, {"y": -67.99230490004527, "percentile": 30}, {"y": -63.814393030110736, "percentile": 31}, {"y": -60.740325154198885, "percentile": 32}, {"y": -56.07157187097297, "percentile": 33}, {"y": -48.53265858329275, "percentile": 34}, {"y": -45.981398293498664, "percentile": 35}, {"y": -42.39723763952238, "percentile": 36}, {"y": -36.97761712925085, "percentile": 37}, {"y": -33.26452232646075, "percentile": 38}, {"y": -31.911420870058702, "percentile": 39}, {"y": -28.20075968640002, "percentile": 40}, {"y": -21.71192215969929, "percentile": 41}, {"y": -20.613811369098432, "percentile": 42}, {"y": -15.769485333543091, "percentile": 43}, {"y": -13.435908346501137, "percentile": 44}, {"y": -7.845757682422691, "percentile": 45}, {"y": -4.274909675082899, "percentile": 46}, {"y": -0.3074004978537239, "percentile": 47}, {"y": 2.788385337913249, "percentile": 48}, {"y": 6.728667500764336, "percentile": 49}, {"y": 14.575618468552392, "percentile": 50}, {"y": 16.520205067812604, "percentile": 51}, {"y": 19.436020392895152, "percentile": 52}, {"y": 25.47180493152756, "percentile": 53}, {"y": 34.42985406425026, "percentile": 54}, {"y": 38.48102281322768, "percentile": 55}, {"y": 41.67449078991093, "percentile": 56}, {"y": 46.65879274249497, "percentile": 57}, {"y": 48.49049304230679, "percentile": 58}, {"y": 55.11450020183062, "percentile": 59}, {"y": 57.55704217447, "percentile": 60}, {"y": 61.9167307326847, "percentile": 61}, {"y": 67.7972860501853, "percentile": 62}, {"y": 74.86056713548781, "percentile": 63}, {"y": 78.28336134349712, "percentile": 64}, {"y": 90.05277287891795, "percentile": 65}, {"y": 92.65603782901759, "percentile": 66}, {"y": 99.31266688862766, "percentile": 67}, {"y": 101.10647390741705, "percentile": 68}, {"y": 102.19948053284466, "percentile": 69}, {"y": 111.85747243526058, "percentile": 70}, {"y": 120.88047946652841, "percentile": 71}, {"y": 122.42949795432479, "percentile": 72}, {"y": 127.64771726254492, "percentile": 73}, {"y": 131.74726411945286, "percentile": 74}, {"y": 135.43852133253466, "percentile": 75}, {"y": 139.15043901201068, "percentile": 76}, {"y": 140.56649199146744, "percentile": 77}, {"y": 144.97815036149007, "percentile": 78}, {"y": 156.09722114688284, "percentile": 79}, {"y": 161.9680367390797, "percentile": 80}, {"y": 168.05877291160894, "percentile": 81}, {"y": 174.0234819237287, "percentile": 82}, {"y": 176.17602735053197, "percentile": 83}, {"y": 183.60001089849922, "percentile": 84}, {"y": 189.54519567362985, "percentile": 85}, {"y": 197.1004610848934, "percentile": 86}, {"y": 209.30885091639664, "percentile": 87}, {"y": 214.57999310933724, "percentile": 88}, {"y": 219.21003558091812, "percentile": 89}, {"y": 226.4055149463644, "percentile": 90}, {"y": 251.63664457942005, "percentile": 91}, {"y": 261.79776887850085, "percentile": 92}, {"y": 292.9073422122388, "percentile": 93}, {"y": 310.27943509990195, "percentile": 94}, {"y": 342.82672465408746, "percentile": 95}, {"y": 384.57953214295935, "percentile": 96}, {"y": 398.5356115522994, "percentile": 97}, {"y": 431.1703567571817, "percentile": 98}, {"y": 459.4642261422396, "percentile": 99}, {"y": 635.4261652802663, "percentile": 100}]}};
var embedOpt = {"mode": "vega-lite"};
function showError(el, error){
el.innerHTML = ('<div class="error" style="color:red;">'
+ '<p>JavaScript Error: ' + error.message + '</p>'
+ "<p>This usually means there's a typo in your chart specification. "
+ "See the javascript console for the full traceback.</p>"
+ '</div>');
throw error;
}
const el = document.getElementById('vis');
vegaEmbed("#vis", spec, embedOpt)
.catch(error => showError(el, error));
})(vegaEmbed);
</script>
Normally I will include the chart/graph right below where I ran the .save(chart.html)
section. But you can place it anywhere you want inside the <body>
tag.
Alternative for Pelican Website users to automate things
1. Navigate to your theme/templates/article.html
and theme/templates/base.html
Add the following code into your article.html
right below your {% block title %}
if there's one.
{% block scripts %}
{% if 'vega' in article.content %}
<style>
.vega-actions a {
margin-right: 12px;
color: #757575;
font-weight: normal;
font-size: 13px;
}
.error {
color: red;
}
</style>
<script src="https://cdn.jsdelivr.net/npm//vega@3.3.1"></script>
<script src="https://cdn.jsdelivr.net/npm//vega-lite@2.4.3"></script>
<script src="https://cdn.jsdelivr.net/npm//vega-embed@3.11"></script>
{% endif %}
{% endblock %}
For example, my article.html
looks like this
{% extends "base.html" %}
{% block title %}{{ article.title }}
{% endblock %}
{% block scripts %}
{% if 'vega' in article.content %}
<style>
.vega-actions a {
margin-right: 12px;
color: #757575;
font-weight: normal;
font-size: 13px;
}
.error {
color: red;
}
</style>
<script src="https://cdn.jsdelivr.net/npm//vega@3.3.1"></script>
<script src="https://cdn.jsdelivr.net/npm//vega-lite@2.4.3"></script>
<script src="https://cdn.jsdelivr.net/npm//vega-embed@3.11"></script>
{% endif %}
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8">
<h3>{{ article.title }}</h3>
<label>{{ article.date.strftime('%Y-%m-%d') }}</label>
{{ article.content }}
</div>
</div>
{% endblock %}
2. After that, add the following code into your base.html
right below your {% block title %}
if there's one.
{% block scripts %}{% endblock %}
For example, my base.html
looks like this
<!DOCTYPE html>
<html lang="en">
<head>
<title>{% block title %}{% endblock %}</title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<div class="container">
<h1><a href="{{ SITEURL }}">{{ SITENAME }}</a></h1>
</div>
{% block scripts %}{% endblock %}
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>