Compare commits

..

No commits in common. "main" and "v1.0.1" have entirely different histories.
main ... v1.0.1

11 changed files with 92 additions and 188 deletions

1
.npmrc
View File

@ -1 +0,0 @@
//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}

View File

@ -1,12 +1,7 @@
# CHANGELOG # CHANGELOG
## v1.0.2 (2026-06-12)
- **架构对齐**: 遵循 `base` 包新设计,彻底消灭用户侧 ESM 强依赖。
- **组件注册**: 支持 `<DataChart>` 全局组件声明式用法。
- **构建优化**: 产物调整为纯 UMD 模式,通过 `globalThis.DataChart` 暴露 API。
## v1.0.0 (2026-05-29) ## v1.0.0 (2026-05-29)
...
### 核心特性 ### 核心特性
- **ESM 封装**: 完成对 Chart.js 4.x 的原生 ESM 封装。 - **ESM 封装**: 完成对 Chart.js 4.x 的原生 ESM 封装。
- **DataChart 组件**: 实现声明式图表初始化与数据驱动更新。 - **DataChart 组件**: 实现声明式图表初始化与数据驱动更新。

103
README.md
View File

@ -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 ```html
<script src="https://cdn.jsdelivr.net/npm/@apigo.cc/chart@1.0.1/dist/chart.min.js"></script> <script type="importmap">
{
<script> "imports": {
// 直接使用全局 DataChart 类 "@web/chart": "https://cdn.example.com/chart/dist/chart.js"
const chart = new DataChart(myCanvas, config); }
}
</script> </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` 进行数据绑定。 chart.update({
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
```html datasets: [{
<div $data="{ label: '销售额',
myData: [{ month: 'Jan', sales: 120 }, { month: 'Feb', sales: 190 }], data: [15, 25, 10, 8]
myMap: { labels: 'month', values: 'sales', label: '月销量' } }]
}"> })
<DataChart type="bar" $.state.data="myData" $.state.map="myMap" style="height:300px"></DataChart>
</div>
``` ```
### JS 调用方式 (兼容)
```html
<canvas id="myCanvas" width="400" height="200"></canvas>
...
---
## 3. API 参考 ## 3. API 参考
### `new DataChart(canvas, config)` ### `DataChart(canvas: HTMLCanvasElement, config: Object)`
- **`canvas`**: HTMLCanvasElement。 构造函数,初始化图表。
- **`config.type`**: `'line'`, `'bar'`, `'pie'` - `config.type`: 图表类型 (`'line'`, `'bar'`, `'pie'`, `'doughnut'`)。
- **`config.data`**: 原始对象数组。 - `config.data`: Chart.js 标准数据格式。
- **`config.map`**: 映射规则。 - `config.options`: Chart.js 标准配置。
- **`config.options`**: 透传给底层 Chart.js 的原生配置。
### 实例方法 ### `update(data?: Object)`
- **`update(newData)`**: 传入新数组重绘图表。 平滑更新图表。
- **`destroy()`**: 销毁实例 - `data`: (可选) 新的数据对象。若不传,则强制触发现有数据的重绘
--- ### `destroy()`
销毁图表实例,释放内存。
## 开发者提示 (AI 必读) ## 4. 支持的图表类型
1. **尺寸控制**: Canvas 必须具有明确的高度和宽度。 内置集成了以下控制器,无需额外注册:
2. **全局变量**: UMD 模式下,`DataChart` 类自动挂载到 `window` - Line (折线图)
- Bar (柱状图)
- Pie / Doughnut (饼图/环形图)
- Tooltip & Legend 插件
## 5. 开发与测试
- **测试靶场**: `test/index.html` (支持热更新调试)
- **单元测试**: `npm run test` (基于 Playwright)
- **构建**: `npm run build` (产出 `chart.js``chart.min.js`)

View File

@ -5,14 +5,13 @@
- CPU: Host machine - CPU: Host machine
- Chart.js version: 4.x - Chart.js version: 4.x
## 性能基准 (v1.0.2) ## 性能基准 (v1.0.0)
| 指标 | 耗时 (ms) | 备注 | | 指标 | 耗时 (ms) | 备注 |
| :--- | :--- | :--- | | :--- | :--- | :--- |
| **首次渲染 (Line Chart)** | 44.50 | 包含 Chart.js 初始化、双图表渲染及组件初始化测试 | | **首次渲染 (Line Chart)** | 43.80 | 包含 Chart.js 初始化与双图表渲染测试 |
| **数据更新 (Smooth Update)** | < 10.00 | 基于 `chart.update()` 的增量更新 | | **数据更新 (Smooth Update)** | < 10.00 | 基于 `chart.update()` 的增量更新 |
## 设计决策 ## 设计决策
1. **Tree Shaking**: 仅内置常用的控制器Line, Bar, Pie以控制产物体积。 1. **Tree Shaking**: 仅内置常用的控制器Line, Bar, Pie以控制产物体积。
2. **实例复用**: 严格遵循 `chart.update()` 路径,避免频繁创建 Canvas 渲染上下文。 2. **实例复用**: 严格遵循 `chart.update()` 路径,避免频繁创建 Canvas 渲染上下文。
3. **组件化支持**: 通过 `globalThis.Component` 注册,实现零打包声明式渲染。

4
package-lock.json generated
View File

@ -1,11 +1,11 @@
{ {
"name": "@apigo.cc/chart", "name": "@web/chart",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@apigo.cc/chart", "name": "@web/chart",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"chart.js": "^4.4.0" "chart.js": "^4.4.0"

View File

@ -1,8 +1,9 @@
{ {
"name": "@apigo.cc/chart", "name": "@apigo.cc/chart",
"version": "1.0.2", "version": "1.0.1",
"type": "module", "type": "module",
"main": "dist/chart.js", "main": "dist/chart.js",
"module": "dist/chart.js",
"files": [ "files": [
"dist" "dist"
], ],

View File

@ -4,11 +4,11 @@ export default defineConfig({
testDir: './test', testDir: './test',
testMatch: '**/*.spec.js', testMatch: '**/*.spec.js',
use: { use: {
baseURL: 'http://127.0.0.1:8085', baseURL: 'http://localhost:8082',
}, },
webServer: { webServer: {
command: 'npx vite --port 8085 --strictPort --host 127.0.0.1', command: 'npx vite --port 8082',
url: 'http://127.0.0.1:8085', url: 'http://localhost:8082',
timeout: 120000, timeout: 120000,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },

View File

@ -1,46 +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'));
// npm 要求包名全小写scope + name 强制 toLowerCase
const baseName = (pkg.name.includes('/') ? pkg.name.split('/')[1] : pkg.name).toLowerCase();
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);
}

View File

@ -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;
}

View File

@ -5,7 +5,7 @@
<script type="importmap"> <script type="importmap">
{ {
"imports": { "imports": {
"@apigo.cc/chart": "../src/index.js" "@web/chart": "../src/index.js"
} }
} }
</script> </script>
@ -21,37 +21,13 @@
<div class="chart-container"> <div class="chart-container">
<canvas id="testChart"></canvas> <canvas id="testChart"></canvas>
</div> </div>
<hr>
<h3>Component Test</h3>
<div id="componentTestContainer" class="chart-container">
<!-- Will inject component here -->
</div>
<script type="module"> <script type="module">
import { DataChart } from '@apigo.cc/chart'; import { DataChart } from '@web/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;
}
};
}
async function runTests() { async function runTests() {
const results = document.getElementById('results'); const results = document.getElementById('results');
try { try {
const t0 = performance.now(); const t0 = performance.now();
// ... (existing JS API tests)
const canvas = document.getElementById('testChart'); const canvas = document.getElementById('testChart');
const dc = new DataChart(canvas, { const dc = new DataChart(canvas, {
type: 'line', type: 'line',
@ -79,26 +55,18 @@
}); });
if (dcMapped.chart.data.labels[0] !== 'Alice') throw new Error('Mapping failed: labels'); if (dcMapped.chart.data.labels[0] !== 'Alice') throw new Error('Mapping failed: labels');
if (dcMapped.chart.data.datasets[0].data[1] !== 92) throw new Error('Mapping failed: values');
// 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');
window.renderTime = performance.now() - t0; 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>'; results.innerHTML = '<h1 style="color: green">All Tests Passed 🎉</h1>';
window.testStatus = 'passed'; window.testStatus = 'passed';
} catch (e) { } catch (e) {

View File

@ -5,26 +5,26 @@ import terser from '@rollup/plugin-terser';
export default defineConfig({ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
'@apigo.cc/chart': resolve(__dirname, 'src/index.js') '@web/chart': resolve(__dirname, 'src/index.js')
} }
}, },
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'src/index.js'), entry: resolve(__dirname, 'src/index.js'),
name: 'ApigoChart', name: 'Chart',
formats: ['umd'] formats: ['es']
}, },
rollupOptions: { rollupOptions: {
// 确保外部化处理那些你不希望打包进库的依赖
external: [], external: [],
output: [ output: [
{ {
format: 'umd', format: 'es',
name: 'ApigoChart', entryFileNames: 'chart.js',
entryFileNames: 'chart.js' minifyInternalExports: false
}, },
{ {
format: 'umd', format: 'es',
name: 'ApigoChart',
entryFileNames: 'chart.min.js', entryFileNames: 'chart.min.js',
plugins: [terser()] plugins: [terser()]
} }