@olympusoss/canvas 2.7.2 → 2.8.0

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olympusoss/canvas",
3
- "version": "2.7.2",
3
+ "version": "2.8.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -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
- ref={ref}
56
- className={cn("w-full", className)}
57
- style={{
58
- display: "grid",
59
- gridTemplateRows: `repeat(${data.length}, ${cellHeight}px)`,
60
- gap,
61
- }}
62
- {...props}
63
- >
64
- {data.map((row, r) => (
65
- <div
66
- key={`r-${r}`}
67
- style={{
68
- display: "grid",
69
- gridTemplateColumns: `repeat(${cols}, 1fr)`,
70
- gap,
71
- }}
72
- >
73
- {row.map((raw, c) => {
74
- const v = Math.max(0, Math.min(1, raw));
75
- const opacity = 0.08 + v * 0.85;
76
- const title = cellTitle?.(r, c, v);
77
- return (
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={`c-${r}-${c}`}
80
- data-cell=""
81
- title={title}
82
- aria-hidden
114
+ key={`r-${r}`}
83
115
  style={{
84
- borderRadius: cellRadius,
85
- background: `hsl(var(--${colorVar}) / ${opacity})`,
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
  },