#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
+5 -1
View File
@@ -1,7 +1,11 @@
module.exports = { module.exports = {
babelrc: false, babelrc: false,
cacheDirectory: true, cacheDirectory: true,
presets: [["env", { loose: true, modules: false }], "stage-0", "react"], presets: [
["modern-browsers", { loose: true, modules: false }],
"stage-0",
"react"
],
plugins: [ plugins: [
"react-hot-loader/babel", "react-hot-loader/babel",
"babel-plugin-transform-decorators-legacy" "babel-plugin-transform-decorators-legacy"
+5 -1
View File
@@ -1,6 +1,10 @@
module.exports = { module.exports = {
babelrc: false, babelrc: false,
presets: [["env", { loose: true, modules: false }], "stage-0", "react"], presets: [
["modern-browsers", { loose: true, modules: false }],
"stage-0",
"react"
],
plugins: [ plugins: [
"babel-plugin-transform-react-constant-elements", "babel-plugin-transform-react-constant-elements",
"babel-plugin-transform-decorators-legacy", "babel-plugin-transform-decorators-legacy",
+3 -3
View File
@@ -1,9 +1,9 @@
module.exports = { module.exports = {
babelrc: false, babelrc: false,
presets: [ presets: [
"babel-preset-env", ["modern-browsers", { loose: true, modules: false }],
"babel-preset-stage-0", "stage-0",
"babel-preset-react" "react"
], ],
plugins: ["istanbul"] plugins: ["istanbul"]
}; };
+15 -2
View File
@@ -5,7 +5,7 @@ module.exports = {
env: { browser: true, commonjs: true, es6: true }, env: { browser: true, commonjs: true, es6: true },
globals: { expect: true }, globals: { expect: true },
parserOptions: { parserOptions: {
ecmaVersion: 6, ecmaVersion: 2017,
sourceType: "module", sourceType: "module",
ecmaFeatures: { ecmaFeatures: {
jsx: true, jsx: true,
@@ -17,6 +17,19 @@ module.exports = {
"func-style": "off", "func-style": "off",
"arrow-parens": "off", "arrow-parens": "off",
"no-use-before-define": "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
View File
@@ -13,6 +13,181 @@
"@babel/highlight": "7.0.0-rc.1" "@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": { "@babel/generator": {
"version": "7.0.0-beta.51", "version": "7.0.0-beta.51",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.51.tgz", "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/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": { "@babel/highlight": {
"version": "7.0.0-rc.1", "version": "7.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-rc.1.tgz", "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-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": { "babel-preset-react": {
"version": "6.24.1", "version": "6.24.1",
"resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz", "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.24.1.tgz",
@@ -8579,6 +8933,11 @@
"mimic-fn": "^1.0.0" "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": { "memory-fs": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
+3
View File
@@ -35,6 +35,7 @@
"gl-matrix": "^2.7.1", "gl-matrix": "^2.7.1",
"key-pressed": "0.0.1", "key-pressed": "0.0.1",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"memoize-one": "^4.0.0",
"mouse-position": "^2.0.1", "mouse-position": "^2.0.1",
"mouse-pressed": "^1.0.0", "mouse-pressed": "^1.0.0",
"orbit-camera": "^1.0.0", "orbit-camera": "^1.0.0",
@@ -53,6 +54,7 @@
"urijs": "^1.19.0" "urijs": "^1.19.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.0.0-rc.1",
"autoprefixer": "^9.1.1", "autoprefixer": "^9.1.1",
"babel-core": "^6.13.2", "babel-core": "^6.13.2",
"babel-eslint": "^8.2.6", "babel-eslint": "^8.2.6",
@@ -62,6 +64,7 @@
"babel-plugin-transform-react-constant-elements": "^6.9.1", "babel-plugin-transform-react-constant-elements": "^6.9.1",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
"babel-preset-modern-browsers": "^11.0.1",
"babel-preset-react": "^6.11.1", "babel-preset-react": "^6.11.1",
"babel-preset-stage-0": "^6.5.0", "babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.11.6", "babel-register": "^6.11.6",
+252 -182
View File
@@ -1,101 +1,149 @@
// jshint esversion: 6 // jshint esversion: 6
import _ from "lodash";
import memoize from "memoize-one";
import * as globals from "../globals"; import * as globals from "../globals";
import store from "../reducers"; import store from "../reducers";
import URI from "urijs"; import { Universe } from "../util/stateManager";
import _ from "lodash";
const requestCells = (query = "") => { /*
return dispatch => { Catch unexpected errors and make sure we don't lose them!
dispatch({ type: "request cells started" }); */
return fetch(`${globals.API.prefix}${globals.API.version}cells${query}`, { 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", method: "get",
headers: new Headers({ headers: new Headers({
"Content-Type": "application/json" "Content-Type": "application/json"
}) })
}) }
.then(res => res.json()) );
.then( return res.json();
data => dispatch({ type: "request cells success", data }), }
error => dispatch({ type: "request cells error", error })
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]
); );
}; dispatch({
}; type: "initial data load complete (universe exists)",
universe
/* 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;
}
}); });
} catch (error) {
dispatch({ type: "initial data load error", error });
}
});
}
if (atLeastOneOptionDeselected) { // XXX TODO - this is the old code for doing a regraph. Preserving it solely
_.each(options, (isActive, option) => { // until we port to 0.2 API. The new UX for regraph can't be implemented on
if (isActive) { // the 0.1 API (doesn't allow for re-layout on arbitrary sets of cells), so just
if (selectedMetadata[field]) { // punting for now. See ticket #88
selectedMetadata[field].push(option); //
} else if (!selectedMetadata[field]) { //
selectedMetadata[field] = [option]; // /* 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(); const regraph = () => (dispatch, getState) => {
uri.setSearch(selectedMetadata); const { universe, world, crossfilter } = getState().controls;
console.log(uri.search(), selectedMetadata); dispatch({
type: "set World to current selection",
dispatch(requestCells(uri.search())).then(res => { universe,
if (res.error) { world,
dispatch({ type: "regraph error" }); crossfilter
} else { });
dispatch({ type: "regraph success" });
}
});
};
}; };
const resetGraph = () => { const resetGraph = () => (dispatch, getState) =>
return (dispatch, getState) => { dispatch({
dispatch({ type: "reset graph" }); type: "reset World to eq Universe",
}; universe: getState().controls.universe
}; });
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 })
);
};
};
// This code defends against the case where /expression returns a cellname // This code defends against the case where /expression returns a cellname
// never seen before (ie, not returned by /cells). This should not happen // never seen before (ie, not returned by /cells). This should not happen
// (see https://github.com/chanzuckerberg/cellxgene-rest-api/issues/34) but // (see https://github.com/chanzuckerberg/cellxgene-rest-api/issues/34) but
// occasionally does. // 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) { function cleanupExpressionResponse(data) {
const s = store.getState(); const s = store.getState();
const metadata = s.controls.allCellsMetadataMap; const { universe } = s.controls;
const metadata = makeMetadataMap(universe.obsAnnotations);
let errorFound = false; let errorFound = false;
data.data.cells = _.filter(data.data.cells, cell => { data.data.cells = _.filter(data.data.cells, cell => {
if (!errorFound && !metadata[cell.cellname]) { if (!errorFound && !metadata[cell.cellname]) {
@@ -110,123 +158,145 @@ function cleanupExpressionResponse(data) {
return data; return data;
} }
const requestGeneExpressionCounts = () => { /*
return (dispatch, getState) => { Fetch [gene, ...] from V0.1 API. Not an action function - just a helper
dispatch({ type: "get expression started" }); which implements the new expression data caching.
fetch(`${globals.API.prefix}${globals.API.version}expression`, { */
method: "get", async function _doRequestExpressionData(dispatch, getState, genes) {
headers: new Headers({ const state = getState();
accept: "application/json" /* check cache and only fetch data we do not already have */
}) const { universe } = state.controls;
}) const genesToFetch = _.filter(genes, g => !universe.varDataCache[g]);
.then(res => res.json())
.then(data => cleanupExpressionResponse(data))
.then(
data => dispatch({ type: "get expression success", data }),
error => dispatch({ type: "get expression error", error })
);
};
};
const requestSingleGeneExpressionCountsForColoringPOST = gene => { dispatch({ type: "expression load start" });
return (dispatch, getState) => { let expressionData = {}; // { gene: data }
dispatch({ type: "get single gene expression for coloring started" }); if (genesToFetch.length) {
fetch(`${globals.API.prefix}${globals.API.version}expression`, { try {
method: "POST", const res = await fetch(
body: JSON.stringify({ `${globals.API.prefix}${globals.API.version}expression`,
genelist: [gene] {
}), method: "POST",
headers: new Headers({ body: JSON.stringify({
accept: "application/json", genelist: genes
"Content-Type": "application/json"
})
})
.then(res => res.json())
.then(data => cleanupExpressionResponse(data))
.then(
data =>
dispatch({
type: "color by expression",
gene: gene,
data
}), }),
error => headers: new Headers({
dispatch({ accept: "application/json",
type: "get single gene expression for coloring error", "Content-Type": "application/json"
error
}) })
}
); );
}; 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 => { // add the cached values
return (dispatch, getState) => { _.forEach(genes, g => {
dispatch({ type: "get expression started" }); if (expressionData[g] === undefined) {
fetch(`${globals.API.prefix}${globals.API.version}expression`, { expressionData[g] = universe.varDataCache[g];
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 })
);
};
};
const requestDifferentialExpression = (celllist1, celllist2, num_genes = 7) => { return dispatch({ type: "expression load success", expressionData });
return (dispatch, getState) => { }
dispatch({ type: "request differential expression started" });
fetch(`${globals.API.prefix}${globals.API.version}diffexpression`, { function requestSingleGeneExpressionCountsForColoringPOST(gene) {
method: "POST", return async (dispatch, getState) => {
body: JSON.stringify({ dispatch({ type: "get single gene expression for coloring started" });
celllist1, try {
celllist2, await _doRequestExpressionData(dispatch, getState, [gene]);
num_genes dispatch({
}), type: "color by expression",
headers: new Headers({ gene,
accept: "application/json", data: {
"Content-Type": "application/json" [gene]: getState().controls.world.varDataCache[gene]
}) }
}) });
.then(res => res.json()) } catch (error) {
.then( dispatch({
data => { type: "get single gene expression for coloring error",
/* kick off a secondary action to get all expression counts for all cells now that we know what the top expressed are */ error
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"] const requestGeneExpressionCountsPOST = genes => async (dispatch, getState) => {
) dispatch({ type: "get expression started" });
); try {
/* then send the success case action through */ await _doRequestExpressionData(dispatch, getState, genes);
return dispatch({ return dispatch({
type: "request differential expression success", type: "get expression success",
data 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 { export default {
initialize,
requestCells,
regraph, regraph,
resetGraph, resetGraph,
requestGeneExpressionCounts,
requestGeneExpressionCountsPOST,
requestSingleGeneExpressionCountsForColoringPOST, requestSingleGeneExpressionCountsForColoringPOST,
requestDifferentialExpression requestDifferentialExpression,
doInitialDataLoad
}; };
+11 -22
View File
@@ -7,9 +7,7 @@ import { connect } from "react-redux";
// import PulseLoader from "halogen/PulseLoader"; // import PulseLoader from "halogen/PulseLoader";
import LeftSideBar from "./leftsidebar"; import LeftSideBar from "./leftsidebar";
import Parallel from "./continuous/parallel";
import Legend from "./continuousLegend"; import Legend from "./continuousLegend";
// import Joy from "./joy/joy";
import Graph from "./graph/graph"; import Graph from "./graph/graph";
import * as globals from "../globals"; import * as globals from "../globals";
import actions from "../actions"; import actions from "../actions";
@@ -18,8 +16,8 @@ import SectionHeader from "./framework/sectionHeader";
@connect(state => { @connect(state => {
return { return {
cells: state.cells, loading: state.controls.loading,
initialize: state.initialize error: state.controls.error
}; };
}) })
class App extends React.Component { class App extends React.Component {
@@ -27,20 +25,18 @@ class App extends React.Component {
super(props); super(props);
this.state = {}; this.state = {};
} }
_onURLChanged() { _onURLChanged() {
this.props.dispatch({ type: "url changed", url: document.location.href }); this.props.dispatch({ type: "url changed", url: document.location.href });
} }
componentDidMount() { componentDidMount() {
/* listen for url changes, fire one when we start the app up */ /* listen for url changes, fire one when we start the app up */
window.addEventListener("popstate", this._onURLChanged); window.addEventListener("popstate", this._onURLChanged);
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 */ /* listen for resize events */
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
this.props.dispatch({ this.props.dispatch({
@@ -61,10 +57,11 @@ class App extends React.Component {
} }
render() { render() {
const { loading, error } = this.props;
return ( return (
<Container> <Container>
<Helmet title="cellxgene" /> <Helmet title="cellxgene" />
{this.props.cells.loading || this.props.initialize.loading ? ( {loading ? (
<div <div
style={{ style={{
position: "fixed", position: "fixed",
@@ -76,12 +73,9 @@ class App extends React.Component {
loading cellxgene loading cellxgene
</div> </div>
) : null} ) : null}
{this.props.cells.error ? "Error loading cells" : null} {error ? "Error loading cells" : null}
<div> <div>
{this.props.cells.loading || this.props.initialize.loading ? null : ( {loading ? null : <LeftSideBar />}
<LeftSideBar />
)}
<div <div
style={{ style={{
padding: 15, padding: 15,
@@ -89,13 +83,10 @@ class App extends React.Component {
marginLeft: 350 /* but responsive */ marginLeft: 350 /* but responsive */
}} }}
> >
{this.props.cells.loading || {loading ? null : <Graph />}
this.props.initialize.loading ? null : (
<Graph />
)}
<Legend /> <Legend />
{/*<Parallel/>*/} {}
</div> </div>
</div> </div>
</Container> </Container>
@@ -104,5 +95,3 @@ class App extends React.Component {
} }
export default App; export default App;
// <Joy data={this.state.expressions && this.state.expressions.data} />
@@ -3,16 +3,14 @@ import React from "react";
import _ from "lodash"; import _ from "lodash";
import { connect } from "react-redux"; 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 FaArrowRight from "react-icons/lib/fa/angle-right";
import FaArrowDown from "react-icons/lib/fa/angle-down"; import FaArrowDown from "react-icons/lib/fa/angle-down";
import FaPaintBrush from "react-icons/lib/fa/paint-brush"; import FaPaintBrush from "react-icons/lib/fa/paint-brush";
import * as globals from "../../globals";
import Value from "./value";
import { alphabeticallySortedValues } from "./util";
@connect(state => { @connect(state => {
return { return {
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
@@ -27,6 +25,7 @@ class Category extends React.Component {
isExpanded: false isExpanded: false
}; };
} }
componentDidUpdate() { componentDidUpdate() {
const valuesAsBool = _.values( const valuesAsBool = _.values(
this.props.categoricalAsBooleansMap[this.props.metadataField] this.props.categoricalAsBooleansMap[this.props.metadataField]
@@ -45,12 +44,14 @@ class Category extends React.Component {
this.checkbox.indeterminate = true; this.checkbox.indeterminate = true;
} }
} }
handleColorChange() { handleColorChange() {
this.props.dispatch({ this.props.dispatch({
type: "color by categorical metadata", type: "color by categorical metadata",
colorAccessor: this.props.metadataField colorAccessor: this.props.metadataField
}); });
} }
toggleAll() { toggleAll() {
this.props.dispatch({ this.props.dispatch({
type: "categorical metadata filter all of these", type: "categorical metadata filter all of these",
@@ -58,6 +59,7 @@ class Category extends React.Component {
}); });
this.setState({ isChecked: true }); this.setState({ isChecked: true });
} }
toggleNone() { toggleNone() {
this.props.dispatch({ this.props.dispatch({
type: "categorical metadata filter none of these", type: "categorical metadata filter none of these",
@@ -66,6 +68,7 @@ class Category extends React.Component {
}); });
this.setState({ isChecked: false }); this.setState({ isChecked: false });
} }
renderCategoryItems() { renderCategoryItems() {
return _.map(alphabeticallySortedValues(this.props.values), (v, i) => { return _.map(alphabeticallySortedValues(this.props.values), (v, i) => {
return ( return (
@@ -79,6 +82,7 @@ class Category extends React.Component {
); );
}); });
} }
handleToggleAllClick() { handleToggleAllClick() {
// || this.checkbox.indeterminate === false // || this.checkbox.indeterminate === false
if (this.state.isChecked) { if (this.state.isChecked) {
@@ -89,6 +93,7 @@ class Category extends React.Component {
this.toggleAll(); this.toggleAll();
} }
} }
render() { render() {
return ( return (
<div <div
@@ -164,8 +169,7 @@ class Category extends React.Component {
} }
@connect(state => { @connect(state => {
const ranges = _.get(state, "cells.cells.data.ranges", null); const ranges = _.get(state.controls.world, "summary.obs", null);
return { return {
ranges ranges
}; };
@@ -192,7 +196,7 @@ class Categories extends React.Component {
> >
{_.map(this.props.ranges, (value, key) => { {_.map(this.props.ranges, (value, key) => {
const isColorField = key.includes("color") || key.includes("Color"); const isColorField = key.includes("color") || key.includes("Color");
if (value.options && key !== "CellName" && !isColorField) { if (value.options && !isColorField && key !== "name") {
return ( return (
<Category key={key} metadataField={key} values={value.options} /> <Category key={key} metadataField={key} values={value.options} />
); );
@@ -17,18 +17,15 @@ import HistogramBrush from "./histogramBrush";
import { margin, width, height, createDimensions } from "./util"; import { margin, width, height, createDimensions } from "./util";
@connect(state => { @connect(state => {
const ranges = _.get(state, "cells.cells.data.ranges", null); const metadata = _.get(state.controls.world, "obsAnnotations", null);
const metadata = _.get(state, "cells.cells.data.metadata", null); const ranges = _.get(state.controls.world, "summary.obs", null);
const initializeRanges = _.get(state, "initialize.data.data.ranges", null);
return { return {
ranges, ranges,
metadata, metadata,
initializeRanges,
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale, colorScale: state.controls.colorScale,
graphBrushSelection: state.controls.graphBrushSelection, selectionUpdate: _.get(state.controls, "crossfilter.updateTime", null)
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
}; };
}) })
class Continuous extends React.Component { class Continuous extends React.Component {
@@ -41,17 +38,19 @@ class Continuous extends React.Component {
dimensions: null dimensions: null
}; };
} }
handleBrushAction(selection) { handleBrushAction(selection) {
this.props.dispatch({ this.props.dispatch({
type: "continuous selection using parallel coords brushing", type: "continuous selection using parallel coords brushing",
data: selection data: selection
}); });
} }
handleColorAction(key) { handleColorAction(key) {
this.props.dispatch({ this.props.dispatch({
type: "color by continuous metadata", type: "color by continuous metadata",
colorAccessor: key, 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> <div>
{_.map(this.props.ranges, (value, key) => { {_.map(this.props.ranges, (value, key) => {
const isColorField = key.includes("color") || key.includes("Color"); const isColorField = key.includes("color") || key.includes("Color");
if (value.range && key !== "CellName" && !isColorField) { if (value.range && key !== "name" && !isColorField) {
return ( return (
<HistogramBrush <HistogramBrush
key={key} key={key}
@@ -8,87 +8,90 @@ import React from "react";
import _ from "lodash"; import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
import FaPaintBrush from "react-icons/lib/fa/paint-brush"; import FaPaintBrush from "react-icons/lib/fa/paint-brush";
import * as globals from "../../globals";
import * as d3 from "d3"; import * as d3 from "d3";
import memoize from "memoize-one";
import * as globals from "../../globals";
@connect(state => { @connect(state => {
const initializeRanges = _.get(state, "initialize.data.data.ranges", null);
return { return {
initializeRanges, initializeRanges: _.get(state.controls.world, "summary.obs"),
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale, colorScale: state.controls.colorScale,
cellsMetadata: state.controls.cellsMetadata obsAnnotations: _.get(state.controls.world, "obsAnnotations", null)
}; };
}) })
class HistogramBrush extends React.Component { 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) { constructor(props) {
super(props); super(props);
this.width = 300; this.width = 300;
this.height = 100; this.height = 100;
this.marginBottom = 20; this.marginBottom = 20;
this.histogramCache = {};
this.state = { this.state = {
svg: null,
ctx: null,
axes: null,
dimensions: null,
brush: 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) { onBrush(selection, x) {
return () => { return () => {
const { dispatch, metadataField } = this.props;
if (d3.event.selection) { if (d3.event.selection) {
this.props.dispatch({ dispatch({
type: "continuous metadata histogram brush", type: "continuous metadata histogram brush",
selection: this.props.metadataField, selection: metadataField,
range: [x(d3.event.selection[0]), x(d3.event.selection[1])] range: [x(d3.event.selection[0]), x(d3.event.selection[1])]
}); });
} else { } else {
this.props.dispatch({ dispatch({
type: "continuous metadata histogram brush", type: "continuous metadata histogram brush",
selection: this.props.metadataField, selection: metadataField,
range: null range: null
}); });
} }
}; };
} }
drawHistogram(svgRef) { drawHistogram(svgRef) {
const x = this.histogramCache.x; const { obsAnnotations, metadataField, ranges } = this.props;
const y = this.histogramCache.y; const histogramCache = this.calcHistogramCache(
const bins = this.histogramCache.bins; obsAnnotations,
const numValues = this.histogramCache.numValues; metadataField,
ranges
);
const { x, y, bins, numValues } = histogramCache;
d3.select(svgRef)
.selectAll(".bar")
.remove();
d3.select(svgRef) d3.select(svgRef)
.insert("g", "*") .insert("g", "*")
@@ -97,6 +100,7 @@ class HistogramBrush extends React.Component {
.data(bins) .data(bins)
.enter() .enter()
.append("rect") .append("rect")
.attr("class", "bar")
.attr("x", function(d) { .attr("x", function(d) {
return x(d.x0) + 1; return x(d.x0) + 1;
}) })
@@ -144,6 +148,7 @@ class HistogramBrush extends React.Component {
this.setState({ brush, xAxis }); this.setState({ brush, xAxis });
} }
} }
handleColorAction() { handleColorAction() {
this.props.dispatch({ this.props.dispatch({
type: "color by continuous metadata", type: "color by continuous metadata",
@@ -153,6 +158,7 @@ class HistogramBrush extends React.Component {
].range.max ].range.max
}); });
} }
render() { render() {
return ( return (
<div <div
@@ -1,145 +0,0 @@
// jshint esversion: 6
/* rc slider https://www.npmjs.com/package/rc-slider */
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
import styles from "./parallelCoordinates.css";
import SectionHeader from "../framework/sectionHeader";
import setupParallelCoordinates from "./setupParallelCoordinates";
import drawAxes from "./drawAxes";
import drawLinesCanvas from "./drawLinesCanvas";
import { margin, width, height, createDimensions } from "./util";
@connect(state => {
const ranges = _.get(state, "cells.cells.data.ranges", null);
const metadata = _.get(state, "cells.cells.data.metadata", null);
const initializeRanges = _.get(state, "initialize.data.data.ranges", null);
return {
ranges,
metadata,
initializeRanges,
colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale,
graphBrushSelection: state.controls.graphBrushSelection,
cellsMetadata: state.controls.cellsMetadata,
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
};
})
class Parallel extends React.Component {
constructor(props) {
super(props);
this.state = {
svg: null,
ctx: null,
axes: null,
dimensions: null
};
}
componentDidMount() {
const { svg, ctx } = setupParallelCoordinates(width, height, margin);
this.setState({ svg, ctx });
}
componentWillReceiveProps(nextProps) {
this.maybeDrawAxes(nextProps);
this.maybeDrawLines(nextProps);
}
maybeDrawAxes(nextProps) {
if (
!this.state.axes &&
nextProps.initializeRanges /* axes are created on full range of data */
) {
const dimensions = createDimensions(nextProps.initializeRanges);
const xscale = d3
.scalePoint()
.domain(d3.range(dimensions.length))
.range([0, width]);
const axes = drawAxes(
this.state.svg,
this.state.ctx,
dimensions,
xscale,
height,
width,
this.handleBrushAction.bind(this),
this.handleColorAction.bind(this)
);
this.setState({
axes,
xscale,
dimensions
});
this.props.dispatch({
type: "parallel coordinates axes have been drawn"
});
}
}
maybeDrawLines = _.debounce(nextProps => {
/* https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js */
if (
nextProps.ranges &&
nextProps.cellsMetadata &&
nextProps.axesHaveBeenDrawn
) {
if (this.state._drawLinesCanvas) {
this.state._drawLinesCanvas.invalidate(); /* this is only necessary if the internals of drawLinesCanvas are using the render queue */
}
this.state.ctx.clearRect(0, 0, width, height);
const _drawLinesCanvas = drawLinesCanvas(
nextProps.cellsMetadata,
this.state.dimensions,
this.state.xscale,
this.state.ctx,
nextProps.colorAccessor,
nextProps.colorScale
);
this.setState({
_drawLinesCanvas /* this will only exist if the internals of drawLinesCanvas are using the render queue */
});
}
}, 200);
handleBrushAction(selection) {
this.props.dispatch({
type: "continuous selection using parallel coords brushing",
data: selection
});
}
handleColorAction(key) {
this.props.dispatch({
type: "color by continuous metadata",
colorAccessor: key,
rangeMaxForColorAccessor: this.props.initializeRanges[key].range.max
});
}
render() {
return (
<div id="parcoords_wrapper">
<div
className={styles.parcoords}
id="parcoords"
style={{
width: width + margin.left + margin.right + "px",
height: height + margin.top + margin.bottom + "px"
}}
/>
</div>
);
}
}
export default Parallel;
// <SectionHeader text="Continuous Metadata"/>
@@ -1,19 +1,17 @@
// jshint esversion: 6 // jshint esversion: 6
import React from "react"; import React from "react";
import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
import * as globals from "../../globals";
import * as d3 from "d3"; import * as d3 from "d3";
import { interpolateViridis } from "d3-scale-chromatic"; import { interpolateViridis } from "d3-scale-chromatic";
// create continuous color legend // create continuous color legend
// http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f // http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f
const continuous = (selector_id, colorscale) => { const continuous = (selector_id, colorscale) => {
var legendheight = 200, const legendheight = 200;
legendwidth = 80, const legendwidth = 80;
margin = { top: 10, right: 60, bottom: 10, left: 2 }; const margin = { top: 10, right: 60, bottom: 10, left: 2 };
var canvas = d3 const canvas = d3
.select(selector_id) .select(selector_id)
.style("height", legendheight + "px") .style("height", legendheight + "px")
.style("width", legendwidth + "px") .style("width", legendwidth + "px")
@@ -104,6 +102,7 @@ class ContinuousLegend extends React.Component {
super(props); super(props);
this.state = {}; this.state = {};
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if ( if (
prevProps.colorAccessor !== this.props.colorAccessor || prevProps.colorAccessor !== this.props.colorAccessor ||
@@ -122,13 +121,15 @@ class ContinuousLegend extends React.Component {
continuous( continuous(
"#continuous_legend", "#continuous_legend",
d3 d3
.scaleSequential(d3.interpolateViridis) .scaleSequential(interpolateViridis)
.domain(this.props.colorScale.domain()) .domain(this.props.colorScale.domain())
); );
} }
} }
} }
drawScale() {} drawScale() {}
render() { render() {
return ( return (
<div <div
@@ -9,7 +9,7 @@ import actions from "../../actions";
@connect() @connect()
class CellSetButton extends React.Component { class CellSetButton extends React.Component {
set() { set() {
const set = _.map(this.props.crossfilter.cells.allFiltered(), "CellName"); const set = _.map(this.props.crossfilter.allFiltered(), "name");
this.props.dispatch({ this.props.dispatch({
type: type:
@@ -18,6 +18,7 @@ class CellSetButton extends React.Component {
data: set data: set
}); });
} }
render() { render() {
return ( return (
<span style={{ marginRight: 10 }}> <span style={{ marginRight: 10 }}>
@@ -1,6 +1,7 @@
// jshint esversion: 6 // jshint esversion: 6
import React from "react"; import React from "react";
import _ from "lodash"; import _ from "lodash";
import memoize from "memoize-one";
import { connect } from "react-redux"; import { connect } from "react-redux";
import * as globals from "../../globals"; import * as globals from "../../globals";
import styles from "./expression.css"; import styles from "./expression.css";
@@ -19,6 +20,7 @@ class HeatmapSquare extends React.Component {
value: "" value: ""
}; };
} }
render() { render() {
const contrastColor = getContrast( const contrastColor = getContrast(
this.props.backgroundColor this.props.backgroundColor
@@ -66,6 +68,7 @@ class HeatmapRow extends React.Component {
value: "" value: ""
}; };
} }
handleGeneColorScaleClick(gene) { handleGeneColorScaleClick(gene) {
return () => { return () => {
this.props.dispatch( this.props.dispatch(
@@ -75,6 +78,7 @@ class HeatmapRow extends React.Component {
); );
}; };
} }
handleSetGeneAsScatterplotX(gene) { handleSetGeneAsScatterplotX(gene) {
return () => { return () => {
this.props.dispatch({ this.props.dispatch({
@@ -83,6 +87,7 @@ class HeatmapRow extends React.Component {
}); });
}; };
} }
handleSetGeneAsScatterplotY(gene) { handleSetGeneAsScatterplotY(gene) {
return () => { return () => {
this.props.dispatch({ this.props.dispatch({
@@ -91,6 +96,7 @@ class HeatmapRow extends React.Component {
}); });
}; };
} }
render() { render() {
return ( return (
<div <div
@@ -211,7 +217,7 @@ class HeatmapRow extends React.Component {
@connect(state => { @connect(state => {
return { return {
differential: state.differential, differential: state.differential,
allGeneNames: state.controls.allGeneNames world: state.controls.world
}; };
}) })
class Heatmap extends React.Component { class Heatmap extends React.Component {
@@ -221,12 +227,18 @@ class Heatmap extends React.Component {
value: "" value: ""
}; };
} }
getAllGeneNames = memoize(world =>
_.map(this.props.world.varAnnotations, "name")
);
render() { render() {
if (!this.props.differential.diffExp) if (!this.props.differential.diffExp)
return <p>Select cells & compute differential to see heatmap</p>; return <p>Select cells & compute differential to see heatmap</p>;
const topGenesForCellSet1 = this.props.differential.diffExp.data.celllist1; const topGenesForCellSet1 = this.props.differential.diffExp.data.celllist1;
const topGenesForCellSet2 = this.props.differential.diffExp.data.celllist2; const topGenesForCellSet2 = this.props.differential.diffExp.data.celllist2;
// const allGeneNames = this.getAllGeneNames(this.props.world);
const extent = d3.extent( const extent = d3.extent(
_.union( _.union(
@@ -9,7 +9,9 @@ import CellSetButton from "./cellSetButtons";
@connect(state => { @connect(state => {
return { return {
differential: state.differential, 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 { class Expression extends React.Component {
@@ -17,6 +19,7 @@ class Expression extends React.Component {
super(props); super(props);
this.state = {}; this.state = {};
} }
handleClick(gene) { handleClick(gene) {
return () => { return () => {
this.props.dispatch({ this.props.dispatch({
@@ -25,6 +28,7 @@ class Expression extends React.Component {
}); });
}; };
} }
computeDiffExp() { computeDiffExp() {
this.props.dispatch( this.props.dispatch(
actions.requestDifferentialExpression( actions.requestDifferentialExpression(
@@ -33,6 +37,7 @@ class Expression extends React.Component {
) )
); );
} }
render() { render() {
if (!this.props.differential) { if (!this.props.differential) {
return null; return null;
+36 -32
View File
@@ -24,10 +24,12 @@ import FaSave from "react-icons/lib/fa/download";
@connect(state => { @connect(state => {
return { return {
cellsMetadata: state.controls.cellsMetadata, world: state.controls.world,
opacityForDeselectedCells: state.controls.opacityForDeselectedCells, crossfilter: state.controls.crossfilter,
responsive: state.responsive, 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 { class Graph extends React.Component {
@@ -43,13 +45,12 @@ class Graph extends React.Component {
colors: null colors: null
}; };
this.state = { this.state = {
drawn: false,
svg: null, svg: null,
ctx: null,
brush: null, brush: null,
mode: "brush" mode: "brush"
}; };
} }
reglDraw(regl, drawPoints, sizeBuffer, colorBuffer, pointBuffer, camera) { reglDraw(regl, drawPoints, sizeBuffer, colorBuffer, pointBuffer, camera) {
regl.clear({ regl.clear({
depth: 1, depth: 1,
@@ -64,6 +65,7 @@ class Graph extends React.Component {
view: camera.view() view: camera.view()
}); });
} }
restartReglLoop() { restartReglLoop() {
const reglRender = this.state.regl.frame(() => { const reglRender = this.state.regl.frame(() => {
this.reglDraw( this.reglDraw(
@@ -83,6 +85,7 @@ class Graph extends React.Component {
reglRender reglRender
}); });
} }
componentDidMount() { componentDidMount() {
// setup canvas and camera // setup canvas and camera
const camera = _camera(this.reglCanvas, { scale: true, rotate: false }); const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
@@ -120,7 +123,16 @@ class Graph extends React.Component {
reglRender reglRender
}); });
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const {
world,
crossfilter,
selectionUpdate,
colorRGB,
responsive
} = this.props;
if ( if (
this.state.reglRender && this.state.reglRender &&
this.reglRenderState === "rendering" && this.reglRenderState === "rendering" &&
@@ -130,25 +142,22 @@ class Graph extends React.Component {
this.reglRenderState = "paused"; this.reglRenderState = "paused";
} }
if (this.state.regl && this.props.crossfilter) { if (this.state.regl && world) {
/* update the regl state */ /* update the regl state */
const crossfilter = this.props.crossfilter.cells; const obsLayout = world.obsLayout;
const cells = crossfilter.all(); const cellCount = crossfilter.size();
const cellCount = cells.length;
// X/Y positions for each point - a cached value that only // X/Y positions for each point - a cached value that only
// changes if we have loaded entirely new cell data // changes if we have loaded entirely new cell data
// //
if ( if (
!this.renderCache.positions || !this.renderCache.positions ||
this.props.crossfilter.cells != prevProps.crossfilter.cells selectionUpdate != prevProps.selectionUpdate
) { ) {
if (!this.renderCache.positions) if (!this.renderCache.positions)
this.renderCache.positions = new Float32Array(2 * cellCount); this.renderCache.positions = new Float32Array(2 * cellCount);
// d3.scaleLinear().domain([0,1]).range([-1,1])
const glScaleX = scaleLinear([0, 1], [-1, 1]); const glScaleX = scaleLinear([0, 1], [-1, 1]);
// d3.scaleLinear().domain([0,1]).range([1,-1])
const glScaleY = scaleLinear([0, 1], [1, -1]); const glScaleY = scaleLinear([0, 1], [1, -1]);
for ( for (
@@ -156,8 +165,8 @@ class Graph extends React.Component {
i < cellCount; i < cellCount;
i++ i++
) { ) {
positions[2 * i] = glScaleX(cells[i].__x__); positions[2 * i] = glScaleX(obsLayout.X[i]);
positions[2 * i + 1] = glScaleY(cells[i].__y__); positions[2 * i + 1] = glScaleY(obsLayout.Y[i]);
} }
this.state.pointBuffer({ this.state.pointBuffer({
data: this.renderCache.positions, 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 // could have changed for some other reason, but for now color is
// the only metadata that changes client-side. If this is problematic, // the only metadata that changes client-side. If this is problematic,
// we could add some sort of color-specific indicator to the app state. // we could add some sort of color-specific indicator to the app state.
if ( if (!this.renderCache.colors || colorRGB != prevProps.colorRGB) {
!this.renderCache.colors || const rgb = colorRGB;
this.props.cellsMetadata != prevProps.cellsMetadata
) {
if (!this.renderCache.colors) if (!this.renderCache.colors)
this.renderCache.colors = new Float32Array(3 * cellCount); this.renderCache.colors = new Float32Array(3 * rgb.length);
for (let i = 0, colors = this.renderCache.colors; i < cellCount; i++) { for (let i = 0, colors = this.renderCache.colors; i < rgb.length; i++) {
colors.set(cells[i].__colorRGB__, 3 * i); colors.set(rgb[i], 3 * i);
} }
this.state.colorBuffer({ data: this.renderCache.colors, dimension: 3 }); 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 // most property upates are due to changes driving a crossfilter
// selection set change. // selection set change.
// //
if ( if (!this.renderCache.sizes)
!this.renderCache.sizes ||
this.props.crossfilter.cells != prevProps.crossfilter.cells
) {
this.renderCache.sizes = new Float32Array(cellCount); this.renderCache.sizes = new Float32Array(cellCount);
}
crossfilter.fillByIsFiltered(this.renderCache.sizes, 4, 0.2); crossfilter.fillByIsFiltered(this.renderCache.sizes, 4, 0.2);
this.state.sizeBuffer({ data: this.renderCache.sizes, dimension: 1 }); this.state.sizeBuffer({ data: this.renderCache.sizes, dimension: 1 });
@@ -211,12 +214,10 @@ class Graph extends React.Component {
} }
if ( if (
prevProps.responsive.height !== this.props.responsive.height || prevProps.responsive.height !== responsive.height ||
prevProps.responsive.width !== this.props.responsive.width || prevProps.responsive.width !== responsive.width ||
/* first time */ /* first time */
(this.props.responsive.height && (responsive.height && responsive.width && !this.state.svg)
this.props.responsive.width &&
!this.state.svg)
) { ) {
/* clear out whatever was on the div, even if nothing, but usually the brushes etc */ /* clear out whatever was on the div, even if nothing, but usually the brushes etc */
d3.select("#graphAttachPoint") d3.select("#graphAttachPoint")
@@ -225,12 +226,13 @@ class Graph extends React.Component {
const { svg, brush, brushContainer } = setupSVGandBrushElements( const { svg, brush, brushContainer } = setupSVGandBrushElements(
this.handleBrushSelectAction.bind(this), this.handleBrushSelectAction.bind(this),
this.handleBrushDeselectAction.bind(this), this.handleBrushDeselectAction.bind(this),
this.props.responsive, responsive,
this.graphPaddingTop this.graphPaddingTop
); );
this.setState({ svg, brush, brushContainer }); this.setState({ svg, brush, brushContainer });
} }
} }
handleBrushSelectAction() { handleBrushSelectAction() {
/* This conditional handles procedural brush deselect. Brush emits an event on procedural deselect because it is move: null */ /* This conditional handles procedural brush deselect. Brush emits an event on procedural deselect because it is move: null */
if (d3.event.sourceEvent !== null) { if (d3.event.sourceEvent !== null) {
@@ -280,6 +282,7 @@ class Graph extends React.Component {
}); });
} }
} }
handleBrushDeselectAction() { handleBrushDeselectAction() {
if (d3.event && !d3.event.selection) { if (d3.event && !d3.event.selection) {
this.props.dispatch({ this.props.dispatch({
@@ -295,6 +298,7 @@ class Graph extends React.Component {
}); });
} }
} }
handleOpacityRangeChange(e) { handleOpacityRangeChange(e) {
this.props.dispatch({ this.props.dispatch({
type: "change opacity deselected cells in 2d graph background", type: "change opacity deselected cells in 2d graph background",
-163
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;
-42
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;
}
-38
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;
-29
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;
+111 -116
View File
@@ -5,38 +5,55 @@
import React from "react"; import React from "react";
import _ from "lodash"; import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
import _regl from "regl";
import scatterplot from "./scatterplot";
import setupScatterplot from "./setupScatterplot";
import styles from "./scatterplot.css";
import * as d3 from "d3"; import * as d3 from "d3";
import mat4 from "gl-mat4"; import _camera from "../../util/camera";
import fit from "canvas-fit";
import _camera from "../../util/camera.js"; import setupScatterplot from "./setupScatterplot";
import _regl from "regl"; import styles from "./scatterplot.css";
import _drawPoints from "./drawPointsRegl"; import _drawPoints from "./drawPointsRegl";
import { scaleLinear } from "../../util/scaleLinear"; import { scaleLinear } from "../../util/scaleLinear";
import { margin, width, height, createDimensions } from "./util"; import { margin, width, height } from "./util";
@connect(state => { @connect(state => {
const ranges = _.get(state, "cells.cells.data.ranges", null); const {
const metadata = _.get(state, "cells.cells.data.metadata", null); world,
const initializeRanges = _.get(state, "initialize.data.data.ranges", null); 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 { return {
ranges, world,
metadata,
initializeRanges, colorRGB: state.controls.colorRGB,
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale, colorScale: state.controls.colorScale,
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor, // Accessors are var/gene names (strings)
scatterplotXXaccessor,
scatterplotYYaccessor,
opacityForDeselectedCells: state.controls.opacityForDeselectedCells, opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
crossfilter: state.controls.crossfilter,
differential: state.differential, 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 { class Scatterplot extends React.Component {
@@ -46,9 +63,6 @@ class Scatterplot extends React.Component {
this.axes = false; this.axes = false;
this.state = { this.state = {
svg: null, svg: null,
// ctx: null,
axes: null,
dimensions: null,
xScale: null, xScale: null,
yScale: null yScale: null
}; };
@@ -57,19 +71,10 @@ class Scatterplot extends React.Component {
componentDidMount() { componentDidMount() {
const { svg } = setupScatterplot(width, height, margin); const { svg } = setupScatterplot(width, height, margin);
let scales; 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 (svg && expressionX && expressionY) {
if ( scales = Scatterplot.setupScales(expressionX, expressionY);
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
);
this.drawAxesSVG(scales.xScale, scales.yScale, svg); this.drawAxesSVG(scales.xScale, scales.yScale, svg);
} }
@@ -115,115 +120,101 @@ class Scatterplot extends React.Component {
colorBuffer colorBuffer
}); });
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const {
svg,
xScale,
yScale,
regl,
pointBuffer,
colorBuffer,
sizeBuffer
} = this.state;
const {
world,
crossfilter,
scatterplotXXaccessor,
scatterplotYYaccessor,
expressionX,
expressionY,
colorRGB
} = this.props;
if ( if (
this.state.svg && world &&
this.state.xScale && svg &&
this.state.yScale && xScale &&
this.props.scatterplotXXaccessor && yScale &&
this.props.scatterplotYYaccessor && scatterplotXXaccessor &&
(this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc scatterplotYYaccessor &&
this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor || // was CLU now FTH1 etc (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.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 ( if (
this.props.metadata && world &&
this.state.regl && regl &&
this.state.pointBuffer && pointBuffer &&
this.state.colorBuffer && colorBuffer &&
this.state.sizeBuffer && sizeBuffer &&
this.props.expression.data && expressionX &&
this.props.expression.data.genes && expressionY &&
this.props.scatterplotXXaccessor && scatterplotXXaccessor &&
this.props.scatterplotYYaccessor && scatterplotYYaccessor &&
this.state.xScale && xScale &&
this.state.yScale yScale
) { ) {
const crossfilter = this.props.crossfilter.cells; const cellCount = expressionX.length;
const data = this.props.expression.data; const positionsBuf = new Float32Array(2 * cellCount);
const cells = data.cells; const colorsBuf = new Float32Array(3 * cellCount);
const genes = data.genes; const sizesBuf = new Float32Array(cellCount);
const cellCount = cells.length;
const positions = new Float32Array(2 * cellCount);
const colors = new Float32Array(3 * cellCount);
const sizes = new Float32Array(cellCount);
// d3.scaleLinear().domain([0, width]).range([-0.95, 0.95])
const glScaleX = scaleLinear([0, width], [-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 glScaleY = scaleLinear([0, height], [-1, 1]);
const geneXXaccessorIndex = genes.indexOf(
this.props.scatterplotXXaccessor
);
const geneYYaccessorIndex = genes.indexOf(
this.props.scatterplotYYaccessor
);
/* /*
Construct Vectors Construct Vectors
*/ */
for (let i = 0; i < cellCount; i++) { for (let i = 0; i < cellCount; i += 1) {
const cell = cells[i]; positionsBuf[2 * i] = glScaleX(xScale(expressionX[i]));
positionsBuf[2 * i + 1] = glScaleY(yScale(expressionY[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++) { for (let i = 0; i < cellCount; i += 1) {
const metadata = this.props.metadata[i]; colorsBuf.set(colorRGB[i], 3 * i);
colors.set(metadata.__colorRGB__, 3 * i);
} }
crossfilter.fillByIsFiltered(sizes, 4, 0.2); crossfilter.fillByIsFiltered(sizesBuf, 4, 0.2);
this.state.pointBuffer({ data: positions, dimension: 2 }); pointBuffer({ data: positionsBuf, dimension: 2 });
this.state.colorBuffer({ data: colors, dimension: 3 }); colorBuffer({ data: colorsBuf, dimension: 3 });
this.state.sizeBuffer({ data: sizes, dimension: 1 }); sizeBuffer({ data: sizesBuf, dimension: 1 });
this.count = cellCount; this.count = cellCount;
} }
if ( if (
this.props.expression && expressionX &&
this.props.expression.data && expressionY &&
this.props.scatterplotXXaccessor && (scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
this.props.scatterplotYYaccessor && scatterplotYYaccessor !== prevProps.scatterplotYYaccessor)
(this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor)
) { ) {
const scales = this.setupScales( const scales = Scatterplot.setupScales(expressionX, expressionY);
this.props.expression,
this.props.scatterplotXXaccessor,
this.props.scatterplotYYaccessor
);
this.setState(scales); this.setState(scales);
} }
} }
setupScales(expression, scatterplotXXaccessor, scatterplotYYaccessor) {
static setupScales(expressionX, expressionY) {
const xScale = d3 const xScale = d3
.scaleLinear() .scaleLinear()
.domain( .domain(d3.extent(expressionX))
d3.extent(expression.data.cells, (cell, i) => {
return cell.e[expression.data.genes.indexOf(scatterplotXXaccessor)];
})
)
.range([0, width]); .range([0, width]);
const yScale = d3 const yScale = d3
.scaleLinear() .scaleLinear()
.domain( .domain(d3.extent(expressionY))
d3.extent(expression.data.cells, cell => {
return cell.e[expression.data.genes.indexOf(scatterplotYYaccessor)];
})
)
.range([height, 0]); .range([height, 0]);
return { return {
@@ -231,15 +222,19 @@ class Scatterplot extends React.Component {
yScale yScale
}; };
} }
drawAxesSVG(xScale, yScale, svg) { drawAxesSVG(xScale, yScale, svg) {
const { scatterplotYYaccessor, scatterplotXXaccessor } = this.props;
svg.selectAll("*").remove(); svg.selectAll("*").remove();
// the axes are much cleaner and easier now. No need to rotate and orient the axis, just call axisBottom, axisLeft etc. // the axes are much cleaner and easier now. No need to rotate and orient
var xAxis = d3.axisBottom().scale(xScale); // 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 svg
.append("g") .append("g")
.attr("transform", "translate(0," + height + ")") .attr("transform", "translate(0," + height + ")")
@@ -259,7 +254,7 @@ class Scatterplot extends React.Component {
.attr("x", 10) .attr("x", 10)
.attr("y", 10) .attr("y", 10)
.attr("class", "label") .attr("class", "label")
.text(this.props.scatterplotYYaccessor); .text(scatterplotYYaccessor);
svg svg
.append("text") .append("text")
@@ -267,7 +262,7 @@ class Scatterplot extends React.Component {
.attr("y", height - 10) .attr("y", height - 10)
.attr("text-anchor", "end") .attr("text-anchor", "end")
.attr("class", "label") .attr("class", "label")
.text(this.props.scatterplotXXaccessor); .text(scatterplotXXaccessor);
} }
render() { render() {
+2 -2
View File
@@ -41,6 +41,8 @@ export const brightBlue = "#4a90e2";
export const brightGreen = "#A2D729"; export const brightGreen = "#A2D729";
export const darkGreen = "#448C4D"; export const darkGreen = "#448C4D";
export const defaultCellColor = "rgb(0,0,0,1)";
export const tiniestFontSize = 12; export const tiniestFontSize = 12;
export const bolder = 700; export const bolder = 700;
@@ -48,8 +50,6 @@ export const bolder = 700;
export let API = { export let API = {
// prefix: "http://api.clustering.czi.technology/api/", // prefix: "http://api.clustering.czi.technology/api/",
//prefix: "http://tabulamuris.cxg.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/", prefix: "http://api-staging.clustering.czi.technology/api/",
version: "v0.1/" version: "v0.1/"
+84 -107
View File
@@ -1,138 +1,115 @@
// jshint esversion: 6 // jshint esversion: 6
import uri from "urijs";
import * as globals from "../globals";
import _ from "lodash"; import _ from "lodash";
import { parseRGB } from "../util/parseRGB";
import * as d3 from "d3"; import * as d3 from "d3";
import { interpolateViridis } from "d3-scale-chromatic"; 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 https://medium.com/@jacobp100/you-arent-using-redux-middleware-enough-94ffe991e6
storeInstance => functionToCallWithAnActionThatWillSendItToTheNextMiddleware => actionThatDispatchWasCalledWith => valueToUseAsTheReturnValueOfTheDispatchCall storeInstance =>
functionToCallWithAnActionThatWillSendItToTheNextMiddleware =>
actionThatDispatchWasCalledWith =>
valueToUseAsTheReturnValueOfTheDispatchCall
*/ */
/* /*
What this file does: What this file does:
1. fire a filter action anywhere in the app 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 2. ** this middleware checks to see the state of all the currently selected filters,
3. ** create updated selection from a copy of all the cells presently on the client (this may be a subset of 'all') including the new one
4. ** append that new selection to the action so that it magically appears in the reducer just because the action was fired 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 => { const updateCellColorsMiddleware = store => next => action => {
return next => { const s = store.getState();
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 = this is a hardcoded map of the things we need to keep an eye on and update
action.type === "color by expression" || global cell selection in response to
action.type === "color by continuous metadata" || */
action.type === "color by categorical metadata"; const filterJustChanged =
action.type === "color by expression" ||
action.type === "color by continuous metadata" ||
action.type === "color by categorical metadata";
if (!filterJustChanged || !s.controls.cellsMetadata) { if (!filterJustChanged || !s.controls.world.obsAnnotations) {
return next( return next(
action action
); /* if the cells haven't loaded or the action wasn't a color change, bail */ ); /* if the cells haven't loaded or the action wasn't a color change, bail */
} }
let cellsMetadataWithUpdatedColors = s.controls.cellsMetadata.slice(0); const { obsAnnotations } = s.controls.world;
let colorScale; 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. if (action.type === "color by categorical metadata") {
(b) each time a user changes a color control we need to update cellsMetadata colors 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") { if (action.type === "color by continuous metadata") {
colorScale = d3.scaleOrdinal().range(globals.ordinalColors); colorScale = d3
.scaleLinear()
.domain([0, action.rangeMaxForColorAccessor])
.range([1, 0]);
for (let i = 0; i < cellsMetadataWithUpdatedColors.length; i++) { for (let i = 0; i < obsAnnotations.length; i += 1) {
const cell = cellsMetadataWithUpdatedColors[i]; const obs = obsAnnotations[i];
let c = colorScale(cell[action.colorAccessor]); const c = interpolateViridis(colorScale(obs[action.colorAccessor]));
cell.__color__ = c; colorsByName[i] = c;
cell.__colorRGB__ = parseRGB(c); colorsByRGB[i] = parseRGB(c);
} }
} }
if (action.type === "color by continuous metadata") { if (action.type === "color by expression") {
colorScale = d3 const { gene, data } = action;
.scaleLinear() const expression = data[gene]; // Float32Array
.domain([0, action.rangeMaxForColorAccessor]) colorScale = d3
.range([1, 0]); .scaleLinear()
.domain([_.min(expression), _.max(expression)])
.range([
1,
0
]); /* invert viridis... probably pass this scale through to others */
_.each(cellsMetadataWithUpdatedColors, (cell, i) => { for (let i = 0, len = expression.length; i < len; i += 1) {
let c = interpolateViridis(colorScale(cell[action.colorAccessor])); const c = interpolateViridis(colorScale(expression[i]));
cellsMetadataWithUpdatedColors[i]["__color__"] = c; colorsByName[i] = c;
cellsMetadataWithUpdatedColors[i]["__colorRGB__"] = parseRGB(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 = {}; return next(modifiedAction);
/*
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);
};
};
}; };
export default updateCellColorsMiddleware; export default updateCellColorsMiddleware;
@@ -1,105 +0,0 @@
// jshint esversion: 6
import uri from "urijs";
import * as globals from "../globals";
/*
XXX: this file should be obsolete. We just need to complete the refactoring
of parallel.js and it can be removed entirely.
It is currently not in use - the middleware constructor does not include include it
*/
/*
https://medium.com/@jacobp100/you-arent-using-redux-middleware-enough-94ffe991e6
storeInstance => functionToCallWithAnActionThatWillSendItToTheNextMiddleware => actionThatDispatchWasCalledWith => valueToUseAsTheReturnValueOfTheDispatchCall
*/
/*
What this file does:
1. fire a filter action anywhere in the app
2. ** this middleware checks to see the state of all the currently selected filters, including the new one
3. ** create updated selection from a copy of all the cells presently on the client (this may be a subset of 'all')
4. ** append that new selection to the action so that it magically appears in the reducer just because the action was fired
This is nice because we keep a lot of filtering business logic centralized (what it means in practice to be selected)
*/
const updateCellSelectionMiddleware = store => {
return next => {
return action => {
const s = store.getState();
/* this is a hardcoded map of the things we need to keep an eye on and update global cell selection in response to */
const filterJustChanged =
action.type === "continuous selection using parallel coords brushing" ||
action.type === "continuous metadata histogram brush" ||
action.type === "graph brush selection change" ||
action.type === "graph brush deselect" ||
action.type === "categorical metadata filter deselect" ||
action.type === "categorical metadata filter select" ||
action.type === "categorical metadata filter none of these" ||
action.type === "categorical metadata filter all of these";
if (!filterJustChanged || !s.controls.cellsMetadata) {
return next(
action
); /* if the cells haven't loaded or the action wasn't a filter, bail */
}
/*
- make a FRESH copy of all of the cells
- metadata has cellname and index, and that's all we ever need to reference cell info
*/
let newSelection = s.controls.cellsMetadata.slice(0);
// _.forEach(newSelection, cell => (cell.__selected__ = true));
for (let i = 0; i < newSelection.length; i++) {
newSelection[i].__selected__ = true;
}
/*
in plain language...
(a) once the cells have loaded.
(b) each time a user changes ANY control we need to update cellsMetadata
there are two states:
1. control state we already know about (state.foo)
2. control states that override states we already know about (action.foo applied instead of state.foo)
*/
if (
(action.type ===
"continuous selection using parallel coords brushing" &&
s.controls.continuousSelection) ||
s.controls.continuousSelection
) {
_.each(newSelection, (cell, i) => {
const cellExtentsAreWithinContinuousSelectionBounds = s.controls.continuousSelection.every(
active => {
// test if point is within extents for each active brush
return active.dimension.type.within(
cell[active.dimension.key],
active.extent,
active.dimension
);
}
);
if (!cellExtentsAreWithinContinuousSelectionBounds) {
newSelection[i]["__selected__"] = false;
}
});
}
let modifiedAction = Object.assign({}, action, {
newSelection
}); /* append the result of all the filters to the action the user just triggered */
return next(modifiedAction);
};
};
};
export default updateCellSelectionMiddleware;
-41
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;
+200 -240
View File
@@ -1,117 +1,44 @@
// jshint esversion: 6 // jshint esversion: 6
import _ from "lodash"; import _ from "lodash";
import { World, kvCache } from "../util/stateManager";
import { parseRGB } from "../util/parseRGB"; 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 function createCategoricalAsBooleansMap(world) {
// schema description. const res = {};
// _.each(world.summary.obs, (value, key) => {
function deduceDimensionType(attributes, fieldName) { if (value.options) {
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 */
) {
const optionsAsBooleans = {}; const optionsAsBooleans = {};
_.each(value.options, (_value, _key) => { _.each(value.options, (_value, _key) => {
optionsAsBooleans[_key] = true; optionsAsBooleans[_key] = true;
}); });
categoricalAsBooleansMap[key] = optionsAsBooleans; res[key] = optionsAsBooleans;
} }
}); });
return res;
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
};
} }
const Controls = ( const Controls = (
state = { state = {
/* Universe - all cells known to us. Set once, during initial load */ // data loading flag
_ranges: null /* this comes from initialize, this is universe */, loading: false,
allGeneNames: null, error: 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,
/* View / World - all cells currently being displayed. May be a subset of Universe. */ universe: null,
cellsMetadata: null,
crossfilter: null /* the current user selection state */, // all of the data + selection state
world: null,
colorName: null,
colorRGB: null,
categoricalAsBooleansMap: null, categoricalAsBooleansMap: null,
crossfilter: null,
dimensionMap: null,
colorAccessor: null, colorAccessor: null,
colorScale: null, colorScale: null,
opacityForDeselectedCells: 0.2, opacityForDeselectedCells: 0.2,
graphBrushSelection: null, graphBrushSelection: null,
continuousSelection: null, continuousSelection: null,
@@ -123,118 +50,161 @@ const Controls = (
}, },
action 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) { switch (action.type) {
/********************************** /*****************************************************
Keep a copy of 'universe' Initialization, World/Universe management
***********************************/ and data loading.
case "initialize success": { ******************************************************/
if (!action.data.data.schema) { case "initial data load start": {
console.error("Warning - REST API omitted schema description."); return { ...state, loading: true };
}
return Object.assign({}, state, {
_ranges: action.data.data.ranges,
allGeneNames: action.data.data.genes,
schema: action.data.data.schema
});
} }
case "request cells success": { case "initial data load complete (universe exists)":
// If we don't have a schema (bad server!), fake it by inferring case "reset World to eq Universe": {
// important fields from the ranges element. /* first light - create world & other data-driven defaults */
// const { universe } = action;
if (!state.schema) { const world = World.createWorldFromEntireUniverse(universe);
state.schema = createSchemaByDataSniffing(action.data.data.ranges); const colorName = new Array(universe.nObs).fill(globals.defaultCellColor);
} const colorRGB = _.map(colorName, c => parseRGB(c));
const categoricalAsBooleansMap = createCategoricalAsBooleansMap(world);
/* Set viewable world to the provided cell data */ const crossfilter = Crossfilter(world.obsAnnotations);
const viewState = createViewState(state.schema, action.data.data); const dimensionMap = World.createObsDimensionMap(crossfilter, world);
return Object.assign({}, state, { return {
/* Universe - initialize once */ ...state,
allCells: state.allCells ? state.allCells : action.data, loading: false,
allCellsMetadata: state.allCellsMetadata error: null,
? state.allCellsMetadata universe,
: viewState.cellsMetadata, world,
allCellsMetadataMap: state.allCellsMetadataMap colorName,
? state.allCellsMetadataMap colorRGB,
: _.keyBy(viewState.cellsMetadata, "CellName"), categoricalAsBooleansMap,
crossfilter,
/* World */ dimensionMap,
...viewState, colorAccessor: null
};
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 "set World to current selection": {
User events /* Set viewable world to be the currently selected data */
* * * * * * * * * * * * * * * * * */ const world = World.createWorldFromCurrentSelection(
case "reset graph": { action.universe,
/* Reset viewable world to the entire Universe */ action.world,
const viewState = createViewState(state.schema, state.allCells.data); action.crossfilter
return Object.assign({}, state, { );
...viewState 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": { case "expression load success": {
return Object.assign({}, state, { const { world, universe } = state;
axesHaveBeenDrawn: true let universeVarDataCache = universe.varDataCache;
}); let worldVarDataCache = world.varDataCache;
} _.forEach(action.expressionData, (val, key) => {
case "continuous selection using parallel coords brushing": { universeVarDataCache = kvCache.set(universeVarDataCache, key, val);
return Object.assign({}, state, { if (kvCache.get(worldVarDataCache, key) === undefined) {
continuousSelection: action.data, worldVarDataCache = kvCache.set(
crossfilter: { worldVarDataCache,
...state.crossfilter 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": { case "graph brush selection change": {
state.crossfilter.dimensionMap.x.filterRange([ state.dimensionMap.x.filterRange([
action.brushCoords.northwest[0], action.brushCoords.northwest[0],
action.brushCoords.southeast[0] action.brushCoords.southeast[0]
]); ]);
state.crossfilter.dimensionMap.y.filterRange([ state.dimensionMap.y.filterRange([
action.brushCoords.southeast[1], action.brushCoords.southeast[1],
action.brushCoords.northwest[1] action.brushCoords.northwest[1]
]); ]);
return Object.assign({}, state, { return {
graphBrushSelection: action.brushCoords, ...state,
crossfilter: { graphBrushSelection: action.brushCoords
...state.crossfilter };
}
});
} }
case "graph brush deselect": { case "graph brush deselect": {
state.crossfilter.dimensionMap.x.filterAll(); state.dimensionMap.x.filterAll();
state.crossfilter.dimensionMap.y.filterAll(); state.dimensionMap.y.filterAll();
return Object.assign({}, state, { return {
graphBrushSelection: null, ...state,
crossfilter: { graphBrushSelection: null
...state.crossfilter };
}
});
} }
case "continuous metadata histogram brush": { case "continuous metadata histogram brush": {
// action.selection: metadata name being selected // action.selection: metadata name being selected
// action.range: filter range, or null if deselected // action.range: filter range, or null if deselected
if (!action.range) { if (!action.range) {
state.crossfilter.dimensionMap[action.selection].filterAll(); state.dimensionMap[action.selection].filterAll();
} else { } else {
state.crossfilter.dimensionMap[action.selection].filterRange( state.dimensionMap[action.selection].filterRange(action.range);
action.range
);
} }
return Object.assign({}, state, { return { ...state };
crossfilter: {
...state.crossfilter
}
});
} }
case "change opacity deselected cells in 2d graph background": case "change opacity deselected cells in 2d graph background":
return Object.assign({}, state, { return {
...state,
opacityForDeselectedCells: action.data opacityForDeselectedCells: action.data
}); };
/******************************* /*******************************
Categorical metadata Categorical metadata
*******************************/ *******************************/
case "categorical metadata filter select": { case "categorical metadata filter select": {
const newCategoricalAsBooleansMap = { const newCategoricalAsBooleansMap = {
...state.categoricalAsBooleansMap, ...state.categoricalAsBooleansMap,
@@ -244,7 +214,7 @@ const Controls = (
} }
}; };
// update the filter for the one category that changed state // update the filter for the one category that changed state
state.crossfilter.dimensionMap[action.metadataField].filterEnum( state.dimensionMap[action.metadataField].filterEnum(
_.filter( _.filter(
_.map( _.map(
newCategoricalAsBooleansMap[action.metadataField], newCategoricalAsBooleansMap[action.metadataField],
@@ -252,12 +222,10 @@ const Controls = (
) )
) )
); );
return Object.assign({}, state, { return {
categoricalAsBooleansMap: newCategoricalAsBooleansMap, ...state,
crossfilter: { categoricalAsBooleansMap: newCategoricalAsBooleansMap
...state.crossfilter };
}
});
} }
case "categorical metadata filter deselect": { case "categorical metadata filter deselect": {
const newCategoricalAsBooleansMap = { const newCategoricalAsBooleansMap = {
@@ -268,7 +236,7 @@ const Controls = (
} }
}; };
// update the filter for the one category that changed state // update the filter for the one category that changed state
state.crossfilter.dimensionMap[action.metadataField].filterEnum( state.dimensionMap[action.metadataField].filterEnum(
_.filter( _.filter(
_.map( _.map(
newCategoricalAsBooleansMap[action.metadataField], newCategoricalAsBooleansMap[action.metadataField],
@@ -276,12 +244,10 @@ const Controls = (
) )
) )
); );
return Object.assign({}, state, { return {
categoricalAsBooleansMap: newCategoricalAsBooleansMap, ...state,
crossfilter: { categoricalAsBooleansMap: newCategoricalAsBooleansMap
...state.crossfilter };
}
});
} }
case "categorical metadata filter none of these": { case "categorical metadata filter none of these": {
const newCategoricalAsBooleansMap = { const newCategoricalAsBooleansMap = {
@@ -293,13 +259,11 @@ const Controls = (
c[k] = false; c[k] = false;
} }
); );
state.crossfilter.dimensionMap[action.metadataField].filterNone(); state.dimensionMap[action.metadataField].filterNone();
return Object.assign({}, state, { return {
categoricalAsBooleansMap: newCategoricalAsBooleansMap, ...state,
crossfilter: { categoricalAsBooleansMap: newCategoricalAsBooleansMap
...state.crossfilter };
}
});
} }
case "categorical metadata filter all of these": { case "categorical metadata filter all of these": {
const newCategoricalAsBooleansMap = { const newCategoricalAsBooleansMap = {
@@ -311,54 +275,50 @@ const Controls = (
c[k] = true; c[k] = true;
} }
); );
state.crossfilter.dimensionMap[action.metadataField].filterAll(); state.dimensionMap[action.metadataField].filterAll();
return Object.assign({}, state, { return {
categoricalAsBooleansMap: newCategoricalAsBooleansMap, ...state,
crossfilter: { categoricalAsBooleansMap: newCategoricalAsBooleansMap
...state.crossfilter };
}
});
} }
/******************************* /*******************************
Color Scale 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
});
case "color by categorical metadata": case "color by categorical metadata":
return Object.assign({}, state, { case "color by continuous metadata": {
colorAccessor: return {
action.colorAccessor /* pass the scale through additionally, and it's a legend! */, ...state,
cellsMetadata: colorName: action.colors.name,
action.cellsMetadataWithUpdatedColors /* this comes from middleware */, colorRGB: action.colors.rgb,
colorAccessor: action.colorAccessor,
colorScale: action.colorScale colorScale: action.colorScale
}); };
case "store current cell selection as differential set 1": }
return Object.assign({}, state, { case "color by expression": {
__storedStateForCelllist1__: action.data return {
}); ...state,
colorName: action.colors.name,
colorRGB: action.colors.rgb,
colorAccessor: action.gene,
colorScale: action.colorScale
};
}
/******************************* /*******************************
Scatterplot Scatterplot
*******************************/ *******************************/
case "set scatterplot x": case "set scatterplot x":
return Object.assign({}, state, { return {
...state,
scatterplotXXaccessor: action.data scatterplotXXaccessor: action.data
}); };
case "set scatterplot y": case "set scatterplot y":
return Object.assign({}, state, { return {
...state,
scatterplotYYaccessor: action.data scatterplotYYaccessor: action.data
}); };
default: default:
return state; return state;
} }
+15 -10
View File
@@ -11,29 +11,34 @@ const Differential = (
) => { ) => {
switch (action.type) { switch (action.type) {
case "request differential expression started": case "request differential expression started":
return Object.assign({}, state, { return {
...state,
loading: true, loading: true,
error: null error: null
}); };
case "request differential expression success": case "request differential expression success":
return Object.assign({}, state, { return {
...state,
error: null, error: null,
loading: false, loading: false,
diffExp: action.data diffExp: action.data
}); };
case "request differential expression error": case "request differential expression error":
return Object.assign({}, state, { return {
...state,
loading: false, loading: false,
error: action.data error: action.data
}); };
case "store current cell selection as differential set 1": case "store current cell selection as differential set 1":
return Object.assign({}, state, { return {
...state,
celllist1: action.data celllist1: action.data
}); };
case "store current cell selection as differential set 2": case "store current cell selection as differential set 2":
return Object.assign({}, state, { return {
...state,
celllist2: action.data celllist2: action.data
}); };
default: default:
return state; return state;
} }
-33
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;
+6 -19
View File
@@ -1,36 +1,23 @@
// jshint esversion: 6 // jshint esversion: 6
import { combineReducers, createStore, applyMiddleware } from "redux"; import { combineReducers, createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import updateURLMiddleware from "../middleware/updateURLMiddleware"; import updateURLMiddleware from "../middleware/updateURLMiddleware";
// import updateCellSelectionMiddleware from "../middleware/updateCellSelectionMiddleware";
import updateCellColors from "../middleware/updateCellColors"; 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 differential from "./differential";
import responsive from "./responsive"; import responsive from "./responsive";
import controls from "./controls";
const Reducer = combineReducers({ const Reducer = combineReducers({
initialize, responsive,
cells,
expression,
controls, controls,
differential, differential
responsive
}); });
let store = createStore( const store = createStore(
Reducer, Reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
applyMiddleware( applyMiddleware(thunk, updateURLMiddleware, updateCellColors)
thunk,
updateURLMiddleware,
// updateCellSelectionMiddleware,
updateCellColors
)
); );
export default store; export default store;
-33
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;
+3 -2
View File
@@ -8,10 +8,11 @@ const Responsive = (
) => { ) => {
switch (action.type) { switch (action.type) {
case "window resize": case "window resize":
return Object.assign({}, state, { return {
...state,
width: action.data.width, width: action.data.width,
height: action.data.height height: action.data.height
}); };
default: default:
return state; return state;
} }
+19
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";
@@ -0,0 +1,72 @@
// jshint esversion: 6
import _ from "lodash";
/*
Very simple key/value cache for use by World & Universe.
* constructor(lowWatermark, cachekey):
- lowWatermark defines the number of cache elements below which
flushing will not occur.
- minTTL defines minimum time in MS that cache entries will live.
A value of -1 disables automatic flushing (flush() can still
be called by external user).
- cachekey is a key that will be assigned to any value to track age
* set() - add a key/val pair.
* get() - get a value or undefined if not present.
* flush(minAgeMs) - flush cache entries in excess of lowWatermark if those
entries are older than minAgeMs.
*/
const cachePrivateKey = "__kvcachekey__";
function create(lowWatermark = 32, minTTL = 1000) {
return {
[cachePrivateKey]: {
lowWatermark,
minTTL
}
};
}
function get(kvcache, key) {
const val = kvcache[key];
if (val) {
val[cachePrivateKey] = Date.now();
}
return val;
}
function set(kvcache, key, val) {
const newKvCache = { ...kvcache };
newKvCache[key] = val;
val[cachePrivateKey] = Date.now();
flush(newKvCache, newKvCache[cachePrivateKey].minTTL);
return newKvCache;
}
/*
Flush elements from cache IF cache size is greater than lowWatermark, and
those elements are older than minAgeMS
*/
function flush(kvcache, minAgeMs = 0) {
if (minAgeMs < 0) return kvcache;
const eol = Date.now() - minAgeMs;
const { lowWatermark } = kvcache[cachePrivateKey];
const keys = _(kvcache)
.keys()
.filter(k => k !== cachePrivateKey)
.filter(k => kvcache[k][cachePrivateKey] < eol)
.sortBy([k => kvcache[k][cachePrivateKey]])
.value();
if (keys.length > lowWatermark) {
const numKeysToDelete = keys.length - lowWatermark;
const keysToDelete = _.slice(keys, 0, numKeysToDelete);
_.forEach(keysToDelete, k => delete kvcache[k]);
}
return kvcache;
}
export { create, get, set, flush };
+252
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;
}
+315
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;
}
+42 -44
View File
@@ -1,5 +1,5 @@
"use strict";
// jshint esversion: 6 // jshint esversion: 6
/* eslint no-bitwise: "off" */
// BitArray is a 2D bitarray with size [length, nBitWidth]. // BitArray is a 2D bitarray with size [length, nBitWidth].
// Each bit is referred to as a `dimension`. Dimensions may be // Each bit is referred to as a `dimension`. Dimensions may be
@@ -29,7 +29,7 @@ class BitArray {
// Fixed for the life of this object. // Fixed for the life of this object.
this.length = length; 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.width = 1; // underlying number of 32 bit arrays
this.dimensionCount = 0; // num allocated dimensions this.dimensionCount = 0; // num allocated dimensions
@@ -48,10 +48,16 @@ class BitArray {
// //
countAllOnes() { countAllOnes() {
let count = 0; let count = 0;
for (let i = 0; i < this.width; i++) { const { bitarray, bitmask, length, width } = this;
const bitmask = this.bitmask[i]; for (let l = 0; l < length; l += 1) {
for (let j = i * this.length, len = j + this.length; j < len; j++) { let dimensionsSet = 0;
if (this.bitarray[i * this.length + j] === bitmask) count++; for (let w = 0; w < width; w += 1) {
if (bitarray[w * length + l] === bitmask[w]) {
dimensionsSet += 1;
}
}
if (dimensionsSet === width) {
count += 1;
} }
} }
return count; return count;
@@ -59,10 +65,11 @@ class BitArray {
// count trailing zeros - hard to do fast in JS! // count trailing zeros - hard to do fast in JS!
// https://en.wikipedia.org/wiki/Find_first_set#CTZ // https://en.wikipedia.org/wiki/Find_first_set#CTZ
static ctz(v) { static ctz(av) {
let c = 32; let c = 32;
let v = av;
v &= -v; // isolate lowest non-zero bit v &= -v; // isolate lowest non-zero bit
if (v) c--; if (v) c -= 1;
if (v & 0x0000ffff) c -= 16; if (v & 0x0000ffff) c -= 16;
if (v & 0x00ff00ff) c -= 8; if (v & 0x00ff00ff) c -= 8;
if (v & 0x0f0f0f0f) c -= 4; if (v & 0x0f0f0f0f) c -= 4;
@@ -74,8 +81,7 @@ class BitArray {
// find a free dimension. Return undefined if none // find a free dimension. Return undefined if none
_findFreeDimension() { _findFreeDimension() {
let dim; let dim;
for (let col = 0; col < this.width; col++) { for (let col = 0; col < this.width; col += 1) {
const bitmask = this.bitmask[col];
const lowestZeroBit = ~this.bitmask[col] & -~this.bitmask[col]; const lowestZeroBit = ~this.bitmask[col] & -~this.bitmask[col];
if (lowestZeroBit) { if (lowestZeroBit) {
this.bitmask[col] |= lowestZeroBit; this.bitmask[col] |= lowestZeroBit;
@@ -92,7 +98,7 @@ class BitArray {
// if we did not find free dimension, expand the bitarray. // if we did not find free dimension, expand the bitarray.
if (dim === undefined) { if (dim === undefined) {
this.width++; this.width += 1;
const biggerBitArray = new Int32Array(this.width * this.length); const biggerBitArray = new Int32Array(this.width * this.length);
biggerBitArray.set(this.bitarray); biggerBitArray.set(this.bitarray);
@@ -105,7 +111,7 @@ class BitArray {
dim = this._findFreeDimension(); dim = this._findFreeDimension();
} }
this.dimensionCount++; this.dimensionCount += 1;
return dim; return dim;
} }
@@ -117,17 +123,15 @@ class BitArray {
this.deselectAll(dim); this.deselectAll(dim);
const col = dim >>> 5; const col = dim >>> 5;
this.bitmask[col] &= ~(1 << dim % 32); this.bitmask[col] &= ~(1 << dim % 32);
this.dimensionCount--; this.dimensionCount -= 1;
} }
// return true if this index is selected in ALL dimensions. // return true if this index is selected in ALL dimensions.
// //
isSelected(index) { isSelected(index) {
const width = this.width; const { width, length, bitarray } = this;
const length = this.length;
const bitarray = this.bitarray;
for (let w = 0; w < width; w++) { for (let w = 0; w < width; w += 1) {
const bitmask = this.bitmask[w]; const bitmask = this.bitmask[w];
if (!bitmask || bitarray[w * length + index] !== bitmask) return false; if (!bitmask || bitarray[w * length + index] !== bitmask) return false;
} }
@@ -140,20 +144,19 @@ class BitArray {
const ignoreOffset = dim >>> 5; const ignoreOffset = dim >>> 5;
const ignoreMask = ~(1 << dim % 32); const ignoreMask = ~(1 << dim % 32);
const width = this.width; const { width, length, bitarray } = this;
const length = this.length;
const bitarray = this.bitarray;
for (let w = 0; w < width; w++) { for (let w = 0; w < width; w += 1) {
const bitmask = this.bitmask[w]; const bitmask = this.bitmask[w];
if (w === ignoreOffset) { if (w === ignoreOffset) {
if ( if (
bitmask && bitmask &&
(bitarray[w * length + index] & ignoreMask) !== (bitmask & ignoreMask) (bitarray[w * length + index] & ignoreMask) !== (bitmask & ignoreMask)
) ) {
return false; return false;
} else { }
if (bitmask && bitarray[w * length + index] !== bitmask) return false; } else if (bitmask && bitarray[w * length + index] !== bitmask) {
return false;
} }
} }
return true; return true;
@@ -180,24 +183,20 @@ class BitArray {
// select all indices on dimension. // select all indices on dimension.
// //
selectAll(dim) { selectAll(dim) {
let col = dim >> 5; const col = dim >> 5;
const bitmask = this.bitmask[col];
const bitarray = this.bitarray;
const one = 1 << dim % 32; const one = 1 << dim % 32;
for (let i = col * this.length, len = i + this.length; i < len; i++) { for (let i = col * this.length, len = i + this.length; i < len; i += 1) {
bitarray[i] |= one; this.bitarray[i] |= one;
} }
} }
// deselect all indices on dimension // deselect all indices on dimension
// //
deselectAll(dim) { deselectAll(dim) {
let col = dim >> 5; const col = dim >> 5;
const bitmask = this.bitmask[col];
const bitarray = this.bitarray;
const zero = ~(1 << dim % 32); const zero = ~(1 << dim % 32);
for (let i = col * this.length, len = i + this.length; i < len; i++) { for (let i = col * this.length, len = i + this.length; i < len; i += 1) {
bitarray[i] &= zero; this.bitarray[i] &= zero;
} }
} }
@@ -208,11 +207,10 @@ class BitArray {
const col = dim >>> 5; const col = dim >>> 5;
const first = range[0]; const first = range[0];
const last = range[1]; const last = range[1];
const bitarray = this.bitarray;
const one = 1 << dim % 32; const one = 1 << dim % 32;
const offset = col * this.length; const offset = col * this.length;
for (let i = first; i < last; i++) { for (let i = first; i < last; i += 1) {
bitarray[offset + indirect[i]] |= one; this.bitarray[offset + indirect[i]] |= one;
} }
} }
@@ -222,11 +220,10 @@ class BitArray {
const col = dim >>> 5; const col = dim >>> 5;
const first = range[0]; const first = range[0];
const last = range[1]; const last = range[1];
const bitarray = this.bitarray;
const zero = ~(1 << dim % 32); const zero = ~(1 << dim % 32);
const offset = col * this.length; const offset = col * this.length;
for (let i = first; i < last; i++) { for (let i = first; i < last; i += 1) {
bitarray[offset + indirect[i]] &= zero; this.bitarray[offset + indirect[i]] &= zero;
} }
} }
@@ -237,13 +234,14 @@ class BitArray {
// special case (width === 1) for performance // special case (width === 1) for performance
if (this.width === 1) { if (this.width === 1) {
const bitmask = this.bitmask[0]; const bitmask = this.bitmask[0];
const bitarray = this.bitarray; for (let i = 0, len = this.length; i < len; i += 1) {
for (let i = 0, len = this.length; i < len; i++) {
result[i] = result[i] =
bitmask && bitarray[i] === bitmask ? selectedValue : deselectedValue; bitmask && this.bitarray[i] === bitmask
? selectedValue
: deselectedValue;
} }
} else { } 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; result[i] = this.isSelected(i) ? selectedValue : deselectedValue;
} }
} }
+51 -57
View File
@@ -1,4 +1,3 @@
"use strict";
// jshint esversion: 6 // jshint esversion: 6
/* /*
@@ -35,7 +34,6 @@ import {
fillRange, fillRange,
lowerBound, lowerBound,
lowerBoundIndirect, lowerBoundIndirect,
upperBound,
upperBoundIndirect upperBoundIndirect
} from "./util"; } from "./util";
@@ -57,6 +55,7 @@ class TypedCrossfilter {
// filters: array of { id, dimension } // filters: array of { id, dimension }
this.filters = []; this.filters = [];
this.selection = new BitArray(data.length); this.selection = new BitArray(data.length);
this.updateTime = 0;
} }
size() { size() {
@@ -82,15 +81,15 @@ class TypedCrossfilter {
_freeDimension(id) { _freeDimension(id) {
this.selection.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 // return array of all records that are selected/filtered
// by all dimensions. // by all dimensions.
allFiltered() { allFiltered() {
const selection = this.selection; const { selection } = this;
const res = []; 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)) { if (selection.isSelected(i)) {
res.push(this.data[i]); res.push(this.data[i]);
} }
@@ -120,8 +119,8 @@ class TypedCrossfilter {
// and value array must be a TypedArray. // and value array must be a TypedArray.
// //
class ScalarDimension { class ScalarDimension {
constructor(value, valueArrayType, crossfilter, id) { constructor(value, ValueArrayType, xfltr, id) {
this.crossfilter = crossfilter; this.crossfilter = xfltr;
this._id = id; this._id = id;
// current selection filter, expressed as PostiveIntervals. // current selection filter, expressed as PostiveIntervals.
@@ -130,7 +129,7 @@ class ScalarDimension {
// Create value array // Create value array
const array = this._createValueArray( const array = this._createValueArray(
value, value,
new valueArrayType(this.crossfilter.data.length) new ValueArrayType(this.crossfilter.data.length)
); );
this.value = array; this.value = array;
@@ -144,12 +143,13 @@ class ScalarDimension {
_createValueArray(value, array) { _createValueArray(value, array) {
// create dimension value array // create dimension value array
const data = this.crossfilter.data; const { data } = this.crossfilter;
const len = data.length; const len = data.length;
for (let i = 0; i < len; i++) { const larray = array;
array[i] = value(data[i]); for (let i = 0; i < len; i += 1) {
larray[i] = value(data[i]);
} }
return array; return larray;
} }
dispose() { dispose() {
@@ -164,10 +164,10 @@ class ScalarDimension {
// Argument is an array of intervals indicating records newly selected/filtered // Argument is an array of intervals indicating records newly selected/filtered
// //
_updateFilters(newFilter) { _updateFilters(newFilter) {
newFilter = PositiveIntervals.canonicalize(newFilter); const cNewFilter = PositiveIntervals.canonicalize(newFilter);
const adds = PositiveIntervals.difference(newFilter, this.currentFilter); const adds = PositiveIntervals.difference(cNewFilter, this.currentFilter);
const dels = PositiveIntervals.difference(this.currentFilter, newFilter); const dels = PositiveIntervals.difference(this.currentFilter, cNewFilter);
this.crossfilter.filters.forEach(f => this.crossfilter.filters.forEach(f =>
f.dim.groups.forEach(grp => grp._updateReduceDel(this, dels)) f.dim.groups.forEach(grp => grp._updateReduceDel(this, dels))
@@ -193,7 +193,8 @@ class ScalarDimension {
f.dim.groups.forEach(grp => grp._updateReduceAdd(this, adds)) f.dim.groups.forEach(grp => grp._updateReduceAdd(this, adds))
); );
this.currentFilter = newFilter; this.currentFilter = cNewFilter;
this.crossfilter.updateTime += 1;
} }
// filter by value - exact match // filter by value - exact match
@@ -213,7 +214,7 @@ class ScalarDimension {
// filter by a set of values, eg. enum. // filter by a set of values, eg. enum.
filterEnum(values) { filterEnum(values) {
const newFilter = []; 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 = [ const intv = [
lowerBoundIndirect( lowerBoundIndirect(
this.value, this.value,
@@ -269,9 +270,8 @@ class ScalarDimension {
// return top k records, starting with offset, in descending order. // return top k records, starting with offset, in descending order.
// Order is this dimension's sort order // Order is this dimension's sort order
top(k, offset = 0) { top(k, offset = 0) {
const data = this.crossfilter.data; const { data, selection } = this.crossfilter;
const selection = this.crossfilter.selection; const { index } = this;
const index = this.index;
const len = index.length; const len = index.length;
const ret = []; const ret = [];
let i = 0; let i = 0;
@@ -279,17 +279,17 @@ class ScalarDimension {
let found = 0; let found = 0;
// skip up to offset records // 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])) { if (selection.isSelected(index[i])) {
skip++; skip += 1;
} }
} }
// grab up to k records // grab up to k records
for (; 0 <= i && found < k; i--) { for (; 0 <= i && found < k; i -= 1) {
if (selection.isSelected(index[i])) { if (selection.isSelected(index[i])) {
ret.push(data[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. // return bottom k records, starting with offset, in ascending order.
// Order is this dimension's sort order // Order is this dimension's sort order
bottom(k, offset = 0) { bottom(k, offset = 0) {
const data = this.crossfilter.data; const { data, selection } = this.crossfilter;
const selection = this.crossfilter.selection; const { index } = this;
const index = this.index;
const len = index.length; const len = index.length;
const ret = []; const ret = [];
let skip = 0; let skip = 0;
@@ -309,17 +308,17 @@ class ScalarDimension {
let i = 0; let i = 0;
// skip up to offset records // 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])) { if (selection.isSelected(index[i])) {
skip++; skip += 1;
} }
} }
// grab up to k records // grab up to k records
for (; i < len && found < k; i++) { for (; i < len && found < k; i += 1) {
if (selection.isSelected(index[i])) { if (selection.isSelected(index[i])) {
ret.push(data[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). // strings, which can be mapped into an fixed numeric range [0..n).
// //
class EnumDimension extends ScalarDimension { class EnumDimension extends ScalarDimension {
constructor(value, crossfilter, id) { constructor(value, xfltr, id) {
super(value, Uint32Array, crossfilter, id); super(value, Uint32Array, xfltr, id);
} }
_createValueArray(value, array) { _createValueArray(value, array) {
const data = this.crossfilter.data; const { data } = this.crossfilter;
const len = data.length; const len = data.length;
const larray = array;
// create enumeration table - mapping between the value // create enumeration table - mapping between the value
// and the enum. // and the enum.
const s = new Set(); const s = new Set();
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i += 1) {
s.add(value(data[i])); s.add(value(data[i]));
} }
this.enumIndex = Array.from(s); this.enumIndex = Array.from(s);
@@ -360,12 +360,12 @@ class EnumDimension extends ScalarDimension {
// create dimension value array // create dimension value array
const enumLen = this.enumIndex.length; 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 v = value(data[i]);
const e = lowerBound(this.enumIndex, v, 0, enumLen); const e = lowerBound(this.enumIndex, v, 0, enumLen);
array[i] = e; larray[i] = e;
} }
return array; return larray;
} }
filterExact(value) { filterExact(value) {
@@ -415,7 +415,7 @@ class ScalarGroup {
// internal support function - map all dimension values to group values. // 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 // groupValue is optional. Defaults to identity. Used to perform
// initial map operation. // initial map operation.
// //
@@ -424,8 +424,8 @@ class ScalarGroup {
const data = dimension.value; const data = dimension.value;
const len = data.length; const len = data.length;
const mapValue = new groupValueType(dimension.value.length); const mapValue = new GroupValueType(dimension.value.length);
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i += 1) {
mapValue[i] = groupValue(data[i]); mapValue[i] = groupValue(data[i]);
} }
return mapValue; return mapValue;
@@ -444,10 +444,9 @@ class ScalarGroup {
// Each item in the range was just added to `dim`. It was NOT previously // Each item in the range was just added to `dim`. It was NOT previously
// selected - reduceAdd if it is now selected. // selected - reduceAdd if it is now selected.
const selection = this.dimension.crossfilter.selection; const { data, selection } = this.dimension.crossfilter;
const data = this.dimension.crossfilter.data;
intv.forEach(rng => { 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]; const i = dim.index[r];
if (selection.isSelectedIgnoringDim(i, this.dimension.id())) { if (selection.isSelectedIgnoringDim(i, this.dimension.id())) {
const group = this.groups[this.groupIndex[i]]; 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 // Each item in the range will be remved from `dim`. reduceRemove if it
// is currently selected. // is currently selected.
const selection = this.dimension.crossfilter.selection; const { data, selection } = this.dimension.crossfilter.selection;
const data = this.dimension.crossfilter.data;
intv.forEach(rng => { 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]; const i = dim.index[r];
if (selection.isSelectedIgnoringDim(i, this.dimension.id())) { if (selection.isSelectedIgnoringDim(i, this.dimension.id())) {
const group = this.groups[this.groupIndex[i]]; const group = this.groups[this.groupIndex[i]];
@@ -487,8 +485,8 @@ class ScalarGroup {
// groups data. // groups data.
// //
_reduce() { _reduce() {
const dimension = this.dimension; const { dimension } = this;
const data = dimension.crossfilter.data; const { data } = dimension.crossfilter;
// Create groups // Create groups
const groupNames = new Set(this.mapValue); const groupNames = new Set(this.mapValue);
@@ -500,13 +498,13 @@ class ScalarGroup {
}); });
// Create groupIndex - index map between data record index and group index // 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]]; this.groupIndex[i] = groupIndexByName[this.mapValue[i]];
} }
// reduce all filtered records, IGNORING the current dimension's filter // reduce all filtered records, IGNORING the current dimension's filter
const selection = dimension.crossfilter.selection; const { selection } = dimension.crossfilter;
for (let i = 0, len = data.length; i < len; i++) { for (let i = 0, len = data.length; i < len; i += 1) {
if (selection.isSelectedIgnoringDim(i, dimension.id())) { if (selection.isSelectedIgnoringDim(i, dimension.id())) {
const group = this.groups[this.groupIndex[i]]; const group = this.groups[this.groupIndex[i]];
group.value = this.reduceAdd(group.value, data[i]); group.value = this.reduceAdd(group.value, data[i]);
@@ -554,11 +552,7 @@ class ScalarGroup {
} }
class EnumGroup extends ScalarGroup { class EnumGroup extends ScalarGroup {
constructor(groupValue, groupValueType, dimension) { static _map(groupValue, groupValueType, dimension) {
super(groupValue, groupValueType, dimension);
}
_map(groupValue, groupValueType, dimension) {
// groupValue is optional. Defaults to identity. Used to perform // groupValue is optional. Defaults to identity. Used to perform
// initial map operation. // initial map operation.
// //
@@ -1,4 +1,3 @@
"use strict";
// jshint esversion: 6 // jshint esversion: 6
// Interval operations - very simple version of interval set relationship // Interval operations - very simple version of interval set relationship
@@ -21,11 +20,11 @@ class PositiveIntervals {
// //
static canonicalize(A) { static canonicalize(A) {
if (A.length <= 1) return A; if (A.length <= 1) return A;
let copy = A.slice(); const copy = A.slice();
copy.sort((a, b) => a[0] - b[0]); copy.sort((a, b) => a[0] - b[0]);
const res = []; const res = [];
res.push(copy[0]); 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]) { if (copy[i][0] > res[res.length - 1][1]) {
// non-overlapping, add to result // non-overlapping, add to result
res.push(copy[i]); res.push(copy[i]);
@@ -45,12 +44,12 @@ class PositiveIntervals {
} }
static _flatten(A, B) { static _flatten(A, B) {
let points = []; /* point, A, start */ const points = []; /* point, A, start */
for (let a = 0; a < A.length; a++) { for (let a = 0; a < A.length; a += 1) {
points.push([A[a][0], true, true]); points.push([A[a][0], true, true]);
points.push([A[a][1], true, false]); 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][0], false, true]);
points.push([B[b][1], false, false]); points.push([B[b][1], false, false]);
} }
@@ -68,18 +67,16 @@ class PositiveIntervals {
return PositiveIntervals.canonicalize(A); return PositiveIntervals.canonicalize(A);
} }
A = PositiveIntervals.canonicalize(A); const cA = PositiveIntervals.canonicalize(A);
B = PositiveIntervals.canonicalize(B); const cB = PositiveIntervals.canonicalize(B);
const points = PositiveIntervals._flatten(A, B); const points = PositiveIntervals._flatten(cA, cB);
const res = []; const res = [];
let aDepth = 0; let aDepth = 0;
let depth = 0; let depth = 0;
let intervalStart; let intervalStart;
let prevPoint; for (let i = 0; i < points.length; i += 1) {
for (let i = 0; i < points.length; i++) {
const p = points[i]; const p = points[i];
const before = depth;
const delta = p[2] ? 1 : -1; const delta = p[2] ? 1 : -1;
depth += delta; depth += delta;
if (p[1]) aDepth += delta; if (p[1]) aDepth += delta;
@@ -92,7 +89,6 @@ class PositiveIntervals {
intervalStart = undefined; intervalStart = undefined;
} }
} }
prevPoint = p[0];
} }
// guaranteed to be in canonical form // guaranteed to be in canonical form
return res; return res;
@@ -106,16 +102,15 @@ class PositiveIntervals {
return []; return [];
} }
A = PositiveIntervals.canonicalize(A); const cA = PositiveIntervals.canonicalize(A);
B = PositiveIntervals.canonicalize(B); const cB = PositiveIntervals.canonicalize(B);
const points = PositiveIntervals._flatten(A, B); const points = PositiveIntervals._flatten(cA, cB);
const res = []; const res = [];
let depth = 0; let depth = 0;
let intervalStart; let intervalStart;
for (let i = 0; i < points.length; i++) { for (let i = 0; i < points.length; i += 1) {
const p = points[i]; const p = points[i];
const before = depth;
depth += p[2] ? 1 : -1; depth += p[2] ? 1 : -1;
if (depth === 2) { if (depth === 2) {
intervalStart = p[0]; intervalStart = p[0];
+33 -24
View File
@@ -1,5 +1,5 @@
"use strict";
// jshint esversion: 6 // jshint esversion: 6
/* eslint no-bitwise: "off" */
/* /*
Utility functions, private to this module. Utility functions, private to this module.
@@ -9,10 +9,11 @@
// starting with `start` // starting with `start`
// //
export function fillRange(arr, start = 0) { export function fillRange(arr, start = 0) {
for (let i = 0, len = arr.length; i < len; i++) { const larr = arr;
arr[i] = i + start; 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). // 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). // a special-cased version for lining the indirection).
// //
export function lowerBound(valueArray, value, first, last) { export function lowerBound(valueArray, value, first, last) {
let lfirst = first;
let llast = last;
// this is just a binary search // this is just a binary search
while (first < last) { while (lfirst < llast) {
const middle = (first + last) >>> 1; const middle = (lfirst + llast) >>> 1;
if (valueArray[middle] < value) { if (valueArray[middle] < value) {
first = middle + 1; lfirst = middle + 1;
} else { } else {
last = middle; llast = middle;
} }
} }
return first; return lfirst;
} }
// Inlined performance optimization - used to indirect through a sort map. // Inlined performance optimization - used to indirect through a sort map.
// //
export function lowerBoundIndirect(valueArray, indexArray, value, first, last) { export function lowerBoundIndirect(valueArray, indexArray, value, first, last) {
let lfirst = first;
let llast = last;
// this is just a binary search // this is just a binary search
while (first < last) { while (lfirst < llast) {
const middle = (first + last) >>> 1; const middle = (lfirst + llast) >>> 1;
if (valueArray[indexArray[middle]] < value) { if (valueArray[indexArray[middle]] < value) {
first = middle + 1; lfirst = middle + 1;
} else { } else {
last = middle; llast = middle;
} }
} }
return first; return lfirst;
} }
// Search for `value in the sorted array `arr`, in the range [first, last). // 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() // Python: bisect.bisect_right()
// //
export function upperBound(valueArray, value, first, last) { export function upperBound(valueArray, value, first, last) {
let lfirst = first;
let llast = last;
// this is just a binary search // this is just a binary search
while (first < last) { while (lfirst < llast) {
const middle = (first + last) >>> 1; const middle = (lfirst + llast) >>> 1;
if (valueArray[middle] > value) { if (valueArray[middle] > value) {
last = middle; llast = middle;
} else { } else {
first = middle + 1; lfirst = middle + 1;
} }
} }
return first; return lfirst;
} }
// Inline performance optimization // Inline performance optimization
// //
export function upperBoundIndirect(valueArray, indexArray, value, first, last) { export function upperBoundIndirect(valueArray, indexArray, value, first, last) {
let lfirst = first;
let llast = last;
// this is just a binary search // this is just a binary search
while (first < last) { while (lfirst < llast) {
const middle = (first + last) >>> 1; const middle = (lfirst + llast) >>> 1;
if (valueArray[indexArray[middle]] > value) { if (valueArray[indexArray[middle]] > value) {
last = middle; llast = middle;
} else { } else {
first = middle + 1; lfirst = middle + 1;
} }
} }
return first; return lfirst;
} }