import { type ChartInstance } from '@/domain/entities/ChartInstance'; import { type FieldBinding } from '@/domain/entities/FieldBinding'; function getBinding(bindings: FieldBinding[], axis: string): FieldBinding | undefined { return bindings.find((b) => b.axis === axis); } export function buildHeatmapOption( chart: ChartInstance, data: Record[], ): Record { const { bindings, style } = chart; const xBinding = getBinding(bindings, 'x'); const yBinding = getBinding(bindings, 'y'); const valueBinding = getBinding(bindings, 'value'); const xField = xBinding?.columnName ?? ''; const yField = yBinding?.columnName ?? ''; const valueField = valueBinding?.columnName ?? ''; const option: Record = {}; 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 || 'transparent'; option.tooltip = { position: 'top' }; option.grid = { containLabel: true, top: 60, right: 80, bottom: 30, left: 30 }; if (style.animation.enabled) { option.animation = true; option.animationDuration = style.animation.duration; } else { option.animation = false; } const xCategories = [...new Set(data.map((row) => String(row[xField])))]; const yCategories = [...new Set(data.map((row) => String(row[yField])))]; option.xAxis = { type: 'category', data: xCategories, show: style.xAxis.visible, axisLabel: { show: style.xAxis.labelVisible, fontSize: style.xAxis.labelFontSize, color: style.xAxis.labelColor, rotate: style.xAxis.labelRotation, }, splitArea: { show: true }, }; option.yAxis = { type: 'category', data: yCategories, show: style.yAxis.visible, axisLabel: { show: style.yAxis.labelVisible, fontSize: style.yAxis.labelFontSize, color: style.yAxis.labelColor, }, splitArea: { show: true }, }; const allValues = data.map((row) => Number(row[valueField])); const minVal = Math.min(...allValues); const maxVal = Math.max(...allValues); const heatmapData = data.map((row) => [ xCategories.indexOf(String(row[xField])), yCategories.indexOf(String(row[yField])), Number(row[valueField]), ]); option.visualMap = { min: minVal, max: maxVal, calculable: true, orient: 'horizontal', left: 'center', bottom: 0, inRange: { color: style.colors.length >= 2 ? style.colors.slice(0, 2) : ['#313695', '#d73027'], }, }; let label: Record; if (style.dataLabel.visible) { 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; } label = { show: true, fontSize: style.dataLabel.fontSize, color: style.dataLabel.color, formatter, }; } else { label = { show: false }; } option.series = [ { type: 'heatmap', data: heatmapData, label, emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' }, }, }, ]; return option; }