IN4MATX 133: User Interface Software — Lecture 7:TypeScript 与数据可视化工具
目录
- 1. 什么是 TypeScript
- 2. 类型
- 3. 类、接口与继承
- 4. tsconfig.json 与声明文件
- 5. 优点、缺点与其他转译器
- 6. 交互式软件与 UI 工具
- 7. 评估工具:门槛与天花板
- 8. 可视化工具与声明式语言
- 9. Vega-Lite:图形语法
- 10. 在 Vega-Lite 中构建直方图
- 11. 合理的默认值及其覆盖
- 12. 课堂问题与演示(附解答)
- 13. 速查与必记的 5 个要点
UCI IN4MATX 133: User Interface Software — Prof. Travis Linden Reid,2026 年秋季学期,周四 10/15
内容整理自 Lecture 7 的幻灯片(共 78 页)和 demo 仓库文件夹W3_L2_Oct15_TypeScript_VegaLite。按概念组织,而不是按来源组织。
📅 课程安排:本讲是 A2 所需的最后一部分内容;周五的 discussion 会有更深入的 Vega-Lite 演示。
加分幻灯片 p70–78(更多 TypeScript 内容)已并入第 2–4 节,并标注为“(加分幻灯片)”。
1. 什么是 TypeScript
安装(全局 npm 包,见 Lecture 6):
npm install -g typescript # installs tsc, the TypeScript transpiler也可以通过 Homebrew(Mac)或 Chocolatey(PC)安装。
关于 TypeScript(幻灯片 6–7):
- Microsoft,2012 年;最初只支持 Visual Studio,现在支持 Eclipse、Sublime、WebStorm、Atom 等。最新版本:TypeScript 7.0(2026 年 7 月)。
- 为 JavaScript 增加了静态类型、类型检查和对象,且这些都是可选的。
- 转译(transpile,也叫“transcompile”)成 JavaScript:
tsc把 TS 转换成等价的 JS;浏览器只运行 JS。 - 是 JavaScript 的严格超集:合法的 JS 就是合法的 TS,所以 TS 可以使用流行的 JS 库。
类型在编译期检查(和 C++ 一样),之后被擦除:.js 里不会留下任何类型。
🔗 延伸阅读:TypeScript: Download
2. 类型
2.1 标注变量与转译
hello.ts 在冒号后写类型:
var courseNumber:number = 133;
console.log('Hello, IN4MATX ' + courseNumber + '!');tsc hello.ts 生成 hello.js,类型标注消失:
var courseNumber = 133;
console.log('Hello, IN4MATX ' + courseNumber + '!');会输出 Hello, IN4MATX 133!(现在的 tsc 还会额外加上 "use strict";)。(幻灯片的 bug:hello.js 末尾是 ‘!');,用了弯引号,是语法错误 → 应为 '!'。)
预定义类型(幻灯片 9):number、boolean、string、void(通常用作函数返回类型)、null、undefined、any。
类型是可选的。下面两种写法都能初始化 courseNum:
var courseNum:any = 133;
var courseNum = 133;
console.log('Hello, IN4MATX ' + courseNum + '!');这是两种备选写法(lecture.ts 里把其中一个注释掉了);放在一起会冲突,因为第二个会推断为 number。(幻灯片的 bug:同样的弯引号,‘!'。)
2.2 带类型的函数与错误检查
函数可以指定参数类型和返回类型:
function area(shape: string, width: number, height: number)
{
var area = width * height;
return "I'm a " + shape + " of area " + area + " cm^2.";
}
// "better" function
function area2(shape: string, width: number, height: number):string {
var area:number = width * height;
return "I'm a " + shape + " of area " + area + " cm^2.";
}两者都返回 I'm a square of area 225 cm^2.;area2 之所以“更好”,是因为它连返回值和局部变量的类型也写明了。
类型让错误检查成为可能(幻灯片 12):参数顺序写反了:
document.body.innerHTML = area2(15, 15, 'square');tsc 报错:error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.(幻灯片上写的是 type '15';在 lecture.ts 中被注释掉了。)
演示(tsc 输出的 JS):第 1 步的错误调用会悄悄地运行,输出 I'm a 15 of area NaN cm^2.
{"title": "Slides 11–12 · the JavaScript tsc emits for area2 (the browser runs JS, not TS)", "interactive": true, "html": "<p>See the console.</p>", "js": "// compiled from area2 in lecture.ts (types stripped by tsc)\nfunction area2(shape, width, height) {\n var area = width * height;\n return \"I'm a \" + shape + \" of area \" + area + \" cm^2.\";\n}\nconsole.log(area2('square', 15, 15));", "height": 60, "steps": [{"note": "The slide's bad call: tsc rejects it, but if you skip the type check the JS runs and silently prints NaN.", "js": "console.log(area2(15, 15, 'square'));"}]}🧪 演示文件:lecture.ts · lecture.js
⚠️ 易错点:即使有类型错误,tsc(默认)仍然会写出
.js,所以一定要看报错信息。
2.3 联合类型、可选参数、泛型(加分幻灯片)
联合类型(union type,幻灯片 77,来自 TypeScript 1.4 的发布公告):| 表示可以是几种类型之一:
function f(x: number | number[]) {
if (typeof x === "number") {
return x + 10;
}
else {
// return sum of numbers
}
}f(5) → 15;f([1, 2, 3]) → undefined(else 里只有注释);f("5") 是类型错误。
可选参数与默认参数(幻灯片 78,TypeScript Handbook):? 表示参数可选,= 给出默认值:
function f(n: number) {
console.log(n.toFixed()); // 0 arguments
console.log(n.toFixed(3)); // 1 argument
}
function f(x?: number) {
// ...
}
f(); // OK
f(10); // OK
function f(x = 10) {
// ...
}(这是三段互相独立的代码。)省略的 x? 是 undefined;没有 ? 时 f() 会报错;x = 10 的默认值为 10。当 n = 3.14159 时:toFixed() → 3,toFixed(3) → 3.142。
泛型(generics,幻灯片 75)“和 Java 一样”:类型参数 T 保留调用者传入的类型:
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("myString"); // type of output will be 'string'
let output = identity("myString"); // type of output will be 'string'第二次调用会推断 T(类似 C++ 的模板)。(幻灯片的 bug:let output 写了两次 → Cannot redeclare block-scoped variable 'output';改掉其中一个名字即可。)
🔗 延伸阅读:TypeScript Handbook: Generics · Announcing TypeScript 1.4 (union types) · TypeScript Handbook: Optional parameters
3. 类、接口与继承
3.1 类
类“和 Java 一样”:顶部是带类型的字段,然后是构造函数和方法:
class Shape {
area: number;
color: string;
name: string;
constructor (name: string, width: number, height: number ) {
this.name = name
this.area = width * height;
this.color = "pink";
};
shoutout() {
return "I'm " + this.color + " " + this.name + " with an area of " + this.area + " cm squared.";
}
}
var square = new Shape("square", 30, 30);square.shoutout() → I'm pink square with an area of 900 cm squared.(lecture.js 中是同样的 JS class,字段没有类型。)
3.2 接口(加分幻灯片)
接口(interface)描述一个对象的“输入”和“输出”:它必需的属性及其类型:
interface Shape {
name: string;
width: number;
height: number;
color?: string; // ? Specifies an "optional" value
}
function area(shape : Shape):string {
var area = shape.width * shape.height;
return "I'm " + shape.name + " with area " + area + " cm squared";
}
console.log(area({name: "rectangle", width: 30, height: 15}));
console.log(area({name: "square", width: 30, height: 30, color: "red"}));会输出面积 450 和 900。任何带有正确属性的对象字面量都是一个 Shape;省略 height 会报类型错误。(幻灯片的 bug:弯引号 “red" → "red"。)
3.3 继承(加分幻灯片)
类和接口都可以被扩展(extend):super(...) 调用基类构造函数,同名方法会重写(override),super.method() 调用基类的版本:
class Shape3D extends Shape {
volume: number;
constructor (name: string, width: number, height: number, length: number ) {
super( name, width, height ); //calls base class constructor
this.volume = length * this.area;
};
shoutout() { //overrides the base class
return "I'm " + this.name + " with a volume of " + this.volume + " cm cube.";
}
superShout() { //calls base class shoutout method
return super.shoutout();
}
}
var cube = new Shape3D("cube", 30, 30, 30);
console.log( cube.shoutout() );
console.log( cube.superShout() );会输出 I'm cube with a volume of 27000 cm cube.,然后(基类方法)输出 I'm pink cube with an area of 900 cm squared. 演示:
{"title": "Slides 17 & 74 · compiled JS of Shape and Shape3D", "interactive": true, "html": "<p>See the console.</p>", "js": "// compiled JavaScript of the slide 17 Shape class + slide 74 Shape3D (types stripped)\nclass Shape {\n constructor(name, width, height) {\n this.name = name;\n this.area = width * height;\n this.color = \"pink\";\n }\n ;\n shoutout() {\n return \"I'm \" + this.color + \" \" + this.name + \" with an area of \" + this.area + \" cm squared.\";\n }\n}\nvar square = new Shape(\"square\", 30, 30);\nconsole.log(square.shoutout());\nclass Shape3D extends Shape {\n constructor(name, width, height, length) {\n super(name, width, height); //calls base class constructor\n this.volume = length * this.area;\n }\n ;\n shoutout() {\n return \"I'm \" + this.name + \" with a volume of \" + this.volume + \" cm cube.\";\n }\n superShout() {\n return super.shoutout();\n }\n}\n\nvar cube = new Shape3D(\"cube\", 30, 30, 30);\nconsole.log(cube.shoutout());\nconsole.log(cube.superShout());\n", "height": 60}3.4 Getter 与 setter(加分幻灯片)
get/set 在读取或赋值属性时运行代码;private 隐藏底层字段:
const fullNameMaxLength = 10;
class Employee {
private _fullName: string = "";
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (newName && newName.length > fullNameMaxLength) {
throw new Error("fullName has a max length of " + fullNameMaxLength);
}
this._fullName = newName;
}
}emp.fullName = "Peter" 会存入该值;一个 14 个字符的名字会抛出异常。从外部访问 emp._fullName 只会产生编译期错误。
4. tsconfig.json 与声明文件
tsconfig.json 文件:
- 表明这是一个 TypeScript 项目;
- 指定要转译哪些文件或文件夹;
- 选择转译器选项,例如是否删除注释;
- 指定要转译到哪个版本的 ECMAScript(JavaScript)。
用下面的命令构建整个项目:
tsc --project tsconfig.json声明文件(加分幻灯片 71–72)。.d.ts 保留了被擦除的类型,这样库的使用者就可以检查自己的调用:
tsc --declaration test.ts
npm install --save @types/your-library-here # typings for many existing JS libraries//test.ts
function area(shape: string, width: number, height: number):string {
var area:number = width * height;
return "I'm a " + shape + " of area " + area + " cm^2.";
}
document.body.innerHTML = area('square', 15, 15);// transpiled test.js
function area(shape, width, height) {
var area = width * height;
return "I'm a " + shape + " of area " + area + " cm^2.";
}
document.body.innerHTML = area('square', 15, 15);// generated test.d.ts
declare function area(shape: string, width: number, height: number): string;.d.ts 就像 C++ 的头文件:只有函数签名,没有函数体。
5. 优点、缺点与其他转译器
| TypeScript 的优点 | TypeScript 的缺点 |
|---|---|
| 类型检查:在代码运行之前就能发现错误 | 转译有时很烦人;大项目里可能很慢 |
| 编辑器可以自动补全方法和 API | 对新的 JavaScript 库可能无法开箱即用 |
| 更容易重构代码 | 适配 Angular 和 React 花了一段时间(现在是 Angular 的默认语言) |
| 面向对象:更容易管理和维护大型代码库 | |
| 足够简单:与 Java 等面向对象语言原理相同 |
其他值得一提的 JavaScript 转译器:
| Dart | Kotlin | |
|---|---|---|
| 开发者 | JetBrains(开发了很多 IDE) | |
| 理念 | 引入类型和类似的面向对象实践 | “更好的” Java,与 Java 完全互操作 |
| 转成 JavaScript 的方式 | dart2js |
kotlin.js(配合 React 等使用) |
6. 交互式软件与 UI 工具
话题切换:软件工具与可视化,这只是很窄的一小部分;想学更多,请选 IN4MATX 143。
顺序式程序(命令行)。程序先给出提示;使用者等待,然后输入,可以建模成一个文件(input()、std::cin)。
事件驱动编程(event-driven programming)。程序等待使用者;输入以事件(鼠标按下、拖动、按键松开)的形式到达,所有事件放进同一个有序的事件队列(event queue),从而屏蔽设备细节(addEventListener,见 Lecture 6)。
基本的交互式软件循环在所有交互式软件的某个地方都存在:
do {
e = read_event(); // Input
dispatch_event(e); // Processing
if (damage_exists())
update_display(); // Output
} while (e.type != WM_QUIT); 给每个网页或应用都写一遍这个循环会很繁琐,所以我们依赖工具来处理常见操作。
例子:按钮。设置 X/Y 边界,检查鼠标按下和松开是否都在边界内,然后触发事件,每个按钮都要这样做。
理解工具。
- 什么是 UI 工具?帮助构建 UI 的软件/库:Bootstrap(响应式)、Angular、React 等。
- 为什么要用?它们把常见实践打包进框架,让这些实践容易遵循,让你专注于自己的应用。
- 它们带来更快、更迭代式的设计,更好的实现,以及使用同一工具的各个应用之间的一致性。
- 为什么工具很难设计?核心实践和人们的任务一直在变。
7. 评估工具:门槛与天花板
评估一个工具“有多有用”的四个关键术语(Myers、Hudson & Pausch,TOCHI 2000):
| 术语 | 含义 | 幻灯片中的说明 |
|---|---|---|
| 门槛(Threshold) | 上手有多难 | 取决于已有知识;新语言会提高门槛,借用已有概念会降低门槛。 |
| 天花板(Ceiling) | 能做到多少事 | 与直接访问设备相比,工具会限制可能性。 |
| 阻力最小的路径(Path of least resistance) | 工具会影响人们做出什么样的界面 | Bootstrap 页面看起来都差不多(Lecture 4)。Sapir-Whorf 假说:语言限制思维;工具框定了我们思考设计的方式。 |
| 移动靶(Moving targets) | 需求变化会让工具过时 | 固化的规范会约束设计;认知和技术(例如 VR)一直在演进。 |
两者都是相对的。最好的工具是低门槛、高天花板(Photoshop 通过提供教程来降低门槛)。
📝 练习:问题 2
🔗 延伸阅读:Myers, Hudson & Pausch (2000): Past, present, and future of user interface software tools
8. 可视化工具与声明式语言
这和可视化有什么关系?可视化工具遵循同样的原则。
8.1 SVG 与 Excel
SVG(Scalable Vector Graphics,可缩放矢量图形):基于 XML、类似 HTML 的标记,用来描述线、圆、矩形;浏览器负责渲染。(幻灯片:由像素组成的位图“S”与平滑的矢量“S”的对比。)
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>一个黄色的圆,绿色边框,圆心 (50, 50),半径 40:
{"title": "Slide 43 · an SVG circle", "interactive": true, "html": "<svg width=\"100\" height=\"100\">\n <circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"green\" stroke-width=\"4\" fill=\"yellow\" />\n</svg>", "height": 120, "steps": [{"note": "Add a rectangle: SVG is built from shapes like this.", "html": "<svg width=\"100\" height=\"100\">\n <circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"green\" stroke-width=\"4\" fill=\"yellow\" />\n <rect x=\"35\" y=\"35\" width=\"30\" height=\"30\" fill=\"blue\" />\n</svg>"}]}| 工具 | 门槛 | 天花板 |
|---|---|---|
| SVG | 高 | 高(什么都能画,例如 Minard 的拿破仑远征图) |
| Microsoft Excel | 低 | 低(标准图表,例如幻灯片上的“Wildlife Population”条形图) |
8.2 可视化工具的谱系
幻灯片 45,从门槛较低(上)到天花板较高(下):
| 层次 | 类型 | 例子 |
|---|---|---|
| 图表工具(Charting tools) | 图表类型库 | Excel、Google Charts |
| 声明式语言 | 可视分析语法 | ggplot、VizQL |
| 声明式语言 | 可视化语法(幻灯片中高亮) | Protovis、D3.js、Vega-Lite |
| 编程语言 | 图形 API | Processing、SVG、OpenGL |
8.3 声明式与命令式
声明式语言(declarative language)描述的是做什么(what),而不是怎么做(how),把规范(specification)与执行(execution)分离;命令式(imperative)代码则给出明确的步骤。
幻灯片把 HTML(标记)和 CSS(样式)重新归为声明式,把 JavaScript(编程)归为命令式。幻灯片 49:
<main class="container">
<div class="row">
<div class="col-3">A</div>
<div class="col-6">B</div>
<div class="col-4">C</div>
<div class="col">D</div>
<div class="col">E</div>
</div>
</main>var array = ['1', 'fish', 2, 'blue'];
array[5] = 'dog';
array.push('2');
array[2] = array[array.length - 1] - 4;
array[0] = typeof array[2];
array[4] = array.indexOf('blue');
console.log(array.join('*'));网格:描述是什么。JS:一步一步地执行,最终输出 number*fish*-2*blue*3*dog*2(Lecture 5)。(幻灯片的 bug:弯引号 “col-4"、“col";已修正。)
为什么用声明式?迭代更快,代码更少,门槛更低,而且可以用程序生成。
9. Vega-Lite:图形语法
目标(Satyanarayan 等,IEEE Vis 2017):“一种表达力强(高天花板)又简洁(低门槛)的、用于描述可视化的声明式语言”。
图形语法(grammar of graphics):一张图表由以下几部分组成:
| 部分 | 含义 |
|---|---|
| Data(数据) | 要可视化的输入数据 |
| Mark(标记) | 代表数据的图形:面积(area)、柱(bar)、点(point)、图像(image)、线(line)、标签(text)、规则线(rule)、弧(arc) |
| Transform(变换) | 是否过滤、聚合、分箱等 |
| Encoding(编码) | 数据字段与标记属性(x、y、color 等)之间的映射 |
| Scale(比例尺) | (编码的一部分)把数据值映射成视觉值 |
| Guides(引导元素) | 把比例尺可视化的坐标轴和图例 |
一份规范(spec)就是一个 JSON 对象;数据通常是由记录组成的 JSON 数组(仓库里的 seattle-weather.json,2015 年共 365 行):
[
{
"date": "2015/01/01",
"weather": "sun",
"temperature": 1.1999999999999997
},
{
"date": "2015/01/02",
"weather": "fog",
"temperature": 2.8
},
...
]加载 vega、vega-lite 和 vega-embed 脚本之后,用 vegaEmbed('#id', spec) 把它画出来(演示 2)。
🔗 延伸阅读:Satyanarayan et al. (2017): Vega-Lite: A Grammar of Interactive Graphics
10. 在 Vega-Lite 中构建直方图
直方图 = 柱状图,x = 分箱后的字段,y = 计数,再按 weather 着色。一次只改一处,逐步构建:
第 1 步:每条记录一个刻度线(幻灯片 55):mark 为 "tick",x = "temperature":
{
data: {url: "weather-seattle.json"},
mark: "tick",
encoding: {
x: {
field: "temperature",
type: "quantitative"
}
}
}每个字段都属于四种类型之一:
| 类型 | 含义 | 例子 |
|---|---|---|
quantitative |
数值型 | temperature |
temporal |
时间 | date |
ordinal |
有序类别 | 年龄段、月份 |
nominal |
无序类别 | weather、origin |
第 2 步:分箱(幻灯片 56):在 x 上加 bin: true;坐标轴标题变成 BIN(temperature),每个箱一个刻度线,还没有计数。
第 3 步:计数(幻灯片 57):y 使用 aggregate: "count";仍然是刻度线,位于各自计数的高度。
第 4 步:柱(幻灯片 58):mark: "bar" 就变成了直方图:
{
data: {url: "weather-seattle.json"},
mark: "bar",
encoding: {
x: {
bin: true,
field: "temperature",
type: "quantitative"
},
y: {
aggregate: "count",
type: "quantitative"
}
}
}第 5 步:颜色(幻灯片 59):color: {field: "weather", type: "nominal"} 把每根柱拆成堆叠的多段,并带有图例。
演示用了 12 行内联数据(data: {values: …}),所以计数很小:
{"title": "Slides 55–59 · building the histogram one encoding at a time (inline rows from seattle-weather.json)", "interactive": true, "libs": ["vega-lite"], "html": "<div id=\"vis\"></div>", "js": "// 12 rows from the repo's seattle-weather.json (the demo can't load the file)\nvar weather = [\n {\"date\": \"2015/01/01\", \"weather\": \"sun\", \"temperature\": 1.2},\n {\"date\": \"2015/01/02\", \"weather\": \"fog\", \"temperature\": 2.8},\n {\"date\": \"2015/01/18\", \"weather\": \"rain\", \"temperature\": 10.55},\n {\"date\": \"2015/03/25\", \"weather\": \"fog\", \"temperature\": 10.8},\n {\"date\": \"2015/04/01\", \"weather\": \"rain\", \"temperature\": 9.2},\n {\"date\": \"2015/05/22\", \"weather\": \"sun\", \"temperature\": 14.2},\n {\"date\": \"2015/06/15\", \"weather\": \"drizzle\", \"temperature\": 23.05},\n {\"date\": \"2015/07/08\", \"weather\": \"drizzle\", \"temperature\": 22.2},\n {\"date\": \"2015/07/31\", \"weather\": \"sun\", \"temperature\": 26.1},\n {\"date\": \"2015/08/14\", \"weather\": \"rain\", \"temperature\": 16.65},\n {\"date\": \"2015/08/22\", \"weather\": \"drizzle\", \"temperature\": 19.45},\n {\"date\": \"2015/10/19\", \"weather\": \"fog\", \"temperature\": 14.7}\n];\n\nvegaEmbed(\"#vis\", {\n data: {values: weather},\n mark: \"tick\",\n encoding: {\n x: {field: \"temperature\", type: \"quantitative\"}\n }\n}, {actions: false});", "height": 260, "tab": "js", "steps": [{"note": "Slide 56: bin: true groups the temperatures into bins.", "js": "vegaEmbed(\"#vis\", {\n data: {values: weather},\n mark: \"tick\",\n encoding: {\n x: {bin: true, field: \"temperature\", type: \"quantitative\"}\n }\n}, {actions: false});"}, {"note": "Slide 57: y counts the records in each bin (still ticks).", "js": "vegaEmbed(\"#vis\", {\n data: {values: weather},\n mark: \"tick\",\n encoding: {\n x: {bin: true, field: \"temperature\", type: \"quantitative\"},\n y: {aggregate: \"count\", type: \"quantitative\"}\n }\n}, {actions: false});"}, {"note": "Slide 58: change the mark to bar, and it's a histogram.", "js": "vegaEmbed(\"#vis\", {\n data: {values: weather},\n mark: \"bar\",\n encoding: {\n x: {bin: true, field: \"temperature\", type: \"quantitative\"},\n y: {aggregate: \"count\", type: \"quantitative\"}\n }\n}, {actions: false});"}, {"note": "Slide 59: color by the nominal weather field; the bars stack automatically.", "js": "vegaEmbed(\"#vis\", {\n data: {values: weather},\n mark: \"bar\",\n encoding: {\n x: {bin: true, field: \"temperature\", type: \"quantitative\"},\n y: {aggregate: \"count\", type: \"quantitative\"},\n color: {field: \"weather\", type: \"nominal\"}\n }\n}, {actions: false});"}]}🧪 演示文件:seattle-weather.json
⚠️ 易错点:幻灯片加载的是
"weather-seattle.json",而仓库里的文件叫seattle-weather.json;url写错会得到一张空图表,且不报错。(幻灯片里的snow在仓库的 2015 年数据中并不存在。)
11. 合理的默认值及其覆盖
幻灯片 59 从未指定颜色、分箱宽度、坐标轴标签或堆叠方式:Vega-Lite 自己选了“合理的默认值”(sensible defaults)。
覆盖默认值(幻灯片 61):自定义颜色比例尺,把每个 domain 值映射到一个 range 颜色:
color: {
field: "weather",
type: "nominal",
scale: {
domain: ["sun","fog","drizzle", "rain","snow"],
range: ["#e7ba52","#c7c7c7","#aec7e8","#1f77b4","#9467bd"]
}
}幻灯片的 bug:scale 被放在了 color 旁边,所以 Vega-Lite 会丢弃它(并给出警告)→ 应像上面这样放进 color 里面(弯引号 “#1f77b4" 也已修正)。
每月降水量(幻灯片 62):timeUnit: "month" 按月份对日期(例如 "2018-10-17")分组并标注;aggregate: "mean" 求平均值:
{
data: {url: "weather-seattle.json"},
mark: "bar",
encoding: {
x: {
timeUnit: "month", field: "date",
type: "quantitative"
},
y: {
aggregate: "mean",
field: "precipitation",
type: "quantitative"
}
}
}幻灯片的 bug:仓库数据里没有 precipitation(演示改用 temperature),而且 quantitative 类型的月份画不出柱;请使用 "ordinal"(如幻灯片所示)或 "temporal"。
{"title": "Slides 61–62 · overriding the color scale, then a monthly mean (inline rows from seattle-weather.json)", "interactive": true, "libs": ["vega-lite"], "html": "<div id=\"vis\"></div>", "js": "// 12 rows from the repo's seattle-weather.json (the demo can't load the file)\nvar weather = [\n {\"date\": \"2015/01/01\", \"weather\": \"sun\", \"temperature\": 1.2},\n {\"date\": \"2015/01/02\", \"weather\": \"fog\", \"temperature\": 2.8},\n {\"date\": \"2015/01/18\", \"weather\": \"rain\", \"temperature\": 10.55},\n {\"date\": \"2015/03/25\", \"weather\": \"fog\", \"temperature\": 10.8},\n {\"date\": \"2015/04/01\", \"weather\": \"rain\", \"temperature\": 9.2},\n {\"date\": \"2015/05/22\", \"weather\": \"sun\", \"temperature\": 14.2},\n {\"date\": \"2015/06/15\", \"weather\": \"drizzle\", \"temperature\": 23.05},\n {\"date\": \"2015/07/08\", \"weather\": \"drizzle\", \"temperature\": 22.2},\n {\"date\": \"2015/07/31\", \"weather\": \"sun\", \"temperature\": 26.1},\n {\"date\": \"2015/08/14\", \"weather\": \"rain\", \"temperature\": 16.65},\n {\"date\": \"2015/08/22\", \"weather\": \"drizzle\", \"temperature\": 19.45},\n {\"date\": \"2015/10/19\", \"weather\": \"fog\", \"temperature\": 14.7}\n];\n\n// slide 61, with scale moved inside color (see the note above)\nvegaEmbed(\"#vis\", {\n data: {values: weather},\n mark: \"bar\",\n encoding: {\n x: {bin: true, field: \"temperature\", type: \"quantitative\"},\n y: {aggregate: \"count\", type: \"quantitative\"},\n color: {\n field: \"weather\", type: \"nominal\",\n scale: {\n domain: [\"sun\",\"fog\",\"drizzle\",\"rain\",\"snow\"],\n range: [\"#e7ba52\",\"#c7c7c7\",\"#aec7e8\",\"#1f77b4\",\"#9467bd\"]\n }\n }\n }\n}, {actions: false});", "height": 260, "tab": "js", "steps": [{"note": "Slide 62's idea on the repo data: timeUnit month on date, y = mean temperature (the repo file has no precipitation field).", "js": "// slide 62 (ordinal month; temperature instead of precipitation)\nvegaEmbed(\"#vis\", {\n data: {values: weather},\n mark: \"bar\",\n encoding: {\n x: {timeUnit: \"month\", field: \"date\", type: \"ordinal\"},\n y: {aggregate: \"mean\", field: \"temperature\", type: \"quantitative\"}\n }\n}, {actions: false});"}]}合理的默认值是 Vega-Lite 的秘诀(幻灯片 67):更低的门槛(规范简洁),同样的天花板(可以覆盖)。缺点:图表看起来很相似(阻力最小的路径,和栅格框架一样)(第 7 节)。
课堂上的 Vega-Lite 演示(幻灯片 63,setup2.js,“Not TypeScript!”)就是演示 2。
12. 课堂问题与演示(附解答)
12.1 问题 1:哪些 TypeScript 文件会转译成这段 JS?(幻灯片 14)
下面哪些 TypeScript 文件会转译成
function pythagorean(a, b) {
var cSq = a * a + b * b;
return Math.sqrt(cSq);
}// 1
function pythagorean(a:number, b:number):number {
var cSq = a*a + b*b;
return Math.sqrt(cSq);
}
// 2
function pythagorean(a:number, b:number) {
var cSq = a*a + b*b;
return Math.sqrt(cSq);
}
// 3
function pythagorean(a, b) {
var cSq = a * a + b * b;
return Math.sqrt(cSq);
}- A 1
- B 2
- C 3
- D 1 和 2
- E 1、2 和 3
✍️ 先自己试一试。
答案:E(幻灯片 16 公布)。
- 类型会被擦除:文件 1 和 2 的
:number都会消失(用 tsc 转译仓库里q1.ts的各个版本可以确认)。 - 文件 3 是纯 JS,而 TS 是超集,所以它转译后还是它自己。
- 陷阱:严格模式会对文件 3 报错(
implicitly has an 'any' type),但仍然会输出同样的 JS。
返回 2. 类型。
12.2 演示 1:给 joinNTimes 加类型(幻灯片 13 · setup1 → solution1)
任务,来自 setup1.ts:
- 用纯 JavaScript 写出这个函数:把字符串数组用“join”字符连接起来,并在每次结果后拼上换行符(
'\n')。 - 给该函数的所有参数和返回值加上类型。
- 这会如何改变哪些函数调用是合法的?
要尝试的三个调用(在起始代码中被注释掉了):
//console.log(joinNTimes(['hello', 'my', 'name', 'is', 'theresa'], ' ', 3));
//console.log(joinNTimes(['in4matx', 133], '-', 2))
//console.log(joinNTimes(['two', 'too', 'to'], 2, 4))✍️ 先自己试一试。
答案。纯 JS(仓库里的 setup1.js):
function joinNTimes(arrOfStr, joinStr, n) {
var joinedStr = '';
for (var i = 0; i < n; i++) {
joinedStr += arrOfStr.join(joinStr) + '\n';
}
return joinedStr;
}没有类型时,三个调用都能运行(幻灯片上的 console 输出):
hello my name is theresa
hello my name is theresa
hello my name is theresa
in4matx-133
in4matx-133
two2too2to
two2too2to
two2too2to
two2too2tojoin 会把 133 强制转换成 "133",并把 2 当作分隔符 "2"。
加上类型后(来自 solution1.ts):
function joinNTimes(arrOfStr:string[], joinStr:string, n:number):string {
var joinedStr:string = '';
for(var i:number=0;i<n;i++) {
joinedStr += arrOfStr.join(joinStr) + '\n';
}
return joinedStr;
}现在只有第一个调用是合法的:
| 调用 | tsc |
|---|---|
joinNTimes(['hello', …, 'theresa'], ' ', 3) |
通过 |
joinNTimes(['in4matx', 133], '-', 2) |
Type 'number' is not assignable to type 'string'.(string[] 里有 133) |
joinNTimes(['two', 'too', 'to'], 2, 4) |
Argument of type 'number' is not assignable to parameter of type 'string'.(joinStr) |
solution1.js 只保留了第一个调用。(仓库里的小怪癖:solution1.ts 整个都在 /* … */ 里。)
🧪 演示文件:setup1.html · setup1.ts · setup1.js · solution1.ts · solution1.js
返回 2. 类型。
12.3 问题 2:Photoshop 与 Paint 的门槛和天花板(幻灯片 38)
相对而言,Adobe Photoshop 和 Microsoft Paint 的门槛和天花板分别如何?(它们是制作图像的工具,但原则同样适用。)
- A Photoshop:高天花板、高门槛;Paint:低天花板、低门槛
- B Photoshop:高天花板、低门槛;Paint:低天花板、高门槛
- C Photoshop:低天花板、高门槛;Paint:高天花板、低门槛
- D Photoshop:低天花板、低门槛;Paint:高天花板、高门槛
- E 我很困惑,只是点一下拿参与分
✍️ 先自己试一试。
答案:A(幻灯片 40 公布)。
- 天花板:Photoshop 能做的事多得多(图层、滤镜、照片编辑),因此排除 C 和 D。
- 门槛:Paint 很容易上手;Photoshop 需要学习(所以它提供教程,幻灯片 41),因此排除 B。
- 两个术语都是相对的。
返回 7. 评估工具。
12.4 演示 2:2000 年美国年龄分布条形图(幻灯片 63 · setup2 → solution2)
任务(setup2.js):画一张美国年龄分布的条形图,年龄在 Y 轴,人数在 X 轴。population.json 的记录(从网上加载;文件夹里也有一份副本):
[{"year":1850,"age":0,"sex":1,"people":1483789},
{"year":1850,"age":0,"sex":2,"people":1450376},
...]起始代码(setup2.html 从 jsDelivr 加载 vega 5.21.0、vega-lite 5.2.0、vega-embed 6.20.2,外加一个 <h1> 和一个空的 <div id="plot">):
document.addEventListener('DOMContentLoaded', function (event) {
var schema = {
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"data": { "url": "https://vega.github.io/editor/data/population.json"},
"transform": [{"filter": "datum.year == 2000"}], /*Only select population data from 2000*/
/*Make a bar chart, encode the X and Y axes*/
}
vegaEmbed('#plot', schema, {actions:false});
});按原样运行会失败:Invalid specification … includes at least one of … "mark", "layer", …。
✍️ 先自己试一试。
答案(solution2.js 用来替换那行注释的部分):
"mark": "bar",
"encoding": {
"y": {
"field": "age", "type": "ordinal"
},
"x": {
"aggregate": "sum", "field": "people", "type": "quantitative",
"axis": {"title": "population"}
}
}mark: "bar",y = age,类型ordinal:每个年龄组一根柱,按顺序排列。- x =
people的sum:把每个年龄的两条记录(sex 1 和 2)加起来。 axis: {"title": "population"}覆盖了默认标题(Sum of people)。transform过滤器只保留 2000 年的行。
结果:19 根柱,年龄 0–90,最长的在 35–40 岁(约 2300 万)。v4 的 $schema 会产生一条无害的警告。
演示:包含 0、10、…、90 岁的 2000 年数据,外加一行会被过滤器丢弃的 1990 年数据:
{"title": "Demo 2 · solution2.js with inline rows (DOMContentLoaded wrapper dropped; the demo runs after the page)", "libs": ["vega-lite"], "html": "<h1>U.S. age distribution in 2000</h1>\n<div id=\"plot\">\n</div>", "js": "// population.json, year 2000 rows for ages 0,10,...,90 (+ one 1990 row the filter drops)\nvar population = [\n {\"year\": 2000, \"age\": 0, \"sex\": 1, \"people\": 9735380},\n {\"year\": 2000, \"age\": 0, \"sex\": 2, \"people\": 9310714},\n {\"year\": 2000, \"age\": 10, \"sex\": 1, \"people\": 10563233},\n {\"year\": 2000, \"age\": 10, \"sex\": 2, \"people\": 10022524},\n {\"year\": 2000, \"age\": 20, \"sex\": 1, \"people\": 9731315},\n {\"year\": 2000, \"age\": 20, \"sex\": 2, \"people\": 9324244},\n {\"year\": 2000, \"age\": 30, \"sex\": 1, \"people\": 10205879},\n {\"year\": 2000, \"age\": 30, \"sex\": 2, \"people\": 10119296},\n {\"year\": 2000, \"age\": 40, \"sex\": 1, \"people\": 11320252},\n {\"year\": 2000, \"age\": 40, \"sex\": 2, \"people\": 11488578},\n {\"year\": 2000, \"age\": 50, \"sex\": 1, \"people\": 8507934},\n {\"year\": 2000, \"age\": 50, \"sex\": 2, \"people\": 8911133},\n {\"year\": 2000, \"age\": 60, \"sex\": 1, \"people\": 5123399},\n {\"year\": 2000, \"age\": 60, \"sex\": 2, \"people\": 5668961},\n {\"year\": 2000, \"age\": 70, \"sex\": 1, \"people\": 3792145},\n {\"year\": 2000, \"age\": 70, \"sex\": 2, \"people\": 5184855},\n {\"year\": 2000, \"age\": 80, \"sex\": 1, \"people\": 1902638},\n {\"year\": 2000, \"age\": 80, \"sex\": 2, \"people\": 3221898},\n {\"year\": 2000, \"age\": 90, \"sex\": 1, \"people\": 336303},\n {\"year\": 2000, \"age\": 90, \"sex\": 2, \"people\": 1064581},\n {\"year\": 1990, \"age\": 0, \"sex\": 1, \"people\": 9307465},\n {\"year\": 1990, \"age\": 0, \"sex\": 2, \"people\": 8894007}\n];\n\nvar schema = {\n \"data\": { \"values\": population },\n \"transform\": [{\"filter\": \"datum.year == 2000\"}], /*Only select population data from 2000*/\n \"mark\": \"bar\",\n \"encoding\": {\n \"y\": {\n \"field\": \"age\", \"type\": \"ordinal\"\n },\n \"x\": {\n \"aggregate\": \"sum\", \"field\": \"people\", \"type\": \"quantitative\",\n \"axis\": {\"title\": \"population\"}\n }\n }\n}\nvegaEmbed('#plot', schema, {actions:false});", "height": 340, "tab": "js"}🧪 演示文件:setup2.html · setup2.js · solution2.html · solution2.js · population.json
返回 11. 合理的默认值。
12.5 问题 3:哪个 Vega-Lite 规范能画出这张散点图?(幻灯片 64)
这张可视化图在 Vega-Lite 中应该怎么写?(假设字段名都是正确的。)图表:空心圆,Horsepower 在 x 轴(0–200+),Miles_per_Gallon 在 y 轴(0–50),按 Origin 着色(Europe 蓝、Japan 橙、USA 红)。
- A
mark: "point", encoding: { x: {field: "MPG", type: "ordinal"}, y: {field: “Horsepower", type: "ordinal"}, color: {field: "Origin", type: "nominal"} } - B
mark: "point", encoding: { x: {field: "Horsepower", type: "quantitative"}, y: {field: "MPG", type: "quantitative"}, color: {field: "Origin", type: "nominal"} } - C
mark: "ticks", encoding: { x: {field: "Horsepower", type: "quantitative"}, y: {field: "MPG", type: "quantitative"}, color: {field: "Origin", type: "nominal"} } - D
mark: "point", encoding: { x: {field: "Horsepower", type: "quantitative"}, y: {field: "MPG", type: "quantitative"}, color: {field: "Origin", type: "ordinal"} } - E
mark: "ticks", encoding: { x: {field: "MPT", type: "ordinal"}, y: {field: “Horsepower", type: "ordinal"}, color: {field: "Origin", type: "nominal"} }
(选项字母以公布答案的幻灯片 66 为准;题目幻灯片上的顺序不同。A 和 E 保留了幻灯片中的弯引号 “。)
✍️ 先自己试一试。
答案:B(幻灯片 66 公布)。
- Mark:空心圆 =
"point";"ticks"不是合法的 mark(C 会报错),因此排除 C 和 E。 - 坐标轴:Horsepower 在 x,MPG 在 y,都是
quantitative;A 和 E 把它们互换并设成了ordinal(E 里还有拼写错误"MPT")。 - 颜色:Origin 是无序的,所以用
nominal(三种色相)。D 的ordinal会给出同一色相的不同深浅:
{"title": "Question 3 · option B (nominal color), then option D (ordinal color)", "libs": ["vega-lite"], "html": "<div id=\"vis\"></div>", "js": "// three made-up rows, just to compare B (nominal) with D (ordinal)\nvar cars = [\n {\"Horsepower\": 130, \"MPG\": 18, \"Origin\": \"USA\"},\n {\"Horsepower\": 95, \"MPG\": 24, \"Origin\": \"Japan\"},\n {\"Horsepower\": 46, \"MPG\": 26, \"Origin\": \"Europe\"}\n];\nfunction show(colorType) {\n vegaEmbed(\"#vis\", {\n data: {values: cars},\n mark: \"point\",\n encoding: {\n x: {field: \"Horsepower\", type: \"quantitative\"},\n y: {field: \"MPG\", type: \"quantitative\"},\n color: {field: \"Origin\", type: colorType}\n }\n }, {actions: false});\n}\nshow(\"nominal\"); // option B", "height": 240, "tab": "js", "steps": [{"note": "Option D: ordinal color turns the three origins into shades of one hue, unlike the slide's chart.", "js": "show(\"ordinal\"); // option D"}]}返回 11. 合理的默认值。
13. 速查与必记的 5 个要点
TypeScript
| 语法 / 命令 | 含义 |
|---|---|
npm install -g typescript |
安装 tsc |
tsc hello.ts |
写出 hello.js(类型被去掉) |
tsc --project tsconfig.json |
按配置文件构建项目 |
tsc --declaration test.ts |
同时写出 test.d.ts(只含类型) |
npm install --save @types/lib |
某个 JS 库的类型声明 |
var x:number = 133; |
带标注的变量;var x = 133; 推断为 number |
number, boolean, string, void, null, undefined, any |
预定义类型 |
function f(a: string, b: number): string |
参数类型与返回类型 |
string[] |
字符串数组 |
x: number | number[] |
联合类型 |
f(x?: number) / f(x = 10) |
可选参数 / 默认参数 |
interface Shape { color?: string; } |
对象的形状;? = 可选属性 |
class B extends A { constructor(){ super(...) } } |
继承;super.m() 调用基类方法 |
private, get / set |
隐藏字段;访问器属性 |
function identity<T>(arg: T): T |
泛型函数 |
工具评估
| 术语 | 含义 |
|---|---|
| 门槛(Threshold) | 上手有多难(希望低) |
| 天花板(Ceiling) | 能做到多少事(希望高) |
| 阻力最小的路径(Path of least resistance) | 工具塑造了人们做出的东西(所有 Bootstrap / Vega-Lite 的产出看起来都很像) |
| 移动靶(Moving targets) | 需求变化会让工具过时 |
Vega-Lite
| 部分 | 例子 |
|---|---|
| data | data: {url: "file.json"} 或 data: {values: [...]} |
| mark | "bar", "tick", "point", "line", "area", "rule", "text", "arc" |
| 字段类型 | quantitative, temporal, ordinal, nominal |
| bin | x: {bin: true, field: "temperature", type: "quantitative"} |
| aggregate | y: {aggregate: "count", type: "quantitative"};还有 "sum"、"mean" |
| color | color: {field: "weather", type: "nominal"} |
| 自定义 scale | color: {…, scale: {domain: [...], range: [...]}}(写在通道内部) |
| time | x: {timeUnit: "month", field: "date", type: "ordinal"} |
| filter | transform: [{"filter": "datum.year == 2000"}] |
| 画出来 | vegaEmbed('#plot', spec, {actions: false}) |
必记的 5 个要点
- TS 的类型是先检查,再擦除:有类型和没类型的代码输出同样的 JS,即使有类型错误也一样。
- 类型是可选的:
var x = 133会被推断为number;参数类型决定哪些调用是合法的。 - 门槛 = 上手有多难,天花板 = 能做到多少事;希望门槛低、天花板高;两者都是相对的(Photoshop 高/高,Paint 低/低)。
- 声明式 = 做什么(HTML、CSS、Vega-Lite);命令式 = 一步步的怎么做(JS)。Vega-Lite 的合理默认值降低了门槛;可以覆盖则保住了天花板。
- Vega-Lite 规范 = data + mark + encoding,并为字段选对类型;直方图 =
bar、分箱的 x、count的 y。