软软的 Echarts

4,033 阅读5分钟
原文链接: zhuanlan.zhihu.com
一种 Echarts 的屏幕适配的方案。

欢迎关注专栏熵与单子的代码本

在 web 开发中,特别是 dashboard 类型的SPA中,常常要求各组件都能对不同的屏幕分辨率进行缩放适配,常见的解决方案之一是在CSS中使用 rem 和 em 的长度单位结合媒体查询。

最近做到的一些数据可视化需求中,图表组件用到 Echarts,发现由于 Echarts 是基于绘制 Canvas 的方式生成图表,在配置项中,所有的距离和大小只能设置单位为 px,这就使得做屏幕适配十分不便,分辨率一变,不仅图表里的字体大小、距离与整体界面不协调,而且经常会发生字体重叠到一起。

后来想了一想, Echarts 每次重绘是在 JavaScript 中通过 setOption 方法传入配置的,可以利用 window 对象获取到窗口的尺寸,动态设置一个变量起到根元素 font-size 的作用,模拟出 rem 与媒体查询的效果。

首先为组件搞一个样式配置文件,或添加在已有的配置文件中:

// styleProps.js

const viewPort = window.screen.width;
let rootFontSize = 100;
if (viewPort <= 1480) rootFontSize = 71;
if (viewPort >= 1481 && viewPort <= 1760) rootFontSize = 83;
if (viewPort >= 2241 && viewPort <= 3200) rootFontSize = 130;
if (viewPort >= 3201) rootFontSize = 200;

export default {
  titleFontColor: 'rgba(255,255,255,0.97)',
  bodyFontColor: 'rgba(255,255,255,0.91)',
  titleFontSize: rootFontSize * 0.18,
  bodyFontSize: rootFontSize * 0.16,

  titleFontFamily: 'FangZhengZhengZhongHeiJT',
  bodyFontFamily: 'FangZhengZhengXianHeiJT',
};

为了解决 Chrome 浏览器下字体最小为 12px 的问题,习惯上会把根元素的 font-size 设为 100px ,这里也将 rootFontSize 设为 100 。

通过变量 viewPort 获取屏幕宽度,针对以下常见的屏幕分辨率,按区间进行适配:

1366*768
1600*900
1920*1080
2560*1400
3840*2160

取宽度的话,基本上全屏、投影拼接屏等情况也不会影响,当然了,因为是 dashboard 类型的的应用,就不考虑用户拉小窗体之类奇奇怪怪的情况了。

这个 styleProps.js 配置,与我们在整个应用的 CSS 中的设置是“同构”的:

html{
  font-size: 625%;
}
@media (max-width: 1480px) {
  html{
    font-size: 444%;
  }
}
@media (min-width: 1481px) and (max-width: 1760px) {
  html{
    font-size: 521%;
  }
}
@media (min-width: 2241px) and (max-width: 3200px) {
  html{
    font-size: 810%;
  }
}
@media (min-width: 3201px) {
  html{
    font-size: 1250%;
  }
}

body{
  font-size: 0.2rem;
  font-family: FangZhengZhengXianHeiJT;
  color: rgba(255,255,255,0.91);
}

h1 {
  font-size: 0.44rem;
  font-family: FangZhengZhengZhongHeiJT;
  color: rgba(255,255,255,0.97);
}
h2 {
  font-size: 0.32rem;
  font-family: FangZhengZhengZhongHeiJT;
  color: rgba(255,255,255,0.97);
}
h3 {
  font-size: 0.24rem;
  font-family: FangZhengZhengZhongHeiJT;
  color: rgba(255,255,255,0.97);
}

然后为曲线和柱状图表简单的封装了一个组件:

import React from 'react';
import echarts from 'echarts';
import styleProps from '../styleProps';

