Add CytoGraph component to re-present the graph
This commit is contained in:
parent
b4f27edd5d
commit
b43dfd77b0
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
125
components/Graph.js
Normal file
125
components/Graph.js
Normal file
@ -0,0 +1,125 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import CytoscapeComponent from "react-cytoscapejs";
|
||||
|
||||
const layout = {
|
||||
name: "breadthfirst",
|
||||
fit: true,
|
||||
circle: true,
|
||||
directed: true,
|
||||
padding: 50,
|
||||
spacingFactor: 1.5,
|
||||
animate: true,
|
||||
// animationDuration: 300,
|
||||
avoidOverlap: false,
|
||||
nodeDimensionsIncludeLabels: true
|
||||
};
|
||||
|
||||
const styleSheet = [
|
||||
{
|
||||
selector: "node",
|
||||
style: {
|
||||
backgroundColor: "#4a56a6",
|
||||
width: 10,
|
||||
height: 10,
|
||||
label: "data(label)",
|
||||
|
||||
// "width": "mapData(score, 0, 0.006769776522008331, 20, 60)",
|
||||
// "height": "mapData(score, 0, 0.006769776522008331, 20, 60)",
|
||||
// "text-valign": "center",
|
||||
// "text-halign": "center",
|
||||
"overlay-padding": "6px",
|
||||
"z-index": "10",
|
||||
//text props
|
||||
"text-outline-color": "#4a56a6",
|
||||
"text-outline-width": "1px",
|
||||
color: "red",
|
||||
fontSize: 20
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: "node:selected",
|
||||
style: {
|
||||
"border-width": "6px",
|
||||
"border-color": "#AAD8FF",
|
||||
"border-opacity": "0.5",
|
||||
"background-color": "#77828C",
|
||||
width: 50,
|
||||
height: 50,
|
||||
//text props
|
||||
"text-outline-color": "#77828C",
|
||||
"text-outline-width": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: "node[type='device']",
|
||||
style: {
|
||||
shape: "square"
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: "edge",
|
||||
style: {
|
||||
width: 3,
|
||||
// "line-color": "#6774cb",
|
||||
"line-color": "#AAD8FF",
|
||||
"target-arrow-color": "#6774cb",
|
||||
"target-arrow-shape": "triangle",
|
||||
"curve-style": "bezier"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
|
||||
function Graph({graph}) {
|
||||
const [width, setWith] = useState("100%");
|
||||
const [height, setHeight] = useState("400px");
|
||||
const [graphData, setGraphData] = useState({
|
||||
nodes: graph.nodes,
|
||||
edges: graph.edges
|
||||
});
|
||||
|
||||
let myCyRef;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h1>Cytoscape example</h1>
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid",
|
||||
backgroundColor: "#f5f6fe"
|
||||
}}
|
||||
>
|
||||
<CytoscapeComponent
|
||||
elements={CytoscapeComponent.normalizeElements(graphData)}
|
||||
// pan={{ x: 200, y: 200 }}
|
||||
style={{ width: width, height: height }}
|
||||
zoomingEnabled={true}
|
||||
maxZoom={2}
|
||||
minZoom={0.5}
|
||||
autounselectify={false}
|
||||
boxSelectionEnabled={true}
|
||||
layout={layout}
|
||||
stylesheet={styleSheet}
|
||||
cy={cy => {
|
||||
myCyRef = cy;
|
||||
|
||||
console.log("EVT", cy);
|
||||
|
||||
cy.on("tap", "node", evt => {
|
||||
var node = evt.target;
|
||||
console.log("EVT", evt);
|
||||
console.log("TARGET", node.data());
|
||||
console.log("TARGET TYPE", typeof node[0]);
|
||||
});
|
||||
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Graph;
|
@ -1,200 +0,0 @@
|
||||
import Cytoscape from "cytoscape";
|
||||
var nodeHtmlLabel = require('cytoscape-node-html-label');
|
||||
|
||||
|
||||
const Graph = ({ el, graphdata, current }) => {
|
||||
nodeHtmlLabel( Cytoscape );
|
||||
|
||||
var cy = Cytoscape({
|
||||
container:el,
|
||||
elements:graphdata,
|
||||
style:[{
|
||||
selector: "node",
|
||||
style:{
|
||||
"background-color" : el => el.data("id") === current ? '#5221c4' : "#666",
|
||||
"font-size": "10px",
|
||||
"width": "20px",
|
||||
"height": "20px"
|
||||
//"label": el => el.data("id") === current ? "" : el.data('title') ? el.data("title").slice(0,16) : el.data("id")
|
||||
}
|
||||
},{
|
||||
selector: "label",
|
||||
style: {"font-size": "12px"},
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'width': 2,
|
||||
"height":200,
|
||||
'line-color': '#ffffff',
|
||||
'target-arrow-color': '#ccc',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'curve-style': 'bezier'
|
||||
}
|
||||
}],
|
||||
layout: {
|
||||
name: 'circle',
|
||||
|
||||
fit: true, // whether to fit the viewport to the graph
|
||||
padding: 32, // the padding on fit
|
||||
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
||||
avoidOverlap: true, // prevents node overlap, may overflow boundingBox and radius if not enough space
|
||||
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
|
||||
spacingFactor: 0.9, // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
|
||||
radius: 180, // the radius of the circle
|
||||
startAngle: -2 / 4 * Math.PI, // where nodes start in radians
|
||||
//sweep: undefined, // how many radians should be between the first and last node (defaults to full circle)
|
||||
clockwise: true, // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)
|
||||
sort: undefined, // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
|
||||
animate: false, // whether to transition the node positions
|
||||
animationDuration: 500, // duration of animation in ms if enabled
|
||||
animationEasing: undefined, // easing of animation if enabled
|
||||
//animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
|
||||
ready: undefined, // callback on layoutready
|
||||
stop: undefined, // callback on layoutstop
|
||||
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
|
||||
|
||||
|
||||
|
||||
},
|
||||
zoom: 10,
|
||||
hideEdgesOnViewport:false,
|
||||
wheelSensitivity:0.2,
|
||||
})
|
||||
|
||||
cy.nodeHtmlLabel( [{
|
||||
query: "node",
|
||||
halign:"top",
|
||||
valign:"center",
|
||||
cssClass: 'label',
|
||||
tpl: el => {
|
||||
//el.data("id") === current ? "" : el.data('title') ? el.data("title").slice(0,16) : el.data("id")
|
||||
const label = el.id === current ? "" : el.title ? el.title : el.id
|
||||
return `<div title="${el.title ? el.title : el.id}" style='font-weight:400; margin-top:32px;max-width:180px;font-size:12px;color:white;cursor:pointer;'>${label}</div>`
|
||||
}}],
|
||||
{
|
||||
enablePointerEvents: true
|
||||
}
|
||||
)
|
||||
return cy
|
||||
}
|
||||
|
||||
|
||||
export const Network = ({ el, graphdata, current, routeHandler, allNodes }) => {
|
||||
var jsnx = require('jsnetworkx');
|
||||
|
||||
|
||||
//const grouper = (id) => id === "index" ? 1 : (id === "codesheet" ? 2 : 3)
|
||||
|
||||
|
||||
var currentnode = graphdata.filter(g => g.data.id === current)[0]
|
||||
currentnode = [currentnode.data.id, {
|
||||
label:current==="index" ? "HOME" : currentnode.data.title ? currentnode.data.title : currentnode.data.id,
|
||||
href:current==="index" ? "/" : `/note/${currentnode.data.id}`,
|
||||
//group:grouper(current)
|
||||
}];
|
||||
//var currentTargetNames = graphdata.filter(g => g.data.source === current).map(e => e.data.target)
|
||||
//var currentTargetNodes = graphdata.filter(g => currentTargetNames.includes(g.data.id))
|
||||
|
||||
var othernodes, edges;
|
||||
if (allNodes){
|
||||
othernodes = graphdata.filter(g => (g.data.id !== current) && !g.data.source)
|
||||
othernodes = othernodes.map(on => [on.data.id ,{
|
||||
label:on.data.title ? on.data.title : on.data.id,
|
||||
href: on.data.id === "index" ? "/" : `/note/${on.data.id}`,
|
||||
//group: grouper(on.data.id)
|
||||
}
|
||||
])
|
||||
//console.log(othernodes)
|
||||
edges = graphdata.filter(g => g.data.source)
|
||||
edges = edges.map(e => [e.data.source, e.data.target])
|
||||
}
|
||||
else {
|
||||
//console.log("else")
|
||||
var indexnode = graphdata.filter(g => g.data.id === "index")[0]
|
||||
indexnode = ["Home", {
|
||||
width:30,
|
||||
height:30,
|
||||
weight:1,
|
||||
href:`/`,
|
||||
title: "Home",
|
||||
fill:"blueviolet",
|
||||
|
||||
}]
|
||||
|
||||
var currentRawEdges = graphdata.filter(g => g.data.source === current)
|
||||
edges = currentRawEdges.map(ce => [ce.data.source, ce.data.target, {weight:1 } ])
|
||||
|
||||
var currentTargetNames = currentRawEdges.map(ie => ie.data.target)
|
||||
var currentTargets = graphdata.filter(g => currentTargetNames.includes(g.data.id))
|
||||
othernodes = currentTargets.map(ct => [ct.data.id, {size:6, href:`/note/${ct.data.id}`}])
|
||||
if (current !== "index"){othernodes.push(indexnode)}
|
||||
//othernodes = [indexnode, ...othernodes]
|
||||
}
|
||||
|
||||
|
||||
|
||||
var G = new jsnx.DiGraph();
|
||||
G.addNodesFrom(
|
||||
[
|
||||
currentnode,
|
||||
...othernodes,
|
||||
],
|
||||
{color: '#999999', width:40, height:40}
|
||||
);
|
||||
G.addEdgesFrom(edges);
|
||||
|
||||
jsnx.draw(G, {
|
||||
element: el,
|
||||
withLabels: true,
|
||||
labelStyle:{
|
||||
color:"#333",
|
||||
fill:function(n){
|
||||
return n.node === current ? "#fff" : "#000"
|
||||
}
|
||||
},
|
||||
labelAttr:{
|
||||
class: "node-label",
|
||||
y:16,
|
||||
click:function(l){
|
||||
this.addEventListener("click", function(){
|
||||
routeHandler(l.data.href)
|
||||
})
|
||||
}
|
||||
},
|
||||
weighted:true,
|
||||
layoutAttr:{
|
||||
linkDistance:200,
|
||||
linkStrength:1.5,
|
||||
friction:0.3,
|
||||
charge: -180,
|
||||
//charge:function(c){ return -80},
|
||||
},
|
||||
nodeStyle: {
|
||||
fill: function(d) {
|
||||
return "#999"
|
||||
//console.log("group", d.data.group)
|
||||
//return color(d.data.group);
|
||||
},
|
||||
stroke: 'none'
|
||||
},
|
||||
nodeAttr:{
|
||||
class: "node-node",
|
||||
click:function(l){
|
||||
this.addEventListener("click", function(){
|
||||
console.log("lll", l.data);
|
||||
routeHandler(l.data.href)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
edgeStyle:{
|
||||
height:120,
|
||||
strokeWidth:2,
|
||||
stroke:"#999"
|
||||
}
|
||||
}, true);
|
||||
return G
|
||||
}
|
||||
|
||||
export default Graph;
|
58
lib/utils.js
58
lib/utils.js
@ -6,7 +6,7 @@ import markdown from "remark-parse";
|
||||
import {toString} from 'mdast-util-to-string'
|
||||
|
||||
export function getContent(filename) {
|
||||
let { currentFilePath} = getFileNames(filename);
|
||||
let {currentFilePath} = getFileNames(filename);
|
||||
//console.log("currentFilePath: ", currentFilePath)
|
||||
if (currentFilePath === undefined || currentFilePath == null) return null
|
||||
return Node.readFileSync(currentFilePath)
|
||||
@ -65,10 +65,6 @@ export function getSinglePost(filename) {
|
||||
var fileContent = Node.readFileSync(currentFilePath)
|
||||
|
||||
const currentFileFrontMatter = Transformer.getFrontMatterData(fileContent)
|
||||
//console.log("\tFounded front matter data: ", currentFileFrontMatter, "\n")
|
||||
// fileContent = Transformer.preprocessThreeDashes(fileContent)
|
||||
//fileContent = fileContent.split("---").join("")
|
||||
//console.log("filecontent end")
|
||||
|
||||
const [htmlContent] = Transformer.getHtmlContent(fileContent, {
|
||||
fileNames: fileNames,
|
||||
@ -88,7 +84,7 @@ export function constructBackLinks() {
|
||||
const edges = []
|
||||
const nodes = []
|
||||
|
||||
filePaths.forEach( filename => {
|
||||
filePaths.forEach(filename => {
|
||||
const {currentFilePath, fileNames} = getFileNames(filename)
|
||||
const internalLinks = Transformer.getInternalLinks(currentFilePath)
|
||||
internalLinks.forEach(aLink => {
|
||||
@ -114,44 +110,34 @@ export function constructBackLinks() {
|
||||
|
||||
|
||||
export function getGraphData() {
|
||||
const backlinkData = constructBackLinks()
|
||||
|
||||
const elements = []
|
||||
const {nodes, edges} = constructBackLinks()
|
||||
|
||||
// First create Nodes
|
||||
backlinkData.forEach(el => {
|
||||
const node = {data: {id: el.id}};
|
||||
|
||||
if (el.title) {
|
||||
node.data.title = el.title
|
||||
const newNodes = nodes.map(aNode => (
|
||||
{data : {
|
||||
id: aNode.slug.toString(),
|
||||
label: aNode.slug.toString(),
|
||||
type: "ip"
|
||||
}
|
||||
if (el.description) {
|
||||
node.data.description = el.description
|
||||
}
|
||||
elements.push(node)
|
||||
}
|
||||
)
|
||||
))
|
||||
|
||||
|
||||
// Second create Edges
|
||||
backlinkData.forEach(el => {
|
||||
// check if has any internal link
|
||||
if (el.to.length > 0) {
|
||||
// create edge from element to its links
|
||||
el.to.forEach(linkElement => {
|
||||
const edge = {
|
||||
const newEdges = edges.map(anEdge => ({
|
||||
data: {
|
||||
id: `${el.id}-${linkElement}`,
|
||||
source: el.id,
|
||||
target: linkElement
|
||||
source: anEdge.source,
|
||||
target: anEdge.target,
|
||||
// label: anEdge.source + " => " + anEdge.target
|
||||
}
|
||||
}
|
||||
elements.push(edge)
|
||||
})
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
return elements
|
||||
// Remove edges that don't have any connections
|
||||
const existingNodeID = newNodes.map(aNode => aNode.data.id)
|
||||
const filteredEdges = newEdges.filter(edge => existingNodeID.includes(edge.data.source)).filter(edge => existingNodeID.includes(edge.data.target))
|
||||
|
||||
return {
|
||||
nodes: newNodes,
|
||||
edges: filteredEdges
|
||||
}
|
||||
}
|
||||
|
||||
export function getContentPaths() {
|
||||
|
@ -27,6 +27,7 @@
|
||||
"next": "12",
|
||||
"path": "^0.12.7",
|
||||
"react": "^17.0.2",
|
||||
"react-cytoscapejs": "^1.2.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-folder-tree": "^5.0.3",
|
||||
"rehype-react": "^7.0.4",
|
||||
@ -47,6 +48,8 @@
|
||||
"devDependencies": {
|
||||
"bimap": "^0.0.15",
|
||||
"cytoscape": "^3.17.0",
|
||||
"eslint": "8.13.0",
|
||||
"eslint-config-next": "12.1.5",
|
||||
"remark-frontmatter": "^3.0.0",
|
||||
"remark-react": "^8.0.0",
|
||||
"remark-stringify": "^9.0.0",
|
||||
|
@ -1,10 +1,17 @@
|
||||
import Layout from "../components/layout";
|
||||
import {getSinglePost, getDirectoryData, convertObject, getFlattenArray} from "../lib/utils";
|
||||
import {getSinglePost, getDirectoryData, convertObject, getFlattenArray, getGraphData} from "../lib/utils";
|
||||
import FolderTree from "../components/FolderTree";
|
||||
import MDContainer from "../components/MDContainer";
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
export default function Home({content, tree, flattenNodes}) {
|
||||
|
||||
// This trick is to dynamically load component that interact with window object (browser only)
|
||||
const DynamicGraph = dynamic(
|
||||
() => import('../components/Graph'),
|
||||
{ loading: () => <p>Loading ...</p>, ssr: false }
|
||||
)
|
||||
|
||||
export default function Home({graphData, content, tree, flattenNodes}) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className = 'container'>
|
||||
@ -13,6 +20,8 @@ export default function Home({content, tree, flattenNodes}) {
|
||||
</nav>
|
||||
<MDContainer post={content.data}/>
|
||||
</div>
|
||||
<hr/>
|
||||
<DynamicGraph graph={graphData}/>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@ -22,11 +31,13 @@ export function getStaticProps() {
|
||||
const tree = convertObject(getDirectoryData());
|
||||
const contentData = getSinglePost("index");
|
||||
const flattenNodes = getFlattenArray(tree)
|
||||
const graphData = getGraphData();
|
||||
return {
|
||||
props: {
|
||||
content: contentData,
|
||||
tree: tree,
|
||||
flattenNodes: flattenNodes
|
||||
flattenNodes: flattenNodes,
|
||||
graphData:graphData,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user