├── .gitignore ├── deploy.sh ├── doc ├── banner-animation.gif ├── hello_world.gif └── hello_world_negative.gif ├── favicon.svg ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── readme.md ├── src ├── customTypings │ └── external.d.ts ├── d3 │ ├── D3Appendable.ts │ ├── D3Circle.ts │ ├── D3DragHandler.ts │ ├── D3Graph.ts │ ├── D3Label.ts │ ├── D3Link.ts │ ├── D3Node.ts │ ├── D3Particle.ts │ ├── D3Relationship.ts │ ├── D3Simulation.ts │ ├── D3Tickable.ts │ └── D3_CONFIG.ts ├── grammar │ ├── attributeGrammar.ts │ ├── lexicalRuleset.ts │ ├── semanticRuleset.ts │ └── syntaxRuleset.ts ├── graph │ ├── Graph.ts │ ├── Link.ts │ ├── Node.ts │ └── Relationship.ts ├── main.ts ├── style.css ├── ui │ └── components │ │ ├── Component.ts │ │ ├── iFrame │ │ └── IFrameComponent.ts │ │ ├── preview │ │ ├── PreviewComponent.ts │ │ ├── SettingsComponent.ts │ │ └── SvgComponent.ts │ │ └── webapp │ │ ├── ContentComponent.ts │ │ ├── InputSidebarComponent.ts │ │ ├── NavbarComponent.ts │ │ ├── ShareButtonComponent.ts │ │ ├── SharePopupComponent.ts │ │ └── WebappComponent.ts ├── utils │ ├── CompilerModel.ts │ ├── InputModel.ts │ ├── LinearAlgebra.ts │ ├── Observable.ts │ └── SettingsModel.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | npm run build 4 | cd dist 5 | git init 6 | git checkout master 7 | git add -A 8 | git commit -m 'deploy' 9 | git push -f git@github.com:AlexW00/Flow_Graph.git master:gh-pages 10 | cd - -------------------------------------------------------------------------------- /doc/banner-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexW00/Flow_Graph/5e0ea37767bf2cbe0eb0d0ad5d8b43a03e833ffd/doc/banner-animation.gif -------------------------------------------------------------------------------- /doc/hello_world.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexW00/Flow_Graph/5e0ea37767bf2cbe0eb0d0ad5d8b43a03e833ffd/doc/hello_world.gif -------------------------------------------------------------------------------- /doc/hello_world_negative.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexW00/Flow_Graph/5e0ea37767bf2cbe0eb0d0ad5d8b43a03e833ffd/doc/hello_world_negative.gif -------------------------------------------------------------------------------- /favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | FlowGraph 8 | 9 | 10 | 11 | 24 | 25 | 26 | 30 | 31 | 32 | 82 | 83 | 84 | 90 | 91 | 92 | 136 | 137 | 138 | 142 | 143 | 144 | 145 | 146 | 147 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flow-graph", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "devDependencies": { 11 | "@types/d3": "^7.1.0", 12 | "@types/express": "^4.17.13", 13 | "@types/jsdom": "^16.2.14", 14 | "autoprefixer": "^10.4.2", 15 | "d3": "^7.3.0", 16 | "express": "^4.17.3", 17 | "postcss": "^8.4.7", 18 | "tailwindcss": "^3.0.23", 19 | "tiny-comp": "^1.2.9", 20 | "typescript": "^4.5.4", 21 | "vite": "^2.8.0" 22 | }, 23 | "dependencies": { 24 | "vite-plugin-vue-jsx": "^0.0.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # FlowGraph - alpha 2 | 3 | 4 | 5 | 6 | ## What is this? 7 | 8 | FlowGraph is a tool for visualizing relationships between concepts, making them easier to understand/explain. 9 | 10 | → Check it out on the [online Playground](https://alexw00.github.io/Flow_Graph/) 11 | 12 | ## 📖 Documentation 13 | 14 | ### Syntax 15 | 16 | #### → Basic Relationship 17 | 18 | To create a basic (positive) relationship, use the following syntax: 19 | 20 | `""---->""` 21 | 22 | - ``: the name of the node 23 | 24 |
25 | Example 26 | "Hello"---->"World"
27 | 28 |
29 | 30 | 31 | #### ⚙️ Relationship options 32 | 33 | You can also configure relationships like so: 34 | 35 | `""--(, )-->""` 36 | - ``: the type of the relationship, supported types: 37 | - `+`: Plus 38 | - `-`: Minus 39 | - `*`: Multiplication 40 | - `/`: Division 41 | - ``: the impact of the relationship 42 | - ``: how fast the relationship will be executed 43 | 44 |
45 | Example 46 | "Hello"--(-10, 2)-->"World"
47 | 48 |
49 | 50 | 51 | ### Default values 52 | 53 | - Node size: 50 54 | - Relationship: 55 | - ``: Plus 56 | - ``: 5 57 | - ``: 1 58 | 59 | ## 👨‍💻 Development 60 | 61 | ### Roadmap: 62 | 63 | - [ ] Further polishing & bug fixes 64 | - [ ] Obsidian Plugin 65 | - [ ] Play/Pause controls 66 | - [ ] GIF export 67 | 68 | ## Credits 69 | 70 | Inspired by [Loopy](https://ncase.me/loopy/) 71 | -------------------------------------------------------------------------------- /src/customTypings/external.d.ts: -------------------------------------------------------------------------------- 1 | declare module JSX { 2 | type Element = string; 3 | interface IntrinsicElements { 4 | [elemName: string]: any; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/d3/D3Appendable.ts: -------------------------------------------------------------------------------- 1 | export default interface D3Appendable { 2 | $selection: d3.Selection; 3 | _append($svg: d3.Selection): void; 4 | } 5 | -------------------------------------------------------------------------------- /src/d3/D3Circle.ts: -------------------------------------------------------------------------------- 1 | import { Event, LiveData } from "../utils/Observable"; 2 | import SettingsModel from "../utils/SettingsModel"; 3 | import D3Appendable from "./D3Appendable"; 4 | import D3Node from "./D3Node"; 5 | 6 | export default class D3Circle implements D3Appendable { 7 | $selection: d3.Selection; 8 | radius: number; 9 | 10 | constructor( 11 | $svg: d3.Selection, 12 | radius: number 13 | ) { 14 | this.radius = radius; 15 | 16 | this.$selection = this._append($svg); 17 | SettingsModel.nodeColor.addEventListener( 18 | LiveData.EVENT_DATA_CHANGED, 19 | (e: Event) => this._changeColor(e.data) 20 | ); 21 | } 22 | 23 | _changeColor(color: string) { 24 | this.$selection.attr("fill", color); 25 | } 26 | 27 | updateRadius(radius: number) { 28 | this.radius = radius; 29 | this.$selection.attr("r", this.radius); 30 | } 31 | 32 | _append($svg: d3.Selection) { 33 | return $svg 34 | .append("circle") 35 | .attr("r", this.radius) 36 | .attr("fill", SettingsModel.nodeColor.value); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/d3/D3DragHandler.ts: -------------------------------------------------------------------------------- 1 | export default class D3DragHandler { 2 | static dragHandler: d3.DragBehavior | null = null; 3 | 4 | static create( 5 | d3: any, 6 | simulation: d3.Simulation 7 | ): d3.DragBehavior { 8 | const dragstarted = ( 9 | e: { active: any }, 10 | d: { fx: any; x: any; fy: any; y: any } 11 | ) => { 12 | if (!e.active) simulation.alphaTarget(0.3).restart(); 13 | d.fx = d.x; 14 | d.fy = d.y; 15 | }; 16 | 17 | const dragged = (e: { x: any; y: any }, d: { fx: any; fy: any }) => { 18 | d.fx = e.x; 19 | d.fy = e.y; 20 | }; 21 | 22 | const dragended = (e: { active: any }, d: { fx: null; fy: null }) => { 23 | if (!e.active) simulation.alphaTarget(0); 24 | d.fx = null; 25 | d.fy = null; 26 | }; 27 | 28 | D3DragHandler.dragHandler = d3 29 | .drag() 30 | .on("start", dragstarted) 31 | .on("drag", dragged) 32 | .on("end", dragended); 33 | 34 | return D3DragHandler.dragHandler!; 35 | } 36 | 37 | static applyDragHandler( 38 | selection: d3.Selection 39 | ): void { 40 | D3DragHandler.dragHandler!(selection); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/d3/D3Graph.ts: -------------------------------------------------------------------------------- 1 | import Graph from "../graph/Graph.js"; 2 | import * as d3 from "d3"; 3 | import D3_CONFIG from "./D3_CONFIG.js"; 4 | import { Event, D3EventBus } from "../utils/Observable.js"; 5 | 6 | import D3Simulation from "./D3Simulation.js"; 7 | import D3Appendable from "./D3Appendable.js"; 8 | import D3Relationship from "./D3Relationship.js"; 9 | import Relationship from "../graph/Relationship.js"; 10 | import D3Node from "./D3Node.js"; 11 | import Node from "../graph/Node.js"; 12 | import D3DragHandler from "./D3DragHandler.js"; 13 | 14 | export default class D3Graph extends Graph implements D3Appendable { 15 | $svg: d3.Selection; 16 | 17 | static width: number; 18 | static height: number; 19 | 20 | d3_color = D3_CONFIG.colorScale; 21 | d3Simulation: d3.Simulation; 22 | d3DragHandler: D3DragHandler; 23 | 24 | d3Relationships: D3Relationship[]; 25 | $selection: d3.Selection; 26 | 27 | constructor(graph: Graph, svg: SVGElement) { 28 | super(graph.relationships); 29 | this.$svg = d3.select(svg); 30 | // get width of element with id "svg" 31 | 32 | D3Graph.width = this.$svg.node()?.getBoundingClientRect().width ?? 0; 33 | D3Graph.height = this.$svg.node()?.getBoundingClientRect().height ?? 0; 34 | this.d3Simulation = D3Simulation.create(d3); 35 | this.d3DragHandler = D3DragHandler.create(d3, this.d3Simulation); 36 | 37 | this.$selection = this._append(this.$svg); 38 | this.d3Relationships = this.relationships.map( 39 | (relationship: Relationship) => { 40 | return new D3Relationship(relationship, this.$selection); 41 | } 42 | ); 43 | 44 | const _nodes = this.d3Relationships.reduce( 45 | (nodes: D3Node[], relationship: D3Relationship) => { 46 | return [...nodes, ...relationship.getD3Nodes()]; 47 | }, 48 | [] 49 | ); 50 | 51 | D3Simulation.simulation!.nodes(_nodes).on("tick", () => { 52 | D3Simulation.isActive = true; 53 | D3EventBus.notifyAll(new Event(D3Simulation.TICK_EVENT, {})); 54 | }); 55 | D3Simulation.simulation!.nodes(_nodes).on("end", () => { 56 | D3Simulation.simulation!.alphaTarget(0.01).restart(); 57 | }); 58 | 59 | const zoomBehavior = d3.zoom().on("zoom", (d) => this._onZoom(d)) as any; 60 | this.$svg.call(zoomBehavior).on("dblclick.zoom", null); 61 | } 62 | 63 | _onZoom(d: any) { 64 | this.$selection.attr("transform", d.transform); 65 | } 66 | 67 | delete() { 68 | this.$svg.selectAll("*").remove(); 69 | D3Simulation.simulation = null; 70 | D3DragHandler.dragHandler = null; 71 | D3EventBus.clear(); 72 | D3Node.d3Nodes = []; 73 | Node.nodes = []; 74 | } 75 | 76 | _append($svg: d3.Selection) { 77 | return $svg.append("g").attr("class", "graph"); 78 | } 79 | 80 | zoomed = (d: any) => { 81 | this.$svg.attr("transform", () => d.transform); 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/d3/D3Label.ts: -------------------------------------------------------------------------------- 1 | import D3Appendable from "./D3Appendable"; 2 | import D3Node from "./D3Node"; 3 | 4 | export default class D3Label implements D3Appendable { 5 | $selection: d3.Selection; 6 | 7 | radius: number; 8 | 9 | constructor( 10 | $svg: d3.Selection, 11 | radius: number 12 | ) { 13 | this.radius = radius; 14 | this.$selection = this._append($svg); 15 | } 16 | 17 | updateTextSize(radius: number) { 18 | this.$selection.attr( 19 | "font-size", 20 | this._calculateFontSizeFromRadius(radius) 21 | ); 22 | } 23 | 24 | _calculateFontSizeFromRadius(radius: number) { 25 | return radius / 4; 26 | } 27 | 28 | _append($svg: d3.Selection) { 29 | return $svg 30 | .append("text") 31 | .text(function (d) { 32 | return d.name; 33 | }) 34 | .attr("font-size", this._calculateFontSizeFromRadius(this.radius)) 35 | .attr("text-anchor", "middle") 36 | .attr("dy", ".35em") 37 | .attr("x", 0) 38 | .attr("y", 0); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/d3/D3Link.ts: -------------------------------------------------------------------------------- 1 | import Link from "../graph/Link"; 2 | import D3Appendable from "./D3Appendable"; 3 | import D3Relationship, { D3NodeConnection } from "./D3Relationship"; 4 | import D3Tickable from "./D3Tickable"; 5 | import { Event, D3EventBus, LiveData } from "../utils/Observable"; 6 | import D3Simulation from "./D3Simulation"; 7 | import { 8 | calcClosestPointsOfCircles, 9 | makeCircleFromD3Node, 10 | VectorPair, 11 | } from "../utils/LinearAlgebra"; 12 | import D3_CONFIG from "./D3_CONFIG"; 13 | import SettingsModel from "../utils/SettingsModel"; 14 | 15 | export default class D3Link extends Link implements D3Appendable, D3Tickable { 16 | static UPDATE_LINKS_EVENT: string = "updateLinks"; 17 | 18 | $selection!: any; 19 | $arrowHead: any; 20 | 21 | nodeConnection: D3NodeConnection; 22 | path: VectorPair = { 23 | source: { x: 0, y: 0 }, 24 | target: { x: 0, y: 0 }, 25 | }; 26 | 27 | constructor( 28 | $svg: d3.Selection, 29 | link: Link, 30 | nodeConnection: D3NodeConnection 31 | ) { 32 | super(link.linkDirection, link.linkOptions); 33 | this.nodeConnection = nodeConnection; 34 | this._append($svg); 35 | D3EventBus.addEventListener(D3Simulation.TICK_EVENT, this.onTicked); 36 | D3EventBus.addEventListener(D3Link.UPDATE_LINKS_EVENT, this._onUpdateLinks); 37 | SettingsModel.linkColor.addEventListener( 38 | LiveData.EVENT_DATA_CHANGED, 39 | (e: Event) => this._changeColor(e.data) 40 | ); 41 | } 42 | 43 | _changeColor(color: string) { 44 | this.$selection.style("stroke", color); 45 | this.$arrowHead.style("fill", color); 46 | } 47 | 48 | _append( 49 | $svg: d3.Selection 50 | ) { 51 | this.$arrowHead = this._appendArrowHead($svg); 52 | this.$selection = $svg 53 | .append("g") 54 | .attr("class", "links") 55 | .selectAll("line") 56 | .data([this.path]) 57 | .enter() 58 | .append("line") 59 | .style("stroke", D3_CONFIG.link.strokeColor) 60 | .style("stroke-width", D3_CONFIG.link.strokeWidth) 61 | .attr("marker-end", () => "url(#arrow)"); 62 | } 63 | 64 | _onUpdateLinks = (e: Event) => { 65 | const data = e.data; 66 | if (data.updatedNodeId === this.nodeConnection.target.id) this.onTicked(); 67 | }; 68 | 69 | _appendArrowHead( 70 | $svg: d3.Selection 71 | ) { 72 | return ( 73 | $svg 74 | .append("svg:defs") 75 | .append("svg:marker") 76 | .attr("id", "arrow") 77 | .attr("viewBox", "0 -5 10 10") 78 | .attr("refX", D3_CONFIG.link.arrow.height) //so that it comes towards the center. 79 | .attr("markerWidth", D3_CONFIG.link.arrow.width) 80 | .attr("markerHeight", D3_CONFIG.link.arrow.height) 81 | .attr("orient", "auto") 82 | // change color 83 | .style("fill", SettingsModel.linkColor.value) 84 | .append("svg:path") 85 | .attr("d", "M0,-5L10,0L0,5") 86 | ); 87 | } 88 | 89 | _updatePath() { 90 | const closestPoints = calcClosestPointsOfCircles( 91 | makeCircleFromD3Node(this.nodeConnection.source), 92 | makeCircleFromD3Node(this.nodeConnection.target) 93 | ); 94 | this.path.source = closestPoints[0]; 95 | this.path.target = closestPoints[1]; 96 | } 97 | 98 | onTicked = (): void => { 99 | this._updatePath(); 100 | try { 101 | //console.log(this.path); 102 | this.$selection 103 | .attr("x1", (d: any) => d.source.x) 104 | .attr("y1", (d: any) => d.source.y) 105 | .attr("x2", (d: any) => d.target.x) 106 | .attr("y2", (d: any) => d.target.y); 107 | } catch (e) { 108 | console.trace(e); 109 | console.log(e, "SHIT"); 110 | } 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /src/d3/D3Node.ts: -------------------------------------------------------------------------------- 1 | import { SimulationNodeDatum } from "d3"; 2 | import Node from "../graph/Node"; 3 | import D3Appendable from "./D3Appendable"; 4 | import D3Circle from "./D3Circle"; 5 | import D3Label from "./D3Label"; 6 | import D3Relationship from "./D3Relationship"; 7 | import D3Tickable from "./D3Tickable"; 8 | import { Event, D3EventBus } from "../utils/Observable"; 9 | import D3Simulation from "./D3Simulation"; 10 | import D3DragHandler from "./D3DragHandler"; 11 | import { LinkStrength, performOperation } from "../graph/Link"; 12 | import D3_CONFIG from "./D3_CONFIG"; 13 | import * as d3 from "d3"; 14 | 15 | export default class D3Node 16 | extends Node 17 | implements D3Appendable, SimulationNodeDatum, D3Tickable 18 | { 19 | static d3Nodes: D3Node[] = []; 20 | 21 | $selection!: d3.Selection; 22 | d3_Circle!: D3Circle; 23 | d3_Label!: D3Label; 24 | 25 | // ~~~~~~~~~ SimulationNodeDatum ~~~~~~~~~ // 26 | index?: number; 27 | x?: number; 28 | y?: number; 29 | vx?: number; 30 | vy?: number; 31 | fx?: number; 32 | fy?: number; 33 | static EMIT_PARTICLE_EVENT: string = "emitParticle"; 34 | 35 | constructor( 36 | node: Node, 37 | $svg: d3.Selection 38 | ) { 39 | const existingNode = D3Node.findNodeById(node.id()); 40 | if (existingNode) return existingNode; 41 | super(node.name, node.nodeType); 42 | this.$selection = this._append($svg); 43 | this.d3_Circle = new D3Circle(this.$selection, this.weightToRadius()); 44 | this.d3_Label = new D3Label(this.$selection, this.weightToRadius()); 45 | 46 | D3EventBus.addEventListener(D3Simulation.TICK_EVENT, this.onTicked); 47 | D3DragHandler.applyDragHandler(this.$selection as any); 48 | this.$selection.on("click", () => 49 | this.notifyAll(new Event(D3Node.EMIT_PARTICLE_EVENT, this)) 50 | ); 51 | D3Node.d3Nodes.push(this); 52 | } 53 | 54 | weightToRadius(): number { 55 | return this.weight * D3_CONFIG.node.weightToRadiusCoefficient; 56 | } 57 | 58 | updateWeight(linkStrength: LinkStrength) { 59 | this.weight = performOperation( 60 | this.weight, 61 | linkStrength.strength, 62 | linkStrength.type 63 | ); 64 | this.d3_Circle.updateRadius(this.weightToRadius()); 65 | this.d3_Label.updateTextSize(this.weightToRadius()); 66 | this.notifyAll(new Event(D3Node.EMIT_PARTICLE_EVENT, this)); 67 | D3Simulation.updateChargeForceStrength(); 68 | } 69 | 70 | _append = ( 71 | $svg: d3.Selection 72 | ) => { 73 | return $svg 74 | .append("g") 75 | .attr("class", "nodes") 76 | .selectAll("g") 77 | .data([this as D3Node]) 78 | .enter() 79 | .append("g") 80 | .style("cursor", "grab"); 81 | }; 82 | 83 | static findNodeById(id: string): D3Node | undefined { 84 | return D3Node.d3Nodes.find((d3Node) => d3Node.id() === id); 85 | } 86 | 87 | onTicked = (): void => { 88 | this.$selection.attr("transform", `translate(${this.x}, ${this.y})`); 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /src/d3/D3Particle.ts: -------------------------------------------------------------------------------- 1 | import { Selection, SimulationNodeDatum } from "d3"; 2 | import { 3 | getColorByType, 4 | getLinkStrengthOperatorByType, 5 | LinkStrengthType, 6 | } from "../graph/Link"; 7 | import { calcJointVector } from "../utils/LinearAlgebra"; 8 | import { Event, D3EventBus, Observable } from "../utils/Observable"; 9 | import D3Appendable from "./D3Appendable"; 10 | import D3Link from "./D3Link"; 11 | import D3Relationship from "./D3Relationship"; 12 | import D3_CONFIG from "./D3_CONFIG"; 13 | import * as d3 from "d3"; 14 | import D3Simulation from "./D3Simulation"; 15 | 16 | // ====================================================== // 17 | // ===================== D3Particle ===================== // 18 | // ====================================================== // 19 | 20 | export default class D3Particle 21 | extends Observable 22 | implements D3Appendable, SimulationNodeDatum 23 | { 24 | static PARTICLE_DESTROYED_EVENT: string = "particleDestroyed"; 25 | static PARTICLE_TICK_EVENT: string = "particleTick"; 26 | 27 | d3Relationship: D3Relationship; 28 | $selection: d3.Selection; 29 | $circle: d3.Selection; 30 | $text: d3.Selection; 31 | 32 | // ~~~~~~~~~~~~~~~ Particle ~~~~~~~~~~~~~~ // 33 | id: string; 34 | radius: number = D3_CONFIG.particle.radius; 35 | creationTime = new Date().getTime(); // the creation time of the particle 36 | travelTime: number; // how long this particle will be alive 37 | 38 | travelVector!: { x: number; y: number }; 39 | 40 | // ~~~~~~~~~ SimulationNodeDatum ~~~~~~~~~ // 41 | index?: number; 42 | x?: number; 43 | y?: number; 44 | vx?: number; 45 | vy?: number; 46 | fx?: number; 47 | fy?: number; 48 | 49 | static particles: D3Particle[] = []; 50 | 51 | constructor(d3Relationship: D3Relationship) { 52 | super(); 53 | this.d3Relationship = d3Relationship; 54 | this.id = d3Relationship.id() + "-" + this.creationTime; 55 | this.travelTime = this._calcTravelTime(); 56 | this._setInitialPosition(d3Relationship.d3Link); 57 | 58 | this.$selection = this._append(d3Relationship.$selection); 59 | this.$circle = this._appendCircle( 60 | this.$selection, 61 | d3Relationship.link.linkOptions.linkStrength.type 62 | ); 63 | this.$text = this._appendText( 64 | this.$selection, 65 | d3Relationship.link.linkOptions.linkStrength.type 66 | ); 67 | 68 | D3EventBus.addEventListener(D3Simulation.TICK_EVENT, this._update); 69 | D3Particle.particles.push(this); 70 | } 71 | 72 | _appendText( 73 | $selection: Selection, 74 | linkStrengthType: LinkStrengthType 75 | ) { 76 | return $selection 77 | .append("text") 78 | .text(() => { 79 | return getLinkStrengthOperatorByType(linkStrengthType); 80 | }) 81 | .attr("dy", ".35em") 82 | .attr("text-anchor", "middle") 83 | .attr("font-size", "10px") 84 | .attr("fill", D3_CONFIG.particle.textColor) 85 | .attr("x", (d: any) => d.x) 86 | .attr("y", (d: any) => d.y); 87 | } 88 | 89 | _appendCircle( 90 | $selection: Selection, 91 | linkStrengthType: LinkStrengthType 92 | ) { 93 | return $selection 94 | .append("circle") 95 | .attr("cx", (d: any) => d.x) 96 | .attr("cy", (d: any) => d.y) 97 | .attr("r", D3_CONFIG.particle.radius) 98 | .attr("fill", getColorByType(linkStrengthType)); 99 | } 100 | 101 | _append($svg: Selection) { 102 | return $svg 103 | .append("g") 104 | .attr("class", "particles") 105 | .selectAll(".particles") 106 | .data([this], (d: any) => d.d3Relationship.id()) 107 | .enter() 108 | .append("g"); 109 | } 110 | 111 | _update = () => { 112 | const progress = this._calcProgress(); 113 | if (progress >= 1) this._destroy(); 114 | 115 | const newPos = this._calcPosition(progress, this.d3Relationship.d3Link); 116 | this._updatePosition(newPos.x, newPos.y); 117 | }; 118 | 119 | _calcTravelTime(): number { 120 | return ( 121 | D3_CONFIG.particle.travelTime / 122 | this.d3Relationship.link.linkOptions.linkSpeed 123 | ); 124 | } 125 | 126 | _setInitialPosition(d3Link: D3Link) { 127 | this.x = d3Link.path.source.x; 128 | this.y = d3Link.path.source.y; 129 | } 130 | 131 | _calcProgress(): number { 132 | const currentTime = new Date().getTime(), 133 | elapsedTime = currentTime - this.creationTime; 134 | return elapsedTime / this.travelTime; 135 | } 136 | 137 | _updatePosition(x: number, y: number) { 138 | this.x = x; 139 | this.y = y; 140 | this.$circle.transition().duration(0).attr("cx", this.x).attr("cy", this.y); 141 | this.$text.transition().duration(0).attr("x", this.x).attr("y", this.y); 142 | } 143 | 144 | _calcPosition(progress: number, d3Link: D3Link) { 145 | this.travelVector = this._calcTravelVector(d3Link); 146 | return { 147 | x: d3Link.path.source.x + this.travelVector.x!! * progress, 148 | y: d3Link.path.source.y + this.travelVector.y!! * progress, 149 | }; 150 | } 151 | 152 | _calcTravelVector(d3Link: D3Link) { 153 | return calcJointVector(d3Link.path.source, d3Link.path.target); 154 | } 155 | 156 | _destroy() { 157 | D3EventBus.removeEventListener(D3Simulation.TICK_EVENT, this._update); 158 | this._remove(); 159 | this.notifyAll(new Event(D3Particle.PARTICLE_DESTROYED_EVENT, this)); 160 | D3Particle.particles.splice(D3Particle.particles.indexOf(this), 1); 161 | } 162 | 163 | _remove() { 164 | this.$selection.remove(); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/d3/D3Relationship.ts: -------------------------------------------------------------------------------- 1 | import { Selection } from "d3"; 2 | 3 | import Relationship from "../graph/Relationship"; 4 | import D3Appendable from "./D3Appendable"; 5 | import D3Link from "./D3Link"; 6 | import D3Node from "./D3Node"; 7 | import D3Particle from "./D3Particle"; 8 | 9 | export default class D3Relationship 10 | extends Relationship 11 | implements D3Appendable 12 | { 13 | $selection: d3.Selection; 14 | 15 | d3Source: D3Node; 16 | d3Target: D3Node; 17 | d3Link: D3Link; 18 | d3Particles: D3Particle[] = []; 19 | 20 | constructor( 21 | relationship: Relationship, 22 | $svg: d3.Selection 23 | ) { 24 | super(relationship.link, relationship.source, relationship.target); 25 | 26 | this.$selection = this._append($svg); 27 | this.d3Source = new D3Node(this.source, this.$selection); 28 | this.d3Source.addEventListener(D3Node.EMIT_PARTICLE_EVENT, () => 29 | this._emitParticle() 30 | ); 31 | this.d3Target = new D3Node(this.target, this.$selection); 32 | this.d3Link = new D3Link( 33 | this.$selection, 34 | this.link, 35 | this.getD3NodeConnection() 36 | ); 37 | } 38 | 39 | _emitParticle() { 40 | const particle = new D3Particle(this); 41 | particle.addEventListener(D3Particle.PARTICLE_DESTROYED_EVENT, () => 42 | this._onParticleDestroyed() 43 | ); 44 | this.d3Particles.push(particle); 45 | } 46 | 47 | _onParticleDestroyed() { 48 | this._removeOldestParticle(); 49 | this.d3Target.updateWeight(this.link.linkOptions.linkStrength); 50 | } 51 | 52 | _removeOldestParticle() { 53 | this.d3Particles.shift(); 54 | } 55 | _append($svg: Selection) { 56 | return $svg 57 | .append("g") 58 | .attr("class", "relationship") 59 | .selectAll("g") 60 | .data([this as D3Relationship]) 61 | .enter() 62 | .append("g"); 63 | } 64 | 65 | getD3Nodes(): D3Node[] { 66 | return [this.d3Source, this.d3Target]; 67 | } 68 | 69 | getD3NodeConnection(): D3NodeConnection { 70 | return { 71 | source: this.d3Source, 72 | target: this.d3Target, 73 | }; 74 | } 75 | } 76 | 77 | export interface D3NodeConnection { 78 | source: D3Node; 79 | target: D3Node; 80 | } 81 | -------------------------------------------------------------------------------- /src/d3/D3Simulation.ts: -------------------------------------------------------------------------------- 1 | import D3Graph from "./D3Graph"; 2 | import D3Node from "./D3Node"; 3 | 4 | class D3Simulation { 5 | static simulation: d3.Simulation | null = 6 | null; 7 | static TICK_EVENT = "d3_tick"; 8 | static isActive = false; 9 | 10 | // ignore ts lint 11 | 12 | static updateChargeForceStrength() { 13 | D3Simulation.simulation 14 | ?.force("charge") 15 | // @ts-ignore 16 | ?.strength(D3Simulation.chargeForceStrength); 17 | } 18 | 19 | static chargeForceStrength(d3Node: D3Node): number { 20 | return -Math.pow(d3Node.weightToRadius(), 2.0) * 0.4; 21 | } 22 | 23 | static create(d3: any): d3.Simulation { 24 | if (D3Simulation.simulation) return D3Simulation.simulation; 25 | D3Simulation.simulation = d3 26 | .forceSimulation() 27 | .force( 28 | "x", 29 | d3.forceX().x(function () { 30 | return D3Graph.width / 2; 31 | }) 32 | ) 33 | .force( 34 | "y", 35 | d3.forceY().y(function () { 36 | return D3Graph.height / 2; 37 | }) 38 | ) 39 | .force("charge", d3.forceManyBody().strength(this.chargeForceStrength)); 40 | return D3Simulation.simulation!; 41 | } 42 | } 43 | 44 | export default D3Simulation; 45 | -------------------------------------------------------------------------------- /src/d3/D3Tickable.ts: -------------------------------------------------------------------------------- 1 | export default interface D3Tickable { 2 | onTicked(): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/d3/D3_CONFIG.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | 3 | const D3_CONFIG = { 4 | colorScale: d3.scaleOrdinal(d3.schemeCategory10), 5 | particle: { 6 | travelTime: 1000, 7 | radius: 7, 8 | fill: "#000", 9 | textColor: "white", 10 | colors: { 11 | plus: "#009900", 12 | minus: "#ff3300", 13 | divide: "#cc0000", 14 | multiply: "#336600", 15 | }, 16 | }, 17 | link: { 18 | strokeWidth: 1, 19 | strokeColor: "#808080", 20 | arrow: { 21 | width: 10, 22 | height: 10, 23 | }, 24 | }, 25 | node: { 26 | weightToRadiusCoefficient: 0.5, 27 | fillColor: "#0284C7", 28 | }, 29 | 30 | website: { 31 | startInput: `"Photosynthesis"--(+10)-->"Energy" 32 | "Photosynthesis"--(-3)-->"CO2" 33 | "Photosynthesis"--(+3)-->"O2" 34 | "Energy"---->"Growth" 35 | "Growth"---->"Size" 36 | "Size"---->"Photosynthesis" 37 | `, 38 | helpLink: "https://github.com/AlexW00/Flow_Graph#-documentation", 39 | }, 40 | }; 41 | 42 | export default D3_CONFIG; 43 | -------------------------------------------------------------------------------- /src/grammar/attributeGrammar.ts: -------------------------------------------------------------------------------- 1 | import lexicalRuleset from "./lexicalRuleset"; 2 | import semanticRuleset from "./semanticRuleset"; 3 | import syntaxRuleset from "./syntaxRuleset"; 4 | 5 | const attributeGrammar = { 6 | lexicalRuleset, 7 | semanticRuleset, 8 | syntaxRuleset, 9 | }; 10 | 11 | export default attributeGrammar; 12 | -------------------------------------------------------------------------------- /src/grammar/lexicalRuleset.ts: -------------------------------------------------------------------------------- 1 | // ##################################################################### // 2 | // ########################### LexicalRuleset ########################## // 3 | // ##################################################################### // 4 | 5 | import { LexicalRuleset } from "tiny-comp"; 6 | 7 | const lexicalRuleset: LexicalRuleset = { 8 | // the name of the token 9 | whitespace: { 10 | regex: /([\s\r\n])/, // the regex that matches the token 11 | }, 12 | text_node: { 13 | regex: /"[^"]+"/, 14 | }, 15 | reference_node: { 16 | regex: /\[\[[^\[^\]]+\]\]/, 17 | }, 18 | link_body: { 19 | regex: /--/, 20 | }, 21 | link_direction_left: { 22 | regex: //, 26 | }, 27 | link_options_start: { 28 | regex: /\(/, 29 | }, 30 | link_options_end: { 31 | regex: /\)/, 32 | }, 33 | link_option_delimiter: { 34 | regex: /,/, 35 | }, 36 | number: { 37 | regex: /[0-9]+/, 38 | }, 39 | math_operator: { 40 | regex: /[\+\-\/\*]/, 41 | }, 42 | 43 | // ... more lexical rules go here 44 | }; 45 | 46 | export default lexicalRuleset; 47 | -------------------------------------------------------------------------------- /src/grammar/semanticRuleset.ts: -------------------------------------------------------------------------------- 1 | // ##################################################################### // 2 | // ########################## SemanticRuleset ########################## // 3 | // ##################################################################### // 4 | 5 | import { 6 | _getFirstSemanticContextBySyntaxRuleName, 7 | _getSemanticContextsBySyntaxRuleName, 8 | SemanticRuleset, 9 | Attribute, 10 | SemanticContext, 11 | } from "tiny-comp"; 12 | import Graph from "../graph/Graph.js"; 13 | 14 | import Link, { 15 | LinkStrength, 16 | LinkOptions, 17 | LinkDirection, 18 | getLinkStrengthTypeByOperator, 19 | } from "../graph/Link.js"; 20 | import Node, { NodeType } from "../graph/Node"; 21 | import Relationship from "../graph/Relationship"; 22 | 23 | const _makeLinkContext = ( 24 | semanticContexts: SemanticContext[], 25 | linkDirection: LinkDirection 26 | ): SemanticContext => { 27 | const semanticContext = new SemanticContext("LINK"); // create a new semantic context (= array of attributes) 28 | 29 | const linkOptions = _getFirstSemanticContextBySyntaxRuleName( 30 | "LINK_OPTIONS", 31 | semanticContexts, 32 | true 33 | ); 34 | 35 | const _deps = []; 36 | if (linkOptions) _deps.push(linkOptions.getAttribute("val")); 37 | 38 | semanticContext.addAttribute( 39 | new Attribute("val", _deps, (...deps) => { 40 | return new Link( 41 | linkDirection, 42 | deps[0] ? deps[0].value() : new LinkOptions() 43 | ); 44 | }) 45 | ); 46 | return semanticContext; 47 | }; 48 | 49 | const _makeNodeContext = ( 50 | semanticContexts: SemanticContext[], 51 | nodeType: NodeType 52 | ): SemanticContext => { 53 | const semanticContext = new SemanticContext("NODE"); 54 | const node = 55 | nodeType === NodeType.TEXT_NODE 56 | ? _getFirstSemanticContextBySyntaxRuleName("text_node", semanticContexts) 57 | : _getFirstSemanticContextBySyntaxRuleName( 58 | "reference_node", 59 | semanticContexts 60 | ); 61 | 62 | semanticContext.addAttribute( 63 | new Attribute("val", [node!!.getAttribute("lex")], (...deps) => { 64 | const n = new Node(deps[0].value(), nodeType); 65 | return n; 66 | }) 67 | ); 68 | return semanticContext; // return the semantic context 69 | }; 70 | 71 | // ====================================================== // 72 | // =================== SemanticRuleset ================== // 73 | // ====================================================== // 74 | 75 | const semanticRuleset: SemanticRuleset = { 76 | GRAPH: { 77 | _: (...semanticContexts) => { 78 | const semanticContext = new SemanticContext("GRAPH"); // create a new semantic context (= array of attributes) 79 | const relationships = _getSemanticContextsBySyntaxRuleName( 80 | "RELATIONSHIP", 81 | semanticContexts 82 | ).map((relationship) => relationship.getAttribute("val")); 83 | 84 | semanticContext.addAttribute( 85 | new Attribute("val", relationships, (...deps) => { 86 | return new Graph(deps.map((dep) => dep.value())); 87 | }) 88 | ); 89 | return semanticContext; // return the semantic context 90 | }, 91 | }, 92 | 93 | RELATIONSHIP: { 94 | _: (...semanticContexts) => { 95 | const semanticContext = new SemanticContext("RELATIONSHIP"); // create a new semantic context (= array of attributes) 96 | 97 | const nodes = _getSemanticContextsBySyntaxRuleName( 98 | "NODE", 99 | semanticContexts 100 | ); 101 | 102 | const link = _getFirstSemanticContextBySyntaxRuleName( 103 | "LINK", 104 | semanticContexts 105 | ); 106 | 107 | semanticContext.addAttribute( 108 | new Attribute( 109 | "val", 110 | [ 111 | link!!.getAttribute("val"), 112 | nodes[0].getAttribute("val"), 113 | nodes[1].getAttribute("val"), 114 | ], 115 | (...deps) => { 116 | const r = new Relationship( 117 | deps[0].value(), 118 | deps[1].value(), 119 | deps[2].value() 120 | ); 121 | 122 | return r; 123 | } 124 | ) 125 | ); 126 | return semanticContext; // return the semantic context 127 | }, 128 | }, 129 | 130 | NODE: { 131 | text_node: (...semanticContexts) => { 132 | return _makeNodeContext(semanticContexts, NodeType.TEXT_NODE); 133 | }, 134 | reference_node: (...semanticContexts) => { 135 | return _makeNodeContext(semanticContexts, NodeType.REFERENCE_NODE); 136 | }, 137 | }, 138 | 139 | LINK: { 140 | right: (...semanticContexts) => { 141 | return _makeLinkContext(semanticContexts, LinkDirection.RIGHT); 142 | }, 143 | left: (...semanticContexts) => { 144 | return _makeLinkContext(semanticContexts, LinkDirection.LEFT); 145 | }, 146 | left_right: (...semanticContexts) => { 147 | return _makeLinkContext(semanticContexts, LinkDirection.LEFT_RIGHT); 148 | }, 149 | }, 150 | 151 | LINK_OPTIONS: { 152 | _: (...semanticContexts) => { 153 | const semanticContext = new SemanticContext("LINK_OPTIONS"); // create a new semantic context (= array of attributes) 154 | 155 | const linkOptionStrength = _getFirstSemanticContextBySyntaxRuleName( 156 | "LINK_OPTION_STRENGTH", 157 | semanticContexts 158 | ); 159 | 160 | const linkOptionSpeed = _getFirstSemanticContextBySyntaxRuleName( 161 | "LINK_OPTION_SPEED", 162 | semanticContexts, 163 | true 164 | ); 165 | 166 | const _deps = []; 167 | _deps.push(linkOptionStrength!!.getAttribute("val")); 168 | if (linkOptionSpeed) _deps.push(linkOptionSpeed.getAttribute("val")); 169 | 170 | semanticContext.addAttribute( 171 | new Attribute("val", _deps, (...deps) => { 172 | return new LinkOptions( 173 | deps[0].value(), 174 | deps.length > 1 ? (deps[1].value() as number) : undefined 175 | ); 176 | }) 177 | ); 178 | return semanticContext; // return the semantic context 179 | }, 180 | }, 181 | 182 | LINK_OPTION_STRENGTH: { 183 | _: (...semanticContexts) => { 184 | const semanticContext = new SemanticContext("LINK_OPTION_STRENGTH"); // create a new semantic context (= array of attributes) 185 | 186 | const strengthType = _getFirstSemanticContextBySyntaxRuleName( 187 | "math_operator", 188 | semanticContexts 189 | ); 190 | 191 | const strength = _getFirstSemanticContextBySyntaxRuleName( 192 | "number", 193 | semanticContexts 194 | ); 195 | 196 | semanticContext.addAttribute( 197 | new Attribute( 198 | "val", 199 | [strengthType!!.getAttribute("lex"), strength!!.getAttribute("lex")], 200 | (...deps) => { 201 | return new LinkStrength( 202 | deps[1].value(), 203 | getLinkStrengthTypeByOperator(deps[0].value()) 204 | ); 205 | } 206 | ) 207 | ); 208 | return semanticContext; 209 | }, 210 | }, 211 | 212 | LINK_OPTION_SPEED: { 213 | _: (...semanticContexts) => { 214 | const semanticContext = new SemanticContext("LINK_OPTION_SPEED"); // create a new semantic context (= array of attributes) 215 | const speed = _getFirstSemanticContextBySyntaxRuleName( 216 | "number", 217 | semanticContexts 218 | ); 219 | 220 | semanticContext.addAttribute( 221 | new Attribute("val", [speed!!.getAttribute("lex")], (...deps) => { 222 | return deps[0].value(); 223 | }) 224 | ); 225 | return semanticContext; // return the semantic context 226 | }, 227 | }, 228 | }; 229 | export default semanticRuleset; 230 | -------------------------------------------------------------------------------- /src/grammar/syntaxRuleset.ts: -------------------------------------------------------------------------------- 1 | // ##################################################################### // 2 | // ########################### syntaxRuleset ########################### // 3 | // ##################################################################### // 4 | 5 | import { SyntaxRuleset } from "tiny-comp"; 6 | 7 | const syntaxRuleset: SyntaxRuleset = { 8 | GRAPH: { 9 | _: ["RELATIONSHIP+"], 10 | }, 11 | 12 | RELATIONSHIP: { 13 | _: ["NODE", "LINK", "NODE"], 14 | }, 15 | 16 | NODE: { 17 | text_node: ["text_node"], 18 | reference_node: ["reference_node"], 19 | }, 20 | 21 | LINK: { 22 | right: ["link_body", "LINK_OPTIONS?", "link_body", "link_direction_right"], 23 | left: ["link_direction_left", "link_body", "LINK_OPTIONS?", "link_body"], 24 | left_right: [ 25 | "link_direction_left", 26 | "link_body", 27 | "LINK_OPTIONS?", 28 | "link_body", 29 | "link_direction_right", 30 | ], 31 | }, 32 | 33 | LINK_OPTIONS: { 34 | _: [ 35 | "link_options_start", 36 | "LINK_OPTION_STRENGTH", 37 | "LINK_OPTION_SPEED?", 38 | "link_options_end", 39 | ], 40 | }, 41 | 42 | LINK_OPTION_STRENGTH: { 43 | _: ["math_operator", "number"], 44 | }, 45 | 46 | LINK_OPTION_SPEED: { 47 | _: ["link_option_delimiter", "number"], 48 | }, 49 | // more syntax rules go here 50 | }; 51 | export default syntaxRuleset; 52 | -------------------------------------------------------------------------------- /src/graph/Graph.ts: -------------------------------------------------------------------------------- 1 | // ====================================================== // 2 | // ======================== Graph ======================= // 3 | // ====================================================== // 4 | 5 | import Relationship from "./Relationship"; 6 | import Node from "./Node"; 7 | import Link from "./Link"; 8 | 9 | export default class Graph { 10 | relationships: Relationship[]; 11 | 12 | constructor(relationships: Relationship[] = []) { 13 | this.relationships = relationships; 14 | } 15 | 16 | addRelationship(relationship: Relationship) { 17 | this.relationships.push(relationship); 18 | } 19 | 20 | removeRelationshipById(id: string) { 21 | this.relationships = this.relationships.filter( 22 | (relationship) => relationship.id() !== id 23 | ); 24 | } 25 | 26 | getRelationshipById(id: string) { 27 | return this.relationships.find((relationship) => relationship.id() === id); 28 | } 29 | 30 | getNodeList(): Node[] { 31 | return this.relationships 32 | .map((relationship) => relationship.getNodes()) 33 | .reduce((acc, val) => acc.concat(val), []); 34 | } 35 | 36 | getLinkList(): Link[] { 37 | return this.relationships.map((relationship) => relationship.link); 38 | } 39 | 40 | getNodeConnections(): NodeConnection[] { 41 | return this.relationships.map((relationship) => { 42 | return { 43 | source: relationship.source, 44 | target: relationship.target, 45 | }; 46 | }); 47 | } 48 | } 49 | 50 | export interface NodeConnection { 51 | source: Node; 52 | target: Node; 53 | } 54 | -------------------------------------------------------------------------------- /src/graph/Link.ts: -------------------------------------------------------------------------------- 1 | // ====================================================== // 2 | // ======================== Link ======================== // 3 | // ====================================================== // 4 | 5 | import D3_CONFIG from "../d3/D3_CONFIG"; 6 | 7 | export default class Link { 8 | linkDirection: LinkDirection; 9 | linkOptions: LinkOptions; 10 | 11 | constructor(linkDirection: LinkDirection, linkOptions: LinkOptions) { 12 | this.linkDirection = linkDirection; 13 | this.linkOptions = linkOptions; 14 | } 15 | } 16 | 17 | // ~~~~~~~~~~~~~ LinkOptions ~~~~~~~~~~~~~ // 18 | 19 | export class LinkOptions { 20 | linkStrength: LinkStrength; 21 | linkSpeed: number; 22 | 23 | constructor( 24 | linkStrength: LinkStrength = new LinkStrength(), 25 | linkSpeed: number = LINK_DEFAULT_VALUES.speed 26 | ) { 27 | this.linkStrength = linkStrength; 28 | this.linkSpeed = _smartStringParse(linkSpeed); 29 | } 30 | } 31 | 32 | // ~~~~~~~~~~~~~ LinkStrength ~~~~~~~~~~~~ // 33 | 34 | export class LinkStrength { 35 | type: LinkStrengthType; 36 | strength: number; 37 | 38 | constructor( 39 | strength: number | string = LINK_DEFAULT_VALUES.strength, 40 | linkStrengthType = LINK_DEFAULT_VALUES.strengthType 41 | ) { 42 | this.type = linkStrengthType; 43 | this.strength = _smartStringParse(strength); 44 | } 45 | } 46 | 47 | function _smartStringParse(str: number | string): number { 48 | return typeof str === typeof String() 49 | ? parseInt(str as string) 50 | : (str as number); 51 | } 52 | 53 | export enum LinkStrengthType { 54 | PLUS = 0, 55 | MINUS = 1, 56 | MULTIPLY = 2, 57 | DIVIDE = 3, 58 | } 59 | 60 | export function getLinkStrengthTypeByOperator( 61 | operator: string 62 | ): LinkStrengthType { 63 | switch (operator) { 64 | case "+": 65 | return LinkStrengthType.PLUS; 66 | case "-": 67 | return LinkStrengthType.MINUS; 68 | case "*": 69 | return LinkStrengthType.MULTIPLY; 70 | case "/": 71 | return LinkStrengthType.DIVIDE; 72 | default: 73 | return LinkStrengthType.PLUS; 74 | } 75 | } 76 | 77 | export function getLinkStrengthOperatorByType(type: LinkStrengthType): string { 78 | switch (type) { 79 | case LinkStrengthType.PLUS: 80 | return "+"; 81 | case LinkStrengthType.MINUS: 82 | return "-"; 83 | case LinkStrengthType.MULTIPLY: 84 | return "*"; 85 | case LinkStrengthType.DIVIDE: 86 | return "/"; 87 | default: 88 | return "+"; 89 | } 90 | } 91 | 92 | export function getColorByType(type: LinkStrengthType): string { 93 | switch (type) { 94 | case LinkStrengthType.PLUS: 95 | return D3_CONFIG.particle.colors.plus; 96 | case LinkStrengthType.MINUS: 97 | return D3_CONFIG.particle.colors.minus; 98 | case LinkStrengthType.MULTIPLY: 99 | return D3_CONFIG.particle.colors.multiply; 100 | case LinkStrengthType.DIVIDE: 101 | return D3_CONFIG.particle.colors.divide; 102 | default: 103 | return "black"; 104 | } 105 | } 106 | 107 | export function performOperation( 108 | operand1: number, 109 | operand2: number, 110 | linkStrengthType: LinkStrengthType 111 | ) { 112 | switch (linkStrengthType) { 113 | case LinkStrengthType.PLUS: 114 | return operand1 + operand2; 115 | case LinkStrengthType.MINUS: 116 | return operand1 - operand2; 117 | case LinkStrengthType.MULTIPLY: 118 | return operand1 * operand2; 119 | case LinkStrengthType.DIVIDE: 120 | return operand1 / operand2; 121 | default: 122 | return operand1 + operand2; 123 | } 124 | } 125 | 126 | // ~~~~~~~~~~~~ LinkDirection ~~~~~~~~~~~~ // 127 | 128 | export enum LinkDirection { 129 | LEFT = "LEFT", 130 | RIGHT = "RIGHT", 131 | LEFT_RIGHT = "LEFT_RIGHT", 132 | } 133 | 134 | export let LINK_DEFAULT_VALUES = { 135 | strength: 5, 136 | strengthType: LinkStrengthType.PLUS, 137 | speed: 1, 138 | }; 139 | -------------------------------------------------------------------------------- /src/graph/Node.ts: -------------------------------------------------------------------------------- 1 | // ====================================================== // 2 | // ======================== Node ======================== // 3 | // ====================================================== // 4 | 5 | import { Observable } from "../utils/Observable"; 6 | 7 | export default class Node extends Observable { 8 | // ~~~~~~~~~~~~~~~~~ Node ~~~~~~~~~~~~~~~~ // 9 | name!: string; 10 | nodeType!: NodeType; 11 | weight: number = 100; 12 | 13 | static nodes: Node[] = []; 14 | 15 | constructor(name: string, nodeType: NodeType) { 16 | const existingNode = Node.findNodeById(Node._generateId(name, nodeType)); 17 | if (existingNode) return existingNode; 18 | super(); 19 | // strip the quotes from the name 20 | this.name = name.replace(/['"]+/g, ""); 21 | this.nodeType = nodeType; 22 | } 23 | 24 | id() { 25 | return Node._generateId(this.name, this.nodeType); 26 | } 27 | 28 | static _generateId(name: string, nodeType: NodeType) { 29 | return name + "-" + nodeType; 30 | } 31 | 32 | static findNodeById(id: string): Node | undefined { 33 | return Node.nodes.find((node) => node.id() === id); 34 | } 35 | } 36 | 37 | export enum NodeType { 38 | TEXT_NODE = "TEXT_NODE", 39 | REFERENCE_NODE = "REFERENCE_NODE", 40 | } 41 | -------------------------------------------------------------------------------- /src/graph/Relationship.ts: -------------------------------------------------------------------------------- 1 | // ====================================================== // 2 | // ==================== Relationship ==================== // 3 | // ====================================================== // 4 | 5 | import Node from "./Node"; 6 | import Link from "./Link"; 7 | import { NodeConnection } from "./Graph"; 8 | 9 | export default class Relationship implements NodeConnection { 10 | link: Link; 11 | source: Node; 12 | target: Node; 13 | id(): string { 14 | return this.source.name + "-" + this.target.name; 15 | } 16 | 17 | constructor(link: Link, node1: Node, node2: Node) { 18 | this.link = link; 19 | this.source = node1; 20 | this.target = node2; 21 | } 22 | 23 | getNodes(): Node[] { 24 | return [this.source, this.target]; 25 | } 26 | 27 | getNodeConnection(): NodeConnection { 28 | return { 29 | source: this.source, 30 | target: this.target, 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import "./style.css"; 2 | import IFrameComponent from "./ui/components/iFrame/IFrameComponent"; 3 | import WebappComponent from "./ui/components/webapp/WebappComponent"; 4 | import CompilerModel from "./utils/CompilerModel"; 5 | import { Event, LiveData } from "./utils/Observable"; 6 | 7 | const url = new URL(window.location.href), 8 | isIframe = url.searchParams.get("iframe") === "true"; 9 | 10 | const app: WebappComponent | IFrameComponent = isIframe 11 | ? new IFrameComponent() 12 | : new WebappComponent(); 13 | document.body.appendChild(app.html()); 14 | CompilerModel.graph.notifyAll(new Event(LiveData.EVENT_DATA_CHANGED, {})); 15 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/ui/components/Component.ts: -------------------------------------------------------------------------------- 1 | // ====================================================== // 2 | // ====================== Component ===================== // 3 | // ====================================================== // 4 | 5 | export default abstract class Component { 6 | protected $root: HTMLElement | SVGElement | undefined; 7 | protected abstract _render(): HTMLElement | SVGElement; 8 | 9 | html(): HTMLElement | SVGElement { 10 | return this.$root ?? this._render(); 11 | } 12 | 13 | static cloneTemplate(id: string): HTMLElement | SVGElement { 14 | const template = document.querySelector( 15 | `#${id}` 16 | )!; 17 | template.classList.remove("hidden"); 18 | return template; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ui/components/iFrame/IFrameComponent.ts: -------------------------------------------------------------------------------- 1 | import Component from "../Component"; 2 | import PreviewComponent from "../preview/PreviewComponent"; 3 | 4 | export default class IFrameComponent extends Component { 5 | previewComponent: PreviewComponent; 6 | 7 | constructor() { 8 | super(); 9 | this.previewComponent = new PreviewComponent(true); 10 | } 11 | 12 | protected _render(): HTMLElement | SVGElement { 13 | this.$root = document.createElement("div"); 14 | this.$root.classList.add(...["w-screen", "h-screen"]); 15 | this.$root.appendChild(this.previewComponent.html()); 16 | return this.$root; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/components/preview/PreviewComponent.ts: -------------------------------------------------------------------------------- 1 | import Component from "../Component"; 2 | import SettingsComponent from "./SettingsComponent"; 3 | import SvgComponent from "./SvgComponent"; 4 | 5 | // ====================================================== // 6 | // ================== PreviewComponent ================== // 7 | // ====================================================== // 8 | 9 | export default class PreviewComponent extends Component { 10 | settingsComponent: SettingsComponent; 11 | svgComponent: SvgComponent; 12 | isIframe: boolean; 13 | 14 | constructor(isIframe: boolean) { 15 | super(); 16 | this.isIframe = isIframe; 17 | this.settingsComponent = new SettingsComponent(); 18 | this.svgComponent = new SvgComponent(); 19 | } 20 | 21 | protected _render(): HTMLElement { 22 | this.$root = Component.cloneTemplate("preview-template") as HTMLElement; 23 | this.$root.classList.add(...this._getSizeClasses(this.isIframe)); 24 | this.$root.appendChild(this.settingsComponent.html()); 25 | this.$root.appendChild(this.svgComponent.html()); 26 | return this.$root; 27 | } 28 | 29 | private _getSizeClasses(isIframe: boolean) { 30 | if (!isIframe) return ["w-3/4", "right-0", "m-1"]; 31 | else return ["w-full", "h-full"]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ui/components/preview/SettingsComponent.ts: -------------------------------------------------------------------------------- 1 | import SettingsModel from "../../../utils/SettingsModel"; 2 | import Component from "../Component"; 3 | 4 | // ====================================================== // 5 | // ================== SettingsComponent ================= // 6 | // ====================================================== // 7 | 8 | export default class SettingsComponent extends Component { 9 | protected _render(): HTMLElement { 10 | this.$root = Component.cloneTemplate("settings-template") as HTMLElement; 11 | 12 | this.$root.classList.remove("hidden"); 13 | this._nodeColorPickerController( 14 | this.$root.querySelector("#node_color")! 15 | ); 16 | this._linkColorPickerController( 17 | this.$root.querySelector("#link_color")! 18 | ); 19 | this._settingsButtonController( 20 | this.$root.querySelector("#settings_button")!, 21 | this.$root.querySelector("#settings_container")! 22 | ); 23 | return this.$root; 24 | } 25 | 26 | private _nodeColorPickerController = (nodeColorPicker: HTMLInputElement) => { 27 | nodeColorPicker.value = SettingsModel.nodeColor.value; 28 | nodeColorPicker.addEventListener("change", (e) => { 29 | const newColor = (e.target as HTMLInputElement).value; 30 | SettingsModel.nodeColor.value = newColor; 31 | }); 32 | }; 33 | 34 | private _linkColorPickerController = (linkColorPicker: HTMLInputElement) => { 35 | linkColorPicker.value = SettingsModel.linkColor.value; 36 | linkColorPicker.addEventListener("change", (e) => { 37 | const newColor = (e.target as HTMLInputElement).value; 38 | SettingsModel.linkColor.value = newColor; 39 | }); 40 | }; 41 | 42 | private _settingsButtonController = ( 43 | settingsButton: HTMLButtonElement, 44 | settingsContainer: HTMLDivElement 45 | ): HTMLButtonElement => { 46 | settingsButton.addEventListener("click", () => toggleSettings()); 47 | 48 | function toggleSettings() { 49 | settingsContainer.classList.toggle("invisible"); 50 | } 51 | 52 | return settingsButton; 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/ui/components/preview/SvgComponent.ts: -------------------------------------------------------------------------------- 1 | import Component from "../Component"; 2 | import D3Graph from "../../../d3/D3Graph"; 3 | import CompilerModel from "../../../utils/CompilerModel"; 4 | import { LiveData } from "../../../utils/Observable"; 5 | import InputModel from "../../../utils/InputModel"; 6 | 7 | // ====================================================== // 8 | // ==================== SvgComponent ==================== // 9 | // ====================================================== // 10 | 11 | export default class SvgComponent extends Component { 12 | private d3Graph: D3Graph | undefined; 13 | 14 | constructor() { 15 | super(); 16 | CompilerModel.graph.addEventListener(LiveData.EVENT_DATA_CHANGED, () => 17 | this._renderGraph() 18 | ); 19 | this._tryCompileGraph(); 20 | } 21 | 22 | protected _render(): SVGElement { 23 | this.$root = Component.cloneTemplate("svg-template") as SVGElement; 24 | return this.$root; 25 | } 26 | 27 | private _tryCompileGraph() { 28 | try { 29 | const input = InputModel.inputString.value, 30 | graph = CompilerModel.compiler.compile(input); 31 | CompilerModel.graph.value = graph; 32 | } catch (e: any) { 33 | console.log(e); 34 | } 35 | } 36 | 37 | private _renderGraph() { 38 | if (this.d3Graph !== undefined) this.d3Graph.delete(); 39 | if (CompilerModel.graph.value !== undefined) 40 | this.d3Graph = new D3Graph( 41 | CompilerModel.graph.value, 42 | this.$root as SVGElement 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ui/components/webapp/ContentComponent.ts: -------------------------------------------------------------------------------- 1 | import Component from "../Component"; 2 | import PreviewComponent from "../preview/PreviewComponent"; 3 | import InputSidebarComponent from "./InputSidebarComponent"; 4 | 5 | export default class ContentComponent extends Component { 6 | previewComponent: PreviewComponent; 7 | inputSidebarComponent: InputSidebarComponent; 8 | 9 | constructor() { 10 | super(); 11 | this.inputSidebarComponent = new InputSidebarComponent(); 12 | this.previewComponent = new PreviewComponent(false); 13 | } 14 | 15 | protected _render(): HTMLElement | SVGElement { 16 | this.$root = Component.cloneTemplate("content-template"); 17 | 18 | this.$root.appendChild(this.inputSidebarComponent.html()); 19 | this.$root.appendChild(this.previewComponent.html()); 20 | 21 | return this.$root; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ui/components/webapp/InputSidebarComponent.ts: -------------------------------------------------------------------------------- 1 | import D3_CONFIG from "../../../d3/D3_CONFIG"; 2 | import Graph from "../../../graph/Graph"; 3 | import CompilerModel from "../../../utils/CompilerModel"; 4 | import InputModel from "../../../utils/InputModel"; 5 | import Component from "../Component"; 6 | import ShareButtonComponent from "./ShareButtonComponent"; 7 | 8 | // ====================================================== // 9 | // ================ InputSidebarComponent =============== // 10 | // ====================================================== // 11 | 12 | export default class InputSidebarComponent extends Component { 13 | $renderButton: HTMLInputElement | undefined; 14 | $errorBox: HTMLDivElement | undefined; 15 | $errorMessage: HTMLDivElement | undefined; 16 | $shareButton: HTMLButtonElement | undefined; 17 | shareButtonComponent: ShareButtonComponent; 18 | 19 | constructor() { 20 | super(); 21 | this.shareButtonComponent = new ShareButtonComponent(); 22 | } 23 | 24 | protected _render(): HTMLElement { 25 | this.$root = Component.cloneTemplate( 26 | "inputSidebar-template" 27 | ) as HTMLElement; 28 | 29 | this.$renderButton = 30 | this.$root.querySelector("#submit_button")!; 31 | this.$errorBox = this.$root.querySelector("#error_box")!; 32 | this.$errorMessage = 33 | this.$root.querySelector("#error_message")!; 34 | this._helpButtonController( 35 | this.$root.querySelector("#help_button")! 36 | ); 37 | 38 | this.$renderButton.addEventListener("click", () => { 39 | const newInput = InputModel.inputString.value; 40 | CompilerModel.graph.value = CompilerModel.compiler.compile(newInput); 41 | }); 42 | 43 | this.$shareButton = this.shareButtonComponent.html() as HTMLButtonElement; 44 | this.$root 45 | .querySelector("#input-button-bar") 46 | ?.appendChild(this.$shareButton!); 47 | this._inputController( 48 | this.$root.querySelector("#input")! 49 | ); 50 | return this.$root; 51 | } 52 | 53 | private _helpButtonController = (helpButton: HTMLButtonElement) => { 54 | helpButton.addEventListener("click", () => { 55 | window.open(D3_CONFIG.website.helpLink); 56 | }); 57 | }; 58 | 59 | private _inputController = (input: HTMLInputElement) => { 60 | input.addEventListener("input", (e) => 61 | this._onInput((e.target as HTMLInputElement).value) 62 | ); 63 | 64 | input.value = InputModel.inputString.value; 65 | this._onInput(input.value); 66 | }; 67 | 68 | private _onInput(newInput: string): any { 69 | InputModel.inputString.value = newInput; 70 | try { 71 | CompilerModel.compiler.compile(InputModel.inputString.value) as Graph; 72 | this._toggleErrorBox(false, null); 73 | this._toggleBlueButton(true, this.$renderButton!); 74 | this._toggleBlueButton(true, this.$shareButton!); 75 | } catch (e: any) { 76 | this._toggleErrorBox(true, e.message); 77 | this._toggleBlueButton(false, this.$renderButton!); 78 | this._toggleBlueButton(false, this.$shareButton!); 79 | } 80 | } 81 | 82 | private _toggleErrorBox(doShow: boolean, message: string | null) { 83 | if (message) this.$errorMessage!.innerText = message; 84 | this.$errorBox!.classList.toggle("hidden", !doShow); 85 | } 86 | 87 | private _toggleBlueButton( 88 | doActivate: boolean, 89 | button: HTMLButtonElement | HTMLInputElement 90 | ) { 91 | const disabledButtonClasses = [ 92 | "bg-gray-500", 93 | "hover:bg-gray-400", 94 | "border-gray-700", 95 | "hover:border-gray-500", 96 | ], 97 | enabledButtonClasses = [ 98 | "bg-blue-500", 99 | "hover:bg-blue-400", 100 | "border-blue-700", 101 | "hover:border-blue-500", 102 | ]; 103 | button.disabled = !doActivate; 104 | if (!doActivate) { 105 | button.classList.remove(...enabledButtonClasses); 106 | button.classList.add(...disabledButtonClasses); 107 | } else { 108 | button.classList.remove(...disabledButtonClasses); 109 | button.classList.add(...enabledButtonClasses); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/ui/components/webapp/NavbarComponent.ts: -------------------------------------------------------------------------------- 1 | import Component from "../Component"; 2 | 3 | // ====================================================== // 4 | // =================== NavbarComponent ================== // 5 | // ====================================================== // 6 | 7 | export default class NavbarComponent extends Component { 8 | protected _render(): HTMLElement { 9 | this.$root = Component.cloneTemplate("navbar-template") as HTMLElement; 10 | return this.$root; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ui/components/webapp/ShareButtonComponent.ts: -------------------------------------------------------------------------------- 1 | import Component from "../Component"; 2 | import SharePopupComponent from "./SharePopupComponent"; 3 | 4 | // ====================================================== // 5 | // ================ ShareButtonComponent ================ // 6 | // ====================================================== // 7 | 8 | export default class ShareButtonComponent extends Component { 9 | sharePopupComponent: SharePopupComponent | undefined; 10 | 11 | protected _render(): HTMLInputElement { 12 | this.$root = Component.cloneTemplate( 13 | "share-button-template" 14 | ) as HTMLInputElement; 15 | this.sharePopupComponent = new SharePopupComponent( 16 | this.$root as HTMLInputElement 17 | ); 18 | this.$root.parentElement?.appendChild(this.sharePopupComponent.html()); 19 | this.shareButtonController(this.$root as HTMLInputElement); 20 | return this.$root as HTMLInputElement; 21 | } 22 | 23 | shareButtonController(shareButton: HTMLInputElement) { 24 | shareButton.addEventListener("click", () => { 25 | this.sharePopupComponent?.toggle(); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ui/components/webapp/SharePopupComponent.ts: -------------------------------------------------------------------------------- 1 | import InputModel from "../../../utils/InputModel"; 2 | import SettingsModel from "../../../utils/SettingsModel"; 3 | import Component from "../Component"; 4 | 5 | export default class SharePopupComponent extends Component { 6 | $shareButton: HTMLInputElement | undefined; 7 | $sharePopupUrlInput: HTMLInputElement | undefined; 8 | $sharePopupIframeInput: HTMLInputElement | undefined; 9 | 10 | constructor($shareButton: HTMLInputElement) { 11 | super(); 12 | this.$shareButton = $shareButton; 13 | } 14 | 15 | protected _render(): HTMLElement { 16 | this.$root = Component.cloneTemplate("share-popup-template"); 17 | this.$sharePopupUrlInput = 18 | document.querySelector("#export_url_field")!; 19 | this.$sharePopupIframeInput = document.querySelector( 20 | "#export_iframe_field" 21 | )!; 22 | 23 | this._sharePopupCopyUrlButtonController( 24 | document.querySelector("#export_url_copy_button")! 25 | ); 26 | this._sharePopupCopyIframeController( 27 | document.querySelector("#export_iframe_copy_button")! 28 | ); 29 | 30 | return this.$root as HTMLElement; 31 | } 32 | 33 | toggle = () => { 34 | this._positionSharePopup(); 35 | const isInvisibile = this.$root!.classList.toggle("invisible"); 36 | if (!isInvisibile) { 37 | this._setExportTexts(); 38 | } 39 | }; 40 | 41 | private _setExportTexts() { 42 | this._setExportUrl(); 43 | this._setExportIframe(); 44 | } 45 | 46 | private _getWindowUrl(): string { 47 | return window.location.origin + window.location.pathname; 48 | } 49 | 50 | private _setExportUrl() { 51 | const url = `${this._getWindowUrl()}?input=${encodeURIComponent( 52 | InputModel.inputString.value 53 | )}&settings=${encodeURIComponent(SettingsModel.exportString())}`; 54 | this.$sharePopupUrlInput!.value = url; 55 | } 56 | 57 | private _setExportIframe() { 58 | const url = `${this._getWindowUrl()}?input=${encodeURIComponent( 59 | InputModel.inputString.value 60 | )}&settings=${encodeURIComponent( 61 | SettingsModel.exportString() 62 | )}&iframe=true`; 63 | this.$sharePopupIframeInput!.value = ``; 64 | } 65 | 66 | private _positionSharePopup() { 67 | const { left, top } = this.$shareButton!.getBoundingClientRect(), 68 | shareButtonWidth = this.$shareButton!.offsetWidth, 69 | popupHeight = (this.$root as HTMLElement).offsetHeight, 70 | popupWidth = (this.$root as HTMLElement).offsetWidth; 71 | 72 | this.$root!.style.left = `${ 73 | left - popupWidth / 2 + shareButtonWidth / 2 74 | }px`; 75 | this.$root!.style.top = `${top - popupHeight - 10}px`; 76 | } 77 | 78 | private _sharePopupCopyIframeController( 79 | sharePopupCopyIframeButton: HTMLButtonElement 80 | ) { 81 | sharePopupCopyIframeButton.addEventListener("click", () => { 82 | const url = this.$sharePopupIframeInput!.value; 83 | navigator.clipboard.writeText(url); 84 | this.closeSharePopup(); 85 | }); 86 | } 87 | 88 | private _sharePopupCopyUrlButtonController( 89 | sharePopupCopyUrlButton: HTMLButtonElement 90 | ) { 91 | sharePopupCopyUrlButton.addEventListener("click", () => { 92 | const url = this.$sharePopupUrlInput!.value; 93 | navigator.clipboard.writeText(url); 94 | this.closeSharePopup(); 95 | }); 96 | } 97 | 98 | closeSharePopup() { 99 | this.$root!.classList.add("invisible"); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/ui/components/webapp/WebappComponent.ts: -------------------------------------------------------------------------------- 1 | import Component from "../Component"; 2 | import ContentComponent from "./ContentComponent"; 3 | import NavbarComponent from "./NavbarComponent"; 4 | 5 | export default class WebappComponent extends Component { 6 | navbarComponent: NavbarComponent; 7 | contentComponent: ContentComponent; 8 | 9 | constructor() { 10 | super(); 11 | this.navbarComponent = new NavbarComponent(); 12 | this.contentComponent = new ContentComponent(); 13 | } 14 | 15 | protected _render(): HTMLElement | SVGElement { 16 | this.$root = document.createElement("div"); 17 | this.$root.appendChild(this.navbarComponent.html()); 18 | this.$root.appendChild(this.contentComponent.html()); 19 | return this.$root; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/CompilerModel.ts: -------------------------------------------------------------------------------- 1 | import TinyComp, { TinyCompOptions } from "tiny-comp"; 2 | import attributeGrammar from "../grammar/attributeGrammar"; 3 | import { LiveData } from "./Observable"; 4 | 5 | const compilerOptions: TinyCompOptions = { 6 | startSymbol: "GRAPH", 7 | ignoreTokensNamed: ["whitespace"], 8 | }; 9 | 10 | export default class CompilerModel { 11 | static compiler = new TinyComp(attributeGrammar, compilerOptions); 12 | static graph: LiveData = new LiveData(null); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/InputModel.ts: -------------------------------------------------------------------------------- 1 | import D3_CONFIG from "../d3/D3_CONFIG"; 2 | import { LiveData } from "./Observable"; 3 | 4 | // ====================================================== // 5 | // ===================== InputModel ===================== // 6 | // ====================================================== // 7 | 8 | // helper function to retrieve input from url // 9 | 10 | const _getInputFromUrl = (): string | null => { 11 | const url = new URL(window.location.href); 12 | return url.searchParams.get("input"); 13 | }; 14 | 15 | // ~~~~~~~~~~~~~~ the model ~~~~~~~~~~~~~~ // 16 | 17 | export default class InputModel { 18 | static inputString: LiveData = new LiveData( 19 | _getInputFromUrl() ?? D3_CONFIG.website.startInput 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/LinearAlgebra.ts: -------------------------------------------------------------------------------- 1 | import D3Node from "../d3/D3Node"; 2 | 3 | export interface Circle { 4 | position: Vector; 5 | radius: number; 6 | } 7 | 8 | export interface Vector { 9 | x: number; 10 | y: number; 11 | } 12 | 13 | export interface VectorPair { 14 | source: Vector; 15 | target: Vector; 16 | } 17 | 18 | export function makeCircleFromD3Node(d3Node: D3Node): Circle { 19 | return { 20 | position: { x: d3Node.x!!, y: d3Node.y!! }, 21 | radius: d3Node.weightToRadius(), 22 | }; 23 | } 24 | 25 | export function calcClosestPointsOfCircles( 26 | sourceCircle: Circle, 27 | targetCircle: Circle 28 | ): Vector[] { 29 | const jointVector = calcJointVector( 30 | sourceCircle.position, 31 | targetCircle.position 32 | ), 33 | jointVectorLength = calcVectorLength(jointVector); 34 | 35 | const sourceCircleRatio = sourceCircle.radius / jointVectorLength, 36 | targetCircleRatio = targetCircle.radius / jointVectorLength; 37 | 38 | const sourceCircleClosestPoint = { 39 | x: sourceCircle.position.x + jointVector.x * sourceCircleRatio, 40 | y: sourceCircle.position.y + jointVector.y * sourceCircleRatio, 41 | }; 42 | 43 | const targetCircleClosestPoint = { 44 | x: targetCircle.position.x - jointVector.x * targetCircleRatio, 45 | y: targetCircle.position.y - jointVector.y * targetCircleRatio, 46 | }; 47 | 48 | return [sourceCircleClosestPoint, targetCircleClosestPoint]; 49 | } 50 | 51 | export function calcVectorLength(vector: Vector): number { 52 | return Math.sqrt(Math.pow(vector.x, 2) + Math.pow(vector.y, 2)); 53 | } 54 | 55 | export function calcJointVector(sourceVector: Vector, targetVector: Vector) { 56 | return { 57 | x: targetVector.x - sourceVector.x, 58 | y: targetVector.y - sourceVector.y, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/Observable.ts: -------------------------------------------------------------------------------- 1 | // ====================================================== // 2 | // ======================== Event ======================= // 3 | // ====================================================== // 4 | 5 | // Modified JS-Class by Alexander Bazo 6 | export class Event { 7 | type: string; 8 | data: any; 9 | constructor(type: string, data: any) { 10 | this.type = type; // event type 11 | this.data = data; // extra data (e.g. click event data) 12 | Object.freeze(this); 13 | } 14 | } 15 | 16 | // ====================================================== // 17 | // ===================== Observable ===================== // 18 | // ====================================================== // 19 | 20 | // JS-Class by Alexander Bazo 21 | 22 | export class Observable { 23 | listener: any = {}; 24 | constructor() { 25 | this.listener = {}; 26 | } 27 | 28 | addEventListener(type: string, callback: Function) { 29 | if (this.listener[type] === undefined) { 30 | this.listener[type] = []; 31 | } 32 | this.listener[type].push(callback); 33 | } 34 | 35 | removeEventListener(type: string, callback: Function) { 36 | if (this.listener[type] !== undefined) { 37 | for (let i = 0; i < this.listener[type].length; i++) { 38 | if (this.listener[type][i] === callback) { 39 | this.listener[type].splice(i, 1); 40 | return; 41 | } 42 | } 43 | } 44 | } 45 | 46 | notifyAll(event: Event) { 47 | if (this.listener[event.type] !== undefined) { 48 | for (let i = 0; i < this.listener[event.type].length; i++) { 49 | this.listener[event.type][i](event); 50 | } 51 | } 52 | } 53 | 54 | clear() { 55 | this.listener = {}; 56 | } 57 | } 58 | 59 | export class LiveData extends Observable { 60 | static EVENT_DATA_CHANGED: string = "dataChanged"; 61 | private _value: any; 62 | constructor(data: any) { 63 | super(); 64 | this._value = data; 65 | } 66 | 67 | get value() { 68 | return this._value; 69 | } 70 | 71 | set value(data: any) { 72 | this._value = data; 73 | this.notifyAll(new Event(LiveData.EVENT_DATA_CHANGED, data)); 74 | } 75 | } 76 | 77 | // ====================================================== // 78 | // ====================== EventBus ====================== // 79 | // ====================================================== // 80 | 81 | // Usage: 82 | // 1. import EventBus from "...EventBus.js"; 83 | // 2. Send event to EventBus: EventBus.notifyAll(new Event(...)); 84 | // 3. Listen to events via: EventBus.addEventListener(EVENT_TYPE, (event) => {...}); 85 | 86 | export const D3EventBus = new Observable(); 87 | export const WebEventBus = new Observable(); 88 | -------------------------------------------------------------------------------- /src/utils/SettingsModel.ts: -------------------------------------------------------------------------------- 1 | import D3_CONFIG from "../d3/D3_CONFIG"; 2 | import { LiveData } from "./Observable"; 3 | 4 | // ====================================================== // 5 | // ==================== SettingsModel =================== // 6 | // ====================================================== // 7 | 8 | // helper function to retrieve settings from url // 9 | 10 | const _getSettingsFromUrl = (): { 11 | nodeColor: string | undefined; 12 | linkColor: string | undefined; 13 | } => { 14 | const url = new URL(window.location.href), 15 | urlSettings = url.searchParams.get("settings"), 16 | retrunSettings = { nodeColor: undefined, linkColor: undefined }; 17 | if (!urlSettings) return retrunSettings; 18 | try { 19 | // ugly but works for now 20 | const parsedData = JSON.parse(urlSettings); 21 | retrunSettings.nodeColor = parsedData.nodeColor; 22 | retrunSettings.linkColor = parsedData.linkColor; 23 | } catch (e) { 24 | console.error(e); 25 | } 26 | return retrunSettings; 27 | }; 28 | 29 | // ~~~~~~~~~~~~~~ the model ~~~~~~~~~~~~~~ // 30 | 31 | export default class SettingsModel { 32 | static nodeColor: LiveData = new LiveData( 33 | _getSettingsFromUrl().nodeColor ?? D3_CONFIG.node.fillColor 34 | ); 35 | static linkColor: LiveData = new LiveData( 36 | _getSettingsFromUrl().linkColor ?? D3_CONFIG.link.strokeColor 37 | ); 38 | 39 | static exportString(): string { 40 | const data = { 41 | nodeColor: SettingsModel.nodeColor.value, 42 | linkColor: SettingsModel.linkColor.value, 43 | }; 44 | return JSON.stringify(data); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./index.html", "./src/**/*.ts"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | }, 17 | 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | 3 | export default { 4 | base: "/Flow_Graph/", 5 | }; 6 | --------------------------------------------------------------------------------