From 90c7daf571e645b76e68fc7ecea658b86c831529 Mon Sep 17 00:00:00 2001 From: bkmartinjr Date: Sat, 5 May 2018 13:15:10 -0700 Subject: [PATCH] Prettier --- README.md | 2 +- configuration/babel/babel.dev.js | 6 +- configuration/babel/babel.prod.js | 10 +- configuration/babel/babel.test.js | 8 +- configuration/eslint/eslint.js | 18 +- configuration/polyfills/polyfills.js | 8 +- configuration/webpack/webpack.config.dev.js | 64 ++-- configuration/webpack/webpack.config.prod.js | 86 ++--- index_template.html | 14 +- server/development.js | 37 +- server/production.js | 23 +- server/utils.js | 41 ++- src/actions/index.js | 216 +++++------ .../_experimentalWebGLHeatmap/heatmap.js | 141 +++---- .../orbit-control.js | 67 ++-- .../_experimentalWebGLHeatmap/popup.js | 45 +-- src/components/app.js | 72 ++-- src/components/categorical/categorical.js | 172 ++++----- src/components/categorical/util.js | 9 +- src/components/categorical/value.js | 53 ++- src/components/continuous/continuous.js | 1 + src/components/continuous/drawAxes.js | 94 ++--- src/components/continuous/drawLinesCanvas.js | 57 ++- src/components/continuous/histogramBrush.js | 146 +++++--- src/components/continuous/parallel.js | 86 +++-- .../continuous/setupParallelCoordinates.js | 40 +- src/components/continuous/util.js | 44 ++- src/components/expression/cellSetButtons.js | 49 ++- src/components/expression/diffExpHeatmap.js | 279 ++++++++------ .../expression/expressionButtons.js | 72 ++-- src/components/framework/container.js | 9 +- src/components/framework/header.js | 12 +- src/components/framework/sectionHeader.js | 15 +- src/components/graph/drawGraph.js | 117 +++--- src/components/graph/drawPointsRegl.js | 25 +- src/components/graph/graph.js | 249 +++++++------ src/components/graph/util.js | 1 + src/components/joy/drawJoy.js | 189 ++++++---- src/components/joy/joy.js | 26 +- src/components/joy/joyParser.js | 23 +- src/components/leftsidebar.js | 109 ++++-- src/components/scatterplot/drawPointsRegl.js | 25 +- .../scatterplot/drawScatterplotCanvas.js | 76 ++-- src/components/scatterplot/scatterplot.js | 249 +++++++------ .../scatterplot/setupScatterplot.js | 27 +- src/components/scatterplot/util.js | 3 +- src/globals.js | 174 +++++++-- src/index.js | 21 +- src/middleware/updateCellColors.js | 113 +++--- .../updateCellSelectionMiddleware.js | 137 ++++--- src/middleware/updateURLMiddleware.js | 15 +- src/reducers/cells.js | 50 +-- src/reducers/controls.js | 343 ++++++++++-------- src/reducers/differential.js | 70 ++-- src/reducers/expression.js | 52 +-- src/reducers/index.js | 7 +- src/reducers/initialize.js | 52 +-- src/reducers/responsive.js | 26 +- src/util/camera.js | 85 ++--- src/util/parseRGB.js | 13 +- src/util/renderQueue.js | 32 +- src/util/scaleRGB.js | 5 +- 62 files changed, 2398 insertions(+), 1912 deletions(-) diff --git a/README.md b/README.md index a7032786..6a645a93 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/configuration/babel/babel.dev.js b/configuration/babel/babel.dev.js index 571d4cd2..bc9dc712 100644 --- a/configuration/babel/babel.dev.js +++ b/configuration/babel/babel.dev.js @@ -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" diff --git a/configuration/babel/babel.prod.js b/configuration/babel/babel.prod.js index c2399a54..d185e68b 100644 --- a/configuration/babel/babel.prod.js +++ b/configuration/babel/babel.prod.js @@ -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")]]) }; diff --git a/configuration/babel/babel.test.js b/configuration/babel/babel.test.js index e18d5e58..63e45b6d 100644 --- a/configuration/babel/babel.test.js +++ b/configuration/babel/babel.test.js @@ -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"] }; diff --git a/configuration/eslint/eslint.js b/configuration/eslint/eslint.js index e50ac01a..012c6c44 100644 --- a/configuration/eslint/eslint.js +++ b/configuration/eslint/eslint.js @@ -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" } }; diff --git a/configuration/polyfills/polyfills.js b/configuration/polyfills/polyfills.js index 1aa797e9..a63249f6 100644 --- a/configuration/polyfills/polyfills.js +++ b/configuration/polyfills/polyfills.js @@ -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"); diff --git a/configuration/webpack/webpack.config.dev.js b/configuration/webpack/webpack.config.dev.js index 07e485e0..1beaf9aa 100644 --- a/configuration/webpack/webpack.config.dev.js +++ b/configuration/webpack/webpack.config.dev.js @@ -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() diff --git a/configuration/webpack/webpack.config.prod.js b/configuration/webpack/webpack.config.prod.js index bb79e005..33a9715b 100644 --- a/configuration/webpack/webpack.config.prod.js +++ b/configuration/webpack/webpack.config.prod.js @@ -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" }) ] }; diff --git a/index_template.html b/index_template.html index b2f683ed..e8e5327d 100644 --- a/index_template.html +++ b/index_template.html @@ -13,12 +13,12 @@ @@ -26,4 +26,4 @@ - \ No newline at end of file + diff --git a/server/development.js b/server/development.js index 6d95e1f6..23f5e859 100644 --- a/server/development.js +++ b/server/development.js @@ -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(); }); diff --git a/server/production.js b/server/production.js index 61b3b3d4..993a4afc 100644 --- a/server/production.js +++ b/server/production.js @@ -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(); }); diff --git a/server/utils.js b/server/utils.js index 59160484..f274b81c 100644 --- a/server/utils.js +++ b/server/utils.js @@ -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." ); } }; diff --git a/src/actions/index.js b/src/actions/index.js index bdbd00b7..45118e80 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -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 -} +}; diff --git a/src/components/_experimentalWebGLHeatmap/heatmap.js b/src/components/_experimentalWebGLHeatmap/heatmap.js index 5f21617a..1e65f694 100644 --- a/src/components/_experimentalWebGLHeatmap/heatmap.js +++ b/src/components/_experimentalWebGLHeatmap/heatmap.js @@ -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 && ( - { - this.canvas = canvas; - }} onChangeViewport={this.onChangeViewport}> - - + return ( + width && + height && ( + { + this.canvas = canvas; + }} + onChangeViewport={this.onChangeViewport} + > + + + ) ); } render() { - const {width, height, popup} = this.state; + const { width, height, popup } = this.state; if (!width || !height) { return null; } - const renderedPopup = (popup.displayed)? : null; + const renderedPopup = popup.displayed ? : null; return (
@@ -231,6 +246,6 @@ class Heatmap extends React.Component {
); } -}; +} export default Heatmap; diff --git a/src/components/_experimentalWebGLHeatmap/orbit-control.js b/src/components/_experimentalWebGLHeatmap/orbit-control.js index 934bdb7d..fb4602e0 100755 --- a/src/components/_experimentalWebGLHeatmap/orbit-control.js +++ b/src/components/_experimentalWebGLHeatmap/orbit-control.js @@ -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 ( -
- + onWheel={this._onWheel.bind(this)} + > {this.props.children} - -
); + + ); } } diff --git a/src/components/_experimentalWebGLHeatmap/popup.js b/src/components/_experimentalWebGLHeatmap/popup.js index e27b563c..dd39a8ff 100755 --- a/src/components/_experimentalWebGLHeatmap/popup.js +++ b/src/components/_experimentalWebGLHeatmap/popup.js @@ -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 {
{title}
- ) - }; + ); + } } Popup.defaultProps = { - id: 'id', - title: '', + id: "id", + title: "", x: 0, - y: 0, + y: 0 }; diff --git a/src/components/app.js b/src/components/app.js index 998b2f67..4c2fad76 100644 --- a/src/components/app.js +++ b/src/components/app.js @@ -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 ( - { - this.props.cells.loading ? -
- - loading cells -
: - null - } + {this.props.cells.loading ? ( +
+ + + loading cells + +
+ ) : null} {this.props.cells.error ? "Error loading cells" : null} - {false ? : ""} + {false ? ( + + ) : ( + "" + )}
- -
- - + +
+ + {/**/}
- ) + ); } -}; +} export default App; diff --git a/src/components/categorical/categorical.js b/src/components/categorical/categorical.js index e7a51e2a..1b04c388 100644 --- a/src/components/categorical/categorical.js +++ b/src/components/categorical/categorical.js @@ -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 ( -
-
-

+

+

+ margin: "3px 10px 3px 0px" + }} + > { - this.setState({isExpanded: !this.state.isExpanded}) - }}> - {this.state.isExpanded ? : } + this.setState({ isExpanded: !this.state.isExpanded }); + }} + > + {this.state.isExpanded ? : } {this.props.metadataField} + onChange={ + this.state.isChecked + ? this.toggleNone.bind(this) + : this.toggleAll.bind(this) + } + checked={this.state.isChecked} + type="checkbox" + /> - + cursor: "pointer" + }} + > +

