「乾貨」用 Vue + Echarts 打造你的專屬可視化界面(下)

前言

接上一篇文章 《「乾貨」用 Vue + Echarts 打造你的專屬可視化界面(上)》),今天着重介紹 標記 的用法,來實現下圖中的效果。

所用的 Echarts 的版本號爲: v4.3。v-charts 的版本號爲:v1.19.0。

標記的用法有很多,今天要介紹的場景有:折線圖、柱狀圖、折線圖 + 柱狀圖。

折線圖標記 —— symbol

上圖中,折線的拐點處,一些 “小圓點”,被替換成了小圖標。

要實現這樣的效果,需要先理一下原始的需求:

  • 每種標記,代表一種活動類型。
  • 有一些活動會發生在某些時間點,或時間段內,需要在活動發生的日期上標註出該活動的類型。
  • 當同一天有多個活動發生時,採用複合圖標,並當展示 tooltip 時,顯示當日的每一個活動的信息。
  • tooltip的佈局爲:首先顯示當前日期,中段展示各個活動的圖標以及活動名稱,最後展示指標名稱和對應的數值。
  • 沒有活動的日期,拐點處與 tooltip 照常顯示原先的樣式。

所以,它的完整效果,應該是這樣的:

要實現這樣的效果,需要思考以下幾點:

  • 如何通過日期的定向匹配,將活動的圖標以“打點”的形式,定在折線拐點處。
  • 圖標的大小,要怎麼設置?它需要區別於正常的拐點標識。
  • tooltip 的樣式要如何改寫?還需要兼容沒有活動的日期樣式。

思路解析:

首先,爲了做日期的定向匹配,需要設計的數據結構如下:

data: [
    {
        id: '1, 1, 3, 2',
        date: '2019-10-10',
        name: 'test-name1, test-name2, test-name3, test-name4'
    },
    ...
]

接下來的這個 核心 屬性:symbol 是關鍵,它其實就是折線上的 拐點

symbol 支持的標記類型有:circle、rect、roundRect、triangle、diamond、pin、arrow、none。默認情況下爲 emptyCircle,也就是空心的圓。

它還支持鏈接的格式:'image://http://xxx.xxx.xxx/a/b.png'。此外,如果需要每個數據的圖形不一樣,可以設置爲如下格式的回調函數:

(value: Array|number, params: Object) => string

其中第一個參數 value 爲 data 中的數據值。第二個參數 params 是其它的數據項參數。

這些正是我們需要的。踩坑親測:上述回調函數,只有在最新版的 V4.3 中才能正常使用,否則會報錯。這也是爲何,我在一開始就先強調了 Echarts 的版本問題。具體實現如下:

<ve-line ... :extend="chartExtend"></ve-line>
...

// mock 包含標註的數據結構
dataList: [
    {
        id: '1, 1, 3, 2',
        date: '2019-10-10',
        name: 'test-name1, test-name2, test-name3, test-name4'
    },
    ...
    {
        id: '1',
        date: '2019-10-17',
        name: 'test-name1'
    }
],
...

setChartExtend () {
    this.chartExtend = {
        series: (v) => {
            Array.from(v).forEach((e, idx) => {
                e.symbol = (value, params) => {
                    return getSymbolIcon(params.name, dataList);
                };
                e.symbolSize = (value, params) => {
                    return getSymbolSize(params.name, dataList);
                };
            });
            return v;
        },
        ...
    };
},

getSymbolIcon (date, dataList) {
    const defaultSymbol = 'circle';

    if (!dataList || dataList.length === 0) {
        return defaultSymbol;
    }

    // 通過日期匹配,找到對應的標註對象
    const dataItem = dataList.find(item => item.date === date);

    const iconUrl = getSymbolUrl(dataItem.id);
    return iconUrl ? iconUrl : defaultSymbol;
},

getSymbolSize (date, dataList) {
    if (!dataList || dataList.length === 0) {
        return 4;
    }

    // 通過日期匹配,找到對應的標註對象
    const dataItem = dataList.find(item => item.date === date);

    return dataItem ? 15 : 4;
},

getSymbolUrl (id) {
    // 這裏需要額外先做一層準備工作:將圖標按 id 對應圖標進行命名,然後傳到自家的cdn上
    // 命名可以像這樣:symbol-icon-1.jpg、symbol-icon-2.jpg 等等
    // 這裏拿到標註的id,拼上鍊接返回即可
    // 形如:image://http://xxx.xxx.com/symbol-icon-1.jpg
    // 遇到多個 id 的情況,可以多加一個複合圖標來處理,id 可以定爲 0
}

