Merge pull request #17 from chanzuckerberg/bkmartinjr-prettier

Prettier
This commit is contained in:
Colin Megill
2018-05-07 11:06:51 -07:00
committed by GitHub
62 changed files with 2398 additions and 1912 deletions
+1 -5
View File
@@ -1,11 +1,7 @@
module.exports = { module.exports = {
babelrc: false, babelrc: false,
cacheDirectory: true, cacheDirectory: true,
presets: [ presets: [["env", { loose: true, modules: false }], "stage-0", "react"],
[ "env", { 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"
+4 -6
View File
@@ -1,12 +1,10 @@
module.exports = { module.exports = {
babelrc: false, babelrc: false,
presets: [ presets: [["env", { loose: true, modules: false }], "stage-0", "react"],
[ "env", { 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"
].map(require.resolve).concat([ [ require.resolve('babel-plugin-transform-runtime') ] ]) ]
.map(require.resolve)
.concat([[require.resolve("babel-plugin-transform-runtime")]])
}; };
+4 -4
View File
@@ -1,9 +1,9 @@
module.exports = { module.exports = {
babelrc: false, babelrc: false,
presets: [ presets: [
'babel-preset-es2015', "babel-preset-es2015",
'babel-preset-stage-0', "babel-preset-stage-0",
'babel-preset-react' "babel-preset-react"
], ],
plugins: [ 'istanbul' ] plugins: ["istanbul"]
}; };
+9 -9
View File
@@ -1,22 +1,22 @@
module.exports = { module.exports = {
root: true, root: true,
parser: 'babel-eslint', parser: "babel-eslint",
extends: 'airbnb', extends: "airbnb",
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: 6,
sourceType: 'module', sourceType: "module",
ecmaFeatures: { ecmaFeatures: {
jsx: true, jsx: true,
generators: true, generators: true
} }
}, },
rules: { rules: {
'no-magic-numbers': 'off', "no-magic-numbers": "off",
'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"
} }
}; };
+4 -4
View File
@@ -1,6 +1,6 @@
if (typeof Promise === 'undefined') { if (typeof Promise === "undefined") {
require('promise/lib/rejection-tracking').enable(); require("promise/lib/rejection-tracking").enable();
window.Promise = require('promise/lib/es6-extensions.js'); window.Promise = require("promise/lib/es6-extensions.js");
} }
require('whatwg-fetch'); require("whatwg-fetch");
+33 -31
View File
@@ -1,72 +1,74 @@
const path = require('path'); // jshint esversion: 6
const autoprefixer = require('autoprefixer'); const path = require("path");
const webpack = require('webpack'); const autoprefixer = require("autoprefixer");
const HtmlWebpackPlugin = require('html-webpack-plugin'); const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const src = path.resolve('src'); const src = path.resolve("src");
const nodeModules = path.resolve('node_modules'); const nodeModules = path.resolve("node_modules");
module.exports = { module.exports = {
devtool: 'eval', devtool: "eval",
entry: [ entry: [
'webpack-hot-middleware/client?quiet=true&noInfo=true', "webpack-hot-middleware/client?quiet=true&noInfo=true",
require.resolve('react-hot-loader/patch'), require.resolve("react-hot-loader/patch"),
require.resolve('../polyfills/polyfills'), require.resolve("../polyfills/polyfills"),
path.join(src, 'index') path.join(src, "index")
], ],
output: { output: {
path: path.resolve('build'), path: path.resolve("build"),
pathinfo: true, pathinfo: true,
filename: 'static/js/bundle.js', filename: "static/js/bundle.js",
publicPath: '/' publicPath: "/"
}, },
resolve: { extensions: [ '.js', '.json' ] }, resolve: { extensions: [".js", ".json"] },
module: { module: {
loaders: [ loaders: [
{ {
test: /\.js$/, test: /\.js$/,
include: src, include: src,
loader: 'babel-loader', loader: "babel-loader",
query: require('../babel/babel.dev') query: require("../babel/babel.dev")
}, },
{ {
test: /\.css$/, test: /\.css$/,
include: [ src, nodeModules ], include: [src, nodeModules],
loader: 'style-loader!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader' loader:
"style-loader!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader"
}, },
{ test: /\.json$/, include: [ src, nodeModules ], loader: 'json-loader' }, { test: /\.json$/, include: [src, nodeModules], loader: "json-loader" },
{ {
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/, test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/,
include: [ src, nodeModules ], include: [src, nodeModules],
loader: 'file-loader', loader: "file-loader",
query: { name: 'static/media/[name].[ext]' } query: { name: "static/media/[name].[ext]" }
}, },
{ {
test: /\.(mp4|webm)(\?.*)?$/, test: /\.(mp4|webm)(\?.*)?$/,
include: [ src, nodeModules ], include: [src, nodeModules],
loader: 'url-loader', loader: "url-loader",
query: { limit: 10000, name: 'static/media/[name].[ext]' } query: { limit: 10000, name: "static/media/[name].[ext]" }
} }
] ]
}, },
plugins: [ plugins: [
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
inject: true, inject: true,
template: path.resolve('index.html'), template: path.resolve("index.html"),
favicon: path.resolve('favicon.png') favicon: path.resolve("favicon.png")
}), }),
new webpack.LoaderOptionsPlugin({ new webpack.LoaderOptionsPlugin({
options: { options: {
eslint: { eslint: {
configFile: path.resolve('./configuration/eslint/eslint.js'), configFile: path.resolve("./configuration/eslint/eslint.js"),
useEslintrc: false useEslintrc: false
}, },
postcss() { postcss() {
return [ autoprefixer ]; return [autoprefixer];
} }
} }
}), }),
new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"development"' }), new webpack.DefinePlugin({ "process.env.NODE_ENV": '"development"' }),
// Note: only CSS is currently hot reloaded // Note: only CSS is currently hot reloaded
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin() new webpack.NoEmitOnErrorsPlugin()
+43 -43
View File
@@ -1,74 +1,74 @@
const path = require('path'); // jshint esversion: 6
const autoprefixer = require('autoprefixer'); const path = require("path");
const webpack = require('webpack'); const autoprefixer = require("autoprefixer");
const HtmlWebpackPlugin = require('html-webpack-plugin'); const webpack = require("webpack");
const ExtractTextPlugin = require('extract-text-webpack-plugin'); const HtmlWebpackPlugin = require("html-webpack-plugin");
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); const ExtractTextPlugin = require("extract-text-webpack-plugin");
const CopyWebpackPlugin = require('copy-webpack-plugin'); const SWPrecacheWebpackPlugin = require("sw-precache-webpack-plugin");
const HtmlWebpackInlineSourcePlugin = require( const CopyWebpackPlugin = require("copy-webpack-plugin");
'html-webpack-inline-source-plugin' const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin");
);
const MinifyPlugin = require("babel-minify-webpack-plugin"); const MinifyPlugin = require("babel-minify-webpack-plugin");
const src = path.resolve('src'); const src = path.resolve("src");
const nodeModules = path.resolve('node_modules'); const nodeModules = path.resolve("node_modules");
const publicPath = '/'; const publicPath = "/";
module.exports = { module.exports = {
bail: true, bail: true,
devtool: 'cheap-source-map', devtool: "cheap-source-map",
entry: [ require.resolve('../polyfills/polyfills'), path.join(src, 'index') ], entry: [require.resolve("../polyfills/polyfills"), path.join(src, "index")],
output: { output: {
path: path.resolve('build'), path: path.resolve("build"),
filename: 'static/js/[name].[chunkhash:8].js', filename: "static/js/[name].[chunkhash:8].js",
chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js', chunkFilename: "static/js/[name].[chunkhash:8].chunk.js",
publicPath publicPath
}, },
resolve: { extensions: [ '.js', '.json' ] }, resolve: { extensions: [".js", ".json"] },
module: { module: {
loaders: [ loaders: [
{ {
test: /\.js$/, test: /\.js$/,
include: src, include: src,
loader: 'babel-loader', loader: "babel-loader",
query: require('../babel/babel.prod') query: require("../babel/babel.prod")
}, },
{ {
test: /\.css$/, test: /\.css$/,
include: [ src, nodeModules ], include: [src, nodeModules],
loader: ExtractTextPlugin.extract({ loader: ExtractTextPlugin.extract({
fallbackLoader: 'style-loader', fallbackLoader: "style-loader",
loader: 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]-autoprefixer!postcss-loader' loader:
"css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]-autoprefixer!postcss-loader"
}) })
}, },
{ {
test: /\.json$/, test: /\.json$/,
include: [ src, nodeModules ], include: [src, nodeModules],
loader: 'json-loader', loader: "json-loader",
exclude: /manifest.json$/ exclude: /manifest.json$/
}, },
{ {
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/, test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/,
include: [ src, nodeModules ], include: [src, nodeModules],
loader: 'file-loader', loader: "file-loader",
query: { name: 'static/media/[name].[hash:8].[ext]' } query: { name: "static/media/[name].[hash:8].[ext]" }
}, },
{ {
test: /\.(mp4|webm)(\?.*)?$/, test: /\.(mp4|webm)(\?.*)?$/,
include: [ src, nodeModules ], include: [src, nodeModules],
loader: 'url-loader', loader: "url-loader",
query: { limit: 10000, name: 'static/media/[name].[hash:8].[ext]' } query: { limit: 10000, name: "static/media/[name].[hash:8].[ext]" }
} }
] ]
}, },
plugins: [ plugins: [
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
inject: 'body', inject: "body",
filename: 'index.html', filename: "index.html",
template: path.resolve('index_template.html'), template: path.resolve("index_template.html"),
favicon: path.resolve('favicon.png'), favicon: path.resolve("favicon.png"),
inlineSource: '.(js|css)$', inlineSource: ".(js|css)$",
minify: { minify: {
removeComments: true, removeComments: true,
collapseWhitespace: true, collapseWhitespace: true,
@@ -86,21 +86,21 @@ module.exports = {
new webpack.LoaderOptionsPlugin({ new webpack.LoaderOptionsPlugin({
options: { options: {
eslint: { eslint: {
configFile: path.resolve('./configuration/eslint/eslint.js'), configFile: path.resolve("./configuration/eslint/eslint.js"),
useEslintrc: false useEslintrc: false
}, },
postcss() { postcss() {
return [ autoprefixer ]; return [autoprefixer];
} }
} }
}), }),
new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }), new webpack.DefinePlugin({ "process.env.NODE_ENV": '"production"' }),
new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.OccurrenceOrderPlugin(),
new MinifyPlugin(), new MinifyPlugin(),
new ExtractTextPlugin('static/css/[name].[contenthash:8].css'), new ExtractTextPlugin("static/css/[name].[contenthash:8].css"),
new SWPrecacheWebpackPlugin({ new SWPrecacheWebpackPlugin({
cacheId: 'cellxgene', cacheId: "cellxgene",
filename: 'service-worker.js' filename: "service-worker.js"
}) })
] ]
}; };
+6 -6
View File
@@ -13,12 +13,12 @@
</head> </head>
<body> <body>
<script type="text/javascript"> <script type="text/javascript">
window.CELLXGENE = {} window.CELLXGENE = {};
window.CELLXGENE.API= { window.CELLXGENE.API = {
prefix: "{{ prefix | safe }}", prefix: "{{ prefix | safe }}",
version: "v0.1/", version: "v0.1/"
} };
window.CELLXGENE.datasetTitle = "{{ datasetTitle }}" window.CELLXGENE.datasetTitle = "{{ datasetTitle }}";
</script> </script>
<noscript>If you're seeing this message, that means <strong>JavaScript has been disabled on your browser</strong>, please <strong>enable JS</strong> to make this app work.</noscript> <noscript>If you're seeing this message, that means <strong>JavaScript has been disabled on your browser</strong>, please <strong>enable JS</strong> to make this app work.</noscript>
+19 -18
View File
@@ -1,26 +1,27 @@
/* eslint-disable */ /* eslint-disable */
var path = require('path'); // jshint esversion: 6
var historyApiFallback = require('connect-history-api-fallback'); var path = require("path");
var chalk = require('chalk'); var historyApiFallback = require("connect-history-api-fallback");
var express = require('express'); var chalk = require("chalk");
var favicon = require('serve-favicon'); var express = require("express");
var webpack = require('webpack'); var favicon = require("serve-favicon");
var config = require('../configuration/webpack/webpack.config.dev'); var webpack = require("webpack");
var utils = require('./utils'); var config = require("../configuration/webpack/webpack.config.dev");
var utils = require("./utils");
process.env.NODE_ENV = 'development'; process.env.NODE_ENV = "development";
var PORT = process.env.PORT || 3000; var PORT = process.env.PORT || 3000;
// Set up compiler // Set up compiler
var compiler = webpack(config); var compiler = webpack(config);
compiler.plugin('invalid', () => { compiler.plugin("invalid", () => {
utils.clearConsole(); utils.clearConsole();
console.log('Compiling...'); console.log("Compiling...");
}); });
compiler.plugin('done', stats => { compiler.plugin("done", stats => {
utils.formatStats(stats, PORT); utils.formatStats(stats, PORT);
}); });
@@ -30,18 +31,18 @@ var app = express();
app.use(historyApiFallback({ verbose: false })); app.use(historyApiFallback({ verbose: false }));
app.use( app.use(
require('webpack-dev-middleware')(compiler, { require("webpack-dev-middleware")(compiler, {
noInfo: true, noInfo: true,
publicPath: config.output.publicPath publicPath: config.output.publicPath
}) })
); );
app.use(require('webpack-hot-middleware')(compiler)); app.use(require("webpack-hot-middleware")(compiler));
app.use(favicon('./favicon.png')); app.use(favicon("./favicon.png"));
app.get('*', (req, res) => { app.get("*", (req, res) => {
res.sendFile(path.resolve('index.html')); res.sendFile(path.resolve("index.html"));
}); });
app.listen(PORT, err => { app.listen(PORT, err => {
@@ -51,6 +52,6 @@ app.listen(PORT, err => {
} }
utils.clearConsole(); utils.clearConsole();
console.log(chalk.cyan('Starting the development server...')); console.log(chalk.cyan("Starting the development server..."));
console.log(); console.log();
}); });
+12 -11
View File
@@ -1,22 +1,23 @@
/* eslint-disable */ /* eslint-disable */
var path = require('path'); // jshint esversion: 6
var fs = require('fs'); var path = require("path");
var chalk = require('chalk'); var fs = require("fs");
var express = require('express'); var chalk = require("chalk");
var favicon = require('serve-favicon'); var express = require("express");
var utils = require('./utils'); var favicon = require("serve-favicon");
var utils = require("./utils");
var PORT = process.env.PORT || 80; var PORT = process.env.PORT || 80;
// Launch server // Launch server
var app = express(); var app = express();
app.use(express.static('./build')); app.use(express.static("./build"));
app.use(favicon('./build/favicon.png')); app.use(favicon("./build/favicon.png"));
app.get('*', (req, res) => { app.get("*", (req, res) => {
res.sendFile(path.resolve('./build/index.html')); res.sendFile(path.resolve("./build/index.html"));
}); });
app.listen(PORT, err => { app.listen(PORT, err => {
@@ -26,6 +27,6 @@ app.listen(PORT, err => {
} }
utils.clearConsole(); utils.clearConsole();
console.log(chalk.cyan('Production server started on port ' + PORT)); console.log(chalk.cyan("Production server started on port " + PORT));
console.log(); console.log();
}); });
+21 -20
View File
@@ -1,7 +1,8 @@
/* eslint-disable */ /* eslint-disable */
var chalk = require('chalk'); // jshint esversion: 6
var chalk = require("chalk");
var friendlySyntaxErrorLabel = 'Syntax error:'; var friendlySyntaxErrorLabel = "Syntax error:";
function isLikelyASyntaxError(message) { function isLikelyASyntaxError(message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1; return message.indexOf(friendlySyntaxErrorLabel) !== -1;
@@ -9,16 +10,16 @@ function isLikelyASyntaxError(message) {
function formatMessage(message) { function formatMessage(message) {
return message return message
.replace('Module build failed: SyntaxError:', friendlySyntaxErrorLabel) .replace("Module build failed: SyntaxError:", friendlySyntaxErrorLabel)
.replace( .replace(
/Module not found: Error: Cannot resolve 'file' or 'directory'/, /Module not found: Error: Cannot resolve 'file' or 'directory'/,
'Module not found:' "Module not found:"
) )
.replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, '') .replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, "")
.replace('./~/css-loader!./~/postcss-loader!', ''); .replace("./~/css-loader!./~/postcss-loader!", "");
} }
var clearConsole = () => { var clearConsole = () => {
process.stdout.write('\x1bc'); process.stdout.write("\x1bc");
}; };
var formatStats = (stats, port) => { var formatStats = (stats, port) => {
@@ -26,23 +27,23 @@ var formatStats = (stats, port) => {
var hasErrors = stats.hasErrors(); var hasErrors = stats.hasErrors();
var hasWarnings = stats.hasWarnings(); var hasWarnings = stats.hasWarnings();
if (!hasErrors && !hasWarnings) { if (!hasErrors && !hasWarnings) {
console.log(chalk.green('Compiled successfully!')); console.log(chalk.green("Compiled successfully!"));
console.log(); console.log();
console.log('The app is running at http://localhost:' + port + '/'); console.log("The app is running at http://localhost:" + port + "/");
console.log(); console.log();
return; return;
} }
var json = stats.toJson(); var json = stats.toJson();
var formattedErrors = json.errors.map( var formattedErrors = json.errors.map(
message => 'Error in ' + formatMessage(message) message => "Error in " + formatMessage(message)
); );
var formattedWarnings = json.warnings.map( var formattedWarnings = json.warnings.map(
message => 'Warning in ' + formatMessage(message) message => "Warning in " + formatMessage(message)
); );
if (hasErrors) { if (hasErrors) {
console.log(chalk.red('Failed to compile.')); console.log(chalk.red("Failed to compile."));
console.log(); console.log();
if (formattedErrors.some(isLikelyASyntaxError)) { if (formattedErrors.some(isLikelyASyntaxError)) {
formattedErrors = formattedErrors.filter(isLikelyASyntaxError); formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
@@ -55,23 +56,23 @@ var formatStats = (stats, port) => {
} }
if (hasWarnings) { if (hasWarnings) {
console.log(chalk.yellow('Compiled with warnings.')); console.log(chalk.yellow("Compiled with warnings."));
console.log(); console.log();
formattedWarnings.forEach(message => { formattedWarnings.forEach(message => {
console.log(message); console.log(message);
console.log(); console.log();
}); });
console.log('You may use special comments to disable some warnings.'); console.log("You may use special comments to disable some warnings.");
console.log( console.log(
'Use ' + "Use " +
chalk.yellow('// eslint-disable-next-line') + chalk.yellow("// eslint-disable-next-line") +
' to ignore the next line.' " to ignore the next line."
); );
console.log( console.log(
'Use ' + "Use " +
chalk.yellow('/* eslint-disable */') + chalk.yellow("/* eslint-disable */") +
' to ignore all warnings in a file.' " to ignore all warnings in a file."
); );
} }
}; };
+113 -103
View File
@@ -1,30 +1,31 @@
// jshint esversion: 6
import * as globals from "../globals"; import * as globals from "../globals";
import URI from "urijs"; import URI from "urijs";
import _ from "lodash"; import _ from "lodash";
const requestCells = (query = "") => { const requestCells = (query = "") => {
return (dispatch) => { return dispatch => {
dispatch({type: "request cells started"}) dispatch({ type: "request cells started" });
return fetch(`${globals.API.prefix}${globals.API.version}cells${query}`, { return fetch(`${globals.API.prefix}${globals.API.version}cells${query}`, {
method: "get", method: "get",
headers: new Headers({ headers: new Headers({
'Content-Type': 'application/json' "Content-Type": "application/json"
})
}) })
})
.then(res => res.json()) .then(res => res.json())
.then( .then(
data => dispatch({type: "request cells success", data}), data => dispatch({ type: "request cells success", data }),
error => dispatch({type: "request cells error", error}) error => dispatch({ type: "request cells error", error })
) );
} };
} };
/* SELECT */ /* SELECT */
const regraph = () => { const regraph = () => {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({type: "regraph started"}) dispatch({ type: "regraph started" });
const state = getState() const state = getState();
const selectedMetadata = {}; const selectedMetadata = {};
_.each(state.controls.categoricalAsBooleansMap, (options, field) => { _.each(state.controls.categoricalAsBooleansMap, (options, field) => {
@@ -40,143 +41,152 @@ const regraph = () => {
_.each(options, (isActive, option) => { _.each(options, (isActive, option) => {
if (isActive) { if (isActive) {
if (selectedMetadata[field]) { if (selectedMetadata[field]) {
selectedMetadata[field].push(option) selectedMetadata[field].push(option);
} else if (!selectedMetadata[field]) { } else if (!selectedMetadata[field]) {
selectedMetadata[field] = [option] selectedMetadata[field] = [option];
} }
} }
}) });
} }
}) });
let uri = new URI() let uri = new URI();
uri.setSearch(selectedMetadata) uri.setSearch(selectedMetadata);
console.log(uri.search(), selectedMetadata) console.log(uri.search(), selectedMetadata);
dispatch( dispatch(requestCells(uri.search())).then(res => {
requestCells(uri.search())
).then((res) => {
if (res.error) { if (res.error) {
dispatch({type: "regraph success"}) dispatch({ type: "regraph success" });
} else { } else {
dispatch({type: "regraph error"}) dispatch({ type: "regraph error" });
} }
}) });
} };
} };
const initialize = () => { const initialize = () => {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({type: "initialize started"}) dispatch({ type: "initialize started" });
fetch(`${globals.API.prefix}${globals.API.version}initialize`, { 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(res => res.json())
.then( .then(
data => dispatch({type: "initialize success", data}), data => dispatch({ type: "initialize success", data }),
error => dispatch({type: "initialize error", error}) error => dispatch({ type: "initialize error", error })
) );
};
} };
}
const requestGeneExpressionCounts = () => { const requestGeneExpressionCounts = () => {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({type: "get expression started"}) dispatch({ type: "get expression started" });
fetch(`${globals.API.prefix}${globals.API.version}expression`, { fetch(`${globals.API.prefix}${globals.API.version}expression`, {
method: "get", method: "get",
headers: new Headers({ headers: new Headers({
'accept': 'application/json' accept: "application/json"
})
}) })
})
.then(res => res.json()) .then(res => res.json())
.then( .then(
data => dispatch({type: "get expression success", data}), data => dispatch({ type: "get expression success", data }),
error => dispatch({type: "get expression error", error}) error => dispatch({ type: "get expression error", error })
) );
} };
} };
const requestSingleGeneExpressionCountsForColoringPOST = (gene) => { const requestSingleGeneExpressionCountsForColoringPOST = gene => {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({type: "get single gene expression for coloring started"}) dispatch({ type: "get single gene expression for coloring started" });
fetch(`${globals.API.prefix}${globals.API.version}expression`, { fetch(`${globals.API.prefix}${globals.API.version}expression`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
"genelist": [gene] genelist: [gene]
}), }),
headers: new Headers({ headers: new Headers({
"accept": "application/json", accept: "application/json",
"Content-Type": "application/json" "Content-Type": "application/json"
})
}) })
})
.then(res => res.json()) .then(res => res.json())
.then( .then(
data => dispatch({ data =>
type: "color by expression", dispatch({
gene: gene, type: "color by expression",
data gene: gene,
}), data
error => dispatch({type: "get single gene expression for coloring error", error}) }),
) error =>
} dispatch({
} type: "get single gene expression for coloring error",
error
})
);
};
};
const requestGeneExpressionCountsPOST = (genes) => { const requestGeneExpressionCountsPOST = genes => {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({type: "get expression started"}) dispatch({ type: "get expression started" });
fetch(`${globals.API.prefix}${globals.API.version}expression`, { fetch(`${globals.API.prefix}${globals.API.version}expression`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
"genelist": genes genelist: genes
}), }),
headers: new Headers({ headers: new Headers({
"accept": "application/json", accept: "application/json",
"Content-Type": "application/json" "Content-Type": "application/json"
})
}) })
})
.then(res => res.json()) .then(res => res.json())
.then( .then(
data => dispatch({type: "get expression success", data}), data => dispatch({ type: "get expression success", data }),
error => dispatch({type: "get expression error", error}) error => dispatch({ type: "get expression error", error })
) );
} };
} };
const requestDifferentialExpression = (celllist1, celllist2, num_genes = 7) => { const requestDifferentialExpression = (celllist1, celllist2, num_genes = 7) => {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({type: "request differential expression started"}) dispatch({ type: "request differential expression started" });
fetch(`${globals.API.prefix}${globals.API.version}diffexpression`, { fetch(`${globals.API.prefix}${globals.API.version}diffexpression`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
celllist1, celllist1,
celllist2, celllist2,
num_genes, num_genes
}), }),
headers: new Headers({ headers: new Headers({
"accept": "application/json", accept: "application/json",
"Content-Type": "application/json" "Content-Type": "application/json"
})
}) })
})
.then(res => res.json()) .then(res => res.json())
.then( .then(
(data) => { data => {
/* kick off a secondary action to get all expression counts for all cells now that we know what the top expressed are */ /* kick off a secondary action to get all expression counts for all cells now that we know what the top expressed are */
dispatch( dispatch(
requestGeneExpressionCountsPOST( requestGeneExpressionCountsPOST(
_.union(data.data.celllist1.topgenes, data.data.celllist2.topgenes) // ["GPM6B", "FEZ1", "TSPAN31", "PCSK1N", "TUBA1A", "GPM6A", "CLU", "FCER1G", "TYROBP", "C1QB", "CD74", "CYBA", "GPX1", "TMSB4X"] _.union(
data.data.celllist1.topgenes,
data.data.celllist2.topgenes
) // ["GPM6B", "FEZ1", "TSPAN31", "PCSK1N", "TUBA1A", "GPM6A", "CLU", "FCER1G", "TYROBP", "C1QB", "CD74", "CYBA", "GPX1", "TMSB4X"]
) )
) );
/* then send the success case action through */ /* then send the success case action through */
return dispatch({type: "request differential expression success", data}) return dispatch({
type: "request differential expression success",
data
});
}, },
error => dispatch({type: "request differential expression error", error}) error =>
) dispatch({ type: "request differential expression error", error })
} );
} };
};
export default { export default {
initialize, initialize,
@@ -186,4 +196,4 @@ export default {
requestGeneExpressionCountsPOST, requestGeneExpressionCountsPOST,
requestSingleGeneExpressionCountsForColoringPOST, requestSingleGeneExpressionCountsForColoringPOST,
requestDifferentialExpression requestDifferentialExpression
} };
@@ -1,11 +1,15 @@
import React from 'react'; // jshint esversion: 6
import React from "react";
import _ from "lodash"; import _ from "lodash";
import DeckGL, {PointCloudLayer, ScreenGridLayer, COORDINATE_SYSTEM} from 'deck.gl'; import DeckGL, {
import OrbitController from './orbit-control'; PointCloudLayer,
import { Popup } from './popup'; ScreenGridLayer,
COORDINATE_SYSTEM
} from "deck.gl";
import OrbitController from "./orbit-control";
import { Popup } from "./popup";
class Heatmap extends React.Component { class Heatmap extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -20,16 +24,16 @@ class Heatmap extends React.Component {
height: 0, height: 0,
points: [], points: [],
sampleExpressionMatrix: [ sampleExpressionMatrix: [
{color:[0, 255, 0], position: [100, 100]}, { color: [0, 255, 0], position: [100, 100] },
{color:[0, 255, 0], position: [100, 100]}, { color: [0, 255, 0], position: [100, 100] },
{color:[0, 255, 0], position: [100, 100]}, { color: [0, 255, 0], position: [100, 100] }
], ],
progress: 0, progress: 0,
popup: { popup: {
displayed: false, displayed: false,
x: 0, x: 0,
y: 0, y: 0,
title: '', title: ""
}, },
viewport: { viewport: {
lookAt: [0, 0, 0], lookAt: [0, 0, 0],
@@ -46,7 +50,7 @@ class Heatmap extends React.Component {
getColor(cluster) { getColor(cluster) {
let color = [0, 0, 0]; let color = [0, 0, 0];
switch(cluster){ switch (cluster) {
case 0: case 0:
color = [166, 206, 227]; color = [166, 206, 227];
break; break;
@@ -82,9 +86,8 @@ class Heatmap extends React.Component {
return color; return color;
} }
componentWillMount() { componentWillMount() {
window.addEventListener('resize', this.onResize); window.addEventListener("resize", this.onResize);
this.onResize(); this.onResize();
} }
@@ -97,30 +100,31 @@ class Heatmap extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('resize', this.onResize); window.removeEventListener("resize", this.onResize);
} }
fetchData() { fetchData() {
fetch('https://raw.githubusercontent.com/zdenekhynek/data-science-capstone-visualisation/master/public/clusters.json') fetch(
.then((res) => { "https://raw.githubusercontent.com/zdenekhynek/data-science-capstone-visualisation/master/public/clusters.json"
res.json().then((obj) => { ).then(res => {
const clusters = Object.keys(obj).map((k) => obj[k]) res.json().then(obj => {
const points = clusters.map((cluster) => { const clusters = Object.keys(obj).map(k => obj[k]);
const position = [cluster.x, cluster.y, cluster.z]; const points = clusters.map(cluster => {
const color = [255,0,0]; const position = [cluster.x, cluster.y, cluster.z];
const id = cluster.id; const color = [255, 0, 0];
const title = cluster.webTitle; const id = cluster.id;
return { id, title, position, color }; const title = cluster.webTitle;
}); return { id, title, position, color };
this.setState({ points, progress: 1 })
}); });
this.setState({ points, progress: 1 });
}); });
});
} }
onHover(d) { onHover(d) {
let popup = { displayed: false }; let popup = { displayed: false };
console.log('d', d) console.log("d", d);
if (d.object) { if (d.object) {
const object = d.object; const object = d.object;
@@ -129,7 +133,7 @@ class Heatmap extends React.Component {
title: object.title, title: object.title,
displayed: true, displayed: true,
x: d.x, x: d.x,
y: d.y, y: d.y
}; };
} }
@@ -137,8 +141,8 @@ class Heatmap extends React.Component {
} }
onResize() { onResize() {
const {innerWidth: width, innerHeight: height} = window; const { innerWidth: width, innerHeight: height } = window;
this.setState({width: width / 1.5, height: height / 1.5}); this.setState({ width: width / 1.5, height: height / 1.5 });
} }
onInitialized(gl) { onInitialized(gl) {
@@ -150,27 +154,30 @@ class Heatmap extends React.Component {
onChangeViewport(viewport) { onChangeViewport(viewport) {
this.setState({ this.setState({
rotating: !viewport.isDragging, rotating: !viewport.isDragging,
viewport: {...this.state.viewport, ...viewport} viewport: { ...this.state.viewport, ...viewport }
}); });
} }
onUpdate() { onUpdate() {
const {viewport} = this.state; const { viewport } = this.state;
window.requestAnimationFrame(this.onUpdate); window.requestAnimationFrame(this.onUpdate);
} }
renderPointCloudLayer() { renderPointCloudLayer() {
return this.state.points.length && new PointCloudLayer({ return (
id: 'point-cloud-layer', this.state.points.length &&
data: this.state.points, new PointCloudLayer({
projectionMode: COORDINATE_SYSTEM.IDENTITY, id: "point-cloud-layer",
pickable: true, data: this.state.points,
onHover: this.onHover, projectionMode: COORDINATE_SYSTEM.IDENTITY,
getPosition: d => d.position, pickable: true,
getNormal: d => [0, 0.5, 0.2], onHover: this.onHover,
getColor: d => d.color, getPosition: d => d.position,
radiusPixels: 2 getNormal: d => [0, 0.5, 0.2],
}); getColor: d => d.color,
radiusPixels: 2
})
);
} }
renderGridLayer() { renderGridLayer() {
@@ -182,7 +189,7 @@ class Heatmap extends React.Component {
* ] * ]
*/ */
const screenGridLayer = new ScreenGridLayer({ const screenGridLayer = new ScreenGridLayer({
id: 'screen-grid-layer', id: "screen-grid-layer",
data: this.state.sampleExpressionMatrix, data: this.state.sampleExpressionMatrix,
projectionMode: COORDINATE_SYSTEM.IDENTITY, projectionMode: COORDINATE_SYSTEM.IDENTITY,
pickable: true, pickable: true,
@@ -195,34 +202,42 @@ class Heatmap extends React.Component {
} }
renderDeckGLCanvas() { renderDeckGLCanvas() {
const {width, height, viewport} = this.state; const { width, height, viewport } = this.state;
const canvasProps = {width, height, ...viewport}; const canvasProps = { width, height, ...viewport };
const glViewport = OrbitController.getViewport(canvasProps); const glViewport = OrbitController.getViewport(canvasProps);
return width && height && ( return (
<OrbitController {...canvasProps} ref={canvas => { width &&
this.canvas = canvas; height && (
}} onChangeViewport={this.onChangeViewport}> <OrbitController
<DeckGL {...canvasProps}
width={width} ref={canvas => {
height={height} this.canvas = canvas;
viewport={glViewport} }}
layers={[ onChangeViewport={this.onChangeViewport}
// this.renderPointCloudLayer(), >
this.renderGridLayer() <DeckGL
].filter(Boolean)} width={width}
onWebGLInitialized={this.onInitialized}/> height={height}
</OrbitController> viewport={glViewport}
layers={[
// this.renderPointCloudLayer(),
this.renderGridLayer()
].filter(Boolean)}
onWebGLInitialized={this.onInitialized}
/>
</OrbitController>
)
); );
} }
render() { render() {
const {width, height, popup} = this.state; const { width, height, popup } = this.state;
if (!width || !height) { if (!width || !height) {
return null; return null;
} }
const renderedPopup = (popup.displayed)? <Popup {...popup} /> : null; const renderedPopup = popup.displayed ? <Popup {...popup} /> : null;
return ( return (
<div id="heatmap"> <div id="heatmap">
@@ -231,6 +246,6 @@ class Heatmap extends React.Component {
</div> </div>
); );
} }
}; }
export default Heatmap; export default Heatmap;
@@ -1,7 +1,8 @@
// jshint esversion: 6
/* global window */ /* global window */
import React, {Component} from 'react'; import React, { Component } from "react";
import {PerspectiveViewport} from 'deck.gl'; import { PerspectiveViewport } from "deck.gl";
import {vec3} from 'gl-matrix'; import { vec3 } from "gl-matrix";
/* Utils */ /* Utils */
// constrain number between bounds // constrain number between bounds
@@ -15,15 +16,24 @@ function clamp(x, min, max) {
return x; return x;
} }
const ua = typeof window.navigator !== 'undefined' ? const ua =
window.navigator.userAgent.toLowerCase() : ''; typeof window.navigator !== "undefined"
const firefox = ua.indexOf('firefox') !== -1; ? window.navigator.userAgent.toLowerCase()
: "";
const firefox = ua.indexOf("firefox") !== -1;
/* Interaction */ /* Interaction */
export default class OrbitController extends Component { export default class OrbitController extends Component {
static getViewport({
static getViewport({width, height, lookAt, distance, rotationX, rotationY, fov}) { width,
height,
lookAt,
distance,
rotationX,
rotationY,
fov
}) {
const cameraPos = vec3.add([], lookAt, [0, 0, distance]); const cameraPos = vec3.add([], lookAt, [0, 0, distance]);
vec3.rotateX(cameraPos, cameraPos, lookAt, rotationX / 180 * Math.PI); vec3.rotateX(cameraPos, cameraPos, lookAt, rotationX / 180 * Math.PI);
vec3.rotateY(cameraPos, cameraPos, lookAt, rotationY / 180 * Math.PI); vec3.rotateY(cameraPos, cameraPos, lookAt, rotationY / 180 * Math.PI);
@@ -45,25 +55,29 @@ export default class OrbitController extends Component {
} }
_onDragStart(evt) { _onDragStart(evt) {
const {pageX, pageY} = evt; const { pageX, pageY } = evt;
this._dragStartPos = [pageX, pageY]; this._dragStartPos = [pageX, pageY];
this.props.onChangeViewport({isDragging: true}); this.props.onChangeViewport({ isDragging: true });
} }
_onDrag(evt) { _onDrag(evt) {
if (this._dragStartPos) { if (this._dragStartPos) {
const {pageX, pageY} = evt; const { pageX, pageY } = evt;
const {width, height} = this.props; const { width, height } = this.props;
const dx = (pageX - this._dragStartPos[0]) / width; const dx = (pageX - this._dragStartPos[0]) / width;
const dy = (pageY - this._dragStartPos[1]) / height; const dy = (pageY - this._dragStartPos[1]) / height;
if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.metaKey) { if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.metaKey) {
// pan // pan
const {lookAt, distance, rotationX, rotationY, fov} = this.props; const { lookAt, distance, rotationX, rotationY, fov } = this.props;
const unitsPerPixel = distance / Math.tan(fov / 180 * Math.PI / 2) / 2; const unitsPerPixel = distance / Math.tan(fov / 180 * Math.PI / 2) / 2;
const newLookAt = vec3.add([], lookAt, [-unitsPerPixel * dx, unitsPerPixel * dy, 0]); const newLookAt = vec3.add([], lookAt, [
-unitsPerPixel * dx,
unitsPerPixel * dy,
0
]);
vec3.rotateX(newLookAt, newLookAt, lookAt, rotationX / 180 * Math.PI); vec3.rotateX(newLookAt, newLookAt, lookAt, rotationX / 180 * Math.PI);
vec3.rotateY(newLookAt, newLookAt, lookAt, rotationY / 180 * Math.PI); vec3.rotateY(newLookAt, newLookAt, lookAt, rotationY / 180 * Math.PI);
@@ -72,7 +86,7 @@ export default class OrbitController extends Component {
}); });
} else { } else {
// rotate // rotate
const {rotationX, rotationY} = this.props; const { rotationX, rotationY } = this.props;
const newRotationX = clamp(rotationX - dy * 180, -90, 90); const newRotationX = clamp(rotationX - dy * 180, -90, 90);
const newRotationY = (rotationY - dx * 180) % 360; const newRotationY = (rotationY - dx * 180) % 360;
@@ -88,7 +102,7 @@ export default class OrbitController extends Component {
_onDragEnd() { _onDragEnd() {
this._dragStartPos = null; this._dragStartPos = null;
this.props.onChangeViewport({isDragging: false}); this.props.onChangeViewport({ isDragging: false });
} }
_onWheel(evt) { _onWheel(evt) {
@@ -107,8 +121,12 @@ export default class OrbitController extends Component {
value = Math.floor(value / 4); value = Math.floor(value / 4);
} }
const {distance, minDistance, maxDistance} = this.props; const { distance, minDistance, maxDistance } = this.props;
const newDistance = clamp(distance * Math.pow(1.01, value), minDistance, maxDistance); const newDistance = clamp(
distance * Math.pow(1.01, value),
minDistance,
maxDistance
);
this.props.onChangeViewport({ this.props.onChangeViewport({
distance: newDistance distance: newDistance
@@ -117,7 +135,7 @@ export default class OrbitController extends Component {
// public API // public API
fitBounds(min, max) { fitBounds(min, max) {
const {fov} = this.props; const { fov } = this.props;
const size = Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]); const size = Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
const newDistance = size / Math.tan(fov / 180 * Math.PI / 2) / 2; const newDistance = size / Math.tan(fov / 180 * Math.PI / 2) / 2;
@@ -128,16 +146,17 @@ export default class OrbitController extends Component {
render() { render() {
return ( return (
<div style={{position: 'relative', userSelect: 'none'}} <div
style={{ position: "relative", userSelect: "none" }}
onMouseDown={this._onDragStart.bind(this)} onMouseDown={this._onDragStart.bind(this)}
onMouseMove={this._onDrag.bind(this)} onMouseMove={this._onDrag.bind(this)}
onMouseLeave={this._onDragEnd.bind(this)} onMouseLeave={this._onDragEnd.bind(this)}
onMouseUp={this._onDragEnd.bind(this)} onMouseUp={this._onDragEnd.bind(this)}
onWheel={this._onWheel.bind(this)} > onWheel={this._onWheel.bind(this)}
>
{this.props.children} {this.props.children}
</div>
</div>); );
} }
} }
@@ -1,30 +1,31 @@
import React, {PureComponent} from 'react'; // jshint esversion: 6
import React, { PureComponent } from "react";
export class Popup extends PureComponent { export class Popup extends PureComponent {
render() { render() {
const { title, x, y } = this.props; const { title, x, y } = this.props;
const style = { const style = {
position: 'absolute', position: "absolute",
top: y, top: y,
left: x, left: x,
maxWidth: '200px', maxWidth: "200px",
padding: '10px', padding: "10px",
color: 'white', color: "white",
backgroundColor: 'black', backgroundColor: "black",
pointerEvents: 'none', pointerEvents: "none",
transform: 'translate(10px, -50%)', transform: "translate(10px, -50%)"
}; };
const arrowStyle = { const arrowStyle = {
position: 'absolute', position: "absolute",
top: '50%', top: "50%",
left: '-14px', left: "-14px",
width: '7px', width: "7px",
height: '5px', height: "5px",
boxSizing: 'border-box', boxSizing: "border-box",
transform: 'translateY(-50%)', transform: "translateY(-50%)",
border: '7px solid transparent', border: "7px solid transparent",
borderRight: '7px solid black', borderRight: "7px solid black"
}; };
return ( return (
@@ -32,13 +33,13 @@ export class Popup extends PureComponent {
<div style={arrowStyle} /> <div style={arrowStyle} />
{title} {title}
</div> </div>
) );
}; }
} }
Popup.defaultProps = { Popup.defaultProps = {
id: 'id', id: "id",
title: '', title: "",
x: 0, x: 0,
y: 0, y: 0
}; };
+40 -32
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import React from "react"; import React from "react";
import _ from "lodash"; import _ from "lodash";
import Helmet from "react-helmet"; import Helmet from "react-helmet";
@@ -15,33 +16,30 @@ import actions from "../actions";
import SectionHeader from "./framework/sectionHeader"; import SectionHeader from "./framework/sectionHeader";
@connect((state) => { @connect(state => {
return { return {
cells: state.cells cells: state.cells
} };
}) })
class App extends React.Component { class App extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {};
}
}; _onURLChanged() {
this.props.dispatch({ type: "url changed", url: document.location.href });
} }
_onURLChanged () {
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.initialize());
/* /*
first request includes query straight off the url bar for now first request includes query straight off the url bar for now
*/ */
this.props.dispatch(actions.requestCells(window.location.search)) this.props.dispatch(actions.requestCells(window.location.search));
} }
render() { render() {
@@ -50,32 +48,42 @@ class App extends React.Component {
return ( return (
<Container> <Container>
<Helmet title="cellxgene" /> <Helmet title="cellxgene" />
{ {this.props.cells.loading ? (
this.props.cells.loading ? <div
<div style={{position: "fixed", left: window.innerWidth / 2, top: 150}}> style={{ position: "fixed", left: window.innerWidth / 2, top: 150 }}
<PulseLoader color="rgb(0,0,0)" size="10px" margin="4px"/> >
<span style={{fontFamily: globals.accentFont, fontStyle: "italic"}}>loading cells</span> <PulseLoader color="rgb(0,0,0)" size="10px" margin="4px" />
</div> : <span
null style={{ fontFamily: globals.accentFont, fontStyle: "italic" }}
} >
loading cells
</span>
</div>
) : null}
{this.props.cells.error ? "Error loading cells" : null} {this.props.cells.error ? "Error loading cells" : null}
{false ? <Joy data={this.state.expressions && this.state.expressions.data}/> : ""} {false ? (
<Joy data={this.state.expressions && this.state.expressions.data} />
) : (
""
)}
<div> <div>
<LeftSideBar/> <LeftSideBar />
<div style={{ <div
padding: 15, style={{
backgroundColor: "#F7F7F7", padding: 15,
width: 1440 - 410 /* but responsive */, backgroundColor: "#F7F7F7",
marginLeft: 350 /* but responsive */ width: 1440 - 410 /* but responsive */,
}}> marginLeft: 350 /* but responsive */
<Graph/> }}
<DynamicScatterplot/> >
<Graph />
<DynamicScatterplot />
{/*<Parallel/>*/} {/*<Parallel/>*/}
</div> </div>
</div> </div>
</Container> </Container>
) );
} }
}; }
export default App; export default App;
+90 -82
View File
@@ -1,22 +1,22 @@
// jshint esversion: 6
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 * as globals from "../../globals"; import * as globals from "../../globals";
import styles from "./categorical.css"; import styles from "./categorical.css";
import SectionHeader from "../framework/sectionHeader" import SectionHeader from "../framework/sectionHeader";
import Value from "./value"; import Value from "./value";
import { alphabeticallySortedValues } from "./util"; 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";
@connect(state => {
@connect((state) => {
return { return {
colorAccessor: state.controls.colorAccessor colorAccessor: state.controls.colorAccessor
} };
}) })
class Category extends React.Component { class Category extends React.Component {
constructor(props) { constructor(props) {
@@ -31,22 +31,22 @@ class Category extends React.Component {
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",
metadataField: this.props.metadataField metadataField: this.props.metadataField
}) });
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",
metadataField: this.props.metadataField, metadataField: this.props.metadataField,
value: this.props.value value: this.props.value
}) });
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) => {
@@ -56,43 +56,60 @@ class Category extends React.Component {
metadataField={this.props.metadataField} metadataField={this.props.metadataField}
count={this.props.values[v]} count={this.props.values[v]}
value={v} value={v}
i={i} /> i={i}
) />
}) );
});
} }
render() { render() {
return ( return (
<div style={{ <div
style={{
// display: "flex", // display: "flex",
// alignItems: "baseline", // alignItems: "baseline",
maxWidth: globals.maxControlsWidth, maxWidth: globals.maxControlsWidth
}}> }}
<div style={{ >
display: "flex", <div
justifyContent: "space-between", style={{
alignItems: "baseline" display: "flex",
}}> justifyContent: "space-between",
<p style={{ alignItems: "baseline"
}}
>
<p
style={{
// flexShrink: 0, // flexShrink: 0,
fontWeight: 500, fontWeight: 500,
// textAlign: "right", // textAlign: "right",
// fontFamily: globals.accentFont, // fontFamily: globals.accentFont,
// fontStyle: "italic", // fontStyle: "italic",
margin: "3px 10px 3px 0px", margin: "3px 10px 3px 0px"
}}> }}
>
<span <span
style={{cursor: "pointer", display: "inline-block", position: "relative", top: 2}} style={{
cursor: "pointer",
display: "inline-block",
position: "relative",
top: 2
}}
onClick={() => { onClick={() => {
this.setState({isExpanded: !this.state.isExpanded}) this.setState({ isExpanded: !this.state.isExpanded });
}}> }}
{this.state.isExpanded ? <FaArrowDown /> : <FaArrowRight/> } >
{this.state.isExpanded ? <FaArrowDown /> : <FaArrowRight />}
</span> </span>
{this.props.metadataField} {this.props.metadataField}
<input <input
onChange={this.state.isChecked ? this.toggleNone.bind(this) : this.toggleAll.bind(this)} onChange={
checked={this.state.isChecked} this.state.isChecked
type="checkbox"/> ? this.toggleNone.bind(this)
: this.toggleAll.bind(this)
}
checked={this.state.isChecked}
type="checkbox"
/>
<span <span
onClick={this.handleColorChange.bind(this)} onClick={this.handleColorChange.bind(this)}
style={{ style={{
@@ -100,77 +117,69 @@ class Category extends React.Component {
marginLeft: 4, marginLeft: 4,
// padding: this.props.colorAccessor === this.props.metadataField ? 3 : "auto", // padding: this.props.colorAccessor === this.props.metadataField ? 3 : "auto",
borderRadius: 3, borderRadius: 3,
color: this.props.colorAccessor === this.props.metadataField ? globals.brightBlue : "black", color:
this.props.colorAccessor === this.props.metadataField
? globals.brightBlue
: "black",
// backgroundColor: this.props.colorAccessor === this.props.metadataField ? globals.brightBlue : "inherit", // backgroundColor: this.props.colorAccessor === this.props.metadataField ? globals.brightBlue : "inherit",
display: "inline-block", display: "inline-block",
position: "relative", position: "relative",
top: 2, top: 2,
cursor: "pointer", cursor: "pointer"
}}> }}
<FaPaintBrush/> >
<FaPaintBrush />
</span> </span>
</p> </p>
</div> </div>
<div> <div>{this.state.isExpanded ? this.renderCategoryItems() : null}</div>
{
this.state.isExpanded ? this.renderCategoryItems() : null
}
</div>
</div> </div>
) );
} }
} }
@connect((state) => { @connect(state => {
const ranges =
const ranges = state.cells.cells && state.cells.cells.data.ranges ? state.cells.cells.data.ranges : null; state.cells.cells && state.cells.cells.data.ranges
? state.cells.cells.data.ranges
: null;
return { return {
ranges ranges
} };
}) })
class Categories extends React.Component { class Categories extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {};
};
} }
render () { render() {
if (!this.props.ranges) return null if (!this.props.ranges) return null;
return ( return (
<div style={{ <div
width: 310, style={{
marginRight: 40, width: 310,
paddingRight: 20, marginRight: 40,
flexShrink: 0, paddingRight: 20,
// height: 700, flexShrink: 0
// overflow: "auto", // height: 700,
}}> // overflow: "auto",
}}
{ >
_.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 ( if (value.options && key !== "CellName" && !isColorField) {
value.options && return (
key !== "CellName" && <Category key={key} metadataField={key} values={value.options} />
!isColorField );
) { }
return ( })}
<Category
key={key}
metadataField={key}
values={value.options}/>
)
}
})
}
</div> </div>
) );
} }
}; }
export default Categories; export default Categories;
@@ -190,7 +199,6 @@ export default Categories;
*/ */
/* /*
Each category has a color associated with it - ie., color by location should show up on these buttons, Each category has a color associated with it - ie., color by location should show up on these buttons,
+5 -4
View File
@@ -1,7 +1,8 @@
export const alphabeticallySortedValues = (values) => { // jshint esversion: 6
export const alphabeticallySortedValues = values => {
return Object.keys(values).sort((a, b) => { return Object.keys(values).sort((a, b) => {
var textA = a.toUpperCase(); var textA = a.toUpperCase();
var textB = b.toUpperCase(); var textB = b.toUpperCase();
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0; return textA < textB ? -1 : textA > textB ? 1 : 0;
}) });
} };
+34 -19
View File
@@ -1,17 +1,17 @@
// jshint esversion: 6
import { connect } from "react-redux"; import { connect } from "react-redux";
import React from "react"; import React from "react";
import * as globals from "../../globals"; import * as globals from "../../globals";
import actions from "../../actions"; import actions from "../../actions";
@connect((state) => { @connect(state => {
return { return {
categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap, categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap,
colorScale: state.controls.colorScale, colorScale: state.controls.colorScale,
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor
} };
}) })
class CategoryValue extends React.Component { class CategoryValue extends React.Component {
toggleOff() { toggleOff() {
this.props.dispatch({ this.props.dispatch({
type: "categorical metadata filter deselect", type: "categorical metadata filter deselect",
@@ -28,11 +28,16 @@ class CategoryValue extends React.Component {
}); });
} }
render () { render() {
if (!this.props.categoricalAsBooleansMap) return null if (!this.props.categoricalAsBooleansMap) return null;
const selected = this.props.categoricalAsBooleansMap[this.props.metadataField][this.props.value] const selected = this.props.categoricalAsBooleansMap[
const c = this.props.metadataField === this.props.colorAccessor /* this is the color scale, so add swatches below */ this.props.metadataField
][this.props.value];
const c =
this.props.metadataField ===
this.props
.colorAccessor; /* this is the color scale, so add swatches below */
return ( return (
<div <div
@@ -41,32 +46,42 @@ class CategoryValue extends React.Component {
display: "flex", display: "flex",
alignItems: "baseline", alignItems: "baseline",
justifyContent: "space-between", justifyContent: "space-between",
fontWeight: selected ? 700 : 400, fontWeight: selected ? 700 : 400
}}> }}
<p style={{ >
<p
style={{
paddingLeft: 15, paddingLeft: 15,
width: 200, width: 200,
flexShrink: 0, flexShrink: 0,
margin: 0, margin: 0,
lineHeight: "1em" lineHeight: "1em"
}}> }}
>
<input <input
onChange={selected ? this.toggleOff.bind(this) : this.toggleOn.bind(this)} onChange={
selected ? this.toggleOff.bind(this) : this.toggleOn.bind(this)
}
checked={selected} checked={selected}
type="checkbox"/> type="checkbox"
{this.props.value} />
{this.props.value}
</p> </p>
<p style={{ <p
style={{
padding: "1px 10px", padding: "1px 10px",
backgroundColor: c ? this.props.colorScale(this.props.value) : "inherit", backgroundColor: c
? this.props.colorScale(this.props.value)
: "inherit",
color: c ? "white" : "black", color: c ? "white" : "black",
margin: 0, margin: 0,
lineHeight: "1em" lineHeight: "1em"
}}> }}
>
{this.props.count} {this.props.count}
</p> </p>
</div> </div>
) );
} }
} }
+1
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
/* rc slider https://www.npmjs.com/package/rc-slider */ /* rc slider https://www.npmjs.com/package/rc-slider */
import React from "react"; import React from "react";
+51 -43
View File
@@ -1,9 +1,6 @@
import styles from './parallelCoordinates.css'; // jshint esversion: 6
import { import styles from "./parallelCoordinates.css";
yAxis, import { yAxis, brushstart } from "./util";
brushstart,
} from "./util";
const drawAxes = ( const drawAxes = (
svg, svg,
@@ -13,19 +10,19 @@ const drawAxes = (
height, height,
width, width,
handleBrushAction, handleBrushAction,
handleColorAction, handleColorAction
) => { ) => {
/***************************************** /*****************************************
****************************************** ******************************************
Handles a brush event, toggling the display of foreground lines. Handles a brush event, toggling the display of foreground lines.
****************************************** ******************************************
******************************************/ ******************************************/
function brush () { function brush() {
var actives = []; var actives = [];
svg.selectAll(".parcoords_axis .parcoords_brush") svg
.filter(function (d) { .selectAll(".parcoords_axis .parcoords_brush")
.filter(function(d) {
return d3.brushSelection(this); return d3.brushSelection(this);
}) })
.each(function(d) { .each(function(d) {
@@ -35,46 +32,57 @@ const drawAxes = (
}); });
}); });
/* fire action, with selected dimensions & their values */ /* fire action, with selected dimensions & their values */
handleBrushAction(actives) handleBrushAction(actives);
} }
var axes = svg.selectAll(".parcoords_axis") var axes = svg
.data(dimensions) .selectAll(".parcoords_axis")
.enter().append("g") .data(dimensions)
.attr("class", `${styles.axis} parcoords_axis`) .enter()
.attr("transform", (d,i) => { return "translate(" + xscale(i) + ")"; }); .append("g")
.attr("class", `${styles.axis} parcoords_axis`)
.attr("transform", (d, i) => {
return "translate(" + xscale(i) + ")";
});
axes.append("g") axes
.each(function(d) { .append("g")
var renderAxis = "axis" in d .each(function(d) {
? d.axis.scale(d.scale) // custom axis var renderAxis =
: yAxis.scale(d.scale); // default axis "axis" in d
d3.select(this).call(renderAxis); ? d.axis.scale(d.scale) // custom axis
}) : yAxis.scale(d.scale); // default axis
d3.select(this).call(renderAxis);
})
.append("text") .append("text")
.on("click", (d) => { handleColorAction(d.key) }) .on("click", d => {
.attr("class", styles.title) handleColorAction(d.key);
.attr("text-anchor", "start") })
.text(function(d) { return "description" in d ? d.description + " 🖌️" : d.key + " 🖌️"; }); .attr("class", styles.title)
.attr("text-anchor", "start")
.text(function(d) {
return "description" in d ? d.description + " 🖌️" : d.key + " 🖌️";
});
// Add and store a brush for each axis. // Add and store a brush for each axis.
axes.append("g") axes
.attr("class", `${styles.brush} parcoords_brush`) .append("g")
.each(function(d) { .attr("class", `${styles.brush} parcoords_brush`)
d3.select(this).call( .each(function(d) {
d.brush = d3.brushY() d3.select(this).call(
.extent([[-10,0], [10, height]]) (d.brush = d3
.on("start", brushstart) .brushY()
.on("brush", brush) .extent([[-10, 0], [10, height]])
.on("end", brush) .on("start", brushstart)
) .on("brush", brush)
}) .on("end", brush))
);
})
.selectAll("rect") .selectAll("rect")
.attr("x", -8) .attr("x", -8)
.attr("width", 16); .attr("width", 16);
return axes; return axes;
};
}
export default drawAxes; export default drawAxes;
+24 -33
View File
@@ -1,7 +1,6 @@
// jshint esversion: 6
import _ from "lodash"; import _ from "lodash";
import { import { project } from "./util";
project
} from "./util";
import renderQueue from "../../util/renderQueue"; import renderQueue from "../../util/renderQueue";
@@ -16,16 +15,15 @@ const drawLinesCanvas = (
dimensions, dimensions,
xscale, xscale,
colorAccessor, colorAccessor,
colorScale, colorScale
) => { ) => {
return (d) => { return d => {
ctx.globalAlpha = 0.1;
ctx.globalAlpha = .1;
if (d["__selected__"]) { if (d["__selected__"]) {
ctx.strokeStyle = d["__color__"]; ctx.strokeStyle = d["__color__"];
} else { } else {
return return;
} }
ctx.beginPath(); ctx.beginPath();
@@ -36,31 +34,31 @@ const drawLinesCanvas = (
// this bit renders horizontal lines on the previous/next // this bit renders horizontal lines on the previous/next
// dimensions, so that sandwiched null values are visible // dimensions, so that sandwiched null values are visible
if (i > 0) { if (i > 0) {
var prev = coords[i-1]; var prev = coords[i - 1];
if (prev !== null) { if (prev !== null) {
ctx.moveTo(prev[0],prev[1]); ctx.moveTo(prev[0], prev[1]);
ctx.lineTo(prev[0]+6,prev[1]); ctx.lineTo(prev[0] + 6, prev[1]);
} }
} }
if (i < coords.length-1) { if (i < coords.length - 1) {
var next = coords[i+1]; var next = coords[i + 1];
if (next !== null) { if (next !== null) {
ctx.moveTo(next[0]-6,next[1]); ctx.moveTo(next[0] - 6, next[1]);
} }
} }
return; return;
} }
if (i == 0) { if (i == 0) {
ctx.moveTo(p[0],p[1]); ctx.moveTo(p[0], p[1]);
return; return;
} }
ctx.lineTo(p[0],p[1]); ctx.lineTo(p[0], p[1]);
}); });
ctx.stroke(); ctx.stroke();
} };
} };
const drawCellLinesUsingRenderQueue = ( const drawCellLinesUsingRenderQueue = (
metadata, metadata,
@@ -68,21 +66,14 @@ const drawCellLinesUsingRenderQueue = (
xscale, xscale,
ctx, ctx,
colorAccessor, colorAccessor,
colorScale, colorScale
) => { ) => {
const _renderLinesWithQueue = renderQueue( const _renderLinesWithQueue = renderQueue(
drawLinesCanvas( drawLinesCanvas(ctx, dimensions, xscale, colorAccessor, colorScale)
ctx,
dimensions,
xscale,
colorAccessor,
colorScale,
)
).rate(50); ).rate(50);
_renderLinesWithQueue(metadata); _renderLinesWithQueue(metadata);
return _renderLinesWithQueue; return _renderLinesWithQueue;
} };
const drawCellLinesSync = ( const drawCellLinesSync = (
metadata, metadata,
@@ -90,17 +81,17 @@ const drawCellLinesSync = (
xscale, xscale,
ctx, ctx,
colorAccessor, colorAccessor,
colorScale, colorScale
) => { ) => {
const _draw = drawLinesCanvas( const _draw = drawLinesCanvas(
ctx, ctx,
dimensions, dimensions,
xscale, xscale,
colorAccessor, colorAccessor,
colorScale, colorScale
) );
_.each(metadata, _draw) _.each(metadata, _draw);
} };
export default drawCellLinesUsingRenderQueue; export default drawCellLinesUsingRenderQueue;
// export default drawCellLinesUsingRenderQueue; // export default drawCellLinesUsingRenderQueue;
+87 -59
View File
@@ -3,22 +3,31 @@ https://bl.ocks.org/mbostock/4341954
https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172 https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172
https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771 https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771
*/ */
import React from 'react'; // jshint esversion: 6
import React from "react";
import _ from "lodash"; import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
@connect((state) => { @connect(state => {
const ranges =
state.cells.cells && state.cells.cells.data.ranges
? state.cells.cells.data.ranges
: null;
const metadata =
state.cells.cells && state.cells.cells.data.metadata
? state.cells.cells.data.metadata
: null;
const ranges = state.cells.cells && state.cells.cells.data.ranges ? state.cells.cells.data.ranges : null; const initializeRanges =
const metadata = state.cells.cells && state.cells.cells.data.metadata ? state.cells.cells.data.metadata : null; state.initialize.data && state.initialize.data.data.ranges
? state.initialize.data.data.ranges
const initializeRanges = state.initialize.data && state.initialize.data.data.ranges ? state.initialize.data.data.ranges : null; : null;
return { return {
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale, colorScale: state.controls.colorScale,
currentCellSelection: state.controls.currentCellSelection, currentCellSelection: state.controls.currentCellSelection
} };
}) })
class HistogramBrush extends React.Component { class HistogramBrush extends React.Component {
constructor(props) { constructor(props) {
@@ -33,15 +42,11 @@ class HistogramBrush extends React.Component {
ctx: null, ctx: null,
axes: null, axes: null,
dimensions: null, dimensions: null,
brush: null, brush: null
}; };
} }
componentDidMount() { componentDidMount() {}
componentDidUpdate() {}
}
componentDidUpdate() {
}
onBrush(selection, x) { onBrush(selection, x) {
return () => { return () => {
if (d3.event.selection) { if (d3.event.selection) {
@@ -57,81 +62,104 @@ class HistogramBrush extends React.Component {
range: null range: null
}); });
} }
};
}
} }
drawHistogram(svgRef) { drawHistogram(svgRef) {
const allValuesForContinuousFieldAsArray = _.map( const allValuesForContinuousFieldAsArray = _.map(
this.props.currentCellSelection, this.props.currentCellSelection,
this.props.metadataField this.props.metadataField
) );
var x = d3.scaleLinear() var x = d3
.domain(d3.extent(allValuesForContinuousFieldAsArray, (d) => +d)) .scaleLinear()
.range([0, this.width]) .domain(d3.extent(allValuesForContinuousFieldAsArray, d => +d))
// .range([margin.left, width - margin.right]); .range([0, this.width]);
// .range([margin.left, width - margin.right]);
var y = d3.scaleLinear() var y = d3.scaleLinear().range([this.height - this.marginBottom, 0]);
.range([this.height - this.marginBottom, 0]) // .range([height - margin.bottom, margin.top]);
// .range([height - margin.bottom, margin.top]);
const bins = d3.histogram() const bins = d3
.domain(x.domain()) .histogram()
.thresholds(40)(allValuesForContinuousFieldAsArray) .domain(x.domain())
.thresholds(40)(allValuesForContinuousFieldAsArray);
d3.select(svgRef) d3
.select(svgRef)
.insert("g", "*") .insert("g", "*")
.attr("fill", "#bbb") .attr("fill", "#bbb")
.selectAll("rect") .selectAll("rect")
.data(bins) .data(bins)
.enter().append("rect") .enter()
.attr("x", function(d) { return x(d.x0) + 1; }) .append("rect")
.attr("y", function(d) { return y(d.length / allValuesForContinuousFieldAsArray.length); }) .attr("x", function(d) {
.attr("width", function(d) { return Math.abs(x(d.x1) - x(d.x0) - 1); }) return x(d.x0) + 1;
.attr("height", function(d) { return y(0) - y(d.length / allValuesForContinuousFieldAsArray.length); }); })
.attr("y", function(d) {
return y(d.length / allValuesForContinuousFieldAsArray.length);
})
.attr("width", function(d) {
return Math.abs(x(d.x1) - x(d.x0) - 1);
})
.attr("height", function(d) {
return y(0) - y(d.length / allValuesForContinuousFieldAsArray.length);
});
if (!this.state.brush && !this.state.axis) { if (!this.state.brush && !this.state.axis) {
const brush = d3.select(svgRef) const brush = d3
.append('g') .select(svgRef)
.attr('class', 'brush') .append("g")
.attr("class", "brush")
.call( .call(
d3.brushX() d3
.on('end', this.onBrush(this.props.metadataField, x.invert).bind(this)) .brushX()
) .on(
"end",
this.onBrush(this.props.metadataField, x.invert).bind(this)
)
);
const xAxis = d3.select(svgRef) const xAxis = d3
.select(svgRef)
.append("g") .append("g")
.attr("class", "axis axis--x") .attr("class", "axis axis--x")
.attr("transform", "translate(0," + (this.height - this.marginBottom) + ")") .attr(
.call( "transform",
d3.axisBottom(x) "translate(0," + (this.height - this.marginBottom) + ")"
.ticks(5)
) )
.call(d3.axisBottom(x).ticks(5))
.append("text") .append("text")
.attr("x", 300) .attr("x", 300)
.attr("y", -6) .attr("y", -6)
.attr("fill", "#000") .attr("fill", "#000")
.attr("text-anchor", "end") .attr("text-anchor", "end")
.attr("font-weight", "bold") .attr("font-weight", "bold")
.text(this.props.metadataField) .text(this.props.metadataField);
this.setState({brush, xAxis}) this.setState({ brush, xAxis });
} }
} }
render() { render() {
return ( return (
<div style={{marginTop: 10}} id={"histogram_" + this.props.metadataField}> <div
<svg width={this.width} height={this.height} ref={(svgRef) => { this.drawHistogram(svgRef)}}> style={{ marginTop: 10 }}
id={"histogram_" + this.props.metadataField}
>
<svg
width={this.width}
height={this.height}
ref={svgRef => {
this.drawHistogram(svgRef);
}}
>
{this.props.ranges.min} {this.props.ranges.min}
{" to "} {" to "}
{this.props.ranges.max} {this.props.ranges.max}
</svg> </svg>
</div> </div>
) );
} }
}; }
export default HistogramBrush; export default HistogramBrush;
+42 -44
View File
@@ -1,29 +1,33 @@
// jshint esversion: 6
/* rc slider https://www.npmjs.com/package/rc-slider */ /* rc slider https://www.npmjs.com/package/rc-slider */
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 styles from './parallelCoordinates.css'; import styles from "./parallelCoordinates.css";
import SectionHeader from "../framework/sectionHeader"; import SectionHeader from "../framework/sectionHeader";
import setupParallelCoordinates from "./setupParallelCoordinates"; import setupParallelCoordinates from "./setupParallelCoordinates";
import drawAxes from "./drawAxes"; import drawAxes from "./drawAxes";
import drawLinesCanvas from "./drawLinesCanvas"; import drawLinesCanvas from "./drawLinesCanvas";
import { import { margin, width, height, createDimensions } from "./util";
margin,
width,
height,
createDimensions,
} from "./util";
@connect((state) => { @connect(state => {
const ranges =
state.cells.cells && state.cells.cells.data.ranges
? state.cells.cells.data.ranges
: null;
const metadata =
state.cells.cells && state.cells.cells.data.metadata
? state.cells.cells.data.metadata
: null;
const ranges = state.cells.cells && state.cells.cells.data.ranges ? state.cells.cells.data.ranges : null; const initializeRanges =
const metadata = state.cells.cells && state.cells.cells.data.metadata ? state.cells.cells.data.metadata : null; state.initialize.data && state.initialize.data.data.ranges
? state.initialize.data.data.ranges
const initializeRanges = state.initialize.data && state.initialize.data.data.ranges ? state.initialize.data.data.ranges : null; : null;
return { return {
ranges, ranges,
@@ -33,8 +37,8 @@ import {
colorScale: state.controls.colorScale, colorScale: state.controls.colorScale,
graphBrushSelection: state.controls.graphBrushSelection, graphBrushSelection: state.controls.graphBrushSelection,
currentCellSelection: state.controls.currentCellSelection, currentCellSelection: state.controls.currentCellSelection,
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn, axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
} };
}) })
class Parallel extends React.Component { class Parallel extends React.Component {
constructor(props) { constructor(props) {
@@ -43,16 +47,12 @@ class Parallel extends React.Component {
svg: null, svg: null,
ctx: null, ctx: null,
axes: null, axes: null,
dimensions: null, dimensions: null
}; };
} }
componentDidMount() { componentDidMount() {
const {svg, ctx} = setupParallelCoordinates( const { svg, ctx } = setupParallelCoordinates(width, height, margin);
width, this.setState({ svg, ctx });
height,
margin
);
this.setState({svg, ctx})
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.maybeDrawAxes(nextProps); this.maybeDrawAxes(nextProps);
@@ -63,10 +63,10 @@ class Parallel extends React.Component {
!this.state.axes && !this.state.axes &&
nextProps.initializeRanges /* axes are created on full range of data */ nextProps.initializeRanges /* axes are created on full range of data */
) { ) {
const dimensions = createDimensions(nextProps.initializeRanges); const dimensions = createDimensions(nextProps.initializeRanges);
const xscale = d3.scalePoint() const xscale = d3
.scalePoint()
.domain(d3.range(dimensions.length)) .domain(d3.range(dimensions.length))
.range([0, width]); .range([0, width]);
@@ -78,28 +78,27 @@ class Parallel extends React.Component {
height, height,
width, width,
this.handleBrushAction.bind(this), this.handleBrushAction.bind(this),
this.handleColorAction.bind(this), this.handleColorAction.bind(this)
); );
this.setState({ this.setState({
axes, axes,
xscale, xscale,
dimensions, dimensions
}) });
this.props.dispatch({ this.props.dispatch({
type: "parallel coordinates axes have been drawn" type: "parallel coordinates axes have been drawn"
}) });
} }
} }
maybeDrawLines = _.debounce((nextProps) => { /* https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js */ maybeDrawLines = _.debounce(nextProps => {
/* https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js */
if ( if (
nextProps.ranges && nextProps.ranges &&
nextProps.currentCellSelection && nextProps.currentCellSelection &&
nextProps.axesHaveBeenDrawn nextProps.axesHaveBeenDrawn
) { ) {
if (this.state._drawLinesCanvas) { if (this.state._drawLinesCanvas) {
this.state._drawLinesCanvas.invalidate(); /* this is only necessary if the internals of drawLinesCanvas are using the render queue */ this.state._drawLinesCanvas.invalidate(); /* this is only necessary if the internals of drawLinesCanvas are using the render queue */
} }
@@ -112,22 +111,22 @@ class Parallel extends React.Component {
this.state.xscale, this.state.xscale,
this.state.ctx, this.state.ctx,
nextProps.colorAccessor, nextProps.colorAccessor,
nextProps.colorScale, nextProps.colorScale
); );
this.setState({ this.setState({
_drawLinesCanvas, /* this will only exist if the internals of drawLinesCanvas are using the render queue */ _drawLinesCanvas /* this will only exist if the internals of drawLinesCanvas are using the render queue */
}) });
} }
}, 200) }, 200);
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,
@@ -136,22 +135,21 @@ class Parallel extends React.Component {
} }
render() { render() {
return ( return (
<div id="parcoords_wrapper"> <div id="parcoords_wrapper">
<div <div
className={styles.parcoords} className={styles.parcoords}
id="parcoords" id="parcoords"
style={{ style={{
width: width + margin.left + margin.right + "px", width: width + margin.left + margin.right + "px",
height: height + margin.top + margin.bottom + "px" height: height + margin.top + margin.bottom + "px"
}}></div> }}
/>
</div> </div>
) );
} }
}; }
export default Parallel; export default Parallel;
// <SectionHeader text="Continuous Metadata"/> // <SectionHeader text="Continuous Metadata"/>
@@ -3,22 +3,19 @@
Setup SVG & Canvas elements Setup SVG & Canvas elements
****************************************** ******************************************
******************************************/ ******************************************/
// jshint esversion: 6
const setupParallelCoordinates = (width, height, margin) => {
var container = d3.select("#parcoords");
const setupParallelCoordinates = ( var svg = container
width, .append("svg")
height, .attr("width", width + margin.left + margin.right)
margin .attr("height", height + margin.top + margin.bottom)
) => {
var container = d3.select("#parcoords")
var svg = container.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g") .append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var canvas = container.append("canvas") var canvas = container
.append("canvas")
.attr("width", width * devicePixelRatio) .attr("width", width * devicePixelRatio)
.attr("height", height * devicePixelRatio) .attr("height", height * devicePixelRatio)
.style("width", width + "px") .style("width", width + "px")
@@ -27,16 +24,15 @@ const setupParallelCoordinates = (
.style("margin-left", margin.left + "px"); .style("margin-left", margin.left + "px");
var ctx = canvas.node().getContext("2d"); var ctx = canvas.node().getContext("2d");
ctx.globalCompositeOperation = 'darken'; ctx.globalCompositeOperation = "darken";
ctx.globalAlpha = 0.15; ctx.globalAlpha = 0.15;
ctx.lineWidth = 1.5; ctx.lineWidth = 1.5;
ctx.scale(devicePixelRatio, devicePixelRatio); ctx.scale(devicePixelRatio, devicePixelRatio);
return { return {
svg, svg,
ctx, ctx
} };
};
} export default setupParallelCoordinates;
export default setupParallelCoordinates
+25 -19
View File
@@ -1,51 +1,57 @@
// jshint esversion: 6
import _ from "lodash"; import _ from "lodash";
const paddingRight = 120; const paddingRight = 120;
const continuousChartWidth = 1200; const continuousChartWidth = 1200;
export const margin = {top: 66, right: 110, bottom: 20, left: 60}; export const margin = { top: 66, right: 110, bottom: 20, left: 60 };
export const width = continuousChartWidth - margin.left - margin.right - paddingRight; export const width =
continuousChartWidth - margin.left - margin.right - paddingRight;
export const height = 340 - margin.top - margin.bottom; export const height = 340 - margin.top - margin.bottom;
export const innerHeight = height - 2; export const innerHeight = height - 2;
export const devicePixelRatio = window.devicePixelRatio || 1; export const devicePixelRatio = window.devicePixelRatio || 1;
export const createDimensions = (data) => { export const createDimensions = data => {
const newArr = [] const newArr = [];
_.each(data, (value, key) => { _.each(data, (value, key) => {
if (value.range) { if (value.range) {
newArr.push({ newArr.push({
key: key, /* room for confusion: lodash calls this key, it's also the name of the property parallel coords code is looking for */ key: key /* room for confusion: lodash calls this key, it's also the name of the property parallel coords code is looking for */,
type: { type: {
within: (d, extent, dim) => { within: (d, extent, dim) => {
return extent[0] <= dim.scale(d) && dim.scale(d) <= extent[1]; return extent[0] <= dim.scale(d) && dim.scale(d) <= extent[1];
}, }
}, },
scale: d3.scaleSqrt().range([innerHeight, 0]).domain([0, value.range.max]) scale: d3
}) .scaleSqrt()
.range([innerHeight, 0])
.domain([0, value.range.max])
});
} }
}) });
return newArr; return newArr;
} };
export const yAxis = d3.axisLeft(); export const yAxis = d3.axisLeft();
export const brushstart = () => { export const brushstart = () => {
d3.event.sourceEvent.stopPropagation(); d3.event.sourceEvent.stopPropagation();
} };
export const d3_functor = (v) => { export const d3_functor = v => {
return typeof v === "function" ? v : () => { return v; }; return typeof v === "function"
? v
: () => {
return v;
};
}; };
export const project = (d, dimensions, xscale) => { export const project = (d, dimensions, xscale) => {
return dimensions.map((p,i) => { return dimensions.map((p, i) => {
// check if data element has property and contains a value // check if data element has property and contains a value
if ( if (!(p.key in d) || d[p.key] === null) return null;
!(p.key in d) ||
d[p.key] === null
) return null;
return [xscale(i),p.scale(d[p.key])]; return [xscale(i), p.scale(d[p.key])];
}); });
}; };
+33 -16
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import React from "react"; import React from "react";
import _ from "lodash"; import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -10,20 +11,22 @@ class CellSetButton extends React.Component {
set() { set() {
const set = []; const set = [];
_.each(this.props.currentCellSelection, (cell) => { _.each(this.props.currentCellSelection, cell => {
if (cell["__selected__"]) { if (cell["__selected__"]) {
set.push(cell.CellName) set.push(cell.CellName);
} }
}) });
this.props.dispatch({ this.props.dispatch({
type: "store current cell selection as differential set " + this.props.eitherCellSetOneOrTwo, type:
"store current cell selection as differential set " +
this.props.eitherCellSetOneOrTwo,
data: set data: set
}) });
} }
render() { render() {
return ( return (
<span style={{marginRight: 10}}> <span style={{ marginRight: 10 }}>
<button <button
style={{ style={{
color: "#FFF", color: "#FFF",
@@ -31,20 +34,34 @@ class CellSetButton extends React.Component {
height: 30, height: 30,
backgroundColor: globals.brightBlue, backgroundColor: globals.brightBlue,
border: "none", border: "none",
cursor: "pointer", cursor: "pointer"
}} }}
onClick={this.set.bind(this)}> onClick={this.set.bind(this)}
<span style={{fontSize: 24, fontWeight: 700}}> {this.props.eitherCellSetOneOrTwo} </span> >
<span style={{fontFamily: "Georgia", fontStyle: "italic", marginLeft: 8, position: "relative", top: -3}}> <span style={{ fontSize: 24, fontWeight: 700 }}>
{ {" "}
this.props.differential["celllist" + this.props.eitherCellSetOneOrTwo] ? {this.props.eitherCellSetOneOrTwo}{" "}
this.props.differential["celllist" + this.props.eitherCellSetOneOrTwo].length + " cells" : </span>
0 + " cells" <span
} style={{
fontFamily: "Georgia",
fontStyle: "italic",
marginLeft: 8,
position: "relative",
top: -3
}}
>
{this.props.differential[
"celllist" + this.props.eitherCellSetOneOrTwo
]
? this.props.differential[
"celllist" + this.props.eitherCellSetOneOrTwo
].length + " cells"
: 0 + " cells"}
</span> </span>
</button> </button>
</span> </span>
) );
} }
} }
+168 -111
View File
@@ -1,42 +1,45 @@
// jshint esversion: 6
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 * as globals from "../../globals"; import * as globals from "../../globals";
import styles from "./expression.css"; import styles from "./expression.css";
import SectionHeader from "../framework/sectionHeader" import SectionHeader from "../framework/sectionHeader";
import actions from "../../actions"; import actions from "../../actions";
import ReactAutocomplete from "react-autocomplete"; /* http://emilebres.github.io/react-virtualized-checkbox/ */ import ReactAutocomplete from "react-autocomplete"; /* http://emilebres.github.io/react-virtualized-checkbox/ */
import getContrast from "font-color-contrast"; // https://www.npmjs.com/package/font-color-contrast import getContrast from "font-color-contrast"; // https://www.npmjs.com/package/font-color-contrast
import FaPaintBrush from 'react-icons/lib/fa/paint-brush'; import FaPaintBrush from "react-icons/lib/fa/paint-brush";
class HeatmapSquare extends React.Component { class HeatmapSquare extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props);
this.state = { this.state = {
value: '', value: ""
} };
} }
render() { render() {
const contrastColor = getContrast( const contrastColor = getContrast(
this.props.backgroundColor this.props.backgroundColor
.substring(4, this.props.backgroundColor.length-1) .substring(4, this.props.backgroundColor.length - 1)
.replace(/ /g, "") .replace(/ /g, "")
.split(",") .split(",")
) );
return ( return (
<p style={{ <p
padding: "12px 6px", style={{
textAlign: "center", padding: "12px 6px",
color: contrastColor, textAlign: "center",
width: 40, color: contrastColor,
flexShrink: 0, width: 40,
fontSize: 12, flexShrink: 0,
margin: 0, fontSize: 12,
backgroundColor: this.props.backgroundColor, margin: 0,
}}> backgroundColor: this.props.backgroundColor
}}
>
{this.props.text} {this.props.text}
</p> </p>
) );
} }
} }
@@ -47,72 +50,66 @@ class HeatmapSquare extends React.Component {
*********************************** ***********************************
*********************************** ***********************************
**********************************/ **********************************/
@connect((state) => { @connect(state => {
return { return {
scatterplotXXaccessor: state.controls.scatterplotXXaccessor, scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor, scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
colorAccessor: state.controls.colorAccessor, colorAccessor: state.controls.colorAccessor
} };
}) })
class HeatmapRow extends React.Component { class HeatmapRow extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props);
this.state = { this.state = {
value: '', value: ""
} };
} }
handleGeneColorScaleClick(gene) { handleGeneColorScaleClick(gene) {
return () => { return () => {
this.props.dispatch( this.props.dispatch(
actions.requestSingleGeneExpressionCountsForColoringPOST(this.props.gene) actions.requestSingleGeneExpressionCountsForColoringPOST(
) this.props.gene
} )
);
};
} }
handleSetGeneAsScatterplotX(gene) { handleSetGeneAsScatterplotX(gene) {
return () => { return () => {
this.props.dispatch({ this.props.dispatch({
type: "set scatterplot x", type: "set scatterplot x",
data: this.props.gene data: this.props.gene
}) });
} };
} }
handleSetGeneAsScatterplotY(gene) { handleSetGeneAsScatterplotY(gene) {
return () => { return () => {
this.props.dispatch({ this.props.dispatch({
type: "set scatterplot y", type: "set scatterplot y",
data: this.props.gene data: this.props.gene
}) });
} };
} }
render() { render() {
return ( return (
<div style={{ <div
width: 220, style={{
display: "flex", width: 220,
justifyContent: "flex-start", display: "flex",
alignItems: "baseline", justifyContent: "flex-start",
}}> alignItems: "baseline"
<div style={{width: 150, flexShrink: 0}}> }}
<span >
onClick={this.handleSetGeneAsScatterplotX(this.props.gene).bind(this)} <div style={{ width: 150, flexShrink: 0 }}>
style={{
fontSize: 16,
color: this.props.scatterplotXXaccessor === this.props.gene ? "white" : globals.brightBlue,
cursor: "pointer",
position: "relative",
top: 1,
fontWeight: 700,
marginRight: 4,
borderRadius: 3,
padding: "2px 3px",
backgroundColor: this.props.scatterplotXXaccessor === this.props.gene ? globals.brightBlue : "inherit",
}}>X</span>
<span <span
onClick={this.handleSetGeneAsScatterplotY(this.props.gene).bind(this)} onClick={this.handleSetGeneAsScatterplotX(this.props.gene).bind(
this
)}
style={{ style={{
fontSize: 16, fontSize: 16,
color: this.props.scatterplotYYaccessor === this.props.gene ? "white" : globals.brightBlue, color:
this.props.scatterplotXXaccessor === this.props.gene
? "white"
: globals.brightBlue,
cursor: "pointer", cursor: "pointer",
position: "relative", position: "relative",
top: 1, top: 1,
@@ -120,8 +117,39 @@ class HeatmapRow extends React.Component {
marginRight: 4, marginRight: 4,
borderRadius: 3, borderRadius: 3,
padding: "2px 3px", padding: "2px 3px",
backgroundColor: this.props.scatterplotYYaccessor === this.props.gene ? globals.brightBlue : "inherit", backgroundColor:
}}>Y</span> this.props.scatterplotXXaccessor === this.props.gene
? globals.brightBlue
: "inherit"
}}
>
X
</span>
<span
onClick={this.handleSetGeneAsScatterplotY(this.props.gene).bind(
this
)}
style={{
fontSize: 16,
color:
this.props.scatterplotYYaccessor === this.props.gene
? "white"
: globals.brightBlue,
cursor: "pointer",
position: "relative",
top: 1,
fontWeight: 700,
marginRight: 4,
borderRadius: 3,
padding: "2px 3px",
backgroundColor:
this.props.scatterplotYYaccessor === this.props.gene
? globals.brightBlue
: "inherit"
}}
>
Y
</span>
<span <span
onClick={this.handleGeneColorScaleClick(this.props.gene).bind(this)} onClick={this.handleGeneColorScaleClick(this.props.gene).bind(this)}
style={{ style={{
@@ -131,32 +159,45 @@ class HeatmapRow extends React.Component {
marginRight: 6, marginRight: 6,
borderRadius: 3, borderRadius: 3,
padding: "0px 2px 2px 2px", padding: "0px 2px 2px 2px",
color: this.props.colorAccessor === this.props.gene ? "white" : "inherit", color:
backgroundColor: this.props.colorAccessor === this.props.gene ? globals.brightBlue : "inherit", this.props.colorAccessor === this.props.gene
}}><FaPaintBrush style={{display: "inline-block"}}/></span> ? "white"
: "inherit",
backgroundColor:
this.props.colorAccessor === this.props.gene
? globals.brightBlue
: "inherit"
}}
>
<FaPaintBrush style={{ display: "inline-block" }} />
</span>
<span <span
style={{ style={{
fontSize: 14 fontSize: 14
}}> }}
>
{this.props.gene} {this.props.gene}
</span> </span>
</div> </div>
<HeatmapSquare <HeatmapSquare
backgroundColor={this.props.greyColorScale(this.props.set1exp)} backgroundColor={this.props.greyColorScale(this.props.set1exp)}
text={this.props.set1exp}/> text={this.props.set1exp}
/>
<HeatmapSquare <HeatmapSquare
backgroundColor={this.props.greyColorScale(this.props.set2exp)} backgroundColor={this.props.greyColorScale(this.props.set2exp)}
text={this.props.set2exp}/> text={this.props.set2exp}
/>
<span <span
title={this.props.aveDiff} title={this.props.aveDiff}
style={{ style={{
fontSize: 14, fontSize: 14,
paddingLeft: 10 paddingLeft: 10
}}> }}
{this.props.aveDiff.toFixed(2)} >
{this.props.aveDiff.toFixed(2)}
</span> </span>
</div> </div>
) );
} }
} }
@@ -168,21 +209,22 @@ class HeatmapRow extends React.Component {
*********************************** ***********************************
**********************************/ **********************************/
@connect((state) => { @connect(state => {
return { return {
differential: state.differential, differential: state.differential,
allGeneNames: state.controls.allGeneNames allGeneNames: state.controls.allGeneNames
} };
}) })
class Heatmap extends React.Component { class Heatmap extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props);
this.state = { this.state = {
value: '', value: ""
} };
} }
render() { render() {
if (!this.props.differential.diffExp) return <p>Select cells & compute differential to see heatmap</p> if (!this.props.differential.diffExp)
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;
@@ -194,71 +236,86 @@ class Heatmap extends React.Component {
topGenesForCellSet2.mean_expression_cellset1, topGenesForCellSet2.mean_expression_cellset1,
topGenesForCellSet2.mean_expression_cellset2 topGenesForCellSet2.mean_expression_cellset2
) )
) );
const greyColorScale = d3.scaleSequential() const greyColorScale = d3
.domain(extent) .scaleSequential()
.interpolator(d3.interpolateGreys); .domain(extent)
.interpolator(d3.interpolateGreys);
return ( return (
<div> <div>
Color by any gene: Color by any gene:
<ReactAutocomplete <ReactAutocomplete
items={this.props.allGeneNames} items={this.props.allGeneNames}
shouldItemRender={(item, value) => item.toLowerCase().indexOf(value.toLowerCase()) > -1} shouldItemRender={(item, value) =>
item.toLowerCase().indexOf(value.toLowerCase()) > -1
}
getItemValue={item => item} getItemValue={item => item}
renderItem={(item, highlighted) => renderItem={(item, highlighted) => (
<div <div
key={item} key={item}
style={{ backgroundColor: highlighted ? '#eee' : 'transparent'}} style={{ backgroundColor: highlighted ? "#eee" : "transparent" }}
> >
{item} {item}
</div> </div>
} )}
value={this.state.value} value={this.state.value}
onChange={e => this.setState({ value: e.target.value })} onChange={e => this.setState({ value: e.target.value })}
onSelect={(value) => { onSelect={value => {
this.setState({ value }); this.setState({ value });
this.props.dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(value)) this.props.dispatch(
actions.requestSingleGeneExpressionCountsForColoringPOST(value)
);
}} }}
/> />
<div style={{ <div
display: "flex", style={{
justifyContent: "flex-start", display: "flex",
width: 400, justifyContent: "flex-start",
fontWeight: 700, width: 400,
}}> fontWeight: 700
<p style={{marginRight: 110}}>Gene</p> }}
<p style={{marginRight: 25}}>1</p> >
<p style={{marginRight: 20}}>2</p> <p style={{ marginRight: 110 }}>Gene</p>
<p style={{ marginRight: 25 }}>1</p>
<p style={{ marginRight: 20 }}>2</p>
<p>ave diff</p> <p>ave diff</p>
</div> </div>
{ {topGenesForCellSet1.topgenes.map((gene, i) => {
topGenesForCellSet1.topgenes.map((gene, i) => { return (
return <HeatmapRow <HeatmapRow
key={gene} key={gene}
gene={gene} gene={gene}
greyColorScale={greyColorScale} greyColorScale={greyColorScale}
aveDiff={topGenesForCellSet1.ave_diff[i]} aveDiff={topGenesForCellSet1.ave_diff[i]}
set1exp={Math.floor(topGenesForCellSet1.mean_expression_cellset1[i])} set1exp={Math.floor(
set2exp={Math.floor(topGenesForCellSet1.mean_expression_cellset2[i])} topGenesForCellSet1.mean_expression_cellset1[i]
/> )}
}) set2exp={Math.floor(
} topGenesForCellSet1.mean_expression_cellset2[i]
{ )}
topGenesForCellSet2.topgenes.map((gene, i) => { />
return <HeatmapRow );
})}
{topGenesForCellSet2.topgenes.map((gene, i) => {
return (
<HeatmapRow
key={gene} key={gene}
gene={gene} gene={gene}
greyColorScale={greyColorScale} greyColorScale={greyColorScale}
aveDiff={topGenesForCellSet2.ave_diff[i]} aveDiff={topGenesForCellSet2.ave_diff[i]}
set1exp={Math.floor(topGenesForCellSet2.mean_expression_cellset1[i])} set1exp={Math.floor(
set2exp={Math.floor(topGenesForCellSet2.mean_expression_cellset2[i])} topGenesForCellSet2.mean_expression_cellset1[i]
/> )}
}) set2exp={Math.floor(
} topGenesForCellSet2.mean_expression_cellset2[i]
)}
/>
);
})}
</div> </div>
) );
} }
} }
+32 -40
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import React from "react"; import React from "react";
import _ from "lodash"; import _ from "lodash";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -5,26 +6,24 @@ import * as globals from "../../globals";
import actions from "../../actions"; import actions from "../../actions";
import CellSetButton from "./cellSetButtons"; import CellSetButton from "./cellSetButtons";
@connect((state) => { @connect(state => {
return { return {
currentCellSelection: state.controls.currentCellSelection, currentCellSelection: state.controls.currentCellSelection,
differential: state.differential differential: state.differential
} };
}) })
class Expression extends React.Component { class Expression extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {};
};
} }
handleClick(gene) { handleClick(gene) {
return () => { return () => {
this.props.dispatch({ this.props.dispatch({
type: "color by expression", type: "color by expression",
gene: gene, gene: gene
}); });
} };
} }
computeDiffExp() { computeDiffExp() {
this.props.dispatch( this.props.dispatch(
@@ -32,36 +31,28 @@ class Expression extends React.Component {
this.props.differential.celllist1, this.props.differential.celllist1,
this.props.differential.celllist2 this.props.differential.celllist2
) )
) );
} }
render () { render() {
if ( if (!this.props.differential) {
!this.props.differential return null;
) {
return null
} }
return ( return (
<div> <div>
<div style={{margin: 10}}> <div style={{ margin: 10 }}>
<div style={{marginBottom: 10, width: 300}}> <div style={{ marginBottom: 10, width: 300 }}>
There are currently There are currently
{ {" " +
" " + _.filter(this.props.currentCellSelection, "__selected__").length +
_.filter(this.props.currentCellSelection, "__selected__").length + " "}
" " cells selected, click a cell set button to store them.
} </div>
cells selected, click a cell set button to store them. <CellSetButton {...this.props} eitherCellSetOneOrTwo={1} />
</div> <CellSetButton {...this.props} eitherCellSetOneOrTwo={2} />
<CellSetButton
{...this.props}
eitherCellSetOneOrTwo={1}/>
<CellSetButton
{...this.props}
eitherCellSetOneOrTwo={2}/>
</div> </div>
<div> <div>
{ {this.props.differential.celllist1 &&
this.props.differential.celllist1 && this.props.differential.celllist2 ? this.props.differential.celllist2 ? (
<button <button
style={{ style={{
fontSize: 18, fontSize: 18,
@@ -71,11 +62,13 @@ class Expression extends React.Component {
padding: "12px 20px", padding: "12px 20px",
backgroundColor: globals.brightBlue, backgroundColor: globals.brightBlue,
border: "none", border: "none",
cursor: "pointer", cursor: "pointer"
}} }}
onClick={this.computeDiffExp.bind(this)}> onClick={this.computeDiffExp.bind(this)}
>
Compute differential expression Compute differential expression
</button> : </button>
) : (
<button <button
style={{ style={{
fontSize: 18, fontSize: 18,
@@ -84,17 +77,16 @@ class Expression extends React.Component {
color: "#FFF", color: "#FFF",
padding: "12px 20px", padding: "12px 20px",
backgroundColor: globals.mediumGrey, backgroundColor: globals.mediumGrey,
border: "none", border: "none"
}} }}
> >
Compute differential expression Compute differential expression
</button> </button>
} )}
</div> </div>
</div> </div>
) );
} }
}; }
export default Expression; export default Expression;
+4 -5
View File
@@ -1,11 +1,10 @@
import React from 'react'; // jshint esversion: 6
import React from "react";
import styles from './container.css'; import styles from "./container.css";
const Container = props => ( const Container = props => (
<div className={styles.container}> <div className={styles.container}>{props.children}</div>
{props.children}
</div>
); );
export default Container; export default Container;
+5 -7
View File
@@ -1,14 +1,12 @@
import React from 'react'; // jshint esversion: 6
import React from "react";
import Container from './container';
import styles from './header.css';
import Container from "./container";
import styles from "./header.css";
const Header = () => ( const Header = () => (
<header className={styles.header}> <header className={styles.header}>
<Container> <Container />
</Container>
</header> </header>
); );
+9 -6
View File
@@ -1,10 +1,13 @@
import React from 'react'; // jshint esversion: 6
import React from "react";
const SectionHeader = ({text}) => ( const SectionHeader = ({ text }) => (
<p style={{ <p
fontSize: 32, style={{
fontWeight: 700 fontSize: 32,
}}> fontWeight: 700
}}
>
{text} {text}
</p> </p>
); );
+58 -59
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import styles from "./graph.css"; import styles from "./graph.css";
import renderQueue from "../../util/renderQueue"; import renderQueue from "../../util/renderQueue";
import _ from "lodash"; import _ from "lodash";
@@ -13,7 +14,6 @@ export const setupGraphElements = (
handleBrushSelectAction, handleBrushSelectAction,
handleBrushDeselectAction handleBrushDeselectAction
) => { ) => {
// var canvas = d3.select("#graphAttachPoint") // var canvas = d3.select("#graphAttachPoint")
// .append("canvas") // .append("canvas")
// .attr("width", globals.graphWidth) // .attr("width", globals.graphWidth)
@@ -22,25 +22,22 @@ export const setupGraphElements = (
// var ctx = canvas.node().getContext("2d"); // var ctx = canvas.node().getContext("2d");
var svg = d3.select("#graphAttachPoint").append("svg") var svg = d3
.select("#graphAttachPoint")
.append("svg")
.attr("width", globals.graphWidth) .attr("width", globals.graphWidth)
.attr("height", globals.graphHeight) .attr("height", globals.graphHeight)
.attr("class", `${styles.graphSVG}`) .attr("class", `${styles.graphSVG}`);
// .append("g") // .append("g")
// .attr("transform", "translate(" + margin.left + " " + margin.top + ")"); // .attr("transform", "translate(" + margin.left + " " + margin.top + ")");
setupGraphBrush( setupGraphBrush(svg, handleBrushSelectAction, handleBrushDeselectAction);
svg,
handleBrushSelectAction,
handleBrushDeselectAction
);
return { return {
svg, svg
// ctx // ctx
} };
};
}
/****************************************** /******************************************
******************************************* *******************************************
@@ -57,36 +54,34 @@ const drawGraph = (
currentCellSelection, currentCellSelection,
graphBrushSelection, graphBrushSelection,
colorScale, colorScale,
graphMap, /* tmp remove when structure exists on server */ graphMap /* tmp remove when structure exists on server */,
opacityForDeselectedCells, opacityForDeselectedCells,
_currentCellSelectionMap, _currentCellSelectionMap
) => { ) => {
return (p) => { return p => {
/* shuffle the data to overcome render order hiding cells, & filter first */ /* shuffle the data to overcome render order hiding cells, & filter first */
// data = d3.shuffle(data); /* make me a control */ // data = d3.shuffle(data); /* make me a control */
context.beginPath(); context.beginPath();
/* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */ /* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */
context.arc( context.arc(
globals.graphXScale(p[1]), /* x */ globals.graphXScale(p[1]) /* x */,
globals.graphYScale(p[2]), /* y */ globals.graphYScale(p[2]) /* y */,
_currentCellSelectionMap[p[0]]["__selected__"] ? 3 : 1.5, /* r */ _currentCellSelectionMap[p[0]]["__selected__"] ? 3 : 1.5 /* r */,
0, /* sAngle */ 0 /* sAngle */,
2 * Math.PI /* eAngle */ 2 * Math.PI /* eAngle */
); );
context.fillStyle = _currentCellSelectionMap[p[0]]["__color__"] context.fillStyle = _currentCellSelectionMap[p[0]]["__color__"];
if (_currentCellSelectionMap[p[0]]["__selected__"]) { if (_currentCellSelectionMap[p[0]]["__selected__"]) {
context.globalAlpha = 1; context.globalAlpha = 1;
} else { } else {
context.globalAlpha = opacityForDeselectedCells; context.globalAlpha = opacityForDeselectedCells;
} }
context.fill(); context.fill();
};
} };
}
const _drawGraphUsingRenderQueue = ( const _drawGraphUsingRenderQueue = (
context, context,
@@ -97,22 +92,26 @@ const _drawGraphUsingRenderQueue = (
currentCellSelection, currentCellSelection,
graphBrushSelection, graphBrushSelection,
colorScale, colorScale,
graphMap, /* tmp remove when structure exists on server */ graphMap /* tmp remove when structure exists on server */,
opacityForDeselectedCells, opacityForDeselectedCells
) => { ) => {
const _currentCellSelectionMap = _.keyBy(currentCellSelection, "CellName"); /* move me to the reducer */ const _currentCellSelectionMap = _.keyBy(
currentCellSelection,
"CellName"
); /* move me to the reducer */
const dataForGraph = []; const dataForGraph = [];
_.each(currentCellSelection, (cell, i) => { _.each(currentCellSelection, (cell, i) => {
if (graphMap[cell["CellName"]]) { /* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */ if (graphMap[cell["CellName"]]) {
/* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */
dataForGraph.push([ dataForGraph.push([
cell["CellName"], cell["CellName"],
graphMap[cell["CellName"]][0], graphMap[cell["CellName"]][0],
graphMap[cell["CellName"]][1] graphMap[cell["CellName"]][1]
]) ]);
} }
}) });
/* clear canvas */ /* clear canvas */
context.clearRect(0, 0, globals.graphWidth, globals.graphHeight); context.clearRect(0, 0, globals.graphWidth, globals.graphHeight);
@@ -127,30 +126,30 @@ const _drawGraphUsingRenderQueue = (
currentCellSelection, currentCellSelection,
graphBrushSelection, graphBrushSelection,
colorScale, colorScale,
graphMap, /* tmp remove when structure exists on server */ graphMap /* tmp remove when structure exists on server */,
opacityForDeselectedCells, opacityForDeselectedCells,
_currentCellSelectionMap, _currentCellSelectionMap
) )
) );
_renderGraphWithFunctionReturnedByQueue(dataForGraph); _renderGraphWithFunctionReturnedByQueue(dataForGraph);
return _renderGraphWithFunctionReturnedByQueue; return _renderGraphWithFunctionReturnedByQueue;
} };
export const drawGraphUsingRenderQueue = _.debounce(_drawGraphUsingRenderQueue, 100); export const drawGraphUsingRenderQueue = _.debounce(
_drawGraphUsingRenderQueue,
100
);
const setupGraphBrush = ( const setupGraphBrush = (
svg, svg,
handleBrushSelectAction, handleBrushSelectAction,
handleBrushDeselectAction handleBrushDeselectAction
) => { ) => {
svg.append("g") svg.append("g").call(
.call( d3
d3.brush() .brush()
.extent([ .extent([[0, 0], [globals.graphWidth, globals.graphHeight]])
[0, 0], .on("brush", handleBrushSelectAction)
[globals.graphWidth, globals.graphHeight] .on("end", handleBrushDeselectAction)
]) );
.on("brush", handleBrushSelectAction) };
.on("end", handleBrushDeselectAction)
);
}
+13 -12
View File
@@ -1,8 +1,9 @@
const mat4 = require('gl-mat4') // jshint esversion: 6
const mat4 = require("gl-mat4");
// opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js // opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js
export default function (regl) { export default function(regl) {
return regl({ return regl({
vert: ` vert: `
precision mediump float; precision mediump float;
@@ -29,14 +30,14 @@ export default function (regl) {
}`, }`,
attributes: { attributes: {
position: regl.prop('position'), position: regl.prop("position"),
color: regl.prop('color'), color: regl.prop("color"),
size: regl.prop('size') size: regl.prop("size")
}, },
uniforms: { uniforms: {
distance: regl.prop('distance'), distance: regl.prop("distance"),
view: regl.prop('view'), view: regl.prop("view"),
projection: (context, props) => { projection: (context, props) => {
return mat4.perspective( return mat4.perspective(
[], [],
@@ -44,12 +45,12 @@ export default function (regl) {
context.viewportWidth * props.scale / context.viewportHeight, context.viewportWidth * props.scale / context.viewportHeight,
0.01, 0.01,
1000 1000
) );
}, }
}, },
count: regl.prop('count'), count: regl.prop("count"),
primitive: 'points' primitive: "points"
}) });
} }
+145 -104
View File
@@ -1,29 +1,38 @@
import React from 'react'; // jshint esversion: 6
import React from "react";
import _ from "lodash"; import _ from "lodash";
import * as globals from "../../globals"; import * as globals from "../../globals";
import styles from "./graph.css"; import styles from "./graph.css";
import {setupGraphElements, drawGraphUsingRenderQueue} from "./drawGraph"; import { setupGraphElements, drawGraphUsingRenderQueue } from "./drawGraph";
import SectionHeader from "../framework/sectionHeader"; import SectionHeader from "../framework/sectionHeader";
import { connect } from "react-redux"; import { connect } from "react-redux";
import actions from "../../actions"; import actions from "../../actions";
import mat4 from 'gl-mat4'; import mat4 from "gl-mat4";
import fit from 'canvas-fit'; import fit from "canvas-fit";
import _camera from '../../util/camera.js' import _camera from "../../util/camera.js";
import _regl from 'regl' import _regl from "regl";
import _drawPoints from './drawPointsRegl' import _drawPoints from "./drawPointsRegl";
import FaCrosshair from 'react-icons/lib/fa/crosshairs'; import FaCrosshair from "react-icons/lib/fa/crosshairs";
import FaZoom from 'react-icons/lib/fa/search-plus'; import FaZoom from "react-icons/lib/fa/search-plus";
import FaSave from 'react-icons/lib/fa/download'; import FaSave from "react-icons/lib/fa/download";
/* https://bl.ocks.org/mbostock/9078690 - quadtree for onClick / hover selections */ /* https://bl.ocks.org/mbostock/9078690 - quadtree for onClick / hover selections */
@connect((state) => { @connect(state => {
const vertices =
const vertices = state.cells.cells && state.cells.cells.data.graph ? state.cells.cells.data.graph : null; state.cells.cells && state.cells.cells.data.graph
const ranges = state.cells.cells && state.cells.cells.data.ranges ? state.cells.cells.data.ranges : null; ? state.cells.cells.data.graph
const metadata = state.cells.cells && state.cells.cells.data.metadata ? state.cells.cells.data.metadata : null; : null;
const ranges =
state.cells.cells && state.cells.cells.data.ranges
? state.cells.cells.data.ranges
: null;
const metadata =
state.cells.cells && state.cells.cells.data.metadata
? state.cells.cells.data.metadata
: null;
return { return {
ranges, ranges,
@@ -35,11 +44,10 @@ import FaSave from 'react-icons/lib/fa/download';
graphMap: state.controls.graphMap, graphMap: state.controls.graphMap,
currentCellSelection: state.controls.currentCellSelection, currentCellSelection: state.controls.currentCellSelection,
graphBrushSelection: state.controls.graphBrushSelection, graphBrushSelection: state.controls.graphBrushSelection,
opacityForDeselectedCells: state.controls.opacityForDeselectedCells, opacityForDeselectedCells: state.controls.opacityForDeselectedCells
} };
}) })
class Graph extends React.Component { class Graph extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.count = 0; this.count = 0;
@@ -48,35 +56,32 @@ class Graph extends React.Component {
svg: null, svg: null,
ctx: null, ctx: null,
brush: null, brush: null,
mode: "brush", mode: "brush"
}; };
} }
componentDidMount() { componentDidMount() {
const { const { svg } = setupGraphElements(
svg
} = setupGraphElements(
this.handleBrushSelectAction.bind(this), this.handleBrushSelectAction.bind(this),
this.handleBrushDeselectAction.bind(this) this.handleBrushDeselectAction.bind(this)
); );
this.setState({svg}); this.setState({ svg });
// 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 });
const regl = _regl(this.reglCanvas) const regl = _regl(this.reglCanvas);
const drawPoints = _drawPoints(regl) const drawPoints = _drawPoints(regl);
// preallocate buffers // preallocate buffers
const pointBuffer = regl.buffer(); const pointBuffer = regl.buffer();
const colorBuffer = regl.buffer(); const colorBuffer = regl.buffer();
const sizeBuffer = regl.buffer(); const sizeBuffer = regl.buffer();
regl.frame(({viewportWidth, viewportHeight}) => { regl.frame(({ viewportWidth, viewportHeight }) => {
regl.clear({ regl.clear({
depth: 1, depth: 1,
color: [1, 1, 1, 1] color: [1, 1, 1, 1]
}) });
drawPoints({ drawPoints({
size: sizeBuffer, size: sizeBuffer,
@@ -86,18 +91,17 @@ class Graph extends React.Component {
count: this.count, count: this.count,
view: camera.view(), view: camera.view(),
scale: viewportHeight / viewportWidth scale: viewportHeight / viewportWidth
}) });
camera.tick() camera.tick();
}) });
this.setState({ this.setState({
regl, regl,
pointBuffer, pointBuffer,
colorBuffer, colorBuffer,
sizeBuffer sizeBuffer
}) });
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
/* maybe should do a check here to confirm ref exists and pass it? */ /* maybe should do a check here to confirm ref exists and pass it? */
@@ -120,12 +124,11 @@ class Graph extends React.Component {
// nextProps.opacityForDeselectedCells, // nextProps.opacityForDeselectedCells,
// ) // )
// } // }
if ( if (this.state.regl && nextProps.vertices) {
this.state.regl && const _currentCellSelectionMap = _.keyBy(
nextProps.vertices nextProps.currentCellSelection,
) { "CellName"
); /* move me to the reducer */
const _currentCellSelectionMap = _.keyBy(nextProps.currentCellSelection, "CellName"); /* move me to the reducer */
const positions = []; const positions = [];
positions.length = nextProps.currentCellSelection.length; positions.length = nextProps.currentCellSelection.length;
@@ -134,33 +137,38 @@ class Graph extends React.Component {
const sizes = []; const sizes = [];
sizes.length = nextProps.currentCellSelection.length; sizes.length = nextProps.currentCellSelection.length;
const glScaleX = d3.scaleLinear() const glScaleX = d3
.domain([0,1]) .scaleLinear()
.range([-1, 1]) /* padding */
const glScaleY = d3.scaleLinear()
.domain([0, 1]) .domain([0, 1])
.range([1, -1]) /* padding */ .range([-1, 1]); /* padding */
const glScaleY = d3
.scaleLinear()
.domain([0, 1])
.range([1, -1]); /* padding */
/* /*
Construct Vectors Construct Vectors
*/ */
_.each(nextProps.currentCellSelection, (cell, i) => { _.each(nextProps.currentCellSelection, (cell, i) => {
if (nextProps.graphMap[cell["CellName"]]) { /* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */ if (nextProps.graphMap[cell["CellName"]]) {
/* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */
positions[i] = [ positions[i] = [
glScaleX(nextProps.graphMap[cell["CellName"]][0]), glScaleX(nextProps.graphMap[cell["CellName"]][0]),
glScaleY(nextProps.graphMap[cell["CellName"]][1]) glScaleY(nextProps.graphMap[cell["CellName"]][1])
] ];
colors[i] = cell.__colorRGB__; colors[i] = cell.__colorRGB__;
sizes[i] = cell["__selected__"] ? 4 : .2 /* make this a function of the number of total cells, including regraph */ sizes[i] = cell["__selected__"]
? 4
: 0.2; /* make this a function of the number of total cells, including regraph */
} }
}) });
this.state.pointBuffer(positions) this.state.pointBuffer(positions);
this.state.colorBuffer(colors) this.state.colorBuffer(colors);
this.state.sizeBuffer(sizes) this.state.sizeBuffer(sizes);
this.count = positions.length this.count = positions.length;
} }
} }
handleBrushSelectAction() { handleBrushSelectAction() {
@@ -183,7 +191,7 @@ class Graph extends React.Component {
northwestY: s[0][1], northwestY: s[0][1],
southeastX: s[1][0], southeastX: s[1][0],
southeastY: s[1][1] southeastY: s[1][1]
} };
brushCoords.dx = brushCoords.southeastX - brushCoords.northwestX; brushCoords.dx = brushCoords.southeastX - brushCoords.northwestX;
brushCoords.dy = brushCoords.southeastY - brushCoords.northwestY; brushCoords.dy = brushCoords.southeastY - brushCoords.northwestY;
@@ -191,20 +199,20 @@ class Graph extends React.Component {
this.props.dispatch({ this.props.dispatch({
type: "graph brush selection change", type: "graph brush selection change",
brushCoords brushCoords
}) });
} }
handleBrushDeselectAction() { handleBrushDeselectAction() {
if (!d3.event.selection) { if (!d3.event.selection) {
this.props.dispatch({ this.props.dispatch({
type: "graph brush deselect" type: "graph brush deselect"
}) });
} }
} }
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",
data: e.target.value data: e.target.value
}) });
} }
render() { render() {
@@ -212,20 +220,24 @@ class Graph extends React.Component {
<div <div
id="graphWrapper" id="graphWrapper"
style={{ style={{
height: 1050, /* move this to globals */ height: 1050 /* move this to globals */,
backgroundColor: "white", backgroundColor: "white",
borderRadius: 3, borderRadius: 3,
boxShadow: "3px 4px 13px 0px rgba(201,201,201,1)", boxShadow: "3px 4px 13px 0px rgba(201,201,201,1)"
}}> }}
>
<div <div
style={{ style={{
padding: 10, padding: 10,
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "baseline" alignItems: "baseline"
}}> }}
>
<button <button
onClick={() => { this.props.dispatch(actions.regraph()) }} onClick={() => {
this.props.dispatch(actions.regraph());
}}
style={{ style={{
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 400,
@@ -233,67 +245,96 @@ class Graph extends React.Component {
padding: "10px 20px", padding: "10px 20px",
backgroundColor: globals.brightBlue, backgroundColor: globals.brightBlue,
border: "none", border: "none",
cursor: "pointer", cursor: "pointer"
}} }}
> >
Regraph present selection Regraph present selection
</button> </button>
<div> <div>
<span style={{ marginRight: 10, fontSize: 12}}> <span style={{ marginRight: 10, fontSize: 12 }}>
deselected opacity deselected opacity
</span> </span>
<input <input
style={{position: "relative", top: 6, marginRight: 20}} style={{ position: "relative", top: 6, marginRight: 20 }}
type="range" type="range"
onChange={this.handleOpacityRangeChange.bind(this)} onChange={this.handleOpacityRangeChange.bind(this)}
min={0} min={0}
max={1} max={1}
step="0.01" step="0.01"
/> />
<span style={{position: "relative", top: 3}}> <span style={{ position: "relative", top: 3 }}>
<button <button
onClick={() => { this.setState({mode: "brush"}) }} onClick={() => {
style={{ this.setState({ mode: "brush" });
cursor: "pointer", }}
border: this.state.mode === "brush" ? "1px solid black" : "1px solid white", style={{
backgroundColor: "white", cursor: "pointer",
padding: 5, border:
borderRadius: 3 this.state.mode === "brush"
}}> <FaCrosshair/> </button> ? "1px solid black"
<button : "1px solid white",
onClick={() => { this.setState({mode: "zoom"}) }} backgroundColor: "white",
style={{ padding: 5,
cursor: "pointer", borderRadius: 3
border: this.state.mode === "zoom" ? "1px solid black" : "1px solid white", }}
backgroundColor: "white", >
padding: 5, {" "}
borderRadius: 3 <FaCrosshair />{" "}
}}> <FaZoom/> </button> </button>
</span> <button
onClick={() => {
this.setState({ mode: "zoom" });
}}
style={{
cursor: "pointer",
border:
this.state.mode === "zoom"
? "1px solid black"
: "1px solid white",
backgroundColor: "white",
padding: 5,
borderRadius: 3
}}
>
{" "}
<FaZoom />{" "}
</button>
</span>
</div> </div>
<div> <div>
<button style={{ <button
fontSize: 12, style={{
fontWeight: 400, fontSize: 12,
color: "white", fontWeight: 400,
padding: "10px 20px", color: "white",
backgroundColor: globals.brightBlue, padding: "10px 20px",
border: "none", backgroundColor: globals.brightBlue,
cursor: "pointer", border: "none",
}}> <FaSave style={{display: "inline-block"}}/> csv url for present selection </button> cursor: "pointer"
}}
>
{" "}
<FaSave style={{ display: "inline-block" }} /> csv url for present
selection{" "}
</button>
</div> </div>
</div> </div>
<div <div
style={{display: this.state.mode === "brush" ? "inherit" : "none"}} style={{ display: this.state.mode === "brush" ? "inherit" : "none" }}
id="graphAttachPoint" id="graphAttachPoint"
> />
</div> <div style={{ padding: 0, margin: 0 }}>
<div style={{padding: 0, margin: 0}}> <canvas
<canvas width={globals.graphWidth} height={globals.graphHeight} ref={(canvas) => { this.reglCanvas = canvas}}/> width={globals.graphWidth}
height={globals.graphHeight}
ref={canvas => {
this.reglCanvas = canvas;
}}
/>
</div> </div>
</div> </div>
) );
} }
}; }
export default Graph; export default Graph;
+1
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
// createExpressionsCountsMap () { // createExpressionsCountsMap () {
// //
// const CHANGE_ME_MAGIC_GENE_INDEX = 5; // const CHANGE_ME_MAGIC_GENE_INDEX = 5;
+118 -71
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import styles from "./joy.css"; import styles from "./joy.css";
/* /*
@@ -5,112 +6,158 @@ import styles from "./joy.css";
*/ */
var margin = { top: 30, right: 10, bottom: 30, left: 100 }, var margin = { top: 30, right: 10, bottom: 30, left: 100 },
width = 400 - margin.left - margin.right, width = 400 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom; height = 600 - margin.top - margin.bottom;
// Percent two area charts can overlap // Percent two area charts can overlap
var overlap = 0.4; var overlap = 0.4;
var formatTime = d3.timeFormat('%I %p'); var formatTime = d3.timeFormat("%I %p");
var x = function(d) { return d.time; }, var x = function(d) {
xScale = d3.scaleTime().range([0, width]), return d.time;
xValue = function(d) { return xScale(x(d)); }, },
xAxis = d3.axisBottom(xScale).tickFormat(formatTime); 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; }, var y = function(d) {
yScale = d3.scaleLinear(), return d.value;
yValue = function(d) { return yScale(y(d)); }; },
yScale = d3.scaleLinear(),
yValue = function(d) {
return yScale(y(d));
};
var activity = function(d) { return d.key; }, var activity = function(d) {
activityScale = d3.scaleBand().range([0, height]), return d.key;
activityValue = function(d) { return activityScale(activity(d)); }, },
activityAxis = d3.axisLeft(activityScale); activityScale = d3.scaleBand().range([0, height]),
activityValue = function(d) {
return activityScale(activity(d));
},
activityAxis = d3.axisLeft(activityScale);
var area = d3.area() var area = d3
.x(xValue) .area()
.y1(yValue); .x(xValue)
.y1(yValue);
var line = area.lineY1(); var line = area.lineY1();
function parseTime(offset) { function parseTime(offset) {
var date = new Date(2017, 0, 1); // chose an arbitrary day var date = new Date(2017, 0, 1); // chose an arbitrary day
return d3.timeMinute.offset(date, offset); return d3.timeMinute.offset(date, offset);
} }
function row(d) { function row(d) {
return { return {
activity: d.activity, activity: d.activity,
time: parseTime(d.time), time: parseTime(d.time),
value: +d.p_smooth value: +d.p_smooth
}; };
} }
const drawJoy = (data) => { const drawJoy = data => {
console.log("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 + ")");
var svg = d3.select('#joyplot').append('svg') d3.tsv(
.attr('width', width + margin.left + margin.right) "https://gist.githubusercontent.com/armollica/3b5f83836c1de5cca7b1d35409a013e3/raw/783d1bbc2dd3aabbfcba83ece4a670de6f1ec371/data.tsv",
.attr('height', height + margin.top + margin.bottom) row,
.append('g') function(error, dataFlat) {
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); // Sort by time
dataFlat.sort(function(a, b) {
return a.time - b.time;
});
d3.tsv("https://gist.githubusercontent.com/armollica/3b5f83836c1de5cca7b1d35409a013e3/raw/783d1bbc2dd3aabbfcba83ece4a670de6f1ec371/data.tsv", row, function(error, dataFlat) { var data = d3
.nest()
// Sort by time .key(function(d) {
dataFlat.sort(function(a, b) { return a.time - b.time; }); return d.activity;
})
var data = d3.nest()
.key(function(d) { return d.activity; })
.entries(dataFlat); .entries(dataFlat);
// Sort activities by peak activity time // Sort activities by peak activity time
function peakTime(d) { function peakTime(d) {
var i = d3.scan(d.values, function(a, b) { return y(b) - y(a); }); var i = d3.scan(d.values, function(a, b) {
return y(b) - y(a);
});
return d.values[i].time; return d.values[i].time;
}; }
data.sort(function(a, b) { return peakTime(b) - peakTime(a); }); data.sort(function(a, b) {
return peakTime(b) - peakTime(a);
});
console.log('sorted', data) console.log("sorted", data);
xScale.domain(d3.extent(dataFlat, x)); xScale.domain(d3.extent(dataFlat, x));
activityScale.domain(data.map(function(d) { return d.key; })); activityScale.domain(
data.map(function(d) {
return d.key;
})
);
var areaChartHeight = (1 + overlap) * (height / activityScale.domain().length); var areaChartHeight =
(1 + overlap) * (height / activityScale.domain().length);
yScale yScale.domain(d3.extent(dataFlat, y)).range([areaChartHeight, 0]);
.domain(d3.extent(dataFlat, y))
.range([areaChartHeight, 0]);
area.y0(yScale(0)); area.y0(yScale(0));
var gActivity = svg.append('g').attr('class', "activities") var gActivity = svg
.selectAll('.activity').data(data) .append("g")
.enter().append('g') .attr("class", "activities")
.attr('class', function(d) { return `${styles.activity} ${styles.activity["--"+d.key]}`; }) .selectAll(".activity")
.attr('transform', function(d) { .data(data)
var ty = activityValue(d) - activityScale.bandwidth() + 5; .enter()
return 'translate(0,' + ty + ')'; .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) gActivity
.datum(function(d) { return d.values; }) .append("path")
.attr('d', area); .attr("class", styles.area)
.datum(function(d) {
return d.values;
})
.attr("d", area);
gActivity.append('path').attr('class', styles.line) gActivity
.datum(function(d) { return d.values; }) .append("path")
.attr('d', line); .attr("class", styles.line)
.datum(function(d) {
return d.values;
})
.attr("d", line);
svg.append('g').attr('class', `${styles.axis} ${styles["axis--x"]}`) svg
.attr('transform', 'translate(0,' + height + ')') .append("g")
.attr("class", `${styles.axis} ${styles["axis--x"]}`)
.attr("transform", "translate(0," + height + ")")
.call(xAxis); .call(xAxis);
svg.append('g').attr('class', `${styles.axis} ${styles["axis--activity"]}`) svg
.append("g")
.attr("class", `${styles.axis} ${styles["axis--activity"]}`)
.call(activityAxis); .call(activityAxis);
}
}) );
} };
export default drawJoy; export default drawJoy;
+13 -13
View File
@@ -1,38 +1,38 @@
import React from 'react'; // jshint esversion: 6
import React from "react";
import _ from "lodash"; import _ from "lodash";
import styles from "./joy.css"; import styles from "./joy.css";
import drawJoy from "./drawJoy"; import drawJoy from "./drawJoy";
import joyParser from "./joyParser"; import joyParser from "./joyParser";
class Joy extends React.Component { class Joy extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {};
};
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (nextProps.data) { if (nextProps.data) {
console.log('joyplot data 44', nextProps.data) console.log("joyplot data 44", nextProps.data);
drawJoy(joyParser(nextProps.data)); drawJoy(joyParser(nextProps.data));
} }
} }
componentDidMount() { componentDidMount() {}
}
render() { render() {
return ( return (
<div id="joyplot_wrapper" style={{marginTop: 50}}> <div id="joyplot_wrapper" style={{ marginTop: 50 }}>
<h3> Joy </h3> <h3> Joy </h3>
<p> Cell expression distribution per gene & if differential expression, Ie., cells for cluster 5, top genes expressed by cluster 8</p> <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 id="joyplot"> </div>
</div> </div>
) );
} }
}; }
export default Joy; export default Joy;
+11 -12
View File
@@ -1,6 +1,4 @@
// jshint esversion: 6
const joyParser = (data, count = 20) => { const joyParser = (data, count = 20) => {
const genes = []; const genes = [];
@@ -9,22 +7,23 @@ const joyParser = (data, count = 20) => {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const gene = { const gene = {
key: data.genes[i], /* key values naming: https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3 */ key:
data.genes[
i
] /* key values naming: https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3 */,
values: [] values: []
} };
data.cells.forEach((cell) => { data.cells.forEach(cell => {
gene.values.push({ gene.values.push({
value: cell["e"][i] value: cell["e"][i]
}) });
}) });
genes.push(gene) genes.push(gene);
} }
return genes; return genes;
} };
export default joyParser; export default joyParser;
+73 -36
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import React from "react"; import React from "react";
import _ from "lodash"; import _ from "lodash";
import Categorical from "./categorical/categorical"; import Categorical from "./categorical/categorical";
@@ -7,10 +8,8 @@ import { connect } from "react-redux";
import Heatmap from "./expression/diffExpHeatmap"; import Heatmap from "./expression/diffExpHeatmap";
import * as globals from "../globals"; import * as globals from "../globals";
@connect((state) => { @connect(state => {
return { return {};
}
}) })
class LeftSideBar extends React.Component { class LeftSideBar extends React.Component {
constructor(props) { constructor(props) {
@@ -21,24 +20,42 @@ class LeftSideBar extends React.Component {
} }
render() { render() {
return ( return (
<div style={{position: "fixed"}}> <div style={{ position: "fixed" }}>
<p style={{margin: 10, fontSize: 16, fontWeight: 700, width: "100%"}}>CELLxGENE {globals.datasetTitle} </p> <p style={{ margin: 10, fontSize: 16, fontWeight: 700, width: "100%" }}>
<div style={{padding: 10}}> CELLxGENE {globals.datasetTitle}{" "}
</p>
<div style={{ padding: 10 }}>
<button <button
style={{ style={{
padding: "10px 30px", padding: "10px 30px",
outline: 0, outline: 0,
fontSize: 18, fontSize: 18,
fontStyle: this.state.currentTab === "metadata" ? "inherit" : "italic", fontStyle:
this.state.currentTab === "metadata" ? "inherit" : "italic",
cursor: "pointer", cursor: "pointer",
border: "none", border: "none",
backgroundColor: "#FFF", backgroundColor: "#FFF",
borderTop: this.state.currentTab === "metadata" ? "4px solid " + globals.brightBlue : "none", borderTop:
borderBottom: this.state.currentTab === "metadata" ? "none" : "1px solid " + globals.lightGrey, this.state.currentTab === "metadata"
borderRight: this.state.currentTab === "metadata" ? "1px solid " + globals.lightGrey : "none", ? "4px solid " + globals.brightBlue
borderLeft: this.state.currentTab === "metadata" ? "1px solid " + globals.lightGrey : "none", : "none",
borderBottom:
this.state.currentTab === "metadata"
? "none"
: "1px solid " + globals.lightGrey,
borderRight:
this.state.currentTab === "metadata"
? "1px solid " + globals.lightGrey
: "none",
borderLeft:
this.state.currentTab === "metadata"
? "1px solid " + globals.lightGrey
: "none"
}} }}
onClick={() => {this.setState({currentTab: "metadata"})}}> onClick={() => {
this.setState({ currentTab: "metadata" });
}}
>
Metadata Metadata
</button> </button>
<button <button
@@ -46,39 +63,59 @@ class LeftSideBar extends React.Component {
padding: "10px 30px", padding: "10px 30px",
outline: 0, outline: 0,
fontSize: 18, fontSize: 18,
fontStyle: this.state.currentTab === "expression" ? "inherit" : "italic", fontStyle:
this.state.currentTab === "expression" ? "inherit" : "italic",
cursor: "pointer", cursor: "pointer",
border: "none", border: "none",
backgroundColor: "#FFF", backgroundColor: "#FFF",
borderTop: this.state.currentTab === "expression" ? "4px solid " + globals.brightBlue : "none", borderTop:
borderBottom: this.state.currentTab === "expression" ? "none" : "1px solid " + globals.lightGrey, this.state.currentTab === "expression"
borderRight: this.state.currentTab === "expression" ? "1px solid " + globals.lightGrey : "none", ? "4px solid " + globals.brightBlue
borderLeft: this.state.currentTab === "expression" ? "1px solid " + globals.lightGrey : "none", : "none",
borderBottom:
this.state.currentTab === "expression"
? "none"
: "1px solid " + globals.lightGrey,
borderRight:
this.state.currentTab === "expression"
? "1px solid " + globals.lightGrey
: "none",
borderLeft:
this.state.currentTab === "expression"
? "1px solid " + globals.lightGrey
: "none"
}} }}
onClick={() => {this.setState({currentTab: "expression"})}}> onClick={() => {
this.setState({ currentTab: "expression" });
}}
>
Expression Expression
</button> </button>
</div> </div>
<div style={{ <div
height: 500, style={{
width: 350, height: 500,
padding: 10, width: 350,
overflowY: "scroll", padding: 10,
overflowX: "hidden", overflowY: "scroll",
}}> overflowX: "hidden"
{this.state.currentTab === "metadata" ? <Categorical/> : null} }}
{this.state.currentTab === "metadata" ? <Continuous/> : null} >
{this.state.currentTab === "expression" ? <Heatmap/> : null} {this.state.currentTab === "metadata" ? <Categorical /> : null}
{this.state.currentTab === "metadata" ? <Continuous /> : null}
{this.state.currentTab === "expression" ? <Heatmap /> : null}
</div> </div>
<div style={{ <div
boxShadow: "-3px -4px 13px 0px rgba(201,201,201,1)", style={{
paddingTop: 10 boxShadow: "-3px -4px 13px 0px rgba(201,201,201,1)",
}}> paddingTop: 10
<ExpressionButtons/> }}
>
<ExpressionButtons />
</div> </div>
</div> </div>
) );
} }
}; }
export default LeftSideBar; export default LeftSideBar;
+13 -12
View File
@@ -1,8 +1,9 @@
const mat4 = require('gl-mat4') // jshint esversion: 6
const mat4 = require("gl-mat4");
// opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js // opacity: https://github.com/spacetx/starfish/blob/master/viz/draw/regions.js
export default function (regl) { export default function(regl) {
return regl({ return regl({
vert: ` vert: `
precision mediump float; precision mediump float;
@@ -29,14 +30,14 @@ export default function (regl) {
}`, }`,
attributes: { attributes: {
position: regl.prop('position'), position: regl.prop("position"),
color: regl.prop('color'), color: regl.prop("color"),
size: regl.prop('size') size: regl.prop("size")
}, },
uniforms: { uniforms: {
distance: regl.prop('distance'), distance: regl.prop("distance"),
view: regl.prop('view'), view: regl.prop("view"),
projection: (context, props) => { projection: (context, props) => {
return mat4.perspective( return mat4.perspective(
[], [],
@@ -44,12 +45,12 @@ export default function (regl) {
context.viewportWidth * props.scale / context.viewportHeight, context.viewportWidth * props.scale / context.viewportHeight,
0.01, 0.01,
1000 1000
) );
}, }
}, },
count: regl.prop('count'), count: regl.prop("count"),
primitive: 'points' primitive: "points"
}) });
} }
@@ -1,12 +1,8 @@
// jshint esversion: 6
import _ from "lodash"; import _ from "lodash";
import renderQueue from "../../util/renderQueue"; import renderQueue from "../../util/renderQueue";
import { import { margin, width, height, createDimensions } from "./util";
margin,
width,
height,
createDimensions,
} from "./util";
const drawScatterplotCanvas = ( const drawScatterplotCanvas = (
context, context,
@@ -17,38 +13,42 @@ const drawScatterplotCanvas = (
expression, expression,
scatterplotXXaccessor, scatterplotXXaccessor,
scatterplotYYaccessor, scatterplotYYaccessor,
_currentCellSelectionMap, _currentCellSelectionMap
) => { ) => {
return cell => {
return (cell) => { /*
/*
this is necessary until we are no longer getting expression for all cells, but only for 'world' this is necessary until we are no longer getting expression for all cells, but only for 'world'
...which will mean refetching when we regraph, or 'go back up to all cells' ...which will mean refetching when we regraph, or 'go back up to all cells'
*/ */
if (!_currentCellSelectionMap[cell.cellname]) { return } if (!_currentCellSelectionMap[cell.cellname]) {
return;
}
context.beginPath(); context.beginPath();
/* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */ /* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */
context.arc( context.arc(
xScale(cell.e[expression.data.genes.indexOf(scatterplotXXaccessor)]), /* x */ xScale(
yScale(cell.e[expression.data.genes.indexOf(scatterplotYYaccessor)]), /* y */ cell.e[expression.data.genes.indexOf(scatterplotXXaccessor)]
_currentCellSelectionMap[cell.cellname]["__selected__"] ? 3 : 1.5, /* r */ ) /* x */,
0, /* sAngle */ yScale(
2 * Math.PI /* eAngle */ cell.e[expression.data.genes.indexOf(scatterplotYYaccessor)]
); ) /* y */,
_currentCellSelectionMap[cell.cellname]["__selected__"] ? 3 : 1.5 /* r */,
0 /* sAngle */,
2 * Math.PI /* eAngle */
);
context.fillStyle = _currentCellSelectionMap[cell.cellname]["__color__"] context.fillStyle = _currentCellSelectionMap[cell.cellname]["__color__"];
if (_currentCellSelectionMap[cell.cellname]["__selected__"]) { if (_currentCellSelectionMap[cell.cellname]["__selected__"]) {
context.globalAlpha = 1; context.globalAlpha = 1;
} else { } else {
context.globalAlpha = opacityForDeselectedCells; context.globalAlpha = opacityForDeselectedCells;
} }
context.fill(); context.fill();
} };
} };
export const drawScatterplotCanvasUsingRenderQueue = ( export const drawScatterplotCanvasUsingRenderQueue = (
context, context,
@@ -63,7 +63,10 @@ export const drawScatterplotCanvasUsingRenderQueue = (
/* clear canvas */ /* clear canvas */
context.clearRect(0, 0, width, height); context.clearRect(0, 0, width, height);
const _currentCellSelectionMap = _.keyBy(currentCellSelection, "CellName"); /* move me to the reducer */ const _currentCellSelectionMap = _.keyBy(
currentCellSelection,
"CellName"
); /* move me to the reducer */
const _renderScatterplotWithFunctionReturnedByQueue = renderQueue( const _renderScatterplotWithFunctionReturnedByQueue = renderQueue(
drawScatterplotCanvas( drawScatterplotCanvas(
@@ -75,13 +78,12 @@ export const drawScatterplotCanvasUsingRenderQueue = (
expression, expression,
scatterplotXXaccessor, scatterplotXXaccessor,
scatterplotYYaccessor, scatterplotYYaccessor,
_currentCellSelectionMap, _currentCellSelectionMap
) )
) );
_renderScatterplotWithFunctionReturnedByQueue(expression.data.cells) _renderScatterplotWithFunctionReturnedByQueue(expression.data.cells);
return _renderScatterplotWithFunctionReturnedByQueue; return _renderScatterplotWithFunctionReturnedByQueue;
};
} export default _.debounce(drawScatterplotCanvasUsingRenderQueue, 100);
export default _.debounce(drawScatterplotCanvasUsingRenderQueue, 100)
+142 -107
View File
@@ -1,33 +1,37 @@
// jshint esversion: 6
// https://bl.ocks.org/Jverma/076377dd0125b1a508621441752735fc // https://bl.ocks.org/Jverma/076377dd0125b1a508621441752735fc
// https://peterbeshai.com/scatterplot-in-d3-with-voronoi-interaction.html // https://peterbeshai.com/scatterplot-in-d3-with-voronoi-interaction.html
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 scatterplot from "./scatterplot"; import scatterplot from "./scatterplot";
import setupScatterplot from "./setupScatterplot"; import setupScatterplot from "./setupScatterplot";
import styles from './scatterplot.css'; import styles from "./scatterplot.css";
import drawScatterplotCanvas from "./drawScatterplotCanvas"; import drawScatterplotCanvas from "./drawScatterplotCanvas";
import mat4 from 'gl-mat4'; import mat4 from "gl-mat4";
import fit from 'canvas-fit'; import fit from "canvas-fit";
import _camera from '../../util/camera.js' import _camera from "../../util/camera.js";
import _regl from 'regl' import _regl from "regl";
import _drawPoints from './drawPointsRegl' import _drawPoints from "./drawPointsRegl";
import { import { margin, width, height, createDimensions } from "./util";
margin,
width,
height,
createDimensions,
} from "./util";
@connect((state) => { @connect(state => {
const ranges =
const ranges = state.cells.cells && state.cells.cells.data.ranges ? state.cells.cells.data.ranges : null; state.cells.cells && state.cells.cells.data.ranges
const metadata = state.cells.cells && state.cells.cells.data.metadata ? state.cells.cells.data.metadata : null; ? state.cells.cells.data.ranges
const initializeRanges = state.initialize.data && state.initialize.data.data.ranges ? state.initialize.data.data.ranges : null; : null;
const metadata =
state.cells.cells && state.cells.cells.data.metadata
? state.cells.cells.data.metadata
: null;
const initializeRanges =
state.initialize.data && state.initialize.data.data.ranges
? state.initialize.data.data.ranges
: null;
return { return {
ranges, ranges,
@@ -40,8 +44,8 @@ import {
scatterplotYYaccessor: state.controls.scatterplotYYaccessor, scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
opacityForDeselectedCells: state.controls.opacityForDeselectedCells, opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
differential: state.differential, differential: state.differential,
expression: state.expression, expression: state.expression
} };
}) })
class Scatterplot extends React.Component { class Scatterplot extends React.Component {
constructor(props) { constructor(props) {
@@ -53,39 +57,31 @@ class Scatterplot extends React.Component {
axes: null, axes: null,
dimensions: null, dimensions: null,
xScale: null, xScale: null,
yScale: null, yScale: null
}; };
} }
componentDidMount() { componentDidMount() {
const { const { svg } = setupScatterplot(width, height, margin);
svg
} = setupScatterplot(
width,
height,
margin
);
this.setState({ this.setState({
svg svg
}) });
const camera = _camera(this.reglCanvas, {scale: true, rotate: false}); const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
const regl = _regl(this.reglCanvas) const regl = _regl(this.reglCanvas);
const drawPoints = _drawPoints(regl) const drawPoints = _drawPoints(regl);
// preallocate buffers // preallocate buffers
const pointBuffer = regl.buffer() const pointBuffer = regl.buffer();
const colorBuffer = regl.buffer() const colorBuffer = regl.buffer();
const sizeBuffer = regl.buffer() const sizeBuffer = regl.buffer();
regl.frame(({viewportWidth, viewportHeight}) => {
regl.frame(({ viewportWidth, viewportHeight }) => {
regl.clear({ regl.clear({
depth: 1, depth: 1,
color: [1, 1, 1, 1] color: [1, 1, 1, 1]
}) });
drawPoints({ drawPoints({
distance: camera.distance, distance: camera.distance,
@@ -95,29 +91,28 @@ class Scatterplot extends React.Component {
count: this.count, count: this.count,
view: camera.view(), view: camera.view(),
scale: viewportHeight / viewportWidth scale: viewportHeight / viewportWidth
}) });
camera.tick() camera.tick();
}) });
this.setState({ this.setState({
regl, regl,
sizeBuffer, sizeBuffer,
pointBuffer, pointBuffer,
colorBuffer, colorBuffer
}) });
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.maybeSetupScalesAndDrawAxes(nextProps); this.maybeSetupScalesAndDrawAxes(nextProps);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if ( if (
(this.state.xScale && this.state.yScale) && (this.state.xScale &&
(this.props.scatterplotXXaccessor && this.props.scatterplotYYaccessor) && this.state.yScale &&
this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc (this.props.scatterplotXXaccessor &&
this.props.scatterplotYYaccessor) &&
this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor) || // was CLU now FTH1 etc
this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor
) { ) {
this.drawAxesSVG(this.state.xScale, this.state.yScale); this.drawAxesSVG(this.state.xScale, this.state.yScale);
@@ -136,20 +131,24 @@ class Scatterplot extends React.Component {
this.state.xScale && this.state.xScale &&
this.state.yScale this.state.yScale
) { ) {
const _currentCellSelectionMap = _.keyBy(this.props.currentCellSelection, "CellName"); /* move me to the reducer */ const _currentCellSelectionMap = _.keyBy(
this.props.currentCellSelection,
"CellName"
); /* move me to the reducer */
const positions = []; const positions = [];
const colors = []; const colors = [];
const sizes = []; const sizes = [];
const glScaleX = d3.scaleLinear() const glScaleX = d3
.scaleLinear()
.domain([0, width]) .domain([0, width])
.range([-.95, .95]) /* padding */ .range([-0.95, 0.95]); /* padding */
const glScaleY = d3.scaleLinear() const glScaleY = d3
.scaleLinear()
.domain([0, height]) .domain([0, height])
.range([-1, 1]) .range([-1, 1]);
/* /*
Construct Vectors Construct Vectors
@@ -159,21 +158,40 @@ class Scatterplot extends React.Component {
this if is necessary until we are no longer getting expression for all cells, but only for 'world' this if is necessary until we are no longer getting expression for all cells, but only for 'world'
...which will mean refetching when we regraph, or 'go back up to all cells' ...which will mean refetching when we regraph, or 'go back up to all cells'
*/ */
if (_currentCellSelectionMap[cell.cellname]) { /* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */ if (_currentCellSelectionMap[cell.cellname]) {
/* fails silently, sometimes this is undefined, in which case the graph array should be shorter than the cell array, check in reducer */
positions.push([ positions.push([
glScaleX(this.state.xScale(cell.e[this.props.expression.data.genes.indexOf(this.props.scatterplotXXaccessor)])), /* scale each point first to the window as we calculate extents separately below, so no need to repeat */ glScaleX(
glScaleY(this.state.yScale(cell.e[this.props.expression.data.genes.indexOf(this.props.scatterplotYYaccessor)])) this.state.xScale(
]) cell.e[
this.props.expression.data.genes.indexOf(
this.props.scatterplotXXaccessor
)
]
)
) /* scale each point first to the window as we calculate extents separately below, so no need to repeat */,
glScaleY(
this.state.yScale(
cell.e[
this.props.expression.data.genes.indexOf(
this.props.scatterplotYYaccessor
)
]
)
)
]);
colors.push(_currentCellSelectionMap[cell.cellname]["__colorRGB__"]) colors.push(_currentCellSelectionMap[cell.cellname]["__colorRGB__"]);
sizes.push(_currentCellSelectionMap[cell.cellname]["__selected__"] ? 4 : .2) /* make this a function of the number of total cells, including regraph */ sizes.push(
_currentCellSelectionMap[cell.cellname]["__selected__"] ? 4 : 0.2
); /* make this a function of the number of total cells, including regraph */
} }
}) });
this.state.pointBuffer(positions) this.state.pointBuffer(positions);
this.state.colorBuffer(colors) this.state.colorBuffer(colors);
this.state.sizeBuffer(sizes) this.state.sizeBuffer(sizes);
this.count = positions.length this.count = positions.length;
} }
} }
maybeSetupScalesAndDrawAxes(nextProps) { maybeSetupScalesAndDrawAxes(nextProps) {
@@ -183,61 +201,75 @@ class Scatterplot extends React.Component {
nextProps.scatterplotXXaccessor && nextProps.scatterplotXXaccessor &&
nextProps.scatterplotYYaccessor nextProps.scatterplotYYaccessor
) { ) {
const xScale = d3.scaleLinear() const xScale = d3
.domain(d3.extent(nextProps.expression.data.cells, (cell, i) => { .scaleLinear()
return cell.e[nextProps.expression.data.genes.indexOf(nextProps.scatterplotXXaccessor)] .domain(
})) d3.extent(nextProps.expression.data.cells, (cell, i) => {
.range([0, width]) return cell.e[
nextProps.expression.data.genes.indexOf(
nextProps.scatterplotXXaccessor
)
];
})
)
.range([0, width]);
const yScale = d3.scaleLinear() const yScale = d3
.domain(d3.extent(nextProps.expression.data.cells, (cell) => { .scaleLinear()
return cell.e[nextProps.expression.data.genes.indexOf(nextProps.scatterplotYYaccessor)] .domain(
})) d3.extent(nextProps.expression.data.cells, cell => {
.range([height, 0]) return cell.e[
nextProps.expression.data.genes.indexOf(
nextProps.scatterplotYYaccessor
)
];
})
)
.range([height, 0]);
this.setState({ this.setState({
xScale, xScale,
yScale yScale
}) });
} }
} }
drawAxesSVG(xScale, yScale) { drawAxesSVG(xScale, yScale) {
this.state.svg.selectAll("*").remove(); this.state.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 the axis, just call axisBottom, axisLeft etc.
var xAxis = d3.axisBottom() var xAxis = d3.axisBottom().scale(xScale);
.scale(xScale);
var yAxis = d3.axisLeft() var yAxis = d3.axisLeft().scale(yScale);
.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.
this.state.svg.append('g') this.state.svg
.attr('transform', 'translate(0,' + height + ')') .append("g")
.attr('class', 'x axis') .attr("transform", "translate(0," + height + ")")
.attr("class", "x axis")
.call(xAxis); .call(xAxis);
// y-axis is translated to (0,0) // y-axis is translated to (0,0)
this.state.svg.append('g') this.state.svg
.attr('transform', 'translate(0,0)') .append("g")
.attr('class', 'y axis') .attr("transform", "translate(0,0)")
.attr("class", "y axis")
.call(yAxis); .call(yAxis);
// adding label. For x-axis, it's at (10, 10), and for y-axis at (width, height-10). // adding label. For x-axis, it's at (10, 10), and for y-axis at (width, height-10).
this.state.svg.append('text') this.state.svg
.attr('x', 10) .append("text")
.attr('y', 10) .attr("x", 10)
.attr('class', 'label') .attr("y", 10)
.attr("class", "label")
.text(this.props.scatterplotYYaccessor); .text(this.props.scatterplotYYaccessor);
this.state.svg.append('text') this.state.svg
.attr('x', width) .append("text")
.attr('y', height - 10) .attr("x", width)
.attr('text-anchor', 'end') .attr("y", height - 10)
.attr('class', 'label') .attr("text-anchor", "end")
.attr("class", "label")
.text(this.props.scatterplotXXaccessor); .text(this.props.scatterplotXXaccessor);
} }
render() { render() {
@@ -248,15 +280,16 @@ class Scatterplot extends React.Component {
borderRadius: 3, borderRadius: 3,
marginTop: 15, marginTop: 15,
paddingBottom: 20, paddingBottom: 20,
boxShadow: "3px 4px 13px 0px rgba(201,201,201,1)", boxShadow: "3px 4px 13px 0px rgba(201,201,201,1)"
}} }}
id="scatterplot_wrapper"> id="scatterplot_wrapper"
>
<div <div
className={styles.scatterplot} className={styles.scatterplot}
id="scatterplot" id="scatterplot"
style={{ style={{
width: width + margin.left + margin.right + "px", width: width + margin.left + margin.right + "px",
height: height + margin.top + margin.bottom + "px", height: height + margin.top + margin.bottom + "px"
}} }}
> >
<canvas <canvas
@@ -266,14 +299,16 @@ class Scatterplot extends React.Component {
marginLeft: margin.left - 7, marginLeft: margin.left - 7,
marginTop: margin.top marginTop: margin.top
}} }}
ref={(canvas) => { this.reglCanvas = canvas}}/> ref={canvas => {
this.reglCanvas = canvas;
}}
/>
</div> </div>
</div> </div>
) );
} }
}; }
export default Scatterplot; export default Scatterplot;
// <SectionHeader text="Continuous Metadata"/> // <SectionHeader text="Continuous Metadata"/>
+11 -16
View File
@@ -1,28 +1,23 @@
// jshint esversion: 6
/***************************************** /*****************************************
****************************************** ******************************************
Setup SVG & Canvas elements Setup SVG & Canvas elements
****************************************** ******************************************
******************************************/ ******************************************/
const setupScatterplot = ( const setupScatterplot = (width, height, margin) => {
width, var container = d3.select("#scatterplot");
height,
margin
) => {
var container = d3.select("#scatterplot") var svg = container
.append("svg")
var svg = container.append("svg") .attr("width", width + margin.left + margin.right)
.attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom)
.attr("height", height + margin.top + margin.bottom)
.append("g") .append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
return { return {
svg, svg
} };
};
}
export default setupScatterplot; export default setupScatterplot;
+2 -1
View File
@@ -1,9 +1,10 @@
// jshint esversion: 6
import _ from "lodash"; import _ from "lodash";
const paddingRight = 120; const paddingRight = 120;
const continuousChartWidth = 340; const continuousChartWidth = 340;
export const margin = {top: 66, right: 110, bottom: 20, left: 60}; export const margin = { top: 66, right: 110, bottom: 20, left: 60 };
export const width = 340; export const width = 340;
export const height = 340 - margin.top - margin.bottom; export const height = 340 - margin.top - margin.bottom;
export const innerHeight = height - 2; export const innerHeight = height - 2;
+138 -36
View File
@@ -1,5 +1,13 @@
// jshint esversion: 6
/* these will be either (preferably) specified or inferred */ /* these will be either (preferably) specified or inferred */
export const categories = ["Sample.type", "Selection", "Location", "Sample.name", "Class", "Neoplastic"]; export const categories = [
"Sample.type",
"Selection",
"Location",
"Sample.name",
"Class",
"Neoplastic"
];
export const continuous = [ export const continuous = [
"Total_reads", "Total_reads",
"Unique_reads", "Unique_reads",
@@ -18,7 +26,7 @@ export const continuous = [
"Unmapped_mismatch", "Unmapped_mismatch",
"Unmapped_other", "Unmapped_other",
"Unmapped_short" "Unmapped_short"
] ];
/* colors */ /* colors */
export const blue = "#4a90e2"; export const blue = "#4a90e2";
@@ -44,55 +52,149 @@ export let API = {
// prefix: "http://pbmc33k.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/"
} };
if (window.CELLXGENE && window.CELLXGENE.API) API = window.CELLXGENE.API; if (window.CELLXGENE && window.CELLXGENE.API) API = window.CELLXGENE.API;
export let datasetTitle = ""; export let datasetTitle = "";
if (window.CELLXGENE && window.CELLXGENE.datasetTitle) datasetTitle = window.CELLXGENE.datasetTitle; if (window.CELLXGENE && window.CELLXGENE.datasetTitle)
datasetTitle = window.CELLXGENE.datasetTitle;
export const accentFont = "Georgia,Times,Times New Roman,serif"; export const accentFont = "Georgia,Times,Times New Roman,serif";
export const maxParagraphWidth = 600; export const maxParagraphWidth = 600;
export const maxControlsWidth = 800; export const maxControlsWidth = 800;
export const graphMargin = {top: 20, right: 10, bottom: 30, left: 40}; export const graphMargin = { top: 20, right: 10, bottom: 30, left: 40 };
// export const graphWidth = 1440 /* window width */ - 410 /* sidebar */ - (15 + 15) /* left right padding */ /* but responsive */; // export const graphWidth = 1440 /* window width */ - 410 /* sidebar */ - (15 + 15) /* left right padding */ /* but responsive */;
// export const graphHeight = 500; // export const graphHeight = 500;
export const graphWidth = 960; export const graphWidth = 960;
export const graphHeight = 960; export const graphHeight = 960;
export const graphXScale = d3.scaleLinear() export const graphXScale = d3
.domain([0, 1]) /* while this is the default for d3, our data is normalized so better to be explicit */ .scaleLinear()
.range([ .domain([
0 + graphMargin.left, 0,
graphWidth - graphMargin.right 1
]); ]) /* while this is the default for d3, our data is normalized so better to be explicit */
.range([0 + graphMargin.left, graphWidth - graphMargin.right]);
export const graphYScale = d3.scaleLinear()
.domain([0, 1]) /* while this is the default for d3, our data is normalized so better to be explicit */
.range([
graphHeight - graphMargin.bottom,
0 + graphMargin.top
]);
export const graphYScale = d3
.scaleLinear()
.domain([
0,
1
]) /* while this is the default for d3, our data is normalized so better to be explicit */
.range([graphHeight - graphMargin.bottom, 0 + graphMargin.top]);
export const ordinalColors = [ export const ordinalColors = [
'#0ac115', '#c10ab6', '#c1710a', '#0a5ac1', '#c1150a', '#0ab6c1', '#5ac10a', "#0ac115",
'#710ac1', '#0ac171', '#c10a5a', '#b6c10a', '#150ac1', '#b2ffb7', '#ffb2fa', "#c10ab6",
'#ffddb2', '#b2d4ff', '#ffb7b2', '#b2faff', '#d4ffb2', '#ddb2ff', '#b2ffdd', "#c1710a",
'#ffb2d4', '#faffb2', '#b7b2ff', '#27a908', '#8b08a9', '#a93a08', '#0877a9', "#0a5ac1",
'#a90827', '#08a98b', '#77a908', '#3a08a9', '#08a93a', '#a90877', '#a98b08', "#c1150a",
'#0827a9', '#00ff0f', '#ff00ef', '#ff8e00', '#0070ff', '#ff0f00', '#00efff', "#0ab6c1",
'#70ff00', '#8e00ff', '#00ff8e', '#ff0070', '#efff00', '#0f00ff', '#006606', "#5ac10a",
'#66005f', '#663900', '#002c66', '#660600', '#005f66', '#2c6600', '#390066', "#710ac1",
'#006639', '#66002c', '#5f6600', '#060066', '#83ff65', '#e165ff', '#ff9565', "#0ac171",
'#65cfff', '#ff6583', '#65ffe1', '#cfff65', '#9565ff', '#65ff95', '#ff65cf', "#c10a5a",
'#ffe165', '#6583ff', '#009909', '#99008f', '#995500', '#004399', '#990900', "#b6c10a",
'#008f99', '#439900', '#550099', '#009955', '#990043', '#8f9900', '#090099', "#150ac1",
'#d9fecc', '#f1ccfe', '#fed7cc', '#ccf3fe', '#feccd9', '#ccfef1', '#f3fecc', "#b2ffb7",
'#d7ccfe', '#ccfed7', '#feccf3', '#fef1cc', '#ccd9fe', '#47ea51', '#ea47e0', "#ffb2fa",
'#eaa247', '#478fea', '#ea5147', '#47e0ea', '#8fea47', '#a247ea', '#47eaa2', "#ffddb2",
'#ea478f', '#e0ea47', '#5147ea' "#b2d4ff",
] "#ffb7b2",
"#b2faff",
"#d4ffb2",
"#ddb2ff",
"#b2ffdd",
"#ffb2d4",
"#faffb2",
"#b7b2ff",
"#27a908",
"#8b08a9",
"#a93a08",
"#0877a9",
"#a90827",
"#08a98b",
"#77a908",
"#3a08a9",
"#08a93a",
"#a90877",
"#a98b08",
"#0827a9",
"#00ff0f",
"#ff00ef",
"#ff8e00",
"#0070ff",
"#ff0f00",
"#00efff",
"#70ff00",
"#8e00ff",
"#00ff8e",
"#ff0070",
"#efff00",
"#0f00ff",
"#006606",
"#66005f",
"#663900",
"#002c66",
"#660600",
"#005f66",
"#2c6600",
"#390066",
"#006639",
"#66002c",
"#5f6600",
"#060066",
"#83ff65",
"#e165ff",
"#ff9565",
"#65cfff",
"#ff6583",
"#65ffe1",
"#cfff65",
"#9565ff",
"#65ff95",
"#ff65cf",
"#ffe165",
"#6583ff",
"#009909",
"#99008f",
"#995500",
"#004399",
"#990900",
"#008f99",
"#439900",
"#550099",
"#009955",
"#990043",
"#8f9900",
"#090099",
"#d9fecc",
"#f1ccfe",
"#fed7cc",
"#ccf3fe",
"#feccd9",
"#ccfef1",
"#f3fecc",
"#d7ccfe",
"#ccfed7",
"#feccf3",
"#fef1cc",
"#ccd9fe",
"#47ea51",
"#ea47e0",
"#eaa247",
"#478fea",
"#ea5147",
"#47e0ea",
"#8fea47",
"#a247ea",
"#47eaa2",
"#ea478f",
"#e0ea47",
"#5147ea"
];
+11 -10
View File
@@ -1,9 +1,10 @@
// jshint esversion: 6
/* eslint-disable no-console */ /* eslint-disable no-console */
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom'; import ReactDOM from "react-dom";
import { AppContainer } from 'react-hot-loader'; import { AppContainer } from "react-hot-loader";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import Redbox from 'redbox-react'; import Redbox from "redbox-react";
/* our code */ /* our code */
import App from "./components/app"; import App from "./components/app";
@@ -12,23 +13,23 @@ import store from "./reducers";
ReactDOM.render( ReactDOM.render(
<AppContainer errorReporter={Redbox}> <AppContainer errorReporter={Redbox}>
<Provider store={store}> <Provider store={store}>
<App/> <App />
</Provider> </Provider>
</AppContainer>, </AppContainer>,
document.getElementById('root') document.getElementById("root")
); );
// Hot Module Replacement API // Hot Module Replacement API
if (module.hot) { if (module.hot) {
module.hot.accept('./components/app', () => { module.hot.accept("./components/app", () => {
const NextApp = require('./components/app').default; const NextApp = require("./components/app").default;
ReactDOM.render( ReactDOM.render(
<AppContainer> <AppContainer>
<Provider store={store}> <Provider store={store}>
<NextApp/> <NextApp />
</Provider> </Provider>
</AppContainer>, </AppContainer>,
document.getElementById('root') document.getElementById("root")
); );
}); });
} }
+50 -63
View File
@@ -1,9 +1,8 @@
// jshint esversion: 6
import uri from "urijs"; import uri from "urijs";
import * as globals from "../globals"; import * as globals from "../globals";
import _ from "lodash"; import _ from "lodash";
import { import { parseRGB } from "../util/parseRGB";
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
@@ -21,27 +20,26 @@ import {
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 updateCellSelectionMiddleware = store => {
const updateCellSelectionMiddleware = (store) => { return next => {
return (next) => { return action => {
return (action) => {
const s = store.getState(); 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 */ /* 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 = const filterJustChanged =
action.type === "color by expression" || action.type === "color by expression" ||
action.type === "color by continuous metadata" || action.type === "color by continuous metadata" ||
action.type === "color by categorical metadata" action.type === "color by categorical metadata";
;
if ( if (!filterJustChanged || !s.controls.allCellsOnClient) {
!filterJustChanged || return next(
!s.controls.allCellsOnClient action
) { ); /* if the cells haven't loaded or the action wasn't a color change, bail */
return next(action); /* if the cells haven't loaded or the action wasn't a color change, bail */
} }
let currentSelectionWithUpdatedColors = s.controls.currentCellSelection.slice(0); let currentSelectionWithUpdatedColors = s.controls.currentCellSelection.slice(
0
);
let colorScale; let colorScale;
/* /*
@@ -54,45 +52,32 @@ const updateCellSelectionMiddleware = (store) => {
*/ */
if (action.type === "color by categorical metadata") { if (action.type === "color by categorical metadata") {
colorScale = d3.scaleOrdinal().range(globals.ordinalColors); colorScale = d3.scaleOrdinal().range(globals.ordinalColors);
_.each(currentSelectionWithUpdatedColors, (cell, i) => { _.each(currentSelectionWithUpdatedColors, (cell, i) => {
let c = colorScale( let c = colorScale(cell[action.colorAccessor]);
cell[action.colorAccessor]
);
currentSelectionWithUpdatedColors[i]["__color__"] = c; currentSelectionWithUpdatedColors[i]["__color__"] = c;
currentSelectionWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c); currentSelectionWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c);
}) });
} }
if (action.type === "color by continuous metadata") { if (action.type === "color by continuous metadata") {
colorScale = d3
colorScale = d3.scaleLinear() .scaleLinear()
.domain([0, action.rangeMaxForColorAccessor]) .domain([0, action.rangeMaxForColorAccessor])
.range([1,0]) .range([1, 0]);
_.each(currentSelectionWithUpdatedColors, (cell, i) => { _.each(currentSelectionWithUpdatedColors, (cell, i) => {
let c = d3.interpolateViridis( let c = d3.interpolateViridis(colorScale(cell[action.colorAccessor]));
colorScale(
cell[action.colorAccessor]
)
);
currentSelectionWithUpdatedColors[i]["__color__"] = c; currentSelectionWithUpdatedColors[i]["__color__"] = c;
currentSelectionWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c); currentSelectionWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c);
}) });
} }
if (action.type === "color by expression") { if (action.type === "color by expression") {
const indexOfGene = 0; /* we only get one, this comes from server as needed now */
const expressionMap = {};
const indexOfGene = 0 /* we only get one, this comes from server as needed now */
const expressionMap = {}
/* /*
converts [{cellname: cell123, e}, {}] converts [{cellname: cell123, e}, {}]
@@ -101,50 +86,52 @@ const updateCellSelectionMiddleware = (store) => {
cell789: [0, 8] cell789: [0, 8]
} }
*/ */
_.each(action.data.data.cells, (cell) => { /* this action is coming directly from the server */ _.each(action.data.data.cells, cell => {
expressionMap[cell.cellname] = cell.e /* this action is coming directly from the server */
}) expressionMap[cell.cellname] = cell.e;
});
const minExpressionCell = _.minBy(action.data.data.cells, (cell) => { const minExpressionCell = _.minBy(action.data.data.cells, cell => {
return cell.e[indexOfGene] return cell.e[indexOfGene];
}) });
const maxExpressionCell = _.maxBy(action.data.data.cells, (cell) => { const maxExpressionCell = _.maxBy(action.data.data.cells, cell => {
return cell.e[indexOfGene] return cell.e[indexOfGene];
}) });
// console.log('middle', action, expressionMap, minExpressionCell) // console.log('middle', action, expressionMap, minExpressionCell)
colorScale = d3.scaleLinear() colorScale = d3
.domain([minExpressionCell.e[indexOfGene], maxExpressionCell.e[indexOfGene]]) .scaleLinear()
.range([1,0]) /* invert viridis... probably pass this scale through to others */ .domain([
minExpressionCell.e[indexOfGene],
maxExpressionCell.e[indexOfGene]
])
.range([
1,
0
]); /* invert viridis... probably pass this scale through to others */
_.each(currentSelectionWithUpdatedColors, (cell, i) => { _.each(currentSelectionWithUpdatedColors, (cell, i) => {
let c = d3.interpolateViridis( let c = d3.interpolateViridis(
colorScale( colorScale(expressionMap[cell.CellName][indexOfGene])
expressionMap[cell.CellName][indexOfGene]
)
); );
currentSelectionWithUpdatedColors[i]["__color__"] = c; currentSelectionWithUpdatedColors[i]["__color__"] = c;
currentSelectionWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c); currentSelectionWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c);
}) });
} }
/* /*
append the result of all the filters to the action the user just triggered append the result of all the filters to the action the user just triggered
*/ */
let modifiedAction = Object.assign( let modifiedAction = Object.assign({}, action, {
{}, currentSelectionWithUpdatedColors,
action, colorScale
{ });
currentSelectionWithUpdatedColors,
colorScale
}
)
return next(modifiedAction); return next(modifiedAction);
} };
} };
}; };
export default updateCellSelectionMiddleware; export default updateCellSelectionMiddleware;
+77 -60
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import uri from "urijs"; import uri from "urijs";
import * as globals from "../globals"; import * as globals from "../globals";
@@ -17,9 +18,9 @@ import * as globals from "../globals";
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 updateCellSelectionMiddleware = (store) => { const updateCellSelectionMiddleware = store => {
return (next) => { return next => {
return (action) => { return action => {
const s = store.getState(); 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 */ /* this is a hardcoded map of the things we need to keep an eye on and update global cell selection in response to */
@@ -31,24 +32,26 @@ const updateCellSelectionMiddleware = (store) => {
action.type === "categorical metadata filter deselect" || action.type === "categorical metadata filter deselect" ||
action.type === "categorical metadata filter select" || action.type === "categorical metadata filter select" ||
action.type === "categorical metadata filter none of these" || action.type === "categorical metadata filter none of these" ||
action.type === "categorical metadata filter all of these" action.type === "categorical metadata filter all of these";
;
if ( if (
!filterJustChanged || !filterJustChanged ||
!s.controls.allCellsOnClient !s.controls.allCellsOnClient
/* graphMap is set at the same time as allCells, so we assume it exists */ /* graphMap is set at the same time as allCells, so we assume it exists */
) { ) {
return next(action); /* if the cells haven't loaded or the action wasn't a filter, bail */ 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 - make a FRESH copy of all of the cells
- metadata has cellname, and that's all we ever need (is a key to graphMap) - metadata has cellname, and that's all we ever need (is a key to graphMap)
*/ */
let newSelection = s.controls.currentCellSelection.slice(0); let newSelection = s.controls.currentCellSelection.slice(0);
_.each(newSelection, (cell) => { cell["__selected__"] = true } ); _.each(newSelection, cell => {
cell["__selected__"] = true;
});
/* /*
in plain language... in plain language...
@@ -61,57 +64,59 @@ const updateCellSelectionMiddleware = (store) => {
*/ */
if ( /* is there a 2d graph brush selection ? */ if (
/* is there a 2d graph brush selection ? */
action.type === "graph brush selection change" || action.type === "graph brush selection change" ||
s.controls.graphBrushSelection s.controls.graphBrushSelection
) { ) {
const graphBrushSelection /* it exists, so is it new or old */ =
const graphBrushSelection = /* it exists, so is it new or old */ action.type === "graph brush selection change"
action.type === "graph brush selection change" ? action.brushCoords : ? action.brushCoords
s.controls.graphBrushSelection : s.controls.graphBrushSelection;
_.each(newSelection, (cell, i) => { _.each(newSelection, (cell, i) => {
if (!s.controls.graphMap[cell["CellName"]]) { if (!s.controls.graphMap[cell["CellName"]]) {
newSelection[i]["__selected__"] = false; /* make a toggle in future */ newSelection[i][
return "__selected__"
] = false; /* make a toggle in future */
return;
} }
const coords = s.controls.graphMap[cell["CellName"]]; // [0.08005009151334168, 0.6907652173913044] const coords = s.controls.graphMap[cell["CellName"]]; // [0.08005009151334168, 0.6907652173913044]
const pointIsInsideBrushBounds = ( const pointIsInsideBrushBounds =
globals.graphXScale(coords[0]) >= graphBrushSelection.northwestX && globals.graphXScale(coords[0]) >= graphBrushSelection.northwestX &&
globals.graphXScale(coords[0]) <= graphBrushSelection.southeastX && globals.graphXScale(coords[0]) <= graphBrushSelection.southeastX &&
globals.graphYScale(coords[1]) >= graphBrushSelection.northwestY && globals.graphYScale(coords[1]) >= graphBrushSelection.northwestY &&
globals.graphYScale(coords[1]) <= graphBrushSelection.southeastY globals.graphYScale(coords[1]) <= graphBrushSelection.southeastY;
);
if (!pointIsInsideBrushBounds) { if (!pointIsInsideBrushBounds) {
newSelection[i]["__selected__"] = false; newSelection[i]["__selected__"] = false;
} }
});
})
} }
if ( if (
action.type === "continuous selection using parallel coords brushing" && s.controls.continuousSelection || (action.type ===
"continuous selection using parallel coords brushing" &&
s.controls.continuousSelection) ||
s.controls.continuousSelection s.controls.continuousSelection
) { ) {
_.each(newSelection, (cell, i) => { _.each(newSelection, (cell, i) => {
const cellExtentsAreWithinContinuousSelectionBounds = s.controls.continuousSelection.every(
const cellExtentsAreWithinContinuousSelectionBounds = s.controls.continuousSelection.every((active) => { active => {
// test if point is within extents for each active brush // test if point is within extents for each active brush
return active.dimension.type.within( return active.dimension.type.within(
cell[active.dimension.key], cell[active.dimension.key],
active.extent, active.extent,
active.dimension active.dimension
); );
})
if (!cellExtentsAreWithinContinuousSelectionBounds) {
newSelection[i]["__selected__"] = false;
} }
}) );
if (!cellExtentsAreWithinContinuousSelectionBounds) {
newSelection[i]["__selected__"] = false;
}
});
} }
/* /*
@@ -121,8 +126,8 @@ const updateCellSelectionMiddleware = (store) => {
Filter based on them Filter based on them
*/ */
let newContinuousUserDefinedRanges = s.controls.continuousUserDefinedRanges; let newContinuousUserDefinedRanges =
s.controls.continuousUserDefinedRanges;
/* check if this is the action and take care of that metadata field */ /* check if this is the action and take care of that metadata field */
if (action.type === "continuous metadata histogram brush") { if (action.type === "continuous metadata histogram brush") {
@@ -139,13 +144,13 @@ const updateCellSelectionMiddleware = (store) => {
_.each(newContinuousUserDefinedRanges, (value, key, i) => { _.each(newContinuousUserDefinedRanges, (value, key, i) => {
if (value !== null) { if (value !== null) {
activeContinuousHistogramFilters.push(key) activeContinuousHistogramFilters.push(key);
} }
}) });
/* see if there are others from previous... */ /* see if there are others from previous... */
if (activeContinuousHistogramFilters.length > 0) { if (activeContinuousHistogramFilters.length > 0) {
_.each(activeContinuousHistogramFilters, (key) => { _.each(activeContinuousHistogramFilters, key => {
_.each(newSelection, (cell, i) => { _.each(newSelection, (cell, i) => {
if ( if (
+cell[key] < newContinuousUserDefinedRanges[key][0] || +cell[key] < newContinuousUserDefinedRanges[key][0] ||
@@ -153,8 +158,8 @@ const updateCellSelectionMiddleware = (store) => {
) { ) {
newSelection[i]["__selected__"] = false; newSelection[i]["__selected__"] = false;
} }
}) });
}) });
} }
/* /*
@@ -177,7 +182,7 @@ const updateCellSelectionMiddleware = (store) => {
...s.controls.categoricalAsBooleansMap[action.metadataField], ...s.controls.categoricalAsBooleansMap[action.metadataField],
[action.value]: true [action.value]: true
} }
} };
} else if (action.type === "categorical metadata filter deselect") { } else if (action.type === "categorical metadata filter deselect") {
newCategoricalAsBooleansMap = { newCategoricalAsBooleansMap = {
...s.controls.categoricalAsBooleansMap, ...s.controls.categoricalAsBooleansMap,
@@ -185,61 +190,73 @@ const updateCellSelectionMiddleware = (store) => {
...s.controls.categoricalAsBooleansMap[action.metadataField], ...s.controls.categoricalAsBooleansMap[action.metadataField],
[action.value]: false [action.value]: false
} }
} };
} else if (action.type === "categorical metadata filter none of these") { } else if (action.type === "categorical metadata filter none of these") {
const metadataFieldWithAllOfTheseValueSelected = {}; const metadataFieldWithAllOfTheseValueSelected = {};
/* set EVERYTHING to false in this intermediate object */ /* set EVERYTHING to false in this intermediate object */
_.each(s.controls.categoricalAsBooleansMap[action.metadataField], (isActive, option) => { _.each(
metadataFieldWithAllOfTheseValueSelected[option] = false; s.controls.categoricalAsBooleansMap[action.metadataField],
}) (isActive, option) => {
metadataFieldWithAllOfTheseValueSelected[option] = false;
}
);
newCategoricalAsBooleansMap = { newCategoricalAsBooleansMap = {
...s.controls.categoricalAsBooleansMap, ...s.controls.categoricalAsBooleansMap,
[action.metadataField]: metadataFieldWithAllOfTheseValueSelected [action.metadataField]: metadataFieldWithAllOfTheseValueSelected
} };
} else if (action.type === "categorical metadata filter all of these") { } else if (action.type === "categorical metadata filter all of these") {
const metadataFieldWithAllOfTheseValueSelected = {}; const metadataFieldWithAllOfTheseValueSelected = {};
/* set EVERYTHING to true in this intermediate object */ /* set EVERYTHING to true in this intermediate object */
_.each(s.controls.categoricalAsBooleansMap[action.metadataField], (isActive, option) => { _.each(
metadataFieldWithAllOfTheseValueSelected[option] = true; s.controls.categoricalAsBooleansMap[action.metadataField],
}) (isActive, option) => {
metadataFieldWithAllOfTheseValueSelected[option] = true;
}
);
newCategoricalAsBooleansMap = { newCategoricalAsBooleansMap = {
...s.controls.categoricalAsBooleansMap, ...s.controls.categoricalAsBooleansMap,
[action.metadataField]: metadataFieldWithAllOfTheseValueSelected [action.metadataField]: metadataFieldWithAllOfTheseValueSelected
} };
} }
const inactiveCategories = []; const inactiveCategories = [];
_.each(newCategoricalAsBooleansMap, (options, category) => { _.each(newCategoricalAsBooleansMap, (options, category) => {
_.each(options, (isActive, option) => { _.each(options, (isActive, option) => {
if (!isActive) { if (!isActive) {
inactiveCategories.push({category, option}) inactiveCategories.push({ category, option });
} }
}) });
}) });
if (inactiveCategories.length > 0) { if (inactiveCategories.length > 0) {
_.each(inactiveCategories, (d) => { _.each(inactiveCategories, d => {
if (s.controls.categoricalAsCellsMap[d.category] && s.controls.categoricalAsCellsMap[d.category][d.option]) { if (
_.forEach(s.controls.categoricalAsCellsMap[d.category][d.option], (c) => { c.__selected__ = false; }); s.controls.categoricalAsCellsMap[d.category] &&
s.controls.categoricalAsCellsMap[d.category][d.option]
) {
_.forEach(
s.controls.categoricalAsCellsMap[d.category][d.option],
c => {
c.__selected__ = false;
}
);
} }
}) });
} }
let modifiedAction = Object.assign({}, action, { let modifiedAction = Object.assign({}, action, {
newSelection, newSelection,
newCategoricalAsBooleansMap, newCategoricalAsBooleansMap,
newContinuousUserDefinedRanges, newContinuousUserDefinedRanges
}) /* append the result of all the filters to the action the user just triggered */ }); /* append the result of all the filters to the action the user just triggered */
return next(modifiedAction); return next(modifiedAction);
} };
} };
}; };
export default updateCellSelectionMiddleware; export default updateCellSelectionMiddleware;
+7 -8
View File
@@ -1,3 +1,4 @@
// jshint esversion: 6
import uri from "urijs"; import uri from "urijs";
/* /*
@@ -5,14 +6,13 @@ import uri from "urijs";
storeInstance => functionToCallWithAnActionThatWillSendItToTheNextMiddleware => actionThatDispatchWasCalledWith => valueToUseAsTheReturnValueOfTheDispatchCall storeInstance => functionToCallWithAnActionThatWillSendItToTheNextMiddleware => actionThatDispatchWasCalledWith => valueToUseAsTheReturnValueOfTheDispatchCall
*/ */
const updateURLMiddleware = store => {
const updateURLMiddleware = (store) => { return next => {
return (next) => { return action => {
return (action) => {
const oldState = store.getState(); const oldState = store.getState();
const nextAction = next(action); const nextAction = next(action);
if (action.type === 'url changed') { if (action.type === "url changed") {
/* we don't handle pop state here - we handle it in the url reducer */ /* we don't handle pop state here - we handle it in the url reducer */
return nextAction; return nextAction;
} }
@@ -43,7 +43,6 @@ const updateURLMiddleware = (store) => {
// //
// window.history.pushState("", "", newURL) // window.history.pushState("", "", newURL)
// //
// // Internal helper for working with URIs // // Internal helper for working with URIs
// const oldURI = new URI(window.location.href); // const oldURI = new URI(window.location.href);
@@ -77,8 +76,8 @@ const updateURLMiddleware = (store) => {
// } // }
return nextAction; return nextAction;
} };
} };
}; };
export default updateURLMiddleware; export default updateURLMiddleware;
+27 -23
View File
@@ -1,27 +1,31 @@
const Cells = (state = { // jshint esversion: 6
cells: null, const Cells = (
loading: null, state = {
error: null, cells: null,
}, action) => { loading: null,
error: null
},
action
) => {
switch (action.type) { switch (action.type) {
case "request cells started": case "request cells started":
return Object.assign({}, state, { return Object.assign({}, state, {
loading: true, loading: true,
error: null error: null
}); });
case "request cells success": case "request cells success":
return Object.assign({}, state, { return Object.assign({}, state, {
error: null, error: null,
loading: false, loading: false,
cells: action.data, cells: action.data
}); });
case "request cells error": case "request cells error":
return Object.assign({}, state, { return Object.assign({}, state, {
loading: false, loading: false,
error: action.data error: action.data
}); });
default: default:
return state; return state;
} }
}; };
+184 -159
View File
@@ -1,184 +1,209 @@
// jshint esversion: 6
import _ from "lodash"; import _ from "lodash";
import { import { parseRGB } from "../util/parseRGB";
parseRGB
} from "../util/parseRGB";
const Controls = (state = { const Controls = (
_ranges: null, /* this comes from initialize, this is universe */ state = {
allGeneNames: null, _ranges: null /* this comes from initialize, this is universe */,
allCellsOnClient: null, /* this comes from cells endpoint, this is world */ allGeneNames: null,
currentCellSelection: null, /* this comes from user actions, all draw components use this, it is created by middleware */ allCellsOnClient: null /* this comes from cells endpoint, this is world */,
graphMap: null, currentCellSelection: null /* this comes from user actions, all draw components use this, it is created by middleware */,
categoricalAsBooleansMap: null, graphMap: null,
colorAccessor: null, categoricalAsBooleansMap: null,
colorScale: null, colorAccessor: null,
opacityForDeselectedCells: .2, colorScale: null,
graphBrushSelection: null, opacityForDeselectedCells: 0.2,
continuousSelection: null, graphBrushSelection: null,
scatterplotXXaccessor: null, // just easier to read continuousSelection: null,
scatterplotYYaccessor: null, scatterplotXXaccessor: null, // just easier to read
axesHaveBeenDrawn: false, scatterplotYYaccessor: null,
__storedStateForCelllist1__: null, /* will need procedural control of brush ie., brush.extent https://bl.ocks.org/micahstubbs/3cda05ca68cba260cb81 */ axesHaveBeenDrawn: false,
__storedStateForCelllist2__: null, __storedStateForCelllist1__: null /* will need procedural control of brush ie., brush.extent https://bl.ocks.org/micahstubbs/3cda05ca68cba260cb81 */,
}, action) => { __storedStateForCelllist2__: null
},
action
) => {
switch (action.type) { switch (action.type) {
/********************************** /**********************************
Keep a copy of 'universe' Keep a copy of 'universe'
***********************************/ ***********************************/
case "initialize success": case "initialize success":
return Object.assign({}, state, { return Object.assign({}, state, {
_ranges: action.data.data.ranges, _ranges: action.data.data.ranges,
allGeneNames: action.data.data.genes allGeneNames: action.data.data.genes
}); });
case "request cells success": case "request cells success":
const graphMap = {}; const graphMap = {};
const currentCellSelection = action.data.data.metadata.slice(0); const currentCellSelection = action.data.data.metadata.slice(0);
_.each(action.data.data.graph, (g) => { graphMap[g[0]] = [g[1], g[2]] }); _.each(action.data.data.graph, g => {
graphMap[g[0]] = [g[1], g[2]];
});
/* /*
construct a copy of the ranges object that only has categorical construct a copy of the ranges object that only has categorical
replace all counts with bool flags replace all counts with bool flags
ie., everything starts out checked ie., everything starts out checked
we mutate this map in the actions below we mutate this map in the actions below
*/ */
const categoricalAsBooleansMap = {}, categoricalAsCellsMap = {}; const categoricalAsBooleansMap = {},
const continuousUserDefinedRanges = {}; categoricalAsCellsMap = {};
_.each(action.data.data.ranges, (value, key) => { const continuousUserDefinedRanges = {};
if ( _.each(action.data.data.ranges, (value, key) => {
key !== "CellName" && if (
value.options /* it's categorical, it has options instead of ranges */ key !== "CellName" &&
) { value.options /* it's categorical, it has options instead of ranges */
const optionsAsBooleans = {}, optionsAsCells = {}; ) {
_.each(value.options, (_value, _key) => { const optionsAsBooleans = {},
optionsAsBooleans[_key] = true; optionsAsCells = {};
optionsAsCells[_key] = []; _.each(value.options, (_value, _key) => {
}) optionsAsBooleans[_key] = true;
categoricalAsBooleansMap[key] = optionsAsBooleans; optionsAsCells[_key] = [];
categoricalAsCellsMap[key] = optionsAsCells; });
} else if ( categoricalAsBooleansMap[key] = optionsAsBooleans;
key !== "CellName" && categoricalAsCellsMap[key] = optionsAsCells;
value.range } else if (key !== "CellName" && value.range) {
) { continuousUserDefinedRanges[key] = null;
continuousUserDefinedRanges[key] = null;
}
})
_.each(currentCellSelection, (cell) => {
cell["__selected__"] = true;
cell["__color__"] = "rgba(0,0,0,1)" /* initial color for all cells in all charts */
cell["__colorRGB__"] = parseRGB(cell["__color__"]);
// Add each cell to its categorical metadata set.
_.forEach(cell, (_value, key) => {
if (categoricalAsCellsMap[key] && categoricalAsCellsMap[key][_value]) {
const s = categoricalAsCellsMap[key][_value];
if (s) s.push(cell);
} }
}); });
});
return Object.assign({}, state, { _.each(currentCellSelection, cell => {
allCellsOnClient: action.data.data, cell["__selected__"] = true;
currentCellSelection, cell["__color__"] =
graphMap, "rgba(0,0,0,1)"; /* initial color for all cells in all charts */
categoricalAsBooleansMap, cell["__colorRGB__"] = parseRGB(cell["__color__"]);
categoricalAsCellsMap,
continuousUserDefinedRanges, // Add each cell to its categorical metadata set.
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 */ _.forEach(cell, (_value, key) => {
}); if (
/* * * * * * * * * * * * * * * * * * categoricalAsCellsMap[key] &&
categoricalAsCellsMap[key][_value]
) {
const s = categoricalAsCellsMap[key][_value];
if (s) s.push(cell);
}
});
});
return Object.assign({}, state, {
allCellsOnClient: action.data.data,
currentCellSelection,
graphMap,
categoricalAsBooleansMap,
categoricalAsCellsMap,
continuousUserDefinedRanges,
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 */
});
/* * * * * * * * * * * * * * * * * *
User events User events
* * * * * * * * * * * * * * * * * */ * * * * * * * * * * * * * * * * * */
case "parallel coordinates axes have been drawn": case "parallel coordinates axes have been drawn":
return Object.assign({}, state, { return Object.assign({}, state, {
axesHaveBeenDrawn: true axesHaveBeenDrawn: true
}); });
case "continuous selection using parallel coords brushing": { case "continuous selection using parallel coords brushing": {
return Object.assign({}, state, { return Object.assign({}, state, {
continuousSelection: action.data, continuousSelection: action.data,
currentCellSelection: action.newSelection /* this comes from middleware */ currentCellSelection:
}); action.newSelection /* this comes from middleware */
} });
case "graph brush selection change": }
return Object.assign({}, state, { case "graph brush selection change":
graphBrushSelection: action.brushCoords, /* this has already been applied in middleware but store it for next time */ return Object.assign({}, state, {
currentCellSelection: action.newSelection /* this comes from middleware */ graphBrushSelection:
}) action.brushCoords /* this has already been applied in middleware but store it for next time */,
case "graph brush deselect": currentCellSelection:
return Object.assign({}, state, { action.newSelection /* this comes from middleware */
graphBrushSelection: null, });
currentCellSelection: action.newSelection /* this comes from middleware */ case "graph brush deselect":
}) return Object.assign({}, state, {
case "continuous metadata histogram brush": graphBrushSelection: null,
return Object.assign({}, state, { currentCellSelection:
newContinuousUserDefinedRanges: action.newContinuousUserDefinedRanges, /* this has already been applied in middleware but store it for next time */ action.newSelection /* this comes from middleware */
currentCellSelection: action.newSelection /* this comes from middleware */ });
}) case "continuous metadata histogram brush":
case "change opacity deselected cells in 2d graph background": return Object.assign({}, state, {
return Object.assign({}, state, { newContinuousUserDefinedRanges:
opacityForDeselectedCells: action.data, action.newContinuousUserDefinedRanges /* this has already been applied in middleware but store it for next time */,
}) currentCellSelection:
/******************************* action.newSelection /* this comes from middleware */
});
case "change opacity deselected cells in 2d graph background":
return Object.assign({}, state, {
opacityForDeselectedCells: action.data
});
/*******************************
Categorical metadata Categorical metadata
*******************************/ *******************************/
case "categorical metadata filter select": case "categorical metadata filter select":
return Object.assign({}, state, { return Object.assign({}, state, {
categoricalAsBooleansMap: action.newCategoricalAsBooleansMap, /* this comes from middleware */ categoricalAsBooleansMap:
currentCellSelection: action.newSelection /* this comes from middleware */ action.newCategoricalAsBooleansMap /* this comes from middleware */,
}) currentCellSelection:
case "categorical metadata filter deselect": action.newSelection /* this comes from middleware */
return Object.assign({}, state, { });
categoricalAsBooleansMap: action.newCategoricalAsBooleansMap, /* this comes from middleware */ case "categorical metadata filter deselect":
currentCellSelection: action.newSelection /* this comes from middleware */ return Object.assign({}, state, {
}) categoricalAsBooleansMap:
case "categorical metadata filter none of these": action.newCategoricalAsBooleansMap /* this comes from middleware */,
return Object.assign({}, state, { currentCellSelection:
categoricalAsBooleansMap: action.newCategoricalAsBooleansMap, /* this comes from middleware */ action.newSelection /* this comes from middleware */
currentCellSelection: action.newSelection /* this comes from middleware */ });
}) case "categorical metadata filter none of these":
case "categorical metadata filter all of these": return Object.assign({}, state, {
return Object.assign({}, state, { categoricalAsBooleansMap:
categoricalAsBooleansMap: action.newCategoricalAsBooleansMap, /* this comes from middleware */ action.newCategoricalAsBooleansMap /* this comes from middleware */,
currentCellSelection: action.newSelection /* this comes from middleware */ currentCellSelection:
}) action.newSelection /* this comes from middleware */
/******************************* });
case "categorical metadata filter all of these":
return Object.assign({}, state, {
categoricalAsBooleansMap:
action.newCategoricalAsBooleansMap /* this comes from middleware */,
currentCellSelection:
action.newSelection /* this comes from middleware */
});
/*******************************
Color Scale Color Scale
*******************************/ *******************************/
case "color by continuous metadata": case "color by continuous metadata":
return Object.assign({}, state, { return Object.assign({}, state, {
colorAccessor: action.colorAccessor, colorAccessor: action.colorAccessor,
currentCellSelection: action.currentSelectionWithUpdatedColors, /* this comes from middleware */ currentCellSelection:
colorScale: action.colorScale, action.currentSelectionWithUpdatedColors /* this comes from middleware */,
}); colorScale: action.colorScale
case "color by expression": });
return Object.assign({}, state, { case "color by expression":
colorAccessor: action.gene, return Object.assign({}, state, {
currentCellSelection: action.currentSelectionWithUpdatedColors, /* this comes from middleware */ colorAccessor: action.gene,
colorScale: action.colorScale, currentCellSelection:
}) action.currentSelectionWithUpdatedColors /* this comes from middleware */,
case "color by categorical metadata": colorScale: action.colorScale
return Object.assign({}, state, { });
colorAccessor: action.colorAccessor, /* pass the scale through additionally, and it's a legend! */ case "color by categorical metadata":
currentCellSelection: action.currentSelectionWithUpdatedColors, /* this comes from middleware */ return Object.assign({}, state, {
colorScale: action.colorScale, colorAccessor:
}) action.colorAccessor /* pass the scale through additionally, and it's a legend! */,
case "store current cell selection as differential set 1": currentCellSelection:
return Object.assign({}, state, { action.currentSelectionWithUpdatedColors /* this comes from middleware */,
__storedStateForCelllist1__: action.data colorScale: action.colorScale
}); });
/******************************* case "store current cell selection as differential set 1":
return Object.assign({}, state, {
__storedStateForCelllist1__: action.data
});
/*******************************
Scatterplot Scatterplot
*******************************/ *******************************/
case "set scatterplot x": case "set scatterplot x":
return Object.assign({}, state, { return Object.assign({}, state, {
scatterplotXXaccessor: action.data scatterplotXXaccessor: action.data
}); });
case "set scatterplot y": case "set scatterplot y":
return Object.assign({}, state, { return Object.assign({}, state, {
scatterplotYYaccessor: action.data scatterplotYYaccessor: action.data
}); });
default: default:
return state; return state;
} }
}; };
+37 -33
View File
@@ -1,37 +1,41 @@
const Differential = (state = { // jshint esversion: 6
diffExp: null, const Differential = (
loading: null, state = {
error: null, diffExp: null,
celllist1: null, loading: null,
celllist2: null, error: null,
}, action) => { celllist1: null,
celllist2: null
},
action
) => {
switch (action.type) { switch (action.type) {
case "request differential expression started": case "request differential expression started":
return Object.assign({}, state, { return Object.assign({}, state, {
loading: true, loading: true,
error: null error: null
}); });
case "request differential expression success": case "request differential expression success":
return Object.assign({}, state, { return Object.assign({}, 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 Object.assign({}, 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 Object.assign({}, 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 Object.assign({}, state, {
celllist2: action.data celllist2: action.data
}); });
default: default:
return state; return state;
} }
}; };
+28 -24
View File
@@ -1,28 +1,32 @@
const Expression = (state = { // jshint esversion: 6
data: null, const Expression = (
loading: null, state = {
error: null, data: null,
}, action) => { loading: null,
error: null
},
action
) => {
switch (action.type) { switch (action.type) {
case "get expression started": case "get expression started":
return Object.assign({}, state, { return Object.assign({}, state, {
loading: true, loading: true,
error: null error: null
}); });
case "get expression success": case "get expression success":
return Object.assign({}, state, { return Object.assign({}, state, {
error: null, error: null,
loading: false, loading: false,
data: action.data.data, data: action.data.data
}); });
case "get expression error": case "get expression error":
return Object.assign({}, state, { return Object.assign({}, state, {
data: null, data: null,
loading: false, loading: false,
error: action.data error: action.data
}); });
default: default:
return state; return state;
} }
}; };
+4 -3
View File
@@ -1,4 +1,5 @@
import { combineReducers, createStore, applyMiddleware } from 'redux'; // jshint esversion: 6
import { combineReducers, createStore, applyMiddleware } from "redux";
import updateURLMiddleware from "../middleware/updateURLMiddleware"; import updateURLMiddleware from "../middleware/updateURLMiddleware";
import updateCellSelectionMiddleware from "../middleware/updateCellSelectionMiddleware"; import updateCellSelectionMiddleware from "../middleware/updateCellSelectionMiddleware";
import updateCellColors from "../middleware/updateCellColors"; import updateCellColors from "../middleware/updateCellColors";
@@ -16,8 +17,8 @@ const Reducer = combineReducers({
cells, cells,
expression, expression,
controls, controls,
differential, differential
}) });
let store = createStore( let store = createStore(
Reducer, Reducer,
+28 -24
View File
@@ -1,28 +1,32 @@
const Initialize = (state = { // jshint esversion: 6
data: null, const Initialize = (
loading: null, state = {
error: null, data: null,
}, action) => { loading: null,
error: null
},
action
) => {
switch (action.type) { switch (action.type) {
case "initialize started": case "initialize started":
return Object.assign({}, state, { return Object.assign({}, state, {
loading: true, loading: true,
error: null error: null
}); });
case "initialize success": case "initialize success":
return Object.assign({}, state, { return Object.assign({}, state, {
error: null, error: null,
loading: false, loading: false,
data: action.data, data: action.data
}); });
case "initialize error": case "initialize error":
return Object.assign({}, state, { return Object.assign({}, state, {
data: null, data: null,
loading: false, loading: false,
error: action.data error: action.data
}); });
default: default:
return state; return state;
} }
}; };
+15 -11
View File
@@ -1,15 +1,19 @@
const Responsive = (state = { // jshint esversion: 6
width: null, const Responsive = (
height: null, state = {
}, action) => { width: null,
height: null
},
action
) => {
switch (action.type) { switch (action.type) {
case "resize event": case "resize event":
return Object.assign({}, state, { return Object.assign({}, state, {
width: action.data, width: action.data,
height: action.data height: action.data
}); });
default: default:
return state; return state;
} }
}; };
+44 -41
View File
@@ -1,70 +1,73 @@
var createCamera = require('orbit-camera') // jshint esversion: 6
var createScroll = require('scroll-speed') var createCamera = require("orbit-camera");
var mp = require('mouse-position') var createScroll = require("scroll-speed");
var mb = require('mouse-pressed') var mp = require("mouse-position");
var key = require('key-pressed') var mb = require("mouse-pressed");
var key = require("key-pressed");
const panSpeed = 0.4 const panSpeed = 0.4;
const scaleSpeed = 0.5 const scaleSpeed = 0.5;
const scaleMax = 3 const scaleMax = 3;
// const scaleMin = 1.15 // const scaleMin = 1.15
const scaleMin = 1.03 const scaleMin = 1.03;
function attachCamera(canvas, opts) { function attachCamera(canvas, opts) {
opts = opts || {} opts = opts || {};
opts.pan = opts.pan !== false opts.pan = opts.pan !== false;
opts.scale = opts.scale !== false opts.scale = opts.scale !== false;
opts.rotate = opts.rotate !== false opts.rotate = opts.rotate !== false;
var scroll = createScroll(canvas, opts.scale) var scroll = createScroll(canvas, opts.scale);
var mbut = mb(canvas, opts.rotate) var mbut = mb(canvas, opts.rotate);
var mpos = mp(canvas) var mpos = mp(canvas);
var camera = createCamera( var camera = createCamera([0, 0, 1], [0, 0, -1], [0, 1, 0]);
[0, 0, 1]
, [0, 0, -1]
, [0, 1, 0]
)
camera.tick = tick camera.tick = tick;
return camera return camera;
function tick() { function tick() {
var ctrl = key('<control>') || key('<alt>') var ctrl = key("<control>") || key("<alt>");
var alt = key('<shift>') var alt = key("<shift>");
var height = canvas.height var height = canvas.height;
var width = canvas.width var width = canvas.width;
if (opts.rotate && mbut.left && ctrl && !alt) { if (opts.rotate && mbut.left && ctrl && !alt) {
camera.rotate( camera.rotate(
[ mpos.x / width - 0.5, mpos.y / height - 0.5 ] [mpos.x / width - 0.5, mpos.y / height - 0.5],
, [ mpos.prevX / width - 0.5, mpos.prevY / height - 0.5 ] [mpos.prevX / width - 0.5, mpos.prevY / height - 0.5]
) );
} }
if (opts.pan && mbut.right || (mbut.left && !ctrl && !alt)) { if ((opts.pan && mbut.right) || (mbut.left && !ctrl && !alt)) {
camera.pan([ camera.pan([
(panSpeed * (mpos[0] - mpos.prev[0]) / width) * Math.pow(camera.distance, 1) panSpeed *
, (panSpeed * (mpos[1] - mpos.prev[1]) / height) * Math.pow(camera.distance, 1) (mpos[0] - mpos.prev[0]) /
]) width *
Math.pow(camera.distance, 1),
panSpeed *
(mpos[1] - mpos.prev[1]) /
height *
Math.pow(camera.distance, 1)
]);
} }
if (opts.scale && scroll[1]) { if (opts.scale && scroll[1]) {
camera.distance *= Math.exp(scroll[1] * scaleSpeed / height) camera.distance *= Math.exp(scroll[1] * scaleSpeed / height);
} }
if (opts.scale && (mbut.middle || (mbut.left && !ctrl && alt))) { if (opts.scale && (mbut.middle || (mbut.left && !ctrl && alt))) {
var d = (mpos.y - mpos.prevY) var d = mpos.y - mpos.prevY;
if (!d) return; if (!d) return;
camera.distance *= Math.exp(d / height) camera.distance *= Math.exp(d / height);
} }
if (camera.distance > scaleMax) camera.distance = scaleMax if (camera.distance > scaleMax) camera.distance = scaleMax;
if (camera.distance < scaleMin) camera.distance = scaleMin if (camera.distance < scaleMin) camera.distance = scaleMin;
scroll.flush() scroll.flush();
mpos.flush() mpos.flush();
} }
} }
+5 -8
View File
@@ -1,13 +1,10 @@
import {scaleRGB} from "./scaleRGB"; // jshint esversion: 6
import { scaleRGB } from "./scaleRGB";
export const parseRGB = (c) => { export const parseRGB = c => {
if (c[0] !== "#") { if (c[0] !== "#") {
const _c = c.replace(/[^\d,.]/g, '').split(','); const _c = c.replace(/[^\d,.]/g, "").split(",");
return [ return [scaleRGB(+_c[0]), scaleRGB(+_c[1]), scaleRGB(+_c[2])];
scaleRGB(+_c[0]),
scaleRGB(+_c[1]),
scaleRGB(+_c[2])
];
} else { } else {
var parsedHex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(c); var parsedHex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(c);
return [ return [
+18 -14
View File
@@ -1,14 +1,15 @@
// jshint esversion: 6
/***************************************** /*****************************************
****************************************** ******************************************
Render Queue via http://bl.ocks.org/syntagmatic/raw/3341641/render-queue.js Render Queue via http://bl.ocks.org/syntagmatic/raw/3341641/render-queue.js
****************************************** ******************************************
******************************************/ ******************************************/
const renderQueue = (function(callback1234) { const renderQueue = function(callback1234) {
var _queue = [], // data to be rendered var _queue = [], // data to be rendered
_rate = 300, // number of calls per frame _rate = 300, // number of calls per frame
_invalidate = function() {}, // invalidate last render queue _invalidate = function() {}, // invalidate last render queue
_clear = function() {}; // clearing function _clear = function() {}; // clearing function
var rq = function(ARRAY_FROM_CELLXGENE) { var rq = function(ARRAY_FROM_CELLXGENE) {
if (ARRAY_FROM_CELLXGENE) rq.data(ARRAY_FROM_CELLXGENE); if (ARRAY_FROM_CELLXGENE) rq.data(ARRAY_FROM_CELLXGENE);
@@ -25,7 +26,7 @@ const renderQueue = (function(callback1234) {
function doFrame() { function doFrame() {
if (!valid) return true; if (!valid) return true;
var chunk = _queue.splice(0,_rate); var chunk = _queue.splice(0, _rate);
chunk.map(callback1234); chunk.map(callback1234);
timer_frame(doFrame); timer_frame(doFrame);
} }
@@ -35,7 +36,7 @@ const renderQueue = (function(callback1234) {
rq.data = function(ARRAY_FROM_CELLXGENE) { rq.data = function(ARRAY_FROM_CELLXGENE) {
_invalidate(); _invalidate();
_queue = ARRAY_FROM_CELLXGENE.slice(0); // creates a copy of the data _queue = ARRAY_FROM_CELLXGENE.slice(0); // creates a copy of the data
return rq; return rq;
}; };
@@ -65,14 +66,17 @@ const renderQueue = (function(callback1234) {
rq.invalidate = _invalidate; rq.invalidate = _invalidate;
var timer_frame = window.requestAnimationFrame var timer_frame =
|| window.webkitRequestAnimationFrame window.requestAnimationFrame ||
|| window.mozRequestAnimationFrame window.webkitRequestAnimationFrame ||
|| window.oRequestAnimationFrame window.mozRequestAnimationFrame ||
|| window.msRequestAnimationFrame window.oRequestAnimationFrame ||
|| function(callback) { setTimeout(callback, 17); }; window.msRequestAnimationFrame ||
function(callback) {
setTimeout(callback, 17);
};
return rq; return rq;
}); };
export default renderQueue; export default renderQueue;
+3 -2
View File
@@ -1,4 +1,5 @@
export const scaleRGB = (input) => { // jshint esversion: 6
export const scaleRGB = input => {
const outputMax = 1; const outputMax = 1;
const outputMin = 0; const outputMin = 0;
@@ -7,4 +8,4 @@ export const scaleRGB = (input) => {
const percent = (input - inputMin) / (inputMax - inputMin); const percent = (input - inputMin) / (inputMax - inputMin);
return percent * (outputMax - outputMin) + outputMin; return percent * (outputMax - outputMin) + outputMin;
} };