使用 Observable

概述

Quarto 原生支持 Observable JS,这是由 Mike Bostock(也是 D3 的作者)创建的一组对原生 JavaScript 的增强功能。Observable JS 以其 响应式运行时 为特点,特别适合交互式数据探索和分析。

Observable JS 的创建者(Observable, Inc.)在 https://observablehq.com/ 上运行一个托管服务,你可以在那里创建和发布笔记本。此外,你可以通过其 核心库 在独立的文档和网站中使用 Observable JS(“OJS”)。Quarto 使用这些库以及一个在渲染时运行的 编译器 来实现在 Quarto 文档中使用 OJS。

OJS 在任何 Quarto 文档中都可以工作(纯 Markdown 以及 Jupyter 和 Knitr 文档)。只需在 {ojs} 可执行代码块中包含你的代码。本文的其余部分将解释如何使用 OJS 与 Quarto 的基本知识。

示例

我们将从一个基于 Allison Horst 的 Palmer Penguins 数据集的简单示例开始。这里我们看一下企鹅的体重如何随性别和物种变化(使用提供的输入按喙长和岛屿过滤数据集):

让我们来看看这个示例的源代码。首先我们创建一个 {ojs} 单元格,使用 FileAttachment 从 CSV 文件中读取一些数据:

上面的示例并没有绘制所有的数据,而是绘制了一个过滤后的子集。为了创建我们的过滤器,我们需要一些输入,并且我们希望能够在我们过滤函数中使用这些输入的值。为此,我们使用 viewof 关键字和一些标准的 Inputs

```{ojs}
viewof bill_length_min = Inputs.range(
  [32, 50], 
  {value: 35, step: 1, label: "喙长(最小值):"}
)
viewof islands = Inputs.checkbox(
  ["Torgersen", "Biscoe", "Dream"], 
  { value: ["Torgersen", "Biscoe"], 
    label: "岛屿:"
  }
)
```

现在我们编写过滤函数,该函数将使用 bill_length_minisland 的值对从 CSV 读取的 data 进行转换。

```{ojs}
filtered = data.filter(function(penguin) {
  return bill_length_min < penguin.bill_length_mm &&
         islands.includes(penguin.island);
})
```

这里我们可以看到响应式的作用:我们不需要任何特殊语法来引用动态输入值,它们“直接可用”,并且当输入变化时,过滤代码会自动重新运行。这与你在更新单元格时,其他引用它的单元格会重新计算的工作方式非常相似。

最后,我们将使用 Observable Plot(一个用于快速可视化表格数据的开源 JavaScript 库)绘制过滤后的数据:

```{ojs}
Plot.rectY(filtered, 
  Plot.binX(
    {y: "count"}, 
    {x: "body_mass_g", fill: "species", thresholds: 20}
  ))
  .plot({
    facet: {
      data: filtered,
      x: "sex",
      y: "species",
      marginRight: 80
    },
    marks: [
      Plot.frame(),
    ]
  }
)
```

请注意,与我们输入的方式一样,我们在没有任何特殊语法的情况下引用 filtered 变量——绘图代码将在 filtered 变化时自动重新运行(而这又是每当输入变化时更新的)。

这涵盖了 OJS 的基本端到端使用(有关完整的源代码,请参阅 Penguins 示例)。

如果你查看 Penguins 代码,你会注意到一些有趣的地方:输入和绘图代码是在数据处理代码之前定义的。这展示了 OJS 单元格执行与传统笔记本之间的一个重要区别:单元格不需要按特定顺序定义。 由于执行是完全响应式的,运行时会根据单元格之间的引用关系自动按正确顺序执行单元格。这更像是一个电子表格,而不是传统的线性单元格执行的笔记本。

我们上面的示例使用了几个标准库,包括:

  1. Observable stdlib —— DOM操作、文件处理、代码导入等的核心原语。

  2. Observable Inputs —— 标准的输入控件,包括滑块、下拉菜单、表格、复选框等。

  3. Observable Plot —— 用于探索性数据可视化的高级绘图库。

这些库有些特殊,因为它们在https://observablehq.com上的笔记本以及Quarto文档中的{ojs}单元格中自动可用。

使用其他JavaScript库也很简单,只需明确导入即可。例如,这里我们使用require函数导入一些库(该函数从jsDelivr加载NPM模块):

```{ojs}
d3 = require("d3@7")
topojson = require("topojson")
```

有关使用标准库和第三方库的更多信息,请参阅Libraries文章。

数据源

在我们最初的示例中,我们使用了一个FileAttachment作为数据源。文件附件支持多种格式,包括CSV、TSV、JSON、Arrow(未压缩)和SQLite,因此是读取已准备好进行分析的数据集的便捷方式。

通常,您需要在进行可视化之前使用Python或R对数据进行预处理。使用Quarto,您可以在文档渲染期间进行此预处理,然后将结果提供给OJS。

使用Python或R中的ojs_define()函数定义您希望在JavaScript中使用的变量。例如,要在Python中重现简单的CSV读取,您可以这样做:

```{python}
import pandas as pd
penguins = pd.read_csv("palmer-penguins.csv")
ojs_define(data = penguins)
```

调用ojs_define(data = penguins)表示我们希望将名为data的变量(其值为penguins数据框)提供给OJS。

根据您使用的可视化库,可能需要一个额外的步骤来从JavaScript中消费数据。在这种情况下,Plot函数期望按行而不是按列的数据,因此我们在过滤之前进行transpose()

```{ojs}
filtered = transpose(data).filter(function(penguin) {
  return bill_length_min < penguin.bill_length_mm &&
         islands.includes(penguin.island);
})
```

有关准备和读取数据的更多方式,请参阅Data Sources文章。

OJS单元格

有许多选项可用于自定义{ojs}代码单元格的行为,包括显示、隐藏和折叠代码,以及控制输出的可见性和布局。

最重要的单元格选项是echo选项,它控制是否显示源代码。根据您是将可视化嵌入文章中还是创建笔记本或完整的教程,您会有不同的偏好。

默认情况下,{ojs}单元格中的代码会被显示。要阻止整个文档的代码显示,请在YAML元数据中设置echo: false选项:

---
title: "My Document"
execute:
  echo: false
---

您也可以在每个单元格基础上指定此选项。例如:

```{ojs}
//| echo: false
data = FileAttachment("palmer-penguins.csv").csv({ typed: true })
```

要了解所有可用选项,请参阅OJS Cells文章。

除了中断markdown流程的OJS单元格,您还可以包含内联代码。更多关于内联代码的信息,请参阅Inline Code文章。

学习更多

这些文章更深入地介绍了在Quarto文档中使用OJS:

  • Libraries 涵盖了使用标准库和外部JavaScript库。

  • Data Sources 概述了读取和预处理数据的各种方式。

  • OJS Cells 更深入地介绍了单元格执行、输出和布局。

  • Shiny Reactives 描述了如何将Shiny与OJS集成。

  • Code Reuse 深入探讨了在多个文档中重用OJS代码的方式。

如果您想了解更多关于响应式底层机制的信息,请查看Mike Bostock的这些笔记本: - 五分钟入门