Skip to content

Commit

Permalink
Merge pull request #1921 from broadinstitute/development
Browse files Browse the repository at this point in the history
Release 1.59.0
  • Loading branch information
jlchang authored Oct 31, 2023
2 parents 973188c + 43de75d commit 9656c8d
Show file tree
Hide file tree
Showing 20 changed files with 950 additions and 173 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,8 @@ def facets
study_file_id = scope == 'study' ? @study.metadata_file.id : cluster.study_file_id
array_query = {
name: annotation[:name], array_type: 'annotations', linear_data_type: data_obj.class.name,
linear_data_id: data_obj.id, study_id: @study.id, study_file_id:
linear_data_id: data_obj.id, study_id: @study.id, study_file_id:, subsample_annotation: nil,
subsample_threshold: nil
}
annotation_arrays[identifier] = DataArray.concatenate_arrays(array_query)
facets << { annotation: identifier, groups: annotation[:values] }
Expand Down
228 changes: 176 additions & 52 deletions app/javascript/components/explore/CellFilteringPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@

import React, { useState, useEffect } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowLeft, faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { faArrowLeft, faChevronDown, faChevronRight, faUndo } from '@fortawesome/free-solid-svg-icons'

import Select from '~/lib/InstrumentedSelect'
import LoadingSpinner from '~/lib/LoadingSpinner'
import { annotationKeyProperties, clusterSelectStyle } from '~/lib/cluster-utils'

const tooltipAttrs = {
'data-toggle': 'tooltip',
'data-delay': '{"show": 150}' // Avoid flurry of tooltips on passing hover
}

