spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { ExpansionVelocity } from "@dci/core";2import { StepLines } from "@/components/intelligence/client-charts";3import { HonestEmpty } from "@/components/intelligence/shared";4import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";5import { fmtInt, fmtMw } from "@/lib/format";67const WINDOW_LABEL: Record<ExpansionVelocity["windows"][number]["label"], string> = { "12m": "Last 12 months", "3y": "Last 3 years", "5y": "Last 5 years" };89/**10 * Expansion velocity: three windows (12 m / 3 y / 5 y) as a compact table, then the cumulative11 * countries / metros / facilities step chart. Every point carries its basis: "opened" = published12 * opening date; "first_seen" = when the index first observed the facility (NOT when it opened).13 */14export function ExpansionVelocityBlock({ velocity }: { velocity: ExpansionVelocity }) {15 const windows = velocity.windows;16 const series = velocity.countriesOverTime;17 const bases = new Set(series.map((p) => p.basis));18 const firstSeenYears = series.filter((p) => p.basis === "first_seen").map((p) => p.year);19 const fmtN = (n: number | null | undefined) => (n === null || n === undefined ? <span className="text-ink-4">—</span> : fmtInt(n));20 const fmtM = (n: number | null | undefined) => (n === null || n === undefined ? <span className="text-ink-4" title="No published MW figure">—</span> : fmtMw(n));2122 return (23 <div className="flex flex-col gap-5">24 {windows.length ? (25 <Table dense>26 <THead>27 <tr>28 <Th>Window</Th>29 <Th numeric>New facilities</Th>30 <Th numeric>New projects</Th>31 <Th numeric>New countries</Th>32 <Th numeric>New markets</Th>33 <Th numeric className="hidden sm:table-cell">34 Opened MW35 </Th>36 <Th numeric className="hidden sm:table-cell">37 Announced MW38 </Th>39 </tr>40 </THead>41 <TBody>42 {windows.map((w) => (43 <Tr key={w.label}>44 <Td primary>{WINDOW_LABEL[w.label]}</Td>45 <Td label="New facilities" numeric lead>46 {fmtN(w.newFacilities)}47 </Td>48 <Td label="New projects" numeric>49 {fmtN(w.newProjects)}50 </Td>51 <Td label="New countries" numeric>52 {fmtN(w.newCountries)}53 </Td>54 <Td label="New markets" numeric>55 {fmtN(w.newMetros)}56 </Td>57 <Td label="Opened MW" numeric className="hidden sm:table-cell">58 {fmtM(w.openedMw)}59 </Td>60 <Td label="Announced MW" numeric className="hidden sm:table-cell">61 {fmtM(w.announcedMw)}62 </Td>63 </Tr>64 ))}65 </TBody>66 </Table>67 ) : (68 <HonestEmpty title="No dated expansion yet" detail="windows need opening, announcement or first-observation dates" />69 )}70 <p className="text-[11.5px] text-ink-3">71 “New facilities” counts facilities whose opening date — or, when none is published, the date the index first observed them — falls in the window. MW columns show published figures only.72 </p>7374 <div>75 <div className="label-caps mb-2">Cumulative footprint by year</div>76 {series.length >= 2 ? (77 <>78 <StepLines79 height={180}80 format="int"81 series={[82 { name: "Countries", color: "var(--ink)", data: series.map((p) => ({ x: p.year, y: p.countries })) },83 { name: "Markets", color: "var(--st-planned)", data: series.map((p) => ({ x: p.year, y: p.metros })) },84 { name: "Facilities", color: "var(--accent)", dashed: true, data: series.map((p) => ({ x: p.year, y: p.facilities })) },85 ]}86 ariaLabel="Cumulative countries, markets and facilities by year"87 />88 <ul className="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-[11.5px] text-ink-3">89 {series.map((p) => (90 <li key={p.year} className="figure">91 {p.year}92 <span className="text-ink-4"> · {p.basis === "opened" ? "opened" : "first seen"}</span>93 </li>94 ))}95 </ul>96 </>97 ) : (98 <HonestEmpty title="Not enough dated points to chart" detail="a step chart appears once two or more years are known" />99 )}100 <p className="mt-2 text-[11.5px] text-ink-3">101 Basis per point: <strong className="font-medium text-ink-2">opened</strong> = published opening date; <strong className="font-medium text-ink-2">first seen</strong> = the year the index first observed the facility, which is <em>not</em> when it opened.102 {bases.has("first_seen") && firstSeenYears.length > 0 && <> The {firstSeenYears.length === 1 ? "point for" : "points for"} {firstSeenYears.join(", ")} {firstSeenYears.length === 1 ? "is" : "are"} first-seen values and overstate recent growth.</>}103 </p>104 </div>105 </div>106 );107}108