Hoang Nguyen
Hoang Nguyen

Reputation: 1

Create a heatmap using Canvas QML

I'm trying to create a heatmap using canvas QML but it doesn't work correctly.

I based it on the heatmap tutorial using JS HTML5 canvas and converted it to QML code. The link is hear: http://zhangwenli.com/blog/2015/06/12/drawing-heatmap-with-html-canvas.

But the heatmap is drawn by messy rectangles stacked on top of each other. I don't know how to make it smoother

Result

Expected result

Code:

import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    visible: true
    width: 800
    height: 600

    Canvas {
        id: mainCanvas
        width: 800
        height: 600

        property var data: []

        onPaint: {
            var ctx = mainCanvas.getContext("2d");
            var brushSize = 10;
            var brushBlurSize = 10;
            var r = brushSize + brushBlurSize;
            var d = r * 2;

            var len = data.length;
            for (var i = 0; i < len; ++i) {
                var p = data[i];
                var x = p[0];
                var y = p[1];
                var alpha = p[2];

                ctx.globalAlpha = alpha;
                ctx.drawImage(brushCanvas, x - r, y - r);

                var gradient = gradientCanvas.getContext("2d").getImageData(0, 0, 1, 256).data;
                var colorOffset = Math.floor(alpha * 255);
                var red = gradient[colorOffset * 4];
                var green = gradient[colorOffset * 4 + 1];
                var blue = gradient[colorOffset * 4 + 2];

                //Set fill color from gradient
                ctx.fillStyle = "rgb(" + red + "," + green + "," + blue + ")";
                ctx.fillRect(x - r, y - r, d, d);
            }
        }

        Component.onCompleted: {
            // Generate random data with alpha values
            for (var i = 0; i < 1000; ++i) {
                data.push([Math.random() * 400, Math.random() * 300, Math.random()]);
            }
            for (var i = 0; i < 100; ++i) {
                data.push([Math.random() * 20 + i / 2 + 100,
                    Math.random() * 20 + 200, Math.random()]);
            }
            for (var i = 0; i < 100; ++i) {
                data.push([Math.random() * 20 + i / 2 + 300,
                    Math.random() * 20 - i / 3 + 200, Math.random()]);
            }
            mainCanvas.requestPaint();
        }
    }


    Canvas {
        id: brushCanvas
        width: 40
        height: 40

        onPaint: {
            var ctx = brushCanvas.getContext("2d");

            var brushSize = 10;
            var brushBlurSize = 10;
            var r = brushSize + brushBlurSize;
            var d = r * 2;

            brushCanvas.width = d;
            brushCanvas.height = d;

            ctx.shadowOffsetX = d;
            ctx.shadowBlur = brushBlurSize;
            ctx.shadowColor = "black";

            ctx.beginPath();
            ctx.arc(-r, r, brushSize, 0, Math.PI * 2, true);
            ctx.closePath();
            ctx.fill();
        }
    }

    Canvas {
        id: gradientCanvas
        x: 100
        width: 1
        height: 256

        onPaint: {
            var ctx = gradientCanvas.getContext("2d");

            var levels = 256;

            var gradient = ctx.createLinearGradient(0, 0, 0, levels);

            var gradientColors = {
                0.4: 'blue',
                0.5: 'cyan',
                0.6: 'lime',
                0.8: 'yellow',
                1.0: 'red'
            };

            for (var pos in gradientColors) {
                gradient.addColorStop(pos, gradientColors[pos]);
            }

            ctx.fillStyle = gradient;
            ctx.fillRect(0, 0, gradientCanvas.width, gradientCanvas.height);

            var gradientPixels = ctx.getImageData(0, 0, 1, levels).data;
        }
    }
}

Upvotes: 0

Views: 81

Answers (1)

Stephen Quan
Stephen Quan

Reputation: 26214

I applied the following fixes to your algorithm:

  • Use Qt.hsla() to generate the heatmap where
    • h = 0.0 is red
    • h = 0.7 is blue
    • Therefore h = (1 - alpha) * 0.7 to convert alpha values to hsl values
  • Prepopulate a heatmap 2d array with 0.0
  • Update the heatmap 2d array by iterating your points
    • update the heatmap and take the maximum value

The code is in QML since that's your original asking question, but, at the same time, it is terribly inefficient.

import QtQuick
import QtQuick.Controls
Page {
   Canvas {
        id: mainCanvas
        width: 400
        height: 300
        anchors.centerIn: parent

        property var data: []
        property int brush: 10

        onPaint: {
            let ctx = mainCanvas.getContext("2d");

            let heatmap = new Array(height);
            for (let y = 0; y < height; ++y) {
                heatmap[y] = new Array(width);
                for (let x = 0; x < width; ++x) {
                    heatmap[y][x] = 0.0;
                }
            }

            let len = data.length;
            for (let i = 0; i < len; ++i) {
                let p = data[i];
                let x = Math.trunc(p[0]);
                let y = Math.trunc(p[1]);
                let alpha = p[2];
                for (let dy = -brush; dy <= brush; ++dy)
                {
                   for (let dx = -brush; dx <= brush; ++dx)
                   {
                       if (y + dy < 0 || y + dy >= height || x + dx < 0 || x + dx >= width) continue;
                       let d = Math.sqrt(dx * dx + dy * dy);
                       let dalpha = alpha - d / brush
                       if (dalpha > heatmap[y + dy][x + dx]) {
                           heatmap[y + dy][x + dx] = dalpha;
                       }
                   }
                }
            }

            for (let y = 0; y < height; y++) {
                for (let x = 0; x < width; x++) {
                    let alpha = heatmap[y][x];
                    let h = (1 - alpha) * 0.7;
                    ctx.fillStyle = Qt.hsla(h, 1.0, 0.5, 1.0);
                    ctx.fillRect(x, y, 1, 1);
                }
            }
        }

        Component.onCompleted: {
            // Generate random data with alpha values
            for (var i = 0; i < 1000; ++i) {
                data.push([Math.random() * 400, Math.random() * 300, Math.random()]);
            }
            for (var i = 0; i < 100; ++i) {
                data.push([Math.random() * 20 + i / 2 + 100,
                    Math.random() * 20 + 200, Math.random()]);
            }
            for (var i = 0; i < 100; ++i) {
                data.push([Math.random() * 20 + i / 2 + 300,
                    Math.random() * 20 - i / 3 + 200, Math.random()]);
            }
            mainCanvas.requestPaint();
        }
    }
}

You can Try it Online!

heatmap.png

Upvotes: 0

Related Questions