Appearance
一维可视化应用
概述
一维可视化主要处理线性数据,如时间序列、进度条、音频波形等。
常见应用场景
1. 时间轴
javascript
class Timeline {
constructor(container, options = {}) {
this.container = container
this.options = {
width: options.width || 800,
height: options.height || 100,
startTime: options.startTime || 0,
endTime: options.endTime || 100,
...options
}
this.canvas = this.createCanvas()
this.ctx = this.canvas.getContext('2d')
}
createCanvas() {
const canvas = document.createElement('canvas')
canvas.width = this.options.width
canvas.height = this.options.height
this.container.appendChild(canvas)
return canvas
}
render(events) {
const { width, height, startTime, endTime } = this.options
const duration = endTime - startTime
const scale = width / duration
this.ctx.clearRect(0, 0, width, height)
// 绘制时间轴
this.ctx.strokeStyle = '#ddd'
this.ctx.beginPath()
this.ctx.moveTo(0, height / 2)
this.ctx.lineTo(width, height / 2)
this.ctx.stroke()
// 绘制事件
events.forEach(event => {
const x = (event.time - startTime) * scale
this.ctx.fillStyle = event.color || '#1890ff'
this.ctx.beginPath()
this.ctx.arc(x, height / 2, 5, 0, Math.PI * 2)
this.ctx.fill()
})
}
}2. 进度条
vue
<template>
<div class="progress-container">
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: progress + '%' }"
></div>
</div>
<div class="progress-text">{{ progress }}%</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
value: { type: Number, default: 0 },
max: { type: Number, default: 100 }
})
const progress = ref(0)
watch(() => props.value, (val) => {
progress.value = Math.round((val / props.max) * 100)
}, { immediate: true })
</script>
<style scoped>
.progress-container {
display: flex;
align-items: center;
gap: 12px;
}
.progress-bar {
flex: 1;
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #1890ff, #40a9ff);
transition: width 0.3s ease;
}
.progress-text {
min-width: 50px;
text-align: right;
}
</style>3. 音频波形
javascript
class AudioWaveform {
constructor(canvas) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.audioContext = new (window.AudioContext || window.webkitAudioContext)()
this.analyser = this.audioContext.createAnalyser()
}
async loadAudio(url) {
const response = await fetch(url)
const arrayBuffer = await response.arrayBuffer()
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer)
return audioBuffer
}
drawWaveform(audioBuffer) {
const data = audioBuffer.getChannelData(0)
const step = Math.ceil(data.length / this.canvas.width)
const amp = this.canvas.height / 2
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.ctx.beginPath()
this.ctx.moveTo(0, amp)
for (let i = 0; i < this.canvas.width; i++) {
let min = 1.0
let max = -1.0
for (let j = 0; j < step; j++) {
const datum = data[(i * step) + j]
if (datum < min) min = datum
if (datum > max) max = datum
}
this.ctx.lineTo(i, (1 + min) * amp)
this.ctx.lineTo(i, (1 + max) * amp)
}
this.ctx.strokeStyle = '#1890ff'
this.ctx.stroke()
}
visualize(stream) {
const source = this.audioContext.createMediaStreamSource(stream)
source.connect(this.analyser)
this.analyser.fftSize = 256
const bufferLength = this.analyser.frequencyBinCount
const dataArray = new Uint8Array(bufferLength)
const draw = () => {
requestAnimationFrame(draw)
this.analyser.getByteFrequencyData(dataArray)
this.ctx.fillStyle = 'rgb(0, 0, 0)'
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
const barWidth = (this.canvas.width / bufferLength) * 2.5
let barHeight
let x = 0
for (let i = 0; i < bufferLength; i++) {
barHeight = dataArray[i] / 2
this.ctx.fillStyle = `rgb(${barHeight + 100}, 50, 50)`
this.ctx.fillRect(x, this.canvas.height - barHeight, barWidth, barHeight)
x += barWidth + 1
}
}
draw()
}
}数据可视化库
1. D3.js 线性图表
javascript
import * as d3 from 'd3'
function createLineChart(data, container) {
const margin = { top: 20, right: 20, bottom: 30, left: 50 }
const width = 800 - margin.left - margin.right
const height = 400 - margin.top - margin.bottom
const svg = d3.select(container)
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`)
const x = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, width])
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height, 0])
const line = d3.line()
.x(d => x(d.date))
.y(d => y(d.value))
.curve(d3.curveMonotoneX)
svg.append('g')
.attr('class', 'x axis')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x))
svg.append('g')
.attr('class', 'y axis')
.call(d3.axisLeft(y))
svg.append('path')
.datum(data)
.attr('class', 'line')
.attr('d', line)
.attr('fill', 'none')
.attr('stroke', '#1890ff')
.attr('stroke-width', 2)
}2. Chart.js 线性图表
javascript
import Chart from 'chart.js/auto'
const ctx = document.getElementById('myChart')
new Chart(ctx, {
type: 'line',
data: {
labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
datasets: [{
label: '销售额',
data: [12, 19, 3, 5, 2, 3],
borderColor: '#1890ff',
backgroundColor: 'rgba(24, 144, 255, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'top'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
})最佳实践
1. 性能优化
- 使用 Canvas 而非 SVG 处理大量数据
- 实现数据采样和聚合
- 使用 Web Worker 进行数据处理
2. 交互设计
- 支持缩放和平移
- 提供数据点提示
- 支持数据筛选
3. 响应式设计
- 自适应容器大小
- 移动端优化
- 支持触摸操作