Skip to content

Commit

Permalink
feat: add custom hook and controls to graph
Browse files Browse the repository at this point in the history
  • Loading branch information
mbhrznr committed Dec 7, 2024
1 parent 5a6be69 commit ab50303
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 27 deletions.
19 changes: 19 additions & 0 deletions frontend/components/icons/ChevronUp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
export function ChevronUp(props: { class?: string }) {
return (
<svg
class={`h-4 w-4 ${props.class ?? ""}`}
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
transform="translate(-1 0) rotate(270 7 7)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.96967 12.5303C3.67678 12.2374 3.67678 11.7626 3.96967 11.4697L8.43934 7L3.96967 2.53033C3.67678 2.23744 3.67678 1.76256 3.96967 1.46967C4.26256 1.17678 4.73744 1.17678 5.03033 1.46967L10.0303 6.46967C10.3232 6.76256 10.3232 7.23744 10.0303 7.53033L5.03033 12.5303C4.73744 12.8232 4.26256 12.8232 3.96967 12.5303Z"
fill="currentColor"
/>
</svg>
);
}
18 changes: 18 additions & 0 deletions frontend/components/icons/Minus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
export function Minus(props: { class?: string }) {
return (
<svg
class={`w-4 h-4 ${props.class ?? ""}`}
aria-hidden="true"
viewBox="0 0 14 14"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1 7C1 6.58579 1.33579 6.25 1.75 6.25H12.25C12.6642 6.25 13 6.58579 13 7C13 7.41421 12.6642 7.75 12.25 7.75H1.75C1.33579 7.75 1 7.41421 1 7Z"
fill="currentColor"
/>
</svg>
);
}
18 changes: 18 additions & 0 deletions frontend/components/icons/Reset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
export function Reset(props: { class?: string }) {
return (
<svg
class={`w-4 h-4 ${props.class ?? ""}`}
aria-hidden="true"
viewBox="0 0 16 16"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z"
fill="currentColor"
/>
</svg>
);
}
165 changes: 138 additions & 27 deletions frontend/routes/package/(_islands)/DependencyGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
// Copyright 2024 the JSR authors. All rights reserved. MIT license.
import { useEffect, useRef } from "preact/hooks";
import type { ComponentChildren } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";
import { useSignal } from "@preact/signals";
import { instance, type Viz } from "@viz-js/viz";
import { ChevronDown } from "../../../components/icons/ChevronDown.tsx";
import { ChevronLeft } from "../../../components/icons/ChevronLeft.tsx";
import { ChevronRight } from "../../../components/icons/ChevronRight.tsx";
import { ChevronUp } from "../../../components/icons/ChevronUp.tsx";
import { Minus } from "../../../components/icons/Minus.tsx";
import { Plus } from "../../../components/icons/Plus.tsx";
import { Reset } from "../../../components/icons/Reset.tsx";

