diff --git a/frontend/src/adapters/gateways/EChartsOptionBuilder/wordcloudOptionBuilder.ts b/frontend/src/adapters/gateways/EChartsOptionBuilder/wordcloudOptionBuilder.ts index 2ce24d1..adb3029 100644 --- a/frontend/src/adapters/gateways/EChartsOptionBuilder/wordcloudOptionBuilder.ts +++ b/frontend/src/adapters/gateways/EChartsOptionBuilder/wordcloudOptionBuilder.ts @@ -5,6 +5,51 @@ function getBinding(bindings: FieldBinding[], axis: string): FieldBinding | unde return bindings.find((b) => b.axis === axis); } +// Wordcloud-specific config stored in style.wordcloud (custom extension) +interface WordcloudConfig { + shape: string; // circle, cardioid, diamond, triangle, star, pentagon + sizeMin: number; // min font size + sizeMax: number; // max font size + rotationMin: number; // rotation range min (degrees) + rotationMax: number; // rotation range max (degrees) + rotationStep: number; + gridSize: number; // gap between words + fontFamily: string; + fontWeight: string; + colorMode: 'palette' | 'random' | 'gradient'; +} + +const DEFAULT_WORDCLOUD: WordcloudConfig = { + shape: 'circle', + sizeMin: 14, + sizeMax: 80, + rotationMin: -45, + rotationMax: 45, + rotationStep: 15, + gridSize: 8, + fontFamily: 'sans-serif', + fontWeight: 'bold', + colorMode: 'palette', +}; + +function getWordcloudConfig(style: any): WordcloudConfig { + const wc = style?.wordcloud ?? {}; + return { ...DEFAULT_WORDCLOUD, ...wc }; +} + +function randomColor(): string { + const hue = Math.floor(Math.random() * 360); + const sat = 50 + Math.floor(Math.random() * 40); + const light = 35 + Math.floor(Math.random() * 30); + return `hsl(${hue}, ${sat}%, ${light}%)`; +} + +function gradientColor(ratio: number, colors: string[]): string { + if (colors.length < 2) return colors[0] || '#5470c6'; + const idx = ratio * (colors.length - 1); + return colors[Math.round(idx)] || colors[0]; +} + export function buildWordcloudOption( chart: ChartInstance, data: Record[], @@ -16,12 +61,19 @@ export function buildWordcloudOption( const labelField = labelBinding?.columnName ?? ''; const valueField = valueBinding?.columnName ?? ''; + if (!labelField || !valueField || !data.length) { + return { title: { text: '请绑定 标签 和 值 字段', left: 'center', top: 'center', textStyle: { color: '#999', fontSize: 14 } } }; + } + + const wc = getWordcloudConfig(style); + const option: Record = {}; - if (style.title.visible) { + // Title + if (style.title?.visible) { option.title = { text: style.title.text, - left: style.title.align, + left: style.title.align || 'center', textStyle: { fontSize: style.title.fontSize, fontWeight: style.title.fontWeight, @@ -30,30 +82,62 @@ export function buildWordcloudOption( }; } - option.backgroundColor = style.background.color || 'transparent'; - option.tooltip = { show: true }; + option.backgroundColor = style.background?.color || 'transparent'; + option.tooltip = { + show: true, + formatter: (params: any) => `${params.name}: ${Number(params.value).toLocaleString()}`, + }; - const wordData = data.map((row, idx) => ({ - name: String(row[labelField]), - value: Number(row[valueField]), - textStyle: { - color: style.colors[idx % style.colors.length], - }, - })); + // Sort by value descending for better layout + const sorted = [...data].sort((a, b) => Number(b[valueField]) - Number(a[valueField])); + const maxVal = Number(sorted[0]?.[valueField]) || 1; + + const wordData = sorted.map((row, idx) => { + const ratio = Number(row[valueField]) / maxVal; + let color: string; + if (wc.colorMode === 'random') { + color = randomColor(); + } else if (wc.colorMode === 'gradient') { + color = gradientColor(ratio, style.colors || ['#5470c6', '#91cc75', '#fac858', '#ee6666']); + } else { + color = (style.colors || [])[idx % (style.colors?.length || 8)] || '#5470c6'; + } + + return { + name: String(row[labelField]), + value: Number(row[valueField]), + textStyle: { + color, + fontFamily: wc.fontFamily, + }, + }; + }); option.series = [ { type: 'wordCloud', - shape: 'circle', + shape: wc.shape, left: 'center', - top: 'center', - width: '90%', - height: '90%', - sizeRange: [14, 60], - rotationRange: [-45, 45], - rotationStep: 15, - gridSize: 8, + top: style.title?.visible ? 40 : 'center', + width: '85%', + height: style.title?.visible ? '80%' : '90%', + sizeRange: [wc.sizeMin, wc.sizeMax], + rotationRange: [wc.rotationMin, wc.rotationMax], + rotationStep: wc.rotationStep, + gridSize: wc.gridSize, drawOutOfBound: false, + layoutAnimation: true, + textStyle: { + fontFamily: wc.fontFamily, + fontWeight: wc.fontWeight, + }, + emphasis: { + focus: 'self', + textStyle: { + textShadowBlur: 10, + textShadowColor: 'rgba(0,0,0,0.3)', + }, + }, data: wordData, }, ]; diff --git a/frontend/src/frameworks/components/configPanel/StyleConfig.tsx b/frontend/src/frameworks/components/configPanel/StyleConfig.tsx index 5055bf0..d075508 100644 --- a/frontend/src/frameworks/components/configPanel/StyleConfig.tsx +++ b/frontend/src/frameworks/components/configPanel/StyleConfig.tsx @@ -10,6 +10,7 @@ import AxisConfig from './AxisConfig'; import LabelConfig from './LabelConfig'; import SizeConfig from './SizeConfig'; import InteractionConfig from './InteractionConfig'; +import WordcloudConfig from './WordcloudConfig'; export default function StyleConfigPanel() { const { chart } = useChartConfig(); @@ -18,13 +19,25 @@ export default function StyleConfigPanel() { return ; } + const isWordcloud = chart.type === 'wordcloud'; + const isKPI = chart.type === 'kpi'; + const isPie = chart.type === 'pie' || chart.type === 'donut'; + const isTable = chart.type === 'data-table'; + const items = [ { key: 'title', label: '标题', children: }, { key: 'colors', label: '颜色', children: }, - { key: 'legend', label: '图例', children: }, - { key: 'xAxis', label: 'X轴', children: }, - { key: 'yAxis', label: 'Y轴', children: }, - { key: 'dataLabel', label: '数据标签', children: }, + // Wordcloud-specific settings + ...(isWordcloud ? [{ key: 'wordcloud', label: '词云设置', children: }] : []), + // Legend not for KPI/table + ...(!isKPI && !isTable ? [{ key: 'legend', label: '图例', children: }] : []), + // Axes only for cartesian charts + ...(!isWordcloud && !isPie && !isKPI && !isTable ? [ + { key: 'xAxis', label: 'X轴', children: }, + { key: 'yAxis', label: 'Y轴', children: }, + ] : []), + // Data labels not for KPI/wordcloud + ...(!isKPI && !isWordcloud ? [{ key: 'dataLabel', label: '数据标签', children: }] : []), { key: 'background', label: '背景', children: }, { key: 'animation', label: '动画', children: }, ]; @@ -34,7 +47,7 @@ export default function StyleConfigPanel() { accordion size="small" items={items} - defaultActiveKey={['title']} + defaultActiveKey={isWordcloud ? ['wordcloud'] : ['title']} style={{ background: 'transparent' }} /> ); diff --git a/frontend/src/frameworks/components/configPanel/WordcloudConfig.tsx b/frontend/src/frameworks/components/configPanel/WordcloudConfig.tsx new file mode 100644 index 0000000..75e2c06 --- /dev/null +++ b/frontend/src/frameworks/components/configPanel/WordcloudConfig.tsx @@ -0,0 +1,166 @@ +'use client'; + +import React, { useCallback } from 'react'; +import { Select, Slider, InputNumber, Space, Typography, Radio } from 'antd'; +import { useChartConfig } from '@/frameworks/hooks/useChartConfig'; + +const { Text } = Typography; + +const SHAPES = [ + { label: '圆形', value: 'circle' }, + { label: '心形', value: 'cardioid' }, + { label: '菱形', value: 'diamond' }, + { label: '三角形', value: 'triangle' }, + { label: '五角星', value: 'star' }, + { label: '五边形', value: 'pentagon' }, +]; + +const FONTS = [ + { label: '默认 (sans-serif)', value: 'sans-serif' }, + { label: '宋体', value: 'SimSun, serif' }, + { label: '微软雅黑', value: 'Microsoft YaHei, sans-serif' }, + { label: '等宽', value: 'monospace' }, + { label: 'Impact', value: 'Impact, sans-serif' }, +]; + +const WEIGHTS = [ + { label: '正常', value: 'normal' }, + { label: '粗体', value: 'bold' }, + { label: '更粗', value: '900' }, +]; + +export default function WordcloudConfig() { + const { chart, setStyle } = useChartConfig(); + + const wc = (chart?.style as any)?.wordcloud ?? {}; + const config = { + shape: wc.shape ?? 'circle', + sizeMin: wc.sizeMin ?? 14, + sizeMax: wc.sizeMax ?? 80, + rotationMin: wc.rotationMin ?? -45, + rotationMax: wc.rotationMax ?? 45, + rotationStep: wc.rotationStep ?? 15, + gridSize: wc.gridSize ?? 8, + fontFamily: wc.fontFamily ?? 'sans-serif', + fontWeight: wc.fontWeight ?? 'bold', + colorMode: wc.colorMode ?? 'palette', + }; + + const update = useCallback( + (partial: Record) => { + setStyle({ wordcloud: { ...config, ...partial } } as any); + }, + [config, setStyle], + ); + + if (chart?.type !== 'wordcloud') return null; + + return ( + + {/* Shape */} +
+ 形状 + update({ fontFamily: val })} + options={FONTS} + size="small" + style={{ width: '100%', marginTop: 4 }} + /> +
+ + {/* Font Weight */} +
+ 字重 +