web-mojo 2.2.57 → 2.2.59

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 (119) hide show
  1. package/dist/admin.cjs.js +1 -1
  2. package/dist/admin.cjs.js.map +1 -1
  3. package/dist/admin.es.js +1 -10105
  4. package/dist/admin.es.js.map +1 -1
  5. package/dist/auth.cjs.js +1 -1
  6. package/dist/auth.es.js +1 -588
  7. package/dist/auth.es.js.map +1 -1
  8. package/dist/charts.cjs.js +1 -1
  9. package/dist/charts.es.js +1 -571
  10. package/dist/charts.es.js.map +1 -1
  11. package/dist/chunks/ChatView-D4A9rIX3.js +2 -0
  12. package/dist/chunks/ChatView-D4A9rIX3.js.map +1 -0
  13. package/dist/chunks/ChatView-nxaq8aIo.js +2 -0
  14. package/dist/chunks/ChatView-nxaq8aIo.js.map +1 -0
  15. package/dist/chunks/Collection-1sPoIFvQ.js +2 -0
  16. package/dist/chunks/{Collection-DaiL0uGl.js.map → Collection-1sPoIFvQ.js.map} +1 -1
  17. package/dist/chunks/{Collection-CxbNKOas.js → Collection-DSBRXpwK.js} +2 -2
  18. package/dist/chunks/{Collection-CxbNKOas.js.map → Collection-DSBRXpwK.js.map} +1 -1
  19. package/dist/chunks/{ContextMenu-ClwHEbbD.js → ContextMenu-BWy7WqF4.js} +2 -2
  20. package/dist/chunks/{ContextMenu-ClwHEbbD.js.map → ContextMenu-BWy7WqF4.js.map} +1 -1
  21. package/dist/chunks/ContextMenu-BvniQz-N.js +3 -0
  22. package/dist/chunks/{ContextMenu-sgvgSACY.js.map → ContextMenu-BvniQz-N.js.map} +1 -1
  23. package/dist/chunks/DataView--nUWtq6r.js +2 -0
  24. package/dist/chunks/{DataView-Dzo0jbs2.js.map → DataView--nUWtq6r.js.map} +1 -1
  25. package/dist/chunks/{DataView-1xh3GFeC.js → DataView-CK3Z0TJH.js} +2 -2
  26. package/dist/chunks/{DataView-1xh3GFeC.js.map → DataView-CK3Z0TJH.js.map} +1 -1
  27. package/dist/chunks/Dialog-BcgSR01Z.js +2 -0
  28. package/dist/chunks/{Dialog-DOGDalUq.js.map → Dialog-BcgSR01Z.js.map} +1 -1
  29. package/dist/chunks/{Dialog-CQlTDhZS.js → Dialog-DwCTFV6O.js} +2 -2
  30. package/dist/chunks/{Dialog-CQlTDhZS.js.map → Dialog-DwCTFV6O.js.map} +1 -1
  31. package/dist/chunks/FormPlugins-DvQ-G5J5.js +2 -0
  32. package/dist/chunks/{FormPlugins-DY6e88YT.js.map → FormPlugins-DvQ-G5J5.js.map} +1 -1
  33. package/dist/chunks/{FormView-DaKA4Sys.js → FormView-CRmEReTC.js} +3 -3
  34. package/dist/chunks/{FormView-DaKA4Sys.js.map → FormView-CRmEReTC.js.map} +1 -1
  35. package/dist/chunks/FormView-OLA7t-yv.js +3 -0
  36. package/dist/chunks/{FormView-Dz3mYasQ.js.map → FormView-OLA7t-yv.js.map} +1 -1
  37. package/dist/chunks/ListView-6JQ6tRXs.js +2 -0
  38. package/dist/chunks/{ListView-X5w5jf51.js.map → ListView-6JQ6tRXs.js.map} +1 -1
  39. package/dist/chunks/{ListView-CDzKIpd8.js → ListView-DVStKiMi.js} +2 -2
  40. package/dist/chunks/{ListView-CDzKIpd8.js.map → ListView-DVStKiMi.js.map} +1 -1
  41. package/dist/chunks/{MetricsCountryMapView-Dx2cw7ya.js → MetricsCountryMapView-CnAEbUw_.js} +2 -2
  42. package/dist/chunks/{MetricsCountryMapView-Dx2cw7ya.js.map → MetricsCountryMapView-CnAEbUw_.js.map} +1 -1
  43. package/dist/chunks/MetricsCountryMapView-J067qrrt.js +2 -0
  44. package/dist/chunks/{MetricsCountryMapView-B2xz6zUw.js.map → MetricsCountryMapView-J067qrrt.js.map} +1 -1
  45. package/dist/chunks/{MetricsMiniChartWidget-CBuso0OE.js → MetricsMiniChartWidget-BeD1slGs.js} +2 -2
  46. package/dist/chunks/{MetricsMiniChartWidget-CBuso0OE.js.map → MetricsMiniChartWidget-BeD1slGs.js.map} +1 -1
  47. package/dist/chunks/MetricsMiniChartWidget-x2gFjHOU.js +2 -0
  48. package/dist/chunks/{MetricsMiniChartWidget-DvKd7Qrk.js.map → MetricsMiniChartWidget-x2gFjHOU.js.map} +1 -1
  49. package/dist/chunks/PDFViewer-CsyKn-gh.js +2 -0
  50. package/dist/chunks/{PDFViewer-EJ9cOfPF.js.map → PDFViewer-CsyKn-gh.js.map} +1 -1
  51. package/dist/chunks/{PDFViewer-ofMGdSaj.js → PDFViewer-DSa4BZCm.js} +2 -2
  52. package/dist/chunks/{PDFViewer-ofMGdSaj.js.map → PDFViewer-DSa4BZCm.js.map} +1 -1
  53. package/dist/chunks/Rest-DHbszkuP.js +2 -0
  54. package/dist/chunks/Rest-DHbszkuP.js.map +1 -0
  55. package/dist/chunks/Rest-Ds9e8tN8.js +2 -0
  56. package/dist/chunks/Rest-Ds9e8tN8.js.map +1 -0
  57. package/dist/chunks/TokenManager-D6SjKgPZ.js +2 -0
  58. package/dist/chunks/{TokenManager-DoN9e6q6.js.map → TokenManager-D6SjKgPZ.js.map} +1 -1
  59. package/dist/chunks/{TokenManager-Gqvj7SDX.js → TokenManager-REbha1Le.js} +2 -2
  60. package/dist/chunks/{TokenManager-Gqvj7SDX.js.map → TokenManager-REbha1Le.js.map} +1 -1
  61. package/dist/chunks/WebApp-CULZpO_0.js +2 -0
  62. package/dist/chunks/{WebApp-6qvqmOts.js.map → WebApp-CULZpO_0.js.map} +1 -1
  63. package/dist/chunks/{WebApp-_dgpwtFw.js → WebApp-DovLtA60.js} +2 -2
  64. package/dist/chunks/{WebApp-_dgpwtFw.js.map → WebApp-DovLtA60.js.map} +1 -1
  65. package/dist/chunks/WebSocketClient-B-wc3mez.js +2 -0
  66. package/dist/chunks/{WebSocketClient-DG2olXpH.js.map → WebSocketClient-B-wc3mez.js.map} +1 -1
  67. package/dist/chunks/{WebSocketClient-MFkFlSue.js → WebSocketClient-BdZ9QYll.js} +2 -2
  68. package/dist/chunks/{WebSocketClient-MFkFlSue.js.map → WebSocketClient-BdZ9QYll.js.map} +1 -1
  69. package/dist/chunks/version-C3dnl1bg.js +2 -0
  70. package/dist/chunks/version-C3dnl1bg.js.map +1 -0
  71. package/dist/chunks/{version-BVADfTA5.js → version-ioN546cp.js} +2 -2
  72. package/dist/chunks/{version-BVADfTA5.js.map → version-ioN546cp.js.map} +1 -1
  73. package/dist/css/web-mojo.css +1 -1
  74. package/dist/docit.cjs.js +1 -1
  75. package/dist/docit.es.js +1 -957
  76. package/dist/docit.es.js.map +1 -1
  77. package/dist/index.cjs.js +1 -1
  78. package/dist/index.es.js +1 -3252
  79. package/dist/index.es.js.map +1 -1
  80. package/dist/lightbox.cjs.js +1 -1
  81. package/dist/lightbox.es.js +1 -3737
  82. package/dist/lightbox.es.js.map +1 -1
  83. package/dist/loader.umd.js +2 -2
  84. package/dist/map.cjs.js +1 -1
  85. package/dist/map.es.js +1 -1032
  86. package/dist/map.es.js.map +1 -1
  87. package/dist/mojo-auth.es.js +338 -0
  88. package/dist/mojo-auth.umd.js +1 -0
  89. package/dist/timeline.cjs.js +1 -1
  90. package/dist/timeline.es.js +1 -224
  91. package/dist/timeline.es.js.map +1 -1
  92. package/dist/web-mojo.lite.iife.js +14 -3
  93. package/dist/web-mojo.lite.iife.js.map +1 -1
  94. package/dist/web-mojo.lite.iife.min.js +6 -6
  95. package/dist/web-mojo.lite.iife.min.js.map +1 -1
  96. package/package.json +2 -2
  97. package/dist/chunks/ChatView-9k6xBWXk.js +0 -7632
  98. package/dist/chunks/ChatView-9k6xBWXk.js.map +0 -1
  99. package/dist/chunks/ChatView-CdtuCDYm.js +0 -2
  100. package/dist/chunks/ChatView-CdtuCDYm.js.map +0 -1
  101. package/dist/chunks/Collection-DaiL0uGl.js +0 -1014
  102. package/dist/chunks/ContextMenu-sgvgSACY.js +0 -1535
  103. package/dist/chunks/DataView-Dzo0jbs2.js +0 -862
  104. package/dist/chunks/Dialog-DOGDalUq.js +0 -1579
  105. package/dist/chunks/FormPlugins-DY6e88YT.js +0 -124
  106. package/dist/chunks/FormView-Dz3mYasQ.js +0 -8636
  107. package/dist/chunks/ListView-X5w5jf51.js +0 -495
  108. package/dist/chunks/MetricsCountryMapView-B2xz6zUw.js +0 -1054
  109. package/dist/chunks/MetricsMiniChartWidget-DvKd7Qrk.js +0 -3283
  110. package/dist/chunks/PDFViewer-EJ9cOfPF.js +0 -946
  111. package/dist/chunks/Rest-CgSjfMaU.js +0 -2
  112. package/dist/chunks/Rest-CgSjfMaU.js.map +0 -1
  113. package/dist/chunks/Rest-W-sPfGh9.js +0 -4375
  114. package/dist/chunks/Rest-W-sPfGh9.js.map +0 -1
  115. package/dist/chunks/TokenManager-DoN9e6q6.js +0 -1423
  116. package/dist/chunks/WebApp-6qvqmOts.js +0 -1386
  117. package/dist/chunks/WebSocketClient-DG2olXpH.js +0 -209
  118. package/dist/chunks/version-OyPGnx30.js +0 -38
  119. package/dist/chunks/version-OyPGnx30.js.map +0 -1
