UNPKG

heatmap-cluster

Version:
339 lines (285 loc) 14.2 kB
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')}`; } /** * Creates a color scale function that maps numeric values to colors. * Supports linear interpolation between multiple color stops and quantization. * @param minValue 数值范围最小值 * @param maxValue 数值范围最大值 * @param colors 颜色数组,表示渐变的颜色停靠点 (HEX 或颜色名称). At least two colors for a gradient. * @param numColors 期望的离散颜色数量 ( quantize 标度的值域大小). If > 1, creates a quantized scale. If 1, returns a single color. * @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); // Create a continuous interpolator based on the provided color stops // This interpolator maps a factor [0, 1] to a color along the multi-stop gradient. const multiStopInterpolator = (factor: number): string => { // Ensure factor is within [0, 1] factor = Math.max(0, Math.min(1, factor)); // Find which color segment the factor falls into const segment = factor * (hexColors.length - 1); const segmentIndex = Math.floor(segment); const segmentFactor = segment - segmentIndex; // Position within the segment [0, 1] // Handle the case where factor is exactly 1 if (factor === 1) { return hexColors[hexColors.length - 1]; } // Get the start and end colors for the segment const startColorIndex = Math.max(0, Math.min(hexColors.length - 2, segmentIndex)); const endColorIndex = startColorIndex + 1; const startColor = hexColors[startColorIndex]; const endColor = hexColors[endColorIndex]; return interpolateColor(startColor, endColor, segmentFactor); }; // If numColors is 1, return a scale that always returns the middle color of the multi-stop gradient if (numColors === 1) { const middleColor = multiStopInterpolator(0.5); return (value: number): string => middleColor; } // Generate numColors discrete colors from the continuous interpolator const colorRange: string[] = []; for (let i = 0; i < numColors; i++) { const factor = i / (numColors - 1); colorRange.push(multiStopInterpolator(factor)); } console.log("Generated colorRange:", colorRange); // Return a quantize-like color scale function return (value: number): string => { // Handle boundary cases if (value <= minValue) return colorRange[0]; if (value >= maxValue) return colorRange[colorRange.length - 1]; // If min and max are the same, return the first color in the quantized range if (minValue === maxValue) { return colorRange[0]; } // Calculate value's position in the domain [minValue, maxValue] const proportion = (value - minValue) / (maxValue - minValue); // Determine which of the numColors bins the value falls into // Map proportion from [0, 1] to [0, numColors - epsilon] const binIndex = Math.floor(proportion * numColors); // Ensure index is within bounds [0, numColors - 1] const colorIndex = Math.max(0, Math.min(numColors - 1, binIndex)); console.log(`Value: ${value}, Proportion: ${proportion.toFixed(2)}, Bin Index: ${binIndex}, Color Index: ${colorIndex}, Color: ${colorRange[colorIndex]}`); return colorRange[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]; console.log("Using manual valueRange:", [minValue, maxValue]); } else { // 如果没有手动指定范围,则从数据中计算 if (allNumericValues.length === 0) { // 如果没有数值数据且没有手动指定范围,则默认范围为 [0, 0] minValue = 0; maxValue = 0; } else { minValue = Math.min(...allNumericValues); maxValue = Math.max(...allNumericValues); } console.log("Using calculated valueRange:", [minValue, maxValue]); } // 如果没有数值数据,或者手动指定范围后数据都在范围外,则对任何数字应用默认颜色 // 默认颜色使用颜色数组的第一个颜色 (如果颜色数组不为空) if (allNumericValues.length === 0 && !(valueRange && valueRange.length === 2)) { const defaultColor = colors.length > 0 ? colorToHex(colors[0]) : '#000000'; // 默认黑色 console.log("No numeric data and no manual range, using default color:", defaultColor); 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 对象 return 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; }); }); } // // 示例用法 (您可以使用这些示例来测试) // // 示例 1: 使用颜色数组和自动计算范围 // const sampleData1 = [ // [10, 20, 30], // [40, 50, 60], // [70, 80, 90] // ]; // const colors1 = ['#ffffcc', '#a1dab4', '#41b6c4', '#2c7fb8', '#253494']; // 5个颜色 // const colorValues1 = 5; // 期望 5 个离散颜色 // console.log("\n--- 示例 1 (自动范围) ---"); // const processedData1 = preprocessValues(sampleData1, colors1, colorValues1); // // console.log("示例 1 结果:", processedData1); // // 示例 2: 使用颜色数组和手动指定范围 (您的场景) // const sampleData2 = [ // [ 100, 90 ], // 简化您的数据结构,只包含数值 // [ 1000, 0 ] // ]; // 包含 1000 和 90 // const colors2 = ['blue', 'red', 'yellow']; // 3个颜色 // const colorValues2 = 3; // 期望 3 个离散颜色 // const manualValueRange: [number, number] = [0, 100]; // 手动指定范围 0 到 100 // console.log("\n--- 示例 2 (手动范围 - 您的场景) ---"); // const processedData2 = preprocessValues(sampleData2, colors2, colorValues2, manualValueRange); // // console.log("示例 2 结果:", processedData2); // // 示例 3: 只有一种颜色 // const sampleData3 = [ // [1, 2], [3, 4] // ]; // const colors3 = ['red']; // const colorValues3 = 3; // 期望 3 个离散颜色,但只提供了一种颜色 // console.log("\n--- 示例 3 (只有一种颜色) ---"); // const processedData3 = preprocessValues(sampleData3, colors3, colorValues3); // // console.log("示例 3 结果:", processedData3); // // 示例 4: 没有数值数据,有手动范围 // const sampleData4: (number | HeatmapValue)[][] = [ // [ { label: 'A' }, { label: 'B' } ] // ]; // const colors4 = ['green', 'purple']; // const colorValues4 = 2; // const manualValueRange4: [number, number] = [0, 100]; // console.log("\n--- 示例 4 (没有数值数据,有手动范围) ---"); // const processedData4 = preprocessValues(sampleData4, colors4, colorValues4, manualValueRange4); // // console.log("示例 4 结果:", processedData4); // // 示例 5: 没有数值数据,没有手动范围 // const sampleData5: (number | HeatmapValue)[][] = [ // [ { label: 'C' }, { label: 'D' } ] // ]; // const colors5 = ['orange', 'brown']; // const colorValues5 = 2; // // valueRange 默认为空数组 // console.log("\n--- 示例 5 (没有数值数据,没有手动范围) ---"); // const processedData5 = preprocessValues(sampleData5, colors5, colorValues5); // // console.log("示例 5 结果:", processedData5);