Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

web: Animate in async WebWorker, not sync "live & visible" on load #1028

Merged
merged 4 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions tools/version/version.bash
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ set -euo pipefail

# Inspired e.g. by https://github.com/palantir/gradle-git-version

# This is a PITA (!) during development, because every change and commit on //web/
# triggers a full rebuild of //java/. We therefore now only run this on CI:
if [ -z "${CI:-""}" ]; then
exit 0
fi

# It's *VERY* important that this script *ONLY* touches the tools/version/VERSION file
# when its content actually changed. This is otherwise it triggers a frequent full rebuild.
# This is because Bazel (also?) looks at the timestamp of the file to determine if it needs
# when its content actually changed. Otherwise it triggers a frequent full rebuild. This is
# because Bazel (also?) looks at the timestamp of the file to determine if it needs
# to rebuild, not ([only?] a hash of) its content.

NEW_VERSION=$(git describe --tags --always --first-parent)
Expand Down
11 changes: 4 additions & 7 deletions web/ToDo.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
OR make `../enola server` send either (better) `Cache-Control: no-cache` & `ETag: "abcdef1234"`
(or just `Cache-Control: no-store`; or `max-age=0`, really same?)
1. Fix CORS ?
1. Is it OK to have a `package.json` e.g. for the Prettier version at the root, and have another one in `web/package.json`?

## Functional

* Play more with https://graphology.github.io/standard-library/layout-forceatlas2.html#settings
* Try out https://graphology.github.io/standard-library/layout-noverlap
* Do coloring using https://graphology.github.io/standard-library/communities-louvain; see https://gemini.google.com/app/4e3c639fc5213673

Expand All @@ -40,14 +40,11 @@
* Highlight when hovering over label as well, not just dot
* Let users drag nodes around
* Hover over Nodes should highlight all its edges
* Click on node should open Enola details page on the right
* Dark Modus support

## Visual
* Merge with //java/dev/enola/web/resources/static/main.css and show green NavBar
* Click on node should open Enola details page on the right

* Introduce a CSS so that controls are in a single row, with a nicer font
* `<div id="container">` should fill entire available space
* There shouldn't be any space around the graph
* Dark Modus support! ;-)

## Technical

Expand Down
3 changes: 3 additions & 0 deletions web/bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@types/jest": "^29.5.14",
"@types/sigmajs": "^1.0.32",
"graphology-types": "^0.24.8",
"prettier": "^3.4.2",
"typescript": "^5.7.3",
},
},
Expand Down Expand Up @@ -328,6 +329,8 @@

