joyplots working

This commit is contained in:
Colin Megill
2017-09-06 22:19:55 -07:00
parent 129ffa1ec6
commit 0e36aaf830
20 changed files with 680 additions and 146 deletions
-93
View File
@@ -5,99 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#db5945">
<title>Cellx</title>
<style>
/*
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;
}
</style>
</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>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+3 -1
View File
@@ -24,6 +24,7 @@
},
"dependencies": {
"d3": "^4.10.0",
"dsv-loader": "^2.0.0",
"express": "^4.14.0",
"lodash": "^4.17.4",
"react": "^15.3.0",
@@ -31,7 +32,8 @@
"react-helmet": "^4.0.0",
"react-hot-loader": "^3.0.0-beta.2",
"react-router-dom": "4.1.2",
"serve-favicon": "^2.3.0"
"serve-favicon": "^2.3.0",
"whatwg-fetch": "^2.0.3"
},
"devDependencies": {
"autoprefixer": "^6.4.0",
+7
View File
@@ -0,0 +1,7 @@
.primaryButton {
border: 1px solid black;
padding: 10px 15px;
background: none;
cursor: pointer;
font-size: 14px;
}
+10 -1
View File
@@ -42,7 +42,7 @@ const Category = ({category, title}) => (
const Categorical = () => {
return (
<div>
<div style={{marginTop: 50}}>
<h3>Categorical Metadata</h3>
{
_.map(createCategoryCounts(cells),
@@ -66,3 +66,12 @@ export default Categorical;
<p> <button> Tumor [400] [40] [4] </button> </p>
*/
/*
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.
*/
+46 -7
View File
@@ -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 (
<div>
<h3> Continuous Metadata </h3>
</div>
)
constructor(props) {
super(props);
this.state = {
ctx: null,
};
}
componentDidMount() {
drawParallelCoordinates(cells)
}
render() {
return (
<div id="parcoords_wrapper" style={{marginTop: 50}}>
<h3> Continuous Metadata </h3>
<div style={{marginBottom: 30}}>
Color By:
<button style={{marginLeft: 10}}>Cluster_2d_color</button>
<button style={{marginLeft: 10}}>Cluster_CNV_color</button>
<button style={{marginLeft: 10}}>Location.color</button>
<button style={{marginLeft: 10}}>Sample.name.color</button>
<button style={{marginLeft: 10}}>Sample.type.color</button>
<button style={{marginLeft: 10}}>Selection.color</button>
<button style={{marginLeft: 10}}>housekeeping_cluster_color</button>
<button style={{marginLeft: 10}}>recluster_myeloid</button>
<button style={{marginLeft: 10}}>recluster_myeloid_color</button>
</div>
<div
className={styles.parcoords}
id="parcoords"
style={{
width: width + margin.left + margin.right + "px",
height: height + margin.top + margin.bottom + "px"
}}></div>
<pre id="parcoords_output" className={styles.pre}></pre>
</div>
)
}
};
export default Continuous;
@@ -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) {
@@ -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;
}
+4 -2
View File
@@ -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;
+118
View File
@@ -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
];
});
}
+31
View File
@@ -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;
}
+37
View File
@@ -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 (
<div id="joyplot_wrapper" style={{marginTop: 50}}>
<h3> Graph </h3>
<div id="joyplot"> </div>
</div>
)
}
};
export default Graph;
+26
View File
@@ -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 (
<div style={{marginTop: 50}}>
<h3> Heatmap </h3>
</div>
)
}
};
export default Heatmap;
+59 -13
View File
@@ -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 (
<Container>
<Helmet title="cellxgene" />
<h1><Link to="/page2">cellxgene</Link></h1>
<Categorical/>
<Continuous/>
</Container>
)
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 (
<Container>
<Helmet title="cellxgene" />
<h1><Link to="/page2">cellxgene</Link></h1>
<h3 style={{marginTop: 50}}> Gene selection criteria </h3>
<Joy data={this.state.heatmap && this.state.heatmap.data}/>
<Categorical/>
<Continuous/>
<h3 style={{marginTop: 50}}> T-SNE plot </h3>
<button onClick={() => { this.setState({showClustersPlaceholderImage: true}) }} style={{marginBottom: 20}} className={buttonStyles.primaryButton}>Compute clustering using [n] cells in current metadata selection</button>
<Graph data={this.state.graph}/>
{this.state.showClustersPlaceholderImage ? <img width={750} src="https://s10.postimg.org/s6z3zbcvt/tsne.png"></img> : null}
</Container>
)
}
};
export default Home;
+116
View File
@@ -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;
+42
View File
@@ -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;
}
+38
View File
@@ -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 (
<div id="joyplot_wrapper" style={{marginTop: 50}}>
<h3> Joy </h3>
<p> Cell expression distribution per gene & if differential expression, Ie., cells for cluster 5, top genes expressed by cluster 8</p>
<div id="joyplot"> </div>
</div>
)
}
};
export default Joy;
+30
View File
@@ -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;
+4 -12
View File
@@ -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 {
+4 -2
View File
@@ -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.
</p>
<div>
<Link to="/"><button className={styles.primaryButton}>explore a sample dataset</button></Link>
<Link to="/"><button className={buttonStyles.primaryButton}>explore a sample dataset</button></Link>
<span className={styles.or}>or</span>
<button className={styles.primaryButton}>get started with your own data</button>
<button className={buttonStyles.primaryButton}>get started with your own data</button>
</div>
</div>
</Container>