awesome-heatmap 0.1.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/LICENSE +23 -0
- package/README.md +358 -0
- package/dist/cjs/index.js +215 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.js +805 -0
- package/dist/esm/index.js.map +1 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
AwesomeHeatmap Contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
23
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# awesome-heatmap
|
|
2
|
+
|
|
3
|
+
A zero-dependency **heatmap grid** React component. Feed it any data shape — arrays of objects, REST API URLs, GeoJSON, CSV-style nested arrays — and it auto-extracts a numeric matrix, renders colour-coded cells, and gives you a full toolbar with colour scale picker, row sorting, cell size slider, theme switcher, and a floating tooltip. All styles are injected at runtime, so no CSS import is needed.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Universal data ingestion** — arrays of objects, URL strings (JSON / REST API), GeoJSON FeatureCollections, CSV-style `[[headers], [row], ...]`, envelope-wrapped responses (`{ data: [...] }`, `{ results: [...] }`, etc.)
|
|
10
|
+
- **Auto matrix extraction** — detects numeric columns (≥ 75% parseable values), picks the first non-numeric column as the row label, flattens nested objects up to 3 levels deep
|
|
11
|
+
- **8 colour scales** — Plasma, Inferno, Viridis, Magma, Thermal, Ice, Red→Blue, Spectral — all using perceptually uniform control-point interpolation
|
|
12
|
+
- **3 built-in themes** — `dark`, `light`, `ember` — all colours driven by CSS custom properties
|
|
13
|
+
- **Toolbar controls** — colour scale dropdown with preview dots, row sort (original / A→Z / Z→A / sum↑ / sum↓), cell size slider (16–80px), show/hide cell values toggle, theme switcher
|
|
14
|
+
- **Column header mini-bars** — each column header shows a colour-coded bar for its average value relative to the global range
|
|
15
|
+
- **Sticky row labels + column headers** — both stay in view while scrolling a large matrix
|
|
16
|
+
- **Floating tooltip** — shows row label, column label, formatted value, and a colour-matched progress bar
|
|
17
|
+
- **Summary footer** — global Σ, x̄, max, min, and cell count
|
|
18
|
+
- **IndexedDB preference persistence** — theme, scale, sort, cell size, and show-values are saved per `storageKey` and restored on next load
|
|
19
|
+
- **Self-contained styles** — CSS Module replaced with a runtime-injected `<style>` tag; no stylesheet import required
|
|
20
|
+
- **ESM + CJS dual build** — works in Vite, Next.js, CRA, and any modern bundler
|
|
21
|
+
- **Zero runtime dependencies** beyond React
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install awesome-heatmap
|
|
29
|
+
# or
|
|
30
|
+
yarn add awesome-heatmap
|
|
31
|
+
# or
|
|
32
|
+
pnpm add awesome-heatmap
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> **Peer dependencies** — React ≥ 18 and ReactDOM ≥ 18 must already be installed.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
No CSS import needed — styles are injected automatically.
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
import { AwesomeHeatmap } from 'awesome-heatmap';
|
|
45
|
+
|
|
46
|
+
var data = [
|
|
47
|
+
{ city: 'New York', jan: 2, feb: 4, mar: 10, apr: 17, may: 23, jun: 28 },
|
|
48
|
+
{ city: 'London', jan: 5, feb: 6, mar: 9, apr: 13, may: 17, jun: 20 },
|
|
49
|
+
{ city: 'Tokyo', jan: 6, feb: 7, mar: 11, apr: 17, may: 22, jun: 26 },
|
|
50
|
+
{ city: 'Sydney', jan: 26, feb: 26, mar: 23, apr: 18, may: 14, jun: 11 },
|
|
51
|
+
{ city: 'Dubai', jan: 19, feb: 21, mar: 25, apr: 33, may: 38, jun: 40 },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
export default function App() {
|
|
55
|
+
return (
|
|
56
|
+
<div style={{ width: '100%', height: '500px' }}>
|
|
57
|
+
<AwesomeHeatmap
|
|
58
|
+
data={data}
|
|
59
|
+
title="Monthly Temperatures (°C)"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Props
|
|
69
|
+
|
|
70
|
+
| Prop | Type | Default | Description |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| `data` | `Array \| string` | `[]` | Data to visualise. Pass an array of objects, or a URL string pointing to a JSON endpoint. See [Data formats](#-data-formats) below. |
|
|
73
|
+
| `title` | `string` | `'Heatmap'` | Title shown in the toolbar. |
|
|
74
|
+
| `theme` | `'dark' \| 'light' \| 'ember'` | `'dark'` | Initial colour theme. User can switch at runtime via the toolbar. |
|
|
75
|
+
| `colorScale` | `string` | `'plasma'` | Initial colour scale key. Must be one of the `COLOR_SCALES` keys. |
|
|
76
|
+
| `storageKey` | `string` | `'awesome-heatmap'` | IndexedDB key used to persist and restore user preferences. Pass a unique value per instance. Set to `''` to disable persistence. |
|
|
77
|
+
| `height` | `string \| number` | `'100%'` | CSS height of the component. Numbers are treated as px. |
|
|
78
|
+
| `onThemeChange` | `(theme: string) => void` | — | Called whenever the user switches theme, with the new theme name. |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Named Exports
|
|
83
|
+
|
|
84
|
+
### `COLOR_SCALES`
|
|
85
|
+
|
|
86
|
+
An object mapping scale keys to display labels. Use this to build your own scale picker or to pass a valid value to the `colorScale` prop.
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
import { COLOR_SCALES } from 'awesome-heatmap';
|
|
90
|
+
|
|
91
|
+
console.log(COLOR_SCALES);
|
|
92
|
+
// {
|
|
93
|
+
// plasma: "Plasma",
|
|
94
|
+
// inferno: "Inferno",
|
|
95
|
+
// viridis: "Viridis",
|
|
96
|
+
// magma: "Magma",
|
|
97
|
+
// thermal: "Thermal",
|
|
98
|
+
// ice: "Ice",
|
|
99
|
+
// rdbu: "Red → Blue",
|
|
100
|
+
// spectral: "Spectral",
|
|
101
|
+
// }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `PAGE_BG`
|
|
105
|
+
|
|
106
|
+
An object mapping theme names to their recommended outer page background colours. Useful for styling the page wrapper to match the heatmap.
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { PAGE_BG } from 'awesome-heatmap';
|
|
110
|
+
|
|
111
|
+
console.log(PAGE_BG);
|
|
112
|
+
// { dark: "#04060c", light: "#e0e5f0", ember: "#070402" }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Data Formats
|
|
118
|
+
|
|
119
|
+
AwesomeHeatmap accepts data in many shapes and normalises them automatically.
|
|
120
|
+
|
|
121
|
+
### Array of objects (recommended)
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
var data = [
|
|
125
|
+
{ product: 'Shoes', mon: 120, tue: 95, wed: 143 },
|
|
126
|
+
{ product: 'Bags', mon: 88, tue: 112, wed: 76 },
|
|
127
|
+
{ product: 'Hats', mon: 54, tue: 67, wed: 91 },
|
|
128
|
+
];
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The first non-numeric column (`product`) becomes the row label. All numeric columns become the heatmap columns.
|
|
132
|
+
|
|
133
|
+
### URL string — JSON endpoint
|
|
134
|
+
|
|
135
|
+
```jsx
|
|
136
|
+
<AwesomeHeatmap data="https://api.example.com/metrics" title="Server Metrics" />
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The component fetches the URL, then unwraps common envelope keys (`data`, `results`, `items`, `rows`, etc.) automatically.
|
|
140
|
+
|
|
141
|
+
### Envelope-wrapped response
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{ "results": [ { "region": "APAC", "q1": 4.2, "q2": 5.1, "q3": 3.8 } ] }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### CSV-style nested arrays (first row = headers)
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
var data = [
|
|
151
|
+
['city', 'q1', 'q2', 'q3', 'q4'],
|
|
152
|
+
['Berlin', 8.2, 14.1, 19.3, 9.5 ],
|
|
153
|
+
['Paris', 9.1, 15.0, 20.2, 10.1],
|
|
154
|
+
['Madrid', 11.5, 17.3, 24.8, 13.2],
|
|
155
|
+
];
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### GeoJSON FeatureCollection
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
var geojson = {
|
|
162
|
+
type: 'FeatureCollection',
|
|
163
|
+
features: [
|
|
164
|
+
{ type: 'Feature', geometry: { type: 'Point', coordinates: [0,0] }, properties: { name: 'A', value: 42 } },
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Properties are extracted automatically; the geometry is ignored.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Examples
|
|
174
|
+
|
|
175
|
+
### Example 1 — Dark theme with Viridis scale and persistence disabled
|
|
176
|
+
|
|
177
|
+
```jsx
|
|
178
|
+
import { AwesomeHeatmap } from 'awesome-heatmap';
|
|
179
|
+
|
|
180
|
+
var salesData = [
|
|
181
|
+
{ rep: 'Alice', jan: 42000, feb: 38000, mar: 51000, apr: 47000, may: 55000, jun: 61000 },
|
|
182
|
+
{ rep: 'Bob', jan: 31000, feb: 44000, mar: 39000, apr: 52000, may: 48000, jun: 43000 },
|
|
183
|
+
{ rep: 'Carol', jan: 58000, feb: 62000, mar: 55000, apr: 67000, may: 71000, jun: 69000 },
|
|
184
|
+
{ rep: 'David', jan: 27000, feb: 31000, mar: 35000, apr: 29000, may: 38000, jun: 42000 },
|
|
185
|
+
{ rep: 'Eva', jan: 49000, feb: 53000, mar: 61000, apr: 58000, may: 64000, jun: 72000 },
|
|
186
|
+
{ rep: 'Frank', jan: 36000, feb: 41000, mar: 38000, apr: 44000, may: 46000, jun: 50000 },
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
export default function SalesDashboard() {
|
|
190
|
+
return (
|
|
191
|
+
<div style={{ width: '100%', height: '480px', borderRadius: 16, overflow: 'hidden' }}>
|
|
192
|
+
<AwesomeHeatmap
|
|
193
|
+
data={salesData}
|
|
194
|
+
title="Sales by Rep — H1"
|
|
195
|
+
theme="dark"
|
|
196
|
+
colorScale="viridis"
|
|
197
|
+
storageKey=""
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### Example 2 — Light theme, live data from a REST API, `onThemeChange` callback
|
|
207
|
+
|
|
208
|
+
```jsx
|
|
209
|
+
import { useState } from 'react';
|
|
210
|
+
import { AwesomeHeatmap, PAGE_BG } from 'awesome-heatmap';
|
|
211
|
+
|
|
212
|
+
export default function LiveMetrics() {
|
|
213
|
+
var [theme, setTheme] = useState('light');
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div style={{ background: PAGE_BG[theme], minHeight: '100vh', padding: 32, transition: 'background 0.4s' }}>
|
|
217
|
+
<div style={{ width: '100%', height: '600px', borderRadius: 20, overflow: 'hidden', boxShadow: '0 4px 24px rgba(0,0,0,0.12)' }}>
|
|
218
|
+
<AwesomeHeatmap
|
|
219
|
+
data="https://jsonplaceholder.typicode.com/users"
|
|
220
|
+
title="User Metrics (live)"
|
|
221
|
+
theme={theme}
|
|
222
|
+
colorScale="thermal"
|
|
223
|
+
storageKey="live-metrics-heatmap"
|
|
224
|
+
onThemeChange={function(t) { setTheme(t); }}
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### Example 3 — Ember theme, CSV-style array, controlled height, multiple instances
|
|
235
|
+
|
|
236
|
+
```jsx
|
|
237
|
+
import { AwesomeHeatmap, COLOR_SCALES } from 'awesome-heatmap';
|
|
238
|
+
|
|
239
|
+
var weeklyTraffic = [
|
|
240
|
+
['page', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'],
|
|
241
|
+
['Home', 9420, 8830, 9210, 9640, 11200, 8100, 7300],
|
|
242
|
+
['Products', 4210, 4650, 4120, 4980, 5340, 3800, 3200],
|
|
243
|
+
['Blog', 2100, 2340, 2890, 2670, 2450, 3100, 2900],
|
|
244
|
+
['Pricing', 1840, 2010, 1950, 2200, 2680, 1400, 1100],
|
|
245
|
+
['Contact', 820, 910, 880, 970, 1050, 640, 580 ],
|
|
246
|
+
['Docs', 3400, 3800, 4200, 4100, 3900, 2200, 1900],
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
var teamPerformance = [
|
|
250
|
+
['team', 'sprints_completed', 'bugs_closed', 'prs_merged', 'reviews_done', 'deploys'],
|
|
251
|
+
['Frontend', 8, 24, 31, 42, 12],
|
|
252
|
+
['Backend', 6, 38, 22, 28, 9],
|
|
253
|
+
['Mobile', 7, 19, 17, 24, 6],
|
|
254
|
+
['DevOps', 5, 11, 8, 15, 28],
|
|
255
|
+
['Data', 4, 14, 12, 18, 4],
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
export default function MultiHeatmap() {
|
|
259
|
+
return (
|
|
260
|
+
<div style={{ background: '#070402', minHeight: '100vh', padding: 32, display: 'flex', flexDirection: 'column', gap: 24 }}>
|
|
261
|
+
|
|
262
|
+
<div style={{ height: 420, borderRadius: 16, overflow: 'hidden' }}>
|
|
263
|
+
<AwesomeHeatmap
|
|
264
|
+
data={weeklyTraffic}
|
|
265
|
+
title="Weekly Page Traffic"
|
|
266
|
+
theme="ember"
|
|
267
|
+
colorScale="plasma"
|
|
268
|
+
storageKey="traffic-heatmap"
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<div style={{ height: 360, borderRadius: 16, overflow: 'hidden' }}>
|
|
273
|
+
<AwesomeHeatmap
|
|
274
|
+
data={teamPerformance}
|
|
275
|
+
title="Team Performance — Current Sprint"
|
|
276
|
+
theme="ember"
|
|
277
|
+
colorScale="inferno"
|
|
278
|
+
storageKey="team-heatmap"
|
|
279
|
+
/>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{/* Display available colour scales */}
|
|
283
|
+
<div style={{ color: '#a0836a', fontFamily: 'monospace', fontSize: 12 }}>
|
|
284
|
+
Available scales: {Object.keys(COLOR_SCALES).join(', ')}
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
No separate CSS file is produced. All styles are bundled as a JavaScript string and injected into `<head>` when the component first mounts.
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
## Toolbar Controls
|
|
297
|
+
|
|
298
|
+
| Control | Description |
|
|
299
|
+
|---|---|
|
|
300
|
+
| **Cell size slider** | Resize all cells from 16px to 80px. Values are shown inside cells when size ≥ 28px and the eye toggle is on. |
|
|
301
|
+
| **Eye toggle** | Show or hide numeric values inside each cell. |
|
|
302
|
+
| **Colour scale dropdown** | Switch between the 8 available scales. Each option shows a preview strip. |
|
|
303
|
+
| **Sort dropdown** | Sort rows by: original order, label A→Z / Z→A, or row sum ascending / descending. |
|
|
304
|
+
| **Theme dots** | Switch between Dark (blue), Light (indigo), and Ember (orange) themes. |
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## CSS Custom Properties
|
|
309
|
+
|
|
310
|
+
All visual tokens are CSS custom properties set on the root element. Override them per-instance via a wrapper class:
|
|
311
|
+
|
|
312
|
+
```css
|
|
313
|
+
.my-heatmap-wrapper {
|
|
314
|
+
--hm-bg: #0a0a0a;
|
|
315
|
+
--hm-surface: #141414;
|
|
316
|
+
--hm-surface2: #1c1c1c;
|
|
317
|
+
--hm-surface3: #242424;
|
|
318
|
+
--hm-border: rgba(255,255,255,0.06);
|
|
319
|
+
--hm-border2: rgba(255,255,255,0.12);
|
|
320
|
+
--hm-text1: #f0f0f0;
|
|
321
|
+
--hm-text2: #888;
|
|
322
|
+
--hm-text3: #444;
|
|
323
|
+
--hm-accent: #7c3aed;
|
|
324
|
+
--hm-accentbg: rgba(124,58,237,0.12);
|
|
325
|
+
--hm-shadow: 0 8px 40px rgba(0,0,0,0.7);
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Then wrap the component:
|
|
330
|
+
|
|
331
|
+
```jsx
|
|
332
|
+
<div className="my-heatmap-wrapper" style={{ height: 500 }}>
|
|
333
|
+
<AwesomeHeatmap data={data} />
|
|
334
|
+
</div>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## Preference Persistence
|
|
340
|
+
|
|
341
|
+
User preferences (theme, colour scale, sort order, cell size, show-values toggle) are automatically saved to IndexedDB under the `storageKey` and restored on the next mount. To use multiple independent instances on the same page, pass a unique `storageKey` to each:
|
|
342
|
+
|
|
343
|
+
```jsx
|
|
344
|
+
<AwesomeHeatmap data={salesData} storageKey="sales-heatmap" />
|
|
345
|
+
<AwesomeHeatmap data={trafficData} storageKey="traffic-heatmap" />
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
To disable persistence entirely, pass an empty string:
|
|
349
|
+
|
|
350
|
+
```jsx
|
|
351
|
+
<AwesomeHeatmap data={data} storageKey="" />
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## License
|
|
357
|
+
|
|
358
|
+
MIT
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";const t=require("react/jsx-runtime"),c=require("react"),e="ahm",B=(...s)=>s.filter(Boolean).join(" "),ue=`
|
|
2
|
+
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Outfit:wght@400;500;600;700&display=swap");
|
|
3
|
+
|
|
4
|
+
.${e}-root {
|
|
5
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
6
|
+
background: var(--hm-bg); color: var(--hm-text1);
|
|
7
|
+
font-family: "Outfit", sans-serif;
|
|
8
|
+
border-radius: 16px; box-shadow: var(--hm-shadow);
|
|
9
|
+
position: relative; container-type: inline-size;
|
|
10
|
+
}
|
|
11
|
+
.${e}-toolbar {
|
|
12
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
13
|
+
padding: 0 16px; height: 52px; flex-shrink: 0;
|
|
14
|
+
border-bottom: 1px solid var(--hm-border);
|
|
15
|
+
background: var(--hm-surface); gap: 12px;
|
|
16
|
+
}
|
|
17
|
+
.${e}-toolbarL { display: flex; align-items: center; gap: 10px; overflow: hidden; }
|
|
18
|
+
.${e}-toolbarR { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
|
19
|
+
.${e}-logoMark { display: flex; align-items: center; flex-shrink: 0; }
|
|
20
|
+
.${e}-title {
|
|
21
|
+
font-family: "Outfit", sans-serif; font-size: 15px; font-weight: 600;
|
|
22
|
+
color: var(--hm-text1); white-space: nowrap; overflow: hidden;
|
|
23
|
+
text-overflow: ellipsis; letter-spacing: -.01em;
|
|
24
|
+
}
|
|
25
|
+
.${e}-dimBadge {
|
|
26
|
+
font-family: "IBM Plex Mono", monospace; font-size: 11px; font-weight: 500;
|
|
27
|
+
color: var(--hm-text3); background: var(--hm-surface2);
|
|
28
|
+
border: 1px solid var(--hm-border); border-radius: 6px;
|
|
29
|
+
padding: 2px 8px; white-space: nowrap; letter-spacing: .03em; flex-shrink: 0;
|
|
30
|
+
}
|
|
31
|
+
.${e}-sizeControl { display: flex; align-items: center; gap: 5px; color: var(--hm-text3); }
|
|
32
|
+
.${e}-sizeIcon { flex-shrink: 0; }
|
|
33
|
+
.${e}-sizeSlider { width: 72px; accent-color: var(--hm-accent); cursor: pointer; height: 3px; }
|
|
34
|
+
.${e}-iconBtn {
|
|
35
|
+
display: flex; align-items: center; justify-content: center;
|
|
36
|
+
width: 30px; height: 30px; border-radius: 8px;
|
|
37
|
+
border: 1px solid var(--hm-border); background: var(--hm-surface2);
|
|
38
|
+
color: var(--hm-text2); cursor: pointer; transition: all .14s; flex-shrink: 0;
|
|
39
|
+
}
|
|
40
|
+
.${e}-iconBtn:hover { border-color: var(--hm-border2); color: var(--hm-text1); }
|
|
41
|
+
.${e}-iconBtnOn { background: var(--hm-accentbg) !important; border-color: var(--hm-accent) !important; color: var(--hm-accent) !important; }
|
|
42
|
+
.${e}-dropdownWrap { position: relative; }
|
|
43
|
+
.${e}-dropBtn {
|
|
44
|
+
display: flex; align-items: center; gap: 6px; padding: 5px 10px;
|
|
45
|
+
border-radius: 8px; border: 1px solid var(--hm-border);
|
|
46
|
+
background: var(--hm-surface2); color: var(--hm-text1);
|
|
47
|
+
font-family: "Outfit", sans-serif; font-size: 12px; font-weight: 500;
|
|
48
|
+
cursor: pointer; white-space: nowrap; transition: all .14s; height: 30px;
|
|
49
|
+
}
|
|
50
|
+
.${e}-dropBtn:hover { border-color: var(--hm-border2); }
|
|
51
|
+
.${e}-dropBtnOn { border-color: var(--hm-accent) !important; background: var(--hm-accentbg) !important; }
|
|
52
|
+
.${e}-dropBtnLabel { font-size: 11.5px; color: var(--hm-text2); max-width: 90px; overflow: hidden; text-overflow: ellipsis; }
|
|
53
|
+
.${e}-dropArrow { color: var(--hm-text3); flex-shrink: 0; }
|
|
54
|
+
.${e}-dropdown {
|
|
55
|
+
position: absolute; right: 0; top: calc(100% + 6px); z-index: 500;
|
|
56
|
+
background: var(--hm-surface); border: 1px solid var(--hm-border2);
|
|
57
|
+
border-radius: 10px; box-shadow: var(--hm-shadow);
|
|
58
|
+
padding: 4px; min-width: 170px; display: flex; flex-direction: column; gap: 1px;
|
|
59
|
+
}
|
|
60
|
+
.${e}-dropItem {
|
|
61
|
+
display: flex; align-items: center; gap: 8px; padding: 7px 10px;
|
|
62
|
+
border-radius: 7px; border: none; background: none;
|
|
63
|
+
color: var(--hm-text1); font-family: "Outfit", sans-serif;
|
|
64
|
+
font-size: 12.5px; font-weight: 500; cursor: pointer;
|
|
65
|
+
text-align: left; transition: background .10s; white-space: nowrap;
|
|
66
|
+
}
|
|
67
|
+
.${e}-dropItem:hover { background: var(--hm-surface2); }
|
|
68
|
+
.${e}-dropItemOn { background: var(--hm-accentbg) !important; color: var(--hm-accent) !important; }
|
|
69
|
+
.${e}-scalePreview { display: flex; gap: 2px; align-items: center; flex-shrink: 0; }
|
|
70
|
+
.${e}-scalePreviewDot { width: 10px; height: 10px; border-radius: 2px; display: inline-block; flex-shrink: 0; }
|
|
71
|
+
.${e}-themeToggle { display: flex; gap: 5px; margin-left: 4px; }
|
|
72
|
+
.${e}-themeBtn {
|
|
73
|
+
width: 14px; height: 14px; border-radius: 50%;
|
|
74
|
+
border: 2px solid transparent; background: var(--dot);
|
|
75
|
+
cursor: pointer; transition: all .15s; flex-shrink: 0; padding: 0;
|
|
76
|
+
}
|
|
77
|
+
.${e}-themeBtn:hover { transform: scale(1.25); }
|
|
78
|
+
.${e}-themeBtnOn { border-color: var(--hm-text1) !important; transform: scale(1.2); box-shadow: 0 0 0 1px var(--hm-bg); }
|
|
79
|
+
.${e}-body { flex: 1; overflow: hidden; display: flex; flex-direction: column; position: relative; }
|
|
80
|
+
.${e}-stateWrap {
|
|
81
|
+
flex: 1; display: flex; flex-direction: column;
|
|
82
|
+
align-items: center; justify-content: center;
|
|
83
|
+
gap: 12px; color: var(--hm-text2); font-size: 14px; padding: 40px 20px;
|
|
84
|
+
}
|
|
85
|
+
.${e}-stateHint { font-size: 12.5px; color: var(--hm-text3); text-align: center; max-width: 360px; line-height: 1.6; }
|
|
86
|
+
.${e}-stateHint code {
|
|
87
|
+
font-family: "IBM Plex Mono", monospace; font-size: 11px;
|
|
88
|
+
background: var(--hm-surface2); padding: 1px 5px;
|
|
89
|
+
border-radius: 4px; color: var(--hm-accent);
|
|
90
|
+
}
|
|
91
|
+
.${e}-spinner {
|
|
92
|
+
width: 28px; height: 28px;
|
|
93
|
+
border: 2.5px solid var(--hm-border2);
|
|
94
|
+
border-top-color: var(--hm-accent);
|
|
95
|
+
border-radius: 50%;
|
|
96
|
+
animation: ${e}-spin .65s linear infinite;
|
|
97
|
+
}
|
|
98
|
+
@keyframes ${e}-spin { to { transform: rotate(360deg); } }
|
|
99
|
+
.${e}-errBanner {
|
|
100
|
+
margin: 16px; padding: 12px 16px;
|
|
101
|
+
background: rgba(239,68,68,.08); border: 1px solid rgba(239,68,68,.25);
|
|
102
|
+
border-radius: 10px; color: #f87171; font-size: 13px; line-height: 1.5;
|
|
103
|
+
}
|
|
104
|
+
.${e}-heatmapWrap { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
|
|
105
|
+
.${e}-colHeaderRow {
|
|
106
|
+
display: flex; align-items: flex-end; padding-bottom: 2px;
|
|
107
|
+
border-bottom: 1px solid var(--hm-border); flex-shrink: 0;
|
|
108
|
+
background: var(--hm-bg); position: sticky; top: 0; z-index: 20; overflow: hidden;
|
|
109
|
+
}
|
|
110
|
+
.${e}-cornerCell { width: 140px; min-width: 140px; flex-shrink: 0; }
|
|
111
|
+
.${e}-colHeader {
|
|
112
|
+
flex-shrink: 0; display: flex; flex-direction: column;
|
|
113
|
+
align-items: center; justify-content: flex-end;
|
|
114
|
+
padding-bottom: 4px; height: 72px; position: relative;
|
|
115
|
+
transition: background .12s; border-radius: 4px 4px 0 0;
|
|
116
|
+
}
|
|
117
|
+
.${e}-colHeaderOn { background: var(--hm-accentbg) !important; }
|
|
118
|
+
.${e}-colHeaderText {
|
|
119
|
+
font-family: "IBM Plex Mono", monospace; font-size: 10px; font-weight: 500;
|
|
120
|
+
color: var(--hm-text2); writing-mode: vertical-rl; transform: rotate(180deg);
|
|
121
|
+
white-space: nowrap; overflow: hidden; max-height: 56px;
|
|
122
|
+
text-overflow: ellipsis; letter-spacing: .02em; line-height: 1;
|
|
123
|
+
padding-bottom: 2px; transition: color .12s;
|
|
124
|
+
}
|
|
125
|
+
.${e}-colHeaderOn .${e}-colHeaderText { color: var(--hm-text1) !important; }
|
|
126
|
+
.${e}-colMiniBar {
|
|
127
|
+
width: 100%; height: 3px; background: var(--hm-surface3);
|
|
128
|
+
margin-top: 3px; border-radius: 2px; overflow: hidden;
|
|
129
|
+
display: flex; align-items: flex-end;
|
|
130
|
+
}
|
|
131
|
+
.${e}-colMiniBarFill { width: 100%; border-radius: 2px; transition: height .3s ease; }
|
|
132
|
+
.${e}-grid {
|
|
133
|
+
flex: 1; overflow: auto;
|
|
134
|
+
scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.08) transparent;
|
|
135
|
+
}
|
|
136
|
+
.${e}-grid::-webkit-scrollbar { width: 5px; height: 5px; }
|
|
137
|
+
.${e}-grid::-webkit-scrollbar-thumb { background: rgba(255,255,255,.08); border-radius: 4px; }
|
|
138
|
+
.${e}-row { display: flex; align-items: stretch; }
|
|
139
|
+
.${e}-row:hover .${e}-rowLabel { color: var(--hm-text1); background: var(--hm-accentbg); }
|
|
140
|
+
.${e}-rowLabel {
|
|
141
|
+
width: 140px; min-width: 140px; flex-shrink: 0;
|
|
142
|
+
font-family: "IBM Plex Mono", monospace; font-size: 10.5px; font-weight: 400;
|
|
143
|
+
color: var(--hm-text2); padding: 0 10px; display: flex; align-items: center;
|
|
144
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
145
|
+
border-right: 1px solid var(--hm-border); background: var(--hm-bg);
|
|
146
|
+
position: sticky; left: 0; z-index: 10; transition: all .12s; letter-spacing: .01em;
|
|
147
|
+
}
|
|
148
|
+
.${e}-rowLabelOn { color: var(--hm-text1) !important; background: var(--hm-accentbg) !important; }
|
|
149
|
+
.${e}-cell {
|
|
150
|
+
display: flex; align-items: center; justify-content: center;
|
|
151
|
+
flex-shrink: 0; cursor: crosshair;
|
|
152
|
+
transition: outline .08s, filter .08s;
|
|
153
|
+
outline: 2px solid transparent; outline-offset: -1px;
|
|
154
|
+
position: relative; background: var(--hm-surface3);
|
|
155
|
+
}
|
|
156
|
+
.${e}-cell:hover { outline-color: rgba(255,255,255,.6); z-index: 5; filter: brightness(1.15); }
|
|
157
|
+
.${e}-cellVal {
|
|
158
|
+
font-family: "IBM Plex Mono", monospace; font-size: 9px; font-weight: 600;
|
|
159
|
+
letter-spacing: -.01em; pointer-events: none;
|
|
160
|
+
text-shadow: 0 1px 2px rgba(0,0,0,.3); line-height: 1;
|
|
161
|
+
max-width: 100%; overflow: hidden; text-overflow: ellipsis;
|
|
162
|
+
white-space: nowrap; padding: 0 2px;
|
|
163
|
+
}
|
|
164
|
+
.${e}-cellNull { font-family: "IBM Plex Mono", monospace; font-size: 11px; color: var(--hm-text3); opacity: .4; }
|
|
165
|
+
.${e}-footer {
|
|
166
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
167
|
+
padding: 8px 12px; border-top: 1px solid var(--hm-border);
|
|
168
|
+
background: var(--hm-surface); flex-shrink: 0; gap: 16px; flex-wrap: wrap;
|
|
169
|
+
}
|
|
170
|
+
.${e}-legend { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
171
|
+
.${e}-legendMin, .${e}-legendMax {
|
|
172
|
+
font-family: "IBM Plex Mono", monospace; font-size: 10px;
|
|
173
|
+
color: var(--hm-text3); white-space: nowrap; min-width: 32px;
|
|
174
|
+
}
|
|
175
|
+
.${e}-legendMax { text-align: right; }
|
|
176
|
+
.${e}-legendBar { border-radius: 4px; display: block; height: 12px; width: 140px; flex-shrink: 0; }
|
|
177
|
+
.${e}-summaryStats { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
|
|
178
|
+
.${e}-statItem {
|
|
179
|
+
font-family: "IBM Plex Mono", monospace; font-size: 11px;
|
|
180
|
+
color: var(--hm-text2); display: flex; align-items: center; gap: 4px; white-space: nowrap;
|
|
181
|
+
}
|
|
182
|
+
.${e}-statItem span:first-child { color: var(--hm-text3); font-size: 10px; }
|
|
183
|
+
.${e}-statNulls { font-family: "IBM Plex Mono", monospace; font-size: 10px; color: var(--hm-text3); white-space: nowrap; }
|
|
184
|
+
.${e}-tooltip {
|
|
185
|
+
position: fixed; z-index: 9999; pointer-events: none;
|
|
186
|
+
background: var(--hm-surface); border: 1px solid var(--hm-border2);
|
|
187
|
+
border-radius: 10px; box-shadow: var(--hm-shadow);
|
|
188
|
+
padding: 0; overflow: hidden; min-width: 180px; max-width: 260px;
|
|
189
|
+
animation: ${e}-ttIn .1s ease;
|
|
190
|
+
}
|
|
191
|
+
@keyframes ${e}-ttIn {
|
|
192
|
+
from { opacity: 0; transform: scale(.96) translateY(3px); }
|
|
193
|
+
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
194
|
+
}
|
|
195
|
+
.${e}-ttBody { padding: 12px 14px; display: flex; flex-direction: column; gap: 5px; }
|
|
196
|
+
.${e}-ttRow { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
|
|
197
|
+
.${e}-ttKey {
|
|
198
|
+
font-family: "Outfit", sans-serif; font-size: 10px; font-weight: 600;
|
|
199
|
+
text-transform: uppercase; letter-spacing: .07em; color: var(--hm-text3); flex-shrink: 0;
|
|
200
|
+
}
|
|
201
|
+
.${e}-ttVal {
|
|
202
|
+
font-family: "IBM Plex Mono", monospace; font-size: 12.5px; font-weight: 500;
|
|
203
|
+
color: var(--hm-text1); text-align: right; overflow: hidden;
|
|
204
|
+
text-overflow: ellipsis; white-space: nowrap; max-width: 140px;
|
|
205
|
+
}
|
|
206
|
+
.${e}-ttDivider { height: 1px; background: var(--hm-border); margin: 2px 0; }
|
|
207
|
+
.${e}-ttBar { height: 4px; background: var(--hm-surface3); border-radius: 3px; overflow: hidden; margin-top: 3px; }
|
|
208
|
+
.${e}-ttBarFill { height: 100%; border-radius: 3px; transition: width .15s ease; }
|
|
209
|
+
/* Theme variants */
|
|
210
|
+
.${e}-themeLight .${e}-grid::-webkit-scrollbar-thumb { background: rgba(0,0,0,.12); }
|
|
211
|
+
.${e}-themeLight .${e}-cell:hover { outline-color: rgba(0,0,0,.5); }
|
|
212
|
+
.${e}-themeEmber .${e}-grid::-webkit-scrollbar-thumb { background: rgba(255,100,20,.12); }
|
|
213
|
+
.${e}-themeEmber .${e}-cell:hover { outline-color: rgba(255,160,60,.6); }
|
|
214
|
+
`;function ge(){const s="__ahm_styles__";if(typeof document>"u"||document.getElementById(s))return;const o=document.createElement("style");o.id=s,o.textContent=ue,document.head.appendChild(o)}const be=s=>String(s??"").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/[_\-]+/g," ").replace(/\b\w/g,o=>o.toUpperCase()).trim()||String(s),we=s=>String(s??"").trim().replace(/[_\s\-]+(.)?/g,(o,a)=>a?a.toUpperCase():"").replace(/^(.)/,o=>o.toLowerCase()),ne=(s,o="",a=0)=>a>3||typeof s!="object"||s===null?{[o]:s}:Object.entries(s).reduce((n,[d,l])=>{const x=o?`${o}_${d}`:d;return typeof l=="object"&&l!==null&&!Array.isArray(l)&&a<3?{...n,...ne(l,x,a+1)}:{...n,[x]:l}},{}),$e=s=>{const o=ne(s);return Object.fromEntries(Object.entries(o).map(([a,n])=>[we(a),n]))},se=["data","items","results","rows","records","list","content","payload","body","response","output","dataset","datasets","values","entries"],ve=s=>{if(!s)return[];if(Array.isArray(s)){if(!s.length)return[];const o=s[0];if(typeof o!="object"||o===null)return s.map((a,n)=>({index:n,value:a}));if(Array.isArray(o)){const a=o.map((n,d)=>n!=null?String(n):`col${d}`);return s.slice(1).map(n=>{const d={};return a.forEach((l,x)=>{d[l]=n[x]??null}),d})}return s}if(s.type==="FeatureCollection"&&Array.isArray(s.features))return s.features.filter(o=>(o==null?void 0:o.properties)&&typeof o.properties=="object").map(o=>{var a;return{...o.properties,_geomType:(a=o.geometry)==null?void 0:a.type}});for(const o of se){if(Array.isArray(s[o]))return s[o];if(s[o]&&typeof s[o]=="object"){for(const a of se)if(Array.isArray(s[o][a]))return s[o][a]}}return typeof s=="object"?[s]:[]},ye=s=>{if(s==null||s==="")return!1;const o=parseFloat(String(s).replace(/[,$%\s€£¥]/g,""));return!isNaN(o)&&isFinite(o)},je=s=>{if(s==null||s==="")return null;const o=parseFloat(String(s).replace(/[,$%\s€£¥]/g,""));return isNaN(o)?null:o},ke=s=>{if(!s.length)return null;const o=s.slice(0,200),a=[...new Set(o.flatMap(Object.keys))],n=a.filter(w=>{const f=o.map(k=>k[w]).filter(k=>k!=null&&k!=="");return f.length?f.filter(ye).length/f.length>.75:!1}),d=a.find(w=>!n.includes(w))??null;if(!n.length)return null;const l=s,x=n,v=l.map((w,f)=>d?String(w[d]??`Row ${f+1}`):`Row ${f+1}`),j=x.map(be),b=l.map(w=>x.map(f=>je(w[f])));let g=1/0,$=-1/0;return b.forEach(w=>w.forEach(f=>{f!==null&&(f<g&&(g=f),f>$&&($=f))})),isFinite(g)||(g=0,$=1),g===$&&(g=g-1,$=$+1),{rows:l,cols:x,rowLabels:v,colLabels:j,values:b,min:g,max:$,labelKey:d}},_={plasma:"Plasma",inferno:"Inferno",viridis:"Viridis",magma:"Magma",thermal:"Thermal",ice:"Ice",rdbu:"Red → Blue",spectral:"Spectral"},Ne=(s,o)=>{const a=s.length-1,n=Math.min(Math.floor(o*a),a-1),d=o*a-n,[l,x,v]=s[n],[j,b,g]=s[n+1];return[Math.round(l+(j-l)*d),Math.round(x+(b-x)*d),Math.round(v+(g-v)*d)]},oe={plasma:[[13,8,135],[84,2,163],[139,10,165],[185,50,137],[219,92,104],[244,136,73],[254,188,43],[240,249,33]],inferno:[[0,0,4],[40,11,84],[101,21,110],[159,42,99],[212,72,66],[245,125,21],[250,193,39],[252,255,164]],viridis:[[68,1,84],[72,40,120],[62,83,160],[49,120,183],[38,153,180],[31,183,152],[122,209,81],[253,231,37]],magma:[[0,0,4],[28,16,68],[79,18,123],[129,37,129],[181,54,122],[229,80,100],[251,135,97],[252,253,191]],thermal:[[4,35,51],[23,84,107],[60,146,153],[99,197,174],[162,220,193],[218,241,211],[246,210,169],[249,142,82],[214,41,21]],ice:[[4,0,46],[32,20,115],[66,59,170],[103,107,211],[142,156,232],[181,205,247],[215,238,255],[252,255,255]],rdbu:[[178,24,43],[214,96,77],[244,165,130],[253,219,199],[209,229,240],[146,197,222],[67,147,195],[33,102,172]],spectral:[[158,1,66],[213,62,79],[244,109,67],[253,174,97],[254,224,139],[230,245,152],[171,221,164],[102,194,165],[50,136,189],[94,79,162]]},E=(s,o)=>Ne(oe[s]??oe.plasma,Math.max(0,Math.min(1,o))),H=([s,o,a])=>`rgb(${s},${o},${a})`,S={dark:{"--hm-bg":"#080c14","--hm-surface":"#0e1521","--hm-surface2":"#141d2e","--hm-surface3":"#1a243b","--hm-border":"rgba(255,255,255,.07)","--hm-border2":"rgba(255,255,255,.13)","--hm-text1":"#e4eaf8","--hm-text2":"#6b7a9e","--hm-text3":"#3a445e","--hm-accent":"#4f7cff","--hm-accentbg":"rgba(79,124,255,.12)","--hm-shadow":"0 8px 40px rgba(0,0,0,.6)","--hm-page-bg":"#04060c"},light:{"--hm-bg":"#f0f2f7","--hm-surface":"#ffffff","--hm-surface2":"#e8ecf5","--hm-surface3":"#dde2ee","--hm-border":"rgba(0,0,0,.08)","--hm-border2":"rgba(0,0,0,.14)","--hm-text1":"#0d1324","--hm-text2":"#4a5370","--hm-text3":"#99a3bc","--hm-accent":"#2f5cf0","--hm-accentbg":"rgba(47,92,240,.09)","--hm-shadow":"0 4px 24px rgba(0,0,0,.10)","--hm-page-bg":"#e0e5f0"},ember:{"--hm-bg":"#0e0804","--hm-surface":"#170e06","--hm-surface2":"#1f1408","--hm-surface3":"#271a0a","--hm-border":"rgba(255,160,60,.08)","--hm-border2":"rgba(255,160,60,.16)","--hm-text1":"#faebd7","--hm-text2":"#a0836a","--hm-text3":"#5a3d2a","--hm-accent":"#ff7a2f","--hm-accentbg":"rgba(255,122,47,.13)","--hm-shadow":"0 8px 40px rgba(80,20,0,.7)","--hm-page-bg":"#070402"}};S.dark["--hm-page-bg"],S.light["--hm-page-bg"],S.ember["--hm-page-bg"];const ie="awesome-heatmap",z="prefs",Me=s=>new Promise(o=>{try{const a=indexedDB.open(ie,1);a.onupgradeneeded=n=>n.target.result.createObjectStore(z),a.onsuccess=n=>{try{const l=n.target.result.transaction(z,"readonly").objectStore(z).get(s);l.onsuccess=()=>o(l.result??null),l.onerror=()=>o(null)}catch{o(null)}},a.onerror=()=>o(null)}catch{o(null)}}),Se=(s,o)=>new Promise(a=>{try{const n=indexedDB.open(ie,1);n.onupgradeneeded=d=>d.target.result.createObjectStore(z),n.onsuccess=d=>{try{const l=d.target.result.transaction(z,"readwrite");l.objectStore(z).put(o,s),l.oncomplete=()=>a(!0),l.onerror=()=>a(!1)}catch{a(!1)}},n.onerror=()=>a(!1)}catch{a(!1)}}),C=s=>s==null?"—":Math.abs(s)>=1e6?(s/1e6).toFixed(2)+"M":Math.abs(s)>=1e3?(s/1e3).toFixed(1)+"K":Number.isInteger(s)?String(s):s.toFixed(2),ae=[{id:"none",label:"Original order"},{id:"rowAsc",label:"Row A → Z"},{id:"rowDesc",label:"Row Z → A"},{id:"sumAsc",label:"Row sum ↑"},{id:"sumDesc",label:"Row sum ↓"}];function Be({content:s,x:o,y:a,visible:n,theme:d}){const l=c.useRef(null),[x,v]=c.useState({top:0,left:0});return c.useEffect(()=>{if(!n||!l.current)return;const j=l.current,b=window.innerWidth,g=window.innerHeight,$=j.offsetWidth||200,w=j.offsetHeight||80,f=14;let k=o+f,y=a-w/2;k+$>b-8&&(k=o-$-f),y<8&&(y=8),y+w>g-8&&(y=g-w-8),v({top:y,left:k})},[o,a,n,s]),n?t.jsx("div",{ref:l,className:`${e}-tooltip`,style:{...S[d],top:x.top,left:x.left},children:s}):null}function Le({scale:s,min:o,max:a,theme:n}){const d=S[n]??S.dark,l=c.useRef(null);return c.useEffect(()=>{const x=l.current;if(!x)return;const v=x.getContext("2d"),j=x.width;for(let b=0;b<j;b++){const[g,$,w]=E(s,b/(j-1));v.fillStyle=`rgb(${g},${$},${w})`,v.fillRect(b,0,1,x.height)}},[s]),t.jsxs("div",{className:`${e}-legend`,style:d,children:[t.jsx("span",{className:`${e}-legendMin`,children:C(o)}),t.jsx("canvas",{ref:l,width:200,height:12,className:`${e}-legendBar`}),t.jsx("span",{className:`${e}-legendMax`,children:C(a)})]})}const Ce=c.memo(function({value:o,t:a,scaleName:n,showVal:d,cellSize:l,onEnter:x,onLeave:v}){if(o===null)return t.jsx("div",{className:`${e}-cell`,style:{width:l,height:l},onMouseEnter:x,onMouseLeave:v,children:t.jsx("span",{className:`${e}-cellNull`,children:"·"})});const[j,b,g]=E(n,a),w=.299*j+.587*b+.114*g>140?"rgba(0,0,0,.85)":"rgba(255,255,255,.9)";return t.jsx("div",{className:`${e}-cell`,style:{width:l,height:l,background:`rgb(${j},${b},${g})`,color:w},onMouseEnter:x,onMouseLeave:v,children:d&&t.jsx("span",{className:`${e}-cellVal`,children:C(o)})})});function Oe({data:s=[],title:o="Heatmap",theme:a="dark",colorScale:n="plasma",storageKey:d="awesome-heatmap",height:l="100%",onThemeChange:x=null}){var te;ge();const[v,j]=c.useState([]),[b,g]=c.useState(!1),[$,w]=c.useState("");c.useEffect(()=>{let r=!1;return(async()=>{g(!0),w("");try{let h;if(typeof s=="string"){const m=await fetch(s);if(!m.ok)throw new Error(`HTTP ${m.status} — ${s}`);h=await m.json()}else h=s;const u=ve(h).map($e);r||j(u)}catch(h){r||w(h.message)}finally{r||g(!1)}})(),()=>{r=!0}},[s]);const[f,k]=c.useState(a),[y,V]=c.useState(n),[M,q]=c.useState("none"),[R,K]=c.useState(!1),[L,U]=c.useState(40),[Ee,Z]=c.useState(null),[W,F]=c.useState({visible:!1,content:null,x:0,y:0}),[X,P]=c.useState(!1),[G,D]=c.useState(!1),[le,J]=c.useState(null),[ce,Q]=c.useState(null),T=c.useRef(!1),I=S[f]??S.dark;c.useEffect(()=>{d&&Me(d).then(r=>{if(!r){T.current=!0;return}r.theme&&S[r.theme]&&k(r.theme),r.scale&&_[r.scale]&&V(r.scale),r.sortMode&&q(r.sortMode),typeof r.showVals=="boolean"&&K(r.showVals),r.cellSize&&r.cellSize>=16&&U(r.cellSize),T.current=!0})},[d]),c.useEffect(()=>{!d||!T.current||Se(d,{theme:f,scale:y,sortMode:M,showVals:R,cellSize:L})},[d,f,y,M,R,L]),c.useEffect(()=>{S[a]&&(k(a),x==null||x(a))},[a,x]),c.useEffect(()=>{_[n]&&V(n)},[n]);const i=c.useMemo(()=>ke(v),[v]),Y=c.useMemo(()=>{if(!i)return[];const r=i.rows.length,p=Array.from({length:r},(u,m)=>m);if(M==="none")return p;if(M==="rowAsc")return[...p].sort((u,m)=>String(i.rowLabels[u]).localeCompare(String(i.rowLabels[m])));if(M==="rowDesc")return[...p].sort((u,m)=>String(i.rowLabels[m]).localeCompare(String(i.rowLabels[u])));const h=u=>i.values[u].reduce((m,A)=>m+(A??0),0);return M==="sumAsc"?[...p].sort((u,m)=>h(u)-h(m)):M==="sumDesc"?[...p].sort((u,m)=>h(m)-h(u)):p},[i,M]),ee=c.useMemo(()=>i?i.cols.map((r,p)=>{const h=i.values.map(m=>m[p]).filter(m=>m!==null);if(!h.length)return{min:0,max:0,avg:0,sum:0};const u=h.reduce((m,A)=>m+A,0);return{min:Math.min(...h),max:Math.max(...h),avg:u/h.length,sum:u}}):[],[i]),O=c.useCallback(r=>!i||r===null?0:(r-i.min)/(i.max-i.min),[i]),de=r=>{k(r),x==null||x(r)},he=c.useCallback((r,p,h)=>{var re;if(!i)return;const u=Y[r],m=(re=i.values[u])==null?void 0:re[p],A=i.rowLabels[u],fe=i.colLabels[p];Q(r),J(p),Z({row:r,col:p}),F({visible:!0,x:h.clientX,y:h.clientY,content:t.jsxs("div",{className:`${e}-ttBody`,style:I,children:[t.jsxs("div",{className:`${e}-ttRow`,children:[t.jsx("span",{className:`${e}-ttKey`,children:"Row"}),t.jsx("span",{className:`${e}-ttVal`,children:A})]}),t.jsxs("div",{className:`${e}-ttRow`,children:[t.jsx("span",{className:`${e}-ttKey`,children:"Column"}),t.jsx("span",{className:`${e}-ttVal`,children:fe})]}),t.jsx("div",{className:`${e}-ttDivider`}),t.jsxs("div",{className:`${e}-ttRow`,children:[t.jsx("span",{className:`${e}-ttKey`,children:"Value"}),t.jsx("span",{className:`${e}-ttVal`,style:{color:m!==null?H(E(y,O(m))):void 0},children:m!==null?C(m):"—"})]}),m!==null&&t.jsx("div",{className:`${e}-ttBar`,children:t.jsx("div",{className:`${e}-ttBarFill`,style:{width:`${O(m)*100}%`,background:H(E(y,O(m)))}})})]})})},[i,Y,y,O,I]),pe=c.useCallback(()=>{Z(null),Q(null),J(null),F(r=>({...r,visible:!1}))},[]),me=c.useCallback(r=>{F(p=>p.visible?{...p,x:r.clientX,y:r.clientY}:p)},[]),N=c.useMemo(()=>{if(!i)return null;const r=i.values.flat().filter(h=>h!==null);if(!r.length)return null;const p=r.reduce((h,u)=>h+u,0);return{count:r.length,total:i.rows.length*i.cols.length,min:i.min,max:i.max,avg:p/r.length,sum:p,rows:i.rows.length,cols:i.cols.length}},[i]);c.useEffect(()=>{const r=()=>{P(!1),D(!1)};return document.addEventListener("mousedown",r),()=>document.removeEventListener("mousedown",r)},[]);const xe=f==="light"?`${e}-themeLight`:f==="ember"?`${e}-themeEmber`:`${e}-themeDark`;return t.jsxs("div",{className:B(`${e}-root`,xe),style:{...I,height:typeof l=="number"?`${l}px`:l},onMouseMove:me,children:[t.jsxs("div",{className:`${e}-toolbar`,children:[t.jsxs("div",{className:`${e}-toolbarL`,children:[t.jsx("div",{className:`${e}-logoMark`,"aria-hidden":"true",children:t.jsxs("svg",{width:"22",height:"22",viewBox:"0 0 22 22",fill:"none",children:[t.jsx("rect",{x:"1",y:"1",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".3"}),t.jsx("rect",{x:"6",y:"1",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".5"}),t.jsx("rect",{x:"11",y:"1",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".9"}),t.jsx("rect",{x:"16",y:"1",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".5"}),t.jsx("rect",{x:"1",y:"6",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".5"}),t.jsx("rect",{x:"6",y:"6",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".9"}),t.jsx("rect",{x:"11",y:"6",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".5"}),t.jsx("rect",{x:"16",y:"6",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".3"}),t.jsx("rect",{x:"1",y:"11",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".9"}),t.jsx("rect",{x:"6",y:"11",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".5"}),t.jsx("rect",{x:"11",y:"11",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".3"}),t.jsx("rect",{x:"16",y:"11",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".5"}),t.jsx("rect",{x:"1",y:"16",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".5"}),t.jsx("rect",{x:"6",y:"16",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".3"}),t.jsx("rect",{x:"11",y:"16",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".5"}),t.jsx("rect",{x:"16",y:"16",width:"4",height:"4",rx:"1",fill:"var(--hm-accent)",opacity:".9"})]})}),t.jsx("h1",{className:`${e}-title`,children:o}),N&&t.jsxs("span",{className:`${e}-dimBadge`,children:[N.rows.toLocaleString()," × ",N.cols.toLocaleString()]})]}),t.jsxs("div",{className:`${e}-toolbarR`,children:[t.jsxs("div",{className:`${e}-sizeControl`,children:[t.jsxs("svg",{viewBox:"0 0 14 14",fill:"none",width:"12",height:"12",className:`${e}-sizeIcon`,children:[t.jsx("rect",{x:"1",y:"1",width:"5",height:"5",rx:"1",stroke:"currentColor",strokeWidth:"1.3"}),t.jsx("rect",{x:"8",y:"1",width:"5",height:"5",rx:"1",stroke:"currentColor",strokeWidth:"1.3"}),t.jsx("rect",{x:"1",y:"8",width:"5",height:"5",rx:"1",stroke:"currentColor",strokeWidth:"1.3"}),t.jsx("rect",{x:"8",y:"8",width:"5",height:"5",rx:"1",stroke:"currentColor",strokeWidth:"1.3"})]}),t.jsx("input",{type:"range",min:16,max:80,step:4,value:L,onChange:r=>U(Number(r.target.value)),className:`${e}-sizeSlider`,title:`Cell size: ${L}px`})]}),t.jsx("button",{className:B(`${e}-iconBtn`,R&&`${e}-iconBtnOn`),onClick:()=>K(r=>!r),title:R?"Hide cell values":"Show cell values",children:t.jsxs("svg",{viewBox:"0 0 16 16",fill:"none",width:"14",height:"14",children:[t.jsx("path",{d:"M1 8C1 8 3.5 3 8 3s7 5 7 5-2.5 5-7 5S1 8 1 8z",stroke:"currentColor",strokeWidth:"1.4"}),t.jsx("circle",{cx:"8",cy:"8",r:"2",stroke:"currentColor",strokeWidth:"1.4"})]})}),t.jsxs("div",{className:`${e}-dropdownWrap`,onMouseDown:r=>r.stopPropagation(),children:[t.jsxs("button",{className:B(`${e}-dropBtn`,X&&`${e}-dropBtnOn`),onClick:()=>{P(r=>!r),D(!1)},title:"Color scale",children:[t.jsx("span",{className:`${e}-scalePreview`,children:[0,.25,.5,.75,1].map(r=>t.jsx("span",{style:{background:H(E(y,r))},className:`${e}-scalePreviewDot`},r))}),t.jsx("span",{className:`${e}-dropBtnLabel`,children:_[y]}),t.jsx("svg",{viewBox:"0 0 10 6",fill:"none",width:"9",className:`${e}-dropArrow`,children:t.jsx("path",{d:"M1 1l4 4 4-4",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})})]}),X&&t.jsx("div",{className:`${e}-dropdown`,style:I,children:Object.entries(_).map(([r,p])=>t.jsxs("button",{className:B(`${e}-dropItem`,y===r&&`${e}-dropItemOn`),onClick:()=>{V(r),P(!1)},children:[t.jsx("span",{className:`${e}-scalePreview`,children:[0,.2,.4,.6,.8,1].map(h=>t.jsx("span",{style:{background:H(E(r,h))},className:`${e}-scalePreviewDot`},h))}),t.jsx("span",{children:p})]},r))})]}),t.jsxs("div",{className:`${e}-dropdownWrap`,onMouseDown:r=>r.stopPropagation(),children:[t.jsxs("button",{className:B(`${e}-dropBtn`,G&&`${e}-dropBtnOn`),onClick:()=>{D(r=>!r),P(!1)},title:"Sort rows",children:[t.jsx("svg",{viewBox:"0 0 14 14",fill:"none",width:"13",height:"13",children:t.jsx("path",{d:"M2 3h10M4 7h6M6 11h2",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})}),t.jsx("span",{className:`${e}-dropBtnLabel`,children:((te=ae.find(r=>r.id===M))==null?void 0:te.label)??"Sort"}),t.jsx("svg",{viewBox:"0 0 10 6",fill:"none",width:"9",className:`${e}-dropArrow`,children:t.jsx("path",{d:"M1 1l4 4 4-4",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round"})})]}),G&&t.jsx("div",{className:`${e}-dropdown`,style:I,children:ae.map(r=>t.jsx("button",{className:B(`${e}-dropItem`,M===r.id&&`${e}-dropItemOn`),onClick:()=>{q(r.id),D(!1)},children:r.label},r.id))})]}),t.jsx("div",{className:`${e}-themeToggle`,children:[{id:"dark",color:"#4f7cff",label:"Dark"},{id:"light",color:"#2f5cf0",label:"Light"},{id:"ember",color:"#ff7a2f",label:"Ember"}].map(r=>t.jsx("button",{className:B(`${e}-themeBtn`,f===r.id&&`${e}-themeBtnOn`),style:{"--dot":r.color},onClick:()=>de(r.id),title:r.label},r.id))})]})]}),t.jsxs("div",{className:`${e}-body`,children:[b&&t.jsxs("div",{className:`${e}-stateWrap`,children:[t.jsx("div",{className:`${e}-spinner`}),t.jsx("span",{children:"Loading data…"})]}),!b&&$&&t.jsxs("div",{className:`${e}-errBanner`,children:[t.jsx("strong",{children:"Failed to load:"})," ",$]}),!b&&!$&&!i&&v.length>0&&t.jsxs("div",{className:`${e}-stateWrap`,children:[t.jsxs("svg",{viewBox:"0 0 48 48",fill:"none",width:"48",height:"48",opacity:".3",children:[t.jsx("rect",{x:"4",y:"4",width:"40",height:"40",rx:"4",stroke:"currentColor",strokeWidth:"2"}),t.jsx("path",{d:"M12 24h24M24 12v24",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round"})]}),t.jsx("p",{children:"No numeric columns found in this dataset."}),t.jsx("p",{className:`${e}-stateHint`,children:"AwesomeHeatmap needs at least one numeric column to render."})]}),!b&&!$&&v.length===0&&t.jsxs("div",{className:`${e}-stateWrap`,children:[t.jsxs("svg",{viewBox:"0 0 48 48",fill:"none",width:"48",height:"48",opacity:".3",children:[t.jsx("rect",{x:"4",y:"8",width:"40",height:"32",rx:"4",stroke:"currentColor",strokeWidth:"2"}),t.jsx("line",{x1:"4",y1:"16",x2:"44",y2:"16",stroke:"currentColor",strokeWidth:"2"}),t.jsx("line",{x1:"16",y1:"8",x2:"16",y2:"40",stroke:"currentColor",strokeWidth:"2",opacity:".5"})]}),t.jsx("p",{children:"No data loaded"}),t.jsxs("p",{className:`${e}-stateHint`,children:["Pass an array, a JSON file path, or a REST API URL to the ",t.jsx("code",{children:"data"})," prop."]})]}),!b&&i&&t.jsxs("div",{className:`${e}-heatmapWrap`,children:[t.jsxs("div",{className:`${e}-colHeaderRow`,style:{paddingLeft:0},children:[t.jsx("div",{className:`${e}-cornerCell`}),i.colLabels.map((r,p)=>{var h,u;return t.jsxs("div",{className:B(`${e}-colHeader`,le===p&&`${e}-colHeaderOn`),style:{width:L,minWidth:L},title:r,children:[t.jsx("span",{className:`${e}-colHeaderText`,children:r}),t.jsx("div",{className:`${e}-colMiniBar`,children:t.jsx("div",{className:`${e}-colMiniBarFill`,style:{height:`${Math.round(O(((h=ee[p])==null?void 0:h.avg)??0)*100)}%`,background:H(E(y,O(((u=ee[p])==null?void 0:u.avg)??0)))}})})]},p)})]}),t.jsx("div",{className:`${e}-grid`,children:Y.map((r,p)=>t.jsxs("div",{className:`${e}-row`,children:[t.jsx("div",{className:B(`${e}-rowLabel`,ce===p&&`${e}-rowLabelOn`),title:i.rowLabels[r],children:i.rowLabels[r]}),i.values[r].map((h,u)=>t.jsx(Ce,{value:h,t:O(h),scaleName:y,showVal:R&&L>=28,cellSize:L,onEnter:m=>he(p,u,m),onLeave:pe},u))]},r))}),t.jsxs("div",{className:`${e}-footer`,children:[t.jsx(Le,{scale:y,min:i.min,max:i.max,theme:f}),N&&t.jsxs("div",{className:`${e}-summaryStats`,children:[t.jsxs("span",{className:`${e}-statItem`,children:[t.jsx("span",{children:"Σ"})," ",C(N.sum)]}),t.jsxs("span",{className:`${e}-statItem`,children:[t.jsx("span",{children:"x̄"})," ",C(N.avg)]}),t.jsxs("span",{className:`${e}-statItem`,children:[t.jsx("span",{children:"↑"})," ",C(N.max)]}),t.jsxs("span",{className:`${e}-statItem`,children:[t.jsx("span",{children:"↓"})," ",C(N.min)]}),t.jsx("span",{className:`${e}-statNulls`,children:N.total-N.count>0?`${(N.total-N.count).toLocaleString()} null cells`:`${N.count.toLocaleString()} cells`})]})]})]})]}),t.jsx(Be,{content:W.content,x:W.x,y:W.y,visible:W.visible,theme:f})]})}module.exports=Oe;
|
|
215
|
+
//# sourceMappingURL=index.js.map
|