@spider-analyzer/timeline 4.0.2 → 5.0.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/README.md +275 -637
  3. package/dist/index.d.mts +132 -0
  4. package/dist/index.d.ts +132 -0
  5. package/dist/index.js +2913 -22
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +2906 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/timeline-EX2XZ6IP.css +139 -0
  10. package/package.json +48 -15
  11. package/src/Cursor.jsx +5 -13
  12. package/src/TimeLine.tsx +994 -0
  13. package/src/TimeLineResizer.jsx +2 -8
  14. package/src/ToolTip.jsx +7 -7
  15. package/src/cursorElements/CursorIcon.jsx +6 -29
  16. package/src/cursorElements/CursorSelection.jsx +4 -19
  17. package/src/cursorElements/DragOverlay.jsx +2 -12
  18. package/src/cursorElements/LeftHandle.jsx +3 -19
  19. package/src/cursorElements/LeftToolTip.jsx +2 -7
  20. package/src/cursorElements/RightHandle.jsx +3 -19
  21. package/src/cursorElements/RightToolTip.jsx +4 -13
  22. package/src/cursorElements/ZoomIn.jsx +5 -25
  23. package/src/cursorElements/ZoomOut.jsx +4 -21
  24. package/src/cursorElements/utils.js +1 -1
  25. package/src/index.js +6 -0
  26. package/src/index.ts +158 -0
  27. package/src/moment-shim.ts +169 -0
  28. package/src/styles.ts +15 -0
  29. package/src/time.ts +52 -0
  30. package/src/timeLineElements/Button.jsx +5 -30
  31. package/src/timeLineElements/HistoToolTip.jsx +3 -17
  32. package/src/timeLineElements/Histogram.jsx +4 -16
  33. package/src/timeLineElements/Legend.jsx +2 -16
  34. package/src/timeLineElements/QualityLine.jsx +4 -11
  35. package/src/timeLineElements/Tools.jsx +1 -1
  36. package/src/timeLineElements/XAxis.jsx +5 -8
  37. package/src/timeLineElements/XGrid.jsx +3 -7
  38. package/src/timeLineElements/YAxis.jsx +4 -7
  39. package/src/timeLineElements/YGrid.jsx +2 -6
  40. package/src/timeLineElements/axesStyles.jsx +0 -49
  41. package/src/timeline.css +139 -0
  42. package/src/utils.ts +60 -0
  43. package/.babelrc +0 -8
  44. package/.gitlab-ci.yml +0 -27
  45. package/Makefile +0 -20
  46. package/dist/Cursor.js +0 -290
  47. package/dist/TimeLine.js +0 -1173
  48. package/dist/TimeLineResizer.js +0 -70
  49. package/dist/ToolTip.js +0 -43
  50. package/dist/cursorElements/CursorIcon.js +0 -98
  51. package/dist/cursorElements/CursorSelection.js +0 -179
  52. package/dist/cursorElements/DragOverlay.js +0 -168
  53. package/dist/cursorElements/LeftHandle.js +0 -95
  54. package/dist/cursorElements/LeftToolTip.js +0 -70
  55. package/dist/cursorElements/RightHandle.js +0 -95
  56. package/dist/cursorElements/RightToolTip.js +0 -75
  57. package/dist/cursorElements/ZoomIn.js +0 -93
  58. package/dist/cursorElements/ZoomOut.js +0 -67
  59. package/dist/cursorElements/commonStyles.js +0 -28
  60. package/dist/cursorElements/handleHistoHovering.js +0 -79
  61. package/dist/cursorElements/utils.js +0 -30
  62. package/dist/theme.js +0 -59
  63. package/dist/timeLineElements/Button.js +0 -101
  64. package/dist/timeLineElements/HistoToolTip.js +0 -78
  65. package/dist/timeLineElements/Histogram.js +0 -110
  66. package/dist/timeLineElements/Legend.js +0 -70
  67. package/dist/timeLineElements/QualityLine.js +0 -81
  68. package/dist/timeLineElements/Tools.js +0 -115
  69. package/dist/timeLineElements/XAxis.js +0 -76
  70. package/dist/timeLineElements/XGrid.js +0 -47
  71. package/dist/timeLineElements/YAxis.js +0 -60
  72. package/dist/timeLineElements/YGrid.js +0 -46
  73. package/dist/timeLineElements/axesStyles.js +0 -57
  74. package/src/TimeLine.jsx +0 -1158
  75. package/src/cursorElements/commonStyles.js +0 -21
  76. /package/dist/{tipDark.css → tipDark-BQEJ43KY.css} +0 -0
