恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
seisplotjs:浏览器端解析miniSEED与绘制地震波形实战
首页
资讯中心
/
seisplotjs:浏览器端解析miniSEED与绘制地震波形实战
seisplotjs:浏览器端解析miniSEED与绘制地震波形实战
发布时间:2026/10/14 15:14:00
简介seisplotjs是一套面向地震数据处理与可视化的JavaScript模块适合需要解析台网波形、查询FDSN数据服务、绘制地震图的前端开发者与地球物理研究人员。压缩包内共184个文件体积约1.02MB其中83个js文件构成核心逻辑60个html文件为可运行示例页面14个css文件负责样式展示另外还打包了sac、sacpz、mseed等真实测试数据以及少量python脚本、json配置和说明文档方便直接对照工程结构学习。资源学习热度为319人。模块设计按功能拆分数据链路WebSocket协议、日期时间选择器、经纬度距离计算、FDSN可用性/注册表/事件/站点/波形数据查询并借助OregonDSP完成FFT变换再用d3绘制频谱与波形。通过阅读源码与示例可以快速理解各子模块的调用接口掌握从原始地震数据到交互式可视化图表的完整处理链路适合在该领域做原型开发、教学演示或进一步扩展。1. 浏览器里解析地震数据为什么需要seisplotjs第一次在浏览器里加载台站波形时我把miniSEED文件当文本读出来看到一堆二进制乱码才意识到地震数据格式和JSON完全是两个世界。seisplotjs是专门解决这个问题的Javascript模块它在浏览器端直接解析、处理并绘制地震数据让前端不再依赖后端渲染波形图。它能让你把原始SEED/miniSEED数据变成可交互的波形、频谱和震相标记适合做台网可视化、波形展示、地震科普工具甚至轻量级实时监控的开发者。接下来我会按真实项目里最常见的落地路径把流程、参数和踩过的坑完整讲一遍。2. seisplotjs的模块拆解与数据流从miniSEED到屏幕像素2.1 模块分成哪四块seisplotjs不是“大而全”的绘图库它更像一组面向地震学数据流的工具集。我一般把它拆成四块来看这样能帮你判断它到底解决了哪些问题哪些事还得自己写。第一块是格式解析。地震学领域最常见的格式是miniSEED它是二进制记录块包含固定头、可变头、采样率编码和整型/浮点数据子格式。解析这类格式看起来不复杂但处理时间重叠、记录块分包、Steim压缩算法时细节多到让人抓狂。seisplotjs把这层封装好了你不需要手写字节偏移。除了miniSEED它还支持SAC、QuakeML事件标记和StationXML台站响应常用的几种数据源基本覆盖全。第二块是信号处理。去均值、去趋势、高低通滤波、旋转ZNE转ZRT这些操作在原始数组上做并不难但难的是处理边界条件滤波后的瞬态响应、单位换算、时间对齐。seisplotjs把处理步骤封装成对trace对象的方法链式调用很直观。第三块是绘图。它基于浏览器canvas或SVG画波形自带时间轴和震相标记的支持也能处理交互缩放。不过要说明的是seisplotjs的绘图组件在不同版本里差异较大有的版本更像个底层解析器绘图交给Chart.js或ECharts也行。我的经验是如果项目只需要展示简单波形用它自带的组件最快如果需要复杂交互可以只用它的解析和信号处理部分绘图自研。第四块是辅助查询。它提供了一些工具方法帮你拼FDSN风格的事件服务、台站服务和波形服务URL并把返回的XML/JSON解析成对象。这部分能省很多拼接字符串的功夫也避免URL编码踩坑。这种拆法还有个好处你可以按需引入。用Vite或Webpack打包时只把用到的那几个文件打进产物不会让整个库的体积拖慢首页。2.2 数据流fetch-parse-filter-plot无论你从本地拖入一个.mseed文件还是从远端数据中心按时间窗拉取波形seisplotjs遵循的都是一条清晰的链路。我通常把它表达成四个环节获取通过HTTP拿到ArrayBuffer或者前端上传文件得到ArrayBuffer解析调用Seismogram.parse把二进制变成有数学意义的对象处理对trace做去均值、滤波、旋转等操作绘制把处理后的数据数组映射到canvas像素并把时间轴、震相线覆盖在上面。用一个极简伪代码表达// fetch拿到二进制数据 const resp await fetch(https://example.com/waveform.mseed); const buffer await resp.arrayBuffer(); // parse解析成一个“地震图”对象 const seismogram await Seismogram.parse(buffer); // 取其中一条连续trace处理 const trace seismogram.traces[0].removeMean().filterHighpass(1); // plot把处理后的数据交给绘图函数 drawWaveform(trace, canvas);这里最容易被忽略的是raw字节的获取方式。fetch出来后如果先调用了resp.text()再想转回ArrayBuffer就会丢字节必须直接arrayBuffer()。解析出的seismogram下会有一个traces数组每条trace有dataFloat32Array或Float64Array、samplerate、startTime、endTime这几个关键属性。后续所有处理都是围绕trace展开。调用Seismogram.parse时要记住两个惯例第一解析是异步的多数版本返回Promise别把它当成同步函数第二如果文件里包含多个台站、多个通道的数据traces会拆成多条那就要通过元数据里的“台网-台站-通道”去筛出你关心的那条trace。2.3 为什么把处理放在浏览器而不是后端早期很多地震可视化系统是后端先用Python或Java把波形渲染成静态PNG再推给前端。这样架构简单但交互弱用户想放大一段得重新请求一张图想看滤波前后的对比得等后端异步任务跑完。把解析和处理搬到浏览器后这种问题消失了——波形数据全部留在本地用户拖缩放条、调通带频率秒级响应。更关键的是物理量换算。台站原始记录是地面运动速度但用户有时想要加速度或位移。频域积分/微分如果放在后端做前端拿到的只是结果数组而在浏览器端做你可以随时重新积分、重新滤波UI上多一个下拉框就能实现“速度/加速度/位移”切换。但也要知道边界。浏览器内存不是无限的把一段一个月的连续波形全拉到前端不仅网络慢内存也撑不住。所以用seisplotjs的项目通常会做分层拉取先取低采样率的概览数据用户放大到分钟级再取完整数据。这个策略后面的避坑章里我会专门讲。3. 在项目里接入seisplotjs最小运行示例3.1 两种引入方式npm与CDN接入seisplotjs前要先确定项目的构建方式。当前前端项目大多基于Vite、Webpack或React脚手架适合用npm安装并做模块化引入。在命令行里执行npm install seisplotjs然后在代码中按需导入import { Seismogram } from seisplotjs;这里要注意不同版本的库导出结构不一样。有的版本导出Seismogram这个类有的把解析函数挂在外层。最稳妥的做法是装完包后打开node_modules/seisplotjs下的package.json看exports字段里有哪些模块入口再确定你的导入路径。如果只是写一个临时HTML页面做验证不想搭构建工具也可以走script typemodule的方式从CDN加载。比如script typemodule import { Seismogram } from https://some-cdn.example/seisplotjs.esm.js; // 之后按模块方式使用 /script但我不建议生产环境这么干。CDN版本通常是一个整包ESM文件体积大也难以tree-shaking一旦CDN地址变动或网络不稳定整个页面就白屏。本地npm打包器能按需引包还有利于版本锁定。3.2 连接台网数据服务并解析波形的最小代码下面这段代码是我在真实项目里“从事件查询一路到波形解析”的骨架。它假设你已经有一个带事件目录和波形服务的上游接口接口风格类似FDSN标准但台网和台站请替换成你自己的配置。// 1. 查询某个时间窗内的事件 const eventUrl buildEventUrl({ startTime: 2024-01-01T00:00:00Z, endTime: 2024-01-02T00:00:00Z, minMag: 5 }); const eventXml await (await fetch(eventUrl)).text(); const quake parseQuakeMl(eventXml); const origin quake.events[0].origins[0]; // 取第一个事件的第一个震源解 // 2. 查询台站通道信息 const stationUrl buildStationUrl({ network: XX, station: TEST, channel: BHZ, level: channel }); const stationXml await (await fetch(stationUrl)).text(); const channel parseStationXml(stationXml).channels[0]; // 3. 按事件时间和台站信息抓取miniSEED const waveformUrl buildWaveformUrl({ network: XX, station: TEST, channel: BHZ, startTime: origin.time.toISOString(), endTime: new Date(origin.time.getTime() 1800 * 1000).toISOString() }); const mseedBuffer await (await fetch(waveformUrl)).arrayBuffer(); const seismogram await Seismogram.parse(mseedBuffer); // 4. 取出我们要的那条trace const trace seismogram.traces.find(t t.channelCode BHZ); console.log(trace.samplerate, trace.startTime.toISOString(), trace.data.length);这段代码里的buildEventUrl、buildStationUrl、buildWaveformUrl、parseQuakeMl、parseStationXml这些函数在seisplotjs里都有对应的工具具体名字以你用的版本为准。它的价值在于把“拼URL”和“解析XML”这两步变得可靠你不需要自己去手写查询字符串编码。参数上要注意三点channel: BHZ只取垂直向通道如果写BH*会拿到BHZ、BHN、BHE三通道流量翻三倍但方便画三分量。level: channel表示台站服务只返回通道级元数据如果把级别调成stationXML里不含采样率信息后续计算时间轴会困难。波形时间窗我截取了发震后30分钟因为一次远震事件的主要震相通常在半小时内到达。如果你的目标是本地小震时间窗可以缩到5分钟。3.3 网络-台站-通道三件套怎么定这个三件套看似简单实际最容易被用户问懵。真实台网里同一个台站可能有BHZ、HHZ、EHZ并存它们的频带和采样率不同展示出来的物理意义也不同。我自己的习惯是给下拉框里的通道做优先级排序HHZ BHZ EHZ。现代宽频带台站通常用HH*高频或BH*宽带中增益老台站才有EHZ这种短周期通道。当用户选了“网络-台站”之后前端应该先请求一次台站服务把可用通道列表拉下来再填进通道下拉框而不是写死一个BHZ。不然碰到某个台站只有HHH分量你画出来的图就是一条空线。如果通道列表里同时有BHZ和HHZ要默认选采样率更高的那个而不是第一个。因为地震预警里P波到时精度很大程度上由采样率决定100Hz和40Hz在图上差不了太多但自动读数时差异明显。还有一点不要忽略通道方向。BHZ是垂直向BHN是南北向BHE是东西向。如果你做旋转到ZRT坐标系前端必须知道三个分量的方位角这些信息在StationXML的azimuth字段里。所以台站解析不能只拿采样率最好把azimuth和dip一并存起来。4. 绘制与处理地震数据从波形到滤波参数的完整链路4.1 画一个能动的波形图拿到trace后最直接的做法是把它画到一个canvas上。seisplotjs如果自带绘图组件我会优先用它因为它已经处理好“时间轴刻度”和“极小值压成竖线”这些细节。但如果你只用了它的解析模块想自己画波形下面这段代码是一个可靠起点function drawTrace(canvas, trace) { const ctx canvas.getContext(2d); const W canvas.width; const H canvas.height; const data trace.data; const n data.length; // 计算振幅最大值用于纵向缩放 let maxAbs 0; for (let i 0; i n; i) { const absVal Math.abs(data[i]); if (absVal maxAbs) maxAbs absVal; } if (maxAbs 0) maxAbs 1; // 防止除零 ctx.clearRect(0, 0, W, H); ctx.beginPath(); const yMid H / 2; const yScale H * 0.8 / (2 * maxAbs); for (let i 0; i n; i) { const x (i / (n - 1)) * W; const y yMid - data[i] * yScale; if (i 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.strokeStyle #333; ctx.lineWidth 1; ctx.stroke(); }这段代码最大的问题是当trace有几十万个点时循环里执行大量lineTo页面会卡顿。所以实际使用时你要先判断数据点数再决定是否降采样。常见做法是当点数超过canvas宽度像素数的两倍时每Math.floor(n / (W * 2))个点取一个极值点最大值和最小值分别采样这样绘出来的波形包络视觉上几乎不失真。另一个要注意的是trace.data里的单位。原始miniSEED通常以“counts”为单位不同台站仪器的灵敏度不同直接画出来的振幅没有物理意义。如果需要展示物理量要把counts除以灵敏度因子这一步在通道级元数据里可以拿到。4.2 在图上标P波S波震相波形图只有曲线还不够专业用户要在图上看到P波、S波到时。地震学里P波、S波的理论到时通常由速度模型计算得到前端拿到的是几个时间字符串。绘制时只需要把“时间”换算成“像素x坐标”function timeToX(timeMs, trace, width) { const startMs trace.startTime.getTime(); const endMs trace.endTime.getTime(); return ((timeMs - startMs) / (endMs - startMs)) * width; } // 假设理论P波到时为pArrivalS波到时为sArrival ctx.strokeStyle #2196F3; ctx.beginPath(); ctx.moveTo(timeToX(pArrival.getTime(), trace, W), 0); ctx.lineTo(timeToX(pArrival.getTime(), trace, W), H); ctx.stroke(); ctx.strokeStyle #F44336; ctx.beginPath(); ctx.moveTo(timeToX(sArrival.getTime(), trace, W), 0); ctx.lineTo(timeToX(sArrival.getTime(), trace, W), H); ctx.stroke();这里有个容易忽略的细节时间的时区。trace.startTime、理论到时都应当统一按UTC存储如果直接调用getHours()浏览器会返回本地时间导致震相线整体偏移。我的习惯是全程用Date对象只显式格式化为ISO字符串给用户看。当UI要求用户能手动修正震相位置时我一般会在canvas上做事件监听记录鼠标按下到抬起后的横向偏移然后用这个偏移量去调整arrial time并重新画线。这个交互看起来简单但需要把“像素偏移”转换成“毫秒偏移”deltaMs deltaPx * (durationMs / width)。4.3 滤波与去均值的参数怎么调处理数据的目的是去掉噪声突出感兴趣的频段。常见的处理链是let trace rawTrace.removeMean(); trace trace.removeTrend(); trace trace.filterHighpass(0.5); trace trace.filterLowpass(8);removeMean去掉的是信号里的直流分量如果不做后续低通滤波后曲线会整体抬高一个大台阶。removeTrend去掉的是基线漂移通常由于仪器温度变化引起线性回归后把趋势减掉。滤波参数选取要看研究目标地方震震中距几十公里P波频率较高常用1~10Hz带通远震数千公里体波主频在0.5~2Hz用0.5~5Hz长周期面波研究用到0.01~0.1Hz数据必须来自宽频带台站。高通滤波的频率不能设得过高否则会削掉P波低频成分导致初至不清晰低通滤波频率也不宜太低否则会丢失高频震相信息。一个初步经验是目标频段带宽至少要有2倍倍频程比如研究地方震用1Hz高通、8Hz低通这是一个相对通用的组合。滤波后还会出现一个“伪影”头尾震荡。这来自IIR滤波器的瞬态响应。规避办法是滤波前给数据两端各外接一段长度为滤波器阶数倍的常数或零值滤波后再裁掉。有些seisplotjs版本内部做了稳态处理但如果你看到波形前100个点出现大幅摆动就不要怀疑数据了先想想是不是滤波前没做去均值。4.4 长波形的降采样与性能优化当一次画出多个通道的波形性能挑战会迅速放大。我踩过一次同时请求某台阵20个台站的三分量BHZ波形每个通道约600秒100Hz采样总共120万点直接for循环绘制浏览器卡了近3秒。后来我改成三步先按通道对数据做“概览”索引。只取每个时间窗口比如每秒内的最大最小值形成两个数组绘制时用这两个数组画填充区域或竖线。只有当用户放大到窗口内点数小于1万时才重绘原始采样数据。所有canvas重绘都放进requestAnimationFrame避免在连续的鼠标滚轮事件里重复绘制。此外还可以把解析和滤波放到Web Worker里避免阻塞UI。这个方案后面避坑章会给出代码。性能调优不是seisplotjs自带的能力但它提供的trace对象足够简单让你可以自由做优化。5. seisplotjs避坑指南5条来自真实项目的踩坑记录5.1 时间轴不对UTC与本地时区现象从接口返回的startTime写的是2024-01-01T10:00:00Z但画图时坐标轴显示成了本地时间波形位置和事件标签全偏了8小时。原因台网标准时间统一用UTC但显示组件默认调用toLocaleString()它会自动转成用户本地时区。如果前端组件里再次调用new Date(isoString)并格式化就可能出现三重偏移。解决所有时间对象在内部存储和计算时都保持UTC只在显示刻度时显式格式化。// 错误示范toLocaleString会自动转本地时区 ctx.fillText(time.toLocaleString(), x, y); // 正确做法用UTC字符串显示时再决定要不要加“Z” ctx.fillText(time.toISOString(), x, y);如果UI要显示“北京时间”再手动加上8小时不要依赖toLocaleString的默认行为。5.2 采样率不一致导致波形错位现象三条通道的分量分别来自不同采样率前端用同一个数组索引对齐结果S波峰在一个图上位于25%位置另一个图上跑到了75%位置。原因trace.data是离散采样点数组数组索引对应时间必须乘以1 / samplerate。不同采样率的通道索引差值会随采样点累积到1000个点时可能已经偏了2秒。解决绘制和计算时以“绝对时间”为基准把数组索引转成时间戳function sampleToTime(trace, index) { return trace.startTime.getTime() (index / trace.samplerate) * 1000; }所有对比、震相拾取都基于这个函数不要直接用trace.data[i]和trace.data[j]做算术。5.3 滤波后数据头尾出现尖峰现象对一段噪声信号做高通滤波滤波结果的前200个点和尾部都出现了大幅摆动看起来很“震”但这在原始数据里并不存在。原因经典IIR滤波器对数据起始段有瞬态响应因为滤波器的状态变量初始为零需要几个时间常数才能收敛。滤波器阶数越高、截止频率越低瞬态越长。解决给滤波器的输入数据两端加“过渡带”用一个pad函数延长数组const padLen Math.round(trace.samplerate * 5); // 5秒填充 const padded new Array(trace.data.length padLen * 2).fill(0); padded.set(trace.data, padLen); const filtered filt(padded); trace.data filtered.subarray(padLen, padLen trace.data.length);如果你在seisplotjs里找不到pad参数就直接把绘制窗口内缩不显示滤波后前5%的数据。对大多数展示场景这个方法简单有效。5.4 内存爆掉一次性加载一个月波形现象用户选了某台站一个月的BHZ数据浏览器标签页直接崩溃控制台报“Out of memory”。原因一个月连续波形按40Hz采样约1亿个点即使使用Float32Array也有约400MB再加上时间计算和canvas缓存很容易超过普通电脑的浏览器内存限制。解决我采用“概览图选区拉取”策略先请求低采样率的概览数据例如1Hz当用户放大到10分钟粒度时再去请求原始采样率数据。具体做法是// 先请求低采样率概览 const lowRateUrl buildWaveformUrl({ start, end, sampleRate: 1 }); // 当用户点击放大后再请求原始采样率 const highRateUrl buildWaveformUrl({ start: newStart, end: newEnd });很多台网服务支持sampleRate参数或者可以通过先下载再抽稀。如果服务不支持宁可分段拉取也不要一次性加载超过10分钟的100Hz波形到浏览器。5.5 fetch请求被CORS拦死现象在本地直接用file://打开HTML调用远程台网数据接口控制台报CORS policy错误有人因此以为seisplotjs有Bug。原因浏览器安全策略限制跨域请求远程接口如果没有返回Access-Control-Allow-Origin头前端永远无法拿到数据。很多地震数据服务出于安全考虑默认只允许特定域访问或者根本不开启CORS。解决开发阶段不要用file://本地起一个静态服务器并在构建工具里配置代理。// vite.config.js 常见配置 export default { server: { proxy: { /api: { target: https://your-data-service.example, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } };这样前端请求/api/...就变成同源请求绕开CORS。如果服务端支持JSONP也可以试试但JSONP只能用于GET且无法传大二进制不是主流方案。6. 进阶把seisplotjs接到你的地图与实时系统里如果你已经跑通了上面的最小链路下一步就是把它封装成一个可复用的波形组件。我的推荐做法是做一个“波形卡片”对外只暴露一个方法load({ network, station, channel, start, end })内部负责请求、解析、处理、绘制和销毁。这样不管是放在地图弹窗、事件详情页还是聊天工具里都能直接使用。地图联动是我做得最多的场景。用户在Leaflet或Mapbox地图上点击台站圆点弹窗里出现波形卡片。台站多时我只加载可视区域内的台站波形当用户移动地图或缩放时用abort取消未返回的请求避免过期响应覆盖新数据。这个交互听起来简单但“请求取消”一定要做否则用户快速点两个台站前一个台站的波形可能晚到并覆盖后一个。实时系统里常见方式是后端用WebSocket推送最新的miniSEED片段前端每收到一个片段就追加到trace尾部。追加时要注意如果直接用concat来拼接数组每次都是O(n)复制几百条片段后页面就会卡死。我维护一个环形缓冲区将每个通道的连续数据放在固定大小的Float32Array里用写指针和读指针控制有效区间。这个方案的优点是内存恒定缺点是要自己处理跨缓冲区边界的时间轴换算。最后说一个真实教训我最早用seisplotjs做实时监控时没有释放旧trace的引用每换一次台站就累积一块内存监听了几小时后标签页内存占用连续增长。后来我强制在切换台站时执行trace.data null并把canvas宽度重置一次内存才稳定下来。这件事让我认识到seisplotjs解析出的对象虽然好用但GC压力全在JS堆上数据量大时必须主动释放。希望这篇能帮你绕过我走过的弯路快速在浏览器端把地震数据用起来。本文还有配套的精品资源点击获取