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; } }