initial commit

This commit is contained in:
Colin Megill
2017-08-23 16:28:01 -07:00
commit f6d7cc3e23
40 changed files with 1661 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# dependencies
node_modules
# coverage
coverage
.nyc_output
# production
build
# misc
.DS_Store
npm-debug.log
.vscode
data

29
.travis.yml Normal file
View File

@@ -0,0 +1,29 @@
language: node_js
node_js:
- 4
- 5
- 6
# Use container-based Travis infrastructure.
sudo: false
branches:
only:
- master
- /^greenkeeper-.*$/
notifications:
email:
on_success: change
on_failure: always
before_install:
- npm install -g npm@3
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
script:
- npm run lint
- npm run test
- cat coverage/lcov.info | node_modules/.bin/coveralls || echo "Coveralls upload failed"

20
LICENSE.txt Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
#cellxgene
##Quickstart:
* `npm install`
* `npm run dev`
* `localhost:3000`

View File

@@ -0,0 +1,10 @@
module.exports = {
babelrc: false,
cacheDirectory: true,
presets: [
[ 'es2015', { loose: true, modules: false } ],
'stage-0',
'react'
],
plugins: [ 'react-hot-loader/babel' ]
};

View File

@@ -0,0 +1,11 @@
module.exports = {
babelrc: false,
presets: [
[ 'es2015', { loose: true, modules: false } ],
'stage-0',
'react'
],
plugins: [ 'babel-plugin-transform-react-constant-elements' ]
.map(require.resolve)
.concat([ [ require.resolve('babel-plugin-transform-runtime') ] ])
};

View File

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

View File

@@ -0,0 +1,29 @@
module.exports = {
root: true,
parser: 'babel-eslint',
extends: 'formidable/configurations/es6-react',
env: { browser: true, commonjs: true, es6: true, node: true, mocha: true },
globals: { expect: true },
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
generators: true,
experimentalObjectRestSpread: true
}
},
rules: {
quotes: [ 2, 'single', { allowTemplateLiterals: true } ],
'no-magic-numbers': 'off',
'func-style': 'off',
'arrow-parens': 'off',
'no-use-before-define': 'off',
'react/jsx-filename-extension': 'off',
'react/require-extension': 'off',
'react/no-multi-comp': 'warn',
'react/prop-types': 'warn',
'react/sort-comp': 'warn',
'react/sort-prop-types': 'warn'
}
};

View File

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

View File

@@ -0,0 +1,74 @@
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');
module.exports = {
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')
],
output: {
path: path.resolve('build'),
pathinfo: true,
filename: 'static/js/bundle.js',
publicPath: '/'
},
resolve: { extensions: [ '.js', '.json' ] },
module: {
loaders: [
{
test: /\.js$/,
include: src,
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'
},
{ 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]' }
},
{
test: /\.(mp4|webm)(\?.*)?$/,
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')
}),
new webpack.LoaderOptionsPlugin({
options: {
eslint: {
configFile: path.resolve('./configuration/eslint/eslint.js'),
useEslintrc: false
},
postcss() {
return [ autoprefixer ];
}
}
}),
new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"development"' }),
// Note: only CSS is currently hot reloaded
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
]
};

View File

@@ -0,0 +1,112 @@
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 src = path.resolve('src');
const nodeModules = path.resolve('node_modules');
const publicPath = '/';
module.exports = {
bail: true,
devtool: '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',
publicPath
},
resolve: { extensions: [ '.js', '.json' ] },
module: {
loaders: [
{
test: /\.js$/,
include: src,
loader: 'babel-loader',
query: require('../babel/babel.prod')
},
{
test: /\.css$/,
include: [ src, nodeModules ],
loader: ExtractTextPlugin.extract({
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',
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]' }
},
{
test: /\.(mp4|webm)(\?.*)?$/,
include: [ src, nodeModules ],
loader: 'url-loader',
query: { limit: 10000, name: 'static/media/[name].[hash:8].[ext]' }
}
]
},
plugins: [
new HtmlWebpackPlugin({
inject: 'body',
template: path.resolve('index.html'),
favicon: path.resolve('favicon.png'),
inlineSource: '.(js|css)$',
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
}
}),
new HtmlWebpackInlineSourcePlugin(),
new webpack.LoaderOptionsPlugin({
options: {
eslint: {
configFile: path.resolve('./configuration/eslint/eslint.js'),
useEslintrc: false
},
postcss() {
return [ autoprefixer ];
}
}
}),
new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }),
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({
compress: { screw_ie8: true, warnings: false },
mangle: { screw_ie8: true },
output: { comments: false, screw_ie8: true }
}),
new ExtractTextPlugin('static/css/[name].[contenthash:8].css'),
new CopyWebpackPlugin([
{ from: 'public' },
{ from: 'manifest.webmanifest' }
]),
new SWPrecacheWebpackPlugin({
cacheId: 'formidable-react-starter',
filename: 'service-worker.js'
})
]
};

