@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
|