victor HF Staff commited on
Commit
ba507da
·
verified ·
1 Parent(s): 169e643

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +357 -204
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Provider Inference Metrics Dashboard</title>
7
  <script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
8
  <style>
9
  :root {
@@ -14,6 +14,9 @@
14
  --border-color: #dee2e6;
15
  --shadow-color: rgba(0, 0, 0, 0.05);
16
  --primary-color: #0d6efd;
 
 
 
17
  --plot-colorway: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']; /* Plotly default */
18
  }
19
  body {
@@ -24,16 +27,36 @@
24
  line-height: 1.5;
25
  }
26
  .container {
27
- max-width: 1600px;
28
  margin: 20px auto;
29
  padding: 0 20px;
30
  }
31
  h1 {
32
  text-align: center;
33
  color: var(--text-color);
34
- margin-bottom: 30px;
35
  font-weight: 500;
36
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  .kpi-container {
38
  display: grid;
39
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
@@ -51,19 +74,20 @@
51
  .kpi-card h3 {
52
  margin-top: 0;
53
  margin-bottom: 10px;
54
- font-size: 1rem;
55
  color: var(--muted-text-color);
56
  font-weight: 400;
57
  }
58
  .kpi-card .value {
59
- font-size: 1.8rem;
60
  font-weight: 600;
61
  color: var(--text-color);
 
62
  }
63
  .kpi-card .unit {
64
- font-size: 0.9rem;
65
  color: var(--muted-text-color);
66
- margin-left: 5px;
67
  }
68
 
69
  .dashboard-container {
@@ -71,7 +95,7 @@
71
  grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); /* Responsive grid */
72
  gap: 25px; /* Space between plots */
73
  }
74
- .plot-container {
75
  background-color: var(--card-bg-color);
76
  padding: 20px;
77
  border-radius: 8px;
@@ -79,13 +103,20 @@
79
  border: 1px solid var(--border-color);
80
  min-height: 450px; /* Ensure plots have some height */
81
  display: flex; /* For centering loading/error inside */
 
82
  justify-content: center;
83
  align-items: center;
84
  }
85
- /* Style Plotly chart container */
86
- .plot-container .plotly {
87
  width: 100%;
88
  height: 100%;
 
 
 
 
 
 
 
89
  }
90
 
91
  #loading, #error {
@@ -96,12 +127,51 @@
96
  color: var(--muted-text-color);
97
  }
98
  #error {
99
- color: #dc3545; /* Bootstrap danger color */
100
  font-weight: 500;
101
  background-color: #f8d7da;
102
  border: 1px solid #f5c2c7;
103
  border-radius: 8px;
