Визуализация веб-данных — научит вас реализовывать тепловую карту

Canvas

Введение в тепловые карты

Данные о местоположении являются важным информационным ресурсом, соединяющим онлайн и офлайн, а эффективное представление данных с помощью графических средств на интерфейсе является важным средством анализа данных. Исходя из этого, мы разработали компонент визуализации данных на основе карты и добавили его в JSAPI в виде дополнительной библиотеки, которая на данный момент включает в себя в основном тепловую карту, точечную карту, карту области и карту миграции.

可视化组件

Тепловая карта — это тип визуализации, который выражает силу и размер данных, а также тенденцию их распределения в цвете.Как показано в верхнем левом углу рисунка выше, тепловые карты можно применять для анализа плотности населения, анализа активности и т. д. Данные, представляющие тепловую карту, в основном включают дискретные координатные точки и соответствующие значения прочности.

Реализация тепловой карты

подготовка данных

В этой статье рассматривается только базовая реализация тепловых карт.Используете ли вы их для карт, анализа фокуса веб-страницы или других сценариев, вам необходимо преобразовать координаты соответствующей сцены в двумерные координаты на холсте Canvas.Окончательные данные нам нужен следующий формат:

// x, y 表示二维坐标; value表示强弱值
var data = [
	{x: 471, y: 277, value: 25},
	{x: 438, y: 375, value: 97},
	{x: 373, y: 19, value: 71},
	{x: 473, y: 42, value: 63},
	{x: 463, y: 95, value: 97},
	{x: 590, y: 437, value: 34},
	{x: 377, y: 442, value: 66},
	{x: 171, y: 254, value: 20},
	{x: 6, y: 582, value: 64},
	{x: 387, y: 477, value: 14},
	{x: 300, y: 300, value: 80}
];

Принцип реализации

Давайте поработаем в обратном направлении от результатов о том, как мы должны реализовать тепловую карту.

热力图原理
Мы интуитивно чувствуем:

  1. На тепловой карте каждая точка данных представляет собой круг, заполненный радиальным градиентом (так называемый радиальный градиент означает, что центр круга постепенно изменяется с увеличением радиуса), и этот круг градиента показывает, что данные изменяются. сильным ослабленным радиационным эффектом
  2. Два круга могут быть наложены друг на друга, и это линейная суперпозиция, и ее суть заключается в суперпозиции силы данных.
  3. Значения силы данных и цвета отображаются одно за другим, обычно показывая линейный градиент красного, сильного и синего.Конечно, вы также можете создать свой собственный спектр интенсивности

Согласно нашей интуиции, нам нужно сделать следующее:

  1. Сопоставьте все данные с кругом
  2. Выберите линейное измерение, чтобы указать силу данных, и цвет будет градиентным, и заполните круг в соответствии с этим измерением.
  3. накладывать круги
  4. Картирование цвета со спектром интенсивности

Следует отметить в приведенных выше шагах, что на шаге 2 мы не заполняем круг непосредственно спектром интенсивности, потому что цвет, полученный таким образом, является 3-мерным и не является линейным при наложении. Эта статья выбираетalphaТо есть прозрачность цвета используется в качестве измерения для выражения силы, вы также можете выбратьrилиgИли другое, выбор объясню позжеalphaпреимущества.

руки вверх

нарисовать круг

Чтобы нарисовать дуги или окружности в Canvas, вы можете использоватьarc()метод:

arc(x, y, radius, startAngle, endAngle, anticlockwise)

xа такжеyКоординаты, соответствующие данным,radiusможно установить свободно,startAngleа такжеendAngleУказывает начальный и конечный углы соответственно0а также2 * Math.PI,anticlockwiseУказывает, против часовой стрелки или нет, и не может быть установлено.

Градиент

Может использоваться в холстеcanvasGradientОбъекты создают градиенты, разделенные на линейные градиентыcreateLinearGradient(x1, y1, x2, y2)и радиальные градиентыcreateRadialGradient(x1, y1, r1, x2, y2, r2), мы принимаем последнее. Чтобы создать цвет радиального градиента, вам нужно определить две окружности, а цвет градиента в области между двумя окружностями, поэтому мы устанавливаем центр двух окружностей в координатной точке данных, а радиус первой окружность равна 0, а радиус второй окружности равен 0. Это то же самое, что и радиус окружности, которую нам нужно нарисовать.

