This commit is contained in:
Vincent van der Wal
2026-07-19 16:13:51 +02:00
parent e031716ce6
commit baa4840b38
26 changed files with 1077 additions and 702 deletions
-102
View File
@@ -1,102 +0,0 @@
export default [
'#800080',
'#800083',
'#800087',
'#7f008a',
'#7f008d',
'#7e0090',
'#7d0094',
'#7c0097',
'#7a009a',
'#79009d',
'#7700a1',
'#7600a4',
'#7400a7',
'#7200aa',
'#6f00ae',
'#6d00b1',
'#6a00b4',
'#6700b7',
'#6400bb',
'#6100be',
'#5e00c1',
'#5b00c4',
'#5700c8',
'#5300cb',
'#4f00ce',
'#4b00d1',
'#4700d5',
'#4200d8',
'#3e00db',
'#3900de',
'#3400e2',
'#2f00e5',
'#2a00e8',
'#2400eb',
'#1f00ef',
'#1900f2',
'#1300f5',
'#0d00f8',
'#0600fc',
'#0000ff',
'#0000ff',
'#0021f7',
'#003fee',
'#005ce6',
'#0076dd',
'#008ed5',
'#00a3cc',
'#00b7c4',
'#00bbaf',
'#00b38f',
'#00aa72',
'#00a256',
'#00993d',
'#009127',
'#008812',
'#008000',
'#008000',
'#118c00',
'#259700',
'#3ca300',
'#56ae00',
'#72ba00',
'#92c500',
'#b4d100',
'#d9dc00',
'#e8cf00',
'#f3bb00',
'#ffa500',
'#ffa500',
'#ff9800',
'#ff8c00',
'#ff7f00',
'#ff7200',
'#ff6600',
'#ff5900',
'#ff4c00',
'#ff3f00',
'#ff3300',
'#ff2600',
'#ff1900',
'#ff0d00',
'#ff0000',
'#ff0000',
'#f8000f',
'#f0001c',
'#e90029',
'#e10035',
'#da0040',
'#d2004a',
'#cb0053',
'#c3005c',
'#bc0063',
'#b4006a',
'#ad0070',
'#a50075',
'#9e0079',
'#96007c',
'#8f007e',
'#870080',
'#800080'
];
+100
View File
@@ -0,0 +1,100 @@
/**
* Temperature color scale ported from the open-meteo/weather-map-layer
* project (src/utils/color-scales.ts) so tables and maps share the same
* color language. Values between breakpoints are linearly interpolated.
*/
export type RGBA = [number, number, number, number];
export interface BreakpointScale {
unit: string;
breakpoints: number[];
colors: RGBA[];
}
export const temperatureScale: BreakpointScale = {
unit: '°C',
breakpoints: [
-80, -65, -50, -40, -32, -28, -24, -20, -17.5, -15, -12.5, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8,
10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50
],
colors: [
[74, 13, 0, 1],
[130, 1, 29, 1],
[185, 4, 114, 1],
[221, 6, 193, 1],
[207, 6, 241, 1],
[163, 5, 243, 1],
[118, 4, 246, 1],
[71, 3, 249, 1],
[41, 2, 250, 1],
[11, 1, 252, 1],
[1, 22, 253, 1],
[0, 52, 255, 1],
[35, 99, 251, 1],
[69, 139, 247, 1],
[102, 171, 245, 1],
[134, 197, 245, 1],
[114, 232, 165, 1],
[78, 232, 133, 1],
[40, 233, 96, 1],
[17, 220, 61, 1],
[9, 191, 36, 1],
[4, 160, 15, 1],
[0, 128, 0, 1],
[40, 160, 0, 1],
[96, 192, 0, 1],
[167, 223, 0, 1],
[255, 255, 0, 1],
[255, 237, 0, 1],
[255, 219, 0, 1],
[255, 201, 0, 1],
[255, 183, 0, 1],
[255, 165, 0, 1],
[255, 138, 0, 1],
[255, 110, 0, 1],
[255, 83, 0, 1],
[255, 55, 0, 1],
[255, 27, 0, 1],
[255, 0, 0, 1],
[228, 0, 10, 1],
[201, 0, 18, 1],
[174, 0, 23, 1],
[147, 0, 26, 1]
]
};
const lerp = (a: number, b: number, t: number): number => a + (b - a) * t;
/** Linearly interpolated color for `value` on a breakpoint scale. */
export const sampleScale = (breakpoints: number[], colors: RGBA[], value: number): RGBA => {
if (!Number.isFinite(value) || value <= breakpoints[0]) return colors[0];
const last = breakpoints.length - 1;
if (value >= breakpoints[last]) return colors[last];
let i = 0;
while (value > breakpoints[i + 1]) i++;
const t = (value - breakpoints[i]) / (breakpoints[i + 1] - breakpoints[i]);
const [r1, g1, b1, a1] = colors[i];
const [r2, g2, b2, a2] = colors[i + 1];
return [
Math.round(lerp(r1, r2, t)),
Math.round(lerp(g1, g2, t)),
Math.round(lerp(b1, b2, t)),
lerp(a1, a2, t)
];
};
export const rgbaCss = ([r, g, b, a]: RGBA): string =>
`rgba(${r}, ${g}, ${b}, ${Math.round(a * 1000) / 1000})`;
/**
* Whether text over `color` should be white, after alpha-compositing the
* color onto the page background (light or dark).
*/
export const needsWhiteText = ([r, g, b, a]: RGBA, dark = false): boolean => {
const base = dark ? 26 : 255;
const cr = r * a + base * (1 - a);
const cg = g * a + base * (1 - a);
const cb = b * a + base * (1 - a);
return cr * 0.299 + cg * 0.587 + cb * 0.114 <= 150;
};
+9 -71
View File
@@ -1,57 +1,13 @@
import colorScaleHex from './color-scale-hex';
import { type RGBA, needsWhiteText, rgbaCss, sampleScale, temperatureScale } from './color-scales';
const componentFromStr = (numStr: string, percent: number) => {
const num = Math.max(0, parseInt(numStr, 10));
return percent ? Math.floor((255 * Math.min(100, num)) / 100) : Math.min(255, num);
};
const toCelsius = (temperature: number, unit: string): number =>
unit === 'celsius' ? temperature : ((temperature - 32) * 5) / 9;
export const rgbToHex = (rgb: string): string => {
const rgbRegex = /^rgb\(\s*(-?\d+)(%?)\s*,\s*(-?\d+)(%?)\s*,\s*(-?\d+)(%?)\s*\)$/;
let result,
r,
g,
b,
hex = '';
if ((result = rgbRegex.exec(rgb))) {
r = componentFromStr(result[1], Number(result[2]));
g = componentFromStr(result[3], Number(result[4]));
b = componentFromStr(result[5], Number(result[6]));
export const getTempColor = (temperature: number, unit = 'celsius'): RGBA =>
sampleScale(temperatureScale.breakpoints, temperatureScale.colors, toCelsius(temperature, unit));
hex = (0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
if (!rgb) {
return '355522';
}
return hex;
};
export const hexToRgb = (hex: string): [number, number, number] => {
const h = hex.replace('#', '');
return [
parseInt(h.substring(0, 2), 16),
parseInt(h.substring(2, 4), 16),
parseInt(h.substring(4, 6), 16)
];
};
export const getColor = (temperature: number, unit = 'celsius'): string => {
if (unit !== 'celsius') {
temperature = Math.round(((temperature - 32) * 5) / 9);
}
let index: number;
if (temperature <= -40) {
index = 0;
} else if (temperature >= 60) {
index = colorScaleHex.length - 1;
} else {
// clamp: the scale has exactly 100 entries (-45..54), temperatures in
// [55, 60) would otherwise index past the end
index = Math.min(colorScaleHex.length - 1, Math.max(0, Math.round(temperature) + 45));
}
return colorScaleHex[index];
};
export const getColor = (temperature: number, unit = 'celsius'): string =>
rgbaCss(getTempColor(temperature, unit));
export interface TempStyle {
bg: string;
@@ -59,24 +15,6 @@ export interface TempStyle {
}
export const getTempStyle = (temp: number, unit: string): TempStyle => {
const bg = getColor(temp, unit);
const fg = textWhite(hexToRgb(bg)) ? 'white' : 'black';
return { bg, fg };
};
export const textWhite = (
[r, g, b, a]: [number, number, number, number] | [number, number, number],
dark?: boolean,
globalOpacity?: number
): boolean => {
const alpha = ((a || 1) * (globalOpacity || 100)) / 100;
if (alpha < 0.65) {
if (dark) {
return true;
} else {
return false;
}
}
// check luminance
return r * 0.299 + g * 0.587 + b * 0.114 <= 150;
const rgba = getTempColor(temp, unit);
return { bg: rgbaCss(rgba), fg: needsWhiteText(rgba) ? 'white' : 'black' };
};