0. 缘起

照理来说,春节过后的我现在应该还在快乐地摸鱼划水,但小测试猛地发来测试文档和示例,我对了一波之后对其中有个Echarts的label显示超过20截断有了些许冷汗泠泠的感觉。遂看了一波原本的代码,感慨下开山祖师爷细节处理的到位。

1. 柱状图数值显示的大致情况

xAxis

竖向

对于按照高度限制竖直文本高度的情况,需要按照 Math.floor(限制高度/行高),计算出限制字数

横向

全显示

其他方向

根据垂直高度及倾斜角,计算倾斜状态下的最大字符宽度

yAxis

y轴数值是什么就显示什么

2. 对xAxis的处理

xAxis配置中注意两个属性axisLabelnameTextStyle

axisLabel: {
      //   interval: 0,
      show: config.showLabel,
      rotate: getRotate(config.labelRotate),
      formatter: getFormatter(config.labelRotate, aAxisHeight),
      rich: {
        text: {
          align: 'right',
          fontSize: LABEL_FONT_SIZE,
          lineHeight: LABEL_FONT_SIZE,
        },
        ellipsis: verticalEllipsis.style,
      },
    },
    nameTextStyle: {
      // 当显示label时,让name稍微下移,不然与label在同一行,容易混淆
      // 隐藏label时,让name上移,否则会超出视口
      lineHeight: config.showLabel ? 50 : 25,
    },

3. 关键函数

getFormatter

xAxislabel进行格式化,是入口函数

/**
 * get xAxis label formatter
 * */
const getFormatter = (rotate: XAxisLabelRotate, height: number) => {
  /**
   * echarts会根据xaxis label 高度动态的调整底部间距,我们需要做的
   * 是限制xaxis label 高度,不让其超过 “20个中文字符高度数值” 的高度
   *
   * 1. 默认遵从 从左到右原则
   *
   * 2. 对于竖直排列的文本,要这样展示i
   * -----------------------------
   *      乐
   *      盘
   *      游
   *
   *
   * */
  const formatter = (v: any) => {
    const text = String(v);

    // 竖直排列
    // 对于按照高度限制竖直文本高度的情况,只需要按照 Math.floor(限制高度/行高)
    // 计算出限制字数,然后对文本进行截取即可 (因为中英文字符高度相同)
    if (rotate === XAxisLabelRotate.Vertical) {
      const chartCount = Math.floor(height / LABEL_FONT_SIZE);

      return renderVerticalText({
        text,
        count: chartCount,
      });
    }

    // 水平
    if (rotate === XAxisLabelRotate.Horizontal) {
      return `{text|${text}}`;
    }

    // 其他角度
    // 根据垂直高度及倾斜角,计算倾斜状态下的最大字符宽度
    const stringWidth = height / Math.sin(Math.PI * (Math.abs(rotate) / 180));
    // 单个中文字符宽度
    const charWidth = textRuler.measureText('乐', {
      fontSize: `${LABEL_FONT_SIZE}px`,
    });
    // 计算最大字符数
    const chartCount = Math.floor(stringWidth / charWidth);
    // 截取字符
    const sub = subString(text, chartCount);
    return `{text|${sub}${sub !== text ? '...' : ''}}`;
  };

  return formatter;
};

renderVerticalText

将横向文本渲染成竖向的

/**
 * 将普通的字符串按照格式化成如下格式用于echarts渲染:
 *
 * "乐盘游"
 *  ↓↓↓↓↓↓
 * --------------
 *    乐
 *    盘
 *    游
 *
 * NOTE: 需要结合echarts rich属性使用
 *
 * 对于按照高度限制竖直文本高度的情况,只需要按照 Math.floor(限制高度/行高)
 * 计算出限制字数即可
 *
 * */
