mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-15 20:57:56 +08:00
* mocks for redux refactor - for discussion * more API design on redux refactor * add new reuqired dependencies for build * change babel target to use modern browser * remove dead code * remove dead code - joy plots * checkpoint on redux refactoring * checkpoint on redux refactoring * fix mistaken rebase conflict resolution * dead code removal; add name to dataframe backmap * rename dataframe to universe * update eslint config to more closely match prettier * lint * more eslint updates to match prettier * additional config to make eslint match prettier * add expression data to Universe/World * remove obsolete reducers * lint fixes * more eslint cleanup * lint * lint * fix but in countAllOnes when dimensions gt 1 * lint; do not display name metadata field * lint; colors refactor * lint; colors refactor * update comments * first cut at regraph and reset * enable object-curly-braces consistent mode * lint, handle regraph with no selection * fix expression scatterplot bugs * fix regression legend display * add expression data cache * remove console logging * reset cell color on regraph/reset * remove obsolete server URLs * rename UniverseV01 to Universe_REST_API_v01 * add additional comments on the varDataCache * merge universe reducer into controls reducer; simplify initialization-related actions * use spread operator * fix erroneous comment * convert universe and world state to plain objects, and functionalize supporting code (remove ES6 classes) * use spread operator * lint * improve variable names * rename obsCrossfilter to crossfilter and obsDimensionMap to dimensionMap * rename controls2 to controls
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
module.exports = {
|
||||
babelrc: false,
|
||||
cacheDirectory: true,
|
||||
presets: [["env", { loose: true, modules: false }], "stage-0", "react"],
|
||||
presets: [
|
||||
["modern-browsers", { loose: true, modules: false }],
|
||||
"stage-0",
|
||||
"react"
|
||||
],
|
||||
plugins: [
|
||||
"react-hot-loader/babel",
|
||||
"babel-plugin-transform-decorators-legacy"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
module.exports = {
|
||||
babelrc: false,
|
||||
presets: [["env", { loose: true, modules: false }], "stage-0", "react"],
|
||||
presets: [
|
||||
["modern-browsers", { loose: true, modules: false }],
|
||||
"stage-0",
|
||||
"react"
|
||||
],
|
||||
plugins: [
|
||||
"babel-plugin-transform-react-constant-elements",
|
||||
"babel-plugin-transform-decorators-legacy",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
module.exports = {
|
||||
babelrc: false,
|
||||
presets: [
|
||||
"babel-preset-env",
|
||||
"babel-preset-stage-0",
|
||||
"babel-preset-react"
|
||||
["modern-browsers", { loose: true, modules: false }],
|
||||
"stage-0",
|
||||
"react"
|
||||
],
|
||||
plugins: ["istanbul"]
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ module.exports = {
|
||||
env: { browser: true, commonjs: true, es6: true },
|
||||
globals: { expect: true },
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
ecmaVersion: 2017,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
@@ -17,6 +17,19 @@ module.exports = {
|
||||
"func-style": "off",
|
||||
"arrow-parens": "off",
|
||||
"no-use-before-define": "off",
|
||||
"react/jsx-filename-extension": "off"
|
||||
"react/jsx-filename-extension": "off",
|
||||
"comma-dangle": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
quotes: ["error", "double"],
|
||||
"implicit-arrow-linebreak": "off",
|
||||
"operator-linebreak": [
|
||||
"error",
|
||||
"after",
|
||||
{ overrides: { "?": "before", ":": "before" } }
|
||||
],
|
||||
"no-console": "off",
|
||||
"spaced-comment": ["error", "always", { exceptions: ["*"] }],
|
||||
"no-param-reassign": "off",
|
||||
"object-curly-newline": ["error", { consistent: true }]
|
||||
}
|
||||
};
|
||||
|
||||
359
client/package-lock.json
generated
359
client/package-lock.json
generated
@@ -13,6 +13,181 @@
|
||||
"@babel/highlight": "7.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"@babel/core": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-8VZqKdLMUBfvSDq+V8CWjVBh7y+b2FY+4daFAWN0pgrdgw/UfrEy8afe9CVfppwblROZZVCxGWSSGOBo84rQjg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "7.0.0-rc.2",
|
||||
"@babel/generator": "7.0.0-rc.2",
|
||||
"@babel/helpers": "7.0.0-rc.2",
|
||||
"@babel/parser": "7.0.0-rc.2",
|
||||
"@babel/template": "7.0.0-rc.2",
|
||||
"@babel/traverse": "7.0.0-rc.2",
|
||||
"@babel/types": "7.0.0-rc.2",
|
||||
"convert-source-map": "^1.1.0",
|
||||
"debug": "^3.1.0",
|
||||
"json5": "^0.5.0",
|
||||
"lodash": "^4.17.10",
|
||||
"resolve": "^1.3.2",
|
||||
"semver": "^5.4.1",
|
||||
"source-map": "^0.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-+cVix+HBNakVp7IU1WReJV8dnJl/yaBA5JRXc758BSrvJCH2hKp1Z0xHIiUaOvxMwKXc3EXGIYhlnx5T+6ofGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/highlight": "7.0.0-rc.2"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-kD6hlprDaBy17V8qd9uXJbYC5ZYyCggieT+tiGzCwayA7oyT5ynPec3MNkWQHkLyhB7IP2n3c/Ep329jOPQY/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "7.0.0-rc.2",
|
||||
"jsesc": "^2.5.1",
|
||||
"lodash": "^4.17.10",
|
||||
"source-map": "^0.5.0",
|
||||
"trim-right": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@babel/helper-function-name": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-1frd4Bm/8yfZoAj87tmB6gtQNWtKAzfRzjASVdmsItzq9X13yUlyFLdo6/tNhazftwJO8iIZeadOpi3rNKDXhg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-get-function-arity": "7.0.0-rc.2",
|
||||
"@babel/template": "7.0.0-rc.2",
|
||||
"@babel/types": "7.0.0-rc.2"
|
||||
}
|
||||
},
|
||||
"@babel/helper-get-function-arity": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-5tjNc0hYngGqBGdjvzN89p92WY6aCntaDv8AadB/xgyUx4VievZwEbz8pc6GKkO6+qfghfZhv1F3+9SC6IA3Eg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "7.0.0-rc.2"
|
||||
}
|
||||
},
|
||||
"@babel/helper-split-export-declaration": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-MBtzTAeZT7MxWETY0JRh5yyIKY4tN/q68BU4/XgzZUaHJ+G74fJUoR7mPO3TbTiwLIEFVBbZQA9AG4yYqe5W2g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "7.0.0-rc.2"
|
||||
}
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-96V6XHAh9XHzjmucShCP8tULwXsC446doZ6REaLVdZDPNj3NsWbsC7OBeY+u6UWiFxHTTv6YmA4Veh4wXuucYw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.0.0",
|
||||
"esutils": "^2.0.2",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-zDB1QPgQWYwuJty3Ymbx1hq7zbBEbZjTprHOhforvzyQFV86LNh6FS0InjnOUXM6p6QUyONz8KTt/v+MRMd0Hg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-CryGZ01Nko2/g8gkYiiPc7x9ZinrX59/BTWMZV1sDj5cAeia64vhyNnXTcNeim885IdGOdYyia1PNBWKnFxuSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "7.0.0-rc.2",
|
||||
"@babel/parser": "7.0.0-rc.2",
|
||||
"@babel/types": "7.0.0-rc.2"
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-x8y9E+KZHs3Xmy5uiYmr1TtDhOBAZnL9vUtLIt95Pw3jovkY9q2NIwgLzfSlzOU83sQvzAooZWuJ65JERwxx+Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "7.0.0-rc.2",
|
||||
"@babel/generator": "7.0.0-rc.2",
|
||||
"@babel/helper-function-name": "7.0.0-rc.2",
|
||||
"@babel/helper-split-export-declaration": "7.0.0-rc.2",
|
||||
"@babel/parser": "7.0.0-rc.2",
|
||||
"@babel/types": "7.0.0-rc.2",
|
||||
"debug": "^3.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.10"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-I2SMGD8bUX7sysOwGM8TcwCoaHiOx2YWZmT9h5oAncsPQ9Wy068yJneCF4vkOGTCzPFIETPDR5i3EIEm5QgMFg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esutils": "^2.0.2",
|
||||
"lodash": "^4.17.10",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
|
||||
"integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "11.7.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz",
|
||||
"integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==",
|
||||
"dev": true
|
||||
},
|
||||
"jsesc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz",
|
||||
"integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
|
||||
"dev": true
|
||||
},
|
||||
"to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.0.0-beta.51",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.51.tgz",
|
||||
@@ -69,6 +244,170 @@
|
||||
"@babel/types": "7.0.0-beta.51"
|
||||
}
|
||||
},
|
||||
"@babel/helpers": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-X5e6FnKEhS8UtSJfjjkEvY8Mq+W52FES6p55g16gHmVycVrggjwZryQKqK+iMJlus7Dgz6MrrdOtC1SWx4jDDg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/template": "7.0.0-rc.2",
|
||||
"@babel/traverse": "7.0.0-rc.2",
|
||||
"@babel/types": "7.0.0-rc.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-+cVix+HBNakVp7IU1WReJV8dnJl/yaBA5JRXc758BSrvJCH2hKp1Z0xHIiUaOvxMwKXc3EXGIYhlnx5T+6ofGA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/highlight": "7.0.0-rc.2"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-kD6hlprDaBy17V8qd9uXJbYC5ZYyCggieT+tiGzCwayA7oyT5ynPec3MNkWQHkLyhB7IP2n3c/Ep329jOPQY/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "7.0.0-rc.2",
|
||||
"jsesc": "^2.5.1",
|
||||
"lodash": "^4.17.10",
|
||||
"source-map": "^0.5.0",
|
||||
"trim-right": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@babel/helper-function-name": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-1frd4Bm/8yfZoAj87tmB6gtQNWtKAzfRzjASVdmsItzq9X13yUlyFLdo6/tNhazftwJO8iIZeadOpi3rNKDXhg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-get-function-arity": "7.0.0-rc.2",
|
||||
"@babel/template": "7.0.0-rc.2",
|
||||
"@babel/types": "7.0.0-rc.2"
|
||||
}
|
||||
},
|
||||
"@babel/helper-get-function-arity": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-5tjNc0hYngGqBGdjvzN89p92WY6aCntaDv8AadB/xgyUx4VievZwEbz8pc6GKkO6+qfghfZhv1F3+9SC6IA3Eg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "7.0.0-rc.2"
|
||||
}
|
||||
},
|
||||
"@babel/helper-split-export-declaration": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-MBtzTAeZT7MxWETY0JRh5yyIKY4tN/q68BU4/XgzZUaHJ+G74fJUoR7mPO3TbTiwLIEFVBbZQA9AG4yYqe5W2g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "7.0.0-rc.2"
|
||||
}
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-96V6XHAh9XHzjmucShCP8tULwXsC446doZ6REaLVdZDPNj3NsWbsC7OBeY+u6UWiFxHTTv6YmA4Veh4wXuucYw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.0.0",
|
||||
"esutils": "^2.0.2",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-zDB1QPgQWYwuJty3Ymbx1hq7zbBEbZjTprHOhforvzyQFV86LNh6FS0InjnOUXM6p6QUyONz8KTt/v+MRMd0Hg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-CryGZ01Nko2/g8gkYiiPc7x9ZinrX59/BTWMZV1sDj5cAeia64vhyNnXTcNeim885IdGOdYyia1PNBWKnFxuSw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "7.0.0-rc.2",
|
||||
"@babel/parser": "7.0.0-rc.2",
|
||||
"@babel/types": "7.0.0-rc.2"
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-x8y9E+KZHs3Xmy5uiYmr1TtDhOBAZnL9vUtLIt95Pw3jovkY9q2NIwgLzfSlzOU83sQvzAooZWuJ65JERwxx+Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "7.0.0-rc.2",
|
||||
"@babel/generator": "7.0.0-rc.2",
|
||||
"@babel/helper-function-name": "7.0.0-rc.2",
|
||||
"@babel/helper-split-export-declaration": "7.0.0-rc.2",
|
||||
"@babel/parser": "7.0.0-rc.2",
|
||||
"@babel/types": "7.0.0-rc.2",
|
||||
"debug": "^3.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.10"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-rc.2.tgz",
|
||||
"integrity": "sha512-I2SMGD8bUX7sysOwGM8TcwCoaHiOx2YWZmT9h5oAncsPQ9Wy068yJneCF4vkOGTCzPFIETPDR5i3EIEm5QgMFg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esutils": "^2.0.2",
|
||||
"lodash": "^4.17.10",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
|
||||
"integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "11.7.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz",
|
||||
"integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==",
|
||||
"dev": true
|
||||
},
|
||||
"jsesc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz",
|
||||
"integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
|
||||
"dev": true
|
||||
},
|
||||
"to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-rc.1.tgz",
|
||||
@@ -2316,6 +2655,21 @@
|
||||
"babel-plugin-syntax-object-rest-spread": "^6.13.0"
|
||||
}
|
||||
},
|
||||
"babel-preset-modern-browsers": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-modern-browsers/-/babel-preset-modern-browsers-11.0.1.tgz",
|
||||
"integrity": "sha512-9QR7Mq4BkHyWZ3I2lRwgfpYOhqOaw9wBGAyXfyKV3hasIEBNehBOTmEFwFYnQqVjLtovGrmHE/l+SV3MunwkdA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"babel-plugin-check-es2015-constants": "^6.22.0",
|
||||
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
|
||||
"babel-plugin-syntax-trailing-function-commas": "^6.22.0",
|
||||
"babel-plugin-transform-es2015-arrow-functions": "^6.22.0",
|
||||
"babel-plugin-transform-es2015-function-name": "^6.24.1",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0"
|
||||
}
|
||||
},
|
||||
"babel-preset-react": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz",
|
||||
@@ -8579,6 +8933,11 @@
|
||||
"mimic-fn": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"memoize-one": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.0.tgz",
|
||||
"integrity": "sha512-wdpOJ4XBejprGn/xhd1i2XR8Dv1A25FJeIvR7syQhQlz9eXsv+06llcvcmBxlWVGv4C73QBsWA8kxvZozzNwiQ=="
|
||||
},
|
||||
"memory-fs": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"gl-matrix": "^2.7.1",
|
||||
"key-pressed": "0.0.1",
|
||||
"lodash": "^4.17.4",
|
||||
"memoize-one": "^4.0.0",
|
||||
"mouse-position": "^2.0.1",
|
||||
"mouse-pressed": "^1.0.0",
|
||||
"orbit-camera": "^1.0.0",
|
||||
@@ -53,6 +54,7 @@
|
||||
"urijs": "^1.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0-rc.1",
|
||||
"autoprefixer": "^9.1.1",
|
||||
"babel-core": "^6.13.2",
|
||||
"babel-eslint": "^8.2.6",
|
||||
@@ -62,6 +64,7 @@
|
||||
"babel-plugin-transform-react-constant-elements": "^6.9.1",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-modern-browsers": "^11.0.1",
|
||||
"babel-preset-react": "^6.11.1",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-register": "^6.11.6",
|
||||
|
||||
@@ -1,101 +1,149 @@
|
||||
// jshint esversion: 6
|
||||
import _ from "lodash";
|
||||
import memoize from "memoize-one";
|
||||
import * as globals from "../globals";
|
||||
import store from "../reducers";
|
||||
import URI from "urijs";
|
||||
import _ from "lodash";
|
||||
import { Universe } from "../util/stateManager";
|
||||
|
||||
const requestCells = (query = "") => {
|
||||
return dispatch => {
|
||||
dispatch({ type: "request cells started" });
|
||||
return fetch(`${globals.API.prefix}${globals.API.version}cells${query}`, {
|
||||
/*
|
||||
Catch unexpected errors and make sure we don't lose them!
|
||||
*/
|
||||
function catchErrorsWrap(fn) {
|
||||
return (dispatch, getState) => {
|
||||
fn(dispatch, getState).catch(error => {
|
||||
console.error(error);
|
||||
dispatch({ type: "UNEXPECTED ERROR", error });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async function doRequestInitialize() {
|
||||
const res = await fetch(
|
||||
`${globals.API.prefix}${globals.API.version}initialize`,
|
||||
{
|
||||
method: "get",
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json"
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
data => dispatch({ type: "request cells success", data }),
|
||||
error => dispatch({ type: "request cells error", error })
|
||||
}
|
||||
);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function doRequestCells(query) {
|
||||
const res = await fetch(
|
||||
`${globals.API.prefix}${globals.API.version}cells${query}`,
|
||||
{
|
||||
method: "get",
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json"
|
||||
})
|
||||
}
|
||||
);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function doInitialDataLoad(query = "") {
|
||||
return catchErrorsWrap(async dispatch => {
|
||||
dispatch({ type: "initial data load start" });
|
||||
try {
|
||||
const res = await Promise.all([
|
||||
doRequestInitialize(),
|
||||
doRequestCells(query)
|
||||
]);
|
||||
const universe = Universe.createUniverseFromRESTv01Response(
|
||||
res[0],
|
||||
res[1]
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/* SELECT */
|
||||
const regraph = () => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: "regraph started" });
|
||||
|
||||
const state = getState();
|
||||
const selectedMetadata = {};
|
||||
|
||||
_.each(state.controls.categoricalAsBooleansMap, (options, field) => {
|
||||
let atLeastOneOptionDeselected = false;
|
||||
|
||||
_.each(options, (isActive, option) => {
|
||||
if (!isActive) {
|
||||
atLeastOneOptionDeselected = true;
|
||||
}
|
||||
dispatch({
|
||||
type: "initial data load complete (universe exists)",
|
||||
universe
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch({ type: "initial data load error", error });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (atLeastOneOptionDeselected) {
|
||||
_.each(options, (isActive, option) => {
|
||||
if (isActive) {
|
||||
if (selectedMetadata[field]) {
|
||||
selectedMetadata[field].push(option);
|
||||
} else if (!selectedMetadata[field]) {
|
||||
selectedMetadata[field] = [option];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// XXX TODO - this is the old code for doing a regraph. Preserving it solely
|
||||
// until we port to 0.2 API. The new UX for regraph can't be implemented on
|
||||
// the 0.1 API (doesn't allow for re-layout on arbitrary sets of cells), so just
|
||||
// punting for now. See ticket #88
|
||||
//
|
||||
//
|
||||
// /* SELECT */
|
||||
// const regraph = () => {
|
||||
// return (dispatch, getState) => {
|
||||
// dispatch({ type: "regraph started" });
|
||||
//
|
||||
// const state = getState();
|
||||
// const selectedMetadata = {};
|
||||
//
|
||||
// _.each(state.controls.categoricalAsBooleansMap, (options, field) => {
|
||||
// let atLeastOneOptionDeselected = false;
|
||||
//
|
||||
// _.each(options, (isActive, option) => {
|
||||
// if (!isActive) {
|
||||
// atLeastOneOptionDeselected = true;
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// if (atLeastOneOptionDeselected) {
|
||||
// _.each(options, (isActive, option) => {
|
||||
// if (isActive) {
|
||||
// if (selectedMetadata[field]) {
|
||||
// selectedMetadata[field].push(option);
|
||||
// } else if (!selectedMetadata[field]) {
|
||||
// selectedMetadata[field] = [option];
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// let uri = new URI();
|
||||
// uri.setSearch(selectedMetadata);
|
||||
// console.log(uri.search(), selectedMetadata);
|
||||
//
|
||||
// dispatch(requestCells(uri.search())).then(res => {
|
||||
// if (res.error) {
|
||||
// dispatch({ type: "regraph error" });
|
||||
// } else {
|
||||
// dispatch({ type: "regraph success" });
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
// };
|
||||
|
||||
let uri = new URI();
|
||||
uri.setSearch(selectedMetadata);
|
||||
console.log(uri.search(), selectedMetadata);
|
||||
|
||||
dispatch(requestCells(uri.search())).then(res => {
|
||||
if (res.error) {
|
||||
dispatch({ type: "regraph error" });
|
||||
} else {
|
||||
dispatch({ type: "regraph success" });
|
||||
}
|
||||
});
|
||||
};
|
||||
const regraph = () => (dispatch, getState) => {
|
||||
const { universe, world, crossfilter } = getState().controls;
|
||||
dispatch({
|
||||
type: "set World to current selection",
|
||||
universe,
|
||||
world,
|
||||
crossfilter
|
||||
});
|
||||
};
|
||||
|
||||
const resetGraph = () => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: "reset graph" });
|
||||
};
|
||||
};
|
||||
|
||||
const initialize = () => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: "initialize started" });
|
||||
fetch(`${globals.API.prefix}${globals.API.version}initialize`, {
|
||||
method: "get",
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/json"
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
data => dispatch({ type: "initialize success", data }),
|
||||
error => dispatch({ type: "initialize error", error })
|
||||
);
|
||||
};
|
||||
};
|
||||
const resetGraph = () => (dispatch, getState) =>
|
||||
dispatch({
|
||||
type: "reset World to eq Universe",
|
||||
universe: getState().controls.universe
|
||||
});
|
||||
|
||||
// This code defends against the case where /expression returns a cellname
|
||||
// never seen before (ie, not returned by /cells). This should not happen
|
||||
// (see https://github.com/chanzuckerberg/cellxgene-rest-api/issues/34) but
|
||||
// occasionally does.
|
||||
//
|
||||
// XXX TODO - this code is only relevant in v0.1 REST API, and can be retired
|
||||
// when we port to 0.2.
|
||||
//
|
||||
const makeMetadataMap = memoize(metadata => _.keyBy(metadata, "CellName"));
|
||||
function cleanupExpressionResponse(data) {
|
||||
const s = store.getState();
|
||||
const metadata = s.controls.allCellsMetadataMap;
|
||||
const { universe } = s.controls;
|
||||
const metadata = makeMetadataMap(universe.obsAnnotations);
|
||||
let errorFound = false;
|
||||
data.data.cells = _.filter(data.data.cells, cell => {
|
||||
if (!errorFound && !metadata[cell.cellname]) {
|
||||
@@ -110,123 +158,145 @@ function cleanupExpressionResponse(data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const requestGeneExpressionCounts = () => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: "get expression started" });
|
||||
fetch(`${globals.API.prefix}${globals.API.version}expression`, {
|
||||
method: "get",
|
||||
headers: new Headers({
|
||||
accept: "application/json"
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => cleanupExpressionResponse(data))
|
||||
.then(
|
||||
data => dispatch({ type: "get expression success", data }),
|
||||
error => dispatch({ type: "get expression error", error })
|
||||
);
|
||||
};
|
||||
};
|
||||
/*
|
||||
Fetch [gene, ...] from V0.1 API. Not an action function - just a helper
|
||||
which implements the new expression data caching.
|
||||
*/
|
||||
async function _doRequestExpressionData(dispatch, getState, genes) {
|
||||
const state = getState();
|
||||
/* check cache and only fetch data we do not already have */
|
||||
const { universe } = state.controls;
|
||||
const genesToFetch = _.filter(genes, g => !universe.varDataCache[g]);
|
||||
|
||||
const requestSingleGeneExpressionCountsForColoringPOST = gene => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: "get single gene expression for coloring started" });
|
||||
fetch(`${globals.API.prefix}${globals.API.version}expression`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
genelist: [gene]
|
||||
}),
|
||||
headers: new Headers({
|
||||
accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => cleanupExpressionResponse(data))
|
||||
.then(
|
||||
data =>
|
||||
dispatch({
|
||||
type: "color by expression",
|
||||
gene: gene,
|
||||
data
|
||||
dispatch({ type: "expression load start" });
|
||||
let expressionData = {}; // { gene: data }
|
||||
if (genesToFetch.length) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${globals.API.prefix}${globals.API.version}expression`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
genelist: genes
|
||||
}),
|
||||
error =>
|
||||
dispatch({
|
||||
type: "get single gene expression for coloring error",
|
||||
error
|
||||
headers: new Headers({
|
||||
accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
})
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
let data = await res.json();
|
||||
data = cleanupExpressionResponse(data);
|
||||
data = Universe.convertExpressionRESTv01ToObject(universe, data);
|
||||
expressionData = {
|
||||
...expressionData,
|
||||
...data
|
||||
};
|
||||
} catch (error) {
|
||||
dispatch({ type: "expression load error", error });
|
||||
throw error; // rethrow
|
||||
}
|
||||
}
|
||||
|
||||
const requestGeneExpressionCountsPOST = genes => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: "get expression started" });
|
||||
fetch(`${globals.API.prefix}${globals.API.version}expression`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
genelist: genes
|
||||
}),
|
||||
headers: new Headers({
|
||||
accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => cleanupExpressionResponse(data))
|
||||
.then(
|
||||
data => dispatch({ type: "get expression success", data }),
|
||||
error => dispatch({ type: "get expression error", error })
|
||||
);
|
||||
};
|
||||
};
|
||||
// add the cached values
|
||||
_.forEach(genes, g => {
|
||||
if (expressionData[g] === undefined) {
|
||||
expressionData[g] = universe.varDataCache[g];
|
||||
}
|
||||
});
|
||||
|
||||
const requestDifferentialExpression = (celllist1, celllist2, num_genes = 7) => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: "request differential expression started" });
|
||||
fetch(`${globals.API.prefix}${globals.API.version}diffexpression`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
celllist1,
|
||||
celllist2,
|
||||
num_genes
|
||||
}),
|
||||
headers: new Headers({
|
||||
accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
data => {
|
||||
/* kick off a secondary action to get all expression counts for all cells now that we know what the top expressed are */
|
||||
dispatch(
|
||||
requestGeneExpressionCountsPOST(
|
||||
_.union(
|
||||
data.data.celllist1.topgenes,
|
||||
data.data.celllist2.topgenes
|
||||
) // ["GPM6B", "FEZ1", "TSPAN31", "PCSK1N", "TUBA1A", "GPM6A", "CLU", "FCER1G", "TYROBP", "C1QB", "CD74", "CYBA", "GPX1", "TMSB4X"]
|
||||
)
|
||||
);
|
||||
/* then send the success case action through */
|
||||
return dispatch({
|
||||
type: "request differential expression success",
|
||||
data
|
||||
});
|
||||
return dispatch({ type: "expression load success", expressionData });
|
||||
}
|
||||
|
||||
function requestSingleGeneExpressionCountsForColoringPOST(gene) {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({ type: "get single gene expression for coloring started" });
|
||||
try {
|
||||
await _doRequestExpressionData(dispatch, getState, [gene]);
|
||||
dispatch({
|
||||
type: "color by expression",
|
||||
gene,
|
||||
data: {
|
||||
[gene]: getState().controls.world.varDataCache[gene]
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: "get single gene expression for coloring error",
|
||||
error
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const requestGeneExpressionCountsPOST = genes => async (dispatch, getState) => {
|
||||
dispatch({ type: "get expression started" });
|
||||
try {
|
||||
await _doRequestExpressionData(dispatch, getState, genes);
|
||||
return dispatch({
|
||||
type: "get expression success",
|
||||
genes,
|
||||
data: _.transform(
|
||||
genes,
|
||||
(res, gene) => {
|
||||
res[gene] = getState().controls.world.varDataCache[gene];
|
||||
},
|
||||
error =>
|
||||
dispatch({ type: "request differential expression error", error })
|
||||
);
|
||||
};
|
||||
{}
|
||||
)
|
||||
});
|
||||
} catch (error) {
|
||||
return dispatch({ type: "get expression error", error });
|
||||
}
|
||||
};
|
||||
|
||||
const requestDifferentialExpression = (
|
||||
celllist1,
|
||||
celllist2,
|
||||
num_genes = 7
|
||||
) => dispatch => {
|
||||
dispatch({ type: "request differential expression started" });
|
||||
fetch(`${globals.API.prefix}${globals.API.version}diffexpression`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
celllist1,
|
||||
celllist2,
|
||||
num_genes
|
||||
}),
|
||||
headers: new Headers({
|
||||
accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
data => {
|
||||
/*
|
||||
kick off a secondary action to get all expression counts for all cells
|
||||
now that we know what the top expressed are
|
||||
*/
|
||||
dispatch(
|
||||
requestGeneExpressionCountsPOST(
|
||||
_.union(data.data.celllist1.topgenes, data.data.celllist2.topgenes)
|
||||
)
|
||||
);
|
||||
/* then send the success case action through */
|
||||
return dispatch({
|
||||
type: "request differential expression success",
|
||||
data
|
||||
});
|
||||
},
|
||||
error =>
|
||||
dispatch({
|
||||
type: "request differential expression error",
|
||||
error
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
requestCells,
|
||||
regraph,
|
||||
resetGraph,
|
||||
requestGeneExpressionCounts,
|
||||
requestGeneExpressionCountsPOST,
|
||||
requestSingleGeneExpressionCountsForColoringPOST,
|
||||
requestDifferentialExpression
|
||||
requestDifferentialExpression,
|
||||
doInitialDataLoad
|
||||
};
|
||||
|
||||
@@ -7,9 +7,7 @@ import { connect } from "react-redux";
|
||||
// import PulseLoader from "halogen/PulseLoader";
|
||||
|
||||
import LeftSideBar from "./leftsidebar";
|
||||
import Parallel from "./continuous/parallel";
|
||||
import Legend from "./continuousLegend";
|
||||
// import Joy from "./joy/joy";
|
||||
import Graph from "./graph/graph";
|
||||
import * as globals from "../globals";
|
||||
import actions from "../actions";
|
||||
@@ -18,8 +16,8 @@ import SectionHeader from "./framework/sectionHeader";
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
cells: state.cells,
|
||||
initialize: state.initialize
|
||||
loading: state.controls.loading,
|
||||
error: state.controls.error
|
||||
};
|
||||
})
|
||||
class App extends React.Component {
|
||||
@@ -27,20 +25,18 @@ class App extends React.Component {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
_onURLChanged() {
|
||||
this.props.dispatch({ type: "url changed", url: document.location.href });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
/* listen for url changes, fire one when we start the app up */
|
||||
window.addEventListener("popstate", this._onURLChanged);
|
||||
this._onURLChanged();
|
||||
|
||||
this.props.dispatch(actions.initialize());
|
||||
this.props.dispatch(actions.doInitialDataLoad(window.location.search));
|
||||
|
||||
/*
|
||||
first request includes query straight off the url bar for now
|
||||
*/
|
||||
this.props.dispatch(actions.requestCells(window.location.search));
|
||||
/* listen for resize events */
|
||||
window.addEventListener("resize", () => {
|
||||
this.props.dispatch({
|
||||
@@ -61,10 +57,11 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, error } = this.props;
|
||||
return (
|
||||
<Container>
|
||||
<Helmet title="cellxgene" />
|
||||
{this.props.cells.loading || this.props.initialize.loading ? (
|
||||
{loading ? (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
@@ -76,12 +73,9 @@ class App extends React.Component {
|
||||
loading cellxgene
|
||||
</div>
|
||||
) : null}
|
||||
{this.props.cells.error ? "Error loading cells" : null}
|
||||
|
||||
{error ? "Error loading cells" : null}
|
||||
<div>
|
||||
{this.props.cells.loading || this.props.initialize.loading ? null : (
|
||||
<LeftSideBar />
|
||||
)}
|
||||
{loading ? null : <LeftSideBar />}
|
||||
<div
|
||||
style={{
|
||||
padding: 15,
|
||||
@@ -89,13 +83,10 @@ class App extends React.Component {
|
||||
marginLeft: 350 /* but responsive */
|
||||
}}
|
||||
>
|
||||
{this.props.cells.loading ||
|
||||
this.props.initialize.loading ? null : (
|
||||
<Graph />
|
||||
)}
|
||||
{loading ? null : <Graph />}
|
||||
|
||||
<Legend />
|
||||
{/*<Parallel/>*/}
|
||||
{}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
@@ -104,5 +95,3 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
// <Joy data={this.state.expressions && this.state.expressions.data} />
|
||||
|
||||
@@ -3,16 +3,14 @@ import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import styles from "./categorical.css";
|
||||
import SectionHeader from "../framework/sectionHeader";
|
||||
import Value from "./value";
|
||||
import { alphabeticallySortedValues } from "./util";
|
||||
|
||||
import FaArrowRight from "react-icons/lib/fa/angle-right";
|
||||
import FaArrowDown from "react-icons/lib/fa/angle-down";
|
||||
import FaPaintBrush from "react-icons/lib/fa/paint-brush";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import Value from "./value";
|
||||
import { alphabeticallySortedValues } from "./util";
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
@@ -27,6 +25,7 @@ class Category extends React.Component {
|
||||
isExpanded: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const valuesAsBool = _.values(
|
||||
this.props.categoricalAsBooleansMap[this.props.metadataField]
|
||||
@@ -45,12 +44,14 @@ class Category extends React.Component {
|
||||
this.checkbox.indeterminate = true;
|
||||
}
|
||||
}
|
||||
|
||||
handleColorChange() {
|
||||
this.props.dispatch({
|
||||
type: "color by categorical metadata",
|
||||
colorAccessor: this.props.metadataField
|
||||
});
|
||||
}
|
||||
|
||||
toggleAll() {
|
||||
this.props.dispatch({
|
||||
type: "categorical metadata filter all of these",
|
||||
@@ -58,6 +59,7 @@ class Category extends React.Component {
|
||||
});
|
||||
this.setState({ isChecked: true });
|
||||
}
|
||||
|
||||
toggleNone() {
|
||||
this.props.dispatch({
|
||||
type: "categorical metadata filter none of these",
|
||||
@@ -66,6 +68,7 @@ class Category extends React.Component {
|
||||
});
|
||||
this.setState({ isChecked: false });
|
||||
}
|
||||
|
||||
renderCategoryItems() {
|
||||
return _.map(alphabeticallySortedValues(this.props.values), (v, i) => {
|
||||
return (
|
||||
@@ -79,6 +82,7 @@ class Category extends React.Component {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
handleToggleAllClick() {
|
||||
// || this.checkbox.indeterminate === false
|
||||
if (this.state.isChecked) {
|
||||
@@ -89,6 +93,7 @@ class Category extends React.Component {
|
||||
this.toggleAll();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
@@ -164,8 +169,7 @@ class Category extends React.Component {
|
||||
}
|
||||
|
||||
@connect(state => {
|
||||
const ranges = _.get(state, "cells.cells.data.ranges", null);
|
||||
|
||||
const ranges = _.get(state.controls.world, "summary.obs", null);
|
||||
return {
|
||||
ranges
|
||||
};
|
||||
@@ -192,7 +196,7 @@ class Categories extends React.Component {
|
||||
>
|
||||
{_.map(this.props.ranges, (value, key) => {
|
||||
const isColorField = key.includes("color") || key.includes("Color");
|
||||
if (value.options && key !== "CellName" && !isColorField) {
|
||||
if (value.options && !isColorField && key !== "name") {
|
||||
return (
|
||||
<Category key={key} metadataField={key} values={value.options} />
|
||||
);
|
||||
|
||||
@@ -17,18 +17,15 @@ import HistogramBrush from "./histogramBrush";
|
||||
import { margin, width, height, createDimensions } from "./util";
|
||||
|
||||
@connect(state => {
|
||||
const ranges = _.get(state, "cells.cells.data.ranges", null);
|
||||
const metadata = _.get(state, "cells.cells.data.metadata", null);
|
||||
const initializeRanges = _.get(state, "initialize.data.data.ranges", null);
|
||||
const metadata = _.get(state.controls.world, "obsAnnotations", null);
|
||||
const ranges = _.get(state.controls.world, "summary.obs", null);
|
||||
|
||||
return {
|
||||
ranges,
|
||||
metadata,
|
||||
initializeRanges,
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
graphBrushSelection: state.controls.graphBrushSelection,
|
||||
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
|
||||
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null)
|
||||
};
|
||||
})
|
||||
class Continuous extends React.Component {
|
||||
@@ -41,17 +38,19 @@ class Continuous extends React.Component {
|
||||
dimensions: null
|
||||
};
|
||||
}
|
||||
|
||||
handleBrushAction(selection) {
|
||||
this.props.dispatch({
|
||||
type: "continuous selection using parallel coords brushing",
|
||||
data: selection
|
||||
});
|
||||
}
|
||||
|
||||
handleColorAction(key) {
|
||||
this.props.dispatch({
|
||||
type: "color by continuous metadata",
|
||||
colorAccessor: key,
|
||||
rangeMaxForColorAccessor: this.props.initializeRanges[key].range.max
|
||||
rangeMaxForColorAccessor: this.props.ranges[key].range.max
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,7 +59,7 @@ class Continuous extends React.Component {
|
||||
<div>
|
||||
{_.map(this.props.ranges, (value, key) => {
|
||||
const isColorField = key.includes("color") || key.includes("Color");
|
||||
if (value.range && key !== "CellName" && !isColorField) {
|
||||
if (value.range && key !== "name" && !isColorField) {
|
||||
return (
|
||||
<HistogramBrush
|
||||
key={key}
|
||||
|
||||
@@ -8,87 +8,90 @@ import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import FaPaintBrush from "react-icons/lib/fa/paint-brush";
|
||||
import * as globals from "../../globals";
|
||||
import * as d3 from "d3";
|
||||
import memoize from "memoize-one";
|
||||
import * as globals from "../../globals";
|
||||
|
||||
@connect(state => {
|
||||
const initializeRanges = _.get(state, "initialize.data.data.ranges", null);
|
||||
|
||||
return {
|
||||
initializeRanges,
|
||||
initializeRanges: _.get(state.controls.world, "summary.obs"),
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
cellsMetadata: state.controls.cellsMetadata
|
||||
obsAnnotations: _.get(state.controls.world, "obsAnnotations", null)
|
||||
};
|
||||
})
|
||||
class HistogramBrush extends React.Component {
|
||||
calcHistogramCache = memoize((obsAnnotations, metadataField, ranges) => {
|
||||
// recalculate expensive stuff
|
||||
const allValuesForContinuousFieldAsArray = _.map(
|
||||
obsAnnotations,
|
||||
metadataField
|
||||
);
|
||||
const histogramCache = {};
|
||||
|
||||
histogramCache.x = d3
|
||||
.scaleLinear()
|
||||
.domain([ranges.min, ranges.max])
|
||||
.range([0, this.width]);
|
||||
|
||||
histogramCache.y = d3
|
||||
.scaleLinear()
|
||||
.range([this.height - this.marginBottom, 0]);
|
||||
// .range([height - margin.bottom, margin.top]);
|
||||
|
||||
histogramCache.bins = d3
|
||||
.histogram()
|
||||
.domain(histogramCache.x.domain())
|
||||
.thresholds(40)(allValuesForContinuousFieldAsArray);
|
||||
|
||||
histogramCache.numValues = allValuesForContinuousFieldAsArray.length;
|
||||
|
||||
return histogramCache;
|
||||
});
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.width = 300;
|
||||
this.height = 100;
|
||||
this.marginBottom = 20;
|
||||
this.histogramCache = {};
|
||||
|
||||
this.state = {
|
||||
svg: null,
|
||||
ctx: null,
|
||||
axes: null,
|
||||
dimensions: null,
|
||||
brush: null
|
||||
};
|
||||
}
|
||||
calcHistogramCache(nextProps) {
|
||||
// recalculate expensive stuff
|
||||
const allValuesForContinuousFieldAsArray = _.map(
|
||||
nextProps.cellsMetadata,
|
||||
nextProps.metadataField
|
||||
);
|
||||
|
||||
this.histogramCache.x = d3
|
||||
.scaleLinear()
|
||||
.domain([nextProps.ranges.min, nextProps.ranges.max])
|
||||
.range([0, this.width]);
|
||||
|
||||
this.histogramCache.y = d3
|
||||
.scaleLinear()
|
||||
.range([this.height - this.marginBottom, 0]);
|
||||
// .range([height - margin.bottom, margin.top]);
|
||||
|
||||
this.histogramCache.bins = d3
|
||||
.histogram()
|
||||
.domain(this.histogramCache.x.domain())
|
||||
.thresholds(40)(allValuesForContinuousFieldAsArray);
|
||||
|
||||
this.histogramCache.numValues = allValuesForContinuousFieldAsArray.length;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.calcHistogramCache(this.props);
|
||||
}
|
||||
|
||||
onBrush(selection, x) {
|
||||
return () => {
|
||||
const { dispatch, metadataField } = this.props;
|
||||
if (d3.event.selection) {
|
||||
this.props.dispatch({
|
||||
dispatch({
|
||||
type: "continuous metadata histogram brush",
|
||||
selection: this.props.metadataField,
|
||||
selection: metadataField,
|
||||
range: [x(d3.event.selection[0]), x(d3.event.selection[1])]
|
||||
});
|
||||
} else {
|
||||
this.props.dispatch({
|
||||
dispatch({
|
||||
type: "continuous metadata histogram brush",
|
||||
selection: this.props.metadataField,
|
||||
selection: metadataField,
|
||||
range: null
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
drawHistogram(svgRef) {
|
||||
const x = this.histogramCache.x;
|
||||
const y = this.histogramCache.y;
|
||||
const bins = this.histogramCache.bins;
|
||||
const numValues = this.histogramCache.numValues;
|
||||
const { obsAnnotations, metadataField, ranges } = this.props;
|
||||
const histogramCache = this.calcHistogramCache(
|
||||
obsAnnotations,
|
||||
metadataField,
|
||||
ranges
|
||||
);
|
||||
|
||||
const { x, y, bins, numValues } = histogramCache;
|
||||
d3.select(svgRef)
|
||||
.selectAll(".bar")
|
||||
.remove();
|
||||
|
||||
d3.select(svgRef)
|
||||
.insert("g", "*")
|
||||
@@ -97,6 +100,7 @@ class HistogramBrush extends React.Component {
|
||||
.data(bins)
|
||||
.enter()
|
||||
.append("rect")
|
||||
.attr("class", "bar")
|
||||
.attr("x", function(d) {
|
||||
return x(d.x0) + 1;
|
||||
})
|
||||
@@ -144,6 +148,7 @@ class HistogramBrush extends React.Component {
|
||||
this.setState({ brush, xAxis });
|
||||
}
|
||||
}
|
||||
|
||||
handleColorAction() {
|
||||
this.props.dispatch({
|
||||
type: "color by continuous metadata",
|
||||
@@ -153,6 +158,7 @@ class HistogramBrush extends React.Component {
|
||||
].range.max
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
/* rc slider https://www.npmjs.com/package/rc-slider */
|
||||
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import styles from "./parallelCoordinates.css";
|
||||
import SectionHeader from "../framework/sectionHeader";
|
||||
|
||||
import setupParallelCoordinates from "./setupParallelCoordinates";
|
||||
import drawAxes from "./drawAxes";
|
||||
import drawLinesCanvas from "./drawLinesCanvas";
|
||||
|
||||
import { margin, width, height, createDimensions } from "./util";
|
||||
|
||||
@connect(state => {
|
||||
const ranges = _.get(state, "cells.cells.data.ranges", null);
|
||||
const metadata = _.get(state, "cells.cells.data.metadata", null);
|
||||
const initializeRanges = _.get(state, "initialize.data.data.ranges", null);
|
||||
|
||||
return {
|
||||
ranges,
|
||||
metadata,
|
||||
initializeRanges,
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
graphBrushSelection: state.controls.graphBrushSelection,
|
||||
cellsMetadata: state.controls.cellsMetadata,
|
||||
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
|
||||
};
|
||||
})
|
||||
class Parallel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
svg: null,
|
||||
ctx: null,
|
||||
axes: null,
|
||||
dimensions: null
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
const { svg, ctx } = setupParallelCoordinates(width, height, margin);
|
||||
this.setState({ svg, ctx });
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.maybeDrawAxes(nextProps);
|
||||
this.maybeDrawLines(nextProps);
|
||||
}
|
||||
maybeDrawAxes(nextProps) {
|
||||
if (
|
||||
!this.state.axes &&
|
||||
nextProps.initializeRanges /* axes are created on full range of data */
|
||||
) {
|
||||
const dimensions = createDimensions(nextProps.initializeRanges);
|
||||
|
||||
const xscale = d3
|
||||
.scalePoint()
|
||||
.domain(d3.range(dimensions.length))
|
||||
.range([0, width]);
|
||||
|
||||
const axes = drawAxes(
|
||||
this.state.svg,
|
||||
this.state.ctx,
|
||||
dimensions,
|
||||
xscale,
|
||||
height,
|
||||
width,
|
||||
this.handleBrushAction.bind(this),
|
||||
this.handleColorAction.bind(this)
|
||||
);
|
||||
|
||||
this.setState({
|
||||
axes,
|
||||
xscale,
|
||||
dimensions
|
||||
});
|
||||
|
||||
this.props.dispatch({
|
||||
type: "parallel coordinates axes have been drawn"
|
||||
});
|
||||
}
|
||||
}
|
||||
maybeDrawLines = _.debounce(nextProps => {
|
||||
/* https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js */
|
||||
if (
|
||||
nextProps.ranges &&
|
||||
nextProps.cellsMetadata &&
|
||||
nextProps.axesHaveBeenDrawn
|
||||
) {
|
||||
if (this.state._drawLinesCanvas) {
|
||||
this.state._drawLinesCanvas.invalidate(); /* this is only necessary if the internals of drawLinesCanvas are using the render queue */
|
||||
}
|
||||
|
||||
this.state.ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const _drawLinesCanvas = drawLinesCanvas(
|
||||
nextProps.cellsMetadata,
|
||||
this.state.dimensions,
|
||||
this.state.xscale,
|
||||
this.state.ctx,
|
||||
nextProps.colorAccessor,
|
||||
nextProps.colorScale
|
||||
);
|
||||
|
||||
this.setState({
|
||||
_drawLinesCanvas /* this will only exist if the internals of drawLinesCanvas are using the render queue */
|
||||
});
|
||||
}
|
||||
}, 200);
|
||||
|
||||
handleBrushAction(selection) {
|
||||
this.props.dispatch({
|
||||
type: "continuous selection using parallel coords brushing",
|
||||
data: selection
|
||||
});
|
||||
}
|
||||
handleColorAction(key) {
|
||||
this.props.dispatch({
|
||||
type: "color by continuous metadata",
|
||||
colorAccessor: key,
|
||||
rangeMaxForColorAccessor: this.props.initializeRanges[key].range.max
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="parcoords_wrapper">
|
||||
<div
|
||||
className={styles.parcoords}
|
||||
id="parcoords"
|
||||
style={{
|
||||
width: width + margin.left + margin.right + "px",
|
||||
height: height + margin.top + margin.bottom + "px"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Parallel;
|
||||
|
||||
// <SectionHeader text="Continuous Metadata"/>
|
||||
@@ -1,19 +1,17 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import * as d3 from "d3";
|
||||
import { interpolateViridis } from "d3-scale-chromatic";
|
||||
|
||||
// create continuous color legend
|
||||
// http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f
|
||||
const continuous = (selector_id, colorscale) => {
|
||||
var legendheight = 200,
|
||||
legendwidth = 80,
|
||||
margin = { top: 10, right: 60, bottom: 10, left: 2 };
|
||||
const legendheight = 200;
|
||||
const legendwidth = 80;
|
||||
const margin = { top: 10, right: 60, bottom: 10, left: 2 };
|
||||
|
||||
var canvas = d3
|
||||
const canvas = d3
|
||||
.select(selector_id)
|
||||
.style("height", legendheight + "px")
|
||||
.style("width", legendwidth + "px")
|
||||
@@ -104,6 +102,7 @@ class ContinuousLegend extends React.Component {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.colorAccessor !== this.props.colorAccessor ||
|
||||
@@ -122,13 +121,15 @@ class ContinuousLegend extends React.Component {
|
||||
continuous(
|
||||
"#continuous_legend",
|
||||
d3
|
||||
.scaleSequential(d3.interpolateViridis)
|
||||
.scaleSequential(interpolateViridis)
|
||||
.domain(this.props.colorScale.domain())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawScale() {}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -9,7 +9,7 @@ import actions from "../../actions";
|
||||
@connect()
|
||||
class CellSetButton extends React.Component {
|
||||
set() {
|
||||
const set = _.map(this.props.crossfilter.cells.allFiltered(), "CellName");
|
||||
const set = _.map(this.props.crossfilter.allFiltered(), "name");
|
||||
|
||||
this.props.dispatch({
|
||||
type:
|
||||
@@ -18,6 +18,7 @@ class CellSetButton extends React.Component {
|
||||
data: set
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span style={{ marginRight: 10 }}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import memoize from "memoize-one";
|
||||
import { connect } from "react-redux";
|
||||
import * as globals from "../../globals";
|
||||
import styles from "./expression.css";
|
||||
@@ -19,6 +20,7 @@ class HeatmapSquare extends React.Component {
|
||||
value: ""
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const contrastColor = getContrast(
|
||||
this.props.backgroundColor
|
||||
@@ -66,6 +68,7 @@ class HeatmapRow extends React.Component {
|
||||
value: ""
|
||||
};
|
||||
}
|
||||
|
||||
handleGeneColorScaleClick(gene) {
|
||||
return () => {
|
||||
this.props.dispatch(
|
||||
@@ -75,6 +78,7 @@ class HeatmapRow extends React.Component {
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
handleSetGeneAsScatterplotX(gene) {
|
||||
return () => {
|
||||
this.props.dispatch({
|
||||
@@ -83,6 +87,7 @@ class HeatmapRow extends React.Component {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
handleSetGeneAsScatterplotY(gene) {
|
||||
return () => {
|
||||
this.props.dispatch({
|
||||
@@ -91,6 +96,7 @@ class HeatmapRow extends React.Component {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
@@ -211,7 +217,7 @@ class HeatmapRow extends React.Component {
|
||||
@connect(state => {
|
||||
return {
|
||||
differential: state.differential,
|
||||
allGeneNames: state.controls.allGeneNames
|
||||
world: state.controls.world
|
||||
};
|
||||
})
|
||||
class Heatmap extends React.Component {
|
||||
@@ -221,12 +227,18 @@ class Heatmap extends React.Component {
|
||||
value: ""
|
||||
};
|
||||
}
|
||||
|
||||
getAllGeneNames = memoize(world =>
|
||||
_.map(this.props.world.varAnnotations, "name")
|
||||
);
|
||||
|
||||
render() {
|
||||
if (!this.props.differential.diffExp)
|
||||
return <p>Select cells & compute differential to see heatmap</p>;
|
||||
|
||||
const topGenesForCellSet1 = this.props.differential.diffExp.data.celllist1;
|
||||
const topGenesForCellSet2 = this.props.differential.diffExp.data.celllist2;
|
||||
// const allGeneNames = this.getAllGeneNames(this.props.world);
|
||||
|
||||
const extent = d3.extent(
|
||||
_.union(
|
||||
|
||||
@@ -9,7 +9,9 @@ import CellSetButton from "./cellSetButtons";
|
||||
@connect(state => {
|
||||
return {
|
||||
differential: state.differential,
|
||||
crossfilter: state.controls.crossfilter
|
||||
world: state.controls.world,
|
||||
crossfilter: _.get(state.controls, "crossfilter", null),
|
||||
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null)
|
||||
};
|
||||
})
|
||||
class Expression extends React.Component {
|
||||
@@ -17,6 +19,7 @@ class Expression extends React.Component {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
handleClick(gene) {
|
||||
return () => {
|
||||
this.props.dispatch({
|
||||
@@ -25,6 +28,7 @@ class Expression extends React.Component {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
computeDiffExp() {
|
||||
this.props.dispatch(
|
||||
actions.requestDifferentialExpression(
|
||||
@@ -33,6 +37,7 @@ class Expression extends React.Component {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.differential) {
|
||||
return null;
|
||||
|
||||
@@ -24,10 +24,12 @@ import FaSave from "react-icons/lib/fa/download";
|
||||
|
||||
@connect(state => {
|
||||
return {
|
||||
cellsMetadata: state.controls.cellsMetadata,
|
||||
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
|
||||
world: state.controls.world,
|
||||
crossfilter: state.controls.crossfilter,
|
||||
responsive: state.responsive,
|
||||
crossfilter: state.controls.crossfilter
|
||||
colorRGB: _.get(state.controls, "colorRGB", null),
|
||||
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
|
||||
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null)
|
||||
};
|
||||
})
|
||||
class Graph extends React.Component {
|
||||
@@ -43,13 +45,12 @@ class Graph extends React.Component {
|
||||
colors: null
|
||||
};
|
||||
this.state = {
|
||||
drawn: false,
|
||||
svg: null,
|
||||
ctx: null,
|
||||
brush: null,
|
||||
mode: "brush"
|
||||
};
|
||||
}
|
||||
|
||||
reglDraw(regl, drawPoints, sizeBuffer, colorBuffer, pointBuffer, camera) {
|
||||
regl.clear({
|
||||
depth: 1,
|
||||
@@ -64,6 +65,7 @@ class Graph extends React.Component {
|
||||
view: camera.view()
|
||||
});
|
||||
}
|
||||
|
||||
restartReglLoop() {
|
||||
const reglRender = this.state.regl.frame(() => {
|
||||
this.reglDraw(
|
||||
@@ -83,6 +85,7 @@ class Graph extends React.Component {
|
||||
reglRender
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// setup canvas and camera
|
||||
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
|
||||
@@ -120,7 +123,16 @@ class Graph extends React.Component {
|
||||
reglRender
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
world,
|
||||
crossfilter,
|
||||
selectionUpdate,
|
||||
colorRGB,
|
||||
responsive
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
this.state.reglRender &&
|
||||
this.reglRenderState === "rendering" &&
|
||||
@@ -130,25 +142,22 @@ class Graph extends React.Component {
|
||||
this.reglRenderState = "paused";
|
||||
}
|
||||
|
||||
if (this.state.regl && this.props.crossfilter) {
|
||||
if (this.state.regl && world) {
|
||||
/* update the regl state */
|
||||
const crossfilter = this.props.crossfilter.cells;
|
||||
const cells = crossfilter.all();
|
||||
const cellCount = cells.length;
|
||||
const obsLayout = world.obsLayout;
|
||||
const cellCount = crossfilter.size();
|
||||
|
||||
// X/Y positions for each point - a cached value that only
|
||||
// changes if we have loaded entirely new cell data
|
||||
//
|
||||
if (
|
||||
!this.renderCache.positions ||
|
||||
this.props.crossfilter.cells != prevProps.crossfilter.cells
|
||||
selectionUpdate != prevProps.selectionUpdate
|
||||
) {
|
||||
if (!this.renderCache.positions)
|
||||
this.renderCache.positions = new Float32Array(2 * cellCount);
|
||||
|
||||
// d3.scaleLinear().domain([0,1]).range([-1,1])
|
||||
const glScaleX = scaleLinear([0, 1], [-1, 1]);
|
||||
// d3.scaleLinear().domain([0,1]).range([1,-1])
|
||||
const glScaleY = scaleLinear([0, 1], [1, -1]);
|
||||
|
||||
for (
|
||||
@@ -156,8 +165,8 @@ class Graph extends React.Component {
|
||||
i < cellCount;
|
||||
i++
|
||||
) {
|
||||
positions[2 * i] = glScaleX(cells[i].__x__);
|
||||
positions[2 * i + 1] = glScaleY(cells[i].__y__);
|
||||
positions[2 * i] = glScaleX(obsLayout.X[i]);
|
||||
positions[2 * i + 1] = glScaleY(obsLayout.Y[i]);
|
||||
}
|
||||
this.state.pointBuffer({
|
||||
data: this.renderCache.positions,
|
||||
@@ -171,14 +180,12 @@ class Graph extends React.Component {
|
||||
// could have changed for some other reason, but for now color is
|
||||
// the only metadata that changes client-side. If this is problematic,
|
||||
// we could add some sort of color-specific indicator to the app state.
|
||||
if (
|
||||
!this.renderCache.colors ||
|
||||
this.props.cellsMetadata != prevProps.cellsMetadata
|
||||
) {
|
||||
if (!this.renderCache.colors || colorRGB != prevProps.colorRGB) {
|
||||
const rgb = colorRGB;
|
||||
if (!this.renderCache.colors)
|
||||
this.renderCache.colors = new Float32Array(3 * cellCount);
|
||||
for (let i = 0, colors = this.renderCache.colors; i < cellCount; i++) {
|
||||
colors.set(cells[i].__colorRGB__, 3 * i);
|
||||
this.renderCache.colors = new Float32Array(3 * rgb.length);
|
||||
for (let i = 0, colors = this.renderCache.colors; i < rgb.length; i++) {
|
||||
colors.set(rgb[i], 3 * i);
|
||||
}
|
||||
this.state.colorBuffer({ data: this.renderCache.colors, dimension: 3 });
|
||||
}
|
||||
@@ -188,12 +195,8 @@ class Graph extends React.Component {
|
||||
// most property upates are due to changes driving a crossfilter
|
||||
// selection set change.
|
||||
//
|
||||
if (
|
||||
!this.renderCache.sizes ||
|
||||
this.props.crossfilter.cells != prevProps.crossfilter.cells
|
||||
) {
|
||||
if (!this.renderCache.sizes)
|
||||
this.renderCache.sizes = new Float32Array(cellCount);
|
||||
}
|
||||
crossfilter.fillByIsFiltered(this.renderCache.sizes, 4, 0.2);
|
||||
this.state.sizeBuffer({ data: this.renderCache.sizes, dimension: 1 });
|
||||
|
||||
@@ -211,12 +214,10 @@ class Graph extends React.Component {
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.responsive.height !== this.props.responsive.height ||
|
||||
prevProps.responsive.width !== this.props.responsive.width ||
|
||||
prevProps.responsive.height !== responsive.height ||
|
||||
prevProps.responsive.width !== responsive.width ||
|
||||
/* first time */
|
||||
(this.props.responsive.height &&
|
||||
this.props.responsive.width &&
|
||||
!this.state.svg)
|
||||
(responsive.height && responsive.width && !this.state.svg)
|
||||
) {
|
||||
/* clear out whatever was on the div, even if nothing, but usually the brushes etc */
|
||||
d3.select("#graphAttachPoint")
|
||||
@@ -225,12 +226,13 @@ class Graph extends React.Component {
|
||||
const { svg, brush, brushContainer } = setupSVGandBrushElements(
|
||||
this.handleBrushSelectAction.bind(this),
|
||||
this.handleBrushDeselectAction.bind(this),
|
||||
this.props.responsive,
|
||||
responsive,
|
||||
this.graphPaddingTop
|
||||
);
|
||||
this.setState({ svg, brush, brushContainer });
|
||||
}
|
||||
}
|
||||
|
||||
handleBrushSelectAction() {
|
||||
/* This conditional handles procedural brush deselect. Brush emits an event on procedural deselect because it is move: null */
|
||||
if (d3.event.sourceEvent !== null) {
|
||||
@@ -280,6 +282,7 @@ class Graph extends React.Component {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleBrushDeselectAction() {
|
||||
if (d3.event && !d3.event.selection) {
|
||||
this.props.dispatch({
|
||||
@@ -295,6 +298,7 @@ class Graph extends React.Component {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleOpacityRangeChange(e) {
|
||||
this.props.dispatch({
|
||||
type: "change opacity deselected cells in 2d graph background",
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
import styles from "./joy.css";
|
||||
|
||||
/*
|
||||
via https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3
|
||||
*/
|
||||
|
||||
var margin = { top: 30, right: 10, bottom: 30, left: 100 },
|
||||
width = 400 - margin.left - margin.right,
|
||||
height = 600 - margin.top - margin.bottom;
|
||||
|
||||
// Percent two area charts can overlap
|
||||
var overlap = 0.4;
|
||||
|
||||
var formatTime = d3.timeFormat("%I %p");
|
||||
|
||||
var x = function(d) {
|
||||
return d.time;
|
||||
},
|
||||
xScale = d3.scaleTime().range([0, width]),
|
||||
xValue = function(d) {
|
||||
return xScale(x(d));
|
||||
},
|
||||
xAxis = d3.axisBottom(xScale).tickFormat(formatTime);
|
||||
|
||||
var y = function(d) {
|
||||
return d.value;
|
||||
},
|
||||
yScale = d3.scaleLinear(),
|
||||
yValue = function(d) {
|
||||
return yScale(y(d));
|
||||
};
|
||||
|
||||
var activity = function(d) {
|
||||
return d.key;
|
||||
},
|
||||
activityScale = d3.scaleBand().range([0, height]),
|
||||
activityValue = function(d) {
|
||||
return activityScale(activity(d));
|
||||
},
|
||||
activityAxis = d3.axisLeft(activityScale);
|
||||
|
||||
var area = d3
|
||||
.area()
|
||||
.x(xValue)
|
||||
.y1(yValue);
|
||||
|
||||
var line = area.lineY1();
|
||||
|
||||
function parseTime(offset) {
|
||||
var date = new Date(2017, 0, 1); // chose an arbitrary day
|
||||
return d3.timeMinute.offset(date, offset);
|
||||
}
|
||||
|
||||
function row(d) {
|
||||
return {
|
||||
activity: d.activity,
|
||||
time: parseTime(d.time),
|
||||
value: +d.p_smooth
|
||||
};
|
||||
}
|
||||
|
||||
const drawJoy = data => {
|
||||
console.log("drawJoy: ", data);
|
||||
|
||||
var svg = d3
|
||||
.select("#joyplot")
|
||||
.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
d3.tsv(
|
||||
"https://gist.githubusercontent.com/armollica/3b5f83836c1de5cca7b1d35409a013e3/raw/783d1bbc2dd3aabbfcba83ece4a670de6f1ec371/data.tsv",
|
||||
row,
|
||||
function(error, dataFlat) {
|
||||
// Sort by time
|
||||
dataFlat.sort(function(a, b) {
|
||||
return a.time - b.time;
|
||||
});
|
||||
|
||||
var data = d3
|
||||
.nest()
|
||||
.key(function(d) {
|
||||
return d.activity;
|
||||
})
|
||||
.entries(dataFlat);
|
||||
|
||||
// Sort activities by peak activity time
|
||||
function peakTime(d) {
|
||||
var i = d3.scan(d.values, function(a, b) {
|
||||
return y(b) - y(a);
|
||||
});
|
||||
return d.values[i].time;
|
||||
}
|
||||
data.sort(function(a, b) {
|
||||
return peakTime(b) - peakTime(a);
|
||||
});
|
||||
|
||||
console.log("sorted", data);
|
||||
|
||||
xScale.domain(d3.extent(dataFlat, x));
|
||||
|
||||
activityScale.domain(
|
||||
data.map(function(d) {
|
||||
return d.key;
|
||||
})
|
||||
);
|
||||
|
||||
var areaChartHeight =
|
||||
(1 + overlap) * (height / activityScale.domain().length);
|
||||
|
||||
yScale.domain(d3.extent(dataFlat, y)).range([areaChartHeight, 0]);
|
||||
|
||||
area.y0(yScale(0));
|
||||
|
||||
var gActivity = svg
|
||||
.append("g")
|
||||
.attr("class", "activities")
|
||||
.selectAll(".activity")
|
||||
.data(data)
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", function(d) {
|
||||
return `${styles.activity} ${styles.activity["--" + d.key]}`;
|
||||
})
|
||||
.attr("transform", function(d) {
|
||||
var ty = activityValue(d) - activityScale.bandwidth() + 5;
|
||||
return "translate(0," + ty + ")";
|
||||
});
|
||||
|
||||
gActivity
|
||||
.append("path")
|
||||
.attr("class", styles.area)
|
||||
.datum(function(d) {
|
||||
return d.values;
|
||||
})
|
||||
.attr("d", area);
|
||||
|
||||
gActivity
|
||||
.append("path")
|
||||
.attr("class", styles.line)
|
||||
.datum(function(d) {
|
||||
return d.values;
|
||||
})
|
||||
.attr("d", line);
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
.attr("class", `${styles.axis} ${styles["axis--x"]}`)
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(xAxis);
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
.attr("class", `${styles.axis} ${styles["axis--activity"]}`)
|
||||
.call(activityAxis);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default drawJoy;
|
||||
@@ -1,42 +0,0 @@
|
||||
svg {
|
||||
display: block;
|
||||
/*margin: 0 auto;*/
|
||||
}
|
||||
|
||||
.axis .domain {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.axis--x text {
|
||||
fill: #999;
|
||||
}
|
||||
|
||||
.axis--x line {
|
||||
stroke: #aaa;
|
||||
}
|
||||
|
||||
.axis--activity .tick line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.axis--activity text {
|
||||
font-size: 12px;
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
/*.axis--activity .tick:nth-child(odd) text {
|
||||
fill: #222;
|
||||
}*/
|
||||
|
||||
.line {
|
||||
fill: none;
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
.area {
|
||||
fill: #448cab;
|
||||
}
|
||||
|
||||
.activity:nth-child(odd) .area {
|
||||
fill: #5ca3c1;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import styles from "./joy.css";
|
||||
import drawJoy from "./drawJoy";
|
||||
import joyParser from "./joyParser";
|
||||
|
||||
class Joy extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.data) {
|
||||
console.log("joyplot data 44", nextProps.data);
|
||||
drawJoy(joyParser(nextProps.data));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="joyplot_wrapper" style={{ marginTop: 50 }}>
|
||||
<h3> Joy </h3>
|
||||
<p>
|
||||
{" "}
|
||||
Cell expression distribution per gene & if differential expression,
|
||||
Ie., cells for cluster 5, top genes expressed by cluster 8
|
||||
</p>
|
||||
<div id="joyplot"> </div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Joy;
|
||||
@@ -1,29 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
|
||||
const joyParser = (data, count = 20) => {
|
||||
const genes = [];
|
||||
|
||||
/* setup */
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const gene = {
|
||||
key:
|
||||
data.genes[
|
||||
i
|
||||
] /* key values naming: https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3 */,
|
||||
values: []
|
||||
};
|
||||
|
||||
data.cells.forEach(cell => {
|
||||
gene.values.push({
|
||||
value: cell["e"][i]
|
||||
});
|
||||
});
|
||||
|
||||
genes.push(gene);
|
||||
}
|
||||
|
||||
return genes;
|
||||
};
|
||||
|
||||
export default joyParser;
|
||||
@@ -5,38 +5,55 @@
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import scatterplot from "./scatterplot";
|
||||
import setupScatterplot from "./setupScatterplot";
|
||||
import styles from "./scatterplot.css";
|
||||
import _regl from "regl";
|
||||
import * as d3 from "d3";
|
||||
|
||||
import mat4 from "gl-mat4";
|
||||
import fit from "canvas-fit";
|
||||
import _camera from "../../util/camera.js";
|
||||
import _regl from "regl";
|
||||
import _camera from "../../util/camera";
|
||||
|
||||
import setupScatterplot from "./setupScatterplot";
|
||||
import styles from "./scatterplot.css";
|
||||
|
||||
import _drawPoints from "./drawPointsRegl";
|
||||
import { scaleLinear } from "../../util/scaleLinear";
|
||||
|
||||
import { margin, width, height, createDimensions } from "./util";
|
||||
import { margin, width, height } from "./util";
|
||||
|
||||
@connect(state => {
|
||||
const ranges = _.get(state, "cells.cells.data.ranges", null);
|
||||
const metadata = _.get(state, "cells.cells.data.metadata", null);
|
||||
const initializeRanges = _.get(state, "initialize.data.data.ranges", null);
|
||||
const {
|
||||
world,
|
||||
crossfilter,
|
||||
scatterplotXXaccessor,
|
||||
scatterplotYYaccessor
|
||||
} = state.controls;
|
||||
const expressionX =
|
||||
world && scatterplotXXaccessor
|
||||
? state.controls.world.varDataCache[scatterplotXXaccessor]
|
||||
: null;
|
||||
const expressionY =
|
||||
world && scatterplotYYaccessor
|
||||
? state.controls.world.varDataCache[scatterplotYYaccessor]
|
||||
: null;
|
||||
|
||||
return {
|
||||
ranges,
|
||||
metadata,
|
||||
initializeRanges,
|
||||
world,
|
||||
|
||||
colorRGB: state.controls.colorRGB,
|
||||
colorAccessor: state.controls.colorAccessor,
|
||||
colorScale: state.controls.colorScale,
|
||||
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
|
||||
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
|
||||
|
||||
// Accessors are var/gene names (strings)
|
||||
scatterplotXXaccessor,
|
||||
scatterplotYYaccessor,
|
||||
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
|
||||
crossfilter: state.controls.crossfilter,
|
||||
|
||||
differential: state.differential,
|
||||
expression: state.expression
|
||||
|
||||
expressionX,
|
||||
expressionY,
|
||||
|
||||
crossfilter,
|
||||
// updated whenever the crossfilter selection is updated
|
||||
selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null)
|
||||
};
|
||||
})
|
||||
class Scatterplot extends React.Component {
|
||||
@@ -46,9 +63,6 @@ class Scatterplot extends React.Component {
|
||||
this.axes = false;
|
||||
this.state = {
|
||||
svg: null,
|
||||
// ctx: null,
|
||||
axes: null,
|
||||
dimensions: null,
|
||||
xScale: null,
|
||||
yScale: null
|
||||
};
|
||||
@@ -57,19 +71,10 @@ class Scatterplot extends React.Component {
|
||||
componentDidMount() {
|
||||
const { svg } = setupScatterplot(width, height, margin);
|
||||
let scales;
|
||||
const { expressionX, expressionY } = this.props;
|
||||
|
||||
/* if we've already got the data, user clicked back and forth between tabs, so render the scatterplot */
|
||||
if (
|
||||
this.props.expression &&
|
||||
this.props.expression.data &&
|
||||
this.props.scatterplotXXaccessor &&
|
||||
this.props.scatterplotYYaccessor
|
||||
) {
|
||||
scales = this.setupScales(
|
||||
this.props.expression,
|
||||
this.props.scatterplotXXaccessor,
|
||||
this.props.scatterplotYYaccessor
|
||||
);
|
||||
if (svg && expressionX && expressionY) {
|
||||
scales = Scatterplot.setupScales(expressionX, expressionY);
|
||||
this.drawAxesSVG(scales.xScale, scales.yScale, svg);
|
||||
}
|
||||
|
||||
@@ -115,115 +120,101 @@ class Scatterplot extends React.Component {
|
||||
colorBuffer
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
svg,
|
||||
xScale,
|
||||
yScale,
|
||||
regl,
|
||||
pointBuffer,
|
||||
colorBuffer,
|
||||
sizeBuffer
|
||||
} = this.state;
|
||||
const {
|
||||
world,
|
||||
crossfilter,
|
||||
scatterplotXXaccessor,
|
||||
scatterplotYYaccessor,
|
||||
expressionX,
|
||||
expressionY,
|
||||
colorRGB
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
this.state.svg &&
|
||||
this.state.xScale &&
|
||||
this.state.yScale &&
|
||||
this.props.scatterplotXXaccessor &&
|
||||
this.props.scatterplotYYaccessor &&
|
||||
(this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
||||
this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor || // was CLU now FTH1 etc
|
||||
world &&
|
||||
svg &&
|
||||
xScale &&
|
||||
yScale &&
|
||||
scatterplotXXaccessor &&
|
||||
scatterplotYYaccessor &&
|
||||
(scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
||||
scatterplotYYaccessor !== prevProps.scatterplotYYaccessor || // was CLU now FTH1 etc
|
||||
!this.axes) // clicked off the tab and back again, rerender
|
||||
) {
|
||||
this.drawAxesSVG(this.state.xScale, this.state.yScale, this.state.svg);
|
||||
this.drawAxesSVG(xScale, yScale, svg);
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.metadata &&
|
||||
this.state.regl &&
|
||||
this.state.pointBuffer &&
|
||||
this.state.colorBuffer &&
|
||||
this.state.sizeBuffer &&
|
||||
this.props.expression.data &&
|
||||
this.props.expression.data.genes &&
|
||||
this.props.scatterplotXXaccessor &&
|
||||
this.props.scatterplotYYaccessor &&
|
||||
this.state.xScale &&
|
||||
this.state.yScale
|
||||
world &&
|
||||
regl &&
|
||||
pointBuffer &&
|
||||
colorBuffer &&
|
||||
sizeBuffer &&
|
||||
expressionX &&
|
||||
expressionY &&
|
||||
scatterplotXXaccessor &&
|
||||
scatterplotYYaccessor &&
|
||||
xScale &&
|
||||
yScale
|
||||
) {
|
||||
const crossfilter = this.props.crossfilter.cells;
|
||||
const data = this.props.expression.data;
|
||||
const cells = data.cells;
|
||||
const genes = data.genes;
|
||||
const cellCount = cells.length;
|
||||
const positions = new Float32Array(2 * cellCount);
|
||||
const colors = new Float32Array(3 * cellCount);
|
||||
const sizes = new Float32Array(cellCount);
|
||||
const cellCount = expressionX.length;
|
||||
const positionsBuf = new Float32Array(2 * cellCount);
|
||||
const colorsBuf = new Float32Array(3 * cellCount);
|
||||
const sizesBuf = new Float32Array(cellCount);
|
||||
|
||||
// d3.scaleLinear().domain([0, width]).range([-0.95, 0.95])
|
||||
const glScaleX = scaleLinear([0, width], [-0.95, 0.95]);
|
||||
|
||||
// d3.scaleLinear().domain([0, height]).range([-1, 1])
|
||||
const glScaleY = scaleLinear([0, height], [-1, 1]);
|
||||
|
||||
const geneXXaccessorIndex = genes.indexOf(
|
||||
this.props.scatterplotXXaccessor
|
||||
);
|
||||
const geneYYaccessorIndex = genes.indexOf(
|
||||
this.props.scatterplotYYaccessor
|
||||
);
|
||||
|
||||
/*
|
||||
Construct Vectors
|
||||
*/
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const cell = cells[i];
|
||||
|
||||
positions[2 * i] = glScaleX(
|
||||
this.state.xScale(cell.e[geneXXaccessorIndex])
|
||||
); /* scale each point first to the window as we calculate extents separately below, so no need to repeat */
|
||||
positions[2 * i + 1] = glScaleY(
|
||||
this.state.yScale(cell.e[geneYYaccessorIndex])
|
||||
);
|
||||
for (let i = 0; i < cellCount; i += 1) {
|
||||
positionsBuf[2 * i] = glScaleX(xScale(expressionX[i]));
|
||||
positionsBuf[2 * i + 1] = glScaleY(yScale(expressionY[i]));
|
||||
}
|
||||
|
||||
for (let i = 0; i < cellCount; i++) {
|
||||
const metadata = this.props.metadata[i];
|
||||
colors.set(metadata.__colorRGB__, 3 * i);
|
||||
for (let i = 0; i < cellCount; i += 1) {
|
||||
colorsBuf.set(colorRGB[i], 3 * i);
|
||||
}
|
||||
|
||||
crossfilter.fillByIsFiltered(sizes, 4, 0.2);
|
||||
crossfilter.fillByIsFiltered(sizesBuf, 4, 0.2);
|
||||
|
||||
this.state.pointBuffer({ data: positions, dimension: 2 });
|
||||
this.state.colorBuffer({ data: colors, dimension: 3 });
|
||||
this.state.sizeBuffer({ data: sizes, dimension: 1 });
|
||||
pointBuffer({ data: positionsBuf, dimension: 2 });
|
||||
colorBuffer({ data: colorsBuf, dimension: 3 });
|
||||
sizeBuffer({ data: sizesBuf, dimension: 1 });
|
||||
this.count = cellCount;
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.expression &&
|
||||
this.props.expression.data &&
|
||||
this.props.scatterplotXXaccessor &&
|
||||
this.props.scatterplotYYaccessor &&
|
||||
(this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
||||
this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor)
|
||||
expressionX &&
|
||||
expressionY &&
|
||||
(scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
|
||||
scatterplotYYaccessor !== prevProps.scatterplotYYaccessor)
|
||||
) {
|
||||
const scales = this.setupScales(
|
||||
this.props.expression,
|
||||
this.props.scatterplotXXaccessor,
|
||||
this.props.scatterplotYYaccessor
|
||||
);
|
||||
const scales = Scatterplot.setupScales(expressionX, expressionY);
|
||||
this.setState(scales);
|
||||
}
|
||||
}
|
||||
setupScales(expression, scatterplotXXaccessor, scatterplotYYaccessor) {
|
||||
|
||||
static setupScales(expressionX, expressionY) {
|
||||
const xScale = d3
|
||||
.scaleLinear()
|
||||
.domain(
|
||||
d3.extent(expression.data.cells, (cell, i) => {
|
||||
return cell.e[expression.data.genes.indexOf(scatterplotXXaccessor)];
|
||||
})
|
||||
)
|
||||
.domain(d3.extent(expressionX))
|
||||
.range([0, width]);
|
||||
|
||||
const yScale = d3
|
||||
.scaleLinear()
|
||||
.domain(
|
||||
d3.extent(expression.data.cells, cell => {
|
||||
return cell.e[expression.data.genes.indexOf(scatterplotYYaccessor)];
|
||||
})
|
||||
)
|
||||
.domain(d3.extent(expressionY))
|
||||
.range([height, 0]);
|
||||
|
||||
return {
|
||||
@@ -231,15 +222,19 @@ class Scatterplot extends React.Component {
|
||||
yScale
|
||||
};
|
||||
}
|
||||
|
||||
drawAxesSVG(xScale, yScale, svg) {
|
||||
const { scatterplotYYaccessor, scatterplotXXaccessor } = this.props;
|
||||
svg.selectAll("*").remove();
|
||||
|
||||
// the axes are much cleaner and easier now. No need to rotate and orient the axis, just call axisBottom, axisLeft etc.
|
||||
var xAxis = d3.axisBottom().scale(xScale);
|
||||
// the axes are much cleaner and easier now. No need to rotate and orient
|
||||
// the axis, just call axisBottom, axisLeft etc.
|
||||
const xAxis = d3.axisBottom().scale(xScale);
|
||||
|
||||
var yAxis = d3.axisLeft().scale(yScale);
|
||||
const yAxis = d3.axisLeft().scale(yScale);
|
||||
|
||||
// adding axes is also simpler now, just translate x-axis to (0,height) and it's alread defined to be a bottom axis.
|
||||
// adding axes is also simpler now, just translate x-axis to (0,height)
|
||||
// and it's alread defined to be a bottom axis.
|
||||
svg
|
||||
.append("g")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
@@ -259,7 +254,7 @@ class Scatterplot extends React.Component {
|
||||
.attr("x", 10)
|
||||
.attr("y", 10)
|
||||
.attr("class", "label")
|
||||
.text(this.props.scatterplotYYaccessor);
|
||||
.text(scatterplotYYaccessor);
|
||||
|
||||
svg
|
||||
.append("text")
|
||||
@@ -267,7 +262,7 @@ class Scatterplot extends React.Component {
|
||||
.attr("y", height - 10)
|
||||
.attr("text-anchor", "end")
|
||||
.attr("class", "label")
|
||||
.text(this.props.scatterplotXXaccessor);
|
||||
.text(scatterplotXXaccessor);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -41,6 +41,8 @@ export const brightBlue = "#4a90e2";
|
||||
export const brightGreen = "#A2D729";
|
||||
export const darkGreen = "#448C4D";
|
||||
|
||||
export const defaultCellColor = "rgb(0,0,0,1)";
|
||||
|
||||
export const tiniestFontSize = 12;
|
||||
|
||||
export const bolder = 700;
|
||||
@@ -48,8 +50,6 @@ export const bolder = 700;
|
||||
export let API = {
|
||||
// prefix: "http://api.clustering.czi.technology/api/",
|
||||
//prefix: "http://tabulamuris.cxg.czi.technology/api/",
|
||||
// prefix: "http://pbmc3k.cxg.czi.technology/api/",
|
||||
// prefix: "http://pbmc33k.cxg.czi.technology/api/",
|
||||
|
||||
prefix: "http://api-staging.clustering.czi.technology/api/",
|
||||
version: "v0.1/"
|
||||
|
||||
@@ -1,138 +1,115 @@
|
||||
// jshint esversion: 6
|
||||
import uri from "urijs";
|
||||
import * as globals from "../globals";
|
||||
import _ from "lodash";
|
||||
import { parseRGB } from "../util/parseRGB";
|
||||
import * as d3 from "d3";
|
||||
import { interpolateViridis } from "d3-scale-chromatic";
|
||||
import * as globals from "../globals";
|
||||
import { parseRGB } from "../util/parseRGB";
|
||||
|
||||
/*
|
||||
https://medium.com/@jacobp100/you-arent-using-redux-middleware-enough-94ffe991e6
|
||||
storeInstance => functionToCallWithAnActionThatWillSendItToTheNextMiddleware => actionThatDispatchWasCalledWith => valueToUseAsTheReturnValueOfTheDispatchCall
|
||||
storeInstance =>
|
||||
functionToCallWithAnActionThatWillSendItToTheNextMiddleware =>
|
||||
actionThatDispatchWasCalledWith =>
|
||||
valueToUseAsTheReturnValueOfTheDispatchCall
|
||||
*/
|
||||
|
||||
/*
|
||||
What this file does:
|
||||
|
||||
1. fire a filter action anywhere in the app
|
||||
2. ** this middleware checks to see the state of all the currently selected filters, including the new one
|
||||
3. ** create updated selection from a copy of all the cells presently on the client (this may be a subset of 'all')
|
||||
4. ** append that new selection to the action so that it magically appears in the reducer just because the action was fired
|
||||
2. ** this middleware checks to see the state of all the currently selected filters,
|
||||
including the new one
|
||||
3. ** create updated selection from a copy of all the cells presently on the client
|
||||
(this may be a subset of 'all')
|
||||
4. ** append that new selection to the action so that it magically appears in the reducer
|
||||
just because the action was fired
|
||||
|
||||
This is nice because we keep a lot of filtering business logic centralized (what it means in practice to be selected)
|
||||
This is nice because we keep a lot of filtering business logic centralized
|
||||
(what it means in practice to be selected)
|
||||
*/
|
||||
|
||||
const updateCellColorsMiddleware = store => {
|
||||
return next => {
|
||||
return action => {
|
||||
const s = store.getState();
|
||||
const updateCellColorsMiddleware = store => next => action => {
|
||||
const s = store.getState();
|
||||
|
||||
/* this is a hardcoded map of the things we need to keep an eye on and update global cell selection in response to */
|
||||
const filterJustChanged =
|
||||
action.type === "color by expression" ||
|
||||
action.type === "color by continuous metadata" ||
|
||||
action.type === "color by categorical metadata";
|
||||
/*
|
||||
this is a hardcoded map of the things we need to keep an eye on and update
|
||||
global cell selection in response to
|
||||
*/
|
||||
const filterJustChanged =
|
||||
action.type === "color by expression" ||
|
||||
action.type === "color by continuous metadata" ||
|
||||
action.type === "color by categorical metadata";
|
||||
|
||||
if (!filterJustChanged || !s.controls.cellsMetadata) {
|
||||
return next(
|
||||
action
|
||||
); /* if the cells haven't loaded or the action wasn't a color change, bail */
|
||||
}
|
||||
if (!filterJustChanged || !s.controls.world.obsAnnotations) {
|
||||
return next(
|
||||
action
|
||||
); /* if the cells haven't loaded or the action wasn't a color change, bail */
|
||||
}
|
||||
|
||||
let cellsMetadataWithUpdatedColors = s.controls.cellsMetadata.slice(0);
|
||||
let colorScale;
|
||||
const { obsAnnotations } = s.controls.world;
|
||||
let colorScale;
|
||||
const colorsByName = new Array(obsAnnotations.length);
|
||||
const colorsByRGB = new Array(obsAnnotations.length);
|
||||
|
||||
/*
|
||||
in plain language...
|
||||
/*
|
||||
in plain language...
|
||||
(a) once the cells have loaded.
|
||||
(b) each time a user changes a color control we need to update cellsMetadata colors
|
||||
This is available to all the draw functions as world.colorName[index] or world.colorRGB[index]
|
||||
*/
|
||||
|
||||
(a) once the cells have loaded.
|
||||
(b) each time a user changes a color control we need to update cellsMetadata colors
|
||||
if (action.type === "color by categorical metadata") {
|
||||
colorScale = d3.scaleOrdinal().range(globals.ordinalColors);
|
||||
|
||||
This is available to all the draw functions as cell["__color__"] and cell["__colorRGB__"]
|
||||
*/
|
||||
for (let i = 0; i < obsAnnotations.length; i += 1) {
|
||||
const obs = obsAnnotations[i];
|
||||
const c = colorScale(obs[action.colorAccessor]);
|
||||
colorsByName[i] = c;
|
||||
colorsByRGB[i] = parseRGB(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === "color by categorical metadata") {
|
||||
colorScale = d3.scaleOrdinal().range(globals.ordinalColors);
|
||||
if (action.type === "color by continuous metadata") {
|
||||
colorScale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, action.rangeMaxForColorAccessor])
|
||||
.range([1, 0]);
|
||||
|
||||
for (let i = 0; i < cellsMetadataWithUpdatedColors.length; i++) {
|
||||
const cell = cellsMetadataWithUpdatedColors[i];
|
||||
let c = colorScale(cell[action.colorAccessor]);
|
||||
cell.__color__ = c;
|
||||
cell.__colorRGB__ = parseRGB(c);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < obsAnnotations.length; i += 1) {
|
||||
const obs = obsAnnotations[i];
|
||||
const c = interpolateViridis(colorScale(obs[action.colorAccessor]));
|
||||
colorsByName[i] = c;
|
||||
colorsByRGB[i] = parseRGB(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === "color by continuous metadata") {
|
||||
colorScale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, action.rangeMaxForColorAccessor])
|
||||
.range([1, 0]);
|
||||
if (action.type === "color by expression") {
|
||||
const { gene, data } = action;
|
||||
const expression = data[gene]; // Float32Array
|
||||
colorScale = d3
|
||||
.scaleLinear()
|
||||
.domain([_.min(expression), _.max(expression)])
|
||||
.range([
|
||||
1,
|
||||
0
|
||||
]); /* invert viridis... probably pass this scale through to others */
|
||||
|
||||
_.each(cellsMetadataWithUpdatedColors, (cell, i) => {
|
||||
let c = interpolateViridis(colorScale(cell[action.colorAccessor]));
|
||||
cellsMetadataWithUpdatedColors[i]["__color__"] = c;
|
||||
cellsMetadataWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c);
|
||||
});
|
||||
}
|
||||
for (let i = 0, len = expression.length; i < len; i += 1) {
|
||||
const c = interpolateViridis(colorScale(expression[i]));
|
||||
colorsByName[i] = c;
|
||||
colorsByRGB[i] = parseRGB(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (action.type === "color by expression") {
|
||||
const indexOfGene = 0; /* we only get one, this comes from server as needed now */
|
||||
/*
|
||||
append the result of all the filters to the action the user just triggered
|
||||
*/
|
||||
const modifiedAction = Object.assign({}, action, {
|
||||
colors: { name: colorsByName, rgb: colorsByRGB },
|
||||
colorScale
|
||||
});
|
||||
|
||||
const expressionMap = {};
|
||||
/*
|
||||
converts [{cellname: cell123, e}, {}]
|
||||
|
||||
expressionMap = {
|
||||
cell123: [123, 2],
|
||||
cell789: [0, 8]
|
||||
}
|
||||
*/
|
||||
_.each(action.data.data.cells, cell => {
|
||||
/* this action is coming directly from the server */
|
||||
expressionMap[cell.cellname] = cell.e;
|
||||
});
|
||||
|
||||
const minExpressionCell = _.minBy(action.data.data.cells, cell => {
|
||||
return cell.e[indexOfGene];
|
||||
});
|
||||
|
||||
const maxExpressionCell = _.maxBy(action.data.data.cells, cell => {
|
||||
return cell.e[indexOfGene];
|
||||
});
|
||||
|
||||
// console.log('middle', action, expressionMap, minExpressionCell)
|
||||
|
||||
colorScale = d3
|
||||
.scaleLinear()
|
||||
.domain([
|
||||
minExpressionCell.e[indexOfGene],
|
||||
maxExpressionCell.e[indexOfGene]
|
||||
])
|
||||
.range([
|
||||
1,
|
||||
0
|
||||
]); /* invert viridis... probably pass this scale through to others */
|
||||
|
||||
_.each(cellsMetadataWithUpdatedColors, (cell, i) => {
|
||||
let c = interpolateViridis(
|
||||
colorScale(expressionMap[cell.CellName][indexOfGene])
|
||||
);
|
||||
cellsMetadataWithUpdatedColors[i]["__color__"] = c;
|
||||
cellsMetadataWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
append the result of all the filters to the action the user just triggered
|
||||
*/
|
||||
let modifiedAction = Object.assign({}, action, {
|
||||
cellsMetadataWithUpdatedColors,
|
||||
colorScale
|
||||
});
|
||||
|
||||
return next(modifiedAction);
|
||||
};
|
||||
};
|
||||
return next(modifiedAction);
|
||||
};
|
||||
|
||||
export default updateCellColorsMiddleware;
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
import uri from "urijs";
|
||||
import * as globals from "../globals";
|
||||
|
||||
/*
|
||||
XXX: this file should be obsolete. We just need to complete the refactoring
|
||||
of parallel.js and it can be removed entirely.
|
||||
|
||||
It is currently not in use - the middleware constructor does not include include it
|
||||
*/
|
||||
|
||||
/*
|
||||
https://medium.com/@jacobp100/you-arent-using-redux-middleware-enough-94ffe991e6
|
||||
storeInstance => functionToCallWithAnActionThatWillSendItToTheNextMiddleware => actionThatDispatchWasCalledWith => valueToUseAsTheReturnValueOfTheDispatchCall
|
||||
*/
|
||||
|
||||
/*
|
||||
What this file does:
|
||||
|
||||
1. fire a filter action anywhere in the app
|
||||
2. ** this middleware checks to see the state of all the currently selected filters, including the new one
|
||||
3. ** create updated selection from a copy of all the cells presently on the client (this may be a subset of 'all')
|
||||
4. ** append that new selection to the action so that it magically appears in the reducer just because the action was fired
|
||||
|
||||
This is nice because we keep a lot of filtering business logic centralized (what it means in practice to be selected)
|
||||
*/
|
||||
|
||||
const updateCellSelectionMiddleware = store => {
|
||||
return next => {
|
||||
return action => {
|
||||
const s = store.getState();
|
||||
|
||||
/* this is a hardcoded map of the things we need to keep an eye on and update global cell selection in response to */
|
||||
const filterJustChanged =
|
||||
action.type === "continuous selection using parallel coords brushing" ||
|
||||
action.type === "continuous metadata histogram brush" ||
|
||||
action.type === "graph brush selection change" ||
|
||||
action.type === "graph brush deselect" ||
|
||||
action.type === "categorical metadata filter deselect" ||
|
||||
action.type === "categorical metadata filter select" ||
|
||||
action.type === "categorical metadata filter none of these" ||
|
||||
action.type === "categorical metadata filter all of these";
|
||||
|
||||
if (!filterJustChanged || !s.controls.cellsMetadata) {
|
||||
return next(
|
||||
action
|
||||
); /* if the cells haven't loaded or the action wasn't a filter, bail */
|
||||
}
|
||||
|
||||
/*
|
||||
- make a FRESH copy of all of the cells
|
||||
- metadata has cellname and index, and that's all we ever need to reference cell info
|
||||
*/
|
||||
let newSelection = s.controls.cellsMetadata.slice(0);
|
||||
// _.forEach(newSelection, cell => (cell.__selected__ = true));
|
||||
for (let i = 0; i < newSelection.length; i++) {
|
||||
newSelection[i].__selected__ = true;
|
||||
}
|
||||
|
||||
/*
|
||||
in plain language...
|
||||
|
||||
(a) once the cells have loaded.
|
||||
(b) each time a user changes ANY control we need to update cellsMetadata
|
||||
there are two states:
|
||||
|
||||
1. control state we already know about (state.foo)
|
||||
2. control states that override states we already know about (action.foo applied instead of state.foo)
|
||||
|
||||
*/
|
||||
|
||||
if (
|
||||
(action.type ===
|
||||
"continuous selection using parallel coords brushing" &&
|
||||
s.controls.continuousSelection) ||
|
||||
s.controls.continuousSelection
|
||||
) {
|
||||
_.each(newSelection, (cell, i) => {
|
||||
const cellExtentsAreWithinContinuousSelectionBounds = s.controls.continuousSelection.every(
|
||||
active => {
|
||||
// test if point is within extents for each active brush
|
||||
return active.dimension.type.within(
|
||||
cell[active.dimension.key],
|
||||
active.extent,
|
||||
active.dimension
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (!cellExtentsAreWithinContinuousSelectionBounds) {
|
||||
newSelection[i]["__selected__"] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let modifiedAction = Object.assign({}, action, {
|
||||
newSelection
|
||||
}); /* append the result of all the filters to the action the user just triggered */
|
||||
|
||||
return next(modifiedAction);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export default updateCellSelectionMiddleware;
|
||||
@@ -1,41 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
const Cells = (
|
||||
state = {
|
||||
cells: null /* world */,
|
||||
loading: null,
|
||||
error: null,
|
||||
|
||||
allCells: null /* this comes from cells endpoint, this is universe */
|
||||
},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case "request cells started":
|
||||
return Object.assign({}, state, {
|
||||
loading: true,
|
||||
error: null
|
||||
});
|
||||
case "request cells success":
|
||||
return Object.assign({}, state, {
|
||||
error: null,
|
||||
loading: false,
|
||||
cells: action.data, // most recently loaded cells
|
||||
|
||||
/* Universe - initialize once */
|
||||
allCells: state.allCells ? state.allCells : action.data
|
||||
});
|
||||
case "request cells error":
|
||||
return Object.assign({}, state, {
|
||||
loading: false,
|
||||
error: action.data
|
||||
});
|
||||
case "reset graph":
|
||||
return Object.assign({}, state, {
|
||||
cells: state.allCells
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default Cells;
|
||||
440
client/src/reducers/controls.js
vendored
440
client/src/reducers/controls.js
vendored
@@ -1,117 +1,44 @@
|
||||
// jshint esversion: 6
|
||||
|
||||
import _ from "lodash";
|
||||
import { World, kvCache } from "../util/stateManager";
|
||||
import { parseRGB } from "../util/parseRGB";
|
||||
import { createSchemaByDataSniffing } from "../util/schema";
|
||||
import crossfilter from "../util/typedCrossfilter";
|
||||
import Crossfilter from "../util/typedCrossfilter";
|
||||
import * as globals from "../globals";
|
||||
|
||||
// Deduce the correct crossfilter dimension type from a metadata
|
||||
// schema description.
|
||||
//
|
||||
function deduceDimensionType(attributes, fieldName) {
|
||||
let dimensionType;
|
||||
if (attributes.type === "string") {
|
||||
dimensionType = "enum";
|
||||
} else if (attributes.type === "int") {
|
||||
dimensionType = Int32Array;
|
||||
} else if (attributes.type === "float") {
|
||||
dimensionType = Float32Array;
|
||||
} else {
|
||||
console.error(
|
||||
`Warning - REST API returned unknown metadata schema (${
|
||||
attributes.type
|
||||
}) for field ${fieldName}.`
|
||||
);
|
||||
// skip it - we don't know what to do with this type
|
||||
}
|
||||
return dimensionType;
|
||||
}
|
||||
|
||||
// Create view state from /cells data response. Used both during a data
|
||||
// load and during a graph reset.
|
||||
//
|
||||
function createViewState(schema, data) {
|
||||
const cellsMetadata = data.metadata.slice(0);
|
||||
|
||||
/*
|
||||
construct a copy of the ranges object that only has categorical
|
||||
replace all counts with bool flags
|
||||
ie., everything starts out checked
|
||||
we mutate this map in the actions below
|
||||
*/
|
||||
const categoricalAsBooleansMap = {};
|
||||
_.each(data.ranges, (value, key) => {
|
||||
if (
|
||||
key !== "CellName" &&
|
||||
value.options /* it's categorical, it has options instead of ranges */
|
||||
) {
|
||||
function createCategoricalAsBooleansMap(world) {
|
||||
const res = {};
|
||||
_.each(world.summary.obs, (value, key) => {
|
||||
if (value.options) {
|
||||
const optionsAsBooleans = {};
|
||||
_.each(value.options, (_value, _key) => {
|
||||
optionsAsBooleans[_key] = true;
|
||||
});
|
||||
categoricalAsBooleansMap[key] = optionsAsBooleans;
|
||||
res[key] = optionsAsBooleans;
|
||||
}
|
||||
});
|
||||
|
||||
const graph = data.graph;
|
||||
_.each(cellsMetadata, (cell, idx) => {
|
||||
cell.__cellIndex__ = idx;
|
||||
cell.__color__ =
|
||||
"rgba(0,0,0,1)"; /* initial color for all cells in all charts */
|
||||
cell.__colorRGB__ = parseRGB(cell.__color__);
|
||||
cell.__x__ = graph[idx][1];
|
||||
cell.__y__ = graph[idx][2];
|
||||
});
|
||||
|
||||
// Build the selection crossfilter.
|
||||
//
|
||||
let cellsCrossfilter = crossfilter(cellsMetadata);
|
||||
let cellsDimensionsMap = {};
|
||||
cellsDimensionsMap.x = cellsCrossfilter.dimension(r => r.__x__, Float32Array);
|
||||
cellsDimensionsMap.y = cellsCrossfilter.dimension(r => r.__y__, Float32Array);
|
||||
|
||||
// Now walk the schema and make an appropriate dimension for each
|
||||
// metadata field. This is a simplistic mapping, and could be
|
||||
// optmized to use smaller scalars (to save memory) or larger
|
||||
// floating point where precision is needed.
|
||||
//
|
||||
_.forEach(schema, (attributes, key) => {
|
||||
if (key !== "CellName") {
|
||||
const dimensionType = deduceDimensionType(attributes, key);
|
||||
if (dimensionType) {
|
||||
cellsDimensionsMap[key] = cellsCrossfilter.dimension(
|
||||
r => r[key],
|
||||
dimensionType
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
cellsMetadata,
|
||||
crossfilter: {
|
||||
cells: cellsCrossfilter,
|
||||
dimensionMap: cellsDimensionsMap
|
||||
},
|
||||
categoricalAsBooleansMap
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
const Controls = (
|
||||
state = {
|
||||
/* Universe - all cells known to us. Set once, during initial load */
|
||||
_ranges: null /* this comes from initialize, this is universe */,
|
||||
allGeneNames: null,
|
||||
allCells: null /* this comes from cells endpoint, this is universe */,
|
||||
allCellsMetadata: null /* this comes from cells endpoint, and is just the metadata for universe */,
|
||||
allCellsMetadataMap: null,
|
||||
// data loading flag
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
/* View / World - all cells currently being displayed. May be a subset of Universe. */
|
||||
cellsMetadata: null,
|
||||
crossfilter: null /* the current user selection state */,
|
||||
universe: null,
|
||||
|
||||
// all of the data + selection state
|
||||
world: null,
|
||||
colorName: null,
|
||||
colorRGB: null,
|
||||
categoricalAsBooleansMap: null,
|
||||
crossfilter: null,
|
||||
dimensionMap: null,
|
||||
|
||||
colorAccessor: null,
|
||||
colorScale: null,
|
||||
|
||||
opacityForDeselectedCells: 0.2,
|
||||
graphBrushSelection: null,
|
||||
continuousSelection: null,
|
||||
@@ -123,118 +50,161 @@ const Controls = (
|
||||
},
|
||||
action
|
||||
) => {
|
||||
/*
|
||||
For now, log anything looking like an error to the console.
|
||||
*/
|
||||
if (action.error || /error/i.test(action.type)) {
|
||||
console.error(action.error);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
/**********************************
|
||||
Keep a copy of 'universe'
|
||||
***********************************/
|
||||
case "initialize success": {
|
||||
if (!action.data.data.schema) {
|
||||
console.error("Warning - REST API omitted schema description.");
|
||||
}
|
||||
return Object.assign({}, state, {
|
||||
_ranges: action.data.data.ranges,
|
||||
allGeneNames: action.data.data.genes,
|
||||
schema: action.data.data.schema
|
||||
});
|
||||
/*****************************************************
|
||||
Initialization, World/Universe management
|
||||
and data loading.
|
||||
******************************************************/
|
||||
case "initial data load start": {
|
||||
return { ...state, loading: true };
|
||||
}
|
||||
case "request cells success": {
|
||||
// If we don't have a schema (bad server!), fake it by inferring
|
||||
// important fields from the ranges element.
|
||||
//
|
||||
if (!state.schema) {
|
||||
state.schema = createSchemaByDataSniffing(action.data.data.ranges);
|
||||
}
|
||||
|
||||
/* Set viewable world to the provided cell data */
|
||||
const viewState = createViewState(state.schema, action.data.data);
|
||||
return Object.assign({}, state, {
|
||||
/* Universe - initialize once */
|
||||
allCells: state.allCells ? state.allCells : action.data,
|
||||
allCellsMetadata: state.allCellsMetadata
|
||||
? state.allCellsMetadata
|
||||
: viewState.cellsMetadata,
|
||||
allCellsMetadataMap: state.allCellsMetadataMap
|
||||
? state.allCellsMetadataMap
|
||||
: _.keyBy(viewState.cellsMetadata, "CellName"),
|
||||
|
||||
/* World */
|
||||
...viewState,
|
||||
|
||||
graphBrushSelection: null /* if we are getting new cells from the server, the layout (probably? definitely?) just changed, so this is now irrelevant, and we WILL need to call a function to reset state of this kind when cells success happens */
|
||||
});
|
||||
case "initial data load complete (universe exists)":
|
||||
case "reset World to eq Universe": {
|
||||
/* first light - create world & other data-driven defaults */
|
||||
const { universe } = action;
|
||||
const world = World.createWorldFromEntireUniverse(universe);
|
||||
const colorName = new Array(universe.nObs).fill(globals.defaultCellColor);
|
||||
const colorRGB = _.map(colorName, c => parseRGB(c));
|
||||
const categoricalAsBooleansMap = createCategoricalAsBooleansMap(world);
|
||||
const crossfilter = Crossfilter(world.obsAnnotations);
|
||||
const dimensionMap = World.createObsDimensionMap(crossfilter, world);
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
error: null,
|
||||
universe,
|
||||
world,
|
||||
colorName,
|
||||
colorRGB,
|
||||
categoricalAsBooleansMap,
|
||||
crossfilter,
|
||||
dimensionMap,
|
||||
colorAccessor: null
|
||||
};
|
||||
}
|
||||
/* * * * * * * * * * * * * * * * * *
|
||||
User events
|
||||
* * * * * * * * * * * * * * * * * */
|
||||
case "reset graph": {
|
||||
/* Reset viewable world to the entire Universe */
|
||||
const viewState = createViewState(state.schema, state.allCells.data);
|
||||
return Object.assign({}, state, {
|
||||
...viewState
|
||||
});
|
||||
case "set World to current selection": {
|
||||
/* Set viewable world to be the currently selected data */
|
||||
const world = World.createWorldFromCurrentSelection(
|
||||
action.universe,
|
||||
action.world,
|
||||
action.crossfilter
|
||||
);
|
||||
const colorName = new Array(world.nObs).fill(globals.defaultCellColor);
|
||||
const colorRGB = _.map(colorName, c => parseRGB(c));
|
||||
const categoricalAsBooleansMap = createCategoricalAsBooleansMap(world);
|
||||
const crossfilter = Crossfilter(world.obsAnnotations);
|
||||
const dimensionMap = World.createObsDimensionMap(crossfilter, world);
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
error: null,
|
||||
world,
|
||||
colorName,
|
||||
colorRGB,
|
||||
categoricalAsBooleansMap,
|
||||
crossfilter,
|
||||
dimensionMap,
|
||||
colorAccessor: null
|
||||
};
|
||||
}
|
||||
case "parallel coordinates axes have been drawn": {
|
||||
return Object.assign({}, state, {
|
||||
axesHaveBeenDrawn: true
|
||||
});
|
||||
}
|
||||
case "continuous selection using parallel coords brushing": {
|
||||
return Object.assign({}, state, {
|
||||
continuousSelection: action.data,
|
||||
crossfilter: {
|
||||
...state.crossfilter
|
||||
case "expression load success": {
|
||||
const { world, universe } = state;
|
||||
let universeVarDataCache = universe.varDataCache;
|
||||
let worldVarDataCache = world.varDataCache;
|
||||
_.forEach(action.expressionData, (val, key) => {
|
||||
universeVarDataCache = kvCache.set(universeVarDataCache, key, val);
|
||||
if (kvCache.get(worldVarDataCache, key) === undefined) {
|
||||
worldVarDataCache = kvCache.set(
|
||||
worldVarDataCache,
|
||||
key,
|
||||
World.subsetVarData(world, universe, val)
|
||||
);
|
||||
}
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
universe: {
|
||||
...universe,
|
||||
varDataCache: universeVarDataCache
|
||||
},
|
||||
world: {
|
||||
...world,
|
||||
varDataCache: worldVarDataCache
|
||||
}
|
||||
};
|
||||
}
|
||||
case "expression load error":
|
||||
case "initial data load error": {
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
error: action.error
|
||||
};
|
||||
}
|
||||
|
||||
/*******************************
|
||||
User Events
|
||||
*******************************/
|
||||
case "parallel coordinates axes have been drawn": {
|
||||
return {
|
||||
...state,
|
||||
axesHaveBeenDrawn: true
|
||||
};
|
||||
}
|
||||
case "continuous selection using parallel coords brushing": {
|
||||
return {
|
||||
...state,
|
||||
continuousSelection: action.data
|
||||
};
|
||||
}
|
||||
case "graph brush selection change": {
|
||||
state.crossfilter.dimensionMap.x.filterRange([
|
||||
state.dimensionMap.x.filterRange([
|
||||
action.brushCoords.northwest[0],
|
||||
action.brushCoords.southeast[0]
|
||||
]);
|
||||
state.crossfilter.dimensionMap.y.filterRange([
|
||||
state.dimensionMap.y.filterRange([
|
||||
action.brushCoords.southeast[1],
|
||||
action.brushCoords.northwest[1]
|
||||
]);
|
||||
return Object.assign({}, state, {
|
||||
graphBrushSelection: action.brushCoords,
|
||||
crossfilter: {
|
||||
...state.crossfilter
|
||||
}
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
graphBrushSelection: action.brushCoords
|
||||
};
|
||||
}
|
||||
case "graph brush deselect": {
|
||||
state.crossfilter.dimensionMap.x.filterAll();
|
||||
state.crossfilter.dimensionMap.y.filterAll();
|
||||
return Object.assign({}, state, {
|
||||
graphBrushSelection: null,
|
||||
crossfilter: {
|
||||
...state.crossfilter
|
||||
}
|
||||
});
|
||||
state.dimensionMap.x.filterAll();
|
||||
state.dimensionMap.y.filterAll();
|
||||
return {
|
||||
...state,
|
||||
graphBrushSelection: null
|
||||
};
|
||||
}
|
||||
case "continuous metadata histogram brush": {
|
||||
// action.selection: metadata name being selected
|
||||
// action.range: filter range, or null if deselected
|
||||
if (!action.range) {
|
||||
state.crossfilter.dimensionMap[action.selection].filterAll();
|
||||
state.dimensionMap[action.selection].filterAll();
|
||||
} else {
|
||||
state.crossfilter.dimensionMap[action.selection].filterRange(
|
||||
action.range
|
||||
);
|
||||
state.dimensionMap[action.selection].filterRange(action.range);
|
||||
}
|
||||
return Object.assign({}, state, {
|
||||
crossfilter: {
|
||||
...state.crossfilter
|
||||
}
|
||||
});
|
||||
return { ...state };
|
||||
}
|
||||
case "change opacity deselected cells in 2d graph background":
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
opacityForDeselectedCells: action.data
|
||||
});
|
||||
};
|
||||
|
||||
/*******************************
|
||||
Categorical metadata
|
||||
*******************************/
|
||||
Categorical metadata
|
||||
*******************************/
|
||||
case "categorical metadata filter select": {
|
||||
const newCategoricalAsBooleansMap = {
|
||||
...state.categoricalAsBooleansMap,
|
||||
@@ -244,7 +214,7 @@ const Controls = (
|
||||
}
|
||||
};
|
||||
// update the filter for the one category that changed state
|
||||
state.crossfilter.dimensionMap[action.metadataField].filterEnum(
|
||||
state.dimensionMap[action.metadataField].filterEnum(
|
||||
_.filter(
|
||||
_.map(
|
||||
newCategoricalAsBooleansMap[action.metadataField],
|
||||
@@ -252,12 +222,10 @@ const Controls = (
|
||||
)
|
||||
)
|
||||
);
|
||||
return Object.assign({}, state, {
|
||||
categoricalAsBooleansMap: newCategoricalAsBooleansMap,
|
||||
crossfilter: {
|
||||
...state.crossfilter
|
||||
}
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
categoricalAsBooleansMap: newCategoricalAsBooleansMap
|
||||
};
|
||||
}
|
||||
case "categorical metadata filter deselect": {
|
||||
const newCategoricalAsBooleansMap = {
|
||||
@@ -268,7 +236,7 @@ const Controls = (
|
||||
}
|
||||
};
|
||||
// update the filter for the one category that changed state
|
||||
state.crossfilter.dimensionMap[action.metadataField].filterEnum(
|
||||
state.dimensionMap[action.metadataField].filterEnum(
|
||||
_.filter(
|
||||
_.map(
|
||||
newCategoricalAsBooleansMap[action.metadataField],
|
||||
@@ -276,12 +244,10 @@ const Controls = (
|
||||
)
|
||||
)
|
||||
);
|
||||
return Object.assign({}, state, {
|
||||
categoricalAsBooleansMap: newCategoricalAsBooleansMap,
|
||||
crossfilter: {
|
||||
...state.crossfilter
|
||||
}
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
categoricalAsBooleansMap: newCategoricalAsBooleansMap
|
||||
};
|
||||
}
|
||||
case "categorical metadata filter none of these": {
|
||||
const newCategoricalAsBooleansMap = {
|
||||
@@ -293,13 +259,11 @@ const Controls = (
|
||||
c[k] = false;
|
||||
}
|
||||
);
|
||||
state.crossfilter.dimensionMap[action.metadataField].filterNone();
|
||||
return Object.assign({}, state, {
|
||||
categoricalAsBooleansMap: newCategoricalAsBooleansMap,
|
||||
crossfilter: {
|
||||
...state.crossfilter
|
||||
}
|
||||
});
|
||||
state.dimensionMap[action.metadataField].filterNone();
|
||||
return {
|
||||
...state,
|
||||
categoricalAsBooleansMap: newCategoricalAsBooleansMap
|
||||
};
|
||||
}
|
||||
case "categorical metadata filter all of these": {
|
||||
const newCategoricalAsBooleansMap = {
|
||||
@@ -311,54 +275,50 @@ const Controls = (
|
||||
c[k] = true;
|
||||
}
|
||||
);
|
||||
state.crossfilter.dimensionMap[action.metadataField].filterAll();
|
||||
return Object.assign({}, state, {
|
||||
categoricalAsBooleansMap: newCategoricalAsBooleansMap,
|
||||
crossfilter: {
|
||||
...state.crossfilter
|
||||
}
|
||||
});
|
||||
state.dimensionMap[action.metadataField].filterAll();
|
||||
return {
|
||||
...state,
|
||||
categoricalAsBooleansMap: newCategoricalAsBooleansMap
|
||||
};
|
||||
}
|
||||
|
||||
/*******************************
|
||||
Color Scale
|
||||
*******************************/
|
||||
case "color by continuous metadata":
|
||||
return Object.assign({}, state, {
|
||||
colorAccessor: action.colorAccessor,
|
||||
cellsMetadata:
|
||||
action.cellsMetadataWithUpdatedColors /* this comes from middleware */,
|
||||
colorScale: action.colorScale
|
||||
});
|
||||
case "color by expression":
|
||||
return Object.assign({}, state, {
|
||||
colorAccessor: action.gene,
|
||||
cellsMetadata:
|
||||
action.cellsMetadataWithUpdatedColors /* this comes from middleware */,
|
||||
colorScale: action.colorScale
|
||||
});
|
||||
Color Scale
|
||||
*******************************/
|
||||
case "color by categorical metadata":
|
||||
return Object.assign({}, state, {
|
||||
colorAccessor:
|
||||
action.colorAccessor /* pass the scale through additionally, and it's a legend! */,
|
||||
cellsMetadata:
|
||||
action.cellsMetadataWithUpdatedColors /* this comes from middleware */,
|
||||
case "color by continuous metadata": {
|
||||
return {
|
||||
...state,
|
||||
colorName: action.colors.name,
|
||||
colorRGB: action.colors.rgb,
|
||||
colorAccessor: action.colorAccessor,
|
||||
colorScale: action.colorScale
|
||||
});
|
||||
case "store current cell selection as differential set 1":
|
||||
return Object.assign({}, state, {
|
||||
__storedStateForCelllist1__: action.data
|
||||
});
|
||||
};
|
||||
}
|
||||
case "color by expression": {
|
||||
return {
|
||||
...state,
|
||||
colorName: action.colors.name,
|
||||
colorRGB: action.colors.rgb,
|
||||
colorAccessor: action.gene,
|
||||
colorScale: action.colorScale
|
||||
};
|
||||
}
|
||||
|
||||
/*******************************
|
||||
Scatterplot
|
||||
*******************************/
|
||||
Scatterplot
|
||||
*******************************/
|
||||
case "set scatterplot x":
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
scatterplotXXaccessor: action.data
|
||||
});
|
||||
};
|
||||
case "set scatterplot y":
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
scatterplotYYaccessor: action.data
|
||||
});
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -11,29 +11,34 @@ const Differential = (
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case "request differential expression started":
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
error: null
|
||||
});
|
||||
};
|
||||
case "request differential expression success":
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
loading: false,
|
||||
diffExp: action.data
|
||||
});
|
||||
};
|
||||
case "request differential expression error":
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
error: action.data
|
||||
});
|
||||
};
|
||||
case "store current cell selection as differential set 1":
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
celllist1: action.data
|
||||
});
|
||||
};
|
||||
case "store current cell selection as differential set 2":
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
celllist2: action.data
|
||||
});
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
const Expression = (
|
||||
state = {
|
||||
data: null,
|
||||
loading: null,
|
||||
error: null
|
||||
},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case "get expression started":
|
||||
return Object.assign({}, state, {
|
||||
loading: true,
|
||||
error: null
|
||||
});
|
||||
case "get expression success":
|
||||
return Object.assign({}, state, {
|
||||
error: null,
|
||||
loading: false,
|
||||
data: action.data.data
|
||||
});
|
||||
case "get expression error":
|
||||
return Object.assign({}, state, {
|
||||
data: null,
|
||||
loading: false,
|
||||
error: action.data
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default Expression;
|
||||
@@ -1,36 +1,23 @@
|
||||
// jshint esversion: 6
|
||||
import { combineReducers, createStore, applyMiddleware } from "redux";
|
||||
import thunk from "redux-thunk";
|
||||
import updateURLMiddleware from "../middleware/updateURLMiddleware";
|
||||
// import updateCellSelectionMiddleware from "../middleware/updateCellSelectionMiddleware";
|
||||
import updateCellColors from "../middleware/updateCellColors";
|
||||
|
||||
import thunk from "redux-thunk";
|
||||
|
||||
import initialize from "./initialize";
|
||||
import cells from "./cells";
|
||||
import expression from "./expression";
|
||||
import controls from "./controls";
|
||||
import differential from "./differential";
|
||||
import responsive from "./responsive";
|
||||
import controls from "./controls";
|
||||
|
||||
const Reducer = combineReducers({
|
||||
initialize,
|
||||
cells,
|
||||
expression,
|
||||
responsive,
|
||||
controls,
|
||||
differential,
|
||||
responsive
|
||||
differential
|
||||
});
|
||||
|
||||
let store = createStore(
|
||||
const store = createStore(
|
||||
Reducer,
|
||||
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
|
||||
applyMiddleware(
|
||||
thunk,
|
||||
updateURLMiddleware,
|
||||
// updateCellSelectionMiddleware,
|
||||
updateCellColors
|
||||
)
|
||||
applyMiddleware(thunk, updateURLMiddleware, updateCellColors)
|
||||
);
|
||||
|
||||
export default store;
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
const Initialize = (
|
||||
state = {
|
||||
data: null,
|
||||
loading: null,
|
||||
error: null
|
||||
},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case "initialize started":
|
||||
return Object.assign({}, state, {
|
||||
loading: true,
|
||||
error: null
|
||||
});
|
||||
case "initialize success":
|
||||
return Object.assign({}, state, {
|
||||
error: null,
|
||||
loading: false,
|
||||
data: action.data
|
||||
});
|
||||
case "initialize error":
|
||||
return Object.assign({}, state, {
|
||||
data: null,
|
||||
loading: false,
|
||||
error: action.data
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default Initialize;
|
||||
@@ -8,10 +8,11 @@ const Responsive = (
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case "window resize":
|
||||
return Object.assign({}, state, {
|
||||
return {
|
||||
...state,
|
||||
width: action.data.width,
|
||||
height: action.data.height
|
||||
});
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
19
client/src/util/stateManager/index.js
Normal file
19
client/src/util/stateManager/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// jshint esversion: 6
|
||||
|
||||
/*
|
||||
Model manager providing an abstraction for the use of the reducer code.
|
||||
This module provides several buckets of functionality:
|
||||
- schema and config driven tranformation of the dataframe wire protocol
|
||||
into a format that is easy for the UI code to use.
|
||||
- manage the universe/world abstraction:
|
||||
+ universe: all of the server-provided, read-only data
|
||||
+ world: subset of universe
|
||||
- lazy access and caching of dataframe contents as needed
|
||||
|
||||
This is all VERY tightly integrated with reducers and actions, and
|
||||
exists to support those concepts.
|
||||
*/
|
||||
|
||||
export * as Universe from "./universe";
|
||||
export * as World from "./world";
|
||||
export * as kvCache from "./keyvalcache";
|
||||
72
client/src/util/stateManager/keyvalcache.js
Normal file
72
client/src/util/stateManager/keyvalcache.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// jshint esversion: 6
|
||||
import _ from "lodash";
|
||||
|
||||
/*
|
||||
Very simple key/value cache for use by World & Universe.
|
||||
|
||||
* constructor(lowWatermark, cachekey):
|
||||
- lowWatermark defines the number of cache elements below which
|
||||
flushing will not occur.
|
||||
- minTTL defines minimum time in MS that cache entries will live.
|
||||
A value of -1 disables automatic flushing (flush() can still
|
||||
be called by external user).
|
||||
- cachekey is a key that will be assigned to any value to track age
|
||||
* set() - add a key/val pair.
|
||||
* get() - get a value or undefined if not present.
|
||||
* flush(minAgeMs) - flush cache entries in excess of lowWatermark if those
|
||||
entries are older than minAgeMs.
|
||||
|
||||
*/
|
||||
|
||||
const cachePrivateKey = "__kvcachekey__";
|
||||
|
||||
function create(lowWatermark = 32, minTTL = 1000) {
|
||||
return {
|
||||
[cachePrivateKey]: {
|
||||
lowWatermark,
|
||||
minTTL
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function get(kvcache, key) {
|
||||
const val = kvcache[key];
|
||||
if (val) {
|
||||
val[cachePrivateKey] = Date.now();
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
function set(kvcache, key, val) {
|
||||
const newKvCache = { ...kvcache };
|
||||
newKvCache[key] = val;
|
||||
val[cachePrivateKey] = Date.now();
|
||||
flush(newKvCache, newKvCache[cachePrivateKey].minTTL);
|
||||
return newKvCache;
|
||||
}
|
||||
|
||||
/*
|
||||
Flush elements from cache IF cache size is greater than lowWatermark, and
|
||||
those elements are older than minAgeMS
|
||||
*/
|
||||
function flush(kvcache, minAgeMs = 0) {
|
||||
if (minAgeMs < 0) return kvcache;
|
||||
const eol = Date.now() - minAgeMs;
|
||||
const { lowWatermark } = kvcache[cachePrivateKey];
|
||||
const keys = _(kvcache)
|
||||
.keys()
|
||||
.filter(k => k !== cachePrivateKey)
|
||||
.filter(k => kvcache[k][cachePrivateKey] < eol)
|
||||
.sortBy([k => kvcache[k][cachePrivateKey]])
|
||||
.value();
|
||||
|
||||
if (keys.length > lowWatermark) {
|
||||
const numKeysToDelete = keys.length - lowWatermark;
|
||||
const keysToDelete = _.slice(keys, 0, numKeysToDelete);
|
||||
_.forEach(keysToDelete, k => delete kvcache[k]);
|
||||
}
|
||||
|
||||
return kvcache;
|
||||
}
|
||||
|
||||
export { create, get, set, flush };
|
||||
252
client/src/util/stateManager/universe.js
Normal file
252
client/src/util/stateManager/universe.js
Normal file
@@ -0,0 +1,252 @@
|
||||
// jshint esversion: 6
|
||||
|
||||
import _ from "lodash";
|
||||
import * as kvCache from "./keyvalcache";
|
||||
|
||||
/*
|
||||
This module implements functions that support storage of "Universe",
|
||||
aka all of the var/obs data and annotations.
|
||||
|
||||
These functions are used exclusively by the actions and reducers to
|
||||
build an internal POJO for use by the rendering components.
|
||||
*/
|
||||
|
||||
/*
|
||||
Cherry pick from /api/v0.1 response format to make somethign similar
|
||||
to the v0.2 schema, which we use for internal interfaces.
|
||||
*/
|
||||
function RESTv01ResponseToSchema(response) {
|
||||
/*
|
||||
Annotation schemas in V02 (our target) look like:
|
||||
|
||||
annotations: {
|
||||
obs: [
|
||||
{ name: "name", type: "string" },
|
||||
{ name: "num_reads", type: "int32" },
|
||||
{
|
||||
name: "clusters",
|
||||
type: "categorical",
|
||||
categories=[ 99, 1, "unknown cluster" ]
|
||||
},
|
||||
{ name: "QScore", type: "float32" }
|
||||
],
|
||||
var: [
|
||||
{ "name": "name", "type": "string" },
|
||||
{ "name": "gene", "type": "string" }
|
||||
]
|
||||
}
|
||||
|
||||
In V01, our source, it looks like:
|
||||
|
||||
"schema": {
|
||||
"CellName": {
|
||||
"displayname": "Name",
|
||||
"include": true,
|
||||
"type": "string",
|
||||
"variabletype": "categorical"
|
||||
},
|
||||
"Cluster_2d": {
|
||||
"displayname": "Cluster2d",
|
||||
"include": true,
|
||||
"type": "string",
|
||||
"variabletype": "categorical"
|
||||
},
|
||||
"ERCC_reads": {
|
||||
"displayname": "ERCC Reads",
|
||||
"include": true,
|
||||
"type": "int",
|
||||
"variabletype": "continuous"
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
Mapping between the two assumes:
|
||||
- V01 only has schema for observations
|
||||
- CellName is mapped to 'name'
|
||||
- type conversion: float->float32, int->int32, string->string
|
||||
|
||||
*/
|
||||
return {
|
||||
annotations: {
|
||||
obs: _.map(response.data.schema, (val, key) => {
|
||||
const name = key === "CellName" ? "name" : key;
|
||||
let { type } = val;
|
||||
if (type === "int") {
|
||||
type = "int32";
|
||||
}
|
||||
if (type === "float") {
|
||||
type = "float32";
|
||||
}
|
||||
return {
|
||||
name,
|
||||
type
|
||||
};
|
||||
}),
|
||||
var: [{ name: "name", type: "string" }]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function RESTv01ResponseToVarAnnotations(response) {
|
||||
/*
|
||||
v0.1 initialize response contains 'genes' - names of all genes
|
||||
in order.
|
||||
*/
|
||||
return _.map(response.data.genes, (g, i) => ({ __varIndex__: i, name: g }));
|
||||
}
|
||||
|
||||
function RESTv01ResponseToObsAnnotations(response) {
|
||||
/*
|
||||
v0.1 format for metadata:
|
||||
metadata: [ { key: val, key: val, ... }, ... ]
|
||||
|
||||
Target format is essentially the same, except the CellName key becomes name.
|
||||
*/
|
||||
return _.map(response.data.metadata, (c, i) => ({
|
||||
__obsIndex__: i,
|
||||
name: c.CellName,
|
||||
...c
|
||||
}));
|
||||
}
|
||||
|
||||
function RESTv01ResponseToLayout(obsAnnotations, response) {
|
||||
/*
|
||||
v0.1 format for the graph is:
|
||||
[ [ 'cellname', x, y ], [ 'cellname', x, y, ], ... ]
|
||||
|
||||
NOTE XXX: this code does not assume any particular array ordering in the V0.1
|
||||
response. But for Universe initial load, the layout will be in the same
|
||||
order as annotations, so this extra work isn't really necessary.
|
||||
*/
|
||||
|
||||
const obsAnnotationsByName = _.keyBy(obsAnnotations, "name");
|
||||
const { graph } = response.data;
|
||||
const layout = {
|
||||
X: new Float32Array(graph.length),
|
||||
Y: new Float32Array(graph.length)
|
||||
};
|
||||
|
||||
for (let i = 0; i < graph.length; i += 1) {
|
||||
const [name, x, y] = graph[i];
|
||||
const anno = obsAnnotationsByName[name];
|
||||
const idx = anno.__obsIndex__;
|
||||
layout.X[idx] = x;
|
||||
layout.Y[idx] = y;
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
function finalize(universe) {
|
||||
/* A bit of sanity checking! */
|
||||
const { nObs, nVar } = universe;
|
||||
if (
|
||||
nObs !== universe.obsAnnotations.length ||
|
||||
nObs !== universe.obsLayout.X.length ||
|
||||
nObs !== universe.obsLayout.Y.length ||
|
||||
nVar !== universe.varAnnotations.length
|
||||
) {
|
||||
throw new Error("Universe dimensionality mismatch - failed to load");
|
||||
}
|
||||
|
||||
universe.obsNameToIndexMap = _.transform(
|
||||
universe.obsAnnotations,
|
||||
(acc, value, idx) => {
|
||||
acc[value.name] = idx;
|
||||
},
|
||||
{}
|
||||
);
|
||||
universe.varNameToIndexMap = _.transform(
|
||||
universe.varAnnotations,
|
||||
(acc, value, idx) => {
|
||||
acc[value.name] = idx;
|
||||
},
|
||||
{}
|
||||
);
|
||||
universe.finalized = true;
|
||||
return universe;
|
||||
}
|
||||
|
||||
function templateUniverse() {
|
||||
/* default universe template */
|
||||
const VarDataCacheLowWatermark = 32;
|
||||
const VarDataCacheTTLMs = 1000;
|
||||
|
||||
return {
|
||||
api: "0.1",
|
||||
finalized: true, // XXX: may not be needed
|
||||
|
||||
nObs: 0,
|
||||
nVar: 0,
|
||||
schema: {},
|
||||
|
||||
/*
|
||||
Annotations
|
||||
*/
|
||||
obsAnnotations: [] /* all obs annotations, by obs index */,
|
||||
varAnnotations: [] /* all var annotations, by var index */,
|
||||
obsNameToIndexMap: {} /* reverse map 'name' to index */,
|
||||
varNameToIndexMap: {} /* reverse map 'name' to index */,
|
||||
|
||||
obsLayout: { X: [], Y: [] } /* xy layout */,
|
||||
|
||||
varDataCache: kvCache.create(
|
||||
VarDataCacheLowWatermark,
|
||||
VarDataCacheTTLMs
|
||||
) /* cache of var data (expression) */
|
||||
};
|
||||
}
|
||||
|
||||
export function createUniverseFromRESTv01Response(initResponse, cellsResponse) {
|
||||
/*
|
||||
build & return universe from a REST 0.1 /init and /cells response
|
||||
*/
|
||||
|
||||
const universe = templateUniverse();
|
||||
|
||||
/* extract information from init OTA response */
|
||||
universe.schema = RESTv01ResponseToSchema(initResponse);
|
||||
universe.varAnnotations = RESTv01ResponseToVarAnnotations(initResponse);
|
||||
universe.nVar = universe.varAnnotations.length;
|
||||
|
||||
/* extract information fron cells REST json response */
|
||||
/*
|
||||
NOTE: this code *assumes* that cell order in data.metadata and data.graph
|
||||
are the same. TODO: error checking.
|
||||
*/
|
||||
universe.obsAnnotations = RESTv01ResponseToObsAnnotations(cellsResponse);
|
||||
universe.nObs = universe.obsAnnotations.length;
|
||||
universe.obsLayout = RESTv01ResponseToLayout(
|
||||
universe.obsAnnotations,
|
||||
cellsResponse
|
||||
);
|
||||
|
||||
return finalize(universe);
|
||||
}
|
||||
|
||||
export function convertExpressionRESTv01ToObject(universe, response) {
|
||||
/*
|
||||
v0.1 ota looks like:
|
||||
{
|
||||
genes: [ "name1", "name2", ... ],
|
||||
cells: [
|
||||
{ cellname: 'cell1', e: [ 3, 4, n, x, y, ... ] },
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
convert expression to a simple Float32Array, and return
|
||||
[ [geneName, array], [geneName, array], ... ]
|
||||
*/
|
||||
const result = {};
|
||||
const { genes, cells } = response.data;
|
||||
for (let idx = 0; idx < genes.length; idx += 1) {
|
||||
const gene = genes[idx];
|
||||
const data = new Float32Array(universe.nObs);
|
||||
for (let c = 0; c < cells.length; c += 1) {
|
||||
const obsIndex = universe.obsNameToIndexMap[cells[c].cellname];
|
||||
data[obsIndex] = cells[c].e[idx];
|
||||
}
|
||||
result[gene] = data;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
315
client/src/util/stateManager/world.js
Normal file
315
client/src/util/stateManager/world.js
Normal file
@@ -0,0 +1,315 @@
|
||||
// jshint esversion: 6
|
||||
|
||||
import _ from "lodash";
|
||||
import * as kvCache from "./keyvalcache";
|
||||
|
||||
/*
|
||||
World is a subset of universe. Most code should use world, and should
|
||||
(generally) not use Universe. World contains any per-obs or per-var data
|
||||
that must be consisstent acorss the app when we view/manipulate subsets
|
||||
of Universe.
|
||||
|
||||
Private API indicated by leading underscore in key name (eg, _foo). Anything else
|
||||
is public.
|
||||
|
||||
World contains several public keys, obsAnnotations, and obsLayout, which are
|
||||
arrays contianing information about an OBS in the same order/offset. In
|
||||
other words, world.obsAnnotations[0] and world.obsLayout.X[0] refer to the same
|
||||
obs/cell.
|
||||
|
||||
* obsAnnotations:
|
||||
|
||||
obsAnnotations will return an array of objects. Each object contains all annotation
|
||||
values for a given observation/cell, keyed by annotation name, PLUS a key
|
||||
'__cellId__', containing a REST API ID for this obs/cell (referred to as the
|
||||
obsIndex in the REST 0.2 spec or cellIndex in the 0.1 spec.
|
||||
|
||||
Example: [ { __cellId__: 99, cluster: 'blue', numReads: 93933 } ]
|
||||
|
||||
NOTE: world.obsAnnotation should be identical to the old state.cells value,
|
||||
EXCEPT that
|
||||
* __cellIndex__ renamed to __obsIndex__
|
||||
* __x__ and __y__ are now in world.obsLayout
|
||||
* __color__ and __colorRBG__ should be moved to controls reducer
|
||||
|
||||
* obsLayout:
|
||||
|
||||
obsLayout will return an object containing two arrays, containing X and Y
|
||||
coordinates respectively.
|
||||
|
||||
Example: { X: [ 0.33, 0.23, ... ], Y: [ 0.8, 0.777, ... ]}
|
||||
|
||||
* crossfilter - a crossfilter object across world.obsAnnotations
|
||||
|
||||
* dimensionMap - an object mapping annotation names to dimensions on
|
||||
the crossfilter
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
Summary information for each annotation, keyed by annotation name.
|
||||
Value will be an object, containing either 'range' or 'options' object,
|
||||
depending on the annotation schema type (categorical or continuous).
|
||||
|
||||
Summarize for BOTH obs and var annotations. Result format:
|
||||
|
||||
{
|
||||
obs: {
|
||||
annotation_name: { ... },
|
||||
...
|
||||
},
|
||||
var: {
|
||||
annotation_name: { ... },
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
Example:
|
||||
{
|
||||
"Splice_sites_Annotated": {
|
||||
"range": {
|
||||
"min": 26,
|
||||
"max": 1075869
|
||||
}
|
||||
},
|
||||
"Selection": {
|
||||
"options": {
|
||||
"Astrocytes(HEPACAM)": 714,
|
||||
"Endothelial(BSC)": 123,
|
||||
"Oligodendrocytes(GC)": 294,
|
||||
"Neurons(Thy1)": 685,
|
||||
"Microglia(CD45)": 1108,
|
||||
"Unpanned": 665
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
function summarizeAnnotations(schema, obsAnnotations) {
|
||||
/*
|
||||
Build and return obs/var summary using any annotation in the schema
|
||||
*/
|
||||
const obsSummary = _(schema.annotations.obs)
|
||||
.keyBy("name")
|
||||
.mapValues(anno => {
|
||||
const { name, type } = anno;
|
||||
const continuous = type === "int32" || type === "float32";
|
||||
|
||||
if (!continuous) {
|
||||
return {
|
||||
options: _.countBy(obsAnnotations, name)
|
||||
};
|
||||
}
|
||||
|
||||
if (continuous) {
|
||||
let min = Number.POSITIVE_INFINITY;
|
||||
let max = Number.NEGATIVE_INFINITY;
|
||||
_.forEach(obsAnnotations, obs => {
|
||||
const val = Number(obs[name]);
|
||||
min = val < min ? val : min;
|
||||
max = val > max ? val : max;
|
||||
});
|
||||
return { range: { min, max } };
|
||||
}
|
||||
|
||||
throw new Error("incomprehensible schema");
|
||||
})
|
||||
.value();
|
||||
|
||||
const varSummary = {}; // TODO XXX - not currently used, so skip it
|
||||
|
||||
return {
|
||||
obs: obsSummary,
|
||||
var: varSummary
|
||||
};
|
||||
}
|
||||
|
||||
function templateWorld() {
|
||||
const VarDataCacheLowWatermark = 32;
|
||||
const VarDataCacheTTLMs = 1000;
|
||||
|
||||
return {
|
||||
// map from universe obsIndex to world offset.
|
||||
// Undefined / null indicates identity mapping.
|
||||
worldObsIndex: null,
|
||||
|
||||
/* schema/version related */
|
||||
api: null,
|
||||
schema: null,
|
||||
nObs: 0,
|
||||
nVar: 0,
|
||||
|
||||
/* annotations */
|
||||
obsAnnotations: null,
|
||||
varAnnotations: null,
|
||||
|
||||
/* layout of graph */
|
||||
obsLayout: null,
|
||||
|
||||
/* derived data summaries XXX: consider exploding in place */
|
||||
summary: null,
|
||||
|
||||
varDataCache: kvCache.create(
|
||||
VarDataCacheLowWatermark,
|
||||
VarDataCacheTTLMs
|
||||
) /* cache of var data (expression) */
|
||||
};
|
||||
}
|
||||
|
||||
export function createWorldFromEntireUniverse(universe) {
|
||||
if (!universe.finalized) {
|
||||
throw new Error("World can't be created from an partial Universe");
|
||||
}
|
||||
|
||||
const world = templateWorld();
|
||||
|
||||
// map from the universe obsIndex to our world offset.
|
||||
// undefined/null indicates identity map.
|
||||
world.worldObsIndex = null;
|
||||
|
||||
/*
|
||||
public interface follows
|
||||
*/
|
||||
|
||||
/* Schema related */
|
||||
world.api = universe.api;
|
||||
world.schema = universe.schema;
|
||||
world.nObs = universe.nObs;
|
||||
world.nVar = universe.nVar;
|
||||
|
||||
/* annotations */
|
||||
world.obsAnnotations = universe.obsAnnotations;
|
||||
world.varAnnotations = universe.varAnnotations;
|
||||
|
||||
/* layout and display characteristics */
|
||||
world.obsLayout = universe.obsLayout;
|
||||
|
||||
/* derived data & summaries */
|
||||
world.summary = summarizeAnnotations(world.schema, world.obsAnnotations);
|
||||
|
||||
return world;
|
||||
}
|
||||
|
||||
export function createWorldFromCurrentSelection(universe, world, crossfilter) {
|
||||
const newWorld = templateWorld();
|
||||
|
||||
/* these don't change as only OBS are selected in our current implementation */
|
||||
newWorld.api = world.api;
|
||||
newWorld.nVar = world.nVar;
|
||||
newWorld.schema = world.schema;
|
||||
newWorld.varAnnotations = world.varAnnotations;
|
||||
|
||||
/*
|
||||
Subset world from universe based upon world's current selection. Only those
|
||||
fields which are subset by observation selection/filtering need to be updated.
|
||||
*/
|
||||
const numSelected = crossfilter.countFiltered();
|
||||
|
||||
/*
|
||||
Create a world which is based upon current selection
|
||||
*/
|
||||
newWorld.nObs = numSelected;
|
||||
newWorld.obsAnnotations = new Array(numSelected);
|
||||
newWorld.obsLayout = {
|
||||
X: new Array(numSelected),
|
||||
Y: new Array(numSelected)
|
||||
};
|
||||
newWorld.worldObsIndex = new Array(universe.nObs);
|
||||
|
||||
for (let i = 0, sel = 0; i < world.nObs; i += 1) {
|
||||
if (crossfilter.isElementFiltered(i)) {
|
||||
newWorld.obsAnnotations[sel] = world.obsAnnotations[i];
|
||||
newWorld.obsLayout.X[sel] = world.obsLayout.X[i];
|
||||
newWorld.obsLayout.Y[sel] = world.obsLayout.Y[i];
|
||||
sel += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// build index to our world offset
|
||||
newWorld.worldObsIndex.fill(-1); // default - aka unused
|
||||
for (let i = 0; i < newWorld.nObs; i += 1) {
|
||||
newWorld.worldObsIndex[newWorld.obsAnnotations[i].__obsIndex__] = i;
|
||||
}
|
||||
|
||||
newWorld.summary = summarizeAnnotations(
|
||||
newWorld.schema,
|
||||
newWorld.obsAnnotations
|
||||
);
|
||||
return newWorld;
|
||||
}
|
||||
|
||||
/*
|
||||
Deduce the correct crossfilter dimension type from a metadata
|
||||
schema description.
|
||||
*/
|
||||
function deduceDimensionType(attributes, fieldName) {
|
||||
let dimensionType;
|
||||
if (attributes.type === "string") {
|
||||
dimensionType = "enum";
|
||||
} else if (attributes.type === "int32") {
|
||||
dimensionType = Int32Array;
|
||||
} else if (attributes.type === "float32") {
|
||||
dimensionType = Float32Array;
|
||||
} else {
|
||||
/*
|
||||
Currently not supporting boolean and categorical types.
|
||||
*/
|
||||
console.error(
|
||||
`Warning - REST API returned unknown metadata schema (${
|
||||
attributes.type
|
||||
}) for field ${fieldName}.`
|
||||
);
|
||||
// skip it - we don't know what to do with this type
|
||||
}
|
||||
return dimensionType;
|
||||
}
|
||||
|
||||
export function createObsDimensionMap(crossfilter, world) {
|
||||
/*
|
||||
create and return a crossfilter dimension for every obs annotation
|
||||
for which we have a supported type.
|
||||
*/
|
||||
const { schema, obsLayout, worldObsIndex } = world;
|
||||
|
||||
const dimensionMap = _.transform(
|
||||
schema.annotations.obs,
|
||||
(result, anno) => {
|
||||
const dimType = deduceDimensionType(anno, anno.name);
|
||||
if (dimType) {
|
||||
result[anno.name] = crossfilter.dimension(r => r[anno.name], dimType);
|
||||
} // else ignore the annotation
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
/*
|
||||
Add crossfilter dimensions allowing filtering on layout
|
||||
*/
|
||||
const worldIndex = worldObsIndex ? idx => worldObsIndex[idx] : idx => idx;
|
||||
dimensionMap.x = crossfilter.dimension(
|
||||
r => obsLayout.X[worldIndex(r.__obsIndex__)],
|
||||
Float32Array
|
||||
);
|
||||
dimensionMap.y = crossfilter.dimension(
|
||||
r => obsLayout.Y[worldIndex(r.__obsIndex__)],
|
||||
Float32Array
|
||||
);
|
||||
|
||||
return dimensionMap;
|
||||
}
|
||||
|
||||
function worldEqUniverse(world, universe) {
|
||||
return world.obsAnnotations === universe.obsAnnotations;
|
||||
}
|
||||
|
||||
export function subsetVarData(world, universe, varData) {
|
||||
// If world === universe, just return the entire varData array
|
||||
if (worldEqUniverse(world, universe)) {
|
||||
return varData;
|
||||
}
|
||||
|
||||
const newVarData = new Float32Array(world.nObs);
|
||||
for (let i = 0; i < world.nObs; i += 1) {
|
||||
newVarData[i] = varData[world.obsAnnotations[i].__obsIndex__];
|
||||
}
|
||||
return newVarData;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
"use strict";
|
||||
// jshint esversion: 6
|
||||
/* eslint no-bitwise: "off" */
|
||||
|
||||
// BitArray is a 2D bitarray with size [length, nBitWidth].
|
||||
// Each bit is referred to as a `dimension`. Dimensions may be
|
||||
@@ -29,7 +29,7 @@ class BitArray {
|
||||
// Fixed for the life of this object.
|
||||
this.length = length;
|
||||
|
||||
// Bitarray width. width is always greater than 32*dimensionCount.
|
||||
// Bitarray width. width is always greater than dimensionCount/32.
|
||||
this.width = 1; // underlying number of 32 bit arrays
|
||||
this.dimensionCount = 0; // num allocated dimensions
|
||||
|
||||
@@ -48,10 +48,16 @@ class BitArray {
|
||||
//
|
||||
countAllOnes() {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.width; i++) {
|
||||
const bitmask = this.bitmask[i];
|
||||
for (let j = i * this.length, len = j + this.length; j < len; j++) {
|
||||
if (this.bitarray[i * this.length + j] === bitmask) count++;
|
||||
const { bitarray, bitmask, length, width } = this;
|
||||
for (let l = 0; l < length; l += 1) {
|
||||
let dimensionsSet = 0;
|
||||
for (let w = 0; w < width; w += 1) {
|
||||
if (bitarray[w * length + l] === bitmask[w]) {
|
||||
dimensionsSet += 1;
|
||||
}
|
||||
}
|
||||
if (dimensionsSet === width) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
@@ -59,10 +65,11 @@ class BitArray {
|
||||
|
||||
// count trailing zeros - hard to do fast in JS!
|
||||
// https://en.wikipedia.org/wiki/Find_first_set#CTZ
|
||||
static ctz(v) {
|
||||
static ctz(av) {
|
||||
let c = 32;
|
||||
let v = av;
|
||||
v &= -v; // isolate lowest non-zero bit
|
||||
if (v) c--;
|
||||
if (v) c -= 1;
|
||||
if (v & 0x0000ffff) c -= 16;
|
||||
if (v & 0x00ff00ff) c -= 8;
|
||||
if (v & 0x0f0f0f0f) c -= 4;
|
||||
@@ -74,8 +81,7 @@ class BitArray {
|
||||
// find a free dimension. Return undefined if none
|
||||
_findFreeDimension() {
|
||||
let dim;
|
||||
for (let col = 0; col < this.width; col++) {
|
||||
const bitmask = this.bitmask[col];
|
||||
for (let col = 0; col < this.width; col += 1) {
|
||||
const lowestZeroBit = ~this.bitmask[col] & -~this.bitmask[col];
|
||||
if (lowestZeroBit) {
|
||||
this.bitmask[col] |= lowestZeroBit;
|
||||
@@ -92,7 +98,7 @@ class BitArray {
|
||||
|
||||
// if we did not find free dimension, expand the bitarray.
|
||||
if (dim === undefined) {
|
||||
this.width++;
|
||||
this.width += 1;
|
||||
|
||||
const biggerBitArray = new Int32Array(this.width * this.length);
|
||||
biggerBitArray.set(this.bitarray);
|
||||
@@ -105,7 +111,7 @@ class BitArray {
|
||||
dim = this._findFreeDimension();
|
||||
}
|
||||
|
||||
this.dimensionCount++;
|
||||
this.dimensionCount += 1;
|
||||
return dim;
|
||||
}
|
||||
|
||||
@@ -117,17 +123,15 @@ class BitArray {
|
||||
this.deselectAll(dim);
|
||||
const col = dim >>> 5;
|
||||
this.bitmask[col] &= ~(1 << dim % 32);
|
||||
this.dimensionCount--;
|
||||
this.dimensionCount -= 1;
|
||||
}
|
||||
|
||||
// return true if this index is selected in ALL dimensions.
|
||||
//
|
||||
isSelected(index) {
|
||||
const width = this.width;
|
||||
const length = this.length;
|
||||
const bitarray = this.bitarray;
|
||||
const { width, length, bitarray } = this;
|
||||
|
||||
for (let w = 0; w < width; w++) {
|
||||
for (let w = 0; w < width; w += 1) {
|
||||
const bitmask = this.bitmask[w];
|
||||
if (!bitmask || bitarray[w * length + index] !== bitmask) return false;
|
||||
}
|
||||
@@ -140,20 +144,19 @@ class BitArray {
|
||||
const ignoreOffset = dim >>> 5;
|
||||
const ignoreMask = ~(1 << dim % 32);
|
||||
|
||||
const width = this.width;
|
||||
const length = this.length;
|
||||
const bitarray = this.bitarray;
|
||||
const { width, length, bitarray } = this;
|
||||
|
||||
for (let w = 0; w < width; w++) {
|
||||
for (let w = 0; w < width; w += 1) {
|
||||
const bitmask = this.bitmask[w];
|
||||
if (w === ignoreOffset) {
|
||||
if (
|
||||
bitmask &&
|
||||
(bitarray[w * length + index] & ignoreMask) !== (bitmask & ignoreMask)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
if (bitmask && bitarray[w * length + index] !== bitmask) return false;
|
||||
}
|
||||
} else if (bitmask && bitarray[w * length + index] !== bitmask) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -180,24 +183,20 @@ class BitArray {
|
||||
// select all indices on dimension.
|
||||
//
|
||||
selectAll(dim) {
|
||||
let col = dim >> 5;
|
||||
const bitmask = this.bitmask[col];
|
||||
const bitarray = this.bitarray;
|
||||
const col = dim >> 5;
|
||||
const one = 1 << dim % 32;
|
||||
for (let i = col * this.length, len = i + this.length; i < len; i++) {
|
||||
bitarray[i] |= one;
|
||||
for (let i = col * this.length, len = i + this.length; i < len; i += 1) {
|
||||
this.bitarray[i] |= one;
|
||||
}
|
||||
}
|
||||
|
||||
// deselect all indices on dimension
|
||||
//
|
||||
deselectAll(dim) {
|
||||
let col = dim >> 5;
|
||||
const bitmask = this.bitmask[col];
|
||||
const bitarray = this.bitarray;
|
||||
const col = dim >> 5;
|
||||
const zero = ~(1 << dim % 32);
|
||||
for (let i = col * this.length, len = i + this.length; i < len; i++) {
|
||||
bitarray[i] &= zero;
|
||||
for (let i = col * this.length, len = i + this.length; i < len; i += 1) {
|
||||
this.bitarray[i] &= zero;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,11 +207,10 @@ class BitArray {
|
||||
const col = dim >>> 5;
|
||||
const first = range[0];
|
||||
const last = range[1];
|
||||
const bitarray = this.bitarray;
|
||||
const one = 1 << dim % 32;
|
||||
const offset = col * this.length;
|
||||
for (let i = first; i < last; i++) {
|
||||
bitarray[offset + indirect[i]] |= one;
|
||||
for (let i = first; i < last; i += 1) {
|
||||
this.bitarray[offset + indirect[i]] |= one;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,11 +220,10 @@ class BitArray {
|
||||
const col = dim >>> 5;
|
||||
const first = range[0];
|
||||
const last = range[1];
|
||||
const bitarray = this.bitarray;
|
||||
const zero = ~(1 << dim % 32);
|
||||
const offset = col * this.length;
|
||||
for (let i = first; i < last; i++) {
|
||||
bitarray[offset + indirect[i]] &= zero;
|
||||
for (let i = first; i < last; i += 1) {
|
||||
this.bitarray[offset + indirect[i]] &= zero;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,13 +234,14 @@ class BitArray {
|
||||
// special case (width === 1) for performance
|
||||
if (this.width === 1) {
|
||||
const bitmask = this.bitmask[0];
|
||||
const bitarray = this.bitarray;
|
||||
for (let i = 0, len = this.length; i < len; i++) {
|
||||
for (let i = 0, len = this.length; i < len; i += 1) {
|
||||
result[i] =
|
||||
bitmask && bitarray[i] === bitmask ? selectedValue : deselectedValue;
|
||||
bitmask && this.bitarray[i] === bitmask
|
||||
? selectedValue
|
||||
: deselectedValue;
|
||||
}
|
||||
} else {
|
||||
for (let i = 0, len = this.length; i < len; i++) {
|
||||
for (let i = 0, len = this.length; i < len; i += 1) {
|
||||
result[i] = this.isSelected(i) ? selectedValue : deselectedValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
"use strict";
|
||||
// jshint esversion: 6
|
||||
|
||||
/*
|
||||
@@ -35,7 +34,6 @@ import {
|
||||
fillRange,
|
||||
lowerBound,
|
||||
lowerBoundIndirect,
|
||||
upperBound,
|
||||
upperBoundIndirect
|
||||
} from "./util";
|
||||
|
||||
@@ -57,6 +55,7 @@ class TypedCrossfilter {
|
||||
// filters: array of { id, dimension }
|
||||
this.filters = [];
|
||||
this.selection = new BitArray(data.length);
|
||||
this.updateTime = 0;
|
||||
}
|
||||
|
||||
size() {
|
||||
@@ -82,15 +81,15 @@ class TypedCrossfilter {
|
||||
|
||||
_freeDimension(id) {
|
||||
this.selection.freeDimension(id);
|
||||
this.filters = this.filters.filter(f => f._id != id);
|
||||
this.filters = this.filters.filter(f => f._id !== id);
|
||||
}
|
||||
|
||||
// return array of all records that are selected/filtered
|
||||
// by all dimensions.
|
||||
allFiltered() {
|
||||
const selection = this.selection;
|
||||
const { selection } = this;
|
||||
const res = [];
|
||||
for (let i = 0, len = this.data.length; i < len; i++) {
|
||||
for (let i = 0, len = this.data.length; i < len; i += 1) {
|
||||
if (selection.isSelected(i)) {
|
||||
res.push(this.data[i]);
|
||||
}
|
||||
@@ -120,8 +119,8 @@ class TypedCrossfilter {
|
||||
// and value array must be a TypedArray.
|
||||
//
|
||||
class ScalarDimension {
|
||||
constructor(value, valueArrayType, crossfilter, id) {
|
||||
this.crossfilter = crossfilter;
|
||||
constructor(value, ValueArrayType, xfltr, id) {
|
||||
this.crossfilter = xfltr;
|
||||
this._id = id;
|
||||
|
||||
// current selection filter, expressed as PostiveIntervals.
|
||||
@@ -130,7 +129,7 @@ class ScalarDimension {
|
||||
// Create value array
|
||||
const array = this._createValueArray(
|
||||
value,
|
||||
new valueArrayType(this.crossfilter.data.length)
|
||||
new ValueArrayType(this.crossfilter.data.length)
|
||||
);
|
||||
this.value = array;
|
||||
|
||||
@@ -144,12 +143,13 @@ class ScalarDimension {
|
||||
|
||||
_createValueArray(value, array) {
|
||||
// create dimension value array
|
||||
const data = this.crossfilter.data;
|
||||
const { data } = this.crossfilter;
|
||||
const len = data.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
array[i] = value(data[i]);
|
||||
const larray = array;
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
larray[i] = value(data[i]);
|
||||
}
|
||||
return array;
|
||||
return larray;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
@@ -164,10 +164,10 @@ class ScalarDimension {
|
||||
// Argument is an array of intervals indicating records newly selected/filtered
|
||||
//
|
||||
_updateFilters(newFilter) {
|
||||
newFilter = PositiveIntervals.canonicalize(newFilter);
|
||||
const cNewFilter = PositiveIntervals.canonicalize(newFilter);
|
||||
|
||||
const adds = PositiveIntervals.difference(newFilter, this.currentFilter);
|
||||
const dels = PositiveIntervals.difference(this.currentFilter, newFilter);
|
||||
const adds = PositiveIntervals.difference(cNewFilter, this.currentFilter);
|
||||
const dels = PositiveIntervals.difference(this.currentFilter, cNewFilter);
|
||||
|
||||
this.crossfilter.filters.forEach(f =>
|
||||
f.dim.groups.forEach(grp => grp._updateReduceDel(this, dels))
|
||||
@@ -193,7 +193,8 @@ class ScalarDimension {
|
||||
f.dim.groups.forEach(grp => grp._updateReduceAdd(this, adds))
|
||||
);
|
||||
|
||||
this.currentFilter = newFilter;
|
||||
this.currentFilter = cNewFilter;
|
||||
this.crossfilter.updateTime += 1;
|
||||
}
|
||||
|
||||
// filter by value - exact match
|
||||
@@ -213,7 +214,7 @@ class ScalarDimension {
|
||||
// filter by a set of values, eg. enum.
|
||||
filterEnum(values) {
|
||||
const newFilter = [];
|
||||
for (let v = 0, len = values.length; v < len; v++) {
|
||||
for (let v = 0, len = values.length; v < len; v += 1) {
|
||||
const intv = [
|
||||
lowerBoundIndirect(
|
||||
this.value,
|
||||
@@ -269,9 +270,8 @@ class ScalarDimension {
|
||||
// return top k records, starting with offset, in descending order.
|
||||
// Order is this dimension's sort order
|
||||
top(k, offset = 0) {
|
||||
const data = this.crossfilter.data;
|
||||
const selection = this.crossfilter.selection;
|
||||
const index = this.index;
|
||||
const { data, selection } = this.crossfilter;
|
||||
const { index } = this;
|
||||
const len = index.length;
|
||||
const ret = [];
|
||||
let i = 0;
|
||||
@@ -279,17 +279,17 @@ class ScalarDimension {
|
||||
let found = 0;
|
||||
|
||||
// skip up to offset records
|
||||
for (i = len - 1; 0 <= i && skip < offset; i--) {
|
||||
for (i = len - 1; 0 <= i && skip < offset; i -= 1) {
|
||||
if (selection.isSelected(index[i])) {
|
||||
skip++;
|
||||
skip += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// grab up to k records
|
||||
for (; 0 <= i && found < k; i--) {
|
||||
for (; 0 <= i && found < k; i -= 1) {
|
||||
if (selection.isSelected(index[i])) {
|
||||
ret.push(data[index[i]]);
|
||||
found++;
|
||||
found += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,9 +299,8 @@ class ScalarDimension {
|
||||
// return bottom k records, starting with offset, in ascending order.
|
||||
// Order is this dimension's sort order
|
||||
bottom(k, offset = 0) {
|
||||
const data = this.crossfilter.data;
|
||||
const selection = this.crossfilter.selection;
|
||||
const index = this.index;
|
||||
const { data, selection } = this.crossfilter;
|
||||
const { index } = this;
|
||||
const len = index.length;
|
||||
const ret = [];
|
||||
let skip = 0;
|
||||
@@ -309,17 +308,17 @@ class ScalarDimension {
|
||||
let i = 0;
|
||||
|
||||
// skip up to offset records
|
||||
for (i = 0; i < len && skip < offset; i++) {
|
||||
for (i = 0; i < len && skip < offset; i += 1) {
|
||||
if (selection.isSelected(index[i])) {
|
||||
skip++;
|
||||
skip += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// grab up to k records
|
||||
for (; i < len && found < k; i++) {
|
||||
for (; i < len && found < k; i += 1) {
|
||||
if (selection.isSelected(index[i])) {
|
||||
ret.push(data[index[i]]);
|
||||
found++;
|
||||
found += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,18 +340,19 @@ class ScalarDimension {
|
||||
// strings, which can be mapped into an fixed numeric range [0..n).
|
||||
//
|
||||
class EnumDimension extends ScalarDimension {
|
||||
constructor(value, crossfilter, id) {
|
||||
super(value, Uint32Array, crossfilter, id);
|
||||
constructor(value, xfltr, id) {
|
||||
super(value, Uint32Array, xfltr, id);
|
||||
}
|
||||
|
||||
_createValueArray(value, array) {
|
||||
const data = this.crossfilter.data;
|
||||
const { data } = this.crossfilter;
|
||||
const len = data.length;
|
||||
const larray = array;
|
||||
|
||||
// create enumeration table - mapping between the value
|
||||
// and the enum.
|
||||
const s = new Set();
|
||||
for (let i = 0; i < len; i++) {
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
s.add(value(data[i]));
|
||||
}
|
||||
this.enumIndex = Array.from(s);
|
||||
@@ -360,12 +360,12 @@ class EnumDimension extends ScalarDimension {
|
||||
|
||||
// create dimension value array
|
||||
const enumLen = this.enumIndex.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
const v = value(data[i]);
|
||||
const e = lowerBound(this.enumIndex, v, 0, enumLen);
|
||||
array[i] = e;
|
||||
larray[i] = e;
|
||||
}
|
||||
return array;
|
||||
return larray;
|
||||
}
|
||||
|
||||
filterExact(value) {
|
||||
@@ -415,7 +415,7 @@ class ScalarGroup {
|
||||
|
||||
// internal support function - map all dimension values to group values.
|
||||
//
|
||||
_map(groupValue, groupValueType, dimension) {
|
||||
static _map(groupValue, GroupValueType, dimension) {
|
||||
// groupValue is optional. Defaults to identity. Used to perform
|
||||
// initial map operation.
|
||||
//
|
||||
@@ -424,8 +424,8 @@ class ScalarGroup {
|
||||
|
||||
const data = dimension.value;
|
||||
const len = data.length;
|
||||
const mapValue = new groupValueType(dimension.value.length);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const mapValue = new GroupValueType(dimension.value.length);
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
mapValue[i] = groupValue(data[i]);
|
||||
}
|
||||
return mapValue;
|
||||
@@ -444,10 +444,9 @@ class ScalarGroup {
|
||||
|
||||
// Each item in the range was just added to `dim`. It was NOT previously
|
||||
// selected - reduceAdd if it is now selected.
|
||||
const selection = this.dimension.crossfilter.selection;
|
||||
const data = this.dimension.crossfilter.data;
|
||||
const { data, selection } = this.dimension.crossfilter;
|
||||
intv.forEach(rng => {
|
||||
for (let r = rng[0]; r < rng[1]; r++) {
|
||||
for (let r = rng[0]; r < rng[1]; r += 1) {
|
||||
const i = dim.index[r];
|
||||
if (selection.isSelectedIgnoringDim(i, this.dimension.id())) {
|
||||
const group = this.groups[this.groupIndex[i]];
|
||||
@@ -470,10 +469,9 @@ class ScalarGroup {
|
||||
|
||||
// Each item in the range will be remved from `dim`. reduceRemove if it
|
||||
// is currently selected.
|
||||
const selection = this.dimension.crossfilter.selection;
|
||||
const data = this.dimension.crossfilter.data;
|
||||
const { data, selection } = this.dimension.crossfilter.selection;
|
||||
intv.forEach(rng => {
|
||||
for (let r = rng[0]; r < rng[1]; r++) {
|
||||
for (let r = rng[0]; r < rng[1]; r += 1) {
|
||||
const i = dim.index[r];
|
||||
if (selection.isSelectedIgnoringDim(i, this.dimension.id())) {
|
||||
const group = this.groups[this.groupIndex[i]];
|
||||
@@ -487,8 +485,8 @@ class ScalarGroup {
|
||||
// groups data.
|
||||
//
|
||||
_reduce() {
|
||||
const dimension = this.dimension;
|
||||
const data = dimension.crossfilter.data;
|
||||
const { dimension } = this;
|
||||
const { data } = dimension.crossfilter;
|
||||
|
||||
// Create groups
|
||||
const groupNames = new Set(this.mapValue);
|
||||
@@ -500,13 +498,13 @@ class ScalarGroup {
|
||||
});
|
||||
|
||||
// Create groupIndex - index map between data record index and group index
|
||||
for (let i = 0, len = this.mapValue.length; i < len; i++) {
|
||||
for (let i = 0, len = this.mapValue.length; i < len; i += 1) {
|
||||
this.groupIndex[i] = groupIndexByName[this.mapValue[i]];
|
||||
}
|
||||
|
||||
// reduce all filtered records, IGNORING the current dimension's filter
|
||||
const selection = dimension.crossfilter.selection;
|
||||
for (let i = 0, len = data.length; i < len; i++) {
|
||||
const { selection } = dimension.crossfilter;
|
||||
for (let i = 0, len = data.length; i < len; i += 1) {
|
||||
if (selection.isSelectedIgnoringDim(i, dimension.id())) {
|
||||
const group = this.groups[this.groupIndex[i]];
|
||||
group.value = this.reduceAdd(group.value, data[i]);
|
||||
@@ -554,11 +552,7 @@ class ScalarGroup {
|
||||
}
|
||||
|
||||
class EnumGroup extends ScalarGroup {
|
||||
constructor(groupValue, groupValueType, dimension) {
|
||||
super(groupValue, groupValueType, dimension);
|
||||
}
|
||||
|
||||
_map(groupValue, groupValueType, dimension) {
|
||||
static _map(groupValue, groupValueType, dimension) {
|
||||
// groupValue is optional. Defaults to identity. Used to perform
|
||||
// initial map operation.
|
||||
//
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
"use strict";
|
||||
// jshint esversion: 6
|
||||
|
||||
// Interval operations - very simple version of interval set relationship
|
||||
@@ -21,11 +20,11 @@ class PositiveIntervals {
|
||||
//
|
||||
static canonicalize(A) {
|
||||
if (A.length <= 1) return A;
|
||||
let copy = A.slice();
|
||||
const copy = A.slice();
|
||||
copy.sort((a, b) => a[0] - b[0]);
|
||||
const res = [];
|
||||
res.push(copy[0]);
|
||||
for (let i = 1, len = copy.length; i < len; i++) {
|
||||
for (let i = 1, len = copy.length; i < len; i += 1) {
|
||||
if (copy[i][0] > res[res.length - 1][1]) {
|
||||
// non-overlapping, add to result
|
||||
res.push(copy[i]);
|
||||
@@ -45,12 +44,12 @@ class PositiveIntervals {
|
||||
}
|
||||
|
||||
static _flatten(A, B) {
|
||||
let points = []; /* point, A, start */
|
||||
for (let a = 0; a < A.length; a++) {
|
||||
const points = []; /* point, A, start */
|
||||
for (let a = 0; a < A.length; a += 1) {
|
||||
points.push([A[a][0], true, true]);
|
||||
points.push([A[a][1], true, false]);
|
||||
}
|
||||
for (let b = 0; b < B.length; b++) {
|
||||
for (let b = 0; b < B.length; b += 1) {
|
||||
points.push([B[b][0], false, true]);
|
||||
points.push([B[b][1], false, false]);
|
||||
}
|
||||
@@ -68,18 +67,16 @@ class PositiveIntervals {
|
||||
return PositiveIntervals.canonicalize(A);
|
||||
}
|
||||
|
||||
A = PositiveIntervals.canonicalize(A);
|
||||
B = PositiveIntervals.canonicalize(B);
|
||||
const cA = PositiveIntervals.canonicalize(A);
|
||||
const cB = PositiveIntervals.canonicalize(B);
|
||||
|
||||
const points = PositiveIntervals._flatten(A, B);
|
||||
const points = PositiveIntervals._flatten(cA, cB);
|
||||
const res = [];
|
||||
let aDepth = 0;
|
||||
let depth = 0;
|
||||
let intervalStart;
|
||||
let prevPoint;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
for (let i = 0; i < points.length; i += 1) {
|
||||
const p = points[i];
|
||||
const before = depth;
|
||||
const delta = p[2] ? 1 : -1;
|
||||
depth += delta;
|
||||
if (p[1]) aDepth += delta;
|
||||
@@ -92,7 +89,6 @@ class PositiveIntervals {
|
||||
intervalStart = undefined;
|
||||
}
|
||||
}
|
||||
prevPoint = p[0];
|
||||
}
|
||||
// guaranteed to be in canonical form
|
||||
return res;
|
||||
@@ -106,16 +102,15 @@ class PositiveIntervals {
|
||||
return [];
|
||||
}
|
||||
|
||||
A = PositiveIntervals.canonicalize(A);
|
||||
B = PositiveIntervals.canonicalize(B);
|
||||
const cA = PositiveIntervals.canonicalize(A);
|
||||
const cB = PositiveIntervals.canonicalize(B);
|
||||
|
||||
const points = PositiveIntervals._flatten(A, B);
|
||||
const points = PositiveIntervals._flatten(cA, cB);
|
||||
const res = [];
|
||||
let depth = 0;
|
||||
let intervalStart;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
for (let i = 0; i < points.length; i += 1) {
|
||||
const p = points[i];
|
||||
const before = depth;
|
||||
depth += p[2] ? 1 : -1;
|
||||
if (depth === 2) {
|
||||
intervalStart = p[0];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use strict";
|
||||
// jshint esversion: 6
|
||||
/* eslint no-bitwise: "off" */
|
||||
|
||||
/*
|
||||
Utility functions, private to this module.
|
||||
@@ -9,10 +9,11 @@
|
||||
// starting with `start`
|
||||
//
|
||||
export function fillRange(arr, start = 0) {
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
arr[i] = i + start;
|
||||
const larr = arr;
|
||||
for (let i = 0, len = larr.length; i < len; i += 1) {
|
||||
larr[i] = i + start;
|
||||
}
|
||||
return arr;
|
||||
return larr;
|
||||
}
|
||||
|
||||
// Search for `value` in the sorted array `arr`, in the range [first, last).
|
||||
@@ -31,31 +32,35 @@ export function fillRange(arr, start = 0) {
|
||||
// a special-cased version for lining the indirection).
|
||||
//
|
||||
export function lowerBound(valueArray, value, first, last) {
|
||||
let lfirst = first;
|
||||
let llast = last;
|
||||
// this is just a binary search
|
||||
while (first < last) {
|
||||
const middle = (first + last) >>> 1;
|
||||
while (lfirst < llast) {
|
||||
const middle = (lfirst + llast) >>> 1;
|
||||
if (valueArray[middle] < value) {
|
||||
first = middle + 1;
|
||||
lfirst = middle + 1;
|
||||
} else {
|
||||
last = middle;
|
||||
llast = middle;
|
||||
}
|
||||
}
|
||||
return first;
|
||||
return lfirst;
|
||||
}
|
||||
|
||||
// Inlined performance optimization - used to indirect through a sort map.
|
||||
//
|
||||
export function lowerBoundIndirect(valueArray, indexArray, value, first, last) {
|
||||
let lfirst = first;
|
||||
let llast = last;
|
||||
// this is just a binary search
|
||||
while (first < last) {
|
||||
const middle = (first + last) >>> 1;
|
||||
while (lfirst < llast) {
|
||||
const middle = (lfirst + llast) >>> 1;
|
||||
if (valueArray[indexArray[middle]] < value) {
|
||||
first = middle + 1;
|
||||
lfirst = middle + 1;
|
||||
} else {
|
||||
last = middle;
|
||||
llast = middle;
|
||||
}
|
||||
}
|
||||
return first;
|
||||
return lfirst;
|
||||
}
|
||||
|
||||
// Search for `value in the sorted array `arr`, in the range [first, last).
|
||||
@@ -70,29 +75,33 @@ export function lowerBoundIndirect(valueArray, indexArray, value, first, last) {
|
||||
// Python: bisect.bisect_right()
|
||||
//
|
||||
export function upperBound(valueArray, value, first, last) {
|
||||
let lfirst = first;
|
||||
let llast = last;
|
||||
// this is just a binary search
|
||||
while (first < last) {
|
||||
const middle = (first + last) >>> 1;
|
||||
while (lfirst < llast) {
|
||||
const middle = (lfirst + llast) >>> 1;
|
||||
if (valueArray[middle] > value) {
|
||||
last = middle;
|
||||
llast = middle;
|
||||
} else {
|
||||
first = middle + 1;
|
||||
lfirst = middle + 1;
|
||||
}
|
||||
}
|
||||
return first;
|
||||
return lfirst;
|
||||
}
|
||||
|
||||
// Inline performance optimization
|
||||
//
|
||||
export function upperBoundIndirect(valueArray, indexArray, value, first, last) {
|
||||
let lfirst = first;
|
||||
let llast = last;
|
||||
// this is just a binary search
|
||||
while (first < last) {
|
||||
const middle = (first + last) >>> 1;
|
||||
while (lfirst < llast) {
|
||||
const middle = (lfirst + llast) >>> 1;
|
||||
if (valueArray[indexArray[middle]] > value) {
|
||||
last = middle;
|
||||
llast = middle;
|
||||
} else {
|
||||
first = middle + 1;
|
||||
lfirst = middle + 1;
|
||||
}
|
||||
}
|
||||
return first;
|
||||
return lfirst;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user