diff --git a/client/src/components/app.js b/client/src/components/app.js index eb46d3a6..e1ec6ea4 100644 --- a/client/src/components/app.js +++ b/client/src/components/app.js @@ -8,6 +8,7 @@ import LeftSideBar from "./leftSidebar"; import RightSideBar from "./rightSidebar"; import Legend from "./continuousLegend"; import Graph from "./graph/graph"; +import Dotplot from "./dotplot"; import MenuBar from "./menubar"; import Autosave from "./autosave"; import Embedding from "./embedding"; @@ -19,6 +20,7 @@ import actions from "../actions"; loading: state.controls.loading, error: state.controls.error, graphRenderCounter: state.controls.graphRenderCounter, + layoutChoice: state.layoutChoice, })) class App extends React.Component { componentDidMount() { @@ -39,7 +41,7 @@ class App extends React.Component { } render() { - const { loading, error, graphRenderCounter } = this.props; + const { loading, error, graphRenderCounter, layoutChoice } = this.props; return ( @@ -77,7 +79,11 @@ class App extends React.Component { - + {layoutChoice.dotplot ? ( + + ) : ( + + )} )} diff --git a/client/src/components/dotplot/index.js b/client/src/components/dotplot/index.js new file mode 100644 index 00000000..4ae65719 --- /dev/null +++ b/client/src/components/dotplot/index.js @@ -0,0 +1,342 @@ +import React from "react"; +import { connect, shallowEqual } from "react-redux"; +// import _regl from "regl"; +import Async from "react-async"; +import memoize from "memoize-one"; + +import * as globals from "../../globals"; +import { createCategorySummaryFromDfCol } from "../../util/stateManager/controlsHelpers"; + +import { + createColorTable, + createColorQuery, +} from "../../util/stateManager/colorHelpers"; + +@connect((state) => ({ + annoMatrix: state.annoMatrix, + layoutChoice: state.layoutChoice, + colors: state.colors, + genesets: state.genesets.genesets, + differential: state.differential, + pointDilation: state.pointDilation, +})) +class Dotplot extends React.Component { + static watchAsync(props, prevProps) { + return !shallowEqual(props.watchProps, prevProps.watchProps); + } + + createCategorySummaryFromDfCol = memoize(createCategorySummaryFromDfCol); + + constructor(props) { + super(props); + const viewport = this.getViewportDimensions(); + + this.state = { + viewport, + }; + } + + componentDidMount() { + window.addEventListener("resize", this.handleResize); + } + + componentWillUnmount() { + window.removeEventListener("resize", this.handleResize); + } + + // handleResize = () => {}; + + getViewportDimensions = () => { + const { viewportRef } = this.props; + return { + height: viewportRef.clientHeight, + width: viewportRef.clientWidth, + }; + }; + + createDot = ( + metadataField, + categoryData, + // colorAccessor, + colorData, + categoryValue + // width, + // height + ) => { + /* + Knowing that colorScale is based off continuous data, + createHistogramBins fetches the continuous data in relation to the cells relevant to the category value. + It then separates that data into 50 bins for drawing the mini-histogram + */ + // const groupBy = categoryData.col(metadataField); + const col = colorData.icol(0); + const mean = this.average(col.asArray()); + + console.log( + categoryValue, + mean + ); /* + + // const histogramMap = col.histogram( + // 50, + // [range.min, range.max], + // groupBy + // ); /* Because the signature changes we really need different names for histogram to differentiate signatures */ + + // const bins = histogramMap.has(categoryValue) + // ? histogramMap.get(categoryValue) + // : new Array(50).fill(0); + + // const xScale = d3.scaleLinear().domain([0, bins.length]).range([0, width]); + + // const largestBin = Math.max(...bins); + + // const yScale = d3.scaleLinear().domain([0, largestBin]).range([0, height]); + + // return { + // xScale, + // yScale, + // bins, + // }; + return null; + }; + + createDots = ( + metadataField, + categoryData, + categorySummary, + colorAccessor = "tissue", + colorData, + width, + height + ) => { + if (!colorData) return null; + + return categorySummary.categoryValues.map((val) => { + return this.createDot( + metadataField, + categoryData, + colorAccessor, + colorData, + val, + width, + height + ); + }); + }; + + average = (array) => array.reduce((a, b) => a + b) / array.length; + + fetchAsyncProps = async (props) => { + const { viewport, annoMatrix, colors } = props.watchProps; + + const [categoryData, categorySummary, colorData] = await this.fetchData( + annoMatrix, + "tissue", + colors + ); + + const { width, height } = viewport; + return { + width, + height, + categoryData, + categorySummary, + colorData, + }; + }; + + async fetchData(annoMatrix, metadataField, colors) { + /* + fetch our data and the color-by data if appropriate, and then build a summary + of our category and a color table for the color-by annotation. + */ + const { schema } = annoMatrix; + const { colorAccessor, colorMode } = colors; + const { genesets, differential } = this.props; + let colorDataPromise = Promise.resolve(null); + if (colorAccessor) { + const query = createColorQuery( + colorMode, + colorAccessor, + schema, + genesets, + differential.diffExp + ); + if (query) colorDataPromise = annoMatrix.fetch(...query); + } + const [categoryData, colorData] = await Promise.all([ + annoMatrix.fetch("obs", metadataField), + colorDataPromise, + ]); + + // our data + const column = categoryData.icol(0); + const colSchema = schema.annotations.obsByName[metadataField]; + + const categorySummary = this.createCategorySummaryFromDfCol( + column, + colSchema + ); + return [categoryData, categorySummary, colorData]; + } + + updateColorTable(colors, colorDf) { + const { annoMatrix } = this.props; + const { schema } = annoMatrix; + + /* update color table state */ + if (!colors || !colorDf) { + return createColorTable( + null, // default mode + null, + null, + schema, + null + ); + } + + const { colorAccessor, userColors, colorMode } = colors; + return createColorTable( + colorMode, + colorAccessor, + colorDf, + schema, + userColors + ); + } + + createColorByQuery(colors) { + const { annoMatrix, genesets, differential } = this.props; + const { schema } = annoMatrix; + const { colorMode, colorAccessor } = colors; + + return createColorQuery( + colorMode, + colorAccessor, + schema, + genesets, + differential.diffExp + ); + } + + render() { + const { viewport } = this.state; + const { annoMatrix, colors, pointDilation } = this.props; + + return ( +
+ + + + + + {(error) => ( + + )} + + + {(asyncProps) => { + const { + colorAccessor, + categoryData, + width, + height, + categorySummary, + colorData, + } = asyncProps; + return ( + + + {this.createDots( + "tissue", + categoryData, + categorySummary, + colorAccessor, + colorData, + width, + height + )} + + ); + }} + + +
+ ); + } +} + +const ErrorLoading = ({ error, width, height }) => { + console.log(error); // log to console as this is an unepected error + return ( +
+ Failure loading dotplot +
+ ); +}; + +const StillLoading = ({ width, height }) => { + /* + Render a busy/loading indicator + */ + return ( +
+
+ Loading dotplot +
+
+ ); +}; + +export default Dotplot; diff --git a/client/src/reducers/layoutChoice.js b/client/src/reducers/layoutChoice.js index d1080f2c..6687af8e 100644 --- a/client/src/reducers/layoutChoice.js +++ b/client/src/reducers/layoutChoice.js @@ -24,6 +24,7 @@ function setToDefaultLayout(schema) { const LayoutChoice = ( state = { available: [], // all available choices + dotplot: true /* todo(colinmegill) #632 wire up inputs */, current: undefined, // name of the current layout, eg, 'umap' currentDimNames: [], // dimension name },