本文做者:TalkingData 可視化工程師李鳳祿
編輯:Aresn
inMap 是一款基於 canvas 的大數據可視化庫,專一於大數據方向點線面的可視化效果展現。目前支持散點、圍欄、熱力、網格、聚合等方式;致力於讓大數據可視化變得簡單易用。javascript
GitHub 地址:https://github.com/TalkingData/inmap前端
文檔地址:http://inmap.talkingdata.com/java
在地理信息可視化中,咱們常常會遇到在地圖上標記文字的需求,下面展現的是某流行 chart 圖表框架的效果:react
要顯示的文字空間不夠時,就會形成文字重疊顯示混亂,用戶體驗很不友好。git
怎麼解決這個問題呢?咱們採用文字避讓算法,解決這種坑爹的問題。github
下面展現的是 inMap 文字避讓效果:web
文字標註算法是 GIS 中最複雜的問題之一(屬於 NP 複雜度問題,因此一般不能找到最優解,只能找到較優解)。面試
inMap 避讓算法採用的是四分位模型算法,接下來手把手教你寫避讓算法,老司機帶你裝逼帶你飛。算法
inMap 接收的是經緯度數據,須要把它映射到 canvas 的像素座標,這就用到了墨卡託轉換,墨卡託算法很複雜,之後咱們會有單獨的一篇文章來說講他的原理。通過轉換,你獲得的數據應該是這樣的:canvas
[ { "name": "海門",//要顯示的文字 "lng": 121.15, "lat": 31.89, "count": 7, "pixel": { //像素座標 "x": 968, "y": 736 } }, { "name": "鄂爾多斯", "lng": 109.781327, "lat": 39.608266, "count": 5, "pixel": { "x": 659, "y": 478 } }, ... ]
好了,咱們獲得轉換後的像素座標數據(x、y),就能夠作下面的事情了。
measureText()
是 canvas 內置的方法,返回字體寬度的像素單位:
let ctx = this.container.getContext('2d'); // canvas 上下文 let width= ctx.measureText(name).width;
咱們經過 measureText 獲得每一個文字的寬度,canvas 並無直接獲取文字的方法,那文字的高度如何的獲得呢?
咱們經過反覆測試發現 canvas 的 font 等於 「13px Arial」 字體(別的字體不敢保證)的時候,文字的高度大概是 fontSize 的 1.1 倍。
因此代碼以下:
let fontSize = parseInt(ctx.font); let height = fontSize * 1.1;
文字的寬度和高度獲得後,咱們就能夠建立文字矩形的座標系了。
所謂四分位模型,每個標記點都有上下左右四個放文字的位子,若是左邊放不下,那就放右邊試試,還不行就放到下面試試,以此類推,原理就這麼簡單,哈哈。
建立右側虛擬矩形座標描述:
右側虛擬矩形座標的描述把圓點也包含在內了,是爲了防止文字和圓點重疊。
在計算虛擬矩形的高度時有些坑,圓點大小不是固定的,是根據用戶動態配置的,圓點的直徑可能大於文字的高度,咱們就設定虛擬矩形的高度永遠都是最大的那個,須要作一些特殊處理。
代碼以下:
_getLeftAnchor() { let x = this.center.x - this.radius - this.textReact.width, y = this.center.y - this.textReact.height / 2, diam = this.radius * 2, maxH = diam > this.textReact.height ? diam : this.textReact.height; //矩形的高度 return { x, y, minX: x, maxX: this.center.x + this.radius, minY: this.center.y - maxH / 2, maxY: this.center.y + maxH / 2 }; }
以此類推,描述下面、左面、上面的虛擬矩形座標。
判斷兩個矩形是否覆蓋相交,根據矩形的 minX,maxX,minY,maxY 判斷相交,原理比較簡單,代碼以下:
/** * 判斷分位是否相交 * @param {*} target */ isAnchorMeet(target) { let react = this.getCurrentRect(), targetReact = target.getCurrentRect(); if ((react.minX < targetReact.maxX) && (targetReact.minX < react.maxX) && (react.minY < targetReact.maxY) && (targetReact.minY < react.maxY)) { return true; } return false; }
let labels = pixels.map((val) => { let radius = val.pixel.radius + this.style.normal.borderWidth; //圓點半徑 return new Label(val.pixel.x, val.pixel.y, radius, fontSize, byteWidth, val.name); });
遞歸遍歷虛擬文字集合、判斷是否與其餘相交,若是有相交就移動當前文字位子,直到不相交爲止。當找不到合適位置時,就選擇隱藏當前文字。
代碼以下:
do { var meet = false; //本輪是否有相交 for (let i = 0; i < labels.length; i++) { let temp = labels[i]; for (let j = 0; j < labels.length; j++) { if (i != j && temp.show && temp.isAnchorMeet(labels[j])) { temp.next(); meet = true; break; } } } } while (meet);
labels.forEach(function (item) { if (item.show) { //是否顯示 let pixel = item.getCurrentRect(); ctx.beginPath(); ctx.fillText(item.text, pixel.x, pixel.y); ctx.fill(); } });
文字避讓算法到目前介紹完了,對應的 inMap 文件地址爲https://github.com/TalkingData/inmap/blob/master/src/worker/helper/Label.js,接下來還會繼續給你們分享乾貨。
分享兩位業內大牛的前端課程: