Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9192

node.jsでchart.jsを使う

$
0
0
expressを使ったWebAPIでグラフの画像を生成したかったので、Javascsriptで有名なchart.jsをNode.jsで動かすnpmモジュール「chartjs-node-canvas」を使いこなします。 以下の点を意識しています。 ・Node.jsでchart.jsを動かす場合も日本語がちゃんと表示されるようにする ・chart.jsの凡例やデータの指定方法がグラフの種類によってまちまちなのを統一する ・背景に色を塗ったり、前景全体に文字列を重ね合わせられるようにする 上記をクラス化して、使いやすい形にしていきます。 ソースコードもろもろは、GitHubに上げておきました。 poruruba/MakeChart_test ※node-fetchは、v2系を使った方がよいかも。 > npm install node-fetch@2.6.5 <参考URL> ・https://www.chartjs.org/docs/latest/ ・https://github.com/SeanSobey/ChartjsNodeCanvas/blob/master/API.new.md ・https://github.com/SeanSobey/ChartjsNodeCanvas ・https://github.com/shrhdk/text-to-svg chartjs-node-canvasのインストール 基本的には以下で説明の通りに実施します。 SeanSobey/ChartjsNodeCanvas > npm install chartjs-node-canvas chart.js@2.9.4 chart.jsは、v2系の情報が世の中に多いので、2系の最新バージョンにしています。 あとは、説明の通りに進めればよいのですが、このままでは日本が表示されません。 タイトルや凡例、ラベルなどで使われます。そこで、日本語フォントを登録します。 IPAフォントを使わせていただきました。 IPAexゴシック:ipaexg00401.zipをダウンロードし、適当な場所に解凍します。 解凍する出てく308Bipaexg.ttfというファイルを使います。 api\controllers\makechart-api\genchart.js const FONT_PATH = '【フォントを配置したフォルダ】' + '/ipaexg.ttf'; const FONT_NAME = "IPAEXG"; const FONT_COLOR = "#333333aa"; const { ChartJSNodeCanvas } = require('chartjs-node-canvas'); const chartJSNodeCanvas = new ChartJSNodeCanvas({ width: width, height: height, chartCallback: (ChartJS) => { ChartJS.defaults.global.defaultFontFamily = FONT_NAME; ChartJS.defaults.global.defaultFontColor = FONT_COLOR; } }); chartJSNodeCanvas.registerFont(FONT_PATH, { family: FONT_NAME }); widthとheightには、生成したいグラフ画像のサイズを指定します。 FONT_COLORは、文字の色および透過率を指定しています。お好みで変更してください。 グラフのテンプレートを作る あまり深いことを気にせずに作れるように、6種類ほどのグラフをテンプレート化しておきます。 そうすることで、たくさんあるchart.jsのオプション群を毎回思い出す必要がなくなるのと、最初に述べた通り、凡例とデータの並びに統一性を持たせたいためです。 以下6種類のテンプレートを作ります。 名称 種類 doughnut タコメータのようなもの gauge 横棒ゲージのようなもの line 折れ線グラフ pie 円グラフ stackbar 積み上げ棒グラフ bar 棒グラフ 以降は、テンプレート化後のパラメータ指定方法を示しますが、chart.jsの指定への変換内容は、今回作成したクラスのソースファイルをご参照ください。 タコメータのようなもの 必須 名前1 名前2 内容 備考 〇 value 値 legend 凡例 指定がない場合表示されない title タイトル 指定がない場合表示されない 〇 range max 最大値 100%となる値 例 { "value": 10, "legend": "凡例1", "title": "チャートタイトル", "range": { "max": 50 } } 横棒ゲージのようなもの 必須 名前1 名前2 内容 備考 〇 value 値 legend 凡例 指定がない場合表示されない title タイトル 指定がない場合表示されない 〇 range min 最小値 0%となる値 〇 max 最大値 100%となる値 例 { "value": 10, "legend": "凡例1", "title": "チャートタイトル", "range": { “min”: 0, "max": 50 } } 折れ線グラフ 必須 名前1 名前2 内容 備考 〇 datum 値の2次元配列 〇 labels ラベルの配列 legends 凡例の配列 指定がない場合表示されない title タイトル 指定がない場合表示されない range min 最小値 Y軸の最小値 max 最大値 Y軸の最大値 例 { "datum": [ [880, 740, 900, 520, 930], [380, 440, 500, 220, 630] ], "labels": ["1月", "2月", "3月", "4月", "5月"], "legends": ["プリンター販売台数", "パソコン販売台数"], "range": { "min": 0, "max": 1000 }, "title": "チャートタイトル" } 円グラフ 必須 名前1 名前2 内容 備考 〇 datum 値の配列 legends 凡例の配列 指定がない場合表示されない title タイトル 指定がない場合表示されない 例 { "datum": [880, 740, 100], "legends": ["OK", "NG", "UNKNOWN"], "title": "チャートタイトル" } 積み上げ棒グラフ 必須 名前1 名前2 内容 備考 〇 datum 値の2次元配列 〇 labels ラベルの配列 〇 legends 凡例の配列 title タイトル 指定がない場合表示されない 例 { "datum": [[1, 4], [5, 0], [3, 2], [4, 1]], "labels": ["pc1", "pc2", "pc3", "pc4"], "legends": ["OK", "NG"], "title": "チャートタイトル" } 棒グラフ 必須 名前1 名前2 内容 備考 〇 datum 値の配列 〇 labels ラベルの配列 title タイトル 指定がない場合表示されない range min 最小値 Y軸の最小値 max 最大値 Y軸の最大値 例 { "datum": [1, 2, 3, 5, 2], "labels": ["1月", "2月", "3月", "4月", "5月"], "title": "チャートタイトル", "range": { "min": 0 } } グラフ画像を生成する widthとheightに画像サイズを指定します。 typeは、先ほどの6種類の名称(doughnut, gauge, line, pie, stackbar, bar)です。 paramsはグラフの種類ごとに指定する値です。 mimetypeは画像のフォーマットを指定します。例えば、”image/png”。 api\controllers\makechart-api\genchart.js async makeChart(width, height, type, params, mimetype){ const chartJSNodeCanvas = new ChartJSNodeCanvas({ width: width, height: height, chartCallback: (ChartJS) => { ChartJS.defaults.global.defaultFontFamily = FONT_NAME; ChartJS.defaults.global.defaultFontColor = FONT_COLOR; } }); chartJSNodeCanvas.registerFont(FONT_PATH, { family: FONT_NAME }); var configuration; switch(type){ case 'doughnut': { configuration = make_chart_doughnut(params.value, params.legend, params.title, params.range); break; } case 'gauge': { configuration = make_chart_gauge(params.value, params.legend, params.title, params.range); break; } case 'line': { configuration = make_chart_line(params.datum, params.labels, params.legends, params.title, params.range); break; } case 'pie': { configuration = make_chart_pie(params.datum, params.legends, params.title); break; } case 'stackbar': { configuration = make_chart_stackbar(params.datum, params.labels, params.legends, params.title, params.range); break; } case 'bar': { configuration = make_chart_bar(params.datum, params.labels, params.title, params.range); break; } default:{ throw 'unknown type'; } } return chartJSNodeCanvas.renderToBuffer(configuration, mimetype); } 背景に色を塗ったり、前景全体に文字列を重ね合わせられるようにする 前景全体に文字列を重ね合わせます。 文字列の画像生成には、npmモジュールの「text-to-svg」を使わせていただきました。背景色の生成や画像の合成には、npmモジュールの「sharp」をつかわせていただきました。 > npm install text-to-svg > npm install sharp 文字列画像の生成 chartjs-node-canvasの時と同様に、日本語も表示できるように、日本語フォントを設定します。 api\controllers\makechart-api\genchart.js const TextToSVG = require("text-to-svg"); const FONT_PATH = '【フォントを配置したフォルダ】' + '/ipaexg.ttf'; const textToSVG = TextToSVG.loadSync(FONT_PATH); あとは、文字列を引数にしてgetSVGを呼び出せばよいのですが、画像サイズいっぱいに表示したいため、フォントサイズを動的に決定するようにしています。 api\controllers\makechart-api\genchart.js const CAPTION_FONT_SIZE = 72; const CAPTION_PADDING = 20; makeCaption(width, height, caption){ var fontSize = Math.min(CAPTION_FONT_SIZE, width / caption.length); var svgOptions = { x: 0, y: 0, anchor: "left top", attributes: { fill: FONT_COLOR } }; do { svgOptions.fontSize = fontSize; var metrics = textToSVG.getMetrics(caption, svgOptions); if (metrics.width <= width * (100 - CAPTION_PADDING) / 100 && metrics.height <= height * (100 - CAPTION_PADDING) / 100) break; fontSize -= 2; if( fontSize <= 0 ) throw 'unknown error'; } while (true); return textToSVG.getSVG(caption, svgOptions); } 最後に、グラフ画像に対して、背景色と前景文字列をマージします。sharpの機能を活用します。 背景色の画像は、SVGを使って生成しています。 場合によっては、背景色だけつけたい場合、前景文字列だけつけたい場合、いずれも不要な場合があると思いますので、切り替えられるようにしています。 api\controllers\makechart-api\genchart.js async generateChart(width, height, type, chart_params, caption, bgcolor){ var chart_image = await this.makeChart(width, height, type, chart_params, "image/png"); var caption_image; if( caption ) caption_image = Buffer.from(this.makeCaption(width, height, caption)); var image_buffer; if( bgcolor ){ var background_svg = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg"> <path d="M0 0 L ${width} 0 L ${width} ${height} L 0 ${height}" style="fill:${bgcolor}; stroke-width:0" /> </svg>`; var comps = []; comps.push({ input: chart_image }); if( caption_image ) comps.push({ input: caption_image, gravity: "center" }); image_buffer = await sharp(Buffer.from(background_svg)) .composite(comps) .png() .toBuffer(); }else if( caption_image ){ image_buffer = await sharp(chart_image) .composite([{ input: caption_image, gravity: 'center' }]) .png() .toBuffer(); }else{ image_buffer = chart_image; } return image_buffer; } クラスの使い方 結局は、クラス化で、こんな感じで使えるようにしました。 api\controllers\makechart-api\index.js var image_buffer = await genchart.generateChart(body.width, body.height, body.type, body.chart_params, body.caption, body.bgcolor); WebAPI使用例 HTTP GETまたはHTTP Post(Json)で、グラフ画像(image/png)が返ってくるWebAPIを作成してみました。 以下の3種類を用意しました。GETの呼び出して呼び出せるようにしています。 Endpoint名:/makechart-inspect netdataのグラフ画像 Ping応答チェッカー マシンのメモリ使用状況 また、POST(Json)呼び出して、データやタイトルやらすべて呼び出し側で指定して画像取得することもできるようにしています。 Endpoint名:/makechart-generate まとめるとこんな感じです。 api\controllers\makechart-api\index.js 'use strict'; const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/'; const BinResponse = require(HELPER_BASE + 'binresponse'); const { URLSearchParams } = require('url'); const fetch = require('node-fetch'); const genchart = require('./genchart'); const ping = require('ping'); const osu = require('node-os-utils'); const PING_TIMEOUT = 3; const netdata_base_url = "【netdataのURL】"; exports.handler = async (event, context, callback) => { if( event.path == '/makechart-inspect' ){ console.log(event.queryStringParameters); if (event.queryStringParameters.type == 'netdata' ){ const width = Number(event.queryStringParameters.width); const height = Number(event.queryStringParameters.height); const chart = event.queryStringParameters.chart || 'system.cpu'; var qs = { chart: chart, points: 20, format: 'json', after: -600, group: 'max', options: 'jsonwrap' }; var json = await do_get(netdata_base_url + '/api/v1/data', qs); console.log(json); var labels = []; for( var i = 0 ; i < json.points ; i++ ){ // labels.push(String((qs.after / qs.points) * (json.points - i - 1)) + 's'); var t = new Date(json.result.data[json.points - i - 1][0] * 1000); labels.push(zero2d(t.getHours()) + ':' + zero2d(t.getMinutes()) + ':' + zero2d(t.getSeconds())); } var datum = []; for (var j = 1; j < json.result.labels.length ; j++ ){ var array = []; for (var i = 0; i < json.points; i++) { array.push(json.result.data[json.points - i - 1][j]); } datum.push(array); } var legends = []; for (var i = 1; i < json.result.labels.length ; i++ ) legends.push(json.result.labels[i]); var image_buffer = await genchart.generateChart(width, height, "line", { datum: datum, labels: labels, legends: legends, title: 'netdata: ' + chart, range: (event.queryStringParameters.max) ? { max: Number(event.queryStringParameters.max) } : undefined }); return new BinResponse("image/png", image_buffer); }else if( event.queryStringParameters.type == 'ping' ){ const width = Number(event.queryStringParameters.width); const height = Number(event.queryStringParameters.height); const hosts = event.queryStringParameters.hosts.split(','); const trycount = event.queryStringParameters.trycount ? Number(event.queryStringParameters.trycount) : 3; const promises = hosts.map( async item => { var result = { success: 0, error: 0 }; for (var i = 0; i < trycount ; i++ ){ try{ var res = await ping.promise.probe(item, { timeout: PING_TIMEOUT, }); if( res.alive ) result.success++; else result.error++; }catch(error){ console.log(error); result.error++; } } return result; }); var result = await Promise.all(promises); var datum = result.map(item =>{ return [ item.success, item.error ]; }); var image_buffer = await genchart.generateChart(width, height, "stackbar", { datum: datum, labels: hosts, legends: ["OK", "NG"], title: 'Pingライフチェック', }); return new BinResponse("image/png", image_buffer); }else if (event.queryStringParameters.type == 'memory' ){ const width = Number(event.queryStringParameters.width); const height = Number(event.queryStringParameters.height); var info = await osu.mem.used(); var used = info.usedMemMb / info.totalMemMb * 100; var image_buffer = await genchart.generateChart(width, height, "gauge", { value: used, title: '使用メモリ(%)', range: { min: 0, max: 100 } }, used.toFixed(1) + '%'); return new BinResponse("image/png", image_buffer); } } else if (event.path == '/makechart-generate') { console.log(event.body); var body = JSON.parse(event.body); var image_buffer = await genchart.generateChart(body.width, body.height, body.type, body.chart_params, body.caption, body.bgcolor); return new BinResponse("image/png", image_buffer); } }; function zero2d(val) { return ('00' + String(val)).slice(-2); } function do_get(url, qs) { var params = new URLSearchParams(qs); return fetch(url + `?` + params.toString(), { method: 'GET', }) .then((response) => { if (!response.ok) throw 'status is not 200'; return response.json(); }); } 終わりに これで、GET呼び出しで、グラフ画像が取得できるようになったので、LCD付のESP32で、稼働監視を可視化できそうです。 以上

Viewing all articles
Browse latest Browse all 9192

Trending Articles