@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.
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/internal/types/nile-chart-config-input.type.d.ts +6 -2
- package/dist/src/nile-chart/nile-chart-config-builder.js +2 -1
- package/dist/src/nile-chart/nile-chart.css.js +118 -6
- package/dist/src/nile-chart/nile-chart.d.ts +12 -0
- package/dist/src/nile-chart/nile-chart.js +42 -5
- package/dist/src/nile-executive-summary/index.d.ts +2 -0
- package/dist/src/nile-executive-summary/index.js +2 -0
- package/dist/src/nile-executive-summary/nile-executive-summary-config.d.ts +39 -0
- package/dist/src/nile-executive-summary/nile-executive-summary-config.js +8 -0
- package/dist/src/nile-executive-summary/nile-executive-summary.css.d.ts +9 -0
- package/dist/src/nile-executive-summary/nile-executive-summary.css.js +196 -0
- package/dist/src/nile-executive-summary/nile-executive-summary.d.ts +101 -0
- package/dist/src/nile-executive-summary/nile-executive-summary.js +308 -0
- package/dist/src/nile-kpi-chart/nile-kpi-chart.css.d.ts +5 -0
- package/dist/src/nile-kpi-chart/nile-kpi-chart.css.js +52 -11
- package/dist/src/nile-kpi-chart/nile-kpi-chart.d.ts +22 -4
- package/dist/src/nile-kpi-chart/nile-kpi-chart.js +204 -22
- package/package.json +3 -2
|
@@ -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
|
-
|
|
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 (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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.
|
|
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",
|