Skip to content

Commit a3bff73

Browse files
that-github-userdavclaude
authored
Adaptive calendar heatmap colors based on month's P&L range (#11)
Replace fixed ±10/±20 pt thresholds with continuous HSL interpolation scaled to the visible month's min/max P&L. A +5pt day looks muted in a ±50pt month but vivid in a ±8pt month. Floor of ±2pts prevents degenerate scaling in very quiet months. Co-authored-by: dav <dav@ORACLE.gpu-prophet.lan> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aa1fe5f commit a3bff73

File tree

1 file changed

+29
-9
lines changed

1 file changed

+29
-9
lines changed

src/components/charts/CalendarHeatmap.tsx

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,31 @@ function getETNow() {
2424
return { year, month, day, dateStr, date: et };
2525
}
2626

27-
function getPnlColor(pnl: number): string {
28-
if (pnl > 20) return "#059669"; // strong green
29-
if (pnl > 10) return "#10b981";
30-
if (pnl > 0) return "#34d399"; // light green
31-
if (pnl === 0) return "#475569"; // gray
32-
if (pnl > -10) return "#f87171"; // light red
33-
if (pnl > -20) return "#ef4444";
34-
return "#dc2626"; // strong red
27+
/**
28+
* Adaptive P&L color: intensity scales to the month's min/max range.
29+
* A +5pt day looks muted in a ±50pt month but vivid in a ±8pt month.
30+
* Uses a continuous HSL interpolation for smooth gradients.
31+
*/
32+
function getPnlColor(pnl: number, monthMin: number, monthMax: number): string {
33+
if (pnl === 0) return "#475569";
34+
35+
// Compute intensity as fraction of the month's range on each side
36+
// Use at least ±2 pts as floor so very quiet months still show some range
37+
const posMax = Math.max(monthMax, 2);
38+
const negMax = Math.max(-monthMin, 2);
39+
const t = pnl > 0
40+
? Math.min(pnl / posMax, 1)
41+
: Math.min(-pnl / negMax, 1);
42+
43+
if (pnl > 0) {
44+
// Green ramp: HSL(160, 72%, L) where L goes from 62% (faint) to 36% (vivid)
45+
const lightness = 62 - t * 26;
46+
return `hsl(160, 72%, ${lightness}%)`;
47+
} else {
48+
// Red ramp: HSL(0, 75%, L) where L goes from 70% (faint) to 44% (vivid)
49+
const lightness = 70 - t * 26;
50+
return `hsl(0, 75%, ${lightness}%)`;
51+
}
3552
}
3653

3754
export function CalendarHeatmap({ summaries }: Props) {
@@ -92,6 +109,9 @@ export function CalendarHeatmap({ summaries }: Props) {
92109
const monthPnl = monthSummaries.reduce((s, d) => s + d.total_pnl_pts, 0);
93110
const greenDays = monthSummaries.filter((d) => d.total_pnl_pts > 0).length;
94111
const redDays = monthSummaries.filter((d) => d.total_pnl_pts < 0).length;
112+
const monthPnls = monthSummaries.map((d) => d.total_pnl_pts);
113+
const monthMin = monthPnls.length ? Math.min(...monthPnls) : -10;
114+
const monthMax = monthPnls.length ? Math.max(...monthPnls) : 10;
95115

96116
const [hoveredDate, setHoveredDate] = useState<string | null>(null);
97117
const hoveredSummary = hoveredDate ? lookup[hoveredDate] : null;
@@ -138,7 +158,7 @@ export function CalendarHeatmap({ summaries }: Props) {
138158
const isToday = cell.date === etNow.dateStr;
139159
const hasData = cell.pnl !== null;
140160
const bg = hasData
141-
? getPnlColor(cell.pnl!)
161+
? getPnlColor(cell.pnl!, monthMin, monthMax)
142162
: cell.isWeekend
143163
? "#0f172a"
144164
: cell.isFuture

0 commit comments

Comments
 (0)