UNPKG

bowling-analysis-system

Version:

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

266 lines (223 loc) 8.36 kB
/** * @fileoverview Utility functions for converting time series data between different formats * @module utils/timeSeriesConverter */ /** * Standardizes time series data to ensure proper left/right formatting * @param {Object} timeSeriesData - The time series data to standardize * @returns {Object} Standardized time series data */ function standardizeTimeSeriesFormat(timeSeriesData) { if (!timeSeriesData || typeof timeSeriesData !== 'object') { return {}; } const standardizedData = {}; // Process each category for (const category in timeSeriesData) { if (!timeSeriesData[category] || typeof timeSeriesData[category] !== 'object') { continue; } standardizedData[category] = {}; // Process each metric in the category for (const metricName in timeSeriesData[category]) { const values = timeSeriesData[category][metricName]; // Skip if not an array or empty if (!Array.isArray(values) || values.length === 0) { continue; } // Skip if already processed if (standardizedData[category][metricName]) { continue; } // Check if this is a "left" metric (matches the pattern "left*") if (metricName.startsWith('left')) { const baseName = metricName.substring(4); // Remove 'left' const rightName = `right${baseName}`; // Check if we have a matching right metric if (timeSeriesData[category][rightName] && Array.isArray(timeSeriesData[category][rightName])) { // Define the combined name in CamelCase with first letter lowercase const combinedName = baseName.charAt(0).toLowerCase() + baseName.slice(1); // Create combined structure standardizedData[category][combinedName] = values.map((leftValue, index) => { // Get right value from matching array, use null if out of bounds const rightValue = index < timeSeriesData[category][rightName].length ? timeSeriesData[category][rightName][index] : null; return { left: leftValue, right: rightValue }; }); } else { // No matching right metric, use left only const combinedName = baseName.charAt(0).toLowerCase() + baseName.slice(1); standardizedData[category][combinedName] = values.map(leftValue => ({ left: leftValue, right: null }) ); } } // Check if this is a "right" metric without a matching left already processed else if (metricName.startsWith('right')) { const baseName = metricName.substring(5); // Remove 'right' const leftName = `left${baseName}`; const combinedName = baseName.charAt(0).toLowerCase() + baseName.slice(1); // Skip if we have a matching left metric (it should have been processed already) if (timeSeriesData[category][leftName] && Array.isArray(timeSeriesData[category][leftName])) { continue; } // No matching left metric, use right only standardizedData[category][combinedName] = values.map(rightValue => ({ left: null, right: rightValue }) ); } // All other metrics, keep as is else { standardizedData[category][metricName] = values; } } } return standardizedData; } /** * Calculate a combined metric from left and right values * @param {*} leftValue - The left value * @param {*} rightValue - The right value * @returns {Object|null} - Object with left and right properties, or null if both values are null */ function calculateCombinedMetric(leftValue, rightValue) { // Handle null case if (leftValue === null && rightValue === null) { return null; } // Both values exist or one is null, return the combined structure return { left: leftValue, right: rightValue }; } /** * Extracts left/right time series data from a metric path * @param {Object} data - The metrics data object * @param {string} path - Dot-notation path to the metric * @returns {Object|null} The combined left/right time series or null */ function getTimeSeriesFromPath(data, path) { if (!data || !path) { return null; } const parts = path.split('.'); let current = data; // Navigate to the metric for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (current[part] === undefined) { return null; } current = current[part]; } // Check if we have a valid time series if (!Array.isArray(current)) { return null; } // Check if it's a left/right pair format const firstItem = current.find(item => item !== null); if (firstItem && typeof firstItem === 'object' && ('left' in firstItem || 'right' in firstItem)) { return current; } // It's a regular time series, check if we should look for paired format const lastPart = parts[parts.length - 1]; if (lastPart.startsWith('left') || lastPart.startsWith('right')) { // This is an individual left/right metric, combine might be available at parent level const basePart = lastPart.substring(lastPart.startsWith('left') ? 4 : 5); const combinedName = basePart.charAt(0).toLowerCase() + basePart.slice(1) + 's'; // Navigate to parent const parentPath = parts.slice(0, parts.length - 1).join('.'); const parentObject = getNestedProperty(data, parentPath); if (parentObject && parentObject[combinedName] && Array.isArray(parentObject[combinedName])) { return parentObject[combinedName]; } } // Just return the time series as is return current; } /** * Gets a nested property from an object using dot notation * @param {Object} obj - The object to get the property from * @param {string} path - Dot notation path to the property * @returns {*} The property value or undefined */ function getNestedProperty(obj, path) { if (!obj || !path) { return undefined; } const parts = path.split('.'); let current = obj; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (current[part] === undefined) { return undefined; } current = current[part]; } return current; } /** * Converts time series data to a format suitable for visualization libraries * @param {Array} timeSeriesData - Time series data array * @param {Object} options - Conversion options * @param {boolean} [options.includeBoth=true] - Whether to include both left and right in output * @returns {Object} Data formatted for visualization */ function convertToChartFormat(timeSeriesData, options = {}) { const { includeBoth = true } = options; if (!Array.isArray(timeSeriesData)) { return { labels: [], datasets: [] }; } // Create x-axis labels (frame indices) const labels = timeSeriesData.map((_, index) => index); // Initialize datasets const datasets = []; // Check if data is in left/right format const firstValidItem = timeSeriesData.find(item => item !== null); const isLeftRightFormat = firstValidItem && typeof firstValidItem === 'object' && ('left' in firstValidItem || 'right' in firstValidItem); if (isLeftRightFormat) { // Add left dataset if needed if (includeBoth) { datasets.push({ label: 'Left', data: timeSeriesData.map(item => item ? item.left : null), borderColor: 'rgba(75, 192, 192, 1)', backgroundColor: 'rgba(75, 192, 192, 0.2)', fill: false }); // Add right dataset datasets.push({ label: 'Right', data: timeSeriesData.map(item => item ? item.right : null), borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.2)', fill: false }); } } else { // Simple time series, just add one dataset datasets.push({ label: 'Value', data: timeSeriesData, borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.2)', fill: false }); } return { labels, datasets }; } module.exports = { standardizeTimeSeriesFormat, calculateCombinedMetric, getTimeSeriesFromPath, convertToChartFormat, getNestedProperty };