diff --git a/API.md b/API.md index 614a02d..26c6733 100644 --- a/API.md +++ b/API.md @@ -50,6 +50,8 @@ Content-Type: application/json | `leftAxisName` | string | — | `""` | 左轴名称(如 `"销售额(元)"`) | | `rightAxisName` | string | — | `""` | 右轴名称(如 `"增长率(%)"`) | | `seriesTypes` | array | — | `null` | 每系列图表类型,按系列顺序对应,如 `["bar","line"]`(`bar`/`line`/`auto`) | +| `seriesAxis` | array | — | `null` | 每系列坐标轴,按系列顺序对应,如 `[0,1]`(`0`=左轴 `1`=右轴,配合 `dualYAxis`;优先于 `rightAxisSeries`) | +| `rowsAsSeries` | boolean | — | `false` | 数据方向:`false`=列=系列(第一列是横坐标,每列一个系列);`true`=行=系列(第一行是横坐标,每行一个系列) | | `width` | number | — | `800` | 图片宽度(px) | | `height` | number | — | `500` | 图片高度(px) | | `pixelRatio` | number | — | `2` | 像素倍率(越大越清晰) | @@ -178,10 +180,33 @@ Content-Type: application/json | `gap` | number | 否 | 图间距(默认 24px) | | `pixelRatio` | number | 否 | 像素倍率,默认 2(越清晰文件越大) | | `background` | string | 否 | 背景色,默认 `#ffffff` | +| `bigTitle` | string | 否 | 整张图片的大标题(可空),居中显示在顶部 | +| `legendMode` | string | 否 | 图例方式:`own` 每个小图各自图例(默认)/ `shared-top` 共用一套图例放顶部 / `shared-bottom` 共用一套图例放底部(共用时各子图隐藏自己的图例,系列合并去重) | +| `theme` | string | 否 | 大标题/共享图例文字颜色所属主题,默认 `default`(深色用 `dark`) | > 兼容旧参数:`chart1` + `chart2` 仍可传(等价于 `charts: [chart1, chart2]`)。 -每个子图配置支持:`data`(CSV)、`chartType`(bar/line/bar-line/pie/radar)、`title`、`theme`、`showLegend`、`showGrid`、`showLabel`、`stackMode`、`smoothLine`、`width`、`height`。 +每个子图配置支持:`data`(CSV)、`chartType`(bar/line/bar-line/pie/radar)、`title`、`theme`、`showLegend`、`showGrid`、`showLabel`、`stackMode`、`smoothLine`、`width`、`height`,以及**数据方向与多类型双轴**:`rowsAsSeries`(行=系列)、`dualYAxis`、`leftAxisName`、`rightAxisName`、`seriesTypes`、`seriesAxis`。 + +### 高级示例(大标题 + 底部共享图例 + 行=系列 + 柱状/折线双轴) + +```bash +curl -X POST http://127.0.0.1:16016/api/combine \ + -H "Content-Type: application/json" \ + -d '{ + "bigTitle": "经营总览大图", + "legendMode": "shared-bottom", + "direction": "grid", + "cols": 2, + "charts": [ + {"data": "指标, 2023, 2024, 2025\n营收(元), 1200, 1800, 2100\n销量(个), 100, 150, 130", + "rowsAsSeries": true, "chartType": "bar", "title": "营收与销量", + "dualYAxis": true, "leftAxisName": "元", "rightAxisName": "个", + "seriesTypes": ["bar","line"], "seriesAxis": [0,1]}, + {"data": "产品, Q1, Q2\n手机, 1200, 1800\n平板, 800, 950", "chartType": "bar", "title": "季度销售"} + ] + }' -o combine.png +``` ### curl 示例 @@ -248,7 +273,7 @@ curl http://192.168.0.101:16016/api/health { "status": "ok", "service": "data-chart-tool", - "version": "1.1.0", + "version": "1.16.0", "endpoints": { "POST /api/chart": "生成图表图片(JSON body)", "GET /api/chart": "生成图表图片(URL 参数)", diff --git a/README.md b/README.md index 20aca59..ac5f119 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 一个简洁强大的数据可视化工具,支持 **Web UI** 和 **API** 两种方式生成精美的对比图表和表格图片。 ![License](https://img.shields.io/badge/license-MIT-blue.svg) -![Version](https://img.shields.io/badge/version-1.8.0-green.svg) +![Version](https://img.shields.io/badge/version-1.16.0-green.svg) ## ✨ 功能特性 @@ -14,6 +14,7 @@ - **饼图** - 环形占比展示(第一列=名称,第一系列=数值) - **雷达图** - 多维度对比(第一列=维度,每个系列=一个多边形) - **双Y轴** - 一张图左右两个坐标轴,量度可不同,左右轴可独立命名,系列可指定左轴/右轴且每种类型独立设置(柱状/折线) +- **数据方向(行=系列)** - 支持"行=系列"布局:第一行是横坐标,每行一个系列,每行可独立设置图表类型(柱状/折线)与左右轴量度(如:营收行=柱状左轴,销量行=折线右轴) ### 📋 表格功能 - **表格图片生成** - 根据数据生成精美的表格图片 @@ -35,15 +36,17 @@ ### 🖼️ 多图合并 - 将多张图表合并到一张图片中,适合对比/汇总场景,**默认2张,可一键不断添加**(支持删除) - 支持**横排(单行)**、**竖排(单列)**、**多行多列网格**(可设每行列数),一键切换实时预览 -- 每张图可独立配置:数据、标题、图表类型、主题、图例/网格/标签/堆叠 +- 每张图可独立配置:数据、标题、图表类型、主题、图例/网格/标签/堆叠,以及**数据方向(行=系列)**、**双Y轴(不同左右量度)**、**每系列独立图表类型与坐标轴** +- **图片大标题** - 可编写整张大图的标题,居中显示在顶部(可留空) +- **共用图例** - 图例方式可选:每个小图各自图例 / 共用一套图例放顶部 / 共用一套图例放底部;共用时所有小图的系列合并去重,同名系列只显示一次,各小图隐藏自己的图例 - 支持导出 PNG ### 📡 API 接口 -- **POST /api/chart** - JSON 请求体生成图表(完整参数支持) +- **POST /api/chart** - JSON 请求体生成图表(完整参数支持,含 rowsAsSeries/seriesTypes/seriesAxis/dualYAxis) - **GET /api/chart** - URL 参数生成图表(简单场景) - **POST /api/table** - JSON 请求体生成表格图片 - **GET /api/table** - URL 参数生成表格图片 -- **POST /api/combine** - 多图合并(横排/竖排)生成一张图片 +- **POST /api/combine** - 多图合并(横排/竖排/网格)生成一张图片,支持 bigTitle(大标题)与 legendMode(own/shared-top/shared-bottom 共享图例) - 返回 PNG 图片,支持自定义分辨率和像素倍率 ### 📥 导出功能 diff --git a/app.js b/app.js index ec0f098..9d590be 100644 --- a/app.js +++ b/app.js @@ -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 = '

