Browse Source

refactor client to multiple modules

master
Milan Pässler 6 months ago
parent
commit
5ab913763d
7 changed files with 269 additions and 221 deletions
  1. 4
    0
      .gitignore
  2. 1
    0
      client/index.html
  3. 173
    0
      client/src/canvas.js
  4. 18
    0
      client/src/force.js
  5. 52
    220
      client/src/index.js
  6. 19
    0
      client/src/state.js
  7. 2
    1
      client/style.css

+ 4
- 0
.gitignore View File

@@ -0,0 +1,4 @@
client/main.js
client/main.min.js
client/node_modules
server/node_modules

+ 1
- 0
client/index.html View File

@@ -22,6 +22,7 @@
<input type="text" id="name">
<input type="submit" id="go" value="Go">
</form>
<div id="details"></div>
<div class="pusher"></div>
<a id="source-link" href="https://git.pbb.lc/petabyteboy/pkgvis/">
<h2>Fork me on Gitea</h2>

+ 173
- 0
client/src/canvas.js View File

@@ -0,0 +1,173 @@
import { graph, selectedNode, setSelectedNode } from "./state";

const dpr = window.devicePixelRatio || 1;
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const view = {
offsetX: 0,
offsetY: 0,
scale: 1,
dragging: false,
clicking: false,
pinching: false,
dragStartMouseX: 0,
dragStartMouseY: 0,
dragStartOffsetX: 0,
dragStartOffsetY: 0,
pinchStartDistance: 0,
pinchStartScale: 0,
};

const x = (p) => {
return Math.floor((p + view.offsetX) * view.scale * dpr + canvas.width / 2);
};

const y = (p) => {
return Math.floor((p + view.offsetY) * view.scale * dpr + canvas.height / 2);
};

const drawGraph = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let link of graph.links) {
if (link.source === selectedNode) {
ctx.strokeStyle = "rgba(255, 50, 0, .8)";
ctx.lineWidth = 5;
} else if (link.target === selectedNode) {
ctx.strokeStyle = "rgba(0, 50, 255, .8)";
ctx.lineWidth = 5;
} else {
ctx.strokeStyle = "rgba(255, 255, 255, .2)";
ctx.lineWidth = 2;
}
ctx.beginPath();
ctx.moveTo(x(link.source.x), y(link.source.y));
ctx.lineTo(x(link.target.x), y(link.target.y));
ctx.stroke();
}
for (let node of nodesAsArray()) {
ctx.fillStyle = node === selectedNode ? "yellow" : "red";
ctx.fillRect(x(node.x), y(node.y), node.weight * view.scale * dpr, node.weight * view.scale * dpr);
ctx.fillStyle = "white";
ctx.font = `${node.weight * Math.sqrt(view.scale) / 10 * (1 + dpr) / 2}rem Sans`;
ctx.fillText(node.name, x(node.x), y(node.y));
}
window.requestAnimationFrame(drawGraph);
};

const nodesAsArray = () => {
return Object.keys(graph.nodes).map((key) => graph.nodes[key]);
};

const handleResize = () => {
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
};

const nodeAt = (xPos, yPos) => {
return nodesAsArray().filter(n => x(n.x) <= xPos && y(n.y) <= yPos
&& x(n.x + n.weight) >= xPos && y(n.y + n.weight) >= yPos)[0];
};

const handleClick = (xPos, yPos) => {
const node = nodeAt(xPos, yPos);
if (!node) {
setSelectedNode(null);
} else {
setSelectedNode(node);
}
};

const handleWheel = (evt) => {
if (view.scale < evt.deltaY / 60) {
return;
}

view.scale -= evt.deltaY / 60;
};

const pinchDistance = (touchEvent) => {
return Math.abs(touchEvent.touches[0].pageX - touchEvent.touches[1].pageX) + Math.abs(touchEvent.touches[0].pageY - touchEvent.touches[1].pageY);
};

const mouseDownHandler = (x, y, touchEvent) => {
if (touchEvent && touchEvent.touches.length >= 2) {
view.pinching = true;
view.pinchStartDistance = pinchDistance(touchEvent);
view.pinchStartScale = view.scale;
}

view.dragging = true;
view.clicking = true;
view.dragStartMouseX = x;
view.dragStartMouseY = y;
view.draggedNode = nodeAt(x, y);

if (view.draggedNode) {
view.dragStartOffsetX = view.draggedNode.x;
view.dragStartOffsetY = view.draggedNode.y;
simulation.restart();
} else {
view.dragStartOffsetX = view.offsetX;
view.dragStartOffsetY = view.offsetY;
}
};

