dv/frontend/src/adapters/gateways/EChartsOptionBuilder/barOptionBuilder.ts

220 lines
6.3 KiB
TypeScript

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<string, any>, 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<string, any> = {
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<string, any> = {
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<string, any>[],
): Record<string, any> {
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<string, any> = {};
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;
}