77
88import { useState , useMemo , useCallback , useRef , useEffect } from 'react'
99import type { FileGraphData , FileNode } from '../../../types/electron'
10+ import { squarify } from './file-graph/layout/treemap-layout'
1011
1112export interface FileGraphProps {
1213 data : FileGraphData | null
1314 loading ?: boolean
1415}
1516
16- // Language colors
17- const LANGUAGE_COLORS : Record < string , string > = {
18- TypeScript : '#3178C6' ,
19- JavaScript : '#F7DF1E' ,
20- CSS : '#563D7C' ,
21- SCSS : '#CC6699' ,
22- HTML : '#E34C26' ,
23- JSON : '#F5D800' ,
24- Markdown : '#083FA1' ,
25- Python : '#3776AB' ,
26- Go : '#00ADD8' ,
27- Rust : '#DEA584' ,
28- Java : '#B07219' ,
29- Ruby : '#CC342D' ,
30- PHP : '#4F5D95' ,
31- C : '#555555' ,
32- 'C++' : '#F34B7D' ,
33- 'C#' : '#178600' ,
34- Swift : '#F05138' ,
35- Kotlin : '#A97BFF' ,
36- Shell : '#89E051' ,
37- YAML : '#CB171E' ,
38- TOML : '#9C4221' ,
39- XML : '#0060AC' ,
40- SQL : '#E38C00' ,
41- GraphQL : '#E535AB' ,
42- Vue : '#41B883' ,
43- Svelte : '#FF3E00' ,
44- Other : '#6B7280' ,
45- }
46-
47- interface TreemapRect {
48- node : FileNode
49- x : number
50- y : number
51- width : number
52- height : number
53- }
54-
55- /** Squarified treemap layout */
56- function squarify (
57- nodes : FileNode [ ] ,
58- x : number ,
59- y : number ,
60- width : number ,
61- height : number ,
62- totalValue : number
63- ) : TreemapRect [ ] {
64- if ( nodes . length === 0 || totalValue === 0 ) return [ ]
65-
66- const rects : TreemapRect [ ] = [ ]
67- const sorted = [ ...nodes ] . sort ( ( a , b ) => b . lines - a . lines )
68-
69- let currentX = x
70- let currentY = y
71- let remainingWidth = width
72- let remainingHeight = height
73- let remainingValue = totalValue
74-
75- let row : FileNode [ ] = [ ]
76- let rowValue = 0
77-
78- function aspectRatio ( areas : number [ ] , length : number ) : number {
79- if ( areas . length === 0 || length === 0 ) return Infinity
80- const sum = areas . reduce ( ( a , b ) => a + b , 0 )
81- const min = Math . min ( ...areas )
82- const max = Math . max ( ...areas )
83- const s2 = sum * sum
84- const l2 = length * length
85- return Math . max ( ( l2 * max ) / s2 , s2 / ( l2 * min ) )
86- }
87-
88- function layoutRow ( row : FileNode [ ] , rowValue : number , isHorizontal : boolean ) {
89- if ( row . length === 0 ) return
90- const rowLength = isHorizontal
91- ? ( rowValue / remainingValue ) * remainingHeight
92- : ( rowValue / remainingValue ) * remainingWidth
93- let offset = 0
94- for ( const node of row ) {
95- const nodeRatio = node . lines / rowValue
96- const nodeLength = isHorizontal ? nodeRatio * remainingWidth : nodeRatio * remainingHeight
97- rects . push ( {
98- node,
99- x : isHorizontal ? currentX + offset : currentX ,
100- y : isHorizontal ? currentY : currentY + offset ,
101- width : isHorizontal ? nodeLength : rowLength ,
102- height : isHorizontal ? rowLength : nodeLength ,
103- } )
104- offset += nodeLength
105- }
106- if ( isHorizontal ) {
107- currentY += rowLength
108- remainingHeight -= rowLength
109- } else {
110- currentX += rowLength
111- remainingWidth -= rowLength
112- }
113- remainingValue -= rowValue
114- }
115-
116- for ( const node of sorted ) {
117- const isHorizontal = remainingWidth >= remainingHeight
118- const length = isHorizontal ? remainingWidth : remainingHeight
119- const rowAreas = row . map ( ( n ) => ( n . lines / remainingValue ) * length * ( isHorizontal ? remainingHeight : remainingWidth ) )
120- const newAreas = [ ...rowAreas , ( node . lines / remainingValue ) * length * ( isHorizontal ? remainingHeight : remainingWidth ) ]
121- const currentAspect = aspectRatio ( rowAreas , length )
122- const newAspect = aspectRatio ( newAreas , length )
123-
124- if ( row . length === 0 || newAspect <= currentAspect ) {
125- row . push ( node )
126- rowValue += node . lines
127- } else {
128- layoutRow ( row , rowValue , isHorizontal )
129- row = [ node ]
130- rowValue = node . lines
131- }
132- }
133-
134- if ( row . length > 0 ) {
135- layoutRow ( row , rowValue , remainingWidth >= remainingHeight )
136- }
137-
138- return rects
139- }
17+ const DEFAULT_LANGUAGE_COLOR = 'var(--chart-1)'
18+ const FALLBACK_LANGUAGE_COLOR = 'var(--chart-8)'
14019
14120function formatLines ( lines : number ) : string {
14221 if ( lines >= 1000000 ) return `${ ( lines / 1000000 ) . toFixed ( 1 ) } M`
14322 if ( lines >= 1000 ) return `${ ( lines / 1000 ) . toFixed ( 1 ) } K`
14423 return lines . toString ( )
14524}
14625
147- function getContrastColor ( hex : string ) : string {
148- const r = parseInt ( hex . slice ( 1 , 3 ) , 16 )
149- const g = parseInt ( hex . slice ( 3 , 5 ) , 16 )
150- const b = parseInt ( hex . slice ( 5 , 7 ) , 16 )
151- const luminance = ( 0.299 * r + 0.587 * g + 0.114 * b ) / 255
152- return luminance > 0.5 ? '#000000' : '#FFFFFF'
153- }
154-
15526function truncateLabel ( label : string , width : number ) : string {
15627 const maxChars = Math . floor ( width / 7 )
15728 if ( label . length <= maxChars ) return label
@@ -196,6 +67,26 @@ export function FileGraph({ data, loading }: FileGraphProps) {
19667 return node
19768 } , [ data , currentPath ] )
19869
70+ const languageColorMap = useMemo ( ( ) => {
71+ const map = new Map < string , string > ( )
72+ if ( ! data ) return map
73+ for ( const language of data . languages ) {
74+ map . set ( language . language , language . color || DEFAULT_LANGUAGE_COLOR )
75+ }
76+ if ( ! map . has ( 'Other' ) ) {
77+ map . set ( 'Other' , FALLBACK_LANGUAGE_COLOR )
78+ }
79+ return map
80+ } , [ data ] )
81+
82+ const legendLanguages = useMemo ( ( ) => {
83+ if ( ! data ) return [ ]
84+ return data . languages . slice ( 0 , 8 ) . map ( ( lang ) => ( {
85+ ...lang ,
86+ color : languageColorMap . get ( lang . language ) || lang . color || DEFAULT_LANGUAGE_COLOR ,
87+ } ) )
88+ } , [ data , languageColorMap ] )
89+
19990 // Calculate treemap layout
20091 const treemapRects = useMemo ( ( ) => {
20192 if ( ! currentNode ?. children ) return [ ]
@@ -221,9 +112,10 @@ export function FileGraph({ data, loading }: FileGraphProps) {
221112 } , [ ] )
222113
223114 const getNodeColor = useCallback ( ( node : FileNode ) : string => {
224- if ( node . isDirectory ) return 'var(--bg-tertiary)'
225- return LANGUAGE_COLORS [ node . language || 'Other' ] || LANGUAGE_COLORS . Other
226- } , [ ] )
115+ if ( node . isDirectory ) return 'var(--bg-hover)'
116+ const language = node . language || 'Other'
117+ return languageColorMap . get ( language ) || DEFAULT_LANGUAGE_COLOR
118+ } , [ languageColorMap ] )
227119
228120 if ( loading ) {
229121 return (
@@ -297,7 +189,6 @@ export function FileGraph({ data, loading }: FileGraphProps) {
297189 className = "file-graph-label"
298190 style = { {
299191 fontSize : Math . min ( 12 , minDim / 4 ) ,
300- fill : rect . node . isDirectory ? 'var(--text-primary)' : getContrastColor ( getNodeColor ( rect . node ) ) ,
301192 } }
302193 >
303194 { truncateLabel ( rect . node . name , rect . width ) }
@@ -311,7 +202,7 @@ export function FileGraph({ data, loading }: FileGraphProps) {
311202
312203 { /* Legend */ }
313204 < div className = "file-graph-legend" >
314- { data . languages . slice ( 0 , 8 ) . map ( ( lang ) => (
205+ { legendLanguages . map ( ( lang ) => (
315206 < div key = { lang . language } className = "legend-item" >
316207 < span className = "legend-color" style = { { backgroundColor : lang . color } } />
317208 < span className = "legend-label" > { lang . language } </ span >
0 commit comments