#### # 引言

本次任务是需要在一个指标图上通过点击标记画出此标记参与计算的数据范围、最高最低值、参考线等等，于是有了以下代码。代码仅供参考，如有错误的地方请指正！

#### # 模版代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168

    // 箱体指标
    const boxDataScope = 300 //箱体范围
    // 黄金线参数
    const goldenSectionA = 0.191
    const goldenSectionB = 0.382
    const goldenSectionC = 0.5
    const goldenSectionD = 0.618
    const goldenSectionE = 0.809
    this.chart.addTechnicalIndicatorTemplate({
    	name: 'custom_box_solid',
        shortName: '箱体',
        precision: 2,
        plots: [ // key属性的值最好在主图数据范围内，否则Y轴的值会跟着变大可能会导致主图变成一条线
          { key: 'max', title: '最高：' },
          { key: 'min', title: '最低：' }
        ],
        calcParams: [boxDataScope, goldenSectionA, goldenSectionB, goldenSectionC, goldenSectionD, goldenSectionE],
        calcTechnicalIndicator: (dataList, { params, plots }) => {
          let allDatas = [] // 所有数据
          let selectedDatas = [] //选中的数据
          for (let i = 0; i < dataList.length; i++) {
            let kLineData = dataList[i]
            if (new Date(dateConvert(kLineData.timestamp)).getTime() === getGlobalObject('boxId')) {
              const size = params[0]
              // 找出当前数据往前的size条数据（包含自己）
              let startData = i - size + 1
              if (startData < 0) {
                startData = 0
              }
              let endData = i + 1
              if (endData > dataList.length) {
                endData = dataList.length
              }
              selectedDatas = dataList.slice(startData, endData)
            }
            allDatas.push({
              timestamp: kLineData.timestamp
            })
          }
          // 找出选中数据中最高最低差价
          let max, min
          selectedDatas.forEach(function (item) {
            let value = item.close - item.close2
            if (!max || value > max) {
              max = value
            }
            if (!min || value < min) {
              min = value
            }
          })
    	  // 返回指标最终数据（未选中的数据用空对象替换）
    	  // 必须返回和dataList一样条数的数据，否则title不会显示
          return allDatas.map((data, i) => {
            let item = {
            }
            selectedDatas.map((selected, j) => {
              if (data.timestamp === selected.timestamp) {
                item.timestamp = selected.timestamp
                item.max = max
                item.min = min
              }
            })
            return item
          })
        },
        render: ({ ctx, dataSource, viewport, styles, xAxis, yAxis }) => {
          if (dataSource.technicalIndicatorDataList.length <= 0) { // 无指标数据则不处理
            return
          }
    	  // X轴起始像素
          let x = xAxis.convertToPixel(0)
          // 标记选中数据的起止位置
          let start
          let end = dataSource.technicalIndicatorDataList.length - 1
          dataSource.technicalIndicatorDataList.forEach(function (kLineData, i) {
            if (kLineData.timestamp) {
              if (!start) {
                start = i
              }
              if (i < end && !dataSource.technicalIndicatorDataList[i + 1].timestamp) {
                end = i
              }
              let max = kLineData.max
              let min = kLineData.min
              ctx.fillStyle = '#fff'
              ctx.textBaseline = 'middle'
              ctx.textAlign = 'center'
              // 画箱体
    		  // 箱体颜色
    		  ctx.strokeStyle = '#DC143C'
    		  // y轴最高点位置
    		  let yHigh = yAxis.convertToPixel(max)
    		  // y轴最低点位置
    		  let yLow = yAxis.convertToPixel(min)
    		  ctx.beginPath()
    		  // 画笔移动到数据的x轴起始点，y轴最高点
    		  ctx.moveTo(x, yHigh)
              if (i === start) { // 如果是第一条数据则需要画一条竖线
                ctx.lineTo(x, yLow) // 画竖线
                ctx.moveTo(x, yHigh) // 画笔移回
              }
              if (i === end) { // 如果是最后一条数据则需要画一条竖线
                ctx.lineTo(x, yLow) // 画竖线
                ctx.fillText(max, x + viewport.dataSpace, yHigh) // 标识箱体最高点的值
                ctx.moveTo(x, yLow) // 画笔移动到Y轴最低点
                ctx.fillText(min, x + viewport.dataSpace, yLow) // 标识箱体最低点的值
              } else { // 画两条横线，一条在y轴最高点，一条在y轴最低点
                ctx.lineTo(x + viewport.dataSpace, yHigh) // y轴最高点横线
                ctx.moveTo(x, yLow) // 画笔移动到y轴最低点
                ctx.lineTo(x + viewport.dataSpace, yLow) // y轴最低点横线
              }
              ctx.stroke()
              ctx.closePath()
              // 画黄金线
    		  // 黄金线颜色
              ctx.strokeStyle = '#ffffff'
    		  // 根据黄金线参数计算黄金线的值
              let goldenSectionLineA = (max - min) * goldenSectionA + min
              let goldenSectionLineB = (max - min) * goldenSectionB + min
              let goldenSectionLineC = (max - min) * goldenSectionC + min
              let goldenSectionLineD = (max - min) * goldenSectionD + min
              let goldenSectionLineE = (max - min) * goldenSectionE + min
    		  // 根据黄金线的值获取Y轴高度
              let yA = yAxis.convertToPixel(goldenSectionLineA)
              let yB = yAxis.convertToPixel(goldenSectionLineB)
              let yC = yAxis.convertToPixel(goldenSectionLineC)
              let yD = yAxis.convertToPixel(goldenSectionLineD)
              let yE = yAxis.convertToPixel(goldenSectionLineE)
              ctx.beginPath()
    		  // 画第一条黄金线
              ctx.moveTo(x, yA)
              ctx.lineTo(x + viewport.barSpace / 2, yA)
              if (i === end) { // 是否最后一条数据，如果是则需要标识黄金线的值
    		    // 标识第一条黄金线的值
                ctx.fillText(goldenSectionLineA.toFixed(2), x + viewport.dataSpace, yA)
              }
    		  // 画第二条黄金线
              ctx.moveTo(x, yB)
              ctx.lineTo(x + viewport.barSpace / 2, yB)
              if (i === end) {
                ctx.fillText(goldenSectionLineB.toFixed(2), x + viewport.dataSpace, yB)
              }
    		  // 画第三条黄金线
              ctx.moveTo(x, yC)
              ctx.lineTo(x + viewport.barSpace / 2, yC)
              if (i === end) {
                ctx.fillText(goldenSectionLineC.toFixed(2), x + viewport.dataSpace, yC)
              }
    		  // 画第四条黄金线
              ctx.moveTo(x, yD)
              ctx.lineTo(x + viewport.barSpace / 2, yD)
              if (i === end) {
                ctx.fillText(goldenSectionLineD.toFixed(2), x + viewport.dataSpace, yD)
              }
    		  // 画第五条黄金线
              ctx.moveTo(x, yE)
              ctx.lineTo(x + viewport.barSpace / 2, yE)
              if (i === end) {
                ctx.fillText(goldenSectionLineE.toFixed(2), x + viewport.dataSpace, yE)
              }
              ctx.stroke()
              ctx.closePath()
            }
    		// 计算X轴的下一个位置
            x += viewport.dataSpace
          })
        }
    })

#### # 结语

以上代码只是箱体的指标模版，还需要根据业务逻辑在标记上实现点击事件，然后通过事件动态添加移除箱体指标。

KLineChart K线 自定义指标