web-mojo 2.1.46

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 (91) hide show
  1. package/LICENSE +198 -0
  2. package/README.md +510 -0
  3. package/dist/admin.cjs.js +2 -0
  4. package/dist/admin.cjs.js.map +1 -0
  5. package/dist/admin.css +621 -0
  6. package/dist/admin.es.js +7973 -0
  7. package/dist/admin.es.js.map +1 -0
  8. package/dist/auth.cjs.js +2 -0
  9. package/dist/auth.cjs.js.map +1 -0
  10. package/dist/auth.css +804 -0
  11. package/dist/auth.es.js +2168 -0
  12. package/dist/auth.es.js.map +1 -0
  13. package/dist/charts.cjs.js +2 -0
  14. package/dist/charts.cjs.js.map +1 -0
  15. package/dist/charts.css +1002 -0
  16. package/dist/charts.es.js +16 -0
  17. package/dist/charts.es.js.map +1 -0
  18. package/dist/chunks/ContextMenu-BrHqj0fn.js +80 -0
  19. package/dist/chunks/ContextMenu-BrHqj0fn.js.map +1 -0
  20. package/dist/chunks/ContextMenu-gEcpSz56.js +2 -0
  21. package/dist/chunks/ContextMenu-gEcpSz56.js.map +1 -0
  22. package/dist/chunks/DataView-DPryYpEW.js +2 -0
  23. package/dist/chunks/DataView-DPryYpEW.js.map +1 -0
  24. package/dist/chunks/DataView-DjZQrpba.js +843 -0
  25. package/dist/chunks/DataView-DjZQrpba.js.map +1 -0
  26. package/dist/chunks/Dialog-BsRx4eg3.js +2 -0
  27. package/dist/chunks/Dialog-BsRx4eg3.js.map +1 -0
  28. package/dist/chunks/Dialog-DSlctbon.js +1377 -0
  29. package/dist/chunks/Dialog-DSlctbon.js.map +1 -0
  30. package/dist/chunks/FilePreviewView-BmFHzK5K.js +5868 -0
  31. package/dist/chunks/FilePreviewView-BmFHzK5K.js.map +1 -0
  32. package/dist/chunks/FilePreviewView-DcdRl_ta.js +2 -0
  33. package/dist/chunks/FilePreviewView-DcdRl_ta.js.map +1 -0
  34. package/dist/chunks/FormView-CmBuwKGD.js +2 -0
  35. package/dist/chunks/FormView-CmBuwKGD.js.map +1 -0
  36. package/dist/chunks/FormView-DqUBMPJ9.js +5054 -0
  37. package/dist/chunks/FormView-DqUBMPJ9.js.map +1 -0
  38. package/dist/chunks/MetricsChart-CM4CI6eA.js +2095 -0
  39. package/dist/chunks/MetricsChart-CM4CI6eA.js.map +1 -0
  40. package/dist/chunks/MetricsChart-CPidSMaN.js +2 -0
  41. package/dist/chunks/MetricsChart-CPidSMaN.js.map +1 -0
  42. package/dist/chunks/PDFViewer-BNQlnS83.js +2 -0
  43. package/dist/chunks/PDFViewer-BNQlnS83.js.map +1 -0
  44. package/dist/chunks/PDFViewer-Dyo-Oeyd.js +946 -0
  45. package/dist/chunks/PDFViewer-Dyo-Oeyd.js.map +1 -0
  46. package/dist/chunks/Page-B524zSQs.js +351 -0
  47. package/dist/chunks/Page-B524zSQs.js.map +1 -0
  48. package/dist/chunks/Page-BFgj0pAA.js +2 -0
  49. package/dist/chunks/Page-BFgj0pAA.js.map +1 -0
  50. package/dist/chunks/TokenManager-BXNva8Jk.js +287 -0
  51. package/dist/chunks/TokenManager-BXNva8Jk.js.map +1 -0
  52. package/dist/chunks/TokenManager-Bzn4guFm.js +2 -0
  53. package/dist/chunks/TokenManager-Bzn4guFm.js.map +1 -0
  54. package/dist/chunks/TopNav-D3I3_25f.js +371 -0
  55. package/dist/chunks/TopNav-D3I3_25f.js.map +1 -0
  56. package/dist/chunks/TopNav-MDjL4kV0.js +2 -0
  57. package/dist/chunks/TopNav-MDjL4kV0.js.map +1 -0
  58. package/dist/chunks/User-BalfYTEF.js +3 -0
  59. package/dist/chunks/User-BalfYTEF.js.map +1 -0
  60. package/dist/chunks/User-DwIT-CTQ.js +1937 -0
  61. package/dist/chunks/User-DwIT-CTQ.js.map +1 -0
  62. package/dist/chunks/WebApp-B6mgbNn2.js +4767 -0
  63. package/dist/chunks/WebApp-B6mgbNn2.js.map +1 -0
  64. package/dist/chunks/WebApp-DqDowtkl.js +2 -0
  65. package/dist/chunks/WebApp-DqDowtkl.js.map +1 -0
  66. package/dist/chunks/WebSocketClient-D6i85jl2.js +2 -0
  67. package/dist/chunks/WebSocketClient-D6i85jl2.js.map +1 -0
  68. package/dist/chunks/WebSocketClient-Dvl3AYx1.js +297 -0
  69. package/dist/chunks/WebSocketClient-Dvl3AYx1.js.map +1 -0
  70. package/dist/core.css +1181 -0
  71. package/dist/css/web-mojo.css +17 -0
  72. package/dist/css-manifest.json +6 -0
  73. package/dist/docit.cjs.js +2 -0
  74. package/dist/docit.cjs.js.map +1 -0
  75. package/dist/docit.es.js +959 -0
  76. package/dist/docit.es.js.map +1 -0
  77. package/dist/index.cjs.js +2 -0
  78. package/dist/index.cjs.js.map +1 -0
  79. package/dist/index.es.js +2681 -0
  80. package/dist/index.es.js.map +1 -0
  81. package/dist/lightbox.cjs.js +2 -0
  82. package/dist/lightbox.cjs.js.map +1 -0
  83. package/dist/lightbox.css +606 -0
  84. package/dist/lightbox.es.js +3737 -0
  85. package/dist/lightbox.es.js.map +1 -0
  86. package/dist/loader.es.js +115 -0
  87. package/dist/loader.umd.js +85 -0
  88. package/dist/portal.css +2446 -0
  89. package/dist/table.css +639 -0
  90. package/dist/toast.css +181 -0
  91. package/package.json +179 -0
