项目概述
本项目是一个基于Electron框架开发的简单统计组件应用,为鸿蒙PC平台提供基础的数据统计和分析功能。这是Electron初学者学习数据处理和分析的理想示例,展示了如何在桌面应用中实现统计计算、数据可视化和交互操作。

功能特点
- 基础统计计算:支持均值、中位数、标准差、最大值、最小值等基础统计指标
- 数据分组分析:支持数据分组和分类统计功能
- 直方图展示:使用Canvas API绘制数据分布直方图
- 箱线图分析:支持数据分布的箱线图可视化
- 相关性分析:计算和可视化数据之间的相关性
- 数据导入导出:支持CSV格式数据导入和统计报告导出
- 实时计算:数据更新时自动重新计算统计指标
- 响应式设计:适配不同窗口大小的界面布局
- 鸿蒙PC适配:针对鸿蒙PC平台优化的用户体验
技术架构
核心架构
- 主进程 (Main Process) :由
main.js实现,负责应用程序生命周期管理和窗口创建 - 渲染进程 (Renderer Process) :由
renderer.js实现,负责统计计算、数据可视化和用户交互 - 预加载脚本 (Preload Script) :由
preload.js实现,提供安全的进程间通信桥接
文件结构
99-simple-stats/
├── main.js # 主进程文件,应用入口
├── preload.js # 预加载脚本,进程通信
├── index.html # 应用界面
├── style.css # 样式文件
├── renderer.js # 渲染进程逻辑
├── stats.js # 统计计算核心模块
└── README.md # 项目说明文档
核心代码解析
主进程 (main.js)
主进程负责创建和管理应用窗口,处理文件系统操作:
javascript
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow() {
// 创建浏览器窗口并配置安全选项
const win = new BrowserWindow({
width: 800,
height: 600,
title: '简单统计组件 - Electron for 鸿蒙PC项目实战案例',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
sandbox: true
}
});
win.loadFile('index.html');
}
// 应用生命周期管理
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
统计计算模块 (stats.js)
统计计算模块提供了核心的统计分析功能:
javascript
class SimpleStats {
constructor() {}
// 计算均值
static mean(data) {
if (!data || data.length === 0) return 0;
const sum = data.reduce((acc, val) => acc + val, 0);
return sum / data.length;
}
// 计算中位数
static median(data) {
if (!data || data.length === 0) return 0;
const sortedData = [...data].sort((a, b) => a - b);
const mid = Math.floor(sortedData.length / 2);
if (sortedData.length % 2 === 0) {
return (sortedData[mid - 1] + sortedData[mid]) / 2;
} else {
return sortedData[mid];
}
}
// 计算标准差
static standardDeviation(data) {
if (!data || data.length === 0) return 0;
const avg = this.mean(data);
const squareDiffs = data.map(value => {
const diff = value - avg;
return diff * diff;
});
const avgSquareDiff = this.mean(squareDiffs);
return Math.sqrt(avgSquareDiff);
}
// 计算最小值
static min(data) {
if (!data || data.length === 0) return 0;
return Math.min(...data);
}
// 计算最大值
static max(data) {
if (!data || data.length === 0) return 0;
return Math.max(...data);
}
// 计算四分位数
static quartiles(data) {
if (!data || data.length === 0) return { q1: 0, q2: 0, q3: 0 };
const sortedData = [...data].sort((a, b) => a - b);
const n = sortedData.length;
const q1Index = Math.floor(n * 0.25);
const q2Index = Math.floor(n * 0.5);
const q3Index = Math.floor(n * 0.75);
return {
q1: sortedData[q1Index],
q2: sortedData[q2Index],
q3: sortedData[q3Index]
};
}
// 计算相关性(皮尔逊相关系数)
static correlation(x, y) {
if (!x || !y || x.length !== y.length || x.length === 0) return 0;
const n = x.length;
const sumX = x.reduce((acc, val) => acc + val, 0);
const sumY = y.reduce((acc, val) => acc + val, 0);
const sumXY = x.reduce((acc, val, i) => acc + val * y[i], 0);
const sumX2 = x.reduce((acc, val) => acc + val * val, 0);
const sumY2 = y.reduce((acc, val) => acc + val * val, 0);
const numerator = n * sumXY - sumX * sumY;
const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
return denominator === 0 ? 0 : numerator / denominator;
}
// 创建直方图数据
static histogram(data, binCount = 10) {
if (!data || data.length === 0) return { bins: [], counts: [] };
const min = this.min(data);
const max = this.max(data);
const binWidth = (max - min) / binCount;
const bins = [];
const counts = new Array(binCount).fill(0);
// 创建分箱边界
for (let i = 0; i <= binCount; i++) {
bins.push(min + i * binWidth);
}
// 统计每个分箱的数据数量
data.forEach(value => {
if (value === max) {
counts[binCount - 1]++;
} else {
const binIndex = Math.floor((value - min) / binWidth);
counts[binIndex]++;
}
});
return { bins, counts };
}
// 生成箱线图数据
static boxPlotData(data) {
if (!data || data.length === 0) return null;
const min = this.min(data);
const max = this.max(data);
const { q1, q2, q3 } = this.quartiles(data);
// 计算四分位距
const iqr = q3 - q1;
// 计算异常值边界
const lowerBound = q1 - 1.5 * iqr;
const upperBound = q3 + 1.5 * iqr;
// 找出异常值
const outliers = data.filter(value =>
value < lowerBound || value > upperBound
);
// 找出非异常值的最小值和最大值
const nonOutliers = data.filter(value =>
value >= lowerBound && value <= upperBound
);
const whiskerMin = nonOutliers.length > 0 ? Math.min(...nonOutliers) : min;
const whiskerMax = nonOutliers.length > 0 ? Math.max(...nonOutliers) : max;
return {
min,
max,
q1,
q2,
q3,
iqr,
lowerBound,
upperBound,
whiskerMin,
whiskerMax,
outliers
};
}
// 数据分组
static groupBy(data, groupByKey) {
if (!data || !Array.isArray(data) || data.length === 0) return {};
return data.reduce((groups, item) => {
const key = item[groupByKey];
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
return groups;
}, {});
}
}
module.exports = SimpleStats;
渲染进程 (renderer.js)
渲染进程负责UI交互和数据可视化:
javascript
// 引入统计计算模块
const SimpleStats = require('./stats.js');
class StatsVisualizer {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.data = [];
this.init();
}
// 初始化UI
init() {
// 创建数据输入区域
this.createDataInputArea();
// 创建统计结果展示区域
this.createStatsDisplayArea();
// 创建可视化区域
this.createVisualizationArea();
// 生成示例数据
this.generateSampleData();
// 初始计算和可视化
this.updateStats();
}
// 生成示例数据
generateSampleData() {
// 生成50个正态分布的随机数
const normalData = [];
for (let i = 0; i < 50; i++) {
// Box-Muller变换生成正态分布
let u = 0, v = 0;
while(u === 0) u = Math.random();
while(v === 0) v = Math.random();
const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
normalData.push(50 + z * 10); // 均值50,标准差10
}
this.data = normalData;
this.updateDataInput();
}
// 更新数据输入区域
updateDataInput() {
const dataInput = document.getElementById('dataInput');
dataInput.value = this.data.join(', ');
}
// 从输入区域读取数据
readDataFromInput() {
const dataInput = document.getElementById('dataInput');
const rawData = dataInput.value.split(',').map(item => parseFloat(item.trim())).filter(item => !isNaN(item));
if (rawData.length > 0) {
this.data = rawData;
return true;
}
return false;
}
// 更新统计结果
updateStats() {
if (this.data.length === 0) return;
// 计算统计指标
const mean = SimpleStats.mean(this.data);
const median = SimpleStats.median(this.data);
const stdDev = SimpleStats.standardDeviation(this.data);
const min = SimpleStats.min(this.data);
const max = SimpleStats.max(this.data);
const { q1, q2, q3 } = SimpleStats.quartiles(this.data);
// 更新统计结果显示
document.getElementById('statsMean').textContent = mean.toFixed(2);
document.getElementById('statsMedian').textContent = median.toFixed(2);
document.getElementById('statsStdDev').textContent = stdDev.toFixed(2);
document.getElementById('statsMin').textContent = min.toFixed(2);
document.getElementById('statsMax').textContent = max.toFixed(2);
document.getElementById('statsQ1').textContent = q1.toFixed(2);
document.getElementById('statsQ2').textContent = q2.toFixed(2);
document.getElementById('statsQ3').textContent = q3.toFixed(2);
// 更新可视化
this.renderHistogram();
this.renderBoxPlot();
}
// 渲染直方图
renderHistogram() {
const canvas = document.getElementById('histogramCanvas');
const ctx = canvas.getContext('2d');
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (this.data.length === 0) return;
// 获取直方图数据
const binCount = parseInt(document.getElementById('binCountInput').value) || 10;
const { bins, counts } = SimpleStats.histogram(this.data, binCount);
// 设置绘图区域
const margin = { top: 30, right: 30, bottom: 50, left: 50 };
const width = canvas.width - margin.left - margin.right;
const height = canvas.height - margin.top - margin.bottom;
// 计算比例
const xScale = width / binCount;
const maxCount = Math.max(...counts);
const yScale = height / maxCount;
// 绘制直方图标题
ctx.font = '16px Arial';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
ctx.fillText('数据分布直方图', canvas.width / 2, 20);
// 绘制坐标轴标签
ctx.font = '12px Arial';
ctx.fillText('数据值', canvas.width / 2, canvas.height - 10);
// Y轴标签(旋转)
ctx.save();
ctx.translate(10, canvas.height / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('频数', 0, 0);
ctx.restore();
// 绘制直方图柱
ctx.fillStyle = '#3498db';
for (let i = 0; i < binCount; i++) {
const barWidth = xScale - 2; // 留出间隙
const barHeight = counts[i] * yScale;
const x = margin.left + i * xScale;
const y = canvas.height - margin.bottom - barHeight;
ctx.fillRect(x, y, barWidth, barHeight);
// 绘制刻度标签
ctx.fillStyle = '#666';
ctx.textAlign = 'center';
ctx.fillText(bins[i].toFixed(1), x + xScale / 2, canvas.height - margin.bottom + 15);
}
}
// 渲染箱线图
renderBoxPlot() {
const canvas = document.getElementById('boxPlotCanvas');
const ctx = canvas.getContext('2d');
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (this.data.length === 0) return;
// 获取箱线图数据
const boxData = SimpleStats.boxPlotData(this.data);
// 设置绘图区域
const margin = { top: 30, right: 30, bottom: 30, left: 50 };
const width = canvas.width - margin.left - margin.right;
const height = canvas.height - margin.top - margin.bottom;
// 计算Y轴比例
const dataMin = Math.min(boxData.min, boxData.whiskerMin);
const dataMax = Math.max(boxData.max, boxData.whiskerMax);
const dataRange = dataMax - dataMin;
const yScale = height / dataRange;
const yOffset = y => canvas.height - margin.bottom - (y - dataMin) * yScale;
// 绘制箱线图标题
ctx.font = '16px Arial';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
ctx.fillText('数据分布箱线图', canvas.width / 2, 20);
// 绘制Y轴
ctx.beginPath();
ctx.moveTo(margin.left, margin.top);
ctx.lineTo(margin.left, canvas.height - margin.bottom);
ctx.strokeStyle = '#333';
ctx.stroke();
// 绘制Y轴刻度
for (let i = 0; i <= 5; i++) {
const yValue = dataMin + (dataRange * i / 5);
const y = yOffset(yValue);
// 刻度线
ctx.beginPath();
ctx.moveTo(margin.left - 5, y);
ctx.lineTo(margin.left, y);
ctx.stroke();
// 刻度标签
ctx.fillStyle = '#666';
ctx.textAlign = 'right';
ctx.fillText(yValue.toFixed(1), margin.left - 10, y + 5);
}
// 箱子宽度
const boxWidth = 100;
const boxX = canvas.width / 2 - boxWidth / 2;
// 绘制箱子
ctx.beginPath();
ctx.rect(boxX, yOffset(boxData.q3), boxWidth, yOffset(boxData.q1) - yOffset(boxData.q3));
ctx.fillStyle = 'rgba(231, 76, 60, 0.2)';
ctx.fill();
ctx.strokeStyle = '#e74c3c';
ctx.stroke();
// 绘制中位数线
ctx.beginPath();
ctx.moveTo(boxX, yOffset(boxData.q2));
ctx.lineTo(boxX + boxWidth, yOffset(boxData.q2));
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 2;
ctx.stroke();
ctx.lineWidth = 1;
// 绘制上下须线
ctx.beginPath();
// 上须线
ctx.moveTo(boxX + boxWidth / 2, yOffset(boxData.q3));
ctx.lineTo(boxX + boxWidth / 2, yOffset(boxData.whiskerMax));
// 下须线
ctx.moveTo(boxX + boxWidth / 2, yOffset(boxData.q1));
ctx.lineTo(boxX + boxWidth / 2, yOffset(boxData.whiskerMin));
ctx.strokeStyle = '#e74c3c';
ctx.stroke();
// 绘制须线端点
ctx.beginPath();
// 上须线端点
ctx.moveTo(boxX + boxWidth / 2 - 10, yOffset(boxData.whiskerMax));
ctx.lineTo(boxX + boxWidth / 2 + 10, yOffset(boxData.whiskerMax));
// 下须线端点
ctx.moveTo(boxX + boxWidth / 2 - 10, yOffset(boxData.whiskerMin));
ctx.lineTo(boxX + boxWidth / 2 + 10, yOffset(boxData.whiskerMin));
ctx.strokeStyle = '#e74c3c';
ctx.stroke();
// 绘制异常值
ctx.fillStyle = '#c0392b';
boxData.outliers.forEach(value => {
ctx.beginPath();
ctx.arc(boxX + boxWidth / 2, yOffset(value), 5, 0, 2 * Math.PI);
ctx.fill();
});
}
// 创建UI组件
createDataInputArea() {
const inputArea = document.createElement('div');
inputArea.className = 'input-area';
inputArea.innerHTML = `
<h3>数据输入</h3>
<textarea id="dataInput" rows="3" placeholder="请输入数据,用逗号分隔..."></textarea>
<div class="button-group">
<button id="updateDataButton">更新数据</button>
<button id="generateRandomDataButton">生成随机数据</button>
</div>
`;
this.container.appendChild(inputArea);
// 绑定事件
document.getElementById('updateDataButton').addEventListener('click', () => {
if (this.readDataFromInput()) {
this.updateStats();
}
});
document.getElementById('generateRandomDataButton').addEventListener('click', () => {
this.generateSampleData();
this.updateStats();
});
}
createStatsDisplayArea() {
const statsArea = document.createElement('div');
statsArea.className = 'stats-area';
statsArea.innerHTML = `
<h3>统计结果</h3>
<div class="stats-grid">
<div class="stat-item"><label>均值:</label> <span id="statsMean">0.00</span></div>
<div class="stat-item"><label>中位数:</label> <span id="statsMedian">0.00</span></div>
<div class="stat-item"><label>标准差:</label> <span id="statsStdDev">0.00</span></div>
<div class="stat-item"><label>最小值:</label> <span id="statsMin">0.00</span></div>
<div class="stat-item"><label>最大值:</label> <span id="statsMax">0.00</span></div>
<div class="stat-item"><label>第一四分位数 (Q1):</label> <span id="statsQ1">0.00</span></div>
<div class="stat-item"><label>第二四分位数 (Q2):</label> <span id="statsQ2">0.00</span></div>
<div class="stat-item"><label>第三四分位数 (Q3):</label> <span id="statsQ3">0.00</span></div>
</div>
`;
this.container.appendChild(statsArea);
}
createVisualizationArea() {
const vizArea = document.createElement('div');
vizArea.className = 'visualization-area';
vizArea.innerHTML = `
<h3>数据可视化</h3>
<div class="histogram-container">
<div class="histogram-controls">
<label for="binCountInput">直方图分箱数:</label>
<input type="number" id="binCountInput" min="2" max="50" value="10">
<button id="updateHistogramButton">更新</button>
</div>
<canvas id="histogramCanvas" width="500" height="300"></canvas>
</div>
<div class="boxplot-container">
<canvas id="boxPlotCanvas" width="500" height="300"></canvas>
</div>
`;
this.container.appendChild(vizArea);
// 绑定事件
document.getElementById('updateHistogramButton').addEventListener('click', () => {
this.renderHistogram();
});
}
}
// 应用初始化
window.addEventListener('DOMContentLoaded', () => {
const statsVisualizer = new StatsVisualizer('appContainer');
});
如何运行
- 克隆本项目
- 安装依赖:
npm install - 启动应用:
npm start
鸿蒙PC适配改造指南
1. 环境准备
-
系统要求:Windows 10/11、8GB RAM以上、20GB可用空间
-
工具安装 :
DevEco Studio 5.0+(安装鸿蒙SDK API 20+)
-
Node.js 18.x+
2. 获取Electron鸿蒙编译产物
-
下载Electron 34+版本的Release包(.zip格式)
-
解压到项目目录,确认
electron/libs/arm64-v8a/下包含核心.so库
3. 部署应用代码
将Electron应用代码按以下目录结构放置:

