@quicdata/analytics 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quicdata/analytics",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -4,6 +4,7 @@ import './bar-chart.js';
4
4
  import './line-chart.js';
5
5
  import './pie-chart.js';
6
6
  import './table.js';
7
+ import './number-kpi.js';
7
8
  /**
8
9
  * Generic analytics widget: pass widgetId + apiUrl (and optional config);
9
10
  * fetches the widget definition and renders the appropriate chart/table internally.
@@ -1 +1 @@
1
- {"version":3,"file":"analytics-widget.d.ts","sourceRoot":"","sources":["../../../../libs/analytics/src/widgets/analytics-widget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,OAAO,EAAE,MAAM,KAAK,CAAC;AAIrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAE9E,OAAO,gBAAgB,CAAC;AACxB,OAAO,iBAAiB,CAAC;AACzB,OAAO,gBAAgB,CAAC;AACxB,OAAO,YAAY,CAAC;AAapB;;;;GAIG;AACH,qBACa,eAAgB,SAAQ,UAAU;IAC7C,OAAgB,MAAM,0BAmCpB;IAEF,sGAAsG;IAC1C,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAEtF,2FAA2F;IACjC,MAAM,EAAE,MAAM,CAAC;IAEzE,gGAAgG;IAC5D,KAAK,EAAE,MAAM,CAAC;IAElD,gEAAgE;IAC5B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IAE1F,2EAA2E;IACvC,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAEzE,4EAA4E;IACvC,IAAI,EAAE,OAAO,CAAC;IAEnD,yEAAyE;IACP,cAAc,EAAE,MAAM,CAAC;IAEzF,QAAyB,UAAU,CAAuB;IAC1D,QAAyB,WAAW,CAAgC;IACpE,QAAyB,QAAQ,CAAU;IAC3C,QAAyB,MAAM,CAAgB;;IAiBtC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;YAWvC,eAAe;IAqB7B,OAAO,KAAK,SAAS,GAGpB;IAED,OAAO,CAAC,kBAAkB;IA8DjB,MAAM;CAgBhB"}
1
+ {"version":3,"file":"analytics-widget.d.ts","sourceRoot":"","sources":["../../../../libs/analytics/src/widgets/analytics-widget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,OAAO,EAAE,MAAM,KAAK,CAAC;AAIrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAE9E,OAAO,gBAAgB,CAAC;AACxB,OAAO,iBAAiB,CAAC;AACzB,OAAO,gBAAgB,CAAC;AACxB,OAAO,YAAY,CAAC;AACpB,OAAO,iBAAiB,CAAC;AAazB;;;;GAIG;AACH,qBACa,eAAgB,SAAQ,UAAU;IAC7C,OAAgB,MAAM,0BAmCpB;IAEF,sGAAsG;IAC1C,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAEtF,2FAA2F;IACjC,MAAM,EAAE,MAAM,CAAC;IAEzE,gGAAgG;IAC5D,KAAK,EAAE,MAAM,CAAC;IAElD,gEAAgE;IAC5B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IAE1F,2EAA2E;IACvC,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAEzE,4EAA4E;IACvC,IAAI,EAAE,OAAO,CAAC;IAEnD,yEAAyE;IACP,cAAc,EAAE,MAAM,CAAC;IAEzF,QAAyB,UAAU,CAAuB;IAC1D,QAAyB,WAAW,CAAgC;IACpE,QAAyB,QAAQ,CAAU;IAC3C,QAAyB,MAAM,CAAgB;;IAiBtC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;YAWvC,eAAe;IAqB7B,OAAO,KAAK,SAAS,GAGpB;IAED,OAAO,CAAC,kBAAkB;IAyEjB,MAAM;CAgBhB"}
@@ -7,6 +7,7 @@ import './bar-chart.js';
7
7
  import './line-chart.js';
8
8
  import './pie-chart.js';
9
9
  import './table.js';
10
+ import './number-kpi.js';
10
11
  /** Map API chart_type to inner widget type. */
