From f6d7cc3e239132d73b4bdcd97062db27fe36b785 Mon Sep 17 00:00:00 2001 From: Colin Megill Date: Wed, 23 Aug 2017 16:28:01 -0700 Subject: [PATCH] initial commit --- .gitignore | 16 + .travis.yml | 29 ++ LICENSE.txt | 20 ++ README.md | 7 + configuration/babel/babel.dev.js | 10 + configuration/babel/babel.prod.js | 11 + configuration/babel/babel.test.js | 9 + configuration/eslint/eslint.js | 29 ++ configuration/polyfills/polyfills.js | 6 + configuration/webpack/webpack.config.dev.js | 74 +++++ configuration/webpack/webpack.config.prod.js | 112 +++++++ favicon.png | Bin 0 -> 43986 bytes index.html | 15 + package.json | 91 +++++ server/development.js | 56 ++++ server/production.js | 31 ++ server/utils.js | 79 +++++ src/components/categorical/categorical.js | 68 ++++ .../categorical/createCategoryCounts.js | 45 +++ src/components/container.css | 5 + src/components/container.js | 11 + src/components/continuous/continuous.js | 18 + .../continuous/createContinuousRanges.js | 25 ++ .../continuous/drawParallelCoordinates.js | 314 ++++++++++++++++++ .../continuous/parallelCoordinates.css | 91 +++++ src/components/continuous/util.js | 162 +++++++++ src/components/header.css | 3 + src/components/header.js | 15 + src/components/home.js | 27 ++ src/components/page2.css | 33 ++ src/components/page2.js | 25 ++ src/containers/root.css | 19 ++ src/containers/root.js | 11 + src/globals.js | 27 ++ src/index.css | 9 + src/index.js | 48 +++ src/routes/async-route.js | 40 +++ src/routes/index.js | 27 ++ test/.setup.js | 32 ++ test/specs/routes.spec.js | 11 + 40 files changed, 1661 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 configuration/babel/babel.dev.js create mode 100644 configuration/babel/babel.prod.js create mode 100644 configuration/babel/babel.test.js create mode 100644 configuration/eslint/eslint.js create mode 100644 configuration/polyfills/polyfills.js create mode 100644 configuration/webpack/webpack.config.dev.js create mode 100644 configuration/webpack/webpack.config.prod.js create mode 100644 favicon.png create mode 100644 index.html create mode 100644 package.json create mode 100644 server/development.js create mode 100644 server/production.js create mode 100644 server/utils.js create mode 100644 src/components/categorical/categorical.js create mode 100644 src/components/categorical/createCategoryCounts.js create mode 100644 src/components/container.css create mode 100644 src/components/container.js create mode 100644 src/components/continuous/continuous.js create mode 100644 src/components/continuous/createContinuousRanges.js create mode 100644 src/components/continuous/drawParallelCoordinates.js create mode 100644 src/components/continuous/parallelCoordinates.css create mode 100644 src/components/continuous/util.js create mode 100644 src/components/header.css create mode 100644 src/components/header.js create mode 100644 src/components/home.js create mode 100644 src/components/page2.css create mode 100644 src/components/page2.js create mode 100644 src/containers/root.css create mode 100644 src/containers/root.js create mode 100644 src/globals.js create mode 100644 src/index.css create mode 100644 src/index.js create mode 100644 src/routes/async-route.js create mode 100644 src/routes/index.js create mode 100644 test/.setup.js create mode 100644 test/specs/routes.spec.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..cd52f01f --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# dependencies +node_modules + +# coverage +coverage +.nyc_output + +# production +build + +# misc +.DS_Store +npm-debug.log +.vscode + +data diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..b5c4d700 --- /dev/null +++ b/.travis.yml @@ -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" diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..e0bb8c7d --- /dev/null +++ b/LICENSE.txt @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..8740e0a6 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +#cellxgene + +##Quickstart: + +* `npm install` +* `npm run dev` +* `localhost:3000` diff --git a/configuration/babel/babel.dev.js b/configuration/babel/babel.dev.js new file mode 100644 index 00000000..858ff07b --- /dev/null +++ b/configuration/babel/babel.dev.js @@ -0,0 +1,10 @@ +module.exports = { + babelrc: false, + cacheDirectory: true, + presets: [ + [ 'es2015', { loose: true, modules: false } ], + 'stage-0', + 'react' + ], + plugins: [ 'react-hot-loader/babel' ] +}; diff --git a/configuration/babel/babel.prod.js b/configuration/babel/babel.prod.js new file mode 100644 index 00000000..5c2e7c65 --- /dev/null +++ b/configuration/babel/babel.prod.js @@ -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') ] ]) +}; diff --git a/configuration/babel/babel.test.js b/configuration/babel/babel.test.js new file mode 100644 index 00000000..e18d5e58 --- /dev/null +++ b/configuration/babel/babel.test.js @@ -0,0 +1,9 @@ +module.exports = { + babelrc: false, + presets: [ + 'babel-preset-es2015', + 'babel-preset-stage-0', + 'babel-preset-react' + ], + plugins: [ 'istanbul' ] +}; diff --git a/configuration/eslint/eslint.js b/configuration/eslint/eslint.js new file mode 100644 index 00000000..5648696b --- /dev/null +++ b/configuration/eslint/eslint.js @@ -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' + } +}; diff --git a/configuration/polyfills/polyfills.js b/configuration/polyfills/polyfills.js new file mode 100644 index 00000000..1aa797e9 --- /dev/null +++ b/configuration/polyfills/polyfills.js @@ -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'); diff --git a/configuration/webpack/webpack.config.dev.js b/configuration/webpack/webpack.config.dev.js new file mode 100644 index 00000000..07e485e0 --- /dev/null +++ b/configuration/webpack/webpack.config.dev.js @@ -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() + ] +}; diff --git a/configuration/webpack/webpack.config.prod.js b/configuration/webpack/webpack.config.prod.js new file mode 100644 index 00000000..9649c166 --- /dev/null +++ b/configuration/webpack/webpack.config.prod.js @@ -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' + }) + ] +}; diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..e1a8bd21d236eff8f7db08d29e13808ffc9e74e5 GIT binary patch literal 43986 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 O0000 + + + + + + Cellx + + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 00000000..15ee7091 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/server/development.js b/server/development.js new file mode 100644 index 00000000..6d95e1f6 --- /dev/null +++ b/server/development.js @@ -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(); +}); diff --git a/server/production.js b/server/production.js new file mode 100644 index 00000000..61b3b3d4 --- /dev/null +++ b/server/production.js @@ -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(); +}); diff --git a/server/utils.js b/server/utils.js new file mode 100644 index 00000000..59160484 --- /dev/null +++ b/server/utils.js @@ -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 }; diff --git a/src/components/categorical/categorical.js b/src/components/categorical/categorical.js new file mode 100644 index 00000000..ddd6d06e --- /dev/null +++ b/src/components/categorical/categorical.js @@ -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}) => ( + +) + +const Category = ({category, title}) => ( +
+

