• Stars
    star
    165
  • Rank 228,906 (Top 5 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created about 7 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

This is a demo project for ngraph.path

ngraph.path demo

demo

This repository is a demo for the ngraph.path library. While its main purpose is to show the capabilities of the library, below you can find some design decisions for the demo itself.

Table of contents

Data preparation

Data generated and stored in this repository comes from www.openstreetmap.org (it is made available under ODbL).

NB: The NYC graph was downloaded from http://www.dis.uniroma1.it/challenge9/download.shtml

Before this project, I didn't realize how powerful is the Open Street Map API (OSM API). Tools like http://overpass-turbo.eu/ allows you to quickly build a query and fetch any information about anything on the map. Including roads and their intersections.

For example, this query will return all cycle tracks in Seattle:

[out:json];
// Fetch the area id into variable `a`
(area["name"="Seattle"]["place"="city"])->.a;

// Fetch all ways with a highway tag equal to `cycleway`
// inside area `a`
way["highway"="cycleway"](area.a);

// And join those highways with lat/lon points:
node(w);

// print everything to the output.
out meta;

You can read more about overpass API here: http://wiki.openstreetmap.org/wiki/Overpass_API

At the end, I created different scripts to convert roads into a graph format. In this graph, each road is an edge, and each intersection is a node. The scripts are not documented and are not intended for reuse, but let me know if you need something like this in a separate package.

Storing a graph

Once data is fetched from OSM, I save the graph into a binary format. My main goal here was to compress the data as much as possible, but don't spend too much time on the algorithm.

So, I decided to save graphs into two binary files. One file for coordinates, and the other one for the edges.

The coordinates file is just a flat sequence of x, y pairs (int32, 4 bytes per coordinate). The index where a pair appears, corresponds to a node's identifier.

node id

The edges file then becomes a flat sequence of fromNodeId, toNodeId pairs.

edges

This means that node 1 has a link to 2, and 2 has a link to 3, and so on.

The required storage size for any graph with V nodes and E edges can be calculated as:

 storage_size = V * 4 * 2 +  # 4 bytes per two coordinates per node
                E * 4 * 2 =  # 4 bytes per two node ids per edge
                (V + E) * 8  # in bytes

This is not the most efficient compression algorithm, but it was very easy to implement

Note: Originally I wanted to include edge weights to the format (one more int32 record), but that made loading of the graph over mobile connection very slow.

Mobile first

I believe long gone the times, when mobile was a "nice to have" addition. I imagined, when I publish this project, majority of my visitors will read about it on a mobile device. And if they will not be able to see the demo fast - I'll lose them as much fast.

So, everything what I did was tested on mobile. I have a very fast telephone from "some kind of a fruit company", but I also wanted to be sure that it will work on Android. For these purposes, I bought one of the cheapest phones. This phone helped me discover a lot of usability and performance problems.

Async everything

The slowest part was initial loading of the website. The code to load graph looked something like this:

for (let i = 0; i < points.length; i += 2) {
    let nodeId = Math.floor(i / 2);

    let x = points[i + 0];
    let y = points[i + 1];

    // graph is an instance of https://github.com/anvaka/ngraph.graph
    graph.addNode(nodeId, { x, y })
}

This may not seem like a problem at the first glance, but when you run this code on a large graph and a not very powerful mobile device, the page becomes unresponsive, and the website appears dead.

How can we solve this? I saw some people transfer CPU intensive tasks to WebWorkers. That is a very decent approach in many cases. Unfortunately, using WebWorkers implies more coding complexity, than I wanted to allow for this demo project. We would have to think about data transfer, battery lifetime, threads synchronization, fallback alternatives etc.

So, what can we do instead? I decided to break the loop. I'd run it for a few iterations, check how much time it took us, and then schedule next chunk of work with setTimeout(). This is implemented in rafor.

Using asynchronous for loop, allowed me to constantly inform the outer world about what is going on inside:

rafor

Rendering

Now that we have a graph, it's time to show it on the screen. Obviously, we cannot use SVG to show millions of elements - that would be impossibly slow. One way to go about it would be to generate tiles, and use something like Leaflet or OpenSeadragon to render a map.

I wanted to have more control over the code (as well as learn more about WebGL), so I built a WebGL renderer from scratch. It employs a "scene graph" paradigm, where scene is constructed from primitive elements. During frame rendering, the scene graph is traversed, and nodes are given opportunity to refresh their presentation.

NOTE: The renderer is available here, but it is intentionally under-documented. I'm not planning to "release" it yet, as I'm not 100% sure I like everything about it.

Battery

battery

Initial implementation was re-rendering scene on every frame from scratch. Very quickly I realized that this makes mobile device very hot very soon, and the battery goes from 100% to 0% in a quick fashion.

This was especially painful during programming. I was working on this demo in my spare time from coffee shops, with limited access to power. So I had to either think faster or find a way to conserve energy :).

