Skip to content

DirectorySkills

Headline trend chart: 1440, made in this run
Run 1Headline trend chart“Make a sign-ups card with a trend line for our app dashboard. 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, 10 Nov
Needs
Node 20+Chrome
Licence
Recaply licence

Headline Trend Chart

A headline figure with its change, a 7-day, 30-day or 12-month switch, and a line chart compared with the period before.

$9.99once, yours to keep

Builds a summary card with a headline figure and a line chart into your own project: a white card: "Revenue" with the total for the period in large type and its change against the period before (+12.4%), a switch for 7 days, 30 days and 12 months, then a smooth blue line over a soft fill with the previous period as a dashed grey line; a crosshair and tooltip follow the pointer or the arrow keys. 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, admin or analytics page needs one key measure over time (revenue, sign-ups, visits), or someone asks for a line chart card, trend chart or KPI with a chart.

By RecaplyUpdated 12 Nov

Install Headline Trend Chart

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

Read https://recaply.co/skills/headline-trend-chart/install.md and follow it to install the headline-trend-chart skill from headline-trend-chart.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 Headline Trend Chart is for

What you get

  • One HTML file with its CSS inside: no build step, no framework
  • A summary card with a headline figure and a line chart
  • Checked at phone and desktop widths

Good fit

  • When a dashboard, admin or analytics page needs one key measure over time

Not for

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

About this skill

You say

Make a sign-ups card with a trend line for our app dashboard. Data in brief.md.

It said back

The sign-ups card is built and passes the check at 390, 768 and 1440px. That covers sideways overflow, small text, contrast, focus styles, reduced motion and script errors. I looked at the 390 and 1440px screenshots, and fixed one overlapping x-axis label. I didn't open the 768px screenshot, and I didn't click the 6 months switch in a browser.

Files (in signups/): index.html, signups.css, signups.js. Plain HTML, CSS and JS, since there was no existing project to match. Screenshots are in check/.

Show more

