node环境下给echarts图表加文案

开发环境:node
开发语言:javascript
开发依赖的包:

  • node-charts
  • canvas

图表的宽度是1000
先把最终要呈现的效果贴出来,见下图:


查看charts配置项及实例,给图表增加额外的文本块,只有一个属性graphic【ˈgrafik】似乎可以实现,它指的是原生图形元素组件,可以由多种类型组成,如image, text, circle, sector, ring, polygon, polyline, ...,结合给的实例,看起来很不错,so easy!马上开干。

代码如下:

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();

效果如下图:

问题有俩:

  1. 只有一行,后面的文字都被截断了
  2. 覆盖了图例的文字,没有间距

第2个问题很好解决,熟悉echarts配置的话,给grid的bottom,legend的bottom设个值,但这个值是动态的,和文案的行数有关系,先定一个基值,假如只有一行,grid的bottom为90,legend的bottom为35,每多一行,就多加15,伪代码如下:

const config = {
  legend: { bottom: 3 * 15 + 35, show: true, data: ["身份证数量", "环比增长率"] },
  grid: {bottom:  3 * 15 + 90}
...

要得到具体的行数,其实是解决第1个问题,所以最核心的问题是在限定的宽度里怎么计算一段文字的行数?
先算下一行能放多少个字符,中英文都算一个字符,以都是中文来算,这样算下得到结果是63,行数 = Math.ceil(字符总长度/63)。得到行数后,还要知道每行的字符是什么,毕竟换行是要这样的:

...
  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;

得到效果如下:

换行了,但是每行的字符不一样,而且没行间距,挤得慌。说明计算每一行的长度的方法不对,中文和英文的宽度不一样,需要知道每个字符的宽度才行,然后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
熟悉数学
对于数字要保持敏感,不要写死了


多搜索,多尝试,多思考才会变通

你可能感兴趣的:(echarts3,javascript)