I still haven't figured out how to think faster, so I tried the latter approach. Turns out solution was simple:

Don't render scene on every single frame. Render it only when explicitly asked, or when we know for sure that the scene was changed.

This may sound too obvious now, but it wasn't before. Most WebGL tutorials suggest a simple loop:

function frame() {
    requestAnimationFrame(frame); // schedule next frame;

    renderScene(); // render current frame.
    // nothing wrong with this, but this may drain battery quickly
}

With "conservative" approach, I had to move requestAnimationFrame outside from the frame() method:

let frameToken = 0;

function renderFrame() {
    if (!frameToken) frameToken = requestAnimationFrame(frame);
}

function frame() {
    frameToken = 0;
    renderScene();
}

This approach allows anybody to schedule next frame in response to actions. For example, when user drags scene and changes transformation matrix, we can call renderFrame() to update the scene.

The frameToken de-dupes multiple calls to renderFrame().

Yes, conservative approach required a little bit more work, but at the end, battery life was amazing.

Text and lines

WebGL is not the most intuitive framework. It is notoriously hard to deal with text and "wide lines" (i.e. lines with width greater than 1px) in WebGL.

zoom-scale

As I'm still learning WebGL, I realize that it would take me long time to build a decent wide lines rendering or add text support.

On the other hand, I want wide lines and text only to show a path. A few DOM nodes should be enough...

Turns out, it was straightforward to add a new element to the scene graph, which applies transforms to SVG element. The SVG element is given transparent background and pointer-events: none; so it's completely invisible from interaction standpoint:

svg overlay

Pan and zoom

I wanted to make pan and zoom interaction similar to what you would normally expect from a website like Google Maps.

I've already implemented a pan/zoom library for SVG: anvaka/panzoom. With few changes to the code, I decoupled transform computation from transform application.

So, panzoom listens to input events (mousedown, touchstart, etc.), performs smooth transition on a transformation matrix, and forwards this matrix to a "controller". It is responsibility of the controller to apply transforms.

This is not yet documented in the panzoom library, but this is all it takes to enable pan/zoom in WebGL:

  1. Define custom transformation controller
  2. React to transformation events

Hit testing

At this point we discussed how the data is loaded, how it is rendered, and how we can move around the graph. But how do we know which point is being clicked? Where are the start and the end points of the path?

When we click on the scene, we could naively iterate over all points and find the nearest point to our click. In fact, this is a decent solution if you have a thousand points or less. In our case, with several hundred thousands points, that would be very slow.

I used a QuadTree to build an index of points. After QuadTree is created, you can query it in logarithmic time for the nearest neighbors around any coordinate. While QuadTree may sound scarry, it's not very much different from a regular binary tree. It is easy to learn, easy to build and use.

In particular, I used my own yaqt library, because it had minimal memory overhead for my data layout. There are better alternatives that you might want to try as well (for example, d3-quadtree).

The path finding

We have all pieces in place: we have the graph, we know how to render it, and we know what was clicked. Now it's time to find the shortest path:

let start = window.performance.now();

// pathfinder is an instance of https://github.com/anvaka/ngraph.path
let path = pathFinder.find(fromId, toId);

let end = window.performance.now() - start;

I was contemplating about adding asynchronous path finding, but decided to put that work off, until it is really necessary (let me know).

Developing locally

If you'd like to try this website locally:

# install dependencies
npm install

# serve with hot reload at localhost.
npm start

# build for production with minification
npm run build

# build for production and view the bundle analyzer report
npm run build --report

