2025-11-21 22:55:02 +08:00
|
|
|
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 = [];
|
2026-04-06 23:07:42 +08:00
|
|
|
const midPoint = Math.floor(count + 1 / 2);
|
2025-11-21 22:55:02 +08:00
|
|
|
|
|
|
|
|
// 负值部分
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|