Heart disease is notoriously difficult to catch early — not least because the highest-risk patients are often the ones showing no symptoms at all. In this project we build a Heart Disease Risk Intelligence Dashboard that visualises the UCI Cleveland dataset across 303 patients and uses the Claude API to surface the patterns a human analyst might miss.
You can see the live dashboard here: tiny-biscuit-82b936.netlify.app

The Dataset
The dataset comes from the UCI Cleveland Clinic Heart Disease study. It contains 303 patient records with 14 clinical features including age, sex, chest pain type, resting blood pressure, serum cholesterol, and maximum heart rate achieved. 165 of the 303 patients tested positive for heart disease, giving a prevalence rate of 54.5%.
All the raw data is baked directly into the dashboard as a JavaScript object, keeping the project dependency-free and instantly deployable.
const DATA = { age: [ { label:'< 45', rate:20, d:12, t:58 }, { label:'45–54', rate:44, d:39, t:88 }, { label:'55–64', rate:62, d:60, t:97 }, { label:'65+', rate:72, d:43, t:60 }, ], cp: [ { label:'Typical Angina', rate:28, d:8, t:23 }, { label:'Atypical Angina', rate:42, d:21, t:50 }, { label:'Non-anginal Pain', rate:47, d:39, t:86 }, { label:'Asymptomatic', rate:75, d:105,t:144 }, ], // ...scatter and donut data};KPI Cards with Animated Counters
The four headline numbers at the top of the dashboard animate up from zero on load using GSAP. Each card reads its target value from a data-val attribute, so adding a new metric is as simple as dropping in a new card element.
function counter(el, to, sfx, dur = 1.5) { const float = String(to).includes('.'); gsap.to({ v: 0 }, { v: to, duration: dur, ease: 'power3.out', onUpdate: function () { el.textContent = (float ? this.targets()[0].v.toFixed(1) : Math.round(this.targets()[0].v)) + sfx; } });}Scroll-triggered versions of the same function fire when each chart card enters the viewport, so numbers only count up once they’re actually visible.
Bar Charts and the Scatter Plot
The bar charts are built from plain HTML and CSS — no charting library needed. Each bar starts at width: 0 and expands to its target percentage via a CSS transition triggered by a ScrollTrigger callback. Bars are coloured red when prevalence exceeds 50% and slate otherwise, giving an immediate at-a-glance risk signal.
function mkBars(id, items) { const el = document.getElementById(id); el.innerHTML = items.map(d => ` <div class="group space-y-1.5 cursor-default"> <div class="flex justify-between text-[11px] font-bold uppercase tracking-wider text-slate-500"> <span>${d.label}</span> <span class="font-mono">${d.rate}% (${d.d}/${d.t})</span> </div> <div class="h-3 bg-slate-100 rounded-full overflow-hidden"> <div class="h-full rounded-full transition-all duration-1000" style="width:0; background:${d.rate > 50 ? '#dc2626' : '#64748b'};" data-w="${d.rate}"> </div> </div> </div>`).join('');
ScrollTrigger.create({ trigger: el, start: 'top 95%', once: true, onEnter: () => el.querySelectorAll('[data-w]').forEach((b, i) => setTimeout(() => b.style.width = b.dataset.w + '%', i * 80)) });}The scatter plot is raw SVG. Each patient is a <circle> element positioned by age on the x-axis and maximum heart rate on the y-axis, coloured red for positive and blue for negative. Points animate in with a staggered GSAP fade and respond to hover with a tooltip showing the individual’s age, heart rate, and diagnosis.

The AI Insights Panel
The most interesting part of the dashboard is the AI panel, which calls the Claude API to generate a clinical summary of the dataset on load. The response streams back character-by-character using a typewriter effect, giving it the feel of a live analysis rather than static text.
async function runAI() { const el = document.getElementById('ai-text'); el.innerHTML = `<span class="text-slate-400">Crunching 303 patient records...<span class="cursor"></span></span>`;
// Response is typed out character by character setTimeout(() => { el.textContent = ''; el.classList.add('cursor'); let i = 0; function type() { if (i < raw.length) { el.innerHTML += raw[i]; i++; setTimeout(type, 6); } else { el.classList.remove('cursor'); } } type(); }, 800);}
setTimeout(runAI, 1000);Claude picks out three key findings from the data: the asymptomatic blind spot (75% positive rate among patients showing no symptoms), the early decline in maximum heart rate visible from age 45 onwards, and the 240 mg/dl serum cholesterol threshold above which 72% of positive cases cluster. A Refresh button lets you re-run the analysis at any time.

Styling and Animation
The visual identity leans on a tight palette of off-white, slate, and crimson. A subtle paper texture is applied via an SVG feTurbulence filter fixed to the viewport, giving the dashboard a tactile editorial feel without any image assets. Every card lifts slightly on hover through a shared hover-card class.
.hover-card { transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);}.hover-card:hover { transform: translateY(-4px); box-shadow: 0 14px 28px -10px rgba(15, 23, 42, 0.12);}Fonts are served from Google Fonts: Playfair Display for the large numerics and headings, Inter for body copy, and JetBrains Mono for the model tag and raw numbers in the charts.
Dataset source: UCI Cleveland Heart Disease · Kaggle
Built by Hashi Warsame