diff --git a/src/views/pages/marketing/MarketingAnalysis.vue b/src/views/pages/marketing/MarketingAnalysis.vue index b9a9fa1..c85bc3e 100644 --- a/src/views/pages/marketing/MarketingAnalysis.vue +++ b/src/views/pages/marketing/MarketingAnalysis.vue @@ -934,8 +934,8 @@ const v2ReportNormalized = computed(() => { const drawBarValues = (chart) => { const ctx = chart.ctx; - ctx.save(); // Сохраняем состояние контекста - ctx.font = 'bold 12px "Inter", sans-serif'; + ctx.save(); + ctx.font = 'bold 14px "Inter", sans-serif'; // Шрифт покрупнее, как на фото ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; @@ -944,33 +944,33 @@ const drawBarValues = (chart) => { if (!meta.hidden) { meta.data.forEach((bar, index) => { const value = dataset.data[index]; - if (value !== null && value !== undefined && value !== 0) { + if (value !== null && value !== undefined && value != 0) { let label = value.toLocaleString(); - // Добавляем %, если это указано в датасете или значение похоже на процент - if ((dataset.unit === '%') || (typeof dataset.data[index] === 'string' && dataset.data[index].includes('%'))) { + // Если это проценты, добавляем знак % + if ((dataset.unit === '%') || (typeof dataset.data[index] === 'string' && String(dataset.data[index]).includes('%'))) { label = String(value).replace('%', '') + '%'; } const isHorizontal = meta.indexAxis === 'y'; if (isHorizontal) { - // Горизонтальный график: цифра справа от бара или внутри, если бар длинный + // Горизонтальный: цифра справа темная ctx.fillStyle = document.documentElement.classList.contains('dark') ? '#ffffff' : '#1f2937'; ctx.textAlign = 'left'; ctx.fillText(label, bar.x + 8, bar.y); } else { - // Вертикальный график (Требование 1, 2, 3): Цифра ВНУТРИ бара - // Используем белый цвет для контраста на цветном фоне - ctx.fillStyle = '#ffffff'; - - // Если бар слишком маленький, рисуем сверху, иначе внутри + // === ВЕРТИКАЛЬНЫЙ (КАК НА ФОТО) === const barHeight = Math.abs(bar.base - bar.y); - if (barHeight > 20) { - ctx.fillText(label, bar.x, bar.y + (barHeight / 2)); // По центру бара + + // Если столбец достаточно высокий (>25px), рисуем цифру ВНУТРИ белым цветом + if (barHeight > 25) { + ctx.fillStyle = '#ffffff'; + // bar.y - это верхушка столбца. Добавляем 15px, чтобы спустить цифру чуть ниже верха + ctx.fillText(label, bar.x, bar.y + 15); } else { - // Если бар слишком мелкий, рисуем сверху темным цветом + // Если столбец мелкий, рисуем цифру СВЕРХУ темным цветом ctx.fillStyle = document.documentElement.classList.contains('dark') ? '#ffffff' : '#1f2937'; - ctx.fillText(label, bar.x, bar.y - 10); + ctx.fillText(label, bar.x, bar.y - 12); } } } @@ -980,41 +980,40 @@ const drawBarValues = (chart) => { ctx.restore(); }; -// --- 2. НОВЫЕ ОПЦИИ ДЛЯ ГРАФИКОВ (Требование: убрать нижние цифры и оси) --- -const verticalBarChartInsideOptions = { +// Опции: Дизайн "Как на фото" (С цифрами внутри) +const photoStyleBarOptions = { responsive: true, maintainAspectRatio: false, + layout: { padding: { top: 25 } }, // Отступ сверху для тултипов animation: { duration: 500, onComplete: (animation) => drawBarValues(animation.chart) }, plugins: { - legend: { display: false }, + legend: { display: false }, // Легенда не нужна tooltip: { enabled: true, - callbacks: { - label: (context) => `${context.formattedValue}%` - } + callbacks: { label: (ctx) => `${ctx.formattedValue}%` } } }, scales: { y: { - display: false, // Скрываем ось Y полностью + display: true, // Ось Y оставляем (как на фото 0, 10, 20...), но можно поставить false, если надо скрыть beginAtZero: true, - grid: { display: false } + grid: { display: true, drawBorder: false, color: 'rgba(0,0,0,0.05)' }, + ticks: { font: { size: 10 } } }, x: { - grid: { display: false }, + grid: { display: false }, // Вертикальные линии убираем ticks: { - font: { size: 11, weight: '600' }, - color: '#64748b' // Цвет подписей категорий (Высокая цена, Нет отзывов и т.д.) + font: { size: 12, weight: '600' }, + color: '#64748b' } } - }, - layout: { padding: { top: 20, bottom: 0 } } + } }; -// Опции для горизонтальных баров (как на фото 4/5) +// Опции: Горизонтальные бары const horizontalBarChartOptions = { indexAxis: 'y', responsive: true, @@ -1026,59 +1025,69 @@ const horizontalBarChartOptions = { }, plugins: { legend: { display: false } }, scales: { - x: { display: false }, // Скрываем цифры снизу + x: { display: false, beginAtZero: true }, y: { grid: { display: false }, - ticks: { - font: { size: 12, weight: '500' }, - crossAlign: 'far' - } + ticks: { font: { size: 12, weight: '500' }, crossAlign: 'far' } } } }; +// Опции: Круговая диаграмма +const pieChartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: true, position: 'bottom', align: 'center' }, + tooltip: { + callbacks: { label: (context) => context.label + ': ' + context.parsed + '%' } + } + } +}; + +// Опции: Линейный график (Сезонность) +const seasonalityChartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: true, position: 'bottom' } }, + scales: { y: { beginAtZero: true } } +}; + +// Опции: Воронка +const conversionFunnelChartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: true, position: 'bottom' } }, + scales: { + y: { beginAtZero: true, title: { display: true, text: 'Пользователи' } } + } +}; + +// Опции: Потенциал каналов +const channelsPotentialChartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false } }, + scales: { y: { beginAtZero: true, max: 100 } } +}; + +// Хелпер для цветов +const MARKETING_COLORS = ['#4F46E5', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899']; +const getMarketingColors = (count, offset = 0) => { + const safeCount = Math.max(0, Number(count) || 0); + if (safeCount === 0) return []; + return Array.from({ length: safeCount }, (_, i) => MARKETING_COLORS[(i + offset) % MARKETING_COLORS.length]); +}; + const parsePercentageData = (obj) => { if (!obj || typeof obj !== 'object') return { labels: [], values: [] }; - const labels = Object.keys(obj).map(k => k.replace(/_/g, ' ')); // Убираем _ из ключей + const labels = Object.keys(obj).map(k => k.replace(/_/g, ' ')); const values = Object.values(obj).map(v => { if (typeof v === 'string') return parseFloat(v.replace('%', '')); return v; }); return { labels, values }; }; -const competitorsBarChartOptions = { - responsive: true, - maintainAspectRatio: false, - layout: { padding: { top: 20 } }, - animation: { - duration: 500, - onComplete: (animation) => drawBarValues(animation.chart) - }, - plugins: { - legend: { display: false }, - tooltip: { enabled: true } - }, - scales: { - y: { - beginAtZero: true, - grid: { display: true, drawBorder: false }, - ticks: { display: false } // Скрываем ось Y, цифры уже на барах - }, - x: { grid: { display: false } } - } -}; - - - -const channelsBarChartOptions = { - ...competitorsBarChartOptions, - plugins: { - legend: { display: false }, - tooltip: { - callbacks: { label: (ctx) => `${ctx.formattedValue}%` } - } - } -}; const refreshV2 = async () => { if (!v2AnalysisId.value) return; @@ -1772,80 +1781,6 @@ const pushMarkdownTokens = (segments, markdownChunk) => { } }; -const seasonalityChartOptions = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: true, - position: 'bottom', - align: 'center' - } - }, - scales: { - y: { - beginAtZero: true, - max: 100, - ticks: { - callback: function (value) { - return value + '%'; - } - } - } - } -}; - -const conversionFunnelChartOptions = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: true, - position: 'bottom', - align: 'center' - } - }, - scales: { - y: { - beginAtZero: true, - title: { - display: true, - text: 'Количество пользователей' - } - } - } -}; - -const channelsPotentialChartOptions = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: false - } - }, - scales: { - y: { - beginAtZero: true, - max: 100, - ticks: { - callback: function (value) { - return value; - } - } - } - } -}; - -// Marketing-friendly color palette (cycled as needed) -const MARKETING_COLORS = ['#4F46E5', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899']; - -const getMarketingColors = (count, offset = 0) => { - const safeCount = Math.max(0, Number(count) || 0); - if (safeCount === 0) return []; - const base = MARKETING_COLORS.length ? MARKETING_COLORS : ['#3B82F6']; - return Array.from({ length: safeCount }, (_, i) => base[(i + offset) % base.length]); -}; const buildSeasonalityChartData = (seasonality) => { if (!seasonality || typeof seasonality !== 'object') return null; @@ -3570,25 +3505,6 @@ const toCompactChartOptions = (options) => { return compact; }; -const pieChartOptions = { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: true, - position: 'bottom', - align: 'center' - }, - tooltip: { - callbacks: { - label: function (context) { - return context.label + ': ' + context.parsed + '%'; - } - } - } - } -}; - // Get analysis type label const getAnalysisTypeLabel = (type) => { const labels = {