Utility functions for analyzing colors including contrast calculation, color difference measurement, validation, and data analysis for scale creation.
Calculate contrast ratios between colors for accessibility and readability assessment.
/**
* Calculate WCAG contrast ratio between two colors
* @param color1 - First color (foreground)
* @param color2 - Second color (background)
* @returns Contrast ratio (1-21, higher is better contrast)
*/
function contrast(color1: Color | string, color2: Color | string): number;WCAG Guidelines:
Usage Examples:
import chroma, { contrast } from "chroma-js";
// Basic contrast calculation
const textColor = '#000000';
const bgColor = '#FFFFFF';
console.log(contrast(textColor, bgColor)); // 21 (maximum contrast)
// Test different combinations
const darkGray = '#333333';
const lightGray = '#CCCCCC';
console.log(contrast(darkGray, lightGray)); // ~9.74
// Accessibility testing
function isAccessible(fg, bg, level = 'AA', size = 'normal') {
const ratio = contrast(fg, bg);
const requirements = {
'AA': { normal: 4.5, large: 3.0 },
'AAA': { normal: 7.0, large: 4.5 }
};
return ratio >= requirements[level][size];
}
console.log(isAccessible('#333', '#fff')); // true (AA normal)
console.log(isAccessible('#666', '#999')); // false (AA normal)
// Find accessible color combinations
const background = '#2E86AB';
const colors = ['#ffffff', '#f5f5f5', '#333333', '#000000'];
const accessible = colors.filter(color =>
isAccessible(color, background)
);Advanced Perceptual Contrast Algorithm for more accurate contrast prediction.
/**
* Calculate APCA contrast prediction
* @param text - Text color (foreground)
* @param background - Background color
* @returns APCA contrast value (-108 to 106)
*/
function contrastAPCA(text: Color | string, background: Color | string): number;APCA Guidelines:
Usage Examples:
import chroma, { contrastAPCA } from "chroma-js";
// APCA contrast calculation
const textColor = '#000000';
const bgColor = '#FFFFFF';
console.log(contrastAPCA(textColor, bgColor)); // ~106 (maximum)
// Compare with WCAG contrast
const fg = '#666666';
const bg = '#FFFFFF';
console.log(`WCAG: ${contrast(fg, bg)}`); // ~5.74
console.log(`APCA: ${contrastAPCA(fg, bg)}`); // ~68.54
// APCA accessibility testing
function isAPCAAccessible(text, bg, minContrast = 60) {
return Math.abs(contrastAPCA(text, bg)) >= minContrast;
}
// Test color combinations
const combinations = [
['#000000', '#FFFFFF'], // Black on white
['#333333', '#FFFFFF'], // Dark gray on white
['#666666', '#FFFFFF'], // Medium gray on white
['#999999', '#FFFFFF'] // Light gray on white
];
combinations.forEach(([text, bg]) => {
const apcaValue = contrastAPCA(text, bg);
const accessible = isAPCAAccessible(text, bg);
console.log(`${text} on ${bg}: ${apcaValue.toFixed(1)} (${accessible ? 'Pass' : 'Fail'})`);
});Measure perceptual color differences using Delta E algorithms.
/**
* Calculate Delta E color difference (CIE 2000 formula)
* @param color1 - First color
* @param color2 - Second color
* @param Kl - Lightness weighting factor (default: 1)
* @param Kc - Chroma weighting factor (default: 1)
* @param Kh - Hue weighting factor (default: 1)
* @returns Delta E value (0 = identical, higher = more different)
*/
function deltaE(color1: Color | string, color2: Color | string, Kl?: number, Kc?: number, Kh?: number): number;Delta E Interpretation:
Usage Examples:
import chroma, { deltaE } from "chroma-js";
// Basic color difference
const red1 = '#FF0000';
const red2 = '#FE0000';
console.log(deltaE(red1, red2)); // Very small difference
const red = '#FF0000';
const blue = '#0000FF';
console.log(deltaE(red, blue)); // Large difference (~76)
// Compare similar colors
const colors = ['#FF0000', '#FF1010', '#FF2020', '#FF3030'];
for (let i = 1; i < colors.length; i++) {
const diff = deltaE(colors[0], colors[i]);
console.log(`${colors[0]} vs ${colors[i]}: ΔE = ${diff.toFixed(2)}`);
}
// Quality control for color matching
function isColorMatch(target, sample, tolerance = 2.0) {
return deltaE(target, sample) <= tolerance;
}
// Test color batch
const targetColor = '#FF4444';
const batch = ['#FF4445', '#FF4440', '#FF4450', '#FF4400'];
const matches = batch.filter(color => isColorMatch(targetColor, color));
// Weighted Delta E for specific applications
const printColor = deltaE('#FF0000', '#FE0505', 2, 1, 1); // Weight lightnessCalculate Euclidean distance between colors in various color spaces.
/**
* Calculate Euclidean distance between colors
* @param color1 - First color
* @param color2 - Second color
* @param mode - Color space for calculation (default: 'lab')
* @returns Distance value (0 = identical)
*/
function distance(color1: Color | string, color2: Color | string, mode?: string): number;Available modes:
lab - Lab color space (default, perceptually uniform)rgb - RGB color spacelch - LCH color spacehsl - HSL color spacecmyk - CMYK color spaceUsage Examples:
import chroma, { distance } from "chroma-js";
const red = '#FF0000';
const blue = '#0000FF';
// Distance in different color spaces
console.log(distance(red, blue, 'lab')); // Lab distance
console.log(distance(red, blue, 'rgb')); // RGB distance
console.log(distance(red, blue, 'lch')); // LCH distance
// Find closest color in palette
function findClosestColor(target, palette, mode = 'lab') {
let minDistance = Infinity;
let closest = null;
for (const color of palette) {
const dist = distance(target, color, mode);
if (dist < minDistance) {
minDistance = dist;
closest = color;
}
}
return { color: closest, distance: minDistance };
}
// Usage
const targetColor = '#FF6B35';
const palette = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF'];
const result = findClosestColor(targetColor, palette);
console.log(`Closest color: ${result.color} (distance: ${result.distance.toFixed(2)})`);
// Color clustering
function clusterColors(colors, maxDistance = 10, mode = 'lab') {
const clusters = [];
const used = new Set();
for (const color of colors) {
if (used.has(color)) continue;
const cluster = [color];
used.add(color);
for (const otherColor of colors) {
if (used.has(otherColor)) continue;
if (distance(color, otherColor, mode) <= maxDistance) {
cluster.push(otherColor);
used.add(otherColor);
}
}
clusters.push(cluster);
}
return clusters;
}Analyze numeric data to determine optimal scale parameters and class breaks.
/**
* Analyze numeric data for scale creation
* @param data - Array of numbers or array of objects
* @param key - Property key if data contains objects
* @returns Analysis result with statistical information
*/
function analyze(data: number[] | object[], key?: string): AnalysisResult;
interface AnalysisResult {
min: number;
max: number;
sum: number;
count: number;
domain: [number, number];
}
/**
* Generate class break values from analysis
* @param data - Analysis result from analyze()
* @param mode - Classification mode (default: 'equal')
* @param num - Number of classes (default: 7)
* @returns Array of break values
*/
function limits(data: AnalysisResult, mode?: string, num?: number): number[];Classification modes:
equal - Equal intervalsquantile - Quantile-based breakslogarithmic - Logarithmic scalingjenks - Natural breaks (Jenks optimization)Usage Examples:
import chroma, { analyze, limits } from "chroma-js";
// Analyze simple numeric data
const populationData = [1200, 3400, 5600, 12100, 18900, 25300, 45200];
const analysis = analyze(populationData);
console.log(analysis);
// {
// min: 1200,
// max: 45200,
// sum: 111800,
// count: 7,
// domain: [1200, 45200]
// }
// Generate class breaks
const equalBreaks = limits(analysis, 'equal', 5);
const quantileBreaks = limits(analysis, 'quantile', 5);
const logBreaks = limits(analysis, 'logarithmic', 5);
console.log('Equal intervals:', equalBreaks);
console.log('Quantile breaks:', quantileBreaks);
console.log('Log breaks:', logBreaks);
// Analyze object data
const cityData = [
{ name: 'NYC', population: 8419000, area: 783 },
{ name: 'LA', population: 3980000, area: 1302 },
{ name: 'Chicago', population: 2716000, area: 606 }
];
const popAnalysis = analyze(cityData, 'population');
const areaAnalysis = analyze(cityData, 'area');
// Create scales based on analysis
const popScale = chroma.scale(['lightblue', 'darkblue'])
.domain([popAnalysis.min, popAnalysis.max]);
const areaScale = chroma.scale(['lightgreen', 'darkgreen'])
.domain([areaAnalysis.min, areaAnalysis.max]);
// Use with choropleth mapping
function createChoroplethScale(data, property, colors, classes = 5) {
const analysis = analyze(data, property);
const breaks = limits(analysis, 'quantile', classes);
return chroma.scale(colors).classes(breaks);
}
const choropleth = createChoroplethScale(
cityData,
'population',
['#ffffcc', '#c7e9b4', '#7fcdbb', '#41b6c4', '#225ea8']
);Validate color inputs and check for valid color representations.
/**
* Check if arguments can create a valid color
* @param args - Arguments to test
* @returns True if arguments represent a valid color
*/
function valid(...args: any[]): boolean;Usage Examples:
import chroma, { valid } from "chroma-js";
// Test various color formats
console.log(valid('#FF0000')); // true
console.log(valid('red')); // true
console.log(valid(255, 0, 0)); // true
console.log(valid([255, 0, 0])); // true
console.log(valid('#GGG')); // false
console.log(valid('invalid')); // false
console.log(valid(300, 0, 0)); // false (out of range)
// Validate user input
function parseUserColor(input) {
if (valid(input)) {
return chroma(input);
} else {
throw new Error(`Invalid color: ${input}`);
}
}
// Batch validation
const colorInputs = ['#FF0000', 'blue', '#invalid', 'rgb(255,0,0)', 'badcolor'];
const validColors = colorInputs.filter(valid);
console.log('Valid colors:', validColors);
// Safe color creation
function safeColor(input, fallback = '#000000') {
return valid(input) ? chroma(input) : chroma(fallback);
}
// Form validation
function validateColorInput(input) {
if (!input) return { valid: false, error: 'Color is required' };
if (!valid(input)) return { valid: false, error: 'Invalid color format' };
return { valid: true, color: chroma(input) };
}
// API validation
function processColorData(data) {
const errors = [];
const colors = [];
data.forEach((item, index) => {
if (!valid(item.color)) {
errors.push(`Invalid color at index ${index}: ${item.color}`);
} else {
colors.push({
...item,
chromaColor: chroma(item.color)
});
}
});
return { colors, errors };
}Advanced statistical analysis of color sets.
// Advanced analysis functions (built using core functions)
/**
* Calculate color diversity in a palette
*/
function calculateColorDiversity(colors: (Color | string)[], mode?: string): number;
/**
* Find color outliers in a dataset
*/
function findColorOutliers(colors: (Color | string)[], threshold?: number): (Color | string)[];
/**
* Generate color statistics
*/
function getColorStatistics(colors: (Color | string)[]): ColorStatistics;
interface ColorStatistics {
averageColor: Color;
medianLightness: number;
saturationRange: [number, number];
hueDistribution: number[];
totalColors: number;
}Usage Examples:
// Color diversity calculation
function calculateColorDiversity(colors, mode = 'lab') {
let totalDistance = 0;
let comparisons = 0;
for (let i = 0; i < colors.length; i++) {
for (let j = i + 1; j < colors.length; j++) {
totalDistance += distance(colors[i], colors[j], mode);
comparisons++;
}
}
return comparisons > 0 ? totalDistance / comparisons : 0;
}
// Find outlier colors
function findColorOutliers(colors, threshold = 50) {
const outliers = [];
for (const color of colors) {
const distances = colors
.filter(c => c !== color)
.map(c => distance(color, c, 'lab'));
const avgDistance = distances.reduce((a, b) => a + b, 0) / distances.length;
if (avgDistance > threshold) {
outliers.push(color);
}
}
return outliers;
}
// Color statistics
function getColorStatistics(colors) {
const chromaColors = colors.map(c => chroma(c));
// Average color
const avgColor = average(chromaColors);
// Lightness statistics
const lightnesses = chromaColors.map(c => c.get('hsl.l'));
const medianLightness = lightnesses.sort()[Math.floor(lightnesses.length / 2)];
// Saturation range
const saturations = chromaColors.map(c => c.get('hsl.s'));
const saturationRange = [Math.min(...saturations), Math.max(...saturations)];
// Hue distribution (simplified)
const hues = chromaColors.map(c => c.get('hsl.h')).filter(h => !isNaN(h));
const hueDistribution = new Array(12).fill(0);
hues.forEach(h => {
const bucket = Math.floor(h / 30) % 12;
hueDistribution[bucket]++;
});
return {
averageColor: avgColor,
medianLightness,
saturationRange,
hueDistribution,
totalColors: colors.length
};
}
// Usage
const palette = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF'];
const diversity = calculateColorDiversity(palette);
const outliers = findColorOutliers(palette);
const stats = getColorStatistics(palette);
console.log(`Diversity: ${diversity.toFixed(2)}`);
console.log(`Outliers:`, outliers);
console.log(`Average color:`, stats.averageColor.hex());