@@ -24,19 +24,53 @@ export class QFChart implements ChartContext {
2424 private pluginManager : PluginManager ;
2525 public events : EventBus = new EventBus ( ) ;
2626
27+ // Drawing System
28+ private drawings : import ( "./types" ) . DrawingElement [ ] = [ ] ;
29+
2730 public coordinateConversion = {
2831 pixelToData : ( point : { x : number ; y : number } ) => {
29- const p = this . chart . convertFromPixel ( { seriesIndex : 0 } , [
30- point . x ,
31- point . y ,
32- ] ) ;
33- if ( p ) {
34- return { timeIndex : p [ 0 ] , value : p [ 1 ] } ;
32+ // Find which grid/pane the point is in
33+ // We iterate through all panes (series indices usually match pane indices for base series)
34+ // Actually, we need to know how many panes there are.
35+ // We can use the layout logic or just check grid indices.
36+ // ECharts instance has getOption().
37+ const option = this . chart . getOption ( ) as any ;
38+ if ( ! option || ! option . grid ) return null ;
39+
40+ const gridCount = option . grid . length ;
41+ for ( let i = 0 ; i < gridCount ; i ++ ) {
42+ if ( this . chart . containPixel ( { gridIndex : i } , [ point . x , point . y ] ) ) {
43+ // Found the pane
44+ const p = this . chart . convertFromPixel ( { seriesIndex : i } , [
45+ point . x ,
46+ point . y ,
47+ ] ) ;
48+ // Note: convertFromPixel might need seriesIndex or gridIndex depending on setup.
49+ // Using gridIndex in convertFromPixel is supported in newer ECharts but sometimes tricky.
50+ // Since we have one base series per pane (candlestick at 0, indicators at 1+),
51+ // assuming seriesIndex = gridIndex usually works if they are mapped 1:1.
52+ // Wait, candlestick is series 0. Indicators are subsequent series.
53+ // Series index != grid index necessarily.
54+ // BUT we can use { gridIndex: i } for convertFromPixel too!
55+ const pGrid = this . chart . convertFromPixel ( { gridIndex : i } , [
56+ point . x ,
57+ point . y ,
58+ ] ) ;
59+
60+ if ( pGrid ) {
61+ return { timeIndex : pGrid [ 0 ] , value : pGrid [ 1 ] , paneIndex : i } ;
62+ }
63+ }
3564 }
3665 return null ;
3766 } ,
38- dataToPixel : ( point : { timeIndex : number ; value : number } ) => {
39- const p = this . chart . convertToPixel ( { seriesIndex : 0 } , [
67+ dataToPixel : ( point : {
68+ timeIndex : number ;
69+ value : number ;
70+ paneIndex ?: number ;
71+ } ) => {
72+ const paneIdx = point . paneIndex || 0 ;
73+ const p = this . chart . convertToPixel ( { gridIndex : paneIdx } , [
4074 point . timeIndex ,
4175 point . value ,
4276 ] ) ;
@@ -159,6 +193,26 @@ export class QFChart implements ChartContext {
159193 this . chart = echarts . init ( this . chartContainer ) ;
160194 this . pluginManager = new PluginManager ( this , this . toolbarContainer ) ;
161195
196+ // Bind global chart/ZRender events to the EventBus
197+ this . chart . on ( "dataZoom" , ( params : any ) =>
198+ this . events . emit ( "chart:dataZoom" , params )
199+ ) ;
200+ this . chart . on ( "finished" , ( params : any ) =>
201+ this . events . emit ( "chart:updated" , params )
202+ ) ; // General chart update
203+ this . chart
204+ . getZr ( )
205+ . on ( "mousedown" , ( params : any ) => this . events . emit ( "mouse:down" , params ) ) ;
206+ this . chart
207+ . getZr ( )
208+ . on ( "mousemove" , ( params : any ) => this . events . emit ( "mouse:move" , params ) ) ;
209+ this . chart
210+ . getZr ( )
211+ . on ( "mouseup" , ( params : any ) => this . events . emit ( "mouse:up" , params ) ) ;
212+ this . chart
213+ . getZr ( )
214+ . on ( "click" , ( params : any ) => this . events . emit ( "mouse:click" , params ) ) ;
215+
162216 window . addEventListener ( "resize" , this . resize . bind ( this ) ) ;
163217 }
164218
@@ -188,6 +242,18 @@ export class QFChart implements ChartContext {
188242 this . pluginManager . register ( plugin ) ;
189243 }
190244
245+ // --- Drawing System ---
246+
247+ public addDrawing ( drawing : import ( "./types" ) . DrawingElement ) : void {
248+ this . drawings . push ( drawing ) ;
249+ this . render ( ) ; // Re-render to show new drawing
250+ }
251+
252+ public removeDrawing ( id : string ) : void {
253+ this . drawings = this . drawings . filter ( ( d ) => d . id !== id ) ;
254+ this . render ( ) ;
255+ }
256+
191257 // --------------------------------
192258
193259 public setMarketData ( data : OHLCV [ ] ) : void {
@@ -273,6 +339,30 @@ export class QFChart implements ChartContext {
273339 private render ( ) : void {
274340 if ( this . marketData . length === 0 ) return ;
275341
342+ // Capture current zoom state before rebuilding options
343+ let currentZoomState : { start : number ; end : number } | null = null ;
344+ try {
345+ const currentOption = this . chart . getOption ( ) as any ;
346+ if (
347+ currentOption &&
348+ currentOption . dataZoom &&
349+ currentOption . dataZoom . length > 0
350+ ) {
351+ // Find the slider or inside zoom component that controls the x-axis
352+ const zoomComponent = currentOption . dataZoom . find (
353+ ( dz : any ) => dz . type === "slider" || dz . type === "inside"
354+ ) ;
355+ if ( zoomComponent ) {
356+ currentZoomState = {
357+ start : zoomComponent . start ,
358+ end : zoomComponent . end ,
359+ } ;
360+ }
361+ }
362+ } catch ( e ) {
363+ // Chart might not be initialized yet
364+ }
365+
276366 // --- Sidebar Layout Management ---
277367 const tooltipPos = this . options . tooltip ?. position || "floating" ;
278368 const prevLeftDisplay = this . leftSidebar . style . display ;
@@ -303,6 +393,14 @@ export class QFChart implements ChartContext {
303393 this . options
304394 ) ;
305395
396+ // Apply preserved zoom state if available
397+ if ( currentZoomState && layout . dataZoom ) {
398+ layout . dataZoom . forEach ( ( dz ) => {
399+ dz . start = currentZoomState ! . start ;
400+ dz . end = currentZoomState ! . end ;
401+ } ) ;
402+ }
403+
306404 // Patch X-Axis with Data and Padding
307405 layout . xAxis . forEach ( ( axis ) => {
308406 axis . data = categoryData ;
@@ -338,7 +436,89 @@ export class QFChart implements ChartContext {
338436 this . toggleIndicator . bind ( this )
339437 ) ;
340438
341- // 4. Tooltip Formatter
439+ // 4. Build Drawings Series (One Custom Series per Pane used)
440+ const drawingsByPane = new Map <
441+ number ,
442+ import ( "./types" ) . DrawingElement [ ]
443+ > ( ) ;
444+ this . drawings . forEach ( ( d ) => {
445+ const paneIdx = d . paneIndex || 0 ;
446+ if ( ! drawingsByPane . has ( paneIdx ) ) {
447+ drawingsByPane . set ( paneIdx , [ ] ) ;
448+ }
449+ drawingsByPane . get ( paneIdx ) ! . push ( d ) ;
450+ } ) ;
451+
452+ const drawingSeriesList : any [ ] = [ ] ;
453+ drawingsByPane . forEach ( ( drawings , paneIndex ) => {
454+ drawingSeriesList . push ( {
455+ type : "custom" ,
456+ name : `drawings-pane-${ paneIndex } ` ,
457+ xAxisIndex : paneIndex ,
458+ yAxisIndex : paneIndex ,
459+ renderItem : ( params : any , api : any ) => {
460+ const drawing = drawings [ params . dataIndex ] ;
461+ if ( ! drawing ) return ;
462+
463+ const start = drawing . points [ 0 ] ;
464+ const end = drawing . points [ 1 ] ;
465+
466+ if ( ! start || ! end ) return ;
467+
468+ const p1 = api . coord ( [ start . timeIndex , start . value ] ) ;
469+ const p2 = api . coord ( [ end . timeIndex , end . value ] ) ;
470+
471+ if ( drawing . type === "line" ) {
472+ return {
473+ type : "group" ,
474+ children : [
475+ {
476+ type : "line" ,
477+ shape : {
478+ x1 : p1 [ 0 ] ,
479+ y1 : p1 [ 1 ] ,
480+ x2 : p2 [ 0 ] ,
481+ y2 : p2 [ 1 ] ,
482+ } ,
483+ style : {
484+ stroke : drawing . style ?. color || "#3b82f6" ,
485+ lineWidth : drawing . style ?. lineWidth || 2 ,
486+ } ,
487+ } ,
488+ {
489+ type : "circle" ,
490+ shape : { cx : p1 [ 0 ] , cy : p1 [ 1 ] , r : 4 } ,
491+ style : {
492+ fill : "#fff" ,
493+ stroke : drawing . style ?. color || "#3b82f6" ,
494+ lineWidth : 1 ,
495+ } ,
496+ } ,
497+ {
498+ type : "circle" ,
499+ shape : { cx : p2 [ 0 ] , cy : p2 [ 1 ] , r : 4 } ,
500+ style : {
501+ fill : "#fff" ,
502+ stroke : drawing . style ?. color || "#3b82f6" ,
503+ lineWidth : 1 ,
504+ } ,
505+ } ,
506+ ] ,
507+ } ;
508+ }
509+ } ,
510+ data : drawings . map ( ( d ) => [
511+ d . points [ 0 ] . timeIndex ,
512+ d . points [ 0 ] . value ,
513+ d . points [ 1 ] . timeIndex ,
514+ d . points [ 1 ] . value ,
515+ ] ) ,
516+ z : 100 ,
517+ silent : true ,
518+ } ) ;
519+ } ) ;
520+
521+ // 5. Tooltip Formatter
342522 const tooltipFormatter = ( params : any [ ] ) => {
343523 const html = TooltipFormatter . format ( params , this . options ) ;
344524 const mode = this . options . tooltip ?. position || "floating" ;
@@ -398,7 +578,7 @@ export class QFChart implements ChartContext {
398578 xAxis : layout . xAxis ,
399579 yAxis : layout . yAxis ,
400580 dataZoom : layout . dataZoom ,
401- series : [ candlestickSeries , ...indicatorSeries ] ,
581+ series : [ candlestickSeries , ...indicatorSeries , ... drawingSeriesList ] ,
402582 } ;
403583
404584 // Note: We should preserve any extra options (like custom graphics from plugins)
0 commit comments