mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-28 12:08:12 +08:00
merge main
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
Action creators for user annotation
|
||||
*/
|
||||
import _ from "lodash";
|
||||
import pako from "pako";
|
||||
import * as globals from "../globals";
|
||||
import { MatrixFBS, AnnotationsHelpers } from "../util/stateManager";
|
||||
|
||||
@@ -153,7 +154,7 @@ export const annotationCreateLabelInCategory = (
|
||||
assignSelected
|
||||
) => async (dispatch, getState) => {
|
||||
/*
|
||||
Add a new label to a user-defined category. If assignSelected is true, assign
|
||||
Add a new label to a user-defined category. If assignSelected is true, assign
|
||||
the label to all currently selected cells.
|
||||
*/
|
||||
const {
|
||||
@@ -347,6 +348,7 @@ export const saveObsAnnotationsAction = () => async (dispatch, getState) => {
|
||||
|
||||
const df = await annoMatrix.fetch("obs", writableAnnotations(annoMatrix));
|
||||
const matrix = MatrixFBS.encodeMatrixFBS(df);
|
||||
const compressedMatrix = pako.deflate(matrix);
|
||||
try {
|
||||
const queryString =
|
||||
!dataCollectionNameIsReadOnly && !!dataCollectionName
|
||||
@@ -358,7 +360,7 @@ export const saveObsAnnotationsAction = () => async (dispatch, getState) => {
|
||||
`${globals.API.prefix}${globals.API.version}annotations/obs${queryString}`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: matrix,
|
||||
body: compressedMatrix,
|
||||
headers: new Headers({
|
||||
"Content-Type": "application/octet-stream",
|
||||
}),
|
||||
|
||||
@@ -5,20 +5,23 @@ action creators related to embeddings choice
|
||||
import { AnnoMatrixObsCrossfilter } from "../annoMatrix";
|
||||
import { _setEmbeddingSubset } from "../util/stateManager/viewStackHelpers";
|
||||
|
||||
export async function _switchEmbedding(prevAnnoMatrix, prevCrossfilter, newEmbeddingName) {
|
||||
export async function _switchEmbedding(
|
||||
prevAnnoMatrix,
|
||||
prevCrossfilter,
|
||||
newEmbeddingName
|
||||
) {
|
||||
/*
|
||||
DRY helper used by this and reembedding action creators
|
||||
*/
|
||||
const base = prevAnnoMatrix.base();
|
||||
const embeddingDf = await base.fetch("emb", newEmbeddingName);
|
||||
const annoMatrix = _setEmbeddingSubset(prevAnnoMatrix, embeddingDf);
|
||||
const obsCrossfilter = await new AnnoMatrixObsCrossfilter(annoMatrix, prevCrossfilter.obsCrossfilter).select(
|
||||
"emb",
|
||||
newEmbeddingName,
|
||||
{
|
||||
mode: "all",
|
||||
}
|
||||
);
|
||||
const obsCrossfilter = await new AnnoMatrixObsCrossfilter(
|
||||
annoMatrix,
|
||||
prevCrossfilter.obsCrossfilter
|
||||
).select("emb", newEmbeddingName, {
|
||||
mode: "all",
|
||||
});
|
||||
return [annoMatrix, obsCrossfilter];
|
||||
}
|
||||
|
||||
@@ -30,7 +33,10 @@ export const layoutChoiceAction = (newLayoutChoice) => async (
|
||||
On layout choice, make sure we have selected all on the previous layout, AND the new
|
||||
layout.
|
||||
*/
|
||||
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevCrossfilter } = getState();
|
||||
const {
|
||||
annoMatrix: prevAnnoMatrix,
|
||||
obsCrossfilter: prevCrossfilter,
|
||||
} = getState();
|
||||
const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
|
||||
prevAnnoMatrix,
|
||||
prevCrossfilter,
|
||||
|
||||
@@ -43,12 +43,12 @@ async function configFetch(dispatch) {
|
||||
|
||||
async function userInfoFetch(dispatch) {
|
||||
return fetchJson("userinfo").then((response) => {
|
||||
const { userinfo } = response || {};
|
||||
const { userinfo: userInfo } = response || {};
|
||||
dispatch({
|
||||
type: "userinfo load complete",
|
||||
userinfo,
|
||||
type: "userInfo load complete",
|
||||
userInfo,
|
||||
});
|
||||
return userinfo;
|
||||
return userInfo;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -79,10 +79,14 @@ export function requestReembed() {
|
||||
type: "reembed: request completed",
|
||||
});
|
||||
|
||||
const { annoMatrix: prevAnnoMatrix } = getState();
|
||||
const {
|
||||
annoMatrix: prevAnnoMatrix,
|
||||
obsCrossfilter: prevCrossfilter,
|
||||
} = getState();
|
||||
const base = prevAnnoMatrix.base().addEmbedding(schema);
|
||||
const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
|
||||
base,
|
||||
prevCrossfilter,
|
||||
schema.name
|
||||
);
|
||||
dispatch({
|
||||
|
||||
@@ -3,18 +3,19 @@ import { connect } from "react-redux";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
InputGroup,
|
||||
Dialog,
|
||||
Classes,
|
||||
Code,
|
||||
Colors,
|
||||
Dialog,
|
||||
InputGroup,
|
||||
Tooltip,
|
||||
} from "@blueprintjs/core";
|
||||
|
||||
@connect((state) => ({
|
||||
idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null,
|
||||
annotations: state.annotations,
|
||||
auth: state.config?.authentication,
|
||||
userinfo: state.userinfo,
|
||||
userInfo: state.userInfo,
|
||||
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||
}))
|
||||
class FilenameDialog extends React.Component {
|
||||
@@ -96,7 +97,7 @@ class FilenameDialog extends React.Component {
|
||||
writableCategoriesEnabled,
|
||||
annotations,
|
||||
idhash,
|
||||
userinfo,
|
||||
userInfo,
|
||||
} = this.props;
|
||||
const { filenameText } = this.state;
|
||||
|
||||
@@ -104,7 +105,7 @@ class FilenameDialog extends React.Component {
|
||||
annotations.promptForFilename &&
|
||||
!annotations.dataCollectionNameIsReadOnly &&
|
||||
!annotations.dataCollectionName &&
|
||||
userinfo.is_authenticated ? (
|
||||
userInfo.is_authenticated ? (
|
||||
<Dialog
|
||||
icon="tag"
|
||||
title="Annotations Collection"
|
||||
@@ -145,9 +146,9 @@ class FilenameDialog extends React.Component {
|
||||
<div>
|
||||
<p>
|
||||
Your annotations are stored in this file:
|
||||
<code className="bp3-code">
|
||||
<Code>
|
||||
{filenameText}-{idhash}.csv
|
||||
</code>
|
||||
</Code>
|
||||
</p>
|
||||
<p style={{ fontStyle: "italic" }}>
|
||||
(We added a unique ID to your filename)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import React, { useRef, useEffect } from "react";
|
||||
import { connect, shallowEqual } from "react-redux";
|
||||
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
|
||||
import { AnchorButton, Button, Tooltip, Position } from "@blueprintjs/core";
|
||||
import {
|
||||
AnchorButton,
|
||||
Button,
|
||||
Classes,
|
||||
Position,
|
||||
Tooltip,
|
||||
} from "@blueprintjs/core";
|
||||
import { Flipper, Flipped } from "react-flip-toolkit";
|
||||
import Async from "react-async";
|
||||
import memoize from "memoize-one";
|
||||
@@ -301,9 +307,12 @@ const StillLoading = ({ metadataField, checkboxID }) => {
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<label htmlFor={checkboxID} className="bp3-control bp3-checkbox">
|
||||
<label
|
||||
htmlFor={checkboxID}
|
||||
className={`${Classes.CONTROL} ${Classes.CHECKBOX}`}
|
||||
>
|
||||
<input disabled id={checkboxID} checked type="checkbox" />
|
||||
<span className="bp3-control-indicator" />
|
||||
<span className={Classes.CONTROL_INDICATOR} />
|
||||
</label>
|
||||
<Truncate>
|
||||
<span
|
||||
@@ -375,7 +384,10 @@ const CategoryHeader = React.memo(
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<label className="bp3-control bp3-checkbox" htmlFor={checkboxID}>
|
||||
<label
|
||||
className={`${Classes.CONTROL} ${Classes.CHECKBOX}`}
|
||||
htmlFor={checkboxID}
|
||||
>
|
||||
<input
|
||||
id={checkboxID}
|
||||
data-testclass="category-select"
|
||||
@@ -385,7 +397,7 @@ const CategoryHeader = React.memo(
|
||||
checked={selectionState === "all"}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span className="bp3-control-indicator" />
|
||||
<span className={Classes.CONTROL_INDICATOR} />
|
||||
</label>
|
||||
<span
|
||||
role="menuitem"
|
||||
|
||||
@@ -15,7 +15,7 @@ import actions from "../../actions";
|
||||
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
|
||||
schema: state.annoMatrix?.schema,
|
||||
ontology: state.ontology,
|
||||
userinfo: state.userinfo,
|
||||
userInfo: state.userInfo,
|
||||
}))
|
||||
class Categories extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -132,7 +132,7 @@ class Categories extends React.Component {
|
||||
writableCategoriesEnabled,
|
||||
schema,
|
||||
ontology,
|
||||
userinfo,
|
||||
userInfo,
|
||||
} = this.props;
|
||||
const ontologyEnabled = ontology?.enabled ?? false;
|
||||
/* all names, sorted in display order. Will be rendered in this order */
|
||||
@@ -186,7 +186,7 @@ class Categories extends React.Component {
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<Tooltip
|
||||
content={
|
||||
userinfo.is_authenticated
|
||||
userInfo.is_authenticated
|
||||
? "Create a new category"
|
||||
: "You must be logged in to create new categorical fields"
|
||||
}
|
||||
@@ -203,7 +203,7 @@ class Categories extends React.Component {
|
||||
data-testid="open-annotation-dialog"
|
||||
onClick={this.handleEnableAnnoMode}
|
||||
intent="primary"
|
||||
disabled={!userinfo.is_authenticated}
|
||||
disabled={!userInfo.is_authenticated}
|
||||
>
|
||||
Create new <strong>category</strong>
|
||||
</AnchorButton>
|
||||
@@ -214,7 +214,9 @@ class Categories extends React.Component {
|
||||
{/* READ ONLY CATEGORICAL FIELDS */}
|
||||
{/* this is duplicative but flat, could be abstracted */}
|
||||
{allCategoryNames.map((catName) =>
|
||||
!schema.annotations.obsByName[catName].writable ? (
|
||||
!schema.annotations.obsByName[catName].writable &&
|
||||
(schema.annotations.obsByName[catName].categories?.length > 1 ||
|
||||
!schema.annotations.obsByName[catName].categories) ? (
|
||||
<Category
|
||||
key={catName}
|
||||
metadataField={catName}
|
||||
|
||||
@@ -4,12 +4,13 @@ import * as d3 from "d3";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Classes,
|
||||
Icon,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Popover,
|
||||
Position,
|
||||
Icon,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
} from "@blueprintjs/core";
|
||||
import * as globals from "../../../globals";
|
||||
import styles from "../categorical.css";
|
||||
@@ -410,7 +411,6 @@ class CategoryValue extends React.Component {
|
||||
|
||||
return (
|
||||
<MiniStackedBar
|
||||
/* eslint-disable react/jsx-props-no-spreading -- Disable unneeded on next release of eslint-config-airbnb */
|
||||
{...{
|
||||
colorTable,
|
||||
domainValues,
|
||||
@@ -418,7 +418,6 @@ class CategoryValue extends React.Component {
|
||||
domain,
|
||||
occupancy,
|
||||
}}
|
||||
/* eslint-enable react/jsx-props-no-spreading -- enable */
|
||||
height={VALUE_HEIGHT}
|
||||
width={CHART_WIDTH}
|
||||
/>
|
||||
@@ -461,14 +460,12 @@ class CategoryValue extends React.Component {
|
||||
|
||||
return (
|
||||
<MiniHistogram
|
||||
/* eslint-disable react/jsx-props-no-spreading -- Disable unneeded on next release of eslint-config-airbnb */
|
||||
{...{
|
||||
colorScale,
|
||||
xScale,
|
||||
yScale,
|
||||
bins,
|
||||
}}
|
||||
/* eslint-enable react/jsx-props-no-spreading -- enable */
|
||||
obsOrVarContinuousFieldDisplayName={colorAccessor}
|
||||
domainLabel={label}
|
||||
height={VALUE_HEIGHT}
|
||||
@@ -564,7 +561,7 @@ class CategoryValue extends React.Component {
|
||||
<div style={{ display: "flex", alignItems: "baseline" }}>
|
||||
<label
|
||||
htmlFor={valueToggleLabel}
|
||||
className="bp3-control bp3-checkbox"
|
||||
className={`${Classes.CONTROL} ${Classes.CHECKBOX}`}
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
<input
|
||||
@@ -576,7 +573,7 @@ class CategoryValue extends React.Component {
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
className="bp3-control-indicator"
|
||||
className={Classes.CONTROL_INDICATOR}
|
||||
onMouseEnter={this.handleMouseExit}
|
||||
onMouseLeave={this.handleMouseEnter}
|
||||
/>
|
||||
|
||||
@@ -3,10 +3,10 @@ import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import * as d3 from "d3";
|
||||
import {
|
||||
Classes,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
Classes,
|
||||
} from "@blueprintjs/core";
|
||||
|
||||
@connect((state) => ({
|
||||
@@ -18,8 +18,8 @@ class Occupancy extends React.PureComponent {
|
||||
_HEIGHT = 11;
|
||||
|
||||
createHistogram = () => {
|
||||
/*
|
||||
Knowing that colorScale is based off continous data,
|
||||
/*
|
||||
Knowing that colorScale is based off continous data,
|
||||
createHistogram fetches the continous data in relation to the cells releveant to the catagory value.
|
||||
It then seperates that data into 50 bins for drawing the mini-histogram
|
||||
*/
|
||||
@@ -75,8 +75,8 @@ class Occupancy extends React.PureComponent {
|
||||
};
|
||||
|
||||
createOccupancyStack = () => {
|
||||
/*
|
||||
Knowing that the color scale is based off of catagorical data,
|
||||
/*
|
||||
Knowing that the color scale is based off of catagorical data,
|
||||
createOccupancyStack obtains a map showing the number if cells per colored value
|
||||
Using the colorScale a stack of colored bars is drawn representing the map
|
||||
*/
|
||||
@@ -155,7 +155,7 @@ class Occupancy extends React.PureComponent {
|
||||
popoverClassName={Classes.POPOVER_CONTENT_SIZING}
|
||||
>
|
||||
<canvas
|
||||
className="bp3-popover-targer"
|
||||
className={Classes.POPOVER_TARGET}
|
||||
style={{
|
||||
marginRight: 5,
|
||||
width: this._WIDTH,
|
||||
|
||||
@@ -11,20 +11,20 @@ import {
|
||||
|
||||
// create continuous color legend
|
||||
// http://bl.ocks.org/syntagmatic/e8ccca52559796be775553b467593a9f
|
||||
const continuous = (selectorId, colorscale, colorAccessor) => {
|
||||
const legendheight = 200;
|
||||
const legendwidth = 80;
|
||||
const continuous = (selectorId, colorScale, colorAccessor) => {
|
||||
const legendHeight = 200;
|
||||
const legendWidth = 80;
|
||||
const margin = { top: 10, right: 60, bottom: 10, left: 2 };
|
||||
|
||||
const canvas = d3
|
||||
.select(selectorId)
|
||||
.style("height", `${legendheight}px`)
|
||||
.style("width", `${legendwidth}px`)
|
||||
.style("height", `${legendHeight}px`)
|
||||
.style("width", `${legendWidth}px`)
|
||||
.append("canvas")
|
||||
.attr("height", legendheight - margin.top - margin.bottom)
|
||||
.attr("height", legendHeight - margin.top - margin.bottom)
|
||||
.attr("width", 1)
|
||||
.style("height", `${legendheight - margin.top - margin.bottom}px`)
|
||||
.style("width", `${legendwidth - margin.left - margin.right}px`)
|
||||
.style("height", `${legendHeight - margin.top - margin.bottom}px`)
|
||||
.style("width", `${legendWidth - margin.left - margin.right}px`)
|
||||
.style("position", "absolute")
|
||||
.style("top", `${margin.top + 1}px`)
|
||||
.style("left", `${margin.left + 1}px`)
|
||||
@@ -37,18 +37,18 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const legendscale = d3
|
||||
const legendScale = d3
|
||||
.scaleLinear()
|
||||
.range([1, legendheight - margin.top - margin.bottom])
|
||||
.range([1, legendHeight - margin.top - margin.bottom])
|
||||
.domain([
|
||||
colorscale.domain()[1],
|
||||
colorscale.domain()[0],
|
||||
colorScale.domain()[1],
|
||||
colorScale.domain()[0],
|
||||
]); /* we flip this to make viridis colors dark if high in the color scale */
|
||||
|
||||
// image data hackery based on http://bl.ocks.org/mbostock/048d21cf747371b11884f75ad896e5a5
|
||||
const image = ctx.createImageData(1, legendheight);
|
||||
d3.range(legendheight).forEach((i) => {
|
||||
const c = d3.rgb(colorscale(legendscale.invert(i)));
|
||||
const image = ctx.createImageData(1, legendHeight);
|
||||
d3.range(legendHeight).forEach((i) => {
|
||||
const c = d3.rgb(colorScale(legendScale.invert(i)));
|
||||
image.data[4 * i] = c.r;
|
||||
image.data[4 * i + 1] = c.g;
|
||||
image.data[4 * i + 2] = c.b;
|
||||
@@ -66,20 +66,20 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
|
||||
});
|
||||
*/
|
||||
|
||||
const legendaxis = d3
|
||||
.axisRight(legendscale)
|
||||
const legendAxis = d3
|
||||
.axisRight(legendScale)
|
||||
.ticks(6)
|
||||
.tickFormat(
|
||||
d3.format(
|
||||
legendscale.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
|
||||
legendScale.domain().some((n) => Math.abs(n) >= 10000) ? ".0e" : ","
|
||||
)
|
||||
);
|
||||
|
||||
const svg = d3
|
||||
.select(selectorId)
|
||||
.append("svg")
|
||||
.attr("height", `${legendheight}px`)
|
||||
.attr("width", `${legendwidth}px`)
|
||||
.attr("height", `${legendHeight}px`)
|
||||
.attr("width", `${legendWidth}px`)
|
||||
.style("position", "absolute")
|
||||
.style("left", "0px")
|
||||
.style("top", "0px");
|
||||
@@ -89,16 +89,16 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
|
||||
.attr("class", "axis")
|
||||
.attr(
|
||||
"transform",
|
||||
`translate(${legendwidth - margin.left - margin.right + 3},${margin.top})`
|
||||
`translate(${legendWidth - margin.left - margin.right + 3},${margin.top})`
|
||||
)
|
||||
.call(legendaxis);
|
||||
.call(legendAxis);
|
||||
|
||||
// text label for the y axis
|
||||
svg
|
||||
.append("text")
|
||||
.attr("transform", "rotate(-90)")
|
||||
.attr("y", 2)
|
||||
.attr("x", 0 - legendheight / 2)
|
||||
.attr("x", 0 - legendHeight / 2)
|
||||
.attr("dy", "1em")
|
||||
.style("text-anchor", "middle")
|
||||
.style("fill", "white")
|
||||
@@ -110,24 +110,7 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
|
||||
colors: state.colors,
|
||||
}))
|
||||
class ContinuousLegend extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.ref = null;
|
||||
this.state = {
|
||||
colorAccessor: null,
|
||||
colorScale: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateState(null);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this.updateState(prevProps);
|
||||
}
|
||||
|
||||
async updateState(prevProps) {
|
||||
async componentDidUpdate(prevProps) {
|
||||
const { annoMatrix, colors } = this.props;
|
||||
if (!colors || !annoMatrix) return;
|
||||
|
||||
@@ -161,35 +144,19 @@ class ContinuousLegend extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
colorAccessor,
|
||||
colorScale: colorTable.scale,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { colorAccessor, colorScale } = this.state;
|
||||
if (
|
||||
colorScale?.domain &&
|
||||
colorScale.domain()[1] === colorScale.domain()[0]
|
||||
) {
|
||||
/* it's a single value, not a distribution, min max are the same */
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
id="continuous_legend"
|
||||
ref={(ref) => {
|
||||
this.ref = ref;
|
||||
}}
|
||||
style={{
|
||||
display: colorAccessor ? "inherit" : "none",
|
||||
position: "absolute",
|
||||
left: 8,
|
||||
top: 35,
|
||||
zIndex: 1,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,14 @@ import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { useAsync } from "react-async";
|
||||
import {
|
||||
ButtonGroup,
|
||||
Popover,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
H4,
|
||||
Popover,
|
||||
Position,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Tooltip,
|
||||
Position,
|
||||
} from "@blueprintjs/core";
|
||||
import * as globals from "../../globals";
|
||||
import actions from "../../actions";
|
||||
@@ -80,7 +81,7 @@ class Embedding extends React.PureComponent {
|
||||
width: 400,
|
||||
}}
|
||||
>
|
||||
<h1>Embedding Choice</h1>
|
||||
<H4>Embedding Choice</H4>
|
||||
<p style={{ fontStyle: "italic" }}>
|
||||
There are {schema?.dataframe?.nObs} cells in the entire dataset.
|
||||
</p>
|
||||
|
||||
@@ -6,11 +6,12 @@ import fuzzysort from "fuzzysort";
|
||||
import { connect } from "react-redux";
|
||||
import { Suggest } from "@blueprintjs/select";
|
||||
import {
|
||||
MenuItem,
|
||||
Button,
|
||||
ControlGroup,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
ControlGroup,
|
||||
Intent,
|
||||
MenuItem,
|
||||
} from "@blueprintjs/core";
|
||||
import * as globals from "../../../globals";
|
||||
import actions from "../../../actions";
|
||||
@@ -278,7 +279,7 @@ class AddGenes extends React.Component {
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
<Button
|
||||
className="bp3-button bp3-intent-primary"
|
||||
intent={Intent.PRIMARY}
|
||||
data-testid="add-gene"
|
||||
loading={userDefinedGenesLoading}
|
||||
onClick={() => this.handleClick(activeItem)}
|
||||
|
||||
@@ -730,14 +730,25 @@ class Graph extends React.Component {
|
||||
);
|
||||
});
|
||||
|
||||
updateReglAndRender(asyncProps) {
|
||||
updateReglAndRender(asyncProps, prevAsyncProps) {
|
||||
const { positions, colors, flags } = asyncProps;
|
||||
this.cachedAsyncProps = asyncProps;
|
||||
const { pointBuffer, colorBuffer, flagBuffer } = this.state;
|
||||
pointBuffer({ data: positions, dimension: 2 });
|
||||
colorBuffer({ data: colors, dimension: 3 });
|
||||
flagBuffer({ data: flags, dimension: 1 });
|
||||
this.renderCanvas();
|
||||
let needToRenderCanvas = false;
|
||||
|
||||
if (positions !== prevAsyncProps?.positions) {
|
||||
pointBuffer({ data: positions, dimension: 2 });
|
||||
needToRenderCanvas = true;
|
||||
}
|
||||
if (colors !== prevAsyncProps?.colors) {
|
||||
colorBuffer({ data: colors, dimension: 3 });
|
||||
needToRenderCanvas = true;
|
||||
}
|
||||
if (flags !== prevAsyncProps?.flags) {
|
||||
flagBuffer({ data: flags, dimension: 1 });
|
||||
needToRenderCanvas = true;
|
||||
}
|
||||
if (needToRenderCanvas) this.renderCanvas();
|
||||
}
|
||||
|
||||
updateColorTable(colors, colorDf) {
|
||||
@@ -906,7 +917,7 @@ class Graph extends React.Component {
|
||||
<Async.Fulfilled>
|
||||
{(asyncProps) => {
|
||||
if (regl && !shallowEqual(asyncProps, this.cachedAsyncProps)) {
|
||||
this.updateReglAndRender(asyncProps);
|
||||
this.updateReglAndRender(asyncProps, this.cachedAsyncProps);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
|
||||
@@ -10,6 +10,7 @@ const Lasso = () => {
|
||||
let lassoPolygon;
|
||||
let lassoPath;
|
||||
let closePath;
|
||||
let lassoInProgress;
|
||||
|
||||
const polygonToPath = (polygon) =>
|
||||
`M${polygon.map((d) => d.join(",")).join("L")}`;
|
||||
@@ -25,8 +26,18 @@ const Lasso = () => {
|
||||
lassoPolygon = [d3.mouse(svg.node())]; // current x y of mouse within element
|
||||
|
||||
if (lassoPath) {
|
||||
// If the existing path is in progress
|
||||
if (lassoInProgress) {
|
||||
// cancel the existing lasso
|
||||
handleCancel();
|
||||
// Don't continue with current drag start
|
||||
return;
|
||||
}
|
||||
|
||||
lassoPath.remove();
|
||||
}
|
||||
// We're starting a new drag
|
||||
lassoInProgress = true;
|
||||
|
||||
lassoPath = g
|
||||
.append("path")
|
||||
@@ -67,25 +78,33 @@ const Lasso = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
lassoPath.remove();
|
||||
closePath = closePath?.remove();
|
||||
lassoPath = null;
|
||||
lassoPolygon = null;
|
||||
closePath = null;
|
||||
dispatch.call("cancel");
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
// remove the close path
|
||||
closePath.remove();
|
||||
closePath = null;
|
||||
|
||||
// succesfully closed
|
||||
// successfully closed
|
||||
if (
|
||||
distance(lassoPolygon[0], lassoPolygon[lassoPolygon.length - 1]) <
|
||||
closeDistance
|
||||
) {
|
||||
lassoInProgress = false;
|
||||
|
||||
lassoPath.attr("d", `${polygonToPath(lassoPolygon)}Z`);
|
||||
dispatch.call("end", lasso, lassoPolygon);
|
||||
|
||||
// otherwise cancel
|
||||
} else {
|
||||
lassoPath.remove();
|
||||
lassoPath = null;
|
||||
lassoPolygon = null;
|
||||
dispatch.call("cancel");
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,61 +1,20 @@
|
||||
import React, { PureComponent } from "react";
|
||||
import { connect, shallowEqual } from "react-redux";
|
||||
import { connect } from "react-redux";
|
||||
import { Drawer } from "@blueprintjs/core";
|
||||
import Async from "react-async";
|
||||
|
||||
import InfoFormat from "./infoFormat";
|
||||
import {
|
||||
selectableCategoryNames,
|
||||
createCategorySummaryFromDfCol,
|
||||
} from "../../util/stateManager/controlsHelpers";
|
||||
import { selectableCategoryNames } from "../../util/stateManager/controlsHelpers";
|
||||
|
||||
@connect((state) => {
|
||||
return {
|
||||
annoMatrix: state.annoMatrix,
|
||||
schema: state.annoMatrix.schema,
|
||||
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
||||
aboutURL: state.config?.links?.["about-dataset"],
|
||||
isOpen: state.controls.datasetDrawer,
|
||||
dataPortalProps: state.config?.["corpora_props"] ?? {},
|
||||
dataPortalProps: state.config?.["corpora_props"],
|
||||
};
|
||||
})
|
||||
class InfoDrawer extends PureComponent {
|
||||
static watchAsync(props, prevProps) {
|
||||
return !shallowEqual(props.watchProps, prevProps.watchProps);
|
||||
}
|
||||
|
||||
fetchAsyncProps = async (props) => {
|
||||
const { schema } = props.watchProps;
|
||||
const { annoMatrix } = this.props;
|
||||
|
||||
const allCategoryNames = selectableCategoryNames(schema).sort();
|
||||
|
||||
const nonUserAnnoCategories = allCategoryNames.map((catName) => {
|
||||
const isUserAnno = schema?.annotations?.obsByName[catName]?.writable;
|
||||
if (!isUserAnno) return annoMatrix.fetch("obs", catName);
|
||||
return null;
|
||||
});
|
||||
const singleValueCategories = (
|
||||
await Promise.all(nonUserAnnoCategories)
|
||||
).reduce((acc, categoryData, i) => {
|
||||
const catName = allCategoryNames[i];
|
||||
|
||||
const column = categoryData.icol(0);
|
||||
const colSchema = schema.annotations.obsByName[catName];
|
||||
|
||||
const categorySummary = createCategorySummaryFromDfCol(column, colSchema);
|
||||
|
||||
const { numCategoryValues } = categorySummary;
|
||||
// Add to the array if the category has only one value
|
||||
if (numCategoryValues === 1) {
|
||||
acc.set(catName, categorySummary.allCategoryValues[0]);
|
||||
}
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
return { singleValueCategories };
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
@@ -72,45 +31,31 @@ class InfoDrawer extends PureComponent {
|
||||
dataPortalProps,
|
||||
} = this.props;
|
||||
|
||||
const allCategoryNames = selectableCategoryNames(schema).sort();
|
||||
const singleValueCategories = new Map();
|
||||
|
||||
allCategoryNames.forEach((catName) => {
|
||||
const isUserAnno = schema?.annotations?.obsByName[catName]?.writable;
|
||||
const colSchema = schema.annotations.obsByName[catName];
|
||||
if (!isUserAnno && colSchema.categories?.length === 1) {
|
||||
singleValueCategories.set(catName, colSchema.categories[0]);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title="Dataset Overview"
|
||||
onClose={this.handleClose}
|
||||
{...{ isOpen, position }}
|
||||
>
|
||||
<Async
|
||||
watchFn={InfoDrawer.watchAsync}
|
||||
promiseFn={this.fetchAsyncProps}
|
||||
watchProps={{ schema }}
|
||||
>
|
||||
<Async.Pending>
|
||||
<InfoFormat
|
||||
skeleton
|
||||
{...{ datasetTitle, aboutURL, dataPortalProps }}
|
||||
/>
|
||||
</Async.Pending>
|
||||
<Async.Rejected>
|
||||
{(error) => {
|
||||
console.error(error);
|
||||
return <span>Failed to load info</span>;
|
||||
}}
|
||||
</Async.Rejected>
|
||||
<Async.Fulfilled>
|
||||
{(asyncProps) => {
|
||||
const { singleValueCategories } = asyncProps;
|
||||
return (
|
||||
<InfoFormat
|
||||
{...{
|
||||
datasetTitle,
|
||||
aboutURL,
|
||||
singleValueCategories,
|
||||
dataPortalProps,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Async.Fulfilled>
|
||||
</Async>
|
||||
<InfoFormat
|
||||
{...{
|
||||
datasetTitle,
|
||||
aboutURL,
|
||||
singleValueCategories,
|
||||
dataPortalProps: dataPortalProps ?? {},
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { H3, H1, UL, Classes } from "@blueprintjs/core";
|
||||
import { H3, H1, UL, HTMLTable, Classes } from "@blueprintjs/core";
|
||||
import React from "react";
|
||||
|
||||
const renderContributors = (contributors, affiliations, skeleton) => {
|
||||
const renderContributors = (contributors, affiliations) => {
|
||||
// eslint-disable-next-line no-constant-condition -- Temp removed contributor section to avoid publishing PII
|
||||
if (!contributors || contributors.length === 0 || true) return null;
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>Contributors</H3>
|
||||
<p className={skeleton ? Classes.SKELETON : null}>
|
||||
<H3>Contributors</H3>
|
||||
<p>
|
||||
{contributors.map((contributor) => {
|
||||
const { email, name, institution } = contributor;
|
||||
|
||||
@@ -20,7 +20,7 @@ const renderContributors = (contributors, affiliations, skeleton) => {
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
{renderAffiliations(affiliations, skeleton)}
|
||||
{renderAffiliations(affiliations)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -37,14 +37,14 @@ const buildAffiliations = (contributors = []) => {
|
||||
return affiliations;
|
||||
};
|
||||
|
||||
const renderAffiliations = (affiliations, skeleton) => {
|
||||
const renderAffiliations = (affiliations) => {
|
||||
if (affiliations.length === 0) return null;
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>Affiliations</H3>
|
||||
<H3>Affiliations</H3>
|
||||
<UL>
|
||||
{affiliations.map((item, index) => (
|
||||
<div key={item} className={skeleton ? Classes.SKELETON : null}>
|
||||
<div key={item}>
|
||||
<sup>{index + 1}</sup>
|
||||
{" "}
|
||||
{item}
|
||||
@@ -55,12 +55,12 @@ const renderAffiliations = (affiliations, skeleton) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderDOILink = (type, doi, skeleton) => {
|
||||
const renderDOILink = (type, doi) => {
|
||||
if (!doi) return null;
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>{type}</H3>
|
||||
<p className={skeleton ? Classes.SKELETON : null}>
|
||||
<H3>{type}</H3>
|
||||
<p>
|
||||
<a href={doi} target="_blank" rel="noopener">
|
||||
{doi}
|
||||
</a>
|
||||
@@ -69,54 +69,80 @@ const renderDOILink = (type, doi, skeleton) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderOrganism = (organism, skeleton) => {
|
||||
if (!organism) return null;
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>Organism</H3>
|
||||
<p className={skeleton ? Classes.SKELETON : null}>{organism}</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ONTOLOGY_KEY = "ontology_term_id";
|
||||
// Render list of metadata attributes found in categorical field
|
||||
// Ignores categories with empty or null values
|
||||
const renderSingleValueCategories = (singleValueCategories, skeleton) => {
|
||||
const renderDatasetMetadata = (singleValueCategories, corporaMetadata) => {
|
||||
if (singleValueCategories.size === 0) return null;
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>Dataset Metadata</H3>
|
||||
<UL>
|
||||
{Array.from(singleValueCategories).map((pair) => {
|
||||
if (!pair[1] || pair[1] === "") return null;
|
||||
return (
|
||||
<li
|
||||
className={skeleton ? Classes.SKELETON : null}
|
||||
key={pair[0]}
|
||||
>{`${pair[0]}: ${pair[1]}`}</li>
|
||||
);
|
||||
})}
|
||||
</UL>
|
||||
<H3>Dataset Metadata</H3>
|
||||
<HTMLTable
|
||||
striped
|
||||
condensed
|
||||
style={{ display: "block", width: "100%", overflowX: "auto" }}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Label</th>
|
||||
<th>Ontology ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(corporaMetadata).map(([key, value]) => {
|
||||
return (
|
||||
<tr {...{ key }}>
|
||||
<td>{`${key}:`}</td>
|
||||
<td>{value}</td>
|
||||
<td />
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{Array.from(singleValueCategories).reduce((elems, pair) => {
|
||||
const [category, value] = pair;
|
||||
// If the value is empty skip it
|
||||
if (!value) return elems;
|
||||
|
||||
// If this category is a ontology term, let's add its value to the previous node
|
||||
if (String(category).includes(ONTOLOGY_KEY)) {
|
||||
const prevElem = elems.pop();
|
||||
const newChildren = [...prevElem.props.children];
|
||||
newChildren.splice(2, 1, [<td key="ontology">{value}</td>]);
|
||||
// Props aren't extensible so we must clone and alter the component to append the new child
|
||||
elems.push(
|
||||
React.cloneElement(prevElem, prevElem.props, newChildren)
|
||||
);
|
||||
} else {
|
||||
// Create the list item
|
||||
elems.push(
|
||||
<tr key={category}>
|
||||
<td>{`${category}:`}</td>
|
||||
<td>{value}</td>
|
||||
<td />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return elems;
|
||||
}, [])}
|
||||
</tbody>
|
||||
</HTMLTable>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Renders any links found in the config where link_type is not "SUMMARY"
|
||||
// If there are no links in the config, render the aboutURL
|
||||
const renderLinks = (projectLinks, aboutURL, skeleton) => {
|
||||
const renderLinks = (projectLinks, aboutURL) => {
|
||||
if (!projectLinks && !aboutURL) return null;
|
||||
if (projectLinks)
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>Project Links</H3>
|
||||
<H3>Project Links</H3>
|
||||
<UL>
|
||||
{projectLinks.map((link) => {
|
||||
if (link.link_type === "SUMMARY") return null;
|
||||
return (
|
||||
<li
|
||||
key={link.link_name}
|
||||
className={skeleton ? Classes.SKELETON : null}
|
||||
>
|
||||
<li key={link.link_name}>
|
||||
<a href={link.link_url} target="_blank" rel="noopener">
|
||||
{link.link_name}
|
||||
</a>
|
||||
@@ -129,14 +155,9 @@ const renderLinks = (projectLinks, aboutURL, skeleton) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<H3 className={skeleton ? Classes.SKELETON : null}>More Info</H3>
|
||||
<H3>More Info</H3>
|
||||
<p>
|
||||
<a
|
||||
className={skeleton ? Classes.SKELETON : null}
|
||||
href={aboutURL}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<a href={aboutURL} target="_blank" rel="noopener">
|
||||
{aboutURL}
|
||||
</a>
|
||||
</p>
|
||||
@@ -144,24 +165,9 @@ const renderLinks = (projectLinks, aboutURL, skeleton) => {
|
||||
);
|
||||
};
|
||||
|
||||
const NUM_CATEGORIES = 8;
|
||||
|
||||
// Generates arbitrary placeholder array for singleValueCategories skeleton shape
|
||||
const singleValueCategoriesPlaceholder = Array.from(Array(NUM_CATEGORIES)).map(
|
||||
(_, index) => {
|
||||
return [index, index];
|
||||
}
|
||||
);
|
||||
|
||||
const InfoFormat = React.memo(
|
||||
({
|
||||
datasetTitle,
|
||||
singleValueCategories = new Map(singleValueCategoriesPlaceholder),
|
||||
aboutURL = "thisisabouthtelengthofaurl",
|
||||
dataPortalProps = {},
|
||||
skeleton = false,
|
||||
}) => {
|
||||
if (dataPortalProps.corpora_schema_version === "1.0.0") {
|
||||
({ datasetTitle, singleValueCategories, aboutURL, dataPortalProps = {} }) => {
|
||||
if (dataPortalProps.version?.["corpora_schema_version"] !== "1.0.0") {
|
||||
dataPortalProps = {};
|
||||
}
|
||||
const {
|
||||
@@ -176,16 +182,15 @@ const InfoFormat = React.memo(
|
||||
const affiliations = buildAffiliations(contributors);
|
||||
|
||||
return (
|
||||
<div style={{ margin: 24, overflow: "auto" }}>
|
||||
<H1 className={skeleton ? Classes.SKELETON : null}>
|
||||
{title ?? datasetTitle}
|
||||
</H1>
|
||||
{renderContributors(contributors, affiliations, skeleton)}
|
||||
{renderDOILink("DOI", doi, skeleton)}
|
||||
{renderDOILink("Preprint DOI", preprintDOI, skeleton)}
|
||||
{renderOrganism(organism, skeleton)}
|
||||
{renderSingleValueCategories(singleValueCategories, skeleton)}
|
||||
{renderLinks(projectLinks, aboutURL, skeleton)}
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<H1>{title ?? datasetTitle}</H1>
|
||||
{renderContributors(contributors, affiliations)}
|
||||
{renderDatasetMetadata(singleValueCategories, { organism })}
|
||||
{renderLinks(projectLinks, aboutURL)}
|
||||
{renderDOILink("DOI", doi)}
|
||||
{renderDOILink("Preprint DOI", preprintDOI)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import { Button, Menu, MenuItem, Popover, Position } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
|
||||
const InformationMenu = React.memo((props) => {
|
||||
const { libraryVersions, tosURL, privacyURL } = props;
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
href="https://chanzuckerberg.github.io/cellxgene/"
|
||||
target="_blank"
|
||||
icon="book"
|
||||
text="Documentation"
|
||||
rel="noopener"
|
||||
/>
|
||||
<MenuItem
|
||||
href="https://join-cellxgene-users.herokuapp.com/"
|
||||
target="_blank"
|
||||
icon="chat"
|
||||
text="Chat"
|
||||
rel="noopener"
|
||||
/>
|
||||
<MenuItem
|
||||
href="https://github.com/chanzuckerberg/cellxgene"
|
||||
target="_blank"
|
||||
icon="git-branch"
|
||||
text="Github"
|
||||
rel="noopener"
|
||||
/>
|
||||
<MenuItem target="_blank" text={libraryVersions?.cellxgene || null} />
|
||||
<MenuItem text="MIT License" />
|
||||
{tosURL && (
|
||||
<MenuItem
|
||||
href={tosURL}
|
||||
target="_blank"
|
||||
text="Terms of Service"
|
||||
rel="noopener"
|
||||
/>
|
||||
)}
|
||||
{privacyURL && (
|
||||
<MenuItem
|
||||
href={privacyURL}
|
||||
target="_blank"
|
||||
text="Privacy Policy"
|
||||
rel="noopener"
|
||||
/>
|
||||
)}
|
||||
</Menu>
|
||||
}
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
modifiers={{
|
||||
preventOverflow: { enabled: false },
|
||||
hide: { enabled: false },
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
data-testid="menu"
|
||||
type="button"
|
||||
icon={IconNames.INFO_SIGN}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
export default InformationMenu;
|
||||
@@ -1,19 +1,28 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Button } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
import Logo from "../framework/logo";
|
||||
import Truncate from "../util/truncate";
|
||||
import InfoDrawer from "../infoDrawer/infoDrawer";
|
||||
import InformationMenu from "./infoMenu";
|
||||
|
||||
const DATASET_TITLE_FONT_SIZE = 14;
|
||||
|
||||
@connect((state) => ({
|
||||
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
||||
}))
|
||||
@connect((state) => {
|
||||
const { corpora_props: corporaProps } = state.config;
|
||||
const correctVersion =
|
||||
corporaProps?.version?.["corpora_schema_version"] === "1.0.0";
|
||||
return {
|
||||
datasetTitle: state.config?.displayNames?.dataset ?? "",
|
||||
libraryVersions: state.config?.["library_versions"],
|
||||
aboutLink: state.config?.links?.["about-dataset"],
|
||||
tosURL: state.config?.parameters?.["about_legal_tos"],
|
||||
privacyURL: state.config?.parameters?.["about_legal_privacy"],
|
||||
title: correctVersion ? corporaProps?.title : undefined,
|
||||
};
|
||||
})
|
||||
class LeftSideBar extends React.Component {
|
||||
handleClick = () => {
|
||||
const { dispatch } = this.props;
|
||||
@@ -21,7 +30,15 @@ class LeftSideBar extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { datasetTitle } = this.props;
|
||||
const {
|
||||
datasetTitle,
|
||||
libraryVersions,
|
||||
aboutLink,
|
||||
privacyURL,
|
||||
tosURL,
|
||||
dispatch,
|
||||
title,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -31,50 +48,65 @@ class LeftSideBar extends React.Component {
|
||||
width: globals.leftSidebarWidth,
|
||||
zIndex: 1,
|
||||
borderBottom: `1px solid ${globals.lighterGrey}`,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Logo size={30} />
|
||||
<span
|
||||
style={{
|
||||
fontSize: 28,
|
||||
position: "relative",
|
||||
top: -6,
|
||||
fontWeight: "bold",
|
||||
marginLeft: 5,
|
||||
color: globals.logoColor,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
cell
|
||||
<div>
|
||||
<Logo size={28} />
|
||||
<span
|
||||
style={{
|
||||
position: "relative",
|
||||
top: 1,
|
||||
fontWeight: 300,
|
||||
fontSize: 24,
|
||||
position: "relative",
|
||||
top: -6,
|
||||
fontWeight: "bold",
|
||||
marginLeft: 5,
|
||||
color: globals.logoColor,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
gene
|
||||
</span>
|
||||
<Button
|
||||
minimal
|
||||
icon={IconNames.BOOK}
|
||||
style={{
|
||||
fontSize: DATASET_TITLE_FONT_SIZE,
|
||||
position: "absolute",
|
||||
right: 10,
|
||||
}}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<Truncate>
|
||||
<span style={{ maxWidth: 155 }} data-testid="header">
|
||||
{datasetTitle}
|
||||
cell
|
||||
<span
|
||||
style={{
|
||||
position: "relative",
|
||||
top: 1,
|
||||
fontWeight: 300,
|
||||
fontSize: 24,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</Truncate>
|
||||
</Button>
|
||||
<InfoDrawer />
|
||||
gene
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ marginRight: 5, height: "100%" }}>
|
||||
<Button
|
||||
minimal
|
||||
style={{
|
||||
fontSize: DATASET_TITLE_FONT_SIZE,
|
||||
position: "relative",
|
||||
top: -1,
|
||||
}}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<Truncate>
|
||||
<span style={{ maxWidth: 155 }} data-testid="header">
|
||||
{title ?? datasetTitle}
|
||||
</span>
|
||||
</Truncate>
|
||||
</Button>
|
||||
<InfoDrawer />
|
||||
<InformationMenu
|
||||
{...{
|
||||
libraryVersions,
|
||||
aboutLink,
|
||||
tosURL,
|
||||
privacyURL,
|
||||
dispatch,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,175 @@
|
||||
import React from "react";
|
||||
import { AnchorButton, Tooltip } from "@blueprintjs/core";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import {
|
||||
AnchorButton,
|
||||
Button,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
Popover,
|
||||
Menu,
|
||||
Elevation,
|
||||
PopoverPosition,
|
||||
Checkbox,
|
||||
Card,
|
||||
} from "@blueprintjs/core";
|
||||
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
|
||||
import * as globals from "../../globals";
|
||||
|
||||
import styles from "./menubar.css";
|
||||
|
||||
import { storageGet, storageSet, KEYS } from "../util/localStorage";
|
||||
|
||||
const BASE_EMOJI = [0x1f9d1, 0x1f468, 0x1f469];
|
||||
const SKIN_TONES = [0x1f3fb, 0x1f3fc, 0x1f3fd, 0x1f3fe, 0x1f3ff];
|
||||
const MICROSCOPE = 0x1f52c;
|
||||
const ZERO_WIDTH_JOINER = 0x0200d;
|
||||
|
||||
const LOGIN_PROMPT_OFF = "off";
|
||||
|
||||
const Auth = React.memo((props) => {
|
||||
const { auth, userinfo } = props;
|
||||
const [isPromptOpen, setIsPromptOpen] = useState(shouldShowPrompt());
|
||||
|
||||
if (!auth || (auth && !auth.requires_client_login)) return null;
|
||||
const { auth, userInfo } = props;
|
||||
|
||||
return (
|
||||
<div className={`bp3-button-group ${styles.menubarButton}`}>
|
||||
<Tooltip
|
||||
content="Log in or log out of cellxgene"
|
||||
position="bottom"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
>
|
||||
<AnchorButton
|
||||
type="button"
|
||||
data-testid="auth-button"
|
||||
disabled={false}
|
||||
icon={!userinfo.is_authenticated ? "log-in" : "log-out"}
|
||||
href={!userinfo.is_authenticated ? auth.login : auth.logout}
|
||||
>
|
||||
{!userinfo.is_authenticated ? "Log In" : "Log Out"}
|
||||
</AnchorButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
const isAuthenticated = userInfo && userInfo.is_authenticated;
|
||||
|
||||
window.userInfo = userInfo;
|
||||
|
||||
const randomInt = Math.random() * 15;
|
||||
const sexIndex = Math.floor(randomInt / 5);
|
||||
const skinToneIndex = Math.floor(randomInt % 5);
|
||||
|
||||
const scientist = String.fromCodePoint(
|
||||
BASE_EMOJI[sexIndex],
|
||||
SKIN_TONES[skinToneIndex],
|
||||
ZERO_WIDTH_JOINER,
|
||||
MICROSCOPE
|
||||
);
|
||||
|
||||
if (!shouldShowAuth()) return null;
|
||||
|
||||
if (isAuthenticated) {
|
||||
const PopoverContent = (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
data-testid="user-email"
|
||||
text={`Logged in as: ${userInfo.email}`}
|
||||
/>
|
||||
<MenuItem
|
||||
data-testid="log-out"
|
||||
text="Log Out"
|
||||
href={auth.logout}
|
||||
icon={IconNames.LOG_OUT}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={PopoverContent}>
|
||||
<Button
|
||||
data-testid="user-info"
|
||||
className={styles.menubarButton}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
{/* eslint-disable-next-line no-constant-condition -- disable profile picture until CSP is tweaked */}
|
||||
{userInfo?.picture && false ? (
|
||||
<img alt="profile" size="21px" src={userInfo?.picture} />
|
||||
) : (
|
||||
<span style={{ fontSize: "18px" }}>{scientist}</span>
|
||||
)}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const LoginButton = (
|
||||
<Tooltip
|
||||
content="Log in to cellxgene"
|
||||
position="bottom"
|
||||
hoverOpenDelay={globals.tooltipHoverOpenDelay}
|
||||
>
|
||||
<AnchorButton
|
||||
type="button"
|
||||
data-testid="log-in"
|
||||
href={auth.login}
|
||||
className={styles.menubarButton}
|
||||
>
|
||||
Log In
|
||||
</AnchorButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
if (isPromptOpen) {
|
||||
return (
|
||||
<Popover
|
||||
position={PopoverPosition.AUTO_END}
|
||||
isOpen
|
||||
content={<PromptContent setIsPromptOpen={setIsPromptOpen} />}
|
||||
onInteraction={setIsPromptOpen}
|
||||
>
|
||||
{LoginButton}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
return LoginButton;
|
||||
|
||||
function shouldShowAuth() {
|
||||
return auth && auth.requires_client_login;
|
||||
}
|
||||
|
||||
function shouldShowPrompt() {
|
||||
if (storageGet(KEYS.LOGIN_PROMPT) === LOGIN_PROMPT_OFF) return false;
|
||||
|
||||
return shouldShowAuth && !isAuthenticated;
|
||||
}
|
||||
});
|
||||
|
||||
function PromptContent({ setIsPromptOpen }) {
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
|
||||
function handleOKClick() {
|
||||
if (isChecked) {
|
||||
storageSet(KEYS.LOGIN_PROMPT, LOGIN_PROMPT_OFF);
|
||||
}
|
||||
|
||||
setIsPromptOpen(false);
|
||||
}
|
||||
|
||||
function handleCheckboxChange() {
|
||||
setIsChecked(!isChecked);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card style={{ width: "500px" }} elevation={Elevation.TWO}>
|
||||
<p>
|
||||
Logging in will enable you to create your own categories and labels.
|
||||
Logging in later will reset cellxgene to the default view and cause you
|
||||
to lose progress.
|
||||
</p>
|
||||
<Checkbox
|
||||
style={{ width: "230px" }}
|
||||
checked={isChecked}
|
||||
onChange={handleCheckboxChange}
|
||||
data-testid="login-hint-do-not-show-again"
|
||||
>
|
||||
Do not show me this message again
|
||||
</Checkbox>
|
||||
<div
|
||||
style={{ display: "flex", justifyContent: "flex-end", marginTop: 15 }}
|
||||
>
|
||||
<Button
|
||||
onClick={handleOKClick}
|
||||
intent="primary"
|
||||
data-testid="login-hint-yes"
|
||||
>
|
||||
Acknowledge
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default Auth;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Position,
|
||||
Button,
|
||||
Popover,
|
||||
NumericInput,
|
||||
ButtonGroup,
|
||||
Icon,
|
||||
Intent,
|
||||
NumericInput,
|
||||
Popover,
|
||||
Position,
|
||||
Tooltip,
|
||||
} from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
|
||||
import { tooltipHoverOpenDelay } from "../../globals";
|
||||
import styles from "./menubar.css";
|
||||
|
||||
@@ -28,13 +32,13 @@ const Clip = React.memo((props) => {
|
||||
pendingClipPercentiles?.clipPercentileMin ?? clipPercentileMin;
|
||||
const clipMax =
|
||||
pendingClipPercentiles?.clipPercentileMax ?? clipPercentileMax;
|
||||
const activeClipClass =
|
||||
const intent =
|
||||
clipPercentileMin > 0 || clipPercentileMax < 100
|
||||
? " bp3-intent-warning"
|
||||
: "";
|
||||
? Intent.INTENT_WARNING
|
||||
: Intent.NONE;
|
||||
|
||||
return (
|
||||
<div className={`bp3-button-group ${styles.menubarButton}`}>
|
||||
<ButtonGroup className={`${styles.menubarButton}`}>
|
||||
<Popover
|
||||
target={
|
||||
<Tooltip
|
||||
@@ -45,7 +49,8 @@ const Clip = React.memo((props) => {
|
||||
<Button
|
||||
type="button"
|
||||
data-testid="visualization-settings"
|
||||
className={`bp3-button bp3-icon-timeline-bar-chart ${activeClipClass}`}
|
||||
intent={intent}
|
||||
icon={IconNames.TIMELINE_BAR_CHART}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
@@ -126,7 +131,7 @@ const Clip = React.memo((props) => {
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import * as globals from "../../globals";
|
||||
import styles from "./menubar.css";
|
||||
import actions from "../../actions";
|
||||
import Clip from "./clip";
|
||||
|
||||
import AuthButtons from "./authButtons";
|
||||
import InformationMenu from "./infoMenu";
|
||||
import Subset from "./subset";
|
||||
import UndoRedoReset from "./undoRedo";
|
||||
import DiffexpButtons from "./diffexpButtons";
|
||||
@@ -42,7 +42,7 @@ import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
|
||||
celllist2: state.differential.celllist2,
|
||||
libraryVersions: state.config?.["library_versions"],
|
||||
auth: state.config?.authentication,
|
||||
userinfo: state.userinfo,
|
||||
userInfo: state.userInfo,
|
||||
undoDisabled: state["@@undoable/past"].length === 0,
|
||||
redoDisabled: state["@@undoable/future"].length === 0,
|
||||
aboutLink: state.config?.links?.["about-dataset"],
|
||||
@@ -204,7 +204,6 @@ class MenuBar extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
dispatch,
|
||||
libraryVersions,
|
||||
disableDiffexp,
|
||||
undoDisabled,
|
||||
redoDisabled,
|
||||
@@ -212,17 +211,14 @@ class MenuBar extends React.PureComponent {
|
||||
clipPercentileMin,
|
||||
clipPercentileMax,
|
||||
graphInteractionMode,
|
||||
aboutLink,
|
||||
showCentroidLabels,
|
||||
privacyURL,
|
||||
tosURL,
|
||||
categoricalSelection,
|
||||
colorAccessor,
|
||||
subsetPossible,
|
||||
subsetResetPossible,
|
||||
enableReembedding,
|
||||
userInfo,
|
||||
auth,
|
||||
userinfo,
|
||||
} = this.props;
|
||||
const { pendingClipPercentiles } = this.state;
|
||||
|
||||
@@ -248,10 +244,7 @@ class MenuBar extends React.PureComponent {
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<AuthButtons auth={auth} userinfo={userinfo} />
|
||||
<InformationMenu
|
||||
{...{ libraryVersions, aboutLink, tosURL, privacyURL, dispatch }}
|
||||
/>
|
||||
<AuthButtons {...{ auth, userInfo }} />
|
||||
<UndoRedoReset
|
||||
dispatch={dispatch}
|
||||
undoDisabled={undoDisabled}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
import { Button, Popover, Menu, MenuItem, Position } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
import styles from "./menubar.css";
|
||||
|
||||
const handleClick = (dispatch) => {
|
||||
dispatch({ type: "toggle dataset drawer" });
|
||||
};
|
||||
|
||||
const InformationMenu = React.memo((props) => {
|
||||
const { libraryVersions, tosURL, privacyURL, dispatch } = props;
|
||||
return (
|
||||
<div className={`bp3-button-group ${styles.menubarButton}`}>
|
||||
<Popover
|
||||
content={
|
||||
<Menu>
|
||||
<MenuItem
|
||||
onClick={() => handleClick(dispatch)}
|
||||
icon={IconNames.BOOK}
|
||||
text="Dataset Overview"
|
||||
/>
|
||||
|
||||
<MenuItem
|
||||
href="https://chanzuckerberg.github.io/cellxgene/"
|
||||
target="_blank"
|
||||
icon="help"
|
||||
text="Help"
|
||||
/>
|
||||
<MenuItem
|
||||
href="https://join-cellxgene-users.herokuapp.com/"
|
||||
target="_blank"
|
||||
icon="chat"
|
||||
text="Chat"
|
||||
/>
|
||||
<MenuItem
|
||||
href="https://github.com/chanzuckerberg/cellxgene"
|
||||
target="_blank"
|
||||
icon="git-branch"
|
||||
text="Github"
|
||||
/>
|
||||
<MenuItem
|
||||
target="_blank"
|
||||
text={
|
||||
libraryVersions && libraryVersions.cellxgene
|
||||
? libraryVersions.cellxgene
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<MenuItem text="MIT License" />
|
||||
{tosURL ? (
|
||||
<MenuItem href={tosURL} target="_blank" text="Terms of Service" />
|
||||
) : null}
|
||||
{privacyURL ? (
|
||||
<MenuItem
|
||||
href={privacyURL}
|
||||
target="_blank"
|
||||
text="Privacy Policy"
|
||||
/>
|
||||
) : null}
|
||||
</Menu>
|
||||
}
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
className="bp3-button bp3-icon-info-sign"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default InformationMenu;
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from "react";
|
||||
import { AnchorButton, Tooltip } from "@blueprintjs/core";
|
||||
import { AnchorButton, ButtonGroup, Tooltip } from "@blueprintjs/core";
|
||||
import { IconNames } from "@blueprintjs/icons";
|
||||
import { tooltipHoverOpenDelay } from "../../globals";
|
||||
import styles from "./menubar.css";
|
||||
|
||||
const UndoRedo = React.memo((props) => {
|
||||
const { undoDisabled, redoDisabled, dispatch } = props;
|
||||
return (
|
||||
<div className={`bp3-button-group ${styles.menubarButton}`}>
|
||||
<ButtonGroup className={`${styles.menubarButton}`}>
|
||||
<Tooltip
|
||||
content="Undo"
|
||||
position="bottom"
|
||||
@@ -14,7 +15,7 @@ const UndoRedo = React.memo((props) => {
|
||||
>
|
||||
<AnchorButton
|
||||
type="button"
|
||||
className="bp3-button bp3-icon-undo"
|
||||
icon={IconNames.UNDO}
|
||||
disabled={undoDisabled}
|
||||
onClick={() => {
|
||||
dispatch({ type: "@@undoable/undo" });
|
||||
@@ -32,7 +33,7 @@ const UndoRedo = React.memo((props) => {
|
||||
>
|
||||
<AnchorButton
|
||||
type="button"
|
||||
className="bp3-button bp3-icon-redo"
|
||||
icon={IconNames.REDO}
|
||||
disabled={redoDisabled}
|
||||
onClick={() => {
|
||||
dispatch({ type: "@@undoable/redo" });
|
||||
@@ -43,7 +44,7 @@ const UndoRedo = React.memo((props) => {
|
||||
data-testid="redo"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</ButtonGroup>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ export default class MiniHistogram extends React.PureComponent {
|
||||
popoverClassName={Classes.POPOVER_CONTENT_SIZING}
|
||||
>
|
||||
<canvas
|
||||
className="bp3-popover-targer"
|
||||
style={{
|
||||
marginRight: 5,
|
||||
width,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// jshint esversion: 6
|
||||
import React from "react";
|
||||
|
||||
export default class MiniStackedBar extends React.PureComponent {
|
||||
@@ -59,7 +58,6 @@ export default class MiniStackedBar extends React.PureComponent {
|
||||
|
||||
return (
|
||||
<canvas
|
||||
className="bp3-popover-targer"
|
||||
style={{
|
||||
marginRight: 5,
|
||||
width,
|
||||
|
||||
@@ -8,26 +8,7 @@ import {
|
||||
Colors,
|
||||
Icon,
|
||||
} from "@blueprintjs/core";
|
||||
|
||||
const CookieDecision = "cxg.cookieDecision";
|
||||
|
||||
function storageGet(key, defaultValue = null) {
|
||||
try {
|
||||
const val = window.localStorage.getItem(key);
|
||||
if (val === null) return defaultValue;
|
||||
return val;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function storageSet(key, value) {
|
||||
try {
|
||||
window.localStorage.setItem(key, value);
|
||||
} catch {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
import { storageGet, storageSet, KEYS } from "../util/localStorage";
|
||||
|
||||
@connect((state) => ({
|
||||
tosURL: state.config?.parameters?.["about_legal_tos"],
|
||||
@@ -37,7 +18,7 @@ class TermsPrompt extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { tosURL, privacyURL } = this.props;
|
||||
const cookieDecision = storageGet(CookieDecision, null);
|
||||
const cookieDecision = storageGet(KEYS.COOKIE_DECISION, null);
|
||||
const hasDecided = cookieDecision !== null;
|
||||
this.state = {
|
||||
hasDecided,
|
||||
@@ -55,7 +36,7 @@ class TermsPrompt extends React.PureComponent {
|
||||
|
||||
handleOK = () => {
|
||||
this.setState({ isOpen: false });
|
||||
storageSet(CookieDecision, "yes");
|
||||
storageSet(KEYS.COOKIE_DECISION, "yes");
|
||||
if (window.cookieDecisionCallback instanceof Function) {
|
||||
try {
|
||||
window.cookieDecisionCallback();
|
||||
@@ -67,7 +48,7 @@ class TermsPrompt extends React.PureComponent {
|
||||
|
||||
handleNo = () => {
|
||||
this.setState({ isOpen: false });
|
||||
storageSet(CookieDecision, "no");
|
||||
storageSet(KEYS.COOKIE_DECISION, "no");
|
||||
};
|
||||
|
||||
renderTos() {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export const KEYS = {
|
||||
COOKIE_DECISION: "cxg.cookieDecision",
|
||||
LOGIN_PROMPT: "cxg.LOGIN_PROMPT",
|
||||
};
|
||||
|
||||
export function storageGet(key, defaultValue = null) {
|
||||
try {
|
||||
const val = window.localStorage.getItem(key);
|
||||
if (val === null) return defaultValue;
|
||||
return val;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function storageSet(key, value) {
|
||||
try {
|
||||
window.localStorage.setItem(key, value);
|
||||
} catch {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ const SPLIT_STYLE = {
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%", // There are probably additional styles that we don't want to stack
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
const FIRST_HALF_STYLE = {
|
||||
@@ -40,7 +42,7 @@ export default (props) => {
|
||||
) {
|
||||
throw Error("Only pass a single child with text to Truncate");
|
||||
}
|
||||
const originalString = children.props.children;
|
||||
const originalString = String(children.props.children);
|
||||
|
||||
let firstString;
|
||||
let secondString;
|
||||
@@ -58,7 +60,7 @@ export default (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const inheritedColor = children.props.style.color;
|
||||
const inheritedColor = children.props.style?.color;
|
||||
|
||||
const splitStyle = { ...children.props.style, ...SPLIT_STYLE };
|
||||
const secondHalfContentStyle = {
|
||||
@@ -93,6 +95,7 @@ export default (props) => {
|
||||
preventOverflow: { enabled: false },
|
||||
hide: { enabled: false },
|
||||
}}
|
||||
targetProps={{ style: children.props.style }}
|
||||
>
|
||||
{newChildren}
|
||||
</Tooltip>
|
||||
|
||||
@@ -4,7 +4,7 @@ import thunk from "redux-thunk";
|
||||
import cascadeReducers from "./cascade";
|
||||
import undoable from "./undoable";
|
||||
import config from "./config";
|
||||
import userinfo from "./userinfo";
|
||||
import userInfo from "./userinfo";
|
||||
import annoMatrix from "./annoMatrix";
|
||||
import obsCrossfilter from "./obsCrossfilter";
|
||||
import categoricalSelection from "./categoricalSelection";
|
||||
@@ -44,7 +44,7 @@ const Reducer = undoable(
|
||||
["pointDilation", pointDialation],
|
||||
["reembedController", reembedController],
|
||||
["autosave", autosave],
|
||||
["userinfo", userinfo],
|
||||
["userInfo", userInfo],
|
||||
]),
|
||||
[
|
||||
"annoMatrix",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// jshint esversion: 6
|
||||
const UserInfo = (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case "initial data load start":
|
||||
@@ -7,12 +6,12 @@ const UserInfo = (state = {}, action) => {
|
||||
loading: true,
|
||||
error: null,
|
||||
};
|
||||
case "userinfo load complete":
|
||||
case "userInfo load complete":
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
error: null,
|
||||
...action.userinfo,
|
||||
...action.userInfo,
|
||||
};
|
||||
case "initial data load error":
|
||||
return {
|
||||
@@ -106,15 +106,19 @@ export function loadUserColorConfig(userColors) {
|
||||
return -1;
|
||||
})
|
||||
.reduce(
|
||||
(acc, label, i) => {
|
||||
(acc, label) => {
|
||||
const color = parseRGB(userColors[category][label]);
|
||||
acc[0][label] = color;
|
||||
acc[1][i] = d3.rgb(255 * color[0], 255 * color[1], 255 * color[2]);
|
||||
acc[1][label] = d3.rgb(
|
||||
255 * color[0],
|
||||
255 * color[1],
|
||||
255 * color[2]
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
[{}, {}]
|
||||
);
|
||||
const scale = (i) => scaleMap[i];
|
||||
const scale = (label) => scaleMap[label];
|
||||
convertedUserColors[category] = { colors, scale };
|
||||
});
|
||||
return convertedUserColors;
|
||||
|
||||
@@ -178,26 +178,26 @@ function promoteTypedArray(o) {
|
||||
*/
|
||||
if (isFpTypedArray(o) || Array.isArray(o)) return o;
|
||||
|
||||
let TyepdArrayCtor;
|
||||
let TypedArrayCtor;
|
||||
switch (o.constructor) {
|
||||
case Int8Array:
|
||||
case Uint8Array:
|
||||
case Uint8ClampedArray:
|
||||
case Int16Array:
|
||||
case Uint16Array:
|
||||
TyepdArrayCtor = Float32Array;
|
||||
TypedArrayCtor = Float32Array;
|
||||
break;
|
||||
|
||||
case Int32Array:
|
||||
case Uint32Array:
|
||||
TyepdArrayCtor = Float64Array;
|
||||
TypedArrayCtor = Float64Array;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Unexpected data type returned from server.");
|
||||
}
|
||||
if (o.constructor === TyepdArrayCtor) return o;
|
||||
return new TyepdArrayCtor(o);
|
||||
if (o.constructor === TypedArrayCtor) return o;
|
||||
return new TypedArrayCtor(o);
|
||||
}
|
||||
|
||||
export function matrixFBSToDataframe(arrayBuffers) {
|
||||
|
||||
Reference in New Issue
Block a user