11
12
  const CHART_TYPE_TO_TAG = {
12
13
  Bar: 'analytics-bar-chart',
@@ -14,7 +15,7 @@ const CHART_TYPE_TO_TAG = {
14
15
  Line: 'analytics-line-chart',
15
16
  Pie: 'analytics-pie-chart',
16
17
  Donut: 'analytics-pie-chart',
17
- Number: 'analytics-table', // KPI: fallback to table for now
18
+ Number: 'analytics-number-kpi',
18
19
  Table: 'analytics-table',
19
20
  };
20
21
  /**
@@ -159,6 +160,17 @@ let AnalyticsWidget = class AnalyticsWidget extends LitElement {
159
160
  ?lazy=${common.lazy}
160
161
  prefetch-margin=${common.prefetchMargin}
161
162
  ></analytics-pie-chart>`;
163
+ case 'analytics-number-kpi':
164
+ return html `<analytics-number-kpi
165
+ widget-id=${common.widgetId}
166
+ api-url=${common.apiUrl}
167
+ .title=${common.title}
168
+ .dataParams=${common.dataParams}
169
+ .dashboard=${common.dashboard}
170
+ .initialDefinition=${common.initialDefinition}
171
+ ?lazy=${common.lazy}
172
+ prefetch-margin=${common.prefetchMargin}
173
+ ></analytics-number-kpi>`;
162
174
  case 'analytics-table':
163
175
  return html `<analytics-table
164
176
  widget-id=${common.widgetId}
@@ -3,12 +3,14 @@ export { BarChartWidget } from './bar-chart.js';
3
3
  export { LineChartWidget } from './line-chart.js';
4
4
  export { PieChartWidget } from './pie-chart.js';
5
5
  export { TableWidget } from './table.js';
6
+ export { NumberKpiWidget } from './number-kpi.js';
6
7
  export { AnalyticsWidget } from './analytics-widget.js';
7
8
  export { AnalyticsReport } from './analytics-report.js';
8
9
  import './bar-chart.js';
9
10
  import './line-chart.js';
10
11
  import './pie-chart.js';
11
12
  import './table.js';
13
+ import './number-kpi.js';
12
14
  import './analytics-widget.js';
13
15
  import './analytics-report.js';
14
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../libs/analytics/src/widgets/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,gBAAgB,CAAC;AACxB,OAAO,iBAAiB,CAAC;AACzB,OAAO,gBAAgB,CAAC;AACxB,OAAO,YAAY,CAAC;AACpB,OAAO,uBAAuB,CAAC;AAC/B,OAAO,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../libs/analytics/src/widgets/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,gBAAgB,CAAC;AACxB,OAAO,iBAAiB,CAAC;AACzB,OAAO,gBAAgB,CAAC;AACxB,OAAO,YAAY,CAAC;AACpB,OAAO,iBAAiB,CAAC;AACzB,OAAO,uBAAuB,CAAC;AAC/B,OAAO,uBAAuB,CAAC"}
package/widgets/index.js CHANGED
@@ -3,6 +3,7 @@ export { BarChartWidget } from './bar-chart.js';
3
3
  export { LineChartWidget } from './line-chart.js';
4
4
  export { PieChartWidget } from './pie-chart.js';
5
5
  export { TableWidget } from './table.js';
6
+ export { NumberKpiWidget } from './number-kpi.js';
6
7
  export { AnalyticsWidget } from './analytics-widget.js';
7
8
  export { AnalyticsReport } from './analytics-report.js';
8
9
  // Register custom elements so they work when imported (e.g. in designer/dashboard)
@@ -10,5 +11,6 @@ import './bar-chart.js';
10
11
  import './line-chart.js';
11
12
  import './pie-chart.js';
12
13
  import './table.js';
14
+ import './number-kpi.js';
13
15
  import './analytics-widget.js';
14
16
  import './analytics-report.js';
@@ -0,0 +1,52 @@
1
+ import { LitElement } from 'lit';
2
+ import type { WidgetDefinitionSubset } from '../core/fetch-data.js';
3
+ import type { DashboardContainer } from '../dashboard/dashboard-container.js';
4
+ /**
5
+ * Number KPI widget. Displays one or more large formatted numbers with labels.
6
+ * Data shape from backend transformNumber(): { chartType: 'Number', values, labels, options }.
7
+ */
8
+ export declare class NumberKpiWidget extends LitElement {
9
+ static styles: import("lit").CSSResult;
10
+ title: string;
11
+ hideRefreshButton: boolean;
12
+ dataUrl: string;
13
+ widgetId: string | number;
14
+ apiUrl: string;
15
+ dataParams: Record<string, string | number | boolean>;
16
+ dashboard: DashboardContainer | null;
17
+ lazy: boolean;
18
+ prefetchMargin: string;
19
+ initialDefinition: WidgetDefinitionSubset | undefined;
20
+ private _kpiData;
21
+ private _loading;
22
+ private _error;
23
+ private _dashboardParams;
24
+ private _viewportVisible;
25
+ private _definitionTitle;
26
+ private _loadGeneration;
27
+ private _hasLoadedOnce;
28
+ private _dashboardEl;
29
+ private _viewportUnobserve;
30
+ private _loadDataInFlight;
31
+ private _loadDataPendingRefresh;
32
+ private _loadDataScheduled;
33
+ constructor();
34
+ private _boundOnDashboardFilterChange;
35
+ private _boundOnDashboardRefresh;
36
+ connectedCallback(): void;
37
+ disconnectedCallback(): void;
38
+ updated(changed: Map<string, unknown>): void;
39
+ firstUpdated(): void;
40
+ private _attachDashboard;
41
+ private _detachDashboard;
42
+ private _getEffectiveDataUrl;
43
+ private _getEffectiveParams;
44
+ private get _effectiveTitle();
45
+ private _defer;
46
+ private _loadData;
47
+ private _onRefresh;
48
+ /** Format a number: apply decimal places, shorten_numbers (1234 → 1.2K). */
49
+ private _formatValue;
50
+ render(): import("lit-html").TemplateResult<1>;
51
+ }
52
+ //# sourceMappingURL=number-kpi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-kpi.d.ts","sourceRoot":"","sources":["../../../../libs/analytics/src/widgets/number-kpi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAGrD,OAAO,KAAK,EAA0B,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAM5F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAO9E;;;GAGG;AACH,qBACa,eAAgB,SAAQ,UAAU;IAC7C,OAAgB,MAAM,0BAsMpB;IAEkC,KAAK,EAAE,MAAM,CAAC;IACqB,iBAAiB,EAAE,OAAO,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IACtD,SAAS,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,IAAI,EAAE,OAAO,CAAC;IACe,cAAc,EAAE,MAAM,CAAC;IACrD,iBAAiB,EAAE,sBAAsB,GAAG,SAAS,CAAC;IAE1F,QAAyB,QAAQ,CAA6B;IAC9D,QAAyB,QAAQ,CAAU;IAC3C,QAAyB,MAAM,CAAgB;IAC/C,QAAyB,gBAAgB,CAA4C;IACrF,QAAyB,gBAAgB,CAAU;IACnD,QAAyB,gBAAgB,CAAS;IAElD,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,YAAY,CAAmC;IACvD,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,kBAAkB,CAAS;;IAsBnC,OAAO,CAAC,6BAA6B,CAInC;IAEF,OAAO,CAAC,wBAAwB,CAG9B;IAEO,iBAAiB,IAAI,IAAI;IAKzB,oBAAoB,IAAI,IAAI;IAO5B,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IA2B5C,YAAY,IAAI,IAAI;IAkB7B,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,KAAK,eAAe,GAE1B;IAED,OAAO,CAAC,MAAM;YAQA,SAAS;IA+DvB,OAAO,CAAC,UAAU;IAKlB,4EAA4E;IAC5E,OAAO,CAAC,YAAY;IAqBX,MAAM;CAqFhB"}
@@ -0,0 +1,569 @@
1
+ import { __decorate } from "tslib";
2
+ import { LitElement, html, css, nothing } from 'lit';
3
+ import { customElement, property, state } from 'lit/decorators.js';
4
+ import { buildWidgetDataUrl, fetchWidgetData, fetchWidgetDefinition, } from '../core/fetch-data.js';
5
+ import { EVENT_DASHBOARD_FILTER_CHANGE, EVENT_DASHBOARD_REFRESH, } from '../dashboard/index.js';
6
+ import { observeViewport } from '../core/viewport-observer.js';
7
+ /**
8
+ * Number KPI widget. Displays one or more large formatted numbers with labels.
9
+ * Data shape from backend transformNumber(): { chartType: 'Number', values, labels, options }.
10
+ */
11
+ let NumberKpiWidget = class NumberKpiWidget extends LitElement {
12
+ static { this.styles = css `
13
+ :host {
14
+ display: flex;
15
+ flex-direction: column;
16
+ width: 100%;
17
+ height: 100%;
18
+ }
19
+
20
+ .widget-wrap {
21
+ position: relative;
22
+ width: 100%;
23
+ height: 100%;
24
+ display: flex;
25
+ flex-direction: column;
26
+ min-height: 0;
27
+ }
28
+
29
+ .widget-header {
30
+ flex-shrink: 0;
31
+ padding: 0.5rem 0.75rem 0.25rem 0.75rem;
32
+ min-width: 0;
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ gap: 0.5rem;
37
+ background: var(--analytics-widget-header-bg, #fff);
38
+ color: var(--analytics-widget-header-color, #374151);
39
+ }
40
+
41
+ .widget-title {
42
+ margin: 0;
43
+ font-size: 0.9375rem;
44
+ font-weight: 600;
45
+ color: var(--analytics-widget-header-color, #374151);
46
+ line-height: 1.3;
47
+ display: -webkit-box;
48
+ -webkit-box-orient: vertical;
49
+ -webkit-line-clamp: 1;
50
+ overflow: hidden;
51
+ text-overflow: ellipsis;
52
+ }
53
+
54
+ .skeleton-title {
55
+ height: 1rem;
56
+ width: 8rem;
57
+ max-width: 60%;
58
+ border-radius: 4px;
59
+ background: linear-gradient(90deg, #e5e7eb 0%, #e5e7eb 40%, #f3f4f6 50%, #e5e7eb 60%, #e5e7eb 100%);
60
+ background-size: 200% 100%;
61
+ animation: kpi-shimmer 1.2s ease-in-out infinite;
62
+ }
63
+
64
+ .btn-refresh {
65
+ flex-shrink: 0;
66
+ display: inline-flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ width: 1.75rem;
70
+ height: 1.75rem;
71
+ padding: 0;
72
+ border: none;
73
+ border-radius: 4px;
74
+ background: transparent;
75
+ color: var(--analytics-widget-header-color, #6b7280);
76
+ cursor: pointer;
77
+ transition: color 0.15s ease, background-color 0.15s ease;
78
+ }
79
+ .btn-refresh:hover {
80
+ color: var(--analytics-widget-header-color, #374151);
81
+ background: #f3f4f6;
82
+ }
83
+ .btn-refresh:disabled {
84
+ cursor: not-allowed;
85
+ opacity: 0.6;
86
+ }
87
+ .btn-refresh svg {
88
+ width: 1rem;
89
+ height: 1rem;
90
+ }
91
+
92
+ .kpi-body {
93
+ flex: 1;
94
+ min-height: 0;
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ padding: var(--analytics-kpi-body-padding, 0.75rem);
99
+ gap: var(--analytics-kpi-gap, 1.5rem);
100
+ flex-wrap: wrap;
101
+ }
102
+
103
+ .kpi-block {
104
+ display: flex;
105
+ flex-direction: column;
106
+ align-items: var(--analytics-kpi-align, center);
107
+ gap: var(--analytics-kpi-block-gap, 0.125rem);
108
+ min-width: 0;
109
+ }
110
+
111
+ .kpi-value {
112
+ font-size: var(--analytics-kpi-value-size, 1.75rem);
113
+ font-weight: var(--analytics-kpi-value-weight, 600);
114
+ color: var(--analytics-kpi-value-color, #1f2937);
115
+ line-height: 1.2;
116
+ white-space: nowrap;
117
+ overflow: hidden;
118
+ text-overflow: ellipsis;
119
+ max-width: 100%;
120
+ }
121
+
122
+ .kpi-label {
123
+ font-size: var(--analytics-kpi-label-size, 0.75rem);
124
+ font-weight: var(--analytics-kpi-label-weight, 400);
125
+ color: var(--analytics-kpi-label-color, #6b7280);
126
+ line-height: 1.3;
127
+ white-space: nowrap;
128
+ overflow: hidden;
129
+ text-overflow: ellipsis;
130
+ max-width: 100%;
131
+ }
132
+
133
+ .kpi-prefix,
134
+ .kpi-suffix {
135
+ font-size: var(--analytics-kpi-affix-size, 1rem);
136
+ font-weight: var(--analytics-kpi-affix-weight, 400);
137
+ color: var(--analytics-kpi-affix-color, #6b7280);
138
+ }
139
+
140
+ /* Loading skeleton for KPI */
141
+ .skeleton-body {
142
+ flex: 1;
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: center;
146
+ padding: 1rem;
147
+ gap: 1.5rem;
148
+ }
149
+ .skeleton-block {
150
+ display: flex;
151
+ flex-direction: column;
152
+ align-items: center;
153
+ gap: 0.375rem;
154
+ }
155
+ .skeleton-value {
156
+ width: 4rem;
157
+ height: 1.75rem;
158
+ border-radius: 4px;
159
+ background: linear-gradient(90deg, #e5e7eb 0%, #e5e7eb 40%, #f3f4f6 50%, #e5e7eb 60%, #e5e7eb 100%);
160
+ background-size: 200% 100%;
161
+ animation: kpi-shimmer 1.2s ease-in-out infinite;
162
+ }
163
+ .skeleton-label {
164
+ width: 5rem;
165
+ height: 0.75rem;
166
+ border-radius: 4px;
167
+ background: linear-gradient(90deg, #e5e7eb 0%, #e5e7eb 40%, #f3f4f6 50%, #e5e7eb 60%, #e5e7eb 100%);
168
+ background-size: 200% 100%;
169
+ animation: kpi-shimmer 1.2s ease-in-out infinite;
170
+ }
171
+
172
+ @keyframes kpi-shimmer {
173
+ to { background-position: 200% 0; }
174
+ }
175
+
176
+ .error {
177
+ color: #c00;
178
+ padding: 0.5rem;
179
+ font-size: 0.875rem;
180
+ }
181
+
182
+ .empty {
183
+ padding: 0.5rem;
184
+ color: #6b7280;
185
+ }
186
+
187
+ .loading-overlay {
188
+ position: absolute;
189
+ inset: 0;
190
+ z-index: 10;
191
+ display: flex;
192
+ align-items: center;
193
+ justify-content: center;
194
+ background: rgba(255, 255, 255, 0.8);
195
+ backdrop-filter: blur(4px);
196
+ -webkit-backdrop-filter: blur(4px);
197
+ border-radius: 4px;
198
+ }
199
+ .loading-spinner {
200
+ width: 1.5rem;
201
+ height: 1.5rem;
202
+ border: 2px solid #e5e7eb;
203
+ border-top-color: #3b82f6;
204
+ border-radius: 50%;
205
+ animation: kpi-spin 0.7s linear infinite;
206
+ }
207
+ @keyframes kpi-spin {
208
+ to { transform: rotate(360deg); }
209
+ }
210
+ `; }
211
+ constructor() {
212
+ super();
213
+ this._loadGeneration = 0;
214
+ this._hasLoadedOnce = false;
215
+ this._dashboardEl = null;
216
+ this._viewportUnobserve = null;
217
+ this._loadDataInFlight = false;
218
+ this._loadDataPendingRefresh = false;
219
+ this._loadDataScheduled = false;
220
+ this._boundOnDashboardFilterChange = (e) => {
221
+ const ce = e;
222
+ this._dashboardParams = { ...(ce.detail?.params ?? {}) };
223
+ this._loadData();
224
+ };
225
+ this._boundOnDashboardRefresh = () => {
226
+ if (this.lazy && !this._viewportVisible)
227
+ return;
228
+ this._loadData();
229
+ };
230
+ this.title = '';
231
+ this.hideRefreshButton = false;
232
+ this.dataUrl = '';
233
+ this.widgetId = '';
234
+ this.apiUrl = '';
235
+ this.dataParams = {};
236
+ this.dashboard = null;
237
+ this.lazy = false;
238
+ this.prefetchMargin = '200px';
239
+ this.initialDefinition = undefined;
240
+ this._kpiData = null;
241
+ this._loading = false;
242
+ this._error = null;
243
+ this._dashboardParams = {};
244
+ this._viewportVisible = true;
245
+ this._definitionTitle = '';
246
+ }
247
+ connectedCallback() {
248
+ super.connectedCallback();
249
+ this._attachDashboard();
250
+ }
251
+ disconnectedCallback() {
252
+ this._viewportUnobserve?.();
253
+ this._viewportUnobserve = null;
254
+ this._detachDashboard();
255
+ super.disconnectedCallback();
256
+ }
257
+ updated(changed) {
258
+ if (changed.has('dashboard')) {
259
+ this._detachDashboard();
260
+ this._attachDashboard();
261
+ }
262
+ if (changed.has('widgetId') || changed.has('apiUrl')) {
263
+ this._definitionTitle = '';
264
+ }
265
+ if (changed.has('dataUrl') ||
266
+ changed.has('dataParams') ||
267
+ changed.has('widgetId') ||
268
+ changed.has('apiUrl')) {
269
+ if (this._getEffectiveDataUrl() && !this._loadDataScheduled) {
270
+ this._loadDataScheduled = true;
271
+ queueMicrotask(() => {
272
+ this._loadDataScheduled = false;
273
+ this._loadData();
274
+ });
275
+ }
276
+ }
277
+ if (changed.has('lazy')) {
278
+ this._viewportVisible = !this.lazy;
279
+ }
280
+ }
281
+ firstUpdated() {
282
+ if (this.lazy) {
283
+ this._viewportVisible = false;
284
+ this._viewportUnobserve = observeViewport(this, {
285
+ rootMargin: this.prefetchMargin || '200px',
286
+ threshold: 0,
287
+ }, {
288
+ onEnter: () => {
289
+ this._viewportVisible = true;
290
+ if (!this._hasLoadedOnce)
291
+ this._loadData();
292
+ },
293
+ onLeave: () => {
294
+ this._viewportVisible = false;
295
+ },
296
+ });
297
+ }
298
+ }
299
+ _attachDashboard() {
300
+ const d = this.dashboard;
301
+ if (!d)
302
+ return;
303
+ this._dashboardEl = d;
304
+ this._dashboardParams = typeof d.getDashboardParams === 'function' ? d.getDashboardParams() : {};
305
+ d.addEventListener(EVENT_DASHBOARD_FILTER_CHANGE, this._boundOnDashboardFilterChange);
306
+ d.addEventListener(EVENT_DASHBOARD_REFRESH, this._boundOnDashboardRefresh);
307
+ }
308
+ _detachDashboard() {
309
+ const d = this._dashboardEl;
310
+ if (!d)
311
+ return;
312
+ d.removeEventListener(EVENT_DASHBOARD_FILTER_CHANGE, this._boundOnDashboardFilterChange);
313
+ d.removeEventListener(EVENT_DASHBOARD_REFRESH, this._boundOnDashboardRefresh);
314
+ this._dashboardEl = null;
315
+ this._dashboardParams = {};
316
+ }
317
+ _getEffectiveDataUrl() {
318
+ if (this.apiUrl && this.widgetId) {
319
+ return buildWidgetDataUrl(this.apiUrl, this.widgetId);
320
+ }
321
+ return this.dataUrl;
322
+ }
323
+ _getEffectiveParams() {
324
+ const out = { ...(this.dataParams ?? {}) };
325
+ if (Object.keys(this._dashboardParams).length > 0) {
326
+ out.dashboard_params = this._dashboardParams;
327
+ }
328
+ return out;
329
+ }
330
+ get _effectiveTitle() {
331
+ return (this.title != null && this.title !== '') ? this.title : (this._definitionTitle ?? '');
332
+ }
333
+ _defer(fn) {
334
+ if (typeof requestAnimationFrame !== 'undefined') {
335
+ requestAnimationFrame(fn);
336
+ }
337
+ else {
338
+ setTimeout(fn, 0);
339
+ }
340
+ }
341
+ async _loadData() {
342
+ if (this.lazy && !this._viewportVisible)
343
+ return;
344
+ const dataUrl = this._getEffectiveDataUrl();
345
+ if (!dataUrl) {
346
+ this._error = null;
347
+ this._loading = false;
348
+ return;
349
+ }
350
+ if (this._loadDataInFlight) {
351
+ this._loadDataPendingRefresh = true;
352
+ return;
353
+ }
354
+ const gen = ++this._loadGeneration;
355
+ this._loadDataInFlight = true;
356
+ this._loading = true;
357
+ this._error = null;
358
+ try {
359
+ const useWidgetApi = Boolean(this.apiUrl && this.widgetId);
360
+ let params;
361
+ if (useWidgetApi && !this._definitionTitle) {
362
+ const def = this.initialDefinition ?? (await fetchWidgetDefinition(this.apiUrl, this.widgetId));
363
+ this._definitionTitle = def.title ?? '';
364
+ params = { ...(this.dataParams ?? {}) };
365
+ }
366
+ else {
367
+ params = this._getEffectiveParams();
368
+ }
369
+ if (Object.keys(this._dashboardParams).length > 0) {
370
+ params.dashboard_params = this._dashboardParams;
371
+ }
372
+ const res = await fetchWidgetData(dataUrl, params);
373
+ if (gen !== this._loadGeneration)
374
+ return;
375
+ const wd = res.widgetData;
376
+ const kpiData = wd && wd.chartType === 'Number'
377
+ ? { chartType: 'Number', values: wd.values ?? [], labels: wd.labels ?? [], options: wd.options }
378
+ : null;
379
+ this._defer(() => {
380
+ if (gen !== this._loadGeneration)
381
+ return;
382
+ this._loadDataInFlight = false;
383
+ this._kpiData = kpiData;
384
+ this._hasLoadedOnce = true;
385
+ this._loading = false;
386
+ if (this._loadDataPendingRefresh) {
387
+ this._loadDataPendingRefresh = false;
388
+ this._loadData();
389
+ }
390
+ });
391
+ }
392
+ catch (e) {
393
+ const errMsg = e instanceof Error ? e.message : String(e);
394
+ this._defer(() => {
395
+ if (gen !== this._loadGeneration)
396
+ return;
397
+ this._loadDataInFlight = false;
398
+ this._error = errMsg;
399
+ this._kpiData = null;
400
+ this._loading = false;
401
+ if (this._loadDataPendingRefresh) {
402
+ this._loadDataPendingRefresh = false;
403
+ this._loadData();
404
+ }
405
+ });
406
+ }
407
+ }
408
+ _onRefresh() {
409
+ if (this._loading)
410
+ return;
411
+ this._loadData();
412
+ }
413
+ /** Format a number: apply decimal places, shorten_numbers (1234 → 1.2K). */
414
+ _formatValue(value, decimal) {
415
+ const dec = decimal != null && decimal !== '' ? parseInt(decimal, 10) : undefined;
416
+ const abs = Math.abs(value);
417
+ // shorten_numbers: from widget config (checked via options or future config)
418
+ if (abs >= 1_000_000_000) {
419
+ const v = value / 1_000_000_000;
420
+ return (dec != null ? v.toFixed(dec) : v.toFixed(1)) + 'B';
421
+ }
422
+ if (abs >= 1_000_000) {
423
+ const v = value / 1_000_000;
424
+ return (dec != null ? v.toFixed(dec) : v.toFixed(1)) + 'M';
425
+ }
426
+ if (abs >= 10_000) {
427
+ const v = value / 1_000;
428
+ return (dec != null ? v.toFixed(dec) : v.toFixed(1)) + 'K';
429
+ }
430
+ if (dec != null)
431
+ return value.toFixed(dec);
432
+ if (Number.isInteger(value))
433
+ return value.toLocaleString();
434
+ return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
435
+ }
436
+ render() {
437
+ const canRefresh = Boolean(this._getEffectiveDataUrl()) && !this.hideRefreshButton;
438
+ const showSkeleton = this._loading && !this._hasLoadedOnce;
439
+ const showSpinnerOverlay = this._loading && this._hasLoadedOnce;
440
+ const showTitlePlaceholder = !this._effectiveTitle && !this.hideRefreshButton;
441
+ const headerContent = this._effectiveTitle || canRefresh || showTitlePlaceholder
442
+ ? html `
443
+ <header class="widget-header">
444
+ ${this._effectiveTitle
445
+ ? html `<h2 class="widget-title">${this._effectiveTitle}</h2>`
446
+ : showTitlePlaceholder
447
+ ? html `<div class="skeleton-title" aria-hidden="true"></div>`
448
+ : html `<span></span>`}
449
+ ${canRefresh
450
+ ? html `
451
+ <button
452
+ type="button"
453
+ class="btn-refresh"
454
+ title="Refresh"
455
+ aria-label="Refresh data"
456
+ ?disabled=${this._loading}
457
+ @click=${this._onRefresh}
458
+ >
459
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
460
+ <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
461
+ <path d="M3 3v5h5" />
462
+ <path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
463
+ <path d="M16 21h5v-5" />
464
+ </svg>
465
+ </button>
466
+ `
467
+ : ''}
468
+ </header>
469
+ `
470
+ : '';
471
+ const bodyContent = showSkeleton
472
+ ? html `
473
+ <div class="skeleton-body" aria-busy="true" aria-live="polite">
474
+ <div class="skeleton-block">
475
+ <div class="skeleton-value"></div>
476
+ <div class="skeleton-label"></div>
477
+ </div>
478
+ </div>
479
+ `
480
+ : this._error
481
+ ? html `<div class="error">${this._error}</div>`
482
+ : !this._kpiData || this._kpiData.values.length === 0
483
+ ? html `<div class="empty">No data</div>`
484
+ : html `
485
+ <div class="kpi-body">
486
+ ${this._kpiData.values.map((value, i) => {
487
+ const label = this._kpiData.labels[i] ?? '';
488
+ const opt = this._kpiData.options?.[i];
489
+ const prefix = opt?.prefix ?? '';
490
+ const suffix = opt?.suffix ?? '';
491
+ const formatted = this._formatValue(value, opt?.decimal);
492
+ return html `
493
+ <div class="kpi-block">
494
+ <span class="kpi-value">
495
+ ${prefix ? html `<span class="kpi-prefix">${prefix}</span>` : nothing}
496
+ ${formatted}
497
+ ${suffix ? html `<span class="kpi-suffix">${suffix}</span>` : nothing}
498
+ </span>
499
+ ${label ? html `<span class="kpi-label">${label}</span>` : nothing}
500
+ </div>
501
+ `;
502
+ })}
503
+ </div>
504
+ `;
505
+ return html `
506
+ <div class="widget-wrap">
507
+ ${headerContent}
508
+ ${bodyContent}
509
+ ${showSpinnerOverlay
510
+ ? html `<div class="loading-overlay" aria-busy="true" aria-live="polite">
511
+ <div class="loading-spinner" aria-hidden="true"></div>
512
+ </div>`
513
+ : nothing}
514
+ </div>
515
+ `;
516
+ }
517
+ };
518
+ __decorate([
519
+ property({ type: String })
520
+ ], NumberKpiWidget.prototype, "title", void 0);
521
+ __decorate([
522
+ property({ type: Boolean, attribute: 'hide-refresh-button' })
523
+ ], NumberKpiWidget.prototype, "hideRefreshButton", void 0);
524
+ __decorate([
525
+ property({ type: String, attribute: 'data-url' })
526
+ ], NumberKpiWidget.prototype, "dataUrl", void 0);
527
+ __decorate([
528
+ property({ type: String, attribute: 'widget-id' })
529
+ ], NumberKpiWidget.prototype, "widgetId", void 0);
530
+ __decorate([
531
+ property({ type: String, attribute: 'api-url' })
532
+ ], NumberKpiWidget.prototype, "apiUrl", void 0);
533
+ __decorate([
534
+ property({ type: Object })
535
+ ], NumberKpiWidget.prototype, "dataParams", void 0);
536
+ __decorate([
537
+ property({ type: Object })
538
+ ], NumberKpiWidget.prototype, "dashboard", void 0);
539
+ __decorate([
540
+ property({ type: Boolean })
541
+ ], NumberKpiWidget.prototype, "lazy", void 0);
542
+ __decorate([
543
+ property({ type: String, attribute: 'prefetch-margin' })
544
+ ], NumberKpiWidget.prototype, "prefetchMargin", void 0);
545
+ __decorate([
546
+ property({ type: Object })
547
+ ], NumberKpiWidget.prototype, "initialDefinition", void 0);
548
+ __decorate([
549
+ state()
550
+ ], NumberKpiWidget.prototype, "_kpiData", void 0);
551
+ __decorate([
552
+ state()
553
+ ], NumberKpiWidget.prototype, "_loading", void 0);
554
+ __decorate([
555
+ state()
556
+ ], NumberKpiWidget.prototype, "_error", void 0);
557
+ __decorate([
558
+ state()
559
+ ], NumberKpiWidget.prototype, "_dashboardParams", void 0);
560
+ __decorate([
561
+ state()
562
+ ], NumberKpiWidget.prototype, "_viewportVisible", void 0);
563
+ __decorate([
564
+ state()
565
+ ], NumberKpiWidget.prototype, "_definitionTitle", void 0);
566
+ NumberKpiWidget = __decorate([
567
+ customElement('analytics-number-kpi')
568
+ ], NumberKpiWidget);
569
+ export { NumberKpiWidget };