@joanroucoux/cairn-ui 0.3.0 → 0.5.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.
@@ -1,5 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, signal, input, inject, LOCALE_ID, booleanAttribute, computed, Component, output, ElementRef, DestroyRef, effect, contentChild, afterRenderEffect, forwardRef, Directive, contentChildren, model, viewChildren } from '@angular/core';
2
+ import { InjectionToken, signal, input, inject, LOCALE_ID, booleanAttribute, computed, Component, output, ElementRef, DestroyRef, effect, contentChild, afterRenderEffect, forwardRef, Directive, viewChild, afterNextRender, contentChildren, model, viewChildren } from '@angular/core';
3
+ import { pie, arc, curveMonotoneX, line } from 'd3-shape';
4
+ import { scaleLinear } from 'd3-scale';
3
5
 
4
6
  const MINUS = '−';
5
7
  const EM_DASH = '—';
@@ -594,6 +596,250 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
594
596
  `, styles: ["dialog{transition:opacity var(--duration-base) var(--ease-sheet),transform var(--duration-base) var(--ease-sheet),overlay var(--duration-base) allow-discrete,display var(--duration-base) allow-discrete;opacity:1;transform:translateY(0)}dialog:not([open]){opacity:0;transform:translateY(100%);transition-duration:var(--duration-exit)}@starting-style{dialog[open]{opacity:0;transform:translateY(100%)}}@media(min-width:64rem){dialog{transform:scale(1)}dialog:not([open]){transform:scale(var(--enter-scale))}@starting-style{dialog[open]{transform:scale(var(--enter-scale))}}}dialog::backdrop{transition:opacity var(--duration-base) var(--ease-out);opacity:1}dialog:not([open])::backdrop{opacity:0;transition-duration:var(--duration-exit)}@starting-style{dialog[open]::backdrop{opacity:0}}\n"] }]
595
597
  }], ctorParameters: () => [], propDecorators: { heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }] } });
596
598
 
599
+ const OUTER_RADIUS = 93;
600
+ const INNER_RADIUS = 63;
601
+ const pieLayout = pie()
602
+ .value((slice) => slice.value)
603
+ .sort(null);
604
+ const arcGenerator = arc().innerRadius(INNER_RADIUS).outerRadius(OUTER_RADIUS);
605
+ function positionSlices(slices) {
606
+ return pieLayout(slices).map((arc) => ({ ...arc.data, path: arcGenerator(arc) }));
607
+ }
608
+
609
+ const MAX_RAMP = 6;
610
+ const OTHERS_ID = 'others';
611
+ function shareSlices(slices) {
612
+ const positive = slices.filter((slice) => slice.value > 0).sort((a, b) => b.value - a.value);
613
+ const total = positive.reduce((sum, slice) => sum + slice.value, 0);
614
+ return positive.map((slice) => ({ ...slice, share: slice.value / total }));
615
+ }
616
+ function rankSlices(slices, othersLabel) {
617
+ const shared = shareSlices(slices);
618
+ const toRanked = (slice, ramp, members = []) => ({
619
+ ...slice,
620
+ ramp,
621
+ members,
622
+ });
623
+ if (shared.length <= MAX_RAMP) {
624
+ return shared.map((slice, index) => toRanked(slice, (index + 1)));
625
+ }
626
+ const kept = shared.slice(0, MAX_RAMP - 1);
627
+ const rest = shared.slice(MAX_RAMP - 1);
628
+ const othersValue = rest.reduce((sum, slice) => sum + slice.value, 0);
629
+ const othersShare = rest.reduce((sum, slice) => sum + slice.share, 0);
630
+ return [
631
+ ...kept.map((slice, index) => toRanked(slice, (index + 1))),
632
+ toRanked({ id: OTHERS_ID, label: othersLabel, value: othersValue, share: othersShare }, MAX_RAMP, rest.map((slice) => slice.label)),
633
+ ];
634
+ }
635
+
636
+ const identityValue$1 = (value) => `${value}`;
637
+ const identityShare = (share) => `${Math.round(share * 100)}%`;
638
+ const ACTIVE_SCALE = 1.08;
639
+ const ROW_CLASSES = 'flex w-full items-center gap-3 min-h-12 px-2 rounded-control text-left transition-colors duration-(--duration-fast) ease-out hover:bg-(--glow) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring)';
640
+ const RAMP_FILL_CLASSES = {
641
+ 1: 'fill-(--ramp-1)',
642
+ 2: 'fill-(--ramp-2)',
643
+ 3: 'fill-(--ramp-3)',
644
+ 4: 'fill-(--ramp-4)',
645
+ 5: 'fill-(--ramp-5)',
646
+ 6: 'fill-(--ramp-6)',
647
+ };
648
+ const RAMP_DOT_CLASSES = {
649
+ 1: 'bg-(--ramp-1)',
650
+ 2: 'bg-(--ramp-2)',
651
+ 3: 'bg-(--ramp-3)',
652
+ 4: 'bg-(--ramp-4)',
653
+ 5: 'bg-(--ramp-5)',
654
+ 6: 'bg-(--ramp-6)',
655
+ };
656
+ /**
657
+ * A ring chart with a legend: slices rank from largest to smallest on a grey ramp, and hovering,
658
+ * focusing or activating a legend row highlights its slice and centres it in the ring.
659
+ *
660
+ * @example
661
+ * <ui-donut [slices]="byAssetClass()" label="Par classe d'actif" othersLabel="Autres" [valueFormat]="formatEur" [shareFormat]="formatShare" (sliceSelect)="goToHoldings($event)" />
662
+ */
663
+ class UiDonut {
664
+ slices = input.required(/* @ts-ignore */
665
+ ...(ngDevMode ? [{ debugName: "slices" }] : /* istanbul ignore next */ []));
666
+ label = input.required(/* @ts-ignore */
667
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
668
+ othersLabel = input.required(/* @ts-ignore */
669
+ ...(ngDevMode ? [{ debugName: "othersLabel" }] : /* istanbul ignore next */ []));
670
+ valueFormat = input(identityValue$1, /* @ts-ignore */
671
+ ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
672
+ shareFormat = input(identityShare, /* @ts-ignore */
673
+ ...(ngDevMode ? [{ debugName: "shareFormat" }] : /* istanbul ignore next */ []));
674
+ sliceSelect = output();
675
+ activeId = signal(null, /* @ts-ignore */
676
+ ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
677
+ ranked = computed(() => rankSlices(this.slices(), this.othersLabel()), /* @ts-ignore */
678
+ ...(ngDevMode ? [{ debugName: "ranked" }] : /* istanbul ignore next */ []));
679
+ positioned = computed(() => positionSlices(this.ranked()), /* @ts-ignore */
680
+ ...(ngDevMode ? [{ debugName: "positioned" }] : /* istanbul ignore next */ []));
681
+ active = computed(() => {
682
+ const ranked = this.ranked();
683
+ const id = this.activeId();
684
+ return ranked.find((slice) => slice.id === id) ?? ranked[0] ?? null;
685
+ }, /* @ts-ignore */
686
+ ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
687
+ ariaLabel = computed(() => {
688
+ const detail = shareSlices(this.slices())
689
+ .map((slice) => `${slice.label} ${this.shareFormat()(slice.share)}`)
690
+ .join(', ');
691
+ return detail ? `${this.label()} : ${detail}` : this.label();
692
+ }, /* @ts-ignore */
693
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
694
+ pathClasses(slice) {
695
+ return `cursor-pointer stroke-(--card) ${RAMP_FILL_CLASSES[slice.ramp]}`;
696
+ }
697
+ dotClasses(slice) {
698
+ return `size-3 flex-none rounded-pill shadow-[inset_0_0_0_1px_var(--border)] ${RAMP_DOT_CLASSES[slice.ramp]}`;
699
+ }
700
+ rowClasses(slice) {
701
+ return `${ROW_CLASSES}${this.isActive(slice) ? ' bg-(--soft)' : ''}`;
702
+ }
703
+ subLabel(slice) {
704
+ return slice.members.length > 0 ? slice.members.join(', ') : (slice.sublabel ?? '');
705
+ }
706
+ arcTransform(slice) {
707
+ const scale = this.isActive(slice) ? ACTIVE_SCALE : 1;
708
+ return `translate(100px, 100px) scale(${scale})`;
709
+ }
710
+ isActive(slice) {
711
+ return this.active()?.id === slice.id;
712
+ }
713
+ highlight(id) {
714
+ this.activeId.set(id);
715
+ }
716
+ clearHighlight(id) {
717
+ if (this.activeId() === id) {
718
+ this.activeId.set(null);
719
+ }
720
+ }
721
+ select(slice) {
722
+ this.activeId.set(slice.id);
723
+ this.sliceSelect.emit(slice.id);
724
+ }
725
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDonut, deps: [], target: i0.ɵɵFactoryTarget.Component });
726
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiDonut, isStandalone: true, selector: "ui-donut", inputs: { slices: { classPropertyName: "slices", publicName: "slices", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, othersLabel: { classPropertyName: "othersLabel", publicName: "othersLabel", isSignal: true, isRequired: true, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, shareFormat: { classPropertyName: "shareFormat", publicName: "shareFormat", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sliceSelect: "sliceSelect" }, host: { classAttribute: "flex flex-col items-center gap-6 sm:flex-row sm:items-center" }, ngImport: i0, template: `
727
+ <div class="relative size-[232px] flex-none sm:size-60">
728
+ <svg class="block h-full w-full" role="img" viewBox="0 0 200 200" [attr.aria-label]="ariaLabel()">
729
+ @for (slice of positioned(); track slice.id) {
730
+ <g class="transition-transform duration-(--duration-fast) ease-out" [style.transform]="arcTransform(slice)">
731
+ <path
732
+ stroke-width="1"
733
+ [attr.d]="slice.path"
734
+ [class]="pathClasses(slice)"
735
+ (click)="select(slice)"
736
+ (pointerenter)="highlight(slice.id)"
737
+ (pointerleave)="clearHighlight(slice.id)"
738
+ />
739
+ </g>
740
+ }
741
+ </svg>
742
+
743
+ @if (active(); as slice) {
744
+ <div
745
+ class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-10 text-center"
746
+ data-testid="donut-centre"
747
+ >
748
+ <span class="text-caption max-w-full truncate text-(--muted-foreground)">{{ slice.label }}</span>
749
+ <span class="text-title font-semibold">{{ shareFormat()(slice.share) }}</span>
750
+ <span class="text-caption">{{ valueFormat()(slice.value) }}</span>
751
+ </div>
752
+ }
753
+ </div>
754
+
755
+ <div class="flex min-w-0 flex-1 flex-col">
756
+ @for (slice of ranked(); track slice.id) {
757
+ <button
758
+ type="button"
759
+ [class]="rowClasses(slice)"
760
+ (blur)="clearHighlight(slice.id)"
761
+ (click)="select(slice)"
762
+ (focus)="highlight(slice.id)"
763
+ (pointerenter)="highlight(slice.id)"
764
+ (pointerleave)="clearHighlight(slice.id)"
765
+ >
766
+ <span [class]="dotClasses(slice)"></span>
767
+ <span class="flex min-w-0 flex-1 flex-col">
768
+ <span class="text-body truncate font-medium">{{ slice.label }}</span>
769
+ <span class="text-caption truncate text-(--muted-foreground)">{{ subLabel(slice) }}</span>
770
+ </span>
771
+ <span class="flex flex-none flex-col items-end whitespace-nowrap">
772
+ <span class="text-body font-medium">{{ shareFormat()(slice.share) }}</span>
773
+ <span class="text-caption text-(--muted-foreground)">{{ valueFormat()(slice.value) }}</span>
774
+ </span>
775
+ </button>
776
+ }
777
+ </div>
778
+ `, isInline: true });
779
+ }
780
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiDonut, decorators: [{
781
+ type: Component,
782
+ args: [{
783
+ selector: 'ui-donut',
784
+ template: `
785
+ <div class="relative size-[232px] flex-none sm:size-60">
786
+ <svg class="block h-full w-full" role="img" viewBox="0 0 200 200" [attr.aria-label]="ariaLabel()">
787
+ @for (slice of positioned(); track slice.id) {
788
+ <g class="transition-transform duration-(--duration-fast) ease-out" [style.transform]="arcTransform(slice)">
789
+ <path
790
+ stroke-width="1"
791
+ [attr.d]="slice.path"
792
+ [class]="pathClasses(slice)"
793
+ (click)="select(slice)"
794
+ (pointerenter)="highlight(slice.id)"
795
+ (pointerleave)="clearHighlight(slice.id)"
796
+ />
797
+ </g>
798
+ }
799
+ </svg>
800
+
801
+ @if (active(); as slice) {
802
+ <div
803
+ class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center px-10 text-center"
804
+ data-testid="donut-centre"
805
+ >
806
+ <span class="text-caption max-w-full truncate text-(--muted-foreground)">{{ slice.label }}</span>
807
+ <span class="text-title font-semibold">{{ shareFormat()(slice.share) }}</span>
808
+ <span class="text-caption">{{ valueFormat()(slice.value) }}</span>
809
+ </div>
810
+ }
811
+ </div>
812
+
813
+ <div class="flex min-w-0 flex-1 flex-col">
814
+ @for (slice of ranked(); track slice.id) {
815
+ <button
816
+ type="button"
817
+ [class]="rowClasses(slice)"
818
+ (blur)="clearHighlight(slice.id)"
819
+ (click)="select(slice)"
820
+ (focus)="highlight(slice.id)"
821
+ (pointerenter)="highlight(slice.id)"
822
+ (pointerleave)="clearHighlight(slice.id)"
823
+ >
824
+ <span [class]="dotClasses(slice)"></span>
825
+ <span class="flex min-w-0 flex-1 flex-col">
826
+ <span class="text-body truncate font-medium">{{ slice.label }}</span>
827
+ <span class="text-caption truncate text-(--muted-foreground)">{{ subLabel(slice) }}</span>
828
+ </span>
829
+ <span class="flex flex-none flex-col items-end whitespace-nowrap">
830
+ <span class="text-body font-medium">{{ shareFormat()(slice.share) }}</span>
831
+ <span class="text-caption text-(--muted-foreground)">{{ valueFormat()(slice.value) }}</span>
832
+ </span>
833
+ </button>
834
+ }
835
+ </div>
836
+ `,
837
+ host: {
838
+ class: 'flex flex-col items-center gap-6 sm:flex-row sm:items-center',
839
+ },
840
+ }]
841
+ }], propDecorators: { slices: [{ type: i0.Input, args: [{ isSignal: true, alias: "slices", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], othersLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "othersLabel", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], shareFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "shareFormat", required: false }] }], sliceSelect: [{ type: i0.Output, args: ["sliceSelect"] }] } });
842
+
597
843
  const nextId$1 = (() => {
598
844
  let count = 0;
599
845
  return () => `ui-field-${++count}`;
@@ -801,6 +1047,665 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
801
1047
  }]
802
1048
  }], propDecorators: { errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
803
1049
 
1050
+ const axisTicks = (points, maxTicks = 5) => {
1051
+ if (points.length <= maxTicks) {
1052
+ return [...points];
1053
+ }
1054
+ const count = Math.max(3, Math.min(maxTicks, points.length));
1055
+ const step = (points.length - 1) / (count - 1);
1056
+ const indexes = new Set();
1057
+ for (let tick = 0; tick < count; tick++) {
1058
+ indexes.add(Math.round(tick * step));
1059
+ }
1060
+ return [...indexes].sort((a, b) => a - b).map((index) => points[index]);
1061
+ };
1062
+
1063
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
1064
+ const nearestPointIndex = (points, x) => {
1065
+ let nearest = 0;
1066
+ let nearestDistance = Infinity;
1067
+ points.forEach((point, index) => {
1068
+ const distance = Math.abs(point.x - x);
1069
+ if (distance < nearestDistance) {
1070
+ nearestDistance = distance;
1071
+ nearest = index;
1072
+ }
1073
+ });
1074
+ return nearest;
1075
+ };
1076
+ const navigateIndex = (key, current, last) => {
1077
+ switch (key) {
1078
+ case 'ArrowRight':
1079
+ return { index: clamp((current ?? -1) + 1, 0, last), preventDefault: true };
1080
+ case 'ArrowLeft':
1081
+ return { index: clamp((current ?? last + 1) - 1, 0, last), preventDefault: true };
1082
+ case 'Home':
1083
+ return { index: 0, preventDefault: true };
1084
+ case 'End':
1085
+ return { index: last, preventDefault: true };
1086
+ case 'Escape':
1087
+ return { index: null, preventDefault: false };
1088
+ default:
1089
+ return null;
1090
+ }
1091
+ };
1092
+
1093
+ const CURVE = curveMonotoneX;
1094
+ const buildGeometry = (points, width, height, padding = {}) => {
1095
+ if (points.length === 0) {
1096
+ return null;
1097
+ }
1098
+ const { x: paddingX = 4, top: paddingTop = 4, bottom: paddingBottom = 4 } = padding;
1099
+ const plotTop = paddingTop;
1100
+ const plotBottom = height - paddingBottom;
1101
+ const times = points.map((point) => point.t);
1102
+ const values = points.map((point) => point.v);
1103
+ const minValue = Math.min(...values);
1104
+ const maxValue = Math.max(...values);
1105
+ const x = scaleLinear()
1106
+ .domain([Math.min(...times), Math.max(...times)])
1107
+ .range([paddingX, width - paddingX]);
1108
+ const y = minValue === maxValue
1109
+ ? () => (plotTop + plotBottom) / 2
1110
+ : scaleLinear().domain([minValue, maxValue]).range([plotBottom, plotTop]);
1111
+ const toX = (point) => (points.length === 1 ? paddingX : x(point.t));
1112
+ const lineGenerator = line()
1113
+ .x(toX)
1114
+ .y((point) => y(point.v))
1115
+ .curve(CURVE);
1116
+ const first = points[0];
1117
+ const last = points.at(-1);
1118
+ return {
1119
+ line: lineGenerator(points),
1120
+ end: { x: toX(last), y: y(last.v) },
1121
+ start: { y: y(first.v) },
1122
+ points: points.map((point) => ({ ...point, x: toX(point), y: y(point.v) })),
1123
+ plotBottom,
1124
+ plotTop,
1125
+ };
1126
+ };
1127
+
1128
+ const LINE_GAP = 3;
1129
+ const COLLISION_MARGIN = 3;
1130
+ const EDGE_TOLERANCE = 3;
1131
+ const candidateSlots = (startY, box, label) => {
1132
+ const aboveTop = startY - LINE_GAP - label.height;
1133
+ const aboveBottom = startY - LINE_GAP;
1134
+ const belowTop = startY + LINE_GAP;
1135
+ const belowBottom = startY + LINE_GAP + label.height;
1136
+ const slots = [
1137
+ { anchor: 'end', x: box.width, y: aboveBottom, top: aboveTop, bottom: aboveBottom },
1138
+ { anchor: 'end', x: box.width, y: belowBottom, top: belowTop, bottom: belowBottom },
1139
+ { anchor: 'start', x: 0, y: aboveBottom, top: aboveTop, bottom: aboveBottom },
1140
+ { anchor: 'start', x: 0, y: belowBottom, top: belowTop, bottom: belowBottom },
1141
+ { anchor: 'end', x: box.width, y: label.height, top: 0, bottom: label.height },
1142
+ { anchor: 'start', x: 0, y: label.height, top: 0, bottom: label.height },
1143
+ { anchor: 'end', x: box.width, y: box.height, top: box.height - label.height, bottom: box.height },
1144
+ { anchor: 'start', x: 0, y: box.height, top: box.height - label.height, bottom: box.height },
1145
+ ];
1146
+ return slots.filter((slot) => slot.top >= -EDGE_TOLERANCE && slot.bottom <= box.height + EDGE_TOLERANCE);
1147
+ };
1148
+ const hitsFor = (slot, label, points) => {
1149
+ const left = slot.anchor === 'end' ? slot.x - label.width : slot.x;
1150
+ const right = slot.anchor === 'end' ? slot.x : slot.x + label.width;
1151
+ const top = slot.top - COLLISION_MARGIN;
1152
+ const bottom = slot.bottom + COLLISION_MARGIN;
1153
+ return points.filter((point) => point.x >= left && point.x <= right && point.y >= top && point.y <= bottom).length;
1154
+ };
1155
+ const placeStartLabel = (points, startY, box, label) => {
1156
+ let best = null;
1157
+ for (const slot of candidateSlots(startY, box, label)) {
1158
+ const hits = hitsFor(slot, label, points);
1159
+ if (!best || hits < best.hits) {
1160
+ best = { slot, hits };
1161
+ }
1162
+ if (hits === 0) {
1163
+ break;
1164
+ }
1165
+ }
1166
+ const { anchor, x, y } = best.slot;
1167
+ return { anchor, x, y };
1168
+ };
1169
+
1170
+ const TRANSITION_SAMPLES = 120;
1171
+ const TRANSITION_DURATION = 260;
1172
+ const easeOutQuint = (t) => 1 - (1 - t) ** 5;
1173
+ const prefersReducedMotion = () => typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
1174
+ const monotoneTangents = (xs, ys) => {
1175
+ const n = xs.length;
1176
+ const slopes = [];
1177
+ for (let i = 0; i < n - 1; i++) {
1178
+ const dx = xs[i + 1] - xs[i];
1179
+ slopes.push(dx === 0 ? 0 : (ys[i + 1] - ys[i]) / dx);
1180
+ }
1181
+ const tangents = new Array(n).fill(0);
1182
+ tangents[0] = slopes[0];
1183
+ tangents[n - 1] = slopes[n - 2];
1184
+ for (let i = 1; i < n - 1; i++) {
1185
+ const a = slopes[i - 1];
1186
+ const b = slopes[i];
1187
+ tangents[i] = a * b <= 0 ? 0 : (a + b) / 2;
1188
+ }
1189
+ for (let i = 0; i < n - 1; i++) {
1190
+ const slope = slopes[i];
1191
+ if (slope === 0) {
1192
+ tangents[i] = 0;
1193
+ tangents[i + 1] = 0;
1194
+ continue;
1195
+ }
1196
+ const a = tangents[i] / slope;
1197
+ const b = tangents[i + 1] / slope;
1198
+ const s = a * a + b * b;
1199
+ if (s > 9) {
1200
+ const t = 3 / Math.sqrt(s);
1201
+ tangents[i] = t * a * slope;
1202
+ tangents[i + 1] = t * b * slope;
1203
+ }
1204
+ }
1205
+ return tangents;
1206
+ };
1207
+ const sampleMonotone = (points, sampleXs) => {
1208
+ const xs = points.map((point) => point.x);
1209
+ const ys = points.map((point) => point.y);
1210
+ const tangents = monotoneTangents(xs, ys);
1211
+ let segment = 0;
1212
+ return sampleXs.map((x) => {
1213
+ while (segment < xs.length - 2 && x > xs[segment + 1]) {
1214
+ segment++;
1215
+ }
1216
+ const x0 = xs[segment];
1217
+ const x1 = xs[segment + 1];
1218
+ const dx = x1 - x0;
1219
+ const f = dx === 0 ? 0 : (x - x0) / dx;
1220
+ const f2 = f * f;
1221
+ const f3 = f2 * f;
1222
+ const y0 = ys[segment];
1223
+ const y1 = ys[segment + 1];
1224
+ const m0 = tangents[segment];
1225
+ const m1 = tangents[segment + 1];
1226
+ return (2 * f3 - 3 * f2 + 1) * y0 + (f3 - 2 * f2 + f) * m0 * dx + (-2 * f3 + 3 * f2) * y1 + (f3 - f2) * m1 * dx;
1227
+ });
1228
+ };
1229
+ const buildPolyline = (xs, ys) => xs.map((x, index) => `${index === 0 ? 'M' : 'L'}${x.toFixed(2)},${ys[index].toFixed(2)}`).join(' ');
1230
+ const interpolateLine = (from, to, progress) => {
1231
+ const left = to.points[0].x;
1232
+ const right = to.points.at(-1).x;
1233
+ const samples = Array.from({ length: TRANSITION_SAMPLES }, (_, index) => left + ((right - left) * index) / (TRANSITION_SAMPLES - 1));
1234
+ const fromYs = sampleMonotone(from.points, samples);
1235
+ const toYs = sampleMonotone(to.points, samples);
1236
+ return buildPolyline(samples, fromYs.map((fromY, index) => fromY + (toYs[index] - fromY) * progress));
1237
+ };
1238
+
1239
+ const DEFAULT_WIDTH = 640;
1240
+ const DEFAULT_HEIGHT = 240;
1241
+ const PLOT_PADDING = { x: 4, top: 10, bottom: 18 };
1242
+ const START_LABEL_HEIGHT = 17;
1243
+ const identityValue = (value) => `${value}`;
1244
+ const identityDelta = (delta) => `${delta > 0 ? '+' : ''}${delta}`;
1245
+ const identityTime = (time) => new Date(time).toISOString();
1246
+ /**
1247
+ * Value over time: a monotone curve with a dashed line at the starting value and a tooltip.
1248
+ *
1249
+ * @example
1250
+ * <ui-line-chart [points]="points" label="Net worth over one month" startLabel="Since" [valueFormat]="formatEur" />
1251
+ */
1252
+ class UiLineChart {
1253
+ points = input.required(/* @ts-ignore */
1254
+ ...(ngDevMode ? [{ debugName: "points" }] : /* istanbul ignore next */ []));
1255
+ label = input.required(/* @ts-ignore */
1256
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1257
+ valueFormat = input(identityValue, /* @ts-ignore */
1258
+ ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
1259
+ deltaFormat = input(identityDelta, /* @ts-ignore */
1260
+ ...(ngDevMode ? [{ debugName: "deltaFormat" }] : /* istanbul ignore next */ []));
1261
+ timeFormat = input(identityTime, /* @ts-ignore */
1262
+ ...(ngDevMode ? [{ debugName: "timeFormat" }] : /* istanbul ignore next */ []));
1263
+ axisFormat = input(identityTime, /* @ts-ignore */
1264
+ ...(ngDevMode ? [{ debugName: "axisFormat" }] : /* istanbul ignore next */ []));
1265
+ startLabel = input('', /* @ts-ignore */
1266
+ ...(ngDevMode ? [{ debugName: "startLabel" }] : /* istanbul ignore next */ []));
1267
+ timeColumnLabel = input('Time', /* @ts-ignore */
1268
+ ...(ngDevMode ? [{ debugName: "timeColumnLabel" }] : /* istanbul ignore next */ []));
1269
+ valueColumnLabel = input('Value', /* @ts-ignore */
1270
+ ...(ngDevMode ? [{ debugName: "valueColumnLabel" }] : /* istanbul ignore next */ []));
1271
+ #destroyRef = inject(DestroyRef);
1272
+ #frame = null;
1273
+ #hasRenderedOnce = false;
1274
+ #previousGeometry = null;
1275
+ svgRef = viewChild.required('svgRef', /* @ts-ignore */
1276
+ ...(ngDevMode ? [{ debugName: "svgRef" }] : /* istanbul ignore next */ []));
1277
+ activeIndex = signal(null, /* @ts-ignore */
1278
+ ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
1279
+ animatedLine = signal(null, /* @ts-ignore */
1280
+ ...(ngDevMode ? [{ debugName: "animatedLine" }] : /* istanbul ignore next */ []));
1281
+ measuredSize = signal(null, /* @ts-ignore */
1282
+ ...(ngDevMode ? [{ debugName: "measuredSize" }] : /* istanbul ignore next */ []));
1283
+ effectiveWidth = computed(() => this.measuredSize()?.width ?? DEFAULT_WIDTH, /* @ts-ignore */
1284
+ ...(ngDevMode ? [{ debugName: "effectiveWidth" }] : /* istanbul ignore next */ []));
1285
+ effectiveHeight = computed(() => this.measuredSize()?.height ?? DEFAULT_HEIGHT, /* @ts-ignore */
1286
+ ...(ngDevMode ? [{ debugName: "effectiveHeight" }] : /* istanbul ignore next */ []));
1287
+ geometry = computed(() => buildGeometry(this.points(), this.effectiveWidth(), this.effectiveHeight(), PLOT_PADDING), /* @ts-ignore */
1288
+ ...(ngDevMode ? [{ debugName: "geometry" }] : /* istanbul ignore next */ []));
1289
+ displayLine = computed(() => this.animatedLine() ?? this.geometry().line, /* @ts-ignore */
1290
+ ...(ngDevMode ? [{ debugName: "displayLine" }] : /* istanbul ignore next */ []));
1291
+ ticks = computed(() => axisTicks(this.geometry().points), /* @ts-ignore */
1292
+ ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
1293
+ active = computed(() => {
1294
+ const geometry = this.geometry();
1295
+ const index = this.activeIndex();
1296
+ return geometry && index !== null ? geometry.points[index] : null;
1297
+ }, /* @ts-ignore */
1298
+ ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
1299
+ startLabelInfo = computed(() => {
1300
+ const text = this.startLabel();
1301
+ const geometry = this.geometry();
1302
+ if (!text || !geometry) {
1303
+ return null;
1304
+ }
1305
+ const label = `${text} ${this.valueFormat()(geometry.points[0].v)}`;
1306
+ const size = { width: label.length * 0.56 * 12 + 8, height: START_LABEL_HEIGHT };
1307
+ const placement = placeStartLabel(geometry.points, geometry.start.y, { width: this.effectiveWidth(), height: this.effectiveHeight() }, size);
1308
+ return { label, placement };
1309
+ }, /* @ts-ignore */
1310
+ ...(ngDevMode ? [{ debugName: "startLabelInfo" }] : /* istanbul ignore next */ []));
1311
+ #renderRef = afterNextRender(() => this.#observeSize());
1312
+ constructor() {
1313
+ effect(() => {
1314
+ const geometry = this.geometry();
1315
+ const previous = this.#previousGeometry;
1316
+ if (previous === geometry) {
1317
+ return;
1318
+ }
1319
+ this.#previousGeometry = geometry;
1320
+ this.#cancelAnimation();
1321
+ if (!this.#hasRenderedOnce) {
1322
+ this.#hasRenderedOnce = true;
1323
+ return;
1324
+ }
1325
+ if (!previous || !geometry) {
1326
+ this.animatedLine.set(null);
1327
+ return;
1328
+ }
1329
+ if (prefersReducedMotion()) {
1330
+ this.animatedLine.set(null);
1331
+ return;
1332
+ }
1333
+ this.#animate(previous, geometry);
1334
+ });
1335
+ this.#destroyRef.onDestroy(() => this.#cancelAnimation());
1336
+ }
1337
+ #observeSize() {
1338
+ this.#destroyRef.onDestroy(() => this.#renderRef.destroy());
1339
+ if (typeof ResizeObserver === 'undefined') {
1340
+ return;
1341
+ }
1342
+ const element = this.svgRef().nativeElement;
1343
+ const observer = new ResizeObserver(([entry]) => {
1344
+ if (entry) {
1345
+ const { width, height } = entry.contentRect;
1346
+ this.measuredSize.set({ width, height });
1347
+ }
1348
+ });
1349
+ observer.observe(element);
1350
+ this.#destroyRef.onDestroy(() => observer.disconnect());
1351
+ }
1352
+ #cancelAnimation() {
1353
+ if (this.#frame !== null) {
1354
+ cancelAnimationFrame(this.#frame);
1355
+ this.#frame = null;
1356
+ }
1357
+ }
1358
+ #animate(from, to) {
1359
+ if (from.points.length < 2 || to.points.length < 2) {
1360
+ this.animatedLine.set(null);
1361
+ return;
1362
+ }
1363
+ const startTime = performance.now();
1364
+ const step = (now) => {
1365
+ const t = Math.min(1, (now - startTime) / TRANSITION_DURATION);
1366
+ this.animatedLine.set(interpolateLine(from, to, easeOutQuint(t)));
1367
+ if (t < 1) {
1368
+ this.#frame = requestAnimationFrame(step);
1369
+ }
1370
+ else {
1371
+ this.#frame = null;
1372
+ this.animatedLine.set(null);
1373
+ }
1374
+ };
1375
+ this.#frame = requestAnimationFrame(step);
1376
+ }
1377
+ coreTick(index, count) {
1378
+ if (count <= 3) {
1379
+ return true;
1380
+ }
1381
+ const middle = Math.floor((count - 1) / 2);
1382
+ return index === 0 || index === count - 1 || index === middle;
1383
+ }
1384
+ deltaFor(point) {
1385
+ return point.v - this.points()[0].v;
1386
+ }
1387
+ deltaClasses(point) {
1388
+ const delta = this.deltaFor(point);
1389
+ return delta > 0 ? 'text-(--positive)' : delta < 0 ? 'text-(--negative)' : 'text-(--muted-foreground)';
1390
+ }
1391
+ tooltipStyle(point) {
1392
+ return `left: ${(point.x / this.effectiveWidth()) * 100}%; top: ${(point.y / this.effectiveHeight()) * 100}%`;
1393
+ }
1394
+ onPointerMove(event) {
1395
+ const geometry = this.geometry();
1396
+ if (!geometry || geometry.points.length === 0) {
1397
+ return;
1398
+ }
1399
+ const rect = this.svgRef().nativeElement.getBoundingClientRect();
1400
+ const ratio = rect.width === 0 ? 1 : this.effectiveWidth() / rect.width;
1401
+ const x = (event.clientX - rect.left) * ratio;
1402
+ this.activeIndex.set(nearestPointIndex(geometry.points, x));
1403
+ }
1404
+ onPointerLeave() {
1405
+ this.activeIndex.set(null);
1406
+ }
1407
+ onKeydown(event) {
1408
+ const geometry = this.geometry();
1409
+ if (!geometry || geometry.points.length === 0) {
1410
+ return;
1411
+ }
1412
+ const navigation = navigateIndex(event.key, this.activeIndex(), geometry.points.length - 1);
1413
+ if (!navigation) {
1414
+ return;
1415
+ }
1416
+ if (navigation.preventDefault) {
1417
+ event.preventDefault();
1418
+ }
1419
+ this.activeIndex.set(navigation.index);
1420
+ }
1421
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiLineChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
1422
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiLineChart, isStandalone: true, selector: "ui-line-chart", inputs: { points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, deltaFormat: { classPropertyName: "deltaFormat", publicName: "deltaFormat", isSignal: true, isRequired: false, transformFunction: null }, timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, axisFormat: { classPropertyName: "axisFormat", publicName: "axisFormat", isSignal: true, isRequired: false, transformFunction: null }, startLabel: { classPropertyName: "startLabel", publicName: "startLabel", isSignal: true, isRequired: false, transformFunction: null }, timeColumnLabel: { classPropertyName: "timeColumnLabel", publicName: "timeColumnLabel", isSignal: true, isRequired: false, transformFunction: null }, valueColumnLabel: { classPropertyName: "valueColumnLabel", publicName: "valueColumnLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block h-full" }, viewQueries: [{ propertyName: "svgRef", first: true, predicate: ["svgRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
1423
+ <div class="relative h-full">
1424
+ <svg
1425
+ #svgRef
1426
+ class="block h-full w-full touch-pan-y"
1427
+ role="img"
1428
+ tabindex="0"
1429
+ [attr.aria-label]="label()"
1430
+ [attr.height]="effectiveHeight()"
1431
+ [attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
1432
+ [attr.width]="effectiveWidth()"
1433
+ (keydown)="onKeydown($event)"
1434
+ (pointerleave)="onPointerLeave()"
1435
+ (pointermove)="onPointerMove($event)"
1436
+ >
1437
+ @if (geometry(); as geometry) {
1438
+ <path
1439
+ class="stroke-(--foreground)"
1440
+ data-chart-line
1441
+ fill="none"
1442
+ stroke-linecap="round"
1443
+ stroke-linejoin="round"
1444
+ stroke-width="2"
1445
+ [attr.d]="displayLine()"
1446
+ />
1447
+
1448
+ <line
1449
+ class="stroke-(--border)"
1450
+ data-chart-start-line
1451
+ stroke-dasharray="4 4"
1452
+ stroke-width="1"
1453
+ x1="0"
1454
+ [attr.x2]="effectiveWidth()"
1455
+ [attr.y1]="geometry.start.y"
1456
+ [attr.y2]="geometry.start.y"
1457
+ />
1458
+
1459
+ @if (startLabelInfo(); as info) {
1460
+ <text
1461
+ class="text-caption fill-(--subtle-foreground)"
1462
+ data-chart-start-label
1463
+ [attr.text-anchor]="info.placement.anchor"
1464
+ [attr.x]="info.placement.x"
1465
+ [attr.y]="info.placement.y"
1466
+ >
1467
+ {{ info.label }}
1468
+ </text>
1469
+ }
1470
+
1471
+ <circle
1472
+ class="fill-(--foreground) stroke-(--card)"
1473
+ data-chart-end
1474
+ r="4"
1475
+ stroke-width="2"
1476
+ [attr.cx]="geometry.end.x"
1477
+ [attr.cy]="geometry.end.y"
1478
+ />
1479
+
1480
+ @for (
1481
+ tick of ticks();
1482
+ track tick.t;
1483
+ let index = $index;
1484
+ let count = $count;
1485
+ let first = $first;
1486
+ let last = $last
1487
+ ) {
1488
+ <text
1489
+ class="text-caption fill-(--subtle-foreground)"
1490
+ data-chart-axis-tick
1491
+ [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
1492
+ [attr.x]="tick.x"
1493
+ [attr.y]="effectiveHeight() - 4"
1494
+ [class.max-sm:hidden]="!coreTick(index, count)"
1495
+ >
1496
+ {{ axisFormat()(tick.t) }}
1497
+ </text>
1498
+ }
1499
+
1500
+ @if (active(); as point) {
1501
+ <line
1502
+ class="stroke-(--border)"
1503
+ data-chart-crosshair
1504
+ stroke-width="1"
1505
+ y1="0"
1506
+ [attr.x1]="point.x"
1507
+ [attr.x2]="point.x"
1508
+ [attr.y2]="effectiveHeight()"
1509
+ />
1510
+ <circle
1511
+ class="fill-(--foreground) stroke-(--card)"
1512
+ data-chart-active
1513
+ r="4"
1514
+ stroke-width="2"
1515
+ [attr.cx]="point.x"
1516
+ [attr.cy]="point.y"
1517
+ />
1518
+ }
1519
+ }
1520
+ </svg>
1521
+
1522
+ @if (active(); as point) {
1523
+ <div
1524
+ class="rounded-container text-caption pointer-events-none absolute -translate-x-1/2 -translate-y-full bg-(--card) px-2.5 py-1.5 whitespace-nowrap shadow-[0_4px_16px_rgb(0_0_0/0.12)]"
1525
+ data-testid="chart-tooltip"
1526
+ role="status"
1527
+ [style]="tooltipStyle(point)"
1528
+ >
1529
+ <p class="font-medium text-(--foreground)">{{ valueFormat()(point.v) }}</p>
1530
+ <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
1531
+ <p class="text-(--subtle-foreground)">{{ timeFormat()(point.t) }}</p>
1532
+ </div>
1533
+ }
1534
+
1535
+ @if (points().length > 0) {
1536
+ <table class="sr-only">
1537
+ <caption>
1538
+ {{
1539
+ label()
1540
+ }}
1541
+ </caption>
1542
+ <thead>
1543
+ <tr>
1544
+ <th scope="col">{{ timeColumnLabel() }}</th>
1545
+ <th scope="col">{{ valueColumnLabel() }}</th>
1546
+ </tr>
1547
+ </thead>
1548
+ <tbody>
1549
+ @for (point of points(); track point.t) {
1550
+ <tr>
1551
+ <td>{{ timeFormat()(point.t) }}</td>
1552
+ <td>{{ valueFormat()(point.v) }}</td>
1553
+ </tr>
1554
+ }
1555
+ </tbody>
1556
+ </table>
1557
+ }
1558
+ </div>
1559
+ `, isInline: true });
1560
+ }
1561
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiLineChart, decorators: [{
1562
+ type: Component,
1563
+ args: [{
1564
+ selector: 'ui-line-chart',
1565
+ template: `
1566
+ <div class="relative h-full">
1567
+ <svg
1568
+ #svgRef
1569
+ class="block h-full w-full touch-pan-y"
1570
+ role="img"
1571
+ tabindex="0"
1572
+ [attr.aria-label]="label()"
1573
+ [attr.height]="effectiveHeight()"
1574
+ [attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
1575
+ [attr.width]="effectiveWidth()"
1576
+ (keydown)="onKeydown($event)"
1577
+ (pointerleave)="onPointerLeave()"
1578
+ (pointermove)="onPointerMove($event)"
1579
+ >
1580
+ @if (geometry(); as geometry) {
1581
+ <path
1582
+ class="stroke-(--foreground)"
1583
+ data-chart-line
1584
+ fill="none"
1585
+ stroke-linecap="round"
1586
+ stroke-linejoin="round"
1587
+ stroke-width="2"
1588
+ [attr.d]="displayLine()"
1589
+ />
1590
+
1591
+ <line
1592
+ class="stroke-(--border)"
1593
+ data-chart-start-line
1594
+ stroke-dasharray="4 4"
1595
+ stroke-width="1"
1596
+ x1="0"
1597
+ [attr.x2]="effectiveWidth()"
1598
+ [attr.y1]="geometry.start.y"
1599
+ [attr.y2]="geometry.start.y"
1600
+ />
1601
+
1602
+ @if (startLabelInfo(); as info) {
1603
+ <text
1604
+ class="text-caption fill-(--subtle-foreground)"
1605
+ data-chart-start-label
1606
+ [attr.text-anchor]="info.placement.anchor"
1607
+ [attr.x]="info.placement.x"
1608
+ [attr.y]="info.placement.y"
1609
+ >
1610
+ {{ info.label }}
1611
+ </text>
1612
+ }
1613
+
1614
+ <circle
1615
+ class="fill-(--foreground) stroke-(--card)"
1616
+ data-chart-end
1617
+ r="4"
1618
+ stroke-width="2"
1619
+ [attr.cx]="geometry.end.x"
1620
+ [attr.cy]="geometry.end.y"
1621
+ />
1622
+
1623
+ @for (
1624
+ tick of ticks();
1625
+ track tick.t;
1626
+ let index = $index;
1627
+ let count = $count;
1628
+ let first = $first;
1629
+ let last = $last
1630
+ ) {
1631
+ <text
1632
+ class="text-caption fill-(--subtle-foreground)"
1633
+ data-chart-axis-tick
1634
+ [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
1635
+ [attr.x]="tick.x"
1636
+ [attr.y]="effectiveHeight() - 4"
1637
+ [class.max-sm:hidden]="!coreTick(index, count)"
1638
+ >
1639
+ {{ axisFormat()(tick.t) }}
1640
+ </text>
1641
+ }
1642
+
1643
+ @if (active(); as point) {
1644
+ <line
1645
+ class="stroke-(--border)"
1646
+ data-chart-crosshair
1647
+ stroke-width="1"
1648
+ y1="0"
1649
+ [attr.x1]="point.x"
1650
+ [attr.x2]="point.x"
1651
+ [attr.y2]="effectiveHeight()"
1652
+ />
1653
+ <circle
1654
+ class="fill-(--foreground) stroke-(--card)"
1655
+ data-chart-active
1656
+ r="4"
1657
+ stroke-width="2"
1658
+ [attr.cx]="point.x"
1659
+ [attr.cy]="point.y"
1660
+ />
1661
+ }
1662
+ }
1663
+ </svg>
1664
+
1665
+ @if (active(); as point) {
1666
+ <div
1667
+ class="rounded-container text-caption pointer-events-none absolute -translate-x-1/2 -translate-y-full bg-(--card) px-2.5 py-1.5 whitespace-nowrap shadow-[0_4px_16px_rgb(0_0_0/0.12)]"
1668
+ data-testid="chart-tooltip"
1669
+ role="status"
1670
+ [style]="tooltipStyle(point)"
1671
+ >
1672
+ <p class="font-medium text-(--foreground)">{{ valueFormat()(point.v) }}</p>
1673
+ <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
1674
+ <p class="text-(--subtle-foreground)">{{ timeFormat()(point.t) }}</p>
1675
+ </div>
1676
+ }
1677
+
1678
+ @if (points().length > 0) {
1679
+ <table class="sr-only">
1680
+ <caption>
1681
+ {{
1682
+ label()
1683
+ }}
1684
+ </caption>
1685
+ <thead>
1686
+ <tr>
1687
+ <th scope="col">{{ timeColumnLabel() }}</th>
1688
+ <th scope="col">{{ valueColumnLabel() }}</th>
1689
+ </tr>
1690
+ </thead>
1691
+ <tbody>
1692
+ @for (point of points(); track point.t) {
1693
+ <tr>
1694
+ <td>{{ timeFormat()(point.t) }}</td>
1695
+ <td>{{ valueFormat()(point.v) }}</td>
1696
+ </tr>
1697
+ }
1698
+ </tbody>
1699
+ </table>
1700
+ }
1701
+ </div>
1702
+ `,
1703
+ host: {
1704
+ class: 'block h-full',
1705
+ },
1706
+ }]
1707
+ }], ctorParameters: () => [], propDecorators: { points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], deltaFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "deltaFormat", required: false }] }], timeFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeFormat", required: false }] }], axisFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "axisFormat", required: false }] }], startLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "startLabel", required: false }] }], timeColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeColumnLabel", required: false }] }], valueColumnLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueColumnLabel", required: false }] }], svgRef: [{ type: i0.ViewChild, args: ['svgRef', { isSignal: true }] }] } });
1708
+
804
1709
  const MENU_CLASSES = 'm-0 min-w-44 p-1 rounded-container bg-(--elevated) shadow-[0_8px_24px_rgb(0_0_0/0.16)] text-(--foreground) transition-[opacity,transform,overlay,display] transition-discrete duration-(--duration-fast) ease-out starting:open:opacity-0 starting:open:scale-(--enter-scale) opacity-0 scale-(--enter-scale) open:opacity-100 open:scale-100';
