mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-08 07:58:11 +08:00
Cluster Occupancy (n dim cube) (#513)
* create occupancy component * occupancy working * centering, width, flex spacing * add is-number * extend sort to cover string + int * proof of concept sorted occupancy * handle undefined occupancy entry, cleanup * only render occupancy when colorby is cat * cleanup unused vars * cleanup
This commit is contained in:
Generated
+14
-7
@@ -5202,6 +5202,17 @@
|
|||||||
"randomatic": "^3.0.0",
|
"randomatic": "^3.0.0",
|
||||||
"repeat-element": "^1.1.2",
|
"repeat-element": "^1.1.2",
|
||||||
"repeat-string": "^1.5.2"
|
"repeat-string": "^1.5.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"is-number": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"kind-of": "^3.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"finalhandler": {
|
"finalhandler": {
|
||||||
@@ -6776,13 +6787,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"is-number": {
|
"is-number": {
|
||||||
"version": "2.1.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
"integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"kind-of": "^3.0.2"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"is-obj": {
|
"is-obj": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"fuzzysort": "^1.1.4",
|
"fuzzysort": "^1.1.4",
|
||||||
"gl-mat4": "^1.1.4",
|
"gl-mat4": "^1.1.4",
|
||||||
"gl-matrix": "^2.7.1",
|
"gl-matrix": "^2.7.1",
|
||||||
|
"is-number": "^7.0.0",
|
||||||
"key-pressed": "0.0.1",
|
"key-pressed": "0.0.1",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"memoize-one": "^4.0.0",
|
"memoize-one": "^4.0.0",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Button, Tooltip } from "@blueprintjs/core";
|
|||||||
|
|
||||||
import * as globals from "../../globals";
|
import * as globals from "../../globals";
|
||||||
import Value from "./value";
|
import Value from "./value";
|
||||||
import alphabeticallySortedValues from "./util";
|
import sortedCategoryValues from "./util";
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
colorAccessor: state.controls.colorAccessor,
|
colorAccessor: state.controls.colorAccessor,
|
||||||
@@ -86,9 +86,10 @@ class Category extends React.Component {
|
|||||||
const { categoricalSelectionState, metadataField } = this.props;
|
const { categoricalSelectionState, metadataField } = this.props;
|
||||||
|
|
||||||
const cat = categoricalSelectionState[metadataField];
|
const cat = categoricalSelectionState[metadataField];
|
||||||
const optTuples = alphabeticallySortedValues([...cat.categoryIndices]);
|
const optTuples = sortedCategoryValues([...cat.categoryIndices]);
|
||||||
return _.map(optTuples, (tuple, i) => (
|
return _.map(optTuples, (tuple, i) => (
|
||||||
<Value
|
<Value
|
||||||
|
optTuples={optTuples}
|
||||||
key={tuple[1]}
|
key={tuple[1]}
|
||||||
metadataField={metadataField}
|
metadataField={metadataField}
|
||||||
categoryIndex={tuple[1]}
|
categoryIndex={tuple[1]}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// jshint esversion: 6
|
||||||
|
import React from "react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import * as d3 from "d3";
|
||||||
|
|
||||||
|
@connect()
|
||||||
|
class Occupancy extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
occupancy,
|
||||||
|
colorScale,
|
||||||
|
categoricalSelectionState,
|
||||||
|
colorAccessor,
|
||||||
|
schema
|
||||||
|
} = this.props;
|
||||||
|
const width = 100;
|
||||||
|
const height = 11;
|
||||||
|
|
||||||
|
const categories = _.filter(schema.annotations.obs, {
|
||||||
|
name: colorAccessor
|
||||||
|
})[0].categories;
|
||||||
|
|
||||||
|
const x = d3
|
||||||
|
.scaleLinear()
|
||||||
|
/* get all the keys d[1] as an array, then find the sum */
|
||||||
|
.domain([0, d3.sum(Array.from(occupancy, d => d[1]))])
|
||||||
|
.range([0, width]);
|
||||||
|
|
||||||
|
let currentOffset = 0;
|
||||||
|
|
||||||
|
const stacks = categoricalSelectionState[colorAccessor].categoryValues.map(
|
||||||
|
d => {
|
||||||
|
const o = occupancy.get(d);
|
||||||
|
|
||||||
|
const scaledValue = x(o);
|
||||||
|
|
||||||
|
const stackItem = {
|
||||||
|
key: d,
|
||||||
|
value: o || 0,
|
||||||
|
rectWidth: o ? scaledValue : 0,
|
||||||
|
offset: currentOffset,
|
||||||
|
fill: o ? colorScale(categories.indexOf(d)) : "rgb(255,255,255)"
|
||||||
|
};
|
||||||
|
currentOffset += o ? scaledValue : 0;
|
||||||
|
return stackItem;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
style={{
|
||||||
|
marginRight: 5,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stacks.map(d => (
|
||||||
|
<rect
|
||||||
|
key={d.key}
|
||||||
|
width={d.rectWidth}
|
||||||
|
height={height}
|
||||||
|
x={d.offset}
|
||||||
|
title={d.metadataField}
|
||||||
|
fill={d.fill}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Occupancy;
|
||||||
@@ -3,9 +3,33 @@
|
|||||||
// values is [ [optVal, optIdx], ...]
|
// values is [ [optVal, optIdx], ...]
|
||||||
// index is range array
|
// index is range array
|
||||||
// return sorted index
|
// return sorted index
|
||||||
export default values =>
|
|
||||||
values.sort((a, b) => {
|
import isNumber from "is-number";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
const sortedCategoryValues = values => {
|
||||||
|
/* this sort could be memoized for perf */
|
||||||
|
|
||||||
|
const strings = [];
|
||||||
|
const ints = [];
|
||||||
|
|
||||||
|
_.forEach(values, v => {
|
||||||
|
if (isNumber(v[0])) {
|
||||||
|
ints.push(v);
|
||||||
|
} else {
|
||||||
|
strings.push(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
strings.sort((a, b) => {
|
||||||
const textA = String(a[0]).toUpperCase();
|
const textA = String(a[0]).toUpperCase();
|
||||||
const textB = String(b[0]).toUpperCase();
|
const textB = String(b[0]).toUpperCase();
|
||||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ints.sort((a, b) => +a[0] - +b[0]);
|
||||||
|
|
||||||
|
return ints.concat(strings);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sortedCategoryValues;
|
||||||
|
|||||||
@@ -2,12 +2,16 @@
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import Occupancy from "./occupancy";
|
||||||
|
import { countCategoryValues2D } from "../../util/stateManager/worldUtil";
|
||||||
|
import * as globals from "../../globals";
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
categoricalSelectionState: state.controls.categoricalSelectionState,
|
categoricalSelectionState: state.controls.categoricalSelectionState,
|
||||||
colorScale: state.controls.colorScale,
|
colorScale: state.controls.colorScale,
|
||||||
colorAccessor: state.controls.colorAccessor,
|
colorAccessor: state.controls.colorAccessor,
|
||||||
schema: _.get(state.controls.world, "schema", null)
|
schema: _.get(state.controls.world, "schema", null),
|
||||||
|
world: state.controls.world
|
||||||
}))
|
}))
|
||||||
class CategoryValue extends React.Component {
|
class CategoryValue extends React.Component {
|
||||||
toggleOff() {
|
toggleOff() {
|
||||||
@@ -36,7 +40,8 @@ class CategoryValue extends React.Component {
|
|||||||
colorAccessor,
|
colorAccessor,
|
||||||
colorScale,
|
colorScale,
|
||||||
i,
|
i,
|
||||||
schema
|
schema,
|
||||||
|
world
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!categoricalSelectionState) return null;
|
if (!categoricalSelectionState) return null;
|
||||||
@@ -50,15 +55,24 @@ class CategoryValue extends React.Component {
|
|||||||
).valueOf();
|
).valueOf();
|
||||||
|
|
||||||
/* this is the color scale, so add swatches below */
|
/* this is the color scale, so add swatches below */
|
||||||
const c = metadataField === colorAccessor;
|
const isColorBy = metadataField === colorAccessor;
|
||||||
let categories = null;
|
let categories = null;
|
||||||
|
let occupancy = null;
|
||||||
|
|
||||||
if (c && schema) {
|
if (isColorBy && schema) {
|
||||||
categories = _.filter(schema.annotations.obs, {
|
categories = _.filter(schema.annotations.obs, {
|
||||||
name: colorAccessor
|
name: colorAccessor
|
||||||
})[0].categories;
|
})[0].categories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (colorAccessor && !isColorBy) {
|
||||||
|
occupancy = countCategoryValues2D(
|
||||||
|
metadataField,
|
||||||
|
colorAccessor,
|
||||||
|
world.obsAnnotations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
@@ -72,7 +86,10 @@ class CategoryValue extends React.Component {
|
|||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
userSelect: "none"
|
userSelect: "none",
|
||||||
|
width: globals.leftSidebarWidth - 130,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label className="bp3-control bp3-checkbox">
|
<label className="bp3-control bp3-checkbox">
|
||||||
@@ -86,6 +103,18 @@ class CategoryValue extends React.Component {
|
|||||||
<span className="bp3-control-indicator" />
|
<span className="bp3-control-indicator" />
|
||||||
{displayString}
|
{displayString}
|
||||||
</label>
|
</label>
|
||||||
|
<span style={{ flexShrink: 0 }}>
|
||||||
|
{colorAccessor &&
|
||||||
|
!isColorBy &&
|
||||||
|
categoricalSelectionState[colorAccessor] ? (
|
||||||
|
<Occupancy
|
||||||
|
occupancy={occupancy.get(
|
||||||
|
category.categoryValues[categoryIndex]
|
||||||
|
)}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span>
|
||||||
<span>{count}</span>
|
<span>{count}</span>
|
||||||
@@ -95,7 +124,7 @@ class CategoryValue extends React.Component {
|
|||||||
width: 11,
|
width: 11,
|
||||||
height: 11,
|
height: 11,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
c && categories
|
isColorBy && categories
|
||||||
? colorScale(categories.indexOf(value))
|
? colorScale(categories.indexOf(value))
|
||||||
: "inherit"
|
: "inherit"
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user