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();