Commit all changes
This commit is contained in:
@@ -0,0 +1,615 @@
|
||||
/**
|
||||
* Advanced chart methods for the Sport Science Coach
|
||||
* Contains implementations for heatmaps, radar charts, and drill-down functionality
|
||||
*/
|
||||
|
||||
// Add these methods to the ChartsManager class
|
||||
|
||||
/**
|
||||
* Create sleep heatmap chart
|
||||
* @param {Array} metricsData - Processed metrics data
|
||||
* @param {HTMLElement} wrapper - Wrapper for the chart containers
|
||||
*/
|
||||
ChartsManager.prototype.createSleepHeatmap = function(metricsData, wrapper) {
|
||||
// Filter out days without sleep data
|
||||
const sleepData = metricsData.filter(day =>
|
||||
day.metrics['Sleep Hours'] !== undefined
|
||||
);
|
||||
|
||||
if (sleepData.length === 0) {
|
||||
this.showNoDataMessage(wrapper, 'No sleep data available for heatmap');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create container for heatmap
|
||||
const heatmapContainer = this.createChartContainer(wrapper);
|
||||
heatmapContainer.classList.add('interactive');
|
||||
const canvas = document.createElement('canvas');
|
||||
heatmapContainer.appendChild(canvas);
|
||||
|
||||
// Process data for heatmap
|
||||
// Group by day of week and week number
|
||||
const heatmapData = this.processDataForSleepHeatmap(sleepData);
|
||||
|
||||
// Create heatmap chart
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'matrix',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'Sleep Hours',
|
||||
data: heatmapData,
|
||||
backgroundColor(context) {
|
||||
const value = context.dataset.data[context.dataIndex].v;
|
||||
// Color scale based on sleep hours
|
||||
if (value === null) return 'rgba(0, 0, 0, 0.05)';
|
||||
return value >= 8 ? 'rgba(46, 204, 113, 0.8)' :
|
||||
value >= 7 ? 'rgba(52, 152, 219, 0.8)' :
|
||||
value >= 6 ? 'rgba(241, 196, 15, 0.8)' :
|
||||
'rgba(231, 76, 60, 0.8)';
|
||||
},
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
borderWidth: 1,
|
||||
width: 25,
|
||||
height: 25
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title() {
|
||||
return '';
|
||||
},
|
||||
label(context) {
|
||||
const v = context.dataset.data[context.dataIndex];
|
||||
if (v.v === null) return 'No data';
|
||||
return [
|
||||
`Date: ${v.date}`,
|
||||
`Sleep: ${v.v.toFixed(1)} hours`
|
||||
];
|
||||
}
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Sleep Pattern Heatmap'
|
||||
},
|
||||
subtitle: {
|
||||
display: true,
|
||||
text: 'Click for detailed view',
|
||||
padding: {
|
||||
bottom: 10
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
type: 'category',
|
||||
labels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||
offset: true,
|
||||
ticks: {
|
||||
display: true
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Day of Week'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
type: 'category',
|
||||
labels: this.getWeekLabels(sleepData),
|
||||
offset: true,
|
||||
ticks: {
|
||||
display: true
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Week'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Store chart reference
|
||||
this.charts.push(chart);
|
||||
|
||||
// Add heatmap legend
|
||||
const legend = document.createElement('div');
|
||||
legend.className = 'heatmap-legend';
|
||||
|
||||
const legendItems = [
|
||||
{ label: '< 6 hours', color: 'rgba(231, 76, 60, 0.8)' },
|
||||
{ label: '6-7 hours', color: 'rgba(241, 196, 15, 0.8)' },
|
||||
{ label: '7-8 hours', color: 'rgba(52, 152, 219, 0.8)' },
|
||||
{ label: '8+ hours', color: 'rgba(46, 204, 113, 0.8)' }
|
||||
];
|
||||
|
||||
legendItems.forEach(item => {
|
||||
const legendItem = document.createElement('div');
|
||||
legendItem.className = 'heatmap-legend-item';
|
||||
|
||||
const colorBox = document.createElement('div');
|
||||
colorBox.className = 'heatmap-legend-color';
|
||||
colorBox.style.backgroundColor = item.color;
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = item.label;
|
||||
|
||||
legendItem.appendChild(colorBox);
|
||||
legendItem.appendChild(label);
|
||||
legend.appendChild(legendItem);
|
||||
});
|
||||
|
||||
heatmapContainer.appendChild(legend);
|
||||
|
||||
// Add click event for drill-down
|
||||
canvas.addEventListener('click', (evt) => {
|
||||
const points = chart.getElementsAtEventForMode(evt, 'nearest', { intersect: true }, false);
|
||||
|
||||
if (points.length) {
|
||||
const firstPoint = points[0];
|
||||
const data = chart.data.datasets[firstPoint.datasetIndex].data[firstPoint.index];
|
||||
|
||||
if (data.v !== null) {
|
||||
this.createSleepDetailDrillDown(data.date, heatmapContainer);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Process data for sleep heatmap
|
||||
* @param {Array} sleepData - Sleep metrics data
|
||||
* @returns {Array} - Processed data for heatmap
|
||||
*/
|
||||
ChartsManager.prototype.processDataForSleepHeatmap = function(sleepData) {
|
||||
const heatmapData = [];
|
||||
|
||||
// Get the first date in the dataset
|
||||
const firstDate = new Date(sleepData[0].date);
|
||||
const firstWeek = this.getWeekNumber(firstDate);
|
||||
|
||||
// Create a map of date to sleep hours
|
||||
const dateToSleepHours = {};
|
||||
sleepData.forEach(day => {
|
||||
dateToSleepHours[day.date] = day.metrics['Sleep Hours'];
|
||||
});
|
||||
|
||||
// Generate all days in the range
|
||||
const startDate = new Date(sleepData[0].date);
|
||||
const endDate = new Date(sleepData[sleepData.length - 1].date);
|
||||
|
||||
// Add one day to endDate to include it in the range
|
||||
endDate.setDate(endDate.getDate() + 1);
|
||||
|
||||
for (let d = new Date(startDate); d < endDate; d.setDate(d.getDate() + 1)) {
|
||||
const date = d.toISOString().split('T')[0];
|
||||
const dayOfWeek = d.getDay(); // 0 = Sunday, 6 = Saturday
|
||||
const weekNumber = this.getWeekNumber(d);
|
||||
const weekIndex = weekNumber - firstWeek;
|
||||
|
||||
heatmapData.push({
|
||||
x: weekIndex,
|
||||
y: dayOfWeek,
|
||||
v: dateToSleepHours[date] !== undefined ? dateToSleepHours[date] : null,
|
||||
date: date
|
||||
});
|
||||
}
|
||||
|
||||
return heatmapData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get week number for a date
|
||||
* @param {Date} date - Date to get week number for
|
||||
* @returns {number} - Week number
|
||||
*/
|
||||
ChartsManager.prototype.getWeekNumber = function(date) {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get week labels for heatmap
|
||||
* @param {Array} sleepData - Sleep metrics data
|
||||
* @returns {Array} - Week labels
|
||||
*/
|
||||
ChartsManager.prototype.getWeekLabels = function(sleepData) {
|
||||
const firstDate = new Date(sleepData[0].date);
|
||||
const lastDate = new Date(sleepData[sleepData.length - 1].date);
|
||||
|
||||
const firstWeek = this.getWeekNumber(firstDate);
|
||||
const lastWeek = this.getWeekNumber(lastDate);
|
||||
|
||||
const labels = [];
|
||||
for (let i = firstWeek; i <= lastWeek; i++) {
|
||||
labels.push(`Week ${i}`);
|
||||
}
|
||||
|
||||
return labels;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create fitness radar chart
|
||||
* @param {Object} data - Processed health and workout data
|
||||
* @param {HTMLElement} wrapper - Wrapper for the chart containers
|
||||
*/
|
||||
ChartsManager.prototype.createFitnessRadarChart = function(data, wrapper) {
|
||||
// Create container for radar chart
|
||||
const radarContainer = this.createChartContainer(wrapper);
|
||||
radarContainer.classList.add('radar-chart-container');
|
||||
radarContainer.classList.add('interactive');
|
||||
const canvas = document.createElement('canvas');
|
||||
radarContainer.appendChild(canvas);
|
||||
|
||||
// Process data for radar chart
|
||||
const radarData = this.processDataForRadarChart(data);
|
||||
|
||||
if (!radarData) {
|
||||
this.showNoDataMessage(radarContainer, 'Insufficient data for fitness radar chart');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create radar chart
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'radar',
|
||||
data: {
|
||||
labels: ['Sleep Quality', 'Stress Management', 'Workout Intensity', 'Recovery', 'Consistency'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Current Profile',
|
||||
data: radarData.current,
|
||||
backgroundColor: 'rgba(111, 185, 143, 0.2)',
|
||||
borderColor: this.colors.secondary,
|
||||
pointBackgroundColor: this.colors.secondary,
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: this.colors.secondary
|
||||
},
|
||||
{
|
||||
label: 'Optimal Profile',
|
||||
data: radarData.optimal,
|
||||
backgroundColor: 'rgba(74, 111, 165, 0.2)',
|
||||
borderColor: this.colors.primary,
|
||||
pointBackgroundColor: this.colors.primary,
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: this.colors.primary
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Fitness Balance Radar'
|
||||
},
|
||||
subtitle: {
|
||||
display: true,
|
||||
text: 'Click for detailed view',
|
||||
padding: {
|
||||
bottom: 10
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const value = context.raw;
|
||||
const percentage = (value * 10).toFixed(0);
|
||||
return `${context.dataset.label}: ${percentage}%`;
|
||||
}
|
||||
}
|
||||
},
|
||||
datalabels: {
|
||||
formatter: (value) => {
|
||||
return (value * 10).toFixed(0);
|
||||
},
|
||||
color: '#fff',
|
||||
backgroundColor: function(context) {
|
||||
return context.dataset.borderColor;
|
||||
},
|
||||
borderRadius: 3,
|
||||
font: {
|
||||
weight: 'bold',
|
||||
size: 10
|
||||
},
|
||||
padding: 4
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
r: {
|
||||
angleLines: {
|
||||
display: true,
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
suggestedMin: 0,
|
||||
suggestedMax: 10,
|
||||
ticks: {
|
||||
stepSize: 2,
|
||||
callback: function(value) {
|
||||
return (value * 10) + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Store chart reference
|
||||
this.charts.push(chart);
|
||||
|
||||
// Add click event for drill-down
|
||||
canvas.addEventListener('click', () => {
|
||||
this.createFitnessRadarDrillDown(data, radarData, radarContainer);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Process data for radar chart
|
||||
* @param {Object} data - Processed health and workout data
|
||||
* @returns {Object|null} - Processed data for radar chart or null if insufficient data
|
||||
*/
|
||||
ChartsManager.prototype.processDataForRadarChart = function(data) {
|
||||
// Check if we have enough data
|
||||
if (!data.metrics || data.metrics.length === 0 || !data.workouts || data.workouts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate sleep quality score (0-10)
|
||||
const sleepHours = data.metrics
|
||||
.filter(day => day.metrics['Sleep Hours'] !== undefined)
|
||||
.map(day => day.metrics['Sleep Hours']);
|
||||
|
||||
if (sleepHours.length === 0) return null;
|
||||
|
||||
const avgSleepHours = this.calculateAverage(sleepHours);
|
||||
const sleepQualityScore = Math.min(10, Math.max(0, (avgSleepHours - 4) * 1.67)) / 10;
|
||||
|
||||
// Calculate stress management score (0-10)
|
||||
const stressQualifiers = data.metrics
|
||||
.filter(day => day.metrics['Stress Qualifier'] !== undefined)
|
||||
.map(day => day.metrics['Stress Qualifier']);
|
||||
|
||||
if (stressQualifiers.length === 0) return null;
|
||||
|
||||
const stressScores = {
|
||||
'calm': 10,
|
||||
'balanced': 7.5,
|
||||
'stressful': 4,
|
||||
'very_stressful': 1
|
||||
};
|
||||
|
||||
const avgStressScore = this.calculateAverage(stressQualifiers.map(qualifier => stressScores[qualifier] || 5)) / 10;
|
||||
|
||||
// Calculate workout intensity score (0-10)
|
||||
const workoutIntensities = data.workouts.map(workout => {
|
||||
const heartRateScore = workout.heartRate.average / 180 * 10; // Normalize to 0-10
|
||||
return Math.min(10, Math.max(0, heartRateScore));
|
||||
});
|
||||
|
||||
const avgWorkoutIntensity = this.calculateAverage(workoutIntensities) / 10;
|
||||
|
||||
// Calculate recovery score (0-10)
|
||||
const hrvValues = data.metrics
|
||||
.filter(day => day.metrics['HRV'] !== undefined)
|
||||
.map(day => day.metrics['HRV']);
|
||||
|
||||
if (hrvValues.length === 0) return null;
|
||||
|
||||
const avgHRV = this.calculateAverage(hrvValues);
|
||||
const recoveryScore = Math.min(10, Math.max(0, avgHRV / 7)) / 10;
|
||||
|
||||
// Calculate consistency score (0-10)
|
||||
const totalDays = (new Date(data.metrics[data.metrics.length - 1].date) -
|
||||
new Date(data.metrics[0].date)) / (1000 * 60 * 60 * 24);
|
||||
|
||||
const workoutFrequency = data.workouts.length / (totalDays / 7); // Workouts per week
|
||||
const consistencyScore = Math.min(10, Math.max(0, workoutFrequency * 2.5)) / 10;
|
||||
|
||||
return {
|
||||
current: [
|
||||
sleepQualityScore,
|
||||
avgStressScore,
|
||||
avgWorkoutIntensity,
|
||||
recoveryScore,
|
||||
consistencyScore
|
||||
],
|
||||
optimal: [0.8, 0.8, 0.7, 0.8, 0.7] // Optimal values for comparison
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create fitness radar drill-down
|
||||
* @param {Object} data - Processed health and workout data
|
||||
* @param {Object} radarData - Processed radar chart data
|
||||
* @param {HTMLElement} container - Container element
|
||||
*/
|
||||
ChartsManager.prototype.createFitnessRadarDrillDown = function(data, radarData, container) {
|
||||
// Check if drill-down container already exists
|
||||
let drillDownContainer = container.querySelector('.drill-down-container');
|
||||
|
||||
if (!drillDownContainer) {
|
||||
drillDownContainer = document.createElement('div');
|
||||
drillDownContainer.className = 'drill-down-container';
|
||||
container.appendChild(drillDownContainer);
|
||||
} else {
|
||||
drillDownContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
// Create header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'drill-down-header';
|
||||
|
||||
const title = document.createElement('h4');
|
||||
title.className = 'drill-down-title';
|
||||
title.textContent = 'Fitness Balance Details';
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.className = 'drill-down-back-button';
|
||||
backButton.innerHTML = '<i class="fas fa-arrow-left"></i> Back';
|
||||
backButton.addEventListener('click', () => {
|
||||
drillDownContainer.classList.add('hidden');
|
||||
});
|
||||
|
||||
header.appendChild(title);
|
||||
header.appendChild(backButton);
|
||||
drillDownContainer.appendChild(header);
|
||||
|
||||
// Create metrics table
|
||||
const metricsTable = document.createElement('table');
|
||||
metricsTable.style.width = '100%';
|
||||
metricsTable.style.borderCollapse = 'collapse';
|
||||
metricsTable.style.marginTop = '20px';
|
||||
|
||||
// Add table header
|
||||
const thead = document.createElement('thead');
|
||||
const headerRow = document.createElement('tr');
|
||||
|
||||
['Metric', 'Score', 'Details', 'Recommendation'].forEach(text => {
|
||||
const th = document.createElement('th');
|
||||
th.textContent = text;
|
||||
th.style.padding = '8px';
|
||||
th.style.textAlign = 'left';
|
||||
th.style.borderBottom = '1px solid #ddd';
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
|
||||
thead.appendChild(headerRow);
|
||||
metricsTable.appendChild(thead);
|
||||
|
||||
// Add table body
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
// Define metrics details
|
||||
const metricsDetails = [
|
||||
{
|
||||
name: 'Sleep Quality',
|
||||
score: (radarData.current[0] * 10).toFixed(0) + '%',
|
||||
details: `Average sleep: ${this.calculateAverage(data.metrics.filter(day => day.metrics['Sleep Hours'] !== undefined).map(day => day.metrics['Sleep Hours'])).toFixed(1)} hours`,
|
||||
recommendation: radarData.current[0] < 0.7 ? 'Aim for 7-9 hours of sleep per night' : 'Maintain current sleep schedule'
|
||||
},
|
||||
{
|
||||
name: 'Stress Management',
|
||||
score: (radarData.current[1] * 10).toFixed(0) + '%',
|
||||
details: this.getStressDistribution(data.metrics),
|
||||
recommendation: radarData.current[1] < 0.7 ? 'Incorporate daily mindfulness or meditation practice' : 'Continue effective stress management techniques'
|
||||
},
|
||||
{
|
||||
name: 'Workout Intensity',
|
||||
score: (radarData.current[2] * 10).toFixed(0) + '%',
|
||||
details: `Average heart rate: ${this.calculateAverage(data.workouts.map(w => w.heartRate.average)).toFixed(0)} bpm`,
|
||||
recommendation: radarData.current[2] < 0.6 ? 'Consider adding high-intensity interval training' : (radarData.current[2] > 0.8 ? 'Balance high-intensity workouts with recovery sessions' : 'Current intensity level is well-balanced')
|
||||
},
|
||||
{
|
||||
name: 'Recovery',
|
||||
score: (radarData.current[3] * 10).toFixed(0) + '%',
|
||||
details: `Average HRV: ${this.calculateAverage(data.metrics.filter(day => day.metrics['HRV'] !== undefined).map(day => day.metrics['HRV'])).toFixed(0)} ms`,
|
||||
recommendation: radarData.current[3] < 0.7 ? 'Focus on recovery techniques like stretching and proper nutrition' : 'Maintain current recovery practices'
|
||||
},
|
||||
{
|
||||
name: 'Consistency',
|
||||
score: (radarData.current[4] * 10).toFixed(0) + '%',
|
||||
details: `${data.workouts.length} workouts over ${Math.ceil((new Date(data.metrics[data.metrics.length - 1].date) - new Date(data.metrics[0].date)) / (1000 * 60 * 60 * 24))} days`,
|
||||
recommendation: radarData.current[4] < 0.6 ? 'Establish a regular workout schedule (3-4 times per week)' : 'Maintain current consistency'
|
||||
}
|
||||
];
|
||||
|
||||
// Add metrics rows
|
||||
metricsDetails.forEach(metric => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
['name', 'score', 'details', 'recommendation'].forEach(prop => {
|
||||
const cell = document.createElement('td');
|
||||
cell.textContent = metric[prop];
|
||||
cell.style.padding = '8px';
|
||||
cell.style.borderBottom = '1px solid #eee';
|
||||
row.appendChild(cell);
|
||||
});
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
metricsTable.appendChild(tbody);
|
||||
drillDownContainer.appendChild(metricsTable);
|
||||
|
||||
// Add explanation
|
||||
const explanation = document.createElement('div');
|
||||
explanation.style.marginTop = '20px';
|
||||
explanation.style.padding = '15px';
|
||||
explanation.style.backgroundColor = 'rgba(74, 111, 165, 0.1)';
|
||||
explanation.style.borderRadius = '6px';
|
||||
|
||||
const explanationTitle = document.createElement('h4');
|
||||
explanationTitle.textContent = 'About This Chart';
|
||||
explanationTitle.style.marginTop = '0';
|
||||
|
||||
const explanationText = document.createElement('p');
|
||||
explanationText.textContent = 'The Fitness Balance Radar provides a holistic view of your health and fitness profile across five key dimensions. Each dimension is scored from 0-100% based on your data, with higher scores indicating better performance. The "Optimal Profile" represents balanced targets across all dimensions.';
|
||||
|
||||
explanation.appendChild(explanationTitle);
|
||||
explanation.appendChild(explanationText);
|
||||
drillDownContainer.appendChild(explanation);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get stress distribution text
|
||||
* @param {Array} metricsData - Metrics data
|
||||
* @returns {string} - Stress distribution text
|
||||
*/
|
||||
ChartsManager.prototype.getStressDistribution = function(metricsData) {
|
||||
const stressQualifiers = metricsData
|
||||
.filter(day => day.metrics['Stress Qualifier'] !== undefined)
|
||||
.map(day => day.metrics['Stress Qualifier']);
|
||||
|
||||
const counts = {
|
||||
'calm': 0,
|
||||
'balanced': 0,
|
||||
'stressful': 0,
|
||||
'very_stressful': 0
|
||||
};
|
||||
|
||||
stressQualifiers.forEach(qualifier => {
|
||||
counts[qualifier] = (counts[qualifier] || 0) + 1;
|
||||
});
|
||||
|
||||
const total = stressQualifiers.length;
|
||||
|
||||
return `${Math.round(counts.calm / total * 100)}% Calm, ${Math.round(counts.balanced / total * 100)}% Balanced, ${Math.round(counts.stressful / total * 100)}% Stressful, ${Math.round(counts.very_stressful / total * 100)}% Very Stressful`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate average of array values
|
||||
* @param {Array} values - Array of numbers
|
||||
* @returns {number} - Average value
|
||||
*/
|
||||
ChartsManager.prototype.calculateAverage = function(values) {
|
||||
if (!values || values.length === 0) return 0;
|
||||
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate sum of array values
|
||||
* @param {Array} values - Array of numbers
|
||||
* @returns {number} - Sum of values
|
||||
*/
|
||||
ChartsManager.prototype.calculateSum = function(values) {
|
||||
if (!values || values.length === 0) return 0;
|
||||
return values.reduce((sum, val) => sum + val, 0);
|
||||
};
|
||||
Reference in New Issue
Block a user