From 0e15d43ed4206bf71ad172e224debab10de58324 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Sun, 23 Aug 2026 10:09:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20X=E8=BD=B4=E5=90=8D=E5=8F=B3=E4=BE=A7?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=88=AA=E5=AD=97+=E8=BD=B4=E5=90=8D?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E5=8F=AF=E9=80=89(=E5=8F=B3=E4=BE=A7/?= =?UTF-8?q?=E4=B8=8B=E6=96=B9=E5=B1=85=E4=B8=AD)+=E5=88=BB=E5=BA=A6?= =?UTF-8?q?=E6=96=B9=E5=90=91=E4=B8=8B=E6=8B=8910=C2=B0=E6=A1=A3+=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E6=89=8B=E5=8A=A8=E8=BE=93=E5=85=A5=20v1.17.?= =?UTF-8?q?1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- app.js | 156 ++++++++++++++++++++++++++++++++++++++++++++++++----- index.html | 38 ++++++++++--- 3 files changed, 176 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e130256..984b9a7 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.17.0-green.svg) +![Version](https://img.shields.io/badge/version-1.17.1-green.svg) ## ✨ 功能特性 @@ -14,7 +14,7 @@ - **饼图** - 环形占比展示(第一列=名称,第一系列=数值) - **雷达图** - 多维度对比(第一列=维度,每个系列=一个多边形) - **双Y轴** - 一张图左右两个坐标轴,量度可不同,左右轴可独立命名,系列可指定左轴/右轴且每种类型独立设置(柱状/折线);开启双轴时**图例自动按左右轴分组,放到对应轴边上**(左轴系列图例在左侧、右轴系列图例在右侧,附「左轴/右轴」分组标题),看图一目了然 -- **X轴设置** - 可填写**横坐标轴名**(如:月份);刻度文字方向可调,**默认水平(0°)**,支持倾斜显示且**度数可调(0-90°)**(如45°/90°),解决长类别名重叠问题 +- **X轴设置** - 可填写**横坐标轴名**,**轴名位置可选**(右侧末端 / 轴下方居中),选轴名时自动预留空间不再截字;刻度文字方向**默认水平(0°)**,支持**下拉档位(每10°)** + **手动输入任意度数(0-90°)**,解决长类别名重叠问题 - **数据方向(行=系列)** - 支持"行=系列"布局:第一行是横坐标,每行一个系列,每行可独立设置图表类型(柱状/折线)与左右轴量度(如:营收行=柱状左轴,销量行=折线右轴) ### 📋 表格功能 diff --git a/app.js b/app.js index fb8da8a..3d5956c 100644 --- a/app.js +++ b/app.js @@ -291,8 +291,9 @@ function updateChart() { const dualAxis = document.getElementById('dualAxis').checked; const leftAxisName = document.getElementById('leftAxisName').value; const rightAxisName = document.getElementById('rightAxisName').value; - const xAxisName = document.getElementById('xAxisName').value; - const xLabelRotate = parseInt(document.getElementById('xLabelRotate').value) || 0; + const xAxisName = document.getElementById('xAxisName').value.trim(); + const xAxisNameLoc = document.getElementById('xAxisNameLoc').value; + const xLabelRotate = xLabelRotateValue(); // 饼图/雷达图:走专用构建逻辑(无坐标轴/网格,不支持双轴/堆叠/分割) if (chartType === 'pie' || chartType === 'radar') { @@ -502,6 +503,19 @@ function updateChart() { gridRightCfg = '4%'; } + // X轴名占位:右侧末端需加右 padding,轴下方居中需加下 padding(避免文字被截断) + let gridBottomCfg = '3%'; + if (xAxisName) { + const chartW = chartInstance ? chartInstance.getWidth() : 800; + const nameWidth = measureTextWidth(xAxisName, 12); + if (xAxisNameLoc === 'end') { + const curRight = typeof gridRightCfg === 'number' ? gridRightCfg : 0.04 * chartW; + gridRightCfg = curRight + nameWidth + 26; // nameGap(15) + 边距 + } else if (xAxisNameLoc === 'middle') { + gridBottomCfg = 0.03 * chartW + 32; + } + } + const option = { backgroundColor: bgColor, title: title ? { @@ -539,13 +553,14 @@ function updateChart() { grid: { left: gridLeftCfg, right: gridRightCfg, - bottom: '3%', + bottom: gridBottomCfg, top: dualAxis ? (title ? 52 : 24) : (showLegend ? (title ? 90 : 60) : (title ? 60 : 30)), containLabel: true }, xAxis: { type: 'category', name: xAxisName, + nameLocation: xAxisNameLoc, nameGap: 20, nameTextStyle: { color: textColor, fontSize: 12, fontWeight: 600 }, data: parsedData.categories, @@ -789,7 +804,48 @@ function updateSeriesAxis(origIdx, axis) { updateChart(); } +// ===== X轴刻度文字方向:下拉(10°档) + 自定义手动输入 ===== +function xLabelRotateValue() { + const sel = document.getElementById('xLabelRotate'); + if (sel && sel.value === 'custom') { + return parseInt(document.getElementById('xLabelRotateCustom').value) || 0; + } + return sel ? (parseInt(sel.value) || 0) : 0; +} + +function onXLabelRotateChange() { + const sel = document.getElementById('xLabelRotate'); + const custom = document.getElementById('xLabelRotateCustom'); + if (custom) custom.style.display = (sel && sel.value === 'custom') ? 'block' : 'none'; + updateChart(); +} + +function onXLabelRotateCustomInput() { + const v = parseInt(document.getElementById('xLabelRotateCustom').value) || 0; + const sel = document.getElementById('xLabelRotate'); + const custom = document.getElementById('xLabelRotateCustom'); + // 输入值命中 10° 档位时自动同步下拉显示 + if (v >= 0 && v <= 90 && v % 10 === 0) { + sel.value = String(v); + if (custom) custom.style.display = 'none'; + } else { + sel.value = 'custom'; + if (custom) custom.style.display = 'block'; + } + updateChart(); +} + // ===== 工具函数 ===== +// 测量文字宽度(用于轴名/图例占位计算) +function measureTextWidth(text, fontSize) { + fontSize = fontSize || 12; + if (!text) return 0; + const c = document.createElement('canvas'); + const ctx = c.getContext('2d'); + ctx.font = `${fontSize}px sans-serif`; + return ctx.measureText(text).width; +} + // 测量图例文字宽度(用于双轴侧边图例的 grid 让位) function measureLegendWidth(names, fontSize) { fontSize = fontSize || 12; @@ -1237,7 +1293,7 @@ function buildCombineChartOption(dataText, cfg) { showLegend = true, showGrid = true, showLabel = false, stackMode = false, smoothLine = true, dualYAxis = false, leftAxisName = '', rightAxisName = '', seriesConfig = null, - xAxisName = '', xLabelRotate = 0 + xAxisName = '', xAxisNameLoc = 'end', xLabelRotate = 0 } = cfg; const bgColor = theme === 'dark' ? '#1a1a2e' : '#ffffff'; @@ -1401,6 +1457,18 @@ function buildCombineChartOption(dataText, cfg) { gridRightCfg = '4%'; } + // X轴名占位:右侧末端加右 padding,轴下方居中加下 padding(避免文字被截断) + let gridBottomCfg = '3%'; + if (xAxisName) { + const nameWidth = measureTextWidth(xAxisName, 11); + if (xAxisNameLoc === 'end') { + const curRight = typeof gridRightCfg === 'number' ? gridRightCfg : 0.04 * 800; + gridRightCfg = curRight + nameWidth + 22; + } else if (xAxisNameLoc === 'middle') { + gridBottomCfg = 0.03 * 800 + 30; + } + } + return { backgroundColor: bgColor, title: title ? { @@ -1417,13 +1485,14 @@ function buildCombineChartOption(dataText, cfg) { }, legend: legendCfg, grid: { - left: gridLeftCfg, right: gridRightCfg, bottom: '3%', + left: gridLeftCfg, right: gridRightCfg, bottom: gridBottomCfg, top: dualYAxis ? (title ? 44 : 16) : (showLegend ? (title ? 70 : 45) : (title ? 50 : 30)), containLabel: true }, xAxis: { type: 'category', name: xAxisName, + nameLocation: xAxisNameLoc, nameGap: 16, nameTextStyle: { color: textColor, fontSize: 11, fontWeight: 600 }, data: parsed.categories, @@ -1475,7 +1544,7 @@ function measureCombineChart(dataText, rowsAsSeries) { let combineCharts = []; function defaultCombineChart() { - return { title: '', type: 'bar', theme: 'default', legend: true, grid: true, label: false, stack: false, data: '', rowsAsSeries: false, dualYAxis: false, leftAxisName: '', rightAxisName: '', seriesConfig: [], xAxisName: '', xLabelRotate: 0 }; + return { title: '', type: 'bar', theme: 'default', legend: true, grid: true, label: false, stack: false, data: '', rowsAsSeries: false, dualYAxis: false, leftAxisName: '', rightAxisName: '', seriesConfig: [], xAxisName: '', xAxisNameLoc: 'end', xLabelRotate: 0 }; } function initCombineCharts() { @@ -1543,11 +1612,20 @@ function renderCombineCharts() { ` : ''}
- + +
+ + +
- - · - + + °
@@ -1627,6 +1705,45 @@ function updateCombineSeriesCfg(i, idx, field, value) { generateCombine(); } +// ===== 多图合并:X轴刻度方向(下拉10°档 + 自定义输入) ===== +function combineRotateSelValue(v) { + v = parseInt(v) || 0; + return (v >= 0 && v <= 90 && v % 10 === 0) ? String(v) : 'custom'; +} + +function combineRotateValue(i) { + const sel = document.getElementById('combineRotateSel' + i); + if (!sel) return 0; + if (sel.value === 'custom') { + const custom = document.getElementById('combineRotateCustom' + i); + return parseInt(custom && custom.value) || 0; + } + return parseInt(sel.value) || 0; +} + +function onCombineRotateSel(i, val) { + const custom = document.getElementById('combineRotateCustom' + i); + if (custom) custom.style.display = val === 'custom' ? 'block' : 'none'; + if (combineCharts[i]) { + combineCharts[i].xLabelRotate = combineRotateValue(i); + generateCombine(); + } +} + +function onCombineRotateCustom(i, val) { + const v = parseInt(val) || 0; + const sel = document.getElementById('combineRotateSel' + i); + const custom = document.getElementById('combineRotateCustom' + i); + if (v >= 0 && v <= 90 && v % 10 === 0) { + if (sel) sel.value = String(v); + if (custom) custom.style.display = 'none'; + } else { + if (sel) sel.value = 'custom'; + if (custom) custom.style.display = 'block'; + } + if (combineCharts[i]) { combineCharts[i].xLabelRotate = v; generateCombine(); } +} + // 多图合并:共享图例条目(所有子图系列合并,按名称去重) function buildCombineLegendItems() { const items = []; @@ -1685,7 +1802,7 @@ function generateCombine() { 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 || '', - xAxisName: c.xAxisName || '', xLabelRotate: parseInt(c.xLabelRotate) || 0, + xAxisName: c.xAxisName || '', xAxisNameLoc: c.xAxisNameLoc || 'end', xLabelRotate: parseInt(c.xLabelRotate) || 0, seriesConfig: c.seriesConfig }); if (!option) { reject(new Error('数据格式错误')); return; } @@ -1855,7 +1972,8 @@ function collectChartConfig() { leftAxisName: document.getElementById('leftAxisName').value, rightAxisName: document.getElementById('rightAxisName').value, xAxisName: document.getElementById('xAxisName').value, - xLabelRotate: parseInt(document.getElementById('xLabelRotate').value) || 0, + xAxisNameLoc: document.getElementById('xAxisNameLoc').value, + xLabelRotate: xLabelRotateValue(), enableSplit: document.getElementById('enableSplit').checked, splitIndex: parseInt(document.getElementById('splitIndex').value) || 3, leftLabel: document.getElementById('leftLabel').value, @@ -2063,7 +2181,19 @@ function applyFavoriteConfig(fav) { document.getElementById('leftAxisName').value = cfg.leftAxisName || ''; document.getElementById('rightAxisName').value = cfg.rightAxisName || ''; document.getElementById('xAxisName').value = cfg.xAxisName || ''; - document.getElementById('xLabelRotate').value = cfg.xLabelRotate || 0; + document.getElementById('xAxisNameLoc').value = cfg.xAxisNameLoc || 'end'; + // 刻度方向:命中 10° 档位用下拉,否则用自定义输入 + const rotV = cfg.xLabelRotate || 0; + const rotSel = document.getElementById('xLabelRotate'); + const rotCustom = document.getElementById('xLabelRotateCustom'); + if (rotV >= 0 && rotV <= 90 && rotV % 10 === 0) { + rotSel.value = String(rotV); + rotCustom.style.display = 'none'; + } else { + rotSel.value = 'custom'; + rotCustom.value = rotV; + rotCustom.style.display = 'block'; + } document.getElementById('enableSplit').checked = !!cfg.enableSplit; document.getElementById('splitIndex').value = cfg.splitIndex || 3; document.getElementById('leftLabel').value = cfg.leftLabel || ''; diff --git a/index.html b/index.html index f60b55c..7850bb0 100644 --- a/index.html +++ b/index.html @@ -81,14 +81,38 @@ C, 20, 30, 40
- -
- - · - - ° + +
+ + +
+
+ + +
+
+ +
+ + + ° +
-

轴名可留空不显示;刻度方向:0°=水平(默认),45°=右上倾斜,90°=竖直,实时生效