熊掌号如何做网站,有没有免费建网站,中国建筑网信息查询,手机开发者模式在哪里找当前内容所在位置#xff08;可进入专栏查看其他译好的章节内容#xff09; 第一部分 D3.js 基础知识 第一章 D3.js 简介#xff08;已完结#xff09; 1.1 何为 D3.js#xff1f;1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践#xff08;上#xff09;1.3 数据可… 当前内容所在位置可进入专栏查看其他译好的章节内容 第一部分 D3.js 基础知识 第一章 D3.js 简介已完结 1.1 何为 D3.js1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践上1.3 数据可视化最佳实践下1.4 本章小结 第二章 DOM 的操作方法已完结 2.1 第一个 D3 可视化图表2.2 环境准备2.3 用 D3 选中页面元素2.4 向选择集添加元素2.5 用 D3 设置与修改元素属性2.6 用 D3 设置与修改元素样式2.7 本章小结 第三章 数据的处理 ✔️ 3.1 理解数据已完结3.2 准备数据已完结3.3 将数据绑定到 DOM 元素已完结 3.3.1 利用数据给 DOM 属性动态赋值 3.4 让数据适应屏幕已完结 3.4.1 比例尺简介上篇3.4.2 线性比例尺中篇 3.4.2.1 基于 Mocha 测试 D3 线性比例尺DIY 实战 3.4.3 分段比例尺下篇 3.4.3.1 使用 Observable 在线绘制 D3 条形图DIY 实战 3.5 加注图表标签上篇 3.5.1 人物专访Krisztina Szűcs下篇3.5.2 DIY实战在 Observable 平台实战演练并进行单元测试 ✔️ 3.6 本章小结 文章目录 3.5.2 DIY实战在 Observable 实现带数据标签的 D3 条形图并改造单元测试模块1 起因2 经过2.1 完成条形图剩余部分——绘制数据标签2.2 用 AI 提示重构单元测试模块2.3 集成 Chai.js 的 expect 断言2.4 导出定制的 MyMocha 类及相关断言方法 3 小结 《D3.js in Action》全新第三版封面 前言 本篇不是书中的内容只是昨晚看了自己翻译的那篇给匈牙利设计师 Krisztina Szűcs 做的人物专访一时兴起在 Observable 平台重新实现了一版第 3 章的条形图顺便把上回遇到的单元测试问题一并解决了。建议大家也多动动手到 Observable 从头开始敲一遍代码巩固所学。 3.5.2 DIY实战在 Observable 实现带数据标签的 D3 条形图并改造单元测试模块
1 起因
学完了第三章我也在本地实测了一遍效果还不错。于是就想着同步更新一下放到 Observable 上的版本。没曾想竟然在单元测试模块卡住了Observable 居然不支持 Mocha.js 这样的测试框架无法使用全局的 describe 和 it 方法来写测试套件除了支持 Chai.js 断言库的 CDN 引入其余效果都得自己封装。网上倒是有几个现成的案例但要么过于简单只是粗略对断言模块 expect 方法的封装 1 【图 1 对 Jest 的 expect 断言做简单封装的效果图】
要么又过于复杂 2 【图 2 同样基于 Jest 的 expect 断言实现的一套定制测试框架】
而我只希望能用上 describe 和 it最后将单元测试写到一个测试套件suite里大致长这样 【图 3 希望通过组合 describe 和 it 方法实现的单元测试效果】
没办法Observable 这方面还不成熟还得自力更生。
2 经过
2.1 完成条形图剩余部分——绘制数据标签
参考上一节做好的版本详见我的《3.4 小节 DIY 实战使用 Observable 在线绘制 D3 条形图》先把带标签的 D3 条形图画出来。
和上次一样先上传 data.csv 原始数据集然后转成 Observable 可以使用的对象数组
data {const csv await FileAttachment(data.csv).csv({typed: true});return csv.sort((a, b) d3.descending(a.count, b.count));
}接着定义两个方向上的比例尺放到一个 JavaScript 对象里备用
scales {const x d3.scaleLinear().domain([0, d3.max(data, (d) d.count)]).range([0, 450]);const y d3.scaleBand().domain(data.map((d) d.technology)).range([0, 700]).paddingInner(0.2);return { x, y };
}然后就可以绘制条形图了定义一个图表变量 chart
chart {const svg d3.create(svg).attr(viewBox, 0 0 600 700).attr(width, 100%)// .style(border, 1px solid black);const groups svg.selectAll(g).data(data).join(g).attr(transform, (d) translate(0, ${scales.y(d.technology)}));// append rectsappendRect(groups);// append tech name labelsappendTechNameLabels(groups);// append count labelsappendCountLabels(groups);// data binding partappendAxisLine(svg);return svg.node();
}由于要加注两组标签要用到 SVG 的分组元素g这里需要现将数据绑定到每个 g 元素上如第 8 行所示。然后用 groups 选择集分别完成矩形条、名称标签以及数据标签的绑定与绘制。为了方便查看我把它们都提到了单独的单元格来处理践行“单一职责”原则。
先是技术名称标签。我再原书内容的基础上把 D3.js 对应的得票数也设置了一些样式加粗、变色、调整字号
function appendCountLabels(groups) {// Define predicatesconst target D3.js;const fontSizeHightD3 ({technology: t}) (t target) ? 9px : 8px;const fontWeightByTechName ({technology: t}) (t target) ? 700 : 400;const fillColorByTechName ({technology: t}) (t target) ? yellowgreen : #000;// Append labelsgroups.append(text).attr(x, d 100 scales.x(d.count) 4).attr(y, 12).text(d d.count).style(font-family, sans-serif).style(font-weight, fontWeightByTechName).style(font-size, fontSizeHightD3).style(fill, fillColorByTechName);
}效果还不赖 【图 4 升级版的 D3 数据标签效果】
接着绘制纵轴标签对应各技术名称
function appendTechNameLabels(groups) {groups.append(text).attr(x, 96).attr(y, 12).attr(text-anchor, end).text(d d.technology).style(font-family, sans-serif).style(font-size, 10px);
}然后是矩形条
function appendRect(groups) {const byTechName ({technology: t}) t D3.js ? yellowgreen : skyblue;groups.append(rect).attr(x, 100).attr(y, 0).attr(height, scales.y.bandwidth()).attr(width, d scales.x(d.count)).attr(fill, byTechName);
}最后是纵轴的那条直线
function appendAxisLine(svg) {svg.append(line).attr(x1, 100).attr(y1, 0).attr(x2, 100).attr(y2, 700).attr(stroke, black);
}然后 Shift Enter 一键出图 【图 5 最终在 Observable 平台绘制的加注了图表标签的 D3 条形图效果】
2.2 用 AI 提示重构单元测试模块
接下来才是本篇的重头戏——自己封装一套 describe 方法和 it 方法。还好 Observable 支持断言库 Chai.js 的导入可能在 Mike Bostock 大神看来只要把断言结果放到单元格里就行了干嘛要写成 describe 嵌套 it 的结构呢对于想用 JS 的循环结构来写测试的码畜的想法大神可能无暇顾及
// 这是我精心构建的测试数据多么优雅~我居然还会用 Map
testData new Map([[198, 83],[414, 173],[852, 256], // backup: 852 - 356[1078, 450]
]);本来【图1】是出不来效果的因为 it_old 方法最初的定义是这样的
/*** Test helper to display test title into the notebook*/
function it_old(title, testFunction) {try {testFunction.call(this);return htmldiv stylecolor:green; ✓ : ${title || Test passing }/div;} catch (err) {return htmldiv stylecolor:red; × : ${err.message}/div;}
}如果不逐个返回运行的结果就会乱套
invalidResults {it_old(test1, () expect(2).to.be.lessThan(1));it_old(test2, () expect(5).to.be.lessThan(1));it_old(test3, () expect(10).to.be.lessThan(1));it_old(test4, () expect(100).to.be.lessThan(1));
}运行单元格后看不到任何报错 【图 6 无法将测试结果正确显示到页面旧版 it 方法】
这么一来我要封装的 it 方法和 describe 方法必须自动收集这样的断言结果才行而且还得在后台完成不然太 low与我的码畜风格相悖。于是我想到了 ES6 引入的 class 语法糖先把 describe 和 it 定义的回调函数收集到类的一个成员数组运行的时候再用 this 去挨个遍历它们结果放到另一个数组最后用统一的渲染函数交卷不就搞定了吗
想法成形下一步就让机智的 AI 帮我出个 0.1 版吧。果然不抱太大希望的情况下往往有惊喜居然帮我把 beforeHooks 和 afterHooks 都实现了先不论对错这么端正的态度就值得表扬
class TestSuite {constructor(name) {this.name name;this.tests [];this.beforeHooks [];this.afterHooks [];}describe(name, fn) {const suite new TestSuite(name);fn.call(suite);this.tests.push(suite);}it(name, fn) {this.tests.push({ name, fn });}before(fn) {this.beforeHooks.push(fn);}after(fn) {this.afterHooks.push(fn);}async run() {console.log(Running suite: ${this.name});// Run before hooksfor (const hook of this.beforeHooks) {await hook();}for (const test of this.tests) {if (typeof test.fn function) {try {await test.fn();console.log(✔️ ${test.name});} catch (error) {console.error(❌ ${test.name});console.error(error);}} else {// Recursively run nested suitesawait test.run();}}// Run after hooksfor (const hook of this.afterHooks) {await hook();}}
}// 使用示例
const suite new TestSuite(My Test Suite);suite.describe(Array, function() {this.before(() {console.log(Setting up before tests...);});this.after(() {console.log(Cleaning up after tests...);});this.it(should add items, async () {const arr [];arr.push(1);if (arr.length ! 1) throw new Error(Test failed);});this.it(should remove items, async () {const arr [1];arr.pop();if (arr.length 0) throw new Error(Test failed);});
});suite.run();直接放到 Observable 单元格运行虽然有很多小问题但总算还像那么回事 【图 7 根据 AI 提示词生成的制定代码效果截图】
2.3 集成 Chai.js 的 expect 断言
AI 版本过于粗糙需要调整几个地方
控制台输出需要改为页面显示各单元测试结果需要分别收集起来测试套件和用例描述也得放到结果里统一整体输出样式颜色、缩进等。
逐一解决这些小瑕疵于是就有了 v1.0 版的测试类 MyMocha
// Define customized Mocha class
class MyMocha {constructor(name) {this.name name;this.tests [];this.results [mddiv stylefont-weight: 700; ${name}/div];this.beforeHooks [];this.afterHooks [];}describe(name, fn) {const suite new MyMocha(name);fn.call(suite);this.tests.push(suite);this.results.push(mddiv stylefont-weight: 700; text-indent: 1em;⏳ i${name}/i/div);}it(name, fn) {this.tests.push({ name, fn });}before(fn) {this.beforeHooks.push(fn);}after(fn) {this.afterHooks.push(fn);}// show the results altogether in markdown formatasync showResults() {await this.run();return md${this.results};}isFunction(fn) {return typeof fn function;}async run() {console.log(Running suite: ${this.name});// Run before hooksfor (const hook of this.beforeHooks) {await hook();}for (const test of this.tests) {if (this.isFunction(test.fn)) {try {await test.fn();this.results.push(htmldiv stylecolor: green; text-indent: 2em;✔️ ${test.name}/div);} catch (error) {this.results.push(htmldiv stylecolor:red; text-indent: 2em;❌ ${test.name}/div);this.results.push(htmldiv stylecolor: red; text-indent: 3em;${error.message}/div);}} else {// Recursively run nested suitesawait test.run();}}// Run after hooksfor (const hook of this.afterHooks) {await hook();}}
}然后把 Chai.js 导入再把 expect 断言提出来
chai import(https://unpkg.com/chai/chai.js);
expect chai.expect.bind(chai);写个测试看看
suite {const testData new Map([[198, 83],[414, 173],[852, 256], // backup: 852 - 356[1078, 450]]);const suite new MyMocha(DIY mocha test:);const it suite.it.bind(suite);const describe suite.describe.bind(suite);describe(Testing horizontal scale for my bar chart:, () {testData.forEach((expected, domain) {it(Pass the value ${domain} to the xScale() function, should return ${expected}., () {const actual scales.x(domain);const diff Math.abs(actual - expected);expect(diff, [Diff Exceeded]).to.be.lessThan(1);});});});return suite.showResults();
}效果还行 【图 8 集成了 Chai.js 的 expect 断言后的测试用例运行结果】
2.4 导出定制的 MyMocha 类及相关断言方法
既然都测试通过了就可以考虑放到一个新的 Notebook 里供其它记事本导入了。咱也模仿一下其他网友的套路搞个标题和用法示例 【图 9 拟用于导出 MyMocha 类和 expect 断言的通用 Notebook 页面】
然后将该页面设置为公开访问并根据 Observable 的官方文档用规定的导入语法再写一版测试 【图 10 将 Notebook 记事本页面设置为公开访问】 【图 11 从页面右侧边栏的官方文档找到导入其他记事本单元格的写法】
按照官方文档导入要这么写
import { MyMocha, expect } from anton-playground/combined-unit-tests再测一遍结果发现一个 Bug渲染完成后没有及时清空本次测试结果导致重复运行后上次的结果也在里面。于是切回公共页面改改渲染函数的逻辑勉强算是 v1.1 版吧
// show the results altogether in markdown format
async showResults() {await this.run();const results md${this.results};this.tests this.tests.filter((t) !this.isFunction(t.fn));this.results [];return results;
}再测大功告成 【图 12 最终通过导入公共记事本的自定义方法实现的测试套件的实际效果】
3 小结
虽然成功模拟了 Mocha.js 里的 describe 和 it 原语但毕竟逻辑过于简单稍微上点有难度的测试就不够用了而且写法上也没有 Mocha.js 那么自然对于锚定的几个 hooks 钩子也无暇验证。这个 Notebook 就算抛砖引玉吧以后对 TDD 和 BDD 了解得更深入了再来升级。
两个记事本页面我都共享出来方便大家学习交流可以 Fork 到自己的工作空间Workspace进行修改
定制的 MyMocha 测试类https://observablehq.com/anton-playground/combined-unit-tests加注标签的条形图并通过线性比例尺单元测试的示例页https://observablehq.com/anton-playground/my-bar-chart-with-chaijs 搜到一篇对 Jest 的 expect 方法的轻量级封装案例详见Spencer: Unit testing inside a notebook ↩︎ 详见Tom Larkworthy: Reactive Unit Testing and Reporting Framework ↩︎