diff --git a/.gitignore b/.gitignore
index d1b1f57..8e59901 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
dist
es
demo/demo-compiled.js
+demo/webfonts/*
node_modules
.DS_Store
diff --git a/.npmignore b/.npmignore
index eb14221..f2f25ec 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,4 +1,5 @@
demo/demo.js
+demo/webfonts/*
node_modules
src/components
src/index.js
diff --git a/demo/demo.js b/demo/demo.js
index 5ff738e..58a80d0 100644
--- a/demo/demo.js
+++ b/demo/demo.js
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import _ from 'underscore';
import url from 'url';
import { default as packageJSON } from './../package.json';
+import '@fortawesome/fontawesome-free/css/all.css';
// Loaded on index.html, defined as an external in webpack.config.demo.js
import Graph, { GraphParser } from 'react-workflow-viz';
@@ -180,7 +181,7 @@ class DemoApp extends Component {
onChange={this.handleParsingOptChange} onChangeBasicIO={this.handleChangeBasicIO} />
-
+
);
diff --git a/package-lock.json b/package-lock.json
index bd4c65f..72ca252 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@hms-dbmi-bgm/react-workflow-viz",
- "version": "0.1.11",
+ "version": "0.2.0-beta.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@hms-dbmi-bgm/react-workflow-viz",
- "version": "0.1.11",
+ "version": "0.2.0-beta.1",
"license": "MIT",
"dependencies": {
"memoize-one": "^5.1.1",
@@ -29,14 +29,17 @@
"@babel/preset-react": "^7.16.7",
"@babel/register": "^7.17.0",
"@babel/runtime": "^7.17.8",
+ "@fortawesome/fontawesome-free": ">=6.6.0",
"babel-jest": "^27.5.1",
"babel-loader": "^8.2.3",
"babel-plugin-minify-dead-code-elimination": "^0.5.1",
+ "css-loader": "^3.6.0",
"d3": ">=5.9.0",
"eslint": "^8.10.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-react": "^7.29.2",
"fancy-log": "^1.3.3",
+ "file-loader": "^4.3.0",
"gulp": "^4.0.0",
"http-server": "^0.12.1",
"jasmine": "^3.5.0",
@@ -48,7 +51,9 @@
"sass": "^1.39.0",
"source-map-support": "^0.5.19",
"string-replace-loader": "^2.3.0",
+ "style-loader": "^1.3.0",
"terser-webpack-plugin": "^4.2.3",
+ "url-loader": "^2.3.0",
"webpack": "^4.46.0"
},
"peerDependencies": {
@@ -1975,6 +1980,15 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/@fortawesome/fontawesome-free": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz",
+ "integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -4412,6 +4426,75 @@
"node": "*"
}
},
+ "node_modules/css-loader": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz",
+ "integrity": "sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "cssesc": "^3.0.0",
+ "icss-utils": "^4.1.1",
+ "loader-utils": "^1.2.3",
+ "normalize-path": "^3.0.0",
+ "postcss": "^7.0.32",
+ "postcss-modules-extract-imports": "^2.0.0",
+ "postcss-modules-local-by-default": "^3.0.2",
+ "postcss-modules-scope": "^2.2.0",
+ "postcss-modules-values": "^3.0.0",
+ "postcss-value-parser": "^4.1.0",
+ "schema-utils": "^2.7.0",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^4.0.0 || ^5.0.0"
+ }
+ },
+ "node_modules/css-loader/node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/css-loader/node_modules/loader-utils": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
+ "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/csstype": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
@@ -5987,6 +6070,48 @@
"node": "^10.12.0 || >=12.0.0"
}
},
+ "node_modules/file-loader": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.3.0.tgz",
+ "integrity": "sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA==",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "^1.2.3",
+ "schema-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ },
+ "peerDependencies": {
+ "webpack": "^4.0.0"
+ }
+ },
+ "node_modules/file-loader/node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/file-loader/node_modules/loader-utils": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
+ "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -7181,6 +7306,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/icss-utils": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz",
+ "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.14"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -9599,6 +9736,107 @@
"node": ">=0.10.0"
}
},
+ "node_modules/postcss": {
+ "version": "7.0.39",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
+ "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
+ "dev": true,
+ "dependencies": {
+ "picocolors": "^0.2.1",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ }
+ },
+ "node_modules/postcss-modules-extract-imports": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz",
+ "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz",
+ "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==",
+ "dev": true,
+ "dependencies": {
+ "icss-utils": "^4.1.1",
+ "postcss": "^7.0.32",
+ "postcss-selector-parser": "^6.0.2",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss-modules-scope": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz",
+ "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==",
+ "dev": true,
+ "dependencies": {
+ "postcss": "^7.0.6",
+ "postcss-selector-parser": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss-modules-values": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz",
+ "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==",
+ "dev": true,
+ "dependencies": {
+ "icss-utils": "^4.0.0",
+ "postcss": "^7.0.6"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true
+ },
+ "node_modules/postcss/node_modules/picocolors": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
+ "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==",
+ "dev": true
+ },
+ "node_modules/postcss/node_modules/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,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -11083,6 +11321,26 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/style-loader": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz",
+ "integrity": "sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^2.7.0"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^4.0.0 || ^5.0.0"
+ }
+ },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -11825,6 +12083,67 @@
"integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=",
"dev": true
},
+ "node_modules/url-loader": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.3.0.tgz",
+ "integrity": "sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog==",
+ "dev": true,
+ "dependencies": {
+ "loader-utils": "^1.2.3",
+ "mime": "^2.4.4",
+ "schema-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": ">= 8.9.0"
+ },
+ "peerDependencies": {
+ "file-loader": "*",
+ "webpack": "^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "file-loader": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/url-loader/node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/url-loader/node_modules/loader-utils": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
+ "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
+ "dev": true,
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/url-loader/node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "dev": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/url/node_modules/punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
@@ -14200,6 +14519,12 @@
}
}
},
+ "@fortawesome/fontawesome-free": {
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz",
+ "integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==",
+ "dev": true
+ },
"@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -16189,6 +16514,55 @@
"randomfill": "^1.0.3"
}
},
+ "css-loader": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz",
+ "integrity": "sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.3.1",
+ "cssesc": "^3.0.0",
+ "icss-utils": "^4.1.1",
+ "loader-utils": "^1.2.3",
+ "normalize-path": "^3.0.0",
+ "postcss": "^7.0.32",
+ "postcss-modules-extract-imports": "^2.0.0",
+ "postcss-modules-local-by-default": "^3.0.2",
+ "postcss-modules-scope": "^2.2.0",
+ "postcss-modules-values": "^3.0.0",
+ "postcss-value-parser": "^4.1.0",
+ "schema-utils": "^2.7.0",
+ "semver": "^6.3.0"
+ },
+ "dependencies": {
+ "json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
+ "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ }
+ }
+ }
+ },
+ "cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true
+ },
"csstype": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz",
@@ -17436,6 +17810,38 @@
"flat-cache": "^3.0.4"
}
},
+ "file-loader": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.3.0.tgz",
+ "integrity": "sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA==",
+ "dev": true,
+ "requires": {
+ "loader-utils": "^1.2.3",
+ "schema-utils": "^2.5.0"
+ },
+ "dependencies": {
+ "json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
+ "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ }
+ }
+ }
+ },
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -18382,6 +18788,15 @@
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
+ "icss-utils": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz",
+ "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==",
+ "dev": true,
+ "requires": {
+ "postcss": "^7.0.14"
+ }
+ },
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -20273,6 +20688,87 @@
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
"dev": true
},
+ "postcss": {
+ "version": "7.0.39",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
+ "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
+ "dev": true,
+ "requires": {
+ "picocolors": "^0.2.1",
+ "source-map": "^0.6.1"
+ },
+ "dependencies": {
+ "picocolors": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
+ "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==",
+ "dev": true
+ },
+ "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
+ }
+ }
+ },
+ "postcss-modules-extract-imports": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz",
+ "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==",
+ "dev": true,
+ "requires": {
+ "postcss": "^7.0.5"
+ }
+ },
+ "postcss-modules-local-by-default": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz",
+ "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==",
+ "dev": true,
+ "requires": {
+ "icss-utils": "^4.1.1",
+ "postcss": "^7.0.32",
+ "postcss-selector-parser": "^6.0.2",
+ "postcss-value-parser": "^4.1.0"
+ }
+ },
+ "postcss-modules-scope": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz",
+ "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==",
+ "dev": true,
+ "requires": {
+ "postcss": "^7.0.6",
+ "postcss-selector-parser": "^6.0.0"
+ }
+ },
+ "postcss-modules-values": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz",
+ "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==",
+ "dev": true,
+ "requires": {
+ "icss-utils": "^4.0.0",
+ "postcss": "^7.0.6"
+ }
+ },
+ "postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "requires": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ }
+ },
+ "postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true
+ },
"prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -21487,6 +21983,16 @@
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true
},
+ "style-loader": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz",
+ "integrity": "sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==",
+ "dev": true,
+ "requires": {
+ "loader-utils": "^2.0.0",
+ "schema-utils": "^2.7.0"
+ }
+ },
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -22067,6 +22573,45 @@
"integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=",
"dev": true
},
+ "url-loader": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.3.0.tgz",
+ "integrity": "sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog==",
+ "dev": true,
+ "requires": {
+ "loader-utils": "^1.2.3",
+ "mime": "^2.4.4",
+ "schema-utils": "^2.5.0"
+ },
+ "dependencies": {
+ "json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "loader-utils": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
+ "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
+ "dev": true,
+ "requires": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^1.0.1"
+ }
+ },
+ "mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "dev": true
+ }
+ }
+ },
"use": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
diff --git a/package.json b/package.json
index fa6bb62..ba24b20 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@hms-dbmi-bgm/react-workflow-viz",
- "version": "0.1.11",
+ "version": "0.2.0-beta.1",
"description": "React component for visualizing CWL-like workflows and provenance graphs.",
"main": "./dist/react-workflow-viz.min.js",
"unpkg": "./dist/react-workflow-viz.min.js",
@@ -27,42 +27,47 @@
"author": "4DN DCIC",
"license": "MIT",
"devDependencies": {
+ "@babel/cli": "^7.6.0",
"@babel/core": "^7.17.5",
"@babel/eslint-parser": "^7.5.0",
"@babel/plugin-proposal-class-properties": "^7.16.7",
+ "@babel/plugin-proposal-decorators": "^7.17.8",
+ "@babel/plugin-proposal-export-default-from": "^7.16.7",
"@babel/plugin-proposal-object-rest-spread": "^7.17.3",
+ "@babel/plugin-proposal-pipeline-operator": "^7.17.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
+ "@babel/plugin-transform-async-to-generator": "^7.16.8",
+ "@babel/plugin-transform-runtime": "^7.17.0",
+ "@babel/plugin-transform-template-literals": "^7.16.7",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/register": "^7.17.0",
"@babel/runtime": "^7.17.8",
- "@babel/plugin-transform-runtime": "^7.17.0",
- "@babel/plugin-proposal-export-default-from": "^7.16.7",
- "@babel/plugin-transform-template-literals": "^7.16.7",
- "@babel/plugin-proposal-decorators": "^7.17.8",
- "@babel/plugin-proposal-pipeline-operator": "^7.17.6",
- "@babel/plugin-transform-async-to-generator": "^7.16.8",
+ "@fortawesome/fontawesome-free": ">=6.6.0",
"babel-jest": "^27.5.1",
"babel-loader": "^8.2.3",
"babel-plugin-minify-dead-code-elimination": "^0.5.1",
- "@babel/cli": "^7.6.0",
+ "css-loader": "^3.6.0",
"d3": ">=5.9.0",
"eslint": "^8.10.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-react": "^7.29.2",
"fancy-log": "^1.3.3",
+ "file-loader": "^4.3.0",
"gulp": "^4.0.0",
"http-server": "^0.12.1",
"jasmine": "^3.5.0",
- "sass": "^1.39.0",
"plugin-error": "^1.0.1",
"prop-types": "^15.7.2",
"react": ">=16.14.0",
"react-dom": ">=16.14.0",
"react-transition-group": "^4.4.1",
+ "sass": "^1.39.0",
"source-map-support": "^0.5.19",
"string-replace-loader": "^2.3.0",
+ "style-loader": "^1.3.0",
"terser-webpack-plugin": "^4.2.3",
+ "url-loader": "^2.3.0",
"webpack": "^4.46.0"
},
"peerDependencies": {
diff --git a/src/components/Edge.js b/src/components/Edge.js
index 5639cd8..8bb30a1 100644
--- a/src/components/Edge.js
+++ b/src/components/Edge.js
@@ -8,6 +8,7 @@ import * as d3 from 'd3';
import Node from './Node';
import { traceNodePathAndRun } from './parsing-functions';
+import { roundScaled } from '../utilities';
export const pathDimensionFunctions = {
@@ -428,10 +429,16 @@ export default class Edge extends React.Component {
generatePathDimension(startPtOverride = null, endPtOverride = null, edgeVerticesOverride = null){
const {
- edgeStyle, startX, startY, endX, endY, columnWidth,
- curveRadius, columnSpacing, rowSpacing, nodeEdgeLedgeWidths,
- edge: { vertices: customEdgeVertices = null }
+ edgeStyle, startX, startY, endX, endY, curveRadius,
+ scale = 1, columnWidth: propColumnWidth, columnSpacing: propColumnSpacing, rowSpacing: propRowSpacing,
+ nodeEdgeLedgeWidths, edge: { vertices: customEdgeVertices = null }
} = this.props;
+
+ // scaling
+ const columnWidth = roundScaled(propColumnWidth, scale);
+ const columnSpacing = roundScaled(propColumnSpacing, scale);
+ const rowSpacing = roundScaled(propRowSpacing, scale);
+
const { startOffset, endOffset } = this.getPathOffsets();
const startPt = {
diff --git a/src/components/EdgesLayer.js b/src/components/EdgesLayer.js
index 8de8462..d943307 100644
--- a/src/components/EdgesLayer.js
+++ b/src/components/EdgesLayer.js
@@ -8,8 +8,7 @@ import { TransitionGroup, Transition } from 'react-transition-group';
import { path as d3Path } from 'd3';
import Edge from './Edge';
-
-
+import { roundScaled } from '../utilities';
@@ -410,10 +409,22 @@ export default class EdgesLayer extends React.PureComponent {
*/
render(){
const {
- outerHeight, innerWidth, innerMargin, width, edges: origEdges, nodes,
+ outerHeight, innerWidth, innerHeight, width, edges: origEdges, nodes,
selectedNode, isNodeDisabled, contentWidth,
- columnWidth, columnSpacing, rowSpacing, innerHeight
+ scale = 1, columnWidth: propColumnWidth, columnSpacing: propColumnSpacing, rowSpacing: propRowSpacing, innerMargin: propInnerMargin
} = this.props;
+
+ // scaling
+ const columnWidth = roundScaled(propColumnWidth, scale);
+ const columnSpacing = roundScaled(propColumnSpacing, scale);
+ const rowSpacing = roundScaled(propRowSpacing, scale);
+ const innerMargin = {
+ top: roundScaled(propInnerMargin.top, scale),
+ right: roundScaled(propInnerMargin.right, scale),
+ bottom: roundScaled(propInnerMargin.bottom, scale),
+ left: roundScaled(propInnerMargin.left, scale),
+ };
+
const {
edges,
horizontalSegments
diff --git a/src/components/Graph.js b/src/components/Graph.js
index 6d9c4d9..38f6b13 100644
--- a/src/components/Graph.js
+++ b/src/components/Graph.js
@@ -12,6 +12,8 @@ import NodesLayer from './NodesLayer';
import EdgesLayer from './EdgesLayer';
import { DefaultDetailPane } from './DefaultDetailPane';
import { DefaultNodeElement } from './Node';
+import { ScaleController, ScaleControls } from './ScaleController';
+import { requestAnimationFrame, cancelAnimationFrame, roundScaled } from '../utilities'
import { parseAnalysisSteps, parseBasicIOAnalysisSteps } from './parsing-functions';
@@ -70,7 +72,12 @@ export default class Graph extends React.Component {
'capacity' : PropTypes.string
})).isRequired,
'nodeTitle' : PropTypes.func,
- 'rowSpacingType' : PropTypes.oneOf([ 'compact', 'wide', 'stacked' ])
+ 'rowSpacingType' : PropTypes.oneOf([ 'compact', 'wide', 'stacked' ]),
+ //scale
+ 'showZoomControls': PropTypes.bool,
+ 'scale': PropTypes.number,
+ 'minScale': PropTypes.number,
+ 'maxScale': PropTypes.number
};
static defaultProps = {
@@ -100,7 +107,12 @@ export default class Graph extends React.Component {
return false;
},
'nodeClassName' : function(node){ return ''; },
- 'nodeEdgeLedgeWidths' : [3,5]
+ 'nodeEdgeLedgeWidths' : [3,5],
+ //scale
+ 'showZoomControls': true,
+ 'scale': 1,
+ 'minScale': 0.50,
+ 'maxScale': 1.50
};
static getHeightFromNodes(nodes, nodesPreSortFxn, rowSpacing){
@@ -150,7 +162,6 @@ export default class Graph extends React.Component {
columnSpacing = 56,
isNodeCurrentContext = false
){
-
/** Vertically centers a single node within a column */
function centerNode(n){
n.y = (contentHeight / 2) + innerMargin.top;
@@ -181,14 +192,14 @@ export default class Graph extends React.Component {
else {
var padding = Math.max(0, contentHeight - ((countInCol - 1) * rowSpacing)) / 2;
_.forEach(nodesInColumn, function(nodeInCol, idx){
- nodeInCol.y = ((idx + 0) * rowSpacing) + (innerMargin.top) + padding;
+ nodeInCol.y = ((idx + 0) * rowSpacing) + innerMargin.top + padding;
nodeInCol.nodesInColumn = countInCol;
});
}
} else if (rowSpacingType === 'stacked') {
_.forEach(nodesInColumn, function(nodeInCol, idx){
if (!nodeInCol) return;
- nodeInCol.y = (rowSpacing * idx) + innerMargin.top; //num + (this.props.innerMargin.top + verticalMargin);
+ nodeInCol.y = (rowSpacing * idx) + innerMargin.top;
nodeInCol.nodesInColumn = countInCol;
});
} else if (rowSpacingType === 'wide') {
@@ -223,7 +234,7 @@ export default class Graph extends React.Component {
// Set correct X coordinate on each node depending on column and spacing prop.
_.forEach(nodesWithCoords, (node, i) => {
- node.x = (node.column * (columnWidth + columnSpacing)) + leftOffset;
+ node.x = node.column * (columnWidth + columnSpacing) + leftOffset;
});
// Finally, add boolean `isCurrentContext` flag to each node object if needed.
@@ -240,8 +251,11 @@ export default class Graph extends React.Component {
super(props);
this.height = this.height.bind(this);
this.nodesWithCoordinates = this.nodesWithCoordinates.bind(this);
+ this.setScale = this.setScale.bind(this);
this.state = {
- 'mounted' : false
+ mounted: false,
+ scale: props.scale,
+ minScale: props.minScale
};
this.memoized = {
getHeightFromNodes: memoize(Graph.getHeightFromNodes),
@@ -252,34 +266,124 @@ export default class Graph extends React.Component {
componentDidMount(){
this.setState({ 'mounted' : true });
+
+ const {
+ containerWidth,
+ containerHeight,
+ minScale: propMinScale,
+ maxScale,
+ graphWidth,
+ graphHeight,
+ zoomToExtentsOnMount = true
+ } = this.props;
+
+ // if (typeof containerWidth !== "number" || typeof containerHeight !== "number") {
+ // // Maybe will become set in componentDidUpdate later.
+ // return false;
+ // }
+
+ // if (isNaN(containerWidth) || isNaN(containerHeight)) {
+ // throw new Error("Width or height is NaN.");
+ // }
+
+ // const minScaleUnbounded = Math.min(
+ // (containerWidth / graphWidth),
+ // (containerHeight / graphHeight)
+ // );
+
+ // // Decrease by 5% for scrollbars, etc.
+ // const nextMinScale = Math.floor(
+ // Math.min(1, maxScale, Math.max(propMinScale, minScaleUnbounded))
+ // * 95) / 100;
+ // const retObj = { minScale: nextMinScale, mounted: true };
+
+ // // First time that we've gotten dimensions -- set scale to fit.
+ // // Also, if nextMinScale > scale or we had scale === minScale before.
+ // // TODO: Maybe do this onMount also
+ // if (zoomToExtentsOnMount) {
+ // retObj.scale = nextMinScale;
+ // }
+ // requestAnimationFrame(() => {
+ // this.setState(retObj);
+ // });
+ }
+
+ setScale(scaleToSet, cb){
+ this.setState(function(
+ { minScale: stateMinScale },
+ { minScale: propMinScale, maxScale }
+ ){
+ const scale = Math.max(
+ Math.min(
+ maxScale,
+ scaleToSet
+ ),
+ stateMinScale || propMinScale
+ );
+ return { scale };
+ }, cb);
}
height() {
- const { nodes, nodesPreSortFxn, rowSpacing } = this.props;
+ const { nodes, nodesPreSortFxn, rowSpacing: propRowSpacing } = this.props;
+ const { scale } = this.state;
+
+ const rowSpacing = roundScaled(propRowSpacing, scale);
return this.memoized.getHeightFromNodes(nodes, nodesPreSortFxn, rowSpacing);
}
scrollableWidth(){
- const { nodes, columnWidth, columnSpacing, innerMargin } = this.props;
+ const { nodes, columnWidth: propColumnWidth, columnSpacing: propColumnSpacing, innerMargin } = this.props;
+ const { scale } = this.state;
+
+ const columnWidth = roundScaled(propColumnWidth, scale);
+ const columnSpacing = roundScaled(propColumnSpacing, scale);
return this.memoized.getScrollableWidthFromNodes(nodes, columnWidth, columnSpacing, innerMargin);
}
nodesWithCoordinates(viewportWidth, contentWidth, contentHeight){
- const { nodes, innerMargin, rowSpacingType, rowSpacing, columnWidth, columnSpacing, isNodeCurrentContext } = this.props;
+ const {
+ nodes, innerMargin,
+ rowSpacingType, rowSpacing: propRowSpacing, columnWidth: propColumnWidth, columnSpacing: propColumnSpacing,
+ isNodeCurrentContext
+ } = this.props;
+ const { scale } = this.state;
+
+ const rowSpacing = roundScaled(propRowSpacing, scale);
+ const columnWidth = roundScaled(propColumnWidth, scale);
+ const columnSpacing = roundScaled(propColumnSpacing, scale);
+
return this.memoized.getNodesWithCoordinates(
nodes, viewportWidth, contentWidth, contentHeight, innerMargin,
- rowSpacingType, rowSpacing, columnWidth, columnSpacing, isNodeCurrentContext
+ rowSpacingType, rowSpacing, columnWidth, columnSpacing,
+ isNodeCurrentContext, scale || 1
);
}
render(){
- const { width, innerMargin, edges, minimumHeight } = this.props;
- const { mounted } = this.state;
+ const {
+ width, innerMargin: propInnerMargin, edges, minimumHeight,
+ columnSpacing: propColumnSpacing, rowSpacing: propRowSpacing, columnWidth: propColumnWidth,
+ scale: propScale = 1, maxScale: propMaxScale = 1.1, minScale: propMinScale = 0.9,
+ showZoomControls
+ } = this.props;
+ const { mounted, scale: stateScale } = this.state;
+ const scale = stateScale || propScale;
const innerHeight = this.height();
const contentWidth = this.scrollableWidth();
let innerWidth = width;
- if (!mounted){
+ const columnSpacing = roundScaled(propColumnSpacing, scale);
+ const rowSpacing = roundScaled(propRowSpacing, scale);
+ const columnWidth = roundScaled(propColumnWidth, scale);
+ const innerMargin = {
+ top: roundScaled(propInnerMargin.top, scale),
+ right: roundScaled(propInnerMargin.right, scale),
+ bottom: roundScaled(propInnerMargin.bottom, scale),
+ left: roundScaled(propInnerMargin.left, scale),
+ };
+
+ if (!mounted) {
return (
@@ -302,15 +406,23 @@ export default class Graph extends React.Component {
graphHeight += (spacerCount * this.props.columnSpacing);
}
*/
-
+ let scaleControls = null;
+ if (showZoomControls && typeof this.setScale === "function") {
+ const scaleProps = { scale, minScale: this.state.minScale || propMinScale, maxScale: propMaxScale, setScale: this.setScale };
+ scaleControls =
;
+ }
+
return (
+ {scaleControls}
+ {..._.pick(this.props, 'pathArrows', 'href', 'onNodeClick', 'renderDetailPane')}>
-
-
+
+
diff --git a/src/components/Node.js b/src/components/Node.js
index 61ca5eb..93401fc 100644
--- a/src/components/Node.js
+++ b/src/components/Node.js
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import memoize from 'memoize-one';
import _ from 'underscore';
import { traceNodePathAndRun } from './parsing-functions';
+import { roundScaled } from '../utilities';
/** @todo separate methods out into functional components */
@@ -72,7 +73,7 @@ export class DefaultNodeElement extends React.PureComponent {
render(){
const { node, title, columnWidth } = this.props;
const style = node.nodeType === 'input' || node.nodeType === 'output' ?
- { width : (columnWidth || 100) }
+ { width : columnWidth || 100 }
: null;
return (
1 && lastActiveContextNode === node))
@@ -215,21 +219,21 @@ export default class Node extends React.Component {
return false;
}
- render(){
- var { node, isNodeDisabled, className, columnWidth, renderNodeElement, selectedNode, forwardedRef } = this.props,
- disabled = typeof node.disabled !== 'undefined' ? node.disabled : this.isDisabled(node, isNodeDisabled),
- isCurrentContext = typeof node.isCurrentContext !== 'undefined' ? node.isCurrentContext : null,
- classNameList = ["node", "node-type-" + node.nodeType],
- selected = (!disabled && Node.isSelected(node, selectedNode)) || false,
- related = (!disabled && this.isRelated(node, selectedNode)) || false,
- inSelectionPath = selected || (!disabled && this.isInSelectionPath(node, selectedNode)) || false;
-
- if (disabled) classNameList.push('disabled');
- if (isCurrentContext) classNameList.push('current-context');
- if (typeof className === 'function') classNameList.push(className(node));
- else if (typeof className === 'string' ) classNameList.push(className);
-
- var visibleNodeProps = _.extend(
+ render() {
+ const { node, isNodeDisabled, className, columnWidth, renderNodeElement, selectedNode, forwardedRef, scale = 1 } = this.props;
+ const disabled = typeof node.disabled !== 'undefined' ? node.disabled : this.isDisabled(node, isNodeDisabled);
+ const isCurrentContext = typeof node.isCurrentContext !== 'undefined' ? node.isCurrentContext : null;
+ const classNameList = ["node", "node-type-" + node.nodeType];
+ const selected = (!disabled && Node.isSelected(node, selectedNode)) || false;
+ const related = (!disabled && this.isRelated(node, selectedNode)) || false;
+ const inSelectionPath = selected || (!disabled && this.isInSelectionPath(node, selectedNode)) || false;
+
+ if (disabled) classNameList.push('disabled');
+ if (isCurrentContext) classNameList.push('current-context');
+ if (typeof className === 'function') classNameList.push(className(node));
+ else if (typeof className === 'string') classNameList.push(className);
+
+ const visibleNodeProps = _.extend(
_.omit(this.props, 'children', 'onMouseEnter', 'onMouseLeave', 'onClick', 'className', 'nodeElement'),
{ disabled, selected, related, isCurrentContext, inSelectionPath }
);
@@ -240,10 +244,11 @@ export default class Node extends React.Component {
data-node-selected={selected} data-node-in-selection-path={inSelectionPath}
data-node-related={related} data-node-type-detail={node.ioType && node.ioType.toLowerCase()}
data-node-column={node.column} style={{
- 'top' : node.y,
- 'left' : node.x,
- 'width' : columnWidth || 100,
- 'zIndex' : 2 + (node.indexInColumn || 0)
+ top: node.y,
+ left: node.x + ((scale - 1) * columnWidth) / 2,
+ width: columnWidth || 100,
+ zIndex: 2 + (node.indexInColumn || 0),
+ transform: `scale(${scale})`
}} ref={forwardedRef}>
diff --git a/src/components/NodesLayer.js b/src/components/NodesLayer.js
index e8cae9d..e8514d0 100644
--- a/src/components/NodesLayer.js
+++ b/src/components/NodesLayer.js
@@ -6,6 +6,7 @@ import memoize from 'memoize-one';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import Node from './Node';
+import { roundScaled } from '../utilities';
export default class NodesLayer extends React.PureComponent {
@@ -73,9 +74,17 @@ export default class NodesLayer extends React.PureComponent {
}
render(){
- var { innerMargin, innerWidth, outerHeight, contentWidth } = this.props,
- fullWidth = innerWidth + innerMargin.left + innerMargin.right,
- layerStyle = { 'width' : Math.max(contentWidth, fullWidth), 'height' : outerHeight };
+ const { innerMargin: propInnerMargin, innerWidth, outerHeight, contentWidth, scale } = this.props;
+
+ const innerMargin = {
+ top: roundScaled(propInnerMargin.top, scale),
+ right: roundScaled(propInnerMargin.right, scale),
+ bottom: roundScaled(propInnerMargin.bottom, scale),
+ left: roundScaled(propInnerMargin.left, scale),
+ };
+
+ const fullWidth = innerWidth + innerMargin.left + innerMargin.right;
+ const layerStyle = { 'width': Math.max(contentWidth, fullWidth), 'height': outerHeight };
return (
diff --git a/src/components/ScaleController.js b/src/components/ScaleController.js
new file mode 100644
index 0000000..7782b07
--- /dev/null
+++ b/src/components/ScaleController.js
@@ -0,0 +1,394 @@
+'use strict';
+
+import React from 'react';
+import { requestAnimationFrame, cancelAnimationFrame, roundScaled } from '../utilities'
+
+
+/**
+ * @deprecated Moved most of the functionality into Graph
+ */
+export class ScaleController extends React.PureComponent {
+
+ static defaultProps = {
+ minScale: 0.01,
+ maxScale: 1.25,
+ initialScale: 1,
+ zoomToExtentsOnMount: true
+ };
+
+ constructor(props){
+ super(props);
+ this.handleWheelMove = this.handleWheelMove.bind(this);
+ this.handleInnerContainerMounted = this.handleInnerContainerMounted.bind(this);
+ this.handleInnerContainerWillUnmount = this.handleInnerContainerWillUnmount.bind(this);
+ this.innerElemReference = null;
+ }
+
+ componentDidMount(){
+ // const {
+ // containerWidth,
+ // containerHeight,
+ // minScale: propMinScale,
+ // maxScale,
+ // graphWidth,
+ // graphHeight,
+ // zoomToExtentsOnMount = true
+ // } = this.props;
+
+ // if (typeof containerWidth !== "number" || typeof containerHeight !== "number") {
+ // // Maybe will become set in componentDidUpdate later.
+ // return false;
+ // }
+
+ // if (isNaN(containerWidth) || isNaN(containerHeight)) {
+ // throw new Error("Width or height is NaN.");
+ // }
+
+ // const minScaleUnbounded = Math.min(
+ // (containerWidth / graphWidth),
+ // (containerHeight / graphHeight)
+ // );
+
+ // // Decrease by 5% for scrollbars, etc.
+ // const nextMinScale = Math.floor(
+ // Math.min(1, maxScale, Math.max(propMinScale, minScaleUnbounded))
+ // * 95) / 100;
+ // const retObj = { minScale: nextMinScale };
+
+ // // First time that we've gotten dimensions -- set scale to fit.
+ // // Also, if nextMinScale > scale or we had scale === minScale before.
+ // // TODO: Maybe do this onMount also
+ // if (zoomToExtentsOnMount) {
+ // retObj.scale = nextMinScale;
+ // }
+ // requestAnimationFrame(() => {
+ // this.setState(retObj);
+ // });
+ }
+
+ componentDidUpdate(pastProps, pastState){
+ const {
+ enableMouseWheelZoom,
+ enablePinchZoom,
+ zoomToExtentsOnMount,
+ containerWidth,
+ containerHeight,
+ graphWidth,
+ graphHeight,
+ minScale: propMinScale,
+ maxScale
+ } = this.props;
+ // const { scale, minScale: stateMinScale } = this.state;
+ // const {
+ // enableMouseWheelZoom: pastWheelEnabled,
+ // enablePinchZoom: pastPinchEnabled,
+ // containerWidth: pastWidth,
+ // containerHeight: pastHeight,
+ // graphWidth: pastGraphWidth,
+ // graphHeight: pastGraphHeight
+ // } = pastProps;
+
+ // // Remove or attach listeners if needed.
+ // const listenNow = (enableMouseWheelZoom || enablePinchZoom);
+ // const listenBefore = (pastWheelEnabled || pastPinchEnabled);
+
+ // if (this.innerElemReference){
+ // if (listenNow && !listenBefore){
+ // this.innerElemReference.addEventListener("wheel", this.handleWheelMove, { "passive": false, "capture": true });
+ // } else if (!listenNow && listenBefore){
+ // this.innerElemReference.removeEventListener("wheel", this.handleWheelMove);
+ // }
+ // }
+
+ // // Update minScale (& possibly scale itself)
+ // // We read `pastState` here before updating set
+ // // vs using functional updater because want to avoid
+ // // React's state change queuing mechanisms / reading
+ // // most accurate prev value not important.
+ // if (containerWidth !== pastWidth ||
+ // containerHeight !== pastHeight ||
+ // graphWidth !== pastGraphWidth ||
+ // graphHeight !== pastGraphHeight
+ // ){
+ // const minScaleUnbounded = Math.min(
+ // (containerWidth / graphWidth),
+ // (containerHeight / graphHeight)
+ // );
+
+ // // Decrease by 5% for scrollbars, etc.
+ // const nextMinScale = Math.floor(
+ // Math.min(1, maxScale, Math.max(propMinScale, minScaleUnbounded))
+ // * 95) / 100;
+ // const retObj = { minScale: nextMinScale };
+
+ // // First time that we've gotten dimensions -- set scale to fit.
+ // // Also, if nextMinScale > scale or we had scale === minScale before.
+ // // TODO: Maybe do this onMount also
+ // if (nextMinScale > scale || stateMinScale === scale || (zoomToExtentsOnMount && (!pastHeight || !pastWidth))) {
+ // retObj.scale = nextMinScale;
+ // }
+ // requestAnimationFrame(() => {
+ // this.setState(retObj);
+ // });
+ // }
+ }
+
+
+ /**
+ * If `enableMouseWheelZoom` or `enablePinchZoom` are enabled, will
+ * zoom in response to mouse wheel events or ctrl+mousewheel & touchpad
+ * pinch events, respectively.
+ */
+ handleWheelMove(evt){
+ const { deltaY, deltaX, ctrlKey } = evt;
+ const { enableMouseWheelZoom, enablePinchZoom, scale, setScale } = this.props;
+
+ if (!enableMouseWheelZoom && !enablePinchZoom) {
+ return false;
+ }
+
+ // Chrome registers touchpad-based pinching as scrollwheel with ctrlKey.
+ if (enablePinchZoom && !enableMouseWheelZoom && !ctrlKey) {
+ return false;
+ }
+
+ if (!enablePinchZoom && enableMouseWheelZoom && ctrlKey) {
+ return false;
+ }
+
+ if (enableMouseWheelZoom && Math.abs(deltaX) > 0){
+ // Not perfect --
+ // Make sure is mousewheel and not bidirectional touchpad,
+ // for which we might still wanna allow left/right movement.
+ return false;
+ }
+
+ evt.preventDefault();
+ evt.stopPropagation();
+
+ // `ctrlKey=true` implies 2nd ctrl key being pressed -or- touchpad pinching
+ const deltaMultiplier = ctrlKey ? 0.01 : 0.0005;
+
+ // React uses own state change queuing system, which guessing
+ // gets bypassed w. raf, so below line might work better, since
+ // `state.scale` unchanged.. (vs. functional updater)
+ setScale(scale - (deltaY * deltaMultiplier));
+ }
+
+ handleInnerContainerMounted(innerElem){
+ const { onMount, enableMouseWheelZoom, enablePinchZoom } = this.props;
+ if (typeof onMount === "function"){
+ onMount(...arguments);
+ }
+ this.innerElemReference = innerElem;
+ // We need to listen to `wheel` events directly (not thru React)
+ // as react doesn't handle these (it seems / afaik).
+ if (enableMouseWheelZoom || enablePinchZoom) {
+ // Chrome & some other browsers set passive:true by default.
+ innerElem.addEventListener("wheel", this.handleWheelMove, { "passive": false, "capture": true });
+ }
+ }
+
+ handleInnerContainerWillUnmount(innerElem){
+ const { onWillUnmount } = this.props;
+ if (typeof onMount === "function"){
+ onWillUnmount(...arguments);
+ }
+ if (this.innerElemReference === null) {
+ console.error("No inner elem, exiting");
+ return;
+ }
+ if (this.innerElemReference !== innerElem) {
+ throw new Error("Inner elem is different, exiting");
+ }
+ this.innerElemReference.removeEventListener("wheel", this.handleWheelMove);
+ this.innerElemReference = null;
+ }
+
+ render(){
+ const { children, initialScale = null, scale, setScale, minScale, ...passProps } = this.props;
+ const childProps = {
+ ...passProps,
+ scale: scale || 1,
+ minScale: minScale,
+ setScale: setScale,
+ onMount: this.handleInnerContainerMounted,
+ onWillUnmount: this.handleInnerContainerWillUnmount
+ };
+ return React.Children.map(children, (child) => child && React.cloneElement(child, childProps) );
+ }
+
+}
+
+/**
+ * Component which provides UI for adjusting scale and
+ * calls `ScaleController`'s `setScale` function.
+ *
+ * Uses `requestAnimationFrame` (`raf`) for smooth and performant
+ * zooming transitions.
+ *
+ * To assert whether `raf` makes a meaningful difference, try to comment out
+ * the `raf`-related lines in `onSliderChange` method (except for `setScale(nextVal)`)
+ * and compare performance/smoothness :-D
+ *
+ * React _does_ seem to use requestAnimationFrame under the hood but maybe only
+ * for batched updates, as animation frames aren't always requested (Chrome dev
+ * tools > performance > profiling).
+ *
+ * We're getting performance gain from using `raf` in onSliderChange potentially
+ * because we're listening to `SyntheticEvent`s passed in from React element, which
+ * may be throttled or deferred until after state changes (vs native events).
+ */
+export class ScaleControls extends React.PureComponent {
+
+ static defaultProps = {
+ scaleChangeInterval: 15, // milliseconds
+ scaleChangeUpFactor: 1.025,
+ scaleChangeDownFactor: 0.975
+ };
+
+ constructor(props){
+ super(props);
+ this.onZoomOutDown = this.onZoomOutDown.bind(this);
+ this.onZoomOutUp = this.onZoomOutUp.bind(this);
+ this.onZoomInDown = this.onZoomInDown.bind(this);
+ this.onZoomInUp = this.onZoomInUp.bind(this);
+ this.cancelAnimationFrame = this.cancelAnimationFrame.bind(this);
+ this.onSliderChange = this.onSliderChange.bind(this);
+ this.state = {
+ zoomOutPressed: false,
+ zoomInPressed: false
+ };
+ this.nextAnimationFrame = null;
+ }
+
+ cancelAnimationFrame(){
+ if (this.nextAnimationFrame !== null) {
+ cancelAnimationFrame(this.nextAnimationFrame);
+ this.nextAnimationFrame = null;
+ }
+ }
+
+ onZoomOutDown(evt){
+ evt.preventDefault();
+ evt.stopPropagation();
+ const { setScale, scaleChangeInterval, scaleChangeDownFactor, scale: initScale } = this.props;
+ this.setState({ zoomOutPressed: true }, ()=>{
+ const start = Date.now();
+ //const diff = (scaleChangeDownFactor * initScale) - initScale;
+
+ const performZoom = () => {
+ const { scale, minScale } = this.props;
+ if (scale <= minScale){ // Button becomes disabled so `onZoomOutUp` is not guaranteed to be called.
+ this.setState({ zoomOutPressed: false });
+ this.nextAnimationFrame = null;
+ return;
+ }
+ setScale(
+ //initScale + (diff * Math.floor((Date.now() - start) / scaleChangeInterval))
+ initScale *
+ (scaleChangeDownFactor ** Math.floor((Date.now() - start) / scaleChangeInterval))
+ );
+ this.nextAnimationFrame = requestAnimationFrame(performZoom);
+ };
+
+ this.nextAnimationFrame = requestAnimationFrame(performZoom);
+ });
+ }
+
+ onZoomOutUp(evt){
+ evt.preventDefault();
+ evt.stopPropagation();
+ this.cancelAnimationFrame();
+ this.setState({ zoomOutPressed: false });
+ }
+
+ onZoomInDown(evt){
+ evt.preventDefault();
+ evt.stopPropagation();
+ const { setScale, scaleChangeInterval, scaleChangeUpFactor, scale: initScale } = this.props;
+ this.setState({ zoomInPressed: true }, ()=>{
+ const start = Date.now();
+ //const diff = (scaleChangeUpFactor * initScale) - initScale;
+
+ const performZoom = () => {
+ const { scale, maxScale } = this.props;
+ if (scale >= maxScale){ // Button becomes disabled so `onZoomOutUp` is not guaranteed to be called.
+ this.setState({ zoomInPressed: false });
+ this.nextAnimationFrame = null;
+ return;
+ }
+ setScale(
+ //initScale + (diff * Math.floor((Date.now() - start) / scaleChangeInterval))
+ initScale *
+ (scaleChangeUpFactor ** Math.floor((Date.now() - start) / scaleChangeInterval))
+ );
+ this.nextAnimationFrame = requestAnimationFrame(performZoom);
+ };
+
+ this.nextAnimationFrame = requestAnimationFrame(performZoom);
+ });
+ }
+
+ onZoomInUp(evt){
+ evt.preventDefault();
+ evt.stopPropagation();
+ this.cancelAnimationFrame();
+ this.setState({ zoomInPressed: false });
+ }
+
+ onSliderChange(evt){
+ evt.preventDefault();
+ evt.stopPropagation();
+ const { setScale } = this.props;
+ const nextVal = parseFloat(evt.target.value);
+ this.cancelAnimationFrame();
+ this.nextAnimationFrame = requestAnimationFrame(() => {
+ setScale(nextVal);
+ this.nextAnimationFrame = null;
+ });
+ }
+
+ render(){
+ const { scale, setScale, minScale, maxScale } = this.props;
+
+ if (typeof setScale !== "function" || typeof scale !== "number" || isNaN(scale)) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ { Math.round(scale * 100) }
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export function scaledStyle(graphHeight, graphWidth, scale){
+ return {
+ width: roundScaled(graphWidth, scale),
+ height: roundScaled(graphHeight, scale),
+ transform : "scale3d(" + scale + "," + scale + ",1)"
+ };
+}
diff --git a/src/styles.scss b/src/styles.scss
index 47ab005..cc3d179 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -536,7 +536,7 @@ $workflow-node-color-type-global-context: #ffb3b3 !default;
}
.scroll-container {
-
+ display: flex;
position: relative;
box-sizing: content-box;
transition:
@@ -624,4 +624,79 @@ $workflow-node-color-type-global-context: #ffb3b3 !default;
@include workflow-edge-layer;
}
}
+}
+
+.zoom-controls-container {
+ display: flex;
+ position: absolute;
+ z-index: 999;
+ top: 8px;
+ left: 50%;
+ transform: translateX(-50%);
+ > .zoom-buttons-row {
+ display: flex;
+ align-items: flex-start;
+ > .zoom-btn,
+ > .zoom-value {
+ box-sizing: content-box;
+ text-align: center;
+ padding: 1px 4px;
+ height: 25px;
+ color: #555;
+ background-color: #fff;
+ }
+ > .zoom-btn {
+ border: 1px solid #999;
+ width: 24px;
+
+ &.zoom-out { // Left btn
+ border-radius: 5px 0 0 5px;
+ border-right-color: #ddd !important;
+ }
+ &.zoom-in { // Right btn
+ border-radius: 0 5px 5px 0;
+ border-left-color : #ddd !important;
+ }
+
+ &:disabled {
+ color: #999;
+ pointer-events: none;
+ }
+ &:hover:not(:disabled),
+ &:focus:not(:disabled), &:active:not(:disabled) {
+ color: #000;
+ border-color: #555;
+ outline: none;
+ }
+ &:focus:not(:disabled), &:active:not(:disabled) {
+ box-shadow: inset 0px 0px 2px 0px #333;
+ }
+ }
+ > .zoom-value {
+ border-top: 1px solid #999999;
+ border-bottom: 1px solid #999999;
+ width: 36px;
+ font-size: 0.875em;
+ line-height: 25px;
+ white-space: nowrap;
+ text-align: right;
+ }
+ }
+ > .zoom-slider {
+ width: 112px;
+ padding-top: 3px;
+ padding-left: 3px;
+ > input[type="range"] {
+ width: inherit;
+ // If we want to make it vertical later:
+ //&[orient="vertical"] {
+ // writing-mode: bt-lr; /* IE */
+ // -webkit-appearance: slider-vertical; /* WebKit */
+ // height: 120px;
+ //}
+ }
+ }
+ + div.state-container {
+ margin-top: 40px;
+ }
}
\ No newline at end of file
diff --git a/src/utilities.js b/src/utilities.js
new file mode 100644
index 0000000..74ba619
--- /dev/null
+++ b/src/utilities.js
@@ -0,0 +1,36 @@
+'use strict';
+
+import React from 'react';
+
+/**
+ * Helper function for window.requestAnimationFrame. Falls back to browser-prefixed versions if default not available, or falls back to setTimeout with 0ms delay if no requestAnimationFrame available at all.
+ *
+ * @param {function} cb - Callback method.
+ * @returns {undefined|string} Undefined or timeout ID if falling back to setTimeout.
+ */
+export function requestAnimationFrame(cb){
+ if (/*!isServerSide() && */typeof window !== 'undefined'){
+ if (typeof window.requestAnimationFrame !== 'undefined') return window.requestAnimationFrame(cb);
+ if (typeof window.webkitRequestAnimationFrame !== 'undefined') return window.webkitRequestAnimationFrame(cb);
+ if (typeof window.mozRequestAnimationFrame !== 'undefined') return window.mozRequestAnimationFrame(cb);
+ }
+ return setTimeout(cb, 0); // Mock it for old browsers and server-side.
+}
+
+export function cancelAnimationFrame(identifier){
+ if (/*!isServerSide() && */typeof window !== 'undefined'){
+ if (typeof window.cancelAnimationFrame !== 'undefined') return window.cancelAnimationFrame(identifier);
+ if (typeof window.webkitCancelAnimationFrame !== 'undefined') return window.webkitCancelAnimationFrame(identifier);
+ if (typeof window.mozCancelAnimationFrame !== 'undefined') return window.mozCancelAnimationFrame(identifier);
+ }
+ return clearTimeout(identifier); // Mock it for old browsers and server-side.
+}
+
+export function roundScaled(value, scale, decimals = 2) {
+ //shortcut - useful for most cases
+ if (scale === 1) return value;
+
+ const factor = Math.pow(10, decimals);
+ const result = value * scale;
+ return Math.round(result * factor) / factor;
+}
\ No newline at end of file
diff --git a/webpack.config.demo.js b/webpack.config.demo.js
index 26b0c8a..f460e63 100644
--- a/webpack.config.demo.js
+++ b/webpack.config.demo.js
@@ -64,6 +64,22 @@ module.exports = [{
loader: 'babel-loader'
}
]
+ },
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader']
+ },
+ {
+ test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
+ use: [
+ {
+ loader: 'file-loader',
+ options: {
+ name: '[name].[ext]',
+ outputPath: 'webfonts/',
+ }
+ }
+ ]
}
]
},