diff --git a/docs/public/data_matrix_comparison.jsx b/docs/public/data_matrix_comparison.jsx index b104e8f47..dbd18c299 100644 --- a/docs/public/data_matrix_comparison.jsx +++ b/docs/public/data_matrix_comparison.jsx @@ -10,7 +10,7 @@ "query": { "url": "/data_matrix_aggregations/?type=File&sample_summary.studies=Benchmarking&dataset!=No+value&dataset!=colo829blt_in_silico&dataset!=colo829_snv_indel_challenge_data&dataset!=mei_detection_challenge_data&dataset!=ipsc_snv_indel_challenge_data&status=open&status=open-early&status=open-network&status=protected&status=protected-early&status=protected-network&limit=all", "columnAggFields": ["file_sets.libraries.assay.display_title", "sequencing.sequencer.platform"], - "rowAggFields": ["donors.display_title", "sample_summary.tissues", "dataset", "data_type"] + "rowAggFields": ["donors.display_title", "sample_summary.tissue_short_names", "dataset", "data_type"] }, "resultItemPostProcessFuncKey": "cellLinePostProcess", "resultTransformedPostProcessFuncKey": "dsaChainFile", @@ -48,7 +48,7 @@ "query": { "url": "/data_matrix_aggregations/?type=File&sample_summary.studies=Production&dataset!=No+value&status=open&status=open-early&status=open-network&status=protected&status=protected-early&status=protected-network&limit=all", "columnAggFields": ["file_sets.libraries.assay.display_title", "sequencing.sequencer.platform"], - "rowAggFields": ["donors.display_title", "sample_summary.tissues"] + "rowAggFields": ["donors.display_title", "sample_summary.tissue_short_names"] }, "headerFor": null, "idLabel": "production" diff --git a/src/encoded/item_utils/file.py b/src/encoded/item_utils/file.py index a2d5933a5..98671f6e7 100644 --- a/src/encoded/item_utils/file.py +++ b/src/encoded/item_utils/file.py @@ -20,6 +20,36 @@ get_property_values_from_identifiers, ) +# temporary mapping for tissue short names +# to be used in tests until proper metadata implementation is done +# the format will be: [TPC code] - [Portal facet] +TPC_CODE_TO_FULL_NAME = { + "3A": "3A - Whole Blood", + "3B": "3B - Buccal Swab", + "3C": "3C - Esophagus", + "3E": "3E - Colon, Asc", + "3G": "3G - Colon, Desc", + "3I": "3I - Liver", + "3K": "3K - Adrenal Gland, L", + "3M": "3M - Adrenal Gland, R", + "3O": "3O - Aorta", + "3Q": "3Q - Lung", + "3S": "3S - Heart", + "3U": "3U - Testis, L", + "3W": "3W - Testis, R", + "3Y": "3Y - Ovary, L", + "3AA": "3AA - Ovary, R", + "3AC": "3AC - Fibroblast", + "3AD": "3AD - Skin, Calf", + "3AF": "3AF - Skin, Abdomen", + "3AH": "3AH - Muscle", + "3AK": "3AK - Brain, Frontal Lobe", + "3AL": "3AL - Brain, Temporal Lobe", + "3AM": "3AM - Brain, Cerebellum", + "3AN": "3AN - Brain, Hippocampus, L", + "3AO": "3AO - Brain, Hippocampus, R", +} + def get_file_format(properties: Dict[str, Any]) -> Union[str, Dict[str, Any]]: """Get file format from properties.""" @@ -485,4 +515,30 @@ def get_tissue_category(file: Dict[str, Any], request_handler: RequestHandler) - partial( tissue.get_category, request_handler=request_handler ) - ) \ No newline at end of file + ) + +def get_tissue_protocol_id(file: Dict[str, Any], request_handler: RequestHandler) -> List[str]: + """ + Get tissue protocol ID from external ID. + """ + return get_property_values_from_identifiers( + request_handler, + get_tissues(file, request_handler), + partial( + tissue.get_protocol_id + ) + ) + +def get_tissue_short_name(file: Dict[str, Any], request_handler: RequestHandler) -> List[str]: + """ + Get tissue short name from protocol ID using temporary mapping. + """ + protocol_ids = get_tissue_protocol_id(file, request_handler) + short_names = [] + for pid in protocol_ids: + short_name = TPC_CODE_TO_FULL_NAME.get(pid) + if short_name: + short_names.append(short_name) + else: + short_names.append(pid) # Fallback to protocol ID if not found + return short_names \ No newline at end of file diff --git a/src/encoded/schemas/file.json b/src/encoded/schemas/file.json index 45f86f8ba..bbb0c6602 100644 --- a/src/encoded/schemas/file.json +++ b/src/encoded/schemas/file.json @@ -337,7 +337,7 @@ "title": "Donor Hardy Scale", "aggregation_type": "stats" }, - "sample_summary.tissues": { + "sample_summary.tissue_short_names": { "title": "Tissue" }, "file_sets.libraries.assay.display_title": { diff --git a/src/encoded/static/components/util/Schemas.js b/src/encoded/static/components/util/Schemas.js index 01011366b..7e919d3ab 100644 --- a/src/encoded/static/components/util/Schemas.js +++ b/src/encoded/static/components/util/Schemas.js @@ -256,6 +256,7 @@ export const Field = { 'file_format.display_title': 'File Format', 'data_category': 'Data Category', 'software.display_title': 'Software', + 'sample_summary.tissue_subtypes': 'Subtype', '@id': 'Link', display_title: 'Title', }, diff --git a/src/encoded/static/components/util/data.js b/src/encoded/static/components/util/data.js index 08cb998e6..6ab4cb08e 100644 --- a/src/encoded/static/components/util/data.js +++ b/src/encoded/static/components/util/data.js @@ -5,38 +5,69 @@ const germLayerTissueMapping = { Ectoderm: { values: [ - 'Brain', - 'Brain - Cerebellum', - 'Brain - Frontal lobe', - 'Brain - Hippocampus', - 'Brain - Temporal lobe', - 'Skin', - 'Skin - Abdomen (non-exposed)', - 'Skin - Calf (sun-exposed)', - 'Non-exposed Skin', - 'Sun-exposed Skin', + "Brain", + "Brain, FL 3AK", + "Brain, TL 3AL", + "Brain, CB 3AM", + "Brain, HL 3AN", + "Brain, HR 3AO", + "Skin", + "Non-exposed Skin", + "Sun-exposed Skin", + "Skin, SE 3AD", + "Skin, NE 3AF", ], }, Mesoderm: { - values: ['Aorta', 'Fibroblast', 'Heart', 'Muscle', 'Adrenal Gland'], + values: [ + "Aorta", + "Aorta 3O", + "Fibroblast", + "Fibroblast 3AC", + "Heart", + "Heart 3S", + "Muscle", + "Muscle 3AH", + "Adrenal Gland", + "Adrenal Gland, L 3K", + "Adrenal Gland, R 3M", + ], }, Endoderm: { values: [ - 'Colon', - 'Colon - Ascending', - 'Colon - Descending', - 'Ascending Colon', - 'Descending Colon', - 'Esophagus', - 'Liver', - 'Lung', + "Colon", + "Colon - Ascending", + "Colon - Descending", + "Ascending Colon", + "Descending Colon", + "Colon, Asc 3E", + "Colon, Desc 3G", + "Esophagus", + "Esophagus 3C", + "Liver", + "Liver 3I", + "Lung", + "Lung 3Q", ], }, 'Germ cells': { - values: ['Ovary', 'Testis'], + values: [ + "Ovary", + "Ovary, L 3Y", + "Ovary, R 3AA", + "Testis", + "Testis, L 3U", + "Testis, R 3W", + ], }, 'Clinically accessible': { - values: ['Blood', 'Buccal swab', 'Buccal Swab'], + values: [ + "Blood", + "Blood 3A", + "Buccal swab", + "Buccal Swab", + "Buccal Swab 3B", + ], }, }; diff --git a/src/encoded/static/components/viz/BarPlot/Chart.js b/src/encoded/static/components/viz/BarPlot/Chart.js index a40d6a173..f3a4be64e 100644 --- a/src/encoded/static/components/viz/BarPlot/Chart.js +++ b/src/encoded/static/components/viz/BarPlot/Chart.js @@ -12,6 +12,22 @@ import { barplot_color_cycler } from './../ColorCycler'; import { RotatedLabel } from './../components'; import { PopoverViewContainer } from './ViewContainer'; +function shiftColor(color, delta = 0.1) { + const parsed = d3.color(color); + if (!parsed) return color; + const mixTarget = delta >= 0 ? 255 : 0; + const amount = Math.min(Math.abs(delta), 1); + parsed.r = Math.round(parsed.r + (mixTarget - parsed.r) * amount); + parsed.g = Math.round(parsed.g + (mixTarget - parsed.g) * amount); + parsed.b = Math.round(parsed.b + (mixTarget - parsed.b) * amount); + return parsed.formatHex(); +} + +function variantFromBaseColor(baseColor, index = 0) { + const deltas = [0, 0.12, -0.08, 0.18, -0.14]; + return shiftColor(baseColor, deltas[index % deltas.length]); +} + /** * Return an object containing bar dimensions for first field which has more than 1 possible term, index of field used, and all fields passed originally. @@ -131,7 +147,25 @@ export function genChartBarDims( _.forEach(barData.bars, function(bar){ if (!Array.isArray(bar.bars)) return; _.forEach(bar.bars, function(b){ - return _.extend(b, { 'color' : barplot_color_cycler.colorForNode(b) }); + const baseColor = barplot_color_cycler.colorForNode(b); + _.extend(b, { 'color': baseColor, 'baseColor': baseColor }); + + if (Array.isArray(b.bars)) { + const sortedGrandChildren = b.bars.slice(0).sort(function(g1, g2){ + const g1Count = typeof g1[aggregateType] === 'number' ? g1[aggregateType] : g1.count || 0; + const g2Count = typeof g2[aggregateType] === 'number' ? g2[aggregateType] : g2.count || 0; + return g1Count - g2Count; + }); + const len = sortedGrandChildren.length; + _.forEach(sortedGrandChildren, function(grandChild, idx) { + // If only one secondary bucket, keep the exact primary color to stay in sync with the legend. + const ratio = len > 1 ? idx / (len - 1) : 0; + const delta = len > 1 ? (0.2 - (0.4 * ratio)) : 0; + const variantColor = len > 1 ? shiftColor(baseColor, delta) : baseColor; + _.extend(grandChild, { 'color': variantColor, 'baseColor': baseColor }); + }); + b.bars = sortedGrandChildren; + } }); }); diff --git a/src/encoded/static/components/viz/BarPlot/UIControlsWrapper.js b/src/encoded/static/components/viz/BarPlot/UIControlsWrapper.js index 960083245..b828ef566 100644 --- a/src/encoded/static/components/viz/BarPlot/UIControlsWrapper.js +++ b/src/encoded/static/components/viz/BarPlot/UIControlsWrapper.js @@ -67,6 +67,9 @@ export class UIControlsWrapper extends React.PureComponent { // { title: 'Data Category', field: 'data_category' }, // { title: 'Software', field: 'software.display_title' }, ], + availableFields_SecondarySubdivision: [ + { title: 'Tissue Subtype', field: 'sample_summary.tissue_subtypes' } + ], legend: false, chartHeight: 300, btnVariant: 'outline-secondary', @@ -93,10 +96,12 @@ export class UIControlsWrapper extends React.PureComponent { this.handleDropDownShowTypeToggle = this.handleDropDownToggle.bind(this, 'showType'); this.handleDropDownSubdivisionFieldToggle = this.handleDropDownToggle.bind(this, 'subdivisionField'); + this.handleDropDownSecondarySubdivisionFieldToggle = this.handleDropDownToggle.bind(this, 'secondarySubdivisionField'); this.handleDropDownYAxisFieldToggle = this.handleDropDownToggle.bind(this, 'yAxis'); this.handleDropDownXAxisFieldToggle = this.handleDropDownToggle.bind(this, 'xAxisField'); this.handleFirstFieldSelect = this.handleFieldSelect.bind(this, 0); this.handleSecondFieldSelect = this.handleFieldSelect.bind(this, 1); + this.handleThirdFieldSelect = this.handleFieldSelect.bind(this, 2); this.handleAggregateTypeSelect = _.throttle(this.handleAggregateTypeSelect.bind(this), 750); this.handleExperimentsShowType = _.throttle(this.handleExperimentsShowType.bind(this), 750, { trailing: false }); @@ -178,53 +183,75 @@ export class UIControlsWrapper extends React.PureComponent { * @param {Event} [event] Reference to event of DropDown change. */ handleFieldSelect(fieldIndex, newFieldKey, event = null) { - const { barplot_data_fields, updateBarPlotFields, availableFields_XAxis, availableFields_Subdivision, mapping } = this.props; - let newFields; + const { + barplot_data_fields = [], + updateBarPlotFields, + availableFields_XAxis, + availableFields_Subdivision, + availableFields_SecondarySubdivision, + mapping + } = this.props; + const currentFields = barplot_data_fields.slice(0); - if (newFieldKey === "none") { - // Only applies to subdivision (fieldIndex 1) - newFields = barplot_data_fields.slice(0, 1); - updateBarPlotFields(mapping, newFields); + if (fieldIndex === 1 && newFieldKey === "none") { + updateBarPlotFields(mapping, currentFields.slice(0, 1).filter(Boolean)); return; } - const propToGetFieldFrom = fieldIndex === 0 ? availableFields_XAxis : availableFields_Subdivision; - const newField = _.find(propToGetFieldFrom, { 'field': newFieldKey }); - const otherFieldIndex = fieldIndex === 0 ? 1 : 0; + if (fieldIndex === 2) { + if (!currentFields[1]) { + // Cannot set a secondary group without a primary group by + return; + } + if (newFieldKey === "none") { + updateBarPlotFields(mapping, currentFields.slice(0, 2).filter(Boolean)); + return; + } + } else if (newFieldKey === "none") { + return; + } - if (fieldIndex === 0 && barplot_data_fields.length === 1) { - newFields = [null]; - } else { - newFields = [null, null]; + let propToGetFieldFrom = availableFields_Subdivision; + if (fieldIndex === 0) { + propToGetFieldFrom = availableFields_XAxis; + } else if (fieldIndex === 2) { + propToGetFieldFrom = availableFields_SecondarySubdivision; } + const newField = _.find(propToGetFieldFrom, { 'field': newFieldKey }) || { 'field': newFieldKey, 'title': newFieldKey }; - newFields[fieldIndex] = newField; - if (newFields.length > 1) { - const foundFieldFromProps = _.findWhere( - availableFields_Subdivision.slice(0).concat(availableFields_XAxis.slice(0)), - { 'field': barplot_data_fields[otherFieldIndex] } - ); - newFields[otherFieldIndex] = foundFieldFromProps || { - 'title': barplot_data_fields[otherFieldIndex], - 'field': barplot_data_fields[otherFieldIndex] - }; + const nextFields = currentFields.slice(0); + while (nextFields.length < fieldIndex) { + nextFields.push(null); + } + nextFields[fieldIndex] = newField.field; + + if (!nextFields[1]) { + nextFields.splice(1); + } else if (fieldIndex === 1 && currentFields.length > 2) { + nextFields[2] = currentFields[2]; } - updateBarPlotFields(mapping, _.pluck(newFields, 'field')); - //analytics + + const cleanedFields = nextFields.filter(Boolean); + updateBarPlotFields(mapping, cleanedFields); analytics.event('cursor_detail', 'BarPlot', 'Set Aggregation Field', null, { - 'name': '[' + _.pluck(newFields, 'field').join(', ') + ']', + 'name': '[' + cleanedFields.join(', ') + ']', 'field_key': newFieldKey, }); } getFieldAtIndex(fieldIndex) { - const { barplot_data_fields, availableFields_XAxis, availableFields_Subdivision } = this.props; + const { barplot_data_fields, availableFields_XAxis, availableFields_Subdivision, availableFields_SecondarySubdivision } = this.props; if (!barplot_data_fields) return null; if (!Array.isArray(barplot_data_fields)) return null; if (barplot_data_fields.length < fieldIndex + 1) return null; return ( - _.findWhere(availableFields_Subdivision.slice(0).concat(availableFields_XAxis.slice(0)), { 'field': barplot_data_fields[fieldIndex] }) + _.findWhere( + availableFields_Subdivision.slice(0) + .concat(availableFields_XAxis.slice(0)) + .concat(availableFields_SecondarySubdivision.slice(0)), + { 'field': barplot_data_fields[fieldIndex] } + ) ) || { 'title': barplot_data_fields[fieldIndex], 'field': barplot_data_fields[fieldIndex] @@ -347,10 +374,58 @@ export class UIControlsWrapper extends React.PureComponent { ); } + renderSecondaryGroupByFieldDropdown() { + const { isLoadingChartData, barplot_data_fields = [], availableFields_SecondarySubdivision, btnVariant, mapping } = this.props; + if (mapping !== 'all' && mapping !== 'file') { + return null; + } + const hasPrimaryGroup = Array.isArray(barplot_data_fields) && barplot_data_fields.length > 1 && !!barplot_data_fields[1]; + const primaryField = hasPrimaryGroup ? barplot_data_fields[1] : null; + let title; + + if (isLoadingChartData) { + title = ; + } else if (!hasPrimaryGroup) { + title = Select primary first; + } else { + const field = this.getFieldAtIndex(2); + if (!field) title = "None"; + else title = field.title || Schemas.Field.toName(field.field); + } + + return ( +