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>
|