import { type ChartInstance } from '@/domain/entities/ChartInstance'; import { type StyleConfig } from '@/domain/entities/StyleConfig'; import { type FieldBinding } from '@/domain/entities/FieldBinding'; function getBinding(bindings: FieldBinding[], axis: string): FieldBinding | undefined { return bindings.find((b) => b.axis === axis); } function applyCommonStyle(option: Record, style: StyleConfig): void { if (style.title.visible) { option.title = { text: style.title.text, left: style.title.align, textStyle: { fontSize: style.title.fontSize, fontWeight: style.title.fontWeight, color: style.title.color, }, }; } if (style.legend.visible) { option.legend = { show: true, orient: style.legend.orient, top: style.legend.position === 'top' ? 'top' : style.legend.position === 'bottom' ? 'bottom' : 'middle', left: style.legend.position === 'left' ? 'left' : style.legend.position === 'right' ? 'right' : 'center', textStyle: { fontSize: style.legend.fontSize, color: style.legend.color, }, }; } else { option.legend = { show: false }; } option.backgroundColor = style.background.color !== '' ? style.background.color : 'transparent'; if (style.animation.enabled) { option.animation = true; option.animationDuration = style.animation.duration; option.animationEasing = style.animation.easing === 'ease-in' ? 'cubicIn' : style.animation.easing === 'ease-out' ? 'cubicOut' : style.animation.easing === 'ease-in-out' ? 'cubicInOut' : 'linear'; } else { option.animation = false; } option.grid = { containLabel: true, top: 60, right: 30, bottom: 30, left: 30, }; } function buildAxisConfig(style: StyleConfig, isHorizontal: boolean) { const xAxisStyle = isHorizontal ? style.yAxis : style.xAxis; const yAxisStyle = isHorizontal ? style.xAxis : style.yAxis; const xAxis: Record = { show: xAxisStyle.visible, axisLabel: { show: xAxisStyle.labelVisible, fontSize: xAxisStyle.labelFontSize, color: xAxisStyle.labelColor, rotate: xAxisStyle.labelRotation, }, splitLine: { show: xAxisStyle.gridVisible, lineStyle: { color: xAxisStyle.gridColor }, }, }; if (xAxisStyle.titleVisible && xAxisStyle.titleText) { xAxis.name = xAxisStyle.titleText; } const yAxis: Record = { show: yAxisStyle.visible, axisLabel: { show: yAxisStyle.labelVisible, fontSize: yAxisStyle.labelFontSize, color: yAxisStyle.labelColor, rotate: yAxisStyle.labelRotation, }, splitLine: { show: yAxisStyle.gridVisible, lineStyle: { color: yAxisStyle.gridColor }, }, }; if (yAxisStyle.titleVisible && yAxisStyle.titleText) { yAxis.name = yAxisStyle.titleText; } return { xAxis, yAxis }; } function buildLabelConfig(style: StyleConfig) { 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; } return { show: true, fontSize: style.dataLabel.fontSize, color: style.dataLabel.color, position: style.dataLabel.position, formatter, }; } export function buildBarOption( chart: ChartInstance, data: Record[], ): Record { const { bindings, style, type } = chart; const xBinding = getBinding(bindings, 'x'); const yBinding = getBinding(bindings, 'y'); const seriesBinding = getBinding(bindings, 'series'); const isHorizontal = type === 'horizontal-bar'; const isStacked = type === 'stacked-bar'; const isGrouped = type === 'grouped-bar'; const xField = xBinding?.columnName ?? ''; const yField = yBinding?.columnName ?? ''; const option: Record = {}; applyCommonStyle(option, style); const categories = [...new Set(data.map((row) => String(row[xField])))]; const { xAxis, yAxis } = buildAxisConfig(style, isHorizontal); const label = buildLabelConfig(style); const colorBinding = getBinding(bindings, 'color'); const splitField = seriesBinding?.columnName ?? colorBinding?.columnName; const hasSplit = !!splitField; if (hasSplit) { // Multi-series: split by series or color field const splitNames = [...new Set(data.map((row) => String(row[splitField])))]; 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, ); return matchingRows.reduce((sum, r) => sum + (Number(r[yField]) || 0), 0); }); return { name, type: 'bar', stack: isStacked ? 'total' : undefined, data: seriesData, itemStyle: { color: style.colors[idx % style.colors.length], }, label, }; }); if (isHorizontal) { option.yAxis = { ...xAxis, type: 'category', data: categories }; option.xAxis = { ...yAxis, type: 'value' }; } else { option.xAxis = { ...xAxis, type: 'category', data: categories }; option.yAxis = { ...yAxis, type: 'value' }; } option.series = seriesList; option.tooltip = { trigger: 'axis' }; } else { // Single series: aggregate by x category const seriesData = categories.map((cat) => { const matchingRows = data.filter((r) => String(r[xField]) === cat); return matchingRows.reduce((sum, r) => sum + (Number(r[yField]) || 0), 0); }); if (isHorizontal) { option.yAxis = { ...xAxis, type: 'category', data: categories }; option.xAxis = { ...yAxis, type: 'value' }; } else { option.xAxis = { ...xAxis, type: 'category', data: categories }; option.yAxis = { ...yAxis, type: 'value' }; } option.series = [ { type: 'bar', data: seriesData, itemStyle: { color: style.colors[0], }, label, }, ]; option.tooltip = { trigger: 'axis' }; } return option; }