@spider-analyzer/timeline 5.0.6 → 5.0.9
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 +36 -0
- package/dist/index.js +61 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/Cursor.jsx +3 -3
- package/src/TimeLine.tsx +36 -13
- package/src/cursorElements/CursorSelection.jsx +3 -3
- package/src/cursorElements/DragOverlay.jsx +3 -3
- package/src/moment-shim.ts +25 -0
- package/src/timeLineElements/HistoToolTip.jsx +3 -3
- package/src/timeLineElements/Histogram.jsx +7 -5
- package/src/timeLineElements/QualityLine.jsx +2 -2
- package/src/timeLineElements/XAxis.jsx +7 -5
- package/src/timeLineElements/XGrid.jsx +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spider-analyzer/timeline",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.9",
|
|
4
4
|
"description": "React graphical component to display metric over time with a time selection feature.",
|
|
5
5
|
"author": "Thibaut Raballand <spider.analyzer@gmail.com> (https://spider-analyzer.io)",
|
|
6
6
|
"license": "MIT",
|
package/src/Cursor.jsx
CHANGED
|
@@ -12,7 +12,7 @@ import RightHandle from './cursorElements/RightHandle';
|
|
|
12
12
|
import ZoomIn from './cursorElements/ZoomIn';
|
|
13
13
|
import LeftToolTip from './cursorElements/LeftToolTip';
|
|
14
14
|
import RightToolTip from './cursorElements/RightToolTip';
|
|
15
|
-
import moment from './moment-shim';
|
|
15
|
+
import moment, { momentType } from './moment-shim';
|
|
16
16
|
|
|
17
17
|
export default function Cursor({
|
|
18
18
|
overlayWidth, overlayHeight,
|
|
@@ -209,8 +209,8 @@ Cursor.propTypes = {
|
|
|
209
209
|
overlayHeight: PropTypes.number.isRequired,
|
|
210
210
|
overlayWidth: PropTypes.number.isRequired,
|
|
211
211
|
items: PropTypes.arrayOf(PropTypes.shape({
|
|
212
|
-
start:
|
|
213
|
-
end:
|
|
212
|
+
start: momentType.isRequired,
|
|
213
|
+
end: momentType.isRequired,
|
|
214
214
|
x1: PropTypes.number,
|
|
215
215
|
x2: PropTypes.number,
|
|
216
216
|
metrics: PropTypes.arrayOf(PropTypes.number).isRequired,
|
package/src/TimeLine.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
useRef,
|
|
8
8
|
useState,
|
|
9
9
|
} from 'react';
|
|
10
|
+
import { flushSync } from 'react-dom';
|
|
10
11
|
import moment from './moment-shim';
|
|
11
12
|
import { scaleLinear, scaleTime } from 'd3-scale';
|
|
12
13
|
import { merge as _merge } from './utils';
|
|
@@ -166,6 +167,18 @@ const TimeLineInner = forwardRef<TimeLineHandle, any>(function TimeLine(props, r
|
|
|
166
167
|
|
|
167
168
|
const patchState = useCallback((patch: any | ((s: any) => any)) => {
|
|
168
169
|
setStateRaw((s: any) => ({ ...s, ...(typeof patch === 'function' ? patch(s) : patch) }));
|
|
170
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
// Synchronous variant for drag/resize hot paths. React 18 normally batches
|
|
174
|
+
// state updates from d3-drag's native listeners to the next scheduler tick,
|
|
175
|
+
// so the cursor edge trails the mouse by ~1 frame. flushSync forces the
|
|
176
|
+
// render within the same event, matching the pre-hooks React 17 behavior
|
|
177
|
+
// where the cursor border tracked the pointer precisely.
|
|
178
|
+
const patchStateSync = useCallback((patch: any | ((s: any) => any)) => {
|
|
179
|
+
flushSync(() => {
|
|
180
|
+
setStateRaw((s: any) => ({ ...s, ...(typeof patch === 'function' ? patch(s) : patch) }));
|
|
181
|
+
});
|
|
169
182
|
}, []);
|
|
170
183
|
|
|
171
184
|
// --- Domain math --------------------------------------------------------
|
|
@@ -483,9 +496,9 @@ const TimeLineInner = forwardRef<TimeLineHandle, any>(function TimeLine(props, r
|
|
|
483
496
|
movedSinceLastFetchedRef.current = 0;
|
|
484
497
|
getItems(p, { min, max });
|
|
485
498
|
}
|
|
486
|
-
|
|
499
|
+
patchStateSync({ domain: { min, max }, minTime, maxTime, ticks });
|
|
487
500
|
}
|
|
488
|
-
}, [moveTimeLineCore, getItems,
|
|
501
|
+
}, [moveTimeLineCore, getItems, patchStateSync]);
|
|
489
502
|
|
|
490
503
|
const onResizeLeftCursor = useCallback((delta: number, mouse: any) => {
|
|
491
504
|
const xAxis = xAxisRef.current;
|
|
@@ -509,10 +522,10 @@ const TimeLineInner = forwardRef<TimeLineHandle, any>(function TimeLine(props, r
|
|
|
509
522
|
(newStop !== s.stop && newStop.isSameOrBefore(s.domain.max)) ||
|
|
510
523
|
newStart.isSameOrAfter(s.domain.min)
|
|
511
524
|
) {
|
|
512
|
-
|
|
525
|
+
patchStateSync({ start: newStart, stop: newStop, maxTimespan });
|
|
513
526
|
}
|
|
514
527
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
515
|
-
}, [handleAutoSliding,
|
|
528
|
+
}, [handleAutoSliding, patchStateSync]);
|
|
516
529
|
|
|
517
530
|
const onResizeRightCursor = useCallback((delta: number, mouse: any) => {
|
|
518
531
|
const xAxis = xAxisRef.current;
|
|
@@ -533,10 +546,10 @@ const TimeLineInner = forwardRef<TimeLineHandle, any>(function TimeLine(props, r
|
|
|
533
546
|
}
|
|
534
547
|
handleAutoSliding(mouse, onResizeRightCursor);
|
|
535
548
|
if (newStop.isSameOrBefore(s.domain.max) && newStart.isSameOrAfter(s.domain.min)) {
|
|
536
|
-
|
|
549
|
+
patchStateSync({ start: newStart, stop: newStop, maxTimespan });
|
|
537
550
|
}
|
|
538
551
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
539
|
-
}, [handleAutoSliding,
|
|
552
|
+
}, [handleAutoSliding, patchStateSync]);
|
|
540
553
|
|
|
541
554
|
const onDragCursor = useCallback((delta: number, mouse: any) => {
|
|
542
555
|
const xAxis = xAxisRef.current;
|
|
@@ -551,9 +564,9 @@ const TimeLineInner = forwardRef<TimeLineHandle, any>(function TimeLine(props, r
|
|
|
551
564
|
if (maxDomain.min && newStart.isBefore(maxDomain.min)) newStart = moment(maxDomain.min);
|
|
552
565
|
if (maxDomain.max && newStop.isAfter(maxDomain.max)) newStop = moment(maxDomain.max);
|
|
553
566
|
handleAutoSliding(mouse, onDragCursor);
|
|
554
|
-
|
|
567
|
+
patchStateSync({ start: newStart, stop: newStop });
|
|
555
568
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
556
|
-
}, [handleAutoSliding,
|
|
569
|
+
}, [handleAutoSliding, patchStateSync]);
|
|
557
570
|
|
|
558
571
|
const onStartDrawCursor = useCallback((pos: number) => {
|
|
559
572
|
const xAxis = xAxisRef.current;
|
|
@@ -583,10 +596,10 @@ const TimeLineInner = forwardRef<TimeLineHandle, any>(function TimeLine(props, r
|
|
|
583
596
|
}
|
|
584
597
|
handleAutoSliding(mouse, onDrawCursor);
|
|
585
598
|
if (newStop.isSameOrBefore(s.domain.max) && newStart.isSameOrAfter(s.domain.min)) {
|
|
586
|
-
|
|
599
|
+
patchStateSync({ start: newStart, stop: newStop, maxTimespan });
|
|
587
600
|
}
|
|
588
601
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
589
|
-
}, [handleAutoSliding,
|
|
602
|
+
}, [handleAutoSliding, patchStateSync]);
|
|
590
603
|
|
|
591
604
|
const onEndDrawCursor = useCallback(() => {
|
|
592
605
|
stopAutoMove();
|
|
@@ -611,6 +624,7 @@ const TimeLineInner = forwardRef<TimeLineHandle, any>(function TimeLine(props, r
|
|
|
611
624
|
onCustomRange(start, stop);
|
|
612
625
|
}, [stopAutoMove]);
|
|
613
626
|
|
|
627
|
+
|
|
614
628
|
const zoomOnDomain = useCallback((targetDomain: any) => {
|
|
615
629
|
const p = propsRef.current;
|
|
616
630
|
const s = stateRef.current;
|
|
@@ -758,15 +772,24 @@ const TimeLineInner = forwardRef<TimeLineHandle, any>(function TimeLine(props, r
|
|
|
758
772
|
const { isActive, domain, histoWidth, margin, start, stop, maxZoom, minZoom,
|
|
759
773
|
maxTimespan, barHovered, tooltipVisible, ticks, dragging } = state;
|
|
760
774
|
|
|
775
|
+
// Memoize the expensive per-render bits so cursor drag (which flips
|
|
776
|
+
// only state.start/state.stop 60x/sec) doesn't rebuild the histogram
|
|
777
|
+
// bar list on every event. Under the Luxon-backed shim, rebuilding
|
|
778
|
+
// ~550 bars costs ~150 ms/render — enough to tank drag framerate.
|
|
779
|
+
const verticalScaleData = useMemo(() => prepareVerticalScale(), [prepareVerticalScale]);
|
|
780
|
+
const items = useMemo(
|
|
781
|
+
() => prepareHistogram({ domain, verticalScale: verticalScaleData.verticalScale }),
|
|
782
|
+
[prepareHistogram, domain, verticalScaleData, histoWidth],
|
|
783
|
+
);
|
|
784
|
+
itemsRef.current = items;
|
|
785
|
+
|
|
761
786
|
const xAxisHeight = xAxisProp.height || xAxisDefault.height;
|
|
762
787
|
const pointZero = { x: 0, y: height - margin.bottom - xAxisHeight };
|
|
763
788
|
const originCursor = { x: 0, y: margin.top - 5 };
|
|
764
789
|
|
|
765
790
|
if (!isFunction(xAxisRef.current)) return null;
|
|
766
791
|
|
|
767
|
-
const { verticalScale, verticalMarks, maxHeight } =
|
|
768
|
-
const items = prepareHistogram({ domain, verticalScale });
|
|
769
|
-
itemsRef.current = items;
|
|
792
|
+
const { verticalScale, verticalMarks, maxHeight } = verticalScaleData;
|
|
770
793
|
|
|
771
794
|
return (
|
|
772
795
|
<svg
|
|
@@ -5,7 +5,7 @@ import {pointer, select} from 'd3-selection';
|
|
|
5
5
|
import {drag} from 'd3-drag';
|
|
6
6
|
|
|
7
7
|
import clsx from 'clsx';
|
|
8
|
-
import moment from '../moment-shim';
|
|
8
|
+
import moment, { momentType } from '../moment-shim';
|
|
9
9
|
import {handleHistoOver, handleHistoOut} from './handleHistoHovering';
|
|
10
10
|
|
|
11
11
|
|
|
@@ -106,8 +106,8 @@ CursorSelection.propTypes = {
|
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
items: PropTypes.arrayOf(PropTypes.shape({
|
|
109
|
-
start:
|
|
110
|
-
end:
|
|
109
|
+
start: momentType.isRequired,
|
|
110
|
+
end: momentType.isRequired,
|
|
111
111
|
x1: PropTypes.number,
|
|
112
112
|
x2: PropTypes.number,
|
|
113
113
|
metrics: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
@@ -2,7 +2,7 @@ import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import {pointer, select} from 'd3-selection';
|
|
4
4
|
import {drag} from 'd3-drag';
|
|
5
|
-
import moment from '../moment-shim';
|
|
5
|
+
import moment, { momentType } from '../moment-shim';
|
|
6
6
|
|
|
7
7
|
import {handleHistoOver, handleHistoOut} from './handleHistoHovering';
|
|
8
8
|
|
|
@@ -104,8 +104,8 @@ DragOverlay.propTypes = {
|
|
|
104
104
|
marginBottom: PropTypes.number,
|
|
105
105
|
|
|
106
106
|
items: PropTypes.arrayOf(PropTypes.shape({
|
|
107
|
-
start:
|
|
108
|
-
end:
|
|
107
|
+
start: momentType.isRequired,
|
|
108
|
+
end: momentType.isRequired,
|
|
109
109
|
x1: PropTypes.number,
|
|
110
110
|
x2: PropTypes.number,
|
|
111
111
|
metrics: PropTypes.arrayOf(PropTypes.number).isRequired,
|
package/src/moment-shim.ts
CHANGED
|
@@ -165,5 +165,30 @@ moment.tz = (x: any, zone: string): MomentLike => {
|
|
|
165
165
|
return new MomentLike(base.setZone(zone));
|
|
166
166
|
};
|
|
167
167
|
|
|
168
|
+
// PropTypes helper — the library's internal .jsx components use
|
|
169
|
+
// `PropTypes.instanceOf(moment)` which doesn't work against the shim
|
|
170
|
+
// (moment is the factory function, not a class). Use this instead:
|
|
171
|
+
// time: momentType
|
|
172
|
+
// time: momentType.isRequired
|
|
173
|
+
function validate(props: Record<string, any>, propName: string, componentName: string): Error | null {
|
|
174
|
+
const v = props[propName];
|
|
175
|
+
if (v == null) return null;
|
|
176
|
+
if (moment.isMoment(v)) return null;
|
|
177
|
+
return new Error(
|
|
178
|
+
`Invalid prop \`${propName}\` supplied to \`${componentName}\`, expected a moment-like object.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const required = (props: Record<string, any>, propName: string, componentName: string): Error | null => {
|
|
182
|
+
const v = props[propName];
|
|
183
|
+
if (v == null) {
|
|
184
|
+
return new Error(
|
|
185
|
+
`The prop \`${propName}\` is marked as required in \`${componentName}\`, but its value is \`${v}\`.`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return validate(props, propName, componentName);
|
|
189
|
+
};
|
|
190
|
+
export const momentType: any = validate;
|
|
191
|
+
momentType.isRequired = required;
|
|
192
|
+
|
|
168
193
|
export default moment;
|
|
169
194
|
export type Moment = MomentLike;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from "../styles";
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
|
-
import moment from '../moment-shim';
|
|
4
|
+
import moment, { momentType } from '../moment-shim';
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
7
|
|
|
@@ -50,8 +50,8 @@ export default function HistoTooltip({metricsDefinition, item, onFormatTimeToolT
|
|
|
50
50
|
HistoTooltip.propTypes = {
|
|
51
51
|
classes: PropTypes.object,
|
|
52
52
|
item: PropTypes.shape({
|
|
53
|
-
start:
|
|
54
|
-
end:
|
|
53
|
+
start: momentType.isRequired,
|
|
54
|
+
end: momentType.isRequired,
|
|
55
55
|
x1: PropTypes.number,
|
|
56
56
|
x2: PropTypes.number,
|
|
57
57
|
metrics: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { cn } from "../styles";
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import moment from '../moment-shim';
|
|
2
|
+
import React, { memo } from 'react';
|
|
3
|
+
import moment, { momentType } from '../moment-shim';
|
|
4
4
|
import { sum as _sum } from '../utils';
|
|
5
5
|
import PropTypes from 'prop-types';
|
|
6
6
|
import clsx from 'clsx';
|
|
7
7
|
import ToolTip from '../ToolTip';
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
function Histogram({
|
|
11
11
|
classes, items, metricsDefinition, origin,
|
|
12
12
|
barHovered, tooltipVisible, verticalScale, dragging,
|
|
13
13
|
HistoToolTip, onFormatMetricLegend, onFormatTimeToolTips
|
|
@@ -72,8 +72,8 @@ export default function Histogram({
|
|
|
72
72
|
Histogram.propTypes = {
|
|
73
73
|
classes: PropTypes.object,
|
|
74
74
|
items: PropTypes.arrayOf(PropTypes.shape({
|
|
75
|
-
start:
|
|
76
|
-
end:
|
|
75
|
+
start: momentType.isRequired,
|
|
76
|
+
end: momentType.isRequired,
|
|
77
77
|
x1: PropTypes.number,
|
|
78
78
|
x2: PropTypes.number,
|
|
79
79
|
metrics: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
@@ -99,3 +99,5 @@ Histogram.propTypes = {
|
|
|
99
99
|
barHovered: PropTypes.number,
|
|
100
100
|
tooltipVisible: PropTypes.bool,
|
|
101
101
|
};
|
|
102
|
+
|
|
103
|
+
export default memo(Histogram);
|
|
@@ -3,7 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
|
|
6
|
-
import moment from '../moment-shim';
|
|
6
|
+
import moment, { momentType } from '../moment-shim';
|
|
7
7
|
import ToolTip from '../ToolTip';
|
|
8
8
|
|
|
9
9
|
const qualityHeight = 3;
|
|
@@ -64,7 +64,7 @@ QualityLine.propTypes = {
|
|
|
64
64
|
xAxis: PropTypes.func.isRequired,
|
|
65
65
|
quality: PropTypes.shape({
|
|
66
66
|
items: PropTypes.arrayOf(PropTypes.shape({
|
|
67
|
-
time:
|
|
67
|
+
time: momentType.isRequired,
|
|
68
68
|
quality: PropTypes.number.isRequired,
|
|
69
69
|
tip: PropTypes.node
|
|
70
70
|
})),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import moment from '../moment-shim';
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
import moment, { momentType } from '../moment-shim';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import {timeDay} from 'd3-time';
|
|
5
5
|
import clsx from 'clsx';
|
|
@@ -7,7 +7,7 @@ import clsx from 'clsx';
|
|
|
7
7
|
import { arrow } from "./axesStyles";
|
|
8
8
|
import { cn } from "../styles";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
function XAxis({min, max, origin, axisWidth, marks, xAxis, classes, arrowPath = arrow, onFormatTimeLegend}){
|
|
11
11
|
|
|
12
12
|
const now = moment();
|
|
13
13
|
|
|
@@ -69,8 +69,8 @@ export default function XAxis({min, max, origin, axisWidth, marks, xAxis, classe
|
|
|
69
69
|
XAxis.propTypes = {
|
|
70
70
|
axisWidth:PropTypes.number.isRequired,
|
|
71
71
|
classes: PropTypes.object,
|
|
72
|
-
min:
|
|
73
|
-
max:
|
|
72
|
+
min: momentType,
|
|
73
|
+
max: momentType,
|
|
74
74
|
origin: PropTypes.shape({
|
|
75
75
|
x: PropTypes.number.isRequired,
|
|
76
76
|
y: PropTypes.number.isRequired
|
|
@@ -81,3 +81,5 @@ XAxis.propTypes = {
|
|
|
81
81
|
onFormatTimeLegend: PropTypes.func.isRequired,
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
+
export default memo(XAxis);
|
|
85
|
+
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
2
|
import moment from '../moment-shim';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
5
|
|
|
6
6
|
import { cn } from "../styles";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function XAxis({origin, marks, xAxis, classes, yAxisHeight}){
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
const className = (n) => cn(n, classes);
|
|
@@ -42,3 +42,5 @@ XAxis.propTypes = {
|
|
|
42
42
|
yAxisHeight: PropTypes.number,
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
export default memo(XAxis);
|
|
46
|
+
|