Update MarketingAnalysis.vue
This commit is contained in:
@@ -934,8 +934,8 @@ const v2ReportNormalized = computed(() => {
|
|||||||
|
|
||||||
const drawBarValues = (chart) => {
|
const drawBarValues = (chart) => {
|
||||||
const ctx = chart.ctx;
|
const ctx = chart.ctx;
|
||||||
ctx.save(); // Сохраняем состояние контекста
|
ctx.save();
|
||||||
ctx.font = 'bold 12px "Inter", sans-serif';
|
ctx.font = 'bold 14px "Inter", sans-serif'; // Шрифт покрупнее, как на фото
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
|
|
||||||
@@ -944,33 +944,33 @@ const drawBarValues = (chart) => {
|
|||||||
if (!meta.hidden) {
|
if (!meta.hidden) {
|
||||||
meta.data.forEach((bar, index) => {
|
meta.data.forEach((bar, index) => {
|
||||||
const value = dataset.data[index];
|
const value = dataset.data[index];
|
||||||
if (value !== null && value !== undefined && value !== 0) {
|
if (value !== null && value !== undefined && value != 0) {
|
||||||
let label = value.toLocaleString();
|
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('%', '') + '%';
|
label = String(value).replace('%', '') + '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHorizontal = meta.indexAxis === 'y';
|
const isHorizontal = meta.indexAxis === 'y';
|
||||||
|
|
||||||
if (isHorizontal) {
|
if (isHorizontal) {
|
||||||
// Горизонтальный график: цифра справа от бара или внутри, если бар длинный
|
// Горизонтальный: цифра справа темная
|
||||||
ctx.fillStyle = document.documentElement.classList.contains('dark') ? '#ffffff' : '#1f2937';
|
ctx.fillStyle = document.documentElement.classList.contains('dark') ? '#ffffff' : '#1f2937';
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.fillText(label, bar.x + 8, bar.y);
|
ctx.fillText(label, bar.x + 8, bar.y);
|
||||||
} else {
|
} else {
|
||||||
// Вертикальный график (Требование 1, 2, 3): Цифра ВНУТРИ бара
|
// === ВЕРТИКАЛЬНЫЙ (КАК НА ФОТО) ===
|
||||||
// Используем белый цвет для контраста на цветном фоне
|
|
||||||
ctx.fillStyle = '#ffffff';
|
|
||||||
|
|
||||||
// Если бар слишком маленький, рисуем сверху, иначе внутри
|
|
||||||
const barHeight = Math.abs(bar.base - bar.y);
|
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 {
|
} else {
|
||||||
// Если бар слишком мелкий, рисуем сверху темным цветом
|
// Если столбец мелкий, рисуем цифру СВЕРХУ темным цветом
|
||||||
ctx.fillStyle = document.documentElement.classList.contains('dark') ? '#ffffff' : '#1f2937';
|
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();
|
ctx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 2. НОВЫЕ ОПЦИИ ДЛЯ ГРАФИКОВ (Требование: убрать нижние цифры и оси) ---
|
// Опции: Дизайн "Как на фото" (С цифрами внутри)
|
||||||
const verticalBarChartInsideOptions = {
|
const photoStyleBarOptions = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
layout: { padding: { top: 25 } }, // Отступ сверху для тултипов
|
||||||
animation: {
|
animation: {
|
||||||
duration: 500,
|
duration: 500,
|
||||||
onComplete: (animation) => drawBarValues(animation.chart)
|
onComplete: (animation) => drawBarValues(animation.chart)
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: false },
|
legend: { display: false }, // Легенда не нужна
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
callbacks: {
|
callbacks: { label: (ctx) => `${ctx.formattedValue}%` }
|
||||||
label: (context) => `${context.formattedValue}%`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
display: false, // Скрываем ось Y полностью
|
display: true, // Ось Y оставляем (как на фото 0, 10, 20...), но можно поставить false, если надо скрыть
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
grid: { display: false }
|
grid: { display: true, drawBorder: false, color: 'rgba(0,0,0,0.05)' },
|
||||||
|
ticks: { font: { size: 10 } }
|
||||||
},
|
},
|
||||||
x: {
|
x: {
|
||||||
grid: { display: false },
|
grid: { display: false }, // Вертикальные линии убираем
|
||||||
ticks: {
|
ticks: {
|
||||||
font: { size: 11, weight: '600' },
|
font: { size: 12, weight: '600' },
|
||||||
color: '#64748b' // Цвет подписей категорий (Высокая цена, Нет отзывов и т.д.)
|
color: '#64748b'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
layout: { padding: { top: 20, bottom: 0 } }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Опции для горизонтальных баров (как на фото 4/5)
|
// Опции: Горизонтальные бары
|
||||||
const horizontalBarChartOptions = {
|
const horizontalBarChartOptions = {
|
||||||
indexAxis: 'y',
|
indexAxis: 'y',
|
||||||
responsive: true,
|
responsive: true,
|
||||||
@@ -1026,59 +1025,69 @@ const horizontalBarChartOptions = {
|
|||||||
},
|
},
|
||||||
plugins: { legend: { display: false } },
|
plugins: { legend: { display: false } },
|
||||||
scales: {
|
scales: {
|
||||||
x: { display: false }, // Скрываем цифры снизу
|
x: { display: false, beginAtZero: true },
|
||||||
y: {
|
y: {
|
||||||
grid: { display: false },
|
grid: { display: false },
|
||||||
ticks: {
|
ticks: { font: { size: 12, weight: '500' }, crossAlign: 'far' }
|
||||||
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) => {
|
const parsePercentageData = (obj) => {
|
||||||
if (!obj || typeof obj !== 'object') return { labels: [], values: [] };
|
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 => {
|
const values = Object.values(obj).map(v => {
|
||||||
if (typeof v === 'string') return parseFloat(v.replace('%', ''));
|
if (typeof v === 'string') return parseFloat(v.replace('%', ''));
|
||||||
return v;
|
return v;
|
||||||
});
|
});
|
||||||
return { labels, values };
|
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 () => {
|
const refreshV2 = async () => {
|
||||||
if (!v2AnalysisId.value) return;
|
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) => {
|
const buildSeasonalityChartData = (seasonality) => {
|
||||||
if (!seasonality || typeof seasonality !== 'object') return null;
|
if (!seasonality || typeof seasonality !== 'object') return null;
|
||||||
@@ -3570,25 +3505,6 @@ const toCompactChartOptions = (options) => {
|
|||||||
return compact;
|
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
|
// Get analysis type label
|
||||||
const getAnalysisTypeLabel = (type) => {
|
const getAnalysisTypeLabel = (type) => {
|
||||||
const labels = {
|
const labels = {
|
||||||
|
|||||||
Reference in New Issue
Block a user