Тогда нам нужно пройтиaddColorStop(position, color)Определяет правило цветового градиента между двумя кругами. Эффект, которого мы хотим добиться, заключается в том, что значение цвета в определенном измерении постепенно уменьшается от центра по мере увеличения радиуса, и в то же время значение этого измерения совпадает со значением данных.valueПоложительная корреляция, иначе все точки данных будут отображаться одинаково. Итак, мы выбираемalphaв качестве вариационного измерения, так как мы можем использоватьglobalAlphaустановить глобальную прозрачность, которая аналогичнаvalueположительная корреляция, так что мы можем использовать его единообразноrgba(r,g,b,1)а такжеrgba(r,g,b,0)Цвет как центральная точка и край радиуса.

Затем мы реализуем два вышеуказанных шага с помощью следующего кода:

/*
 * radius: 绘制半径,请自行设置
 * min, max: 强弱阈值,可自行设置,也可取数据最小最大值
 */
data.forEach(point => {
    let {x, y, value} = point; 
    context.beginPath();
    context.arc(x, y, radius, 0, 2 * Math.PI);
    context.closePath();
    
    // 创建渐变色: r,g,b取值比较自由,我们只关注alpha的数值
    let radialGradient = context.createRadialGradient(x, y, 0, x, y, radius);
    radialGradient.addColorStop(0.0, "rgba(0,0,0,1)");
    radialGradient.addColorStop(1.0, "rgba(0,0,0,0)");
    context.fillStyle = radialGradient;

    // 设置globalAlpha: 需注意取值需规范在0-1之间
    let globalAlpha = (value - min) / (max - min);
    context.globalAlpha = Math.max(Math.min(globalAlpha, 1), 0);

    // 填充颜色
    context.fill();
});

в примереmin0,maxявляется максимальным значением данных, пока что график, который мы получили, выглядит следующим образом:

渐变圆形

цветовая карта

Видно, что прозрачность на рисунке уже может представлять силу данных и эффект излучения, а на пересечении выполняется линейная суперпозиция. Теперь мы собираемся раскрасить график, нам нужно использоватьImageDataОбъект выполняет манипуляции с пикселями на изображении, считывает прозрачность каждого пикселя, а затем перезаписывает его сопоставленным цветом.ImageDataчисленная величина.

Не спешите разбираться в том, как выполняются операции с пикселями, первое, что нам нужно определить, это взаимосвязь отображения между значениями прозрачности и цветами.ImageDataЗначение прозрачности представляет собой целое число между [0, 255], мы хотим создать дискретную функцию отображения, чтобы 0 соответствовал самому слабому цвету (светло-синий в примере, вы также можете установить его произвольно), 255 соответствует самый сильный цвет (положительный красный в примере). Процесс этого постепенного изменения не является одномерным приращением К счастью, у нас есть инструменты для решения проблемы постепенного изменения, то есть упомянутые вышеcreateLinearGradient(x1, y1, x2, y2).

调色盘

Как показано на изображении выше, мы можем создать линейный градиент с диапазоном 256 пикселей и заполнить им прямоугольник 256*1, что эквивалентно палитре. Пиксель в позиции (0, 0) на этой палитре показывает самый слабый цвет, а пиксель в позиции (255, 0) показывает самый сильный цвет, поэтому для прозрачности а цвет пикселя в позиции (а, 0) равен его цвет отображения. код показывает, как показано ниже:

const defaultColorStops = {
    0: "#0ff",
    0.2: "#0f0",
    0.4: "#ff0",
    1: "#f00",
};
const width = 20, height = 256;

function Palette(opts) {
    Object.assign(this, opts);
    this.init();
}