最後的一個問題,如何改寫 tooltip 的樣式問題,以做好兼容呢?

之前說到,tooltip 的佈局分爲三塊:日期、標註信息、具體數值。那麼我們就以此,來重新繪製 tooltip。

tooltip 支持 formatter 回調函數,它的返回值類型是 Sting。

// 回調函數格式
(params: Object|Array, ticket: string, callback: (ticket: string, html: string)) => string

日期的信息,可以通過 params[0].axisValue 來獲取。

獲取標註信息的方法,與上述獲取圖標的思路類似,只是這裏需要展示具體的標註類型和名稱。

具體的數值,可以通過 params 中的 marker,seriesName,value 等屬性獲得。具體實現如下:

setChartExtend () {
    this.chartExtend = {
        series: (v) => {
            ...
        },
        tooltip: {
            formatter: (params) => {
                return getTooltipResult(params, dataList);
            }
        }
    };
},

getTooltipResult (params, dataList) {
    const dateResult = params[0].axisValue;
    // 獲取原版 tooltip 的渲染結構
    const originalResultObj = getOriginalTooltipResult(params);

    if (!dataList || dataList.length === 0) {
        return dateResult + originalResultObj.strResult;
    }

    const dataItem = dataList.find(item => item.date === date);

    if (dataItem) {
        return dateResult +  getSymbolResult(dataItem, originalResultObj.strResult);
    }

    return dateResult + originalResultObj.strResult;
},

getOriginalTooltipResult (params) {
    let result = '';

    params.forEach((param, idx) => {
        // value 會因爲 seriesType 的不同,類型也會有不同
        let value = Object.prototype.toString.call(param.value) === '[object Array]' ? param.value[1] : param.value;

        const str = `${param.marker}${param.seriesName}: ${ value }<br>`;
        result += str;
    });

    return {
        strResult: result
    };
},

getSymbolResult (dataItem, originalResult) {
    // 將 dataItem 的 id 轉爲數組的形式,循環渲染輸出圖標與名稱的組合
    const dataIds = dataItem.id.split(',');
    const dataNames = dataItem.name.split(',');

    dataIds.forEach ((id, idx) => {
        // 通過 id 換取 圖標的鏈接
        const iconUrl = ...;

        // 仿照 param.marker 的 style 寫法,渲染圖標樣式
        const str = `<img src="${iconUrl}" width="11" height="11" style="display: inline-block; margin-right: 4px; margin-left: -1px;">${dataNames[idx]}<br>`;

        result += str;
    });

    return result + originalResult;
}

或許有同學會問 getOriginalTooltipResult 方法返回的值,裏面只有一個 strResult,爲何要設計爲對象?

其實是爲了方便擴展。例如,可以在日期的後面跟上下方具體數據的值的總計。那就需要通過 getOriginalTooltipResult 方法裏的 params 循環,計算出 total,在配合上樣式,生成一個 strTotal。

getOriginalTooltipResult (params) {
    ...

    return {
        strTotal: strTotal,
        strResult: result
    };
}

此外,在實際的業務中,還可能會出現某些線的數值是百分比。那就需要再對 getOriginalTooltipResult 方法做擴展,比如傳入一個 options 對象:

getOriginalTooltipResult = (params, options = { isLinePercent: false, isShowTotal: false }) {
    ...
}

至此,折線圖標記的渲染,就能完美地呈現了。

柱狀圖標記 —— markPoint

很尷尬的一點是:柱狀圖沒有 symbol 屬性。也就意味着上面的折線圖的那一套,在柱狀圖中玩不轉了。

沒辦法,只能從頭查文檔,繼續找資料。經過一番“摸爬滾打”,終於發現了 markPoint 這個屬性。在 markPoint 這個對象裏面,可以設置 symbol,這樣的話,那麼之前搞出來的那一套就沒有白費呀?!?

需要注意的是,markPoint 的默認 symbol 爲 'pin',就是一個氣泡的圖標。另外,想要讓 markPoint 的標記出現,就必須設置它的 data 屬性。我們需要設置 data 裏的這樣幾個屬性:

data: [
    {
        symbol: '...', // 設置標記的圖標鏈接
        symbolSize: 15, // 設置標記的大小
        coord: [index, 0], // x 軸的第 index 個上,打標記
        symbolOffset: [0, 0] // 將標記定位在 x 軸上
    },
    ...
]

