问题描述
{
"$schema": "https://vega.github.io/schema/vega-lite/v4.json","data": {"url": "data/stocks.csv"},"transform": [{"filter": "datum.symbol==='GOOG'"}],"width": 800,"title": {
"text": "Google's stock price over time.","subtitle": "selected average: ???"
},"selection": {
"interval": {
"type": "interval","encodings": ["x"]
}
},"mark": "point","encoding": {
"x": {"field": "date","type": "temporal"},"y": {"field": "price","type": "quantitative"}
}
}
求和变换不适合任何图形/视觉表示。一个想法是将其作为选择工具提示,但is not possible。所以我选择了字幕。
解决方法
我不认为有一种简单的方法可以在字幕中插入计算值,但是您可以使用带有包含计算值的文本标记的图层图来实现相同的效果。
例如 (open in editor):
{
"data": {"url": "data/stocks.csv"},"transform": [{"filter": "datum.symbol==='GOOG'"}],"width": 800,"title": {"text": "Google's stock price over time."},"layer": [
{
"transform": [
{"filter": {"selection": "interval"}},{"aggregate": [{"op": "sum","field": "price","as": "total_price"}]}
],"mark": "text","encoding": {
"x": {"value": 400},"y": {"value": -10},"text": {"field": "total_price","type": "quantitative"}
}
},{
"mark": "point","encoding": {
"x": {"field": "date","type": "temporal"},"y": {"field": "price","type": "quantitative"}
},"selection": {"interval": {"type": "interval","encodings": ["x"]}}
}
]
}
请注意,文本层具有 x
和 y
编码值,以图表左上角的像素为单位指定。
我想说有两种可能的方法,但是@jakevdp 发布了his answer,所以我想现在只有三种。
-
在
ExprRef
中使用title.subtitle
。虽然 documentation 中没有明确支持,但只有title.text
支持ExprRef
是没有意义的。所以我试了一下,它奏效了。尽管 Vega 编辑器仍会引发模式验证警告。我在下面有一个使用分箱数据的更复杂的例子。 -
- 过滤和聚合选择。这将修改当前数据流,而 Vega-Lite 不支持多个数据流(除非使用层),因此您必须修补生成的 Vega。
- 使用
title.encode.subtitle.update.text
访问标题的文本标记。再次,有必要修补生成的织女星。 (这是基于副标题不能是ExprRef
的假设)。
关于 (2) 的巧妙之处在于它允许向 selection mark 添加文本标记,因此可以使文本跟随选择。也就是说,@jakevdp 的 answer 也是可能的,他的回答要简单得多。
JavaScript 加载器确保没有任何东西乱序运行:
%%javascript
function loadjs(ls,o) {
let i = 0;
let b = JSON.parse(document.querySelector('#jupyter-config-data').text)["baseUrl"];
ls = ls.reduce((a,l) => {
if (!l.path)
return a;
let p = l.path;
if (l.local)
p = "/" + [b,"/files",p].map(s => s.replace(/^\/+|\/+$/,"")).filter(i => i).join("/");
if (document.querySelector("script[src='" + p + "']"))
return a;
return [...a,p];
},[])
function load() {
if (i >= ls.length)
return o();
let t = document.createElement("script");
[t.type,t.src,t.onload] = ["text/javascript",ls[i],(i+1>=ls.length) ? o : load];
i = i+1;
document.head.appendChild(t);
}
return load();
}
window.loadjs = loadjs
作为 IPython 魔法的 JupyterLab 单元的 JavaScript 加载器:
import IPython
import IPython.core.magic as ipymagic
@ipymagic.magics_class
class LoadJSMagics(ipymagic.Magics):
@ipymagic.cell_magic
def loadjs(self,line,cell):
js = f"loadjs({line},() => {{\n{cell}\n}});"
return IPython.display.Javascript(js)
IPython.get_ipython().register_magics(LoadJSMagics)
JupyterLab 单元的 Jinja 模板作为 IPython 的魔法:
import jinja2
import IPython
import IPython.core.magic as ipymagic
@ipymagic.magics_class
class JinjaMagics(ipymagic.Magics):
@ipymagic.cell_magic
def jinja(self,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})
IPython.get_ipython().run_cell(r)
#d = getattr(IPython.display,line.strip(),IPython.display.display)
#return d(r)
IPython.get_ipython().register_magics(JinjaMagics)
在 Pandas 中生成一些示例时间数据:
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,7,13])
df
必要的导入:
# Convert the Pandas dataframe to a format suitable for Vega-Lite.
import altair
# Tag Vega-Embed div's with UUIDs ensuring the correct div is targeted.
import uuid
import json
vega_libs =\
[ {"path": "https://cdn.jsdelivr.net/npm/vega@5"},{"path": "/libs/vega-lite@4-fix.js","local": True},{"path": "https://cdn.jsdelivr.net/npm/vega-embed@6"}
]
织女星精简版:
s =\
{ "title":
{ "text": "Daily Counts","subtitle": {"expr": "selectionSum(data('interval_store'),data('data_0'))"},"subtitleFont": "monospace"
},"mark": "bar","encoding":
{ "x":
{ "type": "temporal","bin": "binned","field": "start","axis": { "tickCount": "day" }
},"x2": {"field": "end"},"y": {"type": "quantitative","field": "value"}
},"selection":
{ "interval":
{ "type": "interval","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"},12)","as": "end"}
],"height": 250,"width": "container","$schema": "https://vega.github.io/schema/vega-lite/v4.json","config": {"customFormatTypes": "True"},"data": altair.utils.data.to_values(df)
}
最后运行 Vega-Lite:
%%jinja
%%loadjs {{json.dumps(vega_libs)}}
{% set visid = uuid.uuid4() %}
element.innerHTML = `
<style>.vega-embed.has-actions {width:90%}</style>
<div id="vis-{{visid}}"></div>
`
var spec = {{json.dumps(s)}}
vega.expressionFunction("selectionSum",function(selection,data) {
var view = this.context.dataflow;
function intersects(i1,i2) {
return (i1[1] >= i2[0] && i1[0] <= i2[1]);
}
function cmp_interval_pt(i0,i1,p) {
if (i1 < p)
return -1;
if (i0 > p)
return 1;
return 0;
}
function cmp_primitive(a,b) {
if (a < b)
return -1
if (a > b)
return 1;
return 0;
}
function bisect_left(l,v,fc=cmp_primitive) {
return _bisect_left(l,l.length,fc);
}
function _bisect_left(l,l0,l1,fc) {
if (l1 <= l0)
return l0;
var i = Math.floor((l0+l1)/2);
var c = fc(l[i],v);
if (c < 0)
l0 = i + 1;
else
l1 = i;
return _bisect_left(l,fc);
}
function bisect_right(l,fc=cmp_primitive) {
return _bisect_right(l,fc);
}
function _bisect_right(l,v);
if (c <= 0)
l0 = i + 1;
else
l1 = i;
return _bisect_right(l,fc);
}
function cmp_data(lv,v) {
return cmp_interval_pt(lv.start,lv.end,v);
}
function constant_len_digits(s,l) {
return " ".repeat(Math.max(0,l-s.toString().length)) + s
}
if (selection.length) {
var r = selection[0]["values"][0];
var d0 = bisect_left(data,r[0],cmp_data);
var d1 = bisect_right(data,r[1],cmp_data);
var s = data.slice(d0,d1).reduce((a,v)=>a+v.value,0);
}
else
var s = 0
return `selected: ${constant_len_digits(s,3)}`;
});
vegaEmbed('#vis-{{visid}}',spec).then(function(result) {
}).catch(console.error);
结果:
请注意,在着色和选择分箱区域时,您必须自己计算交点。