@oneuptime/common 7.0.2989 → 7.0.2994

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.
Files changed (85) hide show
  1. package/Server/Infrastructure/Queue.ts +36 -1
  2. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +4 -17
  3. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +1005 -0
  4. package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +3 -0
  5. package/UI/Components/Charts/ChartLibrary/Utils/ChartColors.ts +117 -0
  6. package/UI/Components/Charts/ChartLibrary/Utils/Cx.ts +8 -0
  7. package/UI/Components/Charts/ChartLibrary/Utils/GetYAxisDomain.ts +15 -0
  8. package/UI/Components/Charts/ChartLibrary/Utils/HasOnlyOneValueForKey.ts +19 -0
  9. package/UI/Components/Charts/ChartLibrary/Utils/UseWindowOnResize.ts +17 -0
  10. package/UI/Components/Charts/Line/LineChart.tsx +58 -225
  11. package/UI/Components/Charts/Types/ChartCurve.ts +7 -0
  12. package/UI/Components/Charts/Types/DataPoint.ts +7 -0
  13. package/UI/Components/Charts/Types/SeriesPoint.ts +7 -0
  14. package/UI/Components/Charts/Types/SeriesPoints.ts +6 -0
  15. package/UI/Components/Charts/Types/XAxis/XAxis.ts +21 -0
  16. package/UI/Components/Charts/Types/XAxis/XAxisMaxMin.ts +3 -0
  17. package/UI/Components/Charts/Types/XAxis/XAxisPrecision.ts +26 -0
  18. package/UI/Components/Charts/Types/XAxis/XAxisType.ts +6 -0
  19. package/UI/Components/Charts/Types/XValue.ts +3 -0
  20. package/UI/Components/Charts/Types/YAxis/YAxis.ts +22 -0
  21. package/UI/Components/Charts/Types/YAxis/YAxisMaxMin.ts +3 -0
  22. package/UI/Components/Charts/Types/YAxis/YAxisType.ts +5 -0
  23. package/UI/Components/Charts/Types/YValue.ts +3 -0
  24. package/UI/Components/Charts/Utils/DataPoint.ts +188 -0
  25. package/UI/Components/Charts/Utils/XAxis.ts +267 -0
  26. package/build/dist/Server/Infrastructure/Queue.js +20 -1
  27. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  28. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +4 -9
  29. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  30. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +388 -0
  31. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -0
  32. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +2 -0
  33. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -0
  34. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/ChartColors.js +88 -0
  35. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/ChartColors.js.map +1 -0
  36. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/Cx.js +7 -0
  37. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/Cx.js.map +1 -0
  38. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/GetYAxisDomain.js +7 -0
  39. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/GetYAxisDomain.js.map +1 -0
  40. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/HasOnlyOneValueForKey.js +14 -0
  41. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/HasOnlyOneValueForKey.js.map +1 -0
  42. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseWindowOnResize.js +14 -0
  43. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/UseWindowOnResize.js.map +1 -0
  44. package/build/dist/UI/Components/Charts/Line/LineChart.js +30 -136
  45. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  46. package/build/dist/UI/Components/Charts/Types/ChartCurve.js +8 -0
  47. package/build/dist/UI/Components/Charts/Types/ChartCurve.js.map +1 -0
  48. package/build/dist/UI/Components/Charts/Types/DataPoint.js +2 -0
  49. package/build/dist/UI/Components/Charts/Types/DataPoint.js.map +1 -0
  50. package/build/dist/UI/Components/Charts/Types/SeriesPoint.js +2 -0
  51. package/build/dist/UI/Components/Charts/Types/SeriesPoint.js.map +1 -0
  52. package/build/dist/UI/Components/Charts/Types/SeriesPoints.js +2 -0
  53. package/build/dist/UI/Components/Charts/Types/SeriesPoints.js.map +1 -0
  54. package/build/dist/UI/Components/Charts/Types/XAxis/XAxis.js +8 -0
  55. package/build/dist/UI/Components/Charts/Types/XAxis/XAxis.js.map +1 -0
  56. package/build/dist/UI/Components/Charts/Types/XAxis/XAxisMaxMin.js +2 -0
  57. package/build/dist/UI/Components/Charts/Types/XAxis/XAxisMaxMin.js.map +1 -0
  58. package/build/dist/UI/Components/Charts/Types/XAxis/XAxisPrecision.js +27 -0
  59. package/build/dist/UI/Components/Charts/Types/XAxis/XAxisPrecision.js.map +1 -0
  60. package/build/dist/UI/Components/Charts/Types/XAxis/XAxisType.js +7 -0
  61. package/build/dist/UI/Components/Charts/Types/XAxis/XAxisType.js.map +1 -0
  62. package/build/dist/UI/Components/Charts/Types/XValue.js +2 -0
  63. package/build/dist/UI/Components/Charts/Types/XValue.js.map +1 -0
  64. package/build/dist/UI/Components/Charts/Types/YAxis/YAxis.js +8 -0
  65. package/build/dist/UI/Components/Charts/Types/YAxis/YAxis.js.map +1 -0
  66. package/build/dist/UI/Components/Charts/Types/YAxis/YAxisMaxMin.js +2 -0
  67. package/build/dist/UI/Components/Charts/Types/YAxis/YAxisMaxMin.js.map +1 -0
  68. package/build/dist/UI/Components/Charts/Types/YAxis/YAxisType.js +6 -0
  69. package/build/dist/UI/Components/Charts/Types/YAxis/YAxisType.js.map +1 -0
  70. package/build/dist/UI/Components/Charts/Types/YValue.js +2 -0
  71. package/build/dist/UI/Components/Charts/Types/YValue.js.map +1 -0
  72. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +109 -0
  73. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -0
  74. package/build/dist/UI/Components/Charts/Utils/XAxis.js +241 -0
  75. package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -0
  76. package/package.json +6 -4
  77. package/UI/Components/Charts/Bar/Bar.tsx +0 -0
  78. package/UI/Components/Charts/Base/BaseChart.tsx +0 -0
  79. package/UI/Components/Charts/Tooltip/Tooltip.tsx +0 -84
  80. package/build/dist/UI/Components/Charts/Bar/Bar.js +0 -2
  81. package/build/dist/UI/Components/Charts/Bar/Bar.js.map +0 -1
  82. package/build/dist/UI/Components/Charts/Base/BaseChart.js +0 -2
  83. package/build/dist/UI/Components/Charts/Base/BaseChart.js.map +0 -1
  84. package/build/dist/UI/Components/Charts/Tooltip/Tooltip.js +0 -34
  85. package/build/dist/UI/Components/Charts/Tooltip/Tooltip.js.map +0 -1
