Merge branch 'master' into colinmegill/geneset-prototype

This commit is contained in:
Colin Megill
2020-06-08 16:24:52 -04:00
89 changed files with 2516 additions and 820 deletions
+3 -3
View File
@@ -29,7 +29,7 @@ async function obsAnnotationFetchAndLoad(dispatch, schema) {
fetchBinary(
`annotations/obs?annotation-name=${encodeURIComponent(col.name)}`
)
.then((buffer) => Universe.matrixFBSToDataframe(buffer))
.then((buffer) => MatrixFBS.matrixFBSToDataframe(buffer))
.then((df) =>
dispatch({
type: "universe: column load success",
@@ -52,7 +52,7 @@ async function varAnnotationFetchAndLoad(dispatch, schema) {
return Promise.all(
names.map((name) =>
fetchBinary(`annotations/var?annotation-name=${encodeURIComponent(name)}`)
.then((buffer) => Universe.matrixFBSToDataframe(buffer))
.then((buffer) => MatrixFBS.matrixFBSToDataframe(buffer))
.then((df) =>
dispatch({
type: "universe: column load success",
@@ -77,7 +77,7 @@ function layoutFetchAndLoad(dispatch, schema) {
plimit.add(() =>
fetchBinary(
`layout/obs?layout-name=${encodeURIComponent(e)}`
).then((buffer) => Universe.matrixFBSToDataframe(buffer))
).then((buffer) => MatrixFBS.matrixFBSToDataframe(buffer))
)
)
).then((dfs) =>
+3 -3
View File
@@ -1,5 +1,5 @@
import { API } from "../globals";
import { Universe } from "../util/stateManager";
import { MatrixFBS } from "../util/stateManager";
import {
postNetworkErrorToast,
postAsyncSuccessToast,
@@ -24,7 +24,7 @@ function abortableFetch(request, opts, timeout = 0) {
async function doReembedFetch(dispatch, getState) {
const state = getState();
let cells = state.world.obsAnnotations.rowIndex.keys();
let cells = state.world.obsAnnotations.rowIndex.labels();
// These lines ensure that we convert any TypedArray to an Array.
// This is necessary because JSON.stringify() does some very strange
@@ -80,7 +80,7 @@ export function requestReembed() {
const res = await doReembedFetch(dispatch, getState);
const schema = JSON.parse(res.headers.get("CxG-Schema"));
const buffer = await res.arrayBuffer();
const df = Universe.matrixFBSToDataframe(buffer);
const df = MatrixFBS.matrixFBSToDataframe(buffer);
dispatch({
type: "reembed: request completed",
});
+15 -17
View File
@@ -21,7 +21,6 @@ import actions from "../actions";
graphRenderCounter: state.controls.graphRenderCounter,
}))
class App extends React.Component {
componentDidMount() {
const { dispatch } = this.props;
@@ -68,22 +67,21 @@ class App extends React.Component {
error loading
</div>
) : null}
{loading ? null : <Layout>
<LeftSideBar/>
{viewportRef =>
<>
<MenuBar/>
<Autosave/>
<TermsOfServicePrompt/>
<Legend viewportRef={viewportRef}/>
<Graph
key={graphRenderCounter}
viewportRef={viewportRef}
/>
</>
}
<RightSideBar/>
</Layout>}
{loading ? null : (
<Layout>
<LeftSideBar />
{(viewportRef) => (
<>
<MenuBar />
<Autosave />
<TermsOfServicePrompt />
<Legend viewportRef={viewportRef} />
<Graph key={graphRenderCounter} viewportRef={viewportRef} />
</>
)}
<RightSideBar />
</Layout>
)}
</Container>
);
}
@@ -6,9 +6,13 @@ import {
MenuItem,
Popover,
Position,
Tooltip,
Icon,
PopoverInteractionKind,
} from "@blueprintjs/core";
import * as globals from "../../../globals";
@connect((state) => ({
annotations: state.annotations,
}))
@@ -56,46 +60,57 @@ class AnnoMenuCategory extends React.PureComponent {
return (
<>
{isUserAnno ? (
<Popover
interactionKind={PopoverInteractionKind.HOVER}
boundary="window"
position={Position.RIGHT_TOP}
content={
<Menu>
<MenuItem
icon="tag"
data-testclass="handleAddNewLabelToCategory"
data-testid={`${metadataField}:add-new-label-to-category`}
onClick={this.activateAddNewLabelMode}
text={createText}
/>
<MenuItem
icon="edit"
disabled={annotations.isEditingCategoryName}
data-testclass="activateEditCategoryMode"
data-testid={`${metadataField}:edit-category-mode`}
onClick={this.activateEditCategoryMode}
text={editText}
/>
<MenuItem
icon="delete"
intent="danger"
data-testclass="handleDeleteCategory"
data-testid={`${metadataField}:delete-category`}
onClick={this.handleDeleteCategory}
text={deleteText}
/>
</Menu>
}
>
<Button
style={{ marginLeft: 0 }}
data-testclass="seeActions"
data-testid={`${metadataField}:see-actions`}
icon="more"
minimal
/>
</Popover>
<>
<Tooltip
content={createText}
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<Button
style={{ marginLeft: 0, marginRight: 2 }}
data-testclass="handleAddNewLabelToCategory"
data-testid={`${metadataField}:add-new-label-to-category`}
icon={<Icon icon="plus" iconSize={10} />}
onClick={this.activateAddNewLabelMode}
small
minimal
/>
</Tooltip>
<Popover
interactionKind={PopoverInteractionKind.HOVER}
boundary="window"
position={Position.RIGHT_TOP}
content={
<Menu>
<MenuItem
icon="edit"
disabled={annotations.isEditingCategoryName}
data-testclass="activateEditCategoryMode"
data-testid={`${metadataField}:edit-category-mode`}
onClick={this.activateEditCategoryMode}
text={editText}
/>
<MenuItem
icon="delete"
intent="danger"
data-testclass="handleDeleteCategory"
data-testid={`${metadataField}:delete-category`}
onClick={this.handleDeleteCategory}
text={deleteText}
/>
</Menu>
}
>
<Button
style={{ marginLeft: 0, marginRight: 5 }}
data-testclass="seeActions"
data-testid={`${metadataField}:see-actions`}
icon={<Icon icon="more" iconSize={10} />}
small
minimal
/>
</Popover>
</>
) : null}
</>
);
@@ -2,20 +2,18 @@ import React from "react";
import _ from "lodash";
import { connect } from "react-redux";
import { FaChevronRight, FaChevronDown } from "react-icons/fa";
import {
AnchorButton,
Button,
Tooltip,
Icon,
Position,
} from "@blueprintjs/core";
import { AnchorButton, Button, Tooltip } from "@blueprintjs/core";
import CategoryFlipperLayout from "./categoryFlipperLayout";
import AnnoMenu from "./annoMenuCategory";
import AnnoDialogEditCategoryName from "./annoDialogEditCategoryName";
import AnnoDialogAddLabel from "./annoDialogAddLabel";
import Truncate from "../../util/truncate";
import * as globals from "../../../globals";
import maybeTruncateString from "../../../util/maybeTruncateString";
const LABEL_WIDTH = globals.leftSidebarWidth - 100;
const ANNO_BUTTON_WIDTH = 50;
const LABEL_WIDTH_ANNO = LABEL_WIDTH - ANNO_BUTTON_WIDTH;
@connect((state, ownProps) => {
const { metadataField } = ownProps;
@@ -120,10 +118,6 @@ class Category extends React.Component {
We are still loading this category, so render a "busy" signal.
*/
const { metadataField } = this.props;
const truncatedString = maybeTruncateString(
metadataField,
globals.categoryDisplayStringMaxLength
);
const checkboxID = `category-select-${metadataField}`;
@@ -151,26 +145,17 @@ class Category extends React.Component {
<input disabled id={checkboxID} checked type="checkbox" />
<span className="bp3-control-indicator" />
</label>
<Tooltip
content={metadataField}
disabled={truncatedString === null}
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick}
position={Position.LEFT}
usePortal
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<Truncate>
<span
style={{
cursor: "pointer",
display: "inline-block",
width: LABEL_WIDTH,
}}
>
{truncatedString || metadataField}
{metadataField}
</span>
</Tooltip>
</Truncate>
</div>
<div>
<Button minimal loading intent="primary" />
@@ -205,21 +190,22 @@ class Category extends React.Component {
false
);
const truncatedString = maybeTruncateString(
metadataField,
globals.categoryDisplayStringMaxLength
);
if (
!isUserAnno &&
schema?.annotations?.obsByName[metadataField]?.categories?.length === 1
) {
return (
<div style={{ marginBottom: 10, marginTop: 4 }}>
<span style={{ fontWeight: 700 }}>
{truncatedString || metadataField}
</span>
: {schema.annotations.obsByName[metadataField].categories[0]}
<Truncate>
<span style={{ maxWidth: 150, fontWeight: 700 }}>
{metadataField}
</span>
</Truncate>
<Truncate>
<span style={{ maxWidth: 150 }}>
{`: ${schema.annotations.obsByName[metadataField].categories[0]}`}
</span>
</Truncate>
</div>
);
}
@@ -252,49 +238,42 @@ class Category extends React.Component {
/>
<span className="bp3-control-indicator" />
</label>
<Tooltip
content={metadataField}
disabled={truncatedString === null}
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick}
position={Position.LEFT}
usePortal
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
<span
role="menuitem"
tabIndex="0"
data-testid={`${metadataField}:category-expand`}
onKeyPress={(e) => {
if (e.key === "Enter") {
this.handleCategoryClick();
}
}}
style={{
cursor: "pointer",
}}
onClick={this.handleCategoryClick}
>
<span
role="menuitem"
tabIndex="0"
data-testid={`${metadataField}:category-expand`}
onKeyPress={(e) => {
if (e.key === "Enter") {
this.handleCategoryClick();
}
}}
style={{
cursor: "pointer",
display: "inline-block",
}}
onClick={this.handleCategoryClick}
>
{isUserAnno ? (
<Icon style={{ marginRight: 5 }} icon="tag" iconSize={16} />
) : null}
{truncatedString || metadataField}
{isExpanded ? (
<FaChevronDown
data-testclass="category-expand-is-expanded"
style={{ fontSize: 10, marginLeft: 5 }}
/>
) : (
<FaChevronRight
data-testclass="category-expand-is-not-expanded"
style={{ fontSize: 10, marginLeft: 5 }}
/>
)}
</span>
</Tooltip>
<Truncate>
<span
style={{
maxWidth: isUserAnno ? LABEL_WIDTH_ANNO : LABEL_WIDTH,
}}
data-testid={`${metadataField}:category-label`}
>
{metadataField}
</span>
</Truncate>
{isExpanded ? (
<FaChevronDown
data-testclass="category-expand-is-expanded"
style={{ fontSize: 10, marginLeft: 5 }}
/>
) : (
<FaChevronRight
data-testclass="category-expand-is-not-expanded"
style={{ fontSize: 10, marginLeft: 5 }}
/>
)}
</span>
</div>
{<AnnoDialogEditCategoryName metadataField={metadataField} />}
{<AnnoDialogAddLabel metadataField={metadataField} />}
@@ -7,17 +7,17 @@ import {
MenuItem,
Popover,
Position,
Icon,
PopoverInteractionKind,
Tooltip,
} from "@blueprintjs/core";
import Occupancy from "./occupancy";
import * as globals from "../../../globals";
import styles from "../categorical.css";
import AnnoDialog from "../annoDialog";
import LabelInput from "../labelInput";
import Truncate from "../../util/truncate";
import { AnnotationsHelpers } from "../../../util/stateManager";
import maybeTruncateString from "../../../util/maybeTruncateString";
import { labelPrompt, isLabelErroneous } from "../labelUtil";
/* this is defined outside of the class so we can use it in connect() */
@@ -316,13 +316,6 @@ class CategoryValue extends React.Component {
categories = schema.annotations.obsByName[colorAccessor]?.categories;
}
const truncatedString = maybeTruncateString(
displayString,
colorAccessor && !isColorBy
? globals.categoryLabelDisplayStringShortLength
: globals.categoryLabelDisplayStringLongLength
);
const editModeActive =
isUserAnno &&
annotations.labelEditable.category === metadataField &&
@@ -331,6 +324,26 @@ class CategoryValue extends React.Component {
const valueToggleLabel = `value-toggle-checkbox-${displayString}`;
const LEFT_MARGIN = 33;
const CHECKBOX = 26;
const CELL_NUMBER = 61;
const ANNO_MENU = 26;
const LABEL_MARGIN = 24;
const otherElementsWidth =
LEFT_MARGIN +
CHECKBOX +
CELL_NUMBER +
LABEL_MARGIN +
(isUserAnno ? ANNO_MENU : 0);
const OCCUPANCY_WIDTH = 100;
const labelWidth =
colorAccessor && !isColorBy
? globals.leftSidebarWidth - otherElementsWidth - OCCUPANCY_WIDTH
: globals.leftSidebarWidth - otherElementsWidth;
return (
<div
key={i}
@@ -343,7 +356,7 @@ class CategoryValue extends React.Component {
}
data-testclass="categorical-row"
style={{
padding: "4px 7px",
padding: "4px 0px 4px 7px",
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
@@ -383,21 +396,12 @@ class CategoryValue extends React.Component {
onMouseLeave={this.handleMouseEnter}
/>
</label>
<Tooltip
content={displayString}
disabled={truncatedString === null}
hoverOpenDelay={globals.tooltipHoverOpenDelayQuick}
position={Position.LEFT}
usePortal
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
<Truncate>
<span
data-testid={`categorical-value-${metadataField}-${displayString}`}
data-testclass="categorical-value"
style={{
width: labelWidth,
color:
displayString === globals.unassignedCategoryLabel
? "#ababab"
@@ -410,13 +414,13 @@ class CategoryValue extends React.Component {
overflow: "hidden",
lineHeight: "1.1em",
height: "1.1em",
wordBreak: "break-all",
verticalAlign: "middle",
marginRight: LABEL_MARGIN,
}}
>
{truncatedString || displayString}
{displayString}
</span>
</Tooltip>
</Truncate>
{editModeActive ? (
<div>
<AnnoDialog
@@ -561,14 +565,14 @@ class CategoryValue extends React.Component {
>
<Button
style={{
marginLeft: 0,
marginLeft: 2,
position: "relative",
top: -1,
minHeight: 16,
}}
data-testclass="seeActions"
data-testid={`${metadataField}:${displayString}:see-actions`}
icon="more"
icon={<Icon icon="more" iconSize={10} />}
small
minimal
/>
@@ -168,7 +168,7 @@ class Occupancy extends React.PureComponent {
else this.createHistogram();
}}
/>
<div key="text" style={{ fontFamily: "Roboto", fontSize: "14px" }}>
<div key="text" style={{ fontSize: "14px" }}>
<p style={{ margin: "0" }}>
This histograms shows the distribution of{" "}
<strong>{colorAccessor}</strong> within{" "}
@@ -105,10 +105,12 @@ const continuous = (selectorId, colorscale, colorAccessor) => {
colorScale: state.colors.scale,
}))
class ContinuousLegend extends React.Component {
componentDidUpdate(prevProps) {
const { colorAccessor, colorScale } = this.props;
if (prevProps.colorAccessor !== colorAccessor || prevProps.colorScale !== colorScale) {
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,7 +132,9 @@ class ContinuousLegend extends React.Component {
return (
<div
id="continuous_legend"
ref={ref => {this.ref = ref}}
ref={(ref) => {
this.ref = ref;
}}
style={{
display: colorAccessor ? "inherit" : "none",
position: "absolute",
+15 -13
View File
@@ -2,19 +2,21 @@
import React from "react";
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>
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;
+54 -50
View File
@@ -2,9 +2,7 @@
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.
@@ -26,60 +24,66 @@ class Layout extends React.Component {
render() {
const { children } = this.props;
const [ leftSidebar, renderGraph, rightSidebar ] = children;
return <div
style={{
display: "grid",
gridTemplateColumns: `
[left-sidebar-start] ${globals.leftSidebarWidth+1}px
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]
[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",
gridTemplateRows: "[top] auto [bottom]",
gridTemplateAreas: "left-sidebar | graph | right-sidebar",
columnGap: "0px",
justifyItems: "stretch",
alignItems: "stretch",
height: "inherit",
overflowY: "auto"
width: "inherit",
position: "relative",
top: 0,
left: 0,
minWidth: "1240px",
}}
>
{leftSidebar}
<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>
<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>;
);
}
}
@@ -8,6 +8,7 @@ export default
colorAccessor: state.colors.colorAccessor,
dilatedValue: state.pointDilation.categoryField,
labels: state.centroidLabels.labels,
categoricalSelection: state.categoricalSelection,
}))
class CentroidLabels extends PureComponent {
// Check to see if centroids have either just been displayed or removed from the overlay
@@ -33,11 +34,21 @@ class CentroidLabels extends PureComponent {
dilatedValue,
dispatch,
colorAccessor,
categoricalSelection,
} = this.props;
if (!colorAccessor || labels.size === undefined || labels.size === 0)
return null;
const {
categoryValueIndices,
categoryValueSelected,
} = categoricalSelection?.[colorAccessor];
const labelSVGS = [];
let fontSize = "15px";
let fontWeight = null;
const deselectOpacity = 0.375;
labels.forEach((coords, label) => {
fontSize = "15px";
fontWeight = null;
@@ -46,6 +57,8 @@ class CentroidLabels extends PureComponent {
fontWeight = "800";
}
const selected = categoryValueSelected[categoryValueIndices.get(label)];
// Mirror LSB middle truncation
let displayLabel = label;
if (displayLabel.length > categoryLabelDisplayStringLongLength) {
@@ -72,11 +85,11 @@ class CentroidLabels extends PureComponent {
textAnchor="middle"
data-label={label}
style={{
fontFamily: "Roboto Condensed",
fontSize,
fontWeight,
fill: "black",
userSelect: "none",
opacity: selected ? 1 : deselectOpacity,
}}
onMouseEnter={(e) =>
dispatch({
@@ -81,7 +81,7 @@ export default class GraphOverlayLayer extends PureComponent {
position: "absolute",
top: 0,
left: 0,
zIndex: 1,
zIndex: 2,
backgroundColor: displaying ? "rgba(255, 255, 255, 0.55)" : "",
}}
onMouseMove={handleCanvasEvent}
@@ -14,7 +14,7 @@ export default (
handleDragAction,
handleEndAction,
handleCancelAction,
viewport,
viewport
) => {
const svg = d3.select("#graph-wrapper").select("#lasso-layer");
@@ -23,7 +23,7 @@ export default (
.brush()
.extent([
[0, 0],
[viewport.width, viewport.height]
[viewport.width, viewport.height],
])
.on("start", handleStartAction)
.on("brush", handleDragAction)
@@ -11,7 +11,6 @@ import TopLeftLogoAndTitle from "./topLeftLogoAndTitle";
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
}))
class LeftSideBar extends React.Component {
render() {
const { scatterplotXXaccessor, scatterplotYYaccessor } = this.props;
return (
@@ -3,6 +3,10 @@ import React from "react";
import { connect } from "react-redux";
import * as globals from "../../globals";
import Logo from "../framework/logo";
import Truncate from "../util/truncate";
const DATASET_TITLE_WIDTH = 190;
const DATASET_TITLE_FONT_SIZE = 14;
@connect((state) => ({
datasetTitle: state.config?.displayNames?.dataset ?? "",
@@ -14,16 +18,6 @@ class LeftSideBar extends React.Component {
render() {
const { datasetTitle, aboutURL } = this.props;
const displayTitle =
datasetTitle.length > globals.datasetTitleMaxCharacterCount
? `${datasetTitle.substring(
0,
Math.floor(globals.datasetTitleMaxCharacterCount / 2)
)}…${datasetTitle.slice(
-Math.floor(globals.datasetTitleMaxCharacterCount / 2)
)}`
: datasetTitle;
return (
<div
style={{
@@ -60,26 +54,36 @@ class LeftSideBar extends React.Component {
gene
</span>
<div
data-testid="header"
style={{
fontSize: 14,
fontSize: DATASET_TITLE_FONT_SIZE,
position: "relative",
top: -6,
display: "inline-block",
width: "190px",
width: DATASET_TITLE_WIDTH,
marginLeft: "7px",
height: "1.2em",
overflow: "hidden",
wordBreak: "break-all",
}}
title={datasetTitle}
>
{aboutURL ? (
<a href={aboutURL} target="_blank" rel="noopener noreferrer">
{displayTitle}
</a>
<Truncate>
<a
style={{ width: 185 }}
href={aboutURL}
data-testid="header"
target="_blank"
rel="noopener noreferrer"
>
{datasetTitle}
</a>
</Truncate>
) : (
displayTitle
<Truncate>
<span style={{ width: 185 }} data-testid="header">
{datasetTitle}
</span>
</Truncate>
)}
</div>
</div>
+1 -3
View File
@@ -35,9 +35,7 @@ function Clip(props) {
: "";
return (
<div
className={`bp3-button-group ${styles.menubarButton}`}
>
<div className={`bp3-button-group ${styles.menubarButton}`}>
<Popover
target={
<Tooltip
+6
View File
@@ -37,6 +37,7 @@ import DiffexpButtons from "./diffexpButtons";
showCentroidLabels: state.centroidLabels.showLabels,
tosURL: state.config?.parameters?.["about_legal_tos"],
privacyURL: state.config?.parameters?.["about_legal_privacy"],
categoricalSelection: state.categoricalSelection,
}))
class MenuBar extends React.Component {
static isValidDigitKeyEvent(e) {
@@ -203,9 +204,13 @@ class MenuBar extends React.Component {
showCentroidLabels,
privacyURL,
tosURL,
categoricalSelection,
colorAccessor,
} = this.props;
const { pendingClipPercentiles } = this.state;
const isColoredByCategorical = !!categoricalSelection?.[colorAccessor];
// constants used to create selection tool button
const [selectionTooltip, selectionButtonIcon] =
selectionTool === "brush"
@@ -267,6 +272,7 @@ class MenuBar extends React.Component {
onClick={this.handleCentroidChange}
active={showCentroidLabels}
intent={showCentroidLabels ? "primary" : "none"}
disabled={!isColoredByCategorical}
/>
</Tooltip>
<ButtonGroup className={styles.menubarButton}>
+1 -3
View File
@@ -6,9 +6,7 @@ import styles from "./menubar.css";
function InformationMenu(props) {
const { libraryVersions, aboutLink, tosURL, privacyURL } = props;
return (
<div
className={`bp3-button-group ${styles.menubarButton}`}
>
<div className={`bp3-button-group ${styles.menubarButton}`}>
<Popover
content={
<Menu>
+3 -3
View File
@@ -1,4 +1,4 @@
:local(.menubarButton) {
margin-top: 8px;
margin-left: 8px;
}
margin-top: 8px;
margin-left: 8px;
}
+2 -4
View File
@@ -12,9 +12,7 @@ function Subset(props) {
} = props;
return (
<ButtonGroup
className={styles.menubarButton}
>
<ButtonGroup className={styles.menubarButton}>
<Tooltip
content="Subset to currently selected cells and associated metadata"
position="bottom"
@@ -26,7 +24,7 @@ function Subset(props) {
disabled={!subsetPossible}
icon="pie-chart"
onClick={handleSubset}
/>
/>
</Tooltip>
<Tooltip
content="Undo subset and show all cells and associated metadata"
+1 -3
View File
@@ -7,9 +7,7 @@ import styles from "./menubar.css";
function InformationMenu(props) {
const { undoDisabled, redoDisabled, dispatch } = props;
return (
<div
className={`bp3-button-group ${styles.menubarButton}`}
>
<div className={`bp3-button-group ${styles.menubarButton}`}>
<Tooltip
content="Undo"
position="bottom"
+2 -3
View File
@@ -5,12 +5,11 @@ import Continuous from "../continuous/continuous";
import GeneExpression from "../geneExpression";
import * as globals from "../../globals";
@connect(state => ({
@connect((state) => ({
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
}))
class RightSidebar extends React.Component {
render() {
return (
<div
@@ -22,7 +21,7 @@ class RightSidebar extends React.Component {
position: "relative",
overflowY: "inherit",
height: "inherit",
width: "inherit"
width: "inherit",
}}
>
<GeneExpression />
+85
View File
@@ -0,0 +1,85 @@
import React, { cloneElement } from "react";
import { Tooltip, Position } from "@blueprintjs/core";
import { tooltipHoverOpenDelayQuick } from "../../globals";
const SPLIT_STYLE = {
display: "flex",
overflow: "hidden",
justifyContent: "flex-start",
};
const FIRST_HALF_STYLE = {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
flexShrink: 1,
minWidth: "5px",
};
const SECOND_HALF_INNER_STYLE = {
position: "absolute",
right: 0,
};
const SECOND_HALF_STYLE = {
color: "transparent",
position: "relative",
overflow: "hidden",
whiteSpace: "nowrap",
};
export default (props) => {
const { children } = props;
// Truncate only support a single child with a text child
if (
React.Children.count(children) !== 1 ||
React.Children.count(children.props?.children) !== 1
) {
throw Error("Only pass a single child with text to Truncate");
}
const originalString = children.props.children;
const firstString = originalString.substr(0, originalString.length / 2);
const secondString = originalString.substr(originalString.length / 2);
const inheritedColor = children.props.style.color;
const splitStyle = { ...children.props.style, ...SPLIT_STYLE };
const secondHalfInnerStyle = {
...SECOND_HALF_INNER_STYLE,
color: inheritedColor || "initial",
};
const truncatedJSX = (
<span style={splitStyle}>
<span style={FIRST_HALF_STYLE}>{firstString}</span>
<span style={SECOND_HALF_STYLE}>
{secondString}
<span style={secondHalfInnerStyle}>{secondString}</span>
</span>
</span>
);
// clone children, changing the children(text) to the truncated string
const newChildren = React.Children.map(children, (child) =>
cloneElement(child, {
children: truncatedJSX,
"aria-label": originalString,
})
);
return (
<Tooltip
content={originalString}
hoverOpenDelay={tooltipHoverOpenDelayQuick}
position={Position.LEFT}
usePortal
modifiers={{
preventOverflow: { enabled: false },
hide: { enabled: false },
}}
>
{newChildren}
</Tooltip>
);
};
+1 -1
View File
@@ -31,7 +31,7 @@ const CategoricalSelection = (
const names = CH.selectableCategoryNames(
world.schema,
CH.maxCategoryItems(prevSharedState.config),
dataframe.colIndex.keys()
dataframe.colIndex.labels()
);
if (names.length === 0) return state;
return {
+1
View File
@@ -63,6 +63,7 @@ const centroidLabels = (state = initialState, action, sharedNextState) => {
};
case "color by continuous metadata":
case "color by expression":
return { ...state, labels: [] };
case "reset centroid labels":
+5 -5
View File
@@ -93,7 +93,7 @@ const WorldReducer = (
let worldValSlice = val;
if (!World.worldEqUniverse(state, universe)) {
worldValSlice = universeVarData
.subset(state.obsAnnotations.rowIndex.keys(), [key], null)
.subset(state.obsAnnotations.rowIndex.labels(), [key], null)
.icol(0)
.asArray();
}
@@ -129,10 +129,10 @@ const WorldReducer = (
//
let clippedVarData = state.varData;
const keysToDrop = clippedVarData.colIndex
.keys()
.labels()
.filter((k) => !unclippedVarData.hasCol(k));
const keysToAdd = unclippedVarData.colIndex
.keys()
.labels()
.filter((k) => !clippedVarData.hasCol(k));
keysToDrop.forEach((k) => {
clippedVarData = clippedVarData.dropCol(k);
@@ -171,7 +171,7 @@ const WorldReducer = (
let newAnnotation = null;
if (!World.worldEqUniverse(state, universe)) {
newAnnotation = universe.obsAnnotations
.subset(state.obsAnnotations.rowIndex.keys(), [name], null)
.subset(state.obsAnnotations.rowIndex.labels(), [name], null)
.icol(0)
.asArray();
} else {
@@ -303,7 +303,7 @@ const WorldReducer = (
let schema = origSchema;
// alias the names the server sent us, in case they were not the same as the schema
const embedingLabels = embedding.colIndex.keys();
const embedingLabels = embedding.colIndex.labels();
const labels = {
[embedingLabels[0]]: dims[0],
[embedingLabels[1]]: dims[1],
+51 -73
View File
@@ -1,6 +1,5 @@
import { IdentityInt32Index, isLabelIndex } from "./labelIndex";
// weird cross-dependency that we should clean up someday...
import { sortArray } from "../typedCrossfilter/sort";
import {
isTypedArray,
isArrayOrTypedArray,
@@ -389,7 +388,7 @@ class Dataframe {
let dstLabels;
if (!labels) {
// combine all columns
dstLabels = dataframe.colIndex.keys();
dstLabels = dataframe.colIndex.labels();
srcLabels = dstLabels;
} else if (Array.isArray(labels)) {
// combine subset of keys with no aliasing
@@ -537,7 +536,12 @@ class Dataframe {
}
static empty(rowIndex = null, colIndex = null) {
return new Dataframe([0, 0], [], rowIndex, colIndex);
const dims = [
rowIndex ? rowIndex.size() : 0,
colIndex ? colIndex.size() : 0,
];
if (dims[0] && dims[1]) throw new Error("not an empty dataframe");
return new Dataframe(dims, new Array(dims[1]), rowIndex, colIndex);
}
static create(dims, columnarData) {
@@ -551,97 +555,59 @@ class Dataframe {
return new Dataframe(dims, columnarData, null, null);
}
__subset(rowOffsets, colOffsets, withRowIndex) {
__subset(newRowIndex, newColIndex) {
const dims = [...this.dims];
const getSortedLabelAndOffsets = (offsets, index) => {
/*
Given offsets, return both offsets and associated lables,
sorted by offset.
*/
if (!offsets) {
return [null, null];
/* subset columns */
let { __columns, colIndex } = this;
if (newColIndex) {
const colOffsets = this.colIndex.getOffsets(newColIndex.labels());
__columns = new Array(colOffsets.length);
for (let i = 0, l = colOffsets.length; i < l; i += 1) {
__columns[i] = this.__columns[colOffsets[i]];
}
const sortedOffsets = sortArray(offsets);
const sortedLabels = new Array(sortedOffsets.length);
for (let i = 0, l = sortedOffsets.length; i < l; i += 1) {
sortedLabels[i] = index.getLabel(sortedOffsets[i]);
}
return [sortedLabels, sortedOffsets];
};
let { colIndex } = this;
if (colOffsets) {
let colLabels;
[colLabels, colOffsets] = getSortedLabelAndOffsets(
colOffsets,
this.colIndex
);
colIndex = newColIndex;
dims[1] = colOffsets.length;
colIndex = this.colIndex.subsetLabels(colLabels);
}
let { rowIndex } = this;
if (withRowIndex) rowIndex = withRowIndex;
if (rowOffsets) {
let rowLabels;
[rowLabels, rowOffsets] = getSortedLabelAndOffsets(
rowOffsets,
this.rowIndex
);
dims[0] = rowLabels.length;
if (!withRowIndex) rowIndex = this.rowIndex.subsetLabels(rowLabels);
}
/* subset columns */
let columns = this.__columns;
if (colOffsets) {
columns = new Array(colOffsets.length);
for (let i = 0, l = colOffsets.length; i < l; i += 1) {
columns[i] = this.__columns[colOffsets[i]];
}
}
/* subset rows */
if (rowOffsets) {
columns = columns.map((col) => {
if (newRowIndex) {
const rowOffsets = this.rowIndex.getOffsets(newRowIndex.labels());
__columns = __columns.map((col) => {
const newCol = new col.constructor(rowOffsets.length);
for (let i = 0, l = rowOffsets.length; i < l; i += 1) {
newCol[i] = col[rowOffsets[i]];
}
return newCol;
});
rowIndex = newRowIndex;
dims[0] = rowOffsets.length;
}
if (dims[0] === 0 || dims[1] === 0) return Dataframe.empty();
return new Dataframe(dims, columns, rowIndex, colIndex);
return new Dataframe(dims, __columns, rowIndex, colIndex);
}
subset(rowLabels, colLabels = null, withRowIndex = null) {
/*
Subset by row/col labels.
withRowIndex allows assignment of new row index during subset operation.
If withRowIndex === null, it will reset the index to identity (offset)
indexing. if withRowIndex is a label index object, it will be used
for the new dataframe.
withRowIndex allows subset with an index, rather than rowLabels.
If withRowIndex is specified, rowLabels is ignored.
*/
const toOffsets = (labels, index) => {
if (!labels) {
return null;
}
return labels.map((label) => {
const off = index.getOffset(label);
if (off === undefined) {
throw new RangeError(`unknown label: ${label}`);
}
return off;
});
};
let rowIndex = null;
if (withRowIndex) {
rowIndex = withRowIndex;
} else if (rowLabels) {
rowIndex = this.rowIndex.subset(rowLabels);
}
const rowOffsets = toOffsets(rowLabels, this.rowIndex);
const colOffsets = toOffsets(colLabels, this.colIndex);
return this.__subset(rowOffsets, colOffsets, withRowIndex);
let colIndex = null;
if (colLabels) {
colIndex = this.colIndex.subset(colLabels);
}
return this.__subset(rowIndex, colIndex);
}
isubset(rowOffsets, colOffsets = null, withRowIndex = null) {
@@ -653,7 +619,19 @@ class Dataframe {
indexing. If withRowIndex is a label index object, it will be used
for the new dataframe.
*/
return this.__subset(rowOffsets, colOffsets, withRowIndex);
let rowIndex = null;
if (withRowIndex) {
rowIndex = withRowIndex;
} else if (rowOffsets) {
rowIndex = this.rowIndex.isubset(rowOffsets);
}
let colIndex = null;
if (colOffsets) {
colIndex = this.colIndex.isubset(colOffsets);
}
return this.__subset(rowIndex, colIndex);
}
isubsetMask(rowMask, colMask = null, withRowIndex = null) {
@@ -690,7 +668,7 @@ class Dataframe {
};
const rowOffsets = toList(rowMask, nRows);
const colOffsets = toList(colMask, nCols);
return this.__subset(rowOffsets, colOffsets, withRowIndex);
return this.isubset(rowOffsets, colOffsets, withRowIndex);
}
/**
@@ -790,7 +768,7 @@ class Dataframe {
Return true if this is an empty dataframe, ie, has dimensions [0,0]
*/
const [rows, cols] = this.dims;
return rows === 0 && cols === 0;
return rows === 0 || cols === 0;
}
/****
+6 -1
View File
@@ -1,2 +1,7 @@
export { default as Dataframe } from "./dataframe";
export { DenseInt32Index, IdentityInt32Index, KeyIndex } from "./labelIndex";
export {
DenseInt32Index,
IdentityInt32Index,
KeyIndex,
isLabelIndex,
} from "./labelIndex";
+123 -16
View File
@@ -32,10 +32,10 @@ class IdentityInt32Index {
this.maxOffset = maxOffset;
}
keys() {
labels() {
// memoize
const k = fillRange(new Int32Array(this.maxOffset));
this.keys = function keys() {
this.labels = function labels() {
return k;
};
return k;
@@ -47,12 +47,24 @@ class IdentityInt32Index {
return i;
}
// eslint-disable-next-line class-methods-use-this
getOffsets(arr) {
// labels to offsets
return arr;
}
// eslint-disable-next-line class-methods-use-this
getLabel(i) {
// offset to label
return i;
}
// eslint-disable-next-line class-methods-use-this
getLabels(arr) {
// offsets to labels
return arr;
}
size() {
return this.maxOffset;
}
@@ -62,6 +74,9 @@ class IdentityInt32Index {
time/space decision - based on the resulting density
*/
const [minLabel, maxLabel] = extent(labelArray);
if (minLabel === 0 && maxLabel === labelArray.length - 1)
return new IdentityInt32Index(labelArray.length);
const labelSpaceSize = maxLabel - minLabel + 1;
const density = labelSpaceSize / this.maxOffset;
/* 0.1 is a magic number, that needs testing to optimize */
@@ -71,30 +86,43 @@ class IdentityInt32Index {
return new DenseInt32Index(labelArray, [minLabel, maxLabel]);
}
subsetLabels(labelArray) {
return this.__promote(labelArray);
subset(labels) {
/* validate subset */
const { maxOffset } = this;
for (let i = 0, l = labels.length; i < l; i += 1) {
const label = labels[i];
if (label < 0 || label >= maxOffset)
throw new RangeError(`offset or label: ${label}`);
}
return this.__promote(labels);
}
/* identity index - labels are offsets */
isubset(offsets) {
return this.subset(offsets);
}
withLabel(label) {
if (label === this.maxOffset) {
return new IdentityInt32Index(label + 1);
}
return this.__promote([...this.keys(), label]);
return this.__promote([...this.labels(), label]);
}
withLabels(labels) {
return this.__promote([...this.keys(), ...labels]);
return this.__promote([...this.labels(), ...labels]);
}
dropLabel(label) {
if (label === this.maxOffset - 1) {
return new IdentityInt32Index(label);
}
const labelArray = [...this.keys()];
const labelArray = [...this.labels()];
labelArray.splice(labelArray.indexOf(label), 1);
return this.__promote(labelArray);
}
}
class DenseInt32Index {
/*
DenseInt32Index indexes integer labels, and uses Int32Array typed arrays
@@ -129,12 +157,29 @@ class DenseInt32Index {
this.getOffset = function getOffset(l) {
return index[l - minLabel];
};
this.getOffsets = function getOffsets(arr) {
const res = new arr.constructor(arr.length);
for (let i = 0, len = arr.length; i < len; i += 1) {
res[i] = index[arr[i] - minLabel];
}
return res;
};
this.getLabel = function getLabel(i) {
return rindex[i];
};
this.getLabels = function getLabels(arr) {
const res = new arr.constructor(arr.length);
for (let i = 0, len = arr.length; i < len; i += 1) {
res[i] = rindex[arr[i]];
}
return res;
};
}
keys() {
labels() {
return this.rindex;
}
@@ -158,20 +203,44 @@ class DenseInt32Index {
return new DenseInt32Index(labelArray, [minLabel, maxLabel]);
}
subsetLabels(labelArray) {
return this.__promote(labelArray);
subset(labels) {
/* validate subset */
for (let i = 0, l = labels.length; i < l; i += 1) {
const label = labels[i];
const offset = this.getOffset(label);
if (offset === undefined || offset === -1)
throw new RangeError(`unknown label: ${label}`);
}
return this.__promote(labels);
}
// eslint-disable-next-line class-methods-use-this
isubset(offsets) {
/* validate subset */
const { rindex } = this;
const maxOffset = rindex.length;
const labels = new Int32Array(offsets.length);
for (let i = 0, l = offsets.length; i < l; i += 1) {
const offset = offsets[i];
if (offset < 0 || offset >= maxOffset)
throw new RangeError(`out of bounds offset: ${offset}`);
labels[i] = rindex[offset];
}
return this.__promote(labels);
}
withLabel(label) {
return this.__promote([...this.keys(), label]);
return this.__promote([...this.labels(), label]);
}
withLabels(labels) {
return this.__promote([...this.keys(), ...labels]);
return this.__promote([...this.labels(), ...labels]);
}
dropLabel(label) {
const labelArray = [...this.keys()];
const labelArray = [...this.labels()];
labelArray.splice(labelArray.indexOf(label), 1);
return this.__promote(labelArray);
}
@@ -207,12 +276,29 @@ class KeyIndex {
this.getOffset = function getOffset(k) {
return index.get(k);
};
this.getOffsets = function getOffsets(arr) {
const res = new arr.constructor(arr.length);
for (let i = 0, len = arr.length; i < len; i += 1) {
res[i] = index.get(arr[i]);
}
return res;
};
this.getLabel = function getLabel(i) {
return rindex[i];
};
this.getLabels = function getLabels(arr) {
const res = new arr.constructor(arr.length);
for (let i = 0, len = arr.length; i < len; i += 1) {
res[i] = rindex[arr[i]];
}
return res;
};
}
keys() {
labels() {
return this.rindex;
}
@@ -220,9 +306,30 @@ class KeyIndex {
return this.rindex.length;
}
subset(labels) {
/* validate subset */
for (let i = 0, l = labels.length; i < l; i += 1) {
const label = labels[i];
const offset = this.getOffset(label);
if (offset === undefined) throw new RangeError(`unknown label: ${label}`);
}
return new KeyIndex(labels);
}
// eslint-disable-next-line class-methods-use-this
subsetLabels(labelArray) {
return new KeyIndex(labelArray);
isubset(offsets) {
const { rindex } = this;
const maxOffset = rindex.length;
const labels = new Array(offsets.length);
for (let i = 0, l = offsets.length; i < l; i += 1) {
const offset = offsets[i];
if (offset < 0 || offset >= maxOffset)
throw new RangeError(`out of bounds offset: ${offset}`);
labels[i] = rindex[offset];
}
return new KeyIndex(labels);
}
withLabel(label) {
-12
View File
@@ -1,12 +0,0 @@
const maybeTruncateString = (str, maxLength) => {
let truncatedString = null;
if (str.length > maxLength) {
truncatedString = `${str.slice(0, maxLength / 2)}…${str.slice(
-maxLength / 2
)}`;
}
return truncatedString;
};
export default maybeTruncateString;
@@ -100,7 +100,7 @@ export function setLabelByValue(df, colName, fromLabel, toLabel) {
/*
in the dataframe column `colName`, set any value of `fromLabel` to `toLabel`
*/
const keys = df.colIndex.keys();
const keys = df.colIndex.labels();
const ndf = df.mapColumns((col, colIdx) => {
if (colName !== keys[colIdx]) return col;
@@ -118,7 +118,7 @@ export function setLabelByMask(df, colName, mask, label) {
/*
in the dataframe column `colName`, set the masked rows to 'label'
*/
const keys = df.colIndex.keys();
const keys = df.colIndex.labels();
const ndf = df.mapColumns((col, colIdx) => {
if (colName !== keys[colIdx]) return col;
@@ -187,7 +187,7 @@ export function pruneVarDataCache(varData, needed) {
if (numOverWatermark <= 0) return varData;
const { colIndex } = varData;
const all = colIndex.keys();
const all = colIndex.labels();
const unused = _.difference(all, needed);
if (unused.length > 0) {
// sort by offset in the dataframe - ie, psuedo-LRU
@@ -203,9 +203,9 @@ export function pruneVarDataCache(varData, needed) {
export function subsetAndResetGeneLists(state) {
const { userDefinedGenes, diffexpGenes } = state;
const newUserDefinedGenes = []
.concat(userDefinedGenes, diffexpGenes)
.slice(0, globals.maxGenes);
const newUserDefinedGenes = _.uniq(
[].concat(userDefinedGenes, diffexpGenes)
).slice(0, globals.maxGenes);
const newDiffExpGenes = [];
return [newUserDefinedGenes, newDiffExpGenes];
}
+85 -4
View File
@@ -1,7 +1,12 @@
import { flatbuffers } from "flatbuffers";
import { NetEncoding } from "./matrix_generated";
import { isTypedArray } from "../typeHelpers";
import { IdentityInt32Index, DenseInt32Index, KeyIndex } from "../dataframe";
import { isTypedArray, isFpTypedArray } from "../typeHelpers";
import {
Dataframe,
IdentityInt32Index,
DenseInt32Index,
KeyIndex,
} from "../dataframe";
const utf8Decoder = new TextDecoder("utf-8");
@@ -133,14 +138,14 @@ export function encodeMatrixFBS(df) {
encColIndex = encodeTypedArray(
builder,
encColIndexUType,
df.colIndex.keys()
df.colIndex.labels()
);
} else if (colIndexType === KeyIndex) {
encColIndexUType = NetEncoding.TypedArray.JSONEncodedArray;
encColIndex = encodeTypedArray(
builder,
encColIndexUType,
utf8Encoder.encode(JSON.stringify(df.colIndex.keys()))
utf8Encoder.encode(JSON.stringify(df.colIndex.labels()))
);
} else {
throw new Error("Index type FBS encoding unsupported");
@@ -162,3 +167,79 @@ export function encodeMatrixFBS(df) {
builder.finish(root);
return builder.asUint8Array();
}
function promoteTypedArray(o) {
/*
Decide what internal data type to use for the data returned from
the server.
TODO - future optimization: not all int32/uint32 data series require
promotion to float64. We COULD simply look at the data to decide.
*/
if (isFpTypedArray(o) || Array.isArray(o)) return o;
let TyepdArrayCtor;
switch (o.constructor) {
case Int8Array:
case Uint8Array:
case Uint8ClampedArray:
case Int16Array:
case Uint16Array:
TyepdArrayCtor = Float32Array;
break;
case Int32Array:
case Uint32Array:
TyepdArrayCtor = Float64Array;
break;
default:
throw new Error("Unexpected data type returned from server.");
}
if (o.constructor === TyepdArrayCtor) return o;
return new TyepdArrayCtor(o);
}
export function matrixFBSToDataframe(arrayBuffers) {
/*
Convert array of Matrix FBS to a Dataframe.
The application has strong assumptions that all scalar data will be
stored as a float32 or float64 (regardless of underlying data types).
For example, clipping of value ranges (eg, user-selected percentiles)
depends on the ability to use NaN in any numeric type.
All float data from the server is left as is. All non-float is promoted
to an appropriate float.
*/
if (!Array.isArray(arrayBuffers)) {
arrayBuffers = [arrayBuffers];
}
if (arrayBuffers.length === 0) {
return Dataframe.Dataframe.empty();
}
const fbs = arrayBuffers.map((ab) => decodeMatrixFBS(ab, true)); // leave in place
/* check that all FBS have same row dimensionality */
const { nRows } = fbs[0];
fbs.forEach((b) => {
if (b.nRows !== nRows)
throw new Error("FBS with inconsistent dimensionality");
});
const columns = fbs
.map((fb) =>
fb.columns.map((c) => {
if (isFpTypedArray(c) || Array.isArray(c)) return c;
return promoteTypedArray(c);
})
)
.flat();
// colIdx may be TypedArray or Array
const colIdx = fbs
.map((b) => (Array.isArray(b.colIdx) ? b.colIdx : Array.from(b.colIdx)))
.flat();
const nCols = columns.length;
const df = new Dataframe([nRows, nCols], columns, null, new KeyIndex(colIdx));
return df;
}
+1 -79
View File
@@ -40,84 +40,6 @@ These functions are used exclusively by the actions and reducers to
build an internal POJO for use by the rendering components.
*/
function promoteTypedArray(o) {
/*
Decide what internal data type to use for the data returned from
the server.
TODO - future optimization: not all int32/uint32 data series require
promotion to float64. We COULD simply look at the data to decide.
*/
if (isFpTypedArray(o) || Array.isArray(o)) return o;
let TyepdArrayCtor;
switch (o.constructor) {
case Int8Array:
case Uint8Array:
case Uint8ClampedArray:
case Int16Array:
case Uint16Array:
TyepdArrayCtor = Float32Array;
break;
case Int32Array:
case Uint32Array:
TyepdArrayCtor = Float64Array;
break;
default:
throw new Error("Unexpected data type returned from server.");
}
if (o.constructor === TyepdArrayCtor) return o;
return new TyepdArrayCtor(o);
}
export function matrixFBSToDataframe(arrayBuffers) {
/*
Convert array of Matrix FBS to a Dataframe.
The application has strong assumptions that all scalar data will be
stored as a float32 or float64 (regardless of underlying data types).
For example, clipping of value ranges (eg, user-selected percentiles)
depends on the ability to use NaN in any numeric type.
All float data from the server is left as is. All non-float is promoted
to an appropriate float.
*/
if (!Array.isArray(arrayBuffers)) {
arrayBuffers = [arrayBuffers];
}
if (arrayBuffers.length === 0) {
return Dataframe.Dataframe.empty();
}
const fbs = arrayBuffers.map((ab) => decodeMatrixFBS(ab, true)); // leave in place
/* check that all FBS have same row dimensionality */
const { nRows } = fbs[0];
fbs.forEach((b) => {
if (b.nRows !== nRows)
throw new Error("FBS with inconsistent dimensionality");
});
const columns = fbs
.map((fb) =>
fb.columns.map((c) => {
if (isFpTypedArray(c) || Array.isArray(c)) return c;
return promoteTypedArray(c);
})
)
.flat();
const colIdx = fbs.map((b) => b.colIdx).flat();
const nCols = columns.length;
const df = new Dataframe.Dataframe(
[nRows, nCols],
columns,
null,
new Dataframe.KeyIndex(colIdx)
);
return df;
}
export function createUniverseFromResponse(configResponse, schemaResponse) {
/*
build & return universe from a REST 0.2 /config, /schema and /annotations/obs response
@@ -178,7 +100,7 @@ export function addObsAnnotations(universe, df) {
// for all of the new data, reconcile with schema and sort categories.
const dfs = Array.isArray(df) ? df : [df];
const keys = dfs.map((d) => d.colIndex.keys()).flat();
const keys = dfs.map((d) => d.colIndex.labels()).flat();
const { schema } = universe;
keys.forEach((k) => {
const colSchema = schema.annotations.obsByName[k];
+3 -3
View File
@@ -99,7 +99,7 @@ function clipDataframe(
if (upperQuantile > 1) upperQuantile = 1;
if (lowerQuantile === 0 && upperQuantile === 1) return df;
const keys = df.colIndex.keys();
const keys = df.colIndex.labels();
return df.mapColumns((col, colIdx) => {
const colLabel = keys[colIdx];
if (!clipPredicate(df, colIdx, colLabel)) return col;
@@ -277,7 +277,7 @@ export function addObsDimensions(crossfilter, world) {
but not yet in the crossfilter
*/
const schema = world.schema.annotations.obsByName;
const dimsWeNeed = world.obsAnnotations.colIndex.keys();
const dimsWeNeed = world.obsAnnotations.colIndex.labels();
crossfilter = dimsWeNeed.reduce((xfltr, name) => {
const dimName = obsAnnoDimensionName(name);
if (xfltr.hasDimension(dimName)) return xfltr;
@@ -321,7 +321,7 @@ export function getSelectedByIndex(crossfilter) {
return array of obsIndex, containing all selected obs/cells.
*/
const selected = crossfilter.allSelectedMask(); // array of bool-ish
const keys = crossfilter.data.rowIndex.keys(); // row keys, aka universe rowIndex
const keys = crossfilter.data.rowIndex.labels(); // row keys, aka universe rowIndex
const set = new Int32Array(selected.length);
let numElems = 0;
@@ -60,9 +60,7 @@ export default class ImmutableTypedCrossfilter {
}
setData(data) {
const { selectionCache } = this;
this.selectionCache = {};
return new ImmutableTypedCrossfilter(data, this.dimensions, selectionCache);
return new ImmutableTypedCrossfilter(data, this.dimensions);
}
dimensionNames() {