@spider-analyzer/timeline 4.0.3 → 5.0.1
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.
- package/CHANGELOG.md +80 -1
- package/README.md +275 -637
- package/dist/index.d.mts +132 -0
- package/dist/index.d.ts +132 -0
- package/dist/index.js +2913 -22
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2906 -0
- package/dist/index.mjs.map +1 -0
- package/dist/timeline.css +139 -0
- package/package.json +52 -15
- package/src/Cursor.jsx +5 -13
- package/src/TimeLine.tsx +994 -0
- package/src/TimeLineResizer.jsx +2 -8
- package/src/ToolTip.jsx +7 -7
- package/src/cursorElements/CursorIcon.jsx +6 -29
- package/src/cursorElements/CursorSelection.jsx +4 -19
- package/src/cursorElements/DragOverlay.jsx +2 -12
- package/src/cursorElements/LeftHandle.jsx +3 -19
- package/src/cursorElements/LeftToolTip.jsx +2 -7
- package/src/cursorElements/RightHandle.jsx +3 -19
- package/src/cursorElements/RightToolTip.jsx +4 -13
- package/src/cursorElements/ZoomIn.jsx +5 -25
- package/src/cursorElements/ZoomOut.jsx +4 -21
- package/src/cursorElements/utils.js +1 -1
- package/src/index.js +6 -0
- package/src/index.ts +158 -0
- package/src/moment-shim.ts +169 -0
- package/src/styles.ts +15 -0
- package/src/time.ts +52 -0
- package/src/timeLineElements/Button.jsx +5 -30
- package/src/timeLineElements/HistoToolTip.jsx +3 -17
- package/src/timeLineElements/Histogram.jsx +4 -16
- package/src/timeLineElements/Legend.jsx +2 -16
- package/src/timeLineElements/QualityLine.jsx +4 -11
- package/src/timeLineElements/Tools.jsx +1 -1
- package/src/timeLineElements/XAxis.jsx +5 -8
- package/src/timeLineElements/XGrid.jsx +3 -7
- package/src/timeLineElements/YAxis.jsx +4 -7
- package/src/timeLineElements/YGrid.jsx +2 -6
- package/src/timeLineElements/axesStyles.jsx +0 -49
- package/src/timeline.css +139 -0
- package/src/utils.ts +60 -0
- package/.babelrc +0 -8
- package/.gitlab-ci.yml +0 -27
- package/Makefile +0 -20
- package/dist/Cursor.js +0 -290
- package/dist/TimeLine.js +0 -1177
- package/dist/TimeLineResizer.js +0 -70
- package/dist/ToolTip.js +0 -43
- package/dist/cursorElements/CursorIcon.js +0 -98
- package/dist/cursorElements/CursorSelection.js +0 -179
- package/dist/cursorElements/DragOverlay.js +0 -168
- package/dist/cursorElements/LeftHandle.js +0 -95
- package/dist/cursorElements/LeftToolTip.js +0 -70
- package/dist/cursorElements/RightHandle.js +0 -95
- package/dist/cursorElements/RightToolTip.js +0 -75
- package/dist/cursorElements/ZoomIn.js +0 -93
- package/dist/cursorElements/ZoomOut.js +0 -67
- package/dist/cursorElements/commonStyles.js +0 -28
- package/dist/cursorElements/handleHistoHovering.js +0 -79
- package/dist/cursorElements/utils.js +0 -30
- package/dist/theme.js +0 -59
- package/dist/timeLineElements/Button.js +0 -101
- package/dist/timeLineElements/HistoToolTip.js +0 -78
- package/dist/timeLineElements/Histogram.js +0 -110
- package/dist/timeLineElements/Legend.js +0 -70
- package/dist/timeLineElements/QualityLine.js +0 -81
- package/dist/timeLineElements/Tools.js +0 -115
- package/dist/timeLineElements/XAxis.js +0 -76
- package/dist/timeLineElements/XGrid.js +0 -47
- package/dist/timeLineElements/YAxis.js +0 -60
- package/dist/timeLineElements/YGrid.js +0 -46
- package/dist/timeLineElements/axesStyles.js +0 -57
- package/src/TimeLine.jsx +0 -1163
- package/src/cursorElements/commonStyles.js +0 -21
package/src/TimeLine.tsx
ADDED
|
@@ -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
|
+
}
|