@@ -0,0 +1,1005 @@
1
+ // Tremor Raw LineChart [v0.3.1]
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+
4
+ "use client";
5
+
6
+ import React from "react";
7
+ import { RiArrowLeftSLine, RiArrowRightSLine } from "@remixicon/react";
8
+ import {
9
+ CartesianGrid,
10
+ Dot,
11
+ Label,
12
+ Line,
13
+ Legend as RechartsLegend,
14
+ LineChart as RechartsLineChart,
15
+ ResponsiveContainer,
16
+ Tooltip,
17
+ XAxis,
18
+ YAxis,
19
+ } from "recharts";
20
+ import { AxisDomain } from "recharts/types/util/types";
21
+
22
+ import { useOnWindowResize } from "../Utils/UseWindowOnResize";
23
+ import {
24
+ AvailableChartColors,
25
+ AvailableChartColorsKeys,
26
+ constructCategoryColors,
27
+ getColorClassName,
28
+ } from "../Utils/ChartColors";
29
+ import { cx } from "../Utils/Cx";
30
+ import { getYAxisDomain } from "../Utils/GetYAxisDomain";
31
+ import { hasOnlyOneValueForKey } from "../Utils/HasOnlyOneValueForKey";
32
+ import ChartCurve from "../../Types/ChartCurve";
33
+
34
+ //#region Legend
35
+
36
+ interface LegendItemProps {
37
+ name: string;
38
+ color: AvailableChartColorsKeys;
39
+ onClick?: (name: string, color: AvailableChartColorsKeys) => void;
40
+ activeLegend?: string;
41
+ }
42
+
43
+ const LegendItem: ({
44
+ name,
45
+ color,
46
+ onClick,
47
+ activeLegend,
48
+ }: LegendItemProps) => React.JSX.Element = ({
49
+ name,
50
+ color,
51
+ onClick,
52
+ activeLegend,
53
+ }: LegendItemProps) => {
54
+ const hasOnValueChange: boolean = Boolean(onClick);
55
+ return (
56
+ <li
57
+ className={cx(
58
+ // base
59
+ "group inline-flex flex-nowrap items-center gap-1.5 whitespace-nowrap rounded px-2 py-1 transition",
60
+ hasOnValueChange
61
+ ? "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
62
+ : "cursor-default",
63
+ )}
64
+ onClick={(e: React.MouseEvent<HTMLLIElement, MouseEvent>) => {
65
+ e.stopPropagation();
66
+ onClick?.(name, color);
67
+ }}
68
+ >
69
+ <span
70
+ className={cx(
71
+ "h-[3px] w-3.5 shrink-0 rounded-full",
72
+ getColorClassName(color, "bg"),
73
+ activeLegend && activeLegend !== name ? "opacity-40" : "opacity-100",
74
+ )}
75
+ aria-hidden={true}
76
+ />
77
+ <p
78
+ className={cx(
79
+ // base
80
+ "truncate whitespace-nowrap text-xs",
81
+ // text color
82
+ "text-gray-700 dark:text-gray-300",
83
+ hasOnValueChange &&
84
+ "group-hover:text-gray-900 dark:group-hover:text-gray-50",
85
+ activeLegend && activeLegend !== name ? "opacity-40" : "opacity-100",
86
+ )}
87
+ >
88
+ {name}
89
+ </p>
90
+ </li>
91
+ );
92
+ };
93
+
94
+ interface ScrollButtonProps {
95
+ icon: React.ElementType;
96
+ onClick?: () => void;
97
+ disabled?: boolean;
98
+ }
99
+
100
+ const ScrollButton: ({
101
+ icon,
102
+ onClick,
103
+ disabled,
104
+ }: ScrollButtonProps) => React.JSX.Element = ({
105
+ icon,
106
+ onClick,
107
+ disabled,
108
+ }: ScrollButtonProps) => {
109
+ const Icon: React.ElementType<any, keyof React.JSX.IntrinsicElements> = icon;
110
+ const [isPressed, setIsPressed] = React.useState(false);
111
+ const intervalRef: React.MutableRefObject<NodeJS.Timeout | null> =
112
+ React.useRef<NodeJS.Timeout | null>(null);
113
+
114
+ React.useEffect(() => {
115
+ if (isPressed) {
116
+ intervalRef.current = setInterval(() => {
117
+ onClick?.();
118
+ }, 300);
119
+ } else {
120
+ clearInterval(intervalRef.current as NodeJS.Timeout);
121
+ }
122
+ return () => {
123
+ return clearInterval(intervalRef.current as NodeJS.Timeout);
124
+ };
125
+ }, [isPressed, onClick]);
126
+
127
+ React.useEffect(() => {
128
+ if (disabled) {
129
+ clearInterval(intervalRef.current as NodeJS.Timeout);
130
+ setIsPressed(false);
131
+ }
132
+ }, [disabled]);
133
+
134
+ return (
135
+ <button
136
+ type="button"
137
+ className={cx(
138
+ // base
139
+ "group inline-flex size-5 items-center truncate rounded transition",
140
+ disabled
141
+ ? "cursor-not-allowed text-gray-400 dark:text-gray-600"
142
+ : "cursor-pointer text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-50",
143
+ )}
144
+ disabled={disabled}
145
+ onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
146
+ e.stopPropagation();
147
+ onClick?.();
148
+ }}
149
+ onMouseDown={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
150
+ e.stopPropagation();
151
+ setIsPressed(true);
152
+ }}
153
+ onMouseUp={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
154
+ e.stopPropagation();
155
+ setIsPressed(false);
156
+ }}
157
+ >
158
+ <Icon className="size-full" aria-hidden="true" />
159
+ </button>
160
+ );
161
+ };
162
+
163
+ interface LegendProps extends React.OlHTMLAttributes<HTMLOListElement> {
164
+ categories: string[];
165
+ colors?: AvailableChartColorsKeys[];
166
+ onClickLegendItem?: (category: string, color: string) => void;
167
+ activeLegend?: string;
168
+ enableLegendSlider?: boolean;
169
+ }
170
+
171
+ type HasScrollProps = {
172
+ left: boolean;
173
+ right: boolean;
174
+ };
175
+
176
+ const Legend: React.ForwardRefExoticComponent<
177
+ LegendProps & React.RefAttributes<HTMLOListElement>
178
+ > = React.forwardRef<HTMLOListElement, LegendProps>(
179
+ (props: LegendProps, ref: React.ForwardedRef<HTMLOListElement>) => {
180
+ const {
181
+ categories,
182
+ colors = AvailableChartColors,
183
+ className,
184
+ onClickLegendItem,
185
+ activeLegend,
186
+ enableLegendSlider = false,
187
+ ...other
188
+ } = props;
189
+
190
+ const scrollableRef: React.RefObject<HTMLInputElement> =
191
+ React.useRef<HTMLInputElement>(null);
192
+ const scrollButtonsRef: React.RefObject<HTMLDivElement> =
193
+ React.useRef<HTMLDivElement>(null);
194
+ const [hasScroll, setHasScroll] = React.useState<HasScrollProps | null>(
195
+ null,
196
+ );
197
+ const [isKeyDowned, setIsKeyDowned] = React.useState<string | null>(null);
198
+ const intervalRef: React.MutableRefObject<NodeJS.Timeout | null> =
199
+ React.useRef<NodeJS.Timeout | null>(null);
200
+
201
+ const checkScroll: () => void = React.useCallback(() => {
202
+ const scrollable: HTMLInputElement | null = scrollableRef?.current;
203
+ if (!scrollable) {
204
+ return;
205
+ }
206
+
207
+ const hasLeftScroll: boolean = scrollable.scrollLeft > 0;
208
+ const hasRightScroll: boolean =
209
+ scrollable.scrollWidth - scrollable.clientWidth > scrollable.scrollLeft;
210
+
211
+ setHasScroll({ left: hasLeftScroll, right: hasRightScroll });
212
+ }, [setHasScroll]);
213
+
214
+ const scrollToTest: (direction: "left" | "right") => void =
215
+ React.useCallback(
216
+ (direction: "left" | "right") => {
217
+ const element: HTMLInputElement | null = scrollableRef?.current;
218
+ const scrollButtons: HTMLDivElement | null =
219
+ scrollButtonsRef?.current;
220
+ const scrollButtonsWith: number = scrollButtons?.clientWidth ?? 0;
221
+ const width: number = element?.clientWidth ?? 0;
222
+
223
+ if (element && enableLegendSlider) {
224
+ element.scrollTo({
225
+ left:
226
+ direction === "left"
227
+ ? element.scrollLeft - width + scrollButtonsWith
228
+ : element.scrollLeft + width - scrollButtonsWith,
229
+ behavior: "smooth",
230
+ });
231
+ setTimeout(() => {
232
+ checkScroll();
233
+ }, 400);
234
+ }
235
+ },
236
+ [enableLegendSlider, checkScroll],
237
+ );
238
+
239
+ React.useEffect(() => {
240
+ const keyDownHandler: (key: string) => void = (key: string) => {
241
+ if (key === "ArrowLeft") {
242
+ scrollToTest("left");
243
+ } else if (key === "ArrowRight") {
244
+ scrollToTest("right");
245
+ }
246
+ };
247
+ if (isKeyDowned) {
248
+ keyDownHandler(isKeyDowned);
249
+ intervalRef.current = setInterval(() => {
250
+ keyDownHandler(isKeyDowned);
251
+ }, 300);
252
+ } else {
253
+ clearInterval(intervalRef.current!);
254
+ }
255
+ return () => {
256
+ return clearInterval(intervalRef.current as NodeJS.Timeout);
257
+ };
258
+ }, [isKeyDowned, scrollToTest]);
259
+
260
+ const keyDown: (e: KeyboardEvent) => void = (e: KeyboardEvent) => {
261
+ e.stopPropagation();
262
+ if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
263
+ e.preventDefault();
264
+ setIsKeyDowned(e.key);
265
+ }
266
+ };
267
+ const keyUp: (e: KeyboardEvent) => void = (e: KeyboardEvent) => {
268
+ e.stopPropagation();
269
+ setIsKeyDowned(null);
270
+ };
271
+
272
+ React.useEffect(() => {
273
+ const scrollable: HTMLInputElement | null = scrollableRef?.current;
274
+ if (enableLegendSlider) {
275
+ checkScroll();
276
+ scrollable?.addEventListener("keydown", keyDown);
277
+ scrollable?.addEventListener("keyup", keyUp);
278
+ }
279
+
280
+ return () => {
281
+ scrollable?.removeEventListener("keydown", keyDown);
282
+ scrollable?.removeEventListener("keyup", keyUp);
283
+ };
284
+ }, [checkScroll, enableLegendSlider]);
285
+
286
+ return (
287
+ <ol
288
+ ref={ref}
289
+ className={cx("relative overflow-hidden", className)}
290
+ {...other}
291
+ >
292
+ <div
293
+ ref={scrollableRef}
294
+ tabIndex={0}
295
+ className={cx(
296
+ "flex h-full",
297
+ enableLegendSlider
298
+ ? hasScroll?.right || hasScroll?.left
299
+ ? "snap-mandatory items-center overflow-auto pl-4 pr-12 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
300
+ : ""
301
+ : "flex-wrap",
302
+ )}
303
+ >
304
+ {categories.map((category: string, index: number) => {
305
+ return (
306
+ <LegendItem
307
+ key={`item-${index}`}
308
+ name={category}
309
+ color={colors[index] as AvailableChartColorsKeys}
310
+ onClick={onClickLegendItem as any}
311
+ activeLegend={activeLegend!}
312
+ />
313
+ );
314
+ })}
315
+ </div>
316
+ {enableLegendSlider && (hasScroll?.right || hasScroll?.left) ? (
317
+ <>
318
+ <div
319
+ ref={scrollButtonsRef}
320
+ className={cx(
321
+ // base
322
+ "absolute bottom-0 right-0 top-0 flex h-full items-center justify-center pr-1",
323
+ // background color
324
+ "bg-white dark:bg-gray-950",
325
+ )}
326
+ >
327
+ <ScrollButton
328
+ icon={RiArrowLeftSLine}
329
+ onClick={() => {
330
+ setIsKeyDowned(null);
331
+ scrollToTest("left");
332
+ }}
333
+ disabled={!hasScroll?.left}
334
+ />
335
+ <ScrollButton
336
+ icon={RiArrowRightSLine}
337
+ onClick={() => {
338
+ setIsKeyDowned(null);
339
+ scrollToTest("right");
340
+ }}
341
+ disabled={!hasScroll?.right}
342
+ />
343
+ </div>
344
+ </>
345
+ ) : null}
346
+ </ol>
347
+ );
348
+ },
349
+ );
350
+
351
+ Legend.displayName = "Legend";
352
+
353
+ const ChartLegend: (
354
+ { payload }: any,
355
+ categoryColors: Map<string, AvailableChartColorsKeys>,
356
+ setLegendHeight: React.Dispatch<React.SetStateAction<number>>,
357
+ activeLegend: string | undefined,
358
+ onClick?: (category: string, color: string) => void,
359
+ enableLegendSlider?: boolean,
360
+ legendPosition?: "left" | "center" | "right",
361
+ yAxisWidth?: number,
362
+ ) => React.JSX.Element = (
363
+ { payload }: any,
364
+ categoryColors: Map<string, AvailableChartColorsKeys>,
365
+ setLegendHeight: React.Dispatch<React.SetStateAction<number>>,
366
+ activeLegend: string | undefined,
367
+ onClick?: (category: string, color: string) => void,
368
+ enableLegendSlider?: boolean,
369
+ legendPosition?: "left" | "center" | "right",
370
+ yAxisWidth?: number,
371
+ ): React.JSX.Element => {
372
+ const legendRef: React.RefObject<HTMLDivElement> =
373
+ React.useRef<HTMLDivElement>(null);
374
+
375
+ useOnWindowResize(() => {
376
+ const calculateHeight: (height: number | undefined) => number = (
377
+ height: number | undefined,
378
+ ) => {
379
+ return height ? Number(height) + 15 : 60;
380
+ };
381
+ setLegendHeight(calculateHeight(legendRef.current?.clientHeight));
382
+ });
383
+
384
+ const legendPayload: Array<PayloadItem> = payload.filter(
385
+ (item: PayloadItem) => {
386
+ return item.type !== "none";
387
+ },
388
+ );
389
+
390
+ const paddingLeft: number =
391
+ legendPosition === "left" && yAxisWidth ? yAxisWidth - 8 : 0;
392
+
393
+ return (
394
+ <div
395
+ ref={legendRef}
396
+ style={{ paddingLeft: paddingLeft }}
397
+ className={cx(
398
+ "flex items-center",
399
+ { "justify-center": legendPosition === "center" },
400
+ { "justify-start": legendPosition === "left" },
401
+ { "justify-end": legendPosition === "right" },
402
+ )}
403
+ >
404
+ <Legend
405
+ categories={legendPayload.map((entry: PayloadItem) => {
406
+ return entry.value as any;
407
+ })}
408
+ colors={legendPayload.map((entry: PayloadItem) => {
409
+ return categoryColors.get(entry.value! as any)!;
410
+ })}
411
+ onClickLegendItem={onClick!}
412
+ activeLegend={activeLegend!}
413
+ enableLegendSlider={enableLegendSlider!}
414
+ />
415
+ </div>
416
+ );
417
+ };
418
+
419
+ //#region Tooltip
420
+
421
+ type TooltipProps = Pick<ChartTooltipProps, "active" | "payload" | "label">;
422
+
423
+ export type PayloadItem = {
424
+ // eslint-disable-next-line react/no-unused-prop-types
425
+ category: string;
426
+ // eslint-disable-next-line react/no-unused-prop-types
427
+ value: number;
428
+ // eslint-disable-next-line react/no-unused-prop-types
429
+ index: string;
430
+ // eslint-disable-next-line react/no-unused-prop-types
431
+ color: AvailableChartColorsKeys;
432
+ // eslint-disable-next-line react/no-unused-prop-types
433
+ type?: string;
434
+ // eslint-disable-next-line react/no-unused-prop-types
435
+ payload: any;
436
+ };
437
+
438
+ interface ChartTooltipProps {
439
+ active: boolean | undefined;
440
+ payload: PayloadItem[];
441
+ label: string;
442
+ valueFormatter: (value: number) => string;
443
+ }
444
+
445
+ const ChartTooltip: ({
446
+ active,
447
+ payload,
448
+ label,
449
+ valueFormatter,
450
+ }: ChartTooltipProps) => React.JSX.Element | null = ({
451
+ active,
452
+ payload,
453
+ label,
454
+ valueFormatter,
455
+ }: ChartTooltipProps): React.JSX.Element | null => {
456
+ if (active && payload && payload.length) {
457
+ const legendPayload: PayloadItem[] = payload.filter((item: PayloadItem) => {
458
+ return item.type !== "none";
459
+ });
460
+ return (
461
+ <div
462
+ className={cx(
463
+ // base
464
+ "rounded-md border text-sm shadow-md",
465
+ // border color
466
+ "border-gray-200 dark:border-gray-800",
467
+ // background color
468
+ "bg-white dark:bg-gray-950",
469
+ )}
470
+ >
471
+ <div className={cx("border-b border-inherit px-4 py-2")}>
472
+ <p
473
+ className={cx(
474
+ // base
475
+ "font-medium",
476
+ // text color
477
+ "text-gray-900 dark:text-gray-50",
478
+ )}
479
+ >
480
+ {label}
481
+ </p>
482
+ </div>
483
+ <div className={cx("space-y-1 px-4 py-2")}>
484
+ {legendPayload.map(
485
+ ({ value, category, color }: PayloadItem, index: number) => {
486
+ return (
487
+ <div
488
+ key={`id-${index}`}
489
+ className="flex items-center justify-between space-x-8"
490
+ >
491
+ <div className="flex items-center space-x-2">
492
+ <span
493
+ aria-hidden="true"
494
+ className={cx(
495
+ "h-[3px] w-3.5 shrink-0 rounded-full",
496
+ getColorClassName(color, "bg"),
497
+ )}
498
+ />
499
+ <p
500
+ className={cx(
501
+ // base
502
+ "whitespace-nowrap text-right",
503
+ // text color
504
+ "text-gray-700 dark:text-gray-300",
505
+ )}
506
+ >
507
+ {category}
508
+ </p>
509
+ </div>
510
+ <p
511
+ className={cx(
512
+ // base
513
+ "whitespace-nowrap text-right font-medium tabular-nums",
514
+ // text color
515
+ "text-gray-900 dark:text-gray-50",
516
+ )}
517
+ >
518
+ {valueFormatter(value)}
519
+ </p>
520
+ </div>
521
+ );
522
+ },
523
+ )}
524
+ </div>
525
+ </div>
526
+ );
527
+ }
528
+ return null;
529
+ };
530
+
531
+ //#region LineChart
532
+
533
+ interface ActiveDot {
534
+ index?: number;
535
+ dataKey?: string;
536
+ }
537
+
538
+ type BaseEventProps = {
539
+ eventType: "dot" | "category";
540
+ categoryClicked: string;
541
+ [key: string]: number | string;
542
+ };
543
+
544
+ type LineChartEventProps = BaseEventProps | null | undefined;
545
+
546
+ interface LineChartProps extends React.HTMLAttributes<HTMLDivElement> {
547
+ data: Record<string, any>[];
548
+ index: string;
549
+ categories: string[];
550
+ colors?: AvailableChartColorsKeys[];
551
+ valueFormatter?: (value: number) => string;
552
+ startEndOnly?: boolean;
553
+ showXAxis?: boolean;
554
+ showYAxis?: boolean;
555
+ showGridLines?: boolean;
556
+ yAxisWidth?: number;
557
+ intervalType?: "preserveStartEnd" | "equidistantPreserveStart";
558
+ showTooltip?: boolean;
559
+ showLegend?: boolean;
560
+ autoMinValue?: boolean;
561
+ minValue?: number;
562
+ maxValue?: number;
563
+ allowDecimals?: boolean;
564
+ onValueChange?: (value: LineChartEventProps) => void;
565
+ enableLegendSlider?: boolean;
566
+ tickGap?: number;
567
+ connectNulls?: boolean;
568
+ xAxisLabel?: string;
569
+ yAxisLabel?: string;
570
+ curve?: ChartCurve;
571
+ legendPosition?: "left" | "center" | "right";
572
+ tooltipCallback?: (tooltipCallbackContent: TooltipProps) => void;
573
+ customTooltip?: React.ComponentType<TooltipProps>;
574
+ syncId?: string | undefined;
575
+ }
576
+
577
+ const LineChart: React.ForwardRefExoticComponent<
578
+ LineChartProps & React.RefAttributes<HTMLDivElement>
579
+ > = React.forwardRef<HTMLDivElement, LineChartProps>(
580
+ (props: LineChartProps, ref: React.ForwardedRef<HTMLDivElement>) => {
581
+ const {
582
+ data = [],
583
+ categories = [],
584
+ index,
585
+ colors = AvailableChartColors,
586
+ valueFormatter = (value: number) => {
587
+ return value.toString();
588
+ },
589
+ startEndOnly = false,
590
+ showXAxis = true,
591
+ showYAxis = true,
592
+ showGridLines = true,
593
+ yAxisWidth = 56,
594
+ intervalType = "equidistantPreserveStart",
595
+ showTooltip = true,
596
+ showLegend = true,
597
+ autoMinValue = false,
598
+ minValue,
599
+ maxValue,
600
+ allowDecimals = true,
601
+ connectNulls = false,
602
+ className,
603
+ onValueChange,
604
+ enableLegendSlider = false,
605
+ tickGap = 5,
606
+ xAxisLabel,
607
+ yAxisLabel,
608
+ legendPosition = "right",
609
+ tooltipCallback,
610
+ customTooltip,
611
+ ...other
612
+ } = props;
613
+ const CustomTooltip: React.ComponentType<TooltipProps> | undefined =
614
+ customTooltip;
615
+ const paddingValue: 0 | 20 =
616
+ (!showXAxis && !showYAxis) || (startEndOnly && !showYAxis) ? 0 : 20;
617
+ const [legendHeight, setLegendHeight] = React.useState(60);
618
+ const [activeDot, setActiveDot] = React.useState<ActiveDot | undefined>(
619
+ undefined,
620
+ );
621
+ const [activeLegend, setActiveLegend] = React.useState<string | undefined>(
622
+ undefined,
623
+ );
624
+ const categoryColors: Map<string, string | number> =
625
+ constructCategoryColors(categories, colors);
626
+
627
+ const yAxisDomain: (number | "auto")[] = getYAxisDomain(
628
+ autoMinValue,
629
+ minValue,
630
+ maxValue,
631
+ );
632
+ const hasOnValueChange: boolean = Boolean(onValueChange);
633
+ const prevActiveRef: React.MutableRefObject<boolean | undefined> =
634
+ React.useRef<boolean | undefined>(undefined);
635
+ const prevLabelRef: React.MutableRefObject<string | undefined> =
636
+ React.useRef<string | undefined>(undefined);
637
+
638
+ function onDotClick(itemData: any, event: React.MouseEvent): void {
639
+ event.stopPropagation();
640
+
641
+ if (!hasOnValueChange) {
642
+ return;
643
+ }
644
+ if (
645
+ (itemData.index === activeDot?.index &&
646
+ itemData.dataKey === activeDot?.dataKey) ||
647
+ (hasOnlyOneValueForKey(data, itemData.dataKey) &&
648
+ activeLegend &&
649
+ activeLegend === itemData.dataKey)
650
+ ) {
651
+ setActiveLegend(undefined);
652
+ setActiveDot(undefined);
653
+ onValueChange?.(null);
654
+ } else {
655
+ setActiveLegend(itemData.dataKey);
656
+ setActiveDot({
657
+ index: itemData.index,
658
+ dataKey: itemData.dataKey,
659
+ });
660
+ onValueChange?.({
661
+ eventType: "dot",
662
+ categoryClicked: itemData.dataKey,
663
+ ...itemData.payload,
664
+ });
665
+ }
666
+ }
667
+
668
+ function onCategoryClick(dataKey: string): void {
669
+ if (!hasOnValueChange) {
670
+ return;
671
+ }
672
+ if (
673
+ (dataKey === activeLegend && !activeDot) ||
674
+ (hasOnlyOneValueForKey(data, dataKey) &&
675
+ activeDot &&
676
+ activeDot.dataKey === dataKey)
677
+ ) {
678
+ setActiveLegend(undefined);
679
+ onValueChange?.(null);
680
+ } else {
681
+ setActiveLegend(dataKey);
682
+ onValueChange?.({
683
+ eventType: "category",
684
+ categoryClicked: dataKey,
685
+ });
686
+ }
687
+ setActiveDot(undefined);
688
+ }
689
+
690
+ return (
691
+ <div ref={ref} className={cx("h-80 w-full", className)} {...other}>
692
+ <ResponsiveContainer>
693
+ <RechartsLineChart
694
+ data={data}
695
+ syncId={props.syncId}
696
+ onClick={
697
+ hasOnValueChange && (activeLegend || activeDot)
698
+ ? () => {
699
+ setActiveDot(undefined);
700
+ setActiveLegend(undefined);
701
+ onValueChange?.(null);
702
+ }
703
+ : undefined
704
+ }
705
+ margin={{
706
+ bottom: (xAxisLabel ? 30 : undefined) as unknown as number,
707
+ left: (yAxisLabel ? 20 : undefined) as unknown as number,
708
+ right: (yAxisLabel ? 5 : undefined) as unknown as number,
709
+ top: 5,
710
+ }}
711
+ >
712
+ {showGridLines ? (
713
+ <CartesianGrid
714
+ className={cx("stroke-gray-200 stroke-1 dark:stroke-gray-800")}
715
+ horizontal={true}
716
+ vertical={false}
717
+ />
718
+ ) : null}
719
+ <XAxis
720
+ padding={{ left: paddingValue, right: paddingValue }}
721
+ hide={!showXAxis}
722
+ dataKey={index}
723
+ interval={startEndOnly ? "preserveStartEnd" : intervalType}
724
+ tick={{ transform: "translate(0, 6)" }}
725
+ ticks={
726
+ startEndOnly
727
+ ? ([
728
+ (data[0] as any)[index],
729
+ (data[data.length - 1] as any)[index],
730
+ ] as any)
731
+ : undefined
732
+ }
733
+ fill=""
734
+ stroke=""
735
+ className={cx(
736
+ // base
737
+ "text-xs",
738
+ // text fill
739
+ "fill-gray-500 dark:fill-gray-500",
740
+ )}
741
+ tickLine={false}
742
+ axisLine={false}
743
+ minTickGap={tickGap}
744
+ >
745
+ {xAxisLabel && (
746
+ <Label
747
+ position="insideBottom"
748
+ offset={-20}
749
+ className="fill-gray-800 text-sm font-medium dark:fill-gray-200"
750
+ >
751
+ {xAxisLabel}
752
+ </Label>
753
+ )}
754
+ </XAxis>
755
+ <YAxis
756
+ width={yAxisWidth}
757
+ hide={!showYAxis}
758
+ axisLine={false}
759
+ tickLine={false}
760
+ type="number"
761
+ domain={yAxisDomain as AxisDomain}
762
+ tick={{ transform: "translate(-3, 0)" }}
763
+ fill=""
764
+ stroke=""
765
+ className={cx(
766
+ // base
767
+ "text-xs",
768
+ // text fill
769
+ "fill-gray-500 dark:fill-gray-500",
770
+ )}
771
+ tickFormatter={valueFormatter}
772
+ allowDecimals={allowDecimals}
773
+ >
774
+ {yAxisLabel && (
775
+ <Label
776
+ position="insideLeft"
777
+ style={{ textAnchor: "middle" }}
778
+ angle={-90}
779
+ offset={-15}
780
+ className="fill-gray-800 text-sm font-medium dark:fill-gray-200"
781
+ >
782
+ {yAxisLabel}
783
+ </Label>
784
+ )}
785
+ </YAxis>
786
+ <Tooltip
787
+ wrapperStyle={{ outline: "none" }}
788
+ isAnimationActive={true}
789
+ animationDuration={100}
790
+ cursor={{ stroke: "#d1d5db", strokeWidth: 1 }}
791
+ offset={20}
792
+ position={{ y: 0 }}
793
+ content={({ active, payload, label }: any) => {
794
+ const cleanPayload: TooltipProps["payload"] = payload
795
+ ? payload.map((item: any) => {
796
+ return {
797
+ category: item.dataKey,
798
+ value: item.value,
799
+ index: item.payload[index],
800
+ color: categoryColors.get(
801
+ item.dataKey,
802
+ ) as AvailableChartColorsKeys,
803
+ type: item.type,
804
+ payload: item.payload,
805
+ };
806
+ })
807
+ : [];
808
+
809
+ if (
810
+ tooltipCallback &&
811
+ (active !== prevActiveRef.current ||
812
+ label !== prevLabelRef.current)
813
+ ) {
814
+ tooltipCallback({ active, payload: cleanPayload, label });
815
+ prevActiveRef.current = active;
816
+ prevLabelRef.current = label;
817
+ }
818
+
819
+ return showTooltip && active ? (
820
+ CustomTooltip ? (
821
+ <CustomTooltip
822
+ active={active}
823
+ payload={cleanPayload}
824
+ label={label}
825
+ />
826
+ ) : (
827
+ <ChartTooltip
828
+ active={active}
829
+ payload={cleanPayload}
830
+ label={label}
831
+ valueFormatter={valueFormatter}
832
+ />
833
+ )
834
+ ) : null;
835
+ }}
836
+ />
837
+
838
+ {showLegend ? (
839
+ <RechartsLegend
840
+ verticalAlign="top"
841
+ height={legendHeight}
842
+ content={({ payload }: any) => {
843
+ return ChartLegend(
844
+ { payload },
845
+ categoryColors,
846
+ setLegendHeight,
847
+ activeLegend,
848
+ hasOnValueChange
849
+ ? (clickedLegendItem: string) => {
850
+ return onCategoryClick(clickedLegendItem);
851
+ }
852
+ : undefined,
853
+ enableLegendSlider,
854
+ legendPosition,
855
+ yAxisWidth,
856
+ );
857
+ }}
858
+ />
859
+ ) : null}
860
+ {categories.map((category: string) => {
861
+ return (
862
+ <Line
863
+ className={cx(
864
+ getColorClassName(
865
+ categoryColors.get(category) as AvailableChartColorsKeys,
866
+ "stroke",
867
+ ),
868
+ )}
869
+ strokeOpacity={
870
+ activeDot || (activeLegend && activeLegend !== category)
871
+ ? 0.3
872
+ : 1
873
+ }
874
+ activeDot={(props: any) => {
875
+ const {
876
+ cx: cxCoord,
877
+ cy: cyCoord,
878
+ stroke,
879
+ strokeLinecap,
880
+ strokeLinejoin,
881
+ strokeWidth,
882
+ dataKey,
883
+ } = props;
884
+ return (
885
+ <Dot
886
+ className={cx(
887
+ "stroke-white dark:stroke-gray-950",
888
+ onValueChange ? "cursor-pointer" : "",
889
+ getColorClassName(
890
+ categoryColors.get(
891
+ dataKey,
892
+ ) as AvailableChartColorsKeys,
893
+ "fill",
894
+ ),
895
+ )}
896
+ cx={cxCoord}
897
+ cy={cyCoord}
898
+ r={5}
899
+ fill=""
900
+ stroke={stroke}
901
+ strokeLinecap={strokeLinecap}
902
+ strokeLinejoin={strokeLinejoin}
903
+ strokeWidth={strokeWidth}
904
+ onClick={(_: any, event: any) => {
905
+ return onDotClick(props, event);
906
+ }}
907
+ />
908
+ );
909
+ }}
910
+ dot={(props: any) => {
911
+ const {
912
+ stroke,
913
+ strokeLinecap,
914
+ strokeLinejoin,
915
+ strokeWidth,
916
+ cx: cxCoord,
917
+ cy: cyCoord,
918
+ dataKey,
919
+ index,
920
+ } = props;
921
+
922
+ if (
923
+ (hasOnlyOneValueForKey(data, category) &&
924
+ !(
925
+ activeDot ||
926
+ (activeLegend && activeLegend !== category)
927
+ )) ||
928
+ (activeDot?.index === index &&
929
+ activeDot?.dataKey === category)
930
+ ) {
931
+ return (
932
+ <Dot
933
+ key={index}
934
+ cx={cxCoord}
935
+ cy={cyCoord}
936
+ r={5}
937
+ stroke={stroke}
938
+ fill=""
939
+ strokeLinecap={strokeLinecap}
940
+ strokeLinejoin={strokeLinejoin}
941
+ strokeWidth={strokeWidth}
942
+ className={cx(
943
+ "stroke-white dark:stroke-gray-950",
944
+ onValueChange ? "cursor-pointer" : "",
945
+ getColorClassName(
946
+ categoryColors.get(
947
+ dataKey,
948
+ ) as AvailableChartColorsKeys,
949
+ "fill",
950
+ ),
951
+ )}
952
+ />
953
+ );
954
+ }
955
+ return <React.Fragment key={index}></React.Fragment>;
956
+ }}
957
+ key={category}
958
+ name={category}
959
+ type="linear"
960
+ dataKey={category}
961
+ stroke=""
962
+ strokeWidth={2}
963
+ strokeLinejoin="round"
964
+ strokeLinecap="round"
965
+ isAnimationActive={false}
966
+ connectNulls={connectNulls}
967
+ />
968
+ );
969
+ })}
970
+ {/* hidden lines to increase clickable target area */}
971
+ {onValueChange
972
+ ? categories.map((category: string) => {
973
+ return (
974
+ <Line
975
+ className={cx("cursor-pointer")}
976
+ strokeOpacity={0}
977
+ key={category}
978
+ name={category}
979
+ type={props.curve || ChartCurve.LINEAR}
980
+ dataKey={category}
981
+ stroke="transparent"
982
+ fill="transparent"
983
+ legendType="none"
984
+ tooltipType="none"
985
+ strokeWidth={12}
986
+ connectNulls={connectNulls}
987
+ onClick={(props: any, event: any) => {
988
+ event.stopPropagation();
989
+ const { name } = props;
990
+ onCategoryClick(name);
991
+ }}
992
+ />
993
+ );
994
+ })
995
+ : null}
996
+ </RechartsLineChart>
997
+ </ResponsiveContainer>
998
+ </div>
999
+ );
1000
+ },
1001
+ );
1002
+
1003
+ LineChart.displayName = "LineChart";
1004
+
1005
+ export { LineChart, type LineChartEventProps, type TooltipProps };