UNPKG

claritykit-svelte

Version:

A comprehensive Svelte component library focused on accessibility, ADHD-optimized design, developer experience, and full SSR compatibility

268 lines (267 loc) 7.49 kB
import { scaleLinear, scaleTime, scaleBand, scaleLog } from 'd3-scale'; import { extent, min } from 'd3-array'; import { timeFormat } from 'd3-time-format'; import { format as d3Format } from 'd3-format'; import { isBrowser, safelyAccessWindow } from '../../../utils/environment'; /** * Creates a scale based on the specified type */ export function createScale(type, domain, range, padding = 0.1) { switch (type) { case 'time': return scaleTime().domain(domain).range(range); case 'band': return scaleBand() .domain(domain) .range(range) .padding(padding); case 'log': return scaleLog() .domain(domain) .range(range) .nice(); case 'linear': default: return scaleLinear().domain(domain).range(range).nice(); } } /** * Gets the domain for a scale based on data and accessor */ export function getDomain(data, accessor, type = 'linear', padding = 0.1) { if (!data.length) return [0, 1]; if (type === 'band') { return Array.from(new Set(data.map(accessor))); } const [minVal, maxVal] = extent(data, accessor); if (type === 'time') { return [minVal, maxVal]; } if (type === 'log') { const minPositive = min(data, d => { const val = accessor(d); return val > 0 ? val : Infinity; }); return [ minVal >= 0 ? Math.max(1e-10, minVal) : minVal, maxVal || 1 ]; } // Add padding for linear scale const paddingAmount = (maxVal - minVal) * padding; return [ minVal - paddingAmount, maxVal + paddingAmount ]; } /** * Formats a value based on its type */ export function formatValue(value, type = 'number', format) { if (value == null) return ''; if (format) { if (type === 'time') { return timeFormat(format)(new Date(value)); } return d3Format(format)(value); } switch (type) { case 'time': return new Date(value).toLocaleDateString(); case 'percentage': return d3Format('.1%')(value); case 'currency': return d3Format('$,.2f')(value); case 'number': default: return d3Format(',')(value); } } /** * Gets a color from a color scale or array */ export function getColor(value, colors, index = 0) { if (typeof colors === 'function') { return colors(value); } return colors[index % colors.length]; } /** * Calculates the inner dimensions accounting for margins */ export function calculateInnerDimensions(width, height, margin) { return { width: Math.max(0, width - margin.left - margin.right), height: Math.max(0, height - margin.top - margin.bottom) }; } /** * Normalizes margin object */ export function normalizeMargin(margin) { if (typeof margin === 'number') { return { top: margin, right: margin, bottom: margin, left: margin }; } return { top: margin.top || 0, right: margin.right || 0, bottom: margin.bottom || 0, left: margin.left || 0 }; } /** * Gets the position for a tooltip */ export function getTooltipPosition(event, tooltip, offset = 10) { const { clientX: x, clientY: y } = event; const { width, height } = tooltip.getBoundingClientRect(); let left = x + offset; let top = y + offset; // Adjust if tooltip would go off screen (only in browser) if (isBrowser) { const viewport = safelyAccessWindow(() => ({ width: window.innerWidth, height: window.innerHeight }), { width: 1024, height: 768 }); if (viewport && left + width > viewport.width) { left = x - width - offset; } if (viewport && top + height > viewport.height) { top = y - height - offset; } } return { left, top }; } /** * Debounce function for resize and scroll events */ export function debounce(func, wait = 100) { let timeout = null; return function (...args) { if (timeout) clearTimeout(timeout); if (isBrowser) { timeout = setTimeout(() => func(...args), wait); } else { // Immediate execution on server func(...args); } }; } /** * Throttle function for scroll and resize events */ export function throttle(func, limit = 100) { let inThrottle = false; return function (...args) { if (!inThrottle) { func(...args); inThrottle = true; if (isBrowser) { setTimeout(() => (inThrottle = false), limit); } else { // Immediate reset on server inThrottle = false; } } }; } /** * Clamps a value between min and max */ export function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } /** * Calculates the angle in radians from one point to another */ export function angleBetweenPoints(x1, y1, x2, y2) { return Math.atan2(y2 - y1, x2 - x1); } /** * Calculates the distance between two points */ export function distanceBetweenPoints(x1, y1, x2, y2) { return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); } /** * Checks if a point is within a bounding box */ export function isPointInBounds(x, y, bounds) { return (x >= bounds.x && x <= bounds.x + bounds.width && y >= bounds.y && y <= bounds.y + bounds.height); } /** * Generates a unique ID */ export function generateId(prefix = '') { return `${prefix}-${Math.random().toString(36).substr(2, 9)}`; } /** * Merges default props with user props */ export function mergeProps(defaultProps, userProps) { return { ...defaultProps, ...userProps }; } /** * Gets the value at a given path in an object */ export function getValueAtPath(obj, path) { const pathArray = Array.isArray(path) ? path : path.split('.'); return pathArray.reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj); } /** * Deeply clones an object */ export function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } /** * Creates a memoized function */ export function memoize(fn, keyFn) { const cache = new Map(); return ((...args) => { const key = keyFn ? keyFn(...args) : JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } const result = fn(...args); cache.set(key, result); return result; }); } /** * Creates a formatter function for numbers */ export function createNumberFormatter(format = ',.2f', fallback = 'N/A') { const formatter = d3Format(format); return (value) => { if (value == null || isNaN(value)) return fallback; return formatter(value); }; } /** * Creates a date formatter function */ export function createDateFormatter(format = '%b %d, %Y', fallback = 'N/A') { const formatter = timeFormat(format); return (date) => { if (date == null) return fallback; try { const d = date instanceof Date ? date : new Date(date); return isNaN(d.getTime()) ? fallback : formatter(d); } catch (e) { return fallback; } }; }