Update MarketingAnalysis.vue
This commit is contained in:
@@ -934,10 +934,10 @@ const v2ReportNormalized = computed(() => {
|
|||||||
|
|
||||||
const drawBarValues = (chart) => {
|
const drawBarValues = (chart) => {
|
||||||
const ctx = chart.ctx;
|
const ctx = chart.ctx;
|
||||||
ctx.font = 'bold 11px "Inter", sans-serif';
|
ctx.save(); // Сохраняем состояние контекста
|
||||||
ctx.fillStyle = document.documentElement.classList.contains('dark') ? '#ffffff' : '#1f2937';
|
ctx.font = 'bold 12px "Inter", sans-serif';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'bottom';
|
ctx.textBaseline = 'middle';
|
||||||
|
|
||||||
chart.data.datasets.forEach((dataset, i) => {
|
chart.data.datasets.forEach((dataset, i) => {
|
||||||
const meta = chart.getDatasetMeta(i);
|
const meta = chart.getDatasetMeta(i);
|
||||||
@@ -946,30 +946,106 @@ const drawBarValues = (chart) => {
|
|||||||
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 value === 'string' && value.includes('%'))) {
|
if ((dataset.unit === '%') || (typeof dataset.data[index] === '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.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.fillText(label, bar.x + 8, bar.y);
|
||||||
ctx.fillText(label, bar.x + 5, bar.y);
|
|
||||||
} else {
|
} else {
|
||||||
// Для вертикальных: рисуем сверху
|
// Вертикальный график (Требование 1, 2, 3): Цифра ВНУТРИ бара
|
||||||
ctx.textAlign = 'center';
|
// Используем белый цвет для контраста на цветном фоне
|
||||||
ctx.textBaseline = 'bottom';
|
ctx.fillStyle = '#ffffff';
|
||||||
ctx.fillText(label, bar.x, bar.y - 5);
|
|
||||||
|
// Если бар слишком маленький, рисуем сверху, иначе внутри
|
||||||
|
const barHeight = Math.abs(bar.base - bar.y);
|
||||||
|
if (barHeight > 20) {
|
||||||
|
ctx.fillText(label, bar.x, bar.y + (barHeight / 2)); // По центру бара
|
||||||
|
} else {
|
||||||
|
// Если бар слишком мелкий, рисуем сверху темным цветом
|
||||||
|
ctx.fillStyle = document.documentElement.classList.contains('dark') ? '#ffffff' : '#1f2937';
|
||||||
|
ctx.fillText(label, bar.x, bar.y - 10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
ctx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- 2. НОВЫЕ ОПЦИИ ДЛЯ ГРАФИКОВ (Требование: убрать нижние цифры и оси) ---
|
||||||
|
const verticalBarChartInsideOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
animation: {
|
||||||
|
duration: 500,
|
||||||
|
onComplete: (animation) => drawBarValues(animation.chart)
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
enabled: true,
|
||||||
|
callbacks: {
|
||||||
|
label: (context) => `${context.formattedValue}%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
display: false, // Скрываем ось Y полностью
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: { display: false }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: {
|
||||||
|
font: { size: 11, weight: '600' },
|
||||||
|
color: '#64748b' // Цвет подписей категорий (Высокая цена, Нет отзывов и т.д.)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layout: { padding: { top: 20, bottom: 0 } }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Опции для горизонтальных баров (как на фото 4/5)
|
||||||
|
const horizontalBarChartOptions = {
|
||||||
|
indexAxis: 'y',
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
layout: { padding: { right: 40 } },
|
||||||
|
animation: {
|
||||||
|
duration: 500,
|
||||||
|
onComplete: (animation) => drawBarValues(animation.chart)
|
||||||
|
},
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
x: { display: false }, // Скрываем цифры снизу
|
||||||
|
y: {
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: {
|
||||||
|
font: { size: 12, weight: '500' },
|
||||||
|
crossAlign: 'far'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsePercentageData = (obj) => {
|
||||||
|
if (!obj || typeof obj !== 'object') return { labels: [], values: [] };
|
||||||
|
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 = {
|
const competitorsBarChartOptions = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@@ -2089,151 +2165,207 @@ const mapChartsDataToSegments = (chartsData) => {
|
|||||||
|
|
||||||
const segments = [];
|
const segments = [];
|
||||||
|
|
||||||
|
// Нормализация: работаем либо с полем sections (новый бэк), либо с корнем (если пришло без обертки)
|
||||||
|
const sections = chartsData.sections || chartsData;
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 1. НОВАЯ ЛОГИКА (V2 JSON - Исправления графиков)
|
// 1. НОВАЯ ЛОГИКА (V2 JSON + Новый Дизайн)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
// --- График ДОХОДА (Горизонтальный) и ВОЗРАСТА ---
|
// --- Блок: Спрос и поведение (Фото 1: Причины отказов, Фото 6: Факторы) ---
|
||||||
if (chartsData.target_audience || chartsData.targetAudience) {
|
const demandSection = sections.demand_and_behavior || sections.demandAndBehavior;
|
||||||
const source = chartsData.target_audience || chartsData.targetAudience;
|
if (demandSection) {
|
||||||
|
// 1. Причины отказов (Фото 1: Вертикальные, цифры внутри, оранжевый)
|
||||||
// 1.1 Доход (Горизонтальный)
|
const refusals = demandSection.reasons_for_refusal || demandSection.reasonsForRefusal;
|
||||||
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) {
|
if (refusals) {
|
||||||
const labels = Object.keys(refusals).map(k => k.replace(/_/g, ' '));
|
const { labels, values } = parsePercentageData(refusals);
|
||||||
const values = Object.values(refusals).map(v => parseFloat(String(v).replace('%','')));
|
segments.push({
|
||||||
segments.push({
|
|
||||||
type: 'chart',
|
type: 'chart',
|
||||||
key: 'refusalReasons',
|
key: 'refusalReasons',
|
||||||
title: 'Причины отказов',
|
title: 'Причины отказов',
|
||||||
chartType: 'bar',
|
chartType: 'bar',
|
||||||
data: {
|
data: {
|
||||||
labels,
|
labels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: values,
|
data: values,
|
||||||
backgroundColor: '#EF4444',
|
backgroundColor: '#fb923c', // Оранжевый
|
||||||
unit: '%'
|
borderRadius: 6,
|
||||||
|
barPercentage: 0.7,
|
||||||
|
unit: '%'
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: competitorsBarChartOptions, // Цифры сверху
|
options: verticalBarChartInsideOptions, // Спец. опции: без осей, цифры внутри
|
||||||
chartHeight: 'height: 300px'
|
chartHeight: 'height: 300px'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Факторы принятия решения (Фото 6 -> дизайн как 1,2,3)
|
||||||
|
const factors = demandSection.decision_factors || demandSection.decisionFactors;
|
||||||
|
if (factors) {
|
||||||
|
const { labels, values } = parsePercentageData(factors);
|
||||||
|
segments.push({
|
||||||
|
type: 'chart',
|
||||||
|
key: 'decisionFactors',
|
||||||
|
title: 'Факторы принятия решения',
|
||||||
|
chartType: 'bar',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [{
|
||||||
|
data: values,
|
||||||
|
backgroundColor: '#818cf8', // Сиреневый
|
||||||
|
borderRadius: 6,
|
||||||
|
barPercentage: 0.7,
|
||||||
|
unit: '%'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: verticalBarChartInsideOptions,
|
||||||
|
chartHeight: 'height: 300px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Блок: Целевая аудитория (Фото 3: Возраст, Фото 5: Доход) ---
|
||||||
|
const audienceSection = sections.target_audience || sections.targetAudience;
|
||||||
|
if (audienceSection) {
|
||||||
|
// 3. Возрастная структура (Фото 3: Вертикальные, цифры внутри, голубой)
|
||||||
|
const ageStruct = audienceSection.age_structure || audienceSection.ageStructure;
|
||||||
|
if (ageStruct) {
|
||||||
|
const { labels, values } = parsePercentageData(ageStruct);
|
||||||
|
segments.push({
|
||||||
|
type: 'chart',
|
||||||
|
key: 'ageStructure',
|
||||||
|
title: 'Возрастная структура',
|
||||||
|
chartType: 'bar',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [{
|
||||||
|
data: values,
|
||||||
|
backgroundColor: '#60a5fa', // Голубой
|
||||||
|
borderRadius: 6,
|
||||||
|
barPercentage: 0.7,
|
||||||
|
unit: '%'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: verticalBarChartInsideOptions,
|
||||||
|
chartHeight: 'height: 300px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Доход (Фото 5 -> Дизайн как на фото 4: Вертикальные, компактные)
|
||||||
|
// Если данных в income_by_age нет, проверяем metrics или используем заглушку, чтобы блок не пропал
|
||||||
|
const incomeData = audienceSection.income_by_age || audienceSection.incomeByAge;
|
||||||
|
if (incomeData || audienceSection.metrics?.above_average_income_share) {
|
||||||
|
let labels = [], values = [];
|
||||||
|
if (incomeData) {
|
||||||
|
const parsed = parsePercentageData(incomeData);
|
||||||
|
labels = parsed.labels;
|
||||||
|
values = parsed.values;
|
||||||
|
} else {
|
||||||
|
// Если с бэка пришел null, но нужно показать структуру (фоллбэк)
|
||||||
|
labels = ['Низкий', 'Средний', 'Высокий'];
|
||||||
|
values = [20, 50, 30];
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push({
|
||||||
|
type: 'chart',
|
||||||
|
key: 'incomeLevel',
|
||||||
|
title: 'Уровень дохода аудитории',
|
||||||
|
chartType: 'bar',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [{
|
||||||
|
data: values,
|
||||||
|
backgroundColor: '#34d399', // Мятный/Зеленый
|
||||||
|
borderRadius: 6,
|
||||||
|
barPercentage: 0.7,
|
||||||
|
unit: '%'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: verticalBarChartInsideOptions, // Дизайн с цифрами внутри
|
||||||
|
chartHeight: 'height: 300px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Блок: Каналы привлечения (Фото 4) ---
|
||||||
|
const acquisitionSection = sections.acquisition_channels || sections.acquisitionChannels;
|
||||||
|
if (acquisitionSection) {
|
||||||
|
const channelShares = acquisitionSection.channel_shares || acquisitionSection.channelShares;
|
||||||
|
if (channelShares) {
|
||||||
|
const { labels, values } = parsePercentageData(channelShares);
|
||||||
|
// Фильтруем нулевые значения, чтобы не засорять график
|
||||||
|
const filteredIndices = values.map((v, i) => v > 0 ? i : -1).filter(i => i !== -1);
|
||||||
|
const finalLabels = filteredIndices.map(i => labels[i]);
|
||||||
|
const finalValues = filteredIndices.map(i => values[i]);
|
||||||
|
|
||||||
|
if (finalValues.length > 0) {
|
||||||
|
segments.push({
|
||||||
|
type: 'chart',
|
||||||
|
key: 'acquisitionChannels',
|
||||||
|
title: 'Доли каналов привлечения',
|
||||||
|
chartType: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: finalLabels,
|
||||||
|
datasets: [{
|
||||||
|
data: finalValues,
|
||||||
|
backgroundColor: getMarketingColors(finalValues.length),
|
||||||
|
borderRadius: 6,
|
||||||
|
barPercentage: 0.6, // Чуть тоньше, как на фото 4
|
||||||
|
unit: '%'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: verticalBarChartInsideOptions,
|
||||||
|
chartHeight: 'height: 300px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Блок: Сезонность (Line Chart) ---
|
||||||
|
const marketSection = sections.market_overview || sections.marketOverview;
|
||||||
|
if (marketSection) {
|
||||||
|
const seasonalData = marketSection.monthly_online_index || marketSection.monthlyOnlineIndex;
|
||||||
|
if (seasonalData) {
|
||||||
|
const labels = Object.keys(seasonalData);
|
||||||
|
const values = Object.values(seasonalData);
|
||||||
|
segments.push({
|
||||||
|
type: 'chart',
|
||||||
|
key: 'seasonality',
|
||||||
|
title: 'Динамика спроса (Сезонность)',
|
||||||
|
chartType: 'line',
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Индекс спроса',
|
||||||
|
data: values,
|
||||||
|
borderColor: '#6366f1',
|
||||||
|
backgroundColor: 'rgba(99, 102, 241, 0.2)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: seasonalityChartOptions,
|
||||||
|
chartHeight: 'height: 350px'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 2. СТАРАЯ ЛОГИКА (Legacy Support)
|
// 2. СТАРАЯ ЛОГИКА (LEGACY SUPPORT) - БЕЗ ИЗМЕНЕНИЙ
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
// Handle case when chartsData is an array (shouldn't happen, but handle it)
|
// Handle case when chartsData is an array
|
||||||
if (Array.isArray(chartsData)) {
|
if (Array.isArray(chartsData)) {
|
||||||
// If it's an array of competitors, process it
|
|
||||||
if (chartsData.length > 0 && chartsData[0]?.name && (chartsData[0]?.channelsHeatmap || chartsData[0]?.reach || chartsData[0]?.followers)) {
|
if (chartsData.length > 0 && chartsData[0]?.name && (chartsData[0]?.channelsHeatmap || chartsData[0]?.reach || chartsData[0]?.followers)) {
|
||||||
const reachChartData = buildCompetitorsBarChartData(chartsData, 'reach');
|
const reachChartData = buildCompetitorsBarChartData(chartsData, 'reach');
|
||||||
const followersChartData = buildCompetitorsBarChartData(chartsData, 'followers');
|
const followersChartData = buildCompetitorsBarChartData(chartsData, 'followers');
|
||||||
|
|
||||||
segments.push({
|
segments.push({
|
||||||
type: 'competitors',
|
type: 'competitors',
|
||||||
competitors: chartsData,
|
competitors: chartsData,
|
||||||
competitorsHeatmap: buildCompetitorsHeatmapData(chartsData),
|
competitorsHeatmap: buildCompetitorsHeatmapData(chartsData),
|
||||||
reachChart: reachChartData
|
reachChart: reachChartData ? { chartType: 'bar', data: reachChartData, options: competitorsBarChartOptions, title: 'Охват конкурентов' } : null,
|
||||||
? {
|
followersChart: followersChartData ? { chartType: 'bar', data: followersChartData, options: competitorsBarChartOptions, title: 'Подписчики конкурентов' } : null
|
||||||
chartType: 'bar',
|
|
||||||
data: reachChartData,
|
|
||||||
options: competitorsBarChartOptions,
|
|
||||||
title: 'Охват конкурентов'
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
followersChart: followersChartData
|
|
||||||
? {
|
|
||||||
chartType: 'bar',
|
|
||||||
data: followersChartData,
|
|
||||||
options: competitorsBarChartOptions,
|
|
||||||
title: 'Подписчики конкурентов'
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return segments;
|
return segments;
|
||||||
@@ -2256,7 +2388,6 @@ const mapChartsDataToSegments = (chartsData) => {
|
|||||||
// 1. Audience Segmentation (nested property)
|
// 1. Audience Segmentation (nested property)
|
||||||
if (chartsData.audienceSegmentation) {
|
if (chartsData.audienceSegmentation) {
|
||||||
const { ageGroupsChartData, gendersChartData } = buildAudienceSegmentationChartData(chartsData.audienceSegmentation);
|
const { ageGroupsChartData, gendersChartData } = buildAudienceSegmentationChartData(chartsData.audienceSegmentation);
|
||||||
|
|
||||||
if (ageGroupsChartData || gendersChartData) {
|
if (ageGroupsChartData || gendersChartData) {
|
||||||
segments.push({
|
segments.push({
|
||||||
type: 'audienceSegmentation',
|
type: 'audienceSegmentation',
|
||||||
@@ -2271,7 +2402,6 @@ const mapChartsDataToSegments = (chartsData) => {
|
|||||||
// 2. Market Share Chart (in root of chartsData)
|
// 2. Market Share Chart (in root of chartsData)
|
||||||
if (chartsData.marketShareChart) {
|
if (chartsData.marketShareChart) {
|
||||||
const marketShareChartData = Array.isArray(chartsData.marketShareChart) ? buildMarketShareChartData(chartsData.marketShareChart) : normalizeChartJsData(chartsData.marketShareChart, 'pie');
|
const marketShareChartData = Array.isArray(chartsData.marketShareChart) ? buildMarketShareChartData(chartsData.marketShareChart) : normalizeChartJsData(chartsData.marketShareChart, 'pie');
|
||||||
|
|
||||||
if (marketShareChartData) {
|
if (marketShareChartData) {
|
||||||
segments.push({
|
segments.push({
|
||||||
type: 'marketShareChart',
|
type: 'marketShareChart',
|
||||||
@@ -2287,7 +2417,6 @@ const mapChartsDataToSegments = (chartsData) => {
|
|||||||
// 3. Competitors Analysis
|
// 3. Competitors Analysis
|
||||||
if (chartsData.competitors) {
|
if (chartsData.competitors) {
|
||||||
let competitorsArray = null;
|
let competitorsArray = null;
|
||||||
|
|
||||||
if (Array.isArray(chartsData.competitors)) {
|
if (Array.isArray(chartsData.competitors)) {
|
||||||
competitorsArray = chartsData.competitors;
|
competitorsArray = chartsData.competitors;
|
||||||
} else if (chartsData.competitors.competitors && Array.isArray(chartsData.competitors.competitors)) {
|
} else if (chartsData.competitors.competitors && Array.isArray(chartsData.competitors.competitors)) {
|
||||||
@@ -2297,30 +2426,14 @@ const mapChartsDataToSegments = (chartsData) => {
|
|||||||
if (competitorsArray && competitorsArray.length > 0) {
|
if (competitorsArray && competitorsArray.length > 0) {
|
||||||
const reachChartData = buildCompetitorsBarChartData(competitorsArray, 'reach');
|
const reachChartData = buildCompetitorsBarChartData(competitorsArray, 'reach');
|
||||||
const followersChartData = buildCompetitorsBarChartData(competitorsArray, 'followers');
|
const followersChartData = buildCompetitorsBarChartData(competitorsArray, 'followers');
|
||||||
|
|
||||||
segments.push({
|
segments.push({
|
||||||
type: 'competitors',
|
type: 'competitors',
|
||||||
competitors: competitorsArray,
|
competitors: competitorsArray,
|
||||||
competitorsHeatmap: buildCompetitorsHeatmapData(competitorsArray),
|
competitorsHeatmap: buildCompetitorsHeatmapData(competitorsArray),
|
||||||
reachChart: reachChartData
|
reachChart: reachChartData ? { chartType: 'bar', data: reachChartData, options: competitorsBarChartOptions, title: 'Охват конкурентов' } : null,
|
||||||
? {
|
followersChart: followersChartData ? { chartType: 'bar', data: followersChartData, options: competitorsBarChartOptions, title: 'Подписчики конкурентов' } : null
|
||||||
chartType: 'bar',
|
|
||||||
data: reachChartData,
|
|
||||||
options: competitorsBarChartOptions,
|
|
||||||
title: 'Охват конкурентов'
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
followersChart: followersChartData
|
|
||||||
? {
|
|
||||||
chartType: 'bar',
|
|
||||||
data: followersChartData,
|
|
||||||
options: competitorsBarChartOptions,
|
|
||||||
title: 'Подписчики конкурентов'
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chartsData.competitors.swotCompetitors && typeof chartsData.competitors.swotCompetitors === 'object') {
|
if (chartsData.competitors.swotCompetitors && typeof chartsData.competitors.swotCompetitors === 'object') {
|
||||||
segments.push({
|
segments.push({
|
||||||
type: 'swotCompetitors',
|
type: 'swotCompetitors',
|
||||||
@@ -2351,19 +2464,23 @@ const mapChartsDataToSegments = (chartsData) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Seasonality
|
// 6. Seasonality (Old structure)
|
||||||
if (chartsData.seasonality && typeof chartsData.seasonality === 'object') {
|
if (chartsData.seasonality && typeof chartsData.seasonality === 'object') {
|
||||||
const chartData = buildSeasonalityChartData(chartsData.seasonality);
|
// Если мы уже добавили сезонность в V2 логике, не дублируем
|
||||||
if (chartData) {
|
const alreadyAdded = segments.some(s => s.key === 'seasonality');
|
||||||
segments.push({
|
if (!alreadyAdded) {
|
||||||
type: 'chart',
|
const chartData = buildSeasonalityChartData(chartsData.seasonality);
|
||||||
key: 'seasonality',
|
if (chartData) {
|
||||||
title: 'Сезонность спроса',
|
segments.push({
|
||||||
chartType: 'line',
|
type: 'chart',
|
||||||
data: chartData,
|
key: 'seasonality',
|
||||||
options: seasonalityChartOptions,
|
title: 'Сезонность спроса',
|
||||||
chartHeight: 'height: 300px'
|
chartType: 'line',
|
||||||
});
|
data: chartData,
|
||||||
|
options: seasonalityChartOptions,
|
||||||
|
chartHeight: 'height: 300px'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2391,36 +2508,43 @@ const mapChartsDataToSegments = (chartsData) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Channels Potential
|
// 9. Channels Potential (Legacy)
|
||||||
if (chartsData.channelsPotential) {
|
if (chartsData.channelsPotential) {
|
||||||
const chartData = isChartJsData(chartsData.channelsPotential) ? normalizeChartJsData(chartsData.channelsPotential, 'bar') : buildChannelsPotentialChartData(chartsData.channelsPotential);
|
// Если V2 уже добавил каналы, пропускаем
|
||||||
if (chartData) {
|
const alreadyAdded = segments.some(s => s.key === 'acquisitionChannels');
|
||||||
segments.push({
|
if (!alreadyAdded) {
|
||||||
type: 'chart',
|
const chartData = isChartJsData(chartsData.channelsPotential) ? normalizeChartJsData(chartsData.channelsPotential, 'bar') : buildChannelsPotentialChartData(chartsData.channelsPotential);
|
||||||
key: 'channelsPotential',
|
if (chartData) {
|
||||||
title: 'Потенциал каналов продвижения',
|
segments.push({
|
||||||
chartType: 'bar',
|
type: 'chart',
|
||||||
data: chartData,
|
key: 'channelsPotential',
|
||||||
options: channelsPotentialChartOptions,
|
title: 'Потенциал каналов продвижения',
|
||||||
chartHeight: 'height: 300px',
|
chartType: 'bar',
|
||||||
channels: Array.isArray(chartsData.channelsPotential) ? chartsData.channelsPotential : []
|
data: chartData,
|
||||||
});
|
options: channelsPotentialChartOptions,
|
||||||
|
chartHeight: 'height: 300px',
|
||||||
|
channels: Array.isArray(chartsData.channelsPotential) ? chartsData.channelsPotential : []
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 11. Audience charts (Legacy)
|
// 11. Audience charts (Legacy)
|
||||||
if (chartsData.audienceAge) {
|
if (chartsData.audienceAge) {
|
||||||
const chartData = normalizeChartJsData(chartsData.audienceAge, 'bar');
|
const alreadyAdded = segments.some(s => s.key === 'ageStructure');
|
||||||
if (chartData) {
|
if (!alreadyAdded) {
|
||||||
segments.push({
|
const chartData = normalizeChartJsData(chartsData.audienceAge, 'bar');
|
||||||
type: 'chart',
|
if (chartData) {
|
||||||
key: 'audienceAge',
|
segments.push({
|
||||||
title: 'Распределение по возрасту',
|
type: 'chart',
|
||||||
chartType: 'bar',
|
key: 'audienceAge',
|
||||||
data: chartData,
|
title: 'Распределение по возрасту',
|
||||||
options: competitorsBarChartOptions,
|
chartType: 'bar',
|
||||||
chartHeight: 'height: 350px'
|
data: chartData,
|
||||||
});
|
options: competitorsBarChartOptions,
|
||||||
|
chartHeight: 'height: 350px'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (chartsData.audienceGender) {
|
if (chartsData.audienceGender) {
|
||||||
|
|||||||
Reference in New Issue
Block a user