rip-lang 3.10.14 → 3.12.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.
@@ -0,0 +1,921 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ACME Corp Dashboard — Rip</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
11
+ <script type="module" src="dist/rip.min.js"></script>
12
+ <style>
13
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
14
+ body {
15
+ font-family: 'Inter', sans-serif;
16
+ font-feature-settings: "cv02", "tnum";
17
+ background: #f7f7f7;
18
+ color: #2c2c2c;
19
+ padding: 32px;
20
+ }
21
+ h1 { font-size: 24px; font-weight: 600; color: #060606; margin-bottom: 4px; }
22
+ .subtitle { font-size: 14px; color: #717171; margin-bottom: 28px; }
23
+ .section {
24
+ font-size: 13px; font-weight: 500; color: #717171;
25
+ text-transform: uppercase; letter-spacing: 0.05em;
26
+ margin: 36px 0 16px; padding-bottom: 8px;
27
+ border-bottom: 1px solid #e5e5e5;
28
+ }
29
+ .section:first-of-type { margin-top: 0; }
30
+ .grid {
31
+ display: grid;
32
+ grid-template-columns: repeat(auto-fit, minmax(540px, 1fr));
33
+ gap: 20px;
34
+ }
35
+ .card {
36
+ background: #ffffff; border-radius: 8px;
37
+ border: 1px solid #e5e5e5; padding: 20px 20px 12px;
38
+ }
39
+ .card h2 { font-size: 15px; font-weight: 500; color: #060606; margin-bottom: 2px; }
40
+ .card p { font-size: 12px; color: #717171; margin-bottom: 12px; }
41
+ .chart { width: 100%; height: 380px; }
42
+ .chart-tall { width: 100%; height: 440px; }
43
+ .wide { grid-column: 1 / -1; }
44
+ .gauge-row { display: flex; gap: 0; }
45
+ .gauge-cell { flex: 1; height: 300px; }
46
+ .kpi-row {
47
+ display: grid;
48
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
49
+ gap: 16px; margin-bottom: 8px;
50
+ }
51
+ .kpi {
52
+ background: #fff; border: 1px solid #e5e5e5; border-radius: 8px;
53
+ padding: 16px 20px; display: flex; flex-direction: column; gap: 4px;
54
+ }
55
+ .kpi-label { font-size: 12px; color: #717171; font-weight: 500; }
56
+ .kpi-row-inner { display: flex; align-items: center; gap: 10px; }
57
+ .kpi-value { font-size: 26px; font-weight: 600; color: #060606; line-height: 1.1; }
58
+ .kpi-spark { flex-shrink: 0; }
59
+ .kpi-delta { font-size: 12px; font-weight: 500; }
60
+ .kpi-delta.up { color: #16a34a; }
61
+ .kpi-delta.down { color: #dc2626; }
62
+ </style>
63
+ </head>
64
+ <body>
65
+
66
+ <!-- ===== KpiCard ===== -->
67
+
68
+ <script type="text/rip" data-name="kpi-card">
69
+ export KpiCard = component
70
+ @label := ''
71
+ @value := ''
72
+ @spark := []
73
+ @delta := ''
74
+ @up := true
75
+
76
+ sparkSvg ~=
77
+ return '' unless spark and spark.length > 1
78
+ w = 64
79
+ h = 24
80
+ lo = Math.min(...spark)
81
+ hi = Math.max(...spark)
82
+ range = hi - lo or 1
83
+ pts = spark.map((v, i) ->
84
+ x = (i / (spark.length - 1)) * w
85
+ y = h - ((v - lo) / range) * (h - 4) - 2
86
+ "#{x},#{y}"
87
+ ).join(' ')
88
+ "<svg class='kpi-spark' width='#{w}' height='#{h}' viewBox='0 0 #{w} #{h}'><polyline points='#{pts}' fill='none' stroke='#236aa4' stroke-width='1.5' stroke-linejoin='round' stroke-linecap='round'/></svg>"
89
+
90
+ render
91
+ .kpi
92
+ .kpi-label label
93
+ .kpi-row-inner
94
+ .kpi-value value
95
+ span innerHTML: sparkSvg
96
+ div class: "kpi-delta #{up ? 'up' : 'down'}"
97
+ "#{up ? '▲' : '▼'} #{delta}"
98
+ </script>
99
+
100
+ <!-- ===== ChartCard ===== -->
101
+
102
+ <script type="text/rip" data-name="chart-card">
103
+ export ChartCard = component
104
+ @title := ''
105
+ @subtitle := ''
106
+ @chartId := ''
107
+ @wide := false
108
+ @tall := false
109
+
110
+ render
111
+ div class: "card#{wide ? ' wide' : ''}"
112
+ h2 title
113
+ p subtitle
114
+ div id: chartId, class: tall ? 'chart-tall' : 'chart'
115
+ </script>
116
+
117
+ <!-- ===== Dashboard ===== -->
118
+
119
+ <script type="text/rip" data-name="index">
120
+ export Dashboard = component
121
+
122
+ # ── ECharts theme (evidence.dev) ──
123
+
124
+ THEME :=
125
+ darkMode: false
126
+ backgroundColor: '#ffffff'
127
+ textStyle: { fontFamily: ['Inter', 'sans-serif'] }
128
+ color: ['#236aa4','#45a1bf','#a5cdee','#8dacbf','#85c7c6','#d2c6ac','#f4b548','#8f3d56','#71b9f4','#46a485']
129
+ grid: { left: '1%', right: '4%', bottom: '0%', top: '15%', containLabel: true }
130
+ title:
131
+ padding: 0, itemGap: 7
132
+ textStyle: { fontSize: 14, color: '#060606' }
133
+ subtextStyle: { fontSize: 13, color: '#717171', overflow: 'break' }
134
+ top: '1px'
135
+ line: { itemStyle: { borderWidth: 0 }, lineStyle: { width: 2, join: 'round' }, symbolSize: 0, symbol: 'circle', smooth: false }
136
+ radar: { itemStyle: { borderWidth: 0 }, lineStyle: { width: 2 }, symbolSize: 0, symbol: 'circle', smooth: false }
137
+ pie: { itemStyle: { borderWidth: 0, borderColor: '#cccccc' } }
138
+ scatter: { itemStyle: { borderWidth: 0, borderColor: '#cccccc' } }
139
+ boxplot: { itemStyle: { borderWidth: 1.5 } }
140
+ parallel: { itemStyle: { borderWidth: 0, borderColor: '#cccccc' } }
141
+ sankey: { itemStyle: { borderWidth: 0, borderColor: '#cccccc' } }
142
+ funnel: { itemStyle: { borderWidth: 0, borderColor: '#cccccc' } }
143
+ gauge: { itemStyle: { borderWidth: 0, borderColor: '#cccccc' } }
144
+ candlestick: { itemStyle: { color: '#eb5454', color0: '#47b262', borderColor: '#eb5454', borderColor0: '#47b262', borderWidth: 1 } }
145
+ graph:
146
+ itemStyle: { borderWidth: 0, borderColor: '#cccccc' }
147
+ lineStyle: { width: 1, color: '#aaaaaa' }
148
+ symbolSize: 0, symbol: 'circle', smooth: false
149
+ color: ['#923d59','#488f96','#518eca','#b3a9a0','#ffc857','#495867','#bfdbf7','#bc4749','#eeebd0']
150
+ label: { color: '#f2f2f2' }
151
+ categoryAxis:
152
+ axisLine: { show: true, lineStyle: { color: '#717171' } }
153
+ axisTick: { show: false, lineStyle: { color: '#717171' }, length: 3, alignWithLabel: true }
154
+ axisLabel: { show: true, color: '#717171' }
155
+ splitLine: { show: false, lineStyle: { color: ['#d6d6d6'] } }
156
+ splitArea: { show: false }
157
+ valueAxis:
158
+ axisLine: { show: false, lineStyle: { color: '#717171' } }
159
+ axisTick: { show: false, lineStyle: { color: '#717171' }, length: 2 }
160
+ axisLabel: { show: true, color: '#717171' }
161
+ splitLine: { show: true, lineStyle: { color: ['#d6d6d6'], width: 1 } }
162
+ splitArea: { show: false }
163
+ nameTextStyle: { backgroundColor: '#ffffff' }
164
+ logAxis:
165
+ axisLine: { show: false, lineStyle: { color: '#717171' } }
166
+ axisTick: { show: false, lineStyle: { color: '#717171' }, length: 2 }
167
+ axisLabel: { show: true, color: '#717171' }
168
+ splitLine: { show: true, lineStyle: { color: ['#d6d6d6'] } }
169
+ splitArea: { show: false }
170
+ nameTextStyle: { backgroundColor: '#ffffff' }
171
+ timeAxis:
172
+ axisLine: { show: true, lineStyle: { color: '#717171' } }
173
+ axisTick: { show: true, lineStyle: { color: '#717171' }, length: 3 }
174
+ axisLabel: { show: true, color: '#717171' }
175
+ splitLine: { show: false, lineStyle: { color: ['#d6d6d6'] } }
176
+ splitArea: { show: false }
177
+ toolbox: { iconStyle: { borderColor: '#999' }, emphasis: { iconStyle: { borderColor: '#459cde' } } }
178
+ legend:
179
+ textStyle: { padding: [0,0,0,-7], color: '#717171' }, icon: 'circle'
180
+ pageIconColor: '#717171', pageIconSize: 12
181
+ pageTextStyle: { color: '#717171' }
182
+ pageButtonItemGap: -2, animationDurationUpdate: 300
183
+ tooltip:
184
+ axisPointer: { lineStyle: { color: '#ccc', width: 1 }, crossStyle: { color: '#ccc', width: 1 } }
185
+ borderRadius: 4, borderWidth: 1, borderColor: '#d6d6d6', backgroundColor: '#ffffff'
186
+ textStyle: { color: '#2c2c2c', fontSize: 12, fontWeight: 400 }, padding: 6
187
+ visualMap: { color: ['#c41621','#e39588','#f5ed98'] }
188
+ dataZoom:
189
+ type: 'slider', bottom: 10, height: 30, showDetail: false, handleSize: '80%'
190
+ borderColor: '#d6d6d6'
191
+ handleStyle: { borderColor: '#d6d6d6', color: '#d6d6d6' }
192
+ moveHandleStyle: { borderColor: '#d6d6d6', color: '#d6d6d6' }
193
+ emphasis: { handleStyle: { borderColor: '#d6d6d6', color: '#d6d6d6' }, moveHandleStyle: { borderColor: '#d6d6d6', color: '#d6d6d6' } }
194
+ markPoint: { label: { color: '#f2f2f2' }, emphasis: { label: { color: '#f2f2f2' } } }
195
+
196
+ # ── Shared option defaults ──
197
+
198
+ BASE :=
199
+ animationDuration: 500, animationDurationUpdate: 500
200
+ grid: { left: '0.8%', right: '3%', containLabel: true }
201
+ tooltip:
202
+ trigger: 'axis', show: true, confine: true
203
+ axisPointer: { type: 'shadow' }
204
+ extraCssText: 'box-shadow:0 3px 6px rgba(0,0,0,.15);box-shadow:0 2px 4px rgba(0,0,0,.12);z-index:1;font-feature-settings:"cv02","tnum";'
205
+ order: 'valueDesc'
206
+ legend: { show: true, type: 'scroll', padding: [0,0,0,0] }
207
+
208
+ XAXIS :=
209
+ type: 'category', splitLine: { show: false }, axisLine: { show: true }, axisTick: { show: false }
210
+ axisLabel: { show: true, hideOverlap: true, showMaxLabel: true, margin: 6 }, scale: true, z: 2
211
+
212
+ YAXIS :=
213
+ type: 'value', splitLine: { show: true }, axisLine: { show: false, onZero: false }, axisTick: { show: false }
214
+ axisLabel: { show: true, hideOverlap: true, margin: 4 }
215
+ nameLocation: 'end', nameTextStyle: { align: 'left', verticalAlign: 'top', padding: [0,5,0,0] }
216
+ nameGap: 6, scale: true, boundaryGap: ['0%','1%'], z: 2
217
+
218
+ COLORS := ['#236aa4','#45a1bf','#a5cdee','#8dacbf','#85c7c6','#d2c6ac','#f4b548','#8f3d56','#71b9f4','#46a485']
219
+
220
+ # ── Sample Data — ACME Corp FY 2026 ──
221
+
222
+ months := ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
223
+ quarters := ['Q1','Q2','Q3','Q4']
224
+ revenue := [420, 460, 490, 530, 560, 610, 640, 680, 720, 760, 810, 870]
225
+
226
+ products :=
227
+ 'Platform': [180,195,210,225,240,260,275,290,310,330,350,380]
228
+ 'Analytics': [120,130,135,145,150,165,170,180,190,200,215,225]
229
+ 'Integrations':[80, 90, 95,105,110,120,125,135,140,145,155,165]
230
+ 'Support': [40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90,100]
231
+
232
+ quarterlyRev := [1370, 1700, 2040, 2440]
233
+
234
+ productQuarterly :=
235
+ 'Platform': [585, 725, 875, 1060]
236
+ 'Analytics': [385, 460, 540, 640]
237
+ 'Integrations': [265, 335, 400, 465]
238
+ 'Support': [135, 180, 225, 275]
239
+
240
+ margin := [62, 64, 63, 65, 66, 65, 67, 68, 67, 69, 70, 71]
241
+ growthRate := [null, 9.5, 6.5, 8.2, 5.7, 8.9, 4.9, 6.3, 5.9, 5.6, 6.6, 7.4]
242
+
243
+ segments := [
244
+ { value: 1840, name: 'Enterprise' }
245
+ { value: 3200, name: 'Mid-Market' }
246
+ { value: 5100, name: 'SMB' }
247
+ { value: 1200, name: 'Startup' }
248
+ { value: 660, name: 'Free Tier' }
249
+ ]
250
+
251
+ deals := [
252
+ [15,82,12],[25,75,18],[35,68,22],[45,60,28],[55,55,15]
253
+ [20,78,20],[30,72,14],[40,63,24],[50,52,19],[60,45,16]
254
+ [18,80,10],[28,70,21],[38,65,26],[48,58,13],[58,48,17]
255
+ [22,76,16],[32,69,23],[42,61,20],[52,50,11],[62,42,14]
256
+ ]
257
+
258
+ days := ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']
259
+ hours := ['6a','7a','8a','9a','10a','11a','12p','1p','2p','3p','4p','5p','6p','7p','8p','9p']
260
+
261
+ heatData ~=
262
+ result = []
263
+ for d in [0...7]
264
+ for h in [0...16]
265
+ base = d < 5 ? 40 : 15
266
+ base += (d < 5 ? 50 : 10) if h >= 2 and h <= 10
267
+ base += (d < 5 ? 30 : 5) if h >= 4 and h <= 8
268
+ result.push [h, d, Math.round(base + Math.random() * 20)]
269
+ result
270
+
271
+ funnelData := [
272
+ { value: 12000, name: 'Leads' }
273
+ { value: 7200, name: 'Qualified' }
274
+ { value: 4100, name: 'Proposals' }
275
+ { value: 2400, name: 'Negotiation' }
276
+ { value: 1400, name: 'Closed Won' }
277
+ ]
278
+
279
+ sankeyNodes := [
280
+ { name: 'Organic Search' }, { name: 'Paid Search' }, { name: 'Social Media' }, { name: 'Referrals' }, { name: 'Direct' }
281
+ { name: 'Blog' }, { name: 'Landing Page' }, { name: 'Product Page' }, { name: 'Pricing Page' }
282
+ { name: 'Trial Signup' }, { name: 'Demo Request' }, { name: 'Converted' }, { name: 'Churned' }
283
+ ]
284
+
285
+ sankeyLinks := [
286
+ { source: 'Organic Search', target: 'Blog', value: 3200 }
287
+ { source: 'Organic Search', target: 'Product Page', value: 2100 }
288
+ { source: 'Paid Search', target: 'Landing Page', value: 2800 }
289
+ { source: 'Paid Search', target: 'Pricing Page', value: 1200 }
290
+ { source: 'Social Media', target: 'Blog', value: 1800 }
291
+ { source: 'Social Media', target: 'Landing Page', value: 900 }
292
+ { source: 'Referrals', target: 'Product Page', value: 1600 }
293
+ { source: 'Referrals', target: 'Pricing Page', value: 800 }
294
+ { source: 'Direct', target: 'Product Page', value: 1400 }
295
+ { source: 'Direct', target: 'Pricing Page', value: 600 }
296
+ { source: 'Blog', target: 'Trial Signup', value: 3100 }
297
+ { source: 'Blog', target: 'Demo Request', value: 1900 }
298
+ { source: 'Landing Page', target: 'Trial Signup', value: 2400 }
299
+ { source: 'Landing Page', target: 'Demo Request', value: 1300 }
300
+ { source: 'Product Page', target: 'Trial Signup', value: 2800 }
301
+ { source: 'Product Page', target: 'Demo Request', value: 2300 }
302
+ { source: 'Pricing Page', target: 'Trial Signup', value: 1500 }
303
+ { source: 'Pricing Page', target: 'Demo Request', value: 1100 }
304
+ { source: 'Trial Signup', target: 'Converted', value: 6200 }
305
+ { source: 'Trial Signup', target: 'Churned', value: 3600 }
306
+ { source: 'Demo Request', target: 'Converted', value: 4800 }
307
+ { source: 'Demo Request', target: 'Churned', value: 1800 }
308
+ ]
309
+
310
+ stateRevenue := [
311
+ { name: 'California', value: 9200 }, { name: 'New York', value: 7100 }, { name: 'Texas', value: 5800 }
312
+ { name: 'Florida', value: 4200 }, { name: 'Illinois', value: 3600 }, { name: 'Massachusetts', value: 3400 }
313
+ { name: 'Washington', value: 3100 }, { name: 'Pennsylvania', value: 2800 }, { name: 'Georgia', value: 2500 }
314
+ { name: 'Virginia', value: 2400 }, { name: 'New Jersey', value: 2200 }, { name: 'North Carolina', value: 2000 }
315
+ { name: 'Colorado', value: 1900 }, { name: 'Ohio', value: 1800 }, { name: 'Michigan', value: 1600 }
316
+ { name: 'Arizona', value: 1500 }, { name: 'Maryland', value: 1400 }, { name: 'Oregon', value: 1300 }
317
+ { name: 'Minnesota', value: 1200 }, { name: 'Connecticut', value: 1100 }, { name: 'Tennessee', value: 950 }
318
+ { name: 'Indiana', value: 850 }, { name: 'Missouri', value: 800 }, { name: 'Wisconsin', value: 750 }
319
+ { name: 'Utah', value: 700 }, { name: 'Nevada', value: 650 }, { name: 'South Carolina', value: 600 }
320
+ { name: 'Alabama', value: 500 }, { name: 'Kentucky', value: 480 }, { name: 'Louisiana', value: 460 }
321
+ { name: 'Oklahoma', value: 420 }, { name: 'Iowa', value: 400 }, { name: 'Kansas', value: 380 }
322
+ { name: 'Arkansas', value: 320 }, { name: 'Nebraska', value: 300 }, { name: 'Mississippi', value: 280 }
323
+ { name: 'New Mexico', value: 260 }, { name: 'Idaho', value: 240 }, { name: 'Hawaii', value: 220 }
324
+ { name: 'New Hampshire', value: 210 }, { name: 'Maine', value: 190 }, { name: 'Rhode Island', value: 180 }
325
+ { name: 'Delaware', value: 170 }, { name: 'Montana', value: 140 }, { name: 'Vermont', value: 130 }
326
+ { name: 'South Dakota', value: 110 }, { name: 'North Dakota', value: 100 }, { name: 'Alaska', value: 90 }
327
+ { name: 'Wyoming', value: 75 }, { name: 'West Virginia', value: 250 }, { name: 'District of Columbia', value: 1800 }
328
+ ]
329
+
330
+ clusterColors := ['#236aa4','#f4b548','#8f3d56','#46a485']
331
+ clusterNames := ['West Coast','Mountain & Central','South','Northeast']
332
+
333
+ customerLocations := [
334
+ [-122.42,37.77,0],[-118.24,34.05,0],[-122.33,47.61,0],[-121.89,37.34,0],[-117.16,32.72,0]
335
+ [-122.68,45.52,0],[-121.49,38.58,0],[-117.93,33.81,0],[-122.27,37.87,0],[-118.49,34.02,0]
336
+ [-119.70,36.75,0],[-122.03,36.97,0],[-117.39,33.95,0],[-121.94,37.35,0],[-118.16,33.77,0]
337
+ [-122.41,37.78,0],[-122.68,45.52,0],[-117.16,32.72,0],[-123.09,44.05,0],[-120.66,35.28,0]
338
+ [-118.40,33.94,0],[-122.33,47.61,0],[-121.74,36.68,0],[-116.54,33.83,0],[-122.02,37.55,0]
339
+ [-104.99,39.74,1],[-111.89,40.76,1],[-112.07,33.45,1],[-97.74,30.27,1],[-94.58,39.10,1]
340
+ [-96.80,32.78,1],[-95.37,29.76,1],[-87.63,41.88,1],[-93.27,44.98,1],[-90.20,38.63,1]
341
+ [-104.82,38.83,1],[-97.52,35.47,1],[-96.70,40.81,1],[-86.16,39.77,1],[-89.97,35.15,1]
342
+ [-105.94,35.69,1],[-106.65,35.08,1],[-111.83,33.42,1],[-104.99,39.74,1],[-95.99,36.15,1]
343
+ [-84.39,33.75,2],[-80.84,35.23,2],[-81.66,30.33,2],[-80.19,25.76,2],[-82.46,27.95,2]
344
+ [-78.64,35.78,2],[-86.78,36.16,2],[-90.07,29.95,2],[-84.51,38.05,2],[-77.44,37.54,2]
345
+ [-81.38,28.54,2],[-82.55,35.60,2],[-79.93,32.78,2],[-85.76,38.25,2],[-84.28,30.44,2]
346
+ [-80.24,25.79,2],[-81.69,41.50,2],[-76.61,39.29,2],[-77.03,38.90,2],[-83.05,42.33,2]
347
+ [-74.01,40.71,3],[-71.06,42.36,3],[-75.17,39.95,3],[-73.94,40.67,3],[-72.68,41.76,3]
348
+ [-73.76,42.65,3],[-71.41,41.82,3],[-76.15,43.05,3],[-70.26,43.66,3],[-73.79,42.66,3]
349
+ [-74.17,40.74,3],[-71.06,42.36,3],[-75.17,39.95,3],[-73.21,44.48,3],[-71.46,42.10,3]
350
+ [-73.94,40.78,3],[-74.01,40.71,3],[-72.92,41.31,3],[-75.52,39.68,3],[-71.80,42.27,3]
351
+ ]
352
+
353
+ revenueTree := [
354
+ { name: 'Platform', value: 4510, children: [
355
+ { name: 'Core Engine', value: 1800 }, { name: 'API Gateway', value: 1200 }
356
+ { name: 'Auth & SSO', value: 850 }, { name: 'Admin Console', value: 660 }
357
+ ]}
358
+ { name: 'Analytics', value: 2500, children: [
359
+ { name: 'Dashboards', value: 900 }, { name: 'Reports', value: 700 }
360
+ { name: 'Data Explorer', value: 550 }, { name: 'Alerts', value: 350 }
361
+ ]}
362
+ { name: 'Integrations', value: 1650, children: [
363
+ { name: 'CRM Sync', value: 600 }, { name: 'Slack & Teams', value: 450 }
364
+ { name: 'Webhooks', value: 350 }, { name: 'Custom API', value: 250 }
365
+ ]}
366
+ { name: 'Support', value: 890, children: [
367
+ { name: 'Live Chat', value: 380 }, { name: 'Knowledge Base', value: 280 }
368
+ { name: 'Ticketing', value: 230 }
369
+ ]}
370
+ ]
371
+
372
+ radarProducts :=
373
+ 'Platform': [92, 78, 95, 82, 88]
374
+ 'Analytics': [76, 85, 80, 74, 70]
375
+ 'Integrations': [65, 92, 72, 68, 60]
376
+ 'Support': [58, 60, 88, 90, 55]
377
+ radarMetrics := ['Revenue','Growth','Retention','NPS','Usage']
378
+
379
+ waterfallLabels := ['Q1 Base','New Customers','Upsells','Price Increase','Churn','Downgrades','Q2','New Customers','Upsells','Churn','Downgrades','Q3','New Customers','Upsells','Churn','Downgrades','Q4']
380
+ waterfallValues := [1370, 180, 220, 50, -80, -40, null, 200, 160, -60, -30, null, 240, 200, -70, -30, null]
381
+
382
+ boxplotData := [
383
+ [8, 18, 28, 42, 65]
384
+ [10, 22, 35, 52, 78]
385
+ [12, 25, 38, 55, 85]
386
+ [15, 30, 45, 62, 95]
387
+ ]
388
+
389
+ parallelData := [
390
+ ['Platform', 4510, 18.2, 95, 82, 88]
391
+ ['Analytics', 2500, 22.5, 80, 74, 70]
392
+ ['Integrations',1650, 15.8, 72, 68, 60]
393
+ ['Support', 890, 12.0, 88, 90, 55]
394
+ ]
395
+
396
+ calendarData ~=
397
+ result = []
398
+ start = new Date('2024-01-01')
399
+ for i in [0...366]
400
+ d = new Date(start)
401
+ d.setDate(d.getDate() + i)
402
+ break if d.getFullYear() > 2024
403
+ dow = d.getDay()
404
+ base = dow > 0 and dow < 6 ? 800 : 350
405
+ base += d.getMonth() * 25
406
+ base += Math.round(Math.random() * 300 - 100)
407
+ ds = d.toISOString().slice(0, 10)
408
+ result.push [ds, Math.max(50, base)]
409
+ result
410
+
411
+ histBins := ['0-50','50-100','100-150','150-200','200-300','300-500','500-1000','1000+']
412
+ histCounts := [1200, 3400, 5800, 4200, 2600, 1400, 600, 180]
413
+
414
+ features := ['Single Sign-On','API Access','Custom Reports','Slack Integration','Webhooks','Data Export','Role Permissions','Audit Log','2FA','White Label']
415
+ adoption := [94,87,76,72,65,61,58,52,48,34]
416
+
417
+ channels := ['Email','Live Chat','Phone','Social Media','Community Forum','In-App']
418
+ tickets := [3200, 2800, 1900, 1400, 1100, 2100]
419
+
420
+ cohortScatter ~=
421
+ result = []
422
+ for i in [0...40]
423
+ eng = 20 + Math.random() * 75
424
+ ret = 35 + eng * 0.65 + (Math.random() - 0.5) * 25
425
+ result.push [Math.round(eng * 10) / 10, Math.min(99, Math.max(30, Math.round(ret * 10) / 10))]
426
+ result.sort((a, b) -> a[0] - b[0])
427
+ result
428
+
429
+ regression ~=
430
+ n = cohortScatter.length
431
+ sx = cohortScatter.reduce(((s, d) -> s + d[0]), 0)
432
+ sy = cohortScatter.reduce(((s, d) -> s + d[1]), 0)
433
+ sxy = cohortScatter.reduce(((s, d) -> s + d[0] * d[1]), 0)
434
+ sx2 = cohortScatter.reduce(((s, d) -> s + d[0] * d[0]), 0)
435
+ slope = (n * sxy - sx * sy) / (n * sx2 - sx * sx)
436
+ intercept = (sy - slope * sx) / n
437
+ xMin = Math.min(...cohortScatter.map(-> it[0]))
438
+ xMax = Math.max(...cohortScatter.map(-> it[0]))
439
+ { slope, intercept, xMin, xMax }
440
+
441
+ kpis := [
442
+ { label: 'Monthly Revenue', value: '$870k', spark: [420,460,490,530,560,610,640,680,720,760,810,870], delta: '+7.4% vs. prior month', up: true }
443
+ { label: 'Total Customers', value: '12,000', spark: [8200,8600,9100,9400,9800,10200,10500,10900,11200,11500,11700,12000], delta: '+2.6% vs. prior month', up: true }
444
+ { label: 'Avg Deal Size', value: '$38k', spark: [32,30,34,33,36,35,37,36,38,37,39,38], delta: '-2.6% vs. prior month', up: false }
445
+ { label: 'Net Revenue Retention', value: '118%', spark: [108,110,112,111,114,113,115,114,116,117,117,118], delta: '+0.9% vs. prior month', up: true }
446
+ { label: 'Churn Rate', value: '1.8%', spark: [3.2,3.0,2.8,2.6,2.5,2.4,2.3,2.2,2.1,2.0,1.9,1.8], delta: '-5.3% vs. prior month', up: true }
447
+ ]
448
+
449
+ # ── Helpers ──
450
+
451
+ $k = (v) -> "$#{v}k"
452
+ pct = (v) -> "#{v}%"
453
+ $M = (v) -> "$#{(v / 1000).toFixed(1)}M"
454
+ valK = (p) -> "#{p.name}<br>$#{(p.value or 0).toLocaleString()}k"
455
+ tip = (fmt) -> { ...BASE.tooltip, trigger: 'item', formatter: fmt }
456
+
457
+ xaxis = (data = null, opts = {}) ->
458
+ if typeof data is 'object' and not Array.isArray(data) and data isnt null
459
+ opts = data
460
+ data = opts.data or null
461
+ label = if opts.axisLabel then { ...XAXIS.axisLabel, ...opts.axisLabel } else XAXIS.axisLabel
462
+ { ...XAXIS, data, ...opts, axisLabel: label }
463
+ yaxis = (name, fmt, opts = {}) ->
464
+ label = if opts.axisLabel then { ...YAXIS.axisLabel, formatter: fmt, ...opts.axisLabel } else { ...YAXIS.axisLabel, formatter: fmt }
465
+ { ...YAXIS, name, ...opts, axisLabel: label }
466
+
467
+ ic: (id, opts) ->
468
+ el = document.getElementById(id)
469
+ return unless el
470
+ c = echarts.init(el, 'evidence')
471
+ c.setOption({ ...BASE, ...opts })
472
+ window.addEventListener 'resize', -> c.resize({ animation: { duration: 500 } })
473
+ c
474
+
475
+ buildWaterfall: (labels, values) ->
476
+ base = []
477
+ inc = []
478
+ dec = []
479
+ running = 0
480
+ for i in [0...values.length]
481
+ v = values[i]
482
+ if v is null
483
+ base.push 0
484
+ inc.push running
485
+ dec.push 0
486
+ else if i is 0
487
+ running = v
488
+ base.push 0
489
+ inc.push v
490
+ dec.push 0
491
+ else if v >= 0
492
+ base.push running
493
+ inc.push v
494
+ dec.push 0
495
+ running += v
496
+ else
497
+ running += v
498
+ base.push running
499
+ inc.push 0
500
+ dec.push -v
501
+ { base, inc, dec }
502
+
503
+ makeGauge: (id, value, label, fmt, color, max) ->
504
+ @ic id,
505
+ tooltip: { show: false }
506
+ legend: { show: false }
507
+ series: [{
508
+ type: 'gauge', startAngle: 200, endAngle: -20, min: 0, max: max or 100
509
+ pointer: { show: false }
510
+ progress: { show: true, width: 16, roundCap: true, itemStyle: { color } }
511
+ axisLine: { lineStyle: { width: 16, color: [[1, '#e5e5e5']] } }
512
+ axisTick: { show: false }, splitLine: { show: false }, axisLabel: { show: false }, anchor: { show: false }
513
+ detail:
514
+ valueAnimation: true, fontSize: 32, fontWeight: 600, fontFamily: 'Inter'
515
+ color: '#060606', offsetCenter: [0, '10%'], formatter: fmt
516
+ title: { fontSize: 14, fontWeight: 500, color: '#717171', offsetCenter: [0, '60%'] }
517
+ data: [{ value, name: label }]
518
+ }]
519
+
520
+ # ── Chart initialization ──
521
+
522
+ initCoreCharts: ->
523
+ @ic 'line',
524
+ xAxis: xaxis months
525
+ yAxis: yaxis 'Revenue ($k)', $k
526
+ series: [{
527
+ type: 'line', symbol: 'circle', symbolSize: 0, smooth: false
528
+ lineStyle: { width: 2, type: 'solid' }
529
+ emphasis: { focus: 'series', lineStyle: { opacity: 1, width: 3 } }
530
+ data: revenue
531
+ }]
532
+
533
+ @ic 'area',
534
+ xAxis: xaxis months
535
+ yAxis: yaxis 'Revenue ($k)', $k
536
+ series: ({
537
+ type: 'line', name, data
538
+ symbol: 'circle', symbolSize: 0, smooth: false
539
+ lineStyle: { width: 1 }, areaStyle: { opacity: 0.45 }
540
+ stack: 'revenue', emphasis: { focus: 'series' }
541
+ } for name, data of products)
542
+
543
+ @ic 'bar',
544
+ xAxis: xaxis quarters
545
+ yAxis: yaxis 'Revenue ($k)', $M
546
+ series: [{ type: 'bar', barMaxWidth: 60, label: { fontSize: 11 }, data: quarterlyRev }]
547
+
548
+ @ic 'stackedBar',
549
+ xAxis: xaxis quarters
550
+ yAxis: yaxis 'Revenue ($k)', $k
551
+ series: ({ type: 'bar', name, data, stack: 'total', barMaxWidth: 60, emphasis: { focus: 'series' } } for name, data of productQuarterly)
552
+
553
+ @ic 'combo',
554
+ xAxis: xaxis months
555
+ yAxis: [
556
+ yaxis 'Revenue ($k)', $k
557
+ yaxis 'Margin %', pct, position: 'right', splitLine: { show: false }, min: 50, max: 80
558
+ ]
559
+ series: [
560
+ { type: 'bar', name: 'Revenue', data: revenue, barMaxWidth: 60, yAxisIndex: 0 }
561
+ { type: 'line', name: 'Gross Margin', data: margin, yAxisIndex: 1, symbol: 'circle', symbolSize: 0, lineStyle: { width: 2 }, emphasis: { focus: 'series', lineStyle: { opacity: 1, width: 3 } } }
562
+ ]
563
+
564
+ @ic 'multiAxis',
565
+ xAxis: xaxis months
566
+ yAxis: [
567
+ yaxis 'Revenue ($k)', $k
568
+ yaxis 'MoM Growth %', pct, position: 'right', splitLine: { show: false }
569
+ ]
570
+ series: [
571
+ { type: 'line', name: 'Revenue', data: revenue, symbol: 'circle', symbolSize: 0, lineStyle: { width: 2 }, emphasis: { focus: 'series', lineStyle: { opacity: 1, width: 3 } } }
572
+ { type: 'line', name: 'Growth %', data: growthRate, yAxisIndex: 1, symbol: 'circle', symbolSize: 6, lineStyle: { width: 2, type: 'dashed' }, emphasis: { focus: 'series', lineStyle: { opacity: 1, width: 3 } } }
573
+ ]
574
+
575
+ @ic 'pie',
576
+ tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }
577
+ series: [{
578
+ type: 'pie', radius: ['40%','70%'], center: ['50%','55%']
579
+ padAngle: 2, itemStyle: { borderRadius: 4 }, label: { fontSize: 12 }
580
+ data: segments
581
+ }]
582
+
583
+ @ic 'scatter',
584
+ xAxis: xaxis type: 'value', name: 'Avg Deal Size ($k)', axisLabel: { formatter: $k }, axisLine: { show: true }
585
+ yAxis: yaxis 'Close Rate %', pct
586
+ tooltip: tip (p) -> "Deal: $#{p.value[0]}k<br>Close Rate: #{p.value[1]}%<br>Deals: #{p.value[2]}"
587
+ series: [{
588
+ type: 'scatter', data: deals, symbolSize: (d) -> d[2] * 1.8
589
+ emphasis: { focus: 'self', itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.3)' } }
590
+ }]
591
+
592
+ @ic 'heatmap',
593
+ grid: { ...BASE.grid, top: '8%', bottom: '12%' }
594
+ xAxis: xaxis hours, splitArea: { show: false }
595
+ yAxis: { ...YAXIS, type: 'category', data: days, splitLine: { show: false }, splitArea: { show: false } }
596
+ tooltip: tip (p) -> "#{days[p.value[1]]} #{hours[p.value[0]]}<br>Sessions: <b>#{p.value[2]}</b>"
597
+ visualMap:
598
+ min: 10, max: 140, calculable: true, orient: 'horizontal', left: 'center', bottom: '0%'
599
+ inRange: { color: ['#a5cdee','#236aa4'] }
600
+ textStyle: { color: '#717171', fontSize: 11 }
601
+ series: [{ type: 'heatmap', data: heatData, label: { show: false }, emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.3)' } } }]
602
+
603
+ @ic 'funnel',
604
+ tooltip: { trigger: 'item', formatter: '{b}: {c}' }
605
+ series: [{
606
+ type: 'funnel', left: '10%', right: '10%', top: '12%', bottom: '8%', width: '80%'
607
+ sort: 'descending', gap: 4
608
+ label: { show: true, position: 'inside', fontSize: 13, color: '#fff' }
609
+ emphasis: { label: { fontSize: 15 } }
610
+ data: funnelData
611
+ }]
612
+
613
+ @ic 'sankey',
614
+ tooltip: { trigger: 'item', triggerOn: 'mousemove' }
615
+ series: [{
616
+ type: 'sankey', layout: 'none'
617
+ left: '3%', right: '10%', top: '8%', bottom: '8%'
618
+ nodeWidth: 20, nodeGap: 14, layoutIterations: 32
619
+ emphasis: { focus: 'adjacency' }
620
+ lineStyle: { color: 'gradient', curveness: 0.5 }
621
+ label: { fontSize: 12 }
622
+ data: sankeyNodes, links: sankeyLinks
623
+ }]
624
+
625
+ initMaps: ->
626
+ try
627
+ usaJson = fetch!('https://raw.githubusercontent.com/apache/echarts-examples/gh-pages/public/data/asset/geo/USA.json').json!
628
+ echarts.registerMap 'USA', usaJson,
629
+ 'Alaska': { left: -131, top: 25, width: 15 }
630
+ 'Hawaii': { left: -110, top: 28, width: 5 }
631
+ 'Puerto Rico': { left: -76, top: 26, width: 2 }
632
+
633
+ @ic 'choropleth',
634
+ tooltip: { trigger: 'item', formatter: (p) -> "#{p.name}<br>Revenue: $#{(p.value or 0).toLocaleString()}k" }
635
+ visualMap:
636
+ min: 50, max: 9500, text: ['$9.5M+','$50k']
637
+ inRange: { color: ['#a5cdee','#45a1bf','#236aa4'] }
638
+ calculable: true, orient: 'horizontal', left: 'center', bottom: '2%'
639
+ textStyle: { color: '#717171', fontSize: 11 }
640
+ series: [{
641
+ type: 'map', map: 'USA', roam: true
642
+ emphasis: { label: { show: true, fontSize: 11, color: '#060606' }, itemStyle: { areaColor: '#f4b548' } }
643
+ label: { show: false }
644
+ itemStyle: { areaColor: '#e5e5e5', borderColor: '#fff', borderWidth: 1 }
645
+ data: stateRevenue
646
+ }]
647
+
648
+ @ic 'scatterMap',
649
+ tooltip:
650
+ trigger: 'item'
651
+ formatter: (p) ->
652
+ return "#{clusterNames[p.value[2]]} cluster" if p.seriesType is 'scatter'
653
+ p.name
654
+ legend:
655
+ show: true, top: '2%'
656
+ data: clusterNames.map((n, i) -> { name: n, icon: 'circle', itemStyle: { color: clusterColors[i] } })
657
+ geo:
658
+ map: 'USA', roam: true
659
+ itemStyle: { areaColor: '#f0f0f0', borderColor: '#d6d6d6', borderWidth: 0.5 }
660
+ emphasis: { itemStyle: { areaColor: '#e5e5e5' }, label: { show: false } }
661
+ label: { show: false }
662
+ series: clusterNames.map((name, ci) -> {
663
+ type: 'scatter', name, coordinateSystem: 'geo'
664
+ data: customerLocations.filter((d) -> d[2] is ci).map((d) -> { value: [d[0], d[1], d[2]] })
665
+ symbolSize: 8
666
+ itemStyle: { color: clusterColors[ci], opacity: 0.8 }
667
+ emphasis: { itemStyle: { opacity: 1, shadowBlur: 5, shadowColor: 'rgba(0,0,0,0.3)' } }
668
+ })
669
+ catch
670
+ for id in ['choropleth', 'scatterMap']
671
+ el = document.getElementById(id)
672
+ el.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#717171;font-size:13px;">Map data could not be loaded (requires internet)</div>' if el
673
+
674
+ initHierarchicalCharts: ->
675
+ @ic 'treemap',
676
+ tooltip: { trigger: 'item', formatter: valK }
677
+ legend: { show: false }
678
+ series: [{
679
+ type: 'treemap', data: revenueTree
680
+ top: '4%', bottom: '2%', left: '2%', right: '2%'
681
+ roam: false, nodeClick: false, breadcrumb: { show: false }
682
+ label: { show: true, fontSize: 12, color: '#fff', fontWeight: 500 }
683
+ upperLabel: { show: true, height: 24, fontSize: 12, fontWeight: 600, color: '#fff' }
684
+ itemStyle: { borderColor: '#fff', borderWidth: 2, gapWidth: 2 }
685
+ levels: [
686
+ { itemStyle: { borderWidth: 3, gapWidth: 4, borderColor: '#fff' }, upperLabel: { show: true } }
687
+ { itemStyle: { borderWidth: 2, gapWidth: 2, borderColor: '#fff' }, colorSaturation: [0.3, 0.7] }
688
+ ]
689
+ }]
690
+
691
+ @ic 'sunburst',
692
+ tooltip: { trigger: 'item', formatter: valK }
693
+ legend: { show: false }
694
+ series: [{
695
+ type: 'sunburst', data: revenueTree, radius: ['15%','90%'], sort: null
696
+ emphasis: { focus: 'ancestor' }
697
+ itemStyle: { borderRadius: 4, borderWidth: 2, borderColor: '#fff' }
698
+ label: { fontSize: 11, minAngle: 15 }
699
+ levels: [
700
+ {}
701
+ { r0: '15%', r: '45%', label: { fontSize: 13, fontWeight: 500 } }
702
+ { r0: '45%', r: '90%', label: { fontSize: 10 } }
703
+ ]
704
+ }]
705
+
706
+ initAnalyticalCharts: ->
707
+ @ic 'radar',
708
+ tooltip: { trigger: 'item' }
709
+ radar:
710
+ indicator: radarMetrics.map((m) -> { name: m, max: 100 })
711
+ shape: 'polygon', splitNumber: 4
712
+ axisName: { color: '#717171', fontSize: 12 }
713
+ splitLine: { lineStyle: { color: '#e5e5e5' } }
714
+ splitArea: { areaStyle: { color: ['#fff','#fafafa'] } }
715
+ axisLine: { lineStyle: { color: '#e5e5e5' } }
716
+ series: [{
717
+ type: 'radar'
718
+ data: ({ name, value } for name, value of radarProducts)
719
+ areaStyle: { opacity: 0.15 }, lineStyle: { width: 2 }
720
+ symbol: 'circle', symbolSize: 5
721
+ }]
722
+
723
+ wf = @buildWaterfall(waterfallLabels, waterfallValues)
724
+ totals = [0, 6, 11, 16]
725
+ @ic 'waterfall',
726
+ xAxis: xaxis waterfallLabels, axisLabel: { rotate: 45, fontSize: 10 }
727
+ yAxis: yaxis 'Revenue ($k)', $k
728
+ legend: { show: false }
729
+ series: [
730
+ { type: 'bar', stack: 'wf', name: 'base', data: wf.base, barMaxWidth: 36, itemStyle: { color: 'transparent' }, emphasis: { itemStyle: { color: 'transparent' } } }
731
+ { type: 'bar', stack: 'wf', name: 'Increase', barMaxWidth: 36, data: wf.inc.map((v, i) -> { value: v, itemStyle: { color: totals.includes(i) ? '#236aa4' : '#46a485' } }), label: { show: true, position: 'top', fontSize: 10, color: '#717171', formatter: (p) -> v = waterfallValues[p.dataIndex]; return "$#{p.value}k" if v is null; return "+$#{v}k" if v > 0; '' } }
732
+ { type: 'bar', stack: 'wf', name: 'Decrease', data: wf.dec, barMaxWidth: 36, itemStyle: { color: '#8f3d56' }, label: { show: true, position: 'bottom', fontSize: 10, color: '#717171', formatter: (p) -> p.value > 0 ? "-$#{p.value}k" : '' } }
733
+ ]
734
+
735
+ @ic 'boxplot',
736
+ xAxis: xaxis quarters
737
+ yAxis: yaxis 'Deal Size ($k)', $k
738
+ tooltip: tip (p) -> "#{p.name}<br>Max: $#{p.value[5]}k<br>Q3: $#{p.value[4]}k<br>Median: $#{p.value[3]}k<br>Q1: $#{p.value[2]}k<br>Min: $#{p.value[1]}k"
739
+ series: [{
740
+ type: 'boxplot', data: boxplotData
741
+ itemStyle: { color: '#a5cdee', borderColor: '#236aa4', borderWidth: 1.5 }
742
+ emphasis: { itemStyle: { borderColor: '#060606', borderWidth: 2 } }
743
+ }]
744
+
745
+ @ic 'parallel',
746
+ tooltip: { show: false }
747
+ legend: { show: true, data: parallelData.map(-> it[0]) }
748
+ parallelAxis: [
749
+ { dim: 0, name: 'Product', type: 'category', data: parallelData.map(-> it[0]), nameLocation: 'start' }
750
+ { dim: 1, name: 'Revenue ($k)', type: 'value', nameLocation: 'end' }
751
+ { dim: 2, name: 'Growth %', type: 'value', nameLocation: 'end' }
752
+ { dim: 3, name: 'Retention %', type: 'value', min: 50, max: 100, nameLocation: 'end' }
753
+ { dim: 4, name: 'NPS', type: 'value', min: 50, max: 100, nameLocation: 'end' }
754
+ { dim: 5, name: 'Usage Score', type: 'value', min: 40, max: 100, nameLocation: 'end' }
755
+ ]
756
+ parallel:
757
+ left: '5%', right: '5%', top: '18%', bottom: '12%'
758
+ parallelAxisDefault:
759
+ nameTextStyle: { color: '#717171', fontSize: 11 }
760
+ axisLine: { lineStyle: { color: '#d6d6d6' } }
761
+ axisTick: { show: false }
762
+ axisLabel: { color: '#717171', fontSize: 10 }
763
+ splitLine: { show: false }
764
+ series: parallelData.map((d, i) -> {
765
+ type: 'parallel', name: d[0]
766
+ lineStyle: { width: 3, opacity: 0.7, color: COLORS[i] }
767
+ emphasis: { lineStyle: { width: 5, opacity: 1 } }
768
+ data: [d]
769
+ })
770
+
771
+ @ic 'calendar',
772
+ tooltip: { trigger: 'item', formatter: (p) -> "#{p.value[0]}<br>Logins: #{p.value[1].toLocaleString()}" }
773
+ legend: { show: false }
774
+ visualMap:
775
+ min: 200, max: 1400, calculable: true, orient: 'horizontal', left: 'center', bottom: '2%'
776
+ inRange: { color: ['#a5cdee','#45a1bf','#236aa4'] }
777
+ textStyle: { color: '#717171', fontSize: 11 }
778
+ calendar:
779
+ range: '2024', top: '12%', left: '8%', right: '4%', bottom: '18%'
780
+ cellSize: ['auto', 16]
781
+ itemStyle: { borderWidth: 2, borderColor: '#fff' }
782
+ splitLine: { lineStyle: { color: '#d6d6d6', width: 1 } }
783
+ yearLabel: { show: false }
784
+ monthLabel: { color: '#717171', fontSize: 11, nameMap: 'en' }
785
+ dayLabel: { color: '#717171', fontSize: 10, firstDay: 1, nameMap: ['','M','','W','','F',''] }
786
+ series: [{
787
+ type: 'heatmap', coordinateSystem: 'calendar', data: calendarData
788
+ emphasis: { itemStyle: { shadowBlur: 5, shadowColor: 'rgba(0,0,0,0.3)' } }
789
+ }]
790
+
791
+ initAdditionalCharts: ->
792
+ @ic 'histogram',
793
+ xAxis: xaxis histBins, name: 'Response Time (ms)', axisLabel: { fontSize: 10 }
794
+ yAxis: yaxis 'Request Count', (v) -> v >= 1000 ? "#{v / 1000}k" : v
795
+ legend: { show: false }
796
+ series: [{
797
+ type: 'bar', barWidth: '90%'
798
+ data: histCounts.map((v, i) -> { value: v, itemStyle: { color: i <= 2 ? '#46a485' : i <= 4 ? '#f4b548' : '#8f3d56' } })
799
+ emphasis: { itemStyle: { opacity: 0.85 } }
800
+ }]
801
+
802
+ @ic 'horizBar',
803
+ grid: { ...BASE.grid, left: '2%' }
804
+ xAxis: yaxis 'Adoption %', pct, max: 100
805
+ yAxis: xaxis features.slice().reverse(), type: 'category', axisLabel: { fontSize: 11 }, inverse: false
806
+ legend: { show: false }
807
+ tooltip: tip (p) -> "#{p.name}: #{p.value}%"
808
+ series: [{
809
+ type: 'bar', data: adoption.slice().reverse(), barMaxWidth: 20
810
+ itemStyle: { borderRadius: [0,3,3,0] }
811
+ label: { show: true, position: 'right', fontSize: 11, color: '#717171', formatter: (p) -> "#{p.value}%" }
812
+ emphasis: { itemStyle: { opacity: 0.85 } }
813
+ }]
814
+
815
+ @ic 'polar',
816
+ tooltip: { trigger: 'item', formatter: '{b}: {c} tickets' }
817
+ legend: { show: true, type: 'scroll', bottom: '2%' }
818
+ angleAxis:
819
+ type: 'category', data: channels, startAngle: 90
820
+ axisLine: { lineStyle: { color: '#d6d6d6' } }
821
+ axisLabel: { color: '#717171', fontSize: 11 }
822
+ radiusAxis:
823
+ max: 3500, axisLine: { show: false }, axisTick: { show: false }
824
+ axisLabel: { show: false }
825
+ splitLine: { lineStyle: { color: '#e5e5e5' } }
826
+ polar: { radius: ['10%','75%'] }
827
+ series: [{
828
+ type: 'bar', coordinateSystem: 'polar', roundCap: true
829
+ data: tickets.map((v, i) -> { value: v, itemStyle: { color: COLORS[i] } })
830
+ emphasis: { focus: 'self' }, label: { show: false }
831
+ }]
832
+
833
+ r = regression
834
+ @ic 'scatterReg',
835
+ xAxis: xaxis type: 'value', name: 'Engagement Score', axisLine: { show: true }
836
+ yAxis: yaxis 'Retention %', pct
837
+ tooltip: tip (p) -> p.seriesType is 'scatter' ? "Engagement: #{p.value[0]}<br>Retention: #{p.value[1]}%" : 'Trend line'
838
+ legend: { show: false }
839
+ series: [
840
+ { type: 'scatter', data: cohortScatter, symbolSize: 9, itemStyle: { color: '#236aa4', opacity: 0.7 }, emphasis: { itemStyle: { opacity: 1, shadowBlur: 8, shadowColor: 'rgba(0,0,0,0.25)' } } }
841
+ { type: 'line', data: [[r.xMin, r.slope * r.xMin + r.intercept], [r.xMax, r.slope * r.xMax + r.intercept]], symbol: 'none', lineStyle: { width: 2, type: 'dashed', color: '#8f3d56' }, tooltip: { show: false } }
842
+ ]
843
+
844
+ # ── Mount ──
845
+
846
+ ~>
847
+ requestAnimationFrame ->
848
+ echarts.registerTheme('evidence', THEME)
849
+ @initCoreCharts()
850
+ @initMaps()
851
+ @makeGauge('gauge1', 72.4, 'NPS Score', '{value}', '#236aa4', 100)
852
+ @makeGauge('gauge2', 94.2, 'CSAT', '{value}%', '#46a485', 100)
853
+ @makeGauge('gauge3', 99.8, 'Uptime', '{value}%', '#45a1bf', 100)
854
+ @initHierarchicalCharts()
855
+ @initAnalyticalCharts()
856
+ @initAdditionalCharts()
857
+
858
+ # ── Render ──
859
+
860
+ render
861
+ h1 "ACME Corp Dashboard"
862
+ p class: 'subtitle', "FY 2026 SaaS Metrics \u2014 styled with the evidence.dev ECharts theme"
863
+
864
+ .kpi-row
865
+ for kpi in kpis
866
+ KpiCard
867
+ label: kpi.label, value: kpi.value
868
+ spark: kpi.spark, delta: kpi.delta, up: kpi.up
869
+
870
+ p class: 'section', "Core Charts"
871
+ .grid
872
+ ChartCard title: 'Monthly Revenue', subtitle: 'Total recurring revenue by month', chartId: 'line'
873
+ ChartCard title: 'Revenue by Product', subtitle: 'Stacked area breakdown across product lines', chartId: 'area'
874
+ ChartCard title: 'Quarterly Revenue', subtitle: 'Quarter-over-quarter comparison', chartId: 'bar'
875
+ ChartCard title: 'Revenue by Product \u00d7 Quarter', subtitle: 'Stacked bar with product breakdown', chartId: 'stackedBar'
876
+ ChartCard title: 'Revenue & Margin', subtitle: 'Revenue bars with gross margin % line overlay', chartId: 'combo'
877
+ ChartCard title: 'Revenue & Growth Rate', subtitle: 'Dual-axis: revenue (left) and YoY growth (right)', chartId: 'multiAxis'
878
+ ChartCard title: 'Customers by Segment', subtitle: 'Distribution across customer tiers', chartId: 'pie'
879
+ ChartCard title: 'Deal Size vs. Close Rate', subtitle: 'Each bubble represents a sales rep', chartId: 'scatter'
880
+ ChartCard title: 'Feature Usage Heatmap', subtitle: 'Active sessions by day of week and hour', chartId: 'heatmap'
881
+ ChartCard title: 'Sales Pipeline', subtitle: 'Conversion through funnel stages', chartId: 'funnel'
882
+ ChartCard title: 'Customer Acquisition Flow', subtitle: 'Channel attribution from source to conversion', chartId: 'sankey', wide: true
883
+
884
+ p class: 'section', "Geographic"
885
+ .grid
886
+ ChartCard title: 'Revenue by State', subtitle: 'Annual revenue across US states', chartId: 'choropleth', tall: true
887
+ ChartCard title: 'Customer Locations', subtitle: 'Purchase origins clustered by region (k-means)', chartId: 'scatterMap', tall: true
888
+
889
+ p class: 'section', "KPI Gauges"
890
+ .grid
891
+ div class: 'card wide'
892
+ h2 "Key Performance Indicators"
893
+ p "Net Promoter Score, Customer Satisfaction, and Platform Uptime"
894
+ .gauge-row
895
+ div id: 'gauge1', class: 'gauge-cell'
896
+ div id: 'gauge2', class: 'gauge-cell'
897
+ div id: 'gauge3', class: 'gauge-cell'
898
+
899
+ p class: 'section', "Hierarchical"
900
+ .grid
901
+ ChartCard title: 'Revenue Breakdown', subtitle: 'Treemap by division, product, and feature', chartId: 'treemap'
902
+ ChartCard title: 'Revenue Composition', subtitle: 'Sunburst showing contribution at each level', chartId: 'sunburst'
903
+
904
+ p class: 'section', "Analytical"
905
+ .grid
906
+ ChartCard title: 'Product Comparison', subtitle: 'Multi-metric radar across product lines', chartId: 'radar'
907
+ ChartCard title: 'Revenue Bridge', subtitle: 'Waterfall from Q1 baseline through Q4', chartId: 'waterfall'
908
+ ChartCard title: 'Deal Size Distribution', subtitle: 'Boxplot of closed deal values by quarter ($k)', chartId: 'boxplot'
909
+ ChartCard title: 'Product Metrics', subtitle: 'Parallel coordinates across key dimensions', chartId: 'parallel'
910
+ ChartCard title: 'Daily Platform Activity', subtitle: 'Login volume over the past year', chartId: 'calendar', wide: true
911
+
912
+ p class: 'section', "Additional Chart Types"
913
+ .grid
914
+ ChartCard title: 'Response Time Distribution', subtitle: 'Histogram of API response times (ms)', chartId: 'histogram'
915
+ ChartCard title: 'Feature Adoption', subtitle: 'Horizontal bar chart of feature usage rates', chartId: 'horizBar'
916
+ ChartCard title: 'Support Volume by Channel', subtitle: 'Polar area chart of ticket counts', chartId: 'polar'
917
+ ChartCard title: 'Engagement vs. Retention', subtitle: 'Scatter with regression trend by cohort', chartId: 'scatterReg'
918
+ </script>
919
+
920
+ </body>
921
+ </html>