diff --git a/app.js b/app.js index 9d590be..fb8da8a 100644 --- a/app.js +++ b/app.js @@ -291,6 +291,8 @@ 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; // 饼图/雷达图:走专用构建逻辑(无坐标轴/网格,不支持双轴/堆叠/分割) if (chartType === 'pie' || chartType === 'radar') { @@ -460,7 +462,46 @@ function updateChart() { window._splitConfig = null; } - // 图表配置 + // 图表配置:双Y轴图例按左右轴分组放到对应轴边上,grid 相应让位 + let legendConfig, gridLeftCfg, gridRightCfg; + if (dualAxis && showLegend) { + const left = [], right = []; + seriesOrder.forEach(origIdx => { + const name = parsedData.seriesNames[origIdx]; + (seriesAxis[origIdx] === 1 ? right : left).push(name); + }); + legendConfig = [ + { + show: left.length > 0, + orient: 'vertical', left: 8, + top: title ? 56 : 24, + data: left, + title: left.length > 1 ? { text: '左轴', textStyle: { color: textColor, fontSize: 11, fontWeight: 600 } } : undefined, + textStyle: { color: textColor, fontSize: 12 }, itemGap: 10, icon: 'roundRect' + }, + { + show: right.length > 0, + orient: 'vertical', right: 8, + top: title ? 56 : 24, + data: right, + title: right.length > 1 ? { text: '右轴', textStyle: { color: textColor, fontSize: 11, fontWeight: 600 } } : undefined, + textStyle: { color: textColor, fontSize: 12 }, itemGap: 10, icon: 'roundRect' + } + ]; + gridLeftCfg = measureLegendWidth(left) + 30; + gridRightCfg = measureLegendWidth(right) + 30; + } else { + legendConfig = showLegend ? { + show: true, + top: title ? 50 : 15, + textStyle: { color: textColor, fontSize: 12 }, + itemGap: 20, + icon: 'roundRect' + } : { show: false }; + gridLeftCfg = '3%'; + gridRightCfg = '4%'; + } + const option = { backgroundColor: bgColor, title: title ? { @@ -494,29 +535,26 @@ function updateChart() { return html; } }, - legend: showLegend ? { - show: true, - top: title ? 50 : 15, - textStyle: { color: textColor, fontSize: 12 }, - itemGap: 20, - icon: 'roundRect' - } : { show: false }, + legend: legendConfig, grid: { - left: '3%', - right: '4%', + left: gridLeftCfg, + right: gridRightCfg, bottom: '3%', - top: showLegend ? (title ? 90 : 60) : (title ? 60 : 30), + top: dualAxis ? (title ? 52 : 24) : (showLegend ? (title ? 90 : 60) : (title ? 60 : 30)), containLabel: true }, xAxis: { type: 'category', + name: xAxisName, + nameGap: 20, + nameTextStyle: { color: textColor, fontSize: 12, fontWeight: 600 }, data: parsedData.categories, axisLine: { lineStyle: { color: axisLineColor } }, - axisLabel: { - color: textColor, + axisLabel: { + color: textColor, fontSize: 12, interval: 0, - rotate: parsedData.categories.length > 10 ? 30 : 0 + rotate: xLabelRotate }, axisTick: { show: false } }, @@ -752,6 +790,18 @@ function updateSeriesAxis(origIdx, axis) { } // ===== 工具函数 ===== +// 测量图例文字宽度(用于双轴侧边图例的 grid 让位) +function measureLegendWidth(names, fontSize) { + fontSize = fontSize || 12; + if (!names || !names.length) return 0; + const c = document.createElement('canvas'); + const ctx = c.getContext('2d'); + ctx.font = `${fontSize}px sans-serif`; + let max = 0; + names.forEach(n => { const w = ctx.measureText(n).width; if (w > max) max = w; }); + return Math.ceil(max + 44); // 图标(14)+间距(6)+边距(24) +} + function adjustColor(hex, amount) { hex = hex.replace('#', ''); const num = parseInt(hex, 16); @@ -1186,7 +1236,8 @@ function buildCombineChartOption(dataText, cfg) { title = '', chartType = 'bar', theme = 'default', showLegend = true, showGrid = true, showLabel = false, stackMode = false, smoothLine = true, - dualYAxis = false, leftAxisName = '', rightAxisName = '', seriesConfig = null + dualYAxis = false, leftAxisName = '', rightAxisName = '', seriesConfig = null, + xAxisName = '', xLabelRotate = 0 } = cfg; const bgColor = theme === 'dark' ? '#1a1a2e' : '#ffffff'; @@ -1254,10 +1305,11 @@ function buildCombineChartOption(dataText, cfg) { } } + const scMap = {}; + (Array.isArray(seriesConfig) ? seriesConfig : []).forEach(s => { if (s && s.name) scMap[s.name] = s; }); + const series = parsed.seriesNames.map((name, idx) => { const color = palette[idx % palette.length]; - const scMap = {}; - (Array.isArray(seriesConfig) ? seriesConfig : []).forEach(s => { if (s && s.name) scMap[s.name] = s; }); const sc = scMap[name] || {}; let type = chartType === 'bar-line' ? (idx % 2 === 0 ? 'bar' : 'line') : chartType; if (sc.type && sc.type !== 'auto') type = sc.type; @@ -1309,6 +1361,46 @@ function buildCombineChartOption(dataText, cfg) { return s; }); + // 双Y轴图例按左右轴分组放到对应轴边上,grid 相应让位 + let legendCfg, gridLeftCfg, gridRightCfg; + if (dualYAxis && showLegend) { + const left = [], right = []; + parsed.seriesNames.forEach(name => { + const sc = scMap[name] || {}; + ((sc.axis === 1 || sc.axis === '1') ? right : left).push(name); + }); + legendCfg = [ + { + show: left.length > 0, + orient: 'vertical', left: 6, + top: title ? 44 : 12, + data: left, + title: left.length > 1 ? { text: '左轴', textStyle: { color: textColor, fontSize: 10, fontWeight: 600 } } : undefined, + textStyle: { color: textColor, fontSize: 11 }, itemGap: 8, icon: 'roundRect' + }, + { + show: right.length > 0, + orient: 'vertical', right: 6, + top: title ? 44 : 12, + data: right, + title: right.length > 1 ? { text: '右轴', textStyle: { color: textColor, fontSize: 10, fontWeight: 600 } } : undefined, + textStyle: { color: textColor, fontSize: 11 }, itemGap: 8, icon: 'roundRect' + } + ]; + gridLeftCfg = Math.min(measureLegendWidth(left, 11) + 24, 200); + gridRightCfg = Math.min(measureLegendWidth(right, 11) + 24, 200); + } else { + legendCfg = showLegend ? { + show: true, + top: title ? 40 : 10, + textStyle: { color: textColor, fontSize: 12 }, + itemGap: 20, + icon: 'roundRect' + } : { show: false }; + gridLeftCfg = '3%'; + gridRightCfg = '4%'; + } + return { backgroundColor: bgColor, title: title ? { @@ -1323,23 +1415,20 @@ function buildCombineChartOption(dataText, cfg) { borderColor: theme === 'dark' ? '#555' : '#eee', textStyle: { color: textColor } }, - legend: showLegend ? { - show: true, - top: title ? 40 : 10, - textStyle: { color: textColor, fontSize: 12 }, - itemGap: 20, - icon: 'roundRect' - } : { show: false }, + legend: legendCfg, grid: { - left: '3%', right: '4%', bottom: '3%', - top: showLegend ? (title ? 70 : 45) : (title ? 50 : 30), + left: gridLeftCfg, right: gridRightCfg, bottom: '3%', + top: dualYAxis ? (title ? 44 : 16) : (showLegend ? (title ? 70 : 45) : (title ? 50 : 30)), containLabel: true }, xAxis: { type: 'category', + name: xAxisName, + nameGap: 16, + nameTextStyle: { color: textColor, fontSize: 11, fontWeight: 600 }, data: parsed.categories, axisLine: { lineStyle: { color: axisLineColor } }, - axisLabel: { color: textColor, fontSize: 11, interval: 0, rotate: parsed.categories.length > 10 ? 30 : 0 }, + axisLabel: { color: textColor, fontSize: 11, interval: 0, rotate: xLabelRotate }, axisTick: { show: false } }, yAxis: dualYAxis ? [ @@ -1386,7 +1475,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: [] }; + 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 }; } function initCombineCharts() { @@ -1453,6 +1542,15 @@ function renderCombineCharts() { ` : ''} +
+ +
+ + · + + ° +
+
${renderCombineSeriesConfigHTML(i)}
@@ -1587,6 +1685,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, seriesConfig: c.seriesConfig }); if (!option) { reject(new Error('数据格式错误')); return; } @@ -1755,6 +1854,8 @@ function collectChartConfig() { dualAxis: document.getElementById('dualAxis').checked, leftAxisName: document.getElementById('leftAxisName').value, rightAxisName: document.getElementById('rightAxisName').value, + xAxisName: document.getElementById('xAxisName').value, + xLabelRotate: parseInt(document.getElementById('xLabelRotate').value) || 0, enableSplit: document.getElementById('enableSplit').checked, splitIndex: parseInt(document.getElementById('splitIndex').value) || 3, leftLabel: document.getElementById('leftLabel').value, @@ -1961,6 +2062,8 @@ function applyFavoriteConfig(fav) { document.getElementById('dualAxis').checked = !!cfg.dualAxis; 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('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 a32c951..f60b55c 100644 --- a/index.html +++ b/index.html @@ -80,6 +80,17 @@ C, 20, 30, 40
+
+ +
+ + · + + ° +
+

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

+
+