Update MarketingAnalysis.vue

This commit is contained in:
2026-01-19 12:06:50 +00:00
parent f003bd0498
commit ebef3d08bd
+262 -15
View File
@@ -932,6 +932,92 @@ const v2ReportNormalized = computed(() => {
};
});
const drawBarValues = (chart) => {
const ctx = chart.ctx;
ctx.font = 'bold 11px "Inter", sans-serif';
ctx.fillStyle = document.documentElement.classList.contains('dark') ? '#ffffff' : '#1f2937';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
chart.data.datasets.forEach((dataset, i) => {
const meta = chart.getDatasetMeta(i);
if (!meta.hidden) {
meta.data.forEach((bar, index) => {
const value = dataset.data[index];
if (value !== null && value !== undefined && value !== 0) {
let label = value.toLocaleString();
// Добавляем % если это проценты
if ((dataset.unit === '%') || (typeof value === 'string' && value.includes('%'))) {
label = String(value).replace('%', '') + '%';
}
const isHorizontal = meta.indexAxis === 'y';
if (isHorizontal) {
// Для горизонтальных (Доход): рисуем справа
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(label, bar.x + 5, bar.y);
} else {
// Для вертикальных: рисуем сверху
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText(label, bar.x, bar.y - 5);
}
}
});
}
});
};
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 horizontalBarChartOptions = {
indexAxis: 'y', // <-- Делает график горизонтальным
responsive: true,
maintainAspectRatio: false,
layout: { padding: { right: 45 } },
animation: {
duration: 500,
onComplete: (animation) => drawBarValues(animation.chart)
},
plugins: { legend: { display: false } },
scales: {
x: { display: false, beginAtZero: true }, // Скрываем ось X
y: { grid: { display: false } }
}
};
const channelsBarChartOptions = {
...competitorsBarChartOptions,
plugins: {
legend: { display: false },
tooltip: {
callbacks: { label: (ctx) => `${ctx.formattedValue}%` }
}
}
};
const refreshV2 = async () => {
if (!v2AnalysisId.value) return;
v2Loading.value = true;
@@ -1906,6 +1992,58 @@ const buildCompetitorsHeatmapData = (competitors) => {
};
};
// --- ВСТАВИТЬ СЮДА ---
// Сборка данных для ДОХОДА (Фото 2)
const buildIncomeChartData = (targetAudience) => {
// Данные могут быть в snake_case или camelCase
const incomeData = targetAudience?.income_by_age || targetAudience?.incomeByAge;
if (!incomeData || typeof incomeData !== 'object') return null;
const labels = Object.keys(incomeData);
const values = Object.values(incomeData);
return {
labels: labels,
datasets: [
{
label: 'Средний доход (KZT)',
data: values,
backgroundColor: getMarketingColors(values.length),
borderRadius: 4
}
]
};
};
const buildChannelsChartData = (acquisitionChannels) => {
const shares = acquisitionChannels?.channel_shares || acquisitionChannels?.channelShares;
if (!shares || typeof shares !== 'object') return null;
// Превращаем ключи "instagram_target" в "Instagram Target"
const labels = Object.keys(shares).map(key =>
key.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')
);
// Чистим значения от знака % для графика
const values = Object.values(shares).map(val =>
(typeof val === 'string') ? parseFloat(val.replace('%', '')) : val
);
return {
labels: labels,
datasets: [
{
label: 'Доля',
data: values,
unit: '%', // Маркер, чтобы рисовать %
backgroundColor: getMarketingColors(values.length, 2),
borderRadius: 6
}
]
};
};
// Get heatmap color intensity based on value (0-100)
const getHeatmapColor = (value) => {
if (value >= 80) return 'rgba(16, 185, 129, 0.9)'; // green-500
@@ -1973,6 +2111,124 @@ const mapChartsDataToSegments = (chartsData) => {
const segments = [];
// ==========================================
// 1. НОВАЯ ЛОГИКА (V2 JSON - Исправления графиков)
// ==========================================
// --- График ДОХОДА (Горизонтальный) и ВОЗРАСТА ---
if (chartsData.target_audience || chartsData.targetAudience) {
const source = chartsData.target_audience || chartsData.targetAudience;
// 1.1 Доход (Горизонтальный)
const incomeChart = buildIncomeChartData(source);
if (incomeChart) {
segments.push({
type: 'chart',
key: 'incomeByAge',
title: 'Средний доход по возрастным группам',
chartType: 'bar',
data: incomeChart,
options: horizontalBarChartOptions, // <-- Горизонтальный вид
chartHeight: 'height: 350px'
});
}
// 1.2 Возрастная структура (Цифры на барах)
const ageStruct = source.age_structure || source.ageStructure;
if (ageStruct) {
const labels = Object.keys(ageStruct);
const values = Object.values(ageStruct).map(v => parseFloat(String(v).replace('%','')));
segments.push({
type: 'chart',
key: 'ageStructure',
title: 'Возрастная структура',
chartType: 'bar',
data: {
labels,
datasets: [{
data: values,
backgroundColor: getMarketingColors(values.length),
unit: '%'
}]
},
options: competitorsBarChartOptions, // Цифры сверху
chartHeight: 'height: 300px'
});
}
}
// --- График КАНАЛОВ (Динамический) ---
if (chartsData.acquisition_channels || chartsData.acquisitionChannels) {
const source = chartsData.acquisition_channels || chartsData.acquisitionChannels;
const channelsChart = buildChannelsChartData(source);
if (channelsChart) {
segments.push({
type: 'chart',
key: 'acquisitionChannels',
title: 'Каналы привлечения',
chartType: 'bar',
data: channelsChart,
options: channelsBarChartOptions, // Вертикальные с %
chartHeight: 'height: 350px'
});
}
}
// --- Факторы принятия решений и Причины отказов ---
if (chartsData.demand_and_behavior || chartsData.demandAndBehavior) {
const source = chartsData.demand_and_behavior || chartsData.demandAndBehavior;
// Факторы
const factors = source.decision_factors || source.decisionFactors;
if (factors) {
const labels = Object.keys(factors).map(k => k.replace(/_/g, ' '));
const values = Object.values(factors).map(v => parseFloat(String(v).replace('%','')));
segments.push({
type: 'chart',
key: 'decisionFactors',
title: 'Факторы принятия решения',
chartType: 'bar',
data: {
labels,
datasets: [{
data: values,
backgroundColor: '#8B5CF6',
unit: '%'
}]
},
options: competitorsBarChartOptions, // Цифры сверху
chartHeight: 'height: 300px'
});
}
// Причины отказов
const refusals = source.reasons_for_refusal || source.reasonsForRefusal;
if (refusals) {
const labels = Object.keys(refusals).map(k => k.replace(/_/g, ' '));
const values = Object.values(refusals).map(v => parseFloat(String(v).replace('%','')));
segments.push({
type: 'chart',
key: 'refusalReasons',
title: 'Причины отказов',
chartType: 'bar',
data: {
labels,
datasets: [{
data: values,
backgroundColor: '#EF4444',
unit: '%'
}]
},
options: competitorsBarChartOptions, // Цифры сверху
chartHeight: 'height: 300px'
});
}
}
// ==========================================
// 2. СТАРАЯ ЛОГИКА (Legacy Support)
// ==========================================
// Handle case when chartsData is an array (shouldn't happen, but handle it)
if (Array.isArray(chartsData)) {
// If it's an array of competitors, process it
@@ -2006,7 +2262,6 @@ const mapChartsDataToSegments = (chartsData) => {
}
// Handle case when chartsData is an audienceSegmentation object directly
// (has genders, ageGroups, segments, etc. but no audienceSegmentation property)
if (chartsData.genders || chartsData.ageGroups || chartsData.segments) {
const { ageGroupsChartData, gendersChartData } = buildAudienceSegmentationChartData(chartsData);
if (ageGroupsChartData || gendersChartData) {
@@ -2017,8 +2272,6 @@ const mapChartsDataToSegments = (chartsData) => {
segmentsChannelMatrix: buildSegmentsChannelMatrix(chartsData.segmentsChannelMatrix),
keyTakeaways: chartsData.keyTakeaways || []
});
// Return early to avoid processing it again as audienceSegmentation property
// But continue to check for other properties like competitors
}
}
@@ -2054,20 +2307,16 @@ const mapChartsDataToSegments = (chartsData) => {
}
// 3. Competitors Analysis
// Check if competitors is an array (new format) or object with competitors array (old format)
if (chartsData.competitors) {
let competitorsArray = null;
if (Array.isArray(chartsData.competitors)) {
// New format: competitors is directly an array
competitorsArray = chartsData.competitors;
} else if (chartsData.competitors.competitors && Array.isArray(chartsData.competitors.competitors)) {
// Old format: competitors is an object with competitors array inside
competitorsArray = chartsData.competitors.competitors;
}
if (competitorsArray && competitorsArray.length > 0) {
// Build bar charts for reach and followers using flat structure
const reachChartData = buildCompetitorsBarChartData(competitorsArray, 'reach');
const followersChartData = buildCompetitorsBarChartData(competitorsArray, 'followers');
@@ -2094,7 +2343,6 @@ const mapChartsDataToSegments = (chartsData) => {
});
}
// SWOT Competitors (if exists in competitors object)
if (chartsData.competitors.swotCompetitors && typeof chartsData.competitors.swotCompetitors === 'object') {
segments.push({
type: 'swotCompetitors',
@@ -2106,7 +2354,7 @@ const mapChartsDataToSegments = (chartsData) => {
}
}
// 4. Comparison Table (in root of chartsData)
// 4. Comparison Table
if (chartsData.comparisonTable && Array.isArray(chartsData.comparisonTable) && chartsData.comparisonTable.length > 0) {
segments.push({
type: 'comparisonTable',
@@ -2114,7 +2362,7 @@ const mapChartsDataToSegments = (chartsData) => {
});
}
// 5. SWOT Competitors (in root of chartsData, if exists)
// 5. SWOT Competitors
if (chartsData.swotCompetitors && typeof chartsData.swotCompetitors === 'object') {
segments.push({
type: 'swotCompetitors',
@@ -2149,7 +2397,7 @@ const mapChartsDataToSegments = (chartsData) => {
});
}
// 8. Conversion Funnel (supports Chart.js format too)
// 8. Conversion Funnel
if (chartsData.conversionFunnel) {
const chartData = isChartJsData(chartsData.conversionFunnel) ? normalizeChartJsData(chartsData.conversionFunnel, 'bar') : buildConversionFunnelChartData(chartsData.conversionFunnel);
if (chartData) {
@@ -2165,7 +2413,7 @@ const mapChartsDataToSegments = (chartsData) => {
}
}
// 9. Channels Potential (supports Chart.js format too)
// 9. Channels Potential
if (chartsData.channelsPotential) {
const chartData = isChartJsData(chartsData.channelsPotential) ? normalizeChartJsData(chartsData.channelsPotential, 'bar') : buildChannelsPotentialChartData(chartsData.channelsPotential);
if (chartData) {
@@ -2182,7 +2430,7 @@ const mapChartsDataToSegments = (chartsData) => {
}
}
// 11. Audience charts (Chart.js config)
// 11. Audience charts (Legacy)
if (chartsData.audienceAge) {
const chartData = normalizeChartJsData(chartsData.audienceAge, 'bar');
if (chartData) {
@@ -2212,7 +2460,7 @@ const mapChartsDataToSegments = (chartsData) => {
}
}
// 10. SWOT (if exists for backward compatibility)
// 10. SWOT
if (chartsData.swot && typeof chartsData.swot === 'object') {
segments.push({
type: 'swot',
@@ -2227,7 +2475,6 @@ const mapChartsDataToSegments = (chartsData) => {
return segments;
};
const mapJsonBlockToSegment = (identifier, data, rawText) => {
const key = identifier?.toLowerCase();
switch (key) {