feat(biochem): Фаза 6 — график молярных масс + экспорт сравнения в CSV
biochem-properties.html: при сравнении 2+ молекул — столбчатый график молярных масс (canvas, градиентные столбцы с подписями) и кнопка «Экспорт CSV» (UTF-8 BOM, экранирование, скачивание таблицы свойств). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -480,15 +480,79 @@ function renderCompare() {
|
||||
<tr><td class="row-label">Описание</td>${_compare.map(m=>`<td class="mol-val" style="font-size:.7rem;color:#888">${m.description||'—'}</td>`).join('')}</tr>
|
||||
</tbody>
|
||||
</table></div>`;
|
||||
// график молярных масс + экспорт
|
||||
html += `<div class="compare-chart-wrap" style="margin-top:14px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);border-radius:14px;padding:14px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||||
<div class="detail-label" style="margin:0">Молярная масса, г/моль</div>
|
||||
<button onclick="exportCompareCSV()" style="height:28px;padding:0 12px;border-radius:8px;border:1.5px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);color:#aaa;font:700 .72rem Manrope,sans-serif;cursor:pointer">Экспорт CSV</button>
|
||||
</div>
|
||||
<canvas id="cmp-chart" width="600" height="200" style="width:100%;height:auto;display:block"></canvas>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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; } }
|
||||
|
||||
Reference in New Issue
Block a user