Merge pull request #3 from sevenliu1896/feature/tristanliu

[improvement](semantic-fe) add dimension&datasource relation graph an…
This commit is contained in:
Jun Zhang
2023-06-14 20:10:57 +08:00
committed by GitHub
7 changed files with 491 additions and 287 deletions

View File

@@ -40,7 +40,7 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = () => {
}; };
const { currentUser = {} } = initialState as any; const { currentUser = {} } = initialState as any;
console.log(currentUser, 'currentUser');
const menuHeaderDropdown = ( const menuHeaderDropdown = (
<Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}> <Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
<Menu.Item key="logout"> <Menu.Item key="logout">

View File

@@ -7,9 +7,11 @@ interface Props extends AvatarProps {
staffName?: string; staffName?: string;
avatarImg?: string; avatarImg?: string;
} }
const { tmeAvatarUrl } = process.env;
const TMEAvatar: FC<Props> = ({ avatarImg, ...restProps }) => ( const TMEAvatar: FC<Props> = ({ staffName, avatarImg, ...restProps }) => {
<Avatar src={`${avatarImg}`} alt="avatar" icon={<img src={avatarIcon} />} {...restProps} /> const avatarSrc = tmeAvatarUrl ? `${tmeAvatarUrl}${staffName}.png` : avatarImg;
); return (
<Avatar src={`${avatarSrc}`} alt="avatar" icon={<img src={avatarIcon} />} {...restProps} />
);
};
export default TMEAvatar; export default TMEAvatar;

View File

