1. 程式人生 > >微信小程式不支援圖表工具,通過例項帶你瞭解繪製方案

微信小程式不支援圖表工具,通過例項帶你瞭解繪製方案

作者:musiq1989,前端開發工程師,專注於前端技術研究和內容分享,Github地址:https://github.com/xiaolin3303
責編:陳秋歌,關注微信開發等領域,尋求報道或者投稿請發郵件至chenqg#csdn.net。
歡迎加入“CSDN微信開發”群,與作者及更多專家、技術同行進行熱點、難點技術交流。請加群主微信「Rachel_qg」,申請入群,務必註明「姓名+公司+職位」。

由於微信小程式本身框架的限制,很難整合目前已有的圖表工具,顯示圖表目前有兩種方案:

  1. 伺服器端渲染圖表,輸出圖片,微信小程式中直接顯示渲染好的圖片,比如Highcharts提供了服務端渲染的能力hightcharts server render,這種方式需要後臺有一套渲染服務,並且有一定的網路開銷。
  2. 微信小程式API中提供了canvas的支援,利用canvas自行繪製圖表。

下面我們來看下怎麼在微信小程式中繪製圖表。

API

在模板檔案中使用<canvas></canvas>宣告一個canvas元件。

使用wx.createContext獲取繪圖上下文 context。

呼叫wx.drawCanvas進行繪製。

wx.drawCanvas({
    canvasId: 'firstCanvas',
    actions: context.getActions() // 獲取繪圖動作陣列
});

開始圖表的繪製

繪製折線圖

// 獲取繪圖上下文 context
var context = wx.createContext();
// 設定描邊顏色
context.setStrokeStyle("#7cb5ec"); // 設定線寬 context.setLineWidth(4); context.moveTo(50, 70); context.lineTo(150, 150); context.lineTo(250, 30); context.lineTo(350, 120); context.lineTo(450, 150); context.lineTo(550, 95); // 對當前路徑進行描邊 context.stroke(); wx.drawCanvas({ canvasId: 'testCanvas', actions: context.getActions() });

說明:moveTo方法不記錄到路徑中

效果圖:
圖片描述

好像沒有想象中難,看上去效果還不錯。

繪製每個資料點的標識圖案

...

context.beginPath();
// 設定描邊顏色
context.setStrokeStyle("#ffffff");
// 設定填充顏色
context.setFillStyle("#7cb5ec");
context.moveTo(50 + 7, 70);
// 繪製圓形區域
context.arc(50, 70, 8, 0, 2 * Math.PI, false);

context.moveTo(150 + 7, 150);
context.arc(150, 150, 8, 0, 2 * Math.PI, false);

...

context.closePath();
// 填充路徑
context.fill();
context.stroke();

效果圖:
圖片描述

說明:避免之前繪製的折線路徑影響到標識圖案的路徑,這裡包裹在了beginPathclosePath中。

繪製橫座標

規定我們的引數格式是這樣的。

opts = {
    width: 640,    // 畫布區域寬度
    height: 400,   // 畫布區域高度
    categories: ['2016-08', '2016-09', '2016-10', '2016-11', '2016-12', '2017']
}

我們根據引數中的categories來繪製橫座標。

稍微整理下思路:

  1. 根據categories數均分畫布寬度;
  2. 計算出橫座標中每個分類的起始點;
  3. 繪製文案(這兒會多一些程式碼,後面會具體提到)。
var eachSpacing = Math.floor(opts.width / opts.categories.length);
var points = [];
// 起始點x座標
var startX = 0;
// 起始點y座標
var startY = opts.height - 30;
// 終點x座標
var endX = opts.width;
// 終點y座標
var endY = opts.height;

// 計算每個分類的起始點x座標
opts.categories.forEach(function(item, index) {
    points.push(startX + index * eachSpacing);
});
points.push(endX);

// 繪製橫座標
context.beginPath();
context.setStrokeStyle("#cccccc");
context.setLineWidth(1);

// 繪製座標軸橫線
context.moveTo(startX, startY);
context.lineTo(endX, startY);
// 繪製座標軸各區塊豎線
points.forEach(function(item, index) {
    context.moveTo(item, startY);
    context.lineTo(item, endY);
});
context.closePath();
context.stroke();

context.beginPath();
// 設定字型大小
context.setFontSize(20);
// 設定字型填充顏色
context.setFillStyle('#666666');
opts.categories.forEach(function(item, index) {
    context.fillText(item, points[index], startY + 28);
});
context.closePath();
context.stroke();

