diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6e7085b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,55 @@
+# Layoutr
+A graph layout web application
+
+## Installation
+
+After a git checkout, run the following to serve the application locally:
+
+```
+npm i
+npm run serve
+```
+
+## Usage
+
+* Click "UPLOAD CSV OR JSON" to upload a file of the form:
+```
+source,target
+a,b
+b,c
+c,d
+d,a
+```
+for CSV and
+```
+{
+ "nodes": [
+ { "id": "a" },
+ { "id": "b" },
+ { "id": "c" },
+ { "id": "d" }
+ ],
+ "edges": [
+ {
+ "source": "a",
+ "target": "b"
+ },
+ {
+ "source": "b",
+ "target": "c"
+ },
+ {
+ "source": "c",
+ "target": "d"
+ },
+ {
+ "source": "d",
+ "target": "a"
+ }
+ ]
+}
+```
+for JSON
+
+* Click "Start Layout".
+* Play with the controls!
diff --git a/dist/index.html b/dist/index.html
index e6c4c1d..fbfed9f 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -1,30 +1,10 @@
Layoutr
+
-
-
-
+
diff --git a/package-lock.json b/package-lock.json
index 5e59f60..dd04e61 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,6 +33,64 @@
"integrity": "sha512-Uy0PN4R5vgBUXFoJrKryf5aTk3kJ8Rv3PdlHjl6UaX+Cqp1QE0yPQ68MPXGrZOfG7gZVNDIJZYyot0B9ubXUrQ==",
"dev": true
},
+ "@vue/component-compiler-utils": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-2.6.0.tgz",
+ "integrity": "sha512-IHjxt7LsOFYc0DkTncB7OXJL7UzwOLPPQCfEUNyxL2qt+tF12THV+EO33O1G2Uk4feMSWua3iD39Itszx0f0bw==",
+ "dev": true,
+ "requires": {
+ "consolidate": "^0.15.1",
+ "hash-sum": "^1.0.2",
+ "lru-cache": "^4.1.2",
+ "merge-source-map": "^1.1.0",
+ "postcss": "^7.0.14",
+ "postcss-selector-parser": "^5.0.0",
+ "prettier": "1.16.3",
+ "source-map": "~0.6.1",
+ "vue-template-es2015-compiler": "^1.9.0"
+ },
+ "dependencies": {
+ "cssesc": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz",
+ "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==",
+ "dev": true
+ },
+ "lru-cache": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+ "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+ "dev": true,
+ "requires": {
+ "pseudomap": "^1.0.2",
+ "yallist": "^2.1.2"
+ }
+ },
+ "postcss-selector-parser": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz",
+ "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==",
+ "dev": true,
+ "requires": {
+ "cssesc": "^2.0.0",
+ "indexes-of": "^1.0.1",
+ "uniq": "^1.0.1"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "yallist": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+ "dev": true
+ }
+ }
+ },
"@webassemblyjs/ast": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
@@ -987,6 +1045,15 @@
"date-now": "^0.1.4"
}
},
+ "consolidate": {
+ "version": "0.15.1",
+ "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz",
+ "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==",
+ "dev": true,
+ "requires": {
+ "bluebird": "^3.1.1"
+ }
+ },
"constants-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
@@ -1435,6 +1502,12 @@
"integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
"dev": true
},
+ "de-indent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+ "integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=",
+ "dev": true
+ },
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -1642,6 +1715,12 @@
"minimalistic-crypto-utils": "^1.0.0"
}
},
+ "email-addresses": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.0.3.tgz",
+ "integrity": "sha512-kUlSC06PVvvjlMRpNIl3kR1NRXLEe86VQ7N0bQeaCZb2g+InShCeHQp/JvyYNTugMnRN2NvJhHlc3q12MWbbpg==",
+ "dev": true
+ },
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
@@ -1987,6 +2066,33 @@
"integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==",
"dev": true
},
+ "filename-reserved-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz",
+ "integrity": "sha1-5hz4BfDeHJhFZ9A4bcXfUO5a9+Q=",
+ "dev": true
+ },
+ "filenamify": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz",
+ "integrity": "sha1-qfL/0RxQO+0wABUCknI3jx8TZaU=",
+ "dev": true,
+ "requires": {
+ "filename-reserved-regex": "^1.0.0",
+ "strip-outer": "^1.0.0",
+ "trim-repeated": "^1.0.0"
+ }
+ },
+ "filenamify-url": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/filenamify-url/-/filenamify-url-1.0.0.tgz",
+ "integrity": "sha1-syvYExnvWGO3MHi+1Q9GpPeXX1A=",
+ "dev": true,
+ "requires": {
+ "filenamify": "^1.0.0",
+ "humanize-url": "^1.0.0"
+ }
+ },
"fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@@ -2130,6 +2236,17 @@
"readable-stream": "^2.0.0"
}
},
+ "fs-extra": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
+ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
"fs-write-stream-atomic": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
@@ -2731,6 +2848,33 @@
"integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
"dev": true
},
+ "gh-pages": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-2.0.1.tgz",
+ "integrity": "sha512-uFlk3bukljeiWKQ2XvPfjcSi/ou7IfoDf2p+Fj672saLAr8bnOdFVqI/JSgrSgInKpCg5BksxEwGUl++dbg8Dg==",
+ "dev": true,
+ "requires": {
+ "async": "^2.6.1",
+ "commander": "^2.18.0",
+ "email-addresses": "^3.0.1",
+ "filenamify-url": "^1.0.0",
+ "fs-extra": "^7.0.0",
+ "globby": "^6.1.0",
+ "graceful-fs": "^4.1.11",
+ "rimraf": "^2.6.2"
+ },
+ "dependencies": {
+ "async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
+ "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==",
+ "dev": true,
+ "requires": {
+ "lodash": "^4.17.11"
+ }
+ }
+ }
+ },
"gl-mat3": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/gl-mat3/-/gl-mat3-2.0.0.tgz",
@@ -2919,6 +3063,12 @@
"safe-buffer": "^5.0.1"
}
},
+ "hash-sum": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz",
+ "integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=",
+ "dev": true
+ },
"hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
@@ -2929,6 +3079,12 @@
"minimalistic-assert": "^1.0.1"
}
},
+ "he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true
+ },
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -3029,6 +3185,16 @@
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
"dev": true
},
+ "humanize-url": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/humanize-url/-/humanize-url-1.0.1.tgz",
+ "integrity": "sha1-9KuZ4NKIF0yk4eUEB8VfuuRk7/8=",
+ "dev": true,
+ "requires": {
+ "normalize-url": "^1.0.0",
+ "strip-url-auth": "^1.0.0"
+ }
+ },
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -3290,6 +3456,12 @@
"path-is-inside": "^1.0.2"
}
},
+ "is-plain-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+ "dev": true
+ },
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -3366,6 +3538,15 @@
"minimist": "^1.2.0"
}
},
+ "jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
"jszip": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.4.tgz",
@@ -3530,11 +3711,6 @@
"object-visit": "^1.0.0"
}
},
- "materialize-css": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/materialize-css/-/materialize-css-1.0.0.tgz",
- "integrity": "sha512-4/oecXl8y/1i8RDZvyvwAICyqwNoKU4or5uf8uoAd74k76KzZ0Llym4zhJ5lLNUskcqjO0AuMcvNyDkpz8Z6zw=="
- },
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -3579,6 +3755,23 @@
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
"dev": true
},
+ "merge-source-map": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz",
+ "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==",
+ "dev": true,
+ "requires": {
+ "source-map": "^0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ }
+ }
+ },
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -3866,6 +4059,18 @@
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
+ "normalize-url": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz",
+ "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=",
+ "dev": true,
+ "requires": {
+ "object-assign": "^4.0.1",
+ "prepend-http": "^1.0.0",
+ "query-string": "^4.1.0",
+ "sort-keys": "^1.0.0"
+ }
+ },
"nosleep.js": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.7.0.tgz",
@@ -4295,6 +4500,18 @@
"integrity": "sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ==",
"dev": true
},
+ "prepend-http": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
+ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
+ "dev": true
+ },
+ "prettier": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.3.tgz",
+ "integrity": "sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw==",
+ "dev": true
+ },
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -4338,6 +4555,12 @@
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
"dev": true
},
+ "pseudomap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+ "dev": true
+ },
"public-encrypt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
@@ -4397,6 +4620,16 @@
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"dev": true
},
+ "query-string": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
+ "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=",
+ "dev": true,
+ "requires": {
+ "object-assign": "^4.1.0",
+ "strict-uri-encode": "^1.0.0"
+ }
+ },
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
@@ -5024,6 +5257,15 @@
}
}
},
+ "sort-keys": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
+ "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
+ "dev": true,
+ "requires": {
+ "is-plain-obj": "^1.0.0"
+ }
+ },
"source-list-map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@@ -5229,6 +5471,12 @@
"integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=",
"dev": true
},
+ "strict-uri-encode": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
+ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
+ "dev": true
+ },
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
@@ -5264,6 +5512,21 @@
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
"dev": true
},
+ "strip-outer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz",
+ "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.2"
+ }
+ },
+ "strip-url-auth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/strip-url-auth/-/strip-url-auth-1.0.1.tgz",
+ "integrity": "sha1-IrD6OkE4WzO+PzMVUbu4N/oM164=",
+ "dev": true
+ },
"style-loader": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz",
@@ -5437,6 +5700,15 @@
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"dev": true
},
+ "trim-repeated": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
+ "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.2"
+ }
+ },
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
@@ -5501,6 +5773,12 @@
"imurmurhash": "^0.1.4"
}
},
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true
+ },
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -5698,6 +5976,61 @@
}
}
},
+ "vue": {
+ "version": "2.6.10",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
+ "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
+ },
+ "vue-hot-reload-api": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz",
+ "integrity": "sha512-KmvZVtmM26BQOMK1rwUZsrqxEGeKiYSZGA7SNWE6uExx8UX/cj9hq2MRV/wWC3Cq6AoeDGk57rL9YMFRel/q+g==",
+ "dev": true
+ },
+ "vue-loader": {
+ "version": "15.7.0",
+ "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.7.0.tgz",
+ "integrity": "sha512-x+NZ4RIthQOxcFclEcs8sXGEWqnZHodL2J9Vq+hUz+TDZzBaDIh1j3d9M2IUlTjtrHTZy4uMuRdTi8BGws7jLA==",
+ "dev": true,
+ "requires": {
+ "@vue/component-compiler-utils": "^2.5.1",
+ "hash-sum": "^1.0.2",
+ "loader-utils": "^1.1.0",
+ "vue-hot-reload-api": "^2.3.0",
+ "vue-style-loader": "^4.1.0"
+ }
+ },
+ "vue-style-loader": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",
+ "integrity": "sha512-0ip8ge6Gzz/Bk0iHovU9XAUQaFt/G2B61bnWa2tCcqqdgfHs1lF9xXorFbE55Gmy92okFT+8bfmySuUOu13vxQ==",
+ "dev": true,
+ "requires": {
+ "hash-sum": "^1.0.2",
+ "loader-utils": "^1.0.2"
+ }
+ },
+ "vue-template-compiler": {
+ "version": "2.6.10",
+ "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz",
+ "integrity": "sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==",
+ "dev": true,
+ "requires": {
+ "de-indent": "^1.0.2",
+ "he": "^1.1.0"
+ }
+ },
+ "vue-template-es2015-compiler": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz",
+ "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
+ "dev": true
+ },
+ "vuetify": {
+ "version": "1.5.16",
+ "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-1.5.16.tgz",
+ "integrity": "sha512-yBgOsfurKQkeS+l+rrTQZ2bFk0D9ezjHhkuVM5A/yVzcg62sY2nfYaq/H++uezBWC9WYFrp/5OmSocJQcWn9Qw=="
+ },
"watchpack": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
diff --git a/package.json b/package.json
index 107396a..95a4e26 100644
--- a/package.json
+++ b/package.json
@@ -10,18 +10,25 @@
"build": "webpack",
"watch": "webpack --watch",
"serve": "webpack-dev-server --open",
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "predeploy": "npm run build",
+ "deploy": "gh-pages -d dist"
},
"author": "Jeff Baumes (jeff.baumes@kitware.com)",
"license": "MIT",
+ "homepage": "https://kitware.github.io/layoutr",
"dependencies": {
"d3": "^5.9.7",
"geojs": "^0.19.4",
- "materialize-css": "^1.0.0"
+ "vue": "^2.6.10",
+ "vuetify": "^1.5.16"
},
"devDependencies": {
"css-loader": "^3.0.0",
+ "gh-pages": "^2.0.1",
"style-loader": "^0.23.1",
+ "vue-loader": "^15.7.0",
+ "vue-template-compiler": "^2.6.10",
"webpack": "^4.35.2",
"webpack-cli": "^3.3.5",
"webpack-dev-server": "^3.7.2",
diff --git a/scripts/matnet-to-edgelist.js b/scripts/matnet-to-edgelist.js
index 9fb746c..df7c278 100644
--- a/scripts/matnet-to-edgelist.js
+++ b/scripts/matnet-to-edgelist.js
@@ -1,5 +1,4 @@
const fs = require('fs');
-// const system = require('system');
fs.readFile(process.argv[2], 'utf-8', (err, data) => {
graph = JSON.parse(data);
diff --git a/scripts/matnet-to-json.js b/scripts/matnet-to-json.js
new file mode 100644
index 0000000..317dbc9
--- /dev/null
+++ b/scripts/matnet-to-json.js
@@ -0,0 +1,10 @@
+const fs = require('fs');
+
+fs.readFile(process.argv[2], 'utf-8', (err, data) => {
+ let graph = JSON.parse(data);
+ let out = {
+ nodes: Object.keys(graph.nodes).map(id => ({ ...graph.nodes[id], id })),
+ edges: graph.edges.map(e => ({ source: e[0], target: e[1] })),
+ }
+ console.log(JSON.stringify(out, null, 2));
+});
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..829b1c8
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,477 @@
+
+
+
+ Upload CSV or JSON
+
+ {{ nodeCount }} nodes, {{ edgeCount }} edges
+
+
+
+
+ {{ layoutRunning ? 'Stop' : 'Start' }} layout
+
+
+
+
+
+
+
+
+
+
+
+
+ Download JSON
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/force/periodicTable.js b/src/force/periodicTable.js
new file mode 100644
index 0000000..200dc94
--- /dev/null
+++ b/src/force/periodicTable.js
@@ -0,0 +1,144 @@
+const table = [
+ "H", "Hydrogen", "1.00794", 1, 1,
+ "He", "Helium", "4.002602", 18, 1,
+ "Li", "Lithium", "6.941", 1, 2,
+ "Be", "Beryllium", "9.012182", 2, 2,
+ "B", "Boron", "10.811", 13, 2,
+ "C", "Carbon", "12.0107", 14, 2,
+ "N", "Nitrogen", "14.0067", 15, 2,
+ "O", "Oxygen", "15.9994", 16, 2,
+ "F", "Fluorine", "18.9984032", 17, 2,
+ "Ne", "Neon", "20.1797", 18, 2,
+ "Na", "Sodium", "22.98976...", 1, 3,
+ "Mg", "Magnesium", "24.305", 2, 3,
+ "Al", "Aluminium", "26.9815386", 13, 3,
+ "Si", "Silicon", "28.0855", 14, 3,
+ "P", "Phosphorus", "30.973762", 15, 3,
+ "S", "Sulfur", "32.065", 16, 3,
+ "Cl", "Chlorine", "35.453", 17, 3,
+ "Ar", "Argon", "39.948", 18, 3,
+ "K", "Potassium", "39.948", 1, 4,
+ "Ca", "Calcium", "40.078", 2, 4,
+ "Sc", "Scandium", "44.955912", 3, 4,
+ "Ti", "Titanium", "47.867", 4, 4,
+ "V", "Vanadium", "50.9415", 5, 4,
+ "Cr", "Chromium", "51.9961", 6, 4,
+ "Mn", "Manganese", "54.938045", 7, 4,
+ "Fe", "Iron", "55.845", 8, 4,
+ "Co", "Cobalt", "58.933195", 9, 4,
+ "Ni", "Nickel", "58.6934", 10, 4,
+ "Cu", "Copper", "63.546", 11, 4,
+ "Zn", "Zinc", "65.38", 12, 4,
+ "Ga", "Gallium", "69.723", 13, 4,
+ "Ge", "Germanium", "72.63", 14, 4,
+ "As", "Arsenic", "74.9216", 15, 4,
+ "Se", "Selenium", "78.96", 16, 4,
+ "Br", "Bromine", "79.904", 17, 4,
+ "Kr", "Krypton", "83.798", 18, 4,
+ "Rb", "Rubidium", "85.4678", 1, 5,
+ "Sr", "Strontium", "87.62", 2, 5,
+ "Y", "Yttrium", "88.90585", 3, 5,
+ "Zr", "Zirconium", "91.224", 4, 5,
+ "Nb", "Niobium", "92.90628", 5, 5,
+ "Mo", "Molybdenum", "95.96", 6, 5,
+ "Tc", "Technetium", "(98)", 7, 5,
+ "Ru", "Ruthenium", "101.07", 8, 5,
+ "Rh", "Rhodium", "102.9055", 9, 5,
+ "Pd", "Palladium", "106.42", 10, 5,
+ "Ag", "Silver", "107.8682", 11, 5,
+ "Cd", "Cadmium", "112.411", 12, 5,
+ "In", "Indium", "114.818", 13, 5,
+ "Sn", "Tin", "118.71", 14, 5,
+ "Sb", "Antimony", "121.76", 15, 5,
+ "Te", "Tellurium", "127.6", 16, 5,
+ "I", "Iodine", "126.90447", 17, 5,
+ "Xe", "Xenon", "131.293", 18, 5,
+ "Cs", "Caesium", "132.9054", 1, 6,
+ "Ba", "Barium", "132.9054", 2, 6,
+ "La", "Lanthanum", "138.90547", 4, 9,
+ "Ce", "Cerium", "140.116", 5, 9,
+ "Pr", "Praseodymium", "140.90765", 6, 9,
+ "Nd", "Neodymium", "144.242", 7, 9,
+ "Pm", "Promethium", "(145)", 8, 9,
+ "Sm", "Samarium", "150.36", 9, 9,
+ "Eu", "Europium", "151.964", 10, 9,
+ "Gd", "Gadolinium", "157.25", 11, 9,
+ "Tb", "Terbium", "158.92535", 12, 9,
+ "Dy", "Dysprosium", "162.5", 13, 9,
+ "Ho", "Holmium", "164.93032", 14, 9,
+ "Er", "Erbium", "167.259", 15, 9,
+ "Tm", "Thulium", "168.93421", 16, 9,
+ "Yb", "Ytterbium", "173.054", 17, 9,
+ "Lu", "Lutetium", "174.9668", 18, 9,
+ "Hf", "Hafnium", "178.49", 4, 6,
+ "Ta", "Tantalum", "180.94788", 5, 6,
+ "W", "Tungsten", "183.84", 6, 6,
+ "Re", "Rhenium", "186.207", 7, 6,
+ "Os", "Osmium", "190.23", 8, 6,
+ "Ir", "Iridium", "192.217", 9, 6,
+ "Pt", "Platinum", "195.084", 10, 6,
+ "Au", "Gold", "196.966569", 11, 6,
+ "Hg", "Mercury", "200.59", 12, 6,
+ "Tl", "Thallium", "204.3833", 13, 6,
+ "Pb", "Lead", "207.2", 14, 6,
+ "Bi", "Bismuth", "208.9804", 15, 6,
+ "Po", "Polonium", "(209)", 16, 6,
+ "At", "Astatine", "(210)", 17, 6,
+ "Rn", "Radon", "(222)", 18, 6,
+ "Fr", "Francium", "(223)", 1, 7,
+ "Ra", "Radium", "(226)", 2, 7,
+ "Ac", "Actinium", "(227)", 4, 10,
+ "Th", "Thorium", "232.03806", 5, 10,
+ "Pa", "Protactinium", "231.0588", 6, 10,
+ "U", "Uranium", "238.02891", 7, 10,
+ "Np", "Neptunium", "(237)", 8, 10,
+ "Pu", "Plutonium", "(244)", 9, 10,
+ "Am", "Americium", "(243)", 10, 10,
+ "Cm", "Curium", "(247)", 11, 10,
+ "Bk", "Berkelium", "(247)", 12, 10,
+ "Cf", "Californium", "(251)", 13, 10,
+ "Es", "Einstenium", "(252)", 14, 10,
+ "Fm", "Fermium", "(257)", 15, 10,
+ "Md", "Mendelevium", "(258)", 16, 10,
+ "No", "Nobelium", "(259)", 17, 10,
+ "Lr", "Lawrencium", "(262)", 18, 10,
+ "Rf", "Rutherfordium", "(267)", 4, 7,
+ "Db", "Dubnium", "(268)", 5, 7,
+ "Sg", "Seaborgium", "(271)", 6, 7,
+ "Bh", "Bohrium", "(272)", 7, 7,
+ "Hs", "Hassium", "(270)", 8, 7,
+ "Mt", "Meitnerium", "(276)", 9, 7,
+ "Ds", "Darmstadium", "(281)", 10, 7,
+ "Rg", "Roentgenium", "(280)", 11, 7,
+ "Cn", "Copernicium", "(285)", 12, 7,
+ "Nh", "Nihonium", "(286)", 13, 7,
+ "Fl", "Flerovium", "(289)", 14, 7,
+ "Mc", "Moscovium", "(290)", 15, 7,
+ "Lv", "Livermorium", "(293)", 16, 7,
+ "Ts", "Tennessine", "(294)", 17, 7,
+ "Og", "Oganesson", "(294)", 18, 7
+];
+
+let periodicTable = {}
+table.forEach((d, i) => {
+ if (i % 5 === 0) {
+ periodicTable[d] = {
+ x: table[i + 3],
+ y: table[i + 4],
+ }
+ }
+})
+
+function periodicTableForce(alpha) {
+ const nodes = simulation.nodes();
+ let k = alpha * 1;
+ for (var i = 0, n = nodes.length, node; i < n; ++i) {
+ node = nodes[i];
+ if (periodicTable[node.id]) {
+ let tx = (periodicTable[node.id].x - 9) * 750;
+ let ty = (periodicTable[node.id].y - 6) * 750;
+ node.vx -= (node.x - tx) * k;
+ node.vy -= (node.y - ty) * k;
+ }
+ }
+}
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index b79f19e..0000000
--- a/src/index.css
+++ /dev/null
@@ -1,37 +0,0 @@
-html,body,#map{
- width: 100%;
- height: 100%;
- padding: 0;
- margin: 0;
- overflow: hidden;
-}
-#tooltip {
- margin-left: 0px;
- margin-top: -20px;
- height: 16px;
- line-height: 16px;
- padding: 2px 5px;
- background: rgba(255, 255, 255, 0.75);
- border-radius: 10px;
- border-bottom-left-radius: 0;
- border: 1px solid rgba(0, 0, 0, 0.75);
- font-size: 12px;
- color: black;
-}
-#tooltip.hidden {
- display: none;
-}
-.slider-wrapper {
- padding: 0px 24px;
- /* Need to reset line-height since we're in a side nav */
- line-height: 1.6;
-}
-.slider-wrapper .slider-label {
- line-height: 3;
-}
-.noUi-horizontal .noUi-handle, .noUi-vertical .noUi-handle {
- background-color: #2196f3;
-}
-.noUi-target.noUi-horizontal .noUi-tooltip {
- background-color: #2196f3;
-}
diff --git a/src/index.js b/src/index.js
index 9952074..3b7cd8c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,222 +1,14 @@
-import 'materialize-css/dist/css/materialize.min.css';
-import 'materialize-css/dist/js/materialize.min.js';
-import 'materialize-css/extras/noUiSlider/nouislider.css';
-import noUiSlider from 'materialize-css/extras/noUiSlider/nouislider.min.js';
-import geo from 'geojs/geo.js';
+import 'vuetify/dist/vuetify.min.css';
-import LayoutWorker from 'worker-loader!./worker.js';
-import './index.css';
+import Vue from 'vue';
+import Vuetify from 'vuetify';
+import App from './App';
-let b = 20000;
-let bounds = {
- minx: -b,
- maxx: b,
- miny: -b,
- maxy: b,
-};
-let params = geo.util.pixelCoordinateParams(
- '#map', bounds.maxx - bounds.minx, bounds.maxy - bounds.miny);
+Vue.use(Vuetify);
+Vue.config.productionTip = false;
-// the utility function assumes top left is 0, 0. Move it to minx, miny.
-params.map.maxBounds.left += bounds.minx;
-params.map.maxBounds.top += bounds.miny;
-params.map.maxBounds.right += bounds.minx;
-params.map.maxBounds.bottom += bounds.miny;
-params.map.center.x += bounds.minx;
-params.map.center.y += bounds.miny;
-
-// inflate the bounds to add a border
-const maxwh = Math.max(bounds.maxx - bounds.minx, bounds.maxy - bounds.miny);
-params.map.maxBounds.left -= maxwh * 0.1;
-params.map.maxBounds.top -= maxwh * 0.1;
-params.map.maxBounds.right += maxwh * 0.1;
-params.map.maxBounds.bottom += maxwh * 0.1;
-
-// allow zoomming in until 1 unit of space is 2^(value) bigger.
-params.map.max += 3;
-const map = geo.map(params.map);
-const layer = map.createLayer('feature', {features: ['point', 'line']});
-
-const uiLayer = map.createLayer('ui', {zIndex: 2});
-const tooltip = uiLayer.createWidget('dom', {position: {x: 0, y: 0}});
-const tooltipElem = tooltip.canvas();
-tooltipElem.setAttribute('id', 'tooltip');
-tooltipElem.classList.toggle('hidden', true);
-tooltipElem.style['pointer-events'] = 'none';
-
-map.draw();
-
-let points;
-let lines;
-let graph;
-let positions;
-let nodeMap;
-let radiusFactor = 2;
-
-var layoutWorker = new LayoutWorker();
-layoutWorker.onmessage = function(e) {
- if (e.data.type === 'graph') {
- graph = e.data.graph;
-
- nodeMap = {};
- graph.nodes.forEach((n, i) => nodeMap[n.id] = i);
- lines = layer.createFeature('line').data(graph.edges.map(e => [nodeMap[e.source], nodeMap[e.target]])).style({
- position: nodeid => graph.nodes[nodeid],
- width: 1,
- strokeColor: 'black',
- strokeOpacity: 0.01,
- });
- lines.visible(false);
- map.draw();
-
- points = layer.createFeature('point', {
- primitiveShape: 'triangle',
- style: {
- strokeColor: 'black',
- fillColor: 'grey',
- fillOpacity: 0.5,
- strokeOpacity: 0.5,
- radius: nodeid => Math.max(1, Math.pow(2, map.zoom()) * Math.sqrt(graph.nodes[nodeid].degree) * radiusFactor)
- },
- position: nodeid => graph.nodes[nodeid]
- }).data(Object.keys(graph.nodes));
-
- map.geoOn(geo.event.zoom, () => {
- points.modified().draw();
- });
-
- map.draw();
-
- points
- .geoOn(geo.event.feature.mouseon, function (evt) {
- const nodeid = evt.data, node = graph.nodes[nodeid];
- let text = node.id;
- if (text) {
- tooltip.position(evt.mouse.geo);
- tooltipElem.innerText = text;
- }
- tooltipElem.classList.toggle('hidden', !text);
- })
- .geoOn(geo.event.feature.mousemove, function (evt) {
- tooltip.position(evt.mouse.geo);
- })
- .geoOn(geo.event.feature.mouseoff, function (evt) {
- tooltipElem.classList.toggle('hidden', true);
- });
- }
- else if (e.data.type === 'positions') {
- positions = e.data.nodes;
- points.position(nodeid => positions[nodeid]);
- lines.position(nodeid => positions[nodeid]);
- map.draw();
- }
- else if (e.data.type === 'alpha') {
- alpha.noUiSlider.set(e.data.value);
- }
-}
-
-document.getElementById('toggle-start').onclick = () => {
- let mode = document.getElementById('toggle-start').innerText.toLowerCase().split(' ')[0];
- layoutWorker.postMessage({type: mode});
- if (mode === 'start') {
- alpha.setAttribute('disabled', true);
- } else {
- alpha.removeAttribute('disabled');
- }
- document.getElementById('toggle-start').innerText = (mode === 'start' ? 'Stop Layout': 'Start Layout');
-}
-
-document.getElementById('save').onclick = () => {
- const nodesWithPositions = graph.nodes.map((n, i) => ({
- ...n,
- ...positions[i],
- }));
- const saveGraph = {
- nodes: nodesWithPositions,
- edges: graph.edges,
- }
- var blob = new Blob([JSON.stringify(saveGraph, null, 2)], {
- type : "data:text/json;charset=utf-8;"
- });
- const dl = document.getElementById('download');
- dl.setAttribute("href", URL.createObjectURL(blob));
- dl.setAttribute("download", "scene.json");
- dl.click();
-}
-
-document.getElementById('init-upload-edge-list').onclick = () => {
- const upload = document.getElementById('upload-edge-list');
- upload.click();
-}
-
-document.getElementById('upload-edge-list').onchange = () => {
- var file = document.getElementById('upload-edge-list').files[0];
- if (file) {
- var reader = new FileReader();
- reader.readAsText(file, "UTF-8");
- reader.onload = function (evt) {
- layoutWorker.postMessage({type: 'loadEdgeList', text: evt.target.result});
- }
- reader.onerror = function (evt) {
- console.log('Error: ', evt);
- }
- }
-}
-
-function fixedFormat(n) {
- return {
- to: function (value) {
- return value.toFixed(n);
- },
- from: function (value) {
- return Number(value);
- },
- };
-}
-
-let theta = document.getElementById('theta');
-noUiSlider.create(theta, {
- start: 1.5,
- step: 0.1,
- range: {min: 0.5, max: 3.0},
- format: fixedFormat(1),
-});
-theta.noUiSlider.on('update', () => {
- layoutWorker.postMessage({
- type: 'theta',
- value: theta.noUiSlider.get(),
- });
-});
-
-let alpha = document.getElementById('alpha');
-noUiSlider.create(alpha, {
- start: 1.0,
- step: 0.01,
- range: {min: 0.0, max: 1.0},
- format: fixedFormat(2),
-});
-alpha.noUiSlider.on('update', () => {
- layoutWorker.postMessage({
- type: 'alpha',
- value: alpha.noUiSlider.get(),
- });
-});
-
-let radiusFactorSlider = document.getElementById('radius-factor');
-noUiSlider.create(radiusFactorSlider, {
- start: 2.0,
- step: 0.1,
- range: {min: 0.1, max: 10.0},
- format: fixedFormat(1),
-});
-radiusFactorSlider.noUiSlider.on('update', () => {
- radiusFactor = radiusFactorSlider.noUiSlider.get();
- if (points) {
- points.modified();
- map.draw();
- }
- layoutWorker.postMessage({
- type: 'radiusFactor',
- value: radiusFactorSlider.noUiSlider.get(),
- });
+new Vue({
+ el: '#app',
+ components: { App },
+ template: '',
});
diff --git a/src/scales.js b/src/scales.js
new file mode 100644
index 0000000..f01bd1e
--- /dev/null
+++ b/src/scales.js
@@ -0,0 +1,26 @@
+let d3 = require('d3/dist/d3.js');
+
+// generateScale()
+// Create a linear scaling function for a numeric data field.
+// Range will go from `min` to `max`, with invalid (non-numeric) values at `invalid`.
+// If `area` is specified, range will be scaled such that every point on average fills `area` square units.
+export function generateScale(arr, field, {area = null, min = -0.5, max = 0.5, invalid = 0.7}) {
+ const size = area ? Math.sqrt(arr.length * area) : 1;
+ const domain = d3.extent(arr, n => n[field]);
+ const scale = d3.scaleLinear().domain(domain).range([size * min, size * max]);
+ return n => {
+ const val = n[field];
+ if (!isNaN(parseFloat(val)) && isFinite(val)) {
+ return scale(val);
+ }
+ return size * invalid;
+ }
+}
+
+export function generateSizeScale(arr, field, size) {
+ if (field === 'None') {
+ return () => 250 * size;
+ }
+ const sizeScale = generateScale(arr, field, {min: 3, max: 500*500, invalid: 2});
+ return d => Math.sqrt(sizeScale(d)) * size;
+}
diff --git a/src/worker.js b/src/worker.js
index f1c8b06..a19ef5e 100644
--- a/src/worker.js
+++ b/src/worker.js
@@ -1,24 +1,31 @@
let d3 = require('d3/dist/d3.js');
+let scales = require('./scales.js');
-let radiusFactor = 2;
+let size = 1;
+let sizeField = 'degree';
+let linkStrength = 1;
+let xField = 'degree';
+let yField = 'degree';
+let radialField = 'degree';
let linkStrengthFunctions = {
- inverseMinDegree: link => 1 / Math.min(link.source.degree, link.target.degree),
- inverseSumDegree: link => 1 / (link.source.degree + link.target.degree),
- inverseSumSqrtDegree: link => 1 / (Math.sqrt(link.source.degree) + Math.sqrt(link.target.degree)),
+ inverseMinDegree: link => linkStrength / Math.min(link.source.degree, link.target.degree),
+ inverseSumDegree: link => linkStrength / (link.source.degree + link.target.degree),
+ inverseSumSqrtDegree: link => linkStrength / (Math.sqrt(link.source.degree) + Math.sqrt(link.target.degree)),
};
let linkDistanceFunctions = {
- sumSqrtDegree: link => (Math.sqrt(link.source.degree) + Math.sqrt(link.target.degree)) * radiusFactor,
+ sumSqrtDegree: link => (Math.sqrt(link.source.degree) + Math.sqrt(link.target.degree)) * size,
};
let link = d3.forceLink().id(d => d.id).distance(linkDistanceFunctions.sumSqrtDegree).strength(linkStrengthFunctions.inverseMinDegree);
let charge = d3.forceManyBody();
-let collide = d3.forceCollide().radius(d => Math.sqrt(d.degree) * radiusFactor);
+let collide = d3.forceCollide();
+let center = d3.forceCenter();
+let x = d3.forceX();
+let y = d3.forceY();
+let radial = d3.forceRadial();
let simulation = d3.forceSimulation()
- .force('link', link)
- .force('charge', charge)
- .force('collide', collide)
.alphaMin(0)
.alphaTarget(0)
.stop();
@@ -41,6 +48,7 @@ loadGraph = function(graph) {
graph.nodes.forEach(d => {
nodeMap[d.id] = d;
});
+ graph.edges = graph.edges.filter(e => nodeMap[e.source] && nodeMap[e.target]);
graph.edges.forEach(d => {
nodeMap[d.source].degree += 1;
nodeMap[d.target].degree += 1;
@@ -51,38 +59,93 @@ loadGraph = function(graph) {
.nodes(graph.nodes)
.on('tick', tick);
- postMessage({type: 'graph', graph: {
- nodes: graph.nodes.map(n => ({id: n.id, degree: n.degree, x: n.x, y: n.y})),
- edges: graph.edges,
- }});
+ postMessage({type: 'graph', graph});
+ postMessage({type: 'positions', nodes: graph.nodes.map(n => ({x: n.x, y: n.y}))});
- simulation.force('link')
- .links(graph.edges);
+ // Initialize data-dependent scales
+ collide.radius(scales.generateSizeScale(simulation.nodes(), sizeField, size));
+ x.x(scales.generateScale(simulation.nodes(), xField, {area: 1000}));
+ y.y(scales.generateScale(simulation.nodes(), yField, {area: 1000}));
+ radial.radius(scales.generateScale(
+ simulation.nodes(), radialField, {area: 1000, min: 0.5, max: 1.5, invalid: 1.6},
+ ));
+
+ let oldLink = simulation.force('link');
+ simulation.force('link', link);
+ link.links(graph.edges);
+ simulation.force('link', oldLink);
}
onmessage = function(e) {
- if (e.data.type === 'stop') {
- simulation.stop();
- }
- else if (e.data.type === 'start') {
- simulation.restart();
+ if (e.data.type === 'layout') {
+ if (e.data.value) {
+ simulation.restart();
+ } else {
+ simulation.stop();
+ }
}
else if (e.data.type === 'loadEdgeList') {
loadGraph({edges: d3.csvParse(e.data.text)});
}
+ else if (e.data.type === 'loadJSON') {
+ loadGraph(JSON.parse(e.data.text));
+ }
else if (e.data.type === 'theta') {
charge.theta(e.data.value);
}
else if (e.data.type === 'alpha') {
simulation.alpha(e.data.value);
}
- else if (e.data.type === 'radiusFactor') {
- radiusFactor = e.data.value;
- link.strength(linkStrengthFunctions.inverseMinDegree);
- collide.radius(d => Math.sqrt(d.degree) * radiusFactor);
+ else if (e.data.type === 'size') {
+ size = e.data.value;
+ link.strength(link.strength());
+ collide.radius(scales.generateSizeScale(simulation.nodes(), sizeField, size));
+ }
+ else if (e.data.type === 'sizeField') {
+ sizeField = e.data.value;
+ collide.radius(scales.generateSizeScale(simulation.nodes(), sizeField, size));
+ }
+ else if (e.data.type === 'linkStrength') {
+ simulation.force('link', e.data.value ? link : null);
+ linkStrength = e.data.value;
+ link.strength(link.strength());
+ }
+ else if (e.data.type === 'chargeStrength') {
+ simulation.force('charge', e.data.value ? charge : null);
+ charge.strength(-e.data.value);
+ }
+ else if (e.data.type === 'collideStrength') {
+ simulation.force('collide', e.data.value ? collide : null);
+ collide.strength(e.data.value);
+ }
+ else if (e.data.type === 'center') {
+ simulation.force('center', e.data.value ? center : null);
+ }
+ else if (e.data.type === 'xStrength') {
+ simulation.force('x', e.data.value ? x : null);
+ x.strength(e.data.value);
+ }
+ else if (e.data.type === 'xField') {
+ xField = e.data.value;
+ x.x(scales.generateScale(simulation.nodes(), xField, {area: 1000}));
+ }
+ else if (e.data.type === 'yStrength') {
+ simulation.force('y', e.data.value ? y : null);
+ y.strength(e.data.value);
+ }
+ else if (e.data.type === 'yField') {
+ yField = e.data.value;
+ y.y(scales.generateScale(simulation.nodes(), yField, {min: 0.5, max: -0.5, area: 1000}));
+ }
+ else if (e.data.type === 'radialStrength') {
+ simulation.force('radial', e.data.value ? radial : null);
+ radial.strength(e.data.value);
}
- else if (e.data.type === 'collide') {
- simulation.collide(e.data.enabled ? collide : null);
+ else if (e.data.type === 'radialField') {
+ radialField = e.data.value;
+ radial.radius(scales.generateScale(
+ simulation.nodes(), radialField, {area: 1000, min: 0.5, max: 1.5, invalid: 1.6},
+ ));
}
else {
throw Error(`Unknown message type '${e.data.type}'`);
diff --git a/webpack.config.js b/webpack.config.js
index c703b71..a23891e 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,4 +1,5 @@
const path = require('path');
+const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
mode: 'development',
@@ -8,14 +9,27 @@ module.exports = {
},
module: {
rules: [
+ {
+ test: /\.vue$/,
+ loader: 'vue-loader',
+ },
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
+ resolve: {
+ extensions: ['.js', '.vue'],
+ alias: {
+ 'vue$': 'vue/dist/vue.js',
+ }
+ },
output: {
path: path.resolve(__dirname, 'dist'),
- filename: 'bundle.js'
+ filename: 'bundle.js',
},
+ plugins: [
+ new VueLoaderPlugin(),
+ ],
};