第81篇:数据可视化-PM2.5分布——让圆点的胖瘦替每座城市报出浓度

上一篇我们把飞线的方向掉了个头,结尾埋了个话头:

CASE_LIST 里的下一站是数据可视化-PM2.5分布(#81)------从"线"换成"场":不再是一条条飞线,而是一整片连续分布的数值表面。表达方式又换一档,咱们下回再拆。

回顾一下这条线走到哪儿了:#79 注册了 GLMap 坐标系,让 ECharts 的经纬度转去调 Cesium 的投影(背面剔除用 dot(法线, 相机朝向) ≥ 0),再把 camera.percentageChanged 拧到 0.02、把 animation 关掉,图就跟着相机走了;#80 把拓扑从一对多反成多对一,桥接层一行没改。

这一篇,桥接层第三次零改动 ------ChartLayer、GLMapCoordSys、相机设置、覆盖层全部照搬,一个字没动。变的只有 buildOption() 和喂给它的数据。

但这次换掉的东西比上一篇更根本:series 类型从 lines 换成了 scatter 。前两篇画的是"关系"------A 到 B 有一条线,线本身是主角,点只是端点;这一篇画的是"场"------190 座城市各有一个数,谁也不连着谁,点自己成了主角,线彻底消失。

💡 本文涉及的全部代码均已开源,完整的仓库地址见文末,可自行取用、随意魔改。


一、从"线"到"场":同一套坐标系,第三张完全不同的图

先把这三篇摆在一起看,你会发现一件挺有意思的事:

篇号 series 类型 画的是什么 桥接层改动
#79 迁徙 lines ×3 + effectScatter ×3 + scatter ×3 30 条发散的弧线 从零写
#80 信息发送 lines ×1 + effectScatter ×1 + scatter ×1 30 条汇聚的弧线 0
#81 PM2.5 scatter ×1 + effectScatter ×1 190 个大小不一的圆点 0

同样是 30 条线,换的只是数据方向;同样是一个坐标系,换的连图表的种类都变了。 到第三篇,那句"坐标系负责投影、series 负责画什么"才算真正被验证过------GLMapCoordSys 里没有一行代码关心你画的是线还是点,它只回答一个问题:这个经纬度现在在屏幕的哪个像素上。

这一篇里,dataToPoint 被调了 190 次(190 个城市各一次),而且每次相机一动就全部重算一遍。前两篇只有几十个点,这次量级上去了,但桥接层不用为它做任何优化------因为投影本身很便宜,真正贵的是 ECharts 的重绘。

把视线从"画了什么"挪到"怎么投影"上,你才会意识到这套桥接层真正的价值:它是可复用的资产,不是一次性的胶水 。#79 写它、#80 一字不改沿用、#81 还是一字不改------三篇累计下来,你花在"让图表跟着地球转"上的代码量,被摊薄成了接近零。对比那些每接一张图就重写一遍投影逻辑的项目,差距在第三张图时才显出来。这也正是我反复在系列里强调"先搭坐标系、再填 series"的原因:坐标系是地基,series 是装修,地基别跟着装修一起拆。

"场"和"线"在数据组织上的差别也是根本性的:

ts 复制代码
// 线(#79/#80):一条记录 = 两个端点
{ coords: [[lng1, lat1], [lng2, lat2]] }

// 场(#81):一条记录 = 一个位置 + 一个值
{ name: '武汉', value: [lng, lat, pm25] }

注意 convertData() 那句 geoCoord.concat(item.value)------把 [经度, 纬度] 和 浓度 拼成三元组,所以 symbolSize 回调里拿到的 val[2] 才是浓度。这个"第 2 项"没有任何语义保护:哪天你在前面插一个字段,图会照样画出来,只是大小全错,而且不报错。

ts 复制代码
function convertData(data: Pm25Item[]): { name: string; value: number[] }[] {
  const res: { name: string; value: number[] }[] = []
  for (const item of data) {
    const geoCoord = pm25GeoCoordMap[item.name]
    if (geoCoord) {
      res.push({ name: item.name, value: geoCoord.concat(item.value) })
    }
  }
  return res
}

另外,这里的"场"其实是离散采样 :190 个城市是 190 个孤立观测点,中间没有任何插值。真正的连续场要上 heatmap 系列或栅格插值(克里金、IDW),那是另一个话题------散点气泡图的诚实边界是"我只有这 190 个数,中间的我自己也不知道"。

顺便说一句数据质量:190 条数据和 190 条坐标严丝合缝对上了 ,一条都没被 if (geoCoord) 过滤掉(对比 #80 那个"目标名写错 → 全部被 filter → 一条线都不画且零报错"的坑,这份数据是干净的)。不过坐标精度是混的:170 条只给了 2 位小数 (约 1.1 km 量级),另外 20 条给了 6 位小数(看着像从地理编码服务批量取的)。在全国视野下这点误差无所谓(2 位小数 ≈ 0.15 px),但你要是放大到一个城市看,混精度就会露馅。


二、先量一遍这 190 个数:中位数 54,最脏的 279

画图之前先把数据摸清楚,这是我写这类文章养成的习惯------不量一遍就配参数,等于闭着眼睛调色。探针跑了下面这一组:

统计量 值(µg/m³)
最小值 9(海门)
下四分位 p25 36
中位数 54
上四分位 p75 86
最大值 279(大庆)
均值 67.39
标准差 46.58
极差 270

均值 67.39 明显大于中位数 54------右偏,说明有那么十几个城市把尾巴拉得很长。这个形状后面会直接影响"圆点大小"该怎么映射,先记住。

按国标的分级粗粗切一刀(35 / 75 / 115 / 150 / 250):

等级 城市数 占比 累计
优(< 35) 45 23.7% 23.7%
良(35~75) 85 44.7% 68.4%
轻度污染(75~115) 32 16.8% 85.3%
中度污染(115~150) 16 8.4% 93.7%
重度污染(150~250) 10 5.3% 98.9%
严重污染(≥ 250) 2 1.1% 100.0%

68.4% 的城市在"良"以上,真正严重的只有 2 座(大庆 279、武汉 273)。 这个分布对可视化有个直接暗示:大部分点会挤在小尺寸那一端,少数几个点会非常大------也就是后文要说的"极端值主导画面"。

右偏还有第二个后果,提前说一句,到第四节末尾还会回来:它意味着如果你用一条"线性的"视觉通道(比如颜色从浅到深)去编码浓度,那么绝大多数城市会挤在色带最浅的那一格 ------因为中位数 54 和最大值 279 差了五倍多,线性色阶会把中间那一大片全压进同一个浅色调里。空气质量数据最该让人一眼看出的"分级",反而会被线性编码抹平。所以后文要补 visualMap,一定得用分段(离散)色带,不能信手用连续渐变。

浓度最高的十二座城市(≥ 150):

复制代码
大庆 279 | 武汉 273 | 合肥 229 | 菏泽 194 | 廊坊 193 | 衢州 177
长沙 175 | 岳阳 169 | 金华 157 | 湘潭 154 | 保定 153 | 常德 152

记住最后这几个名字------它们在第四节和第五节会反复出现,因为它们既是数值最大的那批,也是地理位置最挤的那批。


三、两个 series:一个铺底,一个点睛

buildOption() 只返回两个 series,非常简单:

ts 复制代码
function buildOption(): Record<string, unknown> {
  const all = convertData(pm25Data)                                    // 190 个
  const top6 = [...all].sort((a, b) => b.value[2] - a.value[2]).slice(0, 6)
  return {
    series: [
      { name: 'pm2.5', type: 'scatter', coordinateSystem: 'GLMap',
        data: all, symbolSize: (val: number[]) => val[2] / 10,
        label: { normal: { formatter: '{b}', position: 'right', show: false },
                 emphasis: { show: true } },
        itemStyle: { normal: { color: '#ddb926' } } },
      { name: 'Top 5', type: 'effectScatter', coordinateSystem: 'GLMap',
        data: top6, symbolSize: (val: number[]) => val[2] / 10,
        showEffectOn: 'render', rippleEffect: { brushType: 'stroke' },
        hoverAnimation: true,
        label: { normal: { formatter: '{b}', position: 'right', show: true } },
        itemStyle: { normal: { color: '#f4e925', shadowBlur: 10, shadowColor: '#333' } },
        zlevel: 1 }
    ]
  }
}

第一个 series 是底 :190 个普通 scatter,颜色统一 #ddb926(暗黄),不常显标签。

第二个 series 是眼睛 :Top6 用 effectScatter(带涟漪扩散动画),颜色提亮到 #f4e925,加了 shadowBlur 投影,常显城市名 ,并且 zlevel: 1。

zlevel: 1 是个好设计,别删

ECharts 里 zlevel 不同的 series 会被画到不同的 canvas 图层 上,zlevel 大的浮在上面。Top6 设了 1,底下的 190 个点是 0,于是高亮的那六个点永远压在最上层------不会被人多势众的底图点盖住,它们的名字也不会被别的标签遮掉。

这是本篇里我最愿意抄走的一招:用 zlevel 给"重点要素"单独开一层 ,比在同一个图层里调 z(绘制次序)更可靠,因为 z 只在同一层内排序。

回到两个 series 的分工本身:底图 190 个点用最朴素的 scatter,不常显标签、不带动画,纯铺底;Top6 用 effectScatter,常显名字、带涟漪、单独开层。这个"铺底 + 点睛"的两段式,是数据量大又想突出重点时的标准套路------让 184 个普通点安静地待在背景里,把读者的注意力用动画和图层"请"到那 6 个最关键的点上。代价你也看得到:底图点小到读不出数,所以"铺底"本质上是在牺牲个体可读性换全局可读性,和第四节讲的尺寸映射是同一个权衡的两面。

两个"名字对不上"的小毛病

其一:name: 'Top 5',但 slice(0, 6) 取了六个。 系列名和实际条目数不一致,纯文字问题,但在排查"为什么图上有六个点却叫 Top5"的时候会浪费人五分钟。

其二:Top6 的门槛薄得像纸。 第 6 名衢州 177,第 7 名长沙 175------只差 2 µg/m³ 。也就是说,这份数据里"进不进高亮名单"取决于一个几乎等于测量误差的差值。真要上生产,这里的取舍应该是"按阈值切"(比如 ≥ 150 的全部高亮,共 12 座)而不是"按名次切"------阈值切出来的分组有物理意义,名次切出来的没有。

把这两个小毛病放在一起看,它们暴露的是同一类问题:这份 demo 的代码是"能跑"优先,不是"好维护"优先 。名字和实际数对不上、阈值切和名次切混用,单独看都不致命,可一旦你要改------比如"把高亮名单改成按阈值切"------你就得先猜清楚 slice(0, 6) 和 name: 'Top 5' 到底哪个是真相。我写这类文章一贯的做法是:凡是会影响读者理解或后续修改的命名,一律让它和代码行为严丝合缝,宁可多花五分钟,省下后来人五个小时。

三个写了等于没写的配置

  • showEffectOn: 'render' ------ 这就是 effectScatter 的默认值;
  • hoverAnimation: true ------ 这也是默认值,而且在这个场景里根本触发不了(见第五节坑二);
  • rippleEffect: { brushType: 'stroke' } ------ 只有 brushType 改了(默认 'fill'),period / scale / number 全用默认 4 / 2.5 / 3(这三个默认值我上一篇已经从源码里核对过了)。

顺带记一笔:rippleEffect 这套默认值是 effectScatter 专属的------你要是手滑把它写在一个普通 scatter 上,ECharts 会安静地忽略,不报错、不画图、也不提醒你写错地方。这也是 ECharts 这类"配置即代码"框架的通病:写错位置的属性不会红,只会被默默吃掉。排错时最该怀疑的,往往是这些"看起来写了其实没生效"的配置。


四、symbolSize = value / 10:一条除式把差异放大了三十一倍

终于到本篇的核心了。整张图只有一条式子决定"每个点多大":

ts 复制代码
symbolSize: (val: number[]) => val[2] / 10

ECharts 的 symbolSize 是直径 ,所以直径 D = v / 10。代入实测的两个端点:

  • 最干净的海门 v = 9 → D = 0.90 px
  • 最脏的大庆 v = 279 → D = 27.90 px

这里有个容易忽略的前提:symbolSize 给出的是 ECharts 自己画布上的像素尺寸 ,而本篇的画布和 Cesium 的画布是 1:1 叠在一起的(覆盖层宽高都等于 canvas.clientWidth/clientHeight),所以这些 px 直接就是屏幕 px------点多大,读者就看到多大,不受相机远近影响 。这点和 #70 那种"贴 billboard、用 scaleByDistance 随距离缩放"的做法恰好相反:那篇的圆图会随你拉远而缩小,本篇的圆点永远是一个绝对尺寸。绝对尺寸的好处是读数稳定,坏处是密集时你没法靠拉远来"散开"团簇------这又绕回了坑一。

直径比 31.0 倍,半径比也是 31.0 倍。而眼睛看到的是面积------面积比 31.0² = 961.0 倍。

这就是 #76 讲比例符号面时专门对比过的两条路:

  • 面积正比 r = √(v·F/π):面积比 = 数值比,是制图学的默认选择;
  • 半径正比 r = v·k:面积 ∝ v²,把差异按平方放大。

本篇选的是后者(k = 1/20,因为半径 = D/2 = v/20)。我把两条曲线在最大值处对齐(即令 v = 279 时两者直径都是 27.9 px),实测各档位:

浓度 v 半径正比(本篇) 面积正比 面积正比 / 半径正比
9 0.90 px 5.01 px 5.568×
50 5.00 px 11.81 px 2.362×
100 10.00 px 16.70 px 1.670×
279 27.90 px 27.90 px 1.000×

对齐到最大值之后,除了最大值那一点,半径正比画出的圆全都更小,而且数值越小被压得越狠。 在 v = 9 那一端,面积正比会给出 5.01 px 的可见圆点,半径正比只给 0.90 px------不到一个像素。

把这条公式反过来,读者其实能在图上读出近似值:因为 直径 = 浓度 / 10,所以看到一个点直径约 15 px,就能反推它的浓度约 150 ------这是个很友好的线性关系,比面积反推(要开平方)省事得多。但便利的另一面是陷阱:上面那个反推只在"你量得准直径"时成立,而 0.90 px 那种点你根本量不出直径,只能看到"有个东西在那儿"。换句话说,半径正比给了你一个好算的读数公式,却又悄悄让半数城市小到读不出数------便利和失真同源。

这不是理论推演,是画面上真的看不见。 实测各尺寸档位的城市数:

直径 城市数 占比
< 1 px 1 0.5%
< 2 px 10 5.3%
< 3 px 31 16.3%
< 5 px 83 43.7%
< 10 px 153 80.5%
≥ 10 px 37 19.5%

八成的城市直径不到 10 px,四成四不到 5 px。 于是画面成了这个样子:绝大多数城市是勉强可见的小米粒,少数几个城市是硕大的圆饼,中间层次几乎空掉------而中位数 54(占 279 的 19.4%)对应的圆点只有 5.4 px,和最大值 27.9 px 之间隔着一条巨大的视觉鸿沟。

什么时候该选半径正比

不全是坏事。如果你的目的是"让极端值一眼跳出来",半径正比反而是对的:它把 279 和 54 的差距(5.2 倍)放大成面积上的 26.6 倍,读者第一眼一定先看到大庆和武汉。做预警、做排行榜、做"哪里最严重"的引导,这是有效的。

但如果读者需要比较数值,它就是错的------面积按平方放大之后,人眼对面积的感知又本来就偏弱,两个圆看起来"差不多大"时,真实数值可能差了一倍。

这也是为什么教科书上的"比例符号图"几乎一律用面积正比:它牺牲一点"极端值跳出来"的冲击力,换来了"任意两点都能公平比较"。本篇反过来选了半径正比,所以它在"预警"上加分、在"对比"上减分------没有绝对的对错,只有场景合不合适。

判断标准很简单:问一句"读者要排序还是要读数" 。要排序(找最值)→ 半径正比可以;要读数(估大小)→ 必须面积正比,而且要配色阶(visualMap)和图例。

本篇还有一个更根本的缺失:颜色没编码任何信息

190 个底图点全是 #ddb926 一个颜色,Top6 全是 #f4e925。整张图里,颜色不携带任何数据,只有尺寸携带。

这意味着:① 你没法从图上看出一个点的浓度是 60 还是 90(它们的直径只差 3 px);② 分级(优/良/轻度/...)在图上完全不可见,而分级恰恰是空气质量数据最该传达的信息。

补法很便宜:加一个 visualMap,把 35 / 75 / 115 / 150 / 250 做成分段色带,再把 symbolSize 换成面积正比------同样是 190 个点,信息量立刻翻倍,代码只多十行。

顺带说,尺寸和颜色是两条正交 的编码通道,加了 visualMap 不会和 symbolSize 打架------一个点可以同时"又大又红"。这正是专题地图的常规做法:大小表量级、颜色表等级,读者一眼既能排序(看大小)又能读数(看色阶)。这篇 demo 只用了大小一条通道,等于把一半的信息承载力闲置了。


五、会糊的三处,和点了也没用的三处

坑一:48 对城市在画面上真的叠在一起

把 190 个点按 Rectangle.fromDegrees(73, 18, 135, 54) 的视野铺开(900 px 宽),逐对算圆心距和半径和,实测:

圆心距小于半径和的城市对 = 48 对。 重叠最严重的几对:

城市对 重叠量 实际距离
湘潭 -- 长沙 11.3 px 34.0 km
株洲 -- 湘潭 11.2 px 24.5 km
株洲 -- 长沙 9.9 px 39.8 km
义乌 -- 金华 7.7 px 44.9 km
北京 -- 廊坊 7.0 px 44.2 km

长株潭(长沙 / 株洲 / 湘潭)三座城市两两相距 24~40 km,而它们的直径分别是 17.5 / 15.7 / 15.4 px ------在这个视野下它们必然糊成一团。偏偏这三座的浓度都不低(175 / 157 / 154,全在"重度污染"档),也就是说图上最该被看清的三座城市,恰恰是糊得最厉害的三座。

有意思的是,把所有圆的面积加起来只有 10015 px²,占画面 2.13% ------整体一点都不挤,挤的是局部。这正是气泡图的典型病灶:稀疏地区浪费空间,密集地区糊成一片。

为什么"挤的局部"几乎都在同一片?我把 190 个点的经度拉出来看:81.1% 的城市(154 座)落在东经 110° 以东,经度小于 100°E 的只有 5 座 。这正贴合胡焕庸线------中国的人口和城镇本来就压在东半壁,经纬度投影又是"度数等距"地铺开,于是城市物理上扎堆的地方,屏幕上必然也扎堆。长株潭、京津冀、长三角这三处团簇,本质上不是图的问题,是数据分布的问题 ;想靠调 symbolSize 把团簇解开,等于和地理较劲。

能缓解的办法,按代价从低到高:① 把 symbolSize 整体调小(治标,小点更看不见);② 给底图点加半透明 + 描边,重叠处能看出层次;③ 换 heatmap 系列, contiguous 的密度面天然不会出现"两个圆叠在一起"这种问题;④ 做聚合(按网格或按省归并)。真要上生产我倾向 ③。

坑二:覆盖层的 pointer-events: none 让 hover 全废

ChartLayer.createChartElement() 里那句:

ts 复制代码
el.style.cssText = `position:absolute; top:0; left:0; ...; pointer-events:none;`

pointer-events: none 对所有后代元素生效 (ECharts 自己创建的 canvas 不会覆盖这个属性)。后果是:鼠标事件一律穿透到 Cesium 的 canvas 上,ECharts 收不到任何事件。

于是下面两处配置成了死代码:

ts 复制代码
// 底图 series:hover 才显示城市名 ------ 永远不会触发
label: { normal: { ..., show: false }, emphasis: { show: true } }

// Top6 series:鼠标悬停动画 ------ 同样永远不会触发
hoverAnimation: true

更可惜的是 tooltip。 气泡图最需要的交互就是"指一下这个圆,告诉我这座城市是多少",而这个桥接层下 tooltip 根本不可能出现。

要修也简单:把覆盖层改成 pointer-events: none,但给 ECharts 的 canvas 单独设 auto------代价是 Cesium 的相机手势会在图表区域被吃掉。这两件事目前是互斥的,只能按业务选一个:要交互就牺牲一部分地图操作,要地图流畅就放弃 hover。

为什么覆盖层默认要写 pointer-events: none?因为 ECharts 那块 div 是严丝合缝盖在 Cesium canvas 上的,如果它吃事件,你拖地图时只要鼠标落在图表区就拖不动------所以这个 none 是为了保住地图操作、牺牲图表交互的取舍,不是疏漏。要交互的那条路,就是针对性地把图表区的 pointer-events 设回 auto,代价是这一小块地面上相机手势失效。

(顺带一句:#80 那个"图例点不动"也是同一个根因,本篇没有图例,所以那里的问题在这里不存在。)

坑三:label: { normal: ... } 是 ECharts 4 的写法

本篇两处都用了 label: { normal: {...}, emphasis: {...} } 这种嵌套。ECharts 从 4.0 起就取消了 normal 层级,源码里现在还留着一段兼容:

js 复制代码
// echarts/dist/echarts.js L20493-20516
if (opt[optType].normal) {
  deprecateLog("'normal' hierarchy in " + optType + " has been removed since 4.0.");
  opt[optType] = normalOpt;          // 把 normal 的内容提到上一层
}

所以它能用,但会在开发构建里打废弃日志,而且哪天兼容层被删掉就直接失效。 新代码应该写成:

ts 复制代码
label: { show: true, formatter: '{b}', position: 'right' },
emphasis: { label: { show: true } }

注意 emphasis 也从"嵌套在 label 里"挪到了顶层 ,并且要写成 emphasis.label。本篇这种老写法里,label.emphasis 会被兼容层搬到 emphasis.label 去------两边最终效果一样,但读代码的人会以为 emphasis 是 label 的属性。

剩下的小事

  • symbolSize 回调只用于取 val[2] ,没有下界保护。浓度 0 的城市会得到 0 px 的圆(ECharts 会画成一个看不见的点),建议写 Math.max(2, val[2] / 10);本篇数据最小值是 9,所以这条保护没被触发,但作为通用组件它必须存在------否则你接任意一个含 0 值的数据源,都会凭空冒出一个看不见却占着坐标的点。
  • 没有 visualMap、没有 legend、没有 tooltip、没有 title------四件套全缺,读者除了"大就是脏"之外得不到任何定量信息;
  • 相机视野是写死的 Rectangle.fromDegrees(73, 18, 135, 54),改数据范围时要记得同步改;
  • percentageChanged = 0.02 和 animation: false 照搬自 #79------这两条在 190 个点上比在 30 条线上更重要,因为重投影的量翻了好几倍。

六、写在最后

把这一篇收一下,其实就三句话。

第一,桥接层到第三篇还是零改动,这次连 series 类型都从 lines 换成了 scatter。 GLMapCoordSys 从头到尾只做一件事------回答"这个经纬度现在在屏幕的第几个像素上",它不需要知道你拿这些像素去画线还是画点。这是这三篇最值钱的东西:把"投影"和"画什么"彻底分开之后,换一张图真的只是换 buildOption()。

第二,symbolSize: val[2] / 10 这十个字符,决定了整张图传达什么。 它是半径正比,把 31 倍的浓度差画成了 961 倍的面积差,代价是八成的城市缩到 10 px 以下、最干净那座只有 0.90 px。这不是 bug,是一个没被说出来的取舍 ------它让极端值一眼跳出来(适合预警),同时让读者读不出中间那些城市的数值(不适合分析)。想两头都要,就把 symbolSize 换成面积正比,再补一条 visualMap 色带。

第三,这张图真正的短板不在代码,在"颜色什么都没编码"。 190 个点一个颜色,分级信息全丢,而空气质量数据最有价值的恰恰是分级。补一个 visualMap 十行代码,信息量翻倍。

CASE_LIST 里的下一站是洪水淹没模拟(#82)------从"把观测值画成点"换到"把一个过程演成动画":这回不再是静态的场,而是随时间推进的水面。

如果你也在做这类"数据铺到地球上"的可视化,欢迎留言聊聊:你的气泡图是半径正比还是面积正比?有没有被"密集区糊成一团"难住过?


🌟 项目已开源,欢迎 Star!(你的 Star ⭐ 就是对我最大的支持!)

仓库地址:https://github.com/jianlei-wang/CESIUM-TREASURE-BOX

代码在 src/cases/chart-pm25/ 目录下,和 #79、#80 共用 src/lib/cesium-chart.ts,核心只有 ChartPm25Demo.vue 和一份 data.ts。搬去自己项目时,盯住四件事:① 桥接层完全复用 (ChartLayer + GLMap 一个字都不用改,换图只改 buildOption());② symbolSize 决定的是直径 ,写的时候想清楚你要半径正比还是面积正比;③ convertData() 拼出来的三元组里,浓度在第 2 项 ,val[2] 这个下标没有语义保护;④ 覆盖层是 pointer-events: none,hover / tooltip 全部用不了,要交互就得改这一行并接受地图手势被吃掉。


Cesium酱の百宝箱 · 第 81 篇

圆点胖瘦藏浓淡,桥接三番未改行

2026 年的秋夜,与你共探「同一套坐标系,第三张完全不同的图」

相关推荐
IT_陈寒1 小时前
Vue的v-for里用了个临时变量,页面直接挂了
前端·人工智能·后端
hunter_0761 小时前
一次 iOS 真机签名 409 的完整排查:证书、设备、描述文件为什么总是"对不上"
前端
枣把儿1 小时前
开源!公众号排版+一键发布工具懒人版
前端
2501_916007471 小时前
抓包工具功能对比,TraceEagle、Charles、Fiddler、Wireshark、mitmproxy 差别
前端·网络协议·ios·adb·https·fiddler·wireshark
Flynt1 小时前
AI 写的页面一眼假?有人把"假指纹"写成 62 条规则,不用大模型就能跑
前端·ai编程·claude
kyriewen2 小时前
我把 10 道面试题答案真跑了一遍:6 道是错的
前端·javascript·面试
WebInfra2 小时前
Rspack 2.3 发布:联手 SWC 团队带来全新编译器
前端·javascript·github
小卿噢3 小时前
同样是插 10 万行,最快的写法比最慢的快 103 倍
前端
weixin_417396833 小时前
星级酒店境外电视频道怎么合法接入?合规要点与核对清单
开发语言·前端·php·1024程序员节·综合布线