diff --git a/frontend/biochem-properties.html b/frontend/biochem-properties.html
index 93516c1..f5e6bdf 100644
--- a/frontend/biochem-properties.html
+++ b/frontend/biochem-properties.html
@@ -480,15 +480,79 @@ function renderCompare() {
| Описание | ${_compare.map(m=>`${m.description||'—'} | `).join('')}
`;
+ // график молярных масс + экспорт
+ html += `
+
+
Молярная масса, г/моль
+
+
+
+
`;
}
area.innerHTML = html;
// Draw compare canvases
setTimeout(() => {
for (const mol of _compare) drawCompareCanvas(mol);
+ if (_compare.length >= 2) drawMassChart();
}, 0);
}
+// ── Столбчатый график молярных масс ──
+function drawMassChart() {
+ const cvs = document.getElementById('cmp-chart');
+ if (!cvs) return;
+ const W = cvs.width, H = cvs.height, ctx = cvs.getContext('2d');
+ ctx.clearRect(0, 0, W, H);
+ const data = _compare.map(m => ({ label: m.formula, v: molarMass(m.formula), name: m.name_ru }));
+ const maxV = Math.max(...data.map(d => d.v), 1);
+ const padB = 34, padT = 18, padL = 8, padR = 8;
+ const n = data.length, gap = 18;
+ const bw = Math.min(90, (W - padL - padR - gap * (n - 1)) / n);
+ const colors = ['#9B5DE5', '#06D6E0', '#facc15', '#4ade80'];
+ // ось
+ ctx.strokeStyle = 'rgba(255,255,255,.1)'; ctx.lineWidth = 1;
+ ctx.beginPath(); ctx.moveTo(padL, H - padB); ctx.lineTo(W - padR, H - padB); ctx.stroke();
+ const totalW = bw * n + gap * (n - 1);
+ let x = (W - totalW) / 2;
+ data.forEach((d, i) => {
+ const bh = (d.v / maxV) * (H - padB - padT);
+ const y = H - padB - bh;
+ const col = colors[i % colors.length];
+ const grd = ctx.createLinearGradient(0, y, 0, H - padB);
+ grd.addColorStop(0, col); grd.addColorStop(1, col + '55');
+ ctx.fillStyle = grd;
+ ctx.beginPath();
+ if (ctx.roundRect) ctx.roundRect(x, y, bw, bh, 6); else ctx.rect(x, y, bw, bh);
+ ctx.fill();
+ ctx.fillStyle = '#ddd'; ctx.font = '700 11px Manrope,sans-serif'; ctx.textAlign = 'center';
+ ctx.fillText(d.v.toFixed(1), x + bw / 2, y - 5);
+ ctx.fillStyle = '#888'; ctx.font = '600 10px Manrope,sans-serif';
+ ctx.fillText(d.label.length > 10 ? d.label.slice(0, 9) + '…' : d.label, x + bw / 2, H - padB + 14);
+ x += bw + gap;
+ });
+}
+
+// ── Экспорт сравнения в CSV ──
+function exportCompareCSV() {
+ const rows = [['Свойство', ..._compare.map(m => m.name_ru)]];
+ const cell = v => `"${String(v == null ? '—' : v).replace(/"/g, '""')}"`;
+ rows.push(['Формула', ..._compare.map(m => m.formula)]);
+ rows.push(['Молярная масса, г/моль', ..._compare.map(m => { const v = molarMass(m.formula); return v > 0 ? v.toFixed(2) : '—'; })]);
+ rows.push(['Агр. состояние', ..._compare.map(m => getPhysProps(m.formula).state)]);
+ rows.push(['Растворимость', ..._compare.map(m => getPhysProps(m.formula).solubility)]);
+ rows.push(['T кипения', ..._compare.map(m => getPhysProps(m.formula).bp)]);
+ rows.push(['T плавления', ..._compare.map(m => getPhysProps(m.formula).mp)]);
+ rows.push(['Категория', ..._compare.map(m => catLabel(m.category))]);
+ const csv = '' + rows.map(r => r.map(cell).join(',')).join('\r\n');
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
+ const a = document.createElement('a');
+ a.href = URL.createObjectURL(blob);
+ a.download = 'сравнение-молекул.csv';
+ a.click();
+ URL.revokeObjectURL(a.href);
+}
+
// Per-card 3D view state: id -> { on, rotY, anim }
const _cc3d = {};
function _stopCC(id) { const s = _cc3d[id]; if (s && s.anim) { cancelAnimationFrame(s.anim); s.anim = null; } }