diff --git a/src/utils/color-generator.js b/src/utils/color-generator.js new file mode 100644 index 0000000..8d7c566 --- /dev/null +++ b/src/utils/color-generator.js @@ -0,0 +1,185 @@ +export class ColorGenerator { + // 生成适合地图显示的区分度高的颜色 + static generateDistinctColors(count) { + const colors = []; + + for (let i = 0; i < count; i++) { + // 使用黄金角分布算法,确保颜色分布均匀 + const hue = (i * 137.5) % 360; // 黄金角约等于 137.5 度 + const saturation = 70 + Math.random() * 20; // 70-90% 饱和度 + const lightness = 45 + Math.random() * 20; // 45-65% 亮度 + + const hsl = { h: hue, s: saturation, l: lightness }; + colors.push(HSLColorGenerator.toCSS(hsl)); + } + + return colors; + } + + // 生成分类数据颜色(确保相邻类别颜色差异明显) + static generateCategoricalColors(count, startHue = 0) { + const hueStep = 360 / count; + const colors = []; + + for (let i = 0; i < count; i++) { + const hue = (startHue + i * hueStep) % 360; + const hsl = { + h: hue, + s: 80 + Math.random() * 15, // 80-95% 饱和度 + l: 50 + Math.random() * 15 // 50-65% 亮度 + }; + colors.push(HSLColorGenerator.toCSS(hsl)); + } + + return colors; + } + + // 生成连续数据颜色(渐变) + static generateSequentialColors( + count, + startColor = '#f7fbff', + endColor = '#08306b' + ) { + const colors = []; + + // 解析起始颜色和结束颜色 + const startRGB = this.parseHexColor(startColor); + const endRGB = this.parseHexColor(endColor); + + for (let i = 0; i < count; i++) { + const ratio = i / (count - 1); + const r = Math.round(startRGB.r + (endRGB.r - startRGB.r) * ratio); + const g = Math.round(startRGB.g + (endRGB.g - startRGB.g) * ratio); + const b = Math.round(startRGB.b + (endRGB.b - startRGB.b) * ratio); + + colors.push(`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`); + } + + return colors; + } + + // 生成发散颜色(用于有正负值的数据) + static generateDivergingColors( + count, + negativeColor = '#d73027', + neutralColor = '#ffffbf', + positiveColor = '#1a9850' + ) { + const colors = []; + const midPoint = Math.floor(count / 2); + + // 负值部分 + const negRGB = this.parseHexColor(negativeColor); + const neutralRGB = this.parseHexColor(neutralColor); + + for (let i = 0; i < midPoint; i++) { + const ratio = i / midPoint; + const r = Math.round(negRGB.r + (neutralRGB.r - negRGB.r) * ratio); + const g = Math.round(negRGB.g + (neutralRGB.g - negRGB.g) * ratio); + const b = Math.round(negRGB.b + (neutralRGB.b - negRGB.b) * ratio); + + colors.push(`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`); + } + + // 正值部分 + const positiveRGB = this.parseHexColor(positiveColor); + + for (let i = midPoint; i < count; i++) { + const ratio = (i - midPoint) / (count - midPoint - 1); + const r = Math.round(neutralRGB.r + (positiveRGB.r - neutralRGB.r) * ratio); + const g = Math.round(neutralRGB.g + (positiveRGB.g - neutralRGB.g) * ratio); + const b = Math.round(neutralRGB.b + (positiveRGB.b - neutralRGB.b) * ratio); + + colors.push(`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`); + } + + return colors; + } + + // 解析十六进制颜色 + static parseHexColor(hex) { + hex = hex.replace('#', ''); + + if (hex.length === 3) { + hex = hex.split('').map(c => c + c).join(''); + } + + return { + r: parseInt(hex.substring(0, 2), 16), + g: parseInt(hex.substring(2, 4), 16), + b: parseInt(hex.substring(4, 6), 16) + }; + } +} + +export class HSLColorGenerator { + // 随机 HSL 颜色 + static randomHSL() { + return { + h: Math.floor(Math.random() * 360), + s: Math.floor(Math.random() * 101), + l: Math.floor(Math.random() * 101) + }; + } + + // 转换为 CSS 字符串 + static toCSS(color) { + if (color.alpha !== undefined) { + return `hsla(${color.h}, ${color.s}%, ${color.l}%, ${color.alpha})`; + } + return `hsl(${color.h}, ${color.s}%, ${color.l}%)`; + } + + // 生成鲜艳的颜色(高饱和度) + static randomVibrantHSL(minSaturation = 80, minLightness = 40) { + return { + h: Math.floor(Math.random() * 360), + s: minSaturation + Math.floor(Math.random() * (101 - minSaturation)), + l: minLightness + Math.floor(Math.random() * (61 - minLightness)) + }; + } + + // 生成柔和的颜色(低饱和度) + static randomPastelHSL(maxSaturation = 40, minLightness = 70) { + return { + h: Math.floor(Math.random() * 360), + s: Math.floor(Math.random() * (maxSaturation + 1)), + l: minLightness + Math.floor(Math.random() * (101 - minLightness)) + }; + } + + // 转换为 RGB + static toRGB(hsl) { + const h = hsl.h / 360; + const s = hsl.s / 100; + const l = hsl.l / 100; + + let r, g, b; + + if (s === 0) { + r = g = b = l; + } else { + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255), + alpha: hsl.alpha + }; + } +} \ No newline at end of file