Stats: heatmap uses the theme accent with a continuous color ramp
Cells blend in OKLab from a neutral empty color to the theme's accent on a square-root scale instead of seven fixed blue steps; the legend shows the gradient. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ed9942829a
commit
e693bb196f
3 files changed
+28
-22
No files matched your search
@@ -6,7 +6,23 @@ import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { useT } from "../i18n";
|
||||
|
||||
export const SERIES = Array.from({ length: 8 }, (_, i) => `var(--series-${i + 1})`);
|
||||
const SEQ = Array.from({ length: 7 }, (_, i) => `var(--seq-${i})`);
|
||||
/**
|
||||
* Sequential heat color from the theme: empty cells use the neutral --heat-empty,
|
||||
* any value > 0 blends continuously (in OKLab, so lightness changes evenly) from a
|
||||
* faint tint to the full theme accent. A square-root scale keeps rare small values
|
||||
* visible next to a few large peaks.
|
||||
*/
|
||||
const HEAT_MIN = 22;
|
||||
function heatShare(v: number, max: number): number {
|
||||
if (v <= 0 || max <= 0) return 0;
|
||||
return HEAT_MIN + (100 - HEAT_MIN) * Math.sqrt(Math.min(v / max, 1));
|
||||
}
|
||||
function heatColor(share: number): string {
|
||||
return share <= 0 ? "var(--heat-empty)" : `color-mix(in oklab, var(--heat-full) ${share.toFixed(1)}%, var(--heat-empty))`;
|
||||
}
|
||||
const HEAT_GRADIENT = `linear-gradient(in oklab to right, ${[0, 0.1, 0.25, 0.45, 0.7, 1]
|
||||
.map((f) => `${heatColor(heatShare(f * f, 1))} ${f * 100}%`)
|
||||
.join(", ")})`;
|
||||
|
||||
function useWidth<T extends HTMLElement>() {
|
||||
const ref = useRef<T>(null);
|
||||
@@ -173,7 +189,7 @@ export function StackedBars({
|
||||
);
|
||||
}
|
||||
|
||||
/** Day × hour heatmap with a 7-step sequential ramp. cells[row*24+hour]; rows top to bottom. */
|
||||
/** Day × hour heatmap with a continuous sequential ramp in the theme accent. cells[row*24+hour]; rows top to bottom. */
|
||||
export function Heatmap({
|
||||
cells,
|
||||
rows,
|
||||
@@ -194,7 +210,6 @@ export function Heatmap({
|
||||
const cell = Math.min(cellW, 26);
|
||||
const max = Math.max(...cells, 0);
|
||||
const order = rows.map((_, i) => i);
|
||||
const bin = (v: number) => (v <= 0 || max === 0 ? 0 : Math.min(6, 1 + Math.floor((v / max) * 5.999)));
|
||||
|
||||
return (
|
||||
<div className="chart" ref={ref} onMouseLeave={() => setTip(undefined)}>
|
||||
@@ -224,7 +239,7 @@ export function Heatmap({
|
||||
width={cellW - 2}
|
||||
height={cell - 2}
|
||||
rx={3}
|
||||
fill={SEQ[bin(v)]}
|
||||
style={{ fill: heatColor(heatShare(v, max)) }}
|
||||
onMouseMove={(e) => {
|
||||
const box = (e.currentTarget.ownerSVGElement as SVGSVGElement).getBoundingClientRect();
|
||||
setTip({
|
||||
@@ -246,12 +261,10 @@ export function Heatmap({
|
||||
)}
|
||||
</svg>
|
||||
<div className="legend" style={{ marginTop: 6 }}>
|
||||
<i style={{ background: SEQ[0], width: 18 }} />
|
||||
<i style={{ background: heatColor(0), width: 18 }} />
|
||||
<span>{t("stats.heatEmpty")}</span>
|
||||
<span style={{ marginLeft: 12 }}>{format(0)}</span>
|
||||
{SEQ.slice(1).map((c, i) => (
|
||||
<i key={i} style={{ background: c, width: 18 }} />
|
||||
))}
|
||||
<i className="heat-scale" style={{ background: HEAT_GRADIENT }} />
|
||||
<span>{format(max)}</span>
|
||||
</div>
|
||||
{tip && (
|
||||
|
||||
@@ -1066,6 +1066,9 @@ table.grid input[type="number"] {
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
.legend i.heat-scale {
|
||||
width: 120px;
|
||||
}
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
|
||||
+4
-14
@@ -9,13 +9,8 @@
|
||||
--series-6: #008300;
|
||||
--series-7: #9085e9;
|
||||
--series-8: #e66767;
|
||||
--seq-0: color-mix(in srgb, var(--fg1) 9%, var(--bg1));
|
||||
--seq-1: #104281;
|
||||
--seq-2: #184f95;
|
||||
--seq-3: #256abf;
|
||||
--seq-4: #3987e5;
|
||||
--seq-5: #6da7ec;
|
||||
--seq-6: #9ec5f4;
|
||||
--heat-empty: color-mix(in srgb, var(--fg1) 9%, var(--bg1));
|
||||
--heat-full: var(--accent);
|
||||
}
|
||||
:root[data-mode="light"] {
|
||||
--series-1: #2a78d6;
|
||||
@@ -26,13 +21,8 @@
|
||||
--series-6: #008300;
|
||||
--series-7: #6250d6;
|
||||
--series-8: #e34948;
|
||||
--seq-0: color-mix(in srgb, var(--fg1) 9%, var(--bg1));
|
||||
--seq-1: #cde2fb;
|
||||
--seq-2: #9ec5f4;
|
||||
--seq-3: #6da7ec;
|
||||
--seq-4: #3987e5;
|
||||
--seq-5: #256abf;
|
||||
--seq-6: #184f95;
|
||||
--heat-empty: color-mix(in srgb, var(--fg1) 9%, var(--bg1));
|
||||
--heat-full: var(--accent);
|
||||
}
|
||||
|
||||
/* Theme-independent tokens. */
|
||||
|
||||
Reference in new issue
Block a user