@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spider-analyzer/timeline",
3
- "version": "5.0.6",
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: PropTypes.instanceOf(moment).isRequired,
213
- end: PropTypes.instanceOf(moment).isRequired,
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
- patchState({ domain: { min, max }, minTime, maxTime, ticks });
499
+ patchStateSync({ domain: { min, max }, minTime, maxTime, ticks });
487
500
  }
488
- }, [moveTimeLineCore, getItems, patchState]);
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
- patchState({ start: newStart, stop: newStop, maxTimespan });
525
+ patchStateSync({ start: newStart, stop: newStop, maxTimespan });
513
526
  }
514
527
  // eslint-disable-next-line react-hooks/exhaustive-deps
515
- }, [handleAutoSliding, patchState]);
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
- patchState({ start: newStart, stop: newStop, maxTimespan });
549
+ patchStateSync({ start: newStart, stop: newStop, maxTimespan });
537
550
  }
538
551
  // eslint-disable-next-line react-hooks/exhaustive-deps
539
- }, [handleAutoSliding, patchState]);
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
- patchState({ start: newStart, stop: newStop });
567
+ patchStateSync({ start: newStart, stop: newStop });
555
568
  // eslint-disable-next-line react-hooks/exhaustive-deps
556
- }, [handleAutoSliding, patchState]);
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
- patchState({ start: newStart, stop: newStop, maxTimespan });
599
+ patchStateSync({ start: newStart, stop: newStop, maxTimespan });
587
600
  }
588
601
  // eslint-disable-next-line react-hooks/exhaustive-deps
589
- }, [handleAutoSliding, patchState]);
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 } = prepareVerticalScale();
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: PropTypes.instanceOf(moment).isRequired,
110
- end: PropTypes.instanceOf(moment).isRequired,
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: PropTypes.instanceOf(moment).isRequired,
108
- end: PropTypes.instanceOf(moment).isRequired,
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,
@@ -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: PropTypes.instanceOf(moment).isRequired,
54
- end: PropTypes.instanceOf(moment).isRequired,
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
- export default function Histogram({
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: PropTypes.instanceOf(moment).isRequired,
76
- end: PropTypes.instanceOf(moment).isRequired,
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: PropTypes.instanceOf(moment).isRequired,
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
- export default function XAxis({min, max, origin, axisWidth, marks, xAxis, classes, arrowPath = arrow, onFormatTimeLegend}){
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: PropTypes.instanceOf(moment),
73
- max: PropTypes.instanceOf(moment),
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
- export default function XAxis({origin, marks, xAxis, classes, yAxisHeight}){
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
+