/** * Chart 4 — Сравнение регионов: две вертикальные панели рядом. * * Левая панель: зелёные бары (лидеры). * Правая панель: оранжевые/светлые бары (аутсайдеры). * Выбранный субъект: жёлтый/оранжевый highlight (isHighlight=true). * * Параметры: * left: { title, bars: [{ label, value, isHighlight? }] } * right: { title, bars: [{ label, value, isHighlight? }] } * regionLabel: string * unit: string */ const sharp = require('sharp') const { PALETTE } = require('../data/palette') const { dark: DARK, mdGray: MDGRAY, ltGray: LTGRAY, grid: GRID, green: GREEN, orange: ORANGE, o: YELLOW } = PALETTE function esc (s) { return String(s).replace(/&/g,'&').replace(//g,'>') } async function chart4_regionCompare ({ left, right, regionLabel, unit = 'руб./кг', width = 920 }) { const W = width const gap = 48 const panelW = (W - gap) / 2 const PL = 28, PR = 12, PT = 50, PB = 64 const cH = 260 // Общая шкала Y для честного сравнения const allVals = [...left.bars.map(b => b.value), ...right.bars.map(b => b.value)] const maxV = Math.max(...allVals) * 1.12 const minV = 0 const H = PT + cH + PB const yV = v => PT + cH - (v - minV) / (maxV - minV) * cH function renderPanel (bars, offsetX, title, defaultColor, isLeftPanel) { const n = bars.length const cW = panelW - PL - PR const slotW = cW / n const barW = Math.min(50, slotW * 0.68) let s = '' // Заголовок s += `${esc(title)}` // Y grid + labels (только у левой панели) if (isLeftPanel) { const step = maxV > 50 ? 10 : 5 for (let v = 0; v <= maxV; v += step) { const y = yV(v) s += `` s += `${v}` } } else { const step = maxV > 50 ? 10 : 5 for (let v = 0; v <= maxV; v += step) { const y = yV(v) s += `` } } // Ось X внизу s += `` bars.forEach((b, i) => { const cx = offsetX + PL + i * slotW + slotW / 2 const x = cx - barW / 2 const bH = Math.abs(yV(b.value) - yV(0)) const by = yV(b.value) // Цвет: highlight → жёлтый, иначе — defaultColor const color = b.isHighlight ? YELLOW : defaultColor s += `` // Значение над баром s += `${b.value.toFixed(1)}` // Подпись под осью (повёрнутая) const labelX = cx, labelY = yV(0) + 12 const maxLen = Math.floor(slotW / 4.8) const name = b.label.length > maxLen ? b.label.slice(0, maxLen) + '…' : b.label s += `${esc(name)}` }) // Подпись единиц s += `${esc(unit)}` return s } const leftSvg = renderPanel(left.bars, 0, left.title, GREEN, true) const rightSvg = renderPanel(right.bars, panelW + gap, right.title, ORANGE, false) // Разделительная линия между панелями const divX = panelW + gap / 2 const dividerLine = `` const fullSvg = ` DairyTrends · dairy-news.ru/dairytrends ${esc(regionLabel)} ${leftSvg}${dividerLine}${rightSvg} ` return { buffer: await sharp(Buffer.from(fullSvg)).png({ quality: 95 }).toBuffer(), width: W, height: H + 40 } } module.exports = { chart4_regionCompare }