@@ -0,0 +1,2095 @@
1
+ import Dialog from "./Dialog-DSlctbon.js";
2
+ import { V as View, d as dataFormatter } from "./WebApp-B6mgbNn2.js";
3
+ import { W as WebSocketClient } from "./WebSocketClient-Dvl3AYx1.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
+ class SeriesChart extends BaseChart {
1022
+ constructor(options = {}) {
1023
+ super({
1024
+ ...options,
1025
+ chartType: options.chartType || "line"
1026
+ });
1027
+ this.showTypeSwitch = true;
1028
+ if (options.showTypeSwitch !== void 0) this.showTypeSwitch = options.showTypeSwitch;
1029
+ this.orientation = options.orientation || "vertical";
1030
+ this.stacked = options.stacked || false;
1031
+ this.stepped = options.stepped || false;
1032
+ this.tension = options.tension || 0.4;
1033
+ this.fill = options.fill || false;
1034
+ this.showRefreshButton = options.showRefreshButton !== false;
1035
+ if (!this.headerConfig) {
1036
+ this.headerConfig = {
1037
+ titleHtml: this.title || "",
1038
+ chartTitle: this.chartTitle || "",
1039
+ showExport: this.exportEnabled,
1040
+ showRefresh: this.refreshEnabled,
1041
+ showTheme: true,
1042
+ controls: []
1043
+ };
1044
+ }
1045
+ this.series = options.series || [];
1046
+ this.xField = options.xField || "x";
1047
+ this.yField = options.yField || "y";
1048
+ this.colors = options.colors || [
1049
+ "rgba(54, 162, 235, 0.8)",
1050
+ // Blue
1051
+ "rgba(255, 99, 132, 0.8)",
1052
+ // Red
1053
+ "rgba(75, 192, 192, 0.8)",
1054
+ // Green
1055
+ "rgba(255, 206, 86, 0.8)",
1056
+ // Yellow
1057
+ "rgba(153, 102, 255, 0.8)",
1058
+ // Purple
1059
+ "rgba(255, 159, 64, 0.8)",
1060
+ // Orange
1061
+ "rgba(199, 199, 199, 0.8)",
1062
+ // Grey
1063
+ "rgba(83, 102, 255, 0.8)"
1064
+ // Indigo
1065
+ ];
1066
+ this.tooltipFormatters = options.tooltip || {};
1067
+ }
1068
+ async getTemplate() {
1069
+ return await super.getTemplate();
1070
+ }
1071
+ async onInit() {
1072
+ if (this.showTypeSwitch) {
1073
+ this.headerConfig.controls.push({
1074
+ type: "buttongroup",
1075
+ size: "sm",
1076
+ buttons: [
1077
+ { action: "set-chart-type", labelHtml: '<i class="bi bi-graph-up"></i>', title: "Line", variant: this.chartType === "line" ? "primary" : "outline-primary", data: { type: "line" } },
1078
+ { action: "set-chart-type", labelHtml: '<i class="bi bi-bar-chart"></i>', title: "Bar", variant: this.chartType === "bar" ? "primary" : "outline-primary", data: { type: "bar" } }
1079
+ ]
1080
+ });
1081
+ }
1082
+ await super.onInit();
1083
+ }
1084
+ // Action Handlers
1085
+ async onActionSetChartType(event, element) {
1086
+ event.stopPropagation();
1087
+ const newType = element.getAttribute("data-type");
1088
+ if (newType && newType !== this.chartType) {
1089
+ await this.setChartType(newType);
1090
+ }
1091
+ }
1092
+ async rebuildChart() {
1093
+ if (this.chart && this.data) {
1094
+ this.chart.destroy();
1095
+ this.chart = null;
1096
+ const processedData = this.processChartData(this.data);
1097
+ await this.createChart(processedData);
1098
+ }
1099
+ }
1100
+ async setChartType(newType) {
1101
+ if (!["line", "bar"].includes(newType)) {
1102
+ throw new Error(`Unsupported chart type: ${newType}`);
1103
+ }
1104
+ const oldType = this.chartType;
1105
+ this.chartType = newType;
1106
+ if (this.chart && this.data) {
1107
+ this.chart.destroy();
1108
+ this.chart = null;
1109
+ const processedData = this.processChartData(this.data);
1110
+ await this.createChart(processedData);
1111
+ }
1112
+ this._updateTypeSwitcherButtons();
1113
+ const eventBus = this.getApp()?.events;
1114
+ if (eventBus) {
1115
+ eventBus.emit("chart:type-changed", {
1116
+ chart: this,
1117
+ oldType,
1118
+ newType: this.chartType
1119
+ });
1120
+ }
1121
+ }
1122
+ processChartData(data) {
1123
+ if (!data) return data;
1124
+ let processedData;
1125
+ if (Array.isArray(data)) {
1126
+ processedData = this.processArrayData(data);
1127
+ } else if (data.labels && data.datasets) {
1128
+ processedData = this.processChartJSData(data);
1129
+ } else if (data.series) {
1130
+ processedData = this.processSeriesData(data);
1131
+ } else {
1132
+ processedData = data;
1133
+ }
1134
+ return this.applyFormattersToData(processedData);
1135
+ }
1136
+ processArrayData(data) {
1137
+ const labels = [];
1138
+ const values = [];
1139
+ data.forEach((item) => {
1140
+ const xValue = item[this.xField];
1141
+ const yValue = item[this.yField];
1142
+ labels.push(xValue);
1143
+ values.push(yValue);
1144
+ });
1145
+ return {
1146
+ labels,
1147
+ datasets: [{
1148
+ label: this.title || "Data",
1149
+ data: values,
1150
+ backgroundColor: this.colors[0].replace("0.8", "0.6"),
1151
+ borderColor: this.colors[0],
1152
+ borderWidth: 2,
1153
+ tension: this.chartType === "line" ? this.tension : 0,
1154
+ fill: this.chartType === "line" ? this.fill : false,
1155
+ stepped: this.chartType === "line" ? this.stepped : false
1156
+ }]
1157
+ };
1158
+ }
1159
+ processChartJSData(data) {
1160
+ const processedData = { ...data };
1161
+ processedData.datasets = processedData.datasets.map((dataset, index) => ({
1162
+ ...dataset,
1163
+ backgroundColor: dataset.backgroundColor || this.colors[index % this.colors.length].replace("0.8", "0.6"),
1164
+ borderColor: dataset.borderColor || this.colors[index % this.colors.length],
1165
+ borderWidth: dataset.borderWidth || 2,
1166
+ tension: this.chartType === "line" ? dataset.tension ?? this.tension : 0,
1167
+ fill: this.chartType === "line" ? dataset.fill ?? this.fill : false,
1168
+ stepped: this.chartType === "line" ? dataset.stepped ?? this.stepped : false
1169
+ }));
1170
+ return processedData;
1171
+ }
1172
+ processSeriesData(data) {
1173
+ const labels = data.labels || [];
1174
+ const datasets = [];
1175
+ data.series.forEach((series, index) => {
1176
+ datasets.push({
1177
+ label: series.name || series.label || `Series ${index + 1}`,
1178
+ data: series.data || [],
1179
+ backgroundColor: series.backgroundColor || this.colors[index % this.colors.length].replace("0.8", "0.6"),
1180
+ borderColor: series.borderColor || this.colors[index % this.colors.length],
1181
+ borderWidth: series.borderWidth || 2,
1182
+ tension: this.chartType === "line" ? series.tension ?? this.tension : 0,
1183
+ fill: this.chartType === "line" ? series.fill ?? this.fill : false,
1184
+ stepped: this.chartType === "line" ? series.stepped ?? this.stepped : false
1185
+ });
1186
+ });
1187
+ return { labels, datasets };
1188
+ }
1189
+ applyFormattersToData(data) {
1190
+ if (!data) return data;
1191
+ const processedData = { ...data };
1192
+ const xAxisCfg = this.normalizeAxis ? this.normalizeAxis(this.xAxis) : {};
1193
+ if (xAxisCfg.formatter && processedData.labels) {
1194
+ processedData.labels = processedData.labels.map(
1195
+ (label) => this.dataFormatter.pipe(label, xAxisCfg.formatter)
1196
+ );
1197
+ }
1198
+ return processedData;
1199
+ }
1200
+ applySubclassChartOptions(options) {
1201
+ if (this.stacked && this.chartType === "bar" && options.scales) {
1202
+ if (options.scales.x) options.scales.x.stacked = true;
1203
+ if (options.scales.y) options.scales.y.stacked = true;
1204
+ }
1205
+ if (this.chartType === "bar" && this.orientation === "horizontal") {
1206
+ options.indexAxis = "y";
1207
+ }
1208
+ options.interaction = options.interaction || {};
1209
+ options.interaction.intersect = false;
1210
+ options.interaction.mode = this.chartType === "line" ? "index" : "nearest";
1211
+ options.elements = options.elements || {};
1212
+ options.elements.line = {
1213
+ ...options.elements.line || {},
1214
+ tension: this.tension,
1215
+ borderWidth: 2
1216
+ };
1217
+ options.elements.point = {
1218
+ ...options.elements.point || {},
1219
+ radius: this.chartType === "line" ? 4 : 0,
1220
+ hoverRadius: 6,
1221
+ hitRadius: 8
1222
+ };
1223
+ options.elements.bar = {
1224
+ ...options.elements.bar || {},
1225
+ borderWidth: 1,
1226
+ borderSkipped: false
1227
+ };
1228
+ }
1229
+ // Process simple axis configuration into detailed config
1230
+ processAxisConfig(axisConfig) {
1231
+ if (!axisConfig) return {};
1232
+ if (typeof axisConfig === "string") {
1233
+ return { formatter: axisConfig };
1234
+ }
1235
+ if (typeof axisConfig === "object") {
1236
+ return {
1237
+ formatter: axisConfig.formatter,
1238
+ label: axisConfig.label,
1239
+ type: axisConfig.type,
1240
+ beginAtZero: axisConfig.beginAtZero,
1241
+ ...axisConfig
1242
+ };
1243
+ }
1244
+ return {};
1245
+ }
1246
+ _updateTypeSwitcherButtons() {
1247
+ const buttons = this.element?.querySelectorAll('[data-action="set-chart-type"]');
1248
+ if (!buttons || buttons.length === 0) return;
1249
+ buttons.forEach((button) => {
1250
+ const buttonType = button.getAttribute("data-type");
1251
+ const isActive = buttonType === this.chartType;
1252
+ button.classList.toggle("btn-primary", isActive);
1253
+ button.classList.toggle("btn-outline-primary", !isActive);
1254
+ button.classList.toggle("active", isActive);
1255
+ });
1256
+ }
1257
+ // Public API extensions
1258
+ setOrientation(orientation) {
1259
+ if (!["vertical", "horizontal"].includes(orientation)) {
1260
+ throw new Error(`Invalid orientation: ${orientation}`);
1261
+ }
1262
+ this.orientation = orientation;
1263
+ if (this.chart) {
1264
+ this.chart.destroy();
1265
+ this.chart = null;
1266
+ if (this.data) {
1267
+ const processedData = this.processChartData(this.data);
1268
+ this.createChart(processedData);
1269
+ }
1270
+ }
1271
+ }
1272
+ setStacked(stacked) {
1273
+ this.stacked = stacked;
1274
+ if (this.chart) {
1275
+ this.chart.options.scales.x.stacked = stacked;
1276
+ this.chart.options.scales.y.stacked = stacked;
1277
+ this.chart.update();
1278
+ }
1279
+ }
1280
+ addSeries(series) {
1281
+ if (!this.data || !this.data.datasets) return;
1282
+ const newDataset = {
1283
+ label: series.label || series.name || `Series ${this.data.datasets.length + 1}`,
1284
+ data: series.data || [],
1285
+ backgroundColor: series.backgroundColor || this.colors[this.data.datasets.length % this.colors.length].replace("0.8", "0.6"),
1286
+ borderColor: series.borderColor || this.colors[this.data.datasets.length % this.colors.length],
1287
+ borderWidth: series.borderWidth || 2,
1288
+ tension: this.chartType === "line" ? series.tension ?? this.tension : 0,
1289
+ fill: this.chartType === "line" ? series.fill ?? this.fill : false
1290
+ };
1291
+ this.data.datasets.push(newDataset);
1292
+ if (this.chart) {
1293
+ this.chart.data.datasets.push(newDataset);
1294
+ this.chart.update();
1295
+ }
1296
+ const eventBus = this.getApp()?.events;
1297
+ if (eventBus) {
1298
+ eventBus.emit("chart:series-added", {
1299
+ chart: this,
1300
+ series: newDataset
1301
+ });
1302
+ }
1303
+ }
1304
+ removeSeries(index) {
1305
+ if (!this.data || !this.data.datasets || index < 0 || index >= this.data.datasets.length) {
1306
+ return;
1307
+ }
1308
+ const removedSeries = this.data.datasets.splice(index, 1)[0];
1309
+ if (this.chart) {
1310
+ this.chart.data.datasets.splice(index, 1);
1311
+ this.chart.update();
1312
+ }
1313
+ const eventBus = this.getApp()?.events;
1314
+ if (eventBus) {
1315
+ eventBus.emit("chart:series-removed", {
1316
+ chart: this,
1317
+ series: removedSeries,
1318
+ index
1319
+ });
1320
+ }
1321
+ }
1322
+ // Static dialog method
1323
+ static async showDialog(options = {}) {
1324
+ const {
1325
+ title = "Chart Viewer",
1326
+ size = "xl",
1327
+ ...chartOptions
1328
+ } = options;
1329
+ const chart = new SeriesChart({
1330
+ ...chartOptions,
1331
+ title
1332
+ });
1333
+ const dialog = new Dialog({
1334
+ title,
1335
+ body: chart,
1336
+ size,
1337
+ centered: true,
1338
+ backdrop: "static",
1339
+ keyboard: true,
1340
+ buttons: [
1341
+ {
1342
+ text: "Export PNG",
1343
+ action: "export",
1344
+ class: "btn btn-outline-primary"
1345
+ },
1346
+ {
1347
+ text: "Close",
1348
+ action: "close",
1349
+ class: "btn btn-secondary",
1350
+ dismiss: true
1351
+ }
1352
+ ]
1353
+ });
1354
+ await dialog.render();
1355
+ document.body.appendChild(dialog.element);
1356
+ await dialog.mount();
1357
+ dialog.show();
1358
+ return new Promise((resolve) => {
1359
+ dialog.on("hidden", () => {
1360
+ dialog.destroy();
1361
+ resolve(chart);
1362
+ });
1363
+ dialog.on("action:export", () => {
1364
+ chart.exportChart("png");
1365
+ });
1366
+ dialog.on("action:close", () => {
1367
+ dialog.hide();
1368
+ });
1369
+ });
1370
+ }
1371
+ }
1372
+ class PieChart extends BaseChart {
1373
+ constructor(options = {}) {
1374
+ super({
1375
+ ...options,
1376
+ chartType: "pie"
1377
+ });
1378
+ this.cutout = options.cutout || 0;
1379
+ this.rotation = options.rotation || 0;
1380
+ this.circumference = options.circumference || 360;
1381
+ this.borderWidth = options.borderWidth || 2;
1382
+ this.borderColor = options.borderColor || "#ffffff";
1383
+ this.hoverBorderWidth = options.hoverBorderWidth || 3;
1384
+ this.showLabels = options.showLabels !== false;
1385
+ this.labelPosition = options.labelPosition || "outside";
1386
+ this.labelFormatter = options.labelFormatter || null;
1387
+ this.valueFormatter = options.valueFormatter || null;
1388
+ this.labelField = options.labelField || "label";
1389
+ this.valueField = options.valueField || "value";
1390
+ this.colors = options.colors || [
1391
+ "#FF6384",
1392
+ // Red
1393
+ "#36A2EB",
1394
+ // Blue
1395
+ "#FFCE56",
1396
+ // Yellow
1397
+ "#4BC0C0",
1398
+ // Teal
1399
+ "#9966FF",
1400
+ // Purple
1401
+ "#FF9F40",
1402
+ // Orange
1403
+ "#C9CBCF",
1404
+ // Grey
1405
+ "#4BC0C0",
1406
+ // Green
1407
+ "#FF6384",
1408
+ // Pink
1409
+ "#36A2EB"
1410
+ // Light Blue
1411
+ ];
1412
+ this.animateRotate = options.animateRotate !== false;
1413
+ this.animateScale = options.animateScale || false;
1414
+ this.clickable = options.clickable !== false;
1415
+ this.hoverable = options.hoverable !== false;
1416
+ this.selectedSegment = null;
1417
+ this.highlightedSegments = /* @__PURE__ */ new Set();
1418
+ this.valueFormatter = options.valueFormatter || null;
1419
+ }
1420
+ processChartData(data) {
1421
+ if (!data) return data;
1422
+ let processedData;
1423
+ if (Array.isArray(data)) {
1424
+ processedData = this.processArrayData(data);
1425
+ } else if (data.labels && data.datasets) {
1426
+ processedData = this.processChartJSData(data);
1427
+ } else if (typeof data === "object" && !data.labels) {
1428
+ processedData = this.processObjectData(data);
1429
+ } else {
1430
+ processedData = data;
1431
+ }
1432
+ return this.applyFormattersToData(processedData);
1433
+ }
1434
+ processArrayData(data) {
1435
+ const labels = [];
1436
+ const values = [];
1437
+ data.forEach((item) => {
1438
+ const label = item[this.labelField];
1439
+ const value = item[this.valueField];
1440
+ if (label !== void 0 && value !== void 0) {
1441
+ labels.push(label);
1442
+ values.push(value);
1443
+ }
1444
+ });
1445
+ return {
1446
+ labels,
1447
+ datasets: [{
1448
+ data: values,
1449
+ backgroundColor: this.generateColors(labels.length),
1450
+ borderColor: this.borderColor,
1451
+ borderWidth: this.borderWidth,
1452
+ hoverBorderWidth: this.hoverBorderWidth
1453
+ }]
1454
+ };
1455
+ }
1456
+ processChartJSData(data) {
1457
+ const processedData = { ...data };
1458
+ processedData.datasets = processedData.datasets.map((dataset) => ({
1459
+ ...dataset,
1460
+ backgroundColor: dataset.backgroundColor || this.generateColors(processedData.labels.length),
1461
+ borderColor: dataset.borderColor || this.borderColor,
1462
+ borderWidth: dataset.borderWidth || this.borderWidth,
1463
+ hoverBorderWidth: dataset.hoverBorderWidth || this.hoverBorderWidth
1464
+ }));
1465
+ return processedData;
1466
+ }
1467
+ processObjectData(data) {
1468
+ const labels = Object.keys(data);
1469
+ const values = Object.values(data);
1470
+ return {
1471
+ labels,
1472
+ datasets: [{
1473
+ data: values,
1474
+ backgroundColor: this.generateColors(labels.length),
1475
+ borderColor: this.borderColor,
1476
+ borderWidth: this.borderWidth,
1477
+ hoverBorderWidth: this.hoverBorderWidth
1478
+ }]
1479
+ };
1480
+ }
1481
+ applyFormattersToData(data) {
1482
+ if (!data) return data;
1483
+ const processedData = { ...data };
1484
+ if (this.labelFormatter && processedData.labels) {
1485
+ processedData.labels = processedData.labels.map(
1486
+ (label) => this.dataFormatter.pipe(label, this.labelFormatter)
1487
+ );
1488
+ }
1489
+ return processedData;
1490
+ }
1491
+ generateColors(count) {
1492
+ const colors = [];
1493
+ for (let i = 0; i < count; i++) {
1494
+ colors.push(this.colors[i % this.colors.length]);
1495
+ }
1496
+ return colors;
1497
+ }
1498
+ buildChartOptions() {
1499
+ const options = super.buildChartOptions();
1500
+ options.cutout = this.cutout;
1501
+ options.rotation = this.rotation;
1502
+ options.circumference = this.circumference;
1503
+ options.animation = {
1504
+ animateRotate: this.animateRotate,
1505
+ animateScale: this.animateScale,
1506
+ duration: this.animations ? 1e3 : 0
1507
+ };
1508
+ options.plugins = {
1509
+ ...options.plugins,
1510
+ legend: {
1511
+ ...options.plugins.legend,
1512
+ position: options.plugins.legend.position || "right",
1513
+ labels: {
1514
+ ...options.plugins.legend.labels,
1515
+ usePointStyle: true,
1516
+ padding: 20,
1517
+ generateLabels: (chart) => {
1518
+ const data = chart.data;
1519
+ if (data.labels.length && data.datasets.length) {
1520
+ return data.labels.map((label, i) => {
1521
+ const dataset = data.datasets[0];
1522
+ const value = dataset.data[i];
1523
+ const backgroundColor = dataset.backgroundColor[i];
1524
+ const total = dataset.data.reduce((sum, val) => sum + val, 0);
1525
+ const percentage = (value / total * 100).toFixed(1);
1526
+ return {
1527
+ text: `${label} (${percentage}%)`,
1528
+ fillStyle: backgroundColor,
1529
+ strokeStyle: backgroundColor,
1530
+ lineWidth: 0,
1531
+ hidden: false,
1532
+ index: i
1533
+ };
1534
+ });
1535
+ }
1536
+ return [];
1537
+ }
1538
+ }
1539
+ },
1540
+ tooltip: {
1541
+ ...options.plugins.tooltip,
1542
+ callbacks: {
1543
+ ...options.plugins.tooltip.callbacks,
1544
+ label: (context) => {
1545
+ const label = context.label || "";
1546
+ const value = context.raw;
1547
+ const dataset = context.dataset;
1548
+ const total = dataset.data.reduce((sum, val) => sum + val, 0);
1549
+ const percentage = (value / total * 100).toFixed(1);
1550
+ let formattedValue = value;
1551
+ if (this.valueFormatter) {
1552
+ formattedValue = this.dataFormatter.pipe(value, this.valueFormatter);
1553
+ } else if (this.tooltipFormatters && this.tooltipFormatters.y) {
1554
+ formattedValue = this.dataFormatter.pipe(value, this.tooltipFormatters.y);
1555
+ }
1556
+ return `${label}: ${formattedValue} (${percentage}%)`;
1557
+ }
1558
+ }
1559
+ }
1560
+ };
1561
+ delete options.scales;
1562
+ return options;
1563
+ }
1564
+ setupChartEventHandlers() {
1565
+ super.setupChartEventHandlers();
1566
+ if (!this.chart || !this.clickable) return;
1567
+ this.chart.options.onClick = (event, elements) => {
1568
+ if (elements.length > 0) {
1569
+ const element = elements[0];
1570
+ const index = element.index;
1571
+ const dataset = this.chart.data.datasets[0];
1572
+ const label = this.chart.data.labels[index];
1573
+ const value = dataset.data[index];
1574
+ const total = dataset.data.reduce((sum, val) => sum + val, 0);
1575
+ const percentage = (value / total * 100).toFixed(1);
1576
+ this.toggleSegmentSelection(index);
1577
+ const eventBus = this.getApp()?.events;
1578
+ if (eventBus) {
1579
+ eventBus.emit("chart:segment-clicked", {
1580
+ chart: this,
1581
+ index,
1582
+ label,
1583
+ value,
1584
+ percentage: parseFloat(percentage),
1585
+ isSelected: this.selectedSegment === index
1586
+ });
1587
+ }
1588
+ }
1589
+ };
1590
+ if (this.hoverable) {
1591
+ this.chart.options.onHover = (event, elements) => {
1592
+ this.canvas.style.cursor = elements.length > 0 ? "pointer" : "default";
1593
+ if (elements.length > 0) {
1594
+ const element = elements[0];
1595
+ const index = element.index;
1596
+ const eventBus = this.getApp()?.events;
1597
+ if (eventBus) {
1598
+ eventBus.emit("chart:segment-hover", {
1599
+ chart: this,
1600
+ index,
1601
+ label: this.chart.data.labels[index],
1602
+ value: this.chart.data.datasets[0].data[index]
1603
+ });
1604
+ }
1605
+ }
1606
+ };
1607
+ }
1608
+ }
1609
+ toggleSegmentSelection(index) {
1610
+ if (this.selectedSegment === index) {
1611
+ this.selectedSegment = null;
1612
+ this.resetSegmentStyle(index);
1613
+ } else {
1614
+ if (this.selectedSegment !== null) {
1615
+ this.resetSegmentStyle(this.selectedSegment);
1616
+ }
1617
+ this.selectedSegment = index;
1618
+ this.highlightSegment(index);
1619
+ }
1620
+ }
1621
+ highlightSegment(index) {
1622
+ if (!this.chart) return;
1623
+ const meta = this.chart.getDatasetMeta(0);
1624
+ const segment = meta.data[index];
1625
+ if (segment) {
1626
+ segment.outerRadius += 10;
1627
+ this.chart.update("none");
1628
+ }
1629
+ }
1630
+ resetSegmentStyle(index) {
1631
+ if (!this.chart) return;
1632
+ const meta = this.chart.getDatasetMeta(0);
1633
+ const segment = meta.data[index];
1634
+ if (segment) {
1635
+ segment.outerRadius -= 10;
1636
+ this.chart.update("none");
1637
+ }
1638
+ }
1639
+ highlightSegments(indices) {
1640
+ if (!Array.isArray(indices)) {
1641
+ indices = [indices];
1642
+ }
1643
+ this.highlightedSegments.clear();
1644
+ indices.forEach((index) => {
1645
+ this.highlightedSegments.add(index);
1646
+ this.highlightSegment(index);
1647
+ });
1648
+ }
1649
+ clearHighlights() {
1650
+ this.highlightedSegments.forEach((index) => {
1651
+ this.resetSegmentStyle(index);
1652
+ });
1653
+ this.highlightedSegments.clear();
1654
+ if (this.selectedSegment !== null) {
1655
+ this.resetSegmentStyle(this.selectedSegment);
1656
+ this.selectedSegment = null;
1657
+ }
1658
+ }
1659
+ // Public API extensions
1660
+ selectSegment(index) {
1661
+ if (index >= 0 && index < this.chart?.data?.labels?.length) {
1662
+ this.toggleSegmentSelection(index);
1663
+ }
1664
+ }
1665
+ getSegmentData(index) {
1666
+ if (!this.chart || !this.chart.data) return null;
1667
+ const dataset = this.chart.data.datasets[0];
1668
+ const label = this.chart.data.labels[index];
1669
+ const value = dataset.data[index];
1670
+ const total = dataset.data.reduce((sum, val) => sum + val, 0);
1671
+ const percentage = (value / total * 100).toFixed(1);
1672
+ return {
1673
+ index,
1674
+ label,
1675
+ value,
1676
+ percentage: parseFloat(percentage),
1677
+ color: dataset.backgroundColor[index],
1678
+ isSelected: this.selectedSegment === index
1679
+ };
1680
+ }
1681
+ getAllSegments() {
1682
+ if (!this.chart || !this.chart.data) return [];
1683
+ return this.chart.data.labels.map((_, index) => this.getSegmentData(index));
1684
+ }
1685
+ updateSegmentColor(index, color) {
1686
+ if (!this.chart || !this.chart.data.datasets[0]) return;
1687
+ this.chart.data.datasets[0].backgroundColor[index] = color;
1688
+ this.chart.update("none");
1689
+ const eventBus = this.getApp()?.events;
1690
+ if (eventBus) {
1691
+ eventBus.emit("chart:segment-color-changed", {
1692
+ chart: this,
1693
+ index,
1694
+ color,
1695
+ segment: this.getSegmentData(index)
1696
+ });
1697
+ }
1698
+ }
1699
+ addSegment(label, value, color = null) {
1700
+ if (!this.chart || !this.chart.data) return;
1701
+ const dataset = this.chart.data.datasets[0];
1702
+ const segmentColor = color || this.colors[this.chart.data.labels.length % this.colors.length];
1703
+ this.chart.data.labels.push(label);
1704
+ dataset.data.push(value);
1705
+ dataset.backgroundColor.push(segmentColor);
1706
+ this.chart.update();
1707
+ const eventBus = this.getApp()?.events;
1708
+ if (eventBus) {
1709
+ eventBus.emit("chart:segment-added", {
1710
+ chart: this,
1711
+ label,
1712
+ value,
1713
+ color: segmentColor,
1714
+ index: this.chart.data.labels.length - 1
1715
+ });
1716
+ }
1717
+ }
1718
+ removeSegment(index) {
1719
+ if (!this.chart || !this.chart.data || index < 0 || index >= this.chart.data.labels.length) {
1720
+ return;
1721
+ }
1722
+ const dataset = this.chart.data.datasets[0];
1723
+ const label = this.chart.data.labels[index];
1724
+ const value = dataset.data[index];
1725
+ this.chart.data.labels.splice(index, 1);
1726
+ dataset.data.splice(index, 1);
1727
+ dataset.backgroundColor.splice(index, 1);
1728
+ if (this.selectedSegment === index) {
1729
+ this.selectedSegment = null;
1730
+ } else if (this.selectedSegment > index) {
1731
+ this.selectedSegment--;
1732
+ }
1733
+ this.chart.update();
1734
+ const eventBus = this.getApp()?.events;
1735
+ if (eventBus) {
1736
+ eventBus.emit("chart:segment-removed", {
1737
+ chart: this,
1738
+ label,
1739
+ value,
1740
+ index,
1741
+ removedSegment: { label, value, index }
1742
+ });
1743
+ }
1744
+ }
1745
+ // Override applyThemeToOptions for pie chart specific theming
1746
+ applyThemeToOptions(options) {
1747
+ super.applyThemeToOptions(options);
1748
+ const isDark = this.theme === "dark";
1749
+ if (isDark) {
1750
+ this.borderColor = "#404449";
1751
+ } else {
1752
+ this.borderColor = "#ffffff";
1753
+ }
1754
+ }
1755
+ // Static dialog method
1756
+ static async showDialog(options = {}) {
1757
+ const {
1758
+ title = "Pie Chart",
1759
+ size = "lg",
1760
+ ...chartOptions
1761
+ } = options;
1762
+ const chart = new PieChart({
1763
+ ...chartOptions,
1764
+ title
1765
+ });
1766
+ const dialog = new Dialog({
1767
+ title,
1768
+ body: chart,
1769
+ size,
1770
+ centered: true,
1771
+ backdrop: "static",
1772
+ keyboard: true,
1773
+ buttons: [
1774
+ {
1775
+ text: "Export PNG",
1776
+ action: "export",
1777
+ class: "btn btn-outline-primary"
1778
+ },
1779
+ {
1780
+ text: "Close",
1781
+ action: "close",
1782
+ class: "btn btn-secondary",
1783
+ dismiss: true
1784
+ }
1785
+ ]
1786
+ });
1787
+ await dialog.render();
1788
+ document.body.appendChild(dialog.element);
1789
+ await dialog.mount();
1790
+ dialog.show();
1791
+ return new Promise((resolve) => {
1792
+ dialog.on("hidden", () => {
1793
+ dialog.destroy();
1794
+ resolve(chart);
1795
+ });
1796
+ dialog.on("action:export", () => {
1797
+ chart.exportChart("png");
1798
+ });
1799
+ dialog.on("action:close", () => {
1800
+ dialog.hide();
1801
+ });
1802
+ });
1803
+ }
1804
+ }
1805
+ class MetricsChart extends SeriesChart {
1806
+ constructor(options = {}) {
1807
+ super({
1808
+ ...options,
1809
+ chartType: options.chartType || "line",
1810
+ title: options.title || "Metrics",
1811
+ colors: options.colors || [
1812
+ "rgba(54, 162, 235, 0.8)",
1813
+ // Blue
1814
+ "rgba(255, 99, 132, 0.8)",
1815
+ // Red
1816
+ "rgba(75, 192, 192, 0.8)",
1817
+ // Green
1818
+ "rgba(255, 206, 86, 0.8)",
1819
+ // Yellow
1820
+ "rgba(153, 102, 255, 0.8)",
1821
+ // Purple
1822
+ "rgba(255, 159, 64, 0.8)",
1823
+ // Orange
1824
+ "rgba(199, 199, 199, 0.8)",
1825
+ // Grey
1826
+ "rgba(83, 102, 255, 0.8)"
1827
+ // Indigo
1828
+ ],
1829
+ yAxis: options.yAxis || { label: "Count", beginAtZero: true },
1830
+ tooltip: options.tooltip || { y: "number" },
1831
+ width: options.width,
1832
+ height: options.height
1833
+ });
1834
+ this.endpoint = options.endpoint || "/api/metrics/fetch";
1835
+ this.account = options.account || "global";
1836
+ this.granularity = options.granularity || "hours";
1837
+ this.slugs = options.slugs || ["api_calls", "api_errors"];
1838
+ this.dateStart = options.dateStart || null;
1839
+ this.dateEnd = options.dateEnd || null;
1840
+ this.defaultDateRange = options.defaultDateRange || "24h";
1841
+ this.showGranularity = options.showGranularity !== false;
1842
+ this.showDateRange = options.showDateRange !== false;
1843
+ this.granularityOptions = options.granularityOptions || [
1844
+ { value: "minutes", label: "Minutes" },
1845
+ { value: "hours", label: "Hours" },
1846
+ { value: "days", label: "Days" },
1847
+ { value: "weeks", label: "Weeks" },
1848
+ { value: "months", label: "Months" }
1849
+ ];
1850
+ this.quickRanges = options.quickRanges || [
1851
+ { value: "1h", label: "1H" },
1852
+ { value: "24h", label: "24H" },
1853
+ { value: "7d", label: "7D" },
1854
+ { value: "30d", label: "30D" }
1855
+ ];
1856
+ this.availableMetrics = options.availableMetrics || [
1857
+ { value: "api_calls", label: "API Calls" },
1858
+ { value: "api_errors", label: "API Errors" },
1859
+ { value: "incident_evt", label: "System Events" },
1860
+ { value: "incidents", label: "Incidents" }
1861
+ ];
1862
+ this.isLoading = false;
1863
+ this.lastFetch = null;
1864
+ if (!this.dateStart || !this.dateEnd) {
1865
+ this.setQuickRange(this.defaultDateRange);
1866
+ }
1867
+ }
1868
+ async onInit() {
1869
+ const controls = [];
1870
+ if (this.showGranularity) {
1871
+ controls.push({
1872
+ type: "select",
1873
+ name: "granularity",
1874
+ action: "granularity-changed",
1875
+ size: "sm",
1876
+ options: this.granularityOptions.map((opt) => ({
1877
+ value: opt.value,
1878
+ label: opt.label,
1879
+ selected: opt.value === this.granularity
1880
+ }))
1881
+ });
1882
+ }
1883
+ if (this.showDateRange) {
1884
+ controls.push({
1885
+ type: "button",
1886
+ action: "show-date-range-dialog",
1887
+ labelHtml: `<i class="bi bi-calendar-range me-1"></i>${this.formatDateRangeDisplay()}`,
1888
+ title: "Select Date Range",
1889
+ variant: "outline-secondary",
1890
+ size: "sm"
1891
+ });
1892
+ }
1893
+ this.headerConfig = {
1894
+ titleHtml: this.title || "Metrics",
1895
+ chartTitle: this.chartTitle || "",
1896
+ showExport: this.exportEnabled === true,
1897
+ showRefresh: this.refreshEnabled,
1898
+ showTheme: false,
1899
+ controls
1900
+ };
1901
+ await super.onInit();
1902
+ }
1903
+ // Action Handlers
1904
+ async onActionGranularityChanged(event, element) {
1905
+ const newGranularity = element.value;
1906
+ if (newGranularity && newGranularity !== this.granularity) {
1907
+ this.granularity = newGranularity;
1908
+ await this.fetchData();
1909
+ }
1910
+ }
1911
+ async onActionShowDateRangeDialog() {
1912
+ try {
1913
+ const result = await Dialog.showForm({
1914
+ title: "Select Date Range",
1915
+ size: "md",
1916
+ fields: [
1917
+ {
1918
+ name: "dateRange",
1919
+ type: "daterange",
1920
+ label: "Date Range",
1921
+ startName: "dt_start",
1922
+ endName: "dt_end",
1923
+ startDate: this.formatDateTimeLocal(this.dateStart),
1924
+ endDate: this.formatDateTimeLocal(this.dateEnd),
1925
+ required: true
1926
+ }
1927
+ ],
1928
+ formConfig: {
1929
+ options: {
1930
+ submitButton: false,
1931
+ resetButton: false
1932
+ }
1933
+ }
1934
+ });
1935
+ if (result && result.startDate && result.endDate) {
1936
+ this.dateStart = new Date(result.startDate);
1937
+ this.dateEnd = new Date(result.endDate);
1938
+ const btn = this.element?.querySelector('[data-action="show-date-range-dialog"]');
1939
+ if (btn) {
1940
+ btn.innerHTML = `<i class="bi bi-calendar-range me-1"></i>${this.formatDateRangeDisplay()}`;
1941
+ }
1942
+ await this.fetchData();
1943
+ }
1944
+ } catch (error) {
1945
+ console.error("Date range dialog error:", error);
1946
+ }
1947
+ }
1948
+ // Data Management
1949
+ buildApiParams() {
1950
+ const params = {
1951
+ granularity: this.granularity,
1952
+ account: this.account,
1953
+ with_labels: true
1954
+ };
1955
+ this.slugs.forEach((slug) => {
1956
+ if (!params["slugs[]"]) params["slugs[]"] = [];
1957
+ params["slugs[]"].push(slug);
1958
+ });
1959
+ if (this.dateStart) {
1960
+ params.dr_start = Math.floor(this.dateStart.getTime() / 1e3);
1961
+ }
1962
+ if (this.dateEnd) {
1963
+ params.dr_end = Math.floor(this.dateEnd.getTime() / 1e3);
1964
+ }
1965
+ params._ = Date.now();
1966
+ return params;
1967
+ }
1968
+ async fetchData() {
1969
+ if (!this.endpoint) return;
1970
+ this.isLoading = true;
1971
+ this.showLoading();
1972
+ try {
1973
+ const rest = this.getApp()?.rest;
1974
+ if (!rest) {
1975
+ throw new Error("No REST client available");
1976
+ }
1977
+ const params = this.buildApiParams();
1978
+ const response = await rest.GET(this.endpoint, params);
1979
+ if (!response.success) {
1980
+ throw new Error(response.message || "Network error");
1981
+ }
1982
+ if (!response.data?.status) {
1983
+ throw new Error(response.data?.error || "Server error");
1984
+ }
1985
+ const metricsData = response.data.data;
1986
+ const chartData = this.processMetricsData(metricsData);
1987
+ await this.setData(chartData);
1988
+ this.lastFetch = /* @__PURE__ */ new Date();
1989
+ this.emit("metrics:data-loaded", {
1990
+ chart: this,
1991
+ data: metricsData,
1992
+ params
1993
+ });
1994
+ } catch (error) {
1995
+ console.error("Failed to fetch metrics data:", error);
1996
+ this.showError(`Failed to load metrics: ${error.message}`);
1997
+ this.emit("metrics:error", { chart: this, error });
1998
+ } finally {
1999
+ this.isLoading = false;
2000
+ this.hideLoading();
2001
+ }
2002
+ }
2003
+ processMetricsData(data) {
2004
+ const { data: metricsData, labels } = data;
2005
+ const datasets = [];
2006
+ Object.keys(metricsData).forEach((metric, index) => {
2007
+ const values = metricsData[metric];
2008
+ const sanitizedValues = values.map((val) => {
2009
+ if (val === null || val === void 0 || val === "") return 0;
2010
+ return typeof val === "number" ? val : parseFloat(val) || 0;
2011
+ });
2012
+ datasets.push({
2013
+ label: this.formatMetricLabel(metric),
2014
+ data: sanitizedValues,
2015
+ backgroundColor: this.colors[index % this.colors.length].replace("0.8", "0.6"),
2016
+ borderColor: this.colors[index % this.colors.length],
2017
+ borderWidth: 2,
2018
+ tension: this.chartType === "line" ? 0.4 : 0,
2019
+ fill: false,
2020
+ pointRadius: this.chartType === "line" ? 3 : 0,
2021
+ pointHoverRadius: 5
2022
+ });
2023
+ });
2024
+ return { labels, datasets };
2025
+ }
2026
+ formatMetricLabel(metric) {
2027
+ return metric.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2028
+ }
2029
+ // Date utilities
2030
+ setQuickRange(range) {
2031
+ const now = /* @__PURE__ */ new Date();
2032
+ let startDate;
2033
+ switch (range) {
2034
+ case "1h":
2035
+ startDate = new Date(now.getTime() - 60 * 60 * 1e3);
2036
+ break;
2037
+ case "24h":
2038
+ startDate = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
2039
+ break;
2040
+ case "7d":
2041
+ startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
2042
+ break;
2043
+ case "30d":
2044
+ startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3);
2045
+ break;
2046
+ default:
2047
+ startDate = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
2048
+ }
2049
+ this.dateStart = startDate;
2050
+ this.dateEnd = now;
2051
+ }
2052
+ formatDateTimeLocal(date) {
2053
+ if (!date) return "";
2054
+ const year = date.getFullYear();
2055
+ const month = String(date.getMonth() + 1).padStart(2, "0");
2056
+ const day = String(date.getDate()).padStart(2, "0");
2057
+ const hours = String(date.getHours()).padStart(2, "0");
2058
+ const minutes = String(date.getMinutes()).padStart(2, "0");
2059
+ return `${year}-${month}-${day}T${hours}:${minutes}`;
2060
+ }
2061
+ // Public API
2062
+ setGranularity(granularity) {
2063
+ this.granularity = granularity;
2064
+ return this.fetchData();
2065
+ }
2066
+ setDateRange(startDate, endDate) {
2067
+ this.dateStart = new Date(startDate);
2068
+ this.dateEnd = new Date(endDate);
2069
+ return this.fetchData();
2070
+ }
2071
+ setMetrics(slugs) {
2072
+ this.slugs = [...slugs];
2073
+ return this.fetchData();
2074
+ }
2075
+ getStats() {
2076
+ const base = super.getStats();
2077
+ return {
2078
+ ...base,
2079
+ lastFetch: this.lastFetch,
2080
+ granularity: this.granularity,
2081
+ slugs: [...this.slugs],
2082
+ dateRange: {
2083
+ start: this.dateStart,
2084
+ end: this.dateEnd
2085
+ }
2086
+ };
2087
+ }
2088
+ }
2089
+ export {
2090
+ BaseChart as B,
2091
+ MetricsChart as M,
2092
+ PieChart as P,
2093
+ SeriesChart as S
2094
+ };
2095
+ //# sourceMappingURL=MetricsChart-CM4CI6eA.js.map