Add CytoGraph component to re-present the graph

This commit is contained in:
Tuan Cao 2022-04-15 09:04:28 +07:00
parent b4f27edd5d
commit b43dfd77b0
7 changed files with 1587 additions and 254 deletions

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

125
components/Graph.js Normal file
View 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;

View File

@ -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;

View File

@ -6,7 +6,7 @@ import markdown from "remark-parse";
import {toString} from 'mdast-util-to-string' import {toString} from 'mdast-util-to-string'
export function getContent(filename) { export function getContent(filename) {
let { currentFilePath} = getFileNames(filename); let {currentFilePath} = getFileNames(filename);
//console.log("currentFilePath: ", currentFilePath) //console.log("currentFilePath: ", currentFilePath)
if (currentFilePath === undefined || currentFilePath == null) return null if (currentFilePath === undefined || currentFilePath == null) return null
return Node.readFileSync(currentFilePath) return Node.readFileSync(currentFilePath)
@ -65,10 +65,6 @@ export function getSinglePost(filename) {
var fileContent = Node.readFileSync(currentFilePath) var fileContent = Node.readFileSync(currentFilePath)
const currentFileFrontMatter = Transformer.getFrontMatterData(fileContent) 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, { const [htmlContent] = Transformer.getHtmlContent(fileContent, {
fileNames: fileNames, fileNames: fileNames,
@ -88,7 +84,7 @@ export function constructBackLinks() {
const edges = [] const edges = []
const nodes = [] const nodes = []
filePaths.forEach( filename => { filePaths.forEach(filename => {
const {currentFilePath, fileNames} = getFileNames(filename) const {currentFilePath, fileNames} = getFileNames(filename)
const internalLinks = Transformer.getInternalLinks(currentFilePath) const internalLinks = Transformer.getInternalLinks(currentFilePath)
internalLinks.forEach(aLink => { internalLinks.forEach(aLink => {
@ -114,44 +110,34 @@ export function constructBackLinks() {
export function getGraphData() { export function getGraphData() {
const backlinkData = constructBackLinks()
const elements = [] const {nodes, edges} = constructBackLinks()
// First create Nodes const newNodes = nodes.map(aNode => (
backlinkData.forEach(el => { {data : {
const node = {data: {id: el.id}}; id: aNode.slug.toString(),
label: aNode.slug.toString(),
if (el.title) { type: "ip"
node.data.title = el.title
} }
if (el.description) {
node.data.description = el.description
} }
elements.push(node) ))
}
)
const newEdges = edges.map(anEdge => ({
// 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 = {
data: { data: {
id: `${el.id}-${linkElement}`, source: anEdge.source,
source: el.id, target: anEdge.target,
target: linkElement // 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() { export function getContentPaths() {

View File

@ -27,6 +27,7 @@
"next": "12", "next": "12",
"path": "^0.12.7", "path": "^0.12.7",
"react": "^17.0.2", "react": "^17.0.2",
"react-cytoscapejs": "^1.2.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-folder-tree": "^5.0.3", "react-folder-tree": "^5.0.3",
"rehype-react": "^7.0.4", "rehype-react": "^7.0.4",
@ -47,6 +48,8 @@
"devDependencies": { "devDependencies": {
"bimap": "^0.0.15", "bimap": "^0.0.15",
"cytoscape": "^3.17.0", "cytoscape": "^3.17.0",
"eslint": "8.13.0",
"eslint-config-next": "12.1.5",
"remark-frontmatter": "^3.0.0", "remark-frontmatter": "^3.0.0",
"remark-react": "^8.0.0", "remark-react": "^8.0.0",
"remark-stringify": "^9.0.0", "remark-stringify": "^9.0.0",

View File

@ -1,10 +1,17 @@
import Layout from "../components/layout"; 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 FolderTree from "../components/FolderTree";
import MDContainer from "../components/MDContainer"; 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 ( return (
<Layout> <Layout>
<div className = 'container'> <div className = 'container'>
@ -13,6 +20,8 @@ export default function Home({content, tree, flattenNodes}) {
</nav> </nav>
<MDContainer post={content.data}/> <MDContainer post={content.data}/>
</div> </div>
<hr/>
<DynamicGraph graph={graphData}/>
</Layout> </Layout>
); );
@ -22,11 +31,13 @@ export function getStaticProps() {
const tree = convertObject(getDirectoryData()); const tree = convertObject(getDirectoryData());
const contentData = getSinglePost("index"); const contentData = getSinglePost("index");
const flattenNodes = getFlattenArray(tree) const flattenNodes = getFlattenArray(tree)
const graphData = getGraphData();
return { return {
props: { props: {
content: contentData, content: contentData,
tree: tree, tree: tree,
flattenNodes: flattenNodes flattenNodes: flattenNodes,
graphData:graphData,
}, },
}; };
} }

1427
yarn.lock

File diff suppressed because it is too large Load Diff