效果圖:
圖片描述

效果不錯,除了文字沒有居中……

檢視微信小程式官方提供的文件並沒有提供HTML5 Canvas中的mesureText(獲取文案寬度)方法,下面我們自己簡單的實現,並不是絕對精確,但是誤差基本可以忽略。

function mesureText (text) {
    var text = text.split('');
    var width = 0;
    text.forEach(function(item) {
        if (/[a-zA-Z]/.test(item)) {
            width += 14;
        } else if (/[0-9]/.test(item)) {
            width += 11;
        } else if (/\./.test(item)) {
            width += 5.4;
        } else if (/-/.test(item)) {
            width += 6.5;
        } else if (/[\u4e00-\u9fa5]/.test(item)) {
            width += 20;
        }
    });
    return width;
}

這裡分別處理了字母數字.-漢字這幾個常用字元。

上面的程式碼稍微修改下:

opts.categories.forEach(function(item, index) {
    var offset = eachSpacing / 2 - mesureText(item) / 2;
    context.fillText(item, points[index] + offset, startY + 28);
});

圖片描述

大功告成!

如何在折線上繪製出每個資料點的數值文案大家可以動手自己實現下。

確定縱座標的範圍並繪製

為了避免縱座標的刻度出現小數的情況,我們把縱座標分為5個區塊,我們取最小單位刻度為例如10(能夠被5整除),當然真實情況會比這複雜,待會兒我們再討論。

所以我們的處理輸入輸出應該是下面的結果。

(5, 34.1)  => (10, 40)
(10, 34)   => (10, 40)
(-5.1, 40) => (-10, 40)
// 確定Y軸取值範圍
function findRange (num, type, limit) {
    limit = limit || 10;

    // upper向上查詢,lower向下查詢
    type = type ? type : 'upper';

    // 進行取整操作,避免while時進入死迴圈
    if (type === 'upper') {
        num = Math.ceil(num);
    } else {
        num = Math.floor(num);
    }
    while (num % limit !== 0) {
        if (type === 'upper') {
            num++;
        } else {
            num--;
        }
    }

    return num;
}

好了,初步的確定範圍已經完成了,但是細想一下這個範圍還是不是很理想,比如使用者傳入的資料都是小數級別的,比如 (0.2, 0.8),我們輸出的範圍是(0, 5)這個範圍偏大,圖表展現的效果則會是上面有大部分的留白,同樣使用者輸入的資料很大,比如(10000, 18000),我們得到的範圍是(10000, 18010),這個範圍則沒什麼意義,所以我們需要根據傳入的資料的範圍來分別確定我們的最小單位刻度。

規定我們的引數格式是這樣的:

opts = {
    ...
    series: [{
            ...
            data: [15, 20, 45, 37, 4, 80]
        }, {
            ...
            data: [70, 40, 65, 100, 34, 18]
        }
    ]
}

讓我們繼續進行優化。

// 合併資料,將series中的每項data整合到一個數組當中
function dataCombine(series) {
    return series.reduce(function(a, b) {
        return (a.data ? a.data : a).concat(b.data);
    }, []);
}

// 根據資料範圍確定最小單位刻度
function getLimit (maxData, minData)
    var limit = 0;
    var range = maxData - minData;
    if (range >= 10000) {
        limit = 1000;
    } else if (range >= 1000) {
        limit = 100;
    } else if (range >= 100) {
        limit = 10;
    } else if (range >= 10) {
        limit = 5;
    } else if (range >= 1) {
        limit = 1;
    } else if (range >= 0.1) {
        limit = 0.1;
    } else {
        limit = 0.01;
    }
}

var dataList = dataCombine(opts.series);
// 獲取傳入資料的最小值
var minData = Math.min.apply(this, dataList);
// 獲取傳入資料的最大值
var maxData = Math.max.apply(this, dataList);

var limit = getLimit(maxData, minData);

var minRange = findRange(minData, 'lower', limit);
var maxRange = findRange(maxData, 'upper', limit);

現在我們動態的確定除了合適的最小刻度範圍,接下來我們接著優化一下上面的findRange方法,主要是增加對小數的支援。

function findRange (num, type, limit) {
    limit = limit || 10;
    type = type ? type : 'upper';
    var multiple = 1;
    while (limit < 1) {
        limit *= 10;
        multiple *= 10;
    }
    if (type === 'upper') {
        num = Math.ceil(num * multiple);
    } else {
        num = Math.floor(num * multiple);
    }
    while (num % limit !== 0) {
        if (type === 'upper') {
            num++;
        } else {
            num--;
        }
    }

    return num / multiple;
}