What it did in run 1

  1. headline-trend-chart
  2. ls -la && cat brief.md
  3. cd ./.claude/skills/headline-trend-chart && cat reference/index.html reference/style.css reference/script.js; ls ../../..
  4. cd ./.claude/skills/headline-trend-chart && cat reference/index.html reference/style.css reference/script.js
  5. cd . && mkdir -p signups && cd signups && 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>Pocketplan admin – Sign-ups</title> <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=Inter:wght@400;500;600;700&display=swap"> <link rel="stylesheet" href="signups.css"> </head> <body> <main id="main"> <section class="wrap" aria-labelledby="su-h"> <h1 class="sr-only">Pocketplan admin dashboard</h1> <div class="d-card"> <div class="d-top"> <div> <h2 class="d-title" id="su-h">Sign-ups</h2> <p class="d-big" data-total>1,880</p> <p><span class="d-delta up" data-delta>Up 16.0%</span> <span class="d-sub" data-vs>on the previous 14 days</span></p> </div> <div class="seg" role="group" aria-label="Period"> <button type="button" data-range="14d" aria-pressed="true">14 days</button> <button type="button" data-range="6m" aria-pressed="false">6 months</button> </div> </div> <p class="key"><span style="--c:var(--accent)"><i></i>Sign-ups</span><span style="--c:var(--prev)" data-key-prev><i></i>Previous 14 days, daily average</span></p> <div class="plot-wrap"> <div class="plot" tabindex="0" role="group" aria-label="Sign-ups chart. Use the left and right arrow keys to read each point." aria-describedby="su-sum"></div> <p class="sr-only" aria-live="polite" data-live></p> </div> <p class="d-hint" id="su-sum" data-summary></p> </div> </section> </main> <script src="signups.js" defer></script> </body> </html> EOF cat > signups.css <<'EOF' :root{--bg:#F4F6FA;--surface:#fff;--ink:#111827;--muted:#5B6476;--line:#E3E7EF;--accent:#0D9488;--em:#0F766E;--good:#047857;--bad:#B91C1C;--prev:#8A94A6;--font:'Inter',system-ui,sans-serif} *,*::before,*::after{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--font);font-size:1.0625rem;line-height:1.6;-webkit-font-smoothing:antialiased} p,h1,h2{margin:0} button{font:inherit;color:inherit} :focus-visible{outline:3px solid var(--accent);outline-offset:3px;border-radius:4px} .wrap{width:min(100% - 40px,720px);margin:clamp(24px,6vw,64px) auto} .sr-only{position:absolute!important;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} .d-card{background:var(--surface);border:1px solid var(--line);border-radius:18px;padding:clamp(16px,2.6vw,28px)} .d-top{display:flex;flex-wrap:wrap;gap:1rem 1.4rem;align-items:flex-end;justify-content:space-between;margin-bottom:1.2rem} .d-title{font-size:1.1rem;font-weight:600} .d-big{font-size:clamp(2rem,4vw,2.8rem);font-weight:700;line-height:1.1;font-variant-numeric:tabular-nums;letter-spacing:-.02em} .d-sub{color:var(--muted);font-size:.95rem} .d-delta{font-weight:600;font-size:.95rem} .d-delta.up{color:var(--good)}.d-delta.down{color:var(--bad)} .seg{display:inline-flex;border:1px solid var(--line);border-radius:999px;padding:3px;gap:2px} .seg button{min-height:40px;min-width:44px;padding:0 .9rem;border:0;border-radius:999px;background:none;cursor:pointer;font-size:.95rem;font-weight:500;color:var(--muted)} .seg button:hover{color:var(--ink)} .seg button[aria-pressed=true]{background:var(--ink);color:var(--surface)} .key{display:flex;flex-wrap:wrap;gap:.3rem 1.1rem;font-size:.95rem;color:var(--muted);margin-bottom:.6rem} .key span{display:inline-flex;align-items:center;gap:.45rem} .key i{width:12px;height:12px;border-radius:3px;background:var(--c)} .key span:last-child i{background:repeating-linear-gradient(90deg,var(--c) 0 4px,transparent 4px 7px)} .plot{position:relative;height:320px;margin-top:.4rem} .plot:focus-visible{outline:3px solid var(--accent);outline-offset:6px;border-radius:8px} .chart-svg{display:block;overflow:visible} .chart-svg text{font-family:var(--font);font-size:13px;fill:var(--muted)} .tip{position:absolute;z-index:5;pointer-events:none;background:var(--ink);color:var(--surface);border-radius:10px;padding:.5rem .75rem;font-size:.92rem;line-height:1.4;white-space:nowrap} .tip b{display:block} .d-hint{font-size:.95rem;color:var(--muted);margin-top:.9rem;max-width:72ch} @media (max-width:760px){.plot{height:260px}} @media (prefers-reduced-motion:reduce){*{transition-duration:.01ms!important;animation-duration:.01ms!important}} EOF cat > signups.js <<'EOF' // Sign-ups card: line chart, optional dashed reference line, crosshair + tooltip (pointer and keyboard). const NS = 'http://www.w3.org/2000/svg'; const svgEl = (t, a, p) => { const e = document.createElementNS(NS, t); for (const k in a || {}) e.setAttribute(k, a[k]); p && p.appendChild(e); return e; }; const svgText = (p, x, y, s, a) => { const t = svgEl('text', Object.assign({ x, y }, a), p); t.textContent = s; return t; }; const fmt = (n, d = 0) => Number(n).toLocaleString('en-GB', { minimumFractionDigits: d, maximumFractionDigits: d }); const sum = (a) => a.reduce((x, y) => x + y, 0); const niceTop = (max) => { const st = [10, 20, 25, 50, 100, 200, 250, 500, 1000].find((s) => max / s <= 4); return { st, top: Math.ceil(max / st) * st }; }; // ---- Data from brief.md ---- const DAILY = [120, 134, 128, 151, 162, 98, 87, 140, 155, 149, 171, 180, 110, 95]; const PREV_DAILY_TOTAL = 1620; const MONTHLY = [2900, 3100, 3450, 3300, 3800, 4050]; // Labels count back from today (the last point is today / this month). const today = new Date(); const dayLab = DAILY.map((_, i) => { const d = new Date(today); d.setDate(today.getDate() - (DAILY.length - 1 - i)); return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }); }); const monLab = MONTHLY.map((_, i) => new Date(today.getFullYear(), today.getMonth() - (MONTHLY.length - 1 - i), 1).toLocaleDateString('en-GB', { month: 'short' })); const RANGES = { '14d': { cur: DAILY, lab: dayLab, ref: PREV_DAILY_TOTAL / 14, refName: 'previous 14 days avg', total: sum(DAILY), change: 100 * (sum(DAILY) - PREV_DAILY_TOTAL) / PREV_DAILY_TOTAL, vs: 'on the previous 14 days', keyPrev: 'Previous 14 days, daily average', unit: 'day' }, // No previous-6-months figure in the brief, so compare the latest month with the month before. '6m': { cur: MONTHLY, lab: monLab, ref: null, total: sum(MONTHLY), change: 100 * (MONTHLY[5] - MONTHLY[4]) / MONTHLY[4], vs: 'latest month on the month before', unit: 'month' }, }; let R = RANGES['14d'], at = -1, geo = null; const host = document.querySelector('.plot'); const css = getComputedStyle(document.documentElement); const ACC = css.getPropertyValue('--accent').trim(), PREV = css.getPropertyValue('--prev').trim(), LINE = css.getPropertyValue('--line').trim(), SURF = css.getPropertyValue('--surface').trim(); const live = document.querySelector('[data-live]'); const tipEl = document.createElement('div'); tipEl.className = 'tip'; tipEl.hidden = true; host.appendChild(tipEl); function tipShow(x, y, html) { tipEl.innerHTML = html; tipEl.hidden = false; const w = tipEl.offsetWidth, h = tipEl.offsetHeight, W = host.clientWidth; tipEl.style.left = Math.max(0, Math.min(W - w, x - w / 2)) + 'px'; tipEl.style.top = (y - h - 12 < -6 ? y + 16 : y - h - 12) + 'px'; } const svg = svgEl('svg', { class: 'chart-svg', 'aria-hidden': 'true', focusable: 'false' }); host.prepend(svg); function draw() { const w = Math.round(host.clientWidth), h = Math.round(host.clientHeight); if (!w || !h) return; svg.setAttribute('width', w); svg.setAttribute('height', h); svg.setAttribute('viewBox', 0 0 ${w} ${h}); svg.replaceChildren(); const L = 44, Rm = 8, T = 12, B = 30, pw = w - L - Rm, ph = h - T - B, n = R.cur.length; const { st, top } = niceTop(Math.max(...R.cur, R.ref || 0) * 1.05); const x = (i) => L + (i / (n - 1)) * pw, y = (v) => T + ph - (v / top) * ph; for (let v = 0; v <= top; 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), { 'text-anchor': 'end' }); } const every = n > 6 ? Math.ceil(n / (w < 560 ? 4 : 7)) : 1; R.lab.forEach((l, i) => { if (i % every === 0 || i === n - 1 && every === 1) svgText(svg, x(i), h - 8, l, { 'text-anchor': i === 0 ? 'start' : i === n - 1 ? 'end' : 'middle' }); }); const g = svgEl('linearGradient', { id: 'sg', x1: 0, x2: 0, y1: 0, y2: 1 }, svgEl('defs', {}, svg)); svgEl('stop', { offset: 0, 'stop-color': ACC, 'stop-opacity': .16 }, g); svgEl('stop', { offset: 1, 'stop-color': ACC, 'stop-opacity': 0 }, g); // smooth line (monotone-ish Catmull-Rom to Bezier, clamped so it never dips below the baseline) const P = R.cur.map((v, i) => [x(i), y(v)]); let d = M${P[0][0]} ${P[0][1]}; for (let i = 0; i < n - 1; i++) { const p0 = P[i - 1] || P[i], p1 = P[i], p2 = P[i + 1], p3 = P[i + 2] || p2, k = 0.18; d += C${p1[0] + (p2[0] - p0[0]) * k} ${p1[1] + (p2[1] - p0[1]) * k},${p2[0] - (p3[0] - p1[0]) * k} ${p2[1] - (p3[1] - p1[1]) * k},${p2[0]} ${p2[1]}; } svgEl('path', { d: ${d}L${x(n - 1)} ${y(0)}L${x(0)} ${y(0)}Z, fill: 'url(#sg)' }, svg); if (R.ref != null) svgEl('line', { x1: x(0), x2: x(n - 1), y1: y(R.ref), y2: y(R.ref), stroke: PREV, 'stroke-width': 1.6, 'stroke-dasharray': '5 4' }, svg); svgEl('path', { d, fill: 'none', stroke: ACC, 'stroke-width': 2.5, 'stroke-linejoin': 'round', 'stroke-linecap': 'round' }, svg); const cross = svgEl('line', { y1: T, y2: T + ph, stroke: '#9AA3B2', 'stroke-width': 1, visibility: 'hidden' }, svg); const dot = svgEl('circle', { r: 5, fill: ACC, stroke: SURF, 'stroke-width': 2.5, visibility: 'hidden' }, svg); geo = { x, y, cross, dot, L, step: pw / (n - 1) }; if (at >= 0) show(at); } function show(i) { at = i; if (!geo) return; if (i < 0) { geo.cross.setAttribute('visibility', 'hidden'); geo.dot.setAttribute('visibility', 'hidden'); tipEl.hidden = true; return; } const X = geo.x(i), Y = geo.y(R.cur[i]); geo.cross.setAttribute('x1', X); geo.cross.setAttribute('x2', X); geo.cross.setAttribute('visibility', 'visible'); geo.dot.setAttribute('cx', X); geo.dot.setAttribute('cy', Y); geo.dot.setAttribute('visibility', 'visible'); tipShow(X, Y, <b>${R.lab[i]}</b>${fmt(R.cur[i])} sign-ups + (R.ref != null ? · avg before ${fmt(R.ref)} : '')); } const speak = (i) => ${R.lab[i]}: ${fmt(R.cur[i])} sign-ups + (R.ref != null ? , previous 14 days averaged ${fmt(R.ref)} a day : ''); host.addEventListener('pointermove', (e) => show(Math.max(0, Math.min(R.cur.length - 1, Math.round((e.clientX - host.getBoundingClientRect().left - geo.L) / geo.step))))); host.addEventListener('pointerleave', () => show(-1)); const setAt = (k) => { show(Math.max(0, Math.min(R.cur.length - 1, k))); live.textContent = speak(at); }; host.addEventListener('keydown', (e) => { const n = R.cur.length, k = { ArrowRight: at + 1, ArrowLeft: at - 1, Home: 0, End: n - 1 }[e.key]; if (k !== undefined) { e.preventDefault(); setAt(at < 0 ? n - 1 : k); } else if (e.key === 'Escape') show(-1); }); host.addEventListener('focus', () => { if (at < 0) setAt(R.cur.length - 1); }); host.addEventListener('blur', () => show(-1)); if ('ResizeObserver' in window) { let w0, h0; new ResizeObserver(() => { if (host.clientWidth !== w0 || host.clientHeight !== h0) { w0 = host.clientWidth; h0 = host.clientHeight; draw(); } }).observe(host); } function update() { const ch = R.change; document.querySelector('[data-total]').textContent = fmt(R.total); const dl = document.querySelector('[data-delta]'); dl.textContent = (ch >= 0 ? 'Up ' : 'Down ') + fmt(Math.abs(ch), 1) + '%'; dl.className = 'd-delta ' + (ch >= 0 ? 'up' : 'down'); document.querySelector('[data-vs]').textContent = R.vs; const kp = document.querySelector('[data-key-prev]'); kp.hidden = R.ref == null; if (R.ref != null) kp.lastChild.textContent = R.keyPrev; const first = R.lab[0], last = R.lab[R.lab.length - 1], hi = R.cur.indexOf(Math.max(...R.cur)); document.querySelector('[data-summary]').textContent = Sign-ups from ${first} to ${last}: ${fmt(R.total)} in total, ${ch >= 0 ? 'up' : 'down'} ${fmt(Math.abs(ch), 1)}% ${R.vs}. Busiest ${R.unit}: ${R.lab[hi]} with ${fmt(R.cur[hi])}.; at = -1; tipEl.hidden = true; draw(); } document.querySelectorAll('[data-range]').forEach((b) => b.addEventListener('click', () => { R = RANGES[b.dataset.range]; document.querySelectorAll('[data-range]').forEach((x) => x.setAttribute('aria-pressed', String(x === b))); update(); })); update(); EOF node ../.claude/skills/headline-trend-chart/scripts/check.mjs --ready; node ../.claude/skills/headline-trend-chart/scripts/check.mjs index.html --out check 2>&1 | tail -30
  6. ./signups/index.html
  7. ./signups/signups.css
  8. ./signups/signups.js
  9. cd . && S=.claude/skills/headline-trend-chart/scripts/check.mjs; node $S --ready; node $S signups/index.html --out check 2>&1 | tail -30
  10. cd . && node .claude/skills/headline-trend-chart/scripts/check.mjs signups/index.html --out check 2>&1 | tail -30
  11. ./check/1440.png
  12. ./check/390.png
  13. ./signups/signups.js
  14. node .claude/skills/headline-trend-chart/scripts/check.mjs signups/index.html --out check 2>&1 | tail -5
  15. ./check/390.png
  16. Done

10 Nov 2026 · Claude Code 2.1.322 with Claude Sonnet 5.5 · 2 min · $0.35 of API use

Questions

Is Headline Trend Chart free?

No. Headline Trend Chart 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 Headline Trend Chart?

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

What do I get when I buy Headline Trend Chart?

A ZIP of the whole skill folder (35 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 Headline Trend Chart?

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 ./headline-trend-chart with your agent's flag.

What do I get from Headline Trend Chart?

One HTML file with its CSS inside: no build step, no framework. A summary card with a headline figure and a line chart. Checked at phone and desktop widths.

What does Headline Trend Chart need?

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

How big is Headline Trend Chart?

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

What does a run cost?

Our 1 runs took 2 min to 2 min and $0.35 to $0.35 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 Headline Trend Chart to use it?

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