{title}

+ { _.map(Object.keys(category), (c, i) =>
+) + +const Categorical = () => { + + return ( +
+

Categorical Metadata

+ { + _.map(createCategoryCounts(cells), + (value, key) => { + return + }) + } +
+ ) +}; + +export default Categorical; + +/* + + [on off] toggle hide deselected filters (shows a menu vs shows what you ordered in compact/narrative form. fold out animation.) + +

+

+

+

+ +*/ diff --git a/src/components/categorical/createCategoryCounts.js b/src/components/categorical/createCategoryCounts.js new file mode 100644 index 00000000..e70c049a --- /dev/null +++ b/src/components/categorical/createCategoryCounts.js @@ -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; diff --git a/src/components/container.css b/src/components/container.css new file mode 100644 index 00000000..d5e3c461 --- /dev/null +++ b/src/components/container.css @@ -0,0 +1,5 @@ +.container { + max-width: 1200px; + margin: auto; + min-height: 100%; +} diff --git a/src/components/container.js b/src/components/container.js new file mode 100644 index 00000000..bd257989 --- /dev/null +++ b/src/components/container.js @@ -0,0 +1,11 @@ +import React from 'react'; + +import styles from './container.css'; + +const Container = props => ( +
+ {props.children} +
+); + +export default Container; diff --git a/src/components/continuous/continuous.js b/src/components/continuous/continuous.js new file mode 100644 index 00000000..200ee2e4 --- /dev/null +++ b/src/components/continuous/continuous.js @@ -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 ( +
+

Continuous Metadata

+
+ ) +}; + +export default Continuous; diff --git a/src/components/continuous/createContinuousRanges.js b/src/components/continuous/createContinuousRanges.js new file mode 100644 index 00000000..adf26524 --- /dev/null +++ b/src/components/continuous/createContinuousRanges.js @@ -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; diff --git a/src/components/continuous/drawParallelCoordinates.js b/src/components/continuous/drawParallelCoordinates.js new file mode 100644 index 00000000..7323dd15 --- /dev/null +++ b/src/components/continuous/drawParallelCoordinates.js @@ -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; diff --git a/src/components/continuous/parallelCoordinates.css b/src/components/continuous/parallelCoordinates.css new file mode 100644 index 00000000..c5cb3021 --- /dev/null +++ b/src/components/continuous/parallelCoordinates.css @@ -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; +} diff --git a/src/components/continuous/util.js b/src/components/continuous/util.js new file mode 100644 index 00000000..b6bf91c7 --- /dev/null +++ b/src/components/continuous/util.js @@ -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(); diff --git a/src/components/header.css b/src/components/header.css new file mode 100644 index 00000000..41907a8b --- /dev/null +++ b/src/components/header.css @@ -0,0 +1,3 @@ +.header { + background: red; +} diff --git a/src/components/header.js b/src/components/header.js new file mode 100644 index 00000000..f2ddf89f --- /dev/null +++ b/src/components/header.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import Container from './container'; +import styles from './header.css'; + + +const Header = () => ( +
+ + + +
+); + +export default Header; diff --git a/src/components/home.js b/src/components/home.js new file mode 100644 index 00000000..75abccb5 --- /dev/null +++ b/src/components/home.js @@ -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 ( + + +

cellxgene

+ + +
+ ) +}; + +export default Home; + +// +// +// +// diff --git a/src/components/page2.css b/src/components/page2.css new file mode 100644 index 00000000..5ffa58d9 --- /dev/null +++ b/src/components/page2.css @@ -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; +} diff --git a/src/components/page2.js b/src/components/page2.js new file mode 100644 index 00000000..f73d8070 --- /dev/null +++ b/src/components/page2.js @@ -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 = () => ( + + +
+

