选择转换为 Text/Title

Selection Transform as Text/Title

如何将选定点上的变换显示为标题或副标题中的文本?

{
  "$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"}
  }
}

求和转换不适用于任何 graphical/visual 表示。一个想法是将其作为选择工具提示,但是 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"]}}
    }
  ]
}

请注意,文本层具有 xy 编码值,以图表左上角的像素为单位指定。

我本来想说有两种可能的方法,但后来@jakevdp post his answer,所以我想现在有三种方法。

  1. title.subtitle 中使用 ExprRef。虽然 documentation 没有明确支持,但只有 title.text 支持 ExprRef 是没有意义的。所以我试了一下,它起作用了。尽管 Vega 编辑器仍会引发架构验证警告。我在下面有一个使用分箱数据的更复杂的示例。

    1. 过滤并聚合选择。这将修改当前数据流,并且 Vega-Lite 不支持多个数据流(除非使用层),因此您必须修补生成的 Vega。
    2. 使用title.encode.subtitle.update.text访问标题的文本标记。再次需要对生成的 Vega 进行修补。 (这是假设字幕不能是 ExprRef)。

(2) 的巧妙之处在于它还允许向 selection mark, so the text could be made to follow the selection. That said that's possible with @jakevdp's answer 添加文本标记,而且他的答案要简单得多。


JavaScript loader 确保没有任何东西运行 out-of-order:

%%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

JavaScript JupyterLab 单元的加载程序作为 IPython 魔法:

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)

Jinja 将 JupyterLab 细胞模板化为 IPython 魔法:

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})
        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,6,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"}
    ]

Vega-Lite:

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"}
    , {"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"}
  , "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, 0, l.length, v, fc);
    }

    function _bisect_left(l, l0, l1, v, 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, l0, l1, v, fc);
    }

    function bisect_right(l, v, fc=cmp_primitive) {
        return _bisect_right(l, 0, l.length, v, fc);
    }

    function _bisect_right(l, l0, l1, v, 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_right(l, l0, l1, v, 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);

结果:

请注意,在着色和选择合并区域时,您必须自己计算交点。