104
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  footer {
106
  text-align: center;
107
  margin-top: 40px;
@@ -124,25 +194,33 @@
124
  <div class="container">
125
  <h1>Provider Inference Metrics Dashboard</h1>
126
 
 
 
 
 
 
 
 
 
127
  <div id="loading">Loading data... Please wait.</div>
128
  <div id="error" style="display: none;"></div>
129
 
130
  <!-- KPI Section -->
131
  <div class="kpi-container" style="display: none;">
132
  <div class="kpi-card">
133
- <h3>Total Requests</h3>
134
  <div class="value" id="kpi-total-requests">--</div>
135
  </div>
136
  <div class="kpi-card">
137
- <h3>Success Rate</h3>
138
  <div class="value" id="kpi-success-rate">--<span class="unit">%</span></div>
139
  </div>
140
  <div class="kpi-card">
141
- <h3>Avg. Latency (Overall)</h3>
142
  <div class="value" id="kpi-avg-latency">--<span class="unit">ms</span></div>
143
  </div>
144
  <div class="kpi-card">
145
- <h3>Fastest Provider (Median)</h3>
146
  <div class="value" id="kpi-fastest-provider">--</div>
147
  </div>
148
  </div>
@@ -151,10 +229,16 @@
151
  <div class="dashboard-container" style="display: none;">
152
  <div id="plotLatencyProvider" class="plot-container"></div>
153
  <div id="plotReliabilityProvider" class="plot-container"></div>
154
- <div id="plotLatencyModel" class="plot-container"></div>
155
  <div id="plotErrorTypesProvider" class="plot-container"></div>
156
- <div id="plotLatencyHeatmap" class="plot-container"></div>
157
- <!-- Add more divs here for additional plots -->
 
 
 
 
 
 
158
  </div>
159
 
160
  <footer id="footer" style="display: none;">
@@ -173,43 +257,43 @@
173
  const footer = document.getElementById('footer');
174
  const dataSourceUrlElement = document.getElementById('data-source-url');
175
  const lastUpdatedElement = document.getElementById('last-updated');
 
 
 
 
 
 
 
 
 
 
176
 
177
  dataSourceUrlElement.href = apiUrl; // Set link href
178
 
 
 
 
179
  // Plotly layout defaults
180
  const baseLayout = {
181
- margin: { l: 60, r: 30, b: 100, t: 60, pad: 4 }, // Default margins
182
  legend: { bgcolor: 'rgba(255,255,255,0.5)', bordercolor: '#ccc', borderwidth: 1 },
183
- colorway: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'], // Plotly default color sequence
184
- paper_bgcolor: 'rgba(0,0,0,0)', // Transparent background for plot area
185
- plot_bgcolor: 'rgba(0,0,0,0)', // Transparent background for plotting area
186
  font: {
187
  family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
188
  color: '#212529'
189
  },
190
  title: {
191
  font: { size: 16, weight: '500' },
192
- x: 0.05, // Title alignment
193
- xanchor: 'left'
194
- },
195
- xaxis: {
196
- gridcolor: '#e9ecef', // Lighter grid lines
197
- linecolor: '#adb5bd',
198
- automargin: true,
199
- tickfont: { size: 10 }
200
  },
201
- yaxis: {
202
- gridcolor: '#e9ecef',
203
- linecolor: '#adb5bd',
204
- automargin: true,
205
- tickfont: { size: 10 }
206
- }
207
  };
208
 
209
- // Helper function to deep merge layout options
210
  function mergeLayout(customLayout) {
211
- // Simple deep merge for nested objects like margin, font, title
212
- let layout = JSON.parse(JSON.stringify(baseLayout)); // Deep copy base
213
  for (const key in customLayout) {
214
  if (typeof customLayout[key] === 'object' && customLayout[key] !== null && !Array.isArray(customLayout[key]) && layout[key]) {
215
  Object.assign(layout[key], customLayout[key]);
@@ -220,7 +304,6 @@
220
  return layout;
221
  }
222
 
223
- // Helper function to calculate median
224
  function calculateMedian(arr) {
225
  if (!arr || arr.length === 0) return null;
226
  const sortedArr = [...arr].sort((a, b) => a - b);
@@ -228,45 +311,83 @@
228
  return sortedArr.length % 2 !== 0 ? sortedArr[mid] : (sortedArr[mid - 1] + sortedArr[mid]) / 2;
229
  }
230
 
 
 
 
 
 
 
 
 
 
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  fetch(apiUrl)
233
  .then(response => {
234
- if (!response.ok) {
235
- throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`);
236
- }
237
  return response.json();
238
  })
239
  .then(data => {
240
- loadingDiv.style.display = 'none'; // Hide loading message
241
- kpiContainer.style.display = 'grid'; // Show KPIs
242
- dashboardContainer.style.display = 'grid'; // Show dashboard plots
243
- footer.style.display = 'block'; // Show footer
244
-
245
- // Extract the actual row data
246
- const rows = data.rows.map(item => item.row);
247
- console.log(`Fetched ${rows.length} rows.`);
248
  lastUpdatedElement.textContent = new Date().toLocaleString();
249
 
250
- // --- Calculate and Display KPIs ---
251
- calculateAndDisplayKPIs(rows);
252
 
253
- // --- Data Processing and Plotting ---
254
- createLatencyByProviderPlot(rows);
255
- createReliabilityByProviderPlot(rows);
256
- createLatencyByModelPlot(rows);
257
- createErrorTypesByProviderPlot(rows);
258
- createLatencyHeatmap(rows);
259
 
260
  })
261
  .catch(error => {
262
  console.error('Error fetching or processing data:', error);
263
  loadingDiv.style.display = 'none';
264
- errorDiv.textContent = `Error loading data: ${error.message}. Please check the console for details. Is the dataset server reachable?`;
265
  errorDiv.style.display = 'block';
266
  });
267
 
268
  // --- KPI Calculation Function ---
269
- function calculateAndDisplayKPIs(rows) {
 
 
270
  const totalRequests = rows.length;
271
  const successfulRequests = rows.filter(r => r.response_status_code === 200).length;
272
  const successRate = totalRequests > 0 ? ((successfulRequests / totalRequests) * 100).toFixed(1) : 0;
@@ -278,7 +399,7 @@
278
  ? (validLatencies.reduce((a, b) => a + b, 0) / validLatencies.length).toFixed(0)
279
  : 0;
280
 
281
- // Calculate median latency per provider to find the fastest
282
  const latencyByProvider = {};
283
  rows.forEach(row => {
284
  if (row.duration_ms !== null && row.duration_ms >= 0) {
@@ -300,6 +421,12 @@
300
  }
301
  }
302
 
 
 
 
 
 
 
303
  document.getElementById('kpi-total-requests').textContent = totalRequests;
304
  document.getElementById('kpi-success-rate').innerHTML = `${successRate}<span class="unit">%</span>`;
305
  document.getElementById('kpi-avg-latency').innerHTML = `${avgLatency}<span class="unit">ms</span>`;
@@ -307,169 +434,134 @@
307
  }
308
 
309
 
310
- // --- Plotting Functions ---
311
 
312
- function createLatencyByProviderPlot(rows) {
 
313
  const dataByProvider = {};
314
  rows.forEach(row => {
315
- if (!dataByProvider[row.provider_name]) {
316
- dataByProvider[row.provider_name] = [];
317
- }
318
  if (row.duration_ms !== null && row.duration_ms >= 0) {
319
  dataByProvider[row.provider_name].push(row.duration_ms);
320
  }
321
  });
322
 
323
- const plotData = Object.keys(dataByProvider).sort().map(provider => ({ // Sort providers alphabetically
324
- y: dataByProvider[provider],
325
- type: 'box',
326
- name: provider,
327
- boxpoints: 'Outliers',
328
- marker: { size: 4 }
329
  }));
330
 
331
  const layout = mergeLayout({
332
- title: { text: 'Latency Distribution by Provider' },
333
  yaxis: { title: 'Duration (ms)', type: 'log', autorange: true },
334
  xaxis: { title: 'Provider', tickangle: -30 },
335
- margin: { b: 120 } // More bottom margin for angled labels
336
  });
337
-
338
- Plotly.newPlot('plotLatencyProvider', plotData, layout, {responsive: true});
339
  }
340
 
341
- function createReliabilityByProviderPlot(rows) {
342
- const statusCountsByProvider = {};
343
- const allProviders = new Set();
344
- const allStatusCodes = new Set();
 
345
 
346
- rows.forEach(row => {
347
  const provider = row.provider_name;
348
- const status = row.response_status_code ?? 'Unknown'; // Handle null status
349
- allProviders.add(provider);
350
- allStatusCodes.add(status);
351
-
352
- if (!statusCountsByProvider[provider]) {
353
- statusCountsByProvider[provider] = {};
354
- }
355
- if (!statusCountsByProvider[provider][status]) {
356
- statusCountsByProvider[provider][status] = 0;
357
- }
358
  statusCountsByProvider[provider][status]++;
359
- });
360
 
361
- const sortedProviders = Array.from(allProviders).sort();
362
- // Sort status codes: 200 first, then numerically, then 'Unknown'
363
- const sortedStatusCodes = Array.from(allStatusCodes).sort((a, b) => {
364
- if (a === 200) return -1;
365
- if (b === 200) return 1;
366
- if (a === 'Unknown') return 1;
367
- if (b === 'Unknown') return -1;
368
  return a - b;
369
- });
370
 
371
- const plotData = sortedStatusCodes.map(status => {
372
- return {
373
- x: sortedProviders,
374
- y: sortedProviders.map(provider => statusCountsByProvider[provider]?.[status] || 0),
375
- name: `Status ${status}`,
376
- type: 'bar',
377
- hovertemplate: `Provider: %{x}<br>Status: ${status}<br>Count: %{y}<extra></extra>`
378
- };
379
- });
380
 
381
- const layout = mergeLayout({
382
- title: { text: 'Request Status Codes by Provider' },
383
  barmode: 'stack',
384
  xaxis: { title: 'Provider', tickangle: -30 },
385
  yaxis: { title: 'Number of Requests', autorange: true },
386
  margin: { b: 120 }
387
- });
388
-
389
- Plotly.newPlot('plotReliabilityProvider', plotData, layout, {responsive: true});
390
  }
391
 
392
- function createLatencyByModelPlot(rows) {
393
  const dataByModel = {};
394
  rows.forEach(row => {
395
  const model = row.model_id;
396
- if (!dataByModel[model]) {
397
- dataByModel[model] = [];
398
- }
399
  if (row.duration_ms !== null && row.duration_ms >= 0) {
400
  dataByModel[model].push(row.duration_ms);
401
  }
402
  });
403
 
404
- const plotData = Object.keys(dataByModel).sort().map(model => ({ // Sort models
405
- y: dataByModel[model],
406
- type: 'box',
407
- name: model,
408
- boxpoints: 'Outliers',
409
- marker: { size: 4 }
410
  }));
411
 
412
  const layout = mergeLayout({
413
- title: { text: 'Latency Distribution by Model' },
414
  yaxis: { title: 'Duration (ms)', type: 'log', autorange: true },
415
- xaxis: {
416
- title: 'Model ID',
417
- tickangle: -30 // Angle labels if they overlap
418
- },
419
- margin: { b: 180 } // More bottom margin for potentially long/angled labels
420
  });
421
-
422
- Plotly.newPlot('plotLatencyModel', plotData, layout, {responsive: true});
423
  }
424
 
425
- function createErrorTypesByProviderPlot(rows) {
426
- const errorCountsByProvider = {};
427
- const allProviders = new Set();
428
- const allErrorCodes = new Set();
 
429
 
430
- rows.forEach(row => {
431
- if (row.response_status_code !== 200 && row.response_status_code !== null) { // Only errors
432
  const provider = row.provider_name;
433
  const status = row.response_status_code;
434
- allProviders.add(provider);
435
- allErrorCodes.add(status);
436
-
437
- if (!errorCountsByProvider[provider]) {
438
- errorCountsByProvider[provider] = {};
439
- }
440
- if (!errorCountsByProvider[provider][status]) {
441
- errorCountsByProvider[provider][status] = 0;
442
- }
443
  errorCountsByProvider[provider][status]++;
444
  }
445
- });
446
 
447
- const sortedProviders = Array.from(allProviders).sort();
448
- const sortedErrorCodes = Array.from(allErrorCodes).sort((a, b) => a - b);
449
-
450
- const plotData = sortedErrorCodes.map(status => {
451
- return {
452
- x: sortedProviders,
453
- y: sortedProviders.map(provider => errorCountsByProvider[provider]?.[status] || 0),
454
- name: `Error ${status}`,
455
- type: 'bar',
456
- hovertemplate: `Provider: %{x}<br>Error: ${status}<br>Count: %{y}<extra></extra>`
457
- };
458
- });
459
 
460
- const layout = mergeLayout({
461
- title: { text: 'Error Types by Provider (Non-200 Status)' },
462
- barmode: 'group', // Group bars side-by-side for comparison
 
 
 
 
 
 
 
463
  xaxis: { title: 'Provider', tickangle: -30 },
464
  yaxis: { title: 'Number of Errors', autorange: true },
465
- margin: { b: 120 }
466
- });
467
-
468
- Plotly.newPlot('plotErrorTypesProvider', plotData, layout, {responsive: true});
469
  }
470
 
471
- function createLatencyHeatmap(rows) {
472
- const latencyData = {}; // { provider: { model: { sum: 0, count: 0 } } }
473
  const allProviders = new Set();
474
  const allModels = new Set();
475
 
@@ -479,10 +571,8 @@
479
  const model = row.model_id;
480
  allProviders.add(provider);
481
  allModels.add(model);
482
-
483
  if (!latencyData[provider]) latencyData[provider] = {};
484
  if (!latencyData[provider][model]) latencyData[provider][model] = { sum: 0, count: 0 };
485
-
486
  latencyData[provider][model].sum += row.duration_ms;
487
  latencyData[provider][model].count++;
488
  }
@@ -491,46 +581,109 @@
491
  const sortedProviders = Array.from(allProviders).sort();
492
  const sortedModels = Array.from(allModels).sort();
493
 
494
- const zValues = sortedModels.map(model => {
495
- return sortedProviders.map(provider => {
496
- const data = latencyData[provider]?.[model];
497
- const avg = data && data.count > 0 ? data.sum / data.count : null;
498
- return avg;
499
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  });
501
 
502
- const hoverText = sortedModels.map(model => {
503
- return sortedProviders.map(provider => {
504
- const data = latencyData[provider]?.[model];
505
- const avg = data && data.count > 0 ? (data.sum / data.count).toFixed(0) : 'N/A';
506
- const count = data?.count || 0;
507
- return `Model: ${model}<br>Provider: ${provider}<br>Avg Latency: ${avg} ms<br>Requests: ${count}<extra></extra>`;
 
 
 
 
 
 
508
  });
 
 
 
 
 
 
 
509
  });
510
 
511
 
512
- const plotData = [{
513
- z: zValues,
514
- x: sortedProviders,
515
- y: sortedModels,
516
- type: 'heatmap',
517
- hoverongaps: false,
518
- colorscale: 'Viridis',
519
- reversescale: true, // Often makes sense for latency (lower is better -> brighter)
520
- colorbar: { title: 'Avg Latency (ms)', titleside: 'right', thickness: 15 },
521
- xgap: 2, // Add gaps between cells
522
- ygap: 2,
523
- hovertemplate: hoverText // Custom hover text
524
- }];
525
 
526
- const layout = mergeLayout({
527
- title: { text: 'Average Latency (ms) - Model vs. Provider' },
528
- xaxis: { title: '', side: 'top', tickangle: -30 }, // Move x-axis labels to top, angle
529
- yaxis: { title: '', autorange: 'reversed' }, // Reverse y-axis to match typical matrix layout
530
- margin: { l: 280, r: 50, b: 50, t: 120 } // Adjust margins significantly for labels
531
- });
532
 
533
- Plotly.newPlot('plotLatencyHeatmap', plotData, layout, {responsive: true});
 
 
 
 
 
 
534
  }
535
 
536
  });
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Provider/Model Performance Dashboard</title>
7
  <script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
8
  <style>
9
  :root {
 
14
  --border-color: #dee2e6;
15
  --shadow-color: rgba(0, 0, 0, 0.05);
16
  --primary-color: #0d6efd;
17
+ --success-color: #198754;
18
+ --warning-color: #ffc107;
19
+ --danger-color: #dc3545;
20
  --plot-colorway: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']; /* Plotly default */
21
  }
22
  body {
 
27
  line-height: 1.5;
28
  }
29
  .container {
30
+ max-width: 1700px; /* Wider container */
31
  margin: 20px auto;
32
  padding: 0 20px;
33
  }
34
  h1 {
35
  text-align: center;
36
  color: var(--text-color);
37
+ margin-bottom: 15px;
38
  font-weight: 500;
39
  }
40
+ .controls {
41
+ display: flex;
42
+ justify-content: center;
43
+ align-items: center;
44
+ margin-bottom: 25px;
45
+ gap: 10px;
46
+ }
47
+ .controls label {
48
+ font-weight: 500;
49
+ color: var(--muted-text-color);
50
+ }
51
+ .controls select {
52
+ padding: 8px 12px;
53
+ border: 1px solid var(--border-color);
54
+ border-radius: 6px;
55
+ background-color: var(--card-bg-color);
56
+ min-width: 300px;
57
+ font-size: 1rem;
58
+ }
59
+
60
  .kpi-container {
61
  display: grid;
62
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
 
74
  .kpi-card h3 {
75
  margin-top: 0;
76
  margin-bottom: 10px;
77
+ font-size: 0.95rem; /* Slightly smaller */
78
  color: var(--muted-text-color);
79
  font-weight: 400;
80
  }
81
  .kpi-card .value {
82
+ font-size: 1.7rem; /* Slightly smaller */
83
  font-weight: 600;
84
  color: var(--text-color);
85
+ word-wrap: break-word; /* Prevent long provider names from overflowing */
86
  }
87
  .kpi-card .unit {
88
+ font-size: 0.85rem;
89
  color: var(--muted-text-color);
90
+ margin-left: 4px;
91
  }
92
 
93
  .dashboard-container {
 
95
  grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); /* Responsive grid */
96
  gap: 25px; /* Space between plots */
97
  }
98
+ .plot-container, .table-container {
99
  background-color: var(--card-bg-color);
100
  padding: 20px;
101
  border-radius: 8px;
 
103
  border: 1px solid var(--border-color);
104
  min-height: 450px; /* Ensure plots have some height */
105
  display: flex; /* For centering loading/error inside */
106
+ flex-direction: column; /* Allow title and content stacking */
107
  justify-content: center;
108
  align-items: center;
109
  }
110
+ .plot-container .plotly, .table-container table {
 
111
  width: 100%;
112
  height: 100%;
113
+ flex-grow: 1; /* Allow content to fill space */
114
+ }
115
+ .plot-title { /* Optional: Style for titles inside containers */
116
+ font-weight: 500;
117
+ margin-bottom: 15px;
118
+ color: var(--text-color);
119
+ align-self: flex-start; /* Align title left */
120
  }
121
 
122
  #loading, #error {
 
127
  color: var(--muted-text-color);
128
  }
129
  #error {
130
+ color: var(--danger-color);
131
  font-weight: 500;
132
  background-color: #f8d7da;
133
  border: 1px solid #f5c2c7;
134
  border-radius: 8px;
135
  }
136
+
137
+ /* Table Styles */
138
+ .table-container {
139
+ overflow-x: auto; /* Allow horizontal scrolling on small screens */
140
+ align-items: flex-start; /* Align table top */
141
+ }
142
+ table {
143
+ border-collapse: collapse;
144
+ font-size: 0.9rem;
145
+ }
146
+ th, td {
147
+ padding: 10px 12px;
148
+ text-align: left;
149
+ border-bottom: 1px solid var(--border-color);
150
+ }
151
+ th {
152
+ background-color: var(--bg-color);
153
+ font-weight: 500;
154
+ cursor: pointer; /* Indicate sortable */
155
+ white-space: nowrap;
156
+ }
157
+ th:hover {
158
+ background-color: #e9ecef;
159
+ }
160
+ td {
161
+ white-space: nowrap; /* Prevent wrapping in cells */
162
+ }
163
+ tbody tr:hover {
164
+ background-color: #f1f1f1;
165
+ }
166
+ .error-count {
167
+ color: var(--danger-color);
168
+ font-weight: 500;
169
+ }
170
+ .success-rate-high { color: var(--success-color); font-weight: 500; }
171
+ .success-rate-medium { color: var(--warning-color); font-weight: 500; }
172
+ .success-rate-low { color: var(--danger-color); font-weight: 500; }
173
+
174
+
175
  footer {
176
  text-align: center;
177
  margin-top: 40px;
 
194
  <div class="container">
195
  <h1>Provider Inference Metrics Dashboard</h1>
196
 
197
+ <div class="controls">
198
+ <label for="modelSelector">Select Model:</label>
199
+ <select id="modelSelector">
200
+ <option value="all">-- All Models --</option>
201
+ <!-- Options will be populated by JS -->
202
+ </select>
203
+ </div>
204
+
205
  <div id="loading">Loading data... Please wait.</div>
206
  <div id="error" style="display: none;"></div>
207
 
208
  <!-- KPI Section -->
209
  <div class="kpi-container" style="display: none;">
210
  <div class="kpi-card">
211
+ <h3 id="kpi-title-requests">Total Requests</h3>
212
  <div class="value" id="kpi-total-requests">--</div>
213
  </div>
214
  <div class="kpi-card">
215
+ <h3 id="kpi-title-success">Success Rate</h3>
216
  <div class="value" id="kpi-success-rate">--<span class="unit">%</span></div>
217
  </div>
218
  <div class="kpi-card">
219
+ <h3 id="kpi-title-latency">Avg. Latency</h3>
220
  <div class="value" id="kpi-avg-latency">--<span class="unit">ms</span></div>
221
  </div>
222
  <div class="kpi-card">
223
+ <h3 id="kpi-title-fastest">Fastest Provider (Median)</h3>
224
  <div class="value" id="kpi-fastest-provider">--</div>
225
  </div>
226
  </div>
 
229
  <div class="dashboard-container" style="display: none;">
230
  <div id="plotLatencyProvider" class="plot-container"></div>
231
  <div id="plotReliabilityProvider" class="plot-container"></div>
232
+ <div id="plotLatencyModel" class="plot-container"></div> {/* Will be hidden when filtered */}
233
  <div id="plotErrorTypesProvider" class="plot-container"></div>
234
+ <div id="modelDetailTableContainer" class="table-container" style="display: none;"> {/* Initially hidden */}
235
+ <h3 class="plot-title" id="table-title">Detailed Comparison</h3>
236
+ <table id="modelDetailTable">
237
+ <thead></thead>
238
+ <tbody></tbody>
239
+ </table>
240
+ </div>
241
+ <div id="plotLatencyHeatmap" class="plot-container"></div> {/* Will be hidden when filtered */}
242
  </div>
243
 
244
  <footer id="footer" style="display: none;">
 
257
  const footer = document.getElementById('footer');
258
  const dataSourceUrlElement = document.getElementById('data-source-url');
259
  const lastUpdatedElement = document.getElementById('last-updated');
260
+ const modelSelector = document.getElementById('modelSelector');
261
+
262
+ // Plot containers
263
+ const plotLatencyProviderDiv = document.getElementById('plotLatencyProvider');
264
+ const plotReliabilityProviderDiv = document.getElementById('plotReliabilityProvider');
265
+ const plotLatencyModelDiv = document.getElementById('plotLatencyModel');
266
+ const plotErrorTypesProviderDiv = document.getElementById('plotErrorTypesProvider');
267
+ const plotLatencyHeatmapDiv = document.getElementById('plotLatencyHeatmap');
268
+ const modelDetailTableContainerDiv = document.getElementById('modelDetailTableContainer');
269
+
270
 
271
  dataSourceUrlElement.href = apiUrl; // Set link href
272
 
273
+ let allRows = []; // Store all fetched rows globally
274
+ let uniqueModels = [];
275
+
276
  // Plotly layout defaults
277
  const baseLayout = {
278
+ margin: { l: 60, r: 30, b: 100, t: 60, pad: 4 },
279
  legend: { bgcolor: 'rgba(255,255,255,0.5)', bordercolor: '#ccc', borderwidth: 1 },
280
+ colorway: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'],
281
+ paper_bgcolor: 'rgba(0,0,0,0)',
282
+ plot_bgcolor: 'rgba(0,0,0,0)',
283
  font: {
284
  family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
285
  color: '#212529'
286
  },
287
  title: {
288
  font: { size: 16, weight: '500' },
289
+ x: 0.05, xanchor: 'left'
 
 
 
 
 
 
 
290
  },
291
+ xaxis: { gridcolor: '#e9ecef', linecolor: '#adb5bd', automargin: true, tickfont: { size: 10 } },
292
+ yaxis: { gridcolor: '#e9ecef', linecolor: '#adb5bd', automargin: true, tickfont: { size: 10 } }
 
 
 
 
293
  };
294
 
 
295
  function mergeLayout(customLayout) {
296
+ let layout = JSON.parse(JSON.stringify(baseLayout));
 
297
  for (const key in customLayout) {
298
  if (typeof customLayout[key] === 'object' && customLayout[key] !== null && !Array.isArray(customLayout[key]) && layout[key]) {
299
  Object.assign(layout[key], customLayout[key]);
 
304
  return layout;
305
  }
306
 
 
307
  function calculateMedian(arr) {
308
  if (!arr || arr.length === 0) return null;
309
  const sortedArr = [...arr].sort((a, b) => a - b);
 
311
  return sortedArr.length % 2 !== 0 ? sortedArr[mid] : (sortedArr[mid - 1] + sortedArr[mid]) / 2;
312
  }
313
 
314
+ function populateModelSelector() {
315
+ uniqueModels = [...new Set(allRows.map(r => r.model_id))].sort();
316
+ uniqueModels.forEach(modelId => {
317
+ const option = document.createElement('option');
318
+ option.value = modelId;
319
+ option.textContent = modelId;
320
+ modelSelector.appendChild(option);
321
+ });
322
+ }
323
 
324
+ function updateDashboard(selectedModelId) {
325
+ const filteredRows = selectedModelId === 'all'
326
+ ? allRows
327
+ : allRows.filter(row => row.model_id === selectedModelId);
328
+
329
+ console.log(`Updating dashboard for: ${selectedModelId}, Rows: ${filteredRows.length}`);
330
+
331
+ // Update KPIs
332
+ calculateAndDisplayKPIs(filteredRows, selectedModelId);
333
+
334
+ // Update Plots
335
+ createLatencyByProviderPlot(filteredRows, selectedModelId);
336
+ createReliabilityByProviderPlot(filteredRows, selectedModelId);
337
+ createErrorTypesByProviderPlot(filteredRows, selectedModelId);
338
+
339
+ // Show/Hide plots based on selection
340
+ if (selectedModelId === 'all') {
341
+ plotLatencyModelDiv.style.display = 'flex';
342
+ plotLatencyHeatmapDiv.style.display = 'flex';
343
+ modelDetailTableContainerDiv.style.display = 'none';
344
+ createLatencyByModelPlot(filteredRows); // Only create these for 'all'
345
+ createLatencyHeatmap(filteredRows);
346
+ } else {
347
+ plotLatencyModelDiv.style.display = 'none';
348
+ plotLatencyHeatmapDiv.style.display = 'none';
349
+ modelDetailTableContainerDiv.style.display = 'flex'; // Show table
350
+ createModelDetailTable(filteredRows, selectedModelId);
351
+ }
352
+ }
353
+
354
+ // --- Event Listener ---
355
+ modelSelector.addEventListener('change', (event) => {
356
+ updateDashboard(event.target.value);
357
+ });
358
+
359
+ // --- Initial Fetch ---
360
  fetch(apiUrl)
361
  .then(response => {
362
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status} ${response.statusText}`);
 
 
363
  return response.json();
364
  })
365
  .then(data => {
366
+ allRows = data.rows.map(item => item.row); // Store globally
367
+ console.log(`Fetched ${allRows.length} rows.`);
 
 
 
 
 
 
368
  lastUpdatedElement.textContent = new Date().toLocaleString();
369
 
370
+ populateModelSelector(); // Populate dropdown
 
371
 
372
+ loadingDiv.style.display = 'none';
373
+ kpiContainer.style.display = 'grid';
374
+ dashboardContainer.style.display = 'grid';
375
+ footer.style.display = 'block';
376
+
377
+ updateDashboard('all'); // Initial load with all models
378
 
379
  })
380
  .catch(error => {
381
  console.error('Error fetching or processing data:', error);
382
  loadingDiv.style.display = 'none';
383
+ errorDiv.textContent = `Error loading data: ${error.message}. Please check the console. Is the dataset server reachable?`;
384
  errorDiv.style.display = 'block';
385
  });
386
 
387
  // --- KPI Calculation Function ---
388
+ function calculateAndDisplayKPIs(rows, selectedModelId) {
389
+ const context = selectedModelId === 'all' ? 'Overall' : `(${selectedModelId.split('/').pop()})`; // Shorten model name for title
390
+
391
  const totalRequests = rows.length;
392
  const successfulRequests = rows.filter(r => r.response_status_code === 200).length;
393
  const successRate = totalRequests > 0 ? ((successfulRequests / totalRequests) * 100).toFixed(1) : 0;
 
399
  ? (validLatencies.reduce((a, b) => a + b, 0) / validLatencies.length).toFixed(0)
400
  : 0;
401
 
402
+ // Calculate median latency per provider for the filtered data
403
  const latencyByProvider = {};
404
  rows.forEach(row => {
405
  if (row.duration_ms !== null && row.duration_ms >= 0) {
 
421
  }
422
  }
423
 
424
+ document.getElementById('kpi-title-requests').textContent = `Total Requests ${context}`;
425
+ document.getElementById('kpi-title-success').textContent = `Success Rate ${context}`;
426
+ document.getElementById('kpi-title-latency').textContent = `Avg. Latency ${context}`;
427
+ document.getElementById('kpi-title-fastest').textContent = `Fastest Provider ${context}`;
428
+
429
+
430
  document.getElementById('kpi-total-requests').textContent = totalRequests;
431
  document.getElementById('kpi-success-rate').innerHTML = `${successRate}<span class="unit">%</span>`;
432
  document.getElementById('kpi-avg-latency').innerHTML = `${avgLatency}<span class="unit">ms</span>`;
 
434
  }
435
 
436
 
437
+ // --- Plotting Functions (Modified to accept data and context) ---
438
 
439
+ function createLatencyByProviderPlot(rows, selectedModelId) {
440
+ const titleContext = selectedModelId === 'all' ? '' : `for ${selectedModelId.split('/').pop()}`;
441
  const dataByProvider = {};
442
  rows.forEach(row => {
443
+ if (!dataByProvider[row.provider_name]) dataByProvider[row.provider_name] = [];
 
 
444
  if (row.duration_ms !== null && row.duration_ms >= 0) {
445
  dataByProvider[row.provider_name].push(row.duration_ms);
446
  }
447
  });
448
 
449
+ const plotData = Object.keys(dataByProvider).sort().map(provider => ({
450
+ y: dataByProvider[provider], type: 'box', name: provider, boxpoints: 'Outliers', marker: { size: 4 }
 
 
 
 
451
  }));
452
 
453
  const layout = mergeLayout({
454
+ title: { text: `Latency Distribution by Provider ${titleContext}` },
455
  yaxis: { title: 'Duration (ms)', type: 'log', autorange: true },
456
  xaxis: { title: 'Provider', tickangle: -30 },
457
+ margin: { b: 120 }
458
  });
459
+ Plotly.react(plotLatencyProviderDiv, plotData, layout, {responsive: true}); // Use react for updates
 
460
  }
461
 
462
+ function createReliabilityByProviderPlot(rows, selectedModelId) {
463
+ const titleContext = selectedModelId === 'all' ? '' : `for ${selectedModelId.split('/').pop()}`;
464
+ const statusCountsByProvider = {};
465
+ const providersInSelection = new Set();
466
+ const statusCodesInSelection = new Set();
467
 
468
+ rows.forEach(row => {
469
  const provider = row.provider_name;
470
+ const status = row.response_status_code ?? 'Unknown';
471
+ providersInSelection.add(provider);
472
+ statusCodesInSelection.add(status);
473
+ if (!statusCountsByProvider[provider]) statusCountsByProvider[provider] = {};
474
+ if (!statusCountsByProvider[provider][status]) statusCountsByProvider[provider][status] = 0;
 
 
 
 
 
475
  statusCountsByProvider[provider][status]++;
476
+ });
477
 
478
+ const sortedProviders = Array.from(providersInSelection).sort();
479
+ const sortedStatusCodes = Array.from(statusCodesInSelection).sort((a, b) => {
480
+ if (a === 200) return -1; if (b === 200) return 1;
481
+ if (a === 'Unknown') return 1; if (b === 'Unknown') return -1;
 
 
 
482
  return a - b;
483
+ });
484
 
485
+ const plotData = sortedStatusCodes.map(status => ({
486
+ x: sortedProviders,
487
+ y: sortedProviders.map(provider => statusCountsByProvider[provider]?.[status] || 0),
488
+ name: `Status ${status}`, type: 'bar',
489
+ hovertemplate: `Provider: %{x}<br>Status: ${status}<br>Count: %{y}<extra></extra>`
490
+ }));
 
 
 
491
 
492
+ const layout = mergeLayout({
493
+ title: { text: `Request Status Codes by Provider ${titleContext}` },
494
  barmode: 'stack',
495
  xaxis: { title: 'Provider', tickangle: -30 },
496
  yaxis: { title: 'Number of Requests', autorange: true },
497
  margin: { b: 120 }
498
+ });
499
+ Plotly.react(plotReliabilityProviderDiv, plotData, layout, {responsive: true});
 
500
  }
501
 
502
+ function createLatencyByModelPlot(rows) { // Only shown for 'all'
503
  const dataByModel = {};
504
  rows.forEach(row => {
505
  const model = row.model_id;
506
+ if (!dataByModel[model]) dataByModel[model] = [];
 
 
507
  if (row.duration_ms !== null && row.duration_ms >= 0) {
508
  dataByModel[model].push(row.duration_ms);
509
  }
510
  });
511
 
512
+ const plotData = Object.keys(dataByModel).sort().map(model => ({
513
+ y: dataByModel[model], type: 'box', name: model, boxpoints: 'Outliers', marker: { size: 4 }
 
 
 
 
514
  }));
515
 
516
  const layout = mergeLayout({
517
+ title: { text: 'Latency Distribution by Model (All Providers)' },
518
  yaxis: { title: 'Duration (ms)', type: 'log', autorange: true },
519
+ xaxis: { title: 'Model ID', tickangle: -30 },
520
+ margin: { b: 180 }
 
 
 
521
  });
522
+ Plotly.react(plotLatencyModelDiv, plotData, layout, {responsive: true});
 
523
  }
524
 
525
+ function createErrorTypesByProviderPlot(rows, selectedModelId) {
526
+ const titleContext = selectedModelId === 'all' ? '' : `for ${selectedModelId.split('/').pop()}`;
527
+ const errorCountsByProvider = {};
528
+ const providersInSelection = new Set();
529
+ const errorCodesInSelection = new Set();
530
 
531
+ rows.forEach(row => {
532
+ if (row.response_status_code !== 200 && row.response_status_code !== null) {
533
  const provider = row.provider_name;
534
  const status = row.response_status_code;
535
+ providersInSelection.add(provider);
536
+ errorCodesInSelection.add(status);
537
+ if (!errorCountsByProvider[provider]) errorCountsByProvider[provider] = {};
538
+ if (!errorCountsByProvider[provider][status]) errorCountsByProvider[provider][status] = 0;
 
 
 
 
 
539
  errorCountsByProvider[provider][status]++;
540
  }
541
+ });
542
 
543
+ const sortedProviders = Array.from(providersInSelection).sort();
544
+ const sortedErrorCodes = Array.from(errorCodesInSelection).sort((a, b) => a - b);
 
 
 
 
 
 
 
 
 
 
545
 
546
+ const plotData = sortedErrorCodes.map(status => ({
547
+ x: sortedProviders,
548
+ y: sortedProviders.map(provider => errorCountsByProvider[provider]?.[status] || 0),
549
+ name: `Error ${status}`, type: 'bar',
550
+ hovertemplate: `Provider: %{x}<br>Error: ${status}<br>Count: %{y}<extra></extra>`
551
+ }));
552
+
553
+ const layout = mergeLayout({
554
+ title: { text: `Error Types by Provider (Non-200 Status) ${titleContext}` },
555
+ barmode: 'group',
556
  xaxis: { title: 'Provider', tickangle: -30 },
557
  yaxis: { title: 'Number of Errors', autorange: true },
558
+ margin: { b: 120 }
559
+ });
560
+ Plotly.react(plotErrorTypesProviderDiv, plotData, layout, {responsive: true});
 
561
  }
562
 
563
+ function createLatencyHeatmap(rows) { // Only shown for 'all'
564
+ const latencyData = {};
565
  const allProviders = new Set();
566
  const allModels = new Set();
567
 
 
571
  const model = row.model_id;
572
  allProviders.add(provider);
573
  allModels.add(model);
 
574
  if (!latencyData[provider]) latencyData[provider] = {};
575
  if (!latencyData[provider][model]) latencyData[provider][model] = { sum: 0, count: 0 };
 
576
  latencyData[provider][model].sum += row.duration_ms;
577
  latencyData[provider][model].count++;
578
  }
 
581
  const sortedProviders = Array.from(allProviders).sort();
582
  const sortedModels = Array.from(allModels).sort();
583
 
584
+ const zValues = sortedModels.map(model => sortedProviders.map(provider => {
585
+ const data = latencyData[provider]?.[model];
586
+ return data?.count > 0 ? data.sum / data.count : null;
587
+ }));
588
+ const hoverText = sortedModels.map(model => sortedProviders.map(provider => {
589
+ const data = latencyData[provider]?.[model];
590
+ const avg = data?.count > 0 ? (data.sum / data.count).toFixed(0) : 'N/A';
591
+ const count = data?.count || 0;
592
+ return `Model: ${model}<br>Provider: ${provider}<br>Avg Latency: ${avg} ms<br>Requests: ${count}<extra></extra>`;
593
+ }));
594
+
595
+ const plotData = [{
596
+ z: zValues, x: sortedProviders, y: sortedModels, type: 'heatmap',
597
+ hoverongaps: false, colorscale: 'Viridis', reversescale: true,
598
+ colorbar: { title: 'Avg Latency (ms)', titleside: 'right', thickness: 15 },
599
+ xgap: 2, ygap: 2, hovertemplate: hoverText
600
+ }];
601
+
602
+ const layout = mergeLayout({
603
+ title: { text: 'Average Latency (ms) - Model vs. Provider' },
604
+ xaxis: { title: '', side: 'top', tickangle: -30 },
605
+ yaxis: { title: '', autorange: 'reversed' },
606
+ margin: { l: 280, r: 50, b: 50, t: 120 }
607
+ });
608
+ Plotly.react(plotLatencyHeatmapDiv, plotData, layout, {responsive: true});
609
+ }
610
+
611
+ function createModelDetailTable(rows, selectedModelId) {
612
+ document.getElementById('table-title').textContent = `Detailed Comparison for ${selectedModelId.split('/').pop()}`;
613
+ const tableHead = document.querySelector('#modelDetailTable thead');
614
+ const tableBody = document.querySelector('#modelDetailTable tbody');
615
+ tableHead.innerHTML = ''; // Clear previous header
616
+ tableBody.innerHTML = ''; // Clear previous body
617
+
618
+ const providerStats = {}; // { provider: { total: 0, success: 0, errors: 0, latencies: [] } }
619
+
620
+ rows.forEach(row => {
621
+ const provider = row.provider_name;
622
+ if (!providerStats[provider]) {
623
+ providerStats[provider] = { total: 0, success: 0, errors: 0, latencies: [] };
624
+ }
625
+ providerStats[provider].total++;
626
+ if (row.response_status_code === 200) {
627
+ providerStats[provider].success++;
628
+ } else if (row.response_status_code !== null) {
629
+ providerStats[provider].errors++;
630
+ }
631
+ if (row.duration_ms !== null && row.duration_ms >= 0) {
632
+ providerStats[provider].latencies.push(row.duration_ms);
633
+ }
634
+ });
635
+
636
+ // Create Header
637
+ const headerRow = tableHead.insertRow();
638
+ const headers = ['Provider', 'Median Latency (ms)', 'Success Rate (%)', 'Error Count', 'Total Requests'];
639
+ headers.forEach(text => {
640
+ const th = document.createElement('th');
641
+ th.textContent = text;
642
+ headerRow.appendChild(th);
643
  });
644
 
645
+ // Create Body Rows
646
+ const providerDataArray = [];
647
+ for (const provider in providerStats) {
648
+ const stats = providerStats[provider];
649
+ const medianLatency = calculateMedian(stats.latencies);
650
+ const successRate = stats.total > 0 ? (stats.success / stats.total * 100) : 0;
651
+ providerDataArray.push({
652
+ provider: provider,
653
+ medianLatency: medianLatency,
654
+ successRate: successRate,
655
+ errors: stats.errors,
656
+ total: stats.total
657
  });
658
+ }
659
+
660
+ // Sort initially by median latency (ascending)
661
+ providerDataArray.sort((a, b) => {
662
+ if (a.medianLatency === null) return 1; // Nulls last
663
+ if (b.medianLatency === null) return -1;
664
+ return a.medianLatency - b.medianLatency;
665
  });
666
 
667
 
668
+ providerDataArray.forEach(data => {
669
+ const row = tableBody.insertRow();
670
+ row.insertCell().textContent = data.provider;
671
+ row.insertCell().textContent = data.medianLatency !== null ? data.medianLatency.toFixed(0) : 'N/A';
 
 
 
 
 
 
 
 
 
672
 
673
+ const successCell = row.insertCell();
674
+ successCell.textContent = data.successRate.toFixed(1);
675
+ // Add color coding for success rate
676
+ if (data.successRate >= 95) successCell.className = 'success-rate-high';
677
+ else if (data.successRate >= 80) successCell.className = 'success-rate-medium';
678
+ else successCell.className = 'success-rate-low';
679
 
680
+
681
+ const errorCell = row.insertCell();
682
+ errorCell.textContent = data.errors;
683
+ if (data.errors > 0) errorCell.classList.add('error-count');
684
+
685
+ row.insertCell().textContent = data.total;
686
+ });
687
  }
688
 
689
  });