简介:seisplotjs 是一套面向地震学数据处理与可视化开发者的 JavaScript 模块库,适合具备一定前端基础、需要解析、处理和绘制地震数据的科研人员与工程师使用。它通过多个子模块覆盖完整工作流:数据链路协议对接、日期时间选择控件、经纬度距离计算,以及基于 FDSN Web 服务的地震图、地震事件、台站与通道查询,还包含 OregonDSP 离散傅立叶变换与 d3 频谱绘制能力。资源包共 184 个文件,以 83 个 js 源码、60 个 html 示例页和 14 个 css 样式为主,另含 sac、mseed、staxml 等地震数据样本及少量 Python 脚本与配置,压缩包约 1.02MB,目录结构便于按模块查阅。目前已有 319 人学习下载。借助这些源码与示例,读者可快速理解各子模块的调用方式,掌握从数据请求、格式解析到波形与频谱绘制的实现思路,并直接复用其中的工具函数与页面结构,降低地震数据可视化项目的搭建成本。
1. 地震波形上浏览器:seisplotjs 到底解决了谁的痛点
如果你手头有一批 MiniSEED 或 SAC 格式的地震波形,想把它变成网页上能缩放、能测震相到时的交互图,传统做法是后端渲染成图片再塞进<img>。这条路能跑通,但代价是每次改时间窗、换台站、调滤波参数都得重新请求一次服务端,交互体验基本为零。seisplotjs 就是冲着这个场景来的:它是一组纯 JavaScript 模块,把地震数据的解析、处理和绘制全部搬到浏览器端完成,不需要后端参与渲染。
它适合三类人:做地震监测台网 Web 展示的前端工程师、需要把波形嵌进科研网页的数据处理人员、以及想用 JavaScript 快速验证波形处理算法的人。核心能力覆盖 SEED/MiniSEED 解析、SAC 读取、台站元数据(StationXML)解析、波形滤波与重采样、以及基于 Canvas 或 SVG 的交互式绘制。换句话说,从拿到原始数据文件到屏幕上出现可交互波形图,中间这条链路它基本包圆了。
2. 模块拆解与数据流:从 MiniSEED 字节流到可绘制对象
2.1 核心模块的职责边界
seisplotjs 不是一个单体库,而是一组按职责拆分的模块。常见的几个是:seisplotjs-seed负责 MiniSEED 和 Dataless SEED 的解析,seisplotjs-sac处理 SAC 格式,seisplotjs-stationxml解析台站元数据,seisplotjs-waveformplot提供绘制能力,seisplotjs-filter做滤波和重采样。这种拆分的好处是你不需要全量引入,比如只做波形展示不碰 SAC,就只装 seed、stationxml 和 waveformplot 三个包。
数据流是这样的:原始文件先经过对应解析器变成Seismogram对象,这个对象里封装了时间序列数据、采样率、起始时间、台站通道标识。然后Seismogram可以经过滤波、重采样、去均值等处理,最后交给WaveformPlot绑定到 DOM 容器上完成绘制。理解这条链路很重要,因为后面所有踩坑几乎都出在「解析出来的对象和绘制期望的对象对不上」这个环节。
2.2 环境搭建与最小可运行示例
先建项目、装依赖。seisplotjs 的包发布在 npm 上,用常见的包管理器即可。
mkdir seisplot-demo && cd seisplot-demo npm init -y npm install seisplotjs-seed seisplotjs-stationxml seisplotjs-waveformplot如果你用原生 ES Module 方式在浏览器里直接跑,也可以走 CDN 引入,但生产环境建议走打包工具,因为模块之间有依赖关系,手动管理容易漏。
下面是一个最小可运行示例:读取一个本地 MiniSEED 文件,解析后绘制到页面上。
import { parseMiniSeed } from 'seisplotjs-seed'; import { WaveformPlot } from 'seisplotjs-waveformplot'; // 假设页面上有一个 id 为 "plot" 的 div 容器 const container = document.getElementById('plot'); async function loadAndPlot(file) { // 1. 读取文件为 ArrayBuffer const buffer = await file.arrayBuffer(); // 2. 解析 MiniSEED,返回 Seismogram 数组 // 一个 MiniSEED 文件可能包含多个台站通道的数据段 const seismograms = parseMiniSeed(buffer); if (seismograms.length === 0) { console.error('解析结果为空,检查文件是否为合法 MiniSEED'); return; } // 3. 创建绘图实例,绑定容器 const plot = new WaveformPlot(container); // 4. 把解析出的所有 Seismogram 加入绘图 // append 方法接受单个或数组,内部会按时间轴对齐 plot.append(seismograms); // 5. 触发渲染 plot.draw(); } // 绑定文件输入控件 document.getElementById('fileInput').addEventListener('change', (e) => { const file = e.target.files[0]; if (file) loadAndPlot(file); });这段代码的逻辑说明:parseMiniSeed接收 ArrayBuffer 而不是文件路径,因为浏览器环境没有文件系统直接访问权限,必须先把文件读成二进制。返回的seismograms是数组,因为一个 MiniSEED 文件里可能打包了多个通道的数据,比如三分量台站就会返回三条记录。WaveformPlot的append方法会自动处理时间轴对齐,如果各通道起始时间不一致,它会按各自的时间范围摆放。draw是显式调用,意味着你可以先 append 多个数据源再统一渲染,避免多次重绘。
参数方面,parseMiniSeed第二个参数可以传一个配置对象,常见的是{ dataRecordOnly: true },表示只解析数据记录、跳过质量标识等元信息,能略微提速。WaveformPlot构造函数第二个参数可以指定宽度、高度、时间轴格式等,不传就用容器尺寸和默认格式。
2.3 StationXML 的配合使用
光有波形数据还不够,图上要显示台站名、通道方向、仪器响应信息,就得靠 StationXML。常见做法是先从 IRIS 或本地台网拿到 StationXML 文件,解析后把台站信息关联到 Seismogram 上。
import { parseStationXML } from 'seisplotjs-stationxml'; async function attachStationInfo(seismograms, stationXmlFile) { const xmlText = await stationXmlFile.text(); const inventory = parseStationXML(xmlText); // 为每个 Seismogram 查找匹配的台站通道 seismograms.forEach(seis => { const network = inventory.networks.find(n => n.code === seis.networkCode); if (!network) return; const station = network.stations.find(s => s.code === seis.stationCode); if (!station) return; const channel = station.channels.find(c => c.code === seis.channelCode); if (channel) { // 把台站经纬度、仪器响应等挂到 Seismogram 上 seis.stationInfo = channel; } }); return seismograms; }这里的关键是匹配逻辑:MiniSEED 里的台网代码、台站代码、通道代码要和 StationXML 里的层级结构一一对应。常见翻车点是通道代码带方位角后缀(比如 BHZ 和 BH1、BH2 的区分),如果 StationXML 里写的是完整方位角而 MiniSEED 里只有通道前缀,匹配就会失败。稳妥做法是先按 network + station + channel 前缀匹配,再根据起始时间做二次筛选。
3. 滤波、重采样与交互绘制的参数怎么设
3.1 滤波器的选型与参数含义
地震波形常见的处理需求是带通滤波,比如去掉长周期漂移和高频噪声。seisplotjs 的滤波模块提供了 Butterworth 等常见 IIR 滤波器。
import { filterSeismogram } from 'seisplotjs-filter'; // 对单个 Seismogram 做 1-10 Hz 带通滤波 const filtered = filterSeismogram(seisogram, { type: 'bandpass', lowCut: 1.0, // 低频截止,单位 Hz highCut: 10.0, // 高频截止,单位 Hz order: 4, // 滤波器阶数 zeroPhase: true // 是否零相位滤波 });参数说明:lowCut和highCut必须小于奈奎斯特频率,也就是采样率的一半。如果原始采样率是 20 Hz,奈奎斯特是 10 Hz,那highCut设 10 就正好卡在边界上,实际会出问题,一般留 10% 余量设 9 以下。order越高过渡带越陡,但相位非线性也越严重,4 阶是常用折中。zeroPhase: true会做前后向两次滤波来消除相位偏移,代价是计算量翻倍,实时场景慎用。
3.2 重采样与时间轴对齐
不同台站的采样率可能不同,要在同一张图上对比就得统一采样率。重采样用resampleSeismogram,底层是插值。
import { resampleSeismogram } from 'seisplotjs-filter'; // 把采样率统一到 20 Hz const resampled = resampleSeismogram(seisogram, 20);重采样有个容易被忽略的坑:如果目标采样率高于原始采样率,插值出来的数据是「假」的高频,看起来平滑但实际没有新信息。所以重采样一般只做降采样,升采样只在时间轴对齐需要时才用,且要在图上标注原始采样率。
3.3 交互绘制的配置项
WaveformPlot的交互能力包括滚轮缩放、拖拽平移、点击测震相到时。这些行为通过配置对象控制。
const plot = new WaveformPlot(container, { width: 900, height: 400, timeAxis: { format: 'HH:mm:ss', // 时间轴显示格式 tickInterval: 60 // 刻度间隔,单位秒 }, zoom: { enabled: true, minScale: 1, // 最小缩放倍数 maxScale: 100 // 最大缩放倍数 }, cursor: { crosshair: true, // 是否显示十字准线 readout: true // 是否显示时间读数 } });tickInterval要根据总时间跨度来调:如果展示的是 24 小时数据,刻度间隔设 60 秒会密到看不清,应该设 3600;如果是 10 分钟数据,设 60 就合适。maxScale设太大没有意义,因为数据点就那么多,放大到超过采样间隔后就是纯插值,看不到新细节。
4. 避坑与排查:那些让我加班到凌晨的报错
4.1 解析结果为空但文件明明有内容
现象:parseMiniSeed返回空数组,控制台没有报错。原因通常是文件不是标准 MiniSEED,而是 SEED 卷格式或者被压缩过(比如 .mseed.gz 没解压)。MiniSEED 有固定的 512 字节记录头,如果文件头不对,解析器会静默跳过。解决办法是先确认文件头,用xxd file.mseed | head看前几个字节,标准 MiniSEED 记录头里会有可读的台站代码。如果是 gzip 压缩的,先解压再喂给解析器。
4.2 波形画出来是一条直线
现象:图渲染出来了,但波形是平的。原因通常是数据值范围极小(比如仪器响应没去掉,原始 counts 值在 10^-6 量级),而绘图默认的 Y 轴范围是按整数设的。解决办法是在 append 之前先做去均值或归一化,或者手动设置 Y 轴范围。另一个可能是解析出来的数据段长度为零,检查 MiniSEED 记录里的样本数是否被正确读取。
4.3 时间轴对不上,各通道错位
现象:三分量波形在图上左右错开,起始时间不一致。原因通常是 MiniSEED 记录头里的起始时间字段解析有误,或者各通道的起始时间确实不同但绘图没有按统一时间轴对齐。解决办法是检查每个 Seismogram 的startTime属性,确认解析是否正确。如果数据本身起始时间就不同,需要在 append 之前手动对齐,或者用plot.setTimeRange(start, end)强制指定显示范围。
4.4 滤波后波形首尾出现异常震荡
现象:带通滤波后,数据段开头和结尾出现大幅震荡。原因是滤波器的边缘效应,IIR 滤波器在数据边界处没有足够的历史样本。解决办法是滤波前先对数据做两端延拓(padding),或者只取滤波后中间段使用。seisplotjs 的滤波函数有个taper选项,可以在两端加余弦窗来抑制边缘效应,代价是两端数据不可用。
4.5 大数据量下页面卡死
现象:加载一个包含几十万采样点的 MiniSEED 后,浏览器无响应。原因是 Canvas 逐点绘制在数据量大时性能急剧下降。解决办法是先用resampleSeismogram降采样到屏幕像素级别,比如屏幕宽度 900 像素,那采样率降到每秒 2-3 个点就够画了。另外可以开启WaveformPlot的decimate选项,让它在绘制时自动抽稀。
5. 进阶技巧:把 seisplotjs 嵌进现有前端框架与性能验证
5.1 在 React/Vue 中封装波形组件
seisplotjs 本身不绑定任何框架,但直接塞进 React 的useEffect里容易出问题,因为WaveformPlot会直接操作 DOM。稳妥做法是封装一个受控组件,把容器 ref 传进去,在useEffect里初始化和销毁。
import { useEffect, useRef } from 'react'; import { WaveformPlot } from 'seisplotjs-waveformplot'; function SeisPlot({ seismograms, width = 900, height = 400 }) { const containerRef = useRef(null); const plotRef = useRef(null); useEffect(() => { if (!containerRef.current) return; // 初始化绘图实例 plotRef.current = new WaveformPlot(containerRef.current, { width, height }); return () => { // 组件卸载时清理,避免内存泄漏 plotRef.current?.destroy(); plotRef.current = null; }; }, [width, height]); useEffect(() => { if (!plotRef.current || !seismograms) return; // 数据变化时重新绑定 plotRef.current.clear(); plotRef.current.append(seismograms); plotRef.current.draw(); }, [seismograms]); return <div ref={containerRef} />; }关键点是destroy调用,seisplotjs 的绘图实例会绑定事件监听器,不销毁的话组件反复挂载会导致监听器堆积,表现为缩放越来越卡。Vue 里同理,在onUnmounted里销毁。
5.2 性能验证:用 performance API 量一下解析和绘制耗时
优化之前先量。在关键路径上打点,确认瓶颈在解析还是绘制。
async function benchmark(file) { const buffer = await file.arrayBuffer(); performance.mark('parse-start'); const seismograms = parseMiniSeed(buffer); performance.mark('parse-end'); performance.measure('parse', 'parse-start', 'parse-end'); const plot = new WaveformPlot(container); performance.mark('draw-start'); plot.append(seismograms); plot.draw(); performance.mark('draw-end'); performance.measure('draw', 'draw-start', 'draw-end'); const parseTime = performance.getEntriesByName('parse')[0].duration; const drawTime = performance.getEntriesByName('draw')[0].duration; console.log(`解析耗时 ${parseTime.toFixed(1)}ms,绘制耗时 ${drawTime.toFixed(1)}ms`); }我一般会拿一个 10 万采样点的文件跑一遍,如果解析超过 200ms 或绘制超过 500ms,就得考虑降采样或分片加载。解析慢通常是文件里记录段太多,可以先用dataRecordOnly跳过元信息;绘制慢基本就是点数太多,降采样到屏幕像素级别即可。
5.3 一个容易忽略的细节:时区处理
地震数据的时间戳通常是 UTC,但用户看的时候可能期望本地时间。seisplotjs 默认按 UTC 显示,如果要转本地时间,需要在时间轴格式化函数里手动处理。我习惯在绘图配置里传一个自定义的timeFormat函数,把 UTC 转成目标时区再输出。这个细节不处理的话,用户看到的时间会差几个小时,排查起来很费劲。
从那以后我每次接地震数据可视化需求,都强制先跑一遍解析和绘制的耗时基准,确认数据量和采样率在可控范围内再往下做。希望帮到你。
本文还有配套的精品资源,点击获取