@aquera/nile-visualization 2.9.5 → 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.
@@ -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 h = Math.max(22, Math.round(rect.height));
638
- this.sparklineChart.setSize(null, h, false);
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.gaugeChart?.reflow();
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 plotKey = seriesType;
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
- const defaults = {
669
- chart: { type: seriesType, height: this.sparklineHeight, margin: [2, 0, 2, 0], backgroundColor: 'transparent' },
670
- title: { text: undefined },
671
- subtitle: { text: undefined },
672
- xAxis: { visible: false },
673
- yAxis: { visible: false },
674
- legend: { enabled: false },
675
- tooltip: { enabled: false },
676
- plotOptions: {
677
- [plotKey]: {
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
- series: [{ type: seriesType, data: this.sparkline }],
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: this.gaugeHeight,
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
- return `<div style="text-align:center"><span style="font-size:${labelFontSize};font-weight:${labelFontWeight};color:${labelColor}">${self.prefix}${fmtResult.display}${self.suffix}</span></div>`;
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
- requestAnimationFrame(() => this.syncSparklineChartSize());
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 class="kpi-trend kpi-trend--${dir}">
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
- ${formatted}${this.trendLabel ? html ` <span>${this.trendLabel}</span>` : nothing}
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 showTooltip = !!fmt && fmt.display !== fmt.full;
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=${showTooltip ? this._onValueEnter : nothing}
857
- @mouseleave=${showTooltip ? this._onTipLeave : nothing}
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquera/nile-visualization",
3
- "version": "2.9.5",
3
+ "version": "2.9.7",
4
4
  "description": "A visualization Library for the Nile Design System",
5
5
  "license": "MIT",
6
6
  "author": "Aquera Inc",