Skip to content

DirectorySkills

KPI sparkline panel: 1440, made in this run
Run 1KPI sparkline panel“Make a row of KPI cards with little trend lines for our clinic dashboard; clicking one should open its chart. Data in brief.md.”
Runs in
  • Claude Code
  • Codex
  • Cursor
  • Gemini CLI
  • GitHub Copilot
  • OpenCode
Generates with
Your agent's model
Tested with
Claude Code 2.1.322Claude Sonnet 5.51 real run, 12 Nov
Needs
Node 20+Chrome
Licence
Recaply licence

KPI Sparkline Panel

Four KPI tabs, each with its figure, change and a sparkline; the chosen KPI opens as a full line chart underneath.

$9.99once, yours to keep

Builds a panel of KPI cards with sparklines that drive a larger line chart into your own project: a row of four KPI cards (active users, new sign-ups, churn, average session), each showing the figure, a coloured change with words and a small sparkline; the chosen card is outlined in violet and its 12-week line chart fills the panel below with points and a tooltip. Plain HTML, CSS and JavaScript, or matched to React, Next.js, Vue, Svelte, Astro or Tailwind when the project uses them. Comes with a working reference and a check that screenshots it at phone, tablet and desktop widths and flags sideways overflow, text under 12px, missing alt text and names, low contrast, no focus styles, motion that ignores reduced motion, leftover sample words and emoji icons. Use when a dashboard needs several headline measures at once with a way to look closer at one, or someone asks for KPI cards, sparklines or metric tiles with a chart.

By RecaplyUpdated 12 Nov

Install KPI Sparkline Panel

After the download, ask your agent to add it, or run the line yourself.

Read https://recaply.co/skills/kpi-sparkline-panel/install.md and follow it to install the kpi-sparkline-panel skill from kpi-sparkline-panel.zip.

Paste it into your agent. It reads our install steps, finds the ZIP you downloaded and adds the skill for you.

New to skills? How to install a skill

Similar skills

All 735 skills

Who KPI Sparkline Panel is for

What you get

  • One HTML file with its CSS inside: no build step, no framework
  • A panel of KPI cards with sparklines that drive a larger line chart
  • Checked at phone and desktop widths

Good fit

  • When a dashboard needs several headline measures at once with a way to look closer at one

Not for

  • An app with a backend, sign-in or a database
  • Copying another company's site

About this skill

You say

Make a row of KPI cards with little trend lines for our clinic dashboard; clicking one should open its chart. Data in brief.md.

It said back

I've built the row of four KPI cards with trend lines. Clicking a card opens its 8-week line chart underneath. Every check passed (no sideways scrolling, text sizes, contrast, focus styles, reduced motion, no script errors). I looked at the 390px and 1440px screenshots and both read cleanly. I didn't open the 768px one. I haven't tried clicking the cards or using the keyboard in a browser.

What's on the page

  • Cards: appointments booked, no-show rate, average wait and patient rating. Each shows the latest figure, the change since week 35 in words ("Up 29.7% since week 35 · better") and a small trend line.
  • Chart: the selected card gets a blue outline and its chart fills the panel below, with a tooltip on hover.
  • Layout: four cards in a row on desktop, two by two on tablet, one column on phone.
  • Keyboard and screen reader: arrow keys, Home and End switch cards. In the chart, the arrow keys read out each week.
  • Table: a "Show the numbers as a table" toggle lists all eight weeks.
Show more

