Categorical Metadata
{
_.map(createCategoryCounts(cells),
@@ -66,3 +66,12 @@ export default Categorical;
Tumor [400] [40] [4]
*/
+
+
+/*
+
+ Each category has a color associated with it - ie., color by location should show up on these buttons,
+ Does colorby live here? Like a global, with the category labels at the top? If not, we have to
+ duplicate the category names somewhere else - but then, not all (like Cluster_2d_color) won't be listed here.
+
+*/
diff --git a/src/components/continuous/continuous.js b/src/components/continuous/continuous.js
index 200ee2e4..1c56758d 100644
--- a/src/components/continuous/continuous.js
+++ b/src/components/continuous/continuous.js
@@ -3,16 +3,55 @@ import _ from "lodash";
import cells from "../../../data/GBM_metadata.js";
import drawParallelCoordinates from "./drawParallelCoordinates";
import createContinuousRanges from "./createContinuousRanges";
+import styles from './parallelCoordinates.css';
-const Continuous = () => {
+import {
+ margin,
+ width,
+ height
+} from "./util";
- drawParallelCoordinates(cells)
+class Continuous extends React.Component {
- return (
-
-
Continuous Metadata
-
- )
+ constructor(props) {
+ super(props);
+ this.state = {
+ ctx: null,
+
+ };
+ }
+
+ componentDidMount() {
+ drawParallelCoordinates(cells)
+ }
+
+ render() {
+ return (
+
+
Continuous Metadata
+
+ Color By:
+ Cluster_2d_color
+ Cluster_CNV_color
+ Location.color
+ Sample.name.color
+ Sample.type.color
+ Selection.color
+ housekeeping_cluster_color
+ recluster_myeloid
+ recluster_myeloid_color
+
+
+
+
+ )
+ }
};
export default Continuous;
diff --git a/src/components/continuous/drawParallelCoordinates.js b/src/components/continuous/drawParallelCoordinates.js
index 58db8046..07877aab 100644
--- a/src/components/continuous/drawParallelCoordinates.js
+++ b/src/components/continuous/drawParallelCoordinates.js
@@ -112,10 +112,7 @@ const drawParallelCoordinates = (data) => {
******************************************
******************************************/
- var container = d3.select("body").append("div")
- .attr("class", "parcoords")
- .style("width", width + margin.left + margin.right + "px")
- .style("height", height + margin.top + margin.bottom + "px");
+ var container = d3.select("#parcoords")
var svg = container.append("svg")
.attr("width", width + margin.left + margin.right)
@@ -131,18 +128,19 @@ const drawParallelCoordinates = (data) => {
.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 output = d3.select("#parcoords_output");
- var axes = svg.selectAll(".axis")
+ var axes = svg.selectAll(".parcoords_axis")
.data(dimensions)
.enter().append("g")
- .attr("class", "axis")
+ .attr("class", `${styles.axis} parcoords_axis`)
.attr("transform", function(d,i) { return "translate(" + xscale(i) + ")"; });
/*****************************************
@@ -196,7 +194,8 @@ const drawParallelCoordinates = (data) => {
*/
- ctx.strokeStyle = d["Cluster_2d_color"]; //"rgba(0,0,0,.4)" /* color(d.food_group); */
+ ctx.strokeStyle = d["Sample.name.color"];
+ // ctx.strokeStyle = "rgba(180,180,180,.4)";
ctx.beginPath();
var coords = project(d);
coords.forEach((p,i) => {
@@ -240,9 +239,8 @@ const drawParallelCoordinates = (data) => {
render.invalidate();
var actives = [];
- svg.selectAll(".axis .brush")
+ svg.selectAll(".parcoords_axis .parcoords_brush")
.filter(function (d) {
- console.log('this', this)
return d3.brushSelection(this);
})
.each(function(d) {
@@ -263,12 +261,14 @@ const drawParallelCoordinates = (data) => {
}
});
+ /* Reset canvas */
ctx.clearRect(0,0,width,height);
ctx.globalAlpha = d3.min([0.85/Math.pow(selected.length,0.3),1]);
+
+ /* pass the result of the filter above to a fresh canvas, using RAF */
render(selected);
- output.text(d3.tsvFormat(selected.slice(0,24)));
- // console.log('fires brush', actives)
+ output.text(d3.tsvFormat(selected.slice(0,22)));
}
function brushstart() {
@@ -297,13 +297,13 @@ const drawParallelCoordinates = (data) => {
d3.select(this).call(renderAxis);
})
.append("text")
- .attr("class", "title")
+ .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", "brush")
+ .attr("class", `${styles.brush} parcoords_brush`)
.each(function(d) {
d3.select(this).call(d.brush = d3.brushY()
.extent([[-10,0], [10,height]])
@@ -316,7 +316,7 @@ const drawParallelCoordinates = (data) => {
.attr("x", -8)
.attr("width", 16);
- output.text(d3.tsvFormat(data.slice(0,24)));
+ output.text(d3.tsvFormat(data.slice(0,22)));
function project(d) {
return dimensions.map(function(p,i) {
diff --git a/src/components/continuous/parallelCoordinates.css b/src/components/continuous/parallelCoordinates.css
index e69de29b..565e6755 100644
--- a/src/components/continuous/parallelCoordinates.css
+++ b/src/components/continuous/parallelCoordinates.css
@@ -0,0 +1,90 @@
+/*
+
+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;
+ 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
index b8e49b07..5e365005 100644
--- a/src/components/continuous/util.js
+++ b/src/components/continuous/util.js
@@ -1,5 +1,7 @@
-export const margin = {top: 66, right: 110, bottom: 20, left: 188};
-export const width = document.body.clientWidth - margin.left - margin.right;
+const paddingRight = 120;
+
+export const margin = {top: 66, right: 110, bottom: 20, left: 60};
+export const width = document.body.clientWidth - margin.left - margin.right - paddingRight;
export const height = 340 - margin.top - margin.bottom;
export const innerHeight = height - 2;
diff --git a/src/components/graph/drawGraph.js b/src/components/graph/drawGraph.js
new file mode 100644
index 00000000..ec6cf470
--- /dev/null
+++ b/src/components/graph/drawGraph.js
@@ -0,0 +1,118 @@
+var margin = {top: 20, right: 10, bottom: 30, left: 40},
+ width = 960 - margin.left - margin.right,
+ height = 500 - margin.top - margin.bottom;
+
+var svg = d3.select("body").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 factory = d3.geom.quadtree()
+ .extent([
+ [0, 0],
+ [width, height]
+ ]);
+
+var x = d3.scale.linear()
+ .range([0, width]);
+
+var y = d3.scale.linear()
+ .range([height, 0]);
+
+var xAxis = d3.svg.axis()
+ .scale(x)
+ .orient("bottom");
+
+var yAxis = d3.svg.axis()
+ .scale(y)
+ .orient("left");
+
+var xg = svg.append("g")
+ .attr("class", "x axis")
+ .attr("transform", "translate(0," + height + ")");
+
+var yg = svg.append("g")
+ .attr("class", "y axis");
+
+var chartArea = d3.select("body").append("div")
+ .style("left", margin.left + "px")
+ .style("top", margin.top + "px");
+
+var canvas = chartArea.append("canvas")
+ .attr("width", width)
+ .attr("height", height);
+
+var context = canvas.node().getContext("2d");
+
+context.fillStyle = "#f0f";
+
+// Layer on top of canvas, example of selection details
+var highlight = chartArea.append("svg")
+ .attr("width", width)
+ .attr("height", height)
+ .append("circle")
+ .attr("r", 7)
+ .classed("hidden", true);
+
+redraw();
+
+function redraw() {
+
+ // Randomize the scale
+ var scale = 1 + Math.floor(Math.random() * 10);
+
+ // Redraw axes
+ x.domain([0, scale]);
+ y.domain([0, scale]);
+ xg.call(xAxis);
+ yg.call(yAxis);
+
+ var points = randomPoints(scale);
+
+ var tree = factory(points);
+
+ // Update canvas
+ context.clearRect(0, 0, width, height);
+
+ points.forEach(function(p,i){
+
+ context.beginPath();
+ context.arc(x(p[0]), y(p[1]), 5, 0, 2 * Math.PI);
+ context.fill();
+
+ });
+
+ canvas.on("mousemove",function(){
+
+ var mouse = d3.mouse(this),
+ closest = tree.find([x.invert(mouse[0]), y.invert(mouse[1])]);
+
+ highlight.attr("cx", x(closest[0]))
+ .attr("cy", y(closest[1]));
+
+ });
+
+ canvas.on("mouseover",function(){
+ highlight.classed("hidden", false);
+ });
+
+ canvas.on("mouseout",function(){
+ highlight.classed("hidden", true);
+ });
+
+}
+
+function randomPoints(scale) {
+
+ // Get points
+ return d3.range(1000).map(function(d){
+
+ return [
+ Math.random() * scale,
+ Math.random() * scale
+ ];
+
+ });
+
+}
diff --git a/src/components/graph/graph.css b/src/components/graph/graph.css
new file mode 100644
index 00000000..12594efa
--- /dev/null
+++ b/src/components/graph/graph.css
@@ -0,0 +1,31 @@
+path,
+line {
+ shape-rendering: crispEdges;
+}
+
+div,
+svg,
+canvas {
+ position: absolute;
+}
+
+svg {
+ pointer-events: none;
+}
+
+.axis path,
+.axis line {
+ fill: none;
+ stroke: #000;
+ stroke-width: 1px;
+}
+
+circle {
+ stroke-width: 4px;
+ stroke: #000;
+ fill: none;
+}
+
+.hidden {
+ display: none;
+}
diff --git a/src/components/graph/graph.js b/src/components/graph/graph.js
new file mode 100644
index 00000000..807e1856
--- /dev/null
+++ b/src/components/graph/graph.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import _ from "lodash";
+// import styles from "./joy.css";
+// import drawGraph from "./drawGraph";
+// import joyParser from "./joyParser";
+
+class Graph extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+
+ };
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.data) {
+ console.log('graph data 55', nextProps.data)
+ // drawGraph(graphParser(nextProps.data));
+ }
+ }
+
+ componentDidMount() {
+
+ }
+
+ render() {
+ return (
+
+ )
+ }
+};
+
+export default Graph;
diff --git a/src/components/heatmap/heatmap.js b/src/components/heatmap/heatmap.js
new file mode 100644
index 00000000..537c0a68
--- /dev/null
+++ b/src/components/heatmap/heatmap.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import _ from "lodash";
+
+class Heatmap extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+
+ };
+ }
+
+ componentDidMount() {
+
+ }
+
+ render() {
+ return (
+
+
Heatmap
+
+ )
+ }
+};
+
+export default Heatmap;
diff --git a/src/components/home.js b/src/components/home.js
index 75abccb5..6e272579 100644
--- a/src/components/home.js
+++ b/src/components/home.js
@@ -1,22 +1,68 @@
-import React from 'react';
-import { Link } from 'react-router-dom';
+import React from "react";
+import { Link } from "react-router-dom";
import _ from "lodash";
-import Helmet from 'react-helmet';
-import Container from './container';
+import Helmet from "react-helmet";
+import Container from "./container";
+import buttonStyles from "./buttons.css";
import Categorical from "./categorical/categorical";
import Continuous from "./continuous/continuous";
+import Joy from "./joy/joy";
+import Graph from "./graph/graph";
-const Home = () => {
+class Home extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ showClustersPlaceholderImage: false,
+ heatmap: null,
+ graph: null,
+ };
+ }
+ componentWillMount() {
+ // d3.csv("http://ec2-34-234-75-156.compute-1.amazonaws.com/api/v0.1/metadata", (d) => {
+ // console.log("from ec2",d)
+ // })
- return (
-
-
- cellxgene
-
-
-
- )
+ const prefix = "http://ec2-34-234-75-156.compute-1.amazonaws.com/api/";
+ const version = "v0.1/";
+
+ const expressions = fetch(`${prefix}${version}expression`, {
+ method: "post",
+ headers: new Headers({
+ 'Content-Type': 'application/json'
+ }),
+ body: JSON.stringify({
+ "celllist": ["1001000173.G8", "1001000173.D4"],
+ "genelist": ["1/2-SBSRNA4", "A1BG", "A1BG-AS1", "A1CF", "A2LD1", "A2M", "A2ML1", "A2MP1", "A4GALT"]
+ })
+ })
+ // const expressions = fetch(`${prefix}${version}${expression}`)
+ .then((res) => res.json())
+ .then((data) => { this.setState({heatmap: data}) })
+
+ const graph = fetch(`${prefix}${version}graph`)
+ .then((res) => res.json())
+ .then((data) => { this.setState({graph: data}) })
+
+ }
+ render() {
+ return (
+
+
+ cellxgene
+ Gene selection criteria
+
+
+
+
+ T-SNE plot
+ { this.setState({showClustersPlaceholderImage: true}) }} style={{marginBottom: 20}} className={buttonStyles.primaryButton}>Compute clustering using [n] cells in current metadata selection
+
+ {this.state.showClustersPlaceholderImage ? : null}
+
+ )
+ }
};
export default Home;
diff --git a/src/components/joy/drawJoy.js b/src/components/joy/drawJoy.js
new file mode 100644
index 00000000..cc094375
--- /dev/null
+++ b/src/components/joy/drawJoy.js
@@ -0,0 +1,116 @@
+import styles from "./joy.css";
+
+/*
+ via https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3
+*/
+
+var margin = { top: 30, right: 10, bottom: 30, left: 100 },
+ 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 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 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 line = area.lineY1();
+
+function parseTime(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
+ };
+}
+
+const 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 + ')');
+
+ 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; })
+ .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); });
+ return d.values[i].time;
+ };
+ data.sort(function(a, b) { return peakTime(b) - peakTime(a); });
+
+ console.log('sorted', data)
+
+ xScale.domain(d3.extent(dataFlat, x));
+
+ activityScale.domain(data.map(function(d) { return d.key; }));
+
+ var areaChartHeight = (1 + overlap) * (height / activityScale.domain().length);
+
+ yScale
+ .domain(d3.extent(dataFlat, y))
+ .range([areaChartHeight, 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 + ')';
+ });
+
+ 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);
+
+ 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"]}`)
+ .call(activityAxis);
+
+ })
+}
+
+export default drawJoy;
diff --git a/src/components/joy/joy.css b/src/components/joy/joy.css
new file mode 100644
index 00000000..a888cdb5
--- /dev/null
+++ b/src/components/joy/joy.css
@@ -0,0 +1,42 @@
+svg {
+ display: block;
+ /*margin: 0 auto;*/
+}
+
+.axis .domain {
+ display: none;
+}
+
+.axis--x text {
+ fill: #999;
+}
+
+.axis--x line {
+ stroke: #aaa;
+}
+
+.axis--activity .tick line {
+ display: none;
+}
+
+.axis--activity text {
+ font-size: 12px;
+ fill: #000;
+}
+
+/*.axis--activity .tick:nth-child(odd) text {
+ fill: #222;
+}*/
+
+.line {
+ fill: none;
+ stroke: #fff;
+}
+
+.area {
+ fill: #448cab;
+}
+
+.activity:nth-child(odd) .area {
+ fill: #5ca3c1;
+}
diff --git a/src/components/joy/joy.js b/src/components/joy/joy.js
new file mode 100644
index 00000000..23efefb0
--- /dev/null
+++ b/src/components/joy/joy.js
@@ -0,0 +1,38 @@
+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 = {
+
+ };
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.data) {
+ console.log('joyplot data 44', nextProps.data)
+ drawJoy(joyParser(nextProps.data));
+ }
+ }
+
+ componentDidMount() {
+
+ }
+
+ render() {
+ return (
+
+
Joy
+
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
new file mode 100644
index 00000000..6ff1bf4d
--- /dev/null
+++ b/src/components/joy/joyParser.js
@@ -0,0 +1,30 @@
+
+
+
+
+const joyParser = (data, count = 20) => {
+ const genes = [];
+
+ /* setup */
+
+ for (let i = 0; i < count; i++) {
+ const gene = {
+ key: data.genes[i], /* key values naming: https://bl.ocks.org/armollica/3b5f83836c1de5cca7b1d35409a013e3 */
+ values: []
+ }
+
+ data.cells.forEach((cell) => {
+ gene.values.push({
+ value: cell["e"][i]
+ })
+ })
+
+ genes.push(gene)
+ }
+
+
+
+ return genes;
+}
+
+export default joyParser;
diff --git a/src/components/page2.css b/src/components/page2.css
index 5ffa58d9..db9b2442 100644
--- a/src/components/page2.css
+++ b/src/components/page2.css
@@ -8,22 +8,14 @@
}
.hero {
- font-size: 48;
+ font-size: 48px;
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;
+ font-size: 18px;
+ max-width: 600px;
+ text-align: center;
}
.or {
diff --git a/src/components/page2.js b/src/components/page2.js
index f73d8070..47098569 100644
--- a/src/components/page2.js
+++ b/src/components/page2.js
@@ -2,6 +2,8 @@ import React from 'react';
import { Link } from 'react-router-dom';
import Helmet from 'react-helmet';
import styles from './page2.css';
+import buttonStyles from './buttons.css';
+
import Container from './container';
@@ -14,9 +16,9 @@ const Page2 = () => (
A data exploration interface for single cell genetic expression matrices. Subset, filter, cluster & validate, all in one place.
- explore a sample dataset
+ explore a sample dataset
or
- get started with your own data
+ get started with your own data