UNPKG

claritykit-svelte

Version:

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

717 lines (700 loc) 21.2 kB
// Performance optimization utilities for data visualizations in ClarityKit /** * Data virtualization for large datasets */ export class DataVirtualizer { constructor(chunkSize = 1000, maxCacheSize = 10) { Object.defineProperty(this, "data", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "chunkSize", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "visibleRange", { enumerable: true, configurable: true, writable: true, value: { start: 0, end: 0 } }); Object.defineProperty(this, "cache", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "maxCacheSize", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.chunkSize = chunkSize; this.maxCacheSize = maxCacheSize; } /** * Set the complete dataset */ setData(data) { this.data = data; this.cache.clear(); this.visibleRange = { start: 0, end: Math.min(this.chunkSize, data.length) }; } /** * Get virtualized data for current viewport */ getVirtualizedData(startIndex = 0, count = this.chunkSize) { const endIndex = Math.min(startIndex + count, this.data.length); const chunkKey = `${startIndex}-${endIndex}`; // Check cache first let chunk = this.cache.get(chunkKey); if (!chunk) { chunk = { id: chunkKey, startIndex, endIndex, data: this.data.slice(startIndex, endIndex) }; this.setCache(chunkKey, chunk); } return { visibleData: chunk.data, totalCount: this.data.length, startIndex, endIndex, hasMore: endIndex < this.data.length }; } /** * Update visible range (e.g., when scrolling) */ updateVisibleRange(start, end) { this.visibleRange = { start, end }; return this.getVirtualizedData(start, end - start); } /** * Preload chunks around current visible range */ preloadAdjacentChunks() { const { start, end } = this.visibleRange; const chunkSize = end - start; // Preload previous chunk if (start > 0) { const prevStart = Math.max(0, start - chunkSize); this.getVirtualizedData(prevStart, chunkSize); } // Preload next chunk if (end < this.data.length) { this.getVirtualizedData(end, chunkSize); } } setCache(key, chunk) { if (this.cache.size >= this.maxCacheSize) { // Remove oldest entry (simple LRU) const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(key, chunk); } /** * Clear cache and reset */ clear() { this.cache.clear(); this.data = []; this.visibleRange = { start: 0, end: 0 }; } /** * Get memory usage statistics */ getMemoryStats() { return { cacheSize: this.cache.size, dataSize: this.data.length, chunks: this.cache.size }; } } /** * Level of Detail (LOD) management for charts */ export class LevelOfDetailManager { constructor() { Object.defineProperty(this, "lodLevels", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "currentLod", { enumerable: true, configurable: true, writable: true, value: 0 }); } /** * Generate different levels of detail for data */ generateLOD(data, levels = 4) { this.lodLevels.clear(); // Level 0: Full data this.lodLevels.set(0, data); // Generate simplified versions for (let level = 1; level < levels; level++) { const simplificationFactor = Math.pow(2, level); const simplified = this.simplifyData(data, simplificationFactor); this.lodLevels.set(level, simplified); } } /** * Get data for appropriate LOD based on zoom level */ getDataForZoom(zoomLevel, dataCount) { // Determine appropriate LOD based on zoom and data density let targetLod = 0; if (dataCount > 10000 && zoomLevel < 0.1) targetLod = 3; else if (dataCount > 5000 && zoomLevel < 0.25) targetLod = 2; else if (dataCount > 1000 && zoomLevel < 0.5) targetLod = 1; this.currentLod = targetLod; return this.lodLevels.get(targetLod) || this.lodLevels.get(0) || []; } /** * Simplify data by reducing point count */ simplifyData(data, factor) { if (factor <= 1) return data; const simplified = []; const step = Math.max(1, Math.floor(factor)); for (let i = 0; i < data.length; i += step) { // For time series, average the values in the step range if (this.isTimeSeries(data)) { const chunk = data.slice(i, Math.min(i + step, data.length)); const averaged = this.averageChunk(chunk); simplified.push(averaged); } else { simplified.push(data[i]); } } return simplified; } isTimeSeries(data) { return data.length > 0 && typeof data[0] === 'object' && 'value' in data[0] && typeof data[0].value === 'number'; } averageChunk(chunk) { if (chunk.length === 0) return null; if (chunk.length === 1) return chunk[0]; const avgValue = chunk.reduce((sum, item) => sum + (item.value || 0), 0) / chunk.length; return { ...chunk[0], value: avgValue, _simplified: true, _originalCount: chunk.length }; } getCurrentLOD() { return this.currentLod; } getLODInfo() { const currentData = this.lodLevels.get(this.currentLod) || []; const totalData = this.lodLevels.get(0) || []; return { level: this.currentLod, pointCount: currentData.length, total: totalData.length }; } } /** * Web Worker manager for offloading computations */ export class ChartWorkerManager { constructor(maxWorkers = navigator.hardwareConcurrency || 4) { Object.defineProperty(this, "workers", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "maxWorkers", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "workerScript", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.maxWorkers = maxWorkers; this.workerScript = this.createWorkerScript(); } /** * Process data in a web worker */ async processData(operation, data, options = {}) { return new Promise((resolve, reject) => { const workerId = `${operation}-${Date.now()}`; const worker = this.createWorker(workerId); const timeout = setTimeout(() => { this.destroyWorker(workerId); reject(new Error('Worker operation timed out')); }, 30000); // 30 second timeout worker.onmessage = (event) => { clearTimeout(timeout); const { success, result, error } = event.data; if (success) { resolve(result); } else { reject(new Error(error)); } this.destroyWorker(workerId); }; worker.onerror = (error) => { clearTimeout(timeout); reject(error); this.destroyWorker(workerId); }; worker.postMessage({ operation, data, options }); }); } /** * Calculate statistics in worker */ async calculateStatistics(data) { return this.processData('calculate', data, { type: 'statistics' }); } /** * Aggregate time series data in worker */ async aggregateTimeSeries(data, interval) { return this.processData('aggregate', data, { interval }); } createWorker(id) { const blob = new Blob([this.workerScript], { type: 'application/javascript' }); const worker = new Worker(URL.createObjectURL(blob)); this.workers.set(id, worker); return worker; } destroyWorker(id) { const worker = this.workers.get(id); if (worker) { worker.terminate(); this.workers.delete(id); } } createWorkerScript() { return ` self.onmessage = function(event) { const { operation, data, options } = event.data; try { let result; switch (operation) { case 'calculate': result = calculateStatistics(data, options); break; case 'aggregate': result = aggregateData(data, options); break; case 'filter': result = filterData(data, options); break; case 'sort': result = sortData(data, options); break; default: throw new Error('Unknown operation: ' + operation); } self.postMessage({ success: true, result }); } catch (error) { self.postMessage({ success: false, error: error.message }); } }; function calculateStatistics(data, options) { if (!Array.isArray(data) || data.length === 0) { throw new Error('Invalid data for statistics calculation'); } const sorted = [...data].sort((a, b) => a - b); const sum = data.reduce((a, b) => a + b, 0); const mean = sum / data.length; const variance = data.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / data.length; const std = Math.sqrt(variance); const median = sorted.length % 2 === 0 ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 : sorted[Math.floor(sorted.length / 2)]; const percentile = (p) => { const index = Math.ceil((p / 100) * sorted.length) - 1; return sorted[Math.max(0, index)]; }; return { mean, median, std, min: sorted[0], max: sorted[sorted.length - 1], percentiles: { p25: percentile(25), p75: percentile(75), p90: percentile(90), p95: percentile(95) } }; } function aggregateData(data, options) { const { interval } = options; const intervals = { hour: 60 * 60 * 1000, day: 24 * 60 * 60 * 1000, week: 7 * 24 * 60 * 60 * 1000, month: 30 * 24 * 60 * 60 * 1000 }; const intervalMs = intervals[interval] || intervals.day; const groups = new Map(); data.forEach(point => { const bucket = Math.floor(point.timestamp / intervalMs) * intervalMs; if (!groups.has(bucket)) { groups.set(bucket, { values: [], count: 0 }); } groups.get(bucket).values.push(point.value); groups.get(bucket).count++; }); return Array.from(groups.entries()).map(([timestamp, group]) => ({ timestamp, value: group.values.reduce((a, b) => a + b, 0) / group.values.length, count: group.count })).sort((a, b) => a.timestamp - b.timestamp); } function filterData(data, options) { const { predicate } = options; return data.filter(eval(predicate)); } function sortData(data, options) { const { field, direction = 'asc' } = options; return [...data].sort((a, b) => { const valA = field ? a[field] : a; const valB = field ? b[field] : b; if (direction === 'desc') { return valB - valA; } return valA - valB; }); } `; } /** * Cleanup all workers */ destroy() { this.workers.forEach((worker, id) => { this.destroyWorker(id); }); } } /** * Efficient data streaming for real-time updates */ export class DataStreamer { constructor(maxBufferSize = 1000, flushIntervalMs = 100, flushCallback) { Object.defineProperty(this, "buffer", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "maxBufferSize", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "flushCallback", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "flushInterval", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "intervalId", { enumerable: true, configurable: true, writable: true, value: null }); this.maxBufferSize = maxBufferSize; this.flushInterval = flushIntervalMs; this.flushCallback = flushCallback; } /** * Add data point to stream */ push(dataPoint) { this.buffer.push(dataPoint); if (this.buffer.length >= this.maxBufferSize) { this.flush(); } } /** * Add multiple data points */ pushBatch(dataPoints) { this.buffer.push(...dataPoints); if (this.buffer.length >= this.maxBufferSize) { this.flush(); } } /** * Start automatic flushing */ start() { if (this.intervalId) return; this.intervalId = setInterval(() => { if (this.buffer.length > 0) { this.flush(); } }, this.flushInterval); } /** * Stop automatic flushing */ stop() { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } // Flush remaining data if (this.buffer.length > 0) { this.flush(); } } /** * Manually flush buffer */ flush() { if (this.buffer.length === 0) return; const data = [...this.buffer]; this.buffer = []; this.flushCallback(data); } /** * Get current buffer size */ getBufferSize() { return this.buffer.length; } } /** * Performance monitoring for charts */ export class ChartPerformanceMonitor { constructor() { Object.defineProperty(this, "metrics", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "startTimes", { enumerable: true, configurable: true, writable: true, value: new Map() }); } /** * Start timing an operation */ startTiming(operation) { this.startTimes.set(operation, performance.now()); } /** * End timing and record metric */ endTiming(operation) { const startTime = this.startTimes.get(operation); if (!startTime) return 0; const duration = performance.now() - startTime; if (!this.metrics.has(operation)) { this.metrics.set(operation, []); } this.metrics.get(operation).push(duration); this.startTimes.delete(operation); // Keep only last 100 measurements const measurements = this.metrics.get(operation); if (measurements.length > 100) { measurements.shift(); } return duration; } /** * Get performance statistics for an operation */ getStats(operation) { const measurements = this.metrics.get(operation); if (!measurements || measurements.length === 0) return null; const sorted = [...measurements].sort((a, b) => a - b); const sum = measurements.reduce((a, b) => a + b, 0); return { avg: sum / measurements.length, min: sorted[0], max: sorted[sorted.length - 1], count: measurements.length, p95: sorted[Math.floor(sorted.length * 0.95)] }; } /** * Get all performance metrics */ getAllStats() { const stats = {}; for (const operation of this.metrics.keys()) { stats[operation] = this.getStats(operation); } return stats; } /** * Clear all metrics */ clear() { this.metrics.clear(); this.startTimes.clear(); } /** * Log performance report to console */ logReport() { const stats = this.getAllStats(); console.group('Chart Performance Report'); for (const [operation, metrics] of Object.entries(stats)) { if (metrics) { console.log(`${operation}:`, { 'Avg': `${metrics.avg.toFixed(2)}ms`, 'Min': `${metrics.min.toFixed(2)}ms`, 'Max': `${metrics.max.toFixed(2)}ms`, 'P95': `${metrics.p95.toFixed(2)}ms`, 'Count': metrics.count }); } } console.groupEnd(); } } /** * Debounce and throttle utilities */ export function debounce(func, waitMs) { let timeoutId = null; return (...args) => { if (timeoutId !== null) { clearTimeout(timeoutId); } timeoutId = setTimeout(() => { func.apply(null, args); }, waitMs); }; } export function throttle(func, waitMs) { let lastCallTime = 0; let timeoutId = null; return (...args) => { const now = Date.now(); const timeSinceLastCall = now - lastCallTime; if (timeSinceLastCall >= waitMs) { lastCallTime = now; func.apply(null, args); } else if (timeoutId === null) { timeoutId = setTimeout(() => { lastCallTime = Date.now(); timeoutId = null; func.apply(null, args); }, waitMs - timeSinceLastCall); } }; } /** * Memory usage monitoring */ export class MemoryMonitor { constructor() { Object.defineProperty(this, "observers", { enumerable: true, configurable: true, writable: true, value: new Set() }); } static getInstance() { if (!MemoryMonitor.instance) { MemoryMonitor.instance = new MemoryMonitor(); } return MemoryMonitor.instance; } /** * Get current memory usage */ getMemoryUsage() { if ('memory' in performance) { return performance.memory; } return null; } /** * Start monitoring memory usage */ startMonitoring(intervalMs = 5000) { setInterval(() => { const usage = this.getMemoryUsage(); if (usage) { this.observers.forEach(observer => observer(usage)); } }, intervalMs); } /** * Subscribe to memory updates */ subscribe(observer) { this.observers.add(observer); return () => this.observers.delete(observer); } /** * Check if memory usage is high */ isMemoryHigh() { const usage = this.getMemoryUsage(); if (!usage) return false; // Consider memory high if using more than 80% of limit return usage.usedJSHeapSize > usage.jsHeapSizeLimit * 0.8; } }