44 * See LICENSE for details.
55 */
66
7- import { useState , useRef , useEffect } from 'react' ;
7+ import { useState , useRef , useEffect , useCallback } from 'react' ;
88import { motion , useReducedMotion } from 'framer-motion' ;
99import { useTranslation } from 'react-i18next' ;
1010import { COMPLEXITY_FUNCTIONS } from '../constants' ;
@@ -17,6 +17,11 @@ import {
1717 COMPLEXITY_DATASETS ,
1818 DEFAULT_COMPLEXITY_DATASET ,
1919} from '../registry/complexityDatasetRegistry' ;
20+ import { useMediaQuery } from '../hooks/useMediaQuery' ;
21+
22+ // Fixed internal viewBox — CSS scales the SVG to its container
23+ const VIEWBOX_WIDTH = 650 ;
24+ const VIEWBOX_HEIGHT = 350 ;
2025
2126/**
2227 * @param {string } algorithm - Current algorithm key
@@ -32,11 +37,24 @@ function ComplexityPanel({
3237 const [ hoveredPoint , setHoveredPoint ] = useState ( null ) ;
3338 const [ animationProgress , setAnimationProgress ] = useState ( 0 ) ;
3439 const svgRef = useRef ( null ) ;
40+
41+ const isSmall = useMediaQuery ( '(max-width: 639px)' ) ;
42+ const isBelowLg = useMediaQuery ( '(max-width: 1023px)' ) ;
43+
3544 const dataset =
3645 COMPLEXITY_DATASETS [ complexityDataset ] ??
3746 COMPLEXITY_DATASETS [ DEFAULT_COMPLEXITY_DATASET ] ;
3847 const complexityData = dataset [ algorithm ] ;
3948
49+ // Touch-friendly tooltip: tap toggles, tap elsewhere dismisses
50+ const handlePointInteraction = useCallback ( point => {
51+ setHoveredPoint ( prev => ( prev && prev . n === point . n ? null : point ) ) ;
52+ } , [ ] ) ;
53+
54+ const dismissTooltip = useCallback ( ( ) => {
55+ setHoveredPoint ( null ) ;
56+ } , [ ] ) ;
57+
4058 // Animate curve drawing on mount
4159 useEffect ( ( ) => {
4260 setAnimationProgress ( 0 ) ;
@@ -74,13 +92,14 @@ function ComplexityPanel({
7492
7593 const graphData = generateGraphData ( ) ;
7694
77- // SVG dimensions - responsive for mobile
78- const isMobile = typeof window !== 'undefined' && window . innerWidth < 640 ;
79- const width = isMobile ? 320 : 650 ;
80- const height = isMobile ? 280 : 350 ;
81- const margin = { top : 30 , right : 50 , bottom : 60 , left : 60 } ;
82- const chartWidth = width - margin . left - margin . right ;
83- const chartHeight = height - margin . top - margin . bottom ;
95+ // Responsive margins for the SVG viewBox coordinate system
96+ const margin = isSmall
97+ ? { top : 20 , right : 25 , bottom : 50 , left : 50 }
98+ : isBelowLg
99+ ? { top : 25 , right : 35 , bottom : 55 , left : 55 }
100+ : { top : 30 , right : 50 , bottom : 60 , left : 60 } ;
101+ const chartWidth = VIEWBOX_WIDTH - margin . left - margin . right ;
102+ const chartHeight = VIEWBOX_HEIGHT - margin . top - margin . bottom ;
84103
85104 // Scales
86105 const maxN = Math . max ( ...graphData . map ( d => d . n ) ) ;
@@ -109,6 +128,12 @@ function ComplexityPanel({
109128 . join ( ' ' ) ;
110129 } ;
111130
131+ const svgTextClass = isSmall
132+ ? 'text-[8px]'
133+ : isBelowLg
134+ ? 'text-[9px]'
135+ : 'text-xs' ;
136+
112137 return (
113138 < motion . div
114139 initial = { modalPanelInitial ( reduceMotion ) }
@@ -118,68 +143,79 @@ function ComplexityPanel({
118143 dir = "auto"
119144 >
120145 < div className = "rounded-xl p-3 sm:p-6 max-w-5xl w-full" >
121- < div className = "flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-4 pb-3 border-b border-gray-200" >
122- < div className = "hidden sm:block" >
123- < h2 className = "text-lg sm:text-xl font-bold text-text-primary" >
146+ < div className = "flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-3 mb-3 sm: mb-4 pb-3 border-b border-gray-200" >
147+ < div >
148+ < h2 className = "text-sm sm:text-xl font-bold text-text-primary" >
124149 { t ( 'complexity_panel.title' ) }
125150 </ h2 >
126- < p className = "text-xs sm:text-sm text-text-secondary" >
151+ < p className = "text-[10px] sm:text-sm text-text-secondary" >
127152 { t ( `algorithms.${ complexityDataset } .${ algorithm } ` , {
128153 defaultValue : complexityData . name || algorithm ,
129154 } ) }
130155 </ p >
131156 </ div >
132- < div className = "flex items-center gap-2 sm: gap-3" >
133- < span className = "text-xs sm:text-sm text-text-secondary" >
157+ < div className = "hidden sm: flex items-center gap-3" >
158+ < span className = "hidden sm:inline text-sm text-text-secondary" >
134159 { t ( 'complexity_panel.linearScale' ) }
135160 </ span >
136161 < button
137162 onClick = { ( ) => setIsLogScale ( ! isLogScale ) }
138- className = { `relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 touch-manipulation ${
163+ className = { `relative inline-flex h-7 w-12 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 touch-manipulation ${
139164 isLogScale ? 'bg-blue-600' : 'bg-gray-300'
140165 } `}
141166 role = "switch"
142167 aria-checked = { isLogScale }
143168 aria-label = { t ( 'complexity_panel.toggleScale' ) }
144169 >
145170 < span
146- className = { `inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
171+ className = { `inline-block h-5 w-5 transform rounded-full bg-white shadow-sm transition-transform ${
147172 isLogScale
148173 ? 'translate-x-6 rtl:-translate-x-6'
149174 : 'translate-x-1 rtl:-translate-x-1'
150175 } `}
151176 />
152177 </ button >
153- < span className = "text-xs sm:text-sm text-text-secondary" >
178+ < span className = "hidden sm:inline text-sm text-text-secondary" >
154179 { t ( 'complexity_panel.logScale' ) }
155180 </ span >
156181 </ div >
157182 </ div >
158- < div className = "flex flex-col lg:flex-row gap-4 sm:gap-6" >
159- < div className = "space-y-3 sm:space-y-4 flex-shrink-0 w-full lg:w-auto" >
160- < div >
161- < h3 className = "text-[10px] sm:text-xs font-semibold text-text-secondary mb-2 uppercase tracking-wide" >
183+ < div className = "flex flex-col lg:flex-row gap-3 sm:gap-6" >
184+ { /* Complexity badges */ }
185+ < div
186+ className = { `flex-shrink-0 w-full lg:w-auto ${
187+ isSmall
188+ ? 'flex flex-row flex-wrap gap-x-4 gap-y-2'
189+ : 'space-y-3 sm:space-y-4'
190+ } `}
191+ >
192+ < div className = { isSmall ? 'min-w-0' : '' } >
193+ < h3 className = "text-[10px] sm:text-xs font-semibold text-text-secondary mb-1.5 sm:mb-2 uppercase tracking-wide" >
162194 { t ( 'complexity_panel.timeComplexity' ) }
163195 </ h3 >
164- < div className = "space-y-1.5" >
165- < div className = "flex items-center gap-2" >
166- < span className = "text-[10px] sm:text-xs text-text-green-500 w-12 sm:w-16 rtl:text-right" >
196+ < div
197+ className = {
198+ isSmall ? 'flex flex-wrap gap-x-3 gap-y-1' : 'space-y-1.5'
199+ }
200+ >
201+ < div className = "flex items-center gap-1.5 sm:gap-2" >
202+ < span className = "text-[10px] sm:text-xs text-text-green-500 w-10 sm:w-16 rtl:text-right" >
167203 { t ( 'complexity_panel.best' ) } :
168204 </ span >
169205 < code className = "bg-green-100 text-green-800 px-1.5 sm:px-2 py-0.5 sm:py-1 rounded font-mono text-[10px] sm:text-xs font-semibold" >
170206 { complexityData . timeComplexity . best }
171207 </ code >
172208 </ div >
173- < div className = "flex items-center gap-2" >
174- < span className = "text-[10px] sm:text-xs text-text-tertiary w-12 sm:w-16 rtl:text-right" >
209+ < div className = "flex items-center gap-1.5 sm:gap- 2" >
210+ < span className = "text-[10px] sm:text-xs text-text-tertiary w-10 sm:w-16 rtl:text-right" >
175211 { t ( 'complexity_panel.average' ) } :
176212 </ span >
177213 < code className = "bg-blue-100 text-blue-800 px-1.5 sm:px-2 py-0.5 sm:py-1 rounded font-mono text-[10px] sm:text-xs font-semibold" >
178214 { complexityData . timeComplexity . average }
179215 </ code >
180216 </ div >
181- < div className = "flex items-center gap-2" >
182- < span className = "text-[10px] sm:text-xs text-text-tertiary w-12 sm:w-16 rtl:text-right" >
217+ < div className = "flex items-center gap-1.5 sm:gap- 2" >
218+ < span className = "text-[10px] sm:text-xs text-text-tertiary w-10 sm:w-16 rtl:text-right" >
183219 { t ( 'complexity_panel.worst' ) } :
184220 </ span >
185221 < code className = "bg-red-100 text-red-800 px-1.5 sm:px-2 py-0.5 sm:py-1 rounded font-mono text-[10px] sm:text-xs font-semibold" >
@@ -188,31 +224,33 @@ function ComplexityPanel({
188224 </ div >
189225 </ div >
190226 </ div >
191- < div >
192- < h3 className = "text-[10px] sm:text-xs font-semibold text-text-secondary mb-2 uppercase tracking-wide" >
227+ < div className = { isSmall ? 'min-w-0' : '' } >
228+ < h3 className = "text-[10px] sm:text-xs font-semibold text-text-secondary mb-1.5 sm:mb- 2 uppercase tracking-wide" >
193229 { t ( 'complexity_panel.spaceComplexity' ) }
194230 </ h3 >
195231 < code className = "bg-purple-100 text-purple-800 px-1.5 sm:px-2 py-0.5 sm:py-1 rounded font-mono text-[10px] sm:text-xs font-semibold inline-block" >
196232 { complexityData . spaceComplexity }
197233 </ code >
198234 </ div >
199235 </ div >
200- < div className = "flex-1 w-full overflow-x-auto" >
201- < h3 className = "text-xs sm:text-sm font-semibold text-text-primary mb-2" >
236+
237+ { /* Chart */ }
238+ < div className = "flex-1 w-full min-w-0" >
239+ < h3 className = "text-xs sm:text-sm font-semibold text-text-primary mb-1 sm:mb-2" >
202240 { t ( 'complexity_panel.performance' ) }
203241 </ h3 >
204- < div className = "text-[10px] sm:text-xs text-text-secondary mb-3" >
242+ < div className = "text-[10px] sm:text-xs text-text-secondary mb-2 sm:mb- 3" >
205243 { t ( 'complexity_panel.axisLabels' , {
206244 complexity : complexityData . timeComplexity . average ,
207245 } ) }
208246 </ div >
209247
210- < div className = "relative w-full overflow-x-auto" >
248+ < div className = "relative w-full" onClick = { dismissTooltip } >
211249 < svg
212250 ref = { svgRef }
213- width = { width }
214- height = { height }
215- className = "border border-gray-200 rounded bg-bg w-full "
251+ viewBox = { `0 0 ${ VIEWBOX_WIDTH } ${ VIEWBOX_HEIGHT } ` }
252+ className = "border border-gray-200 rounded bg-bg w-full h-auto"
253+ preserveAspectRatio = "xMidYMid meet "
216254 >
217255 < g transform = { `translate(${ margin . left } , ${ margin . top } )` } >
218256 < defs >
@@ -276,23 +314,27 @@ function ComplexityPanel({
276314 className = "cursor-pointer transition-all duration-200"
277315 onMouseEnter = { ( ) => setHoveredPoint ( point ) }
278316 onMouseLeave = { ( ) => setHoveredPoint ( null ) }
317+ onClick = { e => {
318+ e . stopPropagation ( ) ;
319+ handlePointInteraction ( point ) ;
320+ } }
279321 />
280322 ) ) }
281323 < text
282324 x = { chartWidth / 2 }
283- y = { chartHeight + 35 }
325+ y = { chartHeight + ( isSmall ? 30 : 35 ) }
284326 textAnchor = "middle"
285- className = "text-xs font-medium"
327+ className = { ` ${ svgTextClass } font-medium` }
286328 fill = "var(--color-text-secondary)"
287329 >
288330 { t ( 'complexity_panel.inputSize' ) }
289331 </ text >
290332 < text
291333 x = { - chartHeight / 2 }
292- y = { - 45 }
334+ y = { isSmall ? - 38 : - 45 }
293335 textAnchor = "middle"
294- transform = { `rotate(-90, -45 , ${ chartHeight / 2 } )` }
295- className = "text-xs font-medium"
336+ transform = { `rotate(-90, ${ isSmall ? - 38 : - 45 } , ${ chartHeight / 2 } )` }
337+ className = { ` ${ svgTextClass } font-medium` }
296338 fill = "var(--color-text-secondary)"
297339 >
298340 { t ( 'complexity_panel.operations' , {
@@ -305,7 +347,7 @@ function ComplexityPanel({
305347 x = "0"
306348 y = { chartHeight + 15 }
307349 textAnchor = "start"
308- className = "text-xs"
350+ className = { svgTextClass }
309351 fill = "var(--color-text-tertiary)"
310352 >
311353 0
@@ -314,7 +356,7 @@ function ComplexityPanel({
314356 x = { chartWidth }
315357 y = { chartHeight + 15 }
316358 textAnchor = "end"
317- className = "text-xs"
359+ className = { svgTextClass }
318360 fill = "var(--color-text-tertiary)"
319361 >
320362 { maxN . toLocaleString ( ) }
@@ -323,7 +365,7 @@ function ComplexityPanel({
323365 x = "-10"
324366 y = { chartHeight + 4 }
325367 textAnchor = "end"
326- className = "text-xs"
368+ className = { svgTextClass }
327369 fill = "var(--color-text-tertiary)"
328370 >
329371 0
@@ -332,7 +374,7 @@ function ComplexityPanel({
332374 x = "-10"
333375 y = "4"
334376 textAnchor = "end"
335- className = "text-xs"
377+ className = { svgTextClass }
336378 fill = "var(--color-text-tertiary)"
337379 >
338380 { isLogScale
@@ -342,7 +384,11 @@ function ComplexityPanel({
342384 </ g >
343385 </ svg >
344386 { hoveredPoint && (
345- < div className = "absolute top-2 ltr:left-2 rtl:right-2 bg-surface-elevated border border-gray-200 text-text-primary text-xs px-3 py-2 rounded shadow-lg z-10" >
387+ < div
388+ className = "absolute top-2 ltr:left-2 rtl:right-2 bg-surface-elevated border border-gray-200 text-text-primary text-[10px] sm:text-xs px-2 sm:px-3 py-1.5 sm:py-2 rounded shadow-lg z-10"
389+ role = "status"
390+ aria-live = "polite"
391+ >
346392 < div >
347393 { t ( 'complexity_panel.hoverN' ) } = { hoveredPoint . n }
348394 </ div >
0 commit comments