Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Round canvas.style dimensions to avoid blurring #9015

Merged
merged 1 commit into from May 8, 2021

Conversation

Flupp
Copy link
Contributor

@Flupp Flupp commented May 2, 2021

When canvas.height and canvas.width are set, the numbers are rounded to
integers. The same rounding must be applied to canvas.style.height and
canvas.style.width to avoid scaling of the canvas which would lead to
blurring.


With the following example page, depending on the size of the browser window, you get the following blurry result:

Chart.js 3.2.1

With this fix, the result looks as follows:

this fix

Example page:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8"/>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<meta name="referrer" content="no-referrer">
	<title>Chart.js Blur Test</title>
	<script src="https://cdn.jsdelivr.net/npm/chart.js@3.2.1/dist/chart.min.js"></script>
	<!-- <script src="dependencies/Chart.js/dist/chart.js"></script> -->
	<style>
		.chart-container {
			position: relative;
			width: 75vw;
			height: 75vh;
		}
	</style>
</head>
<body>
	<div class="chart-container">
		<canvas id="myChart" width="400" height="400"></canvas>
	</div>
    <table border="1">
        <tr><th></th><th>width</th><th>height</th></tr>
        <tr><td>canvas</td><td id="canvasWidth"></td><td id="canvasHeight"></td></tr>
        <tr><td>style</td><td id="styleWidth"></td><td id="styleHeight"></td></tr>
    </table>
    <script>
        Chart.register({
            id: 'info',
            afterUpdate: function(chart, options) {
                document.getElementById('canvasWidth').textContent = chart.canvas.width;
                document.getElementById('canvasHeight').textContent = chart.canvas.height;
                document.getElementById('styleWidth').textContent = chart.canvas.style.width;
                document.getElementById('styleHeight').textContent = chart.canvas.style.height;
            },
        });

        var data = [];
        for (let x = 0; x <= 20; ++x) {
            for (let y = 0; y <= 20; ++y) {
                data.push({x: x, y: y});
            }
        }

        var ctx = document.getElementById('myChart').getContext('2d');
        var myChart = new Chart(ctx, {
            type: 'scatter',
            data: {
                datasets: [{
                    label: "some random data",
                    data: data,
                }]
            },
            options: {
                animation: false,
                maintainAspectRatio: false
            }
        });
    </script>
</body>
</html>

PS: With npm test, some image-based tests fail for me. However, this already happens with master.

@@ -162,8 +162,8 @@ export function retinaScale(chart, forceRatio, forceStyle) {
// 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 = height + 'px';
canvas.style.width = width + 'px';
canvas.style.height = (canvas.height / pixelRatio) + 'px';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When canvas.height is assigned, the value is already implicitly rounded. So no additional flooring should be required.

Regarding potential oscillations: I understand your concerns, however, I do not see a good solution. The following naive ideas come to my mind:

  • Wrap the canvas in yet another div and apply the unrounded dimensions to this div while rounding the dimensions for the canvas.
  • Add the discrepancy between unrounded and rounded values as padding to the canvas.
  • Trade the blurring problem for the potential oscillation problem while assuming the blurring does occur far more often than the oscillation would occur.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. So the width/height are multiplied with pixel ratio and implicitly converted to integers. Using that integer value and dividing it with the ratio should actually result in more accurate result even for retina displays.

Not quite sure if this case has been tested previously.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok that makes more sense. I don't know how we'd get test coverage on this, but the explanation makes sense. If we could adjust to add a comment here explaining why we're doing this, I'm ok with the change

@kurkle
Copy link
Member

kurkle commented May 2, 2021

DPR: 1.25

master:
image
263 X 262 / 210.4px X 210px

pr:
image
263 X 262 / 210.4px x 209.6px

not really noticing any difference.

@kurkle
Copy link
Member

kurkle commented May 2, 2021

master
image
image

pr
image
image

@kurkle
Copy link
Member

kurkle commented May 2, 2021

Not sure if #7351 can be closed with this. And not sure if this should be marked as breaking, since it could affect layouts and resizing in some edge cases.

@kurkle kurkle requested a review from simonbrunel May 3, 2021 04:31
@simonbrunel
Copy link
Member

I seem to remember that deriving the style directly from the canvas values causes some other issues. But that was looong time ago, I can't remember why and the suggested fix seems to make sense indeed because the style and canvas dimensions should be proportional, which is currently not necessarily the case.

@kurkle
Copy link
Member

kurkle commented May 3, 2021

I seem to remember that deriving the style directly from the canvas values causes some other issues.

Maybe it has to do with hidden canvas returning zeros for dimensions?

Another thing that might need consideration is the width and height of the chart itself that would be slightly different in these cases. Maybe those should also be updated or the fix applied to the chart size computation already?

@simonbrunel
Copy link
Member

Maybe it has to do with hidden canvas returning zeros for dimensions?

Could be the reason and you are right, that may not work correctly if the canvas is hidden. That's why we should pre-compute the truncated dimensions. Good point on the chart size, maybe retinaScale should also update the chart size:

// ...
const height = Math.trunc(chart.height * pixelRatio);
const width = Math.trunc(chart.width * pixelRatio);

chart.height = height / pixelRatio;
chart.width = width / pixelRatio;
canvas.height = height;
canvas.width = width;

// ...

if (canvas.style && (forceStyle || (!height && !width))) {
  canvas.style.height = `${chart.height}px`;
  canvas.style.width= `${chart.width}px`;
}

@Flupp
Copy link
Contributor Author

Flupp commented May 5, 2021

I updated the PR following @simonbrunel’s suggestions. Additionally, I changed the detection of the need for redrawing so redraw is only requested if the framebuffer parameters change.

etimberg
etimberg previously approved these changes May 5, 2021
kurkle
kurkle previously approved these changes May 6, 2021
@etimberg
Copy link
Member

etimberg commented May 7, 2021

@simonbrunel would you like to review this as well?

Copy link
Member

@simonbrunel simonbrunel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@etimberg sorry, I missed the update. A few trivial comments (feel free to ignore if you want this PR merged promptly).

src/helpers/helpers.dom.js Outdated Show resolved Hide resolved
src/helpers/helpers.dom.js Outdated Show resolved Hide resolved
src/helpers/helpers.dom.js Outdated Show resolved Hide resolved
When canvas.height and canvas.width are set, the numbers are rounded to
integers. The same rounding must be applied to canvas.style.height and
canvas.style.width to avoid scaling of the canvas which would lead to
blurring.

Acknowledging that canvas.height and canvas.width are integers, the
framebuffer is now only redrawn if those integer values change.
@kurkle kurkle merged commit c955ffa into chartjs:master May 8, 2021
@kurkle kurkle changed the title round canvas.style dimensions to avoid blurring Round canvas.style dimensions to avoid blurring May 8, 2021
@kurkle kurkle linked an issue May 8, 2021 that may be closed by this pull request
@kurkle kurkle linked an issue May 19, 2021 that may be closed by this pull request
@kurkle kurkle mentioned this pull request May 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Chart is blurry when put inside an element with "weird" width Blurry chart lines
4 participants