Repository: chartjs/Chart.js
File: src/helpers/helpers.dom.js (JavaScript)

Code before:
  width = round1(Math.min(width, maxWidth, containerSize.maxWidth));
  height = round1(Math.min(height, maxHeight, containerSize.maxHeight));
  if (width && !height) {
    // https://github.com/chartjs/Chart.js/issues/4659
    // If the canvas has width, but no height, default to aspectRatio of 2 (canvas default)
    height = round1(width / 2);
  }
  return {
    width,
    height
  };
}

export function retinaScale(chart, forceRatio, forceStyle) {
  const pixelRatio = chart.currentDevicePixelRatio = forceRatio || 1;
  const {canvas, width, height} = chart;

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

  // 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 && (forceStyle || (!canvas.style.height && !canvas.style.width))) {
    canvas.style.height = (canvas.height / pixelRatio) + 'px';
    canvas.style.width = (canvas.width / pixelRatio) + 'px';
  }
}

/**
 * Detects support for options object argument in addEventListener.
 * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support
 * @private
 */
export const supportsEventListenerOptions = (function() {
  let passiveSupported = false;
  try {
    const options = {
      get passive() { // This function will be called when the browser attempts to access the passive property.
        passiveSupported = true;
        return false;
      }
    };
    // @ts-ignore
    window.addEventListener('test', null, options);
    // @ts-ignore
    window.removeEventListener('test', null, options);
  } catch (e) {
    // continue regardless of error
  }

Review comment (bug):
I don't think this is the correct fix as it will break retina screens. In the case of a retina display, `pixelRatio` will be a number greater than 1. If we need to round, it feels like it should be `Math.floor(canvas.height)` but I suspect there will be cases where this introduces oscillation during resizing. We saw that a lot in previous implementations of the chart resizing because when the height is reduced from say 300.8 to 300, the need for a scrollbar may disappear triggering another resize which increases the width slightly triggering the scrollbar to re-appear which changes the width leading to an infinite loop.

Code after:
  width = round1(Math.min(width, maxWidth, containerSize.maxWidth));
  height = round1(Math.min(height, maxHeight, containerSize.maxHeight));
  if (width && !height) {
    // https://github.com/chartjs/Chart.js/issues/4659
    // If the canvas has width, but no height, default to aspectRatio of 2 (canvas default)
    height = round1(width / 2);
  }
  return {
    width,
    height
  };
}

/**
 * @param {import('../core/core.controller').default} chart
 * @param {number} [forceRatio]
 * @param {boolean} [forceStyle]
 * @returns {boolean} True if the canvas context size or transformation has changed.
 */
export function retinaScale(chart, forceRatio, forceStyle) {
  const pixelRatio = forceRatio || 1;
  const deviceHeight = Math.floor(chart.height * pixelRatio);
  const deviceWidth = Math.floor(chart.width * pixelRatio);

  chart.height = deviceHeight / pixelRatio;
  chart.width = deviceWidth / pixelRatio;

  const canvas = chart.canvas;

  // 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 && (forceStyle || (!canvas.style.height && !canvas.style.width))) {
    canvas.style.height = `${chart.height}px`;
    canvas.style.width = `${chart.width}px`;
  }

  if (chart.currentDevicePixelRatio !== pixelRatio
      || canvas.height !== deviceHeight
      || canvas.width !== deviceWidth) {
    chart.currentDevicePixelRatio = pixelRatio;
    canvas.height = deviceHeight;
    canvas.width = deviceWidth;
    chart.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
    return true;
  }
  return false;
}

/**
 * Detects support for options object argument in addEventListener.