r(jià)展示系統(tǒng)實(shí)戰(zhàn):從數(shù)據(jù)清洗到地圖渲染的完整技術(shù)方案)
簡(jiǎn)介這份資源是面向高校WebGIS課程學(xué)習(xí)者的期末大作業(yè)完整方案以青島市房?jī)r(jià)分析展示系統(tǒng)為案例適合需要完成課程設(shè)計(jì)、期末項(xiàng)目或想快速上手WebGIS開(kāi)發(fā)的新手與進(jìn)階學(xué)生。壓縮包共16個(gè)文件約7.25MB包含7個(gè)html頁(yè)面、4張png圖片、1個(gè)pptx演示文稿、1個(gè)css樣式表、1個(gè)js腳本、1個(gè)md說(shuō)明文檔和1個(gè)docx實(shí)驗(yàn)報(bào)告覆蓋系統(tǒng)頁(yè)面、圖表展示、熱力圖、緩沖分析、留言模塊及樣式資源等核心內(nèi)容。目前已有441人學(xué)習(xí)下載說(shuō)明該方案在同類課程作業(yè)中具有較高參考價(jià)值。代碼注釋完整部署簡(jiǎn)單下載后即可運(yùn)行使用能幫助讀者快速理解WebGIS房?jī)r(jià)展示系統(tǒng)的整體架構(gòu)與實(shí)現(xiàn)思路同時(shí)配套實(shí)驗(yàn)報(bào)告與演示文稿便于直接用于課程設(shè)計(jì)答辯或作為二次開(kāi)發(fā)的基礎(chǔ)模板具有較高的實(shí)際應(yīng)用與學(xué)習(xí)借鑒價(jià)值。1. 房?jī)r(jià)展示系統(tǒng)到底在做什么從一份課程大作業(yè)說(shuō)起很多人第一次接觸 WebGIS 課程大作業(yè)選題都會(huì)落到「房?jī)r(jià)展示系統(tǒng)」上。原因很直接數(shù)據(jù)好找、地圖好看、答辯時(shí)老師一眼就能看懂你在做什么。但真正動(dòng)手之后才會(huì)發(fā)現(xiàn)這個(gè)題目遠(yuǎn)不是「把點(diǎn)標(biāo)在地圖上」那么簡(jiǎn)單。它背后是一條完整的技術(shù)鏈路——數(shù)據(jù)清洗、坐標(biāo)糾偏、空間索引、前后端接口、地圖渲染、交互篩選每一環(huán)都有坑。我見(jiàn)過(guò)太多同學(xué)把 GeoJSON 直接丟給前端結(jié)果兩萬(wàn)條數(shù)據(jù)一加載瀏覽器就卡死也見(jiàn)過(guò)坐標(biāo)沒(méi)糾偏房子全飄到海里去的翻車現(xiàn)場(chǎng)。這篇文章面向三類人正在做 WebGIS 課程大作業(yè)的學(xué)生、想快速搭一個(gè)房?jī)r(jià)可視化 Demo 的開(kāi)發(fā)者、以及需要一套可復(fù)現(xiàn)技術(shù)方案來(lái)評(píng)估可行性的從業(yè)者。我會(huì)把「房?jī)r(jià)展示系統(tǒng)」拆成能落地的步驟從數(shù)據(jù)準(zhǔn)備到前端渲染從空間查詢到性能優(yōu)化每一步都給出可抄作業(yè)的代碼和參數(shù)說(shuō)明。你不需要有 GIS 專業(yè)背景但需要會(huì)一點(diǎn) Python 和 JavaScript。讀完你應(yīng)該能獨(dú)立跑通一個(gè)帶篩選、聚合、熱力圖的房?jī)r(jià)展示系統(tǒng)并且知道哪些地方最容易翻車。2. 數(shù)據(jù)從哪來(lái)、怎么洗房?jī)r(jià)數(shù)據(jù)的獲取與預(yù)處理2.1 房?jī)r(jià)數(shù)據(jù)的常見(jiàn)來(lái)源與字段設(shè)計(jì)課程大作業(yè)的數(shù)據(jù)來(lái)源通常有三類公開(kāi)數(shù)據(jù)平臺(tái)下載、爬蟲(chóng)抓取、老師直接給一份 CSV。不管哪種你拿到的原始數(shù)據(jù)大概率長(zhǎng)這樣小區(qū)名稱、掛牌單價(jià)、總價(jià)、面積、戶型、樓層、經(jīng)緯度、所屬區(qū)域。字段看似齊全但真正能用的沒(méi)幾個(gè)。我一般會(huì)先做一輪字段審計(jì)把下面這些列出來(lái)字段是否必需常見(jiàn)問(wèn)題經(jīng)緯度必需缺失、精度不夠、坐標(biāo)系不統(tǒng)一單價(jià)必需單位混亂元/平 vs 萬(wàn)元/平總價(jià)可選大量空值面積可選區(qū)間值如「80-100平」小區(qū)名可選重復(fù)、別名區(qū)域可選與行政區(qū)劃對(duì)不上字段審計(jì)做完你會(huì)發(fā)現(xiàn)真正能支撐空間展示的核心字段只有經(jīng)緯度和單價(jià)。其他字段要么缺失嚴(yán)重要么需要額外清洗。我的建議是先把經(jīng)緯度和單價(jià)這兩列做到 100% 可用再考慮加其他維度。2.2 用 Python 做數(shù)據(jù)清洗的最小腳本下面這段腳本是我常用的清洗模板處理缺失值、單位統(tǒng)一、異常值過(guò)濾三件事import pandas as pd import numpy as np # 讀取原始數(shù)據(jù)注意編碼問(wèn)題中文 CSV 常見(jiàn) gbk df pd.read_csv(raw_house.csv, encodinggbk) # 1. 經(jīng)緯度缺失直接丟棄這兩列沒(méi)法補(bǔ) df df.dropna(subset[lng, lat]) # 2. 單價(jià)統(tǒng)一成 元/平米 # 如果原始單位是萬(wàn)元/平乘以 10000 if df[unit_price].median() 1000: df[unit_price] df[unit_price] * 10000 # 3. 過(guò)濾異常值單價(jià)低于 2000 或高于 200000 的基本是錯(cuò)誤數(shù)據(jù) df df[(df[unit_price] 2000) (df[unit_price] 200000)] # 4. 經(jīng)緯度范圍校驗(yàn)中國(guó)范圍大致 lng 73-135, lat 3-54 df df[(df[lng] 73) (df[lng] 135)] df df[(df[lat] 3) (df[lat] 54)] # 5. 去重按經(jīng)緯度單價(jià)組合去重 df df.drop_duplicates(subset[lng, lat, unit_price]) print(f清洗后剩余 {len(df)} 條記錄) df.to_csv(clean_house.csv, indexFalse, encodingutf-8-sig)這段腳本的邏輯很直白先丟掉沒(méi)法補(bǔ)的經(jīng)緯度缺失再統(tǒng)一單價(jià)單位然后用硬閾值過(guò)濾明顯錯(cuò)誤的數(shù)據(jù)最后去重。參數(shù)方面單價(jià)上下限 2000 和 200000 是根據(jù)國(guó)內(nèi)大部分城市行情定的如果你做的是小城市下限可以調(diào)到 1000。經(jīng)緯度范圍用的是中國(guó)大致邊界如果你只做某個(gè)城市可以進(jìn)一步收窄到該城市的 bounding box這樣能過(guò)濾掉坐標(biāo)糾偏失敗導(dǎo)致的漂移點(diǎn)。注意編碼問(wèn)題是最容易被忽略的坑。Windows 下 Excel 導(dǎo)出的 CSV 默認(rèn) gbk直接 utf-8 讀取會(huì)報(bào) UnicodeDecodeError。我一般先試 utf-8失敗就換 gbk再失敗用 chardet 檢測(cè)。2.3 坐標(biāo)系糾偏為什么你的點(diǎn)會(huì)飄到海里國(guó)內(nèi)地圖數(shù)據(jù)涉及兩套坐標(biāo)系WGS84GPS 原始坐標(biāo)和 GCJ02國(guó)測(cè)局加密坐標(biāo)。高德、騰訊地圖用的是 GCJ02百度用的是 BD09而很多公開(kāi)數(shù)據(jù)給的是 WGS84。如果你不糾偏直接把 WGS84 的點(diǎn)標(biāo)在高德地圖上偏移量大概在幾百米密集區(qū)域看起來(lái)就像飄到海里或隔壁小區(qū)。糾偏的常見(jiàn)做法是調(diào)用現(xiàn)成的轉(zhuǎn)換庫(kù)。Python 里可以用coord-convert或自己寫(xiě)一個(gè)簡(jiǎn)版轉(zhuǎn)換函數(shù)。下面是一個(gè) GCJ02 轉(zhuǎn) WGS84 的近似實(shí)現(xiàn)import math def gcj02_to_wgs84(lng, lat): GCJ02 轉(zhuǎn) WGS84近似算法誤差在米級(jí) a 6378245.0 ee 0.00669342162296594323 def transform_lat(x, y): ret -100.0 2.0*x 3.0*y 0.2*y*y 0.1*x*y 0.2*math.sqrt(abs(x)) ret (20.0*math.sin(6.0*x*math.pi) 20.0*math.sin(2.0*x*math.pi)) * 2.0/3.0 ret (20.0*math.sin(y*math.pi) 40.0*math.sin(y/3.0*math.pi)) * 2.0/3.0 ret (160.0*math.sin(y/12.0*math.pi) 320*math.sin(y*math.pi/30.0)) * 2.0/3.0 return ret def transform_lng(x, y): ret 300.0 x 2.0*y 0.1*x*x 0.1*x*y 0.1*math.sqrt(abs(x)) ret (20.0*math.sin(6.0*x*math.pi) 20.0*math.sin(2.0*x*math.pi)) * 2.0/3.0 ret (20.0*math.sin(x*math.pi) 40.0*math.sin(x/3.0*math.pi)) * 2.0/3.0 ret (150.0*math.sin(x/12.0*math.pi) 300.0*math.sin(x/30.0*math.pi)) * 2.0/3.0 return ret dlat transform_lat(lng - 105.0, lat - 35.0) dlng transform_lng(lng - 105.0, lat - 35.0) radlat lat / 180.0 * math.pi magic math.sin(radlat) magic 1 - ee * magic * magic sqrtmagic math.sqrt(magic) dlat (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * math.pi) dlng (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * math.pi) return lng - dlng, lat - dlat這個(gè)函數(shù)的參數(shù)就是經(jīng)緯度返回糾偏后的經(jīng)緯度。誤差在米級(jí)對(duì)于房?jī)r(jià)展示這種場(chǎng)景完全夠用。如果你用的是百度地圖還需要多做一步 BD09 和 GCJ02 之間的轉(zhuǎn)換。實(shí)際項(xiàng)目中我一般會(huì)在數(shù)據(jù)清洗階段就把所有坐標(biāo)統(tǒng)一成 GCJ02這樣前端不管用高德還是騰訊都不用再處理。3. 后端怎么搭從 CSV 到空間查詢接口3.1 技術(shù)選型為什么我選 Flask PostGIS 而不是純前端很多同學(xué)的第一反應(yīng)是前端直接讀 GeoJSON 不就行了小數(shù)據(jù)量確實(shí)可以但房?jī)r(jià)數(shù)據(jù)動(dòng)輒上萬(wàn)條GeoJSON 文件輕松超過(guò) 10MB瀏覽器加載和渲染都會(huì)卡。更關(guān)鍵的是你需要按價(jià)格區(qū)間、區(qū)域、面積等條件篩選純前端做篩選意味著每次都要遍歷全量數(shù)據(jù)體驗(yàn)很差。我的選型是 Flask 做 API 層PostGIS 做空間存儲(chǔ)和查詢。PostGIS 是 PostgreSQL 的空間擴(kuò)展支持空間索引、距離查詢、聚合統(tǒng)計(jì)這些能力在房?jī)r(jià)展示里非常實(shí)用。比如「查詢某個(gè)點(diǎn) 2 公里內(nèi)的所有小區(qū)均價(jià)」這種需求PostGIS 一條 SQL 就能搞定純前端做要寫(xiě)一堆幾何計(jì)算。如果你不想裝 PostgreSQLSQLite SpatiaLite 也能湊合但空間查詢性能和函數(shù)豐富度差不少。課程大作業(yè)的話我建議直接上 PostGIS裝一次一勞永逸。3.2 建表和導(dǎo)入數(shù)據(jù)的完整流程先建一張帶空間字段的表CREATE TABLE house ( id SERIAL PRIMARY KEY, name VARCHAR(100), unit_price NUMERIC(10, 2), area NUMERIC(8, 2), district VARCHAR(50), geom GEOMETRY(Point, 4326) ); -- 建空間索引這是查詢性能的關(guān)鍵 CREATE INDEX idx_house_geom ON house USING GIST(geom);導(dǎo)入 CSV 數(shù)據(jù)可以用 Python 的 psycopg2import psycopg2 import pandas as pd conn psycopg2.connect( hostlocalhost, databasewebgis, userpostgres, passwordyour_password ) cur conn.cursor() df pd.read_csv(clean_house.csv) for _, row in df.iterrows(): cur.execute( INSERT INTO house (name, unit_price, area, district, geom) VALUES (%s, %s, %s, %s, ST_SetSRID(ST_MakePoint(%s, %s), 4326)), (row[name], row[unit_price], row[area], row[district], row[lng], row[lat]) ) conn.commit() cur.close() conn.close()這里的關(guān)鍵是ST_MakePoint和ST_SetSRID。前者根據(jù)經(jīng)緯度生成點(diǎn)幾何后者指定坐標(biāo)系為 4326WGS84。注意如果你前面已經(jīng)糾偏成 GCJ02這里應(yīng)該用 4326 存儲(chǔ)但實(shí)際是 GCJ02 坐標(biāo)前端渲染時(shí)直接用高德底圖即可??臻g索引GIST是必須建的否則幾萬(wàn)條數(shù)據(jù)的范圍查詢會(huì)慢到無(wú)法接受。3.3 三個(gè)核心查詢接口的 SQL 與參數(shù)說(shuō)明房?jī)r(jià)展示系統(tǒng)最常用的查詢有三類按范圍查點(diǎn)、按條件篩選、按區(qū)域聚合。下面分別給出 SQL 和對(duì)應(yīng)的 Flask 路由。范圍查詢給定地圖當(dāng)前視野的 bounding box返回范圍內(nèi)的點(diǎn)。SELECT id, name, unit_price, area, district, ST_X(geom) as lng, ST_Y(geom) as lat FROM house WHERE geom ST_MakeEnvelope(%s, %s, %s, %s, 4326) LIMIT 2000;ST_MakeEnvelope接收左下角和右上角經(jīng)緯度是空間索引運(yùn)算符能快速過(guò)濾出相交的幾何。LIMIT 2000是保護(hù)措施防止視野太大時(shí)返回過(guò)多數(shù)據(jù)。參數(shù)順序是 minLng, minLat, maxLng, maxLat。條件篩選按價(jià)格區(qū)間和區(qū)域篩選。SELECT id, name, unit_price, area, district, ST_X(geom) as lng, ST_Y(geom) as lat FROM house WHERE unit_price BETWEEN %s AND %s AND district %s AND geom ST_MakeEnvelope(%s, %s, %s, %s, 4326) LIMIT 2000;區(qū)域聚合按行政區(qū)統(tǒng)計(jì)均價(jià)和數(shù)量。SELECT district, COUNT(*) as cnt, AVG(unit_price) as avg_price, MIN(unit_price) as min_price, MAX(unit_price) as max_price FROM house GROUP BY district ORDER BY avg_price DESC;這三個(gè)接口覆蓋了房?jī)r(jià)展示系統(tǒng) 90% 的交互需求。Flask 路由里把參數(shù)接進(jìn)來(lái)拼 SQL 執(zhí)行返回 JSON 即可。注意參數(shù)化查詢不要用字符串拼接否則會(huì)有 SQL 注入風(fēng)險(xiǎn)。4. 前端怎么渲染地圖交互與性能優(yōu)化4.1 地圖庫(kù)選型Leaflet 還是 Mapbox GL前端地圖庫(kù)主流就兩個(gè)選擇Leaflet 和 Mapbox GL。Leaflet 輕量、插件多、上手快適合課程大作業(yè)這種場(chǎng)景。Mapbox GL 性能更好、支持矢量瓦片和 3D但學(xué)習(xí)曲線陡而且商用要收費(fèi)。我的建議是如果你只是展示點(diǎn)數(shù)據(jù)和熱力圖Leaflet 完全夠用配合高德或騰訊的底圖瓦片效果不差。如果你要做大規(guī)模數(shù)據(jù)渲染十萬(wàn)級(jí)以上或者 3D 效果再考慮 Mapbox GL。課程大作業(yè)的話Leaflet 是性價(jià)比最高的選擇。4.2 用 Leaflet 加載后端接口并渲染點(diǎn)標(biāo)記下面是一個(gè)完整的前端渲染示例包含地圖初始化、接口調(diào)用、點(diǎn)標(biāo)記渲染// 初始化地圖中心點(diǎn)設(shè)在北京 const map L.map(map).setView([39.9042, 116.4074], 12); // 加載高德底圖瓦片 L.tileLayer(https://webrd0{s}.is.autonavi.com/appmaptile?langzh_cnsize1scale1style8x{x}y{y}z{z}, { subdomains: [1, 2, 3, 4], attribution: 高德地圖 }).addTo(map); // 用圖層組管理標(biāo)記方便清空 let markerLayer L.layerGroup().addTo(map); // 根據(jù)當(dāng)前視野請(qǐng)求數(shù)據(jù) function loadData() { const bounds map.getBounds(); const params new URLSearchParams({ minLng: bounds.getWest(), minLat: bounds.getSouth(), maxLng: bounds.getEast(), maxLat: bounds.getNorth(), minPrice: document.getElementById(minPrice).value || 0, maxPrice: document.getElementById(maxPrice).value || 999999 }); fetch(/api/houses?${params}) .then(res res.json()) .then(data { markerLayer.clearLayers(); data.forEach(item { // 根據(jù)價(jià)格設(shè)置顏色越貴越紅 const color item.unit_price 80000 ? #e74c3c : item.unit_price 50000 ? #f39c12 : #27ae60; const marker L.circleMarker([item.lat, item.lng], { radius: 5, fillColor: color, color: #fff, weight: 1, fillOpacity: 0.8 }); marker.bindPopup( b${item.name}/bbr單價(jià)${item.unit_price} 元/平br區(qū)域${item.district} ); markerLayer.addLayer(marker); }); }); } // 地圖移動(dòng)結(jié)束后重新加載 map.on(moveend, loadData); loadData();這段代碼的核心邏輯是監(jiān)聽(tīng)地圖的moveend事件每次移動(dòng)結(jié)束后根據(jù)當(dāng)前視野的 bounding box 請(qǐng)求后端數(shù)據(jù)然后清空舊標(biāo)記、渲染新標(biāo)記。顏色按單價(jià)分三檔這是最直觀的可視化方式。circleMarker比默認(rèn)的marker性能好因?yàn)樗?Canvas 渲染而不是 DOM 元素。參數(shù)方面radius控制點(diǎn)大小數(shù)據(jù)密集時(shí)可以調(diào)小到 3。fillOpacity控制透明度密集區(qū)域建議 0.6 左右能看到重疊。moveend事件有 200ms 左右的延遲如果覺(jué)得卡頓可以加 debounce。4.3 熱力圖與聚合兩萬(wàn)條數(shù)據(jù)不卡的秘密點(diǎn)標(biāo)記超過(guò)五千條就會(huì)明顯卡頓這時(shí)候需要換渲染方式。兩個(gè)方案熱力圖和聚合。熱力圖用 Leaflet.heat 插件適合展示價(jià)格密度分布// 引入 leaflet-heat 后 const heatData data.map(item [item.lat, item.lng, item.unit_price / 100000]); L.heatLayer(heatData, { radius: 25, blur: 15, maxZoom: 17, gradient: {0.4: blue, 0.6: lime, 0.8: yellow, 1.0: red} }).addTo(map);radius是熱力點(diǎn)的影響半徑blur是模糊程度gradient定義顏色梯度。熱力圖適合宏觀展示但看不到具體房源所以通常和點(diǎn)標(biāo)記配合使用縮放級(jí)別低時(shí)用熱力圖放大后切換到點(diǎn)標(biāo)記。聚合用 Leaflet.markercluster把鄰近的點(diǎn)合并成一個(gè)數(shù)字氣泡const clusterGroup L.markerClusterGroup({ maxClusterRadius: 50, spiderfyOnMaxZoom: true, showCoverageOnHover: false }); data.forEach(item { const marker L.marker([item.lat, item.lng]); marker.bindPopup(b${item.name}/bbr${item.unit_price} 元/平); clusterGroup.addLayer(marker); }); map.addLayer(clusterGroup);maxClusterRadius控制聚合半徑越大聚合越激進(jìn)。spiderfyOnMaxZoom在最大縮放級(jí)別時(shí)展開(kāi)重疊點(diǎn)這個(gè)體驗(yàn)很好。聚合方案能輕松支撐幾萬(wàn)條數(shù)據(jù)因?yàn)閷?shí)際渲染的 DOM 元素只有聚合后的幾十個(gè)。5. 避坑與排查那些讓我熬夜的翻車現(xiàn)場(chǎng)5.1 坐標(biāo)飄移點(diǎn)全跑到隔壁城市去了現(xiàn)象地圖上加載出來(lái)的點(diǎn)整體偏移明明應(yīng)該是市中心的房子全跑到郊區(qū)甚至隔壁城市。原因坐標(biāo)系不統(tǒng)一。數(shù)據(jù)源是 WGS84底圖是高德 GCJ02兩者差了幾百米?;蛘邤?shù)據(jù)源是百度 BD09差得更多。解決在數(shù)據(jù)清洗階段統(tǒng)一轉(zhuǎn)成 GCJ02。用前面給的gcj02_to_wgs84反向轉(zhuǎn)換或者直接用現(xiàn)成的轉(zhuǎn)換庫(kù)。轉(zhuǎn)換后隨機(jī)抽幾個(gè)點(diǎn)在高德地圖上驗(yàn)證確認(rèn)位置正確再導(dǎo)入數(shù)據(jù)庫(kù)。5.2 接口超時(shí)視野一大就轉(zhuǎn)圈現(xiàn)象地圖縮小到全省范圍時(shí)接口請(qǐng)求超時(shí)前端一直 loading。原因沒(méi)有加LIMIT或者LIMIT設(shè)得太大。全省范圍的 bounding box 可能包含幾十萬(wàn)條數(shù)據(jù)一次性返回必然超時(shí)。解決接口層強(qiáng)制加LIMIT 2000同時(shí)在前端做縮放級(jí)別判斷??s放級(jí)別小于 10 時(shí)只返回聚合數(shù)據(jù)不返回原始點(diǎn)。聚合數(shù)據(jù)可以用 PostGIS 的ST_SnapToGrid或者按區(qū)域分組統(tǒng)計(jì)。5.3 中文亂碼小區(qū)名全是問(wèn)號(hào)現(xiàn)象前端彈窗里的小區(qū)名顯示成???或者亂碼。原因數(shù)據(jù)庫(kù)編碼不是 UTF-8或者 Flask 返回 JSON 時(shí)沒(méi)有設(shè)置ensure_asciiFalse。解決建庫(kù)時(shí)指定ENCODING UTF8Flask 里用jsonify時(shí)加app.config[JSON_AS_ASCII] False。如果已經(jīng)建庫(kù)了可以ALTER DATABASE修改編碼但已有數(shù)據(jù)可能需要重新導(dǎo)入。5.4 空間索引失效查詢慢到懷疑人生現(xiàn)象建了 GIST 索引但范圍查詢還是慢幾萬(wàn)條數(shù)據(jù)要好幾秒。原因查詢條件里用了ST_Contains或ST_Within而不是或者對(duì) geom 字段做了函數(shù)操作導(dǎo)致索引失效。解決范圍查詢用運(yùn)算符它直接走空間索引。如果必須用ST_Contains確保它是索引輔助過(guò)濾之后的精確過(guò)濾而不是唯一條件。用EXPLAIN ANALYZE看執(zhí)行計(jì)劃確認(rèn)走了 Index Scan 而不是 Seq Scan。5.5 前端內(nèi)存泄漏切換篩選條件后越來(lái)越卡現(xiàn)象反復(fù)切換價(jià)格篩選頁(yè)面越來(lái)越卡最后瀏覽器崩潰。原因每次請(qǐng)求都創(chuàng)建新的圖層組但沒(méi)有銷毀舊的。Leaflet 的圖層組如果只是remove而沒(méi)有清空內(nèi)部引用標(biāo)記對(duì)象仍然占用內(nèi)存。解決用全局的markerLayer每次請(qǐng)求前clearLayers()而不是重新創(chuàng)建。熱力圖和聚合圖層同理切換前先map.removeLayer()。另外fetch請(qǐng)求加AbortController快速切換時(shí)取消上一個(gè)未完成的請(qǐng)求。6. 進(jìn)階技巧讓房?jī)r(jià)展示系統(tǒng)從及格到高分課程大作業(yè)想拿高分光有基礎(chǔ)功能不夠得有一兩個(gè)亮點(diǎn)。我推薦三個(gè)方向等值線渲染、時(shí)間軸動(dòng)畫(huà)、以及基于空間查詢的「找相似房源」。等值線渲染是把離散的房?jī)r(jià)點(diǎn)插值成連續(xù)曲面用turf.js的interpolate函數(shù)可以做到。核心思路是把點(diǎn)數(shù)據(jù)轉(zhuǎn)成網(wǎng)格再用isobands生成等值面。這個(gè)效果在答辯時(shí)非常抓眼球但計(jì)算量不小建議在后端做插值前端只負(fù)責(zé)渲染 GeoJSON。時(shí)間軸動(dòng)畫(huà)適合有歷史成交數(shù)據(jù)的場(chǎng)景。用 Leaflet 的timeDimension插件或者自己寫(xiě)一個(gè)滑塊控件按月份切換數(shù)據(jù)。實(shí)現(xiàn)上就是在接口里加一個(gè)month參數(shù)前端滑塊變化時(shí)重新請(qǐng)求。這個(gè)功能能體現(xiàn)你對(duì)時(shí)空數(shù)據(jù)的理解加分不少?!刚蚁嗨品吭础故俏覀€(gè)人最喜歡的功能。用戶點(diǎn)擊某個(gè)房源后系統(tǒng)返回單價(jià)、面積、區(qū)域最接近的五個(gè)房源并在地圖上高亮。SQL 實(shí)現(xiàn)如下SELECT id, name, unit_price, area, ST_X(geom) as lng, ST_Y(geom) as lat, ABS(unit_price - %s) ABS(area - %s) * 100 as score FROM house WHERE id ! %s AND geom ST_Buffer(ST_MakePoint(%s, %s)::geography, 3000)::geometry ORDER BY score ASC LIMIT 5;這里用ST_Buffer限定 3 公里范圍score是單價(jià)差和面積差的加權(quán)和。面積差乘以 100 是為了讓兩個(gè)維度的量級(jí)接近。這個(gè)查詢同時(shí)用到了空間索引和普通索引性能很好。最后一個(gè)技巧是關(guān)于數(shù)據(jù)可視化的細(xì)節(jié)。顏色梯度不要用默認(rèn)的彩虹色改用viridis或RdYlGn這類感知均勻的色帶看起來(lái)更專業(yè)。圖例一定要加不然老師看不懂顏色代表什么。彈窗里除了基本信息可以加一個(gè)「周邊均價(jià)對(duì)比」用一條簡(jiǎn)單的 SQL 就能算出來(lái)但展示效果很好。我自己做這類項(xiàng)目最大的教訓(xùn)是不要一開(kāi)始就追求功能多先把「數(shù)據(jù)準(zhǔn)確 地圖流暢 篩選可用」這三件事做到位。我見(jiàn)過(guò)太多同學(xué)花一周做 3D 效果結(jié)果坐標(biāo)是錯(cuò)的答辯時(shí)被老師一句話問(wèn)倒。先把基礎(chǔ)打牢再考慮亮點(diǎn)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取