This commit is contained in:
bkmartinjr
2018-05-05 13:15:10 -07:00
parent d91d42f2c4
commit 90c7daf571
62 changed files with 2398 additions and 1912 deletions

View File

@@ -1,6 +1,6 @@
# cellxgene
A React + Redux web application for exploring large scale single cell RNA sequence data.
A React + Redux web application for exploring large scale single cell RNA sequence data.
##### Quickstart:

View File

@@ -1,11 +1,7 @@
module.exports = {
babelrc: false,
cacheDirectory: true,
presets: [
[ "env", { loose: true, modules: false } ],
"stage-0",
"react"
],
presets: [["env", { loose: true, modules: false }], "stage-0", "react"],
plugins: [
"react-hot-loader/babel",
"babel-plugin-transform-decorators-legacy"

View File

@@ -1,12 +1,10 @@
module.exports = {
babelrc: false,
presets: [
[ "env", { loose: true, modules: false } ],
"stage-0",
"react"
],
presets: [["env", { loose: true, modules: false }], "stage-0", "react"],
plugins: [
"babel-plugin-transform-react-constant-elements",
"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")]])
};

View File

@@ -1,9 +1,9 @@
module.exports = {
babelrc: false,
presets: [
'babel-preset-es2015',
'babel-preset-stage-0',
'babel-preset-react'
"babel-preset-es2015",
"babel-preset-stage-0",
"babel-preset-react"
],
plugins: [ 'istanbul' ]
plugins: ["istanbul"]
};

View File

@@ -1,22 +1,22 @@
module.exports = {
root: true,
parser: 'babel-eslint',
extends: 'airbnb',
parser: "babel-eslint",
extends: "airbnb",
env: { browser: true, commonjs: true, es6: true },
globals: { expect: true },
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
sourceType: "module",
ecmaFeatures: {
jsx: true,
generators: true,
generators: true
}
},
rules: {
'no-magic-numbers': 'off',
'func-style': 'off',
'arrow-parens': 'off',
'no-use-before-define': 'off',
'react/jsx-filename-extension': 'off',
"no-magic-numbers": "off",
"func-style": "off",
"arrow-parens": "off",
"no-use-before-define": "off",
"react/jsx-filename-extension": "off"
}
};

View File