@@ -1,3283 +0,0 @@
1
- import Dialog from "./Dialog-DOGDalUq.js";
2
- import { V as View, d as dataFormatter } from "./Rest-W-sPfGh9.js";
3
- import { W as WebSocketClient } from "./WebSocketClient-DG2olXpH.js";
4
- class BaseChart extends View {
5
- constructor(options = {}) {
6
- super({
7
- ...options,
8
- className: `chart-component ${options.className || ""}`,
9
- tagName: "div"
10
- });
11
- this.chart = null;
12
- this.chartType = options.chartType || "line";
13
- this.endpoint = options.endpoint || null;
14
- this.data = options.data || null;
15
- this.dataTransform = options.dataTransform || null;
16
- this.refreshInterval = options.refreshInterval || null;
17
- this.autoRefresh = options.autoRefresh !== false;
18
- this.refreshTimer = null;
19
- this.websocketUrl = options.websocketUrl || null;
20
- this.websocket = null;
21
- this.websocketReconnect = options.websocketReconnect !== false;
22
- this.width = options.width || null;
23
- this.height = options.height || null;
24
- this.contentStyle = [
25
- this.width ? `width: ${this.width}px;` : "",
26
- this.height ? `height: ${this.height}px;` : ""
27
- ].filter(Boolean).join(" ");
28
- if (options.maintainAspectRatio === void 0) {
29
- options.maintainAspectRatio = true;
30
- }
31
- this.title = options.title || "";
32
- this.chartTitle = options.chartTitle || "";
33
- this.chartOptions = {
34
- responsive: true,
35
- maintainAspectRatio: options.maintainAspectRatio,
36
- interaction: {
37
- intersect: false,
38
- mode: "index"
39
- },
40
- plugins: {
41
- legend: {
42
- display: options.showLegend !== false,
43
- position: options.legendPosition || "top"
44
- },
45
- title: {
46
- display: !!this.chartTitle,
47
- text: this.chartTitle
48
- },
49
- tooltip: {
50
- enabled: options.showTooltips !== false,
51
- backgroundColor: "rgba(0,0,0,0.8)",
52
- titleColor: "#fff",
53
- bodyColor: "#fff",
54
- borderColor: "rgba(255,255,255,0.1)",
55
- borderWidth: 1
56
- }
57
- },
58
- ...options.chartOptions
59
- };
60
- this.xAxis = options.xAxis || null;
61
- this.yAxis = options.yAxis || null;
62
- this.tooltipFormatters = options.tooltip || {};
63
- this.theme = options.theme || "light";
64
- this.colorScheme = options.colorScheme || "default";
65
- this.animations = options.animations !== false;
66
- this.exportEnabled = options.exportEnabled === true;
67
- this.exportFormats = options.exportFormats || ["png", "jpg", "csv"];
68
- this.isLoading = false;
69
- this.hasError = false;
70
- this.lastFetch = null;
71
- this.dataPoints = 0;
72
- this.canvas = null;
73
- this.chartJsCdn = options.chartJsCdn || "https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js";
74
- this.dataFormatter = dataFormatter;
75
- this._essentialListeners = [];
76
- }
77
- get refreshEnabled() {
78
- return !!(this.endpoint || this.websocketUrl);
79
- }
80
- buildDefaultHeaderConfig() {
81
- return {
82
- titleHtml: this.title || "",
83
- chartTitle: this.chartTitle || "",
84
- showExport: this.exportEnabled === true,
85
- showRefresh: this.refreshEnabled,
86
- showTheme: true,
87
- controls: []
88
- };
89
- }
90
- async getTemplate() {
91
- return `
92
- <div class="chart-container" data-theme="{{theme}}">
93
- <div class="chart-header mb-3">
94
- <div data-container="header"></div>
95
- <div class="chart-header-aux mt-2">
96
- <div data-container="header-aux"></div>
97
- </div>
98
- </div>
99
-
100
- <div class="chart-content position-relative" {{#contentStyle}}style="{{contentStyle}}"{{/contentStyle}}>
101
- <canvas class="chart-canvas" data-container="canvas"></canvas>
102
-
103
- <!-- Loading overlay -->
104
- <div class="chart-overlay d-none" data-loading>
105
- <div class="d-flex flex-column align-items-center">
106
- <div class="spinner-border text-primary mb-2" role="status">
107
- <span class="visually-hidden">Loading...</span>
108
- </div>
109
- <small class="text-muted">Loading chart data...</small>
110
- </div>
111
- </div>
112
-
113
- <!-- Error overlay -->
114
- <div class="chart-overlay d-none" data-error>
115
- <div class="alert alert-danger mb-0" role="alert">
116
- <div class="d-flex align-items-center">
117
- <i class="bi bi-exclamation-triangle me-2"></i>
118
- <div class="flex-grow-1">
119
- <strong>Error:</strong> <span class="error-message">Failed to load chart data</span>
120
- </div>
121
- <button class="btn btn-sm btn-outline-danger ms-2" data-action="retry-load">
122
- <i class="bi bi-arrow-clockwise"></i> Retry
123
- </button>
124
- </div>
125
- </div>
126
- </div>
127
-
128
- <!-- No data overlay -->
129
- <div class="chart-overlay d-none" data-no-data>
130
- <div class="text-center text-muted">
131
- <i class="bi bi-bar-chart display-4 mb-3 opacity-50"></i>
132
- <p class="mb-0">No data available</p>
133
- {{#refreshEnabled}}
134
- <button class="btn btn-sm btn-outline-secondary mt-2" data-action="refresh-chart">
135
- <i class="bi bi-arrow-clockwise"></i> Refresh
136
- </button>
137
- {{/refreshEnabled}}
138
- </div>
139
- </div>
140
-
141
- <!-- WebSocket status indicator -->
142
- <div class="position-absolute top-0 end-0 mt-2 me-2">
143
- <span class="badge bg-success websocket-status" style="display: none;" data-websocket-status>
144
- <i class="bi bi-wifi"></i> Live
145
- </span>
146
- </div>
147
- </div>
148
-
149
- <div class="chart-footer mt-2" style="display: none;">
150
- <div class="row">
151
- <div class="col">
152
- <small class="text-muted">
153
- <i class="bi bi-graph-up me-1"></i>
154
- <span class="data-points">0 data points</span>
155
- </small>
156
- </div>
157
- <div class="col text-end">
158
- <small class="text-muted refresh-info">
159
- Auto-refresh: <span class="refresh-status">Off</span>
160
- </small>
161
- </div>
162
- </div>
163
- </div>
164
- </div>
165
- `;
166
- }
167
- async onInit() {
168
- await this.initializeChartJS();
169
- try {
170
- const headerConfig = this.headerConfig || (this.buildDefaultHeaderConfig ? this.buildDefaultHeaderConfig() : null);
171
- if (headerConfig) {
172
- this.headerView = new ChartHeaderView({ ...headerConfig, containerId: "header" });
173
- this.addChild(this.headerView);
174
- }
175
- } catch (e) {
176
- console.debug("ChartHeaderView not available:", e?.message);
177
- }
178
- }
179
- async onAfterRender() {
180
- this.canvas = this.element.querySelector(".chart-canvas");
181
- this.titleElement = this.element.querySelector(".chart-title");
182
- this.contentElement = this.element.querySelector(".chart-content");
183
- this.footerElement = this.element.querySelector(".chart-footer");
184
- this.loadingOverlay = this.element.querySelector("[data-loading]");
185
- this.errorOverlay = this.element.querySelector("[data-error]");
186
- this.noDataOverlay = this.element.querySelector("[data-no-data]");
187
- this.websocketStatus = this.element.querySelector("[data-websocket-status]");
188
- this.refreshBtn = this.element.querySelector(".refresh-btn");
189
- this.themeToggle = this.element.querySelector(".theme-toggle");
190
- this.applyTheme();
191
- if (this.endpoint) {
192
- await this.fetchData();
193
- await this.updateChart(this.data, true);
194
- if (this.height || this.width) {
195
- this._updateChartDimensions();
196
- }
197
- } else if (this.data) {
198
- await this.updateChart(this.data, true);
199
- if (this.height || this.width) {
200
- this._updateChartDimensions();
201
- }
202
- } else {
203
- this.showNoData();
204
- }
205
- if (this.autoRefresh && this.refreshInterval && this.endpoint) {
206
- this.startAutoRefresh();
207
- }
208
- if (this.websocketUrl) {
209
- await this.connectWebSocket();
210
- }
211
- this.setupResizeObserver();
212
- this.showFooter();
213
- }
214
- async initializeChartJS() {
215
- try {
216
- if (typeof window.Chart === "undefined") {
217
- await this.loadChartJS();
218
- }
219
- return true;
220
- } catch (error) {
221
- console.error("Failed to initialize Chart.js:", error);
222
- this.showError("Failed to initialize charting library");
223
- return false;
224
- }
225
- }
226
- async loadChartJS() {
227
- return new Promise((resolve, reject) => {
228
- const script = document.createElement("script");
229
- script.src = this.chartJsCdn;
230
- script.onload = () => {
231
- console.log("Chart.js loaded successfully");
232
- resolve();
233
- };
234
- script.onerror = () => {
235
- reject(new Error("Failed to load Chart.js"));
236
- };
237
- document.head.appendChild(script);
238
- });
239
- }
240
- // Action Handlers (EventDelegate)
241
- async handleActionRefreshChart() {
242
- await this.fetchData();
243
- }
244
- async handleActionRetryLoad() {
245
- this.hideError();
246
- await this.fetchData();
247
- }
248
- async handleActionExportChart(event, element) {
249
- const format = element.getAttribute("data-format") || "png";
250
- this.exportChart(format);
251
- }
252
- async handleActionToggleTheme() {
253
- this.toggleTheme();
254
- }
255
- async handleActionSetChartType(event, element) {
256
- const type = element.getAttribute("data-type");
257
- if (type && this.setChartType) {
258
- await this.setChartType(type);
259
- }
260
- }
261
- // Data Management
262
- async fetchData() {
263
- if (!this.endpoint) return;
264
- this.showLoading();
265
- this.setRefreshButtonState(true);
266
- try {
267
- const response = await fetch(this.endpoint, {
268
- method: "GET",
269
- headers: {
270
- "Content-Type": "application/json",
271
- "Accept": "application/json"
272
- }
273
- });
274
- if (!response.ok) {
275
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
276
- }
277
- let data = await response.json();
278
- if (this.dataTransform && typeof this.dataTransform === "function") {
279
- data = this.dataTransform(data);
280
- }
281
- this.lastFetch = /* @__PURE__ */ new Date();
282
- this.data = data;
283
- this.updateLastUpdatedTime();
284
- const eventBus = this.getApp()?.events;
285
- if (eventBus) {
286
- eventBus.emit("chart:data-loaded", {
287
- chart: this,
288
- data,
289
- source: "http",
290
- endpoint: this.endpoint
291
- });
292
- }
293
- } catch (error) {
294
- console.error("Failed to fetch chart data:", error);
295
- this.showError(`Failed to load data: ${error.message}`);
296
- this.emit("chart:error", {
297
- chart: this,
298
- error,
299
- source: "http",
300
- endpoint: this.endpoint
301
- });
302
- } finally {
303
- this.hideLoading();
304
- this.setRefreshButtonState(false);
305
- }
306
- }
307
- async connectWebSocket() {
308
- if (!this.websocketUrl) return;
309
- try {
310
- this.websocket = new WebSocketClient({
311
- url: this.websocketUrl,
312
- autoReconnect: this.websocketReconnect,
313
- dataTransform: this.dataTransform,
314
- eventBus: this.getApp()?.events,
315
- debug: false
316
- });
317
- this.websocket.on("connected", () => {
318
- this.showWebSocketStatus(true);
319
- console.log("WebSocket connected for chart data");
320
- });
321
- this.websocket.on("disconnected", () => {
322
- this.showWebSocketStatus(false);
323
- console.log("WebSocket disconnected");
324
- });
325
- this.websocket.on("data", async (data) => {
326
- await this.updateChart(data);
327
- this.updateLastUpdatedTime();
328
- this.emit("chart:data-updated", {
329
- chart: this,
330
- data,
331
- source: "websocket"
332
- });
333
- });
334
- this.websocket.on("error", (error) => {
335
- console.error("WebSocket error:", error);
336
- this.showWebSocketStatus(false, "error");
337
- });
338
- await this.websocket.connect();
339
- } catch (error) {
340
- console.error("Failed to connect WebSocket:", error);
341
- this.showWebSocketStatus(false, "error");
342
- }
343
- }
344
- async updateChart(data, recreate = false) {
345
- if (!data) {
346
- this.showNoData();
347
- return;
348
- }
349
- this.data = data;
350
- if (!this.canvas || typeof window.Chart === "undefined") {
351
- return;
352
- }
353
- this.hideAllOverlays();
354
- const processedData = this.processChartData(data);
355
- if (recreate && this.chart) {
356
- this.chart.destroy();
357
- this.chart = null;
358
- }
359
- if (this.chart) {
360
- this.chart.data = processedData;
361
- this.chart.update("none");
362
- } else {
363
- await this.createChart(processedData);
364
- }
365
- this.updateDataStats(processedData);
366
- if (this.height || this.width) {
367
- this._updateChartDimensions();
368
- }
369
- }
370
- processChartData(data) {
371
- let processedData = { ...data };
372
- const xAxisCfg = this.normalizeAxis(this.xAxis);
373
- if (xAxisCfg && xAxisCfg.formatter && processedData.labels) {
374
- processedData.labels = processedData.labels.map(
375
- (label) => this.dataFormatter.pipe(label, xAxisCfg.formatter)
376
- );
377
- }
378
- return processedData;
379
- }
380
- async createChart(data) {
381
- if (!this.canvas || typeof window.Chart === "undefined") {
382
- throw new Error("Chart.js not loaded or canvas not found");
383
- }
384
- const config = {
385
- type: this.chartType,
386
- data,
387
- options: this.buildChartOptions()
388
- };
389
- try {
390
- this.chart = new window.Chart(this.canvas, config);
391
- this.setupChartEventHandlers();
392
- } catch (error) {
393
- console.error("Failed to create chart:", error);
394
- throw error;
395
- }
396
- }
397
- buildChartOptions() {
398
- const options = { ...this.chartOptions };
399
- if (this.width || this.height) {
400
- options.responsive = true;
401
- options.maintainAspectRatio = false;
402
- }
403
- const xAxisCfg = this.normalizeAxis(this.xAxis);
404
- const yAxisCfg = this.normalizeAxis(this.yAxis);
405
- options.scales = options.scales || {};
406
- options.scales.x = {
407
- type: this._detectAxisType(this.data, xAxisCfg, "x"),
408
- display: true,
409
- title: {
410
- display: !!xAxisCfg.label,
411
- text: xAxisCfg.label || ""
412
- },
413
- grid: { display: true },
414
- ticks: {}
415
- };
416
- if (xAxisCfg.formatter) {
417
- options.scales.x.ticks.callback = this._createFormatterCallback(xAxisCfg.formatter);
418
- }
419
- options.scales.y = {
420
- type: this._detectAxisType(this.data, yAxisCfg, "y"),
421
- display: true,
422
- beginAtZero: yAxisCfg.beginAtZero !== false,
423
- title: {
424
- display: !!yAxisCfg.label,
425
- text: yAxisCfg.label || ""
426
- },
427
- grid: { display: true },
428
- ticks: {}
429
- };
430
- if (yAxisCfg.formatter) {
431
- options.scales.y.ticks.callback = this._createFormatterCallback(yAxisCfg.formatter);
432
- }
433
- this.applyThemeToOptions(options);
434
- if (this.tooltipFormatters.x || this.tooltipFormatters.y) {
435
- options.plugins = options.plugins || {};
436
- options.plugins.tooltip = options.plugins.tooltip || {};
437
- options.plugins.tooltip.callbacks = options.plugins.tooltip.callbacks || {};
438
- if (this.tooltipFormatters.x) {
439
- options.plugins.tooltip.callbacks.title = (context) => {
440
- const value = context[0]?.label;
441
- return value ? this.dataFormatter.pipe(value, this.tooltipFormatters.x) : value;
442
- };
443
- }
444
- if (this.tooltipFormatters.y) {
445
- options.plugins.tooltip.callbacks.label = (context) => {
446
- const value = context.raw;
447
- const formattedValue = this.dataFormatter.pipe(value, this.tooltipFormatters.y);
448
- return `${context.dataset.label}: ${formattedValue}`;
449
- };
450
- }
451
- }
452
- if (typeof this.applySubclassChartOptions === "function") {
453
- this.applySubclassChartOptions(options);
454
- }
455
- return options;
456
- }
457
- // Helper method to create Chart.js callback from MOJO formatter
458
- _createFormatterCallback(formatter) {
459
- if (!formatter) return null;
460
- return (value) => {
461
- try {
462
- return this.dataFormatter.pipe(value, formatter);
463
- } catch (error) {
464
- console.warn(`Chart formatter error:`, error);
465
- return value;
466
- }
467
- };
468
- }
469
- // Normalize axis configuration into a consistent object
470
- normalizeAxis(axisConfig) {
471
- if (!axisConfig) return {};
472
- if (typeof axisConfig === "string") {
473
- return { formatter: axisConfig };
474
- }
475
- if (typeof axisConfig === "object") {
476
- const { formatter, label, type, beginAtZero, ...rest } = axisConfig;
477
- return { formatter, label, type, beginAtZero, ...rest };
478
- }
479
- return {};
480
- }
481
- // Smart axis type detection from data
482
- _detectAxisType(data, axisConfig, axisName = "x") {
483
- if (axisConfig && axisConfig.type) {
484
- return axisConfig.type;
485
- }
486
- if (axisConfig && axisConfig.formatter) {
487
- const formatter = axisConfig.formatter.toLowerCase();
488
- if (formatter.includes("date") || formatter.includes("time")) {
489
- return "time";
490
- }
491
- }
492
- if (data) {
493
- if (axisName === "x" && data.labels && data.labels.length > 0) {
494
- const firstLabel = data.labels[0];
495
- if (typeof firstLabel === "string") {
496
- if (!/^\d+\.?\d*$/.test(firstLabel.trim())) {
497
- return "category";
498
- }
499
- }
500
- if (firstLabel instanceof Date || typeof firstLabel === "string" && !isNaN(Date.parse(firstLabel))) {
501
- return "time";
502
- }
503
- return "linear";
504
- } else if (axisName === "y" && data.datasets && data.datasets.length > 0) {
505
- const firstDataset = data.datasets[0];
506
- if (firstDataset.data && firstDataset.data.length > 0) {
507
- const firstValue = firstDataset.data[0];
508
- if (typeof firstValue === "number" || !isNaN(parseFloat(firstValue))) {
509
- return "linear";
510
- }
511
- return "category";
512
- }
513
- }
514
- }
515
- return axisName === "x" ? "category" : "linear";
516
- }
517
- setupChartEventHandlers() {
518
- if (!this.chart) return;
519
- this.chart.options.onClick = (event, elements) => {
520
- if (elements.length > 0) {
521
- const element = elements[0];
522
- const datasetIndex = element.datasetIndex;
523
- const index = element.index;
524
- const value = this.chart.data.datasets[datasetIndex].data[index];
525
- const label = this.chart.data.labels[index];
526
- this.emit("chart:point-clicked", {
527
- chart: this,
528
- datasetIndex,
529
- index,
530
- value,
531
- label,
532
- dataset: this.chart.data.datasets[datasetIndex]
533
- });
534
- }
535
- };
536
- this.chart.options.onHover = (event, elements) => {
537
- this.canvas.style.cursor = elements.length > 0 ? "pointer" : "default";
538
- };
539
- }
540
- // Theme Management
541
- applyTheme() {
542
- this.element.setAttribute("data-theme", this.theme);
543
- if (this.chart) {
544
- this.chart.options = this.buildChartOptions();
545
- this.chart.update("none");
546
- }
547
- }
548
- applyThemeToOptions(options) {
549
- const isDark = this.theme === "dark";
550
- if (options.scales) {
551
- Object.keys(options.scales).forEach((scaleId) => {
552
- const scale = options.scales[scaleId];
553
- scale.grid = scale.grid || {};
554
- scale.ticks = scale.ticks || {};
555
- scale.grid.color = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
556
- scale.ticks.color = isDark ? "#e9ecef" : "#495057";
557
- });
558
- }
559
- if (options.plugins?.legend) {
560
- options.plugins.legend.labels = options.plugins.legend.labels || {};
561
- options.plugins.legend.labels.color = isDark ? "#e9ecef" : "#495057";
562
- }
563
- if (options.plugins?.title) {
564
- options.plugins.title.color = isDark ? "#ffffff" : "#212529";
565
- }
566
- }
567
- toggleTheme() {
568
- this.theme = this.theme === "light" ? "dark" : "light";
569
- this.applyTheme();
570
- this.emit("chart:theme-changed", {
571
- chart: this,
572
- theme: this.theme
573
- });
574
- }
575
- // Auto-refresh Management
576
- startAutoRefresh() {
577
- if (!this.endpoint || !this.refreshInterval) return;
578
- this.stopAutoRefresh();
579
- this.refreshTimer = setInterval(() => {
580
- this.fetchData();
581
- }, this.refreshInterval);
582
- this.updateRefreshStatus(true);
583
- }
584
- stopAutoRefresh() {
585
- if (this.refreshTimer) {
586
- clearInterval(this.refreshTimer);
587
- this.refreshTimer = null;
588
- }
589
- this.updateRefreshStatus(false);
590
- }
591
- // Export Functionality
592
- exportChart(format = "png") {
593
- if (!this.chart) return;
594
- try {
595
- if (format === "csv") {
596
- this.exportCSV();
597
- } else {
598
- const url = this.chart.toBase64Image("image/" + format, 1);
599
- const link = document.createElement("a");
600
- link.download = `chart-${Date.now()}.${format}`;
601
- link.href = url;
602
- link.click();
603
- this.emit("chart:exported", {
604
- chart: this,
605
- format,
606
- filename: link.download
607
- });
608
- }
609
- } catch (error) {
610
- console.error("Failed to export chart:", error);
611
- this.showError("Failed to export chart");
612
- }
613
- }
614
- // CSV Export Functionality
615
- exportCSV() {
616
- if (!this.chart || !this.chart.data) return;
617
- try {
618
- const csvData = this.generateCSV();
619
- const blob = new Blob([csvData], { type: "text/csv;charset=utf-8;" });
620
- const url = URL.createObjectURL(blob);
621
- const link = document.createElement("a");
622
- link.download = `chart-data-${Date.now()}.csv`;
623
- link.href = url;
624
- link.click();
625
- URL.revokeObjectURL(url);
626
- this.emit("chart:exported", {
627
- chart: this,
628
- format: "csv",
629
- filename: link.download
630
- });
631
- } catch (error) {
632
- console.error("Failed to export CSV:", error);
633
- this.showError("Failed to export CSV");
634
- }
635
- }
636
- // Generate CSV data from chart
637
- generateCSV() {
638
- const data = this.chart.data;
639
- const labels = data.labels || [];
640
- const datasets = data.datasets || [];
641
- let csv = "Label";
642
- datasets.forEach((dataset) => {
643
- csv += "," + (dataset.label || "Data");
644
- });
645
- csv += "\n";
646
- labels.forEach((label, index) => {
647
- csv += `"${label}"`;
648
- datasets.forEach((dataset) => {
649
- const value = dataset.data[index] || "";
650
- csv += "," + value;
651
- });
652
- csv += "\n";
653
- });
654
- return csv;
655
- }
656
- // UI State Management
657
- showLoading() {
658
- this.isLoading = true;
659
- this.hideAllOverlays();
660
- this.loadingOverlay?.classList.remove("d-none");
661
- }
662
- hideLoading() {
663
- this.isLoading = false;
664
- this.loadingOverlay?.classList.add("d-none");
665
- }
666
- showError(message) {
667
- this.hasError = true;
668
- this.hideAllOverlays();
669
- const errorMessageEl = this.errorOverlay?.querySelector(".error-message");
670
- if (errorMessageEl) {
671
- errorMessageEl.textContent = message;
672
- }
673
- this.errorOverlay?.classList.remove("d-none");
674
- }
675
- hideError() {
676
- this.hasError = false;
677
- this.errorOverlay?.classList.add("d-none");
678
- }
679
- showNoData() {
680
- this.hideAllOverlays();
681
- this.noDataOverlay?.classList.remove("d-none");
682
- }
683
- hideAllOverlays() {
684
- this.loadingOverlay?.classList.add("d-none");
685
- this.errorOverlay?.classList.add("d-none");
686
- this.noDataOverlay?.classList.add("d-none");
687
- }
688
- showWebSocketStatus(connected, status = "connected") {
689
- if (!this.websocketStatus) return;
690
- if (connected) {
691
- this.websocketStatus.className = "badge bg-success";
692
- this.websocketStatus.innerHTML = '<i class="bi bi-wifi"></i> Live';
693
- } else {
694
- this.websocketStatus.className = status === "error" ? "badge bg-danger" : "badge bg-secondary";
695
- this.websocketStatus.innerHTML = status === "error" ? '<i class="bi bi-wifi-off"></i> Error' : '<i class="bi bi-wifi-off"></i> Offline';
696
- }
697
- this.websocketStatus.style.display = "inline-block";
698
- }
699
- setRefreshButtonState(loading) {
700
- if (!this.refreshBtn) return;
701
- const icon = this.refreshBtn.querySelector("i");
702
- if (loading) {
703
- this.refreshBtn.disabled = true;
704
- icon?.classList.add("spin");
705
- } else {
706
- this.refreshBtn.disabled = false;
707
- icon?.classList.remove("spin");
708
- }
709
- }
710
- updateLastUpdatedTime() {
711
- const lastUpdatedEl = this.element.querySelector(".last-updated");
712
- const timestampEl = this.element.querySelector(".timestamp");
713
- if (lastUpdatedEl && timestampEl) {
714
- timestampEl.textContent = (/* @__PURE__ */ new Date()).toLocaleTimeString();
715
- lastUpdatedEl.style.display = "block";
716
- }
717
- }
718
- updateRefreshStatus(active) {
719
- const statusEl = this.element.querySelector(".refresh-status");
720
- if (statusEl) {
721
- statusEl.textContent = active ? `Every ${this.refreshInterval / 1e3}s` : "Off";
722
- }
723
- }
724
- updateDataStats(data) {
725
- let points = 0;
726
- if (data.datasets) {
727
- points = data.datasets.reduce((sum, dataset) => {
728
- return sum + (dataset.data ? dataset.data.length : 0);
729
- }, 0);
730
- }
731
- this.dataPoints = points;
732
- const dataPointsEl = this.element.querySelector(".data-points");
733
- if (dataPointsEl) {
734
- dataPointsEl.textContent = `${points} data point${points !== 1 ? "s" : ""}`;
735
- }
736
- }
737
- showFooter() {
738
- if (this.footerElement) {
739
- this.footerElement.style.display = "block";
740
- }
741
- }
742
- setupResizeObserver() {
743
- if (!window.ResizeObserver || !this.contentElement) return;
744
- const resizeObserver = new ResizeObserver(() => {
745
- if (this.chart) {
746
- this.chart.resize();
747
- }
748
- });
749
- resizeObserver.observe(this.contentElement);
750
- this._resizeObserver = resizeObserver;
751
- }
752
- // Cleanup
753
- async onBeforeDestroy() {
754
- this.stopAutoRefresh();
755
- if (this.websocket) {
756
- this.websocket.disconnect();
757
- this.websocket = null;
758
- }
759
- if (this.chart) {
760
- this.chart.destroy();
761
- this.chart = null;
762
- }
763
- if (this._resizeObserver) {
764
- this._resizeObserver.disconnect();
765
- this._resizeObserver = null;
766
- }
767
- if (this._essentialListeners) {
768
- this._essentialListeners.forEach(({ el, type, fn }) => {
769
- if (el) el.removeEventListener(type, fn);
770
- });
771
- this._essentialListeners = [];
772
- }
773
- this.emit("chart:destroyed", { chart: this });
774
- }
775
- // Public API
776
- setData(data) {
777
- this.data = data;
778
- return this.updateChart(data);
779
- }
780
- setEndpoint(endpoint) {
781
- this.endpoint = endpoint;
782
- if (endpoint) {
783
- return this.fetchData();
784
- }
785
- }
786
- setWebSocketUrl(url) {
787
- if (this.websocket) {
788
- this.websocket.disconnect();
789
- }
790
- this.websocketUrl = url;
791
- if (url) {
792
- return this.connectWebSocket();
793
- }
794
- }
795
- // Dimension Control Methods
796
- setWidth(width) {
797
- this.width = width;
798
- this.contentStyle = [
799
- this.width ? `width: ${this.width}px;` : "",
800
- this.height ? `height: ${this.height}px;` : ""
801
- ].filter(Boolean).join(" ");
802
- if (this.contentElement) {
803
- this._updateChartDimensions();
804
- }
805
- }
806
- setHeight(height) {
807
- this.height = height;
808
- this.contentStyle = [
809
- this.width ? `width: ${this.width}px;` : "",
810
- this.height ? `height: ${this.height}px;` : ""
811
- ].filter(Boolean).join(" ");
812
- if (this.contentElement) {
813
- this._updateChartDimensions();
814
- }
815
- }
816
- setDimensions(width, height) {
817
- this.width = width;
818
- this.height = height;
819
- this.contentStyle = [
820
- this.width ? `width: ${this.width}px;` : "",
821
- this.height ? `height: ${this.height}px;` : ""
822
- ].filter(Boolean).join(" ");
823
- if (this.contentElement) {
824
- this._updateChartDimensions();
825
- }
826
- }
827
- _updateChartDimensions() {
828
- if (this.chart) {
829
- if (this.width || this.height) {
830
- this.chart.options.responsive = true;
831
- this.chart.options.maintainAspectRatio = false;
832
- if (this.width && this.contentElement) {
833
- this.contentElement.style.width = this.width ? this.width + "px" : "";
834
- }
835
- if (this.height && this.contentElement) {
836
- this.contentElement.style.height = this.height ? this.height + "px" : "";
837
- }
838
- } else {
839
- this.chart.options.responsive = true;
840
- this.chart.options.maintainAspectRatio = this.chartOptions.maintainAspectRatio;
841
- }
842
- this.chart.resize();
843
- }
844
- }
845
- resize() {
846
- if (this.chart) {
847
- this.chart.resize();
848
- }
849
- }
850
- refresh() {
851
- return this.fetchData();
852
- }
853
- export(format = "png") {
854
- return this.exportChart(format);
855
- }
856
- setTheme(theme) {
857
- this.theme = theme;
858
- this.applyTheme();
859
- }
860
- getStats() {
861
- return {
862
- isLoading: this.isLoading,
863
- hasError: this.hasError,
864
- dataPoints: this.dataPoints,
865
- lastFetch: this.lastFetch,
866
- theme: this.theme,
867
- chartType: this.chartType,
868
- autoRefresh: !!this.refreshTimer,
869
- websocketConnected: this.websocket?.isConnected || false
870
- };
871
- }
872
- }
873
- class ChartHeaderView extends View {
874
- constructor(options = {}) {
875
- super({
876
- ...options,
877
- className: `mojo-chart-header ${options.className || ""}`,
878
- tagName: "div"
879
- });
880
- this.titleHtml = options.titleHtml || "";
881
- this.chartTitle = options.chartTitle || "";
882
- this.showExport = options.showExport === true;
883
- this.showRefresh = !!options.showRefresh;
884
- this.showTheme = false;
885
- this.showTheme = options.showTheme === true;
886
- this.controls = Array.isArray(options.controls) ? options.controls : [];
887
- this.controlsHtml = this._buildControlsHtml(this.controls);
888
- }
889
- async getTemplate() {
890
- return `
891
- <div class="d-flex justify-content-between align-items-center">
892
- <div class="chart-title-section">
893
- <h5 class="mb-2 chart-title">{{{titleHtml}}}</h5>
894
- <small class="text-muted last-updated" style="display: none;">
895
- Last updated: <span class="timestamp"></span>
896
- </small>
897
- </div>
898
-
899
- <div class="chart-controls">
900
- <div class="btn-toolbar" role="toolbar">
901
- {{{controlsHtml}}}
902
-
903
- <div class="btn-group btn-group-sm" role="group">
904
-
905
- {{#showTheme}}
906
- <button type="button" class="btn btn-outline-secondary theme-toggle" data-action="toggle-theme" title="Toggle Theme">
907
- <i class="bi bi-palette"></i>
908
- </button>
909
- {{/showTheme}}
910
-
911
- {{#showExport}}
912
- <div class="btn-group btn-group-sm" role="group">
913
- <button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" title="Export Chart">
914
- <i class="bi bi-download"></i>
915
- </button>
916
- <ul class="dropdown-menu">
917
- <li><a class="dropdown-item" href="#" data-action="export-chart" data-format="png">
918
- <i class="bi bi-image"></i> PNG
919
- </a></li>
920
- <li><a class="dropdown-item" href="#" data-action="export-chart" data-format="jpg">
921
- <i class="bi bi-image"></i> JPEG
922
- </a></li>
923
- <li><a class="dropdown-item" href="#" data-action="export-chart" data-format="csv">
924
- <i class="bi bi-file-earmark-spreadsheet"></i> CSV
925
- </a></li>
926
- </ul>
927
- </div>
928
- {{/showExport}}
929
-
930
- {{#showRefresh}}
931
- <button type="button" class="btn btn-outline-secondary refresh-btn" data-action="refresh-chart" title="Refresh Data">
932
- <i class="bi bi-arrow-clockwise"></i>
933
- </button>
934
- {{/showRefresh}}
935
- </div>
936
- </div>
937
- </div>
938
- </div>
939
- `;
940
- }
941
- // Build custom controls HTML for the toolbar from config
942
- _buildControlsHtml(controls) {
943
- if (!Array.isArray(controls) || controls.length === 0) return "";
944
- const parts = [];
945
- controls.forEach((item) => {
946
- if (!item || !item.type) return;
947
- switch (item.type) {
948
- case "select": {
949
- const sizeCls = item.size === "md" ? "" : " form-select-sm";
950
- const cls = `form-select${sizeCls} ${item.className || ""}`.trim();
951
- const optionsHtml = (item.options || []).map((opt) => `<option value="${this._escapeAttr(opt.value)}"${opt.selected ? " selected" : ""}>${this._escapeHtml(opt.label)}</option>`).join("");
952
- parts.push(`
953
- <div class="btn-group btn-group-sm me-2" role="group">
954
- <select class="${cls}" data-change-action="${this._escapeAttr(item.action || item.name || "select-changed")}" style="width: auto;">
955
- ${optionsHtml}
956
- </select>
957
- </div>
958
- `);
959
- break;
960
- }
961
- case "button": {
962
- const { variant = "outline-secondary", size = "sm" } = item;
963
- const sizeCls = size === "md" ? "" : " btn-sm";
964
- const btnCls = `btn btn-${variant}${sizeCls} ${item.className || ""}`.trim();
965
- const titleAttr = item.title ? ` title="${this._escapeAttr(item.title)}"` : "";
966
- const dataAttrs = this._buildDataAttrs(item.data);
967
- parts.push(`
968
- <div class="btn-group btn-group-sm me-2" role="group">
969
- <button type="button" class="${btnCls}" data-action="${this._escapeAttr(item.action || "button-action")}"${titleAttr}${dataAttrs}>
970
- ${item.labelHtml || ""}
971
- </button>
972
- </div>
973
- `);
974
- break;
975
- }
976
- case "buttongroup": {
977
- const size = item.size || "sm";
978
- const groupCls = `btn-group btn-group-${size} me-2 ${item.className || ""}`.trim();
979
- const buttons = (item.buttons || []).map((btn) => {
980
- const variant = btn.variant || "outline-secondary";
981
- const sizeCls = size === "md" ? "" : " btn-sm";
982
- const btnCls = `btn btn-${variant}${sizeCls} ${btn.className || ""}`.trim();
983
- const titleAttr = btn.title ? ` title="${this._escapeAttr(btn.title)}"` : "";
984
- const dataAttrs = this._buildDataAttrs(btn.data);
985
- return `<button type="button" class="${btnCls}" data-action="${this._escapeAttr(btn.action || "button-action")}"${titleAttr}${dataAttrs}>${btn.labelHtml || ""}</button>`;
986
- }).join("");
987
- parts.push(`
988
- <div class="${groupCls}" role="group">
989
- ${buttons}
990
- </div>
991
- `);
992
- break;
993
- }
994
- case "divider": {
995
- parts.push(`<div class="vr mx-2"></div>`);
996
- break;
997
- }
998
- case "html": {
999
- const html = item.html || "";
1000
- parts.push(`<div class="me-2 d-inline-block">${html}</div>`);
1001
- break;
1002
- }
1003
- }
1004
- });
1005
- return parts.join("\n");
1006
- }
1007
- _buildDataAttrs(data) {
1008
- if (!data || typeof data !== "object") return "";
1009
- return Object.entries(data).map(([key, val]) => ` data-${this._kebabCase(String(key))}="${this._escapeAttr(String(val))}"`).join("");
1010
- }
1011
- _kebabCase(str) {
1012
- return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9\-]/g, "-").replace(/--+/g, "-").replace(/^-|-$/g, "");
1013
- }
1014
- _escapeAttr(value) {
1015
- return String(value).replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1016
- }
1017
- _escapeHtml(value) {
1018
- return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1019
- }
1020
- }
1021
- const DEFAULT_CHART_COLORS = [
1022
- "rgba(52, 152, 219, 0.85)",
1023
- // Belize Hole
1024
- "rgba(231, 76, 60, 0.85)",
1025
- // Alizarin
1026
- "rgba(46, 204, 113, 0.85)",
1027
- // Emerald
1028
- "rgba(241, 196, 15, 0.85)",
1029
- // Sunflower
1030
- "rgba(155, 89, 182, 0.85)",
1031
- // Amethyst
1032
- "rgba(230, 126, 34, 0.85)",
1033
- // Carrot
1034
- "rgba(26, 188, 156, 0.85)",
1035
- // Turquoise
1036
- "rgba(52, 73, 94, 0.85)",
1037
- // Wet asphalt
1038
- "rgba(243, 156, 18, 0.85)",
1039
- // Orange
1040
- "rgba(142, 68, 173, 0.85)",
1041
- // Wisteria
1042
- "rgba(39, 174, 96, 0.85)",
1043
- // Nephritis
1044
- "rgba(41, 128, 185, 0.85)",
1045
- // Peter River
1046
- "rgba(192, 57, 43, 0.85)",
1047
- // Pomegranate
1048
- "rgba(127, 140, 141, 0.85)",
1049
- // Asbestos
1050
- "rgba(22, 160, 133, 0.85)",
1051
- // Green Sea
1052
- "rgba(211, 84, 0, 0.85)",
1053
- // Pumpkin
1054
- "rgba(44, 62, 80, 0.85)",
1055
- // Midnight Blue
1056
- "rgba(214, 69, 65, 0.85)",
1057
- // Valencia
1058
- "rgba(149, 165, 166, 0.85)",
1059
- // Concrete
1060
- "rgba(52, 232, 158, 0.85)"
1061
- // Mint
1062
- ];
1063
- class SeriesChart extends BaseChart {
1064
- constructor(options = {}) {
1065
- super({
1066
- ...options,
1067
- chartType: options.chartType || "line"
1068
- });
1069
- this.showTypeSwitch = true;
1070
- if (options.showTypeSwitch !== void 0) this.showTypeSwitch = options.showTypeSwitch;
1071
- this.orientation = options.orientation || "vertical";
1072
- this.stacked = options.stacked || false;
1073
- this.stepped = options.stepped || false;
1074
- this.tension = options.tension || 0.4;
1075
- this.fill = options.fill || false;
1076
- this.showRefreshButton = options.showRefreshButton !== false;
1077
- if (!this.headerConfig) {
1078
- this.headerConfig = {
1079
- titleHtml: this.title || "",
1080
- chartTitle: this.chartTitle || "",
1081
- showExport: this.exportEnabled,
1082
- showRefresh: this.refreshEnabled,
1083
- showTheme: true,
1084
- controls: []
1085
- };
1086
- }
1087
- this.series = options.series || [];
1088
- this.xField = options.xField || "x";
1089
- this.yField = options.yField || "y";
1090
- const providedColors = Array.isArray(options.colors) && options.colors.length ? options.colors : DEFAULT_CHART_COLORS;
1091
- this.colors = [...providedColors];
1092
- this.tooltipFormatters = options.tooltip || {};
1093
- }
1094
- getColor(index) {
1095
- this.ensureColorPool(index + 1);
1096
- return this.colors[index];
1097
- }
1098
- ensureColorPool(count) {
1099
- if (this.colors.length >= count) return;
1100
- while (this.colors.length < count) {
1101
- const nextIndex = this.colors.length;
1102
- const hue = nextIndex * 37 % 360;
1103
- const color = `hsla(${hue}, 70%, 55%, 0.85)`;
1104
- this.colors.push(color);
1105
- }
1106
- }
1107
- withAlpha(color, alpha = 0.4) {
1108
- if (!color) return color;
1109
- const rgbaMatch = color.match(/rgba?\(([^)]+)\)/i);
1110
- if (rgbaMatch) {
1111
- const parts = rgbaMatch[1].split(",").map((part) => part.trim());
1112
- const [r, g, b] = parts;
1113
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1114
- }
1115
- const hslaMatch = color.match(/hsla?\(([^)]+)\)/i);
1116
- if (hslaMatch) {
1117
- const parts = hslaMatch[1].split(",").map((part) => part.trim());
1118
- const [h, s, l] = parts;
1119
- return `hsla(${h}, ${s}, ${l}, ${alpha})`;
1120
- }
1121
- return color;
1122
- }
1123
- async getTemplate() {
1124
- return await super.getTemplate();
1125
- }
1126
- async onInit() {
1127
- if (this.showTypeSwitch) {
1128
- this.headerConfig.controls.push({
1129
- type: "buttongroup",
1130
- size: "sm",
1131
- buttons: [
1132
- { action: "set-chart-type", labelHtml: '<i class="bi bi-graph-up"></i>', title: "Line", variant: this.chartType === "line" ? "primary" : "outline-primary", data: { type: "line" } },
1133
- { action: "set-chart-type", labelHtml: '<i class="bi bi-bar-chart"></i>', title: "Bar", variant: this.chartType === "bar" ? "primary" : "outline-primary", data: { type: "bar" } }
1134
- ]
1135
- });
1136
- }
1137
- await super.onInit();
1138
- }
1139
- // Action Handlers
1140
- async onActionSetChartType(event, element) {
1141
- event.stopPropagation();
1142
- const newType = element.getAttribute("data-type");
1143
- if (newType && newType !== this.chartType) {
1144
- await this.setChartType(newType);
1145
- }
1146
- }
1147
- async rebuildChart() {
1148
- if (this.chart && this.data) {
1149
- this.chart.destroy();
1150
- this.chart = null;
1151
- const processedData = this.processChartData(this.data);
1152
- await this.createChart(processedData);
1153
- }
1154
- }
1155
- async setChartType(newType) {
1156
- if (!["line", "bar"].includes(newType)) {
1157
- throw new Error(`Unsupported chart type: ${newType}`);
1158
- }
1159
- const oldType = this.chartType;
1160
- this.chartType = newType;
1161
- if (this.chart && this.data) {
1162
- this.chart.destroy();
1163
- this.chart = null;
1164
- const processedData = this.processChartData(this.data);
1165
- await this.createChart(processedData);
1166
- }
1167
- this._updateTypeSwitcherButtons();
1168
- const eventBus = this.getApp()?.events;
1169
- if (eventBus) {
1170
- eventBus.emit("chart:type-changed", {
1171
- chart: this,
1172
- oldType,
1173
- newType: this.chartType
1174
- });
1175
- }
1176
- }
1177
- processChartData(data) {
1178
- if (!data) return data;
1179
- let processedData;
1180
- if (Array.isArray(data)) {
1181
- processedData = this.processArrayData(data);
1182
- } else if (data.labels && data.datasets) {
1183
- processedData = this.processChartJSData(data);
1184
- } else if (data.series) {
1185
- processedData = this.processSeriesData(data);
1186
- } else {
1187
- processedData = data;
1188
- }
1189
- return this.applyFormattersToData(processedData);
1190
- }
1191
- processArrayData(data) {
1192
- const labels = [];
1193
- const values = [];
1194
- data.forEach((item) => {
1195
- const xValue = item[this.xField];
1196
- const yValue = item[this.yField];
1197
- labels.push(xValue);
1198
- values.push(yValue);
1199
- });
1200
- this.ensureColorPool(1);
1201
- const baseColor = this.getColor(0);
1202
- const backgroundAlpha = this.chartType === "line" ? 0.25 : 0.65;
1203
- return {
1204
- labels,
1205
- datasets: [{
1206
- label: this.title || "Data",
1207
- data: values,
1208
- backgroundColor: this.withAlpha(baseColor, backgroundAlpha),
1209
- borderColor: baseColor,
1210
- borderWidth: 2,
1211
- tension: this.chartType === "line" ? this.tension : 0,
1212
- fill: this.chartType === "line" ? this.fill : false,
1213
- stepped: this.chartType === "line" ? this.stepped : false
1214
- }]
1215
- };
1216
- }
1217
- processChartJSData(data) {
1218
- const processedData = { ...data };
1219
- if (!processedData.datasets) {
1220
- processedData.datasets = [];
1221
- return processedData;
1222
- }
1223
- const datasetCount = processedData.datasets.length;
1224
- this.ensureColorPool(datasetCount);
1225
- const backgroundAlpha = this.chartType === "line" ? 0.25 : 0.65;
1226
- processedData.datasets = processedData.datasets.map((dataset, index) => {
1227
- const baseColor = dataset.borderColor || this.getColor(index);
1228
- return {
1229
- ...dataset,
1230
- backgroundColor: dataset.backgroundColor || this.withAlpha(baseColor, backgroundAlpha),
1231
- borderColor: baseColor,
1232
- borderWidth: dataset.borderWidth || 2,
1233
- tension: this.chartType === "line" ? dataset.tension ?? this.tension : 0,
1234
- fill: this.chartType === "line" ? dataset.fill ?? this.fill : false,
1235
- stepped: this.chartType === "line" ? dataset.stepped ?? this.stepped : false
1236
- };
1237
- });
1238
- return processedData;
1239
- }
1240
- processSeriesData(data) {
1241
- const labels = data.labels || [];
1242
- const datasets = [];
1243
- const count = data.series?.length || 0;
1244
- this.ensureColorPool(count);
1245
- const backgroundAlpha = this.chartType === "line" ? 0.25 : 0.65;
1246
- data.series.forEach((series, index) => {
1247
- const baseColor = series.borderColor || this.getColor(index);
1248
- datasets.push({
1249
- label: series.name || series.label || `Series ${index + 1}`,
1250
- data: series.data || [],
1251
- backgroundColor: series.backgroundColor || this.withAlpha(baseColor, backgroundAlpha),
1252
- borderColor: baseColor,
1253
- borderWidth: series.borderWidth || 2,
1254
- tension: this.chartType === "line" ? series.tension ?? this.tension : 0,
1255
- fill: this.chartType === "line" ? series.fill ?? this.fill : false,
1256
- stepped: this.chartType === "line" ? series.stepped ?? this.stepped : false
1257
- });
1258
- });
1259
- return { labels, datasets };
1260
- }
1261
- applyFormattersToData(data) {
1262
- if (!data) return data;
1263
- const processedData = { ...data };
1264
- const xAxisCfg = this.normalizeAxis ? this.normalizeAxis(this.xAxis) : {};
1265
- if (xAxisCfg.formatter && processedData.labels) {
1266
- processedData.labels = processedData.labels.map(
1267
- (label) => this.dataFormatter.pipe(label, xAxisCfg.formatter)
1268
- );
1269
- }
1270
- return processedData;
1271
- }
1272
- applySubclassChartOptions(options) {
1273
- if (this.stacked && this.chartType === "bar" && options.scales) {
1274
- if (options.scales.x) options.scales.x.stacked = true;
1275
- if (options.scales.y) options.scales.y.stacked = true;
1276
- }
1277
- if (this.chartType === "bar" && this.orientation === "horizontal") {
1278
- options.indexAxis = "y";
1279
- }
1280
- options.interaction = options.interaction || {};
1281
- options.interaction.intersect = false;
1282
- options.interaction.mode = this.chartType === "line" ? "index" : "nearest";
1283
- options.elements = options.elements || {};
1284
- options.elements.line = {
1285
- ...options.elements.line || {},
1286
- tension: this.tension,
1287
- borderWidth: 2
1288
- };
1289
- options.elements.point = {
1290
- ...options.elements.point || {},
1291
- radius: this.chartType === "line" ? 4 : 0,
1292
- hoverRadius: 6,
1293
- hitRadius: 8
1294
- };
1295
- options.elements.bar = {
1296
- ...options.elements.bar || {},
1297
- borderWidth: 1,
1298
- borderSkipped: false
1299
- };
1300
- }
1301
- // Process simple axis configuration into detailed config
1302
- processAxisConfig(axisConfig) {
1303
- if (!axisConfig) return {};
1304
- if (typeof axisConfig === "string") {
1305
- return { formatter: axisConfig };
1306
- }
1307
- if (typeof axisConfig === "object") {
1308
- return {
1309
- formatter: axisConfig.formatter,
1310
- label: axisConfig.label,
1311
- type: axisConfig.type,
1312
- beginAtZero: axisConfig.beginAtZero,
1313
- ...axisConfig
1314
- };
1315
- }
1316
- return {};
1317
- }
1318
- _updateTypeSwitcherButtons() {
1319
- const buttons = this.element?.querySelectorAll('[data-action="set-chart-type"]');
1320
- if (!buttons || buttons.length === 0) return;
1321
- buttons.forEach((button) => {
1322
- const buttonType = button.getAttribute("data-type");
1323
- const isActive = buttonType === this.chartType;
1324
- button.classList.toggle("btn-primary", isActive);
1325
- button.classList.toggle("btn-outline-primary", !isActive);
1326
- button.classList.toggle("active", isActive);
1327
- });
1328
- }
1329
- // Public API extensions
1330
- setOrientation(orientation) {
1331
- if (!["vertical", "horizontal"].includes(orientation)) {
1332
- throw new Error(`Invalid orientation: ${orientation}`);
1333
- }
1334
- this.orientation = orientation;
1335
- if (this.chart) {
1336
- this.chart.destroy();
1337
- this.chart = null;
1338
- if (this.data) {
1339
- const processedData = this.processChartData(this.data);
1340
- this.createChart(processedData);
1341
- }
1342
- }
1343
- }
1344
- setStacked(stacked) {
1345
- this.stacked = stacked;
1346
- if (this.chart) {
1347
- this.chart.options.scales.x.stacked = stacked;
1348
- this.chart.options.scales.y.stacked = stacked;
1349
- this.chart.update();
1350
- }
1351
- }
1352
- addSeries(series) {
1353
- if (!this.data || !this.data.datasets) return;
1354
- const newDataset = {
1355
- label: series.label || series.name || `Series ${this.data.datasets.length + 1}`,
1356
- data: series.data || [],
1357
- backgroundColor: series.backgroundColor || this.colors[this.data.datasets.length % this.colors.length].replace("0.8", "0.6"),
1358
- borderColor: series.borderColor || this.colors[this.data.datasets.length % this.colors.length],
1359
- borderWidth: series.borderWidth || 2,
1360
- tension: this.chartType === "line" ? series.tension ?? this.tension : 0,
1361
- fill: this.chartType === "line" ? series.fill ?? this.fill : false
1362
- };
1363
- this.data.datasets.push(newDataset);
1364
- if (this.chart) {
1365
- this.chart.data.datasets.push(newDataset);
1366
- this.chart.update();
1367
- }
1368
- const eventBus = this.getApp()?.events;
1369
- if (eventBus) {
1370
- eventBus.emit("chart:series-added", {
1371
- chart: this,
1372
- series: newDataset
1373
- });
1374
- }
1375
- }
1376
- removeSeries(index) {
1377
- if (!this.data || !this.data.datasets || index < 0 || index >= this.data.datasets.length) {
1378
- return;
1379
- }
1380
- const removedSeries = this.data.datasets.splice(index, 1)[0];
1381
- if (this.chart) {
1382
- this.chart.data.datasets.splice(index, 1);
1383
- this.chart.update();
1384
- }
1385
- const eventBus = this.getApp()?.events;
1386
- if (eventBus) {
1387
- eventBus.emit("chart:series-removed", {
1388
- chart: this,
1389
- series: removedSeries,
1390
- index
1391
- });
1392
- }
1393
- }
1394
- // Static dialog method
1395
- static async showDialog(options = {}) {
1396
- const {
1397
- title = "Chart Viewer",
1398
- size = "xl",
1399
- ...chartOptions
1400
- } = options;
1401
- const chart = new SeriesChart({
1402
- ...chartOptions,
1403
- title
1404
- });
1405
- const dialog = new Dialog({
1406
- title,
1407
- body: chart,
1408
- size,
1409
- centered: true,
1410
- backdrop: "static",
1411
- keyboard: true,
1412
- buttons: [
1413
- {
1414
- text: "Export PNG",
1415
- action: "export",
1416
- class: "btn btn-outline-primary"
1417
- },
1418
- {
1419
- text: "Close",
1420
- action: "close",
1421
- class: "btn btn-secondary",
1422
- dismiss: true
1423
- }
1424
- ]
1425
- });
1426
- await dialog.render();
1427
- document.body.appendChild(dialog.element);
1428
- await dialog.mount();
1429
- dialog.show();
1430
- return new Promise((resolve) => {
1431
- dialog.on("hidden", () => {
1432
- dialog.destroy();
1433
- resolve(chart);
1434
- });
1435
- dialog.on("action:export", () => {
1436
- chart.exportChart("png");
1437
- });
1438
- dialog.on("action:close", () => {
1439
- dialog.hide();
1440
- });
1441
- });
1442
- }
1443
- }
1444
- class PieChart extends BaseChart {
1445
- constructor(options = {}) {
1446
- super({
1447
- ...options,
1448
- chartType: "pie"
1449
- });
1450
- this.cutout = options.cutout || 0;
1451
- this.rotation = options.rotation || 0;
1452
- this.circumference = options.circumference || 360;
1453
- this.borderWidth = options.borderWidth || 2;
1454
- this.borderColor = options.borderColor || "#ffffff";
1455
- this.hoverBorderWidth = options.hoverBorderWidth || 3;
1456
- this.showLabels = options.showLabels !== false;
1457
- this.labelPosition = options.labelPosition || "outside";
1458
- this.labelFormatter = options.labelFormatter || null;
1459
- this.valueFormatter = options.valueFormatter || null;
1460
- this.labelField = options.labelField || "label";
1461
- this.valueField = options.valueField || "value";
1462
- this.colors = options.colors || [
1463
- "#FF6384",
1464
- // Red
1465
- "#36A2EB",
1466
- // Blue
1467
- "#FFCE56",
1468
- // Yellow
1469
- "#4BC0C0",
1470
- // Teal
1471
- "#9966FF",
1472
- // Purple
1473
- "#FF9F40",
1474
- // Orange
1475
- "#C9CBCF",
1476
- // Grey
1477
- "#4BC0C0",
1478
- // Green
1479
- "#FF6384",
1480
- // Pink
1481
- "#36A2EB"
1482
- // Light Blue
1483
- ];
1484
- this.animateRotate = options.animateRotate !== false;
1485
- this.animateScale = options.animateScale || false;
1486
- this.clickable = options.clickable !== false;
1487
- this.hoverable = options.hoverable !== false;
1488
- this.selectedSegment = null;
1489
- this.highlightedSegments = /* @__PURE__ */ new Set();
1490
- this.valueFormatter = options.valueFormatter || null;
1491
- }
1492
- processChartData(data) {
1493
- if (!data) return data;
1494
- let processedData;
1495
- if (Array.isArray(data)) {
1496
- processedData = this.processArrayData(data);
1497
- } else if (data.labels && data.datasets) {
1498
- processedData = this.processChartJSData(data);
1499
- } else if (typeof data === "object" && !data.labels) {
1500
- processedData = this.processObjectData(data);
1501
- } else {
1502
- processedData = data;
1503
- }
1504
- return this.applyFormattersToData(processedData);
1505
- }
1506
- processArrayData(data) {
1507
- const labels = [];
1508
- const values = [];
1509
- data.forEach((item) => {
1510
- const label = item[this.labelField];
1511
- const value = item[this.valueField];
1512
- if (label !== void 0 && value !== void 0) {
1513
- labels.push(label);
1514
- values.push(value);
1515
- }
1516
- });
1517
- return {
1518
- labels,
1519
- datasets: [{
1520
- data: values,
1521
- backgroundColor: this.generateColors(labels.length),
1522
- borderColor: this.borderColor,
1523
- borderWidth: this.borderWidth,
1524
- hoverBorderWidth: this.hoverBorderWidth
1525
- }]
1526
- };
1527
- }
1528
- processChartJSData(data) {
1529
- const processedData = { ...data };
1530
- processedData.datasets = processedData.datasets.map((dataset) => ({
1531
- ...dataset,
1532
- backgroundColor: dataset.backgroundColor || this.generateColors(processedData.labels.length),
1533
- borderColor: dataset.borderColor || this.borderColor,
1534
- borderWidth: dataset.borderWidth || this.borderWidth,
1535
- hoverBorderWidth: dataset.hoverBorderWidth || this.hoverBorderWidth
1536
- }));
1537
- return processedData;
1538
- }
1539
- processObjectData(data) {
1540
- const labels = Object.keys(data);
1541
- const values = Object.values(data);
1542
- return {
1543
- labels,
1544
- datasets: [{
1545
- data: values,
1546
- backgroundColor: this.generateColors(labels.length),
1547
- borderColor: this.borderColor,
1548
- borderWidth: this.borderWidth,
1549
- hoverBorderWidth: this.hoverBorderWidth
1550
- }]
1551
- };
1552
- }
1553
- applyFormattersToData(data) {
1554
- if (!data) return data;
1555
- const processedData = { ...data };
1556
- if (this.labelFormatter && processedData.labels) {
1557
- processedData.labels = processedData.labels.map(
1558
- (label) => this.dataFormatter.pipe(label, this.labelFormatter)
1559
- );
1560
- }
1561
- return processedData;
1562
- }
1563
- generateColors(count) {
1564
- const colors = [];
1565
- for (let i = 0; i < count; i++) {
1566
- colors.push(this.colors[i % this.colors.length]);
1567
- }
1568
- return colors;
1569
- }
1570
- buildChartOptions() {
1571
- const options = super.buildChartOptions();
1572
- options.cutout = this.cutout;
1573
- options.rotation = this.rotation;
1574
- options.circumference = this.circumference;
1575
- options.animation = {
1576
- animateRotate: this.animateRotate,
1577
- animateScale: this.animateScale,
1578
- duration: this.animations ? 1e3 : 0
1579
- };
1580
- options.plugins = {
1581
- ...options.plugins,
1582
- legend: {
1583
- ...options.plugins.legend,
1584
- position: options.plugins.legend.position || "right",
1585
- labels: {
1586
- ...options.plugins.legend.labels,
1587
- usePointStyle: true,
1588
- padding: 20,
1589
- generateLabels: (chart) => {
1590
- const data = chart.data;
1591
- if (data.labels.length && data.datasets.length) {
1592
- return data.labels.map((label, i) => {
1593
- const dataset = data.datasets[0];
1594
- const value = dataset.data[i];
1595
- const backgroundColor = dataset.backgroundColor[i];
1596
- const total = dataset.data.reduce((sum, val) => sum + val, 0);
1597
- const percentage = (value / total * 100).toFixed(1);
1598
- return {
1599
- text: `${label} (${percentage}%)`,
1600
- fillStyle: backgroundColor,
1601
- strokeStyle: backgroundColor,
1602
- lineWidth: 0,
1603
- hidden: false,
1604
- index: i
1605
- };
1606
- });
1607
- }
1608
- return [];
1609
- }
1610
- }
1611
- },
1612
- tooltip: {
1613
- ...options.plugins.tooltip,
1614
- callbacks: {
1615
- ...options.plugins.tooltip.callbacks,
1616
- label: (context) => {
1617
- const label = context.label || "";
1618
- const value = context.raw;
1619
- const dataset = context.dataset;
1620
- const total = dataset.data.reduce((sum, val) => sum + val, 0);
1621
- const percentage = (value / total * 100).toFixed(1);
1622
- let formattedValue = value;
1623
- if (this.valueFormatter) {
1624
- formattedValue = this.dataFormatter.pipe(value, this.valueFormatter);
1625
- } else if (this.tooltipFormatters && this.tooltipFormatters.y) {
1626
- formattedValue = this.dataFormatter.pipe(value, this.tooltipFormatters.y);
1627
- }
1628
- return `${label}: ${formattedValue} (${percentage}%)`;
1629
- }
1630
- }
1631
- }
1632
- };
1633
- delete options.scales;
1634
- return options;
1635
- }
1636
- setupChartEventHandlers() {
1637
- super.setupChartEventHandlers();
1638
- if (!this.chart || !this.clickable) return;
1639
- this.chart.options.onClick = (event, elements) => {
1640
- if (elements.length > 0) {
1641
- const element = elements[0];
1642
- const index = element.index;
1643
- const dataset = this.chart.data.datasets[0];
1644
- const label = this.chart.data.labels[index];
1645
- const value = dataset.data[index];
1646
- const total = dataset.data.reduce((sum, val) => sum + val, 0);
1647
- const percentage = (value / total * 100).toFixed(1);
1648
- this.toggleSegmentSelection(index);
1649
- const eventBus = this.getApp()?.events;
1650
- if (eventBus) {
1651
- eventBus.emit("chart:segment-clicked", {
1652
- chart: this,
1653
- index,
1654
- label,
1655
- value,
1656
- percentage: parseFloat(percentage),
1657
- isSelected: this.selectedSegment === index
1658
- });
1659
- }
1660
- }
1661
- };
1662
- if (this.hoverable) {
1663
- this.chart.options.onHover = (event, elements) => {
1664
- this.canvas.style.cursor = elements.length > 0 ? "pointer" : "default";
1665
- if (elements.length > 0) {
1666
- const element = elements[0];
1667
- const index = element.index;
1668
- const eventBus = this.getApp()?.events;
1669
- if (eventBus) {
1670
- eventBus.emit("chart:segment-hover", {
1671
- chart: this,
1672
- index,
1673
- label: this.chart.data.labels[index],
1674
- value: this.chart.data.datasets[0].data[index]
1675
- });
1676
- }
1677
- }
1678
- };
1679
- }
1680
- }
1681
- toggleSegmentSelection(index) {
1682
- if (this.selectedSegment === index) {
1683
- this.selectedSegment = null;
1684
- this.resetSegmentStyle(index);
1685
- } else {
1686
- if (this.selectedSegment !== null) {
1687
- this.resetSegmentStyle(this.selectedSegment);
1688
- }
1689
- this.selectedSegment = index;
1690
- this.highlightSegment(index);
1691
- }
1692
- }
1693
- highlightSegment(index) {
1694
- if (!this.chart) return;
1695
- const meta = this.chart.getDatasetMeta(0);
1696
- const segment = meta.data[index];
1697
- if (segment) {
1698
- segment.outerRadius += 10;
1699
- this.chart.update("none");
1700
- }
1701
- }
1702
- resetSegmentStyle(index) {
1703
- if (!this.chart) return;
1704
- const meta = this.chart.getDatasetMeta(0);
1705
- const segment = meta.data[index];
1706
- if (segment) {
1707
- segment.outerRadius -= 10;
1708
- this.chart.update("none");
1709
- }
1710
- }
1711
- highlightSegments(indices) {
1712
- if (!Array.isArray(indices)) {
1713
- indices = [indices];
1714
- }
1715
- this.highlightedSegments.clear();
1716
- indices.forEach((index) => {
1717
- this.highlightedSegments.add(index);
1718
- this.highlightSegment(index);
1719
- });
1720
- }
1721
- clearHighlights() {
1722
- this.highlightedSegments.forEach((index) => {
1723
- this.resetSegmentStyle(index);
1724
- });
1725
- this.highlightedSegments.clear();
1726
- if (this.selectedSegment !== null) {
1727
- this.resetSegmentStyle(this.selectedSegment);
1728
- this.selectedSegment = null;
1729
- }
1730
- }
1731
- // Public API extensions
1732
- selectSegment(index) {
1733
- if (index >= 0 && index < this.chart?.data?.labels?.length) {
1734
- this.toggleSegmentSelection(index);
1735
- }
1736
- }
1737
- getSegmentData(index) {
1738
- if (!this.chart || !this.chart.data) return null;
1739
- const dataset = this.chart.data.datasets[0];
1740
- const label = this.chart.data.labels[index];
1741
- const value = dataset.data[index];
1742
- const total = dataset.data.reduce((sum, val) => sum + val, 0);
1743
- const percentage = (value / total * 100).toFixed(1);
1744
- return {
1745
- index,
1746
- label,
1747
- value,
1748
- percentage: parseFloat(percentage),
1749
- color: dataset.backgroundColor[index],
1750
- isSelected: this.selectedSegment === index
1751
- };
1752
- }
1753
- getAllSegments() {
1754
- if (!this.chart || !this.chart.data) return [];
1755
- return this.chart.data.labels.map((_, index) => this.getSegmentData(index));
1756
- }
1757
- updateSegmentColor(index, color) {
1758
- if (!this.chart || !this.chart.data.datasets[0]) return;
1759
- this.chart.data.datasets[0].backgroundColor[index] = color;
1760
- this.chart.update("none");
1761
- const eventBus = this.getApp()?.events;
1762
- if (eventBus) {
1763
- eventBus.emit("chart:segment-color-changed", {
1764
- chart: this,
1765
- index,
1766
- color,
1767
- segment: this.getSegmentData(index)
1768
- });
1769
- }
1770
- }
1771
- addSegment(label, value, color = null) {
1772
- if (!this.chart || !this.chart.data) return;
1773
- const dataset = this.chart.data.datasets[0];
1774
- const segmentColor = color || this.colors[this.chart.data.labels.length % this.colors.length];
1775
- this.chart.data.labels.push(label);
1776
- dataset.data.push(value);
1777
- dataset.backgroundColor.push(segmentColor);
1778
- this.chart.update();
1779
- const eventBus = this.getApp()?.events;
1780
- if (eventBus) {
1781
- eventBus.emit("chart:segment-added", {
1782
- chart: this,
1783
- label,
1784
- value,
1785
- color: segmentColor,
1786
- index: this.chart.data.labels.length - 1
1787
- });
1788
- }
1789
- }
1790
- removeSegment(index) {
1791
- if (!this.chart || !this.chart.data || index < 0 || index >= this.chart.data.labels.length) {
1792
- return;
1793
- }
1794
- const dataset = this.chart.data.datasets[0];
1795
- const label = this.chart.data.labels[index];
1796
- const value = dataset.data[index];
1797
- this.chart.data.labels.splice(index, 1);
1798
- dataset.data.splice(index, 1);
1799
- dataset.backgroundColor.splice(index, 1);
1800
- if (this.selectedSegment === index) {
1801
- this.selectedSegment = null;
1802
- } else if (this.selectedSegment > index) {
1803
- this.selectedSegment--;
1804
- }
1805
- this.chart.update();
1806
- const eventBus = this.getApp()?.events;
1807
- if (eventBus) {
1808
- eventBus.emit("chart:segment-removed", {
1809
- chart: this,
1810
- label,
1811
- value,
1812
- index,
1813
- removedSegment: { label, value, index }
1814
- });
1815
- }
1816
- }
1817
- // Override applyThemeToOptions for pie chart specific theming
1818
- applyThemeToOptions(options) {
1819
- super.applyThemeToOptions(options);
1820
- const isDark = this.theme === "dark";
1821
- if (isDark) {
1822
- this.borderColor = "#404449";
1823
- } else {
1824
- this.borderColor = "#ffffff";
1825
- }
1826
- }
1827
- // Static dialog method
1828
- static async showDialog(options = {}) {
1829
- const {
1830
- title = "Pie Chart",
1831
- size = "lg",
1832
- ...chartOptions
1833
- } = options;
1834
- const chart = new PieChart({
1835
- ...chartOptions,
1836
- title
1837
- });
1838
- const dialog = new Dialog({
1839
- title,
1840
- body: chart,
1841
- size,
1842
- centered: true,
1843
- backdrop: "static",
1844
- keyboard: true,
1845
- buttons: [
1846
- {
1847
- text: "Export PNG",
1848
- action: "export",
1849
- class: "btn btn-outline-primary"
1850
- },
1851
- {
1852
- text: "Close",
1853
- action: "close",
1854
- class: "btn btn-secondary",
1855
- dismiss: true
1856
- }
1857
- ]
1858
- });
1859
- await dialog.render();
1860
- document.body.appendChild(dialog.element);
1861
- await dialog.mount();
1862
- dialog.show();
1863
- return new Promise((resolve) => {
1864
- dialog.on("hidden", () => {
1865
- dialog.destroy();
1866
- resolve(chart);
1867
- });
1868
- dialog.on("action:export", () => {
1869
- chart.exportChart("png");
1870
- });
1871
- dialog.on("action:close", () => {
1872
- dialog.hide();
1873
- });
1874
- });
1875
- }
1876
- }
1877
- class MetricsChart extends SeriesChart {
1878
- constructor(options = {}) {
1879
- super({
1880
- ...options,
1881
- chartType: options.chartType || "line",
1882
- title: options.title || "Metrics",
1883
- colors: options.colors,
1884
- yAxis: options.yAxis || { label: "Count", beginAtZero: true },
1885
- tooltip: options.tooltip || { y: "number" },
1886
- width: options.width,
1887
- height: options.height
1888
- });
1889
- this.endpoint = options.endpoint || "/api/metrics/fetch";
1890
- this.account = options.account || "global";
1891
- this.granularity = options.granularity || "hours";
1892
- this.slugs = options.slugs || null;
1893
- this.category = options.category || null;
1894
- this.dateStart = options.dateStart || null;
1895
- this.dateEnd = options.dateEnd || null;
1896
- this.defaultDateRange = options.defaultDateRange || "24h";
1897
- this.showGranularity = options.showGranularity !== false;
1898
- this.showDateRange = options.showDateRange !== false;
1899
- this.granularityOptions = options.granularityOptions || [
1900
- { value: "minutes", label: "Minutes" },
1901
- { value: "hours", label: "Hours" },
1902
- { value: "days", label: "Days" },
1903
- { value: "weeks", label: "Weeks" },
1904
- { value: "months", label: "Months" }
1905
- ];
1906
- this.quickRanges = options.quickRanges || [
1907
- { value: "1h", label: "1H" },
1908
- { value: "24h", label: "24H" },
1909
- { value: "7d", label: "7D" },
1910
- { value: "30d", label: "30D" }
1911
- ];
1912
- this.availableMetrics = options.availableMetrics || [
1913
- { value: "api_calls", label: "API Calls" },
1914
- { value: "api_errors", label: "API Errors" },
1915
- { value: "incident_evt", label: "System Events" },
1916
- { value: "incidents", label: "Incidents" }
1917
- ];
1918
- this.maxDatasets = Number.isFinite(options.maxDatasets) ? options.maxDatasets : null;
1919
- this.groupRemainingLabel = options.groupRemainingLabel || "Other";
1920
- this.isLoading = false;
1921
- this.lastFetch = null;
1922
- if (!this.dateStart || !this.dateEnd) {
1923
- this.setQuickRange(this.defaultDateRange);
1924
- }
1925
- }
1926
- async onInit() {
1927
- const controls = [];
1928
- if (this.showGranularity) {
1929
- controls.push({
1930
- type: "select",
1931
- name: "granularity",
1932
- action: "granularity-changed",
1933
- size: "sm",
1934
- options: this.granularityOptions.map((opt) => ({
1935
- value: opt.value,
1936
- label: opt.label,
1937
- selected: opt.value === this.granularity
1938
- }))
1939
- });
1940
- }
1941
- if (this.showDateRange) {
1942
- controls.push({
1943
- type: "button",
1944
- action: "show-date-range-dialog",
1945
- labelHtml: `<i class="bi bi-calendar-range me-1"></i>${this.formatDateRangeDisplay()}`,
1946
- title: "Select Date Range",
1947
- variant: "outline-secondary",
1948
- size: "sm"
1949
- });
1950
- }
1951
- this.headerConfig = {
1952
- titleHtml: this.title || "Metrics",
1953
- chartTitle: this.chartTitle || "",
1954
- showExport: this.exportEnabled === true,
1955
- showRefresh: this.refreshEnabled,
1956
- showTheme: false,
1957
- controls
1958
- };
1959
- await super.onInit();
1960
- }
1961
- // Action Handlers
1962
- async onActionGranularityChanged(event, element) {
1963
- const newGranularity = element.value;
1964
- if (newGranularity && newGranularity !== this.granularity) {
1965
- this.granularity = newGranularity;
1966
- await this.fetchData();
1967
- }
1968
- }
1969
- async onActionShowDateRangeDialog() {
1970
- try {
1971
- const result = await Dialog.showForm({
1972
- title: "Select Date Range",
1973
- size: "md",
1974
- fields: [
1975
- {
1976
- name: "dateRange",
1977
- type: "daterange",
1978
- label: "Date Range",
1979
- startName: "dt_start",
1980
- endName: "dt_end",
1981
- startDate: this.formatDateTimeLocal(this.dateStart),
1982
- endDate: this.formatDateTimeLocal(this.dateEnd),
1983
- required: true
1984
- }
1985
- ],
1986
- formConfig: {
1987
- options: {
1988
- submitButton: false,
1989
- resetButton: false
1990
- }
1991
- }
1992
- });
1993
- if (result && result.startDate && result.endDate) {
1994
- this.dateStart = new Date(result.startDate);
1995
- this.dateEnd = new Date(result.endDate);
1996
- const btn = this.element?.querySelector('[data-action="show-date-range-dialog"]');
1997
- if (btn) {
1998
- btn.innerHTML = `<i class="bi bi-calendar-range me-1"></i>${this.formatDateRangeDisplay()}`;
1999
- }
2000
- await this.fetchData();
2001
- }
2002
- } catch (error) {
2003
- console.error("Date range dialog error:", error);
2004
- }
2005
- }
2006
- // Data Management
2007
- buildApiParams() {
2008
- const params = {
2009
- granularity: this.granularity,
2010
- account: this.account,
2011
- with_labels: true
2012
- };
2013
- if (this.slugs) {
2014
- this.slugs.forEach((slug) => {
2015
- if (!params["slugs[]"]) params["slugs[]"] = [];
2016
- params["slugs[]"].push(slug);
2017
- });
2018
- }
2019
- if (this.category) {
2020
- params.category = this.category;
2021
- }
2022
- if (this.dateStart) {
2023
- params.dr_start = Math.floor(this.dateStart.getTime() / 1e3);
2024
- }
2025
- if (this.dateEnd) {
2026
- params.dr_end = Math.floor(this.dateEnd.getTime() / 1e3);
2027
- }
2028
- params._ = Date.now();
2029
- return params;
2030
- }
2031
- async fetchData() {
2032
- if (!this.endpoint) return;
2033
- this.isLoading = true;
2034
- this.showLoading();
2035
- try {
2036
- const rest = this.getApp()?.rest;
2037
- if (!rest) {
2038
- throw new Error("No REST client available");
2039
- }
2040
- const params = this.buildApiParams();
2041
- const response = await rest.GET(this.endpoint, params);
2042
- if (!response.success) {
2043
- throw new Error(response.message || "Network error");
2044
- }
2045
- if (!response.data?.status) {
2046
- throw new Error(response.data?.error || "Server error");
2047
- }
2048
- const metricsData = response.data.data;
2049
- const chartData = this.processMetricsData(metricsData);
2050
- await this.setData(chartData);
2051
- this.lastFetch = /* @__PURE__ */ new Date();
2052
- this.emit("metrics:data-loaded", {
2053
- chart: this,
2054
- data: metricsData,
2055
- params
2056
- });
2057
- } catch (error) {
2058
- console.error("Failed to fetch metrics data:", error);
2059
- this.showError(`Failed to load metrics: ${error.message}`);
2060
- this.emit("metrics:error", { chart: this, error });
2061
- } finally {
2062
- this.isLoading = false;
2063
- this.hideLoading();
2064
- }
2065
- }
2066
- processMetricsData(data) {
2067
- const { data: metricsData, labels } = data;
2068
- const metricEntries = Object.entries(metricsData || {});
2069
- const rankedEntries = metricEntries.map(([metric, values]) => {
2070
- const sanitizedValues = values.map((val) => {
2071
- if (val === null || val === void 0 || val === "") return 0;
2072
- return typeof val === "number" ? val : parseFloat(val) || 0;
2073
- });
2074
- const total = sanitizedValues.reduce((sum, val) => sum + val, 0);
2075
- return { metric, values: sanitizedValues, total };
2076
- });
2077
- rankedEntries.sort((a, b) => b.total - a.total);
2078
- let visibleEntries = rankedEntries;
2079
- let otherEntry = null;
2080
- if (this.maxDatasets && this.maxDatasets > 0 && rankedEntries.length > this.maxDatasets) {
2081
- visibleEntries = rankedEntries.slice(0, this.maxDatasets);
2082
- const remaining = rankedEntries.slice(this.maxDatasets);
2083
- const otherValues = labels.map(
2084
- (_, index) => remaining.reduce((sum, entry) => sum + (entry.values[index] || 0), 0)
2085
- );
2086
- otherEntry = {
2087
- metric: this.groupRemainingLabel,
2088
- values: otherValues,
2089
- total: otherValues.reduce((sum, val) => sum + val, 0),
2090
- isGrouped: true
2091
- };
2092
- }
2093
- const datasets = [];
2094
- const allEntries = otherEntry ? [...visibleEntries, otherEntry] : visibleEntries;
2095
- this.ensureColorPool(allEntries.length);
2096
- const backgroundAlpha = this.chartType === "line" ? 0.25 : 0.65;
2097
- allEntries.forEach((entry, index) => {
2098
- const baseColor = this.getColor(index);
2099
- datasets.push({
2100
- label: this.formatMetricLabel(entry.metric),
2101
- data: entry.values,
2102
- backgroundColor: this.withAlpha(baseColor, backgroundAlpha),
2103
- borderColor: baseColor,
2104
- borderWidth: 2,
2105
- tension: this.chartType === "line" ? 0.4 : 0,
2106
- fill: false,
2107
- pointRadius: this.chartType === "line" ? 3 : 0,
2108
- pointHoverRadius: 5
2109
- });
2110
- });
2111
- return { labels, datasets };
2112
- }
2113
- formatMetricLabel(metric) {
2114
- return metric.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2115
- }
2116
- // Date utilities
2117
- setQuickRange(range) {
2118
- const now = /* @__PURE__ */ new Date();
2119
- let startDate;
2120
- switch (range) {
2121
- case "1h":
2122
- startDate = new Date(now.getTime() - 60 * 60 * 1e3);
2123
- break;
2124
- case "24h":
2125
- startDate = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
2126
- break;
2127
- case "7d":
2128
- startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
2129
- break;
2130
- case "30d":
2131
- startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
2132
- break;
2133
- default:
2134
- startDate = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
2135
- }
2136
- this.dateStart = startDate;
2137
- this.dateEnd = now;
2138
- }
2139
- formatDateTimeLocal(date) {
2140
- if (!date) return "";
2141
- const year = date.getFullYear();
2142
- const month = String(date.getMonth() + 1).padStart(2, "0");
2143
- const day = String(date.getDate()).padStart(2, "0");
2144
- const hours = String(date.getHours()).padStart(2, "0");
2145
- const minutes = String(date.getMinutes()).padStart(2, "0");
2146
- return `${year}-${month}-${day}T${hours}:${minutes}`;
2147
- }
2148
- // Public API
2149
- setGranularity(granularity) {
2150
- this.granularity = granularity;
2151
- return this.fetchData();
2152
- }
2153
- setDateRange(startDate, endDate) {
2154
- this.dateStart = new Date(startDate);
2155
- this.dateEnd = new Date(endDate);
2156
- return this.fetchData();
2157
- }
2158
- setMetrics(slugs) {
2159
- this.slugs = [...slugs];
2160
- return this.fetchData();
2161
- }
2162
- getStats() {
2163
- const base = super.getStats();
2164
- return {
2165
- ...base,
2166
- lastFetch: this.lastFetch,
2167
- granularity: this.granularity,
2168
- slugs: [...this.slugs],
2169
- dateRange: {
2170
- start: this.dateStart,
2171
- end: this.dateEnd
2172
- }
2173
- };
2174
- }
2175
- }
2176
- class MiniChart extends View {
2177
- constructor(options = {}) {
2178
- super({
2179
- className: "mini-chart",
2180
- ...options
2181
- });
2182
- this.chartType = options.chartType || "line";
2183
- this.data = options.data || [];
2184
- this.width = options.width || "100%";
2185
- this.height = options.height || 30;
2186
- this.maintainAspectRatio = options.maintainAspectRatio || false;
2187
- this.color = options.color || "rgba(54, 162, 235, 1)";
2188
- this.fillColor = options.fillColor || "rgba(54, 162, 235, 0.1)";
2189
- this.strokeWidth = options.strokeWidth || 2;
2190
- this.barGap = options.barGap || 2;
2191
- this.fill = options.fill !== false;
2192
- this.smoothing = options.smoothing || 0.3;
2193
- this.padding = options.padding || 2;
2194
- this.minValue = options.minValue;
2195
- this.maxValue = options.maxValue;
2196
- this.showDots = options.showDots || false;
2197
- this.dotRadius = options.dotRadius || 2;
2198
- this.animate = options.animate !== false;
2199
- this.animationDuration = options.animationDuration || 300;
2200
- this.showTooltip = options.showTooltip !== false;
2201
- this.tooltipFormatter = options.tooltipFormatter || null;
2202
- this.tooltipTemplate = options.tooltipTemplate || null;
2203
- this.valueFormat = options.valueFormat || null;
2204
- this.labelFormat = options.labelFormat || null;
2205
- this.showCrosshair = options.showCrosshair !== false;
2206
- this.crosshairColor = options.crosshairColor || "rgba(0, 0, 0, 0.2)";
2207
- this.crosshairWidth = options.crosshairWidth || 1;
2208
- this.showXAxis = options.showXAxis || false;
2209
- this.xAxisColor = options.xAxisColor || this.color;
2210
- this.xAxisWidth = options.xAxisWidth || 1;
2211
- this.xAxisDashed = options.xAxisDashed !== false;
2212
- this.tooltip = null;
2213
- this.crosshair = null;
2214
- this.hoveredIndex = -1;
2215
- this.dataFormatter = dataFormatter;
2216
- this.labels = options.labels || null;
2217
- }
2218
- getTemplate() {
2219
- const widthStyle = typeof this.width === "number" ? `${this.width}px` : this.width;
2220
- const heightStyle = typeof this.height === "number" ? `${this.height}px` : this.height;
2221
- const preserveAspectRatio = this.maintainAspectRatio ? "xMidYMid meet" : "none";
2222
- return `
2223
- <div class="mini-chart-wrapper" style="position: relative; display: block; width: ${widthStyle}; height: ${heightStyle};">
2224
- <svg
2225
- class="mini-chart-svg"
2226
- width="100%"
2227
- height="100%"
2228
- viewBox="0 0 100 ${this.height}"
2229
- preserveAspectRatio="${preserveAspectRatio}"
2230
- style="display: block;">
2231
- </svg>
2232
- ${this.showTooltip ? '<div class="mini-chart-tooltip" style="display: none;"></div>' : ""}
2233
- </div>
2234
- `;
2235
- }
2236
- async onAfterRender() {
2237
- this.svg = this.element.querySelector(".mini-chart-svg");
2238
- this.tooltip = this.element.querySelector(".mini-chart-tooltip");
2239
- this.updateDimensions();
2240
- if (this.data && this.data.length > 0) {
2241
- this.renderChart();
2242
- }
2243
- if (this.showTooltip && this.svg) {
2244
- this.setupTooltip();
2245
- }
2246
- this.setupResizeObserver();
2247
- }
2248
- updateDimensions() {
2249
- if (!this.svg) return;
2250
- const rect = this.svg.getBoundingClientRect();
2251
- this.actualWidth = rect.width || 100;
2252
- this.actualHeight = rect.height || this.height;
2253
- this.svg.setAttribute("viewBox", `0 0 ${this.actualWidth} ${this.actualHeight}`);
2254
- }
2255
- setupResizeObserver() {
2256
- if (typeof ResizeObserver === "undefined") return;
2257
- this.resizeObserver = new ResizeObserver(() => {
2258
- this.updateDimensions();
2259
- if (this.data && this.data.length > 0) {
2260
- this.renderChart();
2261
- }
2262
- });
2263
- if (this.svg) {
2264
- this.resizeObserver.observe(this.svg);
2265
- }
2266
- }
2267
- renderChart() {
2268
- if (!this.svg || !this.data || this.data.length === 0) return;
2269
- this.svg.innerHTML = "";
2270
- const { min, max } = this.calculateBounds();
2271
- if (this.showXAxis) {
2272
- this.renderXAxis(min, max);
2273
- }
2274
- if (this.chartType === "line") {
2275
- this.renderLine(min, max);
2276
- } else if (this.chartType === "bar") {
2277
- this.renderBar(min, max);
2278
- }
2279
- if (this.showCrosshair) {
2280
- const height = this.getActualHeight();
2281
- this.crosshair = this.createSVGElement("line", {
2282
- x1: 0,
2283
- y1: 0,
2284
- x2: 0,
2285
- y2: height,
2286
- stroke: this.crosshairColor,
2287
- "stroke-width": this.crosshairWidth,
2288
- "stroke-dasharray": "3,3",
2289
- style: "display: none; pointer-events: none;"
2290
- });
2291
- this.svg.appendChild(this.crosshair);
2292
- }
2293
- if (this.showTooltip && this.tooltip) {
2294
- this.setupTooltip();
2295
- }
2296
- if (this.animate) {
2297
- this.applyAnimation();
2298
- }
2299
- }
2300
- renderXAxis(min, max) {
2301
- const width = this.getActualWidth();
2302
- const height = this.getActualHeight();
2303
- let yPos;
2304
- if (min <= 0 && max >= 0) {
2305
- const range = max - min;
2306
- const yScale = (height - this.padding * 2) / range;
2307
- yPos = height - this.padding - (0 - min) * yScale;
2308
- } else {
2309
- yPos = height - this.padding;
2310
- }
2311
- const xAxis = this.createSVGElement("line", {
2312
- x1: this.padding,
2313
- y1: yPos,
2314
- x2: width - this.padding,
2315
- y2: yPos,
2316
- stroke: this.xAxisColor,
2317
- "stroke-width": this.xAxisWidth,
2318
- "stroke-dasharray": this.xAxisDashed ? "2,2" : "none",
2319
- "stroke-opacity": "0.5"
2320
- });
2321
- this.svg.appendChild(xAxis);
2322
- }
2323
- calculateBounds() {
2324
- const values = this.data.map((d) => typeof d === "object" ? d.value : d);
2325
- let min = this.minValue !== void 0 ? this.minValue : Math.min(...values);
2326
- let max = this.maxValue !== void 0 ? this.maxValue : Math.max(...values);
2327
- const range = max - min;
2328
- if (range === 0) {
2329
- if (this.chartType === "bar") {
2330
- if (min === 0) {
2331
- min = 0;
2332
- max = 1;
2333
- } else {
2334
- min = min - 1;
2335
- max = max + 1;
2336
- }
2337
- } else {
2338
- min = min - 1;
2339
- max = max + 1;
2340
- }
2341
- }
2342
- return { min, max };
2343
- }
2344
- getActualWidth() {
2345
- return this.actualWidth || this.width || 100;
2346
- }
2347
- getActualHeight() {
2348
- return this.actualHeight || this.height || 30;
2349
- }
2350
- renderLine(min, max) {
2351
- const values = this.data.map((d) => typeof d === "object" ? d.value : d);
2352
- const points = this.calculatePoints(values, min, max);
2353
- if (this.fill) {
2354
- const areaPath = this.createAreaPath(points);
2355
- const area = this.createSVGElement("path", {
2356
- d: areaPath,
2357
- fill: this.fillColor,
2358
- stroke: "none"
2359
- });
2360
- this.svg.appendChild(area);
2361
- }
2362
- const linePath = this.smoothing > 0 ? this.createSmoothPath(points) : this.createLinePath(points);
2363
- const line = this.createSVGElement("path", {
2364
- d: linePath,
2365
- fill: "none",
2366
- stroke: this.color,
2367
- "stroke-width": this.strokeWidth,
2368
- "stroke-linecap": "round",
2369
- "stroke-linejoin": "round"
2370
- });
2371
- this.svg.appendChild(line);
2372
- if (this.showDots) {
2373
- points.forEach((point) => {
2374
- const dot = this.createSVGElement("circle", {
2375
- cx: point.x,
2376
- cy: point.y,
2377
- r: this.dotRadius,
2378
- fill: this.color
2379
- });
2380
- this.svg.appendChild(dot);
2381
- });
2382
- }
2383
- }
2384
- renderBar(min, max) {
2385
- const values = this.data.map((d) => typeof d === "object" ? d.value : d);
2386
- const points = this.calculatePoints(values, min, max);
2387
- const width = this.getActualWidth();
2388
- const height = this.getActualHeight();
2389
- const barWidth = (width - this.padding * 2 - this.barGap * (values.length - 1)) / values.length;
2390
- points.forEach((point, index) => {
2391
- const barHeight = height - this.padding * 2 - point.y + this.padding;
2392
- const x = point.x - barWidth / 2;
2393
- const y = point.y;
2394
- const bar = this.createSVGElement("rect", {
2395
- x,
2396
- y,
2397
- width: barWidth,
2398
- height: barHeight,
2399
- fill: this.color,
2400
- rx: 1,
2401
- // Slight rounding
2402
- "data-bar-index": index,
2403
- class: "mini-chart-bar"
2404
- });
2405
- this.svg.appendChild(bar);
2406
- });
2407
- }
2408
- calculatePoints(values, min, max) {
2409
- const range = max - min;
2410
- const width = this.getActualWidth();
2411
- const height = this.getActualHeight();
2412
- const xStep = (width - this.padding * 2) / (values.length - 1 || 1);
2413
- const yScale = (height - this.padding * 2) / range;
2414
- return values.map((value, index) => ({
2415
- x: this.padding + index * xStep,
2416
- y: height - this.padding - (value - min) * yScale
2417
- }));
2418
- }
2419
- createLinePath(points) {
2420
- if (points.length === 0) return "";
2421
- let path = `M ${points[0].x},${points[0].y}`;
2422
- for (let i = 1; i < points.length; i++) {
2423
- path += ` L ${points[i].x},${points[i].y}`;
2424
- }
2425
- return path;
2426
- }
2427
- createSmoothPath(points) {
2428
- if (points.length < 2) return this.createLinePath(points);
2429
- let path = `M ${points[0].x},${points[0].y}`;
2430
- for (let i = 0; i < points.length - 1; i++) {
2431
- const current = points[i];
2432
- const next = points[i + 1];
2433
- const cp1x = current.x + (next.x - current.x) * this.smoothing;
2434
- const cp1y = current.y;
2435
- const cp2x = next.x - (next.x - current.x) * this.smoothing;
2436
- const cp2y = next.y;
2437
- path += ` C ${cp1x},${cp1y} ${cp2x},${cp2y} ${next.x},${next.y}`;
2438
- }
2439
- return path;
2440
- }
2441
- createAreaPath(points) {
2442
- if (points.length === 0) return "";
2443
- const linePath = this.smoothing > 0 ? this.createSmoothPath(points) : this.createLinePath(points);
2444
- const lastPoint = points[points.length - 1];
2445
- const firstPoint = points[0];
2446
- const height = this.getActualHeight();
2447
- return `${linePath} L ${lastPoint.x},${height - this.padding} L ${firstPoint.x},${height - this.padding} Z`;
2448
- }
2449
- createSVGElement(tag, attributes = {}) {
2450
- const element = document.createElementNS("http://www.w3.org/2000/svg", tag);
2451
- Object.entries(attributes).forEach(([key, value]) => {
2452
- element.setAttribute(key, value);
2453
- });
2454
- return element;
2455
- }
2456
- applyAnimation() {
2457
- const paths = this.svg.querySelectorAll("path");
2458
- paths.forEach((path) => {
2459
- const length = path.getTotalLength();
2460
- path.style.strokeDasharray = length;
2461
- path.style.strokeDashoffset = length;
2462
- path.style.animation = `mini-chart-draw ${this.animationDuration}ms ease-out forwards`;
2463
- });
2464
- const bars = this.svg.querySelectorAll("rect");
2465
- bars.forEach((bar, index) => {
2466
- bar.style.transformOrigin = "bottom";
2467
- bar.style.animation = `mini-chart-bar-grow ${this.animationDuration}ms ease-out ${index * 20}ms forwards`;
2468
- bar.style.transform = "scaleY(0)";
2469
- });
2470
- }
2471
- setupTooltip() {
2472
- if (!this.svg || !this.tooltip) return;
2473
- const values = this.data.map((d) => typeof d === "object" ? d.value : d);
2474
- const points = this.calculatePoints(values, ...Object.values(this.calculateBounds()));
2475
- const width = this.getActualWidth();
2476
- const height = this.getActualHeight();
2477
- const barWidth = width / values.length;
2478
- points.forEach((point, index) => {
2479
- const hitArea = this.createSVGElement("rect", {
2480
- x: index * barWidth,
2481
- y: 0,
2482
- width: barWidth,
2483
- height,
2484
- fill: "transparent",
2485
- style: "cursor: pointer;"
2486
- });
2487
- hitArea.addEventListener("mouseenter", (e) => {
2488
- this.showTooltipAtIndex(index, e);
2489
- });
2490
- hitArea.addEventListener("mousemove", (e) => {
2491
- this.updateTooltipPosition(e);
2492
- });
2493
- hitArea.addEventListener("mouseleave", () => {
2494
- this.hideTooltip();
2495
- });
2496
- this.svg.appendChild(hitArea);
2497
- });
2498
- }
2499
- showTooltipAtIndex(index, event) {
2500
- if (!this.tooltip) return;
2501
- this.hoveredIndex = index;
2502
- const value = typeof this.data[index] === "object" ? this.data[index].value : this.data[index];
2503
- const dataLabel = typeof this.data[index] === "object" ? this.data[index].label : null;
2504
- const label = this.labels ? this.labels[index] : dataLabel;
2505
- let content;
2506
- if (this.tooltipTemplate && typeof this.tooltipTemplate === "function") {
2507
- content = this.tooltipTemplate({ value, label, index, data: this.data[index] });
2508
- } else {
2509
- let displayValue = value;
2510
- if (this.valueFormat && this.dataFormatter) {
2511
- displayValue = this.dataFormatter.pipe(value, this.valueFormat);
2512
- } else if (this.tooltipFormatter && typeof this.tooltipFormatter === "function") {
2513
- displayValue = this.tooltipFormatter(value, index);
2514
- } else {
2515
- displayValue = typeof value === "number" ? value.toLocaleString() : value;
2516
- }
2517
- let displayLabel = label;
2518
- if (label && this.labelFormat && this.dataFormatter) {
2519
- displayLabel = this.dataFormatter.pipe(label, this.labelFormat);
2520
- }
2521
- content = `<strong>${displayValue}</strong>`;
2522
- if (displayLabel) {
2523
- content = `<div class="mini-chart-tooltip-label">${displayLabel}</div>${content}`;
2524
- }
2525
- }
2526
- this.tooltip.innerHTML = content;
2527
- this.tooltip.style.display = "block";
2528
- this.updateTooltipPosition(event);
2529
- if (this.chartType === "bar") {
2530
- this.highlightBar(index);
2531
- }
2532
- if (this.crosshair && this.showCrosshair) {
2533
- const width = this.getActualWidth();
2534
- const barWidth = width / this.data.length;
2535
- const x = index * barWidth + barWidth / 2;
2536
- this.crosshair.setAttribute("x1", x);
2537
- this.crosshair.setAttribute("x2", x);
2538
- this.crosshair.style.display = "block";
2539
- }
2540
- }
2541
- updateTooltipPosition(event) {
2542
- if (!this.tooltip || this.tooltip.style.display === "none") return;
2543
- const rect = this.svg.getBoundingClientRect();
2544
- const x = event.clientX - rect.left;
2545
- const y = event.clientY - rect.top;
2546
- this.tooltip.style.left = `${x}px`;
2547
- this.tooltip.style.top = `${y - 10}px`;
2548
- this.tooltip.style.transform = "translate(-50%, -100%)";
2549
- }
2550
- hideTooltip() {
2551
- if (this.tooltip) {
2552
- this.tooltip.style.display = "none";
2553
- this.hoveredIndex = -1;
2554
- }
2555
- if (this.chartType === "bar") {
2556
- this.unhighlightBars();
2557
- }
2558
- if (this.crosshair) {
2559
- this.crosshair.style.display = "none";
2560
- }
2561
- }
2562
- highlightBar(index) {
2563
- if (!this.svg) return;
2564
- this.unhighlightBars();
2565
- const bar = this.svg.querySelector(`rect.mini-chart-bar[data-bar-index="${index}"]`);
2566
- if (bar) {
2567
- bar.style.opacity = "0.7";
2568
- }
2569
- }
2570
- unhighlightBars() {
2571
- if (!this.svg) return;
2572
- const bars = this.svg.querySelectorAll("rect.mini-chart-bar");
2573
- bars.forEach((bar) => {
2574
- bar.style.opacity = "1";
2575
- });
2576
- }
2577
- // Public API
2578
- setData(data) {
2579
- this.data = data;
2580
- if (this.svg) {
2581
- this.renderChart();
2582
- }
2583
- }
2584
- setColor(color) {
2585
- this.color = color;
2586
- if (this.svg) {
2587
- this.renderChart();
2588
- }
2589
- }
2590
- setType(type) {
2591
- if (["line", "bar"].includes(type)) {
2592
- this.chartType = type;
2593
- if (this.svg) {
2594
- this.renderChart();
2595
- }
2596
- }
2597
- }
2598
- resize(width, height) {
2599
- this.width = width;
2600
- this.height = height;
2601
- this.updateDimensions();
2602
- if (this.svg) {
2603
- this.renderChart();
2604
- }
2605
- }
2606
- async onBeforeDestroy() {
2607
- if (this.resizeObserver) {
2608
- this.resizeObserver.disconnect();
2609
- this.resizeObserver = null;
2610
- }
2611
- await super.onBeforeDestroy();
2612
- }
2613
- }
2614
- class MetricsMiniChart extends MiniChart {
2615
- constructor(options = {}) {
2616
- super(options);
2617
- this.endpoint = options.endpoint || "/api/metrics/fetch";
2618
- this.account = options.account || "global";
2619
- this.granularity = options.granularity || "hours";
2620
- this.slugs = options.slugs || null;
2621
- this.category = options.category || null;
2622
- this.dateStart = options.dateStart || null;
2623
- this.dateEnd = options.dateEnd || null;
2624
- this.defaultDateRange = options.defaultDateRange || null;
2625
- this.isLoading = false;
2626
- this.lastFetch = null;
2627
- this.refreshInterval = options.refreshInterval;
2628
- if (this.defaultDateRange && !this.dateStart && !this.dateEnd) {
2629
- this.setQuickRange(this.defaultDateRange);
2630
- }
2631
- if (this.slugs && !Array.isArray(this.slugs)) {
2632
- this.slugs = [this.slugs];
2633
- }
2634
- }
2635
- async onAfterRender() {
2636
- await super.onAfterRender();
2637
- if (this.endpoint && (!this.data || this.data.length === 0)) {
2638
- this.fetchData();
2639
- }
2640
- if (this.refreshInterval && this.endpoint) {
2641
- this.startAutoRefresh();
2642
- }
2643
- }
2644
- buildApiParams() {
2645
- const params = {
2646
- granularity: this.granularity,
2647
- account: this.account,
2648
- with_labels: true
2649
- };
2650
- if (this.slugs && this.slugs.length > 0) {
2651
- this.slugs.forEach((slug) => {
2652
- if (!params["slugs[]"]) params["slugs[]"] = [];
2653
- params["slugs[]"].push(slug);
2654
- });
2655
- }
2656
- if (this.category) {
2657
- params.category = this.category;
2658
- }
2659
- if (this.dateStart) {
2660
- params.dr_start = Math.floor(this.dateStart.getTime() / 1e3);
2661
- }
2662
- if (this.dateEnd) {
2663
- params.dr_end = Math.floor(this.dateEnd.getTime() / 1e3);
2664
- }
2665
- params._ = Date.now();
2666
- return params;
2667
- }
2668
- async fetchData() {
2669
- if (!this.endpoint) return;
2670
- this.isLoading = true;
2671
- try {
2672
- const rest = this.getApp()?.rest;
2673
- if (!rest) {
2674
- throw new Error("No REST client available");
2675
- }
2676
- const params = this.buildApiParams();
2677
- const response = await rest.GET(this.endpoint, params);
2678
- if (!response.success) {
2679
- throw new Error(response.message || "Network error");
2680
- }
2681
- if (!response.data?.status) {
2682
- throw new Error(response.data?.error || "Server error");
2683
- }
2684
- const metricsData = response.data.data;
2685
- this.processMetricsData(metricsData);
2686
- this.lastFetch = /* @__PURE__ */ new Date();
2687
- await this.render();
2688
- this.emit("metrics:loaded", { chart: this, data: metricsData, params });
2689
- } catch (error) {
2690
- console.error("Failed to fetch metrics:", error);
2691
- this.emit("metrics:error", { chart: this, error });
2692
- } finally {
2693
- this.isLoading = false;
2694
- }
2695
- }
2696
- processMetricsData(metricsData) {
2697
- const { data: metrics, labels } = metricsData;
2698
- if (!metrics) return;
2699
- const metricKeys = Object.keys(metrics);
2700
- if (metricKeys.length === 0) return;
2701
- const metricSlug = metricKeys[0];
2702
- const values = metrics[metricSlug];
2703
- const sanitizedValues = values.map((val) => {
2704
- if (val === null || val === void 0 || val === "") return 0;
2705
- return typeof val === "number" ? val : parseFloat(val) || 0;
2706
- });
2707
- this.labels = labels || null;
2708
- this.setData(sanitizedValues);
2709
- }
2710
- setQuickRange(range) {
2711
- const now = /* @__PURE__ */ new Date();
2712
- let startDate;
2713
- switch (range) {
2714
- case "1h":
2715
- startDate = new Date(now.getTime() - 60 * 60 * 1e3);
2716
- break;
2717
- case "24h":
2718
- startDate = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
2719
- break;
2720
- case "7d":
2721
- startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
2722
- break;
2723
- case "30d":
2724
- startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
2725
- break;
2726
- default:
2727
- startDate = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
2728
- }
2729
- this.dateStart = startDate;
2730
- this.dateEnd = now;
2731
- }
2732
- startAutoRefresh() {
2733
- if (this.refreshTimer) {
2734
- clearInterval(this.refreshTimer);
2735
- }
2736
- this.refreshTimer = setInterval(() => {
2737
- this.fetchData();
2738
- }, this.refreshInterval);
2739
- }
2740
- stopAutoRefresh() {
2741
- if (this.refreshTimer) {
2742
- clearInterval(this.refreshTimer);
2743
- this.refreshTimer = null;
2744
- }
2745
- }
2746
- // Public API
2747
- setGranularity(granularity) {
2748
- this.granularity = granularity;
2749
- return this.fetchData();
2750
- }
2751
- setDateRange(startDate, endDate) {
2752
- this.dateStart = new Date(startDate);
2753
- this.dateEnd = new Date(endDate);
2754
- return this.fetchData();
2755
- }
2756
- setMetrics(slugs) {
2757
- this.slugs = Array.isArray(slugs) ? slugs : [slugs];
2758
- return this.fetchData();
2759
- }
2760
- refresh() {
2761
- return this.fetchData();
2762
- }
2763
- async onBeforeDestroy() {
2764
- this.stopAutoRefresh();
2765
- await super.onBeforeDestroy();
2766
- }
2767
- }
2768
- class SettingsView extends View {
2769
- constructor(options = {}) {
2770
- super({
2771
- tagName: "div",
2772
- className: "metrics-chart-settings-content",
2773
- ...options
2774
- });
2775
- this.granularity = options.granularity;
2776
- this.chartType = options.chartType;
2777
- this.dateStart = options.dateStart;
2778
- this.dateEnd = options.dateEnd;
2779
- this.showDateRange = options.showDateRange;
2780
- }
2781
- formatDateForInput(date) {
2782
- if (!date) return "";
2783
- if (typeof date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
2784
- return date;
2785
- }
2786
- const d = date instanceof Date ? date : new Date(date);
2787
- if (isNaN(d.getTime())) return "";
2788
- const year = d.getFullYear();
2789
- const month = String(d.getMonth() + 1).padStart(2, "0");
2790
- const day = String(d.getDate()).padStart(2, "0");
2791
- return `${year}-${month}-${day}`;
2792
- }
2793
- getTemplate() {
2794
- return `
2795
- <div style="min-width: 220px;">
2796
- <div class="d-flex justify-content-between align-items-center mb-2 pb-2 border-bottom">
2797
- <h6 class="mb-0">Chart Settings</h6>
2798
- <button type="button" class="btn-close btn-close-sm" data-action="close" aria-label="Close"></button>
2799
- </div>
2800
-
2801
- <label class="form-label small mb-1">Granularity</label>
2802
- <select class="form-select form-select-sm mb-2" data-setting="granularity">
2803
- <option value="hours" ${this.granularity === "hours" ? "selected" : ""}>Hours</option>
2804
- <option value="days" ${this.granularity === "days" ? "selected" : ""}>Days</option>
2805
- <option value="weeks" ${this.granularity === "weeks" ? "selected" : ""}>Weeks</option>
2806
- <option value="months" ${this.granularity === "months" ? "selected" : ""}>Months</option>
2807
- <option value="years" ${this.granularity === "years" ? "selected" : ""}>Years</option>
2808
- </select>
2809
-
2810
- <label class="form-label small mb-1">Chart Type</label>
2811
- <select class="form-select form-select-sm mb-2" data-setting="chartType">
2812
- <option value="line" ${this.chartType === "line" ? "selected" : ""}>Line</option>
2813
- <option value="bar" ${this.chartType === "bar" ? "selected" : ""}>Bar</option>
2814
- </select>
2815
-
2816
- ${this.showDateRange ? `
2817
- <label class="form-label small mb-1">Date Range</label>
2818
- <input type="date" class="form-control form-control-sm mb-1" data-setting="dateStart" value="${this.formatDateForInput(this.dateStart)}" />
2819
- <input type="date" class="form-control form-control-sm mb-2" data-setting="dateEnd" value="${this.formatDateForInput(this.dateEnd)}" />
2820
- ` : ""}
2821
-
2822
- <div class="d-grid gap-2">
2823
- <button type="button" class="btn btn-sm btn-primary" data-action="apply">Apply</button>
2824
- <button type="button" class="btn btn-sm btn-outline-secondary" data-action="cancel">Cancel</button>
2825
- </div>
2826
- </div>
2827
- `;
2828
- }
2829
- async onActionApply() {
2830
- const granularity = this.element.querySelector('[data-setting="granularity"]')?.value;
2831
- const chartType = this.element.querySelector('[data-setting="chartType"]')?.value;
2832
- const dateStart = this.element.querySelector('[data-setting="dateStart"]')?.value;
2833
- const dateEnd = this.element.querySelector('[data-setting="dateEnd"]')?.value;
2834
- this.emit("settings:apply", { granularity, chartType, dateStart, dateEnd });
2835
- }
2836
- async onActionCancel() {
2837
- this.emit("settings:cancel");
2838
- }
2839
- async onActionClose() {
2840
- this.emit("settings:cancel");
2841
- }
2842
- }
2843
- class MetricsMiniChartWidget extends View {
2844
- constructor(options = {}) {
2845
- super({
2846
- ...options,
2847
- tagName: "div",
2848
- className: `metrics-mini-chart-widget ${options.className || ""}`.trim()
2849
- });
2850
- this.icon = options.icon || null;
2851
- this.title = options.title || "";
2852
- this.subtitle = options.subtitle || "";
2853
- this.background = options.background || null;
2854
- this.textColor = options.textColor || null;
2855
- this.showSettings = options.showSettings || false;
2856
- this.settingsKey = options.settingsKey || null;
2857
- this.showDateRange = options.showDateRange || false;
2858
- this.showRefresh = options.showRefresh !== false;
2859
- this.showTrending = !!options.showTrending;
2860
- this.trendRange = options.trendRange ?? null;
2861
- this.trendOffset = options.trendOffset ?? 0;
2862
- this.prevTrendOffset = options.prevTrendOffset ?? 0;
2863
- this.total = 0;
2864
- this.lastValue = 0;
2865
- this.prevValue = 0;
2866
- this.trendingPercent = 0;
2867
- this.trendingUp = null;
2868
- this.hasTrending = false;
2869
- this.trendingClass = "metrics-mini-chart-trending-text";
2870
- this.trendingIcon = "";
2871
- this.trendingLabel = "";
2872
- this.chartOptions = {
2873
- endpoint: options.endpoint,
2874
- account: options.account,
2875
- granularity: options.granularity,
2876
- slugs: options.slugs,
2877
- category: options.category,
2878
- dateStart: options.dateStart,
2879
- dateEnd: options.dateEnd,
2880
- defaultDateRange: options.defaultDateRange,
2881
- refreshInterval: options.refreshInterval,
2882
- chartType: options.chartType || "line",
2883
- showTooltip: options.showTooltip !== void 0 ? options.showTooltip : true,
2884
- showXAxis: options.showXAxis || false,
2885
- height: options.height || 80,
2886
- width: options.chartWidth || options.width || "100%",
2887
- color: options.color,
2888
- fill: options.fill !== void 0 ? options.fill : true,
2889
- fillColor: options.fillColor,
2890
- smoothing: options.smoothing ?? 0.3,
2891
- strokeWidth: options.strokeWidth,
2892
- barGap: options.barGap,
2893
- valueFormat: options.valueFormat,
2894
- labelFormat: options.labelFormat,
2895
- tooltipFormatter: options.tooltipFormatter,
2896
- tooltipTemplate: options.tooltipTemplate,
2897
- showCrosshair: options.showCrosshair,
2898
- crosshairColor: options.crosshairColor,
2899
- crosshairWidth: options.crosshairWidth,
2900
- xAxisColor: options.xAxisColor,
2901
- xAxisWidth: options.xAxisWidth,
2902
- xAxisDashed: options.xAxisDashed,
2903
- padding: options.padding,
2904
- minValue: options.minValue,
2905
- maxValue: options.maxValue,
2906
- showDots: options.showDots,
2907
- dotRadius: options.dotRadius,
2908
- animate: options.animate,
2909
- animationDuration: options.animationDuration
2910
- };
2911
- }
2912
- async onInit() {
2913
- if (this.showSettings && this.settingsKey) {
2914
- this._loadSettings();
2915
- }
2916
- this.chart = new MetricsMiniChart({
2917
- ...this.chartOptions,
2918
- containerId: "chart"
2919
- });
2920
- this.addChild(this.chart);
2921
- this.header = new View({
2922
- containerId: "chart-header",
2923
- title: this.title,
2924
- icon: this.icon,
2925
- template: `
2926
- <div class="d-flex justify-content-between align-items-start mb-2">
2927
- <div class="flex-grow-1">
2928
- <h6 class="card-title mb-1" style="${this.textColor ? `color: ${this.textColor}` : ""}">${this.title}</h6>
2929
- <div class="metrics-mini-chart-subtitle" style="${this.textColor ? `color: ${this.textColor}` : ""}">${this.subtitle}</div>
2930
- {{#hasTrending}}
2931
- <div class="{{trendingClass}}" style="${this.textColor ? `color: ${this.textColor}` : ""}">
2932
- <i class="{{trendingIcon}} me-1"></i>{{trendingLabel}}
2933
- </div>
2934
- {{/hasTrending}}
2935
- </div>
2936
- ${this.icon ? `<i class="${this.icon} fs-4 flex-shrink-0" aria-hidden="true" style="${this.textColor ? `color: ${this.textColor}` : ""}"></i>` : ""}
2937
- </div>`
2938
- });
2939
- this.addChild(this.header);
2940
- if (this.showSettings) {
2941
- this.settingsView = new SettingsView({
2942
- containerId: "settings",
2943
- granularity: this.chartOptions.granularity,
2944
- chartType: this.chartOptions.chartType,
2945
- dateStart: this.chartOptions.dateStart,
2946
- dateEnd: this.chartOptions.dateEnd,
2947
- showDateRange: this.showDateRange
2948
- });
2949
- this.settingsView.on("settings:apply", (data) => this._handleSettingsApply(data));
2950
- this.settingsView.on("settings:cancel", () => this._handleSettingsCancel());
2951
- this.addChild(this.settingsView);
2952
- }
2953
- if (this.chart?.on) {
2954
- this.chart.on("metrics:loaded", this.onChildMetricsLoaded, this);
2955
- }
2956
- this.updateFromChartData({ render: false });
2957
- }
2958
- async onAfterRender() {
2959
- await super.onAfterRender();
2960
- if (this.showSettings && this.settingsView) {
2961
- this._initSettingsPopover();
2962
- }
2963
- }
2964
- onChildMetricsLoaded() {
2965
- this.updateFromChartData({ render: true });
2966
- }
2967
- updateFromChartData({ render = true } = {}) {
2968
- const values = Array.isArray(this.chart?.data) ? this.chart.data : null;
2969
- if (!values || values.length === 0) {
2970
- this.total = 0;
2971
- this.hasTrending = false;
2972
- this.header.title = this.title;
2973
- if (render) this.render();
2974
- return;
2975
- }
2976
- const nums = values.map((v) => {
2977
- if (typeof v === "number") return v;
2978
- if (v && typeof v.value === "number") return v.value;
2979
- const n = parseFloat(v);
2980
- return Number.isNaN(n) ? 0 : n;
2981
- });
2982
- this.header.title = this.title;
2983
- this.header.total = nums.reduce((a, b) => a + b, 0);
2984
- const offset = Math.max(0, parseInt(this.trendOffset || 0, 10) || 0);
2985
- const endIndex = Math.max(0, nums.length - 1 - offset);
2986
- this.header.now_value = nums[endIndex];
2987
- this._updateGranularityLabels();
2988
- let hasTrend = false;
2989
- let lastSum = 0;
2990
- let prevSum = 0;
2991
- const k = this.trendRange && this.trendRange >= 2 ? Math.max(1, Math.floor(this.trendRange / 2)) : 1;
2992
- if (endIndex >= 0) {
2993
- const lastEnd = endIndex;
2994
- const lastStart = lastEnd - (k - 1);
2995
- let prevStart, prevEnd;
2996
- if (this.prevTrendOffset && this.prevTrendOffset > 0) {
2997
- prevStart = lastStart - this.prevTrendOffset;
2998
- prevEnd = lastEnd - this.prevTrendOffset;
2999
- } else {
3000
- prevEnd = lastStart - 1;
3001
- prevStart = prevEnd - (k - 1);
3002
- }
3003
- if (lastStart >= 0 && prevStart >= 0) {
3004
- const sumRange = (arr, s, e) => {
3005
- let sum = 0;
3006
- for (let i = s; i <= e; i++) sum += arr[i] || 0;
3007
- return sum;
3008
- };
3009
- lastSum = sumRange(nums, lastStart, lastEnd);
3010
- prevSum = sumRange(nums, prevStart, prevEnd);
3011
- hasTrend = true;
3012
- }
3013
- }
3014
- if (!hasTrend) {
3015
- const prevIndex = endIndex - (this.prevTrendOffset && this.prevTrendOffset > 0 ? this.prevTrendOffset : 1);
3016
- if (prevIndex >= 0) {
3017
- lastSum = nums[endIndex];
3018
- prevSum = nums[prevIndex];
3019
- hasTrend = true;
3020
- }
3021
- }
3022
- if (hasTrend) {
3023
- this.header.lastValue = lastSum;
3024
- this.header.prevValue = prevSum;
3025
- let percent = 0;
3026
- if (prevSum === 0) {
3027
- percent = lastSum > 0 ? 100 : 0;
3028
- } else {
3029
- percent = (lastSum - prevSum) / Math.abs(prevSum) * 100;
3030
- }
3031
- this.header.trendingPercent = percent;
3032
- this.header.trendingUp = percent >= 0;
3033
- if (!this.textColor) {
3034
- this.header.trendingClass = this.header.trendingUp ? "text-success" : "text-danger";
3035
- } else {
3036
- this.header.trendingClass = "";
3037
- }
3038
- this.header.trendingIcon = this.header.trendingUp ? "bi bi-arrow-up" : "bi bi-arrow-down";
3039
- const sign = percent > 0 ? "+" : "";
3040
- this.header.trendingLabel = `${sign}${percent.toFixed(1)}%`;
3041
- this.header.hasTrending = this.showTrending;
3042
- } else {
3043
- this.header.hasTrending = false;
3044
- }
3045
- if (render) {
3046
- this.header.render();
3047
- }
3048
- }
3049
- _updateGranularityLabels() {
3050
- const granularity = this.chartOptions.granularity || "days";
3051
- const nowLabels = {
3052
- "hours": "This Hour",
3053
- "days": "Today",
3054
- "weeks": "This Week",
3055
- "months": "This Month",
3056
- "years": "This Year"
3057
- };
3058
- const totalLabels = {
3059
- "hours": "Total (24h)",
3060
- "days": "Total (Period)",
3061
- "weeks": "Total (Period)",
3062
- "months": "Total (Period)",
3063
- "years": "Total (Period)"
3064
- };
3065
- this.header.now_label = nowLabels[granularity] || "Current";
3066
- this.header.total_label = totalLabels[granularity] || "Total";
3067
- }
3068
- get cardStyle() {
3069
- const styles = [];
3070
- if (this.background) styles.push(`background: ${this.background}`);
3071
- if (this.textColor) styles.push(`color: ${this.textColor}`);
3072
- styles.push("border: 0");
3073
- return styles.join("; ");
3074
- }
3075
- async getTemplate() {
3076
- return `
3077
- <div class="card h-100 shadow-sm" style="${this.cardStyle}; position: relative;">
3078
- ${this.showRefresh || this.showSettings ? `
3079
- <div class="metrics-chart-actions">
3080
- ${this.showRefresh ? `
3081
- <button class="btn btn-link p-0 text-muted metrics-refresh-btn" type="button" data-action="refresh-chart" style="${this.textColor ? `color: ${this.textColor} !important` : ""}">
3082
- <i class="bi bi-arrow-clockwise"></i>
3083
- </button>
3084
- ` : ""}
3085
- ${this.showSettings ? `
3086
- <button class="btn btn-link p-0 text-muted metrics-settings-btn" type="button" data-action="toggle-settings" style="${this.textColor ? `color: ${this.textColor} !important` : ""}">
3087
- <i class="bi bi-gear-fill"></i>
3088
- </button>
3089
- ` : ""}
3090
- </div>
3091
- ` : ""}
3092
- <div class="card-body p-3">
3093
- <div data-container="chart-header"></div>
3094
- <div data-container="chart"></div>
3095
- <div data-container="settings" style="display: none;"></div>
3096
- </div>
3097
- </div>
3098
- `;
3099
- }
3100
- async onBeforeDestroy() {
3101
- if (this._settingsPopover) {
3102
- this._settingsPopover.dispose();
3103
- this._settingsPopover = null;
3104
- }
3105
- if (this.chart?.off) {
3106
- this.chart.off("metrics:loaded", this.onChildMetricsLoaded, this);
3107
- }
3108
- await super.onBeforeDestroy();
3109
- }
3110
- /**
3111
- * Toggle settings popover
3112
- */
3113
- async onActionToggleSettings(event, element) {
3114
- if (!this._settingsPopover) {
3115
- this._initSettingsPopover();
3116
- }
3117
- this._settingsPopover?.toggle();
3118
- }
3119
- /**
3120
- * Initialize settings popover (once)
3121
- * @private
3122
- */
3123
- _initSettingsPopover() {
3124
- const button = this.element.querySelector('[data-action="toggle-settings"]');
3125
- if (!button || !this.settingsView || !this.settingsView.element) return;
3126
- if (this._settingsPopover) return;
3127
- this._settingsPopover = new bootstrap.Popover(button, {
3128
- content: this.settingsView.element,
3129
- html: true,
3130
- placement: "bottom",
3131
- trigger: "manual",
3132
- sanitize: false,
3133
- customClass: "metrics-chart-settings-popover"
3134
- });
3135
- }
3136
- /**
3137
- * Handle settings apply
3138
- * @private
3139
- */
3140
- async _handleSettingsApply(data) {
3141
- if (this._settingsPopover) {
3142
- this._settingsPopover.hide();
3143
- }
3144
- let hasChanges = false;
3145
- let granularityChanged = false;
3146
- let datesExplicitlySet = false;
3147
- if (data.dateStart && data.dateStart !== this.chartOptions.dateStart || data.dateEnd && data.dateEnd !== this.chartOptions.dateEnd) {
3148
- datesExplicitlySet = true;
3149
- }
3150
- if (data.granularity && data.granularity !== this.chartOptions.granularity) {
3151
- this.chartOptions.granularity = data.granularity;
3152
- this.chart.granularity = data.granularity;
3153
- granularityChanged = true;
3154
- hasChanges = true;
3155
- }
3156
- if (data.chartType && data.chartType !== this.chartOptions.chartType) {
3157
- this.chartOptions.chartType = data.chartType;
3158
- this.chart.chartType = data.chartType;
3159
- hasChanges = true;
3160
- }
3161
- if (datesExplicitlySet) {
3162
- if (data.dateStart) {
3163
- this.chartOptions.dateStart = new Date(data.dateStart);
3164
- this.chart.dateStart = new Date(data.dateStart);
3165
- }
3166
- if (data.dateEnd) {
3167
- this.chartOptions.dateEnd = new Date(data.dateEnd);
3168
- this.chart.dateEnd = new Date(data.dateEnd);
3169
- }
3170
- hasChanges = true;
3171
- } else if (granularityChanged && (this.chartOptions.dateStart || this.chartOptions.dateEnd)) {
3172
- const endDate = /* @__PURE__ */ new Date();
3173
- let startDate;
3174
- switch (data.granularity) {
3175
- case "hours":
3176
- startDate = new Date(endDate.getTime() - 24 * 60 * 60 * 1e3);
3177
- break;
3178
- case "days":
3179
- startDate = new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3);
3180
- break;
3181
- case "weeks":
3182
- startDate = new Date(endDate.getTime() - 12 * 7 * 24 * 60 * 60 * 1e3);
3183
- break;
3184
- case "months":
3185
- startDate = new Date(endDate);
3186
- startDate.setMonth(startDate.getMonth() - 12);
3187
- break;
3188
- case "years":
3189
- startDate = new Date(endDate);
3190
- startDate.setFullYear(startDate.getFullYear() - 5);
3191
- break;
3192
- default:
3193
- startDate = new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3);
3194
- }
3195
- this.chartOptions.dateStart = startDate;
3196
- this.chart.dateStart = startDate;
3197
- this.chartOptions.dateEnd = endDate;
3198
- this.chart.dateEnd = endDate;
3199
- }
3200
- if (hasChanges) {
3201
- this._saveSettings();
3202
- await this.chart.refresh();
3203
- }
3204
- }
3205
- /**
3206
- * Handle settings cancel
3207
- * @private
3208
- */
3209
- _handleSettingsCancel() {
3210
- if (this._settingsPopover) {
3211
- this._settingsPopover.hide();
3212
- }
3213
- }
3214
- /**
3215
- * Handle refresh button click
3216
- */
3217
- async onActionRefreshChart(event, element) {
3218
- const icon = element.querySelector("i");
3219
- if (icon) {
3220
- icon.classList.add("spin");
3221
- }
3222
- if (this.chart) {
3223
- if (this.account) this.chart.account = this.account;
3224
- await this.chart.refresh();
3225
- }
3226
- if (icon) {
3227
- icon.classList.remove("spin");
3228
- }
3229
- }
3230
- refresh() {
3231
- if (this.chart) {
3232
- if (this.account) this.chart.account = this.account;
3233
- this.chart.refresh();
3234
- }
3235
- }
3236
- _loadSettings() {
3237
- if (!this.settingsKey) return;
3238
- try {
3239
- const stored = localStorage.getItem(`metrics-chart-${this.settingsKey}`);
3240
- if (stored) {
3241
- const settings = JSON.parse(stored);
3242
- if (settings.granularity) {
3243
- this.chartOptions.granularity = settings.granularity;
3244
- }
3245
- if (settings.chartType) {
3246
- this.chartOptions.chartType = settings.chartType;
3247
- }
3248
- if (settings.dateStart !== void 0) {
3249
- this.chartOptions.dateStart = settings.dateStart;
3250
- }
3251
- if (settings.dateEnd !== void 0) {
3252
- this.chartOptions.dateEnd = settings.dateEnd;
3253
- }
3254
- }
3255
- } catch (error) {
3256
- console.error("Failed to load chart settings:", error);
3257
- }
3258
- }
3259
- _saveSettings() {
3260
- if (!this.settingsKey) return;
3261
- try {
3262
- const settings = {
3263
- granularity: this.chartOptions.granularity,
3264
- chartType: this.chartOptions.chartType,
3265
- dateStart: this.chartOptions.dateStart,
3266
- dateEnd: this.chartOptions.dateEnd
3267
- };
3268
- localStorage.setItem(`metrics-chart-${this.settingsKey}`, JSON.stringify(settings));
3269
- } catch (error) {
3270
- console.error("Failed to save chart settings:", error);
3271
- }
3272
- }
3273
- }
3274
- export {
3275
- BaseChart as B,
3276
- MetricsChart as M,
3277
- PieChart as P,
3278
- SeriesChart as S,
3279
- MiniChart as a,
3280
- MetricsMiniChart as b,
3281
- MetricsMiniChartWidget as c
3282
- };
3283
- //# sourceMappingURL=MetricsMiniChartWidget-DvKd7Qrk.js.map