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