Compare commits
No commits in common. "2cc9063af45865463ce8e23d65f8eb58832ce297" and "3e5b41792bc802e36941987f184c4153d0f774f8" have entirely different histories.
2cc9063af4
...
3e5b41792b
@ -1,12 +1,7 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v1.0.2 (2026-06-12)
|
||||
- **架构对齐**: 遵循 `base` 包新设计,彻底消灭用户侧 ESM 强依赖。
|
||||
- **组件注册**: 支持 `<DataChart>` 全局组件声明式用法。
|
||||
- **构建优化**: 产物调整为纯 UMD 模式,通过 `globalThis.DataChart` 暴露 API。
|
||||
|
||||
## v1.0.0 (2026-05-29)
|
||||
...
|
||||
|
||||
### 核心特性
|
||||
- **ESM 封装**: 完成对 Chart.js 4.x 的原生 ESM 封装。
|
||||
- **DataChart 组件**: 实现声明式图表初始化与数据驱动更新。
|
||||
|
||||
103
README.md
103
README.md
@ -1,61 +1,80 @@
|
||||
# @apigo.cc/chart API 手册
|
||||
# Chart.js ESM 封装层 (@web/chart)
|
||||
|
||||
基于 Chart.js 4.x 的轻量级图表封装库。内置数据映射(Data Mapping)功能。
|
||||
基于 **Chart.js 4.x** 的原生 ESM 封装,专为数据驱动的 UI 架构设计。支持“用户侧零打包”引入,通过 `DataChart` 类实现高性能、响应式的图表渲染。
|
||||
|
||||
---
|
||||
## 1. 核心设计原则
|
||||
- **原生 ESM**: 源码与产物均为标准 ESM 格式。
|
||||
- **数据驱动**: 状态变更时调用 `update(data)`,严禁频繁销毁/重建实例。
|
||||
- **极致性能**: 自动处理组件生命周期,内置 Chart.js 核心控制器。
|
||||
|
||||
## 1. 引入方式 (UMD 优先)
|
||||
## 2. 快速开始
|
||||
|
||||
在 HTML 中引入脚本即可,组件已内置 Chart.js,无需额外依赖。
|
||||
### 2.1 引入方式
|
||||
推荐通过 `importmap` 或 `loader.js` 引入:
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/chart@1.0.1/dist/chart.min.js"></script>
|
||||
|
||||
<script>
|
||||
// 直接使用全局 DataChart 类
|
||||
const chart = new DataChart(myCanvas, config);
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"@web/chart": "https://cdn.example.com/chart/dist/chart.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
> **ESM 模式说明**:如需在模块化环境中使用,引入 `dist/chart.min.mjs`。
|
||||
### 2.2 基础用法
|
||||
|
||||
---
|
||||
```javascript
|
||||
import { DataChart } from '@web/chart'
|
||||
|
||||
## 2. 基本用法
|
||||
const canvas = document.getElementById('myChart')
|
||||
const chart = new DataChart(canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
|
||||
datasets: [{
|
||||
label: '销售额',
|
||||
data: [12, 19, 3, 5]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true
|
||||
}
|
||||
})
|
||||
|
||||
### 组件化用法 (推荐)
|
||||
直接在 HTML 中使用 `<DataChart>` 标签,通过 `state` 进行数据绑定。
|
||||
|
||||
```html
|
||||
<div $data="{
|
||||
myData: [{ month: 'Jan', sales: 120 }, { month: 'Feb', sales: 190 }],
|
||||
myMap: { labels: 'month', values: 'sales', label: '月销量' }
|
||||
}">
|
||||
<DataChart type="bar" $.state.data="myData" $.state.map="myMap" style="height:300px"></DataChart>
|
||||
</div>
|
||||
// 响应式更新数据
|
||||
chart.update({
|
||||
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
|
||||
datasets: [{
|
||||
label: '销售额',
|
||||
data: [15, 25, 10, 8]
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
### JS 调用方式 (兼容)
|
||||
```html
|
||||
<canvas id="myCanvas" width="400" height="200"></canvas>
|
||||
...
|
||||
---
|
||||
|
||||
## 3. API 参考
|
||||
|
||||
### `new DataChart(canvas, config)`
|
||||
- **`canvas`**: HTMLCanvasElement。
|
||||
- **`config.type`**: `'line'`, `'bar'`, `'pie'`。
|
||||
- **`config.data`**: 原始对象数组。
|
||||
- **`config.map`**: 映射规则。
|
||||
- **`config.options`**: 透传给底层 Chart.js 的原生配置。
|
||||
### `DataChart(canvas: HTMLCanvasElement, config: Object)`
|
||||
构造函数,初始化图表。
|
||||
- `config.type`: 图表类型 (`'line'`, `'bar'`, `'pie'`, `'doughnut'`)。
|
||||
- `config.data`: Chart.js 标准数据格式。
|
||||
- `config.options`: Chart.js 标准配置。
|
||||
|
||||
### 实例方法
|
||||
- **`update(newData)`**: 传入新数组重绘图表。
|
||||
- **`destroy()`**: 销毁实例。
|
||||
### `update(data?: Object)`
|
||||
平滑更新图表。
|
||||
- `data`: (可选) 新的数据对象。若不传,则强制触发现有数据的重绘。
|
||||
|
||||
---
|
||||
### `destroy()`
|
||||
销毁图表实例,释放内存。
|
||||
|
||||
## 开发者提示 (AI 必读)
|
||||
1. **尺寸控制**: Canvas 必须具有明确的高度和宽度。
|
||||
2. **全局变量**: UMD 模式下,`DataChart` 类自动挂载到 `window`。
|
||||
## 4. 支持的图表类型
|
||||
内置集成了以下控制器,无需额外注册:
|
||||
- Line (折线图)
|
||||
- Bar (柱状图)
|
||||
- Pie / Doughnut (饼图/环形图)
|
||||
- Tooltip & Legend 插件
|
||||
|
||||
## 5. 开发与测试
|
||||
- **测试靶场**: `test/index.html` (支持热更新调试)
|
||||
- **单元测试**: `npm run test` (基于 Playwright)
|
||||
- **构建**: `npm run build` (产出 `chart.js` 与 `chart.min.js`)
|
||||
|
||||
5
TEST.md
5
TEST.md
@ -5,14 +5,13 @@
|
||||
- CPU: Host machine
|
||||
- Chart.js version: 4.x
|
||||
|
||||
## 性能基准 (v1.0.2)
|
||||
## 性能基准 (v1.0.0)
|
||||
|
||||
| 指标 | 耗时 (ms) | 备注 |
|
||||
| :--- | :--- | :--- |
|
||||
| **首次渲染 (Line Chart)** | 44.50 | 包含 Chart.js 初始化、双图表渲染及组件初始化测试 |
|
||||
| **首次渲染 (Line Chart)** | 43.80 | 包含 Chart.js 初始化与双图表渲染测试 |
|
||||
| **数据更新 (Smooth Update)** | < 10.00 | 基于 `chart.update()` 的增量更新 |
|
||||
|
||||
## 设计决策
|
||||
1. **Tree Shaking**: 仅内置常用的控制器(Line, Bar, Pie),以控制产物体积。
|
||||
2. **实例复用**: 严格遵循 `chart.update()` 路径,避免频繁创建 Canvas 渲染上下文。
|
||||
3. **组件化支持**: 通过 `globalThis.Component` 注册,实现零打包声明式渲染。
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@apigo.cc/chart",
|
||||
"name": "@web/chart",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@apigo.cc/chart",
|
||||
"name": "@web/chart",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"chart.js": "^4.4.0"
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@apigo.cc/chart",
|
||||
"version": "1.0.2",
|
||||
"name": "@web/chart",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "dist/chart.js",
|
||||
"module": "dist/chart.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"test": "playwright test",
|
||||
"pub": "node scripts/publish.js"
|
||||
"test": "playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
|
||||
@ -4,11 +4,11 @@ export default defineConfig({
|
||||
testDir: './test',
|
||||
testMatch: '**/*.spec.js',
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:8085',
|
||||
baseURL: 'http://localhost:8082',
|
||||
},
|
||||
webServer: {
|
||||
command: 'npx vite --port 8085 --strictPort --host 127.0.0.1',
|
||||
url: 'http://127.0.0.1:8085',
|
||||
command: 'npx vite --port 8082',
|
||||
url: 'http://localhost:8082',
|
||||
timeout: 120000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
try {
|
||||
// 1. 获取最新 tag
|
||||
let tag;
|
||||
try {
|
||||
tag = execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim();
|
||||
} catch (err) {
|
||||
throw new Error('Failed to find git tags. Please make sure the repository has tags (e.g., v1.0.0) before publishing.');
|
||||
}
|
||||
// 去掉 v 前缀
|
||||
const version = tag.startsWith('v') ? tag.slice(1) : tag;
|
||||
|
||||
console.log(`Latest git tag: ${tag}, Version to publish: ${version}`);
|
||||
|
||||
// 2. 读取并更新 package.json
|
||||
const pkgPath = path.join(__dirname, '../package.json');
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
|
||||
// 保持原有名称(如果已经带有 @apigo.cc/ 前缀)或替换前缀
|
||||
if (!pkg.name.startsWith('@apigo.cc/')) {
|
||||
const baseName = pkg.name.includes('/') ? pkg.name.split('/')[1] : pkg.name;
|
||||
pkg.name = `@apigo.cc/${baseName}`;
|
||||
}
|
||||
pkg.version = version;
|
||||
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||||
console.log(`Updated package.json: name=${pkg.name}, version=${pkg.version}`);
|
||||
|
||||
// 3. 构建
|
||||
console.log('Running build...');
|
||||
execSync('npm run build', { stdio: 'inherit', cwd: path.join(__dirname, '..') });
|
||||
|
||||
// 4. 发布
|
||||
console.log('Publishing to npm...');
|
||||
const args = process.argv.slice(2).join(' ');
|
||||
execSync(`npm publish --access public ${args}`, { stdio: 'inherit', cwd: path.join(__dirname, '..') });
|
||||
|
||||
console.log('Publish successful!');
|
||||
} catch (error) {
|
||||
console.error('Publish failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
31
src/index.js
31
src/index.js
@ -76,34 +76,3 @@ export class DataChart {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 注册为全局组件
|
||||
if (typeof globalThis !== 'undefined' && globalThis.Component) {
|
||||
globalThis.Component.register('DataChart', container => {
|
||||
const canvas = container.querySelector('canvas');
|
||||
let dc = null;
|
||||
const init = () => {
|
||||
if (dc) dc.destroy();
|
||||
dc = new DataChart(canvas, {
|
||||
type: container.getAttribute('type') || 'line',
|
||||
data: container.state.data,
|
||||
options: container.state.options,
|
||||
map: container.state.map
|
||||
});
|
||||
container.chartInstance = dc;
|
||||
};
|
||||
|
||||
container.state.__watch('data', (val) => dc ? dc.update(val) : init());
|
||||
container.state.__watch('options', init);
|
||||
container.state.__watch('map', init);
|
||||
|
||||
init();
|
||||
|
||||
container.addEventListener('unload', () => dc && dc.destroy());
|
||||
}, globalThis.Util?.makeDom('<canvas style="width:100%;height:100%"></canvas>'));
|
||||
}
|
||||
|
||||
// 挂载到全局
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
globalThis.DataChart = DataChart;
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"@apigo.cc/chart": "../src/index.js"
|
||||
"@web/chart": "../src/index.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -21,37 +21,13 @@
|
||||
<div class="chart-container">
|
||||
<canvas id="testChart"></canvas>
|
||||
</div>
|
||||
<hr>
|
||||
<h3>Component Test</h3>
|
||||
<div id="componentTestContainer" class="chart-container">
|
||||
<!-- Will inject component here -->
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { DataChart } from '@apigo.cc/chart';
|
||||
|
||||
// Mock globalThis.Component and globalThis.Util for testing if not present
|
||||
if (!globalThis.Component) {
|
||||
globalThis.Component = {
|
||||
register: (name, setup, tpl) => {
|
||||
globalThis._registeredComponents = globalThis._registeredComponents || {};
|
||||
globalThis._registeredComponents[name] = { setup, tpl };
|
||||
}
|
||||
};
|
||||
globalThis.Util = {
|
||||
makeDom: (html) => {
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = html;
|
||||
return template.content.firstChild;
|
||||
}
|
||||
};
|
||||
}
|
||||
import { DataChart } from '@web/chart';
|
||||
|
||||
async function runTests() {
|
||||
const results = document.getElementById('results');
|
||||
try {
|
||||
const t0 = performance.now();
|
||||
// ... (existing JS API tests)
|
||||
const canvas = document.getElementById('testChart');
|
||||
const dc = new DataChart(canvas, {
|
||||
type: 'line',
|
||||
@ -79,26 +55,18 @@
|
||||
});
|
||||
|
||||
if (dcMapped.chart.data.labels[0] !== 'Alice') throw new Error('Mapping failed: labels');
|
||||
|
||||
// Test Component Registration
|
||||
if (!globalThis._registeredComponents || !globalThis._registeredComponents['DataChart']) {
|
||||
throw new Error('DataChart component not registered');
|
||||
}
|
||||
|
||||
// Manually trigger component setup for verification
|
||||
const compContainer = document.getElementById('componentTestContainer');
|
||||
const compEl = document.createElement('DataChart');
|
||||
compEl.state = {
|
||||
__watch: (p, cb) => { compEl._watchers = compEl._watchers || {}; compEl._watchers[p] = cb; },
|
||||
data: [{ x: 1, y: 10 }, { x: 2, y: 20 }]
|
||||
};
|
||||
compEl.appendChild(globalThis._registeredComponents['DataChart'].tpl.cloneNode(true));
|
||||
compContainer.appendChild(compEl);
|
||||
globalThis._registeredComponents['DataChart'].setup(compEl);
|
||||
|
||||
if (!compEl.chartInstance) throw new Error('Component failed to initialize chartInstance');
|
||||
|
||||
if (dcMapped.chart.data.datasets[0].data[1] !== 92) throw new Error('Mapping failed: values');
|
||||
window.renderTime = performance.now() - t0;
|
||||
|
||||
// Test update
|
||||
dc.update({
|
||||
labels: ['Jan', 'Feb', 'Mar'],
|
||||
datasets: [{
|
||||
label: 'Test Data',
|
||||
data: [15, 25, 35]
|
||||
}]
|
||||
});
|
||||
|
||||
results.innerHTML = '<h1 style="color: green">All Tests Passed 🎉</h1>';
|
||||
window.testStatus = 'passed';
|
||||
} catch (e) {
|
||||
|
||||
@ -5,26 +5,26 @@ import terser from '@rollup/plugin-terser';
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@apigo.cc/chart': resolve(__dirname, 'src/index.js')
|
||||
'@web/chart': resolve(__dirname, 'src/index.js')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.js'),
|
||||
name: 'ApigoChart',
|
||||
formats: ['umd']
|
||||
name: 'Chart',
|
||||
formats: ['es']
|
||||
},
|
||||
rollupOptions: {
|
||||
// 确保外部化处理那些你不希望打包进库的依赖
|
||||
external: [],
|
||||
output: [
|
||||
{
|
||||
format: 'umd',
|
||||
name: 'ApigoChart',
|
||||
entryFileNames: 'chart.js'
|
||||
format: 'es',
|
||||
entryFileNames: 'chart.js',
|
||||
minifyInternalExports: false
|
||||
},
|
||||
{
|
||||
format: 'umd',
|
||||
name: 'ApigoChart',
|
||||
format: 'es',
|
||||
entryFileNames: 'chart.min.js',
|
||||
plugins: [terser()]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user