-
- { - this.state.isExpanded ? this.renderCategoryItems() : null - } -
+
{this.state.isExpanded ? this.renderCategoryItems() : null}
- ) + ); } } -@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 ( -
- - { - _.map(this.props.ranges, (value, key) => { - const isColorField = key.includes("color") || key.includes("Color"); - if ( - value.options && - key !== "CellName" && - !isColorField - ) { - return ( - - ) - } - }) - } +
+ {_.map(this.props.ranges, (value, key) => { + const isColorField = key.includes("color") || key.includes("Color"); + if (value.options && key !== "CellName" && !isColorField) { + return ( + + ); + } + })}
- ) + ); } -}; +} 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, diff --git a/src/components/categorical/util.js b/src/components/categorical/util.js index 9c4fb769..32398200 100644 --- a/src/components/categorical/util.js +++ b/src/components/categorical/util.js @@ -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; + }); +}; diff --git a/src/components/categorical/value.js b/src/components/categorical/value.js index f8afdf5b..fddb6a26 100644 --- a/src/components/categorical/value.js +++ b/src/components/categorical/value.js @@ -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 (
-

+

+ }} + > - {this.props.value} + type="checkbox" + /> + {this.props.value}

-

+ }} + > {this.props.count}

- ) + ); } } diff --git a/src/components/continuous/continuous.js b/src/components/continuous/continuous.js index df16ed85..86f38e5a 100644 --- a/src/components/continuous/continuous.js +++ b/src/components/continuous/continuous.js @@ -1,3 +1,4 @@ +// jshint esversion: 6 /* rc slider https://www.npmjs.com/package/rc-slider */ import React from "react"; diff --git a/src/components/continuous/drawAxes.js b/src/components/continuous/drawAxes.js index c063c604..2ef6df4b 100644 --- a/src/components/continuous/drawAxes.js +++ b/src/components/continuous/drawAxes.js @@ -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; diff --git a/src/components/continuous/drawLinesCanvas.js b/src/components/continuous/drawLinesCanvas.js index 094b082c..05a23ae2 100644 --- a/src/components/continuous/drawLinesCanvas.js +++ b/src/components/continuous/drawLinesCanvas.js @@ -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; diff --git a/src/components/continuous/histogramBrush.js b/src/components/continuous/histogramBrush.js index d3dff58c..c33a2278 100644 --- a/src/components/continuous/histogramBrush.js +++ b/src/components/continuous/histogramBrush.js @@ -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 ( -
- { this.drawHistogram(svgRef)}}> +
+ { + this.drawHistogram(svgRef); + }} + > {this.props.ranges.min} {" to "} {this.props.ranges.max}
- ) + ); } -}; +} - export default HistogramBrush; +export default HistogramBrush; diff --git a/src/components/continuous/parallel.js b/src/components/continuous/parallel.js index 9660e084..ce3bd03e 100644 --- a/src/components/continuous/parallel.js +++ b/src/components/continuous/parallel.js @@ -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 (
+ }} + />
- ) + ); } -}; +} export default Parallel; - // diff --git a/src/components/continuous/setupParallelCoordinates.js b/src/components/continuous/setupParallelCoordinates.js index aafaed37..9b1e20ef 100644 --- a/src/components/continuous/setupParallelCoordinates.js +++ b/src/components/continuous/setupParallelCoordinates.js @@ -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; diff --git a/src/components/continuous/util.js b/src/components/continuous/util.js index fadccb62..5d6ea0a0 100644 --- a/src/components/continuous/util.js +++ b/src/components/continuous/util.js @@ -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])]; }); }; diff --git a/src/components/expression/cellSetButtons.js b/src/components/expression/cellSetButtons.js index 33b3bab4..2be330b0 100644 --- a/src/components/expression/cellSetButtons.js +++ b/src/components/expression/cellSetButtons.js @@ -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 ( - + - ) + ); } } diff --git a/src/components/expression/diffExpHeatmap.js b/src/components/expression/diffExpHeatmap.js index 7b2a6e41..90a8cd6f 100644 --- a/src/components/expression/diffExpHeatmap.js +++ b/src/components/expression/diffExpHeatmap.js @@ -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 ( -

