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
JavaScript
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;
}
};
}