1. 目標
從 echarts 源碼入手了解如何添加定制的圖表組件。
2. echarts 圖表的使用方法
2.1 基本運行環(huán)境
- 使用 vite 的 vanilla-ts 模板,通過
npm create vite@latest
命令可以按照步驟安裝- 使用 vite 的目的是可以實現(xiàn)修改代碼之后的動態(tài)刷新
- 使用 vanilla 的目的是可以使用純粹的 js德澈,并不引入其他的庫
- 使用 ts 的目的是可以方便的跳轉(zhuǎn)各個方法垂寥,方便查找源碼凌摄,畢竟 echarts 實際也是使用 ts 編寫的浪秘。
- 執(zhí)行
npm install
安裝對應(yīng)的包需频。之后運行npm run dev
就可以進入 vite 的例子程序了 - 其中 main.ts 是程序的入口文件丁眼,可以在其中增加一些鏈接,來跳轉(zhuǎn)子頁面昭殉。之后苞七,就在子頁面中調(diào)試獨立的 echarts 相關(guān)內(nèi)容
<div><a href="/subpages/echarts_init.html"> 初始化 echart 并使用 </a></div>
2.2 echarts 基本圖表庫使用
2.2.1 使用 ts 按需引入 echarts 相關(guān)資源并建立配置文件
- 參考 echarts 官網(wǎng)的使用例子,在項目中引入 Apache ECharts
- 安裝 echarts 包
npm install echarts --save
- 按需引入相關(guān)資源
// 引入 echarts 核心包挪丢。用于初始化圖表
import * as echarts from 'echarts/core';
// 引入主要的圖表類型蹂风,以及對應(yīng)的選項類型
// 系列類型的定義后綴都為 SeriesOption
import {
BarChart, // 柱狀圖
BarSeriesOption, // 柱狀圖選項
} from 'echarts/charts';
// 引入圖表使用的附加組件
// 組件類型的定義后綴都為 ComponentOption
import {
TitleComponent, // 標題組件
TitleComponentOption, // 標題組件選項
TooltipComponent, // 提示框組件
TooltipComponentOption, // 提示框組件選項
GridComponent, // 網(wǎng)格組件,或者叫做子圖組件
GridComponentOption, // 網(wǎng)格組件選項
DatasetComponent, // 數(shù)據(jù)集組件
DatasetComponentOption, // 數(shù)據(jù)集組件選項
} from 'echarts/components';
// 引入 canvas 圖表渲染
import { CanvasRenderer } from 'echarts/renderers';
// 通過 ComposeOption 來組合出一個只有必須組件和圖表的 Option 類型
type ECOption = echarts.ComposeOption<
| BarSeriesOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| DatasetComponentOption
>;
// 注冊必須的組件
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
BarChart,
CanvasRenderer
]);
// 建立 echarts 的圖表配置
const option: ECOption = {
// 標題組件配置
title: {
text: 'ECharts 入門示例'
},
// 提示框組件配置
tooltip: {},
// x 軸配置
xAxis: {
data: ['襯衫', '羊毛衫', '雪紡衫', '褲子', '高跟鞋', '襪子']
},
// y 軸配置
yAxis: {},
// 數(shù)據(jù)系列配置
series: [
{
name: '銷量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
}
// 初始化圖表乾蓬,設(shè)置配置項惠啄,完成圖表的顯示
// 這里將 myChart 顯示到 id 為 main 的 DOM 節(jié)點之上,參考對應(yīng) html 文件
const myChart = echarts.init(document.getElementById('main'));
myChart.setOption(option);
2.2.2 建立 html 顯示 echarts 圖表
- 建立一個子頁面到 /subpages/echarts_init.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Echart init</title>
</head>
<body>
<div id="app">
<div id="main" style="width: 800px; height:600px;"></div>
</div>
<script type="module" src="/src/subpages/echart_init.ts"></script>
</body>
</html>
- 其中使用了 es6 的模塊語法引入了我們寫的 echart_init.ts 文件
- 其中 id 為 main 的 div 節(jié)點,之后會用于顯示 myChart 圖表撵渡,同時還設(shè)置了其顯示大小融柬。
2.2.3 顯示效果
myChart的顯示效果
- 鼠標懸浮在每個系列的柱狀圖上面會顯示 toolTip
- 刷新頁面會顯示動畫
3. echarts 圖表的渲染邏輯
由于 echarts 的手冊主要是說明其每個圖表的使用方法,對于底層是如何處理的并沒有詳細介紹趋距,也沒有具體介紹如果需要擴展 echarts 圖表應(yīng)當具體如何去做丹鸿。
所以,需要首先從 echarts 的渲染邏輯入手棚品,逐步了解 echarts 是如何組織圖表上的各個組件元素的布局靠欢,如何共享數(shù)據(jù),以及如何最終完成渲染铜跑。
3.1 查找入口點
- 查看上面最簡單的 echarts 示例代碼门怪,得知整個圖表的渲染通過 echarts.init 函數(shù)處理的。所以以這個函數(shù)為入手點查看具體的渲染邏輯锅纺。
- 在 vscode 編輯器之中掷空,通過右鍵導航的方式查看 echarts.init 方法。
- 發(fā)現(xiàn)定位到了 node_modules\echarts\types\dist\shared.d.ts 這個文件之中囤锉。
- shared.d.ts 是 echars 的包公開的全部的類型和方法坦弟,我們在使用的時候基本上也是使用其中的類型和方法。上面代碼之中的 BarChart官地、
TitleComponent酿傍、BarSeriesOption等圖表、組件和選項也都指向到這個文件驱入。曾經(jīng)嘗試直接使用 node_modules\echarts 里面的源代碼直接使用赤炒,但是發(fā)現(xiàn)出現(xiàn)很多類型沖突的問題。因此在定制圖表的時候亏较,盡量還是使用官方公開的接口莺褒。
3.2 查看 echarts 的源代碼
- shared.d.ts 找到了入口函數(shù)但是并不包含源代碼,還是無法了解業(yè)務(wù)邏輯雪情,因此需要找到源代碼遵岩。
- 在 node_modules\echarts\lib 文件夾包含了 echars 的全部 js 文件源代碼。是很有參考意義的巡通。但是這部分代碼的可讀性并不好尘执。因為他們是通過 ts 生成的。很多 ts 的語法糖已經(jīng)轉(zhuǎn)化為 js 代碼扁达。如果對 js 和 ts 的映射關(guān)系不熟正卧,是無法了解的。
- 所以還是要看到 ts 源碼跪解,這些源碼炉旷,只有去 github 尋找了签孔。echars源碼 可以在 github上進行搜索,這樣就可以知道每個具體的函數(shù)是在什么地方定義的以及在什么地方使用了窘行。
- 為了方便搜索饥追,可以將源碼下載位 zip 文件 echarts-master.zip,解壓縮之后并用 vscode 打開進行搜索罐盔。由于并不是為了重新生成 echarts 的包但绕,因此不需要安裝對應(yīng)的依賴并運行。
3.3 查看 echarts.init 函數(shù)
- 有了源代碼惶看,就可以查看 echarts.init 函數(shù)是如何具體實現(xiàn)的了捏顺。
- 找到 echarts-master\src\echarts.ts 這個文件。其中定義了 init 方法纬黎。
export * from './export/core';
import { use } from './extension';
import { init } from './core/echarts';
import {install as CanvasRenderer} from './renderer/installCanvasRenderer';
import {install as DatasetComponent} from './component/dataset/install';
// Default to have canvas renderer and dataset for compitatble reason.
use([CanvasRenderer, DatasetComponent]);
// TODO: Compatitable with the following code
// import echarts from 'echarts/lib/echarts'
export default {
init() {
if (__DEV__) {
/* eslint-disable-next-line */
console.error(`"import echarts from 'echarts/lib/echarts'" is not supported anymore. Use "import * as echarts from 'echarts/lib/echarts'" instead;`);
}
// @ts-ignore
return init.apply(null, arguments);
}
};
// Import label layout by default.
// TODO remove
import {installLabelLayout} from './label/installLabelLayout';
use(installLabelLayout);
- 源代碼之中包含了不少附加的方法調(diào)用幅骄,比如為了兼容性和設(shè)定默認值,使用use方法加載了CanvasRenderer, DatasetComponent本今、installLabelLayout等拆座。
- 但是核心的還是 export default 之中包含的 init 函數(shù)。這個函數(shù)沒有復(fù)雜的函數(shù)體冠息,真正的代碼是通過
import { init } from './core/echarts';
引入的挪凑,并把傳入的參數(shù)通過 argument 傳遞給引入的 init 函數(shù)。 - 因此進入對應(yīng)的文件查看 echarts-master\src\core\echarts.ts逛艰,這個文件就很大了躏碳。由于我們關(guān)注的是 init 函數(shù),因此瓮孙。只看這一部分代碼
/**
* @param opts.devicePixelRatio Use window.devicePixelRatio by default
* @param opts.renderer Can choose 'canvas' or 'svg' to render the chart.
* @param opts.width Use clientWidth of the input `dom` by default.
* Can be 'auto' (the same as null/undefined)
* @param opts.height Use clientHeight of the input `dom` by default.
* Can be 'auto' (the same as null/undefined)
* @param opts.locale Specify the locale.
* @param opts.useDirtyRect Enable dirty rectangle rendering or not.
*/
export function init(
dom: HTMLElement,
theme?: string | object,
opts?: EChartsInitOpts
): EChartsType {
const isClient = !(opts && opts.ssr);
if (isClient) {
if (__DEV__) {
if (!dom) {
throw new Error('Initialize failed: invalid dom.');
}
}
const existInstance = getInstanceByDom(dom);
if (existInstance) {
if (__DEV__) {
warn('There is a chart instance already initialized on the dom.');
}
return existInstance;
}
if (__DEV__) {
if (isDom(dom)
&& dom.nodeName.toUpperCase() !== 'CANVAS'
&& (
(!dom.clientWidth && (!opts || opts.width == null))
|| (!dom.clientHeight && (!opts || opts.height == null))
)
) {
warn('Can\'t get DOM width or height. Please check '
+ 'dom.clientWidth and dom.clientHeight. They should not be 0.'
+ 'For example, you may need to call this in the callback '
+ 'of window.onload.');
}
}
}
const chart = new ECharts(dom, theme, opts);
chart.id = 'ec_' + idBase++;
instances[chart.id] = chart;
isClient && modelUtil.setAttribute(dom, DOM_ATTRIBUTE_KEY, chart.id);
enableConnect(chart);
lifecycle.trigger('afterinit', chart);
return chart;
}
- 在這個函數(shù)之中唐断,首先要查看參數(shù)
- dom: HTMLElement 渲染的dom節(jié)點
- theme?: string | object, 可選的渲染主題,明亮或者黑暗
- opts?: EChartsInitOpts 圖表的配置項杭抠,也就是剛才的 ECOption
- 這個函數(shù)會返回一個 EChartsType 的對象,這個實際就是一個具體的圖表了
- 接下來查看函數(shù)體恳啥。忽略掉其中的 DEV 相關(guān)的調(diào)試信息偏灿,得到的簡化代碼,根據(jù)代碼填寫對應(yīng)的注釋
export function init(
dom: HTMLElement,
theme?: string | object,
opts?: EChartsInitOpts
): EChartsType {
// 判定是否是客戶端钝的,并不清楚是什么意思翁垂。
// 推測是如果選項不存在,就是客戶端硝桩。通過 getInstanceByDom 獲得實例沿猜。ssr 為服務(wù)端渲染。因此如果是服務(wù)端渲染就直接顯示圖片即可碗脊。
// 測試的基本代碼包含opts啼肩,因此跳過這部分
const isClient = !(opts && opts.ssr);
if (isClient) {
const existInstance = getInstanceByDom(dom);
if (existInstance) {
return existInstance;
}
}
// 此處建立了一個 echarts 對象。并將 dom theme opts 傳給了構(gòu)造函數(shù)
const chart = new ECharts(dom, theme, opts);
// 此處設(shè)定了圖表的id
chart.id = 'ec_' + idBase++;
// 此處將生成的chart添加到instances,估計為了之后可以統(tǒng)一管理
instances[chart.id] = chart;
// 是客戶端才進行處理祈坠,跳過
isClient && modelUtil.setAttribute(dom, DOM_ATTRIBUTE_KEY, chart.id);
// 并不清楚這部分功能害碾,暫時跳過
enableConnect(chart);
// 觸發(fā)生存期的事件,初始化完成
lifecycle.trigger('afterinit', chart);
// 返回對應(yīng)的chart
return chart;
}
- 通過查看赦拘,發(fā)現(xiàn)真正的初始化函數(shù)還是在 ECharts 對象的構(gòu)造函數(shù)之中慌随。這類也是在 echarts-master\src\core\echarts.ts 這個文件中定義的。這個類也很大躺同。包含的屬性和函數(shù)也很多阁猜。首先查看一下構(gòu)造函數(shù) constructor,參考其中的注釋蹋艺,盡量補全其中的邏輯蹦漠。同樣刪除 DEV相關(guān)的調(diào)試信息
class ECharts extends Eventful<ECEventDefinition> {
constructor(
dom: HTMLElement,
theme?: string | ThemeOption,
opts?: EChartsInitOpts
) {
// 調(diào)用父類構(gòu)造函數(shù),處理事件功能
super(new ECEventProcessor());
// 確保配置選項不為undefined
opts = opts || {};
// Get theme by name 獲取主題名字车海,跳過
if (isString(theme)) {
theme = themeStorage[theme] as object;
}
// 設(shè)置dom節(jié)點
this._dom = dom;
// 設(shè)置默認的渲染器
let defaultRenderer = 'canvas';
// 設(shè)置默認的粗指針笛园,不理解?跳過
let defaultCoarsePointer: 'auto' | boolean = 'auto';
// 設(shè)置默認不使用臟矩形侍芝,似乎與渲染有關(guān)
let defaultUseDirtyRect = false;
// 初始化zrender研铆,zrender 是 echarts 使用的底層渲染庫。
// 封裝了canvas和svg的繪制接口州叠。
// 其中的參數(shù)都是與具體的渲染相關(guān)的棵红,包括尺寸,分辨率咧栗。
const zr = this._zr = zrender.init(dom, {
renderer: opts.renderer || defaultRenderer,
devicePixelRatio: opts.devicePixelRatio,
width: opts.width,
height: opts.height,
ssr: opts.ssr,
useDirtyRect: retrieve2(opts.useDirtyRect, defaultUseDirtyRect),
useCoarsePointer: retrieve2(opts.useCoarsePointer, defaultCoarsePointer),
pointerSize: opts.pointerSize
});
// 記錄是否是服務(wù)端渲染逆甜,跳過
this._ssr = opts.ssr;
// Expect 60 fps. 設(shè)定刷新頻率。使用了節(jié)流器致板,1000ms/60 = 16.666 ms
this._throttledZrFlush = throttle(bind(zr.flush, zr), 17);
// 設(shè)置主題交煞,跳過
theme = clone(theme);
theme && backwardCompat(theme as ECUnitOption, true);
this._theme = theme;
// 設(shè)置本地化,跳過
this._locale = createLocaleObject(opts.locale || SYSTEM_LANG);
// 設(shè)置坐標系統(tǒng)管理器斟或,好像很重要素征,與繪制相關(guān)
this._coordSysMgr = new CoordinateSystemManager();
// 設(shè)置 api,重要萝挤,之后需要通過 api 獲取很多信息
const api = this._api = createExtensionAPI(this);
// Sort on demand 設(shè)置按需排序御毅,跳過
function prioritySortFunc(a: StageHandlerInternal, b: StageHandlerInternal): number {
return a.__prio - b.__prio;
}
timsort(visualFuncs, prioritySortFunc);
timsort(dataProcessorFuncs, prioritySortFunc);
// 設(shè)置調(diào)度器,不明白怜珍,跳過
this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs);
// 設(shè)置消息中心
this._messageCenter = new MessageCenter();
// Init mouse events 初始化鼠標事件端蛆,
// 比較重要,自定義的時候用戶交互需要用到
this._initEvents();
// In case some people write `window.onresize = chart.resize`
// 處理一些異常
this.resize = bind(this.resize, this);
// 響應(yīng) zrender 的單幀動畫
zr.animation.on('frame', this._onframe, this);
// 綁定 zrender 渲染事件
bindRenderedEvent(zr, this);
// 綁定 zrender 鼠標事件
bindMouseEvent(zr, this);
// ECharts instance can be used as value.
// 將 ECharts 實例設(shè)置為可以為 value
// 主要是 zrender 使用的
setAsPrimitive(this);
}
}
- 從上面的構(gòu)造函數(shù)可以知道酥泛,ECharts 實例主要是完成了一些基礎(chǔ)資源的綁定今豆。其中包括:zrender嫌拣,坐標系統(tǒng)管理器,事件綁定等晚凿。
- 真正包含渲染數(shù)據(jù)的 opts 并沒有在構(gòu)造函數(shù)里面使用亭罪。因此一定有其他的地方使用了 opts 從而完成了渲染。
3.4 查找渲染入口
- 重新查看了示例代碼歼秽,發(fā)現(xiàn)在初始化 myChart 之后应役,真正繪制圖表是使用的
myChart.setOption(option);
這個語句。 - 因此需要再次查看 ECharts 實例的 setOption 方法燥筷。這個方法在官網(wǎng)有更加詳細的說明
- 其中主要涉及了如何將 option 進行合并箩祥。
- 為了確認是否是通過 setOption這個函數(shù)觸發(fā)圖表的渲染。使用了 chrome 瀏覽器的調(diào)試功能肆氓。
- 在控制臺的 Source 頁袍祖,查找 BarView.js 文件。
- 這個文件實際是在網(wǎng)站的 http://localhost:5173/node_modules/echarts/lib/chart/bar/BarView.js 位置谢揪,也就是lib 的文件夾之中蕉陋。
- 其中 BarView.prototype.render 就是渲染柱狀圖的函數(shù)。
- 在這個渲染函數(shù)上增加斷點拨扶,之后刷新示例的頁面凳鬓,就會進入斷點。
-
進一步的在 Source 頁的右側(cè) call stack 之中可以查看到調(diào)用堆棧患民。
image.png - 在調(diào)用堆棧中可以看到缩举,確實是在 setOption 之中通過 update 方法,進而調(diào)用了 render 方法
- 這些方法都是在 /src/core/echart.ts 之中
- 下面就可以查看 setOption 函數(shù)的主要功能