Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68e4b72d46 | ||
|
|
6a696daeb8 | ||
|
|
d9b3253fa0 | ||
|
|
51d3e09378 |
@@ -45,6 +45,9 @@ Content-Type: application/json
|
|||||||
| `leftLabel` | string | — | `"左侧"` | 左侧区域标签 |
|
| `leftLabel` | string | — | `"左侧"` | 左侧区域标签 |
|
||||||
| `rightLabel` | string | — | `"右侧"` | 右侧区域标签 |
|
| `rightLabel` | string | — | `"右侧"` | 右侧区域标签 |
|
||||||
| `splitStyle` | string | — | `"solid"` | 分割线样式:`solid` / `dashed` / `dotted` |
|
| `splitStyle` | string | — | `"solid"` | 分割线样式:`solid` / `dashed` / `dotted` |
|
||||||
|
| `dualYAxis` | boolean | — | `false` | 启用双Y轴(左右量度不同) |
|
||||||
|
| `rightAxisSeries` | array | — | `null` | 右轴系列名列表,如 `["利润"]`(未列出的系列用左轴) |
|
||||||
|
| `rightAxisName` | string | — | `""` | 右轴名称 |
|
||||||
| `width` | number | — | `800` | 图片宽度(px) |
|
| `width` | number | — | `800` | 图片宽度(px) |
|
||||||
| `height` | number | — | `500` | 图片高度(px) |
|
| `height` | number | — | `500` | 图片高度(px) |
|
||||||
| `pixelRatio` | number | — | `2` | 像素倍率(越大越清晰) |
|
| `pixelRatio` | number | — | `2` | 像素倍率(越大越清晰) |
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
- **柱状图** - 支持单系列/多系列对比
|
- **柱状图** - 支持单系列/多系列对比
|
||||||
- **折线图** - 支持平滑曲线、面积填充
|
- **折线图** - 支持平滑曲线、面积填充
|
||||||
- **混合图** - 柱状图+折线图组合展示
|
- **混合图** - 柱状图+折线图组合展示
|
||||||
|
- **双Y轴** - 一张图左右两个坐标轴,量度可不同,系列可指定左轴/右轴(含右轴名称)
|
||||||
|
|
||||||
### 📋 表格功能
|
### 📋 表格功能
|
||||||
- **表格图片生成** - 根据数据生成精美的表格图片
|
- **表格图片生成** - 根据数据生成精美的表格图片
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ let chartInstance = null;
|
|||||||
let parsedData = null;
|
let parsedData = null;
|
||||||
let seriesColors = [];
|
let seriesColors = [];
|
||||||
let seriesOrder = [];
|
let seriesOrder = [];
|
||||||
|
let seriesAxis = []; // 每个系列所属轴:0=左轴 1=右轴
|
||||||
let currentMode = 'chart'; // 'chart' or 'table'
|
let currentMode = 'chart'; // 'chart' or 'table'
|
||||||
let tableImageBlob = null;
|
let tableImageBlob = null;
|
||||||
|
|
||||||
@@ -131,6 +132,7 @@ function generateChart() {
|
|||||||
seriesOrder = parsedData.seriesNames.map((_, i) => i);
|
seriesOrder = parsedData.seriesNames.map((_, i) => i);
|
||||||
const palette = colorPalettes[document.getElementById('themeStyle').value] || colorPalettes.default;
|
const palette = colorPalettes[document.getElementById('themeStyle').value] || colorPalettes.default;
|
||||||
seriesColors = parsedData.seriesNames.map((_, i) => palette[i % palette.length]);
|
seriesColors = parsedData.seriesNames.map((_, i) => palette[i % palette.length]);
|
||||||
|
seriesAxis = parsedData.seriesNames.map(() => 0);
|
||||||
|
|
||||||
// 渲染系列配置
|
// 渲染系列配置
|
||||||
renderSeriesConfig();
|
renderSeriesConfig();
|
||||||
@@ -172,6 +174,8 @@ function updateChart() {
|
|||||||
const stackMode = document.getElementById('stackMode').checked;
|
const stackMode = document.getElementById('stackMode').checked;
|
||||||
const smoothLine = document.getElementById('smoothLine').checked;
|
const smoothLine = document.getElementById('smoothLine').checked;
|
||||||
const enableSplit = document.getElementById('enableSplit').checked;
|
const enableSplit = document.getElementById('enableSplit').checked;
|
||||||
|
const dualAxis = document.getElementById('dualAxis').checked;
|
||||||
|
const rightAxisName = document.getElementById('rightAxisName').value;
|
||||||
|
|
||||||
// 显示/隐藏分割配置
|
// 显示/隐藏分割配置
|
||||||
document.getElementById('splitConfig').style.display = enableSplit ? 'block' : 'none';
|
document.getElementById('splitConfig').style.display = enableSplit ? 'block' : 'none';
|
||||||
@@ -248,6 +252,11 @@ function updateChart() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 双Y轴:按系列指定左右轴
|
||||||
|
if (dualAxis) {
|
||||||
|
seriesItem.yAxisIndex = (seriesAxis[origIdx] === 1) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
return seriesItem;
|
return seriesItem;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -379,7 +388,30 @@ function updateChart() {
|
|||||||
},
|
},
|
||||||
axisTick: { show: false }
|
axisTick: { show: false }
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: dualAxis ? [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '左轴',
|
||||||
|
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',
|
type: 'value',
|
||||||
axisLine: { show: false },
|
axisLine: { show: false },
|
||||||
axisTick: { show: false },
|
axisTick: { show: false },
|
||||||
@@ -466,6 +498,13 @@ function renderSeriesConfig() {
|
|||||||
seriesOrder.forEach((origIdx, displayIdx) => {
|
seriesOrder.forEach((origIdx, displayIdx) => {
|
||||||
const name = parsedData.seriesNames[origIdx];
|
const name = parsedData.seriesNames[origIdx];
|
||||||
const color = seriesColors[origIdx];
|
const color = seriesColors[origIdx];
|
||||||
|
const dualAxis = document.getElementById('dualAxis').checked;
|
||||||
|
const axisSelect = dualAxis ? `
|
||||||
|
<select onchange="updateSeriesAxis(${origIdx}, this.value)" title="所属坐标轴">
|
||||||
|
<option value="0" ${seriesAxis[origIdx] === 0 ? 'selected' : ''}>左轴</option>
|
||||||
|
<option value="1" ${seriesAxis[origIdx] === 1 ? 'selected' : ''}>右轴</option>
|
||||||
|
</select>
|
||||||
|
` : '';
|
||||||
|
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'series-item';
|
item.className = 'series-item';
|
||||||
@@ -481,6 +520,7 @@ function renderSeriesConfig() {
|
|||||||
<option value="bar">柱状</option>
|
<option value="bar">柱状</option>
|
||||||
<option value="line">折线</option>
|
<option value="line">折线</option>
|
||||||
</select>
|
</select>
|
||||||
|
${axisSelect}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// 拖拽事件
|
// 拖拽事件
|
||||||
@@ -566,6 +606,19 @@ function updateSeriesType(origIdx, type) {
|
|||||||
updateChart();
|
updateChart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 双Y轴 =====
|
||||||
|
function onDualAxisChange() {
|
||||||
|
const on = document.getElementById('dualAxis').checked;
|
||||||
|
document.getElementById('rightAxisGroup').style.display = on ? 'block' : 'none';
|
||||||
|
renderSeriesConfig(); // 显示/隐藏系列轴选择
|
||||||
|
updateChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSeriesAxis(origIdx, axis) {
|
||||||
|
seriesAxis[origIdx] = parseInt(axis) || 0;
|
||||||
|
updateChart();
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 工具函数 =====
|
// ===== 工具函数 =====
|
||||||
function adjustColor(hex, amount) {
|
function adjustColor(hex, amount) {
|
||||||
hex = hex.replace('#', '');
|
hex = hex.replace('#', '');
|
||||||
@@ -648,6 +701,28 @@ function onExportPresetChange() {
|
|||||||
refreshPreview();
|
refreshPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导出设置面板展开/折叠(默认折叠)
|
||||||
|
let exportSettingsOpen = false;
|
||||||
|
function toggleExportSettings() {
|
||||||
|
exportSettingsOpen = !exportSettingsOpen;
|
||||||
|
const body = document.getElementById('exportSettingsBody');
|
||||||
|
const t = document.getElementById('exportToggle');
|
||||||
|
body.style.display = exportSettingsOpen ? 'block' : 'none';
|
||||||
|
t.textContent = exportSettingsOpen ? '▾' : '▸';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一键恢复默认(重置全部设置,历史记录保留)
|
||||||
|
function resetExportSettings() {
|
||||||
|
document.getElementById('exportPreset').value = 'original';
|
||||||
|
document.getElementById('customResGroup').style.display = 'none';
|
||||||
|
document.getElementById('customWidth').value = '';
|
||||||
|
document.getElementById('customHeight').value = '';
|
||||||
|
document.getElementById('exportFilename').value = '';
|
||||||
|
document.getElementById('exportHistory').value = '';
|
||||||
|
refreshExportHint();
|
||||||
|
refreshPreview();
|
||||||
|
}
|
||||||
|
|
||||||
function onCustomResInput() {
|
function onCustomResInput() {
|
||||||
refreshExportHint();
|
refreshExportHint();
|
||||||
refreshPreview();
|
refreshPreview();
|
||||||
@@ -778,12 +853,13 @@ function exportCurrent(prefix) {
|
|||||||
}).catch(err => alert(err.message));
|
}).catch(err => alert(err.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出分辨率提示
|
// 导出分辨率提示 + 折叠栏摘要
|
||||||
function refreshExportHint() {
|
function refreshExportHint() {
|
||||||
const { w, h } = getResolution();
|
const { w, h } = getResolution();
|
||||||
const hint = document.getElementById('exportResHint');
|
const hint = document.getElementById('exportResHint');
|
||||||
if (!hint) return;
|
if (hint) hint.textContent = w > 0 && h > 0 ? `导出分辨率:${w} × ${h}(原图居中缩放)` : '导出分辨率:原始大小';
|
||||||
hint.textContent = w > 0 && h > 0 ? `导出分辨率:${w} × ${h}(原图居中缩放)` : '导出分辨率:原始大小';
|
const sum = document.getElementById('exportSummary');
|
||||||
|
if (sum) sum.textContent = w > 0 && h > 0 ? `${w} × ${h}` : '原始大小';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新导出预览:以原图为中心,居中展示保存后的图
|
// 刷新导出预览:以原图为中心,居中展示保存后的图
|
||||||
|
|||||||
+63
-45
@@ -100,8 +100,15 @@ C, 20, 30, 40</pre>
|
|||||||
<label><input type="checkbox" id="showLabel" onchange="updateChart()"> 数据标签</label>
|
<label><input type="checkbox" id="showLabel" onchange="updateChart()"> 数据标签</label>
|
||||||
<label><input type="checkbox" id="stackMode" onchange="updateChart()"> 堆叠模式</label>
|
<label><input type="checkbox" id="stackMode" onchange="updateChart()"> 堆叠模式</label>
|
||||||
<label><input type="checkbox" id="smoothLine" checked onchange="updateChart()"> 平滑曲线</label>
|
<label><input type="checkbox" id="smoothLine" checked onchange="updateChart()"> 平滑曲线</label>
|
||||||
|
<label><input type="checkbox" id="dualAxis" onchange="onDualAxisChange()"> 双Y轴</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group" id="rightAxisGroup" style="display:none;">
|
||||||
|
<label>右轴名称(量度不同)</label>
|
||||||
|
<input type="text" id="rightAxisName" placeholder="如:利润(万元)" oninput="updateChart()">
|
||||||
|
<p class="hint-text">在下方「系列配置」中可指定每个系列用左轴/右轴</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表格配置区 -->
|
<!-- 表格配置区 -->
|
||||||
@@ -208,47 +215,56 @@ C, 20, 30, 40</pre>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 导出设置 + 按钮 -->
|
<!-- 导出设置 + 按钮(默认折叠) -->
|
||||||
<div class="panel-section" id="exportSection">
|
<div class="panel-section" id="exportSection">
|
||||||
<h2>📤 导出设置</h2>
|
<div class="export-header">
|
||||||
|
<div class="export-header-main" onclick="toggleExportSettings()" title="点击展开/折叠">
|
||||||
<div class="config-group">
|
<h2>📤 导出设置</h2>
|
||||||
<label>分辨率</label>
|
<span class="export-summary" id="exportSummary">原始大小</span>
|
||||||
<select id="exportPreset" onchange="onExportPresetChange()">
|
<span class="export-toggle" id="exportToggle">▸</span>
|
||||||
<option value="original">原始大小</option>
|
|
||||||
<option value="1920x1080">1920 × 1080</option>
|
|
||||||
<option value="1280x720">1280 × 720</option>
|
|
||||||
<option value="1024x768">1024 × 768</option>
|
|
||||||
<option value="800x600">800 × 600</option>
|
|
||||||
<option value="custom">自定义</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="config-group" id="customResGroup" style="display:none;">
|
|
||||||
<label>自定义分辨率(宽 × 高)</label>
|
|
||||||
<div class="res-input-row">
|
|
||||||
<input type="number" id="customWidth" min="1" max="8192" placeholder="宽" oninput="onCustomResInput()">
|
|
||||||
<span class="res-times">×</span>
|
|
||||||
<input type="number" id="customHeight" min="1" max="8192" placeholder="高" oninput="onCustomResInput()">
|
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-success btn-sm" onclick="exportImage('png')">📥 下载</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="config-group">
|
<div id="exportSettingsBody" class="export-settings-body" style="display:none;">
|
||||||
<label>历史分辨率(最近5个)</label>
|
<div class="config-group">
|
||||||
<select id="exportHistory" onchange="onHistorySelect()">
|
<label>分辨率</label>
|
||||||
<option value="">— 暂无历史 —</option>
|
<select id="exportPreset" onchange="onExportPresetChange()">
|
||||||
</select>
|
<option value="original">原始大小</option>
|
||||||
</div>
|
<option value="1920x1080">1920 × 1080</option>
|
||||||
|
<option value="1280x720">1280 × 720</option>
|
||||||
|
<option value="1024x768">1024 × 768</option>
|
||||||
|
<option value="800x600">800 × 600</option>
|
||||||
|
<option value="custom">自定义</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="config-group">
|
<div class="config-group" id="customResGroup" style="display:none;">
|
||||||
<label>文件名</label>
|
<label>自定义分辨率(宽 × 高)</label>
|
||||||
<input type="text" id="exportFilename" placeholder="留空则按日期时间自动命名">
|
<div class="res-input-row">
|
||||||
<p class="hint-text" id="exportResHint"></p>
|
<input type="number" id="customWidth" min="1" max="8192" placeholder="宽" oninput="onCustomResInput()">
|
||||||
</div>
|
<span class="res-times">×</span>
|
||||||
|
<input type="number" id="customHeight" min="1" max="8192" placeholder="高" oninput="onCustomResInput()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="btn-group">
|
<div class="config-group">
|
||||||
<button class="btn btn-success" onclick="exportImage('png')">📥 下载图片</button>
|
<label>历史分辨率(最近5个)</label>
|
||||||
<button class="btn btn-success" id="btnExportSvg" onclick="exportImage('svg')">📥 导出SVG</button>
|
<select id="exportHistory" onchange="onHistorySelect()">
|
||||||
|
<option value="">— 暂无历史 —</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-group">
|
||||||
|
<label>文件名</label>
|
||||||
|
<input type="text" id="exportFilename" placeholder="留空则按日期时间自动命名">
|
||||||
|
<p class="hint-text" id="exportResHint"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-secondary" onclick="resetExportSettings()">🔄 恢复默认</button>
|
||||||
|
<button class="btn btn-success" id="btnExportSvg" onclick="exportImage('svg')">📥 导出SVG</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -385,18 +401,20 @@ C, 20, 30, 40</pre>
|
|||||||
|
|
||||||
<!-- 右侧面板:图表预览 + 导出预览 + 风格切换 -->
|
<!-- 右侧面板:图表预览 + 导出预览 + 风格切换 -->
|
||||||
<div class="right-panel">
|
<div class="right-panel">
|
||||||
<div class="chart-container">
|
<div class="right-main">
|
||||||
<div id="chartArea" class="chart-area">
|
<div class="chart-container">
|
||||||
<div class="placeholder">
|
<div id="chartArea" class="chart-area">
|
||||||
<p>📊</p>
|
<div class="placeholder">
|
||||||
<p>输入数据后点击"生成图表"</p>
|
<p>📊</p>
|
||||||
|
<p>输入数据后点击"生成图表"</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- 导出效果预览(按分辨率居中展示保存后的图) -->
|
||||||
<!-- 导出效果预览(按分辨率居中展示保存后的图) -->
|
<div class="export-preview-box" id="exportPreviewBox" style="display:none;">
|
||||||
<div class="export-preview-box" id="exportPreviewBox" style="display:none;">
|
<div class="export-preview-title" id="exportPreviewTitle">📐 导出预览</div>
|
||||||
<div class="export-preview-title" id="exportPreviewTitle">📐 导出预览</div>
|
<div class="export-preview-inner" id="exportPreviewInner"></div>
|
||||||
<div class="export-preview-inner" id="exportPreviewInner"></div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 风格快速切换按钮(放在图表右侧) -->
|
<!-- 风格快速切换按钮(放在图表右侧) -->
|
||||||
<div class="theme-buttons-section">
|
<div class="theme-buttons-section">
|
||||||
|
|||||||
@@ -113,7 +113,10 @@ function buildChartOption(params) {
|
|||||||
leftLabel = '左侧',
|
leftLabel = '左侧',
|
||||||
rightLabel = '右侧',
|
rightLabel = '右侧',
|
||||||
splitStyle = 'solid',
|
splitStyle = 'solid',
|
||||||
seriesColors: customColors = null
|
seriesColors: customColors = null,
|
||||||
|
dualYAxis = false,
|
||||||
|
rightAxisSeries = null,
|
||||||
|
rightAxisName = ''
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const parsedData = parseData(data);
|
const parsedData = parseData(data);
|
||||||
@@ -148,6 +151,12 @@ function buildChartOption(params) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 双Y轴:rightAxisSeries 指定右轴系列名
|
||||||
|
if (dualYAxis) {
|
||||||
|
const rightNames = Array.isArray(rightAxisSeries) ? rightAxisSeries : (rightAxisSeries ? [rightAxisSeries] : []);
|
||||||
|
seriesItem.yAxisIndex = rightNames.includes(name) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (stackMode) {
|
if (stackMode) {
|
||||||
seriesItem.stack = 'total';
|
seriesItem.stack = 'total';
|
||||||
}
|
}
|
||||||
@@ -159,8 +168,7 @@ function buildChartOption(params) {
|
|||||||
seriesItem.areaStyle = theme === 'gradient' ? { opacity: 0.15 } : undefined;
|
seriesItem.areaStyle = theme === 'gradient' ? { opacity: 0.15 } : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'bar') {
|
if (type === 'bar') {seriesItem.barMaxWidth = 40;
|
||||||
seriesItem.barMaxWidth = 40;
|
|
||||||
seriesItem.itemStyle.borderRadius = stackMode ? [0, 0, 0, 0] : [4, 4, 0, 0];
|
seriesItem.itemStyle.borderRadius = stackMode ? [0, 0, 0, 0] : [4, 4, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +277,33 @@ function buildChartOption(params) {
|
|||||||
},
|
},
|
||||||
axisTick: { show: false }
|
axisTick: { show: false }
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: dualYAxis ? [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '左轴',
|
||||||
|
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',
|
type: 'value',
|
||||||
axisLine: { show: false },
|
axisLine: { show: false },
|
||||||
axisTick: { show: false },
|
axisTick: { show: false },
|
||||||
@@ -366,6 +400,9 @@ app.get('/api/chart', (req, res) => {
|
|||||||
leftLabel: req.query.leftLabel || '左侧',
|
leftLabel: req.query.leftLabel || '左侧',
|
||||||
rightLabel: req.query.rightLabel || '右侧',
|
rightLabel: req.query.rightLabel || '右侧',
|
||||||
splitStyle: req.query.splitStyle || 'solid',
|
splitStyle: req.query.splitStyle || 'solid',
|
||||||
|
dualYAxis: req.query.dualYAxis === 'true',
|
||||||
|
rightAxisSeries: req.query.rightAxisSeries ? req.query.rightAxisSeries.split(',') : null,
|
||||||
|
rightAxisName: req.query.rightAxisName || '',
|
||||||
width: parseInt(req.query.width) || 800,
|
width: parseInt(req.query.width) || 800,
|
||||||
height: parseInt(req.query.height) || 500,
|
height: parseInt(req.query.height) || 500,
|
||||||
format: req.query.format || 'png',
|
format: req.query.format || 'png',
|
||||||
@@ -882,7 +919,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.10.0',
|
version: '1.11.1',
|
||||||
endpoints: {
|
endpoints: {
|
||||||
'POST /api/chart': '生成图表图片(JSON body)',
|
'POST /api/chart': '生成图表图片(JSON body)',
|
||||||
'GET /api/chart': '生成图表图片(URL 参数)',
|
'GET /api/chart': '生成图表图片(URL 参数)',
|
||||||
@@ -897,7 +934,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.10.0',
|
version: '1.11.1',
|
||||||
endpoints: [
|
endpoints: [
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -922,7 +959,10 @@ app.get('/api/docs', (req, res) => {
|
|||||||
width: { type: 'number', default: 800, description: '图片宽度(px)' },
|
width: { type: 'number', default: 800, description: '图片宽度(px)' },
|
||||||
height: { type: 'number', default: 500, description: '图片高度(px)' },
|
height: { type: 'number', default: 500, description: '图片高度(px)' },
|
||||||
format: { type: 'string', default: 'png', options: ['png'], description: '输出格式' },
|
format: { type: 'string', default: 'png', options: ['png'], description: '输出格式' },
|
||||||
pixelRatio: { type: 'number', default: 2, description: '像素倍率(清晰度)' }
|
pixelRatio: { type: 'number', default: 2, description: '像素倍率(清晰度)' },
|
||||||
|
dualYAxis: { type: 'boolean', default: false, description: '是否启用双Y轴(左右量度不同)' },
|
||||||
|
rightAxisSeries: { type: 'array', default: null, description: '右轴系列名列表(如 ["利润"],指定哪些系列用右轴)' },
|
||||||
|
rightAxisName: { type: 'string', default: '', description: '右轴名称' }
|
||||||
},
|
},
|
||||||
returns: 'image/png',
|
returns: 'image/png',
|
||||||
example: {
|
example: {
|
||||||
|
|||||||
@@ -325,9 +325,18 @@ body {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
/* 右侧主区域:原图预览 + 导出预览 上下排列 */
|
||||||
|
.right-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
flex: none;
|
||||||
|
min-width: 0;
|
||||||
background: var(--panel-bg);
|
background: var(--panel-bg);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -337,7 +346,7 @@ body {
|
|||||||
|
|
||||||
.chart-area {
|
.chart-area {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 600px;
|
height: 420px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
.placeholder {
|
||||||
@@ -370,7 +379,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chart-area {
|
.chart-area {
|
||||||
height: 450px;
|
height: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 小屏幕下风格按钮回到图表下方 */
|
/* 小屏幕下风格按钮回到图表下方 */
|
||||||
@@ -671,3 +680,63 @@ input[type="range"]::-moz-range-thumb {
|
|||||||
padding: 1px 8px;
|
padding: 1px 8px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 导出设置折叠栏 ===== */
|
||||||
|
.export-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-header-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-header h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-summary {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--primary);
|
||||||
|
background: #eef2ff;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-toggle {
|
||||||
|
color: var(--text-light);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-settings-body {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导出设置折叠栏下载按钮(更小,避免挤占) */
|
||||||
|
.export-header .btn-sm {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: auto;
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user