/** Top content for cell facet filtering panel shown at right in Explore tab */
export function CellFilteringPanelHeader({
togglePanel, updateFilteredCells
}) {
return (
<>
<span> Filter plotted cells </span>
<span>Filter plotted cells</span>
<button className="action fa-lg cell-filtering-exit-panel"
onClick={() => {
updateFilteredCells(null)
Expand Down Expand Up @@ -71,7 +76,66 @@ function parseAnnotationName(annotationIdentifier) {
}

/** Toggle icon for collapsing a list; for each filter list, and all filter lists */
function CollapseToggleChevron({ isCollapsed, whatToToggle, isLoaded }) {
function FacetTools({
isCollapsed, whatToToggle,
isLoaded,
isRoot=false, facets, checkedMap, handleResetFilters
}) {
return (
<span className="facet-tools">
{!isLoaded &&
<span
{...tooltipAttrs}
data-original-title="Loading data..."
style={{ position: 'relative', top: '-5px', left: '-20px', cursor: 'default' }}
>
<LoadingSpinner height='14px'/>
</span>
}
{isRoot &&
<ResetFiltersButton
facets={facets}
checkedMap={checkedMap}
handleResetFilters={handleResetFilters}
/>
}
<CollapseToggleChevron
isCollapsed={isCollapsed}
whatToToggle={whatToToggle}
/>
</span>
)
}

/** Button to reset all filters to their default, initial state */
function ResetFiltersButton({ facets, checkedMap, handleResetFilters }) {
// Assess if filter-section-level checkbox should be indeterminate, i.e. "-",
// which is a common state in hierarchical checkboxes to indicate that
// some lower checkboxes are checked, and some are not.
let numTotalFilters = 0
facets.forEach(facet => numTotalFilters += facet.groups.length)
let numCheckedFilters = 0
Object.entries(checkedMap).forEach(([facet, filters]) => {
numCheckedFilters += filters.length
})
const isResetEligible = numTotalFilters !== numCheckedFilters
const resetDisplayClass = isResetEligible ? '' : 'hide-reset'

return (
<a
onClick={() => handleResetFilters()}
className={`reset-cell-filters ${resetDisplayClass}`}
data-analytics-name="reset-cell-filters"
data-toggle="tooltip"
data-original-title="Reset filters"
>
<FontAwesomeIcon icon={faUndo}/>
</a>
)
}

/** Toggle icon for collapsing a list; for each filter list, and all filter lists */
function CollapseToggleChevron({ isCollapsed, whatToToggle }) {
let toggleIcon
let toggleIconTooltipText
if (!isCollapsed) {
Expand All @@ -83,23 +147,12 @@ function CollapseToggleChevron({ isCollapsed, whatToToggle, isLoaded }) {
}

return (
<span style={{ float: 'right', marginRight: '5px' }}>
{!isLoaded &&
<span
data-toggle="tooltip"
data-original-title="Loading data..."
style={{ position: 'relative', top: '-5px', left: '-20px', cursor: 'default' }}
>
<LoadingSpinner height='14px'/>
</span>
}
<span
className="facet-toggle-chevron"
data-toggle="tooltip"
data-original-title={toggleIconTooltipText}
>
{toggleIcon}
</span>
<span
className="facet-toggle-chevron"
data-original-title={toggleIconTooltipText}
{...tooltipAttrs}
>
{toggleIcon}
</span>
)
}
Expand All @@ -109,7 +162,6 @@ function isChecked(annotation, item, checkedMap) {
return checkedMap[annotation]?.includes(item)
}


/** Cell filter component */
function CellFilter({
facet, filter, isChecked, checkedMap, handleCheck
Expand All @@ -122,7 +174,7 @@ function CellFilter({
}

return (
<label className="cell-filter-label">
<label className="cell-filter-label" style={{ marginLeft: '18px' }}>
<div style={{ marginLeft: '2px', lineHeight: '14px', ...facetLabelStyle }}>
<input
type="checkbox"
Expand All @@ -148,7 +200,7 @@ function CellFilter({
/** Facet name and collapsible list of filter checkboxes */
function CellFacet({
facet,
checkedMap, handleCheck, updateFilteredCells,
checkedMap, handleCheck, handleCheckAllFiltersInFacet, updateFilteredCells,
isAllListsCollapsed
}) {
if (Object.keys(facet).length === 0) {
Expand Down Expand Up @@ -206,6 +258,8 @@ function CellFacet({
>
<FacetHeader
facet={facet}
checkedMap={checkedMap}
handleCheckAllFiltersInFacet={handleCheckAllFiltersInFacet}
isFullyCollapsed={isFullyCollapsed}
setIsFullyCollapsed={setIsFullyCollapsed}
/>
Expand All @@ -226,7 +280,7 @@ function CellFacet({
{!isFullyCollapsed && filters.length > numFiltersPartlyCollapsed &&
<a
className="facet-toggle"
style={{ 'fontSize': '13px' }}
style={{ 'fontSize': '13px', 'marginLeft': '18px' }}
onClick={() => {setIsPartlyCollapsed(!isPartlyCollapsed)}}
>
{isPartlyCollapsed ? 'More...' : 'Less...'}
Expand All @@ -237,26 +291,24 @@ function CellFacet({
}

/** Get stylized name of facet, optional tooltip, collapse controls */
function FacetHeader({ facet, isFullyCollapsed, setIsFullyCollapsed }) {
function FacetHeader({
facet, checkedMap, handleCheckAllFiltersInFacet, isFullyCollapsed, setIsFullyCollapsed
}) {
const [facetName, rawFacetName] = parseAnnotationName(facet.annotation)
const isConventional = getIsConventionalAnnotation(rawFacetName)

const facetNameStyle = {
fontWeight: 'bold',
marginBottom: '1px',
display: 'inline-block',
width: 'calc(100% - 30px)'
}
const facetNameStyle = {}
const tooltipableFacetNameStyle = {
width: 'content-fit'
}

const loadingClass = !facet.isLoaded ? 'loading' : ''
if (!facet.isLoaded) {
facetNameStyle.color = '#777'
facetNameStyle.cursor = 'default'
}

let title = 'Author annotation'
const tooltipAttrs = { 'data-toggle': 'tooltip' }
if (isConventional) {
title = 'Conventional annotation'
const note = conventionalMetadataGlossary[rawFacetName]
Expand All @@ -265,25 +317,56 @@ function FacetHeader({ facet, isFullyCollapsed, setIsFullyCollapsed }) {
}
}
title += `. Name in data: ${rawFacetName}`
tooltipAttrs['data-original-title'] = title

const toggleClass = `cell-filters-${isFullyCollapsed ? 'hidden' : 'shown'}`

// Assess if facet-level checkbox should be indeterminate, i.e. "-",
// which is a common state in hierarchical checkboxes to indicate that
// some lower checkboxes are checked, and some are not.
const allFiltersInFacet = facet.groups
const allCheckedFiltersInFacet = checkedMap[facet.annotation]
const isFacetCheckboxSelected = allFiltersInFacet.length === allCheckedFiltersInFacet.length
const isIndeterminate = !(
allCheckedFiltersInFacet.length === 0 ||
isFacetCheckboxSelected
)

return (
<div
className={`cell-facet-header ${toggleClass}`}
onClick={() => {setIsFullyCollapsed(!isFullyCollapsed)}}
>
<span style={facetNameStyle}>
<span style={tooltipableFacetNameStyle} {...tooltipAttrs}>
{facetName}
<div>
<input
type="checkbox"
className="cell-facet-header-checkbox"
data-analytics-name={`facet-${facet.annotation}`}
name={`facet-${facet.annotation}`}
onChange={event => {
handleCheckAllFiltersInFacet(event)
}}
checked={isFacetCheckboxSelected}
ref={input => {
if (input) {
input.indeterminate = isIndeterminate
}
}}
/>
<span
className={`cell-facet-header ${toggleClass}`}
onClick={() => setIsFullyCollapsed(!isFullyCollapsed)}
>
<span className={`cell-facet-name ${loadingClass}`}>
<span
style={tooltipableFacetNameStyle}
data-original-title={title}
{...tooltipAttrs}
>
{facetName}
</span>
</span>
<FacetTools
isCollapsed={isFullyCollapsed}
whatToToggle="filter list"
isLoaded={facet.isLoaded}
/>
</span>
<CollapseToggleChevron
isCollapsed={isFullyCollapsed}
whatToToggle="filter list"
isLoaded={facet.isLoaded}
/>
</div>
)
}
Expand Down Expand Up @@ -315,32 +398,66 @@ export function CellFilteringPanel({
})
const [checkedMap, setCheckedMap] = useState(cellFilteringSelection)
const [colorByFacet, setColorByFacet] = useState(shownAnnotation)
const shownFacets = facets
const shownFacets = facets.filter(facet => facet.groups.length > 1)
const [isAllListsCollapsed, setIsAllListsCollapsed] = useState(false)

/** Top header for the "Filter" section, including all-facet controls */
function FilterSectionHeader({ isAllListsCollapsed, setIsAllListsCollapsed }) {
function FilterSectionHeader({ facets, checkedMap, handleResetFilters, isAllListsCollapsed, setIsAllListsCollapsed }) {
return (
<div
className="filter-section-header"
onClick={() => {setIsAllListsCollapsed(!isAllListsCollapsed)}}
onClick={event => {
const domClasses = Array.from(event.target.classList)
if (domClasses.includes('fa-undo') || domClasses.length === 0) {
// Don't toggle facet collapse on "Reset filters" button click
return
}
setIsAllListsCollapsed(!isAllListsCollapsed)
}}
>
<span
className="filter-section-name"
style={{ 'fontWeight': 'bold' }}
data-toggle="tooltip"
{...tooltipAttrs}
data-original-title="Use checkboxes to show or hide cells in plots. Deselected values are
assigned to the '--Filtered--' group. Hover over this legend entry to highlight."
>Filter by</span>
<CollapseToggleChevron
<FacetTools
isCollapsed={isAllListsCollapsed}
setIsCollapsed={setIsAllListsCollapsed}
whatToToggle="all filter lists"
isLoaded={true}
isRoot={true}
facets={facets}
checkedMap={checkedMap}
handleResetFilters={handleResetFilters}
/>
</div>
)
}

/** Add or remove all checked item from list */
function handleCheckAllFiltersInFacet(event) {
const facetName = event.target.name.split(':')[0].replace('facet-', '')
const isCheck = event.target.checked
const allFiltersInFacet = facets.find(f => f.annotation === facetName).groups
const updatedList = isCheck ? allFiltersInFacet : []
checkedMap[facetName] = updatedList
setCheckedMap(checkedMap)
updateFilteredCells(checkedMap)
}

/** Reset all filters to initial, selected state */
function handleResetFilters() {
const initSelection = {}
facets.forEach(facet => {
initSelection[facet.annotation] = facet.groups
})

setCheckedMap(initSelection)
updateFilteredCells(initSelection)
}

/** Add or remove checked item from list */
function handleCheck(event) {
// grab the name of the facet from the check event
Expand Down Expand Up @@ -373,13 +490,16 @@ export function CellFilteringPanel({
const filterSectionHeight = window.innerHeight - verticalPad
const filterSectionHeightProp = `${filterSectionHeight}px`

// Apply custom delay to tooltips added after initial pageload
if (window.$) {window.$('[data-toggle="tooltip"]').tooltip()}

return (
<>
<div>
<label className="labeled-select">
<span
className="cell-filtering-color-by"
data-toggle="tooltip"
{...tooltipAttrs}
data-original-title="Color the plot by an annotation"
>
Color by
Expand All @@ -400,8 +520,11 @@ export function CellFilteringPanel({
</label>
{ Object.keys(checkedMap).length !== 0 &&
<>
<div style={{ marginTop: '10px' }}>
<div className="filter-section" style={{ marginTop: '10px', marginLeft: '-10px' }}>
<FilterSectionHeader
facets={facets}
checkedMap={checkedMap}
handleResetFilters={handleResetFilters}
isAllListsCollapsed={isAllListsCollapsed}
setIsAllListsCollapsed={setIsAllListsCollapsed}
/>
Expand All @@ -412,6 +535,7 @@ export function CellFilteringPanel({
facet={facet}
checkedMap={checkedMap}
handleCheck={handleCheck}
handleCheckAllFiltersInFacet={handleCheckAllFiltersInFacet}
updateFilteredCells={updateFilteredCells}
isAllListsCollapsed={isAllListsCollapsed}
key={i}
Expand Down
Loading

0 comments on commit 9656c8d

Please sign in to comment.