@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.
- package/CHANGELOG.md +68 -0
- package/LICENSE.md +21 -0
- package/README.md +361 -0
- package/bin/cli.ts +169 -0
- package/dist/Analytics.d.ts +558 -0
- package/dist/api.d.ts +109 -0
- package/dist/batching.d.ts +93 -0
- package/dist/chunk-2mx7fq49.js +4 -0
- package/dist/chunk-3z29508k.js +204 -0
- package/dist/chunk-deephkz6.js +56 -0
- package/dist/chunk-j261vgyp.js +305 -0
- package/dist/chunk-xga17tz7.js +207 -0
- package/dist/config.d.ts +132 -0
- package/dist/dashboard/components/index.d.ts +80 -0
- package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
- package/dist/dashboard/index.d.ts +110 -0
- package/dist/dashboard/types/index.d.ts +144 -0
- package/dist/dashboard/utils/index.d.ts +81 -0
- package/dist/dynamodb.d.ts +86 -0
- package/dist/funnels.d.ts +104 -0
- package/dist/geolocation.d.ts +96 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.js +10211 -0
- package/dist/infrastructure/cdk.d.ts +60 -0
- package/dist/infrastructure/cloudformation.d.ts +44 -0
- package/dist/infrastructure/index.d.ts +45 -0
- package/dist/infrastructure/setup.d.ts +133 -0
- package/dist/integrations/cloudflare.d.ts +76 -0
- package/dist/integrations/hono.d.ts +60 -0
- package/dist/integrations/index.d.ts +24 -0
- package/dist/integrations/nuxt.d.ts +7 -0
- package/dist/integrations/nuxt.js +42 -0
- package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
- package/dist/integrations/runtime/use-ts-analytics.js +17 -0
- package/dist/integrations/stx.d.ts +88 -0
- package/dist/integrations/stx.js +18 -0
- package/dist/lib/crypto-random.d.ts +4 -0
- package/dist/lib/salt.d.ts +16 -0
- package/dist/local.d.ts +56 -0
- package/dist/model-connector.d.ts +145 -0
- package/dist/models/AggregatedStats.d.ts +9 -0
- package/dist/models/CampaignStats.d.ts +9 -0
- package/dist/models/Conversion.d.ts +11 -0
- package/dist/models/CustomEvent.d.ts +11 -0
- package/dist/models/DeviceStats.d.ts +9 -0
- package/dist/models/EventStats.d.ts +9 -0
- package/dist/models/GeoStats.d.ts +9 -0
- package/dist/models/Goal.d.ts +9 -0
- package/dist/models/GoalStats.d.ts +9 -0
- package/dist/models/PageStats.d.ts +11 -0
- package/dist/models/PageView.d.ts +13 -0
- package/dist/models/RealtimeStats.d.ts +9 -0
- package/dist/models/ReferrerStats.d.ts +9 -0
- package/dist/models/Session.d.ts +11 -0
- package/dist/models/Site.d.ts +11 -0
- package/dist/models/index.d.ts +28 -0
- package/dist/models/types.d.ts +60 -0
- package/dist/sqs-buffering.d.ts +243 -0
- package/dist/stacks-integration.d.ts +159 -0
- package/dist/tracking-script.d.ts +71 -0
- package/dist/tracking.d.ts +17 -0
- package/dist/tracking.js +26 -0
- package/dist/types.d.ts +595 -0
- package/dist/utils/geolocation.d.ts +111 -0
- package/dist/utils/user-agent.d.ts +17 -0
- package/dist/version.d.ts +7 -0
- package/package.json +119 -0
- package/src/Analytics.ts +3349 -0
- package/src/api.ts +1286 -0
- package/src/assets/crosswind.css +2220 -0
- package/src/batching.ts +452 -0
- package/src/components/dashboard/index.ts +18 -0
- package/src/config.ts +456 -0
- package/src/dashboard/Dashboard.stx +1517 -0
- package/src/dashboard/components/AlertCard.stx +177 -0
- package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
- package/src/dashboard/components/AnimatedNumber.stx +86 -0
- package/src/dashboard/components/BarChart.stx +220 -0
- package/src/dashboard/components/BrowserBreakdown.stx +98 -0
- package/src/dashboard/components/BrowsersTable.stx +125 -0
- package/src/dashboard/components/CampaignBreakdown.stx +163 -0
- package/src/dashboard/components/CampaignTable.stx +238 -0
- package/src/dashboard/components/CountryList.stx +101 -0
- package/src/dashboard/components/DataTable.stx +226 -0
- package/src/dashboard/components/DateRangePicker.stx +77 -0
- package/src/dashboard/components/DeviceBreakdown.stx +94 -0
- package/src/dashboard/components/DevicesTable.stx +163 -0
- package/src/dashboard/components/DonutChart.stories.ts +55 -0
- package/src/dashboard/components/DonutChart.stx +157 -0
- package/src/dashboard/components/EmptyState.stx +90 -0
- package/src/dashboard/components/EngagementMetrics.stx +183 -0
- package/src/dashboard/components/EventsSection.stx +192 -0
- package/src/dashboard/components/FilterBar.stx +104 -0
- package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
- package/src/dashboard/components/FunnelChart.stx +142 -0
- package/src/dashboard/components/GeoTable.stx +337 -0
- package/src/dashboard/components/GoalsPanel.stx +109 -0
- package/src/dashboard/components/Header.stx +39 -0
- package/src/dashboard/components/HeatmapChart.stx +140 -0
- package/src/dashboard/components/LiveActivityFeed.stx +172 -0
- package/src/dashboard/components/MetricComparison.stx +106 -0
- package/src/dashboard/components/MiniStats.stx +96 -0
- package/src/dashboard/components/OSBreakdown.stx +124 -0
- package/src/dashboard/components/PageDetailCard.stx +102 -0
- package/src/dashboard/components/PagesTable.stx +127 -0
- package/src/dashboard/components/ProgressRing.stx +89 -0
- package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
- package/src/dashboard/components/RealtimeCounter.stx +46 -0
- package/src/dashboard/components/ReferrersTable.stx +180 -0
- package/src/dashboard/components/SparklineChart.stx +160 -0
- package/src/dashboard/components/StatCard.stories.ts +58 -0
- package/src/dashboard/components/StatCard.stx +90 -0
- package/src/dashboard/components/SummaryStats.stx +81 -0
- package/src/dashboard/components/TabNav.stx +66 -0
- package/src/dashboard/components/ThemeSwitcher.stx +124 -0
- package/src/dashboard/components/TimeSeriesChart.stx +106 -0
- package/src/dashboard/components/TopList.stories.ts +58 -0
- package/src/dashboard/components/TopList.stx +74 -0
- package/src/dashboard/components/TrendIndicator.stx +84 -0
- package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
- package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
- package/src/dashboard/components/heatmap/index.ts +12 -0
- package/src/dashboard/components/index.ts +86 -0
- package/src/dashboard/composables/useAnalytics.ts +465 -0
- package/src/dashboard/demo/DemoApp.stx +370 -0
- package/src/dashboard/demo/index.ts +8 -0
- package/src/dashboard/demo/mockData.ts +234 -0
- package/src/dashboard/index.ts +117 -0
- package/src/dashboard/stx-shim.d.ts +4 -0
- package/src/dashboard/types/index.ts +203 -0
- package/src/dashboard/utils/index.ts +426 -0
- package/src/dynamodb.ts +344 -0
- package/src/funnels.ts +534 -0
- package/src/geolocation.ts +515 -0
- package/src/handlers/alerts.ts +328 -0
- package/src/handlers/annotations.ts +128 -0
- package/src/handlers/api-keys.ts +240 -0
- package/src/handlers/auth.ts +1020 -0
- package/src/handlers/authz.ts +137 -0
- package/src/handlers/collect.ts +724 -0
- package/src/handlers/data.ts +625 -0
- package/src/handlers/experiments.ts +138 -0
- package/src/handlers/funnels.ts +216 -0
- package/src/handlers/goals.ts +218 -0
- package/src/handlers/heatmaps.ts +272 -0
- package/src/handlers/index.ts +56 -0
- package/src/handlers/lib/read-cache.ts +63 -0
- package/src/handlers/misc.ts +486 -0
- package/src/handlers/oauth.ts +233 -0
- package/src/handlers/performance.ts +388 -0
- package/src/handlers/sessions.ts +413 -0
- package/src/handlers/sharing.ts +198 -0
- package/src/handlers/stats.ts +1368 -0
- package/src/handlers/team.ts +161 -0
- package/src/handlers/uptime.ts +283 -0
- package/src/handlers/views.ts +390 -0
- package/src/handlers/webhooks.ts +226 -0
- package/src/heatmap/index.ts +32 -0
- package/src/heatmap/tracking-script.ts +452 -0
- package/src/heatmap/types.ts +79 -0
- package/src/index.ts +387 -0
- package/src/infrastructure/cdk.ts +496 -0
- package/src/infrastructure/cloudformation.ts +595 -0
- package/src/infrastructure/index.ts +48 -0
- package/src/infrastructure/setup.ts +611 -0
- package/src/integrations/cloudflare.ts +732 -0
- package/src/integrations/hono.ts +589 -0
- package/src/integrations/index.ts +27 -0
- package/src/integrations/nuxt.ts +78 -0
- package/src/integrations/runtime/use-ts-analytics.ts +32 -0
- package/src/integrations/stx.ts +138 -0
- package/src/jobs/index.ts +127 -0
- package/src/lib/crypto-random.ts +41 -0
- package/src/lib/ddb-errors.ts +20 -0
- package/src/lib/dynamodb.ts +216 -0
- package/src/lib/email.ts +38 -0
- package/src/lib/ga-import.ts +471 -0
- package/src/lib/ga4-api.ts +244 -0
- package/src/lib/goals.ts +205 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/ingest-counters.ts +123 -0
- package/src/lib/log.ts +36 -0
- package/src/lib/membership.ts +98 -0
- package/src/lib/plans.ts +90 -0
- package/src/lib/quota.ts +46 -0
- package/src/lib/rate-limit.ts +27 -0
- package/src/lib/rollups.ts +472 -0
- package/src/lib/salt.ts +97 -0
- package/src/lib/scheduler.ts +97 -0
- package/src/lib/significance.ts +66 -0
- package/src/lib/site-retention.ts +56 -0
- package/src/local.ts +360 -0
- package/src/model-connector.ts +616 -0
- package/src/models/AggregatedStats.ts +162 -0
- package/src/models/CampaignStats.ts +141 -0
- package/src/models/Conversion.ts +135 -0
- package/src/models/CustomEvent.ts +123 -0
- package/src/models/DeviceStats.ts +105 -0
- package/src/models/EventStats.ts +108 -0
- package/src/models/GeoStats.ts +112 -0
- package/src/models/Goal.ts +105 -0
- package/src/models/GoalStats.ts +106 -0
- package/src/models/PageStats.ts +152 -0
- package/src/models/PageView.ts +277 -0
- package/src/models/RealtimeStats.ts +96 -0
- package/src/models/ReferrerStats.ts +115 -0
- package/src/models/Session.ts +235 -0
- package/src/models/Site.ts +115 -0
- package/src/models/index.ts +50 -0
- package/src/models/orm/index.ts +2043 -0
- package/src/models/types.ts +71 -0
- package/src/router.ts +521 -0
- package/src/sqs-buffering.ts +806 -0
- package/src/stacks-integration.ts +527 -0
- package/src/tracking-script.ts +944 -0
- package/src/tracking.ts +27 -0
- package/src/types/analytics.ts +219 -0
- package/src/types/api.ts +75 -0
- package/src/types/bun-router.d.ts +15 -0
- package/src/types/dashboard.ts +62 -0
- package/src/types/index.ts +66 -0
- package/src/types/stx.d.ts +27 -0
- package/src/types/window.d.ts +101 -0
- package/src/types.ts +911 -0
- package/src/utils/cache.ts +148 -0
- package/src/utils/date.ts +118 -0
- package/src/utils/filters.ts +71 -0
- package/src/utils/geolocation.ts +323 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/response.ts +180 -0
- package/src/utils/timezone-country.ts +242 -0
- package/src/utils/user-agent.ts +110 -0
- 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
|
+
}
|