生成图表后可在此调整各系列的顺序和颜色

'; } +// 数据方向切换(列=系列 / 行=系列):重新解析并重建系列配置 +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() { + +
+ +
+ ${c.dualYAxis ? ` +
+ +
+ + / + +
+
` : ''} +
+ +
${renderCombineSeriesConfigHTML(i)}
+
@@ -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 '

输入数据后自动显示各系列的图表类型/坐标轴

'; + syncCombineSeriesConfig(i); + const p = parseData(c.data, !!c.rowsAsSeries); + if (!p || !p.seriesNames.length) return '

输入数据后自动显示各系列的图表类型/坐标轴

'; + const dual = !!c.dualYAxis; + return p.seriesNames.map((name, idx) => { + const sc = (c.seriesConfig && c.seriesConfig[idx]) || { name, type: 'auto', axis: 0 }; + return `
+ ${escHtml(name)} + + ${dual ? `` : ''} +
`; + }).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 = '

正在合并...

'; 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; diff --git a/index.html b/index.html index 402c806..a32c951 100644 --- a/index.html +++ b/index.html @@ -66,6 +66,15 @@ C, 20, 30, 40
+
+ + +

「行=系列」示例:
指标, 2023, 2024, 2025
营收(元), 1200, 1800, 2100
销量(个), 100, 150, 130
—— 每行可单独设柱状/折线 + 左右轴

+
+
@@ -284,6 +293,21 @@ C, 20, 30, 40