diff --git a/expenses_manager/expenses/static/expenses/css/base.css b/expenses_manager/expenses/static/expenses/css/base.css index 93b1c82..22b7b85 100644 --- a/expenses_manager/expenses/static/expenses/css/base.css +++ b/expenses_manager/expenses/static/expenses/css/base.css @@ -37,6 +37,9 @@ --color-table-alt: #f8faf9; --color-table-hover: #e9f1ed; --color-progress-track: #e0e0e0; + + --chart-grid-color: rgba(0, 0, 0, 0.1); + --chart-fill-color: rgba(15, 118, 110, 0.2); } [data-theme="dark"] { @@ -68,6 +71,9 @@ --color-table-alt: #182420; --color-table-hover: #20302a; --color-progress-track: #2a3a33; + + --chart-grid-color: rgba(255, 255, 255, 0.12); + --chart-fill-color: rgba(15, 118, 110, 0.35); } * { diff --git a/expenses_manager/expenses/static/expenses/js/charts.js b/expenses_manager/expenses/static/expenses/js/charts.js new file mode 100644 index 0000000..437e2eb --- /dev/null +++ b/expenses_manager/expenses/static/expenses/js/charts.js @@ -0,0 +1,88 @@ +(function () { + function getActiveTheme() { + return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'; + } + + function getChartColors() { + var styles = getComputedStyle(document.documentElement); + var theme = getActiveTheme(); + + return { + theme: theme, + gridColor: styles.getPropertyValue('--chart-grid-color').trim() || 'rgba(0, 0, 0, 0.1)', + tickColor: styles.getPropertyValue('--color-text-muted').trim() || '#6b6560', + primaryColor: styles.getPropertyValue('--color-primary').trim() || '#0f766e', + primaryFill: styles.getPropertyValue('--chart-fill-color').trim() || 'rgba(15, 118, 110, 0.2)', + palette: ['#ff6384', '#36a2eb', '#cc65fe', '#ffce56', '#4bc0c0', '#ffa1b5'] + }; + } + + // Aplica los colores de grid/ticks a las escalas cartesianas (x/y) de un gráfico. + // Se usa tanto al crear el gráfico (colores embebidos en las opciones iniciales) + // como al refrescar el tema en caliente sobre un gráfico ya existente. + function applyCartesianColors(chart, colors) { + var scales = chart.options.scales; + + if (!scales) { + return; + } + + Object.keys(scales).forEach(function (axis) { + var scale = scales[axis]; + + if (!scale) { + return; + } + + // Object.assign crea un objeto grid/ticks nuevo en vez de mutar en el + // sitio (scale.grid.color = ...). Chart.js reconstruye internamente los + // objetos de opciones de un gráfico 'line' ya creado, y mutar una + // propiedad suelta sobre esa estructura reactiva provoca + // "TypeError: t.startsWith is not a function" al redibujar. Reemplazar + // el objeto entero evita tocar esas referencias internas. + if (typeof colors.gridColor === 'string' && colors.gridColor) { + scale.grid = Object.assign({}, scale.grid, { color: colors.gridColor }); + } + + if (typeof colors.tickColor === 'string' && colors.tickColor) { + scale.ticks = Object.assign({}, scale.ticks, { color: colors.tickColor }); + } + }); + } + + // Los colores de tema se fijan en las opciones al CREAR cada gráfico (evita el + // bug de mutar un gráfico 'line' recién construido). Para que el gráfico + // reaccione a un cambio de tema posterior sin recrearlo, cada plantilla registra + // aquí su instancia junto con un callback que sabe cómo reaplicar sus colores. + var registeredCharts = []; + + function registerThemedChart(chart, applyColors) { + registeredCharts.push({ chart: chart, applyColors: applyColors }); + } + + function refreshThemedCharts() { + var colors = getChartColors(); + + registeredCharts.forEach(function (entry) { + entry.applyColors(entry.chart, colors); + // 'none' desactiva la animación de la actualización: solo repinta con + // los nuevos colores, sin la transición de entrada de datos. + entry.chart.update('none'); + }); + } + + // Observa el atributo data-theme del (lo cambia el toggle de tema) para + // refrescar todos los gráficos registrados en cuanto el usuario cambia de tema, + // sin necesidad de recargar la página. + new MutationObserver(function (mutations) { + var themeChanged = mutations.some(function (m) { return m.attributeName === 'data-theme'; }); + + if (themeChanged) { + refreshThemedCharts(); + } + }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); + + window.getChartColors = getChartColors; + window.applyCartesianColors = applyCartesianColors; + window.registerThemedChart = registerThemedChart; +})(); diff --git a/expenses_manager/expenses/templates/expenses/base.html b/expenses_manager/expenses/templates/expenses/base.html index 4db6d73..ca03b8a 100644 --- a/expenses_manager/expenses/templates/expenses/base.html +++ b/expenses_manager/expenses/templates/expenses/base.html @@ -16,6 +16,7 @@ + {% block extra_css %}{% endblock %} diff --git a/expenses_manager/expenses/templates/expenses/dashboard.html b/expenses_manager/expenses/templates/expenses/dashboard.html index e4b3cb7..04480d9 100644 --- a/expenses_manager/expenses/templates/expenses/dashboard.html +++ b/expenses_manager/expenses/templates/expenses/dashboard.html @@ -259,25 +259,32 @@ const catLabels = [{% for row in by_category_chart %}"{{ row.category__name }}",{% endfor %}]; const catData = [{% for row in by_category_chart %}{{ row.total|unlocalize }},{% endfor %}]; - new Chart(document.getElementById('categoryChart'), { + const categoryChartColors = getChartColors(); + + const categoryChart = new Chart(document.getElementById('categoryChart'), { type: 'pie', data: { labels: catLabels, datasets: [{ data: catData, - backgroundColor: [ - '#ff6384', '#36a2eb', '#cc65fe', '#ffce56', '#4bc0c0', '#ffa1b5' - ] + backgroundColor: categoryChartColors.palette }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { - legend: { position: 'right' } + legend: { + position: 'right', + labels: { color: categoryChartColors.tickColor } + } } } }); + + registerThemedChart(categoryChart, function (chart, colors) { + chart.options.plugins.legend.labels.color = colors.tickColor; + });