interface DependencyGraphKindJsr {
type: "jsr";
Expand Down Expand Up @@ -42,15 +50,14 @@ export interface DependencyGraphProps {
}

function createDigraph(dependencies: DependencyGraphProps["dependencies"]) {
return `
digraph "dependencies" {
return `digraph "dependencies" {
node [fontname="Courier", shape="box"]
${
${
dependencies.map(({ children, dependency }, index) => {
return [
[index, renderDependency(dependency)].join(" "),
...children.map((child) => `${index} -> ${child}`),
` ${index} ${renderDependency(dependency)}`,
...children.map((child) => ` ${index} -> ${child}`),
].filter(Boolean).join("\n");
}).join("\n")
}
Expand All @@ -71,57 +78,161 @@ function renderDependency(dependency: DependencyGraphKind) {
}
}

function renderJsrDependency(
dependency: DependencyGraphKindJsr,
) {
function renderJsrDependency(dependency: DependencyGraphKindJsr) {
const label =
`@${dependency.scope}/${dependency.package}@${dependency.version}`;
const href = `/${label}`;

return `[href="${href}", label="${label}", tooltip="${label}"]\n`;
return `[href="${href}", label="${label}", tooltip="${label}"]`;
}

function renderNpmDependency(dependency: DependencyGraphKindNpm) {
const label = `${dependency.package}@${dependency.version}`;
const href = `https://www.npmjs.com/package/${dependency.package}`;

return `[href="${href}", label="${label}", tooltip="${label}"]\n`;
return `[href="${href}", label="${label}", tooltip="${label}"]`;
}

function renderRootDependency(dependency: DependencyGraphKindRoot) {
const label = dependency.path;

return `[label="${label}", tooltip="${label}"]\n`;
return `[label="${label}", tooltip="${label}"]`;
}

function renderErrorDependency(dependency: DependencyGraphKindError) {
return ``;
const label = dependency.error;

return `[label="${label}", tooltip="${label}"]`;
}

export function DependencyGraph(props: DependencyGraphProps) {
const anchor = useRef<HTMLDivElement>(null);
function useDigraph(dependencies: DependencyGraphProps["dependencies"]) {
const controls = useSignal({ pan: { x: 0, y: 0 }, zoom: 1 });
const ref = useRef<HTMLDivElement>(null);
const svg = useRef<SVGSVGElement | null>(null);
const viz = useSignal<Viz | undefined>(undefined);

const pan = useCallback((x: number, y: number) => {
controls.value.pan.x += x;
controls.value.pan.y += y;
if (svg.current) {
svg.current.style.transform =
`translate(${controls.value.pan.x}px, ${controls.value.pan.y}px) scale(${controls.value.zoom})`;
}
}, [controls]);

const zoom = useCallback((zoom: number) => {
controls.value.zoom += zoom;
if (svg.current) {
svg.current.style.transform =
`translate(${controls.value.pan.x}px, ${controls.value.pan.y}px) scale(${controls.value.zoom})`;
}
}, [controls]);

const reset = useCallback(() => {
controls.value = { pan: { x: 0, y: 0 }, zoom: 1 };
if (svg.current) {
svg.current.style.transform = "";
}
}, []);

useEffect(() => {
(async () => {
viz.value = await instance();

if (anchor.current && viz.value) {
const digraph = createDigraph(props.dependencies);
if (ref.current && viz.value) {
const digraph = createDigraph(dependencies);

console.log(digraph);

anchor.current.appendChild(
viz.value.renderSVGElement(digraph),
);
svg.current = viz.value.renderSVGElement(digraph);
ref.current.appendChild(svg.current);
}
})();
}, []);
}, [dependencies]);

return { pan, zoom, reset, ref };
}

interface GraphControlButtonProps {
children: ComponentChildren;
class: string;
onClick: () => void;
title: string;
}

function GraphControlButton(props: GraphControlButtonProps) {
return (
<button
aria-label={props.title}
class={`${props.class} bg-white text-jsr-gray-700 p-1.5 ring-1 ring-jsr-gray-700 rounded-full sm:rounded hover:bg-jsr-gray-100/30"`}
onClick={props.onClick}
title={props.title}
>
{props.children}
</button>
);
}

export function DependencyGraph(props: DependencyGraphProps) {
const { pan, zoom, reset, ref } = useDigraph(props.dependencies);

return (
<div
class="-mx-4 md:mx-0 ring-1 ring-jsr-cyan-100 sm:rounded overflow-hidden"
ref={anchor}
/>
<div class="-mx-4 md:mx-0 ring-1 ring-jsr-cyan-100 sm:rounded overflow-hidden relative">
<div ref={ref} />
<div class="absolute gap-1 grid grid-cols-3 bottom-4 right-4">
{/* zoom */}
<GraphControlButton
class="col-start-3 col-end-3 row-start-1 row-end-1"
onClick={() => zoom(0.1)}
title="Zoom in"
>
<Plus />
</GraphControlButton>
<GraphControlButton
class="col-start-3 col-end-3 row-start-3 row-end-3"
onClick={() => zoom(-0.1)}
title="Zoom out"
>
<Minus />
</GraphControlButton>

{/* pan */}
<GraphControlButton
class="col-start-2 col-end-2 row-start-1 row-end-1"
onClick={() => pan(0, 100)}
title="Pan up"
>
<ChevronUp />
</GraphControlButton>
<GraphControlButton
class="col-start-1 col-end-1 row-start-2 row-end-2"
onClick={() => pan(100, 0)}
title="Pan left"
>
<ChevronLeft />
</GraphControlButton>
<GraphControlButton
class="col-start-3 col-end-3 row-start-2 row-end-2"
onClick={() => pan(-100, 0)}
title="Pan right"
>
<ChevronRight />
</GraphControlButton>
<GraphControlButton
class="col-start-2 col-end-2 row-start-3 row-end-3"
onClick={() => pan(0, -100)}
title="Pan down"
>
<ChevronDown />
</GraphControlButton>

{/* reset */}
<GraphControlButton
class="col-start-2 col-end-2 row-start-2 row-end-2"
onClick={reset}
title="Reset view"
>
<Reset />
</GraphControlButton>
</div>
</div>
);
}

0 comments on commit ab50303

Please sign in to comment.