用 500 行純前端代碼在瀏覽器中構建一個 Tableau
作者:naughty
https://my.oschina.net/taogang/blog/1811573
在Gartner最新的對商務智能軟體的專業分析報告中,Tableau持續領跑。Microsoft因為PowerBI表現出色也處於領導者象限。而昔日的領導者像SAP,SAS,IBM,MicroStrategy等逐漸被拉開了差距。
Tableau因為其靈活,出色的數據表現已經成為BI領域裡無可爭議的領頭羊。而其數據驅動的可視化和核心思想是來自於Leland Wilkinson的The Grammar Of Graphics ,同樣受到該思想影響的還有R的圖形庫ggplot。
在數據可視化開源領域裡,大家對百度開發的echarts可謂耳熟能詳,echarts經過多年的發展,其功能確實非常強大,可用出色來形容。但是螞蟻金服開源的基於The Grammar Of Graphics的語法驅動的可視化庫G2,讓人眼前一亮。那我們就看看如何利用G2和500行左右的純前端代碼來實現一個的類似Tableau的數據分析功能。
演示參見 https://codepen.io/gangtao/full/OZvedx/
代碼參見 https://gist.github.com/gangtao/e053cf9722b64ef8544afa371c2daaee
數據載入
第一步是載入數據:
數據載入主要用到了三個庫:
axios 基於Promise的HTTP客戶端
alasql 基於JS的開源SQL資料庫
jquery datatable JQuery的數據表格插件
數據通過我存放在GitHub中的csv格式的文件,以REST請求的方式來載入。下面的代碼把Axios的Promise變成 async/wait方式。
// Ajax async request
constrequest={
get:url=>{
returnnewPromise((resolve,reject)=>{
axios
.get(url)
.then(response=>{
resolve({data:response.data});
})
.catch(error=>{
resolve({data:error});
});
});
}
};
封裝好後,我們就可以用request.get()方法發送REST請求,獲取csv文件。
let csv = await request.get(url);
這一步可能會遇到跨域請求的問題,github上的文件支持跨域。
把數據存儲在一個SQL資料庫中,這樣做的好處是為了下一步做數據準備的時候,可以方便的利用SQL來進行查詢和分析。
classSqlTable{
constructor(data){
this.data=data;
}
async query(sql){
// following line of code does not run in full page view due to security concern.
// const query_str = sql.replace(/(?
constquery_str=sql.replace("table","CSV(?)");
returnawaitalasql.promise(query_str,[this.data]);
}
}
SqlTable是一個對數據表的封裝,把csv數據存在SQL資料庫表中,提供一個query()方法。這裡要做的是把SQL查詢個從 「SELECT * FROM table」 變成 「SELECT * FROM CSV(?)」 表示查詢參數是CSV數據。因為codepen的安全性限制,運行前向查找的replace語句(這裡的regex表示把前面是「FROM 」詞的替換為CSV(?)的)在full page view下是不能執行的,所以我用了一個更簡單的假定,用戶的表名就是table,這樣做有很多問題,大家如果在codepen之外的環境,可以用注釋掉的代碼。
然後把」SELECT * FROM table」的查詢結果(JSON Array)用datatable來展示。
functionsanitizeData(jsonArray){
letnewKey;
jsonArray.forEach(function(item){
for(keyinitem){
newKey=key.replace(/s/g,"").replace(/./g,"");
if(key!=newKey){
item[newKey]=item[key];
deleteitem[key];
}
}
});
returnjsonArray;
}
functiondisplayData(tableId,data){
// tricky to clone array
letdisplay_data=JSON.parse(JSON.stringify(data));
display_data=sanitizeData(display_data);
letcolumns=[];
for(let itemindisplay_data[]){
columns.push({data:item,title:item});
}
$("#"+tableId).DataTable({
data:display_data,
columns:columns,
destroy:true
});
}
這一步有兩點要注意:
數據中,如果列的名字中有包含點,空格等字元,例如Iris數據集中的Sepal.Length,datatable是無法正常顯示的,這裡要調用sanitizeData()方法把列名,也就是JsonArray中Json對象的屬性名中的點和空格去掉。
sanitizeData()方法會改變輸入對象,所以在傳入之前做了一個深度拷貝,這裡利用JSON的stringfy和parse方法可以對JSON兼容的對象有效的拷貝。
這裡要注意,Iris數據集中在datatable中的列名都不顯示點,但實際數據並沒有改變。
數據準備
數據載入完畢,我們來到第二步的數據準備階段。數據準備是數據科學項目最花時間的一步,通常需要對數據進行大量的清洗,變形,抽取等工作,使得數據變得可用。
在這一步我們做了兩件事:
一是顯示數據的一個摘要,讓我們初步了解數據的概貌,為進一步的數據變形和處理做好準備。
這個是Iris數據集的摘要:
functionisString(o){
returntypeofo=="string"||(typeofo=="object"&&o.constructor===String);
}
functionsummaryData(data){
letsummary={};
summary.count=data.length;
summary.fields=[];
for(letpindata[]){
letfield={};
field.name=p;
if(isString(data[][p])){
field.type="string";
}else{
field.type="number";
}
}
for(letfofsummary.fields){
if(f.type=="number"){
f.max=d3.max(data,x=>x[f.name]);
f.min=d3.min(data,x=>x[f.name]);
f.mean=d3.mean(data,x=>x[f.name]);
f.median=d3.median(data,x=>x[f.name]);
f.deviation=d3.deviation(data,x=>x[f.name]);
}else{
f.values=Array.from(newSet(data.map(x=>x[f.name])));
}
}
returnsummary;
}
這裡我們利用數據的類型判斷出每一個欄位是數值型還是字元型。對於字元型的欄位,我們利用JS6的Set來獲得所有的Unique數據。對於數值型,我們利用d3的max,min,mean,median,deviation方法計算出對應的最大值,最小值,平均數,中位數和偏差。
另一個就是利用SQL查詢來對數據進行進一步的加工。
上圖的例子中我們利用限制條件得到一個Iris數據的子集。
另外G2還提供了Dataset的功能:
源數據的解析,將csv, dsv,geojson 轉成標準的JSON,查看Connector
加工數據,包括 filter,map,fold(補數據) 等操作,查看 Transform
統計函數,匯總統計、百分比、封箱 等統計函數,查看 Transform
特殊數據處理,包括 地理數據、矩形樹圖、桑基圖、文字雲 的數據處理,查看 Transform
數據處理是一個比較大的話題,我們的目標是利用儘可能少的代碼完成一個數據分析的工具,所以這一步僅僅是利用alasql提供的SQL查詢來處理數據。
數據展示
數據處理好後就是我們的核心內容,數據展示了。
這一步主要是利用select2提供的選擇控制項構建圖形語法來驅動數據展示。如上圖所示,對應的G2代碼圖形語法為:
g2chart.facet("rect",{
fields:["Admit","Dept"],
eachView(view){
view.interval().position("Gender*Freq").color("Gender").label("Freq");
}
});
圖形語法主要包含以下幾個主要的元素:
幾何標記 Geometry
幾何標記定義了使用什麼樣的幾何圖形來表徵數據。G2現在支持如下這些幾何標記:
這裡要注意,intervalstack是官方支持的,但是文檔沒有提到,在閱讀G2的API文檔的時候,我也發現文檔講的不是很清楚,有很多地方沒有講清楚如何使用API。這也是開源軟體值得改進的地方。
圖形屬性 Attributes
圖形屬性對應視覺編碼中的不同元素,大家可以參考我的另一博客 數據可視化中的視覺屬性 。
圖形屬性主要有以下幾種。
position:位置,二維坐標系內映射至 x 軸、y 軸;
color:顏色,包含了色調、飽和度和亮度;
size:大小,不同的幾何標記對大小的定義有差異;
shape:形狀,幾何標記的形狀決定了某個具體圖表類型的表現形式,例如點圖,可以使用圓點、三角形、圖片表示;線圖可以有折線、曲線、點線等表現形式;
opacity:透明度,圖形的透明度,這個屬性從某種意義上來說可以使用顏色代替,需要使用 『rgba』 的形式,所以在 G2 中我們獨立出來。
在構建語法的時候,我們把圖形屬性綁定一個或者多個數據欄位。
坐標系 Coordinates
坐標系是將兩種位置標度結合在一起組成的 2 維定位系統,描述了數據是如何映射到圖形所在的平面。
G2提供了以下幾種坐標系:
分面 Facet
分面,將一份數據按照某個維度分隔成若干子集,然後創建一個圖表的矩陣,將每一個數據子集繪製到圖形矩陣的窗格中。分面其實提供了兩個功能:
按照指定的維度劃分數據集;
對圖表進行排版。
G2支持以下的分面類型:
注意,在我的代碼中,為了簡化使用,只支持list和rect,當綁定一個欄位的時候用list,綁定兩個欄位的時候用rect。
除了上面提到的元素,當然還有許多其它的元素我們沒有包含和支持,例如:坐標軸,圖例,提示等等。
關於圖形的語法的更多內容,請參考這裡。
生成圖形語法的核心代碼如下:
functiongetFacet(faced,grammarScript){
letfacedType="list";
letfacedScript=""
grammarScript=grammarScript.replace(chartScriptName,"view");
if(faced.length==2){
facedType="rect";
}
letfacedFields=faced.join("", "")
facedScript=facedScript+`${chartScriptName}.facet("${ facedType }",{n`;
facedScript=facedScript+`fields:["${ facedFields }"],n`;
facedScript=facedScript+`eachView(view){n`;
facedScript=facedScript+`${grammarScript};n`;
facedScript=facedScript+`}n`;
facedScript=facedScript+`});n`;
returnfacedScript
}
functiongetGrammar(){
letgrammar={},grammarScript=chartScriptName+".";
grammar.geom=$("#geomSelect").val();
grammar.coord=$("#coordSelect").val();
grammar.faced=$("#facetSelect").val();
geom_attributes.map(function(attr){
grammar[attr]=$("#"+attr+"attr").val();
});
grammarScript=grammarScript+grammar.geom+"()";
geom_attributes.map(function(attr){
if(grammar[attr].length>){
grammarScript=grammarScript+"."+attr+"(""+grammar[attr].join("*")+"")";
}
});
if(grammar.coord){
grammarScript=grammarScript+";n "+chartScriptName+"."+"coord(""+grammar.coord+"");";
}else{
rammarScript=grammarScript+";";
}
if(grammar.faced){
grammarScript=getFacet(grammar.faced,grammarScript);
}
}
console.log(grammarScript)
returngrammarScript;
}
這裡有幾點要注意:
使用JS的模版字元串可以有效的構造代碼片段
使用eval執行構造好的語法驅動的代碼來響應select的change事件,以獲得良好的交互性。在生產環境,要注意該方法的安全性隱患,因為純前端,eval能帶來的威脅比較小,生產中,可以把這個執行放在安全的沙箱中運行
你需要理解圖形語法,並不是任意的組合都能驅動出有效的圖形。
這裡對於select2的多選,有一個小的提示,在預設情況下,多選的順序是固定的順序,並不依賴選擇的順序,然而許多圖形語法和欄位的順序有關,所以我們使用如下的方法來相應select的選擇事件。
functionupdateSelect2Order(evt){
let$element=$(element);
$element.detach();
$(this).append($element);
$(this).trigger("change");
}
這樣做就是每次選中後,把當前選中的項目移到數據最後的位置。
一些例子
好了,下面我們就來看一些例子,了解一下如何使用圖形語法來分析和探索數據。
Iris數據集散點圖
圖形語法:
g2chart.point().position("Sepal.Length*Petal.Length").color("Species").size("Sepal.Width")
Car數據集折線圖
圖形語法:
g2chart.line().position("id*speed");
切換到極坐標:
圖形語法:
g2chart.line().position("id*speed");
g2chart.coord("polar");
Berkeley數據柱狀圖
數據處理:
SELECT SUM(Freq)asf,Gender FROM table GROUP BYGender
圖形語法:
g2chart.interval().position("Gender*f").color("Gender").label("f");
Berkeley數據堆疊柱狀圖
數據處理:
SELECT SUM(Freq) as f , Gender , Admit FROM table GROUP BY Gender, Admit
圖形語法:
g2chart.intervalStack().position("Gender*f").color("Admit")
Berkeley數據餅圖
數據處理:
SELECT SUM(Freq) as f , Gender FROM table GROUP BY Gender
圖形語法:
g2chart.intervalStack().position("f").color("Gender").label("f");
g2chart.coord("theta")
Berkeley數據分面的應用
圖形語法:
g2chart.facet("rect",{
fields:["Dept","Admit"],
eachView(view){
view.coord("theta");
view.intervalStack().position("Freq").color("Gender");
}
});
更多的分析圖形留給大家去嘗試
總結
本文分享了一個利用純前端技術構建一個類似Tableau的BI應用的例子,整個代碼統計:
JS 370 行 JS6
HTML 69 + 9 + 5 = 83 行
CSS 21 行
總計474 行,用這麼少的代碼就能完成一個看上去還不錯的BI工具,還算不錯吧。當然這裡主要是由於開源社區提供了這麼多好的前端庫以供應用,我要做的僅僅是讓它們有效的工作在一起。這個只能算是個原型,從功能和質量上來說都不成熟,但是能在瀏覽器中不藉助任何的伺服器來實現BI的數據分析功能,應該會有很多人想要在自己的應用中嵌一個吧?
結合我之前分享的TensorflowJS的文章,下面一步可能是加入預測功能,為數據分析加入智能,前端應用的前景,不可限量!
參考
axios 基於Promise的HTTP客戶端
alasql 基於JS的開源SQL資料庫
jquery datatable JQuery的數據表格插件
select2 JQuery的選擇控制項插件
相關博客 使用開源軟體快速搭建數據分析平台
相關博客 數據可視化中的視覺屬性
覺得本文對你有幫助?請分享給更多人
關注「前端大全」,提升前端技能
TAG:前端大全 |