node環境下給echarts圖表加文案

開發環境:node
開發語言:javascript
開發依賴的包:javascript

圖表的寬度是1000
先把最終要呈現的效果貼出來,見下圖:html

最終效果圖


查看charts配置項及實例,給圖表增長額外的文本塊,只有一個屬性graphic【ˈgrafik】彷佛能夠實現,它指的是原生圖形元素組件,能夠由多種類型組成,如image, text, circle, sector, ring, polygon, polyline, ...,結合給的實例,看起來很不錯,so easy!立刻開幹。java

代碼以下:node

var node_echarts = require("node-echarts");
var Path = require("path");
const text = '個人電腦只有4G運行內存,採用默認的idea配置,內存在30分鐘內會飈到 >80% ,同時會發生OOM!Chrome就不敢打開!經過上面的配置能夠將內存使用下降一半以上,只有idea和chrome 的話,內存纔剛剛 40% 。下面的能夠看也能夠不看了,下面的分析是別人就好了分析,經過閱讀可見他的電腦內存的確不小(16G的macbook pro),對於咱們學生黨,默默的使用着4G內存的電腦,就很少說上面了!不過,參與討論的一位開發者給筆者發了一份他的設置,雖然是針對同個項目,該設置卻極其複雜。筆者對本身的設置並沒有不滿,但很是好奇,這些徹底不一樣的設置對比 JetBrains 提供的默認設置。';

const config = {
  legend: { bottom: 0, show: true, data: ["身份證數量", "環比增加率"] },
  xAxis: [
    {
      type: "category",
      data: [
        "201701",
        "201702",
        "201703",
        "201704",
        "201705",
        "201706",
        "201707",
        "201708",
        "201709",
        "201710",
        "201711",
        "201712",
        "201801"
      ],
      axisLabel: { interval: 0 },
      axisPointer: { type: "shadow" },
      splitLine:{show: false}
    }
  ],
  yAxis: [
    { type: "value", axisLabel: { formatter: null }  },
    { type: "value", axisLabel: { formatter: "{value}%" }}
  ],
  series: [
    {
      type: "bar",
      name: "身份證數量",
      data: [
        23620000,
        21060000,
        26420000,
        30180000,
        31430000,
        34100000,
        33740000,
        40170000,
        39910000,
        38420000,
        49300000,
        50710000,
        46550000
      ],
      yAxisIndex: 0
    },
    {
      type: "line",
      name: "環比增加率",
      data: [
        -23,
        -12.13,
        20.26,
        12.46,
        4,
        7.82,
        -1.09,
        16.02,
        -0.65,
        -3.88,
        22.05,
        2.79,
        -8.95
      ],
      yAxisIndex: 1
    }
  ],
  color: ["#4498f6", "#d9e96c"],
  graphic: [
    {
                 type: 'text',
                 left: '10%',
                 bottom: 'bottom',
                 style: {
                     fill: '#333',
                     text: text,
                     font: '14px Microsoft YaHei'
                 }
     }
 ],
};

node_echarts({
  width: 1000,
  height: 400,
  option: config, 
  path: Path.join(__dirname, "./charts.png")
});

process.exit();

效果以下圖:
圖片描述git

問題有倆:github

  1. 只有一行,後面的文字都被截斷了
  2. 覆蓋了圖例的文字,沒有間距

第2個問題很好解決,熟悉echarts配置的話,給grid的bottom,legend的bottom設個值,但這個值是動態的,和文案的行數有關係,先定一個基值,假如只有一行,grid的bottom爲90,legend的bottom爲35,每多一行,就多加15,僞代碼以下:chrome

