import {h} from 'preact' import {css} from 'aphrodite' import {ReloadableComponent} from './reloadable' import {CallTreeNode, Frame} from './profile' import {Flamechart} from './flamechart' import {Rect, Vec2, AffineTransform} from './math' import {formatPercent} from './utils' import {FlamechartMinimapView} from './flamechart-minimap-view' import {style} from './flamechart-style' import {Sizes, commonStyle} from './style' import {CanvasContext} from './canvas-context' import {FlamechartRenderer} from './flamechart-renderer' import {FlamechartDetailView} from './flamechart-detail-view' import {FlamechartPanZoomView} from './flamechart-pan-zoom-view' import {Hovertip} from './hovertip' interface FlamechartViewProps { flamechart: Flamechart canvasContext: CanvasContext flamechartRenderer: FlamechartRenderer getCSSColorForFrame: (frame: Frame) => string } interface FlamechartViewState { hover: { node: CallTreeNode event: MouseEvent } | null selectedNode: CallTreeNode | null configSpaceViewportRect: Rect } export class FlamechartView extends ReloadableComponent { constructor() { super() this.state = { hover: null, selectedNode: null, configSpaceViewportRect: Rect.empty, } } private configSpaceSize() { return new Vec2( this.props.flamechart.getTotalWeight(), this.props.flamechart.getLayers().length, ) } private minConfigSpaceViewportRectWidth() { return Math.min( this.props.flamechart.getTotalWeight(), 3 * this.props.flamechart.getMinFrameWidth(), ) } private setConfigSpaceViewportRect = (viewportRect: Rect): void => { const configSpaceDetailViewHeight = Sizes.DETAIL_VIEW_HEIGHT / Sizes.FRAME_HEIGHT const configSpaceOriginBounds = new Rect( new Vec2(0, -1), Vec2.max( new Vec2(0, 0), this.configSpaceSize() .minus(viewportRect.size) .plus(new Vec2(0, configSpaceDetailViewHeight + 1)), ), ) const configSpaceSizeBounds = new Rect( new Vec2(this.minConfigSpaceViewportRectWidth(), viewportRect.height()), new Vec2(this.configSpaceSize().x, viewportRect.height()), ) this.setState({ configSpaceViewportRect: new Rect( configSpaceOriginBounds.closestPointTo(viewportRect.origin), configSpaceSizeBounds.closestPointTo(viewportRect.size), ), }) } private transformViewport = (transform: AffineTransform): void => { const viewportRect = transform.transformRect(this.state.configSpaceViewportRect) this.setConfigSpaceViewportRect(viewportRect) } onNodeHover = (hover: {node: CallTreeNode; event: MouseEvent} | null) => { this.setState({hover}) } onNodeClick = (node: CallTreeNode | null) => { this.setState({ selectedNode: node, }) } formatValue(weight: number) { const totalWeight = this.props.flamechart.getTotalWeight() const percent = 100 * weight / totalWeight const formattedPercent = formatPercent(percent) return `${this.props.flamechart.formatValue(weight)} (${formattedPercent})` } renderTooltip() { if (!this.container) return null const {hover} = this.state if (!hover) return null const {width, height, left, top} = this.container.getBoundingClientRect() const offset = new Vec2(hover.event.clientX - left, hover.event.clientY - top) return ( {this.formatValue(hover.node.getTotalWeight())} {' '} {hover.node.frame.name} ) } container: HTMLDivElement | null = null containerRef = (container?: Element) => { this.container = (container as HTMLDivElement) || null } panZoomView: FlamechartPanZoomView | null = null panZoomRef = (view: FlamechartPanZoomView | null) => { this.panZoomView = view } subcomponents() { return { panZoom: this.panZoomView, } } render() { return (
{this.renderTooltip()} {this.state.selectedNode && ( )}
) } }