sqPasteBin
New
List
Guide
Editing upload 'worm'
Content
<!doctype html> <html lang="ru"> <head> <meta charset="utf-8" /> <title>ОВС (\$ млрд) · Группа и компании — компактные бары</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <style> body { margin:0; font-family: system-ui,-apple-system,Segoe UI,Roboto,Arial; background:#fafafa; } .grid { display:flex; flex-direction:column; gap:12px; padding:12px 8px; } .row { display:flex; align-items:stretch; gap:12px; } .row-aside{ flex:0 0 220px; display:flex; align-items:center; justify-content:flex-start; padding:0 8px 0 4px; } .row-title{ font-size:22px; font-weight:900; color:#2F454F; letter-spacing:.2px; line-height:1.2; } .row-title.group{ font-size:24px; font-weight:1000; letter-spacing:.3px; } .row-main{ flex:1 1 auto; } .card { background:#fff; border-radius:14px; box-shadow:0 2px 12px rgba(0,0,0,.06); padding:14px 22px 18px; } .chart { width:100%; height:240px; } @media (max-width:720px) { .grid { gap:14px; padding:12px 8px; } .row { flex-direction:column; gap:6px; } .row-aside{ flex:0 0 auto; padding:0 2px; } .row-title{ font-size:20px; } .row-title.group{ font-size:22px; } .chart { height:240px; } } svg text { paint-order: stroke fill; } </style> </head> <body> <div id="data" style="display:none">{{ data.rows|dump|safe }}</div> <div class="grid" id="grid"></div> <script src="/proxy/file-storage/api/v1/storage/ce26108b-18b4-4f8b-9650-8a275913d9df/135fd9c6-e885-42ba-bf4f-b1d0b82f2462/assets/echarts.min.js"></script> <script> ;(() => { const TITLE_EACH = 'Объем внешнеторговых сделок (\$ млрд)'; // Форматтер для Факта (строгий: 1 или 3 знака после запятой) const nfFact = v => { if (v == null || isNaN(v)) return ''; const abs = Math.abs(v); const opts = { minimumFractionDigits: abs < 1 ? 3 : 1, maximumFractionDigits: abs < 1 ? 3 : 1 }; return new Intl.NumberFormat('ru-RU', opts).format(v); }; // Форматтер для Плана/Прогноза (простой: без лишних нулей) const nfPlan = v => { if (v == null || isNaN(v)) return ''; return new Intl.NumberFormat('ru-RU').format(v); }; const pctColor = p => p < 80 ? '#E74C3C' : (p < 90 ? '#F1C40F' : '#2ECC71'); const ymKey = (y,m) => \`\${y}-\${String(m).padStart(2,'0')}\`; const MANUAL = { "Группа РЭЦ": { plan: { "3М": 1.6, "6М": 4.8, "9М": 8.7, "12М": 16.2 }, }, "ЭКСАР": { plan: { "3М": 1.6, "6М": 4.8, "9М": 8.7, "12М": 15.9 }, }, "РЭБ": { plan: { "3М": 0.2, "6М": 0.5, "9М": 0.9, "12М": 1.6 }, } }; const quarterOf = (date) => { const m=date.getMonth()+1; return m<=3?1:m<=6?2:m<=9?3:4; }; function horizonsForQuarter(q){ if(q===1)return[ {type:'fact',label:'Факт'}, {type:'plan',label:'План 3М', months:3, isQuarterPlan: true}, {type:'plan',label:'План 12М',months:12, isYearPlan: true}, {type:'fcst',label:'Прогноз 3М',months:3} ]; if(q===2)return[ {type:'fact',label:'Факт'}, {type:'plan',label:'План 6М', months:6, isQuarterPlan: true}, {type:'plan',label:'План 12М',months:12, isYearPlan: true}, {type:'fcst',label:'Прогноз 6М',months:6} ]; if(q===3)return[ {type:'fact',label:'Факт'}, {type:'plan',label:'План 9М', months:9, isQuarterPlan: true}, {type:'plan',label:'План 12М',months:12, isYearPlan: true}, {type:'fcst',label:'Прогноз 9М',months:9} ]; return[ {type:'fact',label:'Факт'}, {type:'plan',label:'План 12М',months:12, isYearPlan: true, isQuarterPlan: true}, // в 4кв план квартала = план года {type:'fcst',label:'Прогноз 12М',months:12} ]; } function getRowsFromContainer(){ const el = document.querySelector('#data'); let raw = (el?.textContent || '').trim(); if (!raw) return []; if(raw.includes('"')) raw = raw.replace(/"/g, '"'); let arr; try { arr = JSON.parse(raw); } catch { return []; } if(!Array.isArray(arr)) arr = arr?.rows ?? []; if(!Array.isArray(arr)) return []; return arr.map(r => ({ support_date_year: Number(r["1"]), support_date_month: Number(r["2"]), quart: Number(r["3"]), src: String(r["4"] ?? '').trim(), ved_direction: String(r["5"] ?? '').trim(), sev_exporter_mlrd: Number(r["6"] ?? 0) || 0, sev_group_mlrd: Number(r["7"] ?? 0) || 0 })); } function buildMonthlyAgg(rows, companySrc){ const monthly = {}; const monthlyImport = {}; const asOf = new Date(); const year = asOf.getFullYear(); const mNow = asOf.getMonth() + 1; rows.forEach(r => { if(r.src !== companySrc) return; if(r.support_date_year !== year) return; const ym = ymKey(r.support_date_year, r.support_date_month); const val = r.sev_exporter_mlrd || 0; monthly[ym] = (monthly[ym] ?? 0) + val; if(r.ved_direction === 'Импорт') monthlyImport[ym] = (monthlyImport[ym] ?? 0) + val; }); const ymNow = ymKey(year, mNow); const ymPrev = mNow > 1 ? ymKey(year, mNow - 1) : null; let cum = 0, cumImp = 0; const ytd = {}, ytdImport = {}; for (let m = 1; m <= mNow; m++) { const ym = ymKey(year, m); cum += monthly[ym] ?? 0; cumImp += monthlyImport[ym] ?? 0; ytd[ym] = cum; ytdImport[ym] = cumImp; } const isFirstMonth = mNow === 1 || (ymPrev && (ytd[ymPrev] ?? 0) === 0); return { curCum: ytd[ymNow] ?? 0, prevCum: ymPrev ? (ytd[ymPrev] ?? 0) : 0, importCum: ytdImport[ymNow] ?? 0, lastMonth: mNow, isFirstMonth: isFirstMonth }; } function buildMonthlyAggGeneric(rows, key){ const monthly = {}; const monthlyImport = {}; const asOf = new Date(); const year = asOf.getFullYear(); const mNow = asOf.getMonth() + 1; rows.forEach(r => { if(r.support_date_year !== year) return; const ym = ymKey(r.support_date_year, r.support_date_month); const v = r[key] || 0; monthly[ym] = (monthly[ym] ?? 0) + v; if(r.ved_direction === 'Импорт') monthlyImport[ym] = (monthlyImport[ym] ?? 0) + v; }); const ymNow = ymKey(year, mNow); const ymPrev = mNow > 1 ? ymKey(year, mNow - 1) : null; let cum = 0, cumImp = 0; const ytd = {}, ytdImport = {}; for (let m = 1; m <= mNow; m++) { const ym = ymKey(year, m); cum += monthly[ym] ?? 0; cumImp += monthlyImport[ym] ?? 0; ytd[ym] = cum; ytdImport[ym] = cumImp; } const isFirstMonth = mNow === 1 || (ymPrev && (ytd[ymPrev] ?? 0) === 0); return { curCum: ytd[ymNow] ?? 0, prevCum: ymPrev ? (ytd[ymPrev] ?? 0) : 0, importCum: ytdImport[ymNow] ?? 0, lastMonth: mNow, isFirstMonth: isFirstMonth }; } function buildBars(monthAgg, src){ const factCum = monthAgg.curCum; const deltaMoM = monthAgg.isFirstMonth ? 0 : (factCum - (monthAgg.prevCum ?? 0)); const importCum = monthAgg.importCum; const q = quarterOf(new Date()); const manual = MANUAL[src] || {}; const plan = manual.plan || {}; const fcst = manual.forecast || {}; const steps = horizonsForQuarter(q); const labels=[], values=[]; let plan12 = null; let planQ = null; steps.forEach(s=>{ if(s.type==='fact'){ labels.push('Факт'); values.push(factCum); } else if(s.type==='plan'){ const key = \`\${s.months}М\`; if(plan[key]!=null){ const v = plan[key]; labels.push(s.label); values.push(v); // Запоминаем план для кружков if(s.isYearPlan) plan12 = v; if(s.isQuarterPlan) planQ = v; } } else if(s.type==='fcst'){ const key = \`\${s.months}М\`; if(fcst[key]!=null){ labels.push(s.label); values.push(fcst[key]); } } }); // Считаем проценты const pctYear = plan12 > 0 ? (factCum / plan12 * 100) : 0; const pctQuarter = planQ > 0 ? (factCum / planQ * 100) : 0; return { labels, values, deltaMoM, pctYear, pctQuarter, importCum }; } function tooltipRightPosition(pt, params, dom, rect, size) { const [x0, y0] = pt; const vw = size.viewSize[0]; const vh = size.viewSize[1]; const tw = size.contentSize[0]; const th = size.contentSize[1]; let x = x0 + 14; let y = y0 - th / 2; if (x + tw + 10 > vw) x = vw - tw - 10; if (y < 8) y = 8; if (y + th + 8 > vh) y = vh - th - 8; return [x, y]; } // Вспомогательная функция для создания кружка (БЕЗ подписи снизу) // Добавили rightOffset для управления отступом справа function createDonut(pct, label, bottomOffset, rightOffset = 0) { const ringValue = Math.round(Math.max(0, pct)); const ringColor = pctColor(ringValue); return { type: 'group', right: rightOffset, bottom: bottomOffset, z: 200, silent: true, children: [ // Серый фон кольца { type: 'arc', shape: { cx: 0, cy: 0, r: 30, startAngle: 0, endAngle: Math.PI * 2 }, style: { stroke: '#E6E6E6', lineWidth: 5, fill: 'none' } }, // Цветное кольцо прогресса { type: 'arc', shape: { cx: 0, cy: 0, r: 30, startAngle: -Math.PI / 2, endAngle: -Math.PI / 2 + 2 * Math.PI * (ringValue / 100) }, style: { stroke: ringColor, lineWidth: 5, fill: 'none' } }, // Процент { type: 'text', style: { x: 0, y: 0, // По центру text: \`\${ringValue}%\`, fill: ringColor, fontSize: 16, fontWeight: '800', textAlign: 'center', textVerticalAlign: 'middle' } } ] }; } function buildOption({ labels, values, deltaMoM, pctYear, pctQuarter, importCum }){ const colors=[ {c1:'#02B050', c2:'#02B050'}, // Факт {c1:'#0327CE', c2:'#0327CE'}, // План {c1:'#5C5C5C', c2:'#5C5C5C'}, // доп {c1:'#8CA1A5', c2:'#C5D0D2'} // Прогноз ]; // Создаем два пончика: один для квартала (повыше), один для года (пониже) // Прижимаем вправо (right: 0) и нижний в угол (bottom: 0). // Между ними делаем зазор (нижний 0, верхний 75) const donutQuarter = createDonut(pctQuarter, 'Квартал', 75, 0); const donutYear = createDonut(pctYear, 'Год', 0, 0); return { backgroundColor:'transparent', title:{ text: TITLE_EACH, left:'center', top:6, textStyle:{ fontSize:16, fontWeight:800, color:'#2F454F', lineHeight:18 } }, // Увеличиваем отступ справа (right), чтобы влезли два пончика grid:{ left:16, right:120, top:32, bottom:12, containLabel:true }, tooltip:{ trigger:'item', position: tooltipRightPosition, formatter:(p)=>{ const isFact = String(p.name).startsWith('Факт'); if(p.seriesType==='bar' && isFact) { const d = deltaMoM || 0; const sign = d > 0 ? '+' : ''; const deltaTxt = d !== 0 ? \` (\${sign}\${nfFact(d)} прирост на начало текущего месяца)\` : ''; return \`Факт: <b>\${nfFact(p.value)}</b>\${deltaTxt}<br/>В том числе Импорт: <b>\${nfFact(importCum)}</b>\`; } // Для планов и прогнозов используем простой форматтер return \`\${p.name}: <b>\${nfPlan(p.value)}</b>\`; } }, xAxis:{ type:'value', show:false }, yAxis:{ type:'category', inverse:true, data:labels, axisLine:{show:false}, axisTick:{show:false}, axisLabel:{ color:'#111', fontSize:18, fontWeight:700, margin:12 } }, series:[ { type:'bar', barWidth:36, barCategoryGap:'45%', itemStyle:{ borderRadius:[6,6,6,6], shadowBlur:6, shadowColor:'rgba(0,0,0,0.06)' }, data: values.map((v,i)=>({ value:v, itemStyle:{ color: new echarts.graphic.LinearGradient(0,0,1,0,[ { offset:0, color: colors[i%colors.length].c1 }, { offset:1, color: colors[i%colors.length].c2 } ]) } })), label:{ show:true, position:'right', distance:8, fontSize:20, fontWeight:'900', formatter:(p)=>{ if (p.name === 'Факт') { const d = deltaMoM || 0; if (d !== 0) { const sign = d > 0 ? '+' : ''; return \`\${nfFact(p.value)} {delta|(\${sign}\${nfFact(d)})}\`; } return nfFact(p.value); } // Для планов return nfPlan(p.value); }, rich: { delta: { fontSize: 14, color: '#9CA3AF', fontWeight: '500' } } } } ], graphic:[donutQuarter, donutYear], animationDuration:350, animationEasing:'cubicOut' }; } function ensureChart(el){ return echarts.getInstanceByDom(el) || echarts.init(el,null,{renderer:'svg'}); } function createRow(title, isGroup=false){ const row=document.createElement('div'); row.className='row'; const aside=document.createElement('div'); aside.className='row-aside'; const h=document.createElement('div'); h.className='row-title' + (isGroup?' group':''); h.textContent=title || ''; aside.appendChild(h); const main=document.createElement('div'); main.className='row-main'; const card=document.createElement('div'); card.className='card'; const chart=document.createElement('div'); chart.className='chart'; card.appendChild(chart); main.appendChild(card); row.appendChild(aside); row.appendChild(main); return { row, chartEl: chart }; } function renderAll(rows){ const grid=document.getElementById('grid'); grid.innerHTML=''; const charts=[]; const ORDER = ['ЭКСАР','РЭБ']; ORDER.forEach(src=>{ const mAgg = buildMonthlyAgg(rows, src); const r = buildBars(mAgg, src); const { row, chartEl } = createRow(src, false); grid.appendChild(row); const chart=ensureChart(chartEl); chart.setOption(buildOption({ labels:r.labels, values:r.values, deltaMoM:r.deltaMoM, pctYear:r.pctYear, pctQuarter: r.pctQuarter, importCum:r.importCum }), true); charts.push(chart); }); // Группа РЭЦ { const groupAgg = buildMonthlyAggGeneric(rows, 'sev_group_mlrd'); const r = buildBars(groupAgg, "Группа РЭЦ"); const { row, chartEl } = createRow('Группа РЭЦ', true); grid.appendChild(row); const chart=ensureChart(chartEl); chart.setOption(buildOption({ labels:r.labels, values:r.values, deltaMoM:r.deltaMoM, pctYear:r.pctYear, pctQuarter: r.pctQuarter, importCum:r.importCum }), true); charts.push(chart); } window.addEventListener('resize',()=>charts.forEach(ch=>ch.resize())); } window.renderPolymaticaCharts = function(payload){ let rows=[]; if(Array.isArray(payload)) rows=payload; else if(payload?.data?.rows) rows=payload.data.rows; else rows=getRowsFromContainer(); renderAll(rows); }; renderAll(getRowsFromContainer()); })(); </script> </body> </html>
MicroBin
by Dániel Szabó and the FOSS Community. Hosted by
@sqkrv
.