Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

154 linesCodeBlameRaw
1/**
2 * Costs & margin's chart: a day per slot, what customers were charged
3 * (lavender) beside what it cost on Cloudflare (gray, red on a day that
4 * lost money). Drawn on the server as SVG like sudo's other charts: no
5 * script, no inline style, a <title> per day for hover, and a table.
6 */
7import { axisDollars, barPath, niceCeiling, ticks } from "~/lib/chart";
8import { type DayFigures, marginPercent, percentLabel } from "~/lib/costs";
9import { usd } from "~/lib/money";
10
11const CHARGED = "var(--color-accent)";
12const COST = "#7a7a84";
13const LOSS = "var(--color-danger)";
14
15const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
16
17/** `2026-10-05` → `Oct 5`. */
18export function dayLabel(day: string): string {
19 const month = MONTHS[Number(day.slice(5, 7)) - 1] ?? day.slice(5, 7);
20 return `${month} ${Number(day.slice(8, 10))}`;
21}
22
23function Legend({ revenueLabel }: { revenueLabel: string }) {
24 const swatch = (fill: string) => (
25 <svg width="10" height="10" aria-hidden="true">
26 <rect width="10" height="10" rx="2" fill={fill} />
27 </svg>
28 );
29 return (
30 <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted">
31 <span className="inline-flex items-center gap-1.5">
32 {swatch(CHARGED)}
33 {revenueLabel}
34 </span>
35 <span className="inline-flex items-center gap-1.5">
36 {swatch(COST)}
37 Cloudflare cost
38 </span>
39 <span className="inline-flex items-center gap-1.5">
40 {swatch(LOSS)}
41 Cost on a day under the floor
42 </span>
43 </div>
44 );
45}
46
47function DaysSvg({ days, width, height, floor, label }: { days: DayFigures[]; width: number; height: number; floor: number; label: string }) {
48 const left = width < 400 ? 40 : 52;
49 const plot = { x: left, y: 10, width: Math.max(1, width - left - 8), height: Math.max(1, height - 10 - 26) };
50 const top = niceCeiling(Math.max(0, ...days.flatMap((d) => [d.revenueMicros, d.costMicros])));
51 const baseline = plot.y + plot.height;
52 const scale = (value: number) => {
53 const clamped = Math.max(0, value);
54 const h = (clamped / top) * plot.height;
55 return clamped > 0 ? Math.max(1.5, h) : 0;
56 };
57 const slot = days.length > 0 ? plot.width / days.length : plot.width;
58 const gap = slot > 8 ? 1 : 0;
59 const bar = Math.max(1, Math.min(14, slot * 0.38));
60 // About six labels across, whatever the range.
61 const every = Math.max(1, Math.ceil(days.length / (width < 400 ? 4 : 8)));
62 return (
63 <svg viewBox={`0 0 ${width} ${height}`} className="block h-auto w-full" role="img" aria-label={label}>
64 {ticks(top, 4).map((value) => {
65 const y = baseline - (value / top) * plot.height;
66 return (
67 <g key={value}>
68 <line
69 x1={plot.x}
70 x2={plot.x + plot.width}
71 y1={y}
72 y2={y}
73 stroke="var(--color-line)"
74 strokeWidth="1"
75 strokeDasharray={value === 0 ? undefined : "2 4"}
76 />
77 <text x={plot.x - 8} y={y + 3.5} textAnchor="end" fontSize="11" className="fill-faint tabular">
78 {axisDollars(value)}
79 </text>
80 </g>
81 );
82 })}
83 {days.map((d, index) => {
84 const x = plot.x + index * slot;
85 const center = x + slot / 2;
86 const revenue = scale(d.revenueMicros);
87 const cost = scale(d.costMicros);
88 const margin = marginPercent(d.revenueMicros, d.costMicros);
89 const losing = d.costMicros > 0 && (margin == null || margin < floor);
90 const last = index === days.length - 1;
91 return (
92 <g key={d.day}>
93 <title>{`${dayLabel(d.day)}: charged ${usd(d.revenueMicros)}, Cloudflare cost ${usd(d.costMicros)}, margin ${percentLabel(margin)}`}</title>
94 <rect x={x} y={plot.y} width={slot} height={plot.height} fill="transparent" className="hover:fill-raised/40" />
95 <path d={barPath({ x: center - gap / 2 - bar, y: baseline - revenue, width: bar, height: revenue }, 2)} fill={CHARGED} />
96 <path d={barPath({ x: center + gap / 2, y: baseline - cost, width: bar, height: cost }, 2)} fill={losing ? LOSS : COST} />
97 {(index % every === 0 || last) && (
98 <text x={center} y={baseline + 17} textAnchor="middle" fontSize="11" className={last ? "fill-fg-soft" : "fill-faint"}>
99 {dayLabel(d.day)}
100 </text>
101 )}
102 </g>
103 );
104 })}
105 </svg>
106 );
107}
108
109/** Charged against Cloudflare's cost, a day at a time, drawn for a phone and for wider screens. */
110export function DaysChart({ days, floor, revenueLabel, label }: { days: DayFigures[]; floor: number; revenueLabel: string; label: string }) {
111 if (days.length === 0) return <p className="text-sm text-muted">No days to show yet.</p>;
112 return (
113 <div>
114 <Legend revenueLabel={revenueLabel} />
115 <div className="mt-3 sm:hidden">
116 <DaysSvg days={days} width={340} height={200} floor={floor} label={label} />
117 </div>
118 <div className="mt-3 hidden sm:block">
119 <DaysSvg days={days} width={1080} height={240} floor={floor} label={label} />
120 </div>
121 <details className="group mt-2">
122 <summary className="cursor-pointer list-none text-xs text-faint select-none hover:text-muted [&::-webkit-details-marker]:hidden">
123 <span className="group-open:hidden">Show as a table</span>
124 <span className="hidden group-open:inline">Hide the table</span>
125 </summary>
126 <div className="mt-2 overflow-x-auto">
127 <table className="w-full min-w-md text-xs">
128 <thead>
129 <tr className="border-b border-line text-left text-muted">
130 <th className="py-1.5 pr-3 font-medium">Day</th>
131 <th className="py-1.5 pr-3 text-right font-medium">{revenueLabel}</th>
132 <th className="py-1.5 pr-3 text-right font-medium">Cloudflare cost</th>
133 <th className="py-1.5 text-right font-medium">Margin</th>
134 </tr>
135 </thead>
136 <tbody>
137 {[...days].reverse().map((d) => {
138 const margin = marginPercent(d.revenueMicros, d.costMicros);
139 return (
140 <tr key={d.day} className="tabular border-b border-line last:border-0">
141 <td className="py-1.5 pr-3 text-fg-soft">{dayLabel(d.day)}</td>
142 <td className="py-1.5 pr-3 text-right">{usd(d.revenueMicros)}</td>
143 <td className="py-1.5 pr-3 text-right text-muted">{usd(d.costMicros)}</td>
144 <td className={`py-1.5 text-right ${margin != null && margin < floor ? "text-danger" : "text-fg-soft"}`}>{percentLabel(margin)}</td>
145 </tr>
146 );
147 })}
148 </tbody>
149 </table>
150 </div>
151 </details>
152 </div>
153 );
154}