Skip to content

Commit

Permalink
Works with the new framework
Browse files Browse the repository at this point in the history
  • Loading branch information
LPeter1997 committed Sep 22, 2023
1 parent d0577f8 commit de23c28
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 176 deletions.
192 changes: 27 additions & 165 deletions src/Draco.Trace.Visualizer/src/TimelineGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as d3 from "d3";
import React from "react";
import { MessageModel, ThreadModel, TraceModel } from "./Model";
import { TimelineLayoutSettings, focusVisualsOnNode, layoutTimeline } from "./graph_utils";

type Props = {
width: number;
Expand All @@ -10,10 +11,6 @@ type Props = {
endColor?: any;
};

interface TimelineMessageModel extends MessageModel {
isPlaceholder: boolean;
};

const TimelineGraph = (props: Props) => {
const domRef = React.useRef(null);

Expand All @@ -34,15 +31,21 @@ function buildGraph(domRef: React.MutableRefObject<null>, props: Props) {
.attr('height', props.height)
.attr('cursor', 'grab');

const messageHierarchy = d3.hierarchy(toTimelineMessage(props.data.rootMessage), getTimelineChildren);
messageHierarchy.sum(node => node.children && node.children.length > 0 ? 0 : getTimeSpan(node));

const partitionLayout = d3
.partition<TimelineMessageModel>()
.size([props.width, props.height])
.padding(2);

let laidOutMessages = partitionLayout(messageHierarchy);
const layoutSettings: TimelineLayoutSettings<MessageModel> = {
size: [props.width, props.height],
barHeight: 50,
padding: [2, 2],
getChildren: (node) => node.children ?? [],
getRange: (node, parent) => {
if (!parent) return [0, 1];

const parentSpan = parent.endTime - parent.startTime;
const startPercentage = (node.startTime - parent.startTime) / parentSpan;
const endPercentage = (node.endTime - parent.startTime) / parentSpan;
return [startPercentage, endPercentage];
},
};
const laidOutMessages = layoutTimeline(props.data.rootMessage, layoutSettings);

const colorScale = d3.interpolateHsl(props.startColor || 'green', props.endColor || 'red');

Expand All @@ -56,12 +59,11 @@ function buildGraph(domRef: React.MutableRefObject<null>, props: Props) {
// Rects
const allRects = allGroups
.append('rect')
.attr('x', node => node.x0)
.attr('y', node => props.height - node.y1)
.attr('width', node => node.x1 - node.x0)
.attr('height', node => node.y1 - node.y0)
.attr('x', node => node.visualBounds.x0)
.attr('y', node => props.height - node.visualBounds.y1)
.attr('width', node => node.visualBounds.x1 - node.visualBounds.x0)
.attr('height', node => node.visualBounds.y1 - node.visualBounds.y0)
.attr('fill', node => {
if (node.data.isPlaceholder) return 'transparent';
const fillPercentage = node.parent
? getTimeSpan(node.data) / getTimeSpan(node.parent.data)
: 1;
Expand All @@ -75,8 +77,8 @@ function buildGraph(domRef: React.MutableRefObject<null>, props: Props) {
.attr('color', 'black')
.attr('dominant-baseline', 'middle')
.attr('text-anchor', 'middle')
.attr('x', node => node.x0 + (node.x1 - node.x0) / 2)
.attr('y', node => props.height - node.y1 + (node.y1 - node.y0) / 2);
.attr('x', node => node.visualBounds.x0 + (node.visualBounds.x1 - node.visualBounds.x0) / 2)
.attr('y', node => props.height - node.visualBounds.y1 + (node.visualBounds.y1 - node.visualBounds.y0) / 2);

const zoom = d3
.zoom()
Expand All @@ -95,170 +97,30 @@ function buildGraph(domRef: React.MutableRefObject<null>, props: Props) {
.select('text')
.transition(transition)
.attr('transform', (node: any) => {
let target = node.target || node;
return `translate(${x + (k - 1) * ((target.x0 + target.x1) / 2)} 0)`;
return `translate(${x + (k - 1) * ((node.visualBounds.x0 + node.visualBounds.x1) / 2)} 0)`;
});
});

svg.call(zoom as any);

allRects.on('click', function (element, node) {
focus(node);
focusVisualsOnNode(node);

const transition = d3
.transition()
.duration(500);
allRects
.transition(transition)
.attr('x', (node: any) => node.target.x0)
.attr('width', (node: any) => node.target.x1 - node.target.x0);
.attr('x', (node: any) => node.visualBounds.x0)
.attr('width', (node: any) => node.visualBounds.x1 - node.visualBounds.x0);
allTexts
.transition(transition)
.attr('x', (node: any) => node.target.x0 + (node.target.x1 - node.target.x0) / 2);
.attr('x', (node: any) => node.visualBounds.x0 + (node.visualBounds.x1 - node.visualBounds.x0) / 2);
});
}

function focus(node: d3.HierarchyRectangularNode<TimelineMessageModel>) {
function cleanse(node: any) {
(node as any).target = null;
if (node.children) {
for (let child of node.children) cleanse(child);
}
}

function resize(node: d3.HierarchyRectangularNode<TimelineMessageModel>, x0: number, x1: number) {
(node as any).target = {
x0,
y0: node.y0,
x1,
y1: node.y1,
};
}

function scaleUpChild(node: d3.HierarchyRectangularNode<TimelineMessageModel>, parent: any) {
const parentSpan = getTimeSpan(parent.data);
const startPercent = (node.data.startTime - parent.data.startTime) / parentSpan;
const endPercent = (node.data.endTime - parent.data.startTime) / parentSpan;
const parentSize = parent.target.x1 - parent.target.x0;

resize(node, parent.target.x0 + startPercent * parentSize, parent.target.x0 + endPercent * parentSize);

if (node.children) {
for (let child of node.children) scaleUpChild(child, node);
}
}

// Find root
let root = node;
while (root.parent) root = root.parent;

function collapseLeft(node: d3.HierarchyRectangularNode<TimelineMessageModel>) {
resize(node, root.x0, root.x0);
if (node.children) {
for (let child of node.children) collapseLeft(child);
}
}

function collapseRight(node: d3.HierarchyRectangularNode<TimelineMessageModel>) {
resize(node, root.x1, root.x1);
if (node.children) {
for (let child of node.children) collapseRight(child);
}
}

function collapseRemaining(node: any) {
if (!node.children) return;

let isLeft = true;
for (let child of node.children) {
if (child.target) {
isLeft = false;
collapseRemaining(child);
continue;
}

if (isLeft) collapseLeft(child);
else collapseRight(child);
}
}

// Cleanse target transforms
cleanse(root);

// Walk up the ancestry chain, expand
let current = node;
while (current) {
resize(current, root.x0, root.x1);
current = current.parent!;
}

// Walk down, scale up children
if (node.children) {
for (let child of node.children) scaleUpChild(child, node);
}

// Collapse everything else
collapseRemaining(root);
}

function getTimeSpan(msg: MessageModel): number {
return msg.endTime - msg.startTime;
}

function toTimelineMessage(msg: MessageModel): TimelineMessageModel {
return {
...msg,
isPlaceholder: false,
};
}

function getTimelineChildren(msg: MessageModel): TimelineMessageModel[] {
function makePlaceholder(startTime: number, endTime: number): TimelineMessageModel {
return {
name: '',
startTime,
endTime,
isPlaceholder: true,
};
}

if (!msg.children || msg.children.length === 0) return [];

const result = new Array<TimelineMessageModel>();

// Check the gap between first child and parent
if (msg.startTime < msg.children[0].startTime) {
// Push placeholder
result.push(makePlaceholder(msg.startTime, msg.children[0].startTime));
}

for (let i = 0; i < msg.children.length; ++i) {
// Add current message
const child = msg.children[i];
result.push(toTimelineMessage(child));

// Look at next message, if there is a gap, fill it
if (i + 1 < msg.children.length) {
const nextChild = msg.children[i + 1];
if (child.endTime < nextChild.startTime) {
// Push placeholder
result.push({
name: '',
startTime: child.endTime,
endTime: nextChild.startTime,
isPlaceholder: true,
});
}
}
}

// Check the gap between last child and parent
if (msg.children[msg.children.length - 1].endTime < msg.endTime) {
// Push placeholder
result.push(makePlaceholder(msg.children[msg.children.length - 1].endTime, msg.endTime));
}

return result;
}

export default TimelineGraph;
56 changes: 45 additions & 11 deletions src/Draco.Trace.Visualizer/src/graph_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,35 @@ export interface HierarchicalBarGraphNode<TData> {
visualBounds: BoundingBox;
}

/**
* Represents a completed layout for a hierarchical bar graph.
*/
export class HierarchicalBarGraphLayout<TData> {
/**
* The root node.
*/
root: HierarchicalBarGraphNode<TData>;

constructor(root: HierarchicalBarGraphNode<TData>) {
this.root = root;
}

/**
* Retrieves all descendants of this hierarchy.
* @returns All descendants of this layout.
*/
descendants(): Iterable<HierarchicalBarGraphNode<TData>> {
function* recurse(current: HierarchicalBarGraphNode<TData>): Iterable<HierarchicalBarGraphNode<TData>> {
yield current;
for (let child of current.children) {
yield* recurse(child);
}
}

return recurse(this.root);
}
}

/**
* The settings to use for a timeline layout.
* @template TData The associated data type for the nodes.
Expand Down Expand Up @@ -95,7 +124,7 @@ export type TimelineLayoutSettings<TData> = {
* @param root The root node to start the layout from.
* @param settings The settings to use for the layout.
*/
export function layoutTimeline<TData>(root: TData, settings: TimelineLayoutSettings<TData>): HierarchicalBarGraphNode<TData> {
export function layoutTimeline<TData>(root: TData, settings: TimelineLayoutSettings<TData>): HierarchicalBarGraphLayout<TData> {
const [width, height] = settings.size;
const horizontalPadding = settings.padding?.[0] ?? 0;
const horizontalPadding2 = horizontalPadding / 2;
Expand All @@ -118,8 +147,8 @@ export function layoutTimeline<TData>(root: TData, settings: TimelineLayoutSetti
const bounds: BoundingBox = {
x0: xOffset[0] + startPercentage * availableSpace + horizontalPadding2,
x1: xOffset[0] + endPercentage * availableSpace - horizontalPadding2,
y0: yOffset - barHeight,
y1: yOffset,
y0: yOffset,
y1: yOffset + barHeight,
};
const node: HierarchicalBarGraphNode<TData> = {
data: current,
Expand All @@ -130,22 +159,22 @@ export function layoutTimeline<TData>(root: TData, settings: TimelineLayoutSetti
};

for (let child of settings.getChildren(current)) {
const childNode = layoutTimelineImpl(child, node, yOffset - barHeight - verticalPadding, [node.bounds.x0, node.bounds.x1]);
const childNode = layoutTimelineImpl(child, node, yOffset + barHeight + verticalPadding, [node.bounds.x0, node.bounds.x1]);
children.push(childNode);
}

return node;
}

return layoutTimelineImpl(root, undefined, height, [0, width]);
const rootNode = layoutTimelineImpl(root, undefined, 0, [0, width]);
return new HierarchicalBarGraphLayout<TData>(rootNode);
}

function clearVisualBounds<TData>(node: HierarchicalBarGraphNode<TData>) {
(node as any).visualBounds = undefined;
for (let child of node.children) clearVisualBounds(child);
}

function focusVisualsOnNode<TData>(node: HierarchicalBarGraphNode<TData>) {
/**
* Focuses the given node, collapsing every other hierarchy.
* @param node The node to focus on
*/
export function focusVisualsOnNode<TData>(node: HierarchicalBarGraphNode<TData>) {
function resize(node: HierarchicalBarGraphNode<TData>, x0: number, x1: number) {
const sourceBounds = node.visualBounds ?? node.bounds;
node.visualBounds = {
Expand Down Expand Up @@ -221,3 +250,8 @@ function focusVisualsOnNode<TData>(node: HierarchicalBarGraphNode<TData>) {
// Collapse everything else
collapseRemaining(root);
}

function clearVisualBounds<TData>(node: HierarchicalBarGraphNode<TData>) {
(node as any).visualBounds = undefined;
for (let child of node.children) clearVisualBounds(child);
}

0 comments on commit de23c28

Please sign in to comment.