Skip to content

Commit

Permalink
Feat: Editor history visualization (#24)
Browse files Browse the repository at this point in the history
* Update: Editor history visualization

* ui: better styling

* Update: Fixed visualization bugs

* Delete examples/stories/package-lock.json

* Delete package-lock.json

* Update: Reduced space and run time

---------

Co-authored-by: Zixuan Chen <remch183@outlook.com>
  • Loading branch information
Henry-Wow and zxch3n authored Jan 3, 2025
1 parent 8797fdd commit 74cbe41
Show file tree
Hide file tree
Showing 9 changed files with 2,403 additions and 222 deletions.
1 change: 1 addition & 0 deletions examples/stories/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@storybook/react": "^8.0.8",
"@storybook/react-vite": "^8.0.8",
"@storybook/test": "^8.0.8",
"@types/lodash": "^4.17.13",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
Expand Down
39 changes: 39 additions & 0 deletions examples/stories/src/stories/DagView.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.dag-view-message:hover {
color: #777;
cursor: pointer;
}

.dag-view-message {
color: #333;
}

.dag-view-message .author {
color: #777;
margin-left: 0.8em;
}

.dag-view-message:hover .author {
color: #aaa;
margin-left: 0.8em;
}

.dag-view-message .operationType {
color: #777;
margin-left: 0.8em;
}

.dag-view-message:hover .operationType {
color: #aaa;
margin-left: 0.8em;
}

.dag-view-message .timestamp {
color: #777;
margin-left: 0.8em;
}

.dag-view-message:hover .timestamp {
color: #aaa;
margin-left: 0.8em;
}

148 changes: 148 additions & 0 deletions examples/stories/src/stories/DagView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { visualize, DagNode, Row, Thread } from "./dag-view";
import { useMemo } from "react";
import "./DagView.css";

export interface ViewDagNode extends DagNode {
message?: string;
author?: string;
timestamp?: number;
}

const CELL_SIZE = 24;
const NODE_RADIUS = 5;

export function DagViewComponent({ nodes, frontiers }: { nodes: ViewDagNode[], frontiers: string[] }) {
const view = useMemo(() => {
const map = new Map<string, DagNode>();
for (const node of nodes) {
map.set(node.id, node);
}
return visualize(id => map.get(id), frontiers);
}, [nodes, frontiers]);

const rowSvgContents = view.rows.map((row, index) => renderRowAsSvg(row, index, "white"));

return (
<div style={{ position: 'relative' }}>
{rowSvgContents.map((content, index) => (
<div key={`row-${index}`} style={{ position: 'relative', height: CELL_SIZE, display: "flex", flexDirection: "row", alignItems: "center" }}>
<svg width={content.width} height={CELL_SIZE} >
{content.elements}
</svg>
<div
className="dag-view-message"
style={{
fontSize: '12px',
fontFamily: "'Helvetica Neue', Arial, sans-serif"
}}
>
<span>
{(content.row.active.node as ViewDagNode).message ?? content.row.active.node.id}
</span>
<span className="author">
{(content.row.active.node as ViewDagNode).author}
</span>
<span className="timestamp">
{(content.row.active.node as ViewDagNode).timestamp != null && new Date((content.row.active.node as ViewDagNode).timestamp!).toLocaleString()}
</span>
</div>
</div>
))}
</div>
);
}

function renderRowAsSvg(row: Row, rowIndex: number, backgroundColor: string) {
const elements: JSX.Element[] = [];
const y = CELL_SIZE / 2;

// Render connections
const inputConn = renderConnections(row, 'input', y - CELL_SIZE / 2);
const outputConn = renderConnections(row, 'output', y);
elements.push(...inputConn, ...outputConn);

// Render nodes
row.cur_tids.forEach((tid: number, index: number) => {
const x = index * CELL_SIZE / 2 + CELL_SIZE / 4;
const isActive = tid === row.active.tid;
if (isActive) {
elements.push(
<circle
key={`node-${rowIndex}-${index}`}
cx={x}
cy={y}
r={NODE_RADIUS}
fill={"rgb(100, 100, 230)"}
stroke={backgroundColor}
/>
);
}
});

const width = (Math.max(row.cur_tids.length, row.output.length, row.input.length)) * CELL_SIZE / 2 + 8;
return {
width,
elements,
row
};
}

function renderConnections(row: Row, type: 'input' | 'output', y: number): JSX.Element[] {
const ans: JSX.Element[] = []
row[type].forEach((thread: Thread, i: number) => {
const connectionA = row.cur_tids.indexOf(thread.tid);
const connectionB = thread.dep_on_active ? row.active_index : -1;
if (connectionA >= 0) {
ans.push(renderConnection(type, i, connectionA, y, thread.tid))
}
if (connectionB >= 0) {
ans.push(renderConnection(type, i, connectionB, y, thread.tid))
}
});

return ans;
}

function renderConnection(type: 'input' | 'output', xFrom: number, xTo: number, y: number, tid: number): JSX.Element {
const startX = xFrom * CELL_SIZE / 2 + CELL_SIZE / 4;
const endX = xTo * CELL_SIZE / 2 + CELL_SIZE / 4;
const startY = type === 'input' ? y : y + CELL_SIZE / 2;
const endY = type === 'input' ? y + CELL_SIZE / 2 : y;

let path = ""
// const midX = (startX + endX) / 2;
// const midY = (startY + endY) / 2;
if (startX > endX) {
const controlPoint1X = startX;
const controlPoint1Y = endY;
const controlPoint2X = endX;
const controlPoint2Y = startY;
path = `M ${startX} ${startY} C ${controlPoint1X} ${controlPoint1Y}, ${controlPoint2X} ${controlPoint2Y}, ${endX} ${endY}`;
} else {
const controlPoint1X = startX;
const controlPoint1Y = endY;
const controlPoint2X = startX;
const controlPoint2Y = endY
path = `M ${startX} ${startY} C ${controlPoint1X} ${controlPoint1Y}, ${controlPoint2X} ${controlPoint2Y}, ${endX} ${endY}`;
}

return (
<path
key={`connection-${type}-${tid}-${xFrom}-${xTo}`}
d={path}
fill="none"
stroke={tidToColor(tid)}
strokeWidth="2"
/>
);
}


function tidToColor(tid: number): string {
// Generate a beautiful color based on the thread ID
const hue = (tid * 137.508) % 360; // Golden angle approximation for even distribution
const saturation = 70 + (tid % 30); // Vary saturation between 70% and 100%
const lightness = 45 + (tid % 20); // Vary lightness between 45% and 65%

return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
Loading

0 comments on commit 74cbe41

Please sign in to comment.