作者:焦以焜
前言
我們之前已經分享過柱狀圖,折線圖,餅圖,并且留下了關于如何實作互動的懸念,在這篇文章中,我将在分享如何實作散點圖的同時,分享實作使用者與互動的思路。
散點圖
我們在講柱狀圖時,已經詳細的描述了如何繪制我們的坐标軸,折線圖的坐标軸與柱狀圖坐标軸的繪制方法幾乎相同,是以在本文我們将不會讨論。關于坐标軸繪制的講解與箭頭繪制的講解,大家可以檢視柱狀圖繪制:
我們再次聲明,非常希望讀者可以完全讀懂柱狀圖的這一期文章,因為在大部分圖表的繪制中使用到的坐标點,都在該篇文章中詳細的解釋過,這對了解本文一下内容或今後的内容都起到了至關重要的作用。
資料點的繪制
我們之前所分享的柱狀圖,折線圖與餅圖,均隻拿了一組資料來進行舉例,而在散點圖中,應該至少有兩組資料。與柱狀圖和折線圖不同,我們還需要給出散點圖中的每個資料點半徑的算法,代碼如下:
drawData() {
const el = this.$element('the-canvas');
const context = el.getContext('2d');
let data = this.option.data; //擷取資料集
let xLength = (this.option.chartZone[2] - this.option.chartZone[0]);
let yLength = (this.option.chartZone[3] - this.option.chartZone[1]);
let gap = xLength / this.option.xAxisLable.length;
//周遊兩組資料年份
for (let i = 0; i < data.length; i++) {
let x, y, r, c;
context.fillStyle = this.option.colorPool[i]; //從顔色池中選取顔色
context.globalAlpha = 0.8; //為避免點覆寫,采取半透明繪制
//周遊各個資料點
for (let j = 0; j < data[i].length; j++) {
//計算坐标 由于舉例的資料太大,我做了微調的處理,但是這違背了封裝性且為魔鬼數字,不推薦
x = this.option.chartZone[0] + xLength * data[i][j][0] / 70000;
y = this.option.chartZone[3] - yLength * (data[i][j][1] - 55) / (85 - 55);
//散點圖半徑算法
//直接數值
r = data[i][j][2] * 5 / 100000000;
//求對數
r = Math.log(data[i][j][2]);
//開根号
r = Math.pow(data[i][j][2], 0.4) / 100;
let singleData = {
position: [x, y], radius: r, color: this.option.colorPool[i]
}
//将所有的資料點儲存
this.allData.push(singleData)
//繪制散點
context.beginPath();
context.arc(x, y, r, 0, 2 * Math.PI, false);
context.fill();
context.closePath();
}
}
context.restore()
},
實作互動
我們希望實作的效果為:點選一個資料點後,該資料點變大且有動畫效果最好,而點選空白處後該資料點變回原樣。
如下圖所示:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsQTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-YWan5iM4kzM2IGMwQGZ3YGOjdTNyYzX3QDMxUTM0EzLcRDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL0M3Lc9CX6MHc0RHaiojIsJye.gif)
這個功能可以分成三步:
- 找到滑鼠點選位置的資料點
- 将該資料點放大
- 點選空白處後将資料點縮小回原大小
找到滑鼠點選位置的資料點
周遊所有的資料點,滑鼠點選的坐标到某個資料點的距離若小于或等于該資料點的半徑,則可以認定滑鼠在該資料點上。
将該資料點放大
假設我們希望放大後的資料點比原資料點的半徑大15px,我們可以直接以原資料點為圓心,畫一個
原資料點半徑+15
的圓,直接覆寫住原資料點。我們也可以寫一個for循環,循環
i = 300
次,每次都延時300毫秒畫一個
i * 0.05 + 原資料點半徑
的圓,以實作動畫效果。注意,我們的操作隻是在視覺上給了使用者一種對原資料點操作了的錯覺,我們一直沒有對原資料點進行操作。如果我們仔細的看上面的效果圖會發現,變大的資料點顔色變得更深了,那是因為我們畫了300個圓,而這些圓雖然透明度和顔色相同,但是重疊在一起之後使得整體的顔色改變了。
将資料點縮小
我們可以很容易的想到以下兩種方法來實作縮小的需求:
- 我們可以通過畫一個和原來半徑相同,圓心坐标相同的圓來實作嗎?
如果這樣操作得出的效果隻是在放大的圓的基礎上再畫了一個小的圓:因為大圓不會因為畫了一個小圓就消失了。在canvas上畫的内容就像我們在一張白紙上用水彩筆畫畫,除非用塗改液将畫的内容塗掉,否則圖畫将一直在紙上存在(其實用塗改液也隻是将紙上的某一部分與背景顔色相同,用塗改液也隻是在視覺上讓我們覺得畫的内容消失了,但其實它仍然存在着)。
- 我們可以通過把多餘的那部分截掉來實作嗎?
将變大的圓截成一個原大小的圓确實實作了一部分我們的需求:從大變小。可是透明度卻回不去了,在視覺效果上,圓被改變了;如果散點圖中該圓旁邊有其它的相交的圓,那也将會受到影響。
我們可以換一個思考的場景:假設我們現在需要把某張照片中的一個人給P掉,隻需要一張角度和場景完全一樣且沒有該人物的圖檔,在這張圖檔上截取一個與目标人物位置與形狀完全相同的部分,将該部分貼在照片上,這樣照片上我們希望被p掉的這個人就被覆寫了。
同理:我們在繪制該圖表的同時,就可以通過
toDataURL
這個API,将該canvas圖表轉換成一張圖檔并儲存起來。
draw() {
this.drawBackground()
this.drawAxis()
this.drawYLables()
this.drawXLables()
this.drawData()
this.drawArrow()
this.drawArrowY()
//将canvas轉成webp格式的圖檔
const el = this.$element('the-canvas');
this.dataURL = el.toDataURL("image/webp", 1)
console.log("dataurl:" + this.dataURL)
this.canvas = new Image()
this.canvas.src = this.dataURL
}
如果我們點選了某個資料點使它放大了,如下圖:
此時我們就計算出它的左上角的位置坐标:(x-r-15, y-r-15),x和y為圓心的坐标,r為放大前圓的半徑,15是我們放大的長度。
這時候,我們在剛才儲存好的原圖圖檔上,相同的位置截取一個相同大小的正方形,貼在此時的canvas上,覆寫住放大的圓形。
注意:我們已經從視覺上實作了将資料點縮小,但是此時資料點上的圓是圖檔格式,是我們從圖檔上截下來貼在canvas上的圖檔,而不是canvas,我們不能再對它進行操作了,是以我們還需要再在該圓的圓心位置畫一個透明且大小與原資料點一樣的圓形,以便我們能夠再次将該資料點點選放大。
代碼如下:
hover(e) {
const el = this.$element('the-canvas');
const context = el.getContext('2d');
//擷取點選的坐标
this.touchX = e.touches[0].globalX
this.touchY = e.touches[0].globalY
for (let i = 0; i < this.allData.length; i++) {
//周遊所有的資料點,判斷點選的位置是否在某個資料點上
if (Math.pow((this.touchX - this.allData[i].position[0]), 2) + Math.pow((this.touchY - this.allData[i].position[1]), 2) <= Math.pow((this.allData[i].radius), 2)) {
context.fillStyle = this.allData[i].color;
context.globalAlpha = 0.3;
this.hoverData = {
x: this.allData[i].position[0],
y: this.allData[i].position[1],
r: this.allData[i].radius,
color: this.allData[i].color
}
let step = 0.05
//如果點選的位置在資料點上,則将該資料點放大
for (let j = 0; j < 300; j++) {
//用setTimeout實作動畫效果
setTimeout(() => {
context.beginPath();
//畫圓
context.arc(this.allData[i].position[0], this.allData[i].position[1], this.allData[i].radius + j * step, 0, 2 * Math.PI, false);
context.fill();
context.restore()
context.closePath();
}, 300)
}
} else {
//如果不在資料點上,且沒有點選過資料點,則不操作
context.globalAlpha = 1
console.log("this.hoverData:" + JSON.stringify(this.hoverData))
let x = this.hoverData.x
let y = this.hoverData.y
let r = this.hoverData.r
console.log("此時的x為:" + x)
//将圖檔上的部分裁剪下來貼在cavnas規定的位置上
context.drawImage(this.canvas, x - r - 15, y - r - 15, 2 * (r + 15), 2 * (r + 15), x - r - 15, y - r - 15, 2 * (r + 15), 2 * (r + 15))
context.save()
context.beginPath()
//最後畫一個圓形覆寫住貼過來的圖檔上的圓
context.arc(x, y, r - 15, 0, 2 * Math.PI, false)
context.closePath()
context.fillStyle = "black"
context.globalAlpha = 0
context.fill()
context.restore()
}
}
},
然而該方法仍然有不盡人意的地方:從下圖可以看見,通過
drawImage
剪切過來的圖檔與原canvas相比有明顯的鋸齒,清晰度有差異
總結
本文隻提供了一種思路供大家實作這種互動的需求,而且是不夠完善的,假如我們點選了兩圓相交的位置該如何解決?假設點選了某個内含的圓,互動方式應該如何定義?這些具體的情況希望大家能夠積極發帖或者參與讨論,讓canvas資料圖表系列的話題不要停滞,在提高自己的同時也能夠充實社群的知識儲備。而本篇文章也是OpenHarmony的JS canvas資料可視化系列的最後一篇文章,謝謝大家。
更多原創内容請關注:深開鴻技術團隊
入門到精通、技巧到案例,系統化分享HarmonyOS開發技術,歡迎投稿和訂閱,讓我們一起攜手前行共建鴻蒙生态。
想了解更多關于鴻蒙的内容,請通路:
51CTO和華為官方合作共建的鴻蒙技術社群
https://ost.51cto.com/#bkwz