Unify Saturn ownership chart design

This commit is contained in:
KrivovDE
2026-09-09 17:52:27 +03:00
parent 7d3b2c1da6
commit 24848d0cde

View File

@@ -613,6 +613,12 @@ ulyanovskPages['saturn']={
['2017','32 600 000 тыс. руб.','235 900 тыс. руб.'],['2018','40 700 000 тыс. руб.','3 100 000 тыс. руб.'],['2019','46 600 000 тыс. руб.','1 400 000 тыс. руб.'],['2021','52 300 000 тыс. руб.','2 300 000 тыс. руб.'],['2024','74 160 345 тыс. руб.','11 928 233 тыс. руб.']
],
ownership:'Уставный капитал: 63 122 343 274,000 руб. Акционеры: 86,634% АО «Объединённая двигателестроительная корпорация»; 8,420% Государственная корпорация «Ростех»; 4,048% ФГУП «Российский федеральный ядерный центр — Всероссийский НИИ экспериментальной физики»; 0,594% ПАО «ОДК-Сатурн»; 0,899% неизвестные и/или миноритарные акционеры. Держатель реестра акционеров: АО «РТ-Регистратор». Участие в капитале: 100,000% АО «Новые инструментальные решения»; 100,000% АО «Сатурн-Инструментальный завод»; 100,000% ООО «ОДК-Цифровая инфраструктура»; 100,000% АО «Сатурн-Турборус»; 89,863% ООО «Аутсорсинг Технологии обслуживание»; 80,000% ООО «Рейнольдс».',
registryHolder:'АО «РТ-Регистратор»',
ownershipDetails:['Номинал акции — 1,000 руб.'],
participations:[
'100,000% АО «Новые инструментальные решения»','100,000% АО «Сатурн-Инструментальный завод»','100,000% ООО «ОДК-Цифровая инфраструктура»','100,000% АО «Сатурн-Турборус»','89,863% ООО «Аутсорсинг Технологии обслуживание»','80,000% ООО «Рейнольдс»','50,001% PowerJet S.A.','50,000% (!) ЗАО «ПауэрДжет»','30,000% АО «Турборус»','30,000% (!) АО «Смартек»','4,494% (!) ООО «Инжиниринговый центр „Газотурбинные технологии“»'
],
branches:['Филиал ПАО «ОДК-Сатурн» — Омское моторостроительное конструкторское бюро','Филиал «Научно-технический центр г. Санкт-Петербург» ПАО «ОДК-Сатурн»']
};
const importedCardBundle=window.BASTION_IMPORTED_CARDS||{cards:{},parentRelations:{},titleIndex:{},mainExtras:{}};
Object.assign(ulyanovskPages,importedCardBundle.cards||{});
@@ -967,7 +973,7 @@ const directOwnershipShares=value=>{
const source=String(value||'').replace(/\r/g,'');
// Для АО берём именно блок «Акционеры», а не проценты обыкновенных
// акций или доли участия самой организации в других компаниях.
const shareholderBlock=source.match(/(?:Акционеры\s*\(%\s*ОА\)|Участники)\s*:\s*([\s\S]*?)(?=\s*(?:Участие в капитале|ДУ пакетами|Имеет в составе|Имеет филиалы|Имеет выделенные организации|Дополнительная информация)\s*:|$)/i)?.[1];
const shareholderBlock=source.match(/(?:Акционеры(?:\s*\(%\s*ОА\))?|Участники)\s*:\s*([\s\S]*?)(?=\s*(?:Держатель реестра акционеров|Участие в капитале|ДУ пакетами|Имеет в составе|Имеет филиалы|Имеет выделенные организации|Дополнительная информация)\s*:|$)/i)?.[1];
const directPart=shareholderBlock||source.split(/участие в капитале:/i)[0];
const shares=[];
for(const line of directPart.split(/\n+|;\s*(?=\d+(?:[,.]\d+)?%)/)){
@@ -998,21 +1004,24 @@ const genericOwnershipDonut=(title,ownership)=>{
// При неполном раскрытии оставляем нейтральный сектор «остальные», а не
// растягиваем известную долю на весь круг. Так площадь каждого сектора
// всегда равна написанному в карточке проценту.
if(total>100.01)return `<div class="donut-data-note">Диаграмма не показана: сумма раскрытых долей превышает 100%.</div>`;
if(total>101)return `<div class="donut-data-note">Диаграмма не показана: сумма раскрытых долей превышает 100%.</div>`;
// В отдельных исходных карточках итог немного превышает 100%. Для них
// нормализуем только геометрию круга, сохраняя исходные доли в подписях.
const geometryScale=total>100?100/total:1;
const visibleShares=total<99.995
?[...shares,{value:100-total,display:'',isRemainder:true}]
:shares;
let angle=0;
const sectors=visibleShares.map((share,index)=>{
const start=angle;
const end=visibleShares.length===1?359.99:start+share.value*3.6;
const end=visibleShares.length===1?359.99:start+share.value*geometryScale*3.6;
const middle=(start+end)/2;
const labelRadius=share.value<5?122:113;
const label=visibleShares.length===1?{x:180,y:68}:polar(labelRadius,middle);
const labelClass=share.value<3?' donut-label--tiny':share.value<5?' donut-label--small':'';
angle=end;
const fill=share.isRemainder?'#4a4d50':ownershipPalette[index%ownershipPalette.length];
return `<g class="generic-donut-segment"><path d="${donutPath(start,end)}" fill="${fill}"/>${!share.isRemainder&&share.value>=2?`<text class="donut-label${labelClass}" x="${label.x}" y="${label.y}">${share.display}</text>`:''}</g>`;
return `<g class="generic-donut-segment"><path d="${donutPath(start,end)}" fill="${fill}"/>${!share.isRemainder&&share.value>=2?`<text class="donut-label${labelClass}" data-keep-decimals x="${label.x}" y="${label.y}">${share.display}</text>`:''}</g>`;
}).join('');
return `<div class="donut-visual"><svg class="ownership-donut" viewBox="0 0 360 360" role="img" aria-label="Структура собственности ${escapeHtml(title)}">${sectors}<circle class="donut-center" cx="180" cy="180" r="78"/></svg></div>`;
};
@@ -1020,7 +1029,7 @@ const genericOwnershipLegend=ownership=>{
const shares=directOwnershipShares(ownership);
if(!shares.length)return '';
const visibleShares=shares;
return visibleShares.map((share,index)=>`<div class="legend-row"><i class="swatch" style="background:${ownershipPalette[index%ownershipPalette.length]}"></i><span class="legend-copy"><b>${escapeHtml(share.display)}</b><span>${share.name?linkifyUlyText(share.name,Object.keys(ulyanovskPages)):'—'}</span></span></div>`).join('');
return visibleShares.map((share,index)=>`<div class="legend-row"><i class="swatch" style="background:${ownershipPalette[index%ownershipPalette.length]}"></i><span class="legend-copy"><b data-keep-decimals>${escapeHtml(share.display)}</b><span>${share.name?linkifyUlyText(share.name,Object.keys(ulyanovskPages)):'—'}</span></span></div>`).join('');
};
const ownershipTextMarkup=(value,related)=>linkifyUlyText(value,related).replace(/\s(\d+(?:[,.]\d+)?%)/g,'<br><strong class="ownership-inline-percent">$1</strong>');
// Классификаторы, подготовленные для прототипа из внешних реестров.
@@ -1152,6 +1161,8 @@ const renderUlyLomoOrganization=key=>{
const contacts=pairs.filter(([label])=>/^(Телефоны|Факс|E-mail|Интернет-сайт|Сайт|Раскрытие информации)$/.test(label));
const finance=data.finance||[];
const ownership=withoutAuthorizedCapital(data.ownership);
const structureOwner=key==='okb-mei'?'okb':key==='saturn'?'saturn':'';
const structureButton=structureOwner?`<button class="ghost-btn ownership-view-btn" type="button" data-structure-owner="${structureOwner}">Структура собственности в графическом виде <svg class="eye-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/><circle cx="12" cy="12" r="2.8"/></svg></button>`:'';
const financeContent=finance.length?renderFinanceCharts(finance,data.short||data.title,data.employees,data.preserveSourceMetrics):renderEmployeeChart(data.employees);
const financeCompact=finance.length?`<div class="compact-only metric-strip key-metrics"><div class="inner metric">Выручка<br><b>${escapeHtml(finance.at(-1)[0])} г.&nbsp;&nbsp;${escapeHtml(finance.at(-1)[1])}</b></div><div class="inner metric">Чистая прибыль<br><b>${escapeHtml(finance.at(-1)[0])} г.&nbsp;&nbsp;${escapeHtml(finance.at(-1)[2])}</b></div></div>`:'';
const classifiers=classifierTable(pairs,related);
@@ -1187,7 +1198,7 @@ const renderUlyLomoOrganization=key=>{
${data.legal?`<section class="section-card" id="legal" data-legal-detail="true"><div class="card-head"><h2>${key==='saturn'?'Правовой статус/НПА':'Правовой статус'}</h2>${ulyExpand()}</div><div class="inner legal-short">${linkifyUlyText(data.legal,related)}</div><div class="full-only"><h3 class="card-subtitle">Нормативно-правовые акты</h3><div class="inner paragraph">${legalDetailHtml(legalDetails,related)}</div></div></section>`:''}
${informationText?`<section class="section-card" id="about"><div class="card-head"><h2>Информация</h2>${ulyExpand()}</div><h3 class="card-subtitle">${informationTitle}</h3><div class="inner paragraph summary">${linkifyUlyText(informationText,related)}</div>${data.about&&data.history?`<div class="full-only"><h3 class="card-subtitle">Историческая справка</h3><div class="inner paragraph">${linkifyUlyText(data.history,related)}</div></div>`:''}</section>`:''}
${finance.length||data.employees?.length?`<section class="section-card" id="finance"><div class="card-head"><h2>Финансовые показатели</h2>${capitalBadge(data.ownership)}${ulyExpand()}</div><div class="full-only">${financeContent}</div>${financeCompact}${data.financeNote?`<p class="finance-source-note">${escapeHtml(data.financeNote)}</p>`:''}</section>`:''}
${key==='saturn'?saturnOwnershipCard():data.ownership?`<section class="section-card" id="ownership"><div class="card-head"><h2>Структура собственности</h2>${key==='okb-mei'?`<button class="ghost-btn ownership-view-btn" type="button" data-structure-owner="okb">Структура собственности в графическом виде <svg class="eye-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/><circle cx="12" cy="12" r="2.8"/></svg></button>`:''}${ulyExpand()}</div>${directOwnershipShares(ownership).length?`<div class="ownership-meta"><span>${/Акционеры\s*\(%\s*ОА\)/i.test(ownership)?'Акционеры (% ОА)':'Участники (% УК)'}</span></div><div class="inner ownership-panel generic-ownership-panel">${genericOwnershipDonut(data.title,ownership)}<div class="legend">${genericOwnershipLegend(ownership)}</div></div>`:`<div class="inner paragraph">${ownershipTextMarkup(ownership,related)}</div>`}${ownershipExtras}</section>`:data.participations?.length?`<section class="section-card" id="ownership"><div class="card-head"><h2>Структура собственности</h2>${ulyExpand()}</div><h3 class="card-subtitle">Участие в капитале (% ОА / % УК)</h3><div class="inner paragraph compact-only">${data.participations.slice(0,6).map(item=>linkifyUlyText(item,related)).join('<br><br>')}</div><div class="inner paragraph full-only">${data.participations.map(item=>linkifyUlyText(item,related)).join('<br><br>')}</div></section>`:''}
${data.ownership?`<section class="section-card" id="ownership"><div class="card-head"><h2>Структура собственности</h2>${structureButton}${ulyExpand()}</div>${directOwnershipShares(ownership).length?`<div class="ownership-meta"><span>${/Акционеры(?:\s*\(%\s*ОА\))?\s*:/i.test(ownership)?'Акционеры (% ОА)':'Участники (% УК)'}</span></div><div class="inner ownership-panel generic-ownership-panel">${genericOwnershipDonut(data.title,ownership)}<div class="legend">${genericOwnershipLegend(ownership)}</div></div>`:`<div class="inner paragraph">${ownershipTextMarkup(ownership,related)}</div>`}${ownershipExtras}</section>`:data.participations?.length?`<section class="section-card" id="ownership"><div class="card-head"><h2>Структура собственности</h2>${ulyExpand()}</div><h3 class="card-subtitle">Участие в капитале (% ОА / % УК)</h3><div class="inner paragraph compact-only">${data.participations.slice(0,6).map(item=>linkifyUlyText(item,related)).join('<br><br>')}</div><div class="inner paragraph full-only">${data.participations.map(item=>linkifyUlyText(item,related)).join('<br><br>')}</div></section>`:''}
${data.activity||data.licenses?.length||data.specialization?`<section class="section-card" id="activity"><div class="card-head"><h2>Виды деятельности</h2>${ulyExpand()}</div>${data.activity?`<h3 class="card-subtitle">Основной вид деятельности</h3><div class="inner paragraph">${linkifyUlyText(data.activity,related)}</div>`:''}${data.licenses?.length?`<div class="full-only"><h3 class="card-subtitle">Действующие лицензии</h3><div class="inner paragraph">${data.licenses.map(item=>linkifyUlyText(item,related)).join('<br><br>')}</div></div>`:''}${data.specialization?`<div class="full-only"><h3 class="card-subtitle">Специализация</h3><div class="inner paragraph">${linkifyUlyText(data.specialization,related)}</div></div>`:''}</section>`:''}
${stabilitySection(data,related)}`;
if(data.employees?.length){