retold-facto 0.0.4

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.
Files changed (92) hide show
  1. package/.claude/launch.json +11 -0
  2. package/.dockerignore +8 -0
  3. package/.quackage.json +19 -0
  4. package/Dockerfile +26 -0
  5. package/bin/retold-facto.js +909 -0
  6. package/examples/facto-government-data.sqlite +0 -0
  7. package/examples/government-data-catalog.json +137 -0
  8. package/examples/government-data-loader.js +1432 -0
  9. package/package.json +91 -0
  10. package/scripts/facto-download.js +425 -0
  11. package/source/Retold-Facto.js +1042 -0
  12. package/source/services/Retold-Facto-BeaconProvider.js +511 -0
  13. package/source/services/Retold-Facto-CatalogManager.js +1252 -0
  14. package/source/services/Retold-Facto-DataLakeService.js +1642 -0
  15. package/source/services/Retold-Facto-DatasetManager.js +417 -0
  16. package/source/services/Retold-Facto-IngestEngine.js +1315 -0
  17. package/source/services/Retold-Facto-ProjectionEngine.js +3960 -0
  18. package/source/services/Retold-Facto-RecordManager.js +360 -0
  19. package/source/services/Retold-Facto-SchemaManager.js +1110 -0
  20. package/source/services/Retold-Facto-SourceFolderScanner.js +2243 -0
  21. package/source/services/Retold-Facto-SourceManager.js +730 -0
  22. package/source/services/Retold-Facto-StoreConnectionManager.js +441 -0
  23. package/source/services/Retold-Facto-ThroughputMonitor.js +478 -0
  24. package/source/services/web-app/codemirror-entry.js +7 -0
  25. package/source/services/web-app/pict-app/Pict-Application-Facto-Configuration.json +9 -0
  26. package/source/services/web-app/pict-app/Pict-Application-Facto.js +70 -0
  27. package/source/services/web-app/pict-app/Pict-Facto-Bundle.js +11 -0
  28. package/source/services/web-app/pict-app/providers/Pict-Provider-Facto-UI.js +66 -0
  29. package/source/services/web-app/pict-app/providers/Pict-Provider-Facto.js +69 -0
  30. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Catalog.js +93 -0
  31. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Connections.js +42 -0
  32. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Datasets.js +605 -0
  33. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Projections.js +188 -0
  34. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Scanner.js +80 -0
  35. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Schema.js +116 -0
  36. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Sources.js +104 -0
  37. package/source/services/web-app/pict-app/views/PictView-Facto-Catalog.js +526 -0
  38. package/source/services/web-app/pict-app/views/PictView-Facto-Datasets.js +173 -0
  39. package/source/services/web-app/pict-app/views/PictView-Facto-Ingest.js +259 -0
  40. package/source/services/web-app/pict-app/views/PictView-Facto-Layout.js +191 -0
  41. package/source/services/web-app/pict-app/views/PictView-Facto-Projections.js +231 -0
  42. package/source/services/web-app/pict-app/views/PictView-Facto-Records.js +326 -0
  43. package/source/services/web-app/pict-app/views/PictView-Facto-Scanner.js +624 -0
  44. package/source/services/web-app/pict-app/views/PictView-Facto-Sources.js +201 -0
  45. package/source/services/web-app/pict-app/views/PictView-Facto-Throughput.js +456 -0
  46. package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full-Configuration.json +14 -0
  47. package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full.js +391 -0
  48. package/source/services/web-app/pict-app-full/providers/PictRouter-Facto-Configuration.json +56 -0
  49. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-BottomBar.js +68 -0
  50. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Connections.js +340 -0
  51. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboard.js +149 -0
  52. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboards.js +819 -0
  53. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Datasets.js +178 -0
  54. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-IngestJobs.js +99 -0
  55. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Layout.js +62 -0
  56. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-MappingEditor.js +158 -0
  57. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-ProjectionDetail.js +1120 -0
  58. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Projections.js +172 -0
  59. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-QueryPanel.js +119 -0
  60. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-RecordViewer.js +663 -0
  61. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Records.js +648 -0
  62. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Scanner.js +1017 -0
  63. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDetail.js +1404 -0
  64. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDocEditor.js +1036 -0
  65. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaEditor.js +636 -0
  66. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaResearch.js +357 -0
  67. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceDetail.js +822 -0
  68. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceEditor.js +1036 -0
  69. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceResearch.js +487 -0
  70. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Sources.js +165 -0
  71. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Throughput.js +439 -0
  72. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-TopBar.js +335 -0
  73. package/source/services/web-app/pict-app-full/views/projections/Facto-Projections-Constants.js +71 -0
  74. package/source/services/web-app/web/chart.min.js +20 -0
  75. package/source/services/web-app/web/codemirror-bundle.js +30099 -0
  76. package/source/services/web-app/web/css/facto-themes.css +467 -0
  77. package/source/services/web-app/web/css/facto.css +502 -0
  78. package/source/services/web-app/web/index.html +28 -0
  79. package/source/services/web-app/web/retold-facto.js +12138 -0
  80. package/source/services/web-app/web/retold-facto.js.map +1 -0
  81. package/source/services/web-app/web/retold-facto.min.js +2 -0
  82. package/source/services/web-app/web/retold-facto.min.js.map +1 -0
  83. package/source/services/web-app/web/simple/index.html +17 -0
  84. package/test/Facto_Browser_Integration_tests.js +798 -0
  85. package/test/RetoldFacto_tests.js +4117 -0
  86. package/test/fixtures/weather-readings.csv +17 -0
  87. package/test/fixtures/weather-stations.csv +9 -0
  88. package/test/model/MeadowModel-Extended.json +8497 -0
  89. package/test/model/MeadowModel-PICT.json +1 -0
  90. package/test/model/MeadowModel.json +1355 -0
  91. package/test/model/ddl/Facto.ddl +225 -0
  92. package/test/model/fable-configuration.json +14 -0