BIN
favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

15
index.html Normal file
View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#db5945">
<title>Cellx</title>
</head>
<body>
<noscript>If you're seeing this message, that means <strong>JavaScript has been disabled on your browser</strong>, please <strong>enable JS</strong> to make this app work.</noscript>
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.js"></script>
</body>
</html>

91
package.json Normal file
View File

@@ -0,0 +1,91 @@
{
"name": "cellxgene",
"version": "0.0.1",
"license": "MIT",
"repository": "https://github.com/FormidableLabs/formidable-react-starter",
"scripts": {
"build": "npm run clean && webpack --config configuration/webpack/webpack.config.prod.js",
"clean": "rimraf build",
"dev": "node server/development.js",
"lint": "eslint src",
"prod": "cross-env NODE_ENV=production PORT=3000 node server/production.js",
"test": "nyc --reporter=lcov --reporter=text mocha test/.setup.js test/**/*.spec.js"
},
"engineStrict": true,
"engines": {
"npm": ">=3.0.0"
},
"eslintConfig": {
"extends": "./configuration/eslint/eslint.js"
},
"nyc": {
"sourceMap": false,
"instrument": false
},
"dependencies": {
"d3": "^4.10.0",
"express": "^4.14.0",
"lodash": "^4.17.4",
"react": "^15.3.0",
"react-dom": "^15.3.0",
"react-helmet": "^4.0.0",
"react-hot-loader": "^3.0.0-beta.2",
"react-router-dom": "4.1.2",
"serve-favicon": "^2.3.0"
},
"devDependencies": {
"autoprefixer": "^6.4.0",
"babel-core": "^6.13.2",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.4",
"babel-plugin-istanbul": "^3.1.2",
"babel-plugin-transform-react-constant-elements": "^6.9.1",
"babel-plugin-transform-runtime": "^6.12.0",
"babel-preset-es2015": "^6.13.2",
"babel-preset-react": "^6.11.1",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.11.6",
"babel-runtime": "^6.11.6",
"chai": "^3.5.0",
"chalk": "^1.1.3",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1",
"coveralls": "^2.11.12",
"cross-env": "^3.1.4",
"css-loader": "^0.26.1",
"css-modules-require-hook": "^4.0.1",
"enzyme": "^2.4.1",
"eslint": "^2.10.2",
"eslint-config-formidable": "^2.0.1",
"eslint-loader": "^1.5.0",
"eslint-plugin-filenames": "^1.1.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^3.0.2",
"eslint-plugin-react": "^6.0.0",
"extract-text-webpack-plugin": "^2.0.0-beta.3",
"file-loader": "^0.9.0",
"filesize": "^3.3.0",
"gzip-size": "^3.0.0",
"html-webpack-inline-source-plugin": "0.0.6",
"html-webpack-plugin": "^2.22.0",
"jsdom": "^9.4.1",
"json-loader": "^0.5.4",
"mocha": "^3.0.2",
"nyc": "^10.0.0",
"postcss-loader": "^1.2.2",
"promise": "^7.1.1",
"react-addons-test-utils": "^15.3.0",
"redbox-react": "^1.3.0",
"rimraf": "^2.5.4",
"sinon": "^1.17.5",
"sinon-chai": "^2.8.0",
"style-loader": "^0.13.1",
"sw-precache-webpack-plugin": "^0.7.1",
"url-loader": "^0.5.7",
"web-app-manifest-loader": "^0.1.1",
"webpack": "^2.2.0-rc.4",
"webpack-dev-middleware": "^1.6.1",
"webpack-hot-middleware": "^2.12.2",
"whatwg-fetch": "^2.0.1"
}
}

