├── .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 |
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 |
96 |
97 |
100 |
101 |
107 |
113 |
114 |
115 |
116 |
117 |
120 |
121 |
127 |
133 |
134 |
135 |
136 |
137 |
138 |
142 |
143 |
144 |
145 |
146 |
147 |
151 |
152 |
153 |
159 |
160 |
161 |
165 |
166 |
172 |
173 |
179 |
180 |
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: /,
23 | },
24 | link_direction_right: {
25 | 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 |
--------------------------------------------------------------------------------