現在我們已經確定好了Y軸的取值範圍,關於如何畫出Y軸可以參看上文中X軸的繪製方法,此處不再累贅。

Y軸效果圖:

opts = {
    ...
    series: [{
            ...
            data: [15, 20, 45, 37, 4, 80]
        }, {
            ...
            data: [70, 40, 65, 100, 34, 18]
        }
    ]
}

圖片描述

opts = {
    ...
    series: [{
            ...
            data: [0.15, 0.2, 0.45, 0.37, 0.4, 0.8]
        }, {
            ...
            data: [0.30, 0.37, 0.65, 0.78, 0.69, 0.94]
        }
    ]
}

圖片描述

效果還不錯,我們接著往下。

根據真實資料繪製折線

問題的關鍵在於確定每個資料點的(x, y)座標,x座標比較好確定,我們根據畫布的寬度以及opts.categories即可確定。

規定我們的配置為:

config = {
    xAxisHeight: 30, // X軸高度
    yAxisWdith: 30   // Y軸寬度
}
var data = [15, 20, 45, 37, 4, 80];
var xPoints = [];
var validWidth = opts.width - config.yAxisWidth;
var eachSpace = validWidth / opts.categories.length;
var start = config.yAxisWidth;

data.forEach(function (item, index) {
    xPoints.push(start + (index + 0.5) * eachSpace);
});

y座標稍微會複雜一點,需要根據Y軸的範圍已經本身的數值進行計算得出。

圖片描述

所以我們計算出的y應該為:

y = validHeight * (data - min) / (max - min);
// 由於canvas畫布是左上角為原點座標,故我們變化一下
// 得到最終的y繪製點
y = valideHeight - y;

程式碼如下:

