您现在的位置是:网站首页 > 数据转换与预处理文章详情
数据转换与预处理
陈川
【
ECharts
】
50011人已围观
6236字
数据转换与预处理是数据可视化中不可或缺的环节,尤其在ECharts这类工具中,原始数据往往需要经过清洗、格式化或聚合才能适配图表需求。从简单的数据类型转换到复杂的聚合计算,每一步都可能直接影响最终呈现效果。
数据格式标准化
ECharts大部分图表要求数据为特定格式。例如折线图需要xAxis
和yAxis
的数值数组,而饼图需要{value, name}
的对象数组。原始数据可能是CSV字符串或嵌套JSON,需要进行扁平化处理:
// 原始CSV数据
const csvData = `日期,销售额
2023-01,1500
2023-02,3200`;
// 转换为ECharts需要的格式
function parseCSV(csv) {
const lines = csv.split('\n');
const headers = lines[0].split(',');
return lines.slice(1).map(line => {
const values = line.split(',');
return {
[headers[0]]: values[0],
[headers[1]]: parseFloat(values[1])
};
});
}
const seriesData = parseCSV(csvData).map(item => item['销售额']);
const xAxisData = parseCSV(csvData).map(item => item['日期']);
时间序列处理
处理时间数据时经常需要转换时间格式。ECharts的时间轴(xAxis.type='time')要求时间戳或ISO格式字符串:
const rawData = [
{ date: '01/15/2023', value: 42 },
{ date: '02/20/2023', value: 78 }
];
// 使用Day.js进行日期格式化
import dayjs from 'dayjs';
const processedData = rawData.map(item => ({
date: dayjs(item.date, 'MM/DD/YYYY').format('YYYY-MM-DD'),
value: item.value
}));
option = {
xAxis: {
type: 'time',
data: processedData.map(d => d.date)
},
series: [{
data: processedData.map(d => d.value)
}]
};
数据聚合与分组
当原始数据过于详细时,需要按维度聚合。比如将每日数据聚合成月平均值:
const dailyData = [
{ date: '2023-01-01', temperature: 12 },
{ date: '2023-01-02', temperature: 14 },
// ...更多数据
{ date: '2023-02-01', temperature: 18 }
];
const monthlyAvg = {};
dailyData.forEach(item => {
const month = item.date.substring(0, 7); // 提取年月
if (!monthlyAvg[month]) {
monthlyAvg[month] = { sum: 0, count: 0 };
}
monthlyAvg[month].sum += item.temperature;
monthlyAvg[month].count += 1;
});
const result = Object.keys(monthlyAvg).map(month => ({
month,
temperature: monthlyAvg[month].sum / monthlyAvg[month].count
}));
缺失值处理
实际数据常存在缺失值,ECharts中可用以下策略处理:
- 线性插值法填补缺失数据点:
function interpolateMissing(data, key) {
return data.map((current, i) => {
if (current[key] !== null) return current;
// 找到前一个有效值
let prev = i - 1;
while (prev >= 0 && data[prev][key] === null) prev--;
// 找到后一个有效值
let next = i + 1;
while (next < data.length && data[next][key] === null) next++;
// 两端插值
if (prev < 0) return { ...current, [key]: data[next][key] };
if (next >= data.length) return { ...current, [key]: data[prev][key] };
// 线性插值
const ratio = (i - prev) / (next - prev);
const val = data[prev][key] + (data[next][key] - data[prev][key]) * ratio;
return { ...current, [key]: val };
});
}
- 使用ECharts的视觉映射突出显示缺失点:
series: [{
data: [
{ value: 10 },
{ value: null, itemStyle: { color: '#ff0000' } },
{ value: 20 }
]
}]
数据归一化
当不同系列的数据量级差异较大时,需要进行归一化处理:
const dataA = [120, 132, 101, 134, 90];
const dataB = [0.12, 0.19, 0.13, 0.14, 0.17];
function minMaxNormalize(arr) {
const min = Math.min(...arr);
const max = Math.max(...arr);
return arr.map(v => (v - min) / (max - min));
}
option = {
yAxis: [{ type: 'value' }, { type: 'value' }],
series: [
{ data: dataA, yAxisIndex: 0 },
{ data: minMaxNormalize(dataB), yAxisIndex: 1 }
]
};
地理数据编码
处理地图数据时,常需要将地理名称转换为坐标:
// 使用阿里云地理编码API示例
async function geoEncode(address) {
const response = await fetch(
`https://geocode-api.aliyun.com/geocode?address=${encodeURIComponent(address)}`
);
const { lon, lat } = await response.json();
return [parseFloat(lon), parseFloat(lat)];
}
const cities = ['北京', '上海', '广州'];
const coords = await Promise.all(cities.map(geoEncode));
option = {
series: [{
type: 'scatter',
coordinateSystem: 'geo',
data: coords.map((coord, i) => ({
name: cities[i],
value: [...coord, Math.random() * 100] // [经度, 纬度, 数值]
}))
}]
};
树形数据转换
ECharts的树图需要特定层级结构,需将扁平数据转换为嵌套结构:
const flatData = [
{ id: 1, name: '总部', parentId: null },
{ id: 2, name: '华东分部', parentId: 1 },
{ id: 3, name: '杭州办事处', parentId: 2 }
];
function buildTree(data, rootId) {
const nodeMap = {};
data.forEach(item => nodeMap[item.id] = { ...item, children: [] });
const tree = [];
data.forEach(item => {
if (item.parentId === rootId) {
tree.push(nodeMap[item.id]);
} else if (nodeMap[item.parentId]) {
nodeMap[item.parentId].children.push(nodeMap[item.id]);
}
});
return tree;
}
option = {
series: [{
type: 'tree',
data: buildTree(flatData, null)
}]
};
动态数据更新
对于实时数据流,需要进行滑动窗口处理:
class DataWindow {
constructor(size) {
this.size = size;
this.buffer = [];
}
push(data) {
this.buffer.push(data);
if (this.buffer.length > this.size) {
this.buffer.shift();
}
return this.buffer;
}
}
const tempWindow = new DataWindow(60); // 保留最近60个数据点
// 模拟实时数据推送
setInterval(() => {
const newTemp = Math.random() * 30 + 10;
const seriesData = tempWindow.push(newTemp);
myChart.setOption({
series: [{
data: seriesData
}]
});
}, 1000);
多维数据透视
处理多维数据时,可能需要转换为ECharts支持的平行坐标系格式:
const rawData = [
{ cpu: 0.12, memory: 0.45, network: 1.2, disk: 0.8 },
{ cpu: 0.23, memory: 0.67, network: 0.9, disk: 1.1 }
];
const dimensions = ['cpu', 'memory', 'network', 'disk'];
const parallelData = rawData.map(item =>
dimensions.map(dim => item[dim])
);
option = {
parallelAxis: dimensions.map(dim => ({ dim: dimensions.indexOf(dim), name: dim })),
series: {
type: 'parallel',
data: parallelData
}
};
数据采样优化
当数据量过大时,需要进行降采样以提高渲染性能:
function downsample(data, factor, accessor) {
const sampled = [];
for (let i = 0; i < data.length; i += factor) {
const segment = data.slice(i, i + factor);
const avgValue = segment.reduce((sum, d) => sum + accessor(d), 0) / segment.length;
sampled.push({
...data[i],
_value: avgValue // 保留原始数据引用同时存储采样值
});
}
return sampled;
}
const highFreqData = Array.from({ length: 10000 }, (_, i) => ({
timestamp: Date.now() + i * 1000,
value: Math.sin(i / 100) * 50 + 100
}));
const sampledData = downsample(highFreqData, 10, d => d.value);