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 ( +
+
Secondary Group By
+ + { + this.renderDropDownMenuItems( + _.map(availableFields_SecondarySubdivision.slice(0).concat([{ + title: None, + field: "none" + }]), function (field) { + const isDisabled = primaryField === field.field; + return [ + field.field, // Field + field.title || Schemas.Field.toName(field.field), // Title + field.description || null, // Description + isDisabled, // Disabled + isDisabled ? "Field already selected for Group By" : null + ]; // key, title, subtitle, disabled + }), + barplot_data_fields[2] || "none" + ) + } + +
+ ); + } + render() { const { barplot_data_filtered, barplot_data_unfiltered, barplot_data_fields, isLoadingChartData, href, btnVariant, - availableFields_XAxis, availableFields_Subdivision, schemas, chartHeight, windowWidth, cursorDetailActions, + availableFields_XAxis, availableFields_Subdivision, availableFields_SecondarySubdivision, schemas, chartHeight, windowWidth, cursorDetailActions, mapping = 'all' } = this.props; const { aggregateType, showState } = this.state; @@ -430,6 +505,7 @@ export class UIControlsWrapper extends React.PureComponent { {/* {this.renderShowTypeDropdown()} */} {this.renderGroupByFieldDropdown()} + {this.renderSecondaryGroupByFieldDropdown()}
0) { + const combinedHeight = _.reduce(siblingSections, function (sum, bar) { + return sum + ((bar && bar.attr && typeof bar.attr.height === 'number') ? bar.attr.height : 0); + }, 0); + if (combinedHeight > 0 && d.attr && typeof d.attr.height === 'number') { + height = (d.attr.height / combinedHeight) * 100 + '%'; + } + } - //new implementation - sum d.parent.bars.count - normalize height since we want to keep the outermost bar height consistent - uozturk - var parentBarsCount = _.reduce(d.parent.bars, function(sum, bar){ - return sum + bar.count; - }, 0); - height = (d.count / parentBarsCount) * 100 + '%'; - // old implementation - // height = (d.count / d.parent.count) * 100 + '%'; + if (!height) { + var parentBarsCount = _.reduce(d.parent.bars, function(sum, bar){ + return sum + bar.count; + }, 0); + height = parentBarsCount ? (d.count / parentBarsCount) * 100 + '%' : '0%'; + } } + // Use the primary grouping term for highlighting so legend hover matches all fragments of a primary bucket, + // but still keep the secondary term available for future use. + const highlightTermKey = ( + d.parent && d.parent.parent ? + d.parent.term : + d.term + ); + const highlightFieldKey = ( + d.parent && d.parent.parent ? + d.parent.field : + d.field + ); + return (
0) { + _.forEach(child.bars, function (grandChild) { + grandChild.baseColor = grandChild.baseColor || child.baseColor || child.color; + sections.push(grandChild); + }); + } else { + sections.push(child); + } + }); + + if (sections.length === 0) { + return [_.extend({}, node, { 'color': node.color || '#5da5da', 'baseColor': node.baseColor || node.color })]; + } + + return sections; + } + renderBarSection(d, i, all){ var { hoverTerm, hoverParentTerm, selectedTerm, selectedParentTerm, onBarPartClick, onBarPartMouseEnter, onBarPartMouseOver, onBarPartMouseLeave, @@ -173,17 +233,15 @@ class Bar extends React.PureComponent { return ( ); } render(){ const { canBeHighlighted, showBarCount, node: d } = this.props; - const hasSubSections = Array.isArray(d.bars); - const barSections = (hasSubSections ? - // If needed, remove sort + reverse to keep order of heaviest->lightest aggs regardless of color - barplot_color_cycler.sortObjectsByColorPalette(d.bars).reverse() : [_.extend({}, d, { color : '#5da5da' })] - ); + const barSections = this.getRenderableSections(); let className = "chart-bar"; const topLabel = showBarCount ? { d.count } : null; @@ -195,7 +253,7 @@ class Bar extends React.PureComponent { className={className} data-term={d.term} data-count={d.count} - data-field={Array.isArray(d.bars) && d.bars.length > 0 ? d.bars[0].field : null} + data-field={d.field || (Array.isArray(d.bars) && d.bars.length > 0 ? d.bars[0].field : null)} key={"bar-" + d.term} style={this.barStyle()} ref={this.barElemRef}> @@ -371,32 +429,49 @@ export class PopoverViewContainer extends React.PureComponent { constructor(props){ super(props); this.getCoordsCallback = this.getCoordsCallback.bind(this); + this.heightToTop = this.heightToTop.bind(this); + this.getRootNode = this.getRootNode.bind(this); + } + + getRootNode(node){ + let current = node; + while (current && current.parent){ + current = current.parent; + } + return current || node; + } + + heightToTop(node){ + if (!node) return 0; + const parent = node.parent; + if (!parent || !Array.isArray(parent.bars)) { + return (node.attr && typeof node.attr.height === 'number') ? node.attr.height : 0; + } + let heightWithinParent = 0; + let found = false; + _.forEach(parent.bars, function(sibling){ + if (found) return; + heightWithinParent += (sibling.attr && typeof sibling.attr.height === 'number') ? sibling.attr.height : 0; + if (sibling === node) { + found = true; + } + }); + + return this.heightToTop(parent) - ((parent.attr && parent.attr.height) || 0) + heightWithinParent; } getCoordsCallback(node, containerPosition, boundsHeight){ var bottomOffset = (this.props && this.props.styleOptions && this.props.styleOptions.offset && this.props.styleOptions.offset.bottom) || 0; var leftOffset = (this.props && this.props.styleOptions && this.props.styleOptions.offset && this.props.styleOptions.offset.left) || 0; - var barYPos = node.attr.height; - - if (node.parent){ - var done = false; - barYPos = _.reduce( - node.parent.bars,//.slice(0).reverse(), - //_.sortBy(node.parent.bars, 'term').reverse(), - function(m, siblingNode){ - if (done) return m; - if (siblingNode.term === node.term){ - done = true; - } - return m + siblingNode.attr.height; - }, - 0 - ); - } + var rootNode = this.getRootNode(node); + const rootAttrs = (rootNode && rootNode.attr) || {}; + const rootX = typeof rootAttrs.x === 'number' ? rootAttrs.x : 0; + const rootWidth = typeof rootAttrs.width === 'number' ? rootAttrs.width : 0; + var barYPos = this.heightToTop(node); return { - 'x' : containerPosition.left + leftOffset + (node.parent || node).attr.x + ((node.parent || node).attr.width / 2), + 'x' : containerPosition.left + leftOffset + rootX + (rootWidth / 2), 'y' : containerPosition.top + boundsHeight - bottomOffset - barYPos, }; } diff --git a/src/encoded/static/components/viz/ChartDetailCursor/ChartDetailCursor.js b/src/encoded/static/components/viz/ChartDetailCursor/ChartDetailCursor.js index 387f4ee27..6c5b2c874 100644 --- a/src/encoded/static/components/viz/ChartDetailCursor/ChartDetailCursor.js +++ b/src/encoded/static/components/viz/ChartDetailCursor/ChartDetailCursor.js @@ -148,7 +148,9 @@ class Body extends React.PureComponent { if (Array.isArray(path) && path.length === 0){ return null; } - const leafNode = path[path.length - 1]; + + // see workaround in Crumbs component why we do this for length 3 + const leafNode = path[path.length === 3 ? 1 : path.length - 1]; const leafNodeFieldTitle = Schemas.Field.toName(leafNode.field, schemas); return ( @@ -198,10 +200,18 @@ const Crumbs = React.memo(function Crumbs({ path, schemas, primaryCount }){ if (isEmpty) return null; //var maxSkewOffset = (this.props.path.length - 2) * offsetPerCrumb; + //workaround: if path has three elemennts, swap the second and the third to fix the order (clone the path first) + const clonedPath = path.slice(); + if (path.length === 3) { + const temp = clonedPath[1]; + clonedPath[1] = clonedPath[2]; + clonedPath[2] = temp; + } + return (
{ - path.slice(0,-1).map(function(n, i){ + clonedPath.slice(0,-1).map(function(n, i){ return (
.label-container { position: relative; diff --git a/src/encoded/types/file.py b/src/encoded/types/file.py index 10a62add1..7457b2273 100644 --- a/src/encoded/types/file.py +++ b/src/encoded/types/file.py @@ -306,6 +306,8 @@ class CalcPropConstants: SAMPLE_SUMMARY_TISSUES = "tissues" SAMPLE_SUMMARY_TISSUE_SUBTYPES = "tissue_subtypes" SAMPLE_SUMMARY_TISSUE_DETAILS = "tissue_details" + SAMPLE_SUMMARY_TISSUE_PROTOCOL_IDS = "tissue_protocol_ids" + SAMPLE_SUMMARY_TISSUE_SHORT_NAMES = "tissue_short_names" SAMPLE_SUMMARY_SAMPLE_NAMES = "sample_names" SAMPLE_SUMMARY_SAMPLE_DESCRIPTIONS = "sample_descriptions" SAMPLE_SUMMARY_ANALYTES = "analytes" @@ -349,6 +351,20 @@ class CalcPropConstants: "type": "string", }, }, + SAMPLE_SUMMARY_TISSUE_PROTOCOL_IDS: { + "title": "Tissue Protocol ID", + "type": "array", + "items": { + "type": "string", + }, + }, + SAMPLE_SUMMARY_TISSUE_SHORT_NAMES: { + "title": "Tissue Short Name", + "type": "array", + "items": { + "type": "string", + }, + }, SAMPLE_SUMMARY_SAMPLE_NAMES: { "title": "Sample ID", "type": "array", @@ -1232,6 +1248,8 @@ def _get_sample_summary_fields( file_utils.get_tissues(file_properties, request_handler), tissue_utils.get_location, ), + constants.SAMPLE_SUMMARY_TISSUE_PROTOCOL_IDS: file_utils.get_tissue_protocol_id(file_properties, request_handler), + constants.SAMPLE_SUMMARY_TISSUE_SHORT_NAMES: file_utils.get_tissue_short_name(file_properties, request_handler), constants.SAMPLE_SUMMARY_SAMPLE_NAMES: get_property_values_from_identifiers( request_handler, file_utils.get_samples(file_properties, request_handler), @@ -1261,6 +1279,7 @@ def _get_sample_summary_fields( analyte_utils.get_molecule, ), } + return {key: value for key, value in to_include.items() if value} def _get_analysis_summary( diff --git a/src/encoded/types/tissue.py b/src/encoded/types/tissue.py index a99e7c740..bd491e532 100644 --- a/src/encoded/types/tissue.py +++ b/src/encoded/types/tissue.py @@ -85,6 +85,18 @@ def tissue_type(self, request: Request): request_handler = RequestHandler(request=request) tissue_type = tissue_utils.get_tissue_type(self.properties, request_handler=request_handler) return tissue_type or None + + @calculated_property( + schema={ + "title": "Protocol ID", + "description": "Protocol ID associated with tissue", + "type": "string" + } + ) + def protocol_id(self, request: Request) -> Optional[str]: + """Get protocol ID associated with tissue.""" + protocol_id = tissue_utils.get_protocol_id(self.properties) + return protocol_id or None @link_related_validator