805
1710
  const ITEM_CLASSES = 'flex w-full items-center gap-2 min-h-(--row-min) px-3 rounded-control text-label hover:bg-(--soft) focus-visible:bg-(--soft) outline-none';
806
1711
  const nextId = (() => {
@@ -1619,5 +2524,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1619
2524
  * Generated bundle index. Do not edit.
1620
2525
  */
1621
2526
 
1622
- export { AMOUNT_MASKINGS, AMOUNT_NUMERICS, ASYNC_STATES, AVATAR_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, DELTA_EMPHASES, DIALOG_WIDTHS, METER_TONES, STAT_SIZES, UI_AMOUNT_MASKED, UiAmount, UiAsync, UiAvatar, UiBadge, UiButton, UiCard, UiDelta, UiDialog, UiField, UiInput, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiSegmented, UiSelect, UiSkeleton, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiTr, formatAmount };
2527
+ export { AMOUNT_MASKINGS, AMOUNT_NUMERICS, ASYNC_STATES, AVATAR_SIZES, BADGE_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, CARD_PADDINGS, CARD_VARIANTS, CELL_BREAKPOINTS, CONTROL_SIZES, DELTA_EMPHASES, DIALOG_WIDTHS, METER_TONES, STAT_SIZES, UI_AMOUNT_MASKED, UiAmount, UiAsync, UiAvatar, UiBadge, UiButton, UiCard, UiDelta, UiDialog, UiDonut, UiField, UiInput, UiLineChart, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiSegmented, UiSelect, UiSkeleton, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiTr, formatAmount };
1623
2528
  //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map