基于JavaScript的数智地质系统:钻孔柱状图与剖面图绘制源码解析
简介这是一份基于JavaScript的数智地质系统设计源码面向地质信息化开发者和相关研究人员用于搭建地质数据智能化处理与分析平台。压缩包共788个文件约8.71MB涵盖415个XML配置、219个Java后端源文件、38个JS脚本、17个CSS样式、14个YML配置以及HTML页面、图片与字体素材后端服务到前端展示均有覆盖。目前已有323人学习下载适合需要了解地质系统完整结构或进行二次开发的工程师参考。通过这份源码可学习Java与JS协同、XML/YML配置管理、GIS可视化元素应用等多技术栈整合思路也可直接基于清晰目录结构扩展功能用于实际项目设计与教学研究。1. 数智地质系统不是换个皮肤:一份能跑钻孔数据的 JavaScript 源码做岩土勘察的人都有这种体验:野外钻孔数据堆成山,报告要出柱状图、剖面图,用 CAD 一笔一笔描,改一层土就要重画三张图。这个基于 JavaScript 的数智地质系统设计源码,解决的正是这个具体问题——把钻孔数据、地层分层和剖面展示搬进浏览器,双击 index.html 就能跑,不需要 ArcGIS,不需要数据库服务端。数智在这里落地的是数据驱动可视化:数据一变,柱状图、剖面图、信息面板全部跟着更新。整套工程是纯 HTML JavaScript,地质数据用 JSON 组织,Canvas 负责绘制。适合两类人:做地质信息系统原型验证的工程师,想把勘察数据快速变成能看的图;前端开发者,想抄一套钻孔柱状图与剖面图联动绘制的成熟写法。我拆完的感受是,它不 fancy,但数据流是完整闭环的,坑也踩得明明白白。2. 先拆工程:HTML 入口、三层 JSON 模型与 Canvas 画布的分工2.1 从 index.html 开始:先看引用,再谈功能源码包解压之后,别急着开 IDE,先用文本编辑器打开 index.html 扫一遍。我拆任何前端工程都是这个习惯:先看页面引了哪些脚本,这比读 README 快得多,也最能看出系统边界。这个工程的入口结构大致是这样:!DOCTYPE html html langzh-CN head meta charsetUTF-8 title数智地质系统/title link relstylesheet hrefcss/main.css /head body div idtoolbar select idprojectSelect/select select idboreholeSelect/select button idreloadBtn重新加载/button /div div idmapContainer canvas idprofileCanvas width1200 height600/canvas /div div idinfoPanel h3钻孔信息/h3 div idinfoContent/div /div script srcjs/data.js/script script srcjs/geo.js/script script srcjs/render.js/script script srcjs/app.js/script /body /html四个脚本的分工很明确:data.js 存放地质数据和全局变量;geo.js 负责坐标换算,所有数学计算都在这;render.js 只做 Canvas 绘制,不碰业务;app.js 是控制器,做初始化和事件绑定。我把这个拆分方式叫「数据—换算—绘制—控制」四段式。地质系统复杂度不算高,这个分层刚好够用,而且比把代码全堆在一个文件里好维护得多。这里有个容易忽略的点:canvas 的 width 和 height 属性写的是 1200 和 600,单位是物理像素,不是 CSS 像素。普通屏幕上两者一致,高分屏上会发虚,这个坑留到第 5 章细说。另外注意,页面里没有引入任何第三方图表库,说明剖面图和柱状图都是原生 Canvas 画的。这是好事也是坏事:好处是零依赖、离线能跑,坏处是如果你要加 tooltip、缩放、图例拖拽,全得自己写。我把这套源码定位成「教科书级的地质数据可视化骨架」,不是开箱即用的商业 GIS 系统。它最大的价值在于把一条完整的数据链路讲清楚了,你照着抄、照着改都行。文件职责改动的典型场景data.js地质数据与全局变量换项目数据、加钻孔geo.js坐标换算与比例尺换坐标系、改投影范围render.jsCanvas 绘制改颜色、加标注app.js初始化与事件绑定加交互、加联动这张表是我后面改代码时的索引:改数据去 data.js,改坐标系去 geo.js,改颜色标注去 render.js,加交互去 app.js。很多新手拿到源码喜欢到处翻,有了这张表,定位问题的时间能省一半。提示:data.js 里的示例数据是写死的,替换真实数据时保持字段名一致,尤其是 top、bottom 的数值类型,要是被 Excel 导出成了字符串,减法运算直接出 NaN。2.2 三层 JSON 数据模型:项目、钻孔、地层数据模型是这套系统的地基。我读 data.js 的时候,第一反应是看它怎么组织地质数据。它用的是三层嵌套:顶层是项目,项目下面挂钻孔,每个钻孔下面挂地层。结构类似这样:{ projectId: PJ-2024-017, projectName: 某滨江路岩土勘察项目, crs: EPSG:32650, boreholes: [ { id: ZK01, x: 503421.75, y: 3345120.38, elevation: 12.5, depth: 25.6, layers: [ { name: 杂填土, top: 0, bottom: 2.8, color: #8B7355 }, { name: 粉质黏土, top: 2.8, bottom: 9.4, color: #D2B48C }, { name: 中砂, top: 9.4, bottom: 18.2, color: #F0DEB4 }, { name: 强风化岩, top: 18.2,bottom: 25.6, color: #A0522D } ] } ] }这个模型有几个设计比较聪明。第一,地层用 top 和 bottom 表示绝对深度,而不是用厚度,画柱状图只需要做减法,不需要累加;第二,每个地层带 color 字段,渲染时直接用,不用维护岩性到颜色的映射表;第三,crs 字段记录了坐标系,geo.js 里对投影坐标做的是平移缩放,不是真正的投影转换,说明源码默认输入数据已经是同一坐标系下的投影坐标。这个前提很重要:如果你的数据源是经纬度(WGS84),直接喂进去,剖面会完全错位,得先做投影转换。读到这层数据,工程师最关心两个问题:数据从哪来、数据怎么验。data.js 里是写死的示例数据,真实项目应该换成后端接口或勘察数据库导出。常见做法是写一个转换脚本,把理正、华宁这些岩土勘察软件导出的 Excel 或数据库表转成 JSON,再塞进 data.js。至于验证,我用的土办法是:取几个钻孔的终孔深度,和最后一层的 bottom 比对,不一致就是数据有问题。这个三层模型是合理的,但注意它没有处理斜孔和分层缺失的情况。如果某孔缺某一层,JSON 里不填就行,前提是渲染代码有空值判断,render.js 里确实有,后面会讲到。还有一个容易被忽略的字段:elevation,钻孔口标高。剖面图如果只按深度画,不换算标高,两个孔口不在同一水平面时,分层界线就是错的。源码里柱状图按深度画,剖面按标高画,这两套 Y 轴逻辑要分清。实际项目里,地表起伏大的工区,一定要用标高而不是深度,否则剖面图会把起伏的地形画成平的。3. 核心渲染实现:钻孔柱状图与剖面图的坐标换算和 Canvas 绘制3.1 钻孔柱状图:分层数据如何变成填充矩形地质系统的可视化核心是钻孔柱状图。它的本质不复杂:每一层就是一个矩形,深度方向对应矩形高度,岩性对应填充色,层名标注在矩形右侧。render.js 里这个核心函数,简化后长这样:/** * 绘制单个钻孔柱状图 * param {CanvasRenderingContext2D} ctx 画布上下文 * param {Object} bh 钻孔数据对象 * param {number} startX 柱状图左上角 X(像素) * param {number} topY 柱状图顶部 Y(像素) * param {number} scale 垂直比例尺(像素/米) */ function drawBoreholeColumn(ctx, bh, startX, topY, scale) { let currentY topY; bh.layers.forEach(function (layer) { // 用 top/bottom 求厚度,再乘比例尺得到像素高度 const height (layer.bottom - layer.top) * scale; // 填充岩性颜色 ctx.fillStyle layer.color; ctx.fillRect(startX, currentY, 40, height); // 描边,让层与层之间边界清楚 ctx.strokeStyle #666; ctx.lineWidth 1; ctx.strokeRect(startX, currentY, 40, height); // 在右侧标注层名和顶底深度,深度保留一位小数 ctx.fillStyle #333; ctx.font 12px sans-serif; const label layer.name ( layer.top.toFixed(1) ~ layer.bottom.toFixed(1) m); ctx.fillText(label, startX 46, currentY height / 2 4); // 累加当前 Y 坐标 currentY height; }); }这个函数有两点值得说。第一,它把「深度」换算成「像素」的职责留给了调用方——传入的 scale 就是垂直比例尺,比如 scale 20 表示每米 20 像素。比例尺单独传参,而不是在函数内部计算,意味着同一个函数可以同时服务于 1:200 和 1:500 两种图幅,只要外面换 scale。我一般会在主程序里根据钻孔最大深度反推 scale,公式是scale (canvasHeight - marginTop - marginBottom) / maxDepth,这样不管钻孔是 20 米还是 80 米,柱状图都能填满画布。第二,层名的标注用layer.top.toFixed(1)保留了一位小数。深度是地质报告里的重要数据,显示 9.400000000000002 是很掉价的。JavaScript 的浮点运算是这套系统最容易出问题的地方,JSON 里的 9.4 经过减法运算之后可能变成 9.399999999999999,后面专门讲。这里先记住一个原则:凡是上屏的深度数字,一律先 toFixed 再显示,计算的时候用原始值。另外,柱宽写死的是 40 像素,这个数字不是乱拍的。40 像素在 1200 宽的画布上,十个钻孔能排开;再宽,钻孔密集的地方就要打架。实际用的时候,我会把柱宽提成参数 columnWidth,根据钻孔数量动态算:columnWidth Math.min(40, canvasWidth / (bhCount * 3))。钻孔少就画宽一点,钻孔多就自动缩,避免图面拥挤。3.2 剖面坐标换算:地质坐标到 Canvas 像素的完整链路如果说柱状图是「画矩形」,那剖面图就是「画连线」。剖面图要把所有钻孔按平面位置摆到画布上,再把相邻钻孔的同名地层分界点连起来,形成岩层分界面。这一步的核心是坐标换算,geo.js 里的这个函数是我认为整套源码最值得抄的部分:/** * 将地质投影坐标换算为画布像素坐标 * param {number} x 地质 X(投影坐标,单位米) * param {number} y 地质 Y(投影坐标,单位米) * param {Object} bounds 数据范围 {minX, maxX, minY, maxY} * param {number} canvasWidth 画布宽度(像素) * param {number} canvasHeight 画布高度(像素) * returns {{px: number, py: number}} 画布像素坐标 */ function geoToPixel(x, y, bounds, canvasWidth, canvasHeight) { if (bounds.maxX - bounds.minX 0 || bounds.maxY - bounds.minY 0) { throw new Error(数据范围为零,无法换算坐标); } const scaleX canvasWidth / (bounds.maxX - bounds.minX); const scaleY canvasHeight / (bounds.maxY - bounds.minY); const scale Math.min(scaleX, scaleY); // 取较小者保证不变形 // 注意:地质坐标 Y 轴朝北(向上),Canvas Y 轴朝下,这里必须翻转 const px (x - bounds.minX) * scale (canvasWidth - (bounds.maxX - bounds.minX) * scale) / 2; const py (bounds.maxY - y) * scale (canvasHeight - (bounds.maxY - bounds.minY) * scale) / 2; return { px, py }; }这个函数里有三个细节是新手最容易翻车的。第一,Y 轴翻转:地质坐标里北方向是 Y 轴正方向,Canvas 里 Y 轴正方向朝下,所以 py 是(bounds.maxY - y) * scale,而不是(y - bounds.minY) * scale。忘了这一步,整个剖面图会上下颠倒,钻进地表以下。我见过不止一个人在这行代码上翻车,改完之后整个图倒过来,还以为是数据问题。第二,取Math.min(scaleX, scaleY)作为统一比例尺,是为了让 XY 两个方向的比例一致,剖面图不变形。如果直接用 scaleX 画 X、scaleY 画 Y,一个 100:1 一个 50:1,钻孔间距和深度就会失真。代价是图会在某个方向上有留白,后面加的两项就是让图形居中的偏移量。第三,也是最容易被忽略的:bounds 必须是所有钻孔的并集范围,包括最东、最西、最南、最北的孔。我在拆的时候注意到,如果 bounds 写死成固定值,或者只算了前两个钻孔的范围,后面的钻孔就会画出画布,还有可能因为范围差为 0 直接抛异常。这里加一个显式的 throw,是很专业的处理——坐标换算的前提不成立,就别静默返回一个错得离谱的像素坐标。还有一个数量级的细节:(bounds.maxY - y)这一步,投影坐标动辄几十万米,减出来数量级也很大,再乘 scale 时浮点误差会被放大。常见做法是先把坐标减掉一个基准原点(比如项目区西南角),再做换算。geo.js 里是直接减 bounds,在坐标值不超过百万级时没问题,但如果项目跨了多个带,坐标到了八位数,建议先减原点再进 geoToPixel。3.3 岩层分界连线:相邻钻孔怎么连才不出错坐标换算搞定之后,剖面线的绘制就顺理成章了。核心逻辑是:对每一层,把相邻两个钻孔里该层的底部深度换算成像素点,然后用线连起来。render.js 里的连线逻辑简化后是这样:function drawLayerBoundary(ctx, boreholes, layerIndex, geoToPixel, canvasWidth, canvasHeight, bounds) { const points []; boreholes.forEach(function (bh) { const layer bh.layers[layerIndex]; // 某些钻孔可能缺失这一层,跳过 if (!layer) return; const p geoToPixel(bh.x, layer.bottom, bounds, canvasWidth, canvasHeight); points.push(p); }); if (points.length 2) return; // 少于两个点画不了线 ctx.beginPath(); ctx.moveTo(points[0].px, points[0].py); for (let i 1; i points.length; i) { ctx.lineTo(points[i].px, points[i].py); } ctx.strokeStyle #4A708B; ctx.lineWidth 1.5; ctx.stroke(); }注意这里的地质含义:bh.x 是钻孔的平面位置,layer.bottom 是这一层的底界深度。把底界深度当作 Y 值传入 geoToPixel,得到的就是「这一层底界面在剖面上的位置」。把所有钻孔的同一层底界连起来,就是岩层分界线。这套逻辑的假设是:所有钻孔的层序一致,第 0 层都是地表填土,第 1 层都是粉质黏土。如果实际勘察数据里有的钻孔漏了这一层,代码里的if (!layer) return就会跳过它,这条线就会在这个位置断开,而不是画一条错误的斜线。我在实际项目里被这个问题坑过:某个孔中间缺了一层,结果连线把缺层那个孔的顶界和底界交叉连了,剖面图出现了「地层交叉」的假象。后来我定了一条规矩:连线之前,先检查相邻两个钻孔的层序是否一致,如果不一致,要么补数据,要么在图上明确断开。地质剖面最怕的就是画出来很漂亮,但地层关系是错的。源码里这个跳过缺失层的处理是对的,但你要理解它背后的含义,才能判断你的数据适不适合这套逻辑。4. 交互联动与数据接入:事件绑定、命中检测与标注4.1 下拉框联动与画布点击拾取钻孔静态画图只是第一步,地质系统如果只能看不能交互,价值就少了一半。app.js 里做了两个核心交互:下拉框切换钻孔、点击画布拾取钻孔。下拉框的逻辑很直接,就是监听 change 事件,重新渲染:document.getElementById(boreholeSelect).addEventListener(change, function (e) { const bhId e.target.value; const bh boreholes.find(function (b) { return b.id bhId; }); if (bh) { renderBoreholeDetail(bh); // 右侧信息面板 highlightBorehole(bh.id); // 剖面上高亮这个钻孔 } });这段代码里我用到了Array.prototype.find来找钻孔对象,比 for 循环遍历要清爽。两个函数renderBoreholeDetail和highlightBorehole分工明确:一个管右侧信息面板,一个管画布高亮。我拆这套源码时最想强调的是,事件回调里不要写太多逻辑,回调只做「找到数据、调用渲染」这两件事,具体的绘制细节丢给 render.js。项目一大人就容易在回调里堆代码,最后事件回调里塞了 100 行,没人敢动。画布点击拾取是更有意思的部分。它本质上是「命中检测」:算出每个钻孔在画布上的像素位置,判断鼠标点击位置和哪个钻孔足够近。代码大致是这样:profileCanvas.addEventListener(click, function (e) { const rect profileCanvas.getBoundingClientRect(); const clickX e.clientX - rect.left; const clickY e.clientY - rect.top; // 遍历所有钻孔,找距点击位置 15 像素以内的钻孔 const hit boreholes.find(function (bh) { const p geoToPixel(bh.x, bh.y, bounds, profileCanvas.width, profileCanvas.height); const dist Math.sqrt((clickX - p.px) ** 2 (clickY - p.py) ** 2); return dist 15; }); if (hit) { document.getElementById(boreholeSelect).value hit.id; renderBoreholeDetail(hit); highlightBorehole(hit.id); } });这里有个细节值得注意:e.clientX 是相对于浏览器视口的坐标,rect.left 是画布左边距视口的位置,两者相减才是相对于画布左上角的坐标。如果画布外层有滚动或 padding,直接拿 clientX 去和 Canvas 像素坐标比,永远对不上。还有一个隐患:geoToPixel 里用的是profileCanvas.width(物理像素),而 rect 拿的是 CSS 像素,在普通屏上两者一致,在高分屏上不一致,这个兼容问题放到第 5 章。命中半径我取的是 15 像素,实际项目里要根据钻孔密度调整——钻孔密集的项目区,15 像素可能一次命中两三个孔,这时候要取距离最近的那个,而不是 find 返回的第一个。改法是把 find 换成 reduce,比较距离取最小值。另外,如果不清理上一次的高亮,点一次就会多一个高亮圈,图上全是圈。我习惯在 highlightBorehole 开头先重绘一次不带高亮的整图,再画高亮,保证只有一个钻孔被标记。4.2 图例、深度刻度与比例尺的标注剖面图光有图层线是不够的,工程图要有可读性。render.js 里画了三个辅助元素:左侧深度刻度尺、底部岩性图例、右下角比例尺。这套源码的处理方式值得借鉴,它把刻度尺和比例尺画在 Canvas 上,而不是用 DOM 元素拼接。我一开始不理解为什么不用 DOM——写起来更简单啊。后来想通了:Canvas 的内容是整块绘制的,截图、导出图片、打印,全都带上这些标注;如果刻度尺用 DOM 画,截图时还得额外拼图。做地质系统的都知道,报告里的图是要导出来的,这个设计是冲着出图去的。深度刻度尺的绘制逻辑是:每隔 5 米画一条刻度线,在刻度线左侧标注深度值。代码看着不复杂,但有一个关键点——刻度的间距不能是固定像素,而要跟随 scale 变化:function drawDepthScale(ctx, scale, marginLeft, topY, bottomY) { const interval 5; // 每 5 米一个刻度 const maxDepth (bottomY - topY) / scale; // 画布能显示的最大深度 ctx.strokeStyle #999; ctx.fillStyle #666; ctx.font 11px sans-serif; for (let d 0; d maxDepth; d interval) { const y topY d * scale; // 画一条 10 像素长的短线 ctx.beginPath(); ctx.moveTo(marginLeft - 10, y); ctx.lineTo(marginLeft, y); ctx.stroke(); // 标注深度值 ctx.fillText(d.toFixed(0) m, marginLeft - 46, y 4); } }这里的核心是d * scale:深度值是地质单位(米),乘上比例尺才是像素位置。如果刻度间距写死成像素,比如每 40 像素一个刻度,那一旦 scale 变化,刻度对应的深度就不是整数,专业图上看起来很业余。我见过有人写死刻度像素间距,结果剖面图上标注的是 3.7m、8.3m 这种深度,整个图没法看。正确的做法是让刻度跟着地质深度走,米数是整的,像素位置自然由 scale 决定。岩性图例的绘制更简单,在底部画一排小色块,每个色块后面跟一个岩性名称。但要注意一件事:图例要去重。同一个项目里「粉质黏土」可能出现几十次,如果直接把所有钻孔的层名都画上去,图例会有一大串重复项。我一般会先遍历数据,用岩性名称做 key 塞进一个对象去重,再画图例。还有一个习惯:画完整个剖面后,我会调用一次canvas.toDataURL(image/png)把图导出做归档,这样图上所有标注都带走了,不用像 DOM 方案那样重新拼图。4.3 钻孔详情面板与字段兜底:别让整页崩溃详情面板是信息输出的最后一环。选中钻孔后,右侧 infoPanel 要显示孔号、XY 坐标、孔口标高、终孔深度和每层的信息。renderBoreholeDetail 的实现大致是拼 HTML 字符串:function renderBoreholeDetail(bh) { if (!bh) { document.getElementById(infoContent).innerHTML p未选择钻孔/p; return; } let html ul; html li孔号: bh.id /li; html li坐标: bh.x.toFixed(2) , bh.y.toFixed(2) /li; html li孔深: bh.depth.toFixed(2) m/li; html li层数: bh.layers.length /li/ul; html tabletrth层名/thth顶深/thth底深/th/tr; bh.layers.forEach(function (layer) { html trtd layer.name /td; html td layer.top.toFixed(2) /td; html td layer.bottom.toFixed(2) /td/tr; }); html /table; document.getElementById(infoContent).innerHTML html; }这段代码里用了 toFixed(2) 保证深度显示两位小数,避免浮点尾巴上屏。另外注意:先判断 bh 是否为 null,再拼 HTML。事件回调里如果拿到 undefined 就直接操作 DOM,整个页面脚本会停掉,这是很常见的翻车点。我习惯在所有渲染函数的入口做空值兜底,宁可显示「未选择钻孔」,也不能让整页白掉。如果数据来自后端,拼接 HTML 字符串时还有注入风险,我一般会把 layer.name 做一次转义。源码里是示例数据没这个必要,真实项目要加上。5. 避坑与排查:坐标错位、浮点精度、乱码与白屏的五个现场5.1 钻孔点全挤在画布角落现象:剖面图画出来了,但所有钻孔都缩在左下角或某个角落,图形严重变形。原因:geoToPixel 的 bounds 参数不是按所有钻孔的实际范围算的。常见两种:一种是 bounds 写死了固定的经纬度范围,和数据的投影坐标范围差了一万八千里,换算出来的像素全部落在一个小角落;另一种是 bounds 只覆盖了部分钻孔,范围偏小,后面的钻孔直接画到画布外。解决:每次加载数据后,遍历所有钻孔重新计算 minX/maxX/minY/maxY。我习惯把这个计算写成独立函数computeBounds(boreholes),在渲染前调用一次,而不是在 geo.js 里缓存一个固定 bounds。记住一个原则:坐标换算的范围必须来自数据本身,手动填的固定范围只适用于测试。5.2 岩层之间出现一条白缝现象:柱状图里相邻两层之间有一条 1 像素左右的白线,放大看很明显,像是两层没贴紧。原因:浮点精度问题。JSON 里上一层的 bottom 是 9.4,下一层的 top 也是 9.4,但经过 JavaScript 浮点运算,9.4 * scale和累加后的 currentY 可能差出零点几像素,恰好落在两个 fillRect 之间,把背景色露了出来。这是 Canvas 绘图的经典坑,第 3 章的currentY height累加写法在层数多时误差会累积。解决:绘制时不做浮点累积。不要用currentY height,而是每一层直接从layer.top * scale算起点、layer.bottom * scale算终点,再用Math.round()取整:bh.layers.forEach(function (layer) { const yTop Math.round(layer.top * scale); const yBottom Math.round(layer.bottom * scale); ctx.fillRect(startX, yTop, 40, yBottom - yTop); });这样相邻层的边界都取到同一个整数值,白缝自然消失。从那以后,我写任何基于 Canvas 的分层图,都默认用绝对坐标计算,不用累加。5.3 HTML 打开后中文全变乱码现象:双击 index.html,页面标题和地质数据里的层名全变成「锟斤拷」或乱码。原因:绝大多数情况是文件本身被 GBK 或 GB2312 编码保存过,而浏览器按 UTF-8 解码;也有可能是 HTML 里少了meta charsetUTF-8声明。这套源码的 data.js 直接写入了中文岩性名,data.js 的编码一旦不是 UTF-8,导入的字符串全是乱的。解决:用 VS Code 打开所有 JS 和 HTML 文件,看右下角编码,统一改成 UTF-8(带 BOM 更稳妥);确认 index.html 的 head 里有一行meta charsetUTF-8。这是最「低级但致命」的坑——功能逻辑全对,编码一错全盘皆输,而且报错不明显,有时候只是某个字显示不对,很难定位。我处理项目文件的第一步就是全量检查编码,省得后面排查半天。5.4 空数据直接白屏,控制台还报错现象:把 data.js 里的钻孔数组改成空数组,页面直接白屏,控制台报 Infinity 或者Cannot read property x of undefined。原因:render.js 渲染前会调 computeBounds,空数组的Math.min(...[])返回 Infinity,再拿 Infinity 去算 scale 得到 NaN,后续所有绘制全部失败。这是所有数据可视化系统的通病——只处理了「有数据」的正常路径,没处理空数据。解决:渲染入口先做数据校验,空数组就显示提示,不进入绘制流程。我会在 app.js 初始化时加一段:if (!Array.isArray(boreholes) || boreholes.length 0) { document.getElementById(infoPanel).innerHTML p暂无可展示的钻孔数据,请检查 data.js/p; return; // 不再执行任何 Canvas 绘制 }顺带说一句,判断数据类型时Array.isArray才是可靠方式,用typeof判断数组会得到 object,这是 JavaScript 新手最容易混淆的地方,在这个场景里吃一次亏就记住了。5.5 高分屏上 Canvas 模糊,标尺读数对不上现象:同样的代码,在普通笔记本上正常,在 Retina 屏的 MacBook 上整个剖面图发虚,点击画布高亮的钻孔位置总是偏的。原因:Canvas 的 width/height 是物理像素,而浏览器按 CSS 像素拉伸展示。高分屏的 devicePixelRatio 是 2,一个 CSS 像素对应 4 个物理像素,Canvas 内容被放大,自然模糊,点击坐标换算也因此错位。解决:创建画布时按 devicePixelRatio 缩放:function setupHiDPICanvas(canvas) { const dpr window.devicePixelRatio || 1; const rect canvas.getBoundingClientRect(); canvas.width rect.width * dpr; canvas.height rect.height * dpr; const ctx canvas.getContext(2d); ctx.scale(dpr, dpr); return ctx; }注意这样做之后,点击命中检测里不能再用 canvas.width 和 e.clientX 直接比,统一用canvas.getBoundingClientRect()的尺寸和 rect 坐标换算,别混用物理像素和 CSS 像素。我在一次外业成果汇报前遇到过这个坑,演示用的笔记本恰好是高分屏,图发虚看不清,从那以后我把高分屏适配写进了模板,每次新建画布都先调 setupHiDPICanvas。6. 进阶:用已知剖面做像素级验证与离线部署这套源码最容易被忽视的部分,是它缺少一个「验证渲染结果对不对」的环节。我拿到任何地质渲染源码,第一件事不是看效果,而是构造一个已知答案的剖面,去核对渲染结果。方法很简单:造两个钻孔,层位一模一样,深度都是整数,检查画出来的分界线像素是否和手工计算的完全一致。构造一个验证用例:const testData { boreholes: [ { id: T1, x: 0, y: 0, depth: 10, layers: [ { name: 黏土, top: 0, bottom: 5, color: #D2B48C }, { name: 砂层, top: 5, bottom: 10, color: #F0DEB4 } ] }, { id: T2, x: 100, y: 0, depth: 10, layers: [ { name: 黏土, top: 0, bottom: 4, color: #D2B48C }, { name: 砂层, top: 4, bottom: 10, color: #F0DEB4 } ] } ] };设画布 1200x600,bounds 为 x 方向 0~100、y 方向 0~10。scaleX 12,scaleY 60,取小者 scale 12。T1 黏土底界在深度 5 米,像素 Y (10 - 5) * 12 居中偏移 60 偏移。如果手工算出来是 180,渲染出来也是 180 ± 1,说明坐标换算没问题;如果差几十像素,那就是 Y 轴翻转或 bounds 算错了。这一招能把「看起来对不对」变成「算一算就知道对不对」,地质数据是讲精度的,不能靠肉眼验收。验证环节还有一个用途:换数据源。这套源码的数据是写死在 data.js 里的,真实项目要用后端数据。如果数据通过 fetch 加载,直接双击 index.html 打开,浏览器会报 CORS 错误,因为 file:// 协议下默认不允许跨文件读取。两个解决办法:一是在工程目录起一个临时静态服务器,比如python -m http.server 8080,然后访问 localhost:8080;二是把所有数据内联成 data.js 里的 JavaScript 变量,继续保持零依赖。我一般倾向后者,因为地质数据量不大,内联让交付更省事,甲方拿过去双击就能看,不用配环境。最后说一个习惯:每次改完 render.js 或 geo.js,我都强制把这个已知剖面跑一遍,把分界线像素坐标记在注释里,等于给核心算法留了回归测试。这套源码帮我把坐标换算、Canvas 绘制、事件命中这一整套流程捋顺了,尤其是 geoToPixel 里 Y 轴翻转那一步,让我记住「画图之前先想坐标系」。从那以后,我接到任何地质可视化需求,都会先翻这套源码里的函数,拿现成逻辑改一改再用,效率高很多。希望帮到你。本文还有配套的精品资源点击获取