rip-lang 3.12.3 → 3.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/demo.html +1017 -0
- package/docs/dist/rip.js +9483 -0
- package/docs/dist/rip.min.js +48 -48
- package/docs/dist/rip.min.js.br +0 -0
- package/package.json +1 -1
- package/src/app.rip +10 -4
- package/src/components.js +3 -2
- package/src/lexer.js +12 -0
- package/src/typecheck.js +31 -7
package/docs/demo.html
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
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
|
+
cholesterolByAge := [
|
|
421
|
+
[11,169.5],[12,161],[13,150],[14,141.3],[15,141.4],[16,167.5],[17,154.8],[18,156.7],[19,160.4],[20,162.9]
|
|
422
|
+
[21,169.7],[22,164.3],[23,171.1],[24,174.7],[25,176.3],[26,179.3],[27,180.6],[28,183.9],[29,185.1],[30,185.8]
|
|
423
|
+
[31,189.6],[32,190],[33,190.1],[34,190.8],[35,193.1],[36,194.3],[37,195.8],[38,194],[39,195.5],[40,197.2]
|
|
424
|
+
[41,194.8],[42,196.2],[43,195.1],[44,193.4],[45,196.1],[46,196.4],[47,198],[48,199.5],[49,198.8],[50,197.4]
|
|
425
|
+
[51,198.5],[52,197.1],[53,200.9],[54,196.3],[55,198.6],[56,200.4],[57,198.3],[58,197.7],[59,197.3],[60,195.2]
|
|
426
|
+
[61,194.9],[62,192],[63,197.7],[64,190.1],[65,190.3],[66,189.4],[67,186.3],[68,184.6],[69,193],[70,196.7]
|
|
427
|
+
[71,179.9],[72,189.7],[73,195.8],[74,197.8],[75,228.1],[76,235],[77,190.9],[78,125],[79,178.5],[80,191],[85,164]
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
cholesterolRegression ~=
|
|
431
|
+
n = cholesterolByAge.length
|
|
432
|
+
sx = cholesterolByAge.reduce(((s, d) -> s + d[0]), 0)
|
|
433
|
+
sy = cholesterolByAge.reduce(((s, d) -> s + d[1]), 0)
|
|
434
|
+
sxy = cholesterolByAge.reduce(((s, d) -> s + d[0] * d[1]), 0)
|
|
435
|
+
sx2 = cholesterolByAge.reduce(((s, d) -> s + d[0] * d[0]), 0)
|
|
436
|
+
slope = (n * sxy - sx * sy) / (n * sx2 - sx * sx)
|
|
437
|
+
intercept = (sy - slope * sx) / n
|
|
438
|
+
xMin = Math.min(...cholesterolByAge.map(-> it[0]))
|
|
439
|
+
xMax = Math.max(...cholesterolByAge.map(-> it[0]))
|
|
440
|
+
{ slope, intercept, xMin, xMax }
|
|
441
|
+
|
|
442
|
+
cholesterolWithStd := [
|
|
443
|
+
[11,169.5,57.3],[12,161,21.6],[13,150,43.6],[14,141.3,21.8],[15,141.4,27.1],[16,167.5,27],[17,154.8,25.5],[18,156.7,36.3],[19,160.4,34.8],[20,162.9,31]
|
|
444
|
+
[21,169.7,32.4],[22,164.3,31.9],[23,171.1,34.3],[24,174.7,34.7],[25,176.3,32.5],[26,179.3,33.7],[27,180.6,32.9],[28,183.9,35.1],[29,185.1,36.4],[30,185.8,36.9]
|
|
445
|
+
[31,189.6,37.3],[32,190,36.1],[33,190.1,35.9],[34,190.8,35.8],[35,193.1,37.1],[36,194.3,36.6],[37,195.8,38.1],[38,194,37.4],[39,195.5,36.5],[40,197.2,38.4]
|
|
446
|
+
[41,194.8,40.6],[42,196.2,38.2],[43,195.1,36.9],[44,193.4,41.2],[45,196.1,38.6],[46,196.4,40],[47,198,39.8],[48,199.5,42.9],[49,198.8,42.3],[50,197.4,40.5]
|
|
447
|
+
[51,198.5,42.1],[52,197.1,41.7],[53,200.9,45.1],[54,196.3,42.2],[55,198.6,41.9],[56,200.4,45.5],[57,198.3,50.5],[58,197.7,49.7],[59,197.3,45.2],[60,195.2,47]
|
|
448
|
+
[61,194.9,43.5],[62,192,46.5],[63,197.7,42.4],[64,190.1,45.1],[65,190.3,45.5],[66,189.4,51],[67,186.3,46.5],[68,184.6,44.9],[69,193,46],[70,196.7,57.7]
|
|
449
|
+
[71,179.9,49.8],[72,189.7,59.2],[73,195.8,40.7],[74,197.8,40.1],[75,228.1,52],[76,235,0],[77,190.9,37.8],[78,125,0],[79,178.5,29.4],[80,191,0],[85,164,0]
|
|
450
|
+
]
|
|
451
|
+
|
|
452
|
+
a1cValues := [4.2,4.3,4.4,4.5,4.6,4.7,4.8,4.9,5.0,5.1,5.2,5.3,5.4,5.5,5.6,5.7,5.8,5.9,6.0,6.1,6.2,6.3,6.4,6.5,6.6,6.7,6.8,6.9,7.0,7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,8.0,8.1,8.2,8.3,8.4,8.5,8.6,8.7,8.8,8.9,9.0,9.1,9.2,9.3,9.4,9.5,9.6,9.7,9.8,9.9,10.0,10.1,10.2,10.3,10.4,10.5,10.6,10.7,10.8,10.9,11.0,11.1,11.2,11.3,11.4,11.5,11.6,11.7,11.8,11.9,12.0,12.1,12.2,12.3,12.4,12.5,12.6,12.7,12.8,12.9,13.0,13.1,13.2,13.3,13.4,13.5,13.6,13.7,13.8,13.9,14.0,14.1,14.2,14.3,14.4,14.5,14.6,14.7,14.8,15.0,15.1,15.2,15.3,15.4]
|
|
453
|
+
a1cCounts := [12,35,49,82,169,339,598,1137,1983,3060,4349,5225,5955,6122,5530,4704,3755,2660,1791,1256,911,619,492,385,306,263,202,214,192,195,150,136,103,106,101,81,84,84,67,75,64,61,51,47,52,60,34,39,42,35,47,33,24,37,30,15,26,22,11,25,29,18,16,15,17,20,17,22,18,17,21,17,19,11,14,13,22,8,12,12,9,17,13,7,6,14,4,6,6,7,9,3,7,4,8,5,3,6,2,1,4,2,1,3,2,3,2,1,1,2,2,1]
|
|
454
|
+
|
|
455
|
+
cohortScatter ~=
|
|
456
|
+
result = []
|
|
457
|
+
for i in [0...40]
|
|
458
|
+
eng = 20 + Math.random() * 75
|
|
459
|
+
ret = 35 + eng * 0.65 + (Math.random() - 0.5) * 25
|
|
460
|
+
result.push [Math.round(eng * 10) / 10, Math.min(99, Math.max(30, Math.round(ret * 10) / 10))]
|
|
461
|
+
result.sort((a, b) -> a[0] - b[0])
|
|
462
|
+
result
|
|
463
|
+
|
|
464
|
+
regression ~=
|
|
465
|
+
n = cohortScatter.length
|
|
466
|
+
sx = cohortScatter.reduce(((s, d) -> s + d[0]), 0)
|
|
467
|
+
sy = cohortScatter.reduce(((s, d) -> s + d[1]), 0)
|
|
468
|
+
sxy = cohortScatter.reduce(((s, d) -> s + d[0] * d[1]), 0)
|
|
469
|
+
sx2 = cohortScatter.reduce(((s, d) -> s + d[0] * d[0]), 0)
|
|
470
|
+
slope = (n * sxy - sx * sy) / (n * sx2 - sx * sx)
|
|
471
|
+
intercept = (sy - slope * sx) / n
|
|
472
|
+
xMin = Math.min(...cohortScatter.map(-> it[0]))
|
|
473
|
+
xMax = Math.max(...cohortScatter.map(-> it[0]))
|
|
474
|
+
{ slope, intercept, xMin, xMax }
|
|
475
|
+
|
|
476
|
+
kpis := [
|
|
477
|
+
{ 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 }
|
|
478
|
+
{ 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 }
|
|
479
|
+
{ 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 }
|
|
480
|
+
{ 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 }
|
|
481
|
+
{ 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 }
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
# ── Helpers ──
|
|
485
|
+
|
|
486
|
+
$k = (v) -> "$#{v}k"
|
|
487
|
+
pct = (v) -> "#{v}%"
|
|
488
|
+
$M = (v) -> "$#{(v / 1000).toFixed(1)}M"
|
|
489
|
+
valK = (p) -> "#{p.name}<br>$#{(p.value or 0).toLocaleString()}k"
|
|
490
|
+
tip = (fmt) -> { ...BASE.tooltip, trigger: 'item', formatter: fmt }
|
|
491
|
+
|
|
492
|
+
xaxis = (data = null, opts = {}) ->
|
|
493
|
+
if typeof data is 'object' and not Array.isArray(data) and data isnt null
|
|
494
|
+
opts = data
|
|
495
|
+
data = opts.data or null
|
|
496
|
+
label = if opts.axisLabel then { ...XAXIS.axisLabel, ...opts.axisLabel } else XAXIS.axisLabel
|
|
497
|
+
{ ...XAXIS, data, ...opts, axisLabel: label }
|
|
498
|
+
yaxis = (name, fmt, opts = {}) ->
|
|
499
|
+
label = if opts.axisLabel then { ...YAXIS.axisLabel, formatter: fmt, ...opts.axisLabel } else { ...YAXIS.axisLabel, formatter: fmt }
|
|
500
|
+
{ ...YAXIS, name, ...opts, axisLabel: label }
|
|
501
|
+
|
|
502
|
+
ic: (id, opts) ->
|
|
503
|
+
el = document.getElementById(id)
|
|
504
|
+
return unless el
|
|
505
|
+
c = echarts.init(el, 'evidence')
|
|
506
|
+
c.setOption({ ...BASE, ...opts })
|
|
507
|
+
window.addEventListener 'resize', -> c.resize({ animation: { duration: 500 } })
|
|
508
|
+
c
|
|
509
|
+
|
|
510
|
+
buildWaterfall: (labels, values) ->
|
|
511
|
+
base = []
|
|
512
|
+
inc = []
|
|
513
|
+
dec = []
|
|
514
|
+
running = 0
|
|
515
|
+
for i in [0...values.length]
|
|
516
|
+
v = values[i]
|
|
517
|
+
if v is null
|
|
518
|
+
base.push 0
|
|
519
|
+
inc.push running
|
|
520
|
+
dec.push 0
|
|
521
|
+
else if i is 0
|
|
522
|
+
running = v
|
|
523
|
+
base.push 0
|
|
524
|
+
inc.push v
|
|
525
|
+
dec.push 0
|
|
526
|
+
else if v >= 0
|
|
527
|
+
base.push running
|
|
528
|
+
inc.push v
|
|
529
|
+
dec.push 0
|
|
530
|
+
running += v
|
|
531
|
+
else
|
|
532
|
+
running += v
|
|
533
|
+
base.push running
|
|
534
|
+
inc.push 0
|
|
535
|
+
dec.push -v
|
|
536
|
+
{ base, inc, dec }
|
|
537
|
+
|
|
538
|
+
makeGauge: (id, value, label, fmt, color, max) ->
|
|
539
|
+
@ic id,
|
|
540
|
+
tooltip: { show: false }
|
|
541
|
+
legend: { show: false }
|
|
542
|
+
series: [{
|
|
543
|
+
type: 'gauge', startAngle: 200, endAngle: -20, min: 0, max: max or 100
|
|
544
|
+
pointer: { show: false }
|
|
545
|
+
progress: { show: true, width: 16, roundCap: true, itemStyle: { color } }
|
|
546
|
+
axisLine: { lineStyle: { width: 16, color: [[1, '#e5e5e5']] } }
|
|
547
|
+
axisTick: { show: false }, splitLine: { show: false }, axisLabel: { show: false }, anchor: { show: false }
|
|
548
|
+
detail:
|
|
549
|
+
valueAnimation: true, fontSize: 26, fontWeight: 600, fontFamily: 'Inter'
|
|
550
|
+
color: '#060606', offsetCenter: [0, '10%'], formatter: fmt
|
|
551
|
+
title: { fontSize: 13, fontWeight: 500, color: '#717171', offsetCenter: [0, '55%'] }
|
|
552
|
+
data: [{ value, name: label }]
|
|
553
|
+
}]
|
|
554
|
+
|
|
555
|
+
# ── Chart initialization ──
|
|
556
|
+
|
|
557
|
+
initCoreCharts: ->
|
|
558
|
+
@ic 'line',
|
|
559
|
+
xAxis: xaxis months
|
|
560
|
+
yAxis: yaxis 'Revenue ($k)', $k
|
|
561
|
+
series: [{
|
|
562
|
+
type: 'line', symbol: 'circle', symbolSize: 0, smooth: false
|
|
563
|
+
lineStyle: { width: 2, type: 'solid' }
|
|
564
|
+
emphasis: { focus: 'series', lineStyle: { opacity: 1, width: 3 } }
|
|
565
|
+
data: revenue
|
|
566
|
+
}]
|
|
567
|
+
|
|
568
|
+
@ic 'area',
|
|
569
|
+
xAxis: xaxis months
|
|
570
|
+
yAxis: yaxis 'Revenue ($k)', $k
|
|
571
|
+
series: ({
|
|
572
|
+
type: 'line', name, data
|
|
573
|
+
symbol: 'circle', symbolSize: 0, smooth: false
|
|
574
|
+
lineStyle: { width: 1 }, areaStyle: { opacity: 0.45 }
|
|
575
|
+
stack: 'revenue', emphasis: { focus: 'series' }
|
|
576
|
+
} for name, data of products)
|
|
577
|
+
|
|
578
|
+
@ic 'bar',
|
|
579
|
+
xAxis: xaxis quarters
|
|
580
|
+
yAxis: yaxis 'Revenue ($k)', $M
|
|
581
|
+
series: [{ type: 'bar', barMaxWidth: 60, label: { fontSize: 11 }, data: quarterlyRev }]
|
|
582
|
+
|
|
583
|
+
@ic 'stackedBar',
|
|
584
|
+
xAxis: xaxis quarters
|
|
585
|
+
yAxis: yaxis 'Revenue ($k)', $k
|
|
586
|
+
series: ({ type: 'bar', name, data, stack: 'total', barMaxWidth: 60, emphasis: { focus: 'series' } } for name, data of productQuarterly)
|
|
587
|
+
|
|
588
|
+
@ic 'combo',
|
|
589
|
+
xAxis: xaxis months
|
|
590
|
+
yAxis: [
|
|
591
|
+
yaxis 'Revenue ($k)', $k
|
|
592
|
+
yaxis 'Margin %', pct, position: 'right', splitLine: { show: false }, min: 50, max: 80
|
|
593
|
+
]
|
|
594
|
+
series: [
|
|
595
|
+
{ type: 'bar', name: 'Revenue', data: revenue, barMaxWidth: 60, yAxisIndex: 0 }
|
|
596
|
+
{ type: 'line', name: 'Gross Margin', data: margin, yAxisIndex: 1, symbol: 'circle', symbolSize: 0, lineStyle: { width: 2 }, emphasis: { focus: 'series', lineStyle: { opacity: 1, width: 3 } } }
|
|
597
|
+
]
|
|
598
|
+
|
|
599
|
+
@ic 'multiAxis',
|
|
600
|
+
xAxis: xaxis months
|
|
601
|
+
yAxis: [
|
|
602
|
+
yaxis 'Revenue ($k)', $k
|
|
603
|
+
yaxis 'MoM Growth %', pct, position: 'right', splitLine: { show: false }
|
|
604
|
+
]
|
|
605
|
+
series: [
|
|
606
|
+
{ type: 'line', name: 'Revenue', data: revenue, symbol: 'circle', symbolSize: 0, lineStyle: { width: 2 }, emphasis: { focus: 'series', lineStyle: { opacity: 1, width: 3 } } }
|
|
607
|
+
{ 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 } } }
|
|
608
|
+
]
|
|
609
|
+
|
|
610
|
+
@ic 'pie',
|
|
611
|
+
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }
|
|
612
|
+
series: [{
|
|
613
|
+
type: 'pie', radius: ['40%','70%'], center: ['50%','55%']
|
|
614
|
+
padAngle: 2, itemStyle: { borderRadius: 4 }, label: { fontSize: 12 }
|
|
615
|
+
data: segments
|
|
616
|
+
}]
|
|
617
|
+
|
|
618
|
+
@ic 'scatter',
|
|
619
|
+
xAxis: xaxis type: 'value', name: 'Avg Deal Size ($k)', axisLabel: { formatter: $k }, axisLine: { show: true }
|
|
620
|
+
yAxis: yaxis 'Close Rate %', pct
|
|
621
|
+
tooltip: tip (p) -> "Deal: $#{p.value[0]}k<br>Close Rate: #{p.value[1]}%<br>Deals: #{p.value[2]}"
|
|
622
|
+
series: [{
|
|
623
|
+
type: 'scatter', data: deals, symbolSize: (d) -> d[2] * 1.8
|
|
624
|
+
emphasis: { focus: 'self', itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.3)' } }
|
|
625
|
+
}]
|
|
626
|
+
|
|
627
|
+
@ic 'heatmap',
|
|
628
|
+
grid: { ...BASE.grid, top: '8%', bottom: '12%' }
|
|
629
|
+
xAxis: xaxis hours, splitArea: { show: false }
|
|
630
|
+
yAxis: { ...YAXIS, type: 'category', data: days, splitLine: { show: false }, splitArea: { show: false } }
|
|
631
|
+
tooltip: tip (p) -> "#{days[p.value[1]]} #{hours[p.value[0]]}<br>Sessions: <b>#{p.value[2]}</b>"
|
|
632
|
+
visualMap:
|
|
633
|
+
min: 10, max: 140, calculable: true, orient: 'horizontal', left: 'center', bottom: '0%'
|
|
634
|
+
inRange: { color: ['#a5cdee','#236aa4'] }
|
|
635
|
+
textStyle: { color: '#717171', fontSize: 11 }
|
|
636
|
+
series: [{ type: 'heatmap', data: heatData, label: { show: false }, emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.3)' } } }]
|
|
637
|
+
|
|
638
|
+
@ic 'funnel',
|
|
639
|
+
tooltip: { trigger: 'item', formatter: '{b}: {c}' }
|
|
640
|
+
series: [{
|
|
641
|
+
type: 'funnel', left: '10%', right: '10%', top: '12%', bottom: '8%', width: '80%'
|
|
642
|
+
sort: 'descending', gap: 4
|
|
643
|
+
label: { show: true, position: 'inside', fontSize: 13, color: '#fff' }
|
|
644
|
+
emphasis: { label: { fontSize: 15 } }
|
|
645
|
+
data: funnelData
|
|
646
|
+
}]
|
|
647
|
+
|
|
648
|
+
@ic 'sankey',
|
|
649
|
+
tooltip: { trigger: 'item', triggerOn: 'mousemove' }
|
|
650
|
+
series: [{
|
|
651
|
+
type: 'sankey', layout: 'none'
|
|
652
|
+
left: '3%', right: '10%', top: '8%', bottom: '8%'
|
|
653
|
+
nodeWidth: 20, nodeGap: 14, layoutIterations: 32
|
|
654
|
+
emphasis: { focus: 'adjacency' }
|
|
655
|
+
lineStyle: { color: 'gradient', curveness: 0.5 }
|
|
656
|
+
label: { fontSize: 12 }
|
|
657
|
+
data: sankeyNodes, links: sankeyLinks
|
|
658
|
+
}]
|
|
659
|
+
|
|
660
|
+
initMaps: ->
|
|
661
|
+
try
|
|
662
|
+
usaJson = fetch!('https://raw.githubusercontent.com/apache/echarts-examples/gh-pages/public/data/asset/geo/USA.json').json!
|
|
663
|
+
echarts.registerMap 'USA', usaJson,
|
|
664
|
+
'Alaska': { left: -131, top: 25, width: 15 }
|
|
665
|
+
'Hawaii': { left: -110, top: 28, width: 5 }
|
|
666
|
+
'Puerto Rico': { left: -76, top: 26, width: 2 }
|
|
667
|
+
|
|
668
|
+
@ic 'choropleth',
|
|
669
|
+
tooltip: { trigger: 'item', formatter: (p) -> "#{p.name}<br>Revenue: $#{(p.value or 0).toLocaleString()}k" }
|
|
670
|
+
visualMap:
|
|
671
|
+
min: 50, max: 9500, text: ['$9.5M+','$50k']
|
|
672
|
+
inRange: { color: ['#a5cdee','#45a1bf','#236aa4'] }
|
|
673
|
+
calculable: true, orient: 'horizontal', left: 'center', bottom: '2%'
|
|
674
|
+
textStyle: { color: '#717171', fontSize: 11 }
|
|
675
|
+
series: [{
|
|
676
|
+
type: 'map', map: 'USA', roam: true
|
|
677
|
+
emphasis: { label: { show: true, fontSize: 11, color: '#060606' }, itemStyle: { areaColor: '#f4b548' } }
|
|
678
|
+
label: { show: false }
|
|
679
|
+
itemStyle: { areaColor: '#e5e5e5', borderColor: '#fff', borderWidth: 1 }
|
|
680
|
+
data: stateRevenue
|
|
681
|
+
}]
|
|
682
|
+
|
|
683
|
+
@ic 'scatterMap',
|
|
684
|
+
tooltip:
|
|
685
|
+
trigger: 'item'
|
|
686
|
+
formatter: (p) ->
|
|
687
|
+
return "#{clusterNames[p.value[2]]} cluster" if p.seriesType is 'scatter'
|
|
688
|
+
p.name
|
|
689
|
+
legend:
|
|
690
|
+
show: true, top: '2%'
|
|
691
|
+
data: clusterNames.map((n, i) -> { name: n, icon: 'circle', itemStyle: { color: clusterColors[i] } })
|
|
692
|
+
geo:
|
|
693
|
+
map: 'USA', roam: true
|
|
694
|
+
itemStyle: { areaColor: '#f0f0f0', borderColor: '#d6d6d6', borderWidth: 0.5 }
|
|
695
|
+
emphasis: { itemStyle: { areaColor: '#e5e5e5' }, label: { show: false } }
|
|
696
|
+
label: { show: false }
|
|
697
|
+
series: clusterNames.map((name, ci) -> {
|
|
698
|
+
type: 'scatter', name, coordinateSystem: 'geo'
|
|
699
|
+
data: customerLocations.filter((d) -> d[2] is ci).map((d) -> { value: [d[0], d[1], d[2]] })
|
|
700
|
+
symbolSize: 8
|
|
701
|
+
itemStyle: { color: clusterColors[ci], opacity: 0.8 }
|
|
702
|
+
emphasis: { itemStyle: { opacity: 1, shadowBlur: 5, shadowColor: 'rgba(0,0,0,0.3)' } }
|
|
703
|
+
})
|
|
704
|
+
catch
|
|
705
|
+
for id in ['choropleth', 'scatterMap']
|
|
706
|
+
el = document.getElementById(id)
|
|
707
|
+
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
|
|
708
|
+
|
|
709
|
+
initHierarchicalCharts: ->
|
|
710
|
+
@ic 'treemap',
|
|
711
|
+
tooltip: { trigger: 'item', formatter: valK }
|
|
712
|
+
legend: { show: false }
|
|
713
|
+
series: [{
|
|
714
|
+
type: 'treemap', data: revenueTree
|
|
715
|
+
top: '4%', bottom: '2%', left: '2%', right: '2%'
|
|
716
|
+
roam: false, nodeClick: false, breadcrumb: { show: false }
|
|
717
|
+
label: { show: true, fontSize: 12, color: '#fff', fontWeight: 500 }
|
|
718
|
+
upperLabel: { show: true, height: 24, fontSize: 12, fontWeight: 600, color: '#fff' }
|
|
719
|
+
itemStyle: { borderColor: '#fff', borderWidth: 2, gapWidth: 2 }
|
|
720
|
+
levels: [
|
|
721
|
+
{ itemStyle: { borderWidth: 3, gapWidth: 4, borderColor: '#fff' }, upperLabel: { show: true } }
|
|
722
|
+
{ itemStyle: { borderWidth: 2, gapWidth: 2, borderColor: '#fff' }, colorSaturation: [0.3, 0.7] }
|
|
723
|
+
]
|
|
724
|
+
}]
|
|
725
|
+
|
|
726
|
+
@ic 'sunburst',
|
|
727
|
+
tooltip: { trigger: 'item', formatter: valK }
|
|
728
|
+
legend: { show: false }
|
|
729
|
+
series: [{
|
|
730
|
+
type: 'sunburst', data: revenueTree, radius: ['15%','90%'], sort: null
|
|
731
|
+
emphasis: { focus: 'ancestor' }
|
|
732
|
+
itemStyle: { borderRadius: 4, borderWidth: 2, borderColor: '#fff' }
|
|
733
|
+
label: { fontSize: 11, minAngle: 15 }
|
|
734
|
+
levels: [
|
|
735
|
+
{}
|
|
736
|
+
{ r0: '15%', r: '45%', label: { fontSize: 13, fontWeight: 500 } }
|
|
737
|
+
{ r0: '45%', r: '90%', label: { fontSize: 10 } }
|
|
738
|
+
]
|
|
739
|
+
}]
|
|
740
|
+
|
|
741
|
+
initAnalyticalCharts: ->
|
|
742
|
+
@ic 'radar',
|
|
743
|
+
tooltip: { trigger: 'item' }
|
|
744
|
+
radar:
|
|
745
|
+
indicator: radarMetrics.map((m) -> { name: m, max: 100 })
|
|
746
|
+
shape: 'polygon', splitNumber: 4
|
|
747
|
+
axisName: { color: '#717171', fontSize: 12 }
|
|
748
|
+
splitLine: { lineStyle: { color: '#e5e5e5' } }
|
|
749
|
+
splitArea: { areaStyle: { color: ['#fff','#fafafa'] } }
|
|
750
|
+
axisLine: { lineStyle: { color: '#e5e5e5' } }
|
|
751
|
+
series: [{
|
|
752
|
+
type: 'radar'
|
|
753
|
+
data: ({ name, value } for name, value of radarProducts)
|
|
754
|
+
areaStyle: { opacity: 0.15 }, lineStyle: { width: 2 }
|
|
755
|
+
symbol: 'circle', symbolSize: 5
|
|
756
|
+
}]
|
|
757
|
+
|
|
758
|
+
wf = @buildWaterfall(waterfallLabels, waterfallValues)
|
|
759
|
+
totals = [0, 6, 11, 16]
|
|
760
|
+
@ic 'waterfall',
|
|
761
|
+
xAxis: xaxis waterfallLabels, axisLabel: { rotate: 45, fontSize: 10 }
|
|
762
|
+
yAxis: yaxis 'Revenue ($k)', $k
|
|
763
|
+
legend: { show: false }
|
|
764
|
+
series: [
|
|
765
|
+
{ type: 'bar', stack: 'wf', name: 'base', data: wf.base, barMaxWidth: 36, itemStyle: { color: 'transparent' }, emphasis: { itemStyle: { color: 'transparent' } } }
|
|
766
|
+
{ 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; '' } }
|
|
767
|
+
{ 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" : '' } }
|
|
768
|
+
]
|
|
769
|
+
|
|
770
|
+
@ic 'boxplot',
|
|
771
|
+
xAxis: xaxis quarters
|
|
772
|
+
yAxis: yaxis 'Deal Size ($k)', $k
|
|
773
|
+
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"
|
|
774
|
+
series: [{
|
|
775
|
+
type: 'boxplot', data: boxplotData
|
|
776
|
+
itemStyle: { color: '#a5cdee', borderColor: '#236aa4', borderWidth: 1.5 }
|
|
777
|
+
emphasis: { itemStyle: { borderColor: '#060606', borderWidth: 2 } }
|
|
778
|
+
}]
|
|
779
|
+
|
|
780
|
+
@ic 'parallel',
|
|
781
|
+
tooltip: { show: false }
|
|
782
|
+
legend: { show: true, data: parallelData.map(-> it[0]) }
|
|
783
|
+
parallelAxis: [
|
|
784
|
+
{ dim: 0, name: 'Product', type: 'category', data: parallelData.map(-> it[0]), nameLocation: 'start' }
|
|
785
|
+
{ dim: 1, name: 'Revenue ($k)', type: 'value', nameLocation: 'end' }
|
|
786
|
+
{ dim: 2, name: 'Growth %', type: 'value', nameLocation: 'end' }
|
|
787
|
+
{ dim: 3, name: 'Retention %', type: 'value', min: 50, max: 100, nameLocation: 'end' }
|
|
788
|
+
{ dim: 4, name: 'NPS', type: 'value', min: 50, max: 100, nameLocation: 'end' }
|
|
789
|
+
{ dim: 5, name: 'Usage Score', type: 'value', min: 40, max: 100, nameLocation: 'end' }
|
|
790
|
+
]
|
|
791
|
+
parallel:
|
|
792
|
+
left: '5%', right: '5%', top: '18%', bottom: '12%'
|
|
793
|
+
parallelAxisDefault:
|
|
794
|
+
nameTextStyle: { color: '#717171', fontSize: 11 }
|
|
795
|
+
axisLine: { lineStyle: { color: '#d6d6d6' } }
|
|
796
|
+
axisTick: { show: false }
|
|
797
|
+
axisLabel: { color: '#717171', fontSize: 10 }
|
|
798
|
+
splitLine: { show: false }
|
|
799
|
+
series: parallelData.map((d, i) -> {
|
|
800
|
+
type: 'parallel', name: d[0]
|
|
801
|
+
lineStyle: { width: 3, opacity: 0.7, color: COLORS[i] }
|
|
802
|
+
emphasis: { lineStyle: { width: 5, opacity: 1 } }
|
|
803
|
+
data: [d]
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
@ic 'calendar',
|
|
807
|
+
tooltip: { trigger: 'item', formatter: (p) -> "#{p.value[0]}<br>Logins: #{p.value[1].toLocaleString()}" }
|
|
808
|
+
legend: { show: false }
|
|
809
|
+
visualMap:
|
|
810
|
+
min: 200, max: 1400, calculable: true, orient: 'horizontal', left: 'center', bottom: '2%'
|
|
811
|
+
inRange: { color: ['#a5cdee','#45a1bf','#236aa4'] }
|
|
812
|
+
textStyle: { color: '#717171', fontSize: 11 }
|
|
813
|
+
calendar:
|
|
814
|
+
range: '2024', top: '12%', left: '8%', right: '4%', bottom: '18%'
|
|
815
|
+
cellSize: ['auto', 16]
|
|
816
|
+
itemStyle: { borderWidth: 2, borderColor: '#fff' }
|
|
817
|
+
splitLine: { lineStyle: { color: '#d6d6d6', width: 1 } }
|
|
818
|
+
yearLabel: { show: false }
|
|
819
|
+
monthLabel: { color: '#717171', fontSize: 11, nameMap: 'en' }
|
|
820
|
+
dayLabel: { color: '#717171', fontSize: 10, firstDay: 1, nameMap: ['','M','','W','','F',''] }
|
|
821
|
+
series: [{
|
|
822
|
+
type: 'heatmap', coordinateSystem: 'calendar', data: calendarData
|
|
823
|
+
emphasis: { itemStyle: { shadowBlur: 5, shadowColor: 'rgba(0,0,0,0.3)' } }
|
|
824
|
+
}]
|
|
825
|
+
|
|
826
|
+
initAdditionalCharts: ->
|
|
827
|
+
@ic 'histogram',
|
|
828
|
+
xAxis: xaxis histBins, name: 'Response Time (ms)', axisLabel: { fontSize: 10 }
|
|
829
|
+
yAxis: yaxis 'Request Count', (v) -> v >= 1000 ? "#{v / 1000}k" : v
|
|
830
|
+
legend: { show: false }
|
|
831
|
+
series: [{
|
|
832
|
+
type: 'bar', barWidth: '90%'
|
|
833
|
+
data: histCounts.map((v, i) -> { value: v, itemStyle: { color: i <= 2 ? '#46a485' : i <= 4 ? '#f4b548' : '#8f3d56' } })
|
|
834
|
+
emphasis: { itemStyle: { opacity: 0.85 } }
|
|
835
|
+
}]
|
|
836
|
+
|
|
837
|
+
@ic 'horizBar',
|
|
838
|
+
grid: { ...BASE.grid, left: '2%' }
|
|
839
|
+
xAxis: yaxis 'Adoption %', pct, max: 100
|
|
840
|
+
yAxis: xaxis features.slice().reverse(), type: 'category', axisLabel: { fontSize: 11 }, inverse: false
|
|
841
|
+
legend: { show: false }
|
|
842
|
+
tooltip: tip (p) -> "#{p.name}: #{p.value}%"
|
|
843
|
+
series: [{
|
|
844
|
+
type: 'bar', data: adoption.slice().reverse(), barMaxWidth: 20
|
|
845
|
+
itemStyle: { borderRadius: [0,3,3,0] }
|
|
846
|
+
label: { show: true, position: 'right', fontSize: 11, color: '#717171', formatter: (p) -> "#{p.value}%" }
|
|
847
|
+
emphasis: { itemStyle: { opacity: 0.85 } }
|
|
848
|
+
}]
|
|
849
|
+
|
|
850
|
+
@ic 'polar',
|
|
851
|
+
tooltip: { trigger: 'item', formatter: '{b}: {c} tickets' }
|
|
852
|
+
legend: { show: true, type: 'scroll', bottom: '2%' }
|
|
853
|
+
angleAxis:
|
|
854
|
+
type: 'category', data: channels, startAngle: 90
|
|
855
|
+
axisLine: { lineStyle: { color: '#d6d6d6' } }
|
|
856
|
+
axisLabel: { color: '#717171', fontSize: 11 }
|
|
857
|
+
radiusAxis:
|
|
858
|
+
max: 3500, axisLine: { show: false }, axisTick: { show: false }
|
|
859
|
+
axisLabel: { show: false }
|
|
860
|
+
splitLine: { lineStyle: { color: '#e5e5e5' } }
|
|
861
|
+
polar: { radius: ['10%','75%'] }
|
|
862
|
+
series: [{
|
|
863
|
+
type: 'bar', coordinateSystem: 'polar', roundCap: true
|
|
864
|
+
data: tickets.map((v, i) -> { value: v, itemStyle: { color: COLORS[i] } })
|
|
865
|
+
emphasis: { focus: 'self' }, label: { show: false }
|
|
866
|
+
}]
|
|
867
|
+
|
|
868
|
+
r = regression
|
|
869
|
+
@ic 'scatterReg',
|
|
870
|
+
xAxis: xaxis type: 'value', name: 'Engagement Score', axisLine: { show: true }
|
|
871
|
+
yAxis: yaxis 'Retention %', pct
|
|
872
|
+
tooltip: tip (p) -> p.seriesType is 'scatter' ? "Engagement: #{p.value[0]}<br>Retention: #{p.value[1]}%" : 'Trend line'
|
|
873
|
+
legend: { show: false }
|
|
874
|
+
series: [
|
|
875
|
+
{ 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)' } } }
|
|
876
|
+
{ 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 } }
|
|
877
|
+
]
|
|
878
|
+
|
|
879
|
+
@ic 'a1c',
|
|
880
|
+
xAxis: xaxis a1cValues, name: 'Hemoglobin A1c (%)', axisLabel: { fontSize: 10, rotate: 45 }
|
|
881
|
+
yAxis: yaxis 'People', (v) -> v >= 1000 ? "#{(v / 1000).toFixed(1)}k" : v
|
|
882
|
+
legend: { show: false }
|
|
883
|
+
tooltip: tip (p) -> "A1c #{p.name}%<br>Count: #{p.value.toLocaleString()}"
|
|
884
|
+
series: [{
|
|
885
|
+
type: 'bar', data: a1cCounts.map((v, i) -> {
|
|
886
|
+
value: v
|
|
887
|
+
itemStyle: { color: a1cValues[i] < 5.7 ? '#46a485' : a1cValues[i] < 6.5 ? '#f4b548' : '#8f3d56' }
|
|
888
|
+
})
|
|
889
|
+
barWidth: '90%'
|
|
890
|
+
emphasis: { itemStyle: { opacity: 0.85 } }
|
|
891
|
+
}]
|
|
892
|
+
|
|
893
|
+
cr = cholesterolRegression
|
|
894
|
+
@ic 'cholesterol',
|
|
895
|
+
xAxis: xaxis type: 'value', name: 'Age (years)', axisLine: { show: true }
|
|
896
|
+
yAxis: yaxis 'Avg Total Cholesterol (mg/dL)', (v) -> "#{v}"
|
|
897
|
+
tooltip: tip (p) -> p.seriesType is 'scatter' ? "Age #{p.value[0]}<br>Avg Cholesterol: #{p.value[1]} mg/dL" : 'Trend line'
|
|
898
|
+
legend: { show: false }
|
|
899
|
+
series: [
|
|
900
|
+
{ type: 'scatter', data: cholesterolByAge, symbolSize: 9, itemStyle: { color: '#236aa4', opacity: 0.7 }, emphasis: { itemStyle: { opacity: 1, shadowBlur: 8, shadowColor: 'rgba(0,0,0,0.25)' } } }
|
|
901
|
+
{ type: 'line', data: [[cr.xMin, cr.slope * cr.xMin + cr.intercept], [cr.xMax, cr.slope * cr.xMax + cr.intercept]], symbol: 'none', lineStyle: { width: 2, type: 'dashed', color: '#8f3d56' }, tooltip: { show: false } }
|
|
902
|
+
]
|
|
903
|
+
|
|
904
|
+
errorBarRenderer = (params, api) ->
|
|
905
|
+
age = api.value(0)
|
|
906
|
+
avg = api.value(1)
|
|
907
|
+
std = api.value(2)
|
|
908
|
+
return null if std is 0
|
|
909
|
+
hi = api.coord([age, avg + std])
|
|
910
|
+
lo = api.coord([age, avg - std])
|
|
911
|
+
cap = 4
|
|
912
|
+
{ type: 'group', children: [
|
|
913
|
+
{ type: 'line', shape: { x1: hi[0], y1: hi[1], x2: lo[0], y2: lo[1] }, style: { stroke: '#236aa4', lineWidth: 1.5 } }
|
|
914
|
+
{ type: 'line', shape: { x1: hi[0] - cap, y1: hi[1], x2: hi[0] + cap, y2: hi[1] }, style: { stroke: '#236aa4', lineWidth: 1.5 } }
|
|
915
|
+
{ type: 'line', shape: { x1: lo[0] - cap, y1: lo[1], x2: lo[0] + cap, y2: lo[1] }, style: { stroke: '#236aa4', lineWidth: 1.5 } }
|
|
916
|
+
]}
|
|
917
|
+
|
|
918
|
+
@ic 'cholesterolBand',
|
|
919
|
+
xAxis: xaxis type: 'value', name: 'Age (years)', axisLine: { show: true }
|
|
920
|
+
yAxis: yaxis 'Total Cholesterol (mg/dL)', (v) -> "#{v}", min: 80
|
|
921
|
+
tooltip: tip (p) ->
|
|
922
|
+
return 'Trend line' if p.seriesName is 'Trend'
|
|
923
|
+
return '' if p.seriesName is 'Error'
|
|
924
|
+
d = cholesterolWithStd.find(-> it[0] is p.value[0])
|
|
925
|
+
return "Age #{p.value[0]}<br>Avg: #{p.value[1]} mg/dL" unless d and d[2] > 0
|
|
926
|
+
"Age #{d[0]}<br>Avg: #{d[1]} mg/dL<br>\u00b1#{d[2]} std dev"
|
|
927
|
+
legend: { show: false }
|
|
928
|
+
series: [
|
|
929
|
+
{ type: 'custom', name: 'Error', data: cholesterolWithStd, renderItem: errorBarRenderer, z: 1 }
|
|
930
|
+
{ type: 'scatter', name: 'Average', data: cholesterolWithStd.map(-> [it[0], it[1]]), symbolSize: 7, itemStyle: { color: '#236aa4' }, z: 3 }
|
|
931
|
+
{ type: 'line', name: 'Trend', data: [[cr.xMin, cr.slope * cr.xMin + cr.intercept], [cr.xMax, cr.slope * cr.xMax + cr.intercept]], symbol: 'none', lineStyle: { width: 2, type: 'dashed', color: '#8f3d56' }, z: 2, tooltip: { show: false } }
|
|
932
|
+
]
|
|
933
|
+
|
|
934
|
+
# ── Mount ──
|
|
935
|
+
|
|
936
|
+
~>
|
|
937
|
+
requestAnimationFrame ->
|
|
938
|
+
echarts.registerTheme('evidence', THEME)
|
|
939
|
+
@initCoreCharts()
|
|
940
|
+
@initMaps()
|
|
941
|
+
@makeGauge('gauge1', 72.4, 'NPS Score', '{value}', '#236aa4', 100)
|
|
942
|
+
@makeGauge('gauge2', 94.2, 'CSAT', '{value}%', '#46a485', 100)
|
|
943
|
+
@makeGauge('gauge3', 99.8, 'Uptime', '{value}%', '#45a1bf', 100)
|
|
944
|
+
@initHierarchicalCharts()
|
|
945
|
+
@initAnalyticalCharts()
|
|
946
|
+
@initAdditionalCharts()
|
|
947
|
+
|
|
948
|
+
# ── Render ──
|
|
949
|
+
|
|
950
|
+
render
|
|
951
|
+
h1 "ACME Corp Dashboard"
|
|
952
|
+
p class: 'subtitle', "FY 2026 SaaS Metrics \u2014 styled with the evidence.dev ECharts theme"
|
|
953
|
+
|
|
954
|
+
.kpi-row
|
|
955
|
+
for kpi in kpis
|
|
956
|
+
KpiCard
|
|
957
|
+
label: kpi.label, value: kpi.value
|
|
958
|
+
spark: kpi.spark, delta: kpi.delta, up: kpi.up
|
|
959
|
+
|
|
960
|
+
p class: 'section', "Core Charts"
|
|
961
|
+
.grid
|
|
962
|
+
ChartCard title: 'Monthly Revenue', subtitle: 'Total recurring revenue by month', chartId: 'line'
|
|
963
|
+
ChartCard title: 'Revenue by Product', subtitle: 'Stacked area breakdown across product lines', chartId: 'area'
|
|
964
|
+
ChartCard title: 'Quarterly Revenue', subtitle: 'Quarter-over-quarter comparison', chartId: 'bar'
|
|
965
|
+
ChartCard title: 'Revenue by Product \u00d7 Quarter', subtitle: 'Stacked bar with product breakdown', chartId: 'stackedBar'
|
|
966
|
+
ChartCard title: 'Revenue & Margin', subtitle: 'Revenue bars with gross margin % line overlay', chartId: 'combo'
|
|
967
|
+
ChartCard title: 'Revenue & Growth Rate', subtitle: 'Dual-axis: revenue (left) and YoY growth (right)', chartId: 'multiAxis'
|
|
968
|
+
ChartCard title: 'Customers by Segment', subtitle: 'Distribution across customer tiers', chartId: 'pie'
|
|
969
|
+
ChartCard title: 'Deal Size vs. Close Rate', subtitle: 'Each bubble represents a sales rep', chartId: 'scatter'
|
|
970
|
+
ChartCard title: 'Feature Usage Heatmap', subtitle: 'Active sessions by day of week and hour', chartId: 'heatmap'
|
|
971
|
+
ChartCard title: 'Sales Pipeline', subtitle: 'Conversion through funnel stages', chartId: 'funnel'
|
|
972
|
+
ChartCard title: 'Customer Acquisition Flow', subtitle: 'Channel attribution from source to conversion', chartId: 'sankey', wide: true
|
|
973
|
+
|
|
974
|
+
p class: 'section', "Geographic"
|
|
975
|
+
.grid
|
|
976
|
+
ChartCard title: 'Revenue by State', subtitle: 'Annual revenue across US states', chartId: 'choropleth', tall: true
|
|
977
|
+
ChartCard title: 'Customer Locations', subtitle: 'Purchase origins clustered by region (k-means)', chartId: 'scatterMap', tall: true
|
|
978
|
+
|
|
979
|
+
p class: 'section', "KPI Gauges"
|
|
980
|
+
.grid
|
|
981
|
+
div class: 'card wide'
|
|
982
|
+
h2 "Key Performance Indicators"
|
|
983
|
+
p "Net Promoter Score, Customer Satisfaction, and Platform Uptime"
|
|
984
|
+
.gauge-row
|
|
985
|
+
div id: 'gauge1', class: 'gauge-cell'
|
|
986
|
+
div id: 'gauge2', class: 'gauge-cell'
|
|
987
|
+
div id: 'gauge3', class: 'gauge-cell'
|
|
988
|
+
|
|
989
|
+
p class: 'section', "Hierarchical"
|
|
990
|
+
.grid
|
|
991
|
+
ChartCard title: 'Revenue Breakdown', subtitle: 'Treemap by division, product, and feature', chartId: 'treemap'
|
|
992
|
+
ChartCard title: 'Revenue Composition', subtitle: 'Sunburst showing contribution at each level', chartId: 'sunburst'
|
|
993
|
+
|
|
994
|
+
p class: 'section', "Analytical"
|
|
995
|
+
.grid
|
|
996
|
+
ChartCard title: 'Product Comparison', subtitle: 'Multi-metric radar across product lines', chartId: 'radar'
|
|
997
|
+
ChartCard title: 'Revenue Bridge', subtitle: 'Waterfall from Q1 baseline through Q4', chartId: 'waterfall'
|
|
998
|
+
ChartCard title: 'Deal Size Distribution', subtitle: 'Boxplot of closed deal values by quarter ($k)', chartId: 'boxplot'
|
|
999
|
+
ChartCard title: 'Product Metrics', subtitle: 'Parallel coordinates across key dimensions', chartId: 'parallel'
|
|
1000
|
+
ChartCard title: 'Daily Platform Activity', subtitle: 'Login volume over the past year', chartId: 'calendar', wide: true
|
|
1001
|
+
|
|
1002
|
+
p class: 'section', "Additional Chart Types"
|
|
1003
|
+
.grid
|
|
1004
|
+
ChartCard title: 'Response Time Distribution', subtitle: 'Histogram of API response times (ms)', chartId: 'histogram'
|
|
1005
|
+
ChartCard title: 'Feature Adoption', subtitle: 'Horizontal bar chart of feature usage rates', chartId: 'horizBar'
|
|
1006
|
+
ChartCard title: 'Support Volume by Channel', subtitle: 'Polar area chart of ticket counts', chartId: 'polar'
|
|
1007
|
+
ChartCard title: 'Engagement vs. Retention', subtitle: 'Scatter with regression trend by cohort', chartId: 'scatterReg'
|
|
1008
|
+
|
|
1009
|
+
p class: 'section', "Clinical"
|
|
1010
|
+
.grid
|
|
1011
|
+
ChartCard title: 'Hemoglobin A1c Distribution' , subtitle: 'Population distribution of A1c results (green: normal, yellow: pre-diabetic, red: diabetic)', chartId: 'a1c', wide: true
|
|
1012
|
+
ChartCard title: 'Cholesterol by Age (\u00b11\u03c3)', subtitle: 'Average total cholesterol with standard deviation band and trend line', chartId: 'cholesterolBand', wide: true
|
|
1013
|
+
ChartCard title: 'Total Cholesterol by Age' , subtitle: 'Average total cholesterol vs patient age with trend line', chartId: 'cholesterol'
|
|
1014
|
+
</script>
|
|
1015
|
+
|
|
1016
|
+
</body>
|
|
1017
|
+
</html>
|