index.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import { ChatContext } from '@/app/chat-context';
  2. import i18n, { I18nKeys } from '@/app/i18n';
  3. import { DownloadOutlined } from '@ant-design/icons';
  4. import { Advice, Advisor, Datum } from '@antv/ava';
  5. import { Chart, ChartRef } from '@berryv/g2-react';
  6. import { Button, Col, Empty, Row, Select, Space, Tooltip } from 'antd';
  7. import { compact, concat, uniq } from 'lodash';
  8. import { useContext, useEffect, useMemo, useRef, useState } from 'react';
  9. import { downloadImage } from '../helpers/downloadChartImage';
  10. import { customizeAdvisor, getVisAdvices } from './advisor/pipeline';
  11. import { defaultAdvicesFilter } from './advisor/utils';
  12. import { customCharts } from './charts';
  13. import { processNilData, sortData } from './charts/util';
  14. import { AutoChartProps, ChartType, CustomAdvisorConfig, CustomChart, Specification } from './types';
  15. const { Option } = Select;
  16. export const AutoChart = (props: AutoChartProps) => {
  17. const { data: originalData, chartType, scopeOfCharts, ruleConfig } = props;
  18. // 处理空值数据 (为'-'的数据)
  19. const data = processNilData(originalData) as Datum[];
  20. const { mode } = useContext(ChatContext);
  21. const [advisor, setAdvisor] = useState<Advisor>();
  22. const [advices, setAdvices] = useState<Advice[]>([]);
  23. const [renderChartType, setRenderChartType] = useState<ChartType>();
  24. const chartRef = useRef<ChartRef>();
  25. useEffect(() => {
  26. const input_charts: CustomChart[] = customCharts;
  27. const advisorConfig: CustomAdvisorConfig = {
  28. charts: input_charts,
  29. scopeOfCharts: {
  30. // 排除面积图
  31. exclude: ['area_chart', 'stacked_area_chart', 'percent_stacked_area_chart'],
  32. },
  33. ruleConfig,
  34. };
  35. setAdvisor(customizeAdvisor(advisorConfig));
  36. }, [ruleConfig, scopeOfCharts]);
  37. /** 将 AVA 得到的图表推荐结果和模型的合并 */
  38. const getMergedAdvices = (avaAdvices: Advice[]) => {
  39. if (!advisor) return [];
  40. const filteredAdvices = defaultAdvicesFilter({
  41. advices: avaAdvices,
  42. });
  43. const allChartTypes = uniq(
  44. compact(
  45. concat(
  46. chartType,
  47. avaAdvices.map(item => item.type),
  48. ),
  49. ),
  50. );
  51. const allAdvices = allChartTypes
  52. .map(chartTypeItem => {
  53. const avaAdvice = filteredAdvices.find(item => item.type === chartTypeItem);
  54. // 如果在 AVA 推荐列表中,直接采用推荐列表中的结果
  55. if (avaAdvice) {
  56. return avaAdvice;
  57. }
  58. // 如果不在,则单独为其生成图表 spec
  59. const dataAnalyzerOutput = advisor.dataAnalyzer.execute({ data });
  60. if ('data' in dataAnalyzerOutput) {
  61. const specGeneratorOutput = advisor.specGenerator.execute({
  62. data: dataAnalyzerOutput.data,
  63. dataProps: dataAnalyzerOutput.dataProps,
  64. chartTypeRecommendations: [{ chartType: chartTypeItem, score: 1 }],
  65. });
  66. if ('advices' in specGeneratorOutput) return specGeneratorOutput.advices?.[0];
  67. }
  68. })
  69. .filter(advice => advice?.spec) as Advice[];
  70. return allAdvices;
  71. };
  72. useEffect(() => {
  73. if (data && advisor) {
  74. const avaAdvices = getVisAdvices({
  75. data,
  76. myChartAdvisor: advisor,
  77. });
  78. // 合并模型推荐的图表类型和 ava 推荐的图表类型
  79. const allAdvices = getMergedAdvices(avaAdvices);
  80. setAdvices(allAdvices);
  81. setRenderChartType(allAdvices[0]?.type as ChartType);
  82. }
  83. }, [JSON.stringify(data), advisor, chartType]);
  84. const visComponent = useMemo(() => {
  85. /* Advices exist, render the chart. */
  86. if (advices?.length > 0) {
  87. const chartTypeInput = renderChartType ?? advices[0].type;
  88. const spec: Specification = advices?.find((item: Advice) => item.type === chartTypeInput)?.spec ?? undefined;
  89. if (spec) {
  90. if (spec.data && ['line_chart', 'step_line_chart'].includes(chartTypeInput)) {
  91. // 处理 ava 内置折线图的排序问题
  92. const dataAnalyzerOutput = advisor?.dataAnalyzer.execute({ data });
  93. if (dataAnalyzerOutput && 'dataProps' in dataAnalyzerOutput) {
  94. spec.data = sortData({
  95. data: spec.data,
  96. xField: dataAnalyzerOutput.dataProps?.find((field: any) => field.recommendation === 'date'),
  97. chartType: chartTypeInput,
  98. });
  99. }
  100. }
  101. if (chartTypeInput === 'pie_chart' && spec?.encode?.color) {
  102. // 补充饼图的 tooltip title 展示
  103. spec.tooltip = { title: { field: spec.encode.color } };
  104. }
  105. return (
  106. <Chart
  107. key={chartTypeInput}
  108. options={{
  109. ...spec,
  110. autoFit: true,
  111. theme: mode,
  112. height: 300,
  113. }}
  114. ref={chartRef}
  115. />
  116. );
  117. }
  118. }
  119. }, [advices, mode, renderChartType]);
  120. if (renderChartType) {
  121. return (
  122. <div>
  123. <Row justify='space-between' className='mb-2'>
  124. <Col>
  125. <Space>
  126. <span>{i18n.t('Advices')}</span>
  127. <Select
  128. className='w-52'
  129. value={renderChartType}
  130. placeholder={'Chart Switcher'}
  131. onChange={value => setRenderChartType(value)}
  132. size={'small'}
  133. >
  134. {advices?.map(item => {
  135. const name = i18n.t(item.type as I18nKeys);
  136. return (
  137. <Option key={item.type} value={item.type}>
  138. <Tooltip title={name} placement={'right'}>
  139. <div>{name}</div>
  140. </Tooltip>
  141. </Option>
  142. );
  143. })}
  144. </Select>
  145. </Space>
  146. </Col>
  147. <Col>
  148. <Tooltip title={i18n.t('Download')}>
  149. <Button
  150. onClick={() => downloadImage(chartRef.current, i18n.t(renderChartType as I18nKeys))}
  151. icon={<DownloadOutlined />}
  152. type='text'
  153. />
  154. </Tooltip>
  155. </Col>
  156. </Row>
  157. <div className='flex'>{visComponent}</div>
  158. </div>
  159. );
  160. }
  161. return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={'暂无合适的可视化视图'} />;
  162. };
  163. export * from './helpers';