@grafana/flamegraph
Version:
Grafana flamegraph visualization component
245 lines (242 loc) • 7.72 kB
JavaScript
import { jsxs, jsx } from 'react/jsx-runtime';
import { css } from '@emotion/css';
import { useRef, useState, useCallback, useEffect } from 'react';
import { useMeasure } from 'react-use';
import { PIXELS_PER_LEVEL } from '../constants.mjs';
import FlameGraphContextMenu from './FlameGraphContextMenu.mjs';
import FlameGraphTooltip from './FlameGraphTooltip.mjs';
import { useFlameRender, getBarX } from './rendering.mjs';
const FlameGraphCanvas = ({
data,
rangeMin,
rangeMax,
matchedLabels,
setRangeMin,
setRangeMax,
onItemFocused,
focusedItemData,
textAlign,
onSandwich,
colorScheme,
totalProfileTicks,
totalProfileTicksRight,
totalViewTicks,
root,
direction,
depth,
showFlameGraphOnly,
collapsedMap,
setCollapsedMap,
collapsing,
getExtraContextMenuButtons,
selectedView,
search
}) => {
const styles = getStyles();
const [sizeRef, { width: wrapperWidth }] = useMeasure();
const graphRef = useRef(null);
const [tooltipItem, setTooltipItem] = useState();
const [clickedItemData, setClickedItemData] = useState();
useFlameRender({
canvasRef: graphRef,
colorScheme,
data,
focusedItemData,
root,
direction,
depth,
rangeMax,
rangeMin,
matchedLabels,
textAlign,
totalViewTicks,
// We need this so that if we have a diff profile and are in sandwich view we still show the same diff colors.
totalColorTicks: data.isDiffFlamegraph() ? totalProfileTicks : totalViewTicks,
totalTicksRight: totalProfileTicksRight,
wrapperWidth,
collapsedMap
});
const onGraphClick = useCallback(
(e) => {
setTooltipItem(void 0);
const pixelsPerTick = graphRef.current.clientWidth / totalViewTicks / (rangeMax - rangeMin);
const item = convertPixelCoordinatesToBarCoordinates(
{ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY },
root,
direction,
depth,
pixelsPerTick,
totalViewTicks,
rangeMin,
collapsedMap
);
if (item) {
setClickedItemData({
posY: e.clientY,
posX: e.clientX,
item,
label: data.getLabel(item.itemIndexes[0])
});
} else {
setClickedItemData(void 0);
}
},
[data, rangeMin, rangeMax, totalViewTicks, root, direction, depth, collapsedMap]
);
const [mousePosition, setMousePosition] = useState();
const onGraphMouseMove = useCallback(
(e) => {
if (clickedItemData === void 0) {
setTooltipItem(void 0);
setMousePosition(void 0);
const pixelsPerTick = graphRef.current.clientWidth / totalViewTicks / (rangeMax - rangeMin);
const item = convertPixelCoordinatesToBarCoordinates(
{ x: e.nativeEvent.offsetX, y: e.nativeEvent.offsetY },
root,
direction,
depth,
pixelsPerTick,
totalViewTicks,
rangeMin,
collapsedMap
);
if (item) {
setMousePosition({ x: e.clientX, y: e.clientY });
setTooltipItem(item);
}
}
},
[rangeMin, rangeMax, totalViewTicks, clickedItemData, setMousePosition, root, direction, depth, collapsedMap]
);
const onGraphMouseLeave = useCallback(() => {
setTooltipItem(void 0);
}, []);
useEffect(() => {
const handleOnClick = (e) => {
var _a;
if (e.target instanceof HTMLElement && ((_a = e.target.parentElement) == null ? void 0 : _a.id) !== "flameGraphCanvasContainer_clickOutsideCheck") {
setClickedItemData(void 0);
}
};
window.addEventListener("click", handleOnClick);
return () => window.removeEventListener("click", handleOnClick);
}, [setClickedItemData]);
return /* @__PURE__ */ jsxs("div", { className: styles.graph, children: [
/* @__PURE__ */ jsx("div", { className: styles.canvasWrapper, id: "flameGraphCanvasContainer_clickOutsideCheck", ref: sizeRef, children: /* @__PURE__ */ jsx(
"canvas",
{
ref: graphRef,
"data-testid": "flameGraph",
onClick: onGraphClick,
onMouseMove: onGraphMouseMove,
onMouseLeave: onGraphMouseLeave
}
) }),
/* @__PURE__ */ jsx(
FlameGraphTooltip,
{
position: mousePosition,
item: tooltipItem,
data,
totalTicks: totalViewTicks,
collapseConfig: tooltipItem ? collapsedMap.get(tooltipItem) : void 0
}
),
!showFlameGraphOnly && clickedItemData && /* @__PURE__ */ jsx(
FlameGraphContextMenu,
{
data,
itemData: clickedItemData,
collapsing,
collapseConfig: collapsedMap.get(clickedItemData.item),
onMenuItemClick: () => {
setClickedItemData(void 0);
},
onItemFocus: () => {
setRangeMin(clickedItemData.item.start / totalViewTicks);
setRangeMax((clickedItemData.item.start + clickedItemData.item.value) / totalViewTicks);
onItemFocused(clickedItemData);
},
onSandwich: () => {
onSandwich(data.getLabel(clickedItemData.item.itemIndexes[0]));
},
onExpandGroup: () => {
setCollapsedMap(collapsedMap.setCollapsedStatus(clickedItemData.item, false));
},
onCollapseGroup: () => {
setCollapsedMap(collapsedMap.setCollapsedStatus(clickedItemData.item, true));
},
onExpandAllGroups: () => {
setCollapsedMap(collapsedMap.setAllCollapsedStatus(false));
},
onCollapseAllGroups: () => {
setCollapsedMap(collapsedMap.setAllCollapsedStatus(true));
},
allGroupsCollapsed: Array.from(collapsedMap.values()).every((i) => i.collapsed),
allGroupsExpanded: Array.from(collapsedMap.values()).every((i) => !i.collapsed),
getExtraContextMenuButtons,
selectedView,
search
}
)
] });
};
const getStyles = () => ({
graph: css({
label: "graph",
overflow: "auto",
flexGrow: 1,
flexBasis: "50%"
}),
canvasContainer: css({
label: "canvasContainer",
display: "flex"
}),
canvasWrapper: css({
label: "canvasWrapper",
cursor: "pointer",
flex: 1,
overflow: "hidden"
}),
sandwichMarker: css({
label: "sandwichMarker",
writingMode: "vertical-lr",
transform: "rotate(180deg)",
overflow: "hidden",
whiteSpace: "nowrap"
}),
sandwichMarkerIcon: css({
label: "sandwichMarkerIcon",
verticalAlign: "baseline"
})
});
const convertPixelCoordinatesToBarCoordinates = (pos, root, direction, depth, pixelsPerTick, totalTicks, rangeMin, collapsedMap) => {
let next = root;
let currentLevel = direction === "children" ? 0 : depth - 1;
const levelIndex = Math.floor(pos.y / (PIXELS_PER_LEVEL / window.devicePixelRatio));
let found = void 0;
while (next) {
const node = next;
next = void 0;
if (currentLevel === levelIndex) {
found = node;
break;
}
const nextList = direction === "children" ? node.children : node.parents || [];
for (const child of nextList) {
const xStart = getBarX(child.start, totalTicks, rangeMin, pixelsPerTick);
const xEnd = getBarX(child.start + child.value, totalTicks, rangeMin, pixelsPerTick);
if (xStart <= pos.x && pos.x < xEnd) {
next = child;
const collapsedConfig = collapsedMap.get(child);
if (!collapsedConfig || !collapsedConfig.collapsed || collapsedConfig.items[0] === child) {
currentLevel = currentLevel + (direction === "children" ? 1 : -1);
}
break;
}
}
}
return found;
};
export { convertPixelCoordinatesToBarCoordinates, FlameGraphCanvas as default };
//# sourceMappingURL=FlameGraphCanvas.mjs.map