feat: 图例上移不遮轴名+按类型显示图标+系列样式细分+轴标题竖直/顶部+X轴名默认取首字段放轴下 v1.19.0
- 双轴图例上移到更靠上位置,轴名在顶部模式时 grid 自动多让位,不再遮住左右轴名 - 图例图标按系列类型自动(柱状=方块、折线=线条),去掉固定 roundRect - 系列样式细分:柱状实体/空心/阴影,折线实线/虚线/点线(主图+多图合并+服务端API 三处统一) - 左右轴标题显示方式可选:竖直显示(默认)/轴上部横排,双轴图主图/合并/API 均支持 - X轴名默认自动取数据首列字段名,默认放横坐标轴下方居中;用户手动编辑后不再自动覆盖 - 收藏保存/恢复含系列样式与轴名显示方式
This commit is contained in:
@@ -124,6 +124,34 @@ function adjustColor(hex, amount) {
|
||||
}
|
||||
|
||||
// ===== 构建 ECharts option =====
|
||||
function hexToRgba(hex, alpha) {
|
||||
hex = String(hex || '#888').replace('#', '');
|
||||
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
|
||||
const num = parseInt(hex, 16);
|
||||
return `rgba(${(num >> 16) & 255},${(num >> 8) & 255},${num & 255},${alpha})`;
|
||||
}
|
||||
|
||||
// 系列细分样式:柱状(实体/空心/阴影),折线(实线/虚线/点线)
|
||||
function applySeriesStyle(seriesItem, type, color, style) {
|
||||
if (!style || style === 'solid') return;
|
||||
if (type === 'bar') {
|
||||
if (style === 'hollow') {
|
||||
const fill = (typeof color === 'string' && color.charAt(0) === '#') ? hexToRgba(color, 0.12) : 'rgba(120,120,120,0.12)';
|
||||
seriesItem.itemStyle.color = fill;
|
||||
seriesItem.itemStyle.borderColor = (typeof color === 'string' && color.charAt(0) === '#') ? color : '#888';
|
||||
seriesItem.itemStyle.borderWidth = 2;
|
||||
} else if (style === 'shadow') {
|
||||
seriesItem.itemStyle.shadowBlur = 14;
|
||||
seriesItem.itemStyle.shadowColor = (typeof color === 'string' && color.charAt(0) === '#') ? hexToRgba(color, 0.55) : 'rgba(0,0,0,0.35)';
|
||||
seriesItem.itemStyle.shadowOffsetY = 4;
|
||||
}
|
||||
} else if (type === 'line') {
|
||||
if (!seriesItem.lineStyle) seriesItem.lineStyle = { width: 3 };
|
||||
if (style === 'dashed') seriesItem.lineStyle.type = 'dashed';
|
||||
else if (style === 'dotted') seriesItem.lineStyle.type = 'dotted';
|
||||
}
|
||||
}
|
||||
|
||||
function buildChartOption(params) {
|
||||
const {
|
||||
data,
|
||||
@@ -147,6 +175,9 @@ function buildChartOption(params) {
|
||||
rightAxisName = '',
|
||||
seriesTypes = null,
|
||||
seriesAxis = null,
|
||||
seriesStyles = null,
|
||||
leftAxisNameLoc = 'vertical',
|
||||
rightAxisNameLoc = 'vertical',
|
||||
width = 800
|
||||
} = params;
|
||||
|
||||
@@ -280,6 +311,10 @@ function buildChartOption(params) {
|
||||
seriesItem.itemStyle.borderRadius = stackMode ? [0, 0, 0, 0] : [4, 4, 0, 0];
|
||||
}
|
||||
|
||||
// 系列细分样式:柱状(实体/空心/阴影) / 折线(实线/虚线/点线)
|
||||
const st = Array.isArray(seriesStyles) ? seriesStyles[idx] : null;
|
||||
applySeriesStyle(seriesItem, type, color, st);
|
||||
|
||||
if (showLabel) {
|
||||
seriesItem.label = {
|
||||
show: true,
|
||||
@@ -345,12 +380,12 @@ function buildChartOption(params) {
|
||||
};
|
||||
}
|
||||
|
||||
// 双Y轴:图例放到各自轴上方(左图例靠左、右图例靠右),不再占用两侧空白
|
||||
// 双Y轴:图例放到各自轴上方(左图例靠左、右图例靠右),不再占用两侧空白;图标按系列类型自动
|
||||
let legendOpt, gridTop;
|
||||
if (dualYAxis && showLegend) {
|
||||
const left = [], right = [];
|
||||
series.forEach(s => { if (s.yAxisIndex === 1) right.push(s.name); else left.push(s.name); });
|
||||
const legendTop = title ? 50 : 16;
|
||||
const legendTop = title ? 42 : 8;
|
||||
const availW = Math.max(100, (width - 20) / 2);
|
||||
// 粗略估算文字宽度:中文≈字号宽,ASCII≈字号*0.6
|
||||
const textW = (n) => [...String(n)].reduce((w, ch) => w + (ch.charCodeAt(0) > 255 ? 12 : 7), 0);
|
||||
@@ -367,6 +402,7 @@ function buildChartOption(params) {
|
||||
};
|
||||
const rows = Math.max(estRows(left), estRows(right));
|
||||
const legendH = rows * 22 + 6;
|
||||
const needNameTopGap = (leftAxisNameLoc === 'top' || rightAxisNameLoc === 'top');
|
||||
legendOpt = [
|
||||
{
|
||||
show: left.length > 0,
|
||||
@@ -374,7 +410,7 @@ function buildChartOption(params) {
|
||||
top: legendTop, width: '49%',
|
||||
data: left,
|
||||
title: left.length > 1 ? { text: '左轴', textStyle: { color: textColor, fontSize: 11, fontWeight: 600 } } : undefined,
|
||||
textStyle: { color: textColor, fontSize: 12 }, itemGap: 14, icon: 'roundRect'
|
||||
textStyle: { color: textColor, fontSize: 12 }, itemGap: 14
|
||||
},
|
||||
{
|
||||
show: right.length > 0,
|
||||
@@ -382,17 +418,16 @@ function buildChartOption(params) {
|
||||
top: legendTop, width: '49%',
|
||||
data: right,
|
||||
title: right.length > 1 ? { text: '右轴', textStyle: { color: textColor, fontSize: 11, fontWeight: 600 } } : undefined,
|
||||
textStyle: { color: textColor, fontSize: 12 }, itemGap: 14, icon: 'roundRect'
|
||||
textStyle: { color: textColor, fontSize: 12 }, itemGap: 14
|
||||
}
|
||||
];
|
||||
gridTop = legendTop + legendH + 8;
|
||||
gridTop = legendTop + legendH + (needNameTopGap ? 24 : 8);
|
||||
} else {
|
||||
legendOpt = showLegend ? {
|
||||
show: true,
|
||||
top: title ? 50 : 15,
|
||||
top: title ? 44 : 10,
|
||||
textStyle: { color: textColor, fontSize: 12 },
|
||||
itemGap: 20,
|
||||
icon: 'roundRect'
|
||||
itemGap: 20
|
||||
} : { show: false };
|
||||
gridTop = showLegend ? (title ? 90 : 60) : (title ? 60 : 30);
|
||||
}
|
||||
@@ -435,7 +470,10 @@ function buildChartOption(params) {
|
||||
{
|
||||
type: 'value',
|
||||
name: leftAxisName || '左轴',
|
||||
nameTextStyle: { color: textColor, fontSize: 11, padding: [0, 0, 0, 4] },
|
||||
nameLocation: leftAxisNameLoc === 'top' ? 'end' : 'middle',
|
||||
nameRotate: leftAxisNameLoc === 'top' ? 0 : 90,
|
||||
nameGap: leftAxisNameLoc === 'top' ? 12 : 32,
|
||||
nameTextStyle: { color: textColor, fontSize: 11 },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: textColor, fontSize: 11 },
|
||||
@@ -451,7 +489,10 @@ function buildChartOption(params) {
|
||||
type: 'value',
|
||||
name: rightAxisName || '右轴',
|
||||
position: 'right',
|
||||
nameTextStyle: { color: textColor, fontSize: 11, padding: [0, 4, 0, 0] },
|
||||
nameLocation: rightAxisNameLoc === 'top' ? 'end' : 'middle',
|
||||
nameRotate: rightAxisNameLoc === 'top' ? 0 : 90,
|
||||
nameGap: rightAxisNameLoc === 'top' ? 12 : 32,
|
||||
nameTextStyle: { color: textColor, fontSize: 11 },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: textColor, fontSize: 11 },
|
||||
@@ -560,6 +601,9 @@ app.get('/api/chart', (req, res) => {
|
||||
rightAxisName: req.query.rightAxisName || '',
|
||||
seriesTypes: req.query.seriesTypes ? req.query.seriesTypes.split(',') : null,
|
||||
seriesAxis: req.query.seriesAxis ? req.query.seriesAxis.split(',').map(Number) : null,
|
||||
seriesStyles: req.query.seriesStyles ? req.query.seriesStyles.split(',') : null,
|
||||
leftAxisNameLoc: req.query.leftAxisNameLoc || 'vertical',
|
||||
rightAxisNameLoc: req.query.rightAxisNameLoc || 'vertical',
|
||||
rowsAsSeries: req.query.rowsAsSeries === 'true',
|
||||
width: parseInt(req.query.width) || 800,
|
||||
height: parseInt(req.query.height) || 500,
|
||||
@@ -1077,7 +1121,7 @@ app.get('/api/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
service: 'data-chart-tool',
|
||||
version: '1.18.0',
|
||||
version: '1.19.0',
|
||||
endpoints: {
|
||||
'POST /api/chart': '生成图表图片(JSON body)',
|
||||
'GET /api/chart': '生成图表图片(URL 参数)',
|
||||
@@ -1097,7 +1141,7 @@ app.get('/api/health', (req, res) => {
|
||||
app.get('/api/docs', (req, res) => {
|
||||
res.json({
|
||||
name: '数据可视化图表生成器 API',
|
||||
version: '1.18.0',
|
||||
version: '1.19.0',
|
||||
endpoints: [
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -1129,6 +1173,9 @@ app.get('/api/docs', (req, res) => {
|
||||
rightAxisName: { type: 'string', default: '', description: '右轴名称' },
|
||||
seriesTypes: { type: 'array', default: null, description: '每系列图表类型(如 ["bar","line"],按系列顺序对应,bar/line/auto)' },
|
||||
seriesAxis: { type: 'array', default: null, description: '每系列坐标轴(如 [0,1],0=左轴 1=右轴,配合 dualYAxis 使用;优先于 rightAxisSeries)' },
|
||||
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=轴上部横排' },
|
||||
rightAxisNameLoc: { type: 'string', default: 'vertical', options: ['vertical', 'top'], description: '右轴标题显示方式:vertical=竖直显示(默认),top=轴上部横排' },
|
||||
rowsAsSeries: { type: 'boolean', default: false, description: '数据方向:false=列=系列(第一列是横坐标,每列一个系列);true=行=系列(第一行是横坐标,每行一个系列)' }
|
||||
},
|
||||
returns: 'image/png',
|
||||
|
||||
Reference in New Issue
Block a user