@stacksjs/ts-analytics 0.1.6

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.
Files changed (237) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +361 -0
  4. package/bin/cli.ts +169 -0
  5. package/dist/Analytics.d.ts +558 -0
  6. package/dist/api.d.ts +109 -0
  7. package/dist/batching.d.ts +93 -0
  8. package/dist/chunk-2mx7fq49.js +4 -0
  9. package/dist/chunk-3z29508k.js +204 -0
  10. package/dist/chunk-deephkz6.js +56 -0
  11. package/dist/chunk-j261vgyp.js +305 -0
  12. package/dist/chunk-xga17tz7.js +207 -0
  13. package/dist/config.d.ts +132 -0
  14. package/dist/dashboard/components/index.d.ts +80 -0
  15. package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
  16. package/dist/dashboard/index.d.ts +110 -0
  17. package/dist/dashboard/types/index.d.ts +144 -0
  18. package/dist/dashboard/utils/index.d.ts +81 -0
  19. package/dist/dynamodb.d.ts +86 -0
  20. package/dist/funnels.d.ts +104 -0
  21. package/dist/geolocation.d.ts +96 -0
  22. package/dist/index.d.ts +371 -0
  23. package/dist/index.js +10211 -0
  24. package/dist/infrastructure/cdk.d.ts +60 -0
  25. package/dist/infrastructure/cloudformation.d.ts +44 -0
  26. package/dist/infrastructure/index.d.ts +45 -0
  27. package/dist/infrastructure/setup.d.ts +133 -0
  28. package/dist/integrations/cloudflare.d.ts +76 -0
  29. package/dist/integrations/hono.d.ts +60 -0
  30. package/dist/integrations/index.d.ts +24 -0
  31. package/dist/integrations/nuxt.d.ts +7 -0
  32. package/dist/integrations/nuxt.js +42 -0
  33. package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
  34. package/dist/integrations/runtime/use-ts-analytics.js +17 -0
  35. package/dist/integrations/stx.d.ts +88 -0
  36. package/dist/integrations/stx.js +18 -0
  37. package/dist/lib/crypto-random.d.ts +4 -0
  38. package/dist/lib/salt.d.ts +16 -0
  39. package/dist/local.d.ts +56 -0
  40. package/dist/model-connector.d.ts +145 -0
  41. package/dist/models/AggregatedStats.d.ts +9 -0
  42. package/dist/models/CampaignStats.d.ts +9 -0
  43. package/dist/models/Conversion.d.ts +11 -0
  44. package/dist/models/CustomEvent.d.ts +11 -0
  45. package/dist/models/DeviceStats.d.ts +9 -0
  46. package/dist/models/EventStats.d.ts +9 -0
  47. package/dist/models/GeoStats.d.ts +9 -0
  48. package/dist/models/Goal.d.ts +9 -0
  49. package/dist/models/GoalStats.d.ts +9 -0
  50. package/dist/models/PageStats.d.ts +11 -0
  51. package/dist/models/PageView.d.ts +13 -0
  52. package/dist/models/RealtimeStats.d.ts +9 -0
  53. package/dist/models/ReferrerStats.d.ts +9 -0
  54. package/dist/models/Session.d.ts +11 -0
  55. package/dist/models/Site.d.ts +11 -0
  56. package/dist/models/index.d.ts +28 -0
  57. package/dist/models/types.d.ts +60 -0
  58. package/dist/sqs-buffering.d.ts +243 -0
  59. package/dist/stacks-integration.d.ts +159 -0
  60. package/dist/tracking-script.d.ts +71 -0
  61. package/dist/tracking.d.ts +17 -0
  62. package/dist/tracking.js +26 -0
  63. package/dist/types.d.ts +595 -0
  64. package/dist/utils/geolocation.d.ts +111 -0
  65. package/dist/utils/user-agent.d.ts +17 -0
  66. package/dist/version.d.ts +7 -0
  67. package/package.json +119 -0
  68. package/src/Analytics.ts +3349 -0
  69. package/src/api.ts +1286 -0
  70. package/src/assets/crosswind.css +2220 -0
  71. package/src/batching.ts +452 -0
  72. package/src/components/dashboard/index.ts +18 -0
  73. package/src/config.ts +456 -0
  74. package/src/dashboard/Dashboard.stx +1517 -0
  75. package/src/dashboard/components/AlertCard.stx +177 -0
  76. package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
  77. package/src/dashboard/components/AnimatedNumber.stx +86 -0
  78. package/src/dashboard/components/BarChart.stx +220 -0
  79. package/src/dashboard/components/BrowserBreakdown.stx +98 -0
  80. package/src/dashboard/components/BrowsersTable.stx +125 -0
  81. package/src/dashboard/components/CampaignBreakdown.stx +163 -0
  82. package/src/dashboard/components/CampaignTable.stx +238 -0
  83. package/src/dashboard/components/CountryList.stx +101 -0
  84. package/src/dashboard/components/DataTable.stx +226 -0
  85. package/src/dashboard/components/DateRangePicker.stx +77 -0
  86. package/src/dashboard/components/DeviceBreakdown.stx +94 -0
  87. package/src/dashboard/components/DevicesTable.stx +163 -0
  88. package/src/dashboard/components/DonutChart.stories.ts +55 -0
  89. package/src/dashboard/components/DonutChart.stx +157 -0
  90. package/src/dashboard/components/EmptyState.stx +90 -0
  91. package/src/dashboard/components/EngagementMetrics.stx +183 -0
  92. package/src/dashboard/components/EventsSection.stx +192 -0
  93. package/src/dashboard/components/FilterBar.stx +104 -0
  94. package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
  95. package/src/dashboard/components/FunnelChart.stx +142 -0
  96. package/src/dashboard/components/GeoTable.stx +337 -0
  97. package/src/dashboard/components/GoalsPanel.stx +109 -0
  98. package/src/dashboard/components/Header.stx +39 -0
  99. package/src/dashboard/components/HeatmapChart.stx +140 -0
  100. package/src/dashboard/components/LiveActivityFeed.stx +172 -0
  101. package/src/dashboard/components/MetricComparison.stx +106 -0
  102. package/src/dashboard/components/MiniStats.stx +96 -0
  103. package/src/dashboard/components/OSBreakdown.stx +124 -0
  104. package/src/dashboard/components/PageDetailCard.stx +102 -0
  105. package/src/dashboard/components/PagesTable.stx +127 -0
  106. package/src/dashboard/components/ProgressRing.stx +89 -0
  107. package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
  108. package/src/dashboard/components/RealtimeCounter.stx +46 -0
  109. package/src/dashboard/components/ReferrersTable.stx +180 -0
  110. package/src/dashboard/components/SparklineChart.stx +160 -0
  111. package/src/dashboard/components/StatCard.stories.ts +58 -0
  112. package/src/dashboard/components/StatCard.stx +90 -0
  113. package/src/dashboard/components/SummaryStats.stx +81 -0
  114. package/src/dashboard/components/TabNav.stx +66 -0
  115. package/src/dashboard/components/ThemeSwitcher.stx +124 -0
  116. package/src/dashboard/components/TimeSeriesChart.stx +106 -0
  117. package/src/dashboard/components/TopList.stories.ts +58 -0
  118. package/src/dashboard/components/TopList.stx +74 -0
  119. package/src/dashboard/components/TrendIndicator.stx +84 -0
  120. package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
  121. package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
  122. package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
  123. package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
  124. package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
  125. package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
  126. package/src/dashboard/components/heatmap/index.ts +12 -0
  127. package/src/dashboard/components/index.ts +86 -0
  128. package/src/dashboard/composables/useAnalytics.ts +465 -0
  129. package/src/dashboard/demo/DemoApp.stx +370 -0
  130. package/src/dashboard/demo/index.ts +8 -0
  131. package/src/dashboard/demo/mockData.ts +234 -0
  132. package/src/dashboard/index.ts +117 -0
  133. package/src/dashboard/stx-shim.d.ts +4 -0
  134. package/src/dashboard/types/index.ts +203 -0
  135. package/src/dashboard/utils/index.ts +426 -0
  136. package/src/dynamodb.ts +344 -0
  137. package/src/funnels.ts +534 -0
  138. package/src/geolocation.ts +515 -0
  139. package/src/handlers/alerts.ts +328 -0
  140. package/src/handlers/annotations.ts +128 -0
  141. package/src/handlers/api-keys.ts +240 -0
  142. package/src/handlers/auth.ts +1020 -0
  143. package/src/handlers/authz.ts +137 -0
  144. package/src/handlers/collect.ts +724 -0
  145. package/src/handlers/data.ts +625 -0
  146. package/src/handlers/experiments.ts +138 -0
  147. package/src/handlers/funnels.ts +216 -0
  148. package/src/handlers/goals.ts +218 -0
  149. package/src/handlers/heatmaps.ts +272 -0
  150. package/src/handlers/index.ts +56 -0
  151. package/src/handlers/lib/read-cache.ts +63 -0
  152. package/src/handlers/misc.ts +486 -0
  153. package/src/handlers/oauth.ts +233 -0
  154. package/src/handlers/performance.ts +388 -0
  155. package/src/handlers/sessions.ts +413 -0
  156. package/src/handlers/sharing.ts +198 -0
  157. package/src/handlers/stats.ts +1368 -0
  158. package/src/handlers/team.ts +161 -0
  159. package/src/handlers/uptime.ts +283 -0
  160. package/src/handlers/views.ts +390 -0
  161. package/src/handlers/webhooks.ts +226 -0
  162. package/src/heatmap/index.ts +32 -0
  163. package/src/heatmap/tracking-script.ts +452 -0
  164. package/src/heatmap/types.ts +79 -0
  165. package/src/index.ts +387 -0
  166. package/src/infrastructure/cdk.ts +496 -0
  167. package/src/infrastructure/cloudformation.ts +595 -0
  168. package/src/infrastructure/index.ts +48 -0
  169. package/src/infrastructure/setup.ts +611 -0
  170. package/src/integrations/cloudflare.ts +732 -0
  171. package/src/integrations/hono.ts +589 -0
  172. package/src/integrations/index.ts +27 -0
  173. package/src/integrations/nuxt.ts +78 -0
  174. package/src/integrations/runtime/use-ts-analytics.ts +32 -0
  175. package/src/integrations/stx.ts +138 -0
  176. package/src/jobs/index.ts +127 -0
  177. package/src/lib/crypto-random.ts +41 -0
  178. package/src/lib/ddb-errors.ts +20 -0
  179. package/src/lib/dynamodb.ts +216 -0
  180. package/src/lib/email.ts +38 -0
  181. package/src/lib/ga-import.ts +471 -0
  182. package/src/lib/ga4-api.ts +244 -0
  183. package/src/lib/goals.ts +205 -0
  184. package/src/lib/index.ts +6 -0
  185. package/src/lib/ingest-counters.ts +123 -0
  186. package/src/lib/log.ts +36 -0
  187. package/src/lib/membership.ts +98 -0
  188. package/src/lib/plans.ts +90 -0
  189. package/src/lib/quota.ts +46 -0
  190. package/src/lib/rate-limit.ts +27 -0
  191. package/src/lib/rollups.ts +472 -0
  192. package/src/lib/salt.ts +97 -0
  193. package/src/lib/scheduler.ts +97 -0
  194. package/src/lib/significance.ts +66 -0
  195. package/src/lib/site-retention.ts +56 -0
  196. package/src/local.ts +360 -0
  197. package/src/model-connector.ts +616 -0
  198. package/src/models/AggregatedStats.ts +162 -0
  199. package/src/models/CampaignStats.ts +141 -0
  200. package/src/models/Conversion.ts +135 -0
  201. package/src/models/CustomEvent.ts +123 -0
  202. package/src/models/DeviceStats.ts +105 -0
  203. package/src/models/EventStats.ts +108 -0
  204. package/src/models/GeoStats.ts +112 -0
  205. package/src/models/Goal.ts +105 -0
  206. package/src/models/GoalStats.ts +106 -0
  207. package/src/models/PageStats.ts +152 -0
  208. package/src/models/PageView.ts +277 -0
  209. package/src/models/RealtimeStats.ts +96 -0
  210. package/src/models/ReferrerStats.ts +115 -0
  211. package/src/models/Session.ts +235 -0
  212. package/src/models/Site.ts +115 -0
  213. package/src/models/index.ts +50 -0
  214. package/src/models/orm/index.ts +2043 -0
  215. package/src/models/types.ts +71 -0
  216. package/src/router.ts +521 -0
  217. package/src/sqs-buffering.ts +806 -0
  218. package/src/stacks-integration.ts +527 -0
  219. package/src/tracking-script.ts +944 -0
  220. package/src/tracking.ts +27 -0
  221. package/src/types/analytics.ts +219 -0
  222. package/src/types/api.ts +75 -0
  223. package/src/types/bun-router.d.ts +15 -0
  224. package/src/types/dashboard.ts +62 -0
  225. package/src/types/index.ts +66 -0
  226. package/src/types/stx.d.ts +27 -0
  227. package/src/types/window.d.ts +101 -0
  228. package/src/types.ts +911 -0
  229. package/src/utils/cache.ts +148 -0
  230. package/src/utils/date.ts +118 -0
  231. package/src/utils/filters.ts +71 -0
  232. package/src/utils/geolocation.ts +323 -0
  233. package/src/utils/index.ts +9 -0
  234. package/src/utils/response.ts +180 -0
  235. package/src/utils/timezone-country.ts +242 -0
  236. package/src/utils/user-agent.ts +110 -0
  237. package/src/version.ts +7 -0
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Analytics Dashboard Utilities
3
+ */
4
+
5
+ // ============================================================================
6
+ // Number Formatting
7
+ // ============================================================================
8
+
9
+ /**
10
+ * Format a number with locale-aware separators
11
+ */
12
+ export function formatNumber(value: number, decimals = 0): string {
13
+ return new Intl.NumberFormat('en-US', {
14
+ minimumFractionDigits: decimals,
15
+ maximumFractionDigits: decimals,
16
+ }).format(value)
17
+ }
18
+
19
+ /**
20
+ * Format a number as compact (1.2K, 3.4M, etc.)
21
+ */
22
+ export function formatCompact(value: number): string {
23
+ if (value < 1000)
24
+ return String(value)
25
+ if (value < 1000000)
26
+ return `${(value / 1000).toFixed(1)}K`
27
+ if (value < 1000000000)
28
+ return `${(value / 1000000).toFixed(1)}M`
29
+ return `${(value / 1000000000).toFixed(1)}B`
30
+ }
31
+
32
+ /**
33
+ * Format a percentage
34
+ */
35
+ export function formatPercentage(value: number, decimals = 1): string {
36
+ return `${(value * 100).toFixed(decimals)}%`
37
+ }
38
+
39
+ /**
40
+ * Format duration in milliseconds to human-readable string
41
+ */
42
+ export function formatDuration(ms: number): string {
43
+ if (ms < 1000)
44
+ return `${ms}ms`
45
+
46
+ const seconds = Math.floor(ms / 1000)
47
+ if (seconds < 60)
48
+ return `${seconds}s`
49
+
50
+ const minutes = Math.floor(seconds / 60)
51
+ const remainingSeconds = seconds % 60
52
+
53
+ if (minutes < 60) {
54
+ return remainingSeconds > 0
55
+ ? `${minutes}m ${remainingSeconds}s`
56
+ : `${minutes}m`
57
+ }
58
+
59
+ const hours = Math.floor(minutes / 60)
60
+ const remainingMinutes = minutes % 60
61
+
62
+ return remainingMinutes > 0
63
+ ? `${hours}h ${remainingMinutes}m`
64
+ : `${hours}h`
65
+ }
66
+
67
+ /**
68
+ * Format currency
69
+ */
70
+ export function formatCurrency(value: number, currency = 'USD'): string {
71
+ return new Intl.NumberFormat('en-US', {
72
+ style: 'currency',
73
+ currency,
74
+ }).format(value)
75
+ }
76
+
77
+ /**
78
+ * Format a change percentage with + or - sign
79
+ */
80
+ export function formatChange(value: number): string {
81
+ const sign = value >= 0 ? '+' : ''
82
+ return `${sign}${(value * 100).toFixed(1)}%`
83
+ }
84
+
85
+ // ============================================================================
86
+ // Date Formatting
87
+ // ============================================================================
88
+
89
+ /**
90
+ * Format date for display
91
+ */
92
+ export function formatDate(date: Date | string, format: 'short' | 'long' | 'time' = 'short'): string {
93
+ const d = typeof date === 'string' ? new Date(date) : date
94
+
95
+ switch (format) {
96
+ case 'short':
97
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
98
+ case 'long':
99
+ return d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })
100
+ case 'time':
101
+ return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
102
+ default:
103
+ return d.toLocaleDateString()
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Format date range for display
109
+ */
110
+ export function formatDateRange(start: Date, end: Date): string {
111
+ const startStr = formatDate(start, 'short')
112
+ const endStr = formatDate(end, 'short')
113
+
114
+ if (startStr === endStr)
115
+ return startStr
116
+
117
+ return `${startStr} - ${endStr}`
118
+ }
119
+
120
+ /**
121
+ * Get relative time string (e.g., "2 hours ago")
122
+ */
123
+ export function getRelativeTime(date: Date | string): string {
124
+ const d = typeof date === 'string' ? new Date(date) : date
125
+ const now = new Date()
126
+ const diffMs = now.getTime() - d.getTime()
127
+ const diffSeconds = Math.floor(diffMs / 1000)
128
+
129
+ if (diffSeconds < 60)
130
+ return 'just now'
131
+ if (diffSeconds < 3600)
132
+ return `${Math.floor(diffSeconds / 60)}m ago`
133
+ if (diffSeconds < 86400)
134
+ return `${Math.floor(diffSeconds / 3600)}h ago`
135
+ if (diffSeconds < 604800)
136
+ return `${Math.floor(diffSeconds / 86400)}d ago`
137
+
138
+ return formatDate(d, 'short')
139
+ }
140
+
141
+ // ============================================================================
142
+ // Date Range Helpers
143
+ // ============================================================================
144
+
145
+ export interface DateRangePreset {
146
+ label: string
147
+ value: string
148
+ getRange: () => { start: Date, end: Date }
149
+ }
150
+
151
+ export const dateRangePresets: DateRangePreset[] = [
152
+ {
153
+ label: 'Today',
154
+ value: 'today',
155
+ getRange: () => {
156
+ const start = new Date()
157
+ start.setHours(0, 0, 0, 0)
158
+ return { start, end: new Date() }
159
+ },
160
+ },
161
+ {
162
+ label: 'Yesterday',
163
+ value: 'yesterday',
164
+ getRange: () => {
165
+ const start = new Date()
166
+ start.setDate(start.getDate() - 1)
167
+ start.setHours(0, 0, 0, 0)
168
+ const end = new Date(start)
169
+ end.setHours(23, 59, 59, 999)
170
+ return { start, end }
171
+ },
172
+ },
173
+ {
174
+ label: 'Last 7 days',
175
+ value: '7d',
176
+ getRange: () => {
177
+ const end = new Date()
178
+ const start = new Date()
179
+ start.setDate(start.getDate() - 7)
180
+ start.setHours(0, 0, 0, 0)
181
+ return { start, end }
182
+ },
183
+ },
184
+ {
185
+ label: 'Last 30 days',
186
+ value: '30d',
187
+ getRange: () => {
188
+ const end = new Date()
189
+ const start = new Date()
190
+ start.setDate(start.getDate() - 30)
191
+ start.setHours(0, 0, 0, 0)
192
+ return { start, end }
193
+ },
194
+ },
195
+ {
196
+ label: 'Last 90 days',
197
+ value: '90d',
198
+ getRange: () => {
199
+ const end = new Date()
200
+ const start = new Date()
201
+ start.setDate(start.getDate() - 90)
202
+ start.setHours(0, 0, 0, 0)
203
+ return { start, end }
204
+ },
205
+ },
206
+ {
207
+ label: 'This month',
208
+ value: 'month',
209
+ getRange: () => {
210
+ const start = new Date()
211
+ start.setDate(1)
212
+ start.setHours(0, 0, 0, 0)
213
+ return { start, end: new Date() }
214
+ },
215
+ },
216
+ {
217
+ label: 'Last month',
218
+ value: 'lastMonth',
219
+ getRange: () => {
220
+ const end = new Date()
221
+ end.setDate(0) // Last day of previous month
222
+ end.setHours(23, 59, 59, 999)
223
+ const start = new Date(end)
224
+ start.setDate(1)
225
+ start.setHours(0, 0, 0, 0)
226
+ return { start, end }
227
+ },
228
+ },
229
+ {
230
+ label: 'This year',
231
+ value: 'year',
232
+ getRange: () => {
233
+ const start = new Date()
234
+ start.setMonth(0, 1)
235
+ start.setHours(0, 0, 0, 0)
236
+ return { start, end: new Date() }
237
+ },
238
+ },
239
+ ]
240
+
241
+ /**
242
+ * Get date range from preset value
243
+ */
244
+ export function getDateRangeFromPreset(preset: string): { start: Date, end: Date } | null {
245
+ const found = dateRangePresets.find(p => p.value === preset)
246
+ return found ? found.getRange() : null
247
+ }
248
+
249
+ // ============================================================================
250
+ // Chart Helpers
251
+ // ============================================================================
252
+
253
+ /**
254
+ * Generate chart gradient
255
+ */
256
+ export function createChartGradient(
257
+ ctx: CanvasRenderingContext2D,
258
+ color: string,
259
+ height: number,
260
+ ): CanvasGradient {
261
+ const gradient = ctx.createLinearGradient(0, 0, 0, height)
262
+ gradient.addColorStop(0, `${color}40`)
263
+ gradient.addColorStop(1, `${color}00`)
264
+ return gradient
265
+ }
266
+
267
+ /**
268
+ * Calculate nice axis ticks
269
+ */
270
+ export function calculateAxisTicks(min: number, max: number, count = 5): number[] {
271
+ const range = max - min
272
+ const roughStep = range / (count - 1)
273
+ const magnitude = 10 ** Math.floor(Math.log10(roughStep))
274
+ const residual = roughStep / magnitude
275
+
276
+ let step: number
277
+ if (residual > 5)
278
+ step = 10 * magnitude
279
+ else if (residual > 2)
280
+ step = 5 * magnitude
281
+ else if (residual > 1)
282
+ step = 2 * magnitude
283
+ else step = magnitude
284
+
285
+ const ticks: number[] = []
286
+ const start = Math.floor(min / step) * step
287
+ for (let tick = start; tick <= max; tick += step) {
288
+ ticks.push(tick)
289
+ }
290
+
291
+ return ticks
292
+ }
293
+
294
+ // ============================================================================
295
+ // Data Processing
296
+ // ============================================================================
297
+
298
+ /**
299
+ * Calculate percentage change between two values
300
+ */
301
+ export function calculateChange(current: number, previous: number): number {
302
+ if (previous === 0)
303
+ return current > 0 ? 1 : 0
304
+ return (current - previous) / previous
305
+ }
306
+
307
+ /**
308
+ * Aggregate time series data by period
309
+ */
310
+ export function aggregateTimeSeries<T extends { date: string }>(
311
+ data: T[],
312
+ period: 'hour' | 'day' | 'week' | 'month',
313
+ aggregator: (items: T[]) => Omit<T, 'date'>,
314
+ ): T[] {
315
+ const grouped = new Map<string, T[]>()
316
+
317
+ for (const item of data) {
318
+ const date = new Date(item.date)
319
+ let key: string
320
+
321
+ switch (period) {
322
+ case 'hour':
323
+ key = `${date.toISOString().slice(0, 13)}:00:00`
324
+ break
325
+ case 'day':
326
+ key = date.toISOString().slice(0, 10)
327
+ break
328
+ case 'week': {
329
+ const weekStart = new Date(date)
330
+ weekStart.setDate(date.getDate() - date.getDay())
331
+ key = weekStart.toISOString().slice(0, 10)
332
+ break
333
+ }
334
+ case 'month':
335
+ key = date.toISOString().slice(0, 7)
336
+ break
337
+ }
338
+
339
+ if (!grouped.has(key)) {
340
+ grouped.set(key, [])
341
+ }
342
+ grouped.get(key)!.push(item)
343
+ }
344
+
345
+ return Array.from(grouped.entries()).map(([date, items]) => ({
346
+ date,
347
+ ...aggregator(items),
348
+ })) as T[]
349
+ }
350
+
351
+ /**
352
+ * Fill missing dates in time series
353
+ */
354
+ export function fillMissingDates<T extends { date: string }>(
355
+ data: T[],
356
+ startDate: Date,
357
+ endDate: Date,
358
+ defaultValue: Omit<T, 'date'>,
359
+ ): T[] {
360
+ const result: T[] = []
361
+ const dataMap = new Map(data.map(d => [d.date.slice(0, 10), d]))
362
+
363
+ const current = new Date(startDate)
364
+ while (current <= endDate) {
365
+ const dateKey = current.toISOString().slice(0, 10)
366
+ const existing = dataMap.get(dateKey)
367
+
368
+ if (existing) {
369
+ result.push(existing)
370
+ }
371
+ else {
372
+ result.push({ date: dateKey, ...defaultValue } as T)
373
+ }
374
+
375
+ current.setDate(current.getDate() + 1)
376
+ }
377
+
378
+ return result
379
+ }
380
+
381
+ // ============================================================================
382
+ // Color Helpers
383
+ // ============================================================================
384
+
385
+ /**
386
+ * Convert hex to RGB
387
+ */
388
+ export function hexToRgb(hex: string): { r: number, g: number, b: number } | null {
389
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
390
+ return result
391
+ ? {
392
+ r: Number.parseInt(result[1], 16),
393
+ g: Number.parseInt(result[2], 16),
394
+ b: Number.parseInt(result[3], 16),
395
+ }
396
+ : null
397
+ }
398
+
399
+ /**
400
+ * Add alpha to hex color
401
+ */
402
+ export function hexWithAlpha(hex: string, alpha: number): string {
403
+ const rgb = hexToRgb(hex)
404
+ if (!rgb)
405
+ return hex
406
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`
407
+ }
408
+
409
+ /**
410
+ * Generate color palette for charts
411
+ */
412
+ export function generateColorPalette(baseColor: string, count: number): string[] {
413
+ const colors: string[] = []
414
+ const rgb = hexToRgb(baseColor)
415
+ if (!rgb)
416
+ return [baseColor]
417
+
418
+ for (let i = 0; i < count; i++) {
419
+ const factor = 1 - (i * 0.15)
420
+ colors.push(
421
+ `rgb(${Math.round(rgb.r * factor)}, ${Math.round(rgb.g * factor)}, ${Math.round(rgb.b * factor)})`,
422
+ )
423
+ }
424
+
425
+ return colors
426
+ }