Compare commits
No commits in common. "main" and "v1.0.1" have entirely different histories.
@ -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
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
|
```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`)
|
||||||
|
|||||||
5
TEST.md
5
TEST.md
@ -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
4
package-lock.json
generated
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
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">
|
<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) {
|
||||||
|
|||||||
@ -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()]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user