1+ import React , { useState , useMemo , useEffect , useRef } from "react" ;
2+ import { getCountingSortSteps } from "../../algorithms/sorting/countingSort.js" ;
3+
4+ const ArrayDisplay = ( { title, array, highlightIndex = - 1 , readIndex = - 1 , indicesLabel = "Index" } ) => {
5+ return (
6+ < div className = "mt-4" >
7+ < h3 className = "text-xl font-semibold mb-3 text-blue-400" > { title } </ h3 >
8+ < div className = "flex flex-wrap gap-2 justify-center" >
9+ { array . map ( ( value , index ) => {
10+ const isCurrent = index === highlightIndex ;
11+ const isReading = index === readIndex ;
12+
13+ let cellClass = "bg-gray-700 border-gray-600 text-gray-200 hover:border-gray-500" ;
14+ if ( isCurrent ) {
15+ cellClass = "bg-blue-600 border-blue-400 shadow-lg text-white font-bold transform scale-105" ; // Write/Current
16+ } else if ( isReading ) {
17+ cellClass = "bg-yellow-600 border-yellow-400 text-white" ; // Read
18+ }
19+
20+ return (
21+ < div
22+ key = { index }
23+ className = { `p-3 rounded-lg text-center transition-all duration-300 ease-in-out border-2 w-20 flex flex-col justify-center items-center ${ cellClass } ` }
24+ title = { `${ indicesLabel } ${ index } ` }
25+ >
26+ < div className = "text-sm font-light text-gray-400" > { indicesLabel } { index } </ div >
27+ < div className = "text-xl mt-1" > { value === null ? '?' : value } </ div >
28+ </ div >
29+ ) ;
30+ } ) }
31+ </ div >
32+ </ div >
33+ ) ;
34+ } ;
35+
36+
37+ const SPEED_OPTIONS = {
38+ "Slow" : 1500 ,
39+ "Medium" : 500 ,
40+ "Fast" : 200 ,
41+ } ;
42+
43+ export default function CountingSort ( ) {
44+ const [ input , setInput ] = useState ( "4, 1, 3, 4, 0, 2, 1, 7" ) ;
45+
46+ const [ steps , setSteps ] = useState ( [ ] ) ;
47+ const [ currentStep , setCurrentStep ] = useState ( 0 ) ;
48+ const [ isPlaying , setIsPlaying ] = useState ( false ) ;
49+ const [ speed , setSpeed ] = useState ( SPEED_OPTIONS [ "Medium" ] ) ;
50+ const timerRef = useRef ( null ) ;
51+
52+ const handleCompute = ( ) => {
53+ const parsedArray = input
54+ . split ( ',' )
55+ . map ( s => Number ( s . trim ( ) ) )
56+ . filter ( n => ! isNaN ( n ) && n >= 0 ) ;
57+
58+ if ( parsedArray . length === 0 || parsedArray . length > 20 ) {
59+ alert ( "Please enter 1 to 20 non-negative numbers, separated by commas." ) ;
60+ return ;
61+ }
62+
63+ const maxVal = Math . max ( ...parsedArray ) ;
64+ if ( maxVal > 50 ) {
65+ alert ( "The maximum value in the array must be 50 or less for optimal visualization." ) ;
66+ return ;
67+ }
68+
69+ setIsPlaying ( false ) ;
70+ const { steps : newSteps } = getCountingSortSteps ( parsedArray ) ;
71+ setSteps ( newSteps ) ;
72+ setCurrentStep ( 0 ) ;
73+ } ;
74+
75+ useEffect ( ( ) => {
76+ handleCompute ( ) ;
77+ } , [ ] ) ;
78+
79+ useEffect ( ( ) => {
80+ if ( isPlaying && currentStep < steps . length - 1 ) {
81+ timerRef . current = setInterval ( ( ) => {
82+ setCurrentStep ( ( prevStep ) => prevStep + 1 ) ;
83+ } , speed ) ;
84+ } else if ( currentStep === steps . length - 1 ) {
85+ setIsPlaying ( false ) ;
86+ }
87+
88+ return ( ) => {
89+ clearInterval ( timerRef . current ) ;
90+ } ;
91+ } , [ isPlaying , currentStep , steps . length , speed ] ) ;
92+
93+ const togglePlay = ( ) => {
94+ if ( currentStep === steps . length - 1 ) {
95+ setCurrentStep ( 0 ) ;
96+ setIsPlaying ( true ) ;
97+ } else {
98+ setIsPlaying ( ! isPlaying ) ;
99+ }
100+ } ;
101+
102+ const handleNext = ( ) => {
103+ setIsPlaying ( false ) ;
104+ if ( currentStep < steps . length - 1 ) setCurrentStep ( currentStep + 1 ) ;
105+ } ;
106+
107+ const handlePrev = ( ) => {
108+ setIsPlaying ( false ) ;
109+ if ( currentStep > 0 ) setCurrentStep ( currentStep - 1 ) ;
110+ } ;
111+
112+ const currentState = useMemo ( ( ) => steps [ currentStep ] || { } , [ steps , currentStep ] ) ;
113+ const { phase, input : inputArray , count, output, highlight, message } = currentState ;
114+
115+ const isFinalStep = phase === 4 ;
116+
117+ return (
118+ < div className = "p-6 min-h-screen bg-gray-900 text-gray-100 font-sans" >
119+ < div className = "max-w-6xl mx-auto" >
120+ < h1 className = "text-4xl font-extrabold mb-8 text-indigo-400 text-center drop-shadow-lg" >
121+ Counting Sort
122+ </ h1 >
123+
124+ < details className = "mb-8 p-4 rounded-xl bg-gray-800 border border-gray-700 shadow-inner group" >
125+ < summary className = "cursor-pointer text-xl font-bold text-teal-400 hover:text-teal-300 transition-colors" >
126+ ❓ What is Counting Sort?
127+ </ summary >
128+ < div className = "mt-3 p-3 border-t border-gray-700 text-gray-300" >
129+ < p className = "mb-2" >
130+ Counting Sort is a non-comparison algorithm that sorts integers. It works in three phases:
131+ </ p >
132+ < ul className = "list-disc list-inside ml-4 space-y-2" >
133+ < li >
134+ < b > Phase 1 (Frequency):</ b > Count the occurrences of each unique element and store it in a `Count` array.
135+ </ li >
136+ < li >
137+ < b > Phase 2 (Cumulative Sum):</ b > Modify the `Count` array so each element stores the sum of all previous counts. This gives the "ending position" of each element.
138+ </ li >
139+ < li >
140+ < b > Phase 3 (Output):</ b > Iterate the `Input` array in reverse (for stability), place each element in its correct position in the `Output` array, and decrement its count.
141+ </ li >
142+ </ ul >
143+ </ div >
144+ </ details >
145+
146+ < div className = "flex flex-col xl:flex-row justify-center items-center gap-5 mb-8 p-6 rounded-xl bg-gray-800 shadow-2xl border border-gray-700" >
147+ < div className = "flex-1 w-full xl:w-auto" >
148+ < label htmlFor = "input-array" className = "text-gray-300 text-lg mb-2 block" > Input Array (non-negative, max val 50):</ label >
149+ < input
150+ id = "input-array"
151+ type = "text"
152+ className = "border border-gray-600 p-2 rounded-lg w-full bg-gray-700 text-white text-lg focus:ring-indigo-500 focus:border-indigo-500 transition-colors"
153+ value = { input }
154+ onChange = { ( e ) => setInput ( e . target . value ) }
155+ placeholder = "e.g., 4, 1, 3, 4, 0"
156+ />
157+ </ div >
158+ < button
159+ className = "bg-indigo-600 hover:bg-indigo-700 text-white font-bold px-8 py-3 rounded-xl transition duration-300 ease-in-out shadow-md transform hover:-translate-y-0.5"
160+ onClick = { handleCompute }
161+ >
162+ Re-Visualize
163+ </ button >
164+ </ div >
165+
166+ { steps . length > 0 ? (
167+ < >
168+ < div className = "flex flex-wrap justify-between items-center mb-6 p-4 rounded-xl bg-gray-800 border border-gray-700 shadow-lg" >
169+ < button
170+ className = { `px-5 py-2 rounded-lg font-semibold text-lg transition duration-200 ease-in-out transform hover:scale-105 ${ isPlaying ? "bg-red-600 hover:bg-red-700" : "bg-green-600 hover:bg-green-700"
171+ } text-white shadow-md`}
172+ onClick = { togglePlay }
173+ disabled = { isFinalStep && isPlaying }
174+ >
175+ { isFinalStep && ! isPlaying ? "Replay ▶️" : isPlaying ? "Pause ⏸️" : "Play ▶️" }
176+ </ button >
177+ < div className = "flex gap-2" >
178+ < button
179+ className = { `px-3 py-2 rounded-lg font-semibold transition duration-150 ${ currentStep > 0 ? "bg-purple-600 hover:bg-purple-700 text-white" : "bg-gray-600 text-gray-400 cursor-not-allowed" } ` }
180+ onClick = { handlePrev }
181+ disabled = { currentStep === 0 }
182+ >
183+ < Prev
184+ </ button >
185+ < button
186+ className = { `px-3 py-2 rounded-lg font-semibold transition duration-150 ${ ! isFinalStep ? "bg-purple-600 hover:bg-purple-700 text-white" : "bg-gray-600 text-gray-400 cursor-not-allowed" } ` }
187+ onClick = { handleNext }
188+ disabled = { isFinalStep }
189+ >
190+ Next >
191+ </ button >
192+ </ div >
193+ < div className = "flex items-center gap-2" >
194+ < label htmlFor = "speed-select" className = "text-gray-300" > Speed:</ label >
195+ < select
196+ id = "speed-select"
197+ className = "border border-gray-600 p-2 rounded-lg bg-gray-700 text-white focus:ring-indigo-500 focus:border-indigo-500"
198+ value = { speed }
199+ onChange = { ( e ) => setSpeed ( Number ( e . target . value ) ) }
200+ >
201+ { Object . entries ( SPEED_OPTIONS ) . map ( ( [ label , ms ] ) => (
202+ < option key = { label } value = { ms } > { label } </ option >
203+ ) ) }
204+ </ select >
205+ </ div >
206+ </ div >
207+
208+ < div className = "text-center mb-4" >
209+ < p className = "text-2xl font-bold text-yellow-400" >
210+ Step < b > { currentStep + 1 } </ b > / < b > { steps . length } </ b >
211+ { phase && < span className = "ml-4 text-xl text-teal-400" > (Phase { phase } / 3)</ span > }
212+ </ p >
213+ </ div >
214+
215+ < div className = "border border-gray-700 p-6 rounded-xl bg-gray-800 shadow-2xl" >
216+ < div className = "mb-6 p-4 rounded-lg bg-gray-700 border-l-4 border-teal-400 shadow-inner min-h-[100px]" >
217+ < p className = "text-teal-400 font-medium text-md uppercase tracking-wide" > Current Action</ p >
218+ < p className = "text-xl mt-2 text-gray-200 leading-relaxed" >
219+ { message || 'Starting computation...' }
220+ </ p >
221+ </ div >
222+
223+ < ArrayDisplay
224+ title = "Input Array"
225+ array = { inputArray }
226+ highlightIndex = { highlight . input }
227+ indicesLabel = "i"
228+ />
229+
230+ < ArrayDisplay
231+ title = "Count Array"
232+ array = { count }
233+ highlightIndex = { highlight . count }
234+ readIndex = { highlight . countRead }
235+ indicesLabel = "val"
236+ />
237+
238+ < ArrayDisplay
239+ title = "Output Array"
240+ array = { output }
241+ highlightIndex = { highlight . output }
242+ indicesLabel = "pos"
243+ />
244+
245+ { isFinalStep && (
246+ < div className = "mt-8 p-5 rounded-xl bg-green-900 border-green-700 text-center shadow-lg" >
247+ < p className = "text-green-400 text-2xl font-extrabold flex items-center justify-center gap-3" >
248+ < span role = "img" aria-label = "confetti" > 🎉</ span > Algorithm Complete!
249+ </ p >
250+ </ div >
251+ ) }
252+
253+ </ div >
254+ </ >
255+ ) : (
256+ < div className = "text-center p-12 bg-gray-800 rounded-xl text-gray-400 text-xl shadow-xl border border-gray-700" >
257+ < p className = "mb-4" > Welcome to the Counting Sort Visualizer!</ p >
258+ < p > Enter a list of non-negative < b > numbers</ b > (max value 50).</ p >
259+ < p className = "mt-2" > Click < b > Re-Visualize</ b > to begin.</ p >
260+ </ div >
261+ ) }
262+ </ div >
263+ </ div >
264+ ) ;
265+ }
0 commit comments