Merge branch 'main' into colinmegill/geneset-prototype

This commit is contained in:
Colin Megill
2020-09-15 13:13:40 -07:00
32 changed files with 820 additions and 318 deletions
@@ -1,5 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`did launch page launched 1`] = `"<span style=\\"width: 185px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">c3k</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">c3k</span></span></span>"`;
exports[`did launch page launched 1`] = `"<span style=\\"max-width: 155px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">pbm</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">c3k</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">c3k</span></span></span>"`;
exports[`metadata loads categories and values from dataset appear 1`] = `"<div style=\\"display: flex; justify-content: space-between; align-items: baseline;\\"><div style=\\"display: flex; justify-content: flex-start; align-items: flex-start;\\"><label class=\\"bp3-control bp3-checkbox\\" for=\\"category-select-louvain\\"><input id=\\"category-select-louvain\\" data-testclass=\\"category-select\\" data-testid=\\"louvain:category-select\\" type=\\"checkbox\\" checked=\\"\\"><span class=\\"bp3-control-indicator\\"></span></label><span role=\\"menuitem\\" tabindex=\\"0\\" data-testclass=\\"category-expand\\" data-testid=\\"louvain:category-expand\\" style=\\"cursor: pointer;\\"><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><span data-testid=\\"louvain:category-label\\" aria-label=\\"louvain\\" class=\\"\\" tabindex=\\"0\\" style=\\"max-width: 265px;\\"><span style=\\"max-width: 265px; display: flex; overflow: hidden; justify-content: flex-start;\\"><span style=\\"overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; min-width: 5px;\\">lou</span><span style=\\"position: relative; overflow: hidden; white-space: nowrap;\\"><span style=\\"color: transparent;\\">vain</span><span style=\\"position: absolute; right: 0px; color: inherit;\\">vain</span></span></span></span></span></span><svg stroke=\\"currentColor\\" fill=\\"currentColor\\" stroke-width=\\"0\\" viewBox=\\"0 0 320 512\\" data-testclass=\\"category-expand-is-not-expanded\\" height=\\"1em\\" width=\\"1em\\" xmlns=\\"http://www.w3.org/2000/svg\\" style=\\"font-size: 10px; margin-left: 5px;\\"><path d=\\"M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z\\"></path></svg></span></div><div><span class=\\"bp3-popover-wrapper\\"><span class=\\"bp3-popover-target\\"><a role=\\"button\\" data-testclass=\\"colorby\\" data-testid=\\"colorby-louvain\\" class=\\"bp3-button\\" tabindex=\\"0\\"><span icon=\\"tint\\" class=\\"bp3-icon bp3-icon-tint\\"><svg data-icon=\\"tint\\" width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\"><desc>tint</desc><path d=\\"M7.88 1s-4.9 6.28-4.9 8.9c.01 2.82 2.34 5.1 4.99 5.1 2.65-.01 5.03-2.3 5.03-5.13C12.99 7.17 7.88 1 7.88 1z\\" fill-rule=\\"evenodd\\"></path></svg></span></a></span></span></div></div><div style=\\"margin-left: 26px;\\"></div><div></div>"`;
+1
View File
@@ -39,6 +39,7 @@ module.exports = {
},
},
rules: {
"react/jsx-no-target-blank": "off",
"eslint-comments/require-description": ["error"],
"no-magic-numbers": "off",
"no-nested-ternary": "off",
+4 -3
View File
@@ -5,14 +5,14 @@ action creators related to embeddings choice
import { AnnoMatrixObsCrossfilter } from "../annoMatrix";
import { _setEmbeddingSubset } from "../util/stateManager/viewStackHelpers";
export async function _switchEmbedding(prevAnnoMatrix, 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).select(
const obsCrossfilter = await new AnnoMatrixObsCrossfilter(annoMatrix, prevCrossfilter.obsCrossfilter).select(
"emb",
newEmbeddingName,
{
@@ -30,9 +30,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 } = getState();
const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevCrossfilter } = getState();
const [annoMatrix, obsCrossfilter] = await _switchEmbedding(
prevAnnoMatrix,
prevCrossfilter,
newLayoutChoice
);
dispatch({
+1 -1
View File
@@ -43,7 +43,7 @@ async function configFetch(dispatch) {
async function userInfoFetch(dispatch) {
return fetchJson("userinfo").then((response) => {
const userinfo = { ...response.userinfo };
const { userinfo } = response || {};
dispatch({
type: "userinfo load complete",
userinfo,
@@ -491,19 +491,7 @@ const CategoryRender = React.memo(
/*
Entire category has a single value, special case.
*/
const theOneValue = categorySummary.categoryValues[0];
return (
<div style={{ marginBottom: 10, marginTop: 4 }}>
<Truncate>
<span style={{ maxWidth: 150, fontWeight: 700 }}>
{metadataField}
</span>
</Truncate>
<Truncate>
<span style={{ maxWidth: 150 }}>{`: ${theOneValue}`}</span>
</Truncate>
</div>
);
return null;
}
/*
@@ -81,7 +81,7 @@ class CategoryValue extends React.Component {
get shouldRenderStackedBarOrHistogram() {
const { colorAccessor, isColorBy, annotations } = this.props;
return colorAccessor && !isColorBy && !annotations.isEditingLabelName;
return !!colorAccessor && !isColorBy && !annotations.isEditingLabelName;
}
handleDeleteValue = () => {
@@ -439,7 +439,9 @@ class CategoryValue extends React.Component {
if (
!this.shouldRenderStackedBarOrHistogram ||
!AnnotationsHelpers.isContinuousAnnotation(schema, colorAccessor)
// This function returns true on categorical annotations(when stacked bar should not render),
// in cases where the colorAccessor is a gene this function will return undefined since genes do not live on the schema
AnnotationsHelpers.isCategoricalAnnotation(schema, colorAccessor) === true
) {
return null;
}
@@ -0,0 +1,118 @@
import React, { PureComponent } from "react";
import { connect, shallowEqual } from "react-redux";
import { Drawer } from "@blueprintjs/core";
import Async from "react-async";
import InfoFormat from "./infoFormat";
import {
selectableCategoryNames,
createCategorySummaryFromDfCol,
} 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"] ?? {},
};
})
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;
dispatch({ type: "toggle dataset drawer" });
};
render() {
const {
position,
aboutURL,
datasetTitle,
schema,
isOpen,
dataPortalProps,
} = this.props;
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>
</Drawer>
);
}
}
export default InfoDrawer;
@@ -0,0 +1,194 @@
import { H3, H1, UL, Classes } from "@blueprintjs/core";
import React from "react";
const renderContributors = (contributors, affiliations, skeleton) => {
// 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}>
{contributors.map((contributor) => {
const { email, name, institution } = contributor;
return (
<span key={name}>
{name}
{email && `(${email})`}
<sup>{affiliations.indexOf(institution) + 1}</sup>
</span>
);
})}
</p>
{renderAffiliations(affiliations, skeleton)}
</>
);
};
// generates a list of unique institutions by order of appearance in contributors
const buildAffiliations = (contributors = []) => {
const affiliations = [];
contributors.forEach((contributor) => {
const { institution } = contributor;
if (affiliations.indexOf(institution) === -1) {
affiliations.push(institution);
}
});
return affiliations;
};
const renderAffiliations = (affiliations, skeleton) => {
if (affiliations.length === 0) return null;
return (
<>
<H3 className={skeleton ? Classes.SKELETON : null}>Affiliations</H3>
<UL>
{affiliations.map((item, index) => (
<div key={item} className={skeleton ? Classes.SKELETON : null}>
<sup>{index + 1}</sup>
{" "}
{item}
</div>
))}
</UL>
</>
);
};
const renderDOILink = (type, doi, skeleton) => {
if (!doi) return null;
return (
<>
<H3 className={skeleton ? Classes.SKELETON : null}>{type}</H3>
<p className={skeleton ? Classes.SKELETON : null}>
<a href={doi} target="_blank" rel="noopener">
{doi}
</a>
</p>
</>
);
};
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>
</>
);
};
// Render list of metadata attributes found in categorical field
// Ignores categories with empty or null values
const renderSingleValueCategories = (singleValueCategories, skeleton) => {
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>
</>
);
};
// 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) => {
if (!projectLinks && !aboutURL) return null;
if (projectLinks)
return (
<>
<H3 className={skeleton ? Classes.SKELETON : null}>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}
>
<a href={link.link_url} target="_blank" rel="noopener">
{link.link_name}
</a>
</li>
);
})}
</UL>
</>
);
return (
<>
<H3 className={skeleton ? Classes.SKELETON : null}>More Info</H3>
<p>
<a
className={skeleton ? Classes.SKELETON : null}
href={aboutURL}
target="_blank"
rel="noopener"
>
{aboutURL}
</a>
</p>
</>
);
};
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") {
dataPortalProps = {};
}
const {
title,
publication_doi: doi,
preprint_doi: preprintDOI,
organism,
contributors,
project_links: projectLinks,
} = dataPortalProps;
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>
);
}
);
export default InfoFormat;
@@ -1,22 +1,27 @@
// 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";
const DATASET_TITLE_WIDTH = 190;
const DATASET_TITLE_FONT_SIZE = 14;
@connect((state) => ({
datasetTitle: state.config?.displayNames?.dataset ?? "",
aboutURL: state.config?.links?.["about-dataset"],
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
}))
class LeftSideBar extends React.Component {
handleClick = () => {
const { dispatch } = this.props;
dispatch({ type: "toggle dataset drawer" });
};
render() {
const { datasetTitle, aboutURL } = this.props;
const { datasetTitle } = this.props;
return (
<div
@@ -53,39 +58,23 @@ class LeftSideBar extends React.Component {
</span>
gene
</span>
<div
<Button
minimal
icon={IconNames.BOOK}
style={{
fontSize: DATASET_TITLE_FONT_SIZE,
position: "relative",
top: -6,
display: "inline-block",
width: DATASET_TITLE_WIDTH,
marginLeft: "7px",
height: "1.2em",
overflow: "hidden",
wordBreak: "break-all",
position: "absolute",
right: 10,
}}
onClick={this.handleClick}
>
{aboutURL ? (
<Truncate>
<a
style={{ width: 185 }}
href={aboutURL}
data-testid="header"
target="_blank"
rel="noopener noreferrer"
>
{datasetTitle}
</a>
</Truncate>
) : (
<Truncate>
<span style={{ width: 185 }} data-testid="header">
{datasetTitle}
</span>
</Truncate>
)}
</div>
<Truncate>
<span style={{ maxWidth: 155 }} data-testid="header">
{datasetTitle}
</span>
</Truncate>
</Button>
<InfoDrawer />
</div>
);
}
+1 -4
View File
@@ -250,10 +250,7 @@ class MenuBar extends React.PureComponent {
>
<AuthButtons auth={auth} userinfo={userinfo} />
<InformationMenu
libraryVersions={libraryVersions}
aboutLink={aboutLink}
tosURL={tosURL}
privacyURL={privacyURL}
{...{ libraryVersions, aboutLink, tosURL, privacyURL, dispatch }}
/>
<UndoRedoReset
dispatch={dispatch}
+11 -11
View File
@@ -1,25 +1,25 @@
// 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, aboutLink, tosURL, privacyURL } = props;
const { libraryVersions, tosURL, privacyURL, dispatch } = props;
return (
<div className={`bp3-button-group ${styles.menubarButton}`}>
<Popover
content={
<Menu>
{aboutLink ? (
<MenuItem
href={aboutLink}
target="_blank"
icon="document-open"
text="About this dataset"
/>
) : (
""
)}
<MenuItem
onClick={() => handleClick(dispatch)}
icon={IconNames.BOOK}
text="Dataset Overview"
/>
<MenuItem
href="https://chanzuckerberg.github.io/cellxgene/"
+2 -2
View File
@@ -84,7 +84,7 @@ class TermsPrompt extends React.PureComponent {
}}
href={tosURL}
target="_blank"
rel="noopener noreferrer"
rel="noopener"
>
terms of service
</a>
@@ -106,7 +106,7 @@ class TermsPrompt extends React.PureComponent {
}}
href={privacyURL}
target="_blank"
rel="noopener noreferrer"
rel="noopener"
>
privacy policy
</a>
+8
View File
@@ -20,6 +20,8 @@ const Controls = (
scatterplotXXaccessor: null, // just easier to read
scatterplotYYaccessor: null,
graphRenderCounter: 0 /* integer as <Component key={graphRenderCounter} - a change in key forces a remount */,
datasetDrawer: false,
},
action
) => {
@@ -162,6 +164,12 @@ const Controls = (
scatterplotYYaccessor: null,
};
/**************************
Dataset Drawer
**************************/
case "toggle dataset drawer":
return { ...state, datasetDrawer: !state.datasetDrawer };
default:
return state;
}