|
1 | 1 | <script setup lang="ts"> |
2 | | -import type { Asset as AssetInfo } from '@rolldown/debug' |
3 | 2 | import type { GraphBase, GraphBaseOptions } from 'nanovis' |
4 | | -import type { SessionContext } from '~~/shared/types' |
5 | 3 | import type { AssetChartInfo, AssetChartNode } from '~/types/assets' |
6 | | -import { useRouter } from '#app/composables/router' |
7 | | -import { useMouse } from '@vueuse/core' |
8 | | -import { createColorGetterSpectrum, Treemap } from 'nanovis' |
9 | | -import { computed, nextTick, onUnmounted, reactive, ref, shallowRef, watch } from 'vue' |
10 | | -import { isDark } from '~/composables/dark' |
11 | | -import { settings } from '~/state/settings' |
12 | | -import { bytesToHumanSize } from '~/utils/format' |
| 4 | +import { useTemplateRef, watchEffect } from 'vue' |
13 | 5 |
|
14 | 6 | const props = defineProps<{ |
15 | | - assets: AssetInfo[] |
16 | | - session: SessionContext |
| 7 | + graph: GraphBase<AssetChartInfo | undefined, GraphBaseOptions<AssetChartInfo | undefined>> |
| 8 | + selected?: AssetChartNode | undefined |
17 | 9 | }>() |
18 | 10 |
|
19 | | -const router = useRouter() |
20 | | -const mouse = reactive(useMouse()) |
21 | | -const graph = shallowRef<GraphBase<AssetChartInfo | undefined, GraphBaseOptions<AssetChartInfo | undefined>> | undefined>(undefined) |
22 | | -const nodeHover = shallowRef<AssetChartNode | undefined>(undefined) |
23 | | -const nodeSelected = shallowRef<AssetChartNode | undefined>(undefined) |
24 | | -const selectedNode = ref<AssetChartInfo | undefined>(undefined) |
25 | | -let dispose: () => void | undefined |
26 | | -
|
27 | | -const tree = computed(() => { |
28 | | - const assets = props.assets |
29 | | - const map = new Map<string, AssetChartNode>() |
30 | | - let maxDepth = 0 |
31 | | -
|
32 | | - const root: AssetChartNode = { |
33 | | - id: '~root', |
34 | | - text: 'Project', |
35 | | - size: 0, |
36 | | - sizeSelf: 0, |
37 | | - children: [], |
38 | | - } |
39 | | -
|
40 | | - const macrosTasks: (() => void)[] = [] |
41 | | -
|
42 | | - macrosTasks.unshift(() => { |
43 | | - root.size += root.children.reduce((acc, i) => acc + i.size, 0) |
44 | | - root.subtext = bytesToHumanSize(root.size).join(' ') |
45 | | - root.children.sort((a, b) => b.size - a.size || a.id.localeCompare(b.id)) |
46 | | - }) |
47 | | -
|
48 | | - function assetToNode(asset: AssetInfo, path: string, name: string, parent: AssetChartNode, depth: number): AssetChartNode { |
49 | | - if (map.has(path)) { |
50 | | - return map.get(path)! |
51 | | - } |
52 | | -
|
53 | | - if (depth > maxDepth) { |
54 | | - maxDepth = depth |
55 | | - } |
56 | | -
|
57 | | - const node: AssetChartNode = { |
58 | | - id: path, |
59 | | - text: name, |
60 | | - size: 0, |
61 | | - sizeSelf: 0, |
62 | | - children: [], |
63 | | - meta: { |
64 | | - chunk_id: 0, |
65 | | - content: '', |
66 | | - filename: '', |
67 | | - size: 0, |
68 | | - path: name, |
69 | | - type: 'folder', |
70 | | - }, |
71 | | - parent, |
72 | | - } |
73 | | -
|
74 | | - map.set(path, node) |
75 | | - parent.children.push(node) |
76 | | -
|
77 | | - macrosTasks.unshift(() => { |
78 | | - const selfSize = node.sizeSelf |
79 | | - node.size += node.children.reduce((acc, i) => acc + i.size, 0) |
80 | | - node.subtext = bytesToHumanSize(node.size).join(' ') |
81 | | -
|
82 | | - if (node.children.length && selfSize / node.size > 0.1) { |
83 | | - node.children.push({ |
84 | | - id: `${node.id}-self`, |
85 | | - text: '', |
86 | | - size: selfSize, |
87 | | - sizeSelf: selfSize, |
88 | | - subtext: bytesToHumanSize(selfSize).join(' '), |
89 | | - children: [], |
90 | | - meta: { |
91 | | - ...asset, |
92 | | - path: '', |
93 | | - type: 'file', |
94 | | - }, |
95 | | - parent: node, |
96 | | - }) |
97 | | - } |
98 | | -
|
99 | | - node.children.sort((a, b) => b.size - a.size || a.id.localeCompare(b.id)) |
100 | | - }) |
101 | | -
|
102 | | - return node |
103 | | - } |
104 | | -
|
105 | | - function processAsset(asset: AssetInfo) { |
106 | | - const parts = asset.filename.split('/').filter(Boolean) |
107 | | - let current = root |
108 | | - let currentPath = '' |
109 | | - let depth = 0 |
110 | | -
|
111 | | - parts.forEach((part, index) => { |
112 | | - currentPath += (currentPath ? '/' : '') + part |
113 | | - depth++ |
114 | | -
|
115 | | - if (index === parts.length - 1) { |
116 | | - const fileNode: AssetChartNode = { |
117 | | - id: asset.filename, |
118 | | - text: part, |
119 | | - size: asset.size, |
120 | | - sizeSelf: asset.size, |
121 | | - subtext: bytesToHumanSize(asset.size).join(' '), |
122 | | - children: [], |
123 | | - meta: { |
124 | | - ...asset, |
125 | | - path: part, |
126 | | - type: 'file', |
127 | | - }, |
128 | | - } |
129 | | -
|
130 | | - current.children.push(fileNode) |
131 | | - map.set(asset.filename, fileNode) |
132 | | - } |
133 | | - else { |
134 | | - current = assetToNode(asset, currentPath, part, current, depth) |
135 | | - } |
136 | | - }) |
137 | | - } |
138 | | -
|
139 | | - assets.forEach(processAsset) |
140 | | -
|
141 | | - macrosTasks.forEach(fn => fn()) |
142 | | -
|
143 | | - return { |
144 | | - map, |
145 | | - root, |
146 | | - maxDepth, |
147 | | - } |
148 | | -}) |
149 | | -
|
150 | | -const options = computed<GraphBaseOptions<AssetChartInfo | undefined>>(() => { |
151 | | - return { |
152 | | - onClick(node) { |
153 | | - if (node) |
154 | | - nodeHover.value = node |
155 | | - if (node.meta?.type === 'file') { |
156 | | - selectedNode.value = node.meta |
157 | | - router.replace({ query: { asset: node.meta.filename } }) |
158 | | - } |
159 | | - }, |
160 | | - onHover(node) { |
161 | | - if (node) |
162 | | - nodeHover.value = node |
163 | | - }, |
164 | | - onLeave() { |
165 | | - nodeHover.value = undefined |
166 | | - }, |
167 | | - onSelect(node) { |
168 | | - nodeSelected.value = node || tree.value.root |
169 | | - selectedNode.value = node?.meta |
170 | | - }, |
171 | | - animate: settings.value.chartAnimation, |
172 | | - palette: { |
173 | | - stroke: isDark.value ? '#222' : '#555', |
174 | | - fg: isDark.value ? '#fff' : '#000', |
175 | | - bg: isDark.value ? '#111' : '#fff', |
176 | | - }, |
177 | | - getColor: createColorGetterSpectrum( |
178 | | - tree.value.root, |
179 | | - isDark.value ? 0.8 : 0.9, |
180 | | - isDark.value ? 1 : 1.1, |
181 | | - ), |
182 | | - getSubtext: (node) => { |
183 | | - return node.subtext |
184 | | - }, |
185 | | - } |
186 | | -}) |
187 | | -
|
188 | | -function selectNode(node: AssetChartNode | null, animate?: boolean) { |
189 | | - selectedNode.value = node?.meta |
190 | | - if (!node?.children.length) |
191 | | - node = node?.parent || null |
192 | | - graph.value?.select(node, animate) |
193 | | -} |
194 | | -
|
195 | | -watch(() => [tree.value, options.value], () => { |
196 | | - dispose?.() |
197 | | -
|
198 | | - nodeSelected.value = tree.value.root |
199 | | -
|
200 | | - if (tree.value?.root) { |
201 | | - graph.value = new Treemap(tree.value.root, { |
202 | | - ...options.value, |
203 | | - selectedPaddingRatio: 0, |
204 | | - }) |
205 | | - } |
206 | | - nextTick(() => { |
207 | | - const selected = selectedNode.value ? tree.value.map.get(selectedNode.value.filename) || null : null |
208 | | - if (selected) |
209 | | - selectNode(selected, false) |
210 | | - }) |
211 | | -
|
212 | | - dispose = () => { |
213 | | - graph.value?.dispose() |
214 | | - graph.value = undefined |
215 | | - } |
216 | | -}, { |
217 | | - deep: true, |
218 | | - immediate: true, |
219 | | -}) |
| 11 | +const emit = defineEmits<{ |
| 12 | + (e: 'select', node: AssetChartNode | null): void |
| 13 | +}>() |
220 | 14 |
|
221 | | -onUnmounted(() => { |
222 | | - dispose?.() |
223 | | -}) |
| 15 | +const el = useTemplateRef<HTMLDivElement>('el') |
| 16 | +watchEffect(() => el.value?.append(props.graph.el)) |
224 | 17 | </script> |
225 | 18 |
|
226 | 19 | <template> |
227 | | - <div p4> |
228 | | - <ChartAssetTreemap |
229 | | - v-if="graph" |
230 | | - :graph="graph" |
231 | | - :selected="nodeSelected" |
232 | | - @select="x => selectNode(x)" |
233 | | - /> |
234 | | - </div> |
235 | | - <div |
236 | | - v-if="nodeHover?.meta" |
237 | | - bg-glass fixed z-panel-nav border="~ base rounded" p2 text-sm |
238 | | - flex="~ col gap-2" |
239 | | - :style="{ |
240 | | - left: `${mouse.x + 10}px`, |
241 | | - top: `${mouse.y + 10}px`, |
242 | | - }" |
243 | | - > |
244 | | - <div flex="~ gap-1 items-center"> |
245 | | - {{ nodeHover.text }} |
246 | | - </div> |
247 | | - <div flex="~ gap-1 items-center"> |
248 | | - <DisplayFileSizeBadge :bytes="nodeHover.size" :percent="false" /> |
249 | | - </div> |
250 | | - </div> |
| 20 | + <ChartAssetNavBreadcrumb |
| 21 | + border="b base" py2 min-h-10 |
| 22 | + :selected="selected" |
| 23 | + :options="graph.options" |
| 24 | + @select="emit('select', $event)" |
| 25 | + /> |
| 26 | + <div ref="el" /> |
251 | 27 | </template> |
0 commit comments