Charts

Copy-paste chart examples for every Bklit UI component.

Radial charts are experimental and in active development. APIs may change.

Radial Chart - Dot Bars

24-hour clock of stacked dot columns per 15-minute bin, with yesterday ghosted for the rest of the day and a scrubber that follows the pointer

const series = [  { dataKey: "merged", label: "Merged", color: "var(--chart-1)", rampFrom: 0.4 },  { dataKey: "ready", label: "Ready for merge", color: "var(--chart-2)" },  { dataKey: "draft", label: "Draft", color: "var(--chart-3)", emphasize: true },];<RadialChart data={chartData} xDataKey="time" series={series}>  <RadialAxis centerGradient />  <RadialDotBar    ghostDataKey="yesterday"    annotations={[{ index: 8, label: "02:14", color: "var(--chart-3)" }]}  />  <RadialScrubber />  <RadialCenter>    {({ datum, isFuture, totals, values, label }) => {      // Today vs yesterday over the same bins: a count, not a rate      let merged = totals.merged;      let delta = merged - yesterdayMergedSoFar;      let caption = "merged by 14:30";      if (datum) {        merged = isFuture ? datum.yesterdayMerged : values.merged;        delta = isFuture ? null : merged - datum.yesterdayMerged;        caption = isFuture ? `merged at ${label} yesterday` : `merged at ${label}`;      }      return (        <>          <ChartStatFlow value={merged} label={caption} />          {delta !== null && (            <span style={{ color: delta < 0 ? "var(--chart-negative)" : "var(--chart-positive)" }}>              {delta < 0 ? "−" : "+"}              {Math.abs(delta).toLocaleString()} vs yesterday            </span>          )}        </>      );    }}  </RadialCenter>  <RadialLegend /></RadialChart>// 96 × 15-minute bins, clockwise from 00:00.// Bins after "now" have null counts and are drawn as ghost dots from `yesterday`.const chartData = [  { time: "00:00", merged: 29, ready: 0, draft: 0, yesterday: 29, yesterdayMerged: 28 },  // …  { time: "02:00", merged: 47, ready: 0, draft: 720, yesterday: 86, yesterdayMerged: 83 },  // …  { time: "14:15", merged: 778, ready: 60, draft: 0, yesterday: 881, yesterdayMerged: 848 },  { time: "14:30", merged: null, ready: null, draft: null, yesterday: 804, yesterdayMerged: 778 },  // …];// Yesterday's merged PRs over the bins that have data today (00:00–14:30)const yesterdayMergedSoFar = chartData  .filter((d) => d.merged !== null)  .reduce((total, d) => total + d.yesterdayMerged, 0);

The center counts merged PRs and compares them with yesterday over the same window; hover a bin for that 15 minutes. Hover the legend to isolate a series.

Radial Chart - Patterned Dots

Hourly home energy as one column of large dots per hour: solid solar, diagonally striped grid, and the rest of the day as a striped forecast

const series = [  { dataKey: "solar", label: "Solar", color: "var(--chart-2)" },  { dataKey: "grid", label: "Grid", color: "var(--chart-1)" },];<RadialChart data={chartData} xDataKey="time" series={series}>  <RadialAxis />  <RadialDotBar    layout="columns"    rows={5}    dotSize={0.9}    maxValue={3.2}    ghostDataKey="forecast"    ghostColor="var(--muted-foreground)"    ghostDotSize={0.9}    ghostOpacity={0.6}    patterns={{      grid: { preset: "diagonal", strokeWidth: 1.5 },      ghost: { preset: "diagonal", stroke: "none" },    }}  />  <RadialScrubber variant="dashed" />  <RadialCenter>    {({ datum, isFuture, totals, values, label }) => {      const counts = datum ? values : totals;      let caption = "kWh so far";      if (datum) {        caption = isFuture ? `forecast ${label}` : `kWh at ${label}`;      }      return (        <ChartStatFlow          value={isFuture ? datum.forecast : counts.solar + counts.grid}          formatOptions={{ minimumFractionDigits: 1, maximumFractionDigits: 1 }}          label={caption}        />      );    }}  </RadialCenter>  <RadialLegend formatValue={(v) => `${v.toFixed(1)} kWh`} /></RadialChart>// 24 hourly bins, clockwise from 00:00. Metered until 14:00;// later hours have null readings and show the striped forecast.const chartData = [  { time: "00:00", solar: 0, grid: 0.6, forecast: 0.6 },  // …  { time: "12:00", solar: 1.9, grid: 0.2, forecast: 2 },  // …  { time: "18:00", solar: null, grid: null, forecast: 3.2 },  // …];

Key patterns by series dataKey; the "ghost" key styles the comparison dots. Hover a future hour for its forecast.

Radial Chart - Line

Smooth radial lines around a 24-hour clock: today's merged PRs drawn clockwise up to now, with yesterday dashed for the whole day

const series = [  { dataKey: "merged", label: "Merged today", color: "var(--chart-1)" },];<RadialChart data={chartData} xDataKey="time" series={series}>  <RadialAxis />  {/* Not a series: drawn as a comparison line for the whole day */}  <RadialLine    dataKey="yesterday"    dash    stroke="var(--muted-foreground)"    strokeOpacity={0.7}    strokeWidth={1.5}    maxValue={maxValue}  />  <RadialLine dataKey="merged" strokeWidth={2.25} maxValue={maxValue} />  <RadialScrubber variant="gradient" />  <RadialCenter>    {({ datum, totals, label }) => (      <ChartStatFlow        value={datum ? (datum.merged ?? datum.yesterday) : totals.merged}        label={datum ? `merged at ${label}` : "merged by 14:30"}      />    )}  </RadialCenter>  <RadialLegend /></RadialChart>// 96 × 15-minute bins, clockwise from 00:00.// Today's line ends at "now"; yesterday closes the loop.const chartData = [  { time: "00:00", merged: 44, yesterday: 58 },  // …  { time: "14:15", merged: 846, yesterday: 870 },  { time: "14:30", merged: null, yesterday: 862 },  // …];// Share one scale between the linesconst maxValue = Math.max(...chartData.map((d) => Math.max(d.merged ?? 0, d.yesterday)));

Hover around the clock to compare today and yesterday at the same time of day.

Radial Chart - Temperature

A year of San Francisco temperatures: record and normal ranges as radial bands around the daily average, after d3's radial area chart

const series = [  { dataKey: "avg", label: "Daily average", color: "var(--chart-1)" },];<RadialChart  angleType="time"  data={days}  xDataKey="date"  series={series}  valueKeys={["minmin", "maxmax"]}  minValue="auto"  maxValue="auto"  niceValues={false}>  <RadialAxis showCircle={false} valueTicks={4} valueTickFormat={(v) => `${v}°F`} />  {/* Record extremes */}  <RadialArea    dataKey="maxmax"    baseDataKey="minmin"    color="var(--chart-1)"    fillOpacity={0.14}    rampFrom={1}    stroke="none"    curve="linear"  />  {/* Normal range: average low → average high */}  <RadialArea    dataKey="max"    baseDataKey="min"    color="var(--chart-1)"    fillOpacity={0.28}    rampFrom={1}    stroke="none"    curve="linear"  />  <RadialLine dataKey="avg" strokeWidth={1.5} />  <RadialScrubber variant="gradient" />  <RadialCenter>    {({ datum }) => (      <ChartStatFlow        value={datum ? datum.avg : annualAverage}        label={datum ? formatDay(datum.date) : "annual average"}        suffix="°F"        formatOptions={{ maximumFractionDigits: 1 }}      />    )}  </RadialCenter></RadialChart>// 366 days (plotted on the leap year 2000), °F — SFO daily temperatures// 1999–2018 grouped by day of year (NOAA, via d3's radial area chart).const days = [  { date: Date.UTC(2000, 0, 1), avg: 50.2, min: 43.6, max: 55.7, minmin: 37, maxmax: 62 },  { date: Date.UTC(2000, 0, 2), avg: 49.3, min: 43.9, max: 55.2, minmin: 37, maxmax: 62 },  // …];// Shown in the center at restconst annualAverage = days.reduce((total, d) => total + d.avg, 0) / days.length;const dayFormat = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" });const formatDay = (date) => dayFormat.format(date);

Hover around the year to read a day's average, normal range and records.

Radial Chart - Population by State

US state populations stacked by age band around the circle, sorted by total, with rotated state labels and compact value rings on a radial scale

const series = [  { dataKey: "under14", label: "Under 14" },  { dataKey: "age14to24", label: "14–24" },  { dataKey: "age25to44", label: "25–44" },  { dataKey: "age45to64", label: "45–64" },  { dataKey: "age65plus", label: "65+" },]; // colors default to --chart-1 … --chart-5 by positionconst compact = new Intl.NumberFormat("en-US", { notation: "compact" });<RadialChart  angleType="band"  data={states} // sorted by total, largest first  xDataKey="state"  series={series}  maxValue="stacked">  <RadialBar />  {/* after the bars, so value rings + labels sit on top */}  <RadialAxis    showCircle={false}    valueTicks={[10e6, 20e6, 30e6, 40e6]}    valueTickFormat={(v) => compact.format(v)}  />  <RadialCenter>    {({ datum, totals, values }) => {      const counts = datum ? values : totals;      return (        <ChartStatFlow          value={Object.values(counts).reduce((a, b) => a + b, 0)}          formatOptions={{ notation: "compact", maximumFractionDigits: 1 }}          label={datum ? String(datum.name) : "US population"}        />      );    }}  </RadialCenter>  <RadialLegend showValues={false} /></RadialChart>// 50 states + DC (ACS 2008), sorted by total population.const states = [  { state: "CA", name: "California", under14: 7205000, age14to24: 6014000, age25to44: 10605000, age45to64: 8819000, age65plus: 4114000 },  { state: "TX", name: "Texas", under14: 5305000, age14to24: 3875000, age25to44: 7018000, age45to64: 5657000, age65plus: 2472000 },  // …  { state: "WY", name: "Wyoming", under14: 99000, age14to24: 83000, age25to44: 137000, age45to64: 147000, age65plus: 66000 },];

Hover a state for its total and largest age band; hover the legend to isolate a band. Data: US Census ACS 2008 via d3's radial stacked bar chart.

Radial Chart - Semicircle Stacked Rings

Stacked ring bars on a half circle — one ring per device, content types stacked end to end as a share of its capacity, with % ticks along the arc

const series = [  { dataKey: "photos", label: "Photos", color: "var(--chart-1)" },  { dataKey: "apps", label: "Apps", color: "var(--chart-2)" },  { dataKey: "documents", label: "Documents", color: "var(--chart-3)" },  { dataKey: "system", label: "System", color: "var(--chart-4)" },];<RadialChart  data={chartData}  xDataKey="device"  series={series}  orientation="angular"  startAngle={-90}  endAngle={90}  maxValue={100}  ringGap={0.18}>  <RadialAxis    showCircle={false}    valueTicks={[0, 25, 50, 75, 100]}    valueTickFormat={(v) => `${v}%`}  />  <RadialRingBar cornerRadius={3} segmentGap={3} />  {/* The hole sits on the arc's center: lift the content into the half-disc */}  <RadialCenter className="-translate-y-[30%]">    {({ datum }) => (      <ChartStatFlow        value={datum ? percentUsed(datum) : overallUsed}        formatOptions={{ maximumFractionDigits: 0 }}        suffix="%"        label={datum ? `${datum.device} used` : "used across devices"}      />    )}  </RadialCenter>  <RadialLegend className="mt-8" showValues={false} /></RadialChart>// One ring per device, innermost first. Values are % of that device's// capacity, stacked end to end along the arc.const chartData = [  { device: "Phone", capacity: 256, photos: 46.1, apps: 21.1, documents: 3.5, system: 8.6 },  { device: "Laptop", capacity: 1024, photos: 20.7, apps: 16.4, documents: 23, system: 7.2 },  { device: "Cloud", capacity: 2048, photos: 41.3, apps: 0, documents: 20.1, system: 0 },];// % of one device in useconst percentUsed = (d) => d.photos + d.apps + d.documents + d.system;// Shown in the center at rest: % of all capacity (GB) in useconst totalCapacity = chartData.reduce((total, d) => total + d.capacity, 0);const overallUsed =  chartData.reduce((total, d) => total + percentUsed(d) * d.capacity, 0) /  totalCapacity;

Hover a ring to see that device's usage; hover the legend to isolate a content type.