Введение в тепловые карты
Данные о местоположении являются важным информационным ресурсом, соединяющим онлайн и офлайн, а эффективное представление данных с помощью графических средств на интерфейсе является важным средством анализа данных. Исходя из этого, мы разработали компонент визуализации данных на основе карты и добавили его в 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}
];
Принцип реализации
Давайте поработаем в обратном направлении от результатов о том, как мы должны реализовать тепловую карту.
Мы интуитивно чувствуем:- На тепловой карте каждая точка данных представляет собой круг, заполненный радиальным градиентом (так называемый радиальный градиент означает, что центр круга постепенно изменяется с увеличением радиуса), и этот круг градиента показывает, что данные изменяются. сильным ослабленным радиационным эффектом
- Два круга могут быть наложены друг на друга, и это линейная суперпозиция, и ее суть заключается в суперпозиции силы данных.
- Значения силы данных и цвета отображаются одно за другим, обычно показывая линейный градиент красного, сильного и синего.Конечно, вы также можете создать свой собственный спектр интенсивности
Согласно нашей интуиции, нам нужно сделать следующее:
- Сопоставьте все данные с кругом
- Выберите линейное измерение, чтобы указать силу данных, и цвет будет градиентным, и заполните круг в соответствии с этим измерением.
- накладывать круги
- Картирование цвета со спектром интенсивности
Следует отметить в приведенных выше шагах, что на шаге 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();
});
в примереmin
0,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
Если используются координаты с плавающей запятой, браузер выполнит дополнительные вычисления для рендеринга субпикселей, чтобы добиться эффекта сглаживания. Поэтому попробуйте использовать целочисленные координаты.