"pirates": ["[email protected]", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="],

"prettier": ["[email protected]", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],

"pretty-format": ["[email protected]", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],

"react-is": ["[email protected]", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
Expand Down
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@types/jest": "^29.5.14",
"@types/sigmajs": "^1.0.32",
"graphology-types": "^0.24.8",
"prettier": "^3.4.2",
"typescript": "^5.7.3"
}
}
52 changes: 52 additions & 0 deletions web/public/demo/greeting3.gexf
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<gexf xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://gexf.net/1.3"
xsi:schemaLocation="http://gexf.net/1.3 http://gexf.net/1.3/gexf.xsd" version="1.3">
<meta>
<creator>Enola.dev</creator>
</meta>
<graph defaultedgetype="directed" mode="dynamic" timeformat="dateTime"
timerepresentation="interval">
<nodes>
<node
id="file:/home/vorburger/git/github.com/enola-dev/enola/docs/models/example.org/greeting3.ttl"
label="greeting3.ttl" start="2024-07-21T23:35:14.803718995Z">
<parents>
<parent for="https://enola.dev/files/File" />
<parent
for="file:/home/vorburger/git/github.com/enola-dev/enola/docs/models/example.org/greeting3.ttl" />
</parents>
</node>
<node id="https://example.org/Salutation" label="👋ex:Salutation">
<parents>
<parent for="http://www.w3.org/2000/01/rdf-schema#Class" />
<parent
for="file:/home/vorburger/git/github.com/enola-dev/enola/docs/models/example.org/greeting3.ttl" />
</parents>
</node>
<node id="https://example.org/greeting3" label="👋ex:greeting3">
<parents>
<parent for="https://example.org/Salutation" />
<parent
for="file:/home/vorburger/git/github.com/enola-dev/enola/docs/models/example.org/greeting3.ttl" />
</parents>
</node>
</nodes>
<edges>
<edge
source="file:/home/vorburger/git/github.com/enola-dev/enola/docs/models/example.org/greeting3.ttl"
target="file:/home/vorburger/git/github.com/enola-dev/enola/docs/models/example.org/greeting3.ttl"
kind="https://enola.dev/origin" label="enola:origin" />
<edge source="https://example.org/Salutation"
target="http://www.w3.org/2000/01/rdf-schema#Class"
kind="http://www.w3.org/1999/02/22-rdf-syntax-ns#type" label="rdf:type" />
<edge source="https://example.org/Salutation"
target="file:/home/vorburger/git/github.com/enola-dev/enola/docs/models/example.org/greeting3.ttl"
kind="https://enola.dev/origin" label="enola:origin" />
<edge source="https://example.org/greeting3" target="https://example.org/Salutation"
kind="http://www.w3.org/1999/02/22-rdf-syntax-ns#type" label="rdf:type" />
<edge source="https://example.org/greeting3"
target="file:/home/vorburger/git/github.com/enola-dev/enola/docs/models/example.org/greeting3.ttl"
kind="https://enola.dev/origin" label="enola:origin" />
</edges>
</graph>
</gexf>
47 changes: 47 additions & 0 deletions web/public/enola.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
body {
background: lightgrey;
height: 100vh;
margin: 0;
}

#container {
width: 100%;
height: 100vh;
background: white;
z-index: 0;
}

#buttons {
position: absolute;
right: 1em;
top: 2em;
display: flex;
z-index: 1;
}
button,
.button {
display: inline-flex;
align-items: center;
padding: 0.3em 0.3em;
margin: 0 0.5em;
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
}
.button label {
margin-right: 0.5em;
}
.button input[type="range"] {
width: 7em;
}
button:hover,
.button:hover {
background-color: #e0e0e0;
}
#buttons > button img {
height: 2em;
}
#buttons > button:last-child {
margin-right: 0;
}
23 changes: 14 additions & 9 deletions web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,24 @@
<head>
<meta charset="UTF-8" />
<title>Network</title>
<link rel="stylesheet" href="enola.css" />
</head>

<body style="background: lightgrey">
<div id="controls">
<div class="input"><label for="zoom-in">Zoom in</label><button id="zoom-in">+</button></div>
<div class="input"><label for="zoom-out">Zoom out</label><button id="zoom-out">-</button></div>
<div class="input"><label for="zoom-reset">Reset zoom</label><button id="zoom-reset">⊙</button></div>
<div class="input">
<label for="labels-threshold">Labels threshold</label>
<input id="labels-threshold" type="range" min="0" max="15" step="0.5" />
<body>
<div id="graph">
<div id="buttons">
<div class="button">
<label for="labels-threshold">Labels</label>
<input id="labels-threshold" type="range" min="0" max="15" step="0.5" />
</div>

<button type="button" id="zoom-in">Zoom&nbsp;<b>+</b></button>
<button type="button" id="zoom-out">Zoom&nbsp;<b>-</b></button>
<button type="button" id="zoom-reset">Zoom ⊙</button>
</div>

<div id="container"></div>
</div>
<div id="container" style="width: 100%; height: 600px; background: white"></div>
<script src="../src/browser/index.ts" type="module"></script>
</body>
</html>
52 changes: 21 additions & 31 deletions web/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,49 +19,40 @@
import { DirectedGraph } from "graphology"
import { parse } from "graphology-gexf/browser"
import { random } from "graphology-layout"
import {
default as forceAtlas2,
ForceAtlas2SynchronousLayoutParameters,
inferSettings,
} from "graphology-layout-forceatlas2"
// TODO import FA2LayoutSupervisor from "graphology-layout-forceatlas2/worker.js"
import { inferSettings } from "graphology-layout-forceatlas2"
import FA2Layout from "graphology-layout-forceatlas2/worker"
import { Sigma } from "sigma"

// TODO Move this to an util.ts file
function getElementByIdOrFail(id: string): HTMLElement {
const element = document.getElementById(id)
if (!element) {
throw new Error(`HTML Element with ID '${id}' not found.`)
}
return element
}
import { getElementByIdOrFail } from "./util"

// TODO Handle 404 - display e.g. an "🙅🏽‍♀️" in the DIV container DOM
// TODO Replace hard-coded demo with ?q= read from the URL e.g. for "/gexf?q=enola:/inline"
fetch("/demo/picasso.gexf")
fetch("/demo/greeting3.gexf")
.then(res => res.text())
.then(gexf => {
// Parse GEXF string:
// TODO Remove addMissingNodes once GexfGenerator adds them itself
const graph: DirectedGraph = parse(DirectedGraph, gexf, { addMissingNodes: true })

// https://graphology.github.io/standard-library/layout-forceatlas2.html:
// "Each node’s starting position must be set before running ForceAtlas 2 layout"
// "Each node’s starting position must be set before running ForceAtlas 2 layout."
// https://www.npmjs.com/package/graphology-layout-forceatlas2#pre-requisites:
// "(...) edge-case where the layout cannot be computed if all of your nodes starts with x=0 and y=0."
random.assign(graph)

// Configure ForceAtlas2 layout settings
// TODO Review and adjust the default settings...
// TODO Adjust iterations for desired layout quality/performance...
// https://graphology.github.io/standard-library/layout-forceatlas2.html#settings
const fa2Settings = inferSettings(graph) as ForceAtlas2SynchronousLayoutParameters
fa2Settings.iterations = 500
// TODO const layout = new FA2LayoutSupervisor(graph, fa2Settings)
if (fa2Settings.settings) {
fa2Settings.settings.gravity = 1
// ? fa2Settings.settings.scalingRatio = 2
// ? fa2Settings.settings.strongGravityMode = false
}
forceAtlas2.assign(graph, fa2Settings)
const fa2Settings = inferSettings(graph)
fa2Settings.adjustSizes = true // TODO Add Node Sizes to Graph
// NOT, because unstable: fa2Settings.barnesHutOptimize = true
fa2Settings.gravity = 1
fa2Settings.scalingRatio = 1
fa2Settings.strongGravityMode = true

const fa2Layout = new FA2Layout(graph, { settings: fa2Settings })
fa2Layout.start()
// TODO UI Buttons to stop() and re-start() the layout
// TODO layout.kill() when element is removed from DOM - but how do we know when to do that?!

// Retrieve some useful DOM elements
const container = getElementByIdOrFail("container")
Expand All @@ -88,12 +79,11 @@ fetch("/demo/picasso.gexf")
void camera.animatedReset({ duration: 600 })
})

// Bind labels threshold to range input
const initialLabelsThreshold = 0
labelsThresholdRange.value = initialLabelsThreshold.toString()
renderer.setSetting("labelRenderedSizeThreshold", initialLabelsThreshold)
labelsThresholdRange.addEventListener("input", () => {
renderer.setSetting("labelRenderedSizeThreshold", +labelsThresholdRange.value)
})

// Set proper range initial value:
labelsThresholdRange.value = renderer.getSetting("labelRenderedSizeThreshold").toString()
})
.catch((error: Error) => console.error(error))
25 changes: 25 additions & 0 deletions web/src/browser/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2025 The Enola <https://enola.dev> Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export function getElementByIdOrFail(id: string): HTMLElement {
const element = document.getElementById(id)
if (!element) {
throw new Error(`HTML Element with ID '${id}' not found.`)
}
return element
}
Loading