Thank you

Thanks for reading this! I hope you enjoyed it as much as I enjoyed creating the ngraph.path library.

Have fun!

More Repositories

1

city-roads

Visualization of all roads within any city
JavaScript
5,402
star
2

VivaGraphJS

Graph drawing library for JavaScript
JavaScript
3,646
star
3

ngraph.path

Path finding in a graph
JavaScript
2,801
star
4

atree

Just a simple Christmas tree, based on reddit story
JavaScript
2,424
star
5

panzoom

Universal pan and zoom library (DOM, SVG, Custom)
JavaScript
1,570
star
6

pm

package managers visualization
JavaScript
1,408
star
7

ngraph

Beautiful Graphs
1,360
star
8

npmgraph.an

2d visualization of npm
JavaScript
1,160
star
9

fieldplay

A vector field explorer
JavaScript
1,108
star
10

sayit

Visualization of related subreddits
JavaScript
937
star
11

vs

Visualization of Google's autocomplete
JavaScript
919
star
12

word2vec-graph

Exploring word2vec embeddings as a graph of nearest neighbors
Python
691
star
13

time

Simple Google Sheets interface to track time
JavaScript
623
star
14

graph-drawing-libraries

Trying to compare known graph drawing libraries
JavaScript
584
star
15

peak-map

Make a ridge line chart from any region on Earth
JavaScript
583
star
16

map-of-reddit

Interactive map of reddit
JavaScript
540
star
17

common-words

visualization of common words in different programming languages
JavaScript
495
star
18

ngraph.graph

Graph data structure in JavaScript
JavaScript
463
star
19

ngraph.pixel

Fast graph renderer based on low level ShaderMaterial from three.js
JavaScript
295
star
20

npmrank

npm dependencies graph metrics
JavaScript
284
star
21

streamlines

Streamlines calculator
JavaScript
275
star
22

isect

Segments intersection detection library
JavaScript
253
star
23

gauss-distribution

A fun little project to show distribution of pixels in Gauss's portrait
HTML
249
star
24

oflow

Optical flow detection in JavaScript
JavaScript
200
star
25

ghindex

Creates github index for similar repositories discovery
JavaScript
189
star
26

gazer

GitHub analysis and discovery
JavaScript
186
star
27

git-also

For a `file` in your git repository, prints other files that are most often committed together
JavaScript
185
star
28

allnpmviz3d

3d visualization of npm
JavaScript
179
star
29

map-of-github

Inspirational Mapping
Vue
157
star
30

ngraph.forcelayout

Force directed graph layout
JavaScript
146
star
31

pixchart

Turn any image into delightful splash of colors and order
JavaScript
112
star
32

city-script

Collection of scripts that can be loaded into city-roads
JavaScript
112
star
33

winvelviz

Wind visualization over time
JavaScript
100
star
34

yasiv-youtube

Graph of related videos from YouTube
JavaScript
100
star
35

query-state

Application state in query string
JavaScript
97
star
36

e-sum

Visualization of exponential sums
JavaScript
97
star
37

ngraph.forcelayout3d

Force directed graph layout in 3d
JavaScript
95
star
38

index-large-cities

A simple indexer of road networks from OSM. Data for @anvaka/city-roads
JavaScript
92
star
39

graph-start

a simple graph shell to explore ideas
JavaScript
88
star
40

greview

Books that I read and their neighborhoods
86
star
41

lsystem

A simple L-Systems explorer powered by WebGL
JavaScript
86
star
42

jsruntime

Chrome Extension to explore javascript runtime.
JavaScript
85
star
43

map-of-reddit-data

Contains scripts and data to render map of reddit
JavaScript
82
star
44

redsim

reddit discovery
JavaScript
82
star
45

w-gl

A simple WebGL renderer
TypeScript
80
star
46

pplay

Create, play and share pixels. Online WebGL shader editor.
GLSL
76
star
47

ngraph.hde

High dimensional embedding of a graph and its layout
JavaScript
76
star
48

dotparser

Parser of GraphViz dot file format
PEG.js
72
star
49

wind-lines

Streamline animation of wind data
JavaScript
63
star
50

set-vs-object

What is faster Set or Object?
JavaScript
62
star
51