What it did in run 1

  1. kpi-sparkline-panel
  2. ./brief.md
  3. cd .; ls -a; S=.claude/skills/kpi-sparkline-panel; ls $S/reference; cat $S/reference/index.html $S/reference/style.css $S/reference/script.js; node $S/scripts/check.mjs --ready
  4. ls -a && ls .claude/skills/kpi-sparkline-panel/reference && cat .claude/skills/kpi-sparkline-panel/reference/index.html .claude/skills/kpi-sparkline-panel/reference/style.css .claude/skills/kpi-sparkline-panel/reference/script.js && node .claude/skills/kpi-sparkline-panel/scripts/check.mjs --ready
  5. wc -c index.html script.js style.css && head -c 600 index.html; diff -q style.css .claude/skills/kpi-sparkline-panel/reference/style.css; diff -q script.js .claude/skills/kpi-sparkline-panel/reference/script.js
  6. R=.claude/skills/kpi-sparkline-panel/reference && cp $R/style.css style.css && python3 -I - <<'EOF' import re s=open('style.css').read() s=s.replace('--bg:#F4F1EC','--bg:#F3F6FB').replace('--line:#E6E0D6','--line:#DCE3EF').replace('--muted:#5D574D','--muted:#4A5568').replace('--ink:#1E1B16','--ink:#14213D') s=s.replace('--accent:#6D28D9','--accent:#2563EB').replace('--em:#6D28D9','--em:#2563EB') s=s.replace('background:#FBF8FF','background:#F5F8FF').replace('border-color:#C9BFAF','border-color:#AFC0DE') # drop unused reveal/btn rules are harmless; add marker + note styles s+='\n.kp-note{color:var(--muted);font-size:.95rem;margin-top:.9rem;max-width:72ch}\n.chart-svg .mk{stroke:var(--muted);stroke-dasharray:4 4}\n.chart-svg .mk-t{fill:var(--ink);font-weight:600}\n' open('style.css','w').write(s) EOF cat > index.html <<'EOF' <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Steadyline clinic dashboard: weekly KPIs</title> <meta name="description" content="Steadyline clinic weekly figures for weeks 35 to 42 of 2026: appointments booked, no-show rate, average wait and patient rating."> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap"> <link rel="stylesheet" href="style.css"> </head> <body> <a class="skip" href="#main">Skip to content</a> <main id="main"> <section class="d-sec" id="kpis" aria-labelledby="kpis-h"> <div class="sec-head"><h2 id="kpis-h">Clinic <em>at a glance</em></h2><p class="sub">Weekly figures for weeks 35 to 42 of 2026. Choose a measure to see its chart.</p></div> <div class="wrap"> <div class="kp-tabs" role="tablist" aria-label="Measures"> <button class="kp" role="tab" type="button" id="kp-t0" aria-controls="kp-panel" aria-selected="true" tabindex="0"><span class="kp-n">Appointments booked</span><span class="kp-v">402</span><span class="d-delta up">Up 29.7% since week 35 · better</span><span class="kp-spark" aria-hidden="true"></span></button> <button class="kp" role="tab" type="button" id="kp-t1" aria-controls="kp-panel" aria-selected="false" tabindex="-1"><span class="kp-n">No-show rate</span><span class="kp-v">6.8%</span><span class="d-delta up">Down 2.7 pts since week 35 · better</span><span class="kp-spark" aria-hidden="true"></span></button> <button class="kp" role="tab" type="button" id="kp-t2" aria-controls="kp-panel" aria-selected="false" tabindex="-1"><span class="kp-n">Average wait</span><span class="kp-v">4.1 days</span><span class="d-delta up">Down 2.1 days since week 35 · better</span><span class="kp-spark" aria-hidden="true"></span></button> <button class="kp" role="tab" type="button" id="kp-t3" aria-controls="kp-panel" aria-selected="false" tabindex="-1"><span class="kp-n">Patient rating</span><span class="kp-v">4.7</span><span class="d-delta up">Up 0.2 since week 35 · better</span><span class="kp-spark" aria-hidden="true"></span></button> </div> <div class="d-card kp-panel" id="kp-panel" role="tabpanel" aria-labelledby="kp-t0"> <div class="d-top"><div><p class="d-title" data-ktitle>Appointments booked</p><p class="d-sub" data-ksub>Weekly, weeks 35 to 42</p></div></div> <div class="plot-wrap"><div class="plot kp-plot" tabindex="0" role="group" aria-label="Chart of the chosen measure. Use the left and right arrow keys to read each week."></div><p class="sr-only" aria-live="polite" data-live></p></div> <p class="kp-note" data-knote></p> <details class="d-table"><summary>Show the numbers as a table</summary><div class="dt-wrap"><table class="dt"><caption class="sr-only">All four measures, week by week</caption><thead><tr><th scope="col">Week</th><th scope="col">Appointments booked</th><th scope="col">No-show rate</th><th scope="col">Average wait (days)</th><th scope="col">Patient rating</th></tr></thead><tbody> <tr><th scope="row">Week 35</th><td>310</td><td>9.5%</td><td>6.2</td><td>4.5</td></tr> <tr><th scope="row">Week 36</th><td>322</td><td>9.1%</td><td>5.9</td><td>4.5</td></tr> <tr><th scope="row">Week 37</th><td>335</td><td>8.8%</td><td>5.7</td><td>4.6</td></tr> <tr><th scope="row">Week 38</th><td>341</td><td>8.2%</td><td>5.1</td><td>4.6</td></tr> <tr><th scope="row">Week 39</th><td>360</td><td>7.9%</td><td>4.9</td><td>4.6</td></tr> <tr><th scope="row">Week 40</th><td>371</td><td>7.4%</td><td>4.6</td><td>4.7</td></tr> <tr><th scope="row">Week 41</th><td>388</td><td>7.0%</td><td>4.3</td><td>4.6</td></tr> <tr><th scope="row">Week 42</th><td>402</td><td>6.8%</td><td>4.1</td><td>4.7</td></tr> </tbody></table></div></details> <script type="application/json" id="kp-data">{"first":35,"kpis":[ {"name":"Appointments booked","s":[310,322,335,341,360,371,388,402],"unit":"","dec":0,"note":"Bookings rose every week, from 310 to 402."}, {"name":"No-show rate","s":[9.5,9.1,8.8,8.2,7.9,7.4,7.0,6.8],"unit":"%","dec":1,"mark":[37,"Text reminders start"],"note":"Lower is better. The rate fell from 9.5% to 6.8%; text reminders started in week 37."}, {"name":"Average wait","s":[6.2,5.9,5.7,5.1,4.9,4.6,4.3,4.1],"unit":" days","dec":1,"note":"Lower is better. The average wait shortened from 6.2 to 4.1 days."}, {"name":"Patient rating","s":[4.5,4.5,4.6,4.6,4.6,4.7,4.6,4.7],"unit":"","dec":1,"note":"Ratings edged up from 4.5 to 4.7, with one dip in week 41."} ]}</script> </div> </div> </section> </main> <script src="script.js" defer></script> </body> </html> EOF cat > script.js <<'EOF' document.documentElement.classList.add('js'); const NS = 'http://www.w3.org/2000/svg'; function svgEl(tag, attrs, parent) { const e = document.createElementNS(NS, tag); for (const k in attrs || {}) e.setAttribute(k, attrs[k]); if (parent) parent.appendChild(e); return e; } function svgText(parent, x, y, s, attrs) { const t = svgEl('text', Object.assign({ x: x, y: y }, attrs || {}), parent); t.textContent = s; return t; } function niceStep(max, n) { const raw = max / (n || 4), p = Math.pow(10, Math.floor(Math.log10(raw))); return [1, 2, 2.5, 5, 10].map(function (m) { return m * p; }).find(function (s) { return s >= raw; }); } function fmt(n, d) { return Number(n).toLocaleString('en-GB', { minimumFractionDigits: d || 0, maximumFractionDigits: d || 0 }); } function plot(host, draw) { const svg = svgEl('svg', { class: 'chart-svg', 'aria-hidden': 'true', focusable: 'false' }); host.prepend(svg); let w0 = -1, h0 = -1; const render = function () { const w = Math.round(host.clientWidth), h = Math.round(host.clientHeight); if (!w || !h) return; w0 = w; h0 = h; svg.setAttribute('width', w); svg.setAttribute('height', h); svg.setAttribute('viewBox', '0 0 ' + w + ' ' + h); svg.replaceChildren(); draw(svg, w, h); }; if ('ResizeObserver' in window) new ResizeObserver(function () { if (Math.round(host.clientWidth) !== w0 || Math.round(host.clientHeight) !== h0) render(); }).observe(host); render(); return render; } function tooltip(host) { const t = document.createElement('div'); t.className = 'tip'; t.hidden = true; host.appendChild(t); return { show: function (x, y, html) { t.innerHTML = html; t.hidden = false; const w = t.offsetWidth, h = t.offsetHeight, W = host.clientWidth; t.style.left = Math.max(0, Math.min(W - w, x - w / 2)) + 'px'; t.style.top = (y - h - 12 < -6 ? y + 16 : y - h - 12) + 'px'; }, hide: function () { t.hidden = true; } }; } function reader(host, count, move, say) { const live = host.parentElement.querySelector('[data-live]'); let i = -1; const set = function (k) { i = Math.max(0, Math.min(count() - 1, k)); move(i); if (live && say) live.textContent = say(i); }; host.addEventListener('keydown', function (e) { const k = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: count() - 1 }[e.key]; if (k !== undefined) { e.preventDefault(); set(i < 0 ? count() - 1 : k); } else if (e.key === 'Escape') { i = -1; move(-1); } }); host.addEventListener('focus', function () { if (i < 0) set(count() - 1); }); host.addEventListener('blur', function () { i = -1; move(-1); }); } // KPI tabs with sparklines; the chosen KPI drives the big chart. const D = JSON.parse(document.getElementById('kp-data').textContent), K = D.kpis, N = K[0].s.length; const css = getComputedStyle(document.documentElement); const ACC = css.getPropertyValue('--accent').trim(), LINE = css.getPropertyValue('--line').trim(), MUT = css.getPropertyValue('--muted').trim(), SURF = css.getPropertyValue('--surface').trim(); const val = function (k, v) { return fmt(v, K[k].dec) + K[k].unit; }; const wk = function (i) { return D.first + i; }; let cur = 0, at = -1, geo = null; const tabs = [...document.querySelectorAll('.kp')]; const path = function (s, x, y) { return 'M' + s.map(function (v, i) { return x(i).toFixed(1) + ' ' + y(v).toFixed(1); }).join('L'); }; tabs.forEach(function (t, k) { plot(t.querySelector('.kp-spark'), function (svg, w, h) { const s = K[k].s, mn = Math.min.apply(null, s), mx = Math.max.apply(null, s), c = t.getAttribute('aria-selected') === 'true' ? ACC : MUT; const x = function (i) { return 2 + (i / (N - 1)) * (w - 8); }, y = function (v) { return 3 + (1 - (v - mn) / (mx - mn || 1)) * (h - 6); }; svgEl('path', { d: path(s, x, y), fill: 'none', stroke: c, 'stroke-width': 2, 'stroke-linejoin': 'round' }, svg); svgEl('circle', { cx: x(N - 1), cy: y(s[N - 1]), r: 3, fill: c }, svg); }); }); const host = document.querySelector('.kp-plot'), tip = tooltip(host); const render = plot(host, function (svg, w, h) { const k = K[cur], s = k.s, L = 52, Rm = 14, T = 14, B = 30, pw = w - L - Rm, ph = h - T - B; let mn = Math.min.apply(null, s), mx = Math.max.apply(null, s); const pad = (mx - mn) * 0.25 || 1; const st = niceStep(mx - mn + 2 * pad, 4); mn = Math.floor((mn - pad) / st + 1e-9) * st; mx = Math.ceil((mx + pad) / st - 1e-9) * st; const dec = Math.max(k.dec, (String(st).split('.')[1] || '').length), pct = k.unit === '%' ? '%' : ''; const x = function (i) { return L + (i / (N - 1)) * pw; }, y = function (v) { return T + (1 - (v - mn) / (mx - mn)) * ph; }; for (let v = mn; v <= mx + st / 2; v += st) { svgEl('line', { x1: L, x2: w - Rm, y1: y(v), y2: y(v), stroke: LINE }, svg); svgText(svg, L - 8, y(v) + 4, fmt(v, dec) + pct, { 'text-anchor': 'end' }); } for (let i = 0; i < N; i++) if (w >= 480 || i % 2 === 0) svgText(svg, x(i), h - 8, 'W' + wk(i), { 'text-anchor': 'middle' }); if (k.mark) { const mi = k.mark[0] - D.first, mxp = x(mi); svgEl('line', { class: 'mk', x1: mxp, x2: mxp, y1: T, y2: T + ph }, svg); svgText(svg, mxp + 6, T + 14, k.mark[1], { class: 'mk-t' }); } svgEl('path', { d: path(s, x, y), fill: 'none', stroke: ACC, 'stroke-width': 2.5, 'stroke-linejoin': 'round' }, svg); s.forEach(function (v, i) { svgEl('circle', { cx: x(i), cy: y(v), r: 3.5, fill: SURF, stroke: ACC, 'stroke-width': 2 }, svg); }); const ring = svgEl('circle', { r: 7, fill: 'none', stroke: ACC, 'stroke-width': 2, visibility: 'hidden' }, svg); geo = { x: x, y: y, ring: ring, L: L, step: pw / (N - 1) }; if (at >= 0) show(at); }); function show(i) { at = i; if (!geo) return; if (i < 0) { geo.ring.setAttribute('visibility', 'hidden'); tip.hide(); return; } const v = K[cur].s[i]; geo.ring.setAttribute('cx', geo.x(i)); geo.ring.setAttribute('cy', geo.y(v)); geo.ring.setAttribute('visibility', 'visible'); tip.show(geo.x(i), geo.y(v), '<b>Week ' + wk(i) + '</b>' + K[cur].name + ': ' + val(cur, v)); } host.addEventListener('pointermove', function (e) { const r = host.getBoundingClientRect(); show(Math.max(0, Math.min(N - 1, Math.round((e.clientX - r.left - geo.L) / geo.step)))); }); host.addEventListener('pointerleave', function () { show(-1); }); reader(host, function () { return N; }, show, function (i) { return 'Week ' + wk(i) + ', ' + K[cur].name + ': ' + val(cur, K[cur].s[i]); }); function pick(k, focus) { cur = k; tabs.forEach(function (t, j) { t.setAttribute('aria-selected', String(j === k)); t.tabIndex = j === k ? 0 : -1; const c = j === k ? ACC : MUT, sp = t.querySelector('.kp-spark svg'); if (sp) sp.querySelectorAll('path,circle').forEach(function (p) { p.setAttribute(p.tagName === 'path' ? 'stroke' : 'fill', c); }); }); if (focus) tabs[k].focus(); document.getElementById('kp-panel').setAttribute('aria-labelledby', 'kp-t' + k); document.querySelector('[data-ktitle]').textContent = K[k].name; document.querySelector('[data-knote]').textContent = K[k].note; at = -1; tip.hide(); render(); } tabs.forEach(function (t, k) { t.addEventListener('click', function () { pick(k); }); t.addEventListener('keydown', function (e) { const n = tabs.length, j = { ArrowRight: (k + 1) % n, ArrowLeft: (k - 1 + n) % n, Home: 0, End: n - 1 }[e.key]; if (j !== undefined) { e.preventDefault(); pick(j, true); } }); }); document.querySelector('[data-knote]').textContent = K[0].note; EOF node .claude/skills/kpi-sparkline-panel/scripts/check.mjs index.html --out check 2>&1 | tail -40
  7. cp .claude/skills/kpi-sparkline-panel/reference/style.css style.css && sed -i '' -e 's/--bg:#F4F1EC/--bg:#F3F6FB/' -e 's/--line:#E6E0D6/--line:#DCE3EF/' -e 's/--muted:#5D574D/--muted:#4A5568/' -e 's/--ink:#1E1B16/--ink:#14213D/' -e 's/--accent:#6D28D9/--accent:#2563EB/' -e 's/--em:#6D28D9/--em:#2563EB/' -e 's/background:#FBF8FF/background:#F5F8FF/' -e 's/border-color:#C9BFAF/border-color:#AFC0DE/' style.css && printf '\n.kp-note{color:var(--muted);font-size:.95rem;margin-top:.9rem;max-width:72ch}\n.chart-svg .mk{stroke:var(--muted);stroke-dasharray:4 4}\n.chart-svg .mk-t{fill:var(--ink);font-weight:600}\n' >> style.css && grep -c 2563EB style.css
  8. ./style.css
  9. ./index.html
  10. ./script.js
  11. node .claude/skills/kpi-sparkline-panel/scripts/check.mjs index.html --out check 2>&1 | tail -40
  12. sed -i '' -e 's/aria-label="Chart of the chosen measure. Use the left and right arrow keys to read each week."/aria-label="Weekly line chart for the selected clinic measure. Press left or right arrow to hear each week."/' -e 's|<link rel="preconnect" href="https://fonts.googleapis.com">|<meta property="og:title" content="Steadyline clinic dashboard: weekly KPIs"><meta property="og:description" content="Appointments booked, no-show rate, average wait and patient rating for weeks 35 to 42 of 2026."><meta property="og:type" content="website">\n&|' index.html && node .claude/skills/kpi-sparkline-panel/scripts/check.mjs index.html --out check 2>&1 | tail
  13. ./index.html
  14. ./index.html
  15. node .claude/skills/kpi-sparkline-panel/scripts/check.mjs index.html --out check 2>&1 | tail
  16. ./check/1440.png
  17. ./check/390.png
  18. Done

