@aquera/nile-visualization 0.6.0 → 0.8.0

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.
@@ -3,7 +3,18 @@ import { customElement, property, query } from 'lit/decorators.js';
3
3
  import { html, nothing } from 'lit';
4
4
  import NileElement from '../internal/nile-element.js';
5
5
  import { getHighcharts } from '../internal/highcharts-provider.js';
6
- import { styles } from './nile-kpi-chart.css.js';
6
+ import { styles, tooltipCss } from './nile-kpi-chart.css.js';
7
+ // Inject tooltip styles into document.head once per page load.
8
+ let _tooltipStylesInjected = false;
9
+ function ensureTooltipStyles() {
10
+ if (_tooltipStylesInjected || typeof document === 'undefined')
11
+ return;
12
+ const style = document.createElement('style');
13
+ style.dataset['nilekpichart'] = '';
14
+ style.textContent = tooltipCss;
15
+ document.head.appendChild(style);
16
+ _tooltipStylesInjected = true;
17
+ }
7
18
  let NileKpiChart = class NileKpiChart extends NileElement {
8
19
  constructor() {
9
20
  super(...arguments);
@@ -11,6 +22,8 @@ let NileKpiChart = class NileKpiChart extends NileElement {
11
22
  this.sparklineChart = null;
12
23
  this.gaugeChart = null;
13
24
  this.resizeObserver = null;
25
+ /** Tooltip element on document.body — outside shadow DOM, never clipped. */
26
+ this._tipEl = null;
14
27
  /** Full configuration: `{ chart, aq }` (same convention as other Nile charts). */
15
28
  this.config = null;
16
29
  /** Display variant: default (flat), card (bordered container), gauge (Highcharts solid gauge). */
@@ -47,8 +60,133 @@ let NileKpiChart = class NileKpiChart extends NileElement {
47
60
  this.loading = false;
48
61
  /** Highcharts options override for the sparkline or gauge. */
49
62
  this.options = {};
63
+ /**
64
+ * Set by nile-chart: skip host border/shadow (variant card/gauge) so the parent chart-card is the only frame.
65
+ */
66
+ this.embedInNileChart = false;
67
+ // ── Sparkline mousemove tooltip ──────────────────────────────────────────
68
+ this._onSparklineMouseMove = (e) => {
69
+ const chart = this.sparklineChart;
70
+ if (!chart || !this.sparklineContainer)
71
+ return;
72
+ const series = chart.series[0];
73
+ if (!series?.points?.length)
74
+ return;
75
+ const rect = this.sparklineContainer.getBoundingClientRect();
76
+ const mouseXInPlot = e.clientX - rect.left - (chart.plotLeft ?? 0);
77
+ // Snap to nearest point by plotX
78
+ let nearest = series.points[0];
79
+ let minDist = Infinity;
80
+ for (const p of series.points) {
81
+ const dist = Math.abs((p.plotX ?? 0) - mouseXInPlot);
82
+ if (dist < minDist) {
83
+ minDist = dist;
84
+ nearest = p;
85
+ }
86
+ }
87
+ const scale = this.inferSparklineTooltipScale();
88
+ const text = this.getTooltipContent((nearest.y ?? 0) * scale);
89
+ const tipX = rect.left + (chart.plotLeft ?? 0) + (nearest.plotX ?? 0);
90
+ const tipY = rect.top + (chart.plotTop ?? 0) + (nearest.plotY ?? 0);
91
+ this._showTip(text, tipX, tipY);
92
+ };
93
+ this._onSparklineMouseLeave = () => {
94
+ this._hideTip();
95
+ };
96
+ // ── Value / Gauge hover handlers ─────────────────────────────────────────
97
+ this._onValueEnter = (e) => {
98
+ const rect = e.currentTarget.getBoundingClientRect();
99
+ this._showTip(this.getTooltipContent(), rect.left + rect.width / 2, rect.top);
100
+ };
101
+ this._onGaugeEnter = (e) => {
102
+ const rect = e.currentTarget.getBoundingClientRect();
103
+ this._showTip(this.getTooltipContent(), rect.left + rect.width / 2, rect.top + rect.height / 2);
104
+ };
105
+ this._onTipLeave = () => {
106
+ this._hideTip();
107
+ };
108
+ }
109
+ // ── Tooltip ──────────────────────────────────────────────────────────────
110
+ _createTipEl() {
111
+ if (this._tipEl)
112
+ return;
113
+ const el = document.createElement('div');
114
+ el.className = 'nile-kpi-tooltip';
115
+ document.body.appendChild(el);
116
+ this._tipEl = el;
117
+ }
118
+ _showTip(text, x, y) {
119
+ if (!this._tipEl)
120
+ return;
121
+ this._tipEl.textContent = text;
122
+ this._tipEl.style.left = `${x}px`;
123
+ this._tipEl.style.top = `${y}px`;
124
+ this._tipEl.style.display = 'block';
125
+ }
126
+ _hideTip() {
127
+ if (this._tipEl)
128
+ this._tipEl.style.display = 'none';
129
+ }
130
+ // ── Formatting helpers ───────────────────────────────────────────────────
131
+ formatCssLength(value) {
132
+ if (value == null)
133
+ return null;
134
+ if (typeof value === 'number') {
135
+ return Number.isFinite(value) ? `${value}px` : null;
136
+ }
137
+ const s = String(value).trim();
138
+ if (!s)
139
+ return null;
140
+ if (/^-?\d*\.?\d+$/.test(s))
141
+ return `${s}px`;
142
+ return s;
143
+ }
144
+ parseNumericValue(v) {
145
+ if (typeof v === 'number')
146
+ return Number.isFinite(v) ? v : null;
147
+ if (typeof v !== 'string')
148
+ return null;
149
+ const cleaned = v.replace(/,/g, '').trim();
150
+ if (!cleaned)
151
+ return null;
152
+ const n = Number(cleaned);
153
+ return Number.isFinite(n) ? n : null;
154
+ }
155
+ formatTooltipNumber(n) {
156
+ const maxFractionDigits = Number.isInteger(n) ? 0 : 6;
157
+ return new Intl.NumberFormat(undefined, {
158
+ useGrouping: true,
159
+ minimumFractionDigits: 0,
160
+ maximumFractionDigits: maxFractionDigits,
161
+ }).format(n);
162
+ }
163
+ inferSparklineTooltipScale() {
164
+ if (!this.sparkline?.length)
165
+ return 1;
166
+ const main = this.parseNumericValue(this.value);
167
+ const last = this.sparkline[this.sparkline.length - 1];
168
+ if (main == null || !Number.isFinite(last) || last === 0)
169
+ return 1;
170
+ const ratio = Math.abs(main / last);
171
+ const candidates = [1000, 1000000];
172
+ for (const c of candidates) {
173
+ if (Math.abs(ratio - c) / c < 0.02)
174
+ return c;
175
+ }
176
+ return 1;
50
177
  }
51
- /** Apply `{ chart, aq }` to individual properties. */
178
+ getTooltipContent(overrideNumeric) {
179
+ const prefix = this.prefix ?? '';
180
+ const suffix = this.suffix ?? '';
181
+ const numeric = overrideNumeric ?? this.parseNumericValue(this.value) ??
182
+ (this.variant === 'gauge' ? this.gaugeValue : null) ??
183
+ (this.sparkline.length ? this.sparkline[this.sparkline.length - 1] : null);
184
+ const valueText = numeric == null
185
+ ? String(this.value ?? '').trim()
186
+ : this.formatTooltipNumber(numeric);
187
+ return `${prefix}${valueText}${suffix}`.trim();
188
+ }
189
+ // ── Config ───────────────────────────────────────────────────────────────
52
190
  applyConfig(cfg) {
53
191
  const { chart: c, aq } = cfg;
54
192
  if (c) {
@@ -86,11 +224,18 @@ let NileKpiChart = class NileKpiChart extends NileElement {
86
224
  this.loading = c.loading;
87
225
  if (c.options !== undefined)
88
226
  this.options = c.options;
89
- if (c.height !== undefined) {
90
- if (c.height)
91
- this.style.minHeight = c.height;
92
- else
227
+ if ('height' in c) {
228
+ const h = this.formatCssLength(c.height);
229
+ if (h) {
230
+ this.style.minHeight = h;
231
+ this.style.removeProperty('height');
232
+ this.style.removeProperty('max-height');
233
+ }
234
+ else {
93
235
  this.style.removeProperty('min-height');
236
+ this.style.removeProperty('height');
237
+ this.style.removeProperty('max-height');
238
+ }
94
239
  }
95
240
  }
96
241
  if (aq) {
@@ -100,13 +245,19 @@ let NileKpiChart = class NileKpiChart extends NileElement {
100
245
  this.description = aq.chartSubtitle;
101
246
  }
102
247
  }
248
+ // ── Lifecycle ────────────────────────────────────────────────────────────
103
249
  connectedCallback() {
104
250
  super.connectedCallback();
251
+ ensureTooltipStyles();
252
+ this._createTipEl();
105
253
  if (this.config)
106
254
  this.applyConfig(this.config);
107
255
  }
108
256
  disconnectedCallback() {
109
257
  super.disconnectedCallback();
258
+ this._hideTip();
259
+ this._tipEl?.remove();
260
+ this._tipEl = null;
110
261
  this.destroyCharts();
111
262
  this.resizeObserver?.disconnect();
112
263
  this.resizeObserver = null;
@@ -125,6 +276,7 @@ let NileKpiChart = class NileKpiChart extends NileElement {
125
276
  if (sparklineProps.some(p => changedProperties.has(p))) {
126
277
  if (this.sparklineChart) {
127
278
  this.sparklineChart.update(this.buildSparklineOptions(), true, true);
279
+ requestAnimationFrame(() => this.syncSparklineChartSize());
128
280
  }
129
281
  else {
130
282
  this.initSparkline();
@@ -140,9 +292,17 @@ let NileKpiChart = class NileKpiChart extends NileElement {
140
292
  }
141
293
  }
142
294
  }
295
+ // ── Chart sizing ─────────────────────────────────────────────────────────
296
+ syncSparklineChartSize() {
297
+ if (!this.sparklineChart || !this.sparklineContainer)
298
+ return;
299
+ const rect = this.sparklineContainer.getBoundingClientRect();
300
+ const h = Math.max(22, Math.round(rect.height));
301
+ this.sparklineChart.setSize(null, h, false);
302
+ }
143
303
  setupResizeObserver() {
144
304
  this.resizeObserver = new ResizeObserver(() => {
145
- this.sparklineChart?.reflow();
305
+ this.syncSparklineChartSize();
146
306
  this.gaugeChart?.reflow();
147
307
  });
148
308
  if (this.sparklineContainer)
@@ -150,6 +310,7 @@ let NileKpiChart = class NileKpiChart extends NileElement {
150
310
  if (this.gaugeContainer)
151
311
  this.resizeObserver.observe(this.gaugeContainer);
152
312
  }
313
+ // ── Chart options ────────────────────────────────────────────────────────
153
314
  buildSparklineOptions() {
154
315
  const brandColor = this.sparklineColor || '#005EA6';
155
316
  const defaults = {
@@ -245,6 +406,7 @@ let NileKpiChart = class NileKpiChart extends NileElement {
245
406
  ...this.options,
246
407
  };
247
408
  }
409
+ // ── Chart init/destroy ───────────────────────────────────────────────────
248
410
  async initSparkline() {
249
411
  if (!this.sparkline.length || !this.sparklineContainer)
250
412
  return;
@@ -252,6 +414,9 @@ let NileKpiChart = class NileKpiChart extends NileElement {
252
414
  this._hc = await getHighcharts();
253
415
  this.destroySparkline();
254
416
  this.sparklineChart = this._hc.chart(this.sparklineContainer, this.buildSparklineOptions());
417
+ requestAnimationFrame(() => this.syncSparklineChartSize());
418
+ this.sparklineContainer.addEventListener('mousemove', this._onSparklineMouseMove);
419
+ this.sparklineContainer.addEventListener('mouseleave', this._onSparklineMouseLeave);
255
420
  this.emit('nile-chart-ready', { chart: this.sparklineChart });
256
421
  }
257
422
  async initGauge() {
@@ -264,6 +429,10 @@ let NileKpiChart = class NileKpiChart extends NileElement {
264
429
  this.emit('nile-chart-ready', { chart: this.gaugeChart });
265
430
  }
266
431
  destroySparkline() {
432
+ if (this.sparklineContainer) {
433
+ this.sparklineContainer.removeEventListener('mousemove', this._onSparklineMouseMove);
434
+ this.sparklineContainer.removeEventListener('mouseleave', this._onSparklineMouseLeave);
435
+ }
267
436
  if (this.sparklineChart) {
268
437
  this.sparklineChart.destroy();
269
438
  this.sparklineChart = null;
@@ -279,6 +448,7 @@ let NileKpiChart = class NileKpiChart extends NileElement {
279
448
  this.destroySparkline();
280
449
  this.destroyGauge();
281
450
  }
451
+ // ── Render ───────────────────────────────────────────────────────────────
282
452
  renderTrend() {
283
453
  if (this.trendValue === null)
284
454
  return nothing;
@@ -297,18 +467,37 @@ let NileKpiChart = class NileKpiChart extends NileElement {
297
467
  </span>
298
468
  `;
299
469
  }
300
- renderContent() {
470
+ render() {
471
+ if (this.loading) {
472
+ return html `<div class="chart-loading">Loading...</div>`;
473
+ }
301
474
  const isGauge = this.variant === 'gauge';
302
475
  return html `
303
476
  <div class="kpi ${isGauge ? 'kpi--gauge' : ''}">
304
477
  ${this.label ? html `<p class="kpi-label">${this.label}</p>` : nothing}
305
478
 
306
- ${isGauge ? html `<div class="kpi-gauge-container"></div>` : nothing}
479
+ ${isGauge
480
+ ? html `
481
+ <div
482
+ class="kpi-gauge-container"
483
+ @mouseenter=${this._onGaugeEnter}
484
+ @mouseleave=${this._onTipLeave}
485
+ ></div>
486
+ `
487
+ : nothing}
307
488
 
308
489
  <div class="kpi-value-row">
309
- <h2 class="kpi-value">
310
- ${this.prefix ? html `<span class="kpi-prefix">${this.prefix}</span>` : nothing}${this.value}${this.suffix ? html `<span class="kpi-suffix">${this.suffix}</span>` : nothing}
311
- </h2>
490
+ ${!isGauge
491
+ ? html `
492
+ <h2
493
+ class="kpi-value"
494
+ @mouseenter=${this._onValueEnter}
495
+ @mouseleave=${this._onTipLeave}
496
+ >
497
+ ${this.prefix ? html `<span class="kpi-prefix">${this.prefix}</span>` : nothing}${this.value}${this.suffix ? html `<span class="kpi-suffix">${this.suffix}</span>` : nothing}
498
+ </h2>
499
+ `
500
+ : nothing}
312
501
  ${!isGauge ? this.renderTrend() : nothing}
313
502
  </div>
314
503
 
@@ -322,16 +511,6 @@ let NileKpiChart = class NileKpiChart extends NileElement {
322
511
  </div>
323
512
  `;
324
513
  }
325
- render() {
326
- if (this.loading) {
327
- return html `<div class="chart-loading">Loading...</div>`;
328
- }
329
- const useCard = this.variant === 'card' || this.variant === 'gauge';
330
- if (useCard) {
331
- return html `<div class="kpi-card">${this.renderContent()}</div>`;
332
- }
333
- return this.renderContent();
334
- }
335
514
  };
336
515
  NileKpiChart.styles = styles;
337
516
  __decorate([
@@ -394,6 +573,9 @@ __decorate([
394
573
  __decorate([
395
574
  property({ type: Object })
396
575
  ], NileKpiChart.prototype, "options", void 0);
576
+ __decorate([
577
+ property({ type: Boolean, reflect: true, attribute: 'embed-in-nile-chart' })
578
+ ], NileKpiChart.prototype, "embedInNileChart", void 0);
397
579
  NileKpiChart = __decorate([
398
580
  customElement('nile-kpi-chart')
399
581
  ], NileKpiChart);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquera/nile-visualization",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "A visualization Library for the Nile Design System",
5
5
  "license": "MIT",
6
6
  "author": "Aquera Inc",
@@ -42,7 +42,8 @@
42
42
  "./nile-kpi-chart": "./dist/src/nile-kpi-chart/index.js",
43
43
  "./nile-chart": "./dist/src/nile-chart/index.js",
44
44
  "./nile-widget-viewer": "./dist/src/nile-widget-viewer/index.js",
45
- "./nile-dashboard-viewer": "./dist/src/nile-dashboard-viewer/index.js"
45
+ "./nile-dashboard-viewer": "./dist/src/nile-dashboard-viewer/index.js",
46
+ "./nile-executive-summary": "./dist/src/nile-executive-summary/index.js"
46
47
  },
47
48
  "files": [
48
49
  "dist/src/**/*.js",