SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
3.6 KB · 75 lines tsx
Raw Blame History
1import type { MarketConcentration } from "@dci/core";2import { ApiNote, CoverageBar, HonestEmpty, ShareList } from "@/components/intelligence/shared";3import { fmtInt, fmtPct } from "@/lib/format";4import { routes } from "@/lib/routes";56function hhiLabel(hhi: number): string {7  if (hhi >= 2500) return "highly concentrated";8  if (hhi >= 1500) return "moderately concentrated";9  return "unconcentrated";10}1112function Figures({ hhi, top3, top5 }: { hhi: number; top3: number; top5: number }) {13  return (14    <dl className="mb-3 grid grid-cols-3 gap-x-3 border-y border-hair py-2">15      <div className="min-w-0">16        <dt className="label-caps truncate">HHI</dt>17        <dd className="figure text-[18px] font-medium leading-tight text-ink">{fmtInt(hhi)}</dd>18        <dd className="text-[11px] text-ink-3">{hhiLabel(hhi)} · 0–10 000</dd>19      </div>20      <div className="min-w-0">21        <dt className="label-caps truncate">Top-3 share</dt>22        <dd className="figure text-[18px] font-medium leading-tight text-ink">{fmtPct(top3)}</dd>23      </div>24      <div className="min-w-0">25        <dt className="label-caps truncate">Top-5 share</dt>26        <dd className="figure text-[18px] font-medium leading-tight text-ink">{fmtPct(top5)}</dd>27      </div>28    </dl>29  );30}3132/**33 * MARKET CONCENTRATION — computed on facility counts and, separately, on known MW (only when published34 * figures exist). The MW view always shows its coverage and says "incomplete MW data" when partial.35 */36export function MarketConcentrationPanel({ c, facilityCount }: { c: MarketConcentration; facilityCount: number }) {37  const f = c.facilities;38  const mw = c.knownMw;39  return (40    <div className="flex flex-col gap-4">41      <div className="grid gap-6 md:grid-cols-2">42        <div>43          <h3 className="text-[13.5px] font-semibold text-ink">On facility counts</h3>44          <p className="mb-2 text-[11.5px] text-ink-3">45            {fmtInt(c.operatorCount)} operators · {fmtInt(facilityCount)} counted facilities (campus rows with buildings excluded)46          </p>47          <Figures hhi={f.hhi} top3={f.top3Share} top5={f.top5Share} />48          <ShareList items={f.top.slice(0, 10).map((o) => ({ key: o.id, label: o.name, href: routes.operator(o.slug), count: o.count, share: o.share }))} unit="facilities" />49        </div>50        <div>51          <h3 className="text-[13.5px] font-semibold text-ink">On known MW</h3>52          {mw ? (53            <>54              <div className="mb-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-ink-3">55                <span>Published operational MW only</span>56                <CoverageBar value={mw.coverage} label="MW coverage" width="w-[140px]" />57              </div>58              {mw.coverage < 1 && (59                <p className="mb-2 rounded-sm border border-warn/30 bg-warn/5 px-2.5 py-1.5 text-[12px] text-ink-2" role="note">60                  <strong className="text-ink">Incomplete MW data</strong> — {fmtPct(mw.coverage)} of facilities publish a figure; shares below describe the published subset, not the market.61                </p>62              )}63              <Figures hhi={mw.hhi} top3={mw.top3Share} top5={mw.top5Share} />64              <ShareList items={mw.top.slice(0, 10).map((o) => ({ key: o.id, label: o.name, href: routes.operator(o.slug), count: Math.round(o.mw), share: o.share }))} unit="MW" />65            </>66          ) : (67            <HonestEmpty title="Not computed on MW" detail="no operator in this market publishes an operational MW figure, so a capacity view would be invented" className="mt-2" />68          )}69        </div>70      </div>71      <ApiNote note={c.note} label="Method" />72    </div>73  );74}75