Skip to content

Commit e8c03d3

Browse files
Update ui logic
1 parent f763f85 commit e8c03d3

File tree

2 files changed

+161
-130
lines changed

2 files changed

+161
-130
lines changed

src/www/App.tsx

+64-57
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,17 @@ const LicenseText = () => {
9494
)
9595
}
9696

97+
const EmptyGraphRoot: ConvertedTreeNode = {
98+
type: "leaf",
99+
count: 0,
100+
size: 0,
101+
name: "(empty)",
102+
}
103+
97104
const App = () => {
98105
const theme = useTheme()
99106

100-
const [treeData, setTreeData] = useState<ConvertedTreeNode | null>(null)
107+
const [treeData, setTreeData] = useState<ConvertedTreeNode>(EmptyGraphRoot)
101108

102109
const { enqueueSnackbar } = useSnackbar()
103110

@@ -107,6 +114,7 @@ const App = () => {
107114
const [fileName, setFileName] = useState<string>("")
108115
const [fileType, setFileType] = useState("txt")
109116
const [attempted, setAttempted] = useState(false)
117+
const [errored, setErrored] = useState(false)
110118

111119
const [scaleFuncSel, setScaleFuncSel] = useState("none")
112120
const [scaleExp, setScaleExp] = useState(0.75)
@@ -132,6 +140,48 @@ const App = () => {
132140
return func
133141
}, [scaleFuncSel, scaleExp])
134142

143+
async function drawGraph() {
144+
setAttempted(true)
145+
try {
146+
const inputText = await inputRef.current?.files?.item(0)?.text()
147+
if (!inputText) {
148+
throw new Error("Failed to fetch input file content.")
149+
}
150+
let parsed: ParsedModule[]
151+
switch (fileType) {
152+
case "txt":
153+
parsed = parseTextStats(inputText)
154+
break
155+
case "json":
156+
parsed = parseYosysJsonStats(inputText)
157+
break
158+
default:
159+
throw new Error(`Invalid file type: ${fileType}`)
160+
}
161+
const converted = convertParsedModules(parsed, true)
162+
setTreeData(converted)
163+
setErrored(false)
164+
} catch (err) {
165+
setErrored(true)
166+
console.error(err)
167+
err = err instanceof Error ? err : new Error(`${err}`)
168+
enqueueSnackbar({
169+
message: `${err}`,
170+
variant: "error",
171+
})
172+
}
173+
}
174+
175+
async function clearGraph() {
176+
setAttempted(false)
177+
setErrored(false)
178+
setFileName("")
179+
setTreeData(EmptyGraphRoot)
180+
if (inputRef.current) {
181+
inputRef.current.value = ""
182+
}
183+
}
184+
135185
const {
136186
offlineReady: [offlineReady, setOfflineReady],
137187
needRefresh: [needRefresh, setNeedRefresh],
@@ -228,8 +278,9 @@ const App = () => {
228278
type="file"
229279
accept={fileType === "txt" ? ".txt,text/plain" : ".json,application/json"}
230280
ref={inputRef}
231-
onChange={(ev) => {
281+
onChange={async (ev) => {
232282
setFileName(ev.target.files?.[0]?.name ?? "")
283+
await drawGraph()
233284
}}
234285
style={{
235286
display: "none",
@@ -243,7 +294,7 @@ const App = () => {
243294
fullWidth
244295
label="File name"
245296
value={fileName}
246-
error={attempted && !fileName}
297+
error={attempted && errored}
247298
slotProps={{
248299
input: {
249300
readOnly: true,
@@ -272,56 +323,14 @@ const App = () => {
272323
</CenteringGrid>
273324
<CenteringGrid size={1}>
274325
<Tooltip title="Graph">
275-
<IconButton
276-
size="large"
277-
color="primary"
278-
onClick={async () => {
279-
setAttempted(true)
280-
try {
281-
const inputText = await inputRef.current?.files?.item(0)?.text()
282-
if (!inputText) {
283-
throw new Error("Failed to fetch input file content.")
284-
}
285-
let parsed: ParsedModule[]
286-
switch (fileType) {
287-
case "txt":
288-
parsed = parseTextStats(inputText)
289-
break
290-
case "json":
291-
parsed = parseYosysJsonStats(inputText)
292-
break
293-
default:
294-
throw new Error(`Invalid file type: ${fileType}`)
295-
}
296-
const converted = convertParsedModules(parsed, true)
297-
setTreeData(converted)
298-
} catch (err) {
299-
console.error(err)
300-
err = err instanceof Error ? err : new Error(`${err}`)
301-
enqueueSnackbar({
302-
message: `${err}`,
303-
variant: "error",
304-
})
305-
}
306-
}}
307-
>
326+
<IconButton size="large" color="primary" onClick={drawGraph}>
308327
<PieChartIcon />
309328
</IconButton>
310329
</Tooltip>
311330
</CenteringGrid>
312331
<CenteringGrid size={1}>
313332
<Tooltip title="Clear">
314-
<IconButton
315-
size="large"
316-
onClick={() => {
317-
setAttempted(false)
318-
setFileName("")
319-
setTreeData(null)
320-
if (inputRef.current) {
321-
inputRef.current.value = ""
322-
}
323-
}}
324-
>
333+
<IconButton size="large" onClick={clearGraph}>
325334
<DeleteIcon />
326335
</IconButton>
327336
</Tooltip>
@@ -373,16 +382,14 @@ const App = () => {
373382
overflowX: "scroll",
374383
}}
375384
>
376-
{treeData ? (
377-
<Paper
378-
elevation={2}
379-
sx={{
380-
padding: "0.5rem",
381-
}}
382-
>
383-
<D3Treemap data={treeData} scaleFunc={scaleFunc} width={1280} height={720} />
384-
</Paper>
385-
) : null}
385+
<Paper
386+
elevation={2}
387+
sx={{
388+
padding: "0.5rem",
389+
}}
390+
>
391+
<D3Treemap data={treeData} scaleFunc={scaleFunc} width={1280} height={720} />
392+
</Paper>
386393
</Container>
387394
<Divider />
388395
<Container maxWidth="md" component="section">

src/www/D3Treemap.tsx

+97-73
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { styled, useTheme } from "@mui/material"
66
import { ConvertedTreeNode } from "../convert"
77
import DetailsDrawer, { DetailsNode } from "./DetailsDrawer"
88

9+
type NodeType = d3.HierarchyRectangularNode<ConvertedTreeNode>
10+
911
function scaleAdjust(tree: ConvertedTreeNode, f: (x: number) => number): ConvertedTreeNode {
1012
switch (tree.type) {
1113
case "leaf":
@@ -34,14 +36,96 @@ const AnimatedSVGGroup = styled("g")(({ theme }) => ({
3436
},
3537
}))
3638

39+
const TreemapNode = (props: {
40+
node: NodeType
41+
i: number
42+
xScale: d3.ScaleContinuousNumeric<number, number, never>
43+
yScale: d3.ScaleContinuousNumeric<number, number, never>
44+
setDetailsNode: (x: DetailsNode | null) => void
45+
setDetailsOpen: (x: boolean) => void
46+
}) => {
47+
function getNamePath(d: NodeType) {
48+
return d
49+
.ancestors()
50+
.reverse()
51+
.map((d) => d.data.name)
52+
}
53+
54+
const [x, y] = [props.xScale(props.node.x0), props.yScale(props.node.y0)]
55+
const [rectWidth, rectHeight] = [
56+
props.xScale(props.node.x1) - props.xScale(props.node.x0),
57+
props.yScale(props.node.y1) - props.yScale(props.node.y0),
58+
]
59+
60+
const theme = useTheme()
61+
const strokeColor =
62+
theme.palette.mode === "dark" ? theme.palette.grey["400"] : theme.palette.grey["900"]
63+
64+
const leafId = useId()
65+
const clipId = useId()
66+
67+
let area: number | null = null
68+
switch (props.node.data.type) {
69+
case "leaf":
70+
case "adj-leaf":
71+
area = props.node.data.size
72+
break
73+
case "internal":
74+
area = null
75+
break
76+
}
77+
78+
return (
79+
<AnimatedSVGGroup
80+
key={props.i}
81+
transform={`translate(${x},${y})`}
82+
onClick={() => {
83+
props.setDetailsNode({ ...props.node.data, path: getNamePath(props.node) })
84+
props.setDetailsOpen(true)
85+
}}
86+
>
87+
<title>
88+
{getNamePath(props.node).join("/")}
89+
{area ? ` (Area: ${area})` : null}
90+
</title>
91+
<rect id={leafId} stroke={strokeColor} width={rectWidth} height={rectHeight} />
92+
<clipPath id={clipId}>
93+
<use xlinkHref={new URL(`#${leafId}`, location.toString()).toString()} />
94+
</clipPath>
95+
<text
96+
clipPath={clipId}
97+
x={3}
98+
fill={theme.palette.text.primary}
99+
style={{
100+
userSelect: "none",
101+
}}
102+
>
103+
{getNamePath(props.node).map((name, i) => {
104+
return (
105+
<tspan key={i} x={3} dy="1em">
106+
{name}
107+
</tspan>
108+
)
109+
})}
110+
{area ? (
111+
<>
112+
<tspan x={3} dy="2em">
113+
Area:
114+
</tspan>
115+
<tspan dx={3}>{area}</tspan>
116+
</>
117+
) : null}
118+
</text>
119+
</AnimatedSVGGroup>
120+
)
121+
}
122+
37123
const D3Treemap = (props: {
38124
data: ConvertedTreeNode
39125
width: number
40126
height: number
41127
scaleFunc: (x: number) => number
42128
}) => {
43-
type NodeType = d3.HierarchyRectangularNode<ConvertedTreeNode>
44-
45129
const theme = useTheme()
46130

47131
const data = scaleAdjust(props.data, props.scaleFunc)
@@ -84,12 +168,6 @@ const D3Treemap = (props: {
84168
[root, height]
85169
)
86170

87-
const getNamePath = (d: NodeType) =>
88-
d
89-
.ancestors()
90-
.reverse()
91-
.map((d) => d.data.name)
92-
93171
const [detailsOpen, setDetailsOpen] = useState(false)
94172
const [detailsNode, setDetailsNode] = useState<DetailsNode | null>(null)
95173

@@ -108,71 +186,17 @@ const D3Treemap = (props: {
108186
borderColor: svgBorderColor,
109187
}}
110188
>
111-
{(root.leaves() ?? []).map((node, i) => {
112-
const [x, y] = [xScale(node.x0), yScale(node.y0)]
113-
const [rectWidth, rectHeight] = [
114-
xScale(node.x1) - xScale(node.x0),
115-
yScale(node.y1) - yScale(node.y0),
116-
]
117-
118-
const leafId = useId()
119-
const clipId = useId()
120-
121-
let area: number | null = null
122-
switch (node.data.type) {
123-
case "leaf":
124-
case "adj-leaf":
125-
area = node.data.size
126-
break
127-
case "internal":
128-
area = null
129-
break
130-
}
131-
132-
return (
133-
<AnimatedSVGGroup
134-
key={i}
135-
transform={`translate(${x},${y})`}
136-
onClick={() => {
137-
setDetailsNode({ ...node.data, path: getNamePath(node) })
138-
setDetailsOpen(true)
139-
}}
140-
>
141-
<title>
142-
{getNamePath(node).join("/")}
143-
{area ? ` (Area: ${area})` : null}
144-
</title>
145-
<rect id={leafId} stroke={svgBorderColor} width={rectWidth} height={rectHeight} />
146-
<clipPath id={clipId}>
147-
<use xlinkHref={new URL(`#${leafId}`, location.toString()).toString()} />
148-
</clipPath>
149-
<text
150-
clipPath={clipId}
151-
x={3}
152-
fill={theme.palette.text.primary}
153-
style={{
154-
userSelect: "none",
155-
}}
156-
>
157-
{getNamePath(node).map((name, i) => {
158-
return (
159-
<tspan key={i} x={3} dy="1em">
160-
{name}
161-
</tspan>
162-
)
163-
})}
164-
{area ? (
165-
<>
166-
<tspan x={3} dy="2em">
167-
Area:
168-
</tspan>
169-
<tspan dx={3}>{area}</tspan>
170-
</>
171-
) : null}
172-
</text>
173-
</AnimatedSVGGroup>
174-
)
175-
})}
189+
{(root.leaves() ?? []).map((node, i) => (
190+
<TreemapNode
191+
node={node}
192+
i={i}
193+
xScale={xScale}
194+
yScale={yScale}
195+
setDetailsNode={setDetailsNode}
196+
setDetailsOpen={setDetailsOpen}
197+
key={i}
198+
/>
199+
))}
176200
</svg>
177201

178202
<DetailsDrawer

0 commit comments

Comments
 (0)