---
# System prepended metadata

title: zingchart 自製 crosshair & 互動視窗

---

# 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);
    });
  }
}
```




