本文作者:TalkingData 視覺化工程師李鳳祿
編輯:Aresn
inMap 是一款基於 canvas 的大資料視覺化庫,專注於大資料方向點線面的視覺化效果展示。目前支援散點、圍欄、熱力、網格、聚合等方式;致力於讓大資料視覺化變得簡單易用。
GitHub 地址:https://github.com/TalkingData/inmap
文件地址:http://inmap.talkingdata.com/
在地理資訊視覺化中,我們經常會遇到在地圖上標記文字的需求,下面展示的是某流行 chart 圖表框架的效果:
要顯示的文字空間不夠時,就會造成文字重疊顯示混亂,使用者體驗很不友好。
怎麼解決這個問題呢?我們採用文字避讓演算法,解決這種坑爹的問題。
下面展示的是 inMap 文字避讓效果:
文字標註演算法是 GIS 中最複雜的問題之一(屬於 NP 複雜度問題,所以通常不能找到最優解,只能找到較優解)。
inMap 避讓演算法採用的是四分位模型演算法,接下來手把手教你寫避讓演算法,老司機帶你裝逼帶你飛。
準備資料
inMap 接收的是經緯度資料,需要把它對映到 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/...,接下來還會繼續給大家分享乾貨。
福利
分享兩位業內大牛的前端課程:
-
Aresn 大神,開源了很優秀的前端 UI 元件庫 iView,出版了《Vue.js實戰》一書。向大家推薦他的課程,Vue.js實戰系列教程,從本連結過去的打八折優惠,先到先得。
-
Chaos WebGL 專家,擅長 web 3D 開發。 主要有被《玩壞的地球系列課程》,很適合初學者。
本作品採用《CC 協議》,轉載必須註明作者和本文連結