Antv g6 tooltip 实现hover时可点击tooltip里的内容
antv版本: 4.8.21
实现方法:自定义一个tooltip盒子,监听鼠标移入节点事件,获取他的坐标,把盒子位置设置过去。
效果图:
示例代码:
<template><div v-loading="loading" style="width: 100%; height: 100%; position: relative"><div id="custom-tooltip" class="custom-tooltip"></div></div>
</template><style lang="scss" scoped>.custom-tooltip {position: absolute;z-index: 9999;pointer-events: auto;width: 240px;padding: 12px 10px;box-sizing: border-box;visibility: hidden;background: #FFFFFF;box-shadow: 0 1px 8px 0 rgba(0, 0, 0, 0.20);border-radius: 4px;
}
</style><script lang="ts">
import { defineComponent, ref, watch } from 'vue'export default defineComponent({name: 'VRankChart',setup(props, { emit }) {const initChart = () => {nextTick(async () => {customTooltip = document.getElementById('custom-tooltip')if (!graph) {graph = new G6.TreeGraph({container: 'mountNode',});}// 注册节点点击事件graph.on('node:click', debounce(async (evt: any) => {const item = evt.item;const model = item.getModel();const info = model.infolet result = ``if (!['上游', '中游', '下游'].includes(model.id)) {currentClickNode = infoif (currentClickItem) {// 移除上一个节点的点击样式graph.updateItem(currentClickItem, getNodeStyle(currentClickItem.getModel()));}currentClickItem = itememit('clickNode', info)} else {return}}));// 鼠标悬停节点事件graph.on('node:mouseenter', debounce((evt: any) => {if (customTooltip) {customTooltip.innerHTML = result;customTooltip.style.visibility = 'visible';customTooltip.style.left = `${evt.canvasX > 700 ? (evt.canvasX - 250) : evt.canvasX + 10}px`;customTooltip.style.top = `${evt.canvasY + 10}px`;}}));// 点击外部关闭 tooltipdocument.addEventListener('click', (e) => {if (customTooltip && !customTooltip.contains(e.target as Node)) {customTooltip.style.visibility = 'hidden';}});graph.data(downstream.value);graph.render();graph.fitView();})}}})
</script>
我的完整代码:
<template><div v-loading="loading" style="width: 100%; height: 100%; position: relative"><div id="chart-container" style=""><!-- 添加自定义图例 --><div v-if="currentRegion === '特色黄埔'" class="custom-legend" style="position: absolute"><div v-for="item in LINK_LIST" :key="item.value" class="legend-item"><span class="legend-color" :style="{ backgroundColor: item.color }"></span><span class="legend-text">{{ item.text }}</span></div></div><div id="custom-tooltip" class="custom-tooltip"></div><!-- 工具栏容器 --><div id="toolbar-container" style="position: absolute; right: 66px; bottom: 56px;"></div><div id="mountNode" style="height: 430px; width: 100%;"></div></div><!-- <v-nomsg v-else :width="noDataWidth"></v-nomsg> --></div>
</template><style lang="scss" scoped>
.custom-legend {position: absolute;left: 20px;top: 10px;// z-index: 999999;// display: flex;.legend-item {display: flex;align-items: center;gap: 4px;.legend-color {width: 15px;height: 15px;display: inline-block;}.legend-text {font-size: 12px;color: #666666;}}
}.custom-tooltip {position: absolute;z-index: 9999;pointer-events: auto;width: 240px;padding: 12px 10px;box-sizing: border-box;visibility: hidden;background: #FFFFFF;box-shadow: 0 1px 8px 0 rgba(0, 0, 0, 0.20);border-radius: 4px;
}
</style><script lang="ts">
import { defineComponent, ref, watch } from 'vue'
import * as echarts from 'echarts/core'
import {DatasetComponent,GridComponent,TooltipComponent,TransformComponent,LegendComponent
} from 'echarts/components'
import { TreeChart } from 'echarts/charts'
import { CanvasRenderer } from 'echarts/renderers'
import VChart from 'vue-echarts'
import VNomsg from '@/components/v-nomsg'
import { industry } from '@/api/index'
// import { calculateTreeTop } from '@/utils/index'
// import { Graph, treeToGraphData, Rect, register, ExtensionCategory } from '@antv/g6'
import { debounce } from 'lodash-es'
// import * as G6Plugins from '@antv/g6/lib/plugins';
echarts.use([DatasetComponent,GridComponent,TooltipComponent,TransformComponent,TreeChart,CanvasRenderer,LegendComponent
])export default defineComponent({name: 'VRankChart',components: {VChart,VNomsg},props: {currentIndustryInfo: {type: Object,default: () => ({})},data: {type: Array,default: () => []},width: {type: [Number, String],default: 'auto'},height: {type: [Number, String],default: 'auto'},/*** 排序*/currentRegion: {type: String,required: false},/*** 垂直排列*/vertical: {type: Boolean,default: false},/*** 数值精度(小数位数)*/precision: {type: Number,default: 0},otherOption: {type: Object},sum: {//用于计算百分比type: Number},noDataWidth: {type: Number,default: 130},differentColors: {//不同颜色展示type: Boolean,default: false}// convertUnits: {//是否需要转换单位// type: Boolean,// default: true,// }},setup(props, { emit }) {const chartRef = ref<HTMLElement>()const industryChainInfo = ref<any[]>([])const downstream = ref<any>({})const loading = ref<boolean>(false)const loadingNode = ref<string>('') // 新增:记录正在加载的节点codeconst isNationwide = ref<string>('1')let originalNodeStyles: any = nulllet customTooltip: any = null;let currentClickNode: any = {}let currentClickItem: any = null;// 普通节点默认样式const defaultNodeStyle = {style: {fill: '#F8F9FC',stroke: '#D9D9D9',// clipPath: 'round(4px)'},hoverStyle: {fill: '#E3E3E3',stroke: '#333333',// clipPath: 'round(4px)'},labelCfg: {position: 'center',style: {fill: '#333333',}},}const LINK_LIST = [{value: '2',color: '#578CE7',text: '强链',hover: '#E4F1FF'},{value: '1',color: '#37D294',text: '补链',hover: '#EEFFFA'},{value: '3',color: '#FF8321',text: '延链',hover: '#FFF3E5'}]const getNodeStyle = (model: any, hover = false) => {const info = model.info// 默认节点样式let style: any = JSON.parse(JSON.stringify(defaultNodeStyle))style.style.cursor = 'pointer'style.labelCfg.style.cursor = 'pointer'const isRootNode = model.depth === 0;if (isRootNode) {style.style = {fill: '#215DC3', // 背景颜色为蓝色stroke: '#215DC3',cursor: 'pointer'// lineWidth: 2,}style.labelCfg = {position: 'center',style: {cursor: 'pointer',fill: '#FFFFFF', // 文字颜色为白色}}return style}if (['上游', '中游', '下游'].includes(model.id)) return {}if (props.currentRegion === '全国') {if (info && currentClickNode.code === info?.code) {// 当前点击节点样式style.style = {cursor: 'pointer',...defaultNodeStyle.hoverStyle}// style.style = {// cursor: 'pointer',// fill: '#D0F4F7',// stroke: '#578CE7',// }// style.labelCfg = {// position: 'center',// style: {// cursor: 'pointer',// fill: '#2A3C5C',// },// }// return style} else if (hover) {style.style = defaultNodeStyle.hoverStyle}return style} else {if (info && currentClickNode.code === info?.code) {// 当前点击节点样式style.style = {cursor: 'pointer',fill: (LINK_LIST.find(item => item.value === info.linkChain)?.hover || '#E3E3E3'),stroke: LINK_LIST.find(item => item.value === info.linkChain)?.color || '#333333',}style.labelCfg = {position: 'center',style: {cursor: 'pointer',fill: LINK_LIST.find(item => item.value === info.linkChain)?.color || '#333333',},}return style}// 默认特色黄埔节点样式if (!info?.linkChain || info?.linkChain == '0') {if (hover) {style.style = defaultNodeStyle.hoverStyle}return style}style.style = {cursor: 'pointer',fill: hover ? (LINK_LIST.find(item => item.value === info.linkChain)?.hover || '#F8F9FC') : '#F8F9FC',stroke: LINK_LIST.find(item => item.value === info.linkChain)?.color || '#D9D9D9',}style.labelCfg = {position: 'center',style: {cursor: 'pointer',fill: LINK_LIST.find(item => item.value === info.linkChain)?.color || '#D9D9D9',},}console.log(61, style)}return style}var graph: any = null// const tooltip = new G6.Tooltip({// offsetX: 10,// offsetY: 20,// trigger: 'hover',// getContent(evt: any) {// const item = evt.item;// const model = item.getModel();// const info = model.info// let result = ``// if (['上游', '中游', '下游'].includes(model.id)) {// return// }// const data = model.info// // const data = await getIndustryChainNodeStatistics(info) // 异步获取数据// if (props.currentRegion === '全国' || !info.linkChain || info.linkChain == '0' || info.linkChain == '3') {// result = `// <div style="width: 170px;">// <div>科技企业:${data.enterpriseCnt || 0}家</div>// <div>企业平均科技力:${data.techScoreAvg ? Number(data.techScoreAvg).toFixed(1) : 0}分</div>// <div>高价值专利:${data.highValuePatentCnt || 0}件</div>// </div>// `// } else {// result = `// <div data-node-code="${info.code}" style="width: 170px;">// <div>黄埔区拥有节点企业:${data.enterpriseCnt || 0}家</div>// <div>企业平均科技力:${data.techScoreAvg ? Number(data.techScoreAvg).toFixed(1) : 0}分</div>// <div onclick="window.location.href='${getLinkURL(info)}'" style="// width: 170px; height: 32px; background-color: #215DC3;color: white; border-radis: '4px'; display: flex;justify-content: center;align-items: center; cursor: pointer; margin-top: 8px;">// 查看${LINK_LIST.find(item => item.value === info.linkChain)?.text}推荐企业名单</div>// </div>// `// }// return result// // const model = e.item.getModel();// // if (['上游', '中游', '下游'].includes(model.id)) return null // 让它报错,防止显示空弹窗// // // 初始内容,先占位// // const outDiv = document.createElement('div');// // outDiv.innerHTML = `// // <div>// // <div>${model.id}</div>// // <div style="text-align: center">数据加载中...</div>// // </div>`;// // return outDiv;// },// itemTypes: ['node'],// });const toolbar = new G6.ToolBar({container: 'toolbar-container', // 容器 IDgetContent: () => {return `<ul><li code='fullScreen' style="color: #215dc3;"><i class="iconfont icon-quanping"></i></li></ul>`;},handleClick: (code: any, graph: any) => {if (code === 'fullScreen') {const container = document.getElementById('chart-container') as HTMLElement;if (!document.fullscreenElement) {// 进入全屏if (container.requestFullscreen) {container.requestFullscreen();} else if ((container as any).webkitRequestFullscreen) {(container as any).webkitRequestFullscreen(); // Safari}container.style.backgroundColor = '#F8F9FC';} else {// 退出全屏if (document.exitFullscreen) {document.exitFullscreen();} else if ((document as any).webkitExitFullscreen) {(document as any).webkitExitFullscreen(); // Safari}}// 调整图大小setTimeout(() => {if (graph && !graph.get('destroyed')) {graph.changeSize(container.clientWidth, container.clientHeight);graph.fitView();}}, 300); // 等待 DOM 更新}},position: {x: 10,y: 10,}});const initChart = () => {nextTick(async () => {customTooltip = document.getElementById('custom-tooltip')if (!graph) {graph = new G6.TreeGraph({container: 'mountNode',modes: {default: ['drag-canvas','zoom-canvas',],},plugins: [toolbar], //tooltip, defaultNode: {/* node type */type: 'rect',/* node size */size: [60, 30],labelCfg: {position: 'center',},style: {fill: '#F8F9FC',stroke: '#D9D9D9',radius: 4}},defaultEdge: {type: 'cubic-horizontal',},layout: {type: 'mindmap',direction: 'H',getHeight: () => {return 16;},getWidth: () => {return 16;},getVGap: () => {return 12;},getHGap: () => {return 90;},},autoPaint: true,fitView: true,// fitViewPadding: [20, 60, 20, 20] // [top, right, bottom, left]});} else {// graph.changeData(treeToGraphData(downstream.value));// await graph.clear()}// 注册节点点击事件graph.on('node:click', debounce(async (evt: any) => {const item = evt.item;const model = item.getModel();const info = model.infolet result = ``if (!['上游', '中游', '下游'].includes(model.id)) {currentClickNode = infoif (currentClickItem) {// 移除上一个节点的点击样式graph.updateItem(currentClickItem, getNodeStyle(currentClickItem.getModel()));}currentClickItem = itememit('clickNode', info)} else {return}// const tooltipDiv = tooltip.get('tooltip');// const data = await getIndustryChainNodeStatistics(info) // 异步获取数据// if (props.currentRegion === '全国' || !info.linkChain || info.linkChain == '0' || info.linkChain == '3') {// result = `// <div style="width: 170px;">// <div>科技企业:${data.enterpriseCnt || 0}家</div>// <div>企业平均科技力:${data.techScoreAvg ? Number(data.techScoreAvg).toFixed(1) : 0}分</div>// <div>高价值专利:${data.highValuePatentCnt || 0}件</div>// </div>// `// } else {// result = `// <div data-node-code="${info.code}" style="width: 170px;">// <div>黄埔区拥有节点企业:${data.enterpriseCnt || 0}家</div>// <div>企业平均科技力:${data.techScoreAvg ? Number(data.techScoreAvg).toFixed(1) : 0}分</div>// <div onclick="window.location.href='${getLinkURL(info)}'" style="// width: 170px; height: 32px; background-color: #215DC3;color: white; border-radis: '4px'; display: flex;justify-content: center;align-items: center; cursor: pointer; margin-top: 8px;">// 查看${LINK_LIST.find(item => item.value === info.linkChain)?.text}推荐企业名单</div>// </div>// `// }// if (tooltipDiv) {// tooltipDiv.innerHTML = result;// }}));// 鼠标悬停节点事件graph.on('node:mouseenter', debounce((evt: any) => {const node = evt.item;const model = evt.item.getModel();const info = model.info// 获取当前样式并保存originalNodeStyles = { ...model.style, labelCfg: { ...model.labelCfg } };// debuggerconst { style, labelCfg } = getNodeStyle(model, true)graph.updateItem(node, {style,labelCfg,});if (['上游', '中游', '下游'].includes(model.id)) return;let result = ''if (props.currentRegion === '全国' || !info.linkChain || info.linkChain == '0' || info.linkChain == '3') {result = `<div><div>科技企业:${info.enterpriseCnt || 0}家</div><div>企业平均科技力:${info.techScoreAvg ? Number(info.techScoreAvg).toFixed(1) : 0}分</div><div>高价值专利:${info.highValuePatentCnt || 0}件</div></div>`} else {result = `<div data-node-code="${info.code}"><div>黄埔区拥有节点企业:${info.enterpriseCnt || 0}家</div><div>企业平均科技力:${info.techScoreAvg ? Number(info.techScoreAvg).toFixed(1) : 0}分</div><div onclick="window.location.href='${getLinkURL(info)}'" style="width: 170px; height: 32px; background-color: #215DC3;color: white; border-radis: '4px'; display: flex;justify-content: center;align-items: center; cursor: pointer; margin-top: 8px;">查看${LINK_LIST.find(item => item.value === info.linkChain)?.text}推荐企业名单</div></div>`}if (customTooltip) {customTooltip.innerHTML = result;customTooltip.style.visibility = 'visible';customTooltip.style.left = `${evt.canvasX > 700 ? (evt.canvasX - 250) : evt.canvasX + 10}px`;customTooltip.style.top = `${evt.canvasY + 10}px`;}}));// 点击外部关闭 tooltipdocument.addEventListener('click', (e) => {if (customTooltip && !customTooltip.contains(e.target as Node)) {customTooltip.style.visibility = 'hidden';}});// 鼠标离开时恢复原始样式graph.on('node:mouseleave', (evt: any) => {const node = evt.item;if (originalNodeStyles) {graph.updateItem(node, originalNodeStyles);originalNodeStyles = null;}// const isRootNode = node.getID() === 'root'; // 根据实际情况判断是否是根节点// graph.updateItem(node, {// style: isRootNode ? {// fill: '#0074D9',// stroke: '#005DA8',// } : {// fill: '#F8F9FC',// stroke: '#D9D9D9',// },// labelCfg: {// style: isRootNode ? {// fill: '#FFFFFF',// } : {// fill: '#000000',// },// },// });});graph.node(function (node: any) {if (node.id === 'Modeling Methods') {}const isRootNode = node.depth === 0;// 根据文字长度计算矩形的大小const label = node.id || ''const fontSize = 16const padding = 20const textWidth = label.length * fontSize * 0.6const width = Math.max(60, textWidth + padding * 2)const { style, labelCfg } = getNodeStyle(node, false)return {label: node.id,size: [width, 32], // 动态设置宽度style,labelCfg};});graph.data(downstream.value);graph.render();graph.fitView();})}const getLinkURL = (info: any) => {let url = '/enterprise/index?'// 根节点code// url += 'rootCode=' + info.codePath.split(',')[2]// 添加选择条件const selectedData = [{followGroupKey: 'industryCode',followGroupName: '产业链',children: [{label: info.name,tagGroupKey: 'industryCode',value: info.code}]},]if (info.linkChain == '2') {//强链selectedData.push({followGroupKey: 'techSection',followGroupName: '科技力评分',children: [{label: '61~80分,81~100分',tagGroupKey: 'techSection',value: ['61~80分', '81~100分']}]})} else if (info.linkChain == '1') {// 补链selectedData.push({followGroupKey: 'ranking',followGroupName: '产业排名',children: [{label: '全国同产业Top1%',tagGroupKey: 'ranking',value: '全国同产业Top1%'}]})}url += '&selectedData=' + JSON.stringify(selectedData) + '&searchType=' + (info.linkChain == '2' ? '1' : '0')// console.log(info, selectedData, encodeURI(url), url)return encodeURI(url)}const handleData = (data: any) => {if (!data) returnconst item = data[0]const LINK_TYPE_COLOR: any = {1: '#FFD9D9',2: '#D9D9FF',3: '#D9FFD9'}const getColor = (i: any) => {return {color: props.currentRegion === '全国' ? '#fff' : LINK_TYPE_COLOR[i.linkChain] || '#fff'} // 设置节点背景色}const processNode = (node: any): any => {if (!node) return nullreturn {itemStyle: getColor(node),name: node.name,id: node.name,stats: {},info: node,children:node.children && node.children.length > 0? node.children.map((child: any) => processNode(child)): []}}const { children, ...rest } = item;downstream.value = {name: item.name,id: item.name,itemStyle: { color: '#215DC3' }, // 设置根节点背景色label: { color: '#fff' },info: rest,// info: {// code: item.code// },children: [{name: '中游',id: '中游',children: item.children.find((i: any) => i.name === '中游').children.map((i: any) => processNode(i))},{name: '下游',id: '下游',children: item.children.find((i: any) => i.name === '下游').children.map((i: any) => processNode(i))},{name: '上游',id: '上游',children: item.children.find((i: any) => i.name === '上游').children.map((i: any) => processNode(i))},]}}watch(() => props.currentRegion,async (newVal, oldVal) => {currentClickNode = {}currentClickItem = null;isNationwide.value = newVal === '全国' ? '1' : '0'await fetchIndustryChainInfo(props.currentIndustryInfo.code)handleData(industryChainInfo.value)if (graph && !graph.get('destroyed')) {// 图表已存在,可以安全更新数据graph.changeData(downstream.value);graph.render();graph.fitView();}})// 获取产业链信息const fetchIndustryChainInfo = async (industryCode: string) => {try {loading.value = trueconst { data, success } = await industry.getIndustryChainInfo({industryCode,isNationwide: isNationwide.value})if (success) {handleData(data)industryChainInfo.value = datanextTick(() => {if (graph && !graph.get('destroyed')) {// 如果图表已存在,则更新数据graph.changeData(downstream.value);graph.render();graph.fitView();} else {// 否则初始化图表initChart();}})}loading.value = false} catch (error) {loading.value = falseconsole.error('获取产业链信息失败:', error)}}// 获取产业链节点统计信息const getIndustryChainNodeStatistics = async (info: any) => {loadingNode.value = info.code // 设置正在加载的节点try {const { data, success } = await industry.getIndustryChainNodeStatistics({industryCode: info.code,isNationwide: isNationwide.value})if (success) {updateNodeStats(info.code, data)emit('setIndustryChainNodeInfo',data || {enterpriseCnt: 0,highValuePatentCnt: 0,techScoreAvg: 0})}return data} catch (error) {console.error('获取节点统计信息失败:', error)} finally {loadingNode.value = '' // 清除加载状态}return null}// 更新节点统计数据的辅助函数const updateNodeStats = (code: string, statsData: any) => {// 递归查找并更新节点const updateNode = (node: any) => {if (node.info && node.info.code === code) {node.stats = {...statsData,hasData: true,enterpriseCnt: statsData?.enterpriseCnt,highValuePatentCnt: statsData?.highValuePatentCnt,techScoreAvg: statsData?.techScoreAvg}return true}if (node.children) {for (const child of node.children) {if (updateNode(child)) {return true}}}return false}// 在上游和下游数据中查找并更新节点updateNode(downstream.value)}const init = (industryCode: string, industryInfo: any) => {fetchIndustryChainInfo(industryCode)// 默认需要根节点的节点数据getIndustryChainNodeStatistics(industryInfo)}const handleGlobalClick = (event: MouseEvent) => {const container = document.getElementById('mountNode'); // 图表容器 IDif (!container || !graph) return; // || !tooltipconst rect = container.getBoundingClientRect();const isInBound =event.clientX >= rect.left &&event.clientX <= rect.right &&event.clientY >= rect.top &&event.clientY <= rect.bottom;if (!isInBound) {// tooltip.hide(); // 手动隐藏 tooltip}};const handleFullScreenChange = () => {const container = document.getElementById('mountNode') as HTMLElement;if (!document.fullscreenElement) {// 退出全屏// 调整图大小setTimeout(() => {if (graph && !graph.get('destroyed')) {graph.changeSize(container.clientWidth, container.clientHeight);graph.fitView();}}, 300); // 等待 DOM 更新}};onMounted(() => {init(props.currentIndustryInfo.code, props.currentIndustryInfo)// 添加全局点击事件监听document.addEventListener('click', handleGlobalClick);// 添加 fullscreenchange 事件监听器document.addEventListener('fullscreenchange', handleFullScreenChange);document.addEventListener('webkitfullscreenchange', handleFullScreenChange);document.addEventListener('mozfullscreenchange', handleFullScreenChange);document.addEventListener('MSFullscreenChange', handleFullScreenChange);// initChart()// init()// fetchIndustryChainInfo(props.currentIndustryInfo.code)})// 销毁组件前移除监听器onBeforeUnmount(() => {document.removeEventListener('click', handleGlobalClick);// 移除 fullscreenchange 事件监听器document.removeEventListener('fullscreenchange', handleFullScreenChange);document.removeEventListener('webkitfullscreenchange', handleFullScreenChange);document.removeEventListener('mozfullscreenchange', handleFullScreenChange);document.removeEventListener('MSFullscreenChange', handleFullScreenChange);});return {init,loading,chartRef,industryChainInfo,loadingNode, // 返回loadingNodeLINK_LIST}}
})
</script>