node环境下给echarts图表加文案

本文分享了在ECharts中实现复杂图文混排的过程,包括如何处理文本换行、调整行间距以及解决文本与图表元素重叠等问题。

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

图表的宽度是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
熟悉数学
对于数字要保持敏感,不要写死了


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

我希望以下内容,技术人员一看就明白自己要做什么,所以请进一步描写流程和每一个节点。请注意这里相当于该智能体有跨岗位协作引擎工作流,等。1.运营智能体AI工具需求表 目标:构建跨岗位协同中枢,实现“指令-执行-汇报”闭环,打通多平台账号管理 (1)一、核心功能升级 模块 新增功能描述 输入 输出 技术实现建议 跨岗位协作引擎 1.指令中枢:管理岗输入工作任务,AI自动拆解为子任务 工作任务 (如“618大促推广方案”) 三大输出板块: 1.市场分析,ai根据工作任务分析市场趋势,如历年该时期销量最高日期和时间点,爆款商品的宣传点、折扣优惠计划。 2.内容创作,ai根据市场分析完成工作任务。 3.投放排期,ai根据内容创作的结果进行投放排期的安排。 采用DAG工作流引擎,对接企业IM系统(如企业微信/钉钉) 2.任务分发:按岗位职责自动分配至运营/设计/文案人员界面,实时跟踪进度 内容创作、投放排期 运营/设计/文案的各自任务内容,以及工作进度可视化追踪进度 3.自动汇总:各岗位提交成果后,AI生成结构化汇报(含数据看板+优化建议) 运营/设计/文案的工作成果 ai分析后输出工作成果报表,框架为: 1.运营成果,图表呈现:访客-线形图,购率、下单率、支付率-三合一综合饼图,成交额-线形图、各购物途径订单量-柱状图。 2.单个设计成果,图表呈现:新老设计图片对比,新老设计点击量对比-线性图,新老设计转换率对比-线性图,用户跳出率-线性图,ai为设计内容打分。 批量输出:如果有多组设计,批量按以下格式输出新老图片对比+新老点击量对比+新老转换率对比+新老跳出率对比+ai打分。 3.文案成果,新旧文案对比(新旧文案内容、点击率-线性图、购率-线性图) 多账号管理平台 1.一键切换:支持绑定同一平台多个账号(如抖音企业号+员工号),单界面切换账号发布。 各账号的账号和密码/手机号登陆? 发布笔记/视频时,可直接列出所有已登陆的账号名称,选择其中一个账号发布。 集成跨平台API(抖音开放平台/小红书CREATOR等),内置NLP改写模型(如T5+GPT-4o) 2.去重引擎:自动识别即将发布的笔记中,与竞品爆款相似度>85%的内容,标记重复风险 已完成但未发布的笔记 ai将其与竞品爆款笔记的三个方面进行对比:文本相似度比对、图像识别、商品属性分析。如果认为两段内容/商品在核心信息上高度重合,图片为视觉重合,大于85%,则跳出弹窗用红色字进行明确提醒。 3.智能改写:解析爆款笔记,通过ai生成爆款框架,基于爆款框架生成差异化文案(保留核心卖点,调整叙事结构/视觉元素) 爆款笔记的分享链接 1.获取该笔记的标题,标签文案,图片,收藏评论点赞转发。 2.基于ai生成爆款笔记框架。 3.保留标题标签,改写文案,图片根据滤镜和MD5值去重 全域发布协同 1.跨平台适配:同一内容自动调整格式(抖音竖版9:16/小红书3:4) 一组图片/一张图片 1.选择发布的平台 2.根据选择的平台,进行图片格式调整。 调用各平台官方SDK,内置流量规则库(更新机制) 2.定时发布:设置多账号批量发布计划,规避平台限流规则 大致需求 1.ai生成规避平台限流的多账号批量发布计划。 2.根据发布计划,自动进行多账号发布定时的设定。 3.效果追踪:实时统计各账号播放量/转化率,生成跨账号对比报告 已登陆账号 1.实时追踪各账号播放量/转化率,以分钟或小时或日期为单位的线性图。 2.一键生成跨账号播放量/转化率对比报告 (2)二、协同流程示例(以“新品推广”指令为例) graph TD A[管理岗输入指令] --> B[AI拆解任务] B --> C1{运营岗:竞品分析报告} B --> C2{文案岗:爆款文案生成} B --> C3{设计岗:视频分镜脚本} C1 --> D[AI汇总竞品投放策略] C2 --> D[AI优化文案结构] C3 --> D[AI审核分镜可行性] D --> E[自动生成PPT汇报+发布排期表] (3)三、关键技术创新点 1.爆款内容DNA库 o抓取竞品爆款内容 → 拆解为内容要素矩阵(标题结构/BGM/转折点/商品露出时机) o自动生成要素组合建议(例:抖音爆款=悬念标题+0.5s品牌露出+3秒痛点镜头[^1]) 2.跨岗位知识共享 o建立岗位专属知识库:运营侧(平台规则/用户画像)、文案侧(热词库/话术模板)、设计侧(平台视觉规范) o支持智能体跨库检索(如设计岗输入“小红书3C类目首图规范”,自动推送最新案例) 3.安全管控机制 |风险类型|解决方案| |--------------------|---------------------------------| | 账号切换风险 | 操作需双重验证+行为审计日志 | | 内容重复风险 | 相似度实时检测+平台原创度评分 | | 权限泄露风险 | 基于RBAC模型的字段级数据隔离 | (4)四、落地实施建议 1.技术架构# 核心组件组成 Orchestrator = Workflow_Engine() + Cross_Platform_API_Gateway() Content_Module = Plagiarism_Detector() + Multi_Modal_Generator(model="GPT-4o") Account_Manager = Permission_Controller(RBAC) + Auto_Publish_Scheduler() 研发重点提示:优先确保抖音/小红书官方API对接合规性,采用OAuth2.0授权机制;任务流引擎需支持实时回滚(防止某环节失败导致数据丢失)。 附:竞品技术参考 360智语的「智能体业务流程融合」方案(政企场景权限管控[^3]) 爆款内容原子化重组技术(符合工作时空解构趋势[^2])
06-14
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值