+

{this.props.text}

- ) + ); } } @@ -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 ( -
-
- X +
+
Y + backgroundColor: + this.props.scatterplotXXaccessor === this.props.gene + ? globals.brightBlue + : "inherit" + }} + > + X + + + Y + + color: + this.props.colorAccessor === this.props.gene + ? "white" + : "inherit", + backgroundColor: + this.props.colorAccessor === this.props.gene + ? globals.brightBlue + : "inherit" + }} + > + + + }} + > {this.props.gene}
+ text={this.props.set1exp} + /> + text={this.props.set2exp} + /> - {this.props.aveDiff.toFixed(2)} + }} + > + {this.props.aveDiff.toFixed(2)}
- ) + ); } } @@ -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

Select cells & compute differential to see heatmap

+ if (!this.props.differential.diffExp) + return

Select cells & compute differential to see heatmap

; 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 (
Color by any gene: item.toLowerCase().indexOf(value.toLowerCase()) > -1} + shouldItemRender={(item, value) => + item.toLowerCase().indexOf(value.toLowerCase()) > -1 + } getItemValue={item => item} - renderItem={(item, highlighted) => + renderItem={(item, highlighted) => (
{item}
- } + )} 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) + ); }} /> -
-

Gene

-

1

-

2

+
+

Gene

