/** * Pricify - Charts Module * Chart.js-based chart creation with dark theme defaults. * Uses Chart.js from CDN (https://cdn.jsdelivr.net/npm/chart.js) * Vanilla JS, no frameworks, no module exports. */ /* ============================================================ Dark Theme Defaults (applied to all charts) ============================================================ */ /** * Register Chart.js dark theme defaults once. * Call early — before any chart init — to set baseline colours * for fonts, grid lines, and tooltips across the whole page. */ function applyDarkChartDefaults() { if (typeof Chart === 'undefined') return; Chart.defaults.color = '#9da1a8'; Chart.defaults.borderColor = 'rgba(42,46,57,0.6)'; Chart.defaults.font.family = "system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"; Chart.defaults.plugins.tooltip.backgroundColor = '#1e222d'; Chart.defaults.plugins.tooltip.titleColor = '#d4af37'; Chart.defaults.plugins.tooltip.bodyColor = '#d1d4dc'; Chart.defaults.plugins.tooltip.borderColor = '#2a2e39'; Chart.defaults.plugins.tooltip.borderWidth = 1; Chart.defaults.plugins.tooltip.padding = 12; Chart.defaults.plugins.tooltip.cornerRadius = 8; Chart.defaults.plugins.tooltip.displayColors = true; Chart.defaults.plugins.legend.display = false; } /* ============================================================ initPriceChart — Full-size price-history line chart ------------------------------------------------------------ canvasId : element id labels : Array of date/time labels (strings) prices : Array of price values (numbers) color : CSS colour string for the line (e.g. '#2962ff') ============================================================ */ /** * Create a price-history line chart with a gradient fill below * the line, dark theme, and responsive sizing. * Returns the Chart.js instance for later updating. */ function initPriceChart(canvasId, labels, prices, color) { try { const canvas = document.getElementById(canvasId); if (!canvas) { console.warn(`[charts] Canvas #${canvasId} not found`); return null; } if (typeof Chart === 'undefined') { console.warn('[charts] Chart.js not loaded'); return null; } applyDarkChartDefaults(); // Build the gradient fill (same colour, fading to transparent) const ctx = canvas.getContext('2d'); const gradient = ctx.createLinearGradient(0, 0, 0, canvas.offsetHeight * 1.2); gradient.addColorStop(0, color + '40'); // 25 % opacity at top gradient.addColorStop(0.6, color + '10'); // ~6 % mid gradient.addColorStop(1, color + '00'); // transparent at bottom return new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [ { data: prices, borderColor: color, backgroundColor: gradient, borderWidth: 2, pointRadius: 0, pointHitRadius: 10, pointHoverRadius: 5, pointHoverBackgroundColor: color, pointHoverBorderColor: '#d1d4dc', pointHoverBorderWidth: 2, fill: true, tension: 0.2, }, ], }, options: { responsive: true, maintainAspectRatio: true, animation: { duration: 600 }, interaction: { intersect: false, mode: 'index', }, plugins: { legend: { display: false }, tooltip: { enabled: true, displayColors: false, callbacks: { label: function (context) { return formatPrice(context.parsed.y); }, }, }, }, scales: { x: { display: true, grid: { display: true, color: 'rgba(42,46,57,0.4)', drawBorder: false, }, ticks: { color: '#5a5f6a', maxTicksLimit: 10, font: { size: 11 }, }, }, y: { display: true, grid: { display: true, color: 'rgba(42,46,57,0.4)', drawBorder: false, }, ticks: { color: '#5a5f6a', font: { size: 11 }, callback: function (value) { return formatPrice(value); }, }, }, }, // Inner chart area background backgroundColor: 'transparent', }, }); } catch (err) { console.error('[charts] initPriceChart error:', err); return null; } } /* ============================================================ initMiniChart — Sparkline for table cells ------------------------------------------------------------ canvasId : element id (inside .price-sparkline) prices : Array of price values (numbers) color : CSS colour for the line ============================================================ */ /** * Create a tiny sparkline (mini chart) for inline use in listing * tables. No axes, no labels, no legend — just the line. * Returns the Chart.js instance. */ function initMiniChart(canvasId, prices, color) { try { const canvas = document.getElementById(canvasId); if (!canvas) { console.warn(`[charts] Mini chart canvas #${canvasId} not found`); return null; } if (typeof Chart === 'undefined') { console.warn('[charts] Chart.js not loaded'); return null; } applyDarkChartDefaults(); const ctx = canvas.getContext('2d'); const gradient = ctx.createLinearGradient(0, 0, 0, 28); gradient.addColorStop(0, color + '30'); gradient.addColorStop(1, color + '00'); return new Chart(ctx, { type: 'line', data: { labels: prices.map(() => ''), datasets: [ { data: prices, borderColor: color, backgroundColor: gradient, borderWidth: 1.5, pointRadius: 0, pointHitRadius: 0, fill: true, tension: 0.3, }, ], }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 400 }, plugins: { legend: { display: false }, tooltip: { enabled: false }, }, scales: { x: { display: false }, y: { display: false }, }, elements: { point: { radius: 0 }, }, }, }); } catch (err) { console.error('[charts] initMiniChart error:', err); return null; } } /* ============================================================ initPieChart — Market-cap / distribution pie chart ------------------------------------------------------------ canvasId : element id labels : Segment labels (strings) data : Segment values (numbers) colors : Array of CSS colour strings for each segment ============================================================ */ /** * Create a doughnut-style distribution chart (market cap, sector * allocation, etc.) with dark theme. * Returns the Chart.js instance. */ function initPieChart(canvasId, labels, data, colors) { try { const canvas = document.getElementById(canvasId); if (!canvas) { console.warn(`[charts] Pie chart canvas #${canvasId} not found`); return null; } if (typeof Chart === 'undefined') { console.warn('[charts] Chart.js not loaded'); return null; } applyDarkChartDefaults(); const ctx = canvas.getContext('2d'); return new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [ { data: data, backgroundColor: colors, borderColor: '#131722', borderWidth: 2, hoverOffset: 8, }, ], }, options: { responsive: true, maintainAspectRatio: true, cutout: '55%', animation: { duration: 500 }, plugins: { legend: { display: true, position: 'bottom', labels: { color: '#9da1a8', padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 12 }, }, }, tooltip: { enabled: true, displayColors: true, callbacks: { label: function (context) { const total = context.dataset.data.reduce((a, b) => a + b, 0); const pct = ((context.parsed / total) * 100).toFixed(1); return ` ${context.label}: ${formatPrice(context.parsed)} (${pct}%)`; }, }, }, }, }, }); } catch (err) { console.error('[charts] initPieChart error:', err); return null; } } /* ============================================================ Utility — formatPrice Inline helper so charts.js is fully independent. ============================================================ */ /** * Format a number for price display (commas, 2 decimals). * Falls back to raw value if formatting fails. */ function formatPrice(value) { try { if (value === null || value === undefined || isNaN(value)) return '—'; // For very small numbers (crypto), show more decimals if (Math.abs(value) < 0.01) { return value.toFixed(6); } return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2, }); } catch (_) { return String(value); } } /* ============================================================ Auto-init on DOMContentLoaded ============================================================ */ document.addEventListener('DOMContentLoaded', function () { // Apply dark theme defaults early if Chart is loaded if (typeof Chart !== 'undefined') { applyDarkChartDefaults(); } });