<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
因實習的公司是做巨量資料的,而我的工作剛好又是需要繪製一些資料圖表的。繪製圖表有許多現成的元件可以使用,但是要想達到產品所需要的效果,只靠元件內部的一些功能是不太夠的。一些細膩的要求必須在掌握元件原理方法的情況下,自己去寫演演算法來完成。例如,本文要說的這個刻度計算演演算法,開始正文之前,我先描述遇到的問題。
echarts自身的刻度計算有時候並不好用,例如有時候你希望讓圖表只有5條刻度線,即分成4段,echarts提供了一個引數叫splitNumber,把splitNumber設為4可以讓圖表儘量地分成4段,然而當資料波動較大時,echarts會自動增加分割的段數,即即使大部分資料都能正常分出4段刻度,但仍有少部分資料實際分出的段數可能不止4段。如下面這樣:
因此我們得出一個結論,echarts的splitNumber只是預估的分割段數。如果我們需要強制把刻度區間分為4段,則需要我們自己去寫演演算法計算刻度。另外,即使段數正確的情況下,echarts自動計算出的刻度也可能存在區間過大,資料差異不明顯的情況,如下面的圖片,因為刻度區間太大,導致各個資料看起來像是差不多大的,看不出差異:
另外,echarts自動計算出的刻度也有一些其他的問題,例如當圖表在柱狀圖和堆疊圖中切換時,堆疊圖可能出現刻度溢位問題。不過堆疊圖的刻度計算這裡就先不說明了,下面開始正文吧。
刻度計算的演演算法之前我之前也寫了一版,解決了分割段數的問題,但是仍無法解決刻度區間過大的問題。之前那一版演演算法的主要思想是取近似值,分別取最大值和最小值的最小近似整值得到刻度,雖然不是最優的演演算法,但是在構思和調整演演算法的時候我也學到了不少東西,而這一版的演演算法是在我們技術老大的點撥下結合網上的一些文章和專案的需求而寫出來的,演演算法如下:
要求: 根據一組數的最大值、最小值來確定刻度值,確定刻度的最大值maxi、最小值mini和刻度間隔interval。當出現異號資料時可選擇正負兩邊刻度是否需要對稱,當存在異號資料時要求其中一條刻度分割線必須在0刻度上,可以選擇是否允許段數誤差。
演演算法採用javascript語言描述,因為圖表的繪製在前端完成。
/* 刻度計算演演算法,基於魔術陣列 [10, 15, 20, 25, 30, 40, 50, 60, 70, 80, 90, 100]; 解釋:魔數陣列是理想間隔陣列,即我們希望每個刻度的間隔都是魔數陣列中某個數的整數倍。(準確的來說是整10倍) */ //新增,解決js的浮點數存在精度問題,在計算出最後結果時可以四捨五入一次,因為刻度太小也沒有意義,所以這裡忽略設定精度為8位元 function fixedNum(num){ if((""+num).indexOf('.')>=0) num = parseFloat(num.toFixed(8)); return num; } //1.初始化 var symmetrical = false;//是否要求正負刻度對稱。預設為false,需要時請設定為true var deviation = false;//是否允許誤差,即實際分出的段數不等於splitNumber var magic = [10, 15, 20, 25, 30, 40, 50, 60, 70, 80, 90, 100];//魔數陣列經過擴充,放寬魔數限制避免出現取不到魔數的情況。 var arr = [1230, 320, 20, 304, 102, 234];//測試資料 var max,min,splitNumber; splitNumber = 4;//理想的刻度間隔段數,即希望刻度區間有多少段 max = Math.max.apply(null,arr);//呼叫js已有函數計算出最大值 min = Math.min.apply(null,arr);//計算出最小值 //2.計算出初始間隔tempGap和縮放比例multiple var tempGap = (max - min) / splitNumber;//初始刻度間隔的大小。 //設tempGap除以multiple後剛剛處於魔數區間內,先求multiple的冪10指數,例如當tempGap為120,想要把tempGap對映到魔數陣列(即處理為10到100之間的數),則倍數為10,即10的1次方。 var multiple = Math.floor(Math.log10(tempGap)-1);//這裡使用Math.floor的原因是,當Math.log10(tempGap)-1無論是正負數都需要向下取整。不能使用parseInt或其他取整邏輯代替。 multiple = Math.pow(10,multiple);//剛才是求出指數,這裡求出multiple的實際值。分開兩行程式碼避免有人看不懂 //3.取出鄰近較大的魔數執行第一次計算 var tempStep = tempGap / multiple;//對映後的間隔大小 var estep;//期望得到的間隔 var lastIndex = -1;//記錄上一次取到的魔數下標,避免出現死迴圈 for(var i = 0; i < magic.length;i++){ if(magic[i]>tempStep){ estep = magic[i]*multiple;//取出第一個大於tempStep的魔數,並乘以multiple作為期望得到的最佳間隔 break; } } //4.求出期望的最大刻度和最小刻度,為estep的整數倍 var maxi,mini; function countDegree(estep){ //這裡的parseInt是我無意中寫出來的,本來我是想對maxi使用Math.floor,對mini使用Math.ceil的。這樣能向下取到鄰近的一格,不過後面發現用parseInt好像畫出來圖的比較好看 maxi = parseInt(max/estep+1) * estep;//最終效果是當max/estep屬於(-1,Infinity)區間時,向上取1格,否則取2格。 mini = parseInt(min/estep-1) * estep;//當min/estep屬於(-Infinity,1)區間時,向下取1格,否則取2格。 //如果max和min剛好在刻度線的話,則按照上面的邏輯會向上或向下多取一格 if(max===0) maxi = 0;//這裡進行了一次矯正,優先取到0刻度 if(min===0) mini = 0; if(symmetrical&&maxi*mini<0){//如果需要正負刻度對稱且存在異號資料 var tm = Math.max(Math.abs(maxi),Math.abs(mini));//取絕對值較大的一方 maxi = tm; mini = -tm; } } countDegree(estep); if(deviation){//如果允許誤差,即實際分段數可以不等於splitNumber,則直接結束 var interval = fixedNum(estep); console.log(maxi,mini,interval); return; } //5.當正負刻度不對稱且0刻度不在刻度線上時,重新取魔數進行計算//確保其中一條分割線剛好在0刻度上。 else if(!symmetrical||maxi*mini>0){ outter:do{ //計算模擬的實際分段數 var tempSplitNumber = Math.round((maxi-mini)/estep); //當趨勢單調性發生變化時可能出現死迴圈,需要進行校正 if((i-lastIndex)*(tempSplitNumber-splitNumber)<0){//此處檢查單調性變化且未取到理想分段數 //此處的校正基於合理的均勻的魔數陣列,即tempSplitNumber和splitNumber的差值較小如1和2,始終取大刻度 while(tempSplitNumber<splitNumber){//讓maxi或mini增大或減少一個estep直到取到理想分段數 if((mini-min)<=(maxi-max)&&mini!=0||maxi==0){//在儘量保留0刻度的前提下,讓更接近最值的一邊擴充套件一個刻度 mini-=estep; }else{ maxi+=estep; } tempSplitNumber++; if(tempSplitNumber==splitNumber) break outter; } } //當魔數下標越界或取到理想分段數時退出迴圈 if(i>=magic.length-1|| i<=0 || tempSplitNumber==splitNumber) break; //記錄上一次的魔數下標 lastIndex = i; //嘗試取符合趨勢的鄰近魔數 if(tempSplitNumber>splitNumber) estep = magic[++i]*multiple; else estep = magic[--i]*multiple; //重新計算刻度 countDegree(estep); }while(tempSplitNumber!=splitNumber); } //6.無論計算始終把maxi-mini分成splitNumber段,得到間隔interval。不過前面的演演算法已經儘量的保證刻度最優了,即interval接近或等於理想刻度estep。 maxi = fixedNum(maxi); mini = fixedNum(mini); var interval = fixedNum((maxi-mini)/splitNumber); console.log(maxi,mini,interval);
1.如果不處理小數誤差,且強制分為4段,出來的效果是這樣的(20190722版):
2.處理了小數誤差,並允許刻度誤差出來的效果是這樣的(20190723版)
可以看出:
可以看出:
export interface ScaleOption { /** * 資料最大值 * * @type {(number | null)} * @memberof ScaleOption */ max: number | null; /** * 資料最小值 * * @type {(number | null)} * @memberof ScaleOption */ min: number | null; /** * 預期分成幾個區間 * * @type {number} * @memberof ScaleOption */ splitNumber?: number; /** * 存在異號資料時正負區間是否需要對稱 * * @type {boolean} * @memberof ScaleOption */ symmetrical?: boolean; /** * 是否允許實際分成的區間數有誤差 * * @type {boolean} * @memberof ScaleOption */ deviation?: boolean; /** * 是否優先取到0刻度 * * @type {boolean} * @memberof ScaleOption */ preferZero?: boolean; } export interface ScaleResult { max?: number; min?: number; interval?: number; splitNumber?: number; } // 雙精度浮點數有效數位為15位 const maxDecimal = 15; /** * 解決js的浮點數存在精度問題,在計算出最後結果時可以四捨五入一次,刻度太小也沒有意義 * * @export * @param {(number | string)} num * @param {number} [decimal=8] * @returns {number} */ export function fixedNum(num: number | string, decimal: number = maxDecimal): number { let str: string = "" + num; if (str.indexOf(".") >= 0) str = Number.parseFloat(str).toFixed(decimal); return Number.parseFloat(str); } /** * 判斷非Infinity非NaN的number * * @export * @param {*} num * @returns {num is number} */ export function numberValid(num: any): num is number { return typeof num === "number" && Number.isFinite(num); } /** * 計算理想的刻度值,刻度區間大小一般是[10, 15, 20, 25, 30, 40, 50, 60, 70, 80, 90, 100]中某個數位的整10倍 * * @export * @param {ScaleOption} option * @returns {ScaleResult} */ export function scaleCompute(option: ScaleOption): ScaleResult { option = { max: null, min: null, splitNumber: 4, // splitNumber建議取4或者5等這種容易被整除的數位 symmetrical: false, deviation: false, preferZero: true, ...option, }; const magics: number[] = [10, 15, 20, 25, 30, 40, 50, 60, 70, 80, 90, 100, 150]; // 加入150形成閉環 // tslint:disable-next-line: prefer-const let { max : dataMax, min: dataMin, splitNumber, symmetrical, deviation, preferZero } = option; if (!numberValid(dataMax) || !numberValid(dataMin) || dataMax < dataMin) { return { splitNumber }; } else if (dataMax === dataMin && dataMax === 0) { return { max: fixedNum(magics[0] * splitNumber), min: dataMin, interval: magics[0], splitNumber, }; } else if (dataMax === dataMin) { preferZero = true; } if (!numberValid(splitNumber) || splitNumber <= 0) splitNumber = 4; if (preferZero && dataMax * dataMin > 0) { if (dataMax < 0) dataMax = 0; else dataMin = 0; } const tempGap: number = (dataMax - dataMin) / splitNumber; let multiple: number = Math.floor(Math.log10(tempGap) - 1); // 指數 multiple = Math.pow(10, multiple); const tempStep: number = tempGap / multiple; let expectedStep: number = magics[0] * multiple; let storedMagicsIndex: number = -1; let index: number; // 當前魔數下標 for (index = 0; index < magics.length; index++) { if (magics[index] > tempStep) { expectedStep = magics[index] * multiple; // 取出第一個大於tempStep的魔數,並乘以multiple作為期望得到的最佳間隔 break; } } let axisMax: number = dataMax; let axisMin: number = dataMin; function countDegree(step: number): void { axisMax = parseInt("" + (dataMax / step + 1)) * step; // parseInt令小數去尾 -1.8 -> -1 axisMin = parseInt("" + (dataMin / step - 1)) * step; if (dataMax === 0) axisMax = 0; // 優先0刻度 if (dataMin === 0) axisMin = 0; if (symmetrical && axisMax * axisMin < 0) { const tm: number = Math.max(Math.abs(axisMax), Math.abs(axisMin)); axisMax = tm; axisMin = -tm; } } countDegree(expectedStep); if (deviation) { return { max: fixedNum(axisMax), min: fixedNum(axisMin), interval: fixedNum(expectedStep), splitNumber: Math.round((axisMax - axisMin) / expectedStep), }; } else if (!symmetrical || axisMax * axisMin > 0) { let tempSplitNumber: number; out: do { tempSplitNumber = Math.round((axisMax - axisMin) / expectedStep); if ((index - storedMagicsIndex) * (tempSplitNumber - splitNumber) < 0) { // 出現死迴圈 while (tempSplitNumber < splitNumber) { if ((axisMin - dataMin <= axisMax - dataMax && axisMin !== 0) || axisMax === 0) { axisMin -= expectedStep; } else { axisMax += expectedStep; } tempSplitNumber++; if (tempSplitNumber === splitNumber) break out; } } if (index >= magics.length - 1 || index <= 0 || tempSplitNumber === splitNumber) break; storedMagicsIndex = index; if (tempSplitNumber > splitNumber) expectedStep = magics[++index] * multiple; else expectedStep = magics[--index] * multiple; countDegree(expectedStep); } while (tempSplitNumber !== splitNumber); } axisMax = fixedNum(axisMax); axisMin = fixedNum(axisMin); const interval: number = fixedNum((axisMax - axisMin) / splitNumber); return { max: axisMax, min: axisMin, interval, splitNumber, }; }
結語
好久沒有寫部落格,可能是最近比較忙,如果有空的話其實前端有很多東西都可以寫一下,今天寫這個部落格是因為文章篇幅較小,而且我剛好在檢查演演算法,另外這個演演算法比較有參考意義,就忙裡偷閒寫出來給大家看看,也方便我自己以後查閱。目前我參與的專案還在開發完善中,關於echarts我也踩了很多坑,針對專案需求編寫了一些東西,有機會再寫出來大家討論吧。
到此這篇關於座標軸刻度取值演演算法之源於echarts的y軸刻度計算需求的文章就介紹到這了,更多相關座標軸刻度取值演演算法內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45