56
server/development.js Normal file
View File

@@ -0,0 +1,56 @@
/* 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');
process.env.NODE_ENV = 'development';
var PORT = process.env.PORT || 3000;
// Set up compiler
var compiler = webpack(config);
compiler.plugin('invalid', () => {
utils.clearConsole();
console.log('Compiling...');
});
compiler.plugin('done', stats => {
utils.formatStats(stats, PORT);
});
// Launch server
var app = express();
app.use(historyApiFallback({ verbose: false }));
app.use(
require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: config.output.publicPath
})
);
app.use(require('webpack-hot-middleware')(compiler));
app.use(favicon('./favicon.png'));
app.get('*', (req, res) => {
res.sendFile(path.resolve('index.html'));
});
app.listen(PORT, err => {
if (err) {
console.log(err);
return;
}
utils.clearConsole();
console.log(chalk.cyan('Starting the development server...'));
console.log();
});

31
server/production.js Normal file
View File

@@ -0,0 +1,31 @@
/* 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');
var PORT = process.env.PORT || 80;
// Launch server
var app = express();
app.use(express.static('./build'));
app.use(favicon('./build/favicon.png'));
app.get('*', (req, res) => {
res.sendFile(path.resolve('./build/index.html'));
});
app.listen(PORT, err => {
if (err) {
console.log(err);
return;
}
utils.clearConsole();
console.log(chalk.cyan('Production server started on port ' + PORT));
console.log();
});

79
server/utils.js Normal file
View File

@@ -0,0 +1,79 @@
/* eslint-disable */
var chalk = require('chalk');
var friendlySyntaxErrorLabel = 'Syntax error:';
function isLikelyASyntaxError(message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1;
}
function formatMessage(message) {
return message
.replace('Module build failed: SyntaxError:', friendlySyntaxErrorLabel)
.replace(
/Module not found: Error: Cannot resolve 'file' or 'directory'/,
'Module not found:'
)
.replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, '')
.replace('./~/css-loader!./~/postcss-loader!', '');
}
var clearConsole = () => {
process.stdout.write('\x1bc');
};
var formatStats = (stats, port) => {
clearConsole();
var hasErrors = stats.hasErrors();
var hasWarnings = stats.hasWarnings();
if (!hasErrors && !hasWarnings) {
console.log(chalk.green('Compiled successfully!'));
console.log();
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)
);
var formattedWarnings = json.warnings.map(
message => 'Warning in ' + formatMessage(message)
);
if (hasErrors) {
console.log(chalk.red('Failed to compile.'));
console.log();
if (formattedErrors.some(isLikelyASyntaxError)) {
formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
}
formattedErrors.forEach(message => {
console.log(message);
console.log();
});
return;
}
if (hasWarnings) {
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(
'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.'
);
}
};
module.exports = { formatStats: formatStats, clearConsole: clearConsole };

View File

