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) <noreply@anthropic.com>
This commit is contained in:
hailin 2026-04-05 04:07:55 -07:00
parent 49ed6a9bcb
commit b3098eefbd
2 changed files with 50 additions and 26 deletions

View File

@ -46,7 +46,19 @@ function applyCommonStyle(option: Record<string, any>, 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 };

View File

@ -34,8 +34,10 @@ export const ChartRenderer: React.FC<ChartRendererProps> = ({ chart, data }) =>
case 'data-table':
return <DataTable chart={chart} data={data} />;
default:
const borderRadius = chart.style?.background?.borderRadius ?? 0;
const overflow = borderRadius > 0 ? 'hidden' as const : undefined;
return echartsOption
? <EChartsBase option={echartsOption} style={{ width: '100%', height: '100%', minHeight: 250 }} />
? <EChartsBase option={echartsOption} style={{ width: '100%', height: '100%', minHeight: 250, borderRadius, overflow }} />
: <div style={{ padding: 16, color: '#999', textAlign: 'center' }}></div>;
}
};