export default function renderVerticalText({ text, count }: Config): string {
  const shouldSlice = text.length > count;
  const subText = shouldSlice ? text.substring(0, count) : text;
  const verticalText = subText.split('').join('\n');

  if (shouldSlice) {
    return `{text|${verticalText}}\n{ellipsis|${verticalEllipsis.text}}`;
  }

  return `{text|${verticalText}}`;
}
export const verticalEllipsis = {
  text: '.\n.\n.',
  style: {
    lineHeight: 4,
    fontSize: 12,
  },
};

subString

截取指定宽度字符

/**
 * 截取 **指定数量中文字符宽度** 的字符串,超过的部分舍弃
 *
 * */
export default function subString(str: string, len: number): string {
  if (str.length < len) {
    return str;
  }

  // 按照实际长度进行截取
  const subStr = str.substring(0, len);
  // 在截取后的字符串中查找,属于ascii编码的字符,因为这些字符宽度只有中文一半
  // (其他半角字符基本不会出现,所以不做考虑)
  const enChars = subStr.match(/[\u20-\u7f]/g);

  // 如果ascii编码的字符数量大于1个,则实际截取的字符串长度会比预想的
  // 长度小很多,所以需要补齐
  if (enChars && enChars.length > 1) {
    const restLen = Math.floor(enChars.length / 2);

    return subStr + subString(str.substring(len), restLen);
  }

  return subStr;
}

4. 倾斜时的类:

倾斜时还有一些细节处理,看了真感觉叹为观止。

先算单个字符,再算最大字符数,最后截取这一部分显示

textRulerInstance.ts

import createTextRuler from './textRuler';

export default createTextRuler();

textRuler.ts

import { setCSSFont, CSSFont } from './cssFont';

/**
 * 创建一个测量字符绘制像素宽度的尺子📏
 *
 * */
export default function createTextRuler() {
  const canvas = document.createElement('canvas');
  const ctx2D = canvas.getContext('2d');

  if (!ctx2D) {
    throw Error('该浏览器不支持canvas,请使用现代浏览器');
  }

  /**
   * 计算通过css样式设置的字体绘制出的字符的宽度。
   *
   * */
  const measureText = (char: string, font?: CSSFont): number => {
    ctx2D.font = setCSSFont(font);
    const text = ctx2D.measureText(char);
    return text.width;
  };

  const get2DContext = () => {
    return ctx2D;
  };

  return {
    measureText,
    get2DContext,
  };
}

配套的还有个工具类

cssFont.ts

export interface CSSFont {
  fontSize?: string;
  fontFamily?: string;
  fontWeight?: string;
  fontStyle?: string; // normal | italic | oblique <angle>?
  fontVariant?: string;
  lineHeight?: string;
  fontStretch?: string;
}

/**
 * 规则:
 *
 * + 必须包含以下值:
 *      - <font-size>
 *      - <font-family>
 * + 可以选择性包含以下值:
 *      - <font-style>
 *      - <font-variant>
 *      - <font-weight>
 *      - <line-height>
 * + font-style, font-variant 和 font-weight 必须在 font-size 之前
 * + 在 CSS 2.1 中 font-variant 只可以是 normal 和 small-caps
 * + line-height 必须跟在 font-size 后面,由 "/" 分隔,例如 "16px/3"
 * + font-family 必须最后指定
 *
 * 正式语法:
 *
 * `[ [ <'font-style'> || <font-variant-css21> || <'font-weight'> || <'font-stretch'> ]? <'font-size'> [ / <'line-height'> ]? <'font-family'> ]`
 *
 * */
export const setCSSFont = (
  {
    fontWeight = 'normal',
    fontSize = '16px',
    fontFamily = 'sans-serif',
    fontStyle = 'normal',
    fontVariant = 'normal',
    lineHeight = '1',
    fontStretch = 'normal',
  }: CSSFont = {} as CSSFont
): string => {
  return `${fontStyle} ${fontVariant} ${fontWeight} ${fontStretch} ${fontSize}/${lineHeight} ${fontFamily}`;
};

人生到处知何似,应似飞鸿踏雪泥。