97 lines
7.3 KiB
JavaScript
97 lines
7.3 KiB
JavaScript
/* global React, ReactDOM, ForceGraph2D */
|
|
// GraphViewer — 通用时序知识图谱查看组件 (喂 {nodes,edges} 就能看)
|
|
// 数据契约(标准 Neo4j 风格):
|
|
// node: { id, labels:[], properties:{ entity_type?, name?, description? } }
|
|
// edge: { id, source, target, type?, properties?:{ keywords?, invalid_at?, valid_at? } }
|
|
// 三种数据来源: mount(el,{data}) | mount(el,{api:'url'}) | iframe内 postMessage({type:'gv-data',data})
|
|
(function(root, factory){
|
|
if(typeof exports==='object'&&typeof module==='object'){module.exports=factory();}
|
|
else{root.GraphViewer=factory();}
|
|
})(typeof self!=='undefined'?self:this,function(){
|
|
'use strict';
|
|
var R = window.React, RD = window.ReactDOM;
|
|
var PALETTE = ["#60a5fa","#f87171","#fbbf24","#34d399","#a78bfa","#fb7185","#22d3ee","#818cf8","#fb923c","#4ade80"];
|
|
function trunc(s,n){ s=s==null?'':String(s); return s.length>n?s.slice(0,n)+'…':s; }
|
|
var h = R.createElement;
|
|
var useState=R.useState, useMemo=R.useMemo, useRef=R.useRef, useEffect=R.useEffect;
|
|
|
|
function normalize(data){
|
|
var nodes=(data&&data.nodes||[]).map(function(n){return n;});
|
|
var edges=(data&&(data.edges||data.links)||[]).map(function(e){
|
|
return {id:e.id,source:e.source,target:e.target,type:e.type||'',
|
|
keywords:(e.properties&&e.properties.keywords)||e.type||'',
|
|
invalid:!!(e.properties&&e.properties.invalid_at)};
|
|
});
|
|
return {nodes:nodes,edges:edges};
|
|
}
|
|
|
|
function Viewer(props){
|
|
var data=props.data||{nodes:[],edges:[]};
|
|
var sel=props.selected, onSelect=props.onSelect;
|
|
var norm=useMemo(function(){return normalize(data);},[data]);
|
|
var colorMap=useMemo(function(){
|
|
var m=new Map(); var types=[];
|
|
norm.nodes.forEach(function(n){var t=(n.properties&&n.properties.entity_type)||(n.labels&&n.labels[n.labels.length-1])||'default';if(types.indexOf(t)<0)types.push(t);});
|
|
types.forEach(function(t,i){m.set(t,PALETTE[i%PALETTE.length]);});
|
|
return m;
|
|
},[norm]);
|
|
var gd=useMemo(function(){
|
|
return {nodes:norm.nodes.map(function(n){var t=(n.properties&&n.properties.entity_type)||(n.labels&&n.labels[n.labels.length-1])||'default';
|
|
return Object.assign({},n,{color:colorMap.get(t)||'#888',val:6});}),
|
|
links:norm.edges};
|
|
},[norm,colorMap]);
|
|
var ref=useRef(null), box=useRef(null), dim=useState({width:0,height:0});
|
|
useEffect(function(){ if(!box.current)return;
|
|
var ro=new ResizeObserver(function(es){for(var i=0;i<es.length;i++){var cr=es[i].contentRect;dim[1]({width:cr.width,height:cr.height});}});
|
|
ro.observe(box.current); return function(){ro.disconnect();};
|
|
},[]);
|
|
var types=[].concat(Array.from(colorMap.keys()));
|
|
return h('div',{style:{display:'flex',height:'100%',fontFamily:'system-ui,PingFang SC,sans-serif',background:'#01030f',color:'#dbe2ea'}},
|
|
h('div',{style:{flex:1,minWidth:0,position:'relative'},ref:box},
|
|
h(ForceGraph2D,{ref:ref,width:dim[0].width,height:dim[0].height,graphData:gd,backgroundColor:'#01030f',
|
|
linkColor:function(l){return l.invalid?'rgba(150,150,160,.35)':'rgba(255,255,255,.25)';},
|
|
linkLineDash:function(l){return l.invalid?[4,3]:null;},
|
|
linkWidth:1.2,linkDirectionalArrowLength:3,linkDirectionalArrowRelPos:1,linkCurvature:.1,
|
|
linkCanvasObjectMode:function(){return "after";},
|
|
linkCanvasObject:function(l,ctx,gs){ if(gs<1.1)return; var label=l.keywords||l.type; if(!label)return;
|
|
var a=l.source,b=l.target; if(typeof a!=="object"||typeof b!=="object")return;
|
|
var fs=10/gs, mx=a.x+(b.x-a.x)*.5, my=a.y+(b.y-a.y)*.5, ang=Math.atan2(b.y-a.y,b.x-a.x), flip=(ang>Math.PI/2||ang<-Math.PI/2);
|
|
ctx.save(); ctx.translate(mx,my); ctx.rotate(flip?ang+Math.PI:ang); ctx.font=fs+"px Sans-Serif"; ctx.textAlign="center"; ctx.textBaseline="middle";
|
|
var txt=trunc(label,24), tw=ctx.measureText(txt).width; ctx.fillStyle="rgba(1,3,15,.7)"; ctx.fillRect(-tw/2-1,-fs/2,tw+2,fs);
|
|
ctx.fillStyle=l.invalid?"#8a8a94":"#94e2d5"; ctx.fillText(txt,0,0); ctx.restore(); },
|
|
nodeCanvasObject:function(n,ctx,gs){var r=n.val||6;ctx.save();ctx.beginPath();ctx.arc(n.x,n.y,r,0,2*Math.PI);ctx.fillStyle=n.color;ctx.shadowBlur=8/gs;ctx.shadowColor=n.color;ctx.fill();
|
|
if(gs>.5){ctx.font=(11/gs)+'px Sans-Serif';ctx.textAlign='center';ctx.textBaseline='top';ctx.fillStyle='#fff';ctx.shadowBlur=0;ctx.fillText(trunc((n.properties&&n.properties.name)||n.id,20),n.x,n.y+r+2);}ctx.restore();},
|
|
onNodeClick:function(n){onSelect&&onSelect({type:'node',data:n});},
|
|
onLinkClick:function(l){onSelect&&onSelect({type:'edge',data:l});},
|
|
cooldownTicks:120})),
|
|
h('div',{style:{width:280,padding:14,borderLeft:'1px solid #14203a',overflowY:'auto',background:'#050a1a',fontSize:12}},
|
|
h('div',{style:{color:'#94e2d5',marginBottom:8}},'节点 '+norm.nodes.length+' · 边 '+norm.edges.length),
|
|
h('div',{style:{marginBottom:8}},h('span',{style:{display:'inline-block',width:22,borderTop:'2px solid #fff'}}),' 有效 ',h('span',{className:'dash',style:{borderTopStyle:'dashed',borderColor:'#8a8a94',display:'inline-block',width:22}}),' 失效'),
|
|
h('div',{style:{display:'flex',flexWrap:'wrap',gap:6,marginBottom:12}},types.map(function(t){return h('span',{key:t,style:{padding:'2px 8px',borderRadius:10,fontSize:11,color:'#04101f',fontWeight:600,background:colorMap.get(t)}},t);})),
|
|
sel?h('div',null,h('div',{style:{color:'#7c8aa0',textTransform:'uppercase',letterSpacing:1,fontSize:11}},sel.type==='node'?'实体':'关系'),
|
|
h('div',{style:{color:'#94e2d5',margin:'6px 0',wordBreak:'break-all'}},trunc(sel.type==='node'?((sel.data.properties&&sel.data.properties.name)||sel.data.id):(sel.data.keywords||sel.data.type),60)),
|
|
sel.type==='node'&&sel.data.properties&&sel.data.properties.description?h('div',{style:{marginTop:8,padding:10,background:'#0b1428',borderRadius:8,whiteSpace:'pre-wrap',wordBreak:'break-word'}},sel.data.properties.description):null,
|
|
sel.type==='edge'?h('div',{style:{marginTop:8}},'valid:',(sel.data.properties&&sel.data.properties.valid_at)||'-',' / invalid:',(sel.data.properties&&sel.data.properties.invalid_at)||'-'):null)
|
|
:h('div',{style:{color:'#7c8aa0'}},'点击节点/连线查看详情')));
|
|
}
|
|
|
|
function resolveData(opts,cb){
|
|
if(opts.data)return cb(opts.data);
|
|
if(opts.api){fetch(opts.api).then(function(r){return r.json();}).then(cb).catch(function(e){cb({error:String(e)});});return;}
|
|
cb({nodes:[],edges:[]});
|
|
}
|
|
function App(opts){
|
|
var state={data:{nodes:[],edges:[]},sel:null};
|
|
return { _el:null,_root:null,
|
|
render:function(el){this._el=el;this._root=RD.createRoot(el);var self=this;
|
|
resolveData(opts,function(d){ if(d.error){el.textContent='加载失败:'+d.error;return;} self.state_data=d; self.paint(); });
|
|
if(opts.listenPostMessage!==false){window.addEventListener('message',function(ev){var m=ev.data;if(m&&m.type==='gv-data'){self.state_data=m.data||{nodes:[],edges:[]};self.paint();}});}
|
|
return this;},
|
|
setData:function(d){this.state_data=d;this.paint();},
|
|
paint:function(){var self=this;this._root.render(h(Viewer,{data:this.state_data||{nodes:[],edges:[]},selected:this.sel,onSelect:function(s){self.sel=s;self.paint();}}));}
|
|
};
|
|
}
|
|
return { mount:function(el,opts){ return new App(opts||{}).render(el); },
|
|
VERSION:'1.0.0' };
|
|
});
|