@aquera/nile-visualization 2.9.6 → 2.9.7
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-config.d.ts +2 -0
- package/dist/src/nile-chart/nile-chart-skeleton.d.ts +2 -0
- package/dist/src/nile-chart/nile-chart-skeleton.js +188 -0
- package/dist/src/nile-chart/nile-chart.css.js +399 -36
- package/dist/src/nile-chart/nile-chart.js +3 -16
- package/dist/src/nile-kpi-chart/nile-kpi-chart.css.js +310 -17
- package/dist/src/nile-kpi-chart/nile-kpi-chart.d.ts +4 -2
- package/dist/src/nile-kpi-chart/nile-kpi-chart.js +145 -26
- package/package.json +1 -1
|
@@ -169,7 +169,6 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
169
169
|
this._onSparklineMouseLeave = () => {
|
|
170
170
|
this._hideTip();
|
|
171
171
|
};
|
|
172
|
-
// ── Value / Gauge hover handlers ─────────────────────────────────────────
|
|
173
172
|
this._onValueEnter = (e) => {
|
|
174
173
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
175
174
|
const rawNum = this.parseNumericValue(this.value);
|
|
@@ -196,6 +195,21 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
196
195
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
197
196
|
this._showTip(this.getTooltipContent(), rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
198
197
|
};
|
|
198
|
+
this._onTrendEnter = (e) => {
|
|
199
|
+
const el = e.currentTarget;
|
|
200
|
+
if (this.trendValue === null)
|
|
201
|
+
return;
|
|
202
|
+
const textEl = el.querySelector('.kpi-trend-text');
|
|
203
|
+
const overflowing = textEl
|
|
204
|
+
? textEl.scrollWidth > textEl.clientWidth
|
|
205
|
+
: el.scrollWidth > el.clientWidth;
|
|
206
|
+
if (!overflowing)
|
|
207
|
+
return;
|
|
208
|
+
const percent = Math.abs(this.trendValue).toFixed(1) + '%';
|
|
209
|
+
const text = this.trendLabel ? `${percent} ${this.trendLabel}` : percent;
|
|
210
|
+
const rect = el.getBoundingClientRect();
|
|
211
|
+
this._showTip(text, rect.left + rect.width / 2, rect.top);
|
|
212
|
+
};
|
|
199
213
|
this._onTipLeave = () => {
|
|
200
214
|
this._hideTip();
|
|
201
215
|
};
|
|
@@ -634,14 +648,32 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
634
648
|
if (!this.sparklineChart || !this.sparklineContainer)
|
|
635
649
|
return;
|
|
636
650
|
const rect = this.sparklineContainer.getBoundingClientRect();
|
|
637
|
-
const
|
|
638
|
-
|
|
651
|
+
const w = Math.round(rect.width);
|
|
652
|
+
const h = Math.round(rect.height);
|
|
653
|
+
if (w > 0 && h > 0) {
|
|
654
|
+
this.sparklineChart.setSize(w, h, false);
|
|
655
|
+
}
|
|
656
|
+
this.sparklineChart.reflow();
|
|
657
|
+
}
|
|
658
|
+
syncGaugeChartSize() {
|
|
659
|
+
if (!this.gaugeChart || !this.gaugeContainer)
|
|
660
|
+
return;
|
|
661
|
+
const rect = this.gaugeContainer.getBoundingClientRect();
|
|
662
|
+
const w = Math.max(72, Math.round(rect.width));
|
|
663
|
+
const h = Math.max(72, Math.round(rect.height));
|
|
664
|
+
const side = Math.min(w, h);
|
|
665
|
+
const scaledY = Math.round((this.gaugeLabelYOffset * side) / 160);
|
|
666
|
+
this.gaugeChart.update({
|
|
667
|
+
plotOptions: { solidgauge: { dataLabels: { y: scaledY } } },
|
|
668
|
+
}, false, false);
|
|
669
|
+
this.gaugeChart.setSize(w, h, false);
|
|
639
670
|
}
|
|
640
671
|
setupResizeObserver() {
|
|
641
672
|
this.resizeObserver = new ResizeObserver(() => {
|
|
642
673
|
this.syncSparklineChartSize();
|
|
643
|
-
this.
|
|
674
|
+
this.syncGaugeChartSize();
|
|
644
675
|
});
|
|
676
|
+
this.resizeObserver.observe(this);
|
|
645
677
|
if (this.sparklineContainer)
|
|
646
678
|
this.resizeObserver.observe(this.sparklineContainer);
|
|
647
679
|
if (this.gaugeContainer)
|
|
@@ -653,8 +685,8 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
653
685
|
const lineWidth = this.sparklineLineWidth;
|
|
654
686
|
const seriesType = this.sparklineType;
|
|
655
687
|
const topAlpha = Math.round(Math.min(1, Math.max(0, this.sparklineFillOpacity)) * 255).toString(16).padStart(2, '0');
|
|
656
|
-
const
|
|
657
|
-
const fillColor = seriesType === 'area'
|
|
688
|
+
const isPie = seriesType === 'pie';
|
|
689
|
+
const fillColor = seriesType === 'area' || seriesType === 'areaspline'
|
|
658
690
|
? {
|
|
659
691
|
fillColor: {
|
|
660
692
|
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
@@ -665,24 +697,70 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
665
697
|
},
|
|
666
698
|
}
|
|
667
699
|
: {};
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
700
|
+
let pieData = [];
|
|
701
|
+
if (isPie && this.sparkline.length) {
|
|
702
|
+
const n = this.sparkline.length;
|
|
703
|
+
const picks = [
|
|
704
|
+
this.sparkline[0],
|
|
705
|
+
this.sparkline[Math.floor(n / 3)],
|
|
706
|
+
this.sparkline[Math.floor((2 * n) / 3)],
|
|
707
|
+
this.sparkline[n - 1],
|
|
708
|
+
];
|
|
709
|
+
pieData = picks.map(y => ({ y }));
|
|
710
|
+
}
|
|
711
|
+
const plotOptions = isPie
|
|
712
|
+
? {
|
|
713
|
+
pie: {
|
|
714
|
+
dataLabels: { enabled: false, connectorWidth: 0 },
|
|
715
|
+
showInLegend: false,
|
|
716
|
+
borderWidth: 0,
|
|
717
|
+
borderRadius: 0,
|
|
718
|
+
size: '100%',
|
|
719
|
+
center: ['50%', '50%'],
|
|
720
|
+
slicedOffset: 0,
|
|
721
|
+
allowPointSelect: false,
|
|
722
|
+
states: { hover: { halo: null } },
|
|
723
|
+
colors: [
|
|
724
|
+
brandColor + 'ff',
|
|
725
|
+
brandColor + 'cc',
|
|
726
|
+
brandColor + '99',
|
|
727
|
+
brandColor + '66',
|
|
728
|
+
],
|
|
729
|
+
},
|
|
730
|
+
}
|
|
731
|
+
: {
|
|
732
|
+
[seriesType]: {
|
|
678
733
|
marker: { enabled: this.sparklineMarkers },
|
|
679
734
|
lineWidth,
|
|
680
735
|
lineColor: brandColor,
|
|
736
|
+
color: brandColor,
|
|
681
737
|
...fillColor,
|
|
682
738
|
states: { hover: { lineWidth } },
|
|
683
739
|
},
|
|
740
|
+
};
|
|
741
|
+
// Let the chart inherit the container's height by default. An explicit
|
|
742
|
+
// height in options would freeze the chart at that size until setSize() is
|
|
743
|
+
// called, which made the initial render appear as a thin sliver.
|
|
744
|
+
const initialHeight = this.sparklineContainer?.clientHeight || null;
|
|
745
|
+
const defaults = {
|
|
746
|
+
chart: {
|
|
747
|
+
type: seriesType,
|
|
748
|
+
height: initialHeight && initialHeight > 0 ? initialHeight : null,
|
|
749
|
+
margin: isPie ? [0, 0, 0, 0] : [2, 0, 2, 0],
|
|
750
|
+
backgroundColor: 'transparent',
|
|
684
751
|
},
|
|
685
|
-
|
|
752
|
+
title: { text: undefined },
|
|
753
|
+
subtitle: { text: undefined },
|
|
754
|
+
xAxis: { visible: false },
|
|
755
|
+
yAxis: { visible: false },
|
|
756
|
+
legend: { enabled: false },
|
|
757
|
+
tooltip: { enabled: false },
|
|
758
|
+
plotOptions,
|
|
759
|
+
series: [{
|
|
760
|
+
type: seriesType,
|
|
761
|
+
name: '',
|
|
762
|
+
data: isPie ? pieData : this.sparkline,
|
|
763
|
+
}],
|
|
686
764
|
credits: { enabled: false },
|
|
687
765
|
};
|
|
688
766
|
return { ...defaults, ...this.options };
|
|
@@ -701,7 +779,8 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
701
779
|
return {
|
|
702
780
|
chart: {
|
|
703
781
|
type: 'solidgauge',
|
|
704
|
-
height:
|
|
782
|
+
height: null,
|
|
783
|
+
width: null,
|
|
705
784
|
margin: [0, 0, 0, 0],
|
|
706
785
|
backgroundColor: 'transparent',
|
|
707
786
|
},
|
|
@@ -739,9 +818,18 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
739
818
|
useHTML: true,
|
|
740
819
|
formatter: (() => {
|
|
741
820
|
const self = this;
|
|
821
|
+
const basePx = (() => {
|
|
822
|
+
const m = /^(-?\d*\.?\d+)px$/.exec(labelFontSize);
|
|
823
|
+
return m ? parseFloat(m[1]) : 28;
|
|
824
|
+
})();
|
|
742
825
|
return function () {
|
|
743
826
|
const fmtResult = self.formatValue(this.y ?? 0);
|
|
744
|
-
|
|
827
|
+
const chart = this.series?.chart;
|
|
828
|
+
const side = chart
|
|
829
|
+
? Math.min(chart.plotWidth ?? 160, chart.plotHeight ?? 160)
|
|
830
|
+
: 160;
|
|
831
|
+
const scaledPx = Math.max(8, Math.round((basePx * side) / 160));
|
|
832
|
+
return `<div style="text-align:center"><span style="font-size:${scaledPx}px;font-weight:${labelFontWeight};color:${labelColor}">${self.prefix}${fmtResult.display}${self.suffix}</span></div>`;
|
|
745
833
|
};
|
|
746
834
|
})(),
|
|
747
835
|
},
|
|
@@ -771,7 +859,31 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
771
859
|
this._hc = await getHighcharts();
|
|
772
860
|
this.destroySparkline();
|
|
773
861
|
this.sparklineChart = this._hc.chart(this.sparklineContainer, this.buildSparklineOptions());
|
|
774
|
-
|
|
862
|
+
// Retry sync over several frames until the measured container size stabilises.
|
|
863
|
+
// Initial chart creation may run before parent flex layout settles, so the
|
|
864
|
+
// first measure can be too small; we keep syncing until we see two
|
|
865
|
+
// consecutive identical sizes.
|
|
866
|
+
let lastW = -1, lastH = -1, stable = 0, frames = 0;
|
|
867
|
+
const tick = () => {
|
|
868
|
+
if (!this.sparklineChart || !this.sparklineContainer)
|
|
869
|
+
return;
|
|
870
|
+
const rect = this.sparklineContainer.getBoundingClientRect();
|
|
871
|
+
const w = Math.round(rect.width);
|
|
872
|
+
const h = Math.round(rect.height);
|
|
873
|
+
if (w > 0 && h > 0)
|
|
874
|
+
this.sparklineChart.setSize(w, h, false);
|
|
875
|
+
this.sparklineChart.reflow();
|
|
876
|
+
if (w === lastW && h === lastH)
|
|
877
|
+
stable++;
|
|
878
|
+
else
|
|
879
|
+
stable = 0;
|
|
880
|
+
lastW = w;
|
|
881
|
+
lastH = h;
|
|
882
|
+
frames++;
|
|
883
|
+
if (stable < 2 && frames < 30)
|
|
884
|
+
requestAnimationFrame(tick);
|
|
885
|
+
};
|
|
886
|
+
requestAnimationFrame(tick);
|
|
775
887
|
this.sparklineContainer.addEventListener('mousemove', this._onSparklineMouseMove);
|
|
776
888
|
this.sparklineContainer.addEventListener('mouseleave', this._onSparklineMouseLeave);
|
|
777
889
|
this.emit('nile-chart-ready', { chart: this.sparklineChart });
|
|
@@ -783,6 +895,7 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
783
895
|
this._hc = await getHighcharts();
|
|
784
896
|
this.destroyGauge();
|
|
785
897
|
this.gaugeChart = this._hc.chart(this.gaugeContainer, this.buildGaugeOptions());
|
|
898
|
+
requestAnimationFrame(() => this.syncGaugeChartSize());
|
|
786
899
|
this.emit('nile-chart-ready', { chart: this.gaugeChart });
|
|
787
900
|
}
|
|
788
901
|
destroySparkline() {
|
|
@@ -811,8 +924,14 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
811
924
|
return undefined;
|
|
812
925
|
const dir = this.trendDirection;
|
|
813
926
|
const formatted = Math.abs(this.trendValue).toFixed(1) + '%';
|
|
927
|
+
const fullText = this.trendLabel ? `${formatted} ${this.trendLabel}` : formatted;
|
|
814
928
|
return html `
|
|
815
|
-
<span
|
|
929
|
+
<span
|
|
930
|
+
class="kpi-trend kpi-trend--${dir}"
|
|
931
|
+
data-full-text=${fullText}
|
|
932
|
+
@mouseenter=${this._onTrendEnter}
|
|
933
|
+
@mouseleave=${this._onTipLeave}
|
|
934
|
+
>
|
|
816
935
|
<span class="kpi-trend-arrow">
|
|
817
936
|
${dir === 'up'
|
|
818
937
|
? html `<svg viewBox="0 0 12 12"><path d="M6 2.5l4 5H2z"/></svg>`
|
|
@@ -820,7 +939,7 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
820
939
|
? html `<svg viewBox="0 0 12 12"><path d="M6 9.5l4-5H2z"/></svg>`
|
|
821
940
|
: html `<svg viewBox="0 0 12 12"><path d="M2 6.5h8" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>`}
|
|
822
941
|
</span>
|
|
823
|
-
|
|
942
|
+
<span class="kpi-trend-text">${formatted}${this.trendLabel ? html ` ${this.trendLabel}` : nothing}</span>
|
|
824
943
|
</span>
|
|
825
944
|
`;
|
|
826
945
|
}
|
|
@@ -832,9 +951,9 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
832
951
|
const rawNum = this.parseNumericValue(this.value);
|
|
833
952
|
const fmt = rawNum != null ? this.formatValue(rawNum) : null;
|
|
834
953
|
const displayValue = fmt ? fmt.display : String(this.value ?? '');
|
|
835
|
-
const
|
|
954
|
+
const showValueTooltip = !!fmt && fmt.display !== fmt.full;
|
|
836
955
|
return html `
|
|
837
|
-
<div class="kpi ${isGauge ? 'kpi--gauge' : ''}">
|
|
956
|
+
<div class="kpi ${isGauge ? 'kpi--gauge' : ''} ${!isGauge && (this.variant !== 'sparkline' || this.sparkline.length === 0) ? 'kpi--no-chart' : ''} ${!this.description ? 'kpi--no-desc' : ''} ${this.trendValue === null ? 'kpi--no-trend' : ''}">
|
|
838
957
|
${this.label ? html `<p
|
|
839
958
|
class="kpi-label"
|
|
840
959
|
@mouseenter=${this._onLabelEnter}
|
|
@@ -853,8 +972,8 @@ let NileKpiChart = class NileKpiChart extends NileElement {
|
|
|
853
972
|
${!isGauge ? html `
|
|
854
973
|
<h2
|
|
855
974
|
class="kpi-value"
|
|
856
|
-
@mouseenter=${
|
|
857
|
-
@mouseleave=${
|
|
975
|
+
@mouseenter=${showValueTooltip ? this._onValueEnter : nothing}
|
|
976
|
+
@mouseleave=${showValueTooltip ? this._onTipLeave : nothing}
|
|
858
977
|
>
|
|
859
978
|
${this.prefix ? html `<span class="kpi-prefix">${this.prefix}</span>` : nothing}${displayValue}${this.suffix ? html `<span class="kpi-suffix">${this.suffix}</span>` : nothing}
|
|
860
979
|
</h2>
|