plaintext
web_engine/src/main/resources/resfile/resources/app/
├── main.js
├── package.json
└── src/
├── index.html
├── preload.js
├── renderer.js
└── style.css
4. 配置与运行
-
打开项目:在DevEco Studio中打开ohos_hap目录
-
配置签名 :
进入File → Project Structure → Signing Configs
-
自动生成调试签名或导入已有签名
-
连接设备 :
启用鸿蒙设备开发者模式和USB调试
-
通过USB Type-C连接电脑
-
编译运行:点击Run按钮或按Shift+F10
5. 验证检查项
-
✅ 应用窗口正常显示
-
✅ 窗口大小可调整,响应式布局生效
-
✅ 控制台无"SysCap不匹配"或"找不到.so文件"错误
-
✅ 动画效果正常播放
跨平台兼容性
| 平台 | 适配策略 | 特殊处理 |
|---|---|---|
| Windows | 标准Electron运行 | 无特殊配置 |
| macOS | 标准Electron运行 | 保留dock图标激活逻辑 |
| Linux | 标准Electron运行 | 确保系统依赖库完整 |
| 鸿蒙PC | 通过Electron鸿蒙适配层 | 禁用硬件加速,使用特定目录结构 |
鸿蒙开发调试技巧
1. 日志查看
在DevEco Studio的Log面板中过滤"Electron"关键词,查看应用运行日志和错误信息。
2. 常见问题解决
-
"SysCap不匹配"错误:检查module.json5中的reqSysCapabilities,只保留必要系统能力
-
"找不到.so文件"错误:确认arm64-v8a目录下四个核心库文件完整
-
窗口不显示:在main.js中添加app.disableHardwareAcceleration()
-
动画卡顿:简化CSS动画效果,减少重绘频率