feat: 数据方向(行=系列)+每系列类型/双轴 + 多图大标题/共享图例 v1.16.0
- 图表模式新增'数据方向':列=系列(默认)/行=系列(第一行是横坐标,每行一个系列),每行可独立设柱状/折线 + 左右轴量度 - 后端 buildChartOption/GET /api/chart 支持 rowsAsSeries + seriesAxis(按系列下标指定左右轴) - 多图合并每张小图新增:行=系列、双Y轴(左右量度不同)、每系列独立图表类型与坐标轴配置 - 多图合并新增图片大标题(bigTitle),居中显示在顶部 - 多图合并新增图例方式(legendMode):own 各自图例(默认)/shared-top 共用图例顶部/shared-bottom 共用图例底部,共用时各子图隐藏自己的图例、系列合并去重、超宽自动换行 - /api/combine 后端同步支持 bigTitle/legendMode(含深色主题文字适配) - 收藏功能完整支持新字段(combine 大标题/图例方式/每系列配置,chart 数据方向),编辑恢复正常 - 文档/API.md/README 更新,版本 1.16.0
This commit is contained in:
@@ -76,8 +76,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 数据解析 =====
|
||||
function parseData(rawText) {
|
||||
// ===== 数据解析(rowsAsSeries=true 时“行=系列”:第一行是横坐标,每行一个系列) =====
|
||||
function parseData(rawText, rowsAsSeries) {
|
||||
const lines = rawText.trim().split('\n').filter(l => l.trim());
|
||||
if (lines.length < 2) {
|
||||
alert('数据至少需要包含表头和一行数据');
|
||||
@@ -100,6 +100,32 @@ function parseData(rawText) {
|
||||
const categories = [];
|
||||
const seriesData = {};
|
||||
|
||||
if (rowsAsSeries) {
|
||||
// 行=系列:第一列=系列名,表头除第一列外=横坐标
|
||||
const names = [];
|
||||
const nameSet = {};
|
||||
rows.slice(1).forEach(r => {
|
||||
const n = (r[0] || '').trim();
|
||||
if (n && !nameSet[n]) { nameSet[n] = true; names.push(n); }
|
||||
});
|
||||
names.forEach(n => { seriesData[n] = []; });
|
||||
for (let j = 1; j < headers.length; j++) {
|
||||
categories.push(headers[j]);
|
||||
for (let i = 1; i < rows.length; i++) {
|
||||
const r = rows[i];
|
||||
const name = ((r && r[0]) || '').trim();
|
||||
if (!seriesData[name]) continue;
|
||||
const val = parseFloat(r[j]);
|
||||
seriesData[name].push(isNaN(val) ? 0 : val);
|
||||
}
|
||||
}
|
||||
return {
|
||||
categories,
|
||||
seriesNames: names,
|
||||
seriesData
|
||||
};
|
||||
}
|
||||
|
||||
// 第一列是横坐标,其余列是系列
|
||||
for (let i = 1; i < headers.length; i++) {
|
||||
seriesData[headers[i]] = [];
|
||||
@@ -128,7 +154,7 @@ function generateChart() {
|
||||
return;
|
||||
}
|
||||
|
||||
parsedData = parseData(rawText);
|
||||
parsedData = parseData(rawText, document.getElementById('dataOrientation').value === 'rows');
|
||||
if (!parsedData) return;
|
||||
|
||||
// 初始化系列顺序和颜色
|
||||
@@ -759,6 +785,13 @@ function clearData() {
|
||||
document.getElementById('seriesConfig').innerHTML = '<p class="hint-text">生成图表后可在此调整各系列的顺序和颜色</p>';
|
||||
}
|
||||
|
||||
// 数据方向切换(列=系列 / 行=系列):重新解析并重建系列配置
|
||||
function onDataOrientationChange() {
|
||||
if (currentMode === 'chart' && document.getElementById('dataInput').value.trim()) {
|
||||
generateChart();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 导出图表(SVG 专用) =====
|
||||
function exportChartSvg() {
|
||||
if (!chartInstance) {
|
||||
@@ -1146,13 +1179,14 @@ let combinedCanvas = null;
|
||||
|
||||
// 构建单个子图的 echarts option(不依赖全局状态,双图模式专用)
|
||||
function buildCombineChartOption(dataText, cfg) {
|
||||
const parsed = parseData(dataText);
|
||||
const parsed = parseData(dataText, !!cfg.rowsAsSeries);
|
||||
if (!parsed) return null;
|
||||
|
||||
const {
|
||||
title = '', chartType = 'bar', theme = 'default',
|
||||
showLegend = true, showGrid = true, showLabel = false,
|
||||
stackMode = false, smoothLine = true
|
||||
stackMode = false, smoothLine = true,
|
||||
dualYAxis = false, leftAxisName = '', rightAxisName = '', seriesConfig = null
|
||||
} = cfg;
|
||||
|
||||
const bgColor = theme === 'dark' ? '#1a1a2e' : '#ffffff';
|
||||
@@ -1222,7 +1256,11 @@ function buildCombineChartOption(dataText, cfg) {
|
||||
|
||||
const series = parsed.seriesNames.map((name, idx) => {
|
||||
const color = palette[idx % palette.length];
|
||||
const scMap = {};
|
||||
(Array.isArray(seriesConfig) ? seriesConfig : []).forEach(s => { if (s && s.name) scMap[s.name] = s; });
|
||||
const sc = scMap[name] || {};
|
||||
let type = chartType === 'bar-line' ? (idx % 2 === 0 ? 'bar' : 'line') : chartType;
|
||||
if (sc.type && sc.type !== 'auto') type = sc.type;
|
||||
|
||||
const s = {
|
||||
name,
|
||||
@@ -1232,6 +1270,8 @@ function buildCombineChartOption(dataText, cfg) {
|
||||
emphasis: { focus: 'series' }
|
||||
};
|
||||
|
||||
if (dualYAxis) s.yAxisIndex = (sc.axis === 1 || sc.axis === '1') ? 1 : 0;
|
||||
|
||||
if (stackMode) s.stack = 'total';
|
||||
|
||||
if (type === 'line') {
|
||||
@@ -1302,7 +1342,27 @@ function buildCombineChartOption(dataText, cfg) {
|
||||
axisLabel: { color: textColor, fontSize: 11, interval: 0, rotate: parsed.categories.length > 10 ? 30 : 0 },
|
||||
axisTick: { show: false }
|
||||
},
|
||||
yAxis: {
|
||||
yAxis: dualYAxis ? [
|
||||
{
|
||||
type: 'value',
|
||||
name: leftAxisName || '左轴',
|
||||
nameTextStyle: { color: textColor, fontSize: 11, padding: [0, 0, 0, 4] },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: textColor, fontSize: 11 },
|
||||
splitLine: { show: showGrid, lineStyle: { color: theme === 'dark' ? '#333' : '#f0f0f0', type: 'dashed' } }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: rightAxisName || '右轴',
|
||||
position: 'right',
|
||||
nameTextStyle: { color: textColor, fontSize: 11, padding: [0, 4, 0, 0] },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: textColor, fontSize: 11 },
|
||||
splitLine: { show: false }
|
||||
}
|
||||
] : {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
@@ -1315,8 +1375,8 @@ function buildCombineChartOption(dataText, cfg) {
|
||||
}
|
||||
|
||||
// 根据数据量估算子图尺寸
|
||||
function measureCombineChart(dataText) {
|
||||
const p = parseData(dataText);
|
||||
function measureCombineChart(dataText, rowsAsSeries) {
|
||||
const p = parseData(dataText, !!rowsAsSeries);
|
||||
if (!p) return { w: 600, h: 400 };
|
||||
const w = Math.min(900, Math.max(500, p.categories.length * 70 + 140));
|
||||
return { w, h: 400 };
|
||||
@@ -1326,7 +1386,7 @@ function measureCombineChart(dataText) {
|
||||
let combineCharts = [];
|
||||
|
||||
function defaultCombineChart() {
|
||||
return { title: '', type: 'bar', theme: 'default', legend: true, grid: true, label: false, stack: false, data: '' };
|
||||
return { title: '', type: 'bar', theme: 'default', legend: true, grid: true, label: false, stack: false, data: '', rowsAsSeries: false, dualYAxis: false, leftAxisName: '', rightAxisName: '', seriesConfig: [] };
|
||||
}
|
||||
|
||||
function initCombineCharts() {
|
||||
@@ -1378,8 +1438,25 @@ function renderCombineCharts() {
|
||||
<label><input type="checkbox" ${c.grid ? 'checked' : ''} onchange="updateCombineChart(${i},'grid',this.checked)"> 网格线</label>
|
||||
<label><input type="checkbox" ${c.label ? 'checked' : ''} onchange="updateCombineChart(${i},'label',this.checked)"> 数据标签</label>
|
||||
<label><input type="checkbox" ${c.stack ? 'checked' : ''} onchange="updateCombineChart(${i},'stack',this.checked)"> 堆叠</label>
|
||||
<label><input type="checkbox" ${c.rowsAsSeries ? 'checked' : ''} onchange="updateCombineChart(${i},'rowsAsSeries',this.checked)"> 行=系列</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-group">
|
||||
<label><input type="checkbox" ${c.dualYAxis ? 'checked' : ''} onchange="updateCombineChart(${i},'dualYAxis',this.checked)"> 双Y轴(不同左右量度)</label>
|
||||
</div>
|
||||
${c.dualYAxis ? `
|
||||
<div class="config-group">
|
||||
<label>左右轴名称(量度可不同)</label>
|
||||
<div class="res-input-row">
|
||||
<input type="text" value="${escHtml(c.leftAxisName)}" placeholder="左轴,如:销售额" oninput="updateCombineChart(${i},'leftAxisName',this.value)">
|
||||
<span class="res-times">/</span>
|
||||
<input type="text" value="${escHtml(c.rightAxisName)}" placeholder="右轴,如:增长率(%)" oninput="updateCombineChart(${i},'rightAxisName',this.value)">
|
||||
</div>
|
||||
</div>` : ''}
|
||||
<div class="config-group">
|
||||
<label>系列图表类型 / 坐标轴</label>
|
||||
<div class="combine-series-cfg" id="combineSeriesCfg${i}">${renderCombineSeriesConfigHTML(i)}</div>
|
||||
</div>
|
||||
<div class="config-group">
|
||||
<label>数据</label>
|
||||
<textarea rows="5" placeholder="图${i + 1}的CSV数据..." oninput="updateCombineChart(${i},'data',this.value)">${escHtml(c.data)}</textarea>
|
||||
@@ -1391,9 +1468,86 @@ function renderCombineCharts() {
|
||||
function updateCombineChart(i, field, value) {
|
||||
if (!combineCharts[i]) return;
|
||||
combineCharts[i][field] = value;
|
||||
if (field === 'dualYAxis' || field === 'rowsAsSeries') {
|
||||
syncCombineSeriesConfig(i);
|
||||
renderCombineCharts();
|
||||
generateCombine();
|
||||
return;
|
||||
}
|
||||
if (field === 'data') {
|
||||
syncCombineSeriesConfig(i);
|
||||
const block = document.getElementById('combineSeriesCfg' + i);
|
||||
if (block) block.innerHTML = renderCombineSeriesConfigHTML(i);
|
||||
}
|
||||
generateCombine();
|
||||
}
|
||||
|
||||
// ===== 多图合并:系列配置(每行/列可独立设图表类型与左右轴) =====
|
||||
function syncCombineSeriesConfig(i) {
|
||||
const c = combineCharts[i];
|
||||
if (!c) return;
|
||||
const p = parseData(c.data, !!c.rowsAsSeries);
|
||||
if (!p) return;
|
||||
const old = Array.isArray(c.seriesConfig) ? c.seriesConfig : [];
|
||||
const oldMap = {};
|
||||
old.forEach(o => { if (o && o.name) oldMap[o.name] = o; });
|
||||
c.seriesConfig = p.seriesNames.map(n => {
|
||||
const prev = oldMap[n] || {};
|
||||
return { name: n, type: prev.type || 'auto', axis: prev.axis !== undefined ? prev.axis : 0 };
|
||||
});
|
||||
}
|
||||
|
||||
function renderCombineSeriesConfigHTML(i) {
|
||||
const c = combineCharts[i];
|
||||
if (!c) return '<p class="hint-text">输入数据后自动显示各系列的图表类型/坐标轴</p>';
|
||||
syncCombineSeriesConfig(i);
|
||||
const p = parseData(c.data, !!c.rowsAsSeries);
|
||||
if (!p || !p.seriesNames.length) return '<p class="hint-text">输入数据后自动显示各系列的图表类型/坐标轴</p>';
|
||||
const dual = !!c.dualYAxis;
|
||||
return p.seriesNames.map((name, idx) => {
|
||||
const sc = (c.seriesConfig && c.seriesConfig[idx]) || { name, type: 'auto', axis: 0 };
|
||||
return `<div class="combine-series-row">
|
||||
<span class="combine-series-name" title="${escHtml(name)}">${escHtml(name)}</span>
|
||||
<select onchange="updateCombineSeriesCfg(${i}, ${idx}, 'type', this.value)" title="图表类型">
|
||||
<option value="auto" ${sc.type === 'auto' ? 'selected' : ''}>自动</option>
|
||||
<option value="bar" ${sc.type === 'bar' ? 'selected' : ''}>柱状</option>
|
||||
<option value="line" ${sc.type === 'line' ? 'selected' : ''}>折线</option>
|
||||
</select>
|
||||
${dual ? `<select onchange="updateCombineSeriesCfg(${i}, ${idx}, 'axis', this.value)" title="坐标轴">
|
||||
<option value="0" ${String(sc.axis) === '0' ? 'selected' : ''}>左轴</option>
|
||||
<option value="1" ${String(sc.axis) === '1' ? 'selected' : ''}>右轴</option>
|
||||
</select>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function updateCombineSeriesCfg(i, idx, field, value) {
|
||||
const c = combineCharts[i];
|
||||
if (!c) return;
|
||||
syncCombineSeriesConfig(i);
|
||||
if (c.seriesConfig && c.seriesConfig[idx]) c.seriesConfig[idx][field] = value;
|
||||
generateCombine();
|
||||
}
|
||||
|
||||
// 多图合并:共享图例条目(所有子图系列合并,按名称去重)
|
||||
function buildCombineLegendItems() {
|
||||
const items = [];
|
||||
const seen = {};
|
||||
combineCharts.forEach(c => {
|
||||
if (!c.data || !c.data.trim()) return;
|
||||
const p = parseData(c.data, !!c.rowsAsSeries);
|
||||
if (!p) return;
|
||||
const palette = colorPalettes[c.theme] || colorPalettes.default;
|
||||
p.seriesNames.forEach((name, idx) => {
|
||||
if (!seen[name]) {
|
||||
seen[name] = true;
|
||||
items.push({ name, color: palette[idx % palette.length] });
|
||||
}
|
||||
});
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
function addCombineChart() {
|
||||
combineCharts.push(defaultCombineChart());
|
||||
renderCombineCharts();
|
||||
@@ -1421,13 +1575,19 @@ function generateCombine() {
|
||||
|
||||
const direction = document.querySelector('input[name="combineDirection"]:checked').value;
|
||||
const gap = 24;
|
||||
const bigTitle = document.getElementById('combineBigTitle').value;
|
||||
const legendMode = document.getElementById('combineLegendMode').value; // own | shared-top | shared-bottom
|
||||
const sharedLegend = legendMode !== 'own';
|
||||
const chartArea = document.getElementById('chartArea');
|
||||
chartArea.innerHTML = '<div class="placeholder"><p>⏳</p><p>正在合并...</p></div>';
|
||||
|
||||
const renderOne = (c, w, h) => new Promise((resolve, reject) => {
|
||||
const option = buildCombineChartOption(c.data, {
|
||||
title: c.title, chartType: c.type, theme: c.theme,
|
||||
showLegend: c.legend, showGrid: c.grid, showLabel: c.label, stackMode: c.stack
|
||||
showLegend: sharedLegend ? false : c.legend, showGrid: c.grid, showLabel: c.label, stackMode: c.stack,
|
||||
rowsAsSeries: !!c.rowsAsSeries,
|
||||
dualYAxis: !!c.dualYAxis, leftAxisName: c.leftAxisName || '', rightAxisName: c.rightAxisName || '',
|
||||
seriesConfig: c.seriesConfig
|
||||
});
|
||||
if (!option) { reject(new Error('数据格式错误')); return; }
|
||||
const holder = document.createElement('div');
|
||||
@@ -1450,58 +1610,118 @@ function generateCombine() {
|
||||
});
|
||||
|
||||
Promise.all(validCharts.map(c => {
|
||||
const m = measureCombineChart(c.data);
|
||||
const m = measureCombineChart(c.data, c.rowsAsSeries);
|
||||
return renderOne(c, m.w, m.h);
|
||||
})).then(canvases => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// ---- 基础布局(不含大标题/共享图例)----
|
||||
let W, H, cells = [];
|
||||
if (direction === 'grid') {
|
||||
// 多行多列网格:所有子图统一 contain 到最大单元格,按列填充
|
||||
const cols = Math.max(1, parseInt(document.getElementById('gridCols').value) || 2);
|
||||
const rows = Math.ceil(canvases.length / cols);
|
||||
const cellW = Math.max(...canvases.map(c => c.width));
|
||||
const cellH = Math.max(...canvases.map(c => c.height));
|
||||
canvas.width = cols * cellW + (cols - 1) * gap;
|
||||
canvas.height = rows * cellH + (rows - 1) * gap;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
W = cols * cellW + (cols - 1) * gap;
|
||||
H = rows * cellH + (rows - 1) * gap;
|
||||
canvases.forEach((c, i) => {
|
||||
const r = Math.floor(i / cols), col = i % cols;
|
||||
const scale = Math.min(cellW / c.width, cellH / c.height);
|
||||
const dw = Math.round(c.width * scale), dh = Math.round(c.height * scale);
|
||||
const x = col * (cellW + gap) + Math.round((cellW - dw) / 2);
|
||||
const y = r * (cellH + gap) + Math.round((cellH - dh) / 2);
|
||||
ctx.drawImage(c, x, y, dw, dh);
|
||||
cells.push({ c, x: col * (cellW + gap) + Math.round((cellW - dw) / 2), y: r * (cellH + gap) + Math.round((cellH - dh) / 2), w: dw, h: dh });
|
||||
});
|
||||
} else if (direction === 'vertical') {
|
||||
const tw = Math.max(...canvases.map(c => c.width));
|
||||
let y = 0;
|
||||
canvases.forEach(c => {
|
||||
const h = Math.round(c.height * tw / c.width);
|
||||
cells.push({ c, x: 0, y, w: tw, h });
|
||||
y += h + gap;
|
||||
});
|
||||
W = tw;
|
||||
H = y - gap;
|
||||
} else {
|
||||
let W, H, scaled;
|
||||
if (direction === 'vertical') {
|
||||
// 竖排:等宽
|
||||
const tw = Math.max(...canvases.map(c => c.width));
|
||||
scaled = canvases.map(c => ({ c, w: tw, h: Math.round(c.height * tw / c.width) }));
|
||||
W = tw;
|
||||
H = scaled.reduce((s, x) => s + x.h, 0) + gap * (scaled.length - 1);
|
||||
} else {
|
||||
// 横排:等高
|
||||
const th = Math.max(...canvases.map(c => c.height));
|
||||
scaled = canvases.map(c => ({ c, w: Math.round(c.width * th / c.height), h: th }));
|
||||
W = scaled.reduce((s, x) => s + x.w, 0) + gap * (scaled.length - 1);
|
||||
H = th;
|
||||
}
|
||||
const th = Math.max(...canvases.map(c => c.height));
|
||||
let x = 0;
|
||||
canvases.forEach(c => {
|
||||
const w = Math.round(c.width * th / c.height);
|
||||
cells.push({ c, x, y: 0, w, h: th });
|
||||
x += w + gap;
|
||||
});
|
||||
W = x - gap;
|
||||
H = th;
|
||||
}
|
||||
|
||||
canvas.width = W;
|
||||
canvas.height = H;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
// ---- 共享图例:按宽度换行计算 ----
|
||||
const legendFont = 13, swatch = 14, sw = 5, ig = 18, lineH = legendFont + 10;
|
||||
let legendRows = [], legendH = 0;
|
||||
if (sharedLegend) {
|
||||
const items = buildCombineLegendItems();
|
||||
const tc = document.createElement('canvas'); tc.width = 10; tc.height = 10;
|
||||
const tctx = tc.getContext('2d');
|
||||
tctx.font = `${legendFont}px sans-serif`;
|
||||
const maxW = W - 20;
|
||||
let cur = [], curW = 0;
|
||||
items.forEach(it => {
|
||||
const itemW = swatch + sw + tctx.measureText(it.name).width;
|
||||
if (cur.length && curW + ig + itemW > maxW) { legendRows.push(cur); cur = []; curW = 0; }
|
||||
cur.push(it);
|
||||
curW += itemW + (cur.length > 1 ? ig : 0);
|
||||
});
|
||||
if (cur.length) legendRows.push(cur);
|
||||
legendH = legendRows.length * lineH + 8;
|
||||
}
|
||||
|
||||
if (direction === 'vertical') {
|
||||
let y = 0;
|
||||
scaled.forEach(x => { ctx.drawImage(x.c, 0, y, x.w, x.h); y += x.h + gap; });
|
||||
} else {
|
||||
let x = 0;
|
||||
scaled.forEach(s => { ctx.drawImage(s.c, x, 0, s.w, s.h); x += s.w + gap; });
|
||||
}
|
||||
// ---- 大标题区高度 ----
|
||||
const titleFont = 34;
|
||||
const titleH = bigTitle ? titleFont + 26 : 0;
|
||||
const topOffset = titleH + (legendMode === 'shared-top' ? legendH + 12 : 0);
|
||||
const bottomOffset = legendMode === 'shared-bottom' ? legendH + 12 : 0;
|
||||
const totalH = H + topOffset + bottomOffset;
|
||||
|
||||
// ---- 绘制最终画布 ----
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = W; canvas.height = totalH;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, W, totalH);
|
||||
|
||||
if (bigTitle) {
|
||||
ctx.font = `bold ${titleFont}px sans-serif`;
|
||||
ctx.fillStyle = '#333333';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(bigTitle, W / 2, (titleH - 26) / 2 + 2);
|
||||
}
|
||||
|
||||
const drawLegendBlock = (topY) => {
|
||||
legendRows.forEach((row, ri) => {
|
||||
ctx.font = `${legendFont}px sans-serif`;
|
||||
let totalW = 0;
|
||||
row.forEach(it => { totalW += swatch + sw + ctx.measureText(it.name).width + ig; });
|
||||
totalW -= ig;
|
||||
let cx = (W - totalW) / 2;
|
||||
row.forEach(it => {
|
||||
ctx.fillStyle = it.color;
|
||||
ctx.fillRect(cx, topY + ri * lineH + (legendFont - swatch) / 2 + 2, swatch, swatch);
|
||||
ctx.fillStyle = '#333333';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = `${legendFont}px sans-serif`;
|
||||
ctx.fillText(it.name, cx + swatch + sw, topY + ri * lineH + legendFont / 2 + 2);
|
||||
cx += swatch + sw + ctx.measureText(it.name).width + ig;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (sharedLegend && legendMode === 'shared-top') {
|
||||
drawLegendBlock(titleH + 4);
|
||||
}
|
||||
|
||||
cells.forEach(cell => {
|
||||
ctx.drawImage(cell.c, cell.x, cell.y + topOffset, cell.w, cell.h);
|
||||
});
|
||||
|
||||
if (sharedLegend && legendMode === 'shared-bottom') {
|
||||
drawLegendBlock(topOffset + H + 8);
|
||||
}
|
||||
|
||||
combinedCanvas = canvas;
|
||||
@@ -1523,6 +1743,7 @@ function collectChartConfig() {
|
||||
return {
|
||||
mode: 'chart',
|
||||
data: document.getElementById('dataInput').value,
|
||||
rowsAsSeries: document.getElementById('dataOrientation').value === 'rows',
|
||||
chartType: document.getElementById('chartType').value,
|
||||
title: document.getElementById('chartTitle').value,
|
||||
theme: document.getElementById('themeStyle').value,
|
||||
@@ -1561,9 +1782,14 @@ function collectTableConfig() {
|
||||
function collectCombineConfig() {
|
||||
return {
|
||||
mode: 'combine',
|
||||
charts: combineCharts.map(c => ({ ...c })),
|
||||
charts: combineCharts.map(c => ({
|
||||
...c,
|
||||
seriesConfig: Array.isArray(c.seriesConfig) ? c.seriesConfig.map(s => ({ ...s })) : []
|
||||
})),
|
||||
direction: (document.querySelector('input[name="combineDirection"]:checked') || {}).value || 'horizontal',
|
||||
gridCols: parseInt(document.getElementById('gridCols').value) || 2
|
||||
gridCols: parseInt(document.getElementById('gridCols').value) || 2,
|
||||
bigTitle: document.getElementById('combineBigTitle').value,
|
||||
legendMode: document.getElementById('combineLegendMode').value
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1576,7 +1802,7 @@ function collectConfig() {
|
||||
function favoriteTitle(cfg) {
|
||||
if (cfg.mode === 'chart') return cfg.title || '未命名图表';
|
||||
if (cfg.mode === 'table') return cfg.title || '未命名表格';
|
||||
return '多图合并';
|
||||
return cfg.bigTitle || '多图合并';
|
||||
}
|
||||
|
||||
// 点击「⭐ 收藏」:把当前图/表按原始大小保存到收藏区
|
||||
@@ -1723,6 +1949,7 @@ function applyFavoriteConfig(fav) {
|
||||
|
||||
if (mode === 'chart') {
|
||||
document.getElementById('dataInput').value = cfg.data || '';
|
||||
document.getElementById('dataOrientation').value = cfg.rowsAsSeries ? 'rows' : 'columns';
|
||||
document.getElementById('chartType').value = cfg.chartType || 'bar';
|
||||
document.getElementById('chartTitle').value = cfg.title || '';
|
||||
document.getElementById('themeStyle').value = cfg.theme || 'default';
|
||||
@@ -1763,6 +1990,8 @@ function applyFavoriteConfig(fav) {
|
||||
if (Array.isArray(cfg.charts) && cfg.charts.length) {
|
||||
combineCharts = cfg.charts.map(c => ({ ...defaultCombineChart(), ...c }));
|
||||
}
|
||||
document.getElementById('combineBigTitle').value = cfg.bigTitle || '';
|
||||
document.getElementById('combineLegendMode').value = cfg.legendMode || 'own';
|
||||
const dir = document.querySelector(`input[name="combineDirection"][value="${cfg.direction || 'horizontal'}"]`);
|
||||
if (dir) dir.checked = true;
|
||||
document.getElementById('gridCols').value = cfg.gridCols || 2;
|
||||
|
||||
Reference in New Issue
Block a user