Echarts 图表在 Tab 切换时遇到的渲染问题

最近在写的项目中需要将一些统计信息数据可视化展示,所以有用到 Echarts。

1.发现问题

在与 Tab 组件结合使用时,遇到了图表的渲染问题。在Today这个 Tab 中,两个图表显示正常:
spapshot_normal.png
Tab 切换后,两个图表的渲染情况十分怪异:饼状图的height为 0,并且两个图表的width的值与自己设置的不符:
spapshot.jpg

2.探寻源码

Echarts 的图表是由 zrender 初始化的,所以就去查阅了 zrender 关于初始化图表的源码。
zrender/src/Painter.js

var Painter = function (root, storage, opts) {
//...
    if (!singleCanvas) {
        this._width = this._getSize(0);
        this._height = this._getSize(1);

        var domRoot = this._domRoot = createRoot(
            this._width, this._height
        );
        root.appendChild(domRoot);
    }
    else {
        var width = root.width;
        var height = root.height;

        if (opts.width != null) {
            width = opts.width;
        }
        if (opts.height != null) {
            height = opts.height;
        }
        this.dpr = opts.devicePixelRatio || 1;

        // Use canvas width and height directly
        root.width = width * this.dpr;
        root.height = height * this.dpr;

        this._width = width;
        this._height = height;

        // Create layer if only one given canvas
        // Device can be specified to create a high dpi image.
        var mainLayer = new Layer(root, this, this.dpr);
        mainLayer.__builtin__ = true;
        mainLayer.initContext();
        // FIXME Use canvas width and height
        // mainLayer.resize(width, height);
        layers[CANVAS_ZLEVEL] = mainLayer;
        mainLayer.zlevel = CANVAS_ZLEVEL;
        // Not use common zlevel.
        zlevelList.push(CANVAS_ZLEVEL);

        this._domRoot = root;
    }
// ...
}

根据以上源代码可知,init 时若不设置图表宽高,则 canvas 自动获取容器元素的宽高。

3.审查自己的代码

而自己项目中在初始化图表时,并没有设置宽与高:

const weekBarChart = echarts.init(this.weekBarChart);

然而这时容器元素 Tab 的隐藏样式为display: none,图表对象被初始化时,图表的容器元素可能还没渲染,所以 canavs 的宽高获取就成了问题。
在来看一下自己对柱状图与饼状图容器元素的样式设置:

  &__barChart {
    width: 100%;
    height: 260px;
  }

  &__pieChart {
    width: 50%;
    min-height: 250px;
  }

到了这里,就可以理解为什么会出现上图那样的奇怪的渲染情况了:
对于两个图表的宽度设置,我用的都是百分比,对于饼状图的高度设置,我用的是min-height,这些都属于 CSS 计算值,需要根据父元素的样式计算得到真正的值,所以在整个 Tab 为display:none时,这些计算值并不起作用,这也导致了「饼状图的height为 0,并且两个图表的width的值与自己设置的不符」。
那么为什么两个图表的width的值与自己设置的不符?
当宽高设置为百分比时,zrender 会做哪些处理?可以继续看一下 zrender 获取宽高部分的源码:

_getSize: function (whIdx) {
    var opts = this._opts;
    var wh = ['width', 'height'][whIdx];
    var cwh = ['clientWidth', 'clientHeight'][whIdx];
    var plt = ['paddingLeft', 'paddingTop'][whIdx];
    var prb = ['paddingRight', 'paddingBottom'][whIdx];

    if (opts[wh] != null && opts[wh] !== 'auto') {
        return parseFloat(opts[wh]);
    }

    var root = this.root;
    // IE8 does not support getComputedStyle, but it use VML.
    var stl = document.defaultView.getComputedStyle(root);

    return (
        (root[cwh] || parseInt10(stl[wh]) || parseInt10(root.style[wh]))
        - (parseInt10(stl[plt]) || 0)
        - (parseInt10(stl[prb]) || 0)
    ) | 0;
},

改写一下,其实最后 return 的结果是这样的:

=> return (parseInt10(root.style[wh]));
=> return parseInt(document.defaultView.getComputedStyle[width], 10)
=> return parseInt('50%', 10)
=> return 50

所以,这就能解释饼状图的宽度为何是 50px 了。

4. 解决方案

既然 Tab 切换前容器元素还未被渲染,那么我们可以监听 Tab 切换事件,在 Tab 切换后 resize 两个图表,并设置宽和高。

  handleTabClick = (key) => {
    if (key === '1') {
      weekBarChart.resize({
        width: todayBarChart.getWidth()
      });
      weekPieChart.resize({
        width: todayPieChart.getWidth(),
        height: 250
      });
    }
  }
Show Comments