11import Base from '../../../src/core/Base.mjs' ;
22import Neo from '../../../src/Neo.mjs' ;
33
4+ const hasRaf = typeof requestAnimationFrame === 'function' ;
5+
46/**
57 * @class DevRank.canvas.Sparkline
68 * @extends Neo.core.Base
79 * @singleton
810 */
911class Sparkline extends Base {
12+ static colors = {
13+ dark : {
14+ fillStart : 'rgba(62, 99, 221, 0.4)' ,
15+ fillEnd : 'rgba(62, 99, 221, 0.0)' ,
16+ line : '#3E63DD' ,
17+ marker : '#3E63DD' ,
18+ scanner : '#FFFFFF' ,
19+ textYear : '#AAAAAA' ,
20+ textValue : '#FFFFFF'
21+ } ,
22+ light : {
23+ fillStart : 'rgba(62, 99, 221, 0.4)' ,
24+ fillEnd : 'rgba(62, 99, 221, 0.0)' ,
25+ line : '#3E63DD' ,
26+ marker : '#3E63DD' ,
27+ scanner : '#000000' ,
28+ textYear : '#666666' ,
29+ textValue : '#000000'
30+ }
31+ }
32+
1033 static config = {
1134 /**
1235 * @member {String} className='DevRank.canvas.Sparkline'
@@ -18,7 +41,7 @@ class Sparkline extends Base {
1841 * @protected
1942 */
2043 remote : {
21- app : [ 'register' , 'updateData' ]
44+ app : [ 'onMouseLeave' , 'onMouseMove' , ' register', 'updateData' , 'updateSize ']
2245 } ,
2346 /**
2447 * @member {Boolean} singleton=true
@@ -35,6 +58,35 @@ class Sparkline extends Base {
3558 /**
3659 * @param {Object } data
3760 * @param {String } data.canvasId
61+ */
62+ onMouseLeave ( data ) {
63+ let item = this . items . get ( data . canvasId ) ;
64+ if ( item ) {
65+ item . mouseActive = false ;
66+ this . draw ( item ) ; // Redraw to clear overlay
67+ }
68+ }
69+
70+ /**
71+ * @param {Object } data
72+ * @param {String } data.canvasId
73+ * @param {Number } data.x
74+ * @param {Number } data.y
75+ */
76+ onMouseMove ( data ) {
77+ let item = this . items . get ( data . canvasId ) ;
78+ if ( item ) {
79+ item . mouseActive = true ;
80+ item . mouseX = data . x ;
81+ this . draw ( item ) ;
82+ }
83+ }
84+
85+ /**
86+ * @param {Object } data
87+ * @param {String } data.canvasId
88+ * @param {Number } [data.devicePixelRatio=1]
89+ * @param {String } [data.theme='light']
3890 * @param {String } data.windowId
3991 */
4092 register ( data ) {
@@ -45,9 +97,13 @@ class Sparkline extends Base {
4597 if ( canvas ) {
4698 me . items . set ( canvasId , {
4799 canvas,
48- ctx : canvas . getContext ( '2d' ) ,
49- height : canvas . height ,
50- width : canvas . width
100+ ctx : canvas . getContext ( '2d' ) ,
101+ devicePixelRatio : data . devicePixelRatio || 1 ,
102+ height : canvas . height ,
103+ mouseActive : false ,
104+ mouseX : 0 ,
105+ theme : data . theme || 'light' ,
106+ width : canvas . width
51107 } ) ;
52108 }
53109 }
@@ -67,11 +123,37 @@ class Sparkline extends Base {
67123 }
68124 }
69125
126+ /**
127+ * @param {Object } data
128+ * @param {String } data.canvasId
129+ * @param {Number } [data.devicePixelRatio]
130+ * @param {Number } data.height
131+ * @param {Number } data.width
132+ */
133+ updateSize ( data ) {
134+ let me = this ,
135+ item = me . items . get ( data . canvasId ) ;
136+
137+ if ( item ) {
138+ item . devicePixelRatio = data . devicePixelRatio || item . devicePixelRatio || 1 ;
139+ item . height = data . height ;
140+ item . width = data . width ;
141+ me . draw ( item ) ;
142+ }
143+ }
144+
70145 /**
71146 * @param {Object } item
72147 */
73148 draw ( item ) {
74- let { ctx, height, values, width} = item ;
149+ let me = this ,
150+ { ctx, devicePixelRatio, height, values, width, theme} = item ,
151+ colors = me . constructor . colors [ theme ] || me . constructor . colors . light ;
152+
153+ // Handle DPR Scaling
154+ item . canvas . width = width * devicePixelRatio ;
155+ item . canvas . height = height * devicePixelRatio ;
156+ ctx . scale ( devicePixelRatio , devicePixelRatio ) ;
75157
76158 if ( ! Array . isArray ( values ) || values . length < 2 ) {
77159 ctx . clearRect ( 0 , 0 , width , height ) ;
@@ -82,7 +164,7 @@ class Sparkline extends Base {
82164 max = Math . max ( ...values ) ,
83165 min = Math . min ( ...values ) ,
84166 range = max - min || 1 ,
85- padding = 4 , // Increased padding for marker radius
167+ padding = 4 ,
86168 h = height - ( padding * 2 ) ,
87169 stepX = width / ( len - 1 ) ,
88170 points = [ ] ;
@@ -93,23 +175,23 @@ class Sparkline extends Base {
93175 points . push ( {
94176 x : index * stepX ,
95177 y : height - padding - ( normalized * h ) ,
96- val : val
178+ val : val ,
179+ year : 2010 + index
97180 } ) ;
98181 } ) ;
99182
100183 ctx . clearRect ( 0 , 0 , width , height ) ;
101184
102- // 1. Create Gradient
185+ // 1. Draw Base Chart
186+ // Gradient
103187 let gradient = ctx . createLinearGradient ( 0 , 0 , 0 , height ) ;
104- gradient . addColorStop ( 0 , 'rgba(62, 99, 221, 0.4)' ) ; // Primary #3E63DD
105- gradient . addColorStop ( 1 , 'rgba(62, 99, 221, 0.0)' ) ;
188+ gradient . addColorStop ( 0 , colors . fillStart ) ;
189+ gradient . addColorStop ( 1 , colors . fillEnd ) ;
106190
107- // 2. Draw Area (Fill)
108191 ctx . beginPath ( ) ;
109- ctx . moveTo ( points [ 0 ] . x , height ) ; // Start bottom-left
192+ ctx . moveTo ( points [ 0 ] . x , height ) ;
110193 ctx . lineTo ( points [ 0 ] . x , points [ 0 ] . y ) ;
111194
112- // Smooth curve loop
113195 for ( let i = 0 ; i < len - 1 ; i ++ ) {
114196 let p0 = points [ i ] ,
115197 p1 = points [ i + 1 ] ,
@@ -123,12 +205,12 @@ class Sparkline extends Base {
123205 }
124206 }
125207
126- ctx . lineTo ( points [ len - 1 ] . x , height ) ; // Close to bottom-right
208+ ctx . lineTo ( points [ len - 1 ] . x , height ) ;
127209 ctx . closePath ( ) ;
128210 ctx . fillStyle = gradient ;
129211 ctx . fill ( ) ;
130212
131- // 3. Draw Line (Stroke)
213+ // Line
132214 ctx . beginPath ( ) ;
133215 ctx . moveTo ( points [ 0 ] . x , points [ 0 ] . y ) ;
134216
@@ -145,38 +227,81 @@ class Sparkline extends Base {
145227 }
146228 }
147229
148- ctx . strokeStyle = '#3E63DD' ; // Primary
149- ctx . lineWidth = 2 ;
230+ ctx . strokeStyle = colors . line ;
231+ ctx . lineWidth = 1 ;
150232 ctx . lineCap = 'round' ;
151233 ctx . lineJoin = 'round' ;
152234 ctx . stroke ( ) ;
153235
154- // 4. Draw Max Value Marker (if distinct)
155- if ( max > min ) {
156- let maxIndex = values . indexOf ( max ) ,
157- maxPoint = points [ maxIndex ] ;
236+ // 2. Draw Interaction Overlay
237+ if ( item . mouseActive ) {
238+ // Find nearest point
239+ let nearestDist = Infinity ,
240+ nearestPoint = null ;
241+
242+ points . forEach ( p => {
243+ let dist = Math . abs ( p . x - item . mouseX ) ;
244+ if ( dist < nearestDist ) {
245+ nearestDist = dist ;
246+ nearestPoint = p ;
247+ }
248+ } ) ;
249+
250+ if ( nearestPoint ) {
251+ // Scanner Line
252+ ctx . beginPath ( ) ;
253+ ctx . moveTo ( nearestPoint . x , 0 ) ;
254+ ctx . lineTo ( nearestPoint . x , height ) ;
255+ ctx . strokeStyle = colors . scanner ;
256+ ctx . lineWidth = 1 ;
257+ ctx . setLineDash ( [ 2 , 2 ] ) ;
258+ ctx . stroke ( ) ;
259+ ctx . setLineDash ( [ ] ) ;
260+
261+ // Intersection Dot
262+ ctx . beginPath ( ) ;
263+ ctx . arc ( nearestPoint . x , nearestPoint . y , 3 , 0 , Math . PI * 2 ) ;
264+ ctx . fillStyle = colors . scanner ;
265+ ctx . fill ( ) ;
266+ ctx . beginPath ( ) ;
267+ ctx . arc ( nearestPoint . x , nearestPoint . y , 1.5 , 0 , Math . PI * 2 ) ;
268+ ctx . fillStyle = colors . line ;
269+ ctx . fill ( ) ;
270+
271+ // Text Label
272+ ctx . font = 'bold 10px sans-serif' ;
273+ ctx . textAlign = 'center' ;
274+
275+ let textY = 10 ;
276+ let x = nearestPoint . x ;
158277
278+ // Adjust text alignment if near edges
279+ if ( x < 30 ) {
280+ ctx . textAlign = 'left' ;
281+ x += 5 ;
282+ } else if ( x > width - 30 ) {
283+ ctx . textAlign = 'right' ;
284+ x -= 5 ;
285+ }
286+
287+ // Draw Year
288+ ctx . fillStyle = colors . textYear ;
289+ ctx . fillText ( String ( nearestPoint . year ) , x , textY ) ;
290+
291+ // Draw Value
292+ let valueText = new Intl . NumberFormat ( ) . format ( nearestPoint . val ) ;
293+ ctx . fillStyle = colors . textValue ;
294+ ctx . fillText ( valueText , x , textY + 12 ) ;
295+ }
296+ } else {
297+ // Only draw End Point
298+ let lastPoint = points [ len - 1 ] ;
159299 ctx . beginPath ( ) ;
160- ctx . arc ( maxPoint . x , maxPoint . y , 2 , 0 , Math . PI * 2 ) ;
161- ctx . fillStyle = '#3E63DD' ;
300+ ctx . arc ( lastPoint . x , lastPoint . y , 1.5 , 0 , Math . PI * 2 ) ;
301+ ctx . fillStyle = colors . marker ;
162302 ctx . fill ( ) ;
163303 }
164-
165- // 5. Draw End Point (Glowing)
166- let lastPoint = points [ len - 1 ] ;
167-
168- // Glow
169- ctx . beginPath ( ) ;
170- ctx . arc ( lastPoint . x , lastPoint . y , 4 , 0 , Math . PI * 2 ) ;
171- ctx . fillStyle = 'rgba(62, 99, 221, 0.3)' ;
172- ctx . fill ( ) ;
173-
174- // Dot
175- ctx . beginPath ( ) ;
176- ctx . arc ( lastPoint . x , lastPoint . y , 2 , 0 , Math . PI * 2 ) ;
177- ctx . fillStyle = '#3E63DD' ;
178- ctx . fill ( ) ;
179304 }
180305}
181306
182- export default Neo . setupClass ( Sparkline ) ;
307+ export default Neo . setupClass ( Sparkline ) ;
0 commit comments