SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
4.9 KB · 90 lines tsx
Raw Blame History
1import Link from 'next/link';2import { Section } from '@/components/ui/section';3import { EmptyState } from '@/components/ui/empty-state';4import { Freshness } from '@/components/ui/freshness';5import { Badge, ClaimBadge } from '@/components/ui/badge';6import { SourceBadge } from '@/components/ui/source-badge';7import { getGeographyBySlug, cagrFor, CAGR_FORMULA, CAGR_FORMULA_VERSION } from '@/lib/queries/geography';8import { fmtNum, fmtValue, fmtInt, toDate } from '@/lib/format';910/**11 * Home module "Fastest-rising incidence" (§105, §328): ASIR compound annual growth rate over the last ten12 * available years, computed on the fly from observations. Shown only when ≥ 10 years exist; labelled as13 * derived, with the formula and both endpoints on every row so the number can be reproduced.14 */15export async function RisingModule({ slug = 'united-states', limit = 8 }: { slug?: string; limit?: number }) {16  const geo = await getGeographyBySlug(slug);17  const res = geo ? await cagrFor(geo.id, 'as_incidence_rate', 'all', 10) : null;18  const rows = res?.rows.slice(0, limit) ?? [];19  const first = rows[0];20  return (21    <Section22      id="rising"23      kicker="Trend"24      title={`Fastest-rising incidence${geo ? ` · ${geo.name}` : ''}`}25      description={res ? `Age-standardized incidence rate, both sexes, all ages: compound annual growth rate between ${res.startYear} and ${res.endYear} (the last 10 years published). Derived by CancerIndex from registry observations; not a source figure.` : 'Age-standardized incidence rate: compound annual growth rate over the last 10 published years.'}26      actions={27        geo ? (28          <Link href={`/country/${slug}#trend`} className="ci-link">29            Trend chart →30          </Link>31        ) : undefined32      }33    >34      {!res || rows.length === 0 ? (35        <EmptyState title="Not enough years to compute a trend">36          A 10-year growth rate is computed only when at least ten distinct years of age-standardized incidence exist for the geography{res ? ` (${res.yearsAvailable} available)` : ''}. Nothing is extrapolated from shorter series.37        </EmptyState>38      ) : (39        <>40          <div className="ci-table-wrap">41            <table className="ci-table">42              <thead>43                <tr>44                  <th>Cancer</th>45                  <th className="num">CAGR (%/yr)</th>46                  <th className="num">{res.startYear} (per 100,000)</th>47                  <th className="num">{res.endYear} (per 100,000)</th>48                  <th className="num">Years</th>49                </tr>50              </thead>51              <tbody>52                {rows.map((r) => (53                  <tr key={r.cancer_id}>54                    <td>55                      <Link className="ci-link" href={`/cancer/${r.slug}/statistics`}>56                        {r.canonical_name}57                      </Link>58                      {r.estimate_types.some((t) => t !== 'observed') ? <Badge tone="warn" className="ml-1.5">includes estimates</Badge> : null}59                    </td>60                    <td className={`num font-medium ${r.cagr > 0 ? 'text-danger' : r.cagr < 0 ? 'text-ok' : ''}`}>61                      {r.cagr > 0 ? '+' : ''}62                      {fmtNum(r.cagr * 100, 2)}63                    </td>64                    <td className="num">{fmtValue(r.start_value, r.unit)}</td>65                    <td className="num">{fmtValue(r.end_value, r.unit)}</td>66                    <td className="num text-ink-3">{fmtInt(r.n_years)}</td>67                  </tr>68                ))}69              </tbody>70            </table>71          </div>72          <div className="mt-2 flex flex-wrap items-center gap-1.5 text-[12px] text-ink-3">73            <ClaimBadge kind="computed" />74            {first ? <SourceBadge p={{ sourceSlug: first.source_slug, layer: 'derived', note: 'Inputs are the two endpoint observations (start and end year) of the source series.' }} /> : null}75            <span>76              formula: <code className="ci-mono">{CAGR_FORMULA}</code>77            </span>78            <span className="ci-mono">{CAGR_FORMULA_VERSION}</span>79            {first?.standard_population ? <span>standard: {first.standard_population}</span> : null}80          </div>81          <p className="mt-1 text-[12px] text-ink-3">82            Rising incidence can reflect true risk changes, screening or diagnostic practice, coding changes or population ageing beyond what standardization removes; the rate says nothing about causes. Falling rates are listed on the <Link className="ci-link" href="/methodology#cagr">methodology page</Link> definition.83          </p>84          <Freshness dataUpdatedAt={rows.map((r) => toDate(r.updated_at)).filter((d): d is Date => !!d).sort((a, b) => b.getTime() - a.getTime())[0] ?? null} sourceVersion={`${res.startYear}–${res.endYear}`} extra={`${fmtInt(res.rows.length)} cancers with a complete 10-year series`} />85        </>86      )}87    </Section>88  );89}90