Appearance
二维可视化应用
概述
二维可视化处理平面数据,如地图、热力图、散点图、树图等。
常见应用场景
1. 地图可视化
javascript
import * as d3 from 'd3'
import * as topojson from 'topojson-client'
async function createMap(container) {
const width = 800
const height = 600
const svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height)
// 加载地图数据
const world = await d3.json('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json')
const countries = topojson.feature(world, world.objects.countries)
// 投影
const projection = d3.geoNaturalEarth1()
.fitSize([width, height], countries)
const path = d3.geoPath().projection(projection)
// 绘制地图
svg.append('g')
.selectAll('path')
.data(countries.features)
.enter()
.append('path')
.attr('d', path)
.attr('fill', '#f0f0f0')
.attr('stroke', '#999')
.attr('stroke-width', 0.5)
.on('mouseover', function() {
d3.select(this).attr('fill', '#1890ff')
})
.on('mouseout', function() {
d3.select(this).attr('fill', '#f0f0f0')
})
}2. 热力图
javascript
class Heatmap {
constructor(container, data, options = {}) {
this.container = container
this.data = data
this.options = {
width: options.width || 800,
height: options.height || 600,
radius: options.radius || 25,
...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() {
const { width, height, radius } = this.options
this.ctx.clearRect(0, 0, width, height)
// 创建热力图
this.data.forEach(point => {
const gradient = this.ctx.createRadialGradient(
point.x, point.y, 0,
point.x, point.y, radius
)
gradient.addColorStop(0, `rgba(255, 0, 0, ${point.value})`)
gradient.addColorStop(1, 'rgba(255, 0, 0, 0)')
this.ctx.fillStyle = gradient
this.ctx.fillRect(
point.x - radius,
point.y - radius,
radius * 2,
radius * 2
)
})
// 应用颜色映射
const imageData = this.ctx.getImageData(0, 0, width, height)
const pixels = imageData.data
for (let i = 0; i < pixels.length; i += 4) {
const alpha = pixels[i + 3]
if (alpha > 0) {
const color = this.getColor(alpha / 255)
pixels[i] = color.r
pixels[i + 1] = color.g
pixels[i + 2] = color.b
}
}
this.ctx.putImageData(imageData, 0, 0)
}
getColor(value) {
const colors = [
{ r: 0, g: 0, b: 255 }, // 蓝
{ r: 0, g: 255, b: 255 }, // 青
{ r: 0, g: 255, b: 0 }, // 绿
{ r: 255, g: 255, b: 0 }, // 黄
{ r: 255, g: 0, b: 0 } // 红
]
const index = Math.min(Math.floor(value * (colors.length - 1)), colors.length - 2)
const t = (value * (colors.length - 1)) - index
return {
r: colors[index].r + t * (colors[index + 1].r - colors[index].r),
g: colors[index].g + t * (colors[index + 1].g - colors[index].g),
b: colors[index].b + t * (colors[index + 1].b - colors[index].b)
}
}
}3. 散点图
javascript
import * as d3 from 'd3'
function createScatterPlot(data, container) {
const margin = { top: 20, right: 20, bottom: 30, left: 50 }
const width = 800 - margin.left - margin.right
const height = 600 - 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.scaleLinear()
.domain([0, d3.max(data, d => d.x)])
.range([0, width])
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.y)])
.range([height, 0])
const color = d3.scaleOrdinal(d3.schemeCategory10)
// 坐标轴
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x))
svg.append('g')
.call(d3.axisLeft(y))
// 散点
svg.selectAll('.dot')
.data(data)
.enter()
.append('circle')
.attr('class', 'dot')
.attr('cx', d => x(d.x))
.attr('cy', d => y(d.y))
.attr('r', 5)
.attr('fill', d => color(d.category))
.on('mouseover', function(event, d) {
d3.select(this)
.transition()
.duration(200)
.attr('r', 8)
})
.on('mouseout', function() {
d3.select(this)
.transition()
.duration(200)
.attr('r', 5)
})
}4. 树图
javascript
import * as d3 from 'd3'
function createTreeMap(data, container) {
const width = 800
const height = 600
const svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height)
const treemap = d3.treemap()
.size([width, height])
.padding(1)
.round(true)
const root = d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value)
treemap(root)
const color = d3.scaleOrdinal(d3.schemeCategory10)
const leaf = svg.selectAll('g')
.data(root.leaves())
.enter()
.append('g')
.attr('transform', d => `translate(${d.x0},${d.y0})`)
leaf.append('rect')
.attr('width', d => d.x1 - d.x0)
.attr('height', d => d.y1 - d.y0)
.attr('fill', d => color(d.parent.data.name))
.attr('stroke', '#fff')
leaf.append('text')
.attr('x', 4)
.attr('y', 14)
.text(d => d.data.name)
.attr('font-size', '12px')
.attr('fill', '#fff')
}数据可视化库
ECharts
javascript
import * as echarts from 'echarts'
const chart = echarts.init(document.getElementById('chart'))
chart.setOption({
title: {
text: '示例图表'
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}]
})最佳实践
1. 数据处理
- 数据清洗和转换
- 异常值处理
- 数据聚合
2. 视觉设计
- 合理的颜色选择
- 清晰的图例说明
- 适当的标注
3. 交互体验
- 缩放和平移
- 数据筛选
- 详情展示