From cc7332c7ceba68308d194e335bfe8ccb07d2a9d9 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 30 May 2026 13:19:49 +0300 Subject: [PATCH] =?UTF-8?q?feat(biochem):=20=D0=A4=D0=B0=D0=B7=D0=B0=206?= =?UTF-8?q?=20=E2=80=94=20=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=BB=D1=8F=D1=80=D0=BD=D1=8B=D1=85=20=D0=BC=D0=B0=D1=81?= =?UTF-8?q?=D1=81=20+=20=D1=8D=D0=BA=D1=81=D0=BF=D0=BE=D1=80=D1=82=20?= =?UTF-8?q?=D1=81=D1=80=D0=B0=D0=B2=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2?= =?UTF-8?q?=20CSV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit biochem-properties.html: при сравнении 2+ молекул — столбчатый график молярных масс (canvas, градиентные столбцы с подписями) и кнопка «Экспорт CSV» (UTF-8 BOM, экранирование, скачивание таблицы свойств). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/biochem-properties.html | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) 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; } }