var data = [15, 20, 45, 37, 4, 80];
var yPoints = [];
var validHeight = opts.height - config.xAxisHeight;
data.forEach(function(item) {
    var y = validHeight * (item - min) / (max - min);
    y = validHeight - y;

    yPoints.push(y);
}

現在我們已經確定了資料點在畫布上的繪製座標,關於如何繪製折現請檢視 part1 中相關內容,此處不再累贅。

最終效果圖如下:

圖片描述

餅圖繪製

先看一下API。

圖片描述

下面開始(使用ES6語法編寫,後面我們可以使用rollup編譯成ES5的語法)

假設我們有這樣的資料

const series = [
    {data: 15, color: '#7cb5ec'},
    {data: 35, color: '#f7a35c'},
    {data: 78, color: '#434348'},
    {data: 63, color: '#90ed7d'}
];

計算出各項所佔的比例和開始的弧度。

calPieData.js

export function calPieAngle (series) {
    // 計算資料總和
    let count = 0;
    series.forEach((item) => {
        count += item.data;
    });

    // 計算出開始的弧度和所佔比例
    let startAngle = 0;
    return series.map((item) => {
        item.proportion = item.data / count;
        item.startAngle = startAngle;
        startAngle += 2 * Math.PI * item.proportion;
        return item;
    });
}

資料已經計算出來了,下面讓我開始繪製吧。

drawPieChart.js

import { calPieAngle } from 'calPieData'

export default function drawPieChart (series) {
    ...

    let pieSeries = calPieAngle(series);
    pieSeries.forEach((item) => {
        context.beginPath();
        // 設定填充顏色
        context.setFillStyle(item.color);
        // 移動到原點
        context.moveTo(100, 100);    
        // 繪製弧度
        context.arc(100, 100, 80, item.startAngle, item.startAngle + 2 * Math.PI * item.proportion);
        context.closePath();
        context.fill();
    });

    ...

}

呼叫drawPieChart(series)就可以得到下面的結果:

圖片描述

很簡單是不是,下面我們給各區塊加上一個白色的分割線。

因為arc實際上是繪製了一條路徑,所以我們簡單的stroke描邊一下就可以了。

...

context.setLineWidth(2);
context.setStrokeStyle('#ffffff');
pieSeries.forEach((item) => {
    context.beginPath();
    context.setFillStyle(item.color);
    context.moveTo(100, 100);    
    context.arc(100, 100, 80, item.startAngle, item.startAngle + 2 * Math.PI * item.proportion);
    context.closePath();
    context.fill();
    context.stroke();
})

...

圖片描述

新增動畫效果

首先讓我們建立一個動畫工具,這個動畫工具能夠傳入一些自定義的引數,比如動畫時間,能夠有動畫每一步的回撥以及動畫結束的回撥。

animation.js

export default function Animation (opts) {
    // 處理使用者傳入的動畫時間,預設為1000ms
    // 因為使用者有可能傳入duration為0,所以不能用opts.duration = opts.duration || 1000 來做預設值處理
    // 否則使用者傳入0也會處理成預設值1000
    opts.duration = typeof opts.duration === 'undefined' ? 1000 : opts.duration;

    let startTimeStamp = null;

    function step (timestamp) {
        if (startTimeStamp === null) {
            startTimeStamp = timestamp;
        } 
        if (timestamp - startTimeStamp < opts.duration) {
            // 計算出動畫的進度
            let process = (timestamp - startTimeStamp) / opts.duration;
            // 觸發動畫每一步的回撥,傳入進度process
            opts.onProcess && opts.onProcess(process);
            // 動畫進行中,執行下一次動畫
            requestAnimationFrame(step);
        } else {
            // 動畫結束
            opts.onProcess && opts.onProcess(1);
            // 觸發動畫結束回撥
            opts.onAnimationFinish && opts.onAnimationFinish();
        }
    }

    requestAnimationFrame(step);
}

動畫使用了requestAnimationFrame,並且已經滿足了我們上面定義的需求
在實戰中,此處的動畫都是線性的,一般我們還會加入緩動選項,比如緩入,緩出,還有一點,在微信小程式真機中IOS裝置是不支援requestAnimationFrame的,所以要做降級處理,使用setTimeout檢視完整的程式碼

下面我們呼叫animation來完成動畫效果。

app.js

import Animation from 'animation'
import drawPieChart from 'drawPieChart'

Animation({
    duration: 1000,
    onProcess: (process) => {
        drawPieDataChart(series, process);
    }
});

修改一下drawPieDataChart function,能夠接受process引數。

...

export default function drawPieChart (series, process = 1) {
    ...
    // 將process傳入給calPieAngle,計算出對應進度下的圖表角度資料
    let pieSeries = calPieAngle(series, process);

...

同樣,修改一下calPieAngle function,能夠接受process引數。

export function calPieAngle (series, process = 1) {
    ...

    // 計算出開始的弧度和所佔比例
    let startAngle = 0;
    return series.map((item) => {
        // 計算出當前動畫進度的比例
        item.proportion = item.data / count * process;
        item.startAngle = startAngle;
        startAngle += 2 * Math.PI * item.proportion;
        return item;
    });
}

好了,現在我們的動畫就可以動起來了,類似這樣。

圖片描述

使用rollup構建專案

Rollup is a next-generation JavaScript module bundler. Author your app or library using ES2015 modules, then efficiently bundle them up into a single file for use in browsers and Node.js.

也就是說rollup是一個前端構建工具,能夠將我們的整個專案合併輸出成一個最終的編譯結果,上面我們編寫程式碼的時候都是按照不同的功能放到不同的檔案中,這樣有利於後期的可持續性開發和維護,rollup正好能幫助我們構建出最後的編譯結果。

先安裝rollup

npm install -g rollup

新增對ES6的支援。

npm install --save-dev rollup-plugin-babel
npm install --save-dev babel-preset-es2015-rollup

建立.babelrc檔案在專案根目錄,告訴babel轉義時使用哪個presets

{
  "presets": ["es2015-rollup"],
}

好了剩下最後一步,定義我們的rollup.config.js配置檔案:

import babel from 'rollup-plugin-babel';

export default {
  // 入口檔案
  entry: 'app.js',
  // 輸出格式,這裡使用commonJS
  format: 'cjs',
  // 輸出檔案
  dest: 'dist/charts.js',
  // 使用babel進行ES6轉ES5
  plugins: [
      babel({
          exclude: 'node_modules/**',
      })
  ]
};

rollup會從入口檔案開始,查詢我們的依賴(import),逐級往下深入,把依賴的檔案全部收集起來併合併到一起,最後輸出到我們定義的dest檔案中

執行:

rollup -c

好了,我們就得到了我們最後的專案編譯檔案charts.js

CSDN技術公開課誠邀對小程式有實踐經驗的技術專家、資深開發者,擔任微信小程式技術公開課講師,帶來有關小程式更深入、更全面的技術分享。邀請流程請見:。

也歡迎各位專家老師掃描以下二維碼,加秋歌為好友,前來諮詢,相互交流。加好友時,請註明:講師+小程式。

圖片描述