heatmap-cluster
Version:
The Unipept visualisation library
262 lines (221 loc) • 10.6 kB
text/typescript
import HeatmapValue from "./HeatmapValue";
/**
* 将颜色名称或HEX转换为标准的6位HEX格式
* @param color 颜色名称或HEX值
* @returns 6位HEX颜色代码
*/
function colorToHex(color: string): string {
// 如果是HEX格式直接返回
if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color)) {
if (color.length === 4) { // 缩写格式如 #fff
return `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}`;
}
return color;
}
// 创建临时div元素来获取颜色值 (仅在浏览器环境中有效)
if (typeof document === 'undefined') {
// 在非浏览器环境 (如 Node.js) 可能需要其他方式或预定义的颜色映射
// 这里简化处理,如果不在浏览器环境且不是HEX,则抛错
throw new Error(`无法在当前环境解析颜色名称: ${color}`);
}
const tempDiv = document.createElement('div');
tempDiv.style.color = color;
document.body.appendChild(tempDiv);
// 获取计算后的颜色值
const computedColor = window.getComputedStyle(tempDiv).color;
document.body.removeChild(tempDiv);
// 解析rgb/rgba值
const rgbMatch = computedColor.match(/^rgb(a?)\((\d+),\s*(\d+),\s*(\d+)(?:,\s*\d+\.?\d*)?\)$/i);
if (rgbMatch) {
const r = parseInt(rgbMatch[2]).toString(16).padStart(2, '0');
const g = parseInt(rgbMatch[3]).toString(16).padStart(2, '0');
const b = parseInt(rgbMatch[4]).toString(16).padStart(2, '0');
return `#${r}${g}${b}`.toLowerCase();
}
throw new Error(`无法解析颜色: ${color}`);
}
/**
* 在两个HEX颜色之间插值
* @param startHex 起始颜色
* @param endHex 结束颜色
* @param factor 插值因子 (0-1)
* @returns 插值后的HEX颜色
*/
function interpolateColor(startHex: string, endHex: string, factor: number): string {
// 确保 factor 在 0 到 1 之间
factor = Math.max(0, Math.min(1, factor));
// 去除#号并解析RGB分量
const start = startHex.substring(1).match(/.{2}/g)!.map(c => parseInt(c, 16));
const end = endHex.substring(1).match(/.{2}/g)!.map(c => parseInt(c, 16));
// 计算插值
const r = Math.round(start[0] + factor * (end[0] - start[0]));
const g = Math.round(start[1] + factor * (end[1] - start[1]));
const b = Math.round(start[2] + factor * (end[2] - start[2]));
// 返回HEX格式
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}
/**
* 创建一个将数值映射到离散颜色梯度的函数 (类似 d3.scaleQuantize)
* @param minValue 数值范围最小值
* @param maxValue 数值范围最大值
* @param colors 颜色数组,表示渐变的颜色停靠点 (HEX 或颜色名称)
* @param numColors 期望的离散颜色数量 ( quantize 标度的值域大小)
* @returns 一个函数,接收一个数值并返回对应的颜色字符串
*/
function createQuantizeColorScale(
minValue: number,
maxValue: number,
colors: string[], // 颜色数组
numColors: number // 期望的离散颜色数量
): (value: number) => string {
// 确保颜色数组至少有两个颜色才能形成渐变,否则使用第一个颜色
if (colors.length < 2) {
const singleColorHex = colors.length > 0 ? colorToHex(colors[0]) : '#000000'; // 默认黑色
// 如果颜色少于2个,或者 numColors 为 1,都只返回一个颜色
return (value: number): string => singleColorHex;
}
// 确保 numColors 是正数
if (numColors <= 0) {
numColors = 1;
}
// 将所有颜色转换为HEX格式
const hexColors = colors.map(colorToHex);
// 生成 numColors 个离散颜色作为 quantize 标度的值域
const colorRange: string[] = [];
if (numColors === 1) {
// 如果只要求一种颜色,使用颜色数组的中间色
const middleColorIndex = Math.floor((hexColors.length - 1) / 2);
colorRange.push(hexColors[middleColorIndex]);
} else {
// 在提供的颜色之间进行插值,生成 numColors 个颜色
for (let i = 0; i < numColors; i++) {
// 计算在整个颜色范围内的比例 (0 到 1)
const globalFactor = i / (numColors - 1);
// 计算对应的颜色段
const colorSegment = globalFactor * (hexColors.length - 1);
const colorIndex = Math.floor(colorSegment);
const segmentFactor = colorSegment - colorIndex; // 在当前颜色段内的比例
// 确保索引不会超出范围
const startColorIndex = Math.max(0, Math.min(hexColors.length - 2, colorIndex));
const endColorIndex = startColorIndex + 1;
const startColor = hexColors[startColorIndex];
const endColor = hexColors[endColorIndex];
colorRange.push(interpolateColor(startColor, endColor, segmentFactor));
}
}
// 返回一个颜色比例尺函数
return (value: number): string => {
// 处理边界情况
if (value <= minValue) return colorRange[0];
if (value >= maxValue) return colorRange[colorRange.length - 1];
// 如果 min 和 max 相同,所有值都映射到第一个颜色 (或中间色如果 numColors === 1)
if (minValue === maxValue) {
return colorRange[0];
}
// 计算值在范围内的比例 (0 到 1)
const proportion = (value - minValue) / (maxValue - minValue);
// 计算对应的颜色段索引
const colorIndex = Math.floor(proportion * (colorRange.length - 1));
// 返回对应的颜色
// 确保索引不会超出范围
return colorRange[Math.max(0, Math.min(colorRange.length - 1, colorIndex))];
};
}
/**
* Preprocesses input data to extract numeric values, determine color scale,
* and map each numeric value to a color based on the scale.
* @param data Input data array (can contain numbers or HeatmapValue objects).
* @param colors An array of colors defining the gradient stops. At least two colors are recommended.
* @param colorValues The number of discrete colors in the gradient.
* @param valueRange Optional: A tuple [minValue, maxValue] to manually specify the value range for color mapping. If not provided, the range is calculated from the data.
* @returns A 2D array of HeatmapValue objects with colors assigned.
*/
export function preprocessValues(
data: (number | HeatmapValue)[][],
colors: string[], // 颜色数组
colorValues: number, // 期望的离散颜色数量
valueRange: number[] = [] // 手动指定数值范围,默认为空数组
): HeatmapValue[][] {
// 确保 colorValues 是正数
if (colorValues <= 0) {
colorValues = 1; // 至少为1
}
const allNumericValues: number[] = [];
data.forEach(row => {
row.forEach(cell => {
if (typeof cell === "number") {
allNumericValues.push(cell);
} else if (cell && typeof cell.value === 'number') { // cell 是一个包含 value 属性的 HeatmapValue 对象
allNumericValues.push(cell.value);
}
});
});
let minValue: number;
let maxValue: number;
// 根据 valueRange 参数确定数值范围
if (valueRange && valueRange.length === 2 && typeof valueRange[0] === 'number' && typeof valueRange[1] === 'number') {
minValue = valueRange[0];
maxValue = valueRange[1];
} else {
minValue = Math.min(...allNumericValues);
maxValue = Math.max(...allNumericValues);
// // 如果没有手动指定范围,则从数据中计算
// if (allNumericValues.length === 0) {
// // 如果没有数值数据且没有手动指定范围,则默认范围为 [0, 0]
// minValue = 0;
// maxValue = 0;
// } else {
// }
}
console.log('min:',minValue,'max',maxValue);
// 如果没有数值数据,或者手动指定范围后数据都在范围外,则对任何数字应用默认颜色
// 默认颜色使用颜色数组的第一个颜色 (如果颜色数组不为空)
if (allNumericValues.length === 0 && !(valueRange && valueRange.length === 2)) {
const defaultColor = colors.length > 0 ? colorToHex(colors[0]) : '#000000'; // 默认黑色
return data.map((row, rowIdx) => row.map((cell, colIdx) => {
if (typeof cell === "number") {
return {
value: cell,
rowId: rowIdx,
columnId: colIdx,
color: defaultColor
};
}
// 如果 cell 不是数字,保持原样 (假设已经是 HeatmapValue 或其他类型)
return cell as HeatmapValue;
}));
}
// 创建颜色比例尺函数
const colorScale = createQuantizeColorScale(
minValue,
maxValue,
colors, // 使用颜色数组
colorValues
);
// 映射数据到 HeatmapValue 对象
const datas = data.map((row, rowIdx) => {
return row.map((cell, colIdx) => {
if (typeof cell === "number") {
return {
value: cell,
rowId: rowIdx,
columnId: colIdx,
color: colorScale(cell) // 使用新的颜色比例尺
};
} else if (cell && typeof cell.value === 'number') {
// 如果已经是 HeatmapValue 对象并且有数值 value,则重新计算其颜色
// 同时保留可能已存在的 rowId 和 columnId
return {
...cell,
rowId: cell.rowId !== undefined ? cell.rowId : rowIdx,
columnId: cell.columnId !== undefined ? cell.columnId : colIdx,
color: colorScale(cell.value) // 使用新的颜色比例尺
};
}
// 如果 cell 不是数字,也不是包含数字 value 的 HeatmapValue 对象,则原样返回
return cell as HeatmapValue;
});
});
console.log(`datas ===========>`,datas);
return datas
}