mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-25 03:28:12 +08:00
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:
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user