@@ -1,6 +1,6 @@
if (typeof Promise === 'undefined') {
require('promise/lib/rejection-tracking').enable();
window.Promise = require('promise/lib/es6-extensions.js');
if (typeof Promise === "undefined") {
require("promise/lib/rejection-tracking").enable();
window.Promise = require("promise/lib/es6-extensions.js");
}
require('whatwg-fetch');
require("whatwg-fetch");

View File

@@ -1,72 +1,74 @@
const path = require('path');
const autoprefixer = require('autoprefixer');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// jshint esversion: 6
const path = require("path");
const autoprefixer = require("autoprefixer");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const src = path.resolve('src');
const nodeModules = path.resolve('node_modules');
const src = path.resolve("src");
const nodeModules = path.resolve("node_modules");
module.exports = {
devtool: 'eval',
devtool: "eval",
entry: [
'webpack-hot-middleware/client?quiet=true&noInfo=true',
require.resolve('react-hot-loader/patch'),
require.resolve('../polyfills/polyfills'),
path.join(src, 'index')
"webpack-hot-middleware/client?quiet=true&noInfo=true",
require.resolve("react-hot-loader/patch"),
require.resolve("../polyfills/polyfills"),
path.join(src, "index")
],
output: {
path: path.resolve('build'),
path: path.resolve("build"),
pathinfo: true,
filename: 'static/js/bundle.js',
publicPath: '/'
filename: "static/js/bundle.js",
publicPath: "/"
},
resolve: { extensions: [ '.js', '.json' ] },
resolve: { extensions: [".js", ".json"] },
module: {
loaders: [
{
test: /\.js$/,
include: src,
loader: 'babel-loader',
query: require('../babel/babel.dev')
loader: "babel-loader",
query: require("../babel/babel.dev")
},
{
test: /\.css$/,
include: [ src, nodeModules ],
loader: 'style-loader!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader'
include: [src, nodeModules],
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)(\?.*)?$/,
include: [ src, nodeModules ],
loader: 'file-loader',
query: { name: 'static/media/[name].[ext]' }
include: [src, nodeModules],
loader: "file-loader",
query: { name: "static/media/[name].[ext]" }
},
{
test: /\.(mp4|webm)(\?.*)?$/,
include: [ src, nodeModules ],
loader: 'url-loader',
query: { limit: 10000, name: 'static/media/[name].[ext]' }
include: [src, nodeModules],
loader: "url-loader",
query: { limit: 10000, name: "static/media/[name].[ext]" }
}
]
},
plugins: [
new HtmlWebpackPlugin({
inject: true,
template: path.resolve('index.html'),
favicon: path.resolve('favicon.png')
template: path.resolve("index.html"),
favicon: path.resolve("favicon.png")
}),
new webpack.LoaderOptionsPlugin({
options: {
eslint: {
configFile: path.resolve('./configuration/eslint/eslint.js'),
configFile: path.resolve("./configuration/eslint/eslint.js"),
useEslintrc: false
},
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
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()

View File

@@ -1,74 +1,74 @@
const path = require('path');
const autoprefixer = require('autoprefixer');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackInlineSourcePlugin = require(
'html-webpack-inline-source-plugin'
);
// jshint esversion: 6
const path = require("path");
const autoprefixer = require("autoprefixer");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const SWPrecacheWebpackPlugin = require("sw-precache-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin");
const MinifyPlugin = require("babel-minify-webpack-plugin");
const src = path.resolve('src');
const nodeModules = path.resolve('node_modules');
const src = path.resolve("src");
const nodeModules = path.resolve("node_modules");
const publicPath = '/';
const publicPath = "/";
module.exports = {
bail: true,
devtool: 'cheap-source-map',
entry: [ require.resolve('../polyfills/polyfills'), path.join(src, 'index') ],
devtool: "cheap-source-map",
entry: [require.resolve("../polyfills/polyfills"), path.join(src, "index")],
output: {
path: path.resolve('build'),
filename: 'static/js/[name].[chunkhash:8].js',
chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
path: path.resolve("build"),
filename: "static/js/[name].[chunkhash:8].js",
chunkFilename: "static/js/[name].[chunkhash:8].chunk.js",
publicPath
},
resolve: { extensions: [ '.js', '.json' ] },
resolve: { extensions: [".js", ".json"] },
module: {
loaders: [
{
test: /\.js$/,
include: src,
loader: 'babel-loader',
query: require('../babel/babel.prod')
loader: "babel-loader",
query: require("../babel/babel.prod")
},
{
test: /\.css$/,
include: [ src, nodeModules ],
include: [src, nodeModules],
loader: ExtractTextPlugin.extract({
fallbackLoader: 'style-loader',
loader: 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]-autoprefixer!postcss-loader'
fallbackLoader: "style-loader",
loader:
"css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]-autoprefixer!postcss-loader"
})
},
{
test: /\.json$/,
include: [ src, nodeModules ],
loader: 'json-loader',
include: [src, nodeModules],
loader: "json-loader",
exclude: /manifest.json$/
},
{
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/,
include: [ src, nodeModules ],
loader: 'file-loader',
query: { name: 'static/media/[name].[hash:8].[ext]' }
include: [src, nodeModules],
loader: "file-loader",
query: { name: "static/media/[name].[hash:8].[ext]" }
},
{
test: /\.(mp4|webm)(\?.*)?$/,
include: [ src, nodeModules ],
loader: 'url-loader',
query: { limit: 10000, name: 'static/media/[name].[hash:8].[ext]' }
include: [src, nodeModules],
loader: "url-loader",
query: { limit: 10000, name: "static/media/[name].[hash:8].[ext]" }
}
]
},
plugins: [
new HtmlWebpackPlugin({
inject: 'body',
filename: 'index.html',
template: path.resolve('index_template.html'),
favicon: path.resolve('favicon.png'),
inlineSource: '.(js|css)$',
inject: "body",
filename: "index.html",
template: path.resolve("index_template.html"),
favicon: path.resolve("favicon.png"),
inlineSource: ".(js|css)$",
minify: {
removeComments: true,
collapseWhitespace: true,
@@ -86,21 +86,21 @@ module.exports = {
new webpack.LoaderOptionsPlugin({
options: {
eslint: {
configFile: path.resolve('./configuration/eslint/eslint.js'),
configFile: path.resolve("./configuration/eslint/eslint.js"),
useEslintrc: false
},
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 MinifyPlugin(),
new ExtractTextPlugin('static/css/[name].[contenthash:8].css'),
new ExtractTextPlugin("static/css/[name].[contenthash:8].css"),
new SWPrecacheWebpackPlugin({
cacheId: 'cellxgene',
filename: 'service-worker.js'
cacheId: "cellxgene",
filename: "service-worker.js"
})
]
};

View File

@@ -13,12 +13,12 @@
</head>
<body>
<script type="text/javascript">
window.CELLXGENE = {}
window.CELLXGENE.API= {
prefix: "{{ prefix | safe }}",
version: "v0.1/",
}
window.CELLXGENE.datasetTitle = "{{ datasetTitle }}"
window.CELLXGENE = {};
window.CELLXGENE.API = {
prefix: "{{ prefix | safe }}",
version: "v0.1/"
};
window.CELLXGENE.datasetTitle = "{{ datasetTitle }}";
</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>
@@ -26,4 +26,4 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.js"></script>
<script src="//d3js.org/d3-scale-chromatic.v0.3.min.js"></script>
</body>
</html>
</html>

View File

@@ -1,26 +1,27 @@
/* eslint-disable */
var path = require('path');
var historyApiFallback = require('connect-history-api-fallback');
var chalk = require('chalk');
var express = require('express');
var favicon = require('serve-favicon');
var webpack = require('webpack');
var config = require('../configuration/webpack/webpack.config.dev');
var utils = require('./utils');
// jshint esversion: 6
var path = require("path");
var historyApiFallback = require("connect-history-api-fallback");
var chalk = require("chalk");
var express = require("express");
var favicon = require("serve-favicon");
var webpack = require("webpack");
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;
// Set up compiler
var compiler = webpack(config);
compiler.plugin('invalid', () => {
compiler.plugin("invalid", () => {
utils.clearConsole();
console.log('Compiling...');
console.log("Compiling...");
});
compiler.plugin('done', stats => {
compiler.plugin("done", stats => {
utils.formatStats(stats, PORT);
});
@@ -30,18 +31,18 @@ var app = express();
app.use(historyApiFallback({ verbose: false }));
app.use(
require('webpack-dev-middleware')(compiler, {
require("webpack-dev-middleware")(compiler, {
noInfo: true,
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) => {
res.sendFile(path.resolve('index.html'));
app.get("*", (req, res) => {
res.sendFile(path.resolve("index.html"));
});
app.listen(PORT, err => {
@@ -51,6 +52,6 @@ app.listen(PORT, err => {
}
utils.clearConsole();
console.log(chalk.cyan('Starting the development server...'));
console.log(chalk.cyan("Starting the development server..."));
console.log();
});

View File

@@ -1,22 +1,23 @@
/* eslint-disable */
var path = require('path');
var fs = require('fs');
var chalk = require('chalk');
var express = require('express');
var favicon = require('serve-favicon');
var utils = require('./utils');
// jshint esversion: 6
var path = require("path");
var fs = require("fs");
var chalk = require("chalk");
var express = require("express");
var favicon = require("serve-favicon");
var utils = require("./utils");
var PORT = process.env.PORT || 80;
// Launch server
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) => {
res.sendFile(path.resolve('./build/index.html'));
app.get("*", (req, res) => {
res.sendFile(path.resolve("./build/index.html"));
});
app.listen(PORT, err => {
@@ -26,6 +27,6 @@ app.listen(PORT, err => {
}
utils.clearConsole();
console.log(chalk.cyan('Production server started on port ' + PORT));
console.log(chalk.cyan("Production server started on port " + PORT));
console.log();
});

View File

@@ -1,7 +1,8 @@
/* eslint-disable */
var chalk = require('chalk');
// jshint esversion: 6
var chalk = require("chalk");
var friendlySyntaxErrorLabel = 'Syntax error:';
var friendlySyntaxErrorLabel = "Syntax error:";
function isLikelyASyntaxError(message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1;
@@ -9,16 +10,16 @@ function isLikelyASyntaxError(message) {
function formatMessage(message) {
return message
.replace('Module build failed: SyntaxError:', friendlySyntaxErrorLabel)
.replace("Module build failed: SyntaxError:", friendlySyntaxErrorLabel)
.replace(
/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('./~/css-loader!./~/postcss-loader!', '');
.replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, "")
.replace("./~/css-loader!./~/postcss-loader!", "");
}
var clearConsole = () => {
process.stdout.write('\x1bc');
process.stdout.write("\x1bc");
};
var formatStats = (stats, port) => {
@@ -26,23 +27,23 @@ var formatStats = (stats, port) => {
var hasErrors = stats.hasErrors();
var hasWarnings = stats.hasWarnings();
if (!hasErrors && !hasWarnings) {
console.log(chalk.green('Compiled successfully!'));
console.log(chalk.green("Compiled successfully!"));
console.log();
console.log('The app is running at http://localhost:' + port + '/');
console.log("The app is running at http://localhost:" + port + "/");
console.log();
return;
}
var json = stats.toJson();
var formattedErrors = json.errors.map(
message => 'Error in ' + formatMessage(message)
message => "Error in " + formatMessage(message)
);
var formattedWarnings = json.warnings.map(
message => 'Warning in ' + formatMessage(message)
message => "Warning in " + formatMessage(message)
);
if (hasErrors) {
console.log(chalk.red('Failed to compile.'));
console.log(chalk.red("Failed to compile."));
console.log();
if (formattedErrors.some(isLikelyASyntaxError)) {
formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
@@ -55,23 +56,23 @@ var formatStats = (stats, port) => {
}
if (hasWarnings) {
console.log(chalk.yellow('Compiled with warnings.'));
console.log(chalk.yellow("Compiled with warnings."));
console.log();
formattedWarnings.forEach(message => {
console.log(message);
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(
'Use ' +
chalk.yellow('// eslint-disable-next-line') +
' to ignore the next line.'
"Use " +
chalk.yellow("// eslint-disable-next-line") +
" to ignore the next line."
);
console.log(
'Use ' +
chalk.yellow('/* eslint-disable */') +
' to ignore all warnings in a file.'
"Use " +
chalk.yellow("/* eslint-disable */") +
" to ignore all warnings in a file."
);
}
};

View File

@@ -1,30 +1,31 @@
// jshint esversion: 6
import * as globals from "../globals";
import URI from "urijs";
import _ from "lodash";
const requestCells = (query = "") => {
return (dispatch) => {
dispatch({type: "request cells started"})
return dispatch => {
dispatch({ type: "request cells started" });
return fetch(`${globals.API.prefix}${globals.API.version}cells${query}`, {
method: "get",
headers: new Headers({
'Content-Type': 'application/json'
})
method: "get",
headers: new Headers({
"Content-Type": "application/json"
})
})
.then(res => res.json())
.then(
data => dispatch({type: "request cells success", data}),
error => dispatch({type: "request cells error", error})
)
}
}
data => dispatch({ type: "request cells success", data }),
error => dispatch({ type: "request cells error", error })
);
};
};
/* SELECT */
const regraph = () => {
return (dispatch, getState) => {
dispatch({type: "regraph started"})
dispatch({ type: "regraph started" });
const state = getState()
const state = getState();
const selectedMetadata = {};
_.each(state.controls.categoricalAsBooleansMap, (options, field) => {
@@ -40,143 +41,152 @@ const regraph = () => {
_.each(options, (isActive, option) => {
if (isActive) {
if (selectedMetadata[field]) {
selectedMetadata[field].push(option)
selectedMetadata[field].push(option);
} else if (!selectedMetadata[field]) {
selectedMetadata[field] = [option]
selectedMetadata[field] = [option];
}
}
})
});
}
})
});
let uri = new URI()
uri.setSearch(selectedMetadata)
console.log(uri.search(), selectedMetadata)
let uri = new URI();
uri.setSearch(selectedMetadata);
console.log(uri.search(), selectedMetadata);
dispatch(
requestCells(uri.search())
).then((res) => {
dispatch(requestCells(uri.search())).then(res => {
if (res.error) {
dispatch({type: "regraph success"})
dispatch({ type: "regraph success" });
} else {
dispatch({type: "regraph error"})
dispatch({ type: "regraph error" });
}
})
}
}
});
};
};
const initialize = () => {
return (dispatch, getState) => {
dispatch({type: "initialize started"})
dispatch({ type: "initialize started" });
fetch(`${globals.API.prefix}${globals.API.version}initialize`, {
method: "get",
headers: new Headers({
'Content-Type': 'application/json'
})
method: "get",
headers: new Headers({
"Content-Type": "application/json"
})
})
.then(res => res.json())
.then(
data => dispatch({type: "initialize success", data}),
error => dispatch({type: "initialize error", error})
)
}
}
data => dispatch({ type: "initialize success", data }),
error => dispatch({ type: "initialize error", error })
);
};
};
const requestGeneExpressionCounts = () => {
return (dispatch, getState) => {
dispatch({type: "get expression started"})
dispatch({ type: "get expression started" });
fetch(`${globals.API.prefix}${globals.API.version}expression`, {
method: "get",
headers: new Headers({
'accept': 'application/json'
})
method: "get",
headers: new Headers({
accept: "application/json"
})
})
.then(res => res.json())
.then(
data => dispatch({type: "get expression success", data}),
error => dispatch({type: "get expression error", error})
)
}
}
data => dispatch({ type: "get expression success", data }),
error => dispatch({ type: "get expression error", error })
);
};
};
const requestSingleGeneExpressionCountsForColoringPOST = (gene) => {
const requestSingleGeneExpressionCountsForColoringPOST = gene => {
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`, {
method: "POST",
body: JSON.stringify({
"genelist": [gene]
}),
headers: new Headers({
"accept": "application/json",
"Content-Type": "application/json"
})
method: "POST",
body: JSON.stringify({
genelist: [gene]
}),
headers: new Headers({
accept: "application/json",
"Content-Type": "application/json"
})
})
.then(res => res.json())
.then(
data => dispatch({
type: "color by expression",
gene: gene,
data
}),
error => dispatch({type: "get single gene expression for coloring error", error})
)
}
}
data =>
dispatch({
type: "color by expression",
gene: gene,
data
}),
error =>
dispatch({
type: "get single gene expression for coloring error",
error
})
);
};
};
const requestGeneExpressionCountsPOST = (genes) => {
const requestGeneExpressionCountsPOST = genes => {
return (dispatch, getState) => {
dispatch({type: "get expression started"})
dispatch({ type: "get expression started" });
fetch(`${globals.API.prefix}${globals.API.version}expression`, {
method: "POST",
body: JSON.stringify({
"genelist": genes
}),
headers: new Headers({
"accept": "application/json",
"Content-Type": "application/json"
})
method: "POST",
body: JSON.stringify({
genelist: genes
}),
headers: new Headers({
accept: "application/json",
"Content-Type": "application/json"
})
})
.then(res => res.json())
.then(
data => dispatch({type: "get expression success", data}),
error => dispatch({type: "get expression error", error})
)
}
}
data => dispatch({ type: "get expression success", data }),
error => dispatch({ type: "get expression error", error })
);
};
};
const requestDifferentialExpression = (celllist1, celllist2, num_genes = 7) => {
return (dispatch, getState) => {
dispatch({type: "request differential expression started"})
dispatch({ type: "request differential expression started" });
fetch(`${globals.API.prefix}${globals.API.version}diffexpression`, {
method: "POST",
body: JSON.stringify({
celllist1,
celllist2,
num_genes,
}),
headers: new Headers({
"accept": "application/json",
"Content-Type": "application/json"
})
method: "POST",
body: JSON.stringify({
celllist1,
celllist2,
num_genes
}),
headers: new Headers({
accept: "application/json",
"Content-Type": "application/json"
})
})
.then(res => res.json())
.then(
(data) => {
data => {
/* kick off a secondary action to get all expression counts for all cells now that we know what the top expressed are */
dispatch(
requestGeneExpressionCountsPOST(
_.union(data.data.celllist1.topgenes, data.data.celllist2.topgenes) // ["GPM6B", "FEZ1", "TSPAN31", "PCSK1N", "TUBA1A", "GPM6A", "CLU", "FCER1G", "TYROBP", "C1QB", "CD74", "CYBA", "GPX1", "TMSB4X"]
_.union(
data.data.celllist1.topgenes,
data.data.celllist2.topgenes
) // ["GPM6B", "FEZ1", "TSPAN31", "PCSK1N", "TUBA1A", "GPM6A", "CLU", "FCER1G", "TYROBP", "C1QB", "CD74", "CYBA", "GPX1", "TMSB4X"]
)
)
);
/* then send the success case action through */
return dispatch({type: "request differential expression success", data})
return dispatch({
type: "request differential expression success",
data
});
},
error => dispatch({type: "request differential expression error", error})
)
}
}
error =>
dispatch({ type: "request differential expression error", error })
);
};
};
export default {
initialize,
@@ -186,4 +196,4 @@ export default {
requestGeneExpressionCountsPOST,
requestSingleGeneExpressionCountsForColoringPOST,
requestDifferentialExpression
}
};

View File

@@ -1,11 +1,15 @@
import React from 'react';
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import DeckGL, {PointCloudLayer, ScreenGridLayer, COORDINATE_SYSTEM} from 'deck.gl';
import OrbitController from './orbit-control';
import { Popup } from './popup';
import DeckGL, {
PointCloudLayer,
ScreenGridLayer,
COORDINATE_SYSTEM
} from "deck.gl";
import OrbitController from "./orbit-control";
import { Popup } from "./popup";
class Heatmap extends React.Component {
constructor(props) {
super(props);
@@ -20,16 +24,16 @@ class Heatmap extends React.Component {
height: 0,
points: [],
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,
popup: {
displayed: false,
x: 0,
y: 0,
title: '',
title: ""
},
viewport: {
lookAt: [0, 0, 0],
@@ -46,7 +50,7 @@ class Heatmap extends React.Component {
getColor(cluster) {
let color = [0, 0, 0];
switch(cluster){
switch (cluster) {
case 0:
color = [166, 206, 227];
break;
@@ -82,9 +86,8 @@ class Heatmap extends React.Component {
return color;
}
componentWillMount() {
window.addEventListener('resize', this.onResize);
window.addEventListener("resize", this.onResize);
this.onResize();
}
@@ -97,30 +100,31 @@ class Heatmap extends React.Component {
}
componentWillUnmount() {
window.removeEventListener('resize', this.onResize);
window.removeEventListener("resize", this.onResize);
}
fetchData() {
fetch('https://raw.githubusercontent.com/zdenekhynek/data-science-capstone-visualisation/master/public/clusters.json')
.then((res) => {
res.json().then((obj) => {
const clusters = Object.keys(obj).map((k) => obj[k])
const points = clusters.map((cluster) => {
const position = [cluster.x, cluster.y, cluster.z];
const color = [255,0,0];
const id = cluster.id;
const title = cluster.webTitle;
return { id, title, position, color };
});
this.setState({ points, progress: 1 })
fetch(
"https://raw.githubusercontent.com/zdenekhynek/data-science-capstone-visualisation/master/public/clusters.json"
).then(res => {
res.json().then(obj => {
const clusters = Object.keys(obj).map(k => obj[k]);
const points = clusters.map(cluster => {
const position = [cluster.x, cluster.y, cluster.z];
const color = [255, 0, 0];
const id = cluster.id;
const title = cluster.webTitle;
return { id, title, position, color };
});
this.setState({ points, progress: 1 });
});
});
}
onHover(d) {
let popup = { displayed: false };
console.log('d', d)
console.log("d", d);
if (d.object) {
const object = d.object;
@@ -129,7 +133,7 @@ class Heatmap extends React.Component {
title: object.title,
displayed: true,
x: d.x,
y: d.y,
y: d.y
};
}
@@ -137,8 +141,8 @@ class Heatmap extends React.Component {
}
onResize() {
const {innerWidth: width, innerHeight: height} = window;
this.setState({width: width / 1.5, height: height / 1.5});
const { innerWidth: width, innerHeight: height } = window;
this.setState({ width: width / 1.5, height: height / 1.5 });
}
onInitialized(gl) {
@@ -150,27 +154,30 @@ class Heatmap extends React.Component {
onChangeViewport(viewport) {
this.setState({
rotating: !viewport.isDragging,
viewport: {...this.state.viewport, ...viewport}
viewport: { ...this.state.viewport, ...viewport }
});
}
onUpdate() {
const {viewport} = this.state;
const { viewport } = this.state;
window.requestAnimationFrame(this.onUpdate);
}
renderPointCloudLayer() {
return this.state.points.length && new PointCloudLayer({
id: 'point-cloud-layer',
data: this.state.points,
projectionMode: COORDINATE_SYSTEM.IDENTITY,
pickable: true,
onHover: this.onHover,
getPosition: d => d.position,
getNormal: d => [0, 0.5, 0.2],
getColor: d => d.color,
radiusPixels: 2
});
return (
this.state.points.length &&
new PointCloudLayer({
id: "point-cloud-layer",
data: this.state.points,
projectionMode: COORDINATE_SYSTEM.IDENTITY,
pickable: true,
onHover: this.onHover,
getPosition: d => d.position,
getNormal: d => [0, 0.5, 0.2],
getColor: d => d.color,
radiusPixels: 2
})
);
}
renderGridLayer() {
@@ -182,7 +189,7 @@ class Heatmap extends React.Component {
* ]
*/
const screenGridLayer = new ScreenGridLayer({
id: 'screen-grid-layer',
id: "screen-grid-layer",
data: this.state.sampleExpressionMatrix,
projectionMode: COORDINATE_SYSTEM.IDENTITY,
pickable: true,
@@ -195,34 +202,42 @@ class Heatmap extends React.Component {
}
renderDeckGLCanvas() {
const {width, height, viewport} = this.state;
const canvasProps = {width, height, ...viewport};
const { width, height, viewport } = this.state;
const canvasProps = { width, height, ...viewport };
const glViewport = OrbitController.getViewport(canvasProps);
return width && height && (
<OrbitController {...canvasProps} ref={canvas => {
this.canvas = canvas;
}} onChangeViewport={this.onChangeViewport}>
<DeckGL
width={width}
height={height}
viewport={glViewport}
layers={[
// this.renderPointCloudLayer(),
this.renderGridLayer()
].filter(Boolean)}
onWebGLInitialized={this.onInitialized}/>
</OrbitController>
return (
width &&
height && (
<OrbitController
{...canvasProps}
ref={canvas => {
this.canvas = canvas;
}}
onChangeViewport={this.onChangeViewport}
>
<DeckGL
width={width}
height={height}
viewport={glViewport}
layers={[
// this.renderPointCloudLayer(),
this.renderGridLayer()
].filter(Boolean)}
onWebGLInitialized={this.onInitialized}
/>
</OrbitController>
)
);
}
render() {
const {width, height, popup} = this.state;
const { width, height, popup } = this.state;
if (!width || !height) {
return null;
}
const renderedPopup = (popup.displayed)? <Popup {...popup} /> : null;
const renderedPopup = popup.displayed ? <Popup {...popup} /> : null;
return (
<div id="heatmap">
@@ -231,6 +246,6 @@ class Heatmap extends React.Component {
</div>
);
}
};
}
export default Heatmap;

View File

@@ -1,7 +1,8 @@
// jshint esversion: 6
/* global window */
import React, {Component} from 'react';
import {PerspectiveViewport} from 'deck.gl';
import {vec3} from 'gl-matrix';
import React, { Component } from "react";
import { PerspectiveViewport } from "deck.gl";
import { vec3 } from "gl-matrix";
/* Utils */
// constrain number between bounds
@@ -15,15 +16,24 @@ function clamp(x, min, max) {
return x;
}
const ua = typeof window.navigator !== 'undefined' ?
window.navigator.userAgent.toLowerCase() : '';
const firefox = ua.indexOf('firefox') !== -1;
const ua =
typeof window.navigator !== "undefined"
? window.navigator.userAgent.toLowerCase()
: "";
const firefox = ua.indexOf("firefox") !== -1;
/* Interaction */
export default class OrbitController extends Component {
static getViewport({width, height, lookAt, distance, rotationX, rotationY, fov}) {
static getViewport({
width,
height,
lookAt,
distance,
rotationX,
rotationY,
fov
}) {
const cameraPos = vec3.add([], lookAt, [0, 0, distance]);
vec3.rotateX(cameraPos, cameraPos, lookAt, rotationX / 180 * Math.PI);
vec3.rotateY(cameraPos, cameraPos, lookAt, rotationY / 180 * Math.PI);
@@ -45,25 +55,29 @@ export default class OrbitController extends Component {
}
_onDragStart(evt) {
const {pageX, pageY} = evt;
const { pageX, pageY } = evt;
this._dragStartPos = [pageX, pageY];
this.props.onChangeViewport({isDragging: true});
this.props.onChangeViewport({ isDragging: true });
}
_onDrag(evt) {
if (this._dragStartPos) {
const {pageX, pageY} = evt;
const {width, height} = this.props;
const { pageX, pageY } = evt;
const { width, height } = this.props;
const dx = (pageX - this._dragStartPos[0]) / width;
const dy = (pageY - this._dragStartPos[1]) / height;
if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.metaKey) {
// 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 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.rotateY(newLookAt, newLookAt, lookAt, rotationY / 180 * Math.PI);
@@ -72,7 +86,7 @@ export default class OrbitController extends Component {
});
} else {
// rotate
const {rotationX, rotationY} = this.props;
const { rotationX, rotationY } = this.props;
const newRotationX = clamp(rotationX - dy * 180, -90, 90);
const newRotationY = (rotationY - dx * 180) % 360;
@@ -88,7 +102,7 @@ export default class OrbitController extends Component {
_onDragEnd() {
this._dragStartPos = null;
this.props.onChangeViewport({isDragging: false});
this.props.onChangeViewport({ isDragging: false });
}
_onWheel(evt) {
@@ -107,8 +121,12 @@ export default class OrbitController extends Component {
value = Math.floor(value / 4);
}
const {distance, minDistance, maxDistance} = this.props;
const newDistance = clamp(distance * Math.pow(1.01, value), minDistance, maxDistance);
const { distance, minDistance, maxDistance } = this.props;
const newDistance = clamp(
distance * Math.pow(1.01, value),
minDistance,
maxDistance
);
this.props.onChangeViewport({
distance: newDistance
@@ -117,7 +135,7 @@ export default class OrbitController extends Component {
// public API
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 newDistance = size / Math.tan(fov / 180 * Math.PI / 2) / 2;
@@ -128,16 +146,17 @@ export default class OrbitController extends Component {
render() {
return (
<div style={{position: 'relative', userSelect: 'none'}}
<div
style={{ position: "relative", userSelect: "none" }}
onMouseDown={this._onDragStart.bind(this)}
onMouseMove={this._onDrag.bind(this)}
onMouseLeave={this._onDragEnd.bind(this)}
onMouseUp={this._onDragEnd.bind(this)}
onWheel={this._onWheel.bind(this)} >
onWheel={this._onWheel.bind(this)}
>
{this.props.children}
</div>);
</div>
);
}
}

View File

@@ -1,30 +1,31 @@
import React, {PureComponent} from 'react';
// jshint esversion: 6
import React, { PureComponent } from "react";
export class Popup extends PureComponent {
render() {
const { title, x, y } = this.props;
const style = {
position: 'absolute',
position: "absolute",
top: y,
left: x,
maxWidth: '200px',
padding: '10px',
color: 'white',
backgroundColor: 'black',
pointerEvents: 'none',
transform: 'translate(10px, -50%)',
maxWidth: "200px",
padding: "10px",
color: "white",
backgroundColor: "black",
pointerEvents: "none",
transform: "translate(10px, -50%)"
};
const arrowStyle = {
position: 'absolute',
top: '50%',
left: '-14px',
width: '7px',
height: '5px',
boxSizing: 'border-box',
transform: 'translateY(-50%)',
border: '7px solid transparent',
borderRight: '7px solid black',
position: "absolute",
top: "50%",
left: "-14px",
width: "7px",
height: "5px",
boxSizing: "border-box",
transform: "translateY(-50%)",
border: "7px solid transparent",
borderRight: "7px solid black"
};
return (
@@ -32,13 +33,13 @@ export class Popup extends PureComponent {
<div style={arrowStyle} />
{title}
</div>
)
};
);
}
}
Popup.defaultProps = {
id: 'id',
title: '',
id: "id",
title: "",
x: 0,
y: 0,
y: 0
};

View File

@@ -1,3 +1,4 @@
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import Helmet from "react-helmet";
@@ -15,33 +16,30 @@ import actions from "../actions";
import SectionHeader from "./framework/sectionHeader";
@connect((state) => {
@connect(state => {
return {
cells: state.cells
}
};
})
class App extends React.Component {
constructor(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() {
/* 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.props.dispatch(actions.initialize())
this.props.dispatch(actions.initialize());
/*
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() {
@@ -50,32 +48,42 @@ class App extends React.Component {
return (
<Container>
<Helmet title="cellxgene" />
{
this.props.cells.loading ?
<div 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>
</div> :
null
}
{this.props.cells.loading ? (
<div
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>
</div>
) : 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>
<LeftSideBar/>
<div style={{
padding: 15,
backgroundColor: "#F7F7F7",
width: 1440 - 410 /* but responsive */,
marginLeft: 350 /* but responsive */
}}>
<Graph/>
<DynamicScatterplot/>
<LeftSideBar />
<div
style={{
padding: 15,
backgroundColor: "#F7F7F7",
width: 1440 - 410 /* but responsive */,
marginLeft: 350 /* but responsive */
}}
>
<Graph />
<DynamicScatterplot />
{/*<Parallel/>*/}
</div>
</div>
</Container>
)
);
}
};
}
export default App;

View File

@@ -1,22 +1,22 @@
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
import * as globals from "../../globals";
import styles from "./categorical.css";
import SectionHeader from "../framework/sectionHeader"
import SectionHeader from "../framework/sectionHeader";
import Value from "./value";
import { alphabeticallySortedValues } from "./util";
import FaArrowRight from 'react-icons/lib/fa/angle-right';
import FaArrowDown from 'react-icons/lib/fa/angle-down';
import FaPaintBrush from 'react-icons/lib/fa/paint-brush';
import FaArrowRight from "react-icons/lib/fa/angle-right";
import FaArrowDown from "react-icons/lib/fa/angle-down";
import FaPaintBrush from "react-icons/lib/fa/paint-brush";
@connect((state) => {
@connect(state => {
return {
colorAccessor: state.controls.colorAccessor
}
};
})
class Category extends React.Component {
constructor(props) {
@@ -31,22 +31,22 @@ class Category extends React.Component {
this.props.dispatch({
type: "color by categorical metadata",
colorAccessor: this.props.metadataField
})
});
}
toggleAll() {
this.props.dispatch({
type: "categorical metadata filter all of these",
metadataField: this.props.metadataField
})
this.setState({isChecked: true})
});
this.setState({ isChecked: true });
}
toggleNone() {
this.props.dispatch({
type: "categorical metadata filter none of these",
metadataField: this.props.metadataField,
value: this.props.value
})
this.setState({isChecked: false})
});
this.setState({ isChecked: false });
}
renderCategoryItems() {
return _.map(alphabeticallySortedValues(this.props.values), (v, i) => {
@@ -56,43 +56,60 @@ class Category extends React.Component {
metadataField={this.props.metadataField}
count={this.props.values[v]}
value={v}
i={i} />
)
})
i={i}
/>
);
});
}
render() {
return (
<div style={{
<div
style={{
// display: "flex",
// alignItems: "baseline",
maxWidth: globals.maxControlsWidth,
}}>
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline"
}}>
<p style={{
maxWidth: globals.maxControlsWidth
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline"
}}
>
<p
style={{
// flexShrink: 0,
fontWeight: 500,
// textAlign: "right",
// fontFamily: globals.accentFont,
// fontStyle: "italic",
margin: "3px 10px 3px 0px",
}}>
margin: "3px 10px 3px 0px"
}}
>
<span
style={{cursor: "pointer", display: "inline-block", position: "relative", top: 2}}
style={{
cursor: "pointer",
display: "inline-block",
position: "relative",
top: 2
}}
onClick={() => {
this.setState({isExpanded: !this.state.isExpanded})
}}>
{this.state.isExpanded ? <FaArrowDown /> : <FaArrowRight/> }
this.setState({ isExpanded: !this.state.isExpanded });
}}
>
{this.state.isExpanded ? <FaArrowDown /> : <FaArrowRight />}
</span>
{this.props.metadataField}
<input
onChange={this.state.isChecked ? this.toggleNone.bind(this) : this.toggleAll.bind(this)}
checked={this.state.isChecked}
type="checkbox"/>
onChange={
this.state.isChecked
? this.toggleNone.bind(this)
: this.toggleAll.bind(this)
}
checked={this.state.isChecked}
type="checkbox"
/>
<span
onClick={this.handleColorChange.bind(this)}
style={{
@@ -100,77 +117,69 @@ class Category extends React.Component {
marginLeft: 4,
// padding: this.props.colorAccessor === this.props.metadataField ? 3 : "auto",
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",
display: "inline-block",
position: "relative",
top: 2,
cursor: "pointer",
}}>
<FaPaintBrush/>
cursor: "pointer"
}}
>
<FaPaintBrush />
</span>
</p>
</div>
<div>
{
this.state.isExpanded ? this.renderCategoryItems() : null
}
</div>
<div>{this.state.isExpanded ? this.renderCategoryItems() : null}</div>
</div>
)
);
}
}
@connect((state) => {
const ranges = state.cells.cells && state.cells.cells.data.ranges ? state.cells.cells.data.ranges : null;
@connect(state => {
const ranges =
state.cells.cells && state.cells.cells.data.ranges
? state.cells.cells.data.ranges
: null;
return {
ranges
}
};
})
class Categories extends React.Component {
constructor(props) {
super(props);
this.state = {
};
this.state = {};
}
render () {
if (!this.props.ranges) return null
render() {
if (!this.props.ranges) return null;
return (
<div style={{
width: 310,
marginRight: 40,
paddingRight: 20,
flexShrink: 0,
// height: 700,
// overflow: "auto",
}}>
{
_.map(this.props.ranges, (value, key) => {
const isColorField = key.includes("color") || key.includes("Color");
if (
value.options &&
key !== "CellName" &&
!isColorField
) {
return (
<Category
key={key}
metadataField={key}
values={value.options}/>
)
}
})
}
<div
style={{
width: 310,
marginRight: 40,
paddingRight: 20,
flexShrink: 0
// height: 700,
// overflow: "auto",
}}
>
{_.map(this.props.ranges, (value, key) => {
const isColorField = key.includes("color") || key.includes("Color");
if (value.options && key !== "CellName" && !isColorField) {
return (
<Category key={key} metadataField={key} values={value.options} />
);
}
})}
</div>
)
);
}
};
}
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,

View File

@@ -1,7 +1,8 @@
export const alphabeticallySortedValues = (values) => {
// jshint esversion: 6
export const alphabeticallySortedValues = values => {
return Object.keys(values).sort((a, b) => {
var textA = a.toUpperCase();
var textB = b.toUpperCase();
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
})
}
return textA < textB ? -1 : textA > textB ? 1 : 0;
});
};

View File

@@ -1,17 +1,17 @@
// jshint esversion: 6
import { connect } from "react-redux";
import React from "react";
import * as globals from "../../globals";
import actions from "../../actions";
@connect((state) => {
@connect(state => {
return {
categoricalAsBooleansMap: state.controls.categoricalAsBooleansMap,
colorScale: state.controls.colorScale,
colorAccessor: state.controls.colorAccessor,
}
colorAccessor: state.controls.colorAccessor
};
})
class CategoryValue extends React.Component {
toggleOff() {
this.props.dispatch({
type: "categorical metadata filter deselect",
@@ -28,11 +28,16 @@ class CategoryValue extends React.Component {
});
}
render () {
if (!this.props.categoricalAsBooleansMap) return null
render() {
if (!this.props.categoricalAsBooleansMap) return null;
const selected = this.props.categoricalAsBooleansMap[this.props.metadataField][this.props.value]
const c = this.props.metadataField === this.props.colorAccessor /* this is the color scale, so add swatches below */
const selected = this.props.categoricalAsBooleansMap[
this.props.metadataField
][this.props.value];
const c =
this.props.metadataField ===
this.props
.colorAccessor; /* this is the color scale, so add swatches below */
return (
<div
@@ -41,32 +46,42 @@ class CategoryValue extends React.Component {
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
fontWeight: selected ? 700 : 400,
}}>
<p style={{
fontWeight: selected ? 700 : 400
}}
>
<p
style={{
paddingLeft: 15,
width: 200,
flexShrink: 0,
margin: 0,
lineHeight: "1em"
}}>
}}
>
<input
onChange={selected ? this.toggleOff.bind(this) : this.toggleOn.bind(this)}
onChange={
selected ? this.toggleOff.bind(this) : this.toggleOn.bind(this)
}
checked={selected}
type="checkbox"/>
{this.props.value}
type="checkbox"
/>
{this.props.value}
</p>
<p style={{
<p
style={{
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",
margin: 0,
lineHeight: "1em"
}}>
}}
>
{this.props.count}
</p>
</div>
)
);
}
}

View File

@@ -1,3 +1,4 @@
// jshint esversion: 6
/* rc slider https://www.npmjs.com/package/rc-slider */
import React from "react";

View File

@@ -1,9 +1,6 @@
import styles from './parallelCoordinates.css';
import {
yAxis,
brushstart,
} from "./util";
// jshint esversion: 6
import styles from "./parallelCoordinates.css";
import { yAxis, brushstart } from "./util";
const drawAxes = (
svg,
@@ -13,19 +10,19 @@ const drawAxes = (
height,
width,
handleBrushAction,
handleColorAction,
handleColorAction
) => {
/*****************************************
******************************************
Handles a brush event, toggling the display of foreground lines.
******************************************
******************************************/
function brush () {
function brush() {
var actives = [];
svg.selectAll(".parcoords_axis .parcoords_brush")
.filter(function (d) {
svg
.selectAll(".parcoords_axis .parcoords_brush")
.filter(function(d) {
return d3.brushSelection(this);
})
.each(function(d) {
@@ -35,46 +32,57 @@ const drawAxes = (
});
});
/* fire action, with selected dimensions & their values */
handleBrushAction(actives)
handleBrushAction(actives);
}
var axes = svg.selectAll(".parcoords_axis")
.data(dimensions)
.enter().append("g")
.attr("class", `${styles.axis} parcoords_axis`)
.attr("transform", (d,i) => { return "translate(" + xscale(i) + ")"; });
var axes = svg
.selectAll(".parcoords_axis")
.data(dimensions)
.enter()
.append("g")
.attr("class", `${styles.axis} parcoords_axis`)
.attr("transform", (d, i) => {
return "translate(" + xscale(i) + ")";
});
axes.append("g")
.each(function(d) {
var renderAxis = "axis" in d
? d.axis.scale(d.scale) // custom axis
: yAxis.scale(d.scale); // default axis
d3.select(this).call(renderAxis);
})
axes
.append("g")
.each(function(d) {
var renderAxis =
"axis" in d
? d.axis.scale(d.scale) // custom axis
: yAxis.scale(d.scale); // default axis
d3.select(this).call(renderAxis);
})
.append("text")
.on("click", (d) => { handleColorAction(d.key) })
.attr("class", styles.title)
.attr("text-anchor", "start")
.text(function(d) { return "description" in d ? d.description + " 🖌️" : d.key + " 🖌️"; });
.on("click", d => {
handleColorAction(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.
axes.append("g")
.attr("class", `${styles.brush} parcoords_brush`)
.each(function(d) {
d3.select(this).call(
d.brush = d3.brushY()
.extent([[-10,0], [10, height]])
.on("start", brushstart)
.on("brush", brush)
.on("end", brush)
)
})
axes
.append("g")
.attr("class", `${styles.brush} parcoords_brush`)
.each(function(d) {
d3.select(this).call(
(d.brush = d3
.brushY()
.extent([[-10, 0], [10, height]])
.on("start", brushstart)
.on("brush", brush)
.on("end", brush))
);
})
.selectAll("rect")
.attr("x", -8)
.attr("width", 16);
.attr("x", -8)
.attr("width", 16);
return axes;
}
};
export default drawAxes;

View File

@@ -1,7 +1,6 @@
// jshint esversion: 6
import _ from "lodash";
import {
project
} from "./util";
import { project } from "./util";
import renderQueue from "../../util/renderQueue";
@@ -16,16 +15,15 @@ const drawLinesCanvas = (
dimensions,
xscale,
colorAccessor,
colorScale,
colorScale
) => {
return (d) => {
ctx.globalAlpha = .1;
return d => {
ctx.globalAlpha = 0.1;
if (d["__selected__"]) {
ctx.strokeStyle = d["__color__"];
} else {
return
return;
}
ctx.beginPath();
@@ -36,31 +34,31 @@ const drawLinesCanvas = (
// this bit renders horizontal lines on the previous/next
// dimensions, so that sandwiched null values are visible
if (i > 0) {
var prev = coords[i-1];
var prev = coords[i - 1];
if (prev !== null) {
ctx.moveTo(prev[0],prev[1]);
ctx.lineTo(prev[0]+6,prev[1]);
ctx.moveTo(prev[0], prev[1]);
ctx.lineTo(prev[0] + 6, prev[1]);
}
}
if (i < coords.length-1) {
var next = coords[i+1];
if (i < coords.length - 1) {
var next = coords[i + 1];
if (next !== null) {
ctx.moveTo(next[0]-6,next[1]);
ctx.moveTo(next[0] - 6, next[1]);
}
}
return;
}
if (i == 0) {
ctx.moveTo(p[0],p[1]);
ctx.moveTo(p[0], p[1]);
return;
}
ctx.lineTo(p[0],p[1]);
ctx.lineTo(p[0], p[1]);
});
ctx.stroke();
}
}
};
};
const drawCellLinesUsingRenderQueue = (
metadata,
@@ -68,21 +66,14 @@ const drawCellLinesUsingRenderQueue = (
xscale,
ctx,
colorAccessor,
colorScale,
colorScale
) => {
const _renderLinesWithQueue = renderQueue(
drawLinesCanvas(
ctx,
dimensions,
xscale,
colorAccessor,
colorScale,
)
drawLinesCanvas(ctx, dimensions, xscale, colorAccessor, colorScale)
).rate(50);
_renderLinesWithQueue(metadata);
return _renderLinesWithQueue;
}
};
const drawCellLinesSync = (
metadata,
@@ -90,17 +81,17 @@ const drawCellLinesSync = (
xscale,
ctx,
colorAccessor,
colorScale,
colorScale
) => {
const _draw = drawLinesCanvas(
ctx,
dimensions,
xscale,
colorAccessor,
colorScale,
)
_.each(metadata, _draw)
}
colorScale
);
_.each(metadata, _draw);
};
export default drawCellLinesUsingRenderQueue;
// export default drawCellLinesUsingRenderQueue;

View File

@@ -3,22 +3,31 @@ https://bl.ocks.org/mbostock/4341954
https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172
https://bl.ocks.org/SpaceActuary/2f004899ea1b2bd78d6f1dbb2febf771
*/
import React from 'react';
// jshint esversion: 6
import React from "react";
import _ from "lodash";
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 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;
const initializeRanges =
state.initialize.data && state.initialize.data.data.ranges
? state.initialize.data.data.ranges
: null;
return {
colorAccessor: state.controls.colorAccessor,
colorScale: state.controls.colorScale,
currentCellSelection: state.controls.currentCellSelection,
}
currentCellSelection: state.controls.currentCellSelection
};
})
class HistogramBrush extends React.Component {
constructor(props) {
@@ -33,15 +42,11 @@ class HistogramBrush extends React.Component {
ctx: null,
axes: null,
dimensions: null,
brush: null,
brush: null
};
}
componentDidMount() {
}
componentDidUpdate() {
}
componentDidMount() {}
componentDidUpdate() {}
onBrush(selection, x) {
return () => {
if (d3.event.selection) {
@@ -57,81 +62,104 @@ class HistogramBrush extends React.Component {
range: null
});
}
}
};
}
drawHistogram(svgRef) {
const allValuesForContinuousFieldAsArray = _.map(
this.props.currentCellSelection,
this.props.metadataField
)
);
var x = d3.scaleLinear()
.domain(d3.extent(allValuesForContinuousFieldAsArray, (d) => +d))
.range([0, this.width])
// .range([margin.left, width - margin.right]);
var x = d3
.scaleLinear()
.domain(d3.extent(allValuesForContinuousFieldAsArray, d => +d))
.range([0, this.width]);
// .range([margin.left, width - margin.right]);
var y = d3.scaleLinear()
.range([this.height - this.marginBottom, 0])
// .range([height - margin.bottom, margin.top]);
var y = d3.scaleLinear().range([this.height - this.marginBottom, 0]);
// .range([height - margin.bottom, margin.top]);
const bins = d3.histogram()
.domain(x.domain())
.thresholds(40)(allValuesForContinuousFieldAsArray)
const bins = d3
.histogram()
.domain(x.domain())
.thresholds(40)(allValuesForContinuousFieldAsArray);
d3.select(svgRef)
d3
.select(svgRef)
.insert("g", "*")
.attr("fill", "#bbb")
.selectAll("rect")
.data(bins)
.enter().append("rect")
.attr("x", function(d) { return x(d.x0) + 1; })
.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); });
.enter()
.append("rect")
.attr("x", function(d) {
return x(d.x0) + 1;
})
.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) {
const brush = d3.select(svgRef)
.append('g')
.attr('class', 'brush')
const brush = d3
.select(svgRef)
.append("g")
.attr("class", "brush")
.call(
d3.brushX()
.on('end', this.onBrush(this.props.metadataField, x.invert).bind(this))
)
d3
.brushX()
.on(
"end",
this.onBrush(this.props.metadataField, x.invert).bind(this)
)
);
const xAxis = d3.select(svgRef)
const xAxis = d3
.select(svgRef)
.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + (this.height - this.marginBottom) + ")")
.call(
d3.axisBottom(x)
.ticks(5)
.attr(
"transform",
"translate(0," + (this.height - this.marginBottom) + ")"
)
.call(d3.axisBottom(x).ticks(5))
.append("text")
.attr("x", 300)
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "end")
.attr("font-weight", "bold")
.text(this.props.metadataField)
.attr("x", 300)
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "end")
.attr("font-weight", "bold")
.text(this.props.metadataField);
this.setState({brush, xAxis})
this.setState({ brush, xAxis });
}
}
render() {
return (
<div style={{marginTop: 10}} id={"histogram_" + this.props.metadataField}>
<svg width={this.width} height={this.height} ref={(svgRef) => { this.drawHistogram(svgRef)}}>
<div
style={{ marginTop: 10 }}
id={"histogram_" + this.props.metadataField}
>
<svg
width={this.width}
height={this.height}
ref={svgRef => {
this.drawHistogram(svgRef);
}}
>
{this.props.ranges.min}
{" to "}
{this.props.ranges.max}
</svg>
</div>
)
);
}
};
}
export default HistogramBrush;
export default HistogramBrush;

View File

@@ -1,29 +1,33 @@
// jshint esversion: 6
/* rc slider https://www.npmjs.com/package/rc-slider */
import React from 'react';
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
import styles from './parallelCoordinates.css';
import styles from "./parallelCoordinates.css";
import SectionHeader from "../framework/sectionHeader";
import setupParallelCoordinates from "./setupParallelCoordinates";
import drawAxes from "./drawAxes";
import drawLinesCanvas from "./drawLinesCanvas";
import {
margin,
width,
height,
createDimensions,
} from "./util";
import { 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 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;
const initializeRanges =
state.initialize.data && state.initialize.data.data.ranges
? state.initialize.data.data.ranges
: null;
return {
ranges,
@@ -33,8 +37,8 @@ import {
colorScale: state.controls.colorScale,
graphBrushSelection: state.controls.graphBrushSelection,
currentCellSelection: state.controls.currentCellSelection,
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn,
}
axesHaveBeenDrawn: state.controls.axesHaveBeenDrawn
};
})
class Parallel extends React.Component {
constructor(props) {
@@ -43,16 +47,12 @@ class Parallel extends React.Component {
svg: null,
ctx: null,
axes: null,
dimensions: null,
dimensions: null
};
}
componentDidMount() {
const {svg, ctx} = setupParallelCoordinates(
width,
height,
margin
);
this.setState({svg, ctx})
const { svg, ctx } = setupParallelCoordinates(width, height, margin);
this.setState({ svg, ctx });
}
componentWillReceiveProps(nextProps) {
this.maybeDrawAxes(nextProps);
@@ -63,10 +63,10 @@ class Parallel extends React.Component {
!this.state.axes &&
nextProps.initializeRanges /* axes are created on full range of data */
) {
const dimensions = createDimensions(nextProps.initializeRanges);
const xscale = d3.scalePoint()
const xscale = d3
.scalePoint()
.domain(d3.range(dimensions.length))
.range([0, width]);
@@ -78,28 +78,27 @@ class Parallel extends React.Component {
height,
width,
this.handleBrushAction.bind(this),
this.handleColorAction.bind(this),
this.handleColorAction.bind(this)
);
this.setState({
axes,
xscale,
dimensions,
})
dimensions
});
this.props.dispatch({
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 (
nextProps.ranges &&
nextProps.currentCellSelection &&
nextProps.axesHaveBeenDrawn
) {
if (this.state._drawLinesCanvas) {
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.ctx,
nextProps.colorAccessor,
nextProps.colorScale,
nextProps.colorScale
);
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({
type: "continuous selection using parallel coords brushing",
data: selection
})
});
}
handleColorAction (key) {
handleColorAction(key) {
this.props.dispatch({
type: "color by continuous metadata",
colorAccessor: key,
@@ -136,22 +135,21 @@ class Parallel extends React.Component {
}
render() {
return (
<div id="parcoords_wrapper">
<div
className={styles.parcoords}
id="parcoords"
style={{
width: width + margin.left + margin.right + "px",
width: width + margin.left + margin.right + "px",
height: height + margin.top + margin.bottom + "px"
}}></div>
}}
/>
</div>
)
);
}
};
}
export default Parallel;
// <SectionHeader text="Continuous Metadata"/>

View File

@@ -3,22 +3,19 @@
Setup SVG & Canvas elements
******************************************
******************************************/
// jshint esversion: 6
const setupParallelCoordinates = (width, height, margin) => {
var container = d3.select("#parcoords");
const setupParallelCoordinates = (
width,
height,
margin
) => {
var container = d3.select("#parcoords")
var svg = container.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
var svg = container
.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 + ")");
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var canvas = container.append("canvas")
var canvas = container
.append("canvas")
.attr("width", width * devicePixelRatio)
.attr("height", height * devicePixelRatio)
.style("width", width + "px")
@@ -27,16 +24,15 @@ const setupParallelCoordinates = (
.style("margin-left", margin.left + "px");
var ctx = canvas.node().getContext("2d");
ctx.globalCompositeOperation = 'darken';
ctx.globalAlpha = 0.15;
ctx.lineWidth = 1.5;
ctx.scale(devicePixelRatio, devicePixelRatio);
ctx.globalCompositeOperation = "darken";
ctx.globalAlpha = 0.15;
ctx.lineWidth = 1.5;
ctx.scale(devicePixelRatio, devicePixelRatio);
return {
svg,
ctx,
}
ctx
};
};
}
export default setupParallelCoordinates
export default setupParallelCoordinates;

View File

@@ -1,51 +1,57 @@
// jshint esversion: 6
import _ from "lodash";
const paddingRight = 120;
const continuousChartWidth = 1200;
export const margin = {top: 66, right: 110, bottom: 20, left: 60};
export const width = continuousChartWidth - margin.left - margin.right - paddingRight;
export const margin = { top: 66, right: 110, bottom: 20, left: 60 };
export const width =
continuousChartWidth - margin.left - margin.right - paddingRight;
export const height = 340 - margin.top - margin.bottom;
export const innerHeight = height - 2;
export const devicePixelRatio = window.devicePixelRatio || 1;
export const createDimensions = (data) => {
const newArr = []
export const createDimensions = data => {
const newArr = [];
_.each(data, (value, key) => {
if (value.range) {
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: {
within: (d, extent, dim) => {
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;
}
};
export const yAxis = d3.axisLeft();
export const brushstart = () => {
d3.event.sourceEvent.stopPropagation();
}
};
export const d3_functor = (v) => {
return typeof v === "function" ? v : () => { return v; };
export const d3_functor = v => {
return typeof v === "function"
? v
: () => {
return v;
};
};
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
if (
!(p.key in d) ||
d[p.key] === null
) return null;
if (!(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])];
});
};

View File

@@ -1,3 +1,4 @@
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
@@ -10,20 +11,22 @@ class CellSetButton extends React.Component {
set() {
const set = [];
_.each(this.props.currentCellSelection, (cell) => {
_.each(this.props.currentCellSelection, cell => {
if (cell["__selected__"]) {
set.push(cell.CellName)
set.push(cell.CellName);
}
})
});
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
})
});
}
render() {
return (
<span style={{marginRight: 10}}>
<span style={{ marginRight: 10 }}>
<button
style={{
color: "#FFF",
@@ -31,20 +34,34 @@ class CellSetButton extends React.Component {
height: 30,
backgroundColor: globals.brightBlue,
border: "none",
cursor: "pointer",
cursor: "pointer"
}}
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}}>
{
this.props.differential["celllist" + this.props.eitherCellSetOneOrTwo] ?
this.props.differential["celllist" + this.props.eitherCellSetOneOrTwo].length + " cells" :
0 + " cells"
}
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
}}
>
{this.props.differential[
"celllist" + this.props.eitherCellSetOneOrTwo
]
? this.props.differential[
"celllist" + this.props.eitherCellSetOneOrTwo
].length + " cells"
: 0 + " cells"}
</span>
</button>
</span>
)
);
}
}

View File

@@ -1,42 +1,45 @@
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
import * as globals from "../../globals";
import styles from "./expression.css";
import SectionHeader from "../framework/sectionHeader"
import SectionHeader from "../framework/sectionHeader";
import actions from "../../actions";
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 FaPaintBrush from 'react-icons/lib/fa/paint-brush';
import FaPaintBrush from "react-icons/lib/fa/paint-brush";
class HeatmapSquare extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.state = {
value: '',
}
value: ""
};
}
render() {
const contrastColor = getContrast(
this.props.backgroundColor
.substring(4, this.props.backgroundColor.length-1)
.substring(4, this.props.backgroundColor.length - 1)
.replace(/ /g, "")
.split(",")
)
);
return (
<p style={{
padding: "12px 6px",
textAlign: "center",
color: contrastColor,
width: 40,
flexShrink: 0,
fontSize: 12,
margin: 0,
backgroundColor: this.props.backgroundColor,
}}>
<p
style={{
padding: "12px 6px",
textAlign: "center",
color: contrastColor,
width: 40,
flexShrink: 0,
fontSize: 12,
margin: 0,
backgroundColor: this.props.backgroundColor
}}
>
{this.props.text}
</p>
)
);
}
}
@@ -47,72 +50,66 @@ class HeatmapSquare extends React.Component {
***********************************
***********************************
**********************************/
@connect((state) => {
@connect(state => {
return {
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
colorAccessor: state.controls.colorAccessor,
}
colorAccessor: state.controls.colorAccessor
};
})
class HeatmapRow extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.state = {
value: '',
}
value: ""
};
}
handleGeneColorScaleClick(gene) {
return () => {
this.props.dispatch(
actions.requestSingleGeneExpressionCountsForColoringPOST(this.props.gene)
)
}
actions.requestSingleGeneExpressionCountsForColoringPOST(
this.props.gene
)
);
};
}
handleSetGeneAsScatterplotX(gene) {
return () => {
this.props.dispatch({
type: "set scatterplot x",
data: this.props.gene
})
}
});
};
}
handleSetGeneAsScatterplotY(gene) {
return () => {
this.props.dispatch({
type: "set scatterplot y",
data: this.props.gene
})
}
});
};
}
render() {
return (
<div style={{
width: 220,
display: "flex",
justifyContent: "flex-start",
alignItems: "baseline",
}}>
<div style={{width: 150, flexShrink: 0}}>
<span
onClick={this.handleSetGeneAsScatterplotX(this.props.gene).bind(this)}
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>
<div
style={{
width: 220,
display: "flex",
justifyContent: "flex-start",
alignItems: "baseline"
}}
>
<div style={{ width: 150, flexShrink: 0 }}>
<span
onClick={this.handleSetGeneAsScatterplotY(this.props.gene).bind(this)}
onClick={this.handleSetGeneAsScatterplotX(this.props.gene).bind(
this
)}
style={{
fontSize: 16,
color: this.props.scatterplotYYaccessor === this.props.gene ? "white" : globals.brightBlue,
color:
this.props.scatterplotXXaccessor === this.props.gene
? "white"
: globals.brightBlue,
cursor: "pointer",
position: "relative",
top: 1,
@@ -120,8 +117,39 @@ class HeatmapRow extends React.Component {
marginRight: 4,
borderRadius: 3,
padding: "2px 3px",
backgroundColor: this.props.scatterplotYYaccessor === this.props.gene ? globals.brightBlue : "inherit",
}}>Y</span>
backgroundColor:
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
onClick={this.handleGeneColorScaleClick(this.props.gene).bind(this)}
style={{
@@ -131,32 +159,45 @@ class HeatmapRow extends React.Component {
marginRight: 6,
borderRadius: 3,
padding: "0px 2px 2px 2px",
color: this.props.colorAccessor === this.props.gene ? "white" : "inherit",
backgroundColor: this.props.colorAccessor === this.props.gene ? globals.brightBlue : "inherit",
}}><FaPaintBrush style={{display: "inline-block"}}/></span>
color:
this.props.colorAccessor === this.props.gene
? "white"
: "inherit",
backgroundColor:
this.props.colorAccessor === this.props.gene
? globals.brightBlue
: "inherit"
}}
>
<FaPaintBrush style={{ display: "inline-block" }} />
</span>
<span
style={{
fontSize: 14
}}>
}}
>
{this.props.gene}
</span>
</div>
<HeatmapSquare
backgroundColor={this.props.greyColorScale(this.props.set1exp)}
text={this.props.set1exp}/>
text={this.props.set1exp}
/>
<HeatmapSquare
backgroundColor={this.props.greyColorScale(this.props.set2exp)}
text={this.props.set2exp}/>
text={this.props.set2exp}
/>
<span
title={this.props.aveDiff}
style={{
fontSize: 14,
paddingLeft: 10
}}>
{this.props.aveDiff.toFixed(2)}
}}
>
{this.props.aveDiff.toFixed(2)}
</span>
</div>
)
);
}
}
@@ -168,21 +209,22 @@ class HeatmapRow extends React.Component {
***********************************
**********************************/
@connect((state) => {
@connect(state => {
return {
differential: state.differential,
allGeneNames: state.controls.allGeneNames
}
};
})
class Heatmap extends React.Component {
constructor (props) {
super(props)
constructor(props) {
super(props);
this.state = {
value: '',
}
value: ""
};
}
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 topGenesForCellSet2 = this.props.differential.diffExp.data.celllist2;
@@ -194,71 +236,86 @@ class Heatmap extends React.Component {
topGenesForCellSet2.mean_expression_cellset1,
topGenesForCellSet2.mean_expression_cellset2
)
)
);
const greyColorScale = d3.scaleSequential()
.domain(extent)
.interpolator(d3.interpolateGreys);
const greyColorScale = d3
.scaleSequential()
.domain(extent)
.interpolator(d3.interpolateGreys);
return (
<div>
Color by any gene:
<ReactAutocomplete
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}
renderItem={(item, highlighted) =>
renderItem={(item, highlighted) => (
<div
key={item}
style={{ backgroundColor: highlighted ? '#eee' : 'transparent'}}
style={{ backgroundColor: highlighted ? "#eee" : "transparent" }}
>
{item}
</div>
}
)}
value={this.state.value}
onChange={e => this.setState({ value: e.target.value })}
onSelect={(value) => {
onSelect={value => {
this.setState({ value });
this.props.dispatch(actions.requestSingleGeneExpressionCountsForColoringPOST(value))
this.props.dispatch(
actions.requestSingleGeneExpressionCountsForColoringPOST(value)
);
}}
/>
<div style={{
display: "flex",
justifyContent: "flex-start",
width: 400,
fontWeight: 700,
}}>
<p style={{marginRight: 110}}>Gene</p>
<p style={{marginRight: 25}}>1</p>
<p style={{marginRight: 20}}>2</p>
<div
style={{
display: "flex",
justifyContent: "flex-start",
width: 400,
fontWeight: 700
}}
>
<p style={{ marginRight: 110 }}>Gene</p>
<p style={{ marginRight: 25 }}>1</p>
<p style={{ marginRight: 20 }}>2</p>
<p>ave diff</p>
</div>
{
topGenesForCellSet1.topgenes.map((gene, i) => {
return <HeatmapRow
{topGenesForCellSet1.topgenes.map((gene, i) => {
return (
<HeatmapRow
key={gene}
gene={gene}
greyColorScale={greyColorScale}
aveDiff={topGenesForCellSet1.ave_diff[i]}
set1exp={Math.floor(topGenesForCellSet1.mean_expression_cellset1[i])}
set2exp={Math.floor(topGenesForCellSet1.mean_expression_cellset2[i])}
/>
})
}
{
topGenesForCellSet2.topgenes.map((gene, i) => {
return <HeatmapRow
set1exp={Math.floor(
topGenesForCellSet1.mean_expression_cellset1[i]
)}
set2exp={Math.floor(
topGenesForCellSet1.mean_expression_cellset2[i]
)}
/>
);
})}
{topGenesForCellSet2.topgenes.map((gene, i) => {
return (
<HeatmapRow
key={gene}
gene={gene}
greyColorScale={greyColorScale}
aveDiff={topGenesForCellSet2.ave_diff[i]}
set1exp={Math.floor(topGenesForCellSet2.mean_expression_cellset1[i])}
set2exp={Math.floor(topGenesForCellSet2.mean_expression_cellset2[i])}
/>
})
}
set1exp={Math.floor(
topGenesForCellSet2.mean_expression_cellset1[i]
)}
set2exp={Math.floor(
topGenesForCellSet2.mean_expression_cellset2[i]
)}
/>
);
})}
</div>
)
);
}
}

View File

@@ -1,3 +1,4 @@
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
@@ -5,26 +6,24 @@ import * as globals from "../../globals";
import actions from "../../actions";
import CellSetButton from "./cellSetButtons";
@connect((state) => {
@connect(state => {
return {
currentCellSelection: state.controls.currentCellSelection,
differential: state.differential
}
};
})
class Expression extends React.Component {
constructor(props) {
super(props);
this.state = {
};
this.state = {};
}
handleClick(gene) {
return () => {
this.props.dispatch({
type: "color by expression",
gene: gene,
gene: gene
});
}
};
}
computeDiffExp() {
this.props.dispatch(
@@ -32,36 +31,28 @@ class Expression extends React.Component {
this.props.differential.celllist1,
this.props.differential.celllist2
)
)
);
}
render () {
if (
!this.props.differential
) {
return null
render() {
if (!this.props.differential) {
return null;
}
return (
<div>
<div style={{margin: 10}}>
<div style={{marginBottom: 10, width: 300}}>
There are currently
{
" " +
_.filter(this.props.currentCellSelection, "__selected__").length +
" "
}
cells selected, click a cell set button to store them.
</div>
<CellSetButton
{...this.props}
eitherCellSetOneOrTwo={1}/>
<CellSetButton
{...this.props}
eitherCellSetOneOrTwo={2}/>
<div style={{ margin: 10 }}>
<div style={{ marginBottom: 10, width: 300 }}>
There are currently
{" " +
_.filter(this.props.currentCellSelection, "__selected__").length +
" "}
cells selected, click a cell set button to store them.
</div>
<CellSetButton {...this.props} eitherCellSetOneOrTwo={1} />
<CellSetButton {...this.props} eitherCellSetOneOrTwo={2} />
</div>
<div>
{
this.props.differential.celllist1 && this.props.differential.celllist2 ?
{this.props.differential.celllist1 &&
this.props.differential.celllist2 ? (
<button
style={{
fontSize: 18,
@@ -71,11 +62,13 @@ class Expression extends React.Component {
padding: "12px 20px",
backgroundColor: globals.brightBlue,
border: "none",
cursor: "pointer",
cursor: "pointer"
}}
onClick={this.computeDiffExp.bind(this)}>
onClick={this.computeDiffExp.bind(this)}
>
Compute differential expression
</button> :
</button>
) : (
<button
style={{
fontSize: 18,
@@ -84,17 +77,16 @@ class Expression extends React.Component {
color: "#FFF",
padding: "12px 20px",
backgroundColor: globals.mediumGrey,
border: "none",
border: "none"
}}
>
>
Compute differential expression
</button>
}
)}
</div>
</div>
)
);
}
};
}
export default Expression;

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 => (
<div className={styles.container}>
{props.children}
</div>
<div className={styles.container}>{props.children}</div>
);
export default Container;

View File

@@ -1,14 +1,12 @@
import React from 'react';
import Container from './container';
import styles from './header.css';
// jshint esversion: 6
import React from "react";
import Container from "./container";
import styles from "./header.css";
const Header = () => (
<header className={styles.header}>
<Container>
</Container>
<Container />
</header>
);

View File

@@ -1,10 +1,13 @@
import React from 'react';
// jshint esversion: 6
import React from "react";
const SectionHeader = ({text}) => (
<p style={{
fontSize: 32,
fontWeight: 700
}}>
const SectionHeader = ({ text }) => (
<p
style={{
fontSize: 32,
fontWeight: 700
}}
>
{text}
</p>
);

View File

@@ -1,3 +1,4 @@
// jshint esversion: 6
import styles from "./graph.css";
import renderQueue from "../../util/renderQueue";
import _ from "lodash";
@@ -13,7 +14,6 @@ export const setupGraphElements = (
handleBrushSelectAction,
handleBrushDeselectAction
) => {
// var canvas = d3.select("#graphAttachPoint")
// .append("canvas")
// .attr("width", globals.graphWidth)
@@ -22,25 +22,22 @@ export const setupGraphElements = (
// 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("height", globals.graphHeight)
.attr("class", `${styles.graphSVG}`)
// .append("g")
// .attr("transform", "translate(" + margin.left + " " + margin.top + ")");
.attr("class", `${styles.graphSVG}`);
// .append("g")
// .attr("transform", "translate(" + margin.left + " " + margin.top + ")");
setupGraphBrush(
svg,
handleBrushSelectAction,
handleBrushDeselectAction
);
setupGraphBrush(svg, handleBrushSelectAction, handleBrushDeselectAction);
return {
svg,
svg
// ctx
}
}
};
};
/******************************************
*******************************************
@@ -57,36 +54,34 @@ const drawGraph = (
currentCellSelection,
graphBrushSelection,
colorScale,
graphMap, /* tmp remove when structure exists on server */
graphMap /* tmp remove when structure exists on server */,
opacityForDeselectedCells,
_currentCellSelectionMap,
_currentCellSelectionMap
) => {
return (p) => {
return p => {
/* shuffle the data to overcome render order hiding cells, & filter first */
// data = d3.shuffle(data); /* make me a control */
context.beginPath();
/* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */
context.arc(
globals.graphXScale(p[1]), /* x */
globals.graphYScale(p[2]), /* y */
_currentCellSelectionMap[p[0]]["__selected__"] ? 3 : 1.5, /* r */
0, /* sAngle */
2 * Math.PI /* eAngle */
);
context.beginPath();
/* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */
context.arc(
globals.graphXScale(p[1]) /* x */,
globals.graphYScale(p[2]) /* y */,
_currentCellSelectionMap[p[0]]["__selected__"] ? 3 : 1.5 /* r */,
0 /* sAngle */,
2 * Math.PI /* eAngle */
);
context.fillStyle = _currentCellSelectionMap[p[0]]["__color__"]
context.fillStyle = _currentCellSelectionMap[p[0]]["__color__"];
if (_currentCellSelectionMap[p[0]]["__selected__"]) {
context.globalAlpha = 1;
} else {
context.globalAlpha = opacityForDeselectedCells;
}
if (_currentCellSelectionMap[p[0]]["__selected__"]) {
context.globalAlpha = 1;
} else {
context.globalAlpha = opacityForDeselectedCells;
}
context.fill();
}
}
context.fill();
};
};
const _drawGraphUsingRenderQueue = (
context,
@@ -97,22 +92,26 @@ const _drawGraphUsingRenderQueue = (
currentCellSelection,
graphBrushSelection,
colorScale,
graphMap, /* tmp remove when structure exists on server */
opacityForDeselectedCells,
graphMap /* tmp remove when structure exists on server */,
opacityForDeselectedCells
) => {
const _currentCellSelectionMap = _.keyBy(currentCellSelection, "CellName"); /* move me to the reducer */
const _currentCellSelectionMap = _.keyBy(
currentCellSelection,
"CellName"
); /* move me to the reducer */
const dataForGraph = [];
_.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([
cell["CellName"],
graphMap[cell["CellName"]][0],
graphMap[cell["CellName"]][1]
])
]);
}
})
});
/* clear canvas */
context.clearRect(0, 0, globals.graphWidth, globals.graphHeight);
@@ -127,30 +126,30 @@ const _drawGraphUsingRenderQueue = (
currentCellSelection,
graphBrushSelection,
colorScale,
graphMap, /* tmp remove when structure exists on server */
graphMap /* tmp remove when structure exists on server */,
opacityForDeselectedCells,
_currentCellSelectionMap,
_currentCellSelectionMap
)
)
);
_renderGraphWithFunctionReturnedByQueue(dataForGraph);
return _renderGraphWithFunctionReturnedByQueue;
}
};
export const drawGraphUsingRenderQueue = _.debounce(_drawGraphUsingRenderQueue, 100);
export const drawGraphUsingRenderQueue = _.debounce(
_drawGraphUsingRenderQueue,
100
);
const setupGraphBrush = (
svg,
handleBrushSelectAction,
handleBrushDeselectAction
) => {
svg.append("g")
.call(
d3.brush()
.extent([
[0, 0],
[globals.graphWidth, globals.graphHeight]
])
.on("brush", handleBrushSelectAction)
.on("end", handleBrushDeselectAction)
);
}
svg.append("g").call(
d3
.brush()
.extent([[0, 0], [globals.graphWidth, globals.graphHeight]])
.on("brush", handleBrushSelectAction)
.on("end", handleBrushDeselectAction)
);
};

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
export default function (regl) {
export default function(regl) {
return regl({
vert: `
precision mediump float;
@@ -29,14 +30,14 @@ export default function (regl) {
}`,
attributes: {
position: regl.prop('position'),
color: regl.prop('color'),
size: regl.prop('size')
position: regl.prop("position"),
color: regl.prop("color"),
size: regl.prop("size")
},
uniforms: {
distance: regl.prop('distance'),
view: regl.prop('view'),
distance: regl.prop("distance"),
view: regl.prop("view"),
projection: (context, props) => {
return mat4.perspective(
[],
@@ -44,12 +45,12 @@ export default function (regl) {
context.viewportWidth * props.scale / context.viewportHeight,
0.01,
1000
)
},
);
}
},
count: regl.prop('count'),
count: regl.prop("count"),
primitive: 'points'
})
primitive: "points"
});
}

View File

@@ -1,29 +1,38 @@
import React from 'react';
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import * as globals from "../../globals";
import styles from "./graph.css";
import {setupGraphElements, drawGraphUsingRenderQueue} from "./drawGraph";
import { setupGraphElements, drawGraphUsingRenderQueue } from "./drawGraph";
import SectionHeader from "../framework/sectionHeader";
import { connect } from "react-redux";
import actions from "../../actions";
import mat4 from 'gl-mat4';
import fit from 'canvas-fit';
import _camera from '../../util/camera.js'
import _regl from 'regl'
import _drawPoints from './drawPointsRegl'
import mat4 from "gl-mat4";
import fit from "canvas-fit";
import _camera from "../../util/camera.js";
import _regl from "regl";
import _drawPoints from "./drawPointsRegl";
import FaCrosshair from 'react-icons/lib/fa/crosshairs';
import FaZoom from 'react-icons/lib/fa/search-plus';
import FaSave from 'react-icons/lib/fa/download';
import FaCrosshair from "react-icons/lib/fa/crosshairs";
import FaZoom from "react-icons/lib/fa/search-plus";
import FaSave from "react-icons/lib/fa/download";
/* https://bl.ocks.org/mbostock/9078690 - quadtree for onClick / hover selections */
@connect((state) => {
const vertices = state.cells.cells && state.cells.cells.data.graph ? state.cells.cells.data.graph : 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;
@connect(state => {
const vertices =
state.cells.cells && state.cells.cells.data.graph
? state.cells.cells.data.graph
: 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 {
ranges,
@@ -35,11 +44,10 @@ import FaSave from 'react-icons/lib/fa/download';
graphMap: state.controls.graphMap,
currentCellSelection: state.controls.currentCellSelection,
graphBrushSelection: state.controls.graphBrushSelection,
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
}
opacityForDeselectedCells: state.controls.opacityForDeselectedCells
};
})
class Graph extends React.Component {
constructor(props) {
super(props);
this.count = 0;
@@ -48,35 +56,32 @@ class Graph extends React.Component {
svg: null,
ctx: null,
brush: null,
mode: "brush",
mode: "brush"
};
}
componentDidMount() {
const {
svg
} = setupGraphElements(
const { svg } = setupGraphElements(
this.handleBrushSelectAction.bind(this),
this.handleBrushDeselectAction.bind(this)
);
this.setState({svg});
this.setState({ svg });
// setup canvas and camera
const camera = _camera(this.reglCanvas, {scale: true, rotate: false});
const regl = _regl(this.reglCanvas)
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
const regl = _regl(this.reglCanvas);
const drawPoints = _drawPoints(regl)
const drawPoints = _drawPoints(regl);
// preallocate buffers
const pointBuffer = regl.buffer();
const colorBuffer = regl.buffer();
const sizeBuffer = regl.buffer();
regl.frame(({viewportWidth, viewportHeight}) => {
regl.frame(({ viewportWidth, viewportHeight }) => {
regl.clear({
depth: 1,
color: [1, 1, 1, 1]
})
});
drawPoints({
size: sizeBuffer,
@@ -86,18 +91,17 @@ class Graph extends React.Component {
count: this.count,
view: camera.view(),
scale: viewportHeight / viewportWidth
})
});
camera.tick()
})
camera.tick();
});
this.setState({
regl,
pointBuffer,
colorBuffer,
sizeBuffer
})
});
}
componentWillReceiveProps(nextProps) {
/* maybe should do a check here to confirm ref exists and pass it? */
@@ -120,12 +124,11 @@ class Graph extends React.Component {
// nextProps.opacityForDeselectedCells,
// )
// }
if (
this.state.regl &&
nextProps.vertices
) {
const _currentCellSelectionMap = _.keyBy(nextProps.currentCellSelection, "CellName"); /* move me to the reducer */
if (this.state.regl && nextProps.vertices) {
const _currentCellSelectionMap = _.keyBy(
nextProps.currentCellSelection,
"CellName"
); /* move me to the reducer */
const positions = [];
positions.length = nextProps.currentCellSelection.length;
@@ -134,33 +137,38 @@ class Graph extends React.Component {
const sizes = [];
sizes.length = nextProps.currentCellSelection.length;
const glScaleX = d3.scaleLinear()
.domain([0,1])
.range([-1, 1]) /* padding */
const glScaleY = d3.scaleLinear()
const glScaleX = d3
.scaleLinear()
.domain([0, 1])
.range([1, -1]) /* padding */
.range([-1, 1]); /* padding */
const glScaleY = d3
.scaleLinear()
.domain([0, 1])
.range([1, -1]); /* padding */
/*
Construct Vectors
*/
_.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] = [
glScaleX(nextProps.graphMap[cell["CellName"]][0]),
glScaleY(nextProps.graphMap[cell["CellName"]][1])
]
];
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.colorBuffer(colors)
this.state.sizeBuffer(sizes)
this.count = positions.length
this.state.pointBuffer(positions);
this.state.colorBuffer(colors);
this.state.sizeBuffer(sizes);
this.count = positions.length;
}
}
handleBrushSelectAction() {
@@ -183,7 +191,7 @@ class Graph extends React.Component {
northwestY: s[0][1],
southeastX: s[1][0],
southeastY: s[1][1]
}
};
brushCoords.dx = brushCoords.southeastX - brushCoords.northwestX;
brushCoords.dy = brushCoords.southeastY - brushCoords.northwestY;
@@ -191,20 +199,20 @@ class Graph extends React.Component {
this.props.dispatch({
type: "graph brush selection change",
brushCoords
})
});
}
handleBrushDeselectAction() {
if (!d3.event.selection) {
this.props.dispatch({
type: "graph brush deselect"
})
});
}
}
handleOpacityRangeChange(e) {
this.props.dispatch({
type: "change opacity deselected cells in 2d graph background",
data: e.target.value
})
});
}
render() {
@@ -212,20 +220,24 @@ class Graph extends React.Component {
<div
id="graphWrapper"
style={{
height: 1050, /* move this to globals */
height: 1050 /* move this to globals */,
backgroundColor: "white",
borderRadius: 3,
boxShadow: "3px 4px 13px 0px rgba(201,201,201,1)",
}}>
boxShadow: "3px 4px 13px 0px rgba(201,201,201,1)"
}}
>
<div
style={{
padding: 10,
display: "flex",
justifyContent: "space-between",
alignItems: "baseline"
}}>
}}
>
<button
onClick={() => { this.props.dispatch(actions.regraph()) }}
onClick={() => {
this.props.dispatch(actions.regraph());
}}
style={{
fontSize: 12,
fontWeight: 400,
@@ -233,67 +245,96 @@ class Graph extends React.Component {
padding: "10px 20px",
backgroundColor: globals.brightBlue,
border: "none",
cursor: "pointer",
cursor: "pointer"
}}
>
Regraph present selection
Regraph present selection
</button>
<div>
<span style={{ marginRight: 10, fontSize: 12}}>
<span style={{ marginRight: 10, fontSize: 12 }}>
deselected opacity
</span>
<input
style={{position: "relative", top: 6, marginRight: 20}}
style={{ position: "relative", top: 6, marginRight: 20 }}
type="range"
onChange={this.handleOpacityRangeChange.bind(this)}
min={0}
max={1}
step="0.01"
/>
<span style={{position: "relative", top: 3}}>
<button
onClick={() => { this.setState({mode: "brush"}) }}
style={{
cursor: "pointer",
border: this.state.mode === "brush" ? "1px solid black" : "1px solid white",
backgroundColor: "white",
padding: 5,
borderRadius: 3
}}> <FaCrosshair/> </button>
<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>
/>
<span style={{ position: "relative", top: 3 }}>
<button
onClick={() => {
this.setState({ mode: "brush" });
}}
style={{
cursor: "pointer",
border:
this.state.mode === "brush"
? "1px solid black"
: "1px solid white",
backgroundColor: "white",
padding: 5,
borderRadius: 3
}}
>
{" "}
<FaCrosshair />{" "}
</button>
<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>
<button style={{
fontSize: 12,
fontWeight: 400,
color: "white",
padding: "10px 20px",
backgroundColor: globals.brightBlue,
border: "none",
cursor: "pointer",
}}> <FaSave style={{display: "inline-block"}}/> csv url for present selection </button>
<button
style={{
fontSize: 12,
fontWeight: 400,
color: "white",
padding: "10px 20px",
backgroundColor: globals.brightBlue,
border: "none",
cursor: "pointer"
}}
>
{" "}
<FaSave style={{ display: "inline-block" }} /> csv url for present
selection{" "}
</button>
</div>
</div>
<div
style={{display: this.state.mode === "brush" ? "inherit" : "none"}}
style={{ display: this.state.mode === "brush" ? "inherit" : "none" }}
id="graphAttachPoint"
>
</div>
<div style={{padding: 0, margin: 0}}>
<canvas width={globals.graphWidth} height={globals.graphHeight} ref={(canvas) => { this.reglCanvas = canvas}}/>
/>
<div style={{ padding: 0, margin: 0 }}>
<canvas
width={globals.graphWidth}
height={globals.graphHeight}
ref={canvas => {
this.reglCanvas = canvas;
}}
/>
</div>
</div>
)
);
}
};
}
export default Graph;

View File

@@ -1,3 +1,4 @@
// jshint esversion: 6
// createExpressionsCountsMap () {
//
// const CHANGE_ME_MAGIC_GENE_INDEX = 5;

View File

@@ -1,3 +1,4 @@
// jshint esversion: 6
import styles from "./joy.css";
/*
@@ -5,112 +6,158 @@ import styles from "./joy.css";
*/
var margin = { top: 30, right: 10, bottom: 30, left: 100 },
width = 400 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
width = 400 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
// Percent two area charts can overlap
var overlap = 0.4;
var formatTime = d3.timeFormat('%I %p');
var formatTime = d3.timeFormat("%I %p");
var x = function(d) { return d.time; },
xScale = d3.scaleTime().range([0, width]),
xValue = function(d) { return xScale(x(d)); },
xAxis = d3.axisBottom(xScale).tickFormat(formatTime);
var x = function(d) {
return d.time;
},
xScale = d3.scaleTime().range([0, width]),
xValue = function(d) {
return xScale(x(d));
},
xAxis = d3.axisBottom(xScale).tickFormat(formatTime);
var y = function(d) { return d.value; },
yScale = d3.scaleLinear(),
yValue = function(d) { return yScale(y(d)); };
var y = function(d) {
return d.value;
},
yScale = d3.scaleLinear(),
yValue = function(d) {
return yScale(y(d));
};
var activity = function(d) { return d.key; },
activityScale = d3.scaleBand().range([0, height]),
activityValue = function(d) { return activityScale(activity(d)); },
activityAxis = d3.axisLeft(activityScale);
var activity = function(d) {
return d.key;
},
activityScale = d3.scaleBand().range([0, height]),
activityValue = function(d) {
return activityScale(activity(d));
},
activityAxis = d3.axisLeft(activityScale);
var area = d3.area()
.x(xValue)
.y1(yValue);
var area = d3
.area()
.x(xValue)
.y1(yValue);
var line = area.lineY1();
function parseTime(offset) {
var date = new Date(2017, 0, 1); // chose an arbitrary day
return d3.timeMinute.offset(date, offset);
var date = new Date(2017, 0, 1); // chose an arbitrary day
return d3.timeMinute.offset(date, offset);
}
function row(d) {
return {
activity: d.activity,
time: parseTime(d.time),
value: +d.p_smooth
};
return {
activity: d.activity,
time: parseTime(d.time),
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')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
d3.tsv(
"https://gist.githubusercontent.com/armollica/3b5f83836c1de5cca7b1d35409a013e3/raw/783d1bbc2dd3aabbfcba83ece4a670de6f1ec371/data.tsv",
row,
function(error, dataFlat) {
// Sort by time
dataFlat.sort(function(a, b) {
return a.time - b.time;
});
d3.tsv("https://gist.githubusercontent.com/armollica/3b5f83836c1de5cca7b1d35409a013e3/raw/783d1bbc2dd3aabbfcba83ece4a670de6f1ec371/data.tsv", row, function(error, dataFlat) {
// Sort by time
dataFlat.sort(function(a, b) { return a.time - b.time; });
var data = d3.nest()
.key(function(d) { return d.activity; })
var data = d3
.nest()
.key(function(d) {
return d.activity;
})
.entries(dataFlat);
// Sort activities by peak activity time
function peakTime(d) {
var i = d3.scan(d.values, function(a, b) { return y(b) - y(a); });
// Sort activities by peak activity time
function peakTime(d) {
var i = d3.scan(d.values, function(a, b) {
return y(b) - y(a);
});
return d.values[i].time;
};
data.sort(function(a, b) { return peakTime(b) - peakTime(a); });
}
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
.domain(d3.extent(dataFlat, y))
.range([areaChartHeight, 0]);
yScale.domain(d3.extent(dataFlat, y)).range([areaChartHeight, 0]);
area.y0(yScale(0));
area.y0(yScale(0));
var gActivity = svg.append('g').attr('class', "activities")
.selectAll('.activity').data(data)
.enter().append('g')
.attr('class', function(d) { return `${styles.activity} ${styles.activity["--"+d.key]}`; })
.attr('transform', function(d) {
var ty = activityValue(d) - activityScale.bandwidth() + 5;
return 'translate(0,' + ty + ')';
});
var gActivity = svg
.append("g")
.attr("class", "activities")
.selectAll(".activity")
.data(data)
.enter()
.append("g")
.attr("class", function(d) {
return `${styles.activity} ${styles.activity["--" + d.key]}`;
})
.attr("transform", function(d) {
var ty = activityValue(d) - activityScale.bandwidth() + 5;
return "translate(0," + ty + ")";
});
gActivity.append('path').attr('class', styles.area)
.datum(function(d) { return d.values; })
.attr('d', area);
gActivity
.append("path")
.attr("class", styles.area)
.datum(function(d) {
return d.values;
})
.attr("d", area);
gActivity.append('path').attr('class', styles.line)
.datum(function(d) { return d.values; })
.attr('d', line);
gActivity
.append("path")
.attr("class", styles.line)
.datum(function(d) {
return d.values;
})
.attr("d", line);
svg.append('g').attr('class', `${styles.axis} ${styles["axis--x"]}`)
.attr('transform', 'translate(0,' + height + ')')
svg
.append("g")
.attr("class", `${styles.axis} ${styles["axis--x"]}`)
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append('g').attr('class', `${styles.axis} ${styles["axis--activity"]}`)
svg
.append("g")
.attr("class", `${styles.axis} ${styles["axis--activity"]}`)
.call(activityAxis);
})
}
}
);
};
export default drawJoy;

View File

@@ -1,38 +1,38 @@
import React from 'react';
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import styles from "./joy.css";
import drawJoy from "./drawJoy";
import joyParser from "./joyParser";
class Joy extends React.Component {
constructor(props) {
super(props);
this.state = {
};
this.state = {};
}
componentWillReceiveProps(nextProps) {
if (nextProps.data) {
console.log('joyplot data 44', nextProps.data)
console.log("joyplot data 44", nextProps.data);
drawJoy(joyParser(nextProps.data));
}
}
componentDidMount() {
}
componentDidMount() {}
render() {
return (
<div id="joyplot_wrapper" style={{marginTop: 50}}>
<div id="joyplot_wrapper" style={{ marginTop: 50 }}>
<h3> Joy </h3>
<p> Cell expression distribution per gene & if differential expression, Ie., cells for cluster 5, top genes expressed by cluster 8</p>
<p>
{" "}
Cell expression distribution per gene & if differential expression,
Ie., cells for cluster 5, top genes expressed by cluster 8
</p>
<div id="joyplot"> </div>
</div>
)
);
}
};
}
export default Joy;

View File

@@ -1,6 +1,4 @@
// jshint esversion: 6
const joyParser = (data, count = 20) => {
const genes = [];
@@ -9,22 +7,23 @@ const joyParser = (data, count = 20) => {
for (let i = 0; i < count; i++) {
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: []
}
};
data.cells.forEach((cell) => {
data.cells.forEach(cell => {
gene.values.push({
value: cell["e"][i]
})
})
});
});
genes.push(gene)
genes.push(gene);
}
return genes;
}
};
export default joyParser;

View File

@@ -1,3 +1,4 @@
// jshint esversion: 6
import React from "react";
import _ from "lodash";
import Categorical from "./categorical/categorical";
@@ -7,10 +8,8 @@ import { connect } from "react-redux";
import Heatmap from "./expression/diffExpHeatmap";
import * as globals from "../globals";
@connect((state) => {
return {
}
@connect(state => {
return {};
})
class LeftSideBar extends React.Component {
constructor(props) {
@@ -21,24 +20,42 @@ class LeftSideBar extends React.Component {
}
render() {
return (
<div style={{position: "fixed"}}>
<p style={{margin: 10, fontSize: 16, fontWeight: 700, width: "100%"}}>CELLxGENE {globals.datasetTitle} </p>
<div style={{padding: 10}}>
<div style={{ position: "fixed" }}>
<p style={{ margin: 10, fontSize: 16, fontWeight: 700, width: "100%" }}>
CELLxGENE {globals.datasetTitle}{" "}
</p>
<div style={{ padding: 10 }}>
<button
style={{
padding: "10px 30px",
outline: 0,
fontSize: 18,
fontStyle: this.state.currentTab === "metadata" ? "inherit" : "italic",
fontStyle:
this.state.currentTab === "metadata" ? "inherit" : "italic",
cursor: "pointer",
border: "none",
backgroundColor: "#FFF",
borderTop: this.state.currentTab === "metadata" ? "4px solid " + globals.brightBlue : "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",
borderTop:
this.state.currentTab === "metadata"
? "4px solid " + globals.brightBlue
: "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
</button>
<button
@@ -46,39 +63,59 @@ class LeftSideBar extends React.Component {
padding: "10px 30px",
outline: 0,
fontSize: 18,
fontStyle: this.state.currentTab === "expression" ? "inherit" : "italic",
fontStyle:
this.state.currentTab === "expression" ? "inherit" : "italic",
cursor: "pointer",
border: "none",
backgroundColor: "#FFF",
borderTop: this.state.currentTab === "expression" ? "4px solid " + globals.brightBlue : "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",
borderTop:
this.state.currentTab === "expression"
? "4px solid " + globals.brightBlue
: "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
</button>
</div>
<div style={{
height: 500,
width: 350,
padding: 10,
overflowY: "scroll",
overflowX: "hidden",
}}>
{this.state.currentTab === "metadata" ? <Categorical/> : null}
{this.state.currentTab === "metadata" ? <Continuous/> : null}
{this.state.currentTab === "expression" ? <Heatmap/> : null}
<div
style={{
height: 500,
width: 350,
padding: 10,
overflowY: "scroll",
overflowX: "hidden"
}}
>
{this.state.currentTab === "metadata" ? <Categorical /> : null}
{this.state.currentTab === "metadata" ? <Continuous /> : null}
{this.state.currentTab === "expression" ? <Heatmap /> : null}
</div>
<div style={{
boxShadow: "-3px -4px 13px 0px rgba(201,201,201,1)",
paddingTop: 10
}}>
<ExpressionButtons/>
<div
style={{
boxShadow: "-3px -4px 13px 0px rgba(201,201,201,1)",
paddingTop: 10
}}
>
<ExpressionButtons />
</div>
</div>
)
);
}
};
}
export default LeftSideBar;

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
export default function (regl) {
export default function(regl) {
return regl({
vert: `
precision mediump float;
@@ -29,14 +30,14 @@ export default function (regl) {
}`,
attributes: {
position: regl.prop('position'),
color: regl.prop('color'),
size: regl.prop('size')
position: regl.prop("position"),
color: regl.prop("color"),
size: regl.prop("size")
},
uniforms: {
distance: regl.prop('distance'),
view: regl.prop('view'),
distance: regl.prop("distance"),
view: regl.prop("view"),
projection: (context, props) => {
return mat4.perspective(
[],
@@ -44,12 +45,12 @@ export default function (regl) {
context.viewportWidth * props.scale / context.viewportHeight,
0.01,
1000
)
},
);
}
},
count: regl.prop('count'),
count: regl.prop("count"),
primitive: 'points'
})
primitive: "points"
});
}

View File

@@ -1,12 +1,8 @@
// jshint esversion: 6
import _ from "lodash";
import renderQueue from "../../util/renderQueue";
import {
margin,
width,
height,
createDimensions,
} from "./util";
import { margin, width, height, createDimensions } from "./util";
const drawScatterplotCanvas = (
context,
@@ -17,38 +13,42 @@ const drawScatterplotCanvas = (
expression,
scatterplotXXaccessor,
scatterplotYYaccessor,
_currentCellSelectionMap,
_currentCellSelectionMap
) => {
return (cell) => {
/*
return cell => {
/*
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'
*/
if (!_currentCellSelectionMap[cell.cellname]) { return }
if (!_currentCellSelectionMap[cell.cellname]) {
return;
}
context.beginPath();
/* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */
context.arc(
xScale(cell.e[expression.data.genes.indexOf(scatterplotXXaccessor)]), /* x */
yScale(cell.e[expression.data.genes.indexOf(scatterplotYYaccessor)]), /* y */
_currentCellSelectionMap[cell.cellname]["__selected__"] ? 3 : 1.5, /* r */
0, /* sAngle */
2 * Math.PI /* eAngle */
);
context.beginPath();
/* context.arc(x,y,r,sAngle,eAngle,counterclockwise); */
context.arc(
xScale(
cell.e[expression.data.genes.indexOf(scatterplotXXaccessor)]
) /* x */,
yScale(
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__"]) {
context.globalAlpha = 1;
} else {
context.globalAlpha = opacityForDeselectedCells;
}
if (_currentCellSelectionMap[cell.cellname]["__selected__"]) {
context.globalAlpha = 1;
} else {
context.globalAlpha = opacityForDeselectedCells;
}
context.fill();
}
}
context.fill();
};
};
export const drawScatterplotCanvasUsingRenderQueue = (
context,
@@ -63,7 +63,10 @@ export const drawScatterplotCanvasUsingRenderQueue = (
/* clear canvas */
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(
drawScatterplotCanvas(
@@ -75,13 +78,12 @@ export const drawScatterplotCanvasUsingRenderQueue = (
expression,
scatterplotXXaccessor,
scatterplotYYaccessor,
_currentCellSelectionMap,
_currentCellSelectionMap
)
)
);
_renderScatterplotWithFunctionReturnedByQueue(expression.data.cells)
_renderScatterplotWithFunctionReturnedByQueue(expression.data.cells);
return _renderScatterplotWithFunctionReturnedByQueue;
};
}
export default _.debounce(drawScatterplotCanvasUsingRenderQueue, 100)
export default _.debounce(drawScatterplotCanvasUsingRenderQueue, 100);

View File

@@ -1,33 +1,37 @@
// jshint esversion: 6
// https://bl.ocks.org/Jverma/076377dd0125b1a508621441752735fc
// https://peterbeshai.com/scatterplot-in-d3-with-voronoi-interaction.html
import React from 'react';
import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
import scatterplot from "./scatterplot";
import setupScatterplot from "./setupScatterplot";
import styles from './scatterplot.css';
import styles from "./scatterplot.css";
import drawScatterplotCanvas from "./drawScatterplotCanvas";
import mat4 from 'gl-mat4';
import fit from 'canvas-fit';
import _camera from '../../util/camera.js'
import _regl from 'regl'
import _drawPoints from './drawPointsRegl'
import mat4 from "gl-mat4";
import fit from "canvas-fit";
import _camera from "../../util/camera.js";
import _regl from "regl";
import _drawPoints from "./drawPointsRegl";
import {
margin,
width,
height,
createDimensions,
} from "./util";
import { margin, width, height, createDimensions } from "./util";
@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 initializeRanges = state.initialize.data && state.initialize.data.data.ranges ? state.initialize.data.data.ranges : null;
@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 initializeRanges =
state.initialize.data && state.initialize.data.data.ranges
? state.initialize.data.data.ranges
: null;
return {
ranges,
@@ -40,8 +44,8 @@ import {
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
opacityForDeselectedCells: state.controls.opacityForDeselectedCells,
differential: state.differential,
expression: state.expression,
}
expression: state.expression
};
})
class Scatterplot extends React.Component {
constructor(props) {
@@ -53,39 +57,31 @@ class Scatterplot extends React.Component {
axes: null,
dimensions: null,
xScale: null,
yScale: null,
yScale: null
};
}
componentDidMount() {
const {
svg
} = setupScatterplot(
width,
height,
margin
);
const { svg } = setupScatterplot(width, height, margin);
this.setState({
svg
})
});
const camera = _camera(this.reglCanvas, {scale: true, rotate: false});
const regl = _regl(this.reglCanvas)
const camera = _camera(this.reglCanvas, { scale: true, rotate: false });
const regl = _regl(this.reglCanvas);
const drawPoints = _drawPoints(regl)
const drawPoints = _drawPoints(regl);
// preallocate buffers
const pointBuffer = regl.buffer()
const colorBuffer = regl.buffer()
const sizeBuffer = regl.buffer()
regl.frame(({viewportWidth, viewportHeight}) => {
const pointBuffer = regl.buffer();
const colorBuffer = regl.buffer();
const sizeBuffer = regl.buffer();
regl.frame(({ viewportWidth, viewportHeight }) => {
regl.clear({
depth: 1,
color: [1, 1, 1, 1]
})
});
drawPoints({
distance: camera.distance,
@@ -95,29 +91,28 @@ class Scatterplot extends React.Component {
count: this.count,
view: camera.view(),
scale: viewportHeight / viewportWidth
})
});
camera.tick()
})
camera.tick();
});
this.setState({
regl,
sizeBuffer,
pointBuffer,
colorBuffer,
})
colorBuffer
});
}
componentWillReceiveProps(nextProps) {
this.maybeSetupScalesAndDrawAxes(nextProps);
}
componentDidUpdate(prevProps) {
if (
(this.state.xScale && this.state.yScale) &&
(this.props.scatterplotXXaccessor && this.props.scatterplotYYaccessor) &&
this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor || // was CLU now FTH1 etc
(this.state.xScale &&
this.state.yScale &&
(this.props.scatterplotXXaccessor &&
this.props.scatterplotYYaccessor) &&
this.props.scatterplotXXaccessor !== prevProps.scatterplotXXaccessor) || // was CLU now FTH1 etc
this.props.scatterplotYYaccessor !== prevProps.scatterplotYYaccessor
) {
this.drawAxesSVG(this.state.xScale, this.state.yScale);
@@ -136,20 +131,24 @@ class Scatterplot extends React.Component {
this.state.xScale &&
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 colors = [];
const sizes = [];
const glScaleX = d3.scaleLinear()
const glScaleX = d3
.scaleLinear()
.domain([0, width])
.range([-.95, .95]) /* padding */
.range([-0.95, 0.95]); /* padding */
const glScaleY = d3.scaleLinear()
const glScaleY = d3
.scaleLinear()
.domain([0, height])
.range([-1, 1])
.range([-1, 1]);
/*
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'
...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([
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 */
glScaleY(this.state.yScale(cell.e[this.props.expression.data.genes.indexOf(this.props.scatterplotYYaccessor)]))
])
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 */,
glScaleY(
this.state.yScale(
cell.e[
this.props.expression.data.genes.indexOf(
this.props.scatterplotYYaccessor
)
]
)
)
]);
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 */
colors.push(_currentCellSelectionMap[cell.cellname]["__colorRGB__"]);
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.colorBuffer(colors)
this.state.sizeBuffer(sizes)
this.count = positions.length
this.state.pointBuffer(positions);
this.state.colorBuffer(colors);
this.state.sizeBuffer(sizes);
this.count = positions.length;
}
}
maybeSetupScalesAndDrawAxes(nextProps) {
@@ -183,61 +201,75 @@ class Scatterplot extends React.Component {
nextProps.scatterplotXXaccessor &&
nextProps.scatterplotYYaccessor
) {
const xScale = d3.scaleLinear()
.domain(d3.extent(nextProps.expression.data.cells, (cell, i) => {
return cell.e[nextProps.expression.data.genes.indexOf(nextProps.scatterplotXXaccessor)]
}))
.range([0, width])
const xScale = d3
.scaleLinear()
.domain(
d3.extent(nextProps.expression.data.cells, (cell, i) => {
return cell.e[
nextProps.expression.data.genes.indexOf(
nextProps.scatterplotXXaccessor
)
];
})
)
.range([0, width]);
const yScale = d3.scaleLinear()
.domain(d3.extent(nextProps.expression.data.cells, (cell) => {
return cell.e[nextProps.expression.data.genes.indexOf(nextProps.scatterplotYYaccessor)]
}))
.range([height, 0])
const yScale = d3
.scaleLinear()
.domain(
d3.extent(nextProps.expression.data.cells, cell => {
return cell.e[
nextProps.expression.data.genes.indexOf(
nextProps.scatterplotYYaccessor
)
];
})
)
.range([height, 0]);
this.setState({
xScale,
yScale
})
});
}
}
drawAxesSVG(xScale, yScale) {
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.
var xAxis = d3.axisBottom()
.scale(xScale);
var xAxis = d3.axisBottom().scale(xScale);
var yAxis = d3.axisLeft()
.scale(yScale);
var yAxis = d3.axisLeft().scale(yScale);
// adding axes is also simpler now, just translate x-axis to (0,height) and it's alread defined to be a bottom axis.
this.state.svg.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'x axis')
this.state.svg
.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "x axis")
.call(xAxis);
// y-axis is translated to (0,0)
this.state.svg.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'y axis')
this.state.svg
.append("g")
.attr("transform", "translate(0,0)")
.attr("class", "y axis")
.call(yAxis);
// adding label. For x-axis, it's at (10, 10), and for y-axis at (width, height-10).
this.state.svg.append('text')
.attr('x', 10)
.attr('y', 10)
.attr('class', 'label')
this.state.svg
.append("text")
.attr("x", 10)
.attr("y", 10)
.attr("class", "label")
.text(this.props.scatterplotYYaccessor);
this.state.svg.append('text')
.attr('x', width)
.attr('y', height - 10)
.attr('text-anchor', 'end')
.attr('class', 'label')
this.state.svg
.append("text")
.attr("x", width)
.attr("y", height - 10)
.attr("text-anchor", "end")
.attr("class", "label")
.text(this.props.scatterplotXXaccessor);
}
render() {
@@ -248,15 +280,16 @@ class Scatterplot extends React.Component {
borderRadius: 3,
marginTop: 15,
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
className={styles.scatterplot}
id="scatterplot"
style={{
width: width + margin.left + margin.right + "px",
height: height + margin.top + margin.bottom + "px",
width: width + margin.left + margin.right + "px",
height: height + margin.top + margin.bottom + "px"
}}
>
<canvas
@@ -266,14 +299,16 @@ class Scatterplot extends React.Component {
marginLeft: margin.left - 7,
marginTop: margin.top
}}
ref={(canvas) => { this.reglCanvas = canvas}}/>
ref={canvas => {
this.reglCanvas = canvas;
}}
/>
</div>
</div>
)
);
}
};
}
export default Scatterplot;
// <SectionHeader text="Continuous Metadata"/>

View File

@@ -1,28 +1,23 @@
// jshint esversion: 6
/*****************************************
******************************************
Setup SVG & Canvas elements
******************************************
******************************************/
const setupScatterplot = (
width,
height,
margin
) => {
const setupScatterplot = (width, height, margin) => {
var container = d3.select("#scatterplot");
var container = d3.select("#scatterplot")
var svg = container.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
var svg = container
.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 + ")");
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
return {
svg,
}
}
svg
};
};
export default setupScatterplot;

View File

@@ -1,9 +1,10 @@
// jshint esversion: 6
import _ from "lodash";
const paddingRight = 120;
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 height = 340 - margin.top - margin.bottom;
export const innerHeight = height - 2;

View File

@@ -1,5 +1,13 @@
// jshint esversion: 6
/* 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 = [
"Total_reads",
"Unique_reads",
@@ -18,7 +26,7 @@ export const continuous = [
"Unmapped_mismatch",
"Unmapped_other",
"Unmapped_short"
]
];
/* colors */
export const blue = "#4a90e2";
@@ -44,55 +52,149 @@ export let API = {
// prefix: "http://pbmc33k.cxg.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;
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 maxParagraphWidth = 600;
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 graphHeight = 500;
export const graphWidth = 960;
export const graphHeight = 960;
export const graphXScale = d3.scaleLinear()
.domain([0, 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 graphXScale = d3
.scaleLinear()
.domain([
0,
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 ordinalColors = [
'#0ac115', '#c10ab6', '#c1710a', '#0a5ac1', '#c1150a', '#0ab6c1', '#5ac10a',
'#710ac1', '#0ac171', '#c10a5a', '#b6c10a', '#150ac1', '#b2ffb7', '#ffb2fa',
'#ffddb2', '#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'
]
"#0ac115",
"#c10ab6",
"#c1710a",
"#0a5ac1",
"#c1150a",
"#0ab6c1",
"#5ac10a",
"#710ac1",
"#0ac171",
"#c10a5a",
"#b6c10a",
"#150ac1",
"#b2ffb7",
"#ffb2fa",
"#ffddb2",
"#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"
];

View File

@@ -1,9 +1,10 @@
// jshint esversion: 6
/* eslint-disable no-console */
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import React from "react";
import ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader";
import { Provider } from "react-redux";
import Redbox from 'redbox-react';
import Redbox from "redbox-react";
/* our code */
import App from "./components/app";
@@ -12,23 +13,23 @@ import store from "./reducers";
ReactDOM.render(
<AppContainer errorReporter={Redbox}>
<Provider store={store}>
<App/>
<App />
</Provider>
</AppContainer>,
document.getElementById('root')
document.getElementById("root")
);
// Hot Module Replacement API
if (module.hot) {
module.hot.accept('./components/app', () => {
const NextApp = require('./components/app').default;
module.hot.accept("./components/app", () => {
const NextApp = require("./components/app").default;
ReactDOM.render(
<AppContainer>
<Provider store={store}>
<NextApp/>
<NextApp />
</Provider>
</AppContainer>,
document.getElementById('root')
document.getElementById("root")
);
});
}

View File

@@ -1,9 +1,8 @@
// jshint esversion: 6
import uri from "urijs";
import * as globals from "../globals";
import _ from "lodash";
import {
parseRGB
} from "../util/parseRGB";
import { parseRGB } from "../util/parseRGB";
/*
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)
*/
const updateCellSelectionMiddleware = (store) => {
return (next) => {
return (action) => {
const updateCellSelectionMiddleware = store => {
return next => {
return action => {
const s = store.getState();
/* this is a hardcoded map of the things we need to keep an eye on and update global cell selection in response to */
const filterJustChanged =
action.type === "color by expression" ||
action.type === "color by continuous metadata" ||
action.type === "color by categorical metadata"
;
action.type === "color by categorical metadata";
if (
!filterJustChanged ||
!s.controls.allCellsOnClient
) {
return next(action); /* if the cells haven't loaded or the action wasn't a color change, bail */
if (!filterJustChanged || !s.controls.allCellsOnClient) {
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;
/*
@@ -54,45 +52,32 @@ const updateCellSelectionMiddleware = (store) => {
*/
if (action.type === "color by categorical metadata") {
colorScale = d3.scaleOrdinal().range(globals.ordinalColors);
_.each(currentSelectionWithUpdatedColors, (cell, i) => {
let c = colorScale(
cell[action.colorAccessor]
);
let c = colorScale(cell[action.colorAccessor]);
currentSelectionWithUpdatedColors[i]["__color__"] = c;
currentSelectionWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c);
})
});
}
if (action.type === "color by continuous metadata") {
colorScale = d3.scaleLinear()
colorScale = d3
.scaleLinear()
.domain([0, action.rangeMaxForColorAccessor])
.range([1,0])
.range([1, 0]);
_.each(currentSelectionWithUpdatedColors, (cell, i) => {
let c = d3.interpolateViridis(
colorScale(
cell[action.colorAccessor]
)
);
let c = d3.interpolateViridis(colorScale(cell[action.colorAccessor]));
currentSelectionWithUpdatedColors[i]["__color__"] = c;
currentSelectionWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c);
})
});
}
if (action.type === "color by expression") {
const indexOfGene = 0; /* we only get one, this comes from server as needed now */
const indexOfGene = 0 /* we only get one, this comes from server as needed now */
const expressionMap = {}
const expressionMap = {};
/*
converts [{cellname: cell123, e}, {}]
@@ -101,50 +86,52 @@ const updateCellSelectionMiddleware = (store) => {
cell789: [0, 8]
}
*/
_.each(action.data.data.cells, (cell) => { /* this action is coming directly from the server */
expressionMap[cell.cellname] = cell.e
})
_.each(action.data.data.cells, cell => {
/* this action is coming directly from the server */
expressionMap[cell.cellname] = cell.e;
});
const minExpressionCell = _.minBy(action.data.data.cells, (cell) => {
return cell.e[indexOfGene]
})
const minExpressionCell = _.minBy(action.data.data.cells, cell => {
return cell.e[indexOfGene];
});
const maxExpressionCell = _.maxBy(action.data.data.cells, (cell) => {
return cell.e[indexOfGene]
})
const maxExpressionCell = _.maxBy(action.data.data.cells, cell => {
return cell.e[indexOfGene];
});
// console.log('middle', action, expressionMap, minExpressionCell)
colorScale = d3.scaleLinear()
.domain([minExpressionCell.e[indexOfGene], maxExpressionCell.e[indexOfGene]])
.range([1,0]) /* invert viridis... probably pass this scale through to others */
colorScale = d3
.scaleLinear()
.domain([
minExpressionCell.e[indexOfGene],
maxExpressionCell.e[indexOfGene]
])
.range([
1,
0
]); /* invert viridis... probably pass this scale through to others */
_.each(currentSelectionWithUpdatedColors, (cell, i) => {
let c = d3.interpolateViridis(
colorScale(
expressionMap[cell.CellName][indexOfGene]
)
colorScale(expressionMap[cell.CellName][indexOfGene])
);
currentSelectionWithUpdatedColors[i]["__color__"] = c;
currentSelectionWithUpdatedColors[i]["__colorRGB__"] = parseRGB(c);
})
});
}
/*
append the result of all the filters to the action the user just triggered
*/
let modifiedAction = Object.assign(
{},
action,
{
currentSelectionWithUpdatedColors,
colorScale
}
)
let modifiedAction = Object.assign({}, action, {
currentSelectionWithUpdatedColors,
colorScale
});
return next(modifiedAction);
}
}
};
};
};
export default updateCellSelectionMiddleware;

View File

@@ -1,3 +1,4 @@
// jshint esversion: 6
import uri from "urijs";
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)
*/
const updateCellSelectionMiddleware = (store) => {
return (next) => {
return (action) => {
const updateCellSelectionMiddleware = store => {
return next => {
return action => {
const s = store.getState();
/* this is a hardcoded map of the things we need to keep an eye on and update global cell selection in response to */
@@ -31,24 +32,26 @@ const updateCellSelectionMiddleware = (store) => {
action.type === "categorical metadata filter deselect" ||
action.type === "categorical metadata filter select" ||
action.type === "categorical metadata filter none of these" ||
action.type === "categorical metadata filter all of these"
;
action.type === "categorical metadata filter all of these";
if (
!filterJustChanged ||
!s.controls.allCellsOnClient
/* 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
- metadata has cellname, and that's all we ever need (is a key to graphMap)
*/
let newSelection = s.controls.currentCellSelection.slice(0);
_.each(newSelection, (cell) => { cell["__selected__"] = true } );
_.each(newSelection, cell => {
cell["__selected__"] = true;
});
/*
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" ||
s.controls.graphBrushSelection
) {
const graphBrushSelection = /* it exists, so is it new or old */
action.type === "graph brush selection change" ? action.brushCoords :
s.controls.graphBrushSelection
const graphBrushSelection /* it exists, so is it new or old */ =
action.type === "graph brush selection change"
? action.brushCoords
: s.controls.graphBrushSelection;
_.each(newSelection, (cell, i) => {
if (!s.controls.graphMap[cell["CellName"]]) {
newSelection[i]["__selected__"] = false; /* make a toggle in future */
return
newSelection[i][
"__selected__"
] = false; /* make a toggle in future */
return;
}
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.southeastX &&
globals.graphYScale(coords[1]) >= graphBrushSelection.northwestY &&
globals.graphYScale(coords[1]) <= graphBrushSelection.southeastY
);
globals.graphYScale(coords[1]) <= graphBrushSelection.southeastY;
if (!pointIsInsideBrushBounds) {
newSelection[i]["__selected__"] = false;
}
})
});
}
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
) {
_.each(newSelection, (cell, i) => {
const cellExtentsAreWithinContinuousSelectionBounds = s.controls.continuousSelection.every((active) => {
const cellExtentsAreWithinContinuousSelectionBounds = s.controls.continuousSelection.every(
active => {
// test if point is within extents for each active brush
return active.dimension.type.within(
cell[active.dimension.key],
active.extent,
active.dimension
);
})
if (!cellExtentsAreWithinContinuousSelectionBounds) {
newSelection[i]["__selected__"] = false;
}
})
);
if (!cellExtentsAreWithinContinuousSelectionBounds) {
newSelection[i]["__selected__"] = false;
}
});
}
/*
@@ -121,8 +126,8 @@ const updateCellSelectionMiddleware = (store) => {
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 */
if (action.type === "continuous metadata histogram brush") {
@@ -139,13 +144,13 @@ const updateCellSelectionMiddleware = (store) => {
_.each(newContinuousUserDefinedRanges, (value, key, i) => {
if (value !== null) {
activeContinuousHistogramFilters.push(key)
activeContinuousHistogramFilters.push(key);
}
})
});
/* see if there are others from previous... */
if (activeContinuousHistogramFilters.length > 0) {
_.each(activeContinuousHistogramFilters, (key) => {
_.each(activeContinuousHistogramFilters, key => {
_.each(newSelection, (cell, i) => {
if (
+cell[key] < newContinuousUserDefinedRanges[key][0] ||
@@ -153,8 +158,8 @@ const updateCellSelectionMiddleware = (store) => {
) {
newSelection[i]["__selected__"] = false;
}
})
})
});
});
}
/*
@@ -177,7 +182,7 @@ const updateCellSelectionMiddleware = (store) => {
...s.controls.categoricalAsBooleansMap[action.metadataField],
[action.value]: true
}
}
};
} else if (action.type === "categorical metadata filter deselect") {
newCategoricalAsBooleansMap = {
...s.controls.categoricalAsBooleansMap,
@@ -185,61 +190,73 @@ const updateCellSelectionMiddleware = (store) => {
...s.controls.categoricalAsBooleansMap[action.metadataField],
[action.value]: false
}
}
};
} else if (action.type === "categorical metadata filter none of these") {
const metadataFieldWithAllOfTheseValueSelected = {};
/* set EVERYTHING to false in this intermediate object */
_.each(s.controls.categoricalAsBooleansMap[action.metadataField], (isActive, option) => {
metadataFieldWithAllOfTheseValueSelected[option] = false;
})
_.each(
s.controls.categoricalAsBooleansMap[action.metadataField],
(isActive, option) => {
metadataFieldWithAllOfTheseValueSelected[option] = false;
}
);
newCategoricalAsBooleansMap = {
...s.controls.categoricalAsBooleansMap,
[action.metadataField]: metadataFieldWithAllOfTheseValueSelected
}
};
} else if (action.type === "categorical metadata filter all of these") {
const metadataFieldWithAllOfTheseValueSelected = {};
/* set EVERYTHING to true in this intermediate object */
_.each(s.controls.categoricalAsBooleansMap[action.metadataField], (isActive, option) => {
metadataFieldWithAllOfTheseValueSelected[option] = true;
})
_.each(
s.controls.categoricalAsBooleansMap[action.metadataField],
(isActive, option) => {
metadataFieldWithAllOfTheseValueSelected[option] = true;
}
);
newCategoricalAsBooleansMap = {
...s.controls.categoricalAsBooleansMap,
[action.metadataField]: metadataFieldWithAllOfTheseValueSelected
}
};
}
const inactiveCategories = [];
_.each(newCategoricalAsBooleansMap, (options, category) => {
_.each(options, (isActive, option) => {
if (!isActive) {
inactiveCategories.push({category, option})
inactiveCategories.push({ category, option });
}
})
})
});
});
if (inactiveCategories.length > 0) {
_.each(inactiveCategories, (d) => {
if (s.controls.categoricalAsCellsMap[d.category] && s.controls.categoricalAsCellsMap[d.category][d.option]) {
_.forEach(s.controls.categoricalAsCellsMap[d.category][d.option], (c) => { c.__selected__ = false; });
_.each(inactiveCategories, d => {
if (
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, {
newSelection,
newCategoricalAsBooleansMap,
newContinuousUserDefinedRanges,
}) /* append the result of all the filters to the action the user just triggered */
newContinuousUserDefinedRanges
}); /* append the result of all the filters to the action the user just triggered */
return next(modifiedAction);
}
}
};
};
};
export default updateCellSelectionMiddleware;

View File

@@ -1,3 +1,4 @@
// jshint esversion: 6
import uri from "urijs";
/*
@@ -5,14 +6,13 @@ import uri from "urijs";
storeInstance => functionToCallWithAnActionThatWillSendItToTheNextMiddleware => actionThatDispatchWasCalledWith => valueToUseAsTheReturnValueOfTheDispatchCall
*/
const updateURLMiddleware = (store) => {
return (next) => {
return (action) => {
const updateURLMiddleware = store => {
return next => {
return action => {
const oldState = store.getState();
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 */
return nextAction;
}
@@ -43,7 +43,6 @@ const updateURLMiddleware = (store) => {
//
// window.history.pushState("", "", newURL)
//
// // Internal helper for working with URIs
// const oldURI = new URI(window.location.href);
@@ -77,8 +76,8 @@ const updateURLMiddleware = (store) => {
// }
return nextAction;
}
}
};
};
};
export default updateURLMiddleware;

View File

@@ -1,27 +1,31 @@
const Cells = (state = {
cells: null,
loading: null,
error: null,
}, action) => {
// jshint esversion: 6
const Cells = (
state = {
cells: null,
loading: null,
error: null
},
action
) => {
switch (action.type) {
case "request cells started":
return Object.assign({}, state, {
loading: true,
error: null
});
case "request cells success":
return Object.assign({}, state, {
error: null,
loading: false,
cells: action.data,
});
case "request cells error":
return Object.assign({}, state, {
loading: false,
error: action.data
});
default:
return state;
case "request cells started":
return Object.assign({}, state, {
loading: true,
error: null
});
case "request cells success":
return Object.assign({}, state, {
error: null,
loading: false,
cells: action.data
});
case "request cells error":
return Object.assign({}, state, {
loading: false,
error: action.data
});
default:
return state;
}
};

View File

@@ -1,184 +1,209 @@
// jshint esversion: 6
import _ from "lodash";
import {
parseRGB
} from "../util/parseRGB";
import { parseRGB } from "../util/parseRGB";
const Controls = (state = {
_ranges: null, /* this comes from initialize, this is universe */
allGeneNames: null,
allCellsOnClient: null, /* this comes from cells endpoint, this is world */
currentCellSelection: null, /* this comes from user actions, all draw components use this, it is created by middleware */
graphMap: null,
categoricalAsBooleansMap: null,
colorAccessor: null,
colorScale: null,
opacityForDeselectedCells: .2,
graphBrushSelection: null,
continuousSelection: null,
scatterplotXXaccessor: null, // just easier to read
scatterplotYYaccessor: null,
axesHaveBeenDrawn: false,
__storedStateForCelllist1__: null, /* will need procedural control of brush ie., brush.extent https://bl.ocks.org/micahstubbs/3cda05ca68cba260cb81 */
__storedStateForCelllist2__: null,
}, action) => {
const Controls = (
state = {
_ranges: null /* this comes from initialize, this is universe */,
allGeneNames: null,
allCellsOnClient: null /* this comes from cells endpoint, this is world */,
currentCellSelection: null /* this comes from user actions, all draw components use this, it is created by middleware */,
graphMap: null,
categoricalAsBooleansMap: null,
colorAccessor: null,
colorScale: null,
opacityForDeselectedCells: 0.2,
graphBrushSelection: null,
continuousSelection: null,
scatterplotXXaccessor: null, // just easier to read
scatterplotYYaccessor: null,
axesHaveBeenDrawn: false,
__storedStateForCelllist1__: null /* will need procedural control of brush ie., brush.extent https://bl.ocks.org/micahstubbs/3cda05ca68cba260cb81 */,
__storedStateForCelllist2__: null
},
action
) => {
switch (action.type) {
/**********************************
/**********************************
Keep a copy of 'universe'
***********************************/
case "initialize success":
return Object.assign({}, state, {
_ranges: action.data.data.ranges,
allGeneNames: action.data.data.genes
});
case "request cells success":
const graphMap = {};
const currentCellSelection = action.data.data.metadata.slice(0);
_.each(action.data.data.graph, (g) => { graphMap[g[0]] = [g[1], g[2]] });
case "initialize success":
return Object.assign({}, state, {
_ranges: action.data.data.ranges,
allGeneNames: action.data.data.genes
});
case "request cells success":
const graphMap = {};
const currentCellSelection = action.data.data.metadata.slice(0);
_.each(action.data.data.graph, g => {
graphMap[g[0]] = [g[1], g[2]];
});
/*
/*
construct a copy of the ranges object that only has categorical
replace all counts with bool flags
ie., everything starts out checked
we mutate this map in the actions below
*/
const categoricalAsBooleansMap = {}, categoricalAsCellsMap = {};
const continuousUserDefinedRanges = {};
_.each(action.data.data.ranges, (value, key) => {
if (
key !== "CellName" &&
value.options /* it's categorical, it has options instead of ranges */
) {
const optionsAsBooleans = {}, optionsAsCells = {};
_.each(value.options, (_value, _key) => {
optionsAsBooleans[_key] = true;
optionsAsCells[_key] = [];
})
categoricalAsBooleansMap[key] = optionsAsBooleans;
categoricalAsCellsMap[key] = optionsAsCells;
} else if (
key !== "CellName" &&
value.range
) {
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);
const categoricalAsBooleansMap = {},
categoricalAsCellsMap = {};
const continuousUserDefinedRanges = {};
_.each(action.data.data.ranges, (value, key) => {
if (
key !== "CellName" &&
value.options /* it's categorical, it has options instead of ranges */
) {
const optionsAsBooleans = {},
optionsAsCells = {};
_.each(value.options, (_value, _key) => {
optionsAsBooleans[_key] = true;
optionsAsCells[_key] = [];
});
categoricalAsBooleansMap[key] = optionsAsBooleans;
categoricalAsCellsMap[key] = optionsAsCells;
} else if (key !== "CellName" && value.range) {
continuousUserDefinedRanges[key] = null;
}
});
});
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 */
});
/* * * * * * * * * * * * * * * * * *
_.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, {
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
* * * * * * * * * * * * * * * * * */
case "parallel coordinates axes have been drawn":
return Object.assign({}, state, {
axesHaveBeenDrawn: true
});
case "continuous selection using parallel coords brushing": {
return Object.assign({}, state, {
continuousSelection: action.data,
currentCellSelection: action.newSelection /* this comes from middleware */
});
}
case "graph brush selection change":
return Object.assign({}, state, {
graphBrushSelection: action.brushCoords, /* this has already been applied in middleware but store it for next time */
currentCellSelection: action.newSelection /* this comes from middleware */
})
case "graph brush deselect":
return Object.assign({}, state, {
graphBrushSelection: null,
currentCellSelection: action.newSelection /* this comes from middleware */
})
case "continuous metadata histogram brush":
return Object.assign({}, state, {
newContinuousUserDefinedRanges: 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,
})
/*******************************
case "parallel coordinates axes have been drawn":
return Object.assign({}, state, {
axesHaveBeenDrawn: true
});
case "continuous selection using parallel coords brushing": {
return Object.assign({}, state, {
continuousSelection: action.data,
currentCellSelection:
action.newSelection /* this comes from middleware */
});
}
case "graph brush selection change":
return Object.assign({}, state, {
graphBrushSelection:
action.brushCoords /* this has already been applied in middleware but store it for next time */,
currentCellSelection:
action.newSelection /* this comes from middleware */
});
case "graph brush deselect":
return Object.assign({}, state, {
graphBrushSelection: null,
currentCellSelection:
action.newSelection /* this comes from middleware */
});
case "continuous metadata histogram brush":
return Object.assign({}, state, {
newContinuousUserDefinedRanges:
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
*******************************/
case "categorical metadata filter select":
return Object.assign({}, state, {
categoricalAsBooleansMap: action.newCategoricalAsBooleansMap, /* this comes from middleware */
currentCellSelection: action.newSelection /* this comes from middleware */
})
case "categorical metadata filter deselect":
return Object.assign({}, state, {
categoricalAsBooleansMap: action.newCategoricalAsBooleansMap, /* this comes from middleware */
currentCellSelection: action.newSelection /* this comes from middleware */
})
case "categorical metadata filter none of these":
return Object.assign({}, state, {
categoricalAsBooleansMap: action.newCategoricalAsBooleansMap, /* 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 */
})
/*******************************
case "categorical metadata filter select":
return Object.assign({}, state, {
categoricalAsBooleansMap:
action.newCategoricalAsBooleansMap /* this comes from middleware */,
currentCellSelection:
action.newSelection /* this comes from middleware */
});
case "categorical metadata filter deselect":
return Object.assign({}, state, {
categoricalAsBooleansMap:
action.newCategoricalAsBooleansMap /* this comes from middleware */,
currentCellSelection:
action.newSelection /* this comes from middleware */
});
case "categorical metadata filter none of these":
return Object.assign({}, state, {
categoricalAsBooleansMap:
action.newCategoricalAsBooleansMap /* 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
*******************************/
case "color by continuous metadata":
return Object.assign({}, state, {
colorAccessor: action.colorAccessor,
currentCellSelection: action.currentSelectionWithUpdatedColors, /* this comes from middleware */
colorScale: action.colorScale,
});
case "color by expression":
return Object.assign({}, state, {
colorAccessor: action.gene,
currentCellSelection: action.currentSelectionWithUpdatedColors, /* this comes from middleware */
colorScale: action.colorScale,
})
case "color by categorical metadata":
return Object.assign({}, state, {
colorAccessor: action.colorAccessor, /* pass the scale through additionally, and it's a legend! */
currentCellSelection: action.currentSelectionWithUpdatedColors, /* this comes from middleware */
colorScale: action.colorScale,
})
case "store current cell selection as differential set 1":
return Object.assign({}, state, {
__storedStateForCelllist1__: action.data
});
/*******************************
case "color by continuous metadata":
return Object.assign({}, state, {
colorAccessor: action.colorAccessor,
currentCellSelection:
action.currentSelectionWithUpdatedColors /* this comes from middleware */,
colorScale: action.colorScale
});
case "color by expression":
return Object.assign({}, state, {
colorAccessor: action.gene,
currentCellSelection:
action.currentSelectionWithUpdatedColors /* this comes from middleware */,
colorScale: action.colorScale
});
case "color by categorical metadata":
return Object.assign({}, state, {
colorAccessor:
action.colorAccessor /* pass the scale through additionally, and it's a legend! */,
currentCellSelection:
action.currentSelectionWithUpdatedColors /* this comes from middleware */,
colorScale: action.colorScale
});
case "store current cell selection as differential set 1":
return Object.assign({}, state, {
__storedStateForCelllist1__: action.data
});
/*******************************
Scatterplot
*******************************/
case "set scatterplot x":
return Object.assign({}, state, {
scatterplotXXaccessor: action.data
});
case "set scatterplot y":
return Object.assign({}, state, {
scatterplotYYaccessor: action.data
});
default:
return state;
case "set scatterplot x":
return Object.assign({}, state, {
scatterplotXXaccessor: action.data
});
case "set scatterplot y":
return Object.assign({}, state, {
scatterplotYYaccessor: action.data
});
default:
return state;
}
};

View File

@@ -1,37 +1,41 @@
const Differential = (state = {
diffExp: null,
loading: null,
error: null,
celllist1: null,
celllist2: null,
}, action) => {
// jshint esversion: 6
const Differential = (
state = {
diffExp: null,
loading: null,
error: null,
celllist1: null,
celllist2: null
},
action
) => {
switch (action.type) {
case "request differential expression started":
return Object.assign({}, state, {
loading: true,
error: null
});
case "request differential expression success":
return Object.assign({}, state, {
error: null,
loading: false,
diffExp: action.data,
});
case "request differential expression error":
return Object.assign({}, state, {
loading: false,
error: action.data
});
case "store current cell selection as differential set 1":
return Object.assign({}, state, {
celllist1: action.data
});
case "store current cell selection as differential set 2":
return Object.assign({}, state, {
celllist2: action.data
});
default:
return state;
case "request differential expression started":
return Object.assign({}, state, {
loading: true,
error: null
});
case "request differential expression success":
return Object.assign({}, state, {
error: null,
loading: false,
diffExp: action.data
});
case "request differential expression error":
return Object.assign({}, state, {
loading: false,
error: action.data
});
case "store current cell selection as differential set 1":
return Object.assign({}, state, {
celllist1: action.data
});
case "store current cell selection as differential set 2":
return Object.assign({}, state, {
celllist2: action.data
});
default:
return state;
}
};

View File

@@ -1,28 +1,32 @@
const Expression = (state = {
data: null,
loading: null,
error: null,
}, action) => {
// jshint esversion: 6
const Expression = (
state = {
data: null,
loading: null,
error: null
},
action
) => {
switch (action.type) {
case "get expression started":
return Object.assign({}, state, {
loading: true,
error: null
});
case "get expression success":
return Object.assign({}, state, {
error: null,
loading: false,
data: action.data.data,
});
case "get expression error":
return Object.assign({}, state, {
data: null,
loading: false,
error: action.data
});
default:
return state;
case "get expression started":
return Object.assign({}, state, {
loading: true,
error: null
});
case "get expression success":
return Object.assign({}, state, {
error: null,
loading: false,
data: action.data.data
});
case "get expression error":
return Object.assign({}, state, {
data: null,
loading: false,
error: action.data
});
default:
return state;
}
};

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 updateCellSelectionMiddleware from "../middleware/updateCellSelectionMiddleware";
import updateCellColors from "../middleware/updateCellColors";
@@ -16,8 +17,8 @@ const Reducer = combineReducers({
cells,
expression,
controls,
differential,
})
differential
});
let store = createStore(
Reducer,

View File

@@ -1,28 +1,32 @@
const Initialize = (state = {
data: null,
loading: null,
error: null,
}, action) => {
// jshint esversion: 6
const Initialize = (
state = {
data: null,
loading: null,
error: null
},
action
) => {
switch (action.type) {
case "initialize started":
return Object.assign({}, state, {
loading: true,
error: null
});
case "initialize success":
return Object.assign({}, state, {
error: null,
loading: false,
data: action.data,
});
case "initialize error":
return Object.assign({}, state, {
data: null,
loading: false,
error: action.data
});
default:
return state;
case "initialize started":
return Object.assign({}, state, {
loading: true,
error: null
});
case "initialize success":
return Object.assign({}, state, {
error: null,
loading: false,
data: action.data
});
case "initialize error":
return Object.assign({}, state, {
data: null,
loading: false,
error: action.data
});
default:
return state;
}
};

View File

@@ -1,15 +1,19 @@
const Responsive = (state = {
width: null,
height: null,
}, action) => {
// jshint esversion: 6
const Responsive = (
state = {
width: null,
height: null
},
action
) => {
switch (action.type) {
case "resize event":
return Object.assign({}, state, {
width: action.data,
height: action.data
});
default:
return state;
case "resize event":
return Object.assign({}, state, {
width: action.data,
height: action.data
});
default:
return state;
}
};

View File

@@ -1,70 +1,73 @@
var createCamera = require('orbit-camera')
var createScroll = require('scroll-speed')
var mp = require('mouse-position')
var mb = require('mouse-pressed')
var key = require('key-pressed')
// jshint esversion: 6
var createCamera = require("orbit-camera");
var createScroll = require("scroll-speed");
var mp = require("mouse-position");
var mb = require("mouse-pressed");
var key = require("key-pressed");
const panSpeed = 0.4
const scaleSpeed = 0.5
const scaleMax = 3
const panSpeed = 0.4;
const scaleSpeed = 0.5;
const scaleMax = 3;
// const scaleMin = 1.15
const scaleMin = 1.03
const scaleMin = 1.03;
function attachCamera(canvas, opts) {
opts = opts || {}
opts.pan = opts.pan !== false
opts.scale = opts.scale !== false
opts.rotate = opts.rotate !== false
opts = opts || {};
opts.pan = opts.pan !== false;
opts.scale = opts.scale !== false;
opts.rotate = opts.rotate !== false;
var scroll = createScroll(canvas, opts.scale)
var mbut = mb(canvas, opts.rotate)
var mpos = mp(canvas)
var camera = createCamera(
[0, 0, 1]
, [0, 0, -1]
, [0, 1, 0]
)
var scroll = createScroll(canvas, opts.scale);
var mbut = mb(canvas, opts.rotate);
var mpos = mp(canvas);
var camera = createCamera([0, 0, 1], [0, 0, -1], [0, 1, 0]);
camera.tick = tick
camera.tick = tick;
return camera
return camera;
function tick() {
var ctrl = key('<control>') || key('<alt>')
var alt = key('<shift>')
var height = canvas.height
var width = canvas.width
var ctrl = key("<control>") || key("<alt>");
var alt = key("<shift>");
var height = canvas.height;
var width = canvas.width;
if (opts.rotate && mbut.left && ctrl && !alt) {
camera.rotate(
[ mpos.x / width - 0.5, mpos.y / height - 0.5 ]
, [ mpos.prevX / width - 0.5, mpos.prevY / height - 0.5 ]
)
[mpos.x / width - 0.5, mpos.y / 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([
(panSpeed * (mpos[0] - mpos.prev[0]) / width) * Math.pow(camera.distance, 1)
, (panSpeed * (mpos[1] - mpos.prev[1]) / height) * Math.pow(camera.distance, 1)
])
panSpeed *
(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]) {
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))) {
var d = (mpos.y - mpos.prevY)
var d = mpos.y - mpos.prevY;
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 < scaleMin) camera.distance = scaleMin
if (camera.distance > scaleMax) camera.distance = scaleMax;
if (camera.distance < scaleMin) camera.distance = scaleMin;
scroll.flush()
mpos.flush()
scroll.flush();
mpos.flush();
}
}

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] !== "#") {
const _c = c.replace(/[^\d,.]/g, '').split(',');
return [
scaleRGB(+_c[0]),
scaleRGB(+_c[1]),
scaleRGB(+_c[2])
];
const _c = c.replace(/[^\d,.]/g, "").split(",");
return [scaleRGB(+_c[0]), scaleRGB(+_c[1]), scaleRGB(+_c[2])];
} else {
var parsedHex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(c);
return [

View File

@@ -1,14 +1,15 @@
// jshint esversion: 6
/*****************************************
******************************************
Render Queue via http://bl.ocks.org/syntagmatic/raw/3341641/render-queue.js
******************************************
******************************************/
const renderQueue = (function(callback1234) {
var _queue = [], // data to be rendered
_rate = 300, // number of calls per frame
_invalidate = function() {}, // invalidate last render queue
_clear = function() {}; // clearing function
const renderQueue = function(callback1234) {
var _queue = [], // data to be rendered
_rate = 300, // number of calls per frame
_invalidate = function() {}, // invalidate last render queue
_clear = function() {}; // clearing function
var rq = function(ARRAY_FROM_CELLXGENE) {
if (ARRAY_FROM_CELLXGENE) rq.data(ARRAY_FROM_CELLXGENE);
@@ -25,7 +26,7 @@ const renderQueue = (function(callback1234) {
function doFrame() {
if (!valid) return true;
var chunk = _queue.splice(0,_rate);
var chunk = _queue.splice(0, _rate);
chunk.map(callback1234);
timer_frame(doFrame);
}
@@ -35,7 +36,7 @@ const renderQueue = (function(callback1234) {
rq.data = function(ARRAY_FROM_CELLXGENE) {
_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;
};
@@ -65,14 +66,17 @@ const renderQueue = (function(callback1234) {
rq.invalidate = _invalidate;
var timer_frame = window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.oRequestAnimationFrame
|| window.msRequestAnimationFrame
|| function(callback) { setTimeout(callback, 17); };
var timer_frame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
setTimeout(callback, 17);
};
return rq;
});
};
export default renderQueue;

View File

@@ -1,4 +1,5 @@
export const scaleRGB = (input) => {
// jshint esversion: 6
export const scaleRGB = input => {
const outputMax = 1;
const outputMin = 0;
@@ -7,4 +8,4 @@ export const scaleRGB = (input) => {
const percent = (input - inputMin) / (inputMax - inputMin);
return percent * (outputMax - outputMin) + outputMin;
}
};