@aquera/nile-visualization 2.9.10 → 2.9.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/dist/src/nile-chart/nile-chart.css.js +4 -4
- package/dist/src/nile-chart/nile-chart.js +1 -1
- package/dist/src/nile-kpi-chart/nile-kpi-chart.css.js +197 -24
- package/dist/src/nile-kpi-chart/nile-kpi-chart.d.ts +10 -1
- package/dist/src/nile-kpi-chart/nile-kpi-chart.js +205 -26
- package/package.json +1 -1
|
@@ -133,20 +133,20 @@ export const styles = css `
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
@container (max-width: 400px) {
|
|
136
|
-
.nile-chart-header {
|
|
136
|
+
.nile-chart-card:not(.nile-chart-card--kpi) .nile-chart-header {
|
|
137
137
|
padding: var(--nile-spacing-xl, var(--ng-spacing-xl)) var(--nile-spacing-2xl, var(--ng-spacing-2xl)) var(--nile-spacing-lg, var(--ng-spacing-lg));
|
|
138
138
|
}
|
|
139
|
-
.nile-chart-header.nile-chart-header--compact {
|
|
139
|
+
.nile-chart-card:not(.nile-chart-card--kpi) .nile-chart-header.nile-chart-header--compact {
|
|
140
140
|
padding: var(--nile-spacing-lg, var(--ng-spacing-lg)) var(--nile-spacing-2xl, var(--ng-spacing-2xl));
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
@container (max-width: 280px) {
|
|
145
|
-
.nile-chart-header {
|
|
145
|
+
.nile-chart-card:not(.nile-chart-card--kpi) .nile-chart-header {
|
|
146
146
|
padding: var(--nile-spacing-lg, var(--ng-spacing-lg)) var(--nile-spacing-xl, var(--ng-spacing-xl));
|
|
147
147
|
gap: var(--nile-spacing-sm, var(--ng-spacing-sm));
|
|
148
148
|
}
|
|
149
|
-
.nile-chart-header.nile-chart-header--compact {
|
|
149
|
+
.nile-chart-card:not(.nile-chart-card--kpi) .nile-chart-header.nile-chart-header--compact {
|
|
150
150
|
padding: var(--nile-spacing-md, var(--ng-spacing-md)) var(--nile-spacing-xl, var(--ng-spacing-xl));
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -1737,7 +1737,7 @@ let NileChart = class NileChart extends NileElement {
|
|
|
1737
1737
|
const isLoading = this.loading || (this.activeConfig?.loading ?? false);
|
|
1738
1738
|
const isGrid = this.activeConfig?.type === 'grid';
|
|
1739
1739
|
return html `
|
|
1740
|
-
<div class="nile-chart-card ${isGrid ? 'nile-chart-card--grid' : ''} ${isLoading ? 'nile-chart-card--loading' : ''}" part="chart-card">
|
|
1740
|
+
<div class="nile-chart-card ${isGrid ? 'nile-chart-card--grid' : ''} ${isLoading ? 'nile-chart-card--loading' : ''} ${this.activeConfig?.type === 'kpi' ? 'nile-chart-card--kpi' : ''}" part="chart-card">
|
|
1741
1741
|
${this.renderHeader()}
|
|
1742
1742
|
<div class="nile-chart-wrapper">
|
|
1743
1743
|
<div class="nile-chart-inner ${this.activeConfig?.type === 'kpi' ? 'nile-chart-inner--kpi' : ''} ${this.activeConfig?.type === 'filter' ? 'nile-chart-inner--filter' : ''}" part="chart-inner">
|
|
@@ -47,7 +47,7 @@ export const styles = css `
|
|
|
47
47
|
--nile-kpi-label-color: var(--nile-colors-neutral-700, var(--ng-colors-text-secondary-700));
|
|
48
48
|
--nile-kpi-label-font-size: var(--nile-type-scale-3, var(--ng-font-size-text-sm));
|
|
49
49
|
--nile-kpi-label-font-weight: var(--nile-font-weight-medium, var(--ng-font-weight-medium));
|
|
50
|
-
--nile-kpi-value-font-size: clamp(
|
|
50
|
+
--nile-kpi-value-font-size: clamp(22px, 9cqi, 36px);
|
|
51
51
|
--nile-kpi-value-color: var(--nile-colors-dark-900, var(--ng-colors-text-primary-900));
|
|
52
52
|
--nile-kpi-prefix-suffix-font-size: var(--nile-type-scale-6, var(--ng-font-size-text-xl));
|
|
53
53
|
--nile-kpi-prefix-suffix-color: var(--nile-colors-neutral-700, var(--ng-colors-text-secondary-700));
|
|
@@ -63,7 +63,14 @@ export const styles = css `
|
|
|
63
63
|
position: relative;
|
|
64
64
|
box-sizing: border-box;
|
|
65
65
|
overflow: hidden;
|
|
66
|
-
container
|
|
66
|
+
/* size containment so @container queries can use both width and height */
|
|
67
|
+
container-type: size;
|
|
68
|
+
min-height: 120px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Gauge variant needs more vertical room so the ring renders at a usable size. */
|
|
72
|
+
:host([variant='gauge']) {
|
|
73
|
+
min-height: 200px;
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
:host([hidden]) {
|
|
@@ -75,6 +82,7 @@ export const styles = css `
|
|
|
75
82
|
element is already flex:1, so it stretches with the host. */
|
|
76
83
|
:host([fit]) {
|
|
77
84
|
height: 100%;
|
|
85
|
+
min-height: 0;
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
/* Card / gauge chrome on the host when used alone (inside nile-chart, embed-in-nile-chart skips this). */
|
|
@@ -142,6 +150,7 @@ export const styles = css `
|
|
|
142
150
|
align-items: center;
|
|
143
151
|
gap: var(--nile-spacing-md, var(--ng-spacing-md));
|
|
144
152
|
flex-wrap: nowrap;
|
|
153
|
+
flex: 0 0 auto;
|
|
145
154
|
min-width: 0;
|
|
146
155
|
overflow: hidden;
|
|
147
156
|
}
|
|
@@ -155,10 +164,7 @@ export const styles = css `
|
|
|
155
164
|
line-height: 1.2;
|
|
156
165
|
cursor: default;
|
|
157
166
|
white-space: nowrap;
|
|
158
|
-
|
|
159
|
-
flex: 0 1 auto;
|
|
160
|
-
overflow: hidden;
|
|
161
|
-
text-overflow: ellipsis;
|
|
167
|
+
flex: 0 0 auto;
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
.kpi-prefix,
|
|
@@ -178,6 +184,21 @@ export const styles = css `
|
|
|
178
184
|
font-family: var(--nile-font-family-serif, var(--ng-font-family-body));
|
|
179
185
|
font-size: var(--nile-type-scale-2, var(--ng-font-size-text-xs));
|
|
180
186
|
font-weight: var(--nile-font-weight-medium, var(--ng-font-weight-medium));
|
|
187
|
+
flex: 1 1 0;
|
|
188
|
+
min-width: 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.kpi-trend-arrow,
|
|
192
|
+
.kpi-trend-value {
|
|
193
|
+
flex: 0 0 auto;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.kpi-trend-label {
|
|
197
|
+
flex: 0 1 auto;
|
|
198
|
+
min-width: 0;
|
|
199
|
+
overflow: hidden;
|
|
200
|
+
text-overflow: ellipsis;
|
|
201
|
+
white-space: nowrap;
|
|
181
202
|
line-height: 1;
|
|
182
203
|
flex-shrink: 1;
|
|
183
204
|
min-width: 0;
|
|
@@ -229,20 +250,17 @@ export const styles = css `
|
|
|
229
250
|
|
|
230
251
|
.kpi-sparkline {
|
|
231
252
|
width: 100%;
|
|
232
|
-
flex:
|
|
233
|
-
min-height:
|
|
253
|
+
flex: 1 1 24px;
|
|
254
|
+
min-height: 24px;
|
|
234
255
|
margin-top: var(--nile-spacing-xs, var(--ng-spacing-xs));
|
|
235
256
|
}
|
|
236
257
|
|
|
237
258
|
/* ── Container queries: scale down for narrow cards ── */
|
|
238
259
|
|
|
239
|
-
/* Medium-small: ~280px and below —
|
|
260
|
+
/* Medium-small: ~280px and below — shrink prefix/suffix only */
|
|
240
261
|
@container (max-width: 280px) {
|
|
241
262
|
:host {
|
|
242
|
-
--nile-kpi-padding-v: var(--nile-spacing-lg, var(--ng-spacing-lg));
|
|
243
|
-
--nile-kpi-padding-h: var(--nile-spacing-xl, var(--ng-spacing-xl));
|
|
244
263
|
--nile-kpi-prefix-suffix-font-size: var(--nile-type-scale-4, var(--ng-font-size-text-md));
|
|
245
|
-
--nile-kpi-label-font-size: var(--nile-type-scale-2, var(--ng-font-size-text-xs));
|
|
246
264
|
}
|
|
247
265
|
|
|
248
266
|
.kpi-value-row {
|
|
@@ -257,8 +275,6 @@ export const styles = css `
|
|
|
257
275
|
/* Small: ~220px and below — tighten further; description/sparkline stay visible. */
|
|
258
276
|
@container (max-width: 220px) {
|
|
259
277
|
:host {
|
|
260
|
-
--nile-kpi-padding-v: var(--nile-spacing-md, var(--ng-spacing-md));
|
|
261
|
-
--nile-kpi-padding-h: var(--nile-spacing-lg, var(--ng-spacing-lg));
|
|
262
278
|
--nile-kpi-prefix-suffix-font-size: var(--nile-type-scale-3, var(--ng-font-size-text-sm));
|
|
263
279
|
--nile-kpi-description-font-size: var(--nile-type-scale-1, var(--ng-font-size-text-xs));
|
|
264
280
|
}
|
|
@@ -269,15 +285,161 @@ export const styles = css `
|
|
|
269
285
|
}
|
|
270
286
|
}
|
|
271
287
|
|
|
272
|
-
/*
|
|
288
|
+
/* Sparkline is the lowest-priority piece — hide it before the trend so
|
|
289
|
+
the visible order stays value → trend → sparkline. */
|
|
273
290
|
@container (max-width: 180px) {
|
|
291
|
+
.kpi-sparkline {
|
|
292
|
+
display: none;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* At minimum height (h ≤ 75px and not short-wide) —
|
|
297
|
+
hide everything except the value and trend. */
|
|
298
|
+
@container (max-height: 50px) and (max-width: 319.98px) {
|
|
299
|
+
.kpi-label,
|
|
300
|
+
.kpi-description,
|
|
301
|
+
.kpi-sparkline {
|
|
302
|
+
display: none;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
:host([variant='gauge']) .kpi {
|
|
306
|
+
padding: 0;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* Gauge card is short — collapse padding so the gauge still renders. */
|
|
311
|
+
@container (max-height: 109.98px) {
|
|
312
|
+
:host([variant='gauge']) .kpi {
|
|
313
|
+
padding: var(--nile-spacing-xs, var(--ng-spacing-xs)) var(--nile-spacing-md, var(--ng-spacing-md));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Narrow card — keep trend inline next to value. Behavior is the same at
|
|
318
|
+
any height so the trend doesn't appear/disappear when the card grows
|
|
319
|
+
taller, only when it grows wider/narrower. The arrow + numeric value
|
|
320
|
+
stay intact; only the label ellipses when space is tight. */
|
|
321
|
+
@container (max-width: 320px) {
|
|
274
322
|
.kpi-value-row {
|
|
275
|
-
flex-
|
|
276
|
-
|
|
323
|
+
flex-direction: row;
|
|
324
|
+
align-items: center;
|
|
325
|
+
gap: var(--nile-spacing-xs, var(--ng-spacing-xs));
|
|
326
|
+
overflow: hidden;
|
|
277
327
|
}
|
|
278
328
|
|
|
329
|
+
/* Treat arrow + value + label as a single text run that ellipses
|
|
330
|
+
together: switch the trend from flex to a single inline-block with
|
|
331
|
+
white-space:nowrap so text-overflow:ellipsis applies across the
|
|
332
|
+
whole "▲ 14.2% vs last month" string instead of only the label. */
|
|
279
333
|
.kpi-trend {
|
|
280
|
-
|
|
334
|
+
display: inline-block;
|
|
335
|
+
flex: 1 1 0;
|
|
336
|
+
min-width: 0;
|
|
337
|
+
max-width: 100%;
|
|
338
|
+
padding-left: 0;
|
|
339
|
+
overflow: hidden;
|
|
340
|
+
text-overflow: ellipsis;
|
|
341
|
+
white-space: nowrap;
|
|
342
|
+
line-height: 1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.kpi-trend-arrow {
|
|
346
|
+
display: inline-block;
|
|
347
|
+
vertical-align: middle;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.kpi-trend-value,
|
|
351
|
+
.kpi-trend-label {
|
|
352
|
+
display: inline;
|
|
353
|
+
flex: initial;
|
|
354
|
+
overflow: visible;
|
|
355
|
+
text-overflow: clip;
|
|
356
|
+
min-width: 0;
|
|
357
|
+
white-space: nowrap;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* Card too short for a clean sparkline — hide it (unless short-wide layout below restores it). */
|
|
362
|
+
@container (max-height: 70px) {
|
|
363
|
+
.kpi-sparkline {
|
|
364
|
+
display: none;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* Tall — there is enough vertical room, bring the sparkline back even at narrow widths. */
|
|
369
|
+
@container (min-height: 80px) {
|
|
370
|
+
.kpi-sparkline {
|
|
371
|
+
display: block;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* Short but wide (h ≤ 110, w ≥ 320) — lay out value, trend and sparkline horizontally.
|
|
376
|
+
Skip the card-variant gauge here so the gauge ring keeps its vertical room. */
|
|
377
|
+
@container (max-height: 110px) and (min-width: 320px) {
|
|
378
|
+
:host(:not([variant='gauge'])) .kpi {
|
|
379
|
+
flex-direction: row;
|
|
380
|
+
align-items: flex-start;
|
|
381
|
+
justify-content: flex-start;
|
|
382
|
+
gap: var(--nile-spacing-lg, var(--ng-spacing-lg));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.kpi-label,
|
|
386
|
+
.kpi-description {
|
|
387
|
+
display: none;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.kpi-value-row {
|
|
391
|
+
flex: 0 0 auto;
|
|
392
|
+
flex-direction: row;
|
|
393
|
+
align-items: center;
|
|
394
|
+
overflow: visible;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.kpi-trend {
|
|
398
|
+
display: inline-flex;
|
|
399
|
+
flex: 0 1 auto;
|
|
400
|
+
min-width: 0;
|
|
401
|
+
max-width: 100%;
|
|
402
|
+
overflow: hidden;
|
|
403
|
+
padding-left: var(--nile-spacing-md, var(--ng-spacing-md));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.kpi-trend-label {
|
|
407
|
+
overflow: hidden;
|
|
408
|
+
text-overflow: ellipsis;
|
|
409
|
+
white-space: nowrap;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.kpi-sparkline {
|
|
413
|
+
display: block;
|
|
414
|
+
flex: 1 1 0;
|
|
415
|
+
min-width: 0;
|
|
416
|
+
margin-top: 0;
|
|
417
|
+
align-self: stretch;
|
|
418
|
+
height: auto;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
@container (max-width: 100px) {
|
|
423
|
+
.kpi-sparkline {
|
|
424
|
+
display: none;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* Constrained both ways — only show the value. */
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@container (max-width: 180px) and (max-height: 70px) {
|
|
432
|
+
|
|
433
|
+
.kpi-description,
|
|
434
|
+
.kpi-sparkline {
|
|
435
|
+
display: none;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* Short but wide — drop description, keep trend (and sparkline) alongside value. */
|
|
440
|
+
@container (min-width: 180.01px) and (max-height: 85px) {
|
|
441
|
+
.kpi-description {
|
|
442
|
+
display: none;
|
|
281
443
|
}
|
|
282
444
|
}
|
|
283
445
|
|
|
@@ -288,13 +450,24 @@ export const styles = css `
|
|
|
288
450
|
text-align: center;
|
|
289
451
|
}
|
|
290
452
|
|
|
291
|
-
.kpi-gauge-
|
|
453
|
+
.kpi-gauge-slot {
|
|
454
|
+
flex: 1 1 0;
|
|
292
455
|
width: 100%;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
456
|
+
min-width: 0;
|
|
457
|
+
min-height: 40px;
|
|
458
|
+
align-self: stretch;
|
|
459
|
+
display: flex;
|
|
460
|
+
align-items: center;
|
|
461
|
+
justify-content: center;
|
|
462
|
+
overflow: hidden;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.kpi-gauge-container {
|
|
466
|
+
position: relative;
|
|
467
|
+
overflow: hidden;
|
|
468
|
+
box-sizing: border-box;
|
|
469
|
+
display: block;
|
|
470
|
+
flex: 0 0 auto;
|
|
298
471
|
}
|
|
299
472
|
|
|
300
473
|
.kpi--gauge .kpi-value-row {
|
|
@@ -4,7 +4,7 @@ import NileElement from '../internal/nile-element.js';
|
|
|
4
4
|
import type { AqConfigType } from '../internal/types/aq-config.type.js';
|
|
5
5
|
export type TrendDirection = 'up' | 'down' | 'neutral';
|
|
6
6
|
export type KpiVariant = 'default' | 'card' | 'gauge' | 'accent';
|
|
7
|
-
export type SparklineType = 'area' | 'line';
|
|
7
|
+
export type SparklineType = 'area' | 'line' | 'column' | 'bar' | 'spline' | 'areaspline' | 'pie' | 'scatter';
|
|
8
8
|
export type KpiValueFormat = 'auto' | 'K' | 'M' | 'B' | 'T' | 'none';
|
|
9
9
|
export type KpiNumberSystem = 'indian' | 'international';
|
|
10
10
|
/** `chart` slice for `<nile-kpi-chart>.config` (discriminated by `type: 'kpi'`). */
|
|
@@ -136,6 +136,7 @@ export declare class NileKpiChart extends NileElement {
|
|
|
136
136
|
private _tipEl;
|
|
137
137
|
private sparklineContainer;
|
|
138
138
|
private gaugeContainer;
|
|
139
|
+
private gaugeSlot;
|
|
139
140
|
/** Full configuration: `{ chart, aq }` (same convention as other Nile charts). */
|
|
140
141
|
config: NileKpiConfigInputType | null;
|
|
141
142
|
/** Display variant: default (flat), card (bordered container), gauge (Highcharts solid gauge). */
|
|
@@ -264,6 +265,12 @@ export declare class NileKpiChart extends NileElement {
|
|
|
264
265
|
disconnectedCallback(): void;
|
|
265
266
|
protected firstUpdated(): void;
|
|
266
267
|
protected updated(changedProperties: PropertyValues): void;
|
|
268
|
+
private _lastSparkSize;
|
|
269
|
+
private _lastGaugeSize;
|
|
270
|
+
private _gaugeRafScheduled;
|
|
271
|
+
private _scheduleGaugeSync;
|
|
272
|
+
private _syncGaugeNow;
|
|
273
|
+
private syncGaugeChartSize;
|
|
267
274
|
private syncSparklineChartSize;
|
|
268
275
|
private setupResizeObserver;
|
|
269
276
|
private _onSparklineMouseMove;
|
|
@@ -278,7 +285,9 @@ export declare class NileKpiChart extends NileElement {
|
|
|
278
285
|
private _onValueEnter;
|
|
279
286
|
private _onDescEnter;
|
|
280
287
|
private _onLabelEnter;
|
|
288
|
+
private _onTrendEnter;
|
|
281
289
|
private _onGaugeEnter;
|
|
290
|
+
private _onTrendKeydown;
|
|
282
291
|
private _onTipLeave;
|
|
283
292
|
private renderTrend;
|
|
284
293
|
render(): TemplateResult;
|
|
@@ -140,11 +140,18 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
140
140
|
* Set by nile-chart: skip host border/shadow (variant card/gauge) so the parent chart-card is the only frame.
|
|
141
141
|
*/
|
|
142
142
|
this.embedInNileChart = false;
|
|
143
|
+
// ── Chart sizing ─────────────────────────────────────────────────────────
|
|
144
|
+
this._lastSparkSize = { w: 0, h: 0 };
|
|
145
|
+
this._lastGaugeSize = { w: 0, h: 0 };
|
|
146
|
+
this._gaugeRafScheduled = false;
|
|
143
147
|
// ── Sparkline mousemove tooltip ──────────────────────────────────────────
|
|
144
148
|
this._onSparklineMouseMove = (e) => {
|
|
145
149
|
const chart = this.sparklineChart;
|
|
146
150
|
if (!chart || !this.sparklineContainer)
|
|
147
151
|
return;
|
|
152
|
+
// Pie uses Highcharts' native tooltip — the x-snap logic below is meaningless for radial layouts.
|
|
153
|
+
if (this.sparklineType === 'pie')
|
|
154
|
+
return;
|
|
148
155
|
const series = chart.series[0];
|
|
149
156
|
if (!series?.points?.length)
|
|
150
157
|
return;
|
|
@@ -192,10 +199,29 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
192
199
|
const rect = el.getBoundingClientRect();
|
|
193
200
|
this._showTip(this.label ?? '', rect.left + rect.width / 2, rect.top);
|
|
194
201
|
};
|
|
202
|
+
this._onTrendEnter = (e) => {
|
|
203
|
+
const el = e.currentTarget;
|
|
204
|
+
// Only show a tooltip when the trend is actually clipped/ellipsized.
|
|
205
|
+
const hasEllipsis = getComputedStyle(el).textOverflow === 'ellipsis';
|
|
206
|
+
if (!hasEllipsis)
|
|
207
|
+
return;
|
|
208
|
+
if (el.scrollWidth <= el.clientWidth + 2)
|
|
209
|
+
return;
|
|
210
|
+
const rect = el.getBoundingClientRect();
|
|
211
|
+
const dir = this.trendDirection;
|
|
212
|
+
const arrow = dir === 'up' ? '▲' : dir === 'down' ? '▼' : '–';
|
|
213
|
+
const pct = this.trendValue !== null ? `${Math.abs(this.trendValue).toFixed(1)}%` : '';
|
|
214
|
+
const text = `${arrow} ${pct}${this.trendLabel ? ' ' + this.trendLabel : ''}`.trim();
|
|
215
|
+
this._showTip(text, rect.left + rect.width / 2, rect.top);
|
|
216
|
+
};
|
|
195
217
|
this._onGaugeEnter = (e) => {
|
|
196
218
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
197
219
|
this._showTip(this.getTooltipContent(), rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
198
220
|
};
|
|
221
|
+
this._onTrendKeydown = (e) => {
|
|
222
|
+
if (e.key === 'Escape')
|
|
223
|
+
this._hideTip();
|
|
224
|
+
};
|
|
199
225
|
this._onTipLeave = () => {
|
|
200
226
|
this._hideTip();
|
|
201
227
|
};
|
|
@@ -623,29 +649,83 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
623
649
|
if (gaugeProps.some(p => changedProperties.has(p))) {
|
|
624
650
|
if (this.gaugeChart) {
|
|
625
651
|
this.gaugeChart.update(this.buildGaugeOptions(), true, true);
|
|
652
|
+
requestAnimationFrame(() => this.syncGaugeChartSize());
|
|
626
653
|
}
|
|
627
654
|
else {
|
|
628
655
|
this.initGauge();
|
|
629
656
|
}
|
|
630
657
|
}
|
|
631
658
|
}
|
|
632
|
-
|
|
659
|
+
_scheduleGaugeSync() {
|
|
660
|
+
if (this._gaugeRafScheduled)
|
|
661
|
+
return;
|
|
662
|
+
this._gaugeRafScheduled = true;
|
|
663
|
+
requestAnimationFrame(() => {
|
|
664
|
+
this._gaugeRafScheduled = false;
|
|
665
|
+
this._syncGaugeNow();
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
_syncGaugeNow() {
|
|
669
|
+
if (!this.gaugeContainer || !this.gaugeSlot || this.variant !== 'gauge')
|
|
670
|
+
return;
|
|
671
|
+
// Hide the gauge container while measuring the slot
|
|
672
|
+
const prevDisplay = this.gaugeContainer.style.display;
|
|
673
|
+
this.gaugeContainer.style.display = 'none';
|
|
674
|
+
const rect = this.gaugeSlot.getBoundingClientRect();
|
|
675
|
+
this.gaugeContainer.style.display = prevDisplay;
|
|
676
|
+
const w = Math.max(1, Math.round(rect.width));
|
|
677
|
+
const h = Math.max(1, Math.round(rect.height));
|
|
678
|
+
const size = Math.max(1, Math.min(w, h));
|
|
679
|
+
// writing its size re-fires the observer.
|
|
680
|
+
if (size === this._lastGaugeSize.w && this.gaugeChart)
|
|
681
|
+
return;
|
|
682
|
+
this._lastGaugeSize = { w: size, h: size };
|
|
683
|
+
this.gaugeContainer.style.width = `${size}px`;
|
|
684
|
+
this.gaugeContainer.style.height = `${size}px`;
|
|
685
|
+
if (!this._hc)
|
|
686
|
+
return;
|
|
687
|
+
if (this.gaugeChart) {
|
|
688
|
+
this.gaugeChart.setSize(size, size, false);
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
this.gaugeChart = this._hc.chart(this.gaugeContainer, this.buildGaugeOptions());
|
|
692
|
+
this.gaugeChart.setSize(size, size, false);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
syncGaugeChartSize() {
|
|
696
|
+
// always measure after the layout
|
|
697
|
+
this._scheduleGaugeSync();
|
|
698
|
+
}
|
|
633
699
|
syncSparklineChartSize() {
|
|
634
700
|
if (!this.sparklineChart || !this.sparklineContainer)
|
|
635
701
|
return;
|
|
636
702
|
const rect = this.sparklineContainer.getBoundingClientRect();
|
|
703
|
+
const w = Math.max(1, Math.round(rect.width));
|
|
637
704
|
const h = Math.max(22, Math.round(rect.height));
|
|
638
|
-
this.
|
|
705
|
+
if (w === this._lastSparkSize.w && h === this._lastSparkSize.h)
|
|
706
|
+
return;
|
|
707
|
+
this._lastSparkSize = { w, h };
|
|
708
|
+
// Rebuild the chart
|
|
709
|
+
this.destroySparkline();
|
|
710
|
+
if (!this._hc)
|
|
711
|
+
return;
|
|
712
|
+
this.sparklineChart = this._hc.chart(this.sparklineContainer, this.buildSparklineOptions());
|
|
713
|
+
this.sparklineChart.setSize(w, h, false);
|
|
714
|
+
this.sparklineContainer.addEventListener('mousemove', this._onSparklineMouseMove);
|
|
715
|
+
this.sparklineContainer.addEventListener('mouseleave', this._onSparklineMouseLeave);
|
|
639
716
|
}
|
|
640
717
|
setupResizeObserver() {
|
|
641
718
|
this.resizeObserver = new ResizeObserver(() => {
|
|
642
719
|
this.syncSparklineChartSize();
|
|
643
|
-
this.
|
|
720
|
+
this.syncGaugeChartSize();
|
|
644
721
|
});
|
|
722
|
+
this.resizeObserver.observe(this);
|
|
645
723
|
if (this.sparklineContainer)
|
|
646
724
|
this.resizeObserver.observe(this.sparklineContainer);
|
|
647
725
|
if (this.gaugeContainer)
|
|
648
726
|
this.resizeObserver.observe(this.gaugeContainer);
|
|
727
|
+
if (this.gaugeSlot)
|
|
728
|
+
this.resizeObserver.observe(this.gaugeSlot);
|
|
649
729
|
}
|
|
650
730
|
// ── Chart options ────────────────────────────────────────────────────────
|
|
651
731
|
buildSparklineOptions() {
|
|
@@ -653,8 +733,37 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
653
733
|
const lineWidth = this.sparklineLineWidth;
|
|
654
734
|
const seriesType = this.sparklineType;
|
|
655
735
|
const topAlpha = Math.round(Math.min(1, Math.max(0, this.sparklineFillOpacity)) * 255).toString(16).padStart(2, '0');
|
|
736
|
+
// Pie variant — a compact pie with no slice labels / connectors.
|
|
737
|
+
if (seriesType === 'pie') {
|
|
738
|
+
const defaults = {
|
|
739
|
+
chart: { type: 'pie', height: this.sparklineHeight, margin: [0, 0, 0, 0], backgroundColor: 'transparent' },
|
|
740
|
+
title: { text: undefined },
|
|
741
|
+
subtitle: { text: undefined },
|
|
742
|
+
legend: { enabled: false },
|
|
743
|
+
tooltip: {
|
|
744
|
+
enabled: true,
|
|
745
|
+
outside: true,
|
|
746
|
+
pointFormat: '<b>{point.y}</b> ({point.percentage:.1f}%)',
|
|
747
|
+
},
|
|
748
|
+
plotOptions: {
|
|
749
|
+
pie: {
|
|
750
|
+
size: '100%',
|
|
751
|
+
borderWidth: 0,
|
|
752
|
+
dataLabels: { enabled: false },
|
|
753
|
+
states: { hover: { enabled: true } },
|
|
754
|
+
allowPointSelect: false,
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
series: [{ type: 'pie', data: this.sparkline.map((v, i) => ({ y: v, name: `Slice ${i + 1}` })) }],
|
|
758
|
+
credits: { enabled: false },
|
|
759
|
+
};
|
|
760
|
+
return { ...defaults, ...this.options };
|
|
761
|
+
}
|
|
656
762
|
const plotKey = seriesType;
|
|
657
|
-
const
|
|
763
|
+
const isAreaLike = seriesType === 'area' || seriesType === 'areaspline';
|
|
764
|
+
const isBarLike = seriesType === 'column' || seriesType === 'bar';
|
|
765
|
+
const isScatter = seriesType === 'scatter';
|
|
766
|
+
const fillColor = isAreaLike
|
|
658
767
|
? {
|
|
659
768
|
fillColor: {
|
|
660
769
|
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
@@ -665,6 +774,28 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
665
774
|
},
|
|
666
775
|
}
|
|
667
776
|
: {};
|
|
777
|
+
const seriesPlotOptions = isBarLike
|
|
778
|
+
? {
|
|
779
|
+
color: brandColor,
|
|
780
|
+
borderWidth: 0,
|
|
781
|
+
pointPadding: 0.05,
|
|
782
|
+
groupPadding: 0.05,
|
|
783
|
+
states: { hover: { enabled: false } },
|
|
784
|
+
}
|
|
785
|
+
: isScatter
|
|
786
|
+
? {
|
|
787
|
+
color: brandColor,
|
|
788
|
+
marker: { enabled: true, radius: Math.max(2, lineWidth) },
|
|
789
|
+
states: { hover: { enabled: false } },
|
|
790
|
+
}
|
|
791
|
+
: {
|
|
792
|
+
marker: { enabled: this.sparklineMarkers },
|
|
793
|
+
lineWidth,
|
|
794
|
+
lineColor: brandColor,
|
|
795
|
+
color: brandColor,
|
|
796
|
+
...fillColor,
|
|
797
|
+
states: { hover: { lineWidth } },
|
|
798
|
+
};
|
|
668
799
|
const defaults = {
|
|
669
800
|
chart: { type: seriesType, height: this.sparklineHeight, margin: [2, 0, 2, 0], backgroundColor: 'transparent' },
|
|
670
801
|
title: { text: undefined },
|
|
@@ -674,13 +805,7 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
674
805
|
legend: { enabled: false },
|
|
675
806
|
tooltip: { enabled: false },
|
|
676
807
|
plotOptions: {
|
|
677
|
-
[plotKey]:
|
|
678
|
-
marker: { enabled: this.sparklineMarkers },
|
|
679
|
-
lineWidth,
|
|
680
|
-
lineColor: brandColor,
|
|
681
|
-
...fillColor,
|
|
682
|
-
states: { hover: { lineWidth } },
|
|
683
|
-
},
|
|
808
|
+
[plotKey]: seriesPlotOptions,
|
|
684
809
|
},
|
|
685
810
|
series: [{ type: seriesType, data: this.sparkline }],
|
|
686
811
|
credits: { enabled: false },
|
|
@@ -692,17 +817,22 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
692
817
|
const trackColor = this.gaugeTrackColor || '#E5E7EB';
|
|
693
818
|
const innerRadius = this.gaugeInnerRadius || '80%';
|
|
694
819
|
const outerRadius = this.gaugeOuterRadius || '100%';
|
|
695
|
-
const
|
|
820
|
+
const maxLabelFontPx = (() => {
|
|
821
|
+
const raw = this.gaugeLabelFontSize;
|
|
822
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
823
|
+
return raw;
|
|
824
|
+
const m = String(raw).match(/^(\d+(?:\.\d+)?)/);
|
|
825
|
+
return m ? Number(m[1]) : 28;
|
|
826
|
+
})();
|
|
696
827
|
const labelColor = this.gaugeLabelColor || '#101828';
|
|
697
828
|
const labelFontWeight = this.gaugeLabelFontWeight ?? 600;
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
: this.gaugeValue;
|
|
829
|
+
const parsedValue = this.parseNumericValue(this.value);
|
|
830
|
+
const displayValue = parsedValue != null ? parsedValue : this.gaugeValue;
|
|
701
831
|
return {
|
|
702
832
|
chart: {
|
|
703
833
|
type: 'solidgauge',
|
|
704
|
-
height: this.gaugeHeight,
|
|
705
834
|
margin: [0, 0, 0, 0],
|
|
835
|
+
spacing: [0, 0, 0, 0],
|
|
706
836
|
backgroundColor: 'transparent',
|
|
707
837
|
},
|
|
708
838
|
title: { text: undefined },
|
|
@@ -735,13 +865,17 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
735
865
|
dataLabels: {
|
|
736
866
|
enabled: true,
|
|
737
867
|
borderWidth: 0,
|
|
738
|
-
y:
|
|
868
|
+
y: 0,
|
|
869
|
+
verticalAlign: 'middle',
|
|
739
870
|
useHTML: true,
|
|
740
871
|
formatter: (() => {
|
|
741
872
|
const self = this;
|
|
742
873
|
return function () {
|
|
743
874
|
const fmtResult = self.formatValue(this.y ?? 0);
|
|
744
|
-
|
|
875
|
+
const chart = this.series?.chart;
|
|
876
|
+
const size = chart ? Math.min(chart.plotWidth || chart.chartWidth || 160, chart.plotHeight || chart.chartHeight || 160) : 160;
|
|
877
|
+
const fontPx = Math.max(10, Math.min(maxLabelFontPx, Math.round(size * 0.18)));
|
|
878
|
+
return `<div style="text-align:center;line-height:1;"><span style="font-size:${fontPx}px;font-weight:${labelFontWeight};color:${labelColor}">${self.prefix}${fmtResult.display}${self.suffix}</span></div>`;
|
|
745
879
|
};
|
|
746
880
|
})(),
|
|
747
881
|
},
|
|
@@ -771,7 +905,11 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
771
905
|
this._hc = await getHighcharts();
|
|
772
906
|
this.destroySparkline();
|
|
773
907
|
this.sparklineChart = this._hc.chart(this.sparklineContainer, this.buildSparklineOptions());
|
|
774
|
-
|
|
908
|
+
// Force a reflow after the next layout pass
|
|
909
|
+
requestAnimationFrame(() => {
|
|
910
|
+
this.syncSparklineChartSize();
|
|
911
|
+
requestAnimationFrame(() => this.syncSparklineChartSize());
|
|
912
|
+
});
|
|
775
913
|
this.sparklineContainer.addEventListener('mousemove', this._onSparklineMouseMove);
|
|
776
914
|
this.sparklineContainer.addEventListener('mouseleave', this._onSparklineMouseLeave);
|
|
777
915
|
this.emit('nile-chart-ready', { chart: this.sparklineChart });
|
|
@@ -783,6 +921,13 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
783
921
|
this._hc = await getHighcharts();
|
|
784
922
|
this.destroyGauge();
|
|
785
923
|
this.gaugeChart = this._hc.chart(this.gaugeContainer, this.buildGaugeOptions());
|
|
924
|
+
// Force the chart to match the container's measured size
|
|
925
|
+
this._lastGaugeSize = { w: 0, h: 0 };
|
|
926
|
+
this.syncGaugeChartSize();
|
|
927
|
+
requestAnimationFrame(() => {
|
|
928
|
+
this.syncGaugeChartSize();
|
|
929
|
+
requestAnimationFrame(() => this.syncGaugeChartSize());
|
|
930
|
+
});
|
|
786
931
|
this.emit('nile-chart-ready', { chart: this.gaugeChart });
|
|
787
932
|
}
|
|
788
933
|
destroySparkline() {
|
|
@@ -811,8 +956,19 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
811
956
|
return undefined;
|
|
812
957
|
const dir = this.trendDirection;
|
|
813
958
|
const formatted = Math.abs(this.trendValue).toFixed(1) + '%';
|
|
959
|
+
const trendAriaLabel = `Trend ${dir === 'up' ? 'up' : dir === 'down' ? 'down' : 'neutral'} ${formatted}${this.trendLabel ? ' ' + this.trendLabel : ''}`.trim();
|
|
814
960
|
return html `
|
|
815
|
-
<span
|
|
961
|
+
<span
|
|
962
|
+
class="kpi-trend kpi-trend--${dir}"
|
|
963
|
+
tabindex="0"
|
|
964
|
+
role="button"
|
|
965
|
+
aria-label=${trendAriaLabel}
|
|
966
|
+
@mouseenter=${this._onTrendEnter}
|
|
967
|
+
@mouseleave=${this._onTipLeave}
|
|
968
|
+
@focus=${this._onTrendEnter}
|
|
969
|
+
@blur=${this._onTipLeave}
|
|
970
|
+
@keydown=${this._onTrendKeydown}
|
|
971
|
+
>
|
|
816
972
|
<span class="kpi-trend-arrow">
|
|
817
973
|
${dir === 'up'
|
|
818
974
|
? html `<svg viewBox="0 0 12 12"><path d="M6 2.5l4 5H2z"/></svg>`
|
|
@@ -820,7 +976,7 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
820
976
|
? html `<svg viewBox="0 0 12 12"><path d="M6 9.5l4-5H2z"/></svg>`
|
|
821
977
|
: html `<svg viewBox="0 0 12 12"><path d="M2 6.5h8" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`}
|
|
822
978
|
</span>
|
|
823
|
-
|
|
979
|
+
<span class="kpi-trend-value">${formatted}</span>${this.trendLabel ? html ` <span class="kpi-trend-label">${this.trendLabel}</span>` : nothing}
|
|
824
980
|
</span>
|
|
825
981
|
`;
|
|
826
982
|
}
|
|
@@ -837,24 +993,40 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
837
993
|
<div class="kpi ${isGauge ? 'kpi--gauge' : ''}">
|
|
838
994
|
${this.label ? html `<p
|
|
839
995
|
class="kpi-label"
|
|
996
|
+
tabindex="0"
|
|
840
997
|
@mouseenter=${this._onLabelEnter}
|
|
841
998
|
@mouseleave=${this._onTipLeave}
|
|
999
|
+
@focus=${this._onLabelEnter}
|
|
1000
|
+
@blur=${this._onTipLeave}
|
|
1001
|
+
@keydown=${this._onTrendKeydown}
|
|
842
1002
|
>${this.label}</p>` : nothing}
|
|
843
1003
|
|
|
844
1004
|
${isGauge ? html `
|
|
845
|
-
<div
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1005
|
+
<div class="kpi-gauge-slot">
|
|
1006
|
+
<div
|
|
1007
|
+
class="kpi-gauge-container"
|
|
1008
|
+
tabindex="0"
|
|
1009
|
+
role="img"
|
|
1010
|
+
aria-label=${this.getTooltipContent()}
|
|
1011
|
+
@mouseenter=${this._onGaugeEnter}
|
|
1012
|
+
@mouseleave=${this._onTipLeave}
|
|
1013
|
+
@focus=${this._onGaugeEnter}
|
|
1014
|
+
@blur=${this._onTipLeave}
|
|
1015
|
+
@keydown=${this._onTrendKeydown}
|
|
1016
|
+
></div>
|
|
1017
|
+
</div>
|
|
850
1018
|
` : nothing}
|
|
851
1019
|
|
|
852
1020
|
<div class="kpi-value-row">
|
|
853
1021
|
${!isGauge ? html `
|
|
854
1022
|
<h2
|
|
855
1023
|
class="kpi-value"
|
|
1024
|
+
tabindex=${showTooltip ? '0' : nothing}
|
|
856
1025
|
@mouseenter=${showTooltip ? this._onValueEnter : nothing}
|
|
857
1026
|
@mouseleave=${showTooltip ? this._onTipLeave : nothing}
|
|
1027
|
+
@focus=${showTooltip ? this._onValueEnter : nothing}
|
|
1028
|
+
@blur=${showTooltip ? this._onTipLeave : nothing}
|
|
1029
|
+
@keydown=${showTooltip ? this._onTrendKeydown : nothing}
|
|
858
1030
|
>
|
|
859
1031
|
${this.prefix ? html `<span class="kpi-prefix">${this.prefix}</span>` : nothing}${displayValue}${this.suffix ? html `<span class="kpi-suffix">${this.suffix}</span>` : nothing}
|
|
860
1032
|
</h2>
|
|
@@ -866,8 +1038,12 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
866
1038
|
|
|
867
1039
|
${this.description ? html `<p
|
|
868
1040
|
class="kpi-description"
|
|
1041
|
+
tabindex="0"
|
|
869
1042
|
@mouseenter=${this._onDescEnter}
|
|
870
1043
|
@mouseleave=${this._onTipLeave}
|
|
1044
|
+
@focus=${this._onDescEnter}
|
|
1045
|
+
@blur=${this._onTipLeave}
|
|
1046
|
+
@keydown=${this._onTrendKeydown}
|
|
871
1047
|
>${this.description}</p>` : nothing}
|
|
872
1048
|
|
|
873
1049
|
${this.sparkline.length && !isGauge ? html `<div class="kpi-sparkline"></div>` : nothing}
|
|
@@ -882,6 +1058,9 @@ __decorate([
|
|
|
882
1058
|
__decorate([
|
|
883
1059
|
query('.kpi-gauge-container')
|
|
884
1060
|
], NileKpiChart.prototype, "gaugeContainer", void 0);
|
|
1061
|
+
__decorate([
|
|
1062
|
+
query('.kpi-gauge-slot')
|
|
1063
|
+
], NileKpiChart.prototype, "gaugeSlot", void 0);
|
|
885
1064
|
__decorate([
|
|
886
1065
|
property({ type: Object })
|
|
887
1066
|
], NileKpiChart.prototype, "config", void 0);
|