Skip to content

Commit 1b59618

Browse files
committed
Refactor ChartCanvas: replace getTooltipPosition/viewportTop with selectedItemTooltipOffset
Replace the getTooltipPosition callback and viewportTop props with a single pre-computed selectedItemTooltipOffset prop. The parent component (MarkerChartCanvas) now owns the full tooltip position calculation including the viewport bounds check, since it has direct access to all the layout data. This simplifies ChartCanvas by removing internal details it didn't need to know about: - _syncSelectedItemFromProp no longer calls a callback or checks viewport bounds; - The three separate componentDidUpdate trigger conditions collapse into one; - viewportTop is no longer leaked as a prop. Also fix scroll-to-selected-marker on load when the viewport resizes after the initial layout (e.g. flex containers settling), by re-running _scrollSelectionIntoView when containerHeight changes.
1 parent c4152c4 commit 1b59618

File tree

2 files changed

+80
-89
lines changed

2 files changed

+80
-89
lines changed

src/components/marker-chart/Canvas.tsx

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,16 @@ class MarkerChartCanvasImpl extends React.PureComponent<Props> {
148148
_textMeasurement: TextMeasurement | null = null;
149149

150150
override componentDidUpdate(prevProps: Props) {
151-
// When the viewport finishes sizing itself, or when the selected marker changes,
152-
// scroll to bring the selected marker into view (e.g. on initial load from URL).
153151
const viewportDidMount =
154152
!prevProps.viewport.isSizeSet && this.props.viewport.isSizeSet;
153+
const viewportResized =
154+
this.props.viewport.isSizeSet &&
155+
this.props.viewport.containerHeight !==
156+
prevProps.viewport.containerHeight;
155157
const selectedMarkerChanged =
156158
this.props.selectedMarkerIndex !== prevProps.selectedMarkerIndex;
157159

158-
if (viewportDidMount || selectedMarkerChanged) {
160+
if (viewportDidMount || viewportResized || selectedMarkerChanged) {
159161
this._scrollSelectionIntoView();
160162
}
161163
}
@@ -997,31 +999,40 @@ class MarkerChartCanvasImpl extends React.PureComponent<Props> {
997999
};
9981000

9991001
/**
1000-
* Calculate the canvas position for a marker's tooltip based on the marker's index.
1001-
* The method is used when the selected marker comes from the URL and does NOT
1002-
* necessarily correspond to what's being hovered at the moment.
1003-
*
1004-
* @param markerIndex - The marker's index in the original thread's marker table
1005-
* @returns Canvas-relative coordinates {offsetX, offsetY} or null if marker not visible
1002+
* Compute canvas-relative tooltip offset for the selected marker.
1003+
* Returns null if the marker can't be located in the timing data or
1004+
* is outside the visible viewport.
10061005
*/
1007-
getTooltipPosition = (
1008-
markerIndex: MarkerIndex
1009-
): { offsetX: CssPixels; offsetY: CssPixels } | null => {
1006+
_getSelectedItemTooltipOffset(): {
1007+
offsetX: CssPixels;
1008+
offsetY: CssPixels;
1009+
} | null {
1010+
const { selectedMarkerIndex } = this.props;
1011+
if (selectedMarkerIndex === null) {
1012+
return null;
1013+
}
1014+
10101015
const {
10111016
rangeStart,
10121017
rangeEnd,
10131018
markerTimingAndBuckets,
10141019
rowHeight,
10151020
marginLeft,
10161021
marginRight,
1017-
viewport: { containerWidth, viewportLeft, viewportRight, viewportTop },
1022+
viewport: {
1023+
containerWidth,
1024+
containerHeight,
1025+
viewportLeft,
1026+
viewportRight,
1027+
viewportTop,
1028+
},
10181029
} = this.props;
10191030

10201031
// Step 1: Find which row this marker is displayed in
10211032
const markerIndexToTimingRow = this._getMarkerIndexToTimingRow(
10221033
markerTimingAndBuckets
10231034
);
1024-
const rowIndex = markerIndexToTimingRow[markerIndex];
1035+
const rowIndex = markerIndexToTimingRow[selectedMarkerIndex];
10251036

10261037
// Step 2: Get the timing data for all markers in this row
10271038
const markerTiming = markerTimingAndBuckets[rowIndex];
@@ -1033,22 +1044,20 @@ class MarkerChartCanvasImpl extends React.PureComponent<Props> {
10331044
// Step 3: Find the position of our specific marker within this row's data
10341045
let markerTimingIndex = -1;
10351046
for (let i = 0; i < markerTiming.length; i++) {
1036-
if (markerTiming.index[i] === markerIndex) {
1047+
if (markerTiming.index[i] === selectedMarkerIndex) {
10371048
markerTimingIndex = i;
10381049
break;
10391050
}
10401051
}
10411052

10421053
if (markerTimingIndex === -1) {
1043-
// Marker not found in this row's data (shouldn't happen, but handle gracefully)
10441054
return null;
10451055
}
10461056

10471057
// Step 4: Calculate horizontal (X) position
10481058
const startTimestamp = markerTiming.start[markerTimingIndex];
10491059
const endTimestamp = markerTiming.end[markerTimingIndex];
10501060

1051-
// Convert absolute timestamps to relative positions (0.0 to 1.0 of the full range)
10521061
const markerContainerWidth = containerWidth - marginLeft - marginRight;
10531062
const rangeLength: Milliseconds = rangeEnd - rangeStart;
10541063
const viewportLength: UnitIntervalOfProfileRange =
@@ -1058,29 +1067,29 @@ class MarkerChartCanvasImpl extends React.PureComponent<Props> {
10581067
const endTime: UnitIntervalOfProfileRange =
10591068
(endTimestamp - rangeStart) / rangeLength;
10601069

1061-
// Calculate pixel position: map the time range to the visible viewport
10621070
const x: CssPixels =
10631071
((startTime - viewportLeft) * markerContainerWidth) / viewportLength +
10641072
marginLeft;
10651073
const w: CssPixels =
10661074
((endTime - startTime) * markerContainerWidth) / viewportLength;
10671075

1068-
// For instant markers (start === end), use the center point
1069-
// For interval markers, use a point 1/3 into the marker (or 30px, whichever is smaller)
1076+
// For instant markers (start === end), use the center point.
1077+
// For interval markers, use a point 1/3 into the marker (or 30px, whichever is smaller).
10701078
const isInstantMarker = startTimestamp === endTimestamp;
10711079
const offsetX = isInstantMarker ? x : x + Math.min(w / 3, 30);
10721080

10731081
// Step 5: Calculate vertical (Y) position
1074-
// Place the tooltip at the top of the marker's row, with a small offset
10751082
const offsetY: CssPixels = rowIndex * rowHeight - viewportTop + 5;
10761083

1077-
// Return canvas-relative coordinates (should be converted to page coordinates by caller)
1084+
if (offsetY < 0 || offsetY > containerHeight) {
1085+
return null;
1086+
}
1087+
10781088
return { offsetX, offsetY };
1079-
};
1089+
}
10801090

10811091
override render() {
1082-
const { containerWidth, containerHeight, isDragging, viewportTop } =
1083-
this.props.viewport;
1092+
const { containerWidth, containerHeight, isDragging } = this.props.viewport;
10841093
const { selectedMarkerIndex } = this.props;
10851094

10861095
return (
@@ -1100,8 +1109,7 @@ class MarkerChartCanvasImpl extends React.PureComponent<Props> {
11001109
onMouseLeave={this.onMouseLeave}
11011110
stickyTooltips={true}
11021111
selectedItem={selectedMarkerIndex}
1103-
getTooltipPosition={this.getTooltipPosition}
1104-
viewportTop={viewportTop}
1112+
selectedItemTooltipOffset={this._getSelectedItemTooltipOffset()}
11051113
/>
11061114
);
11071115
}

src/components/shared/chart/Canvas.tsx

Lines changed: 45 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ type Props<Item> = {
3535
readonly stickyTooltips?: boolean;
3636
// Selected item coming from URL or other non-click mechanism.
3737
readonly selectedItem?: Item | null;
38-
// Method to compute tooltip position for the selected item above.
39-
readonly getTooltipPosition?: (
40-
item: Item
41-
) => { offsetX: CssPixels; offsetY: CssPixels } | null;
42-
// Current vertical scroll offset of the viewport.
43-
readonly viewportTop?: CssPixels;
38+
// Pre-computed canvas-relative offset for the tooltip of the selected item
39+
// (null when the item is not currently visible in the viewport).
40+
readonly selectedItemTooltipOffset?: {
41+
offsetX: CssPixels;
42+
offsetY: CssPixels;
43+
} | null;
4444
};
4545

4646
// The naming of the X and Y coordinates here correspond to the ones
@@ -367,51 +367,36 @@ export class ChartCanvas<Item> extends React.Component<
367367
this._canvas = canvas;
368368
};
369369

370-
_syncSelectedItemFromProp = (selectedItem: Item | null) => {
371-
if (selectedItem === null) {
372-
this.setState({ selectedItem: null });
373-
return;
374-
}
370+
_syncSelectedItemFromProp = () => {
371+
const { selectedItem, selectedItemTooltipOffset } = this.props;
375372

376-
if (!this.props.getTooltipPosition || !this._canvas) {
373+
if (selectedItem === undefined || selectedItem === null) {
374+
this.setState({ selectedItem: null });
377375
return;
378376
}
379377

380-
const tooltipPosition = this.props.getTooltipPosition(selectedItem);
381-
if (!tooltipPosition) {
382-
// Can't get position, but still set the selectedItem
378+
if (!selectedItemTooltipOffset || !this._canvas) {
379+
// Keep selectedItem set, so the tooltip stays at
380+
// its last known position rather than disappearing.
383381
this.setState({ selectedItem });
384382
return;
385383
}
386384

387-
const { offsetX, offsetY } = tooltipPosition;
388-
389-
// If the item is outside the visible canvas area, skip setting the tooltip
390-
// position for now. The viewport will scroll to bring it into view, and the
391-
// viewportTop change will trigger a re-sync with the correct position.
392-
if (offsetY < 0 || offsetY > this.props.containerHeight) {
393-
return;
394-
}
395-
385+
const { offsetX, offsetY } = selectedItemTooltipOffset;
396386
const canvasRect = this._canvas.getBoundingClientRect();
397387
const pageX = canvasRect.left + window.scrollX + offsetX;
398388
const pageY = canvasRect.top + window.scrollY + offsetY;
399-
this.setState({
400-
selectedItem,
401-
pageX,
402-
pageY,
403-
});
389+
this.setState({ selectedItem, pageX, pageY });
404390
};
405391

406392
override componentDidMount() {
407-
// Initialize selectedItem from props on mount if provided and the canvas
408-
// already has dimensions. If containerWidth is still 0, the viewport hasn't
409-
// been laid out yet - componentDidUpdate will pick it up once it becomes non-zero.
393+
// If the viewport hasn't been laid out yet,
394+
// componentDidUpdate will pick it up once it becomes non-zero.
410395
if (
411396
this.props.selectedItem !== undefined &&
412397
this.props.containerWidth !== 0
413398
) {
414-
this._syncSelectedItemFromProp(this.props.selectedItem);
399+
this._syncSelectedItemFromProp();
415400
}
416401
}
417402

@@ -433,35 +418,33 @@ export class ChartCanvas<Item> extends React.Component<
433418

434419
override componentDidUpdate(prevProps: Props<Item>, prevState: State<Item>) {
435420
if (prevProps !== this.props) {
436-
// Sync selectedItem state with selectedItem prop if it changed
437-
// and the state doesn't already have this item selected.
438-
if (
439-
this.props.selectedItem !== undefined &&
440-
this.props.selectedItem !== prevProps.selectedItem &&
441-
!hoveredItemsAreEqual(this.state.selectedItem, this.props.selectedItem)
442-
) {
443-
this._syncSelectedItemFromProp(this.props.selectedItem);
444-
}
445-
446-
// The canvas just received its dimensions for the first time (containerWidth
447-
// went from 0 to non-zero). If a selectedItem was provided but couldn't be
448-
// synced in componentDidMount, do it now.
449-
if (
450-
prevProps.containerWidth === 0 &&
451-
this.props.containerWidth !== 0 &&
452-
this.props.selectedItem !== undefined
453-
) {
454-
this._syncSelectedItemFromProp(this.props.selectedItem);
455-
}
456-
457-
// The viewport scrolled (e.g. to bring the selected item into view on
458-
// load). Re-sync the tooltip position so it stays on the selected item.
459-
if (
460-
this.props.viewportTop !== undefined &&
461-
this.props.viewportTop !== prevProps.viewportTop &&
462-
this.props.selectedItem !== undefined
463-
) {
464-
this._syncSelectedItemFromProp(this.props.selectedItem);
421+
if (this.props.selectedItem !== undefined) {
422+
const selectedItemChanged =
423+
this.props.selectedItem !== prevProps.selectedItem;
424+
const canvasJustGotSize =
425+
prevProps.containerWidth === 0 && this.props.containerWidth !== 0;
426+
const offsetChanged =
427+
this.props.selectedItemTooltipOffset !==
428+
prevProps.selectedItemTooltipOffset;
429+
430+
const alreadySetInternally =
431+
selectedItemChanged &&
432+
hoveredItemsAreEqual(
433+
this.state.selectedItem,
434+
this.props.selectedItem
435+
);
436+
437+
const offsetSyncRelevant =
438+
offsetChanged &&
439+
!selectedItemChanged &&
440+
this.state.selectedItem !== null;
441+
442+
if (
443+
!alreadySetInternally &&
444+
(selectedItemChanged || canvasJustGotSize || offsetSyncRelevant)
445+
) {
446+
this._syncSelectedItemFromProp();
447+
}
465448
}
466449

467450
if (

0 commit comments

Comments
 (0)