Refactor CSS and React logic for layout (#1424)

* Refactor CSS layout and react logic for layout

Fixes https://github.com/chanzuckerberg/cellxgene/issues/1022

* Menubar should wrap inside middle pane instead of overlapping left
  sidebar when window is scrunched
* cellxgene should have a minimum width of 1240px

1. Replace absolute positioning and dimension calculation with css grid
2. Use flexbox for wrapping menubar buttons

* Middle pane (graph) can calculate its own size
* Removing components calculating their size/position relative to
  eachother increases modularity, decreases use of global variables
* Improved some scrollbar behavior
* Removed responsive reducer, propagating window size to components
  triggers unnecessary events and encourages breaking modularity; doing
  this made some components state agnostic

Reference: https://css-tricks.com/snippets/css/complete-guide-grid/

* Reposition the continuous legend

* Small fixes

* Respond to feedback from @colinmegill

* Respond to feedback from @colinmegill

Add more documentation on the renderGraph method.
This commit is contained in:
Matt Weiden
2020-05-06 13:46:13 -07:00
committed by GitHub
parent a7d641ea6b
commit 888f730526
30 changed files with 451 additions and 417 deletions
+5 -5
View File
@@ -27,7 +27,7 @@ export const datasets = {
lasso: [
{
"coordinates-as-percent": { x1: 0.1, y1: 0.25, x2: 0.7, y2: 0.75 },
count: "1181",
count: "1173"
},
],
categorical: [
@@ -98,7 +98,7 @@ export const datasets = {
},
lasso: {
"coordinates-as-percent": { x1: 0.25, y1: 0.05, x2: 0.75, y2: 0.55 },
count: "329",
count: "331",
},
},
scatter: {
@@ -121,8 +121,8 @@ export const datasets = {
},
newCount: {
bySubsetConfig: {
false: "600",
true: "591",
false: "599",
true: "594",
},
},
},
@@ -130,7 +130,7 @@ export const datasets = {
count: {
bySubsetConfig: {
false: "1161",
true: "856",
true: "852",
},
},
},
+18 -31
View File
@@ -4,6 +4,7 @@ import Helmet from "react-helmet";
import { connect } from "react-redux";
import Container from "./framework/container";
import Layout from "./framework/layout";
import LeftSideBar from "./leftSidebar";
import RightSideBar from "./rightSidebar";
import Legend from "./continuousLegend";
@@ -20,10 +21,6 @@ import actions from "../actions";
graphRenderCounter: state.controls.graphRenderCounter,
}))
class App extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
const { dispatch } = this.props;
@@ -33,24 +30,7 @@ class App extends React.Component {
this._onURLChanged();
dispatch(actions.doInitialDataLoad(window.location.search));
/* listen for resize events */
window.addEventListener("resize", () => {
dispatch({
type: "window resize",
data: {
height: window.innerHeight,
width: window.innerWidth,
},
});
});
dispatch({
type: "window resize",
data: {
height: window.innerHeight,
width: window.innerWidth,
},
});
this.forceUpdate();
}
_onURLChanged() {
@@ -88,15 +68,22 @@ class App extends React.Component {
error loading
</div>
) : null}
<div>
{loading ? null : <LeftSideBar />}
{loading ? null : <RightSideBar />}
{loading ? null : <MenuBar />}
{loading ? null : <Graph key={graphRenderCounter} />}
{loading ? null : <Autosave />}
{loading ? null : <TermsOfServicePrompt />}
<Legend />
</div>
{loading ? null : <Layout>
<LeftSideBar/>
{viewportRef =>
<>
<MenuBar/>
<Autosave/>
<TermsOfServicePrompt/>
<Legend viewportRef={viewportRef}/>
<Graph
key={graphRenderCounter}
viewportRef={viewportRef}
/>
</>
}
<RightSideBar/>
</Layout>}
</Container>
);
}
+4 -3
View File
@@ -80,10 +80,11 @@ class Autosave extends React.Component {
: "autosave-complete"
}
style={{
position: "fixed",
position: "absolute",
display: "inherit",
right: globals.leftSidebarWidth + 5,
bottom: 5,
right: 8,
bottom: 8,
zIndex: 1,
}}
>
{this.statusMessage()}
@@ -88,8 +88,8 @@ class HistogramBrush extends React.PureComponent {
constructor(props) {
super(props);
this.marginLeft = 12; // Space for 0 tick label on X axis
this.marginRight = 52; // space for Y axis & labels
this.marginLeft = 10; // Space for 0 tick label on X axis
this.marginRight = 54; // space for Y axis & labels
this.marginBottom = 25; // space for X axis & labels
this.marginTop = 3;
@@ -3,7 +3,6 @@ import React from "react";
import { connect } from "react-redux";
import * as d3 from "d3";
import { interpolateCool } from "d3-scale-chromatic";
import * as globals from "../../globals";
// create continuous color legend
// http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f
@@ -97,22 +96,12 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
@connect((state) => ({
colorAccessor: state.colors.colorAccessor,
colorScale: state.colors.scale,
responsive: state.responsive,
}))
class ContinuousLegend extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidUpdate(prevProps) {
const { colorAccessor, responsive, colorScale } = this.props;
if (
prevProps.colorAccessor !== colorAccessor ||
prevProps.colorScale !== colorScale ||
prevProps.responsive.height !== responsive.height ||
prevProps.responsive.width !== responsive.width
) {
const { colorAccessor, colorScale } = this.props;
if (prevProps.colorAccessor !== colorAccessor || prevProps.colorScale !== colorScale) {
/* always remove it, if it's not continuous we don't put it back. */
d3.select("#continuous_legend").selectAll("*").remove();
}
@@ -130,15 +119,17 @@ class ContinuousLegend extends React.Component {
}
render() {
const { colorAccessor, responsive } = this.props;
const { colorAccessor } = this.props;
return (
<div
id="continuous_legend"
ref={ref => {this.ref = ref}}
style={{
position: "fixed",
display: colorAccessor ? "inherit" : "none",
right: globals.leftSidebarWidth,
top: responsive.height / 2,
position: "absolute",
left: 8,
top: 35,
zIndex: 1,
}}
/>
);
@@ -1,3 +0,0 @@
:local(.container) {
min-height: 100%;
}
+15 -6
View File
@@ -1,11 +1,20 @@
// jshint esversion: 6
import React from "react";
import styles from "./container.css";
const Container = (props) => {
const { children } = props;
return <div className={styles.container}>{children}</div>;
};
function Container(props) {
const {children} = props;
return <div
className="container"
style={{
height: "calc(100vh - (100vh - 100%))",
width: "calc(100vw - (100vw - 100%))",
position: "absolute",
top: 0,
left: 0,
}}
>
{children}
</div>
}
export default Container;
+86
View File
@@ -0,0 +1,86 @@
// jshint esversion: 6
import React from "react";
import * as globals from "../../globals";
class Layout extends React.Component {
/*
Layout - this react component contains all the layout style and logic for the application once it has loaded.
The layout is based on CSS grid: the left and right sidebars have fixed widths, the graph in the middle takes the
remaining space.
Note, the renderGraph child is a function rather than a fully-instantiated element because the middle pane of the
app is dynamically-sized. It must have access to the containing viewport in order to know how large the graph
should be.
*/
componentDidMount() {
/*
This is a bit of a hack. In order for the graph to size correctly, it needs to know the size of the parent
viewport. Unfortunately, it can only do this once the parent div has been rendered, so we need to render twice.
*/
this.forceUpdate();
}
render() {
const { children } = this.props;
const [ leftSidebar, renderGraph, rightSidebar ] = children;
return <div
style={{
display: "grid",
gridTemplateColumns: `
[left-sidebar-start] ${globals.leftSidebarWidth+1}px
[left-sidebar-end graph-start] auto
[graph-end right-sidebar-start] ${globals.rightSidebarWidth+1}px [right-sidebar-end]
`,
gridTemplateRows: "[top] auto [bottom]",
gridTemplateAreas: "left-sidebar | graph | right-sidebar",
columnGap: "0px",
justifyItems: "stretch",
alignItems: "stretch",
height: "inherit",
width: "inherit",
position: "relative",
top: 0,
left: 0,
minWidth: "1240px",
}}
>
<div
style={{
gridArea: "top / left-sidebar-start / bottom / left-sidebar-end",
position: "relative",
height: "inherit",
overflowY: "auto"
}}
>
{leftSidebar}
</div>
<div
style={{
zIndex: 0,
gridArea: "top / graph-start / bottom / graph-end",
position: "relative",
height: "inherit",
}}
ref={ref => { this.viewportRef = ref; }}
>
{this.viewportRef ? renderGraph(this.viewportRef) : null}
</div>
<div
style={{
gridArea: "top / right-sidebar-start / bottom / right-sidebar-end",
position: "relative",
height: "inherit",
overflowY: "auto"
}}
>
{rightSidebar}
</div>
</div>;
}
}
export default Layout;
@@ -214,7 +214,11 @@ class GeneExpression extends React.Component {
if (!varIndex) return null;
return (
<div>
<div
style={{
borderBottom: `1px solid ${globals.lighterGrey}`,
}}
>
<div>
<div
style={{
-18
View File
@@ -1,21 +1,3 @@
:local(.graphCanvas) path {
shape-rendering: crispEdges;
}
#graphWrapper {
position: relative;
}
#graphAttachPoint,
.graphSVG,
.graphCanvas {
position: absolute;
}
#graphSVG {
pointer-events: none;
}
:local(.axis) path,
:local(.axis) line {
fill: none;
+105 -104
View File
@@ -6,7 +6,6 @@ import { mat3, vec2 } from "gl-matrix";
import _regl from "regl";
import memoize from "memoize-one";
import * as globals from "../../globals";
import setupSVGandBrushElements from "./setupSVGandBrush";
import _camera from "../../util/camera";
import _drawPoints from "./drawPointsRegl";
@@ -77,7 +76,6 @@ function renderThrottle(callback) {
universe: state.universe,
world: state.world,
crossfilter: state.crossfilter,
responsive: state.responsive,
colorRGB: state.colors.rgb,
selectionTool: state.graphSelection.tool,
currentSelection: state.graphSelection.selection,
@@ -177,9 +175,8 @@ class Graph extends React.Component {
constructor(props) {
super(props);
const viewport = this.getViewportDimensions();
this.count = 0;
this.graphPaddingTop = 0;
this.graphPaddingRightLeft = globals.leftSidebarWidth * 2;
this.renderCache = {
X: null,
Y: null,
@@ -193,10 +190,13 @@ class Graph extends React.Component {
tool: null,
container: null,
cameraRender: 0,
viewport
};
}
componentDidMount() {
window.addEventListener("resize", this.handleResize);
// setup canvas, webgl draw function and camera
const camera = _camera(this.reglCanvas);
const regl = _regl(this.reglCanvas);
@@ -209,10 +209,7 @@ class Graph extends React.Component {
// create all default rendering transformations
const modelTF = createModelTF();
const projectionTF = createProjectionTF(
this.reglCanvas.width,
this.reglCanvas.height
);
const projectionTF = createProjectionTF(this.reglCanvas.width, this.reglCanvas.height);
// initial draw to canvas
this.renderPoints(
@@ -238,13 +235,12 @@ class Graph extends React.Component {
});
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps, prevState) {
const { renderCache } = this;
const {
world,
crossfilter,
colorRGB,
responsive,
selectionTool,
currentSelection,
layoutChoice,
@@ -252,26 +248,20 @@ class Graph extends React.Component {
pointDilation,
colorAccessor,
} = this.props;
const { regl, toolSVG, camera, modelTF } = this.state;
const { regl, toolSVG, camera, modelTF, viewport } = this.state;
let { projectionTF } = this.state;
const hasResized = prevState.viewport.height !== this.reglCanvas.height ||
prevState.viewport.width !== this.reglCanvas.width;
let stateChanges = {};
let needsRepaint = hasResized;
if (regl && world && crossfilter) {
/* update the regl and point rendering state */
const { obsLayout, nObs } = world;
const { drawPoints, pointBuffer, colorBuffer, flagBuffer } = this.state;
let { projectionTF } = this.state;
let needsRepaint = false;
if (
prevProps.responsive.height !== responsive.height ||
prevProps.responsive.width !== responsive.width
) {
projectionTF = createProjectionTF(
this.reglCanvas.width,
this.reglCanvas.height
);
needsRepaint = true;
if (hasResized) {
projectionTF = createProjectionTF(this.reglCanvas.width, this.reglCanvas.height);
stateChanges = {
...stateChanges,
projectionTF,
@@ -326,19 +316,13 @@ class Graph extends React.Component {
}
}
if (
prevProps.responsive.height !== responsive.height ||
prevProps.responsive.width !== responsive.width
) {
if (hasResized) {
// If the window size has changed we want to recreate all SVGs
stateChanges = {
...stateChanges,
...this.createToolSVG(),
};
} else if (
(responsive.height && responsive.width && !toolSVG) ||
selectionTool !== prevProps.selectionTool
) {
} else if ((viewport.height && viewport.width && !toolSVG) || selectionTool !== prevProps.selectionTool ) {
// first time or change of selection tool
stateChanges = { ...stateChanges, ...this.createToolSVG() };
} else if (prevProps.graphInteractionMode !== graphInteractionMode) {
@@ -365,11 +349,34 @@ class Graph extends React.Component {
);
}
if (Object.keys(stateChanges).length > 0) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState(stateChanges);
}
}
handleCanvasEvent = (e) => {
componentWillUnmount() {
window.removeEventListener("resize", this.handleResize);
}
handleResize = () => {
const { state } = this.state;
const viewport = this.getViewportDimensions();
this.setState({
...state,
viewport,
});
};
getViewportDimensions = () => {
const { viewportRef } = this.props;
return {
height: viewportRef.clientHeight,
width: viewportRef.clientWidth
};
};
handleCanvasEvent = e => {
const { camera, projectionTF } = this.state;
if (e.type !== "wheel") e.preventDefault();
if (camera.handleEvent(e, projectionTF)) {
@@ -385,7 +392,7 @@ class Graph extends React.Component {
Called from componentDidUpdate. Create the tool SVG, and return any
state changes that should be passed to setState().
*/
const { responsive, selectionTool, graphInteractionMode } = this.props;
const { viewport, selectionTool, graphInteractionMode } = this.props;
/* clear out whatever was on the div, even if nothing, but usually the brushes etc */
@@ -419,8 +426,7 @@ class Graph extends React.Component {
handleDrag,
handleEnd,
handleCancel,
responsive,
this.graphPaddingRightLeft
viewport
);
return { toolSVG: newToolSVG, tool, container };
@@ -512,14 +518,12 @@ class Graph extends React.Component {
accounting for current pan/zoom camera.
*/
const { responsive } = this.props;
const { camera, projectionTF, modelInvTF } = this.state;
const { camera, projectionTF, modelInvTF, viewport } = this.state;
const cameraInvTF = camera.invView();
/* screen -> gl */
const x =
(2 * pin[0]) / (responsive.width - this.graphPaddingRightLeft) - 1;
const y = 2 * (1 - pin[1] / (responsive.height - this.graphPaddingTop)) - 1;
const x = (2 * pin[0]) / viewport.width - 1;
const y = 2 * (1 - pin[1] / viewport.height) - 1;
const xy = vec2.fromValues(x, y);
const projectionInvTF = mat3.invert(mat3.create(), projectionTF);
@@ -535,23 +539,21 @@ class Graph extends React.Component {
of mapScreenToPoint()
*/
const { responsive } = this.props;
const { camera, projectionTF, modelTF } = this.state;
const { camera, projectionTF, modelTF, viewport } = this.state;
const cameraTF = camera.view();
const xy = vec2.transformMat3(vec2.create(), xyCell, modelTF);
vec2.transformMat3(xy, xy, cameraTF);
vec2.transformMat3(xy, xy, projectionTF);
const pin = [
return [
Math.round(
((xy[0] + 1) * (responsive.width - this.graphPaddingRightLeft)) / 2
(xy[0] + 1) * viewport.width / 2
),
Math.round(
-((xy[1] + 1) / 2 - 1) * (responsive.height - this.graphPaddingTop)
),
-((xy[1] + 1) / 2 - 1) * viewport.height
)
];
return pin;
}
handleBrushDragAction() {
@@ -696,7 +698,7 @@ class Graph extends React.Component {
count: this.count,
projView,
nPoints: universe.nObs,
minViewportDimension: Math.min(width || 800, height || 600),
minViewportDimension: Math.min(width, height)
});
regl._gl.flush();
}
@@ -723,66 +725,65 @@ class Graph extends React.Component {
});
render() {
const { responsive, graphInteractionMode } = this.props;
const { modelTF, projectionTF, camera } = this.state;
const { graphInteractionMode } = this.props;
const { modelTF, projectionTF, camera, viewport } = this.state;
const cameraTF = camera?.view()?.slice();
return (
<div id="graphWrapper">
<div
style={{
zIndex: -9999,
position: "fixed",
top: this.graphPaddingTop,
right: globals.leftSidebarWidth,
}}
<div
id="graph-wrapper"
style={{
position: "relative",
top: 0,
left: 0,
}}
>
<GraphOverlayLayer
width={viewport.width}
height={viewport.height}
cameraTF={cameraTF}
modelTF={modelTF}
projectionTF={projectionTF}
handleCanvasEvent={graphInteractionMode === "zoom" ? this.handleCanvasEvent : undefined}
>
<div id="graphAttachPoint">
<GraphOverlayLayer
cameraTF={cameraTF}
modelTF={modelTF}
projectionTF={projectionTF}
graphPaddingRightLeft={this.graphPaddingRightLeft}
graphPaddingTop={this.graphPaddingTop}
responsive={responsive}
handleCanvasEvent={
graphInteractionMode === "zoom"
? this.handleCanvasEvent
: undefined
}
>
<CentroidLabels />
</GraphOverlayLayer>
<svg
id="lasso-layer"
data-testid="layout-overlay"
className={styles.graphSVG}
width={responsive.width - this.graphPaddingRightLeft}
height={responsive.height}
pointerEvents={
graphInteractionMode === "select" ? "auto" : "none"
}
style={{ zIndex: 89 }}
/>
</div>
<div style={{ padding: 0, margin: 0 }}>
<canvas
width={responsive.width - this.graphPaddingRightLeft}
height={responsive.height - this.graphPaddingTop}
data-testid="layout-graph"
ref={(canvas) => {
this.reglCanvas = canvas;
}}
onMouseDown={this.handleCanvasEvent}
onMouseUp={this.handleCanvasEvent}
onMouseMove={this.handleCanvasEvent}
onDoubleClick={this.handleCanvasEvent}
onWheel={this.handleCanvasEvent}
/>
</div>
</div>
<CentroidLabels/>
</GraphOverlayLayer>
<svg
id="lasso-layer"
data-testid="layout-overlay"
className="graph-svg"
style={{
position: "absolute",
top: 0,
left: 0,
zIndex: 1
}}
width={viewport.width}
height={viewport.height}
pointerEvents={
graphInteractionMode === "select" ? "auto" : "none"
}
/>
<canvas
width={viewport.width}
height={viewport.height}
style={{
position: "absolute",
top: 0,
left: 0,
padding: 0,
margin: 0,
shapeRendering: "crispEdges",
}}
className="graph-canvas"
data-testid="layout-graph"
ref={canvas => { this.reglCanvas = canvas; }}
onMouseDown={this.handleCanvasEvent}
onMouseUp={this.handleCanvasEvent}
onMouseMove={this.handleCanvasEvent}
onDoubleClick={this.handleCanvasEvent}
onWheel={this.handleCanvasEvent}
/>
</div>
);
}
@@ -13,7 +13,8 @@ export default
}))
class CentroidLabels extends PureComponent {
// Check to see if centroids have either just been displayed or removed from the overlay
componentDidUpdate = (prevProps) => {
componentDidUpdate(prevProps) {
const { labels, overlayToggled } = this.props;
const prevSize = prevProps.labels.size;
const { size } = labels;
@@ -1,12 +1,8 @@
import React, { PureComponent, cloneElement } from "react";
import { connect } from "react-redux";
import styles from "../graph.css";
export default
@connect((state) => ({
responsive: state.responsive,
}))
class GraphOverlayLayer extends PureComponent {
/*
This component takes its children (assumed in the data coordinate space ([0, 1] range, origin in bottom left corner))
@@ -16,7 +12,6 @@ class GraphOverlayLayer extends PureComponent {
*/
constructor(props) {
super(props);
this.state = {
display: {},
};
@@ -50,16 +45,16 @@ class GraphOverlayLayer extends PureComponent {
cameraTF,
modelTF,
projectionTF,
responsive,
graphPaddingRightLeft,
graphPaddingTop,
children,
handleCanvasEvent,
width,
height,
style
} = this.props;
const { display } = this.state;
if (!cameraTF) return null;
const { display } = this.state;
const displaying = Object.values(display).some((value) => value); // check to see if at least one overlay is currently displayed
const inverseTransform = `${this.reverseMatrixScaleTransformString(
@@ -68,9 +63,7 @@ class GraphOverlayLayer extends PureComponent {
cameraTF
)} ${this.reverseMatrixScaleTransformString(
projectionTF
)} scale(1 2) scale(1 ${
1 / -(responsive.height - graphPaddingTop)
}) scale(2 1) scale(${1 / (responsive.width - graphPaddingRightLeft)} 1)`;
)} scale(1 2) scale(1 ${1 / (-height)}) scale(2 1) scale(${1 / width} 1)`;
// Copy the children passed with the overlay and add the inverse transform and onDisplayChange props
const newChildren = React.Children.map(children, (child) =>
@@ -83,11 +76,14 @@ class GraphOverlayLayer extends PureComponent {
return (
<svg
className={styles.graphSVG}
width={responsive.width - graphPaddingRightLeft}
height={responsive.height}
width={width}
height={height}
pointerEvents="none"
style={{
zIndex: 99,
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
backgroundColor: displaying ? "rgba(255, 255, 255, 0.55)" : "",
}}
onMouseMove={handleCanvasEvent}
@@ -95,15 +91,11 @@ class GraphOverlayLayer extends PureComponent {
>
<g
id="canvas-transformation-group-x"
transform={`scale(${
responsive.width - graphPaddingRightLeft
} 1) scale(.5 1) translate(1 0)`}
transform={`scale(${width} 1) scale(.5 1) translate(1 0)`}
>
<g
id="canvas-transformation-group-y"
transform={`scale(1 ${-(
responsive.height - graphPaddingTop
)}) translate(0 -1) scale(1 .5) translate(0 1)`}
transform={`scale(1 ${-height}) translate(0 -1) scale(1 .5) translate(0 1)`}
>
<g
id="projection-transformation-group"
@@ -14,17 +14,16 @@ export default (
handleDragAction,
handleEndAction,
handleCancelAction,
responsive,
graphPaddingRight
viewport,
) => {
const svg = d3.select("#graphAttachPoint").select("#lasso-layer");
const svg = d3.select("#graph-wrapper").select("#lasso-layer");
if (selectionToolType === "brush") {
const brush = d3
.brush()
.extent([
[0, 0],
[responsive.width - graphPaddingRight, responsive.height],
[viewport.width, viewport.height]
])
.on("start", handleStartAction)
.on("brush", handleDragAction)
+6 -18
View File
@@ -7,41 +7,29 @@ import DynamicScatterplot from "../scatterplot/scatterplot";
import TopLeftLogoAndTitle from "./topLeftLogoAndTitle";
@connect((state) => ({
responsive: state.responsive,
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
}))
class LeftSideBar extends React.Component {
render() {
const {
responsive,
scatterplotXXaccessor,
scatterplotYYaccessor,
} = this.props;
/*
this magic number should be made less fragile,
if cellxgene logo or tabs change, this must as well
*/
const logoRelatedPadding = 50;
const { scatterplotXXaccessor, scatterplotYYaccessor } = this.props;
return (
<div
style={{
position: "fixed",
backgroundColor: "white",
/* x y blur spread color */
borderRight: `1px solid ${globals.lightGrey}`,
display: "flex",
flexDirection: "column",
height: "100%",
}}
>
<TopLeftLogoAndTitle />
<div
style={{
height: responsive.height - logoRelatedPadding,
marginTop: logoRelatedPadding,
height: "100%",
width: globals.leftSidebarWidth,
overflowY: "auto",
overflowX: "hidden",
}}
>
<Categorical />
@@ -4,8 +4,7 @@ import { connect } from "react-redux";
import * as globals from "../../globals";
import Logo from "../framework/logo";
@connect((state) => ({
responsive: state.responsive,
@connect(state => ({
datasetTitle: state.config?.displayNames?.dataset ?? "",
aboutURL: state.config?.links?.["about-dataset"],
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
@@ -15,8 +14,6 @@ class LeftSideBar extends React.Component {
render() {
const { datasetTitle, aboutURL } = this.props;
const paddingToAvoidScrollBar = 15;
const displayTitle =
datasetTitle.length > globals.datasetTitleMaxCharacterCount
? `${datasetTitle.substring(
@@ -32,12 +29,9 @@ class LeftSideBar extends React.Component {
style={{
paddingLeft: 8,
paddingTop: 8,
width: globals.leftSidebarWidth - paddingToAvoidScrollBar,
position: "absolute",
backgroundColor: "white",
zIndex: 8888,
/* x y blur spread color */
// boxShadow: "-5px -1px 4px 2px rgba(225,225,225,0.4)"
width: globals.leftSidebarWidth,
zIndex: 1,
borderBottom: `1px solid ${globals.lighterGrey}`,
}}
>
<Logo size={30} />
+2 -4
View File
@@ -9,6 +9,7 @@ import {
Tooltip,
} from "@blueprintjs/core";
import { tooltipHoverOpenDelay } from "../../globals";
import styles from "./menubar.css";
function Clip(props) {
const {
@@ -35,10 +36,7 @@ function Clip(props) {
return (
<div
className="bp3-button-group"
style={{
marginRight: 10,
}}
className={`bp3-button-group ${styles.menubarButton}`}
>
<Popover
target={
@@ -10,6 +10,7 @@ import {
Position,
} from "@blueprintjs/core";
import * as globals from "../../globals";
import styles from "./menubar.css";
import actions from "../../actions";
import CellSetButton from "./cellSetButtons";
@@ -85,7 +86,7 @@ class DiffexpButtons extends React.Component {
diffexpCellcountMax;
return (
<ButtonGroup style={{ marginRight: 10 }}>
<ButtonGroup className={styles.menubarButton} >
<CellSetButton
{...this.props} // eslint-disable-line react/jsx-props-no-spreading
eitherCellSetOneOrTwo={1}
+2 -3
View File
@@ -11,6 +11,7 @@ import {
} from "@blueprintjs/core";
import { connect } from "react-redux";
import * as globals from "../../globals";
import styles from "./menubar.css";
import { World } from "../../util/stateManager";
import actions from "../../actions";
@@ -69,9 +70,7 @@ class Embedding extends React.PureComponent {
return (
<ButtonGroup
style={{
marginRight: 10,
}}
className={styles.menubarButton}
>
<Popover
target={
+96 -105
View File
@@ -2,7 +2,9 @@
import React from "react";
import { connect } from "react-redux";
import { Button, ButtonGroup, AnchorButton, Tooltip } from "@blueprintjs/core";
import * as globals from "../../globals";
import styles from "./menubar.css";
import actions from "../../actions";
import Clip from "./clip";
import Embedding from "./embedding";
@@ -33,8 +35,8 @@ import DiffexpButtons from "./diffexpButtons";
disableDiffexp: state.config?.parameters?.["disable-diffexp"] ?? false,
diffexpMayBeSlow: state.config?.parameters?.["diffexp-may-be-slow"] ?? false,
showCentroidLabels: state.centroidLabels.showLabels,
tosURL: state.config?.parameters?.about_legal_tos,
privacyURL: state.config?.parameters?.about_legal_privacy,
tosURL: state.config?.parameters?.["about_legal_tos"],
privacyURL: state.config?.parameters?.["about_legal_privacy"],
}))
class MenuBar extends React.Component {
static isValidDigitKeyEvent(e) {
@@ -205,100 +207,35 @@ class MenuBar extends React.Component {
const { pendingClipPercentiles } = this.state;
// constants used to create selection tool button
let selectionTooltip;
let selectionButtonIcon;
if (selectionTool === "brush") {
selectionTooltip = "Brush selection";
selectionButtonIcon = "select";
} else {
selectionTooltip = "Lasso selection";
selectionButtonIcon = "polygon-filter";
}
const [selectionTooltip, selectionButtonIcon] = selectionTool === "brush"
? ["Brush selection", "Lasso selection"]
: ["select", "polygon-filter"];
return (
<div
style={{
position: "fixed",
right: globals.leftSidebarWidth + 8,
top: 8,
position: "absolute",
right: 8,
top: 0,
display: "flex",
flexDirection: "row-reverse",
alignItems: "flex-start",
flexWrap: "wrap",
justifyContent: "flex-start",
zIndex: 3,
}}
>
{disableDiffexp ? null : <DiffexpButtons />}
<Subset
subsetPossible={this.subsetPossible()}
subsetResetPossible={this.subsetResetPossible()}
handleSubset={() => {
dispatch(actions.setWorldToSelection());
dispatch({ type: "increment graph render counter" });
}}
handleSubsetReset={() => {
dispatch(actions.resetWorldToUniverse());
dispatch({ type: "increment graph render counter" });
}}
<InformationMenu
libraryVersions={libraryVersions}
aboutLink={aboutLink}
tosURL={tosURL}
privacyURL={privacyURL}
/>
<UndoRedoReset
dispatch={dispatch}
undoDisabled={undoDisabled}
redoDisabled={redoDisabled}
/>
<ButtonGroup style={{ marginRight: "10px" }}>
<Tooltip
content={selectionTooltip}
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
type="button"
data-testid="mode-lasso"
icon={selectionButtonIcon}
active={graphInteractionMode === "select"}
onClick={() => {
dispatch({
type: "change graph interaction mode",
data: "select",
});
}}
style={{
cursor: "pointer",
}}
/>
</Tooltip>
<Tooltip
content="Drag to pan, scroll to zoom"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
type="button"
data-testid="mode-pan-zoom"
icon="zoom-in"
active={graphInteractionMode === "zoom"}
onClick={() => {
dispatch({
type: "change graph interaction mode",
data: "zoom",
});
}}
style={{
cursor: "pointer",
}}
/>
</Tooltip>
</ButtonGroup>
<Tooltip
content="When a category is colored by, show labels on the graph"
position="bottom"
disabled={graphInteractionMode === "zoom"}
>
<Button
type="button"
data-testid="centroid-label-toggle"
icon="property"
onClick={this.handleCentroidChange}
active={showCentroidLabels}
intent={showCentroidLabels ? "primary" : "none"}
style={{
marginRight: 10,
}}
/>
</Tooltip>
<Embedding />
<Clip
pendingClipPercentiles={pendingClipPercentiles}
clipPercentileMin={clipPercentileMin}
@@ -308,26 +245,80 @@ class MenuBar extends React.Component {
handleClipCommit={this.handleClipCommit}
isClipDisabled={this.isClipDisabled}
handleClipOnKeyPress={this.handleClipOnKeyPress}
handleClipPercentileMaxValueChange={
this.handleClipPercentileMaxValueChange
}
handleClipPercentileMinValueChange={
this.handleClipPercentileMinValueChange
}
handleClipPercentileMaxValueChange={this.handleClipPercentileMaxValueChange}
handleClipPercentileMinValueChange={this.handleClipPercentileMinValueChange}
/>
<UndoRedoReset
dispatch={dispatch}
undoDisabled={undoDisabled}
redoDisabled={redoDisabled}
<Embedding />
<Tooltip
content="When a category is colored by, show labels on the graph"
position="bottom"
disabled={graphInteractionMode === "zoom"}
>
<AnchorButton
className={styles.menubarButton}
type="button"
data-testid="centroid-label-toggle"
icon="property"
onClick={this.handleCentroidChange}
active={showCentroidLabels}
intent={showCentroidLabels ? "primary" : "none"}
/>
</Tooltip>
<ButtonGroup
className={styles.menubarButton}
>
<Tooltip
content={selectionTooltip}
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
type="button"
data-testid="mode-lasso"
icon={selectionButtonIcon}
active={graphInteractionMode === "select"}
onClick={() => {
dispatch({
type: "change graph interaction mode",
data: "select"
});
}}
/>
<InformationMenu
libraryVersions={libraryVersions}
aboutLink={aboutLink}
tosURL={tosURL}
privacyURL={privacyURL}
</Tooltip>
<Tooltip
content="Drag to pan, scroll to zoom"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
type="button"
data-testid="mode-pan-zoom"
icon="zoom-in"
active={graphInteractionMode === "zoom"}
onClick={() => {
dispatch({
type: "change graph interaction mode",
data: "zoom"
});
}}
/>
</div>
);
</Tooltip>
</ButtonGroup>
<Subset
subsetPossible={this.subsetPossible()}
subsetResetPossible={this.subsetResetPossible()}
handleSubset={() => {
dispatch(actions.setWorldToSelection());
dispatch({ type: "increment graph render counter" });
}}
handleSubsetReset={() => {
dispatch(actions.resetWorldToUniverse());
dispatch({ type: "increment graph render counter" });
}}
/>
{disableDiffexp ? null : <DiffexpButtons/>}
</div>
);
}
}
+4 -1
View File
@@ -1,11 +1,14 @@
// jshint esversion: 6
import React from "react";
import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core";
import styles from "./menubar.css";
function InformationMenu(props) {
const { libraryVersions, aboutLink, tosURL, privacyURL } = props;
return (
<div style={{}} className="bp3-button-group">
<div
className={`bp3-button-group ${styles.menubarButton}`}
>
<Popover
content={
<Menu>
@@ -0,0 +1,4 @@
:local(.menubarButton) {
margin-top: 8px;
margin-left: 8px;
}
+7 -2
View File
@@ -1,5 +1,6 @@
import React from "react";
import { AnchorButton, ButtonGroup, Tooltip } from "@blueprintjs/core";
import styles from "./menubar.css";
import * as globals from "../../globals";
function Subset(props) {
@@ -11,18 +12,21 @@ function Subset(props) {
} = props;
return (
<ButtonGroup style={{ marginRight: "10px" }}>
<ButtonGroup
className={styles.menubarButton}
>
<Tooltip
content="Subset to currently selected cells and associated metadata"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
type="button"
data-testid="subset-button"
disabled={!subsetPossible}
icon="pie-chart"
onClick={handleSubset}
/>
/>
</Tooltip>
<Tooltip
content="Undo subset and show all cells and associated metadata"
@@ -30,6 +34,7 @@ function Subset(props) {
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
type="button"
data-testid="reset-subset-button"
disabled={!subsetResetPossible}
icon="full-circle"
+4 -1
View File
@@ -2,11 +2,14 @@
import React from "react";
import { AnchorButton, Tooltip } from "@blueprintjs/core";
import { tooltipHoverOpenDelay } from "../../globals";
import styles from "./menubar.css";
function InformationMenu(props) {
const { undoDisabled, redoDisabled, dispatch } = props;
return (
<div style={{ marginRight: 10 }} className="bp3-button-group">
<div
className={`bp3-button-group ${styles.menubarButton}`}
>
<Tooltip
content="Undo"
position="bottom"
+10 -18
View File
@@ -5,36 +5,28 @@ import Continuous from "../continuous/continuous";
import GeneExpression from "../geneExpression";
import * as globals from "../../globals";
@connect((state) => ({
responsive: state.responsive,
@connect(state => ({
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
}))
class RightSidebar extends React.Component {
render() {
const { responsive } = this.props;
render() {
return (
<div
style={{
position: "fixed",
right: 0,
backgroundColor: "white",
/* x y blur spread color */
borderLeft: `1px solid ${globals.lightGrey}`,
display: "flex",
flexDirection: "column",
position: "relative",
overflowY: "inherit",
height: "inherit",
width: "inherit"
}}
>
<div
style={{
height: responsive.height,
width: globals.leftSidebarWidth,
overflowY: "auto",
overflowX: "hidden",
}}
>
<GeneExpression />
<Continuous />
</div>
<GeneExpression />
<Continuous />
</div>
);
}
@@ -53,8 +53,6 @@ function createProjectionTF(viewportWidth, viewportHeight) {
expressionY,
crossfilter,
responsive: state.responsive,
};
})
class Scatterplot extends React.PureComponent {
@@ -140,6 +138,10 @@ class Scatterplot extends React.PureComponent {
this.state = {
svg: null,
minimized: null,
viewport: {
height: null,
width: null,
}
};
}
@@ -177,6 +179,9 @@ class Scatterplot extends React.PureComponent {
projectionTF
);
window.addEventListener("resize", this.handleResize);
const viewport = this.getViewportDimensions();
this.setState({
regl,
flagBuffer,
@@ -185,6 +190,7 @@ class Scatterplot extends React.PureComponent {
svg,
drawPoints,
projectionTF,
viewport
});
}
@@ -272,6 +278,28 @@ class Scatterplot extends React.PureComponent {
}
}
componentWillUnmount() {
window.removeEventListener("resize", this.updateViewportDimensions);
}
getViewportDimensions = () => {
return {
viewport: {
height: window.height,
width: window.width
}
};
};
handleResize = () => {
const { state } = this.state;
const viewport = this.getViewportDimensions();
this.setState({
...state,
viewport,
});
};
static setupScales(expressionX, expressionY) {
const xScale = d3
.scaleLinear()
@@ -288,6 +316,10 @@ class Scatterplot extends React.PureComponent {
};
}
updateViewportDimensions = () => {
this.setState(this.getViewportDimensions());
};
drawAxesSVG(xScale, yScale, svg) {
const { scatterplotYYaccessor, scatterplotXXaccessor } = this.props;
svg.selectAll("*").remove();
@@ -341,12 +373,8 @@ class Scatterplot extends React.PureComponent {
projectionTF
) {
if (!this.reglCanvas) return;
const { universe, responsive } = this.props;
// The viewport dimension is used to scale points, so we want to pass
// the dimension of the MAIN viewport, not the scatterplot viewport.
// Slightly hacky, but we want all points to scale uniformly. Perhaps
// this should move to the redux state and be shared?
const { width: cvWidth, height: cvHeight } = responsive;
const { universe } = this.props;
const { viewport } = this.state;
regl.poll();
regl.clear({
depth: 1,
@@ -360,9 +388,9 @@ class Scatterplot extends React.PureComponent {
count: this.count,
nPoints: universe.nObs,
minViewportDimension: Math.min(
cvWidth - globals.leftSidebarWidth || width,
cvHeight || height
),
viewport.width - globals.leftSidebarWidth || width,
viewport.height || height
)
});
regl._gl.flush();
}
@@ -379,9 +407,10 @@ class Scatterplot extends React.PureComponent {
borderRadius: "3px 3px 0px 0px",
left: globals.leftSidebarWidth + globals.scatterplotMarginLeft,
padding: "0px 20px 20px 0px",
backgroundColor: "white",
background: "white",
/* x y blur spread color */
boxShadow: "0px 0px 6px 2px rgba(153,153,153,0.4)",
zIndex: 2,
}}
id="scatterplot_wrapper"
>
+2 -3
View File
@@ -9,6 +9,7 @@ import {
Icon,
} from "@blueprintjs/core";
import * as globals from "../../globals";
import { termsOfServiceToast } from "../framework/toasters";
const CookieDecision = "cxg.cookieDecision";
@@ -25,9 +26,7 @@ function storageGet(key, defaultValue = null) {
function storageSet(key, value) {
try {
window.localStorage.setItem(key, value);
} catch {
return;
}
} catch {}
}
@connect((state) => ({
+1
View File
@@ -63,6 +63,7 @@ export const graphWidth = 700;
export const graphHeight = 700;
export const scatterplotMarginLeft = 25;
export const rightSidebarWidth = 365;
export const leftSidebarWidth = 365;
export const leftSidebarSectionHeading = {
fontSize: 18,
-2
View File
@@ -13,7 +13,6 @@ import crossfilter from "./crossfilter";
import colors from "./colors";
import differential from "./differential";
import layoutChoice from "./layoutChoice";
import responsive from "./responsive";
import controls from "./controls";
import resetCache from "./resetCache";
import annotations from "./annotations";
@@ -41,7 +40,6 @@ const Reducer = undoable(
["controls", controls],
["crossfilter", crossfilter],
["differential", differential],
["responsive", responsive],
["centroidLabels", centroidLabels],
["pointDilation", pointDialation],
["reembedController", reembedController],
-21
View File
@@ -1,21 +0,0 @@
// jshint esversion: 6
const Responsive = (
state = {
width: null,
height: null,
},
action
) => {
switch (action.type) {
case "window resize":
return {
...state,
width: action.data.width,
height: action.data.height,
};
default:
return state;
}
};
export default Responsive;