# 背景

在使用 Chart.js 的时候,禁用了所有自适应相关的配置,出现了在高分屏情况下绘图模糊的原因,在调试的时候进入了 Chart.resize() 方法,于是打算一探究竟。

# Resize 函数展示

以下代码出现在 /src/core/core.controller.js 文件中:

resize 代码展示
helpers.extend(Chart.prototype, {
  // ...
  resize: function(silent) {
    var me = this;
    var options = me.options;
    var canvas = me.canvas;
    var aspectRatio = (options.maintainAspectRatio && me.aspectRatio) || null;

    // the canvas render width and height will be casted to integers so make sure that
    // the canvas display style uses the same integer values to avoid blurring effect.

    // Set to 0 instead of canvas.size because the size defaults to 300x150 if the element is collapsed
    var newWidth = Math.max(0, Math.floor(helpers.getMaximumWidth(canvas)));
    var newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : helpers.getMaximumHeight(canvas)));

    if (me.width === newWidth && me.height === newHeight) {
      return;
    }

    canvas.width = me.width = newWidth;
    canvas.height = me.height = newHeight;
    canvas.style.width = newWidth + 'px';
    canvas.style.height = newHeight + 'px';

    helpers.retinaScale(me, options.devicePixelRatio);

    if (!silent) {
      // Notify any plugins about the resize
      var newSize = {width: newWidth, height: newHeight};
      plugins.notify(me, 'resize', [newSize]);

      // Notify of resize
      if (me.options.onResize) {
        me.options.onResize(me, newSize);
      }

      me.stop();
      me.update({
        duration: me.options.responsiveAnimationDuration
      });
    }
  },
  // ...
})

# 过程解析

# 1. 获取 ratio

var aspectRatio = (options.maintainAspectRatio && me.aspectRatio) || null;

这里项目中都被禁用了,所以 aspectRatio 为 null;

# 2. 接下来获取 canvas 的高宽

这里有一段注释:

// the canvas render width and height will be casted to integers so make sure that
// the canvas display style uses the same integer values to avoid blurring effect.

// Set to 0 instead of canvas.size because the size defaults to 300x150 if the element is collapsed

这段注释意思是:为了避免显示模糊,canvas 的 width 和 height 与 style.width 和 style.height 要对应相同,并且需要设置为整数。

然后告诉我们 canvas 的默认尺寸是 300*150,所以不能直接使用 canvas.size 有可能为空。

然后开始计算新的高宽:

var newWidth = Math.max(0, Math.floor(helpers.getMaximumWidth(canvas)));
var newHeight = Math.max(0, Math.floor(aspectRatio ? newWidth / aspectRatio : helpers.getMaximumHeight(canvas)));
getMaximumWidth 代码
helpers.getMaximumWidth = function(domNode) {
  var container = helpers._getParentNode(domNode);
  if (!container) {
    return domNode.clientWidth;
  }

  var clientWidth = container.clientWidth;
  var paddingLeft = helpers._calculatePadding(container, 'padding-left', clientWidth);
  var paddingRight = helpers._calculatePadding(container, 'padding-right', clientWidth);

  var w = clientWidth - paddingLeft - paddingRight;
  var cw = helpers.getConstraintWidth(domNode);
  return isNaN(cw) ? w : Math.min(w, cw);
};

这里的 getMaximumWidth、getMaximumHeight 是 chart.js 中的工具类的方法。

getMaximumWidth(getMaximumHeight 同理)大致意思是获取传入的 dom,获取其父级元素的 clientWidth(有父级元素的话,没有直接返回传入的 dom 的 clientWidth),减去其父级元素左右的 padding 以获取内容宽度。

然后取得传入的 DOM 的最大宽度,与父级元素 content 宽度进行比较,取其中小的一个。

题外话

之所以加粗表示,是因为这里被坑了。

取小的一个,如果没有设置 Canvas 的高宽,则使用默认的尺寸:300*150。

获取到需要的高宽后,如果发现计算出的结果和原本相同,就不需要进行 resize 操作:

if (me.width === newWidth && me.height === newHeight) {
  return;
}

# 3. 设置新的高宽

如果发现计算出的高宽和原本高宽不同,则需要进行 resize 操作。

赋值给当前实例以及设置 Canvas 的高宽:

// 设置新的 width、height 和 style
canvas.width = me.width = newWidth;
canvas.height = me.height = newHeight;
canvas.style.width = newWidth + 'px';
canvas.style.height = newHeight + 'px';

# 4. 针对 Retina 屏幕修改 Canvas 高宽

helpers.retinaScale(me, options.devicePixelRatio);

传入 options.devicePixelRatio 作为强制分辨率 ratio。

retinaScale 代码
helpers.retinaScale = function(chart, forceRatio) {
  var pixelRatio = chart.currentDevicePixelRatio = forceRatio || (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
  if (pixelRatio === 1) {
    return;
  }

  var canvas = chart.canvas;
  var height = chart.height;
  var width = chart.width;

  canvas.height = height * pixelRatio;
  canvas.width = width * pixelRatio;
  chart.ctx.scale(pixelRatio, pixelRatio);

  // If no style has been set on the canvas, the render size is used as display size,
  // making the chart visually bigger, so let's enforce it to the "correct" values.
  // See https://github.com/chartjs/Chart.js/issues/3575
  if (!canvas.style.height && !canvas.style.width) {
    canvas.style.height = height + 'px';
    canvas.style.width = width + 'px';
  }
};

pixelRatio 要么是配置中的内容,要么是 window.devicePixelRatio 要么是 1。

如果是 1 则不操作,否则对 canvas 的高宽做比例缩放,并使用 Canvas 中的 scale(scalewidth, scaleheight) API 对其内容进行缩放(详见:HTML5 canvas scale() 方法)。

最后是对于没有设置 style 的 canvas 进行缩放设置。

# 5. 根据传参进行处理

if (!silent) {
  // Notify any plugins about the resize
  var newSize = {width: newWidth, height: newHeight};
  plugins.notify(me, 'resize', [newSize]);

  // Notify of resize
  if (me.options.onResize) {
    me.options.onResize(me, newSize);
  }

  me.stop();
  me.update({
    duration: me.options.responsiveAnimationDuration
  });
}

如果传入 resize 函数的参数为 Falsy,那么就进行以下操作:

  • 派发 resize 事件告诉所有插件;
  • 执行配置上的 onResize 事件;
  • 取消当前动画,并进行新的一轮动画(根据配置中的 responsiveAnimationDuration 时间进行动画)。

# 解决方案

在 new Chart(canvas, config) 之前,先对 canvas 设置高宽。

# 总结

  • Canvas 默认的高宽为 300*150;
  • 遇到思考不顺的问题,可以试着查看源码,顺着作者的思路想想解决方法;
  • 看源码真有意思嘻嘻嘻 😉
上次更新: 2018/12/27下午12:53:48