From 01c0536986f7a8b2a8a5a88fcc82f9f580d1ee69 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Sun, 23 Aug 2026 18:15:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E9=A2=9C=E8=89=B2=E9=BB=98=E8=AE=A4=E5=90=8C=E7=B3=BB=E5=88=97?= =?UTF-8?q?+=E5=8F=AF=E8=87=AA=E5=AE=9A=E4=B9=89=EF=BC=9BY=E8=BD=B4?= =?UTF-8?q?=E6=9C=80=E5=B0=8F/=E6=9C=80=E5=A4=A7=E5=80=BC=E6=89=8B?= =?UTF-8?q?=E5=8A=A8=E6=8C=87=E5=AE=9A=20v1.20.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 数据标签颜色:默认自动取系列颜色(柱状/折线色),也可选自定义颜色手动指定 - Y轴范围:默认自动计算,可手动指定最小/最大值;双Y轴时左/右轴可分别指定 - 主图 + 多图合并 + 服务端API 三处统一;收藏保存/恢复含两项设置 - API 新增参数 labelColorMode/labelColor, yAxisMin/yAxisMax, leftAxisMin/Max, rightAxisMin/Max --- README.md | 4 +- app.js | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++-- index.html | 34 ++++++++++++++++ server.js | 44 +++++++++++++++++++-- 4 files changed, 185 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9f779f0..3e49ed2 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.19.0-green.svg) +![Version](https://img.shields.io/badge/version-1.20.0-green.svg) ## ✨ 功能特性 @@ -15,6 +15,8 @@ - **雷达图** - 多维度对比(第一列=维度,每个系列=一个多边形) - **双Y轴** - 一张图左右两个坐标轴,量度可不同,左右轴可独立命名,系列可指定左轴/右轴且每种类型独立设置(柱状/折线);开启双轴时**图例放在各自轴上方、各自往两边对齐**(左轴系列图例靠左、右轴系列图例靠右,附「左轴/右轴」分组标题),不再占用两侧空白区,绘图区占满整图宽度;**图例图标随系列类型自动变化**(柱状=方块、折线=线条);**左右轴标题显示方式可选**(竖直显示/轴上部横排) - **系列样式细分** - 每个系列可单独设置图表类型与样式:**柱状**分实体/空心/阴影,**折线**分实线/虚线/点线,图例与样式联动 +- **数据标签颜色** - 显示数据标签时颜色**默认同系列颜色**(柱状/折线色),也可手动指定自定义颜色 +- **Y轴范围** - 默认自动计算,也可**手动指定 Y 轴最小/最大值**;双Y轴时左轴、右轴可分别指定范围 - **X轴设置** - 横坐标轴名**默认自动取数据首列字段名**,**默认放在横坐标轴下方居中**(也可选右侧末端);选轴名时自动预留空间不再截字;刻度文字方向**默认水平(0°)**,支持**下拉档位(每10°)** + **手动输入任意度数(0-90°)**,解决长类别名重叠问题 - **数据方向(行=系列)** - 支持"行=系列"布局:第一行是横坐标,每行一个系列,每行可独立设置图表类型(柱状/折线)与左右轴量度(如:营收行=柱状左轴,销量行=折线右轴) diff --git a/app.js b/app.js index 867992a..a8cdd7d 100644 --- a/app.js +++ b/app.js @@ -302,6 +302,25 @@ function updateChart() { const xAxisNameLoc = document.getElementById('xAxisNameLoc').value; const xLabelRotate = xLabelRotateValue(); + // 数据标签颜色:auto=同系列颜色 / custom=手动指定 + const labelColorMode = (document.getElementById('labelColorMode') || {}).value || 'auto'; + const labelColor = (document.getElementById('labelColor') || {}).value || '#333333'; + // Y轴范围(留空=自动) + const yAxisMin = (document.getElementById('yAxisMin') || {}).value; + const yAxisMax = (document.getElementById('yAxisMax') || {}).value; + const leftAxisMin = (document.getElementById('leftAxisMin') || {}).value; + const leftAxisMax = (document.getElementById('leftAxisMax') || {}).value; + const rightAxisMin = (document.getElementById('rightAxisMin') || {}).value; + const rightAxisMax = (document.getElementById('rightAxisMax') || {}).value; + + // 显示/隐藏:数据标签颜色 + 单/双轴范围输入 + const lcGroup = document.getElementById('labelColorGroup'); + if (lcGroup) lcGroup.style.display = showLabel ? 'block' : 'none'; + const singleR = document.getElementById('singleAxisRange'); + const dualR = document.getElementById('dualAxisRange'); + if (singleR) singleR.style.display = dualAxis ? 'none' : 'block'; + if (dualR) dualR.style.display = dualAxis ? 'block' : 'none'; + // 饼图/雷达图:走专用构建逻辑(无坐标轴/网格,不支持双轴/堆叠/分割) if (chartType === 'pie' || chartType === 'radar') { const option = buildPieRadarOption(chartType); @@ -383,13 +402,13 @@ function updateChart() { const styleOv = window.seriesStyleOverrides && window.seriesStyleOverrides[origIdx]; applySeriesStyle(seriesItem, type, color, styleOv); - // 数据标签 + // 数据标签:颜色默认同系列颜色,可手动自定义 if (showLabel) { seriesItem.label = { show: true, position: type === 'bar' ? 'top' : 'top', fontSize: 11, - color: textColor, + color: labelColorMode === 'custom' ? labelColor : color, formatter: (params) => { if (params.value >= 10000) return (params.value / 10000).toFixed(1) + 'w'; if (params.value >= 1000) return (params.value / 1000).toFixed(1) + 'k'; @@ -599,6 +618,8 @@ function updateChart() { { type: 'value', name: leftAxisName || '左轴', + min: axisNum(leftAxisMin), + max: axisNum(leftAxisMax), nameLocation: leftAxisNameLoc === 'top' ? 'end' : 'middle', nameRotate: leftAxisNameLoc === 'top' ? 0 : 90, nameGap: leftAxisNameLoc === 'top' ? 12 : 32, @@ -615,6 +636,8 @@ function updateChart() { type: 'value', name: rightAxisName || '右轴', position: 'right', + min: axisNum(rightAxisMin), + max: axisNum(rightAxisMax), nameLocation: rightAxisNameLoc === 'top' ? 'end' : 'middle', nameRotate: rightAxisNameLoc === 'top' ? 0 : 90, nameGap: rightAxisNameLoc === 'top' ? 12 : 32, @@ -626,6 +649,8 @@ function updateChart() { } ] : { type: 'value', + min: axisNum(yAxisMin), + max: axisNum(yAxisMax), axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: textColor, fontSize: 11 }, @@ -844,6 +869,10 @@ function updateSeriesStyle(origIdx, style) { function onDualAxisChange() { const on = document.getElementById('dualAxis').checked; document.getElementById('rightAxisGroup').style.display = on ? 'block' : 'none'; + const singleR = document.getElementById('singleAxisRange'); + const dualR = document.getElementById('dualAxisRange'); + if (singleR) singleR.style.display = on ? 'none' : 'block'; + if (dualR) dualR.style.display = on ? 'block' : 'none'; renderSeriesConfig(); // 显示/隐藏系列轴选择 updateChart(); } @@ -954,6 +983,20 @@ function setXAxisNameDefault(firstField) { } } +// Y轴 min/max:空值或非法返回 undefined(走 ECharts 自动) +function axisNum(v) { + if (v === '' || v === undefined || v === null) return undefined; + const n = parseFloat(v); + return isNaN(n) ? undefined : n; +} + +// 数据标签颜色模式切换:auto=同系列颜色 / custom=自定义色 +function onLabelColorModeChange() { + const colorInput = document.getElementById('labelColor'); + if (colorInput) colorInput.style.display = document.getElementById('labelColorMode').value === 'custom' ? 'block' : 'none'; + updateChart(); +} + function adjustColor(hex, amount) { hex = hex.replace('#', ''); const num = parseInt(hex, 16); @@ -1390,6 +1433,8 @@ function buildCombineChartOption(dataText, cfg) { stackMode = false, smoothLine = true, dualYAxis = false, leftAxisName = '', rightAxisName = '', seriesConfig = null, leftAxisNameLoc = 'vertical', rightAxisNameLoc = 'vertical', + labelColorMode = 'auto', labelColor = '#333333', + yAxisMin = '', yAxisMax = '', leftAxisMin = '', leftAxisMax = '', rightAxisMin = '', rightAxisMax = '', xAxisName = '', xAxisNameLoc = 'middle', xLabelRotate = 0, width = 600 } = cfg; @@ -1505,7 +1550,7 @@ function buildCombineChartOption(dataText, cfg) { show: true, position: 'top', fontSize: 11, - color: textColor, + color: labelColorMode === 'custom' ? labelColor : color, formatter: (p) => { if (p.value >= 10000) return (p.value / 10000).toFixed(1) + 'w'; if (p.value >= 1000) return (p.value / 1000).toFixed(1) + 'k'; @@ -1612,6 +1657,8 @@ function buildCombineChartOption(dataText, cfg) { { type: 'value', name: leftAxisName || '左轴', + min: axisNum(leftAxisMin), + max: axisNum(leftAxisMax), nameLocation: leftAxisNameLoc === 'top' ? 'end' : 'middle', nameRotate: leftAxisNameLoc === 'top' ? 0 : 90, nameGap: leftAxisNameLoc === 'top' ? 12 : 30, @@ -1625,6 +1672,8 @@ function buildCombineChartOption(dataText, cfg) { type: 'value', name: rightAxisName || '右轴', position: 'right', + min: axisNum(rightAxisMin), + max: axisNum(rightAxisMax), nameLocation: rightAxisNameLoc === 'top' ? 'end' : 'middle', nameRotate: rightAxisNameLoc === 'top' ? 0 : 90, nameGap: rightAxisNameLoc === 'top' ? 12 : 30, @@ -1636,6 +1685,8 @@ function buildCombineChartOption(dataText, cfg) { } ] : { type: 'value', + min: axisNum(yAxisMin), + max: axisNum(yAxisMax), axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: textColor, fontSize: 11 }, @@ -1658,7 +1709,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: '', leftAxisNameLoc: 'vertical', rightAxisNameLoc: 'vertical', seriesConfig: [], xAxisName: '', xAxisNameLoc: 'middle', xLabelRotate: 0 }; + return { title: '', type: 'bar', theme: 'default', legend: true, grid: true, label: false, stack: false, data: '', rowsAsSeries: false, dualYAxis: false, leftAxisName: '', rightAxisName: '', leftAxisNameLoc: 'vertical', rightAxisNameLoc: 'vertical', labelColorMode: 'auto', labelColor: '#333333', yAxisMin: '', yAxisMax: '', leftAxisMin: '', leftAxisMax: '', rightAxisMin: '', rightAxisMax: '', seriesConfig: [], xAxisName: '', xAxisNameLoc: 'middle', xLabelRotate: 0 }; } function initCombineCharts() { @@ -1713,6 +1764,35 @@ function renderCombineCharts() { +
+ +
+ + +
+
+
+ + ${c.dualYAxis ? ` +
+ + ~ + +
+
+ + ~ + +
` : ` +
+ + ~ + +
`} +
@@ -1940,6 +2020,10 @@ function generateCombine() { rowsAsSeries: !!c.rowsAsSeries, dualYAxis: !!c.dualYAxis, leftAxisName: c.leftAxisName || '', rightAxisName: c.rightAxisName || '', leftAxisNameLoc: c.leftAxisNameLoc || 'vertical', rightAxisNameLoc: c.rightAxisNameLoc || 'vertical', + labelColorMode: c.labelColorMode || 'auto', labelColor: c.labelColor || '#333333', + yAxisMin: c.yAxisMin !== undefined ? c.yAxisMin : '', yAxisMax: c.yAxisMax !== undefined ? c.yAxisMax : '', + leftAxisMin: c.leftAxisMin !== undefined ? c.leftAxisMin : '', leftAxisMax: c.leftAxisMax !== undefined ? c.leftAxisMax : '', + rightAxisMin: c.rightAxisMin !== undefined ? c.rightAxisMin : '', rightAxisMax: c.rightAxisMax !== undefined ? c.rightAxisMax : '', xAxisName: c.xAxisName || '', xAxisNameLoc: c.xAxisNameLoc || 'middle', xLabelRotate: parseInt(c.xLabelRotate) || 0, seriesConfig: c.seriesConfig, width: w }); @@ -2111,6 +2195,14 @@ function collectChartConfig() { rightAxisName: document.getElementById('rightAxisName').value, leftAxisNameLoc: (document.getElementById('leftAxisNameLoc') || {}).value || 'vertical', rightAxisNameLoc: (document.getElementById('rightAxisNameLoc') || {}).value || 'vertical', + labelColorMode: (document.getElementById('labelColorMode') || {}).value || 'auto', + labelColor: (document.getElementById('labelColor') || {}).value || '#333333', + yAxisMin: (document.getElementById('yAxisMin') || {}).value || '', + yAxisMax: (document.getElementById('yAxisMax') || {}).value || '', + leftAxisMin: (document.getElementById('leftAxisMin') || {}).value || '', + leftAxisMax: (document.getElementById('leftAxisMax') || {}).value || '', + rightAxisMin: (document.getElementById('rightAxisMin') || {}).value || '', + rightAxisMax: (document.getElementById('rightAxisMax') || {}).value || '', xAxisName: document.getElementById('xAxisName').value, xAxisNameLoc: document.getElementById('xAxisNameLoc').value, xLabelRotate: xLabelRotateValue(), @@ -2325,6 +2417,17 @@ function applyFavoriteConfig(fav) { const rNameLoc = document.getElementById('rightAxisNameLoc'); if (lNameLoc) lNameLoc.value = cfg.leftAxisNameLoc || 'vertical'; if (rNameLoc) rNameLoc.value = cfg.rightAxisNameLoc || 'vertical'; + const lcMode = document.getElementById('labelColorMode'); + const lcColor = document.getElementById('labelColor'); + if (lcMode) lcMode.value = cfg.labelColorMode || 'auto'; + if (lcColor) lcColor.value = cfg.labelColor || '#333333'; + if (lcColor) lcColor.style.display = (cfg.labelColorMode || 'auto') === 'custom' ? 'block' : 'none'; + document.getElementById('yAxisMin').value = cfg.yAxisMin || ''; + document.getElementById('yAxisMax').value = cfg.yAxisMax || ''; + document.getElementById('leftAxisMin').value = cfg.leftAxisMin || ''; + document.getElementById('leftAxisMax').value = cfg.leftAxisMax || ''; + document.getElementById('rightAxisMin').value = cfg.rightAxisMin || ''; + document.getElementById('rightAxisMax').value = cfg.rightAxisMax || ''; document.getElementById('xAxisName').value = cfg.xAxisName || ''; document.getElementById('xAxisNameLoc').value = cfg.xAxisNameLoc || 'middle'; xAxisNameTouched = true; // 收藏恢复时尊重已存轴名,不再自动覆盖 diff --git a/index.html b/index.html index 30eaa57..a67478d 100644 --- a/index.html +++ b/index.html @@ -155,6 +155,40 @@ C, 20, 30, 40 + + +
+ +
+
+ + ~ + +
+
+ +
+