+

1

+

2

ave diff

- { - topGenesForCellSet1.topgenes.map((gene, i) => { - return { + return ( + - }) - } - { - topGenesForCellSet2.topgenes.map((gene, i) => { - return + ); + })} + {topGenesForCellSet2.topgenes.map((gene, i) => { + return ( + - }) - } + set1exp={Math.floor( + topGenesForCellSet2.mean_expression_cellset1[i] + )} + set2exp={Math.floor( + topGenesForCellSet2.mean_expression_cellset2[i] + )} + /> + ); + })}
- ) + ); } } diff --git a/src/components/expression/expressionButtons.js b/src/components/expression/expressionButtons.js index d1bd04f5..1b83519f 100644 --- a/src/components/expression/expressionButtons.js +++ b/src/components/expression/expressionButtons.js @@ -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 (
-
-
- There are currently - { - " " + - _.filter(this.props.currentCellSelection, "__selected__").length + - " " - } - cells selected, click a cell set button to store them. -
- - +
+
+ There are currently + {" " + + _.filter(this.props.currentCellSelection, "__selected__").length + + " "} + cells selected, click a cell set button to store them. +
+ +
- { - this.props.differential.celllist1 && this.props.differential.celllist2 ? + {this.props.differential.celllist1 && + this.props.differential.celllist2 ? ( : + + ) : ( - } - + )}
- ) + ); } -}; +} export default Expression; diff --git a/src/components/framework/container.js b/src/components/framework/container.js index bd257989..6f21507f 100644 --- a/src/components/framework/container.js +++ b/src/components/framework/container.js @@ -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 => ( -
- {props.children} -
+
{props.children}
); export default Container; diff --git a/src/components/framework/header.js b/src/components/framework/header.js index f2ddf89f..cafe8a3e 100644 --- a/src/components/framework/header.js +++ b/src/components/framework/header.js @@ -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 = () => (
- - - +
); diff --git a/src/components/framework/sectionHeader.js b/src/components/framework/sectionHeader.js index 73446bab..ca07c05f 100644 --- a/src/components/framework/sectionHeader.js +++ b/src/components/framework/sectionHeader.js @@ -1,10 +1,13 @@ -import React from 'react'; +// jshint esversion: 6 +import React from "react"; -const SectionHeader = ({text}) => ( -