@@ -0,0 +1,994 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import moment from './moment-shim';
10
+ import { scaleLinear, scaleTime } from 'd3-scale';
11
+ import { merge as _merge } from './utils';
12
+ import { round as _round } from './utils';
13
+ import { max as _max } from './utils';
14
+ import { floor as _floor } from './utils';
15
+ import { isEqual as isEqual } from './utils';
16
+ import { isFunction as isFunction } from './utils';
17
+
18
+ import CursorRaw from './Cursor';
19
+ import HistogramRaw from './timeLineElements/Histogram';
20
+ import XAxisRaw from './timeLineElements/XAxis';
21
+ import YAxisRaw from './timeLineElements/YAxis';
22
+ import LegendRaw from './timeLineElements/Legend';
23
+ import ToolsRaw from './timeLineElements/Tools';
24
+ import HistoToolTipDefaultRaw from './timeLineElements/HistoToolTip';
25
+
26
+ // Children are still JSX; their PropTypes-inferred types are too narrow
27
+ // for the live prop surface. Cast to any until they are converted to .tsx.
28
+ const Cursor = CursorRaw as any;
29
+ const Histogram = HistogramRaw as any;
30
+ const XAxis = XAxisRaw as any;
31
+ const YAxis = YAxisRaw as any;
32
+ const Legend = LegendRaw as any;
33
+ const Tools = ToolsRaw as any;
34
+ const HistoToolTipDefault = HistoToolTipDefaultRaw as any;
35
+ import theme, { defaultLabels, defaultQualityScale } from './theme';
36
+
37
+ import QualityLine from './timeLineElements/QualityLine';
38
+ import YGrid from './timeLineElements/YGrid';
39
+ import XGrid from './timeLineElements/XGrid';
40
+
41
+ const marginDefault = { left: 50, right: 45, top: 5, bottom: 5 };
42
+ const xAxisDefault = { height: 20, spaceBetweenTicks: 70, barsBetweenTicks: 8 };
43
+ const defaultZoomOutFactor = 1.25;
44
+
45
+ const defaultTools = {
46
+ slideForward: true,
47
+ slideBackward: true,
48
+ resetTimeline: true,
49
+ gotoNow: true,
50
+ cursor: true,
51
+ zoomIn: true,
52
+ zoomOut: true,
53
+ };
54
+
55
+ export interface TimeLineHandle {
56
+ zoomIn(): void;
57
+ zoomOut(): void;
58
+ shiftTimeLine(delta: number): void;
59
+ }
60
+
61
+ function computeMarginAndWidth(props: any) {
62
+ const { margin, showLegend, metricsDefinition, width } = props;
63
+ const localMargin = { ...marginDefault, ...margin };
64
+ if (showLegend) {
65
+ const maxLength = _max(metricsDefinition.legends.map((s: string) => s.length));
66
+ localMargin.left = _max([5 + (maxLength as number) * 9, localMargin.left]);
67
+ }
68
+ return {
69
+ margin: localMargin,
70
+ histoWidth: width - localMargin.left - localMargin.right,
71
+ };
72
+ }
73
+
74
+ import {
75
+ toMoment,
76
+ toDuration,
77
+ domainToMoments,
78
+ timeSpanToMoments,
79
+ } from './time';
80
+
81
+ // --- Outer shell: boundary normalization + single-domain API --------------
82
+ //
83
+ // Consumers of the new v5 API pass:
84
+ // - `domain: {min, max}` (single, controlled by host)
85
+ // - `onDomainChange(domain)` to be notified of zoom/pan/shift changes
86
+ // - `onTimeSpanChange({start, stop})` instead of onCustomRange
87
+ // - `onLoadHisto({intervalMs, start, end})` as an object
88
+ // - `onLoadDefaultDomain()` returns Domain | Promise<Domain>
89
+ // - native `Date` at every instant, number-ms at every duration
90
+ // - required `timeZone: string` (IANA)
91
+ //
92
+ // Internally the component still runs against a `domains` STACK (for
93
+ // zoom-out undo semantics) and moment-timezone types. The outer wrapper
94
+ // owns the stack as useState and translates in both directions.
95
+
96
+ const momentToDate = (m: any): Date | null =>
97
+ m && m.toDate ? m.toDate() : (m ?? null);
98
+
99
+ const dateDomain = (d: any): { min: Date; max: Date } => ({
100
+ min: momentToDate(d?.min)!,
101
+ max: momentToDate(d?.max)!,
102
+ });
103
+
104
+ const TimeLineInner = forwardRef<TimeLineHandle, any>(function TimeLine(props, ref) {
105
+ const {
106
+ width, height, histo, quality, qualityScale = defaultQualityScale, metricsDefinition,
107
+ classes = {}, rcToolTipPrefixCls = theme.rcToolTipPrefixCls, showLegend = true,
108
+ tools = defaultTools,
109
+ onFormatTimeLegend, onFormatMetricLegend, onResetTime, onFormatTimeToolTips,
110
+ xAxis: xAxisProp = xAxisDefault, yAxis: yAxisProp = {},
111
+ showHistoToolTip,
112
+ HistoToolTip = HistoToolTipDefault,
113
+ } = props;
114
+
115
+ // --- Refs for instance-level mutable data (the class's `this.*`) --------
116
+
117
+ const xAxisRef = useRef<any>(null);
118
+ const timelineElementRef = useRef<SVGSVGElement | null>(null);
119
+
120
+ const autoMoveRef = useRef<ReturnType<typeof setInterval> | null>(null);
121
+ const isAutoMovingRef = useRef(false);
122
+ const lastAutoMovingDeltaRef = useRef(0);
123
+ const movedSinceLastFetchedRef = useRef(0);
124
+ const widthOfLastUpdateRef = useRef<number | null>(null);
125
+ const itemsRef = useRef<any[]>([]);
126
+
127
+ const labelsRef = useRef<any>(_merge({ ...defaultLabels }, props.labels));
128
+ useEffect(() => {
129
+ labelsRef.current = _merge({ ...defaultLabels }, props.labels);
130
+ }, [props.labels]);
131
+
132
+ // --- Reactive state (the class's `this.state`) --------------------------
133
+
134
+ const [state, setStateRaw] = useState<any>(() => {
135
+ const initial: any = {
136
+ isActive: false,
137
+ start: props.timeSpan.start,
138
+ stop: props.timeSpan.stop,
139
+ maxTimespan: false,
140
+ waitForLoad: false,
141
+ tooltipVisible: false,
142
+ barHovered: null,
143
+ dragging: false,
144
+ ticks: [],
145
+ verticalMarks: [],
146
+ ...computeMarginAndWidth(props),
147
+ };
148
+ return {
149
+ ...initial,
150
+ ...checkAndCorrectDomainPure({
151
+ domain: props.domains[0],
152
+ biggestVisibleDomain: props.biggestVisibleDomain,
153
+ maxDomain: props.maxDomain,
154
+ smallestResolution: props.smallestResolution,
155
+ histoWidth: initial.histoWidth,
156
+ }),
157
+ };
158
+ });
159
+
160
+ // Mirrors for closure-free access inside callbacks.
161
+ const stateRef = useRef(state);
162
+ stateRef.current = state;
163
+ const propsRef = useRef(props);
164
+ propsRef.current = props;
165
+
166
+ const patchState = useCallback((patch: any | ((s: any) => any)) => {
167
+ setStateRaw((s: any) => ({ ...s, ...(typeof patch === 'function' ? patch(s) : patch) }));
168
+ }, []);
169
+
170
+ // --- Domain math --------------------------------------------------------
171
+
172
+ const checkAndCorrectDomain = useCallback(({ domain }: { domain: any }) => {
173
+ return checkAndCorrectDomainPure({
174
+ domain,
175
+ biggestVisibleDomain: propsRef.current.biggestVisibleDomain,
176
+ maxDomain: propsRef.current.maxDomain,
177
+ smallestResolution: propsRef.current.smallestResolution,
178
+ histoWidth: stateRef.current.histoWidth,
179
+ });
180
+ }, []);
181
+
182
+ // --- Data loading -------------------------------------------------------
183
+
184
+ const getItems = useCallback((p: any, domain: any, shouldReload = true) => {
185
+ const histoWidth = computeMarginAndWidth(propsRef.current).histoWidth;
186
+
187
+ const xAxis = scaleTime().domain([domain.min, domain.max]).range([0, histoWidth]);
188
+ xAxis.clamp(true);
189
+ xAxisRef.current = xAxis;
190
+
191
+ const ticks = xAxis.ticks(_floor(histoWidth / p.xAxis.spaceBetweenTicks));
192
+ const intervalMs = _max([
193
+ _round(moment(ticks[1]).diff(moment(ticks[0])) / p.xAxis.barsBetweenTicks),
194
+ propsRef.current.smallestResolution.asMilliseconds(),
195
+ ]) as number;
196
+
197
+ patchState({ histoWidth, isActive: true, ticks });
198
+
199
+ if (shouldReload && intervalMs) {
200
+ widthOfLastUpdateRef.current = p.width;
201
+ patchState({ waitForLoad: true });
202
+ p.onLoadHisto(intervalMs, domain.min, domain.max);
203
+ }
204
+ }, [patchState]);
205
+
206
+ // --- Mount: mirror componentDidMount ------------------------------------
207
+
208
+ useEffect(() => {
209
+ if (stateRef.current.domain) {
210
+ getItems(propsRef.current, stateRef.current.domain);
211
+ } else {
212
+ propsRef.current.onLoadDefaultDomain();
213
+ }
214
+ patchState(computeMarginAndWidth(propsRef.current));
215
+ // eslint-disable-next-line react-hooks/exhaustive-deps
216
+ }, []);
217
+
218
+ // --- Effects mirroring componentDidUpdate branches ----------------------
219
+
220
+ // Max selection message
221
+ const prevMaxTimespanRef = useRef(state.maxTimespan);
222
+ useEffect(() => {
223
+ if (!prevMaxTimespanRef.current && state.maxTimespan) {
224
+ propsRef.current.onShowMessage(labelsRef.current.maxSelectionMsg);
225
+ }
226
+ prevMaxTimespanRef.current = state.maxTimespan;
227
+ }, [state.maxTimespan]);
228
+
229
+ // Width / margin change
230
+ const prevWidthRef = useRef<number | undefined>(props.width);
231
+ const prevMarginRef = useRef<any>(props.margin);
232
+ useEffect(() => {
233
+ if (prevWidthRef.current === props.width && isEqual(prevMarginRef.current, props.margin)) {
234
+ prevWidthRef.current = props.width;
235
+ prevMarginRef.current = props.margin;
236
+ return;
237
+ }
238
+ const s = stateRef.current;
239
+ if (s.isActive) {
240
+ const big =
241
+ widthOfLastUpdateRef.current !== null
242
+ && Math.abs((props.width ?? 0) - (widthOfLastUpdateRef.current ?? 0)) > 30;
243
+ getItems(props, s.domain, big);
244
+ patchState(computeMarginAndWidth(props));
245
+ } else {
246
+ patchState(computeMarginAndWidth(props));
247
+ }
248
+
249
+ if (stateRef.current.domain) {
250
+ const res = checkAndCorrectDomain({ domain: stateRef.current.domain });
251
+ const { maxZoom, minZoom, domain, domainHasChanged, minTime, maxTime } = res;
252
+ if (maxZoom !== stateRef.current.maxZoom || minZoom !== stateRef.current.minZoom) {
253
+ patchState({
254
+ maxZoom, minZoom, minTime, maxTime,
255
+ domain: domainHasChanged ? domain : stateRef.current.domain,
256
+ });
257
+ if (domainHasChanged) {
258
+ const newDomains = [domain, ...propsRef.current.domains.slice(1)];
259
+ propsRef.current.onUpdateDomains(newDomains);
260
+ }
261
+ }
262
+ }
263
+ prevWidthRef.current = props.width;
264
+ prevMarginRef.current = props.margin;
265
+ }, [props.width, props.margin, getItems, patchState, checkAndCorrectDomain]);
266
+
267
+ // timeSpan change
268
+ const prevTimeSpanRef = useRef(props.timeSpan);
269
+ useEffect(() => {
270
+ const prev = prevTimeSpanRef.current;
271
+ if (prev.start.isSame(props.timeSpan.start) && prev.stop.isSame(props.timeSpan.stop)) {
272
+ return;
273
+ }
274
+ const newDomains = shiftDomainsPure(props.domains, {
275
+ min: props.timeSpan.start,
276
+ max: props.timeSpan.stop,
277
+ }, props.biggestVisibleDomain);
278
+ if (newDomains !== props.domains) {
279
+ props.onUpdateDomains(newDomains);
280
+ }
281
+ patchState({ start: props.timeSpan.start, stop: props.timeSpan.stop });
282
+ prevTimeSpanRef.current = props.timeSpan;
283
+ }, [props.timeSpan, props.domains, props.biggestVisibleDomain, props, patchState]);
284
+
285
+ // Current domain change
286
+ const prevDomain0Ref = useRef(props.domains[0]);
287
+ useEffect(() => {
288
+ const prev = prevDomain0Ref.current;
289
+ const curr = props.domains[0];
290
+ const changed =
291
+ (!prev && curr) ||
292
+ (prev && curr && (!prev.min.isSame(curr.min) || !prev.max.isSame(curr.max)));
293
+ if (!changed) {
294
+ prevDomain0Ref.current = curr;
295
+ return;
296
+ }
297
+ const res = checkAndCorrectDomain({ domain: curr });
298
+ const { maxZoom, minZoom, domain, domainHasChanged, minTime, maxTime } = res;
299
+ patchState({ maxZoom, minZoom, minTime, maxTime, domain });
300
+ if (domainHasChanged) {
301
+ const newDomains = [domain, ...propsRef.current.domains.slice(1)];
302
+ propsRef.current.onUpdateDomains(newDomains);
303
+ } else {
304
+ getItems(propsRef.current, domain);
305
+ }
306
+ prevDomain0Ref.current = curr;
307
+ }, [props.domains, checkAndCorrectDomain, getItems, patchState]);
308
+
309
+ // histo change
310
+ const prevHistoRef = useRef(props.histo);
311
+ useEffect(() => {
312
+ if (prevHistoRef.current !== props.histo) {
313
+ patchState({ waitForLoad: false });
314
+ prevHistoRef.current = props.histo;
315
+ }
316
+ }, [props.histo, patchState]);
317
+
318
+ // legend change → recompute margin
319
+ const prevLegendsRef = useRef(props.metricsDefinition.legends);
320
+ useEffect(() => {
321
+ if (prevLegendsRef.current !== props.metricsDefinition.legends) {
322
+ patchState(computeMarginAndWidth(propsRef.current));
323
+ prevLegendsRef.current = props.metricsDefinition.legends;
324
+ }
325
+ }, [props.metricsDefinition.legends, patchState]);
326
+
327
+ // --- Prepare vertical scale + histogram (render helpers) ----------------
328
+
329
+ const prepareVerticalScale = useCallback(() => {
330
+ const items = histo && histo.items;
331
+ const s = stateRef.current;
332
+ const maxHeight =
333
+ height - s.margin.bottom - s.margin.top - (xAxisProp.height || xAxisDefault.height);
334
+ const maxScale = items ? (_max(items.map((i: any) => i.total)) as number) || 0 : 0;
335
+ const verticalScale = scaleLinear().domain([0, maxScale]).range([0, maxHeight]);
336
+ const verticalMarks =
337
+ yAxisProp.spaceBetweenTicks && maxHeight > yAxisProp.spaceBetweenTicks * 2
338
+ ? verticalScale.ticks(_floor(maxHeight / yAxisProp.spaceBetweenTicks))
339
+ : [maxScale];
340
+ return { verticalScale, verticalMarks, maxHeight };
341
+ }, [histo, height, xAxisProp, yAxisProp]);
342
+
343
+ const prepareHistogram = useCallback((args: { domain: any; verticalScale: any }) => {
344
+ const { domain, verticalScale } = args;
345
+ const xAxis = xAxisRef.current;
346
+ if (histo && histo.items && domain && xAxis) {
347
+ const { min, max } = domain;
348
+ return histo.items
349
+ .filter((item: any) =>
350
+ item.total > 0 && item.time.isSameOrAfter(min) && item.time.isBefore(max),
351
+ )
352
+ .map((item: any) => {
353
+ const start = moment(item.time);
354
+ const end = moment(item.time).add(histo.intervalMs);
355
+ return {
356
+ x1: xAxis(start),
357
+ x2: xAxis(end),
358
+ start, end,
359
+ metrics: item.metrics,
360
+ total: item.total,
361
+ height: verticalScale(item.total),
362
+ };
363
+ });
364
+ }
365
+ return [];
366
+ }, [histo]);
367
+
368
+ // --- Cursor / drag / zoom handlers (former instance methods) ------------
369
+
370
+ const setAutoMove = useCallback((active: boolean, action?: any, delta?: number) => {
371
+ if (active) {
372
+ if (
373
+ !autoMoveRef.current ||
374
+ (autoMoveRef.current && Math.abs(lastAutoMovingDeltaRef.current - (delta ?? 0)) > 5)
375
+ ) {
376
+ if (autoMoveRef.current) clearInterval(autoMoveRef.current);
377
+ autoMoveRef.current = setInterval(() => {
378
+ action(delta);
379
+ moveTimeLine(-(delta ?? 0));
380
+ isAutoMovingRef.current = true;
381
+ lastAutoMovingDeltaRef.current = delta ?? 0;
382
+ }, 30);
383
+ }
384
+ } else {
385
+ if (autoMoveRef.current) clearInterval(autoMoveRef.current);
386
+ autoMoveRef.current = null;
387
+ }
388
+ // eslint-disable-next-line react-hooks/exhaustive-deps
389
+ }, []);
390
+
391
+ const handleAutoSliding = useCallback((mouse: any, action: any) => {
392
+ if (!mouse) return;
393
+ const posX = mouse[0];
394
+ const s = stateRef.current;
395
+ if (posX < 0) setAutoMove(true, action, posX);
396
+ else if (posX >= 0 && posX <= s.histoWidth) setAutoMove(false);
397
+ else if (posX > s.histoWidth) setAutoMove(true, action, posX - s.histoWidth);
398
+ }, [setAutoMove]);
399
+
400
+ const movedTimeLine = useCallback(() => {
401
+ movedSinceLastFetchedRef.current = 0;
402
+ const domain = stateRef.current.domain;
403
+ const p = propsRef.current;
404
+ const domains = [domain, ...p.domains.slice(1)];
405
+ const newDomains = shiftDomainsPure(domains, domain, p.biggestVisibleDomain);
406
+ p.onUpdateDomains(newDomains);
407
+ }, []);
408
+
409
+ const stopAutoMove = useCallback(() => {
410
+ if (autoMoveRef.current) setAutoMove(false);
411
+ if (isAutoMovingRef.current) {
412
+ isAutoMovingRef.current = false;
413
+ movedTimeLine();
414
+ }
415
+ }, [setAutoMove, movedTimeLine]);
416
+
417
+ const moveTimeLineCore = useCallback((delta: number) => {
418
+ const p = propsRef.current;
419
+ const s = stateRef.current;
420
+ const { maxDomain, onShowMessage } = p;
421
+ let { domain: currentDomain, minTime = false, maxTime = false } = s;
422
+ const currentSpan = currentDomain.max.diff(currentDomain.min);
423
+
424
+ if ((minTime && delta > 0) || (maxTime && delta < 0)) {
425
+ return { ...currentDomain, moved: 0, minTime, maxTime };
426
+ }
427
+ const xAxis = xAxisRef.current;
428
+ let min = moment(xAxis.invert(-delta));
429
+ let max = moment(xAxis.invert(s.histoWidth - delta));
430
+ let moved = delta;
431
+
432
+ if (maxDomain.min && min.isBefore(maxDomain.min)) {
433
+ moved = xAxis(maxDomain.min);
434
+ min = moment(maxDomain.min);
435
+ max = moment(min).add(currentSpan);
436
+ minTime = true;
437
+ onShowMessage(labelsRef.current.minDomainMsg);
438
+ }
439
+ if (maxDomain.max && max.isAfter(maxDomain.max)) {
440
+ moved = xAxis(maxDomain.max);
441
+ max = moment(maxDomain.max);
442
+ min = moment(max).subtract(currentSpan);
443
+ maxTime = true;
444
+ onShowMessage(labelsRef.current.maxDomainMsg);
445
+ }
446
+ return { min, max, moved, minTime, maxTime };
447
+ }, []);
448
+
449
+ const moveTimeLine = useCallback((delta: number) => {
450
+ const xAxis = xAxisRef.current;
451
+ xAxis.clamp(false);
452
+ const { min, max, moved, minTime, maxTime } = moveTimeLineCore(delta);
453
+ if (moved !== 0) {
454
+ xAxis.domain([min, max]);
455
+ xAxis.clamp(true);
456
+ const p = propsRef.current;
457
+ const s = stateRef.current;
458
+ const ticks = xAxis.ticks(_floor(s.histoWidth / p.xAxis.spaceBetweenTicks));
459
+ movedSinceLastFetchedRef.current += moved;
460
+ if (Math.abs(movedSinceLastFetchedRef.current) > 75 && p.fetchWhileSliding) {
461
+ movedSinceLastFetchedRef.current = 0;
462
+ getItems(p, { min, max });
463
+ }
464
+ patchState({ domain: { min, max }, minTime, maxTime, ticks });
465
+ }
466
+ }, [moveTimeLineCore, getItems, patchState]);
467
+
468
+ const onResizeLeftCursor = useCallback((delta: number, mouse: any) => {
469
+ const xAxis = xAxisRef.current;
470
+ const s = stateRef.current;
471
+ const p = propsRef.current;
472
+ let newStart = moment(xAxis.invert(xAxis(s.start) + delta));
473
+ let newStop = s.stop;
474
+ let maxTimespan = false;
475
+ if (newStart.isSameOrAfter(newStop)) {
476
+ newStop = moment(xAxis.invert(xAxis(newStop) + delta));
477
+ }
478
+ if (p.biggestTimeSpan) {
479
+ const min = moment(newStop).subtract(p.biggestTimeSpan);
480
+ if (min.isSameOrAfter(newStart)) {
481
+ newStart = min;
482
+ maxTimespan = true;
483
+ }
484
+ }
485
+ handleAutoSliding(mouse, onResizeLeftCursor);
486
+ if (
487
+ (newStop !== s.stop && newStop.isSameOrBefore(s.domain.max)) ||
488
+ newStart.isSameOrAfter(s.domain.min)
489
+ ) {
490
+ patchState({ start: newStart, stop: newStop, maxTimespan });
491
+ }
492
+ // eslint-disable-next-line react-hooks/exhaustive-deps
493
+ }, [handleAutoSliding, patchState]);
494
+
495
+ const onResizeRightCursor = useCallback((delta: number, mouse: any) => {
496
+ const xAxis = xAxisRef.current;
497
+ const s = stateRef.current;
498
+ const p = propsRef.current;
499
+ let newStop = moment(xAxis.invert(xAxis(s.stop) + delta));
500
+ let newStart = s.start;
501
+ let maxTimespan = false;
502
+ if (newStop.isSameOrBefore(newStart)) {
503
+ newStart = moment(xAxis.invert(xAxis(newStart) + delta));
504
+ }
505
+ if (p.biggestTimeSpan) {
506
+ const max = moment(newStart).add(p.biggestTimeSpan);
507
+ if (max.isSameOrBefore(newStop)) {
508
+ newStop = max;
509
+ maxTimespan = true;
510
+ }
511
+ }
512
+ handleAutoSliding(mouse, onResizeRightCursor);
513
+ if (newStop.isSameOrBefore(s.domain.max) && newStart.isSameOrAfter(s.domain.min)) {
514
+ patchState({ start: newStart, stop: newStop, maxTimespan });
515
+ }
516
+ // eslint-disable-next-line react-hooks/exhaustive-deps
517
+ }, [handleAutoSliding, patchState]);
518
+
519
+ const onDragCursor = useCallback((delta: number, mouse: any) => {
520
+ const xAxis = xAxisRef.current;
521
+ const s = stateRef.current;
522
+ const p = propsRef.current;
523
+ xAxis.clamp(false);
524
+ const duration = s.stop.diff(s.start);
525
+ let newStart = moment(xAxis.invert(xAxis(s.start) + delta));
526
+ let newStop = moment(newStart).add(duration, 'milliseconds');
527
+ xAxis.clamp(true);
528
+ const { maxDomain } = p;
529
+ if (maxDomain.min && newStart.isBefore(maxDomain.min)) newStart = moment(maxDomain.min);
530
+ if (maxDomain.max && newStop.isAfter(maxDomain.max)) newStop = moment(maxDomain.max);
531
+ handleAutoSliding(mouse, onDragCursor);
532
+ patchState({ start: newStart, stop: newStop });
533
+ // eslint-disable-next-line react-hooks/exhaustive-deps
534
+ }, [handleAutoSliding, patchState]);
535
+
536
+ const onStartDrawCursor = useCallback((pos: number) => {
537
+ const xAxis = xAxisRef.current;
538
+ patchState({
539
+ start: moment(xAxis.invert(pos)),
540
+ stop: moment(xAxis.invert(pos + 1)),
541
+ maxTimespan: false,
542
+ });
543
+ }, [patchState]);
544
+
545
+ const onDrawCursor = useCallback((delta: number, mouse: any) => {
546
+ const xAxis = xAxisRef.current;
547
+ const s = stateRef.current;
548
+ const p = propsRef.current;
549
+ let newStop = moment(xAxis.invert(xAxis(s.stop) + delta));
550
+ let newStart = s.start;
551
+ let maxTimespan = false;
552
+ if (newStop.isSameOrBefore(newStart)) {
553
+ newStart = moment(xAxis.invert(xAxis(newStart) + delta));
554
+ }
555
+ if (p.biggestTimeSpan) {
556
+ const max = moment(newStart).add(p.biggestTimeSpan);
557
+ if (max.isSameOrBefore(newStop)) {
558
+ newStop = max;
559
+ maxTimespan = true;
560
+ }
561
+ }
562
+ handleAutoSliding(mouse, onDrawCursor);
563
+ if (newStop.isSameOrBefore(s.domain.max) && newStart.isSameOrAfter(s.domain.min)) {
564
+ patchState({ start: newStart, stop: newStop, maxTimespan });
565
+ }
566
+ // eslint-disable-next-line react-hooks/exhaustive-deps
567
+ }, [handleAutoSliding, patchState]);
568
+
569
+ const onEndDrawCursor = useCallback(() => {
570
+ stopAutoMove();
571
+ const { onCustomRange, selectBarOnClick } = propsRef.current;
572
+ const s = stateRef.current;
573
+ let { start, stop, barHovered } = s;
574
+ if (selectBarOnClick && itemsRef.current && barHovered > -1) {
575
+ const item = itemsRef.current[barHovered];
576
+ if (start.isSameOrAfter(item.start) && stop.isSameOrBefore(item.end)) {
577
+ start = moment(item.start);
578
+ stop = moment(item.end);
579
+ patchState({ start, stop });
580
+ }
581
+ }
582
+ onCustomRange(start, stop);
583
+ }, [stopAutoMove, patchState]);
584
+
585
+ const onEndChangeCursor = useCallback(() => {
586
+ stopAutoMove();
587
+ const { onCustomRange } = propsRef.current;
588
+ const { start, stop } = stateRef.current;
589
+ onCustomRange(start, stop);
590
+ }, [stopAutoMove]);
591
+
592
+ const zoomOnDomain = useCallback((targetDomain: any) => {
593
+ const p = propsRef.current;
594
+ const s = stateRef.current;
595
+ if (s.waitForLoad) return;
596
+ if (targetDomain.min.isSame(s.domain.min) && targetDomain.max.isSame(s.domain.max)) {
597
+ p.onShowMessage(labelsRef.current.zoomInWithoutChangingSelectionMsg);
598
+ return;
599
+ }
600
+ const { maxZoom, domain } = checkAndCorrectDomain({ domain: targetDomain });
601
+ const { maxZoom: currentMaxZoom } = s;
602
+ if (maxZoom && maxZoom !== currentMaxZoom) {
603
+ p.onShowMessage(labelsRef.current.zoomSelectionResolutionExtended);
604
+ }
605
+ const domains = [domain, ...p.domains];
606
+ patchState({ maxZoom, minZoom: false, domain });
607
+ p.onUpdateDomains(domains);
608
+ }, [checkAndCorrectDomain, patchState]);
609
+
610
+ const zoomOutTo = useCallback((domains: any[]) => {
611
+ const res = checkAndCorrectDomain({ domain: domains[0] });
612
+ const { minZoom, domain, domainHasChanged, maxTime, minTime } = res;
613
+ const p = propsRef.current;
614
+ if (minZoom) p.onShowMessage(labelsRef.current.minZoomMsg);
615
+ if (minTime) p.onShowMessage(labelsRef.current.minDomainMsg);
616
+ if (maxTime) p.onShowMessage(labelsRef.current.maxDomainMsg);
617
+ const newDomains = domainHasChanged ? [domain, ...p.domains.slice(1)] : domains;
618
+ patchState({ maxZoom: false, minZoom, domain });
619
+ p.onUpdateDomains(newDomains);
620
+ }, [checkAndCorrectDomain, patchState]);
621
+
622
+ const zoomOnTarget = useCallback((x: number) => {
623
+ const xAxis = xAxisRef.current;
624
+ const [minOrigin, maxOrigin] = xAxis.domain();
625
+ const target = moment(xAxis.invert(x));
626
+ const min = moment(target).subtract(0.25 * target.diff(moment(minOrigin)));
627
+ const max = moment(target).add(0.25 * moment(maxOrigin).diff(target));
628
+ zoomOnDomain({ min, max });
629
+ }, [zoomOnDomain]);
630
+
631
+ const zoomOutOfTarget = useCallback((x: number) => {
632
+ const xAxis = xAxisRef.current;
633
+ const [minOrigin, maxOrigin] = xAxis.domain();
634
+ const target = moment(xAxis.invert(x));
635
+ const min = moment(target).subtract(1.25 * target.diff(moment(minOrigin)));
636
+ const max = moment(target).add(1.25 * moment(maxOrigin).diff(target));
637
+ zoomOutTo([{ min, max }]);
638
+ }, [zoomOutTo]);
639
+
640
+ const zoomIn = useCallback(() => {
641
+ const s = stateRef.current;
642
+ if (!s.maxZoom) {
643
+ zoomOnDomain({ min: s.start, max: s.stop });
644
+ } else {
645
+ propsRef.current.onShowMessage(labelsRef.current.doubleClickMaxZoomMsg);
646
+ }
647
+ }, [zoomOnDomain]);
648
+
649
+ const zoomOut = useCallback(() => {
650
+ const s = stateRef.current;
651
+ if (s.waitForLoad || s.minZoom) return;
652
+ const p = propsRef.current;
653
+ const domains = [...p.domains];
654
+ if (domains.length > 1) {
655
+ domains.shift();
656
+ } else {
657
+ const xAxis = xAxisRef.current;
658
+ const [minOrigin, maxOrigin] = xAxis.domain();
659
+ const halfDuration = moment(maxOrigin).diff(moment(minOrigin)) / 2;
660
+ const target = moment(minOrigin).add(halfDuration);
661
+ const zoomOutFactor = p.zoomOutFactor ?? defaultZoomOutFactor;
662
+ const min = moment(target).subtract(zoomOutFactor * halfDuration);
663
+ const max = moment(target).add(zoomOutFactor * halfDuration);
664
+ domains[0] = { min, max };
665
+ }
666
+ zoomOutTo(domains);
667
+ }, [zoomOutTo]);
668
+
669
+ const onWheel = useCallback((event: any) => {
670
+ const s = stateRef.current;
671
+ const p = propsRef.current;
672
+ event.stopPropagation();
673
+ const baseX = timelineElementRef.current?.getBoundingClientRect().left ?? 0;
674
+ const x = event.clientX - baseX - s.margin.left;
675
+ if (s.waitForLoad) return;
676
+ if (event.deltaY < -50) {
677
+ if (s.maxZoom) p.onShowMessage(labelsRef.current.scrollMaxZoomMsg);
678
+ else zoomOnTarget(x);
679
+ } else if (event.deltaY > 50) {
680
+ if (s.minZoom) p.onShowMessage(labelsRef.current.minZoomMsg);
681
+ else if (p.domains.length > 1) zoomOut();
682
+ else zoomOutOfTarget(x);
683
+ }
684
+ }, [zoomOnTarget, zoomOut, zoomOutOfTarget]);
685
+
686
+ const onGotoCursor = useCallback(() => {
687
+ const s = stateRef.current;
688
+ const p = propsRef.current;
689
+ const middle = moment(s.start).add(s.stop.diff(s.start) / 2);
690
+ const xAxis = xAxisRef.current;
691
+ xAxis.clamp(false);
692
+ const currentX = s.histoWidth / 2;
693
+ const newX = xAxis(middle);
694
+ const newDomain = moveTimeLineCore(currentX - newX);
695
+ const domains = [...p.domains];
696
+ domains.splice(0, 1, newDomain);
697
+ const newDomains = shiftDomainsPure(domains, newDomain, p.biggestVisibleDomain);
698
+ p.onUpdateDomains(newDomains);
699
+ xAxis.clamp(true);
700
+ }, [moveTimeLineCore]);
701
+
702
+ const onGoto = useCallback((d: any) => {
703
+ const s = stateRef.current;
704
+ const p = propsRef.current;
705
+ const halfTimeAgg = s.stop.diff(s.start) / 2;
706
+ const when = d || moment();
707
+ const newStop = moment(when).add(halfTimeAgg);
708
+ const newStart = moment(when).subtract(halfTimeAgg);
709
+ p.onCustomRange(newStart, newStop);
710
+ }, []);
711
+
712
+ const shiftTimeLine = useCallback((delta: number) => {
713
+ const incr = (delta / Math.abs(delta)) * 8;
714
+ let i = 0;
715
+ function step() {
716
+ moveTimeLine(incr);
717
+ i += 8;
718
+ if (i < Math.abs(delta)) setTimeout(step, 10);
719
+ else movedTimeLine();
720
+ }
721
+ step();
722
+ }, [moveTimeLine, movedTimeLine]);
723
+
724
+ // --- Cleanup on unmount -------------------------------------------------
725
+
726
+ useEffect(() => () => {
727
+ if (autoMoveRef.current) clearInterval(autoMoveRef.current);
728
+ }, []);
729
+
730
+ // --- Imperative handle --------------------------------------------------
731
+
732
+ useImperativeHandle(ref, () => ({ zoomIn, zoomOut, shiftTimeLine }), [zoomIn, zoomOut, shiftTimeLine]);
733
+
734
+ // --- Render -------------------------------------------------------------
735
+
736
+ const { isActive, domain, histoWidth, margin, start, stop, maxZoom, minZoom,
737
+ maxTimespan, barHovered, tooltipVisible, ticks, dragging } = state;
738
+
739
+ const xAxisHeight = xAxisProp.height || xAxisDefault.height;
740
+ const pointZero = { x: 0, y: height - margin.bottom - xAxisHeight };
741
+ const originCursor = { x: 0, y: margin.top - 5 };
742
+
743
+ if (!isFunction(xAxisRef.current)) return null;
744
+
745
+ const { verticalScale, verticalMarks, maxHeight } = prepareVerticalScale();
746
+ const items = prepareHistogram({ domain, verticalScale });
747
+ itemsRef.current = items;
748
+
749
+ return (
750
+ <svg
751
+ ref={timelineElementRef}
752
+ width={width}
753
+ height={height}
754
+ onWheel={isActive ? onWheel : undefined}
755
+ >
756
+ <g transform={`translate(${margin.left},${margin.top})`}>
757
+ {yAxisProp.showGrid && (
758
+ <YGrid classes={classes} origin={pointZero} yAxis={verticalScale}
759
+ marks={verticalMarks} xAxisWidth={histoWidth + 13} />
760
+ )}
761
+ {xAxisProp.showGrid && (
762
+ <XGrid classes={classes} origin={pointZero} xAxis={xAxisRef.current}
763
+ marks={ticks} yAxisHeight={maxHeight} />
764
+ )}
765
+ {isActive && (
766
+ <Histogram classes={classes} origin={pointZero} items={items} histo={histo}
767
+ verticalScale={verticalScale} metricsDefinition={metricsDefinition}
768
+ barHovered={barHovered} dragging={dragging}
769
+ tooltipVisible={tooltipVisible}
770
+ onFormatTimeToolTips={onFormatTimeToolTips}
771
+ onFormatMetricLegend={onFormatMetricLegend}
772
+ HistoToolTip={HistoToolTip} />
773
+ )}
774
+ <XAxis classes={classes} origin={pointZero} axisWidth={histoWidth + 13}
775
+ min={domain && domain.min} max={domain && domain.max}
776
+ xAxis={xAxisRef.current} marks={ticks}
777
+ arrowPath={xAxisProp.arrowPath} yAxisHeight={maxHeight}
778
+ showGrid={xAxisProp.showGrid} onFormatTimeLegend={onFormatTimeLegend} />
779
+ <Tools classes={classes} rcToolTipPrefixCls={rcToolTipPrefixCls} origin={pointZero}
780
+ histoWidth={histoWidth} isActive={isActive} tools={tools}
781
+ labels={labelsRef.current} onGoto={onGoto}
782
+ onResetTime={onResetTime} shiftTimeLine={shiftTimeLine} />
783
+ <YAxis classes={classes} origin={pointZero} maxHeight={maxHeight}
784
+ yAxis={verticalScale} marks={verticalMarks}
785
+ arrowPath={yAxisProp.arrowPath} showGrid={yAxisProp.showGrid}
786
+ xAxisWidth={histoWidth + 13} onFormatMetricLegend={onFormatMetricLegend} />
787
+ {showLegend && (
788
+ <Legend classes={classes}
789
+ origin={{ x: -5, y: margin.top + 2 }}
790
+ metricsDefinition={metricsDefinition} />
791
+ )}
792
+ {isActive && tools.cursor !== false && (
793
+ <Cursor
794
+ origin={originCursor} rcToolTipPrefixCls={rcToolTipPrefixCls} classes={classes}
795
+ startPos={xAxisRef.current(start)}
796
+ startIsOutOfView={start.isBefore(domain.min) || start.isAfter(domain.max)}
797
+ endPos={xAxisRef.current(stop)}
798
+ endIsOutOfView={stop.isBefore(domain.min) || stop.isAfter(domain.max)}
799
+ cursorIsBeforeView={start.isBefore(domain.min) && stop.isBefore(domain.min)}
800
+ cursorIsAfterView={start.isAfter(domain.max) && stop.isAfter(domain.max)}
801
+ height={maxHeight} overlayHeight={height - margin.top} overlayWidth={histoWidth}
802
+ tooltipVisible={tooltipVisible} barHovered={barHovered}
803
+ setToolTipVisible={(v: boolean) => patchState({ tooltipVisible: v })}
804
+ setBarHovered={(b: any) => patchState({ barHovered: b })}
805
+ items={items} showHistoToolTip={showHistoToolTip}
806
+ dragging={dragging}
807
+ setDragging={(d: any) => patchState({ dragging: d })}
808
+ canZoom={true} minZoom={minZoom} maxZoom={maxZoom}
809
+ startText={onFormatTimeToolTips(start)} stopText={onFormatTimeToolTips(stop)}
810
+ maxSize={maxTimespan} tools={tools}
811
+ gotoCursorLabel={labelsRef.current.gotoCursor}
812
+ zoomInLabel={labelsRef.current.zoomInLabel}
813
+ zoomOutLabel={labelsRef.current.zoomOutLabel}
814
+ zoomIn={zoomIn} zoomOut={zoomOut}
815
+ onResizeLeftCursor={onResizeLeftCursor}
816
+ onResizeRightCursor={onResizeRightCursor}
817
+ onEndResizeCursor={onEndChangeCursor}
818
+ onDragCursor={onDragCursor} onEndDragCursor={onEndChangeCursor}
819
+ onStartDrawCursor={onStartDrawCursor}
820
+ onDrawCursor={onDrawCursor} onEndCursor={onEndDrawCursor}
821
+ onMoveDomain={moveTimeLine} onMovedDomain={movedTimeLine}
822
+ onGotoCursor={onGotoCursor}
823
+ />
824
+ )}
825
+ {isActive && quality && quality.items && (
826
+ <QualityLine classes={classes} rcToolTipPrefixCls={rcToolTipPrefixCls}
827
+ origin={pointZero} quality={quality} qualityScale={qualityScale}
828
+ xAxis={xAxisRef.current} />
829
+ )}
830
+ </g>
831
+ </svg>
832
+ );
833
+ });
834
+
835
+ const TimeLine = forwardRef<TimeLineHandle, any>(function TimeLineWrapper(props, ref) {
836
+ const zone: string | undefined = props.timeZone;
837
+
838
+ // Internal zoom-history stack (kept as moment-typed for the inner).
839
+ // Initial stack: single entry from props.domain (or empty if null).
840
+ const [stack, setStack] = useState<any[]>(() =>
841
+ props.domain ? [domainToMoments(props.domain, zone)] : []
842
+ );
843
+
844
+ // If the host updates `domain` (e.g. via URL / redux), sync the top of
845
+ // the stack — same identity if equal, replaced otherwise.
846
+ const prevDomainRef = useRef<any>(props.domain);
847
+ useEffect(() => {
848
+ if (prevDomainRef.current === props.domain) return;
849
+ prevDomainRef.current = props.domain;
850
+ if (!props.domain) {
851
+ setStack([]);
852
+ return;
853
+ }
854
+ const m = domainToMoments(props.domain, zone);
855
+ setStack((prev) => {
856
+ if (prev.length === 0) return [m];
857
+ const top = prev[0];
858
+ if (top.min.isSame(m.min) && top.max.isSame(m.max)) return prev;
859
+ return [m, ...prev.slice(1)];
860
+ });
861
+ }, [props.domain, zone]);
862
+
863
+ // onUpdateDomains is the inner's single source of truth for domain mutation.
864
+ // Intercept it, persist into `stack`, and report the top to the host.
865
+ const handleUpdateDomains = useCallback((newDomains: any[]) => {
866
+ setStack(newDomains);
867
+ const top = newDomains[0];
868
+ if (top && props.onDomainChange) {
869
+ props.onDomainChange({ min: top.min.toDate(), max: top.max.toDate() });
870
+ }
871
+ }, [props.onDomainChange]);
872
+
873
+ // onLoadDefaultDomain may now return Domain | Promise<Domain>.
874
+ // If it does, seed the stack + inform the host.
875
+ const handleLoadDefault = useCallback(() => {
876
+ const ret = props.onLoadDefaultDomain?.();
877
+ const apply = (d: any) => {
878
+ if (!d) return;
879
+ const m = domainToMoments(d, zone);
880
+ setStack([m]);
881
+ if (props.onDomainChange) {
882
+ props.onDomainChange({ min: m.min.toDate(), max: m.max.toDate() });
883
+ }
884
+ };
885
+ if (ret && typeof (ret as any).then === 'function') (ret as Promise<any>).then(apply);
886
+ else if (ret) apply(ret);
887
+ }, [props.onLoadDefaultDomain, props.onDomainChange, zone]);
888
+
889
+ // Build the moment-typed props the inner expects.
890
+ const innerProps: any = {
891
+ ...props,
892
+ domains: stack,
893
+ timeSpan: props.timeSpan ? timeSpanToMoments(props.timeSpan, zone) : undefined,
894
+ maxDomain: props.maxDomain ? domainToMoments(props.maxDomain, zone) : undefined,
895
+ histo: props.histo?.items
896
+ ? { ...props.histo, items: props.histo.items.map((it: any) => ({ ...it, time: toMoment(it.time, zone) })) }
897
+ : props.histo,
898
+ quality: props.quality?.items
899
+ ? { ...props.quality, items: props.quality.items.map((it: any) => ({ ...it, time: toMoment(it.time, zone) })) }
900
+ : props.quality,
901
+ biggestVisibleDomain: props.biggestVisibleDomain != null ? toDuration(props.biggestVisibleDomain) : undefined,
902
+ biggestTimeSpan: props.biggestTimeSpan != null ? toDuration(props.biggestTimeSpan) : undefined,
903
+ smallestResolution: props.smallestResolution != null ? toDuration(props.smallestResolution) : undefined,
904
+ onUpdateDomains: handleUpdateDomains,
905
+ onLoadDefaultDomain: handleLoadDefault,
906
+ // onLoadHisto: object shape + Date instants
907
+ onLoadHisto: props.onLoadHisto
908
+ ? (ms: number, start: any, end: any) =>
909
+ props.onLoadHisto({ intervalMs: ms, start: momentToDate(start)!, end: momentToDate(end)! })
910
+ : undefined,
911
+ // onCustomRange → onTimeSpanChange, object shape
912
+ onCustomRange: props.onTimeSpanChange
913
+ ? (start: any, stop: any) =>
914
+ props.onTimeSpanChange({ start: momentToDate(start)!, stop: momentToDate(stop)! })
915
+ : props.onCustomRange,
916
+ onFormatTimeToolTips: props.onFormatTimeToolTips
917
+ ? (t: any) => props.onFormatTimeToolTips(momentToDate(t))
918
+ : undefined,
919
+ onFormatTimeLegend: props.onFormatTimeLegend
920
+ ? (t: any) => props.onFormatTimeLegend(momentToDate(t))
921
+ : undefined,
922
+ };
923
+
924
+ return <TimeLineInner {...innerProps} ref={ref} />;
925
+ });
926
+
927
+ export default TimeLine;
928
+
929
+ // --- Pure helpers (extracted so they can be called from constructor+runtime) ---
930
+
931
+ function checkAndCorrectDomainPure({
932
+ domain, biggestVisibleDomain, maxDomain, smallestResolution, histoWidth,
933
+ }: any) {
934
+ if (!domain) {
935
+ return { domain, maxZoom: false, minZoom: false, domainHasChanged: false };
936
+ }
937
+ const newDomain = { ...domain };
938
+ let domainHasChanged = false;
939
+ const duration = newDomain.max.diff(newDomain.min);
940
+ let maxZoom = false;
941
+ const minRes = smallestResolution.asMilliseconds();
942
+ if (duration <= (histoWidth * minRes) / 15) {
943
+ maxZoom = true;
944
+ newDomain.max = moment(newDomain.min).add((histoWidth * minRes) / 15, 'ms');
945
+ }
946
+ let minTime: boolean | undefined;
947
+ let maxTime: boolean | undefined;
948
+ if (maxDomain?.min && newDomain.min.isBefore(maxDomain.min)) {
949
+ newDomain.min = moment(maxDomain.min);
950
+ minTime = true;
951
+ }
952
+ if (maxDomain?.max && newDomain.max.isAfter(maxDomain.max)) {
953
+ newDomain.max = moment(maxDomain.max);
954
+ maxTime = true;
955
+ }
956
+ let minZoom = false;
957
+ if (biggestVisibleDomain && newDomain.max.isSameOrAfter(moment(newDomain.min).add(biggestVisibleDomain))) {
958
+ minZoom = true;
959
+ newDomain.min = moment(newDomain.max).subtract(biggestVisibleDomain);
960
+ }
961
+ if (!(newDomain.min.isSame(domain.min) && newDomain.max.isSame(domain.max))) {
962
+ domainHasChanged = true;
963
+ }
964
+ return {
965
+ domain: domainHasChanged ? newDomain : domain,
966
+ maxZoom, minZoom, maxTime, minTime, domainHasChanged,
967
+ };
968
+ }
969
+
970
+ function shiftDomainsPure(domains: any[], { min, max }: any, biggestVisibleDomain: any) {
971
+ let toUpdate = false;
972
+ const newDomains = domains.map((domain, index) => {
973
+ if ((min && min.isBefore(domain.min)) || (max && max.isAfter(domain.max))) {
974
+ toUpdate = true;
975
+ if (index === domains.length - 1) {
976
+ const newDomain: any = {
977
+ min: min && min.isBefore(domain.min) ? min : domain.min,
978
+ max: max && max.isAfter(domain.max) ? max : domain.max,
979
+ };
980
+ if (biggestVisibleDomain && moment(newDomain.min).add(biggestVisibleDomain).isSameOrBefore(newDomain.max)) {
981
+ if (min.isBefore(domain.min)) newDomain.max = moment(min).add(biggestVisibleDomain);
982
+ else newDomain.min = moment(max).subtract(biggestVisibleDomain);
983
+ }
984
+ return newDomain;
985
+ }
986
+ if (min && min.isBefore(domain.min)) {
987
+ return { min, max: moment(domain.max).subtract(domain.min.diff(min)) };
988
+ }
989
+ return { min: moment(domain.min).add(max.diff(domain.max)), max };
990
+ }
991
+ return domain;
992
+ });
993
+ return toUpdate ? newDomains : domains;
994
+ }