Observable Plot是什么?
Observable Plot是一个免费、开源的JavaScript库,用于可视化表格数据,专注于加速探索性数据分析。它具有简洁、易记、富有表达力的界面,采用了Leland Wilkinson和Hadley Wickham所倡导的图形语法风格,灵感来自Jacques Bertin早期的想法。
以下是一个show case,展示了2016年夏季奥运会运动员身体测量数据的散点图。
Plot
.dot(olympians, {x: "weight", y: "height", stroke: "sex"})
.plot({color: {legend: true}})
一个绘图规范将数据的列(体重、身高和性别)分配给标记的可视属性(x、y和stroke)。Plot会处理剩下的事情!如果需要,您还可以进行更多配置,但是Plot的目标是帮助您快速获得有意义的可视化结果,加快分析速度。
这个散点图存在过度绘制问题:很多点被画在同一个位置上,所以很难感知密度。我们可以通过应用bin转换来解决这个问题,将相似身高、体重(和性别)的运动员分组,然后使用不透明度来编码每个bin中的运动员数量。
Plot.rect(olympians, Plot.bin({fillOpacity: "count"}, {x: "weight", y: "height", fill: "sex", inset: 0})).plot()
或者我们可以尝试使用密度标记(density mark)。
Plot.density(olympians, {x: "weight", y: "height", stroke: "sex"}).plot()
对于这些数据,一个更简单的处理方式是只关注一个维度:体重。我们可以再次使用bin转换,将体重作为x轴,频率作为y轴,生成一个直方图。该图使用了矩形标记(rect mark)和隐式堆叠变换(implicit stack transform)。
Plot.rectY(olympians, Plot.binX({y: "count"}, {x: "weight", fill: "sex"})).plot()
如果我们更喜欢将这两个分布分开显示,可以使用小多面图(small multiples)来在y轴上分割数据(为了保持一致性,保留填充编码,并添加网格线和y = 0的参考线以提高可读性)。
Plot.plot({
grid: true,
marks: [
Plot.rectY(olympians,
Plot.binX({y: "count"}, {x: "weight", fill: "sex", fy: "sex"})),
Plot.ruleY([0])
]
})
为什么是Observable Plot
Observable Plot是用于探索性数据可视化的工具。它能够帮助您快速发现洞察力。虽然其API具有表达力和可配置性,但其优化目标是简洁和易记。我们希望第一个图表的生成速度尽可能快。
而且,速度并不仅限于此:Plot还可以帮助您快速进行数据视图的转换和细化。我们希望通过使用Plot,您将花费更少的时间阅读文档、搜索需要复制粘贴的代码和调试,并且更多地花时间对数据提出问题。
与其他可视化工具相比,包括低级工具如D3和表达性较低的高级工具如图表模板,我们认为使用Plot来探索数据会更加高效。您将花更多的时间“用视觉思考”,而不是处理编程的机械性工作。
或者更简单地说:使用Plot,您将看到更多的图表。
Plot非常简洁
在Plot中,您只需一行代码就能生成一个有意义的图表。
Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm", stroke: "species"}).plot()
是什么使Plot变得简洁?一言以蔽之:默认设置。如果您指定了语义——即您的数据和所需的编码方式——Plot将自动处理其余部分。
默认设置的美妙之处在于您可以根据需要进行覆盖。这对于探索性分析非常理想:您最初只需投入最少的精力来创建图表,当您开始看到有趣的结果时,您可以逐步进行自定义以改进显示效果。也许前面提到的图表可以通过与数据成比例的纵横比、网格和图例来提高可读性?
Plot.plot({
grid: true,
aspectRatio: 1,
inset: 10,
x: {tickSpacing: 80, label: "Culmen length (mm)"},
y: {tickSpacing: 80, label: "Culmen depth (mm)"},
color: {legend: true},
marks: [
Plot.frame(),
Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm", stroke: "species"})
]
})
Plot可以转换数据
在数据分析中,准备数据(而不是可视化数据)通常占据了大部分工作。Plot的转换功能允许您在图表规范中进行数据的聚合和派生,从而减少了准备数据所花费的时间。例如,如果您有一组企鹅的数据集,您可以使用group transform快速计算它们按物种分类的频率。
Plot.plot({
marginLeft: 80,
marginRight: 80,
marks: [
Plot.barX(penguins, Plot.groupY({x: "count"}, {y: "species"})),
Plot.ruleX([0])
]
})
由于转换已经集成到Plot中,它们会自动与其他Plot功能(例如分面显示)配合使用。例如,要通过岛屿将上述图表进行细分,只需添加fy(垂直分面)选项即可。
Plot.plot({
marginLeft: 80,
marginRight: 80,
marks: [
Plot.barX(penguins, Plot.groupY({x: "count"}, {fy: "island", y: "species"})),
Plot.ruleX([0])
]
})
还要按性别进行着色吗?只需添加fill选项;然后,条形标记会自动应用隐式堆叠转换。
Plot.plot({
marginLeft: 80,
marginRight: 80,
color: {legend: true},
marks: [
Plot.barX(penguins, Plot.groupY({x: "count"}, {fy: "island", y: "species", fill: "sex"})),
Plot.ruleX([0])
]
})
Plot的转换功能可以做很多强大的事情,包括对数据进行归一化、计算移动平均值、布局树状图、分组展示、六边形分箱等。
Plot是可组合的
通过组合简单组件,可以获得更强大的功能,比如将多个标记叠加到一个图表中。Plot使得定义自定义复合标记变得容易,比如下面这个组合了规则、区域和线条的标记:
function arealineY(data, {color, fillOpacity = 0.1, ...options} = {}) {
return Plot.marks(
Plot.ruleY([0]),
Plot.areaY(data, {fill: color, fillOpacity, ...options}),
Plot.lineY(data, {stroke: color, ...options})
);
}
您可以像使用任何内置标记一样使用这个复合标记:
arealineY(aapl, {x: "Date", y: "Close", color: "blue"}).plot()
Plot在内部也使用了这种技术:坐标轴标记和框标记都是复合标记。
Plot.boxX(penguins, {x: "body_mass_g", y: "species"}).plot({marginLeft: 60, y: {label: null}})
Plot的转换也是可组合的:要应用多个转换,只需将一个转换的选项传递给下一个转换即可。有些标记甚至会自动应用隐式转换,比如上面展示的堆叠或分箱转换。标记选项是普通的JavaScript对象,因此您还可以在不同的标记之间共享选项,并检查它们进行调试。
Plot是可扩展的
Plot并不是一种全新的语言;它只是使用了普通的JavaScript。Plot支持JavaScript,允许您插入自己的函数作为访问器、缩减器、转换器…甚至自定义标记!而且,Plot生成的是SVG,因此您可以使用CSS对其进行样式化,并像使用D3一样对其进行操作。(可以参考Mike Freeman的tooltip插件,这是一个很好的扩展Plot的示例。)
Plot基于D3构建
Plot基于我们十多年来开发D3的经验,D3是网络上最受欢迎的数据可视化库。
Plot使用D3实现了多种功能:
- 比例尺(刻度、颜色方案、数字格式化)
- 图形(区域、线条、曲线、符号、堆叠)
- 平面几何(Delaunay、Voronoi、等高线、密度估计)
- 球面几何(地理投影)
- 数据操作(分组、汇总、分箱、统计)
- 树状图 ……等等!
如果您已经了解一些D3,您会发现Plot中很多部分都很熟悉。
我们长久以来一直说D3能够让事情变得可能,但并不一定容易。而这一点与具体任务无关。D3确实使得很多困难且惊人的事情成为可能,但即使是本应该很简单的事情,有时候也并非如此。借用Amanda Cox的话来说:“如果你认为为一个柱状图编写一百行代码是再正常不过的事情,那就使用D3吧。”
Plot的目标是让简单的事情变得简单、快速,甚至更多。
提示: Plot是否达到了这个目标取决于您,因此我们非常希望您能就使用Plot时遇到的易于或困难的问题给出反馈。当您遇到困难时,我们鼓励您寻求帮助。我们从帮助中学到了很多!
由于Plot和D3具有不同的目标,它们做出了不同的权衡。Plot更高效:您可以快速创建图表。但它也不够表达性强:使用D3的底层API更适合定制化的可视化,比如具有大量动画和交互的特殊可视化、先进的技术(如力导向图布局)或者开发自己的图表库。
如果您认为D3更有表达力的优势值得投入时间和精力,我们建议您选择D3来定制数据可视化。对于像纽约时报或The Pudding这样的媒体机构来说,D3是一个明智的选择,因为他们的单个图形可能会被数百万读者看到,而且编辑团队可以共同努力推进视觉传达艺术的发展;但它是否是建立团队的私人仪表板或一次性分析的最佳工具?您可能会惊讶于使用Plot时可以实现的程度。
起飞吧
普通的HTML
您可以从jsDelivr等CDN加载Plot,也可以将其本地下载。我们推荐使用托管在CDN上的ES模块捆绑包,因为它会自动加载Plot对D3的依赖项。但对于那些需要的人,我们还提供了一个UMD捆绑包,当作为普通脚本加载时,它会导出全局的Plot对象。
- ESM+ CDN
<!DOCTYPE html>
<div id="myplot"></div>
<script type="module">
import * as Plot from "https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6/+esm";
const plot = Plot.rectY({length: 10000}, Plot.binX({y: "count"}, {x: Math.random})).plot();
const div = document.querySelector("#myplot");
div.append(plot);
</script>
- UMD + CDN
<!DOCTYPE html>
<div id="myplot"></div>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6"></script>
<script type="module">
const plot = Plot.rectY({length: 10000}, Plot.binX({y: "count"}, {x: Math.random})).plot();
const div = document.querySelector("#myplot");
div.append(plot);
</script>
Plot返回的是一个独立的DOM元素,可以是SVG或HTML figure元素。在普通的Web开发中,这意味着您需要将生成的图表插入到页面中才能看到它。通常,这可以通过选择一个DOM元素(比如具有唯一标识符的DIV,就像上面的myplot示例),然后调用element.append方法来实现。
如果您希望本地运行Plot(或完全离线使用),您可以从以下链接下载Plot及其依赖项D3的UMD捆绑包:
d3.js plot.js
然后,按照上面在UMD + local标签页中展示的方式创建一个index.html文件。如果您更喜欢较小的压缩文件,您可以下载d3.min.js和plot.min.js,然后相应地更新上面的src属性。
npm包
如果您正在使用Node开发Web应用程序,您可以通过yarn、npm、pnpm或您喜欢的包管理器安装Plot。
// yarn
yarn add @observablehq/plot
// npm
npm install @observablehq/plot
// pnpm
pnpm add @observablehq/plot
您可以将Plot加载到应用程序中,如下所示:
import * as Plot from "@observablehq/plot";
如果您喜欢,也可以按需引入:
import {barY, groupX} from "@observablehq/plot";
Plot包含了具有详细文档的TypeScript声明。我们强烈推荐使用带有增强代码自动完成功能的编辑器,如Visual Studio Code或Observable。
在react中使用
对于在React中使用Plot,我们根据您的需求推荐两种方法。
第一种是服务端渲染(SSR)图表。这样可以在页面加载时最小化重新布局,提高用户体验。为了实现这种方式,在Plot中使用document plot选项告诉它使用React的虚拟DOM进行渲染。例如,下面是一个PlotFigure组件的示例:
import * as Plot from "@observablehq/plot";
import {createElement as h} from "react";
export default function PlotFigure({options}) {
return Plot.plot({...options, document: new Document()}).toHyperScript();
}
然后这样:
import * as Plot from "@observablehq/plot";
import PlotFigure from "./PlotFigure.js";
import penguins from "./penguins.json";
export default function App() {
return (
<div>
<h1>Penguins</h1>
<PlotFigure
options={{
marks: [
Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm"})
]
}}
/>
</div>
);
}
对于简单的小数据图表而言,服务端渲染是可行的;但对于复杂的图表,例如地理地图或包含数千个元素的图表,由于序列化的SVG文件较大,更适合在客户端进行渲染。对于这种第二种方法,您可以使用useRef来获取DOM元素的引用,然后使用useEffect生成并插入您的图表。
import * as Plot from "@observablehq/plot";
import * as d3 from "d3";
import {useEffect, useRef, useState} from "react";
export default function App() {
const containerRef = useRef();
const [data, setData] = useState();
useEffect(() => {
d3.csv("/gistemp.csv", d3.autoType).then(setData);
}, []);
useEffect(() => {
if (data === undefined) return;
const plot = Plot.plot({
y: {grid: true},
color: {scheme: "burd"},
marks: [
Plot.ruleY([0]),
Plot.dot(data, {x: "Date", y: "Anomaly", stroke: "Anomaly"})
]
});
containerRef.current.append(plot);
return () => plot.remove();
}, [data]);
return <div ref={containerRef} />;
}
这个示例还演示了使用useState异步加载CSV数据的方法。如果您想更新图表,比如因为数据发生了变化,只需使用element.remove将旧的图表移除,然后用新的图表替换它。这在上面的useEffect的清理函数中完成。