Files
ThothII/presentation/prototype/scatter-variants.html
T
Codex 0f1391e1b7 feat: AritmoLab AI presentation deck (work in progress)
Reveal.js deck + prototypes + speaker scripts for the talk
'Role of AI in the Analysis of Unstructured Clinical Databases'
(AritmoLab — Policlinico San Donato, 2-3 October 2026).

Lives on its own branch while in progress; not for main until ready.
2026-09-06 23:06:28 +02:00

356 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PROTOTYPE · Scatter layouts — emblem icons (slides 02 & 03)</title>
<link rel="stylesheet" href="../deck/fonts.css">
<style>
:root {
--bordeaux: #cb333b; --bordeaux-deep: #8f1204; --bordeaux-tint: #f4dfe1;
--surface: #fdfcfc; --surface-alt: #f6f5f5; --surface-sunken: #ecebeb;
--ink: #2a2426; --ink-muted: #6b6568;
--line: #e3e0e1; --line-strong: #c9c5c6;
--sans: 'Source Sans 3', system-ui, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: var(--surface-sunken); font-family: var(--sans); padding: 36px 0 90px; color: var(--ink); }
.note { width: 1280px; margin: 0 auto 18px; padding: 12px 18px; background: #fff8e6; border: 1px solid #e8d9a8;
border-radius: 8px; font: 400 13px/1.5 var(--sans); color: #6b5d2a; }
.note b { font-weight: 700; }
.switcher { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
display: flex; align-items: center; gap: 6px; padding: 8px 10px; background: #2a2426;
border-radius: 9999px; box-shadow: 0 6px 24px rgba(0,0,0,.25); z-index: 10; white-space: nowrap; }
.switcher .tag { font: 700 10px/1 var(--mono); letter-spacing: .1em; color: #9c9598; padding: 0 10px; }
.switcher a { display: inline-flex; height: 30px; align-items: center; padding: 0 14px; border-radius: 9999px;
font: 700 12px/1 var(--sans); color: #fdfcfc; text-decoration: none; opacity: .75; }
.switcher a.active { background: var(--bordeaux); opacity: 1; }
.switcher .who { font: 400 11px/1 var(--sans); color: #9c9598; padding: 0 12px 0 8px; max-width: 430px; white-space: normal; }
.slide { position: relative; width: 1280px; height: 720px; margin: 0 auto 34px;
overflow: hidden; background: var(--surface); color: var(--ink); border: 1px solid var(--line);
border-radius: 8px; display: flex; flex-direction: column; }
.head { flex: 0 0 104px; display: flex; align-items: center; justify-content: space-between;
padding: 0 40px; background: var(--surface); border-bottom: 1px solid var(--line); }
.head img { height: 80px; display: block; }
.head-org { font: 700 11.5px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-muted); }
.band { flex: 0 0 46px; display: flex; align-items: center; justify-content: space-between;
padding: 0 40px; background: var(--bordeaux); color: var(--surface); }
.band-section { font: 700 13px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; }
.foot { flex: 0 0 34px; display: flex; align-items: center; justify-content: space-between; gap: 24px;
padding: 0 40px; border-top: 1px solid var(--line); font: 600 10px/1.4 var(--sans);
letter-spacing: .03em; color: var(--ink-muted); }
.foot span { white-space: nowrap; }
.foot span:first-child { overflow: hidden; text-overflow: ellipsis; }
.sbody { flex: 1; display: flex; flex-direction: column; padding: 26px 48px 16px; }
.kicker { font: 700 12px/1 var(--sans); letter-spacing: .16em; text-transform: uppercase;
color: var(--bordeaux); margin-bottom: 12px; }
h2 { font: 700 30px/1.15 var(--sans); color: var(--ink); margin: 0 0 6px; }
.lead { font: 400 15px/1.5 var(--sans); color: var(--ink-muted); margin: 4px 0 0; }
/* ===== scattered islands canvas ===== */
.scatter { position: relative; flex: 1; min-height: 0; }
.isle { position: absolute; display: flex; flex-direction: column; align-items: center; gap: 2px;
cursor: pointer; transform: rotate(var(--rot, 0deg)); transition: transform .18s ease; border: none; background: none;
font-family: var(--sans); width: var(--w, 240px); }
.isle:hover { transform: rotate(var(--rot, 0deg)) scale(1.04); }
.isle:focus-visible { outline: none; transform: rotate(var(--rot, 0deg)) scale(1.04); }
.isle .ico { width: var(--size, 140px); height: var(--size, 140px); color: var(--bordeaux); display: block; }
.isle .nm { font: 700 16px/1.3 var(--sans); color: var(--ink); margin-top: 4px; }
.isle .sc { font: 400 11.5px/1.4 var(--sans); color: var(--ink-muted); max-width: 240px; }
.isle.ghost .ico { color: var(--ink-muted); opacity: .5; }
.isle.ghost .nm { color: var(--bordeaux-deep); font-size: 14px; }
.isle.ghost .sc { font-style: italic; }
/* slide 03 existed zone */
.flow { display: flex; align-items: stretch; gap: 14px; flex: 1; min-height: 0; margin-top: 6px; }
.zone { border-radius: 8px; position: relative; }
.zone > .zlabel { position: absolute; top: 8px; left: 14px; font: 700 10px/1 var(--mono);
letter-spacing: .14em; text-transform: uppercase; }
.zone.existed { flex: 0 0 250px; background: var(--surface-alt); border: 1px dashed var(--line-strong); position: relative; }
.zone.existed > .zlabel { color: var(--ink-muted); }
.zone.built { flex: 1; background: #faf4f5; border: 1px solid #e7c3c6; }
.zone.built > .zlabel { color: var(--bordeaux); }
.zone.built .row { display: flex; align-items: center; gap: 8px; height: 100%; padding: 0 10px; }
.fcol { flex: 1; display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.fbox { background: #fff; border: 1px solid var(--line-strong); border-radius: 6px; padding: 9px 8px; text-align: center; }
.fbox b { display: block; font: 700 12px/1.25 var(--sans); }
.fbox span { display: block; margin-top: 2px; font: 400 10px/1.3 var(--sans); color: var(--ink-muted); }
.fbox.end { background: var(--bordeaux); border-color: var(--bordeaux); color: #fff; }
.fbox.end span { color: rgba(255,255,255,.85); }
.farrow { align-self: center; color: var(--bordeaux); font: 700 14px/1 var(--sans); }
.src-scatter { position: relative; flex: 1; min-height: 0; }
.src-scatter .isle { --w: 120px; position: absolute; }
.src-scatter .isle .ico { width: var(--size, 64px); height: var(--size, 64px); }
.src-scatter .isle .nm { font-size: 11.5px; }
.todo { border: 2px dashed var(--bordeaux); border-radius: 8px; background: #fff; padding: 10px 14px;
margin-bottom: 10px; box-shadow: 0 2px 8px rgba(42,36,38,.06); }
.todo b { display: block; font: 700 15px/1.3 var(--sans); color: var(--bordeaux-deep); }
.todo span { display: block; font: 400 11px/1.4 var(--sans); color: var(--ink-muted); margin-top: 2px; }
.todo-stack { position: absolute; z-index: 2; }
.isle .ico { color: var(--bordeaux); }
.isle.ghost .ico { color: #9a938f; }
.isle .nm { font-size: 15px; }
.isle .sc { font-size: 11.5px; max-width: 230px; }
/* popup */
.ai-pop { position: fixed; inset: 0; z-index: 100; background: rgba(42,36,38,.45);
display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity .2s; }
.ai-pop.open { opacity: 1; pointer-events: auto; }
.ai-pop-card { position: relative; width: min(560px, 92%); background: #fff; border: 1px solid var(--line);
border-radius: 12px; box-shadow: 0 18px 50px rgba(42,36,38,.28); padding: 24px 28px 22px;
transform: translateY(14px) scale(.95); transition: transform .24s cubic-bezier(.2,.9,.3,1.15); }
.ai-pop.open .ai-pop-card { transform: none; }
.ai-pop-kicker { font: 700 10.5px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase; color: var(--bordeaux); margin-bottom: 10px; }
.ai-pop-title { font: 700 18px/1.3 var(--sans); color: var(--ink); margin-bottom: 12px; }
.ai-pop-card p { font: 400 14px/1.65 var(--sans); color: var(--ink); margin: 0 0 12px; }
.pop-list { list-style: none; display: flex; flex-direction: column; gap: 9px; }
.pop-list li { position: relative; padding-left: 20px; font: 400 13.5px/1.5 var(--sans); }
.pop-list li::before { content: ''; position: absolute; left: 0; top: 7px; width: 8px; height: 8px; border-radius: 9999px; background: var(--bordeaux); }
.pop-list b { font-weight: 700; }
.roi { margin-top: 14px; border: 1.5px solid var(--bordeaux); background: #faf4f5; border-radius: 8px; padding: 10px 14px; font: 400 13.5px/1.55 var(--sans); }
.roi .k { display: block; margin-bottom: 4px; font: 800 10.5px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase; color: var(--bordeaux-deep); }
.ai-pop-x { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; border: none; border-radius: 9999px;
background: var(--surface-alt); font: 700 15px/1 var(--sans); color: var(--ink-muted); cursor: pointer; }
</style>
</head>
<body>
<div class="note"><b>EVALUATION</b> — icone Emblems (le tue preferite), <b>più grandi, senza bordi/card</b>, disposte in modo
<b>sparsamente organico</b> invece che a griglia. Tre composizioni per slide 02 e adattamento alla 03.</div>
<main id="deck"></main>
<nav class="switcher">
<span class="tag">PROTOTYPE</span>
<a href="?s=1" data-v="1">A · Constellation</a>
<a href="?s=2" data-v="2">B · Cascade</a>
<a href="?s=3" data-v="3">C · Orbit</a>
<span class="who" id="who"></span>
</nav>
<script>
/* ---- emblem icons (style 3, winner) ---- */
const EMB = {
cardioref: `<circle cx="24" cy="24" r="20" stroke-width="2.6"/>
<path d="M24 33 c-6-3.5-9-7-9-10.5 0-3 4-5.5 7-3 1 .8 1.6 1.8 2 3 .4-1.2 1-2.2 2-3 3-2.5 7 0 7 3 0 3.5-3 7-9 10.5z" fill="currentColor" stroke="none"/>
<path d="M22.5 21 h3 v3 h3 v3 h-3 v3 h-3 v-3 h-3 v-3 h3 z" fill="#fff" stroke="none"/>`,
genetic: `<circle cx="24" cy="24" r="20" stroke-width="2.6"/>
<path d="M18 8 C28 14 28 20 18 26 C12 30 12 35 18 40"/>
<path d="M30 8 C20 14 20 20 30 26 C36 30 36 35 30 40"/>
<path d="M17 12 H31"/><path d="M15.5 18 H32.5"/><path d="M17 24 H31"/><path d="M14.5 31 H22"/>
<rect x="25" y="29" width="12" height="12" rx="1.5" stroke-width="2"/>
<path d="M25 34 H37"/><path d="M30.5 29 V41"/>`,
omics: `<circle cx="24" cy="24" r="20" stroke-width="2.6"/>
<circle cx="24" cy="24" r="4" fill="currentColor" stroke="none"/>
<circle cx="24" cy="10" r="3.4"/><circle cx="11.5" cy="31" r="3.4"/><circle cx="36.5" cy="31" r="3.4"/>
<path d="M24 20.5 V13.5"/><path d="M21 26.5 L14 29.5"/><path d="M27 26.5 L34 29.5"/>`,
ecg: `<circle cx="24" cy="24" r="20" stroke-width="2.6"/>
<path d="M9 24 h6 l3-9 5 16 3-7 h14" stroke-width="2.4"/>
<circle cx="24" cy="24" r="1.8" fill="currentColor" stroke="none"/>`,
};
const GHOST = `<circle cx="24" cy="24" r="20" stroke-dasharray="5 4" stroke-width="2.4"/>
<path d="M15 33 L33 15" stroke-width="2.4"/><circle cx="17" cy="17" r="2.4" fill="currentColor" stroke="none"/>`;
const ico = (key, size) =>
`<svg class="ico" viewBox="0 0 48 48" style="width:${size}px;height:${size}px" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round">${key === 'ghost' ? GHOST : EMB[key]}</svg>`;
const SYS = [
{ key: 'cardioref', name: 'Cardioref', scope: 'visits · operations · reports — twenty years of management on structured forms' },
{ key: 'genetic', name: 'Genetic data', scope: 'DNA instruments — results typed into Excel by hand' },
{ key: 'omics', name: 'Omics Portal', scope: 'meant to host Cardioref + genetics — still immature' },
{ key: 'ecg', name: 'ECG', scope: 'paper strip + a CSV on request' },
];
const GHOSTS = [
{ name: 'No Health Intelligence', scope: 'nothing for historical data' },
{ name: 'No ML-ready datamarts', scope: 'nothing for predictive statistics' },
];
const CONTRIBS = {
cardioref: { kicker: 'The starting point · 20 years ago', title: 'Cardioref — the daily workhorse',
body: `<p class="lead">A traditional relational application: classic forms for the typical hospital procedures.</p>
<ul class="pop-list"><li><b>Used for</b> — visits, operations, reports: the daily management of the Unit</li>
<li><b>Good at it</b> — twenty years of patient management on structured records</li>
<li><b>The catch</b> — the clinical story lives in free-text fields, not in the boxes designed for it</li></ul>
<div class="roi"><b class="k">Problem</b>Almost unusable for research: the knowledge is there — locked in prose.</div>` },
genetic: { kicker: 'The starting point · 20 years ago', title: 'Genetic data — sophisticated in, Excel out',
body: `<p class="lead">The DNA instruments are state-of-the-art. What happens after them is not.</p>
<ul class="pop-list"><li><b>Used for</b> — running the genetic analyses the Unit prescribes</li>
<li><b>The catch</b> — results are transcribed by hand into Excel files</li>
<li><b>Never connected</b> — Cardioref has no idea genetics exists</li></ul>
<div class="roi"><b class="k">Problem</b>Two parallel worlds: genotypes on one side, clinical records on the other.</div>` },
omics: { kicker: 'The starting point · 20 years ago', title: 'Omics Portal — the right idea, too soon',
body: `<p class="lead">A portal meant to host both Cardioref and the genetic data — the seed of AritmoLab.</p>
<ul class="pop-list"><li><b>Used for</b> — the first 360° patient-management features</li>
<li><b>The catch</b> — functionality still immature</li>
<li><b>Never connected</b> — it could not see the data it was meant to host</li></ul>
<div class="roi"><b class="k">Problem</b>A promising idea without a bridge: too young to carry the data.</div>` },
ecg: { kicker: 'The starting point · 20 years ago', title: 'ECG — paper, and a CSV if you ask',
body: `<p class="lead">Classic ECG devices: they print the strip, and they can export a file.</p>
<ul class="pop-list"><li><b>Used for</b> — acquiring and viewing ECG traces</li>
<li><b>The catch</b> — no machine-to-machine communication</li>
<li><b>The only door out</b> — a CSV file, produced on request</li></ul>
<div class="roi"><b class="k">Problem</b>Every ECG meant a manual download — automation was impossible by design.</div>` },
};
const chrome = (band) => `
<header class="head"><img src="logo.png" alt=""><span class="head-org">AritmoLab · Policlinico San Donato</span></header>
<nav class="band"><span class="band-section">${band}</span><span class="jump">0X · …</span></nav>`;
const foot = `<footer class="foot"><span>Role of AI in the Analysis of Unstructured Clinical Databases · Dr. Marco Pancotti - MultiPhysixLab · Dr. Sara Paratico - Gruppo San Donato · San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span>0X / 15</span></footer>`;
function isle(s, size, rot, x, y) {
const pos = x ? `left:${x}; top:${y};` : '';
return `<button class="isle" data-start="${s.key}" style="--rot:${rot}deg; --size:${size}px; ${pos}">
${ico(s.key, size)}<span class="nm">${s.name}</span><span class="sc">${s.scope}</span></button>`;
}
function ghostIsle(g, size, rot, x, y) {
const pos = x ? `left:${x}; top:${y};` : '';
return `<div class="isle ghost" style="--rot:${rot}deg; --size:${size}px; ${pos}">
${ico('ghost', size)}<span class="nm">${g.name}</span><span class="sc">${g.scope}</span></div>`;
}
/* ---- A · Constellation: scattered islands (hand-placed, organic jitter) ---- */
const TODO_STACK = [
['Clinical Intelligence', 'dashboards on the clinical history of the Unit and its patients'],
['Datawarehouse', 'the single source of truth'],
['ML-ready data', 'cohort tables, ready for model training'],
['Professional Portal', 'AritmoLab — the grown-up Omics Portal'],
];
/* three placements of the central "to build" rectangles */
const ORBIT_CFG = {
1: { isles: [
{ key: 'cardioref', x: '0%', y: '2%', rot: -3, size: 124 },
{ key: 'genetic', x: '72%', y: '2%', rot: 3, size: 118 },
{ key: 'omics', x: '0%', y: '56%', rot: -2, size: 120 },
{ key: 'ecg', x: '72%', y: '56%', rot: 2, size: 118 } ],
stack: { left: '27%', top: '20%', width: '46%' }, offsets: ['0%', '8%', '4%', '10%'], icon: 116 },
2: { isles: [
{ key: 'cardioref', x: '36%', y: '4%', rot: -3, size: 118 },
{ key: 'genetic', x: '64%', y: '18%', rot: 3, size: 112 },
{ key: 'omics', x: '36%', y: '48%', rot: -2, size: 114 },
{ key: 'ecg', x: '63%', y: '60%', rot: 2, size: 112 } ],
stack: { left: '3%', top: '16%', width: '25%' }, offsets: ['0%', '10%', '4%', '12%'], icon: 106 },
3: { isles: [
{ key: 'cardioref', x: '1%', y: '6%', rot: -3, size: 120 },
{ key: 'genetic', x: '75%', y: '6%', rot: 3, size: 114 },
{ key: 'omics', x: '1%', y: '56%', rot: -2, size: 118 },
{ key: 'ecg', x: '75%', y: '56%', rot: 2, size: 114 } ],
stack: { left: '25%', top: '20%', width: '50%' }, offsets: ['0%', '7%', '4%', '9%'], icon: 114, grid: true },
};
function orbitSlide02(v) {
const cfg = ORBIT_CFG[v];
let h = `<div class="todo-stack" style="left:${cfg.stack.left}; top:${cfg.stack.top}; width:${cfg.stack.width}">` +
TODO_STACK.map(([n, c], i) => `<div class="todo" style="margin-left:${cfg.offsets[i]}"><b>${n}</b><span>${c}</span></div>`).join('') + `</div>`;
for (const s of cfg.isles) {
const sys = SYS.find(vv => vv.key === s.key);
h += `<button class="isle" data-start="${s.key}" style="--rot:${s.rot}deg; --size:${s.size}px; left:${s.x}; top:${s.y}; width:230px">
${ico(s.key, s.size)}<span class="nm">${sys.name}</span><span class="sc">${sys.scope}</span></button>`;
}
const ghosts = [
['No Health Intelligence', 'nothing for historical data', '8%', '72%'],
['No ML-ready datamarts', 'nothing for predictive research', '62%', '72%'],
];
for (const [name, scope, x, y] of ghosts) {
h += `<div class="isle ghost" style="--rot:0deg; --size:92px; left:${x}; top:${y}; width:220px">
${ico('ghost', 92)}<span class="nm" style="color:var(--bordeaux-deep)">${name}</span><span class="sc">${scope}</span></div>`;
}
return h;
}
function layoutZone(v) {
const items = [
['cardioref', 62, -3, '2%', '0%'],
['genetic', 62, 3, '52%', '16%'],
['omics', 62, -2, '4%', '38%'],
['ecg', 62, 3, '54%', '60%'],
];
let h = items.map(([k, size, rot, x, y]) => {
const s = SYS.find(vv => vv.key === k);
return `<div class="isle" style="--rot:${rot}deg; --size:${size}px; left:${x}; top:${y}; width:170px">
${ico(k, size)}<span class="nm">${s.name}</span></div>`;
}).join('');
h += `<div class="isle ghost" style="--rot:-2deg; --size:56px; left:32%; top:82%; width:170px">
${ico('ghost', 56)}<span class="nm" style="color:var(--bordeaux-deep)">missing</span></div>`;
return h;
}
const LAYOUTS = {
1: { desc: 'A · Center stack: i quattro obiettivi impilati al centro, i sistemi in orbita attorno.' },
2: { desc: 'B · Side column: la pila degli obiettivi a sinistra, i sistemi orbitano più larghi a destra.' },
3: { desc: 'C · Compact 2×2: griglia compatta e sfalsata al centro, orbita larga.' },
};
const q = new URLSearchParams(location.search);
const v = ['1','2','3'].includes(q.get('s')) ? q.get('s') : '1';
document.body.className = 's' + v;
document.querySelectorAll('.switcher a').forEach(a => a.classList.toggle('active', a.dataset.v === v));
document.getElementById('who').textContent = LAYOUTS[v].desc;
document.getElementById('deck').innerHTML = `
<section class="slide">
${chrome('Where we started')}
<div class="sbody">
<div class="kicker">Twenty years ago</div>
<h2>Four islands and two missing pieces</h2>
<div class="scatter">${orbitSlide02(v)}</div>
</div>
${foot}
</section>
<section class="slide">
${chrome('The plan')}
<div class="sbody">
<div class="kicker">The plan · what we wanted to build</div>
<h2>One platform, two destinations</h2>
<div class="flow">
<div class="zone existed">
<span class="zlabel">What existed — to connect</span>
<div class="src-scatter">${layoutZone(v)}</div>
</div>
<div class="farrow">→</div>
<div class="zone built">
<span class="zlabel">What we wanted to build</span>
<div class="row">
<div class="fcol"><div class="fbox"><b>Staging</b><span>raw replica</span></div></div>
<div class="farrow">→</div>
<div class="fcol"><div class="fbox"><b>Integration</b><span>clean · text mining</span></div></div>
<div class="farrow">→</div>
<div class="fcol"><div class="fbox"><b>Star schema</b><span>warehouse</span></div></div>
<div class="farrow">→</div>
<div class="fcol"><div class="fbox"><b>Datamarts</b><span>for research</span></div></div>
<div class="farrow">→</div>
<div class="fcol"><div class="fbox end"><b>AritmoLab</b><span>portal + warehouse</span></div></div>
</div>
</div>
</div>
</div>
${foot}
</section>`;
const pop = document.createElement('div');
pop.className = 'ai-pop';
pop.innerHTML = `<div class="ai-pop-card" role="dialog"><button class="ai-pop-x">✕</button>
<div class="ai-pop-kicker" id="kick"></div><div class="ai-pop-title" id="ttl"></div><div id="bdy"></div></div>`;
document.body.appendChild(pop);
document.addEventListener('click', (e) => {
const s = e.target.closest('[data-start]');
if (s) {
const c = CONTRIBS[s.dataset.start];
document.getElementById('kick').textContent = c.kicker;
document.getElementById('ttl').textContent = c.title;
document.getElementById('bdy').innerHTML = c.body;
pop.classList.add('open');
return;
}
if (e.target.closest('.ai-pop-x') || e.target === pop) pop.classList.remove('open');
});
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') pop.classList.remove('open'); });
</script>
</body>
</html>