Skip to content

Commit

Permalink
Add a popover menu to the AreaGraph component
Browse files Browse the repository at this point in the history
  • Loading branch information
yomete committed Dec 5, 2024
1 parent 2e1089b commit f1cbb9a
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const MetricsTooltip = ({
const highlightedNameLabel: Label = nameLabel !== undefined ? nameLabel : {name: '', value: ''};

return (
<div ref={setPopperElement} style={styles.popper} {...attributes.popper} className="z-10">
<div ref={setPopperElement} style={styles.popper} {...attributes.popper} className="z-20">
<div className="flex max-w-md">
<div className="m-auto">
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2022 The Parca Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {useEffect, useRef, useState} from 'react';

import {useParcaContext} from '@parca/components';
import {formatDate, timePattern, valueFormatter} from '@parca/utilities';

interface TooltipProps {
x: number;
y: number;
timestamp: Date;
value: number;
containerWidth: number;
}

export function Tooltip({x, y, timestamp, value, containerWidth}: TooltipProps): JSX.Element {
const {timezone} = useParcaContext();
const tooltipRef = useRef<HTMLDivElement>(null);
const [tooltipPosition, setTooltipPosition] = useState({x, y});

const baseOffset = {
x: 16,
y: -8,
};

useEffect(() => {
if (tooltipRef.current != null) {
const tooltipWidth = tooltipRef.current.offsetWidth;

let newX = x + baseOffset.x;
let newY = y + baseOffset.y;

if (newX + tooltipWidth > containerWidth) {
newX = x - tooltipWidth - baseOffset.x;
}

if (newY < 0) {
newY = y + Math.abs(baseOffset.y);
}

setTooltipPosition({x: newX, y: newY});
}
}, [x, y, containerWidth, baseOffset.x, baseOffset.y]);

return (
<div
ref={tooltipRef}
className="absolute bg-white dark:bg-gray-800 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 p-2 text-sm z-20"
style={{
left: `${tooltipPosition.x}px`,
top: `${tooltipPosition.y}px`,
pointerEvents: 'none',
}}
>
<div className="flex flex-col gap-1">
<div className="flex gap-1 items-center">
<div>Timestamp:</div>
<div className="text-gray-600 dark:text-gray-300">
{formatDate(timestamp, timePattern(timezone), timezone)}
</div>
</div>

<div className="flex gap-1 items-center">
<div>Value:</div>
<div className="text-gray-600 dark:text-gray-300">
{valueFormatter(value, 'nanoseconds', 2)}
</div>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import cx from 'classnames';
import * as d3 from 'd3';

import {NumberDuo} from '../../utils';
import {Tooltip} from './Tooltip';

export interface DataPoint {
timestamp: number;
Expand Down Expand Up @@ -143,7 +144,7 @@ const ZoomWindow = ({
<div
style={{height: '100%', width: beforeWidth, left: beforeStart}}
className={cx(
'bg-gray-500/50 absolute top-0 border-r-2 border-gray-900 dark:border-gray-100 z-20'
'bg-gray-500/50 absolute top-0 border-r-2 border-gray-900 dark:border-gray-100 z-10'
)}
>
<div
Expand Down Expand Up @@ -208,6 +209,8 @@ export const AreaGraph = ({
const [mousePosition, setMousePosition] = useState<NumberDuo | undefined>(undefined);
const [dragStart, setDragStart] = useState<number | undefined>(undefined);
const [isHoveringDragHandle, setIsHoveringDragHandle] = useState(false);
const [hoverData, setHoverData] = useState<{timestamp: number; value: number} | null>(null);
const [isMouseOverGraph, setIsMouseOverGraph] = useState(false);
const isDragging = dragStart !== undefined;

// Declare the x (horizontal position) scale.
Expand Down Expand Up @@ -245,12 +248,39 @@ export const AreaGraph = ({
<div
style={{height, width}}
onMouseMove={e => {
const [x, y] = d3.pointer(e);
setMousePosition([x, y]);
const [xPos, yPos] = d3.pointer(e);

if (
xPos >= marginLeft &&
xPos <= width - marginRight &&
yPos >= marginTop &&
yPos <= height - marginBottom
) {
setMousePosition([xPos, yPos]);

// Find the closest data point
if (!isHoveringDragHandle && !isDragging) {
const xDate = x.invert(xPos);
const bisect = d3.bisector((d: DataPoint) => d.timestamp).left;
const index = bisect(data, xDate.getTime());
const dataPoint = data[index];
if (dataPoint !== undefined) {
setHoverData(dataPoint);
}
}
} else {
setMousePosition(undefined);
setHoverData(null);
}
}}
onMouseEnter={() => {
setIsMouseOverGraph(true);
}}
onMouseLeave={() => {
setIsMouseOverGraph(false);
setMousePosition(undefined);
setDragStart(undefined);
setHoverData(null);
}}
onMouseDown={e => {
// only left mouse button
Expand Down Expand Up @@ -313,6 +343,21 @@ export const AreaGraph = ({
})}
></div>

{/* Update Tooltip conditional render */}
{mousePosition !== undefined &&
hoverData !== null &&
!isDragging &&
!isHoveringDragHandle &&
isMouseOverGraph && (
<Tooltip
x={mousePosition[0]}
y={mousePosition[1]}
timestamp={new Date(hoverData.timestamp)}
value={hoverData.value}
containerWidth={width}
/>
)}

<svg style={{width: '100%', height: '100%'}}>
<path fill={fill} d={area(data) as string} className="opacity-80" />
</svg>
Expand Down
5 changes: 4 additions & 1 deletion ui/packages/shared/profile/src/MetricsGraphStrips/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ export const MetricsGraphStrips = ({
return (
<div className="relative min-h-5" style={{width: width ?? 1468}} key={labelStr}>
<div
className="text-xs absolute top-0 left-0 flex gap-[2px] items-center bg-white/50 px-1 rounded-sm cursor-pointer z-30"
className="text-xs absolute top-0 left-0 flex gap-[2px] items-center bg-white/50 px-1 rounded-sm cursor-pointer"
style={{
zIndex: 15,
}}
onClick={() => {
const newCollapsedIndices = [...collapsedIndices];
if (collapsedIndices.includes(i)) {
Expand Down

0 comments on commit f1cbb9a

Please sign in to comment.