141 lines
3.8 KiB
TypeScript
141 lines
3.8 KiB
TypeScript
|
|
/**
|
|||
|
|
* 图表工具函数
|
|||
|
|
* 提供数据格式化、颜色处理等通用功能
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 格式化金额
|
|||
|
|
* @param amount 金额
|
|||
|
|
* @param decimals 小数位数
|
|||
|
|
*/
|
|||
|
|
export function formatMoney(amount: number, decimals: number = 2): string {
|
|||
|
|
return amount.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 格式化百分比
|
|||
|
|
* @param value 值
|
|||
|
|
* @param total 总数
|
|||
|
|
* @param decimals 小数位数
|
|||
|
|
*/
|
|||
|
|
export function formatPercentage(value: number, total: number, decimals: number = 1): string {
|
|||
|
|
if (total === 0) return '0%'
|
|||
|
|
return ((value / total) * 100).toFixed(decimals) + '%'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生成渐变色
|
|||
|
|
* @param color 基础颜色
|
|||
|
|
* @param alpha 透明度
|
|||
|
|
*/
|
|||
|
|
export function colorWithAlpha(color: string, alpha: number): string {
|
|||
|
|
// 如果是 hex 颜色,转换为 rgba
|
|||
|
|
if (color.startsWith('#')) {
|
|||
|
|
const r = parseInt(color.slice(1, 3), 16)
|
|||
|
|
const g = parseInt(color.slice(3, 5), 16)
|
|||
|
|
const b = parseInt(color.slice(5, 7), 16)
|
|||
|
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
|||
|
|
}
|
|||
|
|
// 如果已经是 rgb/rgba,替换 alpha
|
|||
|
|
return color.replace(/rgba?\(([^)]+)\)/, (match, values) => {
|
|||
|
|
const parts = values.split(',').slice(0, 3)
|
|||
|
|
return `rgba(${parts.join(',')}, ${alpha})`
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 创建渐变背景(用于折线图填充)
|
|||
|
|
* @param ctx Canvas 上下文
|
|||
|
|
* @param chartArea 图表区域
|
|||
|
|
* @param color 颜色
|
|||
|
|
*/
|
|||
|
|
export function createGradient(ctx: CanvasRenderingContext2D, chartArea: any, color: string) {
|
|||
|
|
const gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top)
|
|||
|
|
gradient.addColorStop(0, colorWithAlpha(color, 0.0))
|
|||
|
|
gradient.addColorStop(0.5, colorWithAlpha(color, 0.1))
|
|||
|
|
gradient.addColorStop(1, colorWithAlpha(color, 0.3))
|
|||
|
|
return gradient
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 截断文本(移动端长标签处理)
|
|||
|
|
* @param text 文本
|
|||
|
|
* @param maxLength 最大长度
|
|||
|
|
*/
|
|||
|
|
export function truncateText(text: string, maxLength: number = 12): string {
|
|||
|
|
if (text.length <= maxLength) return text
|
|||
|
|
return text.slice(0, maxLength) + '...'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 合并小分类为 "Others"
|
|||
|
|
* @param data 数据数组 { label, value, color }
|
|||
|
|
* @param threshold 阈值百分比(默认 3%)
|
|||
|
|
* @param maxCategories 最大分类数(默认 8)
|
|||
|
|
*/
|
|||
|
|
export function mergeSmallCategories(
|
|||
|
|
data: Array<{ label: string; value: number; color?: string }>,
|
|||
|
|
threshold: number = 0.03,
|
|||
|
|
maxCategories: number = 8
|
|||
|
|
) {
|
|||
|
|
const total = data.reduce((sum, item) => sum + item.value, 0)
|
|||
|
|
|
|||
|
|
// 按值降序排序
|
|||
|
|
const sorted = [...data].sort((a, b) => b.value - a.value)
|
|||
|
|
|
|||
|
|
// 分离大分类和小分类
|
|||
|
|
const main: typeof data = []
|
|||
|
|
const others: typeof data = []
|
|||
|
|
|
|||
|
|
sorted.forEach((item) => {
|
|||
|
|
const percentage = item.value / total
|
|||
|
|
if (main.length < maxCategories && percentage >= threshold) {
|
|||
|
|
main.push(item)
|
|||
|
|
} else {
|
|||
|
|
others.push(item)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 如果有小分类,合并为 "Others"
|
|||
|
|
if (others.length > 0) {
|
|||
|
|
const othersValue = others.reduce((sum, item) => sum + item.value, 0)
|
|||
|
|
main.push({
|
|||
|
|
label: '其他',
|
|||
|
|
value: othersValue,
|
|||
|
|
color: '#bbb'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return main
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 数据抽样(用于大数据量场景)
|
|||
|
|
* @param data 数据数组
|
|||
|
|
* @param maxPoints 最大点数
|
|||
|
|
*/
|
|||
|
|
export function decimateData<T>(data: T[], maxPoints: number = 100): T[] {
|
|||
|
|
if (data.length <= maxPoints) return data
|
|||
|
|
|
|||
|
|
const step = Math.ceil(data.length / maxPoints)
|
|||
|
|
return data.filter((_, index) => index % step === 0)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检测是否为移动端
|
|||
|
|
*/
|
|||
|
|
export function isMobile(): boolean {
|
|||
|
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 根据屏幕宽度调整字体大小
|
|||
|
|
*/
|
|||
|
|
export function getResponsiveFontSize(baseSize: number): number {
|
|||
|
|
const screenWidth = window.innerWidth
|
|||
|
|
if (screenWidth < 375) {
|
|||
|
|
return Math.max(baseSize - 2, 10)
|
|||
|
|
}
|
|||
|
|
return baseSize
|
|||
|
|
}
|