Update MarketingAnalysis.vue
This commit is contained in:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user