@@ -12,7 +12,7 @@ import { postUserLogin, userRegister } from './services';
import { AUTH_TOKEN_KEY } from '@/common/constants'; import { AUTH_TOKEN_KEY } from '@/common/constants';
import { queryCurrentUser } from '@/services/user'; import { queryCurrentUser } from '@/services/user';
import { history, useModel } from 'umi'; import { history, useModel } from 'umi';
import { setToken as setChatSdkToken } from 'supersonic-chat-sdk'; // import { setToken as setChatSdkToken } from 'supersonic-chat-sdk';
const { Item } = Form; const { Item } = Form;
const LoginPage: React.FC = () => { const LoginPage: React.FC = () => {
@@ -25,7 +25,7 @@ const LoginPage: React.FC = () => {
const { code, data, msg } = await postUserLogin(values); const { code, data, msg } = await postUserLogin(values);
if (code === 200) { if (code === 200) {
localStorage.setItem(AUTH_TOKEN_KEY, data); localStorage.setItem(AUTH_TOKEN_KEY, data);
setChatSdkToken(data || ''); // setChatSdkToken(data || '');
const { code: queryUserCode, data: queryUserData } = await queryCurrentUser(); const { code: queryUserCode, data: queryUserData } = await queryCurrentUser();
if (queryUserCode === 200) { if (queryUserCode === 200) {
const currentUser = { const currentUser = {

View File

@@ -167,7 +167,12 @@ const FieldForm: React.FC<Props> = ({ fields, onFieldChange }) => {
disabled={!editState} disabled={!editState}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
handleFieldChange(record, 'name', value); // handleFieldChange(record, 'name', value);
onFieldChange(record.bizName, {
...record,
name: value,
[isCreateName]: 1,
});
}} }}
placeholder="请输入中文名" placeholder="请输入中文名"
/> />

View File

@@ -1,82 +1,140 @@
import G6 from '@antv/g6'; import G6 from '@antv/g6';
// import { modifyCSS, createDom } from '@antv/dom-util';
import { createDom } from '@antv/dom-util'; import { createDom } from '@antv/dom-util';
const initToolBar = () => {
// const defaultConfig = G6.ToolBar
const toolBarInstance = new G6.ToolBar();
const searchIconSvgPath = `<path d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z" />`;
const searchNode = (graph) => {
const toolBarSearchInput = document.getElementById('toolBarSearchInput') as HTMLInputElement;
const searchText = toolBarSearchInput.value.trim();
let lastFoundNode = null;
graph.getNodes().forEach((node) => {
const model = node.getModel();
const isFound = searchText && model.label.includes(searchText);
if (isFound) {
graph.setItemState(node, 'active', true);
lastFoundNode = node;
} else {
graph.setItemState(node, 'active', false);
}
});
if (lastFoundNode) {
// 将视图移动到找到的节点位置
graph.focusItem(lastFoundNode, true, {
duration: 300,
easing: 'easeCubic',
});
}
};
const generatorSearchInputDom = (graph) => {
const domString =
'<input placeholder="请输入指标/维度名称" class="ant-input" id="toolBarSearchInput" type="text" value="" />';
const searchInputDom = createDom(domString);
searchInputDom.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
searchNode(graph);
}
});
return searchInputDom;
};
const generatorSearchBtnDom = (graph) => {
const domString = `<button
id="toolBarSearchBtn"
type="button"
class="ant-btn ant-btn-default ant-btn-icon-only ant-input-search-button"
>
<span role="img" aria-label="search" class="anticon anticon-search">
<svg
viewBox="64 64 896 896"
focusable="false"
data-icon="search"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
>
${searchIconSvgPath}
</svg>
</span>
</button>`;
const searchBtnDom = createDom(domString);
searchBtnDom.addEventListener('click', () => {
searchNode(graph);
});
return searchBtnDom;
};
const searchInputDOM = (graph) => {
const searchInputDom = generatorSearchInputDom(graph);
const searchBtnDom = generatorSearchBtnDom(graph);
const searchInput = `
<div id="searchInputContent" class="g6-component-toolbar-search-input" style="position: absolute;top: 38px;width: 190px;left: 0;display:none">
<span class="ant-input-group-wrapper ant-input-search" >
<span class="ant-input-wrapper ant-input-group" id="toolBarSearchWrapper">
<span class="ant-input-group-addon"></span>
</span>
</span>
</div>`;
const searchDom = createDom(searchInput);
const searchWrapperDom = searchDom.querySelector('#toolBarSearchWrapper');
searchWrapperDom.insertBefore(searchInputDom, searchWrapperDom.firstChild);
searchWrapperDom.querySelector('.ant-input-group-addon').appendChild(searchBtnDom);
return searchDom;
};
const initToolBar = () => {
const toolBarInstance = new G6.ToolBar();
const config = toolBarInstance._cfgs; const config = toolBarInstance._cfgs;
const defaultContentDomString = config.getContent(); const defaultContentDomString = config.getContent();
// const regex = /<ul[^>]*>|<\/ul>/g;
// const innerDom = defaultContentDom.replace(regex, '');
const defaultContentDom = createDom(defaultContentDomString); const defaultContentDom = createDom(defaultContentDomString);
// @ts-ignore // @ts-ignore
const elements = defaultContentDom.querySelectorAll('li[code="redo"], li[code="undo"]'); const elements = defaultContentDom.querySelectorAll('li[code="redo"], li[code="undo"]');
elements.forEach((element) => { elements.forEach((element) => {
element.remove(); element.remove();
}); });
const searchBtnDom = `<li code="search"> const searchBtnDom = `<li code="search">
<svg <svg
viewBox="64 64 896 896" viewBox="64 64 896 896"
focusable="false" class="icon"
data-icon="search" data-icon="search"
width="24" width="24"
height="24" height="24"
fill="currentColor" fill="currentColor"
aria-hidden="true" aria-hidden="true"
> >
<path d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z" /> ${searchIconSvgPath}
</svg> </svg>
</li>`; </li>`;
defaultContentDom.insertAdjacentHTML('afterbegin', searchBtnDom);
let searchInputContentVisible = false;
const toolbar = new G6.ToolBar({ const toolbar = new G6.ToolBar({
position: { x: 10, y: 10 }, position: { x: 10, y: 10 },
getContent: () => { className: 'semantic-graph-toolbar',
return `${searchBtnDom}${defaultContentDom}`; getContent: (graph) => {
const searchInput = searchInputDOM(graph);
const content = `<div class="g6-component-toolbar-content">${defaultContentDom.outerHTML}</div>`;
const contentDom = createDom(content);
contentDom.appendChild(searchInput);
return contentDom;
},
handleClick: (code, graph) => {
if (code === 'search') {
const searchText = document.getElementById('searchInputContent');
if (searchText) {
const visible = searchInputContentVisible ? 'none' : 'block';
searchText.style.display = visible;
searchInputContentVisible = !searchInputContentVisible;
}
} else {
// handleDefaultOperator public方法缺失graph作为参数传入将graph挂载在cfgs上源码通过get会获取到graph,完成默认code的执行逻辑
toolBarInstance._cfgs.graph = graph;
toolBarInstance.handleDefaultOperator(code);
}
}, },
}); });
// const toolbar = new G6.ToolBar({
// getContent: (graph) => {
// const searchInput = document.createElement('input');
// searchInput.id = 'search-input';
// searchInput.placeholder = '搜索节点';
// const searchBtn = document.createElement('button');
// searchBtn.id = 'search-btn';
// searchBtn.innerHTML = '搜索';
// const container = document.createElement('div');
// container.appendChild(searchInput);
// container.appendChild(searchBtn);
// return container;
// },
// handleClick: (name, graph) => {
// if (name === 'search-btn') {
// const searchText = document.getElementById('search-input').value.trim();
// if (!searchText) {
// return;
// }
// const foundNode = graph.getNodes().find((node) => {
// const model = node.getModel();
// return model.label === searchText;
// });
// if (foundNode) {
// // 如果找到了节点,将其设置为选中状态
// graph.setItemState(foundNode, 'active', true);
// // 将视图移动到找到的节点位置
// graph.focusItem(foundNode, true, {
// duration: 300,
// easing: 'easeCubic',
// });
// } else {
// alert('未找到节点');
// }
// }
// },
// });
return toolbar; return toolbar;
}; };
export default initToolBar; export default initToolBar;

View File

@@ -0,0 +1,73 @@
import G6 from '@antv/g6';
import moment from 'moment';
const initTooltips = () => {
const tooltip = new G6.Tooltip({
offsetX: 10,
offsetY: 10,
fixToNode: [1, 0.5],
// the types of items that allow the tooltip show up
// 允许出现 tooltip 的 item 类型
// itemTypes: ['node', 'edge'],
itemTypes: ['node'],
// custom the tooltip's content
// 自定义 tooltip 内容
getContent: (e) => {
const outDiv = document.createElement('div');
outDiv.style.width = 'fit-content';
outDiv.style.height = 'fit-content';
const model = e.item.getModel();
console.log(model, e.item, 'model');
const { name, bizName, createdBy, updatedAt, description } = model;
const list = [
{
label: '名称:',
value: name,
},
{
label: '字段:',
value: bizName,
},
{
label: '创建人:',
value: createdBy,
},
{
label: '更新时间:',
value: updatedAt ? moment(updatedAt).format('YYYY-MM-DD HH:mm:ss') : '',
},
{
label: '描述:',
value: description,
},
];
const listHtml = list.reduce((htmlString, item) => {
const { label, value } = item;
if (value) {
htmlString += `<p style="margin-bottom:0">
<span>${label} </span>
<span>${value}</span>
</p>`;
}
return htmlString;
}, '');
const html = `<div>
${listHtml}
</div>`;
if (e.item.getType() === 'node') {
outDiv.innerHTML = html;
}
// else {
// const source = e.item.getSource();
// const target = e.item.getTarget();
// outDiv.innerHTML = `来源:${source.getModel().name}<br/>去向:${
// target.getModel().name
// }`;
// }
return outDiv;
},
});
return tooltip;
};
export default initTooltips;

View File

@@ -3,9 +3,10 @@ import { connect } from 'umi';
import type { StateType } from '../model'; import type { StateType } from '../model';
import type { Dispatch } from 'umi'; import type { Dispatch } from 'umi';
import { typeConfigs } from './utils'; import { typeConfigs } from './utils';
import { message } from 'antd'; import { message, Row, Col, Radio } from 'antd';
import { getDatasourceList, getDomainSchemaRela } from '../service'; import { getDatasourceList, getDomainSchemaRela } from '../service';
import initToolBar from './components/ToolBar'; import initToolBar from './components/ToolBar';
import initTooltips from './components/ToolTips';
import G6 from '@antv/g6'; import G6 from '@antv/g6';
type Props = { type Props = {
@@ -17,11 +18,13 @@ type Props = {
const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => { const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
const ref = useRef(null); const ref = useRef(null);
const [graphData, setGraphData] = useState<any>({}); const [graphData, setGraphData] = useState<any>({});
const [dataSourceListData, setDataSourceListData] = useState<any[]>([]);
const [graphShowType, setGraphShowType] = useState<string>('dimension');
const legendDataRef = useRef<any[]>([]); const legendDataRef = useRef<any[]>([]);
const graphRef = useRef<any>(null); const graphRef = useRef<any>(null);
const legendDataFilterFunctions = useRef<any>({}); const legendDataFilterFunctions = useRef<any>({});
const { dimensionList } = domainManger; // const { dimensionList } = domainManger;
const toggleNodeVisibility = (graph, node, visible) => { const toggleNodeVisibility = (graph, node, visible) => {
if (visible) { if (visible) {
@@ -42,37 +45,80 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
} }
}; };
const formatterRelationData = (dataSourceList: any[]) => { const getDimensionChildren = (dimensions: any[], dataSourceId: string) => {
const relationData = dataSourceList.reduce((relationList: any[], item: any) => { const dimensionChildrenList = dimensions.reduce((dimensionChildren: any[], dimension: any) => {
const { id, name } = item; const {
const dataSourceId = `dataSource-${id}`; id: dimensionId,
const dimensionChildrenList = dimensionList.reduce( name: dimensionName,
(dimensionChildren: any[], dimension: any) => { bizName,
const { id: dimensionId, name: dimensionName, datasourceId } = dimension; description,
if (datasourceId === id) { createdBy,
dimensionChildren.push({ updatedAt,
nodeType: 'dimension', } = dimension;
legendType: dataSourceId, // if (datasourceId === id) {
id: `dimension-${dimensionId}`, dimensionChildren.push({
name: dimensionName, nodeType: 'dimension',
style: { legendType: dataSourceId,
lineWidth: 2, id: `dimension-${dimensionId}`,
fill: '#f0f7ff', name: dimensionName,
stroke: '#a6ccff', bizName,
}, description,
}); createdBy,
} updatedAt,
return dimensionChildren; style: {
lineWidth: 2,
fill: '#f0f7ff',
stroke: '#a6ccff',
}, },
[], });
); // }
return dimensionChildren;
}, []);
return dimensionChildrenList;
};
const getMetricChildren = (metrics: any[], dataSourceId: string) => {
const metricsChildrenList = metrics.reduce((metricsChildren: any[], dimension: any) => {
const { id, name, bizName, description, createdBy, updatedAt } = dimension;
metricsChildren.push({
nodeType: 'metric',
legendType: dataSourceId,
id: `dimension-${id}`,
name,
bizName,
description,
createdBy,
updatedAt,
style: {
lineWidth: 2,
fill: '#f0f7ff',
stroke: '#a6ccff',
},
});
return metricsChildren;
}, []);
return metricsChildrenList;
};
const formatterRelationData = (dataSourceList: any[], type = graphShowType) => {
const relationData = dataSourceList.reduce((relationList: any[], item: any) => {
const { datasource, dimensions, metrics } = item;
const { id, name } = datasource;
const dataSourceId = `dataSource-${id}`;
let childrenList = [];
if (type === 'metirc') {
childrenList = getMetricChildren(metrics, dataSourceId);
}
if (type === 'dimension') {
childrenList = getDimensionChildren(dimensions, dataSourceId);
}
relationList.push({ relationList.push({
name, name,
legendType: dataSourceId, legendType: dataSourceId,
id: dataSourceId, id: dataSourceId,
nodeType: 'datasource', nodeType: 'datasource',
size: 40, size: 40,
children: [...dimensionChildrenList], children: [...childrenList],
style: { style: {
lineWidth: 2, lineWidth: 2,
fill: '#BDEFDB', fill: '#BDEFDB',
@@ -84,26 +130,33 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
return relationData; return relationData;
}; };
const changeGraphData = (data: any, type?: string) => {
const relationData = formatterRelationData(data, type);
const legendList = relationData.map((item: any) => {
const { id, name } = item;
return {
id,
label: name,
order: 4,
...typeConfigs.datasource,
};
});
legendDataRef.current = legendList;
const graphRootData = {
id: 'root',
name: domainManger.selectDomainName,
children: relationData,
};
setGraphData(graphRootData);
};
const queryDataSourceList = async (params: any) => { const queryDataSourceList = async (params: any) => {
getDomainSchemaRela(params.domainId); const { code, data, msg } = await getDomainSchemaRela(params.domainId);
const { code, data, msg } = await getDatasourceList({ ...params });
if (code === 200) { if (code === 200) {
const relationData = formatterRelationData(data); if (data) {
const legendList = relationData.map((item: any) => { changeGraphData(data);
const { id, name } = item; setDataSourceListData(data);
return { }
id,
label: name,
order: 4,
...typeConfigs.datasource,
};
});
legendDataRef.current = legendList;
setGraphData({
id: 'root',
name: domainManger.selectDomainName,
children: relationData,
});
} else { } else {
message.error(msg); message.error(msg);
} }
@@ -111,7 +164,7 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
useEffect(() => { useEffect(() => {
queryDataSourceList({ domainId }); queryDataSourceList({ domainId });
}, []); }, [domainId]);
const getLegendDataFilterFunctions = () => { const getLegendDataFilterFunctions = () => {
legendDataRef.current.map((item: any) => { legendDataRef.current.map((item: any) => {
@@ -143,201 +196,214 @@ const DomainManger: React.FC<Props> = ({ domainManger, domainId }) => {
}; };
// const [visible, setVisible] = useState(false); // const [visible, setVisible] = useState(false);
useEffect(() => { useEffect(() => {
console.log(domainId, graphData, 'domainId');
if (!(Array.isArray(graphData.children) && graphData.children.length > 0)) { if (!(Array.isArray(graphData.children) && graphData.children.length > 0)) {
return; return;
} }
const container = document.getElementById('semanticGraph'); const container = document.getElementById('semanticGraph');
const width = container!.scrollWidth; const width = container!.scrollWidth;
const height = container!.scrollHeight || 500; const height = container!.scrollHeight || 500;
if (!graphRef.current) { // if (!graphRef.current) {
getLegendDataFilterFunctions(); getLegendDataFilterFunctions();
const toolbar = initToolBar(); const toolbar = initToolBar();
// const toolbar = new G6.ToolBar({ const tooltip = initTooltips();
// getContent: (graph) => { const legend = new G6.Legend({
// const searchIcon = document.createElement('i'); // container: 'legendContainer',
// searchIcon.className = 'g6-toolbar-search-icon'; data: {
// searchIcon.style.cssText = ` nodes: legendDataRef.current,
// display: inline-block; },
// width: 16px; align: 'center',
// height: 16px; layout: 'horizontal', // vertical
// background-image: url(https://gw.alipayobjects.com/zos/rmsportal/wzQIcOMRTkQwFgaaDIFs.svg); position: 'bottom-right',
// background-size: 16px 16px; vertiSep: 12,
// margin-right: 8px; horiSep: 24,
// cursor: pointer; offsetY: -24,
// `; padding: [4, 16, 8, 16],
containerStyle: {
// searchIcon.addEventListener('click', () => { fill: '#ccc',
// setVisible((prevVisible) => !prevVisible); lineWidth: 1,
// }); },
title: '可见数据源',
// const ul = document.createElement('ul'); titleConfig: {
// ul.className = 'g6-component-toolbar'; position: 'center',
// ul.appendChild(searchIcon); offsetX: 0,
offsetY: 12,
// return ul; style: {
// }, fontSize: 12,
// }); fontWeight: 500,
fill: '#000',
const tooltip = new G6.Tooltip({
offsetX: 10,
offsetY: 10,
fixToNode: [1, 0.5],
// the types of items that allow the tooltip show up
// 允许出现 tooltip 的 item 类型
// itemTypes: ['node', 'edge'],
itemTypes: ['node'],
// custom the tooltip's content
// 自定义 tooltip 内容
getContent: (e) => {
const outDiv = document.createElement('div');
outDiv.style.width = 'fit-content';
outDiv.style.height = 'fit-content';
const model = e.item.getModel();
if (e.item.getType() === 'node') {
outDiv.innerHTML = `${model.name}`;
}
// else {
// const source = e.item.getSource();
// const target = e.item.getTarget();
// outDiv.innerHTML = `来源:${source.getModel().name}<br/>去向:${
// target.getModel().name
// }`;
// }
return outDiv;
}, },
}); },
const legend = new G6.Legend({ filter: {
// container: 'legendContainer', enable: true,
data: { multiple: true,
nodes: legendDataRef.current, trigger: 'click',
graphActiveState: 'activeByLegend',
graphInactiveState: 'inactiveByLegend',
filterFunctions: {
...legendDataFilterFunctions.current,
}, },
align: 'center', },
layout: 'horizontal', // vertical });
position: 'bottom-right', // 我使用TreeGraph进行layout布局采用{type: 'compactBox',direction: 'LR'}模式,如何使子节点与根节点的连线只连接到上下连接桩上
vertiSep: 12,
horiSep: 24, graphRef.current = new G6.TreeGraph({
offsetY: -24, container: 'semanticGraph',
padding: [4, 16, 8, 16], width,
containerStyle: { height,
fill: '#ccc', linkCenter: true,
lineWidth: 1, modes: {
}, default: [
title: '可见数据源', {
titleConfig: { type: 'collapse-expand',
position: 'center', onChange: function onChange(item, collapsed) {
offsetX: 0, const data = item.get('model');
offsetY: 12, data.collapsed = collapsed;
return true;
},
},
'drag-node',
'drag-canvas',
// 'activate-relations',
'zoom-canvas',
{
type: 'activate-relations',
trigger: 'mouseenter', // 触发方式,可以是 'mouseenter' 或 'click'
resetSelected: true, // 点击空白处时,是否取消高亮
},
],
},
defaultNode: {
size: 26,
labelCfg: {
position: 'right',
offset: 5,
style: { style: {
fontSize: 12, stroke: '#fff',
fontWeight: 500, lineWidth: 4,
fill: '#000',
}, },
}, },
filter: { },
enable: true, defaultEdge: {
multiple: true, type: 'cubic-horizontal',
trigger: 'click', // type: 'flow-line',
graphActiveState: 'activeByLegend', // type: 'polyline',
graphInactiveState: 'inactiveByLegend', // type: 'line',
filterFunctions: { /* configure the bending radius and min distance to the end nodes */
...legendDataFilterFunctions.current, style: {
}, radius: 10,
offset: 30,
endArrow: true,
/* and other styles */
// stroke: '#F6BD16',
}, },
}); // style: {
// stroke: '#A3B1BF',
graphRef.current = new G6.TreeGraph({ // },
container: 'semanticGraph', },
width, layout: {
height, type: 'mindmap',
linkCenter: true, direction: 'H',
modes: { getId: function getId(d) {
default: [ return d.id;
{
type: 'collapse-expand',
onChange: function onChange(item, collapsed) {
const data = item.get('model');
data.collapsed = collapsed;
return true;
},
},
'drag-node',
'drag-canvas',
// 'activate-relations',
'zoom-canvas',
{
type: 'activate-relations',
trigger: 'mouseenter', // 触发方式,可以是 'mouseenter' 或 'click'
resetSelected: true, // 点击空白处时,是否取消高亮
},
],
}, },
defaultNode: { getHeight: function getHeight() {
size: 26, return 16;
labelCfg: {
position: 'bottom',
style: {
stroke: '#fff',
lineWidth: 4,
},
},
}, },
getWidth: function getWidth() {
layout: { return 16;
type: 'dendrogram',
direction: 'LR',
nodeSep: 200,
rankSep: 300,
radial: true,
}, },
plugins: [legend, tooltip, toolbar], getVGap: function getVGap() {
}); return 30;
},
getHGap: function getHGap() {
return 100;
},
// type: 'dendrogram',
// direction: 'LR',
// nodeSep: 200,
// rankSep: 300,
// radial: true,
},
plugins: [legend, tooltip, toolbar],
});
const legendCanvas = legend._cfgs.legendCanvas; const legendCanvas = legend._cfgs.legendCanvas;
// legend模式事件方法bindEvents会有点击图例空白清空选中的逻辑在注册click事件前先将click事件队列清空 // legend模式事件方法bindEvents会有点击图例空白清空选中的逻辑在注册click事件前先将click事件队列清空
legend._cfgs.legendCanvas._events.click = []; legend._cfgs.legendCanvas._events.click = [];
legendCanvas.on('click', (e) => { legendCanvas.on('click', (e) => {
const shape = e.target; const shape = e.target;
const shapeGroup = shape.get('parent'); const shapeGroup = shape.get('parent');
const shapeGroupId = shapeGroup?.cfg?.id; const shapeGroupId = shapeGroup?.cfg?.id;
if (shapeGroupId) { if (shapeGroupId) {
const isActive = shapeGroup.get('active'); const isActive = shapeGroup.get('active');
const targetNode = graph.findById(shapeGroupId); const targetNode = graph.findById(shapeGroupId);
// const model = targetNode.getModel(); // const model = targetNode.getModel();
toggleNodeVisibility(graph, targetNode, isActive); toggleNodeVisibility(graph, targetNode, isActive);
toggleChildrenVisibility(graph, targetNode, isActive); toggleChildrenVisibility(graph, targetNode, isActive);
} }
}); });
const graph = graphRef.current; const graph = graphRef.current;
graph.node(function (node) { graph.node(function (node) {
return { return {
label: node.name, label: node.name,
labelCfg: { style: { fill: '#3c3c3c' } }, labelCfg: { style: { fill: '#3c3c3c' } },
}; };
}); });
console.log(graphData, 'graphData');
// graph.data(graphData);
graph.changeData(graphData);
graph.render();
graph.fitView();
graph.data(graphData); setAllActiveLegend(legend);
graph.render();
graph.fitView();
setAllActiveLegend(legend); const rootNode = graph.findById('root');
graph.hideItem(rootNode);
if (typeof window !== 'undefined')
window.onresize = () => {
if (!graph || graph.get('destroyed')) return;
if (!container || !container.scrollWidth || !container.scrollHeight) return;
graph.changeSize(container.scrollWidth, container.scrollHeight);
};
// }
}, [graphData]);
const rootNode = graph.findById('root'); return (
graph.hideItem(rootNode); <>
if (typeof window !== 'undefined') <Row>
window.onresize = () => { <Col flex="auto" />
if (!graph || graph.get('destroyed')) return; <Col flex="100px">
if (!container || !container.scrollWidth || !container.scrollHeight) return; <Radio.Group
graph.changeSize(container.scrollWidth, container.scrollHeight); buttonStyle="solid"
}; size="small"
} value={graphShowType}
}, [domainId, graphData]); onChange={(e) => {
const { value } = e.target;
return <div ref={ref} id="semanticGraph" style={{ width: '100%', height: '100%' }} />; console.log(value, 'value');
setGraphShowType(value);
changeGraphData(dataSourceListData, value);
}}
>
<Radio.Button value="dimension"></Radio.Button>
<Radio.Button value="metric"></Radio.Button>
</Radio.Group>
</Col>
</Row>
<div
ref={ref}
key={`${domainId}-${graphShowType}`}
id="semanticGraph"
style={{ width: '100%', height: '100%' }}
/>
</>
);
}; };
export default connect(({ domainManger }: { domainManger: StateType }) => ({ export default connect(({ domainManger }: { domainManger: StateType }) => ({
domainManger, domainManger,