Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a5f0b6273 | ||
|
|
32db3644b8 | ||
|
|
01c0536986 |
@@ -3,7 +3,7 @@
|
|||||||
一个简洁强大的数据可视化工具,支持 **Web UI** 和 **API** 两种方式生成精美的对比图表和表格图片。
|
一个简洁强大的数据可视化工具,支持 **Web UI** 和 **API** 两种方式生成精美的对比图表和表格图片。
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## ✨ 功能特性
|
## ✨ 功能特性
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
- **雷达图** - 多维度对比(第一列=维度,每个系列=一个多边形)
|
- **雷达图** - 多维度对比(第一列=维度,每个系列=一个多边形)
|
||||||
- **双Y轴** - 一张图左右两个坐标轴,量度可不同,左右轴可独立命名,系列可指定左轴/右轴且每种类型独立设置(柱状/折线);开启双轴时**图例放在各自轴上方、各自往两边对齐**(左轴系列图例靠左、右轴系列图例靠右,附「左轴/右轴」分组标题),不再占用两侧空白区,绘图区占满整图宽度;**图例图标随系列类型自动变化**(柱状=方块、折线=线条);**左右轴标题显示方式可选**(竖直显示/轴上部横排)
|
- **双Y轴** - 一张图左右两个坐标轴,量度可不同,左右轴可独立命名,系列可指定左轴/右轴且每种类型独立设置(柱状/折线);开启双轴时**图例放在各自轴上方、各自往两边对齐**(左轴系列图例靠左、右轴系列图例靠右,附「左轴/右轴」分组标题),不再占用两侧空白区,绘图区占满整图宽度;**图例图标随系列类型自动变化**(柱状=方块、折线=线条);**左右轴标题显示方式可选**(竖直显示/轴上部横排)
|
||||||
- **系列样式细分** - 每个系列可单独设置图表类型与样式:**柱状**分实体/空心/阴影,**折线**分实线/虚线/点线,图例与样式联动
|
- **系列样式细分** - 每个系列可单独设置图表类型与样式:**柱状**分实体/空心/阴影,**折线**分实线/虚线/点线,图例与样式联动
|
||||||
|
- **数据标签颜色** - 显示数据标签时颜色**默认同系列颜色**(柱状/折线色),也可手动指定自定义颜色
|
||||||
|
- **Y轴范围** - 默认自动计算,也可**手动指定 Y 轴最小/最大值**;双Y轴时左轴、右轴可分别指定范围
|
||||||
- **X轴设置** - 横坐标轴名**默认自动取数据首列字段名**,**默认放在横坐标轴下方居中**(也可选右侧末端);选轴名时自动预留空间不再截字;刻度文字方向**默认水平(0°)**,支持**下拉档位(每10°)** + **手动输入任意度数(0-90°)**,解决长类别名重叠问题
|
- **X轴设置** - 横坐标轴名**默认自动取数据首列字段名**,**默认放在横坐标轴下方居中**(也可选右侧末端);选轴名时自动预留空间不再截字;刻度文字方向**默认水平(0°)**,支持**下拉档位(每10°)** + **手动输入任意度数(0-90°)**,解决长类别名重叠问题
|
||||||
- **数据方向(行=系列)** - 支持"行=系列"布局:第一行是横坐标,每行一个系列,每行可独立设置图表类型(柱状/折线)与左右轴量度(如:营收行=柱状左轴,销量行=折线右轴)
|
- **数据方向(行=系列)** - 支持"行=系列"布局:第一行是横坐标,每行一个系列,每行可独立设置图表类型(柱状/折线)与左右轴量度(如:营收行=柱状左轴,销量行=折线右轴)
|
||||||
|
|
||||||
|
|||||||
@@ -302,6 +302,25 @@ function updateChart() {
|
|||||||
const xAxisNameLoc = document.getElementById('xAxisNameLoc').value;
|
const xAxisNameLoc = document.getElementById('xAxisNameLoc').value;
|
||||||
const xLabelRotate = xLabelRotateValue();
|
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') {
|
if (chartType === 'pie' || chartType === 'radar') {
|
||||||
const option = buildPieRadarOption(chartType);
|
const option = buildPieRadarOption(chartType);
|
||||||
@@ -383,13 +402,13 @@ function updateChart() {
|
|||||||
const styleOv = window.seriesStyleOverrides && window.seriesStyleOverrides[origIdx];
|
const styleOv = window.seriesStyleOverrides && window.seriesStyleOverrides[origIdx];
|
||||||
applySeriesStyle(seriesItem, type, color, styleOv);
|
applySeriesStyle(seriesItem, type, color, styleOv);
|
||||||
|
|
||||||
// 数据标签
|
// 数据标签:颜色默认同系列颜色,可手动自定义
|
||||||
if (showLabel) {
|
if (showLabel) {
|
||||||
seriesItem.label = {
|
seriesItem.label = {
|
||||||
show: true,
|
show: true,
|
||||||
position: type === 'bar' ? 'top' : 'top',
|
position: type === 'bar' ? 'top' : 'top',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: textColor,
|
color: labelColorMode === 'custom' ? labelColor : color,
|
||||||
formatter: (params) => {
|
formatter: (params) => {
|
||||||
if (params.value >= 10000) return (params.value / 10000).toFixed(1) + 'w';
|
if (params.value >= 10000) return (params.value / 10000).toFixed(1) + 'w';
|
||||||
if (params.value >= 1000) return (params.value / 1000).toFixed(1) + 'k';
|
if (params.value >= 1000) return (params.value / 1000).toFixed(1) + 'k';
|
||||||
@@ -599,6 +618,8 @@ function updateChart() {
|
|||||||
{
|
{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
name: leftAxisName || '左轴',
|
name: leftAxisName || '左轴',
|
||||||
|
min: axisNum(leftAxisMin),
|
||||||
|
max: axisNum(leftAxisMax),
|
||||||
nameLocation: leftAxisNameLoc === 'top' ? 'end' : 'middle',
|
nameLocation: leftAxisNameLoc === 'top' ? 'end' : 'middle',
|
||||||
nameRotate: leftAxisNameLoc === 'top' ? 0 : 90,
|
nameRotate: leftAxisNameLoc === 'top' ? 0 : 90,
|
||||||
nameGap: leftAxisNameLoc === 'top' ? 12 : 32,
|
nameGap: leftAxisNameLoc === 'top' ? 12 : 32,
|
||||||
@@ -615,6 +636,8 @@ function updateChart() {
|
|||||||
type: 'value',
|
type: 'value',
|
||||||
name: rightAxisName || '右轴',
|
name: rightAxisName || '右轴',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
|
min: axisNum(rightAxisMin),
|
||||||
|
max: axisNum(rightAxisMax),
|
||||||
nameLocation: rightAxisNameLoc === 'top' ? 'end' : 'middle',
|
nameLocation: rightAxisNameLoc === 'top' ? 'end' : 'middle',
|
||||||
nameRotate: rightAxisNameLoc === 'top' ? 0 : 90,
|
nameRotate: rightAxisNameLoc === 'top' ? 0 : 90,
|
||||||
nameGap: rightAxisNameLoc === 'top' ? 12 : 32,
|
nameGap: rightAxisNameLoc === 'top' ? 12 : 32,
|
||||||
@@ -626,6 +649,8 @@ function updateChart() {
|
|||||||
}
|
}
|
||||||
] : {
|
] : {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
min: axisNum(yAxisMin),
|
||||||
|
max: axisNum(yAxisMax),
|
||||||
axisLine: { show: false },
|
axisLine: { show: false },
|
||||||
axisTick: { show: false },
|
axisTick: { show: false },
|
||||||
axisLabel: { color: textColor, fontSize: 11 },
|
axisLabel: { color: textColor, fontSize: 11 },
|
||||||
@@ -844,6 +869,10 @@ function updateSeriesStyle(origIdx, style) {
|
|||||||
function onDualAxisChange() {
|
function onDualAxisChange() {
|
||||||
const on = document.getElementById('dualAxis').checked;
|
const on = document.getElementById('dualAxis').checked;
|
||||||
document.getElementById('rightAxisGroup').style.display = on ? 'block' : 'none';
|
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(); // 显示/隐藏系列轴选择
|
renderSeriesConfig(); // 显示/隐藏系列轴选择
|
||||||
updateChart();
|
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) {
|
function adjustColor(hex, amount) {
|
||||||
hex = hex.replace('#', '');
|
hex = hex.replace('#', '');
|
||||||
const num = parseInt(hex, 16);
|
const num = parseInt(hex, 16);
|
||||||
@@ -1390,6 +1433,8 @@ function buildCombineChartOption(dataText, cfg) {
|
|||||||
stackMode = false, smoothLine = true,
|
stackMode = false, smoothLine = true,
|
||||||
dualYAxis = false, leftAxisName = '', rightAxisName = '', seriesConfig = null,
|
dualYAxis = false, leftAxisName = '', rightAxisName = '', seriesConfig = null,
|
||||||
leftAxisNameLoc = 'vertical', rightAxisNameLoc = 'vertical',
|
leftAxisNameLoc = 'vertical', rightAxisNameLoc = 'vertical',
|
||||||
|
labelColorMode = 'auto', labelColor = '#333333',
|
||||||
|
yAxisMin = '', yAxisMax = '', leftAxisMin = '', leftAxisMax = '', rightAxisMin = '', rightAxisMax = '',
|
||||||
xAxisName = '', xAxisNameLoc = 'middle', xLabelRotate = 0, width = 600
|
xAxisName = '', xAxisNameLoc = 'middle', xLabelRotate = 0, width = 600
|
||||||
} = cfg;
|
} = cfg;
|
||||||
|
|
||||||
@@ -1505,7 +1550,7 @@ function buildCombineChartOption(dataText, cfg) {
|
|||||||
show: true,
|
show: true,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: textColor,
|
color: labelColorMode === 'custom' ? labelColor : color,
|
||||||
formatter: (p) => {
|
formatter: (p) => {
|
||||||
if (p.value >= 10000) return (p.value / 10000).toFixed(1) + 'w';
|
if (p.value >= 10000) return (p.value / 10000).toFixed(1) + 'w';
|
||||||
if (p.value >= 1000) return (p.value / 1000).toFixed(1) + 'k';
|
if (p.value >= 1000) return (p.value / 1000).toFixed(1) + 'k';
|
||||||
@@ -1612,6 +1657,8 @@ function buildCombineChartOption(dataText, cfg) {
|
|||||||
{
|
{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
name: leftAxisName || '左轴',
|
name: leftAxisName || '左轴',
|
||||||
|
min: axisNum(leftAxisMin),
|
||||||
|
max: axisNum(leftAxisMax),
|
||||||
nameLocation: leftAxisNameLoc === 'top' ? 'end' : 'middle',
|
nameLocation: leftAxisNameLoc === 'top' ? 'end' : 'middle',
|
||||||
nameRotate: leftAxisNameLoc === 'top' ? 0 : 90,
|
nameRotate: leftAxisNameLoc === 'top' ? 0 : 90,
|
||||||
nameGap: leftAxisNameLoc === 'top' ? 12 : 30,
|
nameGap: leftAxisNameLoc === 'top' ? 12 : 30,
|
||||||
@@ -1625,6 +1672,8 @@ function buildCombineChartOption(dataText, cfg) {
|
|||||||
type: 'value',
|
type: 'value',
|
||||||
name: rightAxisName || '右轴',
|
name: rightAxisName || '右轴',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
|
min: axisNum(rightAxisMin),
|
||||||
|
max: axisNum(rightAxisMax),
|
||||||
nameLocation: rightAxisNameLoc === 'top' ? 'end' : 'middle',
|
nameLocation: rightAxisNameLoc === 'top' ? 'end' : 'middle',
|
||||||
nameRotate: rightAxisNameLoc === 'top' ? 0 : 90,
|
nameRotate: rightAxisNameLoc === 'top' ? 0 : 90,
|
||||||
nameGap: rightAxisNameLoc === 'top' ? 12 : 30,
|
nameGap: rightAxisNameLoc === 'top' ? 12 : 30,
|
||||||
@@ -1636,6 +1685,8 @@ function buildCombineChartOption(dataText, cfg) {
|
|||||||
}
|
}
|
||||||
] : {
|
] : {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
min: axisNum(yAxisMin),
|
||||||
|
max: axisNum(yAxisMax),
|
||||||
axisLine: { show: false },
|
axisLine: { show: false },
|
||||||
axisTick: { show: false },
|
axisTick: { show: false },
|
||||||
axisLabel: { color: textColor, fontSize: 11 },
|
axisLabel: { color: textColor, fontSize: 11 },
|
||||||
@@ -1658,7 +1709,7 @@ function measureCombineChart(dataText, rowsAsSeries) {
|
|||||||
let combineCharts = [];
|
let combineCharts = [];
|
||||||
|
|
||||||
function defaultCombineChart() {
|
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() {
|
function initCombineCharts() {
|
||||||
@@ -1713,6 +1764,35 @@ function renderCombineCharts() {
|
|||||||
<label><input type="checkbox" ${c.rowsAsSeries ? 'checked' : ''} onchange="updateCombineChart(${i},'rowsAsSeries',this.checked)"> 行=系列</label>
|
<label><input type="checkbox" ${c.rowsAsSeries ? 'checked' : ''} onchange="updateCombineChart(${i},'rowsAsSeries',this.checked)"> 行=系列</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="config-group">
|
||||||
|
<label>数据标签颜色(默认同系列颜色)</label>
|
||||||
|
<div class="res-input-row">
|
||||||
|
<select onchange="updateCombineChart(${i},'labelColorMode',this.value)" style="flex:1">
|
||||||
|
<option value="auto" ${(c.labelColorMode || 'auto') === 'auto' ? 'selected' : ''}>自动(同系列颜色)</option>
|
||||||
|
<option value="custom" ${c.labelColorMode === 'custom' ? 'selected' : ''}>自定义…</option>
|
||||||
|
</select>
|
||||||
|
<input type="color" value="${c.labelColor || '#333333'}" style="width:44px;height:32px" onchange="updateCombineChart(${i},'labelColor',this.value)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-group">
|
||||||
|
<label>Y轴范围(留空=自动)</label>
|
||||||
|
${c.dualYAxis ? `
|
||||||
|
<div class="res-input-row">
|
||||||
|
<input type="number" value="${escHtml(c.leftAxisMin)}" placeholder="左轴最小" oninput="updateCombineChart(${i},'leftAxisMin',this.value)">
|
||||||
|
<span class="res-times">~</span>
|
||||||
|
<input type="number" value="${escHtml(c.leftAxisMax)}" placeholder="左轴最大" oninput="updateCombineChart(${i},'leftAxisMax',this.value)">
|
||||||
|
</div>
|
||||||
|
<div class="res-input-row" style="margin-top:6px">
|
||||||
|
<input type="number" value="${escHtml(c.rightAxisMin)}" placeholder="右轴最小" oninput="updateCombineChart(${i},'rightAxisMin',this.value)">
|
||||||
|
<span class="res-times">~</span>
|
||||||
|
<input type="number" value="${escHtml(c.rightAxisMax)}" placeholder="右轴最大" oninput="updateCombineChart(${i},'rightAxisMax',this.value)">
|
||||||
|
</div>` : `
|
||||||
|
<div class="res-input-row">
|
||||||
|
<input type="number" value="${escHtml(c.yAxisMin)}" placeholder="最小值" oninput="updateCombineChart(${i},'yAxisMin',this.value)">
|
||||||
|
<span class="res-times">~</span>
|
||||||
|
<input type="number" value="${escHtml(c.yAxisMax)}" placeholder="最大值" oninput="updateCombineChart(${i},'yAxisMax',this.value)">
|
||||||
|
</div>`}
|
||||||
|
</div>
|
||||||
<div class="config-group">
|
<div class="config-group">
|
||||||
<label><input type="checkbox" ${c.dualYAxis ? 'checked' : ''} onchange="updateCombineChart(${i},'dualYAxis',this.checked)"> 双Y轴(不同左右量度)</label>
|
<label><input type="checkbox" ${c.dualYAxis ? 'checked' : ''} onchange="updateCombineChart(${i},'dualYAxis',this.checked)"> 双Y轴(不同左右量度)</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -1940,6 +2020,10 @@ function generateCombine() {
|
|||||||
rowsAsSeries: !!c.rowsAsSeries,
|
rowsAsSeries: !!c.rowsAsSeries,
|
||||||
dualYAxis: !!c.dualYAxis, leftAxisName: c.leftAxisName || '', rightAxisName: c.rightAxisName || '',
|
dualYAxis: !!c.dualYAxis, leftAxisName: c.leftAxisName || '', rightAxisName: c.rightAxisName || '',
|
||||||
leftAxisNameLoc: c.leftAxisNameLoc || 'vertical', rightAxisNameLoc: c.rightAxisNameLoc || 'vertical',
|
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,
|
xAxisName: c.xAxisName || '', xAxisNameLoc: c.xAxisNameLoc || 'middle', xLabelRotate: parseInt(c.xLabelRotate) || 0,
|
||||||
seriesConfig: c.seriesConfig, width: w
|
seriesConfig: c.seriesConfig, width: w
|
||||||
});
|
});
|
||||||
@@ -2111,6 +2195,14 @@ function collectChartConfig() {
|
|||||||
rightAxisName: document.getElementById('rightAxisName').value,
|
rightAxisName: document.getElementById('rightAxisName').value,
|
||||||
leftAxisNameLoc: (document.getElementById('leftAxisNameLoc') || {}).value || 'vertical',
|
leftAxisNameLoc: (document.getElementById('leftAxisNameLoc') || {}).value || 'vertical',
|
||||||
rightAxisNameLoc: (document.getElementById('rightAxisNameLoc') || {}).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,
|
xAxisName: document.getElementById('xAxisName').value,
|
||||||
xAxisNameLoc: document.getElementById('xAxisNameLoc').value,
|
xAxisNameLoc: document.getElementById('xAxisNameLoc').value,
|
||||||
xLabelRotate: xLabelRotateValue(),
|
xLabelRotate: xLabelRotateValue(),
|
||||||
@@ -2325,6 +2417,17 @@ function applyFavoriteConfig(fav) {
|
|||||||
const rNameLoc = document.getElementById('rightAxisNameLoc');
|
const rNameLoc = document.getElementById('rightAxisNameLoc');
|
||||||
if (lNameLoc) lNameLoc.value = cfg.leftAxisNameLoc || 'vertical';
|
if (lNameLoc) lNameLoc.value = cfg.leftAxisNameLoc || 'vertical';
|
||||||
if (rNameLoc) rNameLoc.value = cfg.rightAxisNameLoc || '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('xAxisName').value = cfg.xAxisName || '';
|
||||||
document.getElementById('xAxisNameLoc').value = cfg.xAxisNameLoc || 'middle';
|
document.getElementById('xAxisNameLoc').value = cfg.xAxisNameLoc || 'middle';
|
||||||
xAxisNameTouched = true; // 收藏恢复时尊重已存轴名,不再自动覆盖
|
xAxisNameTouched = true; // 收藏恢复时尊重已存轴名,不再自动覆盖
|
||||||
|
|||||||
+34
@@ -155,6 +155,40 @@ C, 20, 30, 40</pre>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group" id="labelColorGroup" style="display:none;">
|
||||||
|
<label>数据标签颜色(默认同系列颜色)</label>
|
||||||
|
<div class="res-input-row">
|
||||||
|
<select id="labelColorMode" onchange="onLabelColorModeChange()" style="flex:1">
|
||||||
|
<option value="auto" selected>自动(同系列颜色)</option>
|
||||||
|
<option value="custom">自定义…</option>
|
||||||
|
</select>
|
||||||
|
<input type="color" id="labelColor" value="#333333" style="width:44px;height:32px;display:none" onchange="updateChart()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group">
|
||||||
|
<label>Y轴范围(留空=自动)</label>
|
||||||
|
<div id="singleAxisRange">
|
||||||
|
<div class="res-input-row">
|
||||||
|
<input type="number" id="yAxisMin" placeholder="最小值" oninput="updateChart()">
|
||||||
|
<span class="res-times">~</span>
|
||||||
|
<input type="number" id="yAxisMax" placeholder="最大值" oninput="updateChart()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="dualAxisRange" style="display:none;">
|
||||||
|
<div class="res-input-row">
|
||||||
|
<input type="number" id="leftAxisMin" placeholder="左轴最小" oninput="updateChart()">
|
||||||
|
<span class="res-times">~</span>
|
||||||
|
<input type="number" id="leftAxisMax" placeholder="左轴最大" oninput="updateChart()">
|
||||||
|
</div>
|
||||||
|
<div class="res-input-row" style="margin-top:8px">
|
||||||
|
<input type="number" id="rightAxisMin" placeholder="右轴最小" oninput="updateChart()">
|
||||||
|
<span class="res-times">~</span>
|
||||||
|
<input type="number" id="rightAxisMax" placeholder="右轴最大" oninput="updateChart()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="config-group" id="rightAxisGroup" style="display:none;">
|
<div class="config-group" id="rightAxisGroup" style="display:none;">
|
||||||
<label>左右轴名称(量度可不同)</label>
|
<label>左右轴名称(量度可不同)</label>
|
||||||
<div class="res-input-row">
|
<div class="res-input-row">
|
||||||
|
|||||||
@@ -152,6 +152,13 @@ function applySeriesStyle(seriesItem, type, color, style) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
function buildChartOption(params) {
|
function buildChartOption(params) {
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -178,6 +185,17 @@ function buildChartOption(params) {
|
|||||||
seriesStyles = null,
|
seriesStyles = null,
|
||||||
leftAxisNameLoc = 'vertical',
|
leftAxisNameLoc = 'vertical',
|
||||||
rightAxisNameLoc = 'vertical',
|
rightAxisNameLoc = 'vertical',
|
||||||
|
labelColorMode = 'auto',
|
||||||
|
labelColor = '#333333',
|
||||||
|
yAxisMin = '',
|
||||||
|
yAxisMax = '',
|
||||||
|
leftAxisMin = '',
|
||||||
|
leftAxisMax = '',
|
||||||
|
rightAxisMin = '',
|
||||||
|
rightAxisMax = '',
|
||||||
|
xLabelRotate = '',
|
||||||
|
xLabelInterval = '',
|
||||||
|
xLabelFontSize = 0,
|
||||||
width = 800
|
width = 800
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
@@ -315,12 +333,13 @@ function buildChartOption(params) {
|
|||||||
const st = Array.isArray(seriesStyles) ? seriesStyles[idx] : null;
|
const st = Array.isArray(seriesStyles) ? seriesStyles[idx] : null;
|
||||||
applySeriesStyle(seriesItem, type, color, st);
|
applySeriesStyle(seriesItem, type, color, st);
|
||||||
|
|
||||||
|
// 数据标签:颜色默认同系列颜色,可手动自定义
|
||||||
if (showLabel) {
|
if (showLabel) {
|
||||||
seriesItem.label = {
|
seriesItem.label = {
|
||||||
show: true,
|
show: true,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: textColor,
|
color: labelColorMode === 'custom' ? labelColor : color,
|
||||||
formatter: (p) => {
|
formatter: (p) => {
|
||||||
if (p.value >= 10000) return (p.value / 10000).toFixed(1) + 'w';
|
if (p.value >= 10000) return (p.value / 10000).toFixed(1) + 'w';
|
||||||
if (p.value >= 1000) return (p.value / 1000).toFixed(1) + 'k';
|
if (p.value >= 1000) return (p.value / 1000).toFixed(1) + 'k';
|
||||||
@@ -460,9 +479,11 @@ function buildChartOption(params) {
|
|||||||
axisLine: { lineStyle: { color: axisLineColor } },
|
axisLine: { lineStyle: { color: axisLineColor } },
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: textColor,
|
color: textColor,
|
||||||
fontSize: 12,
|
fontSize: xLabelFontSize > 0 ? xLabelFontSize : 12,
|
||||||
interval: 0,
|
interval: xLabelInterval === '' ? 0 : Number(xLabelInterval),
|
||||||
rotate: parsedData.categories.length > 10 ? 30 : 0
|
rotate: xLabelRotate === ''
|
||||||
|
? (parsedData.categories.length > 10 ? 30 : 0)
|
||||||
|
: Number(xLabelRotate)
|
||||||
},
|
},
|
||||||
axisTick: { show: false }
|
axisTick: { show: false }
|
||||||
},
|
},
|
||||||
@@ -470,6 +491,8 @@ function buildChartOption(params) {
|
|||||||
{
|
{
|
||||||
type: 'value',
|
type: 'value',
|
||||||
name: leftAxisName || '左轴',
|
name: leftAxisName || '左轴',
|
||||||
|
min: axisNum(leftAxisMin),
|
||||||
|
max: axisNum(leftAxisMax),
|
||||||
nameLocation: leftAxisNameLoc === 'top' ? 'end' : 'middle',
|
nameLocation: leftAxisNameLoc === 'top' ? 'end' : 'middle',
|
||||||
nameRotate: leftAxisNameLoc === 'top' ? 0 : 90,
|
nameRotate: leftAxisNameLoc === 'top' ? 0 : 90,
|
||||||
nameGap: leftAxisNameLoc === 'top' ? 12 : 32,
|
nameGap: leftAxisNameLoc === 'top' ? 12 : 32,
|
||||||
@@ -489,6 +512,8 @@ function buildChartOption(params) {
|
|||||||
type: 'value',
|
type: 'value',
|
||||||
name: rightAxisName || '右轴',
|
name: rightAxisName || '右轴',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
|
min: axisNum(rightAxisMin),
|
||||||
|
max: axisNum(rightAxisMax),
|
||||||
nameLocation: rightAxisNameLoc === 'top' ? 'end' : 'middle',
|
nameLocation: rightAxisNameLoc === 'top' ? 'end' : 'middle',
|
||||||
nameRotate: rightAxisNameLoc === 'top' ? 0 : 90,
|
nameRotate: rightAxisNameLoc === 'top' ? 0 : 90,
|
||||||
nameGap: rightAxisNameLoc === 'top' ? 12 : 32,
|
nameGap: rightAxisNameLoc === 'top' ? 12 : 32,
|
||||||
@@ -500,6 +525,8 @@ function buildChartOption(params) {
|
|||||||
}
|
}
|
||||||
] : {
|
] : {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
min: axisNum(yAxisMin),
|
||||||
|
max: axisNum(yAxisMax),
|
||||||
axisLine: { show: false },
|
axisLine: { show: false },
|
||||||
axisTick: { show: false },
|
axisTick: { show: false },
|
||||||
axisLabel: { color: textColor, fontSize: 11 },
|
axisLabel: { color: textColor, fontSize: 11 },
|
||||||
@@ -604,6 +631,14 @@ app.get('/api/chart', (req, res) => {
|
|||||||
seriesStyles: req.query.seriesStyles ? req.query.seriesStyles.split(',') : null,
|
seriesStyles: req.query.seriesStyles ? req.query.seriesStyles.split(',') : null,
|
||||||
leftAxisNameLoc: req.query.leftAxisNameLoc || 'vertical',
|
leftAxisNameLoc: req.query.leftAxisNameLoc || 'vertical',
|
||||||
rightAxisNameLoc: req.query.rightAxisNameLoc || 'vertical',
|
rightAxisNameLoc: req.query.rightAxisNameLoc || 'vertical',
|
||||||
|
labelColorMode: req.query.labelColorMode || 'auto',
|
||||||
|
labelColor: req.query.labelColor || '#333333',
|
||||||
|
yAxisMin: req.query.yAxisMin || '',
|
||||||
|
yAxisMax: req.query.yAxisMax || '',
|
||||||
|
leftAxisMin: req.query.leftAxisMin || '',
|
||||||
|
leftAxisMax: req.query.leftAxisMax || '',
|
||||||
|
rightAxisMin: req.query.rightAxisMin || '',
|
||||||
|
rightAxisMax: req.query.rightAxisMax || '',
|
||||||
rowsAsSeries: req.query.rowsAsSeries === 'true',
|
rowsAsSeries: req.query.rowsAsSeries === 'true',
|
||||||
width: parseInt(req.query.width) || 800,
|
width: parseInt(req.query.width) || 800,
|
||||||
height: parseInt(req.query.height) || 500,
|
height: parseInt(req.query.height) || 500,
|
||||||
@@ -1121,7 +1156,7 @@ app.get('/api/health', (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
service: 'data-chart-tool',
|
service: 'data-chart-tool',
|
||||||
version: '1.19.0',
|
version: '1.21.0',
|
||||||
endpoints: {
|
endpoints: {
|
||||||
'POST /api/chart': '生成图表图片(JSON body)',
|
'POST /api/chart': '生成图表图片(JSON body)',
|
||||||
'GET /api/chart': '生成图表图片(URL 参数)',
|
'GET /api/chart': '生成图表图片(URL 参数)',
|
||||||
@@ -1141,7 +1176,7 @@ app.get('/api/health', (req, res) => {
|
|||||||
app.get('/api/docs', (req, res) => {
|
app.get('/api/docs', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
name: '数据可视化图表生成器 API',
|
name: '数据可视化图表生成器 API',
|
||||||
version: '1.19.0',
|
version: '1.21.0',
|
||||||
endpoints: [
|
endpoints: [
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -1176,6 +1211,14 @@ app.get('/api/docs', (req, res) => {
|
|||||||
seriesStyles: { type: 'array', default: null, description: '每系列样式(如 ["solid","dashed"],柱状支持 solid实体/hollow空心/shadow阴影,折线支持 solid实线/dashed虚线/dotted点线)' },
|
seriesStyles: { type: 'array', default: null, description: '每系列样式(如 ["solid","dashed"],柱状支持 solid实体/hollow空心/shadow阴影,折线支持 solid实线/dashed虚线/dotted点线)' },
|
||||||
leftAxisNameLoc: { type: 'string', default: 'vertical', options: ['vertical', 'top'], description: '左轴标题显示方式:vertical=竖直显示(默认),top=轴上部横排' },
|
leftAxisNameLoc: { type: 'string', default: 'vertical', options: ['vertical', 'top'], description: '左轴标题显示方式:vertical=竖直显示(默认),top=轴上部横排' },
|
||||||
rightAxisNameLoc: { type: 'string', default: 'vertical', options: ['vertical', 'top'], description: '右轴标题显示方式:vertical=竖直显示(默认),top=轴上部横排' },
|
rightAxisNameLoc: { type: 'string', default: 'vertical', options: ['vertical', 'top'], description: '右轴标题显示方式:vertical=竖直显示(默认),top=轴上部横排' },
|
||||||
|
labelColorMode: { type: 'string', default: 'auto', options: ['auto', 'custom'], description: '数据标签颜色:auto=同系列颜色(默认),custom=手动指定 labelColor' },
|
||||||
|
labelColor: { type: 'string', default: '#333333', description: '数据标签自定义颜色(labelColorMode=custom 时生效)' },
|
||||||
|
yAxisMin: { type: 'string', default: '', description: '单Y轴最小值(留空=自动)' },
|
||||||
|
yAxisMax: { type: 'string', default: '', description: '单Y轴最大值(留空=自动)' },
|
||||||
|
leftAxisMin: { type: 'string', default: '', description: '双Y轴左轴最小值(留空=自动)' },
|
||||||
|
leftAxisMax: { type: 'string', default: '', description: '双Y轴左轴最大值(留空=自动)' },
|
||||||
|
rightAxisMin: { type: 'string', default: '', description: '双Y轴右轴最小值(留空=自动)' },
|
||||||
|
rightAxisMax: { type: 'string', default: '', description: '双Y轴右轴最大值(留空=自动)' },
|
||||||
rowsAsSeries: { type: 'boolean', default: false, description: '数据方向:false=列=系列(第一列是横坐标,每列一个系列);true=行=系列(第一行是横坐标,每行一个系列)' }
|
rowsAsSeries: { type: 'boolean', default: false, description: '数据方向:false=列=系列(第一列是横坐标,每列一个系列);true=行=系列(第一行是横坐标,每行一个系列)' }
|
||||||
},
|
},
|
||||||
returns: 'image/png',
|
returns: 'image/png',
|
||||||
|
|||||||
Reference in New Issue
Block a user