@@ -0,0 +1,68 @@
import React from "react";
import _ from "lodash";
import * as globals from "../../globals";
import cells from "../../../data/GBM_metadata.js";
import createCategoryCounts from "./createCategoryCounts";
const Button = ({category, c, i}) => (
<button
key={i}
style={{
border: i === 0 ? "none" : `1px solid ${globals.lightgrey}`,
background: i === 0 ? globals.hcaBlue : "none",
color: i === 0 ? "white" : "black",
padding: "7px 10px",
marginRight: 20,
borderRadius: 3,
cursor: "pointer"
}}>
{c}
<span
style={{
fontWeight: i === 0 ? globals.bolder : "auto",
background: i === 0 ? "white" : "none",
color: "black",
marginLeft: 10,
padding: "2px 4px",
borderRadius: 3
}}>
{category[c]}
</span>
</button>
)
const Category = ({category, title}) => (
<div>
<p>{title}</p>
{ _.map(Object.keys(category), (c, i) => <Button key={i} category={category} c={c} i={i} />) }
</div>
)
const Categorical = () => {
return (
<div>
<h3>Categorical Metadata</h3>
{
_.map(createCategoryCounts(cells),
(value, key) => {
return <Category key={key} title={key} category={value} />
})
}
</div>
)
};
export default Categorical;
/*
[on off] toggle hide deselected filters (shows a menu vs shows what you ordered in compact/narrative form. fold out animation.)
<p> <button> Field name [initial state] [create 2d graph] [explain part of 2d graph] </button> </p>
<p> <button> Field name [initial state] [create hypothesis] [validate hypothesis] </button> </p>
<p> <button> Field name [total] [selected in current filters] [selected in graph selection] </button> </p>
<p> <button> Tumor [400] [40] [4] </button> </p>
*/

View File

@@ -0,0 +1,45 @@
import * as globals from "../../globals.js"
/*
end result:
counts: {
Sample.type: {
glioblastoma: 3452
},
Selection: {
foo: 234,
bar: 21
}
}
*/
const categories = globals.categories;
const createCategoryCounts = (cells) => {
/* instantiate counts obj */
const counts = {};
categories.forEach((category) => {
counts[category] = {};
})
cells.forEach((cell) => {
categories.forEach((category) => {
/* if, for the given category (ie., categories.Location), we do not have ie., glioblastoma already, create it. Otherwise increment it. */
if (!counts[category][cell[category]]) {
counts[category][cell[category]] = 1;
} else {
counts[category][cell[category]]++
}
})
});
return counts;
}
export default createCategoryCounts;

View File

@@ -0,0 +1,5 @@
.container {
max-width: 1200px;
margin: auto;
min-height: 100%;
}

View File

@@ -0,0 +1,11 @@
import React from 'react';
import styles from './container.css';
const Container = props => (
<div className={styles.container}>
{props.children}
</div>
);
export default Container;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import _ from "lodash";
import cells from "../../../data/GBM_metadata.js";
import drawParallelCoordinates from "./drawParallelCoordinates";
import createContinuousRanges from "./createContinuousRanges";
const Continuous = () => {
drawParallelCoordinates(cells)
return (
<div>
<h3> Continuous Metadata </h3>
</div>
)
};
export default Continuous;

View File

@@ -0,0 +1,25 @@
import * as globals from "../../globals.js"
/*
end result:
ranges: {
Total_reads: {
min: 11111,
max: 33333
},
Genes_detected: {
min: 444,
max: 555
}
}
*/
const createContinuousRanges = (cells) => {
const ranges = {};
return ranges;
}
export default createContinuousRanges;

View File

