Blame view

components/uni-charts/uni-charts.vue 14.2 KB
4f475013   刘淇   k线图
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
  <template>
    <view class="uni-charts">
      <canvas
          v-if="type !== 'map'"
          class="charts-canvas"
          :style="{width: width + 'px', height: height + 'px'}"
          canvas-id="uni-charts"
          @touchstart="touchStart"
          @touchmove="touchMove"
          @touchend="touchEnd"
      ></canvas>
      <view v-else class="map-container" :style="{width: width + 'px', height: height + 'px'}">
        <slot name="map"></slot>
      </view>
    </view>
  </template>
  
  <script>
  export default {
    name: 'uniCharts',
    props: {
      type: {
        type: String,
        default: 'line'
      },
      data: {
        type: Array,
        default () {
          return []
        }
      },
      categories: {
        type: Array,
        default () {
          return []
        }
      },
      option: {
        type: Object,
        default () {
53012a16   刘淇   首页 优化
41
42
43
44
45
          return {
            yAxis: {
              tickCount: 5 // 默认5个刻度,可通过option自定义,无上限
            }
          }
4f475013   刘淇   k线图
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
        }
      },
      width: {
        type: [Number, String],
        default: 375
      },
      height: {
        type: [Number, String],
        default: 200
      }
    },
    data () {
      return {
        ctx: null,
        chartData: {},
        touchInfo: {}
      }
    },
    watch: {
      data: {
        deep: true,
        handler () {
          this.initChart()
        }
      },
      categories: {
        deep: true,
        handler () {
          this.initChart()
        }
      },
      option: {
        deep: true,
        handler () {
          this.initChart()
        }
      }
    },
    mounted () {
      this.initChart()
    },
    methods: {
      initChart () {
        if (this.type === 'kline') {
          this.drawKline()
        } else if (this.type === 'line') {
          this.drawLine()
        }
      },
53012a16   刘淇   首页 优化
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
      // 核心:自适应计算Y轴整数刻度(无任何数值限制)
      calculateYAxisTicks (minVal, maxVal, tickCount = 5) {
        // 1. 确保是整数,且最大值>最小值(无数值范围限制)
        minVal = parseInt(minVal, 10)
        maxVal = parseInt(maxVal, 10)
  
        // 边界处理:极值相等时自动扩展范围(避免刻度重复)
        if (maxVal <= minVal) {
          maxVal = minVal + tickCount // 扩展范围,保证刻度不重复
        }
  
        // 2. 动态计算整数步长(完全自适应,无固定值限制)
        const totalRange = maxVal - minVal
        let step = Math.ceil(totalRange / (tickCount - 1)) // 关键:按刻度数算步长
        step = step < 1 ? 1 : step // 步长至少为1,保证整数刻度
  
        // 3. 生成连续不重复的整数刻度(无数值上限)
        const ticks = []
        for (let i = 0; i < tickCount; i++) {
          ticks.push(minVal + i * step)
        }
  
        // 4. 确保最大值能覆盖数据(无限制)
        if (ticks[ticks.length - 1] < maxVal) {
          ticks.push(maxVal)
        }
  
        return {
          ticks: ticks, // 最终刻度数组(纯整数、不重复、无限制)
          min: minVal,
          max: Math.max(...ticks),
          step: step
        }
      },
      // 折线图绘制(无限制版)
4f475013   刘淇   k线图
130
131
132
133
134
135
136
137
138
139
140
      drawLine () {
        if (!this.data.length || !this.categories.length) return;
  
        const ctx = uni.createCanvasContext('uni-charts', this)
        this.ctx = ctx
        const { width, height } = this
        const {
          grid = {},
          xAxis = {},
          yAxis = {},
          legend = {},
cf70629b   刘淇   养护计划 照片 自己写样式
141
          color = ['#25AF69', '#B34C17']
4f475013   刘淇   k线图
142
143
144
145
146
        } = this.option
  
        // 清空画布
        ctx.clearRect(0, 0, width, height)
  
53012a16   刘淇   首页 优化
147
        // Grid布局(自适应,无固定值)
4f475013   刘淇   k线图
148
149
150
151
152
153
154
155
        const gridTop = grid.top ? (typeof grid.top === 'string' ? parseFloat(grid.top) / 100 * height : grid.top) : 60
        const gridLeft = grid.left ? (typeof grid.left === 'string' ? parseFloat(grid.left) / 100 * width : grid.left) : 70
        const gridRight = grid.right ? (typeof grid.right === 'string' ? parseFloat(grid.right) / 100 * width : grid.right) : 20
        const gridBottom = grid.bottom ? (typeof grid.bottom === 'string' ? parseFloat(grid.bottom) / 100 * height : grid.bottom) : 50
  
        const drawWidth = width - gridLeft - gridRight
        const drawHeight = height - gridTop - gridBottom
  
53012a16   刘淇   首页 优化
156
        // 收集所有Y轴数据(无数值限制)
4f475013   刘淇   k线图
157
158
159
160
161
162
        let allValues = []
        this.data.forEach(series => {
          allValues = allValues.concat(series.data)
        })
        if (allValues.length === 0) return;
  
53012a16   刘淇   首页 优化
163
        // 基础极值(完全按数据来,无任何限制)
4f475013   刘淇   k线图
164
165
        const rawMaxVal = Math.max(...allValues)
        const rawMinVal = Math.min(...allValues)
53012a16   刘淇   首页 优化
166
167
168
169
170
171
172
173
174
175
176
177
        // 用户可通过yAxis.min/max自定义极值,否则用数据极值(无限制)
        const maxVal = yAxis.max !== undefined ? yAxis.max : rawMaxVal
        const minVal = yAxis.min !== undefined ? yAxis.min : rawMinVal
        // 刻度数量自定义(默认5个,可传任意数,无上限)
        const tickCount = yAxis.tickCount || 5
  
        // 核心:计算无限制、不重复的整数刻度
        const yAxisConfig = this.calculateYAxisTicks(minVal, maxVal, tickCount)
        const { ticks: yTicks, min: alignedMinVal, max: alignedMaxVal } = yAxisConfig
        const valRange = alignedMaxVal - alignedMinVal || 1
  
        // X轴步长(自适应)
4f475013   刘淇   k线图
178
179
        const xStep = drawWidth / (this.categories.length - 1 || 1)
  
53012a16   刘淇   首页 优化
180
        // X轴标签防拥挤(自适应,无限制)
4f475013   刘淇   k线图
181
182
183
184
185
186
        const minLabelWidth = 30
        const maxShowLabels = Math.floor(drawWidth / minLabelWidth)
        const labelInterval = maxShowLabels < this.categories.length
            ? Math.ceil(this.categories.length / maxShowLabels)
            : 1
  
53012a16   刘淇   首页 优化
187
        // 绘制网格线 + Y轴整数刻度(无数值限制)
4f475013   刘淇   k线图
188
189
        ctx.setStrokeStyle(yAxis.splitLine?.lineStyle?.color || '#f5f5f7')
        ctx.setLineWidth(1)
4f475013   刘淇   k线图
190
  
53012a16   刘淇   首页 优化
191
192
        yTicks.forEach((tickVal, i) => {
          const y = gridTop + drawHeight - (i * drawHeight / (yTicks.length - 1))
4f475013   刘淇   k线图
193
194
195
196
197
198
          // 绘制网格线
          ctx.beginPath()
          ctx.moveTo(gridLeft, y)
          ctx.lineTo(width - gridRight, y)
          ctx.stroke()
  
53012a16   刘淇   首页 优化
199
          // Y轴显示纯整数(无小数点、无任何数值限制)
4f475013   刘淇   k线图
200
201
          ctx.setFillStyle(yAxis.axisLabel?.color || '#666')
          ctx.setFontSize(yAxis.axisLabel?.fontSize || 12)
53012a16   刘淇   首页 优化
202
          const valText = tickVal.toString() // 纯整数,直接转字符串
4f475013   刘淇   k线图
203
204
          const textWidth = ctx.measureText(valText).width
          ctx.fillText(valText, gridLeft - 10 - textWidth, y + 5)
53012a16   刘淇   首页 优化
205
        })
4f475013   刘淇   k线图
206
  
53012a16   刘淇   首页 优化
207
        // 绘制X轴标签(自适应)
4f475013   刘淇   k线图
208
209
210
211
212
213
214
215
216
217
        ctx.setFillStyle(xAxis.axisLabel?.color || '#666')
        ctx.setFontSize(xAxis.axisLabel?.fontSize || 12)
        this.categories.forEach((text, index) => {
          if (index % labelInterval === 0) {
            const x = gridLeft + index * xStep
            let labelText = text
            if (text.length > 6) {
              labelText = text.slice(0, 6) + '...'
            }
            const textLines = labelText.split('\n')
4f475013   刘淇   k线图
218
219
220
221
222
223
224
            textLines.forEach((line, lineIdx) => {
              const textWidth = ctx.measureText(line).width
              ctx.fillText(line, x - textWidth / 2, height - gridBottom + 20 + (lineIdx * 12))
            })
          }
        })
  
53012a16   刘淇   首页 优化
225
        // 绘制图例(核心修复:颜色块和文字垂直居中对齐)
4f475013   刘淇   k线图
226
        if (legend.show) {
53012a16   刘淇   首页 优化
227
228
229
230
231
232
          // 获取图例文字大小(统一基准)
          const legendFontSize = legend.textStyle?.fontSize || 12
          ctx.setFontSize(legendFontSize)
          // 定义颜色块尺寸(和文字高度匹配)
          const legendBlockSize = legendFontSize * 0.8 // 颜色块大小 = 文字大小的80%,视觉更协调
          const legendBlockMargin = 5 // 颜色块和文字的间距
4f475013   刘淇   k线图
233
          let legendX = gridLeft + 10
53012a16   刘淇   首页 优化
234
235
236
          // 计算垂直居中基准线
          const legendYBase = gridTop - 30
  
4f475013   刘淇   k线图
237
238
239
240
241
242
          this.data.forEach((series, idx) => {
            let seriesName = series.name || `系列${idx + 1}`
            if (seriesName.length > 8) {
              seriesName = seriesName.slice(0, 8) + '...'
            }
            const textWidth = ctx.measureText(seriesName).width
4f475013   刘淇   k线图
243
  
53012a16   刘淇   首页 优化
244
245
246
247
248
249
250
            // 核心:计算颜色块和文字的垂直居中位置
            // 颜色块Y坐标 = 基准线 - 颜色块高度/2(居中)
            const legendBlockY = legendYBase - legendBlockSize / 2
            // 文字Y坐标 = 基准线 + 文字高度/4(canvas文字居中的黄金比例)
            const legendTextY = legendYBase + legendFontSize / 4
  
            // 绘制颜色块(居中对齐)
4f475013   刘淇   k线图
251
            ctx.setFillStyle(series.color || color[idx % color.length])
53012a16   刘淇   首页 优化
252
253
254
            ctx.fillRect(legendX, legendBlockY, legendBlockSize, legendBlockSize)
  
            // 绘制文字(和颜色块垂直居中)
4f475013   刘淇   k线图
255
            ctx.setFillStyle('#666')
53012a16   刘淇   首页 优化
256
            ctx.fillText(seriesName, legendX + legendBlockSize + legendBlockMargin, legendTextY)
4f475013   刘淇   k线图
257
  
53012a16   刘淇   首页 优化
258
259
            // 更新下一个图例的X坐标(包含间距)
            legendX += legendBlockSize + legendBlockMargin + textWidth + 10
4f475013   刘淇   k线图
260
261
262
          })
        }
  
53012a16   刘淇   首页 优化
263
        // 绘制折线(纯直线,无数值限制)
4f475013   刘淇   k线图
264
265
266
267
268
269
270
271
272
        this.data.forEach((series, seriesIdx) => {
          const seriesColor = series.color || color[seriesIdx % color.length]
  
          ctx.setStrokeStyle(seriesColor)
          ctx.setLineWidth(2)
          ctx.beginPath()
  
          series.data.forEach((value, index) => {
            const x = gridLeft + index * xStep
53012a16   刘淇   首页 优化
273
274
            // 自适应计算Y坐标(无数值限制)
            const y = gridTop + drawHeight - ((value - alignedMinVal) / valRange) * drawHeight
4f475013   刘淇   k线图
275
276
277
278
  
            if (index === 0) {
              ctx.moveTo(x, y)
            } else {
cf70629b   刘淇   养护计划 照片 自己写样式
279
              ctx.lineTo(x, y)
4f475013   刘淇   k线图
280
281
282
283
284
285
286
287
            }
          })
  
          ctx.stroke()
        })
  
        ctx.draw()
      },
53012a16   刘淇   首页 优化
288
      // K线图绘制(同步无限制逻辑 + 图例对齐修复)
4f475013   刘淇   k线图
289
290
291
292
293
294
295
296
      drawKline () {
        const ctx = uni.createCanvasContext('uni-charts', this)
        this.ctx = ctx
        const { width, height } = this
        const { grid = {}, xAxis = {}, yAxis = {}, color = ['#B34C17', '#25AF69'] } = this.option
  
        ctx.clearRect(0, 0, width, height)
  
53012a16   刘淇   首页 优化
297
        // Grid布局(自适应)
4f475013   刘淇   k线图
298
299
300
301
302
303
304
305
        const gridTop = grid.top ? (typeof grid.top === 'string' ? parseFloat(grid.top) / 100 * height : grid.top) : 50
        const gridLeft = grid.left ? (typeof grid.left === 'string' ? parseFloat(grid.left) / 100 * width : grid.left) : 70
        const gridRight = grid.right ? (typeof grid.right === 'string' ? parseFloat(grid.right) / 100 * width : grid.right) : 20
        const gridBottom = grid.bottom ? (typeof grid.bottom === 'string' ? parseFloat(grid.bottom) / 100 * height : grid.bottom) : 40
  
        const drawWidth = width - gridLeft - gridRight
        const drawHeight = height - gridTop - gridBottom
  
53012a16   刘淇   首页 优化
306
        // 收集所有Y轴数据(无限制)
4f475013   刘淇   k线图
307
308
309
310
311
312
        let allValues = []
        this.data.forEach(item => {
          allValues = allValues.concat([item.open, item.high, item.low, item.close])
        })
        if (allValues.length === 0) return;
  
53012a16   刘淇   首页 优化
313
        // 基础极值(无限制)
4f475013   刘淇   k线图
314
315
        const rawMaxVal = Math.max(...allValues)
        const rawMinVal = Math.min(...allValues)
53012a16   刘淇   首页 优化
316
317
318
319
320
321
322
323
        const maxVal = yAxis.max !== undefined ? yAxis.max : rawMaxVal
        const minVal = yAxis.min !== undefined ? yAxis.min : rawMinVal
        const tickCount = yAxis.tickCount || 5
  
        // 核心:无限制整数刻度计算
        const yAxisConfig = this.calculateYAxisTicks(minVal, maxVal, tickCount)
        const { ticks: yTicks, min: alignedMinVal, max: alignedMaxVal } = yAxisConfig
        const valRange = alignedMaxVal - alignedMinVal || 1
4f475013   刘淇   k线图
324
325
326
  
        const xStep = drawWidth / (this.data.length || 1)
  
53012a16   刘淇   首页 优化
327
        // 绘制网格线和Y轴刻度(无限制)
4f475013   刘淇   k线图
328
329
        ctx.setStrokeStyle('#f5f5f7')
        ctx.setLineWidth(1)
53012a16   刘淇   首页 优化
330
331
332
  
        yTicks.forEach((tickVal, i) => {
          const y = gridTop + drawHeight - (i * drawHeight / (yTicks.length - 1))
4f475013   刘淇   k线图
333
334
335
336
337
          ctx.beginPath()
          ctx.moveTo(gridLeft, y)
          ctx.lineTo(width - gridRight, y)
          ctx.stroke()
  
53012a16   刘淇   首页 优化
338
          // 纯整数显示(无限制)
4f475013   刘淇   k线图
339
340
          ctx.setFillStyle(yAxis.axisLabel?.color || '#666')
          ctx.setFontSize(yAxis.axisLabel?.fontSize || 12)
53012a16   刘淇   首页 优化
341
          const valText = tickVal.toString()
4f475013   刘淇   k线图
342
343
          const textWidth = ctx.measureText(valText).width
          ctx.fillText(valText, gridLeft - 10 - textWidth, y + 5)
53012a16   刘淇   首页 优化
344
        })
4f475013   刘淇   k线图
345
  
53012a16   刘淇   首页 优化
346
        // X轴标签(自适应)
4f475013   刘淇   k线图
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
        ctx.setFillStyle(xAxis.axisLabel?.color || '#666')
        ctx.setFontSize(xAxis.axisLabel?.fontSize || 12)
        const minLabelWidth = 30
        const maxShowLabels = Math.floor(drawWidth / minLabelWidth)
        const labelInterval = maxShowLabels < this.categories.length
            ? Math.ceil(this.categories.length / maxShowLabels)
            : 1
  
        this.categories.forEach((text, index) => {
          if (index % labelInterval === 0) {
            const x = gridLeft + index * xStep + xStep / 2
            let labelText = text
            if (text.length > 6) labelText = text.slice(0, 6) + '...'
            ctx.fillText(labelText, x - ctx.measureText(labelText).width / 2, height - gridBottom + 20)
          }
        })
  
53012a16   刘淇   首页 优化
364
        // 绘制K线(无数值限制)
4f475013   刘淇   k线图
365
366
367
        this.data.forEach((item, index) => {
          const { open, high, low, close } = item
          const x = gridLeft + index * xStep + xStep / 2
53012a16   刘淇   首页 优化
368
369
370
371
372
          // 自适应坐标计算
          const yOpen = gridTop + drawHeight - ((open - alignedMinVal) / valRange) * drawHeight
          const yClose = gridTop + drawHeight - ((close - alignedMinVal) / valRange) * drawHeight
          const yHigh = gridTop + drawHeight - ((high - alignedMinVal) / valRange) * drawHeight
          const yLow = gridTop + drawHeight - ((low - alignedMinVal) / valRange) * drawHeight
4f475013   刘淇   k线图
373
  
53012a16   刘淇   首页 优化
374
          // 绘制高低线
4f475013   刘淇   k线图
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
          ctx.setStrokeStyle(close >= open ? color[0] : color[1])
          ctx.setLineWidth(1)
          ctx.beginPath()
          ctx.moveTo(x, yHigh)
          ctx.lineTo(x, yLow)
          ctx.stroke()
  
          // 绘制实体
          const rectWidth = xStep / 3
          ctx.setFillStyle(close >= open ? color[0] : color[1])
          const rectY = Math.min(yOpen, yClose)
          const rectHeight = Math.abs(yClose - yOpen) || 2
          ctx.fillRect(x - rectWidth / 2, rectY, rectWidth, rectHeight)
        })
  
        ctx.draw()
      },
      touchStart (e) {
        this.touchInfo = {
          x: e.changedTouches[0].x,
          y: e.changedTouches[0].y,
          time: Date.now()
        }
      },
      touchMove (e) {
        this.touchInfo.x = e.changedTouches[0].x
        this.touchInfo.y = e.changedTouches[0].y
      },
      touchEnd (e) {
53012a16   刘淇   首页 优化
404
        // 可扩展交互逻辑,无限制
4f475013   刘淇   k线图
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
      }
    }
  }
  </script>
  
  <style scoped>
  .uni-charts {
    position: relative;
  }
  .charts-canvas {
    display: block;
  }
  .map-container {
    position: relative;
  }
  </style>