cellxgene

+

+ A data exploration interface for single cell genetic expression matrices. Subset, filter, cluster & validate, all in one place. +

+
+ + or + +
+
+
+); + +export default Page2; diff --git a/src/containers/root.css b/src/containers/root.css new file mode 100644 index 00000000..2e26aa05 --- /dev/null +++ b/src/containers/root.css @@ -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; + } +*/ diff --git a/src/containers/root.js b/src/containers/root.js new file mode 100644 index 00000000..0cbb0d82 --- /dev/null +++ b/src/containers/root.js @@ -0,0 +1,11 @@ +import React from 'react'; + +import styles from './root.css'; + +const Root = props => ( +
+ {props.children} +
+); + +export default Root; diff --git a/src/globals.js b/src/globals.js new file mode 100644 index 00000000..19c6d554 --- /dev/null +++ b/src/globals.js @@ -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; diff --git a/src/index.css b/src/index.css new file mode 100644 index 00000000..56a96c28 --- /dev/null +++ b/src/index.css @@ -0,0 +1,9 @@ +body { + margin: 0; + padding: 0; + font-family: sans-serif; +} + +* { + box-sizing: border-box; +} \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..75887d63 --- /dev/null +++ b/src/index.js @@ -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(, document.getElementById('root')); +} else { + ReactDOM.render( + + + , + 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 ;') === -1 + ) { + orgError.apply(console, [ message ]); + } + }; + + const RoutesUpdate = require('./routes').default; + + ReactDOM.render( + + + , + document.getElementById('root') + ); + }); + } +} diff --git a/src/routes/async-route.js b/src/routes/async-route.js new file mode 100644 index 00000000..c6dd25ff --- /dev/null +++ b/src/routes/async-route.js @@ -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 ; + } + return null; + } + }; +}; diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 00000000..8f100763 --- /dev/null +++ b/src/routes/index.js @@ -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 = () => ( + + +
+ System.import('../components/home'))} + /> + System.import('../components/page2'))} + /> + + +); + +export default Routes; diff --git a/test/.setup.js b/test/.setup.js new file mode 100644 index 00000000..4fd1c060 --- /dev/null +++ b/test/.setup.js @@ -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; diff --git a/test/specs/routes.spec.js b/test/specs/routes.spec.js new file mode 100644 index 00000000..0967e621 --- /dev/null +++ b/test/specs/routes.spec.js @@ -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().find(Router)).to.have.length(1); + }); +});