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;
|
|
66
lib/utils.js
66
lib/utils.js
@ -6,10 +6,10 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getShortSummary(filename) {
|
export function getShortSummary(filename) {
|
||||||
@ -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)
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
// 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: {
|
|
||||||
id: `${el.id}-${linkElement}`,
|
|
||||||
source: el.id,
|
|
||||||
target: linkElement
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elements.push(edge)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
))
|
||||||
|
|
||||||
return elements
|
const newEdges = edges.map(anEdge => ({
|
||||||
|
data: {
|
||||||
|
source: anEdge.source,
|
||||||
|
target: anEdge.target,
|
||||||
|
// label: anEdge.source + " => " + anEdge.target
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user