@@ -0,0 +1,314 @@
import styles from './parallelCoordinates.css';
import {
margin,
width,
height,
innerHeight,
color,
dimensions,
types,
xscale,
yAxis,
} from "./util";
/*****************************************
******************************************
Canvas Parallel coordinates with svg brushing via /* via https://bl.ocks.org/syntagmatic/05a5b0897a48890133beb59c815bd953
******************************************
******************************************/
/*****************************************
******************************************
Render Queue via http://bl.ocks.org/syntagmatic/raw/3341641/render-queue.js
******************************************
******************************************/
const renderQueue = (function(callback1234) {
var _queue = [], // data to be rendered
_rate = 1000, // number of calls per frame
_invalidate = function() {}, // invalidate last render queue
_clear = function() {}; // clearing function
var rq = function(data) {
if (data) rq.data(data);
_invalidate();
_clear();
rq.render();
};
rq.render = function() {
var valid = true;
_invalidate = rq.invalidate = function() {
valid = false;
};
function doFrame() {
if (!valid) return true;
var chunk = _queue.splice(0,_rate);
chunk.map(callback1234);
timer_frame(doFrame);
}
doFrame();
};
rq.data = function(data) {
_invalidate();
_queue = data.slice(0); // creates a copy of the data
return rq;
};
rq.add = function(data) {
_queue = _queue.concat(data);
};
rq.rate = function(value) {
if (!arguments.length) return _rate;
_rate = value;
return rq;
};
rq.remaining = function() {
return _queue.length;
};
// clear the canvas
rq.clear = function(func) {
if (!arguments.length) {
_clear();
return rq;
}
_clear = func;
return rq;
};
rq.invalidate = _invalidate;
var timer_frame = window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.oRequestAnimationFrame
|| window.msRequestAnimationFrame
|| function(callback) { setTimeout(callback, 17); };
return rq;
});
/*****************************************
******************************************
MAIN: drawParallelCoordinates
******************************************
******************************************/
const drawParallelCoordinates = (data) => {
const d3_functor = (v) => {
return typeof v === "function" ? v : function() { return v; };
};
/*****************************************
******************************************
Setup SVG & Canvas elements
******************************************
******************************************/
var container = d3.select("body").append("div")
.attr("class", styles.parcoords)
.style("width", width + margin.left + margin.right + "px")
.style("height", height + margin.top + margin.bottom + "px");
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 + ")");
var canvas = container.append("canvas")
.attr("width", width * devicePixelRatio)
.attr("height", height * devicePixelRatio)
.style("width", width + "px")
.style("height", height + "px")
.style("margin-top", margin.top + "px")
.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);
var output = d3.select("body").append("pre");
var axes = svg.selectAll(".axis")
.data(dimensions)
.enter().append("g")
.attr("class", styles.axis)
.attr("transform", function(d,i) { return "translate(" + xscale(i) + ")"; });
/*****************************************
******************************************
Setup SVG & Canvas elements
******************************************
******************************************/
d3.csv("https://gist.githubusercontent.com/syntagmatic/05a5b0897a48890133beb59c815bd953/raw/310a68d713eb4d351f809b6cfbcffe3d9d96a205/nutrient.csv", (error, data) => {
if (error) throw error;
// shuffle the data!
data = d3.shuffle(data);
data.forEach(function(d) {
dimensions.forEach(function(p) {
d[p.key] = !d[p.key] ? null : p.type.coerce(d[p.key]);
});
// truncate long text strings to fit in data table
for (var key in d) {
if (d[key] && d[key].length > 35) d[key] = d[key].slice(0,36);
}
});
// type/dimension default setting happens here
dimensions.forEach(function(dim) {
if (!("domain" in dim)) {
// detect domain using dimension type's extent function
dim.domain = d3_functor(dim.type.extent)(data.map(function(d) { return d[dim.key]; }));
}
if (!("scale" in dim)) {
// use type's default scale for dimension
dim.scale = dim.type.defaultScale.copy();
}
dim.scale.domain(dim.domain);
});
const draw = (d) => {
ctx.strokeStyle = "rgba(0,0,0,.4)" /* color(d.food_group); */
ctx.beginPath();
var coords = project(d);
coords.forEach((p,i) => {
// this tricky bit avoids rendering null values as 0
if (p === null) {
// 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];
if (prev !== null) {
ctx.moveTo(prev[0],prev[1]);
ctx.lineTo(prev[0]+6,prev[1]);
}
}
if (i < coords.length-1) {
var next = coords[i+1];
if (next !== null) {
ctx.moveTo(next[0]-6,next[1]);
}
}
return;
}
if (i == 0) {
ctx.moveTo(p[0],p[1]);
return;
}
ctx.lineTo(p[0],p[1]);
});
ctx.stroke();
}
/*****************************************
******************************************
Handles a brush event, toggling the display of foreground lines.
******************************************
******************************************/
const brush = () => {
render.invalidate();
var actives = [];
svg.selectAll(".axis .brush")
.filter((d) => {
return d3.brushSelection(this);
})
.each(function(d) {
actives.push({
dimension: d,
extent: d3.brushSelection(this)
});
});
var selected = data.filter(function(d) {
if (actives.every(function(active) {
var dim = active.dimension;
// test if point is within extents for each active brush
return dim.type.within(d[dim.key], active.extent, dim);
})) {
return true;
}
});
ctx.clearRect(0,0,width,height);
ctx.globalAlpha = d3.min([0.85/Math.pow(selected.length,0.3),1]);
render(selected);
output.text(d3.tsvFormat(selected.slice(0,24)));
console.log('fires brush', actives)
}
function brushstart() {
d3.event.sourceEvent.stopPropagation();
}
var render = renderQueue(draw).rate(50);
ctx.clearRect(0,0,width,height);
ctx.globalAlpha = d3.min([0.85/Math.pow(data.length,0.3),1]);
render(data);
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")
.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)
.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);
output.text(d3.tsvFormat(data.slice(0,24)));
function project(d) {
return dimensions.map(function(p,i) {
// check if data element has property and contains a value
if (
!(p.key in d) ||
d[p.key] === null
) return null;
return [xscale(i),p.scale(d[p.key])];
});
};
});
}
export default drawParallelCoordinates;

View File

@@ -0,0 +1,91 @@
/*
This code can be found: https://bl.ocks.org/syntagmatic/05a5b0897a48890133beb59c815bd953
body {
min-width: 760px;
}
*/
.parcoords {
display: block;
}
.parcoords svg,
.parcoords canvas {
font: 10px sans-serif;
position: absolute;
}
.parcoords canvas {
opacity: 0.9;
pointer-events: none;
}
.axis .title {
font-size: 10px;
transform: rotate(-21deg) translate(-5px,-6px);
fill: #222;
}
.axis line,
.axis path {
fill: none;
stroke: #ccc;
stroke-width: 1px;
}
.axis .tick text {
fill: #222;
opacity: 0;
pointer-events: none;
}
/*
old, from reference bl.ocks
.axis.manufac_name .tick text,
.axis.food_group .tick text {
opacity: 1;
}
*/
.axis:hover line,
.axis:hover path,
.axis.active line,
.axis.active path {
fill: none;
stroke: #222;
stroke-width: 1px;
}
.axis:hover .title {
font-weight: bold;
}
.axis:hover .tick text {
opacity: 1;
}
.axis.active .title {
font-weight: bold;
}
.axis.active .tick text {
opacity: 1;
font-weight: bold;
}
.brush .extent {
fill-opacity: .3;
stroke: #fff;
stroke-width: 1px;
}
pre {
width: 100%;
height: 300px;
margin: 6px 12px;
tab-size: 40;
font-size: 10px;
overflow: auto;
}

View File

@@ -0,0 +1,162 @@
export const margin = {top: 66, right: 110, bottom: 20, left: 188};
export const width = document.body.clientWidth - margin.left - margin.right;
export const height = 340 - margin.top - margin.bottom;
export const innerHeight = height - 2;
export const devicePixelRatio = window.devicePixelRatio || 1;
export const color = d3.scaleOrdinal()
.range(["#5DA5B3","#D58323","#DD6CA7","#54AF52","#8C92E8","#E15E5A","#725D82","#776327","#50AB84","#954D56","#AB9C27","#517C3F","#9D5130","#357468","#5E9ACF","#C47DCB","#7D9E33","#DB7F85","#BA89AD","#4C6C86","#B59248","#D8597D","#944F7E","#D67D4B","#8F86C2"]);
export const types = {
"Number": {
key: "Number",
coerce: function(d) { return +d; },
extent: d3.extent,
within: function(d, extent, dim) { return extent[0] <= dim.scale(d) && dim.scale(d) <= extent[1]; },
defaultScale: d3.scaleLinear().range([innerHeight, 0])
},
"String": {
key: "String",
coerce: String,
extent: function (data) { return data.sort(); },
within: function(d, extent, dim) { return extent[0] <= dim.scale(d) && dim.scale(d) <= extent[1]; },
defaultScale: d3.scalePoint().range([0, innerHeight])
},
"Date": {
key: "Date",
coerce: function(d) { return new Date(d); },
extent: d3.extent,
within: function(d, extent, dim) { return extent[0] <= dim.scale(d) && dim.scale(d) <= extent[1]; },
defaultScale: d3.scaleTime().range([0, innerHeight])
}
};
export const dimensions = [
{
key: "Total lipid (fat) (g)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Sugars, total (g)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Calcium, Ca (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Sodium, Na (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Phosphorus, P (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Potassium, K (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Thiamin (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Riboflavin (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Niacin (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Iron, Fe (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Magnesium, Mg (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Protein (g)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Zinc, Zn (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Vitamin B-6 (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Vitamin B-12 (mcg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Folic acid (mcg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Selenium, Se (mcg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Vitamin A, IU (IU)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Vitamin K (phylloquinone) (mcg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Vitamin C, total ascorbic acid (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Vitamin D (IU)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Cholesterol (mg)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Fiber, total dietary (g)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
},
{
key: "Carbohydrate, by difference (g)",
type: types["Number"],
scale: d3.scaleSqrt().range([innerHeight, 0])
}
];
export const xscale = d3.scalePoint()
.domain(d3.range(dimensions.length))
.range([0, width]);
export const yAxis = d3.axisLeft();

View File

@@ -0,0 +1,3 @@
.header {
background: red;
}

15
src/components/header.js Normal file
View File

@@ -0,0 +1,15 @@
import React from 'react';
import Container from './container';
import styles from './header.css';
const Header = () => (
<header className={styles.header}>
<Container>
</Container>
</header>
);
export default Header;

27
src/components/home.js Normal file
View File

@@ -0,0 +1,27 @@
import React from 'react';
import { Link } from 'react-router-dom';
import _ from "lodash";
import Helmet from 'react-helmet';
import Container from './container';
import Categorical from "./categorical/categorical";
import Continuous from "./continuous/continuous";
const Home = () => {
return (
<Container>
<Helmet title="cellxgene" />
<h1><Link to="/page2">cellxgene</Link></h1>
<Categorical/>
<Continuous/>
</Container>
)
};
export default Home;
// <Categorical title={"Sample type"} category={types}/>
// <Categorical title={"Selection"} category={selection}/>
// <Categorical title={"Location"} category={location}/>
// <Categorical title={"Sample name"} category={names}/>

33
src/components/page2.css Normal file
View File

@@ -0,0 +1,33 @@
.heroContainer {
width: 100%;
height: 95vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.hero {
font-size: 48;
margin-bottom: 0;
}
.subHero {
font-size: 18;
max-width: 600;
text-align: "center";
}
.primaryButton {
border: 1px solid black;
padding: 10px 15px;
background: none;
cursor: pointer;
font-size: 14px;
}
.or {
margin: 20px;
font-family: Georgia;
font-style: italic;
}

25
src/components/page2.js Normal file
View File

@@ -0,0 +1,25 @@
import React from 'react';
import { Link } from 'react-router-dom';
import Helmet from 'react-helmet';
import styles from './page2.css';
import Container from './container';
const Page2 = () => (
<Container>
<Helmet title="cellxgene" />
<div className={styles.heroContainer}>
<h1 className={styles.hero}>cellxgene</h1>
<p className={styles.subHero}>
A data exploration interface for single cell genetic expression matrices. Subset, filter, cluster & validate, all in one place.
</p>
<div>
<Link to="/"><button className={styles.primaryButton}>explore a sample dataset</button></Link>
<span className={styles.or}>or</span>
<button className={styles.primaryButton}>get started with your own data</button>
</div>
</div>
</Container>
);
export default Page2;

19
src/containers/root.css Normal file
View File

@@ -0,0 +1,19 @@
.root {
background: #fff;
min-height: 100%;
}
h1, h2, h3, h4, h5, h6 {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
p {
font-family: Georgia,serif;
line-height: 1.6;
}
/*
a {
color: #ad1b11;
}
*/

11
src/containers/root.js Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
import styles from './root.css';
const Root = props => (
<div className={styles.root}>
{props.children}
</div>
);
export default Root;

27
src/globals.js Normal file
View File

@@ -0,0 +1,27 @@
/* these will be either (preferably) specified or inferred */
export const categories = ["Sample.type", "Selection", "Location", "Sample.name", "Class", "Neoplastic"];
export const continuous = [
"ERCC_reads",
"ERCC_to_non_ERCC",
"Genes_detected",
"Multimapping_reads_percent",
"Non_ERCC_reads",
"Splice_sites_AT.AC",
"Splice_sites_Annotated",
"Splice_sites_GC.AG",
"Splice_sites_GT.AG",
"Splice_sites_non_canonical",
"Splice_sites_total",
"Total_reads",
"Unique_reads",
"Unique_reads_percent",
"Unmapped_mismatch",
"Unmapped_other",
"Unmapped_short"
]
/* colors */
export const hcaBlue = "#1c7cc7"
export const lightgrey = "rgb(211,211,211)";
export const bolder = 700;

9
src/index.css Normal file
View File

@@ -0,0 +1,9 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
* {
box-sizing: border-box;
}

48
src/index.js Normal file
View File

@@ -0,0 +1,48 @@
/* eslint-disable no-console */
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import Redbox from 'redbox-react';
import Routes from './routes';
import './index.css';
if (process.env.NODE_ENV !== 'development') {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js');
}
ReactDOM.render(<Routes />, document.getElementById('root'));
} else {
ReactDOM.render(
<AppContainer errorReporter={Redbox}>
<Routes />
</AppContainer>,
document.getElementById('root')
);
if (module.hot) {
module.hot.accept('./routes', () => {
// TODO: Remove console override when
// https://github.com/reactjs/react-router/issues/2704 is fixed
const orgError = console.error;
console.error = message => {
if (
message &&
message.indexOf('You cannot change <Router routes>;') === -1
) {
orgError.apply(console, [ message ]);
}
};
const RoutesUpdate = require('./routes').default;
ReactDOM.render(
<AppContainer errorReporter={Redbox}>
<RoutesUpdate />
</AppContainer>,
document.getElementById('root')
);
});
}
}

40
src/routes/async-route.js Normal file
View File

@@ -0,0 +1,40 @@
import React from 'react';
export default getComponent => {
return class AsyncComponent extends React.Component {
static Component = null;
mounted = false;
state = { Component: AsyncComponent.Component };
componentWillMount() {
if (this.state.Component === null) {
getComponent()
.then(m => m.default)
.then(Component => {
AsyncComponent.Component = Component;
if (this.mounted) {
this.setState({ Component });
}
});
}
}
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
}
render() {
const { Component } = this.state;
if (Component !== null) {
return <Component {...this.props} />;
}
return null;
}
};
};