const mouseMoveHandler = (x, y, touchEvent) => {
if (touchEvent && view.pinching) {
view.scale = view.pinchStartScale * (pinchDistance(touchEvent) / view.pinchStartDistance);
}

if (view.dragging) {
if (Math.abs(view.dragStartMouseX - x) + Math.abs(view.dragStartMouseY - y) > 10) {
view.clicking = false;
}

if (view.draggedNode) {
view.draggedNode.fx = view.dragStartOffsetX - (view.dragStartMouseX - x) / view.scale;
view.draggedNode.fy = view.dragStartOffsetY - (view.dragStartMouseY - y) / view.scale;
simulation.alpha(0.5);
} else {
view.offsetX = view.dragStartOffsetX - (view.dragStartMouseX - x) / view.scale;
view.offsetY = view.dragStartOffsetY - (view.dragStartMouseY - y) / view.scale;
}
}
};

const mouseUpHandler = (x, y, touchEvent) => {
if (touchEvent) {
view.dragging = touchEvent.touches.length >= 1;
view.pinching = touchEvent.touches.length >= 2;
if (view.dragging) {
return;
}
}

view.dragging = false;
if (view.draggedNode) {
view.draggedNode.fx = undefined;
view.draggedNode.fy = undefined;
view.draggedNode = undefined;
}

if (view.clicking) {
handleClick(x, y);
}
};

/* initialization */

if ("ontouchstart" in document.documentElement) {
canvas.addEventListener("touchstart", (evt) => mouseDownHandler(evt.touches[0].pageX, evt.touches[0].pageY, evt));
window.addEventListener("touchmove", (evt) => mouseMoveHandler(evt.touches[0].pageX, evt.touches[0].pageY, evt));
window.addEventListener("touchend", (evt) => mouseUpHandler(evt.changedTouches[0].pageX * dpr, evt.changedTouches[0].pageY * dpr, evt));
} else {
canvas.addEventListener("mousedown", (evt) => mouseDownHandler(evt.pageX, evt.pageY));
window.addEventListener("mousemove", (evt) => mouseMoveHandler(evt.pageX, evt.pageY));
window.addEventListener("mouseup", (evt) => mouseUpHandler(evt.pageX, evt.pageY));
}

window.addEventListener("wheel", handleWheel);
window.addEventListener("resize", handleResize);

handleResize();
drawGraph();

+ 18
- 0
client/src/force.js View File

@@ -0,0 +1,18 @@
import { graph, nodesAsArray } from "./state";
import * as d3Force from "d3-force";

let simulation = d3Force.forceSimulation();
let linkForce = d3Force.forceLink([]).id(d => String(d.id));

export const updateGraph = () => {
simulation.nodes(nodesAsArray());
linkForce.links(graph.links);
simulation.alpha(0.5);
simulation.restart();
};

simulation
.force("link", linkForce)
.force("charge", d3Force.forceManyBody())
.force("center", d3Force.forceCenter());


+ 52
- 220
client/src/index.js View File

@@ -1,79 +1,58 @@
import * as d3Force from 'd3-force';
import { graph, selectedNode, setGraph, setSelectedNode } from "./state";
import { updateGraph } from "./force";
import "./canvas";

const apiUri = "/api";
const backendSelect = document.getElementById("backend");
let currentRequestNumber = 0;

const dpr = window.devicePixelRatio || 1;

const view = {
offsetX: 0,
offsetY: 0,
scale: 1,
dragging: false,
clicking: false,
pinching: false,
dragStartMouseX: 0,
dragStartMouseY: 0,
dragStartOffsetX: 0,
dragStartOffsetY: 0,
pinchStartDistance: 0,
pinchStartScale: 0,
};