12 Nov 2026 · Claude Code 2.1.322 with Claude Sonnet 5.5 · 2 min 25 s · $0.43 of API use

Questions

Is KPI Sparkline Panel free?

No. KPI Sparkline Panel costs $9.99 once and stays in your Recaply account, or it comes with the Recaply plan ($19.99 a month), which opens every premium skill and prompt.

Which agents can use KPI Sparkline Panel?

Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode.

What do I get when I buy KPI Sparkline Panel?

A ZIP of the whole skill folder (34 KB): the SKILL.md your agent follows and its 6 other files, the references, templates and scripts it uses. You download it from this page whenever you need it, and install it in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode. The Recaply plan opens it too, with every other premium skill and prompt.

How do I install KPI Sparkline Panel?

Buy it, then download the ZIP from this page. For Claude Code, unzip it into .claude/skills/ in your project. For Codex, Cursor and Gemini CLI, unzip it and run npx skills add ./kpi-sparkline-panel with your agent's flag.

What do I get from KPI Sparkline Panel?

One HTML file with its CSS inside: no build step, no framework. A panel of KPI cards with sparklines that drive a larger line chart. Checked at phone and desktop widths.

What does KPI Sparkline Panel need?

Node 20+ and Chrome, and no API key. Each use costs nothing beyond your agent.

How big is KPI Sparkline Panel?

7 files, about 22,496 tokens in all. The SKILL.md is about 2,153 tokens, and your agent reads it only when the job fits.

What does a run cost?

Our 1 runs took 2 min 25 s to 2 min 25 s and $0.43 to $0.43 of API use each, in Claude Code with claude-sonnet-5-5. On a Claude plan, a run counts toward your usage instead.

Do I have to name KPI Sparkline Panel to use it?

Type /kpi-sparkline-panel first to make sure it loads. Without it, Claude Code may judge a small job simple enough to do without the skill.