three.map.control

A three.js camera that mimics 2d maps navigation with pan and zoom
JavaScript
53
star
52

ngraph.native

C++ implementation of force-based layout from ngraph
C++
49
star
53

circles

A simple spirograph toy
JavaScript
49
star
54

yaot

Yet another octree
JavaScript
48
star
55

ngraph.three

3D graph renderer powered by three.js
JavaScript
44
star
56

ngraph.generators

Graph generators
JavaScript
42
star
57

citations

Most cited papers by keyword
C++
42
star
58

rafor

requestAnimationFrame friendly async for iterator
JavaScript
41
star
59

ngraph.fabric

Fabric.js graph renderer
JavaScript
38
star
60

playground

Just a set of experiments that I want to play with, but they are too small to be in their own repository
JavaScript
35
star
61

ngraph.centrality

Module to calculate graph centrality metrics
JavaScript
33
star
62

streak

Streak tracking with Google Sheets
JavaScript
33
star
63

sayit-data

data with similar subreddits graph
JavaScript
32
star
64

ngraph.offline.layout

Performs offline layout of large graphs and saves results to the disk
JavaScript
32
star
65

cord-19

exploring research papers about coronaviruses
JavaScript
31
star
66

how-to-debug-node-js-addons

How to debug node.js addons in xcode
30
star
67

wheel

Mouse wheel event unified for all browsers
JavaScript
30
star
68

npmgraphbuilder

Builds graph of npm dependencies from npm registry
JavaScript
29
star
69

generator-n

minimalistic node package yeoman generator
JavaScript
28
star
70

allgithub

Crawling github data
JavaScript
27
star
71

tiny.xml

Tiny (1.6KB) in-browser xml parser
JavaScript
27
star
72

mars

Map of Mars
JavaScript
26
star
73

ngraph.pixi

PIXI.js graph renderer
JavaScript
26
star
74

ngraph.pagerank

PageRank calculation for ngraph.graph
JavaScript
25
star
75

noisylines

Tracking noise with streamlines
JavaScript
23
star
76

nb

Neighborhood beautification: Graph layout through message passing
JavaScript
23
star
77

ngraph.physics.simulator

Physics library for ngraph
JavaScript
23
star
78

strangeb

The strangest thing happens when you rotate Bezier control points
JavaScript
22
star
79

graph-to-vector-field

Converts a graph into vector field texture
JavaScript
20
star
80

amator

Tiny animation library
JavaScript
20
star
81

ngraph.louvain

Given a graph instance detects communities using the Louvain Method
JavaScript
20
star
82

npmgraph

Visualization of NPM dependencies
JavaScript
19
star
83

similar-cities

Visualization of cities with similar road networks
JavaScript
19
star
84

allnpm

Graph generator for entire npm registry
JavaScript
18
star
85

twitter-recommended-graph

Building a proposal for Twitter to show a map of recommended people
JavaScript
18
star
86

streaming-svg-parser

Streaming SVG/XML parser with zero dependencies
JavaScript
18
star
87

extract-osm-roads

A simple utility to fetch a city graph from OSM
JavaScript
18
star
88

quadtree.cc

A C++ implementation of quadtree
C++
17
star
89

rules-of-ml

A simple visualization of Martin Zinkevich article
JavaScript
17
star
90

ngraph.events

Events support in ngraph.*
JavaScript
17
star
91

simplegrad

Simple reverse mode automatic differentiation of scalar values in javascript
JavaScript
16
star
92

portrait

Portrait of quotes
JavaScript
16
star
93

sunburst

For a given tree builds an SVG based SunBurst diagram
JavaScript
15
star
94

local-chat

Local instance of ChatGPT for my kiddo
HTML
15
star
95

what-people-google

Visualization of what people google
JavaScript
15
star
96

vuereddit

A simple reddit client written as a vue component.
Vue
14
star
97

ngraph.fromjson

Library to load graph from simple json format
JavaScript
12
star
98

color-high

A demo of ngraph.forcelayout in 6D space
JavaScript
12
star
99

color-force-vis

Visualizing forces acting on nodes during force layout
JavaScript
11
star
100

allnpmviz.an

Visualization of entire npm
JavaScript
11
star