diff --git a/src/charts/chart4.js b/src/charts/chart4.js index dfd7984..eaa7a25 100644 --- a/src/charts/chart4.js +++ b/src/charts/chart4.js @@ -1,9 +1,8 @@ /** * Chart 4 — Сравнение регионов: две вертикальные панели рядом. * - * Левая панель: зелёные бары (лидеры). - * Правая панель: оранжевые/светлые бары (аутсайдеры). - * Выбранный субъект: жёлтый/оранжевый highlight (isHighlight=true). + * Обе панели с Y-осью и сеткой. Общая шкала Y. + * Левая: зелёные бары. Правая: оранжевые. isHighlight=true → жёлтый. * * Параметры: * left: { title, bars: [{ label, value, isHighlight? }] } @@ -14,92 +13,94 @@ 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 +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 + const DIVIDER_GAP = 52 // зазор между панелями (пунктирная линия посредине) + const PL = 36, PR = 10 // отступы внутри каждой панели + const PT = 56, PB = 64 // верх/низ (PB — место для подписей оси X + unit) + const cH = 280 // высота области баров - // Общая шкала 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 panelW = (W - DIVIDER_GAP) / 2 + const cWp = panelW - PL - PR // ширина области баров в каждой панели + + // Общая шкала Y — берём max по обеим панелям + const allVals = [...left.bars.map(b => b.value), ...right.bars.map(b => b.value)] + const rawMax = Math.max(...allVals) + const rawMin = 0 + // Красивые деления + const tickStep = rawMax > 40 ? 10 : rawMax > 20 ? 5 : 2 + const maxV = Math.ceil(rawMax / tickStep) * tickStep + tickStep + const minV = 0 const yV = v => PT + cH - (v - minV) / (maxV - minV) * cH - function renderPanel (bars, offsetX, title, defaultColor, isLeftPanel) { + // Рисуем одну панель + function renderPanel (bars, offsetX, title, defaultColor, showYLabels) { const n = bars.length - const cW = panelW - PL - PR - const slotW = cW / n - const barW = Math.min(50, slotW * 0.68) + const slotW = cWp / n + const barW = Math.min(52, slotW * 0.68) let s = '' - // Заголовок - s += `${esc(title)}` + // Заголовок панели + 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 += `` + // Y grid + лейблы + for (let v = 0; v <= maxV; v += tickStep) { + const y = yV(v) + s += `` + if (showYLabels) { + s += `${v}` } } - // Ось X внизу - 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)}` + s += `${b.value.toFixed(1)}` // Подпись под осью (повёрнутая) - const labelX = cx, labelY = yV(0) + 12 - const maxLen = Math.floor(slotW / 4.8) + const labelX = cx + const labelY = yV(0) + 12 + const maxLen = Math.floor(slotW / 4.6) const name = b.label.length > maxLen ? b.label.slice(0, maxLen) + '…' : b.label s += `${esc(name)}` }) - // Подпись единиц - s += `${esc(unit)}` + // Подпись единиц под панелью + 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 leftSvg = renderPanel(left.bars, 0, left.title, GREEN, true) + const rightSvg = renderPanel(right.bars, panelW + DIVIDER_GAP, right.title, ORANGE, false) - // Разделительная линия между панелями - const divX = panelW + gap / 2 - const dividerLine = `` + // Пунктирный разделитель между панелями + const divX = panelW + DIVIDER_GAP / 2 + const divider = `` const fullSvg = ` DairyTrends · dairy-news.ru/dairytrends ${esc(regionLabel)} - ${leftSvg}${dividerLine}${rightSvg} + ${leftSvg}${divider}${rightSvg} ` return {