2025-11-05 15:10:44 +08:00
|
|
|
|
import { number } from "vue-types"
|
|
|
|
|
|
|
2025-10-31 13:41:48 +08:00
|
|
|
|
const dataProcessing = (arr: any[]) => {
|
|
|
|
|
|
return arr
|
|
|
|
|
|
.filter(item => typeof item === 'number' || (typeof item === 'string' && !isNaN(parseFloat(item))))
|
|
|
|
|
|
.map(item => (typeof item === 'number' ? item : parseFloat(item)))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const calculateValue = (o: number, value: number, num: number, isMin: boolean) => {
|
|
|
|
|
|
if (value === 0) {
|
|
|
|
|
|
return 0
|
|
|
|
|
|
} else if (value > 0 && Math.abs(value) < 1 && isMin == true) {
|
|
|
|
|
|
return 0
|
|
|
|
|
|
} else if (value > -1 && value < 0 && isMin == false) {
|
|
|
|
|
|
return 0
|
|
|
|
|
|
}
|
2025-11-10 13:10:15 +08:00
|
|
|
|
|
2025-10-31 13:41:48 +08:00
|
|
|
|
let base
|
|
|
|
|
|
if (Math.abs(o) >= 100) {
|
|
|
|
|
|
base = 100
|
|
|
|
|
|
} else if (Math.abs(o) >= 10) {
|
|
|
|
|
|
base = 10
|
|
|
|
|
|
} else if (Math.abs(o) >= 1) {
|
|
|
|
|
|
base = 1
|
|
|
|
|
|
} else {
|
2025-11-05 15:09:00 +08:00
|
|
|
|
const multiple = 1 / 0.1
|
|
|
|
|
|
// 先放大→向上取整→再缩小
|
|
|
|
|
|
return Math.ceil(Math.abs(o) * multiple) / multiple
|
2025-10-31 13:41:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
let calculatedValue
|
|
|
|
|
|
if (isMin) {
|
|
|
|
|
|
if (value < 0) {
|
|
|
|
|
|
calculatedValue = value + num * value
|
|
|
|
|
|
} else {
|
|
|
|
|
|
calculatedValue = value - num * value
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (value < 0) {
|
|
|
|
|
|
calculatedValue = value - num * value
|
|
|
|
|
|
} else {
|
|
|
|
|
|
calculatedValue = value + num * value
|
2025-11-05 15:10:44 +08:00
|
|
|
|
|
2025-10-31 13:41:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-05 15:12:06 +08:00
|
|
|
|
|
2025-10-31 13:41:48 +08:00
|
|
|
|
if (base === 0.1) {
|
|
|
|
|
|
return parseFloat(calculatedValue.toFixed(1))
|
|
|
|
|
|
} else if (isMin) {
|
|
|
|
|
|
return Math.floor(calculatedValue / base) * base
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return Math.ceil(calculatedValue / base) * base
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理y轴最大最小值
|
|
|
|
|
|
export const yMethod = (arr: any) => {
|
|
|
|
|
|
let num = 0.2
|
|
|
|
|
|
let numList = dataProcessing(arr)
|
|
|
|
|
|
let maxValue = 0
|
|
|
|
|
|
let minValue = 0
|
|
|
|
|
|
let max = 0
|
|
|
|
|
|
let min = 0
|
|
|
|
|
|
maxValue = Math.max(...numList)
|
|
|
|
|
|
minValue = Math.min(...numList)
|
|
|
|
|
|
const o = maxValue - minValue
|
|
|
|
|
|
min = calculateValue(o, minValue, num, true)
|
2025-11-05 15:10:44 +08:00
|
|
|
|
|
2025-10-31 13:41:48 +08:00
|
|
|
|
max = calculateValue(o, maxValue, num, false)
|
|
|
|
|
|
// if (-100 >= minValue) {
|
|
|
|
|
|
// min = Math.floor((minValue + num * minValue) / 100) * 100
|
|
|
|
|
|
// } else if (-10 >= minValue && minValue > -100) {
|
|
|
|
|
|
// min = Math.floor((minValue + num * minValue) / 10) * 10
|
|
|
|
|
|
// } else if (-1 >= minValue && minValue > -10) {
|
|
|
|
|
|
// min = Math.floor(minValue + num * minValue)
|
|
|
|
|
|
// } else if (0 > minValue && minValue > -1) {
|
|
|
|
|
|
// min = parseFloat((minValue + num * minValue).toFixed(1))
|
|
|
|
|
|
// } else if (minValue == 0) {
|
|
|
|
|
|
// min = 0
|
|
|
|
|
|
// } else if (0 < minValue && minValue < 1) {
|
|
|
|
|
|
// min = parseFloat((minValue - num * minValue).toFixed(1))
|
|
|
|
|
|
// } else if (1 <= minValue && minValue < 10) {
|
|
|
|
|
|
// min = Math.floor(minValue - num * minValue)
|
|
|
|
|
|
// } else if (10 <= minValue && minValue < 100) {
|
|
|
|
|
|
// min = Math.floor((minValue - num * minValue) / 10) * 10
|
|
|
|
|
|
// } else if (100 <= minValue) {
|
|
|
|
|
|
// min = Math.floor((minValue - num * minValue) / 100) * 100
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
// if (-100 >= maxValue) {
|
|
|
|
|
|
// max = Math.ceil((maxValue - num * maxValue) / 100) * 100
|
|
|
|
|
|
// } else if (-10 >= maxValue && maxValue > -100) {
|
|
|
|
|
|
// max = Math.ceil((maxValue - num * maxValue) / 10) * 10
|
|
|
|
|
|
// } else if (-1 >= maxValue && maxValue > -10) {
|
|
|
|
|
|
// max = Math.ceil(maxValue - num * maxValue)
|
|
|
|
|
|
// } else if (0 > maxValue && maxValue > -1) {
|
|
|
|
|
|
// max = parseFloat((maxValue - num * maxValue).toFixed(1))
|
|
|
|
|
|
// } else if (maxValue == 0) {
|
|
|
|
|
|
// max = 0
|
|
|
|
|
|
// } else if (0 < maxValue && maxValue < 1) {
|
|
|
|
|
|
// max = parseFloat((maxValue + num * maxValue).toFixed(1))
|
|
|
|
|
|
// } else if (1 <= maxValue && maxValue < 10) {
|
|
|
|
|
|
// max = Math.ceil(maxValue + num * maxValue)
|
|
|
|
|
|
// } else if (10 <= maxValue && maxValue < 100) {
|
|
|
|
|
|
// max = Math.ceil((maxValue + num * maxValue) / 10) * 10
|
|
|
|
|
|
// } else if (100 <= maxValue) {
|
|
|
|
|
|
// max = Math.ceil((maxValue + num * maxValue) / 100) * 100
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
// if (maxValue > 1000 || minValue < -1000) {
|
|
|
|
|
|
// max = Math.ceil(maxValue / 100) * 100
|
|
|
|
|
|
// if (minValue == 0) {
|
|
|
|
|
|
// min = 0
|
|
|
|
|
|
// } else {
|
|
|
|
|
|
// min = Math.floor(minValue / 100) * 100
|
|
|
|
|
|
// }
|
|
|
|
|
|
// } else if (maxValue < 60 && minValue > 40) {
|
|
|
|
|
|
// max = 60
|
|
|
|
|
|
// min = 40
|
|
|
|
|
|
// } else if (maxValue == minValue && maxValue < 10 && minValue > 0) {
|
|
|
|
|
|
// max = Math.ceil(maxValue / 10) * 10
|
|
|
|
|
|
// min = Math.floor(minValue / 10) * 10
|
|
|
|
|
|
// } else if (maxValue == minValue && maxValue != 0 && minValue != 0) {
|
|
|
|
|
|
// max = Math.ceil(maxValue / 10 + 1) * 10
|
|
|
|
|
|
// min = Math.floor(minValue / 10 - 1) * 10
|
|
|
|
|
|
// } else {
|
|
|
|
|
|
// max = Math.ceil(maxValue / 10) * 10
|
|
|
|
|
|
// min = Math.floor(minValue / 10) * 10
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
// if (maxValue > 0 && maxValue < 1) {
|
|
|
|
|
|
// max = 1
|
|
|
|
|
|
// } else if (max == 0 && minValue > -1 && minValue < 0) {
|
|
|
|
|
|
// min = -1
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
return [min, max]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* title['A相','B相',]
|
|
|
|
|
|
* data[[1,2],[3,4]]
|
|
|
|
|
|
*/
|
|
|
|
|
|
// 导出csv文件
|
|
|
|
|
|
const convertToCSV = (title: object, data: any) => {
|
|
|
|
|
|
let csv = ''
|
|
|
|
|
|
// 添加列头
|
|
|
|
|
|
csv += ',' + title.join(',') + '\n'
|
|
|
|
|
|
// 遍历数据并添加到CSV字符串中
|
|
|
|
|
|
data?.map(item => {
|
|
|
|
|
|
csv += '\u200B' + item.join(',') + '\n'
|
|
|
|
|
|
})
|
|
|
|
|
|
return csv
|
|
|
|
|
|
}
|
|
|
|
|
|
export const exportCSV = (title: object, data: any, filename: string) => {
|
|
|
|
|
|
const csv = convertToCSV(title, data)
|
|
|
|
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
|
|
|
|
|
const link = document.createElement('a')
|
|
|
|
|
|
link.href = URL.createObjectURL(blob)
|
|
|
|
|
|
link.download = filename
|
|
|
|
|
|
link.click()
|
|
|
|
|
|
// 释放URL对象
|
|
|
|
|
|
URL.revokeObjectURL(link.href)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 补全时间序列数据中缺失的条目
|
|
|
|
|
|
* @param rawData 原始数据,格式为 [["时间字符串", "数值", "单位", "类型"], ...]
|
|
|
|
|
|
* @returns 补全后的数据,缺失条目数值为 null
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const completeTimeSeries = (rawData: string[][]): (string | null)[][] => {
|
|
|
|
|
|
// 步骤1:校验原始数据并解析时间
|
|
|
|
|
|
if (rawData.length < 2) {
|
|
|
|
|
|
console.warn('数据量不足2条,无法计算时间间隔,直接返回原始数据')
|
|
|
|
|
|
return rawData.map(item => [...item])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 解析所有时间为Date对象,过滤无效时间并按时间排序
|
|
|
|
|
|
const validData = rawData
|
|
|
|
|
|
.map(item => {
|
|
|
|
|
|
// 确保至少有时间和数值字段
|
|
|
|
|
|
if (!item[0]) {
|
|
|
|
|
|
return { time: new Date(0), item, isValid: false }
|
|
|
|
|
|
}
|
|
|
|
|
|
const time = new Date(item[0])
|
|
|
|
|
|
return { time, item, isValid: !isNaN(time.getTime()) }
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(data => data.isValid)
|
|
|
|
|
|
.sort((a, b) => a.time.getTime() - b.time.getTime()) // 确保数据按时间排序
|
|
|
|
|
|
.map(data => data.item)
|
|
|
|
|
|
|
|
|
|
|
|
if (validData.length < 2) {
|
|
|
|
|
|
throw new Error('有效时间数据不足2条,无法继续处理')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 步骤2:计算时间间隔(分析前几条数据确定最可能的间隔)
|
|
|
|
|
|
const intervals: number[] = []
|
|
|
|
|
|
// 分析前10条数据来确定间隔,避免单一间隔出错
|
|
|
|
|
|
const analyzeCount = Math.min(10, validData.length - 1)
|
|
|
|
|
|
for (let i = 0; i < analyzeCount; i++) {
|
|
|
|
|
|
const currentTime = new Date(validData[i][0]!).getTime()
|
|
|
|
|
|
const nextTime = new Date(validData[i + 1][0]!).getTime()
|
|
|
|
|
|
const interval = nextTime - currentTime
|
|
|
|
|
|
if (interval > 0) {
|
|
|
|
|
|
intervals.push(interval)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 取最常见的间隔作为标准间隔
|
|
|
|
|
|
const timeInterval = getMostFrequentValue(intervals)
|
|
|
|
|
|
if (timeInterval <= 0) {
|
|
|
|
|
|
throw new Error('无法确定有效的时间间隔')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 步骤3:生成完整的时间序列范围(从第一条到最后一条)
|
|
|
|
|
|
const startTime = new Date(validData[0][0]!).getTime()
|
|
|
|
|
|
const endTime = new Date(validData[validData.length - 1][0]!).getTime()
|
|
|
|
|
|
const completeTimes: Date[] = []
|
|
|
|
|
|
|
|
|
|
|
|
// 生成从 startTime 到 endTime 的所有间隔时间点
|
|
|
|
|
|
for (let time = startTime; time <= endTime; time += timeInterval) {
|
|
|
|
|
|
completeTimes.push(new Date(time))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 步骤4:将原始数据转为时间映射表,使用精确的时间字符串匹配
|
|
|
|
|
|
const timeDataMap = new Map<string, (string | undefined)[]>()
|
|
|
|
|
|
validData.forEach(item => {
|
|
|
|
|
|
// 使用原始时间字符串作为键,避免格式转换导致的匹配问题
|
|
|
|
|
|
if (item[0]) {
|
|
|
|
|
|
timeDataMap.set(item[0], item)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 提取模板数据(从第一条有效数据中提取单位和类型,处理可能的缺失)
|
|
|
|
|
|
const template = validData[0]
|
|
|
|
|
|
|
|
|
|
|
|
// 步骤5:对比补全数据,缺失条目数值为 null
|
|
|
|
|
|
const completedData = completeTimes.map(time => {
|
|
|
|
|
|
// 保持与原始数据相同的时间格式
|
|
|
|
|
|
const timeStr = formatTime(time)
|
|
|
|
|
|
const existingItem = timeDataMap.get(timeStr)
|
|
|
|
|
|
|
|
|
|
|
|
if (existingItem) {
|
|
|
|
|
|
// 存在该时间,返回原始数据
|
|
|
|
|
|
return [...existingItem]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 缺失该时间,数值设为 null,其他字段沿用第一个有效数据的格式
|
|
|
|
|
|
// 处理可能缺失的单位和类型字段
|
|
|
|
|
|
const result: (string | null | undefined)[] = [timeStr, '/']
|
|
|
|
|
|
// 仅在原始数据有单位字段时才添加
|
|
|
|
|
|
if (template.length > 2) {
|
|
|
|
|
|
result.push(template[2])
|
|
|
|
|
|
}
|
|
|
|
|
|
// 仅在原始数据有类型字段时才添加
|
|
|
|
|
|
if (template.length > 3) {
|
|
|
|
|
|
result.push(template[3])
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return completedData
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 格式化时间为 "YYYY-MM-DD HH:mm:ss" 格式
|
|
|
|
|
|
* @param date 日期对象
|
|
|
|
|
|
* @returns 格式化后的时间字符串
|
|
|
|
|
|
*/
|
|
|
|
|
|
function formatTime(date: Date): string {
|
|
|
|
|
|
const year = date.getFullYear()
|
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
|
const day = String(date.getDate()).padStart(2, '0')
|
|
|
|
|
|
const hours = String(date.getHours()).padStart(2, '0')
|
|
|
|
|
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
|
|
|
|
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
|
|
|
|
|
|
|
|
|
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取数组中出现频率最高的值
|
|
|
|
|
|
* @param arr 数字数组
|
|
|
|
|
|
* @returns 出现频率最高的值
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getMostFrequentValue(arr: number[]): number {
|
|
|
|
|
|
if (arr.length === 0) return 0
|
|
|
|
|
|
|
|
|
|
|
|
const frequencyMap = new Map<number, number>()
|
|
|
|
|
|
arr.forEach(num => {
|
|
|
|
|
|
frequencyMap.set(num, (frequencyMap.get(num) || 0) + 1)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
let maxFrequency = 0
|
|
|
|
|
|
let mostFrequent = arr[0]
|
|
|
|
|
|
|
|
|
|
|
|
frequencyMap.forEach((frequency, num) => {
|
|
|
|
|
|
if (frequency > maxFrequency) {
|
|
|
|
|
|
maxFrequency = frequency
|
|
|
|
|
|
mostFrequent = num
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return mostFrequent
|
|
|
|
|
|
}
|