具體的實現代碼如下:

<ve-histogram :data="chartData" :extend="chartExtend"></ve-histogram>
...

// mock 包含標註的數據結構
dataList: [
    {
        id: '1, 1, 3, 2',
        date: '2019-10-10',
        name: 'test-name1, test-name2, test-name3, test-name4'
    },
    ...
    {
        id: '1',
        date: '2019-10-17',
        name: 'test-name1'
    }
],
...

setChartExtend () {
    this.chartExtend = {
        series: (v) => {
            Array.from(v).forEach((e, idx) => {
                e.markPoint = {
                    data: getMarkPointData(this.chartData.rows, dataList)
                };
            });
        },
        ...
    };
},

getMarkPointData (rows, dataList) {
    const results = [];

    rows.forEach((row, index) => {
        // 通過日期匹配,找到對應的標註對象
        const dataItem = dataList.find(item => item.date === row.date);
        if (dataItem) {
            results.push({
                symbol: getSymbolUrl(dataItem.id),
                symbolSize: 15,
                coord: [index, 0],
                symbolOffset: [0, 0]
            });
        }
    });

    return results;
},

getSymbolUrl (id) {
    // 這裏需要額外先做一層準備工作:將圖標按 id 對應圖標進行命名,然後傳到自家的cdn上
    // 命名可以像這樣:symbol-icon-1.jpg、symbol-icon-2.jpg 等等
    // 這裏拿到標註的id,拼上鍊接返回即可
    // 形如:image://http://xxx.xxx.com/symbol-icon-1.jpg
    // 遇到多個 id 的情況,可以多加一個複合圖標來處理,id 可以定爲 0
}

因爲 markPoint 在設置 data 時,取不到日期的數據,所以就需要用到 chartData 中的 rows 了。

在 rows 的循環中,如果匹配到當天需要打標記,則往結果數組中存入剛纔預設的數據結構,最終返回給 markPoint 的 data,渲染展現。效果如下:

tooltip 的實現方法,不受圖表的類型影響,是可以通用的,故此處不再贅述。

另外,有的時候,會遇到需要處理柱狀圖是否堆疊的效果。這會影響 symbolOffset 的定位,爲了美觀,可以這樣處理:對於非堆疊的項,將之往右偏移 50%,將標記居中展示,即 symbolOffset : ['50%', 0]

折線圖 + 柱狀圖

最後來個 組合拳,折線圖與柱狀圖的複合型圖表結構,像下面這樣:

從代碼實現上,我們當然可以給每一根符合條件的柱子,和折線的拐點,都打上標記。但在界面設計上,爲了美觀,我們選擇只給折線的拐點打上標記,並且忽略了虛線的拐點。

這樣的設計初衷是:標記,只是爲了給人提個醒,是爲了告訴查閱者,這一天因爲發生了某些特殊事件,而導致數據發生了較爲明顯的變化。所以,由此得到的結論是:每天只要出現一個標記就夠了。

具體的實現,其實很簡單,只需要在渲染時判斷 series 的 type 即可:

setChartExtend () {
    this.chartExtend = {
        series: (v) => {
            Array.from(v).forEach((e, idx) => {
                if (e.type === 'bar') {
                    // 設置柱狀圖 markPoint 的方法
                    ...
                }

                if (e.type === 'line') {
                    // 設置折線圖 symbol、symbolSize 的方法
                    ...
                }
            });
        },
        ...
    };
}

總結

Echarts 可以實現的效果有很多,本篇涉及其中 “標記” 的渲染。在折線圖的拐點處,用 symbol 做了匹配化的處理。在柱狀圖中,因爲沒有直接的 symbol,轉而使用 markPoint 來實現,採用將標記定位在某個維度上的做法。效果都聽不錯的。

不過,在後續的使用中,發現了另一個尷尬的情況:在折線圖中,當點擊圖例中的某一項,使其數據隱藏,然後再次點擊重新渲染後,發現 symbol 的自定義圖標不顯示了

我查了很久,還是沒找到有用的信息。懷疑是個渲染的 bug,所以給 Echarts 提了 issue,希望能得到解決吧。也歡迎大家在留言區中,共同探討相關的問題,感謝!

PS:歡迎關注我的公衆號 “超哥前端小棧”,交流更多的想法與技術。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章