# zingchart 自製 crosshair & 互動視窗 > **實作目標** > 在 zingchart 圖表上方實作使用者互動的 crosshair 及顯示數值用的小視窗(plotlabel)。 > crosshair 應隨手指點擊到的線段變色,並跟隨著手指拖曳而移動。而互動視窗固定顯示在 crosshair 上方,並在視窗中顯示 crosshair 經過的即時數值。 > ![](https://i.imgur.com/Cud4MWc.gif) 在使用 zingchart 時,嘗試用官方提供的 crosshair 及 plotlabel 功能來實作客戶需求,但因為官方未提供讓 crosshair 變換顏色的功能(若強制變換 crosshair 顏色會造成閃爍問題),且用來顯示 crosshair 數值用的視窗無法固定顯示位置,因此在 zingchart 上層實作模擬該功能。 --- ### 監聽使用者點擊畫面 使用 zingchart 提供的 guide_mousemove event 來監聽使用者點擊圖表線段,其中提供了使用者點擊到的 X Y 軸數值,並在 event.ev 包裹了 touchstart touchmove 事件。 > 在 zingchart 的 crosshair 開啟時 guide_mousemove event 才會被回傳,因此需要在 config 中將其開啟,並設為透明: > ```json= > // zingchart render config > crosshairX: { > alpha: 0, > trigger: 'hover', > plotLabel: { > text: ``, > visible: false, > }, > scaleLabel: { > visible: false, > }, > } > ``` 監聽 zingchart 發出的 guide_mousemove 事件 ```javascript= zingchart.guide_mousemove = event => { const x = event.guide.x; const age = event.items[0].keyvalue; const value = event.items[0].value; // 當使用者點擊到圖表線段時,添加 crosshair 及互動視窗至畫面上 if ( event.ev.type === 'touchstart' ) { // 取得點擊圖表線段的 index const plotIdx = event.items[0].plotindex; // 取得圖表各線段 lineColor 陣列 const graphset = this.chartConfig.data.graphset[0]; const seriesColors = graphset.series.map( item => item.lineColor ); // 計算 crosshair 應顯示的顏色 const lineColor = seriesColors[plotIdx]; // create new crosshair this.newCrosshair(x, lineColor); // 準備互動視窗中要顯示的資料 const roiList = this.planDetail.po_PremiumTable.map( item => item.ROI); roiList.push(null); const roi = roiList[plotIdx]; const decimalPoint = this.decimalPoint; // create new plotlabel this.newPlotLabel({x, roi, age, value, decimalPoint}); // 手指後續拖曳時,更新 crosshair 及互動視窗位置及數值 } else { this.updateCrosshairPosition(x); this.updatePlotLabelData({x, age, value}); } }; ``` 其中,在使用者點擊圖表(`event.ev.type === 'touchstart'`)時,會添加新的 crosshair 及 plotlabel 至畫面上,否則(`event.ev.type === 'touchmove'`)僅會更新 crosshair 及 plotlabel 的 x 軸位置及資料數值。 --- ### Create & Update Crosshair 用 div 繪製模擬 crosshair,容器高度扣除自行設定的 margin 即是 crosshair 的 height。 ```javascript= newCrosshair(x: number, lineColor: string) { const corsshairHeight = this.chartContainer.nativeElement.clientHeight - this.plotAreaMargin.top - this.plotAreaMargin.bottom; const crosshair = this.renderer.createElement('div'); this.renderer.addClass(crosshair, 'crosshair'); this.renderer.setStyle(crosshair, 'borderColor', lineColor); this.renderer.setStyle(crosshair, 'height', `${corsshairHeight}px`); this.renderer.setStyle(crosshair, 'left', `${x}px`); this.renderer.appendChild(this.chartContainer.nativeElement, crosshair); } ``` 更新 crosshair 位置 ```javascript= updateCrosshairPosition(x: number) { const crosshair = this.el.nativeElement.querySelector('.crosshair'); if (crosshair) { this.renderer.setStyle(crosshair, 'left', `${x}px`); } } ``` --- ### Create & Update Plotlabel 要將 PlotLabelComponent 動態添加到 dom tree 上,需要使用 angular 提供的 componentFactoryResolver ```javascript= @ViewChild('plotLabel', {read: ViewContainerRef}) plotLabel: ViewContainerRef; // ... newPlotLabel(data) { const compFactory = this.componentFactoryResolver.resolveComponentFactory(PlotLabelComponent); this.plotLabelCompRef = this.plotLabel.createComponent(compFactory); (<PlotLabelComponent>this.plotLabelCompRef.instance).data = data; } ``` 更新 plotlabel 部分資料時,將舊資料與欲更新的資料合併再塞回 PlotLabelComponent 中。 ```javascript= updatePlotLabelData(newData) { const oldData = (<PlotLabelComponent>this.plotLabelCompRef.instance).data; (<PlotLabelComponent>this.plotLabelCompRef.instance).data = {...oldData, ...newData}; } ``` --- ### Clear Crosshair & Plotlabel 在使用者手指離開螢幕時,crosshair 和 plotlabel 就應該消失,所以需要將清除這兩個元件的功能掛載在 `touchend` event 上 zingchart 的 guide_mousemove event 除了 `touchstart`、`touchmove` 外,並沒有提供 `touchend` event,所以我們利用 `HostListener` 監聽 `touchend` 事件: ```javascript= @HostListener('touchend', ['$event']) onTouchEnd(e) { this.clearCrosshairs(); this.plotLabel.clear(); // ViewContainerRef 提供的 clear method } ``` 為了避免畫面上可能殘留未清除的 crosshair,使用 `querySelectorAll` 選取畫面上所有的 crosshair 一併清理。 ```javascript= clearCrosshairs() { const crosshairList: NodeList = this.el.nativeElement.querySelectorAll('.crosshair'); if (crosshairList.length) { Array.from(crosshairList).forEach( node => { this.renderer.removeChild(this.chartContainer.nativeElement, node); }); } } ```