/** * Chart.js configurations for WeightTracker * Uses a consistent dark theme with accent colors */ const chartDefaults = { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false, }, tooltip: { backgroundColor: '#1a2233', titleColor: '#f1f5f9', bodyColor: '#94a3b8', borderColor: '#2a3548', borderWidth: 1, cornerRadius: 8, padding: 10, displayColors: false, }, }, scales: { x: { grid: { color: 'rgba(42, 53, 72, 0.5)', drawBorder: false }, ticks: { color: '#64748b', font: { size: 11, family: 'Inter' } }, }, y: { grid: { color: 'rgba(42, 53, 72, 0.5)', drawBorder: false }, ticks: { color: '#64748b', font: { size: 11, family: 'Inter' } }, }, }, }; function createWeightChart(canvasId, labels, weights) { const ctx = document.getElementById(canvasId); if (!ctx) return; new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [{ label: 'Weight (kg)', data: weights, borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', fill: true, tension: 0.3, pointBackgroundColor: '#3b82f6', pointBorderColor: '#1a2233', pointBorderWidth: 2, pointRadius: 4, pointHoverRadius: 6, }], }, options: { ...chartDefaults, plugins: { ...chartDefaults.plugins, tooltip: { ...chartDefaults.plugins.tooltip, callbacks: { label: ctx => `${ctx.parsed.y} kg`, }, }, }, }, }); } function createWeeklyChangeChart(canvasId, labels, changes) { const ctx = document.getElementById(canvasId); if (!ctx) return; const colors = changes.map(c => c <= 0 ? '#10b981' : '#ef4444'); const bgColors = changes.map(c => c <= 0 ? 'rgba(16, 185, 129, 0.7)' : 'rgba(239, 68, 68, 0.7)'); new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ label: 'Change (kg)', data: changes, backgroundColor: bgColors, borderColor: colors, borderWidth: 1, borderRadius: 4, }], }, options: { ...chartDefaults, plugins: { ...chartDefaults.plugins, tooltip: { ...chartDefaults.plugins.tooltip, callbacks: { label: ctx => `${ctx.parsed.y > 0 ? '+' : ''}${ctx.parsed.y} kg`, }, }, }, }, }); } function createComparisonChart(canvasId, names, pctLost) { const ctx = document.getElementById(canvasId); if (!ctx) return; const hues = [210, 160, 270, 30, 340, 190]; const bgColors = pctLost.map((_, i) => `hsla(${hues[i % hues.length]}, 70%, 55%, 0.7)`); const borderColors = pctLost.map((_, i) => `hsl(${hues[i % hues.length]}, 70%, 55%)`); new Chart(ctx, { type: 'bar', data: { labels: names, datasets: [{ label: '% Lost', data: pctLost, backgroundColor: bgColors, borderColor: borderColors, borderWidth: 1, borderRadius: 6, }], }, options: { ...chartDefaults, indexAxis: names.length > 5 ? 'y' : 'x', plugins: { ...chartDefaults.plugins, tooltip: { ...chartDefaults.plugins.tooltip, callbacks: { label: ctx => `${ctx.parsed.y || ctx.parsed.x}% lost`, }, }, }, }, }); } /** * Create a multi-series progress-over-time line chart with best-fit lines. * @param {string} canvasId - canvas element ID * @param {Array} users - array of { id, name, color, data: [{date, pct_lost}], best_fit: {slope, intercept} } * @returns {Chart} the Chart.js instance */ function createProgressChart(canvasId, users) { const ctx = document.getElementById(canvasId); if (!ctx) return null; const datasets = []; users.forEach(user => { // Actual data line datasets.push({ label: user.name, data: user.data.map(d => ({ x: d.date, y: d.weight })), borderColor: user.color, backgroundColor: user.color.replace(')', ', 0.1)').replace('hsl(', 'hsla('), fill: false, tension: 0.3, pointBackgroundColor: user.color, pointBorderColor: '#1a2233', pointBorderWidth: 2, pointRadius: 4, pointHoverRadius: 6, borderWidth: 2.5, }); // Best-fit line (dashed) — only if we have ≥ 2 data points if (user.data.length >= 2) { const baseDate = new Date(user.data[0].date); const firstDay = 0; const lastDay = Math.round( (new Date(user.data[user.data.length - 1].date) - baseDate) / 86400000 ); const fitStart = user.best_fit.intercept + user.best_fit.slope * firstDay; const fitEnd = user.best_fit.intercept + user.best_fit.slope * lastDay; datasets.push({ label: user.name + ' (trend)', data: [ { x: user.data[0].date, y: Math.round(fitStart * 100) / 100 }, { x: user.data[user.data.length - 1].date, y: Math.round(fitEnd * 100) / 100 }, ], borderColor: user.color, borderDash: [6, 4], borderWidth: 2, pointRadius: 0, pointHoverRadius: 0, fill: false, tension: 0, }); } }); return new Chart(ctx, { type: 'line', data: { datasets }, options: { ...chartDefaults, scales: { x: { type: 'time', time: { unit: 'day', tooltipFormat: 'dd MMM yyyy', displayFormats: { day: 'dd MMM' }, }, grid: { color: 'rgba(42, 53, 72, 0.5)', drawBorder: false }, ticks: { color: '#64748b', font: { size: 11, family: 'Inter' } }, }, y: { ...chartDefaults.scales.y, title: { display: true, text: 'Weight (kg)', color: '#64748b', font: { size: 12, family: 'Inter' }, }, }, }, plugins: { ...chartDefaults.plugins, legend: { display: true, labels: { color: '#94a3b8', font: { size: 12, family: 'Inter' }, usePointStyle: true, pointStyle: 'circle', padding: 16, // Hide trend lines from legend filter: item => !item.text.endsWith('(trend)'), }, }, tooltip: { ...chartDefaults.plugins.tooltip, callbacks: { label: ctx => `${ctx.dataset.label}: ${ctx.parsed.y} kg`, }, }, }, }, }); }