#130 redux refactor (#208)

* 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:
Bruce Martin
2018-09-17 20:43:39 -07:00
committed by GitHub
parent 7b00fea44d
commit d4e850d18f
40 changed files with 2008 additions and 1584 deletions

View File

@@ -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"

View File

@@ -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",

View File

@@ -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"]
};

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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
};

View File

@@ -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} />

View File

@@ -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} />
);

View File

@@ -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}

View File

@@ -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

View File

@@ -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"/>

View File

@@ -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

View File

@@ -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 }}>

View File

@@ -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(

View File

@@ -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;

View File

@@ -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",

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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/"

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View 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";

View 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 };

View 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;
}

View 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;
}

View File

@@ -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;
}
}

View File

@@ -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.
//

View File

@@ -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];

View File

@@ -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;
}