const config = {
  legend: { bottom: 3 * 15 + 35, show: true, data: ["身份證數量", "環比增加率"] },
  grid: {bottom:  3 * 15 + 90}
...

要獲得具體的行數,實際上是解決第1個問題,因此最核心的問題是在限定的寬度裏怎麼計算一段文字的行數?
先算下一行能放多少個字符,中英文都算一個字符,以都是中文來算,這樣算下獲得結果是63,行數 = Math.ceil(字符總長度/63)。獲得行數後,還要知道每行的字符是什麼,畢竟換行是要這樣的:canvas

...
  graphic: [
    {
                 type: 'text',
                 left: '10%',
                 bottom: 'bottom',
                 style: {
                     fill: '#333',
                     text: ['text','text2', 'text3'].join('\n'),
                     font: '14px Microsoft YaHei'
                 }
     }
 ],
...

想了半天,代碼出來了數組

...
const buildText = (result=text) => {
    const graphicY = 15;
    let texts = [];
    let gridBottom = 90;
    let legendBottom = 35;
    

    const rlen = 63; // 一行最多的字符長度
    const len = result.length; // 全部字符長度
    // 大於一行
    if (len > rlen) {
      const temp = Math.ceil(len / rlen); // 總行數
      const arr = result.split(""); // 把文案分割爲每一個字符
      const newArrs = {};
      // 循環總行數
      for (let k = 0; k < temp; k++) {
        newArrs[k] = []; // 存儲每行的字符

        for (let i = rlen * k; i < rlen * (k + 1); i++) {
          if(arr[i] != undefined)
            newArrs[k].push(arr[i]);
        }
      }
      for(let j in newArrs){
        texts.push(newArrs[j].join(''));
      }
      const lastLen = texts.length-1;
      gridBottom =  lastLen * graphicY + gridBottom;
      legendBottom = lastLen * graphicY + legendBottom;
    } else {
      texts = [result];
    }
   // console.log(texts);

    return {
      graphic: [
        {
          type: "text",
          left: "10%",
          bottom: "bottom",
          style: {
            fill: "#333",
            text: texts.join("\n"),
            font: "14px Microsoft YaHei"
          }
        }
      ],
      gridBottom: gridBottom,
      legendBottom: legendBottom
    };
  }
const texts = buildText();
const config = {
...
 legend: {  bottom: texts.legendBottom, show: true, data: ["身份證數量", "環比增加率"] },
  grid:{ bottom: texts.gridBottom},
...
}
config.graphic = texts.graphic;

獲得效果以下:
圖片描述echarts

換行了,可是每行的字符不同,並且沒行間距,擠得慌。說明計算每一行的長度的方法不對,中文和英文的寬度不同,須要知道每一個字符的寬度才行,而後graphic不是支持image嘛,我就換個思路,把文字換成圖片吧,由於和canvas相關,google一把「canvas 文字換行」後,找到這篇canvas文本繪製自動換行、字間距、豎排等實現,再此安利下這位做者,他寫的博文都是滿滿的乾貨,canvas中有一個頗有用的API:measureText,用來計算字符寬度,把文字變成圖片的代碼以下:

var fs = require('fs')
var path = require('path')
var Canvas = require('canvas')
const maxWidth = 1000;
let height = 20;
const text = '個人電腦只有4G運行內存,採用默認的idea配置,內存在30分鐘內會飈到 >80% ,同時會發生OOM!Chrome就不敢打開!經過上面的配置能夠將內存使用下降一半以上,只有idea和chrome 的話,內存纔剛剛 40% 。下面的能夠看也能夠不看了,下面的分析是別人就好了分析,經過閱讀可見他的電腦內存的確不小(16G的macbook pro),對於咱們學生黨,默默的使用着4G內存的電腦,就很少說上面了!不過,參與討論的一位開發者給筆者發了一份他的設置,雖然是針對同個項目,該設置卻極其複雜。筆者對本身的設置並沒有不滿,但很是好奇,這些徹底不一樣的設置對比 JetBrains 提供的默認設置。';
const rlen = 63; // 一行最多的字符長度
const len = text.length; // 全部字符長度
const temp = Math.ceil(len / rlen); // 總行數

var canvas = Canvas.createCanvas(maxWidth, temp * height)
var ctx = canvas.getContext('2d')

ctx.globalAlpha = 1
ctx.font = '14px Microsoft Yahei'
ctx.lineWidth = 1
ctx.fillStyle = '#000'

const arrText = text.split('');
let line = '';
let y = 20;
const lineHeight = 25;
  // 核心思路是這段代碼,循環每一個字符,當字符寬度大於最大寬度就換行,且Y座標也增長
  for (var n = 0; n < arrText.length; n++) {
      var testLine = line + arrText[n];
      var metrics = ctx.measureText(testLine);
      var testWidth = metrics.width;
      if (testWidth > maxWidth && n > 0) {
        ctx.fillText(line, 0, y);
        line = arrText[n];
        y += lineHeight;
      } else {
        line = testLine;
      }
    //  console.log(line)
  }
  console.log(line)
  ctx.fillText(line, 0, y);

canvas.createPNGStream().pipe(fs.createWriteStream(path.join(__dirname, 'text.png')))

拿到的圖片以下:
圖片描述

而後修改graphic

...
  graphic:[{
                 type: 'image',
                 left: '10%',
                 bottom: 'bottom',
                 style: {
                     image: path.join(__dirname, 'text.png')
                     width: 1000,
                     height: 200  
                 }
     }],
   ...

但是圖片沒有生成,上面這段代碼沒有發生做用。查了源代碼,也沒找到緣由,只好提個issue。這又回到起點,不過計算字符長度的方法採用上述方法

const buildText = (text=text) => {
  const graphicY = 15;
  let gridBottom = 90;
  let legendBottom = 35;
  const maxWidth = 900;
  const canvas = createCanvas(maxWidth,100);
  const ctx = canvas.getContext("2d");
  ctx.font = "normal 14px SongTi";
  const arrText = text.split('');
  let line = '';
  const newArrs = [];
  for (var n = 0; n < arrText.length; n++) {
      var testLine = line + arrText[n];
      var metrics = ctx.measureText(testLine);
      var testWidth = metrics.width;
     
      if (testWidth > maxWidth & n>0) {
        line = arrText[n];
        newArrs.push(testLine.substr(0, testLine.length-1));
      } else {
        line = testLine;
      }
  }
  newArrs.push(line);
  //console.log(newArrs);
  const row = newArrs.length;
  if (row > 1) {
    gridBottom = row * graphicY + gridBottom;
    legendBottom = row * graphicY + legendBottom;
  }
 
  return {
    graphic:[
        {
          type: "text",
          left: "10%",
          bottom: "bottom",
          style: {
            fill: "#333",
            text: newArrs.join("\n"),
            font: "14px Songti"
          }
        }
      ],
    gridBottom: gridBottom,
    legendBottom: legendBottom
  };
}

獲得的效果以下:
圖片描述

行間距的問題沒解決,想到graphic既然是數組,把每行拆開做爲單獨的對象,bottom的值都不同。

const buildText = (text=text) =>{
  const graphicY = 15;
  const lineY = 20; // 設置每行文字bottom的基值
  let gridBottom = 90;
  let legendBottom = 35;
  const maxWidth = 900;
  const canvas = createCanvas(maxWidth,100);
  const ctx = canvas.getContext("2d");
  ctx.font = "normal 14px SongTi";
  const arrText = text.split('');
  let line = '';
  const newArrs = [];
  for (var n = 0; n < arrText.length; n++) {
      var testLine = line + arrText[n];
      var metrics = ctx.measureText(testLine);
      var testWidth = metrics.width;
     
      if (testWidth > maxWidth & n>0) {
        line = arrText[n];
        newArrs.push(testLine.substr(0, testLine.length-1));
      } else {
        line = testLine;
      }
  }
  newArrs.push(line);
  //console.log(newArrs);
  const row = newArrs.length; // 總行數
  if (row > 1) {
    gridBottom = row * graphicY + gridBottom;
    legendBottom = row * graphicY + legendBottom;
  }
  let graphics = [];
  // 循環每行文字 
  for (let k=0; k < row; k++){
     const temp = {
      type: "text",
      left: "5%",
      bottom: (row-1-k) * lineY, // 數值越大,越靠前
      style: {
        fill: "#333",
        text: [`${newArrs[k]}`].join("\n"),
        font: "14px SongTi"
      }
    }
    graphics.push(temp);
  }
 // console.log(graphics);
  return {
    graphic: graphics,
    gridBottom: gridBottom,
    legendBottom: legendBottom
  };

至此,問題都解決了。

最終的源代碼傳送門:github


總結

瞭解echarts座標系
熟悉echarts基本配置
熟悉echarts graphic配置
瞭解canvas基本API
熟悉數學
對於數字要保持敏感,不要寫死了


多搜索,多嘗試,多思考纔會變通

相關文章
相關標籤/搜索