bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
266 lines (223 loc) • 8.36 kB
JavaScript
/**
* @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
};