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