可视化我的睡眠日程

Lobsters Hottest 新闻

摘要

一个使用来自Discord和Twitter的带时间戳在线活动来可视化睡眠障碍的个人项目,使用Deno和TypeScript实现。

<p><a href="https://lobste.rs/s/20c9v3/graphing_my_sleep_schedule">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/22 12:37

# 绘制我的睡眠时间表 - 夏洛特! 来源: https://char.lt/blog/2026/09/non24swd 嗨!我有一种睡眠障碍(https://en.wikipedia.org/wiki/Non-24-hour_sleep%E2%80%93wake_disorder)^\-^ 我当时想:“我应该把这个画出来”,但因为我感官上的原因无法忍受手表戴在手腕上的感觉,所以没有智能手表之类能直接绘制睡眠数据的设备。于是我转而利用能找到的尽可能多的带时间戳的在线活动数据,将其“负空间”作为睡眠数据来绘制。这效果相当不错,因为我也不怎么出门 :3 **深入我的超级自窥镜** 我们首先需要收集大量日期数据,然后绘制一张SVG图。对于这类一次性脚本,我喜欢用Deno,因为可以*直接*写TypeScript而完全不用设置项目,甚至能直接从jsr和npm引入依赖 :) ## 数据抓取 我们需要读取压缩文件,因此要在Deno中连接zip\.js: ``` import { ZipReader, Reader, TextWriter } from "npm:@zip.js/zip.js" // 为zip.js适配Deno API class DenoFileReader extends Reader { constructor(private file: Deno.FsFile) { super(file); this.size = file.statSync().size; // 阻塞式I/O;无所谓 >:) } override async readUint8Array(offset: number, length: number): Promise<Uint8Array> { this.file.seekSync(offset, Deno.SeekMode.Start); const bytes = new Uint8Array(length); let read = 0; while (read < length) { const n = await this.file.read(bytes.subarray(read)); if (n === null) throw new Error("unexpected EOF"); read += n; } return bytes; } } ``` ### Discord Discord数据是我目前最丰富的数据源:由于它是我与大多数人主要的沟通方式,我几乎全程都在线。申请数据包并等待几天后,我会得到一个结构如下的压缩文件: ``` Account/ Ads/ [...] Messages/ # 许多频道ID: c{channel_id}/ channel.json messages.json ``` 每个`messages.json`都是一个顶层JSON数组,包含元素`{ ID: string, Timestamp: string, Contents: string, Attachments: string }`。时间戳是不带时区的`YYYY-MM-DD HH:mm:ss`格式字符串,但通过与应用界面的时间交叉核对,我们可以安全地将其视为UTC时间。 ``` type DiscordMessage = { Timestamp: string; }; async function extractDiscord(zip: ZipReader, timestamps: number[]) { for (const entry of await zip.getEntries()) { if (entry.directory) continue; if (!entry.filename.endsWith("/messages.json")) continue; const text = await entry.getData(new TextWriter()); const messages: DiscordMessage[] = JSON.parse(text); for (const message of messages) { const isoTimestamp = message.Timestamp.replace(" ", "T") + "Z"; timestamps.push(Date.parse(isoTimestamp)); } } } const timestamps: number[] = []; using discordZip = await Deno.open("./discord-2026-09-19.zip"); await extractDiscord(new ZipReader(new DenoFileReader(discordZip)), timestamps); ``` ### Twitter Twitter的GDPR导出有点奇怪。我们有一个`data/tweet-headers.js`文件,好像是JSONP格式?我猜我们只需截取字符串去掉前缀`window.YTD.tweet_headers.part0 =`即可。 ``` import { expandGlob } from "jsr:@std/fs" type TwitterTweet = { tweet: { created_at: string; }; }; async function extractTwitter(zip: ZipReader, timestamps: number[]) { for (const entry of await zip.getEntries()) { if (entry.directory) continue; if (!entry.filename.endsWith("/tweet-headers.js")) continue; const text = await entry.getData(new TextWriter()); const tweets: TwitterTweet[] = JSON.parse(text.slice(text.indexOf("=") + 1)); for (const { tweet } of tweets) { timestamps.push(Date.parse(tweet.created_at)); } } } for await (const archive of expandGlob("twitter-*.zip")) { using file = await Deno.open(archive.path); await extractTwitter(new ZipReader(new DenoFileReader(file)), timestamps); } ``` ### AT Protocol (atproto) 我喜爱AT Protocol ^-^ 所有关心的记录都有一个TID rkey,这实际上是一个(可排序的)Base32编码的时间戳。我只需获取我的CAR文件,用`@atcute/repo`(https://npmx.dev/package/@atcute/repo)解析它们,然后使用`@atcute/tid`(https://npmx.dev/package/@atcute/tid)解析时间戳 - 感谢mary <3 `@atcute/repo`封装了CAR读取器,并为我们提供每条记录的rkey。我们将跳过非TID键(如`self`),并将解码后的时间戳除以1,000,000将微秒转换为秒。 ``` import { fromUint8Array } from "npm:@atcute/repo" import * as TID from "npm:@atcute/tid" for await (const archive of expandGlob("*.car")) { const repo = fromUint8Array(await Deno.readFile(archive.path)); for (const { rkey } of repo) { if (!TID.validate(rkey)) continue; timestamps.push(TID.parse(rkey).timestamp / 1000); } } ``` ### ActivityPub 我以前待过的大多数Mastodon实例现在都已失效,但我还有我的个人自托管Akkoma实例!我将直接从PostgreSQL数据库中提取我的活动时间戳 :) ``` sudo -u pleroma psql -XAt > akkoma-timestamps.txt <<'SQL' SELECT extract(epoch FROM a.inserted_at AT TIME ZONE 'UTC') FROM activities a JOIN users u ON u.ap_id = a.actor WHERE a.local AND u.local AND u.nickname IN ('bun', 'charlotte') AND a.data->>'type' IN ('Create', 'Like') ORDER BY a.inserted_at; SQL ``` 并将此文件也导入到TypeScript中: ``` const akkomaText = await Deno.readTextFile("./akkoma-timestamps.txt"); for (const line of akkomaText.split(/\r?\n/)) { if (!line.trim()) continue; timestamps.push(Number(line) * 1000); } ``` ## 渲染图表 我们将从提取脚本中导出`timestamps`,并编写另一个脚本来输出活动图(actogram)的SVG!我们将使用2021年1月作为截止点,因为在此之前的可靠数据较少(已删除的Discord服务器、不存在的Twitter账户等)。此外,由于大量数据跨越UTC午夜,我们将采用一点视觉冗余,时间轴延伸到30时(https://en.wikipedia.org/wiki/Date_and_time_notation_in_Japan#Times_past_midnight):D 我们不会为图表中的每个时间点单独生成一个``元素(这将创建数十万个SVG对象!),而是按显示内容进行批量处理——我们利用一个``元素可以包含多个不相连子路径的特性来保持元素数量低:我们为每种活动强度类型(包括睡眠间隔)创建一个``对象,然后使用许多断开的`M...z`矩形作为`d`属性。耶! 如果您没有启用JavaScript,可以查看这个[静态SVG](https://char.lt/blobs/actogram-202609.svg)(警告!3MB)。或者,请看看这个交互式视图: 或者绘制您自己的活动图(需要一个`number[]`格式的JSON输入):

相似文章

我让 AI 帮我构建了一个工具,用来找出夜里吵醒我的原因

Hacker News Top

作者详细介绍了如何使用 AI 编程助手构建一个定制的智能家居工具,该工具通过关联音频录音、传感器数据和睡眠指标来识别夜间干扰源。这个 DIY 项目将树莓派、麦克风、Home Assistant 和 Garmin 手表整合到一个个性化的网页仪表板中。