@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.
@@ -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(1rem, 5cqi + 0.25rem, 36px);
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-type: inline-size;
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
- min-width: 0;
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: 0 1 48px;
233
- min-height: 22px;
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 — tighten padding, shrink prefix/suffix. */
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
- /* Very small: under ~180pxstack the trend below the value so neither clips. */
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-wrap: wrap;
276
- row-gap: var(--nile-spacing-xs, var(--ng-spacing-xs));
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
- flex-basis: 100%;
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-container {
453
+ .kpi-gauge-slot {
454
+ flex: 1 1 0;
292
455
  width: 100%;
293
- max-width: 160px;
294
- aspect-ratio: 1;
295
- flex: 0 1 160px;
296
- min-width: 72px;
297
- margin: 0 auto;
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
- // ── Chart sizing ─────────────────────────────────────────────────────────
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.sparklineChart.setSize(null, h, false);
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.gaugeChart?.reflow();
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 fillColor = seriesType === 'area'
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 labelFontSize = this.formatCssLength(this.gaugeLabelFontSize) ?? '28px';
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 displayValue = typeof this.value !== 'undefined' && this.value !== ''
699
- ? (typeof this.value === 'number' ? this.value : this.gaugeValue)
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: this.gaugeLabelYOffset,
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
- return `<div style="text-align:center"><span style="font-size:${labelFontSize};font-weight:${labelFontWeight};color:${labelColor}">${self.prefix}${fmtResult.display}${self.suffix}</span></div>`;
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
- requestAnimationFrame(() => this.syncSparklineChartSize());
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 class="kpi-trend kpi-trend--${dir}">
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
- ${formatted}${this.trendLabel ? html ` <span>${this.trendLabel}</span>` : nothing}
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
- class="kpi-gauge-container"
847
- @mouseenter=${this._onGaugeEnter}
848
- @mouseleave=${this._onTipLeave}
849
- ></div>
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquera/nile-visualization",
3
- "version": "2.9.10",
3
+ "version": "2.9.11",
4
4
  "description": "A visualization Library for the Nile Design System",
5
5
  "license": "MIT",
6
6
  "author": "Aquera Inc",