-
Notifications
You must be signed in to change notification settings - Fork 318
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add clinical attributes styling from cBioPortal
- Add clinicalAttributesUtil.js - use set innerHTML to prevent having to rewrite for JSX Element list - Add attribute selection styling of clinical attribute spans
- Loading branch information
Showing
9 changed files
with
431 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
200 changes: 200 additions & 0 deletions
200
src/pages/patientView/clinicalInformation/lib/clinicalAttributesUtil.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
import * as $ from 'jquery'; | ||
import _ from 'underscore'; | ||
|
||
/** | ||
* Functions for dealing with clinical attributes. | ||
*/ | ||
/** | ||
* Clean clinical attributes. Useful for rounding numbers, or other types of | ||
* data cleanup steps. Probably differs per institution. | ||
* @param {object} clinicalData - key/value pairs of clinical data | ||
*/ | ||
function clean(clinicalData) { | ||
// Shallow Copy clinicalData | ||
const cleanClinicalData = $.extend({}, clinicalData); | ||
const NULL_VALUES = [ | ||
'not applicable', | ||
'not available', | ||
'pending', | ||
'discrepancy', | ||
'completed', | ||
'', | ||
'null', | ||
'unknown', | ||
'na', | ||
'n/a', | ||
'[unkown]', | ||
'[not submitted]', | ||
'[not evaluated]', | ||
'[not applicable]', | ||
'[not available]', | ||
'[undefined]' | ||
]; | ||
|
||
const keys = Object.keys(clinicalData); | ||
for (let i = 0; i < keys.length; i += 1) { | ||
let value; | ||
const key = keys[i]; | ||
|
||
value = clinicalData[key]; | ||
|
||
// Remove null values | ||
if (NULL_VALUES.indexOf(value.toLowerCase()) > -1) { | ||
delete cleanClinicalData[key]; | ||
} else { | ||
// Change values for certain attributes, e.g. rounding | ||
switch (key) { | ||
case 'OS_MONTHS': | ||
case 'DFS_MONTHS': | ||
if ($.isNumeric(value)) { | ||
value = Math.round(value); | ||
} | ||
cleanClinicalData[key] = value; | ||
break; | ||
default: | ||
} | ||
} | ||
} | ||
return cleanClinicalData; | ||
} | ||
|
||
/** | ||
* Get first key found in object. Otherwise return null. | ||
* @param {object} object - object with key/value pairs | ||
* @param {array} keys - array of keys | ||
*/ | ||
function getFirstKeyFound(object, keys) { | ||
if (!object) { | ||
return null; | ||
} | ||
|
||
for (let i = 0; i < keys.length; i += 1) { | ||
const value = object[keys[i]]; | ||
if (typeof value !== 'undefined' && value !== null) { | ||
return value; | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
|
||
/** | ||
* Derive clinical attributes from existing clinical attributes .e.g. age based | ||
* on a date of birth. TODO: Now only includes a funky hack to keep current | ||
* derived clinical attributes working. | ||
* @param {object} clinicalData - key/value pairs of clinical data | ||
*/ | ||
function derive(clinicalData) { | ||
const derivedClinicalAttributes = $.extend({}, clinicalData); | ||
|
||
/** | ||
* TODO: Pretty funky function to get a normalized case type. This should | ||
* probably also be a clinical attribute with a restricted vocabulary. Once | ||
* the database has been changed to include normalized case types, this | ||
* function should be removed. | ||
* @param {object} clinicalData - key/value pairs of clinical data | ||
* @param {string} caseTypeAttrs - TUMOR_TYPE or SAMPLE_TYPE value to normalize | ||
*/ | ||
function normalizedCaseType(cData, caseTypeAttrs) { | ||
let caseTypeNormalized = null; | ||
let caseType; | ||
let caseTypeLower; | ||
let i; | ||
|
||
for (i = 0; i < caseTypeAttrs.length; i += 1) { | ||
caseType = cData[caseTypeAttrs[i]]; | ||
|
||
if (caseType !== null && typeof caseType !== 'undefined') { | ||
caseTypeLower = caseType.toLowerCase(); | ||
|
||
if (caseTypeLower.indexOf('metasta') >= 0) { | ||
caseTypeNormalized = 'Metastasis'; | ||
} else if (caseTypeLower.indexOf('recurr') >= 0) { | ||
caseTypeNormalized = 'Recurrence'; | ||
} else if (caseTypeLower.indexOf('progr') >= 0) { | ||
caseTypeNormalized = 'Progressed'; | ||
} else if (caseTypeLower.indexOf('prim') >= 0 || | ||
caseTypeLower.indexOf('prim') >= 0) { | ||
caseTypeNormalized = 'Primary'; | ||
} | ||
if (caseTypeNormalized !== null && typeof caseTypeNormalized !== 'undefined') { | ||
break; | ||
} | ||
} | ||
} | ||
|
||
return caseTypeNormalized; | ||
} | ||
|
||
const caseTypeNormalized = normalizedCaseType(clinicalData, ['SAMPLE_TYPE', 'TUMOR_TISSUE_SITE', 'TUMOR_TYPE']); | ||
if (caseTypeNormalized !== null) { | ||
let loc; | ||
|
||
derivedClinicalAttributes.DERIVED_NORMALIZED_CASE_TYPE = caseTypeNormalized; | ||
|
||
// TODO: DERIVED_SAMPLE_LOCATION should probably be a clinical attribute. | ||
if (derivedClinicalAttributes.DERIVED_NORMALIZED_CASE_TYPE === 'Metastasis') { | ||
loc = getFirstKeyFound(clinicalData, ['METASTATIC_SITE', 'TUMOR_SITE']); | ||
} else if (derivedClinicalAttributes.DERIVED_NORMALIZED_CASE_TYPE === 'Primary') { | ||
loc = getFirstKeyFound(clinicalData, ['PRIMARY_SITE', 'TUMOR_SITE']); | ||
} else { | ||
loc = getFirstKeyFound(clinicalData, ['TUMOR_SITE']); | ||
} | ||
if (loc !== null) { | ||
derivedClinicalAttributes.DERIVED_SAMPLE_LOCATION = loc; | ||
} | ||
} | ||
|
||
return derivedClinicalAttributes; | ||
} | ||
|
||
/** | ||
* Run both clean and derive on the clinicalData. | ||
*/ | ||
function cleanAndDerive(clinicalData) { | ||
return derive(clean(clinicalData)); | ||
} | ||
|
||
/** | ||
* Return string of spans representing the clinical attributes. The spans | ||
* have been made specifically to add clinical attribute information as | ||
* attributes to allow for easy styling with CSS. | ||
* @param {object} clinicalData - key/value pairs of clinical data | ||
* @param {string} cancerStudyId - short name of cancer study | ||
*/ | ||
function getSpans(clinicalData, cancerStudyId) { | ||
let spans = ''; | ||
const clinicalAttributesCleanDerived = cleanAndDerive(clinicalData); | ||
|
||
const keys = Object.keys(clinicalAttributesCleanDerived); | ||
for (let i = 0; i < keys.length; i += 1) { | ||
const key = keys[i]; | ||
const value = clinicalAttributesCleanDerived[key]; | ||
spans += `<span class="clinical-attribute" attr-id="${key}" attr-value="${value}" study="${cancerStudyId}">${value}</span>`; | ||
} | ||
|
||
return spans; | ||
} | ||
|
||
/* | ||
* Add .first-order class to all elements with the lowest order attribute. | ||
* This way the first element can be styled in a different manner. If flex | ||
* order attributes were working properly in CSS, one would be able to say | ||
* .clinical-attribute:first, this is unfortunately not the case, therefore | ||
* this hack is required. See clinical-attributes.css to see how this is | ||
* used. | ||
*/ | ||
function addFirstOrderClass() { | ||
$('.sample-record-inline, #more-patient-info').each(() => { | ||
const orderSortedAttributes = _.sortBy($(this).find('a > .clinical-attribute'), (y) => { | ||
const order = parseInt($(y).css('order'), 10); | ||
if (isNaN(order)) { | ||
console.log('Warning: No order attribute found in .clinical-attribute.'); | ||
} | ||
return order; | ||
}); | ||
$(orderSortedAttributes[0]).addClass('first-order'); | ||
}); | ||
} | ||
|
||
export { cleanAndDerive, getSpans, addFirstOrderClass }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
src/pages/patientView/patientHeader/ClinicalAttributesInline.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import * as React from 'react'; | ||
import {ClinicalData} from "../../../shared/api/CBioPortalAPI"; | ||
|
||
export type IClinicalAttributesInlineProps = { | ||
clinicalData?: ClinicalData; | ||
cancerStudyId: string; | ||
}; | ||
|
||
//export default class ClinicalAttributesInline extends React.Component<IClinicalAttributesInlineProps, {}> { | ||
// public render() { | ||
// switch (this.props.status) { | ||
// case 'fetching': | ||
// return <div><Spinner spinnerName='three-bounce' /></div>; | ||
// | ||
// case 'complete': | ||
// return this.draw(); | ||
// | ||
// case 'error': | ||
// return <div>There was an error.</div>; | ||
// | ||
// default: | ||
// return <div />; | ||
// } | ||
// } | ||
//} | ||
|
||
type IClinicalAttributeProps ={ | ||
key: string; | ||
value: string; | ||
}; | ||
|
||
// class ClinicalAttribute extends React.Component<IClinicalAttributeProps, {}> { | ||
// public render() { | ||
// return <span className={`clinical-attribute`} attrId={key} attrValue={value} study={cancerStudyId}>{value}</span>; | ||
// } | ||
// } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,36 @@ | ||
import * as React from "react"; | ||
import {SampleLabelHTML} from "../../../shared/components/sampleLabel/SampleLabel"; | ||
import { ClinicalDataBySampleId } from "../../../shared/api/api-types-extended"; | ||
import {fromPairs} from 'lodash'; | ||
import {getSpans} from '../clinicalInformation/lib/clinicalAttributesUtil.js'; | ||
|
||
interface ISampleInlineProps { | ||
sample: ClinicalDataBySampleId; | ||
sampleNumber: number; | ||
showClinical: boolean; | ||
} | ||
|
||
export default class SampleInline extends React.Component<ISampleInlineProps, {}> { | ||
public render() { | ||
const { sample, sampleNumber } = this.props; | ||
const { sample, sampleNumber, showClinical } = this.props; | ||
|
||
return ( | ||
<SampleLabelHTML color={'black'} label={(sampleNumber).toString()} /> | ||
); | ||
|
||
if (showClinical) { | ||
return ( | ||
<span style={{paddingRight: '10px'}}> | ||
<SampleLabelHTML color={'black'} label={(sampleNumber).toString()} /> | ||
{' ' + sample.id} | ||
<span className="clinical-spans" dangerouslySetInnerHTML={{__html: | ||
getSpans(fromPairs(sample.clinicalData.map((x) => [x.clinicalAttributeId, x.value])), 'lgg_ucsf_2014')}}> | ||
</span> | ||
</span> | ||
); | ||
} else { | ||
return ( | ||
<span style={{paddingRight: '10px'}}> | ||
<SampleLabelHTML color={'black'} label={(sampleNumber).toString()} /> | ||
</span> | ||
); | ||
} | ||
} | ||
} |
Oops, something went wrong.