@@ -0,0 +1,819 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const _ViewConfiguration =
4
+ {
5
+ ViewIdentifier: "Facto-Full-Dashboards",
6
+
7
+ DefaultRenderable: "Facto-Full-Dashboards-Content",
8
+ DefaultDestinationAddress: "#Facto-Full-Content-Container",
9
+
10
+ AutoRender: false,
11
+
12
+ CSS: /*css*/`
13
+ .facto-dashboards-summary
14
+ {
15
+ display: grid;
16
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
17
+ gap: 1em;
18
+ margin-bottom: 2em;
19
+ }
20
+
21
+ .facto-dashboards-stat
22
+ {
23
+ text-align: center;
24
+ padding: 1.2em 0.5em;
25
+ }
26
+
27
+ .facto-dashboards-stat-value
28
+ {
29
+ font-size: 2em;
30
+ font-weight: 700;
31
+ color: var(--facto-text-heading);
32
+ line-height: 1.2;
33
+ }
34
+
35
+ .facto-dashboards-stat-label
36
+ {
37
+ font-size: 0.85em;
38
+ color: var(--facto-text-secondary);
39
+ margin-top: 0.3em;
40
+ }
41
+
42
+ .facto-dashboards-chart-row
43
+ {
44
+ display: grid;
45
+ grid-template-columns: 1fr 1fr;
46
+ gap: 1.5em;
47
+ margin-bottom: 1.5em;
48
+ }
49
+
50
+ @media (max-width: 800px)
51
+ {
52
+ .facto-dashboards-chart-row
53
+ {
54
+ grid-template-columns: 1fr;
55
+ }
56
+ }
57
+
58
+ .facto-dashboards-chart-card
59
+ {
60
+ background: var(--facto-bg-elevated, var(--facto-bg-surface));
61
+ border: 1px solid var(--facto-border);
62
+ border-radius: 8px;
63
+ padding: 1.25em;
64
+ position: relative;
65
+ min-height: 320px;
66
+ }
67
+
68
+ .facto-dashboards-chart-title
69
+ {
70
+ font-size: 0.95em;
71
+ font-weight: 600;
72
+ color: var(--facto-text-heading);
73
+ margin-bottom: 0.75em;
74
+ }
75
+
76
+ .facto-dashboards-chart-wrap
77
+ {
78
+ position: relative;
79
+ width: 100%;
80
+ max-height: 300px;
81
+ }
82
+
83
+ .facto-dashboards-chart-wrap canvas
84
+ {
85
+ width: 100% !important;
86
+ max-height: 300px;
87
+ }
88
+
89
+ .facto-dashboards-loading
90
+ {
91
+ color: var(--facto-text-tertiary);
92
+ font-size: 0.9em;
93
+ text-align: center;
94
+ padding: 4em 0;
95
+ }
96
+
97
+ .facto-dashboards-header-row
98
+ {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 1em;
102
+ flex-wrap: wrap;
103
+ }
104
+
105
+ .facto-dashboards-header-row h1
106
+ {
107
+ margin: 0;
108
+ }
109
+
110
+ .facto-dashboards-updated
111
+ {
112
+ font-size: 0.8em;
113
+ color: var(--facto-text-tertiary);
114
+ }
115
+ `,
116
+
117
+ Templates:
118
+ [
119
+ {
120
+ Hash: "Facto-Full-Dashboards-Template",
121
+ Template: /*html*/`
122
+ <div class="facto-content">
123
+ <div class="facto-content-header">
124
+ <div class="facto-dashboards-header-row">
125
+ <h1>Dashboards</h1>
126
+ <button class="facto-btn facto-btn-secondary" onclick="{~P~}.views['Facto-Full-Dashboards'].refreshAll()">↻ Refresh</button>
127
+ <span class="facto-dashboards-updated" id="Facto-Dashboards-Updated"></span>
128
+ </div>
129
+ <p>Analytics and visualizations across your data warehouse.</p>
130
+ </div>
131
+
132
+ <div class="facto-dashboards-summary" id="Facto-Dashboards-Summary">
133
+ <div class="facto-card facto-dashboards-stat">
134
+ <div class="facto-dashboards-stat-value" id="Facto-Dash-Sources">--</div>
135
+ <div class="facto-dashboards-stat-label">Sources</div>
136
+ </div>
137
+ <div class="facto-card facto-dashboards-stat">
138
+ <div class="facto-dashboards-stat-value" id="Facto-Dash-Datasets">--</div>
139
+ <div class="facto-dashboards-stat-label">Datasets</div>
140
+ </div>
141
+ <div class="facto-card facto-dashboards-stat">
142
+ <div class="facto-dashboards-stat-value" id="Facto-Dash-Records">--</div>
143
+ <div class="facto-dashboards-stat-label">Records</div>
144
+ </div>
145
+ <div class="facto-card facto-dashboards-stat">
146
+ <div class="facto-dashboards-stat-value" id="Facto-Dash-IngestJobs">--</div>
147
+ <div class="facto-dashboards-stat-label">Ingest Jobs</div>
148
+ </div>
149
+ <div class="facto-card facto-dashboards-stat">
150
+ <div class="facto-dashboards-stat-value" id="Facto-Dash-Certainty">--</div>
151
+ <div class="facto-dashboards-stat-label">Certainty Indices</div>
152
+ </div>
153
+ </div>
154
+
155
+ <div class="facto-dashboards-chart-row">
156
+ <div class="facto-dashboards-chart-card">
157
+ <div class="facto-dashboards-chart-title">Records by Dataset</div>
158
+ <div class="facto-dashboards-chart-wrap">
159
+ <canvas id="Facto-Dash-Chart-RecordsByDataset"></canvas>
160
+ </div>
161
+ <div class="facto-dashboards-loading" id="Facto-Dash-Loading-RecordsByDataset">Loading…</div>
162
+ </div>
163
+ <div class="facto-dashboards-chart-card">
164
+ <div class="facto-dashboards-chart-title">Records by Source</div>
165
+ <div class="facto-dashboards-chart-wrap">
166
+ <canvas id="Facto-Dash-Chart-RecordsBySource"></canvas>
167
+ </div>
168
+ <div class="facto-dashboards-loading" id="Facto-Dash-Loading-RecordsBySource">Loading…</div>
169
+ </div>
170
+ </div>
171
+
172
+ <div class="facto-dashboards-chart-row">
173
+ <div class="facto-dashboards-chart-card">
174
+ <div class="facto-dashboards-chart-title">Datasets by Type</div>
175
+ <div class="facto-dashboards-chart-wrap">
176
+ <canvas id="Facto-Dash-Chart-DatasetsByType"></canvas>
177
+ </div>
178
+ <div class="facto-dashboards-loading" id="Facto-Dash-Loading-DatasetsByType">Loading…</div>
179
+ </div>
180
+ <div class="facto-dashboards-chart-card">
181
+ <div class="facto-dashboards-chart-title">Ingest Job Activity</div>
182
+ <div class="facto-dashboards-chart-wrap">
183
+ <canvas id="Facto-Dash-Chart-IngestActivity"></canvas>
184
+ </div>
185
+ <div class="facto-dashboards-loading" id="Facto-Dash-Loading-IngestActivity">Loading…</div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ `
190
+ }
191
+ ],
192
+
193
+ Renderables:
194
+ [
195
+ {
196
+ RenderableHash: "Facto-Full-Dashboards-Content",
197
+ TemplateHash: "Facto-Full-Dashboards-Template",
198
+ DestinationAddress: "#Facto-Full-Content-Container",
199
+ RenderMethod: "replace"
200
+ }
201
+ ]
202
+ };
203
+
204
+ // A small curated palette that works across light and dark themes.
205
+ // The view also reads theme CSS variables at render time for accents.
206
+ const CHART_PALETTE =
207
+ [
208
+ '#18a5a0', '#c44836', '#3a9468', '#6366f1', '#e5a036',
209
+ '#d94882', '#2e86de', '#8b5cf6', '#10b981', '#f59e0b',
210
+ '#ef4444', '#06b6d4', '#a855f7', '#84cc16', '#f97316'
211
+ ];
212
+
213
+ class FactoFullDashboardsView extends libPictView
214
+ {
215
+ constructor(pFable, pOptions, pServiceHash)
216
+ {
217
+ super(pFable, pOptions, pServiceHash);
218
+
219
+ this._chartRecordsByDataset = null;
220
+ this._chartRecordsBySource = null;
221
+ this._chartDatasetsByType = null;
222
+ this._chartIngestActivity = null;
223
+ }
224
+
225
+ onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
226
+ {
227
+ this.loadAllDashboardData();
228
+ return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
229
+ }
230
+
231
+ // ------------------------------------------------------------------
232
+ // Theme helpers
233
+ // ------------------------------------------------------------------
234
+
235
+ getThemeColor(pVarName, pFallback)
236
+ {
237
+ let tmpStyle = getComputedStyle(document.body);
238
+ let tmpVal = tmpStyle.getPropertyValue(pVarName).trim();
239
+ return tmpVal || pFallback || '#888888';
240
+ }
241
+
242
+ getChartPalette(pCount)
243
+ {
244
+ // Build a palette from the curated set, cycling if needed
245
+ let tmpPalette = [];
246
+ for (let i = 0; i < pCount; i++)
247
+ {
248
+ tmpPalette.push(CHART_PALETTE[i % CHART_PALETTE.length]);
249
+ }
250
+ return tmpPalette;
251
+ }
252
+
253
+ getChartDefaults()
254
+ {
255
+ return {
256
+ textColor: this.getThemeColor('--facto-text', '#cccccc'),
257
+ textSecondary: this.getThemeColor('--facto-text-secondary', '#999999'),
258
+ gridColor: this.getThemeColor('--facto-border-subtle', '#333333'),
259
+ borderColor: this.getThemeColor('--facto-border', '#444444')
260
+ };
261
+ }
262
+
263
+ // ------------------------------------------------------------------
264
+ // Formatting helpers
265
+ // ------------------------------------------------------------------
266
+
267
+ formatNumber(pNum)
268
+ {
269
+ if (typeof pNum !== 'number') return '--';
270
+ return pNum.toLocaleString();
271
+ }
272
+
273
+ setStatText(pID, pValue)
274
+ {
275
+ let tmpEl = document.getElementById(pID);
276
+ if (tmpEl) tmpEl.textContent = this.formatNumber(pValue);
277
+ }
278
+
279
+ hideLoading(pID)
280
+ {
281
+ let tmpEl = document.getElementById(pID);
282
+ if (tmpEl) tmpEl.style.display = 'none';
283
+ }
284
+
285
+ // ------------------------------------------------------------------
286
+ // Master data load
287
+ // ------------------------------------------------------------------
288
+
289
+ loadAllDashboardData()
290
+ {
291
+ let tmpProvider = this.pict.providers.Facto;
292
+
293
+ // 1) Summary stats + datasets-by-type
294
+ tmpProvider.loadProjectionSummary().then(
295
+ (pSummary) =>
296
+ {
297
+ if (!pSummary) return;
298
+ this.setStatText('Facto-Dash-Sources', pSummary.Sources);
299
+ this.setStatText('Facto-Dash-Datasets', pSummary.Datasets);
300
+ this.setStatText('Facto-Dash-Records', pSummary.Records);
301
+ this.setStatText('Facto-Dash-IngestJobs', pSummary.IngestJobs);
302
+ this.setStatText('Facto-Dash-Certainty', pSummary.CertaintyIndices);
303
+ this.renderDatasetsByTypeChart(pSummary.DatasetsByType || {});
304
+ }).catch(
305
+ (pErr) =>
306
+ {
307
+ this.pict.log.error('Dashboards: failed to load summary', pErr);
308
+ });
309
+
310
+ // 2) Records by dataset
311
+ tmpProvider.loadDatasets().then(
312
+ () =>
313
+ {
314
+ return tmpProvider.loadDatasetCounts();
315
+ }).then(
316
+ () =>
317
+ {
318
+ this.renderRecordsByDatasetChart();
319
+ }).catch(
320
+ (pErr) =>
321
+ {
322
+ this.pict.log.error('Dashboards: failed to load dataset counts', pErr);
323
+ });
324
+
325
+ // 3) Records by source
326
+ tmpProvider.loadSources().then(
327
+ () =>
328
+ {
329
+ return tmpProvider.loadSourceCounts();
330
+ }).then(
331
+ () =>
332
+ {
333
+ this.renderRecordsBySourceChart();
334
+ }).catch(
335
+ (pErr) =>
336
+ {
337
+ this.pict.log.error('Dashboards: failed to load source counts', pErr);
338
+ });
339
+
340
+ // 4) Ingest job activity
341
+ tmpProvider.loadIngestJobs().then(
342
+ () =>
343
+ {
344
+ this.renderIngestActivityChart();
345
+ }).catch(
346
+ (pErr) =>
347
+ {
348
+ this.pict.log.error('Dashboards: failed to load ingest jobs', pErr);
349
+ });
350
+
351
+ // Update timestamp
352
+ let tmpEl = document.getElementById('Facto-Dashboards-Updated');
353
+ if (tmpEl) tmpEl.textContent = 'Updated ' + new Date().toLocaleTimeString();
354
+ }
355
+
356
+ refreshAll()
357
+ {
358
+ // Destroy existing charts before re-rendering
359
+ this.destroyCharts();
360
+
361
+ // Reset loading indicators
362
+ let tmpLoadingIDs =
363
+ [
364
+ 'Facto-Dash-Loading-RecordsByDataset',
365
+ 'Facto-Dash-Loading-RecordsBySource',
366
+ 'Facto-Dash-Loading-DatasetsByType',
367
+ 'Facto-Dash-Loading-IngestActivity'
368
+ ];
369
+ for (let i = 0; i < tmpLoadingIDs.length; i++)
370
+ {
371
+ let tmpEl = document.getElementById(tmpLoadingIDs[i]);
372
+ if (tmpEl) tmpEl.style.display = '';
373
+ }
374
+
375
+ // Reset stat values
376
+ let tmpStatIDs = ['Facto-Dash-Sources', 'Facto-Dash-Datasets', 'Facto-Dash-Records', 'Facto-Dash-IngestJobs', 'Facto-Dash-Certainty'];
377
+ for (let i = 0; i < tmpStatIDs.length; i++)
378
+ {
379
+ let tmpEl = document.getElementById(tmpStatIDs[i]);
380
+ if (tmpEl) tmpEl.textContent = '--';
381
+ }
382
+
383
+ this.loadAllDashboardData();
384
+ }
385
+
386
+ destroyCharts()
387
+ {
388
+ if (this._chartRecordsByDataset) { this._chartRecordsByDataset.destroy(); this._chartRecordsByDataset = null; }
389
+ if (this._chartRecordsBySource) { this._chartRecordsBySource.destroy(); this._chartRecordsBySource = null; }
390
+ if (this._chartDatasetsByType) { this._chartDatasetsByType.destroy(); this._chartDatasetsByType = null; }
391
+ if (this._chartIngestActivity) { this._chartIngestActivity.destroy(); this._chartIngestActivity = null; }
392
+ }
393
+
394
+ // ------------------------------------------------------------------
395
+ // Chart: Records by Dataset (Doughnut)
396
+ // ------------------------------------------------------------------
397
+
398
+ renderRecordsByDatasetChart()
399
+ {
400
+ this.hideLoading('Facto-Dash-Loading-RecordsByDataset');
401
+ let tmpCanvas = document.getElementById('Facto-Dash-Chart-RecordsByDataset');
402
+ if (!tmpCanvas || typeof Chart === 'undefined') return;
403
+
404
+ let tmpDatasets = this.pict.AppData.Facto.Datasets || [];
405
+
406
+ // Filter to datasets that have records
407
+ let tmpLabels = [];
408
+ let tmpData = [];
409
+ let tmpIDs = [];
410
+ for (let i = 0; i < tmpDatasets.length; i++)
411
+ {
412
+ let tmpCount = tmpDatasets[i].RecordCount || 0;
413
+ if (tmpCount > 0)
414
+ {
415
+ tmpLabels.push(tmpDatasets[i].Name || ('Dataset ' + tmpDatasets[i].IDDataset));
416
+ tmpData.push(tmpCount);
417
+ tmpIDs.push(tmpDatasets[i].IDDataset);
418
+ }
419
+ }
420
+
421
+ if (tmpLabels.length === 0)
422
+ {
423
+ let tmpCard = tmpCanvas.closest('.facto-dashboards-chart-card');
424
+ if (tmpCard)
425
+ {
426
+ let tmpWrap = tmpCard.querySelector('.facto-dashboards-chart-wrap');
427
+ if (tmpWrap) tmpWrap.innerHTML = '<div style="text-align:center; color:var(--facto-text-tertiary); padding:3em 0;">No records associated with datasets yet.</div>';
428
+ }
429
+ return;
430
+ }
431
+
432
+ let tmpColors = this.getChartPalette(tmpLabels.length);
433
+ let tmpDefaults = this.getChartDefaults();
434
+
435
+ if (this._chartRecordsByDataset) this._chartRecordsByDataset.destroy();
436
+ this._chartRecordsByDataset = new Chart(tmpCanvas,
437
+ {
438
+ type: 'doughnut',
439
+ data:
440
+ {
441
+ labels: tmpLabels,
442
+ datasets:
443
+ [{
444
+ data: tmpData,
445
+ backgroundColor: tmpColors,
446
+ borderColor: tmpDefaults.borderColor,
447
+ borderWidth: 1
448
+ }]
449
+ },
450
+ options:
451
+ {
452
+ responsive: true,
453
+ maintainAspectRatio: false,
454
+ plugins:
455
+ {
456
+ legend:
457
+ {
458
+ position: 'bottom',
459
+ labels:
460
+ {
461
+ color: tmpDefaults.textColor,
462
+ font: { size: 11 },
463
+ padding: 8,
464
+ boxWidth: 12
465
+ }
466
+ },
467
+ tooltip:
468
+ {
469
+ callbacks:
470
+ {
471
+ label: function(pContext)
472
+ {
473
+ let tmpLabel = pContext.label || '';
474
+ let tmpValue = pContext.parsed || 0;
475
+ let tmpTotal = pContext.dataset.data.reduce(function(a, b) { return a + b; }, 0);
476
+ let tmpPct = tmpTotal > 0 ? ((tmpValue / tmpTotal) * 100).toFixed(1) : 0;
477
+ return tmpLabel + ': ' + tmpValue.toLocaleString() + ' (' + tmpPct + '%)';
478
+ }
479
+ }
480
+ }
481
+ },
482
+ onClick: (pEvent, pElements) =>
483
+ {
484
+ if (pElements.length > 0)
485
+ {
486
+ let tmpIdx = pElements[0].index;
487
+ let tmpID = tmpIDs[tmpIdx];
488
+ if (tmpID)
489
+ {
490
+ this.pict.PictApplication.navigateTo('/Projection/' + tmpID);
491
+ }
492
+ }
493
+ }
494
+ }
495
+ });
496
+ }
497
+
498
+ // ------------------------------------------------------------------
499
+ // Chart: Records by Source (Horizontal Bar)
500
+ // ------------------------------------------------------------------
501
+
502
+ renderRecordsBySourceChart()
503
+ {
504
+ this.hideLoading('Facto-Dash-Loading-RecordsBySource');
505
+ let tmpCanvas = document.getElementById('Facto-Dash-Chart-RecordsBySource');
506
+ if (!tmpCanvas || typeof Chart === 'undefined') return;
507
+
508
+ let tmpSources = this.pict.AppData.Facto.Sources || [];
509
+ if (tmpSources.length === 0) return;
510
+
511
+ let tmpLabels = [];
512
+ let tmpData = [];
513
+ let tmpIDs = [];
514
+ for (let i = 0; i < tmpSources.length; i++)
515
+ {
516
+ let tmpName = tmpSources[i].Name || ('Source ' + tmpSources[i].IDSource);
517
+ // Truncate long names for the chart axis
518
+ if (tmpName.length > 40) tmpName = tmpName.substring(0, 37) + '…';
519
+ tmpLabels.push(tmpName);
520
+ tmpData.push(tmpSources[i].RecordCount || 0);
521
+ tmpIDs.push(tmpSources[i].IDSource);
522
+ }
523
+
524
+ let tmpColors = this.getChartPalette(tmpLabels.length);
525
+ let tmpDefaults = this.getChartDefaults();
526
+
527
+ if (this._chartRecordsBySource) this._chartRecordsBySource.destroy();
528
+ this._chartRecordsBySource = new Chart(tmpCanvas,
529
+ {
530
+ type: 'bar',
531
+ data:
532
+ {
533
+ labels: tmpLabels,
534
+ datasets:
535
+ [{
536
+ label: 'Records',
537
+ data: tmpData,
538
+ backgroundColor: tmpColors,
539
+ borderColor: tmpDefaults.borderColor,
540
+ borderWidth: 1,
541
+ borderRadius: 3
542
+ }]
543
+ },
544
+ options:
545
+ {
546
+ indexAxis: 'y',
547
+ responsive: true,
548
+ maintainAspectRatio: false,
549
+ plugins:
550
+ {
551
+ legend: { display: false },
552
+ tooltip:
553
+ {
554
+ callbacks:
555
+ {
556
+ label: function(pContext)
557
+ {
558
+ return 'Records: ' + (pContext.parsed.x || 0).toLocaleString();
559
+ }
560
+ }
561
+ }
562
+ },
563
+ scales:
564
+ {
565
+ x:
566
+ {
567
+ beginAtZero: true,
568
+ ticks: { color: tmpDefaults.textSecondary },
569
+ grid: { color: tmpDefaults.gridColor }
570
+ },
571
+ y:
572
+ {
573
+ ticks:
574
+ {
575
+ color: tmpDefaults.textColor,
576
+ font: { size: 11 }
577
+ },
578
+ grid: { display: false }
579
+ }
580
+ },
581
+ onClick: (pEvent, pElements) =>
582
+ {
583
+ if (pElements.length > 0)
584
+ {
585
+ let tmpIdx = pElements[0].index;
586
+ let tmpID = tmpIDs[tmpIdx];
587
+ if (tmpID)
588
+ {
589
+ this.pict.PictApplication.navigateTo('/Source/' + tmpID);
590
+ }
591
+ }
592
+ }
593
+ }
594
+ });
595
+ }
596
+
597
+ // ------------------------------------------------------------------
598
+ // Chart: Datasets by Type (Bar)
599
+ // ------------------------------------------------------------------
600
+
601
+ renderDatasetsByTypeChart(pDatasetsByType)
602
+ {
603
+ this.hideLoading('Facto-Dash-Loading-DatasetsByType');
604
+ let tmpCanvas = document.getElementById('Facto-Dash-Chart-DatasetsByType');
605
+ if (!tmpCanvas || typeof Chart === 'undefined') return;
606
+
607
+ let tmpTypes = ['Raw', 'Compositional', 'Projection', 'Derived'];
608
+ let tmpLabels = tmpTypes.slice();
609
+ let tmpData = [];
610
+ for (let i = 0; i < tmpTypes.length; i++)
611
+ {
612
+ tmpData.push(pDatasetsByType[tmpTypes[i]] || 0);
613
+ }
614
+
615
+ let tmpTypeColors =
616
+ [
617
+ this.getThemeColor('--facto-brand', '#18a5a0'),
618
+ '#6366f1',
619
+ this.getThemeColor('--facto-success', '#3a9468'),
620
+ '#e5a036'
621
+ ];
622
+ let tmpDefaults = this.getChartDefaults();
623
+
624
+ if (this._chartDatasetsByType) this._chartDatasetsByType.destroy();
625
+ this._chartDatasetsByType = new Chart(tmpCanvas,
626
+ {
627
+ type: 'bar',
628
+ data:
629
+ {
630
+ labels: tmpLabels,
631
+ datasets:
632
+ [{
633
+ label: 'Datasets',
634
+ data: tmpData,
635
+ backgroundColor: tmpTypeColors,
636
+ borderColor: tmpDefaults.borderColor,
637
+ borderWidth: 1,
638
+ borderRadius: 4
639
+ }]
640
+ },
641
+ options:
642
+ {
643
+ responsive: true,
644
+ maintainAspectRatio: false,
645
+ plugins:
646
+ {
647
+ legend: { display: false },
648
+ tooltip:
649
+ {
650
+ callbacks:
651
+ {
652
+ label: function(pContext)
653
+ {
654
+ return pContext.label + ': ' + (pContext.parsed.y || 0).toLocaleString();
655
+ }
656
+ }
657
+ }
658
+ },
659
+ scales:
660
+ {
661
+ y:
662
+ {
663
+ beginAtZero: true,
664
+ ticks:
665
+ {
666
+ color: tmpDefaults.textSecondary,
667
+ stepSize: 1,
668
+ precision: 0
669
+ },
670
+ grid: { color: tmpDefaults.gridColor }
671
+ },
672
+ x:
673
+ {
674
+ ticks: { color: tmpDefaults.textColor },
675
+ grid: { display: false }
676
+ }
677
+ }
678
+ }
679
+ });
680
+ }
681
+
682
+ // ------------------------------------------------------------------
683
+ // Chart: Ingest Job Activity (Stacked Bar)
684
+ // ------------------------------------------------------------------
685
+
686
+ renderIngestActivityChart()
687
+ {
688
+ this.hideLoading('Facto-Dash-Loading-IngestActivity');
689
+ let tmpCanvas = document.getElementById('Facto-Dash-Chart-IngestActivity');
690
+ if (!tmpCanvas || typeof Chart === 'undefined') return;
691
+
692
+ let tmpJobs = this.pict.AppData.Facto.IngestJobs || [];
693
+ if (tmpJobs.length === 0)
694
+ {
695
+ // Show empty state text
696
+ let tmpCard = tmpCanvas.closest('.facto-dashboards-chart-card');
697
+ if (tmpCard)
698
+ {
699
+ let tmpWrap = tmpCard.querySelector('.facto-dashboards-chart-wrap');
700
+ if (tmpWrap) tmpWrap.innerHTML = '<div style="text-align:center; color:var(--facto-text-tertiary); padding:3em 0;">No ingest jobs yet.</div>';
701
+ }
702
+ return;
703
+ }
704
+
705
+ // Show the most recent jobs (up to 20), sorted by ID ascending
706
+ let tmpSorted = tmpJobs.slice().sort(
707
+ function(a, b) { return (a.IDIngestJob || 0) - (b.IDIngestJob || 0); }
708
+ );
709
+ if (tmpSorted.length > 20) tmpSorted = tmpSorted.slice(tmpSorted.length - 20);
710
+
711
+ let tmpLabels = [];
712
+ let tmpCreated = [];
713
+ let tmpUpdated = [];
714
+ let tmpErrored = [];
715
+
716
+ for (let i = 0; i < tmpSorted.length; i++)
717
+ {
718
+ let tmpJob = tmpSorted[i];
719
+ let tmpLabel = 'Job ' + (tmpJob.IDIngestJob || '?');
720
+ if (tmpJob.StartDate)
721
+ {
722
+ let tmpDate = new Date(tmpJob.StartDate);
723
+ if (!isNaN(tmpDate.getTime()))
724
+ {
725
+ tmpLabel = (tmpDate.getMonth() + 1) + '/' + tmpDate.getDate();
726
+ }
727
+ }
728
+ tmpLabels.push(tmpLabel);
729
+ tmpCreated.push(tmpJob.RecordsCreated || 0);
730
+ tmpUpdated.push(tmpJob.RecordsUpdated || 0);
731
+ tmpErrored.push(tmpJob.RecordsErrored || 0);
732
+ }
733
+
734
+ let tmpDefaults = this.getChartDefaults();
735
+
736
+ if (this._chartIngestActivity) this._chartIngestActivity.destroy();
737
+ this._chartIngestActivity = new Chart(tmpCanvas,
738
+ {
739
+ type: 'bar',
740
+ data:
741
+ {
742
+ labels: tmpLabels,
743
+ datasets:
744
+ [
745
+ {
746
+ label: 'Created',
747
+ data: tmpCreated,
748
+ backgroundColor: this.getThemeColor('--facto-success', '#3a9468'),
749
+ borderRadius: 2
750
+ },
751
+ {
752
+ label: 'Updated',
753
+ data: tmpUpdated,
754
+ backgroundColor: this.getThemeColor('--facto-brand', '#18a5a0'),
755
+ borderRadius: 2
756
+ },
757
+ {
758
+ label: 'Errored',
759
+ data: tmpErrored,
760
+ backgroundColor: this.getThemeColor('--facto-error', '#c44836'),
761
+ borderRadius: 2
762
+ }
763
+ ]
764
+ },
765
+ options:
766
+ {
767
+ responsive: true,
768
+ maintainAspectRatio: false,
769
+ plugins:
770
+ {
771
+ legend:
772
+ {
773
+ labels:
774
+ {
775
+ color: tmpDefaults.textColor,
776
+ font: { size: 11 },
777
+ boxWidth: 12,
778
+ padding: 10
779
+ }
780
+ },
781
+ tooltip:
782
+ {
783
+ mode: 'index',
784
+ intersect: false
785
+ }
786
+ },
787
+ scales:
788
+ {
789
+ x:
790
+ {
791
+ stacked: true,
792
+ ticks:
793
+ {
794
+ color: tmpDefaults.textSecondary,
795
+ font: { size: 10 },
796
+ maxRotation: 45
797
+ },
798
+ grid: { display: false }
799
+ },
800
+ y:
801
+ {
802
+ stacked: true,
803
+ beginAtZero: true,
804
+ ticks:
805
+ {
806
+ color: tmpDefaults.textSecondary,
807
+ precision: 0
808
+ },
809
+ grid: { color: tmpDefaults.gridColor }
810
+ }
811
+ }
812
+ }
813
+ });
814
+ }
815
+ }
816
+
817
+ module.exports = FactoFullDashboardsView;
818
+
819
+ module.exports.default_configuration = _ViewConfiguration;