echarts 圖表定制 01

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 核心包挪丢。用于初始化圖表
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ù)的主要功能
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末匹颤,一起剝皮案震驚了整個濱河市仅孩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌印蓖,老刑警劉巖辽慕,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異另伍,居然都是意外死亡鼻百,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門摆尝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人因悲,你說我怎么就攤上這事堕汞。” “怎么了晃琳?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵讯检,是天一觀的道長琐鲁。 經(jīng)常有香客問我,道長人灼,這世上最難降的妖魔是什么围段? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮投放,結(jié)果婚禮上奈泪,老公的妹妹穿的比我還像新娘。我一直安慰自己灸芳,他們只是感情好涝桅,可當我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著烙样,像睡著了一般冯遂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谒获,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天蛤肌,我揣著相機與錄音,去河邊找鬼批狱。 笑死裸准,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的精耐。 我是一名探鬼主播狼速,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼卦停!你這毒婦竟也來了向胡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤惊完,失蹤者是張志新(化名)和其女友劉穎僵芹,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體小槐,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡拇派,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了凿跳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片件豌。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖控嗜,靈堂內(nèi)的尸體忽然破棺而出茧彤,到底是詐尸還是另有隱情,我是刑警寧澤疆栏,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布曾掂,位于F島的核電站惫谤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏珠洗。R本人自食惡果不足惜溜歪,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望许蓖。 院中可真熱鬧蝴猪,春花似錦、人聲如沸蛔糯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蚁飒。三九已至动壤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間淮逻,已是汗流浹背琼懊。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留爬早,地道東北人哼丈。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像筛严,于是被迫代替她去往敵國和親醉旦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,107評論 2 356

推薦閱讀更多精彩內(nèi)容