187 lines
6.4 KiB
TypeScript
187 lines
6.4 KiB
TypeScript
'use client'
|
||
|
||
import React, { useRef, useEffect, useState } from 'react'
|
||
import { createColorMap, ColorMapType } from '@/lib/color-maps'
|
||
import { cn } from '@/lib/utils'
|
||
|
||
interface ColorbarProps {
|
||
colorMapType: ColorMapType
|
||
width?: number
|
||
height?: number
|
||
showLabels?: boolean
|
||
minValue?: number
|
||
maxValue?: number
|
||
unit?: string
|
||
orientation?: 'horizontal' | 'vertical'
|
||
}
|
||
|
||
export function Colorbar({
|
||
colorMapType,
|
||
width = 20,
|
||
height = 20,
|
||
showLabels = true,
|
||
minValue = 0,
|
||
maxValue = 100,
|
||
unit = '',
|
||
orientation = 'horizontal'
|
||
}: ColorbarProps) {
|
||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||
const [isHovered, setIsHovered] = useState(false)
|
||
const [hoverValue, setHoverValue] = useState<number | null>(null)
|
||
|
||
useEffect(() => {
|
||
const canvas = canvasRef.current
|
||
if (!canvas) return
|
||
|
||
const ctx = canvas.getContext('2d')
|
||
if (!ctx) return
|
||
|
||
// 设置canvas尺寸
|
||
canvas.width = width
|
||
canvas.height = height
|
||
|
||
// 获取色标数据
|
||
const colorMap = createColorMap(colorMapType)
|
||
|
||
// 绘制色标
|
||
const imageData = ctx.createImageData(width, height)
|
||
const colorBarThickness = orientation === 'horizontal' ? Math.min(height, 4) : Math.min(width, 4) // 限制颜色带厚度
|
||
|
||
if (orientation === 'horizontal') {
|
||
// 水平方向:从左到右
|
||
const yStart = Math.floor((height - colorBarThickness) / 2)
|
||
for (let x = 0; x < width; x++) {
|
||
const t = x / (width - 1) // 归一化到 0-1
|
||
const colorIndex = Math.floor(t * 255) * 4
|
||
|
||
const r = colorMap[colorIndex]
|
||
const g = colorMap[colorIndex + 1]
|
||
const b = colorMap[colorIndex + 2]
|
||
const a = colorMap[colorIndex + 3]
|
||
|
||
for (let y = yStart; y < yStart + colorBarThickness; y++) {
|
||
const pixelIndex = (y * width + x) * 4
|
||
imageData.data[pixelIndex] = r
|
||
imageData.data[pixelIndex + 1] = g
|
||
imageData.data[pixelIndex + 2] = b
|
||
imageData.data[pixelIndex + 3] = a
|
||
}
|
||
}
|
||
} else {
|
||
// 垂直方向:从下到上
|
||
const xStart = Math.floor((width - colorBarThickness) / 2)
|
||
for (let y = 0; y < height; y++) {
|
||
const t = (height - 1 - y) / (height - 1) // 归一化到 0-1,颠倒方向
|
||
const colorIndex = Math.floor(t * 255) * 4
|
||
|
||
const r = colorMap[colorIndex]
|
||
const g = colorMap[colorIndex + 1]
|
||
const b = colorMap[colorIndex + 2]
|
||
const a = colorMap[colorIndex + 3]
|
||
|
||
for (let x = xStart; x < xStart + colorBarThickness; x++) {
|
||
const pixelIndex = (y * width + x) * 4
|
||
imageData.data[pixelIndex] = r
|
||
imageData.data[pixelIndex + 1] = g
|
||
imageData.data[pixelIndex + 2] = b
|
||
imageData.data[pixelIndex + 3] = a
|
||
}
|
||
}
|
||
}
|
||
|
||
ctx.putImageData(imageData, 0, 0)
|
||
|
||
// 添加边框
|
||
// ctx.strokeStyle = '#666'
|
||
// ctx.lineWidth = 1
|
||
// ctx.strokeRect(0, 0, width, height)
|
||
|
||
}, [colorMapType, width, height, orientation])
|
||
|
||
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||
const canvas = canvasRef.current
|
||
if (!canvas) return
|
||
|
||
const rect = canvas.getBoundingClientRect()
|
||
let t: number
|
||
|
||
if (orientation === 'horizontal') {
|
||
const x = e.clientX - rect.left
|
||
t = x / width
|
||
} else {
|
||
const y = e.clientY - rect.top
|
||
t = (height - y) / height // 垂直方向:从下到上
|
||
}
|
||
|
||
const value = minValue + t * (maxValue - minValue)
|
||
setHoverValue(value)
|
||
setIsHovered(true)
|
||
}
|
||
|
||
const handleMouseLeave = () => {
|
||
setIsHovered(false)
|
||
setHoverValue(null)
|
||
}
|
||
|
||
const formatValue = (value: number) => {
|
||
if (unit === '%') {
|
||
return `${value.toFixed(1)}%`
|
||
} else if (unit === 'dBZ') {
|
||
return `${value.toFixed(1)} dBZ`
|
||
} else if (unit === 'mm/h') {
|
||
return `${value.toFixed(1)} mm/h`
|
||
} else {
|
||
return value.toFixed(1)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className={
|
||
cn(
|
||
"bg-black/20 backdrop-blur-xl m-3 border border-white/10 rounded-xl shadow-2xl overflow-hidden",
|
||
orientation === 'horizontal' ? 'relative' : 'flex items-center'
|
||
)
|
||
}>
|
||
<canvas
|
||
ref={canvasRef}
|
||
className="cursor-crosshair"
|
||
onMouseMove={handleMouseMove}
|
||
onMouseLeave={handleMouseLeave}
|
||
style={{ display: 'block' }}
|
||
/>
|
||
|
||
{showLabels && (
|
||
orientation === 'horizontal' ? (
|
||
<div className="flex justify-between text-xs text-gray-600 mt-1">
|
||
<span>{formatValue(minValue)}</span>
|
||
<span>{formatValue(maxValue)}</span>
|
||
</div>
|
||
) : (
|
||
<div className="flex flex-col justify-between text-xs text-gray-600 ml-2 h-full" style={{ height: `${height}px` }}>
|
||
<span>{formatValue(maxValue)}</span>
|
||
<span>{formatValue(minValue)}</span>
|
||
</div>
|
||
)
|
||
)}
|
||
|
||
{isHovered && hoverValue !== null && (
|
||
<div
|
||
className="absolute bg-black text-white text-xs px-2 py-1 pointer-events-none z-10"
|
||
style={
|
||
orientation === 'horizontal' ? {
|
||
left: `${(hoverValue - minValue) / (maxValue - minValue) * width}px`,
|
||
top: '-30px',
|
||
transform: 'translateX(-50%)'
|
||
} : {
|
||
left: `${width + 10}px`,
|
||
top: `${((maxValue - hoverValue) / (maxValue - minValue)) * height}px`,
|
||
transform: 'translateY(-50%)'
|
||
}
|
||
}
|
||
>
|
||
{formatValue(hoverValue)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|