From b3098eefbdb5b3b1aeeb245b89c745fc713899ac Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 5 Apr 2026 04:07:55 -0700 Subject: [PATCH] fix: percent labels, background opacity, border radius - Percent label format: calculate actual % from total instead of {d} - Value labels: use toLocaleString for thousand separators - Background opacity: convert hex+opacity to rgba - Border radius: apply to ECharts container div Co-Authored-By: Claude Opus 4.6 (1M context) --- .../EChartsOptionBuilder/barOptionBuilder.ts | 72 ++++++++++++------- .../components/charts/ChartRenderer.tsx | 4 +- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/frontend/src/adapters/gateways/EChartsOptionBuilder/barOptionBuilder.ts b/frontend/src/adapters/gateways/EChartsOptionBuilder/barOptionBuilder.ts index a559f00..db78a7d 100644 --- a/frontend/src/adapters/gateways/EChartsOptionBuilder/barOptionBuilder.ts +++ b/frontend/src/adapters/gateways/EChartsOptionBuilder/barOptionBuilder.ts @@ -46,7 +46,19 @@ function applyCommonStyle(option: Record, style: StyleConfig): void } option.color = style.colors; - option.backgroundColor = style.background.color !== '' ? style.background.color : 'transparent'; + // Convert background color + opacity to rgba + const bgColor = style.background.color || 'transparent'; + const bgOpacity = style.background.opacity ?? 1; + if (bgColor !== 'transparent' && bgOpacity < 1) { + // Parse hex to rgba + const hex = bgColor.replace('#', ''); + const r = parseInt(hex.substring(0, 2), 16) || 0; + const g = parseInt(hex.substring(2, 4), 16) || 0; + const b = parseInt(hex.substring(4, 6), 16) || 0; + option.backgroundColor = `rgba(${r},${g},${b},${bgOpacity})`; + } else { + option.backgroundColor = bgColor; + } if (style.animation.enabled) { option.animation = true; @@ -121,27 +133,36 @@ function buildAxisConfig(style: StyleConfig, isHorizontal: boolean) { return { xAxis, yAxis }; } -function buildLabelConfig(style: StyleConfig) { +function buildLabelConfig(style: StyleConfig, data?: number[]) { if (!style.dataLabel.visible) return { show: false }; - let formatter: string | undefined; - switch (style.dataLabel.format) { - case 'percent': - formatter = '{b}: {d}%'; - break; - case 'custom': - formatter = style.dataLabel.template; - break; - case 'value': - default: - formatter = '{c}'; - break; + + const position = style.dataLabel.position ?? 'top'; + const fontSize = style.dataLabel.fontSize ?? 12; + const color = style.dataLabel.color ?? '#333'; + + if (style.dataLabel.format === 'percent' && data) { + const total = data.reduce((a, b) => a + (typeof b === 'number' ? b : 0), 0); + return { + show: true, fontSize, color, position, + formatter: (params: any) => { + const val = typeof params.value === 'object' ? params.value.value ?? params.value : params.value; + const pct = total > 0 ? ((val / total) * 100).toFixed(1) : '0'; + return pct + '%'; + }, + }; } + + if (style.dataLabel.format === 'custom' && style.dataLabel.template) { + return { show: true, fontSize, color, position, formatter: style.dataLabel.template }; + } + + // Default: value return { - show: true, - fontSize: style.dataLabel.fontSize, - color: style.dataLabel.color, - position: style.dataLabel.position, - formatter, + show: true, fontSize, color, position, + formatter: (params: any) => { + const val = typeof params.value === 'object' ? params.value.value ?? params.value : params.value; + return typeof val === 'number' ? val.toLocaleString() : String(val); + }, }; } @@ -168,11 +189,6 @@ export function buildBarOption( const categories = [...new Set(data.map((row) => String(row[xField])))]; const { xAxis, yAxis } = buildAxisConfig(style, isHorizontal); - // If a label binding exists, force show data labels - const label = labelBinding - ? { show: true, position: 'top' as const, fontSize: 12, formatter: '{c}' } - : buildLabelConfig(style); - const colorBinding = getBinding(bindings, 'color'); const splitField = seriesBinding?.columnName ?? colorBinding?.columnName; const hasSplit = !!splitField; @@ -180,10 +196,13 @@ export function buildBarOption( if (hasSplit) { // Multi-series: split by series or color field const splitNames = [...new Set(data.map((row) => String(row[splitField])))]; + const allNums = data.map((r) => Number(r[yField]) || 0); + const label = labelBinding + ? { show: true, position: 'top' as const, fontSize: 12, formatter: (p: any) => (typeof p.value === 'number' ? p.value.toLocaleString() : String(p.value)) } + : buildLabelConfig(style, allNums); const seriesList = splitNames.map((name, idx) => { const seriesData = categories.map((cat) => { - // Sum all matching rows (there may be multiple) const matchingRows = data.filter( (r) => String(r[xField]) === cat && String(r[splitField]) === name, ); @@ -217,6 +236,9 @@ export function buildBarOption( const matchingRows = data.filter((r) => String(r[xField]) === cat); return matchingRows.reduce((sum, r) => sum + (Number(r[yField]) || 0), 0); }); + const label = labelBinding + ? { show: true, position: 'top' as const, fontSize: 12, formatter: (p: any) => { const v = typeof p.value === 'object' ? p.value.value : p.value; return typeof v === 'number' ? v.toLocaleString() : String(v); } } + : buildLabelConfig(style, seriesData); if (isHorizontal) { option.yAxis = { ...xAxis, type: 'category', data: categories }; diff --git a/frontend/src/frameworks/components/charts/ChartRenderer.tsx b/frontend/src/frameworks/components/charts/ChartRenderer.tsx index af24a77..1f16281 100644 --- a/frontend/src/frameworks/components/charts/ChartRenderer.tsx +++ b/frontend/src/frameworks/components/charts/ChartRenderer.tsx @@ -34,8 +34,10 @@ export const ChartRenderer: React.FC = ({ chart, data }) => case 'data-table': return ; default: + const borderRadius = chart.style?.background?.borderRadius ?? 0; + const overflow = borderRadius > 0 ? 'hidden' as const : undefined; return echartsOption - ? + ? :
请绑定数据字段
; } };