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:
parent
49ed6a9bcb
commit
b3098eefbd
|
|
@ -46,7 +46,19 @@ function applyCommonStyle(option: Record<string, any>, style: StyleConfig): void
|
||||||
}
|
}
|
||||||
|
|
||||||
option.color = style.colors;
|
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) {
|
if (style.animation.enabled) {
|
||||||
option.animation = true;
|
option.animation = true;
|
||||||
|
|
@ -121,27 +133,36 @@ function buildAxisConfig(style: StyleConfig, isHorizontal: boolean) {
|
||||||
return { xAxis, yAxis };
|
return { xAxis, yAxis };
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLabelConfig(style: StyleConfig) {
|
function buildLabelConfig(style: StyleConfig, data?: number[]) {
|
||||||
if (!style.dataLabel.visible) return { show: false };
|
if (!style.dataLabel.visible) return { show: false };
|
||||||
let formatter: string | undefined;
|
|
||||||
switch (style.dataLabel.format) {
|
const position = style.dataLabel.position ?? 'top';
|
||||||
case 'percent':
|
const fontSize = style.dataLabel.fontSize ?? 12;
|
||||||
formatter = '{b}: {d}%';
|
const color = style.dataLabel.color ?? '#333';
|
||||||
break;
|
|
||||||
case 'custom':
|
if (style.dataLabel.format === 'percent' && data) {
|
||||||
formatter = style.dataLabel.template;
|
const total = data.reduce((a, b) => a + (typeof b === 'number' ? b : 0), 0);
|
||||||
break;
|
return {
|
||||||
case 'value':
|
show: true, fontSize, color, position,
|
||||||
default:
|
formatter: (params: any) => {
|
||||||
formatter = '{c}';
|
const val = typeof params.value === 'object' ? params.value.value ?? params.value : params.value;
|
||||||
break;
|
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 {
|
return {
|
||||||
show: true,
|
show: true, fontSize, color, position,
|
||||||
fontSize: style.dataLabel.fontSize,
|
formatter: (params: any) => {
|
||||||
color: style.dataLabel.color,
|
const val = typeof params.value === 'object' ? params.value.value ?? params.value : params.value;
|
||||||
position: style.dataLabel.position,
|
return typeof val === 'number' ? val.toLocaleString() : String(val);
|
||||||
formatter,
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,11 +189,6 @@ export function buildBarOption(
|
||||||
const categories = [...new Set(data.map((row) => String(row[xField])))];
|
const categories = [...new Set(data.map((row) => String(row[xField])))];
|
||||||
const { xAxis, yAxis } = buildAxisConfig(style, isHorizontal);
|
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 colorBinding = getBinding(bindings, 'color');
|
||||||
const splitField = seriesBinding?.columnName ?? colorBinding?.columnName;
|
const splitField = seriesBinding?.columnName ?? colorBinding?.columnName;
|
||||||
const hasSplit = !!splitField;
|
const hasSplit = !!splitField;
|
||||||
|
|
@ -180,10 +196,13 @@ export function buildBarOption(
|
||||||
if (hasSplit) {
|
if (hasSplit) {
|
||||||
// Multi-series: split by series or color field
|
// Multi-series: split by series or color field
|
||||||
const splitNames = [...new Set(data.map((row) => String(row[splitField])))];
|
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 seriesList = splitNames.map((name, idx) => {
|
||||||
const seriesData = categories.map((cat) => {
|
const seriesData = categories.map((cat) => {
|
||||||
// Sum all matching rows (there may be multiple)
|
|
||||||
const matchingRows = data.filter(
|
const matchingRows = data.filter(
|
||||||
(r) => String(r[xField]) === cat && String(r[splitField]) === name,
|
(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);
|
const matchingRows = data.filter((r) => String(r[xField]) === cat);
|
||||||
return matchingRows.reduce((sum, r) => sum + (Number(r[yField]) || 0), 0);
|
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) {
|
if (isHorizontal) {
|
||||||
option.yAxis = { ...xAxis, type: 'category', data: categories };
|
option.yAxis = { ...xAxis, type: 'category', data: categories };
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,10 @@ export const ChartRenderer: React.FC<ChartRendererProps> = ({ chart, data }) =>
|
||||||
case 'data-table':
|
case 'data-table':
|
||||||
return <DataTable chart={chart} data={data} />;
|
return <DataTable chart={chart} data={data} />;
|
||||||
default:
|
default:
|
||||||
|
const borderRadius = chart.style?.background?.borderRadius ?? 0;
|
||||||
|
const overflow = borderRadius > 0 ? 'hidden' as const : undefined;
|
||||||
return echartsOption
|
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>;
|
: <div style={{ padding: 16, color: '#999', textAlign: 'center' }}>请绑定数据字段</div>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue