This document describes how the current application is structured.
EULER is a single-page app built with vanilla ES modules. It combines:
- text and visual graph editing
- Sigma.js and Graphology for rendering and interaction
- a small pub/sub store in
js/core/state.js - local persistence for saved graphs
- separate mobile and desktop UI behavior
The app is bundled with Vite for development and production builds, but the runtime code is plain browser JavaScript.
js/
main.js app bootstrap and teardown
core/
state.js central state store
algorithm.js Euler + Chinese Postman logic
storage.js saved graphs and examples
graph/
model.js in-memory graph model
sigma-adapter.js Sigma.js integration layer
sigma-facade.js grouped adapter API
sigma-controller.js parsing, orchestration, rendering flow
ui/
init.js UI bootstrap, notifications, mobile content modes
saved-graphs.js saved/example graph panel
visual-editor.js visual editing mode
components.js reusable UI state-bound helpers
desktop-ui.js desktop-only controls and graph info strip
desktop-resize.js resizable desktop split layout
euler-toggles.js directed/weighted toggles
loader.js calculation loader
utils/
dom.js tiny DOM helpers
events.js tracked event listeners
validation.js graph input validation
canvas-gesture.js mobile gesture controller
The application boots from js/main.js:
- Wait for
DOMContentLoaded. - Start the page loader and orbital welcome sequence.
- Register orbital subscriptions on
ui.calculationStartedandui.savedGraphLoaded. - Initialize Sigma first with
initializeSigmaGraph('#cy'). - Initialize the UI with
initializeUI(). - Enable desktop resize behavior with
initDesktopResize(). - Register cleanup handlers and expose
window.cleanupEULER.
The Sigma-before-UI ordering still matters because UI actions assume graph listeners already exist.
js/core/state.js owns a single mutable state object and exposes:
getState(path)setState(path, value)subscribe(path, callback)
Important characteristics:
getState()deep-clones via JSON serialization.setState()creates missing nested objects on demand.- listeners fire for the exact path and all parent paths.
- there is no reducer layer or immutability enforcement beyond cloning on reads.
Current top-level state:
{
graph: {
directed: false,
weighted: false,
edges: []
},
ui: {
sidebarExpanded: true,
activeTab: 'input',
explanationVisible: false,
editorMode: 'text',
selectedNodes: [],
selectedEdges: [],
propertyEditing: { ... },
savedGraphs: { ... }
},
animation: {
inProgress: false,
currentStep: 0,
path: null
},
euler: {
hasPath: false,
isCircuit: false,
path: null,
explanation: '',
isChinesePostman: false,
totalWeight: 0,
duplicatedEdges: []
}
}Transient paths such as ui.contentMode, ui.calculationStarted, ui.savedGraphLoaded, graph.pendingDirected, and graph.pendingWeighted are also written dynamically.
The main graph-processing flow lives in js/graph/sigma-controller.js:
- Read the edge input from
#edges. - Parse and validate the input with
parseEdgeInput()and the validation helpers. - Trigger the welcome-to-graph animation with
setState('ui.calculationStarted', true). - Build/update the in-memory
Graphmodel. - Persist the normalized edges into
state.graph.edges. - Render the graph through the Sigma facade.
- Build an adjacency list with
buildAdjacencyList(). - Run
findEulerPath(...), which switches tochinesePostmanAlgorithm(...)when the graph is weighted. - Store the result under
state.eulerand reveal the results UI.
js/core/algorithm.js provides:
findEulerPath(adjacencyList, startVertex, isWeighted)chinesePostmanAlgorithm(adjacencyList, startVertex)buildAdjacencyList(edges, isDirected)getExplanation(result)
Behavior that matters today:
- empty graphs return a non-throwing "Graph is empty" result
- unweighted graphs use Hierholzer's algorithm
- unweighted graphs reject disconnected edge-bearing graphs
- weighted graphs route through the Chinese Postman implementation
- the Chinese Postman branch uses greedy matching plus shortest-path duplication, not an exact minimum perfect matching solver
Rendering is split across these files:
js/graph/model.js: project graph modeljs/graph/sigma-adapter.js: Sigma/Graphology lifecycle, layout, selection, export, path highlightingjs/graph/sigma-facade.js: grouped API used by the controllerjs/graph/sigma-controller.js: app-facing graph orchestration
Important current facts:
sigma-adapter.jsis still the largest and most coupled graph file.- The controller now imports the facade, not the raw adapter, for most operations.
- Selection state is mirrored into app state via
ui.selectedNodes,ui.selectedEdges, andui.propertyEditing. - The cleanup path should always call
destroySigma()to release listeners, intervals, and renderer resources.
js/ui/init.js is the top-level UI entry point. It coordinates:
- mobile content modes and canvas gestures
- section navigation
- graph mode controls
- saved graphs initialization
- Euler property toggles
- visual editor setup
- notification display
Related modules:
js/ui/saved-graphs.js: save/load/rename/delete/import/export flowsjs/ui/visual-editor.js: text/visual editor synchronizationjs/ui/desktop-ui.js: desktop graph controls and section helpersjs/ui/desktop-resize.js: draggable desktop splitjs/ui/loader.js: staged loading overlay
The app has two distinct interaction models:
- Desktop: side-by-side content and graph, plus resize handle and desktop controls.
- Mobile: a sliding content layer driven by
js/utils/canvas-gesture.jswithnormal,split, andretractedmodes.
js/core/storage.js stores user graphs in localStorage.
Keys:
euler_saved_graphseuler_graph_counter
It also provides the built-in example list and the random Euler graph generator.
The app loads css/bundle.css directly from index.html. The unbundled CSS files in css/ remain the editable source layout for major style areas such as base tokens, layout, graph controls, notifications, saved graphs, and orbital animation.
- shared design tokens live in
css/base.css - the interface uses dark backgrounds with orange accents
- many controls intentionally use square corners and uppercase labels
- desktop and mobile layouts diverge heavily, so layout changes should be checked in both modes
Current commands from package.json:
npm testnpm run test:watchnpm run test:coveragenpm run test:e2enpm run test:e2e:uinpm run devnpm run buildnpm run previewnpm run serve
Current automated test files:
js/core/algorithm.test.jsjs/core/storage.test.jsjs/graph/sigma-controller.test.jstests/e2e/euler-app.spec.js
Practical verification flow:
- run
npm testfor logic and parsing changes - run
npm run test:e2efor user-facing workflow changes when feasible - run
npm run buildbefore shipping - for a quick manual smoke test, enter
[a,b],[b,c],[c,a], run calculate, and confirm both the results panel and graph rendering update