@olympusoss/canvas 2.7.2 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -27,6 +27,26 @@ export interface ActivityHeatmapProps extends React.HTMLAttributes<HTMLDivElemen
|
|
|
27
27
|
* string. Returns nothing → no title set.
|
|
28
28
|
*/
|
|
29
29
|
cellTitle?: (row: number, col: number, value: number) => string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Optional left-side row labels (Y-axis). When provided, must align with
|
|
32
|
+
* `data.length`. Empty / nullish entries render as blank cells so the
|
|
33
|
+
* label column stays aligned with the grid.
|
|
34
|
+
*/
|
|
35
|
+
rowLabels?: React.ReactNode[];
|
|
36
|
+
/**
|
|
37
|
+
* Optional bottom-side column labels (X-axis). When provided, must align
|
|
38
|
+
* with `data[0].length`. Pass empty strings (or `null`) for indices you
|
|
39
|
+
* want to leave blank — useful for sparse hour ticks (e.g. only label
|
|
40
|
+
* 0/6/12/18/23 across a 24-column matrix).
|
|
41
|
+
*/
|
|
42
|
+
colLabels?: React.ReactNode[];
|
|
43
|
+
/**
|
|
44
|
+
* Show a "Fewer ↔ More" gradient legend below the grid. Pass `true` for
|
|
45
|
+
* the default labels, or an object to override one or both ends. The
|
|
46
|
+
* gradient mirrors the cell-opacity ramp (`0.08` → `0.93`).
|
|
47
|
+
* @default false
|
|
48
|
+
*/
|
|
49
|
+
legend?: boolean | { fromLabel?: React.ReactNode; toLabel?: React.ReactNode };
|
|
30
50
|
}
|
|
31
51
|
|
|
32
52
|
/**
|
|
@@ -34,6 +54,10 @@ export interface ActivityHeatmapProps extends React.HTMLAttributes<HTMLDivElemen
|
|
|
34
54
|
* matrices (token issuance, sign-in concentration, queue depth) where a full
|
|
35
55
|
* chart would be overkill. Rendering is a flat `display: grid` — no canvas, no
|
|
36
56
|
* SVG, fully interactive via hover titles.
|
|
57
|
+
*
|
|
58
|
+
* Optionally renders row labels on the left, sparse column labels below, and a
|
|
59
|
+
* `Fewer ↔ More` gradient legend — toggle each with `rowLabels` / `colLabels` /
|
|
60
|
+
* `legend` props.
|
|
37
61
|
*/
|
|
38
62
|
export const ActivityHeatmap = React.forwardRef<HTMLDivElement, ActivityHeatmapProps>(
|
|
39
63
|
(
|
|
@@ -44,51 +68,110 @@ export const ActivityHeatmap = React.forwardRef<HTMLDivElement, ActivityHeatmapP
|
|
|
44
68
|
gap = 2,
|
|
45
69
|
cellRadius = 3,
|
|
46
70
|
cellTitle,
|
|
71
|
+
rowLabels,
|
|
72
|
+
colLabels,
|
|
73
|
+
legend = false,
|
|
47
74
|
className,
|
|
48
75
|
...props
|
|
49
76
|
},
|
|
50
77
|
ref,
|
|
51
78
|
) => {
|
|
52
79
|
const cols = data[0]?.length ?? 0;
|
|
80
|
+
const legendObj = typeof legend === "object" ? legend : null;
|
|
81
|
+
const fromLabel = legendObj?.fromLabel ?? "Fewer";
|
|
82
|
+
const toLabel = legendObj?.toLabel ?? "More";
|
|
83
|
+
const showLegend = legend !== false;
|
|
84
|
+
|
|
53
85
|
return (
|
|
54
|
-
<div
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
87
|
+
<div className="flex gap-2">
|
|
88
|
+
{rowLabels && rowLabels.length > 0 && (
|
|
89
|
+
<div
|
|
90
|
+
className="grid text-[10px] tabular-nums text-muted-foreground"
|
|
91
|
+
style={{
|
|
92
|
+
gridTemplateRows: `repeat(${data.length}, ${cellHeight}px)`,
|
|
93
|
+
rowGap: gap,
|
|
94
|
+
}}
|
|
95
|
+
aria-hidden
|
|
96
|
+
>
|
|
97
|
+
{Array.from({ length: data.length }, (_, i) => (
|
|
98
|
+
<span key={`row-label-${i}`} className="flex items-center leading-none">
|
|
99
|
+
{rowLabels[i] ?? ""}
|
|
100
|
+
</span>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
<div className="flex-1">
|
|
105
|
+
<div
|
|
106
|
+
style={{
|
|
107
|
+
display: "grid",
|
|
108
|
+
gridTemplateRows: `repeat(${data.length}, ${cellHeight}px)`,
|
|
109
|
+
gap,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{data.map((row, r) => (
|
|
78
113
|
<div
|
|
79
|
-
key={`
|
|
80
|
-
data-cell=""
|
|
81
|
-
title={title}
|
|
82
|
-
aria-hidden
|
|
114
|
+
key={`r-${r}`}
|
|
83
115
|
style={{
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
display: "grid",
|
|
117
|
+
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
|
118
|
+
gap,
|
|
86
119
|
}}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
120
|
+
>
|
|
121
|
+
{row.map((raw, c) => {
|
|
122
|
+
const v = Math.max(0, Math.min(1, raw));
|
|
123
|
+
const opacity = 0.08 + v * 0.85;
|
|
124
|
+
const title = cellTitle?.(r, c, v);
|
|
125
|
+
return (
|
|
126
|
+
<div
|
|
127
|
+
key={`c-${r}-${c}`}
|
|
128
|
+
data-cell=""
|
|
129
|
+
title={title}
|
|
130
|
+
aria-hidden
|
|
131
|
+
style={{
|
|
132
|
+
borderRadius: cellRadius,
|
|
133
|
+
background: `hsl(var(--${colorVar}) / ${opacity})`,
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
})}
|
|
138
|
+
</div>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
{colLabels && colLabels.length > 0 && (
|
|
142
|
+
<div
|
|
143
|
+
className="mt-2 grid text-[10px] tabular-nums text-muted-foreground"
|
|
144
|
+
style={{
|
|
145
|
+
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
|
146
|
+
columnGap: gap,
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{Array.from({ length: cols }, (_, i) => {
|
|
150
|
+
const label = colLabels[i];
|
|
151
|
+
const empty = label === undefined || label === null || label === "";
|
|
152
|
+
return (
|
|
153
|
+
<span key={`col-label-${i}`} className="text-center" aria-hidden={empty}>
|
|
154
|
+
{empty ? "" : label}
|
|
155
|
+
</span>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
{showLegend && (
|
|
163
|
+
<div className="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
|
|
164
|
+
<span>{fromLabel}</span>
|
|
165
|
+
<div
|
|
166
|
+
className="h-2 flex-1 rounded-full"
|
|
167
|
+
style={{
|
|
168
|
+
background: `linear-gradient(90deg, hsl(var(--${colorVar}) / 0.08) 0%, hsl(var(--${colorVar}) / 0.93) 100%)`,
|
|
169
|
+
}}
|
|
170
|
+
aria-hidden
|
|
171
|
+
/>
|
|
172
|
+
<span>{toLabel}</span>
|
|
90
173
|
</div>
|
|
91
|
-
)
|
|
174
|
+
)}
|
|
92
175
|
</div>
|
|
93
176
|
);
|
|
94
177
|
},
|