Palette.prototype.init = function() {
    let colorStops = this.colorStops || defaultColorStops;

    // 创建canvas
    let canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    let ctx = canvas.getContext("2d");

    // 创建线性渐变色
    let linearGradient = ctx.createLinearGradient(0, 0, 0, height);
    for (const key in colorStops) {
        linearGradient.addColorStop(key, colorStops[key]);
    }

    // 绘制渐变色条
    ctx.fillStyle = linearGradient;
    ctx.fillRect(0, 0, width, height);

    // 读取像素数据
    this.imageData = ctx.getImageData(0, 0, 1, height).data;
    this.canvas = canvas;
};

/**
 * 取色器
 * @param {Number} position 像素位置
 * @return {Array.<Number>} [r, g, b]
 */
Palette.prototype.colorPicker = function(position) {
    return this.imageData.slice(position * 4, position * 4 + 3);
};

затенение пикселей

Краткое введениеImageDataобъект, в котором хранятся фактические пиксельные данные объекта Canvas, в том числеwidth, height, dataтри свойства. Мы можем:

  • пройти черезcreateImageData(anotherImageData | width, height)создать новый объект
  • илиgetImageData(left, top, width, height)для создания объекта с пиксельными данными для определенной области холста Canvas
  • использоватьputImageData(myImageData, left, top)для записи пиксельных данных на холст Canvas

Исходя из этого, мы сначала получаем данные холста, просматриваем пиксели для чтения прозрачности, получаем цвет карты прозрачности, перезаписываем данные пикселей и, наконец, записываем на холст.

// 像素着色
let imageData = context.getImageData(0, 0, width, height);
let data = imageData.data;
for (var i = 3; i < data.length; i+=4) {
    let alpha = data[i];
    let color = palette.colorPicker(alpha);
    data[i - 3] = color[0];
    data[i - 2] = color[1];
    data[i - 1] = color[2];
}
context.putImageData(imageData, 0, 0);

Пока что мы завершили отрисовку тепловой карты, давайте посмотрим на эффект:

热力图

оптимизация производительности

закадровый рендеринг

Если мы представим тепловую карту на карте, координаты точек данных будут меняться по мере движения карты, но соответствующее круглое изображение на самом деле останется тем же. Таким образом, чтобы избежать многократного создания градиентов, установкаglobalAlpha, рисование и заливка цветами и т.д., мы можем предварительно нарисовать изображение каждой точки данных, сохранить его через Canvas, которого нет в документообороте, и передать при повторном рендерингеdrawImageНарисуйте его на холсте:

function Radiation(opts) {
    Object.assign(this, opts);
    this.init();
}

Radiation.prototype.init = function() {
    let {radius, globalAlpha} = this;

    // 创建canvas
    let canvas = document.createElement("canvas");
    canvas.width = canvas.height = radius * 2;
    
    // 获取上下文,初始化设置
    let ctx = canvas.getContext("2d");
    ctx.translate(radius, radius);
    ctx.globalAlpha = Math.max(Math.min(globalAlpha, 1), 0);
    
    // 创建径向渐变色:灰度由强到弱
    let radialGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, radius);
    radialGradient.addColorStop(0.0, "rgba(0,0,0,1)");
    radialGradient.addColorStop(1.0, "rgba(0,0,0,0)");
    ctx.fillStyle = radialGradient;
    
    // 画圆
    ctx.arc(0, 0, radius, 0, Math.PI * 2);
    ctx.fill();

    this.canvas = canvas;
};

Radiation.prototype.draw = function(context) {
    let {canvas, x, y, radius} = this;
    context.drawImage(canvas, x - radius, y - radius);
};

Обновление 2019.1.14: После тестирования производительности было обнаружено, что приведенное выше описание неверно. Внеэкранный рендеринг в основном используется в сценах, где процесс частичной отрисовки сложен, а часть отрисовывается многократно, при этом закадровый холст должен быть умеренного размера, потому что дублирование чрезмерно большого холста принесет много потеря производительности. Вышеупомянутый процесс локального рисования на самом деле очень прост, а прямое использованиеdrawImageВремя почти такое же, поэтому нет необходимости использовать внеэкранный рендеринг.

Избегайте координат с плавающей запятой

использоватьdrawImageЕсли используются координаты с плавающей запятой, браузер выполнит дополнительные вычисления для рендеринга субпикселей, чтобы добиться эффекта сглаживания. Поэтому попробуйте использовать целочисленные координаты.