11<script setup lang="ts">
22import type { RolldownChunkInfo , SessionContext } from ' ~~/shared/types/data'
33import type { ClientSettings } from ' ~/state/settings'
4+ import type { ChunkChartInfo , ChunkChartNode } from ' ~/types/chart'
45import { useRpc } from ' #imports'
5- import { computedWithControl , useAsyncState } from ' @vueuse/core'
6+ import { computedWithControl , useAsyncState , useMouse } from ' @vueuse/core'
67import Fuse from ' fuse.js'
7- import { computed , ref } from ' vue'
8+ import { Flamegraph , Sunburst , Treemap } from ' nanovis'
9+ import { computed , reactive , ref , watch } from ' vue'
10+ import ChartTreemap from ' ~/components/chart/Treemap.vue'
11+ import { useChartGraph } from ' ~/composables/chart'
812import { useGraphPathManager } from ' ~/composables/graph-path-selector'
913import { settings } from ' ~/state/settings'
1014
1115const props = defineProps <{
1216 session: SessionContext
1317}>()
1418
19+ const mouse = reactive (useMouse ())
20+
1521const chunkViewTypes = [
1622 {
1723 label: ' List' ,
@@ -28,6 +34,21 @@ const chunkViewTypes = [
2834 value: ' graph' ,
2935 icon: ' i-ph-graph-duotone' ,
3036 },
37+ {
38+ label: ' Treemap' ,
39+ value: ' treemap' ,
40+ icon: ' i-ph-checkerboard-duotone' ,
41+ },
42+ {
43+ label: ' Sunburst' ,
44+ value: ' sunburst' ,
45+ icon: ' i-ph-chart-donut-duotone' ,
46+ },
47+ {
48+ label: ' Flamegraph' ,
49+ value: ' flamegraph' ,
50+ icon: ' i-ph-chart-bar-horizontal-duotone' ,
51+ },
3152] as const
3253
3354const searchValue = ref <{ search: string | false }>({
@@ -89,6 +110,95 @@ const { pathSelectorVisible, pathNodes, selectPathNodes, togglePathSelector, nor
89110function toggleDisplay(type : ClientSettings [' chunkViewType' ]) {
90111 settings .value .chunkViewType = type
91112}
113+
114+ // Calculate chunk size from modules
115+ const modulesMap = computed (() => {
116+ const map = new Map ()
117+ for (const module of props .session .modulesList ) {
118+ map .set (module .id , module )
119+ }
120+ return map
121+ })
122+
123+ function getChunkSize(chunk : RolldownChunkInfo ): number {
124+ // First try to use asset size if available
125+ if (chunk .asset ?.size ) {
126+ return chunk .asset .size
127+ }
128+
129+ // Otherwise, calculate from module transforms
130+ return chunk .modules .reduce ((total , id ) => {
131+ const moduleInfo = modulesMap .value .get (id )
132+ if (! moduleInfo || ! moduleInfo .buildMetrics ?.transforms ?.length )
133+ return total
134+
135+ const transforms = moduleInfo .buildMetrics .transforms
136+ return total + transforms [transforms .length - 1 ]! .transformed_code_size
137+ }, 0 )
138+ }
139+
140+ // Normalize chunks with size for chart visualization
141+ const chunksWithSize = computed (() => {
142+ return searched .value .map (chunk => ({
143+ ... chunk ,
144+ filename: chunk .name || ` chunk-${chunk .chunk_id } ` ,
145+ size: getChunkSize (chunk ),
146+ }))
147+ })
148+
149+ // Chart graph setup for nanovis visualizations
150+ const { tree, chartOptions, graph, nodeHover, nodeSelected, selectedNode, selectNode, buildGraph } = useChartGraph <
151+ RolldownChunkInfo & { id : string , filename : string , size : number },
152+ ChunkChartInfo ,
153+ ChunkChartNode
154+ > ({
155+ data: chunksWithSize ,
156+ nameKey: ' filename' ,
157+ sizeKey: ' size' ,
158+ rootText: ' Chunks' ,
159+ nodeType: ' chunk' ,
160+ graphOptions: {
161+ onClick(node ) {
162+ if (node )
163+ nodeHover .value = node
164+ },
165+ onHover(node ) {
166+ if (node )
167+ nodeHover .value = node
168+ if (node === null )
169+ nodeHover .value = undefined
170+ },
171+ onLeave() {
172+ nodeHover .value = undefined
173+ },
174+ onSelect(node ) {
175+ nodeSelected .value = node || tree .value .root
176+ selectedNode .value = node ?.meta
177+ },
178+ },
179+ onUpdate() {
180+ switch (settings .value .chunkViewType ) {
181+ case ' sunburst' :
182+ graph .value = new Sunburst (tree .value .root , chartOptions .value )
183+ break
184+ case ' treemap' :
185+ graph .value = new Treemap (tree .value .root , {
186+ ... chartOptions .value ,
187+ selectedPaddingRatio: 0 ,
188+ })
189+ break
190+ case ' flamegraph' :
191+ graph .value = new Flamegraph (tree .value .root , chartOptions .value )
192+ break
193+ }
194+ },
195+ })
196+
197+ watch (() => settings .value .chunkViewType , () => {
198+ if ([' treemap' , ' sunburst' , ' flamegraph' ].includes (settings .value .chunkViewType )) {
199+ buildGraph ()
200+ }
201+ })
92202 </script >
93203
94204<template >
@@ -166,5 +276,64 @@ function toggleDisplay(type: ClientSettings['chunkViewType']) {
166276 :entry-id =" pathNodes.start"
167277 />
168278 </template >
279+ <template v-else-if =" settings .chunkViewType === ' treemap' " >
280+ <div of-auto h-screen flex =" ~ col gap-2" pt32 >
281+ <ChartTreemap
282+ v-if =" graph" :graph =" graph"
283+ :selected =" nodeSelected"
284+ @select =" x => selectNode(x)"
285+ >
286+ <template #default =" { selected , options , onSelect } " >
287+ <ChartNavBreadcrumb
288+ border =" b base" py2 min-h-10
289+ :selected =" selected"
290+ :options =" options"
291+ @select =" onSelect"
292+ />
293+ </template >
294+ </ChartTreemap >
295+ </div >
296+ </template >
297+ <template v-else-if =" settings .chunkViewType === ' sunburst' " >
298+ <div of-auto h-screen flex =" ~ col gap-2" pt32 >
299+ <ChunksSunburst
300+ v-if =" graph" :graph =" graph"
301+ :selected =" nodeSelected"
302+ @select =" x => selectNode(x)"
303+ />
304+ </div >
305+ </template >
306+ <template v-else-if =" settings .chunkViewType === ' flamegraph' " >
307+ <div of-auto h-screen flex =" ~ col gap-2" pt32 >
308+ <ChunksFlamegraph
309+ v-if =" graph" :graph =" graph"
310+ />
311+ </div >
312+ </template >
313+ <DisplayGraphHoverView :hover-x =" mouse.x" :hover-y =" mouse.y" >
314+ <div
315+ v-if =" nodeHover?.meta"
316+ border =" ~ base rounded-lg" bg-base p2
317+ flex =" ~ col gap-2"
318+ min-w-50
319+ shadow-lg
320+ >
321+ <div flex =" ~ gap-2 items-center" >
322+ <i i-ph-shapes-duotone flex-none />
323+ <span truncate >{{ nodeHover.meta.name || '[unnamed]' }}</span >
324+ </div >
325+ <div flex =" ~ gap-2 items-center" >
326+ <span op50 text-xs >Size:</span >
327+ <DisplayFileSizeBadge :bytes =" nodeHover.meta.size" text-xs />
328+ </div >
329+ <div v-if =" nodeHover.meta.modules?.length" flex =" ~ gap-2 items-center" >
330+ <span op50 text-xs >Modules:</span >
331+ <span text-xs >{{ nodeHover.meta.modules?.length }}</span >
332+ </div >
333+ <div v-if =" nodeHover.meta.is_initial" flex =" ~ gap-2 items-center" >
334+ <DisplayBadge text =" initial" />
335+ </div >
336+ </div >
337+ </DisplayGraphHoverView >
169338 </div >
170339</template >
0 commit comments