@joanroucoux/cairn-ui 0.3.0 → 0.4.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 { scaleLinear } from 'd3-scale';
4
+ import { curveMonotoneX, line } from 'd3-shape';
3
5
 
4
6
  const MINUS = '−';
5
7
  const EM_DASH = '—';
@@ -801,6 +803,665 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
801
803
  }]
802
804
  }], propDecorators: { errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
803
805
 
806
+ const axisTicks = (points, maxTicks = 5) => {
807
+ if (points.length <= maxTicks) {
808
+ return [...points];
809
+ }
810
+ const count = Math.max(3, Math.min(maxTicks, points.length));
811
+ const step = (points.length - 1) / (count - 1);
812
+ const indexes = new Set();
813
+ for (let tick = 0; tick < count; tick++) {
814
+ indexes.add(Math.round(tick * step));
815
+ }
816
+ return [...indexes].sort((a, b) => a - b).map((index) => points[index]);
817
+ };
818
+
819
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
820
+ const nearestPointIndex = (points, x) => {
821
+ let nearest = 0;
822
+ let nearestDistance = Infinity;
823
+ points.forEach((point, index) => {
824
+ const distance = Math.abs(point.x - x);
825
+ if (distance < nearestDistance) {
826
+ nearestDistance = distance;
827
+ nearest = index;
828
+ }
829
+ });
830
+ return nearest;
831
+ };
832
+ const navigateIndex = (key, current, last) => {
833
+ switch (key) {
834
+ case 'ArrowRight':
835
+ return { index: clamp((current ?? -1) + 1, 0, last), preventDefault: true };
836
+ case 'ArrowLeft':
837
+ return { index: clamp((current ?? last + 1) - 1, 0, last), preventDefault: true };
838
+ case 'Home':
839
+ return { index: 0, preventDefault: true };
840
+ case 'End':
841
+ return { index: last, preventDefault: true };
842
+ case 'Escape':
843
+ return { index: null, preventDefault: false };
844
+ default:
845
+ return null;
846
+ }
847
+ };
848
+
849
+ const CURVE = curveMonotoneX;
850
+ const buildGeometry = (points, width, height, padding = {}) => {
851
+ if (points.length === 0) {
852
+ return null;
853
+ }
854
+ const { x: paddingX = 4, top: paddingTop = 4, bottom: paddingBottom = 4 } = padding;
855
+ const plotTop = paddingTop;
856
+ const plotBottom = height - paddingBottom;
857
+ const times = points.map((point) => point.t);
858
+ const values = points.map((point) => point.v);
859
+ const minValue = Math.min(...values);
860
+ const maxValue = Math.max(...values);
861
+ const x = scaleLinear()
862
+ .domain([Math.min(...times), Math.max(...times)])
863
+ .range([paddingX, width - paddingX]);
864
+ const y = minValue === maxValue
865
+ ? () => (plotTop + plotBottom) / 2
866
+ : scaleLinear().domain([minValue, maxValue]).range([plotBottom, plotTop]);
867
+ const toX = (point) => (points.length === 1 ? paddingX : x(point.t));
868
+ const lineGenerator = line()
869
+ .x(toX)
870
+ .y((point) => y(point.v))
871
+ .curve(CURVE);
872
+ const first = points[0];
873
+ const last = points.at(-1);
874
+ return {
875
+ line: lineGenerator(points),
876
+ end: { x: toX(last), y: y(last.v) },
877
+ start: { y: y(first.v) },
878
+ points: points.map((point) => ({ ...point, x: toX(point), y: y(point.v) })),
879
+ plotBottom,
880
+ plotTop,
881
+ };
882
+ };
883
+
884
+ const LINE_GAP = 3;
885
+ const COLLISION_MARGIN = 3;
886
+ const EDGE_TOLERANCE = 3;
887
+ const candidateSlots = (startY, box, label) => {
888
+ const aboveTop = startY - LINE_GAP - label.height;
889
+ const aboveBottom = startY - LINE_GAP;
890
+ const belowTop = startY + LINE_GAP;
891
+ const belowBottom = startY + LINE_GAP + label.height;
892
+ const slots = [
893
+ { anchor: 'end', x: box.width, y: aboveBottom, top: aboveTop, bottom: aboveBottom },
894
+ { anchor: 'end', x: box.width, y: belowBottom, top: belowTop, bottom: belowBottom },
895
+ { anchor: 'start', x: 0, y: aboveBottom, top: aboveTop, bottom: aboveBottom },
896
+ { anchor: 'start', x: 0, y: belowBottom, top: belowTop, bottom: belowBottom },
897
+ { anchor: 'end', x: box.width, y: label.height, top: 0, bottom: label.height },
898
+ { anchor: 'start', x: 0, y: label.height, top: 0, bottom: label.height },
899
+ { anchor: 'end', x: box.width, y: box.height, top: box.height - label.height, bottom: box.height },
900
+ { anchor: 'start', x: 0, y: box.height, top: box.height - label.height, bottom: box.height },
901
+ ];
902
+ return slots.filter((slot) => slot.top >= -EDGE_TOLERANCE && slot.bottom <= box.height + EDGE_TOLERANCE);
903
+ };
904
+ const hitsFor = (slot, label, points) => {
905
+ const left = slot.anchor === 'end' ? slot.x - label.width : slot.x;
906
+ const right = slot.anchor === 'end' ? slot.x : slot.x + label.width;
907
+ const top = slot.top - COLLISION_MARGIN;
908
+ const bottom = slot.bottom + COLLISION_MARGIN;
909
+ return points.filter((point) => point.x >= left && point.x <= right && point.y >= top && point.y <= bottom).length;
910
+ };
911
+ const placeStartLabel = (points, startY, box, label) => {
912
+ let best = null;
913
+ for (const slot of candidateSlots(startY, box, label)) {
914
+ const hits = hitsFor(slot, label, points);
915
+ if (!best || hits < best.hits) {
916
+ best = { slot, hits };
917
+ }
918
+ if (hits === 0) {
919
+ break;
920
+ }
921
+ }
922
+ const { anchor, x, y } = best.slot;
923
+ return { anchor, x, y };
924
+ };
925
+
926
+ const TRANSITION_SAMPLES = 120;
927
+ const TRANSITION_DURATION = 260;
928
+ const easeOutQuint = (t) => 1 - (1 - t) ** 5;
929
+ const prefersReducedMotion = () => typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;
930
+ const monotoneTangents = (xs, ys) => {
931
+ const n = xs.length;
932
+ const slopes = [];
933
+ for (let i = 0; i < n - 1; i++) {
934
+ const dx = xs[i + 1] - xs[i];
935
+ slopes.push(dx === 0 ? 0 : (ys[i + 1] - ys[i]) / dx);
936
+ }
937
+ const tangents = new Array(n).fill(0);
938
+ tangents[0] = slopes[0];
939
+ tangents[n - 1] = slopes[n - 2];
940
+ for (let i = 1; i < n - 1; i++) {
941
+ const a = slopes[i - 1];
942
+ const b = slopes[i];
943
+ tangents[i] = a * b <= 0 ? 0 : (a + b) / 2;
944
+ }
945
+ for (let i = 0; i < n - 1; i++) {
946
+ const slope = slopes[i];
947
+ if (slope === 0) {
948
+ tangents[i] = 0;
949
+ tangents[i + 1] = 0;
950
+ continue;
951
+ }
952
+ const a = tangents[i] / slope;
953
+ const b = tangents[i + 1] / slope;
954
+ const s = a * a + b * b;
955
+ if (s > 9) {
956
+ const t = 3 / Math.sqrt(s);
957
+ tangents[i] = t * a * slope;
958
+ tangents[i + 1] = t * b * slope;
959
+ }
960
+ }
961
+ return tangents;
962
+ };
963
+ const sampleMonotone = (points, sampleXs) => {
964
+ const xs = points.map((point) => point.x);
965
+ const ys = points.map((point) => point.y);
966
+ const tangents = monotoneTangents(xs, ys);
967
+ let segment = 0;
968
+ return sampleXs.map((x) => {
969
+ while (segment < xs.length - 2 && x > xs[segment + 1]) {
970
+ segment++;
971
+ }
972
+ const x0 = xs[segment];
973
+ const x1 = xs[segment + 1];
974
+ const dx = x1 - x0;
975
+ const f = dx === 0 ? 0 : (x - x0) / dx;
976
+ const f2 = f * f;
977
+ const f3 = f2 * f;
978
+ const y0 = ys[segment];
979
+ const y1 = ys[segment + 1];
980
+ const m0 = tangents[segment];
981
+ const m1 = tangents[segment + 1];
982
+ return (2 * f3 - 3 * f2 + 1) * y0 + (f3 - 2 * f2 + f) * m0 * dx + (-2 * f3 + 3 * f2) * y1 + (f3 - f2) * m1 * dx;
983
+ });
984
+ };
985
+ const buildPolyline = (xs, ys) => xs.map((x, index) => `${index === 0 ? 'M' : 'L'}${x.toFixed(2)},${ys[index].toFixed(2)}`).join(' ');
986
+ const interpolateLine = (from, to, progress) => {
987
+ const left = to.points[0].x;
988
+ const right = to.points.at(-1).x;
989
+ const samples = Array.from({ length: TRANSITION_SAMPLES }, (_, index) => left + ((right - left) * index) / (TRANSITION_SAMPLES - 1));
990
+ const fromYs = sampleMonotone(from.points, samples);
991
+ const toYs = sampleMonotone(to.points, samples);
992
+ return buildPolyline(samples, fromYs.map((fromY, index) => fromY + (toYs[index] - fromY) * progress));
993
+ };
994
+
995
+ const DEFAULT_WIDTH = 640;
996
+ const DEFAULT_HEIGHT = 240;
997
+ const PLOT_PADDING = { x: 4, top: 10, bottom: 18 };
998
+ const START_LABEL_HEIGHT = 17;
999
+ const identityValue = (value) => `${value}`;
1000
+ const identityDelta = (delta) => `${delta > 0 ? '+' : ''}${delta}`;
1001
+ const identityTime = (time) => new Date(time).toISOString();
1002
+ /**
1003
+ * Value over time: a monotone curve with a dashed line at the starting value and a tooltip.
1004
+ *
1005
+ * @example
1006
+ * <ui-line-chart [points]="points" label="Net worth over one month" startLabel="Since" [valueFormat]="formatEur" />
1007
+ */
1008
+ class UiLineChart {
1009
+ points = input.required(/* @ts-ignore */
1010
+ ...(ngDevMode ? [{ debugName: "points" }] : /* istanbul ignore next */ []));
1011
+ label = input.required(/* @ts-ignore */
1012
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1013
+ valueFormat = input(identityValue, /* @ts-ignore */
1014
+ ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
1015
+ deltaFormat = input(identityDelta, /* @ts-ignore */
1016
+ ...(ngDevMode ? [{ debugName: "deltaFormat" }] : /* istanbul ignore next */ []));
1017
+ timeFormat = input(identityTime, /* @ts-ignore */
1018
+ ...(ngDevMode ? [{ debugName: "timeFormat" }] : /* istanbul ignore next */ []));
1019
+ axisFormat = input(identityTime, /* @ts-ignore */
1020
+ ...(ngDevMode ? [{ debugName: "axisFormat" }] : /* istanbul ignore next */ []));
1021
+ startLabel = input('', /* @ts-ignore */
1022
+ ...(ngDevMode ? [{ debugName: "startLabel" }] : /* istanbul ignore next */ []));
1023
+ timeColumnLabel = input('Time', /* @ts-ignore */
1024
+ ...(ngDevMode ? [{ debugName: "timeColumnLabel" }] : /* istanbul ignore next */ []));
1025
+ valueColumnLabel = input('Value', /* @ts-ignore */
1026
+ ...(ngDevMode ? [{ debugName: "valueColumnLabel" }] : /* istanbul ignore next */ []));
1027
+ #destroyRef = inject(DestroyRef);
1028
+ #frame = null;
1029
+ #hasRenderedOnce = false;
1030
+ #previousGeometry = null;
1031
+ svgRef = viewChild.required('svgRef', /* @ts-ignore */
1032
+ ...(ngDevMode ? [{ debugName: "svgRef" }] : /* istanbul ignore next */ []));
1033
+ activeIndex = signal(null, /* @ts-ignore */
1034
+ ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
1035
+ animatedLine = signal(null, /* @ts-ignore */
1036
+ ...(ngDevMode ? [{ debugName: "animatedLine" }] : /* istanbul ignore next */ []));
1037
+ measuredSize = signal(null, /* @ts-ignore */
1038
+ ...(ngDevMode ? [{ debugName: "measuredSize" }] : /* istanbul ignore next */ []));
1039
+ effectiveWidth = computed(() => this.measuredSize()?.width ?? DEFAULT_WIDTH, /* @ts-ignore */
1040
+ ...(ngDevMode ? [{ debugName: "effectiveWidth" }] : /* istanbul ignore next */ []));
1041
+ effectiveHeight = computed(() => this.measuredSize()?.height ?? DEFAULT_HEIGHT, /* @ts-ignore */
1042
+ ...(ngDevMode ? [{ debugName: "effectiveHeight" }] : /* istanbul ignore next */ []));
1043
+ geometry = computed(() => buildGeometry(this.points(), this.effectiveWidth(), this.effectiveHeight(), PLOT_PADDING), /* @ts-ignore */
1044
+ ...(ngDevMode ? [{ debugName: "geometry" }] : /* istanbul ignore next */ []));
1045
+ displayLine = computed(() => this.animatedLine() ?? this.geometry().line, /* @ts-ignore */
1046
+ ...(ngDevMode ? [{ debugName: "displayLine" }] : /* istanbul ignore next */ []));
1047
+ ticks = computed(() => axisTicks(this.geometry().points), /* @ts-ignore */
1048
+ ...(ngDevMode ? [{ debugName: "ticks" }] : /* istanbul ignore next */ []));
1049
+ active = computed(() => {
1050
+ const geometry = this.geometry();
1051
+ const index = this.activeIndex();
1052
+ return geometry && index !== null ? geometry.points[index] : null;
1053
+ }, /* @ts-ignore */
1054
+ ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
1055
+ startLabelInfo = computed(() => {
1056
+ const text = this.startLabel();
1057
+ const geometry = this.geometry();
1058
+ if (!text || !geometry) {
1059
+ return null;
1060
+ }
1061
+ const label = `${text} ${this.valueFormat()(geometry.points[0].v)}`;
1062
+ const size = { width: label.length * 0.56 * 12 + 8, height: START_LABEL_HEIGHT };
1063
+ const placement = placeStartLabel(geometry.points, geometry.start.y, { width: this.effectiveWidth(), height: this.effectiveHeight() }, size);
1064
+ return { label, placement };
1065
+ }, /* @ts-ignore */
1066
+ ...(ngDevMode ? [{ debugName: "startLabelInfo" }] : /* istanbul ignore next */ []));
1067
+ #renderRef = afterNextRender(() => this.#observeSize());
1068
+ constructor() {
1069
+ effect(() => {
1070
+ const geometry = this.geometry();
1071
+ const previous = this.#previousGeometry;
1072
+ if (previous === geometry) {
1073
+ return;
1074
+ }
1075
+ this.#previousGeometry = geometry;
1076
+ this.#cancelAnimation();
1077
+ if (!this.#hasRenderedOnce) {
1078
+ this.#hasRenderedOnce = true;
1079
+ return;
1080
+ }
1081
+ if (!previous || !geometry) {
1082
+ this.animatedLine.set(null);
1083
+ return;
1084
+ }
1085
+ if (prefersReducedMotion()) {
1086
+ this.animatedLine.set(null);
1087
+ return;
1088
+ }
1089
+ this.#animate(previous, geometry);
1090
+ });
1091
+ this.#destroyRef.onDestroy(() => this.#cancelAnimation());
1092
+ }
1093
+ #observeSize() {
1094
+ this.#destroyRef.onDestroy(() => this.#renderRef.destroy());
1095
+ if (typeof ResizeObserver === 'undefined') {
1096
+ return;
1097
+ }
1098
+ const element = this.svgRef().nativeElement;
1099
+ const observer = new ResizeObserver(([entry]) => {
1100
+ if (entry) {
1101
+ const { width, height } = entry.contentRect;
1102
+ this.measuredSize.set({ width, height });
1103
+ }
1104
+ });
1105
+ observer.observe(element);
1106
+ this.#destroyRef.onDestroy(() => observer.disconnect());
1107
+ }
1108
+ #cancelAnimation() {
1109
+ if (this.#frame !== null) {
1110
+ cancelAnimationFrame(this.#frame);
1111
+ this.#frame = null;
1112
+ }
1113
+ }
1114
+ #animate(from, to) {
1115
+ if (from.points.length < 2 || to.points.length < 2) {
1116
+ this.animatedLine.set(null);
1117
+ return;
1118
+ }
1119
+ const startTime = performance.now();
1120
+ const step = (now) => {
1121
+ const t = Math.min(1, (now - startTime) / TRANSITION_DURATION);
1122
+ this.animatedLine.set(interpolateLine(from, to, easeOutQuint(t)));
1123
+ if (t < 1) {
1124
+ this.#frame = requestAnimationFrame(step);
1125
+ }
1126
+ else {
1127
+ this.#frame = null;
1128
+ this.animatedLine.set(null);
1129
+ }
1130
+ };
1131
+ this.#frame = requestAnimationFrame(step);
1132
+ }
1133
+ coreTick(index, count) {
1134
+ if (count <= 3) {
1135
+ return true;
1136
+ }
1137
+ const middle = Math.floor((count - 1) / 2);
1138
+ return index === 0 || index === count - 1 || index === middle;
1139
+ }
1140
+ deltaFor(point) {
1141
+ return point.v - this.points()[0].v;
1142
+ }
1143
+ deltaClasses(point) {
1144
+ const delta = this.deltaFor(point);
1145
+ return delta > 0 ? 'text-(--positive)' : delta < 0 ? 'text-(--negative)' : 'text-(--muted-foreground)';
1146
+ }
1147
+ tooltipStyle(point) {
1148
+ return `left: ${(point.x / this.effectiveWidth()) * 100}%; top: ${(point.y / this.effectiveHeight()) * 100}%`;
1149
+ }
1150
+ onPointerMove(event) {
1151
+ const geometry = this.geometry();
1152
+ if (!geometry || geometry.points.length === 0) {
1153
+ return;
1154
+ }
1155
+ const rect = this.svgRef().nativeElement.getBoundingClientRect();
1156
+ const ratio = rect.width === 0 ? 1 : this.effectiveWidth() / rect.width;
1157
+ const x = (event.clientX - rect.left) * ratio;
1158
+ this.activeIndex.set(nearestPointIndex(geometry.points, x));
1159
+ }
1160
+ onPointerLeave() {
1161
+ this.activeIndex.set(null);
1162
+ }
1163
+ onKeydown(event) {
1164
+ const geometry = this.geometry();
1165
+ if (!geometry || geometry.points.length === 0) {
1166
+ return;
1167
+ }
1168
+ const navigation = navigateIndex(event.key, this.activeIndex(), geometry.points.length - 1);
1169
+ if (!navigation) {
1170
+ return;
1171
+ }
1172
+ if (navigation.preventDefault) {
1173
+ event.preventDefault();
1174
+ }
1175
+ this.activeIndex.set(navigation.index);
1176
+ }
1177
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiLineChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
1178
+ 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: `
1179
+ <div class="relative h-full">
1180
+ <svg
1181
+ #svgRef
1182
+ class="block h-full w-full touch-pan-y"
1183
+ role="img"
1184
+ tabindex="0"
1185
+ [attr.aria-label]="label()"
1186
+ [attr.height]="effectiveHeight()"
1187
+ [attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
1188
+ [attr.width]="effectiveWidth()"
1189
+ (keydown)="onKeydown($event)"
1190
+ (pointerleave)="onPointerLeave()"
1191
+ (pointermove)="onPointerMove($event)"
1192
+ >
1193
+ @if (geometry(); as geometry) {
1194
+ <path
1195
+ class="stroke-(--foreground)"
1196
+ data-chart-line
1197
+ fill="none"
1198
+ stroke-linecap="round"
1199
+ stroke-linejoin="round"
1200
+ stroke-width="2"
1201
+ [attr.d]="displayLine()"
1202
+ />
1203
+
1204
+ <line
1205
+ class="stroke-(--border)"
1206
+ data-chart-start-line
1207
+ stroke-dasharray="4 4"
1208
+ stroke-width="1"
1209
+ x1="0"
1210
+ [attr.x2]="effectiveWidth()"
1211
+ [attr.y1]="geometry.start.y"
1212
+ [attr.y2]="geometry.start.y"
1213
+ />
1214
+
1215
+ @if (startLabelInfo(); as info) {
1216
+ <text
1217
+ class="text-caption fill-(--subtle-foreground)"
1218
+ data-chart-start-label
1219
+ [attr.text-anchor]="info.placement.anchor"
1220
+ [attr.x]="info.placement.x"
1221
+ [attr.y]="info.placement.y"
1222
+ >
1223
+ {{ info.label }}
1224
+ </text>
1225
+ }
1226
+
1227
+ <circle
1228
+ class="fill-(--foreground) stroke-(--card)"
1229
+ data-chart-end
1230
+ r="4"
1231
+ stroke-width="2"
1232
+ [attr.cx]="geometry.end.x"
1233
+ [attr.cy]="geometry.end.y"
1234
+ />
1235
+
1236
+ @for (
1237
+ tick of ticks();
1238
+ track tick.t;
1239
+ let index = $index;
1240
+ let count = $count;
1241
+ let first = $first;
1242
+ let last = $last
1243
+ ) {
1244
+ <text
1245
+ class="text-caption fill-(--subtle-foreground)"
1246
+ data-chart-axis-tick
1247
+ [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
1248
+ [attr.x]="tick.x"
1249
+ [attr.y]="effectiveHeight() - 4"
1250
+ [class.max-sm:hidden]="!coreTick(index, count)"
1251
+ >
1252
+ {{ axisFormat()(tick.t) }}
1253
+ </text>
1254
+ }
1255
+
1256
+ @if (active(); as point) {
1257
+ <line
1258
+ class="stroke-(--border)"
1259
+ data-chart-crosshair
1260
+ stroke-width="1"
1261
+ y1="0"
1262
+ [attr.x1]="point.x"
1263
+ [attr.x2]="point.x"
1264
+ [attr.y2]="effectiveHeight()"
1265
+ />
1266
+ <circle
1267
+ class="fill-(--foreground) stroke-(--card)"
1268
+ data-chart-active
1269
+ r="4"
1270
+ stroke-width="2"
1271
+ [attr.cx]="point.x"
1272
+ [attr.cy]="point.y"
1273
+ />
1274
+ }
1275
+ }
1276
+ </svg>
1277
+
1278
+ @if (active(); as point) {
1279
+ <div
1280
+ 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)]"
1281
+ data-testid="chart-tooltip"
1282
+ role="status"
1283
+ [style]="tooltipStyle(point)"
1284
+ >
1285
+ <p class="font-medium text-(--foreground)">{{ valueFormat()(point.v) }}</p>
1286
+ <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
1287
+ <p class="text-(--subtle-foreground)">{{ timeFormat()(point.t) }}</p>
1288
+ </div>
1289
+ }
1290
+
1291
+ @if (points().length > 0) {
1292
+ <table class="sr-only">
1293
+ <caption>
1294
+ {{
1295
+ label()
1296
+ }}
1297
+ </caption>
1298
+ <thead>
1299
+ <tr>
1300
+ <th scope="col">{{ timeColumnLabel() }}</th>
1301
+ <th scope="col">{{ valueColumnLabel() }}</th>
1302
+ </tr>
1303
+ </thead>
1304
+ <tbody>
1305
+ @for (point of points(); track point.t) {
1306
+ <tr>
1307
+ <td>{{ timeFormat()(point.t) }}</td>
1308
+ <td>{{ valueFormat()(point.v) }}</td>
1309
+ </tr>
1310
+ }
1311
+ </tbody>
1312
+ </table>
1313
+ }
1314
+ </div>
1315
+ `, isInline: true });
1316
+ }
1317
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiLineChart, decorators: [{
1318
+ type: Component,
1319
+ args: [{
1320
+ selector: 'ui-line-chart',
1321
+ template: `
1322
+ <div class="relative h-full">
1323
+ <svg
1324
+ #svgRef
1325
+ class="block h-full w-full touch-pan-y"
1326
+ role="img"
1327
+ tabindex="0"
1328
+ [attr.aria-label]="label()"
1329
+ [attr.height]="effectiveHeight()"
1330
+ [attr.viewBox]="'0 0 ' + effectiveWidth() + ' ' + effectiveHeight()"
1331
+ [attr.width]="effectiveWidth()"
1332
+ (keydown)="onKeydown($event)"
1333
+ (pointerleave)="onPointerLeave()"
1334
+ (pointermove)="onPointerMove($event)"
1335
+ >
1336
+ @if (geometry(); as geometry) {
1337
+ <path
1338
+ class="stroke-(--foreground)"
1339
+ data-chart-line
1340
+ fill="none"
1341
+ stroke-linecap="round"
1342
+ stroke-linejoin="round"
1343
+ stroke-width="2"
1344
+ [attr.d]="displayLine()"
1345
+ />
1346
+
1347
+ <line
1348
+ class="stroke-(--border)"
1349
+ data-chart-start-line
1350
+ stroke-dasharray="4 4"
1351
+ stroke-width="1"
1352
+ x1="0"
1353
+ [attr.x2]="effectiveWidth()"
1354
+ [attr.y1]="geometry.start.y"
1355
+ [attr.y2]="geometry.start.y"
1356
+ />
1357
+
1358
+ @if (startLabelInfo(); as info) {
1359
+ <text
1360
+ class="text-caption fill-(--subtle-foreground)"
1361
+ data-chart-start-label
1362
+ [attr.text-anchor]="info.placement.anchor"
1363
+ [attr.x]="info.placement.x"
1364
+ [attr.y]="info.placement.y"
1365
+ >
1366
+ {{ info.label }}
1367
+ </text>
1368
+ }
1369
+
1370
+ <circle
1371
+ class="fill-(--foreground) stroke-(--card)"
1372
+ data-chart-end
1373
+ r="4"
1374
+ stroke-width="2"
1375
+ [attr.cx]="geometry.end.x"
1376
+ [attr.cy]="geometry.end.y"
1377
+ />
1378
+
1379
+ @for (
1380
+ tick of ticks();
1381
+ track tick.t;
1382
+ let index = $index;
1383
+ let count = $count;
1384
+ let first = $first;
1385
+ let last = $last
1386
+ ) {
1387
+ <text
1388
+ class="text-caption fill-(--subtle-foreground)"
1389
+ data-chart-axis-tick
1390
+ [attr.text-anchor]="first ? 'start' : last ? 'end' : 'middle'"
1391
+ [attr.x]="tick.x"
1392
+ [attr.y]="effectiveHeight() - 4"
1393
+ [class.max-sm:hidden]="!coreTick(index, count)"
1394
+ >
1395
+ {{ axisFormat()(tick.t) }}
1396
+ </text>
1397
+ }
1398
+
1399
+ @if (active(); as point) {
1400
+ <line
1401
+ class="stroke-(--border)"
1402
+ data-chart-crosshair
1403
+ stroke-width="1"
1404
+ y1="0"
1405
+ [attr.x1]="point.x"
1406
+ [attr.x2]="point.x"
1407
+ [attr.y2]="effectiveHeight()"
1408
+ />
1409
+ <circle
1410
+ class="fill-(--foreground) stroke-(--card)"
1411
+ data-chart-active
1412
+ r="4"
1413
+ stroke-width="2"
1414
+ [attr.cx]="point.x"
1415
+ [attr.cy]="point.y"
1416
+ />
1417
+ }
1418
+ }
1419
+ </svg>
1420
+
1421
+ @if (active(); as point) {
1422
+ <div
1423
+ 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)]"
1424
+ data-testid="chart-tooltip"
1425
+ role="status"
1426
+ [style]="tooltipStyle(point)"
1427
+ >
1428
+ <p class="font-medium text-(--foreground)">{{ valueFormat()(point.v) }}</p>
1429
+ <p data-chart-delta [class]="deltaClasses(point)">{{ deltaFormat()(deltaFor(point)) }}</p>
1430
+ <p class="text-(--subtle-foreground)">{{ timeFormat()(point.t) }}</p>
1431
+ </div>
1432
+ }
1433
+
1434
+ @if (points().length > 0) {
1435
+ <table class="sr-only">
1436
+ <caption>
1437
+ {{
1438
+ label()
1439
+ }}
1440
+ </caption>
1441
+ <thead>
1442
+ <tr>
1443
+ <th scope="col">{{ timeColumnLabel() }}</th>
1444
+ <th scope="col">{{ valueColumnLabel() }}</th>
1445
+ </tr>
1446
+ </thead>
1447
+ <tbody>
1448
+ @for (point of points(); track point.t) {
1449
+ <tr>
1450
+ <td>{{ timeFormat()(point.t) }}</td>
1451
+ <td>{{ valueFormat()(point.v) }}</td>
1452
+ </tr>
1453
+ }
1454
+ </tbody>
1455
+ </table>
1456
+ }
1457
+ </div>
1458
+ `,
1459
+ host: {
1460
+ class: 'block h-full',
1461
+ },
1462
+ }]
1463
+ }], 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 }] }] } });
1464
+
804
1465
  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
1466
  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
1467
  const nextId = (() => {
@@ -1619,5 +2280,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
1619
2280
  * Generated bundle index. Do not edit.
1620
2281
  */
1621
2282
 
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 };
2283
+ 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, UiLineChart, UiMenu, UiMenuItem, UiMenuTrigger, UiMeter, UiNavItem, UiRow, UiSegmented, UiSelect, UiSkeleton, UiStat, UiSwitch, UiTab, UiTabBar, UiTable, UiTd, UiTextarea, UiTh, UiTr, formatAmount };
1623
2284
  //# sourceMappingURL=joanroucoux-cairn-ui.mjs.map