let graph = {
nodes: {},
links: []
};
const go = async (backend, name) => {
document.querySelector('#backend [value="' + backend + '"]').selected = true;

let currentRequestNumber = 0;
document.getElementById("name").blur();
document.getElementById("name").value = name;

let simulation = d3Force.forceSimulation();
let linkForce = d3Force.forceLink([]).id(d => String(d.id));
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

function x(p) {
return Math.floor((p + view.offsetX) * view.scale * dpr + canvas.width / 2);
}

function y(p) {
return Math.floor((p + view.offsetY) * view.scale * dpr + canvas.height / 2);
}

function drawGraph() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let link of graph.links) {
if (link.source === view.selectedNode) {
ctx.strokeStyle = "rgba(255, 50, 0, .8)";
ctx.lineWidth = 5;
} else if (link.target === view.selectedNode) {
ctx.strokeStyle = "rgba(0, 50, 255, .8)";
ctx.lineWidth = 5;
} else {
ctx.strokeStyle = "rgba(255, 255, 255, .2)";
ctx.lineWidth = 2;
}
ctx.beginPath();
ctx.moveTo(x(link.source.x), y(link.source.y));
ctx.lineTo(x(link.target.x), y(link.target.y));
ctx.stroke();
document.getElementById("loading").classList.remove("hidden");
currentRequestNumber++;
const thisRequest = currentRequestNumber;
let newGraph;
try {
newGraph = await fetch(`${apiUri}/${backend}/${name}`)
.then(resp => resp.json());
} catch(err) {
console.log(err)
document.getElementById("loading").classList.add("hidden");
return;
}
for (let node of nodesAsArray()) {
ctx.fillStyle = node === view.selectedNode ? "yellow" : "red";
ctx.fillRect(x(node.x), y(node.y), node.weight * view.scale * dpr, node.weight * view.scale * dpr);
ctx.fillStyle = "white";
ctx.font = `${node.weight * Math.sqrt(view.scale) / 10 * (1 + dpr) / 2}rem Sans`;
ctx.fillText(node.name, x(node.x), y(node.y));
if (currentRequestNumber !== thisRequest) return;
setGraph(newGraph);
document.getElementById("loading").classList.add("hidden");

for (let id of Object.keys(graph.nodes)) {
graph.nodes[id].weight = Math.min(50, 5 + (graph.nodes[id].size / 5000000) || 0);
}
window.requestAnimationFrame(drawGraph);
}

function nodesAsArray() {
return Object.keys(graph.nodes).map((key) => graph.nodes[key]);
}
updateGraph();
setSelectedNode(nodesAsArray().filter(n => n.name === name)[0]);
};

const handleSubmit = (evt) => {
evt.preventDefault();
const backend = backendSelect[backendSelect.selectedIndex].value;
const name = document.getElementById("name").value;
window.location.hash = `#${backend}:${name}`;
return true;
};