27
src/routes/index.js Normal file
View File

@@ -0,0 +1,27 @@
/* eslint-disable no-console */
import React from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import asyncRoute from './async-route';
import Root from '../containers/root';
import Header from '../components/header';
const Routes = () => (
<Router>
<Root>
<Header />
<Route
exact
path="/"
component={asyncRoute(() => System.import('../components/home'))}
/>
<Route
path="/page2"
component={asyncRoute(() => System.import('../components/page2'))}
/>
</Root>
</Router>
);
export default Routes;

32
test/.setup.js Normal file
View File

@@ -0,0 +1,32 @@
var babelConfig = require('../configuration/babel/babel.test.js');
require('babel-register')(babelConfig);
// Deobfuscate CSS modules classes for testing
require('css-modules-require-hook')({ generateScopedName: '[local]' });
require.extensions['.svg'] = () => null;
var jsdom = require('jsdom').jsdom;
var chai = require('chai');
var sinonChai = require('sinon-chai');
var exposedProperties = [ 'window', 'navigator', 'document' ];
global.document = jsdom('');
global.window = document.defaultView;
global.expect = chai.expect;
chai.use(sinonChai);
Object.keys(document.defaultView).forEach(property => {
if (typeof global[property] === 'undefined') {
exposedProperties.push(property);
global[property] = document.defaultView[property];
}
});
global.navigator = { userAgent: 'node.js' };
documentRef = document;

11
test/specs/routes.spec.js Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
import { shallow } from 'enzyme';
import { BrowserRouter as Router } from 'react-router-dom';
import Routes from '../../src/routes';
describe('Routes', () => {
it('contains spec with an expectation', () => {
expect(shallow(<Routes />).find(Router)).to.have.length(1);
});
});