1- import React , { useCallback , useEffect , useMemo , useState } from "react" ;
1+ import React , { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
22import { group } from "d3-array" ;
3- import type { HierarchyNode , HierarchyRectangularNode } from "d3-hierarchy" ;
3+ import type { HierarchyNode } from "d3-hierarchy" ;
44import { hierarchy , treemap , treemapSquarify } from "d3-hierarchy" ;
55import { useTitle , useWindowSize } from "react-use" ;
66
@@ -11,6 +11,8 @@ import { Node } from "./Node.tsx";
1111
1212import "./style.scss" ;
1313import { trimPrefix } from "./tool/utils.ts" ;
14+ import { shallowCopy } from "./tool/copy.ts" ;
15+ import { useMouse } from "./tool/useMouse.ts" ;
1416
1517interface TreeMapProps {
1618 entry : Entry ;
@@ -26,17 +28,36 @@ function TreeMap({ entry }: TreeMapProps) {
2628 const { width, height } = useWindowSize ( ) ;
2729
2830 const rawHierarchy = useMemo ( ( ) => {
29- return hierarchy ( entry , e => e . getChildren ( ) ) ;
31+ return hierarchy ( entry , e => e . getChildren ( ) )
32+ . sum ( ( e ) => {
33+ if ( e . getChildren ( ) . length === 0 ) {
34+ return e . getSize ( ) ;
35+ }
36+ return 0 ;
37+ } )
38+ . sort ( ( a , b ) => a . data . getSize ( ) - b . data . getSize ( ) ) ;
3039 } , [ entry ] ) ;
3140
32- const getModuleColor = useMemo ( ( ) => {
41+ const rawHierarchyID = useMemo ( ( ) => {
42+ const cache = new Map < number , HierarchyNode < Entry > > ( ) ;
43+ rawHierarchy . descendants ( ) . forEach ( ( node ) => {
44+ cache . set ( node . data . getID ( ) , node ) ;
45+ } ) ;
46+ return cache ;
47+ } , [ rawHierarchy ] ) ;
48+
49+ const getModuleColorRaw = useMemo ( ( ) => {
3350 return createRainbowColor ( rawHierarchy ) ;
3451 } , [ rawHierarchy ] ) ;
3552
53+ const getModuleColor = useCallback ( ( id : number ) => {
54+ return getModuleColorRaw ( rawHierarchyID . get ( id ) ! ) ;
55+ } , [ getModuleColorRaw , rawHierarchyID ] ) ;
56+
3657 const layout = useMemo ( ( ) => {
3758 return treemap < Entry > ( )
3859 . size ( [ width , height ] )
39- . paddingInner ( 2 )
60+ . paddingInner ( 1 )
4061 . paddingTop ( 20 )
4162 . round ( true )
4263 . tile ( treemapSquarify ) ;
@@ -64,66 +85,66 @@ function TreeMap({ entry }: TreeMapProps) {
6485 cur = found ;
6586 }
6687
67- return cur ;
88+ return cur . data . getID ( ) ;
6889 } , [ entry , rawHierarchy ] ) ;
6990
70- const [ selectedNode , setSelectedNode ] = useState < HierarchyNode < Entry > | null > ( loadNodeFromHash ) ;
71- const selectedNodeLeaveIDSet = useMemo ( ( ) => {
72- if ( selectedNode === null ) {
73- return new Set < number > ( ) ;
91+ const [ selectedNodeID , setSelectedNodeID ] = useState < number | null > ( loadNodeFromHash ) ;
92+
93+ const layoutRoot = useMemo ( ( ) => {
94+ let root : HierarchyNode < Entry > | null ;
95+ if ( ! selectedNodeID || selectedNodeID === rawHierarchy . data . getID ( ) ) {
96+ root = rawHierarchy ;
7497 }
98+ else {
99+ const selectedNode = rawHierarchyID . get ( selectedNodeID ) ! ;
100+ const ancestors = selectedNode . ancestors ( ) . reverse ( ) ;
75101
76- return new Set ( selectedNode . leaves ( ) . map ( d => d . data . getID ( ) ) ) ;
77- } , [ selectedNode ] ) ;
102+ function writeValue ( n : HierarchyNode < Entry > , value ?: number ) {
103+ // @ts -expect-error write to readonly
104+ // noinspection JSConstantReassignment
105+ n . value = value ;
106+ }
78107
79- const root = useMemo ( ( ) => {
80- const rootWithSizesAndSorted = rawHierarchy
81- . sum ( ( node ) => {
82- if ( node . getChildren ( ) . length === 0 ) {
83- if ( selectedNode ) {
84- if ( ! selectedNodeLeaveIDSet . has ( node . getID ( ) ) ) {
85- return 0 ;
86- }
87- }
108+ root = shallowCopy ( rawHierarchy ) ;
109+ writeValue ( root , selectedNode . value ) ;
88110
89- return node . getSize ( ) ;
90- }
91- return 0 ;
92- } )
93- . sort ( ( a , b ) => a . data . getSize ( ) - b . data . getSize ( ) ) ;
111+ let cur = root ;
112+ for ( let i = 1 ; i < ancestors . length ; i ++ ) {
113+ // use shallowCopy
114+ const node = shallowCopy ( ancestors [ i ] ) ;
115+ writeValue ( node , selectedNode . value ) ;
116+ cur . children = [ node ] ;
117+ cur = node ;
118+ }
119+ }
94120
95- return layout ( rootWithSizesAndSorted ) ;
96- } , [ layout , rawHierarchy , selectedNode , selectedNodeLeaveIDSet ] ) ;
121+ return layout ( root ! ) ;
122+ } , [ layout , rawHierarchy , rawHierarchyID , selectedNodeID ] ) ;
97123
98- const nestedData = useMemo ( ( ) => {
99- const nestedDataMap = group (
100- root . descendants ( ) ,
124+ const layers = useMemo ( ( ) => {
125+ const layerMap = group (
126+ layoutRoot . descendants ( ) ,
101127 ( d : HierarchyNode < Entry > ) => d . height ,
102128 ) ;
103- const nested = Array . from ( nestedDataMap , ( [ key , values ] ) => ( {
129+ const layerArray = Array . from ( layerMap , ( [ key , values ] ) => ( {
104130 key,
105131 values,
106132 } ) ) ;
107- nested . sort ( ( a , b ) => b . key - a . key ) ;
108- return nested ;
109- } , [ root ] ) ;
110-
111- const allNodes = useMemo ( ( ) => {
112- const cache = new Map < number , HierarchyRectangularNode < Entry > > ( ) ;
113- root . descendants ( ) . forEach ( ( node ) => {
114- cache . set ( node . data . getID ( ) , node ) ;
115- } ) ;
116- return cache ;
117- } , [ root ] ) ;
133+ layerArray . sort ( ( a , b ) => b . key - a . key ) ;
134+ return layerArray ;
135+ } , [ layoutRoot ] ) ;
118136
119137 useEffect ( ( ) => {
120- if ( selectedNode === null ) {
138+ if ( selectedNodeID === null ) {
121139 if ( location . hash !== "" ) {
122140 history . replaceState ( null , "" , " " ) ;
123141 }
124142 return ;
125143 }
126-
144+ const selectedNode = rawHierarchyID . get ( selectedNodeID ) ;
145+ if ( ! selectedNode ) {
146+ return ;
147+ }
127148 const path = `#${ selectedNode
128149 . ancestors ( )
129150 . map ( ( d ) => {
@@ -135,27 +156,21 @@ function TreeMap({ entry }: TreeMapProps) {
135156 if ( location . hash !== path ) {
136157 history . replaceState ( null , "" , path ) ;
137158 }
138- } , [ selectedNode ] ) ;
159+ } , [ rawHierarchyID , selectedNodeID ] ) ;
139160
140- const [ showTooltip , setShowTooltip ] = useState ( false ) ;
141- const [ tooltipPosition , setTooltipPosition ] = useState < [ number , number ] > ( [ 0 , 0 ] ) ;
142161 const [ tooltipNode , setTooltipNode ]
143- = useState < HierarchyRectangularNode < Entry > | undefined > ( undefined ) ;
144-
145- const onMouseEnter = useCallback ( ( ) => {
146- setShowTooltip ( true ) ;
147- } , [ ] ) ;
148-
149- const onMouseLeave = useCallback ( ( ) => {
150- setShowTooltip ( false ) ;
151- } , [ ] ) ;
152-
153- const getTargetNode = useCallback ( ( e : React . MouseEvent < SVGSVGElement > ) => {
154- if ( ! e . target ) {
155- return null ;
156- }
157-
158- const target = ( e . target as SVGElement ) . parentNode ;
162+ = useState < Entry | undefined > ( undefined ) ;
163+ const svgRef = useRef < SVGSVGElement > ( null ) ;
164+
165+ const {
166+ clientX,
167+ clientY,
168+ isOver,
169+ eventTarget : mouseEventTarget ,
170+ } = useMouse ( svgRef ) ;
171+
172+ const getTargetNode = useCallback ( ( e : EventTarget ) => {
173+ const target = ( e as SVGElement ) . parentNode ;
159174 if ( ! target ) {
160175 return null ;
161176 }
@@ -167,85 +182,83 @@ function TreeMap({ entry }: TreeMapProps) {
167182
168183 const dataId = Number . parseInt ( dataIdStr ) ;
169184
170- return allNodes . get ( dataId ) ?? null ;
171- } , [ allNodes ] ) ;
172-
173- const onMouseMove = useCallback ( ( e : React . MouseEvent < SVGSVGElement > ) => {
174- setTooltipPosition ( [ e . clientX , e . clientY ] ) ;
185+ return rawHierarchyID . get ( dataId ) ?. data ?? null ;
186+ } , [ rawHierarchyID ] ) ;
175187
176- const node = getTargetNode ( e ) ;
188+ const onClick = useCallback ( ( e : React . MouseEvent < SVGSVGElement > ) => {
189+ const node = getTargetNode ( e . target ) ;
177190 if ( node === null ) {
178- setTooltipNode ( undefined ) ;
179191 return ;
180192 }
181- setTooltipNode ( node ) ;
182- } , [ getTargetNode ] ) ;
183193
184- const onClick = useCallback ( ( e : React . MouseEvent < SVGSVGElement > ) => {
185- const node = getTargetNode ( e ) ;
186- if ( node === null ) {
194+ if ( e . ctrlKey ) {
195+ console . log ( node ) ;
187196 return ;
188197 }
189198
190- if ( selectedNode ?. data . getID ( ) === node . data . getID ( ) ) {
191- setSelectedNode ( null ) ;
199+ if ( selectedNodeID === node . getID ( ) ) {
200+ setSelectedNodeID ( null ) ;
192201 }
193202 else {
194- setSelectedNode ( node ) ;
203+ setSelectedNodeID ( node . getID ( ) ) ;
195204 }
196- } , [ getTargetNode , selectedNode ] ) ;
205+ } , [ getTargetNode , selectedNodeID ] ) ;
206+
207+ const tooltipVisible = useMemo ( ( ) => {
208+ return isOver && tooltipNode ;
209+ } , [ isOver , tooltipNode ] ) ;
210+
211+ useEffect ( ( ) => {
212+ if ( ! mouseEventTarget ) {
213+ return ;
214+ }
215+ const node = getTargetNode ( mouseEventTarget ) ;
216+ if ( node ) {
217+ setTooltipNode ( node ) ;
218+ }
219+ } , [ getTargetNode , mouseEventTarget ] ) ;
197220
198221 const nodes = useMemo ( ( ) => {
199- const selectedID = selectedNode ?. data . getID ( ) ;
200-
201- return (
202- nestedData . map ( ( { key, values } ) => {
203- return (
204- < g className = "layer" key = { key } >
205- { values . map ( ( node ) => {
206- const { backgroundColor, fontColor } = getModuleColor ( node ) ;
207-
208- if ( node . x0 === node . x1 || node . y0 === node . y1 ) {
209- return null ;
210- }
211-
212- return (
213- < Node
214- key = { node . data . getID ( ) }
215- id = { node . data . getID ( ) }
216- title = { node . data . getName ( ) }
217- selected = { selectedID === node . data . getID ( ) }
218- x0 = { node . x0 }
219- y0 = { node . y0 }
220- x1 = { node . x1 }
221- y1 = { node . y1 }
222-
223- backgroundColor = { backgroundColor }
224- fontColor = { fontColor }
225- hasChildren = { node . children !== undefined }
226- />
227- ) ;
228- } ) . filter ( Boolean ) }
229- </ g >
230- ) ;
231- } )
232- ) ;
233- } , [ getModuleColor , nestedData , selectedNode ] ) ;
222+ return layers . map ( ( { key, values } ) => {
223+ return (
224+ < g className = "layer" key = { key } >
225+ { values . map ( ( node ) => {
226+ const { backgroundColor, fontColor } = getModuleColor ( node . data . getID ( ) ) ;
227+
228+ if ( node . x1 - node . x0 < 6 || node . y1 - node . y0 < 6 ) {
229+ return null ;
230+ }
234231
235- const tooltipVisible = useMemo ( ( ) => {
236- return showTooltip && tooltipNode && tooltipPosition . every ( v => v > 0 ) ;
237- } , [ showTooltip , tooltipNode , tooltipPosition ] ) ;
232+ return (
233+ < Node
234+ key = { node . data . getID ( ) }
235+ id = { node . data . getID ( ) }
236+ title = { node . data . getName ( ) }
237+ selected = { selectedNodeID === node . data . getID ( ) }
238+ x0 = { node . x0 }
239+ y0 = { node . y0 }
240+ x1 = { node . x1 }
241+ y1 = { node . y1 }
242+
243+ backgroundColor = { backgroundColor }
244+ fontColor = { fontColor }
245+ hasChildren = { node . children !== undefined }
246+ />
247+ ) ;
248+ } ) . filter ( Boolean ) }
249+ </ g >
250+ ) ;
251+ } ) ;
252+ } , [ getModuleColor , layers , selectedNodeID ] ) ;
238253
239254 return (
240255 < >
241- { ( tooltipVisible ) && < Tooltip node = { tooltipNode ! . data } x = { tooltipPosition [ 0 ] } y = { tooltipPosition [ 1 ] } /> }
256+ { ( tooltipVisible ) && < Tooltip node = { tooltipNode ! } x = { clientX ! } y = { clientY ! } /> }
242257 < svg
243258 xmlns = "http://www.w3.org/2000/svg"
244259 viewBox = { `0 0 ${ width } ${ height } ` }
245- onMouseEnter = { onMouseEnter }
246- onMouseLeave = { onMouseLeave }
247- onMouseMove = { onMouseMove }
248260 onClick = { onClick }
261+ ref = { svgRef }
249262 >
250263 { nodes }
251264 </ svg >
0 commit comments