+const SectionHeader = ({ text }) => ( +

{text}

); diff --git a/src/components/graph/drawGraph.js b/src/components/graph/drawGraph.js index ca532e21..593d2d40 100644 --- a/src/components/graph/drawGraph.js +++ b/src/components/graph/drawGraph.js @@ -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) + ); +}; diff --git a/src/components/graph/drawPointsRegl.js b/src/components/graph/drawPointsRegl.js index 7448bc41..e8c6b39b 100644 --- a/src/components/graph/drawPointsRegl.js +++ b/src/components/graph/drawPointsRegl.js @@ -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" + }); } diff --git a/src/components/graph/graph.js b/src/components/graph/graph.js index a39fd716..a4b74781 100644 --- a/src/components/graph/graph.js +++ b/src/components/graph/graph.js @@ -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 {
+ boxShadow: "3px 4px 13px 0px rgba(201,201,201,1)" + }} + >
+ }} + >
- + deselected opacity - - - - + /> + + + +
- +
-
-
- { this.reglCanvas = canvas}}/> + /> +
+ { + this.reglCanvas = canvas; + }} + />
- ) + ); } -}; +} export default Graph; diff --git a/src/components/graph/util.js b/src/components/graph/util.js index ef2e39d9..40a9d740 100644 --- a/src/components/graph/util.js +++ b/src/components/graph/util.js @@ -1,3 +1,4 @@ +// jshint esversion: 6 // createExpressionsCountsMap () { // // const CHANGE_ME_MAGIC_GENE_INDEX = 5; diff --git a/src/components/joy/drawJoy.js b/src/components/joy/drawJoy.js index cc094375..e2026f33 100644 --- a/src/components/joy/drawJoy.js +++ b/src/components/joy/drawJoy.js @@ -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; diff --git a/src/components/joy/joy.js b/src/components/joy/joy.js index 23efefb0..2ac66fe9 100644 --- a/src/components/joy/joy.js +++ b/src/components/joy/joy.js @@ -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 ( -
+

Joy

-

Cell expression distribution per gene & if differential expression, Ie., cells for cluster 5, top genes expressed by cluster 8

+

+ {" "} + Cell expression distribution per gene & if differential expression, + Ie., cells for cluster 5, top genes expressed by cluster 8 +

