Skip to content

Commit 1e2e2bc

Browse files
committed
Add basic DAG visualization with example job data
1 parent ebe38bc commit 1e2e2bc

File tree

3 files changed

+781
-1
lines changed

3 files changed

+781
-1
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<script lang="ts">
2+
import type { SvelteHTMLElements } from 'svelte/elements';
3+
import { transformContext } from 'layerchart';
4+
5+
import IconArrowUturnLeft from '~icons/heroicons/arrow-uturn-left';
6+
import IconMagnifyingGlassPlus from '~icons/heroicons/magnifying-glass-plus';
7+
import IconMagnifyingGlassMinus from '~icons/heroicons/magnifying-glass-minus';
8+
9+
import { Button } from '$lib/components/ui/button';
10+
import { Tooltip, TooltipContent, TooltipTrigger } from '$lib/components/ui/tooltip';
11+
import { cn } from '$lib/utils';
12+
13+
type Placement =
14+
| 'top-left'
15+
| 'top'
16+
| 'top-right'
17+
| 'left'
18+
| 'center'
19+
| 'right'
20+
| 'bottom-left'
21+
| 'bottom'
22+
| 'bottom-right';
23+
24+
type Actions = 'zoomIn' | 'zoomOut' | 'center' | 'reset' | 'scrollMode';
25+
interface Props {
26+
placement?: Placement;
27+
orientation?: 'horizontal' | 'vertical';
28+
size?: SvelteHTMLElements['svg']['width'];
29+
show?: Actions[];
30+
class?: string;
31+
}
32+
33+
let {
34+
placement = 'top-right',
35+
orientation = 'vertical',
36+
size = '16',
37+
show = ['zoomIn', 'zoomOut', 'center', 'reset', 'scrollMode'],
38+
...restProps
39+
}: Props = $props();
40+
41+
const transform = transformContext();
42+
</script>
43+
44+
<!-- svelte-ignore a11y_no_static_element_interactions -->
45+
<div
46+
class={cn(
47+
'bg-surface-100/50 border rounded-full m-1 backdrop-blur z-10 flex p-1',
48+
orientation === 'vertical' && 'flex-col',
49+
{
50+
'top-left': 'absolute top-0 left-0',
51+
top: 'absolute top-0 left-1/2 -translate-x-1/2',
52+
'top-right': 'absolute top-0 right-0',
53+
left: 'absolute top-1/2 left-0 -translate-y-1/2',
54+
center: 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',
55+
right: 'absolute top-1/2 right-0 -translate-y-1/2',
56+
'bottom-left': 'absolute bottom-0 left-0',
57+
bottom: 'absolute bottom-0 left-1/2 -translate-x-1/2',
58+
'bottom-right': 'absolute bottom-0 right-0'
59+
}[placement],
60+
restProps.class
61+
)}
62+
ondblclick={(e) => {
63+
// Stop from propagating to TransformContext
64+
e.stopPropagation();
65+
}}
66+
>
67+
{#if show.includes('zoomIn')}
68+
<Tooltip>
69+
<TooltipTrigger>
70+
<Button
71+
on:click={() => transform.zoomIn()}
72+
size="icon"
73+
variant="ghost"
74+
class="rounded-full"
75+
>
76+
<IconMagnifyingGlassPlus width={size} height={size} />
77+
</Button>
78+
</TooltipTrigger>
79+
<TooltipContent side="left">Zoom in</TooltipContent>
80+
</Tooltip>
81+
{/if}
82+
83+
{#if show.includes('zoomOut')}
84+
<Tooltip>
85+
<TooltipTrigger>
86+
<Button
87+
on:click={() => transform.zoomOut()}
88+
size="icon"
89+
variant="ghost"
90+
class="rounded-full"
91+
>
92+
<IconMagnifyingGlassMinus width={size} height={size} />
93+
</Button>
94+
</TooltipTrigger>
95+
<TooltipContent side="left">Zoom out</TooltipContent>
96+
</Tooltip>
97+
{/if}
98+
99+
{#if show.includes('reset')}
100+
<Tooltip>
101+
<TooltipTrigger>
102+
<Button on:click={() => transform.reset()} size="icon" variant="ghost" class="rounded-full">
103+
<IconArrowUturnLeft width={size} height={size} />
104+
</Button>
105+
</TooltipTrigger>
106+
<TooltipContent side="left">Reset</TooltipContent>
107+
</Tooltip>
108+
{/if}
109+
</div>
Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,101 @@
1-
<div class="py-4">Lineage content coming soon...</div>
1+
<script lang="ts">
2+
import { cubicOut } from 'svelte/easing';
3+
import { Chart, Dagre, Group, Rect, Spline, Svg, Text } from 'layerchart';
4+
import { curveBasis } from 'd3';
5+
import { cn } from 'tailwind-variants';
6+
7+
import TransformControls from '$lib/components/charts/TransformControls.svelte';
8+
9+
// TODO: Replace with Ken's job data once merged
10+
import { jobDependencyGraphExample, jobRunListExample } from './job-sample-data';
11+
12+
const data = {
13+
nodes: Object.entries(jobRunListExample).map(([id, jobRuns]) => {
14+
return {
15+
id,
16+
jobRuns
17+
};
18+
}),
19+
edges: Object.entries(jobDependencyGraphExample.adjacencyList).flatMap(([source, targets]) => {
20+
return targets.map((target) => {
21+
return { source, target };
22+
});
23+
})
24+
};
25+
26+
// const jobRunListSampleData = generateJobRunListSampleData(100, 21); // todo: get from api
27+
// const jobDependencyGraphSampleData = generateJobDependencyGraphSampleData(jobRunListSampleData); // todo: get from api
28+
// const data = {
29+
// nodes: Object.entries(jobRunListSampleData).map(([id, jobRuns]) => {
30+
// return {
31+
// id,
32+
// jobRuns
33+
// };
34+
// }),
35+
// edges: Object.entries(jobDependencyGraphSampleData.adjacencyList).flatMap(
36+
// ([source, targets]) => {
37+
// return targets.map((target) => {
38+
// return { source, target };
39+
// });
40+
// }
41+
// )
42+
// };
43+
</script>
44+
45+
<div class="flex gap-2">
46+
<div class="flex-1 h-[calc(100vh-106px)] p-4 border rounded overflow-hidden">
47+
<Chart
48+
{data}
49+
transform={{
50+
mode: 'canvas',
51+
initialScrollMode: 'scale',
52+
tweened: { duration: 800, easing: cubicOut }
53+
}}
54+
>
55+
<TransformControls />
56+
57+
<Svg>
58+
<Dagre {data} direction="left-right" let:nodes let:edges>
59+
<!-- {#snippet children({ nodes, edges })} -->
60+
<g class="edges">
61+
{#each edges as edge (edge.v + '-' + edge.w)}
62+
<Spline
63+
data={edge.points}
64+
x="x"
65+
y="y"
66+
class="stroke-neutral-400 stroke-[1.5]"
67+
tweened
68+
curve={curveBasis}
69+
markerEnd="triangle"
70+
/>
71+
{/each}
72+
</g>
73+
74+
<g class="nodes">
75+
{#each nodes as node (node.label)}
76+
<Group x={node.x - node.width / 2} y={node.y - node.height / 2} tweened>
77+
<Rect
78+
width={node.width}
79+
height={node.height}
80+
class="fill-neutral-300 stroke-neutral-400"
81+
rx={10}
82+
/>
83+
84+
<Text
85+
value={node.label}
86+
x={node.width / 2}
87+
y={node.height / 2}
88+
dy={-2}
89+
textAnchor="middle"
90+
verticalAnchor="middle"
91+
class={cn('text-xs pointer-events-none')}
92+
/>
93+
</Group>
94+
{/each}
95+
</g>
96+
<!-- {/snippet} -->
97+
</Dagre>
98+
</Svg>
99+
</Chart>
100+
</div>
101+
</div>

0 commit comments

Comments
 (0)