feat: 双图合并升级为多图合并 + 数据输入框加宽
- 合并模式默认2张图,新增'+添加图表'按钮可不断添加,每张图可独立删除 - 每张图卡片:标题/类型/主题/显示选项/数据,动态渲染(combineCharts 数组驱动) - 合并逻辑支持 N 张图:横排等高依次排列、竖排等宽依次堆叠 - 后端 /api/combine 支持 charts 数组(N 张),兼容 chart1+chart2 - 修复合并图数据输入框过窄:.config-group textarea 统一 width:100%(与设置区一致)
This commit is contained in:
@@ -928,7 +928,7 @@ app.get('/api/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'data-chart-tool',
|
||||
version: '1.12.0',
|
||||
version: '1.13.0',
|
||||
endpoints: {
|
||||
'POST /api/chart': '生成图表图片(JSON body)',
|
||||
'GET /api/chart': '生成图表图片(URL 参数)',
|
||||
@@ -943,7 +943,7 @@ app.get('/api/health', (req, res) => {
|
||||
app.get('/api/docs', (req, res) => {
|
||||
res.json({
|
||||
name: '数据可视化图表生成器 API',
|
||||
version: '1.12.0',
|
||||
version: '1.13.0',
|
||||
endpoints: [
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -1055,13 +1055,12 @@ app.get('/api/docs', (req, res) => {
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/combine',
|
||||
description: '将两张图表合并到一张图片中(支持横排/竖排)',
|
||||
description: '将多张图表合并到一张图片中(支持横排/竖排)',
|
||||
'Content-Type': 'application/json',
|
||||
params: {
|
||||
chart1: { type: 'object', required: true, description: '第一张图配置(同 /api/chart 参数)' },
|
||||
chart2: { type: 'object', required: true, description: '第二张图配置(同 /api/chart 参数)' },
|
||||
charts: { type: 'array', required: true, description: '图表配置数组(N 张),每项同 /api/chart 参数;兼容 chart1 + chart2' },
|
||||
direction: { type: 'string', default: 'horizontal', options: ['horizontal', 'vertical'], description: '排列方向:horizontal 横排(左右)/ vertical 竖排(上下)' },
|
||||
gap: { type: 'number', default: 24, description: '两图间距(px)' },
|
||||
gap: { type: 'number', default: 24, description: '图间距(px)' },
|
||||
pixelRatio: { type: 'number', default: 2, description: '像素倍率(清晰度)' },
|
||||
background: { type: 'string', default: '#ffffff', description: '背景色' }
|
||||
},
|
||||
@@ -1081,22 +1080,29 @@ app.get('/api/docs', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ===== API: 双图合并生成图片 =====
|
||||
// 将两张图(图表/表格均可)合并到一张图片中,支持横排(左右)或竖排(上下)
|
||||
// ===== API: 多图合并生成图片 =====
|
||||
app.post('/api/combine', async (req, res) => {
|
||||
try {
|
||||
const body = req.body || {};
|
||||
const chart1 = body.chart1 || {};
|
||||
const chart2 = body.chart2 || {};
|
||||
// 支持 charts 数组(N 张图),也兼容旧的 chart1 + chart2
|
||||
let charts;
|
||||
if (Array.isArray(body.charts) && body.charts.length) {
|
||||
charts = body.charts;
|
||||
} else if (body.chart1 && body.chart2) {
|
||||
charts = [body.chart1, body.chart2];
|
||||
} else {
|
||||
return res.status(400).json({ error: '需要提供 charts 数组(或 chart1 + chart2)' });
|
||||
}
|
||||
charts = charts.filter(c => c && c.data);
|
||||
if (charts.length === 0) {
|
||||
return res.status(400).json({ error: '至少需要一个有数据的图表(data 参数)' });
|
||||
}
|
||||
|
||||
const direction = body.direction === 'vertical' ? 'vertical' : 'horizontal';
|
||||
const gap = parseInt(body.gap) || 24;
|
||||
const pixelRatio = parseInt(body.pixelRatio) || 2;
|
||||
const background = body.background || '#ffffff';
|
||||
|
||||
if (!chart1.data || !chart2.data) {
|
||||
return res.status(400).json({ error: 'chart1 和 chart2 都需要提供 data 参数(CSV 格式)' });
|
||||
}
|
||||
|
||||
// 渲染单个子图(直接按目标尺寸渲染,避免二次缩放损失)
|
||||
// 注意:必须先 toBuffer 再 chart.dispose(),dispose 会清空 canvas 内容!
|
||||
const renderSub = async (params, w, h) => {
|
||||
@@ -1115,23 +1121,22 @@ app.post('/api/combine', async (req, res) => {
|
||||
return { img, w, h };
|
||||
};
|
||||
|
||||
const a = await renderSub(chart1, parseInt(chart1.width) || 640, parseInt(chart1.height) || 420);
|
||||
const b = await renderSub(chart2, parseInt(chart2.width) || 640, parseInt(chart2.height) || 420);
|
||||
const subs = await Promise.all(charts.map(c =>
|
||||
renderSub(c, parseInt(c.width) || 640, parseInt(c.height) || 420)
|
||||
));
|
||||
|
||||
let W, H, aRect, bRect;
|
||||
let W, H, scaled;
|
||||
if (direction === 'vertical') {
|
||||
// 竖排(上下):等宽对齐
|
||||
const tw = Math.max(a.w, b.w);
|
||||
aRect = { w: tw, h: Math.round(a.h * (tw / a.w)) };
|
||||
bRect = { w: tw, h: Math.round(b.h * (tw / b.w)) };
|
||||
const tw = Math.max(...subs.map(s => s.w));
|
||||
scaled = subs.map(s => ({ img: s.img, w: tw, h: Math.round(s.h * (tw / s.w)) }));
|
||||
W = tw;
|
||||
H = aRect.h + bRect.h + gap;
|
||||
H = scaled.reduce((sum, s) => sum + s.h, 0) + gap * (scaled.length - 1);
|
||||
} else {
|
||||
// 横排(左右):等高对齐
|
||||
const th = Math.max(a.h, b.h);
|
||||
aRect = { w: Math.round(a.w * (th / a.h)), h: th };
|
||||
bRect = { w: Math.round(b.w * (th / b.h)), h: th };
|
||||
W = aRect.w + bRect.w + gap;
|
||||
const th = Math.max(...subs.map(s => s.h));
|
||||
scaled = subs.map(s => ({ img: s.img, w: Math.round(s.w * (th / s.h)), h: th }));
|
||||
W = scaled.reduce((sum, s) => sum + s.w, 0) + gap * (scaled.length - 1);
|
||||
H = th;
|
||||
}
|
||||
|
||||
@@ -1142,11 +1147,17 @@ app.post('/api/combine', async (req, res) => {
|
||||
|
||||
if (direction === 'vertical') {
|
||||
// 5 参数形式:drawImage(img, dx, dy, dw, dh),源整图缩放到目标矩形
|
||||
ctx.drawImage(a.img, 0, 0, aRect.w * pixelRatio, aRect.h * pixelRatio);
|
||||
ctx.drawImage(b.img, 0, (aRect.h + gap) * pixelRatio, bRect.w * pixelRatio, bRect.h * pixelRatio);
|
||||
let y = 0;
|
||||
scaled.forEach(s => {
|
||||
ctx.drawImage(s.img, 0, y * pixelRatio, s.w * pixelRatio, s.h * pixelRatio);
|
||||
y += s.h + gap;
|
||||
});
|
||||
} else {
|
||||
ctx.drawImage(a.img, 0, 0, aRect.w * pixelRatio, aRect.h * pixelRatio);
|
||||
ctx.drawImage(b.img, (aRect.w + gap) * pixelRatio, 0, bRect.w * pixelRatio, bRect.h * pixelRatio);
|
||||
let x = 0;
|
||||
scaled.forEach(s => {
|
||||
ctx.drawImage(s.img, x * pixelRatio, 0, s.w * pixelRatio, s.h * pixelRatio);
|
||||
x += s.w + gap;
|
||||
});
|
||||
}
|
||||
|
||||
const buffer = canvas.toBuffer('image/png');
|
||||
@@ -1154,13 +1165,14 @@ app.post('/api/combine', async (req, res) => {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': buffer.length,
|
||||
'X-Combine-Direction': direction,
|
||||
'X-Combine-Charts': String(scaled.length),
|
||||
'X-Chart-Width': W,
|
||||
'X-Chart-Height': H
|
||||
});
|
||||
res.send(buffer);
|
||||
} catch (err) {
|
||||
console.error('Combine error:', err);
|
||||
res.status(500).json({ error: '双图合并失败: ' + err.message });
|
||||
res.status(500).json({ error: '多图合并失败: ' + err.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user