從零開發(fā)可視化大屏制作平臺(技術(shù)拆解版)
最近由于可視化大屏的服務(wù)器到期了, 用于演示的網(wǎng)站換了域名, 如果大家想體驗(yàn)或者學(xué)習(xí)可視化大屏設(shè)計思路, 可以移步新網(wǎng)址:
https://v6.dooring.vip
賬號: xujiang156@qq.com
密碼: 12345678
接下來筆者就來帶大家一起看看我對的V6.Dooring可視化大屏方案設(shè)計和技術(shù)實(shí)現(xiàn)。
你將收獲
- 可視化大屏產(chǎn)品設(shè)計思路
- 主流可視化圖表庫技術(shù)選型
- 大屏編輯器設(shè)計思路
- 大屏可視化編輯器Schema設(shè)計
- 用戶數(shù)據(jù)自治探索
在介紹之前, 我們先看看實(shí)現(xiàn)的效果展示.
效果預(yù)覽
方案實(shí)現(xiàn)
可視化大屏產(chǎn)品設(shè)計思路
目前很多企業(yè)或多或少的面臨“信息孤島”問題,各個系統(tǒng)平臺之間的數(shù)據(jù)無法實(shí)現(xiàn)互通共享,難以實(shí)現(xiàn)一體化的數(shù)據(jù)分析和實(shí)時呈現(xiàn)。
相比于傳統(tǒng)手工定制的圖表與數(shù)據(jù)儀表盤,可視化大屏制作平臺的出現(xiàn),可以打破抵消的定制開發(fā), 數(shù)據(jù)分散的問題,通過數(shù)據(jù)采集、清洗、分析到直觀實(shí)時的數(shù)據(jù)可視化展現(xiàn),能夠多方位、多角度、全景展現(xiàn)各項(xiàng)指標(biāo),實(shí)時監(jiān)控,動態(tài)一目了然。
針對以上需求, 我們設(shè)計了一套可視化大屏解決方案, 具體包含如下幾點(diǎn):
上圖是筆者4個月前設(shè)計的基本草圖, 后期會持續(xù)更新. 通過以上的設(shè)計分解, 我們基本可以搭建一個可自己定制的數(shù)據(jù)大屏。
主流可視化圖表庫技術(shù)選型
圖片
目前筆者調(diào)研的已知主流可視化庫有:
- echart 一個基于 JavaScript 的老牌開源可視化圖表庫。
- D3.js 一個數(shù)據(jù)驅(qū)動的可視化庫, 可以不需要其他任何框架獨(dú)立運(yùn)行在現(xiàn)代瀏覽器中,它結(jié)合強(qiáng)大的可視化組件來驅(qū)動 DOM 操作。
- antv 包含一套完整的可視化組件體系。
- Chart.js 基于 HTML5 的 簡單易用的 JavaScript 圖表庫。
- metrics-graphics 建立在D3之上的可視化庫, 針對可視化和布置時間序列數(shù)據(jù)進(jìn)行了優(yōu)化。
- C3.js 通過包裝構(gòu)造整個圖表所需的代碼,使生成基于D3的圖表變得容易。
我們使用以上任何一個庫都可以實(shí)現(xiàn)我們的可視化大屏搭建的需求, 各位可以根據(jù)喜好來選擇。
大屏編輯器設(shè)計思路
在上面的分析中我們知道一個大屏編輯器需要有個編輯器核心, 主要包含以下部分:
- 組件庫
- 拖拽(自由拖拽, 參考線, 自動提示)
- 畫布渲染器
- 屬性編輯器
如下圖所示:
組件庫我們可以用任何組件封裝方式(react/vue等), 這里沿用H5-Dooring的可視化組件設(shè)計方式, 對組件模型進(jìn)行優(yōu)化和設(shè)計。
類似的代碼如下:
import { Chart } from '@antv/f2';
import React, { memo, useEffect, useRef } from 'react';
import styles from './index.less';
import { IChartConfig } from './schema';
const XChart = (props:IChartConfig) => {
const { data, color, size, paddingTop, title } = props;
const chartRef = useRef(null);
useEffect(() => {
const chart = new Chart({
el: chartRef.current || undefined,
pixelRatio: window.devicePixelRatio, // 指定分辨率
});
// step 2: 處理數(shù)據(jù)
const dataX = data.map(item => ({ ...item, value: Number(item.value) }));
// Step 2: 載入數(shù)據(jù)源
chart.source(dataX);
// Step 3:創(chuàng)建圖形語法,繪制柱狀圖,由 genre 和 sold 兩個屬性決定圖形位置,genre 映射至 x 軸,sold 映射至 y 軸
chart
.interval()
.position('name*value')
.color('name');
// Step 4: 渲染圖表
chart.render();
}, [data]);
return (
<div className={styles.chartWrap}>
<div className={styles.chartTitle} style={{ color, fontSize: size, paddingTop }}>
{title}
</div>
<canvas ref={chartRef}></canvas>
</div>
);
};
export default memo(XChart);
以上只是一個簡單的例子, 更具業(yè)務(wù)需求的復(fù)雜度我們往往會做更多的控制, 比如動畫(animation), 事件(event), 數(shù)據(jù)獲取(data inject)等。
當(dāng)然實(shí)際應(yīng)用中大屏展現(xiàn)的內(nèi)容和形式遠(yuǎn)比這復(fù)雜, 我們從上圖可以提煉出大屏頁面的2個直觀特征:
- 可視化組件集
- 空間坐標(biāo)關(guān)系
因?yàn)槲覀兛梢暬笃凛d體是頁面, 是html, 所以還有另外一個特征: 事件/交互。綜上我們總結(jié)出了可視化大屏的必備要素:
我們只要充分的理解了可視化大屏的組成和特征, 我們才能更好的設(shè)計可視化大屏搭建引擎, 基于以上分析, 我設(shè)計了一張基礎(chǔ)引擎的架構(gòu)圖:
接下來我就帶大家一起來拆解并實(shí)現(xiàn)上面的搭建引擎。
大屏搭建引擎核心功能實(shí)現(xiàn)
俗話說: “好的拆解是成功的一半”, 任何一個復(fù)雜任務(wù)或者系統(tǒng), 我們只要能將其拆解成很多細(xì)小的子模塊, 就能很好的解決并實(shí)現(xiàn)它. (學(xué)習(xí)也是一樣)。
接下來我們就逐一解決上述基礎(chǔ)引擎的幾個核心子模塊:
- 拖拽器實(shí)現(xiàn)
- 物料中心設(shè)計
- 動態(tài)渲染器實(shí)現(xiàn)
- 配置面板設(shè)計
- 控制中心概述
- 功能輔助設(shè)計
拖拽器實(shí)現(xiàn)
拖拽器是可視化搭建引擎的核心模塊, 也是用來解決上述提到的大屏頁面特征中的“空間坐標(biāo)關(guān)系”這一問題。我們先來看一下實(shí)現(xiàn)效果:
組件拖拽可以采用市面已有的Dragable等插件, 也可以采用H5-Dooring的智能網(wǎng)格拖拽. 這里筆者選擇自由拖拽來實(shí)現(xiàn). 已有的有:
- rc-drag
- sortablejs
- react-dnd
- react-dragable
- vue-dragable
等等. 具體拖拽呈現(xiàn)流程如下:
具體拖拽流程就是:
- 使用H5 dragable API拖拽左側(cè)組件(component data)進(jìn)入目標(biāo)容器(targetBox)。
- 監(jiān)聽拖拽結(jié)束事件拿到拖拽事件傳遞的data來渲染真實(shí)的可視化組件。
- 可視化組件掛載, schema注入編輯面板, 編輯面板渲染組件屬性編輯器。
- 拖拽, 屬性修改, 更新。
- 預(yù)覽, 發(fā)布。
組件的schema參考Dooring DSL設(shè)計。
物料中心設(shè)計
物料中心主要為大屏頁面提供“原材料”。為了設(shè)計健壯且通用的物料, 我們需要設(shè)計一套標(biāo)準(zhǔn)組件結(jié)構(gòu)和屬性協(xié)議。并且為了方便物料管理和查詢, 我們還需要對物料進(jìn)行分類, 我的分類如下:
- 可視化組件 (柱狀圖, 餅圖, 條形圖, 地圖可視化等)。
- 修飾型組件 (圖片, 輪播圖, 修飾素材等)。
- 文字類組件 (文本, 文本跑馬燈, 文字看板)。
具體的物料庫演示如下:
這里我拿一個可視化組件的實(shí)現(xiàn)來舉例說明:
import React, { memo, useEffect } from 'react'
import { Chart } from '@antv/g2'
import { colors } from '@/components/BasicShop/common'
import { ChartConfigType } from './schema'
interface ChartComponentProps extends ChartConfigType {
id: string
}
const ChartComponent: React.FC<ChartComponentProps> = ({
id, data, width, height,
toggle, legendPosition, legendLayout, legendShape,
labelColor, axisColor, multiColor, tipEvent, titleEvent,
dataType, apiAddress, apiMethod, apiData, refreshTime,
}) => {
useEffect(() => {
let timer:any = null;
const chart = new Chart({
container: `chart-${id}`,
autoFit: true,
width,
height
})
// 數(shù)據(jù)過濾, 接入
const dataX = data.map(item => ({ ...item, value: Number(item.value) }))
chart.data(dataX)
// 圖表屬性組裝
chart.legend(
toggle
? {
position: legendPosition,
layout: legendLayout,
marker: {
symbol: legendShape
},
}
: false,
)
chart.tooltip({
showTitle: false,
showMarkers: false,
})
// 其他圖表信息源配置, 方法雷同, 此處省略
// ...
chart.render()
}, [])
return <div id={`chart-${id}`} />
}
export default memo(ChartComponent)
以上就是我們的基礎(chǔ)物料的實(shí)現(xiàn)模式, 可視化組件采用了g2, 當(dāng)然大家也可以使用熟悉的echart, D3.js等. 不同物料既有通用的 props , 也有專有的 props, 取決于我們?nèi)绾味x物料的Schema。
在設(shè)計 Schema 前我們需要明確組件的屬性劃分, 為了滿足組件配置的靈活性和通用性, 我做了如下劃分:
- 外觀屬性 (組件寬高, 顏色, 標(biāo)簽, 展現(xiàn)模式等)。
- 數(shù)據(jù)配置 (靜態(tài)數(shù)據(jù), 動態(tài)數(shù)據(jù))。
- 事件/交互 (如單擊, 跳轉(zhuǎn)等)。
有了以上劃分, 我們就可以輕松設(shè)計想要的通用Schema了。我們先來看看實(shí)現(xiàn)后的配置面板:
這些屬性項(xiàng)都是基于我們定義的schema
配置項(xiàng), 通過 解析引擎 動態(tài)渲染出來的, 有關(guān) 解析引擎 和配置面板, 我會在下面的章節(jié)和大家介紹。我們先看看組件的 schema 結(jié)構(gòu):
const Chart: ChartSchema = {
editAttrs: [
{
key: 'layerName',
type: 'Text',
cate: 'base',
},
{
key: 'y',
type: 'Number',
cate: 'base',
},
...DataConfig, // 數(shù)據(jù)配置項(xiàng)
...eventConfig, // 事件配置項(xiàng)
],
config: {
width: 200,
height: 200,
zIndex: 1,
layerName: '柱狀圖',
labelColor: 'rgba(188,200,212,1)',
// ... 其他配置初始值
multiColor: ['rgba(91, 143, 249, 1)', 'rgba(91, 143, 249, 1)', 'rgba(91, 143, 249,,1)', 'rgba(91, 143, 249, 1)'],
data: [
{
name: 'A',
value: 25,
},
{
name: 'B',
value: 66,
}
],
},
}
其中 editAttrs 表示可編輯的屬性列表, config 為屬性的初始值, 當(dāng)然大家也可以根據(jù)自己的喜好, 設(shè)計類似的通用schema。
我們通過以上設(shè)計的標(biāo)準(zhǔn)組件和標(biāo)準(zhǔn)schema, 就可以批量且高效的生產(chǎn)各種物料, 還可以輕松集成任何第三方可視化組件庫。
動態(tài)渲染器實(shí)現(xiàn)
我們都知道, 一個頁面中元素很多時會影響頁面整體的加載速度, 因?yàn)闉g覽器渲染頁面需要消耗CPU / GPU。對于可視化頁面來說, 每一個可視化組件都需要渲染大量的信息元, 這無疑會對頁面性能造成不小的影響, 所以我們需要設(shè)計一種機(jī)制, 讓組件異步加載到畫布上, 而不是一次性加載幾十個幾百個組件(這樣的話頁面會有大量的白屏?xí)r間, 用戶體驗(yàn)極度下降)。
動態(tài)加載器就是提供了這樣一種機(jī)制, 保證組件的加載都是異步的, 一方面可以減少頁面體積, 另一方面用戶可以更早的看到頁面元素。目前我們熟的動態(tài)加載機(jī)制也有很多, Vue 和 React 生態(tài)都提供了開箱即用的解決方案(雖然我們可以用 webpack 自行設(shè)計這樣的動態(tài)模型, 此處為了提高行文效率, 我們直接基于現(xiàn)成方案封裝)。我們先看一下動態(tài)渲染組件的過程:
上面的演示可以細(xì)微的看出從左側(cè)組件菜單拖動某個組件圖標(biāo)到畫布上后, 真正的組件才開始加載渲染。
這里我們以 umi3.0 提供的 dynamic 函數(shù)來最小化實(shí)現(xiàn)一個動態(tài)渲染器. 如果不熟悉 umi 生態(tài)的朋友, 也不用著急, 看完我的實(shí)現(xiàn)過程和原理之后, 就可以利用任何熟悉的動態(tài)加載機(jī)制實(shí)現(xiàn)它了。實(shí)現(xiàn)如下:
import React, { useMemo, memo, FC } from 'react'
import { dynamic } from 'umi'
import LoadingComponent from '@/components/LoadingComponent'
const DynamicFunc = (cpName: string, category: string) => {
return dynamic({
async loader() {
// 動態(tài)加載組件
const { default: Graph } = await import(`@/components/materies/${cpName}`)
return (props: DynamicType) => {
const { config, id } = props
return <Graph {...config} id={id} />
}
},
loading: () => <LoadingComponent />
})
}
const DynamicRenderEngine: FC<DynamicType> = memo((props) => {
const {
type,
config,
// 其他配置...
} = props
const Dynamic = useMemo(() => {
return DynamicFunc(config)
}, [config])
return <Dynamic {...props} />
})
export default DynamicRenderEngine
是不是很簡單? 當(dāng)然我們也可以根據(jù)自身業(yè)務(wù)需要, 設(shè)計更復(fù)雜強(qiáng)大的動態(tài)渲染器。
配置面板設(shè)計
實(shí)現(xiàn)配置面板的前提是對組件 Schema 結(jié)構(gòu)有一個系統(tǒng)的設(shè)計, 在介紹組件庫實(shí)現(xiàn)中我們介紹了通用組件 schema 的一個設(shè)計案例, 我們基于這樣的案例結(jié)構(gòu), 來實(shí)現(xiàn) 動態(tài)配置面板。
由上圖可以知道, 動態(tài)配置面板的一個核心要素就是 表單渲染器。表單渲染器的目的就是基于屬性配置列表 attrs 來動態(tài)渲染出對應(yīng)的表單項(xiàng)。我之前寫了一篇文章詳細(xì)的介紹了表單設(shè)計器的技術(shù)實(shí)現(xiàn)的文章, 大家感興趣也可以參考一下: Dooring可視化之從零實(shí)現(xiàn)動態(tài)表單設(shè)計器。
我這里來簡單實(shí)現(xiàn)一個基礎(chǔ)的表單渲染器模型:
const FormEditor = (props: FormEditorProps) => {
const { attrs, defaultValue, onSave } = props;
const onFinish = (values: Store) => {
// 保存配置項(xiàng)數(shù)據(jù)
onSave && onSave(values);
};
const handlechange = (value) => {
// 更新邏輯
}
const [form] = Form.useForm();
return (
<Form
form={form}
{...formItemLayout}
notallow={onFinish}
initialValues={defaultValue}
notallow={handlechange}
>
{
attrs.map((item, i) => {
return (
<React.Fragment key={i}>
{item.type === 'Number' && (
<Form.Item label={item.name} name={item.key}>
<InputNumber />
</Form.Item>
)}
{item.type === 'Text' && (
<Form.Item label={item.name} name={item.key}>
<Input placeholder={item.placeholder} />
</Form.Item>
)}
{item.type === 'TextArea' && (
<Form.Item label={item.name} name={item.key}>
<TextArea rows={4} />
</Form.Item>
)}
// 其他配置類型
</React.Fragment>
);
})}
</Form>
);
};
如果大家想看更完整的配置面板實(shí)現(xiàn), 可以參考開源項(xiàng)目 H5-Dooring | H5可視化編輯器
我們可以看看最終的配置面板實(shí)現(xiàn)效果:
控制中心概述 & 功能輔助設(shè)計
控制中心的實(shí)現(xiàn)主要是業(yè)務(wù)層的, 沒有涉及太多復(fù)雜的技術(shù), 所以這里我簡單介紹一下。因?yàn)榭梢暬笃另撁嬲故镜男畔⒂行┛赡苁撬矫軘?shù)據(jù), 只希望一部分人看到, 所以我們需要對頁面的訪問進(jìn)行控制。其次由于企業(yè)內(nèi)部業(yè)務(wù)戰(zhàn)略需求, 可能會對頁面進(jìn)行各種驗(yàn)證, 狀態(tài)校驗(yàn), 數(shù)據(jù)更新頻率等, 所以我們需要設(shè)計一套控制中心來管理。最基本的就是訪問控制, 如下:
功能輔助設(shè)計 主要是一些用戶操作上的優(yōu)化, 比如快捷鍵, 畫布縮放, 大屏快捷導(dǎo)航, 撤銷重做等操作, 這塊可以根據(jù)具體的產(chǎn)品需求來完善。大家后期設(shè)計搭建產(chǎn)品時也可以參考實(shí)現(xiàn)。
可視化大屏數(shù)據(jù)自治探索
目前我們實(shí)現(xiàn)的搭建平臺可以靜態(tài)的設(shè)計數(shù)據(jù)源, 也可以注入第三方接口, 如下:
我們可以調(diào)用內(nèi)部接口來實(shí)時獲取數(shù)據(jù), 這塊在可視化監(jiān)控平臺用的場景比較多, 方式如下:
參數(shù)(params)編輯區(qū)可以自定義接口參數(shù). 代碼編輯器筆者這里推薦兩款, 大家可以選用:
- react-monaco-editor
- react-codemirror2
使用以上之一可以實(shí)現(xiàn)mini版vscode, 大家也可以嘗試一下。
輔助功能
可視化大屏一鍵截圖 一鍵截圖功能還是沿用H5-Dooring 的快捷截圖方案, 主要用于對大屏的分享, 海報制作等需求, 我們可以使用以下任何一個組件實(shí)現(xiàn):
- dom-to-image
- html2canvas
- 撤銷重做
撤銷重做功能我們可以使用已有的庫比如react-undo, 也可以自己實(shí)現(xiàn), 實(shí)現(xiàn)原理:
有點(diǎn)鏈表的意思, 我們將每一個狀態(tài)存儲到數(shù)組中, 通過指針來實(shí)現(xiàn)撤銷重做的功能, 如果要想更健壯一點(diǎn), 我們可以設(shè)計一套“狀態(tài)淘汰機(jī)制”, 設(shè)置可保留的最大狀態(tài)數(shù), 之前的自動淘汰(刪除, 更高大上一點(diǎn)的叫出棧). 這樣可以避免復(fù)雜操作中的大量狀態(tài)存儲, 節(jié)約瀏覽器內(nèi)存。
標(biāo)尺參考線 標(biāo)尺和參考線這里我們自己實(shí)現(xiàn), 通過動態(tài)dom渲染來實(shí)現(xiàn)參考線在縮放后的動態(tài)收縮, 實(shí)現(xiàn)方案核心如下:
arr.forEach(el => {
let dom = [...Array.from(el.querySelectorAll('.calibrationNumber'))][0] as HTMLElement;
if (dom) {
dom.style.transform = `translate3d(-4px, -8px, 0px) scale(${(multiple + 0.1).toFixed(
1,
)})`;
}
});