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,1228 @@
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</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
+ <style>
12
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
13
+ body {
14
+ font-family: 'Inter', sans-serif;
15
+ font-feature-settings: "cv02", "tnum";
16
+ background: #f7f7f7;
17
+ color: #2c2c2c;
18
+ padding: 32px;
19
+ }
20
+ h1 {
21
+ font-size: 24px;
22
+ font-weight: 600;
23
+ color: #060606;
24
+ margin-bottom: 4px;
25
+ }
26
+ .subtitle {
27
+ font-size: 14px;
28
+ color: #717171;
29
+ margin-bottom: 28px;
30
+ }
31
+ .section {
32
+ font-size: 13px;
33
+ font-weight: 500;
34
+ color: #717171;
35
+ text-transform: uppercase;
36
+ letter-spacing: 0.05em;
37
+ margin: 36px 0 16px;
38
+ padding-bottom: 8px;
39
+ border-bottom: 1px solid #e5e5e5;
40
+ }
41
+ .section:first-of-type { margin-top: 0; }
42
+ .grid {
43
+ display: grid;
44
+ grid-template-columns: repeat(auto-fit, minmax(540px, 1fr));
45
+ gap: 20px;
46
+ }
47
+ .card {
48
+ background: #ffffff;
49
+ border-radius: 8px;
50
+ border: 1px solid #e5e5e5;
51
+ padding: 20px 20px 12px;
52
+ }
53
+ .card h2 {
54
+ font-size: 15px;
55
+ font-weight: 500;
56
+ color: #060606;
57
+ margin-bottom: 2px;
58
+ }
59
+ .card p {
60
+ font-size: 12px;
61
+ color: #717171;
62
+ margin-bottom: 12px;
63
+ }
64
+ .chart { width: 100%; height: 380px; }
65
+ .chart-tall { width: 100%; height: 440px; }
66
+ .wide { grid-column: 1 / -1; }
67
+ .gauge-row {
68
+ display: flex;
69
+ gap: 0;
70
+ }
71
+ .gauge-cell {
72
+ flex: 1;
73
+ height: 300px;
74
+ }
75
+ .kpi-row {
76
+ display: grid;
77
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
78
+ gap: 16px;
79
+ margin-bottom: 8px;
80
+ }
81
+ .kpi {
82
+ background: #fff;
83
+ border: 1px solid #e5e5e5;
84
+ border-radius: 8px;
85
+ padding: 16px 20px;
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: 4px;
89
+ }
90
+ .kpi-label {
91
+ font-size: 12px;
92
+ color: #717171;
93
+ font-weight: 500;
94
+ }
95
+ .kpi-row-inner {
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 10px;
99
+ }
100
+ .kpi-value {
101
+ font-size: 26px;
102
+ font-weight: 600;
103
+ color: #060606;
104
+ line-height: 1.1;
105
+ }
106
+ .kpi-spark {
107
+ flex-shrink: 0;
108
+ }
109
+ .kpi-delta {
110
+ font-size: 12px;
111
+ font-weight: 500;
112
+ }
113
+ .kpi-delta.up { color: #16a34a; }
114
+ .kpi-delta.down { color: #dc2626; }
115
+ </style>
116
+ </head>
117
+ <body>
118
+
119
+ <h1>ACME Corp Dashboard</h1>
120
+ <p class="subtitle">FY 2025 SaaS Metrics &mdash; styled with the evidence.dev ECharts theme</p>
121
+
122
+ <div class="kpi-row" id="kpiRow"></div>
123
+
124
+ <p class="section">Core Charts</p>
125
+ <div class="grid">
126
+ <div class="card">
127
+ <h2>Monthly Revenue</h2>
128
+ <p>Total recurring revenue by month</p>
129
+ <div id="line" class="chart"></div>
130
+ </div>
131
+ <div class="card">
132
+ <h2>Revenue by Product</h2>
133
+ <p>Stacked area breakdown across product lines</p>
134
+ <div id="area" class="chart"></div>
135
+ </div>
136
+ <div class="card">
137
+ <h2>Quarterly Revenue</h2>
138
+ <p>Quarter-over-quarter comparison</p>
139
+ <div id="bar" class="chart"></div>
140
+ </div>
141
+ <div class="card">
142
+ <h2>Revenue by Product &times; Quarter</h2>
143
+ <p>Stacked bar with product breakdown</p>
144
+ <div id="stackedBar" class="chart"></div>
145
+ </div>
146
+ <div class="card">
147
+ <h2>Revenue &amp; Margin</h2>
148
+ <p>Revenue bars with gross margin % line overlay</p>
149
+ <div id="combo" class="chart"></div>
150
+ </div>
151
+ <div class="card">
152
+ <h2>Revenue &amp; Growth Rate</h2>
153
+ <p>Dual-axis: revenue (left) and YoY growth (right)</p>
154
+ <div id="multiAxis" class="chart"></div>
155
+ </div>
156
+ <div class="card">
157
+ <h2>Customers by Segment</h2>
158
+ <p>Distribution across customer tiers</p>
159
+ <div id="pie" class="chart"></div>
160
+ </div>
161
+ <div class="card">
162
+ <h2>Deal Size vs. Close Rate</h2>
163
+ <p>Each bubble represents a sales rep</p>
164
+ <div id="scatter" class="chart"></div>
165
+ </div>
166
+ <div class="card">
167
+ <h2>Feature Usage Heatmap</h2>
168
+ <p>Active sessions by day of week and hour</p>
169
+ <div id="heatmap" class="chart"></div>
170
+ </div>
171
+ <div class="card">
172
+ <h2>Sales Pipeline</h2>
173
+ <p>Conversion through funnel stages</p>
174
+ <div id="funnel" class="chart"></div>
175
+ </div>
176
+ <div class="card wide">
177
+ <h2>Customer Acquisition Flow</h2>
178
+ <p>Channel attribution from source to conversion</p>
179
+ <div id="sankey" class="chart"></div>
180
+ </div>
181
+ </div>
182
+
183
+ <p class="section">Geographic</p>
184
+ <div class="grid">
185
+ <div class="card">
186
+ <h2>Revenue by State</h2>
187
+ <p>Annual revenue across US states</p>
188
+ <div id="choropleth" class="chart-tall"></div>
189
+ </div>
190
+ <div class="card">
191
+ <h2>Customer Locations</h2>
192
+ <p>Purchase origins clustered by region (k-means)</p>
193
+ <div id="scatterMap" class="chart-tall"></div>
194
+ </div>
195
+ </div>
196
+
197
+ <p class="section">KPI Gauges</p>
198
+ <div class="grid">
199
+ <div class="card wide">
200
+ <h2>Key Performance Indicators</h2>
201
+ <p>Net Promoter Score, Customer Satisfaction, and Platform Uptime</p>
202
+ <div class="gauge-row">
203
+ <div id="gauge1" class="gauge-cell"></div>
204
+ <div id="gauge2" class="gauge-cell"></div>
205
+ <div id="gauge3" class="gauge-cell"></div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+
210
+ <p class="section">Hierarchical</p>
211
+ <div class="grid">
212
+ <div class="card">
213
+ <h2>Revenue Breakdown</h2>
214
+ <p>Treemap by division, product, and feature</p>
215
+ <div id="treemap" class="chart"></div>
216
+ </div>
217
+ <div class="card">
218
+ <h2>Revenue Composition</h2>
219
+ <p>Sunburst showing contribution at each level</p>
220
+ <div id="sunburst" class="chart"></div>
221
+ </div>
222
+ </div>
223
+
224
+ <p class="section">Analytical</p>
225
+ <div class="grid">
226
+ <div class="card">
227
+ <h2>Product Comparison</h2>
228
+ <p>Multi-metric radar across product lines</p>
229
+ <div id="radar" class="chart"></div>
230
+ </div>
231
+ <div class="card">
232
+ <h2>Revenue Bridge</h2>
233
+ <p>Waterfall from Q1 baseline through Q4</p>
234
+ <div id="waterfall" class="chart"></div>
235
+ </div>
236
+ <div class="card">
237
+ <h2>Deal Size Distribution</h2>
238
+ <p>Boxplot of closed deal values by quarter ($k)</p>
239
+ <div id="boxplot" class="chart"></div>
240
+ </div>
241
+ <div class="card">
242
+ <h2>Product Metrics</h2>
243
+ <p>Parallel coordinates across key dimensions</p>
244
+ <div id="parallel" class="chart"></div>
245
+ </div>
246
+ <div class="card wide">
247
+ <h2>Daily Platform Activity</h2>
248
+ <p>Login volume over the past year</p>
249
+ <div id="calendar" class="chart"></div>
250
+ </div>
251
+ </div>
252
+
253
+ <p class="section">Additional Chart Types</p>
254
+ <div class="grid">
255
+ <div class="card">
256
+ <h2>Response Time Distribution</h2>
257
+ <p>Histogram of API response times (ms)</p>
258
+ <div id="histogram" class="chart"></div>
259
+ </div>
260
+ <div class="card">
261
+ <h2>Feature Adoption</h2>
262
+ <p>Horizontal bar chart of feature usage rates</p>
263
+ <div id="horizBar" class="chart"></div>
264
+ </div>
265
+ <div class="card">
266
+ <h2>Support Volume by Channel</h2>
267
+ <p>Polar area chart of ticket counts</p>
268
+ <div id="polar" class="chart"></div>
269
+ </div>
270
+ <div class="card">
271
+ <h2>Engagement vs. Retention</h2>
272
+ <p>Scatter with regression trend by cohort</p>
273
+ <div id="scatterReg" class="chart"></div>
274
+ </div>
275
+ </div>
276
+
277
+ <script>
278
+ // ===================================================================
279
+ // Theme (from evidence.dev — pass directly to echarts.registerTheme)
280
+ // ===================================================================
281
+ const THEME = {
282
+ darkMode: false,
283
+ backgroundColor: "#ffffff",
284
+ textStyle: { fontFamily: ["Inter", "sans-serif"] },
285
+ color: ["#236aa4","#45a1bf","#a5cdee","#8dacbf","#85c7c6","#d2c6ac","#f4b548","#8f3d56","#71b9f4","#46a485"],
286
+ grid: { left:"1%", right:"4%", bottom:"0%", top:"15%", containLabel:true },
287
+ title: {
288
+ padding:0, itemGap:7,
289
+ textStyle: { fontSize:14, color:"#060606" },
290
+ subtextStyle: { fontSize:13, color:"#717171", overflow:"break" },
291
+ top: "1px"
292
+ },
293
+ line: { itemStyle:{borderWidth:0}, lineStyle:{width:2,join:"round"}, symbolSize:0, symbol:"circle", smooth:false },
294
+ radar: { itemStyle:{borderWidth:0}, lineStyle:{width:2}, symbolSize:0, symbol:"circle", smooth:false },
295
+ pie: { itemStyle:{borderWidth:0, borderColor:"#cccccc"} },
296
+ scatter: { itemStyle:{borderWidth:0, borderColor:"#cccccc"} },
297
+ boxplot: { itemStyle:{borderWidth:1.5} },
298
+ parallel:{ itemStyle:{borderWidth:0, borderColor:"#cccccc"} },
299
+ sankey: { itemStyle:{borderWidth:0, borderColor:"#cccccc"} },
300
+ funnel: { itemStyle:{borderWidth:0, borderColor:"#cccccc"} },
301
+ gauge: { itemStyle:{borderWidth:0, borderColor:"#cccccc"} },
302
+ candlestick: { itemStyle:{ color:"#eb5454", color0:"#47b262", borderColor:"#eb5454", borderColor0:"#47b262", borderWidth:1 } },
303
+ graph: {
304
+ itemStyle:{borderWidth:0, borderColor:"#cccccc"}, lineStyle:{width:1,color:"#aaaaaa"},
305
+ symbolSize:0, symbol:"circle", smooth:false,
306
+ color:["#923d59","#488f96","#518eca","#b3a9a0","#ffc857","#495867","#bfdbf7","#bc4749","#eeebd0"],
307
+ label:{color:"#f2f2f2"}
308
+ },
309
+ categoryAxis: {
310
+ axisLine:{show:true, lineStyle:{color:"#717171"}},
311
+ axisTick:{show:false, lineStyle:{color:"#717171"}, length:3, alignWithLabel:true},
312
+ axisLabel:{show:true, color:"#717171"},
313
+ splitLine:{show:false, lineStyle:{color:["#d6d6d6"]}},
314
+ splitArea:{show:false}
315
+ },
316
+ valueAxis: {
317
+ axisLine:{show:false, lineStyle:{color:"#717171"}},
318
+ axisTick:{show:false, lineStyle:{color:"#717171"}, length:2},
319
+ axisLabel:{show:true, color:"#717171"},
320
+ splitLine:{show:true, lineStyle:{color:["#d6d6d6"], width:1}},
321
+ splitArea:{show:false},
322
+ nameTextStyle:{backgroundColor:"#ffffff"}
323
+ },
324
+ logAxis: {
325
+ axisLine:{show:false, lineStyle:{color:"#717171"}},
326
+ axisTick:{show:false, lineStyle:{color:"#717171"}, length:2},
327
+ axisLabel:{show:true, color:"#717171"},
328
+ splitLine:{show:true, lineStyle:{color:["#d6d6d6"]}},
329
+ splitArea:{show:false},
330
+ nameTextStyle:{backgroundColor:"#ffffff"}
331
+ },
332
+ timeAxis: {
333
+ axisLine:{show:true, lineStyle:{color:"#717171"}},
334
+ axisTick:{show:true, lineStyle:{color:"#717171"}, length:3},
335
+ axisLabel:{show:true, color:"#717171"},
336
+ splitLine:{show:false, lineStyle:{color:["#d6d6d6"]}},
337
+ splitArea:{show:false}
338
+ },
339
+ toolbox: { iconStyle:{borderColor:"#999"}, emphasis:{iconStyle:{borderColor:"#459cde"}} },
340
+ legend: {
341
+ textStyle:{padding:[0,0,0,-7], color:"#717171"}, icon:"circle",
342
+ pageIconColor:"#717171", pageIconSize:12, pageTextStyle:{color:"#717171"},
343
+ pageButtonItemGap:-2, animationDurationUpdate:300
344
+ },
345
+ tooltip: {
346
+ axisPointer:{ lineStyle:{color:"#ccc",width:1}, crossStyle:{color:"#ccc",width:1} },
347
+ borderRadius:4, borderWidth:1, borderColor:"#d6d6d6", backgroundColor:"#ffffff",
348
+ textStyle:{color:"#2c2c2c", fontSize:12, fontWeight:400}, padding:6
349
+ },
350
+ visualMap: { color:["#c41621","#e39588","#f5ed98"] },
351
+ dataZoom: {
352
+ type:"slider", bottom:10, height:30, showDetail:false, handleSize:"80%",
353
+ borderColor:"#d6d6d6",
354
+ handleStyle:{borderColor:"#d6d6d6",color:"#d6d6d6"},
355
+ moveHandleStyle:{borderColor:"#d6d6d6",color:"#d6d6d6"},
356
+ emphasis:{ handleStyle:{borderColor:"#d6d6d6",color:"#d6d6d6"}, moveHandleStyle:{borderColor:"#d6d6d6",color:"#d6d6d6"} }
357
+ },
358
+ markPoint: { label:{color:"#f2f2f2"}, emphasis:{label:{color:"#f2f2f2"}} }
359
+ };
360
+
361
+ echarts.registerTheme('evidence', THEME);
362
+
363
+ // ===================================================================
364
+ // Shared option defaults (from evidence's _Chart.svelte)
365
+ // ===================================================================
366
+ const BASE = {
367
+ animationDuration: 500,
368
+ animationDurationUpdate: 500,
369
+ grid: { left:"0.8%", right:"3%", containLabel:true },
370
+ tooltip: {
371
+ trigger:"axis", show:true, confine:true,
372
+ axisPointer:{type:"shadow"},
373
+ 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";',
374
+ order:"valueDesc"
375
+ },
376
+ legend: { show:true, type:"scroll", padding:[0,0,0,0] }
377
+ };
378
+
379
+ const XAXIS = {
380
+ type:"category", splitLine:{show:false}, axisLine:{show:true}, axisTick:{show:false},
381
+ axisLabel:{show:true, hideOverlap:true, showMaxLabel:true, margin:6}, scale:true, z:2
382
+ };
383
+
384
+ const YAXIS = {
385
+ type:"value", splitLine:{show:true}, axisLine:{show:false,onZero:false}, axisTick:{show:false},
386
+ axisLabel:{show:true, hideOverlap:true, margin:4},
387
+ nameLocation:"end", nameTextStyle:{align:"left",verticalAlign:"top",padding:[0,5,0,0]},
388
+ nameGap:6, scale:true, boundaryGap:["0%","1%"], z:2
389
+ };
390
+
391
+ const COLORS = THEME.color;
392
+
393
+ function init(id, opts) {
394
+ const el = document.getElementById(id);
395
+ if (!el) return null;
396
+ const c = echarts.init(el, 'evidence');
397
+ c.setOption({ ...BASE, ...opts });
398
+ window.addEventListener('resize', () => c.resize({ animation: { duration: 500 } }));
399
+ return c;
400
+ }
401
+
402
+ // ===================================================================
403
+ // Sample Data — ACME Corp FY 2025
404
+ // ===================================================================
405
+ const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
406
+ const quarters = ['Q1','Q2','Q3','Q4'];
407
+
408
+ const revenue = [420, 460, 490, 530, 560, 610, 640, 680, 720, 760, 810, 870];
409
+
410
+ const products = {
411
+ 'Platform': [180,195,210,225,240,260,275,290,310,330,350,380],
412
+ 'Analytics': [120,130,135,145,150,165,170,180,190,200,215,225],
413
+ 'Integrations':[80, 90, 95,105,110,120,125,135,140,145,155,165],
414
+ 'Support': [40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90,100]
415
+ };
416
+
417
+ const quarterlyRev = [1370, 1700, 2040, 2440];
418
+ const productQuarterly = {
419
+ 'Platform': [585, 725, 875, 1060],
420
+ 'Analytics': [385, 460, 540, 640],
421
+ 'Integrations':[265, 335, 400, 465],
422
+ 'Support': [135, 180, 225, 275]
423
+ };
424
+
425
+ const margin = [62, 64, 63, 65, 66, 65, 67, 68, 67, 69, 70, 71];
426
+ const growthRate = [null, 9.5, 6.5, 8.2, 5.7, 8.9, 4.9, 6.3, 5.9, 5.6, 6.6, 7.4];
427
+
428
+ const segments = [
429
+ {value:1840, name:'Enterprise'},
430
+ {value:3200, name:'Mid-Market'},
431
+ {value:5100, name:'SMB'},
432
+ {value:1200, name:'Startup'},
433
+ {value:660, name:'Free Tier'}
434
+ ];
435
+
436
+ const deals = [
437
+ [15,82,12],[25,75,18],[35,68,22],[45,60,28],[55,55,15],
438
+ [20,78,20],[30,72,14],[40,63,24],[50,52,19],[60,45,16],
439
+ [18,80,10],[28,70,21],[38,65,26],[48,58,13],[58,48,17],
440
+ [22,76,16],[32,69,23],[42,61,20],[52,50,11],[62,42,14]
441
+ ];
442
+
443
+ const days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
444
+ const hours = ['6a','7a','8a','9a','10a','11a','12p','1p','2p','3p','4p','5p','6p','7p','8p','9p'];
445
+ const heatData = [];
446
+ for (let d = 0; d < 7; d++) {
447
+ for (let h = 0; h < 16; h++) {
448
+ let base = (d < 5) ? 40 : 15;
449
+ if (h >= 2 && h <= 10) base += (d < 5) ? 50 : 10;
450
+ if (h >= 4 && h <= 8) base += (d < 5) ? 30 : 5;
451
+ heatData.push([h, d, Math.round(base + Math.random() * 20)]);
452
+ }
453
+ }
454
+
455
+ const funnelData = [
456
+ {value:12000, name:'Leads'},
457
+ {value:7200, name:'Qualified'},
458
+ {value:4100, name:'Proposals'},
459
+ {value:2400, name:'Negotiation'},
460
+ {value:1400, name:'Closed Won'}
461
+ ];
462
+
463
+ const sankeyNodes = [
464
+ {name:'Organic Search'},{name:'Paid Search'},{name:'Social Media'},{name:'Referrals'},{name:'Direct'},
465
+ {name:'Blog'},{name:'Landing Page'},{name:'Product Page'},{name:'Pricing Page'},
466
+ {name:'Trial Signup'},{name:'Demo Request'},{name:'Converted'},{name:'Churned'}
467
+ ];
468
+ const sankeyLinks = [
469
+ {source:'Organic Search',target:'Blog',value:3200},
470
+ {source:'Organic Search',target:'Product Page',value:2100},
471
+ {source:'Paid Search',target:'Landing Page',value:2800},
472
+ {source:'Paid Search',target:'Pricing Page',value:1200},
473
+ {source:'Social Media',target:'Blog',value:1800},
474
+ {source:'Social Media',target:'Landing Page',value:900},
475
+ {source:'Referrals',target:'Product Page',value:1600},
476
+ {source:'Referrals',target:'Pricing Page',value:800},
477
+ {source:'Direct',target:'Product Page',value:1400},
478
+ {source:'Direct',target:'Pricing Page',value:600},
479
+ {source:'Blog',target:'Trial Signup',value:3100},
480
+ {source:'Blog',target:'Demo Request',value:1900},
481
+ {source:'Landing Page',target:'Trial Signup',value:2400},
482
+ {source:'Landing Page',target:'Demo Request',value:1300},
483
+ {source:'Product Page',target:'Trial Signup',value:2800},
484
+ {source:'Product Page',target:'Demo Request',value:2300},
485
+ {source:'Pricing Page',target:'Trial Signup',value:1500},
486
+ {source:'Pricing Page',target:'Demo Request',value:1100},
487
+ {source:'Trial Signup',target:'Converted',value:6200},
488
+ {source:'Trial Signup',target:'Churned',value:3600},
489
+ {source:'Demo Request',target:'Converted',value:4800},
490
+ {source:'Demo Request',target:'Churned',value:1800}
491
+ ];
492
+
493
+ // ===================================================================
494
+ // Additional data for new charts
495
+ // ===================================================================
496
+
497
+ // State revenue data (top 20 states + rest)
498
+ const stateRevenue = [
499
+ {name:'California',value:9200},{name:'New York',value:7100},{name:'Texas',value:5800},
500
+ {name:'Florida',value:4200},{name:'Illinois',value:3600},{name:'Massachusetts',value:3400},
501
+ {name:'Washington',value:3100},{name:'Pennsylvania',value:2800},{name:'Georgia',value:2500},
502
+ {name:'Virginia',value:2400},{name:'New Jersey',value:2200},{name:'North Carolina',value:2000},
503
+ {name:'Colorado',value:1900},{name:'Ohio',value:1800},{name:'Michigan',value:1600},
504
+ {name:'Arizona',value:1500},{name:'Maryland',value:1400},{name:'Oregon',value:1300},
505
+ {name:'Minnesota',value:1200},{name:'Connecticut',value:1100},{name:'Tennessee',value:950},
506
+ {name:'Indiana',value:850},{name:'Missouri',value:800},{name:'Wisconsin',value:750},
507
+ {name:'Utah',value:700},{name:'Nevada',value:650},{name:'South Carolina',value:600},
508
+ {name:'Alabama',value:500},{name:'Kentucky',value:480},{name:'Louisiana',value:460},
509
+ {name:'Oklahoma',value:420},{name:'Iowa',value:400},{name:'Kansas',value:380},
510
+ {name:'Arkansas',value:320},{name:'Nebraska',value:300},{name:'Mississippi',value:280},
511
+ {name:'New Mexico',value:260},{name:'Idaho',value:240},{name:'Hawaii',value:220},
512
+ {name:'New Hampshire',value:210},{name:'Maine',value:190},{name:'Rhode Island',value:180},
513
+ {name:'Delaware',value:170},{name:'Montana',value:140},{name:'Vermont',value:130},
514
+ {name:'South Dakota',value:110},{name:'North Dakota',value:100},{name:'Alaska',value:90},
515
+ {name:'Wyoming',value:75},{name:'West Virginia',value:250},{name:'District of Columbia',value:1800}
516
+ ];
517
+
518
+ // Customer locations with k-means cluster assignments
519
+ // Clusters: West Coast, Mountain/Central, South, Northeast
520
+ const clusterColors = ['#236aa4','#f4b548','#8f3d56','#46a485'];
521
+ const clusterNames = ['West Coast','Mountain & Central','South','Northeast'];
522
+ const customerLocations = [
523
+ // West Coast (cluster 0)
524
+ [-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],
525
+ [-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],
526
+ [-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],
527
+ [-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],
528
+ [-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],
529
+ // Mountain & Central (cluster 1)
530
+ [-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],
531
+ [-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],
532
+ [-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],
533
+ [-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],
534
+ // South (cluster 2)
535
+ [-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],
536
+ [-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],
537
+ [-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],
538
+ [-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],
539
+ // Northeast (cluster 3)
540
+ [-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],
541
+ [-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],
542
+ [-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],
543
+ [-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]
544
+ ];
545
+
546
+ // Treemap & Sunburst hierarchy
547
+ const revenueTree = [
548
+ { name:'Platform', value:4510, children:[
549
+ {name:'Core Engine',value:1800},{name:'API Gateway',value:1200},
550
+ {name:'Auth & SSO',value:850},{name:'Admin Console',value:660}
551
+ ]},
552
+ { name:'Analytics', value:2500, children:[
553
+ {name:'Dashboards',value:900},{name:'Reports',value:700},
554
+ {name:'Data Explorer',value:550},{name:'Alerts',value:350}
555
+ ]},
556
+ { name:'Integrations', value:1650, children:[
557
+ {name:'CRM Sync',value:600},{name:'Slack & Teams',value:450},
558
+ {name:'Webhooks',value:350},{name:'Custom API',value:250}
559
+ ]},
560
+ { name:'Support', value:890, children:[
561
+ {name:'Live Chat',value:380},{name:'Knowledge Base',value:280},
562
+ {name:'Ticketing',value:230}
563
+ ]}
564
+ ];
565
+
566
+ // Radar metrics (normalized 0-100)
567
+ const radarProducts = {
568
+ 'Platform': [92, 78, 95, 82, 88],
569
+ 'Analytics': [76, 85, 80, 74, 70],
570
+ 'Integrations':[65, 92, 72, 68, 60],
571
+ 'Support': [58, 60, 88, 90, 55]
572
+ };
573
+ const radarMetrics = ['Revenue','Growth','Retention','NPS','Usage'];
574
+
575
+ // Waterfall data: Q1 baseline → expansions/contractions → Q4
576
+ const waterfallLabels = ['Q1 Base','New Customers','Upsells','Price Increase','Churn','Downgrades','Q2','New Customers','Upsells','Churn','Downgrades','Q3','New Customers','Upsells','Churn','Downgrades','Q4'];
577
+ const waterfallValues = [1370, 180, 220, 50, -80, -40, null, 200, 160, -60, -30, null, 240, 200, -70, -30, null];
578
+
579
+ function buildWaterfallSeries(labels, values) {
580
+ const base = [], inc = [], dec = [];
581
+ let running = 0;
582
+ for (let i = 0; i < values.length; i++) {
583
+ const v = values[i];
584
+ if (v === null) {
585
+ base.push(0); inc.push(running); dec.push(0);
586
+ } else if (i === 0) {
587
+ running = v; base.push(0); inc.push(v); dec.push(0);
588
+ } else if (v >= 0) {
589
+ base.push(running); inc.push(v); dec.push(0); running += v;
590
+ } else {
591
+ running += v; base.push(running); inc.push(0); dec.push(-v);
592
+ }
593
+ }
594
+ return { base, inc, dec };
595
+ }
596
+
597
+ // Boxplot data: [min, Q1, median, Q3, max] per quarter
598
+ const boxplotData = [
599
+ [8, 18, 28, 42, 65],
600
+ [10, 22, 35, 52, 78],
601
+ [12, 25, 38, 55, 85],
602
+ [15, 30, 45, 62, 95]
603
+ ];
604
+
605
+ // Parallel coordinates: products across 5 metrics
606
+ const parallelData = [
607
+ ['Platform', 4510, 18.2, 95, 82, 88],
608
+ ['Analytics', 2500, 22.5, 80, 74, 70],
609
+ ['Integrations',1650, 15.8, 72, 68, 60],
610
+ ['Support', 890, 12.0, 88, 90, 55]
611
+ ];
612
+
613
+ // Calendar heatmap: daily logins for 2024
614
+ const calendarData = [];
615
+ const calStart = new Date('2024-01-01');
616
+ for (let i = 0; i < 366; i++) {
617
+ const d = new Date(calStart);
618
+ d.setDate(d.getDate() + i);
619
+ if (d.getFullYear() > 2024) break;
620
+ const dow = d.getDay();
621
+ let base = (dow > 0 && dow < 6) ? 800 : 350;
622
+ const month = d.getMonth();
623
+ base += month * 25;
624
+ base += Math.round(Math.random() * 300 - 100);
625
+ const ds = d.toISOString().slice(0, 10);
626
+ calendarData.push([ds, Math.max(50, base)]);
627
+ }
628
+
629
+ // ===================================================================
630
+ // 1. Line Chart — Monthly Revenue
631
+ // ===================================================================
632
+ init('line', {
633
+ xAxis: { ...XAXIS, data: months },
634
+ yAxis: { ...YAXIS, name:'Revenue ($k)', axisLabel:{...YAXIS.axisLabel, formatter: v => '$'+v+'k'} },
635
+ series: [{
636
+ type:'line', symbol:'circle', symbolSize:0, smooth:false,
637
+ lineStyle:{width:2, type:'solid'},
638
+ emphasis:{focus:'series', lineStyle:{opacity:1, width:3}},
639
+ data: revenue
640
+ }]
641
+ });
642
+
643
+ // ===================================================================
644
+ // 2. Area Chart — Revenue by Product (stacked)
645
+ // ===================================================================
646
+ init('area', {
647
+ xAxis: { ...XAXIS, data: months },
648
+ yAxis: { ...YAXIS, name:'Revenue ($k)', axisLabel:{...YAXIS.axisLabel, formatter: v => '$'+v+'k'} },
649
+ series: Object.entries(products).map(([name, data]) => ({
650
+ type:'line', name, data,
651
+ symbol:'circle', symbolSize:0, smooth:false,
652
+ lineStyle:{width:1}, areaStyle:{opacity:0.45},
653
+ stack:'revenue', emphasis:{focus:'series'}
654
+ }))
655
+ });
656
+
657
+ // ===================================================================
658
+ // 3. Bar Chart — Quarterly Revenue
659
+ // ===================================================================
660
+ init('bar', {
661
+ xAxis: { ...XAXIS, data: quarters },
662
+ yAxis: { ...YAXIS, name:'Revenue ($k)', axisLabel:{...YAXIS.axisLabel, formatter: v => '$'+(v/1000).toFixed(1)+'M'} },
663
+ series: [{ type:'bar', barMaxWidth:60, label:{fontSize:11}, data:quarterlyRev }]
664
+ });
665
+
666
+ // ===================================================================
667
+ // 4. Stacked Bar — Revenue by Product × Quarter
668
+ // ===================================================================
669
+ init('stackedBar', {
670
+ xAxis: { ...XAXIS, data: quarters },
671
+ yAxis: { ...YAXIS, name:'Revenue ($k)', axisLabel:{...YAXIS.axisLabel, formatter: v => '$'+v+'k'} },
672
+ series: Object.entries(productQuarterly).map(([name, data]) => ({
673
+ type:'bar', name, data, stack:'total', barMaxWidth:60, emphasis:{focus:'series'}
674
+ }))
675
+ });
676
+
677
+ // ===================================================================
678
+ // 5. Combo Chart — Revenue + Margin %
679
+ // ===================================================================
680
+ init('combo', {
681
+ xAxis: { ...XAXIS, data: months },
682
+ yAxis: [
683
+ { ...YAXIS, name:'Revenue ($k)', axisLabel:{...YAXIS.axisLabel, formatter: v => '$'+v+'k'} },
684
+ { ...YAXIS, name:'Margin %', position:'right', splitLine:{show:false},
685
+ axisLabel:{...YAXIS.axisLabel, formatter: v => v+'%'}, min:50, max:80 }
686
+ ],
687
+ series: [
688
+ { type:'bar', name:'Revenue', data:revenue, barMaxWidth:60, yAxisIndex:0 },
689
+ { type:'line', name:'Gross Margin', data:margin, yAxisIndex:1,
690
+ symbol:'circle', symbolSize:0, lineStyle:{width:2},
691
+ emphasis:{focus:'series', lineStyle:{opacity:1, width:3}} }
692
+ ]
693
+ });
694
+
695
+ // ===================================================================
696
+ // 6. Multi-Axis — Revenue + Growth Rate
697
+ // ===================================================================
698
+ init('multiAxis', {
699
+ xAxis: { ...XAXIS, data: months },
700
+ yAxis: [
701
+ { ...YAXIS, name:'Revenue ($k)', axisLabel:{...YAXIS.axisLabel, formatter: v => '$'+v+'k'} },
702
+ { ...YAXIS, name:'MoM Growth %', position:'right', splitLine:{show:false},
703
+ axisLabel:{...YAXIS.axisLabel, formatter: v => v+'%'} }
704
+ ],
705
+ series: [
706
+ { type:'line', name:'Revenue', data:revenue, symbol:'circle', symbolSize:0, lineStyle:{width:2},
707
+ emphasis:{focus:'series', lineStyle:{opacity:1, width:3}} },
708
+ { type:'line', name:'Growth %', data:growthRate, yAxisIndex:1,
709
+ symbol:'circle', symbolSize:6, lineStyle:{width:2, type:'dashed'},
710
+ emphasis:{focus:'series', lineStyle:{opacity:1, width:3}} }
711
+ ]
712
+ });
713
+
714
+ // ===================================================================
715
+ // 7. Pie / Donut
716
+ // ===================================================================
717
+ init('pie', {
718
+ tooltip: { trigger:'item', formatter:'{b}: {c} ({d}%)' },
719
+ series: [{
720
+ type:'pie', radius:['40%','70%'], center:['50%','55%'],
721
+ padAngle:2, itemStyle:{borderRadius:4}, label:{fontSize:12},
722
+ data: segments
723
+ }]
724
+ });
725
+
726
+ // ===================================================================
727
+ // 8. Scatter — Deal Size vs. Close Rate
728
+ // ===================================================================
729
+ init('scatter', {
730
+ xAxis: { ...XAXIS, type:'value', name:'Avg Deal Size ($k)',
731
+ axisLabel:{...XAXIS.axisLabel, formatter: v => '$'+v+'k'}, axisLine:{show:true} },
732
+ yAxis: { ...YAXIS, name:'Close Rate %', axisLabel:{...YAXIS.axisLabel, formatter: v => v+'%'} },
733
+ tooltip: { ...BASE.tooltip, trigger:'item',
734
+ formatter: p => `Deal: $${p.value[0]}k<br>Close Rate: ${p.value[1]}%<br>Deals: ${p.value[2]}` },
735
+ series: [{
736
+ type:'scatter', data:deals, symbolSize: d => d[2]*1.8,
737
+ emphasis:{focus:'self', itemStyle:{shadowBlur:10, shadowColor:'rgba(0,0,0,0.3)'}}
738
+ }]
739
+ });
740
+
741
+ // ===================================================================
742
+ // 9. Heatmap — Feature Usage
743
+ // ===================================================================
744
+ init('heatmap', {
745
+ grid: { ...BASE.grid, top:'8%', bottom:'12%' },
746
+ xAxis: { ...XAXIS, data:hours, splitArea:{show:false} },
747
+ yAxis: { ...YAXIS, type:'category', data:days, splitLine:{show:false}, splitArea:{show:false} },
748
+ tooltip: { ...BASE.tooltip, trigger:'item',
749
+ formatter: p => `${days[p.value[1]]} ${hours[p.value[0]]}<br>Sessions: <b>${p.value[2]}</b>` },
750
+ visualMap: {
751
+ min:10, max:140, calculable:true, orient:'horizontal', left:'center', bottom:'0%',
752
+ inRange:{color:['#a5cdee','#236aa4']}, textStyle:{color:'#717171',fontSize:11}
753
+ },
754
+ series: [{ type:'heatmap', data:heatData, label:{show:false},
755
+ emphasis:{itemStyle:{shadowBlur:10, shadowColor:'rgba(0,0,0,0.3)'}} }]
756
+ });
757
+
758
+ // ===================================================================
759
+ // 10. Funnel
760
+ // ===================================================================
761
+ init('funnel', {
762
+ tooltip: { trigger:'item', formatter:'{b}: {c}' },
763
+ series: [{
764
+ type:'funnel', left:'10%', right:'10%', top:'12%', bottom:'8%', width:'80%',
765
+ sort:'descending', gap:4,
766
+ label:{show:true, position:'inside', fontSize:13, color:'#fff'},
767
+ emphasis:{label:{fontSize:15}},
768
+ data: funnelData
769
+ }]
770
+ });
771
+
772
+ // ===================================================================
773
+ // 11. Sankey
774
+ // ===================================================================
775
+ init('sankey', {
776
+ tooltip: { trigger:'item', triggerOn:'mousemove' },
777
+ series: [{
778
+ type:'sankey', layout:'none',
779
+ left:'3%', right:'10%', top:'8%', bottom:'8%',
780
+ nodeWidth:20, nodeGap:14, layoutIterations:32,
781
+ emphasis:{focus:'adjacency'},
782
+ lineStyle:{color:'gradient', curveness:0.5},
783
+ label:{fontSize:12},
784
+ data:sankeyNodes, links:sankeyLinks
785
+ }]
786
+ });
787
+
788
+ // ===================================================================
789
+ // 12–13. US Maps (loaded async)
790
+ // ===================================================================
791
+ fetch('https://raw.githubusercontent.com/apache/echarts-examples/gh-pages/public/data/asset/geo/USA.json')
792
+ .then(r => r.json())
793
+ .then(usaJson => {
794
+ echarts.registerMap('USA', usaJson, {
795
+ 'Alaska': { left:-131, top:25, width:15 },
796
+ 'Hawaii': { left:-110, top:28, width:5 },
797
+ 'Puerto Rico': { left:-76, top:26, width:2 }
798
+ });
799
+
800
+ // 12. Choropleth
801
+ init('choropleth', {
802
+ tooltip: { trigger:'item', formatter: p => p.name + '<br>Revenue: $' + (p.value||0).toLocaleString() + 'k' },
803
+ visualMap: {
804
+ min:50, max:9500, text:['$9.5M+','$50k'],
805
+ inRange: { color:['#a5cdee','#45a1bf','#236aa4'] },
806
+ calculable:true, orient:'horizontal', left:'center', bottom:'2%',
807
+ textStyle:{color:'#717171', fontSize:11}
808
+ },
809
+ series: [{
810
+ type:'map', map:'USA', roam:true,
811
+ emphasis: { label:{show:true, fontSize:11, color:'#060606'},
812
+ itemStyle:{areaColor:'#f4b548'} },
813
+ label:{show:false},
814
+ itemStyle: { areaColor:'#e5e5e5', borderColor:'#fff', borderWidth:1 },
815
+ data: stateRevenue
816
+ }]
817
+ });
818
+
819
+ // 13. Scatter Map with clusters
820
+ init('scatterMap', {
821
+ tooltip: { trigger:'item',
822
+ formatter: p => {
823
+ if (p.seriesType === 'scatter') return clusterNames[p.value[2]] + ' cluster';
824
+ return p.name;
825
+ }
826
+ },
827
+ legend: {
828
+ show:true, top:'2%', data: clusterNames.map((n,i) => ({name:n, icon:'circle',
829
+ itemStyle:{color:clusterColors[i]}}))
830
+ },
831
+ geo: {
832
+ map:'USA', roam:true,
833
+ itemStyle: { areaColor:'#f0f0f0', borderColor:'#d6d6d6', borderWidth:0.5 },
834
+ emphasis: { itemStyle:{areaColor:'#e5e5e5'}, label:{show:false} },
835
+ label:{show:false}
836
+ },
837
+ series: clusterNames.map((name, ci) => ({
838
+ type:'scatter', name, coordinateSystem:'geo',
839
+ data: customerLocations.filter(d => d[2] === ci).map(d => ({value:[d[0],d[1],d[2]]})),
840
+ symbolSize: 8,
841
+ itemStyle: { color:clusterColors[ci], opacity:0.8 },
842
+ emphasis: { itemStyle:{opacity:1, shadowBlur:5, shadowColor:'rgba(0,0,0,0.3)'} }
843
+ }))
844
+ });
845
+ })
846
+ .catch(() => {
847
+ ['choropleth','scatterMap'].forEach(id => {
848
+ const el = document.getElementById(id);
849
+ if (el) 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>';
850
+ });
851
+ });
852
+
853
+ // ===================================================================
854
+ // 14. Gauges — Clean arc style (not NASCAR)
855
+ // ===================================================================
856
+ function makeGauge(id, value, label, fmt, color, max) {
857
+ init(id, {
858
+ tooltip: {show:false},
859
+ legend: {show:false},
860
+ series: [{
861
+ type: 'gauge',
862
+ startAngle: 200,
863
+ endAngle: -20,
864
+ min: 0,
865
+ max: max || 100,
866
+ pointer: { show:false },
867
+ progress: {
868
+ show: true,
869
+ width: 16,
870
+ roundCap: true,
871
+ itemStyle: { color: color }
872
+ },
873
+ axisLine: {
874
+ lineStyle: { width:16, color:[[1, '#e5e5e5']] }
875
+ },
876
+ axisTick: { show:false },
877
+ splitLine: { show:false },
878
+ axisLabel: { show:false },
879
+ anchor: { show:false },
880
+ detail: {
881
+ valueAnimation: true,
882
+ fontSize: 32,
883
+ fontWeight: 600,
884
+ fontFamily: 'Inter',
885
+ color: '#060606',
886
+ offsetCenter: [0, '10%'],
887
+ formatter: fmt
888
+ },
889
+ title: {
890
+ fontSize: 14,
891
+ fontWeight: 500,
892
+ color: '#717171',
893
+ offsetCenter: [0, '60%']
894
+ },
895
+ data: [{ value: value, name: label }]
896
+ }]
897
+ });
898
+ }
899
+
900
+ makeGauge('gauge1', 72, 'NPS Score', '{value}', '#236aa4', 100);
901
+ makeGauge('gauge2', 94.2, 'CSAT', '{value}%', '#46a485', 100);
902
+ makeGauge('gauge3', 99.97, 'Uptime', '{value}%', '#45a1bf', 100);
903
+
904
+ // ===================================================================
905
+ // 15. Treemap
906
+ // ===================================================================
907
+ init('treemap', {
908
+ tooltip: { trigger:'item', formatter: p => p.name + '<br>$' + (p.value||0).toLocaleString() + 'k' },
909
+ legend: {show:false},
910
+ series: [{
911
+ type:'treemap',
912
+ data: revenueTree,
913
+ top:'4%', bottom:'2%', left:'2%', right:'2%',
914
+ roam: false,
915
+ nodeClick: false,
916
+ breadcrumb: { show:false },
917
+ label: { show:true, fontSize:12, color:'#fff', fontWeight:500 },
918
+ upperLabel: { show:true, height:24, fontSize:12, fontWeight:600, color:'#fff' },
919
+ itemStyle: { borderColor:'#fff', borderWidth:2, gapWidth:2 },
920
+ levels: [
921
+ { itemStyle:{borderWidth:3, gapWidth:4, borderColor:'#fff'},
922
+ upperLabel:{show:true} },
923
+ { itemStyle:{borderWidth:2, gapWidth:2, borderColor:'#fff'},
924
+ colorSaturation:[0.3,0.7] }
925
+ ]
926
+ }]
927
+ });
928
+
929
+ // ===================================================================
930
+ // 16. Sunburst
931
+ // ===================================================================
932
+ init('sunburst', {
933
+ tooltip: { trigger:'item', formatter: p => p.name + '<br>$' + (p.value||0).toLocaleString() + 'k' },
934
+ legend: {show:false},
935
+ series: [{
936
+ type:'sunburst',
937
+ data: revenueTree,
938
+ radius: ['15%','90%'],
939
+ sort: null,
940
+ emphasis: { focus:'ancestor' },
941
+ itemStyle: { borderRadius:4, borderWidth:2, borderColor:'#fff' },
942
+ label: { fontSize:11, minAngle:15 },
943
+ levels: [
944
+ {},
945
+ { r0:'15%', r:'45%', label:{fontSize:13, fontWeight:500} },
946
+ { r0:'45%', r:'90%', label:{fontSize:10} }
947
+ ]
948
+ }]
949
+ });
950
+
951
+ // ===================================================================
952
+ // 17. Radar
953
+ // ===================================================================
954
+ init('radar', {
955
+ tooltip: { trigger:'item' },
956
+ radar: {
957
+ indicator: radarMetrics.map(m => ({name:m, max:100})),
958
+ shape:'polygon',
959
+ splitNumber:4,
960
+ axisName: { color:'#717171', fontSize:12 },
961
+ splitLine: { lineStyle:{color:'#e5e5e5'} },
962
+ splitArea: { areaStyle:{color:['#fff','#fafafa']} },
963
+ axisLine: { lineStyle:{color:'#e5e5e5'} }
964
+ },
965
+ series: [{
966
+ type:'radar',
967
+ data: Object.entries(radarProducts).map(([name, value]) => ({name, value})),
968
+ areaStyle: { opacity:0.15 },
969
+ lineStyle: { width:2 },
970
+ symbol: 'circle',
971
+ symbolSize: 5
972
+ }]
973
+ });
974
+
975
+ // ===================================================================
976
+ // 18. Waterfall (stacked bar simulation)
977
+ // ===================================================================
978
+ (function() {
979
+ const wf = buildWaterfallSeries(waterfallLabels, waterfallValues);
980
+ const totals = [0, 6, 11, 16];
981
+ init('waterfall', {
982
+ xAxis: { ...XAXIS, data:waterfallLabels,
983
+ axisLabel:{...XAXIS.axisLabel, rotate:45, fontSize:10} },
984
+ yAxis: { ...YAXIS, name:'Revenue ($k)', axisLabel:{...YAXIS.axisLabel, formatter: v => '$'+v+'k'} },
985
+ legend: {show:false},
986
+ series: [
987
+ { type:'bar', stack:'wf', name:'base', data:wf.base, barMaxWidth:36,
988
+ itemStyle:{color:'transparent'}, emphasis:{itemStyle:{color:'transparent'}} },
989
+ { type:'bar', stack:'wf', name:'Increase', data:wf.inc.map((v,i) => ({
990
+ value:v, itemStyle:{color: totals.includes(i) ? '#236aa4' : '#46a485'} })),
991
+ barMaxWidth:36,
992
+ label:{show:true, position:'top', fontSize:10, color:'#717171',
993
+ formatter: p => { const v=waterfallValues[p.dataIndex]; return v===null?'$'+p.value+'k':v>0?'+$'+v+'k':''; }}
994
+ },
995
+ { type:'bar', stack:'wf', name:'Decrease', data:wf.dec, barMaxWidth:36,
996
+ itemStyle:{color:'#8f3d56'},
997
+ label:{show:true, position:'bottom', fontSize:10, color:'#717171',
998
+ formatter: p => p.value>0 ? '-$'+p.value+'k' : ''} }
999
+ ]
1000
+ });
1001
+ })();
1002
+
1003
+ // ===================================================================
1004
+ // 19. Boxplot
1005
+ // ===================================================================
1006
+ init('boxplot', {
1007
+ xAxis: { ...XAXIS, data:quarters },
1008
+ yAxis: { ...YAXIS, name:'Deal Size ($k)', axisLabel:{...YAXIS.axisLabel, formatter: v => '$'+v+'k'} },
1009
+ tooltip: { ...BASE.tooltip, trigger:'item',
1010
+ formatter: 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` },
1011
+ series: [{
1012
+ type:'boxplot',
1013
+ data: boxplotData,
1014
+ itemStyle: { color:'#a5cdee', borderColor:'#236aa4', borderWidth:1.5 },
1015
+ emphasis: { itemStyle:{borderColor:'#060606', borderWidth:2} }
1016
+ }]
1017
+ });
1018
+
1019
+ // ===================================================================
1020
+ // 20. Parallel Coordinates
1021
+ // ===================================================================
1022
+ init('parallel', {
1023
+ tooltip: {show:false},
1024
+ legend: { show:true, data:parallelData.map(d => d[0]) },
1025
+ parallelAxis: [
1026
+ {dim:0, name:'Product', type:'category', data:parallelData.map(d=>d[0]), nameLocation:'start'},
1027
+ {dim:1, name:'Revenue ($k)', type:'value', nameLocation:'end'},
1028
+ {dim:2, name:'Growth %', type:'value', nameLocation:'end'},
1029
+ {dim:3, name:'Retention %', type:'value', min:50, max:100, nameLocation:'end'},
1030
+ {dim:4, name:'NPS', type:'value', min:50, max:100, nameLocation:'end'},
1031
+ {dim:5, name:'Usage Score', type:'value', min:40, max:100, nameLocation:'end'}
1032
+ ],
1033
+ parallel: {
1034
+ left:'5%', right:'5%', top:'18%', bottom:'12%',
1035
+ parallelAxisDefault: {
1036
+ nameTextStyle: { color:'#717171', fontSize:11 },
1037
+ axisLine: { lineStyle:{color:'#d6d6d6'} },
1038
+ axisTick: { show:false },
1039
+ axisLabel: { color:'#717171', fontSize:10 },
1040
+ splitLine: { show:false }
1041
+ }
1042
+ },
1043
+ series: parallelData.map((d,i) => ({
1044
+ type:'parallel', name:d[0],
1045
+ lineStyle: { width:3, opacity:0.7, color:COLORS[i] },
1046
+ emphasis: { lineStyle:{width:5, opacity:1} },
1047
+ data: [d]
1048
+ }))
1049
+ });
1050
+
1051
+ // ===================================================================
1052
+ // 21. Calendar Heatmap
1053
+ // ===================================================================
1054
+ init('calendar', {
1055
+ tooltip: { trigger:'item', formatter: p => p.value[0] + '<br>Logins: ' + p.value[1].toLocaleString() },
1056
+ legend: {show:false},
1057
+ visualMap: {
1058
+ min:200, max:1400, calculable:true, orient:'horizontal', left:'center', bottom:'2%',
1059
+ inRange:{color:['#a5cdee','#45a1bf','#236aa4']},
1060
+ textStyle:{color:'#717171', fontSize:11}
1061
+ },
1062
+ calendar: {
1063
+ range:'2024',
1064
+ top:'12%', left:'8%', right:'4%', bottom:'18%',
1065
+ cellSize:['auto',16],
1066
+ itemStyle: { borderWidth:2, borderColor:'#fff' },
1067
+ splitLine: { lineStyle:{color:'#d6d6d6', width:1} },
1068
+ yearLabel: { show:false },
1069
+ monthLabel: { color:'#717171', fontSize:11, nameMap:'en' },
1070
+ dayLabel: { color:'#717171', fontSize:10, firstDay:1, nameMap:['','M','','W','','F',''] }
1071
+ },
1072
+ series: [{
1073
+ type:'heatmap', coordinateSystem:'calendar',
1074
+ data: calendarData,
1075
+ emphasis: { itemStyle:{shadowBlur:5, shadowColor:'rgba(0,0,0,0.3)'} }
1076
+ }]
1077
+ });
1078
+
1079
+ // ===================================================================
1080
+ // KPI Sparkline Cards (pure SVG, evidence-style)
1081
+ // ===================================================================
1082
+ function sparklineSVG(data, color, w, h) {
1083
+ const min = Math.min(...data), max = Math.max(...data);
1084
+ const range = max - min || 1;
1085
+ const pts = data.map((v, i) =>
1086
+ `${(i / (data.length - 1)) * w},${h - ((v - min) / range) * (h - 4) - 2}`
1087
+ ).join(' ');
1088
+ return `<svg class="kpi-spark" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">` +
1089
+ `<polyline points="${pts}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>` +
1090
+ `</svg>`;
1091
+ }
1092
+
1093
+ const kpis = [
1094
+ { label:'Monthly Revenue', value:'$870k', spark:revenue, delta:'+7.4% vs. prior month', up:true },
1095
+ { 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 },
1096
+ { 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 },
1097
+ { 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 },
1098
+ { 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 }
1099
+ ];
1100
+
1101
+ document.getElementById('kpiRow').innerHTML = kpis.map(k =>
1102
+ `<div class="kpi">` +
1103
+ `<div class="kpi-label">${k.label}</div>` +
1104
+ `<div class="kpi-row-inner">` +
1105
+ `<div class="kpi-value">${k.value}</div>` +
1106
+ sparklineSVG(k.spark, '#236aa4', 64, 24) +
1107
+ `</div>` +
1108
+ `<div class="kpi-delta ${k.up ? 'up' : 'down'}">${k.up ? '▲' : '▼'} ${k.delta}</div>` +
1109
+ `</div>`
1110
+ ).join('');
1111
+
1112
+ // ===================================================================
1113
+ // 22. Histogram — API Response Time Distribution
1114
+ // ===================================================================
1115
+ const histBins = ['0-50','50-100','100-150','150-200','200-300','300-500','500-1000','1000+'];
1116
+ const histCounts = [1200, 3400, 5800, 4200, 2600, 1400, 600, 180];
1117
+
1118
+ init('histogram', {
1119
+ xAxis: { ...XAXIS, data:histBins, name:'Response Time (ms)',
1120
+ axisLabel:{...XAXIS.axisLabel, fontSize:10} },
1121
+ yAxis: { ...YAXIS, name:'Request Count',
1122
+ axisLabel:{...YAXIS.axisLabel, formatter: v => v>=1000 ? (v/1000)+'k' : v} },
1123
+ legend: {show:false},
1124
+ series: [{
1125
+ type:'bar', data:histCounts.map((v,i) => ({
1126
+ value:v,
1127
+ itemStyle:{ color: i <= 2 ? '#46a485' : i <= 4 ? '#f4b548' : '#8f3d56' }
1128
+ })),
1129
+ barWidth:'90%',
1130
+ emphasis: { itemStyle:{opacity:0.85} }
1131
+ }]
1132
+ });
1133
+
1134
+ // ===================================================================
1135
+ // 23. Horizontal Bar — Feature Adoption
1136
+ // ===================================================================
1137
+ const features = ['Single Sign-On','API Access','Custom Reports','Slack Integration',
1138
+ 'Webhooks','Data Export','Role Permissions','Audit Log','2FA','White Label'];
1139
+ const adoption = [94,87,76,72,65,61,58,52,48,34];
1140
+
1141
+ init('horizBar', {
1142
+ grid: { ...BASE.grid, left:'2%' },
1143
+ xAxis: { ...YAXIS, name:'Adoption %', max:100,
1144
+ axisLabel:{...YAXIS.axisLabel, formatter: v => v+'%'} },
1145
+ yAxis: { ...XAXIS, type:'category', data:features.slice().reverse(),
1146
+ axisLabel:{...XAXIS.axisLabel, fontSize:11},
1147
+ inverse:false },
1148
+ legend: {show:false},
1149
+ tooltip: { ...BASE.tooltip, trigger:'item',
1150
+ formatter: p => p.name + ': ' + p.value + '%' },
1151
+ series: [{
1152
+ type:'bar', data:adoption.slice().reverse(), barMaxWidth:20,
1153
+ itemStyle: { borderRadius:[0,3,3,0] },
1154
+ label: { show:true, position:'right', fontSize:11, color:'#717171',
1155
+ formatter: p => p.value+'%' },
1156
+ emphasis: { itemStyle:{opacity:0.85} }
1157
+ }]
1158
+ });
1159
+
1160
+ // ===================================================================
1161
+ // 24. Polar Area Chart — Support Volume by Channel
1162
+ // ===================================================================
1163
+ const channels = ['Email','Live Chat','Phone','Social Media','Community Forum','In-App'];
1164
+ const tickets = [3200, 2800, 1900, 1400, 1100, 2100];
1165
+
1166
+ init('polar', {
1167
+ tooltip: { trigger:'item', formatter: '{b}: {c} tickets' },
1168
+ legend: { show:true, type:'scroll', bottom:'2%' },
1169
+ angleAxis: { type:'category', data:channels, startAngle:90,
1170
+ axisLine:{lineStyle:{color:'#d6d6d6'}},
1171
+ axisLabel:{color:'#717171', fontSize:11} },
1172
+ radiusAxis: { max:3500,
1173
+ axisLine:{show:false}, axisTick:{show:false},
1174
+ axisLabel:{show:false},
1175
+ splitLine:{lineStyle:{color:'#e5e5e5'}} },
1176
+ polar: { radius:['10%','75%'] },
1177
+ series: [{
1178
+ type:'bar', coordinateSystem:'polar',
1179
+ data: tickets.map((v,i) => ({value:v, itemStyle:{color:COLORS[i]}})),
1180
+ roundCap: true,
1181
+ emphasis: { focus:'self' },
1182
+ label: { show:false }
1183
+ }]
1184
+ });
1185
+
1186
+ // ===================================================================
1187
+ // 25. Scatter with Trend — Engagement vs. Retention
1188
+ // ===================================================================
1189
+ const cohortScatter = [];
1190
+ for (let i = 0; i < 40; i++) {
1191
+ const eng = 20 + Math.random() * 75;
1192
+ const ret = 35 + eng * 0.65 + (Math.random() - 0.5) * 25;
1193
+ cohortScatter.push([Math.round(eng*10)/10, Math.min(99, Math.max(30, Math.round(ret*10)/10))]);
1194
+ }
1195
+ cohortScatter.sort((a,b) => a[0]-b[0]);
1196
+
1197
+ const n = cohortScatter.length;
1198
+ const sx = cohortScatter.reduce((s,d) => s+d[0], 0);
1199
+ const sy = cohortScatter.reduce((s,d) => s+d[1], 0);
1200
+ const sxy = cohortScatter.reduce((s,d) => s+d[0]*d[1], 0);
1201
+ const sx2 = cohortScatter.reduce((s,d) => s+d[0]*d[0], 0);
1202
+ const slope = (n*sxy - sx*sy) / (n*sx2 - sx*sx);
1203
+ const intercept = (sy - slope*sx) / n;
1204
+ const xMin = Math.min(...cohortScatter.map(d=>d[0]));
1205
+ const xMax = Math.max(...cohortScatter.map(d=>d[0]));
1206
+
1207
+ init('scatterReg', {
1208
+ xAxis: { ...XAXIS, type:'value', name:'Engagement Score',
1209
+ axisLabel:{...XAXIS.axisLabel}, axisLine:{show:true} },
1210
+ yAxis: { ...YAXIS, name:'Retention %',
1211
+ axisLabel:{...YAXIS.axisLabel, formatter: v => v+'%'} },
1212
+ tooltip: { ...BASE.tooltip, trigger:'item',
1213
+ formatter: p => p.seriesType==='scatter'
1214
+ ? `Engagement: ${p.value[0]}<br>Retention: ${p.value[1]}%`
1215
+ : 'Trend line' },
1216
+ legend: {show:false},
1217
+ series: [
1218
+ { type:'scatter', data:cohortScatter, symbolSize:9,
1219
+ itemStyle:{color:'#236aa4', opacity:0.7},
1220
+ emphasis:{itemStyle:{opacity:1, shadowBlur:8, shadowColor:'rgba(0,0,0,0.25)'}} },
1221
+ { type:'line', data:[[xMin, slope*xMin+intercept],[xMax, slope*xMax+intercept]],
1222
+ symbol:'none', lineStyle:{width:2, type:'dashed', color:'#8f3d56'},
1223
+ tooltip:{show:false} }
1224
+ ]
1225
+ });
1226
+ </script>
1227
+ </body>
1228
+ </html>