Merge branch 'arys-main-patch-49109' into 'main'
Update MarketingAnalysis.vue See merge request web/marketing!1
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user