Video
![]()
![]()
Editorial hero for a report




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
Payments are non-refundable. See our Terms of service.
Yours. Download it, then add it to your agent.
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
Video
![]()
![]()
Editorial hero for a report
Video1 view, 0 downloads
![]()
![]()
Lavender page for an AI app
Video
![]()
![]()
Product page for headphones
Video
![]()
![]()
Page with a node-flow hero
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/.
What it did in run 1
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 -30No. 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.
Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode.
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.
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.
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.
Node 20+ and Chrome, and no API key. Each use costs nothing beyond your agent.
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.
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.
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.