/** * Charts Module * Handles the creation and rendering of data visualizations for health reports */ class ChartsManager { constructor() { // Store references to created charts for cleanup this.charts = []; // Define chart colors this.colors = { primary: '#4a6fa5', secondary: '#6fb98f', accent: '#ff7e67', dark: '#2c3e50', light: '#ecf0f1', sleep: { total: '#4a6fa5', deep: '#2c3e50', light: '#95a5a6', rem: '#3498db', awake: '#e74c3c' }, stress: { calm: '#2ecc71', balanced: '#3498db', stressful: '#f39c12', veryStressful: '#e74c3c' } }; } /** * Create all charts for the report * @param {Object} data - Processed health and workout data * @param {HTMLElement} container - Container element for the charts */ createCharts(data, container) { // Clear any existing charts this.clearCharts(); // Create chart sections const sleepSection = this.createChartSection('Sleep Patterns', container); const stressSection = this.createChartSection('Stress Levels', container); const workoutSection = this.createChartSection('Workout Analysis', container); const hrvSection = this.createChartSection('Recovery Metrics', container); // Create charts this.createSleepChart(data.metrics, sleepSection); this.createStressChart(data.metrics, stressSection); this.createWorkoutChart(data.workouts, workoutSection); this.createHRVChart(data.metrics, hrvSection); } /** * Create a section for a chart * @param {string} title - Section title * @param {HTMLElement} container - Parent container * @returns {HTMLElement} - The charts wrapper element */ createChartSection(title, container) { const section = document.createElement('div'); section.className = 'chart-section'; const heading = document.createElement('h3'); heading.textContent = title; section.appendChild(heading); // Create a wrapper for all charts in this section const chartsWrapper = document.createElement('div'); chartsWrapper.className = 'charts-wrapper'; section.appendChild(chartsWrapper); container.appendChild(section); return chartsWrapper; } /** * Create an individual chart container * @param {HTMLElement} wrapper - Parent wrapper element * @returns {HTMLElement} - The chart container element */ createChartContainer(wrapper) { const chartContainer = document.createElement('div'); chartContainer.className = 'chart-container'; wrapper.appendChild(chartContainer); return chartContainer; } /** * Helper: Calculate linear trend (slope, intercept, trend line) for a dataset * @param {Array} y - Array of y values (e.g., totalSleep) * @returns {Object} { slope, intercept, trendLine } */ getLinearTrend(y) { const n = y.length; if (n < 2) return { slope: 0, intercept: y[0] || 0, trendLine: y.map(() => y[0] || 0) }; const x = Array.from({ length: n }, (_, i) => i); const sumX = x.reduce((a, b) => a + b, 0); const sumY = y.reduce((a, b) => a + b, 0); const sumXY = x.reduce((acc, xi, i) => acc + xi * y[i], 0); const sumXX = x.reduce((acc, xi) => acc + xi * xi, 0); const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX); const intercept = (sumY - slope * sumX) / n; const trendLine = x.map(xi => slope * xi + intercept); return { slope, intercept, trendLine }; } /** * Create sleep patterns chart * @param {Array} metricsData - Processed metrics data * @param {HTMLElement} wrapper - Wrapper for the chart containers */ createSleepChart(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'); return; } // Prepare data for chart const dates = sleepData.map(day => day.date); const totalSleep = sleepData.map(day => day.metrics['Sleep Hours']); const deepSleep = sleepData.map(day => day.metrics['Time In Deep Sleep'] || 0); const lightSleep = sleepData.map(day => day.metrics['Time In Light Sleep'] || 0); const remSleep = sleepData.map(day => day.metrics['Time In REM Sleep'] || 0); // --- Trend Detection for Sleep Duration --- const { slope, trendLine } = this.getLinearTrend(totalSleep); let trendLabel = ''; if (slope > 0.1) { trendLabel = 'Improving Sleep Duration'; } else if (slope < -0.1) { trendLabel = 'Declining Sleep Duration'; } else { trendLabel = 'Stable Sleep Duration'; } // Create container for stacked bar chart const stackedContainer = this.createChartContainer(wrapper); const canvas = document.createElement('canvas'); stackedContainer.appendChild(canvas); // Create chart const chart = new Chart(canvas, { type: 'bar', data: { labels: dates, datasets: [ { label: 'Deep Sleep', data: deepSleep, backgroundColor: this.colors.sleep.deep, stack: 'sleep' }, { label: 'Light Sleep', data: lightSleep, backgroundColor: this.colors.sleep.light, stack: 'sleep' }, { label: 'REM Sleep', data: remSleep, backgroundColor: this.colors.sleep.rem, stack: 'sleep' } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'Sleep Composition by Night' }, tooltip: { mode: 'index', callbacks: { afterTitle: (items) => { const index = items[0].dataIndex; return `Total: ${totalSleep[index]} hours`; } } } }, scales: { x: { stacked: true, title: { display: true, text: 'Date' } }, y: { stacked: true, title: { display: true, text: 'Hours' } } } } }); // Store chart reference this.charts.push(chart); // Create container for trend line chart const trendContainer = this.createChartContainer(wrapper); const trendCanvas = document.createElement('canvas'); trendContainer.appendChild(trendCanvas); // --- Goal Line and Progress --- let goalDataset = null; let goalLabelDiv = null; if (typeof this.sleepGoal === "number" && !isNaN(this.sleepGoal) && this.sleepGoal > 0) { goalDataset = { label: `Goal (${this.sleepGoal}h)`, data: totalSleep.map(() => this.sleepGoal), borderColor: "#2ecc71", backgroundColor: "rgba(46, 204, 113, 0.08)", fill: false, borderDash: [2, 6], pointRadius: 0, tension: 0, order: 0 }; } // Compose datasets for trend chart const trendDatasets = [ { label: 'Sleep Duration', data: totalSleep, borderColor: this.colors.sleep.total, backgroundColor: 'rgba(74, 111, 165, 0.1)', fill: true, tension: 0.3 }, { label: 'Trend', data: trendLine, borderColor: '#ff7e67', backgroundColor: 'rgba(255, 126, 103, 0.08)', fill: false, borderDash: [8, 4], pointRadius: 0, tension: 0 } ]; if (goalDataset) trendDatasets.push(goalDataset); const trendChart = new Chart(trendCanvas, { type: 'line', data: { labels: dates, datasets: trendDatasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'Sleep Duration Trend' }, legend: { display: true } }, scales: { y: { title: { display: true, text: 'Hours' }, min: Math.max(0, Math.min(...totalSleep) - 1), max: Math.max(...totalSleep) + 1 } } } }); // Add trend label below the chart const trendLabelDiv = document.createElement('div'); trendLabelDiv.className = 'trend-label'; trendLabelDiv.style.margin = '8px 0 0 0'; trendLabelDiv.style.fontWeight = 'bold'; trendLabelDiv.style.color = slope > 0.1 ? '#2ecc71' : (slope < -0.1 ? '#e74c3c' : '#3498db'); trendLabelDiv.textContent = trendLabel; trendContainer.appendChild(trendLabelDiv); // Add goal progress below the trend label if goal is set if (goalDataset) { const nightsMet = totalSleep.filter(h => h >= this.sleepGoal).length; const percentMet = Math.round((nightsMet / totalSleep.length) * 100); const goalLabelDiv = document.createElement('div'); goalLabelDiv.className = 'goal-label'; goalLabelDiv.style.margin = '4px 0 16px 0'; goalLabelDiv.style.fontWeight = 'bold'; goalLabelDiv.style.color = '#2ecc71'; goalLabelDiv.textContent = `Goal met on ${nightsMet} of ${totalSleep.length} nights (${percentMet}%)`; trendContainer.appendChild(goalLabelDiv); } // Store chart reference this.charts.push(trendChart); } /** * Create stress levels chart * @param {Array} metricsData - Processed metrics data * @param {HTMLElement} wrapper - Wrapper for the chart containers */ createStressChart(metricsData, wrapper) { // Filter out days without stress data const stressData = metricsData.filter(day => day.metrics['Stress Qualifier'] !== undefined ); if (stressData.length === 0) { this.showNoDataMessage(wrapper, 'No stress data available'); return; } // Prepare data for chart const dates = stressData.map(day => day.date); const stressQualifiers = stressData.map(day => day.metrics['Stress Qualifier']); // Count occurrences of each stress level const stressLevels = ['calm', 'balanced', 'stressful', 'very_stressful']; const stressCounts = {}; stressLevels.forEach(level => { stressCounts[level] = stressQualifiers.filter(q => q === level).length; }); // Create container for pie chart const pieContainer = this.createChartContainer(wrapper); const pieCanvas = document.createElement('canvas'); pieContainer.appendChild(pieCanvas); // Create pie chart const pieChart = new Chart(pieCanvas, { type: 'doughnut', data: { labels: ['Calm', 'Balanced', 'Stressful', 'Very Stressful'], datasets: [{ data: [ stressCounts.calm || 0, stressCounts.balanced || 0, stressCounts.stressful || 0, stressCounts.very_stressful || 0 ], backgroundColor: [ this.colors.stress.calm, this.colors.stress.balanced, this.colors.stress.stressful, this.colors.stress.veryStressful ] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'Stress Level Distribution' }, legend: { position: 'right' } } } }); // Store chart reference this.charts.push(pieChart); // Create container for timeline chart const timelineContainer = this.createChartContainer(wrapper); const timelineCanvas = document.createElement('canvas'); timelineContainer.appendChild(timelineCanvas); // Map stress qualifiers to numeric values for the chart const stressValues = stressQualifiers.map(qualifier => { switch(qualifier) { case 'calm': return 1; case 'balanced': return 2; case 'stressful': return 3; case 'very_stressful': return 4; default: return 0; } }); // --- Trendline and Goal for Stress Level Timeline --- const { slope: stressSlope, trendLine: stressTrendLine } = this.getLinearTrend(stressValues); // Default goal: 2 ("balanced") const stressGoal = 2; const goalDataset = { label: 'Goal (Balanced)', data: stressValues.map(() => stressGoal), borderColor: "#2ecc71", backgroundColor: "rgba(46, 204, 113, 0.08)", fill: false, borderDash: [2, 6], pointRadius: 0, tension: 0, order: 0 }; // Compose datasets for timeline chart const timelineDatasets = [ { label: 'Stress Level', data: stressValues, borderColor: this.colors.accent, backgroundColor: 'rgba(255, 126, 103, 0.1)', fill: true, stepped: true }, { label: 'Trend', data: stressTrendLine, borderColor: '#ff7e67', backgroundColor: 'rgba(255, 126, 103, 0.08)', fill: false, borderDash: [8, 4], pointRadius: 0, tension: 0 }, goalDataset ]; const timelineChart = new Chart(timelineCanvas, { type: 'line', data: { labels: dates, datasets: timelineDatasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'Stress Level Timeline' }, tooltip: { callbacks: { label: (context) => { const value = context.raw; let label = ''; switch(value) { case 1: label = 'Calm'; break; case 2: label = 'Balanced'; break; case 3: label = 'Stressful'; break; case 4: label = 'Very Stressful'; break; default: label = 'Unknown'; } return label; } } }, legend: { display: true } }, scales: { y: { min: 0.5, max: 4.5, title: { display: true, text: 'Stress Level' }, ticks: { callback: (value) => { switch(value) { case 1: return 'Calm'; case 2: return 'Balanced'; case 3: return 'Stressful'; case 4: return 'Very Stressful'; default: return ''; } } } } } } }); // Add trend label below the chart const trendLabelDiv = document.createElement('div'); trendLabelDiv.className = 'trend-label'; trendLabelDiv.style.margin = '8px 0 0 0'; trendLabelDiv.style.fontWeight = 'bold'; trendLabelDiv.style.color = stressSlope < -0.1 ? '#2ecc71' : (stressSlope > 0.1 ? '#e74c3c' : '#3498db'); trendLabelDiv.textContent = stressSlope < -0.1 ? 'Improving Stress Levels' : (stressSlope > 0.1 ? 'Declining Stress Levels' : 'Stable Stress Levels'); timelineContainer.appendChild(trendLabelDiv); // Add goal progress below the trend label const daysMet = stressValues.filter(v => v <= stressGoal).length; const percentMet = Math.round((daysMet / stressValues.length) * 100); const goalLabelDiv = document.createElement('div'); goalLabelDiv.className = 'goal-label'; goalLabelDiv.style.margin = '4px 0 16px 0'; goalLabelDiv.style.fontWeight = 'bold'; goalLabelDiv.style.color = '#2ecc71'; goalLabelDiv.textContent = `Goal met on ${daysMet} of ${stressValues.length} days (${percentMet}%)`; timelineContainer.appendChild(goalLabelDiv); // Store chart reference this.charts.push(timelineChart); } /** * Create workout analysis chart * @param {Array} workoutsData - Processed workouts data * @param {HTMLElement} wrapper - Wrapper for the chart containers */ createWorkoutChart(workoutsData, wrapper) { if (workoutsData.length === 0) { this.showNoDataMessage(wrapper, 'No workout data available'); return; } // Prepare data for chart const dates = workoutsData.map(workout => workout.date); const heartRates = workoutsData.map(workout => workout.heartRate.average); const durations = workoutsData.map(workout => workout.duration * 60); // Convert to minutes // Create container for bar/line chart const barContainer = this.createChartContainer(wrapper); const canvas = document.createElement('canvas'); barContainer.appendChild(canvas); // Create chart const chart = new Chart(canvas, { type: 'bar', data: { labels: dates, datasets: [ { label: 'Duration (minutes)', data: durations, backgroundColor: this.colors.secondary, yAxisID: 'y', order: 1 // Draw bars first (underneath) }, { label: 'Avg Heart Rate (bpm)', data: heartRates, borderColor: this.colors.accent, backgroundColor: 'transparent', // No fill borderWidth: 3, // Thicker line pointRadius: 4, // Visible points pointBackgroundColor: this.colors.accent, pointBorderColor: '#fff', pointHoverRadius: 6, pointHoverBackgroundColor: this.colors.accent, tension: 0.3, // Slightly curved line type: 'line', yAxisID: 'y1', order: 0 // Draw line last (on top) } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { title: { display: true, text: 'Workout Duration and Intensity' }, tooltip: { callbacks: { title: (tooltipItems) => { return `Date: ${tooltipItems[0].label}`; } } } }, scales: { y: { type: 'linear', position: 'left', title: { display: true, text: 'Duration (minutes)' }, min: 0 }, y1: { type: 'linear', position: 'right', title: { display: true, text: 'Heart Rate (bpm)' }, min: Math.max(0, Math.min(...heartRates) - 10), max: Math.max(...heartRates) + 10, grid: { drawOnChartArea: false } } } } }); // Store chart reference this.charts.push(chart); // Create workout type distribution chart if we have type data if (workoutsData.some(workout => workout.type)) { // Count workout types const workoutTypes = {}; workoutsData.forEach(workout => { const type = workout.type || 'Unknown'; workoutTypes[type] = (workoutTypes[type] || 0) + 1; }); // Create container for pie chart const pieContainer = this.createChartContainer(wrapper); const pieCanvas = document.createElement('canvas'); pieContainer.appendChild(pieCanvas); // Create color array based on number of workout types const typeColors = Object.keys(workoutTypes).map((_, index) => { const hue = (index * 137) % 360; // Golden angle approximation for good distribution return `hsl(${hue}, 70%, 60%)`; }); // Create pie chart const pieChart = new Chart(pieCanvas, { type: 'pie', data: { labels: Object.keys(workoutTypes), datasets: [{ data: Object.values(workoutTypes), backgroundColor: typeColors }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'Workout Type Distribution' }, legend: { position: 'right' } } } }); // Store chart reference this.charts.push(pieChart); } } /** * Create HRV and recovery metrics chart * @param {Array} metricsData - Processed metrics data * @param {HTMLElement} wrapper - Wrapper for the chart containers */ createHRVChart(metricsData, wrapper) { // Filter out days without HRV data const hrvData = metricsData.filter(day => day.metrics['HRV'] !== undefined ); if (hrvData.length === 0) { this.showNoDataMessage(wrapper, 'No HRV data available'); return; } // Prepare data for chart const dates = hrvData.map(day => day.date); const hrvValues = hrvData.map(day => day.metrics['HRV']); const pulseValues = hrvData.map(day => day.metrics['Pulse']); // --- Trendline and Goal for HRV and Resting Pulse --- const { slope: hrvSlope, trendLine: hrvTrendLine } = this.getLinearTrend(hrvValues); const { slope: pulseSlope, trendLine: pulseTrendLine } = this.getLinearTrend(pulseValues); // Default goals const hrvGoal = 50; const pulseGoal = 60; const hrvGoalDataset = { label: 'Goal (HRV 50ms)', data: hrvValues.map(() => hrvGoal), borderColor: "#2ecc71", backgroundColor: "rgba(46, 204, 113, 0.08)", fill: false, borderDash: [2, 6], pointRadius: 0, tension: 0, yAxisID: 'y', order: 0 }; const pulseGoalDataset = { label: 'Goal (Pulse 60bpm)', data: pulseValues.map(() => pulseGoal), borderColor: "#3498db", backgroundColor: "rgba(52, 152, 219, 0.08)", fill: false, borderDash: [2, 6], pointRadius: 0, tension: 0, yAxisID: 'y1', order: 0 }; // Compose datasets for HRV/Pulse chart const hrvPulseDatasets = [ { label: 'HRV', data: hrvValues, borderColor: this.colors.primary, backgroundColor: 'rgba(74, 111, 165, 0.1)', fill: true, yAxisID: 'y' }, { label: 'HRV Trend', data: hrvTrendLine, borderColor: '#ff7e67', backgroundColor: 'rgba(255, 126, 103, 0.08)', fill: false, borderDash: [8, 4], pointRadius: 0, tension: 0, yAxisID: 'y' }, hrvGoalDataset, { label: 'Resting Pulse', data: pulseValues, borderColor: this.colors.accent, backgroundColor: 'rgba(255, 126, 103, 0.1)', fill: true, yAxisID: 'y1' }, { label: 'Pulse Trend', data: pulseTrendLine, borderColor: '#8e44ad', backgroundColor: 'rgba(142, 68, 173, 0.08)', fill: false, borderDash: [8, 4], pointRadius: 0, tension: 0, yAxisID: 'y1' }, pulseGoalDataset ]; // Create container for HRV/Pulse chart const hrvContainer = this.createChartContainer(wrapper); const canvas = document.createElement('canvas'); hrvContainer.appendChild(canvas); // Create chart const chart = new Chart(canvas, { type: 'line', data: { labels: dates, datasets: hrvPulseDatasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'HRV and Resting Pulse' }, legend: { display: true } }, scales: { y: { type: 'linear', position: 'left', title: { display: true, text: 'HRV (ms)' } }, y1: { type: 'linear', position: 'right', title: { display: true, text: 'Pulse (bpm)' }, grid: { drawOnChartArea: false } } } } }); // Add trend and goal progress labels below the chart // HRV const hrvTrendLabelDiv = document.createElement('div'); hrvTrendLabelDiv.className = 'trend-label'; hrvTrendLabelDiv.style.margin = '8px 0 0 0'; hrvTrendLabelDiv.style.fontWeight = 'bold'; hrvTrendLabelDiv.style.color = hrvSlope > 0.1 ? '#2ecc71' : (hrvSlope < -0.1 ? '#e74c3c' : '#3498db'); hrvTrendLabelDiv.textContent = hrvSlope > 0.1 ? 'Improving HRV' : (hrvSlope < -0.1 ? 'Declining HRV' : 'Stable HRV'); hrvContainer.appendChild(hrvTrendLabelDiv); const hrvDaysMet = hrvValues.filter(v => v >= hrvGoal).length; const hrvPercentMet = Math.round((hrvDaysMet / hrvValues.length) * 100); const hrvGoalLabelDiv = document.createElement('div'); hrvGoalLabelDiv.className = 'goal-label'; hrvGoalLabelDiv.style.margin = '4px 0 0 0'; hrvGoalLabelDiv.style.fontWeight = 'bold'; hrvGoalLabelDiv.style.color = '#2ecc71'; hrvGoalLabelDiv.textContent = `HRV Goal met on ${hrvDaysMet} of ${hrvValues.length} days (${hrvPercentMet}%)`; hrvContainer.appendChild(hrvGoalLabelDiv); // Pulse const pulseTrendLabelDiv = document.createElement('div'); pulseTrendLabelDiv.className = 'trend-label'; pulseTrendLabelDiv.style.margin = '8px 0 0 0'; pulseTrendLabelDiv.style.fontWeight = 'bold'; pulseTrendLabelDiv.style.color = pulseSlope < -0.1 ? '#2ecc71' : (pulseSlope > 0.1 ? '#e74c3c' : '#3498db'); pulseTrendLabelDiv.textContent = pulseSlope < -0.1 ? 'Improving Resting Pulse' : (pulseSlope > 0.1 ? 'Declining Resting Pulse' : 'Stable Resting Pulse'); hrvContainer.appendChild(pulseTrendLabelDiv); const pulseDaysMet = pulseValues.filter(v => v <= pulseGoal).length; const pulsePercentMet = Math.round((pulseDaysMet / pulseValues.length) * 100); const pulseGoalLabelDiv = document.createElement('div'); pulseGoalLabelDiv.className = 'goal-label'; pulseGoalLabelDiv.style.margin = '4px 0 16px 0'; pulseGoalLabelDiv.style.fontWeight = 'bold'; pulseGoalLabelDiv.style.color = '#2ecc71'; pulseGoalLabelDiv.textContent = `Pulse Goal met on ${pulseDaysMet} of ${pulseValues.length} days (${pulsePercentMet}%)`; hrvContainer.appendChild(pulseGoalLabelDiv); // Store chart reference this.charts.push(chart); // Create body battery chart if data is available const bodyBatteryData = metricsData.filter(day => day.metrics['Body Battery'] !== undefined ); if (bodyBatteryData.length > 0) { // Parse body battery values (format: "Min : X / Max : Y / Avg : Z") const batteryDates = bodyBatteryData.map(day => day.date); const batteryValues = bodyBatteryData.map(day => { const batteryString = day.metrics['Body Battery']; const avgMatch = batteryString.match(/Avg\s*:\s*(\d+)/); return avgMatch ? parseInt(avgMatch[1]) : null; }).filter(val => val !== null); // --- Trendline and Goal for Body Battery --- const { slope: bbSlope, trendLine: bbTrendLine } = this.getLinearTrend(batteryValues); const bbGoal = 80; const bbGoalDataset = { label: 'Goal (80)', data: batteryValues.map(() => bbGoal), borderColor: "#2ecc71", backgroundColor: "rgba(46, 204, 113, 0.08)", fill: false, borderDash: [2, 6], pointRadius: 0, tension: 0, order: 0 }; // Compose datasets for Body Battery chart const bbDatasets = [ { label: 'Body Battery (Average)', data: batteryValues, borderColor: this.colors.secondary, backgroundColor: 'rgba(111, 185, 143, 0.1)', fill: true }, { label: 'Trend', data: bbTrendLine, borderColor: '#ff7e67', backgroundColor: 'rgba(255, 126, 103, 0.08)', fill: false, borderDash: [8, 4], pointRadius: 0, tension: 0 }, bbGoalDataset ]; // Create container for body battery chart const bbContainer = this.createChartContainer(wrapper); const bbCanvas = document.createElement('canvas'); bbContainer.appendChild(bbCanvas); // Create chart const bbChart = new Chart(bbCanvas, { type: 'line', data: { labels: batteryDates, datasets: bbDatasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: 'Body Battery Trend' }, legend: { display: true } }, scales: { y: { min: 0, max: 100, title: { display: true, text: 'Energy Level' } } } } }); // Add trend and goal progress labels below the chart const bbTrendLabelDiv = document.createElement('div'); bbTrendLabelDiv.className = 'trend-label'; bbTrendLabelDiv.style.margin = '8px 0 0 0'; bbTrendLabelDiv.style.fontWeight = 'bold'; bbTrendLabelDiv.style.color = bbSlope > 0.1 ? '#2ecc71' : (bbSlope < -0.1 ? '#e74c3c' : '#3498db'); bbTrendLabelDiv.textContent = bbSlope > 0.1 ? 'Improving Body Battery' : (bbSlope < -0.1 ? 'Declining Body Battery' : 'Stable Body Battery'); bbContainer.appendChild(bbTrendLabelDiv); const bbDaysMet = batteryValues.filter(v => v >= bbGoal).length; const bbPercentMet = Math.round((bbDaysMet / batteryValues.length) * 100); const bbGoalLabelDiv = document.createElement('div'); bbGoalLabelDiv.className = 'goal-label'; bbGoalLabelDiv.style.margin = '4px 0 16px 0'; bbGoalLabelDiv.style.fontWeight = 'bold'; bbGoalLabelDiv.style.color = '#2ecc71'; bbGoalLabelDiv.textContent = `Goal met on ${bbDaysMet} of ${batteryValues.length} days (${bbPercentMet}%)`; bbContainer.appendChild(bbGoalLabelDiv); // Store chart reference this.charts.push(bbChart); } } /** * Show a message when no data is available for a chart * @param {HTMLElement} container - Container element * @param {string} message - Message to display */ showNoDataMessage(container, message) { const messageElement = document.createElement('div'); messageElement.className = 'no-data-message'; messageElement.textContent = message; container.appendChild(messageElement); } /** * Clear all charts */ clearCharts() { // Destroy all chart instances this.charts.forEach(chart => chart.destroy()); this.charts = []; } } // Create a global instance const chartsManager = new ChartsManager();