Doing this from JupyterLab can be a little tricky, working around some issues, but here is the solution in several cells:
First the example data:
import pandas as pd
import numpy as np
c1 = np.random.randint(1,6, size=15)
c2 = pd.date_range(start="2021-01-01",end="2021-01-15")
df = pd.DataFrame({"day": c2, "value": c1})
df = df.drop([2, 5,6,7,13])
df
Then Jinja for JupyterLab so you can substitute the Vega-Lite schema into Vega-Embed via Python.
import jinja2
import IPython
import IPython.core.magic as ipymagic
@ipymagic.magics_class
class JinjaMagics(ipymagic.Magics):
@ipymagic.cell_magic
def jinja(self, line, cell):
t = jinja2.Template(cell)
r = t.render({k:v for k,v in self.shell.user_ns.items()
if k not in self.shell.user_ns_hidden})
d = getattr(IPython.display, line.strip(), IPython.display.display)
return d(r)
IPython.get_ipython().register_magics(JinjaMagics)
Then the example schema, a little more complicated to demonstrate some pitfalls.
import altair
s =
{ "mark": "bar"
, "encoding":
{ "x":
{ "type": "temporal"
, "bin": "binned"
, "field": "start"
, "axis":
{ "tickCount": "day"
# For whatever reason a custom format function won't work
# without a transform array.
, "formatType": "interactiveFormatDate"
, "format": "test"
}
}
, "x2": {"field": "end"}
, "y": {"type": "quantitative", "field": "value"}
}
, "selection":
{ "interactive":
{ "type": "interval"
, "bind": "scales"
, "encodings": ["x"]
}
}
, "transform":
[ # Convert 'day' from 'string' to timestamp ('number')
{"calculate": "toDate(datum.day)", "as": "day"}
# Provide "start" and "end" as Date objects to match the
# type of temporal domain objects
, {"calculate": "timeOffset('hours', datum.day, -12)", "as": "start"}
, {"calculate": "timeOffset('hours', datum.day, 12)", "as": "end"}
]
, "height": 250
, "width": "container"
, "$schema": "https://vega.github.io/schema/vega-lite/v4.json"
, "config": {"customFormatTypes": "True"}
}
s["data"] = altair.utils.data.to_values(df)
# While a data array can be named, transform array results can't.
# Look at the Vega source to get the resulting name.
s["data"]["name"] = "exp"
s
Some imports for using Vega-Embed.
# Tag Vega-Embed div's with UUIDs ensuring the correct div is targeted.
import uuid
import json
And finally, Vega-Embed. In all fairness, I'm not sure if this is fetching the scripts or using those packaged with JupyterLab.
%%jinja HTML
{% set visid = uuid.uuid4() %}
<html>
<head>
<style>.vega-embed.has-actions {width:90%}</style>
<!-- Import Vega & Vega-Lite (does not have to be from CDN) -->
<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@4"></script>
<!-- Import vega-embed -->
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
</head>
<body>
<div id="vis-{{visid}}"></div>
<script type="text/javascript">
vega.expressionFunction('interactiveFormatDate', function(datum, params) {
// Data points come from both the data fields backing the current channel,
// and from the axis domain. Temporal domain axis points are 'Date' objects
// and are a subset of the visible domain. Data field points are always
// included even when offscreen and are of whatever type provided to vega.
var view = this.context.dataflow;
// These are hacks because you have to inspect the generated vega to
// determine the dataset names.
var selection = view.data("interactive_store");
var data = view.data("data_0");
// Unless the view has been shifted the selection will be empty
if (selection.length == 0){
var d1 = data[0]["start"];
var d2 = data[data.length-1]["start"];
}
else if (selection.length != 0){
var d1 = selection[0]["values"][0][0];
var d2 = selection[0]["values"][0][1];
}
var dd = (d2 - d1)/1000/60/60/24;
var tf = vega.timeFormatLocale().timeFormat;
var tl = ["%d %b", "%Y"];
return tl.map(x => tf(x)(datum));
});
var spec = {{ json.dumps(s)}}
vegaEmbed('#vis-{{visid}}', spec).then(function(result) {
// Access the Vega view instance
// (https://vega.github.io/vega/docs/api/view/) as result.view
}).catch(console.error);
</script>
</body>
</html>
The most annoying aspect is the lack of proper syntax highlighting for the HTML/JavaScript cells.