- ) + ); } -}; +} export default Joy; diff --git a/src/components/joy/joyParser.js b/src/components/joy/joyParser.js index 6ff1bf4d..7ccaa08a 100644 --- a/src/components/joy/joyParser.js +++ b/src/components/joy/joyParser.js @@ -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; diff --git a/src/components/leftsidebar.js b/src/components/leftsidebar.js index b3ce6f88..62cc7a72 100644 --- a/src/components/leftsidebar.js +++ b/src/components/leftsidebar.js @@ -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 ( -
-

CELLxGENE {globals.datasetTitle}

-
+
+

+ CELLxGENE {globals.datasetTitle}{" "} +

+
-
- {this.state.currentTab === "metadata" ? : null} - {this.state.currentTab === "metadata" ? : null} - {this.state.currentTab === "expression" ? : null} +
+ {this.state.currentTab === "metadata" ? : null} + {this.state.currentTab === "metadata" ? : null} + {this.state.currentTab === "expression" ? : null}
-
- +
+
- ) + ); } -}; +} export default LeftSideBar; diff --git a/src/components/scatterplot/drawPointsRegl.js b/src/components/scatterplot/drawPointsRegl.js index 7448bc41..e8c6b39b 100644 --- a/src/components/scatterplot/drawPointsRegl.js +++ b/src/components/scatterplot/drawPointsRegl.js @@ -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" + }); } diff --git a/src/components/scatterplot/drawScatterplotCanvas.js b/src/components/scatterplot/drawScatterplotCanvas.js index 772db0f9..e8cd60c2 100644 --- a/src/components/scatterplot/drawScatterplotCanvas.js +++ b/src/components/scatterplot/drawScatterplotCanvas.js @@ -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); diff --git a/src/components/scatterplot/scatterplot.js b/src/components/scatterplot/scatterplot.js index b0a5c9a7..c404b29f 100644 --- a/src/components/scatterplot/scatterplot.js +++ b/src/components/scatterplot/scatterplot.js @@ -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" + >
{ this.reglCanvas = canvas}}/> + ref={canvas => { + this.reglCanvas = canvas; + }} + />
- ) + ); } -}; +} export default Scatterplot; - // diff --git a/src/components/scatterplot/setupScatterplot.js b/src/components/scatterplot/setupScatterplot.js index 6f6e3f94..42c1baca 100644 --- a/src/components/scatterplot/setupScatterplot.js +++ b/src/components/scatterplot/setupScatterplot.js @@ -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; diff --git a/src/components/scatterplot/util.js b/src/components/scatterplot/util.js index a121cbfb..bb5fa832 100644 --- a/src/components/scatterplot/util.js +++ b/src/components/scatterplot/util.js @@ -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; diff --git a/src/globals.js b/src/globals.js index 508cecad..bd3bf6ef 100644 --- a/src/globals.js +++ b/src/globals.js @@ -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" +]; diff --git a/src/index.js b/src/index.js index 393e7357..17f7a212 100644 --- a/src/index.js +++ b/src/index.js @@ -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( - + , - 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( - + , - document.getElementById('root') + document.getElementById("root") ); }); } diff --git a/src/middleware/updateCellColors.js b/src/middleware/updateCellColors.js index 6151f7c9..46003647 100644 --- a/src/middleware/updateCellColors.js +++ b/src/middleware/updateCellColors.js @@ -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; diff --git a/src/middleware/updateCellSelectionMiddleware.js b/src/middleware/updateCellSelectionMiddleware.js index 4b119286..6d4e04f1 100644 --- a/src/middleware/updateCellSelectionMiddleware.js +++ b/src/middleware/updateCellSelectionMiddleware.js @@ -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; diff --git a/src/middleware/updateURLMiddleware.js b/src/middleware/updateURLMiddleware.js index f6982749..0ab9da5a 100644 --- a/src/middleware/updateURLMiddleware.js +++ b/src/middleware/updateURLMiddleware.js @@ -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; diff --git a/src/reducers/cells.js b/src/reducers/cells.js index ce85a66c..c17ee6d4 100644 --- a/src/reducers/cells.js +++ b/src/reducers/cells.js @@ -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; } }; diff --git a/src/reducers/controls.js b/src/reducers/controls.js index 0718cc04..3f514613 100644 --- a/src/reducers/controls.js +++ b/src/reducers/controls.js @@ -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; } }; diff --git a/src/reducers/differential.js b/src/reducers/differential.js index 49839dde..5ff9a73b 100644 --- a/src/reducers/differential.js +++ b/src/reducers/differential.js @@ -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; } }; diff --git a/src/reducers/expression.js b/src/reducers/expression.js index bd3a3c0b..aae4c760 100644 --- a/src/reducers/expression.js +++ b/src/reducers/expression.js @@ -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; } }; diff --git a/src/reducers/index.js b/src/reducers/index.js index c8375fe8..0b089a25 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -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, diff --git a/src/reducers/initialize.js b/src/reducers/initialize.js index 97370757..201f98fc 100644 --- a/src/reducers/initialize.js +++ b/src/reducers/initialize.js @@ -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; } }; diff --git a/src/reducers/responsive.js b/src/reducers/responsive.js index dbd38e47..c61ae129 100644 --- a/src/reducers/responsive.js +++ b/src/reducers/responsive.js @@ -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; } }; diff --git a/src/util/camera.js b/src/util/camera.js index 3174090a..8820d803 100644 --- a/src/util/camera.js +++ b/src/util/camera.js @@ -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('') || key('') - var alt = key('') - var height = canvas.height - var width = canvas.width + var ctrl = key("") || key(""); + var alt = key(""); + 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(); } } diff --git a/src/util/parseRGB.js b/src/util/parseRGB.js index ab08c6d2..bdd9ebee 100644 --- a/src/util/parseRGB.js +++ b/src/util/parseRGB.js @@ -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 [ diff --git a/src/util/renderQueue.js b/src/util/renderQueue.js index 88a933bb..be5d6d97 100644 --- a/src/util/renderQueue.js +++ b/src/util/renderQueue.js @@ -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; diff --git a/src/util/scaleRGB.js b/src/util/scaleRGB.js index de2cf8db..f4995667 100644 --- a/src/util/scaleRGB.js +++ b/src/util/scaleRGB.js @@ -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; -} +};