UNPKG

bowling-analysis-system

Version:

A comprehensive system for analyzing bowling techniques using video processing and metrics calculation

339 lines (294 loc) 10 kB
/** * Comparison Generator Processor * * Generates comparisons between multiple metrics datasets * @module processors/ComparisonGeneratorProcessor */ const { BaseProcessor } = require('../core/BaseProcessor'); /** * @class ComparisonGeneratorProcessor * @description Processor for generating comparisons between multiple metrics datasets * @extends BaseProcessor */ class ComparisonGeneratorProcessor extends BaseProcessor { /** * Create a new comparison generator processor * @param {Object} config - Processor configuration */ constructor(config = {}) { super('comparisonGenerator', config); this.formatters = new Map(); // Register default formatters this.registerFormatter('html', this._formatHtml.bind(this)); this.registerFormatter('json', this._formatJson.bind(this)); this.registerFormatter('csv', this._formatCsv.bind(this)); } /** * Register a comparison formatter * @param {string} format - Format name * @param {Function} formatter - Formatter function * @returns {ComparisonGeneratorProcessor} Processor instance for chaining */ registerFormatter(format, formatter) { this.formatters.set(format, formatter); return this; } /** * Process input by generating a comparison * @param {Array<Object>} input - Input metrics datasets * @param {Object} context - Processing context * @returns {Promise<*>} Generated comparison * @protected */ async _process(input, context) { if (!Array.isArray(input) || input.length < 2) { throw new Error('Comparison requires at least two datasets'); } const format = this.config.format || 'html'; const metrics = this.config.metrics || null; // Extract metrics from each dataset const datasets = input.map((item, index) => { return { name: item.path ? item.path.split('/').pop() : `Dataset ${index + 1}`, metrics: item.data?.metrics || {} }; }); // Generate comparison data const comparison = this._generateComparison(datasets, metrics); // Get formatter const formatter = this.formatters.get(format); if (!formatter) { throw new Error(`Unknown comparison format: ${format}`); } // Apply formatter return formatter(comparison, context); } /** * Generate comparison data * @param {Array<Object>} datasets - Metrics datasets * @param {Array<string>|null} metricsList - List of metrics to compare * @returns {Object} Comparison data * @private */ _generateComparison(datasets, metricsList) { // Collect all available metrics const allMetrics = new Set(); datasets.forEach(dataset => { this._collectMetricPaths(dataset.metrics, '', allMetrics); }); // Filter metrics if specified const metricsToCompare = metricsList ? Array.from(allMetrics).filter(metric => metricsList.some(pattern => metric.includes(pattern)) ) : Array.from(allMetrics); // Generate comparison for each metric const comparisons = {}; metricsToCompare.forEach(metric => { const values = datasets.map(dataset => { return this._getNestedValue(dataset.metrics, metric.split('.')); }); comparisons[metric] = { values: values.map((value, index) => ({ dataset: datasets[index].name, value })), min: this._getNumericMin(values), max: this._getNumericMax(values), mean: this._getNumericMean(values), diff: values.length >= 2 ? this._calculateDiff(values[0], values[1]) : null }; }); return { timestamp: Date.now(), datasets: datasets.map(d => d.name), metrics: metricsToCompare, comparisons }; } /** * Collect all metric paths from an object * @param {Object} obj - Object to collect metrics from * @param {string} prefix - Current path prefix * @param {Set<string>} result - Set to collect paths * @private */ _collectMetricPaths(obj, prefix, result) { if (!obj || typeof obj !== 'object') { return; } Object.entries(obj).forEach(([key, value]) => { const path = prefix ? `${prefix}.${key}` : key; if (value !== null && typeof value === 'object' && !Array.isArray(value)) { this._collectMetricPaths(value, path, result); } else { result.add(path); } }); } /** * Get a nested value from an object * @param {Object} obj - Object to get value from * @param {Array<string>} path - Path to value * @returns {*} Nested value * @private */ _getNestedValue(obj, path) { let current = obj; for (const key of path) { if (current === undefined || current === null) { return undefined; } current = current[key]; } return current; } /** * Get minimum numeric value * @param {Array<*>} values - Values to get minimum from * @returns {number|null} Minimum value * @private */ _getNumericMin(values) { const numericValues = values.filter(v => typeof v === 'number'); return numericValues.length > 0 ? Math.min(...numericValues) : null; } /** * Get maximum numeric value * @param {Array<*>} values - Values to get maximum from * @returns {number|null} Maximum value * @private */ _getNumericMax(values) { const numericValues = values.filter(v => typeof v === 'number'); return numericValues.length > 0 ? Math.max(...numericValues) : null; } /** * Get mean numeric value * @param {Array<*>} values - Values to get mean from * @returns {number|null} Mean value * @private */ _getNumericMean(values) { const numericValues = values.filter(v => typeof v === 'number'); return numericValues.length > 0 ? numericValues.reduce((sum, v) => sum + v, 0) / numericValues.length : null; } /** * Calculate difference between two values * @param {*} a - First value * @param {*} b - Second value * @returns {Object|null} Difference information * @private */ _calculateDiff(a, b) { if (typeof a === 'number' && typeof b === 'number') { const absoluteDiff = b - a; const percentDiff = a !== 0 ? (absoluteDiff / Math.abs(a)) * 100 : null; return { absolute: absoluteDiff, percent: percentDiff, improved: absoluteDiff > 0 }; } return null; } /** * Format comparison as HTML * @param {Object} comparison - Comparison data * @param {Object} context - Context data * @returns {string} HTML comparison * @private */ _formatHtml(comparison, context) { // Generate HTML table for comparison const metricsHtml = comparison.metrics.map(metric => { const data = comparison.comparisons[metric]; const values = data.values.map(v => `<td>${typeof v.value === 'number' ? v.value.toFixed(2) : v.value || 'N/A'}</td>` ).join(''); const diff = data.diff ? `<td class="${data.diff.improved ? 'improved' : 'declined'}">${data.diff.absolute.toFixed(2)} (${data.diff.percent.toFixed(1)}%)</td>` : '<td>N/A</td>'; return ` <tr> <td>${metric}</td> ${values} ${comparison.datasets.length === 2 ? diff : ''} </tr> `; }).join(''); return `<!DOCTYPE html> <html> <head> <title>Metrics Comparison</title> <style> body { font-family: Arial, sans-serif; margin: 0; padding: 20px; } h1 { color: #333; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } tr:nth-child(even) { background-color: #f9f9f9; } .improved { color: green; } .declined { color: red; } </style> </head> <body> <h1>Metrics Comparison</h1> <p>Generated on ${new Date().toLocaleString()}</p> <p>Comparing ${comparison.datasets.length} datasets: ${comparison.datasets.join(', ')}</p> <table> <thead> <tr> <th>Metric</th> ${comparison.datasets.map(name => `<th>${name}</th>`).join('')} ${comparison.datasets.length === 2 ? '<th>Difference</th>' : ''} </tr> </thead> <tbody> ${metricsHtml} </tbody> </table> </body> </html>`; } /** * Format comparison as JSON * @param {Object} comparison - Comparison data * @param {Object} context - Context data * @returns {string} JSON comparison * @private */ _formatJson(comparison, context) { return JSON.stringify(comparison, null, 2); } /** * Format comparison as CSV * @param {Object} comparison - Comparison data * @param {Object} context - Context data * @returns {string} CSV comparison * @private */ _formatCsv(comparison, context) { // Generate CSV header const header = ['Metric', ...comparison.datasets]; if (comparison.datasets.length === 2) { header.push('Difference', 'Percent'); } // Generate CSV rows const rows = comparison.metrics.map(metric => { const data = comparison.comparisons[metric]; const values = data.values.map(v => typeof v.value === 'number' ? v.value.toFixed(4) : (v.value || 'N/A') ); const row = [metric, ...values]; if (comparison.datasets.length === 2 && data.diff) { row.push(data.diff.absolute.toFixed(4), data.diff.percent.toFixed(2) + '%'); } return row.join(','); }); return [header.join(','), ...rows].join('\n'); } } module.exports = ComparisonGeneratorProcessor;