async function init() {
const handleHashChange = () => {
const hash = window.location.hash.substr(1).split(":");
if (hash.length < 2) return;
const backend = hash[0];
const name = hash[1];
go(backend, name);
};

const backendSelect = document.getElementById("backend");
const fetchBackendList = async () => {
const backends = await fetch(`${apiUri}/backends`)
.then(resp => resp.json());

@@ -85,167 +64,20 @@ async function init() {
}

document.querySelector("option:first-child").selected = true;
};

simulation
.force("link", linkForce)
.force("charge", d3Force.forceManyBody())
.force("center", d3Force.forceCenter());

async function go(backend, name) {
document.querySelector('#backend [value="' + backend + '"]').selected = true;
document.getElementById("name").blur();
document.getElementById("name").value = name;

graph = {
nodes: {},
links: [],
};
simulation.nodes(nodesAsArray());
linkForce.links(graph.links);

document.getElementById("loading").classList.remove("hidden");
currentRequestNumber++;
const thisRequest = currentRequestNumber;
const newGraph = await fetch(`${apiUri}/${backend}/${name}`)
.then(resp => resp.json());
if (currentRequestNumber !== thisRequest) return;
graph = newGraph;
document.getElementById("loading").classList.add("hidden");

for (let id of Object.keys(graph.nodes)) {
graph.nodes[id].weight = Math.min(50, 5 + (graph.nodes[id].size / 5000000) || 0);
}

simulation.nodes(nodesAsArray());
linkForce.links(graph.links);
simulation.alpha(0.5);
simulation.restart();

view.selectedNode = nodesAsArray().filter(n => n.name === name);
}
/* initialization */

document.getElementById("inputs").addEventListener("submit", function(event) {
event.preventDefault();
const backendSelect = document.getElementById("backend");
const backend = backendSelect[backendSelect.selectedIndex].value;
const name = document.getElementById("name").value;
window.location.hash = `#${backend}:${name}`;
return true;
});

drawGraph();

function goLocationHash() {
const hash = window.location.hash.substr(1).split(":");
if (hash.length < 2) return;
const backend = hash[0];
const name = hash[1];
go(backend, name);
}
fetchBackendList().then(() => {
document.getElementById("loading").classList.add("hidden");

if (window.location.hash.length) {
goLocationHash();
handleHashChange();
} else {
document.getElementById("name").focus();
}

window.addEventListener("hashchange", goLocationHash);
}

function handleResize() {
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
}

function nodeAt(xPos, yPos) {
return nodesAsArray().filter(n => x(n.x) <= xPos && y(n.y) <= yPos
&& x(n.x + n.weight) >= xPos && y(n.y + n.weight) >= yPos)[0];
}

function handleClick(xPos, yPos) {
const node = nodeAt(xPos, yPos);
if (!node)
view.selectedNode = undefined;
else
view.selectedNode = node;
}

window.addEventListener('wheel', function (evt) {
if (view.scale < evt.deltaY / 60)
return;
view.scale -= evt.deltaY / 60;
});

function pinchDistance(touchEvent) {
return Math.abs(touchEvent.touches[0].pageX - touchEvent.touches[1].pageX) + Math.abs(touchEvent.touches[0].pageY - touchEvent.touches[1].pageY);
}
document.getElementById("inputs").addEventListener("submit", handleSubmit);
window.addEventListener("hashchange", handleHashChange);

function mouseDownHandler(x, y, touchEvent) {
if (touchEvent && touchEvent.touches.length >= 2) {
view.pinching = true;
view.pinchStartDistance = pinchDistance(touchEvent);
view.pinchStartScale = view.scale;
}
view.dragging = true;
view.clicking = true;
view.dragStartMouseX = x;
view.dragStartMouseY = y;
view.draggedNode = nodeAt(x, y);
if (view.draggedNode) {
view.dragStartOffsetX = view.draggedNode.x;
view.dragStartOffsetY = view.draggedNode.y;
simulation.restart();
} else {
view.dragStartOffsetX = view.offsetX;
view.dragStartOffsetY = view.offsetY;
}
}

function mouseMoveHandler(x, y, touchEvent) {
if (touchEvent && view.pinching) {
view.scale = view.pinchStartScale * (pinchDistance(touchEvent) / view.pinchStartDistance);
}
if (view.dragging) {
if (Math.abs(view.dragStartMouseX - x) + Math.abs(view.dragStartMouseY - y) > 10)
view.clicking = false;
if (view.draggedNode) {
view.draggedNode.fx = view.dragStartOffsetX - (view.dragStartMouseX - x) / view.scale;
view.draggedNode.fy = view.dragStartOffsetY - (view.dragStartMouseY - y) / view.scale;
simulation.alpha(0.5);
} else {
view.offsetX = view.dragStartOffsetX - (view.dragStartMouseX - x) / view.scale;
view.offsetY = view.dragStartOffsetY - (view.dragStartMouseY - y) / view.scale;
}
}
}

function mouseUpHandler(x, y, touchEvent) {
if (touchEvent) {
view.dragging = touchEvent.touches.length >= 1;
view.pinching = touchEvent.touches.length >= 2;
if (view.dragging)
return;
}
view.dragging = false;
if (view.draggedNode) {
view.draggedNode.fx = undefined;
view.draggedNode.fy = undefined;
view.draggedNode = undefined;
}
if (view.clicking)
handleClick(x, y);
}

if ('ontouchstart' in document.documentElement) {
canvas.addEventListener("touchstart", (evt) => mouseDownHandler(evt.touches[0].pageX, evt.touches[0].pageY, evt));
window.addEventListener("touchmove", (evt) => mouseMoveHandler(evt.touches[0].pageX, evt.touches[0].pageY, evt));
window.addEventListener("touchend", (evt) => mouseUpHandler(evt.changedTouches[0].pageX * dpr, evt.changedTouches[0].pageY * dpr, evt));
} else {
canvas.addEventListener("mousedown", (evt) => mouseDownHandler(evt.pageX, evt.pageY));
window.addEventListener("mousemove", (evt) => mouseMoveHandler(evt.pageX, evt.pageY));
window.addEventListener("mouseup", (evt) => mouseUpHandler(evt.pageX, evt.pageY));
}

window.addEventListener("resize", handleResize);
handleResize();
init();

+ 19
- 0
client/src/state.js View File

@@ -0,0 +1,19 @@
export let graph = {
nodes: {},
links: []
};

export let selectedNode;

export const setGraph = (val) => {
graph = val;
};

export const setSelectedNode = (val) => {
selectedNode = val;
};

export const nodesAsArray = () => {
return Object.keys(graph.nodes).map((key) => graph.nodes[key]);
};


+ 2
- 1
client/style.css View File

@@ -38,7 +38,7 @@ canvas {
position: fixed;
top: 0;
left: 0;
background-color: rgba(255, 255, 255, 0.2);
background-color: rgba(255, 255, 255, 0.4);
height: 100vh;
padding: 15px;
padding-top: 75px;
@@ -100,6 +100,7 @@ canvas {
display: flex;
align-items: center;
justify-content: center;
background-color: black;
}

@keyframes spin {

Loading…
Cancel
Save