export default class HArrayChart extends React.Component {
  static defaultProps = {
    title: '',
    /**
     * dataItem {
     *  data: [],
     *  type: '',
     *  name: '',
     *  axis: 'left' | 'right',
     * }
     */
    dataList: [],
    /**
     * {
     *  name: '',
     *  data: []
     * }
     */
    index: {},
    /**
     * ['发电量(kWh)','功率(kW)']
     */
    axes: []
  }
  componentDidMount() {
    this.chart = echarts.init(this.chart);
    this.updateChart();
  }
  componentDidUpdate() {
    this.updateChart();
  }
  updateChart = () => {
    const getSerisOption = (type) => {
      switch (type) {
        case 'line':
          return {
            symbol: 'none'
          };
        default:
          break;
      }
      return null;
    };
    this.chart.setOption({
      title: {
        text: this.props.title,
        x: 'center',
        textStyle: {
          color: styleProps.titleFontColor,
          fontFamily: styleProps.titleFontFamily,
          fontSize: styleProps.titleFontSize
        }
      },
      grid: {
        top: (styleProps.bodyFontSize * 2.4) + styleProps.titleFontSize,
        right: (styleProps.bodyFontSize * 3.8) + 8,
        bottom: (styleProps.bodyFontSize * 4.2) + 8,
        left: (styleProps.bodyFontSize * 3.8) + 8
      },
      yAxis: this.props.axes.map(name => ({
        name,
        nameGap: styleProps.bodyFontSize * 0.8,
        type: 'value',
        nameTextStyle: {
          color: styleProps.bodyFontColor,
          fontFamily: styleProps.bodyFontFamily,
          fontSize: styleProps.bodyFontSize
        },
        axisLine: {
          lineStyle: {
            color: styleProps.bodyFontColor
          }
        },
        axisLabel: {
          color: styleProps.bodyFontColor,
          fontFamily: styleProps.bodyFontFamily,
          fontSize: styleProps.bodyFontSize
        }
      })),
      legend: {
        data: this.props.dataList.map(dataItem => dataItem.name),
        icon: 'bar',
        bottom: 0,
        textStyle: {
          color: styleProps.bodyFontColor,
          fontFamily: styleProps.bodyFontFamily,
          fontSize: styleProps.bodyFontSize
        }
      },
      xAxis: {
        data: this.props.index.data,
        type: 'category',
        name: this.props.index.name,
        nameLocation: 'center',
        nameGap: (styleProps.bodyFontSize * 1.2) + 8,
        nameTextStyle: {
          color: styleProps.bodyFontColor,
          fontFamily: styleProps.bodyFontFamily,
          fontSize: styleProps.bodyFontSize
        },
        axisLine: {
          lineStyle: {
            color: styleProps.bodyFontColor
          }
        },
        axisLabel: {
          color: styleProps.bodyFontColor,
          fontFamily: styleProps.bodyFontFamily,
          fontSize: styleProps.bodyFontSize
        }
      },
      series: this.props.dataList.map(dataItem => ({
        data: dataItem.data,
        type: dataItem.type || 'line',
        name: dataItem.name,
        animation: false,
        yAxisIndex: dataItem.axis === 'right' ? 1 : 0,
        ...getSerisOption(dataItem.type || 'line')
      }))
    });
  }
  render() {
    return <div className={this.props.className} ref={(div) => { this.chart = div; }} />;
  }
}

之前的 styleProps.js 配置文件通过 import 引入,相关的参数会在组件生成之前就计算好,这样足以应对屏幕分辨率的适配了,当然如果希望能够更动态的调整,也可以设置一个 state 监视视口宽度,在每次 componentWillUpdate 里重算参数。

为了方便不熟悉 Echarts 配置的使用者,这个组件只暴露了4个参数:

title: '',
dataList: [],
index: {},
axes: []

dataList 通过数组可传入多条数据 series ,每条可独立配置曲线或柱状类型,以及关联到左还是右坐标轴,每条数据 series 的长度需与横坐标的 index 长度对应。当然这样的配置使得自由度很小,可以给使用者也暴露个 option 配置项,覆盖默认的配置。

由于在styleProps.js 配置中对字体大小的设置,与我们在整个应用的 CSS 中的设置是“同构”的,所以通过 titleFontSize 和 bodyFontSize 就使得当自适应匹配屏幕分辨率变化时, Echarts 与整个界面的行为保持一致,字体大小、距离等变化协调。

图表中各个元素之间的距离,比如 Legend 和 xAxis 的距离、 grid 与四边的距离等,也用bodyFontSize的比例进行设置,这样当图表中字体变化后也不会发生相互覆盖的情况了:

yAxis: this.props.axes.map(name => ({
  nameGap: styleProps.bodyFontSize * 0.8,        
})),

由于项目中使用了 styled-components ,所以组件根元素要传入 className={this.props.className} 。

最后组件使用的时候大致是这个样子:

import styled from 'styled-components';
import { HArrayChart } from './hyphix';

const Chart = styled(HArrayChart)`
  width: 5rem;
  height: 3rem;
`;
const dataList = [
  {
    data: [2, 4, 5, 80, 22, 27, 39, 0, 12],
    name: '今日功率',
  },
  {
    data: [32, 33, 3, 8, 2, 27, 3, 3, 15],
    name: '昨日功率',
    type: 'bar'
  },
  {
    data: [837, 3099, 122, 3999, 100, 299, 3987, 3298, 1500],
    name: '昨日效率',
    axis: 'right'
  },
];
const index = { data: [1, 2, 3, 4, 5, 6, 7, 8, 9], name: '时间' };const Chart = styled(HArrayChart)`
  width: 500px;
  height: 300px;
`;
const dataList = [
  {
    data: [2, 4, 5, 80, 22, 27, 39, 0, 12],
    name: '今日功率',
  },
  {
    data: [32, 33, 3, 8, 2, 27, 3, 3, 15],
    name: '昨日功率',
    type: 'bar'
  },
  {
    data: [837, 3099, 122, 3999, 100, 299, 3987, 3298, 1500],
    name: '昨日效率',
    axis: 'right'
  },
];
const index = { data: [1, 2, 3, 4, 5, 6, 7, 8, 9], name: '时间' };

<Route path="/test" exact component={() => <Chart title={'今日发电功率曲线'} dataList={dataList} index={index} axes={['发电量(kWh)', '功率(kW)']} />} />

由于没有好看的数据,图就不上了,组件只是个大致的思路,要做的好看细节上还需要再完善。