grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.pr-dashboard-tilda .kpi-grid-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.pr-dashboard-tilda .kpi-tile {
border: 1px solid var(--line);
border-radius: 14px;
background: #fff;
padding: 12px;
}
.pr-dashboard-tilda .kpi-tile-title {
font-size: 14px;
font-weight: 800;
margin-bottom: 8px;
}
.pr-dashboard-tilda .kpi-tile-text {
color: var(--muted);
line-height: 1.45;
font-size: 14px;
}
.pr-dashboard-tilda select {
appearance: none;
border: 1px solid var(--line);
border-radius: 10px;
padding: 10px 14px;
font: inherit;
color: var(--text);
background: #fff;
min-width: 220px;
}
.pr-dashboard-tilda .chart {
width: 100%;
min-height: 360px;
}
.pr-dashboard-tilda .table-wrap {
overflow-x: auto;
border: 1px solid var(--line);
border-radius: 14px;
background: #FFFFFF;
}
.pr-dashboard-tilda table {
width: 100%;
border-collapse: collapse;
min-width: 760px;
}
.pr-dashboard-tilda th, .pr-dashboard-tilda td {
padding: 11px 12px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
font-size: 14px;
}
.pr-dashboard-tilda thead th {
position: sticky;
top: 0;
background: rgba(255,255,255,0.96);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
}
.pr-dashboard-tilda .delta {
font-weight: 700;
}
.positive { color: var(--good); }
.negative { color: var(--bad); }
.pr-dashboard-tilda .kpi-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.pr-dashboard-tilda .kpi-item {
background: #FFFFFF;
border: 1px solid var(--line);
border-radius: 14px;
padding: 14px;
}
.pr-dashboard-tilda .kpi-item strong {
display: block;
margin-bottom: 6px;
font-size: 15px;
}
.pr-dashboard-tilda ul.clean {
margin: 0;
padding-left: 18px;
color: var(--text);
}
.pr-dashboard-tilda ul.clean li {
margin-bottom: 10px;
line-height: 1.5;
}
@media (max-width: 1120px) {
.grid-2, .kpi-list, .metric-grid, .kpi-grid-3 { grid-template-columns: 1fr; }
.summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
.shell { padding: 16px 12px 32px; }
.summary-grid { grid-template-columns: 1fr; }
.metric-grid-2, .kpi-grid-3, .kpi-grid-2 { grid-template-columns: 1fr; }
.hero-copy h1 { font-size: 34px; }
.section { padding: 16px; }
.chart { min-height: 320px; }
}
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: { title: 'Квартал', gridcolor: 'rgba(0,0,0,0.07)' },
yaxis: { title: 'Доля', tickformat: '.0%', gridcolor: 'rgba(0,0,0,0.07)' },
legend: { orientation: 'h', y: -0.27, x: 0, yanchor: 'top' }
}, { responsive: true, displayModeBar: false });
Plotly.newPlot('shareOfVoiceChart', [
{
x: sov.map(r => r.label),
y: sov.map(r => r.share),
type: 'scatter',
mode: 'lines+markers',
line: { color: '#3187EF', width: 3 },
marker: { size: 8 },
name: 'Доля'
},
{
x: sov.map(r => r.label),
y: sov.map(r => r.value),
type: 'bar',
yaxis: 'y2',
marker: { color: 'rgba(166, 210, 255, 0.65)' },
name: 'Упоминания'
}
], {
margin: { l: 46, r: 46, t: 12, b: 46 },
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: { title: 'Квартал', gridcolor: 'rgba(0,0,0,0.07)' },
yaxis: { title: 'Доля', tickformat: '.0%', gridcolor: 'rgba(0,0,0,0.07)' },
yaxis2: { title: 'Упоминания', overlaying: 'y', side: 'right', showgrid: false },
legend: { orientation: 'h' }
}, { responsive: true, displayModeBar: false });
}
function renderPeerCharts() {
const sovPeers = dashboardData.shareOfVoice
.map(row => {
const latest = row['2026Q1'] || row['2025Q4'] || row['2025Q3'];
return latest ? { company: row.company, share: latest.share } : null;
})
.filter(Boolean)
.sort((a, b) => b.share - a.share)
.slice(0, 8);
Plotly.newPlot('shareOfVoicePeerChart', [{
x: sovPeers.map(r => r.share).reverse(),
y: sovPeers.map(r => r.company).reverse(),
type: 'bar',
orientation: 'h',
marker: { color: '#3187EF' },
hovertemplate: '%{y}
%{x:.1%}
',
}], {
margin: { l: 110, r: 18, t: 12, b: 46 },
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: { title: 'Доля голоса', tickformat: '.0%', gridcolor: 'rgba(0,0,0,0.07)' },
}, { responsive: true, displayModeBar: false });
}
function renderKpi() {
const halfYearRoot = document.getElementById('halfYearKpi');
const labels = {
'Brand Strategy Book': ['Общее / Фокус SA / Фокус SS / Фокус Ai продукт', 'Фокус МПИ', 'Ребрендинг МПИ'],
'eLama Index': ['DAAI', 'Трендбук', 'Спец исследование', 'Отраслевые']
};
const normalizedGroups = {
'Brand Strategy Book': [
{
title: labels['Brand Strategy Book'][0],
text: 'Артефакт: согласован и передан BSB'
},
{
title: labels['Brand Strategy Book'][1],
text: dashboardData.kpiHalfYear['Brand Strategy Book'][4]?.kpi || ''
},
{
title: labels['Brand Strategy Book'][2],
text: dashboardData.kpiHalfYear['Brand Strategy Book'][5]?.kpi || ''
}
],
'eLama Index': (dashboardData.kpiHalfYear['eLama Index'] || []).map((item, index) => ({
title: labels['eLama Index'][index] || item.focus || item.project || 'Без уточнения',
text: item.kpi
}))
};
halfYearRoot.innerHTML = Object.entries(normalizedGroups).map(([group, items]) => `
${group}
${items.map((item) => `
${item.title}
${item.text}
`).join('')}
`).join('');
const yearList = document.getElementById('yearKpi');
const yearCards = [
{ value: 'до 34%', label: 'Aided Awareness в сегменте агентств и фрилансеров' },
{ value: 'до 36%', label: 'Aided Awareness в сегменте SMB и LE' },
{ value: 'до 30%', label: 'Aided Awareness среди агентств в целевых регионах' },
{ value: '+8-12 п.п.', label: 'Атрибут «эксперт рынка / источник данных о digital-рекламе»' },
{ value: '10-15 п.п.', label: 'Атрибут «маркетинг-хаб развития бизнеса»' }
];
yearList.innerHTML = yearCards.map(item => `
${item.value}
${item.label}
`).join('');
}
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: { title: 'Квартал', gridcolor: 'rgba(0,0,0,0.07)' },
yaxis: { title: 'Доля', tickformat: '.0%', gridcolor: 'rgba(0,0,0,0.07)' },
legend: { orientation: 'h', y: -0.27, x: 0, yanchor: 'top' }
}, { responsive: true, displayModeBar: false });
Plotly.newPlot('shareOfVoiceChart', [
{
x: sov.map(r => r.label),
y: sov.map(r => r.share),
type: 'scatter',
mode: 'lines+markers',
line: { color: '#3187EF', width: 3 },
marker: { size: 8 },
name: 'Доля'
},
{
x: sov.map(r => r.label),
y: sov.map(r => r.value),
type: 'bar',
yaxis: 'y2',
marker: { color: 'rgba(166, 210, 255, 0.65)' },
name: 'Упоминания'
}
], {
margin: { l: 46, r: 46, t: 12, b: 46 },
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: { title: 'Квартал', gridcolor: 'rgba(0,0,0,0.07)' },
yaxis: { title: 'Доля', tickformat: '.0%', gridcolor: 'rgba(0,0,0,0.07)' },
yaxis2: { title: 'Упоминания', overlaying: 'y', side: 'right', showgrid: false },
legend: { orientation: 'h' }
}, { responsive: true, displayModeBar: false });
}
function renderPeerCharts() {
const sovPeers = dashboardData.shareOfVoice
.map(row => {
const latest = row['2026Q1'] || row['2025Q4'] || row['2025Q3'];
return latest ? { company: row.company, share: latest.share } : null;
})
.filter(Boolean)
.sort((a, b) => b.share - a.share)
.slice(0, 8);
Plotly.newPlot('shareOfVoicePeerChart', [{
x: sovPeers.map(r => r.share).reverse(),
y: sovPeers.map(r => r.company).reverse(),
type: 'bar',
orientation: 'h',
marker: { color: '#3187EF' },
hovertemplate: '%{y}
%{x:.1%}
',
}], {
margin: { l: 110, r: 18, t: 12, b: 46 },
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
xaxis: { title: 'Доля голоса', tickformat: '.0%', gridcolor: 'rgba(0,0,0,0.07)' },
}, { responsive: true, displayModeBar: false });
}
function renderKpi() {
const halfYearRoot = document.getElementById('halfYearKpi');
const labels = {
'Brand Strategy Book': ['Общее / Фокус SA / Фокус SS / Фокус Ai продукт', 'Фокус МПИ', 'Ребрендинг МПИ'],
'eLama Index': ['DAAI', 'Трендбук', 'Спец исследование', 'Отраслевые']
};
const normalizedGroups = {
'Brand Strategy Book': [
{
title: labels['Brand Strategy Book'][0],
text: 'Артефакт: согласован и передан BSB'
},
{
title: labels['Brand Strategy Book'][1],
text: dashboardData.kpiHalfYear['Brand Strategy Book'][4]?.kpi || ''
},
{
title: labels['Brand Strategy Book'][2],
text: dashboardData.kpiHalfYear['Brand Strategy Book'][5]?.kpi || ''
}
],
'eLama Index': (dashboardData.kpiHalfYear['eLama Index'] || []).map((item, index) => ({
title: labels['eLama Index'][index] || item.focus || item.project || 'Без уточнения',
text: item.kpi
}))
};
halfYearRoot.innerHTML = Object.entries(normalizedGroups).map(([group, items]) => `
${group}
${items.map((item) => `
${item.title}
${item.text}
`).join('')}
`).join('');
const yearList = document.getElementById('yearKpi');
const yearCards = [
{ value: 'до 34%', label: 'Aided Awareness в сегменте агентств и фрилансеров' },
{ value: 'до 36%', label: 'Aided Awareness в сегменте SMB и LE' },
{ value: 'до 30%', label: 'Aided Awareness среди агентств в целевых регионах' },
{ value: '+8-12 п.п.', label: 'Атрибут «эксперт рынка / источник данных о digital-рекламе»' },
{ value: '10-15 п.п.', label: 'Атрибут «маркетинг-хаб развития бизнеса»' }
];
yearList.innerHTML = yearCards.map(item => `
${item.value}
${item.label}
`).join('');
}
function init() {
renderSummaryCards();
buildMetricOptions('monthlyMetric');
buildMetricOptions('quarterlyMetric');
const defaultMonthlyMetric = 'Медиа присутствие СМИ';
const defaultQuarterlyMetric = 'media outreach СМИ';
document.getElementById('monthlyMetric').value = defaultMonthlyMetric;
document.getElementById('quarterlyMetric').value = defaultQuarterlyMetric;
renderMonthlyChart(defaultMonthlyMetric);
renderQuarterlyChart(defaultQuarterlyMetric);
renderBrandAwareness();
const mainYoyDefaults = ['Медиа присутствие СМИ', 'media outreach СМИ', 'media outreach New Media'];
renderToggleControls('mainYoyControls', mainYoyDefaults, mainYoyDefaults, renderMainYoyCards);
renderMainYoyCards(mainYoyDefaults);
populateBrandPeriodOptions();
renderBrandYoyCards();
renderShareCharts();
renderPeerCharts();
renderKpi();
document.getElementById('monthlyMetric').addEventListener('change', (event) => {
renderMonthlyChart(event.target.value);
});
document.getElementById('quarterlyMetric').addEventListener('change', (event) => {
renderQuarterlyChart(event.target.value);
});
document.getElementById('brandCompareMode').addEventListener('change', () => {
populateBrandPeriodOptions();
renderBrandYoyCards();
});
document.getElementById('brandComparePeriod').addEventListener('change', () => {
renderBrandYoyCards();
});
}
init();