diff --git a/index.html b/index.html index 58a307ec..ddf78831 100644 --- a/index.html +++ b/index.html @@ -5,99 +5,6 @@ Cellx - diff --git a/joyplot.png b/joyplot.png new file mode 100644 index 00000000..3944ab62 Binary files /dev/null and b/joyplot.png differ diff --git a/package.json b/package.json index 15ee7091..620e10c3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/buttons.css b/src/components/buttons.css new file mode 100644 index 00000000..90a34366 --- /dev/null +++ b/src/components/buttons.css @@ -0,0 +1,7 @@ +.primaryButton { + border: 1px solid black; + padding: 10px 15px; + background: none; + cursor: pointer; + font-size: 14px; +} diff --git a/src/components/categorical/categorical.js b/src/components/categorical/categorical.js index 318d4d9a..7e7c88b2 100644 --- a/src/components/categorical/categorical.js +++ b/src/components/categorical/categorical.js @@ -42,7 +42,7 @@ const Category = ({category, title}) => ( const Categorical = () => { return ( -
+

Categorical Metadata

{ _.map(createCategoryCounts(cells), @@ -66,3 +66,12 @@ export default Categorical;

*/ + + +/* + + 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: + + + + + + + + + +
+
+

+      
+ ) + } }; 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 ( +
+

Graph

+
+
+ ) + } +}; + +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.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.

- + or - +