mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-12 12:37:55 +00:00
@@ -81,6 +81,7 @@ export enum MsgContentTypeEnum {
|
|||||||
METRIC_TREND = 'METRIC_TREND',
|
METRIC_TREND = 'METRIC_TREND',
|
||||||
METRIC_BAR = 'METRIC_BAR',
|
METRIC_BAR = 'METRIC_BAR',
|
||||||
MARKDOWN = 'MARKDOWN',
|
MARKDOWN = 'MARKDOWN',
|
||||||
|
METRIC_PIE = 'METRIC_PIE',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ChatContextTypeQueryTypeEnum {
|
export enum ChatContextTypeQueryTypeEnum {
|
||||||
|
|||||||
@@ -123,9 +123,11 @@ const ExecuteItem: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{[MsgContentTypeEnum.METRIC_TREND, MsgContentTypeEnum.METRIC_BAR].includes(
|
{[
|
||||||
msgContentType as MsgContentTypeEnum
|
MsgContentTypeEnum.METRIC_TREND,
|
||||||
) && (
|
MsgContentTypeEnum.METRIC_BAR,
|
||||||
|
MsgContentTypeEnum.METRIC_PIE,
|
||||||
|
].includes(msgContentType as MsgContentTypeEnum) && (
|
||||||
<Switch
|
<Switch
|
||||||
checkedChildren="表格"
|
checkedChildren="表格"
|
||||||
unCheckedChildren="表格"
|
unCheckedChildren="表格"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { useExportByEcharts } from '../../../hooks';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: MsgDataType;
|
data: MsgDataType;
|
||||||
question: string;
|
question?: string;
|
||||||
triggerResize?: boolean;
|
triggerResize?: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
metricField: ColumnType;
|
metricField: ColumnType;
|
||||||
@@ -32,7 +32,7 @@ type Props = {
|
|||||||
|
|
||||||
const BarChart: React.FC<Props> = ({
|
const BarChart: React.FC<Props> = ({
|
||||||
data,
|
data,
|
||||||
question,
|
question="",
|
||||||
triggerResize,
|
triggerResize,
|
||||||
loading,
|
loading,
|
||||||
metricField,
|
metricField,
|
||||||
|
|||||||
123
webapp/packages/chat-sdk/src/components/ChatMsg/Pie/PieChart.tsx
Normal file
123
webapp/packages/chat-sdk/src/components/ChatMsg/Pie/PieChart.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { PREFIX_CLS, THEME_COLOR_LIST } from '../../../common/constants';
|
||||||
|
import { MsgDataType } from '../../../common/type';
|
||||||
|
import { formatByDecimalPlaces, getFormattedValue } from '../../../utils/utils';
|
||||||
|
import type { ECharts } from 'echarts';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { ColumnType } from '../../../common/type';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: MsgDataType;
|
||||||
|
metricField: ColumnType;
|
||||||
|
categoryField: ColumnType;
|
||||||
|
triggerResize?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PieChart: React.FC<Props> = ({
|
||||||
|
data,
|
||||||
|
metricField,
|
||||||
|
categoryField,
|
||||||
|
triggerResize,
|
||||||
|
}) => {
|
||||||
|
const chartRef = useRef<any>();
|
||||||
|
const instanceRef = useRef<ECharts>();
|
||||||
|
|
||||||
|
const { queryResults } = data;
|
||||||
|
const categoryColumnName = categoryField?.bizName || '';
|
||||||
|
const metricColumnName = metricField?.bizName || '';
|
||||||
|
|
||||||
|
const renderChart = () => {
|
||||||
|
let instanceObj: any;
|
||||||
|
if (!instanceRef.current) {
|
||||||
|
instanceObj = echarts.init(chartRef.current);
|
||||||
|
instanceRef.current = instanceObj;
|
||||||
|
} else {
|
||||||
|
instanceObj = instanceRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = queryResults || [];
|
||||||
|
const seriesData = data.map((item, index) => {
|
||||||
|
const value = item[metricColumnName];
|
||||||
|
const name = item[categoryColumnName] !== undefined ? item[categoryColumnName] : '未知';
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
itemStyle: {
|
||||||
|
color: THEME_COLOR_LIST[index % THEME_COLOR_LIST.length],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
instanceObj.setOption({
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: function (params: any) {
|
||||||
|
const value = params.value;
|
||||||
|
return `${params.name}: ${
|
||||||
|
metricField.dataFormatType === 'percent'
|
||||||
|
? `${formatByDecimalPlaces(
|
||||||
|
metricField.dataFormat?.needMultiply100 ? +value * 100 : value,
|
||||||
|
metricField.dataFormat?.decimalPlaces || 2
|
||||||
|
)}%`
|
||||||
|
: getFormattedValue(value)
|
||||||
|
}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 'left',
|
||||||
|
type: 'scroll',
|
||||||
|
data: seriesData.map(item => item.name),
|
||||||
|
selectedMode: true,
|
||||||
|
textStyle: {
|
||||||
|
color: '#666',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '占比',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 10,
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
position: 'center',
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: '14',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
data: seriesData,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
instanceObj.resize();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (queryResults && queryResults.length > 0) {
|
||||||
|
renderChart();
|
||||||
|
}
|
||||||
|
}, [queryResults, metricField, categoryField]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (triggerResize && instanceRef.current) {
|
||||||
|
instanceRef.current.resize();
|
||||||
|
}
|
||||||
|
}, [triggerResize]);
|
||||||
|
|
||||||
|
return <div className={`${PREFIX_CLS}-pie-chart`} ref={chartRef} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PieChart;
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { PREFIX_CLS } from '../../../common/constants';
|
||||||
|
import { MsgDataType } from '../../../common/type';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import NoPermissionChart from '../NoPermissionChart';
|
||||||
|
import { ColumnType } from '../../../common/type';
|
||||||
|
import { Spin, Select } from 'antd';
|
||||||
|
import PieChart from './PieChart';
|
||||||
|
import Bar from '../Bar';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: MsgDataType;
|
||||||
|
question: string;
|
||||||
|
triggerResize?: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
metricField: ColumnType;
|
||||||
|
categoryField: ColumnType;
|
||||||
|
onApplyAuth?: (model: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const metricChartSelectOptions = [
|
||||||
|
{
|
||||||
|
value: 'pie',
|
||||||
|
label: '饼图',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'bar',
|
||||||
|
label: '柱状图',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Pie: React.FC<Props> = ({
|
||||||
|
data,
|
||||||
|
question,
|
||||||
|
triggerResize,
|
||||||
|
loading,
|
||||||
|
metricField,
|
||||||
|
categoryField,
|
||||||
|
onApplyAuth,
|
||||||
|
}) => {
|
||||||
|
const [chartType, setChartType] = useState('pie');
|
||||||
|
const { entityInfo } = data;
|
||||||
|
|
||||||
|
if (metricField && !metricField?.authorized) {
|
||||||
|
return (
|
||||||
|
<NoPermissionChart
|
||||||
|
model={entityInfo?.dataSetInfo?.name || ''}
|
||||||
|
chartType="pieChart"
|
||||||
|
onApplyAuth={onApplyAuth}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefixCls = `${PREFIX_CLS}-pie`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={prefixCls}>
|
||||||
|
<div className={`${prefixCls}-metric-fields ${prefixCls}-metric-field-single`}>
|
||||||
|
{question}
|
||||||
|
</div>
|
||||||
|
<div className={`${prefixCls}-select-options`}>
|
||||||
|
<Select
|
||||||
|
defaultValue="pie"
|
||||||
|
bordered={false}
|
||||||
|
options={metricChartSelectOptions}
|
||||||
|
onChange={(value: string) => setChartType(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{chartType === 'pie' ? (
|
||||||
|
<PieChart
|
||||||
|
data={data}
|
||||||
|
metricField={metricField}
|
||||||
|
categoryField={categoryField}
|
||||||
|
triggerResize={triggerResize}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Bar
|
||||||
|
data={data}
|
||||||
|
triggerResize={triggerResize}
|
||||||
|
loading={loading}
|
||||||
|
metricField={metricField}
|
||||||
|
onApplyAuth={onApplyAuth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pie;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
@import '../../../styles/index.less';
|
||||||
|
|
||||||
|
@pie-prefix-cls: ~'@{prefix-cls}-pie';
|
||||||
|
|
||||||
|
.@{pie-prefix-cls} {
|
||||||
|
&-select-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 400px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-metric-fields {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
column-gap: 8px;
|
||||||
|
row-gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-metric-field-single {
|
||||||
|
padding-left: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: default;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-indicator-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import Text from './Text';
|
|||||||
import DrillDownDimensions from '../DrillDownDimensions';
|
import DrillDownDimensions from '../DrillDownDimensions';
|
||||||
import MetricOptions from '../MetricOptions';
|
import MetricOptions from '../MetricOptions';
|
||||||
import { isMobile } from '../../utils/utils';
|
import { isMobile } from '../../utils/utils';
|
||||||
|
import Pie from './Pie';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
queryId?: number;
|
queryId?: number;
|
||||||
@@ -120,6 +121,16 @@ const ChatMsg: React.FC<Props> = ({
|
|||||||
return MsgContentTypeEnum.METRIC_TREND;
|
return MsgContentTypeEnum.METRIC_TREND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMetricPie =
|
||||||
|
metricFields.length > 0 &&
|
||||||
|
metricFields?.length === 1 &&
|
||||||
|
(isMobile ? dataSource?.length <= 5 : dataSource?.length <= 10) &&
|
||||||
|
dataSource.every(item => item[metricFields[0].bizName] > 0);
|
||||||
|
|
||||||
|
if (isMetricPie) {
|
||||||
|
return MsgContentTypeEnum.METRIC_PIE;
|
||||||
|
}
|
||||||
|
|
||||||
const isMetricBar =
|
const isMetricBar =
|
||||||
categoryField?.length > 0 &&
|
categoryField?.length > 0 &&
|
||||||
metricFields?.length === 1 &&
|
metricFields?.length === 1 &&
|
||||||
@@ -148,7 +159,7 @@ const ChatMsg: React.FC<Props> = ({
|
|||||||
[queryColumns.length > 5 ? 'width' : 'minWidth']: queryColumns.length * 150,
|
[queryColumns.length > 5 ? 'width' : 'minWidth']: queryColumns.length * 150,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (type === MsgContentTypeEnum.METRIC_TREND) {
|
if (type === MsgContentTypeEnum.METRIC_TREND || type === MsgContentTypeEnum.METRIC_PIE) {
|
||||||
return { width: 'calc(100vw - 410px)' };
|
return { width: 'calc(100vw - 410px)' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -213,6 +224,21 @@ const ChatMsg: React.FC<Props> = ({
|
|||||||
metricField={metricFields[0]}
|
metricField={metricFields[0]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case MsgContentTypeEnum.METRIC_PIE:
|
||||||
|
const categoryField = columns.find(item => item.showType === 'CATEGORY');
|
||||||
|
if (!categoryField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Pie
|
||||||
|
data={{ ...data, queryColumns: columns, queryResults: dataSource }}
|
||||||
|
question={question}
|
||||||
|
triggerResize={triggerResize}
|
||||||
|
loading={loading}
|
||||||
|
metricField={metricFields[0]}
|
||||||
|
categoryField={categoryField}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case MsgContentTypeEnum.MARKDOWN:
|
case MsgContentTypeEnum.MARKDOWN:
|
||||||
return (
|
return (
|
||||||
<div style={{ maxHeight: 800 }}>
|
<div style={{ maxHeight: 800 }}>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
@import "../components/ChatMsg/MetricTrend/style.less";
|
@import "../components/ChatMsg/MetricTrend/style.less";
|
||||||
|
|
||||||
|
@import "../components/ChatMsg/Pie/style.less";
|
||||||
|
|
||||||
@import "../components/ChatMsg/ApplyAuth/style.less";
|
@import "../components/ChatMsg/ApplyAuth/style.less";
|
||||||
|
|
||||||
@import "../components/ChatMsg/NoPermissionChart/style.less";
|
@import "../components/ChatMsg/NoPermissionChart/style.less";
|
||||||
|
|||||||
Reference in New Issue
Block a user